← Worms results

Worms

Qwen 3.6 27b · composer

Initial view of Worms

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>