← Worms results

Worms

Grok 4.5 · legacy

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 97,485 bytes · SHA-256 dff2ca71adcb
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Wormageddon — Artillery Tactics</title>
<style>
  @import url('https://fonts.googleapis.com/css2?family=Fredoka:wght@400;600;700&family=Press+Start+2P&display=swap');

  :root {
    --sky-top: #1a3a6b;
    --sky-mid: #4a90c8;
    --sky-bot: #87ceeb;
    --water: #1e6fad;
    --water-deep: #0d3d5c;
    --panel: rgba(12, 18, 32, 0.88);
    --panel-border: rgba(120, 180, 255, 0.35);
    --accent: #ffd36b;
    --danger: #ff5a5a;
    --ok: #6bff9a;
    --text: #f0f4ff;
    --muted: #8aa0c0;
  }

  * { box-sizing: border-box; margin: 0; padding: 0; }

  html, body {
    width: 100%; height: 100%;
    overflow: hidden;
    background: #0a0e18;
    font-family: 'Fredoka', system-ui, sans-serif;
    color: var(--text);
    user-select: none;
  }

  #app {
    width: 100%; height: 100%;
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
  }

  canvas#game {
    display: block;
    image-rendering: pixelated;
    background: #000;
    box-shadow: 0 0 80px rgba(80, 140, 255, 0.2);
    border-radius: 4px;
  }

  /* ===== SCREENS ===== */
  .screen {
    position: absolute;
    inset: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    z-index: 50;
    background:
      radial-gradient(ellipse at 50% 20%, rgba(60,120,220,0.25), transparent 55%),
      linear-gradient(180deg, #0b1220 0%, #152040 50%, #0a1428 100%);
    transition: opacity 0.4s ease, visibility 0.4s;
  }
  .screen.hidden { opacity: 0; visibility: hidden; pointer-events: none; }

  .logo {
    font-family: 'Press Start 2P', monospace;
    font-size: clamp(22px, 4vw, 42px);
    background: linear-gradient(180deg, #fff8c8 0%, #ffb347 40%, #ff6b35 100%);
    -webkit-background-clip: text;
    -webkit-text-fill-color: transparent;
    background-clip: text;
    text-shadow: none;
    filter: drop-shadow(0 4px 0 #8b2e0e) drop-shadow(0 8px 20px rgba(255,120,40,0.4));
    margin-bottom: 8px;
    letter-spacing: 2px;
    animation: logoBob 3s ease-in-out infinite;
  }
  @keyframes logoBob {
    0%,100% { transform: translateY(0) rotate(-1deg); }
    50% { transform: translateY(-8px) rotate(1deg); }
  }

  .tagline {
    color: var(--muted);
    font-size: 15px;
    margin-bottom: 36px;
    letter-spacing: 3px;
    text-transform: uppercase;
  }

  .menu-card {
    background: var(--panel);
    border: 1px solid var(--panel-border);
    border-radius: 18px;
    padding: 28px 36px;
    min-width: min(420px, 92vw);
    backdrop-filter: blur(16px);
    box-shadow: 0 20px 60px rgba(0,0,0,0.5), inset 0 1px 0 rgba(255,255,255,0.06);
  }

  .menu-section { margin-bottom: 20px; }
  .menu-section label {
    display: block;
    font-size: 12px;
    text-transform: uppercase;
    letter-spacing: 2px;
    color: var(--muted);
    margin-bottom: 10px;
  }

  .chip-row {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
  }

  .chip {
    flex: 1;
    min-width: 70px;
    padding: 10px 12px;
    border-radius: 10px;
    border: 1px solid rgba(120,160,220,0.25);
    background: rgba(30,45,80,0.6);
    color: var(--text);
    font-family: inherit;
    font-size: 14px;
    font-weight: 600;
    cursor: pointer;
    transition: all 0.15s ease;
  }
  .chip:hover { border-color: var(--accent); transform: translateY(-1px); }
  .chip.active {
    background: linear-gradient(180deg, #3a6ab0, #254a88);
    border-color: #7ab0ff;
    box-shadow: 0 0 0 2px rgba(120,180,255,0.25), 0 4px 12px rgba(40,80,180,0.4);
  }

  .btn-primary {
    width: 100%;
    margin-top: 10px;
    padding: 16px 24px;
    border: none;
    border-radius: 12px;
    font-family: 'Fredoka', sans-serif;
    font-size: 18px;
    font-weight: 700;
    letter-spacing: 1px;
    cursor: pointer;
    color: #2a1800;
    background: linear-gradient(180deg, #ffe08a 0%, #ffb020 55%, #e08810 100%);
    box-shadow: 0 4px 0 #9a5800, 0 8px 24px rgba(255,160,20,0.35);
    transition: transform 0.1s, box-shadow 0.1s;
  }
  .btn-primary:hover { transform: translateY(-2px); box-shadow: 0 6px 0 #9a5800, 0 12px 30px rgba(255,160,20,0.4); }
  .btn-primary:active { transform: translateY(2px); box-shadow: 0 2px 0 #9a5800; }

  .btn-ghost {
    width: 100%;
    margin-top: 10px;
    padding: 12px;
    border-radius: 10px;
    border: 1px solid rgba(120,160,220,0.3);
    background: transparent;
    color: var(--muted);
    font-family: inherit;
    font-size: 14px;
    cursor: pointer;
    transition: all 0.15s;
  }
  .btn-ghost:hover { color: var(--text); border-color: var(--accent); }

  /* ===== HUD ===== */
  #hud {
    position: absolute;
    inset: 0;
    pointer-events: none;
    z-index: 20;
    display: none;
  }
  #hud.visible { display: block; }

  .hud-top {
    position: absolute;
    top: 0; left: 0; right: 0;
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
    padding: 14px 18px;
    background: linear-gradient(180deg, rgba(0,0,0,0.55) 0%, transparent 100%);
  }

  .team-strip {
    display: flex;
    gap: 10px;
    flex-wrap: wrap;
  }

  .team-badge {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 6px 12px;
    border-radius: 10px;
    background: rgba(8,12,24,0.75);
    border: 1px solid rgba(255,255,255,0.1);
    font-size: 13px;
    font-weight: 600;
    transition: all 0.3s;
  }
  .team-badge.active {
    border-color: var(--accent);
    box-shadow: 0 0 16px rgba(255,211,107,0.3);
    transform: scale(1.05);
  }
  .team-dot {
    width: 12px; height: 12px;
    border-radius: 50%;
    box-shadow: 0 0 8px currentColor;
  }

  .timer-wrap {
    text-align: center;
    min-width: 120px;
  }
  .timer-label {
    font-size: 10px;
    letter-spacing: 2px;
    text-transform: uppercase;
    color: var(--muted);
  }
  .timer-val {
    font-family: 'Press Start 2P', monospace;
    font-size: 28px;
    color: #fff;
    transition: transform 0.2s, color 0.2s;
    text-shadow: 0 0 20px rgba(255,200,80,0.5);
  }
  .timer-val.urgent {
    color: #ff5050;
    animation: timerPulse 0.4s ease-in-out infinite;
  }
  @keyframes timerPulse {
    0%,100% { transform: scale(1); }
    50% { transform: scale(1.15); }
  }

  .wind-meter {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 8px 14px;
    background: rgba(8,12,24,0.75);
    border-radius: 10px;
    border: 1px solid rgba(255,255,255,0.1);
    font-size: 13px;
  }
  .wind-arrow {
    font-size: 18px;
    transition: transform 0.3s;
  }

  .hud-bottom {
    position: absolute;
    bottom: 0; left: 0; right: 0;
    padding: 12px 16px 16px;
    background: linear-gradient(0deg, rgba(0,0,0,0.65) 0%, transparent 100%);
    display: flex;
    flex-direction: column;
    gap: 10px;
    pointer-events: auto;
  }

  .weapon-bar {
    display: flex;
    gap: 6px;
    justify-content: center;
    flex-wrap: wrap;
  }

  .wep-btn {
    position: relative;
    width: 54px; height: 54px;
    border-radius: 12px;
    border: 2px solid rgba(255,255,255,0.12);
    background: rgba(15,22,40,0.9);
    color: #fff;
    font-size: 22px;
    cursor: pointer;
    transition: all 0.15s;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  .wep-btn:hover { border-color: rgba(255,200,80,0.5); transform: translateY(-3px); }
  .wep-btn.selected {
    border-color: var(--accent);
    background: linear-gradient(180deg, #3a5080, #1a2848);
    box-shadow: 0 0 0 2px rgba(255,211,107,0.3), 0 6px 16px rgba(0,0,0,0.4);
    transform: translateY(-4px) scale(1.08);
  }
  .wep-btn .key {
    position: absolute;
    top: 2px; right: 4px;
    font-size: 9px;
    color: var(--muted);
    font-family: monospace;
  }
  .wep-btn .ammo {
    position: absolute;
    bottom: 2px; right: 4px;
    font-size: 10px;
    font-weight: 700;
    color: var(--accent);
  }

  .turn-banner {
    position: absolute;
    top: 28%;
    left: 50%;
    transform: translate(-50%, -50%);
    font-family: 'Press Start 2P', monospace;
    font-size: clamp(16px, 3vw, 28px);
    text-align: center;
    padding: 18px 32px;
    border-radius: 14px;
    background: rgba(8,12,24,0.9);
    border: 2px solid var(--accent);
    box-shadow: 0 0 40px rgba(255,180,40,0.3);
    z-index: 30;
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.3s, transform 0.3s;
  }
  .turn-banner.show {
    opacity: 1;
    transform: translate(-50%, -50%) scale(1);
  }
  .turn-banner.hide {
    opacity: 0;
    transform: translate(-50%, -60%) scale(0.9);
  }

  .float-text-layer {
    position: absolute;
    inset: 0;
    pointer-events: none;
    overflow: hidden;
    z-index: 25;
  }
  .float-dmg {
    position: absolute;
    font-family: 'Press Start 2P', monospace;
    font-size: 14px;
    font-weight: 700;
    color: #ff6b6b;
    text-shadow: 0 2px 0 #800, 0 0 10px rgba(255,50,50,0.6);
    animation: floatUp 1.2s ease-out forwards;
    pointer-events: none;
  }
  .float-dmg.heal {
    color: #6bff9a;
    text-shadow: 0 2px 0 #060, 0 0 10px rgba(50,255,100,0.5);
  }
  @keyframes floatUp {
    0% { opacity: 1; transform: translateY(0) scale(0.6); }
    20% { transform: translateY(-10px) scale(1.2); }
    100% { opacity: 0; transform: translateY(-50px) scale(1); }
  }

  .help-hint {
    text-align: center;
    font-size: 12px;
    color: rgba(200,220,255,0.55);
    letter-spacing: 0.5px;
  }

  .power-bar-wrap {
    position: absolute;
    bottom: 140px;
    left: 50%;
    transform: translateX(-50%);
    width: 220px;
    height: 18px;
    background: rgba(0,0,0,0.7);
    border-radius: 9px;
    border: 2px solid rgba(255,255,255,0.2);
    overflow: hidden;
    display: none;
    z-index: 22;
  }
  .power-bar-wrap.show { display: block; }
  .power-bar-fill {
    height: 100%;
    width: 0%;
    background: linear-gradient(90deg, #4ade80, #facc15, #f97316, #ef4444);
    border-radius: 7px;
    transition: none;
  }
  .power-label {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 11px;
    font-weight: 700;
    letter-spacing: 1px;
    text-shadow: 0 1px 2px #000;
  }

  .end-screen-content {
    text-align: center;
  }
  .end-screen-content h2 {
    font-family: 'Press Start 2P', monospace;
    font-size: clamp(18px, 3vw, 28px);
    margin-bottom: 16px;
    background: linear-gradient(180deg, #fff, #ffd36b);
    -webkit-background-clip: text;
    -webkit-text-fill-color: transparent;
  }
  .end-screen-content p {
    color: var(--muted);
    margin-bottom: 24px;
    font-size: 16px;
  }

  .tooltip {
    position: absolute;
    bottom: 70px;
    left: 50%;
    transform: translateX(-50%);
    background: rgba(10,14,28,0.95);
    border: 1px solid var(--panel-border);
    padding: 8px 14px;
    border-radius: 8px;
    font-size: 12px;
    white-space: nowrap;
    pointer-events: none;
    opacity: 0;
    transition: opacity 0.15s;
    z-index: 40;
  }
  .tooltip.show { opacity: 1; }

  /* Loading / gen */
  .gen-overlay {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    background: rgba(5,8,16,0.85);
    z-index: 60;
    flex-direction: column;
    gap: 16px;
  }
  .gen-overlay.hidden { display: none; }
  .spinner {
    width: 48px; height: 48px;
    border: 4px solid rgba(120,160,255,0.2);
    border-top-color: var(--accent);
    border-radius: 50%;
    animation: spin 0.8s linear infinite;
  }
  @keyframes spin { to { transform: rotate(360deg); } }
</style>
</head>
<body>
<div id="app">
  <canvas id="game"></canvas>

  <!-- MENU -->
  <div id="menu" class="screen">
    <div class="logo">WORMAGEDDON</div>
    <div class="tagline">Artillery · Destruction · Chaos</div>
    <div class="menu-card">
      <div class="menu-section">
        <label>Teams</label>
        <div class="chip-row" id="teamCountChips">
          <button class="chip active" data-v="2">2 Teams</button>
          <button class="chip" data-v="3">3 Teams</button>
          <button class="chip" data-v="4">4 Teams</button>
        </div>
      </div>
      <div class="menu-section">
        <label>Players / CPU</label>
        <div class="chip-row" id="cpuChips">
          <button class="chip active" data-v="1">1 Human</button>
          <button class="chip" data-v="2">2 Humans</button>
          <button class="chip" data-v="0">All CPU</button>
          <button class="chip" data-v="-1">All Human</button>
        </div>
      </div>
      <div class="menu-section">
        <label>Terrain</label>
        <div class="chip-row" id="terrainChips">
          <button class="chip active" data-v="island">🏝️ Island</button>
          <button class="chip" data-v="two">🏝️🏝️ Twin Isles</button>
          <button class="chip" data-v="cave">🕳️ Cave</button>
        </div>
      </div>
      <button class="btn-primary" id="btnStart">🔥 Deploy Worms!</button>
      <button class="btn-ghost" id="btnHow">How to Play</button>
    </div>
  </div>

  <!-- HOW TO -->
  <div id="howScreen" class="screen hidden">
    <div class="menu-card" style="max-width:520px">
      <div style="font-family:'Press Start 2P';font-size:14px;margin-bottom:18px;color:var(--accent)">CONTROLS</div>
      <div style="font-size:14px;line-height:1.7;color:#c8d8f0;margin-bottom:16px">
        <b>A/D</b> or ←/→ — Move &nbsp;·&nbsp; <b>W</b> or ↑ — Jump<br>
        <b>Mouse</b> — Aim &nbsp;·&nbsp; <b>Hold LMB</b> — Charge power · release to fire<br>
        <b>1–9</b> — Select weapon &nbsp;·&nbsp; <b>Q/E</b> cycle weapons<br>
        <b>Space</b> — Jump &nbsp;·&nbsp; <b>S</b> — Dig / use utility<br>
        <b>Esc</b> — Pause / Menu
      </div>
      <div style="font-size:13px;line-height:1.6;color:var(--muted);margin-bottom:20px">
        Wind pushes projectiles. Fall damage hurts. Don't drown.
        Last team standing wins. Worms have 100 HP each — 4 per team.
      </div>
      <button class="btn-primary" id="btnBackHow">Got it!</button>
    </div>
  </div>

  <!-- END -->
  <div id="endScreen" class="screen hidden">
    <div class="menu-card end-screen-content">
      <h2 id="endTitle">VICTORY!</h2>
      <p id="endDesc">Team Red remains victorious!</p>
      <button class="btn-primary" id="btnRematch">Play Again</button>
      <button class="btn-ghost" id="btnMenu">Main Menu</button>
    </div>
  </div>

  <div id="genOverlay" class="gen-overlay hidden">
    <div class="spinner"></div>
    <div style="color:var(--muted);letter-spacing:2px;font-size:13px">DIGGING TUNNELS…</div>
  </div>

  <!-- HUD -->
  <div id="hud">
    <div class="hud-top">
      <div class="team-strip" id="teamStrip"></div>
      <div class="timer-wrap">
        <div class="timer-label">TURN</div>
        <div class="timer-val" id="timerVal">45</div>
      </div>
      <div class="wind-meter" id="windMeter">
        <span class="wind-arrow" id="windArrow">→</span>
        <span id="windText">Wind 0</span>
      </div>
    </div>

    <div class="turn-banner" id="turnBanner"></div>
    <div class="float-text-layer" id="floatLayer"></div>

    <div class="power-bar-wrap" id="powerBar">
      <div class="power-bar-fill" id="powerFill"></div>
      <div class="power-label">POWER</div>
    </div>

    <div class="tooltip" id="tooltip"></div>

    <div class="hud-bottom">
      <div class="weapon-bar" id="weaponBar"></div>
      <div class="help-hint" id="helpHint">Aim with mouse · Hold fire to charge · Release to launch</div>
    </div>
  </div>
</div>

<script>
// ═══════════════════════════════════════════════════════════
// WORMAGEDDON — Highly polished Worms-like Artillery Game
// ═══════════════════════════════════════════════════════════

(() => {
'use strict';

// ---------- Canvas / Core ----------
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const WORLD_W = 1920;
const WORLD_H = 1080;
const WATER_Y = WORLD_H - 90;
let SCALE = 1;
let dpr = 1;

function resize() {
  dpr = Math.min(window.devicePixelRatio || 1, 2);
  const maxW = window.innerWidth;
  const maxH = window.innerHeight;
  SCALE = Math.min(maxW / WORLD_W, maxH / WORLD_H);
  canvas.style.width = (WORLD_W * SCALE) + 'px';
  canvas.style.height = (WORLD_H * SCALE) + 'px';
  canvas.width = Math.floor(WORLD_W * dpr);
  canvas.height = Math.floor(WORLD_H * dpr);
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
window.addEventListener('resize', resize);
resize();

// ---------- Audio (Web Audio quirky SFX) ----------
const AudioSys = {
  ctx: null,
  master: null,
  muted: false,
  tempo: 1,

  init() {
    if (this.ctx) return;
    const AC = window.AudioContext || window.webkitAudioContext;
    this.ctx = new AC();
    this.master = this.ctx.createGain();
    this.master.gain.value = 0.28;
    this.master.connect(this.ctx.destination);
  },

  resume() {
    this.init();
    if (this.ctx.state === 'suspended') this.ctx.resume();
  },

  tone(freq, dur, type = 'square', vol = 0.3, slide = 0, delay = 0) {
    if (this.muted || !this.ctx) return;
    const t0 = this.ctx.currentTime + delay;
    const o = this.ctx.createOscillator();
    const g = this.ctx.createGain();
    o.type = type;
    o.frequency.setValueAtTime(freq, t0);
    if (slide) o.frequency.linearRampToValueAtTime(freq + slide, t0 + dur);
    g.gain.setValueAtTime(vol, t0);
    g.gain.exponentialRampToValueAtTime(0.001, t0 + dur);
    o.connect(g); g.connect(this.master);
    o.start(t0); o.stop(t0 + dur + 0.02);
  },

  noise(dur, vol = 0.2, bpFreq = 800) {
    if (this.muted || !this.ctx) return;
    const t0 = this.ctx.currentTime;
    const n = Math.floor(this.ctx.sampleRate * dur);
    const buf = this.ctx.createBuffer(1, n, this.ctx.sampleRate);
    const d = buf.getChannelData(0);
    for (let i = 0; i < n; i++) d[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / n, 1.5);
    const src = this.ctx.createBufferSource();
    src.buffer = buf;
    const bp = this.ctx.createBiquadFilter();
    bp.type = 'bandpass';
    bp.frequency.value = bpFreq;
    bp.Q.value = 0.6;
    const g = this.ctx.createGain();
    g.gain.setValueAtTime(vol, t0);
    g.gain.exponentialRampToValueAtTime(0.001, t0 + dur);
    src.connect(bp); bp.connect(g); g.connect(this.master);
    src.start(t0);
  },

  click() { this.tone(880, 0.04, 'square', 0.12); this.tone(1320, 0.05, 'triangle', 0.08, 0, 0.02); },
  select() { this.tone(520, 0.06, 'triangle', 0.15); this.tone(780, 0.08, 'sine', 0.1, 0, 0.04); },
  jump() { this.tone(220, 0.1, 'square', 0.15, 180); },
  land() { this.noise(0.08, 0.15, 300); this.tone(90, 0.06, 'triangle', 0.1); },
  charge() { this.tone(180 + Math.random() * 40, 0.05, 'sawtooth', 0.05); },
  fire() { this.noise(0.12, 0.22, 600); this.tone(140, 0.15, 'sawtooth', 0.2, -80); },
  boom(size = 1) {
    this.noise(0.25 + size * 0.2, 0.3 * Math.min(1.4, size), 200 / size);
    this.tone(60 / size, 0.35 + size * 0.1, 'sine', 0.35, -30);
    this.tone(40, 0.5, 'triangle', 0.15 * size);
  },
  splash() { this.noise(0.3, 0.2, 1200); this.tone(300, 0.2, 'sine', 0.1, -200); },
  hurt() { this.tone(180, 0.08, 'square', 0.2, -60); this.tone(120, 0.15, 'sawtooth', 0.12, -40, 0.05); },
  death() {
    this.tone(400, 0.1, 'square', 0.2, -100);
    this.tone(300, 0.12, 'square', 0.15, -80, 0.08);
    this.tone(200, 0.2, 'square', 0.12, -60, 0.16);
    this.noise(0.35, 0.15, 400);
  },
  tick(urgent) {
    const f = urgent ? 880 : 640;
    this.tone(f, 0.04, 'square', urgent ? 0.15 : 0.08);
  },
  win() {
    [523, 659, 784, 1046].forEach((f, i) => this.tone(f, 0.2, 'triangle', 0.18, 0, i * 0.12));
  },
  uiHover() { this.tone(600, 0.03, 'sine', 0.05); },
  whoosh() { this.noise(0.18, 0.12, 2000); this.tone(600, 0.15, 'sine', 0.08, -400); },
  dig() { this.noise(0.06, 0.12, 500); this.tone(100, 0.05, 'square', 0.08); },
  ice() { this.tone(900, 0.08, 'sine', 0.1); this.tone(1200, 0.12, 'triangle', 0.08, 200, 0.04); },
  beep() { this.tone(1100, 0.05, 'square', 0.1); },
};

// ---------- Input ----------
const keys = {};
const mouse = { x: 0, y: 0, worldX: 0, worldY: 0, down: false, justUp: false };
let lastKeyWeapon = -1;

window.addEventListener('keydown', e => {
  keys[e.code] = true;
  if (['Space', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.code)) e.preventDefault();
  AudioSys.resume();
});
window.addEventListener('keyup', e => { keys[e.code] = false; });

canvas.addEventListener('mousemove', e => {
  const r = canvas.getBoundingClientRect();
  mouse.x = (e.clientX - r.left) / SCALE;
  mouse.y = (e.clientY - r.top) / SCALE;
});
canvas.addEventListener('mousedown', e => {
  if (e.button === 0) { mouse.down = true; AudioSys.resume(); }
});
canvas.addEventListener('mouseup', e => {
  if (e.button === 0) { mouse.down = false; mouse.justUp = true; }
});
canvas.addEventListener('contextmenu', e => e.preventDefault());

// ---------- Math / helpers ----------
const PI = Math.PI;
const TAU = PI * 2;
const clamp = (v, a, b) => Math.max(a, Math.min(b, v));
const lerp = (a, b, t) => a + (b - a) * t;
const dist = (x1, y1, x2, y2) => Math.hypot(x2 - x1, y2 - y1);
const rand = (a, b) => a + Math.random() * (b - a);
const randi = (a, b) => (a + Math.floor(Math.random() * (b - a + 1)));
const chance = p => Math.random() < p;
const sign = v => (v < 0 ? -1 : v > 0 ? 1 : 0);
const easeOut = t => 1 - Math.pow(1 - t, 3);

// ---------- Game Config ----------
const TEAM_COLORS = [
  { name: 'Crimson',  primary: '#e74c3c', light: '#ff7b6e', dark: '#8b1e14', skin: '#f5a89a' },
  { name: 'Azure',    primary: '#3498db', light: '#7ec8f5', dark: '#1a5276', skin: '#a8d4f0' },
  { name: 'Emerald',  primary: '#2ecc71', light: '#7dffa8', dark: '#145a32', skin: '#a0e8b8' },
  { name: 'Gold',     primary: '#f1c40f', light: '#ffe66d', dark: '#9a7b00', skin: '#f5e6a0' },
];
const UNITS_PER_TEAM = 4;
const UNIT_HP = 100;
const TURN_TIME = 45;
const MOVE_SPEED = 1.65;
const JUMP_V = -7.2;
const GRAVITY = 0.22;
const FRICTION = 0.82;
const AIR_FRICTION = 0.995;
const FALL_DMG_THRESH = 8.5;

// ---------- Weapons ----------
const WEAPONS = [
  {
    id: 'bazooka', name: 'Bazooka', icon: '🚀', key: '1', cat: 'indirect', ammo: Infinity,
    desc: 'Classic rocket — arcs with wind', radius: 55, dmg: 48, powerNeeded: true,
    fire(u, ang, power) {
      const spd = 6 + power * 14;
      projectiles.push(new Projectile({
        x: u.x + Math.cos(ang) * 18, y: u.y + Math.sin(ang) * 18 - 6,
        vx: Math.cos(ang) * spd, vy: Math.sin(ang) * spd,
        r: 5, team: u.team, owner: u, color: '#ff8844',
        trail: true, explodeRadius: 55, damage: 48, wind: true, gravity: 0.16,
      }));
      AudioSys.fire();
    }
  },
  {
    id: 'grenade', name: 'Grenade', icon: '💣', key: '2', cat: 'indirect', ammo: Infinity,
    desc: 'Bouncy timed grenade (3s)', radius: 60, dmg: 55, powerNeeded: true,
    fire(u, ang, power) {
      const spd = 4 + power * 12;
      projectiles.push(new Projectile({
        x: u.x + Math.cos(ang) * 14, y: u.y + Math.sin(ang) * 14 - 6,
        vx: Math.cos(ang) * spd, vy: Math.sin(ang) * spd,
        r: 6, team: u.team, owner: u, color: '#2d5a27',
        trail: false, explodeRadius: 60, damage: 55, wind: true, gravity: 0.22,
        bounce: 0.55, fuse: 3.0, sticky: false, sprite: 'grenade',
      }));
      AudioSys.fire();
    }
  },
  {
    id: 'shotgun', name: 'Shotgun', icon: '🔫', key: '3', cat: 'direct', ammo: 8,
    desc: 'Close-range blast of pellets', radius: 18, dmg: 18, powerNeeded: false,
    fire(u, ang) {
      AudioSys.noise(0.1, 0.25, 1500);
      AudioSys.tone(90, 0.1, 'sawtooth', 0.2);
      spawnMuzzleFlash(u.x + Math.cos(ang) * 16, u.y + Math.sin(ang) * 16 - 6);
      for (let i = 0; i < 6; i++) {
        const spread = (Math.random() - 0.5) * 0.28;
        const a = ang + spread;
        const spd = 16 + Math.random() * 4;
        projectiles.push(new Projectile({
          x: u.x + Math.cos(a) * 16, y: u.y + Math.sin(a) * 16 - 6,
          vx: Math.cos(a) * spd, vy: Math.sin(a) * spd,
          r: 2.5, team: u.team, owner: u, color: '#ffdd88',
          trail: true, explodeRadius: 16, damage: 14 + randi(0, 6),
          wind: false, gravity: 0.05, lifetime: 0.45, shrink: true,
        }));
      }
      // Knock back self slightly
      u.vx -= Math.cos(ang) * 1.5;
    }
  },
  {
    id: 'holy', name: 'Holy Bomb', icon: '✨', key: '4', cat: 'aoe', ammo: 2,
    desc: 'Divine devastation! Huge crater', radius: 110, dmg: 80, powerNeeded: true,
    fire(u, ang, power) {
      const spd = 5 + power * 11;
      projectiles.push(new Projectile({
        x: u.x + Math.cos(ang) * 14, y: u.y + Math.sin(ang) * 14 - 6,
        vx: Math.cos(ang) * spd, vy: Math.sin(ang) * spd,
        r: 8, team: u.team, owner: u, color: '#ffe566',
        trail: true, explodeRadius: 110, damage: 80, wind: true, gravity: 0.2,
        bounce: 0.4, fuse: 3.5, sprite: 'holy', glow: true,
      }));
      AudioSys.tone(660, 0.15, 'sine', 0.15);
      AudioSys.fire();
    }
  },
  {
    id: 'airstrike', name: 'Airstrike', icon: '✈️', key: '5', cat: 'aoe', ammo: 2,
    desc: 'Click map to rain bombs', radius: 45, dmg: 40, powerNeeded: false, placeCursor: true,
    fire(u, ang, power, tx, ty) {
      const cx = tx ?? mouse.worldX;
      AudioSys.whoosh();
      for (let i = 0; i < 5; i++) {
        setTimeout(() => {
          projectiles.push(new Projectile({
            x: cx + (i - 2) * 40 + rand(-10, 10), y: -30,
            vx: wind * 0.4 + rand(-0.3, 0.3), vy: 5 + i * 0.3,
            r: 5, team: u.team, owner: u, color: '#cc4422',
            trail: true, explodeRadius: 48, damage: 38, wind: true, gravity: 0.18,
          }));
        }, i * 180);
      }
    }
  },
  {
    id: 'bat', name: 'Baseball Bat', icon: '🏏', key: '6', cat: 'direct', ammo: Infinity,
    desc: 'Melee smash — send foes flying!', radius: 30, dmg: 25, powerNeeded: true, melee: true,
    fire(u, ang, power) {
      AudioSys.tone(200, 0.08, 'square', 0.2);
      AudioSys.noise(0.1, 0.15, 800);
      const reach = 36;
      const hx = u.x + Math.cos(ang) * reach;
      const hy = u.y + Math.sin(ang) * reach - 6;
      particles.burst(hx, hy, { count: 8, color: '#fff', speed: 3, life: 0.3 });
      for (const w of allUnits()) {
        if (w === u || w.dead) continue;
        if (dist(hx, hy, w.x, w.y) < 38) {
          const force = 6 + power * 16;
          w.vx = Math.cos(ang) * force;
          w.vy = Math.sin(ang) * force - 3;
          w.grounded = false;
          dmgUnit(w, 15 + power * 20, u);
          particles.burst(w.x, w.y, { count: 12, color: TEAM_COLORS[w.team].primary, speed: 4 });
        }
      }
      // Terrain chip
      digCircle(hx, hy, 12, false);
    }
  },
  {
    id: 'dynamite', name: 'Dynamite', icon: '🧨', key: '7', cat: 'aoe', ammo: 3,
    desc: 'Place & run! 5 second fuse', radius: 75, dmg: 70, powerNeeded: false,
    fire(u) {
      projectiles.push(new Projectile({
        x: u.x + u.facing * 14, y: u.y - 4,
        vx: u.facing * 0.8, vy: -1,
        r: 7, team: u.team, owner: u, color: '#cc2222',
        trail: false, explodeRadius: 75, damage: 70, wind: false, gravity: 0.25,
        bounce: 0.2, fuse: 5.0, sprite: 'dynamite',
      }));
      AudioSys.beep();
    }
  },
  {
    id: 'sheep', name: 'Bouncy Sheep', icon: '🐑', key: '8', cat: 'indirect', ammo: 2,
    desc: 'It lives! Walks then BOOM', radius: 70, dmg: 60, powerNeeded: false,
    fire(u, ang) {
      const dir = Math.cos(ang) >= 0 ? 1 : -1;
      projectiles.push(new Sheep({
        x: u.x + dir * 20, y: u.y - 10,
        vx: dir * 3.5, vy: -2,
        team: u.team, owner: u,
      }));
      AudioSys.tone(400, 0.08, 'sine', 0.15);
      AudioSys.tone(500, 0.08, 'sine', 0.12, 0, 0.08);
    }
  },
  {
    id: 'rope', name: 'Ninja Rope', icon: '🪢', key: '9', cat: 'utility', ammo: 5,
    desc: 'Swing into position', radius: 0, dmg: 0, powerNeeded: false, utility: true,
    fire(u, ang) {
      const maxLen = 220;
      const steps = 40;
      let hx = u.x, hy = u.y - 8;
      const dx = Math.cos(ang) * (maxLen / steps);
      const dy = Math.sin(ang) * (maxLen / steps);
      let hit = false;
      for (let i = 0; i < steps; i++) {
        hx += dx; hy += dy;
        if (terrain.solid(hx, hy)) { hit = true; break; }
      }
      if (hit) {
        u.rope = { x: hx, y: hy, len: dist(u.x, u.y, hx, hy), active: true };
        u.grounded = false;
        AudioSys.whoosh();
      } else {
        AudioSys.tone(150, 0.1, 'square', 0.1);
      }
    }
  },
  {
    id: 'teleport', name: 'Teleport', icon: '🌀', key: '0', cat: 'utility', ammo: 2,
    desc: 'Click to warp (safe ground)', radius: 0, dmg: 0, powerNeeded: false, placeCursor: true, utility: true,
    fire(u, ang, power, tx, ty) {
      const px = tx ?? mouse.worldX;
      const py = ty ?? mouse.worldY;
      // Find safe ground near point
      let sx = px, sy = py;
      let found = false;
      for (let dy = 0; dy < 80 && !found; dy++) {
        if (terrain.solid(px, py + dy) && !terrain.solid(px, py + dy - 18)) {
          sx = px; sy = py + dy - 12; found = true;
        }
      }
      if (!found) {
        for (let r = 10; r < 60 && !found; r += 5) {
          for (let a = 0; a < TAU && !found; a += 0.4) {
            const cx = px + Math.cos(a) * r, cy = py + Math.sin(a) * r;
            if (terrain.solid(cx, cy + 10) && !terrain.solid(cx, cy - 8) && cy < WATER_Y - 10) {
              sx = cx; sy = cy; found = true;
            }
          }
        }
      }
      if (found && sy < WATER_Y - 15) {
        particles.burst(u.x, u.y, { count: 20, color: '#a78bfa', speed: 3, size: 0.5 });
        u.x = sx; u.y = sy; u.vx = 0; u.vy = 0;
        particles.burst(u.x, u.y, { count: 20, color: '#c4b5fd', speed: 3, life: 0.5 });
        AudioSys.tone(300, 0.1, 'sine', 0.15, 400);
        AudioSys.tone(600, 0.15, 'triangle', 0.12, 200, 0.1);
      } else {
        AudioSys.tone(100, 0.15, 'sawtooth', 0.15);
      }
    }
  },
  {
    id: 'jetpack', name: 'Jetpack', icon: '🎒', key: 'J', cat: 'utility', ammo: 2,
    desc: 'Fly for a short burn (fuel)', radius: 0, dmg: 0, powerNeeded: false, utility: true, holdUse: true,
    fire(u) {
      u.jetFuel = 2.8;
      u.usingJet = true;
      AudioSys.tone(120, 0.05, 'sawtooth', 0.1);
    }
  },
  {
    id: 'girder', name: 'Girder Kit', icon: '🧱', key: 'G', cat: 'utility', ammo: 3,
    desc: 'Build a temporary bridge', radius: 0, dmg: 0, powerNeeded: false, placeCursor: true, utility: true,
    fire(u, ang, power, tx, ty) {
      const px = tx ?? mouse.worldX;
      const py = ty ?? mouse.worldY;
      const a = Math.atan2((ty ?? mouse.worldY) - u.y, (tx ?? mouse.worldX) - u.x);
      // Stamp a girder rectangle into terrain
      const len = 90, thick = 12;
      const ca = Math.cos(a), sa = Math.sin(a);
      for (let i = -len / 2; i < len / 2; i++) {
        for (let j = -thick / 2; j < thick / 2; j++) {
          const gx = px + ca * i - sa * j;
          const gy = py + sa * i + ca * j;
          terrain.setSolid(gx, gy, true, 3); // metal type
        }
      }
      particles.burst(px, py, { count: 10, color: '#888', speed: 1.5, life: 0.4 });
      AudioSys.dig();
      AudioSys.tone(200, 0.08, 'triangle', 0.1);
    }
  },
  {
    id: 'freeze', name: 'Ice Bomb', icon: '❄️', key: 'F', cat: 'indirect', ammo: 3,
    desc: 'Freeze enemies in place', radius: 50, dmg: 10, powerNeeded: true,
    fire(u, ang, power) {
      const spd = 5 + power * 12;
      projectiles.push(new Projectile({
        x: u.x + Math.cos(ang) * 14, y: u.y + Math.sin(ang) * 14 - 6,
        vx: Math.cos(ang) * spd, vy: Math.sin(ang) * spd,
        r: 6, team: u.team, owner: u, color: '#88ddff',
        trail: true, explodeRadius: 55, damage: 12, wind: true, gravity: 0.18,
        bounce: 0.3, fuse: 2.5, freeze: true, glow: true, sprite: 'ice',
      }));
      AudioSys.ice();
    }
  },
];

// ---------- Terrain ----------
const TERR = { empty: 0, dirt: 1, grass: 2, rock: 3, metal: 4 };

class Terrain {
  constructor(w, h) {
    this.w = w; this.h = h;
    this.data = new Uint8Array(w * h);
    this.imageData = null;
    this.dirty = true;
    this.off = document.createElement('canvas');
    this.off.width = w; this.off.height = h;
    this.octx = this.off.getContext('2d');
  }

  idx(x, y) {
    x = x | 0; y = y | 0;
    if (x < 0 || y < 0 || x >= this.w || y >= this.h) return -1;
    return y * this.w + x;
  }

  get(x, y) {
    const i = this.idx(x, y);
    return i < 0 ? TERR.rock : this.data[i];
  }

  solid(x, y) {
    return this.get(x, y) !== TERR.empty;
  }

  setSolid(x, y, on, type = TERR.dirt) {
    const i = this.idx(x, y);
    if (i < 0) return;
    this.data[i] = on ? type : TERR.empty;
    this.dirty = true;
  }

  generate(shape) {
    this.data.fill(TERR.empty);
    const w = this.w, h = this.h;
    const noise2 = (x, y, s) => {
      // Simple value noise-ish layers of sin
      return (
        Math.sin(x * 0.008 * s + y * 0.003) * 0.4 +
        Math.sin(x * 0.02 * s + 3.1) * 0.25 +
        Math.sin(x * 0.05 * s + y * 0.01 + 1.7) * 0.15 +
        Math.sin(x * 0.11 * s + 4.2) * 0.08 +
        Math.sin((x + y) * 0.015 * s) * 0.12
      );
    };

    const surface = new Float32Array(w);
    if (shape === 'island') {
      for (let x = 0; x < w; x++) {
        const edge = Math.pow(Math.sin((x / w) * PI), 0.7);
        const n = noise2(x, 0, 1);
        surface[x] = h * 0.55 - n * 120 * edge - edge * 80 + (1 - edge) * 200;
        // Beach shoulders
        if (x < 80 || x > w - 80) surface[x] = h;
      }
    } else if (shape === 'two') {
      for (let x = 0; x < w; x++) {
        const t = x / w;
        const island1 = Math.exp(-Math.pow((t - 0.25) * 5.5, 2));
        const island2 = Math.exp(-Math.pow((t - 0.75) * 5.5, 2));
        const n = noise2(x, 10, 1.2);
        const m = Math.max(island1, island2);
        surface[x] = h * 0.62 - m * 220 - n * 70 * m;
        if (m < 0.08) surface[x] = h;
      }
    } else { // cave
      for (let x = 0; x < w; x++) {
        const n = noise2(x, 20, 0.8);
        surface[x] = h * 0.28 + n * 40;
      }
    }

    // Fill solid below surface
    for (let x = 0; x < w; x++) {
      const sy = clamp(surface[x] | 0, 0, h);
      for (let y = sy; y < h; y++) {
        const depth = y - sy;
        let type = TERR.dirt;
        if (depth < 4) type = TERR.grass;
        else if (depth > 120 + noise2(x, y, 2) * 40) type = TERR.rock;
        // Cave: carve tunnels
        if (shape === 'cave') {
          const caveN = noise2(x * 1.5, y * 1.8, 2.5);
          const caveN2 = noise2(x * 0.7 + 50, y * 0.9, 1.4);
          if (y > surface[x] + 40 && y < WATER_Y - 40) {
            if (caveN > 0.35 || caveN2 > 0.42) type = TERR.empty;
          }
          // Ceiling fill already done by surface
          // Floor
          if (y > h * 0.72 + noise2(x, 0, 1) * 30) type = TERR.rock;
        }
        // Water area stays empty above water... terrain fills underwater as seabed
        if (y > WATER_Y + 5 && type !== TERR.empty) {
          // keep seabed
        }
        this.data[y * w + x] = type;
      }
      // cave ceiling rock
      if (shape === 'cave') {
        for (let y = 0; y < surface[x]; y++) {
          this.data[y * w + x] = TERR.rock;
        }
        // stalactites
        if (chance(0.02)) {
          const len = randi(20, 70);
          for (let y = surface[x]; y < surface[x] + len && y < h; y++) {
            const thick = 1 + (1 - (y - surface[x]) / len) * 4;
            for (let dx = -thick; dx < thick; dx++) {
              const xi = (x + dx) | 0;
              if (xi >= 0 && xi < w) this.data[y * w + xi] = TERR.rock;
            }
          }
        }
      }
    }

    // Sculpt some hills / platforms mid-air for island modes
    if (shape !== 'cave') {
      const plats = shape === 'two' ? 5 : 4;
      for (let p = 0; p < plats; p++) {
        const cx = rand(120, w - 120);
        const cy = rand(h * 0.32, h * 0.55);
        const rw = rand(50, 110), rh = rand(18, 35);
        for (let x = -rw; x < rw; x++) {
          for (let y = -rh; y < rh; y++) {
            const nx = x / rw, ny = y / rh;
            if (nx * nx + ny * ny * 1.8 < 1) {
              const px = (cx + x) | 0, py = (cy + y) | 0;
              if (px >= 0 && px < w && py >= 0 && py < h) {
                const i = py * w + px;
                if (this.data[i] === TERR.empty) {
                  this.data[i] = y < -rh * 0.5 ? TERR.grass : TERR.dirt;
                }
              }
            }
          }
        }
      }
    }

    // Smooth a bit / grass top pass
    for (let x = 1; x < w - 1; x++) {
      for (let y = 1; y < h - 1; y++) {
        const i = y * w + x;
        if (this.data[i] !== TERR.empty && this.data[i - w] === TERR.empty) {
          this.data[i] = TERR.grass;
          if (y + 1 < h && this.data[i + w] === TERR.dirt) { /* ok */ }
        }
      }
    }

    this.dirty = true;
    this.rebuildImage();
  }

  rebuildImage() {
    const img = this.octx.createImageData(this.w, this.h);
    const d = img.data;
    const dirtPalette = [
      [0, 0, 0, 0],
      [139, 90, 43, 255],   // dirt
      [76, 160, 60, 255],   // grass
      [90, 90, 100, 255],   // rock
      [140, 145, 160, 255], // metal
    ];
    // Variation
    for (let i = 0; i < this.data.length; i++) {
      const t = this.data[i];
      const p = dirtPalette[t] || dirtPalette[0];
      const o = i * 4;
      if (t === 0) {
        d[o] = d[o + 1] = d[o + 2] = d[o + 3] = 0;
      } else {
        const j = ((i * 1103515245 + 12345) >>> 0) % 40;
        const v = j - 20;
        d[o] = clamp(p[0] + v, 0, 255);
        d[o + 1] = clamp(p[1] + v * 0.8, 0, 255);
        d[o + 2] = clamp(p[2] + v * 0.5, 0, 255);
        d[o + 3] = 255;
        // dirt depth darken
        if (t === TERR.dirt) {
          const y = (i / this.w) | 0;
          const dark = (y / this.h) * 30;
          d[o] = clamp(d[o] - dark, 0, 255);
          d[o + 1] = clamp(d[o + 1] - dark, 0, 255);
          d[o + 2] = clamp(d[o + 2] - dark * 0.5, 0, 255);
        }
        // grass highlight
        if (t === TERR.grass) {
          d[o + 1] = clamp(d[o + 1] + 10, 0, 255);
        }
      }
    }
    this.octx.putImageData(img, 0, 0);
    // Soft grass edge overlay
    this.octx.save();
    this.dirty = false;
  }

  carveCircle(cx, cy, radius, spawnDebris = true) {
    const r = radius | 0;
    const r2 = r * r;
    const chunks = [];
    for (let y = -r; y <= r; y++) {
      for (let x = -r; x <= r; x++) {
        if (x * x + y * y <= r2) {
          const px = (cx + x) | 0, py = (cy + y) | 0;
          const i = this.idx(px, py);
          if (i >= 0 && this.data[i] !== TERR.empty) {
            if (spawnDebris && chance(0.08) && this.data[i] !== TERR.metal) {
              chunks.push({ x: px, y: py, type: this.data[i] });
            }
            this.data[i] = TERR.empty;
          }
        }
      }
    }
    // Jagged edge — destroy some extra perimeter
    for (let a = 0; a < TAU; a += 0.15) {
      const j = r + randi(0, 4);
      const px = (cx + Math.cos(a) * j) | 0;
      const py = (cy + Math.sin(a) * j) | 0;
      const i = this.idx(px, py);
      if (i >= 0 && this.data[i] !== TERR.metal) this.data[i] = TERR.empty;
    }
    this.dirty = true;
    // Spawn debris entities
    if (spawnDebris) {
      for (let i = 0; i < chunks.length && i < 40; i++) {
        const c = chunks[i];
        debris.push(new Debris(c.x, c.y, c.type, (c.x - cx) * 0.15, (c.y - cy) * 0.15 - rand(1, 3)));
      }
    }
  }

  // stamp image each frame if dirty
  draw(ctx, cam) {
    if (this.dirty) this.rebuildImage();
    ctx.drawImage(this.off, 0, 0);
  }

  // Find stand positions for unit placement
  findSpawnPoints(count) {
    const pts = [];
    const margin = 80;
    const attempts = 4000;
    for (let k = 0; k < attempts && pts.length < count; k++) {
      const x = randi(margin, this.w - margin);
      // scan downward for surface
      for (let y = 40; y < WATER_Y - 30; y++) {
        if (!this.solid(x, y) && this.solid(x, y + 1)) {
          // open headroom
          let clear = true;
          for (let h = 1; h <= 20; h++) if (this.solid(x, y - h)) { clear = false; break; }
          if (!clear) break;
          // not too close to others
          if (pts.some(p => Math.abs(p.x - x) < 55)) break;
          pts.push({ x, y: y - 1 });
          break;
        }
      }
    }
    // Sort left to right, interleave teams later
    pts.sort((a, b) => a.x - b.x);
    return pts;
  }
}

// ---------- Particles ----------
class ParticleSystem {
  constructor() { this.list = []; }

  add(p) { this.list.push(p); }

  burst(x, y, opts = {}) {
    const n = opts.count || 12;
    for (let i = 0; i < n; i++) {
      const a = opts.angle != null ? opts.angle + rand(-0.5, 0.5) : rand(0, TAU);
      const sp = (opts.speed || 3) * rand(0.3, 1.2);
      this.list.push({
        x, y,
        vx: Math.cos(a) * sp + (opts.vx || 0),
        vy: Math.sin(a) * sp + (opts.vy || 0),
        life: (opts.life || 0.6) * rand(0.5, 1.2),
        maxLife: opts.life || 0.6,
        r: (opts.r || 3) * rand(0.5, 1.4),
        color: opts.color || '#ffaa44',
        grav: opts.grav != null ? opts.grav : 0.12,
        fade: opts.fade !== false,
        type: opts.type || 'circle',
      });
    }
  }

  explosion(x, y, radius) {
    const n = clamp((radius * 0.8) | 0, 15, 80);
    for (let i = 0; i < n; i++) {
      const a = rand(0, TAU);
      const sp = rand(1, radius * 0.18);
      this.list.push({
        x, y,
        vx: Math.cos(a) * sp,
        vy: Math.sin(a) * sp - rand(0, 2),
        life: rand(0.4, 1.1),
        maxLife: 1,
        r: rand(2, 7),
        color: chance(0.4) ? '#ff6622' : chance(0.5) ? '#ffcc33' : '#fff0c0',
        grav: 0.08,
        fade: true,
        type: 'circle',
      });
    }
    // Smoke
    for (let i = 0; i < n * 0.4; i++) {
      this.list.push({
        x: x + rand(-radius * 0.3, radius * 0.3),
        y: y + rand(-radius * 0.3, radius * 0.3),
        vx: rand(-0.5, 0.5),
        vy: rand(-1.5, -0.3),
        life: rand(0.8, 1.8),
        maxLife: 1.5,
        r: rand(6, 16),
        color: `rgba(60,60,70,0.7)`,
        grav: -0.02,
        fade: true,
        type: 'smoke',
      });
    }
    // Dirt debris colors
    for (let i = 0; i < n * 0.5; i++) {
      this.list.push({
        x, y,
        vx: rand(-radius * 0.12, radius * 0.12),
        vy: rand(-radius * 0.15, -1),
        life: rand(0.5, 1.2),
        maxLife: 1,
        r: rand(2, 5),
        color: chance(0.5) ? '#8B5A2B' : '#5a7a3a',
        grav: 0.2,
        fade: true,
        type: 'circle',
      });
    }
  }

  dust(x, y, dir = 0) {
    this.burst(x, y, { count: 5, color: '#c2a882', speed: 1.2, life: 0.35, r: 2, angle: -PI / 2 + dir * 0.5 });
  }

  update(dt) {
    for (let i = this.list.length - 1; i >= 0; i--) {
      const p = this.list[i];
      p.vy += p.grav;
      p.x += p.vx;
      p.y += p.vy;
      p.vx *= 0.99;
      p.life -= dt;
      if (p.life <= 0) this.list.splice(i, 1);
    }
  }

  draw(ctx) {
    for (const p of this.list) {
      const a = p.fade ? clamp(p.life / (p.maxLife * 0.5), 0, 1) : 1;
      ctx.globalAlpha = a;
      if (p.type === 'smoke') {
        ctx.fillStyle = p.color;
        ctx.beginPath();
        ctx.arc(p.x, p.y, p.r * (1.5 - a * 0.5), 0, TAU);
        ctx.fill();
      } else {
        ctx.fillStyle = p.color;
        ctx.beginPath();
        ctx.arc(p.x, p.y, p.r * a, 0, TAU);
        ctx.fill();
      }
    }
    ctx.globalAlpha = 1;
  }
}

const particles = new ParticleSystem();

// ---------- Debris chunks ----------
class Debris {
  constructor(x, y, type, vx, vy) {
    this.x = x; this.y = y;
    this.vx = vx + rand(-1, 1);
    this.vy = vy + rand(-1, 1);
    this.type = type;
    this.r = rand(2, 5);
    this.rot = rand(0, TAU);
    this.vr = rand(-0.3, 0.3);
    this.life = rand(1.5, 3.5);
    this.dead = false;
  }
  update(dt) {
    this.vy += GRAVITY * 0.9;
    this.vx *= 0.995;
    this.x += this.vx;
    this.y += this.vy;
    this.rot += this.vr;
    this.life -= dt;
    // collide terrain
    if (terrain.solid(this.x, this.y + this.r)) {
      this.vy *= -0.3;
      this.vx *= 0.6;
      this.y -= 2;
      if (Math.abs(this.vy) < 0.5) this.vy = 0;
    }
    if (this.y > WATER_Y) {
      this.vy *= 0.5;
      this.vx *= 0.9;
      if (chance(0.1)) particles.burst(this.x, WATER_Y, { count: 1, color: '#8cf', speed: 0.8, life: 0.3 });
    }
    if (this.life <= 0 || this.y > WORLD_H + 20) this.dead = true;
  }
  draw(ctx) {
    ctx.save();
    ctx.translate(this.x, this.y);
    ctx.rotate(this.rot);
    ctx.globalAlpha = clamp(this.life, 0, 1);
    ctx.fillStyle = this.type === TERR.grass ? '#4ca03c' : this.type === TERR.rock ? '#666' : '#8B5A2B';
    ctx.fillRect(-this.r, -this.r * 0.7, this.r * 2, this.r * 1.4);
    ctx.restore();
  }
}

// ---------- Projectile ----------
class Projectile {
  constructor(o) {
    Object.assign(this, {
      x: 0, y: 0, vx: 0, vy: 0, r: 4,
      team: 0, owner: null, color: '#fff',
      trail: false, explodeRadius: 40, damage: 30,
      wind: true, gravity: 0.18, bounce: 0, fuse: 0,
      sticky: false, lifetime: 0, shrink: false,
      sprite: null, glow: false, freeze: false,
      dead: false, age: 0, prevX: 0, prevY: 0,
    }, o);
    this.prevX = this.x; this.prevY = this.y;
    this.trailPts = [];
  }

  update(dt) {
    if (this.dead) return;
    this.age += dt;
    this.prevX = this.x; this.prevY = this.y;

    if (this.wind) this.vx += wind * 0.012;
    this.vy += this.gravity;
    this.x += this.vx;
    this.y += this.vy;

    if (this.trail) {
      this.trailPts.push({ x: this.x, y: this.y, a: 1 });
      if (this.trailPts.length > 18) this.trailPts.shift();
      for (const t of this.trailPts) t.a *= 0.9;
      if (chance(0.4)) {
        particles.add({
          x: this.x, y: this.y, vx: rand(-0.3, 0.3), vy: rand(-0.3, 0.3),
          life: 0.3, maxLife: 0.3, r: 2, color: this.color, grav: 0, fade: true, type: 'circle'
        });
      }
    }

    // Fuse / lifetime
    if (this.fuse > 0) {
      this.fuse -= dt;
      if (this.fuse <= 0) { this.explode(); return; }
      // tick sound near end
      if (this.fuse < 1.5 && Math.floor(this.fuse * 6) !== Math.floor((this.fuse + dt) * 6)) {
        AudioSys.beep();
      }
    }
    if (this.lifetime > 0) {
      this.lifetime -= dt;
      if (this.lifetime <= 0) { this.explode(); return; }
    }

    // Out of bounds
    if (this.x < -50 || this.x > WORLD_W + 50 || this.y > WORLD_H + 50) {
      this.dead = true; return;
    }

    // Water splash
    if (this.y > WATER_Y && this.prevY <= WATER_Y) {
      particles.burst(this.x, WATER_Y, { count: 10, color: '#8cf', speed: 2, life: 0.5, vy: -2 });
      AudioSys.splash();
      // most projectiles slow/sink and explode if fuse else live
      this.vx *= 0.4; this.vy *= 0.3;
    }

    // Terrain collision — sub-step
    const steps = Math.ceil(Math.hypot(this.vx, this.vy) / 3);
    for (let s = 1; s <= steps; s++) {
      const t = s / steps;
      const cx = lerp(this.prevX, this.x, t);
      const cy = lerp(this.prevY, this.y, t);
      if (terrain.solid(cx, cy)) {
        if (this.bounce > 0 && this.fuse > 0) {
          // bounce
          this.x = this.prevX; this.y = this.prevY;
          // simple normal estimate
          let nx = 0, ny = 0;
          for (let a = 0; a < TAU; a += PI / 4) {
            if (terrain.solid(cx + Math.cos(a) * 4, cy + Math.sin(a) * 4)) {
              nx -= Math.cos(a); ny -= Math.sin(a);
            }
          }
          const nl = Math.hypot(nx, ny) || 1;
          nx /= nl; ny /= nl;
          const dot = this.vx * nx + this.vy * ny;
          this.vx = (this.vx - 2 * dot * nx) * this.bounce;
          this.vy = (this.vy - 2 * dot * ny) * this.bounce;
          this.x += nx * 3; this.y += ny * 3;
          AudioSys.land();
          particles.dust(cx, cy);
          return;
        } else {
          this.x = cx; this.y = cy;
          this.explode();
          return;
        }
      }
    }

    // Hit units
    for (const w of allUnits()) {
      if (w.dead || w === this.owner) continue;
      if (dist(this.x, this.y, w.x, w.y - 6) < this.r + 10) {
        this.explode();
        return;
      }
    }
  }

  explode() {
    if (this.dead) return;
    this.dead = true;
    const R = this.explodeRadius;
    // Impact freeze for large
    if (R >= 70) {
      impactFreeze = 0.12;
    }
    shake = Math.min(22, R * 0.22);
    particles.explosion(this.x, this.y, R);
    terrain.carveCircle(this.x, this.y, R * 0.85, true);
    AudioSys.boom(R / 50);

    // Damage units
    for (const w of allUnits()) {
      if (w.dead) continue;
      const d = dist(this.x, this.y, w.x, w.y - 6);
      if (d < R + 10) {
        const falloff = 1 - d / (R + 10);
        const dmg = this.damage * falloff * falloff;
        const force = falloff * (R * 0.18);
        const ang = Math.atan2(w.y - this.y, w.x - this.x);
        w.vx += Math.cos(ang) * force;
        w.vy += Math.sin(ang) * force - 2 * falloff;
        w.grounded = false;
        if (this.freeze) {
          w.frozen = 3.5;
          particles.burst(w.x, w.y, { count: 15, color: '#aef', speed: 2, life: 0.6 });
          AudioSys.ice();
        }
        dmgUnit(w, dmg, this.owner);
      }
    }

    // Secondary debris hydro
    if (this.y > WATER_Y - 20) {
      particles.burst(this.x, Math.min(this.y, WATER_Y), { count: 20, color: '#6bf', speed: 4, life: 0.7, vy: -3 });
    }
  }

  draw(ctx) {
    if (this.dead) return;
    // trail
    if (this.trailPts.length > 1) {
      ctx.beginPath();
      for (let i = 0; i < this.trailPts.length; i++) {
        const t = this.trailPts[i];
        if (i === 0) ctx.moveTo(t.x, t.y); else ctx.lineTo(t.x, t.y);
      }
      ctx.strokeStyle = this.color;
      ctx.globalAlpha = 0.35;
      ctx.lineWidth = 2;
      ctx.stroke();
      ctx.globalAlpha = 1;
    }

    ctx.save();
    ctx.translate(this.x, this.y);
    if (this.glow) {
      ctx.shadowColor = this.color;
      ctx.shadowBlur = 16;
    }
    const ang = Math.atan2(this.vy, this.vx);

    if (this.sprite === 'grenade') {
      ctx.rotate(this.age * 8);
      ctx.fillStyle = '#2d5a27';
      ctx.beginPath(); ctx.arc(0, 0, 6, 0, TAU); ctx.fill();
      ctx.fillStyle = '#444';
      ctx.fillRect(-2, -9, 4, 4);
      // spark
      if (this.fuse < 99 && Math.sin(this.age * 30) > 0) {
        ctx.fillStyle = '#ff0';
        ctx.fillRect(-1, -12, 2, 3);
      }
    } else if (this.sprite === 'holy') {
      ctx.rotate(this.age * 5);
      ctx.fillStyle = '#ffe566';
      ctx.beginPath(); ctx.arc(0, 0, 8, 0, TAU); ctx.fill();
      ctx.fillStyle = '#fff';
      ctx.font = 'bold 10px sans-serif';
      ctx.textAlign = 'center';
      ctx.fillText('†', 0, 3);
    } else if (this.sprite === 'dynamite') {
      ctx.fillStyle = '#c22';
      ctx.fillRect(-4, -10, 8, 18);
      ctx.fillStyle = '#222';
      ctx.fillRect(-5, -11, 10, 3);
      // fuse spark
      ctx.fillStyle = Math.sin(this.age * 40) > 0 ? '#ff0' : '#f80';
      ctx.beginPath(); ctx.arc(0, -14, 2, 0, TAU); ctx.fill();
    } else if (this.sprite === 'ice') {
      ctx.rotate(this.age * 6);
      ctx.fillStyle = '#8df';
      ctx.beginPath();
      for (let i = 0; i < 6; i++) {
        const a = i / 6 * TAU;
        const rr = i % 2 === 0 ? 8 : 4;
        if (i === 0) ctx.moveTo(Math.cos(a) * rr, Math.sin(a) * rr);
        else ctx.lineTo(Math.cos(a) * rr, Math.sin(a) * rr);
      }
      ctx.closePath(); ctx.fill();
    } else {
      // rocket / generic
      ctx.rotate(ang);
      ctx.fillStyle = this.color;
      ctx.beginPath();
      ctx.moveTo(8, 0); ctx.lineTo(-6, -4); ctx.lineTo(-6, 4);
      ctx.closePath(); ctx.fill();
      if (this.trail) {
        ctx.fillStyle = '#ff8800';
        ctx.beginPath(); ctx.arc(-7, 0, 3 + Math.random() * 2, 0, TAU); ctx.fill();
      }
    }
    ctx.restore();
  }
}

// ---------- Sheep special ----------
class Sheep extends Projectile {
  constructor(o) {
    super({
      r: 10, color: '#f5f5f5', explodeRadius: 70, damage: 60,
      wind: false, gravity: 0.22, bounce: 0.4, fuse: 5.5,
      trail: false, sprite: 'sheep', ...o
    });
    this.dir = Math.sign(this.vx) || 1;
    this.walkTimer = 0;
  }
  update(dt) {
    if (this.dead) return;
    this.age += dt;
    this.prevX = this.x; this.prevY = this.y;
    this.fuse -= dt;
    if (this.fuse <= 0) { this.explode(); return; }

    this.vy += this.gravity;
    // walk
    if (terrain.solid(this.x, this.y + 11)) {
      this.vy = Math.min(0, this.vy);
      this.vx = this.dir * 2.8;
      // hop tiny ledges
      if (terrain.solid(this.x + this.dir * 10, this.y) && !terrain.solid(this.x + this.dir * 10, this.y - 14)) {
        this.vy = -4;
      }
      // edge turn
      if (!terrain.solid(this.x + this.dir * 12, this.y + 14) || terrain.solid(this.x + this.dir * 10, this.y - 2)) {
        if (chance(0.02)) this.dir *= -1;
      }
      // random jump SEMI often like worms sheep
      this.walkTimer += dt;
      if (this.walkTimer > 0.8 && chance(0.03)) {
        this.vy = -5.5;
        this.walkTimer = 0;
      }
    }
    this.x += this.vx;
    this.y += this.vy;

    if (this.y > WATER_Y) {
      this.explode();
      return;
    }
    if (this.x < 10) this.dir = 1;
    if (this.x > WORLD_W - 10) this.dir = -1;

    // baa occasionally
    if (chance(0.005)) {
      AudioSys.tone(350, 0.08, 'sine', 0.1, 80);
      AudioSys.tone(280, 0.1, 'sine', 0.08, -40, 0.08);
    }
  }
  draw(ctx) {
    if (this.dead) return;
    ctx.save();
    ctx.translate(this.x, this.y);
    ctx.scale(this.dir, 1);
    // body
    ctx.fillStyle = '#f4f4f4';
    ctx.beginPath(); ctx.ellipse(0, 0, 12, 9, 0, 0, TAU); ctx.fill();
    // head
    ctx.fillStyle = '#222';
    ctx.beginPath(); ctx.arc(10, -2, 5, 0, TAU); ctx.fill();
    // legs
    ctx.fillStyle = '#222';
    ctx.fillRect(-6, 6, 3, 5);
    ctx.fillRect(2, 6, 3, 5);
    // fuse indicator
    if (this.fuse < 2) {
      ctx.fillStyle = `rgba(255,0,0,${0.5 + Math.sin(this.age * 20) * 0.5})`;
      ctx.beginPath(); ctx.arc(0, -12, 4, 0, TAU); ctx.fill();
    }
    ctx.restore();
  }
}

// ---------- Unit (Worm) ----------
class Unit {
  constructor(x, y, team, index) {
    this.x = x; this.y = y;
    this.vx = 0; this.vy = 0;
    this.team = team;
    this.index = index;
    this.hp = UNIT_HP;
    this.maxHp = UNIT_HP;
    this.dead = false;
    this.facing = 1;
    this.grounded = false;
    this.anim = 0;
    this.name = WORM_NAMES[team % WORM_NAMES.length][index % WORM_NAMES[0].length];
    this.blink = 0;
    this.lookAngle = 0;
    this.fallSpeed = 0;
    this.rope = null;
    this.frozen = 0;
    this.jetFuel = 0;
    this.usingJet = false;
    this.drownTimer = 0;
    this.prevGrounded = false;
    this.bob = Math.random() * TAU;
    this.floatVY = 0; // water float
  }

  update(dt, isActive, canControl) {
    if (this.dead) return;
    this.anim += dt;
    this.bob += dt * 3;
    this.blink -= dt;
    if (this.blink < -2 && chance(0.01)) this.blink = 0.15;
    if (this.frozen > 0) {
      this.frozen -= dt;
      this.vx *= 0.9;
      // still apply gravity lightly
      this.vy += GRAVITY * 0.5;
      this.movePhysics(dt);
      return;
    }

    // Jetpack
    if (this.usingJet && this.jetFuel > 0 && isActive) {
      this.jetFuel -= dt;
      this.vy += -0.55;
      this.grounded = false;
      if (keys['KeyA'] || keys['ArrowLeft']) this.vx -= 0.15;
      if (keys['KeyD'] || keys['ArrowRight']) this.vx += 0.15;
      particles.burst(this.x - this.facing * 2, this.y + 8, {
        count: 2, color: '#ff8800', speed: 1.5, life: 0.25, angle: PI / 2, r: 2
      });
      if (chance(0.3)) AudioSys.tone(80 + Math.random() * 40, 0.04, 'sawtooth', 0.04);
      if (this.jetFuel <= 0) this.usingJet = false;
    }

    // Rope physics
    if (this.rope && this.rope.active) {
      this.grounded = false;
      const dx = this.x - this.rope.x;
      const dy = this.y - this.rope.y;
      const distR = Math.hypot(dx, dy) || 1;
      // constraint
      if (distR > this.rope.len) {
        const nx = dx / distR, ny = dy / distR;
        this.x = this.rope.x + nx * this.rope.len;
        this.y = this.rope.y + ny * this.rope.len;
        // dampen radial
        const vrad = this.vx * nx + this.vy * ny;
        this.vx -= nx * vrad;
        this.vy -= ny * vrad;
      }
      this.vy += GRAVITY * 0.85;
      // swing control
      if (canControl) {
        if (keys['KeyA'] || keys['ArrowLeft']) this.vx -= 0.2;
        if (keys['KeyD'] || keys['ArrowRight']) this.vx += 0.2;
        if (keys['KeyW'] || keys['ArrowUp']) this.rope.len = Math.max(30, this.rope.len - 2.5);
        if (keys['KeyS'] || keys['ArrowDown']) this.rope.len = Math.min(260, this.rope.len + 2.5);
        if (keys['Space']) {
          this.rope = null;
          this.vy -= 2;
          AudioSys.jump();
        }
      }
      this.vx *= 0.995;
      this.x += this.vx; this.y += this.vy;
      // collide
      this.resolveCollision();
      if (this.grounded) this.rope = null;
      return;
    }

    // Water
    if (this.y > WATER_Y - 4) {
      this.grounded = false;
      // buoyancy
      const submersion = clamp((this.y - (WATER_Y - 8)) / 20, 0, 1);
      this.vy += GRAVITY * 0.3;
      this.vy -= submersion * 0.45;
      this.vx *= 0.92;
      this.vy *= 0.94;
      // wind less
      if (canControl && isActive) {
        if (keys['KeyA'] || keys['ArrowLeft']) { this.vx -= 0.08; this.facing = -1; }
        if (keys['KeyD'] || keys['ArrowRight']) { this.vx += 0.08; this.facing = 1; }
        if ((keys['KeyW'] || keys['ArrowUp'] || keys['Space']) && submersion > 0.3) this.vy -= 0.25;
      }
      this.x += this.vx; this.y += this.vy;
      if (this.y > WATER_Y + 8) {
        this.drownTimer += dt;
        if (chance(0.15)) particles.burst(this.x, this.y - 6, { count: 1, color: '#cdf', speed: 0.6, life: 0.6, vy: -1 });
        if (this.drownTimer > 1.2) {
          dmgUnit(this, 35 * dt, null); // drowning damage
        }
      }
      // fully under map
      if (this.y > WORLD_H + 30) {
        this.hp = 0;
        killUnit(this);
      }
      return;
    } else {
      this.drownTimer = 0;
    }

    // Controls
    if (canControl && isActive && !this.usingJet) {
      let moving = false;
      if (keys['KeyA'] || keys['ArrowLeft']) {
        this.vx = -MOVE_SPEED; this.facing = -1; moving = true;
      } else if (keys['KeyD'] || keys['ArrowRight']) {
        this.vx = MOVE_SPEED; this.facing = 1; moving = true;
      }
      if (moving && this.grounded) {
        if (chance(0.15)) particles.dust(this.x, this.y + 10, this.facing);
      }
      // step-up stairs
      if (moving && this.grounded) {
        const ahead = this.x + this.facing * 6;
        if (terrain.solid(ahead, this.y) && !terrain.solid(ahead, this.y - 12) && !terrain.solid(this.x, this.y - 12)) {
          this.y -= 8;
        }
      }
      if ((keys['KeyW'] || keys['ArrowUp'] || keys['Space']) && this.grounded) {
        this.vy = JUMP_V;
        this.grounded = false;
        AudioSys.jump();
        particles.dust(this.x, this.y + 10);
      }
    }

    // wind nudge when midair
    if (!this.grounded) this.vx += wind * 0.004;

    this.movePhysics(dt);
  }

  movePhysics(dt) {
    this.prevGrounded = this.grounded;
    this.vy += GRAVITY;
    if (this.grounded) this.vx *= FRICTION;
    else this.vx *= AIR_FRICTION;

    // integrate with substeps
    const steps = 3;
    for (let s = 0; s < steps; s++) {
      this.x += this.vx / steps;
      this.y += this.vy / steps;
      this.resolveCollision();
    }

    // fall damage
    if (this.grounded && !this.prevGrounded) {
      if (this.fallSpeed > FALL_DMG_THRESH) {
        const dmg = (this.fallSpeed - FALL_DMG_THRESH) * 4.5;
        dmgUnit(this, dmg, null);
        particles.burst(this.x, this.y + 8, { count: 8, color: '#c2a882', speed: 2 });
        shake = Math.min(shake + dmg * 0.1, 10);
      }
      AudioSys.land();
      this.fallSpeed = 0;
    }
    if (!this.grounded) this.fallSpeed = Math.max(this.fallSpeed, this.vy);
    else this.fallSpeed = 0;

    // bounds
    this.x = clamp(this.x, 8, WORLD_W - 8);
  }

  resolveCollision() {
    const hw = 7, hh = 10;
    this.grounded = false;
    // bottom
    if (terrain.solid(this.x, this.y + hh) || terrain.solid(this.x - 4, this.y + hh) || terrain.solid(this.x + 4, this.y + hh)) {
      // push up
      let push = 0;
      while (push < 20 && (terrain.solid(this.x, this.y + hh - push) || terrain.solid(this.x - 4, this.y + hh - push))) push++;
      this.y -= push;
      if (this.vy > 0) this.vy = 0;
      this.grounded = true;
    }
    // top
    if (terrain.solid(this.x, this.y - hh) || terrain.solid(this.x - 3, this.y - hh) || terrain.solid(this.x + 3, this.y - hh)) {
      this.y += 3;
      if (this.vy < 0) this.vy = 0;
    }
    // left / right
    if (terrain.solid(this.x - hw, this.y) || terrain.solid(this.x - hw, this.y - 5) || terrain.solid(this.x - hw, this.y + 5)) {
      this.x += 2;
      this.vx = Math.max(0, this.vx);
    }
    if (terrain.solid(this.x + hw, this.y) || terrain.solid(this.x + hw, this.y - 5) || terrain.solid(this.x + hw, this.y + 5)) {
      this.x -= 2;
      this.vx = Math.min(0, this.vx);
    }
  }

  draw(ctx) {
    if (this.dead) return;
    const c = TEAM_COLORS[this.team];
    ctx.save();
    ctx.translate(this.x, this.y);

    // shadow
    ctx.fillStyle = 'rgba(0,0,0,0.25)';
    ctx.beginPath();
    ctx.ellipse(0, 11, 10, 3, 0, 0, TAU);
    ctx.fill();

    // rope line
    // drawn separately

    // frozen tint ice
    if (this.frozen > 0) {
      ctx.fillStyle = 'rgba(150,220,255,0.35)';
      ctx.beginPath();
      ctx.arc(0, -2, 16, 0, TAU);
      ctx.fill();
    }

    const walkBob = this.grounded && Math.abs(this.vx) > 0.3 ? Math.sin(this.anim * 18) * 1.5 : 0;
    ctx.translate(0, walkBob);

    // body (worm sausage)
    ctx.fillStyle = c.primary;
    ctx.beginPath();
    ctx.ellipse(0, 0, 9, 11, 0, 0, TAU);
    ctx.fill();
    // belly
    ctx.fillStyle = c.skin;
    ctx.beginPath();
    ctx.ellipse(0, 3, 6, 6, 0, 0, TAU);
    ctx.fill();

    // helmet / bandana
    ctx.fillStyle = c.dark;
    ctx.beginPath();
    ctx.ellipse(0, -8, 9, 6, 0, PI, TAU);
    ctx.fill();
    ctx.fillStyle = c.light;
    ctx.fillRect(-9, -9, 18, 3);

    // eyes
    const eyeX = this.facing * 3;
    if (this.blink > 0) {
      ctx.strokeStyle = '#222';
      ctx.lineWidth = 1.5;
      ctx.beginPath();
      ctx.moveTo(eyeX - 3, -4); ctx.lineTo(eyeX + 3, -4);
      ctx.stroke();
    } else {
      ctx.fillStyle = '#fff';
      ctx.beginPath(); ctx.ellipse(eyeX - 2.5, -4, 3.2, 3.5, 0, 0, TAU); ctx.fill();
      ctx.beginPath(); ctx.ellipse(eyeX + 3, -4, 3.2, 3.5, 0, 0, TAU); ctx.fill();
      // pupils look toward aim
      let px = clamp(Math.cos(this.lookAngle) * 1.5, -1.5, 1.5);
      let py = clamp(Math.sin(this.lookAngle) * 1.5, -1.5, 1.5);
      ctx.fillStyle = '#111';
      ctx.beginPath(); ctx.arc(eyeX - 2.5 + px, -4 + py, 1.5, 0, TAU); ctx.fill();
      ctx.beginPath(); ctx.arc(eyeX + 3 + px, -4 + py, 1.5, 0, TAU); ctx.fill();
    }

    // smile
    ctx.strokeStyle = c.dark;
    ctx.lineWidth = 1.2;
    ctx.beginPath();
    ctx.arc(this.facing * 1, 2, 3, 0.2, PI - 0.2);
    ctx.stroke();

    // jetpack
    if (this.usingJet || this.jetFuel > 0) {
      ctx.fillStyle = '#555';
      ctx.fillRect(-12, -4, 5, 10);
      ctx.fillRect(7, -4, 5, 10);
    }

    // name + hp
    ctx.restore();
    ctx.save();
    ctx.translate(this.x, this.y);
    ctx.font = 'bold 11px Fredoka, sans-serif';
    ctx.textAlign = 'center';
    ctx.fillStyle = 'rgba(0,0,0,0.5)';
    ctx.fillText(this.name, 0.5, -24.5);
    ctx.fillStyle = c.light;
    ctx.fillText(this.name, 0, -25);

    // HP bar
    const bw = 28, bh = 4;
    ctx.fillStyle = 'rgba(0,0,0,0.5)';
    ctx.fillRect(-bw / 2 - 1, -20, bw + 2, bh + 2);
    const hpr = this.hp / this.maxHp;
    ctx.fillStyle = hpr > 0.5 ? '#4ade80' : hpr > 0.25 ? '#facc15' : '#ef4444';
    ctx.fillRect(-bw / 2, -19, bw * hpr, bh);

    // Active indicator
    if (this === activeUnit && phase === 'playing') {
      ctx.fillStyle = varAccent();
      ctx.beginPath();
      ctx.moveTo(0, -32 + Math.sin(performance.now() / 200) * 3);
      ctx.lineTo(-5, -40 + Math.sin(performance.now() / 200) * 3);
      ctx.lineTo(5, -40 + Math.sin(performance.now() / 200) * 3);
      ctx.fill();
    }
    ctx.restore();
  }
}

function varAccent() { return '#ffd36b'; }

const WORM_NAMES = [
  ['Bazooka Bob', 'Sarge', 'Noodle', 'Wiggly'],
  ['Captain uke', 'Splash', 'Gyro', 'Pip'],
  ['Leaf', 'Moss', 'Clover', 'Bean'],
  ['Sunny', 'Clank', 'Nugget', 'Zap'],
];

// ---------- Global game state ----------
let terrain = null;
let teams = []; // arrays of Unit
let projectiles = [];
let debris = [];
let teamCount = 2;
let humanCount = 1; // special: -1 all human, 0 all cpu
let terrainShape = 'island';
let wind = 0;
let activeTeam = 0;
let activeIndex = 0;
let activeUnit = null;
let turnTime = TURN_TIME;
let phase = 'menu'; // menu, playing, waiting, end
let selectedWeapon = 0;
let weaponAmmo = {};
let charging = false;
let chargePower = 0;
let shake = 0;
let impactFreeze = 0;
let cam = { x: WORLD_W / 2, y: WORLD_H / 2, zoom: 1, tx: 0, ty: 0, tzoom: 1 };
let aimAngle = 0;
let turnLocked = false; // after shooting, wait for projectiles
let aiState = null;
let waterTime = 0;
let isCPUTurn = false;
let placeMode = false;
let gameOver = false;
let waitTimer = 0;

function allUnits() {
  return teams.flat();
}

function livingTeams() {
  return teams.map((t, i) => ({ i, alive: t.some(u => !u.dead && u.hp > 0) })).filter(t => t.alive);
}

// ---------- Damage / death ----------
function dmgUnit(u, amount, source) {
  if (u.dead || amount <= 0) return;
  amount = Math.round(amount);
  if (amount <= 0) return;
  u.hp -= amount;
  floatText(u.x, u.y - 20, '-' + amount, false);
  AudioSys.hurt();
  particles.burst(u.x, u.y - 4, { count: 6, color: TEAM_COLORS[u.team].primary, speed: 2.5, life: 0.4 });
  if (u.hp <= 0) {
    u.hp = 0;
    killUnit(u);
  }
}

function killUnit(u) {
  if (u.dead) return;
  u.dead = true;
  u.rope = null;
  AudioSys.death();
  particles.explosion(u.x, u.y, 30);
  // tombstone particle
  floatText(u.x, u.y - 30, '💀 ' + u.name, false);
  checkWin();
}

function floatText(x, y, text, heal) {
  // convert world to screen
  const layer = document.getElementById('floatLayer');
  const el = document.createElement('div');
  el.className = 'float-dmg' + (heal ? ' heal' : '');
  el.textContent = text;
  const sx = (x - cam.x) * cam.zoom + WORLD_W / 2;
  const sy = (y - cam.y) * cam.zoom + WORLD_H / 2;
  el.style.left = (sx * SCALE) + 'px';
  el.style.top = (sy * SCALE) + 'px';
  layer.appendChild(el);
  setTimeout(() => el.remove(), 1200);
}

function checkWin() {
  const living = livingTeams();
  if (living.length <= 1 && phase === 'playing') {
    phase = 'end';
    gameOver = true;
    const winner = living[0];
    const end = document.getElementById('endScreen');
    end.classList.remove('hidden');
    const title = document.getElementById('endTitle');
    const desc = document.getElementById('endDesc');
    if (!winner) {
      title.textContent = 'DRAW!';
      desc.textContent = 'No worms survived the mayhem…';
    } else {
      const c = TEAM_COLORS[winner.i];
      title.textContent = c.name.toUpperCase() + ' WINS!';
      desc.textContent = `Team ${c.name} remains victorious!`;
      title.style.color = c.primary;
    }
    AudioSys.win();
    document.getElementById('hud').classList.remove('visible');
  }
}

// ---------- Camera ----------
function updateCamera(dt) {
  if (activeUnit && !activeUnit.dead) {
    cam.tx = activeUnit.x;
    cam.ty = activeUnit.y - 40;
    // zoom out when projectile flying
    if (projectiles.length > 0 || turnLocked) {
      cam.tzoom = 0.75;
      // follow last projectile if any
      if (projectiles.length) {
        const p = projectiles[projectiles.length - 1];
        cam.tx = p.x;
        cam.ty = p.y;
      }
    } else if (charging) {
      cam.tzoom = 0.95;
    } else {
      cam.tzoom = 1.05;
    }
  }
  // follow interesting event
  cam.x = lerp(cam.x, cam.tx, 1 - Math.pow(0.001, dt));
  cam.y = lerp(cam.y, cam.ty, 1 - Math.pow(0.001, dt));
  cam.zoom = lerp(cam.zoom, cam.tzoom, 1 - Math.pow(0.0001, dt));
  // shake
  if (shake > 0) shake = Math.max(0, shake - dt * 30);
}

// ---------- Trajectory preview ----------
function predictTrajectory(x, y, vx, vy, gravity, useWind, steps = 60) {
  const pts = [];
  let px = x, py = y, pvx = vx, pvy = vy;
  for (let i = 0; i < steps; i++) {
    if (useWind) pvx += wind * 0.012;
    pvy += gravity;
    px += pvx; py += pvy;
    pts.push({ x: px, y: py });
    if (terrain.solid(px, py) || py > WATER_Y + 20) break;
  }
  return pts;
}

// ---------- Turns ----------
function beginTurn() {
  turnLocked = false;
  placeMode = false;
  charging = false;
  chargePower = 0;
  turnTime = TURN_TIME;
  waitTimer = 0;

  // next living unit — round robin teams
  let tries = 0;
  do {
    // find next living on active team
    const team = teams[activeTeam];
    let found = false;
    for (let k = 0; k < team.length; k++) {
      activeIndex = (activeIndex + 1) % team.length;
      if (!team[activeIndex].dead && team[activeIndex].hp > 0) {
        found = true;
        break;
      }
    }
    if (!found) {
      // advance team
      activeTeam = (activeTeam + 1) % teams.length;
      activeIndex = -1;
    } else break;
    tries++;
  } while (tries < 20);

  // If we need new team starting from first valid
  if (!teams[activeTeam].some(u => !u.dead)) {
    for (let t = 0; t < teams.length; t++) {
      if (teams[t].some(u => !u.dead)) { activeTeam = t; break; }
    }
    activeIndex = teams[activeTeam].findIndex(u => !u.dead);
  }

  activeUnit = teams[activeTeam][activeIndex];
  if (!activeUnit || activeUnit.dead) {
    checkWin();
    return;
  }

  // Randomize wind each turn
  wind = rand(-1, 1) * rand(0.5, 5);
  updateWindUI();

  // Reset jet/rope state residual
  activeUnit.usingJet = false;

  isCPUTurn = !isHumanTeam(activeTeam);
  aiState = isCPUTurn ? { stage: 'think', t: 0.6 + Math.random() * 0.5, plan: null } : null;

  // Banner
  showBanner(`Team ${TEAM_COLORS[activeTeam].name}\n${activeUnit.name}'s Turn` + (isCPUTurn ? ' (CPU)' : ''));
  updateTeamStrip();
  updateWeaponBar();
  selectedWeapon = Math.min(selectedWeapon, WEAPONS.length - 1);
  // pick default weapon with ammo
  ensureWeaponAmmo();
}

function isHumanTeam(t) {
  if (humanCount === 0) return false;
  if (humanCount === -1) return true;
  return t < humanCount;
}

function endTurn() {
  if (phase !== 'playing') return;
  // clear residual controls
  charging = false;
  // Wait for world to settle
  turnLocked = true;
  waitTimer = 0.6;
}

function afterActionSettle(dt) {
  waitTimer += dt;
  // wait until projectiles done and debris slow + units nearly still
  const busy = projectiles.length > 0;
  const unitsFlying = allUnits().some(u => !u.dead && !u.grounded && Math.abs(u.vy) > 1.5 && u.y < WATER_Y);
  if (!busy && !unitsFlying && waitTimer > 1.2) {
    // next team
    activeTeam = (activeTeam + 1) % teams.length;
    activeIndex = -1;
    beginTurn();
  }
  // safety timeout
  if (waitTimer > 12) {
    activeTeam = (activeTeam + 1) % teams.length;
    activeIndex = -1;
    beginTurn();
  }
}

function showBanner(text) {
  const b = document.getElementById('turnBanner');
  b.innerHTML = text.replace('\n', '<br>');
  b.style.borderColor = TEAM_COLORS[activeTeam].primary;
  b.classList.remove('hide');
  b.classList.add('show');
  setTimeout(() => { b.classList.add('hide'); b.classList.remove('show'); }, 1800);
}

// ---------- UI updates ----------
function updateWindUI() {
  const el = document.getElementById('windText');
  const arrow = document.getElementById('windArrow');
  const strength = Math.abs(wind).toFixed(1);
  el.textContent = `Wind ${strength}`;
  arrow.textContent = wind > 0.15 ? '→' : wind < -0.15 ? '←' : '≈';
  arrow.style.transform = `scaleX(${clamp(Math.abs(wind) / 2, 0.5, 2.5)})`;
  arrow.style.color = Math.abs(wind) > 3 ? '#ff8844' : '#8cf';
}

function updateTeamStrip() {
  const strip = document.getElementById('teamStrip');
  strip.innerHTML = '';
  teams.forEach((team, i) => {
    const alive = team.filter(u => !u.dead).length;
    const totalHp = team.reduce((s, u) => s + Math.max(0, u.hp), 0);
    const badge = document.createElement('div');
    badge.className = 'team-badge' + (i === activeTeam ? ' active' : '');
    badge.innerHTML = `<span class="team-dot" style="background:${TEAM_COLORS[i].primary};color:${TEAM_COLORS[i].primary}"></span>
      ${TEAM_COLORS[i].name} · ${alive}/${team.length} · ${totalHp}HP
      ${isHumanTeam(i) ? '' : ' <span style="opacity:0.5">CPU</span>'}`;
    strip.appendChild(badge);
  });
}

function ensureWeaponAmmo() {
  WEAPONS.forEach(w => {
    if (weaponAmmo[w.id] == null) weaponAmmo[w.id] = w.ammo;
  });
}

function updateWeaponBar() {
  ensureWeaponAmmo();
  const bar = document.getElementById('weaponBar');
  bar.innerHTML = '';
  WEAPONS.forEach((w, i) => {
    const ammo = weaponAmmo[w.id];
    if (ammo !== Infinity && ammo <= 0) return;
    const btn = document.createElement('button');
    btn.className = 'wep-btn' + (i === selectedWeapon ? ' selected' : '');
    btn.innerHTML = `${w.icon}<span class="key">${w.key}</span>${ammo !== Infinity ? `<span class="ammo">${ammo}</span>` : ''}`;
    btn.title = w.name + ' — ' + w.desc;
    btn.addEventListener('mouseenter', () => {
      AudioSys.uiHover();
      const tip = document.getElementById('tooltip');
      tip.textContent = `${w.name}: ${w.desc}`;
      tip.classList.add('show');
    });
    btn.addEventListener('mouseleave', () => document.getElementById('tooltip').classList.remove('show'));
    btn.addEventListener('click', () => {
      if (isCPUTurn || turnLocked) return;
      selectedWeapon = i;
      placeMode = !!WEAPONS[i].placeCursor;
      AudioSys.select();
      updateWeaponBar();
      document.getElementById('helpHint').textContent = w.desc;
    });
    bar.appendChild(btn);
  });
}

// ---------- AI ----------
function updateAI(dt) {
  if (!aiState || !activeUnit || activeUnit.dead) return;
  aiState.t -= dt;

  if (aiState.stage === 'think') {
    if (aiState.t > 0) return;
    aiState.plan = planAI();
    aiState.stage = 'move';
    aiState.t = aiState.plan.moveTime || 1.2;
    return;
  }

  if (aiState.stage === 'move') {
    // risk assessment movement
    const plan = aiState.plan;
    if (plan.moveDir !== 0) {
      keys['KeyA'] = plan.moveDir < 0;
      keys['KeyD'] = plan.moveDir > 0;
      // avoid water / cliff
      const lookX = activeUnit.x + plan.moveDir * 25;
      if (!terrain.solid(lookX, activeUnit.y + 20) && activeUnit.grounded) {
        // cliff! stop or jump back
        keys['KeyA'] = keys['KeyD'] = false;
        if (activeUnit.y > WATER_Y - 80) plan.moveDir *= -1;
      }
      if (activeUnit.y > WATER_Y - 30) {
        // panic jump away from water
        keys['Space'] = true;
        plan.moveDir = activeUnit.x < WORLD_W / 2 ? 1 : -1;
      }
    }
    if (plan.jump && activeUnit.grounded && aiState.t < (plan.moveTime || 1) * 0.5) {
      keys['Space'] = true;
    }
    if (aiState.t <= 0) {
      keys['KeyA'] = keys['KeyD'] = keys['Space'] = false;
      aiState.stage = 'aim';
      aiState.t = 0.5;
    }
    return;
  }

  if (aiState.stage === 'aim') {
    keys['KeyA'] = keys['KeyD'] = keys['Space'] = false;
    if (aiState.t > 0) return;
    // select weapon
    selectedWeapon = aiState.plan.weapon;
    updateWeaponBar();
    aimAngle = aiState.plan.angle;
    activeUnit.lookAngle = aimAngle;
    activeUnit.facing = Math.cos(aimAngle) >= 0 ? 1 : -1;
    aiState.stage = 'charge';
    aiState.t = 0;
    chargePower = 0;
    charging = WEAPONS[selectedWeapon].powerNeeded;
    if (!charging) {
      fireSelected(aiState.plan.power || 0.7, aiState.plan.targetX, aiState.plan.targetY);
      aiState.stage = 'done';
    }
    return;
  }

  if (aiState.stage === 'charge') {
    chargePower += dt * 0.55;
    if (chance(0.2)) AudioSys.charge();
    document.getElementById('powerBar').classList.add('show');
    document.getElementById('powerFill').style.width = (clamp(chargePower, 0, 1) * 100) + '%';
    if (chargePower >= aiState.plan.power) {
      charging = false;
      document.getElementById('powerBar').classList.remove('show');
      fireSelected(clamp(chargePower, 0.15, 1), aiState.plan.targetX, aiState.plan.targetY);
      aiState.stage = 'done';
    }
    return;
  }
}

function planAI() {
  const me = activeUnit;
  // find high-value targets (lowest HP or clustered)
  const enemies = allUnits().filter(u => !u.dead && u.team !== me.team);
  if (!enemies.length) return { moveDir: 0, moveTime: 0.3, weapon: 0, angle: -0.4, power: 0.5, jump: false };

  // score targets
  let best = null;
  let bestScore = -Infinity;
  for (const e of enemies) {
    let score = (UNIT_HP - e.hp) * 1.5; // prefer wounded
    // cluster bonus
    const nearby = enemies.filter(o => o !== e && dist(o.x, o.y, e.x, e.y) < 80).length;
    score += nearby * 25;
    // prefer closer if using direct
    const d = dist(me.x, me.y, e.x, e.y);
    score += clamp(400 - d, 0, 200) * 0.1;
    // avoid if target near water hazard less? actually push into water is good
    if (e.y > WATER_Y - 60) score += 15;
    // risk: am I near cliff/water?
    if (me.y > WATER_Y - 80) score -= 20;
    if (score > bestScore) { bestScore = score; best = e; }
  }
  const target = best;
  const dx = target.x - me.x;
  const dy = target.y - me.y;
  const d = Math.hypot(dx, dy);

  // pick weapon
  let weapon = 0;
  let power = 0.55;
  let angle = Math.atan2(dy - 40, dx);
  let placeX = target.x, placeY = target.y;

  ensureWeaponAmmo();

  const can = id => {
    const w = WEAPONS.find(x => x.id === id);
    const idx = WEAPONS.indexOf(w);
    return weaponAmmo[id] > 0 || weaponAmmo[id] === Infinity ? idx : -1;
  };

  if (d < 50 && chance(0.5)) {
    const b = can('bat');
    if (b >= 0) { weapon = b; power = 0.7 + Math.random() * 0.3; angle = Math.atan2(dy, dx); }
  } else if (d < 160 && chance(0.4)) {
    const s = can('shotgun');
    if (s >= 0) { weapon = s; angle = Math.atan2(dy, dx); }
  } else if (d > 280 && chance(0.35) && can('airstrike') >= 0) {
    weapon = can('airstrike');
    placeX = target.x; placeY = target.y;
  } else if (chance(0.2) && can('holy') >= 0 && d < 350) {
    weapon = can('holy');
    // lob
    angle = -Math.PI / 3 * Math.sign(dx || 1) + (dx > 0 ? -0.3 : -Math.PI + 0.3);
    angle = dx > 0 ? -PI / 3 : -PI + PI / 3;
    power = clamp(d / 500, 0.35, 0.95);
  } else if (chance(0.25) && can('sheep') >= 0) {
    weapon = can('sheep');
    angle = dx > 0 ? -0.2 : -PI + 0.2;
  } else if (chance(0.3) && can('grenade') >= 0) {
    weapon = can('grenade');
    angle = dx > 0 ? -PI / 3.5 : -PI + PI / 3.5;
    power = clamp(d / 450 + 0.1, 0.3, 0.95);
  } else {
    weapon = can('bazooka') >= 0 ? can('bazooka') : 0;
    // ballistic estimate
    angle = dx > 0 ? -PI / 3.2 : -PI + PI / 3.2;
    // tune for height
    if (dy < -50) angle += (dx > 0 ? -0.2 : 0.2);
    power = clamp(d / 480 + Math.abs(dy) / 800, 0.25, 0.98);
    // wind compensation
    angle -= wind * 0.02;
  }

  // freeze wounded stacked targets
  if (target.hp < 40 && can('freeze') >= 0 && chance(0.3)) {
    weapon = can('freeze');
    angle = dx > 0 ? -PI / 3 : -PI + PI / 3;
    power = clamp(d / 450, 0.3, 0.9);
  }

  // movement: get to better ground / away from water / closer if melee
  let moveDir = 0;
  let moveTime = rand(0.4, 1.5);
  let jump = chance(0.25);
  if (me.y > WATER_Y - 70) {
    moveDir = me.x < WORLD_W / 2 ? 1 : -1;
    moveTime = 1.5;
    jump = true;
  } else if (WEAPONS[weapon].melee) {
    moveDir = sign(dx);
    moveTime = clamp(d / 60, 0.3, 2);
  } else if (chance(0.4)) {
    // fortify position slightly
    moveDir = chance(0.5) ? 1 : -1;
    // risk: don't walk off
    const probe = me.x + moveDir * 40;
    if (!terrain.solid(probe, me.y + 25)) moveDir *= -1;
    // don't approach water
    if (terrain.solid(probe, WATER_Y - 5) === false && me.y > WATER_Y - 100) moveDir = 0;
  }

  // utility: teleport out of bad place
  if (me.hp < 30 && me.y > WATER_Y - 90 && can('teleport') >= 0 && chance(0.5)) {
    weapon = can('teleport');
    placeX = me.x + (me.x < WORLD_W / 2 ? 120 : -120);
    placeY = me.y - 80;
  }

  return {
    moveDir, moveTime, jump, weapon, angle, power, targetX: placeX, targetY: placeY
  };
}

// ---------- Firing ----------
function fireSelected(power, tx, ty) {
  if (!activeUnit || turnLocked || phase !== 'playing') return;
  const w = WEAPONS[selectedWeapon];
  ensureWeaponAmmo();
  if (weaponAmmo[w.id] !== Infinity && weaponAmmo[w.id] <= 0) return;

  const ang = aimAngle;
  w.fire(activeUnit, ang, power, tx, ty);

  if (weaponAmmo[w.id] !== Infinity) {
    weaponAmmo[w.id]--;
  }
  updateWeaponBar();

  // utilities that allow continued turn? rope/jetpack don't end immediately the same way
  if (w.id === 'rope' || w.id === 'jetpack') {
    // partial turn continue
    placeMode = false;
    return;
  }
  placeMode = false;
  endTurn();
}

function spawnMuzzleFlash(x, y) {
  particles.burst(x, y, { count: 10, color: '#ffaa33', speed: 3, life: 0.2, r: 2 });
}

// ---------- Dig / utilities ----------
function digCircle(x, y, r, debris = true) {
  terrain.carveCircle(x, y, r, debris);
}

// ---------- Drawing sky / water ----------
function drawBackground(ctx) {
  // sky gradient
  const g = ctx.createLinearGradient(0, 0, 0, WORLD_H);
  g.addColorStop(0, '#0d1b3a');
  g.addColorStop(0.35, '#1e4d8c');
  g.addColorStop(0.7, '#5ba3d9');
  g.addColorStop(1, '#9fd4f0');
  ctx.fillStyle = g;
  ctx.fillRect(0, 0, WORLD_W, WORLD_H);

  // stars / atmosphere
  ctx.fillStyle = 'rgba(255,255,255,0.7)';
  for (let i = 0; i < 40; i++) {
    const sx = ((i * 347) % WORLD_W);
    const sy = ((i * 191) % (WORLD_H * 0.4));
    const tw = 0.5 + Math.sin(waterTime * 2 + i) * 0.5;
    ctx.globalAlpha = 0.3 + tw * 0.5;
    ctx.fillRect(sx, sy, 2, 2);
  }
  ctx.globalAlpha = 1;

  // sun / moon
  const sunX = WORLD_W * 0.82, sunY = WORLD_H * 0.18;
  const rg = ctx.createRadialGradient(sunX, sunY, 10, sunX, sunY, 80);
  rg.addColorStop(0, 'rgba(255,240,180,0.9)');
  rg.addColorStop(0.4, 'rgba(255,200,100,0.3)');
  rg.addColorStop(1, 'rgba(255,200,100,0)');
  ctx.fillStyle = rg;
  ctx.beginPath(); ctx.arc(sunX, sunY, 80, 0, TAU); ctx.fill();
  ctx.fillStyle = '#ffe8a0';
  ctx.beginPath(); ctx.arc(sunX, sunY, 28, 0, TAU); ctx.fill();

  // far clouds
  drawCloud(ctx, 200 + Math.sin(waterTime * 0.1) * 30, 120, 1.2, 0.25);
  drawCloud(ctx, 700 + Math.sin(waterTime * 0.07 + 1) * 40, 180, 1.6, 0.2);
  drawCloud(ctx, 1400 + Math.sin(waterTime * 0.09 + 2) * 20, 100, 1.0, 0.22);
  drawCloud(ctx, 1000, 220, 1.4, 0.18);

  // distant mountains silhouette
  ctx.fillStyle = 'rgba(40,70,100,0.35)';
  ctx.beginPath();
  ctx.moveTo(0, WORLD_H * 0.55);
  for (let x = 0; x <= WORLD_W; x += 20) {
    const y = WORLD_H * 0.52 + Math.sin(x * 0.005) * 40 + Math.sin(x * 0.012) * 20;
    ctx.lineTo(x, y);
  }
  ctx.lineTo(WORLD_W, WORLD_H); ctx.lineTo(0, WORLD_H);
  ctx.fill();
}

function drawCloud(ctx, x, y, s, a) {
  ctx.fillStyle = `rgba(255,255,255,${a})`;
  ctx.beginPath();
  ctx.ellipse(x, y, 40 * s, 18 * s, 0, 0, TAU);
  ctx.ellipse(x + 30 * s, y + 5 * s, 30 * s, 14 * s, 0, 0, TAU);
  ctx.ellipse(x - 30 * s, y + 5 * s, 28 * s, 14 * s, 0, 0, TAU);
  ctx.ellipse(x + 10 * s, y - 12 * s, 25 * s, 14 * s, 0, 0, TAU);
  ctx.fill();
}

function drawWater(ctx) {
  const t = waterTime;
  // deep
  ctx.fillStyle = 'rgba(10,50,90,0.75)';
  ctx.fillRect(0, WATER_Y, WORLD_W, WORLD_H - WATER_Y);

  // surface gradient
  const wg = ctx.createLinearGradient(0, WATER_Y - 10, 0, WATER_Y + 80);
  wg.addColorStop(0, 'rgba(80,180,230,0.55)');
  wg.addColorStop(0.3, 'rgba(30,100,170,0.7)');
  wg.addColorStop(1, 'rgba(8,40,70,0.85)');
  ctx.fillStyle = wg;
  ctx.beginPath();
  ctx.moveTo(0, WORLD_H);
  ctx.lineTo(0, WATER_Y);
  for (let x = 0; x <= WORLD_W; x += 8) {
    const wave = Math.sin(x * 0.02 + t * 2.5) * 3 + Math.sin(x * 0.05 + t * 3.5) * 2;
    ctx.lineTo(x, WATER_Y + wave);
  }
  ctx.lineTo(WORLD_W, WORLD_H);
  ctx.fill();

  // shimmer lines
  ctx.strokeStyle = 'rgba(180,230,255,0.2)';
  ctx.lineWidth = 1;
  for (let i = 0; i < 6; i++) {
    ctx.beginPath();
    const yy = WATER_Y + 10 + i * 12;
    for (let x = 0; x <= WORLD_W; x += 12) {
      const y = yy + Math.sin(x * 0.03 + t * 2 + i) * 2;
      if (x === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
    }
    ctx.stroke();
  }

  // caustics-ish
  ctx.fillStyle = 'rgba(150,220,255,0.05)';
  for (let i = 0; i < 8; i++) {
    const cx = ((t * 30 + i * 240) % (WORLD_W + 100)) - 50;
    ctx.beginPath();
    ctx.ellipse(cx, WATER_Y + 30 + (i % 3) * 20, 40, 8, 0, 0, TAU);
    ctx.fill();
  }
}

// ---------- Main loop ----------
let lastT = performance.now();
function frame(now) {
  let dt = Math.min(0.05, (now - lastT) / 1000);
  lastT = now;

  if (impactFreeze > 0) {
    impactFreeze -= dt;
    dt *= 0.15; // slow mo
  }

  waterTime += dt;

  if (phase === 'playing') {
    updateGame(dt);
  }

  render();
  requestAnimationFrame(frame);
}
requestAnimationFrame(frame);

function updateGame(dt) {
  // turn timer
  if (!turnLocked && !isCPUTurn) {
    turnTime -= dt;
    const tv = document.getElementById('timerVal');
    tv.textContent = Math.max(0, Math.ceil(turnTime));
    const urgent = turnTime < 10;
    tv.classList.toggle('urgent', urgent);
    // ticking
    if (urgent && Math.floor(turnTime * (turnTime < 5 ? 4 : 2)) !== Math.floor((turnTime + dt) * (turnTime < 5 ? 4 : 2))) {
      AudioSys.tick(turnTime < 5);
      AudioSys.tempo = turnTime < 5 ? 1.5 : 1.2;
    }
    // UI scale tension via style
    tv.style.transform = urgent ? `scale(${1 + (10 - turnTime) * 0.03})` : '';

    if (turnTime <= 0) {
      endTurn();
    }
  }

  if (isCPUTurn && !turnLocked) updateAI(dt);
  else if (!isCPUTurn && !turnLocked) handlePlayerInput(dt);

  // units
  for (const u of allUnits()) {
    const isAct = u === activeUnit;
    const canCtrl = isAct && !turnLocked && !isCPUTurn && phase === 'playing';
    u.update(dt, isAct, canCtrl);
  }

  // projectiles
  for (let i = projectiles.length - 1; i >= 0; i--) {
    projectiles[i].update(dt);
    if (projectiles[i].dead) projectiles.splice(i, 1);
  }

  // debris
  for (let i = debris.length - 1; i >= 0; i--) {
    debris[i].update(dt);
    if (debris[i].dead) debris.splice(i, 1);
  }

  particles.update(dt);
  updateCamera(dt);

  if (turnLocked) afterActionSettle(dt);

  // mouse world pos
  const invZ = 1 / cam.zoom;
  mouse.worldX = (mouse.x - WORLD_W / 2) * invZ + cam.x;
  mouse.worldY = (mouse.y - WORLD_H / 2) * invZ + cam.y;

  if (activeUnit && !isCPUTurn) {
    aimAngle = Math.atan2(mouse.worldY - (activeUnit.y - 6), mouse.worldX - activeUnit.x);
    activeUnit.lookAngle = aimAngle;
    activeUnit.facing = Math.cos(aimAngle) >= 0 ? 1 : -1;
  }

  mouse.justUp = false;
}

function handlePlayerInput(dt) {
  if (!activeUnit || activeUnit.dead || activeUnit.frozen > 0) return;
  const w = WEAPONS[selectedWeapon];

  // weapon keys
  for (let i = 0; i < WEAPONS.length; i++) {
    const code = 'Digit' + WEAPONS[i].key;
    if (WEAPONS[i].key === '0' && keys['Digit0']) selectWep(i);
    else if (WEAPONS[i].key.length === 1 && keys['Digit' + WEAPONS[i].key]) selectWep(i);
    else if (WEAPONS[i].key === 'J' && keys['KeyJ']) selectWep(i);
    else if (WEAPONS[i].key === 'G' && keys['KeyG']) selectWep(i);
    else if (WEAPONS[i].key === 'F' && keys['KeyF']) selectWep(i);
  }
  if (keys['KeyQ']) { cycleWep(-1); keys['KeyQ'] = false; }
  if (keys['KeyE']) { cycleWep(1); keys['KeyE'] = false; }

  // place mode click
  if (placeMode || w.placeCursor) {
    document.getElementById('helpHint').textContent = 'Click target location on the map';
    if (mouse.justUp) {
      fireSelected(0.5, mouse.worldX, mouse.worldY);
    }
    return;
  }

  // charge & fire
  if (w.powerNeeded) {
    if (mouse.down && !charging) {
      charging = true; chargePower = 0;
      document.getElementById('powerBar').classList.add('show');
    }
    if (charging && mouse.down) {
      chargePower = Math.min(1, chargePower + dt * 0.7);
      document.getElementById('powerFill').style.width = (chargePower * 100) + '%';
      if (chance(0.15)) AudioSys.charge();
    }
    if (charging && !mouse.down) {
      charging = false;
      document.getElementById('powerBar').classList.remove('show');
      fireSelected(clamp(chargePower, 0.08, 1));
    }
  } else if (w.holdUse) {
    // jetpack already fired via select
  } else {
    if (mouse.justUp) {
      fireSelected(0.7);
    }
  }
}

function selectWep(i) {
  if (turnLocked || isCPUTurn) return;
  selectedWeapon = i;
  placeMode = !!WEAPONS[i].placeCursor;
  AudioSys.select();
  updateWeaponBar();
  document.getElementById('helpHint').textContent = WEAPONS[i].desc;
  // auto-activate jetpack / dynamite style on select for jetpack
  if (WEAPONS[i].id === 'jetpack' && weaponAmmo.jetpack > 0) {
    // wait for click
  }
}

let wepCycleLock = false;
function cycleWep(dir) {
  if (turnLocked || isCPUTurn || wepCycleLock) return;
  let i = selectedWeapon;
  for (let k = 0; k < WEAPONS.length; k++) {
    i = (i + dir + WEAPONS.length) % WEAPONS.length;
    const ammo = weaponAmmo[WEAPONS[i].id];
    if (ammo === Infinity || ammo > 0) { selectWep(i); break; }
  }
}

// ---------- Render ----------
function render() {
  ctx.save();
  // clear
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
  ctx.clearRect(0, 0, WORLD_W, WORLD_H);

  // camera transform
  const sx = shake > 0 ? rand(-shake, shake) : 0;
  const sy = shake > 0 ? rand(-shake, shake) : 0;

  ctx.save();
  ctx.translate(WORLD_W / 2 + sx, WORLD_H / 2 + sy);
  ctx.scale(cam.zoom, cam.zoom);
  ctx.translate(-cam.x, -cam.y);

  if (phase === 'playing' || phase === 'end') {
    drawBackground(ctx);
    terrain.draw(ctx, cam);

    // ropes
    for (const u of allUnits()) {
      if (u.rope && u.rope.active && !u.dead) {
        ctx.strokeStyle = '#ddd';
        ctx.lineWidth = 2;
        ctx.beginPath();
        ctx.moveTo(u.rope.x, u.rope.y);
        ctx.lineTo(u.x, u.y - 6);
        ctx.stroke();
        ctx.fillStyle = '#888';
        ctx.beginPath(); ctx.arc(u.rope.x, u.rope.y, 3, 0, TAU); ctx.fill();
      }
    }

    // debris
    for (const d of debris) d.draw(ctx);

    // units
    for (const u of allUnits()) u.draw(ctx);

    // projectiles
    for (const p of projectiles) p.draw(ctx);

    particles.draw(ctx);
    drawWater(ctx);

    // aim trajectory + crosshair for player
    if (phase === 'playing' && activeUnit && !activeUnit.dead && !turnLocked && !isCPUTurn) {
      const w = WEAPONS[selectedWeapon];
      if (w.powerNeeded && !placeMode) {
        const power = charging ? chargePower : 0.55;
        const spd = w.id === 'grenade' || w.id === 'holy' || w.id === 'freeze'
          ? 4 + power * 12
          : 6 + power * 14;
        const grav = w.id === 'bazooka' ? 0.16 : 0.2;
        const ox = activeUnit.x + Math.cos(aimAngle) * 18;
        const oy = activeUnit.y + Math.sin(aimAngle) * 18 - 6;
        const pts = predictTrajectory(ox, oy, Math.cos(aimAngle) * spd, Math.sin(aimAngle) * spd, grav, true, 50);
        ctx.strokeStyle = 'rgba(255,255,255,0.45)';
        ctx.lineWidth = 2;
        ctx.setLineDash([4, 6]);
        ctx.beginPath();
        pts.forEach((p, i) => { if (i === 0) ctx.moveTo(p.x, p.y); else ctx.lineTo(p.x, p.y); });
        ctx.stroke();
        ctx.setLineDash([]);
        if (pts.length) {
          const last = pts[pts.length - 1];
          ctx.strokeStyle = 'rgba(255,100,100,0.6)';
          ctx.beginPath(); ctx.arc(last.x, last.y, w.radius * 0.3, 0, TAU); ctx.stroke();
        }
      }

      // aim line short
      ctx.strokeStyle = TEAM_COLORS[activeTeam].light;
      ctx.globalAlpha = 0.6;
      ctx.lineWidth = 2;
      ctx.beginPath();
      ctx.moveTo(activeUnit.x, activeUnit.y - 6);
      ctx.lineTo(activeUnit.x + Math.cos(aimAngle) * 50, activeUnit.y - 6 + Math.sin(aimAngle) * 50);
      ctx.stroke();
      ctx.globalAlpha = 1;

      // crosshair at mouse
      const mx = mouse.worldX, my = mouse.worldY;
      ctx.strokeStyle = 'rgba(255,255,255,0.8)';
      ctx.lineWidth = 1.5;
      ctx.beginPath();
      ctx.moveTo(mx - 10, my); ctx.lineTo(mx - 3, my);
      ctx.moveTo(mx + 3, my); ctx.lineTo(mx + 10, my);
      ctx.moveTo(mx, my - 10); ctx.lineTo(mx, my - 3);
      ctx.moveTo(mx, my + 3); ctx.lineTo(mx, my + 10);
      ctx.stroke();
      if (placeMode || w.placeCursor) {
        ctx.beginPath(); ctx.arc(mx, my, 20, 0, TAU); ctx.stroke();
        ctx.fillStyle = 'rgba(255,200,50,0.15)';
        ctx.fill();
      }
    }
  } else {
    // menu bg pretty
    drawBackground(ctx);
    drawWater(ctx);
  }

  ctx.restore();
  ctx.restore();
}

// ---------- Game start ----------
function startGame() {
  AudioSys.resume();
  document.getElementById('genOverlay').classList.remove('hidden');
  document.getElementById('menu').classList.add('hidden');
  document.getElementById('endScreen').classList.add('hidden');

  setTimeout(() => {
    terrain = new Terrain(WORLD_W, WORLD_H);
    terrain.generate(terrainShape);

    teams = [];
    projectiles = [];
    debris = [];
    particles.list = [];
    weaponAmmo = {};
    gameOver = false;

    const spawns = terrain.findSpawnPoints(teamCount * UNITS_PER_TEAM);
    // distribute spawns across map for fairness
    // interleave team assignment left-right
    const slots = spawns.slice(0, teamCount * UNITS_PER_TEAM);
    // give each team spread positions
    const perTeam = [];
    for (let t = 0; t < teamCount; t++) perTeam.push([]);
    slots.forEach((s, i) => {
      // snake distribute
      const t = i % teamCount;
      perTeam[t].push(s);
    });

    for (let t = 0; t < teamCount; t++) {
      const units = [];
      for (let u = 0; u < UNITS_PER_TEAM; u++) {
        const p = perTeam[t][u] || { x: 200 + t * 200 + u * 40, y: 400 };
        // drop to ground
        let y = p.y;
        while (y < WATER_Y - 20 && !terrain.solid(p.x, y + 1)) y++;
        units.push(new Unit(p.x, y - 1, t, u));
      }
      teams.push(units);
    }

    ensureWeaponAmmo();
    activeTeam = randi(0, teamCount - 1);
    activeIndex = -1;
    phase = 'playing';
    cam.x = WORLD_W / 2; cam.y = WORLD_H / 2; cam.zoom = 0.8;
    document.getElementById('hud').classList.add('visible');
    document.getElementById('genOverlay').classList.add('hidden');
    beginTurn();
  }, 80);
}

// ---------- Menu wiring ----------
function wireChips(id, cb) {
  const root = document.getElementById(id);
  root.querySelectorAll('.chip').forEach(chip => {
    chip.addEventListener('click', () => {
      root.querySelectorAll('.chip').forEach(c => c.classList.remove('active'));
      chip.classList.add('active');
      AudioSys.click();
      cb(chip.dataset.v);
    });
    chip.addEventListener('mouseenter', () => AudioSys.uiHover());
  });
}

wireChips('teamCountChips', v => { teamCount = parseInt(v, 10); });
wireChips('cpuChips', v => { humanCount = parseInt(v, 10); });
wireChips('terrainChips', v => { terrainShape = v; });

document.getElementById('btnStart').addEventListener('click', () => { AudioSys.click(); startGame(); });
document.getElementById('btnHow').addEventListener('click', () => {
  AudioSys.click();
  document.getElementById('howScreen').classList.remove('hidden');
});
document.getElementById('btnBackHow').addEventListener('click', () => {
  AudioSys.click();
  document.getElementById('howScreen').classList.add('hidden');
});
document.getElementById('btnRematch').addEventListener('click', () => { AudioSys.click(); startGame(); });
document.getElementById('btnMenu').addEventListener('click', () => {
  AudioSys.click();
  phase = 'menu';
  document.getElementById('endScreen').classList.add('hidden');
  document.getElementById('menu').classList.remove('hidden');
  document.getElementById('hud').classList.remove('visible');
});

window.addEventListener('keydown', e => {
  if (e.code === 'Escape') {
    if (phase === 'playing') {
      phase = 'menu';
      document.getElementById('menu').classList.remove('hidden');
      document.getElementById('hud').classList.remove('visible');
    }
  }
});

// initial decorative camera sway on menu
setInterval(() => {
  if (phase === 'menu') {
    cam.x = WORLD_W / 2 + Math.sin(performance.now() / 3000) * 40;
    cam.y = WORLD_H / 2 + Math.cos(performance.now() / 4000) * 20;
    cam.zoom = 0.55;
  }
}, 50);

// generate a preview terrain for menu?
terrain = new Terrain(WORLD_W, WORLD_H);
terrain.generate('island');
phase = 'menu';

})();
</script>
</body>
</html>