电子斗蛐蛐 - 学科大战
by EpicCoder88931 lines38.6 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;
// ==========================================
// 📖 英雄图鉴(5 名学科英雄)
// ==========================================
const HERO_LIST = [
{ id: 'newton', name: '牛顿', color: '#dddddd', desc: '阵地法师,三棱镜真伤,苹果雨续航' },
{ id: 'lavoisier', name: '拉瓦锡', color: '#ff8c00', desc: '化学消耗,质量守恒,炼金炸药' },
{ id: 'mendel', name: '孟德尔', color: '#33cc33', desc: '召唤豌豆,概率遗传,铺场压制' },
{ id: 'taylor', name: '泰勒', color: '#3366ff', desc: '数学狙击,余项标记,高阶逼近' },
{ id: 'starling', name: '斯他林', color: '#ff44cc', desc: '内分泌续航,稳态调节,激素风暴' }
];
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 satellites = [];
let oxygenFields = [];
let massParticles = [];
let peaShooters = [];
let hormonePools = [];
let geneSeeds = [];
let lastTime = 0;
let rafId = 0;
const camera = { x: 0, y: 0 };
// 全局计时器
let globalTimers = { appleTimer: 0, newtonSatelliteTimer: 0, mendelPeaTimer: 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.title = hero.desc;
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 = [];
satellites = []; oxygenFields = []; massParticles = []; peaShooters = [];
hormonePools = []; geneSeeds = [];
globalTimers = { appleTimer: 0, newtonSatelliteTimer: 0, mendelPeaTimer: 0 };
}
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);
}
// ==========================================
// 🏭 英雄工厂(5 名英雄属性设置)
// ==========================================
function createHero(id, index) {
const heroData = HERO_MAP[id] || { name: '未知', color: '#fff' };
const base = {
id: id, name: heroData.name, color: heroData.color,
hp: 1000, maxHp: 1000, radius: 20, speed: 120,
attackTimer: 0, attackAnimTimer: 0, stunTimer: 0,
wanderTimer: 0, wanderAngle: Math.random() * Math.PI * 2,
facingRight: index === 0,
x: index === 0 ? ARENA_X + 200 : ARENA_X + ARENA_WIDTH - 200,
y: ARENA_Y + ARENA_HEIGHT / 2,
dr: 0,
skillCooldown: 0, // 通用技能冷却
// 专属状态
prismCooldown: 0, satelliteTimer: 0, appleTimer: 0,
massParticles: 0, preciseStacks: 0,
peaTimer: 0, geneSeed: 0,
taylorMark: 0, taylorMarkTimer: 0, taylorUltCooldown: 0,
hormoneStacks: 0, hormoneStormCooldown: 0
};
// 根据英雄 ID 定制属性
switch (id) {
case 'newton':
base.hp = 1100; base.maxHp = 1100; base.speed = 120; base.color = '#dddddd';
base.prismCooldown = 0; base.satelliteTimer = 5000; base.appleTimer = 30000;
break;
case 'lavoisier':
base.hp = 950; base.maxHp = 950; base.speed = 115; base.color = '#ff8c00';
base.attackCooldown = 6000; base.ultCooldown = 12000;
break;
case 'mendel':
base.hp = 850; base.maxHp = 850; base.speed = 105; base.color = '#33cc33';
base.attackCooldown = 2000; base.peaTimer = 8000;
break;
case 'taylor':
base.hp = 800; base.maxHp = 800; base.speed = 110; base.color = '#3366ff';
base.attackCooldown = 1500; base.ultCooldown = 15000;
break;
case 'starling':
base.hp = 900; base.maxHp = 900; base.speed = 115; base.color = '#ff44cc';
base.attackCooldown = 2000; base.hormoneStormCooldown = 15000;
break;
}
return base;
}
// ==========================================
// 🔄 主循环
// ==========================================
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 && 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.skillCooldown > 0) h.skillCooldown -= dt * 1000;
if (h.prismCooldown > 0) h.prismCooldown -= dt * 1000;
if (h.satelliteTimer > 0) h.satelliteTimer -= dt * 1000;
if (h.appleTimer > 0) h.appleTimer -= dt * 1000;
if (h.ultCooldown > 0) h.ultCooldown -= dt * 1000;
if (h.taylorUltCooldown > 0) h.taylorUltCooldown -= dt * 1000;
if (h.hormoneStormCooldown > 0) h.hormoneStormCooldown -= dt * 1000;
// 斯他林被动:稳态调节
if (h.id === 'starling') {
if (h.hp < h.maxHp * 0.5) h.speed = 115 * 1.2;
else if (h.hp > h.maxHp * 0.8) h.speed = 115;
}
}
// 全局刷怪计时器
globalTimers.appleTimer += dt * 1000;
if (globalTimers.appleTimer >= 30000) {
globalTimers.appleTimer = 0;
if (apples.length < 20) { // 牛顿苹果上限20
for (let i = 0; i < 10; i++) {
let tx = ARENA_X + Math.random() * ARENA_WIDTH;
let ty = ARENA_Y + Math.random() * ARENA_HEIGHT;
apples.push({ x: tx, y: ty, radius: 8, vx: 0, vy: 0 });
// 砸中判定
for (const h of heroEntities) {
if (h.hp > 0 && Math.hypot(h.x - tx, h.y - ty) < h.radius + 8) {
applyDamage(h, 40, null, null, false, false);
}
}
}
}
}
// 牛顿卫星生成
globalTimers.newtonSatelliteTimer += dt * 1000;
if (globalTimers.newtonSatelliteTimer >= 5000) {
globalTimers.newtonSatelliteTimer = 0;
for (const h of heroEntities) {
if (h.id === 'newton' && h.hp > 0 && Math.random() < 0.3) {
if (satellites.filter(s => s.ownerId === h.id).length < 5) {
satellites.push({ ownerId: h.id, angle: Math.random() * Math.PI * 2, distance: CONFIG.GRID_SIZE * 2, x: h.x, y: h.y });
}
}
}
}
// 孟德尔豌豆射手种植
globalTimers.mendelPeaTimer += dt * 1000;
if (globalTimers.mendelPeaTimer >= 8000) {
globalTimers.mendelPeaTimer = 0;
for (const h of heroEntities) {
if (h.id === 'mendel' && h.hp > 0 && peaShooters.filter(p => p.ownerId === h.id).length < 4) {
let tx = h.x + (Math.random() - 0.5) * 100;
let ty = h.y + (Math.random() - 0.5) * 100;
peaShooters.push({ ownerId: h.id, x: tx, y: ty, hp: 50, maxHp: 50, attackTimer: 0 });
}
}
}
// 逐英雄 AI
for (const hero of heroEntities) {
if (hero.hp <= 0) continue;
if (hero.stunTimer > 0) continue;
updateHeroAI(hero, dt);
}
// 更新实体系统
updateSatellites(dt);
updateOxygenFields(dt);
updateMassParticles(dt);
updatePeaShooters(dt);
updateHormonePools(dt);
updateGeneSeeds(dt);
resolveCollisions();
updateProjectiles(dt);
updateEffects(dt);
}
// ==========================================
// 🧠 英雄专属 AI
// ==========================================
function updateHeroAI(hero, dt) {
// 寻找最近的敌人
let target = null, minDist = Infinity;
for (const other of heroEntities) {
if (other === hero || other.hp <= 0) continue;
const d = Math.hypot(other.x - hero.x, other.y - hero.y);
if (d < minDist) { minDist = d; target = other; }
}
if (!target) return;
hero.facingRight = target.x > hero.x;
// 走位逻辑
let idealMin = 3 * CONFIG.GRID_SIZE, idealMax = 6 * CONFIG.GRID_SIZE;
if (hero.id === 'taylor') { idealMin = 5 * CONFIG.GRID_SIZE; idealMax = 7 * CONFIG.GRID_SIZE; }
if (hero.id === 'mendel') { idealMin = 2 * CONFIG.GRID_SIZE; idealMax = 4 * CONFIG.GRID_SIZE; }
if (minDist > idealMax) {
const dx = target.x - hero.x, dy = target.y - hero.y, dist = Math.hypot(dx, dy) || 1;
hero.x += (dx / dist) * hero.speed * dt; hero.y += (dy / dist) * hero.speed * dt;
} else if (minDist < idealMin) {
const dx = hero.x - target.x, dy = hero.y - target.y, dist = Math.hypot(dx, dy) || 1;
hero.x += (dx / dist) * hero.speed * dt; hero.y += (dy / dist) * hero.speed * dt;
} else {
// 环绕走位
const dx = target.x - hero.x, dy = target.y - hero.y, dist = Math.hypot(dx, dy) || 1;
hero.x += (-dy / dist) * hero.speed * 0.5 * dt;
hero.y += (dx / dist) * hero.speed * 0.5 * dt;
}
constrainToArena(hero);
// 牛顿特殊逻辑:三棱镜
if (hero.id === 'newton') {
if (hero.prismCooldown <= 0) {
const angle = Math.atan2(target.y - hero.y, target.x - hero.x);
const px = hero.x + Math.cos(angle) * CONFIG.GRID_SIZE * 3;
const py = hero.y + Math.sin(angle) * CONFIG.GRID_SIZE * 3;
newtonPrisms.push({ x: px, y: py, angle: angle, life: 10, sweep: 0 });
hero.prismCooldown = 5000;
}
// 牛顿无普攻,跳过后续
return;
}
// 常规普攻
if (hero.attackTimer <= 0) {
const baseData = HERO_MAP[hero.id] || {};
let cd = 1000;
if (hero.id === 'lavoisier') cd = 6000;
if (hero.id === 'mendel') cd = 2000;
if (hero.id === 'taylor') cd = 1500;
if (hero.id === 'starling') cd = 2000;
doAttack(hero, target);
hero.attackTimer = cd;
}
}
// ==========================================
// 💥 攻击逻辑分发
// ==========================================
function doAttack(hero, target) {
if (!target) return;
hero.attackAnimTimer = 300;
switch (hero.id) {
case 'lavoisier': // 氧气燃烧
spawnProjectile({
x: hero.x, y: hero.y, startX: hero.x, startY: hero.y,
target: target, speed: 500, damage: 0, type: 'oxygen', ownerId: hero.id
});
break;
case 'mendel': // 性状分离(豌豆)
spawnProjectile({
x: hero.x, y: hero.y, startX: hero.x, startY: hero.y,
target: target, speed: 700, damage: 15, type: 'pea', ownerId: hero.id
});
break;
case 'taylor': // 泰勒展开
spawnProjectile({
x: hero.x, y: hero.y, startX: hero.x, startY: hero.y,
target: target, speed: 900, damage: 5, type: 'taylor', ownerId: hero.id
});
break;
case 'starling': // 促胰液素
spawnProjectile({
x: hero.x, y: hero.y, startX: hero.x, startY: hero.y,
target: target, speed: 800, damage: 15, type: 'hormone', ownerId: hero.id
});
break;
}
}
// ==========================================
// 🛰️ 卫星系统
// ==========================================
function updateSatellites(dt) {
for (let i = satellites.length - 1; i >= 0; i--) {
const s = satellites[i];
const owner = heroEntities.find(h => h.id === s.ownerId);
if (!owner || owner.hp <= 0) { satellites.splice(i, 1); continue; }
s.angle += 2.5 * dt;
s.x = owner.x + Math.cos(s.angle) * s.distance;
s.y = owner.y + Math.sin(s.angle) * s.distance;
for (const h of heroEntities) {
if (h.hp <= 0 || h.id === s.ownerId) continue;
if (Math.hypot(h.x - s.x, h.y - s.y) < h.radius + 10) {
applyDamage(h, 40, owner, null);
h.stunTimer = 1000;
addImpact(s.x, s.y, '#00ffff', 0.5);
satellites.splice(i, 1);
break;
}
}
}
}
// ==========================================
// 🔥 氧气燃烧领域
// ==========================================
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, heroEntities.find(he => he.id === f.ownerId), null, false, true);
h.speed = 115 * 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, '+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, '显性纯合 +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: 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) {
let heal = starling.hp > starling.maxHp * 0.8 ? 60 : 30;
starling.hp = Math.min(starling.maxHp, starling.hp + heal * dt * 5);
addDamageText(starling.x, starling.y - 20, '+' + Math.round(heal * dt * 5), '#00ff00');
}
}
}
// ==========================================
// 🧬 基因种子
// ==========================================
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);
addDamageText(mendel.x, mendel.y, '超级豌豆就绪!', '#ffff00');
}
}
}
// ==========================================
// 🚀 弹道更新引擎
// ==========================================
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.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 + 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') {
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) {
hitTarget.dr = Math.min(50, (hitTarget.dr || 0) + 15); // 简单模拟易伤
addDamageText(hitTarget.x, hitTarget.y, '显性!', '#33cc33');
} else {
hitTarget.speed *= 0.7;
addDamageText(hitTarget.x, hitTarget.y, '隐性!', '#33cc33');
}
} else if (p.type === 'taylor') {
applyDamage(hitTarget, p.damage, attacker, p);
hitTarget.x += (hitTarget.x - p.x) * 0.05; // 小击退
// 叠加余项
hitTarget.taylorMark = Math.min(5, (hitTarget.taylorMark || 0) + 1);
hitTarget.taylorMarkTimer = 3;
addDamageText(hitTarget.x, hitTarget.y, '余项+' + hitTarget.taylorMark, '#3366ff');
} else if (p.type === 'hormone') {
applyDamage(hitTarget, p.damage, attacker, p);
hitTarget.hormoneStacks = Math.min(5, (hitTarget.hormoneStacks || 0) + 1);
hitTarget.attackPower = 1 - hitTarget.hormoneStacks * 0.1;
addDamageText(hitTarget.x, hitTarget.y, '失调x' + hitTarget.hormoneStacks, '#ff44cc');
// 未命中残留激素池
} else {
applyDamage(hitTarget, p.damage, attacker, p);
}
// 泰勒大招:高阶逼近
if (p.type === 'taylor' && hitTarget.taylorMark >= 5 && attacker && attacker.taylorUltCooldown <= 0) {
const dist = Math.hypot(hitTarget.x - attacker.x, hitTarget.y - attacker.y);
let dmg = 100 + (dist / CONFIG.GRID_SIZE) * 15;
dmg = Math.min(250, dmg);
applyDamage(hitTarget, dmg, attacker, null, true, false);
hitTarget.taylorMark = 0;
attacker.taylorUltCooldown = 15000;
addImpact(hitTarget.x, hitTarget.y, '#3366ff', 1.0);
addDamageText(hitTarget.x, hitTarget.y - 30, '高阶逼近 ' + Math.round(dmg), '#3366ff');
}
addImpact(p.x, p.y, '#ffffff', 0.2);
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;
let dx = h2.x - h1.x, dy = h2.y - h1.y;
let dist = Math.hypot(dx, dy);
const minDist = h1.radius + h2.radius;
if (dist < 0.01) { dx = 0.01; dy = 0; dist = 0.01; }
if (dist < minDist) {
const overlap = minDist - dist;
const nx = dx / dist, ny = dy / dist;
h1.x -= nx * overlap * 0.5; h1.y -= ny * overlap * 0.5;
h2.x += nx * overlap * 0.5; h2.y += ny * overlap * 0.5;
}
}
}
for (const h of heroEntities) constrainToArena(h);
}
function constrainToArena(h) {
if (!isFinite(h.x)) h.x = ARENA_X + ARENA_WIDTH / 2;
if (!isFinite(h.y)) h.y = ARENA_Y + ARENA_HEIGHT / 2;
if (h.x < ARENA_X + h.radius) { h.x = ARENA_X + h.radius; h.wanderAngle += Math.PI; }
if (h.x > ARENA_X + ARENA_WIDTH - h.radius) { h.x = ARENA_X + ARENA_WIDTH - h.radius; h.wanderAngle += Math.PI; }
if (h.y < ARENA_Y + h.radius) { h.y = ARENA_Y + h.radius; h.wanderAngle += Math.PI; }
if (h.y > ARENA_Y + ARENA_HEIGHT - h.radius) { h.y = ARENA_Y + ARENA_HEIGHT - h.radius; h.wanderAngle += Math.PI; }
}
// ==========================================
// 💥 伤害结算
// ==========================================
function applyDamage(target, amount, attacker, projectile, isTrueDamage = false, silent = false) {
if (!target || target.hp <= 0) return { absorbed: false, killed: false };
if (!isFinite(amount)) amount = 0; amount = Math.max(0, amount);
if (!isTrueDamage && 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 (attacker && attacker.id === 'lavoisier' && Math.random() < 0.5) {
massParticles.push({ x: target.x + (Math.random()-0.5)*30, y: target.y + (Math.random()-0.5)*30, life: 10 });
}
// 斯他林大招:内分泌风暴自动触发
if (target && target.hormoneStacks >= 5 && target.id !== 'starling') {
const starling = heroEntities.find(h => h.id === 'starling' && h.hp > 0);
if (starling && starling.hormoneStormCooldown <= 0) {
starling.hormoneStormCooldown = 15000;
// 引爆全场激素池
for (const pool of hormonePools) {
for (const h of heroEntities) {
if (h.hp > 0 && h.id !== 'starling' && Math.hypot(h.x - pool.x, h.y - pool.y) < 80) {
applyDamage(h, 80 + target.hormoneStacks * 10, starling, null, true, false);
}
}
}
target.hormoneStacks = 0;
starling.speed = 115 * 2;
addDamageText(starling.x, starling.y - 30, '内分泌风暴!', '#ff44cc');
}
}
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, ...opts
});
}
// ==========================================
// ✨ 特效更新与工具
// ==========================================
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 = 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) {
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();
drawNewtonPrisms();
drawSatellites();
drawMassParticles();
drawPeaShooters();
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 (let f of oxygenFields) {
ctx.beginPath(); ctx.arc(f.x, f.y, f.radius, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(255, 140, 0, 0.2)'; ctx.fill();
ctx.strokeStyle = '#ff8c00'; ctx.lineWidth = 2; ctx.stroke();
}
}
function drawHormonePools() {
for (let 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 (let 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 (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) {
p.life -= 0.016; if (p.life <= 0) continue;
ctx.save(); ctx.translate(p.x, p.y); ctx.rotate(p.angle);
ctx.beginPath(); ctx.moveTo(0, 0);
ctx.arc(0, 0, 240, -Math.PI / 6 + p.sweep, Math.PI / 6 + p.sweep); 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.sweep + (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();
// 伤害判定
for (const h of heroEntities) {
if (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 angleToHero = Math.atan2(dy, dx);
let diff = Math.abs(angleToHero - p.angle - p.sweep);
diff = Math.min(diff, Math.PI * 2 - diff);
if (diff < Math.PI / 6) { applyDamage(h, 1 * 0.016, null, null, true, true); }
}
}
p.sweep += 0.8 * 0.016; if (p.sweep > Math.PI / 3) p.sweep = -Math.PI / 3;
}
}
function drawSatellites() {
for (let s of satellites) {
ctx.beginPath(); ctx.arc(s.x, s.y, 8, 0, Math.PI * 2);
ctx.fillStyle = '#00ffff'; ctx.fill(); ctx.strokeStyle = '#fff'; ctx.lineWidth = 2; ctx.stroke();
}
}
function drawMassParticles() {
for (let 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 (let 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 drawEffects() {
for (let 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 === 'taylor' ? '#3366ff' : p.type === 'hormone' ? '#ff44cc' : '#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.save(); ctx.translate(entity.x, entity.y);
ctx.beginPath(); ctx.arc(0, 0, 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();
ctx.fillStyle = '#111'; ctx.font = 'bold 14px sans-serif'; ctx.textAlign = 'center';
let sym = '?';
if (entity.id === 'newton') sym = '🍎';
if (entity.id === 'lavoisier') sym = '⚖️';
if (entity.id === 'mendel') sym = '🌱';
if (entity.id === 'taylor') sym = '∑';
if (entity.id === 'starling') sym = '🧪';
ctx.fillText(sym, 0, 5); ctx.restore();
const barW = 50, barH = 5, barX = entity.x - barW / 2, 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);
ctx.fillStyle = '#fff'; ctx.font = '11px sans-serif'; ctx.textAlign = 'center';
ctx.fillText(entity.name, entity.x, barY - 5);
ctx.globalAlpha = 1.0;
}
}
function drawDamageTexts() {
for (const txt of damageTexts) {
ctx.fillStyle = txt.color; ctx.font = 'bold 12px sans-serif'; ctx.textAlign = 'center';
ctx.globalAlpha = Math.max(0, Math.min(1, txt.life)); ctx.fillText(txt.text, txt.x, txt.y);
}
ctx.globalAlpha = 1.0;
}
})();
</script>
</body>
</html>Game Source: 电子斗蛐蛐 - 学科大战
Creator: EpicCoder88
Libraries: none
Complexity: complex (931 lines, 38.6 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-muhuw5fv" to link back to the original. Then publish at arcadelab.ai/publish.