- add SoundManager class to handle bgm and sfx - add script to generate wav sound assets - integrate sfx for shop purchases and player transform - add dynamic bgm switching based on player state - add sound file paths and volume configurations to settings
146 lines
4.9 KiB
Python
146 lines
4.9 KiB
Python
"""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)
|