"""Small playable demo that uses ``src.map.Map`` and the actor system. Controls: - WASD / arrow keys: move Knoll around the map - Space: switch between good/normal and bad/angry sprite - Esc / Q: quit """ from __future__ import annotations from pathlib import Path from typing import Optional import pygame from src.actors import Actor from src.map import Map, color_void, screen_h, screen_w, tile_size from src.settings import DEMO_ACTOR_HEIGHT, FPS, PLAYER_SPEED from src.startscreen import Startscreen from src.sounds import SoundManager def tile_center(column: int, row: int) -> tuple[int, int]: """Return the pixel center of a map tile.""" return column * tile_size + tile_size // 2, row * tile_size + tile_size // 2 class Game: def __init__(self) -> None: pygame.init() self.screen = pygame.display.set_mode((screen_w, screen_h)) self.clock = pygame.time.Clock() self.font = pygame.font.SysFont("monospace", 22, bold=True) self.running = True self.active_monster = None # --- SOUND & SYSTEMS --- self.sound_manager = SoundManager() self.startscreen = Startscreen(self.screen) self.state = "startscreen" self.player_name: str = "" self.map = Map() assets_dir = Path(__file__).resolve().parent.parent / "assets" actor_sides = Actor.load_sprite_sides(assets_dir, target_height=DEMO_ACTOR_HEIGHT) if not actor_sides: raise RuntimeError(f"Keine Sprites in {assets_dir} gefunden!") self.player = Actor.from_sides(actor_sides[0], self.find_start_position()) def run(self, max_frames: Optional[int] = None) -> None: frame_count = 0 while self.running and (max_frames is None or frame_count < max_frames): dt = min(self.clock.tick(FPS) / 1000.0, 0.05) if self.state == "startscreen": next_state = self.startscreen.run() if next_state == "quit": self.running = False elif next_state == "game": self.player_name = self.startscreen.player_name self.state = "game" self.sound_manager.play_bgm("normal") elif self.state == "game": self.handle_events() self.update(dt) self.draw() pygame.display.flip() frame_count += 1 pygame.quit() def handle_events(self) -> None: for event in pygame.event.get(): if event.type == pygame.QUIT: self.running = False elif event.type == pygame.KEYDOWN: if event.key in (pygame.K_ESCAPE, pygame.K_q): self.running = False elif event.key == pygame.K_SPACE: self.player.toggle_side() self.sound_manager.play_sfx("monster_transform") def update(self, dt: float) -> None: if self.player.is_bad: self.active_monster = self.player self.sound_manager.play_bgm("creepy") else: self.active_monster = None self.sound_manager.play_bgm("normal") movement = self.get_player_movement(dt) self.player.move_by(movement) if not self.is_position_walkable(self.player.position): self.player.hit_wall() self.player.update_animation(dt) def get_player_movement(self, dt: float) -> pygame.Vector2: keys = pygame.key.get_pressed() movement = pygame.Vector2( (keys[pygame.K_RIGHT] or keys[pygame.K_d]) - (keys[pygame.K_LEFT] or keys[pygame.K_a]), (keys[pygame.K_DOWN] or keys[pygame.K_s]) - (keys[pygame.K_UP] or keys[pygame.K_w]), ) if movement.length_squared() == 0: return movement if abs(movement.x) > abs(movement.y): self.player.direction = "right" if movement.x > 0 else "left" else: self.player.direction = "down" if movement.y > 0 else "up" return movement.normalize() * PLAYER_SPEED * dt def is_position_walkable(self, position: pygame.Vector2) -> bool: column = int(position.x // tile_size) row = int(position.y // tile_size) return self.map.is_accessible(column, row) def find_start_position(self) -> tuple[int, int]: for row_index, row in enumerate(self.map.map): column_index = row.find("S") if column_index != -1: return ( column_index * tile_size + tile_size // 2, (row_index + 1) * tile_size + tile_size // 2, ) return tile_size, tile_size def draw(self) -> None: self.screen.fill(color_void) self.map.draw_map(self.screen, self.font) for actor in sorted([self.player], key=lambda item: item.position.y): actor.draw(self.screen)