From 88b3255ab2e40c30aacc40f84b9e406be62a335c Mon Sep 17 00:00:00 2001 From: hefanyang Date: Wed, 20 May 2026 20:21:27 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B8=B8=E6=88=8F=E5=8F=AF=E4=BB=A5=E8=BF=90?= =?UTF-8?q?=E8=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- game/__init__.py | 0 game/config.py | 68 ++++++++++++++++++++ game/enemy.py | 81 ++++++++++++++++++++++++ game/main.py | 151 +++++++++++++++++++++++++++++++++++++++++++++ game/map.py | 60 ++++++++++++++++++ game/projectile.py | 61 ++++++++++++++++++ game/tower.py | 82 ++++++++++++++++++++++++ game/ui.py | 117 +++++++++++++++++++++++++++++++++++ game/utils.py | 9 +++ game/wave.py | 60 ++++++++++++++++++ requirements.txt | 1 + 11 files changed, 690 insertions(+) create mode 100644 game/__init__.py create mode 100644 game/config.py create mode 100644 game/enemy.py create mode 100644 game/main.py create mode 100644 game/map.py create mode 100644 game/projectile.py create mode 100644 game/tower.py create mode 100644 game/ui.py create mode 100644 game/utils.py create mode 100644 game/wave.py create mode 100644 requirements.txt diff --git a/game/__init__.py b/game/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/game/config.py b/game/config.py new file mode 100644 index 0000000..68d280d --- /dev/null +++ b/game/config.py @@ -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), +] diff --git a/game/enemy.py b/game/enemy.py new file mode 100644 index 0000000..482e74b --- /dev/null +++ b/game/enemy.py @@ -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)) diff --git a/game/main.py b/game/main.py new file mode 100644 index 0000000..148229d --- /dev/null +++ b/game/main.py @@ -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() diff --git a/game/map.py b/game/map.py new file mode 100644 index 0000000..3d99065 --- /dev/null +++ b/game/map.py @@ -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) diff --git a/game/projectile.py b/game/projectile.py new file mode 100644 index 0000000..1548d2e --- /dev/null +++ b/game/projectile.py @@ -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) diff --git a/game/tower.py b/game/tower.py new file mode 100644 index 0000000..05164e8 --- /dev/null +++ b/game/tower.py @@ -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) diff --git a/game/ui.py b/game/ui.py new file mode 100644 index 0000000..e8de155 --- /dev/null +++ b/game/ui.py @@ -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) diff --git a/game/utils.py b/game/utils.py new file mode 100644 index 0000000..6cb7270 --- /dev/null +++ b/game/utils.py @@ -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 diff --git a/game/wave.py b/game/wave.py new file mode 100644 index 0000000..9912250 --- /dev/null +++ b/game/wave.py @@ -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) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..18caa77 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +pygame>=2.5