diff --git a/game/config.py b/game/config.py index 68d280d..ae65f37 100644 --- a/game/config.py +++ b/game/config.py @@ -32,10 +32,17 @@ COLOR_GRAY = (120, 120, 120) COLOR_DARK_GRAY = (60, 60, 60) COLOR_GOLD = (255, 215, 0) COLOR_HIGHLIGHT = (255, 255, 255, 80) +COLOR_DARK_BLUE = (30, 60, 150) +COLOR_DARK_GREEN = (30, 130, 50) +COLOR_BRIGHT_PURPLE = (180, 80, 255) +COLOR_BRIGHT_GREEN = (100, 255, 100) +COLOR_SILVER = (192, 192, 210) +COLOR_PINK = (255, 130, 170) +COLOR_LIGHT_BLUE = (100, 150, 255, 40) # Game params INITIAL_GOLD = 200 -INITIAL_LIVES = 20 +INITIAL_LIVES = 10 TOTAL_WAVES = 10 # Tower stats @@ -43,6 +50,9 @@ 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}, + "sniper": {"damage": 120, "range": 200, "fire_rate": 2.0, "price": 120, "color": COLOR_DARK_BLUE, "name": "狙击塔"}, + "poison": {"damage": 5, "range": 110, "fire_rate": 1.2, "price": 80, "color": COLOR_DARK_GREEN, "name": "毒塔", "poison_dps": 8, "poison_duration": 3.0}, + "lightning": {"damage": 40, "range": 140, "fire_rate": 1.0, "price": 150, "color": COLOR_BRIGHT_PURPLE, "name": "雷电塔", "chain_count": 2, "chain_range": 80, "chain_decay": 0.5}, } # Enemy stats @@ -51,6 +61,9 @@ ENEMY_DATA = { "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"}, + "swarm": {"hp": 30, "speed": 5, "reward": 3, "color": COLOR_BRIGHT_GREEN, "size": 7, "name": "虫群兵"}, + "shield": {"hp": 200, "speed": 1.5, "reward": 20, "color": COLOR_SILVER, "size": 15, "name": "护盾兵", "shield_hp": 200}, + "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) diff --git a/game/enemy.py b/game/enemy.py index 482e74b..a7ff96a 100644 --- a/game/enemy.py +++ b/game/enemy.py @@ -1,6 +1,7 @@ import pygame import math -from game.config import ENEMY_DATA, PATH_WAYPOINTS, COLOR_BLACK, COLOR_GREEN, COLOR_RED +from game.config import ENEMY_DATA, PATH_WAYPOINTS, COLOR_BLACK, COLOR_GREEN, COLOR_RED, COLOR_BLUE +from game.utils import distance class Enemy: @@ -17,6 +18,20 @@ class Enemy: self.alive = True self.reached_end = False + # Shield + self.max_shield = data.get("shield_hp", 0) + self.shield_hp = self.max_shield + + # Poison DoT + self.poison_timer = 0 + self.poison_dps = 0 + + # Healer + self.heal_range = data.get("heal_range", 0) + self.heal_amount = data.get("heal_amount", 0) + self.heal_interval = data.get("heal_interval", 2.0) + self.heal_timer = self.heal_interval + self.x, self.y = PATH_WAYPOINTS[0] self.waypoint_index = 1 self.slow_timer = 0 @@ -25,21 +40,50 @@ class Enemy: self.speed = self.base_speed * factor self.slow_timer = duration + def apply_poison(self, dps, duration): + self.poison_dps = dps + self.poison_timer = duration + def take_damage(self, damage): - self.hp -= damage + if self.shield_hp > 0: + absorbed = min(self.shield_hp, damage) + self.shield_hp -= absorbed + damage -= absorbed + if damage > 0: + self.hp -= damage if self.hp <= 0: self.hp = 0 self.alive = False - def update(self, dt): + def update(self, dt, enemies=None): if not self.alive: return + # Poison DoT + if self.poison_timer > 0: + self.poison_timer -= dt + self.hp -= self.poison_dps * dt + if self.hp <= 0: + self.hp = 0 + self.alive = False + return + + # Slow if self.slow_timer > 0: self.slow_timer -= dt if self.slow_timer <= 0: self.speed = self.base_speed + # Healer ability + if self.type == "healer" and enemies: + self.heal_timer -= dt + if self.heal_timer <= 0: + self.heal_timer = self.heal_interval + for e in enemies: + if e is not self and e.alive and distance(self.x, self.y, e.x, e.y) <= self.heal_range: + e.hp = min(e.hp + self.heal_amount, e.max_hp) + + # Movement if self.waypoint_index >= len(PATH_WAYPOINTS): self.reached_end = True self.alive = False @@ -62,14 +106,43 @@ class Enemy: if not self.alive: return ix, iy = int(self.x), int(self.y) + + # Healer aura + if self.type == "healer": + aura = pygame.Surface((self.heal_range * 2, self.heal_range * 2), pygame.SRCALPHA) + pygame.draw.circle(aura, (255, 130, 170, 30), (self.heal_range, self.heal_range), self.heal_range) + surface.blit(aura, (ix - self.heal_range, iy - self.heal_range)) + + # Poison tint + if self.poison_timer > 0: + pygame.draw.circle(surface, (0, 180, 0), (ix, iy), self.size + 3) + pygame.draw.circle(surface, self.color, (ix, iy), self.size) pygame.draw.circle(surface, COLOR_BLACK, (ix, iy), self.size, 2) + # Boss eyes 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) + # Shield ring + if self.type == "shield" and self.shield_hp > 0: + shield_ratio = self.shield_hp / self.max_shield + arc_color = COLOR_BLUE + pygame.draw.circle(surface, arc_color, (ix, iy), self.size + 4, 2) + + # Swarm wings + if self.type == "swarm": + for dx_off in [-4, 4]: + pygame.draw.line(surface, (200, 255, 200), (ix + dx_off, iy - 3), (ix + dx_off * 2, iy - 7), 1) + + # Healer cross + if self.type == "healer": + pygame.draw.rect(surface, (255, 255, 255), (ix - 1, iy - 5, 3, 10)) + pygame.draw.rect(surface, (255, 255, 255), (ix - 5, iy - 1, 10, 3)) + + # Health bar bar_w = self.size * 2 + 4 if self.type == "boss": bar_w = self.size * 3 @@ -79,3 +152,10 @@ class Enemy: 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)) + + # Shield bar + if self.max_shield > 0 and self.shield_hp > 0: + s_bar_h = 3 + s_ratio = self.shield_hp / self.max_shield + pygame.draw.rect(surface, (80, 80, 80), (bx, by - s_bar_h - 1, bar_w, s_bar_h)) + pygame.draw.rect(surface, COLOR_BLUE, (bx, by - s_bar_h - 1, int(bar_w * s_ratio), s_bar_h)) diff --git a/game/main.py b/game/main.py index 148229d..bbb0481 100644 --- a/game/main.py +++ b/game/main.py @@ -88,10 +88,13 @@ class Game: self.ui.selected_tower = None def _update(self, dt): + if self.ui.selected_tower is not None: + dt *= 0.3 + self.wave_mgr.update(dt, self.enemies) for e in self.enemies: - e.update(dt) + e.update(dt, self.enemies) if e.reached_end: self.lives -= 2 if e.type == "boss" else 1 if self.lives <= 0: @@ -141,6 +144,8 @@ class Game: self.font, self.small_font, ) + self.ui.draw_slow_overlay(self.screen) + if self.game_over: self.ui.draw_game_over(self.screen, self.won, self.big_font) diff --git a/game/projectile.py b/game/projectile.py index 1548d2e..5af6152 100644 --- a/game/projectile.py +++ b/game/projectile.py @@ -1,11 +1,14 @@ import pygame import math -from game.config import COLOR_WHITE, COLOR_ORANGE, COLOR_CYAN +from game.config import COLOR_WHITE, COLOR_ORANGE, COLOR_CYAN, COLOR_DARK_GREEN, COLOR_BRIGHT_PURPLE 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): + def __init__(self, x, y, target, damage, proj_type="arrow", splash=0, + slow_factor=0, slow_duration=0, + poison_dps=0, poison_duration=0, + chain_count=0, chain_range=0, chain_decay=0): self.x = x self.y = y self.target = target @@ -14,6 +17,11 @@ class Projectile: self.splash = splash self.slow_factor = slow_factor self.slow_duration = slow_duration + self.poison_dps = poison_dps + self.poison_duration = poison_duration + self.chain_count = chain_count + self.chain_range = chain_range + self.chain_decay = chain_decay self.speed = 400 self.alive = True @@ -39,6 +47,7 @@ class Projectile: 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: @@ -49,6 +58,33 @@ class Projectile: if self.proj_type == "slow" and self.target.alive: self.target.apply_slow(self.slow_factor, self.slow_duration) + if self.proj_type == "poison" and self.target.alive: + self.target.apply_poison(self.poison_dps, self.poison_duration) + + if self.proj_type == "lightning" and self.chain_count > 0: + self._chain_lightning(enemies, self.target, self.damage, self.chain_count) + + def _chain_lightning(self, enemies, source, damage, chains_left): + if chains_left <= 0: + return + hit = [self.target, source] + current = source + current_damage = damage + for _ in range(chains_left): + current_damage *= self.chain_decay + best = None + best_dist = self.chain_range + for e in enemies: + if e.alive and e not in hit: + d = distance(current.x, current.y, e.x, e.y) + if d < best_dist: + best_dist = d + best = e + if best: + best.take_damage(current_damage) + hit.append(best) + current = best + def draw(self, surface): if not self.alive: return @@ -59,3 +95,12 @@ class Projectile: pygame.draw.circle(surface, COLOR_ORANGE, (ix, iy), 5) elif self.proj_type == "slow": pygame.draw.circle(surface, COLOR_CYAN, (ix, iy), 4) + elif self.proj_type == "sniper": + pygame.draw.circle(surface, (150, 180, 255), (ix, iy), 4) + pygame.draw.circle(surface, COLOR_WHITE, (ix, iy), 2) + elif self.proj_type == "poison": + pygame.draw.circle(surface, COLOR_DARK_GREEN, (ix, iy), 5) + pygame.draw.circle(surface, (100, 255, 100), (ix, iy), 3) + elif self.proj_type == "lightning": + pygame.draw.circle(surface, COLOR_BRIGHT_PURPLE, (ix, iy), 5) + pygame.draw.circle(surface, (255, 255, 255), (ix, iy), 2) diff --git a/game/tower.py b/game/tower.py index 05164e8..5747547 100644 --- a/game/tower.py +++ b/game/tower.py @@ -22,6 +22,11 @@ class Tower: self.splash = data.get("splash", 0) self.slow_factor = data.get("slow_factor", 0) self.slow_duration = data.get("slow_duration", 0) + self.poison_dps = data.get("poison_dps", 0) + self.poison_duration = data.get("poison_duration", 0) + self.chain_count = data.get("chain_count", 0) + self.chain_range = data.get("chain_range", 0) + self.chain_decay = data.get("chain_decay", 0) self.cooldown = 0 self.target = None @@ -54,11 +59,16 @@ class Tower: if self.target and self.cooldown <= 0: self.cooldown = self.fire_rate - proj_type = {"arrow": "arrow", "cannon": "cannon", "slow": "slow"}[self.type] + proj_type = { + "arrow": "arrow", "cannon": "cannon", "slow": "slow", + "sniper": "sniper", "poison": "poison", "lightning": "lightning", + }[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, + poison_dps=self.poison_dps, poison_duration=self.poison_duration, + chain_count=self.chain_count, chain_range=self.chain_range, chain_decay=self.chain_decay, )) def draw(self, surface): @@ -74,6 +84,25 @@ class Tower: 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) + elif self.type == "sniper": + pts = [(ix, iy - 16), (ix + 14, iy + 12), (ix - 14, iy + 12)] + pygame.draw.polygon(surface, self.color, pts) + pygame.draw.polygon(surface, COLOR_BLACK, pts, 2) + pygame.draw.line(surface, (200, 200, 255), (ix, iy - 10), (ix, iy + 6), 2) + elif self.type == "poison": + pts = [] + for i in range(6): + angle = math.pi / 3 * i - math.pi / 6 + pts.append((ix + int(14 * math.cos(angle)), iy + int(14 * math.sin(angle)))) + pygame.draw.polygon(surface, self.color, pts) + pygame.draw.polygon(surface, COLOR_BLACK, pts, 2) + elif self.type == "lightning": + pts = [ + (ix - 2, iy - 14), (ix + 6, iy - 4), (ix, iy - 4), + (ix + 4, iy + 14), (ix - 4, iy + 2), (ix + 1, iy + 2), + ] + pygame.draw.polygon(surface, self.color, pts) + pygame.draw.polygon(surface, COLOR_BLACK, pts, 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) diff --git a/game/ui.py b/game/ui.py index e8de155..094d9e6 100644 --- a/game/ui.py +++ b/game/ui.py @@ -1,9 +1,10 @@ +import math 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, + TOWER_DATA, INITIAL_LIVES, TOTAL_WAVES, COLOR_LIGHT_BLUE, ) @@ -11,13 +12,17 @@ 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.wave_button = pygame.Rect(WINDOW_WIDTH - 120, WINDOW_HEIGHT - UI_BOTTOM_HEIGHT + 10, 110, 40) self._init_tower_buttons() def _init_tower_buttons(self): - types = ["arrow", "cannon", "slow"] + types = ["arrow", "cannon", "slow", "sniper", "poison", "lightning"] + btn_w = 120 + gap = 5 + total_w = len(types) * btn_w + (len(types) - 1) * gap + start_x = (WINDOW_WIDTH - 120 - 20 - total_w) // 2 + 10 for i, t in enumerate(types): - rect = pygame.Rect(10 + i * 160, WINDOW_HEIGHT - UI_BOTTOM_HEIGHT + 10, 150, 40) + rect = pygame.Rect(start_x + i * (btn_w + gap), WINDOW_HEIGHT - UI_BOTTOM_HEIGHT + 10, btn_w, 40) self.tower_buttons.append((rect, t)) def handle_click(self, pos, gold): @@ -45,11 +50,16 @@ class UI: 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)) + 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) - surface.blit(wave_text, (380, 10)) + surface.blit(wave_text, (310, 10)) + + # Slow-time indicator + if self.selected_tower: + slow_text = small_font.render("[ 子弹时间 ]", True, (100, 180, 255)) + surface.blit(slow_text, (500, 12)) # Bottom bar pygame.draw.rect(surface, COLOR_DARK_GRAY, (0, WINDOW_HEIGHT - UI_BOTTOM_HEIGHT, WINDOW_WIDTH, UI_BOTTOM_HEIGHT)) @@ -61,7 +71,7 @@ class UI: 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_x = rect.x + 18 icon_y = rect.y + rect.height // 2 if t == "arrow": pygame.draw.rect(surface, data["color"], (icon_x - 5, icon_y - 5, 10, 10)) @@ -70,9 +80,24 @@ class UI: 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) + elif t == "sniper": + pts = [(icon_x, icon_y-8), (icon_x+7, icon_y+6), (icon_x-7, icon_y+6)] + pygame.draw.polygon(surface, data["color"], pts) + elif t == "poison": + pts = [] + for i in range(6): + angle = math.pi / 3 * i - math.pi / 6 + pts.append((icon_x + int(7 * math.cos(angle)), icon_y + int(7 * math.sin(angle)))) + pygame.draw.polygon(surface, data["color"], pts) + elif t == "lightning": + pts = [ + (icon_x-1, icon_y-7), (icon_x+4, icon_y-2), (icon_x, icon_y-2), + (icon_x+2, icon_y+7), (icon_x-3, icon_y+1), (icon_x+1, icon_y+1), + ] + 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)) + surface.blit(name_text, (icon_x + 12, icon_y - 7)) # Wave button if has_more: @@ -115,3 +140,15 @@ class UI: 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) + + def draw_slow_overlay(self, surface): + if self.selected_tower is None: + return + border = pygame.Surface((WINDOW_WIDTH, WINDOW_HEIGHT), pygame.SRCALPHA) + pygame.draw.rect(border, (100, 150, 255, 25), (0, 0, WINDOW_WIDTH, WINDOW_HEIGHT)) + for side_w in [4]: + pygame.draw.rect(border, (100, 150, 255, 60), (0, 0, side_w, WINDOW_HEIGHT)) + pygame.draw.rect(border, (100, 150, 255, 60), (WINDOW_WIDTH - side_w, 0, side_w, WINDOW_HEIGHT)) + pygame.draw.rect(border, (100, 150, 255, 60), (0, 0, WINDOW_WIDTH, side_w)) + pygame.draw.rect(border, (100, 150, 255, 60), (0, WINDOW_HEIGHT - side_w, WINDOW_WIDTH, side_w)) + surface.blit(border, (0, 0)) diff --git a/game/wave.py b/game/wave.py index 9912250..b876265 100644 --- a/game/wave.py +++ b/game/wave.py @@ -3,15 +3,15 @@ from game.enemy import Enemy WAVE_DATA = [ [{"type": "normal", "count": 5, "interval": 1.0}], - [{"type": "normal", "count": 8, "interval": 0.8}], + [{"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": "fast", "count": 8, "interval": 0.5}], + [{"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": 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}], + [{"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}], ]