class FireSpell(Spell): """Огненное заклинание Обычное Эффективно против: Зомби, Древних магов Неэффективно против: Демонов, Огненных магов""" damage = 50 spell_type = Spell.FIRE mana_cost = 60 UPDATE_TIME = 40 speed = TILE_SIZE * 0.22 acceleration = 2 action_time = 0 size = (TILE_SIZE // 4 * 3, ) * 2 frames = cut_sheet(load_image("assets/sprites/spells/fire_laser.png"), 6, 1, size) frames += cut_sheet(load_image("assets/sprites/spells/fire_explosion.png"), 7, 9, size) # Канал для звуков sounds_channel = pygame.mixer.Channel(3) # Звуки CAST_SOUND = load_sound("assets/audio/sfx/spells/cast_sound_2.ogg") SPELL_SOUNDS = ( load_sound("assets/audio/sfx/spells/spell_sound_1.ogg"), load_sound("assets/audio/sfx/spells/spell_sound_3.ogg"), load_sound("assets/audio/sfx/spells/spell_sound_4.ogg"), load_sound("assets/audio/sfx/spells/spell_sound_5.ogg"), load_sound("assets/audio/sfx/spells/spell_sound_14.ogg"), load_sound("assets/audio/sfx/spells/spell_sound_24.ogg"), )
class TeleportSpell(Spell): spell_type = Spell.TELEPORT mana_cost = 300 UPDATE_TIME = 40 speed = TILE_SIZE * 1 acceleration = 0 damage_frame = 2 action_time = 0 size = (TILE_SIZE // 4 * 7,) * 2 frames = cut_sheet(load_image('EMPTY.png', 'assets\\tiles'), 1, 1, size) frames += cut_sheet(load_image('teleport_puf.png', 'assets\\spells'), 8, 1, size) # Канал для звуков sounds_channel = pygame.mixer.Channel(3) # Звуки CAST_SOUND = pygame.mixer.Sound(concat_two_file_paths("assets/spells/audio", "teleport_sound.ogg")) CAST_SOUND.set_volume(DEFAULT_SOUNDS_VOLUME) SPELL_SOUNDS = ( pygame.mixer.Sound(concat_two_file_paths("assets/spells/audio", "teleport_sound.ogg")), ) for sound in SPELL_SOUNDS: sound.set_volume(DEFAULT_SOUNDS_VOLUME) def __init__(self, subject_x: float, subject_y: float, object_x: float, object_y: float, object_group, *groups): super().__init__(subject_x, subject_y, object_x, object_y, object_group, *groups) self.start_sprite = pygame.sprite.Sprite() self.start_sprite.start_position = None self.start_sprite.point = None self.start_sprite.image = TeleportSpell.frames[0][0] self.start_sprite.rect = self.start_sprite.image.get_rect() self.start_sprite.rect.center = object_group[0].rect.center
class FireSpell(Spell): damage = 50 spell_type = Spell.FIRE mana_cost = 50 UPDATE_TIME = 40 speed = TILE_SIZE * 0.22 acceleration = 2 action_time = 0 size = (TILE_SIZE // 4 * 3,) * 2 frames = cut_sheet(load_image('fire_laser.png', 'assets\\spells'), 6, 1, size) frames += cut_sheet(load_image('fire_explosion.png', 'assets\\spells'), 7, 8, size) # Канал для звуков sounds_channel = pygame.mixer.Channel(3) # Звуки CAST_SOUND = pygame.mixer.Sound(concat_two_file_paths("assets/spells/audio", "cast_sound_2.ogg")) CAST_SOUND.set_volume(DEFAULT_SOUNDS_VOLUME) SPELL_SOUNDS = ( pygame.mixer.Sound(concat_two_file_paths("assets/spells/audio", "spell_sound_1.ogg")), pygame.mixer.Sound(concat_two_file_paths("assets/spells/audio", "spell_sound_3.ogg")), pygame.mixer.Sound(concat_two_file_paths("assets/spells/audio", "spell_sound_4.ogg")), pygame.mixer.Sound(concat_two_file_paths("assets/spells/audio", "spell_sound_5.ogg")), pygame.mixer.Sound(concat_two_file_paths("assets/spells/audio", "spell_sound_14.ogg")), pygame.mixer.Sound(concat_two_file_paths("assets/spells/audio", "spell_sound_24.ogg")), ) for sound in SPELL_SOUNDS: sound.set_volume(DEFAULT_SOUNDS_VOLUME) def __init__(self, subject_x: float, subject_y: float, object_x: float, object_y: float, object_group, *groups): super().__init__(subject_x, subject_y, object_x, object_y, object_group, *groups)
class FlashSpell(Spell): damage = 50 spell_type = Spell.FLASH mana_cost = 200 UPDATE_TIME = 60 speed = TILE_SIZE * 0.5 acceleration = 1 damage_frame = 5 action_time = 0 size = (TILE_SIZE // 2 * 3, ) * 2 frames = cut_sheet(load_image('EMPTY.png', 'assets\\tiles'), 1, 1, size) frames += cut_sheet(load_image('light.png', 'assets\\spells'), 15, 1, size) # Канал для звуков sounds_channel = pygame.mixer.Channel(3) # Звуки CAST_SOUND = pygame.mixer.Sound( concat_two_file_paths("assets/spells/audio", "cast_sound_4.ogg")) CAST_SOUND.set_volume(DEFAULT_SOUNDS_VOLUME) SPELL_SOUNDS = ( pygame.mixer.Sound( concat_two_file_paths("assets/spells/audio", "spell_sound_3.ogg")), pygame.mixer.Sound( concat_two_file_paths("assets/spells/audio", "spell_sound_4.ogg")), pygame.mixer.Sound( concat_two_file_paths("assets/spells/audio", "spell_sound_5.ogg")), pygame.mixer.Sound( concat_two_file_paths("assets/spells/audio", "spell_sound_6.ogg")), pygame.mixer.Sound( concat_two_file_paths("assets/spells/audio", "spell_sound_7.ogg")), pygame.mixer.Sound( concat_two_file_paths("assets/spells/audio", "spell_sound_8.ogg")), pygame.mixer.Sound( concat_two_file_paths("assets/spells/audio", "spell_sound_9.ogg")), pygame.mixer.Sound( concat_two_file_paths("assets/spells/audio", "spell_sound_10.ogg")), pygame.mixer.Sound( concat_two_file_paths("assets/spells/audio", "spell_sound_11.ogg")), pygame.mixer.Sound( concat_two_file_paths("assets/spells/audio", "spell_sound_14.ogg")), pygame.mixer.Sound( concat_two_file_paths("assets/spells/audio", "spell_sound_26.ogg")), ) for sound in SPELL_SOUNDS: sound.set_volume(DEFAULT_SOUNDS_VOLUME) def __init__(self, subject_x: float, subject_y: float, object_x: float, object_y: float, object_group, *groups): super().__init__(subject_x, subject_y, object_x, object_y, object_group, *groups)
class VoidSpell(Spell): damage = 70 spell_type = Spell.VOID mana_cost = 120 speed = TILE_SIZE * 0.24 acceleration = 3 UPDATE_TIME = 40 damage_frame = 5 action_time = 0 size = (TILE_SIZE * 3, ) * 2 frames = cut_sheet(load_image('void_laser.png', 'assets\\spells'), 10, 1) frames += cut_sheet(load_image('void_explosion.png', 'assets\\spells'), 12, 2, size) frames += cut_sheet(load_image('void_explosions.png', 'assets\\spells'), 10, 5, size) # Канал для звуков sounds_channel = pygame.mixer.Channel(3) # Звуки CAST_SOUND = pygame.mixer.Sound( concat_two_file_paths("assets/spells/audio", "cast_sound_5.ogg")) CAST_SOUND.set_volume(DEFAULT_SOUNDS_VOLUME) SPELL_SOUNDS = ( pygame.mixer.Sound( concat_two_file_paths("assets/spells/audio", "spell_sound_2.ogg")), pygame.mixer.Sound( concat_two_file_paths("assets/spells/audio", "spell_sound_5.ogg")), pygame.mixer.Sound( concat_two_file_paths("assets/spells/audio", "spell_sound_14.ogg")), pygame.mixer.Sound( concat_two_file_paths("assets/spells/audio", "spell_sound_15.ogg")), pygame.mixer.Sound( concat_two_file_paths("assets/spells/audio", "spell_sound_16.ogg")), pygame.mixer.Sound( concat_two_file_paths("assets/spells/audio", "spell_sound_17.ogg")), pygame.mixer.Sound( concat_two_file_paths("assets/spells/audio", "spell_sound_18.ogg")), pygame.mixer.Sound( concat_two_file_paths("assets/spells/audio", "spell_sound_25.ogg")), ) for sound in SPELL_SOUNDS: sound.set_volume(DEFAULT_SOUNDS_VOLUME) def __init__(self, subject_x: float, subject_y: float, object_x: float, object_y: float, object_group, *groups): super().__init__(subject_x, subject_y, object_x, object_y, object_group, *groups)
class Demon(WalkingMonster): """ Демон Мало жизней Быстрый Больно бьёт Устойчивойть к огню Слабость к льду """ damage = 50 size = (int(TILE_SIZE // 8 * 5), ) * 2 frames = cut_sheet(load_image('demon_run.png', 'assets\\enemies'), 4, 2, size) frames += cut_sheet(load_image('demon_idle.png', 'assets\\enemies'), 4, 2, size) death_frames = cut_sheet(load_image('demon_dying.png', 'assets\\enemies'), 16, 1)[0] UPDATE_TIME = 60 default_speed = TILE_SIZE * 0.023 look_directions = { (-1, -1): 1, (-1, 0): 1, (-1, 1): 1, (0, -1): 1, (0, 0): 0, (0, 1): 0, (1, -1): 0, (1, 0): 0, (1, 1): 0 } # Канал для звуков sounds_channel = pygame.mixer.Channel(3) # Звуки FOOTSTEP_SOUND = pygame.mixer.Sound( concat_two_file_paths("assets/enemies/audio", "little_steps.mp3")) FOOTSTEP_SOUND.set_volume(DEFAULT_SOUNDS_VOLUME) def __init__(self, x, y, *args): super().__init__(x, y, *args) self.alive = True self.visibility_range = TILE_SIZE * 7 self.health = 40 self.full_health = self.health
class LongWizard(ShootingMonster): """ Большой маг Подвижный Среднее количество жизней Большой урон Устойчивость к молниям Слабость к огню """ damage = 20 size = (int(TILE_SIZE // 8 * 7), ) * 2 frames = cut_sheet(load_image('long_wizard_run.png', 'assets\\enemies'), 4, 2) frames += cut_sheet(load_image('long_wizard_idle.png', 'assets\\enemies'), 4, 2) death_frames = cut_sheet( load_image('long_wizard_dying.png', 'assets\\enemies'), 16, 1)[0] default_speed = TILE_SIZE * 0.02 look_directions = { (-1, -1): 1, (-1, 0): 1, (-1, 1): 1, (0, -1): 1, (0, 0): 0, (0, 1): 0, (1, -1): 0, (1, 0): 0, (1, 1): 0 } # Канал для звуков sounds_channel = pygame.mixer.Channel(2) # Звуки FOOTSTEP_SOUND = pygame.mixer.Sound( concat_two_file_paths("assets/enemies/audio", "wizard_rustle.mp3")) FOOTSTEP_SOUND.set_volume(DEFAULT_SOUNDS_VOLUME) def __init__(self, x, y, *args): super().__init__(x, y, *args) self.visibility_range = TILE_SIZE * 13 self.health = 80 self.full_health = self.health self.reload_time = self.reload_time * 4 / 3
class DirtySlime(WalkingMonster): """ Грязный слизень Я как Зеленый, но чуть крепче Медленный Много жизней Не очень большой урон Устойчивость к льду и отравлению Слабость к молниям """ damage = 70 size = (int(TILE_SIZE // 8 * 7), ) * 2 frames = cut_sheet(load_image('dirty_slime_any.png', 'assets\\enemies'), 4, 2) frames += cut_sheet(load_image('dirty_slime_any.png', 'assets\\enemies'), 4, 2) death_frames = cut_sheet( load_image('dirty_slime_dying.png', 'assets\\enemies'), 16, 1)[0] default_speed = TILE_SIZE * 0.02 look_directions = { (-1, -1): 1, (-1, 0): 1, (-1, 1): 1, (0, -1): 1, (0, 0): 0, (0, 1): 0, (1, -1): 0, (1, 0): 0, (1, 1): 0 } # Канал для звуков sounds_channel = pygame.mixer.Channel(4) # Звуки FOOTSTEP_SOUND = pygame.mixer.Sound( concat_two_file_paths("assets/enemies/audio", "slime_sound_1.ogg")) FOOTSTEP_SOUND.set_volume(DEFAULT_SOUNDS_VOLUME) def __init__(self, x, y, *args): super().__init__(x, y, *args) self.alive = True self.visibility_range = TILE_SIZE * 5 self.health = 100 self.full_health = self.health
class Wizard(ShootingMonster): """ Маг Подвижный Маловато жизней Средний урон Устойчивость к молниям Слабость к огню (МОЙ ПЛАЩ ГОРИТ) """ damage = 10 size = (TILE_SIZE // 8 * 7, ) * 2 frames = cut_sheet(load_image('wizard_run.png', 'assets\\enemies'), 4, 2, size) frames += cut_sheet(load_image('wizard_idle.png', 'assets\\enemies'), 4, 2, size) death_frames = cut_sheet(load_image('wizard_dying.png', 'assets\\enemies'), 16, 1)[0] default_speed = TILE_SIZE * 0.022 look_directions = { (-1, -1): 1, (-1, 0): 1, (-1, 1): 1, (0, -1): 1, (0, 0): 0, (0, 1): 0, (1, -1): 0, (1, 0): 0, (1, 1): 0 } # Канал для звуков sounds_channel = pygame.mixer.Channel(3) # Звуки FOOTSTEP_SOUND = pygame.mixer.Sound( concat_two_file_paths("assets/enemies/audio", "wizard_rustle.mp3")) FOOTSTEP_SOUND.set_volume(DEFAULT_SOUNDS_VOLUME) def __init__(self, x, y, *args): super().__init__(x, y, *args) self.alive = True self.visibility_range = TILE_SIZE * 9 self.health = 60 self.full_health = self.health
class Zombie(WalkingMonster): """ Зомби Не медленный, но и не быстрый Среднее количество жизней Средний урон Устойчивость к молниям (они двигают мои нейроны) Слабостей не обнаружено (земля пухом ученым) """ damage = 30 size = (int(TILE_SIZE // 4 * 3), ) * 2 frames = cut_sheet(load_image('zombie_run.png', 'assets\\enemies'), 4, 2) frames += cut_sheet(load_image('zombie_idle.png', 'assets\\enemies'), 4, 2) death_frames = cut_sheet(load_image('zombie_dying.png', 'assets\\enemies'), 16, 1)[0] default_speed = TILE_SIZE * 0.02 look_directions = { (-1, -1): 1, (-1, 0): 1, (-1, 1): 1, (0, -1): 1, (0, 0): 0, (0, 1): 0, (1, -1): 0, (1, 0): 0, (1, 1): 0 } # Канал для звуков sounds_channel = pygame.mixer.Channel(3) # Звуки FOOTSTEP_SOUND = pygame.mixer.Sound( concat_two_file_paths("assets/enemies/audio", "stone_steps_1.mp3")) FOOTSTEP_SOUND.set_volume(DEFAULT_SOUNDS_VOLUME) def __init__(self, x, y, *args): super().__init__(x, y, *args) self.alive = True self.visibility_range = TILE_SIZE * 6 self.health = 80 self.full_health = self.health
class Chest(pygame.sprite.Sprite): size = (TILE_SIZE, TILE_SIZE) frames = cut_sheet(load_image("assets/sprites/tiles/CHEST.png"), 8, 1, size)[0] back_of_chest = load_tile('back_of_chest.png') chest_group: pygame.sprite.Group UPDATE_TIME = 40 def __init__(self, x, y, new_boxes_seed, index, *args): super().__init__(Chest.chest_group, *args) # Сохраняем индекс и сид, чтоб потом убрать из сида сундук, как открытый self.seed = new_boxes_seed self.index = index self.opened = False self.last_update_time = 0 self.current_frame = 0 self.update_time = self.UPDATE_TIME self.image = self.frames[self.current_frame] self.back_image = self.back_of_chest.copy() self.rect = self.image.get_rect().move(x * TILE_SIZE, y * TILE_SIZE) self.back_image_rect = self.back_image.get_rect() self.collider = Collider(x, y) def draw_back_image(self, screen): screen.blit(self.back_image, self.rect) def open(self): self.opened = True def update(self): # Если сундук не открыт, с ним ничего не происходит if not self.opened: return ticks = pygame.time.get_ticks() if ticks - self.last_update_time > self.update_time: self.last_update_time = ticks # Чтоб анимация замедлялась self.update_time *= 1.2 # Если первый фрейм, убираем из сида и спавним предмет if self.current_frame == 0: spawn_item(*self.rect.center, all_sprites=Entity.all_sprites, k=5) self.seed[self.index] = '0' # Если изображения кончились, убиваем if self.current_frame >= len(self.frames): self.kill() return # Меняем изображение self.image = self.frames[self.current_frame] self.current_frame += 1
class PoisonSpell(Spell): """Заклинание отравления Отравляет противника, постепенно нанося урон Эффективно против: Огненных магов Неэффективно против: Зеленых слизеней""" spell_type = Spell.POISON damage = 10 action_time = 10 extra_damage = Entity.POISON_DAMAGE * action_time mana_cost = 50 UPDATE_TIME = 40 speed = TILE_SIZE * 0.2 acceleration = 0.5 size = (TILE_SIZE // 4 * 3, ) * 2 frames = cut_sheet(load_image("assets/sprites/spells/poison_laser.png"), 7, 1, size) frames += cut_sheet( load_image("assets/sprites/spells/poison_explosion.png"), 11, 1, size) frames += cut_sheet( load_image("assets/sprites/spells/poison_explosion_1.png"), 25, 1, size) # Канал для звуков sounds_channel = pygame.mixer.Channel(3) # Звуки CAST_SOUND = load_sound("assets/audio/sfx/spells/cast_sound_3.ogg") SPELL_SOUNDS = ( load_sound("assets/audio/sfx/spells/spell_sound_1.ogg"), load_sound("assets/audio/sfx/spells/spell_sound_12.ogg"), load_sound("assets/audio/sfx/spells/spell_sound_13.ogg"), load_sound("assets/audio/sfx/spells/spell_sound_17.ogg"), load_sound("assets/audio/sfx/spells/spell_sound_23.ogg"), load_sound("assets/audio/sfx/spells/spell_sound_24.ogg"), load_sound("assets/audio/sfx/spells/spell_sound_26.ogg"), load_sound("assets/audio/sfx/spells/spell_sound_27.ogg"), )
class FlashSpell(Spell): """Заклинание молнии Бьёт через стены Эффективно против: Зеленых слизеней Неэффективно против: Зомби""" damage = 50 spell_type = Spell.FLASH mana_cost = 150 UPDATE_TIME = 60 speed = TILE_SIZE * 3 acceleration = 1 damage_frame = 2 action_time = 0 size = (TILE_SIZE // 2 * 2, TILE_SIZE // 2 * 5) frames = cut_sheet(load_image("assets/sprites/tiles/EMPTY.png"), 1, 1, size) frames += cut_sheet(load_image("assets/sprites/spells/light.png"), 15, 1, size) # Канал для звуков sounds_channel = pygame.mixer.Channel(3) # Звуки CAST_SOUND = load_sound("assets/audio/sfx/spells/cast_sound_4.ogg") SPELL_SOUNDS = ( load_sound("assets/audio/sfx/spells/spell_sound_3.ogg"), load_sound("assets/audio/sfx/spells/spell_sound_4.ogg"), load_sound("assets/audio/sfx/spells/spell_sound_5.ogg"), load_sound("assets/audio/sfx/spells/spell_sound_6.ogg"), load_sound("assets/audio/sfx/spells/spell_sound_7.ogg"), load_sound("assets/audio/sfx/spells/spell_sound_8.ogg"), load_sound("assets/audio/sfx/spells/spell_sound_9.ogg"), load_sound("assets/audio/sfx/spells/spell_sound_10.ogg"), load_sound("assets/audio/sfx/spells/spell_sound_11.ogg"), load_sound("assets/audio/sfx/spells/spell_sound_14.ogg"), load_sound("assets/audio/sfx/spells/spell_sound_26.ogg"), )
class VoidSpell(Spell): """Заклинание пустоты Урон по площади Эффективно против: Грязных слизеней Неэффективно против: Древних магов""" damage = 60 spell_type = Spell.VOID mana_cost = 100 speed = TILE_SIZE * 0.24 acceleration = 3 UPDATE_TIME = 40 damage_frame = 2 action_time = 0 size = (TILE_SIZE * 3, ) * 2 frames = cut_sheet(load_image("assets/sprites/spells/void_laser.png"), 10, 1) frames += cut_sheet(load_image("assets/sprites/spells/void_explosion.png"), 12, 2, size) frames += cut_sheet( load_image("assets/sprites/spells/void_explosions.png"), 10, 5, size) # Канал для звуков sounds_channel = pygame.mixer.Channel(3) # Звуки CAST_SOUND = load_sound("assets/audio/sfx/spells/cast_sound_5.ogg") SPELL_SOUNDS = ( load_sound("assets/audio/sfx/spells/spell_sound_2.ogg"), load_sound("assets/audio/sfx/spells/spell_sound_5.ogg"), load_sound("assets/audio/sfx/spells/spell_sound_14.ogg"), load_sound("assets/audio/sfx/spells/spell_sound_15.ogg"), load_sound("assets/audio/sfx/spells/spell_sound_16.ogg"), load_sound("assets/audio/sfx/spells/spell_sound_17.ogg"), load_sound("assets/audio/sfx/spells/spell_sound_18.ogg"), load_sound("assets/audio/sfx/spells/spell_sound_25.ogg"), )
class IceSpell(Spell): """Ледяное заклинание На время замедляет противника (включая перезарядку заклинаний) Эффективно против: Демонов Неэффективно против: Грязных слизней""" damage = 25 spell_type = Spell.ICE mana_cost = 40 UPDATE_TIME = 40 speed = TILE_SIZE * 0.3 acceleration = 4 action_time = 500 size = (TILE_SIZE // 4 * 3, ) * 2 frames = cut_sheet(load_image("assets/sprites/spells/ice_laser.png"), 30, 1, size) frames += cut_sheet(load_image("assets/sprites/spells/ice_explosion.png"), 28, 1, size) # Канал для звуков sounds_channel = pygame.mixer.Channel(3) # Звуки CAST_SOUND = load_sound("assets/audio/sfx/spells/cast_sound_1.ogg") SPELL_SOUNDS = ( load_sound("assets/audio/sfx/spells/spell_sound_2.ogg"), load_sound("assets/audio/sfx/spells/spell_sound_12.ogg"), load_sound("assets/audio/sfx/spells/spell_sound_18.ogg"), load_sound("assets/audio/sfx/spells/spell_sound_19.ogg"), load_sound("assets/audio/sfx/spells/spell_sound_20.ogg"), load_sound("assets/audio/sfx/spells/spell_sound_21.ogg"), load_sound("assets/audio/sfx/spells/spell_sound_22.ogg"), load_sound("assets/audio/sfx/spells/spell_sound_26.ogg"), )
class TeleportSpell(Spell): """Заклинание телепортации Переносит игрока в позицию прицела""" spell_type = Spell.TELEPORT damage = "---" mana_cost = 250 UPDATE_TIME = 40 speed = TILE_SIZE * 3 acceleration = 0 damage_frame = 2 action_time = 0 size = (TILE_SIZE // 4 * 7, ) * 2 frames = cut_sheet(load_image("assets/sprites/tiles/EMPTY.png"), 1, 1, size) frames += cut_sheet(load_image("assets/sprites/spells/teleport_puf.png"), 8, 1, size) # Канал для звуков sounds_channel = pygame.mixer.Channel(3) # Звуки CAST_SOUND = load_sound("assets/audio/sfx/spells/teleport_sound.ogg") SPELL_SOUNDS = (load_sound("assets/audio/sfx/spells/teleport_sound.ogg"), ) def __init__(self, subject_x: float, subject_y: float, object_x: float, object_y: float, extra_damage: float, object_group, *groups): super().__init__(subject_x, subject_y, object_x, object_y, extra_damage, object_group, *groups) self.start_sprite = pygame.sprite.Sprite() self.start_sprite.start_position = None self.start_sprite.point = None self.start_sprite.image = TeleportSpell.frames[0][0] self.start_sprite.rect = self.start_sprite.image.get_rect() self.start_sprite.rect.center = object_group[0].rect.center
class Torch(pygame.sprite.Sprite): frames = cut_sheet(load_image("assets/sprites/tiles/TORCH.png"), 8, 1, (round(TILE_SIZE / 4 * 3), ) * 2)[0] # Канал для звуков sounds_channel = pygame.mixer.Channel(0) min_distance_to_player = 100 update_sounds_channel = 0 # Звуки BURNING_SOUND = load_sound("assets/audio/sfx/world/torch_sound.mp3") BURNING_SOUND.set_volume(DEFAULT_SOUNDS_VOLUME) def __init__(self, x: float, y: float, *groups): super().__init__(*groups) self.image = Torch.frames[randint(0, len(Torch.frames) - 1)] self.rect = self.image.get_rect().move(x * TILE_SIZE, y * TILE_SIZE) self.cur_frame = 0 self.update_time = pygame.time.get_ticks() def update(self, player=None) -> None: ticks = pygame.time.get_ticks() # Через каждые назначенные промежутки времени обновляем минимальное расстояние до игрока if ticks - Torch.update_sounds_channel > 100: Torch.update_sounds_channel = ticks + randint(-20, 20) Torch.min_distance_to_player = 100 return if ticks - self.update_time > 100: self.update_time = pygame.time.get_ticks() while 1: n = randint(0, len(self.frames) - 1) if n != self.cur_frame: self.cur_frame = n break self.image = Torch.frames[self.cur_frame] # Вычисляем расстояние до игрока и ставим с этим коэффиццентом громкость звука # Так он будет меняться, когда мы подходим к факелам и отходим distance_to_player = dist(player.rect.center, self.rect.center) Torch.min_distance_to_player = min(max(distance_to_player, 0.000001), Torch.min_distance_to_player) self.BURNING_SOUND.set_volume( min( DEFAULT_SOUNDS_VOLUME / (Torch.min_distance_to_player / TILE_SIZE) * 3, 1.2)) if not self.sounds_channel.get_busy(): self.sounds_channel.play(self.BURNING_SOUND)
class Torch(pygame.sprite.Sprite): frames = cut_sheet(load_image('TORCH.png', 'assets\\tiles'), 8, 1, (round(TILE_SIZE / 4 * 3), ) * 2) # Канал для звуков sounds_channel = pygame.mixer.Channel(0) min_distance_to_player = 100 # Звуки BURNING_SOUND = pygame.mixer.Sound( concat_two_file_paths("assets/audio", "torch_sound.mp3")) BURNING_SOUND.set_volume(DEFAULT_SOUNDS_VOLUME - SOUNDS_VOLUME_REDUCER) def __init__(self, x: float, y: float, *groups): super().__init__(*groups) self.image = Torch.frames[0][randint(0, len(Torch.frames[0]) - 1)] self.rect = self.image.get_rect().move(x * TILE_SIZE, y * TILE_SIZE) self.cur_frame = 0 self.update_time = pygame.time.get_ticks() def update(self, player=None) -> None: if not player: Torch.min_distance_to_player = 100000 return if pygame.time.get_ticks() - self.update_time > 100 + randint(-20, 20): self.update_time = pygame.time.get_ticks() while 1: n = randint(0, 7) if n != self.cur_frame: self.cur_frame = n break self.image = Torch.frames[0][self.cur_frame] dx, dy = player.rect.centerx - self.rect.centerx, player.rect.centery - self.rect.centery Torch.min_distance_to_player = min(max((dx**2 + dy**2)**0.5, 0.000001), Torch.min_distance_to_player) self.BURNING_SOUND.set_volume( min( DEFAULT_SOUNDS_VOLUME / (Torch.min_distance_to_player / TILE_SIZE) * 1, 1.2)) if not self.sounds_channel.get_busy(): self.sounds_channel.play(self.BURNING_SOUND)
class Entity(pygame.sprite.Sprite): """ Класс, отвечающий за предстовление базовой сущности в игре """ # Группа со спрайтами, которые считаются физическими объектами # общими для всех сущностей. collisions_group: pygame.sprite.Group WAITING_TIME = 2000 UPDATE_TIME = 120 HEALTH_LINE_WIDTH = 5 HEALTH_LINE_TIME = 5000 POISON_DAMAGE = 0.075 size = (int(TILE_SIZE), ) * 2 sleeping_frames = cut_sheet( load_image('sleep_icon_spritesheet.png', 'assets\\enemies'), 4, 1) poison_frames = cut_sheet( load_image('poison_static.png', 'assets\\spells'), 5, 1, size)[0] def __init__(self, x: float, y: float, *args): # Конструктор класса Sprite super().__init__(*args) # Изображение self.cur_frame = 0 self.image = self.__class__.frames[0][self.cur_frame] self.last_update = pygame.time.get_ticks() self.width, self.height = self.image.get_size() self.last_damage_time = -Entity.HEALTH_LINE_TIME self.sleeping_time = None self.cur_poison_frame = 0 self.poison_static_time = 0 self.ice_buff = 0 self.poison_buff = 0 self.start_position = x, y self.point = None self.rect = self.image.get_rect() self.rect.centerx = x self.rect.centery = y self.collider = Collider(self.rect.centerx, self.rect.centery) # Скорость self.dx = self.dy = 0 # Направление взгляда self.look_direction_x = 0 self.look_direction_y = 1 def update(self) -> None: if self.ice_buff: self.ice_buff -= 1 self.speed = self.__class__.default_speed * 0.3 else: self.speed = self.__class__.default_speed if self.poison_buff: self.poison_buff -= 1 self.get_damage(Entity.POISON_DAMAGE) def move(self, dx, dy): """ Метод передвижения Сдвинется на указанные параметры, если там свободно :param dx: Изменение координаты по Х :param dy: Изменение координаты по Y :return: None """ if not self.alive: return # Запоминаем координаты pos = self.rect.x, self.rect.y self.rect.x = round(self.rect.x + dx) self.collider.update(self.rect.centerx, self.rect.centery) # Если плохо, возвращаем к исходному if pygame.sprite.spritecollide(self.collider, Entity.collisions_group, False): self.rect.x = pos[0] self.dx = 0 if self.__class__.__name__.lower() == "player": self.dash_force_x = 0 self.dash_direction_x = self.look_direction_x self.dash_force_y = 0 self.dash_direction_y = self.look_direction_y self.rect.y = round(self.rect.y + dy) self.collider.update(self.rect.centerx, self.rect.centery) # Если плохо, возвращаем к исходному if pygame.sprite.spritecollide(self.collider, Entity.collisions_group, False): self.rect.y = pos[1] self.dy = 0 if self.__class__.__name__.lower() == "player": self.dash_force_x = 0 self.dash_direction_x = self.look_direction_x self.dash_force_y = 0 self.dash_direction_y = self.look_direction_y def update_frame_state(self, n=0): """ Воспроизводит звук и сменяет кадр анимации :param n: если есть, сдвигает номер анимации (стояние вместо движения) :return: None """ tick = pygame.time.get_ticks() if tick - self.last_update > self.UPDATE_TIME: self.last_update = tick if not self.alive: self.cur_frame = self.cur_frame + 1 if self.cur_frame >= len(self.__class__.death_frames) - 1: for group in self.groups(): group.remove(self) try: self.image = self.__class__.death_frames[self.cur_frame] except IndexError: pass return look = self.__class__.look_directions[self.look_direction_x, self.look_direction_y] look += n self.cur_frame = (self.cur_frame + 1) % len( self.__class__.frames[look]) self.image = self.__class__.frames[look][self.cur_frame] if (self.__class__.__name__ != 'Player' and DEFAULT_SOUNDS_VOLUME * 200 / self.distance_to_player > 0.1 and (look < 2 or 'Slime' in self.__class__.__name__ or 'Demon' in self.__class__.__name__) and not self.sounds_channel.get_busy()): self.FOOTSTEP_SOUND.set_volume( min( DEFAULT_SOUNDS_VOLUME / (self.distance_to_player / TILE_SIZE) * 3, 1)) self.sounds_channel.play(self.FOOTSTEP_SOUND) def draw_health_bar(self, screen): """ Функция отрисовки полоски здоровья :param screen: Экран отрисовки :return: None """ if abs(pygame.time.get_ticks() - self.last_damage_time) > Entity.HEALTH_LINE_TIME: return line_width = Entity.HEALTH_LINE_WIDTH x, y = self.rect.centerx, self.rect.centery width, height = self.rect.size x1, y1 = x - width * 0.5, y - height * 0.5 pygame.draw.rect(screen, 'grey', (x1 - 1, y1 - 10 - 1, width + 2, line_width + 2)) health_length = width * max(self.health, 0) / self.full_health color = '#00b300' if str( self.__class__.__name__) == 'Player' else 'red' pygame.draw.rect(screen, color, (x1, y1 - 10, health_length, line_width)) def draw_sign(self, screen): """ Отрисовка знака сна (Z-Z-Z) или восклицательного знака. :param screen: Экран отрисовки :return: None """ if not self.alive: return ticks = pygame.time.get_ticks() if self.poison_buff: if ticks - self.poison_static_time > Entity.UPDATE_TIME: self.poison_static_time = ticks self.cur_poison_frame = (self.cur_poison_frame + 1) % len( Entity.poison_frames) screen.blit(Entity.poison_frames[self.cur_poison_frame], (self.rect.x, self.rect.y)) if self.player_observed: font = pygame.font.Font("assets\\UI\\pixel_font.ttf", 96) text = font.render("!", True, (250, 20, 20)) screen.blit(text, (self.rect.centerx, self.rect.y - 60)) if not self.player_observed: if not self.sleeping_time or ticks - self.sleeping_time >= 250: if not self.sleeping_time: self.cur_sleeping_frame = 0 self.cur_sleeping_frame = (self.cur_sleeping_frame + 1) % len( Entity.sleeping_frames[0]) self.sleeping_time = ticks screen.blit(Entity.sleeping_frames[0][self.cur_sleeping_frame], (self.rect.centerx + 10, self.rect.y - 35)) def get_damage(self, damage, spell_type='', action_time=0): """ Получение дамага :param damage: Столько здоровья надо отнять :return: None """ if spell_type == 'ice': self.ice_buff += action_time if spell_type == 'poison': self.poison_buff += action_time self.last_damage_time = pygame.time.get_ticks() damage *= 10000 damage += randint(-damage * 0.1, damage * 0.1) damage /= 10000 self.health -= damage if self.health <= 0: self.death() def set_first_frame(self): """ Установка первого спрайта :return: None """ look = self.__class__.look_directions[self.look_direction_x, self.look_direction_y] self.cur_frame = 0 self.image = self.__class__.frames[look][self.cur_frame] @staticmethod def set_global_collisions_group(group: pygame.sprite.Group): """ Метод устанавливает группу со спрайтами, которые будут считаться физическими объектами для всех сущностей на уровне. (Кроме индивидуальных спрайтов у конкретных объектов, например у врагов будет отдельное взаимодействие с игроком). Метод нужен при инициализации :param group: Новая группа """ Entity.collisions_group = group
class Entity(pygame.sprite.Sprite): """ Класс, отвечающий за предстовление базовой сущности в игре """ # Группа со спрайтами, которые считаются физическими объектами # общими для всех сущностей. collisions_group: pygame.sprite.Group all_sprites: pygame.sprite.Group player = None # Группа со всеми сущностями (экземплярами этого класса) # Нужна в основном для коллизий между существами entities_group = pygame.sprite.Group() damages_group = pygame.sprite.Group() spells_group = pygame.sprite.Group() default_speed = TILE_SIZE * 0.2 WAITING_TIME = 2000 UPDATE_TIME = 120 HEALTH_LINE_WIDTH = 10 HEALTH_LINE_TIME = 5000 POISON_DAMAGE = 5 BETWEEN_POISON_DAMAGE = 1000 size = (int(TILE_SIZE),) * 2 sleeping_frames = cut_sheet(load_image('assets/sprites/enemies/sleep_icon_spritesheet.png'), 4, 1, size) poison_frames = cut_sheet(load_image('assets/sprites/spells/poison_static.png'), 5, 1, size)[0] small_font = load_game_font(15) font = load_game_font(24) def __init__(self, x: float, y: float, *args): # Конструктор класса Sprite super().__init__(*((Entity.entities_group,) + args)) self.alive = True # Изображение self.cur_frame = 0 self.image = self.__class__.frames[0][self.cur_frame] self.last_update = pygame.time.get_ticks() self.width, self.height = self.image.get_size() self.last_damage_time = -Entity.HEALTH_LINE_TIME self.last_poison_damage = 0 self.sleeping_time = None self.cur_sleeping_frame = 0 self.cur_poison_frame = 0 self.poison_static_time = 0 self.ice_buff = 0 self.poison_buff = 0 self.start_position = x, y self.point = None self.speed = 0.0001 self.rect = self.image.get_rect() self.rect.center = x, y self.collider = Collider(*self.rect.center) # Скорость self.dx = self.dy = 0 # Направление взгляда self.look_direction_x = 0 self.look_direction_y = 1 def update(self) -> None: if self.ice_buff: self.ice_buff -= 1 self.speed = self.__class__.default_speed * 0.2 else: self.speed = self.__class__.default_speed ticks = pygame.time.get_ticks() if self.poison_buff and ticks - self.last_poison_damage > Entity.BETWEEN_POISON_DAMAGE: self.last_poison_damage = ticks self.poison_buff -= 1 self.get_damage(Entity.POISON_DAMAGE, 'poison') def move(self, dx, dy): """ Метод передвижения. Сущность сдвинется на указанные параметры, если там свободно. :param dx: Изменение координаты по Х :param dy: Изменение координаты по Y """ if not self.alive: return # Координаты до движения pos = self.rect.x, self.rect.y # Перемещение по x и обновление столкновений self.rect.x = round(self.rect.x + dx) self.collider.update(*self.rect.center) # Если сущность врезалась во что-то, возвращаем к исходному x if pygame.sprite.spritecollide(self.collider, Entity.collisions_group, False): self.rect.x = pos[0] self.dx = 0 if self.__class__.__name__ == 'Player': self.dash_force_x *= 0.8 self.dash_direction_x = self.look_direction_x elif self.__class__.__name__ == 'PlayerAssistant': self.point = None # Перемещение по y и обновление столкновений self.rect.y = round(self.rect.y + dy) self.collider.update(*self.rect.center) # Если сущность врезалась во что-то, возвращаем к исходному y if pygame.sprite.spritecollide(self.collider, Entity.collisions_group, False): self.rect.y = pos[1] self.dy = 0 if self.__class__.__name__ == 'Player': self.dash_force_y *= 0.8 self.dash_direction_y = self.look_direction_y elif self.__class__.__name__ == 'PlayerAssistant': self.point = None def update_frame_state(self, n=0): """ Воспроизводит звук и сменяет кадр анимации :param n: если есть, сдвигает номер анимации (стояние вместо движения) """ tick = pygame.time.get_ticks() if tick - self.last_update > self.UPDATE_TIME: self.last_update = tick if not self.alive: self.cur_frame = self.cur_frame + 1 if self.cur_frame >= len(self.__class__.death_frames) - 1: if self.__class__.__name__ == 'Player': self.destroyed = True else: for group in self.groups(): group.remove(self) if self.cur_frame < len(self.__class__.death_frames): self.image = self.__class__.death_frames[self.cur_frame] return look = self.__class__.look_directions[self.look_direction_x, self.look_direction_y] look += n self.cur_frame = (self.cur_frame + 1) % len(self.__class__.frames[look]) self.image = self.__class__.frames[look][self.cur_frame] if (self.__class__.__name__ != 'Player' and DEFAULT_SOUNDS_VOLUME * 200 / self.distance_to_player > 0.1 and (look < 2 or 'Slime' in self.__class__.__name__ or 'Demon' in self.__class__.__name__) and not self.sounds_channel.get_busy()): self.FOOTSTEP_SOUND.set_volume(min(DEFAULT_SOUNDS_VOLUME / (self.distance_to_player / TILE_SIZE) * 3, 1)) self.sounds_channel.play(self.FOOTSTEP_SOUND) def draw_health_bar(self, screen): """ Отрисовка полоски здоровья и отрисовка знака сна (Z-Z-Z). :param screen: Экран """ line_width = Entity.HEALTH_LINE_WIDTH # длинна полоски здоровья x, y = self.rect.center # текущая позиция сущности width, height = self.rect.size # текущие размеры сущности # Позиция над сущностью x1, y1 = x - width * 0.5, y - height * 0.5 # При получении урона или наведении прицелом выводиться шкала здоровья if pygame.time.get_ticks() - self.last_damage_time < Entity.HEALTH_LINE_TIME or \ pygame.sprite.collide_rect(self, Entity.player.scope): # Обводка вокруг шкалы здоровья pygame.draw.rect(screen, 'dark grey', (x1 - 1, y1 - 10 - 1, width + 2, line_width + 2)) # Длинная полоски здоровья health_length = width * max(self.health, 0) / self.full_health # Для игрока и его помошника зелёный цвет, для остальных красный color = '#00b300' if self.__class__.__name__ in ('Player', 'PlayerAssistant') else 'red' # Сама полоска здоровья pygame.draw.rect(screen, color, (x1, y1 - 10, health_length, line_width)) # Текст с текущем здоровьем health_text = f'{round(self.health + 0.5)}/{self.full_health}' health = self.small_font.render(health_text, True, (255, 255, 255)) # Отцентровка текста по середине rect = health.get_rect() rect.center = (x1 + width // 2, y1 - 5) # Вывод на экран screen.blit(health, rect.topleft) # Для всех сущностей кроме игрока выводиться их название if self.__class__.__name__ not in ('Player',): name = self.font.render(self.name, True, (255, 255, 255)) rect = name.get_rect() rect.center = (x1 + width // 2, y1 - 12 - line_width) screen.blit(name, rect.topleft) if not self.alive: return # Отрисовка эффетка яда и обновление параметров ticks = pygame.time.get_ticks() if self.poison_buff: if ticks - self.poison_static_time > Entity.UPDATE_TIME: self.poison_static_time = ticks self.cur_poison_frame = (self.cur_poison_frame + 1) % len(Entity.poison_frames) screen.blit(Entity.poison_frames[self.cur_poison_frame], (self.rect.x, self.rect.y)) # Отрисовка анимации сна и обновление параметров if self.__class__.__name__ not in ('Player',) and not self.target_observed: if not self.sleeping_time or ticks - self.sleeping_time >= 250: self.cur_sleeping_frame = (self.cur_sleeping_frame + 1) % len(self.sleeping_frames[0]) self.sleeping_time = ticks screen.blit(self.sleeping_frames[0][self.cur_sleeping_frame], (self.rect.centerx + 10, self.rect.y - 35)) def get_damage(self, damage, spell_type='', action_time=0): """ Получение дамага :param damage: Столько здоровья надо отнять :param spell_type: Тип урона, чтоб узнавать, на кого он действует сильнее :param action_time: Время действия (для льда и отравления) """ if not self.alive: return if spell_type == 'ice': self.ice_buff += action_time if spell_type == 'poison' and damage >= 5: self.poison_buff += action_time if damage >= 0: look = self.__class__.look_directions[self.look_direction_x, self.look_direction_y] self.image = self.get_damage_frames[look] self.last_update = pygame.time.get_ticks() + 75 self.last_damage_time = pygame.time.get_ticks() if (self.__class__.__name__ == 'Demon' and spell_type == 'ice' or self.__class__.__name__ == 'GreenSlime' and spell_type == 'flash' or self.__class__.__name__ == 'DirtySlime' and spell_type == 'void' or self.__class__.__name__ == 'Zombie' and spell_type == 'fire' or self.__class__.__name__ == 'FireWizard' and spell_type == 'poison' or self.__class__.__name__ == 'VoidWizard' and spell_type == 'fire'): damage *= 2 if (self.__class__.__name__ == 'Demon' and spell_type == 'fire' or self.__class__.__name__ == 'GreenSlime' and spell_type == 'poison' or self.__class__.__name__ == 'DirtySlime' and spell_type == 'ice' or self.__class__.__name__ == 'Zombie' and spell_type == 'flash' or self.__class__.__name__ == 'FireWizard' and spell_type == 'fire' or self.__class__.__name__ == 'VoidWizard' and spell_type == 'void'): damage *= 0.25 damage *= 1000 damage += randint(-abs(round(-damage * 0.2)), abs(round(damage * 0.2))) damage /= 1000 x, y = self.rect.midtop colors = { 'poison': (100, 230, 125), 'ice': (66, 170, 255), 'void': (148, 0, 211), 'flash': (255, 255, 0), 'fire': (226, 88, 34), } ReceivingDamage(x, y, damage, Entity.all_sprites, Entity.damages_group, color=colors[spell_type]) self.health = min(self.health - damage, self.full_health) if self.health <= 0: self.health = 0 self.death() def set_first_frame(self): """Установка первого спрайта""" tick = pygame.time.get_ticks() if tick - self.last_update > self.UPDATE_TIME: self.last_update = tick look = self.__class__.look_directions[self.look_direction_x, self.look_direction_y] self.cur_frame = 0 self.image = self.__class__.frames[look][self.cur_frame] @staticmethod def set_global_groups(collisions_group: pygame.sprite.Group, all_sprites: pygame.sprite.Group): """ Метод устанавливает группу со спрайтами, которые будут считаться физическими объектами для всех сущностей на уровне. (Кроме индивидуальных спрайтов у конкретных объектов, например у врагов будет отдельное взаимодействие с игроком). Метод нужен при инициализации :param collisions_group: Новая группа :param all_sprites: Группа всех спрайтов """ Entity.collisions_group = collisions_group Entity.all_sprites = all_sprites