# -*- coding: utf-8 -*-
"""
明日方舟 7-18 爱国者之死 —— pygame 塔防复刻
运行： python arknights_7_18.py
操作：
  鼠标左键点击卡片 -> 选择干员
  鼠标左键点击战场 -> 部署干员（近战放路径，远程放高台）
  鼠标右键 / ESC -> 取消选择
  数字键 1-6 -> 快速选择干员
  E 键 -> 进入/退出地雷放置模式（地雷可放路径或高台）
  空格键 -> 引爆所有已放置的地雷
  R 键 -> 重新开始
"""

import pygame
import math
import random

pygame.init()

# ============================================================
# 基础常量
# ============================================================
TILE = 64
COLS, ROWS = 12, 8
GRID_W, GRID_H = COLS * TILE, ROWS * TILE
UI_H = 180
WIDTH, HEIGHT = GRID_W, GRID_H + UI_H
FPS = 60

screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Arknights 7-18 - 爱国者之死 (pygame)")
clock = pygame.time.Clock()


def _load_font(size, bold=False):
    for name in ["consola.ttf", "consolas.ttf", "cour.ttf", "arial.ttf"]:
        try:
            f = pygame.font.Font(name, size)
            f.set_bold(bold)
            return f
        except Exception:
            continue
    try:
        f = pygame.font.Font(None, size)
        f.set_bold(bold)
        return f
    except Exception:
        return pygame.font.Font(None, size)


F_S = _load_font(12)
F_M = _load_font(15, bold=True)
F_L = _load_font(24, bold=True)
F_XL = _load_font(40, bold=True)

# ---------- 颜色 ----------
C_BG = (18, 22, 30)
C_UI_BG = (26, 32, 44)
C_PATH = (120, 98, 72)       # 路径（地面，可放近战）
C_PATH_EDGE = (150, 122, 88)
C_HIGH = (55, 68, 92)         # 高台（可放远程）
C_HIGH_EDGE = (85, 105, 140)
C_GROUND = (30, 36, 44)       # 装饰格
C_GRIDLINE = (24, 28, 36)
C_TEXT = (225, 232, 240)
C_TEXT_DIM = (140, 152, 170)
C_COST = (255, 200, 60)
C_HP = (110, 230, 120)
C_MINE = (255, 220, 80)
C_ALTAR = (180, 80, 220)

# ============================================================
# 地图数据
# ============================================================
PATH_CELLS = set()
# 下方路径
for c in range(0, 7):
    PATH_CELLS.add((c, 5))
# 中路向上
for r in range(2, 6):
    PATH_CELLS.add((6, r))
# 上方路径向右
for c in range(6, 11):
    PATH_CELLS.add((c, 2))
# 右上方出口
for r in range(1, 3):
    PATH_CELLS.add((10, r))

# 高台格（远程位）
HIGH_GROUND = {
    (2, 1), (3, 1), (4, 1), (7, 1), (8, 1),
    (2, 3), (3, 3), (4, 3), (7, 3), (8, 3),
    (2, 4), (3, 4), (4, 4), (7, 4), (8, 4),
    (2, 6), (3, 6), (4, 6), (7, 6), (8, 6),
    (1, 7), (2, 7), (3, 7), (4, 7), (5, 7), (6, 7), (7, 7), (8, 7),
}

ALTAR_POS = (3, 6)

BOSS_ROUTE = [(-1, 5), (6, 5), (6, 2), (10, 2), (10, 1), (12, 1)]
NORMAL_ROUTE = [(-1, 5), (6, 5), (6, 2), (12, 2)]


def wp_px(cell):
    return pygame.Vector2(cell[0] * TILE + TILE / 2, cell[1] * TILE + TILE / 2)


def cell_type(c, r):
    if (c, r) in PATH_CELLS:
        return 'path'
    if (c, r) in HIGH_GROUND:
        return 'high'
    return 'ground'


# ============================================================
# 干员数据
# ============================================================
OP_DATA = {
    "VG": dict(name="先锋", cost=8, hp=1100, atk=200, interval=1.0,
               block=1, rng=1.4, melee=True, heal=0, color=(90, 180, 255)),
    "GD": dict(name="近卫", cost=14, hp=1600, atk=400, interval=1.2,
               block=2, rng=1.4, melee=True, heal=0, color=(255, 120, 120)),
    "DF": dict(name="重装", cost=18, hp=3000, atk=180, interval=1.4,
               block=3, rng=1.4, melee=True, heal=0, color=(255, 200, 90)),
    "SN": dict(name="狙击", cost=13, hp=750, atk=280, interval=1.0,
               block=0, rng=3.6, melee=False, heal=0, color=(150, 255, 180)),
    "CS": dict(name="术师", cost=19, hp=850, atk=500, interval=1.7,
               block=0, rng=3.2, melee=False, heal=0, color=(200, 140, 255)),
    "MD": dict(name="医疗", cost=15, hp=800, atk=0, interval=1.1,
               block=0, rng=2.8, melee=False, heal=350, color=(120, 255, 220)),
}
CARD_KEYS = ["VG", "GD", "DF", "SN", "CS", "MD"]

ENEMY_TYPES = {
    "hound":   dict(name="游击队猎犬", hp=2900, atk=350, interval=1.4,
                    speed=75, radius=14, reward=1, color=(220, 90, 80), melee=True),
    "soldier": dict(name="游击队战士", hp=3300, atk=300, interval=2.0,
                    speed=48, radius=16, reward=1, color=(200, 120, 90), melee=True),
    "sniper":  dict(name="游击队狙击手", hp=3000, atk=340, interval=2.5,
                    speed=52, radius=14, reward=2, color=(240, 140, 100), melee=False, rng=2.2),
    "mortar":  dict(name="游击队炮兵", hp=5500, atk=450, interval=4.8,
                    speed=30, radius=18, reward=3, color=(180, 100, 200), melee=False, rng=4.5),
    "assault": dict(name="游击队强袭兵", hp=3800, atk=360, interval=1.9,
                    speed=58, radius=16, reward=2, color=(160, 80, 220), melee=True),
}

BOSS_DATA = dict(
    name="爱国者",
    hp=45000,
    atk=1600,
    defense=500,
    magic_res=45,
    speed=0.4 * 50,
    interval=4.0,
    radius=28,
    reward=10,
)

# ============================================================
# 全局状态
# ============================================================
operators = []
enemies = []
effects = []
mines = []
mine_explosions = []

cost = 10.0
base_hp = 3
wave = 1
wave_timer = 30.0
spawn_timer = 2.0
game_over = False
victory = False
selected = None
mouse_pos = (0, 0)
boss_spawned = False
boss_ref = None

MINE_COUNT = 15
mines_left = MINE_COUNT
mine_mode = False

altar_timer = 4.0
altar_pulse = 0.0

CARD_W, CARD_H = 94, 118
CARD_Y = GRID_H + 26
CARD_X0 = 130
CARD_GAP = 5


# ============================================================
# 特效
# ============================================================
class Effect:
    def __init__(self, start, end, color, life=0.18, width=3):
        self.start = pygame.Vector2(start)
        self.end = pygame.Vector2(end)
        self.color = color
        self.life = life
        self.max_life = life
        self.width = width

    def update(self, dt):
        self.life -= dt

    def draw(self, surf):
        if self.life <= 0:
            return
        a = max(0.0, self.life / self.max_life)
        w = max(1, int(self.width * a))
        pygame.draw.line(surf, self.color, self.start, self.end, w)


class RingEffect:
    def __init__(self, pos, color, max_r, life=0.5):
        self.pos = pygame.Vector2(pos)
        self.color = color
        self.max_r = max_r
        self.life = life
        self.max_life = life

    def update(self, dt):
        self.life -= dt

    def draw(self, surf):
        if self.life <= 0:
            return
        t = 1 - self.life / self.max_life
        r = int(self.max_r * t)
        a = max(0, int(255 * (1 - t)))
        if r > 0:
            s = pygame.Surface((r * 2 + 4, r * 2 + 4), pygame.SRCALPHA)
            pygame.draw.circle(s, (*self.color, a), (r + 2, r + 2), r, 3)
            surf.blit(s, (self.pos.x - r - 2, self.pos.y - r - 2))


# ============================================================
# 干员
# ============================================================
class Operator:
    def __init__(self, key, col, row):
        d = OP_DATA[key]
        self.key = key
        self.d = d
        self.col, self.row = col, row
        self.pos = pygame.Vector2(col * TILE + TILE / 2, row * TILE + TILE / 2)
        self.max_hp = d["hp"]
        self.hp = self.max_hp
        self.atk = d["atk"]
        self.interval = d["interval"]
        self.rng = d["rng"]
        self.block = d["block"]
        self.melee = d["melee"]
        self.heal = d["heal"]
        self.color = d["color"]
        self.atk_timer = d["interval"] * 0.5
        self.blocked = []
        self.alive = True
        self.hit_flash = 0.0

    def take_damage(self, dmg):
        if not self.alive:
            return
        self.hp -= dmg
        self.hit_flash = 0.12
        if self.hp <= 0:
            self.hp = 0
            self.alive = False
            for e in self.blocked:
                if e.alive:
                    e.blocked_by = None
            self.blocked = []

    def update(self, dt):
        if self.hit_flash > 0:
            self.hit_flash -= dt
        self.blocked = [e for e in self.blocked if e.alive]

        self.atk_timer -= dt
        if self.atk_timer > 0:
            return

        if self.heal > 0:
            best, best_ratio = None, 1.0
            for op in operators:
                if not op.alive:
                    continue
                if (op.pos - self.pos).length() <= self.rng * TILE:
                    ratio = op.hp / op.max_hp
                    if ratio < best_ratio:
                        best_ratio, best = ratio, op
            if best is not None and best_ratio < 0.999:
                best.hp = min(best.max_hp, best.hp + self.heal)
                self.atk_timer = self.interval
                effects.append(Effect(self.pos, best.pos, (120, 255, 200)))
            return

        target, best_d = None, 1e18
        for e in enemies:
            if not e.alive:
                continue
            d = (e.pos - self.pos).length()
            if d <= self.rng * TILE and d < best_d:
                best_d, target = d, e
        if target is not None:
            dmg = self.atk
            if isinstance(target, PatriotBoss) and target.phase == "march":
                dmg = max(50, dmg * 0.15)
            target.take_damage(dmg)
            self.atk_timer = self.interval
            effects.append(Effect(self.pos, target.pos, self.color))

    def draw(self, surf):
        x, y = int(self.pos.x), int(self.pos.y)
        col = (255, 255, 255) if self.hit_flash > 0 else self.color
        pygame.draw.circle(surf, col, (x, y), 22)
        pygame.draw.circle(surf, (16, 20, 28), (x, y), 22, 3)
        t = F_M.render(self.key, True, (16, 20, 28))
        surf.blit(t, t.get_rect(center=(x, y)))
        w = 46
        ratio = self.hp / self.max_hp
        pygame.draw.rect(surf, (36, 40, 50), (x - w / 2, y + 26, w, 5))
        pygame.draw.rect(surf, C_HP, (x - w / 2, y + 26, w * ratio, 5))
        if self.block > 0:
            for i in range(self.block):
                cx = x - (self.block - 1) * 5 + i * 10
                filled = i < len(self.blocked)
                pygame.draw.circle(surf, (255, 210, 90) if filled else (70, 78, 92),
                                   (int(cx), y - 30), 3)


# ============================================================
# 普通敌人
# ============================================================
class Enemy:
    def __init__(self, etype, wave_no, route=None):
        d = ENEMY_TYPES[etype]
        self.etype = etype
        self.name = d["name"]
        self.max_hp = int(d["hp"] * (1 + 0.12 * (wave_no - 1)))
        self.hp = self.max_hp
        self.atk = d["atk"] * (1 + 0.08 * (wave_no - 1))
        self.interval = d["interval"]
        self.speed = d["speed"]
        self.radius = d["radius"]
        self.color = d["color"]
        self.reward = d["reward"]
        self.melee = d["melee"]
        self.rng = d.get("rng", 0)
        self.route = route or NORMAL_ROUTE
        self.wp = 0
        self.pos = wp_px(self.route[0])
        self.blocked_by = None
        self.atk_timer = self.interval * 0.5
        self.alive = True
        self.reached_goal = False
        self.hit_flash = 0.0
        self.damage_amp_timer = 0.0

    def take_damage(self, dmg):
        global cost
        if not self.alive:
            return
        if self.damage_amp_timer > 0:
            dmg *= 1.5
        self.hp -= dmg
        self.hit_flash = 0.1
        if self.hp <= 0:
            self.hp = 0
            self.alive = False
            cost = min(99.0, cost + self.reward)

    def update(self, dt):
        global base_hp
        if self.hit_flash > 0:
            self.hit_flash -= dt
        if self.damage_amp_timer > 0:
            self.damage_amp_timer -= dt

        if self.blocked_by is not None and not self.blocked_by.alive:
            self.blocked_by = None

        if self.blocked_by is not None:
            self.atk_timer -= dt
            if self.atk_timer <= 0:
                self.atk_timer = self.interval
                self.blocked_by.take_damage(self.atk)
            return

        if self.melee:
            for op in operators:
                if (op.alive and op.block > 0 and len(op.blocked) < op.block
                        and (self.pos - op.pos).length() < TILE * 1.35):
                    op.blocked.append(self)
                    self.blocked_by = op
                    self.atk_timer = self.interval * 0.6
                    return

        target_idx = self.wp + 1
        if target_idx >= len(self.route):
            self.alive = False
            self.reached_goal = True
            return
        target = wp_px(self.route[target_idx])
        delta = target - self.pos
        dist = delta.length()
        step = self.speed * dt
        if dist <= step:
            self.pos = target
            self.wp += 1
        else:
            self.pos += delta.normalize() * step

    def draw(self, surf):
        x, y = int(self.pos.x), int(self.pos.y)
        col = (255, 255, 255) if self.hit_flash > 0 else self.color
        pygame.draw.circle(surf, col, (x, y), self.radius)
        pygame.draw.circle(surf, (20, 24, 32), (x, y), self.radius, 2)
        if self.damage_amp_timer > 0:
            pygame.draw.circle(surf, (255, 255, 100), (x, y), self.radius + 4, 2)
        w = self.radius * 2 + 8
        ratio = self.hp / self.max_hp
        pygame.draw.rect(surf, (36, 40, 50), (x - w / 2, y - self.radius - 10, w, 5))
        pygame.draw.rect(surf, (235, 80, 80), (x - w / 2, y - self.radius - 10, w * ratio, 5))


# ============================================================
# 爱国者 Boss
# ============================================================
class PatriotBoss:
    def __init__(self, wave_no=1):
        d = BOSS_DATA
        self.name = d["name"]
        self.max_hp = d["hp"]
        self.hp = self.max_hp
        self.atk = d["atk"]
        self.defense = d["defense"]
        self.magic_res = d["magic_res"]
        self.speed = d["speed"]
        self.interval = d["interval"]
        self.radius = d["radius"]
        self.reward = d["reward"]
        self.route = BOSS_ROUTE
        self.wp = 0
        self.pos = wp_px(self.route[0])
        self.blocked_by = None
        self.atk_timer = self.interval * 0.5
        self.alive = True
        self.reached_goal = False
        self.hit_flash = 0.0
        self.damage_amp_timer = 0.0
        self.phase = "march"
        self.reborn_timer = 0.0
        self.invuln_timer = 0.0
        self.reborn_max = 60.0
        self.spear_timer = 8.0

    def take_damage(self, dmg):
        global cost
        if not self.alive:
            return
        if self.invuln_timer > 0 and self.phase == "destroy":
            return
        if self.damage_amp_timer > 0:
            dmg *= 1.5
        if self.phase == "march":
            dmg = max(20, dmg - self.defense * 0.3)
        elif self.phase == "destroy":
            dmg = max(30, dmg - self.defense * 0.15)

        self.hp -= dmg
        self.hit_flash = 0.1

        if self.hp <= 0:
            if self.phase == "march":
                self.hp = 0
                self.phase = "reborn"
                self.reborn_timer = self.reborn_max
                self.alive = True
                self.blocked_by = None
                for op in operators:
                    if self in op.blocked:
                        op.blocked.remove(self)
            elif self.phase == "destroy":
                self.hp = 0
                self.alive = False
                cost = min(99.0, cost + self.reward)

    def update(self, dt):
        global base_hp
        if self.hit_flash > 0:
            self.hit_flash -= dt
        if self.damage_amp_timer > 0:
            self.damage_amp_timer -= dt
        if self.invuln_timer > 0:
            self.invuln_timer -= dt

        if self.phase == "reborn":
            self.reborn_timer -= dt
            for op in operators:
                if op.alive and (op.pos - self.pos).length() < TILE * 1.75:
                    op.take_damage(self.atk * 0.0521 * dt * 60)
            if self.reborn_timer <= 0:
                self.phase = "destroy"
                self.hp = self.max_hp
                self.invuln_timer = 15.0
                self.spear_timer = 5.0
                effects.append(RingEffect(self.pos, (255, 80, 80), 160, 1.0))
            return

        if self.phase == "destroy":
            self.spear_timer -= dt
            if self.spear_timer <= 0:
                self.spear_timer = 15.0
                best, best_d = None, -1
                for op in operators:
                    if not op.alive:
                        continue
                    if cell_type(op.col, op.row) == 'high':
                        d = (op.pos - self.pos).length()
                        if d > best_d:
                            best_d, best = d, op
                if best is not None:
                    dmg = self.atk * 1.35
                    effects.append(Effect(self.pos, best.pos, (255, 60, 60), 0.4, 6))
                    best.take_damage(dmg)

            for op in operators:
                if op.alive and (op.pos - self.pos).length() < TILE * 1.75:
                    op.take_damage(self.atk * 0.0521 * dt * 60)

        if self.phase == "march":
            if self.blocked_by is not None and not self.blocked_by.alive:
                self.blocked_by = None
            if self.blocked_by is None:
                for op in operators:
                    if (op.alive and op.block > 0 and len(op.blocked) < op.block
                            and (self.pos - op.pos).length() < TILE * 1.5):
                        op.blocked.append(self)
                        self.blocked_by = op
                        self.atk_timer = self.interval * 0.6
                        break
        else:
            self.blocked_by = None

        if self.blocked_by is not None:
            self.atk_timer -= dt
            if self.atk_timer <= 0:
                self.atk_timer = self.interval
                for _ in range(4):
                    if self.blocked_by and self.blocked_by.alive:
                        self.blocked_by.take_damage(self.atk * 0.35)
            return

        speed = self.speed
        if self.phase == "destroy":
            speed *= 1.25

        target_idx = self.wp + 1
        if target_idx >= len(self.route):
            self.alive = False
            self.reached_goal = True
            base_hp -= 3
            return
        target = wp_px(self.route[target_idx])
        delta = target - self.pos
        dist = delta.length()
        step = speed * dt
        if dist <= step:
            self.pos = target
            self.wp += 1
        else:
            self.pos += delta.normalize() * step

    def draw(self, surf):
        x, y = int(self.pos.x), int(self.pos.y)
        if self.phase == "reborn":
            t = pygame.time.get_ticks() / 200
            col = (180, 80, 255) if int(t) % 2 == 0 else (120, 50, 200)
        elif self.phase == "destroy":
            col = (255, 60, 60) if self.hit_flash <= 0 else (255, 255, 255)
        else:
            col = (220, 50, 50) if self.hit_flash <= 0 else (255, 255, 255)
        if self.invuln_timer > 0:
            col = (255, 215, 0)

        pygame.draw.circle(surf, col, (x, y), self.radius)
        pygame.draw.circle(surf, (20, 20, 30), (x, y), self.radius, 4)

        nt = F_S.render(self.name, True, (255, 220, 220))
        surf.blit(nt, nt.get_rect(center=(x, y - self.radius - 18)))

        w = 100
        ratio = max(0, self.hp / self.max_hp)
        pygame.draw.rect(surf, (36, 40, 50), (x - w // 2, y - self.radius - 8, w, 7))
        hp_col = (255, 80, 80) if self.phase != "reborn" else (180, 80, 255)
        pygame.draw.rect(surf, hp_col, (x - w // 2, y - self.radius - 8, w * ratio, 7))

        if self.phase == "reborn":
            st = F_S.render(f"重生 {int(self.reborn_timer)}s", True, (200, 150, 255))
            surf.blit(st, st.get_rect(center=(x, y + self.radius + 14)))
        elif self.phase == "destroy":
            if self.invuln_timer > 0:
                st = F_S.render(f"无敌 {int(self.invuln_timer)}s", True, (255, 215, 0))
                surf.blit(st, st.get_rect(center=(x, y + self.radius + 14)))
        else:
            st = F_S.render("行军姿态", True, (255, 180, 180))
            surf.blit(st, st.get_rect(center=(x, y + self.radius + 14)))

        if self.damage_amp_timer > 0:
            pygame.draw.circle(surf, (255, 255, 100), (x, y), self.radius + 6, 3)


# ============================================================
# 游戏逻辑
# ============================================================
def reset():
    global cost, base_hp, wave, wave_timer, spawn_timer, game_over, victory
    global selected, boss_spawned, boss_ref, mines_left, mine_mode
    global altar_timer, altar_pulse
    operators.clear()
    enemies.clear()
    effects.clear()
    mines.clear()
    mine_explosions.clear()
    cost = 10.0
    base_hp = 3
    wave = 1
    wave_timer = 30.0
    spawn_timer = 2.0
    game_over = False
    victory = False
    selected = None
    boss_spawned = False
    boss_ref = None
    mines_left = MINE_COUNT
    mine_mode = False
    altar_timer = 4.0
    altar_pulse = 0.0


def cell_free(c, r):
    for op in operators:
        if op.alive and op.col == c and op.row == r:
            return False
    return True


def can_deploy(key, c, r):
    """部署规则：
       近战干员 -> 只能放在路径（地面）格
       远程/医疗干员 -> 只能放在高台格
    """
    if not (0 <= c < COLS and 0 <= r < ROWS):
        return False
    ct = cell_type(c, r)
    d = OP_DATA[key]
    if d["melee"]:
        if ct != 'path':
            return False
    else:
        if ct != 'high':
            return False
    if not cell_free(c, r):
        return False
    return cost >= d["cost"]


def try_deploy(key, c, r):
    global cost
    if not can_deploy(key, c, r):
        return False
    cost -= OP_DATA[key]["cost"]
    operators.append(Operator(key, c, r))
    return True


def place_mine(c, r):
    """地雷可以放在路径或高台上"""
    global mines_left
    if mines_left <= 0:
        return False
    if not (0 <= c < COLS and 0 <= r < ROWS):
        return False
    ct = cell_type(c, r)
    if ct not in ('path', 'high'):
        return False
    for m in mines:
        if m[0] == c and m[1] == r:
            return False
    mines.append((c, r))
    mines_left -= 1
    return True


def detonate_all_mines():
    global boss_ref
    for (c, r) in mines:
        pos = pygame.Vector2(c * TILE + TILE / 2, r * TILE + TILE / 2)
        mine_explosions.append((pos, 0.4))
        effects.append(RingEffect(pos, (255, 220, 80), TILE * 2.5, 0.5))
        for e in enemies:
            if e.alive and (e.pos - pos).length() < TILE * 2.5:
                e.damage_amp_timer = 15.0
                e.take_damage(2000)
        if boss_ref and boss_ref.alive:
            if (boss_ref.pos - pos).length() < TILE * 2.5:
                boss_ref.damage_amp_timer = 15.0
                boss_ref.take_damage(2000)
    mines.clear()


def spawn_wave_enemy():
    w = wave
    roll = random.random()
    if w >= 5 and roll < 0.15:
        etype = "mortar"
    elif w >= 3 and roll < 0.35:
        etype = "assault"
    elif w >= 2 and roll < 0.55:
        etype = "sniper"
    elif roll < 0.75:
        etype = "hound"
    else:
        etype = "soldier"
    enemies.append(Enemy(etype, w))


def spawn_boss():
    global boss_ref
    boss_ref = PatriotBoss(wave)
    enemies.append(boss_ref)


def update(dt):
    global cost, base_hp, wave, wave_timer, spawn_timer, game_over, victory
    global boss_spawned, altar_timer, altar_pulse

    if game_over or victory:
        return

    cost = min(99.0, cost + dt * 0.7)

    altar_timer -= dt
    if altar_timer <= 0:
        altar_timer = 5.0
        altar_pulse = 0.6
        apos = pygame.Vector2(ALTAR_POS[0] * TILE + TILE / 2,
                              ALTAR_POS[1] * TILE + TILE / 2)
        effects.append(RingEffect(apos, C_ALTAR, TILE * 3, 0.8))
        for e in enemies:
            if e.alive and (e.pos - apos).length() < TILE * 3:
                if not isinstance(e, PatriotBoss):
                    e.take_damage(300)
        for op in operators:
            if op.alive and (op.pos - apos).length() < TILE * 3:
                op.take_damage(150)
    if altar_pulse > 0:
        altar_pulse -= dt

    wave_timer -= dt
    if wave_timer <= 0:
        wave += 1
        wave_timer = 30.0

    spawn_timer -= dt
    if spawn_timer <= 0:
        spawn_timer = max(1.0, 3.0 - wave * 0.2) * random.uniform(0.7, 1.3)
        if not boss_spawned and wave >= 2:
            spawn_boss()
            boss_spawned = True
        else:
            spawn_wave_enemy()

    for op in operators:
        if op.alive:
            op.update(dt)

    for e in enemies:
        if not e.alive:
            continue
        e.update(dt)
        if e.reached_goal:
            if not isinstance(e, PatriotBoss):
                base_hp -= 1

    for fx in effects:
        fx.update(dt)

    for i in range(len(mine_explosions) - 1, -1, -1):
        pos, t = mine_explosions[i]
        t -= dt
        if t <= 0:
            mine_explosions.pop(i)
        else:
            mine_explosions[i] = (pos, t)

    operators[:] = [o for o in operators if o.alive]
    enemies[:] = [e for e in enemies if e.alive]
    effects[:] = [f for f in effects if f.life > 0]

    if boss_ref and not boss_ref.alive and boss_ref.phase == "destroy":
        victory = True

    if base_hp <= 0:
        base_hp = 0
        game_over = True


# ============================================================
# 渲染
# ============================================================
def draw_map():
    for r in range(ROWS):
        for c in range(COLS):
            x, y = c * TILE, r * TILE
            ct = cell_type(c, r)
            if ct == 'path':
                pygame.draw.rect(screen, C_PATH, (x, y, TILE, TILE))
                # 地面纹理点
                for i in range(2):
                    for j in range(2):
                        pygame.draw.circle(screen, (140, 116, 88),
                                           (x + 16 + i * 32, y + 16 + j * 32), 2)
                pygame.draw.rect(screen, C_PATH_EDGE, (x, y, TILE, TILE), 1)
            elif ct == 'high':
                pygame.draw.rect(screen, C_HIGH, (x, y, TILE, TILE))
                # 高台斜纹
                pygame.draw.line(screen, (70, 85, 115),
                                 (x + 4, y + 4), (x + 20, y + 4), 2)
                pygame.draw.line(screen, (70, 85, 115),
                                 (x + 4, y + 4), (x + 4, y + 20), 2)
                pygame.draw.rect(screen, C_HIGH_EDGE, (x, y, TILE, TILE), 1)
            else:
                pygame.draw.rect(screen, C_GROUND, (x, y, TILE, TILE))
                pygame.draw.rect(screen, C_GRIDLINE, (x, y, TILE, TILE), 1)

    # 源石祭坛
    ax, ay = ALTAR_POS[0] * TILE, ALTAR_POS[1] * TILE
    pulse = 1.0 + 0.3 * math.sin(pygame.time.get_ticks() / 300)
    r = int(TILE * 0.4 * pulse)
    cx, cy = ax + TILE // 2, ay + TILE // 2
    s = pygame.Surface((r * 2 + 4, r * 2 + 4), pygame.SRCALPHA)
    pygame.draw.circle(s, (*C_ALTAR, 120), (r + 2, r + 2), r)
    screen.blit(s, (cx - r - 2, cy - r - 2))
    pygame.draw.circle(screen, C_ALTAR, (cx, cy), 6)
    at = F_S.render("祭坛", True, (220, 180, 255))
    screen.blit(at, at.get_rect(center=(cx, cy + 24)))


def draw_path_markers():
    for i in range(len(BOSS_ROUTE) - 1):
        a = wp_px(BOSS_ROUTE[i])
        b = wp_px(BOSS_ROUTE[i + 1])
        d = b - a
        if d.length() < 1:
            continue
        d = d.normalize()
        steps = int((b - a).length() // 50)
        for s in range(1, steps):
            p = a + d * (s * 50)
            if not (0 <= p.x < GRID_W and 0 <= p.y < GRID_H):
                continue
            n = pygame.Vector2(-d.y, d.x)
            p1 = p + d * 10
            p2 = p - d * 6 + n * 7
            p3 = p - d * 6 - n * 7
            pygame.draw.polygon(screen, (165, 138, 105), [p1, p2, p3])


def draw_hover():
    c = mouse_pos[0] // TILE
    r = mouse_pos[1] // TILE
    if not (0 <= c < COLS and 0 <= r < ROWS):
        return
    if mouse_pos[1] >= GRID_H:
        return

    for op in operators:
        if op.alive and op.col == c and op.row == r:
            s = pygame.Surface((WIDTH, GRID_H), pygame.SRCALPHA)
            pygame.draw.circle(s, (*op.color, 45),
                               (int(op.pos.x), int(op.pos.y)), int(op.rng * TILE))
            screen.blit(s, (0, 0))
            return

    if mine_mode:
        ct = cell_type(c, r)
        if ct in ('path', 'high'):
            col = (255, 220, 80)
            s = pygame.Surface((TILE, TILE), pygame.SRCALPHA)
            s.fill((*col, 60))
            screen.blit(s, (c * TILE, r * TILE))
            pygame.draw.rect(screen, col, (c * TILE, r * TILE, TILE, TILE), 2)
            # 爆炸范围预览
            s2 = pygame.Surface((WIDTH, GRID_H), pygame.SRCALPHA)
            pygame.draw.circle(s2, (*col, 30),
                               (c * TILE + TILE // 2, r * TILE + TILE // 2),
                               int(TILE * 2.5))
            screen.blit(s2, (0, 0))
        else:
            s = pygame.Surface((TILE, TILE), pygame.SRCALPHA)
            s.fill((235, 80, 80, 50))
            screen.blit(s, (c * TILE, r * TILE))
        return

    if selected is None:
        return

    ok = can_deploy(selected, c, r)
    col = (90, 230, 120) if ok else (235, 80, 80)
    s = pygame.Surface((TILE, TILE), pygame.SRCALPHA)
    s.fill((*col, 70))
    screen.blit(s, (c * TILE, r * TILE))
    pygame.draw.rect(screen, col, (c * TILE, r * TILE, TILE, TILE), 2)

    d = OP_DATA[selected]
    s2 = pygame.Surface((WIDTH, GRID_H), pygame.SRCALPHA)
    pygame.draw.circle(s2, (*col, 40),
                       (c * TILE + TILE // 2, r * TILE + TILE // 2), int(d["rng"] * TILE))
    screen.blit(s2, (0, 0))


def draw_ui():
    pygame.draw.rect(screen, C_UI_BG, (0, GRID_H, WIDTH, UI_H))
    pygame.draw.line(screen, (70, 84, 110), (0, GRID_H), (WIDTH, GRID_H), 2)

    cx, cy = 42, GRID_H + 44
    pygame.draw.circle(screen, C_COST, (cx, cy), 24)
    pygame.draw.circle(screen, (150, 110, 20), (cx, cy), 24, 3)
    t = F_L.render(str(int(cost)), True, (30, 30, 30))
    screen.blit(t, t.get_rect(center=(cx, cy)))
    screen.blit(F_S.render("COST", True, C_TEXT_DIM), (24, GRID_H + 72))

    hp_col = (255, 120, 120) if base_hp > 1 else (255, 60, 60)
    screen.blit(F_M.render(f"防线 {base_hp}", True, hp_col), (12, GRID_H + 96))
    screen.blit(F_S.render(f"波次 {wave}", True, (150, 200, 255)), (12, GRID_H + 120))
    screen.blit(F_S.render(f"敌人 {len(enemies)}", True, C_TEXT_DIM), (12, GRID_H + 142))

    mine_col = (255, 220, 80) if mines_left > 0 else (100, 100, 100)
    screen.blit(F_S.render(f"地雷 {mines_left}", True, mine_col), (12, GRID_H + 162))

    for i, key in enumerate(CARD_KEYS):
        d = OP_DATA[key]
        x = CARD_X0 + i * (CARD_W + CARD_GAP)
        rect = pygame.Rect(x, CARD_Y, CARD_W, CARD_H)
        affordable = cost >= d["cost"]
        hovered = rect.collidepoint(mouse_pos)

        bg = (34, 38, 46)
        if affordable:
            bg = (58, 72, 96) if hovered else (44, 54, 72)
        pygame.draw.rect(screen, bg, rect, border_radius=8)

        edge = d["color"] if affordable else (80, 86, 98)
        lw = 3 if selected == key else 2
        pygame.draw.rect(screen, edge, rect, lw, border_radius=8)

        pygame.draw.rect(screen, edge, (x + 10, CARD_Y + 10, CARD_W - 20, 4),
                         border_radius=2)

        num = F_S.render(str(i + 1), True, (100, 110, 130))
        screen.blit(num, (x + 6, CARD_Y + 6))

        kt = F_L.render(key, True, d["color"] if affordable else (110, 116, 128))
        screen.blit(kt, kt.get_rect(center=(x + CARD_W // 2, CARD_Y + 50)))

        nt = F_S.render(d["name"], True, C_TEXT_DIM if affordable else (90, 96, 108))
        screen.blit(nt, nt.get_rect(center=(x + CARD_W // 2, CARD_Y + 78)))

        ct = F_M.render(str(d["cost"]), True, C_COST if affordable else (120, 120, 120))
        screen.blit(ct, ct.get_rect(center=(x + CARD_W // 2, CARD_Y + 100)))

        # 部署位置提示（地面/高台）
        pos_tip = "地面" if d["melee"] else "高台"
        pt = F_S.render(pos_tip, True, (150, 160, 180))
        screen.blit(pt, pt.get_rect(center=(x + CARD_W // 2, CARD_Y + 114)))

    hint = "1-6选干员 | E放雷 | 空格引爆 | R重开 | 右键取消"
    ht = F_S.render(hint, True, (110, 122, 140))
    screen.blit(ht, (WIDTH - ht.get_width() - 12, GRID_H + 8))

    if mine_mode:
        mt = F_M.render("地雷放置模式 - 点击路径或高台放置 (E取消)", True, (255, 220, 80))
        screen.blit(mt, mt.get_rect(center=(WIDTH // 2, GRID_H + 14)))


def draw():
    screen.fill(C_BG)
    draw_map()
    draw_path_markers()
    draw_hover()

    for (c, r) in mines:
        x, y = c * TILE + TILE // 2, r * TILE + TILE // 2
        pulse = 0.8 + 0.2 * math.sin(pygame.time.get_ticks() / 200)
        pygame.draw.circle(screen, (255, 255, 150), (x, y), int(10 * pulse))
        pygame.draw.circle(screen, (200, 160, 20), (x, y), 12, 2)

    for (pos, t) in mine_explosions:
        r = int(TILE * 2.5 * (1 - t / 0.4))
        a = int(200 * (t / 0.4))
        if r > 0:
            s = pygame.Surface((r * 2 + 4, r * 2 + 4), pygame.SRCALPHA)
            pygame.draw.circle(s, (255, 200, 60, a), (r + 2, r + 2), r)
            screen.blit(s, (pos.x - r - 2, pos.y - r - 2))

    for fx in effects:
        fx.draw(screen)
    for op in operators:
        op.draw(screen)
    for e in enemies:
        e.draw(screen)

    draw_ui()

    if game_over or victory:
        overlay = pygame.Surface((WIDTH, HEIGHT), pygame.SRCALPHA)
        overlay.fill((0, 0, 0, 180))
        screen.blit(overlay, (0, 0))
        if victory:
            t = F_XL.render("行动成功", True, (120, 255, 160))
        else:
            t = F_XL.render("防线失守", True, (255, 90, 90))
        screen.blit(t, t.get_rect(center=(WIDTH // 2, HEIGHT // 2 - 50)))
        if victory:
            t2 = F_M.render("爱国者已被击溃", True, (220, 220, 220))
        else:
            t2 = F_M.render(f"坚持了 {wave} 波", True, (220, 220, 220))
        screen.blit(t2, t2.get_rect(center=(WIDTH // 2, HEIGHT // 2 + 10)))
        t3 = F_M.render("按 R 重新开始", True, (180, 190, 210))
        screen.blit(t3, t3.get_rect(center=(WIDTH // 2, HEIGHT // 2 + 50)))


# ============================================================
# 主循环
# ============================================================
def main():
    global selected, mouse_pos, mine_mode

    running = True
    while running:
        dt = clock.tick(FPS) / 1000.0
        dt = min(dt, 0.05)
        mouse_pos = pygame.mouse.get_pos()

        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                running = False

            elif event.type == pygame.KEYDOWN:
                if event.key == pygame.K_ESCAPE:
                    if mine_mode:
                        mine_mode = False
                    else:
                        selected = None
                elif event.key == pygame.K_r:
                    reset()
                elif event.key == pygame.K_e:
                    mine_mode = not mine_mode
                    selected = None
                elif event.key == pygame.K_SPACE:
                    if mines:
                        detonate_all_mines()
                elif event.key in (pygame.K_1, pygame.K_2, pygame.K_3,
                                   pygame.K_4, pygame.K_5, pygame.K_6):
                    idx = event.key - pygame.K_1
                    if idx < len(CARD_KEYS):
                        key = CARD_KEYS[idx]
                        selected = None if selected == key else key
                        mine_mode = False

            elif event.type == pygame.MOUSEBUTTONDOWN:
                if game_over or victory:
                    continue
                mx, my = event.pos

                if event.button == 3:
                    if mine_mode:
                        mine_mode = False
                    else:
                        selected = None

                elif event.button == 1:
                    if my >= GRID_H:
                        for i, key in enumerate(CARD_KEYS):
                            x = CARD_X0 + i * (CARD_W + CARD_GAP)
                            rect = pygame.Rect(x, CARD_Y, CARD_W, CARD_H)
                            if rect.collidepoint(mx, my):
                                selected = None if selected == key else key
                                mine_mode = False
                                break
                    else:
                        c, r = mx // TILE, my // TILE
                        if mine_mode:
                            if place_mine(c, r):
                                pass
                            if mines_left <= 0:
                                mine_mode = False
                        elif selected is not None:
                            if try_deploy(selected, c, r):
                                selected = None

        update(dt)
        draw()
        pygame.display.flip()

    pygame.quit()


if __name__ == "__main__":
    main()