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
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.
+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]: