def __init__(self, x: float, y: float, damage: float, *groups, color=(255, 255, 255)): super().__init__(*groups) self.font = load_game_font(min(round(24 + abs(damage) / 3), 64)) self.last_update_time = 0 self.damage = abs(round(damage)) self.alpha = 500 self.image = self.font.render(str(self.damage), True, color).convert_alpha() self.rect = self.image.get_rect() self.rect.center = x + randint(-TILE_SIZE // 1.5, TILE_SIZE // 1.5), y + randint(-20, 20)
def __init__(self, position: tuple, text: str, text_size: int, base_button_filename="button.png", hover_button_filename="button_hover.png", *args): super().__init__(*args) # События, которые будут вызываться PyGame внутри update # (с помощью sender_text будет определено какая кнопка нажата) self.PRESS_EVENT = pygame.event.Event(Button.PRESS_TYPE, {"sender_text": text}) self.HOVER_EVENT = pygame.event.Event(Button.HOVER_TYPE, {"sender_text": text}) # Свойство, чтобы при наведении звук воспроизводился только один раз self.was_sound_played = False # Текст self.text = text self.font = load_game_font(text_size) # Базовое изображение self.text_surface = self.font.render(text, True, pygame.Color("white")) self.base_image = load_image( f"assets/sprites/UI/components/{base_button_filename}") self.base_image.blit( self.text_surface, self.text_surface.get_rect( center=self.base_image.get_rect().center)) # Изображение при наведении self.hover_image = load_image( f"assets/sprites/UI/components/{hover_button_filename}") self.hover_image.blit( self.text_surface, self.text_surface.get_rect( center=self.hover_image.get_rect().center)) # Текущее изображение self.image = self.base_image self.rect = self.image.get_rect() # Двигаем кнопку, но с учётом размера self.rect = self.rect.move(position[0] - self.rect.width / 2, position[1] - self.rect.height / 2)
class Message(pygame.sprite.Sprite): """ Класс, представляющий текстовое сообщение появляющиеся при сталкивании с каким-либо объектом. Сами колизии и параметр времени последнего столкновения обрабатываются вне класса """ font = load_game_font(32) # Время отрисовки на экране DRAWING_TIME = 1000 # Время угасания, заимствующееся из времени отрисовки # (равное количество ставить не стоит) FADING_TIME = 500 def __init__(self, screen: pygame.surface.Surface, text: str, height: int): super().__init__() # Изображение self.image = self.font.render(text, True, (255, 244, 79)).convert_alpha() self.rect = self.image.get_rect() self.rect.center = screen.get_width() // 2, int(height) # Последнее время столкновения self.last_collide_time = -self.DRAWING_TIME def draw(self, screen): # Время, прошедшее с последнего вызова отрисовки past_time = pygame.time.get_ticks() - self.last_collide_time # Учёт времени, для отрисовки сообщения if past_time <= Message.DRAWING_TIME: # Обработка эффекта затухании, по мере удаления от объекта if past_time >= Message.DRAWING_TIME - Message.FADING_TIME: # Коэффицент прозрачности, вычисляющийся из времени, # прошедшего с последнего вызова, и времени угасания сообщения k = (past_time - Message.DRAWING_TIME + Message.FADING_TIME) / Message.FADING_TIME self.image.set_alpha(255 - round(255 * k)) else: self.image.set_alpha(255) screen.blit(self.image, self.rect.topleft)
def __init__(self, text: str, text_size: int, position: tuple): self.font = load_game_font(text_size) # шрифт self.image = self.background_image # фон indent = 50 # Отступ text = text.strip() # Высчитывание размера для фона size = (max(int(text_size * 0.38 * max(map(len, text.split('\n')))), 300), round(indent + len(text.split('\n')) * self.font.get_height() * 0.9)) # Отмасштабированый фон с текстурой для красоты self.image = scale_frame(self.image, size, indent) self.rect = self.image.get_rect() self.rect.center = position # местоположение # Текст для диалога self.texts = text.split('\n') # Так нужно для вывода сразу нескольких строк self.text_surfaces = [ self.font.render(part.strip(), True, (255, 255, 255)) for part in self.texts ] # Флаг для отрисовки (если True, то диалог рисуется) self.need_to_draw = True
def __init__(self, item_type: str, count: int, x: float, y: float, all_sprites, *groups): super().__init__(all_sprites, GroundItem.sprites_group, *groups) self.type = item_type # тип предмета self.count = abs(int(count)) if not self.count: self.kill() self.image, self.sound = GroundItem.IMAGES[ self.type], GroundItem.SOUNDS[self.type] self.sound.set_volume(DEFAULT_SOUNDS_VOLUME * 3) self.image = self.image.copy() self.rect = self.image.get_rect() self.rect.center = x, y self.collider = Collider(x, y) for other in pygame.sprite.spritecollide(self, GroundItem.sprites_group, False): other: GroundItem if self.type == other.type and other != self: self.count = self.count + other.count other.kill() font = load_game_font(32) if self.count > 1: count_text = font.render(str(self.count), True, (255, 255, 255)) rect = count_text.get_rect() rect.center = self.rect.right - rect.w // 2, self.rect.bottom - rect.h // 2 self.image.blit( count_text, count_text.get_rect( bottomright=self.image.get_rect().bottomright))
class PlayerIcon: """ Класс, представляющий UI элемент с отображением данных об игроке или компаньёне """ # В этом случае фонт всегда будет общий у всех, поэтому это атрибут класса font = load_game_font(32) # Изображение с иконкой игрока PLAYER_FACE = pygame.transform.scale2x( load_image('assets/sprites/UI/icons/player_face.png')) # Изображение с иконкой помошника ASSISTANT_FACE = pygame.transform.scale2x( load_image('assets/sprites/UI/icons/assistant_face.png')) # Рамка вокруг иконки FRAME = load_image('assets/sprites/UI/icons/player_icon_frame.png') # Иконка яда size = (40, 40) POISON_ICON = load_image('assets/sprites/UI/icons/poison_icon.png', size) def __init__(self, player_or_assistant): # Ссылка на игрока (или асистента) для получение необходимоых # параметров, таких как: здоровье, мана и т.п. self.player_or_assistant = player_or_assistant def draw(self, screen: pygame.surface.Surface, position=(0, 0), size_coefficient=1): """ Рисует UI элемент на экране screen :param screen: Экран для отрисовки :param position: позиция отрисовки от левого верхнего угла экрана :param size_coefficient: Коэффицент размера иконки """ # Позиция x1, y1 = (0, 0) # Пустое изображение всей иконки, куда будут отрисовываться части ниже image = pygame.surface.Surface(self.FRAME.get_size(), pygame.SRCALPHA) # Высчитывание длинны полосы здоровья health_length = round(264 * (self.player_or_assistant.health / self.player_or_assistant.full_health) + 0.5) # Поверхность со здоровьем health_line = pygame.surface.Surface((health_length, 24)) health_line.fill((255, 30, 30)) # Отрисовка полоски со здоровьем и количества здоровья image.blit(health_line, (x1 + 132, y1 + 12)) image.blit( self.font.render( f'{round(self.player_or_assistant.health + 0.5)}/' + f'{self.player_or_assistant.full_health}', True, (255, 255, 255)), (x1 + 220, y1 + 10)) # Высчитывание длинны полосы маны mana_length = round(264 * (self.player_or_assistant.mana / self.player_or_assistant.full_mana) + 0.5) # Поверхность с маной mana_line = pygame.surface.Surface((mana_length, 24)) mana_line.fill((30, 30, 255)) # Отрисовка полоски с маной и количества маны image.blit(mana_line, (x1 + 132, y1 + 52)) image.blit( self.font.render( f'{round(self.player_or_assistant.mana + 0.5)}/' + f'{self.player_or_assistant.full_mana}', True, (255, 255, 255)), (x1 + 220, y1 + 50)) # Если текущая иконка относится к игроку if self.player_or_assistant.__class__.__name__ == 'Player': screen.blit( self.font.render(f'{round(self.player_or_assistant.money)}', True, (255, 255, 30)), (self.FRAME.get_width() + 20, 20)) image.blit(self.PLAYER_FACE, (x1 + 25, y1 + 20)) else: image.blit(self.ASSISTANT_FACE, (x1 + 25, y1 + 20)) # Отрисовка фона (рамки) на иконку image.blit(self.FRAME, (x1, y1)) # Отрисовка пустого текста на иконке # (для смещения, т.е. по сути это декоративный эффект) text_surface = self.font.render('', True, (255, 255, 255)) image.blit(text_surface, (x1 + 8, y1 + 14)) # Вывод всей иклггки на экран с учётом коэффицента размера screen.blit( pygame.transform.scale( image, (int(self.FRAME.get_width() * size_coefficient), int(self.FRAME.get_height() * size_coefficient))), position)
class SpellContainer: """Класс представляет UI элемент с отображением данных о заклинании""" # В этом случае шрифт всегда будет общий у всех, поэтому это атрибут класса font = load_game_font(32) mini_font = load_game_font(16) # Задержка курсора на иконке перед показом рамки delay_time = 35 size = (39, 39) # размер для иконок ниже # Иконки кнопок джойстика, чтобы отображать кнопки для вызова заклинаний JOYSTICK_ICONS = { "o": load_image("assets/sprites/UI/icons/joystick_o.png", size), "x": load_image("assets/sprites/UI/icons/joystick_x.png", size), "triangle": load_image("assets/sprites/UI/icons/joystick_triangle.png", size), "square": load_image("assets/sprites/UI/icons/joystick_square.png", size), "L1": load_image("assets/sprites/UI/icons/joystick_L1.png", size), "L2": load_image("assets/sprites/UI/icons/joystick_L2.png", size), } # Поверхность, которая отображается, если заклинание недоступно # (т.е. эффект замедления) LOCKED = pygame.surface.Surface((20, 20)).convert_alpha() LOCKED.fill((0, 0, 0, 180)) # Рамка (фон) вокруг иконки с заклинанием FRAME = load_image('assets/sprites/UI/icons/spell_icon_frame.png') def __init__(self, icon_filename: str, spell_class, player): # Иконка заклинания self.spell_icon = load_image( f"assets/sprites/UI/icons/{icon_filename}") self.rect = self.spell_icon.get_rect() self.w, self.h = self.spell_icon.get_size() # размер иконки # Картинка затемнения self.locked = pygame.transform.scale(self.LOCKED, (self.w, self.h)) self.mana_cost = spell_class.mana_cost # Стоимость заклинания для игрока # ссылка на игрока для получение параметров, связанных с заклинаниями self.player = player # Информация, которая будет показана в рамке при наведении self.information = f'''{spell_class.__doc__} Урон: {spell_class.damage}{f' + {spell_class.extra_damage}' if spell_class.__name__ == 'PoisonSpell' else ''} {'Время действия: ' + str(spell_class.action_time) + ' c' if spell_class.__name__ in ('IceSpell', 'PoisonSpell') else 'Мгновенное действие'} Затраты маны: {spell_class.mana_cost}'''.strip() # Диалоговое окно для вывода информации при наведении self.massage_box = MessageBox(self.information, 30, (0, 0)) # время наведения, нужное для определение того, когда надо # отрисовать окно с информацией self.hover_time = 0 def draw(self, screen: pygame.surface.Surface, position: tuple, is_joystick: bool, spell_key: str): """ Рисует UI элемент на экране screen :param screen: Экран для отрисовки :param position: Позиция отрисовки :param is_joystick: Подключен ли джойстик :param spell_key: Строка, представляющая либо ключ для вывода иконки для джойстика, либо текст для вывода возле иконки заклинания """ x1, y1 = position # координаты для отрисовки pos = (x1 + 2, y1 + 18) # учёт отступа по размерам края рамки self.rect.topleft = pos # Иконка заклинания screen.blit(self.spell_icon, pos) # Отрисовка затемнения, елси заклинание сейчас недоступно if self.player.mana < self.mana_cost or \ pygame.time.get_ticks() - self.player.shoot_last_time < self.player.between_shoots_range: screen.blit(self.locked, pos) # Отрисовка рамки вокруг иконки заклинания screen.blit(self.FRAME, position) # Смещение между иконкой заклинания и кнопкой для переключения pos = (x1 + 5, y1 + 14) # Если подключён джойстик, то рисуется специальная иконка элемента, # которая активирует заклинание if is_joystick: screen.blit(SpellContainer.JOYSTICK_ICONS[spell_key], pos) # Иначе просто текст кнопки с клавиатуры else: button_text = SpellContainer.font.render(spell_key, True, (255, 255, 255)) screen.blit(button_text, pos) # При наведении курсора на заклинание, рисуется табличка с информацией if self.rect.collidepoint(*self.player.scope.rect.center): # Если время наведения на иконку с заклинанием привысело порог, то # информация выводится if self.hover_time == self.delay_time: # Смещение окошка в сторону прицела и отрисовка self.massage_box.rect.bottomleft = self.player.scope.rect.center self.massage_box.draw(screen) else: self.hover_time += 1 elif self.hover_time: self.hover_time = 0 # Отрисовка цены маны за заклинание в правом нижнем углу pos = (x1 + self.h - 6, y1 + self.w - 2) # позиция cost_text = SpellContainer.mini_font.render(str(self.mana_cost), True, (255, 255, 255)) # цена screen.blit(cost_text, pos)
def execute(screen: pygame.surface.Surface, money: int, count_of_alive_assistants: int, is_win=False): """ Функция запускает конечной экран (либо смерти, либо победы) :param screen: Экран на котором надо отрисовывать менюв :param is_win: Флаг, выиграл ли игрок :param money: Количество собранных игроком и асистентом денег :param count_of_alive_assistants: Количетсво всех живых осистентов к концу игры игры """ is_open = True # Фоновое изображение для всего экрана if is_win: # Фоновая музыка при победе pygame.mixer.music.load("assets/audio/music/win_screen_BG.ogg") pygame.mixer.music.play(-1) animated_background = AnimatedBackground( "win_{0}.png", "assets/sprites/UI/backgrounds/triumph_screen", 1, 8, 80, screen.get_size()) # Картигка с заголовком победы title_you_win = load_image('assets/sprites/UI/you_win.png') you_win_rect = title_you_win.get_rect() you_win_rect.center = screen.get_rect().centerx, int( screen.get_rect().centery * 0.7) else: # Фоновая музыка при проигрыше pygame.mixer.music.load("assets/audio/music/fail_screen_BG.mp3") pygame.mixer.music.play(-1) # Высчитывание размера для фона и сам фон size = screen.get_width() // 3, screen.get_height() // 3 animated_background = AnimatedBackground( "death_{0}.png", "assets/sprites/UI/backgrounds/fail_screen", 1, 23, 140, size, scale_2n=True) # Лого игры logo = LogoImage((screen.get_width() * 0.5, screen.get_height() * 0.1)) # Изображение курсора cursor_image = load_image("assets/sprites/UI/icons/cursor.png") # Получение джойстика (если есть) и определение начальной позиции курсора if check_any_joystick(): joystick = get_joystick() cursor_x, cursor_y = screen.get_rect().center else: joystick = None # Т.к. джойстика нет позиция будет сразу переопределна далее, # поэтому тут начальная позиция не задаётся cursor_x, cursor_y = 0, 0 # Т.к. игрок завершил игру, то файл с сохранением будет перезаписан if os.path.isfile("data/save.txt"): with open('data/save.txt', 'r+', encoding="utf-8") as file: file.truncate(0) # Кортеж с текстом который надо вывести (каждый элемент на новой строке) texts = (f"Деньги собранные игроком вместе с асистентом: {money}", f"Количество живых асистентов: {count_of_alive_assistants}") # Шрифт для поверхностей ниже title_font = load_game_font(64) # Поверхности с одним и тем же текстом, но разный цвет делает крассивый эффект text_surfaces_yellow = [ title_font.render(part.strip(), True, (255, 184, 50)) for part in texts ] text_surfaces_red = [ title_font.render(part.strip(), True, (179, 64, 16)) for part in texts ] # Смещение между наложенными поверхностями для красивого эффекта surfaces_offset = 3 margin = title_font.get_height() * 0.9 # отступ между двумя поверхностями # События, которые активируют закрытие экрана с концном QUITING_EVENTS = ( pygame.QUIT, pygame.MOUSEBUTTONUP, pygame.KEYDOWN, ) # Цикл меню while is_open: # Обработка событий for event in pygame.event.get(): if event.type in QUITING_EVENTS: is_open = False break # Обновление позиции курсора if joystick is not None: # Проверка на выход if joystick.get_button(CONTROLS["JOYSTICK_UI_CLICK"]): break # Значение осей на левом стике axis_x, axis_y = joystick.get_axis(0), joystick.get_axis(1) # Перемещение курсора при движении оси if abs(axis_x) >= JOYSTICK_SENSITIVITY: cursor_x += JOYSTICK_CURSOR_SPEED * axis_x if abs(axis_y) >= JOYSTICK_SENSITIVITY: cursor_y += JOYSTICK_CURSOR_SPEED * axis_y else: cursor_x, cursor_y = pygame.mouse.get_pos() # На экране проигрыша есть фон, которого нет на экране победы if not is_win: screen.fill((31, 30, 36)) # Вывод текущего кадра фонового изображения animated_background.update() screen.blit( animated_background.image, animated_background.image.get_rect( center=screen.get_rect().center)) # Вывод картинки победного заголовка, если игрок выиграл if is_win: # Анализатор может ругаться, но если is_win истина, то # переменные 100% объявлены выше screen.blit(title_you_win, you_win_rect) # следущая позиция по y (будет нужно при вычислении смещения) next_y = 20 # Вывод красного текста for text_surface in text_surfaces_red: y_pos = screen.get_height() * 0.6 + next_y screen.blit( text_surface, text_surface.get_rect(midtop=(screen.get_rect().centerx + surfaces_offset, y_pos + surfaces_offset))) next_y += margin next_y = 20 # Вывод жёлтого текста for text_surface in text_surfaces_yellow: y_pos = screen.get_height() * 0.6 + next_y screen.blit( text_surface, text_surface.get_rect(midtop=(screen.get_rect().centerx, y_pos))) next_y += margin # Вывод логотипа игры screen.blit(logo.image, logo.rect.topleft) # Вывод изображения курсора screen.blit(cursor_image, (cursor_x, cursor_y)) # Обновление состояния джойстика joystick = get_joystick() if check_any_joystick() else None pygame.display.flip()
def play(screen: pygame.surface.Surface, level_number: int = 1, user_seed: str = None) -> int: """ Функция запуска игрового процесса :param screen: Экран для отрисовки :param level_number: Номер текущего уровня :param user_seed: Сид карты. Если он есть, по нему создаются уровни, раставляются враги и прочее :return: Код завершения игры (значения описаны в main.py) """ # Псевдо загрузочный экран (для красоты) loading_screen(screen) # Размеры экрана screen_width, screen_height = screen.get_size() # Группа со всеми спрайтами all_sprites = pygame.sprite.Group() # Группа со спрайтами тайлов пола tiles_group = pygame.sprite.Group() # Группа со спрайтами ящиков и бочек # (они отдельно, т.к. это разрушаемые объекты) furniture_group = pygame.sprite.Group() # Группа со спрайтами преград (т.е. все физические объекты) collidable_tiles_group = pygame.sprite.Group() # Группа со спрайтами врагов enemies_group = pygame.sprite.Group() # Группа со спрайтами дверей doors_group = pygame.sprite.Group() # Группа со спрайтами факелов torches_group = pygame.sprite.Group() # Группа со спрайтом конца уровня (т.е. с лестницой перехода вниз) end_of_level = pygame.sprite.Group() # Группа с предметаими, которые находятся на полу GroundItem.sprites_group = pygame.sprite.Group() # Группа с сундуками Chest.chest_group = pygame.sprite.Group() is_open = True # Поверхность для эффекта затемнения transparent_grey = pygame.surface.Surface((screen_width, screen_height), pygame.SRCALPHA).convert_alpha() clock = pygame.time.Clock() # Часы current_seed = user_seed # текущий сид # Создаем уровень с помощью функции из generation_map level, level_seed = generate_new_level( current_seed.split('\n')[0].split() if current_seed else 0) # Игрок (None, т.к. будет переопределён либо при инициализации, либо при по) player = None if current_seed: data = current_seed.split('\n') # Данные из сида # Получение данных об игроке из сида и создание игрока _, _, player_level, health, mana, money = data[3].split()[:-1] player = Player(0, 0, player_level, all_sprites, health, mana, money) # Получение данных об асистентах игрока for n in range(int(data[3].split()[-1])): # Получениев параметров асистента и его создание x1, y1, health, mana, *name = data[4 + n].split() assistant = PlayerAssistant(0, 0, player, all_sprites, health, mana, name) # Добавление асистента player.add_assistant(assistant) # Необходимые аргументы для инициализации уровня args = (level, level_number, all_sprites, tiles_group, furniture_group, collidable_tiles_group, enemies_group, doors_group, torches_group, end_of_level, current_seed.split('\n')[1].split() if current_seed else [], current_seed.split('\n')[2].split() if current_seed else [], player) # Инициализация уровня и получение данных об игроке и частях сида player, monsters_seed, boxes_seed = initialise_level(*args) if current_seed: # Если сид был передан, сдвигаем игрока на расстояние от начала уровня (лестницы) # Которое было записано в сид x_from_start, y_from_start = map( float, current_seed.split('\n')[3].split()[:2]) player.rect.center = player.rect.centerx + x_from_start, player.rect.centery + y_from_start # Смещение всех асистентов игрока for assistant in player.assistants: assistant.rect.center = player.rect.center # Обновление и сохранение сида после инициализации уровня current_seed = '\n'.join([ ' '.join(level_seed), ' '.join(monsters_seed), ' '.join(boxes_seed), str(player), str(level_number) ]) save(current_seed) camera = Camera(screen.get_size()) # камера # Инициализация начальной позиции прицела игрока player.scope.init_scope_position((screen_width * 0.5, screen_height * 0.5)) # Шрифт для вывода фпс в левом верхнем углу fps_font = load_game_font(48) # Иконка рядом с номером уровня (в правом верхнем углу) level_number_icon = load_tile('DOWNSTAIRS.png') # Иконка рядом с количеством врагов на уровне (в правом верхнем углу) monster_number_icon = load_image( 'assets/sprites/UI/icons/monster_number.png', (TILE_SIZE, ) * 2) # Шрифт для вывода номера уровня и количества врагов level_and_enemies_font = load_game_font(64) # Сообщение, которое будет появлятся при приближении игрока к сундуку chest_title = Message(screen, 'Нажмите Е (или L2), чтобы открыть сундук', screen.get_height() * 0.1) # Сообщение, которое будет появлятся при приближении игрока к спуску вниз downstairs_title = Message( screen, 'Нажмите Е (или L2), чтобы перейти на следующий уровень', screen.get_height() * 0.1) # Иконки для отображения иконок (контейнеров) с заклинаниями внизу экрана spells_containers = ( SpellContainer("fire_spell.png", FireSpell, player), SpellContainer("ice_spell.png", IceSpell, player), SpellContainer("poison_spell.png", PoisonSpell, player), SpellContainer("void_spell.png", VoidSpell, player), SpellContainer("light_spell.png", FlashSpell, player), SpellContainer("teleport_spell.png", TeleportSpell, player), ) # Панель с иконкой и информацией об игроке в левом верхнем углу player_icon = PlayerIcon(player) # Высота для высчитывания позиции отрисовки иконки асистента assistants_height = 180 # Отступ для вывода иконки игрока и его ассистентов indent = 20 # Фоновая музыка pygame.mixer.music.load("assets/audio/music/game_bg.ogg") pygame.mixer.music.play(-1) pygame.mixer.music.set_volume(DEFAULT_MUSIC_VOLUME) # Установка событий, обрабатываемых pygame, чтобы не тратить # время на обработку ненужных событий (это относится ко всей игре в целом, # где обрабатываются события) pygame.event.set_allowed(( pygame.QUIT, pygame.MOUSEBUTTONUP, pygame.KEYDOWN, )) # Игровой цикл while is_open: was_pause_activated = False # была ли активирована пауза keys = pygame.key.get_pressed() # нажатые клавиши buttons = pygame.mouse.get_pressed(5) # нажатые кнопки мыши for event in pygame.event.get(): if event.type == pygame.QUIT: is_open = False if event.type == pygame.KEYDOWN: if event.key == CONTROLS["KEYBOARD_PAUSE"]: was_pause_activated = True # Провверка использования заклинаний с джойстика if player.joystick: if player.joystick.get_button(CONTROLS["JOYSTICK_UI_PAUSE"]): was_pause_activated = True if player.joystick.get_button(CONTROLS["JOYSTICK_SPELL_FIRE"]): player.shoot('fire', enemies_group) if player.joystick.get_button(CONTROLS["JOYSTICK_SPELL_ICE"]): player.shoot('ice', enemies_group) if player.joystick.get_button(CONTROLS["JOYSTICK_SPELL_LIGHT"]): player.shoot('flash', enemies_group) if player.joystick.get_button(CONTROLS["JOYSTICK_SPELL_POISON"]): player.shoot('poison', enemies_group) if player.joystick.get_button(CONTROLS["JOYSTICK_SPELL_VOID"]): player.shoot('void', enemies_group) # Используется ось, т.к. назначен триггер R2 if player.joystick.get_axis(CONTROLS["JOYSTICK_SPELL_TELEPORT"] ) > JOYSTICK_SENSITIVITY: player.shoot('teleport', tiles_group) # Иначе ввод с клавиатуры else: if keys[CONTROLS["KEYBOARD_SPELL_FIRE"]] or buttons[ CONTROLS["MOUSE_SPELL_FIRE"]]: player.shoot('fire', enemies_group) if keys[CONTROLS["KEYBOARD_SPELL_ICE"]] or buttons[ CONTROLS["MOUSE_SPELL_ICE"]]: player.shoot('ice', enemies_group) if keys[CONTROLS["KEYBOARD_SPELL_LIGHT"]] or buttons[ CONTROLS["MOUSE_SPELL_LIGHT"]]: player.shoot('flash', enemies_group) if keys[CONTROLS["KEYBOARD_SPELL_POISON"]] or buttons[ CONTROLS["MOUSE_SPELL_POISON"]]: player.shoot('poison', enemies_group) if keys[CONTROLS["KEYBOARD_SPELL_VOID"]]: player.shoot('void', enemies_group) if keys[CONTROLS["KEYBOARD_SPELL_TELEPORT"]]: player.shoot('teleport', tiles_group) # Обработка паузы if was_pause_activated: # # Остановка звуков и музыки pygame.mixer.pause() pygame.mixer.music.pause() # Запуск меню паузы code = game_menu.execute(screen) if code == 1: # Псевдо экран загрузки перед следующими действиями (для красоты) loading_screen(screen) # Очищаем все группы со спрайтами all_sprites.empty() tiles_group.empty() furniture_group.empty() collidable_tiles_group.empty() enemies_group.empty() doors_group.empty() torches_group.empty() end_of_level.empty() Chest.chest_group.empty() GroundItem.sprites_group.empty() Entity.damages_group.empty() # Сохранение данных перед выходом save('') return 2 if code is not None: # Ставим экран загрузки перед следующими действиями loading_screen(screen) # Очищаем все группы со спрайтами all_sprites.empty() tiles_group.empty() furniture_group.empty() collidable_tiles_group.empty() enemies_group.empty() doors_group.empty() torches_group.empty() end_of_level.empty() Chest.chest_group.empty() GroundItem.sprites_group.empty() Entity.damages_group.empty() # Сохранение данных перед выходом if player.alive: current_seed = '\n'.join([ ' '.join(level_seed), ' '.join(monsters_seed), ' '.join(boxes_seed), str(player), str(level_number) ]) save(current_seed) else: save('') return -1 # Возвращение звука и мызыки так, как было до паузы pygame.mixer.unpause() pygame.mixer.music.unpause() screen.fill(BACKGROUND_COLOR) # Очистка экрана player.update() # Обновление игрока # Если игрок умер, то открывается экран конца игры if player.destroyed: # Остановка звуков и музыки pygame.mixer.pause() pygame.mixer.music.pause() # Подсчёт количества живых асистентов у игрока (для вывода статистики) count_of_alive_assistants = 0 for sprite in player.assistants.sprites(): sprite: PlayerAssistant # Если асистент живой увеличиваем счётчик count_of_alive_assistants += int(sprite.alive) # Запуск экрана с концом end_screen.execute(screen, player.money, count_of_alive_assistants) return -1 # Проверка на столкновение с любым сундуком if pygame.sprite.spritecollideany(player, Chest.chest_group): # Обновление времени столкновения с сундуком для # красивой отрисовки сообщения chest_title.last_collide_time = pygame.time.get_ticks() # Проверка на использование (с джойстика или клавиатуры) if ((player.joystick and player.joystick.get_axis( CONTROLS['JOYSTICK_USE']) > JOYSTICK_SENSITIVITY) or (keys[CONTROLS['KEYBOARD_USE']])): pygame.sprite.spritecollide(player, Chest.chest_group, False)[0].open() enemies_group.update(player) # обновление врагов player.assistants.update(enemies_group) # обновление асистентов Entity.spells_group.update() # обновление заклинаний # Обновление факелов (для звука огня по расстоянию до факела) torches_group.update(player) # Обновление всех дверей doors_group.update(player, enemies_group, [player] + list(player.assistants)) Chest.chest_group.update() # обновление сундуков Entity.damages_group.update() # обновление текста с выводом урона # Проверка перехода на следующий уровень, при соприкосновении с лестницой вниз if pygame.sprite.spritecollideany(player.collider, end_of_level): # Обновление времени столкновения с лестницой вниз для # красивой отрисовки сообщения downstairs_title.last_collide_time = pygame.time.get_ticks() if (keys[pygame.K_e] or (player.joystick and player.joystick.get_axis( CONTROLS['JOYSTICK_USE']) > JOYSTICK_SENSITIVITY)): # Затухание музыки и звуком pygame.mixer.fadeout(1000) pygame.mixer.music.fadeout(1000) # Псевдо загрузочный экран для красоты loading_screen(screen) # Если игрок прошёл 10 уровней, то это победа if level_number == 10: # Подсчёт количества живых асистентов у игрока (для вывода статистики) count_of_alive_assistants = 0 for sprite in player.assistants.sprites(): sprite: PlayerAssistant # Если асистент живой увеличиваем счётчик count_of_alive_assistants += int(sprite.alive) # Победный экран end_screen.execute(screen, player.money, count_of_alive_assistants, is_win=True) return -1 # Иначе перезагружаются параметры для нового уровня else: # Очистка всех групп со спрайтами all_sprites.empty() tiles_group.empty() furniture_group.empty() collidable_tiles_group.empty() enemies_group.empty() doors_group.empty() torches_group.empty() end_of_level.empty() Chest.chest_group.empty() GroundItem.sprites_group.empty() Entity.damages_group.empty() level_number += 1 # увеличение номер уровня # Создание целиком нового уровень функцией из generation_map level, level_seed = generate_new_level(0) # Необходимые аргументы для инициализации уровня args = (level, level_number, all_sprites, tiles_group, furniture_group, collidable_tiles_group, enemies_group, doors_group, torches_group, end_of_level, [], []) # Инициализация уровня и получение данных об игроке и частях сида player, monsters_seed, boxes_seed = initialise_level( *args, player=player) # Добавление игрока и асистентов all_sprites.add(player) all_sprites.add(player.assistants) # Смещение асистентов к игроку for assistant in player.assistants: assistant.rect.center = player.rect.center # Изменение текущего сида и файла сохранения current_seed = '\n'.join([ ' '.join(level_seed), ' '.join(monsters_seed), ' '.join(boxes_seed), str(player), str(level_number) ]) save(current_seed) # Установка начальной позиции приуела player.scope.init_scope_position( (screen_width * 0.5, screen_height * 0.5)) # Иконки для отображения иконок (контейнеров) с заклинаниями внизу экрана spells_containers = ( SpellContainer("fire_spell.png", FireSpell, player), SpellContainer("ice_spell.png", IceSpell, player), SpellContainer("poison_spell.png", PoisonSpell, player), SpellContainer("void_spell.png", VoidSpell, player), SpellContainer("light_spell.png", FlashSpell, player), SpellContainer("teleport_spell.png", TeleportSpell, player), ) # Включение музыки после обновления параметров pygame.mixer.music.play(-1) continue # Применение смещения камеры относительно игрока camera.update(player) for sprite in all_sprites: camera.apply(sprite) # Отрисовка спрайтов в определённом порядке, # чтобы они не перекрывали друг друга tiles_group.draw(screen) # тайлы пола torches_group.draw(screen) # факеда # Сундуки for chest in Chest.chest_group: chest.draw_back_image(screen) # предметы на земле (мясо и деньги) GroundItem.sprites_group.draw(screen) collidable_tiles_group.draw( screen) # физические объекты не являющиеся стенами doors_group.draw(screen) # двери enemies_group.draw(screen) # враги player.assistants.draw(screen) # асистенты # Шкалы здоровья у асистентов for assistant in player.assistants: assistant.draw_health_bar(screen) player.draw(screen) # игрок Entity.spells_group.draw(screen) # заклинания player.draw_health_bar(screen) # шкала здоровья у игрока # Шкала здоровья у врагов for enemy in enemies_group: enemy.draw_health_bar(screen) Entity.damages_group.draw(screen) # текст с уроном chest_title.draw(screen) # сообщение по мере приближении к сундуку # сообщение по мере приближении к лестнице вниз downstairs_title.draw(screen) # Значения для определения того, какие иконки текст, # нужно отображать на иконках с заклинаниями (нужно, чтобы игроку было # проще привыкнуть к управлению) is_joystick = player.joystick is not None if is_joystick: spell_args = ("o", "x", "triangle", "square", "L1", "L2") else: spell_args = ("1", "2", "3", "4", "5", "Space") # Контейнеры с заклинаниями for i in range(len(spells_containers) - 1, -1, -1): pos = (screen_width * (0.375 + 0.05 * i), screen_height * 0.9) spells_containers[i].draw(screen, pos, is_joystick, spell_args[i]) # Панель с игроком в левом верхнем углу player_icon.draw(screen, (indent, indent)) # Иконоки у асистентов for number_of_assistant, assistant in enumerate(player.assistants): if not assistant.icon: assistant.icon = PlayerIcon(assistant) assistant.icon.draw( screen, (indent + 20, assistants_height + number_of_assistant * 80), 0.5) # фпс fps_text = fps_font.render(str(round(clock.get_fps())), True, (100, 255, 100)) screen.blit(fps_text, (2, 2)) # Иконка и количество врагов на уровне monster_number_text = level_and_enemies_font.render( str(len(enemies_group)), True, (255, 255, 255)) screen.blit(monster_number_icon, (screen_width - 70, 80)) screen.blit(monster_number_text, (screen_width - 120, 80)) # Иконка и номер уровня level_number_text = level_and_enemies_font.render( str(level_number), True, (255, 255, 255)) screen.blit(level_number_icon, (screen_width - 70, 10)) screen.blit(level_number_text, (screen_width - 120, 10)) # Прицел игрока player.scope.draw(screen) clock.tick(FPS) pygame.display.flip() # Запись сохранения после закрытия игры save(current_seed) return 0
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