← Worms results

Worms

Qwen 3.6 27b MTP · composer

Initial view of Worms

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 117,854 bytes · SHA-256 86aeb7c93e4c
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Worms Artillery Battle</title>
<style>
/* ── css: layout ── */
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; background: #0a0a1a; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; }
#app { position: relative; width: 100%; height: 100%; }
#gameCanvas { display: block; width: 100%; height: 100%; image-rendering: pixelated; }
#uiOverlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; }
.screen { position: absolute; top: 0; left: 0; width: 100%; height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; pointer-events: auto; transition: opacity 0.3s; }
.screen.hidden { opacity: 0; pointer-events: none !important; }
.screen.active { opacity: 1; }

#menuScreen { background: radial-gradient(ellipse at center, #1a1a3e 0%, #0a0a1a 70%); }
#titleText { font-size: 4em; color: #ff6b35; text-shadow: 0 0 20px rgba(255,107,53,0.5), 0 4px 8px rgba(0,0,0,0.8); margin-bottom: 40px; letter-spacing: 8px; animation: titlePulse 2s ease-in-out infinite; }
@keyframes titlePulse { 0%,100% { transform: scale(1); text-shadow: 0 0 20px rgba(255,107,53,0.5), 0 4px 8px rgba(0,0,0,0.8); } 50% { transform: scale(1.05); text-shadow: 0 0 40px rgba(255,107,53,0.8), 0 4px 8px rgba(0,0,0,0.8); } }
#menuOptions button { font-size: 1.5em; padding: 15px 50px; margin: 10px; border: 2px solid #ff6b35; background: rgba(255,107,53,0.1); color: #fff; cursor: pointer; transition: all 0.2s; letter-spacing: 3px; text-transform: uppercase; }
#menuOptions button:hover { background: rgba(255,107,53,0.4); transform: scale(1.05); box-shadow: 0 0 20px rgba(255,107,53,0.3); }

#setupScreen { background: radial-gradient(ellipse at center, #1a1a3e 0%, #0a0a1a 70%); }
#setupScreen h2 { font-size: 2.5em; color: #ff6b35; margin-bottom: 30px; text-shadow: 0 0 10px rgba(255,107,53,0.3); }
#setupOptions label { display: block; font-size: 1.2em; color: #ccc; margin: 10px 0; }
#setupOptions select { padding: 8px 15px; background: rgba(255,255,255,0.1); border: 1px solid #ff6b35; color: #fff; font-size: 1em; cursor: pointer; }
#teamSetup { margin: 20px 0; }
.teamRow { display: flex; align-items: center; gap: 10px; margin: 8px 0; }
.teamRow label { color: #ccc; font-size: 1em; min-width: 60px; }
.teamRow input, .teamRow select { padding: 5px 10px; background: rgba(255,255,255,0.1); border: 1px solid #666; color: #fff; font-size: 0.9em; }
#btnStart { font-size: 1.3em; padding: 12px 40px; margin-top: 20px; border: 2px solid #ff6b35; background: rgba(255,107,53,0.2); color: #fff; cursor: pointer; letter-spacing: 2px; }
#btnStart:hover { background: rgba(255,107,53,0.5); transform: scale(1.05); }

#hud { justify-content: space-between; padding: 0; pointer-events: none; }
#topBar { display: flex; align-items: center; justify-content: space-between; width: 100%; padding: 10px 20px; background: linear-gradient(to bottom, rgba(0,0,0,0.7), transparent); pointer-events: auto; }
#turnInfo { font-size: 1.4em; color: #fff; text-shadow: 0 2px 4px rgba(0,0,0,0.8); }
#windIndicator { font-size: 1em; color: #aaa; }
#windValue { font-weight: bold; }
#timerContainer { display: flex; align-items: center; gap: 10px; }
#timerText { font-size: 1.2em; color: #fff; min-width: 40px; text-align: right; }

#bottomBar { display: flex; align-items: flex-end; justify-content: space-between; width: 100%; padding: 15px 20px; background: linear-gradient(to top, rgba(0,0,0,0.7), transparent); pointer-events: auto; }
#unitInfo { display: flex; align-items: center; gap: 10px; }
#wormName { font-size: 1.3em; color: #fff; text-shadow: 0 2px 4px rgba(0,0,0,0.8); min-width: 120px; }
#healthBar { width: 150px; height: 16px; background: rgba(0,0,0,0.5); border: 1px solid #444; border-radius: 3px; overflow: hidden; }
#healthFill { height: 100%; background: linear-gradient(to right, #ff4444, #44ff44); transition: width 0.3s; border-radius: 2px; }
#healthText { font-size: 1em; color: #fff; min-width: 35px; text-align: center; }

#actionButtons { display: flex; gap: 8px; }
#actionButtons button { padding: 10px 20px; border: 2px solid rgba(255,255,255,0.3); background: rgba(0,0,0,0.4); color: #fff; cursor: pointer; font-size: 0.9em; letter-spacing: 1px; transition: all 0.15s; }
#actionButtons button:hover { border-color: #ff6b35; background: rgba(255,107,53,0.3); transform: scale(1.05); }

#wheelCanvas { cursor: crosshair; }

#gameOverScreen { background: radial-gradient(ellipse at center, #1a1a3e 0%, #0a0a1a 70%); }
#gameOverScreen h2 { font-size: 3em; color: #ff6b35; margin-bottom: 20px; text-shadow: 0 0 20px rgba(255,107,53,0.5); }
#winnerText { font-size: 1.8em; color: #fff; margin-bottom: 30px; }
#btnRestart { font-size: 1.3em; padding: 12px 40px; border: 2px solid #ff6b35; background: rgba(255,107,53,0.2); color: #fff; cursor: pointer; letter-spacing: 2px; }
#btnRestart:hover { background: rgba(255,107,53,0.5); transform: scale(1.05); }

.hidden { display: none !important; }
</style>
</head>
<body>
<canvas id="gameCanvas"></canvas>
<div id="uiOverlay">
  <div id="menuScreen" class="screen active">
    <h1 id="titleText">WORMS BATTLE</h1>
    <div id="menuOptions">
      <button id="btnQuickPlay" data-sound="click">QUICK PLAY</button>
      <button id="btnSetup" data-sound="click">CUSTOM SETUP</button>
    </div>
  </div>
  <div id="setupScreen" class="screen hidden">
    <h2>BATTLE SETUP</h2>
    <div id="setupOptions">
      <label>Teams: <select id="teamCount"><option value="2">2</option><option value="3">3</option><option value="4">4</option></select></label>
      <label>Terrain: <select id="terrainType"><option value="island">Single Island</option><option value="doubleIsland">Double Island</option><option value="cave">Cave</option></select></label>
      <div id="teamSetup"></div>
    </div>
    <button id="btnStart" data-sound="click">START BATTLE!</button>
  </div>
  <div id="hud" class="screen hidden">
    <div id="topBar">
      <div id="turnInfo"><span id="currentTeamName"></span></div>
      <div id="windIndicator">Wind: <span id="windValue">0</span></div>
      <div id="timerContainer"><canvas id="timerCanvas" width="200" height="30"></canvas><span id="timerText">15s</span></div>
    </div>
    <div id="bottomBar">
      <div id="unitInfo">
        <div id="wormName"></div>
        <div id="healthBar"><div id="healthFill"></div></div>
        <div id="healthText">100</div>
      </div>
      <div id="weaponWheel" class="hidden">
        <canvas id="wheelCanvas" width="320" height="200"></canvas>
      </div>
      <div id="actionButtons">
        <button id="btnMove" data-sound="click">MOVE</button>
        <button id="btnAttack" data-sound="click">ATTACK</button>
        <button id="btnItem" data-sound="click">ITEMS</button>
        <button id="btnSkip" data-sound="click">SKIP</button>
      </div>
    </div>
  </div>
  <div id="gameOverScreen" class="screen hidden">
    <h2>BATTLE OVER!</h2>
    <div id="winnerText"></div>
    <button id="btnRestart" data-sound="click">PLAY AGAIN</button>
  </div>
</div>
<script>
'use strict';

// ── const: CONFIG ──
const CONFIG = {
  WORLD_WIDTH: 2400,
  WORLD_HEIGHT: 800,
  GRAVITY: 980,
  UNIT_RADIUS: 12,
  UNIT_SPEED: 150,
  JUMP_FORCE: -380,
  FALL_DAMAGE_THRESHOLD: 400,
  TURN_TIME: 20,
  MAX_HEALTH: 100,
  WATER_LEVEL_OFFSET: 600,
  TERRAIN_RESOLUTION: 2,
};

// ── const: TEAM_COLORS ──
const TEAM_COLORS = [
  { primary: '#ff4444', secondary: '#cc3333', name: 'Red Team' },
  { primary: '#4488ff', secondary: '#3366cc', name: 'Blue Team' },
  { primary: '#44dd44', secondary: '#33aa33', name: 'Green Team' },
  { primary: '#ffaa00', secondary: '#cc8800', name: 'Yellow Team' },
];

// ── const: WORM_NAMES ──
const WORM_NAMES = [
  'Sir Worms Alot', 'Wormzilla', 'Baron von Worm', 'Colonel Mustard',
  'Wormy McWormface', 'General Noodle', 'Captain Crunch', 'Dr. Wormington',
  'Major Disaster', 'Private Snuggles', 'Sergeant Pepper', 'Lieutenant Dan',
  'Bombastic Chuck', 'Explosive Eddie', 'Rocket Ron', 'Grenade Gary',
];

// ── const: WEAPONS ──
const WEAPONS = [
  { id: 'rifle', name: 'Rifle', type: 'indirect', power: 30, radius: 40, speed: 700, ammoCost: 1, icon: '🔫', desc: 'Standard rifle shot' },
  { id: 'rocket', name: 'Rocket Launcher', type: 'indirect', power: 60, radius: 70, speed: 550, ammoCost: 2, icon: '🚀', desc: 'High damage rocket' },
  { id: 'homingMissile', name: 'Homing Missile', type: 'indirect', power: 50, radius: 60, speed: 450, ammoCost: 2, icon: '💨', desc: 'Locks onto nearest enemy' },
  { id: 'shotgun', name: 'Shotgun', type: 'direct', power: 25, radius: 30, speed: 900, ammoCost: 1, icon: '🔥', desc: 'Close range blast (3 pellets)' },
  { id: 'minigun', name: 'Minigun', type: 'direct', power: 15, radius: 20, speed: 800, ammoCost: 1, icon: '⚡', desc: 'Rapid fire (5 shots)' },
  { id: 'superSaw', name: 'Super Saw', type: 'direct', power: 40, radius: 35, speed: 250, ammoCost: 2, icon: '🪚', desc: 'Flying buzzsaw' },
  { id: 'clusterBomb', name: 'Cluster Bomb', type: 'aoe', power: 80, radius: 100, speed: 600, ammoCost: 3, icon: '💣', desc: 'Splits into sub-bombs' },
  { id: 'earthquake', name: 'Earthquake', type: 'aoe', power: 40, radius: 200, speed: 0, ammoCost: 3, icon: '🌋', desc: 'Shakes the ground' },
];

// ── const: ITEMS ──
const ITEMS = [
  { id: 'parachute', name: 'Parachute', icon: '🪂', desc: 'Slow fall for next jump', effect: 'slowFall' },
  { id: 'extraTime', name: 'Extra Time', icon: '⏰', desc: '+10 seconds on timer', effect: 'addTime' },
  { id: 'windGauge', name: 'Wind Gauge', icon: '🌬️', desc: 'Reveal wind direction', effect: 'showWind' },
  { id: 'medikit', name: 'Medikit', icon: '💊', desc: '+30 health', effect: 'heal' },
  { id: 'mine', name: 'Land Mine', icon: '💀', desc: 'Place explosive mine', effect: 'placeMine' },
];

// ── const: TERRAIN_TYPES ──
const TERRAIN_TYPES = ['island', 'doubleIsland', 'cave'];

// ── class: AudioManager ──
class AudioManager {
  constructor() {
    this.ctx = null;
    this.initialized = false;
  }
  
  init() {
    if (this.initialized) return;
    try {
      this.ctx = new (window.AudioContext || window.webkitAudioContext)();
      this.initialized = true;
    } catch(e) { console.warn('Web Audio not available'); }
  }
  
  play(type, params = {}) {
    if (!this.ctx) return;
    const t = this.ctx.currentTime;
    switch(type) {
      case 'click': this._click(t); break;
      case 'explosion': this._explosion(t, params); break;
      case 'hit': this._hit(t, params); break;
      case 'jump': this._jump(t); break;
      case 'laugh': this._laugh(t); break;
      case 'death': this._death(t); break;
      case 'tick': this._tick(t, params); break;
      case 'whoosh': this._whoosh(t); break;
      case 'splash': this._splash(t); break;
      case 'select': this._select(t); break;
      case 'warning': this._warning(t); break;
    }
  }
  
  _click(t) {
    const o = this.ctx.createOscillator();
    const g = this.ctx.createGain();
    o.connect(g); g.connect(this.ctx.destination);
    o.type = 'square';
    o.frequency.setValueAtTime(800, t);
    o.frequency.exponentialRampToValueAtTime(400, t + 0.05);
    g.gain.setValueAtTime(0.15, t);
    g.gain.exponentialRampToValueAtTime(0.001, t + 0.08);
    o.start(t); o.stop(t + 0.08);
  }
  
  _explosion(t, { size = 'medium' } = {}) {
    const dur = size === 'large' ? 0.6 : size === 'small' ? 0.2 : 0.35;
    const vol = size === 'large' ? 0.4 : size === 'small' ? 0.1 : 0.2;
    // Noise burst
    const bufSize = this.ctx.sampleRate * dur;
    const buf = this.ctx.createBuffer(1, bufSize, this.ctx.sampleRate);
    const data = buf.getChannelData(0);
    for (let i = 0; i < bufSize; i++) {
      data[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / bufSize, 2);
    }
    const src = this.ctx.createBufferSource();
    src.buffer = buf;
    const filt = this.ctx.createBiquadFilter();
    filt.type = 'lowpass';
    filt.frequency.setValueAtTime(size === 'large' ? 800 : 1200, t);
    filt.frequency.exponentialRampToValueAtTime(100, t + dur);
    const g = this.ctx.createGain();
    g.gain.setValueAtTime(vol, t);
    g.gain.exponentialRampToValueAtTime(0.001, t + dur);
    src.connect(filt); filt.connect(g); g.connect(this.ctx.destination);
    src.start(t); src.stop(t + dur);
    // Low boom
    const o = this.ctx.createOscillator();
    const g2 = this.ctx.createGain();
    o.type = 'sine';
    o.frequency.setValueAtTime(80, t);
    o.frequency.exponentialRampToValueAtTime(30, t + dur * 0.7);
    g2.gain.setValueAtTime(vol * 1.5, t);
    g2.gain.exponentialRampToValueAtTime(0.001, t + dur * 0.8);
    o.connect(g2); g2.connect(this.ctx.destination);
    o.start(t); o.stop(t + dur);
  }
  
  _hit(t, { heavy = false } = {}) {
    const o = this.ctx.createOscillator();
    const g = this.ctx.createGain();
    o.type = 'sawtooth';
    o.frequency.setValueAtTime(heavy ? 200 : 400, t);
    o.frequency.exponentialRampToValueAtTime(100, t + 0.15);
    g.gain.setValueAtTime(heavy ? 0.2 : 0.12, t);
    g.gain.exponentialRampToValueAtTime(0.001, t + 0.15);
    o.connect(g); g.connect(this.ctx.destination);
    o.start(t); o.stop(t + 0.15);
  }
  
  _jump(t) {
    const o = this.ctx.createOscillator();
    const g = this.ctx.createGain();
    o.type = 'sine';
    o.frequency.setValueAtTime(300, t);
    o.frequency.exponentialRampToValueAtTime(600, t + 0.15);
    g.gain.setValueAtTime(0.12, t);
    g.gain.exponentialRampToValueAtTime(0.001, t + 0.2);
    o.connect(g); g.connect(this.ctx.destination);
    o.start(t); o.stop(t + 0.2);
  }
  
  _laugh(t) {
    for (let i = 0; i < 4; i++) {
      const o = this.ctx.createOscillator();
      const g = this.ctx.createGain();
      o.type = 'sawtooth';
      const st = t + i * 0.12;
      o.frequency.setValueAtTime(300 + Math.random() * 200, st);
      o.frequency.linearRampToValueAtTime(400 + Math.random() * 150, st + 0.06);
      o.frequency.linearRampToValueAtTime(250 + Math.random() * 100, st + 0.1);
      g.gain.setValueAtTime(0, st);
      g.gain.linearRampToValueAtTime(0.08, st + 0.03);
      g.gain.exponentialRampToValueAtTime(0.001, st + 0.12);
      o.connect(g); g.connect(this.ctx.destination);
      o.start(st); o.stop(st + 0.12);
    }
  }
  
  _death(t) {
    const o = this.ctx.createOscillator();
    const g = this.ctx.createGain();
    o.type = 'sawtooth';
    o.frequency.setValueAtTime(500, t);
    o.frequency.exponentialRampToValueAtTime(80, t + 0.6);
    g.gain.setValueAtTime(0.15, t);
    g.gain.exponentialRampToValueAtTime(0.001, t + 0.6);
    o.connect(g); g.connect(this.ctx.destination);
    o.start(t); o.stop(t + 0.6);
  }
  
  _tick(t, { urgent = false } = {}) {
    const o = this.ctx.createOscillator();
    const g = this.ctx.createGain();
    o.type = 'square';
    o.frequency.setValueAtTime(urgent ? 1200 : 600, t);
    g.gain.setValueAtTime(urgent ? 0.15 : 0.08, t);
    g.gain.exponentialRampToValueAtTime(0.001, t + (urgent ? 0.1 : 0.06));
    o.connect(g); g.connect(this.ctx.destination);
    o.start(t); o.stop(t + 0.12);
  }
  
  _whoosh(t) {
    const bufSize = this.ctx.sampleRate * 0.3;
    const buf = this.ctx.createBuffer(1, bufSize, this.ctx.sampleRate);
    const data = buf.getChannelData(0);
    for (let i = 0; i < bufSize; i++) {
      data[i] = (Math.random() * 2 - 1) * Math.sin(i / bufSize * Math.PI);
    }
    const src = this.ctx.createBufferSource();
    src.buffer = buf;
    const filt = this.ctx.createBiquadFilter();
    filt.type = 'bandpass';
    filt.frequency.setValueAtTime(2000, t);
    filt.Q.value = 5;
    const g = this.ctx.createGain();
    g.gain.setValueAtTime(0.1, t);
    src.connect(filt); filt.connect(g); g.connect(this.ctx.destination);
    src.start(t); src.stop(t + 0.3);
  }
  
  _splash(t) {
    const bufSize = this.ctx.sampleRate * 0.4;
    const buf = this.ctx.createBuffer(1, bufSize, this.ctx.sampleRate);
    const data = buf.getChannelData(0);
    for (let i = 0; i < bufSize; i++) {
      data[i] = (Math.random() * 2 - 1) * Math.pow(Math.sin(i / bufSize * Math.PI), 0.5);
    }
    const src = this.ctx.createBufferSource();
    src.buffer = buf;
    const filt = this.ctx.createBiquadFilter();
    filt.type = 'highpass';
    filt.frequency.value = 2000;
    const g = this.ctx.createGain();
    g.gain.setValueAtTime(0.15, t);
    g.gain.exponentialRampToValueAtTime(0.001, t + 0.4);
    src.connect(filt); filt.connect(g); g.connect(this.ctx.destination);
    src.start(t); src.stop(t + 0.4);
  }
  
  _select(t) {
    const o = this.ctx.createOscillator();
    const g = this.ctx.createGain();
    o.type = 'sine';
    o.frequency.setValueAtTime(523, t);
    o.frequency.setValueAtTime(659, t + 0.08);
    g.gain.setValueAtTime(0.1, t);
    g.gain.exponentialRampToValueAtTime(0.001, t + 0.15);
    o.connect(g); g.connect(this.ctx.destination);
    o.start(t); o.stop(t + 0.15);
  }
  
  _warning(t) {
    const o = this.ctx.createOscillator();
    const g = this.ctx.createGain();
    o.type = 'square';
    o.frequency.setValueAtTime(440, t);
    o.frequency.setValueAtTime(880, t + 0.15);
    o.frequency.setValueAtTime(440, t + 0.3);
    g.gain.setValueAtTime(0.12, t);
    g.gain.exponentialRampToValueAtTime(0.001, t + 0.4);
    o.connect(g); g.connect(this.ctx.destination);
    o.start(t); o.stop(t + 0.4);
  }
}

// ── const: audio ──
const audio = new AudioManager();

// ── class: Terrain ──
class Terrain {
  constructor(width, height) {
    this.width = width;
    this.height = height;
    this.res = CONFIG.TERRAIN_RESOLUTION;
    this.cols = Math.ceil(width / this.res);
    this.rows = Math.ceil(height / this.res);
    this.grid = new Uint8Array(this.cols * this.rows); // 0=air, 1+=solid
    this.waterLevel = height - CONFIG.WATER_LEVEL_OFFSET;
    this.skyTop = 0;
    this.caveMode = false;
    this.caveCeiling = [];
    this.generate('island');
  }

  idx(x, y) { return x + y * this.cols; }
  
  getCell(x, y) {
    const cx = Math.floor(x / this.res);
    const cy = Math.floor(y / this.res);
    if (cx < 0 || cx >= this.cols || cy < 0 || cy >= this.rows) return 1; // out of bounds = solid
    return this.grid[this.idx(cx, cy)];
  }

  isSolid(x, y) { return this.getCell(x, y) > 0; }
  
  isWater(y) { return y >= this.waterLevel; }

  generate(type) {
    this.clear();
    this.caveMode = type === 'cave';
    
    if (type === 'island') this._genIsland(1);
    else if (type === 'doubleIsland') this._genDoubleIsland();
    else if (type === 'cave') this._genCave();
  }

  clear() {
    this.grid.fill(0);
  }

  _noise(x, seed) {
    const n = Math.sin(x * 12.9898 + seed * 78.233) * 43758.5453;
    return n - Math.floor(n);
  }

  _smoothNoise(x, seed) {
    const ix = Math.floor(x);
    const fx = x - ix;
    const t = fx * fx * (3 - 2 * fx); // smoothstep
    return this._noise(ix, seed) * (1 - t) + this._noise(ix + 1, seed) * t;
  }

  _fbm(x, octaves, seed) {
    let val = 0, amp = 1, freq = 1, max = 0;
    for (let i = 0; i < octaves; i++) {
      val += this._smoothNoise(x * freq, seed + i * 100) * amp;
      max += amp;
      amp *= 0.5;
      freq *= 2;
    }
    return val / max;
  }

  _genIsland(count = 1) {
    const baseH = this.height * 0.65;
    for (let x = 0; x < this.cols; x++) {
      let h;
      if (count === 1) {
        // Single island - bell curve shape
        const cx = this.cols / 2;
        const dist = Math.abs(x - cx) / cx;
        const base = Math.max(0, 1 - dist * dist);
        h = baseH + (this._fbm(x * 0.02, 4, 42) - 0.5) * 80 * base;
      } else {
        h = this.height; // flat ground for double island
      }
      
      const terrainTop = Math.floor(h);
      for (let y = terrainTop; y < this.rows; y++) {
        this.grid[this.idx(x, y)] = 1;
      }
    }
  }

  _genDoubleIsland() {
    const baseH = this.height * 0.65;
    for (let x = 0; x < this.cols; x++) {
      // Two peaks with gap in middle
      const peak1 = Math.exp(-Math.pow((x - this.cols * 0.25) / (this.cols * 0.15), 2));
      const peak2 = Math.exp(-Math.pow((x - this.cols * 0.75) / (this.cols * 0.15), 2));
      const shape = Math.max(peak1, peak2);
      
      // Valley in middle is water/air
      let h;
      if (shape > 0.3) {
        h = baseH + (this._fbm(x * 0.02, 4, 77) - 0.5) * 60 * shape;
      } else {
        h = this.height; // deep valley
      }
      
      const terrainTop = Math.floor(h);
      for (let y = terrainTop; y < this.rows; y++) {
        this.grid[this.idx(x, y)] = 1;
      }
    }
  }

  _genCave() {
    // Fill everything solid first
    this.grid.fill(1);
    
    // Carve out cave using cellular automata + noise
    const carveRadius = Math.floor(this.height * 0.35 / this.res);
    const centerY = Math.floor(this.rows * 0.45);
    
    for (let x = 0; x < this.cols; x++) {
      // Main tunnel with variation
      const tunnelY = centerY + Math.sin(x * 0.01) * carveRadius * 0.3 
        + (this._fbm(x * 0.05, 3, 99) - 0.5) * carveRadius;
      
      for (let y = 0; y < this.rows; y++) {
        const dist = Math.abs(y - tunnelY);
        if (dist < carveRadius + (this._fbm(x * 0.1 + y * 0.1, 2, 55) - 0.5) * 30) {
          this.grid[this.idx(x, y)] = 0; // air
        }
      }
    }
    
    // Add some extra tunnels/rooms
    for (let i = 0; i < 8; i++) {
      const cx = Math.floor(Math.random() * this.cols);
      const cy = centerY + (Math.random() - 0.5) * carveRadius * 1.5;
      const r = 20 + Math.random() * 40;
      for (let dx = -r; dx <= r; dx++) {
        for (let dy = -r; dy <= r; dy++) {
          if (dx*dx + dy*dy < r*r) {
            const px = cx + dx, py = cy + dy;
            if (px >= 0 && px < this.cols && py >= 0 && py < this.rows) {
              this.grid[this.idx(px, py)] = 0;
            }
          }
        }
      }
    }
    
    // Ensure top and bottom are solid
    for (let x = 0; x < this.cols; x++) {
      for (let y = 0; y < Math.floor(this.rows * 0.1); y++) {
        this.grid[this.idx(x, y)] = 1;
      }
      for (let y = Math.floor(this.rows * 0.9); y < this.rows; y++) {
        this.grid[this.idx(x, y)] = 1;
      }
    }
    
    // Water at bottom of cave
    this.waterLevel = centerY + carveRadius + 20;
  }

  destroy(x, y, radius) {
    const r = Math.ceil(radius / this.res);
    const cx = Math.floor(x / this.res);
    const cy = Math.floor(y / this.res);
    
    for (let dx = -r; dx <= r; dx++) {
      for (let dy = -r; dy <= r; dy++) {
        if (dx*dx + dy*dy < r*r) {
          const px = cx + dx, py = cy + dy;
          if (px >= 0 && px < this.cols && py >= 0 && py < this.rows) {
            this.grid[this.idx(px, py)] = 0;
          }
        }
      }
    }
  }

  getTerrainHeight(x) {
    const cx = Math.floor(x / this.res);
    if (cx < 0 || cx >= this.cols) return this.height;
    
    for (let y = 0; y < this.rows; y++) {
      if (this.grid[this.idx(cx, y)] > 0) {
        return y * this.res;
      }
    }
    return this.height;
  }

  getSurfaceY(x) {
    const cx = Math.floor(x / this.res);
    if (cx < 0 || cx >= this.cols) return this.height;
    
    for (let y = 0; y < this.rows; y++) {
      if (this.grid[this.idx(cx, y)] > 0 && (y === 0 || this.grid[this.idx(cx, y-1)] === 0)) {
        return y * this.res;
      }
    }
    return this.height;
  }

  findSpawnPoints(count) {
    const points = [];
    const step = Math.floor(this.cols / (count + 2));
    
    for (let i = 1; i <= count; i++) {
      const cx = step * i;
      let surfaceY = this.height;
      
      if (!this.caveMode) {
        // Find top of terrain
        for (let y = 0; y < this.rows; y++) {
          if (this.grid[this.idx(cx, y)] > 0 && (y === 0 || this.grid[this.idx(cx, y-1)] === 0)) {
            surfaceY = y * this.res - CONFIG.UNIT_RADIUS * 2;
            break;
          }
        }
      } else {
        // Find ceiling of cave (bottom-most air cell from top)
        for (let y = this.rows - 1; y >= 0; y--) {
          if (this.grid[this.idx(cx, y)] === 0 && (y === this.rows - 1 || this.grid[this.idx(cx, y+1)] > 0)) {
            surfaceY = y * this.res + CONFIG.UNIT_RADIUS * 2;
            break;
          }
        }
      }
      
      points.push({ x: cx * this.res, y: Math.min(surfaceY, this.waterLevel - 30) });
    }
    
    return points;
  }

  getTerrainColor(x, y) {
    const cell = this.getCell(x, y);
    if (cell === 0) return null; // air
    
    const ny = y / this.height;
    if (!this.caveMode && ny > 0.85) {
      // Sandy bottom
      return `rgb(${139 + Math.random()*20|0}, ${119 + Math.random()*15|0}, ${80 + Math.random()*10|0})`;
    } else if (!this.caveMode && ny < 0.4) {
      // Rocky top
      return `rgb(${100 + Math.random()*30|0}, ${90 + Math.random()*25|0}, ${80 + Math.random()*20|0})`;
    } else if (this.caveMode) {
      // Cave walls - darker stone
      const v = 60 + Math.random() * 40 | 0;
      return `rgb(${v+15}, ${v+10}, ${v})`;
    } else {
      // Dirt/mud middle
      const v = 80 + Math.random() * 30 | 0;
      return `rgb(${v+20}, ${v+10}, ${v-10})`;
    }
  }

  getGrassColor(x) {
    const n = this._noise(x * 0.5, 123);
    const g = 140 + n * 60 | 0;
    return `rgb(${60 + n*30|0}, ${g}, ${30 + n*20|0})`;
  }

  // Check if a point is exposed to air (for debris falling)
  isExposed(x, y) {
    const cx = Math.floor(x / this.res);
    const cy = Math.floor(y / this.res);
    
    // Check above - if there's air above, it can fall
    for (let dy = -1; dy >= -5; dy--) {
      const py = cy + dy;
      if (py < 0) return true;
      if (this.grid[this.idx(cx, py)] === 0) return true;
    }
    return false;
  }

  // Make unsupported terrain fall (debris physics)
  makeDebrisFall() {
    const debris = [];
    
    for (let x = this.cols - 1; x >= 0; x--) {
      let foundAirBelow = false;
      
      for (let y = this.rows - 1; y >= 0; y--) {
        if (this.grid[this.idx(x, y)] === 0) {
          foundAirBelow = true;
        } else if (foundAirBelow && !this._isSupported(x, y)) {
          // This cell is unsupported - make it fall
          debris.push({ x: x * this.res + this.res/2, y: y * this.res + this.res/2 });
          this.grid[this.idx(x, y)] = 0;
        }
      }
    }
    
    return debris;
  }

  _isSupported(cx, cy) {
    // Check if supported from below or sides
    if (cy >= this.rows - 1) return true;
    if (this.grid[this.idx(cx, cy + 1)] > 0) return true;
    if (cx > 0 && this.grid[this.idx(cx-1, cy)] > 0 && 
        this.grid[this.idx(cx-1, cy+1)] > 0) return true;
    if (cx < this.cols - 1 && this.grid[this.idx(cx+1, cy)] > 0 && 
        this.grid[this.idx(cx+1, cy+1)] > 0) return true;
    return false;
  }
}

// ── class: Particle ──
class Particle {
  constructor(x, y, vx, vy, life, color, size, type = 'circle') {
    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.rotation = Math.random() * Math.PI * 2;
    this.rotSpeed = (Math.random() - 0.5) * 10;
    this.gravity = true;
    this.alpha = 1;
  }

  update(dt) {
    if (this.gravity) this.vy += CONFIG.GRAVITY * 0.3 * dt;
    this.x += this.vx * dt;
    this.y += this.vy * dt;
    this.rotation += this.rotSpeed * dt;
    this.life -= dt;
    this.alpha = Math.max(0, this.life / this.maxLife);
    return this.life > 0;
  }

  draw(ctx) {
    ctx.save();
    ctx.globalAlpha = this.alpha;
    ctx.translate(this.x, this.y);
    ctx.rotate(this.rotation);
    
    if (this.type === 'circle') {
      ctx.fillStyle = this.color;
      ctx.beginPath();
      ctx.arc(0, 0, this.size * this.alpha + 1, 0, Math.PI * 2);
      ctx.fill();
    } else if (this.type === 'debris') {
      ctx.fillStyle = this.color;
      const s = this.size * (0.5 + this.alpha * 0.5);
      ctx.fillRect(-s/2, -s/2, s, s);
    } else if (this.type === 'spark') {
      ctx.strokeStyle = this.color;
      ctx.lineWidth = 2;
      ctx.beginPath();
      ctx.moveTo(0, 0);
      ctx.lineTo(this.vx * 0.05, this.vy * 0.05);
      ctx.stroke();
    } else if (this.type === 'smoke') {
      ctx.fillStyle = this.color;
      const s = this.size * (2 - this.alpha);
      ctx.beginPath();
      ctx.arc(0, 0, s, 0, Math.PI * 2);
      ctx.fill();
    } else if (this.type === 'water') {
      ctx.fillStyle = `rgba(100, 180, 255, ${this.alpha})`;
      ctx.beginPath();
      ctx.arc(0, 0, this.size * 0.7, 0, Math.PI * 2);
      ctx.fill();
    } else if (this.type === 'fire') {
      const grad = ctx.createRadialGradient(0, 0, 0, 0, 0, this.size);
      grad.addColorStop(0, `rgba(255, 255, 100, ${this.alpha})`);
      grad.addColorStop(0.5, `rgba(255, 150, 30, ${this.alpha * 0.7})`);
      grad.addColorStop(1, `rgba(200, 50, 0, 0)`);
      ctx.fillStyle = grad;
      ctx.beginPath();
      ctx.arc(0, 0, this.size * (1 + (1-this.alpha) * 0.5), 0, Math.PI * 2);
      ctx.fill();
    }
    
    ctx.restore();
  }
}

// ── class: ParticleSystem ──
class ParticleSystem {
  constructor() {
    this.particles = [];
  }

  emit(x, y, count, config) {
    for (let i = 0; i < count; i++) {
      const angle = config.angle !== undefined ? 
        config.angle + (Math.random() - 0.5) * (config.spread || Math.PI * 2) :
        Math.random() * Math.PI * 2;
      const speed = (config.speedMin || 0) + Math.random() * ((config.speedMax || 100));
      
      const p = new Particle(
        x + (Math.random() - 0.5) * (config.offsetX || 0),
        y + (Math.random() - 0.5) * (config.offsetY || 0),
        Math.cos(angle) * speed,
        Math.sin(angle) * speed,
        config.lifeMin + Math.random() * ((config.lifeMax || 1) - (config.lifeMin || 0.5)),
        typeof config.color === 'function' ? config.color() : config.color,
        config.sizeMin + Math.random() * ((config.sizeMax || 3) - (config.sizeMin || 1)),
        config.type || 'circle'
      );
      
      if (!config.gravity) p.gravity = false;
      this.particles.push(p);
    }
  }

  explosion(x, y, size = 'medium') {
    const scale = size === 'large' ? 2 : size === 'small' ? 0.5 : 1;
    
    // Fire particles
    this.emit(x, y, Math.floor(30 * scale), {
      speedMin: 50, speedMax: 200 * scale,
      lifeMin: 0.2, lifeMax: 0.6,
      color: () => ['#ff4400', '#ff8800', '#ffcc00', '#ffff00'][Math.random()*4|0],
      sizeMin: 3, sizeMax: 10 * scale,
      type: 'fire'
    });
    
    // Smoke
    this.emit(x, y, Math.floor(20 * scale), {
      speedMin: 20, speedMax: 80 * scale,
      lifeMin: 0.5, lifeMax: 1.5,
      color: () => `rgba(${60+Math.random()*40|0}, ${60+Math.random()*40|0}, ${60+Math.random()*40|0}, 0.8)`,
      sizeMin: 5, sizeMax: 15 * scale,
      type: 'smoke'
    });
    
    // Sparks
    this.emit(x, y, Math.floor(25 * scale), {
      speedMin: 100, speedMax: 400 * scale,
      lifeMin: 0.3, lifeMax: 0.8,
      color: () => ['#ffff00', '#ffaa00', '#ffffff'][Math.random()*3|0],
      sizeMin: 1, sizeMax: 3,
      type: 'spark'
    });
    
    // Debris chunks
    this.emit(x, y, Math.floor(15 * scale), {
      speedMin: 80, speedMax: 250 * scale,
      lifeMin: 0.5, lifeMax: 1.5,
      color: () => `rgb(${100+Math.random()*60|0}, ${80+Math.random()*40|0}, ${60+Math.random()*30|0})`,
      sizeMin: 2, sizeMax: 6 * scale,
      type: 'debris'
    });
    
    // Ring effect (expanding circle)
    this.emit(x, y, Math.floor(15 * scale), {
      angle: 0, spread: Math.PI * 2,
      speedMin: 150 * scale, speedMax: 180 * scale,
      lifeMin: 0.3, lifeMax: 0.5,
      color: '#ff6600',
      sizeMin: 2, sizeMax: 4,
      type: 'circle'
    });
  }

  waterSplash(x, y) {
    this.emit(x, y, 15, {
      angle: -Math.PI / 2, spread: Math.PI * 0.8,
      speedMin: 50, speedMax: 150,
      lifeMin: 0.3, lifeMax: 0.7,
      color: '#66bbff',
      sizeMin: 2, sizeMax: 5,
      type: 'water'
    });
    
    this.emit(x, y, 8, {
      angle: -Math.PI / 2, spread: Math.PI * 0.4,
      speedMin: 30, speedMax: 100,
      lifeMin: 0.5, lifeMax: 1.0,
      color: 'rgba(150, 200, 255, 0.6)',
      sizeMin: 4, sizeMax: 8,
      type: 'smoke', gravity: false
    });
  }

  jumpDust(x, y) {
    this.emit(x, y, 8, {
      angle: Math.PI / 2, spread: Math.PI * 0.6,
      speedMin: 20, speedMax: 60,
      lifeMin: 0.3, lifeMax: 0.6,
      color: 'rgba(180, 160, 140, 0.7)',
      sizeMin: 2, sizeMax: 5,
      type: 'smoke', gravity: false
    });
  }

  moveDust(x, y) {
    this.emit(x, y + 8, 3, {
      angle: Math.PI / 2, spread: Math.PI * 0.4,
      speedMin: 5, speedMax: 20,
      lifeMin: 0.2, lifeMax: 0.4,
      color: 'rgba(160, 140, 120, 0.5)',
      sizeMin: 1, sizeMax: 3,
      type: 'smoke', gravity: false
    });
  }

  hitEffect(x, y) {
    this.emit(x, y, 6, {
      speedMin: 30, speedMax: 80,
      lifeMin: 0.2, lifeMax: 0.4,
      color: '#ffffff',
      sizeMin: 1, sizeMax: 3,
      type: 'spark'
    });
  }

  update(dt) {
    this.particles = this.particles.filter(p => p.update(dt));
  }

  draw(ctx) {
    for (const p of this.particles) {
      p.draw(ctx);
    }
  }

  clear() {
    this.particles.length = 0;
  }
}

// ── class: FloatingText ──
class FloatingText {
  constructor(x, y, text, color = '#fff', size = 16) {
    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 = -80;
  }

  update(dt) {
    this.y += this.vy * dt;
    this.vy *= 0.95;
    this.life -= dt;
    return this.life > 0;
  }

  draw(ctx, camX, camY, zoom) {
    const alpha = Math.max(0, this.life / this.maxLife);
    ctx.save();
    ctx.globalAlpha = alpha;
    ctx.font = `bold ${this.size}px Arial`;
    ctx.fillStyle = '#000';
    ctx.textAlign = 'center';
    ctx.fillText(this.text, this.x + 1, this.y + 1);
    ctx.fillStyle = this.color;
    ctx.fillText(this.text, this.x, this.y);
    ctx.restore();
  }
}

// ── class: Worm ──
class Worm {
  constructor(id, teamId, name, x, y) {
    this.id = id;
    this.teamId = teamId;
    this.name = name;
    this.x = x;
    this.y = y;
    this.vx = 0;
    this.vy = 0;
    this.health = CONFIG.MAX_HEALTH;
    this.maxHealth = CONFIG.MAX_HEALTH;
    this.alive = true;
    this.radius = CONFIG.UNIT_RADIUS;
    this.onGround = false;
    this.facing = 1; // 1=right, -1=left
    this.moveTime = 0;
    this.maxMoveTime = 8;
    this.angle = 0; // body angle for rendering
    this.bobPhase = Math.random() * Math.PI * 2;
    this.inWater = false;
    this.hasParachute = false;
    this.laughing = false;
    this.laughTimer = 0;
    this.drowning = false;
    this.drownTimer = 0;
    this.lastFallSpeed = 0;
    this.wasOnGround = true;
    this.moving = false;
    this.eyeBlink = 0;
    this.blinkTimer = Math.random() * 3;
    this.items = ['parachute', 'medikit']; // Starting items
  }

  update(dt, terrain) {
    if (!this.alive) return;

    // Blink timer
    this.blinkTimer -= dt;
    if (this.blinkTimer <= 0) {
      this.eyeBlink = 1;
      this.blinkTimer = 2 + Math.random() * 4;
    }
    if (this.eyeBlink > 0) this.eyeBlink -= dt * 5;

    // Laugh timer
    if (this.laughing) {
      this.laughTimer -= dt;
      if (this.laughTimer <= 0) this.laughing = false;
    }

    // Water physics
    const wasInWater = this.inWater;
    this.inWater = terrain.isWater(this.y + this.radius);
    
    if (this.inWater) {
      this.drownTimer += dt;
      if (!wasInWater) audio.play('splash');
      
      // Buoyancy - slow upward force
      this.vy -= 200 * dt;
      // Water drag
      this.vx *= Math.pow(0.95, dt * 60);
      this.vy *= Math.pow(0.95, dt * 60);
      
      if (this.drownTimer > 3) {
        this.health -= 20 * dt;
        if (this.health <= 0) {
          this.alive = false;
          return;
        }
      }
    } else {
      this.drownTimer = Math.max(0, this.drownTimer - dt * 2);
    }

    // Gravity
    const gravMult = this.hasParachute ? 0.3 : 1;
    this.vy += CONFIG.GRAVITY * gravMult * dt;
    
    // Track fall speed for damage
    if (this.vy > 0) {
      this.lastFallSpeed = Math.max(this.lastFallSpeed, this.vy);
    }

    // Apply velocity
    this.x += this.vx * dt;
    this.y += this.vy * dt;

    // Move time countdown
    if (this.moveTime > 0) {
      this.moveTime -= dt;
      if (this.moving) {
        const moveDist = CONFIG.UNIT_SPEED * dt;
        this.x += this.facing * moveDist;
        this.vx = this.facing * CONFIG.UNIT_SPEED;
      } else {
        this.vx *= 0.9; // friction when not moving
      }
    }

    // Terrain collision
    this._collideTerrain(terrain, dt);

    // 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 on landing
    if (this.onGround && !this.wasOnGround && this.lastFallSpeed > CONFIG.FALL_DAMAGE_THRESHOLD) {
      const dmg = Math.floor((this.lastFallSpeed - CONFIG.FALL_DAMAGE_THRESHOLD) / 15);
      if (dmg > 0) {
        this.health -= dmg;
        game.floatingTexts.push(new FloatingText(this.x, this.y - 30, `-${dmg}`, '#ff4444'));
        audio.play('hit', { heavy: dmg > 15 });
      }
    }

    // Death check
    if (this.health <= 0) {
      this.alive = false;
      this.health = 0;
    }

    // Body angle for rendering
    const targetAngle = Math.atan2(this.vy, Math.abs(this.vx)) * 0.3;
    this.angle += (targetAngle - this.angle) * 0.1;
    
    // Bob animation
    if (this.onGround && !this.moving) {
      this.bobPhase += dt * 3;
    }

    this.wasOnGround = this.onGround;
  }

  _collideTerrain(terrain, dt) {
    const r = this.radius;
    
    // Check feet (bottom of worm)
    const footY = Math.floor((this.y + r) / terrain.res);
    const footX = Math.floor(this.x / terrain.res);
    
    if (footX >= 0 && footX < terrain.cols && footY >= 0 && footY < terrain.rows) {
      // Check below feet
      if (terrain.grid[terrain.idx(footX, footY)] > 0) {
        this.y = footY * terrain.res - r;
        if (this.vy > 0) this.vy = 0;
        this.onGround = true;
        return;
      }
    }

    // Check sides
    const midY = Math.floor(this.y / terrain.res);
    
    // Left side
    const leftX = footX - 1;
    if (leftX >= 0 && midY >= 0 && midY < terrain.rows) {
      if (terrain.grid[terrain.idx(leftX, midY)] > 0) {
        this.x = (leftX + 1) * terrain.res + r;
        this.vx = 0;
      }
    }
    
    // Right side  
    const rightX = footX + 1;
    if (rightX < terrain.cols && midY >= 0 && midY < terrain.rows) {
      if (terrain.grid[terrain.idx(rightX, midY)] > 0) {
        this.x = rightX * terrain.res - r;
        this.vx = 0;
      }
    }

    // Check head (top of worm)
    const headY = Math.floor((this.y - r) / terrain.res);
    if (headY >= 0 && footX >= 0 && footX < terrain.cols) {
      if (terrain.grid[terrain.idx(footX, headY)] > 0) {
        this.y = (headY + 1) * terrain.res + r;
        if (this.vy < 0) this.vy = 0;
      }
    }

    // Check corners for better collision
    const corners = [
      [-r, -r], [r, -r], [-r, r], [r, r]
    ];
    
    for (const [dx, dy] of corners) {
      const cx = Math.floor((this.x + dx) / terrain.res);
      const cy = Math.floor((this.y + dy) / terrain.res);
      
      if (cx >= 0 && cx < terrain.cols && cy >= 0 && cy < terrain.rows) {
        if (terrain.grid[terrain.idx(cx, cy)] > 0) {
          // Push worm out of this cell
          const cellCenterX = (cx + 0.5) * terrain.res;
          const cellCenterY = (cy + 0.5) * terrain.res;
          const overlapX = r - Math.abs(this.x - cellCenterX);
          const overlapY = r - Math.abs(this.y - cellCenterY);
          
          if (overlapX < overlapY) {
            this.x += (this.x > cellCenterX ? 1 : -1) * overlapX;
            this.vx = 0;
          } else {
            this.y += (this.y > cellCenterY ? 1 : -1) * overlapY;
            if (dy > 0 && this.vy > 0) this.vy = 0;
            if (dy < 0 && this.vy < 0) this.vy = 0;
          }
        }
      }
    }

    // If not on ground check, mark as airborne
    const belowY = Math.floor((this.y + r + 2) / terrain.res);
    let grounded = false;
    for (let dx = -1; dx <= 1; dx++) {
      const bx = footX + dx;
      if (bx >= 0 && bx < terrain.cols && belowY >= 0 && belowY < terrain.rows) {
        if (terrain.grid[terrain.idx(bx, belowY)] > 0) {
          grounded = true;
          break;
        }
      }
    }
    this.onGround = grounded;
    
    // If in water and at surface, float
    if (this.inWater && !grounded) {
      this.vy -= 150 * dt; // buoyancy - now dt is passed!
    }
  }

  takeDamage(amount, sourceX, sourceY) {
    if (!this.alive) return;
    
    const dist = Math.hypot(this.x - sourceX, this.y - sourceY);
    const falloff = Math.max(0.2, 1 - dist / 150);
    const actualDmg = Math.floor(amount * falloff);
    
    this.health -= actualDmg;
    if (this.health < 0) this.health = 0;
    
    // Knockback
    const angle = Math.atan2(this.y - sourceY, this.x - sourceX);
    const kbForce = actualDmg * 3;
    this.vx += Math.cos(angle) * kbForce;
    this.vy += Math.sin(angle) * kbForce - 100;
    
    // Floating damage text
    game.floatingTexts.push(new FloatingText(
      this.x + (Math.random()-0.5)*20, 
      this.y - 30, 
      `-${actualDmg}`, 
      actualDmg > 20 ? '#ff4444' : '#ffaa00',
      Math.min(24, 14 + actualDmg * 0.3) | 0
    ));

    if (this.health <= 0) {
      this.alive = false;
      audio.play('death');
    } else {
      audio.play('hit', { heavy: actualDmg > 25 });
    }
  }

  draw(ctx, camX, camY, zoom) {
    if (!this.alive) return;
    
    const sx = this.x - camX;
    const sy = this.y - camY;
    const bob = Math.sin(this.bobPhase) * 2;
    
    ctx.save();
    ctx.translate(sx, sy + bob);
    ctx.scale(this.facing, 1);
    
    // Shadow
    ctx.fillStyle = 'rgba(0,0,0,0.3)';
    ctx.beginPath();
    ctx.ellipse(0, this.radius + 2, this.radius * 0.8, 4, 0, 0, Math.PI * 2);
    ctx.fill();

    // Body (worm shape - segmented)
    const color = TEAM_COLORS[this.teamId].primary;
    const darkColor = TEAM_COLORS[this.teamId].secondary;
    
    // Main body segments
    for (let i = 0; i < 3; i++) {
      const segY = i * 5 - 5;
      const segR = this.radius * (1 - i * 0.15);
      
      ctx.fillStyle = color;
      ctx.beginPath();
      ctx.arc(0, segY + this.angle * 3, segR, 0, Math.PI * 2);
      ctx.fill();
      
      ctx.strokeStyle = darkColor;
      ctx.lineWidth = 1.5;
      ctx.stroke();
    }

    // Head
    ctx.fillStyle = color;
    ctx.beginPath();
    ctx.arc(0, -this.radius + this.angle * 2, this.radius * 0.9, 0, Math.PI * 2);
    ctx.fill();
    ctx.strokeStyle = darkColor;
    ctx.lineWidth = 1.5;
    ctx.stroke();

    // Eyes
    const eyeY = -this.radius + this.angle * 2 - 2;
    if (this.eyeBlink <= 0) {
      // White of eyes
      ctx.fillStyle = '#fff';
      ctx.beginPath();
      ctx.ellipse(-4, eyeY, 4, 5, 0, 0, Math.PI * 2);
      ctx.fill();
      ctx.beginPath();
      ctx.ellipse(4, eyeY, 4, 5, 0, 0, Math.PI * 2);
      ctx.fill();
      
      // Pupils (look in facing direction)
      ctx.fillStyle = '#111';
      ctx.beginPath();
      ctx.arc(-3 + this.facing * 1.5, eyeY, 2, 0, Math.PI * 2);
      ctx.fill();
      ctx.beginPath();
      ctx.arc(5 + this.facing * 1.5, eyeY, 2, 0, Math.PI * 2);
      ctx.fill();
    } else {
      // Blinking - just lines
      ctx.strokeStyle = '#111';
      ctx.lineWidth = 2;
      ctx.beginPath();
      ctx.moveTo(-7, eyeY);
      ctx.lineTo(-1, eyeY);
      ctx.stroke();
      ctx.beginPath();
      ctx.moveTo(1, eyeY);
      ctx.lineTo(7, eyeY);
      ctx.stroke();
    }

    // Mouth (smile or grimace based on health)
    if (this.health < 30) {
      // Worried face
      ctx.strokeStyle = '#111';
      ctx.lineWidth = 1.5;
      ctx.beginPath();
      ctx.arc(0, eyeY + 8, 4, Math.PI * 0.2, Math.PI * 0.8);
      ctx.stroke();
    } else if (this.laughing) {
      // Big laugh mouth
      ctx.fillStyle = '#111';
      ctx.beginPath();
      ctx.arc(0, eyeY + 7, 5, 0, Math.PI);
      ctx.fill();
    } else {
      // Normal smile
      ctx.strokeStyle = '#111';
      ctx.lineWidth = 1.5;
      ctx.beginPath();
      ctx.arc(0, eyeY + 6, 3, 0, Math.PI);
      ctx.stroke();
    }

    // Parachute indicator
    if (this.hasParachute) {
      ctx.fillStyle = '#ff4444';
      ctx.beginPath();
      ctx.arc(0, -this.radius * 2.5, 12, Math.PI, 0);
      ctx.fill();
      ctx.strokeStyle = '#cc3333';
      ctx.lineWidth = 1;
      ctx.stroke();
      
      // Strings
      ctx.strokeStyle = '#fff';
      ctx.beginPath();
      ctx.moveTo(-8, -this.radius * 2.5);
      ctx.lineTo(0, -this.radius + this.angle * 2);
      ctx.moveTo(8, -this.radius * 2.5);
      ctx.lineTo(0, -this.radius + this.angle * 2);
      ctx.stroke();
    }

    // Health bar above worm
    if (this.health < this.maxHealth) {
      const barW = 30;
      const barH = 4;
      const healthPct = this.health / this.maxHealth;
      
      ctx.fillStyle = 'rgba(0,0,0,0.5)';
      ctx.fillRect(-barW/2, -this.radius * 2 - 10 + bob * 0, barW, barH);
      
      const hColor = healthPct > 0.5 ? '#44ff44' : healthPct > 0.25 ? '#ffaa00' : '#ff4444';
      ctx.fillStyle = hColor;
      ctx.fillRect(-barW/2, -this.radius * 2 - 10 + bob * 0, barW * healthPct, barH);
    }

    // Name tag
    ctx.restore();
    
    ctx.save();
    ctx.font = 'bold 11px Arial';
    ctx.textAlign = 'center';
    ctx.fillStyle = '#fff';
    ctx.strokeStyle = '#000';
    ctx.lineWidth = 3;
    const nameY = sy - this.radius * 2.5 + bob;
    ctx.strokeText(this.name, sx, nameY);
    ctx.fillText(this.name, sx, nameY);
    ctx.restore();

    // Drowning indicator
    if (this.drownTimer > 0) {
      const dmgPct = this.drownTimer / 3;
      ctx.save();
      ctx.fillStyle = `rgba(100, 150, 255, ${0.3 + Math.sin(Date.now() * 0.01) * 0.2})`;
      ctx.font = 'bold 14px Arial';
      ctx.textAlign = 'center';
      ctx.fillText('💧', sx, sy - this.radius * 3);
      ctx.restore();
    }
  }
}

// ── class: Projectile ──
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.alive = true;
    this.trail = [];
    this.age = 0;
    this.homingTarget = null;
    this.isHoming = false;
    this.subBombs = [];
    this.rotAngle = 0;
  }

  update(dt, terrain, worms) {
    if (!this.alive) return;
    
    this.age += dt;
    
    // Wind effect
    this.vx += game.wind * 20 * dt;
    
    // Gravity (unless it's a direct weapon with no arc)
    if (this.weapon.type === 'indirect' || this.weapon.id === 'superSaw') {
      this.vy += CONFIG.GRAVITY * dt;
    } else if (this.weapon.id === 'earthquake') {
      // Earthquake doesn't move as projectile, handled separately
      return;
    }

    // Homing missile logic
    if (this.weapon.id === 'homingMissile' && !this.isHoming) {
      let closest = null;
      let closestDist = Infinity;
      for (const w of worms) {
        if (!w.alive || w.teamId === this.owner.teamId) continue;
        const d = Math.hypot(w.x - this.x, w.y - this.y);
        if (d < closestDist) { closestDist = d; closest = w; }
      }
      if (closest) {
        this.isHoming = true;
        this.homingTarget = closest;
      }
    }

    if (this.isHoming && this.homingTarget && this.homingTarget.alive) {
      const angle = Math.atan2(this.homingTarget.y - this.y, this.homingTarget.x - this.x);
      const currentAngle = Math.atan2(this.vy, this.vx);
      let diff = angle - currentAngle;
      while (diff > Math.PI) diff -= Math.PI * 2;
      while (diff < -Math.PI) diff += Math.PI * 2;
      
      const turnRate = 3 * dt;
      const newAngle = currentAngle + Math.sign(diff) * Math.min(Math.abs(diff), turnRate);
      const speed = Math.hypot(this.vx, this.vy);
      this.vx = Math.cos(newAngle) * speed;
      this.vy = Math.sin(newAngle) * speed;
    }

    // Move
    this.x += this.vx * dt;
    this.y += this.vy * dt;
    
    // Rotation for visual
    this.rotAngle = Math.atan2(this.vy, this.vx);

    // Trail
    this.trail.push({ x: this.x, y: this.y, age: 0 });
    if (this.trail.length > 30) this.trail.shift();
    for (const t of this.trail) t.age += dt;

    // Terrain collision
    if (terrain.isSolid(this.x, this.y)) {
      this.explode(terrain, worms);
      return;
    }

    // Water collision
    if (terrain.isWater(this.y)) {
      audio.play('splash');
      game.particles.waterSplash(this.x, terrain.waterLevel);
      
      if (this.weapon.id === 'rocket' || this.weapon.id === 'clusterBomb') {
        this.explode(terrain, worms);
      } else {
        // Most projectiles just stop in water
        this.vy *= 0.5;
        this.vx *= 0.7;
        if (this.age > 2) this.alive = false;
      }
    }

    // Worm collision
    for (const w of worms) {
      if (!w.alive || w.teamId === this.owner.teamId && this.weapon.id !== 'earthquake') continue;
      const dist = Math.hypot(w.x - this.x, w.y - this.y);
      if (dist < w.radius + 5) {
        this.explode(terrain, worms);
        return;
      }
    }

    // Out of bounds or too old
    if (this.x < -100 || this.x > CONFIG.WORLD_WIDTH + 100 || 
        this.y > CONFIG.WORLD_HEIGHT + 100 || this.age > 8) {
      this.alive = false;
    }

    // Cluster bomb splits at apex or after time
    if (this.weapon.id === 'clusterBomb' && !this.subBombs.length && this.vy > -50 && this.age > 1.5) {
      for (let i = -2; i <= 2; i++) {
        const subVx = this.vx + i * 80;
        const subVy = this.vy + Math.random() * 50;
        game.projectiles.push(new Projectile(
          this.x, this.y, subVx, subVy, 
          { ...this.weapon, power: this.weapon.power * 0.4, radius: this.weapon.radius * 0.5 },
          this.owner
        ));
      }
      this.alive = false;
    }
  }

  explode(terrain, worms) {
    if (!this.alive) return;
    this.alive = false;

    const w = this.weapon;
    
    // Determine explosion size
    const expSize = w.radius > 80 ? 'large' : w.radius < 35 ? 'small' : 'medium';
    
    // Screen shake for big explosions
    if (w.power > 50) {
      game.screenShake = Math.min(15, w.power * 0.2);
      game.freezeTimer = Math.max(0, (w.power - 40) * 0.03);
    }

    // Particle effects
    game.particles.explosion(this.x, this.y, expSize);
    
    // Sound
    audio.play('explosion', { size: expSize });

    // Terrain destruction
    terrain.destroy(this.x, this.y, w.radius);
    
    // Debris falling
    const debris = terrain.makeDebrisFall();
    for (const d of debris) {
      game.particles.emit(d.x, d.y, 1, {
        speedMin: 0, speedMax: 30,
        lifeMin: 0.5, lifeMax: 1.2,
        color: `rgb(${80+Math.random()*60|0}, ${70+Math.random()*40|0}, ${50+Math.random()*30|0})`,
        sizeMin: 2, sizeMax: 5,
        type: 'debris'
      });
    }

    // Damage worms
    for (const worm of worms) {
      if (!worm.alive) continue;
      const dist = Math.hypot(worm.x - this.x, worm.y - this.y);
      if (dist < w.radius + worm.radius) {
        worm.takeDamage(w.power, this.x, this.y);
        
        // Make target laugh if not killed
        if (worm.alive && worm.teamId !== this.owner.teamId) {
          worm.laughing = true;
          worm.laughTimer = 1.5;
          audio.play('laugh');
        }
      }
    }

    // Damage debris from falling terrain
    for (const d of debris) {
      for (const worm of worms) {
        if (!worm.alive) continue;
        const dist = Math.hypot(worm.x - d.x, worm.y - d.y);
        if (dist < 20) {
          worm.takeDamage(15, d.x, d.y);
        }
      }
    }

    // Check for game end
    game.checkGameEnd();
  }

  draw(ctx, camX, camY, zoom) {
    if (!this.alive) return;
    
    const sx = this.x - camX;
    const sy = this.y - camY;

    // Trail
    ctx.save();
    for (let i = 0; i < this.trail.length; i++) {
      const t = this.trail[i];
      const alpha = Math.max(0, 1 - t.age * 3) * 0.5;
      if (alpha <= 0) continue;
      
      ctx.globalAlpha = alpha;
      ctx.fillStyle = '#ffaa44';
      const size = this.weapon.id === 'rocket' ? 3 : 2;
      ctx.beginPath();
      ctx.arc(t.x - camX, t.y - camY, size, 0, Math.PI * 2);
      ctx.fill();
    }
    ctx.restore();

    // Projectile body
    ctx.save();
    ctx.translate(sx, sy);
    
    if (this.weapon.id === 'rocket') {
      ctx.rotate(this.rotAngle);
      ctx.fillStyle = '#ff4400';
      ctx.beginPath();
      ctx.ellipse(0, 0, 12, 5, 0, 0, Math.PI * 2);
      ctx.fill();
      ctx.fillStyle = '#cc3300';
      ctx.fillRect(-8, -3, 6, 6);
      // Flame
      if (Math.random() > 0.3) {
        ctx.fillStyle = `rgba(255, ${150+Math.random()*100|0}, 0, 0.8)`;
        ctx.beginPath();
        ctx.moveTo(-12, -2);
        ctx.lineTo(-18 - Math.random() * 6, 0);
        ctx.lineTo(-12, 2);
        ctx.fill();
      }
    } else if (this.weapon.id === 'homingMissile') {
      ctx.rotate(this.rotAngle);
      ctx.fillStyle = '#44aaff';
      ctx.beginPath();
      ctx.ellipse(0, 0, 10, 4, 0, 0, Math.PI * 2);
      ctx.fill();
    } else if (this.weapon.id === 'superSaw') {
      ctx.rotate(this.rotAngle + Date.now() * 0.02);
      ctx.fillStyle = '#aaa';
      ctx.beginPath();
      for (let i = 0; i < 8; i++) {
        const a = (i / 8) * Math.PI * 2;
        const r = i % 2 === 0 ? 10 : 7;
        if (i === 0) ctx.moveTo(Math.cos(a)*r, Math.sin(a)*r);
        else ctx.lineTo(Math.cos(a)*r, Math.sin(a)*r);
      }
      ctx.closePath();
      ctx.fill();
      ctx.strokeStyle = '#666';
      ctx.lineWidth = 1;
      ctx.stroke();
    } else if (this.weapon.id === 'clusterBomb') {
      ctx.fillStyle = '#8B4513';
      ctx.beginPath();
      ctx.arc(0, 0, 7, 0, Math.PI * 2);
      ctx.fill();
      ctx.strokeStyle = '#654321';
      ctx.lineWidth = 1;
      ctx.stroke();
    } else {
      // Default bullet
      ctx.fillStyle = '#ffff00';
      ctx.beginPath();
      ctx.arc(0, 0, 3, 0, Math.PI * 2);
      ctx.fill();
    }
    
    ctx.restore();
  }
}

// ── class: Mine ──
class Mine {
  constructor(x, y, owner) {
    this.x = x; this.y = y;
    this.owner = owner;
    this.alive = true;
    this.radius = 8;
    this.placed = Date.now();
  }

  update(dt, terrain, worms) {
    if (!this.alive) return;
    
    // Check proximity to worms
    for (const w of worms) {
      if (!w.alive) continue;
      const dist = Math.hypot(w.x - this.x, w.y - this.y);
      if (dist < 25) {
        this.explode(terrain, worms);
        return;
      }
    }

    // Check terrain collision (mine falls with terrain)
    if (!terrain.isSolid(this.x, this.y + 10)) {
      this.y += CONFIG.GRAVITY * dt * 0.5;
    } else {
      const surfaceY = terrain.getSurfaceY(this.x);
      this.y = surfaceY - 3;
    }

    // Out of bounds
    if (this.y > CONFIG.WORLD_HEIGHT + 100) this.alive = false;
  }

  explode(terrain, worms) {
    this.alive = false;
    
    game.screenShake = 8;
    game.freezeTimer = 0.15;
    game.particles.explosion(this.x, this.y, 'medium');
    audio.play('explosion', { size: 'medium' });
    
    terrain.destroy(this.x, this.y, 40);
    
    for (const worm of worms) {
      if (!worm.alive) continue;
      const dist = Math.hypot(worm.x - this.x, worm.y - this.y);
      if (dist < 50) {
        worm.takeDamage(35, this.x, this.y);
        if (worm.alive && worm.teamId !== this.owner.teamId) {
          worm.laughing = true;
          worm.laughTimer = 1.5;
          audio.play('laugh');
        }
      }
    }
    
    game.checkGameEnd();
  }

  draw(ctx, camX, camY, zoom) {
    if (!this.alive) return;
    
    const sx = this.x - camX;
    const sy = this.y - camY;
    
    // Mine body
    ctx.fillStyle = '#333';
    ctx.beginPath();
    ctx.arc(sx, sy, 6, Math.PI, 0);
    ctx.fill();
    
    // Spikes
    for (let i = 0; i < 5; i++) {
      const a = Math.PI + (i / 4) * Math.PI;
      ctx.fillStyle = '#555';
      ctx.beginPath();
      ctx.moveTo(sx + Math.cos(a) * 6, sy);
      ctx.lineTo(sx + Math.cos(a - 0.15) * 9, sy - 3);
      ctx.lineTo(sx + Math.cos(a + 0.15) * 9, sy - 3);
      ctx.fill();
    }

    // Blinking light
    if (Math.sin(Date.now() * 0.005) > 0) {
      ctx.fillStyle = '#ff0000';
      ctx.beginPath();
      ctx.arc(sx, sy - 2, 2, 0, Math.PI * 2);
      ctx.fill();
    }
  }
}

// ── class: Camera ──
class Camera {
  constructor() {
    this.x = 0;
    this.y = 0;
    this.targetX = 0;
    this.targetY = 0;
    this.zoom = 1;
    this.targetZoom = 1;
    this.shakeAmount = 0;
    this.shakeDecay = 0.9;
    this.smoothFactor = 0.08;
    this.zoomSmooth = 0.05;
  }

  follow(target, worms) {
    if (target && target.alive) {
      this.targetX = target.x;
      this.targetY = target.y - 50; // Look slightly above
      
      // Zoom based on number of alive worms
      const aliveCount = worms.filter(w => w.alive).length;
      if (aliveCount <= 2) this.targetZoom = 1.3;
      else if (aliveCount <= 4) this.targetZoom = 1.15;
      else if (aliveCount <= 6) this.targetZoom = 1.0;
      else this.targetZoom = 0.85;
    }

    // Smooth follow
    this.x += (this.targetX - this.x) * this.smoothFactor;
    this.y += (this.targetY - this.y) * this.smoothFactor;
    
    // Smooth zoom
    this.zoom += (this.targetZoom - this.zoom) * this.zoomSmooth;

    // Screen shake decay
    if (this.shakeAmount > 0.5) {
      this.shakeAmount *= this.shakeDecay;
    } else {
      this.shakeAmount = 0;
    }
  }

  getShakeOffset() {
    if (this.shakeAmount < 1) return { x: 0, y: 0 };
    return {
      x: (Math.random() - 0.5) * this.shakeAmount * 2,
      y: (Math.random() - 0.5) * this.shakeAmount * 2
    };
  }

  apply(ctx, canvasW, canvasH) {
    const shake = this.getShakeOffset();
    
    ctx.save();
    ctx.translate(canvasW / 2 + shake.x, canvasH / 2 + shake.y);
    ctx.scale(this.zoom, this.zoom);
    ctx.translate(-this.x, -this.y);
  }

  unapply(ctx) {
    ctx.restore();
  }

  getVisibleBounds() {
    return {
      left: this.x - 600 / this.zoom,
      right: this.x + 600 / this.zoom,
      top: this.y - 350 / this.zoom,
      bottom: this.y + 350 / this.zoom
    };
  }

  screenToWorld(sx, sy, canvasW, canvasH) {
    return {
      x: (sx - canvasW/2) / this.zoom + this.x,
      y: (sy - canvasH/2) / this.zoom + this.y
    };
  }
}

// ── class: AIController ──
class AIController {
  constructor(worm, game) {
    this.worm = worm;
    this.game = game;
    this.thinkingTimer = 0;
    this.actionQueue = [];
    this.currentAction = null;
    this.selectedWeapon = null;
    this.aimAngle = -Math.PI / 4;
    this.aimPower = 50;
    this.isAiming = false;
    this.riskAssessment = { waterRisk: 0, cliffRisk: 0 };
  }

  update(dt) {
    if (!this.worm.alive || !this.game.activeWorm || 
        this.game.activeWorm.id !== this.worm.id) return;

    this.thinkingTimer -= dt;

    // Execute queued actions
    if (this.actionQueue.length > 0 && this.thinkingTimer <= 0) {
      const action = this.actionQueue.shift();
      this.executeAction(action);
      this.thinkingTimer = 0.5 + Math.random() * 1.0; // Think time between actions
    }

    // Plan next action if idle and timer is low enough
    if (this.actionQueue.length === 0 && this.thinkingTimer <= 0) {
      this.planActions();
    }

    // Update aim display during aiming
    if (this.isAiming && this.selectedWeapon) {
      this.updateAim(dt);
    }
  }

  planActions() {
    const worm = this.worm;
    const enemies = this.game.teams.flatMap(t => t.units).filter(u => u.alive && u.teamId !== worm.teamId);
    
    if (enemies.length === 0) return; // No enemies left
    
    this.riskAssessment = this.assessRisks();

    // Decide: move, attack, use item, or skip?
    const roll = Math.random();
    
    // Use medikit if low health
    if (worm.health < 40 && worm.items.includes('medikit') && Math.random() > 0.3) {
      this.actionQueue.push({ type: 'useItem', item: 'medikit' });
    }

    // Consider moving to safer position
    if (this.riskAssessment.waterRisk > 0.5 || this.riskAssessment.cliffRisk > 0.7) {
      const moveDir = this.findSafeDirection();
      if (moveDir !== null && worm.moveTime > 1) {
        this.actionQueue.push({ type: 'move', dir: moveDir, duration: Math.min(worm.moveTime, 3) });
      }
    }

    // Attack with random weapon
    const availableWeapons = WEAPONS.filter(w => w.ammoCost <= worm.maxMoveTime);
    if (availableWeapons.length > 0 && enemies.length > 0) {
      // Prefer weapons based on distance to nearest enemy
      const nearestEnemy = this.findNearestEnemy(enemies);
      const distToNearest = Math.hypot(nearestEnemy.x - worm.x, nearestEnemy.y - worm.y);
      
      let weaponChoice;
      if (distToNearest < 100) {
        // Close range - prefer direct weapons
        const directWeapons = availableWeapons.filter(w => w.type === 'direct');
        weaponChoice = directWeapons.length > 0 ? 
          directWeapons[Math.random() * directWeapons.length | 0] : 
          availableWeapons[Math.random() * availableWeapons.length | 0];
      } else if (distToNearest > 400) {
        // Long range - prefer powerful indirect weapons
        const indirectWeapons = availableWeapons.filter(w => w.type === 'indirect');
        weaponChoice = indirectWeapons.length > 0 ? 
          indirectWeapons[Math.random() * indirectWeapons.length | 0] : 
          availableWeapons[Math.random() * availableWeapons.length | 0];
      } else {
        weaponChoice = availableWeapons[Math.random() * availableWeapons.length | 0];
      }

      this.actionQueue.push({ type: 'attack', weapon: weaponChoice, target: nearestEnemy });
    }

    // Use parachute if about to jump near water
    if (this.riskAssessment.waterRisk > 0.3 && worm.items.includes('parachute') && Math.random() > 0.5) {
      this.actionQueue.push({ type: 'useItem', item: 'parachute' });
    }

    // Place mine strategically
    if (worm.items.includes('mine') && enemies.length > 1 && Math.random() > 0.6) {
      const nearEnemy = enemies[Math.random() * enemies.length | 0];
      this.actionQueue.push({ type: 'placeMine', targetX: nearEnemy.x + (Math.random()-0.5)*80 });
    }

    // If no actions planned, skip turn eventually
    if (this.actionQueue.length === 0) {
      this.thinkingTimer = Math.min(worm.moveTime, 3 + Math.random() * 4);
    }
  }

  executeAction(action) {
    const worm = this.worm;
    
    switch (action.type) {
      case 'move':
        if (worm.moveTime <= 0) return;
        worm.facing = action.dir;
        worm.moving = true;
        setTimeout(() => { worm.moving = false; }, action.duration * 1000);
        break;

      case 'attack':
        this.selectedWeapon = action.weapon;
        this.isAiming = true;
        
        // Calculate aim at target with some randomness
        const dx = action.target.x - worm.x;
        const dy = action.target.y - worm.y;
        const dist = Math.hypot(dx, dy);
        
        if (action.weapon.type === 'direct') {
          this.aimAngle = Math.atan2(dy, dx) + (Math.random() - 0.5) * 0.15;
          this.aimPower = Math.min(80, dist * 0.3);
          
          // Fire after short delay
          setTimeout(() => {
            if (!worm.alive || !this.isAiming) return;
            this.fireWeapon();
          }, 500 + Math.random() * 1000);
        } else {
          // Indirect - calculate parabolic trajectory
          const power = 30 + (dist / CONFIG.WORLD_WIDTH) * 60 + Math.random() * 20;
          this.aimPower = Math.min(95, power);
          
          if (dx > 0) {
            this.aimAngle = -Math.PI/4 - Math.random() * Math.PI/6;
          } else {
            this.aimAngle = -Math.PI * 3/4 + Math.random() * Math.PI/6;
          }
          
          setTimeout(() => {
            if (!worm.alive || !this.isAiming) return;
            this.fireWeapon();
          }, 800 + Math.random() * 1500);
        }
        break;

      case 'useItem':
        game.useItem(action.item);
        break;

      case 'placeMine':
        // Move to position and place mine
        const moveDir = action.targetX > worm.x ? 1 : -1;
        worm.facing = moveDir;
        worm.moving = true;
        
        setTimeout(() => {
          worm.moving = false;
          game.placeMine(worm);
        }, 2000);
        break;

      case 'skip':
        game.skipTurn();
        break;
    }
  }

  fireWeapon() {
    if (!this.worm.alive || !this.selectedWeapon) return;
    
    const worm = this.worm;
    const w = this.selectedWeapon;
    
    // Face the aim direction
    worm.facing = Math.cos(this.aimAngle) > 0 ? 1 : -1;

    if (w.id === 'earthquake') {
      game.triggerEarthquake(worm);
      this.isAiming = false;
      return;
    }

    const speed = w.speed * (this.aimPower / 50);
    
    if (w.id === 'shotgun') {
      for (let i = -1; i <= 1; i++) {
        game.projectiles.push(new Projectile(
          worm.x, worm.y - 20,
          Math.cos(this.aimAngle + i * 0.15) * speed,
          Math.sin(this.aimAngle + i * 0.15) * speed,
          w, worm
        ));
      }
    } else if (w.id === 'minigun') {
      for (let i = 0; i < 5; i++) {
        setTimeout(() => {
          if (!worm.alive) return;
          game.projectiles.push(new Projectile(
            worm.x, worm.y - 20,
            Math.cos(this.aimAngle + (Math.random()-0.5)*0.1) * speed,
            Math.sin(this.aimAngle + (Math.random()-0.5)*0.1) * speed,
            w, worm
          ));
        }, i * 120);
      }
    } else {
      game.projectiles.push(new Projectile(
        worm.x, worm.y - 20,
        Math.cos(this.aimAngle) * speed,
        Math.sin(this.aimAngle) * speed,
        w, worm
      ));
    }

    audio.play('whoosh');
    this.isAiming = false;
    game.checkGameEnd();
  }

  updateAim(dt) {
    // Slight aim adjustment over time for AI
    if (Math.random() < 0.02) {
      this.aimAngle += (Math.random() - 0.5) * 0.05;
    }
  }

  findNearestEnemy(enemies) {
    let nearest = enemies[0];
    let minDist = Infinity;
    
    for (const e of enemies) {
      const d = Math.hypot(e.x - this.worm.x, e.y - this.worm.y);
      if (d < minDist) { minDist = d; nearest = e; }
    }
    
    return nearest;
  }

  assessRisks() {
    const terrain = game.terrain;
    const worm = this.worm;
    
    // Water risk - how close to water level?
    const distToWater = Math.max(0, terrain.waterLevel - (worm.y + worm.radius));
    const waterRisk = distToWater < 100 ? 1 - distToWater / 100 : 0;
    
    // Cliff risk - is there solid ground below?
    const belowY = Math.floor((worm.y + worm.radius + 20) / terrain.res);
    const centerX = Math.floor(worm.x / terrain.res);
    let cliffRisk = 0;
    
    if (centerX >= 0 && centerX < terrain.cols && belowY >= 0 && belowY < terrain.rows) {
      if (terrain.grid[terrain.idx(centerX, belowY)] === 0) {
        // No ground below - high risk!
        cliffRisk = 1;
        
        // Check sides for escape route
        let hasSideGround = false;
        for (let dx = -5; dx <= 5; dx++) {
          const sx = centerX + dx;
          if (sx >= 0 && sx < terrain.cols) {
            for (let dy = 0; dy < 10; dy++) {
              if (belowY + dy < terrain.rows && terrain.grid[terrain.idx(sx, belowY + dy)] > 0) {
                hasSideGround = true;
                break;
              }
            }
          }
          if (hasSideGround) break;
        }
        if (hasSideGround) cliffRisk *= 0.5; // Lower risk if there's ground nearby
      }
    }
    
    return { waterRisk, cliffRisk };
  }

  findSafeDirection() {
    const terrain = game.terrain;
    const worm = this.worm;
    const centerX = Math.floor(worm.x / terrain.res);
    const centerY = Math.floor((worm.y + worm.radius) / terrain.res);
    
    // Check which direction has more solid ground below
    let leftScore = 0, rightScore = 0;
    
    for (let dx = -15; dx <= 15; dx++) {
      const cx = centerX + dx;
      if (cx < 0 || cx >= terrain.cols) continue;
      
      // Check ground below this position
      let hasGround = false;
      for (let dy = centerY; dy < Math.min(centerY + 20, terrain.rows); dy++) {
        if (terrain.grid[terrain.idx(cx, dy)] > 0) {
          hasGround = true;
          break;
        }
      }
      
      if (hasGround) {
        if (dx < 0) leftScore++;
        else rightScore++;
      }
    }
    
    // Also consider water level
    const distToLeftWater = Math.max(0, terrain.waterLevel - worm.y);
    
    if (leftScore > rightScore + 3) return -1;
    if (rightScore > leftScore + 3) return 1;
    
    return null; // No clear safe direction
  }

  reset() {
    this.actionQueue = [];
    this.currentAction = null;
    this.selectedWeapon = null;
    this.isAiming = false;
    this.thinkingTimer = 0;
  }
}

// ── let: game ──
let game;

// ── class: ArtilleryGame ──
// Global reference for AI and projectiles to access

class ArtilleryGame {
  constructor() {
    this.canvas = document.getElementById('gameCanvas');
    this.ctx = this.canvas.getContext('2d');
    
    this.state = 'menu'; // menu, setup, playing, gameOver
    this.terrain = null;
    this.teams = [];
    this.worms = [];
    this.projectiles = [];
    this.mines = [];
    this.particles = new ParticleSystem();
    this.camera = new Camera();
    this.floatingTexts = [];
    
    this.activeTeamIndex = 0;
    this.activeWormIndex = 0;
    this.activeWorm = null;
    this.turnTimer = CONFIG.TURN_TIME;
    this.maxTurnTime = CONFIG.TURN_TIME;
    this.wind = (Math.random() - 0.5) * 200;
    
    this.screenShake = 0;
    this.freezeTimer = 0;
    
    // Input state
    this.keys = {};
    this.mousePos = { x: 0, y: 0 };
    this.mouseDown = false;
    this.aimAngle = -Math.PI / 4;
    this.aimPower = 50;
    this.isAiming = false;
    this.selectedWeapon = null;
    this.showingWeapons = false;
    this.showingItems = false;
    this.weaponWheelHover = -1;
    
    // Movement state
    this.movingLeft = false;
    this.movingRight = false;
    this.jumpPressed = false;

    // Terrain type for current game
    this.terrainType = 'island';
  }

  init() {
    this.resize();
    window.addEventListener('resize', () => this.resize());
    
    // Input handlers
    document.addEventListener('keydown', e => {
      this.keys[e.key] = true;
      
      if (this.state === 'playing') {
        if (e.key === ' ' || e.key === 'ArrowUp') {
          e.preventDefault();
          if (!this.jumpPressed && this.activeWorm && this.activeWorm.onGround) {
            this.doJump();
            this.jumpPressed = true;
          }
        }
        if (e.key === 'Escape' || e.key === 'q') {
          this.cancelAim();
        }
        // Number keys for quick weapon select
        const numKey = parseInt(e.key);
        if (numKey >= 1 && numKey <= WEAPONS.length) {
          audio.init();
          this.quickSelectWeapon(numKey - 1);
        }
      }
    });

    document.addEventListener('keyup', e => {
      this.keys[e.key] = false;
      if (e.key === ' ' || e.key === 'ArrowUp') this.jumpPressed = false;
    });

    // Mouse wheel for aim power
    this.canvas.addEventListener('wheel', e => {
      if (this.state !== 'playing' || !this.isAiming) return;
      e.preventDefault();
      this.aimPower = Math.max(10, Math.min(95, this.aimPower + (e.deltaY > 0 ? -3 : 3)));
    }, { passive: false });

    this.canvas.addEventListener('mousemove', e => {
      const rect = this.canvas.getBoundingClientRect();
      const scaleX = this.canvas.width / rect.width;
      const scaleY = this.canvas.height / rect.height;
      this.mousePos.x = (e.clientX - rect.left) * scaleX;
      this.mousePos.y = (e.clientY - rect.top) * scaleY;

      // Update aim angle based on mouse position during aiming
      if (this.isAiming && this.activeWorm) {
        const worldPos = this.camera.screenToWorld(
          e.clientX - rect.left, e.clientY - rect.top, 
          window.innerWidth, window.innerHeight
        );
        const dx = worldPos.x - this.activeWorm.x;
        const dy = (worldPos.y) - (this.activeWorm.y - 20);
        this.aimAngle = Math.atan2(dy, dx);
      }
    });

    this.canvas.addEventListener('mousedown', e => {
      if (this.state !== 'playing') return;
      audio.init();
      
      const rect = this.canvas.getBoundingClientRect();
      const scaleX = this.canvas.width / rect.width;
      const scaleY = this.canvas.height / rect.height;
      this.mousePos.x = (e.clientX - rect.left) * scaleX;
      this.mousePos.y = (e.clientY - rect.top) * scaleY;
      
      if (this.showingWeapons) {
        this.selectWeaponFromWheel();
      } else if (this.isAiming && e.button === 0) {
        // Fire!
        this.fireProjectile();
      } else if (!this.isAiming && !this.showingWeapons && !this.showingItems) {
        // Click on terrain to start aiming direction
        if (this.activeWorm && this.selectedWeapon) {
          const worldPos = this.camera.screenToWorld(
            e.clientX - rect.left, e.clientY - rect.top,
            window.innerWidth, window.innerHeight
          );
          const dx = worldPos.x - this.activeWorm.x;
          const dy = worldPos.y - (this.activeWorm.y - 20);
          this.aimAngle = Math.atan2(dy, dx);
          this.isAiming = true;
        }
      }
    });

    this.canvas.addEventListener('mouseup', () => {
      if (this.movingLeft || this.movingRight) {
        this.activeWorm && (this.activeWorm.moving = false);
        this.movingLeft = false;
        this.movingRight = false;
      }
    });

    // UI button handlers
    document.getElementById('btnQuickPlay').addEventListener('click', () => {
      audio.init(); audio.play('click');
      this.startGame(2, 'island');
    });

    document.getElementById('btnSetup').addEventListener('click', () => {
      audio.init(); audio.play('click');
      this.showScreen('setup');
      this.updateTeamSetupUI();
    });

    document.getElementById('teamCount').addEventListener('change', () => {
      audio.init(); audio.play('click');
      this.updateTeamSetupUI();
    });

    document.getElementById('btnStart').addEventListener('click', () => {
      audio.init(); audio.play('select');
      const teamCount = parseInt(document.getElementById('teamCount').value);
      const terrainType = document.getElementById('terrainType').value;
      
      this.startGame(teamCount, terrainType);
    });

    document.getElementById('btnMove').addEventListener('click', () => {
      audio.init(); audio.play('click');
      if (this.state === 'playing' && !this.isAiming) {
        // Toggle movement hint
        game.floatingTexts.push(new FloatingText(
          this.activeWorm.x, this.activeWorm.y - 50, 
          '← → to move, Space to jump', '#88ccff', 12
        ));
      }
    });

    document.getElementById('btnAttack').addEventListener('click', () => {
      audio.init(); audio.play('click');
      if (this.state === 'playing') {
        this.showingWeapons = !this.showingWeapons;
        this.showingItems = false;
        document.getElementById('weaponWheel').classList.toggle('hidden', !this.showingWeapons);
        if (this.showingWeapons) this.renderWeaponWheel();
      }
    });

    document.getElementById('btnItem').addEventListener('click', () => {
      audio.init(); audio.play('click');
      if (this.state === 'playing') {
        this.showingItems = !this.showingItems;
        this.showingWeapons = false;
        document.getElementById('weaponWheel').classList.toggle('hidden', !this.showingItems);
        if (this.showingItems) this.renderItemWheel();
      }
    });

    document.getElementById('btnSkip').addEventListener('click', () => {
      audio.init(); audio.play('warning');
      if (this.state === 'playing') {
        this.skipTurn();
      }
    });

    document.getElementById('btnRestart').addEventListener('click', () => {
      audio.init(); audio.play('click');
      this.showScreen('menu');
    });

    // Start render loop
    this.lastTime = performance.now();
    requestAnimationFrame(t => this.gameLoop(t));
  }

  resize() {
    const dpr = window.devicePixelRatio || 1;
    this.canvas.width = window.innerWidth * dpr;
    this.canvas.height = window.innerHeight * dpr;
    this.ctx.scale(dpr, dpr);
    this.canvas.style.width = window.innerWidth + 'px';
    this.canvas.style.height = window.innerHeight + 'px';
  }

  showScreen(name) {
    document.querySelectorAll('.screen').forEach(s => s.classList.add('hidden'));
    const screenId = name === 'menu' ? 'menuScreen' : 
      name === 'setup' ? 'setupScreen' : 
      name === 'playing' ? 'hud' : 'gameOverScreen';
    document.getElementById(screenId).classList.remove('hidden');
    
    if (name !== 'playing') {
      document.getElementById('hud').classList.add('hidden');
    } else {
      document.getElementById('hud').classList.remove('hidden');
    }
  }

  updateTeamSetupUI() {
    const count = parseInt(document.getElementById('teamCount').value);
    const container = document.getElementById('teamSetup');
    container.innerHTML = '';
    
    for (let i = 0; i < count; i++) {
      const row = document.createElement('div');
      row.className = 'teamRow';
      
      const colorDot = document.createElement('span');
      colorDot.style.cssText = `display:inline-block;width:16px;height:16px;border-radius:50%;background:${TEAM_COLORS[i].primary};margin-right:8px;`;
      
      const nameInput = document.createElement('input');
      nameInput.type = 'text';
      nameInput.value = TEAM_COLORS[i].name;
      nameInput.id = `teamName${i}`;
      nameInput.style.color = TEAM_COLORS[i].primary;
      
      const typeSelect = document.createElement('select');
      typeSelect.id = `teamType${i}`;
      typeSelect.innerHTML = '<option value="human">Human</option><option value="cpu">CPU</option>';
      
      row.appendChild(colorDot);
      row.appendChild(nameInput);
      row.appendChild(typeSelect);
      container.appendChild(row);
    }
  }

  startGame(teamCount, terrainType) {
    this.terrainType = terrainType;
    this.state = 'playing';
    
    // Generate terrain
    this.terrain = new Terrain(CONFIG.WORLD_WIDTH, CONFIG.WORLD_HEIGHT);
    this.terrain.generate(terrainType);
    
    // Create teams and worms
    const spawnPoints = this.terrain.findSpawnPoints(teamCount * 4);
    this.teams = [];
    this.worms = [];
    
    let wormIdx = 0;
    for (let t = 0; t < teamCount; t++) {
      const isCPU = document.getElementById(`teamType${t}`)?.value === 'cpu';
      const name = document.getElementById(`teamName${t}`)?.value || TEAM_COLORS[t].name;
      
      this.teams.push({
        id: t,
        name: name,
        color: TEAM_COLORS[t],
        isCPU: isCPU,
        units: []
      });
      
      for (let u = 0; u < 4; u++) {
        const sp = spawnPoints[wormIdx % spawnPoints.length];
        const wormName = WORM_NAMES[(t * 4 + u) % WORM_NAMES.length];
        
        const worm = new Worm(wormIdx, t, wormName, sp.x, sp.y);
        // Give random starting items
        this.assignRandomItems(worm);
        
        this.worms.push(worm);
        this.teams[t].units.push(worm);
        wormIdx++;
      }
    }

    // Create AI controllers for CPU teams
    this.aiControllers = {};
    for (const team of this.teams) {
      if (team.isCPU) {
        for (const unit of team.units) {
          this.aiControllers[unit.id] = new AIController(unit, this);
        }
      }
    }

    // Reset game state
    this.activeTeamIndex = 0;
    this.activeWormIndex = 0;
    this.projectiles = [];
    this.mines = [];
    this.particles.clear();
    this.floatingTexts = [];
    this.wind = (Math.random() - 0.5) * 200;
    this.screenShake = 0;
    this.freezeTimer = 0;
    
    // Set first active worm
    this.setActiveWorm(0, 0);
    
    this.showScreen('playing');
    audio.play('select');
  }

  assignRandomItems(worm) {
    worm.items = [];
    const itemPool = ['parachute', 'medikit', 'extraTime', 'windGauge', 'mine'];
    // Give 2-3 random items
    const count = 2 + Math.floor(Math.random() * 2);
    for (let i = 0; i < count; i++) {
      const item = itemPool[Math.floor(Math.random() * itemPool.length)];
      if (!worm.items.includes(item)) {
        worm.items.push(item);
      }
    }
  }

  setActiveWorm(teamIdx, wormIdx) {
    const team = this.teams[teamIdx];
    if (!team) return;
    
    // Find next alive worm in this team
    let found = false;
    for (let i = 0; i < team.units.length; i++) {
      const idx = (wormIdx + i) % team.units.length;
      if (team.units[idx].alive) {
        this.activeWormIndex = idx;
        this.activeWorm = team.units[idx];
        found = true;
        
        // Reset worm state for new turn
        this.activeWorm.moveTime = CONFIG.TURN_TIME * 0.4;
        this.activeWorm.maxMoveTime = CONFIG.TURN_TIME * 0.4;
        this.activeWorm.hasParachute = false;
        this.activeWorm.drownTimer = 0;
        
        // Give random items each turn
        if (!team.isCPU) {
          this.assignRandomItems(this.activeWorm);
        }
        
        break;
      }
    }
    
    if (!found) {
      // Team eliminated! Skip to next team
      this.nextTeam();
      return;
    }

    this.turnTimer = CONFIG.TURN_TIME;
    this.maxTurnTime = CONFIG.TURN_TIME;
    this.isAiming = false;
    this.selectedWeapon = null;
    this.showingWeapons = false;
    this.showingItems = false;
    
    // Reset AI for CPU worms
    if (this.aiControllers[this.activeWorm.id]) {
      this.aiControllers[this.activeWorm.id].reset();
    }

    document.getElementById('weaponWheel').classList.add('hidden');
    this.updateHUD();
  }

  nextTeam() {
    let attempts = 0;
    do {
      this.activeTeamIndex = (this.activeTeamIndex + 1) % this.teams.length;
      this.activeWormIndex = 0;
      attempts++;
      
      // Check if team has any alive worms
      const team = this.teams[this.activeTeamIndex];
      if (team.units.some(u => u.alive)) break;
    } while (attempts < this.teams.length);

    // Change wind each round
    this.wind = Math.max(-200, Math.min(200, this.wind + (Math.random() - 0.5) * 100));
    
    this.setActiveWorm(this.activeTeamIndex, 0);
  }

  nextWorm() {
    const team = this.teams[this.activeTeamIndex];
    let found = false;
    for (let i = 1; i <= team.units.length; i++) {
      const idx = (this.activeWormIndex + i) % team.units.length;
      if (team.units[idx].alive) {
        this.setActiveWorm(this.activeTeamIndex, idx);
        found = true;
        break;
      }
    }
    if (!found) this.nextTeam();
  }

  skipTurn() {
    audio.play('warning');
    
    // Check if current team has more alive worms
    const team = this.teams[this.activeTeamIndex];
    let hasMoreWorms = false;
    for (let i = 0; i < team.units.length; i++) {
      if (i !== this.activeWormIndex && team.units[i].alive) {
        hasMoreWorms = true;
        break;
      }
    }
    
    if (hasMoreWorms) {
      this.nextWorm();
    } else {
      this.nextTeam();
    }
  }

  doJump() {
    if (!this.activeWorm || !this.activeWorm.onGround) return;
    
    this.activeWorm.vy = CONFIG.JUMP_FORCE * (this.activeWorm.hasParachute ? 0.7 : 1);
    audio.play('jump');
    game.particles.jumpDust(this.activeWorm.x, this.activeWorm.y + this.activeWorm.radius);
  }

  quickSelectWeapon(idx) {
    if (!this.activeWorm || !this.activeWorm.alive) return;
    
    const weapon = WEAPONS[idx];
    if (!weapon) return;
    
    // Check ammo cost vs move time
    if (weapon.ammoCost > this.activeWorm.moveTime * 0.5 + 1) {
      game.floatingTexts.push(new FloatingText(
        this.activeWorm.x, this.activeWorm.y - 40, 
        'Not enough time!', '#ff8800'
      ));
      return;
    }

    this.selectedWeapon = weapon;
    audio.play('select');
    
    // Start aiming with default angle toward nearest enemy or facing direction
    const enemies = this.worms.filter(w => w.alive && w.teamId !== this.activeWorm.teamId);
    if (enemies.length > 0) {
      let nearest = enemies[0];
      let minDist = Infinity;
      for (const e of enemies) {
        const d = Math.hypot(e.x - this.activeWorm.x, e.y - this.activeWorm.y);
        if (d < minDist) { minDist = d; nearest = e; }
      }
      
      const dx = nearest.x - this.activeWorm.x;
      const dy = nearest.y - (this.activeWorm.y - 20);
      this.aimAngle = Math.atan2(dy, dx);
    } else {
      this.aimAngle = this.activeWorm.facing > 0 ? -Math.PI/4 : -Math.PI*3/4;
    }
    
    if (weapon.type === 'direct') {
      // Direct weapons fire immediately at angle
      setTimeout(() => this.fireProjectile(), 300);
    } else {
      this.isAiming = true;
    }
    
    this.showingWeapons = false;
    document.getElementById('weaponWheel').classList.add('hidden');
  }

  selectWeaponFromWheel() {
    if (this.weaponWheelHover < 0) return;
    
    const worm = this.activeWorm;
    if (!worm || !worm.alive) return;
    
    // Check ammo cost vs move time
    const weapon = WEAPONS[this.weaponWheelHover];
    if (weapon.ammoCost > worm.moveTime * 0.5 + 1) {
      game.floatingTexts.push(new FloatingText(worm.x, worm.y - 40, 'Not enough time!', '#ff8800'));
      return;
    }

    this.selectedWeapon = weapon;
    audio.play('select');
    
    // Start aiming
    const worldPos = this.camera.screenToWorld(this.mousePos.x, this.mousePos.y, 
      window.innerWidth, window.innerHeight);
    const dx = worldPos.x - worm.x;
    const dy = worldPos.y - (worm.y - 20);
    this.aimAngle = Math.atan2(dy, dx);
    
    if (weapon.type === 'direct') {
      // Direct weapons fire immediately at angle
      setTimeout(() => this.fireProjectile(), 300);
    } else {
      this.isAiming = true;
    }
    
    this.showingWeapons = false;
    document.getElementById('weaponWheel').classList.add('hidden');
  }

  fireProjectile() {
    if (!this.activeWorm || !this.selectedWeapon) return;
    
    const worm = this.activeWorm;
    const w = this.selectedWeapon;
    
    // Face the aim direction
    worm.facing = Math.cos(this.aimAngle) > 0 ? 1 : -1;

    if (w.id === 'earthquake') {
      this.triggerEarthquake(worm);
      this.isAiming = false;
      return;
    }

    const speed = w.speed * (this.aimPower / 50);
    
    if (w.id === 'shotgun') {
      for (let i = -1; i <= 1; i++) {
        this.projectiles.push(new Projectile(
          worm.x, worm.y - 20,
          Math.cos(this.aimAngle + i * 0.15) * speed,
          Math.sin(this.aimAngle + i * 0.15) * speed,
          w, worm
        ));
      }
    } else if (w.id === 'minigun') {
      for (let i = 0; i < 5; i++) {
        setTimeout(() => {
          if (!worm.alive) return;
          this.projectiles.push(new Projectile(
            worm.x, worm.y - 20,
            Math.cos(this.aimAngle + (Math.random()-0.5)*0.1) * speed,
            Math.sin(this.aimAngle + (Math.random()-0.5)*0.1) * speed,
            w, worm
          ));
        }, i * 120);
      }
    } else {
      this.projectiles.push(new Projectile(
        worm.x, worm.y - 20,
        Math.cos(this.aimAngle) * speed,
        Math.sin(this.aimAngle) * speed,
        w, worm
      ));
    }

    audio.play('whoosh');
    this.isAiming = false;
    
    // Deduct move time for weapon use
    worm.moveTime -= w.ammoCost * 2;
    
    this.checkGameEnd();
  }

  cancelAim() {
    this.isAiming = false;
    this.selectedWeapon = null;
    audio.play('click');
  }

  triggerEarthquake(worm) {
    game.screenShake = 15;
    game.freezeTimer = 0.3;
    audio.play('explosion', { size: 'large' });
    
    // Damage all worms based on height above ground
    for (const w of this.worms) {
      if (!w.alive) continue;
      
      const groundY = this.terrain.getTerrainHeight(w.x);
      const heightAboveGround = Math.max(0, groundY - w.y);
      
      // Higher worms take more damage
      const dmg = Math.floor(heightAboveGround / 15 + 10);
      if (dmg > 0) {
        w.takeDamage(dmg, w.x, this.terrain.waterLevel);
        
        // Knock them down
        w.vy += 200;
        w.vx += (Math.random() - 0.5) * 100;
      }
      
      if (w.alive && w.teamId !== worm.teamId) {
        w.laughing = true;
        w.laughTimer = 2;
        audio.play('laugh');
      }
    }

    // Visual effect - shake all particles
    for (let i = 0; i < 50; i++) {
      const x = Math.random() * CONFIG.WORLD_WIDTH;
      const y = this.terrain.getTerrainHeight(x) + Math.random() * 20;
      game.particles.emit(x, y, 2, {
        speedMin: 10, speedMax: 50,
        lifeMin: 0.3, lifeMax: 0.8,
        color: `rgb(${140+Math.random()*40|0}, ${120+Math.random()*30|0}, ${90+Math.random()*20|0})`,
        sizeMin: 2, sizeMax: 5,
        type: 'debris'
      });
    }

    worm.moveTime -= 6;
    this.checkGameEnd();
  }

  useItem(itemId) {
    const worm = this.activeWorm;
    if (!worm || !worm.alive) return;
    
    const itemIdx = worm.items.indexOf(itemId);
    if (itemIdx < 0) return;
    
    worm.items.splice(itemIdx, 1);
    audio.play('select');

    switch (itemId) {
      case 'parachute':
        worm.hasParachute = true;
        game.floatingTexts.push(new FloatingText(worm.x, worm.y - 40, 'Parachute!', '#ff8844'));
        break;
      case 'extraTime':
        this.turnTimer += 10;
        this.maxTurnTime += 10;
        game.floatingTexts.push(new FloatingText(worm.x, worm.y - 40, '+10 seconds!', '#44ff88'));
        break;
      case 'windGauge':
        game.floatingTexts.push(new FloatingText(
          worm.x, worm.y - 50, 
          `Wind: ${Math.abs(this.wind).toFixed(0)} ${this.wind > 0 ? '→' : '←'}`, 
          '#88ccff', 14
        ));
        break;
      case 'medikit':
        worm.health = Math.min(CONFIG.MAX_HEALTH, worm.health + 30);
        game.floatingTexts.push(new FloatingText(worm.x, worm.y - 40, '+30 HP!', '#44ff44'));
        break;
    }

    this.showingItems = false;
    document.getElementById('weaponWheel').classList.add('hidden');
    this.updateHUD();
  }

  placeMine(worm) {
    const mineIdx = worm.items.indexOf('mine');
    if (mineIdx < 0) return;
    
    worm.items.splice(mineIdx, 1);
    this.mines.push(new Mine(worm.x, worm.y - 20, worm));
    game.floatingTexts.push(new FloatingText(worm.x, worm.y - 40, 'Mine placed!', '#ffaa00'));
    audio.play('click');
    
    this.showingItems = false;
    document.getElementById('weaponWheel').classList.add('hidden');
  }

  checkGameEnd() {
    // Check if only one team remains
    const aliveTeams = this.teams.filter(t => t.units.some(u => u.alive));
    
    if (aliveTeams.length <= 1) {
      setTimeout(() => {
        this.state = 'gameOver';
        
        let winnerText;
        if (aliveTeams.length === 0) {
          winnerText = "It's a draw! Everyone died!";
        } else {
          const aliveCount = aliveTeams[0].units.filter(u => u.alive).length;
          winnerText = `${aliveTeams[0].name} wins with ${aliveCount} worm${aliveCount !== 1 ? 's' : ''} left!`;
        }
        
        document.getElementById('winnerText').textContent = winnerText;
        this.showScreen('gameOver');
      }, 2000);
    }
  }

  updateHUD() {
    if (!this.activeWorm) return;
    
    const team = this.teams[this.activeTeamIndex];
    document.getElementById('currentTeamName').textContent = `${team.name}'s Turn`;
    document.getElementById('currentTeamName').style.color = team.color.primary;
    
    document.getElementById('windValue').textContent = 
      `${Math.abs(this.wind).toFixed(0)} ${this.wind > 0 ? '→' : this.wind < 0 ? '←' : '•'}`;
    document.getElementById('windValue').style.color = Math.abs(this.wind) > 100 ? '#ff4444' : '#aaa';
    
    document.getElementById('wormName').textContent = this.activeWorm.name;
    document.getElementById('wormName').style.color = team.color.primary;
    
    const healthPct = (this.activeWorm.health / CONFIG.MAX_HEALTH) * 100;
    document.getElementById('healthFill').style.width = `${healthPct}%`;
    document.getElementById('healthText').textContent = Math.ceil(this.activeWorm.health);

    // Update timer text
    document.getElementById('timerText').textContent = `${Math.ceil(this.turnTimer)}s`;
  }

  renderWeaponWheel() {
    const canvas = document.getElementById('wheelCanvas');
    if (!canvas) return;
    
    const ctx = canvas.getContext('2d');
    const w = canvas.width, h = canvas.height;
    ctx.clearRect(0, 0, w, h);

    const worm = this.activeWorm;
    const availableWeapons = WEAPONS.filter(w => w.ammoCost <= worm.moveTime * 0.5 + 1);
    
    const cols = Math.ceil(Math.sqrt(availableWeapons.length));
    const rows = Math.ceil(availableWeapons.length / cols);
    const cellW = (w - 20) / cols;
    const cellH = (h - 30) / rows;

    this.weaponWheelHover = -1;

    availableWeapons.forEach((weapon, i) => {
      const col = i % cols;
      const row = Math.floor(i / cols);
      const cx = 10 + col * cellW + cellW/2;
      const cy = 15 + row * cellH + cellH/2;

      // Check hover using canvas-relative mouse position
      const rect = canvas.getBoundingClientRect();
      const scaleX = w / rect.width;
      const scaleY = h / rect.height;
      const mx = (this.mousePos.x - this.canvas.offsetLeft) * scaleX;
      const my = (this.mousePos.y - this.canvas.offsetTop) * scaleY;

      // Simple hover detection based on screen position mapped to canvas
      const isHovered = Math.abs(mx - cx) < cellW/2 && Math.abs(my - cy) < cellH/2;
      if (isHovered) this.weaponWheelHover = WEAPONS.indexOf(weapon);

      // Background
      ctx.fillStyle = isHovered ? 'rgba(255,107,53,0.4)' : 'rgba(0,0,0,0.6)';
      ctx.strokeStyle = isHovered ? '#ff6b35' : '#444';
      ctx.lineWidth = 2;
      
      const pad = 4;
      if (ctx.roundRect) {
        ctx.beginPath();
        ctx.roundRect(10 + col * cellW + pad, 15 + row * cellH + pad, 
          cellW - pad*2, cellH - pad*2, 8);
        ctx.fill();
        ctx.stroke();
      } else {
        ctx.fillRect(10 + col * cellW + pad, 15 + row * cellH + pad, 
          cellW - pad*2, cellH - pad*2);
        ctx.strokeRect(10 + col * cellW + pad, 15 + row * cellH + pad, 
          cellW - pad*2, cellH - pad*2);
      }

      // Icon
      ctx.font = '24px Arial';
      ctx.textAlign = 'center';
      ctx.fillText(weapon.icon, cx, cy + 5);

      // Name
      ctx.font = 'bold 10px Arial';
      ctx.fillStyle = '#fff';
      ctx.fillText(weapon.name, cx, cy + cellH/2 - 8);

      // Type indicator
      ctx.font = '9px Arial';
      ctx.fillStyle = weapon.type === 'indirect' ? '#ffaa44' : 
        weapon.type === 'direct' ? '#44aaff' : '#ff4444';
      ctx.fillText(weapon.type.toUpperCase(), cx, cy + cellH/2 - 0);

      // Ammo cost
      ctx.fillStyle = '#aaa';
      ctx.font = '9px Arial';
      ctx.fillText(`Cost: ${weapon.ammoCost}`, cx, cy + cellH/2 + 10);
    });
  }

  renderItemWheel() {
    const canvas = document.getElementById('wheelCanvas');
    if (!canvas) return;
    
    const ctx = canvas.getContext('2d');
    const w = canvas.width, h = canvas.height;
    ctx.clearRect(0, 0, w, h);

    const worm = this.activeWorm;
    
    // Show all items (owned and available)
    const cols = Math.ceil(Math.sqrt(ITEMS.length));
    const cellW = (w - 20) / cols;
    const cellH = (h - 30);

    ITEMS.forEach((item, i) => {
      const col = i % cols;
      const row = Math.floor(i / cols);
      const cx = 10 + col * cellW + cellW/2;
      const cy = 15 + row * cellH + cellH/2;

      const hasItem = worm.items.includes(item.id);
      
      ctx.fillStyle = hasItem ? 'rgba(68,136,255,0.3)' : 'rgba(0,0,0,0.4)';
      ctx.strokeStyle = hasItem ? '#4488ff' : '#333';
      ctx.lineWidth = 2;
      
      const pad = 4;
      if (ctx.roundRect) {
        ctx.beginPath();
        ctx.roundRect(10 + col * cellW + pad, 15 + row * cellH + pad, 
          cellW - pad*2, cellH - pad*2, 8);
        ctx.fill();
        ctx.stroke();
      } else {
        ctx.fillRect(10 + col * cellW + pad, 15 + row * cellH + pad, 
          cellW - pad*2, cellH - pad*2);
        ctx.strokeRect(10 + col * cellW + pad, 15 + row * cellH + pad, 
          cellW - pad*2, cellH - pad*2);
      }

      ctx.font = '24px Arial';
      ctx.textAlign = 'center';
      ctx.globalAlpha = hasItem ? 1 : 0.3;
      ctx.fillText(item.icon, cx, cy + 5);
      ctx.globalAlpha = 1;

      ctx.font = 'bold 10px Arial';
      ctx.fillStyle = '#fff';
      ctx.fillText(item.name, cx, cy + cellH/2 - 8);
      
      if (hasItem) {
        ctx.fillStyle = '#44ff44';
        ctx.font = '9px Arial';
        ctx.fillText('OWNED', cx, cy + cellH/2 + 5);
      } else {
        ctx.fillStyle = '#666';
        ctx.font = '9px Arial';
        ctx.fillText('MISSING', cx, cy + cellH/2 + 5);
      }
    });

    // Click handler for items
    canvas.onclick = (e) => {
      const rect = canvas.getBoundingClientRect();
      const mx = e.clientX - rect.left;
      const my = e.clientY - rect.top;
      
      ITEMS.forEach((item, i) => {
        const col = i % cols;
        const row = Math.floor(i / cols);
        const cx = 10 + col * cellW + cellW/2;
        const cy = 15 + row * cellH + cellH/2;
        
        if (Math.abs(mx - cx) < cellW/2 && Math.abs(my - cy) < cellH/2) {
          if (item.id === 'mine') {
            this.placeMine(worm);
          } else {
            this.useItem(item.id);
          }
        }
      });
    };
  }

  update(dt) {
    if (this.state !== 'playing') return;

    // Freeze frame for big explosions
    if (this.freezeTimer > 0) {
      this.freezeTimer -= dt;
      if (this.freezeTimer > 0) return;
    }

    const worm = this.activeWorm;
    
    // Update turn timer
    this.turnTimer -= dt;
    
    // Timer warning sounds
    if (this.turnTimer <= 5 && this.turnTimer > 4.9) {
      audio.play('warning');
    }

    if (this.turnTimer <= 0) {
      this.skipTurn();
      return;
    }

    // Handle player movement input
    if (!this.isAiming && !this.showingWeapons && !this.showingItems && worm && worm.alive) {
      const wasMoving = worm.moving;
      
      if ((this.keys['ArrowLeft'] || this.keys['a']) && worm.moveTime > 0) {
        worm.facing = -1;
        worm.moving = true;
        worm.vx = -CONFIG.UNIT_SPEED;
        worm.moveTime -= dt;
        
        if (Math.random() < 0.3) game.particles.moveDust(worm.x, worm.y);
      } else if ((this.keys['ArrowRight'] || this.keys['d']) && worm.moveTime > 0) {
        worm.facing = 1;
        worm.moving = true;
        worm.vx = CONFIG.UNIT_SPEED;
        worm.moveTime -= dt;
        
        if (Math.random() < 0.3) game.particles.moveDust(worm.x, worm.y);
      } else {
        worm.moving = false;
        worm.vx *= 0.9;
      }

      // Wind effect on moving worms
      if (worm.moving) {
        worm.vx += this.wind * dt * 0.1;
      }
    }

    // Update active worm
    if (worm && worm.alive) {
      worm.update(dt, this.terrain);
      
      // Check if worm fell off world
      if (worm.y > CONFIG.WORLD_HEIGHT + 200) {
        worm.health = 0;
        worm.alive = false;
        audio.play('death');
        
        setTimeout(() => {
          const team = this.teams[this.activeTeamIndex];
          let hasMoreWorms = team.units.some((u, i) => u.alive && i !== this.activeWormIndex);
          if (hasMoreWorms) this.nextWorm();
          else this.nextTeam();
        }, 1000);
      }
    }

    // Update all worms (for physics even when not active)
    for (const w of this.worms) {
      if (w !== worm && w.alive) {
        w.update(dt, this.terrain);
        
        // Laugh timer decay
        if (w.laughing) {
          w.laughTimer -= dt;
          if (w.laughTimer <= 0) w.laughing = false;
        }
      }
    }

    // Update projectiles
    for (const p of this.projectiles) {
      p.update(dt, this.terrain, this.worms);
    }
    this.projectiles = this.projectiles.filter(p => p.alive);

    // Update mines
    for (const m of this.mines) {
      m.update(dt, this.terrain, this.worms);
    }
    this.mines = this.mines.filter(m => m.alive);

    // Update particles
    this.particles.update(dt);

    // Update floating texts
    this.floatingTexts = this.floatingTexts.filter(t => t.update(dt));

    // Update camera
    this.camera.follow(this.activeWorm, this.worms);
    
    // Apply screen shake from explosions
    if (this.screenShake > 0) {
      this.camera.shakeAmount = Math.max(this.camera.shakeAmount, this.screenShake);
      this.screenShake *= 0.9;
      if (this.screenShake < 0.5) this.screenShake = 0;
    }

    // Update AI controllers
    for (const id in this.aiControllers) {
      this.aiControllers[id].update(dt);
    }

    // Update HUD periodically
    this.updateHUD();
    
    // Render weapon wheel if showing
    if (this.showingWeapons) {
      this.renderWeaponWheel();
    }
  }

  render() {
    const ctx = this.ctx;
    const cw = window.innerWidth;
    const ch = window.innerHeight;
    
    ctx.clearRect(0, 0, cw, ch);

    if (this.state === 'menu' || this.state === 'setup') {
      // Animated background for menu
      this.renderMenuBackground(ctx, cw, ch);
      return;
    }

    if (this.state !== 'playing' && this.state !== 'gameOver') return;

    // Apply camera transform
    this.camera.apply(ctx, cw, ch);

    // Render sky gradient
    const skyGrad = ctx.createLinearGradient(0, 0, 0, CONFIG.WORLD_HEIGHT);
    if (this.terrain.caveMode) {
      skyGrad.addColorStop(0, '#1a1a2e');
      skyGrad.addColorStop(0.5, '#16213e');
      skyGrad.addColorStop(1, '#0f3460');
    } else {
      skyGrad.addColorStop(0, '#87CEEB');
      skyGrad.addColorStop(0.4, '#B0E0FF');
      skyGrad.addColorStop(0.7, '#FFE4C4');
      skyGrad.addColorStop(1, '#DEB887');
    }
    ctx.fillStyle = skyGrad;
    ctx.fillRect(-500, -500, CONFIG.WORLD_WIDTH + 1000, CONFIG.WORLD_HEIGHT + 1000);

    // Render clouds (non-cave mode)
    if (!this.terrain.caveMode) {
      this.renderClouds(ctx);
    }

    // Render terrain
    this.renderTerrain(ctx);

    // Render water
    this.renderWater(ctx);

    // Render mines
    for (const m of this.mines) {
      m.draw(ctx, 0, 0, 1);
    }

    // Render worms
    for (const w of this.worms) {
      if (w.alive) w.draw(ctx, 0, 0, 1);
    }

    // Render projectiles
    for (const p of this.projectiles) {
      p.draw(ctx, 0, 0, 1);
    }

    // Render particles
    this.particles.draw(ctx);

    // Render floating texts
    for (const t of this.floatingTexts) {
      t.draw(ctx, 0, 0, 1);
    }

    // Render aim trajectory
    if (this.isAiming && this.activeWorm && this.selectedWeapon) {
      this.renderTrajectory(ctx);
    }

    // Unapply camera
    this.camera.unapply(ctx);

    // Render timer bar overlay
    this.renderTimerBar(ctx, cw, ch);

    // Render wind indicator on canvas
    this.renderWindArrow(ctx, cw, ch);

    // Render move time indicator
    if (this.activeWorm) {
      this.renderMoveTime(ctx, cw, ch);
    }
  }

  renderMenuBackground(ctx, w, h) {
    const t = Date.now() * 0.001;
    
    // Gradient background
    const grad = ctx.createRadialGradient(w/2, h/2, 0, w/2, h/2, w*0.7);
    grad.addColorStop(0, '#1a1a3e');
    grad.addColorStop(1, '#0a0a1a');
    ctx.fillStyle = grad;
    ctx.fillRect(0, 0, w, h);

    // Animated particles
    for (let i = 0; i < 50; i++) {
      const x = (Math.sin(t * 0.3 + i * 1.7) * 0.5 + 0.5) * w;
      const y = (Math.cos(t * 0.2 + i * 2.3) * 0.5 + 0.5) * h;
      const size = 2 + Math.sin(t + i) * 1;
      
      ctx.fillStyle = `rgba(255, ${107 + i*2|0}, ${53 + i|0}, ${0.2 + Math.sin(t+i)*0.1})`;
      ctx.beginPath();
      ctx.arc(x, y, size, 0, Math.PI * 2);
      ctx.fill();
    }
  }

  renderClouds(ctx) {
    const t = Date.now() * 0.0001;
    
    for (let i = 0; i < 8; i++) {
      const x = ((i * 350 + t * 200) % (CONFIG.WORLD_WIDTH + 400)) - 200;
      const y = 30 + Math.sin(i * 1.5) * 60;
      
      ctx.fillStyle = 'rgba(255, 255, 255, 0.4)';
      for (let j = 0; j < 5; j++) {
        const cx = x + j * 30 - 60;
        const cy = y + Math.sin(j * 1.2) * 10;
        ctx.beginPath();
        ctx.arc(cx, cy, 25 + Math.sin(i+j)*8, 0, Math.PI * 2);
        ctx.fill();
      }
    }
  }

  renderTerrain(ctx) {
    const terrain = this.terrain;
    const res = terrain.res;
    
    // Render visible cells only for performance
    const bounds = this.camera.getVisibleBounds();
    const startX = Math.max(0, Math.floor(bounds.left / res) - 1);
    const endX = Math.min(terrain.cols, Math.ceil(bounds.right / res) + 1);
    const startY = Math.max(0, Math.floor(bounds.top / res) - 1);
    const endY = Math.min(terrain.rows, Math.ceil(bounds.bottom / res) + 1);

    for (let x = startX; x < endX; x++) {
      for (let y = startY; y < endY; y++) {
        if (terrain.grid[terrain.idx(x, y)] === 0) continue;
        
        const px = x * res;
        const py = y * res;
        
        // Determine color based on position and exposure
        let color;
        
        // Check if this is a surface cell (air above it)
        const isSurface = y > 0 && terrain.grid[terrain.idx(x, y-1)] === 0;
        
        if (isSurface && !this.terrain.caveMode) {
          // Grass on top
          color = terrain.getGrassColor(x);
        } else if (this.terrain.caveMode) {
          const isExposed = (y > 0 && terrain.grid[terrain.idx(x, y-1)] === 0) ||
                           (x > 0 && terrain.grid[terrain.idx(x-1, y)] === 0) ||
                           (x < terrain.cols-1 && terrain.grid[terrain.idx(x+1, y)] === 0);
          if (isExposed) {
            const v = 70 + Math.sin(x*0.5+y*0.3)*20 | 0;
            color = `rgb(${v+15}, ${v+10}, ${v})`;
          } else {
            const v = 40 + Math.random()*15 | 0;
            color = `rgb(${v+10}, ${v+8}, ${v+5})`;
          }
        } else {
          // Underground dirt/rock
          const depthRatio = y / terrain.rows;
          if (depthRatio < 0.3) {
            const v = 90 + Math.random()*20 | 0;
            color = `rgb(${v+15}, ${v+8}, ${v-10})`;
          } else if (depthRatio > 0.7) {
            const v = 60 + Math.random()*15 | 0;
            color = `rgb(${v+20}, ${v+15}, ${v+10})`;
          } else {
            const v = 80 + Math.random()*20 | 0;
            color = `rgb(${v+10}, ${v+5}, ${v-5})`;
          }
        }

        ctx.fillStyle = color;
        ctx.fillRect(px, py, res + 0.5, res + 0.5); // +0.5 to avoid gaps
        
        // Grass blades on surface
        if (isSurface && !this.terrain.caveMode && y > 2) {
          ctx.fillStyle = '#3a8c3f';
          for (let g = 0; g < 3; g++) {
            const gx = px + g * res/3 + Math.random()*2;
            const gh = 4 + Math.sin(x*3+g)*2;
            ctx.fillRect(gx, py - gh, 1.5, gh);
          }
        }
      }
    }

    // Render grass line on top of terrain (non-cave)
    if (!this.terrain.caveMode) {
      for (let x = startX; x < endX; x++) {
        let surfaceY = -1;
        for (let y = startY; y < endY; y++) {
          if (terrain.grid[terrain.idx(x, y)] > 0 && 
              (y === 0 || terrain.grid[terrain.idx(x, y-1)] === 0)) {
            surfaceY = y * res;
            break;
          }
        }
        
        if (surfaceY >= 0) {
          ctx.fillStyle = '#4CAF50';
          ctx.fillRect(x * res, surfaceY - 2, res + 1, 3);
          
          // Grass tufts
          if (x % 3 === 0) {
            ctx.strokeStyle = '#66BB6A';
            ctx.lineWidth = 1.5;
            for (let g = 0; g < 2; g++) {
              const gx = x * res + g * 4 + 2;
              ctx.beginPath();
              ctx.moveTo(gx, surfaceY - 2);
              ctx.lineTo(gx - 1, surfaceY - 7 - Math.random()*3);
              ctx.stroke();
            }
          }
        }
      }
    }
  }

  renderWater(ctx) {
    const terrain = this.terrain;
    const waterLevel = terrain.waterLevel;
    
    // Water body
    const bounds = this.camera.getVisibleBounds();
    const grad = ctx.createLinearGradient(0, waterLevel, 0, CONFIG.WORLD_HEIGHT);
    grad.addColorStop(0, 'rgba(30, 100, 200, 0.6)');
    grad.addColorStop(0.3, 'rgba(20, 80, 180, 0.7)');
    grad.addColorStop(1, 'rgba(10, 40, 100, 0.85)');
    
    ctx.fillStyle = grad;
    ctx.fillRect(bounds.left - 100, waterLevel, bounds.right - bounds.left + 200, CONFIG.WORLD_HEIGHT - waterLevel);

    // Water surface waves
    const t = Date.now() * 0.002;
    ctx.strokeStyle = 'rgba(150, 200, 255, 0.6)';
    ctx.lineWidth = 2;
    
    for (let wave = 0; wave < 3; wave++) {
      ctx.beginPath();
      const yOff = waterLevel + wave * 8 - 4;
      
      for (let x = bounds.left; x <= bounds.right; x += 5) {
        const y = yOff + Math.sin(x * 0.03 + t + wave) * 3;
        if (x === bounds.left) ctx.moveTo(x, y);
        else ctx.lineTo(x, y);
      }
      ctx.stroke();
    }

    // Water bubbles
    for (let i = 0; i < 15; i++) {
      const bx = ((i * 173 + t * 30) % CONFIG.WORLD_WIDTH);
      const by = waterLevel + 20 + Math.sin(i * 2.7 + t * 0.5) * (CONFIG.WORLD_HEIGHT - waterLevel - 40);
      const size = 2 + Math.sin(t + i) * 1;
      
      ctx.fillStyle = `rgba(180, 220, 255, ${0.3 + Math.sin(t+i)*0.1})`;
      ctx.beginPath();
      ctx.arc(bx, by, size, 0, Math.PI * 2);
      ctx.fill();
    }
  }

  renderTrajectory(ctx) {
    const worm = this.activeWorm;
    const w = this.selectedWeapon;
    
    if (!worm || !w || w.type === 'direct') return;

    const startX = worm.x;
    const startY = worm.y - 20;
    const speed = w.speed * (this.aimPower / 50);
    let vx = Math.cos(this.aimAngle) * speed;
    let vy = Math.sin(this.aimAngle) * speed;

    ctx.save();
    ctx.setLineDash([4, 6]);
    ctx.strokeStyle = 'rgba(255, 255, 255, 0.4)';
    ctx.lineWidth = 1.5;
    ctx.beginPath();
    ctx.moveTo(startX, startY);

    const dt = 0.03;
    let px = startX, py = startY;
    
    for (let i = 0; i < 200; i++) {
      vx += this.wind * 20 * dt;
      vy += CONFIG.GRAVITY * dt;
      px += vx * dt;
      py += vy * dt;

      if (py > CONFIG.WORLD_HEIGHT || px < 0 || px > CONFIG.WORLD_WIDTH) break;
      
      // Check terrain collision for trajectory
      if (this.terrain.isSolid(px, py)) {
        ctx.lineTo(px, py);
        break;
      }

      if (i % 3 === 0) {
        ctx.lineTo(px, py);
        
        // Draw dot at intervals
        if (i % 15 === 0) {
          ctx.fillStyle = 'rgba(255, 255, 255, 0.6)';
          ctx.fillRect(px - 1, py - 1, 3, 3);
        }
      }
    }

    ctx.stroke();
    
    // Draw impact indicator
    if (px >= 0 && px <= CONFIG.WORLD_WIDTH && py <= CONFIG.WORLD_HEIGHT) {
      const r = w.radius;
      ctx.strokeStyle = 'rgba(255, 100, 50, 0.3)';
      ctx.lineWidth = 1;
      ctx.setLineDash([]);
      ctx.beginPath();
      ctx.arc(px, py, r, 0, Math.PI * 2);
      ctx.stroke();
    }

    ctx.restore();
    
    // Power indicator
    const powerPct = this.aimPower / 100;
    ctx.fillStyle = `rgba(255, ${Math.floor(255*(1-powerPct))}, 0, 0.8)`;
    ctx.font = 'bold 14px Arial';
    ctx.textAlign = 'center';
    ctx.fillText(`Power: ${this.aimPower.toFixed(0)}%`, worm.x, worm.y - 60);
    
    // Angle indicator  
    const angleDeg = ((this.aimAngle * 180 / Math.PI) % 360 + 360) % 360;
    ctx.fillText(`Angle: ${angleDeg.toFixed(0)}°`, worm.x, worm.y - 75);

    // Scroll hint
    ctx.font = '11px Arial';
    ctx.fillStyle = '#aaa';
    ctx.fillText('Scroll to adjust power • Click to fire', worm.x, worm.y - 92);
  }

  renderTimerBar(ctx, cw, ch) {
    if (!this.activeWorm) return;
    
    const timerPct = this.turnTimer / CONFIG.TURN_TIME;
    const barWidth = Math.min(300, cw * 0.25);
    const barHeight = 8;
    const x = (cw - barWidth) / 2;
    const y = ch - 70;

    // Background
    ctx.fillStyle = 'rgba(0,0,0,0.5)';
    ctx.fillRect(x - 1, y - 1, barWidth + 2, barHeight + 2);

    // Timer fill with color based on urgency
    let fillColor;
    if (timerPct > 0.5) fillColor = '#44ff44';
    else if (timerPct > 0.25) fillColor = '#ffaa00';
    else fillColor = '#ff4444';

    // Pulsing effect when low
    if (timerPct < 0.25) {
      const pulse = Math.sin(Date.now() * 0.01) * 0.3 + 0.7;
      ctx.globalAlpha = pulse;
    }

    ctx.fillStyle = fillColor;
    ctx.fillRect(x, y, barWidth * timerPct, barHeight);
    
    ctx.globalAlpha = 1;

    // Timer text
    ctx.font = 'bold 16px Arial';
    ctx.textAlign = 'center';
    ctx.fillStyle = '#fff';
    ctx.strokeStyle = '#000';
    ctx.lineWidth = 3;
    const timeText = `${Math.ceil(this.turnTimer)}s`;
    ctx.strokeText(timeText, cw/2, y - 8);
    ctx.fillText(timeText, cw/2, y - 8);

    // Move time remaining indicator
    if (this.activeWorm) {
      const movePct = this.activeWorm.moveTime / CONFIG.TURN_TIME;
      ctx.fillStyle = 'rgba(0,0,0,0.3)';
      ctx.fillRect(x, y + barHeight + 4, barWidth, 4);
      ctx.fillStyle = '#88aaff';
      ctx.fillRect(x, y + barHeight + 4, barWidth * movePct, 4);
    }
  }

  renderWindArrow(ctx, cw, ch) {
    const x = 60;
    const y = 35;
    
    // Wind arrow on canvas (in addition to HUD)
    ctx.save();
    ctx.translate(x, y);
    
    const windStrength = Math.abs(this.wind) / 200;
    const dir = this.wind > 0 ? 1 : -1;
    
    ctx.fillStyle = 'rgba(0,0,0,0.4)';
    ctx.beginPath();
    ctx.arc(0, 0, 25, 0, Math.PI * 2);
    ctx.fill();

    // Arrow
    ctx.strokeStyle = '#fff';
    ctx.lineWidth = 3;
    ctx.lineCap = 'round';
    
    const arrowLen = 15 + windStrength * 10;
    ctx.beginPath();
    ctx.moveTo(-dir * arrowLen, 0);
    ctx.lineTo(dir * arrowLen, 0);
    ctx.stroke();
    
    // Arrowhead
    ctx.fillStyle = '#fff';
    ctx.beginPath();
    ctx.moveTo(dir * (arrowLen + 5), 0);
    ctx.lineTo(dir * (arrowLen - 3), -6);
    ctx.lineTo(dir * (arrowLen - 3), 6);
    ctx.closePath();
    ctx.fill();

    // Wind label
    ctx.font = 'bold 10px Arial';
    ctx.textAlign = 'center';
    ctx.fillStyle = '#aaa';
    ctx.fillText('WIND', 0, 20);
    
    ctx.restore();
  }

  renderMoveTime(ctx, cw, ch) {
    if (!this.activeWorm) return;
    
    const x = cw - 150;
    const y = 30;
    
    ctx.font = 'bold 12px Arial';
    ctx.textAlign = 'right';
    ctx.fillStyle = '#aaa';
    ctx.fillText('Move Time:', x - 60, y);
    
    // Move time bar
    const movePct = this.activeWorm.moveTime / (CONFIG.TURN_TIME * 0.4);
    const barW = 120;
    const barH = 8;
    
    ctx.fillStyle = 'rgba(0,0,0,0.5)';
    ctx.fillRect(x, y - 6, barW, barH);
    
    ctx.fillStyle = movePct > 0.3 ? '#44aaff' : '#ff4444';
    ctx.fillRect(x, y - 6, barW * Math.max(0, movePct), barH);
    
    ctx.font = 'bold 11px Arial';
    ctx.fillStyle = '#fff';
    ctx.fillText(`${this.activeWorm.moveTime.toFixed(1)}s`, x + barW + 40, y);

    // Items count
    const itemCount = this.activeWorm.items.length;
    if (itemCount > 0) {
      ctx.font = 'bold 12px Arial';
      ctx.textAlign = 'right';
      ctx.fillStyle = '#ffaa44';
      ctx.fillText(`Items: ${itemCount}`, x + barW, y + 20);
    }

    // Team status (alive worms)
    const team = this.teams[this.activeTeamIndex];
    if (team) {
      let aliveStr = '';
      for (const u of team.units) {
        aliveStr += u.alive ? '●' : '○';
      }
      ctx.font = '14px Arial';
      ctx.textAlign = 'right';
      ctx.fillStyle = team.color.primary;
      ctx.fillText(aliveStr, x + barW, y + 38);
    }

    // Controls hint at bottom
    if (this.isAiming) {
      ctx.font = '12px Arial';
      ctx.textAlign = 'center';
      ctx.fillStyle = '#aaa';
      ctx.fillText('Click to fire • Scroll for power • ESC to cancel', cw/2, ch - 30);
    } else if (!this.showingWeapons && !this.showingItems) {
      ctx.font = '11px Arial';
      ctx.textAlign = 'center';
      ctx.fillStyle = '#666';
      ctx.fillText('← → Move • Space Jump • 1-8 Weapons • ATTACK/ITEMS buttons', cw/2, ch - 30);
    }
  }

  gameLoop(timestamp) {
    const dt = Math.min(0.05, (timestamp - this.lastTime) / 1000);
    this.lastTime = timestamp;

    this.update(dt);
    this.render();

    requestAnimationFrame(t => this.gameLoop(t));
  }
}

// ── main ──
// Polyfill roundRect if needed
if (!CanvasRenderingContext2D.prototype.roundRect) {
  CanvasRenderingContext2D.prototype.roundRect = function(x, y, w, h, r) {
    if (typeof r === 'number') r = [r];
    const radii = r.map(v => Math.min(v, w/2, h/2));
    const rad = radii[0] || 0;
    this.moveTo(x + rad, y);
    this.arcTo(x + w, y, x + w, y + h, rad);
    this.arcTo(x + w, y + h, x, y + h, rad);
    this.arcTo(x, y + h, x, y, rad);
    this.arcTo(x, y, x + w, y, rad);
    this.closePath();
  };
}

// Initialize game
game = new ArtilleryGame();
game.init();
</script>
</body>
</html>
<!-- agent-meta {"model":"qwen3.6-27b-mtp","provider":"lmstudio","persona":"composer","sessionId":"198039e7-77ca-45ca-b91f-33a6d08d9b44","tokensIn":2343794,"tokensOut":74575,"tokensTotal":2418369,"turns":42,"toolCalls":41,"failedToolCalls":1,"timestamp":"2026-07-16T02:49:05.233Z"} -->