← Worms results

Worms

Ornith 1.5 35B A3B · full

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 66,526 bytes · SHA-256 0fdb6dab12ea
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WARMFALL — Artillery Duel</title>
<style>
  :root{
    --bg0:#0a0e1a; --bg1:#121a2e; --panel:#0e1626; --panel2:#16223a;
    --ink:#e8eefc; --ink2:#9fb0d0; --accent:#ff4d5e; --accent2:#ffcf4d;
    --good:#3fe08a; --sky1:#1a2a4a; --sky2:#3a5a8a;
    --gold:#ffd35c;
  }
  *{box-sizing:border-box;}
  html,body{margin:0;padding:0;height:100%;overflow:hidden;background:var(--bg0);
    font-family:'Segoe UI',system-ui,-apple-system,sans-serif;color:var(--ink);
    user-select:none;-webkit-user-select:none;}
  #wrap{position:fixed;inset:0;}
  canvas{position:absolute;top:0;left:0;display:block;}
  #game{z-index:1;}

  /* ---------- MENUS ---------- */
  .overlay{position:absolute;inset:0;z-index:50;display:flex;align-items:center;justify-content:center;
    background:radial-gradient(circle at 50% 30%,rgba(30,45,80,.85),rgba(6,9,18,.96));
    backdrop-filter:blur(3px);}
  .overlay.hidden{display:none;}
  .card{width:min(640px,92vw);max-height:92vh;overflow:auto;background:linear-gradient(180deg,var(--panel),var(--bg1));
    border:1px solid rgba(120,160,255,.25);border-radius:18px;padding:26px 30px;
    box-shadow:0 30px 80px rgba(0,0,0,.6),inset 0 1px 0 rgba(255,255,255,.06);}
  h1.title{font-size:52px;margin:0;letter-spacing:2px;text-align:center;
    background:linear-gradient(90deg,#ff4d5e,#ffcf4d,#3fe08a,#4db4ff);
    -webkit-background-clip:text;background-clip:text;color:transparent;
    text-shadow:0 4px 24px rgba(255,207,77,.25);font-weight:900;}
  .subtitle{text-align:center;color:var(--ink2);margin:4px 0 20px;letter-spacing:4px;font-size:12px;text-transform:uppercase;}
  .field{margin:14px 0;}
  .field>label{display:block;font-size:12px;text-transform:uppercase;letter-spacing:2px;color:var(--ink2);margin-bottom:7px;}
  .row{display:flex;gap:10px;flex-wrap:wrap;}
  .seg{display:flex;background:#0a1220;border:1px solid rgba(120,160,255,.2);border-radius:10px;overflow:hidden;}
  .seg button{flex:1;background:transparent;border:0;color:var(--ink2);padding:10px 12px;cursor:pointer;font-size:13px;font-weight:600;
    transition:.15s;}
  .seg button:hover{background:rgba(120,160,255,.12);}
  .seg button.sel{background:linear-gradient(180deg,#3a5a9a,#2a4070);color:#fff;box-shadow:inset 0 0 0 1px rgba(120,160,255,.5);}
  .btn{display:inline-block;background:linear-gradient(180deg,#ff5a6c,#e03046);color:#fff;border:0;border-radius:12px;
    padding:14px 22px;font-size:16px;font-weight:800;cursor:pointer;letter-spacing:1px;transition:.12s;
    box-shadow:0 6px 0 #a01828,0 10px 20px rgba(224,48,70,.4);}
  .btn:hover{transform:translateY(-2px);box-shadow:0 8px 0 #a01828,0 14px 26px rgba(224,48,70,.5);}
  .btn:active{transform:translateY(4px);box-shadow:0 2px 0 #a01828;}
  .btn.block{display:block;width:100%;margin-top:18px;text-align:center;}
  .btn.ghost{background:linear-gradient(180deg,#3a4a6a,#28364f);box-shadow:0 6px 0 #1a2436;}
  .btn.small{padding:9px 14px;font-size:13px;}
  .hint{font-size:12px;color:var(--ink2);margin-top:14px;line-height:1.6;text-align:center;}
  .team-setup{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:6px;}
  .team-card{background:#0c1524;border:1px solid rgba(120,160,255,.15);border-radius:12px;padding:12px;}
  .team-card .tname{display:flex;align-items:center;gap:8px;font-weight:800;margin-bottom:8px;}
  .swatch{width:18px;height:18px;border-radius:5px;box-shadow:0 0 10px currentColor;}
  .team-card label{font-size:11px;color:var(--ink2);}
  .team-card select,.team-card input{width:100%;background:#0a1220;border:1px solid rgba(120,160,255,.2);color:var(--ink);
    border-radius:8px;padding:7px;font-size:13px;margin-top:3px;}
  .badge{display:inline-block;padding:2px 8px;border-radius:20px;font-size:11px;font-weight:700;background:#1a2a4a;color:var(--ink2);}

  /* ---------- HUD ---------- */
  #hud{position:absolute;inset:0;z-index:20;pointer-events:none;}
  #hud *{pointer-events:auto;}
  .topbar{position:absolute;top:12px;left:12px;right:12px;display:flex;justify-content:space-between;align-items:flex-start;gap:10px;}
  .turn-pan{background:linear-gradient(180deg,var(--panel2),rgba(14,22,38,.9));border:1px solid rgba(120,160,255,.2);
    border-radius:14px;padding:10px 16px;min-width:220px;box-shadow:0 8px 26px rgba(0,0,0,.4);}
  .turnwho{display:flex;align-items:center;gap:10px;font-weight:800;font-size:16px;}
  .team-dot{width:14px;height:14px;border-radius:50%;box-shadow:0 0 10px currentColor;}
  .timer-wrap{margin-top:8px;}
  .timer-track{height:8px;border-radius:6px;background:#0a1220;overflow:hidden;box-shadow:inset 0 1px 3px rgba(0,0,0,.5);}
  .timer-fill{height:100%;width:100%;background:linear-gradient(90deg,#3fe08a,#a6e04d);transition:width .1s linear;}
  .timer-fill.warn{background:linear-gradient(90deg,#ffcf4d,#ff8a3d);}
  .timer-fill.danger{background:linear-gradient(90deg,#ff5a6c,#ff2d44);}
  .timer-txt{display:flex;justify-content:space-between;font-size:11px;color:var(--ink2);margin-top:4px;text-transform:uppercase;letter-spacing:2px;}
  #turnTxt{font-size:12px;color:var(--ink2);margin-top:6px;font-weight:600;}

  .wind-box{background:linear-gradient(180deg,var(--panel2),rgba(14,22,38,.9));border:1px solid rgba(120,160,255,.2);
    border-radius:14px;padding:10px 14px;text-align:center;box-shadow:0 8px 26px rgba(0,0,0,.4);}
  .wind-box .lbl{font-size:10px;letter-spacing:3px;color:var(--ink2);text-transform:uppercase;}
  #windCanvas{display:block;margin:6px auto 0;}
  .wind-val{font-size:12px;font-weight:700;color:var(--accent2);}

  .hud-right{position:absolute;right:12px;top:12px;display:flex;flex-direction:column;gap:10px;align-items:flex-end;}
  .team-list{display:flex;flex-direction:column;gap:6px;min-width:170px;}
  .team-row{display:flex;align-items:center;gap:8px;background:linear-gradient(180deg,var(--panel2),rgba(14,22,38,.85));
    border:1px solid rgba(120,160,255,.15);border-radius:10px;padding:6px 10px;font-size:12px;transition:.2s;}
  .team-row.active{border-color:var(--accent);box-shadow:0 0 0 1px var(--accent),0 4px 16px rgba(255,77,94,.3);}
  .team-row .nm{font-weight:800;flex:1;}
  .team-row .hpbar{width:56px;height:7px;border-radius:5px;background:#0a1220;overflow:hidden;}
  .team-row .hpfill{height:100%;background:linear-gradient(90deg,#3fe08a,#a6e04d);transition:width .3s;}
  .team-row.dead{opacity:.4;}

  /* weapon bar */
  .weaponbar{position:absolute;bottom:12px;left:50%;transform:translateX(-50%);display:flex;gap:8px;
    background:linear-gradient(180deg,var(--panel2),rgba(10,16,28,.95));border:1px solid rgba(120,160,255,.2);
    border-radius:16px;padding:8px;box-shadow:0 12px 34px rgba(0,0,0,.55);}
  .weapon{width:64px;height:64px;border-radius:12px;background:#0c1524;border:2px solid rgba(120,160,255,.15);
    cursor:pointer;position:relative;display:flex;flex-direction:column;align-items:center;justify-content:center;
    transition:.12s;}
  .weapon:hover{border-color:rgba(120,160,255,.45);transform:translateY(-3px);}
  .weapon.sel{border-color:var(--accent2);box-shadow:0 0 0 1px var(--accent2),0 0 20px rgba(255,207,77,.35);background:#1a2a44;}
  .weapon .ico{font-size:24px;line-height:1;}
  .weapon .nm{font-size:9px;color:var(--ink2);margin-top:3px;font-weight:700;text-transform:uppercase;letter-spacing:.5px;}
  .weapon .cost{position:absolute;top:3px;right:5px;font-size:9px;color:var(--accent2);font-weight:800;}
  .weapon .hintkey{position:absolute;bottom:3px;left:5px;font-size:9px;color:#5a6a8a;}
  .actionrow{position:absolute;bottom:90px;left:50%;transform:translateX(-50%);display:flex;gap:8px;}
  .actbtn{background:linear-gradient(180deg,#3a4a6a,#28364f);border:1px solid rgba(120,160,255,.25);color:#fff;
    border-radius:10px;padding:9px 16px;font-weight:700;font-size:13px;cursor:pointer;transition:.12s;}
  .actbtn:hover{background:linear-gradient(180deg,#4a5a7a,#34465f);}
  .actbtn.primary{background:linear-gradient(180deg,#3fe08a,#22b06a);box-shadow:0 5px 0 #167a49;}
  .actbtn.danger{background:linear-gradient(180deg,#ff5a6c,#e03046);box-shadow:0 5px 0 #a01828;}
  .actbtn:disabled{opacity:.4;cursor:not-allowed;}

  .hintbar{position:absolute;bottom:12px;left:12px;font-size:11px;color:#6a7aa0;
    background:rgba(10,16,28,.6);border-radius:8px;padding:6px 10px;line-height:1.5;}
  .hintbar b{color:var(--ink2);}

  /* combat / center text */
  #centerMsg{position:absolute;top:34%;left:50%;transform:translate(-50%,-50%);text-align:center;z-index:40;
    pointer-events:none;opacity:0;transition:opacity .3s;}
  #centerMsg .big{font-size:44px;font-weight:900;letter-spacing:2px;text-shadow:0 4px 24px rgba(0,0,0,.6);
    background:linear-gradient(90deg,#ffcf4d,#ff4d5e);-webkit-background-clip:text;background-clip:text;color:transparent;}
  #centerMsg .small{font-size:16px;color:var(--ink2);margin-top:6px;}

  /* scoreboard / gameover */
  .score-row{display:flex;align-items:center;gap:12px;padding:10px 14px;border-radius:10px;margin:6px 0;
    background:#0c1524;border:1px solid rgba(120,160,255,.12);}
  .score-row .rank{font-size:20px;font-weight:900;width:30px;text-align:center;}
  .score-row.winner{border-color:var(--gold);box-shadow:0 0 24px rgba(255,211,92,.3);}

  .toast{position:absolute;top:90px;left:50%;transform:translateX(-50%);z-index:45;
    background:rgba(14,22,38,.95);border:1px solid rgba(120,160,255,.3);border-radius:10px;
    padding:8px 18px;font-weight:700;font-size:14px;opacity:0;transition:.3s;pointer-events:none;}
</style>
</head>
<body>
<div id="wrap">
  <canvas id="game"></canvas>

  <!-- MAIN MENU -->
  <div id="menu" class="overlay">
    <div class="card">
      <h1 class="title">WARMFALL</h1>
      <div class="subtitle">Tactical Artillery Duel</div>

      <div class="field">
        <label>Number of Teams</label>
        <div class="seg" id="teamCount">
          <button data-v="2">2 Teams</button>
          <button data-v="3">3 Teams</button>
          <button data-v="4">4 Teams</button>
        </div>
      </div>

      <div class="field">
        <label>Team Setup</label>
        <div class="team-setup" id="teamSetup"></div>
      </div>

      <div class="field">
        <label>Map</label>
        <div class="seg" id="mapType">
          <button data-v="island">Single Island</button>
          <button data-v="two">Two Islands</button>
          <button data-v="cave">Cave</button>
        </div>
      </div>

      <div class="row">
        <div class="field" style="flex:1">
          <label>Turn Time</label>
          <div class="seg" id="turnTime">
            <button data-v="60">60s</button>
            <button data-v="45" class="sel">45s</button>
            <button data-v="30">30s</button>
          </div>
        </div>
        <div class="field" style="flex:1">
          <label>Win Condition</label>
          <div class="seg" id="winMode">
            <button data-v="surv" class="sel">Survival</button>
            <button data-v="score">Score</button>
          </div>
        </div>
      </div>

      <div class="field">
        <label>Difficulty (CPU)</label>
        <div class="seg" id="diff">
          <button data-v="easy">Casual</button>
          <button data-v="normal" class="sel">Normal</button>
          <button data-v="hard">Brutal</button>
        </div>
      </div>

      <button class="btn block" id="startBtn">START BATTLE</button>
      <div class="hint">Hot-seat: same keyboard for alternating players. Press <b>Space</b> to fire. Aim with arrows / A-D, angle with Q/E.</div>
    </div>
  </div>

  <!-- GAME OVER -->
  <div id="gameover" class="overlay hidden">
    <div class="card">
      <h1 class="title" id="winTitle">GAME OVER</h1>
      <div class="subtitle" id="winSub"></div>
      <div id="scoreboard"></div>
      <button class="btn block" id="againBtn">PLAY AGAIN</button>
      <button class="btn block ghost" id="menuBtn">MAIN MENU</button>
    </div>
  </div>

  <!-- HUD -->
  <div id="hud" class="hidden">
    <div class="topbar">
      <div class="turn-pan">
        <div class="turnwho"><span class="team-dot" id="teamDot"></span>
          <span id="teamName">Team</span></div>
        <div class="timer-wrap"><div class="timer-track"><div class="timer-fill" id="timerFill"></div></div>
          <div class="timer-txt"><span id="whoTxt">Worm</span><span id="timerVal">45s</span></div>
        </div>
      </div>
      <div class="wind-box">
        <div class="lbl">Wind</div>
        <canvas id="windCanvas" width="70" height="42"></canvas>
        <div class="wind-val" id="windVal">0</div>
      </div>
    </div>

    <div class="hud-right">
      <div class="team-list" id="teamList"></div>
    </div>

    <div class="actionrow" id="actionRow"></div>
    <div class="weaponbar" id="weaponbar"></div>
    <div class="hintbar" id="hintbar"><b>←/→</b> Aim <b>↑/↓</b> Angle <b>Space</b> Charge &amp; Fire <b>Q/E</b> Jump <b>F</b> Fire now</div>

    <div id="centerMsg"><div class="big"></div><div class="small"></div></div>
    <div class="toast" id="toast"></div>
  </div>
</div>

<script>
"use strict";
/* ============================================================================
   WARMFALL — a polished Worms-style artillery game
   ============================================================================ */

// ---------- Canvas ----------
const cv = document.getElementById('game');
const ctx = cv.getContext('2d');
let VW = 0, VH = 0, DPR = Math.min(window.devicePixelRatio||1, 2);
function resize(){
  VW = window.innerWidth; VH = window.innerHeight;
  cv.width = VW*DPR; cv.height = VH*DPR;
  cv.style.width = VW+'px'; cv.style.height = VH+'px';
  ctx.setTransform(DPR,0,0,DPR,0,0);
}
window.addEventListener('resize', resize); resize();

// ---------- Audio (WebAudio) ----------
const Sound = (()=>{
  let ac=null, master=null;
  function init(){ if(!ac){ ac=new (window.AudioContext||window.webkitAudioContext)();
      master=ac.createGain(); master.gain.value=0.6; master.connect(ac.destination); }
    if(ac.state==='suspended') ac.resume(); }
  function env(node, t, dur, peak){ const g=ac.createGain(); g.gain.setValueAtTime(0,t);
    g.gain.linearRampToValueAtTime(peak,t+0.005); g.gain.exponentialRampToValueAtTime(0.0001,t+dur);
    node.connect(g); g.connect(master); return g; }
  function noise(t, dur, type, freq, gain){
    const n=Math.floor(ac.sampleRate*dur); const buf=ac.createBuffer(1,n,ac.sampleRate);
    const d=buf.getChannelData(0); for(let i=0;i<n;i++) d[i]=(Math.random()*2-1)*(type==='sine'?0:1);
    const src=ac.createBufferSource(); src.buffer=buf;
    const f=ac.createBiquadFilter(); f.type=type||'lowpass'; f.frequency.value=freq||800;
    env(src,t,dur,gain||0.5); src.connect(f); f.connect(master); src.start(t); }
  function beep(t, freq, dur, type, gain, slideTo){
    const o=ac.createOscillator(); o.type=type||'square'; o.frequency.setValueAtTime(freq,t);
    if(slideTo) o.frequency.exponentialRampToValueAtTime(slideTo,t+dur);
    env(o,t,dur,gain||0.2); o.connect(master); o.start(t); o.stop(t+dur+0.02); }
  const S={
    click(){ init(); const t=ac.currentTime; beep(t,520,0.05,'square',0.14,300); },
    choose(){ init(); const t=ac.currentTime; beep(t,700,0.06,'triangle',0.16,950); },
    aim(){ init(); beep(ac.currentTime,240,0.08,'sine',0.12,180); },
    charge(lv){ init(); beep(ac.currentTime,180+lv*360,0.09,'sawtooth',0.10,120+lv*500); },
    fire(){ init(); const t=ac.currentTime; noise(t,0.18,'lowpass',600,0.35); beep(t,300,0.12,'sawtooth',0.18,80); },
    explode(big){ init(); const t=ac.currentTime; noise(t,big?0.6:0.3,'lowpass',big?400:700,big?0.6:0.4);
      beep(t,big?90:140,big?0.5:0.28,'sine',big?0.5:0.35,big?30:60); },
    hit(){ init(); const t=ac.currentTime; beep(t,900,0.05,'square',0.12,400); },
    hurt(){ init(); const t=ac.currentTime; beep(t,220,0.18,'sawtooth',0.2,70); },
    jump(){ init(); const t=ac.currentTime; beep(t,400,0.08,'sine',0.12,600); },
    step(){ init(); beep(ac.currentTime,120,0.04,'sine',0.06,90); },
    water(){ init(); beep(ac.currentTime,300,0.12,'sine',0.1,500); },
    drown(){ init(); const t=ac.currentTime; beep(t,400,0.3,'sine',0.15,120); },
    wind(){ init(); noise(ac.currentTime,0.4,'highpass',3000,0.12); },
    win(){ init(); const t=ac.currentTime; [523,659,784,1046].forEach((f,i)=>beep(t+i*0.12,f,0.2,'triangle',0.2)); },
    menu(){ init(); const t=ac.currentTime; beep(t,660,0.08,'triangle',0.16,880); },
    warn(){ init(); beep(ac.currentTime,880,0.1,'square',0.12,880); },
    countdown(){ init(); beep(ac.currentTime,660,0.12,'square',0.16); },
    boom(){ init(); const t=ac.currentTime; noise(t,0.9,'lowpass',300,0.7); beep(t,60,0.8,'sine',0.6,20); },
    float(){ init(); beep(ac.currentTime,500,0.1,'sine',0.08,700); },
  };
  window.S = S;
  return S;
})();

// ---------- Utility ----------
const clamp=(v,a,b)=>Math.max(a,Math.min(b,v));
const lerp=(a,b,t)=>a+(b-a)*t;
const rand=(a,b)=>a+Math.random()*(b-a);
const randi=(a,b)=>Math.floor(rand(a,b+1));
const dist=(x1,y1,x2,y2)=>Math.hypot(x2-x1,y2-y1);
const TAU=Math.PI*2;

// ---------- Constants ----------
const WORLD_W=2000, WORLD_H=1200;
const SKY_TOP=0, GROUND_X=400; // terrain top baseline world coords
const GRAV=1400, UNIT_GRAV=900, UNIT_SPEED=70, JUMP_V=620, MAX_FALL=700;
const FRICTION=0.86, GROUND_FRICTION=0.7;
const TILE=4; // destruction resolution (px)

// ---------- Game State ----------
const G={
  state:'menu', // menu | aiming | charging | firing | cooldown | turnend | over
  mapType:'island', turnTime:45, winMode:'surv', diff:'normal', teamCount:2,
  teams:[], units:[], current:0, // current team index
  activeUnit:null, activeTeam:null,
  terrain:null, waterLevel:0, waterImg:null,
  wind:0, windTimer:0,
  projectiles:[], particles:[], debris:[], floaters:[], pickups:[], shockwaves:[],
  cam:{x:0,y:0,z:1,tz:1,shake:0,shakeX:0,shakeY:0,freeze:0},
  turnStart:0, turnLeft:0, charge:0,
  scoreTarget:0,
};

// ---------- Team colors ----------
const TEAM_COLORS=[
  {main:'#ff4d5e',dark:'#c0203a',light:'#ff8a96'},
  {main:'#4db4ff',dark:'#1f78d0',light:'#8fd0ff'},
  {main:'#3fe08a',dark:'#1fae60',light:'#8ff0ba'},
  {main:'#ffcf4d',dark:'#d8a020',light:'#ffe58a'},
];
const DEFAULT_NAMES=['RED','BLUE','GREEN','GOLD'];

// ---------- Weapons ----------
// categories: indirect (projectile), direct (instant), aoe (map-altering), utility
const WEAPONS=[
  {id:'shell', name:'Shell', ico:'💣', cost:1, cat:'indirect', dmg:100, radius:90, weight:1,
   desc:'Standard explosive shell. Full power = big.'},
  {id:'gun', name:'Gun', ico:'🔫', cost:1, cat:'direct', dmg:35, range:1400,
   desc:'Instant-hit bullet. Low damage, hitscan.'},
  {id:'dynamite', name:'Dynamite', ico:'🧨', cost:1, cat:'indirect', dmg:110, radius:80, fuse:0.9,
   desc:'Fuse burns — plant it, then BOOM after a delay.'},
  {id:'bazooka', name:'Bazooka', ico:'🚀', cost:1, cat:'indirect', dmg:90, radius:110, weight:1.4,
   desc:'Slow arcing rocket, massive blast.'},
  {id:'nuke', name:'Nuke', ico:'☢️', cost:3, cat:'aoe', dmg:150, radius:200, weight:1.8,
   desc:'Screen-shaking nuke. Devastates the map.'},
  {id:'dynam', name:'TNT', ico:'⬛', cost:2, cat:'aoe', dmg:120, radius:120, fuse:1.2,
   desc:'Throw & detonate for a huge crater.'},
  // utilities
  {id:'rope', name:'Rope', ico:'🪢', cost:1, cat:'utility', desc:'Grappling rope — zip to high ground.'},
  {id:'jetpack', name:'Jetpack', ico:'🎒', cost:2, cat:'utility', desc:'Limited thrust to reach islands.'},
  {id:'bomb', name:'Bomb', ico:'💥', cost:1, cat:'utility', dmg:40, radius:60, desc:'Throw a bomb that home-mines the area.'},
  {id:'shield', name:'Shield', ico:'🛡️', cost:2, cat:'utility', desc:'Absorb your next hit for free.'},
  {id:'banana', name:'Banana', ico:'🍌', cost:0, cat:'utility', desc:'Slap hazard. Classic trap.'},
  {id:'spring', name:'Spring', ico:'🌀', cost:1, cat:'utility', desc:'Magnetic pull toward the nearest enemy.'},
];

// ---------- Terrain ----------
function generateTerrain(type){
  const w=Math.ceil(WORLD_W/TILE), h=Math.ceil(WORLD_H/TILE);
  const t=new Uint8Array(w*h); // 0 air, 1 ground
  // base water-fill level in tiles
  const waterTileY=Math.floor(G.waterLevel/TILE);
  const set=(x,y,v)=>{ if(x>=0&&x<w&&y>=0&&y<h)t[y*w+x]=v; };
  const hgt=(x)=>0; // placeholder
  if(type==='island'){
    // single dome island rising in the middle
    for(let x=0;x<w;x++){
      const nx=x/w;
      const edge=Math.pow(clamp(1-Math.pow(Math.abs(nx-0.5)*2,1.4),0,1),1.0);
      const base=GROUND_X + (1-edge)*360;
      const hill=Math.sin(nx*Math.PI)* -220 *edge;
      let top=Math.round(base+hill + Math.sin(x*0.03)*14 + Math.sin(x*0.11)*7);
      for(let y=0;y<h;y++) set(x,y,y>=top?1:0);
    }
  } else if(type==='two'){
    for(let x=0;x<w;x++){
      const nx=x/w;
      // two humps
      const bump=(cx,wid)=>Math.exp(-Math.pow((nx-cx)/wid,2));
      const h1=bump(0.28,0.16), h2=bump(0.72,0.16);
      const top=Math.round(GROUND_X + 250 - 230*h1 - 230*h2 + Math.sin(x*0.03)*12 + Math.sin(x*0.13)*6);
      for(let y=0;y<h;y++) set(x,y,y>=top?1:0);
    }
  } else { // cave
    for(let x=0;x<w;x++){
      const nx=x/w;
      const topEdge=Math.round(GROUND_X + Math.sin(x*0.02)*180 + Math.sin(x*0.07)*60 + 60);
      // outer shell solid, inner hollow
      for(let y=0;y<h;y++){
        const y0=Math.max(0,topEdge), y1=Math.min(h, topEdge+420);
        set(x,y,(y<y0||y>y1)?1:0);
      }
    }
  }
  G.terrain=t; G.terrainW=w; G.terrainH=h;
}

function isGround(x,y){
  const tx=Math.floor(x/TILE), ty=Math.floor(y/TILE);
  if(tx<0||ty<0||tx>=G.terrainW||ty>=G.terrainH)return false;
  return G.terrain[ty*G.terrainW+tx]===1;
}
function sampleTerrainHeight(x){
  const tx=Math.floor(x/TILE);
  for(let y=0;y<G.terrainH;y++){ if(G.terrain[y*G.terrainW+tx]) return y*TILE; }
  return WORLD_H;
}
// return {hit, x, y} nearest ground point going down from (x,y)
function groundUnder(x,y){
  const tx=Math.floor(x/TILE);
  for(let yw=0;yw<WORLD_H;yw+=TILE){ if(isGround(x,yw))return{x,y:yw}; }
  return{x,y:WORLD_H};
}

// destroy terrain around a circle, produce debris
function destroyTerrain(cx,cy,radius,dmg){
  const w=G.terrainW, h=G.terrainH, t=G.terrain;
  const x0=Math.max(0,Math.floor((cx-radius)/TILE));
  const x1=Math.min(w-1,Math.ceil((cx+radius)/TILE));
  const y0=Math.max(0,Math.floor((cy-radius)/TILE));
  const y1=Math.min(h-1,Math.ceil((cy+radius)/TILE));
  const r2=radius*radius;
  const created=[];
  for(let ty=y0;ty<=y1;ty++){
    for(let tx=x0;tx<=x1;tx++){
      const px=(tx+0.5)*TILE, py=(ty+0.5)*TILE;
      const d2=(px-cx)*(px-cx)+(py-cy)*(py-cy);
      if(d2<=r2){
        // destructive falloff: edge crumbles to water/air
        const falloff=1-Math.sqrt(d2)/radius;
        t[ty*w+tx]= (falloff<0.25 && Math.random()<0.5)? 2 /*water*/ : 0;
        if(falloff>0.5 && Math.random()<0.35){
          created.push({x:px,y:py});
        }
      }
    }
  }
  // spawn debris chunks
  const n=Math.min(40, Math.floor(radius*0.7));
  for(let i=0;i<n;i++){
    const c=created[randi(0,created.length-1)];
    spawnDebris(c.x,c.y,radius);
  }
}
function spawnDebris(x,y,power){
  const count=Math.max(4,Math.floor(power*0.25));
  for(let i=0;i<count;i++){
    const a=Math.random()*TAU, sp=rand(40,220)*Math.min(2,power/60);
    G.debris.push({x,y,vx:Math.cos(a)*sp,vy:Math.sin(a)*sp-rand(20,120),
      life:rand(1.2,2.6),t:Math.random()*TAU,sz:rand(4,9),
      col:Math.random()<0.5?'#6b4a2a':'#3f6b34',spin:rand(-8,8)});
  }
}

// ---------- Worms ----------
function makeWorm(team,i){
  const names=['Slappy','Nibbles','Buzz','Rico','Vix','Gus','Pinky','Sneaky','Dizzy','Fang','Kip','Lox'];
  return {
    name:names[(team*4+i)%names.length]+'-'+(i+1),
    team, alive:true, health:100, maxHealth:100,
    x:0,y:0, vx:0,vy:0,
    dir: i%2===0?1:-1, // facing
    onGround:true, grounded:false,
    angle:Math.PI*0.25, // barrel angle (0 = right, PI = left)
    aimDir:1,
    fuel:3, // jetpack fuel
    shield:0,
    drownT:0, fallDmgT:0,
    animT:0, onSpring:false,
    alive:true,
  };
}

function placeWorms(){
  G.units=[];
  for(const team of G.teams){
    const placed=[];
    for(let i=0;i<4;i++){
      let attempts=0, x, w;
      do{
        x=rand(WORLD_W*0.08, WORLD_W*0.92);
        const gy=groundUnder(x,0).y;
        w={x, y:gy-TILE*1.2};
        attempts++;
      }while(placed.some(p=>dist(p.x,p.y,x,w.y)<140) && attempts<60);
      const worm=makeWorm(team.wteam,i);
      worm.x=w.x; worm.y=w.y;
      G.units.push(worm); placed.push(w);
    }
  }
}

// ---------- Setup / New Game ----------
function startGame(){
  G.teams=G.teams.map((t,i)=>({...t}));
  generateTerrain(G.mapType);
  // water level depends on terrain
  if(G.mapType==='cave'){ G.waterLevel=GROUND_X+420; }
  else if(G.mapType==='island'){ G.waterLevel=GROUND_X+300; }
  else G.waterLevel=GROUND_X+330;
  G.waterImg=buildWaterImage();
  placeWorms();
  G.units.forEach(u=>{ if(u.team===G.teams[0].wteam && G.teams[0].cpu) {} });
  // assign first player
  G.current=0;
  G.activeTeam=G.teams[G.current];
  G.activeUnit=nextAliveUnit(G.current);
  G.wind=rand(-1,1)*60;
  G.windTimer=rand(6,12);
  G.projectiles=[]; G.particles=[]; G.debris=[]; G.floaters=[]; G.pickups=[]; G.shockwaves=[];
  G.turnStart=performance.now()/1000;
  G.turnLeft=G.turnTime; G.charge=0; G.state='aiming';
  G.cam={x:0,y:0,z:1,tz:1,shake:0,shakeX:0,shakeY:0,freeze:0};
  document.getElementById('menu').classList.add('hidden');
  document.getElementById('gameover').classList.add('hidden');
  document.getElementById('hud').classList.remove('hidden');
  buildWeaponBar(); buildActionRow();
  updateHUD();
  showCenter(G.activeTeam.teamName+"'s Turn", 1400);
  S.wind();
}

function nextAliveUnit(teamIdx){
  const team=G.teams[teamIdx];
  const members=G.units.filter(u=>u.team===team.wteam && u.alive);
  if(!members.length)return null;
  // find current member index
  const cur=G.activeUnit;
  if(cur && cur.alive && cur.team===team.wteam){
    const idx=G.units.indexOf(cur);
    // next
    for(let k=1;k<=G.units.length;k++){ const u=G.units[(idx+k)%G.units.length]; if(u.team===team.wteam && u.alive)return u; }
  }
  return members[0];
}

function nextTurn(){
  // advance to next alive team
  let tries=0;
  do{
    G.current=(G.current+1)%G.teams.length;
    tries++;
    if(tries>G.teams.length+2){ endGame(); return; }
  }while(!G.units.some(u=>u.team===G.teams[G.current].wteam && u.alive));
  G.activeTeam=G.teams[G.current];
  G.activeUnit=nextAliveUnit(G.current);
  G.state='aiming'; G.turnStart=performance.now()/1000; G.turnLeft=G.turnTime; G.charge=0;
  G.windTimer-=0.5;
  updateHUD();
  showCenter(G.activeTeam.teamName+"'s Turn", 1300);
  if(G.activeTeam.cpu){ G.aiThinking=true; setTimeout(()=>{ if(G.state==='aiming'&&G.activeTeam.cpu){ G.aiThinking=false; aiThink(); } }, 900); }
}

// ---------- Projectile ----------
function spawnProjectile(owner, ox, oy, angle, power, weapon){
  const speed=UNIT_SPEED*2.4*power;
  const p={
    x:ox+Math.cos(angle)*26, y:oy+Math.sin(angle)*26,
    vx:Math.cos(angle)*speed, vy:Math.sin(angle)*speed,
    weapon, owner:owner.team, life:8, angle,
    spin:0, trail:[], fuse:weapon.fuse||0,
    color:G.teams[owner.team].main,
  };
  G.projectiles.push(p);
  G.state='firing';
  S.fire();
}

// ---------- Explosions ----------
function explode(x,y,weapon,owner){
  const dmg=weapon.dmg, rad=weapon.radius;
  destroyTerrain(x,y,rad,dmg);
  // damage units
  for(const u of G.units){
    if(!u.alive)continue;
    const d=dist(x,y,u.x,u.y);
    if(d<rad){
      let d2=dmg*(1-d/rad);
      // fall/angle damage mitigation if shield
      if(u.shield>0){ u.shield=0; d2=0; S.shield&&S.shield(); }
      damageUnit(u,d2,x,y);
    }
  }
  // effects
  const big=weapon.id==='nuke'||weapon.id==='dynam';
  G.cam.shake+=big?26:10;
  if(big){ G.cam.freeze=Math.max(G.cam.freeze, big?0.28:0.12); S.boom(); }
  else S.explode(big);
  G.shockwaves.push({x,y,r:4,max:rad*1.3,alpha:1,col:big?'#fff0a0':'#ffcf4d'});
  // explosion particles
  const pc=big?90:40;
  for(let i=0;i<pc;i++){
    const a=Math.random()*TAU, sp=rand(60,big?420:260);
    G.particles.push({x,y,vx:Math.cos(a)*sp,vy:Math.sin(a)*sp-rand(20,90),
      life:rand(0.5,1.4),t:Math.random()*2,sz:rand(2,6),
      type:Math.random()<0.5?'fire':'smoke',
      col:['#ffcf4d','#ff7a3d','#ff4d5e','#ffb03d'][randi(0,3)]});
  }
  G.floaters.push({x,y,text:'-'+Math.round(dmg),vy:-40,life:1.4,col:'#fff',big:true});
  // impact freeze handled in update
}

function damageUnit(u,dmg,fromX,fromY){
  if(!u.alive)return;
  u.health-=dmg;
  if(fromX!==undefined){ // knockback
    const a=Math.atan2(u.y-fromY,u.x-fromX);
    u.vx+=Math.cos(a)*dmg*0.9; u.vy+=Math.sin(a)*dmg*0.7-200;
    u.onGround=false;
  }
  u.fallDmgT=0;
  S.hurt();
  if(u.health<=0){ u.health=0; killWorm(u); }
}
function killWorm(u){
  u.alive=false;
  // death particles
  for(let i=0;i<30;i++){
    const a=Math.random()*TAU, sp=rand(40,200);
    G.particles.push({x:u.x,y:u.y,vx:Math.cos(a)*sp,vy:Math.sin(a)*sp-100,
      life:rand(0.6,1.4),t:0,sz:rand(2,5),type:'fire',col:G.teams[u.team].main});
  }
  G.floaters.push({x:u.x,y:u.y-20,text:'KO',vy:-50,life:1.6,col:G.teams[u.team].main,big:true});
  S.hurt();
}

// ---------- Particles / debris / floaters ----------
function spawnDust(x,y,n){
  for(let i=0;i<n;i++){
    const a=-Math.PI*Math.random(), sp=rand(20,120);
    G.particles.push({x,y,vx:Math.cos(a)*sp,vy:Math.sin(a)*sp-30,
      life:rand(0.4,0.9),t:0,sz:rand(2,5),type:'dust',col:'#8a6b4a'});
  }
}
function spawnSpark(x,y,col,n){
  for(let i=0;i<n;i++){
    const a=Math.random()*TAU, sp=rand(60,240);
    G.particles.push({x,y,vx:Math.cos(a)*sp,vy:Math.sin(a)*sp,
      life:rand(0.3,0.8),t:0,sz:rand(1,3),type:'spark',col});
  }
}

// ---------- Water image ----------
function buildWaterImage(){
  const w=Math.ceil(WORLD_W/TILE), h=Math.ceil((WORLD_H-G.waterLevel)/TILE);
  const c=document.createElement('canvas'); c.width=w; c.height=h;
  const x=c.getContext('2d');
  const grd=x.createLinearGradient(0,0,0,h);
  grd.addColorStop(0,'rgba(60,130,200,0.45)'); grd.addColorStop(1,'rgba(20,70,140,0.72)');
  x.fillStyle=grd; x.fillRect(0,0,w,h);
  return {canvas:c,w,h};
}

// ============================================================================
//  UPDATE LOOP
// ============================================================================
let last=performance.now();
function loop(now){
  const dt=Math.min(0.033,(now-last)/1000); last=now;
  if(G.state!=='menu' && G.state!=='over') update(dt);
  render();
  requestAnimationFrame(loop);
}

function update(dt){
  // wind
  G.windTimer-=dt;
  if(G.windTimer<=0){ G.wind=rand(-1,1)*(G.mapType==='cave'?80:60); G.windTimer=rand(6,12); S.wind(); }
  // cam freeze
  if(G.cam.freeze>0) G.cam.freeze=Math.max(0,G.cam.freeze-dt);
  // camera smoothing
  const target=G.activeUnit && G.activeUnit.alive?G.activeUnit:{x:G.cam.x,y:G.cam.y};
  const zoom=G.state==='firing'||G.state==='charging'?1.06:1;
  G.cam.tz=zoom;
  G.cam.x=lerp(G.cam.x,target.x,dt*4);
  G.cam.y=lerp(G.cam.y,target.y,dt*4);
  G.cam.z=lerp(G.cam.z,G.cam.tz,dt*4);
  // shake
  if(G.cam.shake>0){
    G.cam.shakeX+=(rand(-1,1)*G.cam.shake - G.cam.shakeX)*0.2;
    G.cam.shakeY+=(rand(-1,1)*G.cam.shake - G.cam.shakeY)*0.2;
    G.cam.shake=Math.max(0,G.cam.shake-dt*40);
  } else { G.cam.shakeX=0; G.cam.shakeY=0; }

  updateUnits(dt);
  updateProjectiles(dt);
  updateParticles(dt);
  updateDebris(dt);
  updateFloaters(dt);
  updatePickups(dt);
  updateShockwaves(dt);

  // after firing, end turn when projectile gone
  if(G.state==='firing'){
    if(G.awaitTurnEnd && G.projectiles.length===0){
      G.awaitTurnEnd=false;
      endTurn();
    }
  }

  // jetpack while holding R during charging
  const u=G.activeUnit;
  if(jetActive && u && u.alive && (G.state==='charging' || G.state==='firing')){
    u.vy-=900*dt; u.fuel--; u._jetT=0.6;
    if(u.fuel<0)u.fuel=0;
    if(Math.random()<0.7)G.particles.push({x:u.x-4,y:u.y+6,vx:rand(-30,30),vy:rand(60,140),
      life:0.4,t:0,sz:rand(3,6),type:'fire',col:'#ffb03d'});
  }

  updateTurnTimer(dt);
  maybeSpawnPickup();

  // win check
  checkWin();
}

function updateUnits(dt){
  for(const u of G.units){
    if(!u.alive)continue;
    const inWater = u.y > G.waterLevel;
    if(inWater){
      // buoyancy + slow
      u.vy = lerp(u.vy, 40, dt*3);
      u.vx *= (1-dt*1.5);
      u.drownT+=dt;
      if(u.drownT>2.2){ killWorm(u); continue; }
      // float visual
      u.y-=Math.sin(performance.now()/300+u.x)*0.3;
    } else {
      u.drownT=0;
      // gravity (reduced in water handled above)
      u.vy+=UNIT_GRAV*dt;
      if(u.onGround && Math.abs(u.vy)<20){ /* idle */ }
    }
    // apply velocity
    u.x+=u.vx*dt; u.y+=u.vy*dt;
    // world bounds
    if(u.x<20){u.x=20;u.vx*=-0.3;} if(u.x>WORLD_W-20){u.x=WORLD_W-20;u.vx*=-0.3;}
    // ground collision
    const gy=groundUnder(u.x,u.y).y;
    const feet=u.y;
    if(feet>=gy){
      u.y=gy;
      const wasFall=u.vy>MAX_FALL;
      u.vy=0;
      u.onGround=true;
      // friction
      u.vx*=GROUND_FRICTION;
      // fall damage
      if(wasFall){
        const dmg=clamp((u.vy-MAX_FALL)*0.6,0,40);
        if(dmg>3){ damageUnit(u,dmg,u.x,u.y); spawnDust(u.x,u.y,8); }
      }
      if(u.vx>20 && Math.random()<0.2) spawnDust(u.x,u.y,1);
    } else {
      u.onGround=false;
    }
    // air friction (horizontal)
    if(!u.onGround || inWater) u.vx*= (inWater?0.96:0.995);
    u.animT+=dt;
    // jetpack thrust handled in firing
  }
}

function updateProjectiles(dt){
  for(let i=G.projectiles.length-1;i>=0;i--){
    const p=G.projectiles[i];
    p.life-=dt;
    // wind effect
    p.vx+=G.wind*dt;
    p.vy+=(UNIT_GRAV*dt); // gravity on projectile
    if(p.weapon.weight)p.vy+=(p.weapon.weight-1)*120*dt;
    // move
    const dx=p.vx*dt, dy=p.vy*dt;
    p.trail.push({x:p.x,y:p.y}); if(p.trail.length>14)p.trail.shift();
    p.x+=dx; p.y+=dy;
    p.angle=Math.atan2(p.vy,p.vx);
    p.spin+=dt*20;
    // fuse weapons (dynamite)
    if(p.fuse>0){ p.fuse-=dt; if(p.fuse<=0){ explode(p.x,p.y,p.weapon,p.owner); G.projectiles.splice(i,1); continue; } }
    // collision with ground
    if(isGround(p.x,p.y)){
      if(p.fuse>0){ /* keep falling until fuse */ }
      else explode(p.x,p.y,p.weapon,p.owner);
      G.projectiles.splice(i,1); continue;
    }
    // collision with units
    let hit=false;
    for(const u of G.units){
      if(!u.alive||u.team===p.owner)continue;
      if(dist(p.x,p.y,u.x,u.y)<16){
        explode(p.x,p.y,p.weapon,p.owner); hit=true; break;
      }
    }
    if(hit){ G.projectiles.splice(i,1); continue; }
    if(p.y>WORLD_H||p.x<-50||p.x>WORLD_W+50||p.life<=0){
      if(p.fuse<=0) explode(p.x,p.y,p.weapon,p.owner);
      G.projectiles.splice(i,1);
    }
  }
}

function updateParticles(dt){
  for(let i=G.particles.length-1;i>=0;i--){
    const p=G.particles[i];
    p.life-=dt;
    if(p.life<=0){G.particles.splice(i,1);continue;}
    p.vy+= (p.type==='smoke'?-30:300)*dt;
    p.x+=p.vx*dt; p.y+=p.vy*dt;
    p.vx*=0.96; p.vy*=0.96;
    if(p.type==='smoke')p.sz+=dt*20;
  }
}
function updateDebris(dt){
  for(let i=G.debris.length-1;i>=0;i--){
    const d=G.debris[i];
    d.life-=dt;
    if(d.life<=0){G.debris.splice(i,1);continue;}
    d.vy+=GRAV*dt;
    d.x+=d.vx*dt; d.y+=d.vy*dt;
    d.t+=d.spin*dt;
    d.vx*=0.99;
    const gy=groundUnder(d.x,d.y).y;
    if(d.y>=gy){ d.y=gy; d.vy*=-0.35; d.vx*=0.6; if(Math.abs(d.vy)<20)d.vy=0; }
    if(d.life<0.3)G.debris.splice(i,1);
  }
}
function updateFloaters(dt){
  for(let i=G.floaters.length-1;i>=0;i--){
    const f=G.floaters[i]; f.life-=dt; f.y+=f.vy*dt; f.vy*=0.96;
    if(f.life<=0)G.floaters.splice(i,1);
  }
}
function updatePickups(dt){
  for(const p of G.pickups){
    // gentle bob
    p.bobT=(p.bobT||0)+dt;
    // water float
    p.y+=Math.sin(p.bobT*3)*0.4;
    // gravity if below waterline
    if(p.y<G.waterLevel){ p.vy=(p.vy||0)+UNIT_GRAV*dt; p.y+=p.vy*dt; if(p.y>G.waterLevel){p.vy=0;} }
    // pickup by active worm
    const u=G.activeUnit;
    if(u&&u.alive&&dist(u.x,u.y,p.x,p.y)<24){
      applyPickup(u,p.kind);
      spawnSpark(p.x,p.y,'#ffd35c',12);
      const idx=G.pickups.indexOf(p); if(idx>=0)G.pickups.splice(idx,1);
      S.choose();
    }
  }
}
function updateShockwaves(dt){
  for(let i=G.shockwaves.length-1;i>=0;i--){
    const s=G.shockwaves[i]; s.r+=(s.max-s.r)*dt*10; s.alpha-=dt*2.5;
    if(s.alpha<=0)G.shockwaves.splice(i,1);
  }
}

// turn timer
function updateTurnTimer(dt){
  if(G.state==='aiming'||G.state==='charging'||G.state==='action'){
    G.turnLeft-=dt;
    if(G.turnLeft<=0){ G.turnLeft=0; toast('Time up!'); S.warn(); endTurn(); }
  }
}

function endTurn(){
  if(G.state==='firing'||G.state==='cooldown')return;
  G.state='turnend';
  // clear active worm state
  if(G.activeUnit){ G.activeUnit.vx=0; G.activeUnit.vy=0; }
  setTimeout(()=>nextTurn(), 400);
}

function checkWin(){
  const aliveTeams=G.teams.filter(t=>G.units.some(u=>u.team===t.wteam&&u.alive));
  if(G.winMode==='surv'){
    if(aliveTeams.length<=1){
      endGame(aliveTeams[0]||null);
    }
  } else {
    // score mode handled via kills -> tracked; if a team reaches scoreTarget
    for(const t of G.teams){ if(t.score>=G.scoreTarget){ endGame(t); } }
  }
}

// ============================================================================
//  RENDER
// ============================================================================
function render(){
  ctx.save();
  // sky gradient
  const g=ctx.createLinearGradient(0,0,0,VH);
  g.addColorStop(0,'#1a2a4a'); g.addColorStop(0.5,'#2a4a7a'); g.addColorStop(1,'#4a6a9a');
  ctx.fillStyle=g; ctx.fillRect(0,0,VW,VH);
  // stars
  drawStars();
  // distant mountains (parallax)
  drawMountains();

  // world transform
  ctx.save();
  ctx.translate(VW/2, VH*0.55);
  ctx.scale(G.cam.z,G.cam.z);
  ctx.translate(-G.cam.x+G.cam.shakeX, -G.cam.y+G.cam.shakeY);

  drawWorld();

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

  if(G.cam.freeze>0.02){ ctx.fillStyle=`rgba(255,255,255,${G.cam.freeze*0.5})`; ctx.fillRect(0,0,VW,VH); }
}

let starField=null;
function drawStars(){
  if(!starField){ starField=[]; for(let i=0;i<80;i++)starField.push({x:Math.random(),y:Math.random()*0.4,s:Math.random()*1.6+0.3}); }
  ctx.fillStyle='rgba(255,255,255,0.5)';
  for(const s of starField){ ctx.globalAlpha=0.4+Math.random()*0.3; ctx.fillRect(s.x*VW,s.y*VH,s.s,s.s); }
  ctx.globalAlpha=1;
}
function drawMountains(){
  const ox=G.cam.x*0.2;
  ctx.fillStyle='rgba(30,45,75,0.7)';
  ctx.beginPath(); ctx.moveTo(0,VH);
  for(let x=0;x<=VW;x+=40){
    const wx=x+ox;
    const y=VH*0.5+Math.sin(wx*0.0008)*60+Math.sin(wx*0.002)*30;
    ctx.lineTo(x,y);
  }
  ctx.lineTo(VW,VH); ctx.closePath(); ctx.fill();
  ctx.fillStyle='rgba(25,38,64,0.6)';
  ctx.beginPath(); ctx.moveTo(0,VH);
  for(let x=0;x<=VW;x+=40){
    const wx=x+ox*1.6;
    const y=VH*0.6+Math.sin(wx*0.0012)*50+Math.sin(wx*0.003)*24;
    ctx.lineTo(x,y);
  }
  ctx.lineTo(VW,VH); ctx.closePath(); ctx.fill();
}

function drawWorld(){
  // water
  if(G.waterImg){
    const ox=0, oy=G.waterLevel;
    ctx.save();
    ctx.globalAlpha=1;
    // draw water as filled region using terrain mask
    ctx.beginPath();
    // surface follows terrain top
    for(let x=0;x<=WORLD_W;x+=TILE){
      const gy=sampleTerrainHeight(x);
      const sy=Math.max(gy,G.waterLevel);
      if(x===0)ctx.moveTo(x,sy); else ctx.lineTo(x,sy);
    }
    ctx.lineTo(WORLD_W,WORLD_H); ctx.lineTo(0,WORLD_H); ctx.closePath();
    const grd=ctx.createLinearGradient(0,G.waterLevel,0,WORLD_H);
    grd.addColorStop(0,'rgba(70,140,210,0.5)'); grd.addColorStop(1,'rgba(20,70,140,0.78)');
    ctx.fillStyle=grd; ctx.fill();
    // surface shimmer
    ctx.strokeStyle='rgba(150,200,255,0.4)'; ctx.lineWidth=2;
    ctx.beginPath();
    for(let x=0;x<=WORLD_W;x+=TILE){
      const gy=sampleTerrainHeight(x);
      const sy=Math.max(gy,G.waterLevel);
      if(x===0)ctx.moveTo(x,sy); else ctx.lineTo(x,sy);
    }
    ctx.stroke();
    ctx.restore();
  }

  // terrain
  const w=G.terrainW, h=G.terrainH;
  if(w && h){
  // Build offscreen terrain canvas (rebuilt on destruction or generation)
  if(!G._terrCanvas || G._terrW!==w || G._terrH!==h){
    G._terrCanvas=document.createElement('canvas');
    G._terrCanvas.width=w; G._terrCanvas.height=h;
    G._terrW=w; G._terrH=h;
  }
  const tcx=G._terrCanvas.getContext('2d');
  tcx.clearRect(0,0,w,h);
  const img=tcx.createImageData(w,h);
  const d=img.data;
  for(let y=0;y<h;y++){
    for(let x=0;x<w;x++){
      if(G.terrain[y*w+x]===1){
        const idx=(y*w+x)*4;
        if(y<3){ d[idx]=123;d[idx+1]=179;d[idx+2]=74;d[idx+3]=255; }
        else { d[idx]=107;d[idx+1]=74;d[idx+2]=42;d[idx+3]=255; }
      }
    }
  }
  tcx.putImageData(img,0,0);
  ctx.save();
  ctx.imageSmoothingEnabled=false;
  ctx.drawImage(G._terrCanvas,0,0,WORLD_W,WORLD_H);
  // grass top highlight line following surface
  ctx.strokeStyle='#7bb34a'; ctx.lineWidth=4;
  ctx.beginPath();
  for(let x=0;x<=WORLD_W;x+=TILE){
    const gy=sampleTerrainHeight(x);
    if(x===0)ctx.moveTo(x,gy); else ctx.lineTo(x,gy);
  }
  ctx.stroke();
  ctx.restore();
  }

  // draw individual terrain detail (subtle texture via small rects) - optional, skip perf
  drawDebrisWorld();
  drawPickups();
  drawParticles();
  drawProjectiles();
  drawShockwaves();
  drawUnits();
  drawFloaters();

  if(G.state==='aiming'||G.state==='charging'||G.state==='action') drawAim();
}

function drawDebrisWorld(){
  for(const d of G.debris){
    ctx.save(); ctx.translate(d.x,d.y); ctx.rotate(d.t);
    ctx.fillStyle=d.col; ctx.fillRect(-d.sz/2,-d.sz/2,d.sz,d.sz);
    ctx.restore();
  }
}
function drawPickups(){
  for(const p of G.pickups){
    const bob=Math.sin(p.bobT*3)*3;
    ctx.save(); ctx.translate(p.x,p.y+bob);
    ctx.beginPath(); ctx.arc(0,0,14,0,TAU);
    ctx.fillStyle='#0a1220'; ctx.strokeStyle='#ffd35c'; ctx.lineWidth=2; ctx.fill(); ctx.stroke();
    ctx.font='16px serif'; ctx.textAlign='center'; ctx.textBaseline='middle';
    ctx.fillText(p.emoji,0,1);
    ctx.restore();
  }
}
function drawParticles(){
  for(const p of G.particles){
    ctx.save();
    ctx.globalAlpha=clamp(p.life,0,1);
    if(p.type==='smoke'){ ctx.fillStyle='rgba(80,80,80,'+clamp(p.life*0.6,0,0.6)+')'; }
    else if(p.type==='dust'){ ctx.fillStyle='#8a6b4a'; }
    else if(p.type==='spark'){ ctx.fillStyle=p.col; }
    else { ctx.fillStyle=p.col; }
    ctx.beginPath(); ctx.arc(p.x,p.y,p.sz,0,TAU); ctx.fill();
    ctx.restore();
  }
}
function drawProjectiles(){
  for(const p of G.projectiles){
    // trail
    ctx.save();
    for(let i=0;i<p.trail.length;i++){
      const t=p.trail[i], a=i/p.trail.length;
      ctx.globalAlpha=a*0.6;
      ctx.fillStyle=p.weapon.id==='nuke'?'#ffcf4d':'#ffb03d';
      ctx.beginPath(); ctx.arc(t.x,t.y,3*a+1,0,TAU); ctx.fill();
    }
    ctx.restore();
    // bomb
    ctx.save(); ctx.translate(p.x,p.y); ctx.rotate(p.angle);
    ctx.fillStyle='#222'; ctx.beginPath(); ctx.arc(0,0,7,0,TAU); ctx.fill();
    ctx.fillStyle=p.color; ctx.fillRect(-2,-9,4,4);
    if(p.fuse>0 && Math.floor(performance.now()/80)%2){ ctx.fillStyle='#ffcf4d'; ctx.beginPath(); ctx.arc(9,0,3,0,TAU); ctx.fill(); }
    ctx.restore();
  }
}
function drawShockwaves(){
  for(const s of G.shockwaves){
    ctx.save(); ctx.globalAlpha=clamp(s.alpha,0,1);
    ctx.strokeStyle=s.col; ctx.lineWidth=3;
    ctx.beginPath(); ctx.arc(s.x,s.y,s.r,0,TAU); ctx.stroke();
    ctx.restore();
  }
}
function drawUnits(){
  for(const u of G.units){
    if(!u.alive)continue;
    drawWorm(u, u===G.activeUnit);
  }
}
function drawWorm(u, isActive){
  const col=G.teams[u.team];
  ctx.save();
  ctx.translate(u.x,u.y);
  // shadow
  ctx.fillStyle='rgba(0,0,0,0.25)';
  ctx.beginPath(); ctx.ellipse(0,2,12,5,0,0,TAU); ctx.fill();
  const flip=u.dir<0?-1:1;
  ctx.scale(flip,1);
  const walking=Math.abs(u.vx)>10 && u.onGround;
  const legPhase=walking?Math.sin(u.animT*14):0;
  // legs
  ctx.strokeStyle='#2a5a3a'; ctx.lineWidth=4;
  ctx.beginPath();
  ctx.moveTo(-4,0); ctx.lineTo(-6+legPhase*3,8);
  ctx.moveTo(4,0); ctx.lineTo(6-legPhase*3,8);
  ctx.stroke();
  // body
  ctx.fillStyle=col.main;
  ctx.beginPath();
  ctx.moveTo(-11,2);
  ctx.quadraticCurveTo(-13,-14,0,-16);
  ctx.quadraticCurveTo(13,-14,11,2);
  ctx.quadraticCurveTo(0,8,-11,2);
  ctx.fill();
  // stripe
  ctx.fillStyle=col.dark;
  ctx.fillRect(-11,-2,22,4);
  // head / eye
  ctx.fillStyle='#fff';
  ctx.beginPath(); ctx.arc(6,-10,5,0,TAU); ctx.fill();
  ctx.fillStyle='#111';
  ctx.beginPath(); ctx.arc(7.5,-10,2.2,0,TAU); ctx.fill();
  // barrel — drawn OUTSIDE the flip so aim direction is correct
  const aim=aimAngle(u);
  ctx.save();
  ctx.translate(4,-8);
  ctx.rotate(-aim);
  ctx.fillStyle='#333'; ctx.fillRect(0,-3,20,6);
  ctx.fillStyle='#555'; ctx.fillRect(16,-4,4,8);
  ctx.restore();
  // shield bubble
  if(u.shield>0){
    ctx.strokeStyle='rgba(120,200,255,0.8)'; ctx.lineWidth=2;
    ctx.beginPath(); ctx.arc(0,-6,18,0,TAU); ctx.stroke();
  }
  // jetpack
  if(u.fuel>0){ ctx.fillStyle='#888'; ctx.fillRect(-14,-10,5,12); }
  // drown indicator
  if(u.drownT>0){ ctx.fillStyle='rgba(120,200,255,0.7)'; ctx.font='10px sans-serif'; ctx.textAlign='center';
    ctx.fillText('💧',0,-24); }
  ctx.restore();
  // health bar (not for active to avoid clutter? show for all)
  if(!isActive){
    ctx.save(); ctx.translate(u.x,u.y-30);
    ctx.fillStyle='rgba(0,0,0,0.5)'; ctx.fillRect(-14,-3,28,5);
    ctx.fillStyle=col.main; ctx.fillRect(-14,-3,28*(u.health/100),5);
    ctx.restore();
  } else {
    // active worm marker
    ctx.fillStyle=col.light; ctx.font='bold 10px sans-serif'; ctx.textAlign='center';
    ctx.fillText('▲',0,-34);
  }
  // name label
  ctx.fillStyle='rgba(255,255,255,0.85)'; ctx.font='10px sans-serif'; ctx.textAlign='center';
  ctx.fillText(u.name,u.x,u.y-26);
}

function drawAim(){
  const u=G.activeUnit; if(!u||!u.alive)return;
  const ang=aimAngle(u);
  const power=G.state==='charging'?G.charge:0.55;
  const speed=UNIT_SPEED*2.4*power;
  let px=u.x+Math.cos(ang)*26, py=u.y+Math.sin(ang)*26;
  let pvx=Math.cos(ang)*speed, pvy=Math.sin(ang)*speed;
  ctx.save();
  ctx.strokeStyle='rgba(255,255,255,0.5)';
  ctx.setLineDash([4,6]); ctx.lineWidth=2;
  ctx.beginPath(); ctx.moveTo(px,py);
  for(let i=0;i<60;i++){
    pvx+=G.wind*0.016; pvy+=UNIT_GRAV*0.016;
    px+=pvx*0.016; py+=pvy*0.016;
    if(py>WORLD_H||isGround(px,py))break;
    ctx.lineTo(px,py);
  }
  ctx.stroke(); ctx.setLineDash([]);
  // predicted landing marker
  ctx.restore();
}

function drawFloaters(){
  for(const f of G.floaters){
    ctx.save();
    ctx.globalAlpha=clamp(f.life,0,1);
    ctx.fillStyle=f.col;
    ctx.font=(f.big?'bold 22px':'bold 14px')+' sans-serif';
    ctx.textAlign='center';
    ctx.strokeStyle='rgba(0,0,0,0.6)'; ctx.lineWidth=3;
    ctx.strokeText(f.text,f.x,f.y); ctx.fillText(f.text,f.x,f.y);
    ctx.restore();
  }
}
// absolute aim angle from worm facing + barrel angle
function aimAngle(u){
  return u.dir>=0 ? u.angle : Math.PI - u.angle;
}

// ============================================================================
//  INPUT
// ============================================================================
const keys={};
window.addEventListener('keydown',e=>{
  const k=e.key.toLowerCase();
  if(keys[k])return; keys[k]=true;
  if(G.state==='menu'||G.state==='over')return;
  handleInput(k,e);
});
window.addEventListener('keyup',e=>{ keys[e.key.toLowerCase()]=false; });

function handleInput(k,e){
  const u=G.activeUnit;
  if(G.state==='aiming'||G.state==='action'){
    if(k==='arrowright'||k==='d'){ u.dir=1; S.aim(); }
    if(k==='arrowleft'||k==='a'){ u.dir=-1; S.aim(); }
    if(k==='arrowup'||k==='w'){ u.angle=clamp(u.angle-0.03,0.05,Math.PI-0.05); S.aim(); }
    if(k==='arrowdown'||k==='s'){ u.angle=clamp(u.angle+0.03,0.05,Math.PI-0.05); S.aim(); }
    if(k==='q'){ // jump left
      jump(-1);
    }
    if(k==='e'){ jump(1); }
    if(k===' '){ e.preventDefault(); beginCharge(); }
    if(k==='f'){ if(G.state==='aiming')fireNow(); }
    // weapon nav
    if(k>='1'&&k<='9'){ selectWeapon(parseInt(k)-1); }
  } else if(G.state==='charging'){
    if(k==='q'){ jump(-1); }
    if(k==='e'){ jump(1); }
    if(k===' '){ e.preventDefault(); fireNow(); } // release
    if(k==='escape'){ cancelCharge(); }
  }
}
function jump(dir){
  const u=G.activeUnit; if(!u||!u.alive||!u.onGround)return;
  u.vx+=dir*UNIT_SPEED*1.4; u.vy=-JUMP_V; u.onGround=false;
  spawnDust(u.x,u.y,6); S.jump();
}
function beginCharge(){
  if(G.state!=='aiming')return;
  G.state='charging'; G.charge=0.4;
  S.choose();
}
function cancelCharge(){
  G.state='aiming'; G.charge=0; S.click();
}
function fireNow(){
  const u=G.activeUnit; if(!u)return;
  const wp=selectedWeapon();
  if(wp.cost>u.team.credits){ toast('Not enough credits!'); S.warn(); return; }
  u.team.credits-=wp.cost;
  const ang=aimAngle(u);
  const power=G.state==='charging'?clamp(G.charge,0.1,1):0.55;
  spawnProjectile(u, u.x, u.y-10, ang, power, wp);
  // recoil
  u.vx-=Math.cos(ang)*power*40;
  spawnDust(u.x,u.y-10,8);
  G.state='firing';
  G.awaitTurnEnd=true;
}
function weaponFlightTime(wp){ return wp.cat==='direct'?700:1100; }

// ---------- Jetpack / rope / spring / etc utilities ----------
function useUtility(kind){
  const u=G.activeUnit; if(!u||!u.alive)return;
  const wp=WEAPONS.find(w=>w.id===kind);
  if(wp.cost>u.team.credits){ toast('Not enough credits!'); S.warn(); return; }
  u.team.credits-=wp.cost;
  if(kind==='jetpack'){
    if(u.fuel<=0){toast('No fuel!');return;}
    u.vy=-520; u.onGround=false; u._jetT=0.6;
    S.fire();
  } else if(kind==='shield'){
    u.shield=1; toast('Shield active!'); S.choose();
  } else if(kind==='banana'){
    // drop trap in front
    G.pickups.push({x:u.x+u.dir*20,y:u.y-4,vy:-100,kind:'banana',emoji:'🍌',bobT:0});
    S.water();
  } else if(kind==='rope'){
    // zip upward toward highest ground in front
    const targetX=u.x+u.dir*300;
    const gy=sampleTerrainHeight(clamp(targetX,0,WORLD_W));
    u.x=targetX; u.y=gy-20; u.vx=u.dir*300;
    spawnDust(u.x,u.y,10); S.jump();
  } else if(kind==='spring'){
    // pull nearest enemy
    let best=null,bd=1e9;
    for(const o of G.units){ if(!o.alive||o.team===u.team)continue; const d=dist(u.x,u.y,o.x,o.y); if(d<bd){bd=d;best=o;} }
    if(best){ u.vx+=(u.x-best.x)*0.08; u.vy+=(u.y-best.y)*0.06; u.onGround=false; S.spring&&S.spring(); }
  }
  updateHUD();
  if(G.state==='aiming'){ /* utilities don't end turn */ }
}

// jetpack continuous while a key held — handled via charging-like state
let jetActive=false;
window.addEventListener('keydown',e=>{ if(e.key.toLowerCase()==='r' && G.state==='charging'){ jetActive=true; } });
window.addEventListener('keyup',e=>{ if(e.key.toLowerCase()==='r'){ jetActive=false; } });

// update jetpack during firing state
const _origUpdateUnits=updateUnits;

// ---------- Pickups spawn ----------
function maybeSpawnPickup(){
  if(G.pickups.length<2 && Math.random()<0.01){
    const kinds=['jetpack','shield','banana','dynamite','shell'];
    const kind=kinds[randi(0,kinds.length-1)];
    const wp=WEAPONS.find(w=>w.id===kind);
    const x=rand(100,WORLD_W-100);
    const gy=groundUnder(x,0).y;
    G.pickups.push({x,y:gy-40,vy:0,kind,emoji:wp.ico,bobT:0});
    S.choose();
  }
}

// ============================================================================
//  AI
// ============================================================================
function aiThink(){
  const u=G.activeUnit; if(!u)return;
  const team=G.activeTeam;
  const diff=G.diff;
  const acc = diff==='easy'?0.55:diff==='normal'?0.78:0.93;
  // risk assessment: prefer low-value (low HP) targets that are on solid ground,
  // avoiding those near water/cliffs (environmental hazard)
  let targets=G.units.filter(o=>o.alive && o.team!==team.wteam);
  if(!targets.length){ endTurn(); return; }
  // score targets: prefer low HP, penalize near water, prefer close
  function riskScore(t){
    let s=0;
    s += (100-t.health)/100*40;      // prefer damaged
    s += Math.min(1, dist(u.x,u.y,t.x,t.y)/1200)*20; // prefer closer
    // environmental hazard: near water below feet
    const feetY=sampleTerrainHeight(t.x);
    if(t.y > G.waterLevel-10) s -= 60; // standing in water = risky for them anyway (good target) but also we might miss
    if(feetY > G.waterLevel-30 && feetY < G.waterLevel+40) s += 25; // push to shoot near water edge
    return s;
  }
  let best=null,bestS=-1e9;
  for(const t of targets){ const s=riskScore(t); if(s>bestS){bestS=s;best=t;} }
  const t=best;
  const dx=t.x-u.x, dy=t.y-(u.y-10); // vertical drop (down positive)
  const horizontal=dx;
  u.dir = horizontal>=0?1:-1;
  // numerical solve: find angle & power so projectile lands on target
  let bestErr=1e9, bestAngle, bestPower;
  const g=UNIT_GRAV, speedK=UNIT_SPEED*2.4;
  // try a grid of launch angles (0..90 measured from horizontal)
  for(let deg=8; deg<88; deg+=2){
    const a=deg*Math.PI/180;
    const cv=Math.cos(a), sv=Math.sin(a);
    // solve power so that at t when x reaches horizontal:
    // x = cv*speedK*power*t -> t = horizontal/(cv*speedK*power)
    // but power appears in both t and dy. Use parametric: for unit speed=1,
    // x=speedK*power*t, y=sv*speedK*power*t + 0.5*g*power²*t² ... messy.
    // Instead binary search power for this angle.
    let lo=0.1, hi=0.98, mid;
    for(let it=0; it<24; it++){
      mid=(lo+hi)/2;
      const speed=speedK*mid;
      const tx2 = horizontal/(cv*speed);
      if(tx2<=0||tx2>10){ if(lo<hi)lo=mid; continue; }
      const yHit = sv*speed*tx2 + 0.5*g*mid*mid*tx2*tx2; // down positive
      const err=Math.abs(yHit-dy);
      if(err<bestErr){ bestErr=err; bestAngle=a; bestPower=mid; }
      if(yHit<dy) lo=mid; else hi=mid;
    }
  }
  if(!bestAngle){ bestAngle=Math.atan2(Math.abs(dy),Math.abs(horizontal)); bestPower=0.55; }
  // apply facing + barrel angle (u.angle is 0..PI from +x/up)
  u.angle = horizontal>=0 ? (Math.PI/2 - bestAngle) + rand(-0.03,0.03)*(1-acc)
                          : (Math.PI/2 + bestAngle) + rand(-0.03,0.03)*(1-acc);
  let power = bestPower + rand(-0.04,0.04)*(1-acc);
  // wind compensation
  power += G.wind*0.0003*Math.sign(horizontal||1);
  power=clamp(power,0.15,0.98);
  // execute after delay
  const delay=500+Math.random()*700;
  setTimeout(()=>{
    if(G.activeUnit!==u || !u.alive){ return; }
    // risk avoidance: if low health & credits, prefer shield utility
    if(u.health<30 && Math.random()<0.4 && u.team.credits>=2){
      u.team.credits-=2; u.shield=1; updateHUD(); toast(u.name+' uses Shield!'); S.choose();
      setTimeout(()=>endTurn(),500); return;
    }
    fireAt(power);
  }, delay);
}
function fireAt(power){
  const u=G.activeUnit; const wp=selectedWeapon();
  u.team.credits-=wp.cost;
  const ang=aimAngle(u);
  spawnProjectile(u,u.x,u.y-10,ang,power,wp);
  S.fire();
  G.awaitTurnEnd=true;
}

// ============================================================================
//  HUD / UI
// ============================================================================
let selectedWeaponIdx=0;
function selectedWeapon(){ return WEAPONS[selectedWeaponIdx]; }
function selectWeapon(i){
  if(i<0||i>=WEAPONS.length)return;
  selectedWeaponIdx=i; S.choose();
  buildWeaponBar();
}
function buildWeaponBar(){
  const bar=document.getElementById('weaponbar'); bar.innerHTML='';
  WEAPONS.forEach((w,i)=>{
    const el=document.createElement('div');
    el.className='weapon'+(i===selectedWeaponIdx?' sel':'');
    el.innerHTML=`<div class="ico">${w.ico}</div><div class="nm">${w.name}</div>${w.cost?`<div class="cost">${w.cost}</div>`:''}`;
    el.title=w.desc;
    el.onclick=()=>{ selectWeapon(i); if(w.cat==='utility'&&G.state==='aiming')useUtility(w.id); };
    el.addEventListener('mouseenter',()=>{ if(i===selectedWeaponIdx)S.menu(); });
    bar.appendChild(el);
  });
}
function buildActionRow(){
  const row=document.getElementById('actionRow');
  row.innerHTML='';
  const info=document.createElement('div');
  info.style.cssText='background:rgba(14,22,38,.85);border:1px solid rgba(120,160,255,.2);border-radius:10px;padding:8px 14px;font-size:12px;color:var(--ink2);max-width:340px;text-align:center;';
  info.id='weaponInfo';
  row.appendChild(info);
}
function updateWeaponInfo(){
  const info=document.getElementById('weaponInfo'); if(!info)return;
  const w=selectedWeapon();
  const team=G.activeTeam;
  info.innerHTML=`<b style="color:${G.teams[team.wteam].main}">${w.name}</b> — ${w.desc}<br>
    ${w.cat==='indirect'?`Dmg ${w.dmg} · Blast ${w.radius}`:w.cat==='direct'?`Dmg ${w.dmg} · Range ${w.range}`:
    w.cat==='aoe'?`Dmg ${w.dmg} · Blast ${w.radius}`:`Utility`}<br>
    <span style="color:${team.credits>=w.cost?'#3fe08a':'#ff5a6c'}">Credits: ${team.credits}</span> · <b>Space</b> charge & fire`;
}
function updateHUD(){
  const team=G.activeTeam;
  document.getElementById('teamDot').style.background=G.teams[team.wteam].main;
  document.getElementById('teamDot').style.color=G.teams[team.wteam].main;
  document.getElementById('teamName').textContent=team.teamName;
  document.getElementById('whoTxt').textContent=(team.cpu?'CPU: ':'')+(G.activeUnit?G.activeUnit.name:'');
  document.getElementById('timerVal').textContent=Math.ceil(G.turnLeft)+'s';
  buildTeamList();
  updateWeaponInfo();
}
function buildTeamList(){
  const list=document.getElementById('teamList'); list.innerHTML='';
  for(const t of G.teams){
    const alive=G.units.filter(u=>u.team===t.wteam&&u.alive).length;
    const totalHealth=G.units.filter(u=>u.team===t.wteam).reduce((s,u)=>s+u.health,0);
    const maxH=G.units.filter(u=>u.team===t.wteam).length*100;
    const row=document.createElement('div');
    row.className='team-row'+(t===G.activeTeam?' active':'')+((alive===0?' dead':''));
    row.innerHTML=`<span class="swatch" style="background:${G.teams[t.wteam].main};color:${G.teams[t.wteam].main}"></span>
      <span class="nm">${t.teamName}${t.cpu?' 🤖':''}</span>
      <span style="font-size:10px;color:#8a9ab0">${alive}</span>
      <span class="hpbar"><span class="hpfill" style="width:${100*totalHealth/maxH}%"></span></span>`;
    row.onclick=()=>{ if(!t.cpu){ /* hot seat manual switch could go here */ } };
    list.appendChild(row);
  }
}

function updateTimerBar(){
  const pct=clamp(G.turnLeft/G.turnTime,0,1);
  const fill=document.getElementById('timerFill');
  fill.style.width=(pct*100)+'%';
  fill.className='timer-fill'+(pct<0.2?' danger':pct<0.4?' warn':'');
  document.getElementById('timerVal').textContent=Math.ceil(G.turnLeft)+'s';
  // tension scaling
  const scale=1+ (1-pct)*0.15;
  document.getElementById('teamName').style.transform='scale('+scale+')';
  document.getElementById('teamName').style.transformOrigin='left center';
  // tempo
  if(pct<0.25 && Math.random()<0.03)S.countdown();
}

let centerTimer=null;
function showCenter(big,small,dur){
  const el=document.getElementById('centerMsg');
  el.querySelector('.big').textContent=big;
  el.querySelector('.small').textContent=small||'';
  el.style.opacity=1;
  if(centerTimer)clearTimeout(centerTimer);
  if(dur)centerTimer=setTimeout(()=>el.style.opacity=0,dur);
}
let toastTimer=null;
function toast(msg){
  const el=document.getElementById('toast');
  el.textContent=msg; el.style.opacity=1;
  if(toastTimer)clearTimeout(toastTimer);
  toastTimer=setTimeout(()=>el.style.opacity=0,1400);
}

// ---------- Wind gauge ----------
const wc=document.getElementById('windCanvas'); const wcx=wc.getContext('2d');
function drawWindGauge(){
  wcx.clearRect(0,0,70,42);
  const w=G.wind, max=80;
  const nx=35+ (w/max)*22;
  wcx.strokeStyle='rgba(255,255,255,0.3)'; wcx.lineWidth=1;
  wcx.beginPath(); wcx.moveTo(35,21); wcx.lineTo(35,21); wcx.stroke();
  wcx.fillStyle='rgba(255,255,255,0.15)'; wcx.fillRect(10,18,50,6);
  wcx.strokeStyle='#ffcf4d'; wcx.lineWidth=2;
  wcx.beginPath(); wcx.moveTo(35,21); wcx.lineTo(nx,21); wcx.stroke();
  // arrow
  wcx.fillStyle='#ffcf4d'; wcx.beginPath();
  const dir=w>=0?1:-1;
  wcx.moveTo(nx,21); wcx.lineTo(nx-dir*7,16); wcx.lineTo(nx-dir*7,26); wcx.closePath(); wcx.fill();
  wcx.fillStyle='rgba(255,255,255,0.5)'; wcx.font='9px sans-serif'; wcx.textAlign='center';
  wcx.fillText(Math.abs(w).toFixed(0),35,38);
  document.getElementById('windVal').textContent=(w>=0?'→':'←')+' '+Math.abs(w).toFixed(0);
}

// ============================================================================
//  GAME FLOW CONTROL
// ============================================================================
function endTurn(){
  if(G.state==='firing'||G.state==='cooldown'||G.state==='over'||G.state==='menu')return;
  G.state='turnend';
  G.turnLeft=0;
  // cleanup
  G.projectiles=[];
  // ensure credits refresh
  if(G.activeTeam)G.activeTeam.credits+=3;
  updateHUD();
  setTimeout(()=>nextTurn(), 500);
}

function endGame(winner){
  if(G.state==='over')return;
  G.state='over';
  // rank teams by score / health
  const ranked=[...G.teams].sort((a,b)=>(b.score||0)-(a.score||0));
  const sb=document.getElementById('scoreboard'); sb.innerHTML='';
  ranked.forEach((t,i)=>{
    const r=document.createElement('div');
    r.className='score-row'+(t===winner?' winner':'');
    r.innerHTML=`<span class="rank" style="color:${G.teams[t.wteam].main}">${i+1}</span>
      <span class="swatch" style="background:${G.teams[t.wteam].main}"></span>
      <span style="flex:1;font-weight:700">${t.teamName}${t.cpu?' 🤖':''}</span>
      <span class="badge">Score ${t.score||0}</span>`;
    sb.appendChild(r);
  });
  if(winner){
    document.getElementById('winTitle').textContent=winner.teamName+' WINS!';
    document.getElementById('winSub').textContent=G.winMode==='surv'?'Last worm standing!':'Reached the score goal!';
    S.win();
  } else {
    document.getElementById('winTitle').textContent="Draw!";
    document.getElementById('winSub').textContent='No winners this round.';
  }
  setTimeout(()=>{ document.getElementById('gameover').classList.remove('hidden'); }, 800);
  showCenter(winner?winner.teamName+' WINS!':'GAME OVER','',3000);
}

// ============================================================================
//  MAIN MENU LOGIC
// ============================================================================
function setupMenu(){
  // team count
  const tc=document.getElementById('teamCount');
  tc.querySelectorAll('button').forEach(b=>b.onclick=()=>{
    tc.querySelectorAll('button').forEach(x=>x.classList.remove('sel'));
    b.classList.add('sel'); G.teamCount=parseInt(b.dataset.v); S.choose(); renderTeamSetup();
  });
  // map type
  const mt=document.getElementById('mapType');
  mt.querySelectorAll('button').forEach(b=>b.onclick=()=>{
    mt.querySelectorAll('button').forEach(x=>x.classList.remove('sel'));
    b.classList.add('sel'); G.mapType=b.dataset.v; S.choose();
  });
  // turn time
  const tt=document.getElementById('turnTime');
  tt.querySelectorAll('button').forEach(b=>b.onclick=()=>{
    tt.querySelectorAll('button').forEach(x=>x.classList.remove('sel'));
    b.classList.add('sel'); G.turnTime=parseInt(b.dataset.v); S.choose();
  });
  // win mode
  const wm=document.getElementById('winMode');
  wm.querySelectorAll('button').forEach(b=>b.onclick=()=>{
    wm.querySelectorAll('button').forEach(x=>x.classList.remove('sel'));
    b.classList.add('sel'); G.winMode=b.dataset.v; S.choose();
  });
  // difficulty
  const df=document.getElementById('diff');
  df.querySelectorAll('button').forEach(b=>b.onclick=()=>{
    df.querySelectorAll('button').forEach(x=>x.classList.remove('sel'));
    b.classList.add('sel'); G.diff=b.dataset.v; S.choose();
  });
  renderTeamSetup();
  document.getElementById('startBtn').onclick=()=>{ Sound.init(); startGame(); };
  document.getElementById('againBtn').onclick=()=>{ startGame(); };
  document.getElementById('menuBtn').onclick=()=>{
    document.getElementById('gameover').classList.add('hidden');
    document.getElementById('hud').classList.add('hidden');
    document.getElementById('menu').classList.remove('hidden');
    G.state='menu'; S.menu();
  };
}
function renderTeamSetup(){
  const box=document.getElementById('teamSetup'); box.innerHTML='';
  G.teams=[];
  for(let i=0;i<G.teamCount;i++){
    const col=TEAM_COLORS[i];
    const card=document.createElement('div');
    card.className='team-card';
    card.innerHTML=`<div class="tname"><span class="swatch" style="background:${col.main};color:${col.main}"></span>
        <input value="${DEFAULT_NAMES[i]}" data-i="${i}" data-f="name" style="border:0;background:#0a1220;font-weight:800;padding:2px"></div>
      <label>Human / CPU</label>
      <select data-i="${i}" data-f="cpu">
        <option value="0">Human</option>
        <option value="1">CPU</option>
      </select>`;
    box.appendChild(card);
  }
  box.querySelectorAll('input').forEach(inp=>inp.oninput=e=>{
    const i=parseInt(inp.dataset.i); G.teams[i].name=e.target.value;
  });
  box.querySelectorAll('select').forEach(sel=>sel.onchange=e=>{
    const i=parseInt(sel.dataset.i); G.teams[i].cpu=sel.value==='1'; S.choose();
  });
}

// ============================================================================
//  MAIN UPDATE ENHANCEMENT: jetpack, pickups spawn, team credits
// ============================================================================
// wrap update to spawn pickups and handle jetpack
const _origUpdate = update;
function update(dt){
  _origUpdate(dt);
  maybeSpawnPickup();
  // jetpack while holding R during charging
  const u=G.activeUnit;
  if(jetActive && u && u.alive && u._jetT>0){
    u.vy-=800*dt; u.fuel--; u._jetT-=dt;
    if(u.fuel<0)u.fuel=0;
    if(Math.random()<0.6)G.particles.push({x:u.x-4,y:u.y+6,vx:rand(-30,30),vy:rand(60,140),
      life:0.4,t:0,sz:rand(3,6),type:'fire',col:'#ffb03d'});
  }
}

// ============================================================================
//  INIT
// ============================================================================
G.teams=[];
// default teams placeholder for menu
for(let i=0;i<4;i++){ G.teams.push({wteam:i,teamName:DEFAULT_NAMES[i],cpu:true,credits:5,score:0}); }
setupMenu();
requestAnimationFrame(loop);
</script>
</body>
</html>
<!-- agent-meta {"model":"bartowski/Ornith-1.5-35B-A3B","provider":"brigid","persona":"full","sessionId":"91da0f53-258a-4984-beae-20756e4e6dbd","tokensIn":2978962,"tokensOut":58274,"tokensTotal":3037236,"cost":0,"turns":37,"toolCalls":36,"failedToolCalls":2,"timestamp":"2026-08-24T01:09:48.316Z"} -->