电子斗蛐蛐 - 学科大战(法拉第&霍金版)
by EpicCoder881513 lines64.9 KB
<!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; }
#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;
const WANDER_SPEED = 0.6;
// ==========================================
// 📖 英雄图鉴
// ==========================================
const HERO_LIST = [
{ id: 'newton', name: '牛顿', color: '#dddddd', maxHp: 1100, speed: 120 },
{ id: 'lavoisier', name: '拉瓦锡', color: '#ff8c00', maxHp: 950, speed: 115 },
{ id: 'mendel', name: '孟德尔', color: '#33cc33', maxHp: 850, speed: 105 },
{ id: 'taylor', name: '泰勒', color: '#3366ff', maxHp: 800, speed: 110 },
{ id: 'starling', name: '斯他林', color: '#ff44cc', maxHp: 900, speed: 115 },
{ id: 'faraday', name: '法拉第', color: '#ffd700', maxHp: 1000, speed: 110 },
{ id: 'hawking', name: '霍金', color: '#8a2be2', maxHp: 900, 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 impactEffects = [];
let newtonPrisms = [];
let apples = [];
let oxygenFields = [];
let massParticles = [];
let peaShooters = [];
let hormonePools = [];
let geneSeeds = [];
let wormholes = [];
let faradayCages = [];
let radiationZones = [];
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', () => {
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.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 = []; impactEffects = [];
newtonPrisms = []; apples = []; oxygenFields = [];
massParticles = []; peaShooters = []; hormonePools = []; geneSeeds = [];
wormholes = []; faradayCages = []; radiationZones = [];
}
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 data = HERO_MAP[id];
const hero = {
id: id, name: data.name, color: data.color,
hp: data.maxHp, maxHp: data.maxHp,
radius: 20, speed: data.speed,
x: index === 0 ? ARENA_X + 200 : ARENA_X + ARENA_WIDTH - 200,
y: ARENA_Y + ARENA_HEIGHT / 2,
facingRight: index === 0,
stunTimer: 0, wallPushTimer: 0, wallPushX: 0, wallPushY: 0,
wanderAngle: Math.random() * Math.PI * 2,
wanderTimer: 0, wanderX: 0, wanderY: 0,
attackTimer: 0, attackAnimTimer: 0, slowTimer: 0,
attackPower: 1, dr: 0,
// 通用状态挂载
chargeStacks: 0, chargeTimer: 0,
radiationStacks: 0, radiationTimer: 0
};
// 牛顿
if (id === 'newton') {
hero.prismCooldown = 5000;
hero.appleTimer = 30000;
hero.satellites = [];
hero.satelliteTimer = 5000;
}
// 拉瓦锡
else if (id === 'lavoisier') {
hero.oxygenCooldown = 6000;
hero.preciseStacks = 0;
hero.ultCooldown = 12000;
}
// 孟德尔
else if (id === 'mendel') {
hero.attackCooldown = 2000;
hero.peaTimer = 8000;
hero.geneSeed = 0;
}
// 泰勒
else if (id === 'taylor') {
hero.attackCooldown = 1500;
hero.taylorMark = 0;
hero.taylorMarkTimer = 0;
hero.ultCooldown = 0;
}
// 斯他林
else if (id === 'starling') {
hero.attackCooldown = 2000;
hero.hormoneStormCooldown = 15000;
hero.overdriveTimer = 0;
}
// 法拉第
else if (id === 'faraday') {
hero.attackCooldown = 1800;
hero.shieldCooldown = 8000;
hero.shieldTimer = 0;
hero.cageCooldown = 15000;
}
// 霍金
else if (id === 'hawking') {
hero.attackCooldown = 2000;
hero.wheelchairCooldown = 8000;
hero.wheelchairTimer = 0;
hero.wheelchairDirX = 0;
hero.wheelchairDirY = 0;
hero.wheelchairDamageAccum = 0;
hero.wheelchairHitMap = {};
hero.wormholeTimer = 5000;
hero.radiationCooldown = 18000;
}
return hero;
}
// ==========================================
// 🔄 主循环
// ==========================================
function gameLoop(timestamp) {
if (gameState !== 'playing') return;
try {
const dt = Math.min((timestamp - lastTime) / 1000, MAX_DT);
if (!isFinite(dt) || dt <= 0) { rafId = requestAnimationFrame(gameLoop); return; }
lastTime = timestamp;
update(dt);
if (gameState === 'playing') { updateCamera(dt); render(); }
} catch (e) { console.error("Game Loop Error:", e); }
rafId = requestAnimationFrame(gameLoop);
}
// ==========================================
// 📷 摄像机
// ==========================================
function updateCamera(dt) {
if (heroEntities.length < 2) return;
const a = heroEntities[0], b = heroEntities[1];
let cx = (a.x + b.x) / 2, cy = (a.y + b.y) / 2;
if (!isFinite(cx)) cx = MAP_WIDTH / 2;
if (!isFinite(cy)) cy = MAP_HEIGHT / 2;
const targetX = Math.max(0, Math.min(MAP_WIDTH - canvas.width, cx - canvas.width / 2));
const targetY = Math.max(0, Math.min(MAP_HEIGHT - canvas.height, cy - canvas.height / 2));
const k = Math.min(1, 8 * dt);
camera.x += (targetX - camera.x) * k;
camera.y += (targetY - camera.y) * k;
}
// ==========================================
// 🏃 通用逻辑更新
// ==========================================
function update(dt) {
const alive = heroEntities.filter(h => h.hp > 0);
if (alive.length <= 1 && heroEntities.length >= 2) {
gameState = 'gameover';
showGameOver(alive);
return;
}
for (const h of heroEntities) {
if (h.hp <= 0) continue;
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.slowTimer > 0) { h.slowTimer -= dt * 1000; if (h.slowTimer <= 0) h.speed = HERO_MAP[h.id].speed; }
if (h.wallPushTimer > 0) { h.wallPushTimer -= dt; h.x += h.wallPushX * h.speed * dt; h.y += h.wallPushY * h.speed * dt; }
if (h.taylorMarkTimer > 0) { h.taylorMarkTimer -= dt; if (h.taylorMarkTimer <= 0) h.taylorMark = 0; }
if (h.hormoneTimer > 0) { h.hormoneTimer -= dt; if (h.hormoneTimer <= 0) { h.hormoneStacks = 0; h.attackPower = 1; } }
if (h.overdriveTimer > 0) { h.overdriveTimer -= dt * 1000; if (h.overdriveTimer <= 0 && h.id === 'starling') h.speed = HERO_MAP[h.id].speed; }
if (h.id === 'starling' && h.overdriveTimer <= 0) {
h.speed = (h.hp < h.maxHp * 0.5) ? HERO_MAP[h.id].speed * 1.2 : HERO_MAP[h.id].speed;
}
// 法拉第护盾计时
if (h.id === 'faraday') {
if (h.shieldTimer > 0) h.shieldTimer -= dt * 1000;
if (h.shieldCooldown > 0) h.shieldCooldown -= dt * 1000;
if (h.shieldCooldown <= 0 && h.shieldTimer <= 0) {
h.shieldCooldown = 8000;
h.shieldTimer = 5000;
addDamageText(h.x, h.y - 50, '⚡ 力场护盾!', '#ffd700');
}
if (h.cageCooldown > 0) h.cageCooldown -= dt * 1000;
}
// 霍金计时
if (h.id === 'hawking') {
if (h.wheelchairCooldown > 0) h.wheelchairCooldown -= dt * 1000;
if (h.wormholeTimer > 0) h.wormholeTimer -= dt * 1000;
if (h.radiationCooldown > 0) h.radiationCooldown -= dt * 1000;
// 低血量触发霍金辐射
if (h.hp < h.maxHp * 0.5 && h.radiationCooldown <= 0) {
h.radiationCooldown = 18000;
radiationZones.push({ x: h.x, y: h.y, radius: 120, life: 4, ownerId: h.id, tickAccum: 0 });
addDamageText(h.x, h.y - 50, '🕳️ 霍金辐射!', '#8a2be2');
}
}
// 电荷标记过期
if (h.chargeTimer > 0) {
h.chargeTimer -= dt;
if (h.chargeTimer <= 0) h.chargeStacks = 0;
}
// 辐射侵蚀过期
if (h.radiationTimer > 0) {
h.radiationTimer -= dt;
if (h.radiationTimer <= 0) h.radiationStacks = 0;
}
// 辐射侵蚀持续伤害
if (h.radiationStacks > 0) {
applyDamage(h, h.radiationStacks * 2 * dt, null, null, true, true);
}
}
// 逐英雄 AI
for (const hero of heroEntities) {
if (hero.hp <= 0 || hero.stunTimer > 0 || hero.wallPushTimer > 0) continue;
// 霍金冲锋时使用专门的逻辑
if (hero.id === 'hawking' && hero.wheelchairTimer > 0) {
updateWheelchairCharge(hero, dt);
continue;
}
if (hero.id === 'newton') updateNewton(hero, dt);
else if (hero.id === 'lavoisier') updateLavoisier(hero, dt);
else if (hero.id === 'mendel') updateMendel(hero, dt);
else if (hero.id === 'taylor') updateTaylor(hero, dt);
else if (hero.id === 'starling') updateStarling(hero, dt);
else if (hero.id === 'faraday') updateFaraday(hero, dt);
else if (hero.id === 'hawking') updateHawking(hero, dt);
}
// 全局实体
updatePrisms(dt);
updateSatellites(dt);
updateOxygenFields(dt);
updateMassParticles(dt);
updatePeaShooters(dt);
updateHormonePools(dt);
updateGeneSeeds(dt);
updateWormholes(dt);
updateFaradayCages(dt);
updateRadiationZones(dt);
resolveCollisions();
updateProjectiles(dt);
updateEffects(dt);
}
// ==========================================
// 🎲 随机游走
// ==========================================
function updateWander(h, dt) {
h.wanderTimer -= dt;
if (h.wanderTimer <= 0) {
h.wanderTimer = 2.5 + Math.random() * 3.0;
h.wanderAngle += (Math.random() - 0.5) * Math.PI * 2.0;
}
h.wanderX = Math.cos(h.wanderAngle) * h.speed * WANDER_SPEED;
h.wanderY = Math.sin(h.wanderAngle) * h.speed * WANDER_SPEED;
}
// ==========================================
// 🔺 牛顿 AI
// ==========================================
function updateNewton(hero, dt) {
hero.prismCooldown -= dt * 1000;
hero.appleTimer -= dt * 1000;
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 {
const tangent = Math.sin(performance.now() / 2000) > 0 ? 1 : -1;
moveX = -dirY * tangent * hero.speed * 0.8;
moveY = dirX * tangent * hero.speed * 0.8;
}
hero.x += (moveX + hero.wanderX) * dt;
hero.y += (moveY + hero.wanderY) * dt;
constrainToArena(hero);
if (hero.prismCooldown <= 0) {
hero.prismCooldown = 5000;
const px = hero.x + (target.x - hero.x) * 0.5;
const py = hero.y + (target.y - hero.y) * 0.5;
const baseAngle = Math.atan2(target.y - py, target.x - px);
newtonPrisms.push({ ownerId: hero.id, x: px, y: py, timer: 10000, baseAngle, angle: 0, sweepDir: 1 });
addDamageText(hero.x, hero.y - 40, '🔺 三棱镜!', '#00ffff');
}
if (hero.appleTimer <= 0) {
hero.appleTimer = 30000;
for (let i = 0; i < 10; i++) {
if (apples.length >= 20) break;
const ax = ARENA_X + Math.random() * ARENA_WIDTH;
const 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.push({ x: ax, y: ay, radius: 8 });
}
addDamageText(hero.x, hero.y - 40, '🍎 苹果雨!', '#ff4444');
}
for (let i = apples.length - 1; i >= 0; i--) {
const ap = apples[i];
for (const h of heroEntities) {
if (h.hp <= 0) continue;
if (Math.hypot(h.x - ap.x, h.y - ap.y) < h.radius + ap.radius) {
const heal = h.id === 'newton' ? 50 : 10;
h.hp = Math.min(h.maxHp, h.hp + heal);
addDamageText(h.x, h.y - 20, '+' + heal, '#00ff00');
apples.splice(i, 1); break;
}
}
}
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 });
}
}
const orbitRadius = 2 * CONFIG.GRID_SIZE;
const orbitSpeed = 2.5;
for (let i = hero.satellites.length - 1; i >= 0; i--) {
const sat = hero.satellites[i];
sat.angle += orbitSpeed * dt;
const satX = hero.x + Math.cos(sat.angle) * orbitRadius;
const 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);
}
}
}
// ==========================================
// 🔥 拉瓦锡 AI
// ==========================================
function updateLavoisier(hero, dt) {
hero.oxygenCooldown -= dt * 1000;
hero.ultCooldown -= dt * 1000;
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.6; moveY = dirY * hero.speed * 0.6; }
else {
const tangent = Math.sin(performance.now() / 2500) > 0 ? 1 : -1;
moveX = -dirY * tangent * hero.speed * 0.7;
moveY = dirX * tangent * hero.speed * 0.7;
}
hero.x += (moveX + hero.wanderX) * dt;
hero.y += (moveY + hero.wanderY) * dt;
constrainToArena(hero);
if (hero.oxygenCooldown <= 0) {
hero.oxygenCooldown = 6000;
spawnProjectile({ x: hero.x, y: hero.y, startX: hero.x, startY: hero.y, target, speed: 500, damage: 20, type: 'oxygen', ownerId: hero.id });
}
if (hero.preciseStacks >= 3 && hero.ultCooldown <= 0) {
hero.preciseStacks = 0;
hero.ultCooldown = 12000;
applyDamage(target, 120, hero, null, false, false);
addImpact(target.x, target.y, '#4488ff', 1.0);
addDamageText(target.x, target.y - 40, '💥 炼金炸药!', '#4488ff');
const kdx = target.x - hero.x, kdy = target.y - hero.y, kd = Math.hypot(kdx, kdy) || 1;
target.x += (kdx / kd) * CONFIG.GRID_SIZE;
target.y += (kdy / kd) * CONFIG.GRID_SIZE;
constrainToArena(target);
}
}
// ==========================================
// 🌱 孟德尔 AI
// ==========================================
function updateMendel(hero, dt) {
hero.peaTimer -= dt * 1000;
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 < 2) { moveX = -dirX * hero.speed; moveY = -dirY * hero.speed; }
else if (distGrid > 4) { moveX = dirX * hero.speed * 0.5; moveY = dirY * hero.speed * 0.5; }
else {
const tangent = Math.sin(performance.now() / 2200) > 0 ? 1 : -1;
moveX = -dirY * tangent * hero.speed * 0.6;
moveY = dirX * tangent * hero.speed * 0.6;
}
hero.x += (moveX + hero.wanderX) * dt;
hero.y += (moveY + hero.wanderY) * dt;
constrainToArena(hero);
if (hero.attackTimer <= 0) {
hero.attackTimer = 2000;
hero.attackAnimTimer = 300;
if (hero.geneSeed) {
hero.geneSeed = 0;
spawnProjectile({ x: hero.x, y: hero.y, startX: hero.x, startY: hero.y, target, speed: 900, damage: 50, type: 'superpea', ownerId: hero.id, knockback: 30 });
addDamageText(hero.x, hero.y - 40, '超级豌豆!', '#ffff00');
} else {
spawnProjectile({ x: hero.x, y: hero.y, startX: hero.x, startY: hero.y, target, speed: 700, damage: 15, type: 'pea', ownerId: hero.id });
}
}
if (hero.peaTimer <= 0) {
hero.peaTimer = 8000;
if (peaShooters.filter(p => p.ownerId === hero.id).length < 4) {
peaShooters.push({ ownerId: hero.id, x: hero.x + (Math.random() - 0.5) * 100, y: hero.y + (Math.random() - 0.5) * 100, hp: 50, maxHp: 50, attackTimer: 0 });
}
}
}
// ==========================================
// ∑ 泰勒 AI
// ==========================================
function updateTaylor(hero, dt) {
hero.ultCooldown -= dt * 1000;
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 > 7) { moveX = dirX * hero.speed * 0.6; moveY = dirY * hero.speed * 0.6; }
else {
const tangent = Math.sin(performance.now() / 1800) > 0 ? 1 : -1;
moveX = -dirY * tangent * hero.speed * 0.6;
moveY = dirX * tangent * hero.speed * 0.6;
}
hero.x += (moveX + hero.wanderX) * dt;
hero.y += (moveY + hero.wanderY) * dt;
constrainToArena(hero);
if (hero.attackTimer <= 0) {
hero.attackTimer = 1500;
hero.attackAnimTimer = 300;
spawnProjectile({ x: hero.x, y: hero.y, startX: hero.x, startY: hero.y, target, speed: 900, damage: 5, type: 'taylor', ownerId: hero.id });
}
}
// ==========================================
// 🧪 斯他林 AI
// ==========================================
function updateStarling(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 > 6) { moveX = dirX * hero.speed * 0.6; moveY = dirY * hero.speed * 0.6; }
else {
const tangent = Math.sin(performance.now() / 2100) > 0 ? 1 : -1;
moveX = -dirY * tangent * hero.speed * 0.7;
moveY = dirX * tangent * hero.speed * 0.7;
}
hero.x += (moveX + hero.wanderX) * dt;
hero.y += (moveY + hero.wanderY) * dt;
constrainToArena(hero);
if (hero.attackTimer <= 0) {
hero.attackTimer = 2000;
hero.attackAnimTimer = 300;
spawnProjectile({ x: hero.x, y: hero.y, startX: hero.x, startY: hero.y, target, speed: 800, damage: 15, type: 'hormone', ownerId: hero.id });
}
}
// ==========================================
// ⚡ 法拉第 AI
// ==========================================
function updateFaraday(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 > 6) { moveX = dirX * hero.speed * 0.6; moveY = dirY * hero.speed * 0.6; }
else {
const tangent = Math.sin(performance.now() / 2000) > 0 ? 1 : -1;
moveX = -dirY * tangent * hero.speed * 0.7;
moveY = dirX * tangent * hero.speed * 0.7;
}
hero.x += (moveX + hero.wanderX) * dt;
hero.y += (moveY + hero.wanderY) * dt;
constrainToArena(hero);
// 普攻:电磁感应
if (hero.attackTimer <= 0) {
hero.attackTimer = 1800;
hero.attackAnimTimer = 300;
spawnProjectile({ x: hero.x, y: hero.y, startX: hero.x, startY: hero.y, target, speed: 850, damage: 12, type: 'spark', ownerId: hero.id });
}
// 大招:法拉第笼(电荷满4层触发)
if (target.chargeStacks >= 4 && hero.cageCooldown <= 0) {
hero.cageCooldown = 15000;
faradayCages.push({ x: target.x, y: target.y, radius: 120, life: 5, ownerId: hero.id, tickAccum: 0, shockAccum: 0 });
addDamageText(hero.x, hero.y - 50, '⚡ 法拉第笼!', '#ffd700');
target.chargeStacks = 0;
target.chargeTimer = 0;
}
}
// ==========================================
// ♿ 霍金 AI
// ==========================================
function updateHawking(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 > 5) { moveX = dirX * hero.speed * 0.6; moveY = dirY * hero.speed * 0.6; }
else {
const tangent = Math.sin(performance.now() / 2300) > 0 ? 1 : -1;
moveX = -dirY * tangent * hero.speed * 0.6;
moveY = dirX * tangent * hero.speed * 0.6;
}
hero.x += (moveX + hero.wanderX) * dt;
hero.y += (moveY + hero.wanderY) * dt;
constrainToArena(hero);
// 轮椅冲锋触发
if (hero.wheelchairCooldown <= 0 && distGrid > 1.5) {
hero.wheelchairCooldown = 8000;
hero.wheelchairTimer = 1.0;
hero.wheelchairDirX = dirX;
hero.wheelchairDirY = dirY;
hero.wheelchairDamageAccum = 0;
hero.wheelchairHitMap = {};
addDamageText(hero.x, hero.y - 50, '♿ 轮椅冲锋!', '#8a2be2');
}
// 虫洞生成
if (hero.wormholeTimer <= 0) {
hero.wormholeTimer = 5000;
if (Math.random() < 0.3) {
const entryAngle = Math.random() * Math.PI * 2;
const entryDist = CONFIG.GRID_SIZE * 1.5;
const entryX = hero.x + Math.cos(entryAngle) * entryDist;
const entryY = hero.y + Math.sin(entryAngle) * entryDist;
// 出口:距离入口至少 8 格
let exitX, exitY, tries = 0;
do {
exitX = ARENA_X + Math.random() * ARENA_WIDTH;
exitY = ARENA_Y + Math.random() * ARENA_HEIGHT;
tries++;
} while (Math.hypot(exitX - entryX, exitY - entryY) < CONFIG.GRID_SIZE * 8 && tries < 20);
wormholes.push({
entryX: clampX(entryX), entryY: clampY(entryY),
exitX: clampX(exitX), exitY: clampY(exitY),
life: 15, used: false
});
addDamageText(hero.x, hero.y - 50, '🌀 虫洞生成!', '#8a2be2');
}
}
// 霍金辐射已在通用循环中自动触发,不在此处理
}
function clampX(x) { return Math.max(ARENA_X + 20, Math.min(ARENA_X + ARENA_WIDTH - 20, x)); }
function clampY(y) { return Math.max(ARENA_Y + 20, Math.min(ARENA_Y + ARENA_HEIGHT - 20, y)); }
// ==========================================
// ♿ 轮椅冲锋 - 独立位移逻辑
// ==========================================
function updateWheelchairCharge(hero, dt) {
hero.wheelchairTimer -= dt;
// 冲锋位移(固定 450 像素/秒)
hero.x += hero.wheelchairDirX * 450 * dt;
hero.y += hero.wheelchairDirY * 450 * dt;
constrainToArena(hero);
// 碾压伤害:每 0.1 秒对同一敌人造成 10 点
hero.wheelchairDamageAccum += dt;
while (hero.wheelchairDamageAccum >= 0.1) {
hero.wheelchairDamageAccum -= 0.1;
for (const h of heroEntities) {
if (h.id === hero.id || h.hp <= 0) continue;
if (Math.hypot(h.x - hero.x, h.y - hero.y) < h.radius + hero.radius + 8) {
applyDamage(h, 10, hero, null, false, false);
if (hero.wheelchairHitMap[h.id] === undefined) {
hero.wheelchairHitMap[h.id] = true;
h.stunTimer = 500;
addDamageText(h.x, h.y - 30, '碾压!', '#ff00ff');
}
}
}
}
// 冲锋结束
if (hero.wheelchairTimer <= 0) {
hero.wheelchairTimer = 0;
addImpact(hero.x, hero.y, '#8a2be2', 0.6);
}
}
// ==========================================
// 🌀 虫洞更新
// ==========================================
function updateWormholes(dt) {
for (let i = wormholes.length - 1; i >= 0; i--) {
const w = wormholes[i];
w.life -= dt;
if (w.life <= 0 || w.used) { wormholes.splice(i, 1); continue; }
for (const h of heroEntities) {
if (h.hp <= 0) continue;
// 踩到入口
if (Math.hypot(h.x - w.entryX, h.y - w.entryY) < h.radius + 12) {
h.x = w.exitX; h.y = w.exitY;
w.used = true;
addImpact(w.entryX, w.entryY, '#8a2be2', 0.8);
addImpact(w.exitX, w.exitY, '#8a2be2', 0.8);
addDamageText(h.x, h.y - 30, '🌀 传送!', '#8a2be2');
break;
}
// 踩到出口
if (Math.hypot(h.x - w.exitX, h.y - w.exitY) < h.radius + 12) {
h.x = w.entryX; h.y = w.entryY;
w.used = true;
addImpact(w.entryX, w.entryY, '#8a2be2', 0.8);
addImpact(w.exitX, w.exitY, '#8a2be2', 0.8);
addDamageText(h.x, h.y - 30, '🌀 传送!', '#8a2be2');
break;
}
}
}
}
// ==========================================
// ⚡ 法拉第笼更新
// ==========================================
function updateFaradayCages(dt) {
for (let i = faradayCages.length - 1; i >= 0; i--) {
const cage = faradayCages[i];
cage.life -= dt;
if (cage.life <= 0) { faradayCages.splice(i, 1); continue; }
cage.tickAccum += dt;
cage.shockAccum += dt;
for (const h of heroEntities) {
if (h.hp <= 0 || h.id === cage.ownerId) continue;
const dist = Math.hypot(h.x - cage.x, h.y - cage.y);
if (dist < cage.radius) {
// 持续伤害
if (cage.tickAccum >= 0.5) {
applyDamage(h, 25 * dt, null, null, false, true);
}
// 禁锢:限制离开范围
if (dist > cage.radius - h.radius) {
const dirX = (cage.x - h.x) / (dist || 1);
const dirY = (cage.y - h.y) / (dist || 1);
h.x += dirX * h.speed * dt * 0.8;
h.y += dirY * h.speed * dt * 0.8;
}
}
}
// 每秒随机电击
if (cage.shockAccum >= 1) {
cage.shockAccum = 0;
for (const h of heroEntities) {
if (h.hp <= 0 || h.id === cage.ownerId) continue;
if (Math.hypot(h.x - cage.x, h.y - cage.y) < cage.radius) {
applyDamage(h, 15, null, null, true, false);
addDamageText(h.x, h.y - 30, '⚡ 15', '#ffd700');
}
}
}
}
}
// ==========================================
// 🕳️ 霍金辐射区域
// ==========================================
function updateRadiationZones(dt) {
for (let i = radiationZones.length - 1; i >= 0; i--) {
const zone = radiationZones[i];
zone.life -= dt;
if (zone.life <= 0) { radiationZones.splice(i, 1); continue; }
const owner = heroEntities.find(h => h.id === zone.ownerId);
if (!owner || owner.hp <= 0) { radiationZones.splice(i, 1); continue; }
for (const h of heroEntities) {
if (h.hp <= 0) continue;
if (Math.hypot(h.x - zone.x, h.y - zone.y) < zone.radius) {
if (h.id === zone.ownerId) {
// 自愈
owner.hp = Math.min(owner.maxHp, owner.hp + 20 * dt);
} else {
// 真伤
applyDamage(h, 15 * dt, owner, null, true, true);
}
}
}
}
}
// ==========================================
// 🔺 三棱镜更新
// ==========================================
function updatePrisms(dt) {
for (let i = newtonPrisms.length - 1; i >= 0; i--) {
const 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 (const h of heroEntities) {
if (h.id === 'newton' || h.hp <= 0) continue;
const ddx = h.x - p.x, ddy = h.y - p.y;
const dist = Math.hypot(ddx, ddy);
if (dist < 240) {
const angleToTarget = Math.atan2(ddy, ddx);
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, null, null, true, true);
}
}
}
}
}
function updateSatellites(dt) {}
function updateOxygenFields(dt) {
for (let i = oxygenFields.length - 1; i >= 0; i--) {
const f = oxygenFields[i];
f.life -= dt;
if (f.life <= 0) { oxygenFields.splice(i, 1); continue; }
for (const h of heroEntities) {
if (h.hp <= 0 || h.id === f.ownerId) continue;
if (Math.hypot(h.x - f.x, h.y - f.y) < f.radius) {
applyDamage(h, 15 * dt, null, null, false, true);
h.slowTimer = 500;
h.speed = HERO_MAP[h.id].speed * 0.7;
}
}
}
}
function updateMassParticles(dt) {
for (let i = massParticles.length - 1; i >= 0; i--) {
const p = massParticles[i];
p.life -= dt;
if (p.life <= 0) { massParticles.splice(i, 1); continue; }
const lavoisier = heroEntities.find(h => h.id === 'lavoisier' && h.hp > 0);
if (lavoisier && Math.hypot(lavoisier.x - p.x, lavoisier.y - p.y) < lavoisier.radius + 15) {
lavoisier.hp = Math.min(lavoisier.maxHp, lavoisier.hp + 30);
lavoisier.preciseStacks = Math.min(3, lavoisier.preciseStacks + 1);
massParticles.splice(i, 1);
addDamageText(lavoisier.x, lavoisier.y - 20, '+30', '#00ff00');
}
}
}
function updatePeaShooters(dt) {
for (let i = peaShooters.length - 1; i >= 0; i--) {
const p = peaShooters[i];
p.hp -= dt * 5;
if (p.hp <= 0) {
peaShooters.splice(i, 1);
const mendel = heroEntities.find(h => h.id === 'mendel' && h.hp > 0);
if (mendel) {
if (Math.random() < 0.5) {
mendel.hp = Math.min(mendel.maxHp, mendel.hp + 100);
addDamageText(mendel.x, mendel.y - 20, '显性纯合 +100', '#00ff00');
} else {
geneSeeds.push({ x: p.x, y: p.y, radius: 10 });
}
}
continue;
}
p.attackTimer += dt * 1000;
if (p.attackTimer >= 1500) {
p.attackTimer = 0;
let target = null, minDist = Infinity;
for (const h of heroEntities) {
if (h.hp <= 0 || h.id === p.ownerId) continue;
const d = Math.hypot(h.x - p.x, h.y - p.y);
if (d < minDist) { minDist = d; target = h; }
}
if (target) spawnProjectile({ x: p.x, y: p.y, startX: p.x, startY: p.y, target, speed: 800, damage: 10, type: 'pea', ownerId: p.ownerId });
}
}
}
function updateHormonePools(dt) {
for (let i = hormonePools.length - 1; i >= 0; i--) {
const p = hormonePools[i];
p.life -= dt;
if (p.life <= 0) { hormonePools.splice(i, 1); continue; }
const starling = heroEntities.find(h => h.id === 'starling' && h.hp > 0);
if (starling && Math.hypot(starling.x - p.x, starling.y - p.y) < 40) {
starling.hp = Math.min(starling.maxHp, starling.hp + 30 * dt);
}
}
}
function updateGeneSeeds(dt) {
for (let i = geneSeeds.length - 1; i >= 0; i--) {
const g = geneSeeds[i];
const mendel = heroEntities.find(h => h.id === 'mendel' && h.hp > 0);
if (mendel && Math.hypot(mendel.x - g.x, mendel.y - g.y) < mendel.radius + 15) {
mendel.geneSeed = 1;
geneSeeds.splice(i, 1);
}
}
}
// ==========================================
// 🚀 弹道更新
// ==========================================
function updateProjectiles(dt) {
for (let i = projectiles.length - 1; i >= 0; i--) {
const p = projectiles[i];
if (p.target && p.target.hp <= 0) {
if (p.type === 'oxygen') oxygenFields.push({ x: p.target.x, y: p.target.y, radius: 120, life: 5, ownerId: p.ownerId });
projectiles.splice(i, 1); continue;
}
let hitTarget = null;
if (p.target) {
const t = p.target, pdx = t.x - p.x, pdy = t.y - p.y;
const pDist = Math.hypot(pdx, pdy) || 1;
if (pDist < t.radius + 10) 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;
if (p.type === 'oxygen') {
applyDamage(hitTarget, p.damage, attacker, p);
oxygenFields.push({ x: p.x, y: p.y, radius: 120, life: 5, ownerId: p.ownerId });
addImpact(p.x, p.y, '#ff8c00', 0.5);
} else if (p.type === 'pea') {
applyDamage(hitTarget, p.damage, attacker, p);
if (Math.random() < 0.75) addDamageText(hitTarget.x, hitTarget.y, '显性!', '#33cc33');
else { hitTarget.slowTimer = 2000; hitTarget.speed = HERO_MAP[hitTarget.id].speed * 0.7; addDamageText(hitTarget.x, hitTarget.y, '隐性!', '#33cc33'); }
} else if (p.type === 'superpea') {
applyDamage(hitTarget, p.damage, attacker, p);
addImpact(hitTarget.x, hitTarget.y, '#ffff00', 0.8);
} else if (p.type === 'taylor') {
applyDamage(hitTarget, p.damage, attacker, p);
const kdx = hitTarget.x - p.x, kdy = hitTarget.y - p.y, kd = Math.hypot(kdx, kdy) || 1;
hitTarget.x += (kdx / kd) * 8; hitTarget.y += (kdy / kd) * 8;
constrainToArena(hitTarget);
hitTarget.taylorMark = Math.min(5, (hitTarget.taylorMark || 0) + 1);
hitTarget.taylorMarkTimer = 3;
if (hitTarget.taylorMark >= 5 && attacker && attacker.ultCooldown <= 0) {
const dist = Math.hypot(hitTarget.x - attacker.x, hitTarget.y - attacker.y);
const dmg = Math.min(250, Math.round(100 + (dist / CONFIG.GRID_SIZE) * 15));
applyDamage(hitTarget, dmg, attacker, null, true, false);
hitTarget.taylorMark = 0;
attacker.ultCooldown = 15000;
addImpact(hitTarget.x, hitTarget.y, '#3366ff', 1.0);
addDamageText(hitTarget.x, hitTarget.y - 30, '高阶逼近 ' + dmg, '#3366ff');
}
} else if (p.type === 'hormone') {
applyDamage(hitTarget, p.damage, attacker, p);
hitTarget.hormoneStacks = Math.min(5, (hitTarget.hormoneStacks || 0) + 1);
hitTarget.hormoneTimer = 3;
hitTarget.attackPower = 1 - hitTarget.hormoneStacks * 0.1;
if (hitTarget.hormoneStacks >= 5 && attacker && attacker.hormoneStormCooldown <= 0) {
attacker.hormoneStormCooldown = 15000;
for (const h of heroEntities) {
if (h.hp > 0 && h.id !== attacker.id && Math.hypot(h.x - hitTarget.x, h.y - hitTarget.y) < 150) {
applyDamage(h, 130, attacker, null, true, false);
}
}
hitTarget.hormoneStacks = 0;
hitTarget.hormoneTimer = 0;
hitTarget.attackPower = 1;
attacker.speed = HERO_MAP[attacker.id].speed * 2;
attacker.overdriveTimer = 5000;
}
} else if (p.type === 'spark') {
// 法拉第:电磁感应
applyDamage(hitTarget, p.damage, attacker, p);
hitTarget.chargeStacks = Math.min(4, (hitTarget.chargeStacks || 0) + 1);
hitTarget.chargeTimer = 4;
addDamageText(hitTarget.x, hitTarget.y, '⚡电荷x' + hitTarget.chargeStacks, '#ffd700');
// 连锁闪电
if (hitTarget.chargeStacks >= 4) {
applyDamage(hitTarget, 40, attacker, null, true, false);
addDamageText(hitTarget.x, hitTarget.y - 30, '⚡ 引爆40', '#ffd700');
hitTarget.chargeStacks = 0;
hitTarget.chargeTimer = 0;
// 弹射周围敌人
for (const h of heroEntities) {
if (h.hp > 0 && h.id !== hitTarget.id && h.id !== attacker.id) {
if (Math.hypot(h.x - hitTarget.x, h.y - hitTarget.y) < 80) {
applyDamage(h, 20, attacker, null, false, false);
addDamageText(h.x, h.y - 20, '⚡ 连锁20', '#ffd700');
}
}
}
}
} else {
applyDamage(hitTarget, p.damage, attacker, p);
}
projectiles.splice(i, 1);
}
}
}
// ==========================================
// 🧱 碰撞与边界
// ==========================================
function resolveCollisions() {
for (let i = 0; i < heroEntities.length; i++) {
for (let j = i + 1; j < heroEntities.length; j++) {
const h1 = heroEntities[i], h2 = heroEntities[j];
if (h1.hp <= 0 || h2.hp <= 0) continue;
// 冲锋时忽略碰撞
if (h1.id === 'hawking' && h1.wheelchairTimer > 0) continue;
if (h2.id === 'hawking' && h2.wheelchairTimer > 0) 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, nx = dx / dist, ny = dy / dist;
h1.x -= nx * overlap * 0.5; h1.y -= ny * overlap * 0.5;
h2.x += nx * overlap * 0.5; h2.y += ny * overlap * 0.5;
}
}
}
for (const h of heroEntities) constrainToArena(h);
}
function constrainToArena(h) {
if (!isFinite(h.x)) h.x = ARENA_X + ARENA_WIDTH / 2;
if (!isFinite(h.y)) h.y = ARENA_Y + ARENA_HEIGHT / 2;
let pushed = false, pushX = 0, pushY = 0;
if (h.x < ARENA_X + h.radius) { h.x = ARENA_X + h.radius; pushX = 1; pushed = true; }
if (h.x > ARENA_X + ARENA_WIDTH - h.radius) { h.x = ARENA_X + ARENA_WIDTH - h.radius; pushX = -1; pushed = true; }
if (h.y < ARENA_Y + h.radius) { h.y = ARENA_Y + h.radius; pushY = 1; pushed = true; }
if (h.y > ARENA_Y + ARENA_HEIGHT - h.radius) { h.y = ARENA_Y + ARENA_HEIGHT - h.radius; pushY = -1; pushed = true; }
if (pushed) { h.wallPushTimer = 0.25; h.wallPushX = pushX; h.wallPushY = pushY; }
}
// ==========================================
// 💥 伤害结算
// ==========================================
function applyDamage(target, amount, attacker, projectile, isTrueDamage = false, silent = false) {
if (!target || target.hp <= 0) return;
if (!isFinite(amount)) amount = 0;
amount = Math.max(0, amount);
if (!isTrueDamage && attacker && attacker.attackPower !== undefined && attacker.attackPower < 1) amount *= attacker.attackPower;
// 法拉第力场护盾:减伤 + 反伤
if (target.id === 'faraday' && target.shieldTimer > 0 && !isTrueDamage) {
amount *= 0.7;
// 反伤给攻击者
if (attacker && attacker.hp > 0 && attacker.id !== 'faraday' && !silent) {
const reflectDmg = Math.round(amount * 0.2);
if (reflectDmg > 0) {
attacker.hp -= reflectDmg;
addDamageText(attacker.x, attacker.y - 20, '反伤 ' + reflectDmg, '#ffd700');
// 反伤给攻击者叠一层电荷
attacker.chargeStacks = Math.min(4, (attacker.chargeStacks || 0) + 1);
attacker.chargeTimer = 4;
}
}
}
if (!isTrueDamage && target.dr > 0) amount = amount * (1 - target.dr / 100);
target.hp -= amount;
if (!silent && amount >= 0.5) {
addDamageText(target.x, target.y, Math.round(amount), isTrueDamage ? '#ff00ff' : '#ff6666');
}
if (attacker && attacker.id === 'lavoisier' && amount > 0 && Math.random() < 0.5) {
if (massParticles.length < 15) {
massParticles.push({
x: target.x + (Math.random() - 0.5) * 30,
y: target.y + (Math.random() - 0.5) * 30,
life: 10
});
}
}
return { killed: target.hp <= 0 };
}
function spawnProjectile(opts) {
projectiles.push(Object.assign({
x: 0, y: 0, startX: 0, startY: 0, target: null,
speed: 600, damage: 0, type: 'bullet', ownerId: null
}, opts));
}
// ==========================================
// ✨ 特效
// ==========================================
function updateEffects(dt) {
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);
}
}
function addDamageText(x, y, amount, color) {
if (damageTexts.length > 100) damageTexts.shift();
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();
drawOxygenFields();
drawHormonePools();
drawGeneSeeds();
drawApples();
drawPrisms();
drawSatellites();
drawMassParticles();
drawPeaShooters();
drawFaradayCages();
drawRadiationZones();
drawWormholes();
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 drawOxygenFields() { for (const f of oxygenFields) { ctx.beginPath(); ctx.arc(f.x, f.y, f.radius, 0, Math.PI * 2); ctx.fillStyle = 'rgba(255, 140, 0, 0.18)'; ctx.fill(); ctx.strokeStyle = '#ff8c00'; ctx.lineWidth = 2; ctx.stroke(); } }
function drawHormonePools() { for (const p of hormonePools) { ctx.beginPath(); ctx.arc(p.x, p.y, 40, 0, Math.PI * 2); ctx.fillStyle = 'rgba(255, 68, 204, 0.2)'; ctx.fill(); ctx.strokeStyle = '#ff44cc'; ctx.lineWidth = 2; ctx.stroke(); } }
function drawGeneSeeds() { for (const g of geneSeeds) { ctx.beginPath(); ctx.arc(g.x, g.y, g.radius, 0, Math.PI * 2); ctx.fillStyle = '#ffff00'; ctx.fill(); ctx.strokeStyle = '#fff'; ctx.lineWidth = 2; ctx.stroke(); } }
function drawApples() { for (const 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 drawPrisms() {
for (const 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++) {
const 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 drawSatellites() {
for (const hero of heroEntities) {
if (hero.id !== 'newton' || !hero.satellites) continue;
const orbitRadius = 2 * CONFIG.GRID_SIZE;
for (const sat of hero.satellites) {
const satX = hero.x + Math.cos(sat.angle) * orbitRadius;
const satY = hero.y + Math.sin(sat.angle) * orbitRadius;
ctx.beginPath(); ctx.arc(satX, satY, 8, 0, Math.PI * 2);
ctx.fillStyle = '#00ffff'; ctx.fill();
ctx.strokeStyle = '#ffffff'; ctx.lineWidth = 2; ctx.stroke();
ctx.beginPath(); ctx.moveTo(satX, satY - 8); ctx.lineTo(satX, satY - 14);
ctx.strokeStyle = '#fff'; ctx.lineWidth = 2; ctx.stroke();
}
}
}
function drawMassParticles() { for (const p of massParticles) { ctx.beginPath(); ctx.arc(p.x, p.y, 6, 0, Math.PI * 2); ctx.fillStyle = '#ffffff'; ctx.fill(); ctx.strokeStyle = '#ff8c00'; ctx.lineWidth = 2; ctx.stroke(); } }
function drawPeaShooters() {
for (const p of peaShooters) {
ctx.fillStyle = '#33cc33'; ctx.fillRect(p.x - 6, p.y - 6, 12, 12);
ctx.strokeStyle = '#fff'; ctx.lineWidth = 1; ctx.strokeRect(p.x - 6, p.y - 6, 12, 12);
const hpP = p.hp / p.maxHp;
ctx.fillStyle = '#333'; ctx.fillRect(p.x - 15, p.y - 15, 30, 3);
ctx.fillStyle = '#00ff00'; ctx.fillRect(p.x - 15, p.y - 15, 30 * hpP, 3);
}
}
function drawFaradayCages() {
for (const cage of faradayCages) {
const alpha = Math.min(1, cage.life / 1);
ctx.beginPath(); ctx.arc(cage.x, cage.y, cage.radius, 0, Math.PI * 2);
ctx.fillStyle = `rgba(255, 215, 0, ${0.12 * alpha})`; ctx.fill();
ctx.strokeStyle = `rgba(255, 215, 0, ${alpha})`; ctx.lineWidth = 3; ctx.stroke();
// 笼状网格
for (let i = 0; i < 8; i++) {
const angle = (i / 8) * Math.PI * 2;
ctx.beginPath();
ctx.moveTo(cage.x, cage.y);
ctx.lineTo(cage.x + Math.cos(angle) * cage.radius, cage.y + Math.sin(angle) * cage.radius);
ctx.strokeStyle = `rgba(255, 215, 0, ${0.4 * alpha})`; ctx.lineWidth = 1; ctx.stroke();
}
}
}
function drawRadiationZones() {
for (const zone of radiationZones) {
const alpha = Math.min(1, zone.life / 1);
ctx.beginPath(); ctx.arc(zone.x, zone.y, zone.radius, 0, Math.PI * 2);
ctx.fillStyle = `rgba(138, 43, 226, ${0.15 * alpha})`; ctx.fill();
ctx.strokeStyle = `rgba(138, 43, 226, ${alpha})`; ctx.lineWidth = 2; ctx.stroke();
// 内部漂浮粒子
for (let i = 0; i < 8; i++) {
const angle = performance.now() / 1000 + i * Math.PI / 4;
const r = zone.radius * (0.3 + 0.5 * Math.abs(Math.sin(performance.now() / 800 + i)));
const px = zone.x + Math.cos(angle) * r;
const py = zone.y + Math.sin(angle) * r;
ctx.beginPath(); ctx.arc(px, py, 3, 0, Math.PI * 2);
ctx.fillStyle = `rgba(138, 43, 226, ${alpha})`; ctx.fill();
}
}
}
function drawWormholes() {
for (const w of wormholes) {
if (w.used) continue;
const alpha = Math.min(1, w.life / 2);
// 入口
ctx.beginPath(); ctx.arc(w.entryX, w.entryY, 18, 0, Math.PI * 2);
ctx.fillStyle = `rgba(138, 43, 226, ${0.5 * alpha})`; ctx.fill();
ctx.strokeStyle = `rgba(138, 43, 226, ${alpha})`; ctx.lineWidth = 3; ctx.stroke();
ctx.beginPath(); ctx.arc(w.entryX, w.entryY, 8, 0, Math.PI * 2);
ctx.fillStyle = '#000'; ctx.fill();
// 出口
ctx.beginPath(); ctx.arc(w.exitX, w.exitY, 18, 0, Math.PI * 2);
ctx.fillStyle = `rgba(138, 43, 226, ${0.5 * alpha})`; ctx.fill();
ctx.strokeStyle = `rgba(200, 100, 255, ${alpha})`; ctx.lineWidth = 3; ctx.stroke();
// 出口扩散白光,标识"出口"
const pulse = 8 + Math.sin(performance.now() / 300) * 4;
ctx.beginPath(); ctx.arc(w.exitX, w.exitY, pulse, 0, Math.PI * 2);
ctx.strokeStyle = `rgba(255, 255, 255, ${alpha})`; ctx.lineWidth = 2; ctx.stroke();
}
}
function drawEffects() {
for (const e of impactEffects) {
const t = 1 - Math.max(0, Math.min(1, e.life / (e.maxLife || 0.3)));
ctx.beginPath(); ctx.arc(e.x, e.y, Math.max(0.5, t * 20), 0, Math.PI * 2);
ctx.strokeStyle = e.color || '#fff'; ctx.lineWidth = 3; ctx.stroke();
}
}
function drawProjectiles() {
for (const p of projectiles) {
ctx.beginPath(); ctx.moveTo(p.startX, p.startY); ctx.lineTo(p.x, p.y);
ctx.strokeStyle = p.type === 'pea' ? '#33cc33'
: p.type === 'superpea' ? '#ffff00'
: p.type === 'taylor' ? '#3366ff'
: p.type === 'hormone' ? '#ff44cc'
: p.type === 'oxygen' ? '#ff8c00'
: p.type === 'spark' ? '#ffd700'
: '#fff';
ctx.lineWidth = 4; ctx.stroke();
ctx.beginPath(); ctx.arc(p.x, p.y, 5, 0, Math.PI * 2);
ctx.fillStyle = '#fff'; ctx.fill();
}
}
// ==========================================
// 🎨 英雄渲染
// ==========================================
function drawEntities() {
for (const entity of heroEntities) {
ctx.globalAlpha = entity.hp > 0 ? 1.0 : 0.2;
ctx.beginPath();
ctx.arc(entity.x, entity.y, entity.radius, 0, Math.PI * 2);
ctx.fillStyle = entity.stunTimer > 0 ? '#ffff00' : entity.color;
ctx.fill();
ctx.strokeStyle = 'rgba(255,255,255,0.8)';
ctx.lineWidth = 2; ctx.stroke();
// 力场护盾光环
if (entity.id === 'faraday' && entity.shieldTimer > 0) {
ctx.beginPath(); ctx.arc(entity.x, entity.y, entity.radius + 6, 0, Math.PI * 2);
ctx.strokeStyle = 'rgba(255, 215, 0, 0.8)'; ctx.lineWidth = 3; ctx.stroke();
}
// 专属图标
ctx.save();
ctx.translate(entity.x, entity.y);
if (entity.id === 'newton') {
ctx.fillStyle = '#fff';
ctx.beginPath();
ctx.moveTo(0, -entity.radius - 4);
ctx.lineTo(6, -entity.radius + 2);
ctx.lineTo(-6, -entity.radius + 2);
ctx.closePath(); ctx.fill();
ctx.strokeStyle = '#aaa'; ctx.lineWidth = 1; ctx.stroke();
} else if (entity.id === 'lavoisier') {
ctx.strokeStyle = '#111'; ctx.lineWidth = 2;
ctx.beginPath(); ctx.moveTo(0, 6); ctx.lineTo(0, -6); ctx.stroke();
ctx.beginPath(); ctx.moveTo(-8, -4); ctx.lineTo(8, -4); ctx.stroke();
ctx.beginPath(); ctx.moveTo(-8, -4); ctx.lineTo(-10, 2); ctx.lineTo(-6, 2); ctx.closePath(); ctx.fillStyle = '#111'; ctx.fill();
ctx.beginPath(); ctx.moveTo(8, -4); ctx.lineTo(6, 2); ctx.lineTo(10, 2); ctx.closePath(); ctx.fill();
} else if (entity.id === 'mendel') {
ctx.beginPath(); ctx.arc(0, 2, 8, 0, Math.PI * 2); ctx.fillStyle = '#33cc33'; ctx.fill();
ctx.beginPath(); ctx.moveTo(0, 0); ctx.quadraticCurveTo(5, -10, 12, -8);
ctx.strokeStyle = '#00ff00'; ctx.lineWidth = 3; ctx.stroke();
} else if (entity.id === 'taylor') {
ctx.fillStyle = '#111'; ctx.font = 'bold 18px Arial, sans-serif';
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.fillText('∑', 0, 2);
} else if (entity.id === 'starling') {
ctx.strokeStyle = '#ff44cc'; ctx.lineWidth = 2;
ctx.beginPath(); ctx.moveTo(-6, -8); ctx.bezierCurveTo(6, -4, -6, 0, 6, 4); ctx.stroke();
ctx.beginPath(); ctx.moveTo(6, -8); ctx.bezierCurveTo(-6, -4, 6, 0, -6, 4); ctx.stroke();
} else if (entity.id === 'faraday') {
// 闪电符号
ctx.fillStyle = '#111';
ctx.beginPath();
ctx.moveTo(-2, -10); ctx.lineTo(4, -2); ctx.lineTo(0, -2);
ctx.lineTo(3, 8); ctx.lineTo(-4, -1); ctx.lineTo(-1, -1); ctx.closePath();
ctx.fill();
} else if (entity.id === 'hawking') {
// 黑洞旋涡
ctx.strokeStyle = '#000'; ctx.lineWidth = 2;
for (let i = 0; i < 3; i++) {
ctx.beginPath();
ctx.arc(0, 0, 4 + i * 3, i * 1.5, i * 1.5 + Math.PI * 1.4);
ctx.stroke();
}
// 轮椅轮子
ctx.beginPath(); ctx.arc(-8, 12, 4, 0, Math.PI * 2); ctx.fillStyle = '#333'; ctx.fill();
ctx.beginPath(); ctx.arc(8, 12, 4, 0, Math.PI * 2); ctx.fill();
}
ctx.restore();
// 血条
const barW = 50, barH = 5;
const barX = entity.x - barW / 2;
const barY = entity.y - entity.radius - 15;
const hpP = Math.max(0, entity.hp) / entity.maxHp;
ctx.fillStyle = '#333'; ctx.fillRect(barX, barY, barW, barH);
ctx.fillStyle = hpP > 0.3 ? '#00ff00' : '#ff0000';
ctx.fillRect(barX, barY, barW * hpP, barH);
// 专属冷却条
const tY = barY + barH + 2;
if (entity.id === 'newton') {
ctx.fillStyle = '#333'; ctx.fillRect(barX, tY, barW, 3);
ctx.fillStyle = '#00ffff'; ctx.fillRect(barX, tY, barW * (1 - entity.prismCooldown / 5000), 3);
ctx.fillStyle = '#333'; ctx.fillRect(barX, tY + 4, barW, 3);
ctx.fillStyle = '#ff4444'; ctx.fillRect(barX, tY + 4, barW * (1 - entity.appleTimer / 30000), 3);
} else if (entity.id === 'lavoisier') {
ctx.fillStyle = '#333'; ctx.fillRect(barX, tY, barW, 3);
ctx.fillStyle = '#ff8c00'; ctx.fillRect(barX, tY, barW * (1 - entity.oxygenCooldown / 6000), 3);
for (let i = 0; i < 3; i++) {
ctx.fillStyle = i < entity.preciseStacks ? '#4488ff' : '#333';
ctx.fillRect(barX + i * 10, tY + 4, 8, 3);
}
} else if (entity.id === 'mendel') {
ctx.fillStyle = '#333'; ctx.fillRect(barX, tY, barW, 3);
ctx.fillStyle = '#33cc33'; ctx.fillRect(barX, tY, barW * (1 - entity.peaTimer / 8000), 3);
} else if (entity.id === 'taylor') {
for (let i = 0; i < 5; i++) {
ctx.fillStyle = i < entity.taylorMark ? '#3366ff' : '#333';
ctx.fillRect(barX + i * 10, tY, 8, 3);
}
} else if (entity.id === 'starling') {
ctx.fillStyle = '#333'; ctx.fillRect(barX, tY, barW, 3);
ctx.fillStyle = '#ff44cc'; ctx.fillRect(barX, tY, barW * (1 - entity.hormoneStormCooldown / 15000), 3);
} else if (entity.id === 'faraday') {
ctx.fillStyle = '#333'; ctx.fillRect(barX, tY, barW, 3);
ctx.fillStyle = '#ffd700'; ctx.fillRect(barX, tY, barW * (1 - entity.cageCooldown / 15000), 3);
// 护盾状态
ctx.fillStyle = '#333'; ctx.fillRect(barX, tY + 4, barW, 3);
ctx.fillStyle = '#00ffcc'; ctx.fillRect(barX, tY + 4, barW * (entity.shieldTimer / 5000), 3);
} else if (entity.id === 'hawking') {
ctx.fillStyle = '#333'; ctx.fillRect(barX, tY, barW, 3);
ctx.fillStyle = '#8a2be2'; ctx.fillRect(barX, tY, barW * (1 - entity.wheelchairCooldown / 8000), 3);
ctx.fillStyle = '#333'; ctx.fillRect(barX, tY + 4, barW, 3);
ctx.fillStyle = '#c864ff'; ctx.fillRect(barX, tY + 4, barW * (1 - entity.wormholeTimer / 5000), 3);
}
// 名字
ctx.fillStyle = '#fff';
ctx.font = '11px sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'alphabetic';
ctx.fillText(entity.name, entity.x, barY - 5);
ctx.globalAlpha = 1.0;
}
}
function drawDamageTexts() {
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
for (const txt of damageTexts) {
ctx.fillStyle = txt.color;
ctx.font = 'bold 12px Arial, sans-serif';
ctx.globalAlpha = Math.max(0, Math.min(1, txt.life));
ctx.fillText(txt.text, txt.x, txt.y);
}
ctx.globalAlpha = 1.0;
}
})();
</script>
</body>
</html>Game Source: 电子斗蛐蛐 - 学科大战(法拉第&霍金版)
Creator: EpicCoder88
Libraries: none
Complexity: complex (1513 lines, 64.9 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-mui09gum" to link back to the original. Then publish at arcadelab.ai/publish.