implement missing stuff

This commit is contained in:
Niklas Haiden
2026-05-08 19:23:19 +02:00
17 changed files with 316 additions and 83 deletions
+38
View File
@@ -0,0 +1,38 @@
import wave, struct, math, os, random
"""
run once to get sounds generated - KI helped here to do this!
"""
SOUND_DIR = "../assets/sounds"
def save_wav(filename, data):
if not os.path.exists(SOUND_DIR):
os.makedirs(SOUND_DIR)
path = os.path.join(SOUND_DIR, filename)
with wave.open(path, 'w') as f:
f.setnchannels(1)
f.setsampwidth(2)
f.setframerate(44100)
for s in data:
# Den Soundwert in das richtige Format (16-bit PCM) umwandeln
f.writeframesraw(struct.pack('<h', int(s * 32767)))
def gen(dur, func, vol=0.4):
num_samples = int(dur * 44100)
return [func(i/44100, i) * vol * min(1.0, (num_samples-i)/(4410*dur)) for i in range(num_samples)]
print(f"Das Skript liegt in 'src', speichert aber nach: {os.path.abspath(SOUND_DIR)}")
save_wav("bgm_normal.wav", gen(10.0, lambda t, i: (math.sin(2*math.pi*110*t) + math.sin(2*math.pi*440*t)*0.2)/2, 0.2))
save_wav("bgm_creepy.wav", gen(10.0, lambda t, i: math.sin(2*math.pi*(60+5*math.sin(t))*t), 0.2))
save_wav("tower_shoot.wav", gen(0.15, lambda t, i: 1.0 if math.sin(2*math.pi*1200*(1-t*5)*t) > 0 else -1.0, 0.1))
save_wav("monster_hit.wav", gen(0.1, lambda t, i: random.uniform(-1, 1), 0.3))
save_wav("buy_success.wav", gen(0.3, lambda t, i: math.sin(2*math.pi*(660 if t<0.1 else 880)*t)))
save_wav("buy_fail.wav", gen(0.4, lambda t, i: math.sin(2*math.pi*120*t) * (1+math.sin(40*t))))
save_wav("monster_healed.wav", gen(0.8, lambda t, i: math.sin(2*math.pi*(440+800*t**2)*t)))
save_wav("monster_transform.wav", gen(1.2, lambda t, i: math.sin(2*math.pi*(200-150*t)*t) * random.uniform(0.5, 1.0)))
print("Fertig! Schau jetzt in deinem Hauptverzeichnis unter assets/sounds nach.")
+7
View File
@@ -8,6 +8,8 @@ from src.settings import (COLOR_ENDSCREEN_BACKGROUND,
COLOR_BUTTON,
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
View File
@@ -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
View File
@@ -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
View File
@@ -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:
+34
View File
@@ -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
View File
@@ -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):
+51
View File
@@ -0,0 +1,51 @@
import pygame
from src.settings import (
SFX_TOWER_SHOOT, SFX_MONSTER_HIT, SFX_BUY_SUCCESS,
SFX_BUY_FAIL, SFX_MONSTER_HEALED, SFX_MONSTER_TRANSFORM,
BGM_NORMAL, BGM_CREEPY, VOLUME_SFX, VOLUME_BGM
)
class SoundManager:
def __init__(self):
pygame.mixer.init()
self.effects = {}
self.current_bgm = None
self._load_sfx("tower_shoot", SFX_TOWER_SHOOT)
self._load_sfx("monster_hit", SFX_MONSTER_HIT)
self._load_sfx("buy_success", SFX_BUY_SUCCESS)
self._load_sfx("buy_fail", SFX_BUY_FAIL)
self._load_sfx("monster_healed", SFX_MONSTER_HEALED)
self._load_sfx("monster_transform", SFX_MONSTER_TRANSFORM)
# set volume via settings param
for sound in self.effects.values():
if sound:
sound.set_volume(VOLUME_SFX)
def _load_sfx(self, name, path):
try:
self.effects[name] = pygame.mixer.Sound(path)
except pygame.error:
print(f"Could not find sound: {path}")
self.effects[name] = None
def play_sfx(self, name):
sound = self.effects.get(name)
if isinstance(sound, pygame.mixer.Sound):
sound.play()
# set BackGroundMusic dependent if monster is active or tutor is active
def play_bgm(self, mode):
if self.current_bgm == mode:
return
path = BGM_NORMAL if mode == "normal" else BGM_CREEPY
try:
pygame.mixer.music.load(path)
pygame.mixer.music.set_volume(VOLUME_BGM)
pygame.mixer.music.play(-1)
self.current_bgm = mode
except pygame.error:
print(f"Could not find sound: {path}")
+56 -29
View File
@@ -4,7 +4,13 @@ from typing import Optional
import pygame
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]: