feat: implement game loop for monster interaction
This commit is contained in:
+207
-11
@@ -1,8 +1,8 @@
|
|||||||
"""Small playable demo that uses ``src.map.Map`` and the actor system.
|
"""Small playable demo that uses ``src.map.Map`` and the actor system.
|
||||||
|
|
||||||
Controls:
|
Controls:
|
||||||
- WASD / arrow keys: move Knoll around the map
|
- WASD / arrow keys: move Niklas around the map
|
||||||
- Space: switch between good/normal and bad/angry sprite
|
- Space: toggle player state
|
||||||
- Esc / Q: quit
|
- Esc / Q: quit
|
||||||
|
|
||||||
The demo also places a few bad/angry actors as monsters in the rooms.
|
The demo also places a few bad/angry actors as monsters in the rooms.
|
||||||
@@ -16,9 +16,15 @@ from typing import Optional
|
|||||||
import pygame
|
import pygame
|
||||||
|
|
||||||
from src.actors import Actor
|
from src.actors import Actor
|
||||||
|
from src.door import Door
|
||||||
|
from src.ects import ECTS
|
||||||
|
from src.endscreen import Endscreen
|
||||||
|
from src.hud import HUD
|
||||||
from src.map import Map, color_void, screen_h, screen_w, tile_size
|
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.monster import MonsterManager
|
||||||
|
from src.settings import DEMO_ACTOR_HEIGHT, DOOR_HEIGHT, DOOR_HP, DOOR_WIDTH, FPS, PLAYER_SPEED
|
||||||
from src.startscreen import Startscreen
|
from src.startscreen import Startscreen
|
||||||
|
from src.tower import Tower
|
||||||
|
|
||||||
|
|
||||||
# Keep the actor close to the map scale. The map tiles are 30x30, so 2 tiles
|
# Keep the actor close to the map scale. The map tiles are 30x30, so 2 tiles
|
||||||
@@ -40,20 +46,34 @@ class Game:
|
|||||||
self.running = True
|
self.running = True
|
||||||
|
|
||||||
self.startscreen = Startscreen(self.screen)
|
self.startscreen = Startscreen(self.screen)
|
||||||
|
self.hud = HUD()
|
||||||
|
self.ects = ECTS()
|
||||||
|
self.tower = Tower()
|
||||||
self.state = "startscreen"
|
self.state = "startscreen"
|
||||||
self.player_name: str = ""
|
self.player_name: str = ""
|
||||||
|
self.game_start_time = 0
|
||||||
|
self.endscreen: Optional[Endscreen] = None
|
||||||
|
|
||||||
self.map = Map()
|
self.map = Map()
|
||||||
|
self.active_door: Optional[Door] = None
|
||||||
|
self.doors_by_room: dict[tuple[int, int, int, int], Door] = {}
|
||||||
|
|
||||||
# resolve the assets from the folder
|
# Use Niklas as the player sprite.
|
||||||
assets_dir = Path(__file__).resolve().parent.parent / "assets"
|
assets_dir = Path(__file__).resolve().parent.parent / "assets"
|
||||||
actor_sides = Actor.load_sprite_sides(assets_dir, target_height=DEMO_ACTOR_HEIGHT)
|
player_asset = Actor.load_sprite_asset(
|
||||||
|
assets_dir / "niklas_sprites",
|
||||||
|
target_height=DEMO_ACTOR_HEIGHT,
|
||||||
|
)
|
||||||
|
self.player = Actor(
|
||||||
|
name="Niklas",
|
||||||
|
good_asset=player_asset,
|
||||||
|
bad_asset=player_asset,
|
||||||
|
position=self.find_start_position(),
|
||||||
|
)
|
||||||
|
|
||||||
if not actor_sides:
|
monster_sides = Actor.load_sprite_sides(assets_dir, target_height=DEMO_ACTOR_HEIGHT)
|
||||||
raise RuntimeError("No normal_*/angry_* actor sprite pairs found in assets/")
|
self.monsters = MonsterManager(monster_sides, self.map)
|
||||||
|
self.monsters.spawn_initial(self.get_monster_spawn_blockers())
|
||||||
# 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:
|
def run(self, max_frames: Optional[int] = None) -> None:
|
||||||
"""Run the demo. ``max_frames`` is only used for automated smoke tests."""
|
"""Run the demo. ``max_frames`` is only used for automated smoke tests."""
|
||||||
@@ -68,11 +88,20 @@ class Game:
|
|||||||
self.running = False
|
self.running = False
|
||||||
elif next_state == "game":
|
elif next_state == "game":
|
||||||
self.player_name = self.startscreen.player_name
|
self.player_name = self.startscreen.player_name
|
||||||
|
self.game_start_time = pygame.time.get_ticks()
|
||||||
self.state = "game"
|
self.state = "game"
|
||||||
elif self.state == "game":
|
elif self.state == "game":
|
||||||
self.handle_events()
|
self.handle_events()
|
||||||
self.update(dt)
|
self.update(dt)
|
||||||
self.draw()
|
self.draw()
|
||||||
|
elif self.state == "endscreen":
|
||||||
|
next_state = self.endscreen.run() if self.endscreen is not None else "startscreen"
|
||||||
|
if next_state == "quit":
|
||||||
|
self.running = False
|
||||||
|
elif next_state == "startscreen":
|
||||||
|
self.state = "startscreen"
|
||||||
|
elif next_state == "game":
|
||||||
|
self.restart_game()
|
||||||
|
|
||||||
pygame.display.flip()
|
pygame.display.flip()
|
||||||
frame_count += 1
|
frame_count += 1
|
||||||
@@ -97,6 +126,16 @@ class Game:
|
|||||||
self.player.hit_wall()
|
self.player.hit_wall()
|
||||||
|
|
||||||
self.player.update_animation(dt)
|
self.player.update_animation(dt)
|
||||||
|
self.ects.update(self.is_player_at_desk())
|
||||||
|
self.handle_room_table_collision()
|
||||||
|
self.monsters.update(
|
||||||
|
dt,
|
||||||
|
self.player.position,
|
||||||
|
self.is_position_walkable,
|
||||||
|
self.get_monster_spawn_blockers(),
|
||||||
|
)
|
||||||
|
self.update_tower()
|
||||||
|
self.check_end_conditions()
|
||||||
|
|
||||||
def get_player_movement(self, dt: float) -> pygame.Vector2:
|
def get_player_movement(self, dt: float) -> pygame.Vector2:
|
||||||
keys = pygame.key.get_pressed()
|
keys = pygame.key.get_pressed()
|
||||||
@@ -124,6 +163,143 @@ class Game:
|
|||||||
row = int(position.y // tile_size)
|
row = int(position.y // tile_size)
|
||||||
return self.map.is_accessible(column, row)
|
return self.map.is_accessible(column, row)
|
||||||
|
|
||||||
|
def is_player_at_desk(self) -> bool:
|
||||||
|
"""Return True while the player's hitbox touches a desk in the current room."""
|
||||||
|
|
||||||
|
player_rect = self.get_actor_rect(self.player)
|
||||||
|
room_rect = self.map.get_current_room(player_rect)
|
||||||
|
if room_rect is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return any(desk.check_collision(player_rect) for desk in self.map.get_desks_for_room(room_rect))
|
||||||
|
|
||||||
|
def update_tower(self) -> None:
|
||||||
|
"""Spawn a tower when enough ECTS are available and let it attack the zombie."""
|
||||||
|
|
||||||
|
player_rect = self.get_actor_rect(self.player)
|
||||||
|
room_rect = self.map.get_current_room(player_rect)
|
||||||
|
monster = self.monsters.monsters[0] if self.monsters.monsters else None
|
||||||
|
if monster is None or not monster.is_alive:
|
||||||
|
self.tower.clear()
|
||||||
|
return
|
||||||
|
|
||||||
|
damage = self.tower.update(
|
||||||
|
room_rect,
|
||||||
|
self.get_tower_spawn_blockers(),
|
||||||
|
self.ects,
|
||||||
|
monster.rect,
|
||||||
|
)
|
||||||
|
if monster is not None and damage > 0:
|
||||||
|
monster.take_damage(damage)
|
||||||
|
if not monster.is_alive:
|
||||||
|
self.tower.clear()
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
def show_endscreen(self, won: bool) -> None:
|
||||||
|
self.endscreen = Endscreen(
|
||||||
|
self.screen,
|
||||||
|
self.player_name,
|
||||||
|
self.ects.ects_balance,
|
||||||
|
self.get_elapsed_seconds(),
|
||||||
|
won,
|
||||||
|
)
|
||||||
|
self.state = "endscreen"
|
||||||
|
|
||||||
|
def restart_game(self) -> None:
|
||||||
|
self.ects = ECTS()
|
||||||
|
self.tower.clear()
|
||||||
|
self.active_door = None
|
||||||
|
self.doors_by_room = {}
|
||||||
|
self.player.set_position(self.find_start_position())
|
||||||
|
self.monsters.clear()
|
||||||
|
self.monsters.spawn_initial(self.get_monster_spawn_blockers())
|
||||||
|
self.game_start_time = pygame.time.get_ticks()
|
||||||
|
self.endscreen = None
|
||||||
|
self.state = "game"
|
||||||
|
|
||||||
|
def handle_room_table_collision(self) -> None:
|
||||||
|
"""Close/draw the room door and wake monsters when player hits a desk."""
|
||||||
|
|
||||||
|
player_rect = self.get_actor_rect(self.player)
|
||||||
|
room_rect = self.map.get_current_room(player_rect)
|
||||||
|
if room_rect is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
for desk in self.map.get_desks_for_room(room_rect):
|
||||||
|
if desk.check_collision(player_rect):
|
||||||
|
door = self.get_or_create_door_for_room(room_rect)
|
||||||
|
if door.current_hp > 0:
|
||||||
|
door.is_closed = True
|
||||||
|
self.active_door = door
|
||||||
|
self.monsters.activate_for_room(room_rect, door)
|
||||||
|
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)
|
||||||
|
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]
|
||||||
|
|
||||||
|
def create_door_for_room(self, room_rect: pygame.Rect) -> Door:
|
||||||
|
"""Create a Door object centered on the map's D tiles for this room."""
|
||||||
|
|
||||||
|
door_tiles: list[pygame.Rect] = []
|
||||||
|
for row_index, row in enumerate(self.map.map):
|
||||||
|
for column_index, tile in enumerate(row):
|
||||||
|
if tile != "D":
|
||||||
|
continue
|
||||||
|
|
||||||
|
tile_rect = pygame.Rect(
|
||||||
|
column_index * tile_size,
|
||||||
|
row_index * tile_size,
|
||||||
|
tile_size,
|
||||||
|
tile_size,
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
room_rect.left <= tile_rect.centerx <= room_rect.right
|
||||||
|
and room_rect.bottom <= tile_rect.centery <= room_rect.bottom + 2 * tile_size
|
||||||
|
):
|
||||||
|
door_tiles.append(tile_rect)
|
||||||
|
|
||||||
|
if not door_tiles:
|
||||||
|
return Door(room_rect.centerx - DOOR_WIDTH // 2, room_rect.bottom, DOOR_HP)
|
||||||
|
|
||||||
|
door_area = door_tiles[0].copy()
|
||||||
|
for tile_rect in door_tiles[1:]:
|
||||||
|
door_area.union_ip(tile_rect)
|
||||||
|
|
||||||
|
return Door(
|
||||||
|
door_area.centerx - DOOR_WIDTH // 2,
|
||||||
|
door_area.centery - DOOR_HEIGHT // 2,
|
||||||
|
DOOR_HP,
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_monster_spawn_blockers(self) -> list[pygame.Rect]:
|
||||||
|
"""Rects that monsters should not spawn on."""
|
||||||
|
|
||||||
|
blockers = [self.get_actor_rect(self.player)]
|
||||||
|
for desk_list in self.map.desks.values():
|
||||||
|
blockers.extend(desk.rect for desk in desk_list)
|
||||||
|
return blockers
|
||||||
|
|
||||||
|
def get_tower_spawn_blockers(self) -> list[pygame.Rect]:
|
||||||
|
"""Rects that towers should not spawn on."""
|
||||||
|
|
||||||
|
blockers = self.get_monster_spawn_blockers()
|
||||||
|
if self.active_door is not None:
|
||||||
|
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)
|
||||||
|
return blockers
|
||||||
|
|
||||||
|
@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]:
|
def find_start_position(self) -> tuple[int, int]:
|
||||||
"""Find the first S tile and place the actor's feet near its center."""
|
"""Find the first S tile and place the actor's feet near its center."""
|
||||||
|
|
||||||
@@ -139,10 +315,30 @@ class Game:
|
|||||||
|
|
||||||
return tile_size, tile_size
|
return tile_size, tile_size
|
||||||
|
|
||||||
|
def get_elapsed_seconds(self) -> int:
|
||||||
|
if self.game_start_time == 0:
|
||||||
|
return 0
|
||||||
|
return (pygame.time.get_ticks() - self.game_start_time) // 1000
|
||||||
|
|
||||||
def draw(self) -> None:
|
def draw(self) -> None:
|
||||||
self.screen.fill(color_void)
|
self.screen.fill(color_void)
|
||||||
self.map.draw_map(self.screen, self.font)
|
self.map.draw_map(self.screen, self.font)
|
||||||
|
|
||||||
|
if self.active_door is not None and self.active_door.is_closed:
|
||||||
|
self.active_door.draw(self.screen)
|
||||||
|
|
||||||
|
self.tower.draw(self.screen, self.font)
|
||||||
|
|
||||||
# Draw by Y position so actors lower on the map appear in front.
|
# 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):
|
actors = [self.player, *self.monsters.actors]
|
||||||
|
for actor in sorted(actors, key=lambda item: item.position.y):
|
||||||
actor.draw(self.screen)
|
actor.draw(self.screen)
|
||||||
|
|
||||||
|
self.hud.draw(
|
||||||
|
self.screen,
|
||||||
|
self.player_name,
|
||||||
|
self.active_door,
|
||||||
|
self.monsters.monsters[0] if self.monsters.monsters else None,
|
||||||
|
self.ects,
|
||||||
|
self.get_elapsed_seconds(),
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user