← Worms results

Worms

thinkingcap qwen3.6 27b · 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 63,486 bytes · SHA-256 85ed9c67d5d3
<!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; }
body { background: #1a1a2e; overflow: hidden; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; user-select: none; }
canvas { display: block; }
#hud { position: fixed; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 10; }
#topBar { display: flex; justify-content: space-between; align-items: center; padding: 8px 16px; background: linear-gradient(180deg, rgba(0,0,0,0.7) 0%, transparent 100%); pointer-events: auto; }
#teamInfo { color: #fff; font-size: 14px; display: flex; gap: 12px; align-items: center; }
.team-badge { padding: 4px 12px; border-radius: 12px; font-weight: bold; font-size: 13px; border: 2px solid rgba(255,255,255,0.3); transition: all 0.3s; }
.team-badge.active { transform: scale(1.15); box-shadow: 0 0 12px currentColor; }
#turnTimer { text-align: center; }
#timerValue { font-size: 36px; font-weight: bold; color: #fff; text-shadow: 0 0 10px rgba(255,255,255,0.5); transition: transform 0.1s; }
#windIndicator { color: #aef; font-size: 14px; background: rgba(0,0,0,0.4); padding: 6px 14px; border-radius: 8px; }
#bottomBar { position: absolute; bottom: 0; left: 0; right: 0; display: flex; justify-content: center; align-items: flex-end; gap: 20px; padding: 12px; background: linear-gradient(0deg, rgba(0,0,0,0.8) 0%, transparent 100%); pointer-events: auto; }
#weaponSelect { display: flex; gap: 6px; }
.weapon-btn { width: 56px; height: 56px; border-radius: 10px; border: 2px solid rgba(255,255,255,0.3); background: rgba(0,0,0,0.5); color: #fff; font-size: 24px; cursor: pointer; transition: all 0.15s; display: flex; align-items: center; justify-content: center; position: relative; }
.weapon-btn:hover { transform: translateY(-4px); border-color: #fff; background: rgba(255,255,255,0.15); }
.weapon-btn.selected { border-color: #ffeb3b; box-shadow: 0 0 16px rgba(255,235,59,0.5); transform: translateY(-4px); }
.weapon-btn .wname { position: absolute; bottom: -18px; font-size: 9px; white-space: nowrap; color: #ccc; }
#actionButtons { display: flex; gap: 6px; align-items: center; }
.action-btn { padding: 8px 20px; border-radius: 8px; border: none; background: linear-gradient(135deg, #e94560, #c62a71); color: #fff; font-weight: bold; cursor: pointer; font-size: 14px; transition: all 0.15s; }
.action-btn:hover { transform: scale(1.08); filter: brightness(1.2); }
#healthBars { position: absolute; top: 50px; left: 0; right: 0; pointer-events: none; }
.overlay { position: fixed; inset: 0; background: rgba(10,10,30,0.92); display: flex; flex-direction: column; align-items: center; justify-content: center; z-index: 100; pointer-events: auto; }
.overlay.hidden { display: none !important; }
.overlay h1 { font-size: 52px; color: #ffeb3b; text-shadow: 0 0 30px rgba(255,235,59,0.4); margin-bottom: 30px; }
.overlay h2 { font-size: 36px; color: #fff; margin-bottom: 20px; }
.overlay button { padding: 14px 40px; border-radius: 12px; border: none; background: linear-gradient(135deg, #e94560, #c62a71); color: #fff; font-size: 20px; font-weight: bold; cursor: pointer; transition: all 0.2s; margin-top: 10px; }
.overlay button:hover { transform: scale(1.08); filter: brightness(1.2); box-shadow: 0 0 20px rgba(233,69,96,0.5); }
label { color: #ccc; margin: 8px; font-size: 16px; display: inline-block; }
select { padding: 6px 12px; border-radius: 6px; background: #333; color: #fff; border: 1px solid #555; font-size: 14px; margin-left: 6px; }
#cpuSelect { display: flex; gap: 10px; margin: 12px 0; flex-wrap: wrap; justify-content: center; }
.cpu-toggle { padding: 8px 16px; border-radius: 8px; border: 2px solid #555; background: rgba(0,0,0,0.4); color: #fff; cursor: pointer; transition: all 0.15s; }
.cpu-toggle.active { border-color: #e94560; background: rgba(233,69,96,0.3); }
</style>
</head>
<body>
<canvas id="gameCanvas"></canvas>
<div id="hud">
  <div id="topBar">
    <div id="teamInfo"></div>
    <div id="turnTimer"><span id="timerValue">30</span></div>
    <div id="windIndicator">Wind: → 0</div>
  </div>
  <div id="bottomBar">
    <div id="weaponSelect"></div>
    <div id="actionButtons"></div>
  </div>
  <div id="healthBars"></div>
  <div id="menuOverlay" class="overlay">
    <h1>🐛 WORMS BATTLE 🐛</h1>
    <button id="btnSetup">Start Game</button>
  </div>
  <div id="setupOverlay" class="overlay hidden">
    <h2>Game Setup</h2>
    <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="twoIslands">Two Islands</option><option value="cave">Cave</option></select></label>
    <div id="cpuSelect"></div>
    <button id="btnStart">Begin Battle!</button>
  </div>
  <div id="gameOverOverlay" class="overlay hidden">
    <h2 id="winnerText"></h2>
    <button id="btnRestart">Play Again</button>
  </div>
</div>
<script>
'use strict';

// ── const: AudioEngine ──
// ===== AUDIO ENGINE =====
const AudioEngine = {
  ctx: null,
  init() {
    this.ctx = new (window.AudioContext || window.webkitAudioContext)();
  },
  resume() {
    if (this.ctx && this.ctx.state === 'suspended') this.ctx.resume();
  },
  play(type, params = {}) {
    if (!this.ctx) return;
    const t = this.ctx.currentTime;
    const g = this.ctx.createGain();
    g.connect(this.ctx.destination);
    
    switch(type) {
      case 'explosion': {
        const dur = params.dur || 0.6;
        const bufSize = this.ctx.sampleRate * dur;
        const buf = this.ctx.createBuffer(1, bufSize, this.ctx.sampleRate);
        const d = buf.getChannelData(0);
        for (let i = 0; i < bufSize; i++) d[i] = (Math.random() * 2 - 1) * Math.pow(1 - i/bufSize, 2);
        const src = this.ctx.createBufferSource();
        src.buffer = buf;
        g.gain.setValueAtTime(params.vol || 0.3, t);
        g.gain.exponentialRampToValueAtTime(0.001, t + dur);
        src.connect(g).connect(this.ctx.destination);
        src.start(t);
        break;
      }
      case 'hit': {
        const o = this.ctx.createOscillator();
        o.type = 'square';
        o.frequency.setValueAtTime(params.freq || 400, t);
        o.frequency.exponentialRampToValueAtTime(100, t + 0.15);
        g.gain.setValueAtTime(0.2, t);
        g.gain.exponentialRampToValueAtTime(0.001, t + 0.15);
        o.connect(g); o.start(t); o.stop(t + 0.15);
        break;
      }
      case 'jump': {
        const o = this.ctx.createOscillator();
        o.type = 'sine';
        o.frequency.setValueAtTime(300, t);
        o.frequency.exponentialRampToValueAtTime(600, t + 0.12);
        g.gain.setValueAtTime(0.15, t);
        g.gain.exponentialRampToValueAtTime(0.001, t + 0.12);
        o.connect(g); o.start(t); o.stop(t + 0.12);
        break;
      }
      case 'click': {
        const o = this.ctx.createOscillator();
        o.type = 'sine';
        o.frequency.setValueAtTime(800, t);
        g.gain.setValueAtTime(0.1, t);
        g.gain.exponentialRampToValueAtTime(0.001, t + 0.05);
        o.connect(g); o.start(t); o.stop(t + 0.05);
        break;
      }
      case 'splash': {
        const dur = 0.4;
        const bufSize = this.ctx.sampleRate * dur;
        const buf = this.ctx.createBuffer(1, bufSize, this.ctx.sampleRate);
        const d = buf.getChannelData(0);
        for (let i = 0; i < bufSize; i++) d[i] = (Math.random() * 2 - 1) * Math.pow(1 - i/bufSize, 3) * 0.5;
        const src = this.ctx.createBufferSource();
        src.buffer = buf;
        g.gain.setValueAtTime(0.2, t);
        g.gain.exponentialRampToValueAtTime(0.001, t + dur);
        src.connect(g).connect(this.ctx.destination);
        src.start(t);
        break;
      }
      case 'drown': {
        const o = this.ctx.createOscillator();
        o.type = 'sawtooth';
        o.frequency.setValueAtTime(200, t);
        o.frequency.exponentialRampToValueAtTime(50, t + 0.8);
        g.gain.setValueAtTime(0.15, t);
        g.gain.exponentialRampToValueAtTime(0.001, t + 0.8);
        o.connect(g); o.start(t); o.stop(t + 0.8);
        break;
      }
      case 'tick': {
        const o = this.ctx.createOscillator();
        o.type = 'sine';
        o.frequency.setValueAtTime(1200, t);
        g.gain.setValueAtTime(0.06, t);
        g.gain.exponentialRampToValueAtTime(0.001, t + 0.03);
        o.connect(g); o.start(t); o.stop(t + 0.03);
        break;
      }
      case 'turnEnd': {
        [523, 659, 784].forEach((f, i) => {
          const o = this.ctx.createOscillator();
          const gg = this.ctx.createGain();
          o.type = 'sine';
          o.frequency.value = f;
          gg.gain.setValueAtTime(0.15, t + i * 0.12);
          gg.gain.exponentialRampToValueAtTime(0.001, t + i * 0.12 + 0.3);
          o.connect(gg).connect(this.ctx.destination);
          o.start(t + i * 0.12); o.stop(t + i * 0.12 + 0.3);
        });
        break;
      }
      case 'kill': {
        const dur = 0.5;
        const bufSize = this.ctx.sampleRate * dur;
        const buf = this.ctx.createBuffer(1, bufSize, this.ctx.sampleRate);
        const d = buf.getChannelData(0);
        for (let i = 0; i < bufSize; i++) {
          const env = Math.pow(1 - i/bufSize, 1.5);
          d[i] = Math.sin(i * 0.02) * env + (Math.random() * 2 - 1) * env * 0.3;
        }
        const src = this.ctx.createBufferSource();
        src.buffer = buf;
        g.gain.setValueAtTime(0.35, t);
        g.gain.exponentialRampToValueAtTime(0.001, t + dur);
        src.connect(g).connect(this.ctx.destination);
        src.start(t);
        break;
      }
    }
  }
};

// ── const: Particles ──
// ===== PARTICLE SYSTEM =====
const Particles = {
  pool: [],
  
  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||1) : Math.random()*Math.PI*2;
      const speed = (config.speedMin||1) + Math.random()*((config.speedMax||4)-(config.speedMin||1));
      this.pool.push({
        x, y,
        vx: Math.cos(angle)*speed*(config.vxMul||1),
        vy: Math.sin(angle)*speed*(config.vyMul||1),
        life: 1, decay: config.decay || (0.02 + Math.random()*0.03),
        size: (config.sizeMin||2) + Math.random()*((config.sizeMax||5)-(config.sizeMin||2)),
        color: config.colors ? config.colors[Math.floor(Math.random()*config.colors.length)] : '#fff',
        gravity: config.gravity ?? 0.1,
        friction: config.friction ?? 0.98,
        type: config.type || 'circle'
      });
    }
  },
  
  update(dt) {
    for (let i = this.pool.length - 1; i >= 0; i--) {
      const p = this.pool[i];
      p.vy += p.gravity * dt * 60;
      p.vx *= Math.pow(p.friction, dt*60);
      p.x += p.vx * dt * 60;
      p.y += p.vy * dt * 60;
      p.life -= p.decay * dt * 60;
      if (p.life <= 0) this.pool.splice(i, 1);
    }
  },
  
  render(ctx, camX, camY, scale) {
    for (const p of this.pool) {
      const sx = (p.x - camX) * scale;
      const sy = (p.y - camY) * scale;
      ctx.globalAlpha = Math.max(0, p.life);
      ctx.fillStyle = p.color;
      if (p.type === 'circle') {
        ctx.beginPath();
        ctx.arc(sx, sy, p.size * scale * 0.5, 0, Math.PI*2);
        ctx.fill();
      } else {
        ctx.fillRect(sx - p.size*scale*0.3, sy - p.size*scale*0.3, p.size*scale*0.6, p.size*scale*0.6);
      }
    }
    ctx.globalAlpha = 1;
  },
  
  explosion(x, y, size) {
    const colors = ['#ff4500','#ff8c00','#ffd700','#fff','#ff6347'];
    this.emit(x, y, Math.floor(size*2), { speedMin:1, speedMax:size*0.3, decay:0.025, sizeMin:2, sizeMax:size*0.4, colors, gravity: 0.08 });
    // Smoke
    this.emit(x, y, Math.floor(size*0.5), { speedMin:0.5, speedMax:size*0.15, decay:0.012, sizeMin:4, sizeMax:size*0.6, colors:['#555','#777','#999'], gravity:-0.03 });
    // Sparks
    this.emit(x, y, Math.floor(size), { speedMin:3, speedMax:size*0.5, decay:0.04, sizeMin:1, sizeMax:2, colors:['#fff','#ffeb3b','#ff9800'], gravity: 0.15 });
  },
  
  dust(x, y) {
    this.emit(x, y, 6, { speedMin:0.5, speedMax:2, decay:0.04, sizeMin:2, sizeMax:4, colors:['#c8a96e','#b8956a'], gravity:-0.02 });
  },
  
  splash(x, y) {
    this.emit(x, y, 15, { speedMin:1, speedMax:4, decay:0.03, sizeMin:2, sizeMax:5, colors:['#4fc3f7','#81d4fa','#b3e5fc'], gravity:0.1 });
  },
  
  smoke(x, y) {
    this.emit(x, y, 8, { speedMin:0.3, speedMax:1.5, decay:0.015, sizeMin:5, sizeMax:12, colors:['#666','#888','#aaa'], gravity:-0.04 });
  }
};

// ── const: CombatText ──
// ===== FLOATING COMBAT TEXT =====
const CombatText = {
  texts: [],
  
  add(x, y, text, color='#fff', size=18) {
    this.texts.push({ x, y, text, color, size, life:1, vy:-2 });
  },
  
  update(dt) {
    for (let i = this.texts.length-1; i >= 0; i--) {
      const t = this.texts[i];
      t.y += t.vy * dt * 60;
      t.life -= dt * 1.2;
      if (t.life <= 0) this.texts.splice(i, 1);
    }
  },
  
  render(ctx, camX, camY, scale) {
    ctx.textAlign = 'center';
    for (const t of this.texts) {
      const sx = (t.x - camX) * scale;
      const sy = (t.y - camY) * scale;
      ctx.globalAlpha = Math.max(0, t.life);
      ctx.font = `bold ${Math.floor(t.size*scale)}px sans-serif`;
      ctx.strokeStyle = '#000';
      ctx.lineWidth = 3;
      ctx.strokeText(t.text, sx, sy);
      ctx.fillStyle = t.color;
      ctx.fillText(t.text, sx, sy);
    }
    ctx.globalAlpha = 1;
  }
};

// ── const: WEAPONS ──
// ===== WEAPON DEFINITIONS =====
const WEAPONS = [
  { id:'gun', name:'Gun', icon:'🔫', category:'direct', desc:'Instant hit, low damage', power:15, radius:0, color:'#ffeb3b' },
  { id:'rocket', name:'Rocket', icon:'🚀', category:'indirect', desc:'Parabolic projectile', power:40, radius:60, color:'#ff5722' },
  { id:'homingMissile', name:'Homing', icon:'🎯', category:'indirect', desc:'Tracks nearest enemy', power:35, radius:50, color:'#e040fb' },
  { id:'bouncingBetty', name:'B.Betty', icon:'💣', category:'indirect', desc:'Bounces then explodes', power:50, radius:70, color:'#ff9800' },
  { id:'nuke', name:'Nuke', icon:'☢️', category:'aoe', desc:'Massive AoE destruction', power:120, radius:160, color:'#f44336' },
  { id:'mines', name:'Mines', icon:'💥', category:'utility', desc:'Place explosive mines', power:45, radius:55, color:'#795548' },
  { id:'teleport', name:'Teleport', icon:'🌀', category:'utility', desc:'Warp to location', power:0, radius:0, color:'#2196f3' },
  { id:'parachute', name:'Parachute', icon:'🪂', category:'utility', desc:'Drop from sky', power:0, radius:0, color:'#4caf50' }
];

// ── class: Projectile ──
// ===== PROJECTILE SYSTEM =====
class Projectile {
  constructor(x, y, vx, vy, weapon, ownerTeamId) {
    this.x = x; this.y = y;
    this.vx = vx; this.vy = vy;
    this.weapon = weapon;
    this.ownerTeamId = ownerTeamId;
    this.alive = true;
    this.bounces = 0;
    this.maxBounces = weapon.id === 'bouncingBetty' ? 3 : 0;
    this.homingTarget = null;
    this.trailTimer = 0;
    this.age = 0;
  }
  
  update(dt, wind) {
    if (!this.alive) return;
    this.age += dt;
    
    // Homing missile logic
    if (this.weapon.id === 'homingMissile' && this.homingTarget && this.homingTarget.alive) {
      const dx = this.homingTarget.x - this.x;
      const dy = this.homingTarget.y - this.y;
      const angle = Math.atan2(dy, dx);
      const turnRate = 3 * dt;
      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 newAngle = currentAngle + Math.sign(diff) * Math.min(Math.abs(diff), turnRate);
      const speed = Math.sqrt(this.vx*this.vx + this.vy*this.vy);
      this.vx = Math.cos(newAngle) * speed;
      this.vy = Math.sin(newAngle) * speed;
    }
    
    // Wind effect
    this.vx += wind * 8 * dt;
    
    // Gravity (less for direct weapons)
    const gravMult = this.weapon.category === 'direct' ? 0.1 : 500;
    this.vy += gravMult * dt;
    
    this.x += this.vx * dt * 60;
    this.y += this.vy * dt * 60;
    
    // Trail particles
    this.trailTimer += dt;
    if (this.trailTimer > 0.03) {
      this.trailTimer = 0;
      Particles.emit(this.x, this.y, 1, { speedMin:0.2, speedMax:0.8, decay:0.06, sizeMin:1, sizeMax:3, colors:['#ffeb3b','#ff9800'], gravity:0 });
    }
    
    // Bouncing Betty bounce
    if (this.weapon.id === 'bouncingBetty' && Terrain.isSolid(this.x, this.y) && this.bounces < this.maxBounces) {
      this.vy = -Math.abs(this.vy) * 0.6;
      this.vx *= 0.7;
      this.bounces++;
      Particles.dust(this.x, this.y);
      AudioEngine.play('hit', {freq: 300});
    }
    
    // Terrain collision or out of bounds
    if (Terrain.isSolid(this.x, this.y) || this.x < -50 || this.x > Terrain.width+50 || this.y > Terrain.height + 50 || this.age > 12) {
      this.explode();
    }
    
    // Hit unit check
    for (const u of Game.units) {
      if (!u.alive || u.teamId === this.ownerTeamId) continue;
      const dx = u.x - this.x, dy = u.y - this.y;
      if (Math.sqrt(dx*dx+dy*dy) < 20) {
        this.explode();
        break;
      }
    }
  }
  
  explode() {
    this.alive = false;
    
    if (this.weapon.power > 0) {
      const dmg = Math.floor(this.weapon.power * (0.8 + Math.random()*0.4));
      
      // Damage units in radius
      for (const u of Game.units) {
        if (!u.alive || u.teamId === this.ownerTeamId) continue;
        const dx = u.x - this.x, dy = u.y - this.y;
        const dist = Math.sqrt(dx*dx+dy*dy);
        if (dist < this.weapon.radius + 20) {
          const falloff = Math.max(0.3, 1 - dist/(this.weapon.radius+20));
          u.takeDamage(Math.floor(dmg * falloff), this.x, this.y);
        }
      }
      
      // Destroy terrain
      Terrain.destroy(this.x, this.y, this.weapon.radius);
      
      // Effects
      Particles.explosion(this.x, this.y, Math.min(this.weapon.radius*0.3, 25));
      AudioEngine.play('explosion', {dur: Math.min(1, this.weapon.radius/100), vol: Math.min(0.4, this.weapon.radius/200)});
      
      // Screen shake for big explosions
      if (this.weapon.radius > 80) {
        Game.screenShake = Math.min(this.weapon.radius / 15, 12);
        Game.freezeTimer = Math.min(this.weapon.radius / 300, 0.15);
      } else {
        Game.screenShake = Math.max(2, this.weapon.radius / 20);
      }
      
      // Water splash if near water level
      if (this.y > Terrain.waterLevel - 40) {
        Particles.splash(this.x, Terrain.waterLevel);
        AudioEngine.play('splash');
      }
    } else if (this.weapon.id === 'mines') {
      Game.mines.push({ x: this.x, y: this.y, teamId: this.ownerTeamId, weapon: this.weapon });
      Particles.smoke(this.x, this.y);
    }
  }
  
  render(ctx, camX, camY, scale) {
    if (!this.alive) return;
    
    const sx = (this.x - camX) * scale;
    const sy = (this.y - camY) * scale;
    
    ctx.save();
    ctx.translate(sx, sy);
    
    switch(this.weapon.id) {
      case 'gun':
        ctx.fillStyle = '#ffeb3b';
        ctx.beginPath();
        ctx.arc(0, 0, 4*scale, 0, Math.PI*2);
        ctx.fill();
        break;
      case 'rocket':
        ctx.fillStyle = '#ff5722';
        ctx.rotate(Math.atan2(this.vy, this.vx));
        ctx.beginPath();
        ctx.moveTo(8*scale, 0);
        ctx.lineTo(-4*scale, -4*scale);
        ctx.lineTo(-4*scale, 4*scale);
        ctx.closePath();
        ctx.fill();
        break;
      case 'homingMissile':
        ctx.fillStyle = '#e040fb';
        ctx.rotate(Math.atan2(this.vy, this.vx));
        ctx.beginPath();
        ctx.moveTo(10*scale, 0);
        ctx.lineTo(-5*scale, -3*scale);
        ctx.lineTo(-5*scale, 3*scale);
        ctx.closePath();
        ctx.fill();
        // Glow
        ctx.shadowColor = '#e040fb';
        ctx.shadowBlur = 10;
        ctx.beginPath();
        ctx.arc(0, 0, 3*scale, 0, Math.PI*2);
        ctx.fill();
        break;
      case 'bouncingBetty':
        ctx.fillStyle = '#ff9800';
        ctx.beginPath();
        ctx.arc(0, 0, 5*scale, 0, Math.PI*2);
        ctx.fill();
        // Fuse spark
        Particles.emit(this.x + (Math.random()-0.5)*4, this.y - 5, 1, { speedMin:0.3, speedMax:1, decay:0.1, sizeMin:1, sizeMax:2, colors:['#fff','#ffeb3b'], gravity:-0.05 });
        break;
      case 'nuke':
        ctx.fillStyle = '#f44336';
        ctx.shadowColor = '#f44336';
        ctx.shadowBlur = 15;
        ctx.beginPath();
        ctx.arc(0, 0, 8*scale, 0, Math.PI*2);
        ctx.fill();
        break;
    }
    
    ctx.restore();
  }
}

// ── const: AI ──
// ===== AI SYSTEM =====
const AI = {
  calculateShot(teamId, weapon) {
    // Find nearest enemy
    let bestTarget = null;
    let bestDist = Infinity;
    for (const u of Game.units) {
      if (!u.alive || u.teamId === teamId) continue;
      const dx = u.x - Game.activeUnit.x;
      const dy = u.y - Game.activeUnit.y;
      const dist = Math.sqrt(dx*dx + dy*dy);
      if (dist < bestDist) { bestDist = dist; bestTarget = u; }
    }
    
    if (!bestTarget) return null;
    
    // Risk assessment: check for hazards near active unit
    const au = Game.activeUnit;
    const hazardRisk = this.assessHazard(au);
    if (hazardRisk > 0.7 && weapon.category !== 'utility') {
      // Consider moving away first - but we'll just shoot anyway with adjusted aim
    }
    
    const dx = bestTarget.x - au.x;
    const dy = bestTarget.y - au.y;
    const dist = Math.sqrt(dx*dx + dy*dy);
    
    if (weapon.category === 'direct') {
      return { angle: Math.atan2(dy, dx), power: 10 };
    } else if (weapon.category === 'indirect' || weapon.category === 'aoe') {
      // Calculate parabolic trajectory
      const wind = Game.wind;
      const g = 500;
      
      // Try multiple angles and pick best
      let bestAngle = Math.PI * 0.25 + Math.random() * 0.3;
      let bestPower = 8 + dist / 150;
      
      if (dx > 0) {
        bestAngle = Math.atan2(-dy, dist * 0.7) - Math.PI * 0.1;
      } else {
        bestAngle = Math.PI - Math.atan2(-dy, dist * 0.7) + Math.PI * 0.1;
      }
      
      // Add some randomness for realism
      bestAngle += (Math.random() - 0.5) * 0.15;
      bestPower = Math.max(4, Math.min(20, dist / 80));
      
      return { angle: bestAngle, power: bestPower };
    } else if (weapon.category === 'utility') {
      if (weapon.id === 'mines') {
        // Place mine near enemy path or cliff edge
        const mx = au.x + au.facing * 40;
        return { angle: Math.atan2(1, 0), power: 5, targetX: mx };
      } else if (weapon.id === 'teleport') {
        // Teleport to safe location near enemy
        const tx = bestTarget.x + (Math.random()-0.5)*100;
        return { angle: Math.atan2(1, 0), power: 5, targetX: tx };
      } else if (weapon.id === 'parachute') {
        // Drop near enemy
        const px = bestTarget.x + (Math.random()-0.5)*80;
        return { angle: Math.atan2(1, 0), power: 5, targetX: px };
      }
    }
    
    return null;
  },
  
  assessHazard(unit) {
    let risk = 0;
    // Check water proximity
    if (Terrain.isWater(unit.x, unit.y + unit.height/2)) risk += 0.5;
    else if (unit.y + unit.height/2 > Terrain.waterLevel - 30) risk += 0.2;
    
    // Check cliff edge
    const leftGround = Terrain.getGroundY(unit.x - 15);
    const rightGround = Terrain.getGroundY(unit.x + 15);
    if (Math.abs(leftGround - unit.groundY) > 40 || Math.abs(rightGround - unit.groundY) > 40) risk += 0.3;
    
    return Math.min(1, risk);
  },
  
  executeTurn(unit, weapon) {
    const shot = this.calculateShot(unit.teamId, weapon);
    if (!shot) return Promise.resolve();
    
    return new Promise(resolve => {
      Game.fireProjectile(weapon, shot.angle, shot.power, unit);
      
      // For utility weapons with target position
      if (shot.targetX && (weapon.id === 'teleport' || weapon.id === 'parachute')) {
        setTimeout(() => {
          if (weapon.id === 'teleport') {
            Particles.explosion(unit.x, unit.y, 8);
            unit.x = shot.targetX;
            unit.y = Terrain.getGroundY(shot.targetX) - unit.height/2 - 5;
            Particles.explosion(unit.x, unit.y, 8);
          } else if (weapon.id === 'parachute') {
            unit.hasParachute = true;
            unit.parachuteOpen = true;
            unit.x = shot.targetX;
            unit.y = 50;
            unit.vy = 0;
          }
          resolve();
        }, 300);
      } else {
        setTimeout(resolve, 1000);
      }
    });
  }
};

// ── const: Sky ──
// ===== SKY RENDERER =====
const Sky = {
  gradient: null,
  
  render(ctx, camX, camY, scale) {
    if (!this.gradient) {
      this.gradient = ctx.createLinearGradient(0, 0, 0, ctx.canvas.height);
      this.gradient.addColorStop(0, '#1a237e');
      this.gradient.addColorStop(0.4, '#283593');
      this.gradient.addColorStop(0.7, '#5c6bc0');
      this.gradient.addColorStop(1, '#9fa8da');
    }
    
    // Draw sky background (in screen space)
    ctx.save();
    ctx.setTransform(1, 0, 0, 1, 0, 0);
    ctx.fillStyle = this.gradient;
    ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
    
    // Stars
    const starSeed = 42;
    for (let i = 0; i < 50; i++) {
      const sx = ((i * 7919 + starSeed) % ctx.canvas.width);
      const sy = ((i * 6271 + starSeed) % (ctx.canvas.height * 0.5));
      const brightness = 0.3 + Math.sin(Date.now()*0.003 + i)*0.3;
      ctx.fillStyle = `rgba(255,255,255,${brightness})`;
      ctx.fillRect(sx, sy, 1.5, 1.5);
    }
    
    // Clouds (parallax)
    const time = Date.now() * 0.0001;
    for (let i = 0; i < 6; i++) {
      const cx = ((i * 437 + 100 + time * (20+i*5)) % (ctx.canvas.width + 200)) - 100;
      const cy = 40 + i * 35;
      ctx.fillStyle = `rgba(255,255,255,${0.08 + i*0.01})`;
      ctx.beginPath();
      ctx.arc(cx, cy, 30+i*5, 0, Math.PI*2);
      ctx.fill();
      ctx.beginPath();
      ctx.arc(cx+25, cy-10, 20+i*3, 0, Math.PI*2);
      ctx.fill();
    }
    
    ctx.restore();
  }
};

// ── const: Trajectory ──
// ===== TRAJECTORY PREDICTOR =====
const Trajectory = {
  draw(ctx, startX, startY, angle, power, weapon) {
    const g = 500;
    const wind = Game.wind;
    let vx = Math.cos(angle) * power * 8;
    let vy = -Math.sin(angle) * power * 8;
    let px = startX, py = startY;
    
    ctx.save();
    ctx.setLineDash([4, 6]);
    ctx.strokeStyle = 'rgba(255,255,255,0.3)';
    ctx.lineWidth = 1.5;
    ctx.beginPath();
    ctx.moveTo(px, py);
    
    for (let i = 0; i < 80; i++) {
      vx += wind * 8 * 0.016;
      vy += g * 0.016;
      px += vx * 0.016 * 60;
      py += vy * 0.016 * 60;
      
      if (Terrain.isSolid(px, py) || py > Terrain.height) break;
      
      ctx.lineTo(px, py);
    }
    
    ctx.stroke();
    ctx.setLineDash([]);
    ctx.restore();
  }
};

// ── const: Camera ──
// ===== CAMERA SYSTEM =====
const Camera = {
  x: 0, y: 0,
  targetX: 0, targetY: 0,
  zoom: 1,
  targetZoom: 1,
  shakeX: 0, shakeY: 0,
  
  follow(unit) {
    if (unit && unit.alive) {
      this.targetX = unit.x;
      this.targetY = unit.y - 50;
    }
  },
  
  update(dt) {
    const smooth = Math.min(1, dt * 4);
    this.x += (this.targetX - this.x) * smooth;
    this.y += (this.targetY - this.y) * smooth;
    
    this.zoom += (this.targetZoom - this.zoom) * dt * 3;
    
    // Clamp to map bounds using Terrain dimensions
    const halfW = Terrain.width / 2;
    const halfH = Terrain.height / 2;
    this.x = Math.max(halfW, Math.min(Terrain.width - halfW, this.x));
    this.y = Math.max(-100, Math.min(halfH, this.y));
  },
  
  apply(ctx) {
    ctx.save();
    
    if (Game.screenShake > 0.5) {
      this.shakeX = (Math.random()-0.5) * Game.screenShake;
      this.shakeY = (Math.random()-0.5) * Game.screenShake;
      Game.screenShake *= 0.85;
    } else {
      this.shakeX = 0; this.shakeY = 0; Game.screenShake = 0;
    }
    
    ctx.translate(ctx.canvas.width/2 + this.shakeX, ctx.canvas.height/2 + this.shakeY);
    ctx.scale(this.zoom, this.zoom);
    ctx.translate(-this.x, -this.y);
  },
  
  restore(ctx) {
    ctx.restore();
  }
};

// ── class: Unit ──
// ===== UNIT / WORM CLASS =====
class Unit {
  constructor(teamId, index, x, y) {
    this.teamId = teamId;
    this.index = index;
    this.x = x;
    this.y = y;
    this.vx = 0;
    this.vy = 0;
    this.hp = 100;
    this.maxHp = 100;
    this.alive = true;
    this.onGround = false;
    this.facing = 1;
    this.width = 20;
    this.height = 30;
    this.moveTimeLeft = 8;
    this.isMoving = false;
    this.jumpCount = 0;
    this.maxJumps = 2;
    this.inWater = false;
    this.drowningTimer = 0;
    this.hasParachute = false;
    this.parachuteOpen = false;
    this.animPhase = Math.random() * Math.PI * 2;
    this.bobOffset = 0;
    this.name = 'Worm ' + String.fromCharCode(65+index);
    this.spawnInvuln = 1.5;
  }
  
  get groundY() {
    return Terrain.getGroundY(this.x);
  }
  
  update(dt, wind) {
    if (!this.alive) return;
    
    this.animPhase += dt * 3;
    this.bobOffset = Math.sin(this.animPhase) * 2;
    
    if (this.spawnInvuln > 0) {
      this.spawnInvuln -= dt;
    }
    
    const gravMult = this.parachuteOpen ? 0.15 : 1;
    this.vy += (this.inWater ? 80 : 600) * dt * gravMult;
    
    if (this.inWater && !this.hasParachute) {
      this.drowningTimer += dt;
      if (this.drowningTimer > 5 && this.spawnInvuln <= 0) {
        this.hp -= 20 * dt;
        if (this.hp <= 0) this.die();
      }
    } else {
      this.drowningTimer = Math.max(0, this.drowningTimer - dt * 2);
    }
    
    if (!this.onGround && !this.parachuteOpen) {
      this.vx += wind * 15 * dt;
    }
    
    if (this.onGround) {
      this.vx *= Math.pow(0.85, dt*60);
    } else {
      this.vx *= Math.pow(0.99, dt*60);
    }
    
    const prevY = this.y;
    this.x += this.vx * dt * 60;
    this.y += this.vy * dt * 60;
    
    this.x = Math.max(20, Math.min(Terrain.width - 20, this.x));
    
    const feetY = this.y + this.height/2;
    if (Terrain.isSolid(this.x, feetY)) {
      this.y = Terrain.getGroundY(this.x) - this.height/2 - 1;
      
      // Fall damage only after spawn invulnerability
      if (this.spawnInvuln <= 0 && feetY - prevY > 80 && !this.inWater) {
        const dmg = Math.floor((feetY - prevY - 80) * 0.3);
        if (dmg > 0) {
          this.hp -= dmg;
          CombatText.add(this.x, this.y - 20, '-' + dmg, '#ff9800');
          AudioEngine.play('hit', {freq: 200});
        }
      }
      
      this.vy = 0;
      this.onGround = true;
      this.jumpCount = 0;
    } else {
      this.onGround = false;
    }
    
    if (Terrain.isSolid(this.x, this.y - this.height/2)) {
      this.vy = Math.max(0, this.vy);
    }
    
    this.inWater = Terrain.isWater(this.x, this.y + this.height/2);
    
    if (this.hp <= 0) this.die();
  }
  
  jump() {
    if (this.jumpCount < this.maxJumps && this.alive) {
      this.vy = -18;
      this.jumpCount++;
      AudioEngine.play('jump');
      Particles.dust(this.x, this.y + this.height/2);
    }
  }
  
  move(dir, dt) {
    if (!this.alive || !this.isMoving || this.moveTimeLeft <= 0) return;
    const speed = this.inWater ? 40 : 120;
    this.vx += dir * speed * dt;
    this.facing = dir > 0 ? 1 : -1;
    this.moveTimeLeft -= dt;
    
    if (this.onGround && Math.abs(this.vx) > 10) {
      Particles.dust(this.x, this.y + this.height/2);
    }
  }
  
  takeDamage(dmg, fromX, fromY) {
    if (this.spawnInvuln > 0) return;
    
    this.hp -= dmg;
    CombatText.add(this.x, this.y - 30, '-' + dmg, '#ff4444', 22);
    AudioEngine.play('hit');
    
    const dx = this.x - fromX;
    const dy = this.y - fromY;
    const dist = Math.max(1, Math.sqrt(dx*dx + dy*dy));
    this.vx += (dx/dist) * 5;
    this.vy -= 3;
    
    if (this.hp <= 0) this.die();
  }
  
  die() {
    this.alive = false;
    Particles.explosion(this.x, this.y, 12);
    AudioEngine.play('kill');
    CombatText.add(this.x, this.y - 30, '💀', '#ff0000', 28);
  }
  
  render(ctx, camX, camY, scale) {
    if (!this.alive) return;
    
    const sx = (this.x - camX) * scale;
    const sy = (this.y - camY + this.bobOffset) * scale;
    const s = scale;
    
    ctx.save();
    ctx.translate(sx, sy);
    
    const teamColors = ['#e94560','#2196f3','#4caf50','#ff9800'];
    const bodyColor = teamColors[this.teamId] || '#fff';
    
    // Shadow
    ctx.fillStyle = 'rgba(0,0,0,0.2)';
    ctx.beginPath();
    ctx.ellipse(0, this.height/2*s*0.8, 14*s, 5*s, 0, 0, Math.PI*2);
    ctx.fill();
    
    // Body segments
    for (let i = 3; i >= -3; i--) {
      const segY = i * 4 * s;
      const segW = (12 - Math.abs(i)*1.5) * s;
      ctx.fillStyle = bodyColor;
      ctx.beginPath();
      ctx.ellipse(0, segY, segW*0.5, 3*s, 0, 0, Math.PI*2);
      ctx.fill();
    }
    
    // Head
    ctx.fillStyle = bodyColor;
    ctx.beginPath();
    ctx.arc(0, -10*s, 8*s, 0, Math.PI*2);
    ctx.fill();
    
    // Eyes
    const eyeOffX = this.facing * 3 * s;
    ctx.fillStyle = '#fff';
    ctx.beginPath();
    ctx.ellipse(-4*s + eyeOffX*0.3, -12*s, 3.5*s, 4*s, 0, 0, Math.PI*2);
    ctx.fill();
    ctx.beginPath();
    ctx.ellipse(4*s + eyeOffX*0.3, -12*s, 3.5*s, 4*s, 0, 0, Math.PI*2);
    ctx.fill();
    
    // Pupils
    ctx.fillStyle = '#000';
    ctx.beginPath();
    ctx.arc(-4*s + eyeOffX, -12*s, 1.8*s, 0, Math.PI*2);
    ctx.fill();
    ctx.beginPath();
    ctx.arc(4*s + eyeOffX, -12*s, 1.8*s, 0, Math.PI*2);
    ctx.fill();
    
    // Mouth
    if (!this.alive) {
      ctx.strokeStyle = '#fff';
      ctx.lineWidth = 1.5 * s;
      ctx.beginPath();
      ctx.moveTo(-3*s, -6*s);
      ctx.lineTo(0, -8*s);
      ctx.lineTo(3*s, -6*s);
      ctx.stroke();
    } else {
      ctx.fillStyle = '#fff';
      ctx.beginPath();
      ctx.arc(this.facing * 2*s, -7*s, 2*s, 0, Math.PI*2);
      ctx.fill();
    }
    
    // Parachute
    if (this.parachuteOpen) {
      ctx.fillStyle = '#ff5722';
      ctx.beginPath();
      ctx.arc(0, -35*s, 18*s, Math.PI, 0);
      ctx.fill();
      ctx.strokeStyle = '#fff';
      ctx.lineWidth = 1;
      ctx.beginPath();
      ctx.moveTo(-15*s, -35*s);
      ctx.lineTo(0, -10*s);
      ctx.moveTo(15*s, -35*s);
      ctx.lineTo(0, -10*s);
      ctx.stroke();
    }
    
    // Health bar above unit
    if (this.hp < this.maxHp) {
      const barW = 24 * s;
      const barH = 4 * s;
      const hpRatio = Math.max(0, this.hp / this.maxHp);
      ctx.fillStyle = 'rgba(0,0,0,0.5)';
      ctx.fillRect(-barW/2, -22*s, barW, barH);
      ctx.fillStyle = hpRatio > 0.5 ? '#4caf50' : hpRatio > 0.25 ? '#ff9800' : '#f44336';
      ctx.fillRect(-barW/2, -22*s, barW * hpRatio, barH);
    }
    
    // Spawn invulnerability indicator
    if (this.spawnInvuln > 0) {
      ctx.strokeStyle = 'rgba(255,255,255,' + this.spawnInvuln + ')';
      ctx.lineWidth = 2;
      ctx.beginPath();
      ctx.arc(0, -10*s, 16*s, 0, Math.PI*2);
      ctx.stroke();
    }
    
    // Drowning indicator
    if (this.drowningTimer > 0) {
      ctx.fillStyle = 'rgba(100,200,255,0.3)';
      ctx.beginPath();
      ctx.arc(0, -10*s, 16*s, 0, Math.PI*2);
      ctx.fill();
    }
    
    ctx.restore();
  }
}

// ── const: Game ──
// ===== GAME STATE & MAIN LOOP =====
const Game = {
  canvas: null, ctx: null,
  units: [],
  projectiles: [],
  mines: [],
  teams: [],
  teamColors: ['#e94560','#2196f3','#4caf50','#ff9800'],
  teamNames: ['Red','Blue','Green','Orange'],
  
  currentTeamIndex: 0,
  activeUnitIndex: 0,
  selectedWeapon: 0,
  turnTimeLeft: 30,
  maxTurnTime: 30,
  
  wind: 0,
  windTimer: 0,
  
  phase: 'menu',
  
  screenShake: 0,
  freezeTimer: 0,
  
  aimAngle: -Math.PI/4,
  aimPower: 10,
  isAiming: false,
  mouseDown: false,
  mouseX: 0, mouseY: 0,
  
  moveDir: 0,
  
  cpuTeams: new Set(),
  
  init() {
    this.canvas = document.getElementById('gameCanvas');
    this.ctx = this.canvas.getContext('2d');
    this.resize();
    
    window.addEventListener('resize', () => this.resize());
    
    document.getElementById('btnSetup').addEventListener('click', () => {
      AudioEngine.play('click');
      document.getElementById('menuOverlay').classList.add('hidden');
      document.getElementById('setupOverlay').classList.remove('hidden');
      this.buildCpuSelect();
    });
    
    document.getElementById('btnStart').addEventListener('click', () => {
      AudioEngine.play('click');
      this.startGame();
    });
    
    document.getElementById('btnRestart').addEventListener('click', () => {
      AudioEngine.play('click');
      document.getElementById('gameOverOverlay').classList.add('hidden');
      document.getElementById('menuOverlay').classList.remove('hidden');
      this.phase = 'menu';
    });
  },
  
  resize() {
    this.canvas.width = window.innerWidth;
    this.canvas.height = window.innerHeight;
  },
  
  buildCpuSelect() {
    const container = document.getElementById('cpuSelect');
    container.innerHTML = '';
    for (let i = 0; i < parseInt(document.getElementById('teamCount').value); i++) {
      const btn = document.createElement('div');
      btn.className = 'cpu-toggle';
      btn.textContent = this.teamNames[i] + ' Team';
      btn.addEventListener('click', () => {
        AudioEngine.play('click');
        if (this.cpuTeams.has(i)) {
          this.cpuTeams.delete(i);
          btn.classList.remove('active');
        } else {
          this.cpuTeams.add(i);
          btn.classList.add('active');
        }
      });
      container.appendChild(btn);
    }
  },
  
  startGame() {
    AudioEngine.resume();
    
    document.getElementById('setupOverlay').classList.add('hidden');
    
    const teamCount = parseInt(document.getElementById('teamCount').value);
    const terrainType = document.getElementById('terrainType').value;
    
    Terrain.generate(terrainType);
    
    this.units = [];
    this.teams = [];
    for (let t = 0; t < teamCount; t++) {
      this.teams.push({ id: t, name: this.teamNames[t], color: this.teamColors[t], alive: 4 });
      
      const spacing = Terrain.width / (teamCount * 4 + 1);
      for (let u = 0; u < 4; u++) {
        const x = spacing * (t * 4 + u + 1);
        // Place unit well above terrain, then settle it
        const groundY = Terrain.getGroundY(x);
        this.units.push(new Unit(t, u, x, groundY - 20));
      }
    }
    
    // Settle units onto terrain (simulate a few frames)
    for (let frame = 0; frame < 30; frame++) {
      for (const unit of this.units) {
        if (!unit.alive) continue;
        const dt = 1/60;
        // Apply gravity
        unit.vy += 600 * dt;
        unit.y += unit.vy * dt * 60;
        
        // Ground collision
        const feetY = unit.y + unit.height/2;
        if (Terrain.isSolid(unit.x, feetY)) {
          unit.y = Terrain.getGroundY(unit.x) - unit.height/2 - 1;
          unit.vy = 0;
          unit.onGround = true;
        } else {
          unit.onGround = false;
        }
      }
    }
    
    this.currentTeamIndex = 0;
    this.activeUnitIndex = 0;
    this.selectedWeapon = 1;
    this.turnTimeLeft = this.maxTurnTime;
    this.wind = (Math.random() - 0.5) * 6;
    this.projectiles = [];
    this.mines = [];
    this.phase = 'playing';
    
    Camera.follow(this.getActiveUnit());
    Camera.targetZoom = Math.min(1.2, 1400 / this.canvas.width);
    
    this.updateHUD();
    AudioEngine.play('turnEnd');
  },
  
  getActiveUnit() {
    return this.units[this.activeUnitIndex];
  },
  
  getCurrentTeam() {
    return this.teams[this.currentTeamIndex];
  },
  
  fireProjectile(weapon, angle, power, unit) {
    const vx = Math.cos(angle) * power * 8;
    const vy = -Math.sin(angle) * power * 8;
    
    if (weapon.category === 'direct') {
      const dx = Math.cos(angle);
      const dy = Math.sin(angle);
      let hitX = unit.x, hitY = unit.y;
      let dist = 0;
      while (dist < 600) {
        hitX += dx * 5;
        hitY += dy * 5;
        dist += 5;
        
        if (Terrain.isSolid(hitX, hitY)) break;
        
        for (const u of this.units) {
          if (!u.alive || u.teamId === unit.teamId) continue;
          const ddx = u.x - hitX, ddy = u.y - hitY;
          if (Math.sqrt(ddx*ddx+ddy*ddy) < 20) break;
        }
      }
      
      for (const u of this.units) {
        if (!u.alive || u.teamId === unit.teamId) continue;
        const ddx = u.x - hitX, ddy = u.y - hitY;
        const dist2 = Math.sqrt(ddx*ddx+ddy*ddy);
        if (dist2 < 40) {
          u.takeDamage(Math.floor(weapon.power * (1 - dist2/50)), unit.x, unit.y);
        }
      }
      
      Particles.explosion(hitX, hitY, 8);
      AudioEngine.play('hit', {freq: 600});
      Terrain.destroy(hitX, hitY, 15);
    } else if (weapon.id === 'mines') {
      const proj = new Projectile(unit.x + unit.facing * 30, unit.y - 20, unit.facing * 2, -3, weapon, unit.teamId);
      this.projectiles.push(proj);
    } else if (weapon.id === 'teleport' || weapon.id === 'parachute') {
      return;
    } else {
      const proj = new Projectile(unit.x + unit.facing * 15, unit.y - 20, vx, vy, weapon, unit.teamId);
      
      if (weapon.id === 'homingMissile') {
        let bestTarget = null, bestDist = Infinity;
        for (const u of this.units) {
          if (!u.alive || u.teamId === unit.teamId) continue;
          const d = Math.sqrt((u.x-unit.x)*(u.x-unit.x) + (u.y-unit.y)*(u.y-unit.y));
          if (d < bestDist) { bestDist = d; bestTarget = u; }
        }
        proj.homingTarget = bestTarget;
      }
      
      this.projectiles.push(proj);
    }
  },
  
  nextTurn() {
    let found = false;
    for (let attempt = 0; attempt < this.units.length; attempt++) {
      this.activeUnitIndex = (this.activeUnitIndex + 1) % this.units.length;
      
      if (!this.units[this.activeUnitIndex].alive) continue;
      
      const newTeamId = this.units[this.activeUnitIndex].teamId;
      if (newTeamId !== this.currentTeamIndex) {
        this.currentTeamIndex = newTeamId;
        AudioEngine.play('turnEnd');
      }
      
      this.turnTimeLeft = this.maxTurnTime;
      this.moveDir = 0;
      found = true;
      break;
    }
    
    if (!found) {
      this.endGame();
      return;
    }
    
    this.windTimer++;
    if (this.windTimer >= 3) {
      this.wind += (Math.random() - 0.5) * 2;
      this.wind = Math.max(-8, Math.min(8, this.wind));
      this.windTimer = 0;
    }
    
    Camera.follow(this.getActiveUnit());
    this.updateHUD();
  },
  
  endGame() {
    this.phase = 'gameOver';
    
    let bestTeam = null, bestAlive = 0;
    for (const t of this.teams) {
      if (t.alive > bestAlive) { bestAlive = t.alive; bestTeam = t; }
    }
    
    document.getElementById('winnerText').textContent = bestTeam.name + ' Team Wins! 🎉';
    document.getElementById('gameOverOverlay').classList.remove('hidden');
  },
  
  updateHUD() {
    const au = this.getActiveUnit();
    if (!au) return;
    
    const teamInfo = document.getElementById('teamInfo');
    teamInfo.innerHTML = '';
    for (const t of this.teams) {
      const badge = document.createElement('div');
      badge.className = 'team-badge' + (t.id === this.currentTeamIndex ? ' active' : '');
      badge.style.background = t.color;
      badge.style.color = '#fff';
      badge.textContent = t.name + ' (' + t.alive + ')';
      teamInfo.appendChild(badge);
    }
    
    const weaponSelect = document.getElementById('weaponSelect');
    weaponSelect.innerHTML = '';
    for (let i = 0; i < WEAPONS.length; i++) {
      const btn = document.createElement('button');
      btn.className = 'weapon-btn' + (i === this.selectedWeapon ? ' selected' : '');
      btn.textContent = WEAPONS[i].icon;
      btn.title = WEAPONS[i].name + ' - ' + WEAPONS[i].desc;
      btn.addEventListener('click', () => {
        AudioEngine.play('click');
        this.selectedWeapon = i;
        this.updateHUD();
      });
      weaponSelect.appendChild(btn);
    }
    
    const actionBtns = document.getElementById('actionButtons');
    actionBtns.innerHTML = '';
    
    if (this.phase === 'playing') {
      const skipBtn = document.createElement('button');
      skipBtn.className = 'action-btn';
      skipBtn.textContent = 'Skip Turn';
      skipBtn.addEventListener('click', () => {
        AudioEngine.play('click');
        this.nextTurn();
      });
      actionBtns.appendChild(skipBtn);
    }
    
    const windEl = document.getElementById('windIndicator');
    const dir = this.wind >= 0 ? '→' : '←';
    windEl.textContent = 'Wind: ' + dir + ' ' + Math.abs(this.wind).toFixed(1);
  },
  
  update(dt) {
    if (this.phase !== 'playing') return;
    
    if (this.freezeTimer > 0) {
      this.freezeTimer -= dt;
      return;
    }
    
    const au = this.getActiveUnit();
    
    this.turnTimeLeft -= dt;
    document.getElementById('timerValue').textContent = Math.ceil(this.turnTimeLeft);
    
    if (this.turnTimeLeft <= 0) {
      this.nextTurn();
      return;
    }
    
    if (this.turnTimeLeft < 10 && Math.floor(this.turnTimeLeft + dt) !== Math.floor(this.turnTimeLeft)) {
      AudioEngine.play('tick');
    }
    
    const timerEl = document.getElementById('timerValue');
    if (this.turnTimeLeft <= 5) {
      timerEl.style.transform = 'scale(' + (1 + (5-this.turnTimeLeft)*0.1) + ')';
      timerEl.style.color = '#f44336';
    } else {
      timerEl.style.transform = '';
      timerEl.style.color = '#fff';
    }
    
    if (this.cpuTeams.has(this.currentTeamIndex)) {
      this.handleCPUTurn(dt);
    }
    
    if (au && au.alive) {
      if (this.moveDir !== 0) {
        au.isMoving = true;
        au.move(this.moveDir, dt);
      } else {
        au.isMoving = false;
      }
      
      au.update(dt, this.wind);
    }
    
    for (const p of this.projectiles) {
      p.update(dt, this.wind);
    }
    this.projectiles = this.projectiles.filter(p => p.alive);
    
    // Mine collision check
    for (let i = this.mines.length - 1; i >= 0; i--) {
      const m = this.mines[i];
      let exploded = false;
      for (const u of this.units) {
        if (!u.alive || u.teamId === m.teamId) continue;
        const dx = u.x - m.x, dy = u.y - m.y;
        if (Math.sqrt(dx*dx+dy*dy) < 25) {
          for (const uu of this.units) {
            if (!uu.alive || uu.teamId === m.teamId) continue;
            const ddx = uu.x - m.x, ddy = uu.y - m.y;
            const dist = Math.sqrt(ddx*ddx+ddy*ddy);
            if (dist < m.weapon.radius + 20) {
              const falloff = Math.max(0.3, 1 - dist/(m.weapon.radius+20));
              uu.takeDamage(Math.floor(m.weapon.power * falloff), m.x, m.y);
            }
          }
          Terrain.destroy(m.x, m.y, m.weapon.radius);
          Particles.explosion(m.x, m.y, 15);
          AudioEngine.play('explosion');
          this.mines.splice(i, 1);
          exploded = true;
          break;
        }
      }
    }
    
    Terrain.updateDebris(dt);
    Particles.update(dt);
    CombatText.update(dt);
    Camera.update(dt);
    
    // Update team alive counts
    for (const t of this.teams) {
      let count = 0;
      for (const u of this.units) {
        if (u.alive && u.teamId === t.id) count++;
      }
      t.alive = count;
    }
    
    // Check game over
    const aliveTeams = this.teams.filter(t => t.alive > 0);
    if (aliveTeams.length <= 1 && this.phase !== 'gameOver') {
      this.endGame();
    }
    
    this.updateHUD();
  },
  
  cpuTurnTimer: 0,
  cpuActionQueue: [],
  
  handleCPUTurn(dt) {
    const au = this.getActiveUnit();
    if (!au || !au.alive) { this.nextTurn(); return; }
    
    this.cpuTurnTimer += dt;
    
    if (this.cpuTurnTimer < 1.5) return;
    
    if (this.cpuActionQueue.length === 0) {
      const hazardRisk = AI.assessHazard(au);
      
      if (hazardRisk > 0.5 && au.onGround) {
        const safeDir = Terrain.getGroundY(au.x - 30) < Terrain.getGroundY(au.x + 30) ? -1 : 1;
        this.cpuActionQueue.push({ type: 'move', dir: safeDir, duration: 1 });
      }
      
      const weaponIdx = Math.floor(Math.random() * WEAPONS.length);
      const weapon = WEAPONS[weaponIdx];
      this.cpuActionQueue.push({ type: 'fire', weaponIndex: weaponIdx });
    }
    
    if (this.cpuActionQueue.length > 0) {
      const action = this.cpuActionQueue.shift();
      
      if (action.type === 'move') {
        au.isMoving = true;
        au.move(action.dir, dt);
        if (this.cpuTurnTimer - dt <= action.duration + 1.5) {
          this.cpuActionQueue.unshift(action);
        }
      } else if (action.type === 'fire') {
        const weapon = WEAPONS[action.weaponIndex];
        AI.executeTurn(au, weapon).then(() => {
          if (this.cpuActionQueue.length === 0) {
            this.nextTurn();
          }
        });
      }
    }
    
    if (this.turnTimeLeft <= 1 && this.cpuActionQueue.length === 0) {
      this.nextTurn();
    }
  },
  
  render() {
    const ctx = this.ctx;
    const camX = Camera.x, camY = Camera.y, scale = Camera.zoom;
    
    Sky.render(ctx, camX, camY, scale);
    
    Camera.apply(ctx);
    
    Terrain.render(ctx, camX, camY, scale);
    
    // Mines
    for (const m of this.mines) {
      const sx = (m.x - camX) * scale;
      const sy = (m.y - camY) * scale;
      ctx.fillStyle = '#795548';
      ctx.beginPath();
      ctx.arc(sx, sy, 6*scale, 0, Math.PI*2);
      ctx.fill();
    }
    
    // Units
    for (const u of this.units) {
      u.render(ctx, camX, camY, scale);
    }
    
    // Projectiles
    for (const p of this.projectiles) {
      p.render(ctx, camX, camY, scale);
    }
    
    Particles.render(ctx, camX, camY, scale);
    CombatText.render(ctx, camX, camY, scale);
    
    // Trajectory preview
    if (this.phase === 'playing' && !this.cpuTeams.has(this.currentTeamIndex)) {
      const au = this.getActiveUnit();
      if (au && au.alive) {
        const weapon = WEAPONS[this.selectedWeapon];
        if (weapon.category === 'indirect' || weapon.category === 'aoe') {
          Trajectory.draw(ctx, au.x + au.facing*15, au.y - 20, this.aimAngle, this.aimPower, weapon);
        }
      }
    }
    
    // Active unit indicator
    const activeUnit = this.getActiveUnit();
    if (activeUnit && activeUnit.alive) {
      const sx = (activeUnit.x - camX) * scale;
      const sy = (activeUnit.y - 40 - camY) * scale;
      ctx.fillStyle = '#ffeb3b';
      ctx.font = 'bold ' + Math.floor(16*scale) + 'px sans-serif';
      ctx.textAlign = 'center';
      ctx.fillText('▼', sx, sy);
    }
    
    Camera.restore(ctx);
  },
  
  setupInput() {
    const canvas = this.canvas;
    
    canvas.addEventListener('mousedown', (e) => {
      if (this.phase !== 'playing' || this.cpuTeams.has(this.currentTeamIndex)) return;
      AudioEngine.resume();
      this.mouseDown = true;
      this.mouseX = e.clientX;
      this.mouseY = e.clientY;
    });
    
    canvas.addEventListener('mousemove', (e) => {
      if (!this.mouseDown || this.phase !== 'playing') return;
      
      const au = this.getActiveUnit();
      if (!au || !au.alive) return;
      
      const wx = (e.clientX - this.canvas.width/2) / Camera.zoom + Camera.x;
      const wy = (e.clientY - this.canvas.height/2) / Camera.zoom + Camera.y;
      
      const dx = wx - au.x;
      const dy = -(wy - au.y);
      
      this.aimAngle = Math.atan2(Math.abs(dy), Math.abs(dx));
      if (dx < 0) this.aimAngle = Math.PI - this.aimAngle;
      
      const dist = Math.sqrt((wx-au.x)*(wx-au.x) + (wy-au.y)*(wy-au.y));
      this.aimPower = Math.max(3, Math.min(20, dist / 50));
    });
    
    canvas.addEventListener('mouseup', (e) => {
      if (!this.mouseDown || this.phase !== 'playing') return;
      
      const au = this.getActiveUnit();
      if (!au || !au.alive) return;
      
      const weapon = WEAPONS[this.selectedWeapon];
      
      if (weapon.id === 'teleport' || weapon.id === 'parachute') {
        const wx = (e.clientX - this.canvas.width/2) / Camera.zoom + Camera.x;
        
        if (weapon.id === 'teleport') {
          Particles.explosion(au.x, au.y, 8);
          au.x = Math.max(20, Math.min(Terrain.width-20, wx));
          au.y = Terrain.getGroundY(au.x) - au.height/2 - 5;
          Particles.explosion(au.x, au.y, 8);
          AudioEngine.play('hit', {freq: 500});
        } else if (weapon.id === 'parachute') {
          au.hasParachute = true;
          au.parachuteOpen = true;
          au.x = Math.max(20, Math.min(Terrain.width-20, wx));
          au.y = 50;
          au.vy = 0;
          AudioEngine.play('jump');
        }
      } else {
        this.fireProjectile(weapon, this.aimAngle, this.aimPower, au);
      }
      
      this.mouseDown = false;
    });
    
    const keys = {};
    window.addEventListener('keydown', (e) => {
      if (this.phase !== 'playing' || this.cpuTeams.has(this.currentTeamIndex)) return;
      
      switch(e.key) {
        case 'ArrowLeft': case 'a':
          keys['left'] = true;
          this.moveDir = -1;
          break;
        case 'ArrowRight': case 'd':
          keys['right'] = true;
          this.moveDir = 1;
          break;
        case 'ArrowUp': case 'w': case ' ':
          const au2 = this.getActiveUnit();
          if (au2) au2.jump();
          break;
        case 'Escape':
          this.nextTurn();
          break;
      }
    });
    
    window.addEventListener('keyup', (e) => {
      switch(e.key) {
        case 'ArrowLeft': case 'a': keys['left'] = false; if (!keys['right']) this.moveDir = 0; break;
        case 'ArrowRight': case 'd': keys['right'] = false; if (!keys['left']) this.moveDir = 0; break;
      }
    });
    
    window.addEventListener('keydown', (e) => {
      const num = parseInt(e.key);
      if (num >= 1 && num <= WEAPONS.length) {
        AudioEngine.play('click');
        this.selectedWeapon = num - 1;
        this.updateHUD();
      }
    });
  },
  
  lastTime: 0,
  
  loop(timestamp) {
    const dt = Math.min(0.05, (timestamp - this.lastTime) / 1000);
    this.lastTime = timestamp;
    
    Game.update(dt);
    Game.render();
    
    requestAnimationFrame((t) => Game.loop(t));
  },
  
  start() {
    this.init();
    this.setupInput();
    this.lastTime = performance.now();
    requestAnimationFrame((t) => this.loop(t));
  }
};

// ── const: Terrain ──
// ===== TERRAIN SYSTEM =====
const Terrain = {
  width: 2400,
  height: 700,
  waterLevel: 620,
  data: null,
  heightmap: null,
  debris: [],
  
  generate(type) {
    this.data = new Uint8ClampedArray(this.width * this.height * 4);
    this.heightmap = new Float32Array(this.width);
    this.debris = [];
    
    const h = this.height;
    switch(type) {
      case 'island': this.genIsland(); break;
      case 'twoIslands': this.genTwoIslands(); break;
      case 'cave': this.genCave(); break;
    }
  },
  
  genIsland() {
    const w = this.width, h = this.height;
    const baseH = h * 0.45;
    for (let x = 0; x < w; x++) {
      let y = baseH;
      y += Math.sin(x * 0.008) * 60;
      y += Math.sin(x * 0.02 + 1) * 30;
      y += Math.sin(x * 0.05 + 3) * 15;
      y += (Math.random() - 0.5) * 8;
      this.heightmap[x] = Math.max(h * 0.2, Math.min(this.waterLevel - 10, y));
    }
    for (let x = 0; x < w; x++) {
      const sy = Math.floor(this.heightmap[x]);
      for (let y = 0; y < h; y++) {
        const idx = (y * w + x) * 4;
        if (y >= sy && y < this.waterLevel) {
          const depth = (y - sy) / Math.max(1, this.waterLevel - sy);
          // Grass layer on top
          if (y === sy || y === sy + 1) {
            this.data[idx] = 60; this.data[idx+1] = 140; this.data[idx+2] = 50; this.data[idx+3] = 255;
          } else {
            const r = Math.floor(120 - depth * 50 + Math.random() * 10);
            const g = Math.floor(80 - depth * 30 + Math.random() * 8);
            const b = Math.floor(40 - depth * 15 + Math.random() * 5);
            this.data[idx] = r; this.data[idx+1] = g; this.data[idx+2] = b; this.data[idx+3] = 255;
          }
        } else if (y >= this.waterLevel) {
          const wave = Math.sin(x * 0.05 + y * 0.1) * 10;
          this.data[idx] = 30; this.data[idx+1] = 60 + wave; this.data[idx+2] = 140; this.data[idx+3] = 200;
        } else {
          this.data[idx+3] = 0;
        }
      }
    }
  },
  
  genTwoIslands() {
    const w = this.width, h = this.height;
    for (let x = 0; x < w; x++) {
      let y = h * 0.5;
      const d1 = Math.exp(-Math.pow((x - w*0.3) / 200, 2));
      const d2 = Math.exp(-Math.pow((x - w*0.7) / 200, 2));
      y -= (d1 + d2) * 150;
      y += Math.sin(x * 0.015) * 30;
      y += Math.sin(x * 0.04) * 15;
      y += (Math.random() - 0.5) * 6;
      this.heightmap[x] = Math.max(h * 0.2, Math.min(this.waterLevel - 10, y));
    }
    for (let x = 0; x < w; x++) {
      const sy = Math.floor(this.heightmap[x]);
      for (let y = 0; y < h; y++) {
        const idx = (y * w + x) * 4;
        if (y >= sy && y < this.waterLevel) {
          if (y === sy || y === sy + 1) {
            this.data[idx] = 60; this.data[idx+1] = 140; this.data[idx+2] = 50; this.data[idx+3] = 255;
          } else {
            const depth = (y - sy) / Math.max(1, this.waterLevel - sy);
            const r = Math.floor(120 - depth * 50 + Math.random() * 10);
            const g = Math.floor(80 - depth * 30 + Math.random() * 8);
            const b = Math.floor(40 - depth * 15 + Math.random() * 5);
            this.data[idx] = r; this.data[idx+1] = g; this.data[idx+2] = b; this.data[idx+3] = 255;
          }
        } else if (y >= this.waterLevel) {
          const wave = Math.sin(x * 0.05 + y * 0.1) * 10;
          this.data[idx] = 30; this.data[idx+1] = 60 + wave; this.data[idx+2] = 140; this.data[idx+3] = 200;
        } else {
          this.data[idx+3] = 0;
        }
      }
    }
  },
  
  genCave() {
    const w = this.width, h = this.height;
    for (let x = 0; x < w; x++) {
      let topY = h * 0.15 + Math.sin(x * 0.01) * 20 + Math.random() * 5;
      const arch = Math.exp(-Math.pow((x - w/2) / (w*0.3), 2));
      topY += arch * h * 0.35;
      this.heightmap[x] = topY;
      
      for (let y = 0; y < h; y++) {
        const idx = (y * w + x) * 4;
        if ((y <= topY || y >= this.waterLevel)) {
          if (y >= this.waterLevel) {
            const wave = Math.sin(x * 0.05 + y * 0.1) * 10;
            this.data[idx] = 30; this.data[idx+1] = 60 + wave; this.data[idx+2] = 140; this.data[idx+3] = 200;
          } else {
            const depth = y / Math.max(1, topY);
            const r = Math.floor(100 - depth * 40 + Math.random() * 8);
            const g = Math.floor(70 - depth * 25 + Math.random() * 6);
            const b = Math.floor(35 - depth * 10 + Math.random() * 4);
            this.data[idx] = r; this.data[idx+1] = g; this.data[idx+2] = b; this.data[idx+3] = 255;
          }
        } else {
          this.data[idx+3] = 0;
        }
      }
    }
  },
  
  isSolid(px, py) {
    if (!this.data) return false;
    const x = Math.floor(px), y = Math.floor(py);
    if (x < 0 || x >= this.width || y < 0 || y >= this.height) return true;
    const idx = (y * this.width + x) * 4;
    return this.data[idx+3] > 128 && y < this.waterLevel;
  },
  
  isWater(px, py) {
    if (!this.data) return false;
    const x = Math.floor(px), y = Math.floor(py);
    if (x < 0 || x >= this.width || y < 0 || y >= this.height) return false;
    return y >= this.waterLevel && this.data[(y * this.width + x) * 4 + 3] > 128;
  },
  
  destroy(cx, cy, radius) {
    if (!this.data) return;
    const r = Math.ceil(radius);
    let debrisCount = 0;
    for (let dy = -r; dy <= r && debrisCount < 30; dy++) {
      for (let dx = -r; dx <= r && debrisCount < 30; dx++) {
        const dist = Math.sqrt(dx*dx + dy*dy);
        if (dist > radius) continue;
        const px = Math.floor(cx + dx), py = Math.floor(cy + dy);
        if (px < 0 || px >= this.width || py < 0 || py >= this.height) continue;
        const idx = (py * this.width + px) * 4;
        if (this.data[idx+3] > 128 && py < this.waterLevel) {
          const cr = this.data[idx], cg = this.data[idx+1], cb = this.data[idx+2];
          this.data[idx+3] = 0;
          
          if (Math.random() < 0.15 && debrisCount < 30) {
            this.debris.push({
              x: px, y: py, vx: (Math.random()-0.5)*4, vy: -Math.random()*6 - 2,
              size: 2 + Math.random()*4, color: 'rgb('+cr+','+cg+','+cb+')', life: 1
            });
            debrisCount++;
          }
        }
      }
    }
  },
  
  getGroundY(x) {
    if (!this.heightmap) return this.height * 0.5;
    const xi = Math.floor(Math.max(0, Math.min(this.width-1, x)));
    return this.heightmap[xi];
  },
  
  updateDebris(dt) {
    for (let i = this.debris.length - 1; i >= 0; i--) {
      const d = this.debris[i];
      d.vy += 25 * dt;
      d.x += d.vx * dt * 60;
      d.y += d.vy * dt * 60;
      d.life -= dt * 0.8;
      
      if (d.life <= 0 || this.isSolid(d.x, d.y) || d.y > this.height) {
        this.debris.splice(i, 1);
      }
    }
  },
  
  render(ctx, camX, camY, scale) {
    if (!this.data) return;
    
    const viewW = Math.ceil(ctx.canvas.width / scale);
    const viewH = Math.ceil(ctx.canvas.height / scale);
    const startX = Math.max(0, Math.floor(camX));
    const startY = Math.max(0, Math.floor(camY));
    const endX = Math.min(this.width, startX + viewW);
    const endY = Math.min(this.height, startY + viewH);
    
    if (endX <= startX || endY <= startY) return;
    
    const imgData = ctx.createImageData(endX - startX, endY - startY);
    for (let y = startY; y < endY; y++) {
      for (let x = startX; x < endX; x++) {
        const srcIdx = (y * this.width + x) * 4;
        const dstIdx = ((y - startY) * (endX - startX) + (x - startX)) * 4;
        imgData.data[dstIdx] = this.data[srcIdx];
        imgData.data[dstIdx+1] = this.data[srcIdx+1];
        imgData.data[dstIdx+2] = this.data[srcIdx+2];
        imgData.data[dstIdx+3] = this.data[srcIdx+3];
      }
    }
    
    ctx.putImageData(imgData, 0, 0);
    
    // Draw terrain surface highlight line
    ctx.strokeStyle = 'rgba(100,200,80,0.4)';
    ctx.lineWidth = 2 * scale;
    ctx.beginPath();
    for (let x = startX; x < endX; x++) {
      const sy = this.heightmap[x];
      if (sy >= startY && sy <= endY) {
        const sx = (x - camX) * scale;
        const ssy = (sy - camY) * scale;
        if (x === startX) ctx.moveTo(sx, ssy);
        else ctx.lineTo(sx, ssy);
      }
    }
    ctx.stroke();
    
    // Render debris
    for (const d of this.debris) {
      const sx = (d.x - camX) * scale;
      const sy = (d.y - camY) * scale;
      ctx.globalAlpha = Math.max(0, d.life);
      ctx.fillStyle = d.color;
      ctx.fillRect(sx, sy, d.size * scale, d.size * scale);
    }
    ctx.globalAlpha = 1;
  }
};

// ── main ──
Game.start();
</script>
</body>
</html>
<!-- agent-meta {"model":"thinkingcap-qwen3.6-27b","provider":"lmstudio","persona":"composer","sessionId":"119b9a96-999f-4b2e-acc2-fe28e28083a2","tokensIn":2150378,"tokensOut":57993,"tokensTotal":2208371,"turns":48,"toolCalls":48,"failedToolCalls":3,"timestamp":"2026-07-18T16:41:52.947Z"} -->