← Worms results

Worms

Deepseek v4.1 Flash · 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 113,907 bytes · SHA-256 c6147f1ca253
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
<title>Wiggle Wars — Hot-Seat Artillery</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
html,body{height:100%;overflow:hidden;background:#04050a;color:#eaf0ff;
  font-family:'Trebuchet MS','Segoe UI',system-ui,-apple-system,sans-serif;
  -webkit-user-select:none;user-select:none;-webkit-tap-highlight-color:transparent}
canvas#game{position:fixed;inset:0;width:100%;height:100%;display:block;touch-action:none}

/* ================= HUD ================= */
#hud{position:fixed;inset:0;pointer-events:none;z-index:20}
.glass{background:linear-gradient(180deg,rgba(20,26,44,.86),rgba(10,13,24,.9));
  border:1px solid rgba(160,190,255,.16);border-radius:14px;
  box-shadow:0 8px 26px rgba(0,0,0,.5),inset 0 1px 0 rgba(255,255,255,.09);
  backdrop-filter:blur(9px);-webkit-backdrop-filter:blur(9px)}

#topbar{position:absolute;top:12px;left:50%;transform:translateX(-50%);
  display:flex;gap:14px;align-items:center}
#timerWrap{position:relative;width:250px;height:38px;display:flex;align-items:center;
  padding:0 12px;gap:10px}
#timerIcon{font-size:17px;filter:drop-shadow(0 1px 2px #000);opacity:.9}
#timerCol{flex:1}
#timerBar{height:11px;border-radius:8px;background:rgba(0,0,0,.55);overflow:hidden;
  box-shadow:inset 0 1px 3px rgba(0,0,0,.8),0 0 0 1px rgba(255,255,255,.07)}
#timerFill{display:block;height:100%;width:100%;border-radius:8px;
  background:linear-gradient(90deg,#6ef2a5,#38d9a9 55%,#8ce99a);
  transition:background .35s linear;box-shadow:0 0 10px rgba(90,240,180,.5)}
#timerText{font-size:14px;font-weight:bold;min-width:24px;text-align:right;
  font-variant-numeric:tabular-nums;letter-spacing:.5px;text-shadow:0 1px 3px #000}
#timerWrap.hot #timerFill{background:linear-gradient(90deg,#ffd43b,#ff922b)}
#timerWrap.warn #timerFill{background:linear-gradient(90deg,#ff8787,#f03e3e)}
#timerWrap.warn{animation:pulseWrap .5s ease-in-out infinite alternate}
#timerWrap.crit{animation:pulseWrap .22s ease-in-out infinite alternate;
  border-color:rgba(255,90,90,.6)}
@keyframes pulseWrap{from{transform:scale(1)}to{transform:scale(1.09)}}

#windBox{width:270px;height:38px;display:flex;align-items:center;gap:9px;padding:0 12px;
  position:relative;overflow:hidden}
#windBox:before{content:'';position:absolute;inset:0;background:
  repeating-linear-gradient(90deg,rgba(255,255,255,.045) 0 1px,transparent 1px 14px);pointer-events:none}
.wlabel{font-size:10px;letter-spacing:2.4px;opacity:.62;font-weight:bold}
#windTrack{flex:1;height:12px;position:relative;border-radius:7px;background:rgba(0,0,0,.5);
  box-shadow:inset 0 1px 3px rgba(0,0,0,.8)}
#windTrack:after{content:'';position:absolute;left:50%;top:-3px;width:1px;height:18px;
  background:rgba(255,255,255,.25)}
#windNeedle{position:absolute;top:1px;left:50%;width:12px;height:10px;border-radius:3px;
  background:linear-gradient(180deg,#8cd8ff,#3b7fe0);box-shadow:0 0 9px rgba(110,190,255,.85);
  margin-left:-6px;transition:left .45s cubic-bezier(.4,1.6,.5,1)}
#windVal{font-size:12px;font-weight:bold;min-width:30px;text-align:right;opacity:.9;
  font-variant-numeric:tabular-nums}

#teams{position:absolute;left:12px;top:12px;display:flex;flex-direction:column;gap:7px;width:196px}
.team{position:relative;padding:6px 8px 7px;border-radius:12px;overflow:hidden;
  background:linear-gradient(180deg,rgba(18,23,40,.82),rgba(9,12,22,.86));
  border:1px solid rgba(255,255,255,.09);transition:transform .25s,box-shadow .25s,opacity .35s}
.team .thead{display:flex;justify-content:space-between;align-items:center;font-size:11px;
  font-weight:bold;letter-spacing:1.1px;margin-bottom:5px;text-transform:uppercase}
.team .thead .nm{display:flex;align-items:center;gap:5px}
.team .dot{width:9px;height:9px;border-radius:50%;box-shadow:0 0 8px currentColor}
.team .tot{font-size:10px;opacity:.72;font-variant-numeric:tabular-nums}
.team .units{display:flex;gap:4px}
.uchip{flex:1;height:16px;border-radius:5px;background:rgba(0,0,0,.5);position:relative;
  overflow:hidden;box-shadow:inset 0 1px 2px rgba(0,0,0,.7)}
.uchip i{position:absolute;left:0;top:0;bottom:0;width:100%;border-radius:5px;
  transition:width .3s ease-out;box-shadow:0 0 7px currentColor}
.uchip.dead i{width:0}
.uchip.dead:after{content:'✕';position:absolute;inset:0;font-size:9px;text-align:center;
  line-height:16px;color:#ff8080;opacity:.85}
.team.active{transform:translateX(7px) scale(1.035);
  box-shadow:0 0 0 1px var(--tc), 0 0 24px -2px var(--tc), 0 8px 22px rgba(0,0,0,.55)}
.team.active:after{content:'';position:absolute;left:0;top:0;bottom:0;width:4px;background:var(--tc);
  box-shadow:0 0 12px var(--tc)}
.team.out{opacity:.34;filter:grayscale(.7)}

#bottom{position:absolute;left:50%;bottom:12px;transform:translateX(-50%);
  display:flex;flex-direction:column;align-items:center;gap:7px;pointer-events:none}
#weaponStrip{display:flex;gap:6px;padding:7px;border-radius:15px;background:
  linear-gradient(180deg,rgba(18,23,40,.86),rgba(9,12,22,.92));
  border:1px solid rgba(160,190,255,.16);box-shadow:0 8px 26px rgba(0,0,0,.5),inset 0 1px 0 rgba(255,255,255,.08);
  backdrop-filter:blur(9px);pointer-events:auto}
.wslot{width:50px;height:50px;border-radius:11px;position:relative;cursor:pointer;
  background:linear-gradient(180deg,rgba(255,255,255,.09),rgba(255,255,255,.02));
  border:1px solid rgba(255,255,255,.09);display:flex;align-items:center;justify-content:center;
  transition:transform .14s,box-shadow .2s,background .2s,opacity .2s}
.wslot svg{width:29px;height:29px;filter:drop-shadow(0 1px 2px rgba(0,0,0,.6))}
.wslot:hover{transform:translateY(-4px);background:linear-gradient(180deg,rgba(255,255,255,.18),rgba(255,255,255,.05));
  box-shadow:0 6px 16px rgba(0,0,0,.5)}
.wslot.sel{background:linear-gradient(180deg,rgba(255,214,102,.28),rgba(255,160,60,.12));
  border-color:rgba(255,208,110,.85);box-shadow:0 0 0 1px rgba(255,208,110,.5),0 0 20px -2px rgba(255,190,80,.8)}
.wslot .ammo{position:absolute;right:3px;bottom:2px;font-size:10px;font-weight:bold;
  color:#ffe9a8;text-shadow:0 1px 2px #000;font-variant-numeric:tabular-nums}
.wslot.empty{opacity:.35;cursor:not-allowed}
.wslot.empty:after{content:'';position:absolute;inset:0;border-radius:11px;
  background:repeating-linear-gradient(45deg,rgba(255,0,0,.14) 0 4px,transparent 4px 8px)}
.wkey{position:absolute;left:3px;top:2px;font-size:8px;opacity:.45;font-weight:bold}

#weaponTip{position:absolute;left:50%;bottom:78px;transform:translateX(-50%) translateY(6px);
  padding:6px 12px;border-radius:9px;font-size:12px;opacity:0;transition:.18s;
  background:rgba(6,9,18,.94);border:1px solid rgba(160,190,255,.2);white-space:nowrap;
  box-shadow:0 6px 20px rgba(0,0,0,.6);pointer-events:none;max-width:70vw}
#weaponTip .wn{font-weight:bold;color:#ffd369;letter-spacing:.6px}
#weaponTip .wd{opacity:.72}
#weaponTip.show{opacity:1;transform:translateX(-50%) translateY(0)}

#powerBox{position:absolute;right:14px;bottom:16px;display:flex;flex-direction:column;
  align-items:center;gap:5px;opacity:0;transition:opacity .2s}
#powerBox.show{opacity:1}
#powerBar{width:26px;height:168px;border-radius:13px;background:rgba(6,9,18,.85);
  border:1px solid rgba(160,190,255,.2);position:relative;overflow:hidden;
  box-shadow:0 8px 22px rgba(0,0,0,.55),inset 0 1px 0 rgba(255,255,255,.08)}
#powerFill{position:absolute;left:0;right:0;bottom:0;height:0%;
  background:linear-gradient(0deg,#3ddc97,#ffd43b 55%,#ff5c5c);transition:height .05s linear;
  box-shadow:0 0 14px rgba(255,180,80,.55)}
#powerNum{font-size:13px;font-weight:bold;text-shadow:0 1px 3px #000;font-variant-numeric:tabular-nums}

#banner{position:absolute;left:0;right:0;top:32%;text-align:center;pointer-events:none;
  opacity:0;transition:opacity .3s}
#banner .bt{font-size:min(7.4vw,86px);font-weight:900;letter-spacing:3px;line-height:1;
  text-transform:uppercase;text-shadow:0 4px 0 rgba(0,0,0,.45),0 10px 40px rgba(0,0,0,.7);
  background:linear-gradient(180deg,#fff,var(--bc,#7fd0ff));-webkit-background-clip:text;
  background-clip:text;color:transparent;transform:scale(.9)}
#banner .bs{margin-top:8px;font-size:min(2.4vw,20px);letter-spacing:6px;opacity:.8;
  text-transform:uppercase;font-weight:bold}
#banner.show{opacity:1}
#banner.show .bt{animation:banIn .5s cubic-bezier(.2,1.7,.4,1) forwards}
@keyframes banIn{from{transform:scale(.7) rotate(-2deg);opacity:0}
  to{transform:scale(1) rotate(0);opacity:1}}

#toasts{position:absolute;right:14px;top:12px;display:flex;flex-direction:column;gap:6px;
  align-items:flex-end;width:250px}
.toast{padding:7px 12px;border-radius:10px;font-size:12.5px;font-weight:bold;
  background:linear-gradient(180deg,rgba(20,26,44,.92),rgba(10,13,24,.94));
  border:1px solid rgba(160,190,255,.18);box-shadow:0 6px 18px rgba(0,0,0,.5);
  animation:toastIn .35s cubic-bezier(.2,1.6,.4,1),toastOut .4s 2.6s forwards;
  max-width:250px}
@keyframes toastIn{from{transform:translateX(30px);opacity:0}to{transform:none;opacity:1}}
@keyframes toastOut{to{transform:translateX(30px);opacity:0}}

#hint{position:absolute;left:14px;bottom:14px;font-size:11.5px;line-height:1.65;opacity:.62;
  background:rgba(6,9,18,.6);padding:8px 11px;border-radius:10px;border:1px solid rgba(255,255,255,.07);
  max-width:245px}
#hint b{color:#ffd369}
#menuBtns{position:absolute;right:14px;top:12px;display:flex;gap:6px;pointer-events:auto;
  opacity:0;transition:opacity .3s}
#menuBtns.show{opacity:1}
.mini{padding:6px 10px;font-size:11px;border-radius:9px;cursor:pointer;font-weight:bold;
  background:linear-gradient(180deg,rgba(26,34,56,.92),rgba(12,16,30,.94));color:#dfe8ff;
  border:1px solid rgba(160,190,255,.18);transition:.15s}
.mini:hover{transform:translateY(-2px);border-color:rgba(200,220,255,.5);color:#fff}

/* ================= SCREENS ================= */
.screen{position:fixed;inset:0;z-index:40;display:flex;align-items:center;justify-content:center;
  background:radial-gradient(circle at 50% 30%,rgba(40,60,120,.34),rgba(3,4,9,.93) 70%),
  linear-gradient(160deg,#070a16,#0b0518 60%,#04060d);
  backdrop-filter:blur(3px);transition:opacity .4s,visibility .4s}
.screen.hide{opacity:0;visibility:hidden;pointer-events:none}
.card{width:min(880px,94vw);max-height:94vh;overflow:auto;padding:26px 30px 22px;
  border-radius:22px;background:linear-gradient(180deg,rgba(19,25,44,.92),rgba(8,10,20,.95));
  border:1px solid rgba(160,190,255,.18);
  box-shadow:0 30px 80px rgba(0,0,0,.7),inset 0 1px 0 rgba(255,255,255,.1)}
h1{font-size:min(11vw,74px);line-height:.92;font-weight:900;letter-spacing:-1px;text-align:center}
h1 span{display:block;background:linear-gradient(100deg,#7bf1c0,#7fd0ff 40%,#c58cff 75%,#ff9ad0);
  -webkit-background-clip:text;background-clip:text;color:transparent;
  filter:drop-shadow(0 6px 24px rgba(120,180,255,.35))}
h1 .a{font-size:.55em;letter-spacing:.28em;font-weight:800;color:#9fb4dd;display:block;
  margin-bottom:4px;text-shadow:none}
.tag{text-align:center;font-size:13px;letter-spacing:2.4px;text-transform:uppercase;
  opacity:.6;margin:10px 0 20px}
.grid2{display:grid;grid-template-columns:1.05fr .95fr;gap:22px}
@media(max-width:720px){.grid2{grid-template-columns:1fr}}
h3{font-size:11px;letter-spacing:2.6px;text-transform:uppercase;opacity:.65;margin:0 0 9px;
  font-weight:bold}
.trow{display:flex;align-items:center;gap:9px;padding:7px 10px;border-radius:11px;margin-bottom:7px;
  background:rgba(255,255,255,.035);border:1px solid rgba(255,255,255,.07);transition:.2s}
.trow .chip{width:15px;height:15px;border-radius:5px;box-shadow:0 0 10px currentColor;flex:0 0 auto}
.trow .tn{flex:1;font-size:13.5px;font-weight:bold;letter-spacing:.7px}
.trow.off{opacity:.36}
.tgl{display:flex;border-radius:9px;overflow:hidden;border:1px solid rgba(255,255,255,.12)}
.tgl button{padding:5px 10px;font-size:10.5px;font-weight:bold;letter-spacing:.8px;cursor:pointer;
  background:transparent;color:#a9b8d8;border:0;transition:.15s}
.tgl button.on{background:linear-gradient(180deg,rgba(120,210,255,.32),rgba(60,130,255,.2));color:#fff}
.tgl button:hover{color:#fff}
.keys{list-style:none;font-size:12.5px;line-height:1.9;opacity:.86}
.keys li{margin-bottom:2px}
.keys b{display:inline-block;min-width:78px;color:#ffd369;font-weight:bold}
.wgrid{display:grid;grid-template-columns:1fr 1fr;gap:6px;font-size:11.5px}
.wgrid div{display:flex;gap:8px;align-items:center;opacity:.88}
.wgrid svg{width:21px;height:21px;flex:0 0 auto;filter:drop-shadow(0 1px 2px rgba(0,0,0,.6))}
.row{display:flex;gap:12px;justify-content:center;margin-top:22px;flex-wrap:wrap}
button.big{padding:14px 34px;font-size:16px;font-weight:bold;letter-spacing:2px;border-radius:14px;
  cursor:pointer;color:#04211a;border:0;background:linear-gradient(180deg,#8cf7c5,#31c98d);
  box-shadow:0 10px 26px rgba(40,220,150,.32),inset 0 1px 0 rgba(255,255,255,.6);transition:.16s}
button.big:hover{transform:translateY(-3px) scale(1.02);box-shadow:0 16px 34px rgba(40,220,150,.45)}
button.big:active{transform:translateY(0) scale(.99)}
button.ghost{padding:14px 22px;font-size:13px;font-weight:bold;letter-spacing:1.5px;border-radius:14px;
  cursor:pointer;color:#cfe0ff;background:rgba(255,255,255,.06);
  border:1px solid rgba(160,190,255,.22);transition:.16s}
button.ghost:hover{background:rgba(255,255,255,.13);color:#fff;transform:translateY(-2px)}
#over .card{width:min(620px,94vw);text-align:center}
#winTitle{font-size:min(9vw,56px);font-weight:900;letter-spacing:1px}
#winSub{font-size:13px;opacity:.65;letter-spacing:2px;text-transform:uppercase;margin-top:8px}
#stats{margin:18px auto 0;font-size:12.5px;line-height:2;max-width:420px;text-align:left}
#stats .sr{display:flex;justify-content:space-between;border-bottom:1px dashed rgba(255,255,255,.1)}
#stats .sr b{color:#ffd369}
.credit{text-align:center;font-size:10.5px;opacity:.4;letter-spacing:1.6px;margin-top:16px}
.scroll::-webkit-scrollbar{width:9px}
.scroll::-webkit-scrollbar-thumb{background:rgba(140,180,255,.3);border-radius:9px}
</style>
</head>
<body>
<canvas id="game"></canvas>

<div id="hud">
  <div id="topbar">
    <div id="timerWrap" class="glass">
      <span id="timerIcon">⏳</span>
      <div id="timerCol">
        <div id="timerBar"><i id="timerFill"></i></div>
      </div>
      <span id="timerText">45</span>
    </div>
    <div id="windBox" class="glass">
      <span class="wlabel">WIND</span>
      <div id="windTrack"><i id="windNeedle"></i></div>
      <span id="windVal">0</span>
    </div>
  </div>

  <div id="teams"></div>

  <div id="menuBtns">
    <button class="mini" id="btnMute">🔊</button>
    <button class="mini" id="btnQuit">MENU</button>
  </div>

  <div id="toasts"></div>

  <div id="hint">
    <b>A / D</b> walk &nbsp; <b>W</b> jump<br>
    <b>Mouse</b> aim &nbsp; <b>Hold LMB</b> charge<br>
    <b>1-9</b> weapons &nbsp; <b>RMB</b> cancel
  </div>

  <div id="banner"><div class="bt">TEAM</div><div class="bs">prepare</div></div>
  <div id="weaponTip"><span class="wn"></span> <span class="wd"></span></div>

  <div id="bottom">
    <div id="weaponStrip"></div>
  </div>

  <div id="powerBox">
    <div id="powerBar"><i id="powerFill"></i></div>
    <div id="powerNum">0</div>
  </div>
</div>

<div id="title" class="screen">
  <div class="card scroll">
    <h1><span class="a">HOT-SEAT ARTILLERY</span><span>WIGGLE WARS</span></h1>
    <p class="tag">4 worms per team · destructible earth · shifting wind</p>
    <div class="grid2">
      <div>
        <h3>Combatants</h3>
        <div id="teamSetup"></div>
        <h3 style="margin-top:16px">CPU Skill</h3>
        <div id="diffRow" class="tgl" style="display:inline-flex"></div>
      </div>
      <div>
        <h3>Arsenal</h3>
        <div class="wgrid" id="arsenal"></div>
        <h3 style="margin-top:16px">Controls</h3>
        <ul class="keys">
          <li><b>A / D</b> walk left / right</li>
          <li><b>W / ↑</b> jump (mind fall damage)</li>
          <li><b>Mouse</b> aim · hold <b>LMB</b> to charge</li>
          <li><b>1-9</b> weapon · <b>RMB</b> cancel shot</li>
          <li><b>T</b> teleport · <b>E</b> jetpack thrust</li>
        </ul>
      </div>
    </div>
    <div class="row">
      <button id="btnStart" class="big">► START BATTLE</button>
      <button id="btnSound" class="ghost">🔊 SOUND ON</button>
    </div>
    <div class="credit">built with love, canvas &amp; web audio — no assets, all procedural</div>
  </div>
</div>

<div id="over" class="screen hide">
  <div class="card">
    <div id="winTitle">VICTORY</div>
    <div id="winSub">last worm standing</div>
    <div id="stats"></div>
    <div class="row">
      <button id="btnRematch" class="big">↻ REMATCH</button>
      <button id="btnMenu" class="ghost">MENU</button>
    </div>
  </div>
</div>

<script>
/* =====================================================================
   WIGGLE WARS — single-file hot-seat artillery
   ===================================================================== */
'use strict';
const W = 1280, H = 720; let WATER_Y = 598;

/* ------------------------------ utils ------------------------------ */
const clamp = (v,a,b)=>v<a?a:v>b?b:v;
const lerp  = (a,b,t)=>a+(b-a)*t;
const dist  = (a,b,c,d)=>Math.hypot(a-c,b-d);
const TAU   = Math.PI*2;
function mulberry32(a){return function(){a|=0;a=a+0x6D2B79F5|0;let t=Math.imul(a^a>>>15,1|a);
  t=t+Math.imul(t^t>>>7,61|t)^t;return((t^t>>>14)>>>0)/4294967296}}
let RNG = mulberry32(12345);
const rr = (a=1,b=0)=>b+RNG()*(a-b);
const ri = (a,b)=>Math.floor(rr(b+1,a));
const pick = arr=>arr[Math.floor(RNG()*arr.length)];
function hash2(x,y){let h=x*374761393+y*668265263;h=(h^(h>>>13))*1274126177;return((h^(h>>>16))>>>0)/4294967296}
function makeFbm(rng,smooth){
  const oct=[];for(let i=0;i<4;i++)oct.push({f:Math.pow(2,i)*(smooth||0.9),p:rng()*TAU,a:1/Math.pow(1.85,i)});
  return x=>{let s=0,t=0;for(const o of oct){s+=Math.sin(x*o.f+o.p)*o.a;t+=o.a}return s/t};
}
const fmt = n=>Math.max(0,Math.round(n));

/* ------------------------------ audio ------------------------------ */
const Sound = (()=>{
  let AC=null,master=null,muted=false,noiseBuf=null,windGain=null,windFilt=null;
  function init(){
    if(AC) { if(AC.state==='suspended') AC.resume(); return; }
    const C = window.AudioContext||window.webkitAudioContext; if(!C) return;
    AC = new C();
    master = AC.createGain(); master.gain.value=.5; master.connect(AC.destination);
    const len = Math.floor(AC.sampleRate*2);
    noiseBuf = AC.createBuffer(1,len,AC.sampleRate);
    const d = noiseBuf.getChannelData(0);
    for(let i=0;i<len;i++) d[i]=Math.random()*2-1;
    const src = AC.createBufferSource(); src.buffer=noiseBuf; src.loop=true;
    windFilt = AC.createBiquadFilter(); windFilt.type='lowpass'; windFilt.frequency.value=420;
    windGain = AC.createGain(); windGain.gain.value=0;
    src.connect(windFilt); windFilt.connect(windGain); windGain.connect(master);
    src.start();
  }
  const on = ()=>AC && !muted;
  function tone(f1,f2,dur,type,vol,delay){
    if(!on())return; const t=AC.currentTime+(delay||0);
    const o=AC.createOscillator(), g=AC.createGain();
    o.type=type||'sine'; o.frequency.setValueAtTime(Math.max(20,f1),t);
    if(f2&&f2!==f1) o.frequency.exponentialRampToValueAtTime(Math.max(20,f2),t+dur);
    g.gain.setValueAtTime(0.0001,t);
    g.gain.linearRampToValueAtTime(vol,t+Math.min(dur*0.15,0.012));
    g.gain.exponentialRampToValueAtTime(0.0001,t+dur);
    o.connect(g); g.connect(master); o.start(t); o.stop(t+dur+0.06);
  }
  function nz(dur,vol,f1,f2,type,q,delay,rate){
    if(!on())return; const t=AC.currentTime+(delay||0);
    const s=AC.createBufferSource(); s.buffer=noiseBuf; s.playbackRate.value=rate||1;
    const f=AC.createBiquadFilter(); f.type=type||'lowpass'; f.Q.value=q||1;
    f.frequency.setValueAtTime(Math.max(30,f1),t);
    if(f2) f.frequency.exponentialRampToValueAtTime(Math.max(30,f2),t+dur);
    const g=AC.createGain();
    g.gain.setValueAtTime(0.0001,t);
    g.gain.linearRampToValueAtTime(vol,t+Math.min(dur*0.1,0.02));
    g.gain.exponentialRampToValueAtTime(0.0001,t+dur);
    s.connect(f); f.connect(g); g.connect(master); s.start(t); s.stop(t+dur+0.05);
  }
  return {
    init, on,
    get muted(){return muted},
    toggle(){ muted=!muted; if(master) master.gain.value = muted?0:0.5; return muted; },
    setWind(v){ if(!AC)return; const a=Math.abs(v);
      windGain.gain.setTargetAtTime(0.012+a*0.085, AC.currentTime, .25);
      windFilt.frequency.setTargetAtTime(280+a*620, AC.currentTime, .4); },
    hover(){ tone(760,900,.05,'square',.035); },
    select(){ tone(520,780,.07,'square',.05); tone(1040,1240,.09,'triangle',.035,.04); },
    back(){ tone(420,220,.11,'square',.045); },
    deny(){ tone(180,110,.16,'sawtooth',.05); },
    tick(hi){ tone(hi?1420:1000, hi?1420:1000,.035,'square',.045); },
    tickUrgent(n){ tone(1500+n*60,1200,.05,'square',.06); },
    jump(){ tone(250,620,.14,'sine',.055); nz(.1,.03,600,1600,'bandpass',2); },
    land(){ nz(.13,.055,400,90,'lowpass',1); tone(120,60,.1,'sine',.05); },
    step(){ nz(.05,.028,700,300,'bandpass',1.4); },
    fire(){ nz(.22,.14,1800,140,'lowpass',1); tone(200,70,.2,'triangle',.08); },
    launch(){ nz(.3,.11,1400,180,'highpass',1); tone(420,120,.25,'sawtooth',.05); },
    pellet(){ nz(.08,.1,2600,600,'bandpass',1.6); tone(320,120,.06,'square',.05); },
    mg(){ nz(.045,.07,1800,700,'bandpass',1.2); },
    pop(){ tone(900,1600,.06,'sine',.05); },
    boom(size){
      const s = size||1;
      nz(.55*s,.28,900/(s*0.6+0.6),60,'lowpass',1,null,1/Math.max(.6,s*.5));
      tone(120/s,42,.5*s,'sine',.24);
      nz(.22,.1,3200,400,'highpass',.7,.02);
    },
    crumble(){ nz(.45,.09,300,80,'lowpass',1.2); },
    splash(){ nz(.5,.13,2000,300,'bandpass',1.1); tone(700,180,.3,'sine',.05); },
    drown(){ for(let i=0;i<7;i++) tone(300+RNG()*500, 700+RNG()*600, .07,'sine',.045, i*0.14); },
    hurt(){ tone(300,120,.16,'square',.07); },
    heal(){ tone(560,1200,.22,'triangle',.07); },
    death(){ tone(420,70,.6,'sawtooth',.09); nz(.4,.09,700,120,'lowpass',1,.05); },
    fuseBeep(t){ tone(1500,1500,.05,'square',.09); },
    tele(){ [0,.05,.1,.16].forEach((d,i)=>tone(500+i*260, 1100+i*320, .12,'triangle',.05,d)); },
    whoosh(){ nz(.9,.09,300,1400,'bandpass',1.6); },
    whistle(){ tone(1500,320,1.05,'sine',.05); },
    plane(){ nz(2.2,.07,220,300,'lowpass',1); tone(90,80,1.6,'sawtooth',.035); },
    craten(){ tone(880,1320,.16,'triangle',.07); tone(1320,1760,.14,'triangle',.05,.14); },
    horn(){ [392,523,659,784].forEach((f,i)=>tone(f,f,.3,'triangle',.075,i*.11)); },
    win(){ [523,659,784,1046,1318].forEach((f,i)=>tone(f,f*1.01,.34,'triangle',.08,i*.13)); },
    lose(){ [500,420,340,220].forEach((f,i)=>tone(f,f*.9,.4,'sawtooth',.06,i*.16)); }
  };
})();

/* ------------------------------ terrain ------------------------------ */
const TC = document.createElement('canvas'); TC.width=W; TC.height=H;
const tctx = TC.getContext('2d',{willReadFrequently:true});
let mask = new Uint8Array(W*H);
let noSpawn = [];
let theme = null;
let shapeName = 'island';

const THEMES = [
  {name:'Verdant', grass:[96,182,92], grass2:[58,132,66], dirt:[132,94,58], dirt2:[96,66,42],
    rock:[88,80,84], rock2:[40,38,46], sky:['#122a4d','#3f6d9e','#a8cbe8'], sun:'#ffe9b0',
    haze:'#7fa8cf', water:['#2f7fb5','#123b63'], far:['#28486e','#1b3455'], star:0},
  {name:'Ember', grass:[206,150,72], grass2:[164,104,48], dirt:[150,92,58], dirt2:[104,58,40],
    rock:[96,72,66], rock2:[46,32,34], sky:['#2a1024','#8a3a2e','#e8a45c'], sun:'#ffd07a',
    haze:'#c98b5a', water:['#3b6ea8','#123055'], far:['#5a2c33','#3a1c25'], star:0},
  {name:'Glacier', grass:[222,242,255], grass2:[170,208,236], dirt:[198,208,220], dirt2:[148,160,180],
    rock:[120,132,150], rock2:[62,72,92], sky:['#0d1c3a','#3a6f9e','#cfe8f7'], sun:'#ffffff',
    haze:'#bcd8ee', water:['#2b8fc0','#0d3a5c'], far:['#3a5f88','#25425f'], star:0},
  {name:'Cinder', grass:[124,96,96], grass2:[86,64,68], dirt:[92,74,74], dirt2:[62,48,52],
    rock:[100,88,92], rock2:[42,36,44], sky:['#1a0f1e','#5c2333','#c85c3c'], sun:'#ff9a52',
    haze:'#a05545', water:['#8a3a2a','#3a1210'], far:['#3d1a26','#26101a'], star:0},
  {name:'Midnight', grass:[126,110,220], grass2:[86,72,168], dirt:[92,80,140], dirt2:[62,54,104],
    rock:[96,92,136], rock2:[44,40,80], sky:['#050318','#1b1c4a','#4a3f8a'], sun:'#c9b8ff',
    haze:'#6a5fb0', water:['#5a3fd0','#1a1046'], far:['#241d54','#170f36'], star:1},
];

function solidAt(x,y){
  x|=0; y|=0;
  if(y<0) return 0; if(y>=H) return 1;
  if(x<0) return 0; if(x>=W) return 0;
  return mask[y*W+x];
}
function carveDisc(cx,cy,r,wob,fbm){
  const x0=clamp(Math.floor(cx-r-6),0,W-1), x1=clamp(Math.ceil(cx+r+6),0,W-1);
  const y0=clamp(Math.floor(cy-r-6),0,H-1), y1=clamp(Math.ceil(cy+r+6),0,H-1);
  for(let y=y0;y<=y1;y++){
    for(let x=x0;x<=x1;x++){
      const d=Math.hypot(x-cx,y-cy);
      const rad = wob&&fbm ? r*(1+wob*fbm(x*0.035+y*0.021)) : r;
      if(d<=rad) mask[y*W+x]=0;
    }
  }
}
function fillDisc(cx,cy,r){
  const x0=clamp(Math.floor(cx-r),0,W-1), x1=clamp(Math.ceil(cx+r),0,W-1);
  const y0=clamp(Math.floor(cy-r),0,H-1), y1=clamp(Math.ceil(cy+r),0,H-1);
  for(let y=y0;y<=y1;y++)for(let x=x0;x<=x1;x++)
    if(Math.hypot(x-cx,y-cy)<=r) mask[y*W+x]=1;
}
function genTerrain(shape,seed){
  RNG = mulberry32(seed);
  shapeName = shape;
  mask.fill(0);
  noSpawn=[];
  const f1 = makeFbm(RNG,0.006), fd = makeFbm(RNG,1);
  if(shape==='island' || shape==='two'){
    const surfs = [];
    if(shape==='island') surfs.push({c:W*0.5,half:W*0.46,amp:250});
    else { surfs.push({c:W*0.255,half:W*0.225,amp:255}); surfs.push({c:W*0.745,half:W*0.225,amp:255}); }
    for(let x=0;x<W;x++){
      let y = H+400;
      for(const s of surfs){
        const d=Math.abs(x-s.c)/s.half;
        if(d>=1) continue;
        const fall=Math.pow(Math.cos(d*Math.PI*0.5),0.95);
        let sy = H-30 - fall*(s.amp + 52*f1(x*1.0));
        sy += 11*f1(x*6.0) + 5*f1(x*17.0);
        if(sy<y) y=sy;
      }
      y=Math.max(60,y);
      for(let yy=Math.floor(y);yy<H;yy++) if(yy>=0) mask[yy*W+x]=1;
    }
    // solid floating rock platforms
    noSpawn=[];
    for(let i=0;i<2;i++){
      const cx=W*(0.24+i*0.52)+rr(70,-70), cy=WATER_Y-rr(330,230), rx=rr(115,75), ry=rr(26,15);
      noSpawn.push([cx-rx-58,cx+rx+58]);
      for(let x=Math.floor(cx-rx);x<=cx+rx;x++){
        if(x<2||x>=W-2) continue;
        const k=(x-cx)/rx;
        const h=ry*(1-k*k)*(0.65+0.5*f1(x*0.05+i*3));
        const top=cy-h*0.55;
        for(let y=Math.floor(top);y<=cy+h;y++) if(y>2&&y<H-2) mask[y*W+x]=1;
      }
    }
  } else { // cave
    for(let y=0;y<H;y++)for(let x=0;x<W;x++) mask[y*W+x]=1;
    // uneven ceiling
    for(let x=0;x<W;x++){
      const top = 46 + 34*f1(x*2.4) + 14*f1(x*9);
      for(let y=0;y<top;y++) mask[y*W+x]=0;
    }
    // winding caverns
    let px=W*0.5, py=200;
    for(let i=0;i<26;i++){
      const r=rr(96,52);
      carveDisc(px,py,r,0.22,fd);
      px += rr(1,-1)<0 ? rr(180,70) : -rr(180,70);
      py += rr(90,-90);
      px=clamp(px,110,W-110); py=clamp(py,130,WATER_Y-110);
    }
    // side chambers
    for(let i=0;i<14;i++){
      carveDisc(rr(W-90,90), rr(WATER_Y-120,120), rr(80,42), 0.3, fd);
    }
    // hanging stalactites / spires
    for(let i=0;i<26;i++){
      const x=rr(W-40,40), y=rr(WATER_Y-90,120), r=rr(16,6);
      fillDisc(x,y,r); fillDisc(x,y-r*2,r*0.7);
    }
    // lake basin below the waterline
    for(let y=Math.floor(WATER_Y-26);y<H-14;y++)for(let x=0;x<W;x++) mask[y*W+x]=0;
    for(let i=0;i<22;i++){ // pillars + rock spires in the lake
      const x=rr(W-60,60);
      fillDisc(x, WATER_Y+rr(40,0), rr(26,10));
    }
    for(let y=H-14;y<H;y++)for(let x=0;x<W;x++) mask[y*W+x]=1;
  }
  theme = THEMES[Math.floor(RNG()*THEMES.length)];
  paintRegion(0,W-1);
}

/* paint terrain texture */
function colPick(d, x,y, topY,botY, i, w){
  const t=theme, n=hash2(x,y), n2=hash2(y*3+11,x*7-5);
  const gr=[t.grass[0],t.grass[1],t.grass[2]], gd=[t.grass2[0],t.grass2[1],t.grass2[2]];
  const dirt=[t.dirt[0],t.dirt[1],t.dirt[2]], dirt2=[t.dirt2[0],t.dirt2[1],t.dirt2[2]];
  const rk=[t.rock[0],t.rock[1],t.rock[2]], rk2=[t.rock2[0],t.rock2[1],t.rock2[2]];
  // distance to nearest surface (this column and neighbours)
  let ed=999;
  for(let k=-5;k<=5;k++){
    const j=i+k; if(j<0||j>=w) continue;
    const tt=topY[j], bb=botY[j];
    if(tt>=0) ed=Math.min(ed,Math.abs(y-tt));
    if(bb>=0) ed=Math.min(ed,Math.abs(y-bb));
  }
  let r,g,b;
  if(y===topY[i]){
    const lit=.22+n*.28;
    r=lerp(gr[0],255,lit); g=lerp(gr[1],255,lit); b=lerp(gr[2],255,lit);
  } else if(ed<3.5){
    // grass fading into dirt
    const t0=clamp((ed-0.5)/3.0,0,1);
    const gn=(n-0.5)*22;
    r=lerp(gd[0]+gn, dirt[0], t0); g=lerp(gd[1]+gn, dirt[1], t0); b=lerp(gd[2]+gn, dirt[2], t0);
  } else if(ed<13){
    const t0=clamp((ed-3.5)/9.5,0,1);
    const gn=(n2-0.5)*26;
    r=lerp(dirt[0]+gn, rk[0], t0); g=lerp(dirt[1]+gn, rk[1], t0); b=lerp(dirt[2]+gn, rk[2], t0);
  } else {
    const dep = clamp(y/H,0,1);
    const strata = Math.sin(y*0.075+x*0.006)*7 + Math.sin(y*0.23-x*0.01)*4;
    const gn=(n-0.5)*16 + strata + (n2>0.94?26:0);
    r=lerp(rk[0],rk2[0],dep*0.9)+gn; g=lerp(rk[1],rk2[1],dep*0.9)+gn; b=lerp(rk[2],rk2[2],dep*0.9)+gn;
  }
  return [clamp(r,0,255),clamp(g,0,255),clamp(b,0,255)];
}
function paintRegion(x0,x1){
  x0=clamp(x0|0,0,W-1); x1=clamp(x1|0,0,W-1);
  const w=x1-x0+1;
  const img=tctx.getImageData(x0,0,w,H); const d=img.data;
  const topY=new Int16Array(w).fill(-1), botY=new Int16Array(w).fill(-1);
  for(let i=0;i<w;i++){
    const x=x0+i, base=x;
    for(let y=0;y<H;y++) if(mask[y*W+x]){topY[i]=y;break}
    for(let y=H-1;y>=0;y--) if(mask[y*W+x]){botY[i]=y;break}
  }
  for(let i=0;i<w;i++){
    const x=x0+i;
    for(let y=0;y<H;y++){
      const p=(y*w+i)*4;
      if(!mask[y*W+x]){ d[p+3]=0; continue; }
      const c=colPick(0,x,y,topY,botY,i,w);
      d[p]=c[0]; d[p+1]=c[1]; d[p+2]=c[2]; d[p+3]=255;
    }
  }
  tctx.putImageData(img,x0,0);
}
function paintAround(x0,x1){ paintRegion(x0-3,x1+3); }

/* carve + physical response: returns freed area */
function destroyTerrain(cx,cy,r){
  carveDisc(cx,cy,r,0.14,makeFbm(mulberry32((cx*31+cy*17)|0),0.05));
  paintAround(Math.floor(cx-r)-2, Math.ceil(cx+r)+2);
  return Math.PI*r*r;
}
/* ------------------------------ particles ------------------------------ */
const GRAV = 0.42, WIND_P = 0.034;
const parts = [];
const chunks = [];
const texts  = [];
function spawnPart(o){
  if(parts.length>1500) parts.splice(0,60);
  o.life=o.life||1; o.max=o.max||o.life; o.age=0;
  o.vx=o.vx||0; o.vy=o.vy||0; o.g=o.g===undefined?0.05:o.g;
  o.drag=o.drag===undefined?0.97:o.drag; o.rot=o.rot||0; o.vr=o.vr||0;
  parts.push(o); return o;
}
function dust(x,y,n,col,pw){
  for(let i=0;i<n;i++) spawnPart({type:'dust',x:x+rr(8,-8),y:y+rr(6,-6),
    vx:rr(pw||3,-(pw||3)),vy:rr(-0.4,-3.2),size:rr(9,3),size2:0,g:0.06,drag:0.93,
    life:rr(1.1,0.5),col:col||'rgba(196,176,148,'});
}
function smoke(x,y,n,col,sc){
  for(let i=0;i<n;i++) spawnPart({type:'smoke',x:x+rr(10,-10),y:y+rr(10,-10),
    vx:rr(1.6,-1.6)*(sc||1),vy:rr(0.4,-2.4)*(sc||1),size:rr(14,6)*(sc||1),size2:rr(46,22)*(sc||1),
    g:-0.012,drag:0.94,life:rr(1.9,1.0),col:col||'rgba(120,116,124,'});
}
function sparks(x,y,n,col,pw){
  for(let i=0;i<n;i++){const a=rr(TAU);
    spawnPart({type:'spark',x,y,vx:Math.cos(a)*rr(pw||8,2),vy:Math.sin(a)*rr(pw||8,2),
      size:rr(3,1.5),g:0.22,drag:0.94,life:rr(0.7,0.25),col:col||'rgba(255,206,120,'});}
}
function bubbles(x,y,n,sc){
  for(let i=0;i<n;i++) spawnPart({type:'bubble',x:x+rr(8,-8),y:y+rr(8,-8),
    vx:rr(.5,-.5),vy:rr(-1.2,-.2),size:rr(5,2)*(sc||1),g:-0.03,drag:0.97,
    life:rr(1.4,0.6),col:'rgba(200,240,255,'});
}
function ring(x,y,r0,col,w,lw){
  spawnPart({type:'ring',x,y,size:r0,size2:(w||r0*2.4),life:l0(lw||0.42),g:0,drag:1,col:col||'rgba(255,240,200,'});
}
function l0(v){return v}
function flash(x,y,r,col){
  spawnPart({type:'flash',x,y,size:r,size2:r*0.5,life:0.22,g:0,drag:1,col:col||'rgba(255,248,220,'});
}
function goo(x,y,n,col){
  for(let i=0;i<n;i++){const a=rr(TAU);
    spawnPart({type:'goo',x,y,vx:Math.cos(a)*rr(5,1),vy:Math.sin(a)*rr(5,1)-1,
      size:rr(4,2),g:0.3,drag:0.96,life:rr(0.9,0.4),col:col||'rgba(150,230,140,'});}
}
function textPop(x,y,txt,col,size){
  texts.push({x,y,txt,col:col||'#fff',size:size||18,age:0,life:1.25,vy:-1.15,vx:rr(0.5,-0.5)});
  if(texts.length>26) texts.shift();
}
function spawnChunks(x,y,r,count){
  for(let i=0;i<count;i++){
    const a=rr(TAU), d=Math.sqrt(RNG())*r*0.92;
    const cx=x+Math.cos(a)*d, cy=y+Math.sin(a)*d;
    if(!solidAt(cx,cy)) continue;
    const sz=rr(r*0.22,3)+2;
    const c=theme;
    const shade=hash2(cx|0,cy|0);
    const col=`rgb(${fmt(lerp(c.dirt[0],c.rock[0],shade))},${fmt(lerp(c.dirt[1],c.rock[1],shade))},${fmt(lerp(c.dirt[2],c.rock[2],shade))})`;
    const a2=rr(TAU), pw=rr(6,1.2);
    if(chunks.length>260) chunks.splice(0,20);
    chunks.push({x:cx,y:cy,vx:Math.cos(a2)*pw+(x<cx?-1:1)*rr(3,0.5),vy:Math.sin(a2)*pw-rr(4,0.5),
      r:sz,rot:rr(TAU),vr:rr(.22,-.22),col,still:false,life:0,fade:1,rest:rr(7,5)});
  }
}
function updateParticles(dt,dtS){
  for(let i=parts.length-1;i>=0;i--){
    const p=parts[i]; p.age+=dtS;
    if(p.age>=p.life){ parts.splice(i,1); continue; }
    p.vy+=p.g*dt; p.vx*=Math.pow(p.drag,dt); p.vy*=Math.pow(p.drag,dt);
    p.x+=p.vx*dt; p.y+=p.vy*dt; p.rot+=p.vr*dt;
    if(p.type==='dust'||p.type==='bubble') p.vx += wind*0.02*dt;
  }
  for(let i=chunks.length-1;i>=0;i--){
    const c=chunks[i]; c.life+=dtS;
    if(!c.still){
      c.vy+=GRAV*dt; c.vx+=wind*0.012*dt;
      c.vx*=Math.pow(0.995,dt);
      let steps=Math.max(1,Math.ceil(Math.hypot(c.vx,c.vy)*dt/2.5));
      for(let s=0;s<steps;s++){
        const nx=c.x+c.vx*dt/steps, ny=c.y+c.vy*dt/steps;
        const hitX=solidAt(nx,c.y)||nx<0||nx>=W, hitY=solidAt(c.x,ny)||ny>=H;
        if(hitX) c.vx*=-0.24; else c.x=nx;
        if(hitY){ c.vy*=-0.2; c.vx*=0.7; if(Math.abs(c.vy)<0.6) c.vy=0; } else c.y=ny;
        c.rot+=c.vr*dt;
        if((hitX&&hitY)||(hitY&&Math.abs(c.vx)<0.22&&Math.abs(c.vy)<0.5)){
          if(c.y<H-2 && solidAt(c.x,c.y+1) && Math.abs(c.vx)<0.3){ c.still=true; c.y-=c.r*0.3; }
        }
      }
      if(c.y>WATER_Y+4){ chunks.splice(i,1); bubbles(c.x,c.y,2); continue; }
    } else {
      if(!solidAt(c.x,c.y+2)&&c.y<H-2){ c.still=false; } // ground vanished -> fall
    }
    if(c.life>c.rest) c.fade-=dtS*0.55;
    if(c.fade<=0) chunks.splice(i,1);
  }
  for(let i=texts.length-1;i>=0;i--){
    const t=texts[i]; t.age+=dtS;
    t.y+=t.vy*dt; t.x+=t.vx*dt; t.vy*=Math.pow(0.96,dt);
    if(t.age>=t.life) texts.splice(i,1);
  }
}

/* ------------------------------ camera ------------------------------ */
const fit = {v:1};
const cam = {x:W*0.5,y:H*0.5,z:1,scale:1,shake:0,sx:0,sy:0,flash:0,frz:0};
function camFocus(fx,fy,zoomMul,dt,speed){
  const k=Math.min(1,dt*(speed||3.4));
  cam.x=lerp(cam.x,fx,k); cam.y=lerp(cam.y,fy,k);
  cam.z=lerp(cam.z,zoomMul,k*0.8);
}
function updateCameraShake(dt){
  cam.shake*=Math.pow(0.88,dt);
  if(cam.shake<0.05) cam.shake=0;
  cam.sx=rr(cam.shake,-cam.shake); cam.sy=rr(cam.shake,-cam.shake);
  cam.flash*=Math.pow(0.9,dt);
  if(cam.frz>0) cam.frz-=dt;
}
function screenToWorld(mx,my){
  return {x:(mx-cw/2)/(fit.v*cam.z)+cam.x, y:(my-ch/2)/(fit.v*cam.z)+cam.y};
}

/* ------------------------------ units ------------------------------ */
const TEAM_DEF = [
  {name:'Alpha',   c:'#ff6b6b', c2:'#ffd3d3'},
  {name:'Beta',    c:'#4dabf7', c2:'#cfe8ff'},
  {name:'Gamma',   c:'#7bdc8b', c2:'#d8ffe0'},
  {name:'Delta',   c:'#e599f7', c2:'#f6ddff'},
];
let worms = [], teams = [];

function makeTeams(cfg){
  teams = []; worms = [];
  cfg.forEach((c,i)=>{
    if(!c.on) return;
    const T = {idx:teams.length, name:TEAM_DEF[i].name, cpu:c.cpu, color:TEAM_DEF[i].c,
      color2:TEAM_DEF[i].c2, worms:[], ammo:{}, active:0, alive:true, dealt:0, taken:0};
    // weapon ammo per team
    WEAPONS.forEach(w=>{ T.ammo[w.id] = w.ammo===Infinity?Infinity:w.ammo; });
    teams.push(T);
  });
  return teams;
}
function teamBands(n){
  if(n<=1) return [[0.05,0.95]];
  if(n===2) return [[0.06,0.44],[0.56,0.94]];
  if(n===3) return [[0.03,0.33],[0.35,0.65],[0.67,0.97]];
  return [[0.02,0.26],[0.26,0.5],[0.5,0.74],[0.74,0.98]];
}
function surfaceY(x){
  // first solid pixel with at least 30px of clear air above it (works for caves too)
  let air=0;
  for(let y=26;y<WATER_Y-18;y++){
    if(mask[y*W+x]){ if(air>=30) return y; air=0; }
    else air++;
  }
  return -1;
}
function findTeamSpots(b,count){
  const cand=[];
  for(let x=Math.floor(W*b[0]);x<Math.floor(W*b[1]);x+=5){
    if(noSpawn.some(r=>x>r[0]&&x<r[1])) continue;   // skip floating platforms
    const y=surfaceY(x);
    if(y<0||y>WATER_Y-46) continue;   // keep well clear of the waterline
    let clear=true;
    for(let k=-12;k<=12;k+=6) if(solidAt(x+k,y-30)) clear=false;
    for(let k=-9;k<=9;k+=9) if(!solidAt(x+k,y+7)) clear=false;
    if(!clear) continue;
    const l7=surfaceY(clamp(x-7,1,W-2)), r7=surfaceY(clamp(x+7,1,W-2));
    if(l7<0||r7<0||Math.abs(l7-y)>18||Math.abs(r7-y)>18) continue;   // avoid sheer cliffs
    let flat=0;
    for(let dx=-14;dx<=14;dx+=7){
      const yy=surfaceY(clamp(x+dx,1,W-2));
      flat += yy<0? 60 : Math.abs(yy-y);
    }
    cand.push({x,y,score:flat + Math.abs(y-(WATER_Y-150))*0.3});
  }
  cand.sort((a,b2)=>a.score-b2.score);
  const out=[];
  if(!cand.length) return out;
  out.push(cand[0]);
  while(out.length<count){
    let bestI=-1,best=-1e9;
    for(let i=0;i<cand.length;i++){
      const c=cand[i];
      if(out.some(o=>Math.abs(o.x-c.x)<40)) continue;
      let dmin=1e9; out.forEach(o=>{ dmin=Math.min(dmin,Math.abs(o.x-c.x)); });
      const sc=Math.min(dmin,240)-c.score*0.6;
      if(sc>best){ best=sc; bestI=i; }
    }
    if(bestI<0) break;
    out.push(cand[bestI]);
  }
  return out;
}
function makeWorms(){
  worms=[];
  const bands=teamBands(teams.length);
  teams.forEach((T,ti)=>{
    let spots=findTeamSpots(bands[ti]||[0.05,0.95],4);
    if(spots.length<4){   // wide search so nobody ever spawns over the void
      const wide=findTeamSpots([0.03,0.97],24);
      wide.forEach(s=>{ if(spots.length<4 && !spots.some(o=>Math.abs(o.x-s.x)<44)) spots.push(s); });
    }
    for(let i=0;i<4;i++){
      const sp=spots[i]||spots[spots.length-1]||{x:W*0.5,y:WATER_Y-150};
      const w = {
        id:worms.length, team:T, x:clamp(sp.x+rr(6,-6),20,W-20),
        y:sp.y-16-i*10,
        vx:0,vy:0,r:9,hp:100,maxhp:100,alive:true,facing:1,onGround:false,
        anim:0,walkT:0,blink:rr(4,1),flash:0,onLand:0,inWater:false,waterT:0,sink:0,
        dmg:0,name:'W'+(i+1),idx:i,walkPhase:rr(TAU),squish:0,jet:0,fuel:100,cry:0
      };
      // make sure we are not spawned inside rock
      if(blockedAt(w.x,w.y,w.r)){
        for(let d=1;d<60;d++){
          if(!blockedAt(w.x,w.y-d,w.r)){ w.y-=d; break; }
          if(!blockedAt(w.x,w.y+d,w.r)){ w.y+=d; break; }
        }
      }
      // drop onto ground
      let gy=w.y;
      for(let k=0;k<H;k++){ if(solidAt(w.x,gy)) break; gy+=2; if(gy>WATER_Y-20) break; }
      w.y=Math.min(gy-1,w.y);
      T.worms.push(w); worms.push(w);
    }
  });
}
function findSpawnSpots(n){
  // scan columns for land above water in each horizontal band
  const spots=[];
  const bands=[];
  if(n===2) bands.push([0.08,0.42],[0.58,0.92]);
  else if(n===3) bands.push([0.04,0.32],[0.36,0.64],[0.68,0.96]);
  else bands.push([0.03,0.25],[0.27,0.49],[0.51,0.73],[0.75,0.97]);
  bands.forEach(b=>{
    let best=null;
    for(let x=Math.floor(W*b[0]);x<Math.floor(W*b[1]);x+=4){
      let y=-1;
      for(let yy=40;yy<WATER_Y-14;yy++){ if(mask[yy*W+x]){ y=yy; break } }
      if(y>0){
        // prefer flat ground with headroom and away from water
        let flat=0;
        let clear=true;
        for(let k=-14;k<=14;k+=7) if(solidAt(x+k,y-24)) clear=false;
        if(!clear) continue;
        for(let dx=-12;dx<=12;dx+=6){ let yy=-1;
          for(let t=40;t<WATER_Y-14;t++){ if(mask[t*W+clamp(x+dx,1,W-2)]){yy=t;break} }
          flat+=Math.abs(yy-y);
        }
        const score = flat + Math.abs(y-(WATER_Y-140))*0.35;
        if(!best||score<best.score) best={x,y,score};
      }
    }
    if(best) spots.push(best);
  });
  while(spots.length<n) spots.push({x:W*0.2+RNG()*W*0.6,y:WATER_Y-160});
  return spots;
}

/* worm physics ------------------------------------------------------- */
function blockedAt(x,y,r){
  if(y+r>=H) return true;
  if(x-r<0||x+r>=W) return true;
  const r2=r*r;
  for(let oy=-r;oy<=r;oy+=2.5){
    for(let ox=-r;ox<=r;ox+=2.5){
      if(ox*ox+oy*oy>r2) continue;
      if(solidAt(x+ox,y+oy)) return true;
    }
  }
  return false;
}
function groundUnder(x,y,r){
  if(y+r>=H) return true;
  if(solidAt(x,y+r+0.5)) return true;
  return blockedAt(x,y+1.2,r);
}
function stepWorm(w,dt,dtS){
  if(!w.alive){ w.vy+=GRAV*dt*0.6; w.y+=w.vy*dt; w.vx*=Math.pow(0.97,dt); w.x+=w.vx*dt;
    if(w.y>WATER_Y+10) w.y+=0.2*dt; return; }
  // water buoyancy / drowning
  w.inWater = w.y > WATER_Y-4;
  if(w.inWater){
    w.waterT+=dtS; w.vy = lerp(w.vy,-0.5,Math.min(1,dtS*2.6));
    w.vx*=Math.pow(0.9,dt);
    w.hp-=15*dtS;
    if(Math.random()<0.35*dtS) bubbles(w.x,w.y-4,1);
    if(w.waterT-(w.lastDrown||0)>1.25){ w.lastDrown=w.waterT; Sound.drown(); }
    if(w.hp<=0 && w.alive) killWorm(w,'drowned');
    w.squish=bob(w.waterT);
  } else w.waterT=0;
  // vertical
  w.vy+=GRAV*dt;
  w.vy=clamp(w.vy,-22,26);
  let steps=Math.max(1,Math.ceil(Math.abs(w.vy*dt)/2.4));
  for(let s=0;s<steps;s++){
    const ny=w.y+w.vy*dt/steps;
    if(blockedAt(w.x,ny,w.r)){
      if(w.vy>0){
        if(!w.onGround){
          const fallSpd=w.vy;
          spawnLandDust(w);
          if(fallSpd>8.6){ const d=(fallSpd-8.6)*7.4; hurt(w,d,null,true); }
          else if(fallSpd>3) Sound.land();
        }
        w.squish=Math.min(1,w.vy*0.06);
        // settle onto surface
        let k=0; while(blockedAt(w.x,w.y,w.r)&&k<14){ w.y-=0.6; k++; }
      }
      w.vy=0; break;
    }
    w.y=ny;
  }
  w.onGround = groundUnder(w.x,w.y,w.r);
  // horizontal
  if(Math.abs(w.vx)>0.001){
    let steps=Math.max(1,Math.ceil(Math.abs(w.vx*dt)/1.6));
    for(let s=0;s<steps;s++){
      const nx=w.x+w.vx*dt/steps;
      if(blockedAt(nx,w.y,w.r)){
        let climbed=false;
        for(let up=1;up<=13;up++){
          if(!blockedAt(nx,w.y-up,w.r)&&!blockedAt(w.x,w.y-up,w.r)){ w.y-=up; w.x=nx; climbed=true; break; }
        }
        if(!climbed){ w.vx=0; break; }
      } else w.x=nx;
    }
    if(w.onGround) w.walkT+=dtS;
  }
  if(!w.onGround){ w.vx+=wind*0.022*dt; w.vx*=Math.pow(0.985,dt); }
  w.x=clamp(w.x,w.r,W-w.r);
  // animation decay
  w.squish=lerp(w.squish,0,Math.min(1,dtS*7));
  w.blink-=dtS; if(w.blink<-0.14) w.blink=rr(5,2);
  w.flash=Math.max(0,w.flash-dtS*3.2);
  w.anim = w.onGround&&Math.abs(w.vx)>0.15 ? w.anim+dtS*13 : lerp(w.anim,0,Math.min(1,dtS*8));
  w.cry=Math.max(0,w.cry-dtS);
}
function bob(t){ return Math.sin(t*3.4)*0.5+0.5 }
function spawnLandDust(w){
  const c=theme;
  dust(w.x,w.y+w.r*0.7,8,`rgba(${c.dirt[0]},${c.dirt[1]},${c.dirt[2]},`,2.4);
  Sound.crumble();
}
function hurt(w,amount,byTeam,silent){
  if(!w.alive) return;
  w.hp-=amount; w.flash=1;
  w.team.taken+=amount;
  if(!silent){ textPop(w.x, w.y-22, '-'+fmt(amount), '#ff8a8a', 17+Math.min(10,amount*0.16)); Sound.hurt(); }
  else textPop(w.x,w.y-22,'-'+fmt(amount),'#ffb27a',14);
  goo(w.x,w.y,Math.min(14,3+amount*0.3),`rgba(${hex2rgb(w.team.color)},`);
  if(w.hp<=0){ w.hp=0; killWorm(w,'damage'); }
}
function killWorm(w,how){
  if(!w.alive) return;
  w.alive=false; w.vy=-3.5; w.vx=rr(2,-2);
  goo(w.x,w.y,26,`rgba(${hex2rgb(w.team.color)},`);
  goo(w.x,w.y,14,'rgba(240,250,255,');
  textPop(w.x,w.y-30, how==='drowned'?'GLUG GLUG…':'K.O.!', w.team.color, 22);
  Sound.death();
  addToast(`${w.team.name} worm ${w.idx+1} is down!`, w.team.color);
  if(w.team.worms.every(x=>!x.alive)){
    w.team.alive=false;
    if(how!=='drowned') addToast(`${w.team.name} is eliminated!`, w.team.color);
  }
  checkGameOver();
}
function hex2rgb(h){
  const v=h.replace('#','');
  return [parseInt(v.substr(0,2),16),parseInt(v.substr(2,2),16),parseInt(v.substr(4,2),16)].join(',');
}
/* ------------------------------ weapons ------------------------------ */
const ICON = {
 rocket:'<path d="M3.6 11.4C7.6 6.6 12.6 3.9 18 3.4c.5 5.4-2.2 10.4-7 14.4l-3.6-2.6L3.6 11.4z" fill="#ff9c5b"/><path d="M3.6 15.6l4.8 4.8-6.2 1.4 1.4-6.2z" fill="#ffd06b"/><circle cx="15.2" cy="8.9" r="2.3" fill="#22304d"/>',
 grenade:'<circle cx="11" cy="14" r="7" fill="#6f8f52"/><circle cx="8.6" cy="11.6" r="2.2" fill="#93b96b"/><path d="M11 7.4V4.6h3.4" stroke="#c9c9c9" stroke-width="1.6" fill="none"/><path d="M14.4 4.6l2.2-1.6" stroke="#ffd166" stroke-width="1.6"/><path d="M11.6 4.2h3.4" stroke="#9aa5b1" stroke-width="1.4"/>',
 holy:'<circle cx="12" cy="14.5" r="6.4" fill="#f5e9c8"/><path d="M12 10.6v7.8M8.6 13.4h6.8" stroke="#c89b3c" stroke-width="1.8"/><ellipse cx="12" cy="5.4" rx="5" ry="1.7" fill="none" stroke="#ffe066" stroke-width="1.6"/><path d="M12 8.2v1.6" stroke="#ffe066" stroke-width="1.4"/>',
 dynamite:'<rect x="6.6" y="7" width="3.4" height="13" rx="1.2" fill="#d64545"/><rect x="10.4" y="7" width="3.4" height="13" rx="1.2" fill="#e05c5c"/><rect x="14.2" y="7" width="3.4" height="13" rx="1.2" fill="#c73e3e"/><rect x="6" y="10" width="12" height="1.6" fill="#f0d9a8"/><path d="M12 6.6c0-2 2.4-1.6 2.4-3.4" stroke="#e9c46a" stroke-width="1.3" fill="none"/><circle cx="14.6" cy="2.8" r="1.2" fill="#ffd166"/>',
 shotgun:'<rect x="2.6" y="10.4" width="13" height="3.2" rx="1" fill="#8d99ae"/><rect x="2.6" y="13.4" width="8" height="3.6" rx="1.2" fill="#6b4b2a"/><rect x="15" y="10.8" width="6.4" height="2.4" rx="1" fill="#b9c4d2"/><circle cx="7.6" cy="16.6" r="2.4" fill="none" stroke="#e0a95c" stroke-width="1.4"/>',
 gun:'<rect x="3" y="9" width="12" height="5" rx="2" fill="#7d8896"/><circle cx="17.4" cy="11.5" r="3" fill="#9aa5b1"/><rect x="18.6" y="9.6" width="3.2" height="1.6" rx="0.8" fill="#cbd5e1"/><rect x="3" y="14.4" width="4" height="6" rx="1.4" fill="#5b6470"/><rect x="8" y="14.4" width="3" height="4" rx="1" fill="#39414c"/>',
 plane:'<path d="M2 13.4l8.6-2.2 4.6-6.6 2.4.9-2.2 6 4.4 1.2-3.2 3.4-6.4-1.2-2.4 3.4-2-4.9z" fill="#cbd5e1"/><path d="M2 13.4l8.6-2.2" stroke="#94a3b8" stroke-width="0.8"/>',
 tele:'<circle cx="12" cy="12" r="8.6" fill="none" stroke="none"/><path d="M8.4 16.8L16.4 8" stroke="#7fd0ff" stroke-width="2.6" stroke-linecap="round"/><path d="M16.4 8l-4.6.6 4 4z" fill="#7fd0ff"/><circle cx="6.6" cy="6.6" r="1.7" fill="#c58cff"/><circle cx="17.6" cy="17.6" r="1.7" fill="#c58cff"/>',
 jet:'<rect x="7.4" y="4.6" width="9.2" height="10.4" rx="3" fill="#94a3b8"/><rect x="9.4" y="6.6" width="5.2" height="6.4" rx="2" fill="#cbd5e1"/><path d="M12 16.4c-2.4 0-3.2 2-3.2 4.4 1.6-.8 2.4-2.4 3.2-4.4z" fill="#ff9c5b"/><path d="M12 16.4c2.4 0 3.2 2 3.2 4.4-1.6-.8-2.4-2.4-3.2-4.4z" fill="#ffd06b"/>',
 health:'<rect x="3.4" y="6.4" width="17.2" height="13.2" rx="3" fill="#f2f4f8"/><rect x="3.4" y="6.4" width="17.2" height="4.4" rx="2" fill="#e07a7a"/><path d="M12 10.6v6.6M8.7 13.9h6.6" stroke="#e04b4b" stroke-width="2.4" stroke-linecap="round"/>',
};
const WEAPONS = [
  {id:'bazooka', name:'Bazooka', desc:'contact rocket · infinite ammo · medium crater', kind:'indirect', ammo:Infinity, radius:44, dmg:46, icon:'rocket', wind:1, kb:1},
  {id:'grenade', name:'Grenade', desc:'bouncy · 3s fuse · nice blast', kind:'indirect', ammo:5, radius:50, dmg:50, icon:'grenade', fuse:3.0, bouncy:true, wind:1, kb:1.1},
  {id:'holy', name:'Holy Hand Grenade', desc:'map-altering halo · 3s fuse', kind:'indirect', ammo:2, radius:88, dmg:74, icon:'holy', fuse:3.2, bouncy:true, wind:0.85, kb:1.4},
  {id:'dynamite', name:'Dynamite', desc:'drop it & RUN · 4s fuse · big crater', kind:'placed', ammo:3, radius:76, dmg:64, icon:'dynamite', fuse:4.0, wind:0, kb:1.3},
  {id:'shotgun', name:'Shotgun', desc:'instant hitscan · 4 pellets · 2 shots', kind:'direct', ammo:2, radius:12, dmg:11, icon:'shotgun', shots:4, wind:0, kb:0.7},
  {id:'minigun', name:'Minigun', desc:'instant stream of 12 rounds', kind:'direct', ammo:1, radius:10, dmg:6, icon:'gun', shots:12, wind:0, kb:0.4},
  {id:'airstrike', name:'Airstrike', desc:'click a spot · carpet bombs from the sky', kind:'support', ammo:2, radius:44, dmg:38, icon:'plane', wind:0, kb:1},
  {id:'medkit', name:'Medkit Drop', desc:'click to heal any worm in the blast (+30)', kind:'support', ammo:2, radius:0, dmg:0, icon:'health', wind:0, kb:0},
  {id:'teleport', name:'Teleporter', desc:'click to warp across the map · ends turn', kind:'utility', ammo:2, icon:'tele', wind:0, kb:0},
  {id:'jetpack', name:'Jetpack', desc:'hold mouse for thrust · fuel-limited flight', kind:'utility', ammo:2, icon:'jet', wind:0, kb:0},
];
const WMAP = {}; WEAPONS.forEach(w=>WMAP[w.id]=w);

/* ------------------------------ game state ------------------------------ */
const G = {
  phase:'title', turnTeam:0, active:null, time:45, shotFired:false, round:1,
  wind:0, charging:false, power:0, angle:-0.6, mouse:{x:W*0.5,y:H*0.4},
  hitstop:0, over:false, winner:null, ai:null, aiQueue:[], waveT:0, overT:0,
  sudden:false, waterRise:0, turnCount:0, pendingEnd:0, aimValid:true, matchId:0
};
const TURN_TIME = 45;

/* ------------------------------ projectiles ------------------------------ */
const projs = [];
function makeProj(o){ projs.push(Object.assign({
  kind:'rocket', x:0,y:0,vx:0,vy:0,r:4,team:null,weapon:null,dmg:0,radius:20,
  fuse:0, bouncy:false, dead:false, age:0, spin:0, spinV:0, trail:0, wind:1, spin2:0
},o)); return projs[projs.length-1]; }
function terrainNormal(x,y){
  const e=2.2;
  const dx = (solidAt(x+e,y)?1:0)-(solidAt(x-e,y)?1:0);
  const dy = (solidAt(x,y+e)?1:0)-(solidAt(x,y-e)?1:0);
  const n=Math.hypot(dx,dy)||1; return {x:dx/n,y:dy/n};
}
function updateProjectiles(dt,dtS){
  for(let i=projs.length-1;i>=0;i--){
    const p=projs[i];
    if(p.dead){ projs.splice(i,1); continue; }
    p.age+=dtS; p.spin+=p.spinV*dt;
    if(p.kind==='dyn'){
      p.vy+=GRAV*dt; p.vy=clamp(p.vy,-22,22);
      const steps=Math.max(1,Math.ceil(Math.abs(p.vy*dt)/2.4));
      for(let s=0;s<steps;s++){
        const ny=p.y+p.vy*dt/steps;
        if(blockedAt(p.x,ny,p.r)){ p.vy=0; break; }
        p.y=ny;
      }
      if(p.y>WATER_Y-2){ p.dead=true; Sound.splash(); wave(); if(G.phase==='player') G.dynExploded=true; continue; }
      p.fuse-=dtS; if(p.fuse<0) p.fuse=0;
      if(p.fuse<0.9 && Math.floor(p.fuse*4)!==Math.floor((p.fuse+dtS)*4)) Sound.fuseBeep(p.fuse);
      if(p.fuse<=0){ p.dead=true; explode(p.x,p.y,p.radius,p.dmg,p.team); if(G.phase==='player') G.dynExploded=true; }
      continue;
    }
    if(p.kind==='crate'){
      p.vy+=GRAV*0.85*dt;
      p.y+=p.vy*dt; p.x+=p.vx*dt;
      if(p.chute) p.vy=lerp(p.vy,3.2,Math.min(1,dtS*1.6));
      if(solidAt(p.x,p.y+8)||p.y>WATER_Y-10){
        p.dead=true; Sound.crumble();
        dust(p.x,p.y+8,14,`rgba(${theme.dirt.join(',')},`,3);
        flash(p.x,p.y,60,'rgba(255,240,200,');
        Sound.craten(); textPop(p.x,p.y-30,'SUPPLIES!','#8cf7c5',20);
        worms.forEach(w=>{ if(w.alive&&dist(w.x,w.y,p.x,p.y)<95){
          const before=w.hp; w.hp=Math.min(w.maxhp,w.hp+30);
          if(w.hp>before){ Sound.heal(); textPop(w.x,w.y-26,'+30','#8cf7c5',18); } } });
        for(let k=0;k<24;k++) spawnPart({type:'star',x:p.x+rr(20,-20),y:p.y+rr(20,-20),
          vx:rr(1,-1),vy:rr(-1,-3),size:rr(5,2),g:-0.01,life:rr(1.2,.6),col:'rgba(140,247,197,'});
      }
      continue;
    }
    if(p.kind==='plane'){
      p.x+=p.vx*dt; p.y=p.dropY;
      if(p.dropX!==undefined){
        if(!p.dropping && ((p.vx>0&&p.x>p.dropX-p.spread*0.5)||(p.vx<0&&p.x<p.dropX+p.spread*0.5)))
          { p.dropping=true; p.dt=0; }
        if(p.dropping && p.bombsLeft>0){
          p.dt-=dtS;
          if(p.dt<=0){ p.dt=0.085; p.bombsLeft--; dropBombs(p,1); }
        }
      }
      if(p.x<-140||p.x>W+140) p.dead=true;
      smoke(p.x-p.vx*3,p.y,1,'rgba(220,225,235,',0.5);
      continue;
    }
    if(p.kind==='bomb'){
      p.vy+=GRAV*1.15*dt; p.vy=clamp(p.vy,-24,26);
      p.x+=p.vx*dt; p.y+=p.vy*dt;
      p.trail-=dtS;
      if(p.trail<0){ p.trail=0.05; smoke(p.x,p.y,1,'rgba(200,200,210,',0.5); }
      if(p.y>WATER_Y){ p.dead=true; Sound.splash(); wave(); continue; }
      if(solidAt(p.x,p.y)){ p.dead=true; explode(p.x,p.y-p.r,p.radius,p.dmg,p.team); }
      continue;
    }
    /* --- ballistic projectiles --- */
    p.vy+=GRAV*dt;
    p.vx+=wind*WIND_P*p.wind*dt;
    const spd=Math.hypot(p.vx,p.vy);
    const steps=Math.max(1,Math.ceil(spd*dt/3.2));
    let exploded=false;
    for(let s=0;s<steps&&!exploded;s++){
      const px=p.x, py=p.y;
      const nx=p.x+p.vx*dt/steps, ny=p.y+p.vy*dt/steps;
      if(nx<-150||nx>W+150||ny<-900){ p.dead=true; exploded=true; break; }   // left the battlefield
      if(ny>WATER_Y){ p.x=nx; p.y=WATER_Y; p.dead=true; Sound.splash();
        for(let k=0;k<16;k++) spawnPart({type:'drop',x:p.x+rr(14,-14),y:WATER_Y,vx:rr(3,-3),vy:rr(-1,-7),size:rr(4,2),g:0.3,life:rr(.7,.3),col:'rgba(200,235,255,'});
        wave(); exploded=true; break; }
      if(p.hitsWorms!==false){
        for(const w of worms){
          if(!w.alive) continue;
          if(dist(nx,ny,w.x,w.y)<w.r+2){
            if(p.bouncy){ // bounce off worm
              const a=Math.atan2(p.y-w.y,p.x-w.x), sp2=Math.hypot(p.vx,p.vy)*0.5;
              p.vx=Math.cos(a)*sp2; p.vy=Math.sin(a)*sp2-1; break;
            }
            explode(nx,ny,p.radius,p.dmg,p.team); p.dead=true; exploded=true; break;
          }
        }
        if(exploded) break;
      }
      if(solidAt(nx,ny)){
        if(p.bouncy && p.age < p.fuse-0.05){
          const n=terrainNormal(nx,ny);
          const d=p.vx*n.x+p.vy*n.y;
          p.vx=(p.vx-2*d*n.x)*0.52; p.vy=(p.vy-2*d*n.y)*0.52;
          p.x=px; p.y=py;
          if(Math.hypot(p.vx,p.vy)<1.1){ p.vx*=0.4; p.vy*=0.4; }
          p.spinV=clamp(p.vx*0.12,-0.4,0.4);
          Sound.tick();
        } else {
          explode(px+(nx-px)*0.5, py+(ny-py)*0.5, p.radius, p.dmg, p.team);
          p.dead=true; exploded=true; break;
        }
      } else { p.x=nx; p.y=ny; }
    }
    if(p.dead) continue;
    p.spinV=clamp(p.spinV+(p.vx*0.0006),-0.5,0.5);
    // trails
    p.trail-=dtS;
    if(p.trail<=0){
      p.trail= p.weapon&&p.weapon.id==='holy'?0.02:0.035;
      if(p.bouncy) smoke(p.x,p.y,1,'rgba(210,210,220,',0.35);
      else { spawnPart({type:'trail',x:p.x,y:p.y,vx:0,vy:0,size:p.r*1.6,size2:1,g:-0.01,drag:1,life:0.4,col:'rgba(255,220,180,'});
        smoke(p.x,p.y,1,'rgba(190,190,200,',0.35); }
    }
    if(p.fuse>0 && p.age>=p.fuse){ explode(p.x,p.y,p.radius,p.dmg,p.team); p.dead=true; }
    if(p.age>9){ p.dead=true; }
  }
}
function dropBombs(plane,count){
  const n=count||1;
  for(let i=0;i<n;i++){
    makeProj({kind:'bomb',x:plane.x+rr(9,-9),y:plane.dropY+6,vx:plane.vx*0.75,vy:2+Math.random()*2,r:4,
      team:plane.team,dmg:plane.dmg,radius:plane.radius,trail:0});
    Sound.whistle();
  }
}
function rng(){ return Math.random(); }

/* ------------------------------ explosions ------------------------------ */
function countSolid(x,y,r){
  let n=0; const x0=clamp(Math.floor(x-r),0,W-1),x1=clamp(Math.ceil(x+r),0,W-1);
  const y0=clamp(Math.floor(y-r),0,H-1),y1=clamp(Math.ceil(y+r),0,H-1);
  for(let yy=y0;yy<=y1;yy+=2)for(let xx=x0;xx<=x1;xx+=2)
    if(mask[yy*W+xx]&&Math.hypot(xx-x,yy-y)<=r) n++;
  return n*4;
}
function explode(x,y,radius,dmg,team,opt){
  opt=opt||{};
  const crater = radius*(opt.craterMul||0.86);
  const before = countSolid(x,y,crater);
  destroyTerrain(x,y,crater);
  const dn = clamp(Math.round(before/700),5,26);
  spawnChunks(x,y,crater,dn);
  // juice
  flash(x,y,crater*1.5);
  ring(x,y,crater*0.3,'rgba(255,238,180,');
  sparks(x,y,Math.round(14+radius*0.25),'rgba(255,205,105,',9+radius*0.12);
  smoke(x,y,Math.round(8+radius*0.2),'rgba(96,94,102,',1+radius*0.008);
  dust(x,y,Math.round(10+radius*0.22),`rgba(${theme.dirt.join(',')},`,2+radius*0.05);
  if(radius>60){ ring(x,y,crater*0.5,'rgba(255,255,255,',crater*3.4,0.5); }
  const dmgR = radius*(opt.dmgMul||1.3);
  worms.forEach(w=>{
    if(!w.alive) return;
    const d=dist(x,y,w.x,w.y);
    if(d<dmgR+w.r){
      const f=1-clamp((d-w.r)/dmgR,0,1);
      const amt=dmg*(0.3+0.7*f);
      if(team) team.dealt+=amt;
      if(dmg>0){
        const a=Math.atan2(w.y-y,w.x-x)||0;
        const kb=(opt.kb===undefined?1:opt.kb)*f*(radius*0.12);
        w.vx+=Math.cos(a)*kb; w.vy+=Math.sin(a)*kb-1.6;
        hurt(w,amt,team);
      }
    }
  });
  Sound.boom(radius/46);
  cam.shake=Math.min(34,cam.shake+radius*0.32);
  cam.flash=Math.min(0.8,cam.flash+radius*0.011);
  if(radius>58){ cam.frz=Math.max(cam.frz,0.09); G.hitstop=Math.max(G.hitstop,0.10); }
  else G.hitstop=Math.max(G.hitstop,0.035);
}

/* ------------------------------ firing ------------------------------ */
function muzzleSpeed(power){ return 4.0+(power/100)*16.0; }
function muzzlePos(w){
  const a=G.angle;
  return {x:w.x+Math.cos(a)*(w.r+8), y:w.y-4+Math.sin(a)*(w.r+8)};
}
function simShot(sx,sy,ang,power,weapon){
  const sp=muzzleSpeed(power);
  let x=sx,y=sy,vx=Math.cos(ang)*sp,vy=Math.sin(ang)*sp,t=0;
  const seg=[{x,y}], wk=weapon.wind===undefined?1:weapon.wind, fuse=weapon.fuse||0;
  for(let i=0;i<500;i++){
    vy+=GRAV; vx+=wind*WIND_P*wk;
    const sN=Math.max(1,Math.ceil(Math.hypot(vx,vy)/3.2));
    for(let s=0;s<sN;s++){
      const px=x,py=y;
      x+=vx/sN; y+=vy/sN; t+=1/(sN*60);
      if(fuse&&t>=fuse) return {x,y,path:seg,fuse:true};
      if(y>WATER_Y) return {x,y,water:true,path:seg};
      if(solidAt(x,y)){
        if(weapon.bouncy&&t<fuse-0.06){
          const n=terrainNormal(x,y), d=vx*n.x+vy*n.y;
          vx=(vx-2*d*n.x)*0.52; vy=(vy-2*d*n.y)*0.52; x=px; y=py;
          if(Math.hypot(vx,vy)<1.1) return {x,y,path:seg,fizzle:true};
        } else return {x:px+(x-px)*0.5,y:py+(y-py)*0.5,path:seg,terrain:true};
      }
      seg.push({x,y});
    }
  }
  return {x,y,path:seg,out:true};
}
function raycast(w,ang,maxD){
  let x=w.x+Math.cos(ang)*(w.r+6), y=w.y-4+Math.sin(ang)*(w.r+6);
  const dx=Math.cos(ang), dy=Math.sin(ang);
  for(let t=0;t<maxD;t+=2.5){
    const px=x+dx*t, py=y+dy*t;
    if(py>WATER_Y) return {x:px,y:py,water:true};
    if(solidAt(px,py)) return {x:px,y:py,terrain:true,ang};
    for(const o of worms) if(o.alive&&o!==w&&dist(px,py,o.x,o.y)<o.r+2) return {x:px,y:py,worm:o,ang};
  }
  return {x:x+dx*maxD,y:y+dy*maxD,miss:true,ang};
}
function teamAmmo(t,id){ return t.ammo[id]; }
function fireWeapon(worm,weapon,power){
  const T=worm.team;
  if(weapon.ammo!==Infinity && T.ammo[weapon.id]<=0) return false;
  if(weapon.ammo!==Infinity) T.ammo[weapon.id]--;
  const m=muzzlePos(worm);
  worm.facing = Math.cos(G.angle)>=0?1:-1;
  worm.squish=1;
  if(weapon.kind==='indirect'){
    makeProj({kind:'ball',x:m.x,y:m.y,vx:Math.cos(G.angle)*muzzleSpeed(power),vy:Math.sin(G.angle)*muzzleSpeed(power),
      r:weapon.id==='holy'?6:4,team:T,weapon,dmg:weapon.dmg,radius:weapon.radius,
      fuse:weapon.fuse||0,bouncy:!!weapon.bouncy,wind:weapon.wind,spin:0,spinV:rr(.2,-.2)});
    Sound.launch();
    for(let i=0;i<10;i++) spawnPart({type:'smoke',x:m.x,y:m.y,vx:rr(2,-2)+Math.cos(G.angle)*2,vy:rr(2,-2),
      size:rr(9,4),size2:rr(22,10),g:-0.02,drag:0.9,life:rr(.6,.3),col:'rgba(210,210,215,'});
    sparks(m.x,m.y,8,'rgba(255,200,120,',4);
    G.shotFired=true; G.phase='flight'; G.aimValid=true;
  } else if(weapon.kind==='placed'){
    makeProj({kind:'dyn',x:worm.x,y:worm.y-2,vx:0,vy:0,r:5,team:T,weapon,dmg:weapon.dmg,
      radius:weapon.radius,fuse:weapon.fuse});
    Sound.pop(); G.shotFired=true;
    addToast('Dynamite armed — run!', T.color);
  } else if(weapon.kind==='direct'){
    G.burst={left:weapon.shots||4, weapon, worm, t:0.001, gap: weapon.id==='minigun'?0.05:0.03};
    G.shotFired=true; G.phase='burst';
  } else if(weapon.kind==='support'){
    if(weapon.id==='airstrike'){
      const tx=clamp(G.mouse.x,80,W-80);
      for(let i=0;i<2;i++){
        const fromLeft=(i===0);
        makeProj({kind:'plane', x:fromLeft?-70:W+70, y:0, vx:(fromLeft?1:-1)*3.6, vy:0,
          dropY:62+rr(52), bombsLeft:6, spread:300, dropX:tx+(fromLeft?-45:45),
          dropping:false, dt:0, team:T, dmg:weapon.dmg, radius:weapon.radius});
      }
      Sound.plane(); addToast('Airstrike inbound!', T.color);
      G.shotFired=true; G.phase='flight';
    } else if(weapon.id==='medkit'){
      makeProj({kind:'crate',x:G.mouse.x,y:-30,vx:0,vy:2.5,r:9,chute:true,team:T});
      Sound.whoosh(); G.shotFired=true; G.phase='flight';
    }
  } else if(weapon.kind==='utility'){
    if(weapon.id==='teleport'){
      const p=G.mouse; const ok=!solidAt(p.x,p.y+8)&&!solidAt(p.x,p.y)&&p.y<WATER_Y-14;
      if(!ok){ Sound.deny(); addToast('No room to teleport there!','#ff8787'); T.ammo[weapon.id]++; return false; }
      for(let k=0;k<26;k++) spawnPart({type:'star',x:worm.x+rr(24,-24),y:worm.y+rr(24,-24),
        vx:rr(1.5,-1.5),vy:rr(-2,-4),size:rr(6,2),g:-0.02,life:rr(1,.5),col:'rgba(140,200,255,'});
      Sound.tele();
      worm.x=clamp(p.x,14,W-14); worm.y=p.y;
      let k=0; while(blockedAt(worm.x,worm.y,worm.r)&&k<40){ worm.y--; k++; }
      for(let k2=0;k2<26;k2++) spawnPart({type:'star',x:worm.x+rr(24,-24),y:worm.y+rr(24,-24),
        vx:rr(1.5,-1.5),vy:rr(-2,-4),size:rr(6,2),g:-0.02,life:rr(1,.5),col:'rgba(200,160,255,'});
      flash(worm.x,worm.y,50,'rgba(180,220,255,');
      G.shotFired=true; endTurn(1.0);
    } else if(weapon.id==='jetpack'){
      worm.jet=1; worm.fuel=100; G.phase='jetpack';
      addToast('Jetpack! Hold mouse to thrust','#ffd166'); Sound.whoosh();
      G.shotFired=true;
    }
  }
  if(weapon.kind==='direct'||weapon.kind==='indirect') updateAmmoUI();
  updateAmmoUI();
  G.fireT=0;
  return true;
}
function fireBurst(worm,weapon,angle){
  const m=muzzlePos(worm);
  const spread = (weapon.id==='minigun') ? rr(0.06,-0.06) : rr(0.08,-0.08);
  const ang = (angle===undefined?G.angle:angle)+spread;
  const hit=raycast(worm,ang,940);
  spawnPart({type:'tracer',x:m.x,y:m.y,ex:hit.x,ey:hit.y,size:1,life:0.1,g:0,drag:1,col:'rgba(255,232,165,'});
  if(hit.worm){
    const T=worm.team; T.dealt+=weapon.dmg;
    hurt(hit.worm,weapon.dmg,T);
    const kb=(weapon.kb||0.6)*4.6;
    hit.worm.vx+=Math.cos(ang)*kb; hit.worm.vy+=Math.sin(ang)*kb-1.1;
    goo(hit.x,hit.y,6,`rgba(${hex2rgb(hit.worm.team.color)},`);
    cam.shake=Math.min(10,cam.shake+1.1);
  } else if(hit.terrain){
    carveSmall(hit.x,hit.y,weapon.radius,ang);
    dust(hit.x,hit.y,6,`rgba(${theme.dirt.join(',')},`,2);
    sparks(hit.x,hit.y,7,'rgba(255,220,150,',5);
    Sound.crumble(); cam.shake=Math.min(8,cam.shake+0.7);
  } else if(hit.water){
    for(let k=0;k<10;k++) spawnPart({type:'drop',x:hit.x+rr(8,-8),y:WATER_Y,vx:rr(2,-2),vy:rr(-1,-6),size:rr(3,1.6),g:0.3,life:rr(.6,.3),col:'rgba(200,235,255,'});
    wave();
  }
  if(weapon.id==='minigun'){ Sound.mg(); cam.shake=Math.min(12,cam.shake+1.2); } else Sound.pellet();
}
function carveSmall(x,y,r,ang){
  carveDisc(x,y,r*(0.7+0.3*RNG()),0.3,makeFbm(mulberry32((x*13+y*7)|0),0.3));
  paintAround(Math.floor(x-r-3),Math.ceil(x+r+3));
}
function wave(){
  for(let i=0;i<3;i++){ ring(W*0.5+rng()*W*0.5, WATER_Y+2, 10, 'rgba(190,235,255,', 90, 0.7); }
}

/* ------------------------------ turn flow ------------------------------ */
function addToast(msg,col){
  const box=document.getElementById('toasts'); if(!box) return;
  const d=document.createElement('div'); d.className='toast'; d.textContent=msg;
  if(col) d.style.borderColor=col, d.style.color=col;
  box.appendChild(d);
  setTimeout(()=>d.remove(),3200);
  while(box.children.length>4) box.firstChild.remove();
}
function showBanner(title,sub,color){
  const b=document.getElementById('banner');
  b.querySelector('.bt').textContent=title;
  b.querySelector('.bs').textContent=sub||'';
  b.style.setProperty('--bc',color||'#7fd0ff');
  b.classList.remove('show'); void b.offsetWidth; b.classList.add('show');
  clearTimeout(b._t); b._t=setTimeout(()=>b.classList.remove('show'),1700);
}
function playerTeams(){ return teams.filter(t=>!t.cpu&&t.alive); }
function nextActiveWorm(T){
  const alive=T.worms.filter(w=>w.alive);
  if(!alive.length) return null;
  T.active=(T.active+1)%alive.length;
  return alive[T.active%alive.length];
}
function startTurn(ti){
  if(G.over) return;
  const alive=teams.filter(t=>t.alive);
  if(alive.length<=1){ gameOver(); return; }
  let tries=0;
  while(tries++<8 && !teams[ti].alive) ti=(ti+1)%teams.length;
  G.turnTeam=ti;
  const T=teams[ti];
  G.active=nextActiveWorm(T);
  if(!G.active){ G.active=T.worms[0]; }
  G.time=TURN_TIME; G.shotFired=false; G.power=0; G.charging=false;
  G.turnCount++;
  // wind
  if(Math.random()<1.0){
    const mag=rr(1,-1);
    G.wind=Math.abs(mag)<0.12?0:clamp(mag,-1,1);
  }
  Sound.setWind(G.wind);
  G.phase='banner'; G.pendingEnd=0;
  if(G.turnCount>teams.length*2 && !G.sudden){ G.sudden=true; addToast('SUDDEN DEATH: the tide is rising!','#ff8787'); }
  if(G.sudden && G.turnCount>teams.length*3 && WATER_Y>400){
    WATER_Y-=7; G.waterRise+=7; addToast('The tide rises… '+Math.round(598-WATER_Y)+'px','#8cd8ff');
  }
  updateHud(true);
  const label = T.cpu? 'CPU '+T.name : T.name;
  showBanner(T.name, T.cpu?'calculating…':'your turn', T.color);
  Sound.horn();
  G.bannerT=1.45;
}
function endTurn(delay){
  if(G.over) return;
  G.pendingEnd=delay===undefined?1.0:delay;
  G.phase='settle';
}
function finishTurn(){
  const alive=teams.filter(t=>t.alive);
  if(alive.length<=1){ gameOver(); return; }
  let ti=(G.turnTeam+1)%teams.length, tries=0;
  while(tries++<teams.length && !teams[ti].alive) ti=(ti+1)%teams.length;
  startTurn(ti);
}
function checkGameOver(){
  const alive=teams.filter(t=>t.alive);
  if(alive.length<=1 && !G.over){
    const mid=G.matchId;
    setTimeout(()=>{ if(G.matchId===mid && !G.over) gameOver(); },1400);
  }
}
function gameOver(){
  if(G.over) return;
  if(teams.filter(t=>t.alive).length>1) return;   // never end while 2+ teams stand
  G.over=true; G.phase='over'; G.winner=teams.filter(t=>t.alive)[0]||null;
  Sound.setWind(0);
  if(G.winner) Sound.win(); else Sound.lose();
  showBanner(G.winner?G.winner.name+' WINS!':'DRAW','battle over', G.winner?G.winner.color:'#fff');
  const wt=document.getElementById('winTitle'), ws=document.getElementById('winSub');
  wt.textContent = G.winner? G.winner.name+' VICTORY':'STALEMATE';
  wt.style.color = G.winner? G.winner.color : '#fff';
  ws.textContent = G.winner? 'last worms standing' : 'everyone is worm food';
  const st=document.getElementById('stats');
  st.innerHTML = teams.map(t=>{
    const hp=t.worms.reduce((a,w)=>a+w.hp,0);
    const al=t.worms.filter(w=>w.alive).length;
    return `<div class="sr"><span style="color:${t.color}">${t.name}${t.cpu?' (CPU)':''}</span>
      <b>${al} alive · ${fmt(hp)} hp · ${fmt(t.dealt)} dmg dealt</b></div>`;
  }).join('');
  setTimeout(()=>document.getElementById('over').classList.remove('hide'),1600);
}
/* ------------------------------ CPU brain ------------------------------ */
const DIFFS = { chill:1.05, normal:0.66, brutal:0.28 };
let DIFF = 0.5;
function simFast(sx,sy,ang,power,step){
  const sp=muzzleSpeed(power);
  let x=sx,y=sy,vx=Math.cos(ang)*sp,vy=Math.sin(ang)*sp;
  for(let i=0;i<420;i++){
    vy+=GRAV; vx+=wind*WIND_P;
    const n=Math.max(1,Math.ceil(Math.hypot(vx,vy)/6));
    for(let s=0;s<n;s++){
      x+=vx/n; y+=vy/n;
      if(y>WATER_Y) return {x,y,water:true};
      if(x<-60||x>W+60||y<-700) return {x,y,out:true};
      if(solidAt(x,y)) return {x,y,terrain:true};
      for(const w of worms) if(w.alive&&dist(x,y,w.x,w.y)<w.r+2) return {x,y,worm:w};
    }
  }
  return {x,y,out:true};
}
function aiPlanShot(T,worm,weapon,target){
  const sx=worm.x+Math.cos(0)*10, sy=worm.y-4;
  let best=null;
  if(weapon.kind==='direct'){
    const ang=Math.atan2(target.y-worm.y,target.x-worm.x);
    const hit=raycast(worm,ang,940);
    return {ang:ang,power:60,score:hit.worm===target?60:20,impact:{x:hit.x,y:hit.y}};
  }
  const powers=[38,50,62,74,86,100];
  for(let a=-3.02;a<=-0.24;a+=0.05){
    for(const pw of powers){
      const im=simFast(sx,sy,a,pw);
      const sc=aiScore(im,weapon,T,worm,target);
      if(!best||sc>best.score) best={ang:a,power:pw,score:sc,impact:im};
    }
  }
  // also try low flat shots when very close
  return best;
}
function aiScore(im,weapon,T,shooter,target){
  let s=0;
  const R=weapon.radius*(weapon.kind==='indirect'?1.3:1.1)+10;
  for(const w of worms){
    if(!w.alive) continue;
    const d=dist(im.x,im.y,w.x,w.y);
    if(d<R){
      const f=1-d/R, amt=weapon.dmg*(0.3+0.7*f);
      if(w.team===T) s-=amt*2.1;
      else {
        const value=1+(1-w.hp/100)*0.8;
        const lethal = amt>=w.hp?2.4:1;
        s+=amt*1.7*value*lethal;
        if(w===target) s+=12;
      }
    }
  }
  if(im.water) s-=30;
  if(im.out) s-=60;
  const selfD=dist(im.x,im.y,shooter.x,shooter.y);
  s-=selfD*0.02;
  if(target){
    const tD=dist(im.x,im.y,target.x,target.y);
    s-=tD*0.05;                    // reward accuracy
    s+=clamp(1-(tD/300),0,1)*8;    // reward landing near the target
  }
  return s;
}
function aiPickWeapon(T,worm,target){
  const d=dist(worm.x,worm.y,target.x,target.y);
  const los=!raycast(worm,Math.atan2(target.y-worm.y,target.x-worm.x),940).terrain;
  const A=T.ammo;
  // cluster detection
  let cluster=0;
  for(const w of worms) if(w.alive&&w.team!==T&&dist(w.x,w.y,target.x,target.y)<70) cluster++;
  if(worm.hp<45 && A.medkit>0 && Math.random()<0.35) return {w:WMAP.medkit, mode:'support'};
  if(cluster>=2 && A.holy>0 && d>90 && Math.random()<0.8) return {w:WMAP.holy};
  if(d<170 && los && A.shotgun>0 && Math.random()<0.75) return {w:WMAP.shotgun};
  if(d<260 && los && A.minigun>0 && Math.random()<0.5) return {w:WMAP.minigun};
  if(target.hp<28 && d<300 && A.grenade>0 && Math.random()<0.4) return {w:WMAP.grenade};
  if(A.dynamite>0 && d<95 && Math.random()<0.45) return {w:WMAP.dynamite};
  if(A.airstrike>0 && d>420 && Math.random()<0.28) return {w:WMAP.airstrike, mode:'support'};
  if(A.grenade>0 && Math.random()<0.42) return {w:WMAP.grenade};
  return {w:WMAP.bazooka};
}
function aiTarget(T){
  let list=[];
  for(const w of worms) if(w.alive&&w.team!==T){
    const d=dist(w.x,w.y,G.active?G.active.x:0,G.active?G.active.y:0);
    list.push({w,score:(100-w.hp)*1.5 - d*0.28 + (w.y>WATER_Y-30?25:0)});
  }
  list.sort((a,b)=>b.score-a.score);
  return list.length?list[0].w:null;
}
function aiStart(){
  const T=teams[G.turnTeam], worm=G.active;
  const target=aiTarget(T);
  const plan={phase:'think',t:0,T,worm,target,dir:0,moveT:0,weapon:null,ang:G.angle,power:60,shots:0};
  const wsel=target?aiPickWeapon(T,worm,target):{w:WMAP.bazooka};
  plan.weapon=wsel.w; plan.mode=wsel.mode;
  G.ai=plan;
}
function aiHazard(worm){
  // danger if close to water or perched with nothing beneath
  let danger=0;
  if(worm.y>WATER_Y-80) danger+=2;
  let drop=0;
  for(let y=worm.y;y<WATER_Y;y+=6){ if(solidAt(worm.x,y)) { drop=y-worm.y; break; } drop=y-worm.y; }
  if(drop>120) danger+=1;
  return danger;
}
function aiPickMove(T,worm,target){
  if(!target) return {dir:0,dist:0};
  const nearWater = worm.y>WATER_Y-86;
  let best={dir:0,dist:0,score:-1e9};
  for(const dir of [-1,1]){
    for(let step=1;step<=7;step++){
      const stepD=step*24;
      const x=clamp(worm.x+dir*stepD,14,W-14);
      let gy=-1;
      for(let y=Math.max(20,worm.y-90);y<WATER_Y-6;y++){ if(solidAt(x,y)){ gy=y; break; } }
      if(gy<0) continue;
      if(gy>WATER_Y-46) continue;                       // would stand in the splash zone
      if(blockedAt(x,gy-worm.r-4,worm.r+1)) continue;   // no headroom
      if(!solidAt(x,gy+4)&&!solidAt(x,gy+9)) continue;  // flimsy footing
      const d=dist(x,gy,target.x,target.y);
      let sc=-Math.abs(d-320)*0.5 - dist*0.9 + (WATER_Y-gy)*0.25;
      if(nearWater) sc+=(worm.y-gy)*1.6;
      if(sc>best.score) best={dir,dist,score:sc,x,y:gy};
    }
  }
  return best;
}
function aiUpdate(dt){
  const A=G.ai; if(!A) return;
  const T=A.T, worm=A.worm;
  if(!worm||!worm.alive){ endTurn(0.5); return; }
  if(!A.target||!A.target.alive){ A.target=aiTarget(T); if(!A.target){ endTurn(0.5); return; } }
  A.t+=dt;
  if(A.phase==='think'){
    G.angle=Math.atan2(A.target? A.target.y-worm.y : -1, A.target? A.target.x-worm.x : 1);
    if(A.t>0.72){
      const mv=aiPickMove(T,worm,A.target);
      A.dir=mv.dir; A.dist=mv.dist;
      A.phase = mv.dir? 'move':'aim'; A.t=0; A.moveDur=clamp(mv.dist/1.6/60,0.15,1.6);
      if(A.dir) showBanner('','',T.color);
    }
  } else if(A.phase==='move'){
    worm.vx=A.dir*1.35;
    worm.walkT+=dt;
    if(A.t>A.moveDur*0.5 && Math.random()<dt*2.2 && Math.abs(worm.vx)>0.4&&worm.onGround){
      worm.vy=-6.6; Sound.jump();
    }
    if(A.t>A.moveDur){ worm.vx=0; A.phase='aim'; A.t=0;
      if(!A.target) A.target=aiTarget(T); }
  } else if(A.phase==='aim'){
    if(A.mode==='support'){
      if(A.weapon.id==='airstrike'){
        G.mouse.x=clamp(A.target.x-10,WATER_Y>0?40:40,W-40); G.mouse.y=A.target.y;
        if(A.t>0.8){ A.phase='fire'; A.t=0; }
      } else { // medkit on a hurt friend
        const hurt=worm.team.worms.filter(w=>w.alive).sort((a,b)=>a.hp-b.hp)[0]||worm;
        G.mouse.x=hurt.x; G.mouse.y=hurt.y;
        if(A.t>0.7){ A.phase='fire'; A.t=0; }
      }
      return;
    }
    if(!A.plan){
      const plan=aiPlanShot(T,worm,A.weapon,A.target);
      A.plan=plan||{ang:-1,power:70};
      A.noise=DIFF;
    }
    const k=Math.min(1,dt*3.4);
    G.angle=lerp(G.angle,A.plan.ang,k);
    G.power=lerp(G.power,A.plan.power,k*0.9);
    if(A.t>0.95){ A.phase='fire'; A.t=0; }
  } else if(A.phase==='fire'){
    const noise=DIFF;
    const ang=A.plan? A.plan.ang+rr(noise*0.06,-noise*0.06) : G.angle;
    const pow=clamp((A.plan?A.plan.power:70)+rr(noise*9,-noise*9),12,100);
    G.angle=ang; G.power=pow;
    const ok=fireWeapon(worm,A.weapon,pow);
    if(!ok){ A.weapon=WMAP.bazooka; fireWeapon(worm,WMAP.bazooka,pow); }
    A.phase='done';
  }
}

/* ------------------------------ render ------------------------------ */
const cv=document.getElementById('game'), ctx=cv.getContext('2d');
let cw=0, ch=0, dpr=1;
let clouds=[], stars=[], hills=[[],[]];
function resize(){
  dpr=Math.min(2,window.devicePixelRatio||1);
  cw=window.innerWidth; ch=window.innerHeight;
  cv.width=Math.floor(cw*dpr); cv.height=Math.floor(ch*dpr);
  ctx.setTransform(dpr,0,0,dpr,0,0);
  fit.v=Math.max(cw/W, ch/H);
}
window.addEventListener('resize',resize);
function buildDecor(seed){
  const r=mulberry32(seed);
  clouds=[]; stars=[]; hills=[[],[]];
  for(let i=0;i<11;i++) clouds.push({x:r()*W*1.6-W*0.3,y:40+r()*250,s:0.5+r()*1.5,sp:0.06+r()*0.16,o:0.1+r()*0.22});
  for(let i=0;i<130;i++) stars.push({x:r()*W,y:r()*430,s:r()*1.6+0.4,tw:r()*TAU});
  for(let L=0;L<2;L++){
    const amp=L?44:86, base=L?520:470, f=makeFbm(mulberry32(seed+L*77),L?0.004:0.0022);
    for(let x=-200;x<W+200;x+=14) hills[L].push({x,y:base+f(x)*amp});
  }
}
function drawSky(){
  const t=theme||THEMES[0];
  const g=ctx.createLinearGradient(0,0,0,ch);
  g.addColorStop(0,t.sky[0]); g.addColorStop(0.55,t.sky[1]); g.addColorStop(1,t.sky[2]);
  ctx.fillStyle=g; ctx.fillRect(0,0,cw,ch);
  // stars
  if(t.star){
    const tt=performance.now()*0.001;
    for(const s of stars){
      const a=0.35+0.65*Math.abs(Math.sin(tt*0.9+s.tw));
      ctx.fillStyle=`rgba(255,255,255,${a*0.75})`;
      ctx.fillRect(s.x,s.y,s.s,s.s);
    }
  }
  // sun / moon glow
  const sx=cw*0.78, sy=ch*0.16;
  const rg=ctx.createRadialGradient(sx,sy,4,sx,sy,ch*0.6);
  rg.addColorStop(0,t.sun); rg.addColorStop(0.12,hexA(t.sun,0.35)); rg.addColorStop(1,'rgba(0,0,0,0)');
  ctx.fillStyle=rg; ctx.fillRect(0,0,cw,ch);
  // clouds
  const drift=performance.now()*0.0004;
  for(const c of clouds){
    const px=((c.x+drift*(6+c.sp*26)*(wind>0?1:-1)*(0.4+Math.abs(wind)))-(cam.x-W/2)*0.22*(fit.v*cam.z))%(W*1.6)-200;
    const py=c.y-(cam.y-H/2)*0.22*(fit.v*cam.z);
    const s=c.s*(fit.v*cam.z)*0.55;
    ctx.save(); ctx.translate(px,py); ctx.scale(s,s);
    const cg=ctx.createRadialGradient(0,0,4,0,0,60);
    cg.addColorStop(0,`rgba(255,255,255,${c.o})`); cg.addColorStop(0.6,`rgba(255,255,255,${c.o*0.5})`);
    cg.addColorStop(1,'rgba(255,255,255,0)');
    ctx.fillStyle=cg; ctx.beginPath(); ctx.ellipse(0,0,60,22,0,0,TAU); ctx.fill();
    ctx.beginPath(); ctx.ellipse(-26,6,34,15,0,0,TAU); ctx.fill();
    ctx.beginPath(); ctx.ellipse(30,5,38,17,0,0,TAU); ctx.fill();
    ctx.restore();
  }
}
function hexA(hex,a){
  const c=hex2rgb(hex); return `rgba(${c},${a})`;
}
function drawHills(){
  const t=theme||THEMES[0];
  const sc=fit.v*cam.z;
  for(let L=0;L<2;L++){
    const par=L?0.5:0.3;
    const offX=-(cam.x-W/2)*par*sc+W*0.5*(1-par)*(sc-1);
    const offY=-(cam.y-H/2)*par*sc;
    ctx.save();
    ctx.translate(cw/2+offX, ch/2+offY); ctx.scale(sc,sc); ctx.translate(-W/2,-H/2);
    ctx.beginPath(); ctx.moveTo(-220,H+120);
    for(const p of hills[L]) ctx.lineTo(p.x,p.y);
    ctx.lineTo(W+220,H+120); ctx.closePath();
    ctx.fillStyle=t.far[L]; ctx.globalAlpha=L?0.95:0.75; ctx.fill(); ctx.globalAlpha=1;
    // haze top
    ctx.strokeStyle=hexA(t.haze,L?0.18:0.25); ctx.lineWidth=3; ctx.stroke();
    ctx.restore();
  }
}
function drawWater(){
  const t=theme||THEMES[0];
  const sc=fit.v*cam.z;
  const wy=WATER_Y;
  if(wy>H+10) return;
  const g=ctx.createLinearGradient(0,wy-10,0,H);
  g.addColorStop(0,hexA(t.water[0],0.72)); g.addColorStop(0.35,hexA(t.water[0],0.86));
  g.addColorStop(1,hexA(t.water[1],0.96));
  ctx.save();
  ctx.beginPath();
  const tt=performance.now()*0.0013;
  const step=16;
  const XO=-3400, XW=W+3400;
  ctx.moveTo(XO,wy+6);
  for(let x=XO;x<=XW;x+=step){
    const y=wy+Math.sin(x*0.022+tt*1.6)*3.4+Math.sin(x*0.061-tt*2.3)*1.8;
    ctx.lineTo(x,y);
  }
  ctx.lineTo(XW,H+60); ctx.lineTo(XO,H+60); ctx.closePath();
  ctx.fillStyle=g; ctx.fill();
  // surface highlight
  ctx.beginPath();
  for(let x=XO;x<=XW;x+=step){
    const y=wy+Math.sin(x*0.022+tt*1.6)*3.4+Math.sin(x*0.061-tt*2.3)*1.8;
    x===XO?ctx.moveTo(x,y):ctx.lineTo(x,y);
  }
  ctx.strokeStyle='rgba(255,255,255,.42)'; ctx.lineWidth=2; ctx.stroke();
  // sparkles
  for(let i=0;i<26;i++){
    const x=(i*137+ (tt*30)%W)%W;
    const y=wy+14+((i*53)%80);
    const a=0.1+0.18*Math.abs(Math.sin(tt*1.4+i));
    ctx.fillStyle=`rgba(255,255,255,${a})`;
    ctx.fillRect(x,y, 14+((i*7)%16), 1.4);
  }
  // caustic light under the surface
  ctx.globalCompositeOperation='lighter';
  for(let i=0;i<7;i++){
    const x=((i*211+tt*40)%(W+200))-100;
    const rg=ctx.createRadialGradient(x,wy+30,2,x,wy+30,90);
    rg.addColorStop(0,'rgba(255,255,255,.05)'); rg.addColorStop(1,'rgba(255,255,255,0)');
    ctx.fillStyle=rg; ctx.fillRect(x-90,wy,180,H-wy);
  }
  ctx.globalCompositeOperation='source-over';
  ctx.restore();
}
function drawWorm(w,isActive,team){
  if(!w.alive){ drawGrave(w); return; }
  const t=w.team;
  const sq=1+w.squish*0.28, st=1-w.squish*0.2;
  const wob=Math.sin(w.anim)*0.12;
  ctx.save();
  ctx.globalAlpha = w.inWater? 0.72 : 1;
  ctx.translate(w.x,w.y);
  // shadow
  ctx.fillStyle='rgba(0,0,0,.3)';
  ctx.beginPath(); ctx.ellipse(0,w.r*0.85,w.r*1.05*st,3.2,0,0,TAU); ctx.fill();
  ctx.rotate(wob*0.3);
  ctx.scale(st,sq);
  // body
  const g=ctx.createLinearGradient(0,-w.r,0,w.r);
  g.addColorStop(0,t.color2); g.addColorStop(0.45,t.color); g.addColorStop(1,shade(t.color,-0.45));
  ctx.beginPath(); ctx.ellipse(0,0,w.r*1.02,w.r*0.98,0,0,TAU);
  ctx.fillStyle=g; ctx.fill();
  ctx.lineWidth=1.6; ctx.strokeStyle=shade(t.color,-0.62); ctx.stroke();
  // belly
  ctx.beginPath(); ctx.ellipse(0,w.r*0.28,w.r*0.62,w.r*0.5,0,0,TAU);
  ctx.fillStyle='rgba(255,255,255,.2)'; ctx.fill();
  // eyes
  const look=(()=>{ if(isActive&&(G.phase==='player'||G.phase==='aim')) return Math.atan2(G.mouse.y-w.y,G.mouse.x-w.x);
    return w.facing>0?0:Math.PI; })();
  const ex=Math.cos(look)*1.6, ey=Math.sin(look)*1.2-1.6;
  for(const s of [-1,1]){
    const bx=s*3.6;
    ctx.beginPath(); ctx.ellipse(bx,-4.2,3.1,3.4,0,0,TAU); ctx.fillStyle='#f7fbff'; ctx.fill();
    ctx.lineWidth=1; ctx.strokeStyle='rgba(30,36,50,.55)'; ctx.stroke();
    const bl = w.blink<0? 0.35 : 1;
    if(bl<1){ ctx.beginPath(); ctx.ellipse(bx,-4.2,3.1,3.4*bl,0,0,TAU); ctx.fillStyle=shade(t.color,-0.2); ctx.fill(); }
    else { ctx.beginPath(); ctx.ellipse(bx+ex,-4.2+ey,1.5,1.6,0,0,TAU); ctx.fillStyle='#1b2230'; ctx.fill();
      ctx.beginPath(); ctx.arc(bx+ex-0.5,-5+ey,0.55,0,TAU); ctx.fillStyle='#fff'; ctx.fill(); }
  }
  // mouth – worried when hurt
  ctx.beginPath();
  if(w.hp<40){ ctx.arc(0,3.2,2.6,0.15*Math.PI,0.85*Math.PI); }
  else { ctx.arc(0,1.4,2.4,0.2*Math.PI,0.8*Math.PI); }
  ctx.strokeStyle='rgba(25,30,45,.75)'; ctx.lineWidth=1.2; ctx.stroke();
  if(w.flash>0){ ctx.globalAlpha=w.flash*0.7; ctx.fillStyle='#fff';
    ctx.beginPath(); ctx.ellipse(0,0,w.r*1.05,w.r*1.02,0,0,TAU); ctx.fill(); ctx.globalAlpha=1; }
  ctx.restore();
  ctx.globalAlpha=1;
  // hp bar
  if(w.hp<w.maxhp||isActive){
    const bw=26, bh=4, by=w.y-w.r-13;
    ctx.fillStyle='rgba(6,9,18,.72)'; roundRect(w.x-bw/2-1,by-1,bw+2,bh+2,3); ctx.fill();
    const hpc=w.hp>60?'#6ef2a5':w.hp>30?'#ffd43b':'#ff6b6b';
    ctx.fillStyle=hpc; roundRect(w.x-bw/2,by,bw*clamp(w.hp/100,0,1),bh,2); ctx.fill();
  }
  if(isActive){
    const bob=Math.sin(performance.now()*0.005)*3;
    ctx.save(); ctx.translate(w.x, w.y-w.r-26+bob);
    ctx.beginPath(); ctx.moveTo(0,8); ctx.lineTo(-7,-3); ctx.lineTo(7,-3); ctx.closePath();
    ctx.fillStyle=w.team.color; ctx.fill();
    ctx.strokeStyle='rgba(0,0,0,.45)'; ctx.lineWidth=1.4; ctx.stroke();
    ctx.restore();
    // team ring
    ctx.beginPath(); ctx.arc(w.x,w.y,w.r+7,0,TAU);
    ctx.strokeStyle=hexA(w.team.color,0.42+0.2*Math.sin(performance.now()*0.005)); ctx.lineWidth=2; ctx.stroke();
  }
}
function drawGrave(w){
  ctx.save(); ctx.translate(w.x,Math.min(w.y,WATER_Y+80)); ctx.globalAlpha=0.75;
  ctx.fillStyle='rgba(190,195,205,.85)';
  roundRect(-7,-12,14,17,4); ctx.fill();
  ctx.fillStyle='rgba(120,126,140,.9)'; roundRect(-7,3,14,3.4,1.6); ctx.fill();
  ctx.fillStyle='#ffd166'; ctx.font='bold 8px Trebuchet MS'; ctx.textAlign='center';
  ctx.fillText(w.idx+1,0,-2);
  ctx.restore();
}
function shade(hex,amt){
  const c=hex2rgb(hex).split(',').map(Number);
  return `rgb(${c.map(v=>clamp(Math.round(amt>0?lerp(v,255,amt):v*(1+amt)),0,255)).join(',')})`;
}
function roundRect(x,y,w,h,r){
  ctx.beginPath();
  ctx.moveTo(x+r,y); ctx.arcTo(x+w,y,x+w,y+h,r); ctx.arcTo(x+w,y+h,x,y+h,r);
  ctx.arcTo(x,y+h,x,y,r); ctx.arcTo(x,y,x+w,y,r); ctx.closePath();
}
function drawParticles(){
  for(const p of parts){
    const k=p.age/p.life, a=1-k;
    switch(p.type){
      case 'smoke':{
        const s=lerp(p.size,p.size2,k);
        const g=ctx.createRadialGradient(p.x,p.y,0,p.x,p.y,s);
        g.addColorStop(0,p.col+(a*0.5)+')'); g.addColorStop(1,p.col+'0)');
        ctx.fillStyle=g; ctx.beginPath(); ctx.arc(p.x,p.y,s,0,TAU); ctx.fill(); break; }
      case 'dust':{
        ctx.fillStyle=p.col+(a*0.6)+')';
        ctx.beginPath(); ctx.arc(p.x,p.y,lerp(p.size,p.size2,k),0,TAU); ctx.fill(); break; }
      case 'spark':{
        ctx.strokeStyle=p.col+a+')'; ctx.lineWidth=p.size*0.8; ctx.beginPath();
        ctx.moveTo(p.x,p.y); ctx.lineTo(p.x-p.vx*1.8,p.y-p.vy*1.8); ctx.stroke(); break; }
      case 'goo':{
        ctx.fillStyle=p.col+(a*0.85)+')';
        ctx.beginPath(); ctx.arc(p.x,p.y,lerp(p.size,p.size*0.4,k),0,TAU); ctx.fill(); break; }
      case 'bubble':{
        ctx.strokeStyle=p.col+(a*0.7)+')'; ctx.lineWidth=1;
        ctx.beginPath(); ctx.arc(p.x,p.y,p.size,0,TAU); ctx.stroke(); break; }
      case 'drop':{
        ctx.fillStyle=p.col+(a*0.8)+')';
        ctx.beginPath(); ctx.ellipse(p.x,p.y,p.size*0.7,p.size,0,0,TAU); ctx.fill(); break; }
      case 'ring':{
        const s=lerp(p.size,p.size2,k);
        ctx.strokeStyle=p.col+(a*0.9)+')'; ctx.lineWidth=lerp(7,0.6,k);
        ctx.beginPath(); ctx.arc(p.x,p.y,s,0,TAU); ctx.stroke(); break; }
      case 'flash':{
        const s=lerp(p.size,p.size2,k);
        const g=ctx.createRadialGradient(p.x,p.y,0,p.x,p.y,s);
        g.addColorStop(0,'rgba(255,255,255,'+a*0.95+')');
        g.addColorStop(0.5,p.col+a*0.6+')'); g.addColorStop(1,p.col+'0)');
        ctx.fillStyle=g; ctx.beginPath(); ctx.arc(p.x,p.y,s,0,TAU); ctx.fill(); break; }
      case 'star':{
        ctx.fillStyle=p.col+a+')';
        const s=lerp(p.size,p.size*0.2,k);
        ctx.save(); ctx.translate(p.x,p.y); ctx.rotate(p.age*4);
        ctx.beginPath();
        for(let i=0;i<4;i++){ const ang=i*Math.PI/2; ctx.moveTo(0,0);
          ctx.lineTo(Math.cos(ang)*s*2,Math.sin(ang)*s*2); }
        ctx.lineWidth=1.4; ctx.strokeStyle=p.col+a+')'; ctx.stroke(); ctx.restore(); break; }
      case 'trail':{
        ctx.fillStyle=p.col+a*0.5+')';
        ctx.beginPath(); ctx.arc(p.x,p.y,lerp(p.size,p.size2,k),0,TAU); ctx.fill(); break; }
      case 'tracer':{
        ctx.strokeStyle=p.col+Math.max(a,0.3)+')'; ctx.lineWidth=2.2;
        ctx.beginPath(); ctx.moveTo(p.x,p.y); ctx.lineTo(p.ex,p.ey); ctx.stroke();
        ctx.lineWidth=5; ctx.strokeStyle=p.col+(a*0.25)+')'; ctx.stroke(); break; }
    }
  }
}
function drawChunks(){
  for(const c of chunks){
    ctx.save(); ctx.globalAlpha=clamp(c.fade,0,1);
    ctx.translate(c.x,c.y); ctx.rotate(c.rot);
    ctx.fillStyle=c.col;
    ctx.beginPath();
    const n=5;
    for(let i=0;i<n;i++){
      const a=i/n*TAU, rr2=c.r*(0.75+0.5*hash2((c.x|0)+i,(c.y|0)-i));
      i? ctx.lineTo(Math.cos(a)*rr2,Math.sin(a)*rr2):ctx.moveTo(Math.cos(a)*rr2,Math.sin(a)*rr2);
    }
    ctx.closePath(); ctx.fill();
    ctx.strokeStyle='rgba(0,0,0,.28)'; ctx.lineWidth=1; ctx.stroke();
    ctx.restore();
  }
}
function drawProjectiles(){
  for(const p of projs){
    ctx.save(); ctx.translate(p.x,p.y);
    if(p.kind==='bomb'){ ctx.rotate(Math.atan2(p.vy,p.vx)+Math.PI/2);
      ctx.fillStyle='#cfd6e0'; ctx.beginPath(); ctx.ellipse(0,0,4.6,7.6,0,0,TAU); ctx.fill();
      ctx.fillStyle='#9aa5b1'; ctx.beginPath(); ctx.moveTo(-4,5); ctx.lineTo(0,11); ctx.lineTo(4,5); ctx.closePath(); ctx.fill();
    } else if(p.kind==='plane'){
      ctx.scale(p.vx>0?1:-1,1);
      ctx.fillStyle='#c9d2de';
      ctx.beginPath(); ctx.moveTo(-16,0); ctx.lineTo(6,-4); ctx.lineTo(16,-1); ctx.lineTo(16,2); ctx.lineTo(6,4); ctx.closePath(); ctx.fill();
      ctx.fillStyle='#8f9aa8'; ctx.beginPath(); ctx.moveTo(2,0); ctx.lineTo(-10,-2); ctx.lineTo(-10,2); ctx.closePath(); ctx.fill();
      ctx.fillStyle='#6b7684'; ctx.beginPath(); ctx.moveTo(-16,0); ctx.lineTo(-22,-8); ctx.lineTo(-18,0); ctx.closePath(); ctx.fill();
    } else if(p.kind==='dyn'){
      ctx.rotate(p.age*2);
      ctx.fillStyle='#c73e3e'; ctx.beginPath(); ctx.arc(0,0,5.2,0,TAU); ctx.fill();
      ctx.fillStyle='#f0d9a8'; ctx.fillRect(-5,0,10,1.6);
      const f=Math.abs(Math.sin(performance.now()*0.02));
      ctx.fillStyle=`rgba(255,${180+60*f},80,${0.5+0.5*f})`;
      ctx.beginPath(); ctx.arc(0,-7-2*f,2.4+1.6*f,0,TAU); ctx.fill();
    } else if(p.kind==='crate'){
      ctx.rotate(Math.sin(performance.now()*0.003)*0.1);
      ctx.fillStyle='#f2f4f8'; roundRect(-10,-8,20,16,3); ctx.fill();
      ctx.fillStyle='#e04b4b'; ctx.fillRect(-10,-8,20,4);
      ctx.fillStyle='#e04b4b'; ctx.fillRect(-1.8,-4,3.6,10);
      ctx.fillRect(-6.5,0.5,13,3);
      if(p.chute){
        ctx.strokeStyle='rgba(240,245,255,.8)'; ctx.lineWidth=1.2;
        ctx.beginPath(); ctx.moveTo(-9,-8); ctx.lineTo(-14,-24); ctx.moveTo(9,-8); ctx.lineTo(14,-24); ctx.stroke();
        ctx.fillStyle='rgba(255,120,120,.9)';
        ctx.beginPath(); ctx.moveTo(-17,-22); ctx.quadraticCurveTo(0,-38,17,-22);
        ctx.quadraticCurveTo(0,-30,-17,-22); ctx.fill();
      }
    } else {
      // rocket / grenade / holy
      const holy=p.weapon&&p.weapon.id==='holy';
      if(holy){
        const tt=performance.now()*0.006;
        ctx.globalCompositeOperation='lighter';
        const g=ctx.createRadialGradient(0,0,1,0,0,22);
        g.addColorStop(0,`rgba(255,250,200,${0.5+0.25*Math.sin(tt)})`);
        g.addColorStop(1,'rgba(255,220,120,0)');
        ctx.fillStyle=g; ctx.beginPath(); ctx.arc(0,0,22,0,TAU); ctx.fill();
        ctx.globalCompositeOperation='source-over';
        ctx.rotate(Math.sin(tt)*0.2);
        ctx.fillStyle='#f5e9c8'; ctx.beginPath(); ctx.arc(0,0,6.4,0,TAU); ctx.fill();
        ctx.strokeStyle='#c89b3c'; ctx.lineWidth=1.4;
        ctx.beginPath(); ctx.moveTo(-3.4,-1.6); ctx.lineTo(3.4,-1.6); ctx.moveTo(0,-5); ctx.lineTo(0,1.8); ctx.stroke();
        ctx.strokeStyle='rgba(255,230,140,.9)';
        ctx.beginPath(); ctx.ellipse(0,-9,6,2,0,0,TAU); ctx.stroke();
      } else if(p.bouncy){
        ctx.rotate(p.spin);
        ctx.fillStyle='#6f8f52'; ctx.beginPath(); ctx.arc(0,0,5,0,TAU); ctx.fill();
        ctx.fillStyle='#93b96b'; ctx.beginPath(); ctx.arc(-1.6,-1.6,2,0,TAU); ctx.fill();
        ctx.fillStyle='#b8c0c8'; ctx.fillRect(-1.6,-7.6,3.2,2.8);
      } else {
        ctx.rotate(Math.atan2(p.vy,p.vx));
        ctx.fillStyle='#e8eef7'; ctx.beginPath(); ctx.ellipse(0,0,7,3.4,0,0,TAU); ctx.fill();
        ctx.fillStyle='#ff9c5b'; ctx.beginPath();
        ctx.moveTo(-7,0); ctx.lineTo(-14,-3.4); ctx.lineTo(-14,3.4); ctx.closePath(); ctx.fill();
        ctx.fillStyle='#ffd06b'; ctx.beginPath(); ctx.arc(2.4,0,2.2,0,TAU); ctx.fill();
      }
    }
    ctx.restore();
  }
}
function drawTexts(){
  ctx.textAlign='center';
  for(const t of texts){
    const k=t.age/t.life, a=1-k*k;
    ctx.save();
    ctx.globalAlpha=a;
    ctx.font=`bold ${t.size*(1+0.18*(1-Math.min(1,k*4)))}px Trebuchet MS, sans-serif`;
    ctx.lineWidth=4; ctx.strokeStyle='rgba(6,9,18,.85)';
    ctx.strokeText(t.txt,t.x,t.y);
    ctx.fillStyle=t.col; ctx.fillText(t.txt,t.x,t.y);
    ctx.restore();
  }
  ctx.textAlign='left';
}
function drawAim(){
  if(!G.active||!G.active.alive) return;
  const worm=G.active;
  const human = !worm.team.cpu;
  const wep = currentWeapon();
  if(!WEP_READY(wep)) return;
  if(human && (G.phase!=='player'||G.shotFired)) { /* still draw while charging */ }
  const isHumanTurn = human && (G.phase==='player'||G.phase==='jetpack');
  if(!isHumanTurn) return;
  // reticle of the mouse
  const mx=G.mouse.x,my=G.mouse.y;
  if(wep.kind==='utility'||wep.kind==='support'){
    const ok = wep.id!=='teleport' || (!solidAt(mx,my+8)&&!solidAt(mx,my)&&my<WATER_Y-14);
    ctx.save(); ctx.translate(mx,my);
    ctx.strokeStyle=ok?'rgba(140,247,197,.95)':'rgba(255,120,120,.95)'; ctx.lineWidth=2;
    ctx.beginPath(); ctx.arc(0,0,16,0,TAU); ctx.stroke();
    ctx.beginPath(); ctx.moveTo(-22,0); ctx.lineTo(-9,0); ctx.moveTo(9,0); ctx.lineTo(22,0);
    ctx.moveTo(0,-22); ctx.lineTo(0,-9); ctx.moveTo(0,9); ctx.lineTo(0,22); ctx.stroke();
    if(wep.id==='teleport'||wep.id==='medkit'){
      ctx.setLineDash([4,4]);
      ctx.beginPath(); ctx.arc(0,0,42,0,TAU);
      ctx.strokeStyle='rgba(255,255,255,.35)'; ctx.lineWidth=1.4; ctx.stroke(); ctx.setLineDash([]);
    }
    ctx.restore();
    return;
  }
  // aim direction line
  const m=muzzlePos(worm);
  const a=G.angle;
  ctx.save();
  let path=null, impact=null;
  if(wep.kind==='direct'){
    const hit=raycast(worm,a,940);
    impact=hit;
    ctx.beginPath(); ctx.moveTo(m.x,m.y); ctx.lineTo(hit.x,hit.y);
    ctx.strokeStyle='rgba(255,120,120,.35)'; ctx.lineWidth=5; ctx.stroke();
    ctx.setLineDash([7,7]); ctx.strokeStyle='rgba(255,235,170,.95)'; ctx.lineWidth=1.8;
    ctx.stroke(); ctx.setLineDash([]);
  } else {
    const sim=simShot(m.x,m.y,a,G.power,wep);
    path=sim.path; impact=sim;
    ctx.setLineDash([3,7]); ctx.lineWidth=2.6; ctx.strokeStyle='rgba(255,255,255,.28)';
    ctx.beginPath();
    for(let i=0;i<path.length;i+=2){ i?ctx.lineTo(path[i].x,path[i].y):ctx.moveTo(path[i].x,path[i].y); }
    ctx.stroke();
    ctx.setLineDash([]);
    ctx.lineWidth=1.6; ctx.strokeStyle=hexA(worm.team.color,0.85);
    ctx.beginPath();
    for(let i=0;i<path.length;i+=2){ i?ctx.lineTo(path[i].x,path[i].y):ctx.moveTo(path[i].x,path[i].y); }
    ctx.stroke();
    // dotted markers along the arc
    for(let i=10;i<path.length;i+=26){
      ctx.beginPath(); ctx.arc(path[i].x,path[i].y,1.9,0,TAU);
      ctx.fillStyle='rgba(255,255,255,.55)'; ctx.fill();
    }
  }
  if(impact){
    const hitWorm=impact.worm;
    const col = impact.water? '#79c8ff' : hitWorm? '#ff6b6b' : impact.terrain? '#ffd166' : 'rgba(255,255,255,.7)';
    const R=wep.kind==='direct'?10:wep.radius*0.9;
    ctx.strokeStyle=col; ctx.lineWidth=2;
    ctx.beginPath(); ctx.arc(impact.x,impact.y,R,0,TAU); ctx.stroke();
    ctx.setLineDash([3,4]);
    ctx.beginPath(); ctx.arc(impact.x,impact.y,R*0.55,0,TAU); ctx.stroke();
    ctx.setLineDash([]);
    ctx.beginPath(); ctx.moveTo(impact.x-R-6,impact.y); ctx.lineTo(impact.x-R+2,impact.y);
    ctx.moveTo(impact.x+R+6,impact.y); ctx.lineTo(impact.x+R-2,impact.y);
    ctx.moveTo(impact.x,impact.y-R-6); ctx.lineTo(impact.x,impact.y-R+2);
    ctx.moveTo(impact.x,impact.y+R+6); ctx.lineTo(impact.x,impact.y+R-2);
    ctx.stroke();
  }
  ctx.restore();
  // power arc under the worm
  if(G.charging&&wep.kind==='indirect'){
    const pw=G.power/100;
    ctx.save(); ctx.translate(worm.x,worm.y);
    ctx.beginPath(); ctx.arc(0,0,worm.r+11,-Math.PI*0.5,-Math.PI*0.5+pw*TAU);
    ctx.strokeStyle=`hsl(${lerp(140,0,pw)},90%,60%)`; ctx.lineWidth=4; ctx.lineCap='round'; ctx.stroke();
    ctx.restore();
  }
}
function drawWindStreaks(){
  if(Math.abs(wind)<0.06) return;
  const tt=performance.now()*0.001;
  ctx.save(); ctx.strokeStyle=`rgba(255,255,255,${0.05+Math.abs(wind)*0.11})`; ctx.lineWidth=1.4;
  for(let i=0;i<26;i++){
    const y=(i*47)%H;
    const sp=wind*(130+ (i%5)*36);
    let x=(tt*sp + i*211)%(W+300)-150;
    const len=12+Math.abs(wind)*40;
    ctx.beginPath(); ctx.moveTo(x,y); ctx.lineTo(x+ (wind>0?len:-len),y); ctx.stroke();
  }
  ctx.restore();
}
function render(){
  const sc=fit.v*cam.z;
  ctx.setTransform(dpr,0,0,dpr,0,0);
  ctx.clearRect(0,0,cw,ch);
  drawSky();
  drawHills();
  drawWindStreaks();
  ctx.save();
  ctx.translate(cw/2+cam.sx*sc, ch/2+cam.sy*sc);
  ctx.scale(sc,sc);
  ctx.translate(-cam.x,-cam.y);
  // terrain
  ctx.imageSmoothingEnabled=false;
  ctx.drawImage(TC,0,0);
  ctx.imageSmoothingEnabled=true;
  drawChunks();
  drawWater();
  // worms
  const active=G.active;
  for(const w of worms) drawWorm(w,w===active&&w.alive,w.team);
  drawProjectiles();
  drawParticles();
  drawAim();
  drawTexts();
  ctx.restore();
  // vignette
  const vg=ctx.createRadialGradient(cw/2,ch/2,Math.min(cw,ch)*0.32,cw/2,ch/2,Math.max(cw,ch)*0.78);
  vg.addColorStop(0,'rgba(0,0,0,0)'); vg.addColorStop(1,'rgba(0,0,0,.42)');
  ctx.fillStyle=vg; ctx.fillRect(0,0,cw,ch);
  if(cam.flash>0.002){
    ctx.fillStyle=`rgba(255,246,225,${Math.min(0.55,cam.flash*0.55)})`;
    ctx.fillRect(0,0,cw,ch);
  }
  if(G.hitstop>0){
    ctx.fillStyle=`rgba(255,255,255,${Math.min(0.1,G.hitstop*1.1)})`;
    ctx.fillRect(0,0,cw,ch);
  }
}
/* ------------------------------ HUD (DOM) ------------------------------ */
let G_wepIdx = 0;
function currentWeapon(){ return WEAPONS[G_wepIdx]; }
function WEP_READY(w){ const T=teams[G.turnTeam]; if(!T) return false;
  return w.ammo===Infinity || (T.ammo[w.id]||0)>0; }
function buildHud(){
  const box=document.getElementById('teams'); box.innerHTML='';
  teams.forEach(T=>{
    const el=document.createElement('div');
    el.className='team'; el.dataset.t=T.idx;
    el.style.setProperty('--tc',T.color);
    el.innerHTML=`<div class="thead"><span class="nm"><i class="dot" style="background:${T.color};color:${T.color}"></i>${T.name}${T.cpu?' · CPU':''}</span><span class="tot">400</span></div>
      <div class="units">${T.worms.map(w=>`<div class="uchip" data-u="${T.idx}-${w.idx}"><i style="background:linear-gradient(90deg,${T.color2},${T.color});color:${T.color}"></i></div>`).join('')}</div>`;
    box.appendChild(el);
  });
  const strip=document.getElementById('weaponStrip'); strip.innerHTML='';
  WEAPONS.forEach((w,i)=>{
    const d=document.createElement('div');
    d.className='wslot'; d.dataset.i=i;
    d.innerHTML=`<span class="wkey">${(i+1)%10}</span><svg viewBox="0 0 24 24">${ICON[w.icon]}</svg><span class="ammo"></span>`;
    d.addEventListener('mouseenter',()=>{ if(Sound.on()) Sound.hover();
      const tip=document.getElementById('weaponTip');
      tip.querySelector('.wn').textContent=w.name;
      tip.querySelector('.wd').textContent='— '+w.desc;
      tip.classList.add('show'); });
    d.addEventListener('mouseleave',()=>document.getElementById('weaponTip').classList.remove('show'));
    d.addEventListener('click',()=>selectWeapon(i));
    strip.appendChild(d);
  });
  updateAmmoUI();
}
function selectWeapon(i){
  const w=WEAPONS[i]; if(!w) return;
  const T=teams[G.turnTeam];
  if(T && w.ammo!==Infinity && (T.ammo[w.id]||0)<=0){ Sound.deny();
    addToast('No '+w.name+' left!','#ff8787'); return; }
  if(G.phase==='jetpack'){ return; }
  G_wepIdx=i; Sound.select(); updateAmmoUI();
}
function updateAmmoUI(){
  const T=teams[G.turnTeam]; if(!T) return;
  document.querySelectorAll('.wslot').forEach(el=>{
    const i=+el.dataset.i, w=WEAPONS[i];
    const amt = w.ammo===Infinity?'∞':(T.ammo[w.id]||0);
    el.querySelector('.ammo').textContent=amt;
    el.classList.toggle('empty', w.ammo!==Infinity && (T.ammo[w.id]||0)<=0);
    el.classList.toggle('sel', i===G_wepIdx);
  });
}
let hudPrevSec=-99;
function updateHud(force){
  const T=teams[G.turnTeam];
  const t=Math.max(0,G.time);
  const tf=document.getElementById('timerFill'), tt=document.getElementById('timerText'), tw=document.getElementById('timerWrap');
  tf.style.width=clamp(t/TURN_TIME*100,0,100)+'%';
  tt.textContent = t>0? Math.ceil(t) : (G.over?'–':'OT');
  tw.classList.toggle('hot', t<=24&&t>12);
  tw.classList.toggle('warn', t<=12&&t>5);
  tw.classList.toggle('crit', t<=5);
  const wn=document.getElementById('windNeedle'), wv=document.getElementById('windVal');
  wn.style.left=(50+clamp(wind,-1,1)*46)+'%';
  const wmag=Math.abs(wind);
  wv.textContent=(wind<0?'◀ ':'')+(wmag*100).toFixed(0)+(wind>0?' ▶':'');
  wv.style.color= wmag>0.66?'#ff8787': wmag>0.33?'#ffd43b':'#8cd8ff';
  // teams
  document.querySelectorAll('.team').forEach(el=>{
    const ti=+el.dataset.t, Tm=teams[ti];
    if(!Tm){ el.style.display='none'; return; }
    const hp=Tm.worms.reduce((a,w)=>a+Math.max(0,w.hp),0);
    el.querySelector('.tot').textContent=fmt(hp)+' hp';
    el.classList.toggle('active', ti===G.turnTeam && !G.over);
    el.classList.toggle('out', !Tm.alive);
    Tm.worms.forEach(w=>{
      const chip=el.querySelector(`.uchip[data-u="${ti}-${w.idx}"]`);
      if(!chip) return;
      chip.querySelector('i').style.width=clamp(w.hp/100*100,0,100)+'%';
      chip.classList.toggle('dead',!w.alive);
    });
  });
  // power
  const pb=document.getElementById('powerBox');
  const show = G.charging && T && !T.cpu && G.phase==='player';
  pb.classList.toggle('show',!!show);
  document.getElementById('powerFill').style.height=(G.power)+'%';
  document.getElementById('powerNum').textContent=Math.round(G.power);
  document.getElementById('menuBtns').classList.toggle('show',G.phase!=='title');
}
function hudTick(){
  const t=Math.max(0,G.time);
  const sec=Math.ceil(t);
  if(G.phase==='player'&&!G.shotFired&&sec!==hudPrevSec&&sec<=10&&sec>0){
    hudPrevSec=sec; Sound.tick(sec<=5);
    if(sec<=3){ document.getElementById('timerText').animate(
      [{transform:'scale(1.6)'},{transform:'scale(1)'}],{duration:260,easing:'ease-out'}); }
  }
}

/* ------------------------------ input ------------------------------ */
const keys={};
function keyName(e){ return (e.key||'').toLowerCase(); }
window.addEventListener('keydown',e=>{
  if(['arrowup','arrowdown','arrowleft','arrowright',' '].includes(keyName(e))) e.preventDefault();
  Sound.init();
  const k=keyName(e);
  if(keys[k]) return; keys[k]=true;
  if(G.phase==='title'||G.over) return;
  const T=teams[G.turnTeam]; if(!T||T.cpu) return;
  const worm=G.active; if(!worm) return;
  if(k>='1'&&k<='9'){ selectWeapon((+k)-1); }
  if(k==='0'){ selectWeapon(9); }
  if(k==='t'){ const i=WEAPONS.findIndex(w=>w.id==='teleport'); selectWeapon(i); }
  const canMove = G.phase==='player' && G.time>0 && (!G.shotFired||currentWeapon().kind==='placed');
  if((k==='w'||k==='arrowup'||k===' ') && canMove && worm.onGround && !worm.inWater){
    worm.vy=-7.9; worm.onGround=false; worm.squish=0.6; Sound.jump();
    dust(worm.x,worm.y+worm.r*0.8,5,`rgba(${theme.dirt.join(',')},`,1.6);
  }
});
window.addEventListener('keyup',e=>{ keys[keyName(e)]=false; });
window.addEventListener('blur',()=>{ for(const k in keys) keys[k]=false; });

cv.addEventListener('contextmenu',e=>e.preventDefault());
cv.addEventListener('pointerdown',e=>{
  Sound.init();
  if(G.phase==='title'||G.over) return;
  const r=cv.getBoundingClientRect();
  const p=screenToWorld(e.clientX-r.left, e.clientY-r.top);
  G.mouse=p;
  const T=teams[G.turnTeam]; if(!T||T.cpu) return;
  const worm=G.active; if(!worm||!worm.alive) return;
  const wep=currentWeapon();
  if(G.phase==='jetpack'){ if(e.button===2) endTurn(0.4); return; }
  if(G.phase!=='player') return;
  if(!WEP_READY(wep)){ Sound.deny(); return; }
  if(e.button===2){ if(G.charging){G.charging=false;G.power=0;} return; }
  worm.facing = p.x>worm.x?1:-1;
  G.angle=Math.atan2(p.y-(worm.y-4), p.x-worm.x);
  if(wep.kind==='indirect'){ G.charging=true; G.power=0; Sound.select(); cam.shake=Math.min(cam.shake+0.6,2); }
  else { fireWeapon(worm,wep,70); }
});
window.addEventListener('pointerup',e=>{
  if(!G.charging) return;
  const T=teams[G.turnTeam], worm=G.active;
  G.charging=false;
  if(worm&&T&&!T.cpu&&G.phase==='player'){
    const wep=currentWeapon();
    if(G.power<6){ Sound.deny(); G.power=0; return; }
    const pw=G.power; G.power=0;
    fireWeapon(worm,wep,pw);
  }
});
cv.addEventListener('pointermove',e=>{
  const r=cv.getBoundingClientRect();
  const p=screenToWorld(e.clientX-r.left, e.clientY-r.top);
  G.mouse=p;
  const worm=G.active;
  const T=teams[G.turnTeam];
  if(worm&&worm.alive&&T&&!T.cpu&&(G.phase==='player'||G.phase==='jetpack')){
    G.angle=Math.atan2(p.y-(worm.y-4), p.x-worm.x);
    if(pointerDown) worm.facing = p.x>worm.x?1:-1;
  }
});
let pointerDown=false;
cv.addEventListener('pointerdown',()=>{pointerDown=true;});
window.addEventListener('pointerup',()=>{pointerDown=false;});

/* ------------------------------ simulation ------------------------------ */
let wind=0;
let walkAcc=0;
function canMove(){
  const T=teams[G.turnTeam]; if(!T||T.cpu) return false;
  return G.phase==='player' && G.time>0 && (!G.shotFired||currentWeapon().kind==='placed');
}
function updateGame(dtS){
  const dt=dtS*60;                       // physics frames
  // wind easing
  wind=lerp(wind,G.wind,Math.min(1,dtS*1.2));
  if(G.hitstop>0){ G.hitstop-=dtS; return; }
  // particles always
  updateParticles(dt,dtS);
  const T=teams[G.turnTeam];
  const worm=G.active;
  if(G.phase==='banner'){
    G.bannerT-=dtS;
    if(G.bannerT<=0){ G.phase='player'; hudPrevSec=-99;
      if(T.cpu){ aiStart(); } else { updateAmmoUI(); addToast('Your move — '+WEAPONS[G_wepIdx].name+' ready',T.color); } }
  } else if(G.phase==='player'){
    if(G.time>0) G.time-=dtS;
    if(T.cpu){ if(G.ai) aiUpdate(dtS); }
    else {
      // charge up the shot
      if(G.charging && worm&&worm.alive) G.power=Math.min(100,G.power+dtS*95);
      if(worm&&worm.alive && canMove()){
        let mvx=0;
        if(keys['a']||keys['arrowleft']) mvx-=1;
        if(keys['d']||keys['arrowright']) mvx+=1;
        if(mvx){ worm.vx=mvx*1.38; worm.facing=mvx;
          if(!G.charging) walkAcc+=dtS*(1+Math.abs(worm.vx));
          if(walkAcc>0.26){ walkAcc=0; Sound.step();
            dust(worm.x-worm.facing*5,worm.y+worm.r*0.8,2,`rgba(${theme.dirt.join(',')},`,1.1); } }
        else if(worm.onGround) worm.vx*=Math.pow(0.02,dt);
      } else if(worm&&worm.onGround) worm.vx*=Math.pow(0.05,dt);
    }
    if(worm&&!worm.alive){ G.charging=false; endTurn(1.1); }
    if(G.dynExploded){ G.dynExploded=false; endTurn(1.1); }
    if(G.shotFired){ G.fireT=(G.fireT||0)+dtS;
      if(G.fireT>4.5 && projs.length===0){ G.fireT=0; endTurn(0.6); } }
    if(G.time<-10 && !G.shotFired){ G.charging=false; addToast('Out of time!','#ff8787'); endTurn(0.3); }
  } else if(G.phase==='burst'){
    const b=G.burst;
    if(b){
      b.t+=dtS;
      let guard=0;
      while(b.t>=b.gap && b.left>0 && guard++<8){ b.t-=b.gap; b.left--; fireBurst(b.worm,b.weapon); }
      if(b.left<=0){ G.phase='burstEnd'; G.burstT=0.55; }
    } else G.phase='player';
  } else if(G.phase==='burstEnd'){
    G.burstT-=dtS;
    if(G.burstT<=0){
      const w=G.burst?G.burst.weapon:null;
      const amt = w&&w.ammo!==Infinity ? (T.ammo[w.id]||0) : 0;
      if(amt>0 && w){ G.phase='player'; G.burst=null; updateAmmoUI(); addToast(w.name+' — '+amt+' use left',T.color); }
      else { G.burst=null; endTurn(0.7); }
    }
  } else if(G.phase==='jetpack'){
    if(worm&&worm.alive){
      const thrust = (pointerDown||keys['w']) ? 1:0;
      if(thrust && worm.fuel>0){
        worm.vy-=1.05*dt; worm.fuel-=dtS*34;
        if(worm.fuel<0) worm.fuel=0;
        for(let i=0;i<2;i++) spawnPart({type:'spark',x:worm.x+rr(5,-5),y:worm.y+9,
          vx:rr(1.2,-1.2),vy:rr(3,1.4),size:rr(3.4,1.6),g:0.05,drag:0.9,life:rr(.35,.15),
          col: Math.random()<.5?'rgba(255,170,70,':'rgba(255,230,140,'});
      }
      let mvx=0;
      if(keys['a']||keys['arrowleft']) mvx-=1;
      if(keys['d']||keys['arrowright']) mvx+=1;
      worm.vx=lerp(worm.vx,mvx*2.0,Math.min(1,dtS*3));
      worm.vy=clamp(worm.vy,-9,12);
      if(worm.fuel<=0){ addToast('Out of fuel!','#ff8787'); endTurn(0.5); }
      if(worm.inWater){ endTurn(0.4); }
    }
  } else if(G.phase==='flight'){
    if(projs.length===0){
      const settled = worms.every(w=>!w.alive||w.onGround||w.inWater);
      if(settled && !G.over){ G.flightT=(G.flightT||0)+dtS; if(G.flightT>0.55){ G.flightT=0; endTurn(0.1);} }
      else G.flightT=0;
    } else G.flightT=0;
  } else if(G.phase==='settle'){
    G.pendingEnd-=dtS;
    if(G.pendingEnd<=0) finishTurn();
  }
  // projectiles + worms
  updateProjectiles(dt,dtS);
  for(const w of worms) stepWorm(w,dt,dtS);
  // falling into water splash for worms
  for(const w of worms){
    if(!w.alive||w.inWater) continue;
    if(w.y>WATER_Y-2){ Sound.splash(); for(let i=0;i<14;i++) spawnPart({type:'drop',x:w.x+rr(10,-10),y:WATER_Y,vx:rr(2.6,-2.6),vy:rr(-3,-7),size:rr(3.4,1.4),g:0.32,life:rr(.7,.3),col:'rgba(200,235,255,'}); wave(); }
  }
  updateCamera(dtS);
  // sudden-death tide
  if(G.waterRise>0 && WATER_Y>360){ WATER_Y-=dtS*2.4; }
}
function updateCamera(dtS){
  const worm=G.active;
  let fx=cam.x, fy=cam.y, z=1.0;
  if(projs.length){
    let minx=1e9,maxx=-1e9,miny=1e9,maxy=-1e9,n=0;
    for(const p of projs){ if(p.kind==='plane'&&p.x<-30) continue;
      minx=Math.min(minx,p.x);maxx=Math.max(maxx,p.x);miny=Math.min(miny,p.y);maxy=Math.max(maxy,p.y);n++; }
    if(n){
      if(worm&&worm.alive){ minx=Math.min(minx,worm.x);maxx=Math.max(maxx,worm.x);miny=Math.min(miny,worm.y);maxy=Math.max(maxy,worm.y); }
      fx=(minx+maxx)/2; fy=(miny+maxy)/2;
      const bw=maxx-minx+420, bh=maxy-miny+300;
      z=clamp(Math.min(cw/bw, ch/bh)/fit.v, 0.62, 1.22);
    }
  } else if(worm){
    fx=worm.x; fy=worm.y-10;
    z = (G.phase==='jetpack')? 1.35 : (G.charging? 1.28 : 1.12);
    if(worm.inWater) z=1.2;
  }
  if(G.phase==='title'||G.over){
    const t=performance.now()*0.00006;
    fx=W*0.5+Math.sin(t)*W*0.16; fy=H*0.5+Math.sin(t*0.7)*40; z=1.02;
  }
  camFocus(fx,fy,z,dtS,projs.length?5.5:3.2);
  // keep the view inside the world horizontally (no void at the edges)
  const halfW=cw/(2*fit.v*Math.max(0.3,cam.z));
  if(halfW*2<W) cam.x=clamp(cam.x,halfW,W-halfW); else cam.x=W*0.5;
  cam.scale=fit.v*cam.z;
}
function updateCameraShake(dt,dtS){
  cam.shake*=Math.pow(0.9,dt);
  if(cam.shake<0.06) cam.shake=0;
  cam.sx=rr(cam.shake,-cam.shake); cam.sy=rr(cam.shake,-cam.shake);
  cam.flash*=Math.pow(0.9,dt);
  if(cam.frz>0) cam.frz-=dtS;
}

/* ------------------------------ main loop ------------------------------ */
let lastTs=0;
function loop(ts){
  let dtS=Math.min(0.05,(lastTs? (ts-lastTs)/1000:0.0167)); lastTs=ts;
  if(!(dtS>0)) dtS=0.0167;
  const dtF=Math.min(3,dtS*60);
  if(G.phase!=='title') updateGame(dtS); else { updateParticles(dtF,dtS); updateCamera(dtS); }
  updateCameraShake(dtF,dtS);
  hudTick();
  render();
  if(G.phase!=='title') updateHud();
  requestAnimationFrame(loop);
}

/* ------------------------------ setup menus ------------------------------ */
const COLORS=TEAM_DEF;
let setup=[{on:true,cpu:false},{on:true,cpu:true},{on:false,cpu:true},{on:false,cpu:true}];
let curDiff='normal';
function buildTeamSetup(){
  const box=document.getElementById('teamSetup'); box.innerHTML='';
  setup.forEach((s,i)=>{
    const row=document.createElement('div');
    row.className='trow'+(s.on?'':' off');
    row.innerHTML=`<i class="chip" style="background:${COLORS[i].c};color:${COLORS[i].c}"></i>
      <span class="tn">Team ${COLORS[i].name}</span>
      <span class="tgl">
        <button data-a="human" class="${s.on&&!s.cpu?'on':''}">HUMAN</button>
        <button data-a="cpu" class="${s.on&&s.cpu?'on':''}">CPU</button>
        <button data-a="off" class="${!s.on?'on':''}">OFF</button>
      </span>`;
    row.querySelectorAll('button').forEach(b=>{
      b.addEventListener('mouseenter',()=>Sound.hover());
      b.addEventListener('click',()=>{
        const a=b.dataset.a;
        if(a==='off') s.on=false;
        else { s.on=true; s.cpu=(a==='cpu'); }
        const live=setup.filter(x=>x.on).length;
        if(live<2){ addToast('Need at least 2 teams!','#ff8787'); Sound.deny(); s.on=true; s.cpu=false; }
        else Sound.select();
        buildTeamSetup(); updateSoundBtn();
      });
    });
    box.appendChild(row);
  });
}
function buildDiff(){
  const row=document.getElementById('diffRow'); row.innerHTML='';
  const names={chill:'Chill',normal:'Normal',brutal:'Brutal'};
  Object.keys(names).forEach(k=>{
    const b=document.createElement('button');
    b.textContent=names[k]; b.className=k===curDiff?'on':'';
    b.addEventListener('mouseenter',()=>Sound.hover());
    b.addEventListener('click',()=>{ curDiff=k; Sound.select(); buildDiff(); });
    row.appendChild(b);
  });
}
function buildArsenal(){
  const box=document.getElementById('arsenal');
  box.innerHTML=WEAPONS.map(w=>`<div><svg viewBox="0 0 24 24">${ICON[w.icon]}</svg><span>${w.name}</span></div>`).join('');
}
let soundOn=true;
function updateSoundBtn(){
  const b=document.getElementById('btnSound');
  b.textContent = soundOn? '🔊 SOUND ON':'🔇 SOUND OFF';
  const m=document.getElementById('btnMute');
  if(m) m.textContent = soundOn? '🔊':'🔇';
}
function startGame(){
  Sound.init();
  DIFF=DIFFS[curDiff];
  const shape=pick(['island','two','cave','island','two']);
  const seed=Math.floor(Math.random()*999999);
  projs.length=0; parts.length=0; chunks.length=0; texts.length=0;
  WATER_Y=598; G.waterRise=0; G.sudden=false; G.over=false; G.turnCount=0;
  G.matchId=(G.matchId||0)+1;
  G.hitstop=0; G.burst=null; G.ai=null; G.charging=false; G.power=0; G.shotFired=false;
  buildDecor(seed);
  genTerrain(shape,seed);
  makeTeams(setup);
  makeWorms();
  G_wepIdx=0;
  cam.x=W/2; cam.y=H/2; cam.z=1; cam.shake=0; cam.flash=0;
  buildHud();
  document.getElementById('title').classList.add('hide');
  document.getElementById('over').classList.add('hide');
  updateSoundBtn();
  const label={island:'a single island',two:'twin islands',cave:'the deep caverns'}[shape];
  addToast('The battlefield: '+label,'#8cd8ff');
  G.phase='banner';
  startTurn(Math.floor(Math.random()*Math.max(1,teams.length)));
}
/* ------------------------------ boot ------------------------------ */
function boot(){
  resize();
  buildDecor(4242);
  genTerrain(pick(['island','two','cave']),Math.floor(Math.random()*99999));
  makeTeams([{on:true,cpu:false},{on:true,cpu:true},{on:false,cpu:true},{on:false,cpu:true}]);
  makeWorms();
  buildTeamSetup(); buildDiff(); buildArsenal(); buildHud(); updateSoundBtn();
  document.getElementById('btnStart').addEventListener('click',()=>{ startGame(); });
  document.getElementById('btnSound').addEventListener('click',()=>{
    Sound.init(); soundOn=!Sound.toggle(); if(soundOn) Sound.select(); updateSoundBtn(); });
  document.getElementById('btnMute').addEventListener('click',()=>{
    soundOn=!Sound.toggle(); if(soundOn) Sound.select(); updateSoundBtn(); });
  document.getElementById('btnQuit').addEventListener('click',()=>{
    Sound.back(); G.phase='title'; document.getElementById('title').classList.remove('hide');
    document.getElementById('menuBtns').classList.remove('show'); updateHud(); });
  document.getElementById('btnRematch').addEventListener('click',()=>{ Sound.select(); startGame(); });
  document.getElementById('btnMenu').addEventListener('click',()=>{ Sound.back();
    document.getElementById('over').classList.add('hide');
    document.getElementById('title').classList.remove('hide');
    G.phase='title'; updateHud(); });
  document.querySelectorAll('button').forEach(b=>{
    b.addEventListener('mouseenter',()=>Sound.hover());
  });
  G.phase='title';
  updateHud();
  requestAnimationFrame(loop);
}
window.addEventListener('pointerdown',()=>Sound.init(),{once:true});
window.addEventListener('keydown',()=>Sound.init(),{once:true});
boot();
</script>
</body>
</html>
<!-- agent-meta {"model":"deepseek/deepseek-v4.1-flash","provider":"openrouter","persona":"full","sessionId":"6cb4b0dd-39e8-46d2-98e0-95ee880d9465","tokensIn":42730683,"tokensOut":169245,"tokensTotal":42899928,"cost":0.289883658,"turns":128,"toolCalls":127,"failedToolCalls":0,"timestamp":"2026-09-10T22:59:11.952Z"} -->