diff --git a/main.py b/main.py index 808775c..82a129b 100644 --- a/main.py +++ b/main.py @@ -1,5 +1,9 @@ +from src.game import Game + + def main(): - print("Hello from silent-fh-squad-g!") + game = Game() + game.run() if __name__ == "__main__": diff --git a/src/game.py b/src/game.py new file mode 100644 index 0000000..4425c6e --- /dev/null +++ b/src/game.py @@ -0,0 +1,151 @@ +"""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 + +The demo also places a few bad/angry actors as monsters in the rooms. +""" + +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 + + +# Keep the actor close to the map scale. The map tiles are 30x30, so 2 tiles +# high makes the character readable without covering whole rooms/walls. + + +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.startscreen = Startscreen(self.screen) + self.state = "startscreen" + self.player_name: str = "" + + self.map = Map() + + # resolve the assets from the folder + 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("No normal_*/angry_* actor sprite pairs found in assets/") + + # Use the first pair for the player. With current assets this is Knoll. + self.player = Actor.from_sides(actor_sides[0], self.find_start_position()) + + def run(self, max_frames: Optional[int] = None) -> None: + """Run the demo. ``max_frames`` is only used for automated smoke tests.""" + + 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" + 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() + + def update(self, dt: float) -> None: + 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: + """Check if the actor's feet are standing on a walkable map tile.""" + + 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]: + """Find the first S tile and place the actor's feet near its center.""" + + for row_index, row in enumerate(self.map.map): + column_index = row.find("S") + if column_index != -1: + # The S tile is at the very top of the current map, so place the + # actor one tile lower to keep the sprite visible on screen. + 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) + + + # Draw by Y position so actors lower on the map appear in front. + for actor in sorted([self.player], key=lambda item: item.position.y): + actor.draw(self.screen) + diff --git a/src/settings.py b/src/settings.py index 7cf9232..67a7a48 100644 --- a/src/settings.py +++ b/src/settings.py @@ -1,6 +1,8 @@ # --- DESK --- from typing import Mapping, Literal, Tuple +from src.map import tile_size + DESK_WIDTH = 70 DESK_HEIGHT = 40 @@ -91,6 +93,10 @@ COLOR_HP_BAR_YELLOW = (255, 255, 0) HUD_BAR_WIDTH = 100 HUD_BAR_HEIGHT = 15 +PLAYER_SPEED = 180.0 +DEMO_ACTOR_HEIGHT = tile_size * 3 +FPS = 60 + HUD_X_OFFSET = 20 # left startpoint for texts HUD_BAR_X = 150 # left startpoint for hp bars diff --git a/src/startscreen.py b/src/startscreen.py index 1e9c34a..fddb731 100644 --- a/src/startscreen.py +++ b/src/startscreen.py @@ -1,11 +1,13 @@ import pygame -from button import Button -from settings import (COLOR_BUTTON, - COLOR_BUTTON_HOVER, - COLOR_STARTSCREEN_BACKGROUND, - COLOR_STARTSCREEN_FONT_TITLE, - COLOR_STARTSCREEN_FONT_LABEL) -from text_input import TextInput +from src.button import Button +from src.settings import ( + COLOR_BUTTON, + COLOR_BUTTON_HOVER, + COLOR_STARTSCREEN_BACKGROUND, + COLOR_STARTSCREEN_FONT_TITLE, + COLOR_STARTSCREEN_FONT_LABEL, +) +from src.text_input import TextInput class Startscreen: def __init__(self, screen):