Merge branch 'main' into 'feat/tower'
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
class ECTS:
|
||||
def __init__(self):
|
||||
# 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
|
||||
def upgrade_rate(self, 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,
|
||||
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"
|
||||
|
||||
+18
-24
@@ -4,8 +4,6 @@ Controls:
|
||||
- WASD / arrow keys: move Knoll around the map
|
||||
- Space: switch between good/normal and bad/angry sprite
|
||||
- Esc / Q: quit
|
||||
|
||||
The demo also places a few bad/angry actors as monsters in the rooms.
|
||||
"""
|
||||
|
||||
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.settings import DEMO_ACTOR_HEIGHT, FPS, PLAYER_SPEED
|
||||
from src.startscreen import Startscreen
|
||||
|
||||
|
||||
# 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.
|
||||
from src.sounds import SoundManager
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -39,25 +33,26 @@ class Game:
|
||||
self.font = pygame.font.SysFont("monospace", 22, bold=True)
|
||||
self.running = True
|
||||
|
||||
self.active_monster = None
|
||||
|
||||
# --- SOUND & SYSTEMS ---
|
||||
self.sound_manager = SoundManager()
|
||||
self.startscreen = Startscreen(self.screen)
|
||||
|
||||
self.state = "startscreen"
|
||||
self.player_name: str = ""
|
||||
|
||||
self.map = Map()
|
||||
|
||||
# resolve the assets from the folder
|
||||
assets_dir = Path(__file__).resolve().parent.parent / "assets"
|
||||
actor_sides = Actor.load_sprite_sides(assets_dir, target_height=DEMO_ACTOR_HEIGHT)
|
||||
|
||||
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())
|
||||
|
||||
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)
|
||||
@@ -69,12 +64,13 @@ class Game:
|
||||
elif next_state == "game":
|
||||
self.player_name = self.startscreen.player_name
|
||||
self.state = "game"
|
||||
self.sound_manager.play_bgm("normal")
|
||||
|
||||
elif self.state == "game":
|
||||
self.handle_events()
|
||||
self.update(dt)
|
||||
self.draw()
|
||||
|
||||
|
||||
pygame.display.flip()
|
||||
frame_count += 1
|
||||
|
||||
@@ -89,8 +85,16 @@ class Game:
|
||||
self.running = False
|
||||
elif event.key == pygame.K_SPACE:
|
||||
self.player.toggle_side()
|
||||
self.sound_manager.play_sfx("monster_transform")
|
||||
|
||||
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)
|
||||
|
||||
@@ -106,7 +110,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
|
||||
@@ -119,33 +123,23 @@ 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)
|
||||
|
||||
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 draw(self) -> None:
|
||||
self.screen.fill(color_void)
|
||||
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):
|
||||
actor.draw(self.screen)
|
||||
|
||||
|
||||
+46
-2
@@ -1,9 +1,14 @@
|
||||
import json
|
||||
import pygame
|
||||
from src.settings import HIGHSCORE_COLOR_FONT, HIGHSCORE_COLOR_TABLE
|
||||
|
||||
class Highscore:
|
||||
def __init__(self):
|
||||
# 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
|
||||
try:
|
||||
@@ -89,4 +94,43 @@ class Highscore:
|
||||
|
||||
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
|
||||
|
||||
+5
-1
@@ -11,6 +11,7 @@ class Item:
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.item_type = item_type
|
||||
self.cost = 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)
|
||||
@@ -50,6 +51,9 @@ class Item:
|
||||
pygame.draw.rect(screen, color, draw_rect)
|
||||
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
|
||||
def _random_position(room_rect, blocked_rects):
|
||||
@@ -130,4 +134,4 @@ class ItemManager:
|
||||
# clear items when changing room
|
||||
def clear(self):
|
||||
self.items = []
|
||||
self.last_spawn_time = pygame.time.get_ticks()
|
||||
self.last_spawn_time = pygame.time.get_ticks()
|
||||
|
||||
+24
-4
@@ -1,7 +1,7 @@
|
||||
from typing import Mapping, Literal, Tuple
|
||||
|
||||
from src.map import tile_size
|
||||
from tower import projectile_size
|
||||
# Fenstergröße und Tile Größe (Felder)
|
||||
tile_size = 30
|
||||
|
||||
# --- DESK ---
|
||||
DESK_WIDTH = 70
|
||||
@@ -127,7 +127,6 @@ HUD_Y_DOOR = 20
|
||||
HUD_Y_ZOMBIE = 50
|
||||
HUD_Y_ECTS = 85
|
||||
|
||||
|
||||
# --- MAP ---
|
||||
# Fenstergröße und Tile Größe (Felder)
|
||||
tile_size = 30
|
||||
@@ -142,4 +141,25 @@ 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
|
||||
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,
|
||||
)
|
||||
from src.text_input import TextInput
|
||||
from src.highscore import Highscore
|
||||
|
||||
class Startscreen:
|
||||
def __init__(self, screen):
|
||||
@@ -42,6 +43,8 @@ class Startscreen:
|
||||
20,
|
||||
310)
|
||||
|
||||
self.highscore = Highscore()
|
||||
|
||||
def run(self):
|
||||
# set background color
|
||||
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,
|
||||
(255, 0, 0))
|
||||
|
||||
# draw highscore table
|
||||
self.highscore.draw(self.screen, 560, 90)
|
||||
|
||||
# get all events
|
||||
events = pygame.event.get()
|
||||
|
||||
|
||||
+10
-25
@@ -1,5 +1,3 @@
|
||||
from turtledemo.lindenmayer import draw
|
||||
|
||||
import pygame
|
||||
import random
|
||||
import math
|
||||
@@ -13,7 +11,9 @@ respawn_delay = 10000 # ms bis neues Feld spawnt
|
||||
|
||||
|
||||
class Tower:
|
||||
def __init__(self) -> None:
|
||||
def __init__(self, sound_manager) -> None:
|
||||
self.sound_manager = sound_manager
|
||||
|
||||
# Feld und Tower -> None wenn nicht aktiv
|
||||
# Position interagierbares Feld
|
||||
self.field_rect: pygame.Rect = None
|
||||
@@ -24,7 +24,7 @@ class Tower:
|
||||
# Zeitpunkt, wann der Tower gebaut wurde
|
||||
self.tower_start: int = None
|
||||
# Zeitpunkt, wann Tower despawnt ist
|
||||
self.despawn_timer: int = None
|
||||
self.despawn_time: int = None
|
||||
# Zeitpunkt des letzten Schusses
|
||||
self.last_shot: int = 0
|
||||
|
||||
@@ -65,9 +65,11 @@ class Tower:
|
||||
return False
|
||||
# prüft alle Bedingungen gleichzeitig
|
||||
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
|
||||
|
||||
# ECTS abziehen und Tower bauen
|
||||
self.sound_manager.play_sfx("buy_success")
|
||||
ects.balance -= TOWER_COST
|
||||
self.tower_rect = self.field_rect
|
||||
self.field_rect = None
|
||||
@@ -100,6 +102,7 @@ class Tower:
|
||||
"vel_y": (dy / distance) * PROJECTILE_SPEED,
|
||||
})
|
||||
|
||||
self.sound_manager.play_sfx("tower_shoot")
|
||||
self.last_shot = now
|
||||
|
||||
# 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
|
||||
if monster_rect and pygame.Rect(int(p["x"]), int(p["y"]), projectile_size, projectile_size).colliderect(monster_rect):
|
||||
total_damage += PROJECTILE_DMG
|
||||
self.sound_manager.play_sfx("monster_hit")
|
||||
self.projectiles.remove(p)
|
||||
|
||||
# wenn der Tower abgelaufen ist, soll er despawnen und ein Timer starten
|
||||
@@ -165,8 +169,7 @@ class Tower:
|
||||
else:
|
||||
# Fallback - gelbes Rechteck
|
||||
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.tower_rect = None
|
||||
self.projectiles = []
|
||||
self.despawn_timer = None
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
self.despawn_time = None
|
||||
Reference in New Issue
Block a user