Merge branch 'feat/monsters' into 'main'
feat: implement game loop See merge request swd25-bootcamp/silent-fh!28
This commit is contained in:
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
import pygame
|
import pygame
|
||||||
from button import Button
|
from src.button import Button
|
||||||
from settings import (COLOR_ENDSCREEN_BACKGROUND,
|
from src.settings import (COLOR_ENDSCREEN_BACKGROUND,
|
||||||
COLOR_ENDSCREEN_TEXT,
|
COLOR_ENDSCREEN_TEXT,
|
||||||
COLOR_ENDSCREEN_WIN,
|
COLOR_ENDSCREEN_WIN,
|
||||||
COLOR_ENDSCREEN_LOSE,
|
COLOR_ENDSCREEN_LOSE,
|
||||||
|
|||||||
+279
-10
@@ -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
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -14,10 +14,27 @@ 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.item import Item, ItemManager
|
||||||
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 (
|
||||||
|
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.startscreen import Startscreen
|
||||||
from src.sounds import SoundManager
|
from src.sounds import SoundManager
|
||||||
|
from src.tower import Tower
|
||||||
|
|
||||||
|
|
||||||
def tile_center(column: int, row: int) -> tuple[int, int]:
|
def tile_center(column: int, row: int) -> tuple[int, int]:
|
||||||
@@ -31,26 +48,45 @@ class Game:
|
|||||||
self.screen = pygame.display.set_mode((screen_w, screen_h))
|
self.screen = pygame.display.set_mode((screen_w, screen_h))
|
||||||
self.clock = pygame.time.Clock()
|
self.clock = pygame.time.Clock()
|
||||||
self.font = pygame.font.SysFont("monospace", 22, bold=True)
|
self.font = pygame.font.SysFont("monospace", 22, bold=True)
|
||||||
|
self.item_font = pygame.font.SysFont("Arial", 16, bold=True)
|
||||||
self.running = True
|
self.running = True
|
||||||
|
|
||||||
self.active_monster = None
|
self.active_monster = None
|
||||||
|
self.buy_requested = False
|
||||||
|
|
||||||
# --- SOUND & SYSTEMS ---
|
# --- SOUND & SYSTEMS ---
|
||||||
self.sound_manager = SoundManager()
|
self.sound_manager = SoundManager()
|
||||||
self.startscreen = Startscreen(self.screen)
|
self.startscreen = Startscreen(self.screen)
|
||||||
|
self.hud = HUD()
|
||||||
|
self.ects = ECTS()
|
||||||
|
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.state = "startscreen"
|
||||||
self.player_name: str = ""
|
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.map = Map()
|
||||||
|
self.active_door: Optional[Door] = None
|
||||||
|
self.doors_by_room: dict[tuple[int, int, int, int], Door] = {}
|
||||||
|
|
||||||
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(f"Keine Sprites in {assets_dir} gefunden!")
|
self.monsters = MonsterManager(monster_sides, self.map)
|
||||||
|
self.monsters.spawn_initial(self.get_monster_spawn_blockers())
|
||||||
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:
|
||||||
frame_count = 0
|
frame_count = 0
|
||||||
@@ -63,6 +99,7 @@ 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"
|
||||||
self.sound_manager.play_bgm("normal")
|
self.sound_manager.play_bgm("normal")
|
||||||
|
|
||||||
@@ -70,6 +107,14 @@ class 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
|
||||||
@@ -86,6 +131,8 @@ class Game:
|
|||||||
elif event.key == pygame.K_SPACE:
|
elif event.key == pygame.K_SPACE:
|
||||||
self.player.toggle_side()
|
self.player.toggle_side()
|
||||||
self.sound_manager.play_sfx("monster_transform")
|
self.sound_manager.play_sfx("monster_transform")
|
||||||
|
elif event.key == pygame.K_e:
|
||||||
|
self.buy_requested = True
|
||||||
|
|
||||||
def update(self, dt: float) -> None:
|
def update(self, dt: float) -> None:
|
||||||
if self.player.is_bad:
|
if self.player.is_bad:
|
||||||
@@ -102,6 +149,17 @@ 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.update_items()
|
||||||
|
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()
|
||||||
@@ -127,6 +185,195 @@ 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 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)
|
||||||
|
|
||||||
|
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.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 = {}
|
||||||
|
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 = 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]
|
||||||
|
|
||||||
|
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)
|
||||||
|
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]:
|
def find_start_position(self) -> tuple[int, int]:
|
||||||
for row_index, row in enumerate(self.map.map):
|
for row_index, row in enumerate(self.map.map):
|
||||||
column_index = row.find("S")
|
column_index = row.find("S")
|
||||||
@@ -137,9 +384,31 @@ 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)
|
||||||
|
|
||||||
for actor in sorted([self.player], key=lambda item: item.position.y):
|
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.
|
||||||
|
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(),
|
||||||
|
)
|
||||||
|
|||||||
+45
-15
@@ -3,20 +3,20 @@ from src.settings import (
|
|||||||
HUD_FONT, HUD_FONT_SIZE, HUD_FONT_BOLD,
|
HUD_FONT, HUD_FONT_SIZE, HUD_FONT_BOLD,
|
||||||
COLOR_HUD_TEXT, COLOR_HP_BAR_GREEN, COLOR_HP_BAR_RED, COLOR_HP_BAR_BG,
|
COLOR_HUD_TEXT, COLOR_HP_BAR_GREEN, COLOR_HP_BAR_RED, COLOR_HP_BAR_BG,
|
||||||
HUD_BAR_WIDTH, HUD_BAR_HEIGHT,
|
HUD_BAR_WIDTH, HUD_BAR_HEIGHT,
|
||||||
HUD_X_OFFSET, HUD_BAR_X, HUD_Y_DOOR, HUD_Y_ZOMBIE, HUD_Y_ECTS
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# draw status bar (background bar and the colored hp bar)
|
|
||||||
def _draw_status_bar(screen, x, y, current, max_val, color):
|
def _draw_status_bar(screen, x, y, current, max_val, color):
|
||||||
# bg bar is always the same
|
# bg bar is always the same
|
||||||
pygame.draw.rect(screen, COLOR_HP_BAR_BG, (x, y, HUD_BAR_WIDTH, HUD_BAR_HEIGHT))
|
pygame.draw.rect(screen, COLOR_HP_BAR_BG, (x, y, HUD_BAR_WIDTH, HUD_BAR_HEIGHT))
|
||||||
|
|
||||||
# hp bar
|
# hp bar
|
||||||
if max_val > 0:
|
if max_val > 0:
|
||||||
hp_width = (current / max_val) * HUD_BAR_WIDTH
|
current = max(0, min(current, max_val))
|
||||||
pygame.draw.rect((screen, color), (x, y, hp_width, HUD_BAR_HEIGHT))
|
hp_width = int((current / max_val) * HUD_BAR_WIDTH)
|
||||||
|
pygame.draw.rect(screen, color, (x, y, hp_width, HUD_BAR_HEIGHT))
|
||||||
|
|
||||||
|
|
||||||
# time formatter for MM:SS
|
|
||||||
def _format_time(total_seconds):
|
def _format_time(total_seconds):
|
||||||
mins = total_seconds // 60
|
mins = total_seconds // 60
|
||||||
secs = total_seconds % 60
|
secs = total_seconds % 60
|
||||||
@@ -26,22 +26,52 @@ def _format_time(total_seconds):
|
|||||||
class HUD:
|
class HUD:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.font = pygame.font.SysFont(HUD_FONT, HUD_FONT_SIZE, bold=HUD_FONT_BOLD)
|
self.font = pygame.font.SysFont(HUD_FONT, HUD_FONT_SIZE, bold=HUD_FONT_BOLD)
|
||||||
|
self.small_font = pygame.font.SysFont(HUD_FONT, max(16, HUD_FONT_SIZE - 4), bold=False)
|
||||||
|
|
||||||
|
def draw(self, screen, player_name, door, zombie, ects, total_time):
|
||||||
|
"""Draw the game HUD at the bottom of the screen."""
|
||||||
|
|
||||||
|
panel_height = 86
|
||||||
|
panel_rect = pygame.Rect(0, screen.get_height() - panel_height, screen.get_width(), panel_height)
|
||||||
|
pygame.draw.rect(screen, (10, 10, 15), panel_rect)
|
||||||
|
pygame.draw.rect(screen, (70, 70, 80), panel_rect, 2)
|
||||||
|
|
||||||
|
top = panel_rect.top + 12
|
||||||
|
label_y = top
|
||||||
|
bar_y = top + 34
|
||||||
|
|
||||||
|
# --- PLAYER ---
|
||||||
|
player_text = f"PLAYER: {player_name or 'Student'}"
|
||||||
|
screen.blit(self.font.render(player_text, True, COLOR_HUD_TEXT), (20, label_y))
|
||||||
|
|
||||||
def draw(self, screen, door, zombie, ects, total_time):
|
|
||||||
# --- DOOR HP ---
|
# --- DOOR HP ---
|
||||||
_draw_status_bar(screen, HUD_BAR_X, HUD_Y_DOOR, door.current_hp, COLOR_HP_BAR_GREEN)
|
door_x = 330
|
||||||
# -5 px, small off-set to align the text better with hp bar
|
screen.blit(self.font.render("DOOR HP", True, COLOR_HUD_TEXT), (door_x, label_y))
|
||||||
screen.blit(self.font.render("DOOR HP ", True, COLOR_HUD_TEXT), (HUD_X_OFFSET, HUD_Y_DOOR - 5))
|
if door is not None:
|
||||||
|
_draw_status_bar(screen, door_x, bar_y, door.current_hp, door.max_hp, COLOR_HP_BAR_GREEN)
|
||||||
|
hp_text = f"{door.current_hp}/{door.max_hp}"
|
||||||
|
else:
|
||||||
|
_draw_status_bar(screen, door_x, bar_y, 0, 1, COLOR_HP_BAR_GREEN)
|
||||||
|
hp_text = "--/--"
|
||||||
|
screen.blit(self.small_font.render(hp_text, True, COLOR_HUD_TEXT), (door_x + HUD_BAR_WIDTH + 10, bar_y - 2))
|
||||||
|
|
||||||
# --- ZOMBIE HP ---
|
# --- ZOMBIE HP ---
|
||||||
if zombie:
|
zombie_x = 620
|
||||||
_draw_status_bar(screen, HUD_BAR_X, HUD_Y_ZOMBIE, zombie.current_hp, zombie.max_hp, COLOR_HP_BAR_RED)
|
screen.blit(self.font.render("ZOMBIE HP", True, COLOR_HUD_TEXT), (zombie_x, label_y))
|
||||||
screen.blit(self.font.render("Zombie HP:", True, COLOR_HUD_TEXT), (HUD_X_OFFSET, HUD_Y_ZOMBIE - 5))
|
if zombie is not None:
|
||||||
|
_draw_status_bar(screen, zombie_x, bar_y, zombie.current_hp, zombie.max_hp, COLOR_HP_BAR_RED)
|
||||||
|
zombie_text = f"{zombie.current_hp}/{zombie.max_hp}"
|
||||||
|
else:
|
||||||
|
_draw_status_bar(screen, zombie_x, bar_y, 0, 1, COLOR_HP_BAR_RED)
|
||||||
|
zombie_text = "--/--"
|
||||||
|
screen.blit(self.small_font.render(zombie_text, True, COLOR_HUD_TEXT), (zombie_x + HUD_BAR_WIDTH + 10, bar_y - 2))
|
||||||
|
|
||||||
# --- ECTS ---
|
# --- ECTS ---
|
||||||
ects_info = f"ECTS: {ects.ects_balance} ({ects.ects_per_second}/s)"
|
ects_x = 910
|
||||||
screen.blit(self.font.render(ects_info, True, COLOR_HUD_TEXT), (HUD_X_OFFSET, HUD_Y_ECTS))
|
ects_text = f"ECTS: {ects.ects_balance} ({ects.ects_per_second}/s)"
|
||||||
|
screen.blit(self.font.render(ects_text, True, COLOR_HUD_TEXT), (ects_x, label_y))
|
||||||
|
|
||||||
# --- TIME ---
|
# --- TIME ---
|
||||||
time_str = _format_time(total_time)
|
time_str = _format_time(total_time)
|
||||||
screen.blit(self.font.render(time_str, True, COLOR_HUD_TEXT), (screen.get_width() - 150, HUD_Y_DOOR - 5))
|
time_surface = self.font.render(time_str, True, COLOR_HUD_TEXT)
|
||||||
|
screen.blit(time_surface, (screen.get_width() - time_surface.get_width() - 20, label_y))
|
||||||
|
|||||||
+27
-14
@@ -1,17 +1,27 @@
|
|||||||
import pygame
|
import pygame
|
||||||
import random
|
import random
|
||||||
from src.settings import (
|
from src.settings import (
|
||||||
ITEM_SPAWN_INTERVAL, ITEM_SHOPPING_TIMER, ITEM_SIZE,
|
BEER_COST,
|
||||||
COLOR_HP_BAR_YELLOW, COLOR_HP_BAR_BG
|
COFFEE_COST,
|
||||||
|
ITEM_SPAWN_INTERVAL,
|
||||||
|
ITEM_SHOPPING_TIMER,
|
||||||
|
ITEM_SIZE,
|
||||||
|
COLOR_HP_BAR_YELLOW,
|
||||||
|
COLOR_HP_BAR_BG,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
ITEM_COSTS = {
|
||||||
|
"beer": BEER_COST,
|
||||||
|
"coffee": COFFEE_COST,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class Item:
|
class Item:
|
||||||
def __init__(self, x, y, item_type):
|
def __init__(self, x, y, item_type):
|
||||||
self.x = x
|
self.x = x
|
||||||
self.y = y
|
self.y = y
|
||||||
self.item_type = item_type
|
self.item_type = item_type
|
||||||
self.cost = random.randint(5, 10)
|
self.cost = ITEM_COSTS.get(item_type, random.randint(5, 10))
|
||||||
|
|
||||||
# collision rect for player (2 times item size for width and height)
|
# collision rect for player (2 times item size for width and height)
|
||||||
self.rect = pygame.Rect(self.x, self.y, ITEM_SIZE, ITEM_SIZE)
|
self.rect = pygame.Rect(self.x, self.y, ITEM_SIZE, ITEM_SIZE)
|
||||||
@@ -33,7 +43,7 @@ class Item:
|
|||||||
return pygame.time.get_ticks() - self.spawn_time >= ITEM_SHOPPING_TIMER
|
return pygame.time.get_ticks() - self.spawn_time >= ITEM_SHOPPING_TIMER
|
||||||
|
|
||||||
# cam offset to convert world coordinates to screen coordinates
|
# cam offset to convert world coordinates to screen coordinates
|
||||||
def draw(self, screen, cam_x, cam_y):
|
def draw(self, screen, cam_x, cam_y, font=None):
|
||||||
draw_x = self.x - cam_x
|
draw_x = self.x - cam_x
|
||||||
draw_y = self.y - cam_y
|
draw_y = self.y - cam_y
|
||||||
|
|
||||||
@@ -51,6 +61,12 @@ class Item:
|
|||||||
pygame.draw.rect(screen, color, draw_rect)
|
pygame.draw.rect(screen, color, draw_rect)
|
||||||
pygame.draw.rect(screen, COLOR_HP_BAR_BG, draw_rect, 2)
|
pygame.draw.rect(screen, COLOR_HP_BAR_BG, draw_rect, 2)
|
||||||
|
|
||||||
|
if font is not None:
|
||||||
|
cost_surface = font.render(f"{self.cost} ECTS", True, (255, 255, 255))
|
||||||
|
prompt_surface = font.render("E", True, (255, 255, 255))
|
||||||
|
screen.blit(cost_surface, (draw_x - 10, draw_y - cost_surface.get_height() - 2))
|
||||||
|
screen.blit(prompt_surface, (draw_x + ITEM_SIZE // 2 - prompt_surface.get_width() // 2, draw_y + ITEM_SIZE + 2))
|
||||||
|
|
||||||
def get_cost(self):
|
def get_cost(self):
|
||||||
return self.cost
|
return self.cost
|
||||||
|
|
||||||
@@ -93,14 +109,7 @@ class ItemManager:
|
|||||||
def update(self, room_rect, blocked_rects):
|
def update(self, room_rect, blocked_rects):
|
||||||
now = pygame.time.get_ticks()
|
now = pygame.time.get_ticks()
|
||||||
|
|
||||||
remaining_items = []
|
self.items = [item for item in self.items if not item.is_expired()]
|
||||||
|
|
||||||
for item in self.items:
|
|
||||||
if item.is_expired() == False:
|
|
||||||
remaining_items.append(item)
|
|
||||||
|
|
||||||
# replace the old list with the new temp_list
|
|
||||||
self.items = remaining_items
|
|
||||||
|
|
||||||
# spawn logic for new items
|
# spawn logic for new items
|
||||||
if room_rect is not None:
|
if room_rect is not None:
|
||||||
@@ -116,6 +125,10 @@ class ItemManager:
|
|||||||
# check if player press "e" to buy
|
# check if player press "e" to buy
|
||||||
def check_collect(self, player_rect, keys):
|
def check_collect(self, player_rect, keys):
|
||||||
if keys[pygame.K_e]:
|
if keys[pygame.K_e]:
|
||||||
|
return self.get_touching_item(player_rect)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_touching_item(self, player_rect):
|
||||||
for item in self.items:
|
for item in self.items:
|
||||||
if player_rect.colliderect(item.rect):
|
if player_rect.colliderect(item.rect):
|
||||||
return item
|
return item
|
||||||
@@ -127,9 +140,9 @@ class ItemManager:
|
|||||||
self.items.remove(item)
|
self.items.remove(item)
|
||||||
|
|
||||||
# draw all active items
|
# draw all active items
|
||||||
def draw(self, screen, cam_x, cam_y):
|
def draw(self, screen, cam_x, cam_y, font=None):
|
||||||
for item in self.items:
|
for item in self.items:
|
||||||
item.draw(screen, cam_x, cam_y)
|
item.draw(screen, cam_x, cam_y, font)
|
||||||
|
|
||||||
# clear items when changing room
|
# clear items when changing room
|
||||||
def clear(self):
|
def clear(self):
|
||||||
|
|||||||
+386
@@ -0,0 +1,386 @@
|
|||||||
|
"""Monster spawning and hallway movement behaviour."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import random
|
||||||
|
from collections import deque
|
||||||
|
from typing import Callable, Iterable, Optional
|
||||||
|
|
||||||
|
import pygame
|
||||||
|
|
||||||
|
from src.actors import Actor, ActorSpriteSides
|
||||||
|
from src.door import Door
|
||||||
|
from src.settings import (
|
||||||
|
MONSTER_ATTACK_INTERVAL,
|
||||||
|
MONSTER_DOOR_DAMAGE,
|
||||||
|
MONSTER_HP,
|
||||||
|
MONSTER_MAX_COUNT,
|
||||||
|
MONSTER_SPAWN_INTERVAL,
|
||||||
|
MONSTER_SPEED,
|
||||||
|
tile_size,
|
||||||
|
)
|
||||||
|
|
||||||
|
WalkableCheck = Callable[[pygame.Vector2], bool]
|
||||||
|
GridPosition = tuple[int, int]
|
||||||
|
Position = tuple[int, int]
|
||||||
|
|
||||||
|
|
||||||
|
class Monster:
|
||||||
|
"""A spawned monster backed by a good/bad actor sprite pair."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
actor: Actor,
|
||||||
|
*,
|
||||||
|
max_hp: int = MONSTER_HP,
|
||||||
|
speed: float = MONSTER_SPEED,
|
||||||
|
) -> None:
|
||||||
|
self.actor = actor
|
||||||
|
self.actor.become_good()
|
||||||
|
self.max_hp = max_hp
|
||||||
|
self.current_hp = max_hp
|
||||||
|
self.speed = speed
|
||||||
|
self.last_attack_time = -MONSTER_ATTACK_INTERVAL
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_alive(self) -> bool:
|
||||||
|
return self.current_hp > 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def position(self) -> pygame.Vector2:
|
||||||
|
return self.actor.position
|
||||||
|
|
||||||
|
@property
|
||||||
|
def rect(self) -> pygame.Rect:
|
||||||
|
frame = self.actor.get_frame()
|
||||||
|
return frame.get_rect(midbottom=self._int_position(self.actor.position))
|
||||||
|
|
||||||
|
def take_damage(self, amount: int) -> None:
|
||||||
|
self.current_hp = max(0, self.current_hp - amount)
|
||||||
|
|
||||||
|
def sleep(self) -> None:
|
||||||
|
"""Stay friendly/idling until the player triggers the room."""
|
||||||
|
|
||||||
|
self.actor.become_good()
|
||||||
|
self.actor.stop_walking()
|
||||||
|
|
||||||
|
def wake_up(self) -> None:
|
||||||
|
"""Switch to the angry sprite when the player triggers a room."""
|
||||||
|
|
||||||
|
self.actor.become_bad()
|
||||||
|
|
||||||
|
def update(
|
||||||
|
self,
|
||||||
|
dt: float,
|
||||||
|
target_position: pygame.Vector2,
|
||||||
|
can_move_to: WalkableCheck,
|
||||||
|
) -> None:
|
||||||
|
"""Move toward the target, but only where ``can_move_to`` allows."""
|
||||||
|
|
||||||
|
if not self.is_alive:
|
||||||
|
self.actor.stop_walking()
|
||||||
|
return
|
||||||
|
|
||||||
|
movement = pygame.Vector2(target_position) - self.actor.position
|
||||||
|
if movement.length_squared() == 0:
|
||||||
|
self.actor.stop_walking()
|
||||||
|
self.actor.update_animation(dt)
|
||||||
|
return
|
||||||
|
|
||||||
|
self.face_position(target_position)
|
||||||
|
|
||||||
|
distance = movement.length()
|
||||||
|
max_distance = self.speed * dt
|
||||||
|
if distance > max_distance:
|
||||||
|
movement = movement.normalize() * max_distance
|
||||||
|
|
||||||
|
self.actor.move_by(movement)
|
||||||
|
if not can_move_to(self.actor.position):
|
||||||
|
self.actor.hit_wall()
|
||||||
|
|
||||||
|
self.actor.update_animation(dt)
|
||||||
|
|
||||||
|
def attack_door(self, door: Door) -> None:
|
||||||
|
"""Damage the active door while standing outside it."""
|
||||||
|
|
||||||
|
self.wake_up()
|
||||||
|
self.actor.stop_walking()
|
||||||
|
|
||||||
|
now = pygame.time.get_ticks()
|
||||||
|
if now - self.last_attack_time >= MONSTER_ATTACK_INTERVAL:
|
||||||
|
door.take_damage(MONSTER_DOOR_DAMAGE)
|
||||||
|
self.last_attack_time = now
|
||||||
|
|
||||||
|
def face_position(self, target_position: pygame.Vector2) -> None:
|
||||||
|
movement = pygame.Vector2(target_position) - self.actor.position
|
||||||
|
if movement.length_squared() == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
if abs(movement.x) > abs(movement.y):
|
||||||
|
self.actor.direction = "right" if movement.x > 0 else "left"
|
||||||
|
else:
|
||||||
|
self.actor.direction = "down" if movement.y > 0 else "up"
|
||||||
|
|
||||||
|
def draw(self, surface: pygame.Surface) -> pygame.Rect:
|
||||||
|
return self.actor.draw(surface)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _int_position(position: pygame.Vector2) -> tuple[int, int]:
|
||||||
|
return round(position.x), round(position.y)
|
||||||
|
|
||||||
|
|
||||||
|
class MonsterManager:
|
||||||
|
"""Loads monster sprites, spawns monsters on H hallway tiles, and updates them."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
sprite_sides: Iterable[ActorSpriteSides],
|
||||||
|
game_map,
|
||||||
|
*,
|
||||||
|
max_monsters: int = MONSTER_MAX_COUNT,
|
||||||
|
spawn_interval_ms: int = MONSTER_SPAWN_INTERVAL,
|
||||||
|
) -> None:
|
||||||
|
self.sprite_sides = list(sprite_sides)
|
||||||
|
self.map = game_map
|
||||||
|
self.max_monsters = max_monsters
|
||||||
|
self.spawn_interval_ms = spawn_interval_ms
|
||||||
|
self.monsters: list[Monster] = []
|
||||||
|
self.last_spawn_time = pygame.time.get_ticks()
|
||||||
|
self.active_target_tile: Optional[GridPosition] = None
|
||||||
|
self.active_door: Optional[Door] = None
|
||||||
|
|
||||||
|
if not self.sprite_sides:
|
||||||
|
raise RuntimeError("No normal_*/angry_* monster sprite pairs found in assets/")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def actors(self) -> list[Actor]:
|
||||||
|
return [monster.actor for monster in self.monsters]
|
||||||
|
|
||||||
|
def spawn_initial(self, blocked_rects: Optional[list[pygame.Rect]] = None) -> None:
|
||||||
|
"""Fill the game with the starting monsters on H hallway tiles."""
|
||||||
|
|
||||||
|
blockers = list(blocked_rects or [])
|
||||||
|
while len(self.monsters) < self.max_monsters:
|
||||||
|
monster = self.spawn_monster(blockers)
|
||||||
|
if monster is None:
|
||||||
|
break
|
||||||
|
blockers.append(monster.rect)
|
||||||
|
|
||||||
|
self.last_spawn_time = pygame.time.get_ticks()
|
||||||
|
|
||||||
|
def activate_for_room(self, room_rect: pygame.Rect, door: Door) -> None:
|
||||||
|
"""Wake monsters and send them to the hallway side of this room's door."""
|
||||||
|
|
||||||
|
target_tile = self.get_hall_tile_at_room_door(room_rect)
|
||||||
|
if target_tile is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.active_target_tile = target_tile
|
||||||
|
self.active_door = door
|
||||||
|
for monster in self.monsters:
|
||||||
|
monster.wake_up()
|
||||||
|
|
||||||
|
def update(
|
||||||
|
self,
|
||||||
|
dt: float,
|
||||||
|
player_position: pygame.Vector2,
|
||||||
|
is_position_walkable: WalkableCheck,
|
||||||
|
blocked_rects: Optional[list[pygame.Rect]] = None,
|
||||||
|
) -> None:
|
||||||
|
self.monsters = [monster for monster in self.monsters if monster.is_alive]
|
||||||
|
|
||||||
|
if self.active_target_tile is None:
|
||||||
|
for monster in self.monsters:
|
||||||
|
monster.sleep()
|
||||||
|
else:
|
||||||
|
for monster in self.monsters:
|
||||||
|
waypoint = self.get_next_waypoint(monster, self.active_target_tile)
|
||||||
|
if self.position_to_tile(monster.position) == self.active_target_tile:
|
||||||
|
if self.active_door is not None and self.active_door.is_closed:
|
||||||
|
monster.attack_door(self.active_door)
|
||||||
|
else:
|
||||||
|
monster.actor.stop_walking()
|
||||||
|
else:
|
||||||
|
monster.wake_up()
|
||||||
|
monster.update(dt, waypoint, self.is_hall_position)
|
||||||
|
|
||||||
|
now = pygame.time.get_ticks()
|
||||||
|
if len(self.monsters) < self.max_monsters and now - self.last_spawn_time >= self.spawn_interval_ms:
|
||||||
|
monster = self.spawn_monster(blocked_rects)
|
||||||
|
if monster is not None and self.active_target_tile is not None:
|
||||||
|
monster.wake_up()
|
||||||
|
self.last_spawn_time = now
|
||||||
|
|
||||||
|
def get_hall_tile_at_room_door(self, room_rect: pygame.Rect) -> Optional[GridPosition]:
|
||||||
|
"""Find the H tile directly outside the D-door of ``room_rect``."""
|
||||||
|
|
||||||
|
door_tiles: list[GridPosition] = []
|
||||||
|
for row_index, row in enumerate(self.map.map):
|
||||||
|
for column_index, tile in enumerate(row):
|
||||||
|
if tile != "D":
|
||||||
|
continue
|
||||||
|
|
||||||
|
tile_center_x = column_index * tile_size + tile_size // 2
|
||||||
|
tile_center_y = row_index * tile_size + tile_size // 2
|
||||||
|
if (
|
||||||
|
room_rect.left <= tile_center_x <= room_rect.right
|
||||||
|
and room_rect.bottom <= tile_center_y <= room_rect.bottom + 2 * tile_size
|
||||||
|
):
|
||||||
|
door_tiles.append((column_index, row_index))
|
||||||
|
|
||||||
|
hall_tiles: list[GridPosition] = []
|
||||||
|
for door_tile in door_tiles:
|
||||||
|
for neighbor in self.get_neighbors(door_tile, allowed_tiles={"H"}):
|
||||||
|
hall_tiles.append(neighbor)
|
||||||
|
|
||||||
|
if not hall_tiles:
|
||||||
|
return None
|
||||||
|
|
||||||
|
room_center_tile = (room_rect.centerx // tile_size, room_rect.centery // tile_size)
|
||||||
|
return min(
|
||||||
|
hall_tiles,
|
||||||
|
key=lambda tile: abs(tile[0] - room_center_tile[0]) + abs(tile[1] - room_center_tile[1]),
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_next_waypoint(self, monster: Monster, target_tile: GridPosition) -> pygame.Vector2:
|
||||||
|
"""Return the next H-tile center on a BFS path to the target tile."""
|
||||||
|
|
||||||
|
start_tile = self.position_to_tile(monster.position)
|
||||||
|
if start_tile == target_tile:
|
||||||
|
return self.tile_center(target_tile)
|
||||||
|
|
||||||
|
path = self.find_hall_path(start_tile, target_tile)
|
||||||
|
if len(path) >= 2:
|
||||||
|
return self.tile_center(path[1])
|
||||||
|
if len(path) == 1:
|
||||||
|
return self.tile_center(path[0])
|
||||||
|
|
||||||
|
return monster.position.copy()
|
||||||
|
|
||||||
|
def find_hall_path(self, start: GridPosition, target: GridPosition) -> list[GridPosition]:
|
||||||
|
"""Find a path using only H hallway tiles. D doors are never traversed."""
|
||||||
|
|
||||||
|
if self.map.get_tile(*start) != "H" or self.map.get_tile(*target) != "H":
|
||||||
|
return []
|
||||||
|
|
||||||
|
queue: deque[GridPosition] = deque([start])
|
||||||
|
came_from: dict[GridPosition, Optional[GridPosition]] = {start: None}
|
||||||
|
|
||||||
|
while queue:
|
||||||
|
current = queue.popleft()
|
||||||
|
if current == target:
|
||||||
|
break
|
||||||
|
|
||||||
|
for neighbor in self.get_neighbors(current, allowed_tiles={"H"}):
|
||||||
|
if neighbor in came_from:
|
||||||
|
continue
|
||||||
|
came_from[neighbor] = current
|
||||||
|
queue.append(neighbor)
|
||||||
|
|
||||||
|
if target not in came_from:
|
||||||
|
return []
|
||||||
|
|
||||||
|
path = [target]
|
||||||
|
while path[-1] != start:
|
||||||
|
previous = came_from[path[-1]]
|
||||||
|
if previous is None:
|
||||||
|
break
|
||||||
|
path.append(previous)
|
||||||
|
|
||||||
|
path.reverse()
|
||||||
|
return path
|
||||||
|
|
||||||
|
def get_neighbors(
|
||||||
|
self,
|
||||||
|
tile: GridPosition,
|
||||||
|
*,
|
||||||
|
allowed_tiles: set[str],
|
||||||
|
) -> list[GridPosition]:
|
||||||
|
column, row = tile
|
||||||
|
neighbors: list[GridPosition] = []
|
||||||
|
for next_column, next_row in (
|
||||||
|
(column + 1, row),
|
||||||
|
(column - 1, row),
|
||||||
|
(column, row + 1),
|
||||||
|
(column, row - 1),
|
||||||
|
):
|
||||||
|
if self.map.get_tile(next_column, next_row) in allowed_tiles:
|
||||||
|
neighbors.append((next_column, next_row))
|
||||||
|
return neighbors
|
||||||
|
|
||||||
|
def is_hall_position(self, position: pygame.Vector2) -> bool:
|
||||||
|
"""Monsters may move only on H hallway tiles, never through D doors."""
|
||||||
|
|
||||||
|
return self.map.get_tile(*self.position_to_tile(position)) == "H"
|
||||||
|
|
||||||
|
def spawn_monster(self, blocked_rects: Optional[list[pygame.Rect]] = None) -> Optional[Monster]:
|
||||||
|
"""Spawn one monster on a random H hallway tile if a free tile exists."""
|
||||||
|
|
||||||
|
if len(self.monsters) >= self.max_monsters:
|
||||||
|
return None
|
||||||
|
|
||||||
|
blockers = list(blocked_rects or [])
|
||||||
|
blockers.extend(monster.rect for monster in self.monsters)
|
||||||
|
|
||||||
|
sprite_sides = list(self.sprite_sides)
|
||||||
|
random.shuffle(sprite_sides)
|
||||||
|
|
||||||
|
for sides in sprite_sides:
|
||||||
|
position = self._random_position_on_hall_tile(sides.bad.frames["down"][1], blockers)
|
||||||
|
if position is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
actor = Actor.from_sides(sides, position)
|
||||||
|
monster = Monster(actor)
|
||||||
|
self.monsters.append(monster)
|
||||||
|
return monster
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def draw(self, surface: pygame.Surface) -> list[pygame.Rect]:
|
||||||
|
return [monster.draw(surface) for monster in sorted(self.monsters, key=lambda item: item.position.y)]
|
||||||
|
|
||||||
|
def clear(self) -> None:
|
||||||
|
self.monsters = []
|
||||||
|
self.last_spawn_time = pygame.time.get_ticks()
|
||||||
|
self.active_target_tile = None
|
||||||
|
self.active_door = None
|
||||||
|
|
||||||
|
def _random_position_on_hall_tile(
|
||||||
|
self,
|
||||||
|
frame: pygame.Surface,
|
||||||
|
blocked_rects: list[pygame.Rect],
|
||||||
|
) -> Optional[Position]:
|
||||||
|
hall_tiles = self.get_all_hall_tiles()
|
||||||
|
random.shuffle(hall_tiles)
|
||||||
|
|
||||||
|
for column, row in hall_tiles:
|
||||||
|
position = self.tile_center((column, row))
|
||||||
|
rect = frame.get_rect(midbottom=(round(position.x), round(position.y)))
|
||||||
|
|
||||||
|
if any(rect.colliderect(blocker) for blocker in blocked_rects):
|
||||||
|
continue
|
||||||
|
|
||||||
|
return round(position.x), round(position.y)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_all_hall_tiles(self) -> list[GridPosition]:
|
||||||
|
hall_tiles: list[GridPosition] = []
|
||||||
|
for row_index, row in enumerate(self.map.map):
|
||||||
|
for column_index, tile in enumerate(row):
|
||||||
|
if tile == "H":
|
||||||
|
hall_tiles.append((column_index, row_index))
|
||||||
|
return hall_tiles
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def position_to_tile(position: pygame.Vector2) -> GridPosition:
|
||||||
|
return int(position.x // tile_size), int(position.y // tile_size)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def tile_center(tile: GridPosition) -> pygame.Vector2:
|
||||||
|
column, row = tile
|
||||||
|
return pygame.Vector2(
|
||||||
|
column * tile_size + tile_size // 2,
|
||||||
|
row * tile_size + tile_size // 2,
|
||||||
|
)
|
||||||
+10
-16
@@ -11,17 +11,16 @@ DESK_HEIGHT = 40
|
|||||||
DOOR_WIDTH = 20
|
DOOR_WIDTH = 20
|
||||||
DOOR_HEIGHT = 100
|
DOOR_HEIGHT = 100
|
||||||
DOOR_BORDER_WIDTH = 3
|
DOOR_BORDER_WIDTH = 3
|
||||||
|
DOOR_HP = 100
|
||||||
|
|
||||||
COLOR_DOOR_BEIGE = (245, 245, 220)
|
COLOR_DOOR_BEIGE = (245, 245, 220)
|
||||||
COLOR_DOOR_BORDER = (100, 90, 80)
|
COLOR_DOOR_BORDER = (100, 90, 80)
|
||||||
|
|
||||||
|
|
||||||
# --- PLAYER ---
|
# --- PLAYER ---
|
||||||
# Players Constants / Settings
|
# Players Constants / Settings
|
||||||
# Valid movement/animation directions.
|
# Valid movement/animation directions.
|
||||||
Direction = Literal["down", "left", "right", "up"]
|
Direction = Literal["down", "left", "right", "up"]
|
||||||
|
|
||||||
|
|
||||||
# --- SPRITE ---
|
# --- SPRITE ---
|
||||||
# Sprite files are grouped by direction.
|
# Sprite files are grouped by direction.
|
||||||
# Example: "down" uses sprite_01.png, sprite_02.png, sprite_03.png.
|
# Example: "down" uses sprite_01.png, sprite_02.png, sprite_03.png.
|
||||||
@@ -36,7 +35,6 @@ DIRECTION_FRAMES: Mapping[Direction, Tuple[int, int, int]] = {
|
|||||||
DEFAULT_SPRITE_HEIGHT: int = 138
|
DEFAULT_SPRITE_HEIGHT: int = 138
|
||||||
DEFAULT_FRAME_TIME: float = 0.16
|
DEFAULT_FRAME_TIME: float = 0.16
|
||||||
|
|
||||||
|
|
||||||
# --- ECTS ---
|
# --- ECTS ---
|
||||||
START_ECTS_VALUE = 1 # how many ECTS are generated at the beginning
|
START_ECTS_VALUE = 1 # how many ECTS are generated at the beginning
|
||||||
ECTS_UPDATE_INTERVAL = 1000 # time in ms (1000 = 1 sec)
|
ECTS_UPDATE_INTERVAL = 1000 # time in ms (1000 = 1 sec)
|
||||||
@@ -44,18 +42,15 @@ ECTS_UPDATE_INTERVAL = 1000 # time in ms (1000 = 1 sec)
|
|||||||
BEER_BOTTLE_ECTS_VALUE = 2
|
BEER_BOTTLE_ECTS_VALUE = 2
|
||||||
BEER_COST = 50
|
BEER_COST = 50
|
||||||
|
|
||||||
|
|
||||||
# --- BUTTONS ---
|
# --- BUTTONS ---
|
||||||
COLOR_BUTTON = (0, 255, 0)
|
COLOR_BUTTON = (0, 255, 0)
|
||||||
COLOR_BUTTON_HOVER = (200, 255, 0)
|
COLOR_BUTTON_HOVER = (200, 255, 0)
|
||||||
|
|
||||||
|
|
||||||
# --- STARTSCREEN ---
|
# --- STARTSCREEN ---
|
||||||
COLOR_STARTSCREEN_BACKGROUND = (0, 0, 0)
|
COLOR_STARTSCREEN_BACKGROUND = (0, 0, 0)
|
||||||
COLOR_STARTSCREEN_FONT_TITLE = (0, 255, 0)
|
COLOR_STARTSCREEN_FONT_TITLE = (0, 255, 0)
|
||||||
COLOR_STARTSCREEN_FONT_LABEL = (0, 255, 255)
|
COLOR_STARTSCREEN_FONT_LABEL = (0, 255, 255)
|
||||||
|
|
||||||
|
|
||||||
# --- ENDSCREEN ---
|
# --- ENDSCREEN ---
|
||||||
COLOR_ENDSCREEN_BACKGROUND = (0, 0, 0)
|
COLOR_ENDSCREEN_BACKGROUND = (0, 0, 0)
|
||||||
COLOR_ENDSCREEN_TEXT = (255, 255, 255)
|
COLOR_ENDSCREEN_TEXT = (255, 255, 255)
|
||||||
@@ -63,30 +58,22 @@ COLOR_ENDSCREEN_WIN = (0, 255, 0)
|
|||||||
COLOR_ENDSCREEN_LOSE = (255, 0, 0)
|
COLOR_ENDSCREEN_LOSE = (255, 0, 0)
|
||||||
FONT_ENDSCREEN = "Arial"
|
FONT_ENDSCREEN = "Arial"
|
||||||
|
|
||||||
|
|
||||||
# --- COFFEE ---
|
# --- COFFEE ---
|
||||||
COFFEE_COST = 30
|
COFFEE_COST = 30
|
||||||
COFFEE_HEAL_VALUE = 30
|
COFFEE_HEAL_VALUE = 30
|
||||||
|
|
||||||
|
|
||||||
# --- TOWER ---
|
# --- TOWER ---
|
||||||
TOWER_COST = 150
|
TOWER_COST = 10
|
||||||
PROJECTILE_DMG = 15
|
PROJECTILE_DMG = 7
|
||||||
PROJECTILE_SPEED = 7
|
PROJECTILE_SPEED = 7
|
||||||
COLOR_PROJECTILE = (200, 200, 0)
|
COLOR_PROJECTILE = (200, 200, 0)
|
||||||
TOWER_TIMER = 30000 # how long the tower stays and attack before it disappears
|
TOWER_TIMER = 30000 # how long the tower stays and attack before it disappears
|
||||||
tower_size = 30
|
|
||||||
projectile_size = 8
|
|
||||||
fire_interval = 1000 # ms
|
|
||||||
respawn_delay = 10000 # ms
|
|
||||||
|
|
||||||
|
|
||||||
# --- ITEM SPAWN TIMER ---
|
# --- ITEM SPAWN TIMER ---
|
||||||
ITEM_SPAWN_INTERVAL = 5000 # ms until a new item spawns
|
ITEM_SPAWN_INTERVAL = 5000 # ms until a new item spawns
|
||||||
ITEM_SHOPPING_TIMER = 10000 # how long you have time to buy items before they despawn
|
ITEM_SHOPPING_TIMER = 10000 # how long you have time to buy items before they despawn
|
||||||
ITEM_SIZE = 30
|
ITEM_SIZE = 30
|
||||||
|
|
||||||
|
|
||||||
# --- ZOMBIE ---
|
# --- ZOMBIE ---
|
||||||
ZOMBIE_EASY_HP = 30
|
ZOMBIE_EASY_HP = 30
|
||||||
ZOMBIE_EASY_DMG = 3
|
ZOMBIE_EASY_DMG = 3
|
||||||
@@ -97,6 +84,13 @@ ZOMBIE_MEDIUM_DMG = 7
|
|||||||
ZOMBIE_HARD_HP = 120
|
ZOMBIE_HARD_HP = 120
|
||||||
ZOMBIE_HARD_DMG = 12
|
ZOMBIE_HARD_DMG = 12
|
||||||
|
|
||||||
|
# --- MONSTERS ---
|
||||||
|
MONSTER_HP = ZOMBIE_MEDIUM_HP
|
||||||
|
MONSTER_MAX_COUNT = 1
|
||||||
|
MONSTER_SPAWN_INTERVAL = 8000 # ms until a replacement monster spawns
|
||||||
|
MONSTER_SPEED = 90.0
|
||||||
|
MONSTER_DOOR_DAMAGE = ZOMBIE_MEDIUM_DMG
|
||||||
|
MONSTER_ATTACK_INTERVAL = 1000 # ms between door attacks
|
||||||
|
|
||||||
# --- HUD ---
|
# --- HUD ---
|
||||||
HUD_FONT = "Arial"
|
HUD_FONT = "Arial"
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ class Shop:
|
|||||||
|
|
||||||
def buy(self, item_name):
|
def buy(self, item_name):
|
||||||
item_to_buy = self.get_item(item_name)
|
item_to_buy = self.get_item(item_name)
|
||||||
|
return self.buy_item(item_to_buy)
|
||||||
|
|
||||||
|
def buy_item(self, item_to_buy):
|
||||||
if item_to_buy is None:
|
if item_to_buy is None:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|||||||
+132
-139
@@ -1,181 +1,174 @@
|
|||||||
import pygame
|
|
||||||
import random
|
|
||||||
import math
|
import math
|
||||||
from src.settings import TOWER_COST, PROJECTILE_DMG, PROJECTILE_SPEED, COLOR_PROJECTILE, TOWER_TIMER, projectile_size, tower_size, fire_interval, respawn_delay
|
import random
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
tower_size = 30 # Größe Tower Feld in Pixel
|
import pygame
|
||||||
projectile_size = 8 # Größe Projektil in Pixel
|
|
||||||
|
from src.settings import (
|
||||||
|
COLOR_PROJECTILE,
|
||||||
|
PROJECTILE_DMG,
|
||||||
|
PROJECTILE_SPEED,
|
||||||
|
TOWER_COST,
|
||||||
|
TOWER_TIMER,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
tower_size = 30
|
||||||
|
projectile_size = 20
|
||||||
fire_interval = 1000 # ms zwischen Schüssen
|
fire_interval = 1000 # ms zwischen Schüssen
|
||||||
respawn_delay = 10000 # ms bis neues Feld spawnt
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Tower:
|
class Tower:
|
||||||
def __init__(self, sound_manager) -> None:
|
def __init__(self, sound_manager=None) -> None:
|
||||||
self.sound_manager = sound_manager
|
self.sound_manager = sound_manager
|
||||||
|
self.tower_rect: Optional[pygame.Rect] = None
|
||||||
# Feld und Tower -> None wenn nicht aktiv
|
self.projectiles: list[dict[str, float]] = []
|
||||||
# Position interagierbares Feld
|
self.tower_start: Optional[int] = None
|
||||||
self.field_rect: pygame.Rect = None
|
|
||||||
#Position des gebauten Tower
|
|
||||||
self.tower_rect: pygame.Rect = None
|
|
||||||
# Lister aller aktiven Projektile - dict mit Position und Geschwindigkeit
|
|
||||||
self.projectiles: list[dict] = []
|
|
||||||
# Zeitpunkt, wann der Tower gebaut wurde
|
|
||||||
self.tower_start: int = None
|
|
||||||
# Zeitpunkt, wann Tower despawnt ist
|
|
||||||
self.despawn_time: int = None
|
|
||||||
# Zeitpunkt des letzten Schusses
|
|
||||||
self.last_shot: int = 0
|
self.last_shot: int = 0
|
||||||
|
|
||||||
# Sprite laden für den Tower
|
|
||||||
try:
|
try:
|
||||||
self.tower_image = pygame.image.load("assets/items/tower.png").convert_alpha()
|
self.tower_image = pygame.image.load("assets/items/tower.png").convert_alpha()
|
||||||
self.tower_image = pygame.transform.scale(self.tower_image, (tower_size, tower_size))
|
self.tower_image = pygame.transform.scale(self.tower_image, (tower_size, tower_size))
|
||||||
except Exception as e:
|
except Exception as error:
|
||||||
print(f"Error loading tower sprite: {e}")
|
print(f"Error loading tower sprite: {error}")
|
||||||
self.tower_image = None
|
self.tower_image = None
|
||||||
|
|
||||||
# Sprite laden für Projektil
|
|
||||||
try:
|
try:
|
||||||
self.projectile_image = pygame.image.load("assets/items/book.png").convert_alpha()
|
self.projectile_image = pygame.image.load("assets/items/book.png").convert_alpha()
|
||||||
self.projectile_image = pygame.transform.scale(self.projectile_image, (projectile_size, projectile_size))
|
self.projectile_image = pygame.transform.scale(
|
||||||
except Exception as e:
|
self.projectile_image,
|
||||||
print(f"Error loading projectile sprite: {e}")
|
(projectile_size, projectile_size),
|
||||||
|
)
|
||||||
|
except Exception as error:
|
||||||
|
print(f"Error loading projectile sprite: {error}")
|
||||||
self.projectile_image = None
|
self.projectile_image = None
|
||||||
|
|
||||||
# soll interaktives Tower Feld an zufälliger freier Position im Raum spawnen
|
def try_spawn(self, room_rect: Optional[pygame.Rect], blocked_rects: list[pygame.Rect], ects) -> bool:
|
||||||
def field_spawn(self, room_rect: pygame.Rect, blocked_rects: list[pygame.Rect]):
|
"""Spawn and pay for one tower when the player has enough ECTS."""
|
||||||
# versucht bis zu 50 mal, eine nicht belegte Stelle im Raum zu finden
|
|
||||||
for i in range(50):
|
|
||||||
x = random.randint(room_rect.left + tower_size, room_rect.right - tower_size * 2)
|
|
||||||
y = random.randint(room_rect.top + tower_size, room_rect.bottom - tower_size * 2)
|
|
||||||
rect = pygame.Rect(x, y, tower_size, tower_size)
|
|
||||||
|
|
||||||
# freie Position finden - nicht auf dem Tisch oder blockierten Rects
|
if room_rect is None or self.tower_rect is not None:
|
||||||
if not any(rect.colliderect(b) for b in blocked_rects):
|
|
||||||
self.field_rect = rect
|
|
||||||
return
|
|
||||||
|
|
||||||
# Baut Tower wenn Spieler E drückt, auf Feld steht und genug ects hat
|
|
||||||
def try_build(self, player_rect: pygame.Rect, keys: pygame.key.ScancodeWrapper, ects):
|
|
||||||
|
|
||||||
# bricht ab wenn kein Feld vorhanden ist oder ein Tower bereits aktiv ist
|
|
||||||
if not self.field_rect or self.tower_rect:
|
|
||||||
return False
|
return False
|
||||||
# prüft alle Bedingungen gleichzeitig
|
if ects.ects_balance < TOWER_COST:
|
||||||
if not (self.field_rect.colliderect(player_rect) and keys[pygame.K_e] and ects.balance >= TOWER_COST):
|
|
||||||
self.sound_manager.play_sfx("buy_fail")
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# ECTS abziehen und Tower bauen
|
rect = self._random_free_rect(room_rect, blocked_rects)
|
||||||
self.sound_manager.play_sfx("buy_success")
|
if rect is None:
|
||||||
ects.balance -= TOWER_COST
|
self._play_sfx("buy_fail")
|
||||||
self.tower_rect = self.field_rect
|
return False
|
||||||
self.field_rect = None
|
|
||||||
|
ects.decrease_balance(TOWER_COST)
|
||||||
|
self.tower_rect = rect
|
||||||
self.tower_start = pygame.time.get_ticks()
|
self.tower_start = pygame.time.get_ticks()
|
||||||
|
self.projectiles = []
|
||||||
|
self._play_sfx("buy_success")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# ruft jeden Frame auf und gibt den Schaden auf Monster zurück
|
def update(
|
||||||
# verwaltet Schüsse, Projektile, Tower-Leben und Respawn
|
self,
|
||||||
def update(self, player_rect: pygame.Rect, keys: pygame.key.ScancodeWrapper, ects, monster_rect: pygame.Rect, room_rect: pygame.Rect, blocked_rects: list[pygame.Rect]):
|
room_rect: Optional[pygame.Rect],
|
||||||
|
blocked_rects: list[pygame.Rect],
|
||||||
|
ects,
|
||||||
|
monster_rect: Optional[pygame.Rect],
|
||||||
|
) -> int:
|
||||||
|
"""Update tower spawning/shooting and return damage dealt to the monster."""
|
||||||
|
|
||||||
now = pygame.time.get_ticks()
|
now = pygame.time.get_ticks()
|
||||||
total_damage = 0
|
total_damage = 0
|
||||||
|
|
||||||
# versucht, den Tower zu bauen wenn der Spieler auf dem Feld steht
|
self.try_spawn(room_rect, blocked_rects, ects)
|
||||||
self.try_build(player_rect, keys, ects)
|
|
||||||
|
|
||||||
# schießen dann, wenn Tower aktiv und Monster vorhanden sind
|
if self.tower_rect is not None and monster_rect is not None:
|
||||||
if self.tower_rect and monster_rect:
|
|
||||||
if now - self.last_shot >= fire_interval:
|
if now - self.last_shot >= fire_interval:
|
||||||
#berechnet Richtungsvektor vom Tower zum Monster
|
self._shoot_at(monster_rect)
|
||||||
|
self.last_shot = now
|
||||||
|
|
||||||
|
for projectile in self.projectiles[:]:
|
||||||
|
projectile["x"] += projectile["vel_x"]
|
||||||
|
projectile["y"] += projectile["vel_y"]
|
||||||
|
|
||||||
|
projectile_rect = pygame.Rect(
|
||||||
|
int(projectile["x"]),
|
||||||
|
int(projectile["y"]),
|
||||||
|
projectile_size,
|
||||||
|
projectile_size,
|
||||||
|
)
|
||||||
|
if monster_rect is not None and projectile_rect.colliderect(monster_rect):
|
||||||
|
total_damage += PROJECTILE_DMG
|
||||||
|
self.projectiles.remove(projectile)
|
||||||
|
self._play_sfx("monster_hit")
|
||||||
|
|
||||||
|
if self.tower_rect is not None and self.tower_start is not None:
|
||||||
|
if now - self.tower_start >= TOWER_TIMER:
|
||||||
|
self.clear()
|
||||||
|
|
||||||
|
return total_damage
|
||||||
|
|
||||||
|
def draw(self, surface: pygame.Surface, font: pygame.font.Font, cam_x: int = 0, cam_y: int = 0) -> None:
|
||||||
|
if self.tower_rect is not None and self.tower_start is not None:
|
||||||
|
draw_rect = pygame.Rect(
|
||||||
|
self.tower_rect.x - cam_x,
|
||||||
|
self.tower_rect.y - cam_y,
|
||||||
|
tower_size,
|
||||||
|
tower_size,
|
||||||
|
)
|
||||||
|
|
||||||
|
seconds_left = max(0, TOWER_TIMER - (pygame.time.get_ticks() - self.tower_start)) // 1000
|
||||||
|
|
||||||
|
if self.tower_image is not None:
|
||||||
|
surface.blit(self.tower_image, draw_rect)
|
||||||
|
else:
|
||||||
|
pygame.draw.rect(surface, (40, 160, 80), draw_rect)
|
||||||
|
pygame.draw.rect(surface, (80, 220, 120), draw_rect, 2)
|
||||||
|
|
||||||
|
surface.blit(font.render(str(seconds_left), True, (255, 255, 255)), (draw_rect.x + 8, draw_rect.y - 18))
|
||||||
|
|
||||||
|
for projectile in self.projectiles:
|
||||||
|
projectile_rect = pygame.Rect(
|
||||||
|
int(projectile["x"]) - cam_x,
|
||||||
|
int(projectile["y"]) - cam_y,
|
||||||
|
projectile_size,
|
||||||
|
projectile_size,
|
||||||
|
)
|
||||||
|
if self.projectile_image is not None:
|
||||||
|
surface.blit(self.projectile_image, projectile_rect)
|
||||||
|
else:
|
||||||
|
pygame.draw.rect(surface, COLOR_PROJECTILE, projectile_rect)
|
||||||
|
|
||||||
|
def clear(self) -> None:
|
||||||
|
self.tower_rect = None
|
||||||
|
self.projectiles = []
|
||||||
|
self.tower_start = None
|
||||||
|
self.last_shot = 0
|
||||||
|
|
||||||
|
def _shoot_at(self, monster_rect: pygame.Rect) -> None:
|
||||||
|
if self.tower_rect is None:
|
||||||
|
return
|
||||||
|
|
||||||
dx = monster_rect.centerx - self.tower_rect.centerx
|
dx = monster_rect.centerx - self.tower_rect.centerx
|
||||||
dy = monster_rect.centery - self.tower_rect.centery
|
dy = monster_rect.centery - self.tower_rect.centery
|
||||||
distance = math.sqrt(dx * dx + dy * dy) or 1
|
distance = math.sqrt(dx * dx + dy * dy) or 1
|
||||||
|
self.projectiles.append(
|
||||||
# fügt neues Projektil mit Startposition und Geschwindigkeit hinzu
|
{
|
||||||
self.projectiles.append({
|
|
||||||
"x": float(self.tower_rect.centerx),
|
"x": float(self.tower_rect.centerx),
|
||||||
"y": float(self.tower_rect.centery),
|
"y": float(self.tower_rect.centery),
|
||||||
"vel_x": (dx / distance) * PROJECTILE_SPEED,
|
"vel_x": (dx / distance) * PROJECTILE_SPEED,
|
||||||
"vel_y": (dy / distance) * PROJECTILE_SPEED,
|
"vel_y": (dy / distance) * PROJECTILE_SPEED,
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
self._play_sfx("tower_shoot")
|
||||||
|
|
||||||
self.sound_manager.play_sfx("tower_shoot")
|
def _play_sfx(self, name: str) -> None:
|
||||||
self.last_shot = now
|
if self.sound_manager is not None:
|
||||||
|
self.sound_manager.play_sfx(name)
|
||||||
|
|
||||||
# Bewegung der Projektile und Kollision mit Monster prüfen
|
@staticmethod
|
||||||
for p in self.projectiles[:]: # [:] = Kopie der Liste, dass man sicher löschen kann
|
def _random_free_rect(room_rect: pygame.Rect, blocked_rects: list[pygame.Rect]) -> Optional[pygame.Rect]:
|
||||||
p["x"] += p["vel_x"]
|
for _ in range(50):
|
||||||
p["y"] += p["vel_y"]
|
x = random.randint(room_rect.left + tower_size, room_rect.right - tower_size * 2)
|
||||||
|
y = random.randint(room_rect.top + tower_size, room_rect.bottom - tower_size * 2)
|
||||||
|
rect = pygame.Rect(x, y, tower_size, tower_size)
|
||||||
|
|
||||||
# Projektil trifft auf Monster -> Schaden wird addiert und Projektil entfernt
|
if not any(rect.colliderect(blocked) for blocked in blocked_rects):
|
||||||
if monster_rect and pygame.Rect(int(p["x"]), int(p["y"]), projectile_size, projectile_size).colliderect(monster_rect):
|
return rect
|
||||||
total_damage += PROJECTILE_DMG
|
|
||||||
self.sound_manager.play_sfx("monster_hit")
|
|
||||||
self.projectiles.remove(p)
|
|
||||||
|
|
||||||
# wenn der Tower abgelaufen ist, soll er despawnen und ein Timer starten
|
return None
|
||||||
if self.tower_rect and self.tower_start and now - self.tower_start >= TOWER_TIMER:
|
|
||||||
self.tower_rect = None
|
|
||||||
self.despawn_time = now
|
|
||||||
self.projectiles = []
|
|
||||||
|
|
||||||
# ein neues Feld soll spawnen nach dem Respawn Delay
|
|
||||||
if not self.tower_rect and not self.field_rect and self.despawn_time:
|
|
||||||
if now - self.despawn_time >= respawn_delay:
|
|
||||||
self.field_spawn(room_rect, blocked_rects)
|
|
||||||
self.despawn_time = None
|
|
||||||
|
|
||||||
return total_damage
|
|
||||||
|
|
||||||
|
|
||||||
# Zeichnet Feld, Tower und Projektile auf dem Bildschirm
|
|
||||||
def draw(self, surface: pygame.Surface, font: pygame.font.Font, cam_x: int = 0, cam_y: int = 0):
|
|
||||||
# Feld zeichnen - goldenes Rechteck mit "T" als Hinweis
|
|
||||||
if self.field_rect:
|
|
||||||
r = pygame.Rect(self.field_rect.x - cam_x, self.field_rect.y - cam_y, tower_size, tower_size)
|
|
||||||
pygame.draw.rect(surface, (160,120,30), r)
|
|
||||||
pygame.draw.rect(surface, (220,180,60), r, 2)
|
|
||||||
surface.blit(font.render("T", True, (255, 255, 255)), (r.x + 12, r.y + 10))
|
|
||||||
|
|
||||||
# Tower Zeichnen mit Sprite - als Fallback grünes Rechteck
|
|
||||||
if self.tower_rect and self.tower_start:
|
|
||||||
r = pygame.Rect(self.tower_rect.x - cam_x, self.tower_rect.y - cam_y, tower_size, tower_size)
|
|
||||||
|
|
||||||
# berechnet die verbleibende Zeit in Sekunden
|
|
||||||
sec = max(0, TOWER_TIMER - (pygame.time.get_ticks() - self.tower_start)) // 1000
|
|
||||||
|
|
||||||
if self.tower_image:
|
|
||||||
# Sprite verfügbar -> zeichnen
|
|
||||||
surface.blit(self.tower_image, r)
|
|
||||||
else:
|
|
||||||
# kein Sprite -> Fallback grünes Rechteck
|
|
||||||
pygame.draw.rect(surface, (40, 160, 80), r)
|
|
||||||
pygame.draw.rect(surface, (80, 220, 120), r, 2)
|
|
||||||
|
|
||||||
# Restzeit der Tower über dem Sprite anzeigen (in Sekunden)
|
|
||||||
surface.blit(font.render(str(sec), True, (255, 255, 255)), (r.x + 8, r.y - 18))
|
|
||||||
|
|
||||||
# alle aktiven Projektile zeichnen als Prite oder Fallback als kleine Rechtecke
|
|
||||||
for p in self.projectiles:
|
|
||||||
draw_x = int(p["x"]) - cam_x
|
|
||||||
draw_y = int(p["y"]) - cam_y
|
|
||||||
|
|
||||||
if self.projectile_image:
|
|
||||||
# zeichnet Buch Sprite
|
|
||||||
surface.blit(self.projectile_image, (draw_x, draw_y))
|
|
||||||
else:
|
|
||||||
# Fallback - gelbes Rechteck
|
|
||||||
pygame.draw.rect(surface, COLOR_PROJECTILE,
|
|
||||||
pygame.Rect(draw_x, draw_y, projectile_size, projectile_size))
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# alles zurücksetzen, wenn der Raum verlassen wird
|
|
||||||
def clear(self) -> None:
|
|
||||||
self.field_rect = None
|
|
||||||
self.tower_rect = None
|
|
||||||
self.projectiles = []
|
|
||||||
self.despawn_time = None
|
|
||||||
|
|||||||
Reference in New Issue
Block a user