Код IT
← Каталог

Python — диаблоид — `core/game.py`

Фрагмент из «Python — диаблоид»: `core/game.py`.

python spinoffencyclopedia9-04-razrabotka-igr-praktikum-razrabotki-igr-6 embed URL статья в энциклопедии
Python main.py
import pygame

from core.config import FPS, SCREEN_HEIGHT, SCREEN_WIDTH, TITLE, GameState
from core.event_bus import EventBus
from engine.camera import Camera
from engine.input_handler import InputHandler
from engine.map_renderer import MapRenderer
from engine.renderer import Renderer, screen_to_world
from entities.item import GroundItem
from entities.player import PlayerEntity
from systems.ai_system import AISystem
from systems.combat_system import CombatSystem
from systems.loot_system import LootSystem
from systems.movement_system import MovementSystem
from systems.skill_system import SkillSystem
from ui.hud import HUD
from ui.menu import MenuUI
from world.map import GameMap


class Game:
    def __init__(self) -> None:
        pygame.init()
        self.screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
        pygame.display.set_caption(TITLE)
        self.clock = pygame.time.Clock()
        self.running = True
        self.events = EventBus()
        self.renderer = Renderer(self.screen)
        self.map_renderer = MapRenderer(self.renderer)
        self.camera = Camera()
        self.input = InputHandler()
        self.movement = MovementSystem()
        self.combat = CombatSystem(self.events)
        self.skills = SkillSystem(self.events)
        self.ai = AISystem()
        self.loot = LootSystem(self.events)
        self.hud = HUD(self.renderer)
        self.menu_ui = MenuUI()
        self.state = GameState.MENU
        self.menu_selected = 0
        self.show_inventory = False
        self.floor = 1
        self.player: PlayerEntity | None = None
        self.game_map: GameMap | None = None
        self.enemies = []
        self.ground_items: list[GroundItem] = []
        self._wire_events()

    def _wire_events(self) -> None:
        self.events.subscribe("item_dropped", self._on_item_dropped)
        self.events.subscribe("enemy_killed", self._on_enemy_killed)

    def _on_item_dropped(self, item: GroundItem, **_) -> None:
        self.ground_items.append(item)

    def _on_enemy_killed(self, enemy, killer, **_) -> None:
        if killer is None or self.player is None or self.game_map is None:
            return
        xp = 12 + self.game_map.floor * 2
        ups = killer.experience.add_xp(xp)
        for _ in range(ups):
            killer.stats.on_level_up()
        self.events.emit("level_up", player=killer, levels=ups)

    def _start_new_game(self) -> None:
        self.floor = 1
        self.game_map = GameMap(floor=self.floor, seed=42)
        self.player = PlayerEntity(
            self.game_map.start_tile[0] + 0.5,
            self.game_map.start_tile[1] + 0.5,
        )
        self.enemies = self.ai.spawn_enemies(self.game_map, self.floor)
        self.ground_items.clear()
        self.show_inventory = False

    def _try_use_portal(self) -> None:
        if not self.game_map or not self.player:
            return
        ex, ey = self.game_map.exit_tile
        if self.player.distance_sq_to(ex + 0.5, ey + 0.5) > 1.85**2:
            return
        self.floor += 1
        self.game_map = GameMap(floor=self.floor, seed=1000 + self.floor)
        self.player.x = self.game_map.start_tile[0] + 0.5
        self.player.y = self.game_map.start_tile[1] + 0.5
        self.enemies = self.ai.spawn_enemies(self.game_map, self.floor)
        self.ground_items.clear()

    def _try_interact(self) -> None:
        if not self.player:
            return
        if self.game_map and self._near_portal():
            self._try_use_portal()
            return
        for item in list(self.ground_items):
            if self.player.distance_to(item) < 1.2:
                if item.item_id == "potion":
                    self.player.stats.hp = min(self.player.stats.max_hp, self.player.stats.hp + 40)
                elif item.item_id == "sword_1":
                    if self.player.inventory.add("sword_1"):
                        self.player.equipment.equip("sword_1", "weapon", {"damage": 8.0})
                self.ground_items.remove(item)
                break

    def _near_portal(self) -> bool:
        if not self.game_map or not self.player:
            return False
        ex, ey = self.game_map.exit_tile
        return self.player.distance_sq_to(ex + 0.5, ey + 0.5) <= 1.85**2

    def run(self) -> None:
        while self.running:
            dt = min(self.clock.tick(FPS) / 1000.0, 0.05)
            self._handle_events()
            self._update(dt)
            self._draw()
            pygame.display.flip()
        pygame.quit()

    def _handle_events(self) -> None:
        inp = self.input
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                self.running = False
            elif event.type == pygame.KEYDOWN:
                inp.on_key_down(event.key)
            elif event.type == pygame.KEYUP:
                inp.on_key_up(event.key)
            elif event.type == pygame.MOUSEBUTTONDOWN:
                inp.on_mouse_down(event.button)
            elif event.type == pygame.MOUSEBUTTONUP:
                inp.on_mouse_up(event.button)
            elif event.type == pygame.MOTION:
                inp.on_mouse_motion(event.pos)

        state = inp.state
        if self.state == GameState.MENU:
            if state.menu_up:
                self.menu_selected = (self.menu_selected - 1) % 2
            if state.menu_down:
                self.menu_selected = (self.menu_selected + 1) % 2
            if state.menu_confirm:
                if self.menu_selected == 0:
                    self._start_new_game()
                    self.state = GameState.PLAYING
                else:
                    self.running = False
        elif self.state == GameState.PLAYING:
            if state.menu_back:
                self.state = GameState.PAUSED
            if state.inventory_pressed:
                self.show_inventory = not self.show_inventory
        elif self.state == GameState.PAUSED:
            if state.menu_back:
                self.state = GameState.PLAYING
        elif self.state == GameState.GAME_OVER:
            if state.menu_back:
                self.state = GameState.MENU
                self.menu_selected = 0

        inp.end_frame()

    def _update(self, dt: float) -> None:
        if self.state == GameState.PLAYING and self.player and self.game_map:
            self._update_playing(dt)

    def _update_playing(self, dt: float) -> None:
        assert self.player and self.game_map
        inp = self.input.state

        if inp.dash_pressed:
            self.movement.try_dash(self.player, self.game_map, inp)

        self.movement.update(self.player, self.game_map, inp, dt)
        self.camera.follow(self.player.x, self.player.y, dt)
        mouse_world = screen_to_world(*inp.mouse_pos, self.camera.x, self.camera.y)

        if inp.attack:
            self.combat.try_attack(self.player, self.enemies, mouse_world)
        if inp.skill_pressed == "fireball":
            self.skills.cast_fireball(self.player, self.enemies, mouse_world)
        if inp.interact_pressed:
            self._try_interact()

        self.player.attack_timer = max(0, self.player.attack_timer - dt)
        self.player.stats.mana = min(self.player.stats.max_mana, self.player.stats.mana + 8 * dt)

        self.combat.update(dt)
        self.skills.update(dt, self.player, self.enemies)
        self.ai.update(self.enemies, self.player, self.game_map, dt)

        if self.player.stats.hp <= 0:
            self.state = GameState.GAME_OVER

    def _draw(self) -> None:
        self.screen.fill((12, 14, 26))

        if self.state == GameState.MENU:
            self.menu_ui.draw(self.screen, self.menu_selected)
            return

        if self.game_map and self.player:
            self.map_renderer.draw(self.game_map, self.camera.x, self.camera.y)
            for item in self.ground_items:
                self.renderer.draw_ground_item(item, self.camera.x, self.camera.y)
            for enemy in self.enemies:
                if enemy.alive:
                    self.renderer.draw_entity_circle(
                        enemy.x,
                        enemy.y,
                        self.camera.x,
                        self.camera.y,
                        12,
                        (220, 70, 70),
                    )
                    self.renderer.draw_enemy_hp(enemy, self.camera.x, self.camera.y)
            for slash in self.combat.slashes:
                self.renderer.draw_slash(slash, self.camera.x, self.camera.y)
            for projectile in self.skills.projectiles:
                self.renderer.draw_projectile(projectile, self.camera.x, self.camera.y)
            self.renderer.draw_entity_circle(
                self.player.x,
                self.player.y,
                self.camera.x,
                self.camera.y,
                14,
                (80, 200, 120),
            )

            portal_hint = "E — портал" if self._near_portal() else None
            self.hud.draw(self.player, self.floor, portal_hint)

            if self.show_inventory:
                self._draw_inventory_overlay()

        if self.state == GameState.PAUSED:
            self.menu_ui.draw_pause(self.screen)
        elif self.state == GameState.GAME_OVER:
            self.menu_ui.draw_game_over(self.screen)

    def _draw_inventory_overlay(self) -> None:
        from core.config import SCREEN_HEIGHT, SCREEN_WIDTH, UI_TEXT

        assert self.player
        overlay = pygame.Surface((520, 420), pygame.SRCALPHA)
        overlay.fill((12, 14, 26, 220))
        ox = (SCREEN_WIDTH - 520) // 2
        oy = (SCREEN_HEIGHT - 420) // 2
        self.screen.blit(overlay, (ox, oy))
        font = pygame.font.SysFont("Segoe UI", 16)
        title = font.render("Инвентарь (I — закрыть)", True, UI_TEXT)
        self.screen.blit(title, (ox + 24, oy + 16))
        for i, slot in enumerate(self.player.inventory.slots[:12]):
            col, row = i % 4, i // 4
            x, y = ox + 24 + col * 118, oy + 48 + row * 72
            pygame.draw.rect(self.screen, (40, 48, 72), (x, y, 100, 60), border_radius=6)
            if slot:
                label = font.render(slot, True, UI_TEXT)
                self.screen.blit(label, (x + 8, y + 20))
import pygame

from core.config import FPS, SCREEN_HEIGHT, SCREEN_WIDTH, TITLE, GameState
from core.event_bus import EventBus
from engine.camera import Camera
from engine.input_handler import InputHandler
from engine.map_renderer import MapRenderer
from engine.renderer import Renderer, screen_to_world
from entities.item import GroundItem
from entities.player import PlayerEntity
from systems.ai_system import AISystem
from systems.combat_system import CombatSystem
from systems.loot_system import LootSystem
from systems.movement_system import MovementSystem
from systems.skill_system import SkillSystem
from ui.hud import HUD
from ui.menu import MenuUI
from world.map import GameMap


class Game:
    def __init__(self) -> None:
        pygame.init()
        self.screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
        pygame.display.set_caption(TITLE)
        self.clock = pygame.time.Clock()
        self.running = True
        self.events = EventBus()
        self.renderer = Renderer(self.screen)
        self.map_renderer = MapRenderer(self.renderer)
        self.camera = Camera()
        self.input = InputHandler()
        self.movement = MovementSystem()
        self.combat = CombatSystem(self.events)
        self.skills = SkillSystem(self.events)
        self.ai = AISystem()
        self.loot = LootSystem(self.events)
        self.hud = HUD(self.renderer)
        self.menu_ui = MenuUI()
        self.state = GameState.MENU
        self.menu_selected = 0
        self.show_inventory = False
        self.floor = 1
        self.player: PlayerEntity | None = None
        self.game_map: GameMap | None = None
        self.enemies = []
        self.ground_items: list[GroundItem] = []
        self._wire_events()

    def _wire_events(self) -> None:
        self.events.subscribe("item_dropped", self._on_item_dropped)
        self.events.subscribe("enemy_killed", self._on_enemy_killed)

    def _on_item_dropped(self, item: GroundItem, **_) -> None:
        self.ground_items.append(item)

    def _on_enemy_killed(self, enemy, killer, **_) -> None:
        if killer is None or self.player is None or self.game_map is None:
            return
        xp = 12 + self.game_map.floor * 2
        ups = killer.experience.add_xp(xp)
        for _ in range(ups):
            killer.stats.on_level_up()
        self.events.emit("level_up", player=killer, levels=ups)

    def _start_new_game(self) -> None:
        self.floor = 1
        self.game_map = GameMap(floor=self.floor, seed=42)
        self.player = PlayerEntity(
            self.game_map.start_tile[0] + 0.5,
            self.game_map.start_tile[1] + 0.5,
        )
        self.enemies = self.ai.spawn_enemies(self.game_map, self.floor)
        self.ground_items.clear()
        self.show_inventory = False

    def _try_use_portal(self) -> None:
        if not self.game_map or not self.player:
            return
        ex, ey = self.game_map.exit_tile
        if self.player.distance_sq_to(ex + 0.5, ey + 0.5) > 1.85**2:
            return
        self.floor += 1
        self.game_map = GameMap(floor=self.floor, seed=1000 + self.floor)
        self.player.x = self.game_map.start_tile[0] + 0.5
        self.player.y = self.game_map.start_tile[1] + 0.5
        self.enemies = self.ai.spawn_enemies(self.game_map, self.floor)
        self.ground_items.clear()

    def _try_interact(self) -> None:
        if not self.player:
            return
        if self.game_map and self._near_portal():
            self._try_use_portal()
            return
        for item in list(self.ground_items):
            if self.player.distance_to(item) < 1.2:
                if item.item_id == "potion":
                    self.player.stats.hp = min(self.player.stats.max_hp, self.player.stats.hp + 40)
                elif item.item_id == "sword_1":
                    if self.player.inventory.add("sword_1"):
                        self.player.equipment.equip("sword_1", "weapon", {"damage": 8.0})
                self.ground_items.remove(item)
                break

    def _near_portal(self) -> bool:
        if not self.game_map or not self.player:
            return False
        ex, ey = self.game_map.exit_tile
        return self.player.distance_sq_to(ex + 0.5, ey + 0.5) <= 1.85**2

    def run(self) -> None:
        while self.running:
            dt = min(self.clock.tick(FPS) / 1000.0, 0.05)
            self._handle_events()
            self._update(dt)
            self._draw()
            pygame.display.flip()
        pygame.quit()

    def _handle_events(self) -> None:
        inp = self.input
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                self.running = False
            elif event.type == pygame.KEYDOWN:
                inp.on_key_down(event.key)
            elif event.type == pygame.KEYUP:
                inp.on_key_up(event.key)
            elif event.type == pygame.MOUSEBUTTONDOWN:
                inp.on_mouse_down(event.button)
            elif event.type == pygame.MOUSEBUTTONUP:
                inp.on_mouse_up(event.button)
            elif event.type == pygame.MOTION:
                inp.on_mouse_motion(event.pos)

        state = inp.state
        if self.state == GameState.MENU:
            if state.menu_up:
                self.menu_selected = (self.menu_selected - 1) % 2
            if state.menu_down:
                self.menu_selected = (self.menu_selected + 1) % 2
            if state.menu_confirm:
                if self.menu_selected == 0:
                    self._start_new_game()
                    self.state = GameState.PLAYING
                else:
                    self.running = False
        elif self.state == GameState.PLAYING:
            if state.menu_back:
                self.state = GameState.PAUSED
            if state.inventory_pressed:
                self.show_inventory = not self.show_inventory
        elif self.state == GameState.PAUSED:
            if state.menu_back:
                self.state = GameState.PLAYING
        elif self.state == GameState.GAME_OVER:
            if state.menu_back:
                self.state = GameState.MENU
                self.menu_selected = 0

        inp.end_frame()

    def _update(self, dt: float) -> None:
        if self.state == GameState.PLAYING and self.player and self.game_map:
            self._update_playing(dt)

    def _update_playing(self, dt: float) -> None:
        assert self.player and self.game_map
        inp = self.input.state

        if inp.dash_pressed:
            self.movement.try_dash(self.player, self.game_map, inp)

        self.movement.update(self.player, self.game_map, inp, dt)
        self.camera.follow(self.player.x, self.player.y, dt)
        mouse_world = screen_to_world(*inp.mouse_pos, self.camera.x, self.camera.y)

        if inp.attack:
            self.combat.try_attack(self.player, self.enemies, mouse_world)
        if inp.skill_pressed == "fireball":
            self.skills.cast_fireball(self.player, self.enemies, mouse_world)
        if inp.interact_pressed:
            self._try_interact()

        self.player.attack_timer = max(0, self.player.attack_timer - dt)
        self.player.stats.mana = min(self.player.stats.max_mana, self.player.stats.mana + 8 * dt)

        self.combat.update(dt)
        self.skills.update(dt, self.player, self.enemies)
        self.ai.update(self.enemies, self.player, self.game_map, dt)

        if self.player.stats.hp <= 0:
            self.state = GameState.GAME_OVER

    def _draw(self) -> None:
        self.screen.fill((12, 14, 26))

        if self.state == GameState.MENU:
            self.menu_ui.draw(self.screen, self.menu_selected)
            return

        if self.game_map and self.player:
            self.map_renderer.draw(self.game_map, self.camera.x, self.camera.y)
            for item in self.ground_items:
                self.renderer.draw_ground_item(item, self.camera.x, self.camera.y)
            for enemy in self.enemies:
                if enemy.alive:
                    self.renderer.draw_entity_circle(
                        enemy.x,
                        enemy.y,
                        self.camera.x,
                        self.camera.y,
                        12,
                        (220, 70, 70),
                    )
                    self.renderer.draw_enemy_hp(enemy, self.camera.x, self.camera.y)
            for slash in self.combat.slashes:
                self.renderer.draw_slash(slash, self.camera.x, self.camera.y)
            for projectile in self.skills.projectiles:
                self.renderer.draw_projectile(projectile, self.camera.x, self.camera.y)
            self.renderer.draw_entity_circle(
                self.player.x,
                self.player.y,
                self.camera.x,
                self.camera.y,
                14,
                (80, 200, 120),
            )

            portal_hint = "E — портал" if self._near_portal() else None
            self.hud.draw(self.player, self.floor, portal_hint)

            if self.show_inventory:
                self._draw_inventory_overlay()

        if self.state == GameState.PAUSED:
            self.menu_ui.draw_pause(self.screen)
        elif self.state == GameState.GAME_OVER:
            self.menu_ui.draw_game_over(self.screen)

    def _draw_inventory_overlay(self) -> None:
        from core.config import SCREEN_HEIGHT, SCREEN_WIDTH, UI_TEXT

        assert self.player
        overlay = pygame.Surface((520, 420), pygame.SRCALPHA)
        overlay.fill((12, 14, 26, 220))
        ox = (SCREEN_WIDTH - 520) // 2
        oy = (SCREEN_HEIGHT - 420) // 2
        self.screen.blit(overlay, (ox, oy))
        font = pygame.font.SysFont("Segoe UI", 16)
        title = font.render("Инвентарь (I — закрыть)", True, UI_TEXT)
        self.screen.blit(title, (ox + 24, oy + 16))
        for i, slot in enumerate(self.player.inventory.slots[:12]):
            col, row = i % 4, i // 4
            x, y = ox + 24 + col * 118, oy + 48 + row * 72
            pygame.draw.rect(self.screen, (40, 48, 72), (x, y, 100, 60), border_radius=6)
            if slot:
                label = font.render(slot, True, UI_TEXT)
                self.screen.blit(label, (x + 8, y + 20))