← Worms results

Worms

Claude Opus 5 · 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 95,469 bytes · SHA-256 c6a7f8fb9460
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>WORMFRONT — Artillery Tactics</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
html,body{width:100%;height:100%;overflow:hidden;background:#05070d;font-family:"Trebuchet MS","Segoe UI",system-ui,sans-serif;color:#e8f0ff;user-select:none;-webkit-user-select:none}
#game{position:fixed;inset:0;display:block;cursor:crosshair;image-rendering:pixelated}
#ui{position:fixed;inset:0;pointer-events:none;z-index:10}

/* ---------- HUD ---------- */
#teamBar{position:absolute;top:10px;left:50%;transform:translateX(-50%);display:flex;gap:10px}
.tcard{min-width:150px;background:linear-gradient(180deg,rgba(14,22,38,.88),rgba(8,12,22,.92));
 border:1px solid rgba(255,255,255,.12);border-radius:10px;padding:7px 10px 8px;backdrop-filter:blur(8px);
 box-shadow:0 6px 22px rgba(0,0,0,.5);transition:transform .25s,box-shadow .25s,opacity .3s}
.tcard.active{transform:translateY(4px) scale(1.06)}
.tcard.dead{opacity:.32;filter:grayscale(1)}
.tname{font-size:11px;letter-spacing:2px;font-weight:bold;text-transform:uppercase;display:flex;justify-content:space-between;align-items:center}
.tname i{font-style:normal;font-size:9px;opacity:.6;letter-spacing:1px}
.hpwrap{height:9px;background:rgba(0,0,0,.55);border-radius:5px;margin-top:5px;overflow:hidden;position:relative;border:1px solid rgba(255,255,255,.08)}
.hpfill{height:100%;border-radius:4px;transition:width .45s cubic-bezier(.2,.9,.3,1)}
.hpghost{position:absolute;inset:0;height:100%;background:rgba(255,90,90,.5);border-radius:4px;transition:width .9s ease .18s}
.pips{display:flex;gap:3px;margin-top:5px}
.pip{flex:1;height:4px;border-radius:2px;background:rgba(255,255,255,.14);transition:background .3s}

#timer{position:absolute;top:12px;left:20px;font-size:44px;font-weight:bold;font-variant-numeric:tabular-nums;
 text-shadow:0 0 22px currentColor,0 3px 8px #000;transition:transform .12s;color:#8fe9ff;line-height:1}
#timerlbl{position:absolute;top:60px;left:22px;font-size:9px;letter-spacing:4px;opacity:.55}

#wind{position:absolute;top:14px;right:20px;text-align:right}
#windLbl{font-size:9px;letter-spacing:4px;opacity:.55;margin-bottom:2px}
#windBar{width:170px;height:16px;position:relative;background:rgba(0,0,0,.5);border:1px solid rgba(255,255,255,.14);border-radius:8px;overflow:hidden}
#windFill{position:absolute;top:0;bottom:0;left:50%;background:linear-gradient(90deg,#4ad6ff,#7affc8);transition:width .5s,left .5s,background .5s}
#windBar:after{content:"";position:absolute;left:50%;top:0;bottom:0;width:1px;background:rgba(255,255,255,.5)}
#windTxt{font-size:10px;opacity:.8;margin-top:3px;letter-spacing:1px}

#weaponHud{position:absolute;bottom:14px;left:50%;transform:translateX(-50%);display:flex;gap:7px;pointer-events:auto}
.wslot{width:56px;height:60px;background:linear-gradient(180deg,rgba(16,24,42,.9),rgba(8,12,22,.94));border:1px solid rgba(255,255,255,.11);
 border-radius:9px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;cursor:pointer;
 transition:transform .14s,border-color .14s,box-shadow .14s;position:relative;overflow:hidden}
.wslot:hover{transform:translateY(-5px);border-color:rgba(255,255,255,.4)}
.wslot.sel{border-color:#7affc8;box-shadow:0 0 18px rgba(122,255,200,.4),inset 0 0 20px rgba(122,255,200,.12);transform:translateY(-7px)}
.wslot.out{opacity:.3;filter:grayscale(1)}
.wico{font-size:21px;line-height:1}
.wnm{font-size:7px;letter-spacing:.5px;opacity:.75;text-align:center;padding:0 2px}
.wammo{position:absolute;top:2px;right:4px;font-size:8px;font-weight:bold;color:#ffd166}
.wkey{position:absolute;top:2px;left:4px;font-size:8px;opacity:.4}

#power{position:absolute;bottom:88px;left:50%;transform:translateX(-50%);width:280px;height:13px;
 background:rgba(0,0,0,.6);border:1px solid rgba(255,255,255,.2);border-radius:7px;overflow:hidden;opacity:0;transition:opacity .2s}
#powerFill{height:100%;width:0%;background:linear-gradient(90deg,#4ad6ff,#ffd166,#ff5d5d)}

#msg{position:absolute;top:34%;left:50%;transform:translate(-50%,-50%);font-size:46px;font-weight:bold;
 letter-spacing:8px;text-transform:uppercase;text-shadow:0 4px 30px #000;opacity:0;pointer-events:none;text-align:center}
#sub{position:absolute;top:34%;left:50%;transform:translate(-50%,26px);font-size:13px;letter-spacing:5px;opacity:0;text-align:center}

#hint{position:absolute;bottom:84px;left:18px;font-size:10px;line-height:1.7;opacity:.4;letter-spacing:1px}
#invBar{position:absolute;bottom:14px;right:18px;display:flex;gap:7px;pointer-events:auto}

/* ---------- Menu ---------- */
#menu{position:fixed;inset:0;z-index:40;display:flex;align-items:center;justify-content:center;
 background:radial-gradient(ellipse at 50% 40%,#122036 0%,#070a12 60%,#04060a 100%)}
#menuInner{width:min(780px,92vw);text-align:center}
h1{font-size:clamp(38px,7vw,76px);letter-spacing:12px;font-weight:900;
 background:linear-gradient(180deg,#fff,#7affc8 45%,#2b9bd6);-webkit-background-clip:text;background-clip:text;color:transparent;
 filter:drop-shadow(0 6px 30px rgba(60,220,255,.35))}
.tag{font-size:11px;letter-spacing:9px;opacity:.5;margin-top:2px;margin-bottom:26px}
.row{display:flex;gap:10px;justify-content:center;align-items:center;margin:12px 0;flex-wrap:wrap}
.lbl{font-size:10px;letter-spacing:4px;opacity:.5;width:100%;margin-bottom:2px}
.btn{padding:9px 17px;background:rgba(255,255,255,.05);border:1px solid rgba(255,255,255,.16);border-radius:8px;
 color:#dfe9ff;font-size:12px;letter-spacing:2px;cursor:pointer;transition:all .16s;font-family:inherit}
.btn:hover{background:rgba(122,255,200,.14);border-color:#7affc8;transform:translateY(-2px)}
.btn.on{background:rgba(122,255,200,.2);border-color:#7affc8;color:#fff;box-shadow:0 0 16px rgba(122,255,200,.25)}
#play{margin-top:26px;padding:15px 54px;font-size:17px;letter-spacing:6px;font-weight:bold;
 background:linear-gradient(180deg,rgba(122,255,200,.22),rgba(40,160,200,.18));border-color:#7affc8}
.tsetup{display:flex;gap:8px;justify-content:center;flex-wrap:wrap;margin-top:6px}
.tbox{width:120px;padding:8px;border-radius:9px;border:1px solid rgba(255,255,255,.13);background:rgba(255,255,255,.03)}
.tbox .tn{font-size:11px;font-weight:bold;letter-spacing:2px;margin-bottom:6px}
.tog{font-size:9px;padding:4px 0;border-radius:6px;cursor:pointer;border:1px solid rgba(255,255,255,.15);letter-spacing:2px;transition:all .15s}
.tog:hover{background:rgba(255,255,255,.1)}
#credits{position:absolute;bottom:14px;width:100%;text-align:center;font-size:9px;letter-spacing:3px;opacity:.28}
#fade{position:fixed;inset:0;background:#04060a;z-index:60;pointer-events:none;opacity:0;transition:opacity .4s}
#dmgFlash{position:fixed;inset:0;pointer-events:none;z-index:9;background:radial-gradient(ellipse at center,transparent 40%,rgba(255,40,40,.5));opacity:0;transition:opacity .35s}
</style>
</head>
<body>
<canvas id="game"></canvas>
<div id="dmgFlash"></div>
<div id="ui">
  <div id="timer">60</div><div id="timerlbl">TURN</div>
  <div id="teamBar"></div>
  <div id="wind"><div id="windLbl">WIND</div><div id="windBar"><div id="windFill"></div></div><div id="windTxt">CALM</div></div>
  <div id="msg"></div><div id="sub"></div>
  <div id="power"><div id="powerFill"></div></div>
  <div id="weaponHud"></div>
  <div id="invBar"></div>
  <div id="hint">← → MOVE &nbsp;·&nbsp; ENTER JUMP &nbsp;·&nbsp; ↑↓ AIM &nbsp;·&nbsp; SPACE HOLD=POWER<br>1-9 WEAPON &nbsp;·&nbsp; TAB NEXT &nbsp;·&nbsp; MOUSE AIM &nbsp;·&nbsp; M MUTE</div>
</div>
<div id="menu"><div id="menuInner">
  <h1>WORMFRONT</h1><div class="tag">ARTILLERY TACTICS</div>
  <div class="row"><div class="lbl">TERRAIN</div>
    <button class="btn on" data-map="random">RANDOM</button>
    <button class="btn" data-map="cave">CAVERN</button>
    <button class="btn" data-map="island">ISLAND</button>
    <button class="btn" data-map="twin">TWIN ISLES</button></div>
  <div class="row"><div class="lbl">TEAMS</div>
    <button class="btn" data-teams="2">2</button>
    <button class="btn on" data-teams="3">3</button>
    <button class="btn" data-teams="4">4</button></div>
  <div class="row"><div class="lbl">TURN TIME</div>
    <button class="btn" data-time="30">30s</button>
    <button class="btn on" data-time="45">45s</button>
    <button class="btn" data-time="60">60s</button></div>
  <div class="row"><div class="lbl">ROSTER</div><div class="tsetup" id="tsetup"></div></div>
  <button class="btn" id="play">DEPLOY</button>
  <div id="credits">HOTSEAT · CPU · DESTRUCTIBLE WORLD</div>
</div></div>
<div id="fade"></div>
<script>
"use strict";
/* ============================================================
   WORMFRONT — single-file artillery game
   ============================================================ */
const TAU=Math.PI*2, rnd=(a=1,b=0)=>Math.random()*(a-b)+b, ri=(a,b)=>Math.floor(rnd(b+1,a));
const clamp=(v,a,b)=>v<a?a:v>b?b:v, lerp=(a,b,t)=>a+(b-a)*t;
const dist=(x,y,x2,y2)=>Math.hypot(x2-x,y2-y);
const pick=a=>a[Math.floor(Math.random()*a.length)];

/* ---------------- AUDIO ---------------- */
const Snd=(()=>{
let ctx=null,master=null,muted=false;
function init(){if(ctx)return;ctx=new (window.AudioContext||window.webkitAudioContext)();
 master=ctx.createGain();master.gain.value=.35;
 const comp=ctx.createDynamicsCompressor();comp.threshold.value=-18;comp.ratio.value=8;
 master.connect(comp);comp.connect(ctx.destination);}
function now(){return ctx.currentTime}
function env(g,t,a,d,pk=1){g.gain.setValueAtTime(.0001,t);g.gain.exponentialRampToValueAtTime(pk,t+a);g.gain.exponentialRampToValueAtTime(.0001,t+a+d)}
function tone(f,f2,dur,type="sine",vol=.3,dly=0,atk=.005){
 if(!ctx||muted)return;const t=now()+dly;const o=ctx.createOscillator(),g=ctx.createGain();
 o.type=type;o.frequency.setValueAtTime(f,t);if(f2!=null)o.frequency.exponentialRampToValueAtTime(Math.max(1,f2),t+dur);
 env(g,t,atk,dur,vol);o.connect(g);g.connect(master);o.start(t);o.stop(t+dur+atk+.05);}
function noise(dur,vol=.3,dly=0,f0=1200,f1=200,q=1,type="lowpass"){
 if(!ctx||muted)return;const t=now()+dly;const n=Math.floor(ctx.sampleRate*dur);
 const buf=ctx.createBuffer(1,n,ctx.sampleRate),d=buf.getChannelData(0);
 for(let i=0;i<n;i++)d[i]=(Math.random()*2-1)*(1-i/n);
 const s=ctx.createBufferSource();s.buffer=buf;const flt=ctx.createBiquadFilter();flt.type=type;
 flt.frequency.setValueAtTime(f0,t);flt.frequency.exponentialRampToValueAtTime(Math.max(20,f1),t+dur);flt.Q.value=q;
 const g=ctx.createGain();env(g,t,.004,dur,vol);
 s.connect(flt);flt.connect(g);g.connect(master);s.start(t);}
return{
 init,mute(){muted=!muted;return muted},isMuted:()=>muted,
 hover(){tone(880,1180,.05,"triangle",.055)},
 click(){tone(520,880,.08,"square",.1);tone(1300,1600,.05,"sine",.05,.02)},
 walk(){tone(rnd(150,110),rnd(80,60),.06,"triangle",.07)},
 jump(){tone(300,760,.18,"sine",.16);tone(600,1200,.1,"triangle",.05,.02)},
 fire(){noise(.22,.4,0,2400,180,1);tone(180,50,.28,"sawtooth",.28)},
 shotgun(){noise(.3,.5,0,5000,220,.6);tone(120,40,.3,"square",.25)},
 laser(){tone(1600,240,.22,"sawtooth",.2);tone(3200,700,.16,"square",.06)},
 airstrike(){tone(220,180,1.2,"sawtooth",.13);tone(226,184,1.2,"sawtooth",.13)},
 explode(mag=1){
  noise(.55*mag,.55,0,900*mag,60,.8);
  tone(110/mag,26,.55*mag,"sine",.45);
  tone(70,20,.8*mag,"triangle",.3,.02);
  noise(.9*mag,.18,.05,400,40,.5);},
 tick(p){tone(1000+p*900,null,.035,"square",.06+p*.1)},
 bounce(){tone(rnd(420,300),rnd(200,140),.07,"triangle",.1)},
 splash(){noise(.5,.32,0,1800,140,.7);tone(500,120,.35,"sine",.14)},
 hurt(){tone(420,120,.2,"square",.16);noise(.14,.12,0,900,200)},
 die(){tone(400,60,.7,"sawtooth",.22);tone(260,40,.9,"triangle",.16,.06);noise(.5,.15,.1,600,80)},
 pickup(){[660,880,1320].forEach((f,i)=>tone(f,f*1.02,.14,"triangle",.14,i*.06))},
 heal(){[520,660,784,1046].forEach((f,i)=>tone(f,f,.16,"sine",.12,i*.05))},
 turn(){[392,523,659].forEach((f,i)=>tone(f,f,.2,"triangle",.13,i*.07))},
 win(){[523,659,784,1046,1318].forEach((f,i)=>tone(f,f,.42,"triangle",.16,i*.11))},
 drill(){noise(.3,.16,0,700,500,4,"bandpass")},
 teleport(){tone(200,2400,.35,"sine",.18);tone(2400,200,.35,"triangle",.09,.1)},
 chute(){noise(.6,.1,0,600,200,.5)},
 clank(){tone(1400,700,.09,"square",.09);noise(.08,.1,0,3000,900)},
 rope(){tone(900,1400,.06,"square",.07)},
 wind(v){}
};})();

/* ---------------- CONFIG ---------------- */
const W=()=>innerWidth,H=()=>innerHeight;
const MAPW=2600, MAPH=1100;
const GRAV=0.34, WATER_Y=MAPH-118;
const TEAMS=[
 {name:"CRIMSON", col:"#ff5d6e", col2:"#8e1f34", hat:"#ffd7dc"},
 {name:"AZURE",   col:"#5db8ff", col2:"#1c4d8e", hat:"#d7ecff"},
 {name:"VERDANT", col:"#7affa0", col2:"#1f7a44", hat:"#dcffe6"},
 {name:"AMBER",   col:"#ffc75d", col2:"#8e5f1c", hat:"#fff0d7"}
];
const NAMES=["Bogsley","Nubbin","Grit","Wriggler","Squelch","Torpid","Blip","Mucus","Chaff","Gnash",
"Pip","Slag","Waddle","Crumb","Nozzle","Fizz","Tumbler","Grub","Squib","Noodle","Klank","Dredge"];

/* ---------------- WEAPONS ---------------- */
// cat: 'arc' indirect | 'direct' instant | 'aoe' map-altering | 'util'
const WEAPONS=[
 {id:"bazooka",name:"BAZOOKA",ico:"🚀",cat:"arc",ammo:Infinity,pw:1,dmg:45,rad:52,wind:1,key:"1",
  desc:"Wind-affected rocket"},
 {id:"grenade",name:"GRENADE",ico:"💣",cat:"arc",ammo:Infinity,pw:.9,dmg:48,rad:56,wind:.35,fuse:3,bounce:.55,key:"2",
  desc:"3s fuse, bounces"},
 {id:"cluster",name:"CLUSTER",ico:"🧨",cat:"arc",ammo:4,pw:.9,dmg:26,rad:36,wind:.5,fuse:2.6,bounce:.4,cluster:6,key:"3",
  desc:"Splits into 6 bomblets"},
 {id:"shotgun",name:"SHOTGUN",ico:"🔫",cat:"direct",ammo:6,dmg:26,rad:26,shots:2,pellets:7,spread:.09,range:520,key:"4",
  desc:"2 blasts, hitscan cone"},
 {id:"railgun",name:"RAILGUN",ico:"⚡",cat:"direct",ammo:2,dmg:58,rad:22,pierce:true,range:1800,key:"5",
  desc:"Pierces terrain & worms"},
 {id:"mine",name:"MINE",ico:"🕳️",cat:"arc",ammo:3,pw:.55,dmg:52,rad:60,wind:.15,bounce:.2,proximity:true,key:"6",
  desc:"Armed proximity charge"},
 {id:"holy",name:"HOLY BOMB",ico:"🔔",cat:"aoe",ammo:1,pw:.85,dmg:95,rad:125,wind:.4,fuse:2.4,bounce:.3,shake:26,key:"7",
  desc:"Devastating blast"},
 {id:"airstrike",name:"AIRSTRIKE",ico:"✈️",cat:"aoe",ammo:2,dmg:38,rad:52,target:true,shake:16,key:"8",
  desc:"Click a target zone"},
 {id:"drill",name:"DRILL BOMB",ico:"🛠️",cat:"aoe",ammo:2,pw:.8,dmg:34,rad:46,wind:.3,drill:true,key:"9",
  desc:"Burrows then detonates"},
 {id:"quake",name:"TREMOR",ico:"🌋",cat:"aoe",ammo:1,dmg:30,rad:70,quake:true,shake:34,key:"0",
  desc:"Collapses nearby terrain"},
 // utilities
 {id:"jetpack",name:"JETPACK",ico:"🎒",cat:"util",ammo:2,util:"jet",key:"J",desc:"Fly for 4 seconds"},
 {id:"teleport",name:"TELEPORT",ico:"🌀",cat:"util",ammo:2,util:"tele",key:"T",desc:"Blink anywhere"},
 {id:"girder",name:"GIRDER",ico:"🧱",cat:"util",ammo:3,util:"girder",key:"G",desc:"Place a platform"},
 {id:"medkit",name:"MEDPACK",ico:"➕",cat:"util",ammo:1,util:"heal",key:"H",desc:"Restore 45 HP"},
 {id:"chute",name:"PARACHUTE",ico:"🪂",cat:"util",ammo:2,util:"chute",key:"P",desc:"Negate fall damage"},
];
const WMAP={};WEAPONS.forEach(w=>WMAP[w.id]=w);

/* ---------------- STATE ---------------- */
let cv,ctx2d,terr,tctx,tdata,timg;
let G=null;
const keys={};let mouse={x:0,y:0,down:false,wx:0,wy:0};

/* ============================================================
   TERRAIN  — pixel mask (Uint8) + rendered canvas
   ============================================================ */
const T={w:MAPW,h:MAPH,mask:null,canvas:null,ctx:null,img:null,dirty:true};

function noise1D(n,octaves,seedArr){
 const out=new Float32Array(n);
 for(let o=0;o<octaves;o++){
  const freq=Math.pow(2,o),amp=Math.pow(0.55,o);
  const pts=Math.max(2,Math.ceil(freq*4)+1);const vals=[];
  for(let i=0;i<pts;i++)vals.push(Math.random()*2-1);
  for(let i=0;i<n;i++){
   const t=i/(n-1)*(pts-1);const i0=Math.floor(t),i1=Math.min(pts-1,i0+1);const f=t-i0;
   const s=f*f*(3-2*f);out[i]+=lerp(vals[i0],vals[i1],s)*amp;}
 }
 return out;
}

function genTerrain(kind){
 T.mask=new Uint8Array(MAPW*MAPH);
 const m=T.mask;
 const surf=new Float32Array(MAPW);
 if(kind==="cave"){
  const top=noise1D(MAPW,5), bot=noise1D(MAPW,5), mid=noise1D(MAPW,3);
  for(let x=0;x<MAPW;x++){
   const ceil=110+top[x]*95;
   const floor=MAPH-230+bot[x]*110;
   surf[x]=floor;
   for(let y=0;y<MAPH;y++){
    let solid = (y<ceil)||(y>floor);
    m[y*MAPW+x]=solid?1:0;
   }
  }
  // carve internal blobs + pillars
  for(let i=0;i<26;i++){
   const cx=rnd(MAPW),cy=rnd(MAPH-260,180),r=rnd(120,45);
   for(let y=Math.max(0,cy-r|0);y<Math.min(MAPH,cy+r);y++)for(let x=Math.max(0,cx-r|0);x<Math.min(MAPW,cx+r);x++){
    const d=dist(x,y,cx,cy);if(d<r*(0.75+0.25*Math.sin(x*0.03+y*0.02)))m[y*MAPW+x]=0;}
  }
  for(let i=0;i<16;i++){ // stalactites/mites & floating platforms
   const cx=rnd(MAPW),cy=rnd(MAPH-300,240),rw=rnd(150,50),rh=rnd(46,14);
   for(let y=Math.max(0,cy-rh|0);y<Math.min(MAPH,cy+rh);y++)for(let x=Math.max(0,cx-rw|0);x<Math.min(MAPW,cx+rw);x++){
    const dx=(x-cx)/rw,dy=(y-cy)/rh;if(dx*dx+dy*dy<1)m[y*MAPW+x]=1;}
  }
 } else if(kind==="island"){
  const n=noise1D(MAPW,5);
  const cx=MAPW/2, span=MAPW*0.40;
  for(let x=0;x<MAPW;x++){
   const d=Math.abs(x-cx)/span;
   const dome=d<1?Math.cos(d*Math.PI/2):0;
   const base=MAPH-150 - dome*(430+n[x]*90) - n[x]*35;
   surf[x]=base;
   const thick = d<1? (200+dome*430) : 0;
   for(let y=0;y<MAPH;y++){
    const solid = y>base && y < base+thick+250 && d<1.02;
    m[y*MAPW+x]=solid?1:0;
   }
  }
  carveCaves(m,7);
 } else { // twin
  const n=noise1D(MAPW,5);
  for(let x=0;x<MAPW;x++){
   let best=0;
   for(const c of [MAPW*0.24,MAPW*0.76]){
    const d=Math.abs(x-c)/(MAPW*0.215);
    if(d<1)best=Math.max(best,Math.cos(d*Math.PI/2));
   }
   const base=MAPH-160-best*(400+n[x]*70)-n[x]*28;
   surf[x]=base;
   for(let y=0;y<MAPH;y++) m[y*MAPW+x]= (best>0.02 && y>base && y<base+240+best*400)?1:0;
  }
  // a thin bridge
  if(Math.random()<0.55){
   const by=MAPH-560+rnd(80,-40);
   for(let x=MAPW*0.42;x<MAPW*0.58;x++)for(let y=by;y<by+26;y++) if(y>0&&y<MAPH) m[(y|0)*MAPW+(x|0)]=1;
  }
  carveCaves(m,5);
 }
 buildTerrainCanvas(kind);
}
function carveCaves(m,count){
 for(let i=0;i<count;i++){
  let x=rnd(MAPW*0.85,MAPW*0.15),y=rnd(MAPH-260,MAPH-560),a=rnd(TAU);
  for(let s=0;s<180;s++){
   const r=rnd(42,18);
   for(let yy=Math.max(0,y-r|0);yy<Math.min(MAPH,y+r);yy++)for(let xx=Math.max(0,x-r|0);xx<Math.min(MAPW,x+r);xx++)
    if(dist(xx,yy,x,y)<r)m[yy*MAPW+xx]=0;
   a+=rnd(.5,-.5);x+=Math.cos(a)*10;y+=Math.sin(a)*8;
   if(x<40||x>MAPW-40||y<200||y>MAPH-90)break;
  }
 }
}

let terrainPalette;
function buildTerrainCanvas(kind){
 T.canvas=document.createElement("canvas");T.canvas.width=MAPW;T.canvas.height=MAPH;
 T.ctx=T.canvas.getContext("2d",{willReadFrequently:false});
 const palettes=[
  {top:"#7ee06a",top2:"#4fae43",soil:"#8a5a34",soil2:"#5d3a1f",deep:"#3a2415",rock:"#4a4550"},
  {top:"#8fe0d8",top2:"#3fa8a0",soil:"#5a6b8a",soil2:"#37445d",deep:"#222b3d",rock:"#4c5468"},
  {top:"#e5c56a",top2:"#b8913a",soil:"#a06a3a",soil2:"#6b4322",deep:"#3f2916",rock:"#5a4c40"},
  {top:"#d8a0e0",top2:"#8d4fa8",soil:"#6a4a7a",soil2:"#432c50",deep:"#2a1a33",rock:"#4d4058"}
 ];
 terrainPalette=pick(palettes);
 repaintTerrainRegion(0,0,MAPW,MAPH);
}

function repaintTerrainRegion(x0,y0,w,h){
 x0=clamp(x0|0,0,MAPW);y0=clamp(y0|0,0,MAPH);
 w=clamp(w|0,0,MAPW-x0);h=clamp(h|0,0,MAPH-y0);
 if(w<=0||h<=0)return;
 const p=terrainPalette;
 const img=T.ctx.createImageData(w,h);const d=img.data;const m=T.mask;
 const hexToRgb=hx=>[parseInt(hx.slice(1,3),16),parseInt(hx.slice(3,5),16),parseInt(hx.slice(5,7),16)];
 const cTop=hexToRgb(p.top),cTop2=hexToRgb(p.top2),cSoil=hexToRgb(p.soil),cSoil2=hexToRgb(p.soil2),cDeep=hexToRgb(p.deep),cRock=hexToRgb(p.rock);
 for(let y=0;y<h;y++){
  const gy=y0+y;
  for(let x=0;x<w;x++){
   const gx=x0+x;const i=(y*w+x)*4;
   if(!m[gy*MAPW+gx]){d[i+3]=0;continue;}
   // depth from surface
   let depth=0;
   while(depth<26 && gy-depth-1>=0 && m[(gy-depth-1)*MAPW+gx])depth++;
   const nz=(Math.sin(gx*0.11)*0.5+Math.sin(gy*0.17+gx*0.03)*0.5+Math.sin(gx*0.031+gy*0.041)*1.2);
   let c;
   if(depth<3){c=cTop;}
   else if(depth<7){c=lerpC(cTop,cTop2,(depth-3)/4);}
   else if(depth<12){c=lerpC(cTop2,cSoil,(depth-7)/5);}
   else if(depth<20){c=lerpC(cSoil,cSoil2,(depth-12)/8);}
   else {c=cSoil2;}
   // deep rock band
   if(gy>MAPH-300){const t=clamp((gy-(MAPH-300))/220,0,1);c=lerpC(c,cRock,t*0.75);}
   if(depth>=24){c=lerpC(c,cDeep,.5)}
   const v=nz*9 + (Math.random()*6-3);
   d[i]=clamp(c[0]+v,0,255);d[i+1]=clamp(c[1]+v,0,255);d[i+2]=clamp(c[2]+v,0,255);d[i+3]=255;
  }
 }
 T.ctx.putImageData(img,x0,y0);
}
function lerpC(a,b,t){return [lerp(a[0],b[0],t),lerp(a[1],b[1],t),lerp(a[2],b[2],t)]}

function solidAt(x,y){
 x=x|0;y=y|0;
 if(x<0||x>=MAPW||y<0)return false;
 if(y>=MAPH)return false;
 return T.mask[y*MAPW+x]===1;
}
function addSolid(x0,y0,w,h){
 for(let y=Math.max(0,y0);y<Math.min(MAPH,y0+h);y++)for(let x=Math.max(0,x0);x<Math.min(MAPW,x0+w);x++)T.mask[y*MAPW+x]=1;
 repaintTerrainRegion(x0-2,y0-2,w+4,h+4);
}

// Circular subtraction + spawn debris chunks
function blast(cx,cy,r,spawnDebris=true){
 cx|=0;cy|=0;r|=0;
 const x0=clamp(cx-r-2,0,MAPW),x1=clamp(cx+r+2,0,MAPW),y0=clamp(cy-r-2,0,MAPH),y1=clamp(cy+r+2,0,MAPH);
 let removed=0;
 const m=T.mask;
 for(let y=y0;y<y1;y++){
  const dy=y-cy;
  for(let x=x0;x<x1;x++){
   const dx=x-cx;
   if(dx*dx+dy*dy<=r*r){ if(m[y*MAPW+x]){m[y*MAPW+x]=0;removed++;} }
  }
 }
 // scorch ring: darken remaining edge pixels
 T.ctx.save();
 T.ctx.globalCompositeOperation="destination-out";
 T.ctx.beginPath();T.ctx.arc(cx,cy,r,0,TAU);T.ctx.fill();
 T.ctx.restore();
 // repaint a ring band so edges get fresh surface shading
 repaintRing(cx,cy,r);
 if(spawnDebris&&removed>40){
  const n=clamp(Math.floor(removed/260),3,26);
  for(let i=0;i<n;i++){
   const a=rnd(TAU),rr=rnd(r,r*.25);
   const px=cx+Math.cos(a)*rr,py=cy+Math.sin(a)*rr;
   G.debris.push(new Debris(px,py,Math.cos(a)*rnd(6,1)+rnd(1,-1),Math.sin(a)*rnd(6,1)-rnd(3,0),rnd(9,3)));
  }
 }
 return removed;
}
function repaintRing(cx,cy,r){
 const pad=30;
 repaintTerrainRegion(cx-r-pad,cy-r-pad,(r+pad)*2,(r+pad)*2);
}

/* ============================================================
   ENTITIES
   ============================================================ */
class Particle{
 constructor(x,y,vx,vy,life,col,size,opts={}){
  this.x=x;this.y=y;this.vx=vx;this.vy=vy;this.life=life;this.max=life;this.col=col;this.size=size;
  this.g=opts.g!==undefined?opts.g:.12;this.drag=opts.drag||.99;this.glow=opts.glow||false;
  this.shrink=opts.shrink!==undefined?opts.shrink:1;this.wind=opts.wind||0;this.spark=opts.spark||false;
  this.smoke=opts.smoke||false;this.rot=rnd(TAU);this.vr=rnd(.3,-.3);
 }
 update(dt){
  this.vy+=this.g*dt;this.vx*=Math.pow(this.drag,dt);this.vy*=Math.pow(this.drag,dt);
  this.vx+=G.wind*this.wind*dt*0.06;
  this.x+=this.vx*dt;this.y+=this.vy*dt;this.rot+=this.vr*dt;
  this.life-=dt;
  return this.life>0;
 }
 draw(c){
  const t=this.life/this.max;
  c.globalAlpha=clamp(this.smoke?t*.55:t,0,1);
  const s=this.size*(this.shrink?(0.25+t*0.75):1)*(this.smoke?(2-t):1);
  if(this.glow){c.globalCompositeOperation="lighter";}
  c.fillStyle=this.col;
  if(this.spark){
   c.save();c.translate(this.x,this.y);c.rotate(Math.atan2(this.vy,this.vx));
   c.fillRect(-s*2.2,-s*.4,s*4.4,s*.8);c.restore();
  } else {
   c.beginPath();c.arc(this.x,this.y,Math.max(.4,s),0,TAU);c.fill();
  }
  c.globalCompositeOperation="source-over";c.globalAlpha=1;
 }
}
class Debris{
 constructor(x,y,vx,vy,s){this.x=x;this.y=y;this.vx=vx;this.vy=vy;this.s=s;this.rot=rnd(TAU);this.vr=rnd(.35,-.35);
  this.life=rnd(9,5);this.settled=false;
  const p=terrainPalette;this.col=pick([p.soil,p.soil2,p.top2,p.deep]);}
 update(dt){
  if(this.settled){this.life-=dt*2;return this.life>0;}
  this.vy+=GRAV*dt*.9;this.vx+=G.wind*0.004*dt;this.vx*=Math.pow(.995,dt);
  let nx=this.x+this.vx*dt,ny=this.y+this.vy*dt;
  if(ny>WATER_Y){ splashFX(this.x,WATER_Y,.5); return false; }
  if(solidAt(nx,ny)){
   // try slide
   if(!solidAt(nx,this.y)){this.x=nx;this.vy*=-.25;this.vx*=.7;}
   else if(!solidAt(this.x,ny)){this.y=ny;this.vx*=-.35;}
   else{
    // settle: deposit into terrain
    this.settled=true;this.life=.3;
    depositChunk(this.x,this.y,this.s*.55);
    return true;
   }
   this.vr*=.6;
   if(Math.abs(this.vy)<.6&&Math.abs(this.vx)<.4){this.settled=true;this.life=.3;depositChunk(this.x,this.y,this.s*.55);}
  } else {this.x=nx;this.y=ny;}
  this.rot+=this.vr*dt;
  this.life-=dt*.016;
  if(this.x<-50||this.x>MAPW+50)return false;
  return this.life>0;
 }
 draw(c){
  c.save();c.translate(this.x,this.y);c.rotate(this.rot);
  c.globalAlpha=this.settled?clamp(this.life/.3,0,1):1;
  c.fillStyle=this.col;c.fillRect(-this.s/2,-this.s/2,this.s,this.s);
  c.fillStyle="rgba(0,0,0,.25)";c.fillRect(-this.s/2,this.s*.15,this.s,this.s*.35);
  c.restore();c.globalAlpha=1;
 }
}
function depositChunk(x,y,r){
 x|=0;y|=0;r=Math.max(1,r|0);
 for(let yy=y-r;yy<=y+r;yy++)for(let xx=x-r;xx<=x+r;xx++){
  if(xx<0||xx>=MAPW||yy<0||yy>=MAPH)continue;
  if((xx-x)**2+(yy-y)**2<=r*r)T.mask[yy*MAPW+xx]=1;
 }
 repaintTerrainRegion(x-r-3,y-r-3,r*2+6,r*2+6);
}

class FloatText{
 constructor(x,y,txt,col,size=20){this.x=x;this.y=y;this.txt=txt;this.col=col;this.life=1.7;this.max=1.7;this.size=size;this.vy=-1.5;this.vx=rnd(.6,-.6)}
 update(dt){this.y+=this.vy*dt;this.vy*=Math.pow(.94,dt);this.x+=this.vx*dt;this.life-=dt*.016;return this.life>0}
 draw(c){
  const t=this.life/this.max;c.globalAlpha=clamp(t*1.6,0,1);
  c.font=`900 ${this.size*(1+(1-t)*.25)}px "Trebuchet MS",sans-serif`;c.textAlign="center";
  c.lineWidth=5;c.strokeStyle="rgba(0,0,0,.8)";c.strokeText(this.txt,this.x,this.y);
  c.fillStyle=this.col;c.fillText(this.txt,this.x,this.y);c.globalAlpha=1;
 }
}

/* ---------------- WORM ---------------- */
class Worm{
 constructor(team,name,x,y){
  this.team=team;this.name=name;this.x=x;this.y=y;this.vx=0;this.vy=0;
  this.hp=100;this.maxhp=100;this.dead=false;this.r=7;this.h=16;
  this.face=Math.random()<.5?-1:1;this.aim=-0.6;this.onGround=false;
  this.fallStart=null;this.walkT=0;this.blink=0;this.blinkT=rnd(4,1);
  this.chute=false;this.jet=0;this.drowning=0;this.frozen=0;
  this.hpShown=100;this.dmgAccum=0;this.recoil=0;this.breathe=rnd(TAU);
  this.grave=0;this.launched=false;
 }
 get cx(){return this.x} get cy(){return this.y}
 damage(amt,src){
  if(this.dead)return;
  amt=Math.round(amt);if(amt<=0)return;
  this.hp-=amt;this.dmgAccum+=amt;
  G.floats.push(new FloatText(this.x,this.y-22,"-"+amt,"#ff6b6b",Math.min(34,16+amt*.32)));
  for(let i=0;i<Math.min(18,4+amt/4);i++)
   G.parts.push(new Particle(this.x,this.y,rnd(3,-3),rnd(1,-4),rnd(30,14),pick(["#ff5d6e","#ffa0aa","#ff2d4d"]),rnd(3.5,1.4),{g:.16}));
  if(G.active===this)flashDamage();
  Snd.hurt();
  if(this.hp<=0){this.hp=0;this.die();}
 }
 die(){
  if(this.dead)return;this.dead=true;this.grave=1;
  Snd.die();
  explosion(this.x,this.y,52,32,null,{shake:9});
  G.floats.push(new FloatText(this.x,this.y-34,"K.O.","#ffffff",26));
 }
 update(dt){
  if(this.dead){return;}
  this.breathe+=dt*.06;
  if(this.blinkT>0){this.blinkT-=dt*.016;}else{this.blink+=dt;if(this.blink>7){this.blink=0;this.blinkT=rnd(5,1.6)}}
  if(this.frozen>0)this.frozen-=dt*.016;
  // jetpack
  const isActive=(G.active===this&&(G.phase==="aim"||G.phase==="retreat")&&!G.cpuThinking);
  if(this.jet>0){
   this.jet-=dt*.016;
   let th=0,tx=0;
   if(isActive||this.aiJet){
    if(this.aiJet){th=this.aiJetUp?1:0;tx=this.aiJetX||0;}
    else{ if(keys["ArrowUp"]||keys["KeyW"])th=1; if(keys["ArrowLeft"]||keys["KeyA"])tx=-1; if(keys["ArrowRight"]||keys["KeyD"])tx=1; }
   }
   if(th){this.vy-=0.62*dt;
    for(let i=0;i<2;i++)G.parts.push(new Particle(this.x+rnd(3,-3),this.y+8,rnd(1,-1),rnd(4,1.5),rnd(20,8),pick(["#ffd166","#ff8b3d","#fff2c0"]),rnd(3.4,1.6),{g:.02,glow:true}));
    if(Math.random()<.2)Snd.chute();}
   if(tx){this.vx+=tx*0.14*dt;this.face=tx;}
   if(this.jet<=0){this.jet=0;this.aiJet=false;}
  }
  // physics
  const inWater=this.y>WATER_Y;
  if(inWater){
   this.drowning+=dt*.016;
   this.vy+=(GRAV*0.14)*dt; this.vy-=0.10*dt; // buoyancy-ish sink
   this.vy=clamp(this.vy,-2,1.6);
   this.vx*=Math.pow(.93,dt);
   this.y+=this.vy*dt;this.x+=this.vx*dt;
   if(Math.random()<.14)G.parts.push(new Particle(this.x+rnd(6,-6),this.y,rnd(.4,-.4),-rnd(1.4,.3),rnd(40,18),"#bfefff",rnd(2.6,1),{g:-0.01,drag:.99}));
   this.damage(dt*0.28,"water");
   if(this.hp<=0)return;
   if(this.y>MAPH+40){this.hp=0;this.die();}
   return;
  } else this.drowning=0;

  const moving=(isActive&&this.jet<=0&&!G.firing&&(keys["ArrowLeft"]||keys["ArrowRight"]||keys["KeyA"]||keys["KeyD"]))||this.aiMove;
  let dir=0;
  if(this.aiMove)dir=this.aiMove;
  else if(isActive&&this.jet<=0){ if(keys["ArrowLeft"]||keys["KeyA"])dir=-1; if(keys["ArrowRight"]||keys["KeyD"])dir=1; }
  if(dir&&this.onGround&&this.frozen<=0){
   this.face=dir;
   const speed=1.35;
   // step up/down
   let nx=this.x+dir*speed*dt;
   let placed=false;
   for(let s=0;s<=6;s++){ // try step up
    if(!collidesAt(nx,this.y-s)){this.x=nx;this.y-=s;placed=true;break;}
   }
   if(!placed){/*blocked*/}
   else{
    this.walkT+=dt*.35;
    if(Math.random()<.09)Snd.walk();
    if(Math.random()<.35)G.parts.push(new Particle(this.x-dir*4,this.y+this.h/2,-dir*rnd(1,.2),-rnd(.8,0),rnd(22,10),"rgba(200,190,170,.75)",rnd(2.6,1.2),{g:.03,smoke:true}));
   }
  }
  // wind push while airborne
  if(!this.onGround) this.vx += G.wind*0.0016*dt*(this.chute?4:1);

  this.vy+=GRAV*dt*(this.chute&&this.vy>0?0.12:1);
  if(this.chute&&this.vy>1.1)this.vy=1.1;
  this.vx*=Math.pow(this.onGround?.86:.995,dt);
  if(Math.abs(this.vx)<.02)this.vx=0;

  let steps=Math.max(1,Math.ceil((Math.abs(this.vx)+Math.abs(this.vy))*dt/3));
  const sdt=dt/steps;
  for(let s=0;s<steps;s++){
   let nx=this.x+this.vx*sdt, ny=this.y+this.vy*sdt;
   // vertical
   if(!collidesAt(this.x,ny)){this.y=ny;this.onGround=false;}
   else{
    if(this.vy>0){ // landing
     // find surface
     let yy=Math.floor(this.y);
     while(!collidesAt(this.x,yy+1)&&yy<ny)yy++;
     this.y=yy;
     this.land();
    } else { this.vy=0; }
    this.vy=0;
    this.onGround=true;
   }
   // horizontal
   if(!collidesAt(nx,this.y)){this.x=nx;}
   else{
    let ok=false;
    for(let s2=1;s2<=5;s2++){if(!collidesAt(nx,this.y-s2)){this.x=nx;this.y-=s2;ok=true;break;}}
    if(!ok){this.vx*=-.28;}
   }
  }
  // ground check
  if(this.vy>=0 && collidesAt(this.x,this.y+1)){ if(!this.onGround)this.land(); this.onGround=true; }
  else if(this.vy!==0||!collidesAt(this.x,this.y+1)) this.onGround=collidesAt(this.x,this.y+1);

  // fall damage tracking
  if(!this.onGround){ if(this.fallStart===null)this.fallStart=this.y; else if(this.y<this.fallStart)this.fallStart=this.y; }
  this.x=clamp(this.x,4,MAPW-4);
  if(this.y>MAPH+60){this.hp=0;this.die();}
 }
 land(){
  if(this.fallStart!==null){
   const fall=this.y-this.fallStart;
   if(fall>105&&!this.chute){
    const d=Math.min(45,Math.floor((fall-105)/7));
    if(d>0){this.damage(d,"fall");
     G.shake=Math.min(14,G.shake+d*.3);
     for(let i=0;i<10;i++)G.parts.push(new Particle(this.x+rnd(8,-8),this.y+8,rnd(2.5,-2.5),-rnd(1.6,.2),rnd(26,12),"rgba(210,200,180,.8)",rnd(3,1.4),{g:.06,smoke:true}));
    }
   } else if(fall>40){
    for(let i=0;i<6;i++)G.parts.push(new Particle(this.x+rnd(7,-7),this.y+8,rnd(1.8,-1.8),-rnd(1,.1),rnd(20,10),"rgba(210,200,180,.65)",rnd(2.4,1),{g:.05,smoke:true}));
    Snd.clank();
   }
   this.fallStart=null;
  }
  if(this.chute){this.chute=false;}
  this.launched=false;
 }
 jump(mode=0){
  if(!this.onGround||this.dead||this.frozen>0)return;
  if(mode===0){this.vy=-5.4;this.vx=this.face*2.2;}
  else{this.vy=-7.4;this.vx=-this.face*1.4;}
  this.onGround=false;this.fallStart=this.y;Snd.jump();
  for(let i=0;i<8;i++)G.parts.push(new Particle(this.x+rnd(6,-6),this.y+8,rnd(2,-2),rnd(1,-.3),rnd(20,8),"rgba(220,210,190,.7)",rnd(2.6,1.2),{g:.05,smoke:true}));
 }
 draw(c){
  if(this.dead){return;}
  const t=TEAMS[this.team];
  const bob=Math.sin(this.breathe)*0.8;
  c.save();c.translate(this.x,this.y+bob);
  // shadow
  c.globalAlpha=.28;c.fillStyle="#000";c.beginPath();c.ellipse(0,this.h/2+1,9,3.2,0,0,TAU);c.fill();c.globalAlpha=1;
  const sq=this.onGround?1:1.06;
  c.scale(this.face,1);
  // body
  const grad=c.createLinearGradient(0,-10,0,10);
  grad.addColorStop(0,t.col);grad.addColorStop(1,t.col2);
  c.fillStyle=grad;
  c.beginPath();c.ellipse(0,0,8.5,9.5*sq,0,0,TAU);c.fill();
  c.strokeStyle="rgba(0,0,0,.45)";c.lineWidth=1.4;c.stroke();
  // belly highlight
  c.fillStyle="rgba(255,255,255,.18)";c.beginPath();c.ellipse(-2.4,-3,3.6,4.4,-.3,0,TAU);c.fill();
  // eyes
  const eyeY=-3.2, blinking=this.blink>0&&this.blink<0.9;
  c.fillStyle="#fff";
  if(!blinking){
   c.beginPath();c.ellipse(2.4,eyeY,3.1,3.4,0,0,TAU);c.fill();
   c.beginPath();c.ellipse(-1.4,eyeY,2.6,3,0,0,TAU);c.fill();
   const la=clamp(this.aim,-1.4,1.4);
   const px=Math.cos(la)*1.1,py=Math.sin(la)*1.1;
   c.fillStyle="#101018";
   c.beginPath();c.arc(2.6+px,eyeY+py,1.5,0,TAU);c.fill();
   c.beginPath();c.arc(-1.3+px,eyeY+py,1.3,0,TAU);c.fill();
  }else{
   c.strokeStyle="#101018";c.lineWidth=1.2;
   c.beginPath();c.moveTo(.6,eyeY);c.lineTo(4.4,eyeY);c.moveTo(-3,eyeY);c.lineTo(-.2,eyeY);c.stroke();
  }
  // hat / band
  c.fillStyle=t.hat;
  c.beginPath();c.ellipse(0,-8.4,7.2,2.4,0,0,TAU);c.fill();
  c.fillStyle=t.col2;c.fillRect(-4.2,-12.6,8.4,4.4);
  c.fillStyle="rgba(255,255,255,.25)";c.fillRect(-4.2,-12.6,8.4,1.4);
  c.restore();

  // parachute
  if(this.chute){
   c.save();c.translate(this.x,this.y-8);
   c.fillStyle="rgba(255,255,255,.9)";
   c.beginPath();c.arc(0,-16,20,Math.PI,0);c.closePath();c.fill();
   c.fillStyle=t.col;c.beginPath();c.arc(0,-16,20,Math.PI*1.15,Math.PI*1.45);c.lineTo(0,-16);c.fill();
   c.beginPath();c.arc(0,-16,20,Math.PI*1.55,Math.PI*1.85);c.lineTo(0,-16);c.fill();
   c.strokeStyle="rgba(255,255,255,.7)";c.lineWidth=1;
   c.beginPath();c.moveTo(-19,-16);c.lineTo(-3,-2);c.moveTo(19,-16);c.lineTo(3,-2);c.stroke();
   c.restore();
  }
  // jetpack
  if(this.jet>0){
   c.save();c.translate(this.x,this.y);c.scale(this.face,1);
   c.fillStyle="#3a4052";c.fillRect(-11,-6,5,12);
   c.fillStyle="#6b7488";c.fillRect(-11,-6,5,3);
   c.restore();
  }
  // name + hp tag
  const label=this.name;
  c.font='bold 10px "Trebuchet MS",sans-serif';c.textAlign="center";
  const tw=Math.max(c.measureText(label).width,30)+12;
  const ty=this.y-26;
  c.fillStyle="rgba(6,10,20,.72)";roundRect(c,this.x-tw/2,ty-11,tw,22,5);c.fill();
  c.fillStyle=t.col;c.fillText(label,this.x,ty-1);
  // hp bar
  c.fillStyle="rgba(0,0,0,.6)";c.fillRect(this.x-tw/2+3,ty+3,tw-6,4);
  this.hpShown=lerp(this.hpShown,this.hp,.12);
  c.fillStyle=this.hp>55?"#7affa0":this.hp>25?"#ffd166":"#ff5d6e";
  c.fillRect(this.x-tw/2+3,ty+3,(tw-6)*clamp(this.hpShown/this.maxhp,0,1),4);
  if(G.active===this&&G.phase==="aim"){
   // arrow
   const a=Math.sin(performance.now()*.006)*3;
   c.fillStyle="#fff";
   c.beginPath();c.moveTo(this.x,ty-16+a);c.lineTo(this.x-6,ty-25+a);c.lineTo(this.x+6,ty-25+a);c.closePath();c.fill();
  }
 }
}
function roundRect(c,x,y,w,h,r){c.beginPath();c.moveTo(x+r,y);c.arcTo(x+w,y,x+w,y+h,r);c.arcTo(x+w,y+h,x,y+h,r);c.arcTo(x,y+h,x,y,r);c.arcTo(x,y,x+w,y,r);c.closePath();}

function collidesAt(x,y){
 // worm body approximated by 3 probes
 for(const[ox,oy] of [[0,-6],[0,6],[-5,2],[5,2],[0,0]]){
  if(solidAt(x+ox,y+oy))return true;
 }
 return false;
}

/* ---------------- PROJECTILES ---------------- */
class Proj{
 constructor(x,y,vx,vy,w,owner,opts={}){
  this.x=x;this.y=y;this.vx=vx;this.vy=vy;this.w=w;this.owner=owner;
  this.life=opts.life||14;this.fuse=opts.fuse!==undefined?opts.fuse:(w.fuse||null);
  this.bounce=w.bounce||0;this.windF=(w.wind!==undefined?w.wind:1)*(opts.windMul||1);
  this.dmg=opts.dmg||w.dmg;this.rad=opts.rad||w.rad;this.cluster=opts.noCluster?0:(w.cluster||0);
  this.trailT=0;this.rot=Math.atan2(vy,vx);this.dead=false;this.drill=w.drill||false;this.drilling=0;
  this.proximity=w.proximity||false;this.armed=0;this.smoke=opts.smoke!==undefined?opts.smoke:true;
  this.shake=w.shake||0;this.age=0;this.homing=opts.homing||0;
 }
 update(dt){
  this.age+=dt*.016;
  if(this.fuse!==null){this.fuse-=dt*.016;if(this.fuse<=0){this.boom();return false}}
  if(this.proximity){
   this.armed+=dt*.016;
   if(this.armed>1.2){
    for(const wm of G.worms) if(!wm.dead&&wm!==this.ownerWorm&&dist(this.x,this.y,wm.x,wm.y)<38){this.boom();return false}
   }
  }
  if(this.drilling>0){
   this.drilling-=dt*.016;
   const sp=2.6;
   this.x+=Math.cos(this.rot)*sp*dt;this.y+=Math.sin(this.rot)*sp*dt;
   blast(this.x,this.y,9,false);
   if(Math.random()<.5)G.parts.push(new Particle(this.x,this.y,rnd(2,-2),rnd(2,-2),rnd(18,8),pick([terrainPalette.soil,terrainPalette.soil2]),rnd(3,1.2),{g:.1}));
   if(Math.random()<.3)Snd.drill();
   if(this.drilling<=0||!solidAt(this.x,this.y)&&this.age>1.2){this.boom();return false}
   return true;
  }
  this.vy+=GRAV*dt;
  this.vx+=G.wind*0.0022*this.windF*dt;
  if(this.homing){
   let best=null,bd=1e9;
   for(const wm of G.worms)if(!wm.dead&&wm.team!==this.ownerTeam){const d=dist(this.x,this.y,wm.x,wm.y);if(d<bd){bd=d;best=wm}}
   if(best&&bd<400){const a=Math.atan2(best.y-this.y,best.x-this.x);const sp=Math.hypot(this.vx,this.vy);
    const ca=Math.atan2(this.vy,this.vx);let da=a-ca;while(da>Math.PI)da-=TAU;while(da<-Math.PI)da+=TAU;
    const na=ca+clamp(da,-.05,.05)*dt;this.vx=Math.cos(na)*sp;this.vy=Math.sin(na)*sp;}
  }
  const steps=Math.max(1,Math.ceil((Math.abs(this.vx)+Math.abs(this.vy))*dt/2.5));
  const sdt=dt/steps;
  for(let s=0;s<steps;s++){
   const nx=this.x+this.vx*sdt, ny=this.y+this.vy*sdt;
   // worm hit
   for(const wm of G.worms){
    if(wm.dead)continue;
    if(this.age<0.09&&wm===this.ownerWorm)continue;
    if(dist(nx,ny,wm.x,wm.y)<11){this.x=nx;this.y=ny;this.boom();return false;}
   }
   if(ny>WATER_Y){this.x=nx;this.y=WATER_Y;splashFX(this.x,WATER_Y,1.2);Snd.splash();return false;}
   if(solidAt(nx,ny)){
    if(this.drill){this.rot=Math.atan2(this.vy,this.vx);this.drilling=1.0;this.x=nx;this.y=ny;Snd.drill();return true;}
    if(this.bounce>0){
     const n=surfaceNormal(nx,ny);
     const dot=this.vx*n.x+this.vy*n.y;
     this.vx=(this.vx-2*dot*n.x)*this.bounce;this.vy=(this.vy-2*dot*n.y)*this.bounce;
     this.x+=n.x*2.2;this.y+=n.y*2.2;
     if(Math.hypot(this.vx,this.vy)>1.1)Snd.bounce();
     for(let i=0;i<4;i++)G.parts.push(new Particle(this.x,this.y,rnd(2,-2),rnd(1,-2),rnd(16,7),"rgba(220,210,190,.7)",rnd(2,.8),{g:.1,smoke:true}));
     if(Math.hypot(this.vx,this.vy)<0.35&&this.fuse===null&&!this.proximity){this.boom();return false;}
     break;
    } else {this.x=nx;this.y=ny;this.boom();return false;}
   }
   this.x=nx;this.y=ny;
  }
  this.rot=Math.atan2(this.vy,this.vx);
  if(this.x<-200||this.x>MAPW+200||this.y>MAPH+200){return false;}
  if(this.y<-3000)return false;
  this.trailT+=dt;
  if(this.smoke&&this.trailT>1.4){this.trailT=0;
   G.parts.push(new Particle(this.x,this.y,rnd(.4,-.4),rnd(.2,-.5),rnd(34,18),"rgba(220,220,230,.6)",rnd(3.4,1.8),{g:-0.01,drag:.96,smoke:true,wind:1}));
   if(this.w.id==="bazooka"||this.w.id==="cluster")
    G.parts.push(new Particle(this.x,this.y,rnd(.6,-.6),rnd(.6,-.6),rnd(12,5),pick(["#ffd166","#ff8b3d"]),rnd(2.2,1),{g:0,glow:true}));
  }
  this.life-=dt*.016;
  return this.life>0;
 }
 boom(){
  if(this.dead)return;this.dead=true;
  explosion(this.x,this.y,this.rad,this.dmg,this.ownerWorm,{shake:this.shake||this.rad*.16,big:this.rad>95});
  if(this.cluster){
   for(let i=0;i<this.cluster;i++){
    const a=-Math.PI/2+rnd(1.1,-1.1);const sp=rnd(5.2,2.4);
    const p=new Proj(this.x,this.y-6,Math.cos(a)*sp,Math.sin(a)*sp,this.w,this.owner,{noCluster:true,dmg:this.w.dmg*.75,rad:this.w.rad*.8,fuse:rnd(1.6,.7)});
    p.ownerWorm=this.ownerWorm;p.ownerTeam=this.ownerTeam;p.fuse=rnd(1.7,.6);
    G.projs.push(p);
   }
  }
 }
 draw(c){
  c.save();c.translate(this.x,this.y);
  if(this.drilling>0){
   c.rotate(this.rot);c.fillStyle="#c8ccd8";c.beginPath();c.moveTo(9,0);c.lineTo(-6,-4);c.lineTo(-6,4);c.closePath();c.fill();c.restore();return;
  }
  const id=this.w.id;
  if(id==="bazooka"){
   c.rotate(this.rot);
   c.fillStyle="#e8e8ee";c.beginPath();c.moveTo(9,0);c.lineTo(-5,-3.6);c.lineTo(-5,3.6);c.closePath();c.fill();
   c.fillStyle="#ff5d5d";c.fillRect(-7,-3.4,4,6.8);
   c.globalCompositeOperation="lighter";c.fillStyle="rgba(255,180,80,.85)";
   c.beginPath();c.ellipse(-10-Math.random()*4,0,rnd(7,4),rnd(3,1.6),0,0,TAU);c.fill();
  } else if(id==="grenade"||id==="cluster"){
   c.rotate(this.age*7);
   c.fillStyle=id==="cluster"?"#c94a2a":"#3f5f3a";
   c.beginPath();c.arc(0,0,5.4,0,TAU);c.fill();
   c.strokeStyle="rgba(0,0,0,.4)";c.lineWidth=1;c.stroke();
   c.fillStyle="#8a8a92";c.fillRect(-1.6,-8,3.2,3.4);
   if(this.fuse!==null&&Math.sin(this.age*(18+ (3-this.fuse)*14))>0){
    c.globalCompositeOperation="lighter";c.fillStyle="#ffdd66";c.beginPath();c.arc(0,-9,2.6,0,TAU);c.fill();}
  } else if(id==="mine"){
   c.fillStyle="#4a4a52";c.beginPath();c.arc(0,0,6,0,TAU);c.fill();
   c.strokeStyle="#2a2a30";c.lineWidth=1.5;
   for(let i=0;i<6;i++){const a=i/6*TAU;c.beginPath();c.moveTo(Math.cos(a)*5,Math.sin(a)*5);c.lineTo(Math.cos(a)*8.5,Math.sin(a)*8.5);c.stroke();}
   const on=this.armed>1.2?(Math.sin(this.age*14)>0):(Math.sin(this.age*4)>0);
   c.globalCompositeOperation="lighter";c.fillStyle=on?"#ff3b3b":"rgba(80,20,20,.6)";c.beginPath();c.arc(0,-1,2.4,0,TAU);c.fill();
  } else if(id==="holy"){
   c.rotate(Math.sin(this.age*6)*.4);
   c.fillStyle="#ffd166";c.beginPath();c.moveTo(0,-9);c.lineTo(7,5);c.lineTo(-7,5);c.closePath();c.fill();
   c.fillStyle="#fff3c4";c.fillRect(-8,5,16,3);
   c.globalCompositeOperation="lighter";c.fillStyle="rgba(255,230,150,.35)";c.beginPath();c.arc(0,0,14+Math.sin(this.age*10)*3,0,TAU);c.fill();
  } else if(id==="airstrike"){
   c.rotate(this.rot+Math.PI/2);
   c.fillStyle="#b8bcc8";c.beginPath();c.moveTo(0,-8);c.lineTo(3.4,2);c.lineTo(0,8);c.lineTo(-3.4,2);c.closePath();c.fill();
   c.fillStyle="#ff6b4a";c.fillRect(-4,3,8,2.6);
  } else {
   c.fillStyle="#ddd";c.beginPath();c.arc(0,0,4,0,TAU);c.fill();
  }
  c.restore();c.globalCompositeOperation="source-over";
 }
}
function surfaceNormal(x,y){
 let nx=0,ny=0;const R=6;
 for(let dy=-R;dy<=R;dy+=2)for(let dx=-R;dx<=R;dx+=2){
  if(solidAt(x+dx,y+dy)){nx-=dx;ny-=dy;}
 }
 const l=Math.hypot(nx,ny)||1;return{x:nx/l,y:ny/l};
}

/* ---------------- EFFECTS ---------------- */
function explosion(x,y,r,dmg,srcWorm,opts={}){
 const removed=blast(x,y,r,true);
 Snd.explode(clamp(r/55,.6,2.1));
 G.shake=Math.min(46,G.shake+(opts.shake||r*.16));
 G.flashes.push({x,y,r,life:1,max:1});
 if(opts.big||r>95){G.freeze=Math.max(G.freeze,0.09);}
 else if(r>60)G.freeze=Math.max(G.freeze,0.045);
 const n=Math.floor(clamp(r*1.5,22,120));
 for(let i=0;i<n;i++){
  const a=rnd(TAU),sp=rnd(r*.16,.6);
  G.parts.push(new Particle(x,y,Math.cos(a)*sp,Math.sin(a)*sp,rnd(34,12),
   pick(["#fff6c9","#ffd166","#ff8b3d","#ff4d2d"]),rnd(6,2),{g:.07,drag:.93,glow:true}));
 }
 for(let i=0;i<n*.6;i++){
  const a=rnd(TAU),sp=rnd(r*.09,.3);
  G.parts.push(new Particle(x+rnd(8,-8),y+rnd(8,-8),Math.cos(a)*sp,Math.sin(a)*sp-.4,rnd(70,34),
   "rgba(70,68,72,.75)",rnd(11,4),{g:-.012,drag:.95,smoke:true,wind:1.6}));
 }
 for(let i=0;i<10;i++){
  const a=rnd(TAU),sp=rnd(r*.24,r*.08);
  G.parts.push(new Particle(x,y,Math.cos(a)*sp,Math.sin(a)*sp,rnd(20,9),"#fff8d8",rnd(2.4,1),{g:.05,glow:true,spark:true,drag:.9}));
 }
 // damage & knockback
 for(const wm of G.worms){
  if(wm.dead)continue;
  const d=dist(x,y,wm.x,wm.y);
  if(d<r*1.5){
   const f=clamp(1-d/(r*1.5),0,1);
   const dm=dmg*Math.pow(f,.75);
   if(dm>1){
    const a=Math.atan2(wm.y-y,wm.x-x)||-Math.PI/2;
    const kb=f*(r/40)*3.4;
    wm.vx+=Math.cos(a)*kb;wm.vy+=Math.sin(a)*kb-f*2.2;
    wm.onGround=false;wm.launched=true;
    if(wm.fallStart===null)wm.fallStart=wm.y;
    wm.damage(dm,srcWorm);
    if(srcWorm&&srcWorm!==wm&&srcWorm.team!==wm.team)G.turnDamage+=dm;
   }
  }
 }
 // detonate nearby mines
 for(const p of G.projs){ if(p!==null&&p.proximity&&!p.dead&&dist(x,y,p.x,p.y)<r*1.2){p.fuse=rnd(.35,.05);} }
}
function splashFX(x,y,scale=1){
 Snd.splash();
 for(let i=0;i<Math.floor(26*scale);i++){
  const a=-Math.PI/2+rnd(1.1,-1.1);const sp=rnd(6*scale,1.6);
  G.parts.push(new Particle(x+rnd(6,-6),y,Math.cos(a)*sp,Math.sin(a)*sp,rnd(40,16),pick(["#9fe4ff","#ffffff","#5fc4f0"]),rnd(3.6,1.2),{g:.14,drag:.98}));
 }
 G.ripples.push({x,y,r:4,max:70*scale,life:1});
}
function flashDamage(){
 const f=document.getElementById("dmgFlash");f.style.opacity=".85";
 setTimeout(()=>f.style.opacity="0",70);
}

/* ============================================================
   GAME SETUP
   ============================================================ */
const Cfg={map:"random",teams:3,time:45,cpu:[false,true,true,true]};

function newGame(){
 const kind = Cfg.map==="random"?pick(["cave","island","twin"]):Cfg.map;
 genTerrain(kind);
 G={
  kind, worms:[],projs:[],parts:[],debris:[],floats:[],flashes:[],ripples:[],girders:[],crates:[],
  teamIdx:0, active:null, phase:"aim", turnTime:Cfg.time, timeLeft:Cfg.time,
  wind:rnd(1,-1)*rnd(1,.15)*40, shake:0, freeze:0, cam:{x:MAPW/2,y:MAPH/2,zoom:1,tx:MAPW/2,ty:MAPH/2,tz:1},
  power:0, charging:false, firing:false, weapon:"bazooka", ammo:{}, retreat:0,
  over:false, turnDamage:0, cpuThinking:false, aiPlan:null, waitTimer:0, msgT:0,
  clouds:[], birds:[], stars:[], sunk:0, targetMode:false, teleMode:false, girderMode:false,
  girderAng:0, roundCount:0, lastFireW:null, camLock:null, camLockT:0, hudPulse:0
 };
 // ammo per team
 for(let t=0;t<Cfg.teams;t++){G.ammo[t]={};for(const w of WEAPONS)G.ammo[t][w.id]=w.ammo;}
 // spawn worms
 const spots=findSpawnSpots(Cfg.teams*4);
 let si=0;
 const usedNames=[...NAMES];
 for(let t=0;t<Cfg.teams;t++){
  for(let i=0;i<4;i++){
   const s=spots[si++]||{x:rnd(MAPW-200,200),y:200};
   const nm=usedNames.length?usedNames.splice(ri(0,usedNames.length-1),1)[0]:"Worm"+si;
   const wm=new Worm(t,nm,s.x,s.y);
   G.worms.push(wm);
  }
 }
 // clouds & bg
 for(let i=0;i<18;i++)G.clouds.push({x:rnd(MAPW+600,-300),y:rnd(MAPH*.55,20),s:rnd(1.6,.5),sp:rnd(.5,.12),o:rnd(.5,.16)});
 for(let i=0;i<70;i++)G.stars.push({x:rnd(MAPW),y:rnd(MAPH*.6),r:rnd(1.4,.4),p:rnd(TAU)});
 G.active=G.worms[0];
 centerCam(G.active.x,G.active.y,true);
 buildTeamBar();buildWeaponHud();
 updateWind();
 startTurn(true);
}
function findSpawnSpots(n){
 const spots=[];let tries=0;
 while(spots.length<n&&tries<8000){
  tries++;
  const x=ri(70,MAPW-70);
  let y=-1;
  for(let yy=60;yy<WATER_Y-30;yy++){ if(solidAt(x,yy)&&!solidAt(x,yy-24)){y=yy-11;break;} }
  if(y<0)continue;
  if(y>WATER_Y-40)continue;
  // headroom
  let clear=true;
  for(let k=1;k<26;k++) if(solidAt(x,y-k)){clear=false;break;}
  if(!clear)continue;
  if(spots.some(s=>Math.abs(s.x-x)<90))continue;
  spots.push({x,y});
 }
 // shuffle so teams interleave
 for(let i=spots.length-1;i>0;i--){const j=ri(0,i);[spots[i],spots[j]]=[spots[j],spots[i]];}
 return spots;
}

/* ---------------- TURN LOGIC ---------------- */
function aliveWorms(t){return G.worms.filter(w=>!w.dead&&(t===undefined||w.team===t))}
function aliveTeams(){const s=new Set();G.worms.forEach(w=>{if(!w.dead)s.add(w.team)});return [...s]}

function startTurn(first){
 G.phase="aim";G.firing=false;G.charging=false;G.power=0;G.retreat=0;
 G.turnDamage=0;G.targetMode=false;G.teleMode=false;G.girderMode=false;G.aiPlan=null;G.cpuThinking=false;
 G.timeLeft=G.turnTime;
 if(!first)updateWind();
 // pick next team with alive worms
 const at=aliveTeams();
 if(at.length<=1){endGame();return;}
 let guard=0;
 do{G.teamIdx=(G.teamIdx+1)%Cfg.teams;guard++;}while(!at.includes(G.teamIdx)&&guard<20);
 if(first)G.teamIdx=at[0];
 // next worm in that team (rotate)
 const tw=G.worms.filter(w=>w.team===G.teamIdx);
 const alive=tw.filter(w=>!w.dead);
 let idx=tw.indexOf(G.active);
 let next=null;
 for(let i=1;i<=tw.length;i++){const cand=tw[(Math.max(0,idx)+i)%tw.length];if(!cand.dead){next=cand;break;}}
 if(!next)next=alive[0];
 G.active=next;
 if(!G.active){endGame();return;}
 G.weapon=validWeapon(G.weapon);
 G.camLock=null;
 centerCam(G.active.x,G.active.y);
 G.cam.tz=1.0;
 Snd.turn();
 const t=TEAMS[G.teamIdx];
 showMsg(t.name,(Cfg.cpu[G.teamIdx]?"CPU · ":"")+G.active.name.toUpperCase(),t.col);
 buildTeamBar();buildWeaponHud();
 // maybe drop a crate
 if(!first&&Math.random()<0.34)dropCrate();
 if(Cfg.cpu[G.teamIdx]){G.cpuThinking=true;setTimeout(()=>planAI(),650);}
}
function validWeapon(id){
 if(G.ammo[G.teamIdx][id]>0)return id;
 for(const w of WEAPONS)if(G.ammo[G.teamIdx][w.id]>0)return w.id;
 return "bazooka";
}
function endTurn(){
 if(G.phase==="over")return;
 G.phase="wait";G.waitTimer=0;
}
function afterActions(){
 // check win
 if(aliveTeams().length<=1){endGame();return;}
 G.roundCount++;
 startTurn(false);
}
function endGame(){
 G.phase="over";G.over=true;
 const at=aliveTeams();
 if(at.length===1){
  const t=TEAMS[at[0]];showMsg(t.name+" WINS","VICTORY",t.col,4.5);Snd.win();
 } else {showMsg("DRAW","MUTUAL DESTRUCTION","#ccc",4.5);}
 setTimeout(()=>{document.getElementById("menu").style.display="flex";document.getElementById("menu").style.opacity=0;
  requestAnimationFrame(()=>{document.getElementById("menu").style.transition="opacity .6s";document.getElementById("menu").style.opacity=1;});},4200);
}
function updateWind(){
 const mag=rnd(1,0)**1.4;
 G.wind=(Math.random()<.5?-1:1)*mag*46;
 const f=document.getElementById("windFill");
 const p=clamp(G.wind/50,-1,1);
 f.style.width=Math.abs(p)*50+"%";
 f.style.left=(p<0?50-Math.abs(p)*50:50)+"%";
 f.style.background=Math.abs(p)>.6?"linear-gradient(90deg,#ff8b3d,#ff5d5d)":"linear-gradient(90deg,#4ad6ff,#7affc8)";
 const s=Math.abs(G.wind);
 document.getElementById("windTxt").textContent=(s<4?"CALM":(G.wind<0?"◀ ":"")+Math.round(s)+(G.wind>0?" ▶":""))+(s>34?"  GALE":"");
}
function dropCrate(){
 const kinds=[{k:"health",ico:"➕",col:"#7affa0"},{k:"ammo",ico:"📦",col:"#ffd166"},{k:"util",ico:"🎁",col:"#5db8ff"}];
 const c=pick(kinds);
 const x=rnd(MAPW-140,140);
 G.crates.push({x,y:-30,vx:0,vy:0,...c,chute:true,r:11});
 showToast("SUPPLY DROP INBOUND");
 Snd.chute();
}
let toastT=0;
function showToast(s){const el=document.getElementById("sub");el.textContent=s;el.style.opacity=1;toastT=2.4;}

function showMsg(m,s,col,dur=1.9){
 const e=document.getElementById("msg"),se=document.getElementById("sub");
 e.textContent=m;e.style.color=col||"#fff";e.style.opacity=1;e.style.transition="none";
 e.style.transform="translate(-50%,-50%) scale(1.25)";
 se.textContent=s||"";se.style.opacity=.85;
 requestAnimationFrame(()=>{e.style.transition="opacity .5s ease, transform .5s cubic-bezier(.2,1.2,.3,1)";
  e.style.transform="translate(-50%,-50%) scale(1)";});
 G.msgT=dur;
}

/* ---------------- FIRING ---------------- */
function currentWeapon(){return WMAP[G.weapon]}
function canFire(){return G.phase==="aim"&&!G.firing&&G.active&&!G.active.dead&&G.active.jet<=0}

function fireWeapon(power){
 const w=currentWeapon();const a=G.active;
 if(!a||a.dead)return;
 if(G.ammo[G.teamIdx][w.id]<=0)return;
 if(w.ammo!==Infinity)G.ammo[G.teamIdx][w.id]--;
 G.firing=true;G.lastFireW=w;
 a.recoil=1;
 const ang=a.aim*1;
 const dirx=Math.cos(ang)*a.face, diry=Math.sin(ang);
 const mx=a.x+dirx*14, my=a.y+diry*14;
 if(w.cat==="direct"){
  if(w.id==="shotgun"){
   Snd.shotgun();
   hitscanCone(a,mx,my,dirx,diry,w);
   a.vx-=dirx*1.2;
   if(!a._sgShot){a._sgShot=1;G.firing=false;G.phase="aim";
    G.ammo[G.teamIdx][w.id]++; // second barrel is free
    showToast("ONE BARREL LEFT");
    return;
   }
   a._sgShot=0;
   G.retreat=3.0;G.phase="retreat";
   return;
  }
  if(w.id==="railgun"){
   Snd.laser();railBeam(a,mx,my,dirx,diry,w);
   a.vx-=dirx*2.4;G.shake=Math.max(G.shake,10);
   G.retreat=3.0;G.phase="retreat";return;
  }
 }
 // arc / aoe projectile
 const sp=(w.pw||1)*(6+power*17);
 const p=new Proj(mx,my,dirx*sp,diry*sp,w,G.teamIdx);
 p.ownerWorm=a;p.ownerTeam=G.teamIdx;
 G.projs.push(p);
 Snd.fire();
 a.vx-=dirx*(w.id==="holy"?1.6:0.9);
 // muzzle
 for(let i=0;i<16;i++)G.parts.push(new Particle(mx,my,dirx*rnd(5,1)+rnd(1.4,-1.4),diry*rnd(5,1)+rnd(1.4,-1.4),rnd(20,8),pick(["#fff6c9","#ffd166","#ff8b3d"]),rnd(3.4,1.4),{g:.03,glow:true,drag:.9}));
 G.shake=Math.max(G.shake,4);
 G.phase="fly";G.camLock=p;
}
function hitscanCone(a,mx,my,dirx,diry,w){
 const base=Math.atan2(diry,dirx);
 for(let i=0;i<w.pellets;i++){
  const ang=base+rnd(w.spread,-w.spread);
  const r=raycast(mx,my,ang,w.range);
  // tracer
  G.parts.push(new Particle(mx,my,0,0,8,"rgba(255,240,200,.9)",2,{g:0,glow:true}));
  tracer(mx,my,r.x,r.y,"rgba(255,230,160,.75)",1.2);
  if(r.hitWorm){r.hitWorm.damage(w.dmg/w.pellets*2.2,a);
   if(a.team!==r.hitWorm.team)G.turnDamage+=w.dmg/w.pellets*2.2;
   bloodBurst(r.x,r.y,ang);}
  else if(r.hit){ blast(r.x,r.y,w.rad*.34,true);
   for(let k=0;k<5;k++)G.parts.push(new Particle(r.x,r.y,-Math.cos(ang)*rnd(3,.4)+rnd(1,-1),-Math.sin(ang)*rnd(3,.4)+rnd(1,-1),rnd(20,8),pick([terrainPalette.soil,terrainPalette.top2]),rnd(2.6,1),{g:.1}));
   explosionSmall(r.x,r.y,10);
  }
 }
 G.shake=Math.max(G.shake,7);
}
function railBeam(a,mx,my,dirx,diry,w){
 const ang=Math.atan2(diry,dirx);
 let x=mx,y=my,hits=0;
 const pts=[[x,y]];
 for(let d=0;d<w.range;d+=2){
  x+=Math.cos(ang)*2;y+=Math.sin(ang)*2;
  if(x<0||x>MAPW||y>MAPH||y<-800)break;
  if(solidAt(x,y)){ blast(x,y,13,false); if(Math.random()<.3)G.parts.push(new Particle(x,y,rnd(2,-2),rnd(2,-2),rnd(22,10),pick([terrainPalette.soil,terrainPalette.soil2]),rnd(3,1.2),{g:.12})); }
  for(const wm of G.worms){ if(wm.dead||wm===a)continue;
   if(!wm._railHit&&dist(x,y,wm.x,wm.y)<12){wm._railHit=1;wm.damage(w.dmg,a);
    if(a.team!==wm.team)G.turnDamage+=w.dmg;
    bloodBurst(wm.x,wm.y,ang);wm.vx+=Math.cos(ang)*3;wm.vy+=Math.sin(ang)*3-1;wm.onGround=false;hits++;}
  }
  if(y>WATER_Y){splashFX(x,WATER_Y,.7);break;}
 }
 G.worms.forEach(w2=>w2._railHit=0);
 G.rail={x1:mx,y1:my,x2:x,y2:y,life:1};
 tracer(mx,my,x,y,"rgba(180,240,255,.95)",4);
 repaintRing((mx+x)/2,(my+y)/2,Math.hypot(x-mx,y-my)/2+20);
}
function tracer(x1,y1,x2,y2,col,wd){
 G.tracers=G.tracers||[];G.tracers.push({x1,y1,x2,y2,col,wd,life:1});
}
function bloodBurst(x,y,ang){
 for(let i=0;i<14;i++)G.parts.push(new Particle(x,y,Math.cos(ang)*rnd(4,.5)+rnd(2,-2),Math.sin(ang)*rnd(4,.5)+rnd(2,-2),rnd(28,12),pick(["#ff5d6e","#c22a3d","#ff9aa5"]),rnd(3.2,1.2),{g:.16}));
}
function explosionSmall(x,y,r){
 for(let i=0;i<12;i++){const a=rnd(TAU),sp=rnd(r*.2,.4);
  G.parts.push(new Particle(x,y,Math.cos(a)*sp,Math.sin(a)*sp,rnd(20,8),pick(["#fff6c9","#ffd166","#ff8b3d"]),rnd(3,1),{g:.06,glow:true,drag:.92}));}
 G.flashes.push({x,y,r:r*2,life:1,max:1});
}

/* ---------------- UTILITIES ---------------- */
function useUtility(w){
 const a=G.active;if(!a)return;
 if(G.ammo[G.teamIdx][w.id]<=0)return;
 if(w.util==="jet"){
  G.ammo[G.teamIdx][w.id]--;a.jet=4;Snd.chute();showToast("JETPACK · ↑ THRUST  ← → STEER");
  buildWeaponHud();G.weapon=validWeapon("bazooka");
 } else if(w.util==="tele"){
  G.teleMode=true;showToast("CLICK TO TELEPORT");
 } else if(w.util==="girder"){
  G.girderMode=true;showToast("CLICK TO PLACE · SCROLL TO ROTATE");
 } else if(w.util==="heal"){
  G.ammo[G.teamIdx][w.id]--;
  a.hp=Math.min(a.maxhp,a.hp+45);Snd.heal();
  G.floats.push(new FloatText(a.x,a.y-24,"+45","#7affa0",24));
  for(let i=0;i<26;i++)G.parts.push(new Particle(a.x+rnd(12,-12),a.y+rnd(10,-10),rnd(.8,-.8),-rnd(1.6,.3),rnd(40,18),"#7affa0",rnd(3,1),{g:-.02,glow:true}));
  buildTeamBar();buildWeaponHud();G.weapon=validWeapon("bazooka");
 } else if(w.util==="chute"){
  G.ammo[G.teamIdx][w.id]--;a.chute=true;Snd.chute();showToast("PARACHUTE ARMED");
  buildWeaponHud();G.weapon=validWeapon("bazooka");
 }
}
function doTeleport(x,y){
 const a=G.active;
 if(y>WATER_Y-6)  {showToast("CAN'T TELEPORT INTO WATER");return;}
 // find free spot
 let ty=y;let ok=false;
 for(let k=0;k<80;k++){ if(!collidesAt(x,ty)){ok=true;break;} ty-=2; }
 if(!ok){showToast("BLOCKED");return;}
 G.ammo[G.teamIdx]["teleport"]--;
 Snd.teleport();
 for(let i=0;i<34;i++)G.parts.push(new Particle(a.x,a.y,rnd(4,-4),rnd(4,-4),rnd(30,12),"#b98bff",rnd(3.4,1.2),{g:0,glow:true,drag:.9}));
 a.x=x;a.y=ty;a.vx=0;a.vy=0;a.fallStart=null;a.onGround=false;
 for(let i=0;i<34;i++)G.parts.push(new Particle(x,ty,rnd(4,-4),rnd(4,-4),rnd(30,12),"#8bd6ff",rnd(3.4,1.2),{g:0,glow:true,drag:.9}));
 G.teleMode=false;G.weapon=validWeapon("bazooka");buildWeaponHud();
 centerCam(x,ty);
}
function placeGirder(x,y){
 const ang=G.girderAng;const L=88,Th=13;
 G.ammo[G.teamIdx]["girder"]--;
 // rasterize rotated rect into mask
 const c=Math.cos(ang),s=Math.sin(ang);
 const R=Math.ceil(L/2)+4;
 for(let dy=-R;dy<=R;dy++)for(let dx=-R;dx<=R;dx++){
  const lx=dx*c+dy*s, ly=-dx*s+dy*c;
  if(Math.abs(lx)<=L/2&&Math.abs(ly)<=Th/2){
   const px=(x+dx)|0,py=(y+dy)|0;
   if(px>=0&&px<MAPW&&py>=0&&py<MAPH)T.mask[py*MAPW+px]=1;
  }
 }
 G.girders.push({x,y,ang,L,Th});
 repaintTerrainRegion(x-R-3,y-R-3,R*2+6,R*2+6);
 // paint metal look
 const tc=T.ctx;tc.save();tc.translate(x,y);tc.rotate(ang);
 const g=tc.createLinearGradient(0,-Th/2,0,Th/2);
 g.addColorStop(0,"#9aa4b8");g.addColorStop(.4,"#5b6478");g.addColorStop(1,"#333a49");
 tc.fillStyle=g;tc.fillRect(-L/2,-Th/2,L,Th);
 tc.fillStyle="rgba(255,255,255,.18)";tc.fillRect(-L/2,-Th/2,L,2);
 tc.fillStyle="rgba(0,0,0,.3)";
 for(let i=-L/2+6;i<L/2-2;i+=14){tc.beginPath();tc.arc(i,0,1.8,0,TAU);tc.fill();}
 tc.restore();
 Snd.clank();
 G.girderMode=false;G.weapon=validWeapon("bazooka");buildWeaponHud();
}

/* ---------------- CAMERA ---------------- */
function centerCam(x,y,instant){
 G.cam.tx=clamp(x,0,MAPW);G.cam.ty=clamp(y,0,MAPH);
 if(instant){G.cam.x=G.cam.tx;G.cam.y=G.cam.ty;}
}
function updateCam(dt){
 const c=G.cam;
 let tx=c.tx,ty=c.ty;
 if(G.camLock&&!G.camLock.dead){tx=G.camLock.x;ty=G.camLock.y;}
 else if(G.active&&!G.active.dead&&(G.phase==="aim"||G.phase==="retreat")){tx=G.active.x;ty=G.active.y-20;}
 // manual pan with right-drag handled elsewhere
 if(G.panX!==undefined){tx=G.panX;ty=G.panY;}
 const sp=1-Math.pow(0.86,dt);
 c.x=lerp(c.x,tx,sp);c.y=lerp(c.y,ty,sp);
 c.zoom=lerp(c.zoom,c.tz,1-Math.pow(0.9,dt));
 const vw=W()/c.zoom,vh=H()/c.zoom;
 c.x=clamp(c.x,Math.min(vw/2,MAPW/2),Math.max(MAPW-vw/2,MAPW/2));
 c.y=clamp(c.y,vh/2-160,MAPH-vh/2+120);
}
function w2s(x,y){const c=G.cam;return[(x-c.x)*c.zoom+W()/2,(y-c.y)*c.zoom+H()/2]}
function s2w(x,y){const c=G.cam;return[(x-W()/2)/c.zoom+c.x,(y-H()/2)/c.zoom+c.y]}

/* ============================================================
   CPU AI — risk assessment + ballistic solving
   ============================================================ */
function simulateShot(sx,sy,ang,power,w,ignoreWorm){
 // returns {x,y,hitWorm,steps} landing point
 const sp=(w.pw||1)*(6+power*17);
 let x=sx,y=sy,vx=Math.cos(ang)*sp,vy=Math.sin(ang)*sp;
 const windF=(w.wind!==undefined?w.wind:1);
 let bounce=w.bounce||0, fuse=w.fuse||null;
 for(let i=0;i<900;i++){
  vy+=GRAV;vx+=G.wind*0.0022*windF;
  if(fuse!==null){fuse-=1/60;if(fuse<=0)return{x,y,t:i};}
  const steps=Math.max(1,Math.ceil((Math.abs(vx)+Math.abs(vy))/2.5));
  for(let s=0;s<steps;s++){
   const nx=x+vx/steps,ny=y+vy/steps;
   for(const wm of G.worms){if(wm.dead||wm===ignoreWorm)continue;
    if(dist(nx,ny,wm.x,wm.y)<11)return{x:nx,y:ny,hitWorm:wm,t:i};}
   if(ny>WATER_Y)return{x:nx,y:WATER_Y,water:true,t:i};
   if(solidAt(nx,ny)){
    if(bounce>0){const n=surfaceNormal(nx,ny);const dot=vx*n.x+vy*n.y;
     vx=(vx-2*dot*n.x)*bounce;vy=(vy-2*dot*n.y)*bounce;x+=n.x*2.2;y+=n.y*2.2;
     if(Math.hypot(vx,vy)<0.35&&fuse===null)return{x,y,t:i};
     s=steps;break;}
    return{x:nx,y:ny,t:i};
   }
   x=nx;y=ny;
  }
  if(x<-150||x>MAPW+150||y>MAPH+150)return{x,y,off:true,t:i};
 }
 return{x,y,t:900};
}
function scoreImpact(ix,iy,rad,dmg,me){
 let sc=0;
 for(const wm of G.worms){
  if(wm.dead)continue;
  const d=dist(ix,iy,wm.x,wm.y);
  if(d>rad*1.5)continue;
  const f=clamp(1-d/(rad*1.5),0,1);
  const dm=Math.min(wm.hp,dmg*Math.pow(f,.75));
  if(dm<1)continue;
  let v=dm;
  // kill bonus
  if(dm>=wm.hp)v+=45;
  // near water bonus (knockback into water)
  if(wm.y>WATER_Y-90)v+=dm*.5;
  if(wm.team===me.team){sc-= (wm===me? v*2.6 : v*2.2);}
  else sc+=v*(1+ (1-wm.hp/wm.maxhp)*0.5);
 }
 return sc;
}
function riskAt(x,y){
 // higher = more dangerous
 let r=0;
 if(y>WATER_Y-70)r+=90;
 // proximity to water horizontally (edge of island)
 let ground=-1;
 for(let yy=Math.max(0,y-30);yy<MAPH;yy++)if(solidAt(x,yy)){ground=yy;break;}
 if(ground<0)r+=60; else if(ground>WATER_Y-40)r+=70;
 // nearby mines
 for(const p of G.projs)if(p.proximity&&dist(x,y,p.x,p.y)<70)r+=50;
 return r;
}

function planAI(){
 if(!G.active||G.phase==="over")return;
 const me=G.active;
 const enemies=G.worms.filter(w=>!w.dead&&w.team!==me.team);
 if(!enemies.length){G.cpuThinking=false;endTurn();return;}
 const myAmmo=G.ammo[G.teamIdx];
 const skill=0.86;
 let best=null;
 const candidates=[];
 // weapon list AI can use
 const usable=WEAPONS.filter(w=>w.cat!=="util"&&(myAmmo[w.id]===Infinity||myAmmo[w.id]>0)&&w.id!=="airstrike"&&w.id!=="quake");
 for(const w of usable){
  if(w.cat==="direct"){
   for(const e of enemies){
    const ang=Math.atan2(e.y-me.y,e.x-me.x);
    const d=dist(me.x,me.y,e.x,e.y);
    if(d>(w.range||600))continue;
    // line of sight
    let blocked=0;
    for(let s=14;s<d;s+=4){const px=me.x+Math.cos(ang)*s,py=me.y+Math.sin(ang)*s;if(solidAt(px,py))blocked++;}
    if(w.id==="shotgun"&&blocked>3)continue;
    if(w.id==="railgun"&&blocked>90)continue;
    let sc=w.id==="shotgun"?scoreImpact(e.x,e.y,w.rad,w.dmg*1.6,me):w.dmg*1.3+(w.dmg>=e.hp?45:0);
    if(w.id==="railgun"){ // count pierced enemies
     let n=0;for(const e2 of enemies){const dd=Math.abs(Math.sin(ang)*(e2.x-me.x)-Math.cos(ang)*(e2.y-me.y));
      const along=Math.cos(ang)*(e2.x-me.x)+Math.sin(ang)*(e2.y-me.y);
      if(dd<12&&along>0&&along<w.range)n++;}
     sc=w.dmg*n*1.4+(n>1?40:0);
     for(const f of G.worms)if(!f.dead&&f.team===me.team&&f!==me){
      const dd=Math.abs(Math.sin(ang)*(f.x-me.x)-Math.cos(ang)*(f.y-me.y));
      const along=Math.cos(ang)*(f.x-me.x)+Math.sin(ang)*(f.y-me.y);
      if(dd<12&&along>0)sc-=w.dmg*2.4;}
    }
    if(w.id==="shotgun"&&d<70)sc-=30;
    sc-=(myAmmo[w.id]===Infinity?0:12);
    candidates.push({w,ang,power:1,score:sc,direct:true});
   }
   continue;
  }
  // arc weapons — sample angle/power grid
  const aimSign=1;
  for(let face of [-1,1]){
   for(let ai=0;ai<26;ai++){
    const ang0=-Math.PI*0.92+ai*(Math.PI*0.84/25); // -166° .. +
    const ang=face>0?ang0:Math.PI-ang0;
    for(let pi=3;pi<=10;pi++){
     const power=pi/10;
     const r=simulateShot(me.x+Math.cos(ang)*14,me.y+Math.sin(ang)*14,ang,power,w,me);
     if(r.off||r.water)continue;
     if(r.t>820)continue;
     let sc=scoreImpact(r.x,r.y,w.rad,w.dmg,me);
     if(r.hitWorm&&r.hitWorm.team!==me.team)sc+=14;
     sc-=(myAmmo[w.id]===Infinity?0:14);
     sc-=r.t*0.006;
     if(sc>4)candidates.push({w,ang,power,score:sc,face});
    }
   }
  }
 }
 candidates.sort((a,b)=>b.score-a.score);
 best=candidates[0];
 // Decide: shoot, or move to better ground
 const myRisk=riskAt(me.x,me.y);
 let action;
 if(best&&best.score>16){action={type:"shoot",plan:best};}
 else if(myRisk>60||(!best||best.score<8)){action={type:"reposition"};}
 else if(best&&best.score>4){action={type:"shoot",plan:best};}
 else action={type:"reposition"};

 if(action.type==="reposition"){
  // choose direction that reduces risk & closes distance to nearest enemy
  let near=enemies.reduce((p,c)=>dist(me.x,me.y,c.x,c.y)<dist(me.x,me.y,p.x,p.y)?c:p);
  let dir=Math.sign(near.x-me.x)||1;
  // check risk ahead
  if(riskAt(me.x+dir*70,me.y)>riskAt(me.x-dir*70,me.y)+30)dir*=-1;
  G.aiPlan={mode:"move",dir,until:performance.now()+rnd(2600,1100),then:best};
 } else {
  G.aiPlan={mode:"aimshoot",plan:best,t:0};
 }
 G.cpuThinking=false;
}

function updateAI(dt){
 if(!Cfg.cpu[G.teamIdx]||G.phase!=="aim"||!G.active||G.active.dead)return;
 const me=G.active;
 if(G.cpuThinking)return;
 if(!G.aiPlan){ if(G.timeLeft<G.turnTime-2)endTurn(); return; }
 const P=G.aiPlan;
 if(P.mode==="move"){
  me.aiMove=P.dir;
  me.face=P.dir;
  // jump over obstacles
  if(collidesAt(me.x+P.dir*8,me.y)&&collidesAt(me.x+P.dir*8,me.y-7)&&me.onGround&&Math.random()<.12)me.jump();
  if(performance.now()>P.until||riskAt(me.x+P.dir*30,me.y)>90){
   me.aiMove=0;
   G.cpuThinking=true;
   setTimeout(()=>{ if(G.phase!=="aim")return; 
     const savedT=G.timeLeft; planAI();
     if(G.aiPlan&&G.aiPlan.mode==="move")G.aiPlan={mode:"aimshoot",plan:G.aiPlan.then||null,t:0};
   },420);
  }
  if(G.timeLeft<8){me.aiMove=0;G.aiPlan={mode:"aimshoot",plan:P.then||null,t:0};}
  return;
 }
 if(P.mode==="aimshoot"){
  if(!P.plan){ // nothing good: lob a bazooka at nearest enemy
   const enemies=G.worms.filter(w=>!w.dead&&w.team!==me.team);
   if(!enemies.length){endTurn();return;}
   const e=pick(enemies);
   const ang=Math.atan2(e.y-me.y-120,e.x-me.x);
   P.plan={w:WMAP.bazooka,ang,power:clamp(dist(me.x,me.y,e.x,e.y)/900,.35,1)};
  }
  const pl=P.plan;
  G.weapon=pl.w.id;buildWeaponHud();
  // face + smoothly move aim
  const rel=Math.abs(pl.ang)>Math.PI/2?-1:1;
  me.face=rel>=0?1:-1;
  let target=rel>=0?pl.ang:(pl.ang>0?Math.PI-pl.ang:-Math.PI-pl.ang);
  target=clamp(target,-Math.PI/2+0.02,Math.PI/2-0.02);
  me.aim=lerp(me.aim,target,1-Math.pow(.86,dt));
  P.t+=dt*.016;
  G.charging=true;
  G.power=Math.min(pl.power,G.power+dt*.014);
  const aimed=Math.abs(me.aim-target)<0.02;
  if(P.t>0.7&&aimed&&G.power>=pl.power-0.005){
   G.charging=false;
   // small aim error
   me.aim=target+rnd(.014,-.014);
   fireWeapon(pl.power);
   G.aiPlan=null;
  }
  if(P.t>8){G.charging=false;G.aiPlan=null;endTurn();}
 }
}

/* ============================================================
   BACKGROUND / RENDER
   ============================================================ */
let skyGrad=null,skyKey="";
function drawSky(c){
 const key=W()+"x"+H();
 if(skyKey!==key||!skyGrad){
  skyGrad=c.createLinearGradient(0,0,0,H());
  skyGrad.addColorStop(0,"#0b1430");skyGrad.addColorStop(.35,"#1b3a63");
  skyGrad.addColorStop(.65,"#3d6a92");skyGrad.addColorStop(1,"#7fa6bd");
  skyKey=key;
 }
 c.fillStyle=skyGrad;c.fillRect(0,0,W(),H());
 // sun glow
 const [sx,sy]=w2s(MAPW*0.78,120);
 const g=c.createRadialGradient(sx,sy,0,sx,sy,340*G.cam.zoom);
 g.addColorStop(0,"rgba(255,230,180,.5)");g.addColorStop(.3,"rgba(255,190,120,.16)");g.addColorStop(1,"rgba(255,180,100,0)");
 c.fillStyle=g;c.fillRect(0,0,W(),H());
 c.fillStyle="rgba(255,244,214,.9)";c.beginPath();c.arc(sx,sy,34*G.cam.zoom,0,TAU);c.fill();
 // stars (parallax, faint)
 c.fillStyle="rgba(255,255,255,.5)";
 for(const s of G.stars){
  const px=(s.x-G.cam.x*.12)%MAPW,[X,Y]=[px*.5+W()*0,s.y-G.cam.y*.06];
  const tw=.4+.6*Math.abs(Math.sin(performance.now()*.001+s.p));
  c.globalAlpha=tw*.35;c.fillRect(X,Y,s.r,s.r);
 }
 c.globalAlpha=1;
 // far mountains
 drawParallaxHills(c,.18,"#16283f",MAPH*0.62);
 drawParallaxHills(c,.34,"#1e3550",MAPH*0.72);
}
let hillCache={};
function drawParallaxHills(c,depth,col,base){
 const key=depth+col;
 if(!hillCache[key]){
  const pts=[];const n=40;
  for(let i=0;i<=n;i++)pts.push(Math.sin(i*1.7+depth*30)*0.5+Math.sin(i*0.6+depth*11)*0.5);
  hillCache[key]=pts;
 }
 const pts=hillCache[key];
 const camx=G.cam.x*depth, camy=G.cam.y*depth;
 c.fillStyle=col;c.beginPath();
 const w=MAPW*1.4/(pts.length-1);
 c.moveTo(-200,H()+10);
 for(let i=0;i<pts.length;i++){
  const x=(i*w-camx)*G.cam.zoom+W()/2-MAPW*0.2*G.cam.zoom;
  const y=(base+pts[i]*160-camy)*G.cam.zoom+H()/2;
  c.lineTo(x,y);
 }
 c.lineTo(W()+200,H()+10);c.closePath();c.fill();
}
function drawClouds(c){
 for(const cl of G.clouds){
  cl.x+=G.wind*0.00035*cl.sp*16;
  if(cl.x>MAPW+400)cl.x=-400;if(cl.x<-400)cl.x=MAPW+400;
  const [X,Y]=w2s(cl.x,cl.y);
  const z=G.cam.zoom*(0.5+cl.s*0.4);
  const s=52*cl.s*z;
  if(X<-300||X>W()+300)continue;
  c.globalAlpha=cl.o;c.fillStyle="#dfeaf5";
  c.beginPath();
  c.ellipse(X,Y,s,s*.42,0,0,TAU);
  c.ellipse(X-s*.5,Y+s*.1,s*.55,s*.3,0,0,TAU);
  c.ellipse(X+s*.55,Y+s*.08,s*.6,s*.32,0,0,TAU);
  c.ellipse(X+s*.1,Y-s*.22,s*.5,s*.34,0,0,TAU);
  c.fill();c.globalAlpha=1;
 }
}
function drawWater(c){
 const t=performance.now()*.001;
 const [,wy]=w2s(0,WATER_Y);
 if(wy>H())return;
 const z=G.cam.zoom;
 c.save();
 // body
 const g=c.createLinearGradient(0,wy,0,H());
 g.addColorStop(0,"rgba(70,170,220,.62)");g.addColorStop(.4,"rgba(28,96,150,.82)");g.addColorStop(1,"rgba(10,40,72,.95)");
 c.fillStyle=g;c.fillRect(0,Math.max(0,wy),W(),H()-Math.max(0,wy));
 // wave crests
 c.strokeStyle="rgba(190,238,255,.55)";c.lineWidth=2;
 for(let k=0;k<3;k++){
  c.beginPath();
  for(let x=0;x<=W();x+=8){
   const wx=s2w(x,0)[0];
   const y=wy+k*7*z+Math.sin(wx*0.012+t*(1.2+k*.4)+k)*5*z+Math.sin(wx*0.031-t*1.7)*2.4*z;
   x===0?c.moveTo(x,y):c.lineTo(x,y);
  }
  c.globalAlpha=.5-k*.13;c.stroke();
 }
 c.globalAlpha=1;
 // shimmer
 c.globalCompositeOperation="lighter";
 for(let i=0;i<26;i++){
  const x=((i*137.5+t*30)%W());
  const y=Math.max(0,wy)+((i*57)%Math.max(1,(H()-wy)))*1;
  c.fillStyle="rgba(255,255,255,.05)";c.fillRect(x,y,26,1.4);
 }
 c.globalCompositeOperation="source-over";
 c.restore();
}

/* ---------------- MAIN RENDER ---------------- */
function render(){
 const c=ctx2d;
 c.setTransform(1,0,0,1,0,0);
 c.clearRect(0,0,W(),H());
 drawSky(c);
 drawClouds(c);

 const z=G.cam.zoom;
 let shx=0,shy=0;
 if(G.shake>0.3){shx=rnd(G.shake,-G.shake);shy=rnd(G.shake,-G.shake);}
 c.save();
 c.translate(W()/2+shx,H()/2+shy);c.scale(z,z);c.translate(-G.cam.x,-G.cam.y);

 // terrain
 c.imageSmoothingEnabled=false;
 if(T.canvas)c.drawImage(T.canvas,0,0);
 // subtle terrain rim light
 c.globalCompositeOperation="source-atop";
 c.globalCompositeOperation="source-over";

 // ripples
 for(const r of G.ripples){
  c.strokeStyle=`rgba(200,240,255,${r.life*.5})`;c.lineWidth=2/z;
  c.beginPath();c.ellipse(r.x,r.y,r.r,r.r*.28,0,0,TAU);c.stroke();
 }
 // airstrike plane
 if(G.planeFX&&G.planeFX.life>0){
  const pf=G.planeFX;pf.x+=pf.dir*11;pf.life-=1/60;
  const py=Math.max(-220,G.cam.y-H()/G.cam.zoom/2-60);
  c.save();c.translate(pf.x,py);c.scale(pf.dir,1);
  c.fillStyle="#4a5266";c.beginPath();c.moveTo(34,0);c.lineTo(4,-8);c.lineTo(-30,-7);c.lineTo(-34,2);c.lineTo(-6,8);c.closePath();c.fill();
  c.fillStyle="#2e3444";c.beginPath();c.moveTo(2,-6);c.lineTo(-22,-26);c.lineTo(-30,-24);c.lineTo(-8,-4);c.closePath();c.fill();
  c.fillStyle="#9fd8ff";c.beginPath();c.ellipse(14,-3,7,3.4,0,0,TAU);c.fill();
  c.restore();
  if(Math.random()<.6)G.parts.push(new Particle(pf.x-pf.dir*34,py+2,-pf.dir*rnd(1,.2),rnd(.3,-.3),rnd(60,30),"rgba(220,225,235,.5)",rnd(4,2),{g:-.005,smoke:true,wind:1}));
 }
 // crates
 for(const cr of G.crates)drawCrate(c,cr);
 // debris
 for(const d of G.debris)d.draw(c);
 // worms
 for(const w of G.worms)w.draw(c);
 // graves
 for(const w of G.worms)if(w.dead&&w.grave>0)drawGrave(c,w);
 // projectiles
 for(const p of G.projs)p.draw(c);
 // tracers
 if(G.tracers)for(const t of G.tracers){
  c.globalCompositeOperation="lighter";
  c.strokeStyle=t.col;c.globalAlpha=t.life;c.lineWidth=t.wd*t.life;
  c.beginPath();c.moveTo(t.x1,t.y1);c.lineTo(t.x2,t.y2);c.stroke();
  c.globalAlpha=1;c.globalCompositeOperation="source-over";
 }
 // particles
 for(const p of G.parts)p.draw(c);
 // explosion flashes
 c.globalCompositeOperation="lighter";
 for(const f of G.flashes){
  const t=f.life/f.max;
  const g=c.createRadialGradient(f.x,f.y,0,f.x,f.y,f.r*(1.6-t*.5));
  g.addColorStop(0,`rgba(255,250,220,${t*.95})`);
  g.addColorStop(.35,`rgba(255,190,90,${t*.6})`);
  g.addColorStop(1,"rgba(255,90,30,0)");
  c.fillStyle=g;c.beginPath();c.arc(f.x,f.y,f.r*(1.6-t*.5),0,TAU);c.fill();
 }
 c.globalCompositeOperation="source-over";

 drawWater(c);

 // floats above water
 for(const f of G.floats)f.draw(c);

 // aiming
 if(G.phase==="aim"&&G.active&&!G.active.dead&&!Cfg.cpu[G.teamIdx]&&G.active.jet<=0)drawAimUI(c);
 if(G.phase==="aim"&&Cfg.cpu[G.teamIdx]&&G.aiPlan&&G.aiPlan.mode==="aimshoot")drawAimUI(c,true);
 if(G.girderMode)drawGirderGhost(c);
 if(G.teleMode)drawTeleGhost(c);
 if(G.targetMode)drawTargetGhost(c);

 c.restore();

 // vignette
 const vg=ctx2d.createRadialGradient(W()/2,H()/2,Math.min(W(),H())*.35,W()/2,H()/2,Math.max(W(),H())*.78);
 vg.addColorStop(0,"rgba(0,0,0,0)");vg.addColorStop(1,"rgba(0,0,10,.6)");
 c.fillStyle=vg;c.fillRect(0,0,W(),H());

 drawOffscreenIndicators(c);
}
function drawGrave(c,w){
 const t=TEAMS[w.team];
 c.save();c.translate(w.x,w.y+4);
 c.globalAlpha=.9;
 c.fillStyle="#6b7280";
 roundRect(c,-6,-14,12,16,5);c.fill();
 c.fillRect(-7,0,14,4);
 c.fillStyle="rgba(0,0,0,.35)";c.fillRect(-2,-11,4,2);c.fillRect(-1,-12,2,7);
 c.restore();c.globalAlpha=1;
}
function drawCrate(c,cr){
 c.save();c.translate(cr.x,cr.y);
 if(cr.chute){
  c.fillStyle="rgba(255,255,255,.85)";c.beginPath();c.arc(0,-24,17,Math.PI,0);c.closePath();c.fill();
  c.fillStyle=cr.col;c.beginPath();c.arc(0,-24,17,Math.PI*1.2,Math.PI*1.5);c.lineTo(0,-24);c.fill();
  c.strokeStyle="rgba(255,255,255,.6)";c.lineWidth=1;
  c.beginPath();c.moveTo(-16,-24);c.lineTo(-6,-8);c.moveTo(16,-24);c.lineTo(6,-8);c.stroke();
 }
 c.fillStyle="#8a6236";roundRect(c,-10,-9,20,19,3);c.fill();
 c.strokeStyle="#5c3f20";c.lineWidth=2;c.stroke();
 c.fillStyle=cr.col;c.fillRect(-10,-2,20,4);
 c.font="11px sans-serif";c.textAlign="center";c.fillText(cr.ico,0,4);
 const pulse=.5+.5*Math.sin(performance.now()*.005);
 c.globalCompositeOperation="lighter";
 c.fillStyle=cr.col;c.globalAlpha=.18*pulse;c.beginPath();c.arc(0,0,22,0,TAU);c.fill();
 c.restore();c.globalAlpha=1;c.globalCompositeOperation="source-over";
}
function drawAimUI(c,isAI){
 const a=G.active;const w=currentWeapon();
 const ang=a.aim,dx=Math.cos(ang)*a.face,dy=Math.sin(ang);
 // crosshair
 const cx=a.x+dx*46,cy=a.y+dy*46;
 c.strokeStyle=isAI?"rgba(255,120,120,.8)":"rgba(255,255,255,.85)";c.lineWidth=1.6/G.cam.zoom;
 c.beginPath();c.arc(cx,cy,6,0,TAU);c.stroke();
 c.beginPath();c.moveTo(cx-11,cy);c.lineTo(cx-7,cy);c.moveTo(cx+7,cy);c.lineTo(cx+11,cy);
 c.moveTo(cx,cy-11);c.lineTo(cx,cy-7);c.moveTo(cx,cy+7);c.lineTo(cx,cy+11);c.stroke();
 // weapon barrel
 c.save();c.translate(a.x,a.y);c.rotate(Math.atan2(dy,dx));
 c.fillStyle="#3a4152";c.fillRect(2,-2.6,17,5.2);
 c.fillStyle="#5d677e";c.fillRect(2,-2.6,17,1.6);
 c.restore();

 // predictive trajectory
 if(w.cat==="arc"||w.cat==="aoe"&&!w.target&&!w.quake){
  const pw=G.charging?G.power:(isAI?G.power:0.62);
  const sp=(w.pw||1)*(6+pw*17);
  let x=a.x+dx*14,y=a.y+dy*14,vx=dx*sp,vy=dy*sp;
  const windF=(w.wind!==undefined?w.wind:1);
  c.setLineDash([]);
  const maxPts=G.charging?150:80;
  let prev=null;
  for(let i=0;i<maxPts;i++){
   vy+=GRAV;vx+=G.wind*0.0022*windF;
   x+=vx;y+=vy;
   if(y>WATER_Y||solidAt(x,y)||x<0||x>MAPW)break;
   if(i%3===0){
    const t=1-i/maxPts;
    c.globalAlpha=t*(G.charging?.85:.4);
    c.fillStyle=G.charging?"#ffe082":"rgba(255,255,255,.8)";
    c.beginPath();c.arc(x,y,Math.max(.8,2.1*t),0,TAU);c.fill();
   }
  }
  c.globalAlpha=1;
 } else if(w.cat==="direct"){
  const r=raycast(a.x+dx*14,a.y+dy*14,Math.atan2(dy,dx),w.range||600);
  c.globalAlpha=.35;c.strokeStyle=w.id==="railgun"?"#9fe4ff":"#ffd7a0";
  c.setLineDash([6/G.cam.zoom,6/G.cam.zoom]);c.lineWidth=1.4/G.cam.zoom;
  c.beginPath();c.moveTo(a.x+dx*14,a.y+dy*14);
  if(w.pierce){c.lineTo(a.x+Math.cos(Math.atan2(dy,dx))*w.range,a.y+Math.sin(Math.atan2(dy,dx))*w.range);}
  else c.lineTo(r.x,r.y);
  c.stroke();c.setLineDash([]);c.globalAlpha=1;
 }
}
function raycast(x,y,ang,maxd){
 const cx=Math.cos(ang),cy=Math.sin(ang);
 for(let d=0;d<maxd;d+=2){
  const px=x+cx*d,py=y+cy*d;
  if(px<0||px>MAPW||py>MAPH)return{x:px,y:py,hit:false};
  for(const wm of G.worms){if(wm.dead)continue;if(d>10&&dist(px,py,wm.x,wm.y)<10)return{x:px,y:py,hit:true,hitWorm:wm};}
  if(solidAt(px,py))return{x:px,y:py,hit:true};
 }
 return{x:x+cx*maxd,y:y+cy*maxd,hit:false};
}
function drawGirderGhost(c){
 const [mx,my]=[mouse.wx,mouse.wy];
 c.save();c.translate(mx,my);c.rotate(G.girderAng);
 c.globalAlpha=.6;c.fillStyle="#8a94a8";c.fillRect(-44,-6.5,88,13);
 c.strokeStyle="#dfe8ff";c.lineWidth=1.5/G.cam.zoom;c.strokeRect(-44,-6.5,88,13);
 c.restore();c.globalAlpha=1;
}
function drawTeleGhost(c){
 const [mx,my]=[mouse.wx,mouse.wy];
 const ok=my<WATER_Y-6;
 c.globalAlpha=.65;c.strokeStyle=ok?"#b98bff":"#ff6b6b";c.lineWidth=2/G.cam.zoom;
 c.beginPath();c.arc(mx,my,14+Math.sin(performance.now()*.008)*3,0,TAU);c.stroke();
 c.beginPath();c.arc(mx,my,7,0,TAU);c.stroke();c.globalAlpha=1;
}
function drawTargetGhost(c){
 const [mx,my]=[mouse.wx,mouse.wy];
 c.globalAlpha=.8;c.strokeStyle="#ff5d5d";c.lineWidth=2/G.cam.zoom;
 const r=26+Math.sin(performance.now()*.006)*4;
 c.beginPath();c.arc(mx,my,r,0,TAU);c.stroke();
 c.beginPath();c.moveTo(mx-r-8,my);c.lineTo(mx+r+8,my);c.moveTo(mx,my-r-8);c.lineTo(mx,my+r+8);c.stroke();
 c.globalAlpha=1;
}
function drawOffscreenIndicators(c){
 if(!G.active)return;
 const targets=[];
 if(G.camLock&&!G.camLock.dead)targets.push({x:G.camLock.x,y:G.camLock.y,col:"#ffd166"});
 else if(G.active&&!G.active.dead)targets.push({x:G.active.x,y:G.active.y,col:TEAMS[G.active.team].col});
 for(const t of targets){
  const [sx,sy]=w2s(t.x,t.y);
  if(sx>40&&sx<W()-40&&sy>40&&sy<H()-40)continue;
  const px=clamp(sx,34,W()-34),py=clamp(sy,34,H()-34);
  const ang=Math.atan2(sy-H()/2,sx-W()/2);
  c.save();c.translate(px,py);c.rotate(ang);
  c.fillStyle=t.col;c.globalAlpha=.9;
  c.beginPath();c.moveTo(12,0);c.lineTo(-8,-8);c.lineTo(-8,8);c.closePath();c.fill();
  c.restore();c.globalAlpha=1;
 }
}

/* ============================================================
   HUD
   ============================================================ */
function buildTeamBar(){
 const bar=document.getElementById("teamBar");
 bar.innerHTML="";
 for(let t=0;t<Cfg.teams;t++){
  const ws=G.worms.filter(w=>w.team===t);
  const total=ws.reduce((s,w)=>s+w.hp,0), max=ws.length*100;
  const el=document.createElement("div");
  el.className="tcard"+(t===G.teamIdx?" active":"")+(total<=0?" dead":"");
  el.innerHTML=`<div class="tname" style="color:${TEAMS[t].col}">${TEAMS[t].name}<i>${Cfg.cpu[t]?"CPU":"P"+(t+1)}</i></div>
   <div class="hpwrap"><div class="hpghost" style="width:${total/max*100}%"></div><div class="hpfill" style="width:${total/max*100}%;background:linear-gradient(90deg,${TEAMS[t].col2},${TEAMS[t].col})"></div></div>
   <div class="pips">${ws.map(w=>`<div class="pip" style="background:${w.dead?"rgba(255,255,255,.1)":(w===G.active?"#fff":TEAMS[t].col)}"></div>`).join("")}</div>`;
  bar.appendChild(el);
 }
}
let hpAnimTimer=0;
function refreshHP(){
 const bar=document.getElementById("teamBar");
 [...bar.children].forEach((el,t)=>{
  const ws=G.worms.filter(w=>w.team===t);
  const total=ws.reduce((s,w)=>s+w.hp,0),max=ws.length*100;
  el.querySelector(".hpfill").style.width=(total/max*100)+"%";
  el.querySelector(".hpghost").style.width=(total/max*100)+"%";
  el.classList.toggle("dead",total<=0);
  el.classList.toggle("active",t===G.teamIdx);
  const pips=el.querySelectorAll(".pip");
  ws.forEach((w,i)=>{if(pips[i])pips[i].style.background=w.dead?"rgba(255,255,255,.1)":(w===G.active?"#fff":TEAMS[t].col);});
 });
}
function buildWeaponHud(){
 const hud=document.getElementById("weaponHud"),inv=document.getElementById("invBar");
 hud.innerHTML="";inv.innerHTML="";
 for(const w of WEAPONS){
  const amt=G.ammo[G.teamIdx][w.id];
  const el=document.createElement("div");
  el.className="wslot"+(G.weapon===w.id?" sel":"")+(amt<=0?" out":"");
  el.innerHTML=`<div class="wkey">${w.key}</div><div class="wico">${w.ico}</div><div class="wnm">${w.name}</div>`+
   (w.ammo===Infinity?"":`<div class="wammo">${amt}</div>`);
  el.title=w.name+" — "+w.desc;
  el.onmouseenter=()=>{Snd.hover();showToast(w.name+" · "+w.desc.toUpperCase())};
  el.onclick=()=>{selectWeapon(w.id);Snd.click()};
  (w.cat==="util"?inv:hud).appendChild(el);
 }
}
function selectWeapon(id){
 if(G.phase!=="aim"||Cfg.cpu[G.teamIdx]||G.firing)return;
 if(G.ammo[G.teamIdx][id]<=0){Snd.hurt();return;}
 const w=WMAP[id];
 G.targetMode=false;G.teleMode=false;G.girderMode=false;
 G.weapon=id;
 if(w.cat==="util"){useUtility(w);}
 else if(w.target){G.targetMode=true;showToast("CLICK A TARGET ZONE");}
 else if(w.quake){/* instant on fire */}
 buildWeaponHud();
}
function updateHUD(dt){
 const tEl=document.getElementById("timer");
 const tl=Math.max(0,Math.ceil(G.timeLeft));
 tEl.textContent=String(tl).padStart(2,"0");
 const urgent=G.timeLeft<10&&G.phase==="aim";
 if(urgent){
  const p=1-G.timeLeft/10;
  tEl.style.color=`hsl(${lerp(190,0,p)},100%,${lerp(70,60,p)}%)`;
  const pulse=1+Math.abs(Math.sin(performance.now()*.006*(1+p*2)))*(.12+p*.3);
  tEl.style.transform=`scale(${pulse})`;
 } else {tEl.style.color="#8fe9ff";tEl.style.transform="scale(1)";}
 // power bar
 const pw=document.getElementById("power"),pf=document.getElementById("powerFill");
 pw.style.opacity=G.charging?1:0;
 pf.style.width=(G.power*100)+"%";
 // messages
 if(G.msgT>0){G.msgT-=dt*.016;
  if(G.msgT<=0){document.getElementById("msg").style.opacity=0;document.getElementById("sub").style.opacity=0;}}
 if(toastT>0){toastT-=dt*.016;if(toastT<=0&&G.msgT<=0)document.getElementById("sub").style.opacity=0;}
}

/* ============================================================
   GAME LOOP
   ============================================================ */
let lastT=performance.now(),acc=0;
let tickSoundT=0;
function loop(now){
 requestAnimationFrame(loop);
 let raw=(now-lastT);lastT=now;
 if(raw>100)raw=100;
 let dt=raw/16.6667;
 if(G.freeze>0){G.freeze-=raw/1000;dt*=0.08;}
 step(dt,raw/1000);
 render();
 updateHUD(dt);
}
function step(dt,secs){
 if(!G)return;
 updateCam(dt);
 G.shake*=Math.pow(.88,dt);
 // timers
 if(G.phase==="aim"&&!G.over){
  G.timeLeft-=secs;
  if(G.timeLeft<=0){G.timeLeft=0;endTurn();}
  // escalating tick
  if(G.timeLeft<11){
   tickSoundT-=secs;
   const interval=clamp(G.timeLeft/11,0.14,1)*0.85;
   if(tickSoundT<=0){tickSoundT=interval;Snd.tick(1-G.timeLeft/11);}
  }
 }
 if(G.phase==="retreat"){
  G.retreat-=secs;
  if(G.retreat<=0)endTurn();
 }
 // AI
 if(G.phase==="aim")updateAI(dt);
 // keyboard aim (human)
 if(humanTurn()&&G.active&&G.active.jet<=0){
  let d=0;
  if(keys["ArrowUp"]||keys["KeyW"])d-=1;
  if(keys["ArrowDown"]||keys["KeyS"])d+=1;
  if(d)G.active.aim=clamp(G.active.aim+d*0.026*dt,-Math.PI/2+.02,Math.PI/2-.02);
 }
 // dynamic zoom
 if(!G.over){
  let tz=1.0;
  if(G.phase==="fly")tz=0.82;
  else if(G.charging)tz=1.12;
  else if(G.phase==="settle"||G.phase==="wait")tz=0.9;
  G.cam.tz=lerp(G.cam.tz,tz,1-Math.pow(0.97,dt));
 }
 // wind streaks
 if(Math.abs(G.wind)>18&&Math.random()<0.12*dt){
  const wy=rnd(MAPH*0.85,60);
  const wx=G.wind>0?G.cam.x-W()/G.cam.zoom/2-40:G.cam.x+W()/G.cam.zoom/2+40;
  G.parts.push(new Particle(wx,wy,Math.sign(G.wind)*rnd(9,4),rnd(.4,-.4),rnd(90,50),"rgba(255,255,255,.30)",rnd(1.6,.7),{g:0,drag:1,spark:true,shrink:0}));
 }
 // charging
 if(G.charging&&!Cfg.cpu[G.teamIdx]){
  G.power=Math.min(1,G.power+dt*.018);
  if(G.power>=1){releaseFire();}
 }
 // worms
 for(const w of G.worms)w.update(dt);
 // projectiles
 for(let i=G.projs.length-1;i>=0;i--){ if(!G.projs[i].update(dt))G.projs.splice(i,1); }
 // debris
 for(let i=G.debris.length-1;i>=0;i--){ if(!G.debris[i].update(dt))G.debris.splice(i,1); }
 if(G.debris.length>420)G.debris.splice(0,G.debris.length-420);
 // particles
 for(let i=G.parts.length-1;i>=0;i--){ if(!G.parts[i].update(dt))G.parts.splice(i,1); }
 if(G.parts.length>1400)G.parts.splice(0,G.parts.length-1400);
 // floats
 for(let i=G.floats.length-1;i>=0;i--){ if(!G.floats[i].update(dt))G.floats.splice(i,1); }
 // flashes/ripples/tracers
 for(let i=G.flashes.length-1;i>=0;i--){G.flashes[i].life-=dt*.09;if(G.flashes[i].life<=0)G.flashes.splice(i,1);}
 for(let i=G.ripples.length-1;i>=0;i--){const r=G.ripples[i];r.r+=dt*2.2;r.life-=dt*.02;if(r.life<=0||r.r>r.max)G.ripples.splice(i,1);}
 if(G.tracers)for(let i=G.tracers.length-1;i>=0;i--){G.tracers[i].life-=dt*.12;if(G.tracers[i].life<=0)G.tracers.splice(i,1);}
 // crates
 updateCrates(dt);
 // camera lock release
 if(G.camLock&&(G.camLock.dead||G.projs.indexOf(G.camLock)<0))G.camLock=null;
 // phase transitions
 if(G.phase==="fly"){
  if(G.projs.length===0){ G.phase="settle"; G.settleT=0.9; }
 } else if(G.phase==="settle"){
  G.settleT-=secs;
  const busy=G.projs.length>0||G.worms.some(w=>!w.dead&&(!w.onGround||w.y>WATER_Y));
  if(busy)G.settleT=Math.max(G.settleT,0.35);
  if(G.settleT<=0){ G.phase="wait";G.waitTimer=0; }
 } else if(G.phase==="wait"){
  G.waitTimer+=secs;
  G.waitHard=(G.waitHard||0)+secs;
  const busy=G.projs.length>0||G.worms.some(w=>!w.dead&&(!w.onGround||w.y>WATER_Y));
  if(busy&&G.waitHard<9)G.waitTimer=Math.min(G.waitTimer,0.35);
  if(G.waitTimer>0.75){
   G.waitHard=0;
   if(G.turnDamage>0)showToast(Math.round(G.turnDamage)+" DAMAGE DEALT");
   afterActions();
  }
 }
 refreshHP();
 // dying worms cleanup
 for(const w of G.worms)if(w.dead&&w.grave<1)w.grave=1;
}
function updateCrates(dt){
 for(let i=G.crates.length-1;i>=0;i--){
  const cr=G.crates[i];
  cr.vy+=GRAV*dt*(cr.chute?0.1:1);
  if(cr.chute)cr.vy=Math.min(cr.vy,1.2);
  cr.vx+=G.wind*0.0018*dt*(cr.chute?1.6:.3);
  cr.vx*=Math.pow(.99,dt);
  let ny=cr.y+cr.vy*dt,nx=cr.x+cr.vx*dt;
  if(ny>WATER_Y){splashFX(nx,WATER_Y,.9);G.crates.splice(i,1);continue;}
  if(solidAt(nx,ny)){cr.chute=false;cr.vy=0;cr.vx*=.4;
   while(solidAt(cr.x,cr.y+1)===false&&cr.y<ny)cr.y++;
  } else {cr.y=ny;cr.x=nx;}
  if(cr.y>MAPH+50){G.crates.splice(i,1);continue;}
  // pickup
  for(const w of G.worms){
   if(w.dead)continue;
   if(dist(w.x,w.y,cr.x,cr.y)<18){
    collectCrate(cr,w);G.crates.splice(i,1);break;
   }
  }
 }
}
function collectCrate(cr,w){
 Snd.pickup();
 for(let i=0;i<30;i++)G.parts.push(new Particle(cr.x,cr.y,rnd(3,-3),rnd(1,-4),rnd(34,14),cr.col,rnd(3.4,1.2),{g:.08,glow:true}));
 if(cr.k==="health"){w.hp=Math.min(w.maxhp,w.hp+35);G.floats.push(new FloatText(w.x,w.y-26,"+35","#7affa0",22));showToast("HEALTH CRATE");}
 else if(cr.k==="ammo"){
  const opts=WEAPONS.filter(x=>x.ammo!==Infinity&&x.cat!=="util");
  const pk=pick(opts);G.ammo[w.team][pk.id]+=2;
  G.floats.push(new FloatText(w.x,w.y-26,"+2 "+pk.name,"#ffd166",17));showToast("AMMO CRATE · "+pk.name);
 } else {
  const opts=WEAPONS.filter(x=>x.cat==="util");
  const pk=pick(opts);G.ammo[w.team][pk.id]+=1;
  G.floats.push(new FloatText(w.x,w.y-26,"+1 "+pk.name,"#5db8ff",17));showToast("UTILITY CRATE · "+pk.name);
 }
 buildWeaponHud();
}

/* ---------------- QUAKE / AIRSTRIKE ---------------- */
function doQuake(){
 const a=G.active;
 G.ammo[G.teamIdx].quake--;
 Snd.explode(2.2);G.shake=44;G.freeze=.12;
 showToast("TREMOR!");
 const cx=a.x;
 // collapse terrain in a wide band: convert bottom edges into debris
 for(let i=0;i<70;i++){
  const x=cx+rnd(300,-300);
  let y=-1;
  for(let yy=Math.max(0,a.y-260);yy<MAPH;yy++)if(solidAt(x,yy)){y=yy;break;}
  if(y<0)continue;
  blast(x,y+rnd(30,0),rnd(30,14),true);
 }
 for(const w of G.worms){
  if(w.dead)continue;
  const d=Math.abs(w.x-cx);
  if(d<340){
   const f=1-d/340;
   w.vy-=f*5.5;w.vx+=rnd(3,-3)*f;w.onGround=false;
   if(w!==a){w.damage(30*f,a);if(w.team!==a.team)G.turnDamage+=30*f;}
   else w.damage(8*f,a);
  }
 }
 for(let i=0;i<90;i++)G.parts.push(new Particle(cx+rnd(340,-340),a.y+rnd(90,-40),rnd(3,-3),-rnd(4,.5),rnd(60,25),"rgba(120,110,100,.7)",rnd(9,3),{g:.05,smoke:true}));
 G.phase="retreat";G.retreat=2.6;
 buildWeaponHud();
}
function doAirstrike(tx){
 G.ammo[G.teamIdx].airstrike--;
 G.targetMode=false;
 Snd.airstrike();
 showToast("AIRSTRIKE INBOUND");
 const dir=tx>G.active.x?1:-1;
 const w=WMAP.airstrike;
 G.phase="fly";
 const startY=Math.max(-260,G.cam.y-H()/G.cam.zoom/2-160);
 for(let i=0;i<5;i++){
  setTimeout(()=>{
   if(!G)return;
   const p=new Proj(tx+(i-2)*46*dir - dir*90, startY, dir*2.6, 3.2, w, G.teamIdx,{});
   p.ownerWorm=G.active;p.ownerTeam=G.teamIdx;p.smoke=true;
   G.projs.push(p);
   if(i===0)G.camLock=p;
  },i*110);
 }
 G.planeFX={x:tx-dir*700,dir,life:2.4};
 buildWeaponHud();
}

/* ============================================================
   INPUT
   ============================================================ */
function releaseFire(){
 if(!G.charging)return;
 G.charging=false;
 const p=Math.max(0.08,G.power);
 G.power=0;
 fireWeapon(p);
}
function humanTurn(){return G&&!Cfg.cpu[G.teamIdx]&&G.phase==="aim"&&G.active&&!G.active.dead}

addEventListener("keydown",e=>{
 if(e.code==="Tab")e.preventDefault();
 if(["ArrowUp","ArrowDown","ArrowLeft","ArrowRight","Space"].includes(e.code))e.preventDefault();
 if(keys[e.code])return;
 keys[e.code]=true;
 Snd.init();
 if(e.code==="KeyM"){const m=Snd.mute();showToast(m?"SOUND OFF":"SOUND ON");return;}
 if(!G)return;
 if(e.code==="Equal"||e.code==="NumpadAdd"){G.cam.tz=clamp(G.cam.tz*1.2,.45,2.2);return;}
 if(e.code==="Minus"||e.code==="NumpadSubtract"){G.cam.tz=clamp(G.cam.tz/1.2,.45,2.2);return;}
 if(!humanTurn())return;
 const a=G.active;
 if(e.code==="Enter"||e.code==="NumpadEnter"){a.jump(0);return;}
 if(e.code==="Backspace"){a.jump(1);return;}
 if(e.code==="Space"){
  const w=currentWeapon();
  if(w.cat==="util"){useUtility(w);return;}
  if(w.quake){doQuake();return;}
  if(w.target){showToast("CLICK A TARGET ZONE");return;}
  if(!canFire())return;
  if(w.cat==="direct"){fireWeapon(1);return;}
  G.charging=true;G.power=0;return;
 }
 if(e.code==="Tab"){
  // switch worm in team (only before acting)
  if(G.timeLeft>G.turnTime-0.6||true){
   const tw=G.worms.filter(w=>w.team===G.teamIdx&&!w.dead);
   if(tw.length>1&&!G.firing&&G.turnDamage===0){
    const i=tw.indexOf(G.active);
    G.active=tw[(i+1)%tw.length];
    Snd.click();centerCam(G.active.x,G.active.y);buildTeamBar();
   }
  }
  return;
 }
 const numMap={Digit1:0,Digit2:1,Digit3:2,Digit4:3,Digit5:4,Digit6:5,Digit7:6,Digit8:7,Digit9:8,Digit0:9};
 if(e.code in numMap){const w=WEAPONS[numMap[e.code]];if(w)selectWeapon(w.id);Snd.click();return;}
 const letterMap={KeyJ:"jetpack",KeyT:"teleport",KeyG:"girder",KeyH:"medkit",KeyP:"chute"};
 if(e.code in letterMap){selectWeapon(letterMap[e.code]);Snd.click();return;}
 if(e.code==="Escape"){G.targetMode=false;G.teleMode=false;G.girderMode=false;G.weapon=validWeapon("bazooka");buildWeaponHud();}
});
addEventListener("keyup",e=>{
 keys[e.code]=false;
 if(!G)return;
 if(e.code==="Space"&&G.charging&&!Cfg.cpu[G.teamIdx])releaseFire();
});
addEventListener("blur",()=>{for(const k in keys)keys[k]=false;});

function updateMouseWorld(){
 if(!G)return;
 const [wx,wy]=s2w(mouse.x,mouse.y);mouse.wx=wx;mouse.wy=wy;
}
addEventListener("mousemove",e=>{
 mouse.x=e.clientX;mouse.y=e.clientY;updateMouseWorld();
 if(humanTurn()&&G.active&&!G.charging&&!G.targetMode&&!G.teleMode&&!G.girderMode){
  const a=G.active;
  const dx=mouse.wx-a.x,dy=mouse.wy-a.y;
  if(Math.hypot(dx,dy)>18){
   const raw=Math.atan2(dy,dx);
   const f=Math.abs(raw)>Math.PI/2?-1:1;
   a.face=f;
   a.aim=clamp(f>0?raw:(raw>0?Math.PI-raw:-Math.PI-raw),-Math.PI/2+.02,Math.PI/2-.02);
  }
 }
});
addEventListener("mousedown",e=>{
 Snd.init();
 if(!G)return;
 if(e.button===2)return;
 updateMouseWorld();
 if(!humanTurn())return;
 if(G.teleMode){doTeleport(mouse.wx,mouse.wy);return;}
 if(G.girderMode){placeGirder(mouse.wx,mouse.wy);return;}
 if(G.targetMode){doAirstrike(mouse.wx);return;}
 if(e.target!==cv)return;
 const w=currentWeapon();
 if(w.cat==="util"){useUtility(w);return;}
 if(w.quake){doQuake();return;}
 if(!canFire())return;
 if(w.cat==="direct"){fireWeapon(1);return;}
 G.charging=true;G.power=0;
});
addEventListener("mouseup",e=>{
 if(!G)return;
 if(G.charging&&!Cfg.cpu[G.teamIdx])releaseFire();
});
addEventListener("contextmenu",e=>e.preventDefault());
addEventListener("wheel",e=>{
 if(!G)return;
 if(G.girderMode){G.girderAng+=Math.sign(e.deltaY)*Math.PI/12;Snd.rope();return;}
 G.cam.tz=clamp(G.cam.tz*(e.deltaY>0?.9:1.11),.45,2.0);
},{passive:true});

// Touch (basic)
addEventListener("touchstart",e=>{Snd.init();if(!G)return;const t=e.touches[0];mouse.x=t.clientX;mouse.y=t.clientY;updateMouseWorld();},{passive:true});

/* ============================================================
   MENU
   ============================================================ */
function buildMenuTeams(){
 const el=document.getElementById("tsetup");
 el.innerHTML="";
 for(let i=0;i<Cfg.teams;i++){
  const d=document.createElement("div");d.className="tbox";
  d.innerHTML=`<div class="tn" style="color:${TEAMS[i].col}">${TEAMS[i].name}</div>
   <div class="tog" data-i="${i}" style="border-color:${Cfg.cpu[i]?"#ffd166":"#7affc8"};color:${Cfg.cpu[i]?"#ffd166":"#7affc8"}">${Cfg.cpu[i]?"CPU":"HUMAN"}</div>`;
  el.appendChild(d);
 }
 el.querySelectorAll(".tog").forEach(t=>{
  t.onmouseenter=()=>Snd.hover();
  t.onclick=()=>{const i=+t.dataset.i;Cfg.cpu[i]=!Cfg.cpu[i];Snd.click();buildMenuTeams();};
 });
}
document.querySelectorAll("#menu .btn[data-map],#menu .btn[data-teams],#menu .btn[data-time]").forEach(b=>{
 b.onmouseenter=()=>{Snd.init();Snd.hover()};
 b.onclick=()=>{
  Snd.click();
  const grp=b.dataset.map?"map":b.dataset.teams?"teams":"time";
  b.parentElement.querySelectorAll(".btn").forEach(x=>x.classList.remove("on"));
  b.classList.add("on");
  if(grp==="map")Cfg.map=b.dataset.map;
  if(grp==="teams"){Cfg.teams=+b.dataset.teams;buildMenuTeams();}
  if(grp==="time")Cfg.time=+b.dataset.time;
 };
});
document.getElementById("play").onmouseenter=()=>{Snd.init();Snd.hover()};
document.getElementById("play").onclick=()=>{
 Snd.init();Snd.click();
 const f=document.getElementById("fade");f.style.opacity=1;
 setTimeout(()=>{
  document.getElementById("menu").style.display="none";
  newGame();
  f.style.opacity=0;
 },420);
};

/* ============================================================
   BOOT
   ============================================================ */
function resize(){
 cv.width=Math.floor(innerWidth*Math.min(devicePixelRatio||1,2));
 cv.height=Math.floor(innerHeight*Math.min(devicePixelRatio||1,2));
 cv.style.width=innerWidth+"px";cv.style.height=innerHeight+"px";
 const s=Math.min(devicePixelRatio||1,2);
 ctx2d.setTransform(s,0,0,s,0,0);
 dpr=s;skyGrad=null;
}
let dpr=1;
// Patch W/H to CSS pixels (canvas transform handles dpr)
function initCanvas(){
 cv=document.getElementById("game");
 ctx2d=cv.getContext("2d",{alpha:false});
 resize();
 addEventListener("resize",resize);
}
// override render transform to include dpr
const _render=render;
render=function(){
 const s=dpr;
 ctx2d.setTransform(s,0,0,s,0,0);
 _render();
};
initCanvas();
// idle attract state so loop is safe before newGame
G={worms:[],projs:[],parts:[],debris:[],floats:[],flashes:[],ripples:[],girders:[],crates:[],
   cam:{x:0,y:0,zoom:1,tx:0,ty:0,tz:1},wind:0,shake:0,freeze:0,phase:"menu",teamIdx:0,timeLeft:0,turnTime:45,
   clouds:[],stars:[],ammo:[{}],active:null,msgT:0,power:0,charging:false};
buildMenuTeams();
requestAnimationFrame(loop);
</script>
</body>
</html>
<!-- agent-meta {"model":"anthropic/claude-opus-5","provider":"openrouter","persona":"full","sessionId":"92bdb82c-3674-4b6e-ac9a-fee95207ec15","tokensIn":3103539,"tokensOut":68534,"tokensTotal":3172073,"turns":32,"toolCalls":31,"failedToolCalls":0,"timestamp":"2026-07-25T18:48:23.605Z"} -->