电子斗蛐蛐 - 终极全英雄版
by EpicCoder881970 lines97.2 KB
<!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: 15px; }
#selectScreen h2 { font-weight: normal; color: #aaa; font-size: 18px; letter-spacing: 2px; }
#heroList { display: flex; gap: 12px; flex-wrap: wrap; justify-content: center; max-width: 92vw; max-height: 70vh; overflow-y: auto; padding: 10px; }
.hero-card { padding: 10px 20px; background-color: #2a2a2a; border: 2px solid #444; border-radius: 8px; font-size: 15px; 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.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>
(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;
// ==========================================
// 📖 英雄图鉴(全部14位 + 假人)
// ==========================================
const HERO_LIST = [
{ id: 'unknown', name: '未知数', color: '#00ffcc', maxHp: 1000, speed: 130 },
{ id: 'hanshou', name: '寒守', color: '#ff4444', maxHp: 1000, speed: 250 },
{ id: 'yansien', name: '延施恩', color: '#ffaa00', maxHp: 2000, speed: 150 },
{ id: 'deepseek', name: 'Deepseek', color: '#4488ff', maxHp: 900, speed: 150 },
{ id: 'werewolf', name: '狼人', color: '#aa00aa', maxHp: 1200, speed: 150 },
{ id: 'newton', name: '牛顿', color: '#dddddd', maxHp: 1100, speed: 120 },
{ id: 'knight', name: '骑士', color: '#cccccc', maxHp: 1100, speed: 110 },
{ id: 'pla', name: 'PLA', color: '#4a5d23', maxHp: 1000, speed: 120 },
{ id: 'tangjiaqi', name: '唐佳琪', color: '#ff00ff', maxHp: 1000, speed: 150 },
{ id: 'principal', name: '贪污校长', color: '#b8860b', maxHp: 1200, speed: 110 },
{ id: 'oldpastor', name: '老牧师', color: '#800080', maxHp: 1000, speed: 110 },
{ id: 'hollyleaf', name: '冬青叶', color: '#2e8b57', maxHp: 1500, speed: 150 },
{ id: 'sungod', name: '日神', color: '#ffd700', maxHp: 900, speed: 130 },
{ id: 'clover', name: 'Clover', color: '#00bfff', maxHp: 1000, speed: 130 },
{ 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 prisms = []; // 唐佳琪末影珍珠
let parents = []; // 校长家长
let tunnels = []; // 冬青叶地道
let holyCircles = []; // 老牧师金圈
let textBeams = []; // 日神弹幕
let lasers = []; // Clover激光
let dummyShadows = []; // 校长替身草人
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 = []; prisms = []; parents = []; tunnels = [];
holyCircles = []; textBeams = []; lasers = []; dummyShadows = [];
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,
slowTimer: 0, damageDebuffTimer: 0
};
// ========== 未知数 ==========
if (id === 'unknown') {
hero.weaponState = 'pistol'; hero.shotsFired = 0; hero.shotgunShots = 0;
hero.attackCooldown = 500; hero.attackRangeGrid = 10;
}
// ========== 寒守 ==========
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 === 'yansien') {
hero.attackCooldown = 4500; hero.attackRangeGrid = 4;
}
// ========== Deepseek ==========
if (id === 'deepseek') {
hero.shieldHp = 1500; hero.maxShieldHp = 1500;
hero.shieldCooldown = 0; hero.attackCooldown = 1000;
}
// ========== 牛顿 ==========
if (id === 'newton') {
hero.prismCooldown = 5000; hero.appleTimer = 30000;
hero.attackCooldown = 99999; hero.attackRangeGrid = 999;
hero.satellites = []; hero.satelliteTimer = 5000;
}
// ========== 骑士 ==========
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;
}
// ========== PLA ==========
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;
}
// ========== 唐佳琪 ==========
if (id === 'tangjiaqi') {
hero.phase = 'endure'; // 'endure' | 'divine'
hero.phaseTimer = 30000; // 30秒隐忍
hero.divineTimer = 10000; // 10秒神之领域
hero.totalDivineDamage = 950; // 大招伤害池
hero.dealtDivineDamage = 0;
hero.pearls = 10; // 10颗末影珍珠
hero.attackCooldown = 99999; // 隐忍期无法攻击
hero.attackRangeGrid = 0;
hero.divineAccum = 0; // 累积伤害飘字
}
// ========== 贪污校长 ==========
if (id === 'principal') {
hero.shieldHp = 0; hero.maxShieldHp = 300;
hero.siphonTimer = 0; // 每秒获得护盾
hero.attackCooldown = 4000; // 空头支票
hero.attackRangeGrid = 6;
hero.parentCooldown = 8000; // 家长会
hero.usedRunaway = false; // 卷款跑路只能触发一次
hero.invulnTimer = 0;
}
// ========== 老牧师 ==========
if (id === 'oldpastor') {
hero.rageBar = 0; // 圣怒条0-100
hero.rageState = 'charging'; // 'charging' | 'divine'
hero.chargeTimer = 15000; // 15秒充能
hero.divineTimer = 8000; // 8秒神之领域
hero.attackCooldown = 1500; // 十字架砸人
hero.attackRangeGrid = 1.5;
hero.attackDamage = 15;
}
// ========== 冬青叶 ==========
if (id === 'hollyleaf') {
hero.tunnelCooldown = 0; // 地道冷却
hero.tunnelState = 'idle'; // 'idle' | 'entering' | 'exiting' | 'striking' | 'retreating'
hero.tunnelTimer = 0;
hero.tunnelTargetX = 0;
hero.tunnelTargetY = 0;
hero.strikeCount = 0;
hero.attackCooldown = 1200; hero.attackRangeGrid = 1.5;
}
// ========== 日神 ==========
if (id === 'sungod') {
hero.attackCooldown = 6000; // 弹幕
hero.attackRangeGrid = 8;
hero.attackDamage = 67; // 近战平A
hero.meleeCooldown = 1500;
}
// ========== Clover ==========
if (id === 'clover') {
hero.clipAmmo = 6; hero.maxClipAmmo = 6;
hero.isReloading = false; hero.reloadTimer = 0;
hero.attackCooldown = 200; // 0.2秒/发
hero.attackRangeGrid = 8;
hero.bulletDamage = 15;
hero.rage = 0; // 怒气值
hero.maxRage = 80;
hero.laserCharging = false; // 激光蓄力
hero.laserTimer = 0;
}
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;
let 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.meleeCooldown > 0) h.meleeCooldown -= dt * 1000;
if (h.parentCooldown > 0) h.parentCooldown -= dt * 1000;
if (h.tunnelCooldown > 0) h.tunnelCooldown -= dt * 1000;
if (h.invulnTimer > 0) h.invulnTimer -= dt * 1000;
if (h.slowTimer > 0) h.slowTimer -= dt * 1000;
if (h.damageDebuffTimer > 0) h.damageDebuffTimer -= dt * 1000;
if (h.id === 'werewolf' && !h.enraged) h.battleTimer += dt;
// PLA换弹
if (h.id === 'pla') {
if (h.isReloading) {
h.reloadTimer -= dt * 1000;
if (h.reloadTimer <= 0) { h.isReloading = false; h.clipAmmo = h.maxClipAmmo; }
}
if (h.damageBuffTimer > 0) {
h.damageBuffTimer -= dt * 1000;
if (h.damageBuffTimer <= 0) h.damageBuffAdd = 0;
}
if (h.reactionCooldown > 0) h.reactionCooldown -= dt * 1000;
}
// Clover换弹
if (h.id === 'clover') {
if (h.isReloading) {
h.reloadTimer -= dt * 1000;
if (h.reloadTimer <= 0) { h.isReloading = false; h.clipAmmo = h.maxClipAmmo; }
}
if (h.laserCharging) {
h.laserTimer -= dt * 1000;
if (h.laserTimer <= 0) {
// 发射激光
let target = heroEntities.find(e => e.id !== h.id && e.hp > 0);
if (target) {
lasers.push({ x1: h.x, y1: h.y, x2: target.x, y2: target.y, timer: 0.4, target });
applyDamage(target, 680, h, null, true);
h.rage = 0;
addDamageText(h.x, h.y - 40, '激光!', '#00bfff');
}
h.laserCharging = false;
}
}
}
}
// 寒守拔剑
for (const h of heroEntities) {
if (h.id === 'hanshou' && h.exhaustTimer <= 0 && !h.hasSword && h.hp > 0) {
h.hasSword = true;
addDamageText(h.x, h.y - 30, '拔剑!', '#ffffff');
}
}
// 随机游走
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;
}
// ========== 唐佳琪大招判定 ==========
updateTangJiaqiPhase(dt);
// ========== 老牧师圣怒判定 ==========
updateOldPastorPhase(dt);
// ========== 牛顿苹果雨 ==========
updateNewtonApples(dt);
// ========== 苹果拾取 ==========
updateApplePickup();
// ========== 三棱镜扫射 ==========
updatePrisms(dt);
// ========== 手榴弹 ==========
updateGrenades(dt);
// ========== 校长家长 ==========
updateParents(dt);
// ========== 冬青叶地道 ==========
updateHollyleafTunnels(dt);
// ========== 老牧师金圈 ==========
updateHolyCircles(dt);
// ========== 日神弹幕 ==========
updateTextBeams(dt);
// ========== Clover激光视觉 ==========
updateLasers(dt);
// ========== 逐英雄 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;
switch (hero.id) {
case 'unknown': updateUnknown(hero, dt); break;
case 'hanshou': updateHanshou(hero, dt); break;
case 'yansien': updateYansien(hero, dt); break;
case 'deepseek': updateDeepseek(hero, dt); break;
case 'werewolf': updateWerewolf(hero, dt); break;
case 'newton': updateNewton(hero, dt); break;
case 'knight': updateKnight(hero, dt); break;
case 'pla': updatePla(hero, dt); break;
case 'tangjiaqi': updateTangJiaqi(hero, dt); break;
case 'principal': updatePrincipal(hero, dt); break;
case 'oldpastor': updateOldPastor(hero, dt); break;
case 'hollyleaf': updateHollyleaf(hero, dt); break;
case 'sungod': updateSunGod(hero, dt); break;
case 'clover': updateClover(hero, dt); break;
}
}
// 钉墙流血
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);
}
// ==========================================
// 🌟 唐佳琪阶段控制
// ==========================================
function updateTangJiaqiPhase(dt) {
for (const h of heroEntities) {
if (h.id !== 'tangjiaqi' || h.hp <= 0) continue;
if (h.phase === 'endure') {
h.phaseTimer -= dt * 1000;
if (h.phaseTimer <= 0) {
h.phase = 'divine';
h.divineTimer = 10000;
h.dealtDivineDamage = 0;
addDamageText(h.x, h.y - 50, '尾灯双闪!', '#ff00ff');
impactEffects.push({ x: h.x, y: h.y, color: '#ff00ff', life: 1.0, maxLife: 1.0 });
}
} else if (h.phase === 'divine') {
h.divineTimer -= dt * 1000;
// 强制减速敌人
for (const enemy of heroEntities) {
if (enemy.id === h.id || enemy.hp <= 0) continue;
if (Math.hypot(enemy.x - h.x, enemy.y - h.y) < 8 * CONFIG.GRID_SIZE) {
enemy.slowTimer = 3000;
}
}
// 伤害池判定:每0.1秒一次
if (h.divineTimer > 0 && h.dealtDivineDamage < 950) {
let dmgPerTick = 9.5;
for (const enemy of heroEntities) {
if (enemy.id === h.id || enemy.hp <= 0) continue;
if (Math.hypot(enemy.x - h.x, enemy.y - h.y) < 8 * CONFIG.GRID_SIZE) {
let actual = Math.min(dmgPerTick, 950 - h.dealtDivineDamage);
applyDamage(enemy, actual, h, null, true, true);
h.dealtDivineDamage += actual;
h.divineAccum += actual;
}
}
}
if (h.divineTimer <= 0 || h.dealtDivineDamage >= 950) {
h.phase = 'endure';
h.phaseTimer = 30000;
h.pearls = 10;
addDamageText(h.x, h.y - 50, '白光消散', '#ffffff');
}
}
}
}
// ==========================================
// 🌟 老牧师圣怒控制
// ==========================================
function updateOldPastorPhase(dt) {
for (const h of heroEntities) {
if (h.id !== 'oldpastor' || h.hp <= 0) continue;
if (h.rageState === 'charging') {
h.chargeTimer -= dt * 1000;
h.rageBar = Math.min(100, (1 - h.chargeTimer / 15000) * 100);
if (h.chargeTimer <= 0) {
h.rageState = 'divine';
h.divineTimer = 8000;
h.rageBar = 100;
addDamageText(h.x, h.y - 50, '神圣领域!', '#ffd700');
holyCircles.push({ x: h.x, y: h.y, radius: 5 * CONFIG.GRID_SIZE, ownerId: h.id, timer: 8000 });
}
} else if (h.rageState === 'divine') {
h.divineTimer -= dt * 1000;
h.rageBar = Math.max(0, (h.divineTimer / 8000) * 100);
// 更新圆位置
const c = holyCircles.find(c => c.ownerId === h.id);
if (c) { c.x = h.x; c.y = h.y; c.radius = 5 * CONFIG.GRID_SIZE; }
if (h.divineTimer <= 0) {
h.rageState = 'charging';
h.chargeTimer = 15000;
h.rageBar = 0;
const idx = holyCircles.findIndex(c => c.ownerId === h.id);
if (idx >= 0) holyCircles.splice(idx, 1);
}
}
}
}
// ==========================================
// 🌟 牛顿苹果雨
// ==========================================
function updateNewtonApples(dt) {
for (const hero of heroEntities) {
if (hero.id !== 'newton' || hero.hp <= 0) continue;
if (hero.appleTimer <= 0) {
hero.appleTimer = 30000;
for (let i = 0; i < 10; i++) {
if (apples.length >= 20) break;
let ax = ARENA_X + Math.random() * ARENA_WIDTH;
let ay = ARENA_Y + Math.random() * ARENA_HEIGHT;
let hitEnemy = false;
for (const h of heroEntities) {
if (h.id !== 'newton' && h.hp > 0 && Math.hypot(h.x - ax, h.y - ay) < h.radius + 10) {
applyDamage(h, 40, hero, null);
addDamageText(h.x, h.y - 30, '🍎 40', '#ff4444');
hitEnemy = true; break;
}
}
if (!hitEnemy && apples.length < 20) apples.push({ x: ax, y: ay, radius: 8 });
}
}
}
}
function updateApplePickup() {
for (let i = apples.length - 1; i >= 0; i--) {
let apple = apples[i];
for (let h of heroEntities) {
if (h.hp <= 0 || h.pinnedBy) continue;
if (Math.hypot(h.x - apple.x, h.y - apple.y) < h.radius + apple.radius) {
if (h.id === 'newton') {
h.hp = Math.min(h.maxHp, h.hp + 50);
addDamageText(h.x, h.y - 20, '+50', '#00ff00');
} else {
h.hp = Math.min(h.maxHp, h.hp + 10);
addDamageText(h.x, h.y - 20, '+10', '#00ff00');
}
apples.splice(i, 1);
break;
}
}
}
}
// ==========================================
// 🌟 三棱镜扫射
// ==========================================
function updatePrisms(dt) {
for (let i = newtonPrisms.length - 1; i >= 0; i--) {
let p = newtonPrisms[i];
p.timer -= dt * 1000;
if (p.timer <= 0) { newtonPrisms.splice(i, 1); continue; }
p.angle += p.sweepDir * 2.5 * dt;
if (p.angle > Math.PI / 3) p.sweepDir = -1;
if (p.angle < -Math.PI / 3) p.sweepDir = 1;
for (let h of heroEntities) {
if (h.id === 'newton' || h.hp <= 0) continue;
let dx = h.x - p.x, dy = h.y - p.y;
let dist = Math.hypot(dx, dy);
if (dist < 240) {
let angleToTarget = Math.atan2(dy, dx);
let angleDiff = Math.abs(angleToTarget - p.baseAngle);
while (angleDiff > Math.PI) angleDiff = Math.abs(angleDiff - 2 * Math.PI);
if (angleDiff < p.angle + Math.PI / 6) {
applyDamage(h, 1, heroEntities.find(uh => uh.id === 'newton'), null, true, true);
if (!p.damageAccum) p.damageAccum = {};
if (!p.damageAccum[h.id]) p.damageAccum[h.id] = 0;
p.damageAccum[h.id] += 1;
}
}
}
p.textTimer = (p.textTimer || 0) - dt;
if (p.textTimer <= 0) {
p.textTimer = 0.5;
if (p.damageAccum) {
for (let id in p.damageAccum) {
if (p.damageAccum[id] > 0) {
let h = heroEntities.find(x => x.id === id);
if (h && h.hp > 0) addDamageText(h.x, h.y, p.damageAccum[id], '#ff00ff');
p.damageAccum[id] = 0;
}
}
}
}
}
}
// ==========================================
// 🌟 手榴弹
// ==========================================
function updateGrenades(dt) {
for (let i = grenades.length - 1; i >= 0; i--) {
let g = grenades[i];
g.timer -= dt;
g.x += g.vx * dt; g.y += g.vy * dt;
g.vx *= 0.95; g.vy *= 0.95;
if (g.timer <= 0) {
let radius = 2 * CONFIG.GRID_SIZE;
impactEffects.push({ x: g.x, y: g.y, color: '#ff8800', life: 0.5, maxLife: 0.5 });
for (const h of heroEntities) {
if (h.hp > 0 && Math.hypot(h.x - g.x, h.y - g.y) < radius + h.radius) {
applyDamage(h, 50, heroEntities.find(uh => uh.id === 'pla'), null);
}
}
grenades.splice(i, 1);
}
}
}
// ==========================================
// 🌟 校长家长
// ==========================================
function updateParents(dt) {
for (let i = parents.length - 1; i >= 0; i--) {
let p = parents[i];
p.life -= dt;
if (p.life <= 0) { parents.splice(i, 1); continue; }
p.wanderTimer -= dt;
if (p.wanderTimer <= 0) { p.wanderTimer = 1 + Math.random() * 2; p.wanderAngle += (Math.random() - 0.5) * Math.PI * 2; }
p.x += Math.cos(p.wanderAngle) * 60 * dt;
p.y += Math.sin(p.wanderAngle) * 60 * dt;
constrainToArena(p);
// 检测敌人触碰
for (const h of heroEntities) {
if (h.hp <= 0 || h.id === 'principal') continue;
if (Math.hypot(h.x - p.x, h.y - p.y) < h.radius + p.radius) {
applyDamage(h, 30, heroEntities.find(uh => uh.id === 'principal'), null);
h.stunTimer = Math.max(h.stunTimer, 1500);
addDamageText(h.x, h.y - 20, '家长爆炸!', '#b8860b');
impactEffects.push({ x: p.x, y: p.y, color: '#b8860b', life: 0.5, maxLife: 0.5 });
// 校长加护盾
let principal = heroEntities.find(uh => uh.id === 'principal');
if (principal) principal.shieldHp = Math.min(principal.maxShieldHp, principal.shieldHp + 50);
parents.splice(i, 1);
break;
}
}
}
}
// ==========================================
// 🌟 冬青叶地道
// ==========================================
function updateHollyleafTunnels(dt) {
for (const h of heroEntities) {
if (h.id !== 'hollyleaf' || h.hp <= 0) continue;
// 空闲状态,检查是否刷新入口
if (h.tunnelState === 'idle' && h.tunnelCooldown <= 0) {
// 在敌人附近随机刷新入口
const enemies = heroEntities.filter(e => e.id !== h.id && e.hp > 0);
if (enemies.length > 0) {
const target = enemies[0];
const angle = Math.random() * Math.PI * 2;
const dist = (3 + Math.random() * 2) * CONFIG.GRID_SIZE;
const ex = target.x + Math.cos(angle) * dist;
const ey = target.y + Math.sin(angle) * dist;
tunnels.push({ x: Math.max(ARENA_X + 20, Math.min(ARENA_X + ARENA_WIDTH - 20, ex)),
y: Math.max(ARENA_Y + 20, Math.min(ARENA_Y + ARENA_HEIGHT - 20, ey)),
timer: 4000, type: 'entrance', ownerId: h.id });
h.tunnelCooldown = 8000;
}
}
// 如果入口存在且冬青叶靠近,触发
if (h.tunnelState === 'idle') {
const entrance = tunnels.find(t => t.type === 'entrance' && t.ownerId === h.id);
if (entrance && Math.hypot(h.x - entrance.x, h.y - entrance.y) < h.radius + 15) {
h.tunnelState = 'entering';
h.tunnelTimer = 300;
h.invulnTimer = 1000;
// 在敌人附近生成出口
const enemies = heroEntities.filter(e => e.id !== h.id && e.hp > 0);
if (enemies.length > 0) {
const target = enemies[0];
const angle = Math.random() * Math.PI * 2;
const dist = (2 + Math.random()) * CONFIG.GRID_SIZE;
const ex = target.x + Math.cos(angle) * dist;
const ey = target.y + Math.sin(angle) * dist;
tunnels.push({ x: Math.max(ARENA_X + 20, Math.min(ARENA_X + ARENA_WIDTH - 20, ex)),
y: Math.max(ARENA_Y + 20, Math.min(ARENA_Y + ARENA_HEIGHT - 20, ey)),
timer: 3000, type: 'exit', ownerId: h.id });
}
}
}
// 钻地动画
if (h.tunnelState === 'entering') {
h.tunnelTimer -= dt * 1000;
if (h.tunnelTimer <= 0) {
h.tunnelState = 'exiting';
h.tunnelTimer = 300;
// 瞬移到出口
const exit = tunnels.find(t => t.type === 'exit' && t.ownerId === h.id);
if (exit) { h.x = exit.x; h.y = exit.y; }
}
} else if (h.tunnelState === 'exiting') {
h.tunnelTimer -= dt * 1000;
if (h.tunnelTimer <= 0) {
h.tunnelState = 'striking';
h.tunnelTimer = 200;
h.strikeCount = 0;
}
} else if (h.tunnelState === 'striking') {
h.tunnelTimer -= dt * 1000;
// 三连击
if (h.strikeCount < 3 && h.tunnelTimer <= 0) {
const enemies = heroEntities.filter(e => e.id !== h.id && e.hp > 0);
for (const e of enemies) {
if (Math.hypot(e.x - h.x, e.y - h.y) < 2 * CONFIG.GRID_SIZE) {
applyDamage(e, 130, h, null);
addDamageText(e.x, e.y - 20, '130', '#2e8b57');
}
}
h.strikeCount++;
h.tunnelTimer = 100;
}
if (h.strikeCount >= 3) {
h.tunnelState = 'retreating';
h.tunnelTimer = 300;
}
} else if (h.tunnelState === 'retreating') {
h.tunnelTimer -= dt * 1000;
if (h.tunnelTimer <= 0) {
h.tunnelState = 'idle';
// 移除该英雄的所有地道
tunnels = tunnels.filter(t => t.ownerId !== h.id);
}
}
}
// 地道超时清理
for (let i = tunnels.length - 1; i >= 0; i--) {
tunnels[i].timer -= dt * 1000;
if (tunnels[i].timer <= 0) tunnels.splice(i, 1);
}
}
// ==========================================
// 🌟 老牧师金圈伤害
// ==========================================
function updateHolyCircles(dt) {
for (const c of holyCircles) {
c.tickTimer = (c.tickTimer || 0) - dt;
if (c.tickTimer <= 0) {
c.tickTimer = 1.0;
for (const h of heroEntities) {
if (h.id === c.ownerId || h.hp <= 0) continue;
if (Math.hypot(h.x - c.x, h.y - c.y) < c.radius + h.radius) {
applyDamage(h, 46, heroEntities.find(uh => uh.id === c.ownerId), null, true);
}
}
}
}
}
// ==========================================
// 🌟 日神弹幕
// ==========================================
function updateTextBeams(dt) {
for (let i = textBeams.length - 1; i >= 0; i--) {
let b = textBeams[i];
b.timer -= dt;
if (b.timer <= 0) { textBeams.splice(i, 1); continue; }
b.x += b.vx * dt;
b.y += b.vy * dt;
// 命中检测
for (const h of heroEntities) {
if (h.id === b.ownerId || h.hp <= 0) continue;
if (Math.hypot(h.x - b.x, h.y - b.y) < h.radius + 10) {
applyDamage(h, 11, heroEntities.find(uh => uh.id === b.ownerId), null);
h.damageDebuffTimer = 20000; // 20秒
addDamageText(h.x, h.y - 20, '自我怀疑', '#ffd700');
// 10%困惑
if (Math.random() < 0.1) {
h.slowTimer = 30000;
h.damageDebuffTimer = 30000;
addDamageText(h.x, h.y - 40, '困惑!', '#ffd700');
}
textBeams.splice(i, 1);
break;
}
}
}
}
// ==========================================
// 🌟 Clover激光
// ==========================================
function updateLasers(dt) {
for (let i = lasers.length - 1; i >= 0; i--) {
lasers[i].timer -= dt;
if (lasers[i].timer <= 0) lasers.splice(i, 1);
}
}
// ==========================================
// 🐾 各英雄 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; }
} else { hero.weaponState = 'pistol'; hero.shotsFired = 0; hero.shotgunShots = 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 ndx = target.x - hero.x, ndy = target.y - hero.y;
const nd = Math.hypot(ndx, ndy);
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;
const 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 updateYansien(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 < 4) { moveX = -dirX * hero.speed * 0.7; moveY = -dirY * hero.speed * 0.7;
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.7; }
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.7; moveY = 0; }
} 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 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 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 ndx = target.x - hero.x, ndy = target.y - hero.y;
const nd = Math.hypot(ndx, ndy);
if (nd <= hero.radius + target.radius + 10) {
applyDamage(target, 100, hero, null);
slashEffects.push({ x: hero.x, y: hero.y, angle: Math.atan2(ndy, ndx), 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 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, 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, 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 (const 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 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, 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 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;
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.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() < 0.1) {
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];
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 (hero.phase === 'endure') {
if (distGrid < 5) { moveX = -dirX * hero.speed * 1.2; moveY = -dirY * hero.speed * 1.2; }
else if (distGrid > 10) { moveX = dirX * hero.speed * 0.5; moveY = dirY * hero.speed * 0.5; }
else { let t = (Math.sin(performance.now() / 800) > 0) ? 1 : -1; moveX = -dirY * hero.speed * t; moveY = dirX * hero.speed * t; }
} else {
// 神之领域,站桩不动
moveX = 0; moveY = 0;
}
hero.x += (moveX + hero.wanderX * 0.3) * dt; hero.y += (moveY + hero.wanderY * 0.3) * dt;
constrainToArena(hero);
// 普攻(隐忍期无攻击)
if (hero.phase === 'divine') return;
// 隐忍期受击由 applyDamage 里处理(末影珍珠)
}
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;
// 挪动公款:每秒获得护盾
hero.siphonTimer -= dt;
if (hero.siphonTimer <= 0) { hero.siphonTimer = 1; hero.shieldHp = Math.min(hero.maxShieldHp, hero.shieldHp + 5); }
// 空头支票:4秒冷却
if (hero.attackTimer <= 0 && distGrid <= hero.attackRangeGrid) {
hero.attackTimer = hero.attackCooldown;
spawnProjectile({ x: hero.x, y: hero.y, startX: hero.x, startY: hero.y, target, speed: 500, damage: 20, type: 'check', ownerId: hero.id, debuff: true });
}
// 家长会
if (hero.parentCooldown <= 0) {
hero.parentCooldown = 8000;
for (let i = 0; i < 2; i++) {
parents.push({ x: hero.x + (Math.random() - 0.5) * 60, y: hero.y + (Math.random() - 0.5) * 60, radius: 8, life: 5, wanderTimer: 0, wanderAngle: Math.random() * Math.PI * 2 });
}
addDamageText(hero.x, hero.y - 30, '家长会!', '#b8860b');
}
// 移动
let moveX = 0, moveY = 0;
if (distGrid < 5) { moveX = -dirX * hero.speed * 0.8; moveY = -dirY * hero.speed * 0.8; }
else if (distGrid > 8) { 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);
}
function updateOldPastor(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 > 1.3) { moveX = dirX * hero.speed; moveY = dirY * hero.speed; }
else { let t = (Math.sin(performance.now() / 800) > 0) ? 1 : -1; moveX = -dirY * hero.speed * 0.5 * t; moveY = dirX * hero.speed * 0.5 * t; }
hero.x += (moveX + hero.wanderX * 0.4) * dt; hero.y += (moveY + hero.wanderY * 0.4) * dt;
constrainToArena(hero);
// 普攻
if (hero.attackTimer <= 0 && distGrid <= hero.attackRangeGrid) {
hero.attackTimer = hero.attackCooldown;
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: '#800080' });
}
}
function updateHollyleaf(hero, dt) {
const enemies = heroEntities.filter(h => h.id !== hero.id && h.hp > 0);
if (!enemies.length) return;
const target = enemies[0];
// 空闲时寻找入口
if (hero.tunnelState === 'idle') {
const entrance = tunnels.find(t => t.type === 'entrance' && t.ownerId === hero.id);
if (entrance) {
const dx = entrance.x - hero.x, dy = entrance.y - hero.y;
const dist = Math.hypot(dx, dy) || 1;
if (dist > 5) { hero.x += (dx / dist) * hero.speed * dt; hero.y += (dy / dist) * hero.speed * dt; }
else { hero.x += (dx / dist) * hero.speed * 1.2 * dt; hero.y += (dy / dist) * hero.speed * 1.2 * dt; }
} else {
// 没有入口,正常追击敌人
const dx = target.x - hero.x, dy = target.y - hero.y;
const dist = Math.hypot(dx, dy) || 1;
if (dist > 1.5) { hero.x += (dx / dist) * hero.speed * 0.8 * dt; hero.y += (dy / dist) * hero.speed * 0.8 * dt; }
}
}
// 其他状态由 updateHollyleafTunnels 处理
constrainToArena(hero);
hero.facingRight = (target.x > hero.x);
// 普攻
if (hero.tunnelState === 'idle' && hero.attackTimer <= 0) {
const dx = target.x - hero.x, dy = target.y - hero.y;
const distGrid = Math.hypot(dx, dy) / CONFIG.GRID_SIZE;
if (distGrid <= 1.5) {
hero.attackTimer = hero.attackCooldown;
applyDamage(target, 45, hero, null);
slashEffects.push({ x: hero.x, y: hero.y, angle: Math.atan2(dy, dx), life: 0.3, maxLife: 0.3, color: '#2e8b57' });
}
}
}
function updateSunGod(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 < 4) { moveX = -dirX * hero.speed; moveY = -dirY * hero.speed; }
else if (distGrid > 7) { moveX = dirX * hero.speed * 0.5; moveY = dirY * hero.speed * 0.5; }
else { let t = (Math.sin(performance.now() / 1500) > 0) ? 1 : -1; moveX = -dirY * hero.speed * 0.5 * t; moveY = dirX * hero.speed * 0.5 * t; }
hero.x += (moveX + hero.wanderX * 0.4) * dt; hero.y += (moveY + hero.wanderY * 0.4) * dt;
constrainToArena(hero);
// 弹幕攻击
if (hero.attackTimer <= 0 && distGrid <= hero.attackRangeGrid) {
hero.attackTimer = hero.attackCooldown;
const texts = ['这样倒不像是明确的选择。', '哦,你那样做可真的是愚蠢透了,我有个更明智的办法。'];
const text = texts[Math.floor(Math.random() * texts.length)];
textBeams.push({ x: hero.x, y: hero.y, vx: dirX * 400, vy: dirY * 400, timer: 1.5, ownerId: hero.id, text });
}
// 近战平A
if (hero.meleeCooldown <= 0 && distGrid <= 1.5) {
hero.meleeCooldown = 1500;
applyDamage(target, 67, hero, null);
slashEffects.push({ x: hero.x, y: hero.y, angle: Math.atan2(dy, dx), life: 0.3, maxLife: 0.3, color: '#ffd700' });
}
}
function updateClover(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 < 5) { moveX = -dirX * hero.speed; moveY = -dirY * hero.speed; }
else if (distGrid > 8) { 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.laserCharging) return;
if (!hero.isReloading && hero.attackTimer <= 0 && distGrid <= hero.attackRangeGrid) {
if (hero.clipAmmo > 0) {
hero.clipAmmo--; hero.attackTimer = hero.attackCooldown;
// 计算命中:子弹速度快,敌人是否移动?
const enemyMoving = Math.hypot(target.wanderX, target.wanderY) > 10;
const missChance = enemyMoving ? 0.25 : 0.05;
const miss = Math.random() < missChance;
if (miss) {
// 未命中,攒怒气
hero.rage += 10;
addDamageText(hero.x, hero.y - 20, '+10怒气', '#ffd700');
if (hero.rage >= hero.maxRage) {
hero.laserCharging = true; hero.laserTimer = 800;
hero.rage = hero.maxRage;
addDamageText(hero.x, hero.y - 40, '激光蓄力!', '#00bfff');
}
} else {
spawnProjectile({ x: hero.x, y: hero.y, startX: hero.x, startY: hero.y, target, speed: 900, damage: hero.bulletDamage, type: 'bullet', ownerId: hero.id, color: '#00bfff' });
muzzleFlashes.push({ x: hero.x + dirX * 18, y: hero.y + dirY * 18, life: 0.05, maxLife: 0.05 });
}
if (hero.clipAmmo <= 0) { hero.isReloading = true; hero.reloadTimer = 1500; }
}
}
}
// ==========================================
// 🧱 碰撞
// ==========================================
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.isDashing || h2.isDashing || h1.isCharging || h2.isCharging) 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 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);
// 日神的减伤debuff
if (attacker && attacker.id === 'sungod') {
if (target.damageDebuffTimer > 0) amount *= 0.8;
if (target.slowTimer > 0) amount *= 0.6;
}
// 目标的减伤debuff
if (target.damageDebuffTimer > 0 && !isTrueDamage) amount *= 0.8;
if (target.slowTimer > 0 && !isTrueDamage) amount *= 0.6;
// 唐佳琪末影珍珠闪避
if (target.id === 'tangjiaqi' && target.pearls > 0 && !isTrueDamage) {
target.pearls--;
addDamageText(target.x, target.y - 30, '珍珠闪避!', '#ff00ff');
impactEffects.push({ x: target.x, y: target.y, color: '#ff00ff', life: 0.3, maxLife: 0.3 });
// 随机瞬移
const angle = Math.random() * Math.PI * 2;
const dist = 3 * CONFIG.GRID_SIZE;
target.x = Math.max(ARENA_X + target.radius, Math.min(ARENA_X + ARENA_WIDTH - target.radius, target.x + Math.cos(angle) * dist));
target.y = Math.max(ARENA_Y + target.radius, Math.min(ARENA_Y + ARENA_HEIGHT - target.radius, target.y + Math.sin(angle) * dist));
return { absorbed: true, killed: false };
}
// PLA受击反击
if (target.id === 'pla' && target.reactionCooldown <= 0 && !isTrueDamage) {
target.reactionCooldown = 2000;
let rand = Math.random();
if (rand < 0.3 && attacker && attacker.hp > 0) {
let dx = attacker.x - target.x, dy = attacker.y - target.y;
let dist = Math.hypot(dx, dy) || 1;
attacker.x += (dx / dist) * 3 * CONFIG.GRID_SIZE;
attacker.y += (dy / dist) * 3 * CONFIG.GRID_SIZE;
constrainToArena(attacker);
addDamageText(target.x, target.y - 40, '撞击反击!', '#4a5d23');
} else if (rand < 0.6) {
let enemies = heroEntities.filter(h => h.id !== target.id && h.hp > 0);
if (enemies.length > 0) {
let e = enemies[0];
let dx = target.x - e.x, dy = target.y - e.y;
let dist = Math.hypot(dx, dy) || 1;
target.x += (dx / dist) * 10 * CONFIG.GRID_SIZE;
target.y += (dy / dist) * 10 * CONFIG.GRID_SIZE;
constrainToArena(target);
addDamageText(target.x, target.y - 40, '战术后撤!', '#4a5d23');
}
}
}
// 校长卷款跑路
if (target.id === 'principal' && target.hp / target.maxHp > 0.3 && target.hp - amount <= target.maxHp * 0.3 && !target.usedRunaway) {
target.usedRunaway = true;
target.invulnTimer = 1500;
dummyShadows.push({ x: target.x, y: target.y, hp: 500, maxHp: 500, timer: 5, ownerId: target.id });
// 随机传送
const angle = Math.random() * Math.PI * 2;
target.x = ARENA_X + ARENA_WIDTH / 2 + Math.cos(angle) * ARENA_WIDTH * 0.35;
target.y = ARENA_Y + ARENA_HEIGHT / 2 + Math.sin(angle) * ARENA_HEIGHT * 0.35;
constrainToArena(target);
addDamageText(target.x, target.y - 40, '卷款跑路!', '#b8860b');
}
if (target.invulnTimer > 0 && !isTrueDamage) return { absorbed: true, killed: false };
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.id === 'principal' && 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'); }
else { if (!silent) addDamageText(target.x, target.y, Math.round(amount), '#b8860b'); }
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 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, debuff: false, ...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 === 'check') { hitTarget.damageDebuffTimer = Math.max(hitTarget.damageDebuffTimer, 5000); addDamageText(hitTarget.x, hitTarget.y - 20, '欠费!', '#b8860b'); }
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);
}
}
// 敌方子弹打人民
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 = dummyShadows.length - 1; i >= 0; i--) { dummyShadows[i].timer -= dt; if (dummyShadows[i].timer <= 0) dummyShadows.splice(i, 1); }
}
// ==========================================
// 🛠️ 工具
// ==========================================
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 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();
drawTunnels();
drawHolyCircles();
drawParents();
drawDummyShadows();
drawEffects();
drawGrenades();
drawProjectiles();
drawTextBeams();
drawLasers();
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 drawTunnels() {
for (const t of tunnels) {
ctx.beginPath();
ctx.ellipse(t.x, t.y, 18, 12, 0, 0, Math.PI * 2);
ctx.fillStyle = t.type === 'entrance' ? 'rgba(0,0,0,0.6)' : 'rgba(50,50,50,0.6)';
ctx.fill();
ctx.strokeStyle = t.type === 'entrance' ? '#2e8b57' : '#88cc88';
ctx.lineWidth = 3; ctx.stroke();
}
}
function drawHolyCircles() {
for (const c of holyCircles) {
ctx.beginPath(); ctx.arc(c.x, c.y, c.radius, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(255, 215, 0, 0.12)'; ctx.fill();
ctx.strokeStyle = 'rgba(255, 215, 0, 0.7)'; ctx.lineWidth = 4; ctx.stroke();
}
}
function drawParents() {
for (const p of parents) {
ctx.beginPath(); ctx.arc(p.x, p.y, p.radius, 0, Math.PI * 2);
ctx.fillStyle = '#ffffff'; ctx.fill();
ctx.strokeStyle = '#b8860b'; ctx.lineWidth = 2; ctx.stroke();
ctx.fillStyle = '#000'; ctx.font = '9px sans-serif'; ctx.textAlign = 'center';
ctx.fillText('家长', p.x, p.y + 3);
}
}
function drawDummyShadows() {
for (const s of dummyShadows) {
ctx.beginPath(); ctx.arc(s.x, s.y, 15, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(184, 134, 11, 0.5)'; ctx.fill();
ctx.strokeStyle = '#b8860b'; ctx.lineWidth = 3; ctx.stroke();
}
}
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 = '#ffffff'; ctx.fill(); ctx.strokeStyle = '#aaaaaa'; ctx.lineWidth = 2; ctx.stroke();
const barWidth = 30, barHeight = 4;
const barX = c.x - barWidth / 2, barY = c.y - c.radius - 10;
ctx.fillStyle = '#333'; ctx.fillRect(barX, barY, barWidth, barHeight);
ctx.fillStyle = c.hp / c.maxHp > 0.3 ? '#00ff00' : '#ff0000';
ctx.fillRect(barX, barY, barWidth * (c.hp / c.maxHp), barHeight);
ctx.fillStyle = '#000'; ctx.font = '10px sans-serif'; ctx.textAlign = 'center';
ctx.fillText('人民', c.x, c.y + 3);
}
}
function drawTextBeams() {
for (const b of textBeams) {
ctx.fillStyle = 'rgba(255, 215, 0, 0.25)';
ctx.fillRect(b.x - 90, b.y - 12, 180, 24);
ctx.strokeStyle = '#ffd700'; ctx.lineWidth = 1.5;
ctx.strokeRect(b.x - 90, b.y - 12, 180, 24);
ctx.fillStyle = '#000'; ctx.font = '10px sans-serif'; ctx.textAlign = 'center';
ctx.fillText(b.text, b.x, b.y + 4);
}
}
function drawLasers() {
for (const l of lasers) {
ctx.beginPath(); ctx.moveTo(l.x1, l.y1); ctx.lineTo(l.x2, l.y2);
ctx.strokeStyle = '#00bfff'; ctx.lineWidth = 10; ctx.stroke();
ctx.strokeStyle = '#ffffff'; ctx.lineWidth = 4; ctx.stroke();
}
}
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.type === 'check') {
ctx.fillStyle = '#b8860b'; ctx.fillRect(p.x - 12, p.y - 8, 24, 16);
ctx.strokeStyle = '#fff'; ctx.lineWidth = 1; ctx.strokeRect(p.x - 12, p.y - 8, 24, 16);
ctx.fillStyle = '#fff'; ctx.font = '8px sans-serif'; ctx.textAlign = 'center'; ctx.fillText('¥', p.x, p.y + 3);
} 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 === 'tangjiaqi' && entity.phase === 'divine') fillColor = '#ffffff';
if (entity.id === 'oldpastor' && entity.rageState === 'divine') fillColor = '#ffd700';
if (entity.slowTimer > 0) fillColor = '#8888ff';
ctx.save();
ctx.translate(entity.x, entity.y);
if (!entity.facingRight) ctx.scale(-1, 1);
ctx.scale(scale, scale);
if (entity.id === 'pla') {
ctx.beginPath(); ctx.arc(0, 0, entity.radius, 0, Math.PI * 2);
ctx.fillStyle = '#4a5d23'; ctx.fill();
ctx.strokeStyle = '#ffcc00'; ctx.lineWidth = 3; ctx.stroke();
ctx.fillStyle = '#ffcc00'; ctx.font = 'bold 12px sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.fillText('PLA', 0, 1);
} else {
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 = '#333'; ctx.fillRect(entity.radius - 2, -3, 25, 6); ctx.fillStyle = '#555'; ctx.fillRect(entity.radius + 5, -5, 8, 10); }
if (entity.id === 'hanshou' && entity.hasSword) {
ctx.fillStyle = entity.hanshouState === 1 ? '#ffcc00' : '#dddddd';
ctx.fillRect(entity.radius - 2, -3, 20, 6);
ctx.fillStyle = '#666'; ctx.fillRect(entity.radius - 6, -2, 6, 4);
ctx.beginPath(); ctx.moveTo(entity.radius + 18, -3); ctx.lineTo(entity.radius + 26, 0); ctx.lineTo(entity.radius + 18, 3); ctx.closePath();
ctx.fillStyle = entity.hanshouState === 1 ? '#ffcc00' : '#dddddd'; ctx.fill();
}
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') { 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 === '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 === '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 === 'oldpastor') { ctx.strokeStyle = '#fff'; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(0, -8); ctx.lineTo(0, 8); ctx.moveTo(-6, -2); ctx.lineTo(6, -2); ctx.stroke(); }
if (entity.id === 'hollyleaf') { ctx.fillStyle = '#2e8b57'; ctx.beginPath(); ctx.ellipse(-6, -6, 5, 3, Math.PI/4, 0, Math.PI*2); ctx.fill(); ctx.beginPath(); ctx.ellipse(6, -6, 5, 3, -Math.PI/4, 0, Math.PI*2); ctx.fill(); }
if (entity.id === 'tangjiaqi') { ctx.fillStyle = '#fff'; ctx.fillRect(-8, -8, 16, 16); ctx.fillStyle = '#ff00ff'; ctx.font = 'bold 12px sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText('♪', 0, 1); }
if (entity.id === 'principal') { ctx.fillStyle = '#000'; ctx.fillRect(-10, -6, 20, 12); ctx.fillStyle = '#b8860b'; ctx.font = 'bold 10px sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText('¥', 0, 1); }
if (entity.id === 'sungod') { ctx.fillStyle = '#ffd700'; ctx.font = 'bold 14px sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText('☀', 0, 1); }
if (entity.id === 'clover') { ctx.fillStyle = '#00bfff'; ctx.fillRect(entity.radius - 2, -2, 22, 4); ctx.fillStyle = '#fff'; ctx.font = 'bold 10px sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText('♣', 0, 1); }
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 === 'knight' && entity.shieldHp > 0) {
const shPercent = entity.shieldHp / entity.maxShieldHp;
ctx.beginPath(); ctx.arc(0, 0, entity.radius + 6, 0, Math.PI * 2);
ctx.strokeStyle = `rgba(255, 204, 0, ${0.3 + shPercent * 0.7})`; ctx.lineWidth = 3 + shPercent * 3; ctx.stroke();
}
if (entity.id === 'principal' && entity.shieldHp > 0) {
const shPercent = entity.shieldHp / entity.maxShieldHp;
ctx.beginPath(); ctx.arc(0, 0, entity.radius + 6, 0, Math.PI * 2);
ctx.strokeStyle = `rgba(184, 134, 11, ${0.3 + shPercent * 0.7})`; ctx.lineWidth = 3 + shPercent * 3; ctx.stroke();
}
if (entity.id !== 'dummy' && entity.id !== 'pla' && entity.id !== 'tangjiaqi' && entity.id !== 'principal' && entity.id !== 'sungod' && entity.id !== 'clover') {
ctx.beginPath(); ctx.arc(5, -4, 2.5, 0, Math.PI * 2); ctx.fillStyle = '#fff'; ctx.fill();
}
ctx.restore();
// 敌人身上的剑
if (entity.hasSwordInBack && entity.pinnedBy === 'wall') {
ctx.save(); ctx.translate(entity.x, entity.y); ctx.rotate(Math.PI / 4);
ctx.fillStyle = '#dddddd'; ctx.fillRect(-3, -20, 6, 30);
ctx.fillStyle = '#666'; ctx.fillRect(-6, -26, 12, 6); 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') { ctx.fillStyle = '#333'; ctx.fillRect(barX, barY + 7, barWidth, 3); ctx.fillStyle = '#ffcc00'; ctx.fillRect(barX, barY + 7, barWidth * (entity.dr / 76), 3); }
if (entity.id === 'deepseek' && entity.shieldHp > 0) { ctx.fillStyle = '#333'; ctx.fillRect(barX, barY + 7, barWidth, 3); ctx.fillStyle = '#4488ff'; ctx.fillRect(barX, barY + 7, barWidth * (entity.shieldHp / entity.maxShieldHp), 3); }
if (entity.id === 'werewolf' && !entity.enraged) { ctx.fillStyle = '#333'; ctx.fillRect(barX, barY + 7, barWidth, 3); ctx.fillStyle = '#ff00aa'; ctx.fillRect(barX, barY + 7, barWidth * Math.min(1, entity.battleTimer / 20), 3); }
if (entity.id === 'newton') { ctx.fillStyle = '#333'; ctx.fillRect(barX, barY + 7, barWidth, 3); ctx.fillStyle = '#00ffff'; ctx.fillRect(barX, barY + 7, barWidth * (1 - entity.prismCooldown / 5000), 3); ctx.fillStyle = '#ff4444'; ctx.fillRect(barX, barY + 11, barWidth * (1 - entity.appleTimer / 30000), 3); }
if (entity.id === 'knight') { ctx.fillStyle = '#333'; ctx.fillRect(barX, barY + 7, barWidth, 3); ctx.fillStyle = '#ffcc00'; ctx.fillRect(barX, barY + 7, barWidth * (entity.shieldHp / entity.maxShieldHp), 3); }
if (entity.id === 'pla') { ctx.fillStyle = '#333'; ctx.fillRect(barX, barY + 7, barWidth, 3); ctx.fillStyle = '#ffff00'; ctx.fillRect(barX, barY + 7, barWidth * (entity.clipAmmo / entity.maxClipAmmo), 3); if (entity.damageBuffTimer > 0) { ctx.fillStyle = '#ffcc00'; ctx.fillRect(barX, barY + 11, barWidth * Math.min(1, entity.damageBuffTimer / 30000), 3); } }
if (entity.id === 'tangjiaqi') { ctx.fillStyle = '#333'; ctx.fillRect(barX, barY + 7, barWidth, 3); if (entity.phase === 'endure') { ctx.fillStyle = '#ff00ff'; ctx.fillRect(barX, barY + 7, barWidth * (1 - entity.phaseTimer / 30000), 3); } else { ctx.fillStyle = '#ffffff'; ctx.fillRect(barX, barY + 7, barWidth * (entity.divineTimer / 10000), 3); } ctx.fillStyle = '#ffcc00'; ctx.fillRect(barX, barY + 11, barWidth * (entity.pearls / 10), 3); }
if (entity.id === 'principal') { ctx.fillStyle = '#333'; ctx.fillRect(barX, barY + 7, barWidth, 3); ctx.fillStyle = '#b8860b'; ctx.fillRect(barX, barY + 7, barWidth * (entity.shieldHp / entity.maxShieldHp), 3); }
if (entity.idGame Source: 电子斗蛐蛐 - 终极全英雄版
Creator: EpicCoder88
Libraries: none
Complexity: complex (1970 lines, 97.2 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-muh29gj0" to link back to the original. Then publish at arcadelab.ai/publish.