implement missing stuff
This commit is contained in:
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.
@@ -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.")
|
||||
@@ -8,6 +8,8 @@ from src.settings import (COLOR_ENDSCREEN_BACKGROUND,
|
||||
COLOR_BUTTON,
|
||||
FONT_ENDSCREEN)
|
||||
|
||||
from src.highscore import Highscore
|
||||
|
||||
class Endscreen:
|
||||
def __init__(self, screen, player_name, score_ects, time_survived, won):
|
||||
self.screen = screen
|
||||
@@ -52,6 +54,8 @@ class Endscreen:
|
||||
self.button_hover_color
|
||||
)
|
||||
|
||||
self.highscore = Highscore()
|
||||
|
||||
def run(self):
|
||||
# get events
|
||||
events = pygame.event.get()
|
||||
@@ -113,6 +117,9 @@ class Endscreen:
|
||||
restart_pressed = self.button_restart.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
|
||||
if restart_pressed:
|
||||
return "game"
|
||||
|
||||
+91
-21
@@ -4,8 +4,6 @@ Controls:
|
||||
- WASD / arrow keys: move Niklas around the map
|
||||
- Space: toggle player state
|
||||
- Esc / Q: quit
|
||||
|
||||
The demo also places a few bad/angry actors as monsters in the rooms.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -20,20 +18,27 @@ from src.door import Door
|
||||
from src.ects import ECTS
|
||||
from src.endscreen import Endscreen
|
||||
from src.hud import HUD
|
||||
from src.item import Item, ItemManager
|
||||
from src.map import Map, color_void, screen_h, screen_w, tile_size
|
||||
from src.monster import MonsterManager
|
||||
from src.settings import DEMO_ACTOR_HEIGHT, DOOR_HEIGHT, DOOR_HP, DOOR_WIDTH, FPS, PLAYER_SPEED
|
||||
from src.settings import (
|
||||
BEER_BOTTLE_ECTS_VALUE,
|
||||
COFFEE_HEAL_VALUE,
|
||||
DEMO_ACTOR_HEIGHT,
|
||||
DOOR_HEIGHT,
|
||||
DOOR_HP,
|
||||
DOOR_WIDTH,
|
||||
FPS,
|
||||
PLAYER_SPEED,
|
||||
)
|
||||
from src.shop import Shop
|
||||
from src.startscreen import Startscreen
|
||||
from src.sounds import SoundManager
|
||||
from src.tower import Tower
|
||||
|
||||
|
||||
# Keep the actor close to the map scale. The map tiles are 30x30, so 2 tiles
|
||||
# high makes the character readable without covering whole rooms/walls.
|
||||
|
||||
|
||||
def tile_center(column: int, row: int) -> tuple[int, int]:
|
||||
"""Return the pixel center of a map tile."""
|
||||
|
||||
return column * tile_size + tile_size // 2, row * tile_size + tile_size // 2
|
||||
|
||||
|
||||
@@ -43,22 +48,30 @@ class Game:
|
||||
self.screen = pygame.display.set_mode((screen_w, screen_h))
|
||||
self.clock = pygame.time.Clock()
|
||||
self.font = pygame.font.SysFont("monospace", 22, bold=True)
|
||||
self.item_font = pygame.font.SysFont("Arial", 16, bold=True)
|
||||
self.running = True
|
||||
|
||||
self.active_monster = None
|
||||
self.buy_requested = False
|
||||
|
||||
# --- SOUND & SYSTEMS ---
|
||||
self.sound_manager = SoundManager()
|
||||
self.startscreen = Startscreen(self.screen)
|
||||
self.hud = HUD()
|
||||
self.ects = ECTS()
|
||||
self.tower = Tower()
|
||||
self.item_manager = ItemManager()
|
||||
self.shop = Shop(self.ects, self.item_manager.items, self.sound_manager)
|
||||
self.tower = Tower(self.sound_manager)
|
||||
self.state = "startscreen"
|
||||
self.player_name: str = ""
|
||||
self.game_start_time = 0
|
||||
self.endscreen: Optional[Endscreen] = None
|
||||
self.current_item_room_key: Optional[tuple[int, int, int, int]] = None
|
||||
|
||||
self.map = Map()
|
||||
self.active_door: Optional[Door] = None
|
||||
self.doors_by_room: dict[tuple[int, int, int, int], Door] = {}
|
||||
|
||||
# Use Niklas as the player sprite.
|
||||
assets_dir = Path(__file__).resolve().parent.parent / "assets"
|
||||
player_asset = Actor.load_sprite_asset(
|
||||
assets_dir / "niklas_sprites",
|
||||
@@ -76,8 +89,6 @@ class Game:
|
||||
self.monsters.spawn_initial(self.get_monster_spawn_blockers())
|
||||
|
||||
def run(self, max_frames: Optional[int] = None) -> None:
|
||||
"""Run the demo. ``max_frames`` is only used for automated smoke tests."""
|
||||
|
||||
frame_count = 0
|
||||
while self.running and (max_frames is None or frame_count < max_frames):
|
||||
dt = min(self.clock.tick(FPS) / 1000.0, 0.05)
|
||||
@@ -90,6 +101,8 @@ class Game:
|
||||
self.player_name = self.startscreen.player_name
|
||||
self.game_start_time = pygame.time.get_ticks()
|
||||
self.state = "game"
|
||||
self.sound_manager.play_bgm("normal")
|
||||
|
||||
elif self.state == "game":
|
||||
self.handle_events()
|
||||
self.update(dt)
|
||||
@@ -117,8 +130,18 @@ class Game:
|
||||
self.running = False
|
||||
elif event.key == pygame.K_SPACE:
|
||||
self.player.toggle_side()
|
||||
self.sound_manager.play_sfx("monster_transform")
|
||||
elif event.key == pygame.K_e:
|
||||
self.buy_requested = True
|
||||
|
||||
def update(self, dt: float) -> None:
|
||||
if self.player.is_bad:
|
||||
self.active_monster = self.player
|
||||
self.sound_manager.play_bgm("creepy")
|
||||
else:
|
||||
self.active_monster = None
|
||||
self.sound_manager.play_bgm("normal")
|
||||
|
||||
movement = self.get_player_movement(dt)
|
||||
self.player.move_by(movement)
|
||||
|
||||
@@ -128,6 +151,7 @@ class Game:
|
||||
self.player.update_animation(dt)
|
||||
self.ects.update(self.is_player_at_desk())
|
||||
self.handle_room_table_collision()
|
||||
self.update_items()
|
||||
self.monsters.update(
|
||||
dt,
|
||||
self.player.position,
|
||||
@@ -144,7 +168,7 @@ class Game:
|
||||
- (keys[pygame.K_LEFT] or keys[pygame.K_a]),
|
||||
(keys[pygame.K_DOWN] or keys[pygame.K_s])
|
||||
- (keys[pygame.K_UP] or keys[pygame.K_w]),
|
||||
)
|
||||
)
|
||||
|
||||
if movement.length_squared() == 0:
|
||||
return movement
|
||||
@@ -157,8 +181,6 @@ class Game:
|
||||
return movement.normalize() * PLAYER_SPEED * dt
|
||||
|
||||
def is_position_walkable(self, position: pygame.Vector2) -> bool:
|
||||
"""Check if the actor's feet are standing on a walkable map tile."""
|
||||
|
||||
column = int(position.x // tile_size)
|
||||
row = int(position.y // tile_size)
|
||||
return self.map.is_accessible(column, row)
|
||||
@@ -194,6 +216,41 @@ class Game:
|
||||
if not monster.is_alive:
|
||||
self.tower.clear()
|
||||
|
||||
def update_items(self) -> None:
|
||||
"""Spawn room items and process one player purchase request."""
|
||||
|
||||
player_rect = self.get_actor_rect(self.player)
|
||||
room_rect = self.map.get_current_room(player_rect)
|
||||
room_key = self.get_room_key(room_rect) if room_rect is not None else None
|
||||
|
||||
if room_key != self.current_item_room_key:
|
||||
self.item_manager.clear()
|
||||
self.current_item_room_key = room_key
|
||||
|
||||
self.item_manager.update(room_rect, self.get_item_spawn_blockers())
|
||||
|
||||
if self.buy_requested:
|
||||
self.try_buy_touching_item(player_rect)
|
||||
self.buy_requested = False
|
||||
|
||||
def try_buy_touching_item(self, player_rect: pygame.Rect) -> None:
|
||||
item = self.item_manager.get_touching_item(player_rect)
|
||||
if item is None:
|
||||
return
|
||||
|
||||
if self.shop.buy_item(item):
|
||||
self.apply_item_effect(item)
|
||||
self.item_manager.remove_item(item)
|
||||
|
||||
def apply_item_effect(self, item: Item) -> None:
|
||||
if item.item_type == "beer":
|
||||
self.ects.upgrade_rate(BEER_BOTTLE_ECTS_VALUE)
|
||||
elif item.item_type == "coffee" and self.active_door is not None:
|
||||
self.active_door.current_hp = min(
|
||||
self.active_door.max_hp,
|
||||
self.active_door.current_hp + COFFEE_HEAL_VALUE,
|
||||
)
|
||||
|
||||
def check_end_conditions(self) -> None:
|
||||
if self.active_door is not None and self.active_door.current_hp <= 0:
|
||||
self.show_endscreen(won=False)
|
||||
@@ -210,6 +267,9 @@ class Game:
|
||||
|
||||
def restart_game(self) -> None:
|
||||
self.ects = ECTS()
|
||||
self.shop = Shop(self.ects, self.item_manager.items, self.sound_manager)
|
||||
self.item_manager.clear()
|
||||
self.current_item_room_key = None
|
||||
self.tower.clear()
|
||||
self.active_door = None
|
||||
self.doors_by_room = {}
|
||||
@@ -238,7 +298,7 @@ class Game:
|
||||
return
|
||||
|
||||
def get_or_create_door_for_room(self, room_rect: pygame.Rect) -> Door:
|
||||
key = (room_rect.x, room_rect.y, room_rect.width, room_rect.height)
|
||||
key = self.get_room_key(room_rect)
|
||||
if key not in self.doors_by_room:
|
||||
self.doors_by_room[key] = self.create_door_for_room(room_rect)
|
||||
return self.doors_by_room[key]
|
||||
@@ -293,26 +353,35 @@ class Game:
|
||||
blockers.append(pygame.Rect(self.active_door.x, self.active_door.y, DOOR_WIDTH, DOOR_HEIGHT))
|
||||
for monster in self.monsters.monsters:
|
||||
blockers.append(monster.rect)
|
||||
blockers.extend(item.rect for item in self.item_manager.items)
|
||||
return blockers
|
||||
|
||||
def get_item_spawn_blockers(self) -> list[pygame.Rect]:
|
||||
"""Rects that room items should not spawn on."""
|
||||
|
||||
blockers = self.get_tower_spawn_blockers()
|
||||
if self.tower.tower_rect is not None:
|
||||
blockers.append(self.tower.tower_rect)
|
||||
blockers.extend(item.rect for item in self.item_manager.items)
|
||||
return blockers
|
||||
|
||||
@staticmethod
|
||||
def get_room_key(room_rect: pygame.Rect) -> tuple[int, int, int, int]:
|
||||
return room_rect.x, room_rect.y, room_rect.width, room_rect.height
|
||||
|
||||
@staticmethod
|
||||
def get_actor_rect(actor: Actor) -> pygame.Rect:
|
||||
frame = actor.get_frame()
|
||||
return frame.get_rect(midbottom=(round(actor.position.x), round(actor.position.y)))
|
||||
|
||||
def find_start_position(self) -> tuple[int, int]:
|
||||
"""Find the first S tile and place the actor's feet near its center."""
|
||||
|
||||
for row_index, row in enumerate(self.map.map):
|
||||
column_index = row.find("S")
|
||||
if column_index != -1:
|
||||
# The S tile is at the very top of the current map, so place the
|
||||
# actor one tile lower to keep the sprite visible on screen.
|
||||
return (
|
||||
column_index * tile_size + tile_size // 2,
|
||||
(row_index + 1) * tile_size + tile_size // 2,
|
||||
)
|
||||
|
||||
return tile_size, tile_size
|
||||
|
||||
def get_elapsed_seconds(self) -> int:
|
||||
@@ -327,6 +396,7 @@ class Game:
|
||||
if self.active_door is not None and self.active_door.is_closed:
|
||||
self.active_door.draw(self.screen)
|
||||
|
||||
self.item_manager.draw(self.screen, 0, 0, self.item_font)
|
||||
self.tower.draw(self.screen, self.font)
|
||||
|
||||
# Draw by Y position so actors lower on the map appear in front.
|
||||
|
||||
+30
-17
@@ -1,17 +1,27 @@
|
||||
import pygame
|
||||
import random
|
||||
from src.settings import (
|
||||
ITEM_SPAWN_INTERVAL, ITEM_SHOPPING_TIMER, ITEM_SIZE,
|
||||
COLOR_HP_BAR_YELLOW, COLOR_HP_BAR_BG
|
||||
BEER_COST,
|
||||
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:
|
||||
def __init__(self, x, y, item_type):
|
||||
self.x = x
|
||||
self.y = y
|
||||
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)
|
||||
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
|
||||
|
||||
# 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_y = self.y - cam_y
|
||||
|
||||
@@ -51,6 +61,12 @@ class Item:
|
||||
pygame.draw.rect(screen, color, draw_rect)
|
||||
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):
|
||||
return self.cost
|
||||
|
||||
@@ -93,14 +109,7 @@ class ItemManager:
|
||||
def update(self, room_rect, blocked_rects):
|
||||
now = pygame.time.get_ticks()
|
||||
|
||||
remaining_items = []
|
||||
|
||||
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
|
||||
self.items = [item for item in self.items if not item.is_expired()]
|
||||
|
||||
# spawn logic for new items
|
||||
if room_rect is not None:
|
||||
@@ -116,9 +125,13 @@ class ItemManager:
|
||||
# check if player press "e" to buy
|
||||
def check_collect(self, player_rect, keys):
|
||||
if keys[pygame.K_e]:
|
||||
for item in self.items:
|
||||
if player_rect.colliderect(item.rect):
|
||||
return item
|
||||
return self.get_touching_item(player_rect)
|
||||
return None
|
||||
|
||||
def get_touching_item(self, player_rect):
|
||||
for item in self.items:
|
||||
if player_rect.colliderect(item.rect):
|
||||
return item
|
||||
return None
|
||||
|
||||
# delete item from list after buying it
|
||||
@@ -127,9 +140,9 @@ class ItemManager:
|
||||
self.items.remove(item)
|
||||
|
||||
# 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:
|
||||
item.draw(screen, cam_x, cam_y)
|
||||
item.draw(screen, cam_x, cam_y, font)
|
||||
|
||||
# clear items when changing room
|
||||
def clear(self):
|
||||
|
||||
+1
-15
@@ -1,20 +1,6 @@
|
||||
import pygame
|
||||
from src.desk import Desk
|
||||
from src.settings import DESK_WIDTH, DESK_HEIGHT, tile_size
|
||||
|
||||
# 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
|
||||
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
|
||||
|
||||
|
||||
class Map:
|
||||
|
||||
@@ -119,6 +119,40 @@ HUD_Y_DOOR = 20
|
||||
HUD_Y_ZOMBIE = 50
|
||||
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_COLOR_FONT = (0, 0, 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
@@ -2,12 +2,17 @@ from src.item import Item
|
||||
|
||||
|
||||
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.buyable_items: list[Item] = buyable_items
|
||||
# save soundManager to use it in buy()
|
||||
self.sound_manager = sound_manager
|
||||
|
||||
def buy(self, 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:
|
||||
return False
|
||||
|
||||
@@ -15,9 +20,11 @@ class Shop:
|
||||
|
||||
# is_over_balance() means "has enough balance"
|
||||
if not self.ects.is_over_balance(cost):
|
||||
self.sound_manager.play_sfx("buy_fail")
|
||||
return False
|
||||
|
||||
self.ects.decrease_balance(cost)
|
||||
self.sound_manager.play_sfx("buy_success")
|
||||
return True
|
||||
|
||||
def get_item(self, item_name):
|
||||
|
||||
@@ -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
@@ -4,7 +4,13 @@ from typing import Optional
|
||||
|
||||
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
|
||||
@@ -13,29 +19,32 @@ fire_interval = 1000 # ms zwischen Schüssen
|
||||
|
||||
|
||||
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.projectiles: list[dict] = []
|
||||
self.projectiles: list[dict[str, float]] = []
|
||||
self.tower_start: Optional[int] = None
|
||||
self.last_shot: int = 0
|
||||
|
||||
# Sprite laden für den Tower
|
||||
try:
|
||||
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))
|
||||
except Exception as e:
|
||||
print(f"Error loading tower sprite: {e}")
|
||||
except Exception as error:
|
||||
print(f"Error loading tower sprite: {error}")
|
||||
self.tower_image = None
|
||||
|
||||
# Projectile sprite: the tower throws books.
|
||||
try:
|
||||
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))
|
||||
except Exception:
|
||||
self.projectile_image = pygame.transform.scale(
|
||||
self.projectile_image,
|
||||
(projectile_size, projectile_size),
|
||||
)
|
||||
except Exception as error:
|
||||
print(f"Error loading projectile sprite: {error}")
|
||||
self.projectile_image = None
|
||||
|
||||
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:
|
||||
return False
|
||||
@@ -44,12 +53,14 @@ class Tower:
|
||||
|
||||
rect = self._random_free_rect(room_rect, blocked_rects)
|
||||
if rect is None:
|
||||
self._play_sfx("buy_fail")
|
||||
return False
|
||||
|
||||
ects.decrease_balance(TOWER_COST)
|
||||
self.tower_rect = rect
|
||||
self.tower_start = pygame.time.get_ticks()
|
||||
self.projectiles = []
|
||||
self._play_sfx("buy_success")
|
||||
return True
|
||||
|
||||
def update(
|
||||
@@ -66,17 +77,9 @@ class Tower:
|
||||
|
||||
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:
|
||||
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._shoot_at(monster_rect)
|
||||
self.last_shot = now
|
||||
|
||||
for projectile in self.projectiles[:]:
|
||||
@@ -89,17 +92,19 @@ class Tower:
|
||||
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
|
||||
self.projectiles.remove(projectile)
|
||||
self._play_sfx("monster_hit")
|
||||
|
||||
if self.tower_rect and self.tower_start and now - self.tower_start >= TOWER_TIMER:
|
||||
self.clear()
|
||||
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):
|
||||
if self.tower_rect and self.tower_start:
|
||||
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,
|
||||
@@ -107,15 +112,15 @@ class Tower:
|
||||
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)
|
||||
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(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:
|
||||
projectile_rect = pygame.Rect(
|
||||
@@ -124,7 +129,7 @@ class Tower:
|
||||
projectile_size,
|
||||
projectile_size,
|
||||
)
|
||||
if self.projectile_image:
|
||||
if self.projectile_image is not None:
|
||||
surface.blit(self.projectile_image, projectile_rect)
|
||||
else:
|
||||
pygame.draw.rect(surface, COLOR_PROJECTILE, projectile_rect)
|
||||
@@ -133,6 +138,28 @@ class Tower:
|
||||
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
|
||||
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
|
||||
def _random_free_rect(room_rect: pygame.Rect, blocked_rects: list[pygame.Rect]) -> Optional[pygame.Rect]:
|
||||
|
||||
Reference in New Issue
Block a user