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
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+38
View File
@@ -0,0 +1,38 @@
import wave, struct, math, os, random
"""
run once to get sounds generated - KI helped here to do this!
"""
SOUND_DIR = "../assets/sounds"
def save_wav(filename, data):
if not os.path.exists(SOUND_DIR):
os.makedirs(SOUND_DIR)
path = os.path.join(SOUND_DIR, filename)
with wave.open(path, 'w') as f:
f.setnchannels(1)
f.setsampwidth(2)
f.setframerate(44100)
for s in data:
# Den Soundwert in das richtige Format (16-bit PCM) umwandeln
f.writeframesraw(struct.pack('<h', int(s * 32767)))
def gen(dur, func, vol=0.4):
num_samples = int(dur * 44100)
return [func(i/44100, i) * vol * min(1.0, (num_samples-i)/(4410*dur)) for i in range(num_samples)]
print(f"Das Skript liegt in 'src', speichert aber nach: {os.path.abspath(SOUND_DIR)}")
save_wav("bgm_normal.wav", gen(10.0, lambda t, i: (math.sin(2*math.pi*110*t) + math.sin(2*math.pi*440*t)*0.2)/2, 0.2))
save_wav("bgm_creepy.wav", gen(10.0, lambda t, i: math.sin(2*math.pi*(60+5*math.sin(t))*t), 0.2))
save_wav("tower_shoot.wav", gen(0.15, lambda t, i: 1.0 if math.sin(2*math.pi*1200*(1-t*5)*t) > 0 else -1.0, 0.1))
save_wav("monster_hit.wav", gen(0.1, lambda t, i: random.uniform(-1, 1), 0.3))
save_wav("buy_success.wav", gen(0.3, lambda t, i: math.sin(2*math.pi*(660 if t<0.1 else 880)*t)))
save_wav("buy_fail.wav", gen(0.4, lambda t, i: math.sin(2*math.pi*120*t) * (1+math.sin(40*t))))
save_wav("monster_healed.wav", gen(0.8, lambda t, i: math.sin(2*math.pi*(440+800*t**2)*t)))
save_wav("monster_transform.wav", gen(1.2, lambda t, i: math.sin(2*math.pi*(200-150*t)*t) * random.uniform(0.5, 1.0)))
print("Fertig! Schau jetzt in deinem Hauptverzeichnis unter assets/sounds nach.")
+7
View File
@@ -8,6 +8,8 @@ from src.settings import (COLOR_ENDSCREEN_BACKGROUND,
COLOR_BUTTON, COLOR_BUTTON,
FONT_ENDSCREEN) FONT_ENDSCREEN)
from src.highscore import Highscore
class Endscreen: class Endscreen:
def __init__(self, screen, player_name, score_ects, time_survived, won): def __init__(self, screen, player_name, score_ects, time_survived, won):
self.screen = screen self.screen = screen
@@ -52,6 +54,8 @@ class Endscreen:
self.button_hover_color self.button_hover_color
) )
self.highscore = Highscore()
def run(self): def run(self):
# get events # get events
events = pygame.event.get() events = pygame.event.get()
@@ -113,6 +117,9 @@ class Endscreen:
restart_pressed = self.button_restart.draw(self.screen) restart_pressed = self.button_restart.draw(self.screen)
menu_pressed = self.button_menu.draw(self.screen) menu_pressed = self.button_menu.draw(self.screen)
# draw highscore table
self.highscore.draw(self.screen, 800, 300)
# check button click events # check button click events
if restart_pressed: if restart_pressed:
return "game" return "game"
+91 -21
View File
@@ -4,8 +4,6 @@ Controls:
- WASD / arrow keys: move Niklas around the map - WASD / arrow keys: move Niklas around the map
- Space: toggle player state - Space: toggle player state
- Esc / Q: quit - Esc / Q: quit
The demo also places a few bad/angry actors as monsters in the rooms.
""" """
from __future__ import annotations from __future__ import annotations
@@ -20,20 +18,27 @@ from src.door import Door
from src.ects import ECTS from src.ects import ECTS
from src.endscreen import Endscreen from src.endscreen import Endscreen
from src.hud import HUD 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.monster import MonsterManager 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.startscreen import Startscreen
from src.sounds import SoundManager
from src.tower import Tower 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]: def tile_center(column: int, row: int) -> tuple[int, int]:
"""Return the pixel center of a map tile.""" """Return the pixel center of a map tile."""
return column * tile_size + tile_size // 2, row * tile_size + tile_size // 2 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.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.buy_requested = False
# --- SOUND & SYSTEMS ---
self.sound_manager = SoundManager()
self.startscreen = Startscreen(self.screen) self.startscreen = Startscreen(self.screen)
self.hud = HUD() self.hud = HUD()
self.ects = ECTS() 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.state = "startscreen"
self.player_name: str = "" self.player_name: str = ""
self.game_start_time = 0 self.game_start_time = 0
self.endscreen: Optional[Endscreen] = None 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.active_door: Optional[Door] = None
self.doors_by_room: dict[tuple[int, int, int, int], Door] = {} 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" assets_dir = Path(__file__).resolve().parent.parent / "assets"
player_asset = Actor.load_sprite_asset( player_asset = Actor.load_sprite_asset(
assets_dir / "niklas_sprites", assets_dir / "niklas_sprites",
@@ -76,8 +89,6 @@ class Game:
self.monsters.spawn_initial(self.get_monster_spawn_blockers()) self.monsters.spawn_initial(self.get_monster_spawn_blockers())
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."""
frame_count = 0 frame_count = 0
while self.running and (max_frames is None or frame_count < max_frames): while self.running and (max_frames is None or frame_count < max_frames):
dt = min(self.clock.tick(FPS) / 1000.0, 0.05) dt = min(self.clock.tick(FPS) / 1000.0, 0.05)
@@ -90,6 +101,8 @@ class Game:
self.player_name = self.startscreen.player_name self.player_name = self.startscreen.player_name
self.game_start_time = pygame.time.get_ticks() self.game_start_time = pygame.time.get_ticks()
self.state = "game" self.state = "game"
self.sound_manager.play_bgm("normal")
elif self.state == "game": elif self.state == "game":
self.handle_events() self.handle_events()
self.update(dt) self.update(dt)
@@ -117,8 +130,18 @@ class Game:
self.running = False self.running = False
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")
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:
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) movement = self.get_player_movement(dt)
self.player.move_by(movement) self.player.move_by(movement)
@@ -128,6 +151,7 @@ class Game:
self.player.update_animation(dt) self.player.update_animation(dt)
self.ects.update(self.is_player_at_desk()) self.ects.update(self.is_player_at_desk())
self.handle_room_table_collision() self.handle_room_table_collision()
self.update_items()
self.monsters.update( self.monsters.update(
dt, dt,
self.player.position, self.player.position,
@@ -144,7 +168,7 @@ class Game:
- (keys[pygame.K_LEFT] or keys[pygame.K_a]), - (keys[pygame.K_LEFT] or keys[pygame.K_a]),
(keys[pygame.K_DOWN] or keys[pygame.K_s]) (keys[pygame.K_DOWN] or keys[pygame.K_s])
- (keys[pygame.K_UP] or keys[pygame.K_w]), - (keys[pygame.K_UP] or keys[pygame.K_w]),
) )
if movement.length_squared() == 0: if movement.length_squared() == 0:
return movement return movement
@@ -157,8 +181,6 @@ class Game:
return movement.normalize() * PLAYER_SPEED * dt return movement.normalize() * PLAYER_SPEED * dt
def is_position_walkable(self, position: pygame.Vector2) -> bool: 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) column = int(position.x // tile_size)
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)
@@ -194,6 +216,41 @@ class Game:
if not monster.is_alive: if not monster.is_alive:
self.tower.clear() 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: def check_end_conditions(self) -> None:
if self.active_door is not None and self.active_door.current_hp <= 0: if self.active_door is not None and self.active_door.current_hp <= 0:
self.show_endscreen(won=False) self.show_endscreen(won=False)
@@ -210,6 +267,9 @@ class Game:
def restart_game(self) -> None: def restart_game(self) -> None:
self.ects = ECTS() 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.tower.clear()
self.active_door = None self.active_door = None
self.doors_by_room = {} self.doors_by_room = {}
@@ -238,7 +298,7 @@ class Game:
return return
def get_or_create_door_for_room(self, room_rect: pygame.Rect) -> Door: 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: if key not in self.doors_by_room:
self.doors_by_room[key] = self.create_door_for_room(room_rect) self.doors_by_room[key] = self.create_door_for_room(room_rect)
return self.doors_by_room[key] 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)) blockers.append(pygame.Rect(self.active_door.x, self.active_door.y, DOOR_WIDTH, DOOR_HEIGHT))
for monster in self.monsters.monsters: for monster in self.monsters.monsters:
blockers.append(monster.rect) blockers.append(monster.rect)
blockers.extend(item.rect for item in self.item_manager.items)
return blockers 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 @staticmethod
def get_actor_rect(actor: Actor) -> pygame.Rect: def get_actor_rect(actor: Actor) -> pygame.Rect:
frame = actor.get_frame() frame = actor.get_frame()
return frame.get_rect(midbottom=(round(actor.position.x), round(actor.position.y))) 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."""
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")
if column_index != -1: 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 ( return (
column_index * tile_size + tile_size // 2, column_index * tile_size + tile_size // 2,
(row_index + 1) * tile_size + tile_size // 2, (row_index + 1) * tile_size + tile_size // 2,
) )
return tile_size, tile_size return tile_size, tile_size
def get_elapsed_seconds(self) -> int: 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: if self.active_door is not None and self.active_door.is_closed:
self.active_door.draw(self.screen) self.active_door.draw(self.screen)
self.item_manager.draw(self.screen, 0, 0, self.item_font)
self.tower.draw(self.screen, self.font) 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.
+30 -17
View File
@@ -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,9 +125,13 @@ 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]:
for item in self.items: return self.get_touching_item(player_rect)
if player_rect.colliderect(item.rect): return None
return item
def get_touching_item(self, player_rect):
for item in self.items:
if player_rect.colliderect(item.rect):
return item
return None return None
# delete item from list after buying it # delete item from list after buying it
@@ -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):
+1 -15
View File
@@ -1,20 +1,6 @@
import pygame import pygame
from src.desk import Desk from src.desk import Desk
from src.settings import DESK_WIDTH, DESK_HEIGHT, tile_size from src.settings import DESK_WIDTH, DESK_HEIGHT, tile_size, screen_w, screen_h, color_wall, color_room, color_hall, color_door, color_void, color_text, color_border, color_start
# Fenstergröße
screen_w = 1900
screen_h = 900
# Farben für die einzelnen Tiles
color_wall = (30, 20, 40) # black like
color_room = (75, 40, 55) # dark red-violet
color_hall = (35, 65, 45) # dark green
color_door = (40, 90, 110) # dark blue
color_void = (10, 10, 15) # a little more black
color_text = (220, 180, 210) # rose
color_border = (55, 35, 70)
color_start = (255, 0, 0)# dark violet
class Map: class Map:
+34
View File
@@ -119,6 +119,40 @@ HUD_Y_DOOR = 20
HUD_Y_ZOMBIE = 50 HUD_Y_ZOMBIE = 50
HUD_Y_ECTS = 85 HUD_Y_ECTS = 85
# --- MAP ---
# Fenstergröße und Tile Größe (Felder)
tile_size = 30
screen_w = 1900
screen_h = 900
# Farben für die einzelnen Tiles
color_wall = (30, 20, 40) # black like
color_room = (75, 40, 55) # dark red-violet
color_hall = (35, 65, 45) # dark green
color_door = (40, 90, 110) # dark blue
color_void = (10, 10, 15) # a little more black
color_text = (220, 180, 210) # rose
color_border = (55, 35, 70)
color_start = (255, 0, 0)# dark violet
# --- HIGHSCORE --- # --- HIGHSCORE ---
HIGHSCORE_COLOR_FONT = (0, 0, 0) HIGHSCORE_COLOR_FONT = (0, 0, 0)
HIGHSCORE_COLOR_TABLE = (150, 255, 0) HIGHSCORE_COLOR_TABLE = (150, 255, 0)
# --- SOUND ---
SOUND_DIR = "assets/sounds"
SFX_TOWER_SHOOT = f"{SOUND_DIR}/tower_shoot.wav"
SFX_MONSTER_HIT = f"{SOUND_DIR}/monster_hit.wav"
SFX_BUY_SUCCESS = f"{SOUND_DIR}/buy_success.wav"
SFX_BUY_FAIL = f"{SOUND_DIR}/buy_fail.wav"
SFX_MONSTER_HEALED = f"{SOUND_DIR}/monster_healed.wav"
SFX_MONSTER_TRANSFORM = f"{SOUND_DIR}/monster_transform.wav"
BGM_NORMAL = f"{SOUND_DIR}/bgm_normal.wav"
BGM_CREEPY = f"{SOUND_DIR}/bgm_creepy.wav"
VOLUME_SFX = 0.5
VOLUME_BGM = 0.3
+8 -1
View File
@@ -2,12 +2,17 @@ from src.item import Item
class Shop: class Shop:
def __init__(self, ects, buyable_items): def __init__(self, ects, buyable_items, sound_manager):
self.ects = ects # give the ects object to the shop self.ects = ects # give the ects object to the shop
self.buyable_items: list[Item] = buyable_items self.buyable_items: list[Item] = buyable_items
# save soundManager to use it in buy()
self.sound_manager = sound_manager
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
@@ -15,9 +20,11 @@ class Shop:
# is_over_balance() means "has enough balance" # is_over_balance() means "has enough balance"
if not self.ects.is_over_balance(cost): if not self.ects.is_over_balance(cost):
self.sound_manager.play_sfx("buy_fail")
return False return False
self.ects.decrease_balance(cost) self.ects.decrease_balance(cost)
self.sound_manager.play_sfx("buy_success")
return True return True
def get_item(self, item_name): def get_item(self, item_name):
+51
View File
@@ -0,0 +1,51 @@
import pygame
from src.settings import (
SFX_TOWER_SHOOT, SFX_MONSTER_HIT, SFX_BUY_SUCCESS,
SFX_BUY_FAIL, SFX_MONSTER_HEALED, SFX_MONSTER_TRANSFORM,
BGM_NORMAL, BGM_CREEPY, VOLUME_SFX, VOLUME_BGM
)
class SoundManager:
def __init__(self):
pygame.mixer.init()
self.effects = {}
self.current_bgm = None
self._load_sfx("tower_shoot", SFX_TOWER_SHOOT)
self._load_sfx("monster_hit", SFX_MONSTER_HIT)
self._load_sfx("buy_success", SFX_BUY_SUCCESS)
self._load_sfx("buy_fail", SFX_BUY_FAIL)
self._load_sfx("monster_healed", SFX_MONSTER_HEALED)
self._load_sfx("monster_transform", SFX_MONSTER_TRANSFORM)
# set volume via settings param
for sound in self.effects.values():
if sound:
sound.set_volume(VOLUME_SFX)
def _load_sfx(self, name, path):
try:
self.effects[name] = pygame.mixer.Sound(path)
except pygame.error:
print(f"Could not find sound: {path}")
self.effects[name] = None
def play_sfx(self, name):
sound = self.effects.get(name)
if isinstance(sound, pygame.mixer.Sound):
sound.play()
# set BackGroundMusic dependent if monster is active or tutor is active
def play_bgm(self, mode):
if self.current_bgm == mode:
return
path = BGM_NORMAL if mode == "normal" else BGM_CREEPY
try:
pygame.mixer.music.load(path)
pygame.mixer.music.set_volume(VOLUME_BGM)
pygame.mixer.music.play(-1)
self.current_bgm = mode
except pygame.error:
print(f"Could not find sound: {path}")
+56 -29
View File
@@ -4,7 +4,13 @@ from typing import Optional
import pygame import pygame
from src.settings import TOWER_COST, PROJECTILE_DMG, PROJECTILE_SPEED, COLOR_PROJECTILE, TOWER_TIMER from src.settings import (
COLOR_PROJECTILE,
PROJECTILE_DMG,
PROJECTILE_SPEED,
TOWER_COST,
TOWER_TIMER,
)
tower_size = 30 tower_size = 30
@@ -13,29 +19,32 @@ fire_interval = 1000 # ms zwischen Schüssen
class Tower: class Tower:
def __init__(self) -> None: def __init__(self, sound_manager=None) -> None:
self.sound_manager = sound_manager
self.tower_rect: Optional[pygame.Rect] = None self.tower_rect: Optional[pygame.Rect] = None
self.projectiles: list[dict] = [] self.projectiles: list[dict[str, float]] = []
self.tower_start: Optional[int] = None self.tower_start: Optional[int] = None
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
# Projectile sprite: the tower throws books.
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: self.projectile_image,
(projectile_size, projectile_size),
)
except Exception as error:
print(f"Error loading projectile sprite: {error}")
self.projectile_image = None self.projectile_image = None
def try_spawn(self, room_rect: Optional[pygame.Rect], blocked_rects: list[pygame.Rect], ects) -> bool: def try_spawn(self, room_rect: Optional[pygame.Rect], blocked_rects: list[pygame.Rect], ects) -> bool:
"""Spawn and pay for one tower as soon as the player has enough ECTS.""" """Spawn and pay for one tower when the player has enough ECTS."""
if room_rect is None or self.tower_rect is not None: if room_rect is None or self.tower_rect is not None:
return False return False
@@ -44,12 +53,14 @@ class Tower:
rect = self._random_free_rect(room_rect, blocked_rects) rect = self._random_free_rect(room_rect, blocked_rects)
if rect is None: if rect is None:
self._play_sfx("buy_fail")
return False return False
ects.decrease_balance(TOWER_COST) ects.decrease_balance(TOWER_COST)
self.tower_rect = rect self.tower_rect = rect
self.tower_start = pygame.time.get_ticks() self.tower_start = pygame.time.get_ticks()
self.projectiles = [] self.projectiles = []
self._play_sfx("buy_success")
return True return True
def update( def update(
@@ -66,17 +77,9 @@ class Tower:
self.try_spawn(room_rect, blocked_rects, ects) self.try_spawn(room_rect, blocked_rects, ects)
if self.tower_rect and monster_rect: if self.tower_rect is not None and monster_rect is not None:
if now - self.last_shot >= fire_interval: if now - self.last_shot >= fire_interval:
dx = monster_rect.centerx - self.tower_rect.centerx self._shoot_at(monster_rect)
dy = monster_rect.centery - self.tower_rect.centery
distance = math.sqrt(dx * dx + dy * dy) or 1
self.projectiles.append({
"x": float(self.tower_rect.centerx),
"y": float(self.tower_rect.centery),
"vel_x": (dx / distance) * PROJECTILE_SPEED,
"vel_y": (dy / distance) * PROJECTILE_SPEED,
})
self.last_shot = now self.last_shot = now
for projectile in self.projectiles[:]: for projectile in self.projectiles[:]:
@@ -89,17 +92,19 @@ class Tower:
projectile_size, projectile_size,
projectile_size, projectile_size,
) )
if monster_rect and projectile_rect.colliderect(monster_rect): if monster_rect is not None and projectile_rect.colliderect(monster_rect):
total_damage += PROJECTILE_DMG total_damage += PROJECTILE_DMG
self.projectiles.remove(projectile) self.projectiles.remove(projectile)
self._play_sfx("monster_hit")
if self.tower_rect and self.tower_start and now - self.tower_start >= TOWER_TIMER: if self.tower_rect is not None and self.tower_start is not None:
self.clear() if now - self.tower_start >= TOWER_TIMER:
self.clear()
return total_damage return total_damage
def draw(self, surface: pygame.Surface, font: pygame.font.Font, cam_x: int = 0, cam_y: int = 0): def draw(self, surface: pygame.Surface, font: pygame.font.Font, cam_x: int = 0, cam_y: int = 0) -> None:
if self.tower_rect and self.tower_start: if self.tower_rect is not None and self.tower_start is not None:
draw_rect = pygame.Rect( draw_rect = pygame.Rect(
self.tower_rect.x - cam_x, self.tower_rect.x - cam_x,
self.tower_rect.y - cam_y, self.tower_rect.y - cam_y,
@@ -107,15 +112,15 @@ class Tower:
tower_size, tower_size,
) )
sec = max(0, TOWER_TIMER - (pygame.time.get_ticks() - self.tower_start)) // 1000 seconds_left = max(0, TOWER_TIMER - (pygame.time.get_ticks() - self.tower_start)) // 1000
if self.tower_image: if self.tower_image is not None:
surface.blit(self.tower_image, draw_rect) surface.blit(self.tower_image, draw_rect)
else: else:
pygame.draw.rect(surface, (40, 160, 80), draw_rect) pygame.draw.rect(surface, (40, 160, 80), draw_rect)
pygame.draw.rect(surface, (80, 220, 120), draw_rect, 2) pygame.draw.rect(surface, (80, 220, 120), draw_rect, 2)
surface.blit(font.render(str(sec), True, (255, 255, 255)), (draw_rect.x + 8, draw_rect.y - 18)) surface.blit(font.render(str(seconds_left), True, (255, 255, 255)), (draw_rect.x + 8, draw_rect.y - 18))
for projectile in self.projectiles: for projectile in self.projectiles:
projectile_rect = pygame.Rect( projectile_rect = pygame.Rect(
@@ -124,7 +129,7 @@ class Tower:
projectile_size, projectile_size,
projectile_size, projectile_size,
) )
if self.projectile_image: if self.projectile_image is not None:
surface.blit(self.projectile_image, projectile_rect) surface.blit(self.projectile_image, projectile_rect)
else: else:
pygame.draw.rect(surface, COLOR_PROJECTILE, projectile_rect) pygame.draw.rect(surface, COLOR_PROJECTILE, projectile_rect)
@@ -133,6 +138,28 @@ class Tower:
self.tower_rect = None self.tower_rect = None
self.projectiles = [] self.projectiles = []
self.tower_start = None 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
dy = monster_rect.centery - self.tower_rect.centery
distance = math.sqrt(dx * dx + dy * dy) or 1
self.projectiles.append(
{
"x": float(self.tower_rect.centerx),
"y": float(self.tower_rect.centery),
"vel_x": (dx / distance) * PROJECTILE_SPEED,
"vel_y": (dy / distance) * PROJECTILE_SPEED,
}
)
self._play_sfx("tower_shoot")
def _play_sfx(self, name: str) -> None:
if self.sound_manager is not None:
self.sound_manager.play_sfx(name)
@staticmethod @staticmethod
def _random_free_rect(room_rect: pygame.Rect, blocked_rects: list[pygame.Rect]) -> Optional[pygame.Rect]: def _random_free_rect(room_rect: pygame.Rect, blocked_rects: list[pygame.Rect]) -> Optional[pygame.Rect]: