implement missing stuff

This commit is contained in:
Niklas Haiden
2026-05-08 19:23:19 +02:00
17 changed files with 316 additions and 83 deletions
+91 -21
View File
@@ -4,8 +4,6 @@ Controls:
- WASD / arrow keys: move Niklas around the map
- Space: toggle player state
- Esc / Q: quit
The demo also places a few bad/angry actors as monsters in the rooms.
"""
from __future__ import annotations
@@ -20,20 +18,27 @@ from src.door import Door
from src.ects import ECTS
from src.endscreen import Endscreen
from src.hud import HUD
from src.item import Item, ItemManager
from src.map import Map, color_void, screen_h, screen_w, tile_size
from src.monster import MonsterManager
from src.settings import DEMO_ACTOR_HEIGHT, DOOR_HEIGHT, DOOR_HP, DOOR_WIDTH, FPS, PLAYER_SPEED
from src.settings import (
BEER_BOTTLE_ECTS_VALUE,
COFFEE_HEAL_VALUE,
DEMO_ACTOR_HEIGHT,
DOOR_HEIGHT,
DOOR_HP,
DOOR_WIDTH,
FPS,
PLAYER_SPEED,
)
from src.shop import Shop
from src.startscreen import Startscreen
from src.sounds import SoundManager
from src.tower import Tower
# 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
@@ -43,22 +48,30 @@ class Game:
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.item_font = pygame.font.SysFont("Arial", 16, bold=True)
self.running = True
self.active_monster = None
self.buy_requested = False
# --- SOUND & SYSTEMS ---
self.sound_manager = SoundManager()
self.startscreen = Startscreen(self.screen)
self.hud = HUD()
self.ects = ECTS()
self.tower = Tower()
self.item_manager = ItemManager()
self.shop = Shop(self.ects, self.item_manager.items, self.sound_manager)
self.tower = Tower(self.sound_manager)
self.state = "startscreen"
self.player_name: str = ""
self.game_start_time = 0
self.endscreen: Optional[Endscreen] = None
self.current_item_room_key: Optional[tuple[int, int, int, int]] = None
self.map = Map()
self.active_door: Optional[Door] = None
self.doors_by_room: dict[tuple[int, int, int, int], Door] = {}
# Use Niklas as the player sprite.
assets_dir = Path(__file__).resolve().parent.parent / "assets"
player_asset = Actor.load_sprite_asset(
assets_dir / "niklas_sprites",
@@ -76,8 +89,6 @@ class Game:
self.monsters.spawn_initial(self.get_monster_spawn_blockers())
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)
@@ -90,6 +101,8 @@ class Game:
self.player_name = self.startscreen.player_name
self.game_start_time = pygame.time.get_ticks()
self.state = "game"
self.sound_manager.play_bgm("normal")
elif self.state == "game":
self.handle_events()
self.update(dt)
@@ -117,8 +130,18 @@ class Game:
self.running = False
elif event.key == pygame.K_SPACE:
self.player.toggle_side()
self.sound_manager.play_sfx("monster_transform")
elif event.key == pygame.K_e:
self.buy_requested = True
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)
@@ -128,6 +151,7 @@ class Game:
self.player.update_animation(dt)
self.ects.update(self.is_player_at_desk())
self.handle_room_table_collision()
self.update_items()
self.monsters.update(
dt,
self.player.position,
@@ -144,7 +168,7 @@ class Game:
- (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
@@ -157,8 +181,6 @@ class Game:
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)
@@ -194,6 +216,41 @@ class Game:
if not monster.is_alive:
self.tower.clear()
def update_items(self) -> None:
"""Spawn room items and process one player purchase request."""
player_rect = self.get_actor_rect(self.player)
room_rect = self.map.get_current_room(player_rect)
room_key = self.get_room_key(room_rect) if room_rect is not None else None
if room_key != self.current_item_room_key:
self.item_manager.clear()
self.current_item_room_key = room_key
self.item_manager.update(room_rect, self.get_item_spawn_blockers())
if self.buy_requested:
self.try_buy_touching_item(player_rect)
self.buy_requested = False
def try_buy_touching_item(self, player_rect: pygame.Rect) -> None:
item = self.item_manager.get_touching_item(player_rect)
if item is None:
return
if self.shop.buy_item(item):
self.apply_item_effect(item)
self.item_manager.remove_item(item)
def apply_item_effect(self, item: Item) -> None:
if item.item_type == "beer":
self.ects.upgrade_rate(BEER_BOTTLE_ECTS_VALUE)
elif item.item_type == "coffee" and self.active_door is not None:
self.active_door.current_hp = min(
self.active_door.max_hp,
self.active_door.current_hp + COFFEE_HEAL_VALUE,
)
def check_end_conditions(self) -> None:
if self.active_door is not None and self.active_door.current_hp <= 0:
self.show_endscreen(won=False)
@@ -210,6 +267,9 @@ class Game:
def restart_game(self) -> None:
self.ects = ECTS()
self.shop = Shop(self.ects, self.item_manager.items, self.sound_manager)
self.item_manager.clear()
self.current_item_room_key = None
self.tower.clear()
self.active_door = None
self.doors_by_room = {}
@@ -238,7 +298,7 @@ class Game:
return
def get_or_create_door_for_room(self, room_rect: pygame.Rect) -> Door:
key = (room_rect.x, room_rect.y, room_rect.width, room_rect.height)
key = self.get_room_key(room_rect)
if key not in self.doors_by_room:
self.doors_by_room[key] = self.create_door_for_room(room_rect)
return self.doors_by_room[key]
@@ -293,26 +353,35 @@ class Game:
blockers.append(pygame.Rect(self.active_door.x, self.active_door.y, DOOR_WIDTH, DOOR_HEIGHT))
for monster in self.monsters.monsters:
blockers.append(monster.rect)
blockers.extend(item.rect for item in self.item_manager.items)
return blockers
def get_item_spawn_blockers(self) -> list[pygame.Rect]:
"""Rects that room items should not spawn on."""
blockers = self.get_tower_spawn_blockers()
if self.tower.tower_rect is not None:
blockers.append(self.tower.tower_rect)
blockers.extend(item.rect for item in self.item_manager.items)
return blockers
@staticmethod
def get_room_key(room_rect: pygame.Rect) -> tuple[int, int, int, int]:
return room_rect.x, room_rect.y, room_rect.width, room_rect.height
@staticmethod
def get_actor_rect(actor: Actor) -> pygame.Rect:
frame = actor.get_frame()
return frame.get_rect(midbottom=(round(actor.position.x), round(actor.position.y)))
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 get_elapsed_seconds(self) -> int:
@@ -327,6 +396,7 @@ class Game:
if self.active_door is not None and self.active_door.is_closed:
self.active_door.draw(self.screen)
self.item_manager.draw(self.screen, 0, 0, self.item_font)
self.tower.draw(self.screen, self.font)
# Draw by Y position so actors lower on the map appear in front.