🎮ArcadeLab

电子斗蛐蛐 - 瞬移物理修复版

by EpicCoder88
772 lines46.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, maximum-scale=1.0, user-scalable=no">
    <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: 20px; flex-wrap: wrap; justify-content: center; max-width: 90vw; }
        .hero-card { padding: 15px 30px; background-color: #2a2a2a; border: 2px solid #444; border-radius: 8px; font-size: 18px; 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; object-fit: contain; }
        
        #gameOverScreen { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.8); 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>
        // ==========================================
        // ⚙️ 全局配置
        // ==========================================
        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 HERO_DB = [
            { id: 'unknown', name: '未知数', color: '#00ffcc', maxHp: 1000 },
            { id: 'hanshou', name: '寒守', color: '#ff4444', maxHp: 1000 },
            { id: 'yansien', name: '延施恩', color: '#ffaa00', maxHp: 2000 },
            { id: 'deepseek', name: 'Deepseek', color: '#4488ff', maxHp: 900 },
            { id: 'werewolf', name: '狼人', color: '#aa00aa', maxHp: 1200 },
            { id: 'dummy', name: '测试假人', color: '#888888', maxHp: 500 }
        ];

        // ==========================================
        // 🎮 状态与变量
        // ==========================================
        const menu = document.getElementById('menu');
        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');

        let gameState = 'menu';
        let selectedHeroes = [];
        let heroEntities = []; 
        let projectiles = []; 
        let damageTexts = []; 
        let slashEffects = []; 
        let impactEffects = []; 
        let muzzleFlashes = [];
        let bleedEffects = [];
        let lastTime = 0;
        const camera = { x: 0, y: 0, targetX: 0, targetY: 0 };

        // ==========================================
        // 🖱️ 交互逻辑
        // ==========================================
        document.getElementById('startBtn').addEventListener('click', () => {
            menu.classList.add('hidden'); selectScreen.classList.remove('hidden');
            gameState = 'select'; renderHeroSelection();
        });

        restartBtn.addEventListener('click', () => {
            gameOverScreen.classList.add('hidden'); canvas.classList.add('hidden');
            selectScreen.classList.remove('hidden'); gameState = 'select';
            heroEntities = []; projectiles = []; damageTexts = []; slashEffects = []; impactEffects = [];
            renderHeroSelection();
        });

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

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

        // ==========================================
        // ⚔️ 开始战斗
        // ==========================================
        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) => {
                const dbHero = HERO_DB.find(h => h.id === id);
                const startX = index === 0 ? ARENA_X + 150 : ARENA_X + ARENA_WIDTH - 150;
                const startY = ARENA_Y + ARENA_HEIGHT / 2;

                return {
                    id: dbHero.id,
                    name: dbHero.name,
                    x: startX, y: startY,
                    radius: 15,
                    color: dbHero.color,
                    hp: dbHero.maxHp, maxHp: dbHero.maxHp,
                    speed: id === 'dummy' ? 100 : (id === 'unknown' ? 130 : (id === 'werewolf' ? 150 : 150)),
                    
                    attackRangeGrid: id === 'unknown' ? 10 : (id === 'hanshou' ? 1.125 : (id === 'yansien' ? 4 : (id === 'werewolf' ? 1.5 : 20))),
                    attackCooldown: id === 'unknown' ? 500 : (id === 'hanshou' ? 1500 : (id === 'yansien' ? 4500 : (id === 'deepseek' ? 1000 : (id === 'werewolf' ? 1000 : 99999)))),
                    attackTimer: 0,
                    attackAnimTimer: 0,
                    facingRight: index === 0,
                    
                    // 🌟 狼人专属状态
                    battleTimer: 0,
                    enraged: false,
                    dashCooldown: 0,

                    // 未知数专属武器状态
                    weaponState: 'pistol', shotsFired: 0, shotgunShots: 0,

                    // 随机游走
                    wanderTimer: Math.random() * 2, wanderAngle: Math.random() * Math.PI * 2, wanderX: 0, wanderY: 0,

                    // 寒守 / 延施恩 / Deepseek 状态
                    dr: 0, hanshouState: 0, pinnedTarget: null, exhaustTimer: 0,
                    stunTimer: 0,
                    shieldHp: id === 'deepseek' ? 1500 : 0, maxShieldHp: 1500, shieldCooldown: 0
                };
            });

            projectiles = []; damageTexts = []; slashEffects = []; impactEffects = []; muzzleFlashes = []; bleedEffects = [];
            lastTime = performance.now();
            requestAnimationFrame(gameLoop);
        }

        // ==========================================
        // 🔄 游戏主循环
        // ==========================================
        function gameLoop(timestamp) {
            if (gameState === 'gameover') return;
            const dt = (timestamp - lastTime) / 1000; lastTime = timestamp;
            update(dt); updateCamera(dt); render();
            requestAnimationFrame(gameLoop);
        }

        function updateCamera(dt) {
            if (heroEntities.length < 2) return;
            let centerX = (heroEntities[0].x + heroEntities[1].x) / 2;
            let centerY = (heroEntities[0].y + heroEntities[1].y) / 2;
            
            if (isNaN(centerX)) centerX = MAP_WIDTH / 2;
            if (isNaN(centerY)) centerY = MAP_HEIGHT / 2;

            camera.targetX = centerX - canvas.width / 2;
            camera.targetY = centerY - canvas.height / 2;
            camera.targetX = Math.max(0, Math.min(MAP_WIDTH - canvas.width, camera.targetX));
            camera.targetY = Math.max(0, Math.min(MAP_HEIGHT - canvas.height, camera.targetY));
            camera.x += (camera.targetX - camera.x) * 8 * dt;
            camera.y += (camera.targetY - camera.y) * 8 * dt;
        }

        // ==========================================
        // 🏃 逻辑更新
        // ==========================================
        function update(dt) {
            const aliveHeroes = heroEntities.filter(h => h.hp > 0);
            if (aliveHeroes.length <= 1) { gameState = 'gameover'; showGameOver(aliveHeroes); return; }

            heroEntities.forEach(h => {
                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.id === 'werewolf' && !h.enraged) h.battleTimer += dt;
            });

            heroEntities.forEach(hero => {
                if (hero.id === 'hanshou' && (hero.hanshouState === 1 || hero.hanshouState === 2)) return;
                if (hero.stunTimer > 0) return;
                if (hero.wanderTimer > 0) hero.wanderTimer -= dt;
                else { hero.wanderTimer = 1.5 + Math.random() * 2; hero.wanderAngle += (Math.random() - 0.5) * Math.PI * 1.5; }
                hero.wanderX = Math.cos(hero.wanderAngle) * hero.speed * 0.4;
                hero.wanderY = Math.sin(hero.wanderAngle) * hero.speed * 0.4;
            });

            heroEntities.forEach(hero => {
                if (hero.id === 'dummy' || hero.hp <= 0) return; 

                // ============== 🌟 狼人 AI (瞬移突进) ==============
                if (hero.id === 'werewolf') {
                    if (hero.pinnedBy || hero.stunTimer > 0) return; 
                    const enemies = heroEntities.filter(h => h.id !== hero.id && h.hp > 0);
                    if (enemies.length === 0) return;
                    let target = enemies[0];
                    const dx = target.x - hero.x; const dy = target.y - hero.y;
                    const distPx = Math.hypot(dx, dy) || 1; const distGrid = distPx / CONFIG.GRID_SIZE;
                    const dirX = dx / distPx; const dirY = dy / distPx;

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

                    // 🌟 瞬移突进逻辑
                    if (hero.dashCooldown <= 0 && distGrid < 2.5 && distGrid > 1.5) {
                        hero.dashCooldown = 5000;
                        
                        // 计算瞬移位置(刚好贴在目标身前)
                        const dashDist = distPx - (hero.radius + target.radius + 5);
                        hero.x += dirX * dashDist;
                        hero.y += dirY * dashDist;
                        constrainToArena(hero);
                        
                        // 结算伤害
                        applyDamage(target, 100, hero, null);
                        addDamageText(target.x, target.y, 100, '#ff00aa');
                        addDamageText(hero.x, hero.y - 30, '突进!', '#ff4444');
                        impactEffects.push({ x: hero.x, y: hero.y, life: 0.4, color: '#aa00aa' });
                        slashEffects.push({ x: hero.x, y: hero.y, angle: Math.atan2(dy, dx), life: 0.3 });
                        return; 
                    }

                    // 常规近战攻击与终结
                    if (distGrid <= 1.5) {
                        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');
                                impactEffects.push({ x: target.x, y: target.y, life: 0.8, color: '#ff0000' });
                            }
                            
                            applyDamage(target, dmg, hero, null);
                            slashEffects.push({ x: hero.x, y: hero.y, angle: Math.atan2(dy, dx), life: 0.3 });
                        }
                    } else {
                        // 追击
                        hero.x += dirX * hero.speed * dt;
                        hero.y += dirY * hero.speed * dt;
                    }
                    hero.facingRight = (dirX > 0);
                    constrainToArena(hero);
                }

                // ============== 未知数 AI ==============
                else if (hero.id === 'unknown') {
                    if (hero.pinnedBy || hero.stunTimer > 0) return; 
                    const enemies = heroEntities.filter(h => h.id !== hero.id && h.hp > 0);
                    let target = enemies[0]; let minDist = Infinity;
                    enemies.forEach(e => { const d = Math.hypot(e.x - hero.x, e.y - hero.y); if (d < minDist) { minDist = d; target = e; } });
                    const dx = target.x - hero.x; const dy = target.y - hero.y;
                    const distPx = minDist || 1; const distGrid = distPx / CONFIG.GRID_SIZE; 
                    const dirX = dx / distPx; const dirY = dy / distPx;
                    let moveX = 0; let moveY = 0;
                    
                    if (distGrid < 3) {
                        if (hero.x <= ARENA_X + hero.radius + 60) { moveY = (hero.y > ARENA_Y + ARENA_HEIGHT / 2) ? -hero.speed : hero.speed; moveX = 0; }
                        else if (hero.x >= ARENA_X + ARENA_WIDTH - hero.radius - 60) { moveY = (hero.y > ARENA_Y + ARENA_HEIGHT / 2) ? -hero.speed : hero.speed; moveX = 0; }
                        else if (hero.y <= ARENA_Y + hero.radius + 60) { moveX = (hero.x > ARENA_X + ARENA_WIDTH / 2) ? -hero.speed : hero.speed; moveY = 0; }
                        else if (hero.y >= ARENA_Y + ARENA_HEIGHT - hero.radius - 60) { moveX = (hero.x > ARENA_X + ARENA_WIDTH / 2) ? -hero.speed : hero.speed; moveY = 0; }
                        else {
                            moveX = -dirX * hero.speed * 0.8; moveY = -dirY * hero.speed * 0.8; hero.facingRight = (dirX < 0);
                            const tangentX = -dirY; const tangentY = dirX; const slideDir = (Math.sin(performance.now() / 1500) > 0) ? 1 : -1;
                            moveX += tangentX * hero.speed * 0.8 * slideDir; moveY += tangentY * hero.speed * 0.8 * slideDir;
                        }
                    } else if (distGrid > 6) {
                        moveX = dirX * hero.speed * 0.6; moveY = dirY * hero.speed * 0.6; hero.facingRight = (dirX > 0);
                    } else {
                        moveX = dirX * hero.speed * 0.2; moveY = dirY * hero.speed * 0.2; hero.facingRight = (dirX > 0);
                    }
                    hero.x += (moveX + hero.wanderX) * dt; hero.y += (moveY + hero.wanderY) * dt;

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

                // ============== 寒守 AI (瞬移冲锋与推墙) ==============
                else if (hero.id === 'hanshou') {
                    if (hero.exhaustTimer > 0) { hero.x += hero.wanderX * 0.5 * dt; hero.y += hero.wanderY * 0.5 * dt; constrainToArena(hero); return; }
                    const enemies = heroEntities.filter(h => h.id !== hero.id && h.hp > 0);
                    if (enemies.length === 0) return;
                    let target = enemies[0];
                    const dx = target.x - hero.x; const dy = target.y - hero.y;
                    const distPx = Math.hypot(dx, dy) || 1; const distGrid = distPx / CONFIG.GRID_SIZE;
                    const dirX = dx / distPx; const dirY = dy / distPx;

                    if (hero.hanshouState === 0) { // 常态叠甲
                        if (distGrid > hero.attackRangeGrid) {
                            hero.x += dirX * hero.speed * dt; hero.y += dirY * hero.speed * dt; hero.facingRight = (dirX > 0);
                        } else {
                            if (hero.attackTimer <= 0) {
                                hero.attackTimer = hero.attackCooldown; hero.attackAnimTimer = 300;
                                applyDamage(target, 58, hero, null); hero.dr = Math.min(76, hero.dr + 10);
                                slashEffects.push({ x: hero.x, y: hero.y, angle: Math.atan2(dy, dx), life: 0.3 });
                                if (hero.dr >= 76) { hero.dr = 0; hero.hanshouState = 1; }
                            }
                        }
                    } else if (hero.hanshouState === 1) { // 🌟 冲锋形态:瞬间瞬移
                        // 瞬移到目标身前
                        const dashDist = Math.max(0, distPx - (hero.radius + target.radius + 5));
                        hero.x += dirX * dashDist;
                        hero.y += dirY * dashDist;
                        constrainToArena(hero);
                        
                        // 结算伤害与推墙
                        applyDamage(target, 220, hero, null); 
                        target.pinnedBy = hero; 
                        target.pinnedX = hero.x + dirX * (hero.radius + target.radius);
                        target.pinnedY = hero.y + dirY * (hero.radius + target.radius);
                        hero.hanshouState = 2; 
                        hero.pinnedTarget = target;
                        impactEffects.push({ x: hero.x, y: hero.y, life: 0.5, color: '#ff0000' });
                    } else if (hero.hanshouState === 2) { // 推墙状态
                        const target = hero.pinnedTarget;
                        // 以恒定速度向最近的墙壁移动
                        const moveDirX = (hero.x < ARENA_X + ARENA_WIDTH / 2) ? -1 : 1;
                        const moveDirY = (hero.y < ARENA_Y + ARENA_HEIGHT / 2) ? -1 : 1;
                        
                        // 选择较短的一边作为推墙方向
                        let pushX = (Math.abs(ARENA_X + ARENA_WIDTH/2 - hero.x) > Math.abs(ARENA_Y + ARENA_HEIGHT/2 - hero.y)) ? moveDirX : 0;
                        let pushY = (pushX === 0) ? moveDirY : 0;

                        hero.x += pushX * 400 * dt; 
                        hero.y += pushY * 400 * dt;
                        
                        // 边界检测(撞击墙壁)
                        if (hero.x <= ARENA_X + hero.radius || hero.x >= ARENA_X + ARENA_WIDTH - hero.radius || hero.y <= ARENA_Y + hero.radius || hero.y >= ARENA_Y + ARENA_HEIGHT - hero.radius) {
                            hero.x = Math.max(ARENA_X + hero.radius, Math.min(ARENA_X + ARENA_WIDTH - hero.radius, hero.x));
                            hero.y = Math.max(ARENA_Y + hero.radius, Math.min(ARENA_Y + ARENA_HEIGHT - hero.radius, hero.y));
                            
                            hero.hanshouState = 3; hero.exhaustTimer = 6000; 
                            target.pinnedBy = 'wall'; target.wallTimer = 4000; target.bleedTickTimer = 0; 
                            hero.pinnedTarget = null;
                            impactEffects.push({ x: hero.x, y: hero.y, life: 0.5, color: '#ff0000' });
                        } else {
                            target.x = hero.x + pushX * (hero.radius + target.radius);
                            target.y = hero.y + pushY * (hero.radius + target.radius);
                        }
                    }
                    constrainToArena(hero);
                }

                // ============== 延施恩 AI ==============
                else if (hero.id === 'yansien') {
                    if (hero.pinnedBy || hero.stunTimer > 0) return; 
                    const enemies = heroEntities.filter(h => h.id !== hero.id && h.hp > 0);
                    let target = enemies[0]; let minDist = Infinity;
                    enemies.forEach(e => { const d = Math.hypot(e.x - hero.x, e.y - hero.y); if (d < minDist) { minDist = d; target = e; } });
                    const dx = target.x - hero.x; const dy = target.y - hero.y;
                    const distPx = minDist || 1; const distGrid = distPx / CONFIG.GRID_SIZE; 
                    const dirX = dx / distPx; const dirY = dy / distPx;
                    let moveX = 0; let moveY = 0;

                    if (distGrid < 4) {
                        if (hero.x <= ARENA_X + hero.radius + 40) { moveY = (hero.y > ARENA_Y + ARENA_HEIGHT / 2) ? -hero.speed : hero.speed; moveX = 0; }
                        else if (hero.x >= ARENA_X + ARENA_WIDTH - hero.radius - 40) { moveY = (hero.y > ARENA_Y + ARENA_HEIGHT / 2) ? -hero.speed : hero.speed; moveX = 0; }
                        else if (hero.y <= ARENA_Y + hero.radius + 40) { moveX = (hero.x > ARENA_X + ARENA_WIDTH / 2) ? -hero.speed : hero.speed; moveY = 0; }
                        else if (hero.y >= ARENA_Y + ARENA_HEIGHT - hero.radius - 40) { moveX = (hero.x > ARENA_X + ARENA_WIDTH / 2) ? -hero.speed : hero.speed; moveY = 0; }
                        else { moveX = -dirX * hero.speed * 0.7; moveY = -dirY * hero.speed * 0.7; }
                    } else if (distGrid > 4) { moveX = dirX * hero.speed * 0.5; moveY = dirY * hero.speed * 0.5; }
                    
                    hero.facingRight = (dirX > 0);
                    hero.x += (moveX + hero.wanderX) * dt; hero.y += (moveY + hero.wanderY) * dt;

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

                // ============== Deepseek AI ==============
                else if (hero.id === 'deepseek') {
                    if (hero.pinnedBy || hero.stunTimer > 0) return; 
                    const enemies = heroEntities.filter(h => h.id !== hero.id && h.hp > 0);
                    if (enemies.length === 0) return;
                    let target = enemies[0]; 
                    const dx = target.x - hero.x; const dy = target.y - hero.y;
                    const distPx = Math.hypot(dx, dy) || 1; const distGrid = distPx / CONFIG.GRID_SIZE; 
                    const dirX = dx / distPx; const dirY = dy / distPx;
                    let moveX = 0; let moveY = 0;

                    if (hero.shieldHp > 0) {
                        if (distGrid < 12) { 
                            if (hero.x <= ARENA_X + hero.radius + 40) { moveY = (hero.y > ARENA_Y + ARENA_HEIGHT / 2) ? -hero.speed : hero.speed; moveX = 0; }
                            else if (hero.x >= ARENA_X + ARENA_WIDTH - hero.radius - 40) { moveY = (hero.y > ARENA_Y + ARENA_HEIGHT / 2) ? -hero.speed : hero.speed; moveX = 0; }
                            else if (hero.y <= ARENA_Y + hero.radius + 40) { moveX = (hero.x > ARENA_X + ARENA_WIDTH / 2) ? -hero.speed : hero.speed; moveY = 0; }
                            else if (hero.y >= ARENA_Y + ARENA_HEIGHT - hero.radius - 40) { moveX = (hero.x > ARENA_X + ARENA_WIDTH / 2) ? -hero.speed : hero.speed; moveY = 0; }
                            else { moveX = -dirX * hero.speed; moveY = -dirY * hero.speed; }
                        }
                    } else {
                        if (distGrid < 7) { 
                            if (hero.x <= ARENA_X + hero.radius + 40) { moveY = (hero.y > ARENA_Y + ARENA_HEIGHT / 2) ? -hero.speed : hero.speed; moveX = 0; }
                            else if (hero.x >= ARENA_X + ARENA_WIDTH - hero.radius - 40) { moveY = (hero.y > ARENA_Y + ARENA_HEIGHT / 2) ? -hero.speed : hero.speed; moveX = 0; }
                            else if (hero.y <= ARENA_Y + hero.radius + 40) { moveX = (hero.x > ARENA_X + ARENA_WIDTH / 2) ? -hero.speed : hero.speed; moveY = 0; }
                            else if (hero.y >= ARENA_Y + ARENA_HEIGHT - hero.radius - 40) { moveX = (hero.x > ARENA_X + ARENA_WIDTH / 2) ? -hero.speed : hero.speed; moveY = 0; }
                            else { moveX = -dirX * hero.speed * 0.6; moveY = -dirY * hero.speed * 0.6; }
                        }
                        if (hero.attackTimer <= 0) {
                            hero.attackTimer = hero.attackCooldown;
                            projectiles.push({ x: hero.x, y: hero.y, startX: hero.x, startY: hero.y, target: target, speed: 400, damage: 13, type: 'text', text: '对不起,这个问题我还无法回答。', knockback: 5 });
                        }
                    }
                    
                    hero.facingRight = (dirX > 0);
                    hero.x += (moveX + hero.wanderX) * dt; hero.y += (moveY + hero.wanderY) * dt;
                    constrainToArena(hero);
                }
            });

            heroEntities.forEach(hero => { if (hero.id === 'dummy' && hero.hp > 0 && !hero.pinnedBy && hero.stunTimer <= 0) { hero.x += hero.wanderX * dt; hero.y += hero.wanderY * dt; } });

            heroEntities.forEach(hero => {
                if (hero.pinnedBy === 'wall') {
                    hero.wallTimer -= dt * 1000; hero.bleedTickTimer -= dt * 1000;
                    if (hero.bleedTickTimer <= 0) { hero.bleedTickTimer = 500; applyDamage(hero, 20, null, null); bleedEffects.push({ x: hero.x, y: hero.y, life: 0.5 }); }
                    if (hero.wallTimer <= 0) { hero.pinnedBy = null; hero.x += (hero.x < ARENA_X + ARENA_WIDTH/2) ? 20 : -20; }
                }
            });

            // 物理碰撞与边界处理
            resolveCollisions();

            // 子弹更新
            for (let i = projectiles.length - 1; i >= 0; i--) {
                const p = projectiles[i];
                if (p.type === 'thrownPistol' && p.target && p.target.hp <= 0) { projectiles.splice(i, 1); continue; }

                let isHit = false;

                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; }
                    
                    heroEntities.forEach(h => {
                        if (h.id !== 'unknown' && h.hp > 0 && Math.hypot(h.x - p.x, h.y - p.y) < h.radius + 8) {
                            applyDamage(h, p.damage, heroEntities.find(uh => uh.id === 'unknown'), p);
                            isHit = true;
                        }
                    });
                } else if (p.target) {
                    const t = p.target;
                    const pdx = t.x - p.x; const pdy = t.y - p.y; const pDist = Math.sqrt(pdx * pdx + pdy * pdy) || 1;
                    if (pDist < t.radius + 8) {
                        let attacker = heroEntities.find(uh => uh.id === 'unknown' || uh.id === 'yansien' || uh.id === 'deepseek' || uh.id === 'werewolf');
                        applyDamage(t, p.damage, attacker, p);
                        isHit = true;
                    } else { p.x += (pdx / pDist) * p.speed * dt; p.y += (pdy / pDist) * p.speed * dt; }
                }

                if (isHit) {
                    if (p.type === 'thrownPistol') {
                        heroEntities.find(h => h.id === 'unknown').weaponState = 'shotgun';
                    }
                    
                    // 击退
                    if (p.knockback > 0 && p.target) {
                        const t = p.target;
                        const pdx = t.x - p.x; const pdy = t.y - p.y;
                        const pDist = Math.sqrt(pdx * pdx + pdy * pdy) || 1;
                        const knockDirX = pdx / pDist; const knockDirY = pdy / pDist;
                        t.x += knockDirX * p.knockback * CONFIG.GRID_SIZE;
                        t.y += knockDirY * p.knockback * CONFIG.GRID_SIZE;
                        constrainToArena(t);
                    }
                    // 定身
                    if (p.type === 'pen' && p.target) {
                        p.target.stunTimer = 3000;
                        addDamageText(p.target.x, p.target.y - 20, '定身!', '#ffff00');
                    }
                    
                    impactEffects.push({ x: p.x, y: p.y, life: 0.2, color: p.type === 'fireext' ? '#ff5500' : (p.type === 'text' ? '#4488ff' : '#fff') });
                    projectiles.splice(i, 1);
                }
            }

            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--) { const txt = damageTexts[i]; txt.y -= 30 * dt; txt.life -= dt; if (txt.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); }
        }

        // 🌟 实体碰撞处理(防止重叠导致 NaN 消失)
        function resolveCollisions() {
            for (let i = 0; i < heroEntities.length; i++) {
                for (let j = i + 1; j < heroEntities.length; j++) {
                    let h1 = heroEntities[i];
                    let h2 = heroEntities[j];
                    if (h1.hp <= 0 || h2.hp <= 0) continue;
                    
                    let dx = h2.x - h1.x;
                    let dy = h2.y - h1.y;
                    let dist = Math.hypot(dx, dy);
                    let minDist = h1.radius + h2.radius;
                    
                    if (dist < 0.01) { dx = 0.01; dy = 0; dist = 0.01; }
                    
                    if (dist < minDist) {
                        let overlap = minDist - dist;
                        let nx = dx / dist;
                        let ny = dy / dist;
                        
                        if (h1.id === 'hanshou' && h1.hanshouState === 2) {
                            h2.x = h1.x + nx * minDist;
                            h2.y = h1.y + ny * minDist;
                        } else if (h2.id === 'hanshou' && h2.hanshouState === 2) {
                            h1.x = h2.x + nx * minDist;
                            h1.y = h2.y + ny * minDist;
                        } else {
                            h1.x -= nx * overlap * 0.5;
                            h1.y -= ny * overlap * 0.5;
                            h2.x += nx * overlap * 0.5;
                            h2.y += ny * overlap * 0.5;
                        }
                    }
                }
            }
            heroEntities.forEach(hero => constrainToArena(hero));
        }

        function applyDamage(target, amount, attacker, projectile) {
            if (target.id === 'deepseek' && target.shieldHp > 0) {
                target.shieldHp -= amount;
                if (target.shieldHp < 0) { target.shieldHp = 0; target.shieldCooldown = 60000; }
                
                if (attacker && attacker.hp > 0 && attacker.id !== 'deepseek' && projectile) {
                    let reflectDmg = Math.round(amount * 0.5);
                    projectiles.push({ x: target.x, y: target.y, startX: target.x, startY: target.y, target: attacker, speed: projectile.speed || 800, damage: reflectDmg, type: projectile.type, color: '#4488ff' });
                }
                return;
            }

            if (target.id === 'deepseek' && target.shieldHp <= 0 && target.shieldCooldown <= 0) {
                target.shieldHp = target.maxShieldHp;
            }

            if (target.id === 'hanshou' && target.exhaustTimer <= 0) {
                target.dr = Math.max(0, target.dr - 5);
            }
            
            if (isNaN(amount)) amount = 0;
            
            target.hp -= amount;
            addDamageText(target.x, target.y, amount, target.id === 'hanshou' ? '#ff4444' : (target.id === 'werewolf' ? '#ff00aa' : '#00ffcc'));

            if (target.id === 'yansien' && attacker && (attacker.id === 'hanshou' || attacker.id === 'dummy' || attacker.id === 'werewolf')) {
                applyDamage(attacker, 200, target, null);
                target.hp -= 400;
                addDamageText(target.x, target.y - 20, 400, '#ffaa00');
                impactEffects.push({ x: target.x, y: target.y, life: 0.5, color: '#ffaa00' });
            }
        }

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

        function addDamageText(x, y, amount, color) { damageTexts.push({ x, y: y - 20, text: amount.toString(), color: color, life: 1.0 }); }

        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(); drawEffects(); drawProjectiles(); drawEntities(); 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 drawEffects() {
            muzzleFlashes.forEach(e => { const alpha = e.life / 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 = e.life / 0.3; ctx.beginPath(); ctx.arc(0, 0, 35, -Math.PI / 4, Math.PI / 4); ctx.strokeStyle = `rgba(255, 68, 68, ${alpha})`; ctx.lineWidth = 6; ctx.stroke(); ctx.restore(); });
            impactEffects.forEach(e => { const alpha = e.life / 0.2; ctx.beginPath(); ctx.arc(e.x, e.y, (1 - alpha) * 20, 0, Math.PI * 2); ctx.strokeStyle = e.color || `rgba(255, 255, 255, ${alpha})`; ctx.lineWidth = e.color ? 8 : 2; ctx.stroke(); });
            bleedEffects.forEach(e => { ctx.beginPath(); ctx.arc(e.x, e.y, 6, 0, Math.PI * 2); ctx.fillStyle = `rgba(255, 0, 0, ${e.life})`; ctx.fill(); });
        }

        function drawProjectiles() {
            projectiles.forEach(p => {
                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); 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); 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 {
                    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() {
            heroEntities.forEach(entity => {
                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'; 
                }
                if (entity.stunTimer > 0) fillColor = '#ffff00';
                if (entity.id === 'werewolf' && entity.enraged) fillColor = '#ff00aa';

                ctx.save(); ctx.translate(entity.x, entity.y); if (!entity.facingRight) ctx.scale(-1, 1); ctx.scale(scale, scale);
                
                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 === '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 === 'unknown') {
                    ctx.fillStyle = '#666';
                    if (entity.weaponState === 'pistol' || entity.weaponState === 'throwing') {
                        ctx.fillRect(entity.radius - 2, -5, 12, 10); ctx.fillStyle = '#333'; ctx.fillRect(entity.radius - 2, 5, 6, 6);
                    } else if (entity.weaponState === 'shotgun') {
                        ctx.fillRect(entity.radius - 2, -7, 22, 14); ctx.fillStyle = '#333'; ctx.fillRect(entity.radius - 2, 7, 8, 8);
                    }
                }

                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') { ctx.beginPath(); ctx.arc(5, -4, 2.5, 0, Math.PI * 2); ctx.fillStyle = '#fff'; ctx.fill(); }
                ctx.restore();

                const barWidth = 50; const 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 drHeight = 3; const drY = barY + barHeight + 2;
                    ctx.fillStyle = '#333'; ctx.fillRect(barX, drY, barWidth, drHeight);
                    ctx.fillStyle = '#ffcc00'; ctx.fillRect(barX, drY, barWidth * (entity.dr / 76), drHeight);
                }
                if (entity.id === 'deepseek' && entity.shieldHp > 0) {
                    const shPercent = entity.shieldHp / entity.maxShieldHp;
                    const shY = barY + barHeight + 2;
                    ctx.fillStyle = '#333'; ctx.fillRect(barX, shY, barWidth, 3);
                    ctx.fillStyle = '#4488ff'; ctx.fillRect(barX, shY, barWidth * shPercent, 3);
                }
                if (entity.id === 'werewolf' && !entity.enraged) {
                    const tPercent = entity.battleTimer / 20;
                    const tY = barY + barHeight + 2;
                    ctx.fillStyle = '#333'; ctx.fillRect(barX, tY, barWidth, 3);
                    ctx.fillStyle = '#ff00aa'; ctx.fillRect(barX, tY, barWidth * tPercent, 3);
                }

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

        function drawDamageTexts() {
            damageTexts.forEach(txt => { ctx.fillStyle = txt.color; ctx.font = 'bold 14px sans-serif'; ctx.textAlign = 'center'; ctx.globalAlpha = 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 (772 lines, 46.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-mugmnrow" to link back to the original. Then publish at arcadelab.ai/publish.