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,
|
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"
|
||||||
|
|||||||
+90
-20
@@ -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,
|
||||||
@@ -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.
|
||||||
|
|||||||
+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):
|
||||||
|
|||||||
+1
-15
@@ -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:
|
||||||
|
|||||||
@@ -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
@@ -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):
|
||||||
|
|||||||
@@ -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}")
|
||||||
+55
-28
@@ -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:
|
||||||
|
if now - self.tower_start >= TOWER_TIMER:
|
||||||
self.clear()
|
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]:
|
||||||
|
|||||||
Reference in New Issue
Block a user