Worms
Qwen 3.6 27b · composer

Browser check: Artifact runtime error. This is a technical check, not a quality review.
Reviews & generation details
Reviews
No human review yet. Unreviewed does not mean unsuccessful.
Generation context
Imported historical artifact. Its exact original instructions and lineage are not known.
Generation prompt · exact text unavailable
See the current prompt for reference. It may differ from the instructions used to generate this artifact.
Inspect original source 73,481 bytes · SHA-256 8f55f6350927
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>undefined</title>
<style>
/* ── css: layout ── */
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; background: #1a1a2e; font-family: 'Segoe UI', 'Arial', sans-serif; }
#app { position: relative; width: 100%; height: 100%; }
#gameCanvas { display: block; width: 100%; height: 100%; }
#ui { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; }
#ui > * { pointer-events: auto; }
#topBar { position: absolute; top: 10px; left: 50%; transform: translateX(-50%); display: flex; align-items: center; gap: 20px; background: rgba(0,0,0,0.7); padding: 8px 24px; border-radius: 12px; border: 2px solid rgba(255,255,255,0.2); }
#windIndicator { display: flex; align-items: center; gap: 6px; color: #fff; font-size: 13px; font-weight: 600; }
#windArrow { width: 40px; height: 3px; background: #00d4ff; border-radius: 2px; transition: all 0.3s; }
#windValue { font-size: 12px; color: #00d4ff; }
#turnTimer { position: relative; width: 44px; height: 44px; display: flex; align-items: center; justify-content: center; }
#timerRing { position: absolute; width: 100%; height: 100%; border-radius: 50%; border: 3px solid #444; border-top-color: #ff6b6b; transition: border 0.3s, transform 0.1s; }
#timerText { color: #fff; font-size: 14px; font-weight: 700; }
#mapInfo { color: #aaa; font-size: 12px; }
#teamPanel { position: absolute; top: 10px; left: 10px; background: rgba(0,0,0,0.7); padding: 10px; border-radius: 10px; border: 2px solid rgba(255,255,255,0.15); min-width: 200px; max-height: 50vh; overflow-y: auto; }
#teamList { display: flex; flex-direction: column; gap: 4px; }
.teamInfo { display: flex; align-items: center; gap: 8px; padding: 2px 4px; border-radius: 4px; font-size: 12px; }
.teamInfo.active { background: rgba(255,255,255,0.15); }
.teamColor { width: 12px; height: 12px; border-radius: 3px; flex-shrink: 0; }
.teamName { color: #fff; font-weight: 600; }
.unitDots { display: flex; gap: 2px; }
.unitDot { width: 8px; height: 8px; border-radius: 50%; background: #4CAF50; border: 1px solid rgba(0,0,0,0.3); }
.unitDot.dead { background: #555; }
#weaponPanel { position: absolute; bottom: 10px; left: 50%; transform: translateX(-50%); display: flex; flex-direction: column; align-items: center; gap: 8px; }
#powerBar { width: 300px; height: 20px; background: rgba(0,0,0,0.6); border-radius: 10px; border: 2px solid rgba(255,255,255,0.2); overflow: hidden; cursor: crosshair; }
#powerFill { height: 100%; background: linear-gradient(90deg, #4CAF50, #FFC107, #FF5722); width: 50%; transition: width 0.05s; border-radius: 8px; }
#powerValue { color: #fff; font-size: 13px; font-weight: 700; text-shadow: 0 1px 3px rgba(0,0,0,0.5); }
#wepSlot { display: flex; gap: 6px; flex-wrap: wrap; justify-content: center; max-width: 600px; }
.weaponSlot { width: 56px; height: 64px; background: rgba(0,0,0,0.7); border: 2px solid rgba(255,255,255,0.2); border-radius: 10px; cursor: pointer; display: flex; flex-direction: column; align-items: center; justify-content: center; transition: all 0.2s; position: relative; }
.weaponSlot:hover { border-color: rgba(255,255,255,0.5); transform: translateY(-3px); }
.weaponSlot.selected { border-color: #FFD700; box-shadow: 0 0 12px rgba(255,215,0,0.5); transform: translateY(-3px); }
.weaponSlot.onCooldown { opacity: 0.4; cursor: not-allowed; }
.weaponSlot .wepIcon { font-size: 22px; }
.weaponSlot .wepName { font-size: 8px; color: #ddd; margin-top: 2px; text-transform: uppercase; letter-spacing: 0.5px; }
.weaponSlot .wepCount { position: absolute; top: -4px; right: -4px; background: #FF5722; color: #fff; font-size: 10px; font-weight: 700; width: 18px; height: 18px; border-radius: 50%; display: flex; align-items: center; justify-content: center; }
.utilitySlot { width: 48px; height: 48px; background: rgba(0,0,0,0.6); border: 2px solid rgba(100,200,100,0.3); border-radius: 10px; cursor: pointer; display: flex; flex-direction: column; align-items: center; justify-content: center; transition: all 0.2s; position: relative; }
.utilitySlot:hover { border-color: rgba(100,200,100,0.7); }
.utilitySlot.selected { border-color: #4CAF50; box-shadow: 0 0 10px rgba(76,175,80,0.5); }
.utilitySlot .wepCount { position: absolute; top: -4px; right: -4px; background: #4CAF50; color: #fff; font-size: 10px; font-weight: 700; width: 18px; height: 18px; border-radius: 50%; display: flex; align-items: center; justify-content: center; }
.actionBtn { padding: 8px 20px; background: linear-gradient(135deg, #667eea, #764ba2); color: #fff; border: none; border-radius: 8px; cursor: pointer; font-size: 13px; font-weight: 700; text-transform: uppercase; letter-spacing: 1px; transition: all 0.2s; pointer-events: auto; }
.actionBtn:hover { transform: translateY(-2px); box-shadow: 0 4px 15px rgba(102,126,234,0.4); }
.actionBtn:active { transform: translateY(0); }
.actionBtn.skipBtn { background: linear-gradient(135deg, #f093fb, #f5576c); }
.actionBtn.fireBtn { background: linear-gradient(135deg, #FF5722, #FF9800); }
#actionButtons { position: absolute; bottom: 80px; left: 50%; transform: translateX(-50%); display: flex; gap: 10px; }
#turnMessage { position: absolute; top: 40%; left: 50%; transform: translate(-50%, -50%); color: #fff; font-size: 32px; font-weight: 900; text-shadow: 0 0 20px rgba(0,0,0,0.8), 0 4px 8px rgba(0,0,0,0.5); pointer-events: none; opacity: 0; transition: opacity 0.5s; text-align: center; white-space: nowrap; }
#turnMessage.show { opacity: 1; }
#menuOverlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: rgba(10,10,30,0.95); display: flex; align-items: center; justify-content: center; z-index: 100; }
#mainMenu { text-align: center; color: #fff; }
#mainMenu h1 { font-size: 48px; margin-bottom: 30px; background: linear-gradient(135deg, #FFD700, #FF6B6B, #6C5CE7); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
#menuOptions { display: flex; flex-direction: column; gap: 16px; align-items: center; }
#menuOptions label { font-size: 14px; color: #aaa; }
#menuOptions select { padding: 6px 12px; background: rgba(255,255,255,0.1); border: 2px solid rgba(255,255,255,0.2); border-radius: 8px; color: #fff; font-size: 14px; margin: 0 5px; }
#startBtn { margin-top: 10px; padding: 14px 40px; font-size: 18px; background: linear-gradient(135deg, #00d4ff, #667eea); border: none; border-radius: 12px; color: #fff; font-weight: 700; cursor: pointer; text-transform: uppercase; letter-spacing: 2px; transition: all 0.3s; pointer-events: auto; }
#startBtn:hover { transform: translateY(-3px); box-shadow: 0 8px 25px rgba(0,212,255,0.4); }
#playerTypeSelect { display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px 30px; margin-top: 10px; text-align: left; }
#playerTypeSelect label { color: #aaa; font-size: 12px; }
#gameOverOverlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.85); display: none; align-items: center; justify-content: center; z-index: 200; }
#gameOverContent { text-align: center; color: #fff; }
#winnerText { font-size: 48px; margin-bottom: 30px; }
#restartBtn { padding: 12px 36px; font-size: 18px; background: linear-gradient(135deg, #00d4ff, #667eea); border: none; border-radius: 10px; color: #fff; font-weight: 700; cursor: pointer; text-transform: uppercase; letter-spacing: 1px; pointer-events: auto; }
</style>
</head>
<body>
<canvas id="gameCanvas"></canvas>
<div id="ui">
<div id="topBar">
<div id="windIndicator">
<span id="windLabel">Wind</span>
<div id="windArrow"></div>
<span id="windValue">0</span>
</div>
<div id="turnTimer">
<div id="timerRing"></div>
<span id="timerText">20</span>
</div>
<div id="mapInfo">
<span id="terrainType">Island</span>
</div>
</div>
<div id="teamPanel">
<div id="teamList"></div>
<div id="activeUnit">
<div id="unitName"></div>
<div id="healthBar">
<div id="healthFill"></div>
</div>
<span id="healthText">100</span>
</div>
</div>
<div id="weaponPanel">
<div id="powerBar">
<div id="powerFill"></div>
</div>
<div id="powerValue">50</div>
<div id="weaponSlots"></div>
</div>
<div id="minimap"></div>
<div id="actionButtons">
<button id="fireBtn" class="actionBtn">Fire</button>
<button id="moveBtn" class="actionBtn">Move</button>
<button id="skipBtn" class="actionBtn">Skip</button>
</div>
<div id="turnMessage"></div>
</div>
<div id="menuOverlay">
<div id="mainMenu">
<h1>Worms Battle Arena</h1>
<div id="menuOptions">
<div id="playerCount">
<label>Players</label>
<select id="playerSelect">
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
</select>
</div>
<div id="terrainSelect">
<label>Terrain</label>
<select id="terrainSelectBox">
<option value="single">Single Island</option>
<option value="double">Two Islands</option>
<option value="cave">Cave</option>
</select>
</div>
<div id="difficultySelect">
<label>AI Difficulty</label>
<select id="diffSelect">
<option value="easy">Easy</option>
<option value="medium" selected="">Medium</option>
<option value="hard">Hard</option>
</select>
</div>
<button id="startBtn">Start Game</button>
<div id="playerTypeSelect">
<label>Player 1</label>
<select class="playerType" data-player="0">
<option value="human">Human</option>
<option value="cpu">CPU</option>
</select>
<label>Player 2</label>
<select class="playerType" data-player="1">
<option value="human">Human</option>
<option value="cpu" selected="">CPU</option>
</select>
<label>Player 3</label>
<select class="playerType" data-player="2">
<option value="human">Human</option>
<option value="cpu">CPU</option>
</select>
<label>Player 4</label>
<select class="playerType" data-player="3">
<option value="human">Human</option>
<option value="cpu">CPU</option>
</select>
</div>
</div>
</div>
</div>
<div id="gameOverOverlay">
<div id="gameOverContent">
<h2 id="winnerText">Team 1 Wins!</h2>
<button id="restartBtn">Play Again</button>
</div>
</div>
<script>
'use strict';
// ── const: CONFIG ──
// Game constants
const CONFIG = {
WORLD_WIDTH: 2400,
WORLD_HEIGHT: 800,
GRAVITY: 0.35,
WIND_MAX: 3,
WIND_CHANGE_RATE: 0.01,
UNIT_SPEED: 3,
JUMP_FORCE: -8,
MOVE_PITCH: 100,
TURN_TIME: 20,
FALL_DAMAGE_THRESHOLD: 12,
FALL_DAMAGE_FACTOR: 0.5,
UNITS_PER_TEAM: 4,
UNIT_MAX_HP: 100,
UNIT_RADIUS: 8,
TERRAIN_RES: 1,
WATER_DAMAGE_PER_SEC: 15,
PROJECTILE_LIFE: 300,
CAMERA_SMOOTH: 0.08,
CAMERA_ZOOM_SMOOTH: 0.03,
EXPLOSION_FREEZE_THRESH: 30,
};
// ── const: TEAM_COLORS ──
const TEAM_COLORS = [
{ r: 255, g: 80, b: 80, name: 'Red' },
{ r: 80, g: 180, b: 255, name: 'Blue' },
{ r: 80, g: 220, b: 80, name: 'Green' },
{ r: 220, g: 200, b: 60, name: 'Yellow' },
];
// ── const: TEAM_WORM_NAMES ──
const TEAM_WORM_NAMES = [
['Red Thunder', 'Red Storm', 'Red Fury', 'Red Hawk'],
['Blue Bolt', 'Blue Crush', 'Blue Dash', 'Blue Edge'],
['Green Fang', 'Green Flame', 'Green Fang', 'Green Gale'],
['Gold Star', 'Gold Rush', 'Gold Rush', 'Gold Nova'],
];
// ── class: AudioSystem ──
// Procedural audio system using Web Audio API
class AudioSystem {
constructor() {
this.ctx = null;
this.enabled = true;
this.volume = 0.3;
}
init() {
try {
this.ctx = new (window.AudioContext || window.webkitAudioContext)();
this.masterGain = this.ctx.createGain();
this.masterGain.gain.value = this.volume;
this.masterGain.connect(this.ctx.destination);
} catch(e) { this.enabled = false; }
}
resume() { if (this.ctx && this.ctx.state === 'suspended') this.ctx.resume(); }
playTone(freq, duration, type='square', vol=1, slide=0) {
if (!this.ctx || !this.enabled) return;
this.resume();
const t = this.ctx.currentTime;
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = type;
osc.frequency.setValueAtTime(freq, t);
if (slide) osc.frequency.linearRampToValueAtTime(freq + slide, t + duration);
gain.gain.setValueAtTime(vol * 0.3, t);
gain.gain.exponentialRampToValueAtTime(0.001, t + duration);
osc.connect(gain);
gain.connect(this.masterGain);
osc.start(t);
osc.stop(t + duration);
}
playNoise(duration, vol=0.5, filterFreq=2000) {
if (!this.ctx || !this.enabled) return;
this.resume();
const t = this.ctx.currentTime;
const bufferSize = this.ctx.sampleRate * duration;
const buffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) data[i] = (Math.random() * 2 - 1);
const source = this.ctx.createBufferSource();
source.buffer = buffer;
const filter = this.ctx.createBiquadFilter();
filter.type = 'lowpass';
filter.frequency.setValueAtTime(filterFreq, t);
filter.frequency.exponentialRampToValueAtTime(100, t + duration);
const gain = this.ctx.createGain();
gain.gain.setValueAtTime(vol * 0.3, t);
gain.gain.exponentialRampToValueAtTime(0.001, t + duration);
source.connect(filter);
filter.connect(gain);
gain.connect(this.masterGain);
source.start(t);
source.stop(t + duration);
}
explosion(size) {
const dur = 0.3 + size * 0.05;
this.playNoise(dur, Math.min(size / 20, 1), 3000);
this.playTone(80, dur * 0.8, 'sine', Math.min(size / 15, 0.8), -40);
}
smallExplosion() { this.explosion(10); }
bigExplosion() { this.explosion(50); }
gunShot() {
this.playTone(800, 0.05, 'sawtooth', 0.6, -600);
this.playNoise(0.08, 0.4, 4000);
}
rocket() {
this.playTone(400, 0.3, 'sawtooth', 0.3, -350);
this.playNoise(0.2, 0.3, 2000);
}
grenade() {
this.playTone(300, 0.15, 'square', 0.2, -200);
this.playNoise(0.1, 0.3, 3000);
setTimeout(() => this.smallExplosion(), 300);
}
catapult() {
this.playTone(150, 0.4, 'sine', 0.3, 100);
this.playNoise(0.25, 0.5, 1500);
}
superWeapon() {
this.playTone(100, 1.0, 'sine', 0.7, 200);
this.playNoise(0.8, 0.8, 5000);
setTimeout(() => { this.playTone(800, 0.3, 'sine', 0.4); }, 200);
setTimeout(() => this.bigExplosion(), 400);
}
minigun() {
for (let i = 0; i < 5; i++) {
setTimeout(() => {
this.playTone(600 + Math.random() * 400, 0.03, 'sawtooth', 0.2);
this.playNoise(0.03, 0.3, 5000);
}, i * 60);
}
}
teleport() {
this.playTone(400, 0.5, 'sine', 0.4, 800);
setTimeout(() => this.playTone(1200, 0.3, 'sine', 0.3), 100);
}
medkit() {
this.playTone(500, 0.1, 'sine', 0.3, 100);
setTimeout(() => this.playTone(700, 0.1, 'sine', 0.3, 100), 100);
setTimeout(() => this.playTone(900, 0.15, 'sine', 0.3), 200);
}
flag() { this.playTone(880, 0.15, 'sine', 0.3); setTimeout(() => this.playTone(1100, 0.2, 'sine', 0.3), 100); }
bananaSkin() { this.playTone(300, 0.1, 'square', 0.2, -100); }
jump() { this.playTone(300, 0.08, 'sine', 0.2, 150); }
land() { this.playTone(100, 0.08, 'sine', 0.15, -30); }
hurt() { this.playTone(200, 0.15, 'square', 0.3, -100); }
death() {
this.playTone(400, 0.5, 'sawtooth', 0.4, -350);
this.playNoise(0.3, 0.3, 1000);
}
turnTick() { this.playTone(1000, 0.03, 'sine', 0.15); }
turnEnd() {
this.playTone(500, 0.1, 'sine', 0.2, -300);
setTimeout(() => this.playTone(400, 0.1, 'sine', 0.2, -200), 80);
}
movePress() { this.playTone(200, 0.05, 'square', 0.1); }
click() { this.playTone(800, 0.03, 'sine', 0.2); }
uiSelect() { this.playTone(600, 0.04, 'sine', 0.15, 100); }
waterSplash() { this.playNoise(0.4, 0.5, 1500); }
parachute() { this.playNoise(0.6, 0.2, 800); }
}
// ── class: Particle ──
// Particle effects system
class Particle {
constructor(x, y, vx, vy, life, color, size, type='circle', gravity=true, friction=0.99) {
this.x = x; this.y = y; this.vx = vx; this.vy = vy;
this.life = life; this.maxLife = life;
this.color = color; this.size = size;
this.type = type; this.gravity = gravity; this.friction = friction;
this.rotation = Math.random() * Math.PI * 2;
this.rotSpeed = (Math.random() - 0.5) * 0.2;
this.active = true;
}
update(dt, wind) {
this.life -= dt;
if (this.life <= 0) { this.active = false; return; }
this.vx *= this.friction;
this.vy *= this.friction;
if (this.gravity) this.vy += CONFIG.GRAVITY * dt * 0.5;
this.x += this.vx * dt + (wind || 0) * dt * 0.1;
this.y += this.vy * dt;
this.rotation += this.rotSpeed * dt;
}
get alpha() { return Math.max(0, this.life / this.maxLife); }
get scale() { return Math.max(0.01, this.life / this.maxLife); }
}
// ── class: ParticleSystem ──
class ParticleSystem {
constructor() { this.particles = []; }
emit(particle) { this.particles.push(particle); }
update(dt, wind) {
for (let i = this.particles.length - 1; i >= 0; i--) {
this.particles[i].update(dt, wind);
if (!this.particles[i].active) this.particles.splice(i, 1);
}
}
explosion(x, y, radius, color) {
const count = Math.min(60, radius * 1.5);
for (let i = 0; i < count; i++) {
const angle = Math.random() * Math.PI * 2;
const speed = (Math.random() * 0.7 + 0.3) * radius * 0.5;
const c = { r: (color.r + Math.random() * 40) | 0, g: (color.g + Math.random() * 40) | 0, b: (color.b + Math.random() * 40) | 0 };
this.emit(new Particle(x, y, Math.cos(angle) * speed, Math.sin(angle) * speed, 0.5 + Math.random() * 0.8, `rgb(${c.r},${c.g},${c.b})`, 2 + Math.random() * 4));
}
// Smoke
for (let i = 0; i < 15; i++) {
const angle = Math.random() * Math.PI * 2;
const speed = Math.random() * 30;
const shade = 40 + Math.random() * 60;
this.emit(new Particle(x, y, Math.cos(angle) * speed, Math.sin(angle) * speed - 20, 1 + Math.random() * 1.5, `rgba(${shade},${shade},${shade},0.6)`, 5 + Math.random() * 10, 'smoke'));
}
// Fire core
for (let i = 0; i < 20; i++) {
const angle = Math.random() * Math.PI * 2;
const speed = Math.random() * 15;
const bright = 200 + Math.random() * 55;
this.emit(new Particle(x, y, Math.cos(angle) * speed, Math.sin(angle) * speed, 0.2 + Math.random() * 0.3, `rgb(${bright},${bright*0.5 | 0},0)`, 3 + Math.random() * 5));
}
}
smallExplosion(x, y) {
this.explosion(x, y, 8, { r: 255, g: 200, b: 50 });
}
terrainDebris(x, y, terrain) {
const count = 10 + Math.random() * 10;
for (let i = 0; i < count; i++) {
const angle = Math.random() * Math.PI * 2;
const speed = (Math.random() * 0.5 + 0.2) * 80;
const shade = 30 + Math.random() * 40;
const size = 2 + Math.random() * 6;
const p = new Particle(
x + Math.cos(angle) * 20, y + Math.sin(angle) * 20,
Math.cos(angle) * speed, Math.sin(angle) * speed - Math.random() * 40,
0.5 + Math.random() * 1, `rgb(${shade+60},${shade+40},${shade+20})`, size, 'debris'
);
p.gravity = true;
this.emit(p);
}
}
dustTrail(x, y) {
for (let i = 0; i < 3; i++) {
this.emit(new Particle(
x + (Math.random() - 0.5) * 10, y + (Math.random() - 0.5) * 5,
(Math.random() - 0.5) * 10, -Math.random() * 15,
0.3 + Math.random() * 0.4, `rgba(180,160,120,0.5)`, 2 + Math.random() * 3, 'dust'
));
}
}
waterSplash(x, y) {
for (let i = 0; i < 20; i++) {
const angle = -Math.PI * Math.random();
const speed = 50 + Math.random() * 100;
this.emit(new Particle(
x, y, Math.cos(angle) * speed, Math.sin(angle) * speed,
0.3 + Math.random() * 0.5, `rgba(80,160,255,0.7)`, 2 + Math.random() * 3
));
}
}
landingDust(x, y) {
for (let i = 0; i < 12; i++) {
const angle = -Math.PI * Math.random();
const speed = 20 + Math.random() * 40;
const shade = 140 + Math.random() * 40;
this.emit(new Particle(
x + (Math.random() - 0.5) * 15, y, Math.cos(angle) * speed, Math.sin(angle) * speed,
0.3 + Math.random() * 0.5, `rgba(${shade},${shade-20},${shade-40},0.6)`, 2 + Math.random() * 4, 'dust'
));
}
}
hitEffect(x, y, dmg) {
const count = Math.min(dmg * 0.5, 10);
for (let i = 0; i < count; i++) {
const angle = Math.random() * Math.PI * 2;
const speed = 30 + Math.random() * 60;
this.emit(new Particle(
x, y, Math.cos(angle) * speed, Math.sin(angle) * speed,
0.2 + Math.random() * 0.3, `rgb(255,${50+Math.random()*100|0},0)`, 2 + Math.random() * 2
));
}
}
}
// ── class: FloatingText ──
// Floating combat text
class FloatingText {
constructor(x, y, text, color, size=20) {
this.x = x; this.y = y; this.text = text;
this.color = color; this.size = size;
this.life = 1.5; this.maxLife = 1.5;
this.vy = -50;
this.active = true;
}
update(dt) {
this.life -= dt;
if (this.life <= 0) { this.active = false; return; }
this.y += this.vy * dt;
this.vy += 20 * dt;
}
get alpha() { return Math.max(0, this.life / this.maxLife); }
}
// ── class: Terrain ──
// Terrain system with destruction
class Terrain {
constructor(w, h, waterLevel) {
this.width = w; this.height = h;
this.waterLevel = waterLevel;
this.data = new Uint8Array(w * h); // 0=air, 1-100=terrain density
this.initialData = null;
}
generate(type) {
this.data.fill(0);
if (type === 'single') this._generateSingleIsland();
else if (type === 'double') this._generateDoubleIsland();
else if (type === 'cave') this._generateCave();
this.initialData = new Uint8Array(this.data);
return this;
}
_heightAt(x) {
return this.data[x * this.height + 0] > 0 ?
(this._findFirstSolidColumn(x) >> 0) : -1;
}
_findFirstSolidColumn(x) {
for (let y = 0; y < this.height; y++) {
if (this.data[x * this.height + y] > 0) return y;
}
return -1;
}
_setColumn(x, topY, height) {
for (let y = topY; y < topY + height && y < this.height; y++) {
if (x >= 0 && x < this.width) {
this.data[x * this.height + y] = 100;
}
}
}
_noise(x, seed) {
// Simple multi-octave noise
let v = 0;
for (let o = 0, f = 1, a = 0.5; o < 4; o++, f *= 2, a *= 0.5) {
v += a * Math.sin(x * f * 0.01 + seed * (o + 1)) * Math.sin(x * f * 0.007 + seed * (o + 3) * 1.3);
}
return v;
}
_generateSingleIsland() {
const baseH = this.height * 0.35;
const seed = Math.random() * 100;
for (let x = 0; x < this.width; x++) {
const nx = x / this.width;
// Bell shape for island
const islandShape = 1 - Math.pow((nx - 0.5) * 1.8, 2);
const hill = this._noise(x, seed) * 80;
const surface = baseH + hill;
const topY = Math.max(20, surface * Math.max(0.1, islandShape));
const depth = Math.min(this.height - topY, 200 + Math.abs(hill) * 0.5);
this._setColumn(x, topY >> 0, Math.floor(depth));
}
}
_generateDoubleIsland() {
const baseH = this.height * 0.3;
const seed = Math.random() * 100;
for (let x = 0; x < this.width; x++) {
const nx = x / this.width;
// Two peaks
const d1 = Math.pow(nx - 0.25, 2) * 20;
const d2 = Math.pow(nx - 0.75, 2) * 20;
const shape = Math.max(0, 1 - Math.min(d1, d2));
if (shape < 0.15) continue; // Water gap
const hill = this._noise(x, seed) * 70;
const surface = baseH * shape + hill * shape;
const topY = Math.max(30, surface);
const depth = Math.min(this.height - topY, 150 + Math.abs(hill) * 0.4);
this._setColumn(x, topY >> 0, Math.floor(depth));
}
// Fill from bottom for each island
for (let x = 0; x < this.width; x++) {
for (let y = 0; y < this.height; y++) {
if (!this.data[x * this.height + y]) {
// Check if there's land above
let hasAbove = false;
for (let yy = 0; yy < y; yy++) {
if (this.data[x * this.height + yy]) { hasAbove = true; break; }
}
if (hasAbove) this.data[x * this.height + y] = 100;
}
}
}
}
_generateCave() {
const seed = Math.random() * 100;
// Ceiling
for (let x = 0; x < this.width; x++) {
const h = 40 + this._noise(x, seed) * 30;
this._setColumn(x, 0, Math.floor(h));
}
// Floor
for (let x = 0; x < this.width; x++) {
const h = 60 + this._noise(x, seed + 50) * 25;
this._setColumn(x, this.height - h, h);
}
// Pillars
const pillarCount = 3 + Math.random() * 4;
for (let p = 0; p < pillarCount; p++) {
const px = 200 + Math.random() * (this.width - 400);
const pw = 30 + Math.random() * 50;
const seed2 = seed + p * 17;
for (let x = Math.floor(px - pw/2); x < Math.floor(px + pw/2); x++) {
if (x < 0 || x >= this.width) continue;
let topY = -1, botY = this.height;
for (let y = 0; y < this.height; y++) {
if (this.data[x * this.height + y] > 0 && topY < 0) topY = y;
if (this.data[x * this.height + y] > 0) botY = y;
}
const pillarTop = topY + (botY - topY) * 0.15 + this._noise(x, seed2) * 15;
const pillarBot = topY + (botY - topY) * 0.85 + this._noise(x, seed2) * 15;
this._setColumn(x, pillarTop >> 0, Math.floor(pillarBot - pillarTop));
}
}
// Underwater portion
for (let x = 0; x < this.width; x++) {
let botY = this.height;
for (let y = this.height - 1; y >= 0; y--) {
if (this.data[x * this.height + y] > 0) { botY = y; break; }
}
for (let y = botY; y < this.height; y++) {
this.data[x * this.height + y] = 100;
}
}
// Water level for cave
for (let x = 0; x < this.width; x++) {
for (let y = this.height - 80; y < this.height; y++) {
if (!this.data[x * this.height + y]) {
// This is water
}
}
}
this.waterLevel = this.height - 80;
}
// Check if pixel is solid
isSolid(x, y) {
x = x >> 0; y = y >> 0;
if (x < 0 || x >= this.width || y < 0 || y >= this.height) return y >= this.height;
return this.data[x * this.height + y] > 0;
}
// Check if pixel is in water
isInWater(x, y) {
const wy = this.waterLevel;
if (wy < 0) return false;
x = x >> 0; y = y >> 0;
if (x < 0 || x >= this.width || y < 0 || y >= this.height) return false;
return y > wy;
}
// Get surface Y at x
getSurfaceY(x) {
x = x >> 0;
if (x < 0 || x >= this.width) return -1;
for (let y = 0; y < this.height; y++) {
if (this.data[x * this.height + y] > 0) return y;
}
return -1;
}
// Destroy terrain in circle
destroy(cx, cy, radius) {
let destroyed = false;
const r2 = radius * radius;
const x0 = Math.max(0, Math.floor(cx - radius));
const x1 = Math.min(this.width - 1, Math.ceil(cx + radius));
const y0 = Math.max(0, Math.floor(cy - radius));
const y1 = Math.min(this.height - 1, Math.ceil(cy + radius));
for (let x = x0; x <= x1; x++) {
for (let y = y0; y <= y1; y++) {
const dx = x - cx, dy = y - cy;
if (dx*dx + dy*dy <= r2) {
if (this.data[x * this.height + y] > 0) {
this.data[x * this.height + y] = 0;
destroyed = true;
}
}
}
}
return destroyed;
}
// Get color at pixel (with some variation)
getColor(x, y) {
if (this.isSolid(x, y)) {
const depth = y < this.height * 0.3 ? 0.4 : y < this.height * 0.6 ? 0.7 : 1.0;
return { r: (80 * depth + 20) | 0, g: (120 * depth + 30) | 0, b: (50 * depth + 10) | 0 };
}
return null;
}
// Find spawn positions
findSpawnPositions(count) {
const positions = [];
const step = this.width / (count * 4);
const attempts = count * 20;
for (let i = 0; i < attempts && positions.length < count; i++) {
const x = 50 + Math.floor(i * step / 2 + Math.random() * step * 0.5) % (this.width - 100);
const sy = this.getSurfaceY(x);
if (sy >= 0 && sy < this.height - 10) {
let tooClose = false;
for (const p of positions) {
if (Math.abs(p.x - x) < 80) { tooClose = true; break; }
}
if (!tooClose) positions.push({ x, y: sy - 5 });
}
}
return positions;
}
}
// ── class: Unit ──
// Unit / Worm class
class Unit {
constructor(id, team, name, x, y) {
this.id = id;
this.team = team;
this.name = name;
this.x = x; this.y = y;
this.vx = 0; this.vy = 0;
this.hp = CONFIG.UNIT_MAX_HP;
this.maxHp = CONFIG.UNIT_MAX_HP;
this.alive = true;
this.radius = CONFIG.UNIT_RADIUS;
this.facing = 1; // 1=right, -1=left
this.onGround = false;
this.movePitch = 0;
this.fallStartY = y;
this.inWater = false;
this.isParachuting = false;
}
update(dt, terrain, wind) {
if (!this.alive) return;
const prevGround = this.onGround;
const prevY = this.y;
// Wind effect on airborne units
if (!this.onGround) {
this.vx += wind * dt * 0.5;
}
// Apply velocity
this.x += this.vx * dt;
this.y += this.vy * dt;
// Gravity
if (this.isParachuting) {
this.vy = Math.min(this.vy + CONFIG.GRAVITY * dt, 2);
} else {
this.vy += CONFIG.GRAVITY * dt;
}
// Water check
this.inWater = terrain.isInWater(this.x, this.y + this.radius);
if (this.inWater && !this.isParachuting) {
this.vy *= 0.95; // Buoyancy
this.vx *= 0.95;
}
// World bounds
if (this.x < this.radius) { this.x = this.radius; this.vx = 0; }
if (this.x > CONFIG.WORLD_WIDTH - this.radius) { this.x = CONFIG.WORLD_WIDTH - this.radius; this.vx = 0; }
// Fall damage tracking
if (this.onGround) this.fallStartY = this.y;
// Terrain collision
this._collideWithTerrain(terrain);
// Check if fell off world
if (this.y > CONFIG.WORLD_HEIGHT + 50) {
this.takeDamage(200);
}
}
_collideWithTerrain(terrain) {
this.onGround = false;
// Bottom collision
const bottomY = this.y + this.radius;
if (terrain.isSolid(this.x, bottomY)) {
// Find surface
let surfaceY = bottomY;
while (surfaceY > 0 && terrain.isSolid(this.x, surfaceY)) surfaceY--;
this.y = surfaceY - this.radius;
if (this.vy > 0) {
// Fall damage calculation
const fallDist = this.fallStartY - bottomY;
if (fallDist < 0) {
const dmg = Math.max(0, (Math.abs(fallDist) - CONFIG.FALL_DAMAGE_THRESHOLD) * CONFIG.FALL_DAMAGE_FACTOR);
if (dmg > 0) {
this.takeDamage(dmg);
// Landing particles
game.particles.landingDust(this.x, bottomY);
game.audio.land();
}
}
this.vy = 0;
}
this.onGround = true;
}
// Side collision
if (this.vx > 0 && terrain.isSolid(this.x + this.radius, this.y)) {
this.x = this._findClearX(this.x, -1, terrain);
this.vx = 0;
}
if (this.vx < 0 && terrain.isSolid(this.x - this.radius, this.y)) {
this.x = this._findClearX(this.x, 1, terrain);
this.vx = 0;
}
// Top collision
if (this.vy < 0 && terrain.isSolid(this.x, this.y - this.radius)) {
this.vy = 0;
}
}
_findClearX(x, dir, terrain) {
let nx = x;
for (let i = 0; i < 15; i++) {
nx += dir;
if (!terrain.isSolid(nx + this.radius * Math.sign(dir), this.y)) break;
}
return nx;
}
move(dx) {
if (!this.alive) return;
if (Math.abs(dx) > 0.01) {
this.facing = dx > 0 ? 1 : -1;
this.vx = dx * CONFIG.UNIT_SPEED;
}
}
jump() {
if (!this.alive || !this.onGround) return;
this.vy = CONFIG.JUMP_FORCE;
this.onGround = false;
game.particles.dustTrail(this.x, this.y + this.radius);
game.audio.jump();
}
parachute() {
if (!this.alive) return;
if (!this.isParachuting) {
this.isParachuting = true;
game.audio.parachute();
}
}
takeDamage(amount, source) {
if (!this.alive) return;
this.hp = Math.max(0, this.hp - amount);
if (this.hp <= 0) {
this.alive = false;
game.onUnitDeath(this);
}
return amount;
}
heal(amount) {
if (!this.alive) return false;
this.hp = Math.min(this.maxHp, this.hp + amount);
return this.hp > CONFIG.UNIT_MAX_HP * 0.5;
}
}
// ── class: Projectile ──
// Projectile class
class Projectile {
constructor(x, y, vx, vy, weapon, owner) {
this.x = x; this.y = y;
this.vx = vx; this.vy = vy;
this.weapon = weapon;
this.owner = owner;
this.life = CONFIG.PROJECTILE_LIFE;
this.alive = true;
this.trail = [];
}
update(dt, terrain, wind) {
if (!this.alive) return;
this.life--;
// Wind effect
this.vx += wind * dt * 0.2;
this.vy += CONFIG.GRAVITY * dt;
this.x += this.vx * dt;
this.y += this.vy * dt;
this.trail.push({ x: this.x, y: this.y, life: 0.3 });
if (this.trail.length > 20) this.trail.shift();
for (const t of this.trail) t.life -= dt;
this.trail = this.trail.filter(t => t.life > 0);
// Check terrain collision
if (terrain.isSolid(this.x, this.y)) {
this.weapon.onImpact(this.x, this.y, terrain, game, game.units, this.owner);
this.alive = false;
return;
}
// Check unit collision
for (const unit of game.units) {
if (unit === this.owner || !unit.alive) continue;
const dx = unit.x - this.x, dy = unit.y - this.y;
if (dx*dx + dy*dy < (unit.radius + 4) * (unit.radius + 4)) {
this.weapon.onImpact(this.x, this.y, terrain, game, game.units, this.owner);
this.alive = false;
return;
}
}
// Out of bounds or expired
if (this.life <= 0 || this.y > CONFIG.WORLD_HEIGHT + 50 || this.x < -50 || this.x > CONFIG.WORLD_WIDTH + 50) {
this.alive = false;
}
}
}
// ── const: WEAPONS ──
// Weapon definitions
const WEAPONS = {
grenade: {
name: 'Grenade',
icon: '🧨',
category: 'indirect',
damage: 30,
radius: 25,
maxPower: 12,
count: 3,
cooldown: 0,
fire: (unit, angle, power, terrain, projList) => {
const speed = power * 0.12;
projList.push(new Projectile(
unit.x, unit.y - 10,
Math.cos(angle) * speed, Math.sin(angle) * speed,
WEAPONS.grenade, unit
));
game.audio.grenade();
},
onImpact: (x, y, terrain, g, units, owner) => {
terrain.destroy(x, y, this.radius);
for (const u of units) {
if (!u.alive || u === owner) continue;
const dx = u.x - x, dy = u.y - y;
const dist = Math.sqrt(dx*dx + dy*dy);
if (dist < this.radius) {
const dmg = this.damage * (1 - dist / this.radius * 0.7);
const actual = u.takeDamage(dmg);
u.vx += (dx / dist) * 3;
u.vy -= 2;
if (actual > 0) {
g.floatingTexts.push(new FloatingText(u.x, u.y - 20, `-${actual | 0}`, '#FF6B6B'));
g.audio.hurt();
}
}
}
g.particles.explosion(x, y, this.radius, { r: 255, g: 200, b: 50 });
g.particles.terrainDebris(x, y);
g.audio.smallExplosion();
g.shakeScreen(Math.min(this.radius / 2, 5));
if (this.radius > CONFIG.EXPLOSION_FREEZE_THRESH) g.freezeFrame = 3;
}
},
bazooka: {
name: 'Bazooka',
icon: '🚀',
category: 'indirect',
damage: 50,
radius: 35,
maxPower: 10,
count: 2,
cooldown: 60,
fire: (unit, angle, power, terrain, projList) => {
const speed = power * 0.11;
projList.push(new Projectile(
unit.x, unit.y - 12,
Math.cos(angle) * speed, Math.sin(angle) * speed,
WEAPONS.bazooka, unit
));
game.audio.rocket();
},
onImpact: (x, y, terrain, g, units, owner) => {
terrain.destroy(x, y, this.radius);
for (const u of units) {
if (!u.alive || u === owner) continue;
const dx = u.x - x, dy = u.y - y;
const dist = Math.sqrt(dx*dx + dy*dy);
if (dist < this.radius) {
const dmg = this.damage * (1 - dist / this.radius * 0.5);
const actual = u.takeDamage(dmg);
u.vx += (dx / dist) * 6;
u.vy -= 5;
if (actual > 0) {
g.floatingTexts.push(new FloatingText(u.x, u.y - 20, `-${actual | 0}`, '#FF4444'));
g.audio.hurt();
}
}
}
g.particles.explosion(x, y, this.radius, { r: 255, g: 150, b: 30 });
g.particles.terrainDebris(x, y);
g.audio.bigExplosion();
g.shakeScreen(8);
g.freezeFrame = 5;
}
},
minigun: {
name: 'Minigun',
icon: '🔫',
category: 'direct',
damage: 5,
radius: 3,
maxPower: 8,
count: 2,
cooldown: 0,
fire: (unit, angle, power, terrain, projList) => {
const pellets = 5 + Math.floor(power / 2);
game.audio.minigun();
for (let i = 0; i < pellets; i++) {
const spread = (Math.random() - 0.5) * 0.15;
const p = new Projectile(
unit.x, unit.y - 10,
Math.cos(angle + spread) * 10, Math.sin(angle + spread) * 10,
WEAPONS.minigun, unit
);
p.life = 15;
projList.push(p);
}
},
onImpact: (x, y, terrain, g, units, owner) => {
for (const u of units) {
if (!u.alive || u === owner) continue;
const dx = u.x - x, dy = u.y - y;
const dist = Math.sqrt(dx*dx + dy*dy);
if (dist < 8) {
const actual = u.takeDamage(this.damage);
if (actual > 0) {
g.floatingTexts.push(new FloatingText(u.x, u.y - 20, `-${actual}`, '#FFA500'));
g.particles.hitEffect(u.x, u.y, actual);
g.audio.hurt();
}
}
}
terrain.destroy(x, y, 2);
g.particles.smallExplosion(x, y);
}
},
catapult: {
name: 'Catapult',
icon: '💣',
category: 'indirect',
damage: 60,
radius: 45,
maxPower: 11,
count: 1,
cooldown: 0,
fire: (unit, angle, power, terrain, projList) => {
const speed = power * 0.13;
projList.push(new Projectile(
unit.x, unit.y - 15,
Math.cos(angle) * speed, Math.sin(angle) * speed,
WEAPONS.catapult, unit
));
game.audio.catapult();
},
onImpact: (x, y, terrain, g, units, owner) => {
terrain.destroy(x, y, this.radius);
for (const u of units) {
if (!u.alive || u === owner) continue;
const dx = u.x - x, dy = u.y - y;
const dist = Math.sqrt(dx*dx + dy*dy);
if (dist < this.radius) {
const dmg = this.damage * (1 - dist / this.radius * 0.3);
const actual = u.takeDamage(dmg);
u.vx += (dx / dist) * 8;
u.vy -= 7;
if (actual > 0) {
g.floatingTexts.push(new FloatingText(u.x, u.y - 20, `-${actual | 0}`, '#FF0000'));
g.audio.hurt();
}
}
}
g.particles.explosion(x, y, this.radius, { r: 255, g: 100, b: 0 });
g.particles.terrainDebris(x, y);
g.audio.bigExplosion();
g.shakeScreen(12);
g.freezeFrame = 8;
}
},
holyHand: {
name: 'H. Hand',
icon: '☝️',
category: 'map',
damage: 100,
radius: 0,
maxPower: 5,
count: 1,
cooldown: 0,
fire: (unit, angle, power, terrain, projList) => {
game.audio.superWeapon();
// Beam of destruction
const targetX = unit.x + Math.cos(angle) * CONFIG.WORLD_WIDTH;
const targetY = unit.y + Math.sin(angle) * 200;
for (let t = 0; t < 1; t += 0.02) {
const bx = unit.x + (targetX - unit.x) * t;
const by = unit.y + (targetY - unit.y) * t + Math.sin(t * 10) * 30;
terrain.destroy(bx, by, 20);
}
// Damage all units in a cone
for (const u of units) {
if (!u.alive || u === unit) continue;
const dx = u.x - unit.x, dy = u.y - unit.y;
const dot = (dx * Math.cos(angle) + dy * Math.sin(angle)) / Math.sqrt(dx*dx + dy*dy + 1);
if (dot > 0.5) {
const dist = Math.sqrt(dx*dx + dy*dy);
if (dist < 500) {
u.takeDamage(100);
u.vx += Math.cos(angle) * 15;
u.vy += Math.sin(angle) * 10 - 5;
}
}
}
// Particles along the beam
for (let i = 0; i < 30; i++) {
const t = Math.random();
const bx = unit.x + (targetX - unit.x) * t;
const by = unit.y + (targetY - unit.y) * t + Math.sin(t * 10) * 30;
g.particles.explosion(bx, by, 20, { r: 200, g: 200, b: 255 });
}
g.shakeScreen(20);
g.freezeFrame = 15;
},
onImpact: () => {}
},
shotgun: {
name: 'Shotgun',
icon: '💥',
category: 'direct',
damage: 15,
radius: 8,
maxPower: 10,
count: 2,
cooldown: 0,
fire: (unit, angle, power, terrain, projList) => {
const pellets = 8;
game.audio.gunShot();
for (let i = 0; i < pellets; i++) {
const spread = (Math.random() - 0.5) * 0.3;
const speed = 4 + power * 0.08;
projList.push(new Projectile(
unit.x, unit.y - 10,
Math.cos(angle + spread) * speed, Math.sin(angle + spread) * speed,
WEAPONS.shotgun, unit
));
}
},
onImpact: (x, y, terrain, g, units, owner) => {
for (const u of units) {
if (!u.alive || u === owner) continue;
const dx = u.x - x, dy = u.y - y;
const dist = Math.sqrt(dx*dx + dy*dy);
if (dist < 15) {
const actual = u.takeDamage(this.damage);
if (actual > 0) {
g.floatingTexts.push(new FloatingText(u.x, u.y - 20, `-${actual}`, '#FFD700'));
g.particles.hitEffect(u.x, u.y, actual);
g.audio.hurt();
}
}
}
terrain.destroy(x, y, 3);
g.particles.smallExplosion(x, y);
}
}
};
// ── const: UTILITIES ──
// Utility items
const UTILITIES = {
parachute: {
name: 'Parachute',
icon: '🪂',
count: 1,
use: (unit) => {
unit.parachute();
}
},
medkit: {
name: 'Medkit',
icon: '💊',
count: 1,
use: (unit) => {
if (unit.alive) {
unit.heal(30);
game.audio.medkit();
game.floatingTexts.push(new FloatingText(unit.x, unit.y - 30, '+30', '#4CAF50', 18));
}
}
},
flag: {
name: 'Flag',
icon: '🚩',
count: 1,
use: (unit) => {
game.audio.flag();
unit.x += (Math.random() > 0.5 ? 1 : -1) * 100;
game.floatingTexts.push(new FloatingText(unit.x, unit.y - 30, 'Tele!', '#FFD700', 18));
}
},
banana: {
name: 'Banana',
icon: '🍌',
count: 1,
use: (unit) => {
game.bananaAt = { x: unit.x, y: unit.y + unit.radius };
game.audio.bananaSkin();
}
}
};
// ── class: Camera ──
// Camera system with smooth follow, zoom, and shake
class Camera {
constructor(x, y, target) {
this.x = x; this.y = y;
this.targetX = x; this.targetY = y;
this.zoom = 1; this.targetZoom = 1;
this.shakeX = 0; this.shakeY = 0; this.shakeMag = 0;
this.target = target;
}
update(target) {
if (target.alive) {
this.targetX = target.x;
this.targetY = target.y - 80; // Look a bit upward
}
// Smooth follow
this.x += (this.targetX - this.x) * CONFIG.CAMERA_SMOOTH;
this.y += (this.targetY - this.y) * CONFIG.CAMERA_SMOOTH;
// Smooth zoom
this.zoom += (this.targetZoom - this.zoom) * CONFIG.CAMERA_ZOOM_SMOOTH;
// Shake
if (this.shakeMag > 0) {
this.shakeX = (Math.random() - 0.5) * this.shakeMag;
this.shakeY = (Math.random() - 0.5) * this.shakeMag;
this.shakeMag *= 0.85;
if (this.shakeMag < 0.3) { this.shakeMag = 0; this.shakeX = 0; this.shakeY = 0; }
}
}
applyToContext(ctx, canvasW, canvasH) {
ctx.save();
const screenX = canvasW / 2;
const screenY = canvasH / 2;
ctx.translate(screenX + this.shakeX, screenY + this.shakeY);
ctx.scale(this.zoom, this.zoom);
ctx.translate(-this.x, -this.y);
}
undoContext(ctx) { ctx.restore(); }
shake(magnitude) { this.shakeMag = Math.max(this.shakeMag, magnitude); }
toScreen(worldX, worldY, canvasW, canvasH) {
const sx = (worldX - this.x) * this.zoom + canvasW / 2;
const sy = (worldY - this.y) * this.zoom + canvasH / 2;
return { x: sx, y: sy };
}
}
// ── class: AIController ──
// AI system with risk assessment
class AIController {
constructor(difficulty = 'medium') {
this.difficulty = difficulty;
this.decisionDelay = 0;
this.thinking = false;
this.targetUnit = null;
this.aimAngle = 0;
this.aimPower = 50;
}
get accuracy() {
return { easy: 0.4, medium: 0.65, hard: 0.85 }[this.difficulty] || 0.6;
}
update(unit, game) {
if (this.thinking) return;
if (!unit.alive) return;
this.decisionDelay--;
if (this.decisionDelay > 0) return;
const aliveEnemies = game.units.filter(u => u.alive && u.team !== unit.team);
if (aliveEnemies.length === 0) return;
// Assess risk of current position
const risk = this._assessRisk(unit, game.terrain);
// Decision making
const availableWeapons = Object.keys(WEAPONS).filter(w => unit.weapons && unit.weapons[w] > 0);
if (availableWeapons.length > 0) {
const weapon = this._selectWeapon(availableWeapons, unit, game);
this._computeAim(unit, weapon, game, aliveEnemies, risk);
}
}
_assessRisk(unit, terrain) {
let risk = 0;
const surfaceY = terrain.getSurfaceY(unit.x);
const distToCliff = this._findNearestCliff(unit, terrain);
if (distToCliff < 20) risk += 5; // Near cliff
if (unit.inWater) risk += 10; // In water
if (unit.hp < 30) risk += 3;
// Check exposed position
const aboveY = unit.y - 50;
if (!terrain.isSolid(unit.x, aboveY)) {
risk += 2; // Exposed to air
}
return risk;
}
_findNearestCliff(unit, terrain) {
let minDist = 999;
for (let dir = -1; dir <= 1; dir += 2) {
for (let d = 5; d < 60; d++) {
const cx = unit.x + dir * d;
const cy = unit.y;
const surfaceY = terrain.getSurfaceY(cx);
if (surfaceY < 0 || (surfaceY - unit.y + unit.radius > 15)) {
minDist = Math.min(minDist, d);
break;
}
}
}
return minDist;
}
_selectWeapon(available, unit, game) {
const aliveEnemies = game.units.filter(u => u.alive && u.team !== unit.team);
if (aliveEnemies.length === 0) return null;
let closest = aliveEnemies[0];
let closestDist = Infinity;
for (const e of aliveEnemies) {
const d = Math.abs(e.x - unit.x);
if (d < closestDist) { closestDist = d; closest = e; }
}
// Pick best weapon for distance
const weaponPriority = [];
for (const w of available) {
const wp = WEAPONS[w];
if (wp.category === 'indirect') {
weaponPriority.push({ w, score: 1 + closestDist / 200 });
} else if (wp.category === 'direct' && closestDist < 150) {
weaponPriority.push({ w, score: 2 });
} else if (wp.category === 'map') {
if (closestDist < 400) weaponPriority.push({ w, score: 3 });
else weaponPriority.push({ w, score: 0.5 });
}
}
weaponPriority.sort((a, b) => b.score - a.score);
return weaponPriority.length > 0 ? weaponPriority[0].w : available[0];
}
_computeAim(unit, weaponName, game, enemies, risk) {
if (enemies.length === 0) return;
const target = this._pickTarget(enemies, unit);
// Simple trajectory calculation
const dx = target.x - unit.x;
const dy = target.y - unit.y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 50) {
this.aimAngle = dx > 0 ? 0 : Math.PI;
this.aimPower = 30 + Math.random() * 20;
} else {
// Estimate angle for parabolic trajectory
const speed = 6 + Math.random() * 3;
const windComp = game.wind * 2;
const targetAngle = Math.atan2(dy, dx);
if (dx > 0) {
this.aimAngle = Math.PI / 2 + (Math.random() - 0.5) * 0.4;
} else {
this.aimAngle = Math.PI / 2 + Math.PI + (Math.random() - 0.5) * 0.4;
}
// Adjust for distance
if (dist < 200) this.aimPower = 30 + Math.random() * 15;
else if (dist < 400) this.aimPower = 50 + Math.random() * 20;
else this.aimPower = 70 + Math.random() * 20;
// Add some randomness based on difficulty
this.aimAngle += (Math.random() - 0.5) * (1 - this.accuracy) * 0.8;
this.aimPower += (Math.random() - 0.5) * (1 - this.accuracy) * 30;
}
this.targetUnit = target;
}
_pickTarget(enemies, unit) {
// Prioritize low HP enemies, or closest
let best = enemies[0];
let bestScore = -999;
for (const e of enemies) {
const dist = Math.abs(e.x - unit.x);
const score = (100 - e.hp) / 100 + (1 - dist / CONFIG.WORLD_WIDTH);
if (score > bestScore) { bestScore = score; best = e; }
}
return best;
}
shouldMove(unit, game) {
const risk = this._assessRisk(unit, game.terrain);
return risk > 3;
}
getMoveDirection(unit, game) {
const risk = this._assessRisk(unit, game.terrain);
if (risk <= 2) return 0;
// Move away from danger
if (unit.inWater) return 0; // Can't escape water easily
// Find safest direction
let bestDir = 0;
let bestScore = -999;
for (let dir = -1; dir <= 1; dir += 2) {
let score = 0;
for (let d = 10; d < 100; d += 10) {
const cx = unit.x + dir * d;
const surfaceY = game.terrain.getSurfaceY(cx);
const currentSurface = game.terrain.getSurfaceY(unit.x);
if (surfaceY < 0) { score -= 50; break; } // Cliff
if (surfaceY < currentSurface - 5) { score -= 10; } // Higher ground is good
if (surfaceY > currentSurface + 5) { score += 10; } // Lower ground is safer
}
if (score > bestScore) { bestScore = score; bestDir = dir; }
}
return bestDir;
}
}
// ── function: updateUI ──
// UI rendering functions
function updateUI() {
const g = game;
const unit = g._getActiveUnit();
if (!unit) return;
// Turn timer
const timerEl = document.getElementById('timerText');
const timerRing = document.getElementById('timerRing');
const pct = g.turnTimer / g.turnTimerTotal;
timerEl.textContent = Math.ceil(g.turnTimer);
if (pct < 0.25) {
timerRing.style.borderTopColor = '#F44336';
timerRing.style.transform = `scale(${1 + (0.25 - pct) * 4 * Math.sin(performance.now() * 0.01)})`;
timerRing.style.boxShadow = '0 0 10px rgba(244,67,54,0.5)';
} else if (pct < 0.5) {
timerRing.style.borderTopColor = '#FFC107';
timerRing.style.transform = 'scale(1)';
timerRing.style.boxShadow = 'none';
} else {
timerRing.style.borderTopColor = '#4CAF50';
timerRing.style.transform = 'scale(1)';
timerRing.style.boxShadow = 'none';
}
timerRing.style.borderTopWidth = `${pct * 3}px`;
// Wind indicator
const windArrow = document.getElementById('windArrow');
const windValue = document.getElementById('windValue');
const windStr = g.wind > 0 ? '→' : '←';
const windMag = Math.abs(g.wind).toFixed(1);
windValue.textContent = `${windStr} ${windMag}`;
windArrow.style.transform = `scaleX(${g.wind > 0 ? 1 : -1})`;
windArrow.style.width = `${Math.abs(g.wind) * 12 + 10}px`;
// Active unit info
document.getElementById('unitName').textContent = unit.name;
document.getElementById('unitName').style.color = `rgb(${TEAM_COLORS[unit.team].r},${TEAM_COLORS[unit.team].g},${TEAM_COLORS[unit.team].b})`;
const hpPct = unit.hp / unit.maxHp;
document.getElementById('healthFill').style.width = `${hpPct * 100}%`;
document.getElementById('healthFill').style.background = hpPct > 0.5 ? '#4CAF50' : hpPct > 0.25 ? '#FFC107' : '#F44336';
document.getElementById('healthText').textContent = `${unit.hp}/${unit.maxHp}`;
// Power bar
document.getElementById('powerFill').style.width = `${g.aimPower}%`;
document.getElementById('powerValue').textContent = g.aimPower;
// Team list
renderTeamList();
// Weapon slots
renderWeaponSlots(unit);
}
// ── function: renderTeamList ──
function renderTeamList() {
const container = document.getElementById('teamList');
let html = '';
for (const team of game.teams) {
if (!team) continue;
const isActive = team.id === game.currentTeam;
html += `<div class="teamInfo ${isActive ? 'active' : ''}">
<div class="teamColor" style="background:rgb(${team.color.r},${team.color.g},${team.color.b})" data-team="${team.id}"></div>
<span class="teamName">${team.name}</span>
<div class="unitDots">
${team.units.map(u => `<div class="unitDot ${u.alive ? '' : 'dead'}"></div>`).join('')}
</div>
</div>`;
}
container.innerHTML = html;
}
// ── function: renderWeaponSlots ──
function renderWeaponSlots(unit) {
const slots = document.getElementById('weaponSlots');
let html = '<div id="wepSlot">';
const weaponKeys = ['grenade', 'bazooka', 'minigun', 'shotgun', 'catapult', 'holyHand'];
for (const key of weaponKeys) {
const wp = WEAPONS[key];
const count = unit.weapons[key];
const selected = game.selectedWeapon === key ? 'selected' : '';
const onCooldown = count <= 0 ? 'onCooldown' : '';
html += `<div class="weaponSlot ${selected} ${onCooldown}" data-weapon="${key}" onclick="selectWeapon('${key}')">
<span class="wepIcon">${wp.icon}</span>
<span class="wepName">${wp.name}</span>
${count > 0 ? `<span class="wepCount">${count}</span>` : ''}
</div>`;
}
// Utility slots
const utilKeys = ['parachute', 'medkit', 'flag', 'banana'];
for (const key of utilKeys) {
const util = UTILITIES[key];
const count = unit.utilities[key];
const selected = game.selectedUtility === key ? 'selected' : '';
html += `<div class="utilitySlot ${selected}" data-utility="${key}" onclick="selectUtility('${key}')">
<span class="wepIcon">${util.icon}</span>
${count > 0 ? `<span class="wepCount">${count}</span>` : ''}
</div>`;
}
html += '</div>';
slots.innerHTML = html;
}
// ── function: selectWeapon ──
function selectWeapon(key) {
if (game.state !== 'playing') return;
game.audio.uiSelect();
const unit = game._getActiveUnit();
if (!unit || unit.weapons[key] <= 0) return;
game.selectedWeapon = key;
game.selectedUtility = null;
game.aiming = true;
updateUI();
}
// ── function: selectUtility ──
function selectUtility(key) {
if (game.state !== 'playing') return;
game.audio.uiSelect();
const unit = game._getActiveUnit();
if (!unit || unit.utilities[key] <= 0) return;
game.selectedUtility = key;
game.selectedWeapon = null;
game.aiming = false;
game.useUtility();
game.updateUI();
}
// ── let: mouseDown ──
let mouseDown = false;
// ── let: mouseX ──
let mouseX = 0;
// ── let: mouseY ──
let mouseY = 0;
// ── const: keysDown ──
const keysDown = {};
// ── function: updateMouse ──
function updateMouse(e) {
const rect = game.canvas.getBoundingClientRect();
const scaleX = game.canvas.width / rect.width;
const scaleY = game.canvas.height / rect.height;
mouseX = (e.clientX - rect.left) * scaleX;
mouseY = (e.clientY - rect.top) * scaleY;
if (game.state === 'playing' && game.aiming && !game.aiControllers[game.currentTeam]) {
const unit = game._getActiveUnit();
if (unit) {
const worldMouseX = (mouseX - game.canvas.width / 2) / game.camera.zoom + game.camera.x;
const worldMouseY = (mouseY - game.canvas.height / 2) / game.camera.zoom + game.camera.y;
game.aimAngle = Math.atan2(worldMouseY - (unit.y - 12), worldMouseX - unit.x);
if (worldMouseY > unit.y) {
game.aimAngle = worldMouseX > unit.x ? 0.1 : -0.1;
}
const dist = Math.sqrt((worldMouseX - unit.x) ** 2 + (worldMouseY - unit.y) ** 2);
game.aimPower = Math.min(80, Math.max(10, dist / 8));
}
}
}
// ── function: setupInput ──
function setupInput() {
const canvas = game.canvas;
canvas.addEventListener('mousedown', (e) => {
mouseDown = true;
updateMouse(e);
if (game.state === 'playing' && game.aiming && !game.aiControllers[game.currentTeam]) {
game.shoot();
} else if (game.state === 'playing' && !game.aiming && game.selectedUtility && !game.aiControllers[game.currentTeam]) {
game.useUtility();
}
});
canvas.addEventListener('mouseup', () => { mouseDown = false; });
canvas.addEventListener('mousemove', (e) => { updateMouse(e); });
canvas.addEventListener('contextmenu', (e) => { e.preventDefault(); });
document.addEventListener('keydown', (e) => {
keysDown[e.key] = true;
game.audio.resume();
if (game.state !== 'playing') return;
const isCpu = !!game.aiControllers[game.currentTeam];
if (isCpu) return;
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault();
if (game.aiming) game.shoot();
else {
if (!game.moving) {
game.moving = true;
game.audio.movePress();
}
}
}
if (e.key === 'Shift') {
e.preventDefault();
const unit = game._getActiveUnit();
if (unit) unit.jump();
}
if (e.key === 'Escape') {
game.aiming = false;
game.selectedUtility = null;
}
if (e.key === '1') selectWeapon('grenade');
if (e.key === '2') selectWeapon('bazooka');
if (e.key === '3') selectWeapon('minigun');
if (e.key === '4') selectWeapon('shotgun');
if (e.key === '5') selectWeapon('catapult');
if (e.key === '6') selectWeapon('holyHand');
});
document.addEventListener('keyup', (e) => {
keysDown[e.key] = false;
});
document.getElementById('fireBtn')?.addEventListener('click', () => {
if (game.aiming) game.shoot();
else { game.aiming = true; game.audio.movePress(); }
});
document.getElementById('skipBtn')?.addEventListener('click', () => {
game.turnTimer = 0;
game.audio.turnEnd();
game.updateUI();
});
document.getElementById('startBtn')?.addEventListener('click', () => {
game.audio.resume();
game.audio.init();
const pc = parseInt(document.getElementById('playerSelect').value);
const tt = document.getElementById('terrainSelectBox').value;
const diff = document.getElementById('diffSelect').value;
const ptypes = [];
for (let i = 0; i < pc; i++) {
const sel = document.querySelector(`.playerType[data-player="${i}"]`);
ptypes.push(sel ? sel.value : 'cpu');
}
game.init(pc, tt, diff, ptypes);
});
document.getElementById('restartBtn')?.addEventListener('click', () => {
document.getElementById('menuOverlay').style.display = 'flex';
document.getElementById('gameOverOverlay').style.display = 'none';
});
}
// ── let: game ──
let game;
// ── function: extendGameEngine ──
function extendGameEngine() {
GameEngine.prototype.update = function(dt) {
if (this.state !== 'playing') return;
const unit = this._getActiveUnit();
const isCpu = unit ? !!this.aiControllers[unit.team] : false;
if (this.freezeFrame > 0) { this.freezeFrame--; return; }
this.windTimer -= dt;
if (this.windTimer <= 0) {
this.windTarget = (Math.random() - 0.5) * CONFIG.WIND_MAX * 2;
this.windTimer = 3 + Math.random() * 5;
}
this.wind += (this.windTarget - this.wind) * CONFIG.WIND_CHANGE_RATE;
this.turnTimer -= dt;
this.turnTimerCritical = this.turnTimer < 5;
if (this.turnTimerCritical) {
const currentBeat = Math.floor(this.turnTimer * 2);
const prevBeat = Math.floor((this.turnTimer + dt) * 2);
if (currentBeat !== prevBeat) this.audio.turnTick();
}
if (this.turnTimer <= 0) {
this.audio.turnEnd();
this._nextTurn();
return;
}
if (isCpu && unit.alive) this._handleAI();
for (const u of this.units) {
if (!u.alive) continue;
if (u.inWater && !u.isParachuting) u.takeDamage(CONFIG.WATER_DAMAGE_PER_SEC * dt * 0.1);
if (this.bananaAt) {
const dx = u.x - this.bananaAt.x, dy = u.y - this.bananaAt.y;
if (dx * dx + dy * dy < 100 && u.onGround) {
u.vx += u.facing * 5;
const bx = this.bananaAt.x;
this.bananaAt = null;
this.audio.playTone(200, 0.15, 'sine', 0.2, -100);
this.floatingTexts.push(new FloatingText(u.x, u.y - 30, 'SLIP!', '#FFD700', 20));
}
}
u.update(dt, this.terrain, this.wind);
}
for (let i = this.projectiles.length - 1; i >= 0; i--) {
this.projectiles[i].update(dt, this.terrain, this.wind);
if (!this.projectiles[i].alive) this.projectiles.splice(i, 1);
}
this.particles.update(dt, this.wind);
for (let i = this.floatingTexts.length - 1; i >= 0; i--) {
this.floatingTexts[i].update(dt);
if (!this.floatingTexts[i].active) this.floatingTexts.splice(i, 1);
}
if (unit) this.camera.update(unit);
const aliveCount = this.units.filter(u => u.alive).length;
this.camera.targetZoom = aliveCount > 10 ? 0.7 : aliveCount > 6 ? 0.85 : 0.95;
updateUI();
};
GameEngine.prototype._handleAI = function() {
const unit = this._getActiveUnit();
const ai = this.aiControllers[unit.team];
if (!ai) return;
// Movement
if (ai.shouldMove(unit, this)) {
const dir = ai.getMoveDirection(unit, this);
if (dir !== 0 && Math.abs(unit.vx) < 1) {
unit.move(dir);
this.audio.movePress();
}
}
// Attack
if (!this.aiming) {
ai.update(unit, this);
if (ai.targetUnit) {
const available = Object.keys(WEAPONS).filter(w => unit.weapons[w] > 0);
if (available.length > 0) {
this.aimAngle = ai.aimAngle;
this.aimPower = Math.max(10, Math.min(80, ai.aimPower));
this.selectedWeapon = available[0];
this.aiming = true;
const delay = 300 + Math.random() * 500;
setTimeout(() => {
if (this.state === 'playing' && unit.alive && this._getActiveUnit() === unit) {
this.shoot();
}
}, delay);
}
}
}
};
GameEngine.prototype.render = function() {
const ctx = this.ctx;
const cw = this.canvasW;
const ch = this.canvasH;
ctx.clearRect(0, 0, cw, ch);
// Sky
const skyGrad = ctx.createLinearGradient(0, 0, 0, ch);
skyGrad.addColorStop(0, '#1a0533');
skyGrad.addColorStop(0.4, '#2d1b69');
skyGrad.addColorStop(0.8, '#4a2c8a');
skyGrad.addColorStop(1, '#1a1a2e');
ctx.fillStyle = skyGrad;
ctx.fillRect(0, 0, cw, ch);
// Stars
ctx.fillStyle = 'rgba(255,255,255,0.6)';
for (let i = 0; i < 50; i++) {
ctx.fillRect((i * 137.5) % cw, (i * 73.1) % (ch * 0.5), 0.5 + (i % 3) * 0.5, 0.5 + (i % 3) * 0.5);
}
if (!this.camera) return;
if (!this.terrain) return;
this.camera.applyToContext(ctx, cw, ch);
this._renderTerrain(ctx);
this._renderWater(ctx);
// Banana
if (this.bananaAt) {
ctx.font = '16px serif';
ctx.fillText('🍌', this.bananaAt.x - 8, this.bananaAt.y + 5);
}
// Projectile trails
for (const proj of this.projectiles) {
for (let i = 1; i < proj.trail.length; i++) {
const t = proj.trail[i];
const a = t.life / 0.3;
ctx.fillStyle = 'rgba(255,200,50,' + (a * 0.3) + ')';
ctx.beginPath();
ctx.arc(t.x, t.y, 2 * a, 0, Math.PI * 2);
ctx.fill();
}
}
// Projectiles
for (const proj of this.projectiles) this._renderProjectile(ctx, proj);
// Units
for (const unit of this.units) {
if (unit.alive) this._renderUnit(ctx, unit);
}
// Trajectory
if (this.aiming && !this.aiControllers[this.currentTeam] && this.selectedWeapon && this.selectedWeapon !== 'holyHand') {
this._renderTrajectory(ctx);
}
// Particles
this._renderParticles(ctx);
// Floating texts
for (const ft of this.floatingTexts) {
ctx.save();
ctx.globalAlpha = ft.alpha;
ctx.font = 'bold ' + ft.size + 'px Arial';
ctx.fillStyle = ft.color;
ctx.strokeStyle = 'rgba(0,0,0,0.5)';
ctx.lineWidth = 3;
ctx.textAlign = 'center';
ctx.strokeText(ft.text, ft.x, ft.y);
ctx.fillText(ft.text, ft.x, ft.y);
ctx.restore();
}
this.camera.undoContext(ctx);
this._renderMinimap(ctx);
};
}
// ── class: GameEngine ──
class GameEngine {
constructor() {
this.state = 'menu';
this.terrain = null;
this.teams = [];
this.units = [];
this.currentTeam = 0;
this.currentUnit = 0;
this.projectiles = [];
this.particles = new ParticleSystem();
this.floatingTexts = [];
this.camera = null;
this.audio = new AudioSystem();
this.aiControllers = {};
this.wind = 0;
this.windTarget = 0;
this.windTimer = 0;
this.turnTimer = CONFIG.TURN_TIME;
this.turnTimerTotal = CONFIG.TURN_TIME;
this.turnTimerCritical = false;
this.selectedWeapon = null;
this.selectedUtility = null;
this.aimAngle = 0;
this.aimPower = 50;
this.aiming = false;
this.moving = false;
this.freezeFrame = 0;
this.shakeMag = 0;
this.terrainType = 'single';
this.difficulty = 'medium';
this.playerTypes = ['human', 'cpu', 'cpu', 'cpu'];
this.bananaAt = null;
this.canvas = document.getElementById('gameCanvas');
this.ctx = this.canvas.getContext('2d');
this.canvasW = 1280;
this.canvasH = 720;
}
init(playerCount, terrainType, difficulty, playerTypes) {
this.terrainType = terrainType;
this.difficulty = difficulty;
this.playerTypes = playerTypes;
this.terrain = new Terrain(CONFIG.WORLD_WIDTH, CONFIG.WORLD_HEIGHT, CONFIG.WORLD_HEIGHT * 0.9);
this.terrain.generate(terrainType);
if (terrainType === 'double') this.terrain.waterLevel = CONFIG.WORLD_HEIGHT * 0.85;
else if (terrainType === 'single') this.terrain.waterLevel = CONFIG.WORLD_HEIGHT * 0.95;
this.units = [];
this.teams = [];
let uid = 0;
const positions = this.terrain.findSpawnPositions(playerCount * CONFIG.UNITS_PER_TEAM);
for (let t = 0; t < playerCount; t++) {
const team = { id: t, color: TEAM_COLORS[t], name: TEAM_COLORS[t].name, aliveCount: CONFIG.UNITS_PER_TEAM, units: [] };
for (let u = 0; u < CONFIG.UNITS_PER_TEAM; u++) {
const idx = t * CONFIG.UNITS_PER_TEAM + u;
const pos = positions[Math.min(idx, positions.length - 1)];
const unit = new Unit(uid++, t, TEAM_WORM_NAMES[t][u] || 'Worm ' + (u + 1), pos.x, pos.y);
unit.weapons = { grenade: 3, bazooka: 2, minigun: 2, shotgun: 2, catapult: 1, holyHand: 1 };
unit.utilities = { parachute: 1, medkit: 1, flag: 1, banana: 1 };
unit.teamObj = team;
team.units.push(unit);
this.units.push(unit);
}
this.teams.push(team);
}
this.currentTeam = 0;
this.currentUnit = this._findNextAliveUnit();
this.camera = new Camera(this.units[0].x, this.units[0].y, this.units[0]);
this.turnTimer = CONFIG.TURN_TIME;
this.turnTimerTotal = CONFIG.TURN_TIME;
this.wind = (Math.random() - 0.5) * 4;
this.windTarget = this.wind;
this.windTimer = 0;
this.aiControllers = {};
for (let t = 0; t < playerCount; t++) {
if (playerTypes[t] === 'cpu') this.aiControllers[t] = new AIController(difficulty);
}
this.audio.init();
this.state = 'playing';
this.selectedWeapon = 'grenade';
this.selectedUtility = null;
document.getElementById('menuOverlay').style.display = 'none';
document.getElementById('gameOverOverlay').style.display = 'none';
this.showTurnMessage(this._getActiveUnit());
updateUI();
}
_getActiveUnit() {
if (this.currentUnit < 0 || this.currentUnit >= this.units.length) return null;
return this.units[this.currentUnit];
}
_findNextAliveUnit() {
const tu = this.teams[this.currentTeam]?.units || [];
for (let i = 0; i < tu.length; i++) { if (tu[i].alive) return this.units.indexOf(tu[i]); }
return -1;
}
_nextTurn() {
const tu = this.teams[this.currentTeam]?.units || [];
if (!tu.some(u => u.alive)) { this.teams[this.currentTeam] = null; this._advanceToNextTeam(); return; }
const ci = tu.indexOf(this._getActiveUnit());
let found = false;
for (let i = 1; i <= tu.length; i++) {
const idx = (ci + i) % tu.length;
if (tu[idx].alive) { this.currentUnit = this.units.indexOf(tu[idx]); found = true; break; }
}
if (!found) this._advanceToNextTeam();
this.turnTimer = CONFIG.TURN_TIME;
this.turnTimerTotal = CONFIG.TURN_TIME;
this.selectedUtility = null;
this.aiming = false;
this.moving = false;
this._checkWinCondition();
if (this.state === 'playing') { this.showTurnMessage(this._getActiveUnit()); updateUI(); }
}
_advanceToNextTeam() {
if (this.teams.filter(t => t !== null).length <= 1) { this._checkWinCondition(); return; }
let next = (this.currentTeam + 1) % this.teams.length, tries = 0;
while (this.teams[next] === null && tries < this.teams.length) { next = (next + 1) % this.teams.length; tries++; }
this.currentTeam = next;
this.currentUnit = this._findNextAliveUnit();
}
_checkWinCondition() {
const at = this.teams.filter(t => t && t.units.some(u => u.alive));
if (at.length <= 1) {
this.state = 'gameOver';
const w = at[0];
document.getElementById('winnerText').textContent = w ? w.name + ' Team Wins!' : 'Draw!';
document.getElementById('gameOverOverlay').style.display = 'flex';
this.audio.playTone(440, 0.3, 'sine', 0.5);
setTimeout(() => this.audio.playTone(550, 0.3, 'sine', 0.5), 200);
setTimeout(() => this.audio.playTone(660, 0.5, 'sine', 0.5), 400);
}
}
showTurnMessage(unit) {
if (!unit) return;
const el = document.getElementById('turnMessage');
el.textContent = unit.name + "'s Turn";
el.style.color = 'rgb(' + TEAM_COLORS[unit.team].r + ',' + TEAM_COLORS[unit.team].g + ',' + TEAM_COLORS[unit.team].b + ')';
el.classList.add('show');
setTimeout(() => el.classList.remove('show'), 1500);
}
onUnitDeath(unit) {
game.audio.death();
game.particles.explosion(unit.x, unit.y, 20, TEAM_COLORS[unit.team]);
game.floatingTexts.push(new FloatingText(unit.x, unit.y - 30, 'R.I.P.', '#FFFFFF', 24));
const team = game.teams[unit.team];
if (team) team.aliveCount = team.units.filter(u => u.alive).length;
setTimeout(() => game._checkWinCondition(), 500);
}
shoot() {
const unit = this._getActiveUnit();
if (!unit || !unit.alive || !this.selectedWeapon) return;
const wp = WEAPONS[this.selectedWeapon];
if (unit.weapons[this.selectedWeapon] <= 0) return;
unit.weapons[this.selectedWeapon]--;
this.aiming = false;
wp.fire(unit, this.aimAngle, this.aimPower, this.terrain, this.projectiles);
updateUI();
}
useUtility() {
if (!this.selectedUtility) return;
const unit = this._getActiveUnit();
if (!unit || !unit.alive) return;
const util = UTILITIES[this.selectedUtility];
if (!util || unit.utilities[this.selectedUtility] <= 0) return;
unit.utilities[this.selectedUtility]--;
util.use(unit);
this.selectedUtility = null;
updateUI();
}
}
// ── main ──
// Initialize game
game = new GameEngine();
extendGameEngine();
extendGameEngineRender();
function resizeCanvas() {
const canvas = document.getElementById('gameCanvas');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
game.canvasW = canvas.width;
game.canvasH = canvas.height;
}
window.addEventListener('resize', resizeCanvas);
resizeCanvas();
setupInput();
function handleMovement() {
if (game.state !== 'playing') return;
const isCpu = !!game.aiControllers[game.currentTeam];
if (isCpu) return;
const unit = game._getActiveUnit();
if (!unit || !unit.alive) return;
if (keysDown['ArrowLeft'] || keysDown['a']) {
unit.move(-1);
} else if (keysDown['ArrowRight'] || keysDown['d']) {
unit.move(1);
} else if (!game.moving) {
unit.vx *= 0.8;
}
}
let lastTimestamp = 0;
function gameLoop(timestamp) {
const dt = Math.min((timestamp - lastTimestamp) / 1000, 0.05);
lastTimestamp = timestamp;
handleMovement();
game.update(dt);
game.render();
requestAnimationFrame(gameLoop);
}
requestAnimationFrame(gameLoop);
</script>
</body>
</html>