游戏可以运行
This commit is contained in:
@@ -66,7 +66,7 @@ ENEMY_DATA = {
|
||||
"healer": {"hp": 150, "speed": 1.8, "reward": 30, "color": COLOR_PINK, "size": 12, "name": "治疗兵", "heal_range": 100, "heal_amount": 30, "heal_interval": 2.0},
|
||||
}
|
||||
|
||||
# Path waypoints (pixel coordinates)
|
||||
# Path waypoints (pixel coordinates) — default/legacy
|
||||
PATH_WAYPOINTS = [
|
||||
(0, 180),
|
||||
(160, 180),
|
||||
@@ -79,3 +79,75 @@ PATH_WAYPOINTS = [
|
||||
(760, 220),
|
||||
(800, 220),
|
||||
]
|
||||
|
||||
# Level definitions
|
||||
LEVELS = [
|
||||
{
|
||||
"name": "第一关 - 森林小径",
|
||||
"color": (44, 72, 44),
|
||||
"start_gold": 200,
|
||||
"start_lives": 10,
|
||||
"waypoints": [
|
||||
(0, 180), (160, 180), (160, 340), (400, 340),
|
||||
(400, 100), (600, 100), (600, 340), (760, 340),
|
||||
(760, 220), (800, 220),
|
||||
],
|
||||
"waves": [
|
||||
[{"type": "normal", "count": 5, "interval": 1.0}],
|
||||
[{"type": "normal", "count": 6, "interval": 0.8}, {"type": "swarm", "count": 4, "interval": 0.3}],
|
||||
[{"type": "normal", "count": 5, "interval": 0.8}, {"type": "fast", "count": 3, "interval": 0.6}],
|
||||
[{"type": "swarm", "count": 10, "interval": 0.25}, {"type": "normal", "count": 5, "interval": 0.7}],
|
||||
[{"type": "normal", "count": 5, "interval": 0.6}, {"type": "heavy", "count": 2, "interval": 1.5}, {"type": "boss", "count": 1, "interval": 2.0}],
|
||||
[{"type": "normal", "count": 8, "interval": 0.5}, {"type": "fast", "count": 5, "interval": 0.4}, {"type": "swarm", "count": 8, "interval": 0.2}],
|
||||
[{"type": "shield", "count": 3, "interval": 1.2}, {"type": "healer", "count": 2, "interval": 1.5}, {"type": "normal", "count": 6, "interval": 0.6}],
|
||||
[{"type": "heavy", "count": 4, "interval": 0.8}, {"type": "fast", "count": 5, "interval": 0.5}, {"type": "shield", "count": 3, "interval": 1.0}],
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "第二关 - 沙漠要塞",
|
||||
"color": (82, 77, 50),
|
||||
"start_gold": 180,
|
||||
"start_lives": 8,
|
||||
"waypoints": [
|
||||
(0, 60), (200, 60), (200, 180), (60, 180),
|
||||
(60, 300), (340, 300), (340, 180), (480, 180),
|
||||
(480, 300), (620, 300), (620, 100), (800, 100),
|
||||
],
|
||||
"waves": [
|
||||
[{"type": "normal", "count": 8, "interval": 0.8}],
|
||||
[{"type": "fast", "count": 6, "interval": 0.5}, {"type": "swarm", "count": 6, "interval": 0.25}],
|
||||
[{"type": "normal", "count": 6, "interval": 0.6}, {"type": "heavy", "count": 3, "interval": 1.2}],
|
||||
[{"type": "swarm", "count": 12, "interval": 0.2}, {"type": "shield", "count": 2, "interval": 1.5}],
|
||||
[{"type": "fast", "count": 8, "interval": 0.4}, {"type": "healer", "count": 2, "interval": 1.0}],
|
||||
[{"type": "heavy", "count": 5, "interval": 0.8}, {"type": "normal", "count": 8, "interval": 0.5}],
|
||||
[{"type": "shield", "count": 4, "interval": 1.0}, {"type": "fast", "count": 6, "interval": 0.4}, {"type": "boss", "count": 1, "interval": 2.0}],
|
||||
[{"type": "swarm", "count": 15, "interval": 0.15}, {"type": "heavy", "count": 4, "interval": 0.7}],
|
||||
[{"type": "normal", "count": 10, "interval": 0.4}, {"type": "shield", "count": 4, "interval": 0.8}, {"type": "healer", "count": 3, "interval": 1.0}],
|
||||
[{"type": "heavy", "count": 6, "interval": 0.6}, {"type": "fast", "count": 10, "interval": 0.3}, {"type": "boss", "count": 2, "interval": 2.0}],
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "第三关 - 暗夜城堡",
|
||||
"color": (35, 30, 45),
|
||||
"start_gold": 150,
|
||||
"start_lives": 8,
|
||||
"waypoints": [
|
||||
(0, 260), (120, 260), (120, 60), (280, 60),
|
||||
(280, 180), (440, 180), (440, 60), (560, 60),
|
||||
(560, 260), (680, 260), (680, 100), (800, 100),
|
||||
],
|
||||
"waves": [
|
||||
[{"type": "normal", "count": 8, "interval": 0.7}, {"type": "fast", "count": 4, "interval": 0.5}],
|
||||
[{"type": "heavy", "count": 4, "interval": 1.0}, {"type": "swarm", "count": 8, "interval": 0.2}],
|
||||
[{"type": "shield", "count": 4, "interval": 1.0}, {"type": "fast", "count": 6, "interval": 0.4}],
|
||||
[{"type": "healer", "count": 3, "interval": 1.0}, {"type": "normal", "count": 10, "interval": 0.5}],
|
||||
[{"type": "swarm", "count": 15, "interval": 0.15}, {"type": "heavy", "count": 5, "interval": 0.8}, {"type": "boss", "count": 1, "interval": 2.0}],
|
||||
[{"type": "shield", "count": 5, "interval": 0.8}, {"type": "healer", "count": 3, "interval": 0.8}, {"type": "fast", "count": 8, "interval": 0.3}],
|
||||
[{"type": "heavy", "count": 6, "interval": 0.6}, {"type": "swarm", "count": 12, "interval": 0.15}],
|
||||
[{"type": "normal", "count": 12, "interval": 0.3}, {"type": "shield", "count": 5, "interval": 0.7}, {"type": "boss", "count": 1, "interval": 2.0}],
|
||||
[{"type": "fast", "count": 12, "interval": 0.25}, {"type": "healer", "count": 4, "interval": 0.7}],
|
||||
[{"type": "heavy", "count": 8, "interval": 0.5}, {"type": "shield", "count": 6, "interval": 0.6}, {"type": "swarm", "count": 15, "interval": 0.12}],
|
||||
[{"type": "normal", "count": 12, "interval": 0.25}, {"type": "heavy", "count": 6, "interval": 0.5}, {"type": "fast", "count": 10, "interval": 0.2}, {"type": "boss", "count": 3, "interval": 2.0}],
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -5,7 +5,7 @@ from game.utils import distance
|
||||
|
||||
|
||||
class Enemy:
|
||||
def __init__(self, enemy_type):
|
||||
def __init__(self, enemy_type, waypoints=None):
|
||||
data = ENEMY_DATA[enemy_type]
|
||||
self.type = enemy_type
|
||||
self.max_hp = data["hp"]
|
||||
@@ -32,7 +32,8 @@ class Enemy:
|
||||
self.heal_interval = data.get("heal_interval", 2.0)
|
||||
self.heal_timer = self.heal_interval
|
||||
|
||||
self.x, self.y = PATH_WAYPOINTS[0]
|
||||
self.waypoints = waypoints or PATH_WAYPOINTS
|
||||
self.x, self.y = self.waypoints[0]
|
||||
self.waypoint_index = 1
|
||||
self.slow_timer = 0
|
||||
|
||||
@@ -84,12 +85,12 @@ class Enemy:
|
||||
e.hp = min(e.hp + self.heal_amount, e.max_hp)
|
||||
|
||||
# Movement
|
||||
if self.waypoint_index >= len(PATH_WAYPOINTS):
|
||||
if self.waypoint_index >= len(self.waypoints):
|
||||
self.reached_end = True
|
||||
self.alive = False
|
||||
return
|
||||
|
||||
tx, ty = PATH_WAYPOINTS[self.waypoint_index]
|
||||
tx, ty = self.waypoints[self.waypoint_index]
|
||||
dx = tx - self.x
|
||||
dy = ty - self.y
|
||||
dist = math.hypot(dx, dy)
|
||||
|
||||
162
game/main.py
162
game/main.py
@@ -4,7 +4,7 @@ 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,
|
||||
LEVELS,
|
||||
)
|
||||
from game.map import GameMap
|
||||
from game.enemy import Enemy
|
||||
@@ -23,19 +23,25 @@ class Game:
|
||||
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()
|
||||
self.title_font = pygame.font.SysFont("microsoftyahei", 36)
|
||||
self.state = "select"
|
||||
self.current_level_idx = 0
|
||||
self.ui = UI()
|
||||
|
||||
def reset(self):
|
||||
self.game_map = GameMap()
|
||||
def _start_level(self, level_idx):
|
||||
level = LEVELS[level_idx]
|
||||
self.current_level_idx = level_idx
|
||||
self.game_map = GameMap(level["waypoints"], level["color"])
|
||||
self.enemies = []
|
||||
self.towers = []
|
||||
self.projectiles = []
|
||||
self.wave_mgr = WaveManager()
|
||||
self.wave_mgr = WaveManager(level["waves"], level["waypoints"])
|
||||
self.ui = UI()
|
||||
self.gold = INITIAL_GOLD
|
||||
self.lives = INITIAL_LIVES
|
||||
self.gold = level["start_gold"]
|
||||
self.lives = level["start_lives"]
|
||||
self.game_over = False
|
||||
self.won = False
|
||||
self.state = "playing"
|
||||
|
||||
def run(self):
|
||||
while True:
|
||||
@@ -48,17 +54,35 @@ class Game:
|
||||
sys.exit()
|
||||
self._handle_event(event)
|
||||
|
||||
if not self.game_over:
|
||||
if self.state == "playing" and not self.game_over:
|
||||
self._update(dt)
|
||||
|
||||
self._draw()
|
||||
|
||||
def _handle_event(self, event):
|
||||
if self.state == "select":
|
||||
self._handle_select_event(event)
|
||||
return
|
||||
|
||||
if self.state == "playing":
|
||||
self._handle_playing_event(event)
|
||||
return
|
||||
|
||||
if self.state == "game_over":
|
||||
self._handle_game_over_event(event)
|
||||
|
||||
def _handle_select_event(self, event):
|
||||
if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
|
||||
pos = event.pos
|
||||
for i in range(len(LEVELS)):
|
||||
btn_rect = self._level_button_rect(i)
|
||||
if btn_rect.collidepoint(pos):
|
||||
self._start_level(i)
|
||||
return
|
||||
|
||||
if self.game_over:
|
||||
return
|
||||
def _handle_playing_event(self, event):
|
||||
if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
|
||||
pos = event.pos
|
||||
|
||||
action = self.ui.handle_click(pos, self.gold)
|
||||
if action == "wave":
|
||||
@@ -82,11 +106,43 @@ class Game:
|
||||
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 _handle_game_over_event(self, event):
|
||||
if event.type == pygame.KEYDOWN and event.key == pygame.K_r:
|
||||
self._start_level(self.current_level_idx)
|
||||
return
|
||||
|
||||
if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
|
||||
pos = event.pos
|
||||
# Restart button
|
||||
restart_rect = pygame.Rect(WINDOW_WIDTH // 2 - 80, WINDOW_HEIGHT // 2 + 50, 160, 45)
|
||||
if restart_rect.collidepoint(pos):
|
||||
self._start_level(self.current_level_idx)
|
||||
return
|
||||
|
||||
# Next level button (win only, not last level)
|
||||
if self.won and self.current_level_idx < len(LEVELS) - 1:
|
||||
next_rect = pygame.Rect(WINDOW_WIDTH // 2 - 80, WINDOW_HEIGHT // 2 + 105, 160, 45)
|
||||
if next_rect.collidepoint(pos):
|
||||
self._start_level(self.current_level_idx + 1)
|
||||
return
|
||||
|
||||
# Back to select button
|
||||
select_rect = pygame.Rect(WINDOW_WIDTH // 2 - 80, WINDOW_HEIGHT // 2 + 160, 160, 45)
|
||||
if select_rect.collidepoint(pos):
|
||||
self.state = "select"
|
||||
return
|
||||
|
||||
def _level_button_rect(self, idx):
|
||||
bw, bh = 300, 100
|
||||
col = idx % 3
|
||||
total_w = 3 * bw + 2 * 30
|
||||
start_x = (WINDOW_WIDTH - total_w) // 2
|
||||
start_y = 250
|
||||
return pygame.Rect(start_x + col * (bw + 30), start_y, bw, bh)
|
||||
|
||||
def _update(self, dt):
|
||||
if self.ui.selected_tower is not None:
|
||||
dt *= 0.3
|
||||
@@ -101,6 +157,7 @@ class Game:
|
||||
self.lives = 0
|
||||
self.game_over = True
|
||||
self.won = False
|
||||
self.state = "game_over"
|
||||
|
||||
for t in self.towers:
|
||||
t.update(dt, self.enemies, self.projectiles)
|
||||
@@ -116,12 +173,19 @@ class Game:
|
||||
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):
|
||||
total_waves = self.wave_mgr.total_waves
|
||||
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
|
||||
self.state = "game_over"
|
||||
|
||||
def _draw(self):
|
||||
if self.state == "select":
|
||||
self._draw_select()
|
||||
return
|
||||
|
||||
# Playing / game_over
|
||||
self.screen.fill(COLOR_BG)
|
||||
self.game_map.draw(self.screen)
|
||||
|
||||
@@ -142,15 +206,83 @@ class Game:
|
||||
self.wave_mgr.current_wave, self.wave_mgr.wave_active,
|
||||
self.wave_mgr.has_more_waves(),
|
||||
self.font, self.small_font,
|
||||
self.wave_mgr.total_waves,
|
||||
)
|
||||
|
||||
self.ui.draw_slow_overlay(self.screen)
|
||||
|
||||
if self.game_over:
|
||||
self.ui.draw_game_over(self.screen, self.won, self.big_font)
|
||||
if self.state == "game_over":
|
||||
self._draw_game_over()
|
||||
|
||||
pygame.display.flip()
|
||||
|
||||
def _draw_select(self):
|
||||
self.screen.fill(COLOR_BG)
|
||||
|
||||
title = self.title_font.render("塔防游戏 - 选择关卡", True, COLOR_WHITE)
|
||||
self.screen.blit(title, title.get_rect(center=(WINDOW_WIDTH // 2, 80)))
|
||||
|
||||
subtitle = self.small_font.render("点击关卡开始游戏", True, (180, 180, 180))
|
||||
self.screen.blit(subtitle, subtitle.get_rect(center=(WINDOW_WIDTH // 2, 130)))
|
||||
|
||||
level_colors = [(60, 100, 60), (120, 100, 50), (60, 50, 80)]
|
||||
for i, level in enumerate(LEVELS):
|
||||
btn_rect = self._level_button_rect(i)
|
||||
color = level_colors[i % len(level_colors)]
|
||||
pygame.draw.rect(self.screen, color, btn_rect, border_radius=10)
|
||||
pygame.draw.rect(self.screen, COLOR_WHITE, btn_rect, 2, border_radius=10)
|
||||
|
||||
name_text = self.font.render(level["name"], True, COLOR_WHITE)
|
||||
self.screen.blit(name_text, name_text.get_rect(center=(btn_rect.centerx, btn_rect.y + 25)))
|
||||
|
||||
info = f"{len(level['waves'])}波 | {level['start_gold']}金 | {level['start_lives']}命"
|
||||
info_text = self.small_font.render(info, True, (200, 200, 200))
|
||||
self.screen.blit(info_text, info_text.get_rect(center=(btn_rect.centerx, btn_rect.y + 55)))
|
||||
|
||||
diff = ["简单", "中等", "困难"][i]
|
||||
diff_colors = [COLOR_WHITE, (255, 200, 50), (255, 80, 80)][i]
|
||||
diff_text = self.small_font.render(diff, True, diff_colors)
|
||||
self.screen.blit(diff_text, diff_text.get_rect(center=(btn_rect.centerx, btn_rect.y + 80)))
|
||||
|
||||
pygame.display.flip()
|
||||
|
||||
def _draw_game_over(self):
|
||||
overlay = pygame.Surface((WINDOW_WIDTH, WINDOW_HEIGHT), pygame.SRCALPHA)
|
||||
overlay.fill((0, 0, 0, 150))
|
||||
self.screen.blit(overlay, (0, 0))
|
||||
|
||||
text = "胜利!" if self.won else "失败!"
|
||||
color = (255, 215, 0) if self.won else (220, 50, 50)
|
||||
rendered = self.big_font.render(text, True, color)
|
||||
self.screen.blit(rendered, rendered.get_rect(center=(WINDOW_WIDTH // 2, WINDOW_HEIGHT // 2 - 40)))
|
||||
|
||||
cx = WINDOW_WIDTH // 2
|
||||
|
||||
# Restart button
|
||||
restart_rect = pygame.Rect(cx - 80, WINDOW_HEIGHT // 2 + 50, 160, 45)
|
||||
pygame.draw.rect(self.screen, (80, 120, 80), restart_rect, border_radius=8)
|
||||
pygame.draw.rect(self.screen, COLOR_WHITE, restart_rect, 2, border_radius=8)
|
||||
r_text = self.font.render("重新开始", True, COLOR_WHITE)
|
||||
self.screen.blit(r_text, r_text.get_rect(center=restart_rect.center))
|
||||
|
||||
# Next level button (only on win, not last level)
|
||||
if self.won and self.current_level_idx < len(LEVELS) - 1:
|
||||
next_rect = pygame.Rect(cx - 80, WINDOW_HEIGHT // 2 + 105, 160, 45)
|
||||
pygame.draw.rect(self.screen, (50, 80, 150), next_rect, border_radius=8)
|
||||
pygame.draw.rect(self.screen, COLOR_WHITE, next_rect, 2, border_radius=8)
|
||||
n_text = self.font.render("下一关", True, COLOR_WHITE)
|
||||
self.screen.blit(n_text, n_text.get_rect(center=next_rect.center))
|
||||
|
||||
# Back to select
|
||||
select_rect = pygame.Rect(cx - 80, WINDOW_HEIGHT // 2 + 160, 160, 45)
|
||||
pygame.draw.rect(self.screen, (100, 100, 100), select_rect, border_radius=8)
|
||||
pygame.draw.rect(self.screen, COLOR_WHITE, select_rect, 2, border_radius=8)
|
||||
s_text = self.font.render("选择关卡", True, COLOR_WHITE)
|
||||
self.screen.blit(s_text, s_text.get_rect(center=select_rect.center))
|
||||
|
||||
hint = self.small_font.render("或按 R 重新开始", True, (180, 180, 180))
|
||||
self.screen.blit(hint, hint.get_rect(center=(cx, WINDOW_HEIGHT // 2 + 225)))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
Game().run()
|
||||
|
||||
19
game/map.py
19
game/map.py
@@ -2,20 +2,21 @@ 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,
|
||||
COLOR_GRASS, COLOR_PATH, COLOR_GRID_LINE, PATH_WAYPOINTS,
|
||||
)
|
||||
|
||||
|
||||
class GameMap:
|
||||
def __init__(self):
|
||||
def __init__(self, waypoints=None, bg_color=None):
|
||||
self.waypoints = waypoints or PATH_WAYPOINTS
|
||||
self.bg_color = bg_color or COLOR_GRASS
|
||||
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]
|
||||
for i in range(len(self.waypoints) - 1):
|
||||
x1, y1 = self.waypoints[i]
|
||||
x2, y2 = self.waypoints[i + 1]
|
||||
dx = x2 - x1
|
||||
dy = y2 - y1
|
||||
dist = math.hypot(dx, dy)
|
||||
@@ -52,9 +53,9 @@ class GameMap:
|
||||
row * CELL_SIZE + UI_TOP_HEIGHT,
|
||||
CELL_SIZE, CELL_SIZE,
|
||||
)
|
||||
color = COLOR_PATH if (col, row) in self.path_cells else COLOR_GRASS
|
||||
color = COLOR_PATH if (col, row) in self.path_cells else self.bg_color
|
||||
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)
|
||||
for i in range(len(self.waypoints) - 1):
|
||||
pygame.draw.line(surface, (180, 150, 120), self.waypoints[i], self.waypoints[i + 1], 3)
|
||||
|
||||
@@ -39,16 +39,19 @@ class Tower:
|
||||
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)
|
||||
wp_idx = min(e.waypoint_index, len(e.waypoints) - 1)
|
||||
progress = e.waypoint_index + (1 - distance(e.x, e.y, *e.waypoints[wp_idx]) / 100)
|
||||
if progress > best_progress:
|
||||
best_progress = progress
|
||||
best = e
|
||||
self.target = best
|
||||
|
||||
def _wp(self, idx):
|
||||
if self.target and idx < len(self.target.waypoints):
|
||||
return self.target.waypoints[idx]
|
||||
if self.target:
|
||||
return self.target.waypoints[-1]
|
||||
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):
|
||||
|
||||
@@ -4,7 +4,7 @@ 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, COLOR_LIGHT_BLUE,
|
||||
TOWER_DATA, COLOR_LIGHT_BLUE,
|
||||
)
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ class UI:
|
||||
return None
|
||||
return self.selected_tower
|
||||
|
||||
def draw(self, surface, gold, lives, wave_num, wave_active, has_more, font, small_font):
|
||||
def draw(self, surface, gold, lives, wave_num, wave_active, has_more, font, small_font, total_waves=10):
|
||||
# Top bar
|
||||
pygame.draw.rect(surface, COLOR_DARK_GRAY, (0, 0, WINDOW_WIDTH, UI_TOP_HEIGHT))
|
||||
|
||||
@@ -53,7 +53,7 @@ class UI:
|
||||
lives_text = font.render(f"生命: {lives}", True, COLOR_RED if lives <= 3 else COLOR_GREEN)
|
||||
surface.blit(lives_text, (160, 10))
|
||||
|
||||
wave_text = font.render(f"波次: {wave_num}/{TOTAL_WAVES}", True, COLOR_WHITE)
|
||||
wave_text = font.render(f"波次: {wave_num}/{total_waves}", True, COLOR_WHITE)
|
||||
surface.blit(wave_text, (310, 10))
|
||||
|
||||
# Slow-time indicator
|
||||
|
||||
26
game/wave.py
26
game/wave.py
@@ -1,22 +1,16 @@
|
||||
from game.enemy import Enemy
|
||||
from game.config import PATH_WAYPOINTS
|
||||
|
||||
|
||||
WAVE_DATA = [
|
||||
[{"type": "normal", "count": 5, "interval": 1.0}],
|
||||
[{"type": "normal", "count": 6, "interval": 0.8}, {"type": "swarm", "count": 4, "interval": 0.3}],
|
||||
[{"type": "normal", "count": 5, "interval": 0.8}, {"type": "fast", "count": 3, "interval": 0.6}],
|
||||
[{"type": "swarm", "count": 10, "interval": 0.25}, {"type": "normal", "count": 5, "interval": 0.7}],
|
||||
[{"type": "normal", "count": 5, "interval": 0.6}, {"type": "heavy", "count": 2, "interval": 1.5}, {"type": "boss", "count": 1, "interval": 2.0}],
|
||||
[{"type": "normal", "count": 8, "interval": 0.5}, {"type": "fast", "count": 5, "interval": 0.4}, {"type": "swarm", "count": 8, "interval": 0.2}],
|
||||
[{"type": "shield", "count": 3, "interval": 1.2}, {"type": "healer", "count": 2, "interval": 1.5}, {"type": "normal", "count": 6, "interval": 0.6}],
|
||||
[{"type": "heavy", "count": 4, "interval": 0.8}, {"type": "fast", "count": 5, "interval": 0.5}, {"type": "shield", "count": 3, "interval": 1.0}],
|
||||
[{"type": "swarm", "count": 15, "interval": 0.15}, {"type": "healer", "count": 3, "interval": 1.0}, {"type": "heavy", "count": 4, "interval": 0.7}],
|
||||
[{"type": "normal", "count": 10, "interval": 0.3}, {"type": "heavy", "count": 5, "interval": 0.5}, {"type": "fast", "count": 8, "interval": 0.3}, {"type": "shield", "count": 4, "interval": 0.8}, {"type": "healer", "count": 2, "interval": 1.0}, {"type": "boss", "count": 2, "interval": 2.0}],
|
||||
]
|
||||
|
||||
|
||||
class WaveManager:
|
||||
def __init__(self):
|
||||
def __init__(self, wave_data=None, waypoints=None):
|
||||
self.wave_data = wave_data or WAVE_DATA
|
||||
self.waypoints = waypoints or PATH_WAYPOINTS
|
||||
self.current_wave = 0
|
||||
self.spawn_queue = []
|
||||
self.spawn_timer = 0
|
||||
@@ -24,12 +18,12 @@ class WaveManager:
|
||||
self.all_waves_done = False
|
||||
|
||||
def start_next_wave(self):
|
||||
if self.current_wave >= len(WAVE_DATA):
|
||||
if self.current_wave >= len(self.wave_data):
|
||||
self.all_waves_done = True
|
||||
return False
|
||||
|
||||
self.spawn_queue = []
|
||||
for group in WAVE_DATA[self.current_wave]:
|
||||
for group in self.wave_data[self.current_wave]:
|
||||
for _ in range(group["count"]):
|
||||
self.spawn_queue.append((group["type"], group["interval"]))
|
||||
|
||||
@@ -38,6 +32,10 @@ class WaveManager:
|
||||
self.spawn_timer = 0
|
||||
return True
|
||||
|
||||
@property
|
||||
def total_waves(self):
|
||||
return len(self.wave_data)
|
||||
|
||||
def update(self, dt, enemies):
|
||||
if not self.wave_active:
|
||||
return
|
||||
@@ -50,11 +48,11 @@ class WaveManager:
|
||||
self.spawn_timer -= dt
|
||||
if self.spawn_timer <= 0:
|
||||
enemy_type, interval = self.spawn_queue.pop(0)
|
||||
enemies.append(Enemy(enemy_type))
|
||||
enemies.append(Enemy(enemy_type, self.waypoints))
|
||||
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)
|
||||
return self.current_wave < len(self.wave_data)
|
||||
|
||||
Reference in New Issue
Block a user