游戏可以运行

This commit is contained in:
2026-05-20 20:21:27 +08:00
parent 00908667dd
commit 88b3255ab2
11 changed files with 690 additions and 0 deletions

0
game/__init__.py Normal file
View File

68
game/config.py Normal file
View File

@@ -0,0 +1,68 @@
import pygame
# Window
WINDOW_WIDTH = 800
WINDOW_HEIGHT = 600
UI_TOP_HEIGHT = 40
UI_BOTTOM_HEIGHT = 60
GAME_HEIGHT = WINDOW_HEIGHT - UI_TOP_HEIGHT - UI_BOTTOM_HEIGHT
FPS = 60
# Grid
CELL_SIZE = 40
GRID_COLS = WINDOW_WIDTH // CELL_SIZE
GRID_ROWS = GAME_HEIGHT // CELL_SIZE
# Colors
COLOR_BG = (34, 40, 49)
COLOR_GRASS = (44, 62, 44)
COLOR_PATH = (139, 119, 101)
COLOR_GRID_LINE = (50, 58, 50)
COLOR_WHITE = (255, 255, 255)
COLOR_BLACK = (0, 0, 0)
COLOR_RED = (220, 50, 50)
COLOR_DARK_RED = (160, 30, 30)
COLOR_GREEN = (50, 200, 50)
COLOR_BLUE = (70, 130, 230)
COLOR_YELLOW = (230, 200, 50)
COLOR_PURPLE = (150, 50, 200)
COLOR_ORANGE = (230, 150, 50)
COLOR_CYAN = (50, 200, 200)
COLOR_GRAY = (120, 120, 120)
COLOR_DARK_GRAY = (60, 60, 60)
COLOR_GOLD = (255, 215, 0)
COLOR_HIGHLIGHT = (255, 255, 255, 80)
# Game params
INITIAL_GOLD = 200
INITIAL_LIVES = 20
TOTAL_WAVES = 10
# Tower stats
TOWER_DATA = {
"arrow": {"damage": 20, "range": 120, "fire_rate": 0.5, "price": 50, "color": COLOR_BLUE, "name": "箭塔"},
"cannon": {"damage": 80, "range": 100, "fire_rate": 1.5, "price": 100, "color": COLOR_ORANGE, "name": "炮塔", "splash": 60},
"slow": {"damage": 5, "range": 130, "fire_rate": 0.8, "price": 75, "color": COLOR_CYAN, "name": "减速塔", "slow_factor": 0.5, "slow_duration": 2.0},
}
# Enemy stats
ENEMY_DATA = {
"normal": {"hp": 100, "speed": 2, "reward": 10, "color": COLOR_RED, "size": 12, "name": "普通兵"},
"fast": {"hp": 60, "speed": 4, "reward": 8, "color": COLOR_YELLOW, "size": 10, "name": "快速兵"},
"heavy": {"hp": 300, "speed": 1, "reward": 25, "color": COLOR_PURPLE, "size": 14, "name": "重装兵"},
"boss": {"hp": 800, "speed": 0.8, "reward": 100, "color": COLOR_DARK_RED, "size": 20, "name": "BOSS"},
}
# Path waypoints (pixel coordinates)
PATH_WAYPOINTS = [
(0, 180),
(160, 180),
(160, 340),
(400, 340),
(400, 100),
(600, 100),
(600, 340),
(760, 340),
(760, 220),
(800, 220),
]

81
game/enemy.py Normal file
View File

@@ -0,0 +1,81 @@
import pygame
import math
from game.config import ENEMY_DATA, PATH_WAYPOINTS, COLOR_BLACK, COLOR_GREEN, COLOR_RED
class Enemy:
def __init__(self, enemy_type):
data = ENEMY_DATA[enemy_type]
self.type = enemy_type
self.max_hp = data["hp"]
self.hp = self.max_hp
self.base_speed = data["speed"]
self.speed = self.base_speed
self.reward = data["reward"]
self.color = data["color"]
self.size = data["size"]
self.alive = True
self.reached_end = False
self.x, self.y = PATH_WAYPOINTS[0]
self.waypoint_index = 1
self.slow_timer = 0
def apply_slow(self, factor, duration):
self.speed = self.base_speed * factor
self.slow_timer = duration
def take_damage(self, damage):
self.hp -= damage
if self.hp <= 0:
self.hp = 0
self.alive = False
def update(self, dt):
if not self.alive:
return
if self.slow_timer > 0:
self.slow_timer -= dt
if self.slow_timer <= 0:
self.speed = self.base_speed
if self.waypoint_index >= len(PATH_WAYPOINTS):
self.reached_end = True
self.alive = False
return
tx, ty = PATH_WAYPOINTS[self.waypoint_index]
dx = tx - self.x
dy = ty - self.y
dist = math.hypot(dx, dy)
move = self.speed * 60 * dt
if dist <= move:
self.x, self.y = tx, ty
self.waypoint_index += 1
else:
self.x += dx / dist * move
self.y += dy / dist * move
def draw(self, surface):
if not self.alive:
return
ix, iy = int(self.x), int(self.y)
pygame.draw.circle(surface, self.color, (ix, iy), self.size)
pygame.draw.circle(surface, COLOR_BLACK, (ix, iy), self.size, 2)
if self.type == "boss":
for offset in [(-6, -6), (6, -6)]:
pygame.draw.circle(surface, (255, 200, 0), (ix + offset[0], iy + offset[1]), 4)
pygame.draw.circle(surface, COLOR_BLACK, (ix + offset[0], iy + offset[1]), 4, 1)
bar_w = self.size * 2 + 4
if self.type == "boss":
bar_w = self.size * 3
bar_h = 4
bx = ix - bar_w // 2
by = iy - self.size - 8
ratio = self.hp / self.max_hp
pygame.draw.rect(surface, COLOR_RED, (bx, by, bar_w, bar_h))
pygame.draw.rect(surface, COLOR_GREEN, (bx, by, int(bar_w * ratio), bar_h))

151
game/main.py Normal file
View File

@@ -0,0 +1,151 @@
import sys
import pygame
from game.config import (
WINDOW_WIDTH, WINDOW_HEIGHT, FPS,
COLOR_BG, COLOR_WHITE, INITIAL_GOLD, INITIAL_LIVES,
UI_TOP_HEIGHT, UI_BOTTOM_HEIGHT, GAME_HEIGHT, TOWER_DATA,
TOTAL_WAVES,
)
from game.map import GameMap
from game.enemy import Enemy
from game.tower import Tower
from game.projectile import Projectile
from game.wave import WaveManager
from game.ui import UI
class Game:
def __init__(self):
pygame.init()
self.screen = pygame.display.set_mode((WINDOW_WIDTH, WINDOW_HEIGHT))
pygame.display.set_caption("塔防游戏")
self.clock = pygame.time.Clock()
self.font = pygame.font.SysFont("microsoftyahei", 20)
self.small_font = pygame.font.SysFont("microsoftyahei", 16)
self.big_font = pygame.font.SysFont("microsoftyahei", 48)
self.reset()
def reset(self):
self.game_map = GameMap()
self.enemies = []
self.towers = []
self.projectiles = []
self.wave_mgr = WaveManager()
self.ui = UI()
self.gold = INITIAL_GOLD
self.lives = INITIAL_LIVES
self.game_over = False
self.won = False
def run(self):
while True:
dt = self.clock.tick(FPS) / 1000.0
dt = min(dt, 0.05)
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
self._handle_event(event)
if not self.game_over:
self._update(dt)
self._draw()
def _handle_event(self, event):
if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
pos = event.pos
if self.game_over:
return
action = self.ui.handle_click(pos, self.gold)
if action == "wave":
if self.wave_mgr.has_more_waves() and not self.wave_mgr.wave_active:
self.wave_mgr.start_next_wave()
return
tower_type = self.ui.handle_grid_click(pos)
if tower_type:
col, row = self.game_map.pixel_to_grid(*pos)
if self.game_map.is_buildable(col, row):
occupied = any(t.col == col and t.row == row for t in self.towers)
price = TOWER_DATA[tower_type]["price"]
if not occupied and self.gold >= price:
px, py = self.game_map.grid_to_pixel(col, row)
self.towers.append(Tower(tower_type, col, row, px, py))
self.gold -= price
self.ui.selected_tower = None
return
self.ui.selected_tower = None
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_r and self.game_over:
self.reset()
if event.key == pygame.K_ESCAPE:
self.ui.selected_tower = None
def _update(self, dt):
self.wave_mgr.update(dt, self.enemies)
for e in self.enemies:
e.update(dt)
if e.reached_end:
self.lives -= 2 if e.type == "boss" else 1
if self.lives <= 0:
self.lives = 0
self.game_over = True
self.won = False
for t in self.towers:
t.update(dt, self.enemies, self.projectiles)
for p in self.projectiles:
p.update(dt, self.enemies)
for e in self.enemies:
if not e.alive and not e.reached_end:
self.gold += e.reward
e.reward = 0
self.enemies = [e for e in self.enemies if e.alive]
self.projectiles = [p for p in self.projectiles if p.alive]
if self.wave_mgr.all_waves_done or (self.wave_mgr.current_wave >= TOTAL_WAVES and not self.wave_mgr.wave_active and not self.enemies):
if not self.game_over:
self.game_over = True
self.won = True
def _draw(self):
self.screen.fill(COLOR_BG)
self.game_map.draw(self.screen)
for t in self.towers:
t.draw(self.screen)
for e in self.enemies:
e.draw(self.screen)
for p in self.projectiles:
p.draw(self.screen)
mx, my = pygame.mouse.get_pos()
self.ui.draw_placement_preview(self.screen, mx, my, self.game_map)
self.ui.draw(
self.screen, self.gold, self.lives,
self.wave_mgr.current_wave, self.wave_mgr.wave_active,
self.wave_mgr.has_more_waves(),
self.font, self.small_font,
)
if self.game_over:
self.ui.draw_game_over(self.screen, self.won, self.big_font)
pygame.display.flip()
if __name__ == "__main__":
Game().run()

60
game/map.py Normal file
View File

@@ -0,0 +1,60 @@
import pygame
import math
from game.config import (
CELL_SIZE, GRID_COLS, GRID_ROWS, UI_TOP_HEIGHT,
COLOR_GRASS, COLOR_PATH, COLOR_GRID_LINE,
PATH_WAYPOINTS,
)
class GameMap:
def __init__(self):
self.path_cells = set()
self._calc_path_cells()
def _calc_path_cells(self):
for i in range(len(PATH_WAYPOINTS) - 1):
x1, y1 = PATH_WAYPOINTS[i]
x2, y2 = PATH_WAYPOINTS[i + 1]
dx = x2 - x1
dy = y2 - y1
dist = math.hypot(dx, dy)
steps = int(dist / (CELL_SIZE / 2))
for s in range(steps + 1):
t = s / max(steps, 1)
px = x1 + dx * t
py = y1 + dy * t
col = int(px // CELL_SIZE)
row = int((py - UI_TOP_HEIGHT) // CELL_SIZE)
if 0 <= col < GRID_COLS and 0 <= row < GRID_ROWS:
self.path_cells.add((col, row))
def is_buildable(self, col, row):
if col < 0 or col >= GRID_COLS or row < 0 or row >= GRID_ROWS:
return False
return (col, row) not in self.path_cells
def pixel_to_grid(self, px, py):
col = int(px // CELL_SIZE)
row = int((py - UI_TOP_HEIGHT) // CELL_SIZE)
return col, row
def grid_to_pixel(self, col, row):
px = col * CELL_SIZE + CELL_SIZE // 2
py = row * CELL_SIZE + CELL_SIZE // 2 + UI_TOP_HEIGHT
return px, py
def draw(self, surface):
for row in range(GRID_ROWS):
for col in range(GRID_COLS):
rect = pygame.Rect(
col * CELL_SIZE,
row * CELL_SIZE + UI_TOP_HEIGHT,
CELL_SIZE, CELL_SIZE,
)
color = COLOR_PATH if (col, row) in self.path_cells else COLOR_GRASS
pygame.draw.rect(surface, color, rect)
pygame.draw.rect(surface, COLOR_GRID_LINE, rect, 1)
for i in range(len(PATH_WAYPOINTS) - 1):
pygame.draw.line(surface, (180, 150, 120), PATH_WAYPOINTS[i], PATH_WAYPOINTS[i + 1], 3)

61
game/projectile.py Normal file
View File

@@ -0,0 +1,61 @@
import pygame
import math
from game.config import COLOR_WHITE, COLOR_ORANGE, COLOR_CYAN
from game.utils import distance
class Projectile:
def __init__(self, x, y, target, damage, proj_type="arrow", splash=0, slow_factor=0, slow_duration=0):
self.x = x
self.y = y
self.target = target
self.damage = damage
self.proj_type = proj_type
self.splash = splash
self.slow_factor = slow_factor
self.slow_duration = slow_duration
self.speed = 400
self.alive = True
def update(self, dt, enemies):
if not self.alive:
return
if not self.target or not self.target.alive:
self.alive = False
return
tx, ty = self.target.x, self.target.y
dx = tx - self.x
dy = ty - self.y
dist = math.hypot(dx, dy)
move = self.speed * dt
if dist <= move:
self._hit(enemies)
else:
self.x += dx / dist * move
self.y += dy / dist * move
def _hit(self, enemies):
self.alive = False
if self.proj_type == "cannon" and self.splash > 0:
for e in enemies:
if e.alive and distance(self.target.x, self.target.y, e.x, e.y) <= self.splash:
e.take_damage(self.damage)
else:
self.target.take_damage(self.damage)
if self.proj_type == "slow" and self.target.alive:
self.target.apply_slow(self.slow_factor, self.slow_duration)
def draw(self, surface):
if not self.alive:
return
ix, iy = int(self.x), int(self.y)
if self.proj_type == "arrow":
pygame.draw.circle(surface, COLOR_WHITE, (ix, iy), 3)
elif self.proj_type == "cannon":
pygame.draw.circle(surface, COLOR_ORANGE, (ix, iy), 5)
elif self.proj_type == "slow":
pygame.draw.circle(surface, COLOR_CYAN, (ix, iy), 4)

82
game/tower.py Normal file
View File

@@ -0,0 +1,82 @@
import pygame
import math
from game.config import TOWER_DATA, COLOR_BLACK, COLOR_WHITE
from game.utils import distance
from game.projectile import Projectile
class Tower:
def __init__(self, tower_type, grid_col, grid_row, px, py):
data = TOWER_DATA[tower_type]
self.type = tower_type
self.col = grid_col
self.row = grid_row
self.x = px
self.y = py
self.damage = data["damage"]
self.range = data["range"]
self.fire_rate = data["fire_rate"]
self.price = data["price"]
self.color = data["color"]
self.name = data["name"]
self.splash = data.get("splash", 0)
self.slow_factor = data.get("slow_factor", 0)
self.slow_duration = data.get("slow_duration", 0)
self.cooldown = 0
self.target = None
def find_target(self, enemies):
self.target = None
best = None
best_progress = -1
for e in enemies:
if not e.alive:
continue
d = distance(self.x, self.y, e.x, e.y)
if d <= self.range:
progress = e.waypoint_index + (1 - distance(e.x, e.y, *self._wp(e.waypoint_index)) / 100)
if progress > best_progress:
best_progress = progress
best = e
self.target = best
def _wp(self, idx):
from game.config import PATH_WAYPOINTS
if idx < len(PATH_WAYPOINTS):
return PATH_WAYPOINTS[idx]
return PATH_WAYPOINTS[-1]
def update(self, dt, enemies, projectiles):
if self.cooldown > 0:
self.cooldown -= dt
self.find_target(enemies)
if self.target and self.cooldown <= 0:
self.cooldown = self.fire_rate
proj_type = {"arrow": "arrow", "cannon": "cannon", "slow": "slow"}[self.type]
projectiles.append(Projectile(
self.x, self.y, self.target, self.damage,
proj_type=proj_type, splash=self.splash,
slow_factor=self.slow_factor, slow_duration=self.slow_duration,
))
def draw(self, surface):
ix, iy = int(self.x), int(self.y)
if self.type == "arrow":
pygame.draw.rect(surface, self.color, (ix - 12, iy - 12, 24, 24))
pygame.draw.rect(surface, COLOR_BLACK, (ix - 12, iy - 12, 24, 24), 2)
elif self.type == "cannon":
pygame.draw.circle(surface, self.color, (ix, iy), 14)
pygame.draw.circle(surface, COLOR_BLACK, (ix, iy), 14, 2)
pygame.draw.circle(surface, (180, 100, 30), (ix, iy), 8)
elif self.type == "slow":
points = [(ix, iy - 14), (ix + 14, iy), (ix, iy + 14), (ix - 14, iy)]
pygame.draw.polygon(surface, self.color, points)
pygame.draw.polygon(surface, COLOR_BLACK, points, 2)
if self.target and self.target.alive:
pygame.draw.line(surface, (*self.color, ), (ix, iy), (int(self.target.x), int(self.target.y)), 1)
def draw_range(self, surface):
pygame.draw.circle(surface, COLOR_WHITE, (int(self.x), int(self.y)), self.range, 1)

117
game/ui.py Normal file
View File

@@ -0,0 +1,117 @@
import pygame
from game.config import (
WINDOW_WIDTH, WINDOW_HEIGHT, UI_TOP_HEIGHT, UI_BOTTOM_HEIGHT,
CELL_SIZE, COLOR_BLACK, COLOR_WHITE, COLOR_GOLD, COLOR_RED,
COLOR_GREEN, COLOR_DARK_GRAY, COLOR_GRAY, COLOR_BG,
TOWER_DATA, INITIAL_LIVES, TOTAL_WAVES,
)
class UI:
def __init__(self):
self.selected_tower = None
self.tower_buttons = []
self.wave_button = pygame.Rect(WINDOW_WIDTH - 140, WINDOW_HEIGHT - UI_BOTTOM_HEIGHT + 10, 130, 40)
self._init_tower_buttons()
def _init_tower_buttons(self):
types = ["arrow", "cannon", "slow"]
for i, t in enumerate(types):
rect = pygame.Rect(10 + i * 160, WINDOW_HEIGHT - UI_BOTTOM_HEIGHT + 10, 150, 40)
self.tower_buttons.append((rect, t))
def handle_click(self, pos, gold):
for rect, t in self.tower_buttons:
if rect.collidepoint(pos):
price = TOWER_DATA[t]["price"]
if gold >= price:
self.selected_tower = t if self.selected_tower != t else None
return None
if self.wave_button.collidepoint(pos):
return "wave"
return None
def handle_grid_click(self, pos):
if self.selected_tower is None:
return None
return self.selected_tower
def draw(self, surface, gold, lives, wave_num, wave_active, has_more, font, small_font):
# Top bar
pygame.draw.rect(surface, COLOR_DARK_GRAY, (0, 0, WINDOW_WIDTH, UI_TOP_HEIGHT))
gold_text = font.render(f"金币: {gold}", True, COLOR_GOLD)
surface.blit(gold_text, (10, 10))
lives_text = font.render(f"生命: {lives}", True, COLOR_RED if lives <= 5 else COLOR_GREEN)
surface.blit(lives_text, (200, 10))
wave_text = font.render(f"波次: {wave_num}/{TOTAL_WAVES}", True, COLOR_WHITE)
surface.blit(wave_text, (380, 10))
# Bottom bar
pygame.draw.rect(surface, COLOR_DARK_GRAY, (0, WINDOW_HEIGHT - UI_BOTTOM_HEIGHT, WINDOW_WIDTH, UI_BOTTOM_HEIGHT))
for rect, t in self.tower_buttons:
data = TOWER_DATA[t]
selected = self.selected_tower == t
bg = (80, 80, 120) if selected else COLOR_GRAY
pygame.draw.rect(surface, bg, rect, border_radius=5)
pygame.draw.rect(surface, COLOR_WHITE if selected else COLOR_BLACK, rect, 2, border_radius=5)
icon_x = rect.x + 20
icon_y = rect.y + rect.height // 2
if t == "arrow":
pygame.draw.rect(surface, data["color"], (icon_x - 5, icon_y - 5, 10, 10))
elif t == "cannon":
pygame.draw.circle(surface, data["color"], (icon_x, icon_y), 7)
elif t == "slow":
pts = [(icon_x, icon_y-7), (icon_x+7, icon_y), (icon_x, icon_y+7), (icon_x-7, icon_y)]
pygame.draw.polygon(surface, data["color"], pts)
name_text = small_font.render(f"{data['name']} {data['price']}G", True, COLOR_WHITE)
surface.blit(name_text, (icon_x + 15, icon_y - 7))
# Wave button
if has_more:
btn_color = COLOR_GREEN if not wave_active else COLOR_GRAY
pygame.draw.rect(surface, btn_color, self.wave_button, border_radius=5)
pygame.draw.rect(surface, COLOR_BLACK, self.wave_button, 2, border_radius=5)
btn_text = small_font.render("开始波次" if not wave_active else "进行中...", True, COLOR_WHITE)
surface.blit(btn_text, (self.wave_button.x + 10, self.wave_button.y + 12))
def draw_placement_preview(self, surface, mx, my, game_map):
if self.selected_tower is None:
return
col, row = game_map.pixel_to_grid(mx, my)
if col < 0 or row < 0:
return
px, py = game_map.grid_to_pixel(col, row)
data = TOWER_DATA[self.selected_tower]
buildable = game_map.is_buildable(col, row)
color = (*data["color"][:3], 60) if buildable else (255, 0, 0, 60)
preview = pygame.Surface((CELL_SIZE, CELL_SIZE), pygame.SRCALPHA)
preview.fill(color)
surface.blit(preview, (col * CELL_SIZE, row * CELL_SIZE + UI_TOP_HEIGHT))
if buildable:
pygame.draw.circle(surface, COLOR_WHITE, (px, py), data["range"], 1)
def draw_game_over(self, surface, won, font):
overlay = pygame.Surface((WINDOW_WIDTH, WINDOW_HEIGHT), pygame.SRCALPHA)
overlay.fill((0, 0, 0, 150))
surface.blit(overlay, (0, 0))
text = "胜利!" if won else "失败!"
color = COLOR_GOLD if won else COLOR_RED
rendered = font.render(text, True, color)
rect = rendered.get_rect(center=(WINDOW_WIDTH // 2, WINDOW_HEIGHT // 2 - 20))
surface.blit(rendered, rect)
hint = font.render("按 R 重新开始", True, COLOR_WHITE)
hint_rect = hint.get_rect(center=(WINDOW_WIDTH // 2, WINDOW_HEIGHT // 2 + 30))
surface.blit(hint, hint_rect)

9
game/utils.py Normal file
View File

@@ -0,0 +1,9 @@
import math
def distance(x1, y1, x2, y2):
return math.hypot(x2 - x1, y2 - y1)
def lerp(a, b, t):
return a + (b - a) * t

60
game/wave.py Normal file
View File

@@ -0,0 +1,60 @@
from game.enemy import Enemy
WAVE_DATA = [
[{"type": "normal", "count": 5, "interval": 1.0}],
[{"type": "normal", "count": 8, "interval": 0.8}],
[{"type": "normal", "count": 5, "interval": 0.8}, {"type": "fast", "count": 3, "interval": 0.6}],
[{"type": "fast", "count": 8, "interval": 0.5}],
[{"type": "normal", "count": 5, "interval": 0.6}, {"type": "heavy", "count": 2, "interval": 1.5}, {"type": "boss", "count": 1, "interval": 2.0}],
[{"type": "normal", "count": 10, "interval": 0.5}, {"type": "fast", "count": 5, "interval": 0.4}],
[{"type": "heavy", "count": 5, "interval": 1.0}, {"type": "fast", "count": 5, "interval": 0.5}],
[{"type": "normal", "count": 8, "interval": 0.4}, {"type": "heavy", "count": 3, "interval": 0.8}, {"type": "fast", "count": 6, "interval": 0.4}],
[{"type": "heavy", "count": 8, "interval": 0.7}, {"type": "fast", "count": 8, "interval": 0.3}],
[{"type": "normal", "count": 10, "interval": 0.3}, {"type": "heavy", "count": 5, "interval": 0.5}, {"type": "fast", "count": 10, "interval": 0.3}, {"type": "boss", "count": 2, "interval": 2.0}],
]
class WaveManager:
def __init__(self):
self.current_wave = 0
self.spawn_queue = []
self.spawn_timer = 0
self.wave_active = False
self.all_waves_done = False
def start_next_wave(self):
if self.current_wave >= len(WAVE_DATA):
self.all_waves_done = True
return False
self.spawn_queue = []
for group in WAVE_DATA[self.current_wave]:
for _ in range(group["count"]):
self.spawn_queue.append((group["type"], group["interval"]))
self.current_wave += 1
self.wave_active = True
self.spawn_timer = 0
return True
def update(self, dt, enemies):
if not self.wave_active:
return
if not self.spawn_queue:
if all(not e.alive for e in enemies):
self.wave_active = False
return
self.spawn_timer -= dt
if self.spawn_timer <= 0:
enemy_type, interval = self.spawn_queue.pop(0)
enemies.append(Enemy(enemy_type))
self.spawn_timer = self.spawn_queue[0][1] if self.spawn_queue else 0
def is_wave_complete(self):
return not self.wave_active and self.current_wave > 0
def has_more_waves(self):
return self.current_wave < len(WAVE_DATA)

1
requirements.txt Normal file
View File

@@ -0,0 +1 @@
pygame>=2.5