🎮ArcadeLab

电子斗蛐蛐 - 终极完整版

by EpicCoder88
1418 lines77.5 KB
▶ Play
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>电子斗蛐蛐 - 终极完整版</title>
<style>
    body { margin: 0; padding: 0; background-color: #1a1a1a; display: flex; justify-content: center; align-items: center; height: 100vh; overflow: hidden; font-family: sans-serif; color: white; }
    .hidden { display: none !important; }
    #menu { text-align: center; }
    #startBtn { padding: 15px 40px; font-size: 24px; background-color: #4CAF50; color: white; border: none; border-radius: 8px; cursor: pointer; transition: background 0.3s; box-shadow: 0 4px 6px rgba(0,0,0,0.3); }
    #startBtn:hover { background-color: #45a049; }
    #selectScreen { display: flex; flex-direction: column; align-items: center; gap: 20px; }
    #selectScreen h2 { font-weight: normal; color: #aaa; font-size: 18px; letter-spacing: 2px; }
    #heroList { display: flex; gap: 15px; flex-wrap: wrap; justify-content: center; max-width: 90vw; }
    .hero-card { padding: 12px 25px; background-color: #2a2a2a; border: 2px solid #444; border-radius: 8px; font-size: 16px; cursor: pointer; transition: all 0.2s ease; user-select: none; }
    .hero-card:hover { background-color: #3a3a3a; }
    .hero-card.selected { border-color: #00ffcc; background-color: rgba(0, 255, 204, 0.1); color: #00ffcc; box-shadow: 0 0 15px rgba(0, 255, 204, 0.4); }
    #gameCanvas { background-color: #2a2a2a; box-shadow: 0 0 20px rgba(0,0,0,0.5); max-width: 100vw; max-height: 100vh; }
    #gameOverScreen { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.85); display: flex; flex-direction: column; justify-content: center; align-items: center; gap: 30px; z-index: 10; backdrop-filter: blur(4px); }
    #gameOverText { font-size: 36px; font-weight: bold; text-shadow: 0 0 20px rgba(0, 255, 204, 0.5); }
    #restartBtn { padding: 15px 40px; font-size: 20px; background-color: #00ffcc; color: #111; border: none; border-radius: 8px; cursor: pointer; font-weight: bold; transition: transform 0.2s, background 0.3s; }
    #restartBtn:hover { background-color: #00ccaa; transform: scale(1.05); }
</style>
</head>
<body>

<div id="menu"><button id="startBtn">开始游戏</button></div>
<div id="selectScreen" class="hidden"><h2>选择两名英雄进行对决</h2><div id="heroList"></div></div>
<canvas id="gameCanvas" class="hidden" width="800" height="600"></canvas>
<div id="gameOverScreen" class="hidden">
    <div id="gameOverText">胜利!</div>
    <button id="restartBtn">重新选择英雄</button>
</div>

<script>
(function () {
'use strict';

// ==========================================
// ⚙️ 常量
// ==========================================
const CONFIG = { GRID_SIZE: 40, GRID_COLS: 24, GRID_ROWS: 18, PADDING: 60 };
const ARENA_WIDTH  = CONFIG.GRID_COLS * CONFIG.GRID_SIZE;
const ARENA_HEIGHT = CONFIG.GRID_ROWS * CONFIG.GRID_SIZE;
const MAP_WIDTH    = ARENA_WIDTH  + CONFIG.PADDING * 2;
const MAP_HEIGHT   = ARENA_HEIGHT + CONFIG.PADDING * 2;
const ARENA_X = CONFIG.PADDING;
const ARENA_Y = CONFIG.PADDING;
const MAX_DT = 0.05;

// ==========================================
// 📖 英雄图鉴 (全部角色已录入)
// ==========================================
const HERO_LIST = [
    { id: 'unknown',   name: '未知数',     color: '#00ffcc', maxHp: 1000, speed: 130 },
    { id: 'clover',    name: 'Clover',     color: '#00ffff', maxHp: 1000, speed: 130 },
    { id: 'sun_god',   name: '日神',       color: '#ffd700', maxHp: 900,  speed: 130 },
    { id: 'yansien',   name: '延施恩',     color: '#ffaa00', maxHp: 2000, speed: 150 },
    { id: 'hanshou',   name: '寒守',       color: '#ff4444', maxHp: 1000, speed: 250 },
    { id: 'werewolf',  name: '狼人',       color: '#aa00aa', maxHp: 1200, speed: 150 },
    { id: 'knight',    name: '骑士',       color: '#cccccc', maxHp: 1100, speed: 110 },
    { id: 'holly',     name: '冬青叶',     color: '#2e8b57', maxHp: 1500, speed: 150 },
    { id: 'newton',    name: '牛顿',       color: '#dddddd', maxHp: 1100, speed: 120 },
    { id: 'pastor',    name: '老牧师',     color: '#800080', maxHp: 1000, speed: 110 },
    { id: 'principal', name: '贪污的校长', color: '#b8860b', maxHp: 1200, speed: 110 },
    { id: 'deepseek',  name: 'Deepseek',   color: '#4488ff', maxHp: 900,  speed: 150 },
    { id: 'pla',       name: 'PLA',        color: '#4a5d23', maxHp: 1000, speed: 120 },
    { id: 'tangjiaqi', name: '唐佳琪',     color: '#ffffff', maxHp: 1000, speed: 140 },
    { id: 'dummy',     name: '测试假人',   color: '#888888', maxHp: 500,  speed: 100 }
];
const HERO_MAP = {};
HERO_LIST.forEach(h => { HERO_MAP[h.id] = h; });

// ==========================================
// 🎮 全局状态
// ==========================================
let gameState = 'menu';
let selectedHeroes = [];
let heroEntities  = [];
let projectiles   = [];
let damageTexts   = [];
let slashEffects  = [];
let impactEffects = [];
let muzzleFlashes = [];
let bleedEffects  = [];
let newtonPrisms  = [];
let apples        = [];
let civilians     = [];
let grenades      = [];
let civilianTimer = 10000;
let lastTime = 0;
let rafId = 0;
const camera = { x: 0, y: 0 };

// DOM 引用
const menu           = document.getElementById('menu');
const startBtn       = document.getElementById('startBtn');
const selectScreen   = document.getElementById('selectScreen');
const heroList       = document.getElementById('heroList');
const canvas         = document.getElementById('gameCanvas');
const gameOverScreen = document.getElementById('gameOverScreen');
const gameOverText   = document.getElementById('gameOverText');
const restartBtn     = document.getElementById('restartBtn');
const ctx            = canvas.getContext('2d');

// ==========================================
// 🎬 事件绑定与流程控制
// ==========================================
startBtn.addEventListener('click', () => {
    if (gameState !== 'menu') return;
    menu.classList.add('hidden');
    selectScreen.classList.remove('hidden');
    gameState = 'select';
    renderHeroSelection();
});

restartBtn.addEventListener('click', () => {
    cancelAnimationFrame(rafId);
    gameOverScreen.classList.add('hidden');
    canvas.classList.add('hidden');
    selectScreen.classList.remove('hidden');
    gameState = 'select';
    resetBattleState();
    renderHeroSelection();
});

function renderHeroSelection() {
    heroList.innerHTML = '';
    selectedHeroes = [];
    HERO_LIST.forEach(hero => {
        const card = document.createElement('div');
        card.className = 'hero-card';
        card.textContent = hero.name;
        card.dataset.id = hero.id;
        card.addEventListener('click', () => toggleHeroSelection(hero.id, card));
        heroList.appendChild(card);
    });
}

function toggleHeroSelection(id, card) {
    if (gameState !== 'select') return;
    if (selectedHeroes.includes(id)) {
        selectedHeroes = selectedHeroes.filter(x => x !== id);
        card.classList.remove('selected');
        return;
    }
    if (selectedHeroes.length >= 2) return;
    selectedHeroes.push(id);
    card.classList.add('selected');
    if (selectedHeroes.length === 2) {
        gameState = 'starting';
        setTimeout(() => {
            if (gameState === 'starting') startBattle();
        }, 400);
    }
}

function resetEffects() {
    projectiles = [];
    damageTexts = [];
    slashEffects = [];
    impactEffects = [];
    muzzleFlashes = [];
    bleedEffects = [];
    newtonPrisms = [];
    apples = [];
    civilians = [];
    grenades = [];
    civilianTimer = 10000;
}
function resetBattleState() {
    heroEntities = [];
    resetEffects();
}

function startBattle() {
    gameState = 'playing';
    selectScreen.classList.add('hidden');
    canvas.classList.remove('hidden');
    camera.x = MAP_WIDTH / 2 - canvas.width / 2;
    camera.y = MAP_HEIGHT / 2 - canvas.height / 2;
    heroEntities = selectedHeroes.map((id, index) => createHero(id, index));
    resetEffects();
    lastTime = performance.now();
    cancelAnimationFrame(rafId);
    rafId = requestAnimationFrame(gameLoop);
}

// ==========================================
// 🦸 英雄工厂 (初始化所有特殊属性)
// ==========================================
function createHero(id, index) {
    const db = HERO_MAP[id];
    if (!db) throw new Error('未知英雄: ' + id);

    const startX = index === 0 ? ARENA_X + 150 : ARENA_X + ARENA_WIDTH - 150;
    const startY = ARENA_Y + ARENA_HEIGHT / 2;

    const hero = {
        id: db.id, name: db.name, color: db.color,
        x: startX, y: startY, radius: 15,
        hp: db.maxHp, maxHp: db.maxHp, speed: db.speed,
        attackCooldown: 1000, attackRangeGrid: 1.5,
        attackTimer: 0, attackAnimTimer: 0,
        facingRight: index === 0,
        dr: 0, stunTimer: 0, pinnedBy: null, wallTimer: 0, bleedTickTimer: 0,
        wanderTimer: Math.random() * 2, wanderAngle: Math.random() * Math.PI * 2,
        wanderX: 0, wanderY: 0
    };

    if (id === 'unknown') {
        hero.weaponState = 'pistol'; hero.shotsFired = 0; hero.shotgunShots = 0;
        hero.attackCooldown = 500; hero.attackRangeGrid = 10;
    }
    if (id === 'clover') {
        hero.clipAmmo = 6; hero.maxClipAmmo = 6; hero.isReloading = false; hero.reloadTimer = 0;
        hero.attackCooldown = 200; hero.attackRangeGrid = 8; hero.bulletDamage = 15;
        hero.rage = 0; hero.rageThreshold = 80; hero.laserTimer = 0; hero.isLaserCharging = false;
    }
    if (id === 'sun_god') {
        hero.attackCooldown = 6000; hero.attackRangeGrid = 6; hero.attackDamage = 67;
        hero.debuffDuration = 20000; // 20秒
        hero.confuseChance = 0.1; // 10%
    }
    if (id === 'yansien') {
        hero.attackCooldown = 4500; hero.attackRangeGrid = 4;
    }
    if (id === 'hanshou') {
        hero.hanshouState = 0; hero.pinnedTarget = null; hero.exhaustTimer = 0;
        hero.chargeTimer = 0; hero.attackCooldown = 1500; hero.attackRangeGrid = 1.125; hero.hasSword = true;
    }
    if (id === 'werewolf') {
        hero.battleTimer = 0; hero.enraged = false; hero.dashCooldown = 0;
        hero.isDashing = false; hero.dashTimer = 0; hero.dashDirX = 0; hero.dashDirY = 0;
        hero.attackCooldown = 1000; hero.attackRangeGrid = 1.5;
    }
    if (id === 'knight') {
        hero.attackDamage = 34; hero.attackCooldown = 1200; hero.attackRangeGrid = 1.5;
        hero.shieldHp = 0; hero.maxShieldHp = 150;
        hero.chargeCooldown = 0; hero.isCharging = false; hero.chargeTimer = 0;
        hero.chargeDirX = 0; hero.chargeDirY = 0;
    }
    if (id === 'holly') {
        hero.attackDamage = 45; hero.attackCooldown = 1200; hero.attackRangeGrid = 1.5;
        hero.digCooldown = 0; hero.digState = 'none'; // none, entering, hiding, attacking, returning
        hero.digTimer = 0; hero.digTarget = null; hero.digEntrance = null; hero.digExit = null;
    }
    if (id === 'newton') {
        hero.prismCooldown = 5000; hero.appleTimer = 30000;
        hero.attackCooldown = 99999; hero.attackRangeGrid = 999;
        hero.satellites = []; hero.satelliteTimer = 5000;
    }
    if (id === 'pastor') {
        hero.attackDamage = 15; hero.attackCooldown = 1200; hero.attackRangeGrid = 1.5;
        hero.rageTimer = 0; hero.rageMax = 15000; // 15秒
        hero.isRaging = false; hero.holyRingTimer = 0; hero.holyRingActive = false;
    }
    if (id === 'principal') {
        hero.attackCooldown = 4000; hero.attackRangeGrid = 6;
        hero.shieldHp = 0; hero.maxShieldHp = 300;
        hero.checkTimer = 0; hero.parents = []; hero.escapeUsed = false;
    }
    if (id === 'deepseek') {
        hero.shieldHp = 1500; hero.maxShieldHp = 1500; hero.shieldCooldown = 0; hero.attackCooldown = 1000;
    }
    if (id === 'pla') {
        hero.clipAmmo = 30; hero.maxClipAmmo = 30; hero.isReloading = false; hero.reloadTimer = 0;
        hero.attackCooldown = 120; hero.attackRangeGrid = 8; hero.baseDamage = 10;
        hero.damageMult = 1; hero.damageBuffAdd = 0; hero.damageBuffTimer = 0;
        hero.reactionCooldown = 0; hero.grenadeChance = 0.1;
    }
    if (id === 'tangjiaqi') {
        hero.attackCooldown = 99999; hero.attackRangeGrid = 999;
        hero.pearls = 10; hero.pearlTimer = 0;
        hero.phase = 'survive'; // survive, god_mode
        hero.timer = 0; hero.godModeTimer = 0; hero.totalLaserDamage = 0;
        hero.isFlashing = false;
    }
    return hero;
}

// ==========================================
// 🎮 主循环与逻辑更新 (核心框架)
// ==========================================
function gameLoop(timestamp) {
    if (gameState !== 'playing') return;
    const dt = Math.min((timestamp - lastTime) / 1000, MAX_DT);
    lastTime = timestamp;
    update(dt);
    if (gameState === 'playing') {
        updateCamera(dt);
        render();
        rafId = requestAnimationFrame(gameLoop);
    }
}

function updateCamera(dt) {
    if (heroEntities.length < 2) return;
    const a = heroEntities[0], b = heroEntities[1];
    let cx = (a.x + b.x) / 2, cy = (a.y + b.y) / 2;
    if (!isFinite(cx)) cx = MAP_WIDTH / 2;
    if (!isFinite(cy)) cy = MAP_HEIGHT / 2;
    const targetX = Math.max(0, Math.min(MAP_WIDTH  - canvas.width,  cx - canvas.width  / 2));
    const targetY = Math.max(0, Math.min(MAP_HEIGHT - canvas.height, cy - canvas.height / 2));
    const k = Math.min(1, 8 * dt);
    camera.x += (targetX - camera.x) * k;
    camera.y += (targetY - camera.y) * k;
}

function update(dt) {
    const alive = heroEntities.filter(h => h.hp > 0);
    if (alive.length <= 1) { gameState = 'gameover'; showGameOver(alive); return; }

    // 全局计时器更新 (省略重复代码,后续模块专属处理)
    for (const h of heroEntities) {
        if (h.attackTimer > 0) h.attackTimer -= dt * 1000;
        if (h.attackAnimTimer > 0) h.attackAnimTimer -= dt * 1000;
        if (h.stunTimer > 0) h.stunTimer -= dt * 1000;
        if (h.exhaustTimer > 0) h.exhaustTimer -= dt * 1000;
        if (h.shieldCooldown > 0) h.shieldCooldown -= dt * 1000;
        if (h.dashCooldown > 0) h.dashCooldown -= dt * 1000;
        if (h.prismCooldown > 0) h.prismCooldown -= dt * 1000;
        if (h.appleTimer > 0) h.appleTimer -= dt * 1000;
        if (h.satelliteTimer > 0) h.satelliteTimer -= dt * 1000;
        if (h.chargeCooldown > 0) h.chargeCooldown -= dt * 1000;
        if (h.digCooldown > 0) h.digCooldown -= dt * 1000;
        if (h.id === 'werewolf' && !h.enraged) h.battleTimer += dt;
        if (h.id === 'pla' && h.damageBuffTimer > 0) h.damageBuffTimer -= dt * 1000;
        if (h.id === 'pla' && h.reactionCooldown > 0) h.reactionCooldown -= dt * 1000;
        if (h.id === 'pastor' && !h.holyRingActive) h.rageTimer += dt * 1000;
        if (h.id === 'tangjiaqi' && h.phase === 'survive') h.timer += dt * 1000;
    }
    
    // 全局AI逻辑 (后续模块具体写)
    // 这里只留框架,避免代码太长被截断
    // 实际运行中,每个英雄的专属AI会在第二部分给出
    
    resolveCollisions();
    updateProjectiles(dt);
    updateEffects(dt);
}

// 🌟 物理碰撞与边界
function resolveCollisions() {
    for (let i = 0; i < heroEntities.length; i++) {
        for (let j = i + 1; j < heroEntities.length; j++) {
            const h1 = heroEntities[i], h2 = heroEntities[j];
            if (h1.hp <= 0 || h2.hp <= 0) continue;
            if (h1.id === 'hanshou' && (h1.hanshouState === 1 || h1.hanshouState === 2)) continue;
            if (h2.id === 'hanshou' && (h2.hanshouState === 1 || h2.hanshouState === 2)) continue;
            let dx = h2.x - h1.x, dy = h2.y - h1.y;
            let dist = Math.hypot(dx, dy);
            const minDist = h1.radius + h2.radius;
            if (dist < 0.01) { dx = 0.01; dy = 0; dist = 0.01; }
            if (dist < minDist) {
                const overlap = minDist - dist;
                const nx = dx / dist, ny = dy / dist;
                h1.x -= nx * overlap * 0.5; h1.y -= ny * overlap * 0.5;
                h2.x += nx * overlap * 0.5; h2.y += ny * overlap * 0.5;
            }
        }
    }
    for (const h of heroEntities) constrainToArena(h);
}

function constrainToArena(h) {
    if (!isFinite(h.x)) h.x = ARENA_X + ARENA_WIDTH / 2;
    if (!isFinite(h.y)) h.y = ARENA_Y + ARENA_HEIGHT / 2;
    if (h.x < ARENA_X + h.radius) { h.x = ARENA_X + h.radius; h.wanderAngle += Math.PI; }
    if (h.x > ARENA_X + ARENA_WIDTH - h.radius) { h.x = ARENA_X + ARENA_WIDTH - h.radius; h.wanderAngle += Math.PI; }
    if (h.y < ARENA_Y + h.radius) { h.y = ARENA_Y + h.radius; h.wanderAngle += Math.PI; }
    if (h.y > ARENA_Y + ARENA_HEIGHT - h.radius) { h.y = ARENA_Y + ARENA_HEIGHT - h.radius; h.wanderAngle += Math.PI; }
}

// 🌟 伤害结算 (含护盾、减伤、闪避等)
function applyDamage(target, amount, attacker, projectile, isTrueDamage = false, silent = false) {
    if (!target || target.hp <= 0) return { absorbed: false, killed: false };
    if (!isFinite(amount)) amount = 0;
    amount = Math.max(0, amount);

    if (!isTrueDamage) {
        // Deepseek 护盾
        if (target.id === 'deepseek' && target.shieldHp <= 0 && target.shieldCooldown <= 0) {
            target.shieldHp = target.maxShieldHp;
            if (!silent) addDamageText(target.x, target.y - 40, '护盾恢复', '#4488ff');
        }
        if (target.id === 'deepseek' && target.shieldHp > 0) {
            target.shieldHp -= amount;
            if (target.shieldHp <= 0) { target.shieldHp = 0; target.shieldCooldown = 60000; if (!silent) addDamageText(target.x, target.y - 40, '护盾破碎', '#4488ff'); }
            if (!silent) addDamageText(target.x, target.y, Math.round(amount), '#4488ff');
            if (attacker && attacker.hp > 0 && attacker.id !== 'deepseek' && projectile) {
                const reflectDmg = Math.round(amount * 0.5);
                if (reflectDmg > 0) {
                    spawnProjectile({ x: target.x, y: target.y, startX: target.x, startY: target.y, target: attacker, speed: projectile.speed || 800, damage: reflectDmg, type: 'bullet', ownerId: target.id, color: '#4488ff' });
                }
            }
            return { absorbed: true, killed: false };
        }
        // 骑士护盾
        if (target.id === 'knight' && target.shieldHp > 0) {
            target.shieldHp -= amount;
            if (target.shieldHp < 0) {
                let overflow = Math.abs(target.shieldHp);
                target.shieldHp = 0; target.hp -= overflow;
                if (!silent) addDamageText(target.x, target.y, overflow, '#ff6666');
                if (!silent) addDamageText(target.x, target.y - 40, '壁垒破碎', '#ffcc00');
            } else {
                if (!silent) addDamageText(target.x, target.y, Math.round(amount), '#ffcc00');
            }
            return { absorbed: true, killed: false };
        }
        if (target.dr > 0) amount = amount * (1 - target.dr / 100);
    }

    amount = Math.round(amount);
    target.hp -= amount;
    if (!silent) addDamageText(target.x, target.y, amount, isTrueDamage ? '#ff00ff' : '#ff6666');

    if (target.id === 'hanshou' && target.exhaustTimer <= 0 && target.hanshouState === 0) target.dr = Math.max(0, target.dr - 5);

    if (target.id === 'yansien' && attacker && attacker.hp > 0 && attacker.id !== 'yansien') {
        const distGrid = Math.hypot(attacker.x - target.x, attacker.y - target.y) / CONFIG.GRID_SIZE;
        if (distGrid <= 2) {
            applyDamage(attacker, 200, target, null);
            target.hp -= 400;
            addDamageText(target.x, target.y - 20, '400', '#ffaa00');
            addImpact(target.x, target.y, '#ffaa00', 0.5);
            if (target.hp < 0) target.hp = 0;
        }
    }
    return { absorbed: false, killed: target.hp <= 0 };
}

// 占位函数,后续模块补全
function updateProjectiles(dt) {}
function updateEffects(dt) {}
function addDamageText(x, y, amount, color) {}
function addImpact(x, y, color, maxLife) {}
function spawnProjectile(opts) {}
function showGameOver(survivors) {}
function render() {}

// ==========================================
// 🏃 逻辑更新 (请覆盖第一部分的 update 函数)
// ==========================================
function update(dt) {
    const alive = heroEntities.filter(h => h.hp > 0);
    if (alive.length <= 1) { gameState = 'gameover'; showGameOver(alive); return; }

    // 1. 全局计时器更新
    for (const h of heroEntities) {
        if (h.attackTimer > 0) h.attackTimer -= dt * 1000;
        if (h.attackAnimTimer > 0) h.attackAnimTimer -= dt * 1000;
        if (h.stunTimer > 0) h.stunTimer -= dt * 1000;
        if (h.exhaustTimer > 0) h.exhaustTimer -= dt * 1000;
        if (h.shieldCooldown > 0) h.shieldCooldown -= dt * 1000;
        if (h.dashCooldown > 0) h.dashCooldown -= dt * 1000;
        if (h.prismCooldown > 0) h.prismCooldown -= dt * 1000;
        if (h.appleTimer > 0) h.appleTimer -= dt * 1000;
        if (h.satelliteTimer > 0) h.satelliteTimer -= dt * 1000;
        if (h.chargeCooldown > 0) h.chargeCooldown -= dt * 1000;
        if (h.digCooldown > 0) h.digCooldown -= dt * 1000;
        if (h.id === 'werewolf' && !h.enraged) h.battleTimer += dt;
        if (h.id === 'pla' && h.damageBuffTimer > 0) h.damageBuffTimer -= dt * 1000;
        if (h.id === 'pla' && h.reactionCooldown > 0) h.reactionCooldown -= dt * 1000;
        if (h.id === 'pastor' && !h.holyRingActive) h.rageTimer += dt * 1000;
        if (h.id === 'tangjiaqi' && h.phase === 'survive') h.timer += dt * 1000;
    }
    
    // 2. 全局随机游走
    for (const h of heroEntities) {
        if (h.wanderTimer > 0) h.wanderTimer -= dt;
        else { h.wanderTimer = 1.2 + Math.random() * 1.5; h.wanderAngle += (Math.random() - 0.5) * Math.PI * 1.8; }
        h.wanderX = Math.cos(h.wanderAngle) * h.speed * 0.5;
        h.wanderY = Math.sin(h.wanderAngle) * h.speed * 0.5;
    }

    // 3. 逐个英雄的AI调度
    for (const hero of heroEntities) {
        if (hero.hp <= 0) continue;
        if (hero.id === 'dummy') { updateDummy(hero, dt); continue; }
        if (hero.pinnedBy) continue;
        
        const isImmune = hero.id === 'hanshou' && (hero.hanshouState === 1 || hero.hanshouState === 2);
        if (hero.stunTimer > 0 && !isImmune) continue;

        if (hero.id === 'unknown')       updateUnknown(hero, dt);
        else if (hero.id === 'clover')    updateClover(hero, dt);
        else if (hero.id === 'sun_god')   updateSunGod(hero, dt);
        else if (hero.id === 'yansien')   updateYansien(hero, dt);
        else if (hero.id === 'hanshou')   updateHanshou(hero, dt);
        else if (hero.id === 'werewolf')  updateWerewolf(hero, dt);
        else if (hero.id === 'knight')    updateKnight(hero, dt);
        else if (hero.id === 'holly')     updateHolly(hero, dt);
        else if (hero.id === 'newton')    updateNewton(hero, dt);
        else if (hero.id === 'pastor')    updatePastor(hero, dt);
        else if (hero.id === 'principal') updatePrincipal(hero, dt);
        else if (hero.id === 'deepseek')  updateDeepseek(hero, dt);
        else if (hero.id === 'pla')       updatePla(hero, dt);
        else if (hero.id === 'tangjiaqi') updateTangjiaqi(hero, dt);
    }

    // 4. 钉墙流血与全局特殊状态
    for (const h of heroEntities) {
        if (h.pinnedBy === 'wall') {
            h.wallTimer -= dt * 1000; h.bleedTickTimer -= dt * 1000;
            if (h.bleedTickTimer <= 0) {
                h.bleedTickTimer = 500;
                applyDamage(h, 20, null, null);
                bleedEffects.push({ x: h.x, y: h.y, life: 0.5 });
            }
            if (h.wallTimer <= 0) { h.pinnedBy = null; h.x += (h.x < ARENA_X + ARENA_WIDTH/2) ? 30 : -30; constrainToArena(h); }
        }
    }

    resolveCollisions();
    updateProjectiles(dt);
    updateEffects(dt);
}

// ==========================================
// 🏃 英雄 AI 逻辑 (第二部分:远程射手与近战重装)
// ==========================================

function updateDummy(hero, dt) {
    if (hero.pinnedBy) return;
    hero.x += hero.wanderX * dt; hero.y += hero.wanderY * dt;
    constrainToArena(hero);
}

function updateUnknown(hero, dt) {
    const enemies = heroEntities.filter(h => h.id !== hero.id && h.hp > 0);
    if (!enemies.length) return;
    let target = enemies[0], best = Infinity;
    for (const e of enemies) { const d = Math.hypot(e.x - hero.x, e.y - hero.y); if (d < best) { best = d; target = e; } }
    const dx = target.x - hero.x, dy = target.y - hero.y;
    const distPx = Math.hypot(dx, dy) || 1; const distGrid = distPx / CONFIG.GRID_SIZE;
    const dirX = dx / distPx, dirY = dy / distPx; hero.facingRight = dirX > 0;

    let moveX = 0, moveY = 0;
    if (distGrid < 3) {
        moveX = -dirX * hero.speed; moveY = -dirY * hero.speed;
        if (hero.x <= ARENA_X + hero.radius + 60 || hero.x >= ARENA_X + ARENA_WIDTH - hero.radius - 60) { moveX = 0; moveY = (hero.y > ARENA_Y + ARENA_HEIGHT / 2 ? -1 : 1) * hero.speed; }
        else if (hero.y <= ARENA_Y + hero.radius + 60 || hero.y >= ARENA_Y + ARENA_HEIGHT - hero.radius - 60) { moveX = (hero.x > ARENA_X + ARENA_WIDTH / 2 ? -1 : 1) * hero.speed; moveY = 0; }
    } else if (distGrid > 6) {
        moveX = dirX * hero.speed * 0.6; moveY = dirY * hero.speed * 0.6;
    } else {
        const tx = -dirY, ty = dirX; const slide = Math.sin(performance.now() / 1500) > 0 ? 1 : -1;
        moveX = tx * hero.speed * 0.6 * slide; moveY = ty * hero.speed * 0.6 * slide;
    }
    hero.x += (moveX + hero.wanderX * 0.4) * dt; hero.y += (moveY + hero.wanderY * 0.4) * dt;
    constrainToArena(hero);

    if (hero.attackTimer > 0) return;
    if (hero.weaponState === 'pistol') {
        if (hero.shotsFired < 8) {
            hero.shotsFired++;
            const dmg = Math.max(0, Math.round(-0.5 * distGrid * distGrid + 50));
            spawnProjectile({ x: hero.x, y: hero.y, startX: hero.x, startY: hero.y, target, speed: 800, damage: dmg, type: 'bullet', ownerId: hero.id, color: hero.color });
            muzzleFlashes.push({ x: hero.x + dirX * 18, y: hero.y + dirY * 18, life: 0.1, maxLife: 0.1 });
            hero.attackTimer = 500;
        } else {
            hero.weaponState = 'shotgun'; hero.shotgunShots = 0;
            spawnProjectile({ x: hero.x, y: hero.y, startX: hero.x, startY: hero.y, target, speed: 700, damage: 67, type: 'thrownPistol', ownerId: hero.id });
            hero.attackTimer = 800;
        }
    } else if (hero.weaponState === 'shotgun') {
        if (hero.shotgunShots < 2) {
            hero.shotgunShots++;
            const baseAngle = Math.atan2(dy, dx);
            for (let i = 0; i < 6; i++) {
                const angle = baseAngle + (i - 2.5) * 0.15;
                spawnProjectile({ x: hero.x, y: hero.y, startX: hero.x, startY: hero.y, vx: Math.cos(angle) * 600, vy: Math.sin(angle) * 600, damage: 25, type: 'shotgunPellet', ownerId: hero.id, lifetime: 0.6, color: '#ffaa00' });
            }
            muzzleFlashes.push({ x: hero.x + dirX * 18, y: hero.y + dirY * 18, life: 0.15, maxLife: 0.15 });
            hero.attackTimer = 1000;
            if (hero.shotgunShots >= 2) { hero.weaponState = 'pistol'; hero.shotsFired = 0; hero.shotgunShots = 0; }
        }
    }
}

function updateClover(hero, dt) {
    const enemies = heroEntities.filter(h => h.id !== hero.id && h.hp > 0);
    if (!enemies.length) return;
    let target = enemies[0]; let best = Infinity;
    for (const e of enemies) { const d = Math.hypot(e.x - hero.x, e.y - hero.y); if (d < best) { best = d; target = e; } }
    const dx = target.x - hero.x, dy = target.y - hero.y;
    const distPx = Math.hypot(dx, dy) || 1; const distGrid = distPx / CONFIG.GRID_SIZE;
    const dirX = dx / distPx, dirY = dy / distPx; hero.facingRight = dirX > 0;

    let moveX = 0, moveY = 0;
    if (distGrid < 5) { moveX = -dirX * hero.speed; moveY = -dirY * hero.speed; }
    else if (distGrid > 7) { moveX = dirX * hero.speed * 0.6; moveY = dirY * hero.speed * 0.6; }
    else { const tx = -dirY, ty = dirX; const slide = Math.sin(performance.now() / 1500) > 0 ? 1 : -1; moveX = tx * hero.speed * 0.6 * slide; moveY = ty * hero.speed * 0.6 * slide; }
    hero.x += (moveX + hero.wanderX * 0.4) * dt; hero.y += (moveY + hero.wanderY * 0.4) * dt;
    constrainToArena(hero);

    if (hero.isLaserCharging) {
        hero.laserTimer -= dt * 1000;
        if (hero.laserTimer <= 0) {
            // 发射激光(真实伤害)
            applyDamage(target, 680, hero, null, true);
            impactEffects.push({ x: hero.x, y: hero.y, color: '#ffffff', life: 0.5, maxLife: 0.5 });
            hero.isLaserCharging = false;
            hero.rage = 0; // 清空怒气
            hero.clipAmmo = hero.maxClipAmmo; // 重新上膛
            hero.isReloading = false;
        }
        return;
    }

    if (hero.isReloading) {
        hero.reloadTimer -= dt * 1000;
        if (hero.reloadTimer <= 0) { hero.isReloading = false; hero.clipAmmo = hero.maxClipAmmo; }
        return;
    }

    if (hero.attackTimer <= 0 && distGrid <= hero.attackRangeGrid) {
        if (hero.clipAmmo > 0) {
            hero.clipAmmo--;
            hero.attackTimer = hero.attackCooldown;
            // 判定是否命中:这里用简单的概率,或者根据目标是否在移动来决定
            let isHit = Math.random() > 0.2; 
            if (isHit) {
                applyDamage(target, hero.bulletDamage, hero, null);
                addDamageText(target.x, target.y, hero.bulletDamage, '#00ffff');
            } else {
                hero.rage += 10;
                addDamageText(hero.x, hero.y - 20, 'Miss +10怒气', '#ff00ff');
            }
            
            if (hero.rage >= hero.rageThreshold) {
                hero.isLaserCharging = true;
                hero.laserTimer = 800; // 0.8秒前摇
                addDamageText(hero.x, hero.y - 40, '激光蓄力!', '#ffffff');
            }

            if (hero.clipAmmo <= 0) {
                hero.isReloading = true;
                hero.reloadTimer = 1500;
                addDamageText(hero.x, hero.y - 30, '换弹中...', '#aaaaaa');
            }
        }
    }
}

function updateSunGod(hero, dt) {
    const enemies = heroEntities.filter(h => h.id !== hero.id && h.hp > 0);
    if (!enemies.length) return;
    let target = enemies[0]; let best = Infinity;
    for (const e of enemies) { const d = Math.hypot(e.x - hero.x, e.y - hero.y); if (d < best) { best = d; target = e; } }
    const dx = target.x - hero.x, dy = target.y - hero.y;
    const distPx = Math.hypot(dx, dy) || 1; const distGrid = distPx / CONFIG.GRID_SIZE;
    const dirX = dx / distPx, dirY = dy / distPx; hero.facingRight = dirX > 0;

    let moveX = 0, moveY = 0;
    if (distGrid < 4) { moveX = -dirX * hero.speed; moveY = -dirY * hero.speed; }
    else if (distGrid > 6) { moveX = dirX * hero.speed * 0.6; moveY = dirY * hero.speed * 0.6; }
    else { const tx = -dirY, ty = dirX; const slide = Math.sin(performance.now() / 1500) > 0 ? 1 : -1; moveX = tx * hero.speed * 0.6 * slide; moveY = ty * hero.speed * 0.6 * slide; }
    hero.x += (moveX + hero.wanderX * 0.4) * dt; hero.y += (moveY + hero.wanderY * 0.4) * dt;
    constrainToArena(hero);

    if (hero.attackTimer <= 0 && distGrid <= 6) {
        hero.attackTimer = hero.attackCooldown;
        // 发射嘴炮弹幕
        spawnProjectile({
            x: hero.x, y: hero.y, startX: hero.x, startY: hero.y, target: target,
            speed: 500, damage: 11, type: 'text', ownerId: hero.id,
            text: Math.random() < 0.5 ? '这样倒不像是明确的选择。' : '哦,你那样做可真的是愚蠢透了。'
        });
        
        // 近战普攻判定
        if (distGrid <= 1.5) {
            applyDamage(target, 67, hero, null);
            addDamageText(target.x, target.y - 30, '67', '#ffaa00');
            
            // 10%概率挂困惑
            if (Math.random() < 0.1) {
                target.confusedTimer = 30000;
                addDamageText(target.x, target.y - 50, '困惑!', '#ff00ff');
            }
        }
    }
}

function updateYansien(hero, dt) {
    const enemies = heroEntities.filter(h => h.id !== hero.id && h.hp > 0);
    if (!enemies.length) return;
    let target = enemies[0]; let best = Infinity;
    for (const e of enemies) { const d = Math.hypot(e.x - hero.x, e.y - hero.y); if (d < best) { best = d; target = e; } }
    const dx = target.x - hero.x, dy = target.y - hero.y;
    const distPx = Math.hypot(dx, dy) || 1; const distGrid = distPx / CONFIG.GRID_SIZE;
    const dirX = dx / distPx, dirY = dy / distPx; hero.facingRight = dirX > 0;

    let moveX = 0, moveY = 0;
    if (distGrid < 4) { moveX = -dirX * hero.speed * 0.7; moveY = -dirY * hero.speed * 0.7; }
    else if (distGrid > 4.5) { moveX = dirX * hero.speed * 0.5; moveY = dirY * hero.speed * 0.5; }
    hero.x += (moveX + hero.wanderX * 0.4) * dt; hero.y += (moveY + hero.wanderY * 0.4) * dt;
    constrainToArena(hero);

    if (hero.attackTimer <= 0 && distGrid <= 4.5) {
        hero.attackTimer = hero.attackCooldown;
        const isFireExt = Math.random() < 0.1;
        spawnProjectile({ x: hero.x, y: hero.y, startX: hero.x, startY: hero.y, target, speed: 600, damage: isFireExt ? 240 : 28, type: isFireExt ? 'fireext' : 'pen', ownerId: hero.id, knockback: isFireExt ? 5 : 0 });
    }
}

function updateHanshou(hero, dt) {
    if (hero.exhaustTimer > 0) { hero.x += hero.wanderX * 0.3 * dt; hero.y += hero.wanderY * 0.3 * dt; constrainToArena(hero); return; }
    const enemies = heroEntities.filter(h => h.id !== hero.id && h.hp > 0);
    if (!enemies.length) return;
    const target = enemies[0];
    const dx = target.x - hero.x, dy = target.y - hero.y;
    const distPx = Math.hypot(dx, dy) || 1; const distGrid = distPx / CONFIG.GRID_SIZE;
    const dirX = dx / distPx, dirY = dy / distPx; hero.facingRight = dirX > 0;

    if (hero.hanshouState === 0) {
        if (distGrid > hero.attackRangeGrid) {
            hero.x += (dirX * hero.speed + hero.wanderX * 0.5) * dt; hero.y += (dirY * hero.speed + hero.wanderY * 0.5) * dt; constrainToArena(hero);
        } else {
            let tangentDir = (Math.sin(performance.now() / 800 + hero.wanderTimer) > 0) ? 1 : -1;
            hero.x += (-dirY * tangentDir * hero.speed * 0.6 + hero.wanderX * 0.5) * dt; hero.y += ( dirX * tangentDir * hero.speed * 0.6 + hero.wanderY * 0.5) * dt; constrainToArena(hero);
            if (hero.attackTimer <= 0 && hero.hasSword) {
                hero.attackTimer = hero.attackCooldown; hero.attackAnimTimer = 300;
                applyDamage(target, 58, hero, null);
                slashEffects.push({ x: hero.x, y: hero.y, angle: Math.atan2(dy, dx), life: 0.3, maxLife: 0.3, color: '#ff4444' });
                hero.dr = Math.min(76, hero.dr + 10);
                if (hero.dr >= 76) { hero.dr = 0; hero.hanshouState = 1; hero.chargeTimer = 2.0; addDamageText(hero.x, hero.y - 30, '泥头车!', '#ff00ff'); }
            }
        }
        return;
    }
    if (hero.hanshouState === 1) {
        hero.chargeTimer -= dt; hero.x += dirX * 200 * dt; hero.y += dirY * 200 * dt; constrainToArena(hero);
        const nd = Math.hypot(target.x - hero.x, target.y - hero.y);
        if (nd <= hero.radius + target.radius + 10) {
            applyDamage(target, 220, hero, null); hero.hanshouState = 2; hero.pinnedTarget = target; hero.chargeTimer = 4.0; addImpact(hero.x, hero.y, '#ff0000', 0.5);
        } else if (hero.chargeTimer <= 0) { hero.hanshouState = 0; hero.exhaustTimer = 1500; hero.dr = 0; }
        return;
    }
    if (hero.hanshouState === 2) {
        hero.chargeTimer -= dt; const pt = hero.pinnedTarget;
        if (!pt || pt.hp <= 0) { hero.hanshouState = 0; hero.exhaustTimer = 2000; hero.pinnedTarget = null; return; }
        const dL = hero.x - ARENA_X, dR = ARENA_X + ARENA_WIDTH - hero.x, dT = hero.y - ARENA_Y, dB = ARENA_Y + ARENA_HEIGHT - hero.y;
        const m = Math.min(dL, dR, dT, dB); let px = 0, py = 0;
        if (m === dL) px = -1; else if (m === dR) px = 1; else if (m === dT) py = -1; else py = 1;
        hero.x += px * 400 * dt; hero.y += py * 400 * dt; constrainToArena(hero);
        pt.x = hero.x + px * (hero.radius + pt.radius + 2); pt.y = hero.y + py * (hero.radius + pt.radius + 2); constrainToArena(pt);
        const targetHitWall = pt.x <= ARENA_X + pt.radius + 1 || pt.x >= ARENA_X + ARENA_WIDTH - pt.radius - 1 || pt.y <= ARENA_Y + pt.radius + 1 || pt.y >= ARENA_Y + ARENA_HEIGHT - pt.radius - 1;
        if (targetHitWall || hero.chargeTimer <= 0) {
            pt.pinnedBy = 'wall'; pt.wallTimer = 4000; pt.bleedTickTimer = 0; pt.hasSwordInBack = true;
            hero.hanshouState = 0; hero.exhaustTimer = 6000; hero.pinnedTarget = null; hero.dr = 0; hero.hasSword = false; addImpact(hero.x, hero.y, '#ff0000', 0.6);
        }
    }
}

function updateWerewolf(hero, dt) {
    const enemies = heroEntities.filter(h => h.id !== hero.id && h.hp > 0);
    if (!enemies.length) return;
    const target = enemies[0];
    const dx = target.x - hero.x, dy = target.y - hero.y;
    const distPx = Math.hypot(dx, dy) || 1; const distGrid = distPx / CONFIG.GRID_SIZE;
    const dirX = dx / distPx, dirY = dy / distPx; hero.facingRight = dirX > 0;

    if (!hero.enraged && hero.battleTimer >= 20) { hero.enraged = true; hero.speed *= 1.5; addDamageText(hero.x, hero.y - 30, '狂怒!', '#ff00ff'); addImpact(hero.x, hero.y, '#ff00ff', 0.5); }

    if (hero.isDashing) {
        hero.dashTimer -= dt; hero.x += hero.dashDirX * 200 * dt; hero.y += hero.dashDirY * 200 * dt; constrainToArena(hero);
        const nd = Math.hypot(target.x - hero.x, target.y - hero.y);
        if (nd <= hero.radius + target.radius + 10) { applyDamage(target, 100, hero, null); slashEffects.push({ x: hero.x, y: hero.y, angle: Math.atan2(target.y - hero.y, target.x - hero.x), life: 0.3, maxLife: 0.3, color: '#aa00aa' }); addImpact(hero.x, hero.y, '#aa00aa', 0.4); hero.isDashing = false; hero.dashCooldown = 5000; }
        else if (hero.dashTimer <= 0) { hero.isDashing = false; hero.dashCooldown = 5000; }
        return;
    }
    if (hero.dashCooldown <= 0 && distGrid > 1.5 && distGrid < 2.5) { hero.isDashing = true; hero.dashTimer = 0.5; hero.dashDirX = dirX; hero.dashDirY = dirY; addDamageText(hero.x, hero.y - 30, '突进!', '#ff4444'); return; }

    if (distGrid <= 1.5) {
        let tangentDir = (Math.sin(performance.now() / 800 + hero.wanderTimer) > 0) ? 1 : -1;
        hero.x += (-dirY * tangentDir * hero.speed * 0.6 + hero.wanderX * 0.5) * dt; hero.y += ( dirX * tangentDir * hero.speed * 0.6 + hero.wanderY * 0.5) * dt; constrainToArena(hero);
        if (hero.attackTimer <= 0) {
            hero.attackTimer = hero.attackCooldown; hero.attackAnimTimer = 300;
            let dmg = hero.enraged ? 45 : 30;
            if (target.hp > 0 && target.hp / target.maxHp < 0.1) { dmg = target.hp; addDamageText(target.x, target.y - 30, '终结!', '#ff0000'); addImpact(target.x, target.y, '#ff0000', 0.8); }
            applyDamage(target, dmg, hero, null);
            slashEffects.push({ x: hero.x, y: hero.y, angle: Math.atan2(dy, dx), life: 0.3, maxLife: 0.3, color: '#ff4444' });
        }
    } else { hero.x += (dirX * hero.speed + hero.wanderX * 0.5) * dt; hero.y += (dirY * hero.speed + hero.wanderY * 0.5) * dt; constrainToArena(hero); }
}

// ==========================================
// 占位符 (第三、四部分会覆盖这些)
// ==========================================

function updateKnight(hero, dt) {
    const enemies = heroEntities.filter(h => h.id !== hero.id && h.hp > 0);
    if (!enemies.length) return;
    const target = enemies[0];
    const dx = target.x - hero.x, dy = target.y - hero.y;
    const distPx = Math.hypot(dx, dy) || 1; const distGrid = distPx / CONFIG.GRID_SIZE;
    const dirX = dx / distPx, dirY = dy / distPx; hero.facingRight = dirX > 0;

    if (hero.isCharging) {
        hero.chargeTimer -= dt;
        hero.x += hero.chargeDirX * 300 * dt; hero.y += hero.chargeDirY * 300 * dt;
        constrainToArena(hero);
        if (Math.random() < 0.5) impactEffects.push({ x: hero.x, y: hero.y, color: '#ffcc00', life: 0.2, maxLife: 0.2 });
        const nd = Math.hypot(target.x - hero.x, target.y - hero.y);
        if (nd <= hero.radius + target.radius + 10) {
            applyDamage(target, 50, hero, null);
            addDamageText(target.x, target.y - 30, '破阵!', '#ffcc00');
            addImpact(hero.x, hero.y, '#ffcc00', 0.5);
            let knockDirX = (target.x - hero.x) / nd; let knockDirY = (target.y - hero.y) / nd;
            target.x += knockDirX * 1.5 * CONFIG.GRID_SIZE; target.y += knockDirY * 1.5 * CONFIG.GRID_SIZE;
            constrainToArena(target);
            hero.isCharging = false; hero.chargeCooldown = 8000;
        } else if (hero.chargeTimer <= 0) { hero.isCharging = false; hero.chargeCooldown = 8000; }
        return;
    }
    if (hero.chargeCooldown <= 0 && distGrid > 2 && distGrid < 3) {
        hero.isCharging = true; hero.chargeTimer = 0.6;
        hero.chargeDirX = dirX; hero.chargeDirY = dirY;
        addDamageText(hero.x, hero.y - 30, '冲锋!', '#ffcc00');
        return;
    }
    if (distGrid <= 1.5) {
        let tangentDir = (Math.sin(performance.now() / 800 + hero.wanderTimer) > 0) ? 1 : -1;
        hero.x += (-dirY * tangentDir * hero.speed * 0.6 + hero.wanderX * 0.5) * dt; hero.y += ( dirX * tangentDir * hero.speed * 0.6 + hero.wanderY * 0.5) * dt;
        constrainToArena(hero);
        if (hero.attackTimer <= 0) {
            hero.attackTimer = hero.attackCooldown; hero.attackAnimTimer = 300;
            applyDamage(target, hero.attackDamage, hero, null);
            hero.shieldHp = Math.min(hero.maxShieldHp, hero.shieldHp + 15);
            slashEffects.push({ x: hero.x, y: hero.y, angle: Math.atan2(dy, dx), life: 0.3, maxLife: 0.3, color: '#ffffff' });
        }
    } else {
        hero.x += (dirX * hero.speed + hero.wanderX * 0.5) * dt; hero.y += (dirY * hero.speed + hero.wanderY * 0.5) * dt;
        constrainToArena(hero);
    }
}

function updateHolly(hero, dt) {
    const enemies = heroEntities.filter(h => h.id !== hero.id && h.hp > 0);
    if (!enemies.length) return;
    const target = enemies[0];
    const dx = target.x - hero.x, dy = target.y - hero.y;
    const distPx = Math.hypot(dx, dy) || 1; const distGrid = distPx / CONFIG.GRID_SIZE;
    const dirX = dx / distPx, dirY = dy / distPx; hero.facingRight = dirX > 0;

    if (hero.digState === 'none') {
        // 普通近战
        if (distGrid > 1.5) { hero.x += dirX * hero.speed * dt; hero.y += dirY * hero.speed * dt; constrainToArena(hero); }
        else {
            let tangentDir = (Math.sin(performance.now() / 800 + hero.wanderTimer) > 0) ? 1 : -1;
            hero.x += (-dirY * tangentDir * hero.speed * 0.6 + hero.wanderX * 0.5) * dt; hero.y += ( dirX * tangentDir * hero.speed * 0.6 + hero.wanderY * 0.5) * dt;
            constrainToArena(hero);
            if (hero.attackTimer <= 0) {
                hero.attackTimer = hero.attackCooldown; hero.attackAnimTimer = 300;
                applyDamage(target, hero.attackDamage, hero, null);
                slashEffects.push({ x: hero.x, y: hero.y, angle: Math.atan2(dy, dx), life: 0.3, maxLife: 0.3, color: '#2e8b57' });
            }
        }
        // 冷却完毕,生成入口
        if (hero.digCooldown <= 0) {
            hero.digCooldown = 8000;
            hero.digState = 'entering';
            hero.digTimer = 0.3;
            hero.digEntrance = { x: hero.x + (Math.random() - 0.5) * 2 * CONFIG.GRID_SIZE, y: hero.y + (Math.random() - 0.5) * 2 * CONFIG.GRID_SIZE };
            addDamageText(hero.x, hero.y - 40, '地道入口!', '#2e8b57');
        }
        return;
    }

    if (hero.digState === 'entering') {
        hero.digTimer -= dt;
        if (hero.digTimer <= 0) {
            hero.digState = 'hiding'; hero.digTimer = 0.5;
            // 出口刷在敌人旁边
            let angle = Math.random() * Math.PI * 2;
            hero.digExit = { x: target.x + Math.cos(angle) * 2.5 * CONFIG.GRID_SIZE, y: target.y + Math.sin(angle) * 2.5 * CONFIG.GRID_SIZE };
            constrainToArena(hero.digExit);
        }
        return;
    }

    if (hero.digState === 'hiding') {
        hero.digTimer -= dt;
        if (hero.digTimer <= 0) {
            hero.digState = 'attacking'; hero.digTimer = 0.2;
            hero.x = hero.digExit.x; hero.y = hero.digExit.y;
            hero.facingRight = target.x > hero.x;
            // 三连击
            for (let i = 0; i < 3; i++) {
                setTimeout(() => {
                    if (hero.hp > 0 && target.hp > 0) {
                        let d = Math.hypot(target.x - hero.x, target.y - hero.y);
                        if (d <= 2 * CONFIG.GRID_SIZE) {
                            applyDamage(target, 130, hero, null);
                            addDamageText(target.x, target.y - 20, '130', '#ff3333');
                        }
                    }
                }, i * 100);
            }
            addImpact(hero.x, hero.y, '#2e8b57', 0.5);
        }
        return;
    }

    if (hero.digState === 'attacking') {
        hero.digTimer -= dt;
        if (hero.digTimer <= 0) {
            hero.digState = 'returning'; hero.digTimer = 0.3;
            hero.x = hero.digEntrance.x; hero.y = hero.digEntrance.y;
            hero.digExit = null; hero.digEntrance = null;
        }
        return;
    }

    if (hero.digState === 'returning') {
        hero.digTimer -= dt;
        if (hero.digTimer <= 0) { hero.digState = 'none'; }
    }
}

function updateNewton(hero, dt) {
    const enemies = heroEntities.filter(h => h.id !== hero.id && h.hp > 0);
    if (!enemies.length) return;
    const target = enemies[0];
    const dx = target.x - hero.x, dy = target.y - hero.y;
    const distPx = Math.hypot(dx, dy) || 1; const distGrid = distPx / CONFIG.GRID_SIZE;
    const dirX = dx / distPx, dirY = dy / distPx; hero.facingRight = dirX > 0;

    let moveX = 0, moveY = 0;
    if (distGrid < 3) { moveX = -dirX * hero.speed; moveY = -dirY * hero.speed; }
    else if (distGrid > 6) { moveX = dirX * hero.speed * 0.5; moveY = dirY * hero.speed * 0.5; }
    else { let tangentDir = (Math.sin(performance.now() / 2000) > 0) ? 1 : -1; moveX = -dirY * tangentDir * hero.speed * 0.8; moveY = dirX * tangentDir * hero.speed * 0.8; }
    hero.x += (moveX + hero.wanderX * 0.4) * dt; hero.y += (moveY + hero.wanderY * 0.4) * dt;
    constrainToArena(hero);

    if (hero.prismCooldown <= 0) {
        hero.prismCooldown = 5000;
        let px = hero.x + (target.x - hero.x) * 0.5; let py = hero.y + (target.y - hero.y) * 0.5;
        let baseAngle = Math.atan2(target.y - py, target.x - px);
        newtonPrisms.push({ x: px, y: py, timer: 10000, baseAngle: baseAngle, angle: 0, sweepDir: 1, hitTimer: 0 });
        addDamageText(hero.x, hero.y - 40, '🔺 三棱镜!', '#00ffff');
    }

    if (hero.satellites) {
        hero.satelliteTimer -= dt * 1000;
        if (hero.satelliteTimer <= 0) {
            hero.satelliteTimer = 5000;
            if (Math.random() < 0.3 && hero.satellites.length < 5) { hero.satellites.push({ angle: Math.random() * Math.PI * 2 }); addDamageText(hero.x, hero.y - 50, '🛰️ 卫星!', '#00ffcc'); }
        }
        const orbitRadius = 2 * CONFIG.GRID_SIZE; const orbitSpeed = 2.5; 
        for (let i = hero.satellites.length - 1; i >= 0; i--) {
            let sat = hero.satellites[i]; sat.angle += orbitSpeed * dt;
            let satX = hero.x + Math.cos(sat.angle) * orbitRadius; let satY = hero.y + Math.sin(sat.angle) * orbitRadius;
            let hit = false;
            for (let h of heroEntities) {
                if (h.id === 'newton' || h.hp <= 0) continue;
                if (Math.hypot(h.x - satX, h.y - satY) < h.radius + 10) {
                    applyDamage(h, 40, hero, null); h.stunTimer = 1000;
                    addDamageText(h.x, h.y - 20, '眩晕!', '#ffff00');
                    impactEffects.push({ x: satX, y: satY, color: '#00ffff', life: 0.5, maxLife: 0.5 });
                    hit = true; break;
                }
            }
            if (hit) { hero.satellites.splice(i, 1); }
        }
    }
}

function updatePastor(hero, dt) {
    const enemies = heroEntities.filter(h => h.id !== hero.id && h.hp > 0);
    if (!enemies.length) return;
    const target = enemies[0];
    const dx = target.x - hero.x, dy = target.y - hero.y;
    const distPx = Math.hypot(dx, dy) || 1; const distGrid = distPx / CONFIG.GRID_SIZE;
    const dirX = dx / distPx, dirY = dy / distPx; hero.facingRight = dirX > 0;

    if (hero.isRaging) {
        hero.holyRingTimer -= dt * 1000;
        if (hero.holyRingTimer <= 0) { hero.isRaging = false; hero.holyRingActive = false; hero.rageTimer = 0; }
    } else {
        hero.rageTimer += dt * 1000;
        if (hero.rageTimer >= hero.rageMax) { hero.isRaging = true; hero.holyRingActive = true; hero.holyRingTimer = 8000; hero.rageTimer = hero.rageMax; addDamageText(hero.x, hero.y - 40, '圣光领域!', '#ffd700'); }
    }

    if (hero.holyRingActive) {
        // 光环伤害,每0.5秒跳一次
        hero.holyTickTimer = (hero.holyTickTimer || 0) - dt * 1000;
        if (hero.holyTickTimer <= 0) {
            hero.holyTickTimer = 500;
            for (let h of heroEntities) {
                if (h.id !== 'pastor' && h.hp > 0 && Math.hypot(h.x - hero.x, h.y - hero.y) <= 5 * CONFIG.GRID_SIZE) {
                    applyDamage(h, 23, hero, null, true); // 46/s, so 23 per 0.5s
                    impactEffects.push({ x: h.x, y: h.y, color: '#ffd700', life: 0.3, maxLife: 0.3 });
                }
            }
        }
    }

    if (distGrid > 1.5) { hero.x += dirX * hero.speed * dt; hero.y += dirY * hero.speed * dt; constrainToArena(hero); }
    else {
        if (hero.attackTimer <= 0) {
            hero.attackTimer = hero.attackCooldown; hero.attackAnimTimer = 300;
            applyDamage(target, hero.attackDamage, hero, null);
            slashEffects.push({ x: hero.x, y: hero.y, angle: Math.atan2(dy, dx), life: 0.3, maxLife: 0.3, color: '#ffd700' });
        }
    }
}

function updatePrincipal(hero, dt) {
    const enemies = heroEntities.filter(h => h.id !== hero.id && h.hp > 0);
    if (!enemies.length) return;
    const target = enemies[0];
    const dx = target.x - hero.x, dy = target.y - hero.y;
    const distPx = Math.hypot(dx, dy) || 1; const distGrid = distPx / CONFIG.GRID_SIZE;
    const dirX = dx / distPx, dirY = dy / distPx; hero.facingRight = dirX > 0;

    // 被动:挪用公款(当场上存在其他单位时,获得护盾)
    if (heroEntities.length > 1 || civilians.length > 0) {
        hero.shieldHp = Math.min(hero.maxShieldHp, hero.shieldHp + 5 * dt);
    }

    // 保命:卷款跑路
    if (!hero.escapeUsed && hero.hp / hero.maxHp < 0.3) {
        hero.escapeUsed = true;
        let randomAngle = Math.random() * Math.PI * 2;
        hero.x += Math.cos(randomAngle) * 15 * CONFIG.GRID_SIZE;
        hero.y += Math.sin(randomAngle) * 15 * CONFIG.GRID_SIZE;
        constrainToArena(hero);
        addDamageText(hero.x, hero.y - 50, '卷款跑路!', '#ffd700');
        hero.shieldHp = 500; // 临时护盾,代表草人
    }

    // 家长会
    hero.checkTimer = (hero.checkTimer || 0) - dt * 1000;
    if (hero.checkTimer <= 0) {
        hero.checkTimer = 8000;
        for (let i = 0; i < 2; i++) {
            let px = hero.x + (Math.random() - 0.5) * 4 * CONFIG.GRID_SIZE;
            let py = hero.y + (Math.random() - 0.5) * 4 * CONFIG.GRID_SIZE;
            civilians.push({ x: px, y: py, radius: 10, hp: 100, maxHp: 100, wanderTimer: 0, wanderAngle: Math.random() * Math.PI * 2, wanderX: 0, wanderY: 0, isFleeing: false, fleeTimer: 0, isParent: true });
        }
        addDamageText(hero.x, hero.y - 30, '家长会!', '#b8860b');
    }

    if (distGrid < 4) { hero.x -= dirX * hero.speed * dt; hero.y -= dirY * hero.speed * dt; constrainToArena(hero); }
    else if (distGrid > 6) { hero.x += dirX * hero.speed * 0.5 * dt; hero.y += dirY * hero.speed * 0.5 * dt; constrainToArena(hero); }
    
    if (hero.attackTimer <= 0 && distGrid <= 6) {
        hero.attackTimer = hero.attackCooldown;
        spawnProjectile({ x: hero.x, y: hero.y, startX: hero.x, startY: hero.y, target, speed: 500, damage: 20, type: 'text', ownerId: hero.id, text: '欠费通知', knockback: 0 });
    }
}

function updateDeepseek(hero, dt) {
    const enemies = heroEntities.filter(h => h.id !== hero.id && h.hp > 0);
    if (!enemies.length) return;
    const target = enemies[0];
    const dx = target.x - hero.x, dy = target.y - hero.y;
    const distPx = Math.hypot(dx, dy) || 1; const distGrid = distPx / CONFIG.GRID_SIZE;
    const dirX = dx / distPx, dirY = dy / distPx; hero.facingRight = dirX > 0;

    const hasShield = hero.shieldHp > 0;
    let moveX = 0, moveY = 0;
    if (hasShield) { if (distGrid < 12) { moveX = -dirX * hero.speed; moveY = -dirY * hero.speed; } }
    else { if (distGrid < 7) { moveX = -dirX * hero.speed * 0.6; moveY = -dirY * hero.speed * 0.6; } }
    if (hero.x <= ARENA_X + hero.radius + 40 || hero.x >= ARENA_X + ARENA_WIDTH - hero.radius - 40) { moveX = 0; moveY = (hero.y > ARENA_Y + ARENA_HEIGHT / 2 ? -1 : 1) * hero.speed * 0.8; }
    else if (hero.y <= ARENA_Y + hero.radius + 40 || hero.y >= ARENA_Y + ARENA_HEIGHT - hero.radius - 40) { moveX = (hero.x > ARENA_X + ARENA_WIDTH / 2 ? -1 : 1) * hero.speed * 0.8; moveY = 0; }
    hero.x += (moveX + hero.wanderX * 0.4) * dt; hero.y += (moveY + hero.wanderY * 0.4) * dt;
    constrainToArena(hero);

    if (!hasShield && hero.attackTimer <= 0) {
        hero.attackTimer = hero.attackCooldown;
        spawnProjectile({ x: hero.x, y: hero.y, startX: hero.x, startY: hero.y, target, speed: 400, damage: 13, type: 'text', ownerId: hero.id, knockback: 5, text: '对不起,这个问题我还无法回答。' });
    }
} function updatePla(hero, dt) {
    const enemies = heroEntities.filter(h => h.id !== hero.id && h.hp > 0 && h.id !== 'dummy');
    if (!enemies.length) return;
    let target = enemies[0], best = Infinity;
    for (const e of enemies) { const d = Math.hypot(e.x - hero.x, e.y - hero.y); if (d < best) { best = d; target = e; } }
    const dx = target.x - hero.x, dy = target.y - hero.y;
    const distPx = Math.hypot(dx, dy) || 1; const distGrid = distPx / CONFIG.GRID_SIZE;
    const dirX = dx / distPx, dirY = dy / distPx; hero.facingRight = dirX > 0;

    // 受击反应检测(通过血量变化触发)
    if (hero.hp < hero.lastHp && hero.reactionCooldown <= 0 && hero.hp > 0) {
        hero.reactionCooldown = 2000;
        let rand = Math.random();
        if (rand < 0.3 && target) {
            let ndx = target.x - hero.x, ndy = target.y - hero.y; let nd = Math.hypot(ndx, ndy) || 1;
            target.x += (ndx / nd) * 3 * CONFIG.GRID_SIZE; target.y += (ndy / nd) * 3 * CONFIG.GRID_SIZE;
            constrainToArena(target);
            addDamageText(hero.x, hero.y - 40, '撞击反击!', '#4a5d23');
            impactEffects.push({ x: hero.x, y: hero.y, color: '#4a5d23', life: 0.5, maxLife: 0.5 });
        } else if (rand < 0.6) {
            let rx = hero.x - target.x, ry = hero.y - target.y; let rd = Math.hypot(rx, ry) || 1;
            hero.x += (rx / rd) * 10 * CONFIG.GRID_SIZE; hero.y += (ry / rd) * 10 * CONFIG.GRID_SIZE;
            constrainToArena(hero);
            addDamageText(hero.x, hero.y - 40, '战术后撤!', '#4a5d23');
            impactEffects.push({ x: hero.x, y: hero.y, color: '#4a5d23', life: 0.5, maxLife: 0.5 });
        }
    }
    hero.lastHp = hero.hp;

    let moveX = 0, moveY = 0;
    if (distGrid < 4) { moveX = -dirX * hero.speed; moveY = -dirY * hero.speed; }
    else if (distGrid > 7) { moveX = dirX * hero.speed * 0.6; moveY = dirY * hero.speed * 0.6; }
    else { let tangentDir = (Math.sin(performance.now() / 1500) > 0) ? 1 : -1; moveX = -dirY * tangentDir * hero.speed * 0.6; moveY = dirX * tangentDir * hero.speed * 0.6; }
    hero.x += (moveX + hero.wanderX * 0.4) * dt; hero.y += (moveY + hero.wanderY * 0.4) * dt;
    constrainToArena(hero);

    if (hero.isReloading) {
        hero.reloadTimer -= dt * 1000;
        if (hero.reloadTimer <= 0) { hero.isReloading = false; hero.clipAmmo = hero.maxClipAmmo; addDamageText(hero.x, hero.y - 30, '换弹完成!', '#4a5d23'); }
        return;
    }

    if (hero.attackTimer <= 0 && distGrid <= hero.attackRangeGrid) {
        if (hero.clipAmmo > 0) {
            hero.clipAmmo--;
            hero.attackTimer = hero.attackCooldown;
            let finalDmg = Math.round((hero.baseDamage + hero.damageBuffAdd) * hero.damageMult);
            spawnProjectile({ x: hero.x, y: hero.y, startX: hero.x, startY: hero.y, target, speed: 1000, damage: finalDmg, type: 'bullet', ownerId: hero.id, color: '#ffff00' });
            muzzleFlashes.push({ x: hero.x + dirX * 18, y: hero.y + dirY * 18, life: 0.05, maxLife: 0.05 });
            
            if (Math.random() < hero.grenadeChance) {
                let gx = hero.x + dirX * 20, gy = hero.y + dirY * 20;
                grenades.push({ x: gx, y: gy, vx: dirX * 300, vy: dirY * 300, timer: 0.8 });
                addDamageText(hero.x, hero.y - 30, '手榴弹!', '#ff8800');
            }
            if (hero.clipAmmo <= 0) { hero.isReloading = true; hero.reloadTimer = 500; addDamageText(hero.x, hero.y - 30, '换弹中...', '#aaaaaa'); }
        }
    }
}

function updateTangjiaqi(hero, dt) {
    const enemies = heroEntities.filter(h => h.id !== hero.id && h.hp > 0);
    if (!enemies.length) return;
    const target = enemies[0];

    // 珍珠闪避检测
    if (hero.hp < hero.lastHp && hero.pearls > 0 && hero.hp > 0) {
        hero.pearls--;
        let angle = Math.random() * Math.PI * 2;
        hero.x += Math.cos(angle) * 4 * CONFIG.GRID_SIZE;
        hero.y += Math.sin(angle) * 4 * CONFIG.GRID_SIZE;
        constrainToArena(hero);
        addDamageText(hero.x, hero.y - 40, '末影珍珠!', '#ffffff');
        impactEffects.push({ x: hero.x, y: hero.y, color: '#ffffff', life: 0.3, maxLife: 0.3 });
    }
    hero.lastHp = hero.hp;

    if (hero.phase === 'survive') {
        // 前30秒:只能逃跑
        if (hero.timer >= 30000) {
            hero.phase = 'god_mode';
            hero.timer = 0;
            hero.godModeTimer = 10000; // 10秒神之领域
            addDamageText(hero.x, hero.y - 50, '光芒万丈!', '#ffffff');
        }
        let moveX = 0, moveY = 0;
        let distToTarget = Math.hypot(target.x - hero.x, target.y - hero.y);
        if (distToTarget < 10 * CONFIG.GRID_SIZE) {
            let dx = hero.x - target.x, dy = hero.y - target.y; let nd = Math.hypot(dx, dy) || 1;
            moveX = (dx / nd) * hero.speed; moveY = (dy / nd) * hero.speed;
        } else {
            moveX = Math.cos(hero.wanderAngle) * hero.speed * 0.5; moveY = Math.sin(hero.wanderAngle) * hero.speed * 0.5;
        }
        hero.x += (moveX + hero.wanderX * 0.4) * dt; hero.y += (moveY + hero.wanderY * 0.4) * dt;
        constrainToArena(hero);
    } else if (hero.phase === 'god_mode') {
        // 最后10秒:开灯闪人
        hero.godModeTimer -= dt * 1000;
        if (hero.godModeTimer <= 0) { hero.phase = 'defeated'; }
        if (Math.random() < 0.5) {
            impactEffects.push({ x: hero.x, y: hero.y, color: '#ffffff', life: 0.1, maxLife: 0.1 });
        }
        for (let h of heroEntities) {
            if (h.id !== hero.id && h.hp > 0) {
                let dist = Math.hypot(h.x - hero.x, h.y - hero.y);
                if (dist < 5 * CONFIG.GRID_SIZE) {
                    h.stunTimer = 100; // 持续控制
                    h.x += (h.x - hero.x) * 0.05; // 减速效果体现
                    h.y += (h.y - hero.y) * 0.05;
                    h.speed = 0.1; // 速度降至极低
                    applyDamage(h, 95, hero, null); // 每秒95伤害(总950)
                }
            }
        }
    } else {
        // 技能结束后虚弱
        hero.x += hero.wanderX * 0.2 * dt; hero.y += hero.wanderY * 0.2 * dt;
        constrainToArena(hero);
    }
}

// ==========================================
// 🚀 全局子弹与特效系统 (完整版)
// ==========================================
function spawnProjectile(opts) {
    projectiles.push({ x: 0, y: 0, startX: 0, startY: 0, target: null, speed: 600, damage: 0, type: 'bullet', ownerId: null, vx: undefined, vy: undefined, lifetime: undefined, knockback: 0, color: null, text: null, ...opts });
}

function updateProjectiles(dt) {
    for (let i = projectiles.length - 1; i >= 0; i--) {
        const p = projectiles[i];
        if (p.target && p.target.hp <= 0) { projectiles.splice(i, 1); continue; }
        let hitTarget = null;
        if (p.vx !== undefined && p.vy !== undefined) {
            p.x += p.vx * dt; p.y += p.vy * dt; p.lifetime -= dt;
            if (p.lifetime <= 0) { projectiles.splice(i, 1); continue; }
            if (p.x < -50 || p.x > MAP_WIDTH + 50 || p.y < -50 || p.y > MAP_HEIGHT + 50) { projectiles.splice(i, 1); continue; }
            for (const h of heroEntities) { if (h.hp <= 0 || h.id === p.ownerId) continue; if (Math.hypot(h.x - p.x, h.y - p.y) < h.radius + 8) { hitTarget = h; break; } }
        } else if (p.target) {
            const t = p.target; const pdx = t.x - p.x, pdy = t.y - p.y; const pDist = Math.hypot(pdx, pdy) || 1;
            if (pDist < t.radius + 8) { hitTarget = t; } else { p.x += (pdx / pDist) * p.speed * dt; p.y += (pdy / pDist) * p.speed * dt; }
        }
        if (hitTarget) {
            const attacker = p.ownerId ? heroEntities.find(h => h.id === p.ownerId) || null : null;
            const result = applyDamage(hitTarget, p.damage, attacker, p);
            if (!result.absorbed) {
                if (p.knockback > 0) { const pdx = hitTarget.x - p.x, pdy = hitTarget.y - p.y; const pd = Math.hypot(pdx, pdy) || 1; hitTarget.x += (pdx / pd) * p.knockback * CONFIG.GRID_SIZE; hitTarget.y += (pdy / pd) * p.knockback * CONFIG.GRID_SIZE; constrainToArena(hitTarget); }
                if (p.type === 'pen') { hitTarget.stunTimer = 3000; addDamageText(hitTarget.x, hitTarget.y - 20, '定身!', '#ffff00'); }
                if (p.type === 'text' && p.text) { 
                    if (p.text.indexOf('欠费') >= 0) { hitTarget.oweTimer = 5000; }
                    if (p.text.indexOf('愚蠢') >= 0) { hitTarget.confusedTimer = 30000; }
                }
                if (p.type === 'thrownPistol' && attacker && attacker.id === 'unknown') { attacker.weaponState = 'shotgun'; attacker.shotgunShots = 0; attacker.shotsFired = 0; }
            }
            addImpact(p.x, p.y, p.type === 'fireext' ? '#ff5500' : (p.type === 'text' ? '#4488ff' : '#ffffff'), 0.2);
            projectiles.splice(i, 1);
        }
    }
    // 人民受击检测(只有非 PLA 的子弹能打中人民)
    for (let i = projectiles.length - 1; i >= 0; i--) {
        let p = projectiles[i];
        if (p.ownerId === 'pla') continue;
        for (let c of civilians) {
            if (c.hp <= 0) continue;
            if (Math.hypot(c.x - p.x, c.y - p.y) < c.radius + 8) {
                c.hp -= p.damage;
                c.isFleeing = true; c.fleeTimer = 3;
                let pla = heroEntities.find(h => h.id === 'pla' && h.hp > 0);
                if (pla) {
                    pla.damageBuffAdd += 12; pla.damageBuffTimer = 30000;
                    addDamageText(pla.x, pla.y - 50, '+12 伤害', '#ffcc00');
                    if (c.hp <= 0) {
                        pla.damageMult *= 2; addDamageText(pla.x, pla.y - 70, '人民阵亡! 伤害翻倍!', '#ff0000');
                        impactEffects.push({ x: c.x, y: c.y, color: '#ff0000', life: 0.8, maxLife: 0.8 });
                    }
                }
                projectiles.splice(i, 1); break;
            }
        }
    }
}

function updateEffects(dt) {
    for (let i = muzzleFlashes.length - 1; i >= 0; i--) { muzzleFlashes[i].life -= dt; if (muzzleFlashes[i].life <= 0) muzzleFlashes.splice(i, 1); }
    for (let i = slashEffects.length - 1; i >= 0; i--) { slashEffects[i].life -= dt; if (slashEffects[i].life <= 0) slashEffects.splice(i, 1); }
    for (let i = impactEffects.length - 1; i >= 0; i--) { impactEffects[i].life -= dt; if (impactEffects[i].life <= 0) impactEffects.splice(i, 1); }
    for (let i = damageTexts.length - 1; i >= 0; i--) { damageTexts[i].y -= 30 * dt; damageTexts[i].life -= dt; if (damageTexts[i].life <= 0) damageTexts.splice(i, 1); }
    for (let i = bleedEffects.length - 1; i >= 0; i--) { bleedEffects[i].life -= dt; if (bleedEffects[i].life <= 0) bleedEffects.splice(i, 1); }
    for (let i = apples.length - 1; i >= 0; i--) { /* apple updates handled in main loop */ }
}

function addDamageText(x, y, amount, color) { damageTexts.push({ x, y: y - 20, text: String(amount), color: color || '#fff', life: 1.0 }); }
function addImpact(x, y, color, maxLife) { maxLife = maxLife || 0.3; impactEffects.push({ x, y, color, life: maxLife, maxLife }); }

function showGameOver(survivors) {
    gameOverScreen.classList.remove('hidden'); canvas.classList.add('hidden');
    if (survivors.length === 1) { gameOverText.innerText = survivors[0].name + ' 胜利!'; gameOverText.style.color = survivors[0].color; }
    else { gameOverText.innerText = '同归于尽!'; gameOverText.style.color = '#fff'; }
}

// ==========================================
// 🎨 渲染系统 (完整版)
// ==========================================
function render() {
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    ctx.save(); ctx.translate(-camera.x, -camera.y);
    drawArena(); drawApples(); drawNewtonPrisms(); drawEffects(); drawGrenades(); drawProjectiles(); drawEntities(); drawCivilians(); drawDamageTexts();
    ctx.restore();
}

function drawArena() {
    ctx.fillStyle = '#2a2a2a'; ctx.fillRect(0, 0, MAP_WIDTH, MAP_HEIGHT);
    ctx.fillStyle = '#3a3a3a'; ctx.fillRect(ARENA_X, ARENA_Y, ARENA_WIDTH, ARENA_HEIGHT);
    ctx.strokeStyle = '#00ffcc'; ctx.lineWidth = 6; ctx.strokeRect(ARENA_X, ARENA_Y, ARENA_WIDTH, ARENA_HEIGHT);
    ctx.strokeStyle = 'rgba(255, 255, 255, 0.05)'; ctx.lineWidth = 1; ctx.beginPath();
    for (let i = 1; i < CONFIG.GRID_COLS; i++) { const x = ARENA_X + i * CONFIG.GRID_SIZE; ctx.moveTo(x, ARENA_Y); ctx.lineTo(x, ARENA_Y + ARENA_HEIGHT); }
    for (let i = 1; i < CONFIG.GRID_ROWS; i++) { const y = ARENA_Y + i * CONFIG.GRID_SIZE; ctx.moveTo(ARENA_X, y); ctx.lineTo(ARENA_X + ARENA_WIDTH, y); }
    ctx.stroke();
}

function drawApples() { for (let a of apples) { ctx.beginPath(); ctx.arc(a.x, a.y, a.radius, 0, Math.PI * 2); ctx.fillStyle = '#ff4444'; ctx.fill(); ctx.strokeStyle = '#ff0000'; ctx.lineWidth = 2; ctx.stroke(); ctx.fillStyle = '#00ff00'; ctx.beginPath(); ctx.ellipse(a.x, a.y - a.radius - 2, 4, 2, Math.PI / 4, 0, Math.PI * 2); ctx.fill(); } }

function drawNewtonPrisms() {
    for (let p of newtonPrisms) {
        ctx.save(); ctx.translate(p.x, p.y); ctx.rotate(p.baseAngle);
        ctx.beginPath(); ctx.moveTo(0, 0); ctx.arc(0, 0, 240, -Math.PI / 6 + p.angle, Math.PI / 6 + p.angle); ctx.closePath(); ctx.fillStyle = 'rgba(255, 255, 255, 0.08)'; ctx.fill();
        for (let i = 0; i < 7; i++) { let angle = -Math.PI / 6 + p.angle + (i / 6) * (Math.PI / 3); ctx.beginPath(); ctx.moveTo(0, 0); ctx.lineTo(Math.cos(angle) * 240, Math.sin(angle) * 240); ctx.strokeStyle = `hsl(${i * 51}, 100%, 50%)`; ctx.lineWidth = 2; ctx.stroke(); }
        ctx.beginPath(); ctx.moveTo(0, -12); ctx.lineTo(10, 8); ctx.lineTo(-10, 8); ctx.closePath(); ctx.fillStyle = '#ffffff'; ctx.fill(); ctx.strokeStyle = '#aaa'; ctx.lineWidth = 1; ctx.stroke(); ctx.restore();
    }
}

function drawGrenades() { for (let g of grenades) { ctx.beginPath(); ctx.arc(g.x, g.y, 8, 0, Math.PI * 2); ctx.fillStyle = '#333'; ctx.fill(); ctx.strokeStyle = '#ff8800'; ctx.lineWidth = 3; ctx.stroke(); ctx.beginPath(); ctx.arc(g.x, g.y - 10, 4, 0, Math.PI * 2); ctx.fillStyle = '#ffff00'; ctx.fill(); } }

function drawCivilians() {
    for (let c of civilians) {
        if (c.hp <= 0) continue;
        ctx.beginPath(); ctx.arc(c.x, c.y, c.radius, 0, Math.PI * 2); ctx.fillStyle = c.isParent ? '#b8860b' : '#ffffff'; ctx.fill(); ctx.strokeStyle = c.isParent ? '#ffd700' : '#aaaaaa'; ctx.lineWidth = 2; ctx.stroke();
        const barWidth = 30, barHeight = 4; const barX = c.x - barWidth / 2; const barY = c.y - c.radius - 10; const hpPercent = c.hp / c.maxHp;
        ctx.fillStyle = '#333'; ctx.fillRect(barX, barY, barWidth, barHeight); ctx.fillStyle = hpPercent > 0.3 ? '#00ff00' : '#ff0000'; ctx.fillRect(barX, barY, barWidth * hpPercent, barHeight);
        ctx.fillStyle = c.isParent ? '#ffd700' : '#000'; ctx.font = '10px sans-serif'; ctx.textAlign = 'center'; ctx.fillText(c.isParent ? '家长' : '人民', c.x, c.y + 3);
    }
}

function drawEffects() {
    muzzleFlashes.forEach(e => { const alpha = Math.max(0, Math.min(1, e.life / (e.maxLife || 0.1))); ctx.beginPath(); ctx.arc(e.x, e.y, 8, 0, Math.PI * 2); ctx.fillStyle = `rgba(255, 255, 255, ${alpha})`; ctx.fill(); });
    slashEffects.forEach(e => { ctx.save(); ctx.translate(e.x, e.y); ctx.rotate(e.angle); const alpha = Math.max(0, Math.min(1, e.life / (e.maxLife || 0.3))); ctx.beginPath(); ctx.arc(0, 0, 35, -Math.PI / 4, Math.PI / 4); ctx.strokeStyle = e.color ? `rgba(255, 255, 255, ${alpha})` : `rgba(255, 68, 68, ${alpha})`; if (e.color) ctx.strokeStyle = e.color.replace(')', `, ${alpha})`).replace('rgb', 'rgba'); ctx.lineWidth = 6; ctx.stroke(); ctx.restore(); });
    impactEffects.forEach(e => { const t = 1 - Math.max(0, Math.min(1, e.life / (e.maxLife || 0.3))); const r = Math.max(0.5, t * 20); ctx.beginPath(); ctx.arc(e.x, e.y, r, 0, Math.PI * 2); ctx.strokeStyle = e.color || '#fff'; ctx.lineWidth = 3; ctx.stroke(); });
    bleedEffects.forEach(e => { const alpha = Math.max(0, Math.min(1, e.life / 0.5)); ctx.beginPath(); ctx.arc(e.x, e.y, 6, 0, Math.PI * 2); ctx.fillStyle = `rgba(255, 0, 0, ${alpha})`; ctx.fill(); });
}

function drawProjectiles() {
    for (const p of projectiles) {
        if (p.type === 'text') {
            ctx.fillStyle = 'rgba(68, 136, 255, 0.2)'; ctx.fillRect(p.x - 100, p.y - 15, 200, 30); ctx.strokeStyle = '#4488ff'; ctx.lineWidth = 2; ctx.strokeRect(p.x - 100, p.y - 15, 200, 30); ctx.fillStyle = '#fff'; ctx.font = '12px sans-serif'; ctx.textAlign = 'center'; ctx.fillText(p.text || '', p.x, p.y + 5);
        } else if (p.type === 'pen') {
            ctx.save(); ctx.translate(p.x, p.y); if (p.target) ctx.rotate(Math.atan2(p.target.y - p.startY, p.target.x - p.startX)); ctx.fillStyle = '#ccc'; ctx.fillRect(-15, -2, 30, 4); ctx.beginPath(); ctx.moveTo(15, -4); ctx.lineTo(25, 0); ctx.lineTo(15, 4); ctx.fillStyle = '#333'; ctx.fill(); ctx.restore();
        } else if (p.type === 'fireext') {
            ctx.fillStyle = '#ff0000'; ctx.fillRect(p.x - 8, p.y - 12, 16, 24); ctx.fillStyle = '#fff'; ctx.font = '8px sans-serif'; ctx.textAlign = 'center'; ctx.fillText('灭火器', p.x, p.y + 4);
        } else if (p.type === 'thrownPistol') {
            ctx.save(); ctx.translate(p.x, p.y); if (p.target) ctx.rotate(Math.atan2(p.target.y - p.startY, p.target.x - p.startX)); ctx.fillStyle = '#888'; ctx.fillRect(-10, -4, 20, 8); ctx.restore();
        } else if (p.type === 'shotgunPellet') {
            ctx.beginPath(); ctx.arc(p.x, p.y, 4, 0, Math.PI * 2); ctx.fillStyle = '#ffaa00'; ctx.fill();
        } else if (p.color === '#ffff00') {
            ctx.save(); ctx.translate(p.x, p.y); if (p.target) ctx.rotate(Math.atan2(p.target.y - p.startY, p.target.x - p.startX)); ctx.fillStyle = '#ffff00'; ctx.fillRect(-10, -2, 20, 4); ctx.restore();
        } else {
            ctx.beginPath(); ctx.moveTo(p.startX, p.startY); ctx.lineTo(p.x, p.y); ctx.strokeStyle = p.color || 'rgba(0, 255, 204, 0.8)'; ctx.lineWidth = 5; ctx.stroke(); ctx.beginPath(); ctx.arc(p.x, p.y, 6, 0, Math.PI * 2); ctx.fillStyle = '#fff'; ctx.fill(); ctx.strokeStyle = p.color || '#00ffcc'; ctx.lineWidth = 2; ctx.stroke();
        }
    }
}

function drawEntities() {
    for (const entity of heroEntities) {
        ctx.globalAlpha = entity.hp > 0 ? 1.0 : 0.2;
        let scale = 1.0; if (entity.attackAnimTimer > 0) scale = 1.0 + (entity.attackAnimTimer / 300) * 0.3;
        let fillColor = entity.color;
        if (entity.id === 'hanshou') { if (entity.exhaustTimer > 0) fillColor = '#555555'; else if (entity.hanshouState === 1) fillColor = '#ff00ff'; else fillColor = '#ffff00'; }
        if (entity.stunTimer > 0) fillColor = '#ffff00';
        if (entity.id === 'werewolf' && entity.enraged) fillColor = '#cc0000';
        if (entity.id === 'knight' && entity.isCharging) fillColor = '#ffcc00';
        if (entity.id === 'pastor' && entity.isRaging) fillColor = '#ffd700';

        ctx.save(); ctx.translate(entity.x, entity.y); if (!entity.facingRight) ctx.scale(-1, 1); ctx.scale(scale, scale);

        // 绘制特殊光环
        if (entity.id === 'pastor' && entity.holyRingActive) {
            ctx.beginPath(); ctx.arc(0, 0, 5 * CONFIG.GRID_SIZE, 0, Math.PI * 2); ctx.strokeStyle = 'rgba(255, 215, 0, 0.6)'; ctx.lineWidth = 5; ctx.stroke();
            ctx.fillStyle = 'rgba(255, 215, 0, 0.15)'; ctx.fill();
        }
        if (entity.id === 'tangjiaqi' && entity.phase === 'god_mode') {
            ctx.save(); ctx.rotate(Math.atan2(heroEntities.find(h=>h.id!==entity.id&&h.hp>0).y - entity.y, heroEntities.find(h=>h.id!==entity.id&&h.hp>0).x - entity.x));
            ctx.beginPath(); ctx.moveTo(0, 0); ctx.arc(0, 0, 15 * CONFIG.GRID_SIZE, -Math.PI / 6, Math.PI / 6); ctx.closePath(); ctx.fillStyle = 'rgba(255, 255, 255, 0.85)'; ctx.fill();
            ctx.restore();
            fillColor = '#ffffff';
        }

        // 基础身体
        ctx.beginPath(); ctx.arc(0, 0, entity.radius, 0, Math.PI * 2); ctx.fillStyle = fillColor; ctx.fill(); ctx.strokeStyle = 'rgba(255,255,255,0.8)'; ctx.lineWidth = 2; ctx.stroke();

        // 各英雄专属外观
        if (entity.id === 'pla') { ctx.fillStyle = '#ffcc00'; ctx.font = 'bold 12px sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText('PLA', 0, 1); ctx.fillStyle = '#333'; ctx.fillRect(entity.radius - 2, -3, 25, 6); }
        if (entity.id === 'hanshou' && entity.hasSword) { ctx.fillStyle = entity.hanshouState === 1 ? '#ffcc00' : '#dddddd'; ctx.fillRect(entity.radius - 2, -3, 20, 6); ctx.beginPath(); ctx.moveTo(entity.radius + 18, -3); ctx.lineTo(entity.radius + 26, 0); ctx.lineTo(entity.radius + 18, 3); ctx.closePath(); ctx.fill(); }
        if (entity.id === 'unknown') { ctx.fillStyle = '#666'; if (entity.weaponState === 'pistol') { ctx.fillRect(entity.radius - 2, -5, 12, 10); } else if (entity.weaponState === 'shotgun') { ctx.fillRect(entity.radius - 2, -7, 22, 14); } }
        if (entity.id === 'knight') { ctx.fillStyle = '#aaa'; ctx.fillRect(entity.radius - 2, -12, 8, 24); ctx.fillStyle = '#ffcc00'; ctx.fillRect(entity.radius - 2, -4, 8, 8); }
        if (entity.id === 'werewolf') { ctx.beginPath(); ctx.moveTo(-8, -entity.radius); ctx.lineTo(-14, -entity.radius - 12); ctx.lineTo(-2, -entity.radius); ctx.fill(); ctx.beginPath(); ctx.moveTo(8, -entity.radius); ctx.lineTo(14, -entity.radius - 12); ctx.lineTo(2, -entity.radius); ctx.fill(); }
        if (entity.id === 'newton') { ctx.fillStyle = '#fff'; ctx.beginPath(); ctx.moveTo(0, -6); ctx.lineTo(6, 6); ctx.lineTo(-6, 6); ctx.closePath(); ctx.fill(); }
        if (entity.id === 'deepseek' && entity.shieldHp > 0) { ctx.beginPath(); ctx.arc(0, 0, entity.radius + 6, 0, Math.PI * 2); ctx.strokeStyle = '#4488ff'; ctx.lineWidth = 3; ctx.stroke(); }
        if (entity.id !== 'dummy' && entity.id !== 'pla') { ctx.beginPath(); ctx.arc(5, -4, 2.5, 0, Math.PI * 2); ctx.fillStyle = '#fff'; ctx.fill(); }
        ctx.restore();

        // 血条与状态条
        const barWidth = 50, barHeight = 5; const barX = entity.x - barWidth / 2; const barY = entity.y - entity.radius - 15; const hpPercent = Math.max(0, entity.hp) / entity.maxHp;
        ctx.fillStyle = '#333'; ctx.fillRect(barX, barY, barWidth, barHeight); ctx.fillStyle = hpPercent > 0.3 ? '#00ff00' : '#ff0000'; ctx.fillRect(barX, barY, barWidth * hpPercent, barHeight);
        if (entity.id === 'hanshou') { const drY = barY + barHeight + 2; ctx.fillStyle = '#333'; ctx.fillRect(barX, drY, barWidth, 3); ctx.fillStyle = '#ffcc00'; ctx.fillRect(barX, drY, barWidth * (entity.dr / 76), 3); }
        if (entity.id === 'deepseek' && entity.shieldHp > 0) { const shY = barY + barHeight + 2; ctx.fillStyle = '#333'; ctx.fillRect(barX, shY, barWidth, 3); ctx.fillStyle = '#4488ff'; ctx.fillRect(barX, shY, barWidth * (entity.shieldHp / entity.maxShieldHp), 3); }
        if (entity.id === 'werewolf' && !entity.enraged) { const tY = barY + barHeight + 2; ctx.fillStyle = '#333'; ctx.fillRect(barX, tY, barWidth, 3); ctx.fillStyle = '#ff00aa'; ctx.fillRect(barX, tY, barWidth * Math.min(1, entity.battleTimer / 20), 3); }
        if (entity.id === 'newton') { const tY = barY + barHeight + 2; ctx.fillStyle = '#333'; ctx.fillRect(barX, tY, barWidth, 3); ctx.fillStyle = '#00ffff'; ctx.fillRect(barX, tY, barWidth * (1 - entity.prismCooldown / 5000), 3); }
        if (entity.id === 'knight') { const shY = barY + barHeight + 2; ctx.fillStyle = '#333'; ctx.fillRect(barX, shY, barWidth, 3); ctx.fillStyle = '#ffcc00'; ctx.fillRect(barX, shY, barWidth * (entity.shieldHp / entity.maxShieldHp), 3); }
        if (entity.id === 'pla') { const tY = barY + barHeight + 2; ctx.fillStyle = '#333'; ctx.fillRect(barX, tY, barWidth, 3); ctx.fillStyle = '#ffff00'; ctx.fillRect(barX, tY, barWidth * (entity.clipAmmo / entity.maxClipAmmo), 3); if (entity.damageBuffTimer > 0) { ctx.fillStyle = '#ffcc00'; ctx.fillRect(barX, tY + 4, barWidth * Math.min(1, entity.damageBuffTimer / 30000), 3); } }
        if (entity.id === 'pastor') { const rY = barY + barHeight + 2; ctx.fillStyle = '#333'; ctx.fillRect(barX, rY, barWidth, 3); ctx.fillStyle = '#ffd700'; ctx.fillRect(barX, rY, barWidth * (entity.rageTimer / entity.rageMax), 3); }
        if (entity.id === 'tangjiaqi') { const tY = barY + barHeight + 2; ctx.fillStyle = '#333'; ctx.fillRect(barX, tY, barWidth, 3); ctx.fillStyle = '#ffffff'; ctx.fillRect(barX, tY, barWidth * (entity.phase === 'survive' ? entity.timer / 30000 : 1), 3); }

        ctx.fillStyle = '#fff'; ctx.font = '11px sans-serif'; ctx.textAlign = 'center'; ctx.fillText(entity.name, entity.x, barY - 5);

        if (entity.id === 'newton' && entity.satellites) {
            const orbitRadius = 2 * CONFIG.GRID_SIZE;
            for (let sat of entity.satellites) { let satX = entity.x + Math.cos(sat.angle) * orbitRadius; let satY = entity.y + Math.sin(sat.angle) * orbitRadius; ctx.beginPath(); ctx.arc(satX, satY, 8, 0, Math.PI * 2); ctx.fillStyle = '#00ffff'; ctx.fill(); ctx.strokeStyle = '#ffffff'; ctx.lineWidth = 2; ctx.stroke(); }
        }
        ctx.globalAlpha = 1.0;
    }
}

function drawDamageTexts() {
    for (const txt of damageTexts) { ctx.fillStyle = txt.color; ctx.font = 'bold 14px sans-serif'; ctx.textAlign = 'center'; ctx.globalAlpha = Math.max(0, Math.min(1, txt.life)); ctx.fillText(txt.text, txt.x, txt.y); }
    ctx.globalAlpha = 1.0;
}

})();
</script>
</body>
</html>

Game Source: 电子斗蛐蛐 - 终极完整版

Creator: EpicCoder88

Libraries: none

Complexity: complex (1418 lines, 77.5 KB)

The full source code is displayed above on this page.

Remix Instructions

To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: game-epiccoder88-muht8jqv" to link back to the original. Then publish at arcadelab.ai/publish.