Merge branch 'main' into 'feat/tower'
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 470 KiB |
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.")
|
||||||
@@ -2,6 +2,7 @@ import pygame
|
|||||||
|
|
||||||
from src.settings import START_ECTS_VALUE, ECTS_UPDATE_INTERVAL
|
from src.settings import START_ECTS_VALUE, ECTS_UPDATE_INTERVAL
|
||||||
|
|
||||||
|
|
||||||
class ECTS:
|
class ECTS:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
# total amount of ECTS the player has at the beginning
|
# total amount of ECTS the player has at the beginning
|
||||||
@@ -24,3 +25,9 @@ class ECTS:
|
|||||||
# beer increases amount of how much ECTS are earned per second
|
# beer increases amount of how much ECTS are earned per second
|
||||||
def upgrade_rate(self, extra_ects):
|
def upgrade_rate(self, extra_ects):
|
||||||
self.ects_per_second += extra_ects
|
self.ects_per_second += extra_ects
|
||||||
|
|
||||||
|
def is_over_balance(self, amount):
|
||||||
|
return self.ects_balance >= amount
|
||||||
|
|
||||||
|
def decrease_balance(self, amount):
|
||||||
|
self.ects_balance -= amount
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ from 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"
|
||||||
|
|||||||
+18
-24
@@ -4,8 +4,6 @@ Controls:
|
|||||||
- WASD / arrow keys: move Knoll around the map
|
- WASD / arrow keys: move Knoll around the map
|
||||||
- Space: switch between good/normal and bad/angry sprite
|
- Space: switch between good/normal and bad/angry sprite
|
||||||
- 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
|
||||||
@@ -19,15 +17,11 @@ from src.actors import Actor
|
|||||||
from src.map import Map, color_void, screen_h, screen_w, tile_size
|
from src.map import Map, color_void, screen_h, screen_w, tile_size
|
||||||
from src.settings import DEMO_ACTOR_HEIGHT, FPS, PLAYER_SPEED
|
from src.settings import DEMO_ACTOR_HEIGHT, FPS, PLAYER_SPEED
|
||||||
from src.startscreen import Startscreen
|
from src.startscreen import Startscreen
|
||||||
|
from src.sounds import SoundManager
|
||||||
|
|
||||||
# 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
|
||||||
|
|
||||||
|
|
||||||
@@ -39,25 +33,26 @@ class Game:
|
|||||||
self.font = pygame.font.SysFont("monospace", 22, bold=True)
|
self.font = pygame.font.SysFont("monospace", 22, bold=True)
|
||||||
self.running = True
|
self.running = True
|
||||||
|
|
||||||
|
self.active_monster = None
|
||||||
|
|
||||||
|
# --- SOUND & SYSTEMS ---
|
||||||
|
self.sound_manager = SoundManager()
|
||||||
self.startscreen = Startscreen(self.screen)
|
self.startscreen = Startscreen(self.screen)
|
||||||
|
|
||||||
self.state = "startscreen"
|
self.state = "startscreen"
|
||||||
self.player_name: str = ""
|
self.player_name: str = ""
|
||||||
|
|
||||||
self.map = Map()
|
self.map = Map()
|
||||||
|
|
||||||
# resolve the assets from the folder
|
|
||||||
assets_dir = Path(__file__).resolve().parent.parent / "assets"
|
assets_dir = Path(__file__).resolve().parent.parent / "assets"
|
||||||
actor_sides = Actor.load_sprite_sides(assets_dir, target_height=DEMO_ACTOR_HEIGHT)
|
actor_sides = Actor.load_sprite_sides(assets_dir, target_height=DEMO_ACTOR_HEIGHT)
|
||||||
|
|
||||||
if not actor_sides:
|
if not actor_sides:
|
||||||
raise RuntimeError("No normal_*/angry_* actor sprite pairs found in assets/")
|
raise RuntimeError(f"Keine Sprites in {assets_dir} gefunden!")
|
||||||
|
|
||||||
# Use the first pair for the player. With current assets this is Knoll.
|
|
||||||
self.player = Actor.from_sides(actor_sides[0], self.find_start_position())
|
self.player = Actor.from_sides(actor_sides[0], self.find_start_position())
|
||||||
|
|
||||||
def run(self, max_frames: Optional[int] = None) -> None:
|
def run(self, max_frames: Optional[int] = None) -> None:
|
||||||
"""Run the demo. ``max_frames`` is only used for automated smoke tests."""
|
|
||||||
|
|
||||||
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)
|
||||||
@@ -69,12 +64,13 @@ class Game:
|
|||||||
elif next_state == "game":
|
elif next_state == "game":
|
||||||
self.player_name = self.startscreen.player_name
|
self.player_name = self.startscreen.player_name
|
||||||
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)
|
||||||
self.draw()
|
self.draw()
|
||||||
|
|
||||||
|
|
||||||
pygame.display.flip()
|
pygame.display.flip()
|
||||||
frame_count += 1
|
frame_count += 1
|
||||||
|
|
||||||
@@ -89,8 +85,16 @@ 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")
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
@@ -106,7 +110,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
|
||||||
@@ -119,33 +123,23 @@ 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)
|
||||||
|
|
||||||
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 draw(self) -> None:
|
def draw(self) -> None:
|
||||||
self.screen.fill(color_void)
|
self.screen.fill(color_void)
|
||||||
self.map.draw_map(self.screen, self.font)
|
self.map.draw_map(self.screen, self.font)
|
||||||
|
|
||||||
|
|
||||||
# Draw by Y position so actors lower on the map appear in front.
|
|
||||||
for actor in sorted([self.player], key=lambda item: item.position.y):
|
for actor in sorted([self.player], key=lambda item: item.position.y):
|
||||||
actor.draw(self.screen)
|
actor.draw(self.screen)
|
||||||
|
|
||||||
|
|||||||
+45
-1
@@ -1,9 +1,14 @@
|
|||||||
import json
|
import json
|
||||||
|
import pygame
|
||||||
|
from src.settings import HIGHSCORE_COLOR_FONT, HIGHSCORE_COLOR_TABLE
|
||||||
|
|
||||||
class Highscore:
|
class Highscore:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
# fix path to json file with highscores
|
# fix path to json file with highscores
|
||||||
self.path = "./highscore.json"
|
self.path = "src/highscore.json"
|
||||||
|
|
||||||
|
self.font_title = pygame.font.SysFont("Arial", 28, bold=True)
|
||||||
|
self.font_score = pygame.font.SysFont("Arial", 18, bold=True)
|
||||||
|
|
||||||
# check if file already exists, otherwise create it
|
# check if file already exists, otherwise create it
|
||||||
try:
|
try:
|
||||||
@@ -90,3 +95,42 @@ class Highscore:
|
|||||||
formatted_scores.append(line)
|
formatted_scores.append(line)
|
||||||
|
|
||||||
return formatted_scores
|
return formatted_scores
|
||||||
|
|
||||||
|
def draw(self, screen, x, y):
|
||||||
|
# draw box background
|
||||||
|
box_rect = pygame.Rect(x, y, 500, 300)
|
||||||
|
pygame.draw.rect(screen, HIGHSCORE_COLOR_TABLE, box_rect, border_radius=12)
|
||||||
|
pygame.draw.rect(screen, HIGHSCORE_COLOR_TABLE, box_rect, border_radius=12)
|
||||||
|
|
||||||
|
# title
|
||||||
|
title = self.font_title.render(
|
||||||
|
"ECTS Leaderboard",
|
||||||
|
True,
|
||||||
|
HIGHSCORE_COLOR_FONT
|
||||||
|
)
|
||||||
|
screen.blit(title, (x + 20, y + 20))
|
||||||
|
|
||||||
|
# Get highscore lines
|
||||||
|
highscore_lines = self.get_top_scores_formatted()
|
||||||
|
|
||||||
|
|
||||||
|
if len(highscore_lines) == 0:
|
||||||
|
no_scores_text = self.font_score.render(
|
||||||
|
"No students ranked yet.",
|
||||||
|
True,
|
||||||
|
HIGHSCORE_COLOR_FONT
|
||||||
|
)
|
||||||
|
screen.blit(no_scores_text, (x + 20, y + 70))
|
||||||
|
return
|
||||||
|
|
||||||
|
# Draw scores
|
||||||
|
score_y = y + 70
|
||||||
|
|
||||||
|
for line in highscore_lines:
|
||||||
|
score_text = self.font_score.render(
|
||||||
|
line,
|
||||||
|
True,
|
||||||
|
HIGHSCORE_COLOR_FONT
|
||||||
|
)
|
||||||
|
screen.blit(score_text, (x + 20, score_y))
|
||||||
|
score_y += 35
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ class Item:
|
|||||||
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)
|
||||||
|
|
||||||
# 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)
|
||||||
@@ -50,6 +51,9 @@ 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)
|
||||||
|
|
||||||
|
def get_cost(self):
|
||||||
|
return self.cost
|
||||||
|
|
||||||
|
|
||||||
# try 50 times to find a free position to spawn item
|
# try 50 times to find a free position to spawn item
|
||||||
def _random_position(room_rect, blocked_rects):
|
def _random_position(room_rect, blocked_rects):
|
||||||
|
|||||||
+23
-3
@@ -1,7 +1,7 @@
|
|||||||
from typing import Mapping, Literal, Tuple
|
from typing import Mapping, Literal, Tuple
|
||||||
|
|
||||||
from src.map import tile_size
|
# Fenstergröße und Tile Größe (Felder)
|
||||||
from tower import projectile_size
|
tile_size = 30
|
||||||
|
|
||||||
# --- DESK ---
|
# --- DESK ---
|
||||||
DESK_WIDTH = 70
|
DESK_WIDTH = 70
|
||||||
@@ -127,7 +127,6 @@ HUD_Y_DOOR = 20
|
|||||||
HUD_Y_ZOMBIE = 50
|
HUD_Y_ZOMBIE = 50
|
||||||
HUD_Y_ECTS = 85
|
HUD_Y_ECTS = 85
|
||||||
|
|
||||||
|
|
||||||
# --- MAP ---
|
# --- MAP ---
|
||||||
# Fenstergröße und Tile Größe (Felder)
|
# Fenstergröße und Tile Größe (Felder)
|
||||||
tile_size = 30
|
tile_size = 30
|
||||||
@@ -143,3 +142,24 @@ color_void = (10, 10, 15) # a little more black
|
|||||||
color_text = (220, 180, 210) # rose
|
color_text = (220, 180, 210) # rose
|
||||||
color_border = (55, 35, 70)
|
color_border = (55, 35, 70)
|
||||||
color_start = (255, 0, 0)# dark violet
|
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
|
||||||
|
|||||||
+34
@@ -0,0 +1,34 @@
|
|||||||
|
from src.item import Item
|
||||||
|
|
||||||
|
|
||||||
|
class Shop:
|
||||||
|
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)
|
||||||
|
if item_to_buy is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
cost = item_to_buy.get_cost()
|
||||||
|
|
||||||
|
# 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):
|
||||||
|
for item in self.buyable_items:
|
||||||
|
if item.item_type == item_name:
|
||||||
|
return item
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_items(self):
|
||||||
|
return self.buyable_items
|
||||||
@@ -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}")
|
||||||
@@ -8,6 +8,7 @@ from src.settings import (
|
|||||||
COLOR_STARTSCREEN_FONT_LABEL,
|
COLOR_STARTSCREEN_FONT_LABEL,
|
||||||
)
|
)
|
||||||
from src.text_input import TextInput
|
from src.text_input import TextInput
|
||||||
|
from src.highscore import Highscore
|
||||||
|
|
||||||
class Startscreen:
|
class Startscreen:
|
||||||
def __init__(self, screen):
|
def __init__(self, screen):
|
||||||
@@ -42,6 +43,8 @@ class Startscreen:
|
|||||||
20,
|
20,
|
||||||
310)
|
310)
|
||||||
|
|
||||||
|
self.highscore = Highscore()
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
# set background color
|
# set background color
|
||||||
self.screen.fill(COLOR_STARTSCREEN_BACKGROUND)
|
self.screen.fill(COLOR_STARTSCREEN_BACKGROUND)
|
||||||
@@ -70,6 +73,9 @@ class Startscreen:
|
|||||||
label_input_error = self.font_label.render("Student not found. Please enter valid name/id.", True,
|
label_input_error = self.font_label.render("Student not found. Please enter valid name/id.", True,
|
||||||
(255, 0, 0))
|
(255, 0, 0))
|
||||||
|
|
||||||
|
# draw highscore table
|
||||||
|
self.highscore.draw(self.screen, 560, 90)
|
||||||
|
|
||||||
# get all events
|
# get all events
|
||||||
events = pygame.event.get()
|
events = pygame.event.get()
|
||||||
|
|
||||||
|
|||||||
+10
-25
@@ -1,5 +1,3 @@
|
|||||||
from turtledemo.lindenmayer import draw
|
|
||||||
|
|
||||||
import pygame
|
import pygame
|
||||||
import random
|
import random
|
||||||
import math
|
import math
|
||||||
@@ -13,7 +11,9 @@ respawn_delay = 10000 # ms bis neues Feld spawnt
|
|||||||
|
|
||||||
|
|
||||||
class Tower:
|
class Tower:
|
||||||
def __init__(self) -> None:
|
def __init__(self, sound_manager) -> None:
|
||||||
|
self.sound_manager = sound_manager
|
||||||
|
|
||||||
# Feld und Tower -> None wenn nicht aktiv
|
# Feld und Tower -> None wenn nicht aktiv
|
||||||
# Position interagierbares Feld
|
# Position interagierbares Feld
|
||||||
self.field_rect: pygame.Rect = None
|
self.field_rect: pygame.Rect = None
|
||||||
@@ -24,7 +24,7 @@ class Tower:
|
|||||||
# Zeitpunkt, wann der Tower gebaut wurde
|
# Zeitpunkt, wann der Tower gebaut wurde
|
||||||
self.tower_start: int = None
|
self.tower_start: int = None
|
||||||
# Zeitpunkt, wann Tower despawnt ist
|
# Zeitpunkt, wann Tower despawnt ist
|
||||||
self.despawn_timer: int = None
|
self.despawn_time: int = None
|
||||||
# Zeitpunkt des letzten Schusses
|
# Zeitpunkt des letzten Schusses
|
||||||
self.last_shot: int = 0
|
self.last_shot: int = 0
|
||||||
|
|
||||||
@@ -65,9 +65,11 @@ class Tower:
|
|||||||
return False
|
return False
|
||||||
# prüft alle Bedingungen gleichzeitig
|
# prüft alle Bedingungen gleichzeitig
|
||||||
if not (self.field_rect.colliderect(player_rect) and keys[pygame.K_e] and ects.balance >= TOWER_COST):
|
if not (self.field_rect.colliderect(player_rect) and keys[pygame.K_e] and ects.balance >= TOWER_COST):
|
||||||
|
self.sound_manager.play_sfx("buy_fail")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# ECTS abziehen und Tower bauen
|
# ECTS abziehen und Tower bauen
|
||||||
|
self.sound_manager.play_sfx("buy_success")
|
||||||
ects.balance -= TOWER_COST
|
ects.balance -= TOWER_COST
|
||||||
self.tower_rect = self.field_rect
|
self.tower_rect = self.field_rect
|
||||||
self.field_rect = None
|
self.field_rect = None
|
||||||
@@ -100,6 +102,7 @@ class Tower:
|
|||||||
"vel_y": (dy / distance) * PROJECTILE_SPEED,
|
"vel_y": (dy / distance) * PROJECTILE_SPEED,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
self.sound_manager.play_sfx("tower_shoot")
|
||||||
self.last_shot = now
|
self.last_shot = now
|
||||||
|
|
||||||
# Bewegung der Projektile und Kollision mit Monster prüfen
|
# Bewegung der Projektile und Kollision mit Monster prüfen
|
||||||
@@ -110,6 +113,7 @@ class Tower:
|
|||||||
# Projektil trifft auf Monster -> Schaden wird addiert und Projektil entfernt
|
# Projektil trifft auf Monster -> Schaden wird addiert und Projektil entfernt
|
||||||
if monster_rect and pygame.Rect(int(p["x"]), int(p["y"]), projectile_size, projectile_size).colliderect(monster_rect):
|
if monster_rect and pygame.Rect(int(p["x"]), int(p["y"]), projectile_size, projectile_size).colliderect(monster_rect):
|
||||||
total_damage += PROJECTILE_DMG
|
total_damage += PROJECTILE_DMG
|
||||||
|
self.sound_manager.play_sfx("monster_hit")
|
||||||
self.projectiles.remove(p)
|
self.projectiles.remove(p)
|
||||||
|
|
||||||
# wenn der Tower abgelaufen ist, soll er despawnen und ein Timer starten
|
# wenn der Tower abgelaufen ist, soll er despawnen und ein Timer starten
|
||||||
@@ -165,8 +169,7 @@ class Tower:
|
|||||||
else:
|
else:
|
||||||
# Fallback - gelbes Rechteck
|
# Fallback - gelbes Rechteck
|
||||||
pygame.draw.rect(surface, COLOR_PROJECTILE,
|
pygame.draw.rect(surface, COLOR_PROJECTILE,
|
||||||
pygame.Rect(int(p["x"]) - cam_x, int(p["y"]) - cam_y, projectile_size, projectile_size))
|
pygame.Rect(draw_x, draw_y, projectile_size, projectile_size))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -175,22 +178,4 @@ class Tower:
|
|||||||
self.field_rect = None
|
self.field_rect = None
|
||||||
self.tower_rect = None
|
self.tower_rect = None
|
||||||
self.projectiles = []
|
self.projectiles = []
|
||||||
self.despawn_timer = None
|
self.despawn_time = None
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user