游戏可以运行
This commit is contained in:
0
game/__init__.py
Normal file
0
game/__init__.py
Normal file
68
game/config.py
Normal file
68
game/config.py
Normal 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
81
game/enemy.py
Normal 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
151
game/main.py
Normal 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
60
game/map.py
Normal 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
61
game/projectile.py
Normal 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
82
game/tower.py
Normal 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
117
game/ui.py
Normal 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
9
game/utils.py
Normal 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
60
game/wave.py
Normal 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)
|
||||
Reference in New Issue
Block a user