← Worms results

Worms

GLM 5.3 Flash · full

Initial view of Worms

Reviews & generation details

Reviews

No human review yet. Unreviewed does not mean unsuccessful.

Generation context

Imported historical artifact. Its exact original instructions and lineage are not known.

Generation prompt · exact text unavailable

See the current prompt for reference. It may differ from the instructions used to generate this artifact.

Inspect original source 49,052 bytes · SHA-256 7bb02b066791
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>BOOM BUDDIES — Artillery Mayhem</title>
<style>
:root{--ui:#f4ead5;--panel:rgba(16,14,26,.82)}
*{margin:0;padding:0;box-sizing:border-box;user-select:none;-webkit-user-select:none}
html,body{width:100%;height:100%;overflow:hidden;background:#0b0918;font-family:"Trebuchet MS","Segoe UI",Verdana,sans-serif;color:var(--ui)}
#wrap{position:relative;width:100vw;height:100vh;display:flex;align-items:center;justify-content:center}
#game{background:#000;image-rendering:auto;box-shadow:0 0 80px rgba(0,0,0,.8);cursor:crosshair}
/* ---------- HUD ---------- */
.hud{position:absolute;pointer-events:none;z-index:5}
#topbar{top:0;left:0;right:0;display:flex;align-items:flex-start;justify-content:space-between;padding:10px 14px;gap:10px}
#teamChips{display:flex;gap:8px;flex-wrap:wrap}
.chip{pointer-events:none;background:var(--panel);border:1px solid rgba(255,255,255,.12);border-radius:10px;padding:6px 10px;min-width:130px;backdrop-filter:blur(6px);transition:.25s;opacity:.65}
.chip.active{opacity:1;border-color:#ffd76a;box-shadow:0 0 18px rgba(255,215,106,.35);transform:translateY(2px)}
.chip.dead{opacity:.25;filter:grayscale(1)}
.chip .nm{font-size:12px;font-weight:bold;letter-spacing:1px;display:flex;justify-content:space-between;gap:8px}
.chip .nm .cnt{font-size:10px;opacity:.8}
.chip .bar{height:6px;background:rgba(255,255,255,.12);border-radius:3px;margin-top:4px;overflow:hidden}
.chip .bar i{display:block;height:100%;border-radius:3px;transition:width .4s}
#centerHud{display:flex;flex-direction:column;align-items:center;gap:6px}
#windBox{background:var(--panel);border:1px solid rgba(255,255,255,.12);border-radius:20px;padding:4px 14px;font-size:11px;letter-spacing:2px;display:flex;align-items:center;gap:8px;backdrop-filter:blur(6px)}
#windArrow{display:inline-block;transition:transform .5s;font-size:14px}
#timerWrap{width:220px;height:26px;background:var(--panel);border:1px solid rgba(255,255,255,.15);border-radius:13px;overflow:hidden;position:relative;backdrop-filter:blur(6px)}
#timerBar{height:100%;width:100%;background:linear-gradient(90deg,#37d67a,#ffd76a);transition:width .2s linear}
#timerWrap.urgent #timerBar{background:linear-gradient(90deg,#ff5b5b,#ff9d3c)}
#timerTxt{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-weight:bold;font-size:14px;text-shadow:0 1px 3px #000}
#timerWrap.urgent{animation:pulse .5s infinite alternate}
@keyframes pulse{from{transform:scale(1)}to{transform:scale(1.12)}}
#turnLabel{font-size:15px;font-weight:bold;letter-spacing:3px;text-shadow:0 2px 6px #000;padding:2px 14px;background:var(--panel);border-radius:14px;border:1px solid rgba(255,255,255,.12)}
/* weapon tray */
#tray{bottom:12px;left:50%;transform:translateX(-50%);display:flex;gap:6px;pointer-events:auto;background:var(--panel);padding:8px;border-radius:14px;border:1px solid rgba(255,255,255,.14);backdrop-filter:blur(8px)}
.wpn{width:64px;height:64px;border-radius:10px;border:1px solid rgba(255,255,255,.1);background:rgba(255,255,255,.05);color:var(--ui);display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;cursor:pointer;transition:.15s;position:relative}
.wpn:hover:not(.dis){background:rgba(255,255,255,.16);transform:translateY(-3px)}
.wpn.sel{border-color:#ffd76a;box-shadow:0 0 14px rgba(255,215,106,.5);background:rgba(255,215,106,.12)}
.wpn.dis{opacity:.28;cursor:default}
.wpn .ic{font-size:22px;line-height:1}
.wpn .nm2{font-size:8px;letter-spacing:.5px;text-transform:uppercase;opacity:.85}
.wpn .am{font-size:9px;position:absolute;top:3px;right:5px;opacity:.9}
.wpn .kk{font-size:8px;position:absolute;top:3px;left:5px;opacity:.5}
.wcat{writing-mode:vertical-rl;font-size:8px;letter-spacing:2px;opacity:.5;align-self:center;margin:0 2px;text-transform:uppercase}
/* banners */
#msgBanner{top:84px;left:50%;transform:translateX(-50%);font-size:26px;font-weight:bold;letter-spacing:4px;text-shadow:0 3px 10px #000;opacity:0;transition:opacity .4s}
/* overlays */
.overlay{position:absolute;inset:0;z-index:20;display:flex;align-items:center;justify-content:center;background:rgba(8,6,18,.78);backdrop-filter:blur(6px)}
.panel{background:linear-gradient(160deg,#221d3a,#171329);border:1px solid rgba(255,255,255,.14);border-radius:18px;padding:34px 44px;text-align:center;box-shadow:0 30px 80px rgba(0,0,0,.6);max-width:640px}
h1.logo{font-size:46px;letter-spacing:6px;background:linear-gradient(90deg,#ffd76a,#ff7b5b,#6ae0ff);-webkit-background-clip:text;background-clip:text;color:transparent;margin-bottom:2px;text-shadow:0 0 40px rgba(255,180,90,.25)}
.sub{font-size:12px;letter-spacing:4px;opacity:.6;margin-bottom:22px}
.row{display:flex;gap:10px;justify-content:center;margin:10px 0;flex-wrap:wrap}
.opt{padding:9px 16px;border-radius:10px;border:1px solid rgba(255,255,255,.15);background:rgba(255,255,255,.06);color:var(--ui);font-size:13px;letter-spacing:1px;cursor:pointer;transition:.15s;font-family:inherit}
.opt:hover{background:rgba(255,255,255,.16);transform:translateY(-2px)}
.opt.on{border-color:#ffd76a;background:rgba(255,215,106,.15);box-shadow:0 0 12px rgba(255,215,106,.3)}
.teamRow{display:flex;align-items:center;gap:10px;background:rgba(255,255,255,.05);border-radius:10px;padding:8px 12px;margin:6px 0}
.teamRow .dot{width:16px;height:16px;border-radius:50%}
.teamRow input[type=text]{background:rgba(0,0,0,.3);border:1px solid rgba(255,255,255,.15);border-radius:8px;color:var(--ui);padding:6px 10px;font-family:inherit;width:140px;font-size:13px}
.teamRow label{font-size:12px;display:flex;align-items:center;gap:5px;cursor:pointer}
.bigbtn{margin-top:18px;padding:13px 46px;font-size:17px;letter-spacing:3px;border-radius:12px;border:none;background:linear-gradient(90deg,#ff7b5b,#ff4d6d);color:#fff;font-weight:bold;cursor:pointer;box-shadow:0 8px 24px rgba(255,77,109,.4);transition:.15s;font-family:inherit}
.bigbtn:hover{transform:translateY(-2px) scale(1.03);box-shadow:0 12px 30px rgba(255,77,109,.55)}
#handoffPanel h2{font-size:30px;letter-spacing:4px;margin-bottom:8px}
#handoffPanel p{opacity:.7;font-size:13px;margin-bottom:18px}
#hint{bottom:100px;left:50%;transform:translateX(-50%);font-size:11px;letter-spacing:1px;opacity:.55;background:rgba(0,0,0,.4);padding:4px 12px;border-radius:10px;white-space:nowrap}
.hidden{display:none!important}
</style>
</head>
<body>
<div id="wrap">
  <canvas id="game" width="1280" height="720"></canvas>

  <!-- HUD -->
  <div id="topbar" class="hud">
    <div id="teamChips"></div>
    <div id="centerHud">
      <div id="timerWrap"><div id="timerBar"></div><div id="timerTxt">45</div></div>
      <div id="windBox">WIND <span id="windArrow">←</span><span id="windVal">0.0</span></div>
    </div>
    <div style="width:290px"></div>
  </div>
  <div id="turnLabel" class="hud" style="top:96px;left:14px;">—</div>
  <div id="msgBanner" class="hud"></div>
  <div id="hint" class="hud hidden">← → move · ↑ ↓ aim · SPACE hold=charge · ENTER jump · 1-8 weapons · mouse aim works too</div>
  <div id="tray" class="hud hidden"></div>

  <!-- SETUP -->
  <div id="setup" class="overlay">
    <div class="panel">
      <h1 class="logo">BOOM BUDDIES</h1>
      <div class="sub">TURN-BASED ARTILLERY MAYHEM</div>
      <div style="font-size:11px;letter-spacing:2px;opacity:.6;margin-bottom:6px">MAP TYPE</div>
      <div class="row" id="mapOpts"></div>
      <div style="font-size:11px;letter-spacing:2px;opacity:.6;margin:14px 0 4px">TEAMS</div>
      <div class="row" id="cntOpts"></div>
      <div id="teamList" style="margin-top:10px"></div>
      <button class="bigbtn" id="startBtn">▶ START BATTLE</button>
    </div>
  </div>

  <!-- HANDOFF -->
  <div id="handoff" class="overlay hidden">
    <div class="panel" id="handoffPanel">
      <h2 id="hoName">TEAM</h2>
      <p>Pass the device. When everyone's ready…</p>
      <button class="opt" id="hoBtn" style="font-size:16px;padding:12px 34px">READY!</button>
    </div>
  </div>

  <!-- GAME OVER -->
  <div id="gameover" class="overlay hidden">
    <div class="panel">
      <h1 class="logo" id="goTitle">VICTORY!</h1>
      <div class="sub" id="goSub"></div>
      <button class="bigbtn" id="againBtn">↺ REMATCH</button>
    </div>
  </div>
</div>

<script>
"use strict";
/* ============================== CORE ============================== */
const W=1280,H=720,WATER=H-48,GRAV=0.24,TURN_TIME=45;
const cv=document.getElementById('game'),cx=cv.getContext('2d');
const TC=document.createElement('canvas');TC.width=W;TC.height=H;const tx=TC.getContext('2d');
let solid=new Uint8Array(W*H);
const S=(x,y)=>{x|=0;y|=0;if(x<0||x>=W)return 1;if(y<0||y>=H)return 0;return solid[y*W+x];};
const rnd=(a,b)=>a+Math.random()*(b-a),rndi=(a,b)=>Math.floor(rnd(a,b+1)),clamp=(v,a,b)=>v<a?a:v>b?b:v,lerp=(a,b,t)=>a+(b-a)*t,dist=(a,b,c,d)=>Math.hypot(a-c,b-d);
const SI=x=>((x*2654435761)>>>0)/4294967295;

/* ============================== AUDIO ============================== */
let AC=null,master=null,noiseBuf=null;
function audioInit(){if(AC)return;try{AC=new (window.AudioContext||window.webkitAudioContext)();master=AC.createGain();master.gain.value=.5;master.connect(AC.destination);
noiseBuf=AC.createBuffer(1,AC.sampleRate,AC.sampleRate);const d=noiseBuf.getChannelData(0);for(let i=0;i<d.length;i++)d[i]=Math.random()*2-1;}catch(e){}}
function tone(f,dur,type='square',g=.15,slide=null,delay=0){if(!AC)return;const t=AC.currentTime+delay,o=AC.createOscillator(),gn=AC.createGain();
o.type=type;o.frequency.setValueAtTime(f,t);if(slide)o.frequency.exponentialRampToValueAtTime(Math.max(20,slide),t+dur);
gn.gain.setValueAtTime(g,t);gn.gain.exponentialRampToValueAtTime(.001,t+dur);o.connect(gn);gn.connect(master);o.start(t);o.stop(t+dur+.02);}
function noise(dur,fg=1200,g=.25,slideTo=null,delay=0,q=1){if(!AC)return;const t=AC.currentTime+delay,s=AC.createBufferSource();s.buffer=noiseBuf;s.loop=true;
const f=AC.createBiquadFilter();f.type='lowpass';f.frequency.setValueAtTime(fg,t);f.Q.value=q;
if(slideTo)f.frequency.exponentialRampToValueAtTime(Math.max(30,slideTo),t+dur);
const gn=AC.createGain();gn.gain.setValueAtTime(g,t);gn.gain.exponentialRampToValueAtTime(.001,t+dur);
s.connect(f);f.connect(gn);gn.connect(master);s.start(t);s.stop(t+dur+.02);}
const sfx={
 hover:()=>tone(700,.04,'square',.05),
 click:()=>{tone(520,.06,'square',.09);tone(780,.07,'square',.07,null,.05);},
 jump:()=>tone(280,.14,'sine',.14,620),
 step:()=>noise(.03,900,.03),
 shoot:()=>{noise(.18,2500,.3,300);tone(340,.2,'square',.12,90);},
 shotgun:()=>{noise(.14,3800,.4,500);tone(160,.12,'sawtooth',.15,60);},
 boom:s=>{noise(.5+s*.004,900+s*8,.5,60,0,.6);tone(90,.45+s*.004,'sine',.4,32);},
 bounce:()=>tone(420,.06,'triangle',.12,260),
 hurt:()=>{tone(300,.16,'sawtooth',.14,120);},
 splash:()=>{noise(.5,700,.3,120,0,2);tone(500,.3,'sine',.08,140);},
 tick:u=>tone(u?1150:850,.045,'square',u?.11:.07),
 sel:()=>tone(880,.05,'triangle',.09,1100),
 tp:()=>{[600,800,1050,1400].forEach((f,i)=>tone(f,.12,'sine',.1,null,i*.05));},
 girder:()=>{noise(.12,3000,.15);tone(200,.1,'square',.1,320);},
 charge:p=>{},// handled by chargeOsc
 win:()=>{[523,659,784,1046].forEach((f,i)=>tone(f,.3,'triangle',.15,null,i*.14));}
};
let chargeOsc=null;
function chargeStart(){if(!AC||chargeOsc)return;const o=AC.createOscillator(),g=AC.createGain();o.type='sawtooth';o.frequency.value=120;g.gain.value=.05;o.connect(g);g.connect(master);o.start();chargeOsc={o,g};}
function chargeUp(p){if(chargeOsc)chargeOsc.o.frequency.setTargetAtTime(120+p*480,AC.currentTime,.02);}
function chargeStop(){if(chargeOsc){try{chargeOsc.g.gain.setTargetAtTime(0,AC.currentTime,.02);chargeOsc.o.stop(AC.currentTime+.1);}catch(e){}chargeOsc=null;}}

/* ============================== INPUT ============================== */
const keys={},mouse={x:W/2,y:H/2,down:false};
addEventListener('keydown',e=>{keys[e.code]=true;
 if(G.phase==='aim'&&G.cur&&!G.teams[G.cur.ti].cpu){
   const num=e.code.match(/^Digit([1-8])$/);if(num)selectWeaponIdx(+num[1]-1);
   if(e.code==='Enter')tryJump();
 }
 if(['Space','ArrowUp','ArrowDown','ArrowLeft','ArrowRight'].includes(e.code))e.preventDefault();});
addEventListener('keyup',e=>{keys[e.code]=false;});
cv.addEventListener('mousemove',e=>{const r=cv.getBoundingClientRect();mouse.x=(e.clientX-r.left)*(W/r.width);mouse.y=(e.clientY-r.top)*(H/r.height);});
cv.addEventListener('mousedown',e=>{audioInit();mouse.down=true;onMouseDown();});
addEventListener('mouseup',()=>{mouse.down=false;onMouseUp();});
document.addEventListener('mouseover',e=>{if(e.target.classList&&(e.target.classList.contains('opt')||e.target.classList.contains('wpn')))sfx.hover();});
document.addEventListener('click',e=>{audioInit();if(e.target.classList&&e.target.classList.contains('opt'))sfx.click();});

/* ============================== STATE ============================== */
const TEAM_DEFS=[{name:'RED ROJOS',col:'#ff5252',dark:'#a02020'},{name:'BLUE BRUISERS',col:'#4da6ff',dark:'#1c5aa0'},{name:'GREEN GOBLINS',col:'#5fd35f',dark:'#22882a'},{name:'GOLD GRUBS',col:'#ffc93c',dark:'#a07a10'}];
const MAPS={cave:'CAVE SYSTEM',island:'SINGLE ISLAND',twin:'TWIN ISLANDS'};
const G={phase:'setup',teams:[],cur:null,ti:0,projectiles:[],particles:[],texts:[],chunks:[],
 cam:{x:W/2,y:H/2,z:1,tx:W/2,ty:H/2,tz:1},shake:0,freeze:0,wind:0,windTarget:0,
 timer:TURN_TIME,mapType:'island',time:0,settle:0,msgT:0,lastTick:0,slowmo:0};

/* ============================== TERRAIN GEN ============================== */
let terrSeed=1;
function hnoise(x,scale,seed){return Math.sin(x*scale+seed)*.5+Math.sin(x*scale*2.3+seed*1.7)*.3+Math.sin(x*scale*4.7+seed*3.1)*.2;}
function setPx(x,y,v){if(x>=0&&x<W&&y>=0&&y<H)solid[y*W+x]=v;}
function blob(cx0,cy0,rx,ry){for(let y=Math.max(0,cy0-ry)|0;y<Math.min(H,cy0+ry);y++)for(let x=Math.max(0,cx0-rx)|0;x<Math.min(W,cx0+rx);x++){
 const dx=(x-cx0)/rx,dy=(y-cy0)/ry,n=hnoise(x*.05+y*.03,1,terrSeed)*.18;
 if(dx*dx+dy*dy<1+n)setPx(x,y,1);}}
function carve(cx0,cy0,r){for(let y=Math.max(0,cy0-r)|0;y<Math.min(H,cy0+r);y++)for(let x=Math.max(0,cx0-r)|0;x<Math.min(W,cx0+r);x++){
 if(dist(x,y,cx0,cy0)<r+hnoise(x*.08-y*.06,1,terrSeed+9)*r*.25)setPx(x,y,0);}}
function genTerrain(type){
 solid.fill(0);tx.clearRect(0,0,W,H);terrSeed=rnd(1,999);
 if(type==='island'||type==='twin'){
   const centers=type==='twin'?[W*.27,W*.73]:[W*.5];
   for(const c of centers){
     const wid=type==='twin'?W*.34:W*.62;
     for(let x=0;x<W;x++){const d=Math.abs(x-c)/(wid/2);if(d>1)continue;
       const gy=H*.52+hnoise(x*.012,1,terrSeed)*90*type==='twin'?0:0;// noop guard
     }
     for(let x=0;x<W;x++){
       const d=Math.abs(x-c)/(wid/2);if(d>1)continue;
       const surf=H*.5+(1-d*d)*(-150)+hnoise(x*.02,1,terrSeed)*46+hnoise(x*.07,1,terrSeed+3)*14;
       for(let y=Math.max(0,surf|0);y<H;y++)setPx(x,y,1);
     }
   }
 }else if(type==='cave'){
   solid.fill(1);
   for(let x=0;x<W;x++)for(let y=0;y<52;y++)setPx(x,y,0);
   for(let i=0;i<9;i++)carve(rnd(80,W-80),rnd(120,H-190),rnd(55,120));
   let px=rnd(100,W-100),py=rnd(150,H-220);
   for(let i=0;i<450;i++){carve(px,py,16);px+=rnd(-14,14);py+=rnd(-9,9);px=clamp(px,40,W-40);py=clamp(py,90,H-170);}
   for(let x=0;x<W;x++)for(let y=H-46;y<H;y++)setPx(x,y,1);
 }
 /* paint texture */
 const img=tx.createImageData(W,H),d=img.data;
 const P=type==='cave'
  ?{top:['#49c5b1','#3fb39f','#57d1bc'],mid:['#5c5670','#544e66','#66607c'],deep:['#3e3a50','#46415a']}
  :{top:['#5bbf43','#6fd155','#54ad3e'],mid:['#8a5a33','#7c5030','#96653c'],deep:['#6e675f','#5f5850','#7a736a']};
 const hex=h=>[parseInt(h.slice(1,3),16),parseInt(h.slice(3,5),16),parseInt(h.slice(5,7),16)];
 const PT=P.top.map(hex),PM=P.mid.map(hex),PD=P.deep.map(hex);
 for(let y=0;y<H;y++)for(let x=0;x<W;x++){
   const i=y*W+x;if(!solid[i])continue;
   let c;
   const above=y>0?solid[i-W]:0;
   if(!above){c=PT[(SI(x*31+y*7)*3)|0];}
   else{
     let depth=0;for(let k=y-1;k>=0&&solid[k*W+x];k--)depth++;
     const pal=depth>170?PD:(depth>26?PM:PT);
     c=pal[(SI(x*13+y*57)*pal.length)|0];
   }
   const j=1+(SI(x*7+y*91)-.5)*.22;
   const o=i*4;d[o]=c[0]*j;d[o+1]=c[1]*j;d[o+2]=c[2]*j;d[o+3]=255;
 }
 tx.putImageData(img,0,0);
 /* subtle rim */
 tx.globalCompositeOperation='source-atop';
 tx.fillStyle='rgba(255,255,255,.06)';
 for(let y=0;y<H;y+=2){/*noop*/}
 tx.globalCompositeOperation='source-over';
}
function destruct(x,y,r){
 const x0=Math.max(0,x-r)|0,x1=Math.min(W-1,x+r)|0,y0=Math.max(0,y-r)|0,y1=Math.min(H-1,y+r)|0;
 for(let yy=y0;yy<=y1;yy++)for(let xx=x0;xx<=x1;xx++){
   const d=dist(xx,yy,x,y);
   if(d<r+hnoise(xx*.15,1,99)*3){const i=yy*W+xx;if(solid[i]){solid[i]=0;}}
 }
 tx.clearRect(x0,y0,x1-x0+1,y1-y0+1);
 tx.save();tx.beginPath();tx.arc(x,y,r+hnoise(x*.15,1,99)*3,0,7);tx.globalCompositeOperation='destination-out';tx.fill();tx.restore();
}
function addSolidLine(x1,y1,x2,y2,w,col){ /* girder */
 const n=Math.ceil(dist(x1,y1,x2,y2));
 for(let i=0;i<=n;i++){const x=lerp(x1,x2,i/n),y=lerp(y1,y2,i/n);
  for(let dy=-w/2|0;dy<=w/2;dy++)for(let dx=-w/2|0;dx<=w/2;dx++){
    const px=(x+dx)|0,py=(y+dy)|0;if(px>=0&&px<W&&py>=0&&py<H){solid[py*W+px]=1;}}
 }
 tx.strokeStyle=col;tx.lineWidth=w;tx.lineCap='round';
 tx.beginPath();tx.moveTo(x1,y1);tx.lineTo(x2,y2);tx.stroke();
 tx.strokeStyle='rgba(255,255,255,.25)';tx.lineWidth=2;
 tx.beginPath();tx.moveTo(x1,y1-w/2+2);tx.lineTo(x2,y2-w/2+2);tx.stroke();
}
function findSpawns(n){
 const pts=[];
 if(G.mapType==='cave'){
   let tries=0;
   while(pts.length<n&&tries++<4000){
     const x=rndi(40,W-40),y=rndi(70,H-120);
     if(!S(x,y)&&!S(x,y-10)&&S(x,y+14)&&S(x,y+18))pts.push({x,y:y-10});
   }
 }else{
   for(let x=50;x<W-50&&pts.length<n*3;x+=rndi(30,60)){
     let y=0;while(y<H&&!S(x,y))y++;
     if(y<H-60)pts.push({x,y:y-10});
   }
 }
 pts.sort(()=>Math.random()-.5);
 const out=[];
 for(const p of pts){if(out.every(q=>dist(q.x,q.y,p.x,p.y)>46)){out.push(p);if(out.length===n*4)break;}}
 return out;
}


/* ============================== UNITS ============================== */
const UR=7,WALK=1.35,FALL_SAFE=6.2,FALL_MUL=7;
function makeUnit(x,y,team){return{x,y,vx:0,vy:0,hp:100,alive:true,face:Math.random()<.5?-1:1,ang:35,grounded:false,sub:false,walkT:0,blink:0,name:''};}
function unitAt(x,y,skip){for(const t of G.teams)for(const u of t.units)if(u!==skip&&u.alive&&dist(x,y,u.x,u.y)<UR+3)return u;return null;}
function groundY(x,fromY){let y=Math.max(0,fromY|0);while(y<H&&!S(x,y))y++;return y;}
function updateUnit(u,ti){
 if(!u.alive)return;
 u.vy+=GRAV;
 if(u.sub){u.vy-=.42;u.vx*=.92;u.vy*=.9;u.hp-=.09;if(Math.random()<.3)spawnP(u.x+rnd(-6,6),WATER,'bubble',1);}
 else u.vx+=G.wind*.012;
 u.vx=clamp(u.vx,-14,14);u.vy=clamp(u.vy,-16,18);
 /* vertical */
 let ny=u.y+u.vy;
 if(u.vy>0){
   let steps=Math.ceil(u.vy)+1,hit=false;
   for(let s=1;s<=steps;s++){const yy=u.y+u.vy*s/steps;if(S(u.x-UR+2,yy+UR)||S(u.x+UR-2,yy+UR)||S(u.x,yy+UR)){ny=yy-0.01;hit=true;break;}}
   if(hit){
     if(u.vy>FALL_SAFE&&!u.sub){const dmg=Math.round((u.vy-FALL_SAFE)*FALL_MUL);if(dmg>0){damage(u,dmg,'fall');spawnDust(u.x,ny+UR,8);}}
     u.vy=0;u.grounded=true;u.vx*=.55;
   }else u.grounded=false;
 }else if(u.vy<0){
   if(S(u.x-UR+2,ny-UR)||S(u.x+UR-2,ny-UR)){u.vy=0;ny=u.y;}else u.grounded=false;
 }
 u.y=ny;
 /* horizontal */
 let nx=u.x+u.vx;
 if(u.vx!==0){
   const dir=Math.sign(u.vx);
   if(S(nx+dir*UR,u.y-2)||S(nx+dir*UR,u.y-UR+2)){u.vx=0;nx=u.x;}
 }
 u.x=clamp(nx,UR+2,W-UR-2);
 /* water */
 const wasSub=u.sub;u.sub=u.y>WATER-2;
 if(u.sub&&!wasSub){sfx.splash();spawnSplash(u.x,WATER);if(ti!==undefined)showMsg('SPLASH!');}
 if(u.sub&&u.hp>0&&u.hp<=.1)u.hp=0;
 if(u.hp<=0&&u.alive)killUnit(u,ti);
}
function walk(u,dir){
 if(!u.grounded)return;
 const step=WALK;
 for(let i=1;i<=3;i++){
   const nx=u.x+dir*step;
   if(nx<UR+2||nx>W-UR-2)return;
   let dy=0,found=false;
   for(let d=0;d<=9;d--){if(!S(nx,u.y+UR-1+d)){dy=d;found=true;break;}}
   if(!found){for(let d=1;d<=8;d++){if(S(nx,u.y+UR-1-d))continue;dy=-d;found=true;break;}}
   if(!found)return;
   u.x=nx;u.y+=dy;u.face=dir;u.walkT+=.3;
   if(Math.random()<.1)spawnDust(u.x,u.y+UR,1);
   return;
 }
}
function tryJump(){
 const u=G.cur&&G.cur.u;if(!u||!u.alive||!u.grounded||G.phase!=='aim')return;
 u.vy=-5.4;u.vx=u.face*2.4;u.grounded=false;sfx.jump();spawnDust(u.x,u.y+UR,5);
}
function damage(u,dmg,kind,attackerTi){
 if(!u.alive)return;
 u.hp=Math.max(0,u.hp-dmg);
 addText(u.x,u.y-16,'-'+dmg,kind==='fall'?'#ffd76a':'#ff6b6b');
 sfx.hurt();
 if(u.hp<=0)killUnit(u,undefined,attackerTi);
}
function killUnit(u,ti,attackerTi){
 if(!u.alive)return;u.alive=false;u.hp=0;
 boom(u.x,u.y,22,0,{quietDmg:true});
 addText(u.x,u.y-24,'K.O.!','#ffd76a');
 sfx.hurt();
}
/* ============================== PARTICLES / TEXT ============================== */
function spawnP(x,y,type,sz){
 const P={x,y,type,t:0};
 switch(type){
  case'fire':P.vx=rnd(-2.5,2.5);P.vy=rnd(-3.5,.5);P.life=rnd(18,36);P.size=rnd(3,7)*sz;P.col=['#ffdd55','#ff9933','#ff5522'][rndi(0,2)];P.grav=-.02;break;
  case'smoke':P.vx=rnd(-1,1)+G.wind*.6;P.vy=rnd(-1.6,-.4);P.life=rnd(40,80);P.size=rnd(5,11)*sz;P.col='rgba(120,110,120,';P.grav=-.015;break;
  case'spark':P.vx=rnd(-6,6);P.vy=rnd(-7,2);P.life=rnd(14,30);P.size=rnd(1.5,3);P.col='#ffe08a';P.grav=.18;break;
  case'dust':P.vx=rnd(-1.4,1.4);P.vy=rnd(-1.6,-.2);P.life=rnd(16,30);P.size=rnd(2,4)*sz;P.col='rgba(180,160,130,';P.grav=.03;break;
  case'bubble':P.vx=rnd(-.4,.4);P.vy=rnd(-1.6,-.7);P.life=rnd(20,45);P.size=rnd(1.5,3.5);P.col='rgba(200,230,255,';P.grav=0;break;
  case'splash':P.vx=rnd(-3,3);P.vy=rnd(-6,-1);P.life=rnd(20,40);P.size=rnd(2,4);P.col='#7ec8ff';P.grav=.22;break;
  case'tp':P.vx=rnd(-2,2);P.vy=rnd(-2,2);P.life=rnd(20,40);P.size=rnd(2,5);P.col='#b06aff';P.grav=0;break;
 }
 P.max=P.life;G.particles.push(P);
}
function spawnDust(x,y,n){for(let i=0;i<n;i++)spawnP(x+rnd(-6,6),y,'dust',1);}
function spawnSplash(x,y){for(let i=0;i<18;i++)spawnP(x+rnd(-10,10),y,'splash',1);}
function spawnBoomFx(x,y,r){
 for(let i=0;i<r*.6;i++)spawnP(x,y,'fire',r/30);
 for(let i=0;i<r*.35;i++)spawnP(x,y,'smoke',r/28);
 for(let i=0;i<r*.4;i++)spawnP(x,y,'spark',1);
}
function spawnChunks(x,y,r,n){
 for(let i=0;i<n;i++){
  const a=rnd(0,Math.PI*2),d=rnd(0,r*.7);
  G.chunks.push({x:x+Math.cos(a)*d,y:y+Math.sin(a)*d,vx:Math.cos(a)*rnd(1,4)+G.wind*2,vy:Math.sin(a)*rnd(1,4)-rnd(2,5),
   size:rnd(2,5),rot:rnd(0,7),vr:rnd(-.3,.3),rest:0,
   col:['#8a5a33','#6e675f','#7c5030','#5c5670'][rndi(0,3)]});
 }
}
function addText(x,y,txt,col){G.texts.push({x,y,txt,col,t:0});}
function updateParticles(){
 for(let i=G.particles.length-1;i>=0;i--){const p=G.particles[i];
  p.t++;p.x+=p.vx;p.y+=p.vy;p.vy+=p.grav;
  if(p.type==='spark'&&S(p.x,p.y)){p.vy*=-.4;p.vx*=.5;p.y-=1;}
  if(p.t>=p.life)G.particles.splice(i,1);}
 for(let i=G.chunks.length-1;i>=0;i--){const c=G.chunks[i];
  if(c.rest>0){c.rest--;if(c.rest===0)G.chunks.splice(i,1);continue;}
  c.vy+=GRAV;c.x+=c.vx;c.y+=c.vy;c.rot+=c.vr;
  if(S(c.x,c.y+c.size/2)){
    if(Math.abs(c.vy)>2.2){c.vy*=-.35;c.vx*=.5;c.y-=1;}
    else{ /* stamp into terrain */
      const s=c.size|0;for(let dy=0;dy<s;dy++)for(let dx=0;dx<s;dx++){const px=(c.x-s/2+dx)|0,py=(c.y-s/2+dy)|0;if(px>=0&&px<W&&py>=0&&py<H&&!solid[py*W+px])solid[py*W+px]=1;}
      tx.fillStyle=c.col;tx.fillRect(c.x-s/2,c.y-s/2,s,s);
      c.rest=20;
    }
  }
  if(c.y>H+20)G.chunks.splice(i,1);}
 for(let i=G.texts.length-1;i>=0;i--){const t=G.texts[i];t.t++;t.y-=.55;if(t.t>70)G.texts.splice(i,1);}
}
/* ============================== EXPLOSIONS ============================== */
function boom(x,y,r,dmg,opt={}){
 destruct(x,y,r);
 spawnBoomFx(x,y,r);
 spawnChunks(x,y,r,Math.min(26,r*.45)|0);
 G.shake=Math.min(24,G.shake+r*.28);
 G.freeze=Math.max(G.freeze,r>60?14:7);
 G.cam.tz=Math.max(G.cam.tz,1.25);
 if(!opt.quietDmg&&dmg>0){
   for(let ti=0;ti<G.teams.length;ti++)for(const u of G.teams[ti].units){
     if(!u.alive)continue;
     const d=dist(x,y,u.x,u.y);
     if(d<r*1.35){
       const f=1-d/(r*1.35),dm=Math.max(1,Math.round(dmg*f));
       const ang=Math.atan2(u.y-y,u.x-x);
       u.vx+=Math.cos(ang)*f*(r/9);u.vy+=Math.sin(ang)*f*(r/9)-f*2;u.grounded=false;
       damage(u,dm,'boom',opt.ti);
     }
   }
 }
 sfx.boom(r);
}
/* ============================== PROJECTILES ============================== */
function makeProj(type,x,y,vx,vy,ti,extra){const p={type,x,y,vx,vy,ti,age:0,...extra};G.projectiles.push(p);return p;}
function updateProjectiles(){
 for(let i=G.projectiles.length-1;i>=0;i--){
  const p=G.projectiles[i];p.age++;
  const wd=WEAPONS[p.type];
  p.vy+=wd.grav??GRAV;
  p.vx+=G.wind*(wd.wind??0);
  if(p.type==='dynamite'){p.vx*=.9;}
  const steps=Math.ceil(Math.max(Math.abs(p.vx),Math.abs(p.vy))/3)||1;
  let dead=false;
  for(let s=0;s<steps&&!dead;s++){
   p.x+=p.vx/steps;p.y+=p.vy/steps;
   /* unit hit (not owner early) */
   if(p.age>8){
     const u=unitAt(p.x,p.y,G.teams[p.ti]?.units.find(u=>u.projOwner));
     for(const t of G.teams)for(const uu of t.units){
       if(!uu.alive)continue;
       if(p.age<20&&t.i===p.ti)continue;
       if(dist(p.x,p.y,uu.x,uu.y)<UR+3){
         if(wd.contact){boom(p.x,p.y,wd.r,wd.dmg,{ti:p.ti});dead=true;}
         break;
       }
     }
   }
   if(dead)break;
   if(S(p.x,p.y)){
     if(wd.contact){boom(p.x,p.y,wd.r,wd.dmg,{ti:p.ti});dead=true;}
     else if(wd.bounce&&p.bounces>0){
       /* reflect */
       const lx=S(p.x-p.vx,p.y),ly=S(p.x,p.y-p.vy);
       if(lx)p.vx*=-.5;else p.vx*=.7;
       if(ly||!lx)p.vy*=-.45;else p.vy*=.7;
       if(Math.abs(p.vy)<.8)p.vy=0;
       p.bounces--;sfx.bounce();spawnDust(p.x,p.y,3);
       p.x-=p.vx*2;p.y-=p.vy*2;
     }else if(wd.bounce){p.vy=0;p.vx*=.8;p.y-=1;}
     else{boom(p.x,p.y,wd.r,wd.dmg,{ti:p.ti});dead=true;}
   }
  }
  if(p.type==='holy'&&p.fuse<=30&&!p.beeping){p.beeping=true;}
  if(p.fuse!==undefined){
   p.fuse--;
   if(p.fuse===60||p.fuse===30)tone(1200,.08,'square',.1);
   if(p.fuse<=0){
     if(p.type==='cluster'){
       boom(p.x,p.y,wd.r,wd.dmg,{ti:p.ti});
       for(let b=0;b<wd.bomblets;b++)makeProj('bomblet',p.x,p.y-4,rnd(-4,4),rnd(-8,-3),p.ti,{});
     }else boom(p.x,p.y,wd.r,wd.dmg,{ti:p.ti});
     dead=true;
   }
  }
  if(p.y>H+40||p.x<-40||p.x>W+40){dead=true;if(p.y>H+40){sfx.splash();spawnSplash(clamp(p.x,0,W),WATER);}}
  if(dead)G.projectiles.splice(i,1);
 }
}
/* ============================== CAMERA ============================== */
function updateCam(){
 let fx=G.cam.tx,fy=G.cam.ty,fz=1;
 if(G.phase==='fly'&&G.projectiles.length){
   let best=null,bv=0;for(const p of G.projectiles){const v=Math.abs(p.vx)+Math.abs(p.vy);if(v>bv){bv=v;best=p;}}
   if(best){fx=best.x;fy=best.y;fz=1.3;}
 }else if(G.cur&&G.cur.u&&G.cur.u.alive){
   const u=G.cur.u;fx=u.x+u.face*70;fy=u.y-30;fz=G.phase==='aim'?1.35:1.1;
 }
 const vw=W/fz,vh=H/fz;
 fx=clamp(fx,vw/2,W-vw/2);fy=clamp(fy,vh/2,H-vh/2);
 G.cam.tx=fx;G.cam.ty=fy;G.cam.tz=fz;
 G.cam.x=lerp(G.cam.x,fx,.08);G.cam.y=lerp(G.cam.y,fy,.08);G.cam.z=lerp(G.cam.z,fz,.06);
}


/* ============================== WEAPONS ============================== */
const WEAPONS={
 bazooka:{nm:'Bazooka',ic:'🚀',cat:'ind',contact:true,r:38,dmg:48,wind:1,ammo:Infinity,charge:true},
 grenade:{nm:'Grenade',ic:'💣',cat:'ind',contact:false,bounce:true,bounces:5,fuse:180,r:34,dmg:42,wind:.3,ammo:Infinity,charge:true},
 shotgun:{nm:'Shotgun',ic:'🔫',cat:'dir',contact:true,r:13,dmg:26,ammo:Infinity,shots:2},
 cluster:{nm:'Cluster',ic:'🎆',cat:'ind',contact:false,bounce:true,bounces:4,fuse:150,r:24,dmg:16,wind:.3,bomblets:5,ammo:3,charge:true},
 holy:{nm:'Holy',ic:'⛪',cat:'aoe',contact:false,bounce:true,bounces:4,fuse:240,r:95,dmg:110,wind:.3,ammo:1,charge:true},
 dynamite:{nm:'Dynamite',ic:'🧨',cat:'aoe',contact:false,fuse:300,r:54,dmg:75,wind:0,ammo:2,drop:true},
 bomblet:{nm:'',ic:'',contact:true,r:20,dmg:18,wind:1,hidden:true},
 teleport:{nm:'Teleport',ic:'🌀',cat:'uti',ammo:3,utility:true},
 girder:{nm:'Girder',ic:'🪜',cat:'uti',ammo:2,utility:true},
};
const WLIST=['bazooka','grenade','shotgun','cluster','holy','dynamite','teleport','girder'];
function ammoOf(ti,wk){const a=G.teams[ti].ammo[wk];return a===undefined?Infinity:a;}
function useAmmo(ti,wk){const t=G.teams[ti];if((t.ammo[wk]??0)<Infinity&&t.ammo[wk]!==undefined)t.ammo[wk]--;}
/* ============================== FIRING ============================== */
let aim={charging:false,power:0,mouseAim:false};
function muzzle(u){const a=u.ang*Math.PI/180;return{x:u.x+Math.cos(a)*u.face*16,y:u.y-4-Math.sin(a)*16,a};}
function fire(power){
 const u=G.cur.u,wk=G.cur.weapon,wd=WEAPONS[wk],m=muzzle(u);
 if(wd.utility)return;
 if(wd.charge&&power<=0.02)return;
 useAmmo(G.cur.ti,wk);
 if(wk==='shotgun'){
   sfx.shotgun();G.shake=Math.max(G.shake,4);
   const a=m.a,dirx=Math.cos(a)*u.face,diry=-Math.sin(a);
   let hx=u.x,hy=u.y,hitU=null;
   for(let d=0;d<600;d+=3){
     hx=u.x+dirx*d;hy=u.y-4+diry*d;
     if(hy>WATER)break;
     const uu=unitAt(hx,hy,u);if(uu){hitU=uu;break;}
     if(S(hx,hy))break;
   }
   destruct(hx,hy,wd.r);
   for(let i=0;i<8;i++)spawnP(hx,hy,'spark',1);
   spawnDust(hx,hy,4);
   if(hitU){const dm=Math.round(wd.dmg*rnd(.85,1.15));hitU.vx+=dirx*3;hitU.vy+=diry*3-2;hitU.grounded=false;damage(hitU,dm,'shot',G.cur.ti);}
   G.cur.shots--;
   if(G.cur.shots>0){showMsg('ONE MORE SHELL!');return;} /* stay in aim */
 }else if(wd.drop){
   makeProj('dynamite',u.x,u.y-6,0,0,G.cur.ti,{fuse:wd.fuse});
   sfx.click();
 }else{
   const v=4.5+power*13.5;
   makeProj(wk,m.x,m.y,Math.cos(m.a)*u.face*v,-Math.sin(m.a)*v,G.cur.ti,
     wd.bounce?{fuse:wd.fuse,bounces:wd.bounces}:{});
   sfx.shoot();G.shake=Math.max(G.shake,3);
   for(let i=0;i<6;i++)spawnP(m.x,m.y,'smoke',.6);
 }
 G.cur.u.face=Math.sign(Math.cos(m.a))||u.face;
 endAim();
}
function endAim(){aim.charging=false;chargeStop();G.phase='fly';G.settle=0;}
/* ============================== TURNS ============================== */
function aliveTeams(){return G.teams.filter(t=>t.units.some(u=>u.alive));}
function startTurn(){
 const at=aliveTeams();
 if(at.length<=1){gameOver(at[0]);return;}
 let t=G.teams[G.ti],guard=0;
 do{G.ti=(G.ti+1)%G.teams.length;t=G.teams[G.ti];guard++;}while(guard<20&&!t.units.some(u=>u.alive));
 let ui=t.units.findIndex(u=>u.alive);
 t.units.forEach((u,i)=>{if(u.alive&&(u.hp<t.units[ui].hp||!t.units[ui].alive))ui=i;});
 const u=t.units[ui];
 G.cur={ti:G.ti,ui,u,weapon:'bazooka',shots:2};
 G.timer=TURN_TIME;G.phase='aim';G.windTarget=rnd(-1,1);
 u.ang=35;
 updateTray();
 const humans=G.teams.filter(t2=>!t2.cpu&&t2.units.some(x=>x.alive)).length;
 if(!t.cpu&&humans>1){
   G.phase='handoff';
   document.getElementById('hoName').textContent=t.name;
   document.getElementById('hoName').style.color=t.col;
   document.getElementById('handoff').classList.remove('hidden');
 }else if(t.cpu){G.phase='ai';G.ai={st:'think',t:0,sol:null,moveDir:0,moveT:0};}
 showMsg(t.name+"'S TURN!");
 document.getElementById('turnLabel').textContent=t.name;
 document.getElementById('turnLabel').style.color=t.col;
 document.getElementById('hint').classList.remove('hidden');
}
function hoReady(){
 sfx.click();
 document.getElementById('handoff').classList.add('hidden');
 const t=G.teams[G.ti];
 if(t.cpu){G.phase='ai';G.ai={st:'think',t:0,sol:null,moveDir:0,moveT:0};}
 else G.phase='aim';
}
function endTurn(){
 G.phase='settle';G.settle=40;
}
function nextTurnCheck(){
 if(G.phase!=='fly')return;
 const busy=G.projectiles.length>0||G.freeze>0;
 let moving=false;
 for(const t of G.teams)for(const u of t.units)if(u.alive&&(Math.abs(u.vx)>.3||Math.abs(u.vy)>.5))moving=true;
 if(!busy&&!moving){G.settle++;if(G.settle>50){G.phase='idle';setTimeout(startTurn,600);}}
 else G.settle=0;
}
function gameOver(winner){
 G.phase='over';
 document.getElementById('goTitle').textContent=winner?winner.name+' WINS!':'DRAW!';
 if(winner)document.getElementById('goTitle').style.color=winner.col;
 document.getElementById('goSub').textContent='Total devastation achieved.';
 document.getElementById('gameover').classList.remove('hidden');
 sfx.win();
}
function showMsg(t){const b=document.getElementById('msgBanner');b.textContent=t;b.style.opacity=1;G.msgT=120;}
/* ============================== AIM / CONTROL ============================== */
function selectWeaponIdx(i){
 const wk=WLIST[i];if(!wk)return;
 const wd=WEAPONS[wk];
 if(ammoOf(G.cur.ti,wk)<=0||wd.hidden)return;
 G.cur.weapon=wk;sfx.sel();updateTray();
 if(wd.utility)G.phase='target';
 else if(G.phase==='target')G.phase='aim';
}
function onMouseDown(){
 if(G.phase!=='aim'&&G.phase!=='target')return;
 const t=G.teams[G.cur.ti];if(t.cpu)return;
 const wd=WEAPONS[G.cur.weapon];
 if(G.phase==='target'){
   if(G.cur.weapon==='teleport')doTeleport(mouse.x,mouse.y);
   else if(G.cur.weapon==='girder')doGirderClick();
   return;
 }
 if(wd.utility)return;
 if(!wd.charge){fire(1);return;}
 aim.charging=true;aim.power=0;chargeStart();
}
function onMouseUp(){
 if(aim.charging){fire(aim.power);}
}
function doTeleport(x,y){
 if(S(x,y)){showMsg('SOLID ROCK!');return;}
 const u=G.cur.u;
 for(let i=0;i<20;i++)spawnP(u.x,u.y,'tp',1);
 u.x=clamp(x,UR+2,W-UR-2);u.y=groundY(x,Math.max(0,y-40))-UR-1;u.vx=0;u.vy=0;
 for(let i=0;i<20;i++)spawnP(u.x,u.y,'tp',1);
 sfx.tp();useAmmo(G.cur.ti,'teleport');G.cur.weapon='bazooka';updateTray();G.phase='aim';
}
let girderPts=null;
function doGirderClick(){
 if(!girderPts){girderPts={x:mouse.x,y:mouse.y};addText(mouse.x,mouse.y,'↳ pick end','#ffd76a');sfx.sel();return;}
 addSolidLine(girderPts.x,girderPts.y,mouse.x,mouse.y,7,'#8f8a80');
 sfx.girder();useAmmo(G.cur.ti,'girder');girderPts=null;G.cur.weapon='bazooka';updateTray();G.phase='aim';
}
function updateAimControl(){
 if(!G.cur||G.phase!=='aim')return;
 const t=G.teams[G.cur.ti];if(t.cpu)return;
 const u=G.cur.u,wd=WEAPONS[G.cur.weapon];
 /* movement */
 if(keys['ArrowLeft'])walk(u,-1);
 if(keys['ArrowRight'])walk(u,1);
 /* aim: mouse if moved recently, else arrows */
 if(keys['ArrowUp'])u.ang=Math.min(89,u.ang+1.6);
 if(keys['ArrowDown'])u.ang=Math.max(-80,u.ang-1.6);
 const dx=mouse.x-u.x,dy=mouse.y-u.y;
 if(Math.abs(dx)>10||Math.abs(dy)>10){
   u.face=dx>=0?1:-1;
   u.ang=clamp(-Math.atan2(dy,Math.abs(dx))*180/Math.PI,-80,89);
 }
 /* charge / fire */
 if(wd.charge){
   if(aim.charging){
     aim.power=Math.min(1,aim.power+1/70);chargeUp(aim.power);
     if(!(keys['Space']||mouse.down))fire(aim.power);
   }else if(keys['Space']||mouse.down){aim.charging=true;aim.power=0;chargeStart();}
 }else if(wd.drop&&keys['Space']){keys['Space']=false;fire(1);}
 /* timer */
 G.timer-=1/60;
 if(G.timer<=0){showMsg('TIME UP!');endAim();}
}
/* ============================== AI ============================== */
function simShot(u,angDeg,pow,wk){
 const wd=WEAPONS[wk];if(wd.utility||wd.drop)return null;
 const a=angDeg*Math.PI/180,v=4.5+pow*13.5;
 let x=u.x+Math.cos(a)*u.face*16,y=u.y-4-Math.sin(a)*16,vx=Math.cos(a)*u.face*v,vy=-Math.sin(a)*v;
 const hitMap=new Map();
 for(let step=0;step<400;step++){
  vy+=GRAV;vx+=G.wind*(wd.wind??.3);
  x+=vx;y+=vy;
  if(y>WATER)return{score:-30,x,y,hit:'water'};
  if(x<0||x>W)return{score:-20,x,y,hit:'out'};
  if(step>6){const uu=unitAt(x,y);if(uu)hitMap.set(uu,1);}
  if(S(x,y)||hitMap.size){
    let score=0;
    for(const t of G.teams)for(const uu of t.units){
      if(!uu.alive)continue;
      const d=dist(x,y,uu.x,uu.y);
      if(d<wd.r*1.35){
        const dm=wd.dmg*(1-d/(wd.r*1.35));
        score+=(uu.hp<=dm?100:dm)*(t.i===u.ti?-2.2:1);
      }
    }
    return{score,x,y,hit:'boom'};
  }
 }
 return{score:-10,x,y,hit:'none'};
}
function updateAI(){
 const A=G.ai;if(!A||G.phase!=='ai')return;
 const t=G.teams[G.cur.ti],u=G.cur.u;
 A.t++;
 if(A.st==='think'){
   let best=null;
   for(let ang=-10;ang<=85;ang+=6){
     for(const pow of[.35,.55,.75,1]){
       const r=simShot(u,ang,pow,G.cur.weapon);
       if(r&&(!best||r.score>best.score))best={...r,ang,pow};
     }
   }
   /* risk assessment: unsafe position? */
   const nearEdge=(dir)=>{let d=0;for(let x=u.x;S(x,u.y+UR+2)&&d<90;x+=dir)d++;return d;};
   const unsafe=(!S(u.x-30,u.y+UR+4)&&!S(u.x-60,u.y+UR+4))||(!S(u.x+30,u.y+UR+4)&&!S(u.x+60,u.y+UR+4))||u.y>WATER-120;
   A.sol=best||{ang:40,pow:.7,score:0};
   A.st='move';A.moveT=unsafe?rndi(40,80):rndi(15,40);
   A.moveDir=unsafe?(S(u.x-40,u.y+UR+4)?-1:1):(Math.random()<.5?-1:1);
 }else if(A.st==='move'){
   if(A.moveT-->0&&u.grounded){walk(u,A.moveDir);if(A.moveT%9===0)sfx.step();}
   else{A.st='aim';A.t=0;}
 }else if(A.st==='aim'){
   const target=A.sol.ang;
   u.ang+=clamp(target-u.ang,-2,2);
   u.face=A.sol.x>u.x?1:-1;
   if(A.t>50){A.st='fire';A.t=0;}
 }else if(A.st==='fire'){
   if(A.t>25){
     /* weapon choice: holy if big cluster of enemies, else keep */
     fire(A.sol.pow+rnd(-.04,.04));
     A.st='done';
   }
 }
}
/* ============================== RENDER ============================== */
const clouds=Array.from({length:7},()=>({x:rnd(0,W),y:rnd(40,200),s:rnd(.6,1.6)}));
function draw(){
 const c=G.cam,sh=G.shake;
 const sx=rnd(-sh,sh),sy=rnd(-sh,sh);
 cx.save();
 cx.clearRect(0,0,W,H);
 /* sky */
 const cave=G.mapType==='cave';
 const g=cx.createLinearGradient(0,0,0,H);
 if(cave){g.addColorStop(0,'#141022');g.addColorStop(1,'#241c38');}
 else{g.addColorStop(0,'#2a3d66');g.addColorStop(.5,'#4a6b9c');g.addColorStop(1,'#8fb0c9');}
 cx.fillStyle=g;cx.fillRect(0,0,W,H);
 if(!cave){
   cx.fillStyle='rgba(255,240,200,.9)';cx.beginPath();cx.arc(W-160,110,44,0,7);cx.fill();
   cx.fillStyle='rgba(255,240,200,.25)';cx.beginPath();cx.arc(W-160,110,70,0,7);cx.fill();
 }else{
   cx.fillStyle='rgba(255,255,255,.5)';
   for(let i=0;i<40;i++){const x=(SI(i*3)*W+G.time*.2)%W,y=SI(i*7)*H*.6;cx.fillRect(x,y,1.6,1.6);}
 }
 /* clouds */
 cx.fillStyle=cave?'rgba(255,255,255,.04)':'rgba(255,255,255,.55)';
 for(const cl of clouds){
   cl.x+=G.wind*.3*cl.s;if(cl.x>W+120)cl.x=-120;if(cl.x<-120)cl.x=W+120;
   cx.beginPath();
   cx.ellipse(cl.x,cl.y,44*cl.s,15*cl.s,0,0,7);
   cx.ellipse(cl.x+30*cl.s,cl.y-8*cl.s,30*cl.s,12*cl.s,0,0,7);
   cx.ellipse(cl.x-30*cl.s,cl.y-4*cl.s,26*cl.s,11*cl.s,0,0,7);
   cx.fill();
 }
 /* world transform */
 cx.translate(W/2+sx,H/2+sy);cx.scale(c.z,c.z);cx.translate(-c.x,-c.y);
 cx.drawImage(TC,0,0);
 /* girders already stamped on TC */
 /* units */
 for(let ti=0;ti<G.teams.length;ti++){
   const t=G.teams[ti];
   for(const u of t.units){
     if(!u.alive)continue;
     drawUnit(u,t,ti===G.ti&&u===G.cur?.u);
   }
 }
 /* projectiles */
 for(const p of G.projectiles){
   const wd=WEAPONS[p.type];
   cx.strokeStyle='rgba(255,220,120,.5)';cx.lineWidth=2;
   cx.beginPath();cx.moveTo(p.x-p.vx*2.2,p.y-p.vy*2.2);cx.lineTo(p.x,p.y);cx.stroke();
   cx.font='18px serif';cx.textAlign='center';cx.textBaseline='middle';
   cx.fillText(wd.ic,p.x,p.y);
   if(p.fuse!==undefined&&p.fuse<120&&(p.fuse/10|0)%2===0){
     cx.strokeStyle='rgba(255,80,80,.9)';cx.beginPath();cx.arc(p.x,p.y,13,0,7);cx.stroke();
   }
 }
 /* aim UI */
 if(G.phase==='aim'&&G.cur&&G.cur.u.alive&&!G.teams[G.cur.ti].cpu)drawAim();
 /* chunks */
 for(const ch of G.chunks){cx.save();cx.translate(ch.x,ch.y);cx.rotate(ch.rot);cx.fillStyle=ch.col;cx.fillRect(-ch.size/2,-ch.size/2,ch.size,ch.size);cx.restore();}
 /* particles */
 for(const p of G.particles){
   const f=1-p.t/p.max;
   if(p.col.endsWith(',')){cx.fillStyle=p.col+(f*.7)+')';}
   else{cx.globalAlpha=f;cx.fillStyle=p.col;}
   cx.beginPath();cx.arc(p.x,p.y,p.size*(p.type==='smoke'?1+f:1),0,7);cx.fill();
   cx.globalAlpha=1;
 }
 /* water (front) */
 cx.fillStyle='rgba(40,110,200,.72)';
 cx.beginPath();cx.moveTo(0,H);cx.lineTo(0,WATER+Math.sin(G.time*.05)*3);
 for(let x=0;x<=W;x+=16)cx.lineTo(x,WATER+Math.sin(x*.03+G.time*.06)*3);
 cx.lineTo(W,H);cx.fill();
 cx.strokeStyle='rgba(160,220,255,.6)';cx.lineWidth=2;
 cx.beginPath();for(let x=0;x<=W;x+=16){const y=WATER+Math.sin(x*.03+G.time*.06)*3;x===0?cx.moveTo(x,y):cx.lineTo(x,y);}cx.stroke();
 /* floating text */
 cx.textAlign='center';cx.font='bold 15px "Trebuchet MS"';
 for(const t of G.texts){
   cx.globalAlpha=1-t.t/70;
   cx.fillStyle='#000';cx.fillText(t.txt,t.x+1,t.y+1);
   cx.fillStyle=t.col;cx.fillText(t.txt,t.x,t.y);
   cx.globalAlpha=1;
 }
 cx.restore();
}
function drawUnit(u,t,active){
 cx.save();
 if(u.sub)cx.globalAlpha=.6;
 /* body */
 cx.fillStyle=t.dark;
 cx.beginPath();cx.ellipse(u.x,u.y+3,UR+1,UR,0,0,7);cx.fill();
 cx.fillStyle=t.col;
 cx.beginPath();cx.ellipse(u.x,u.y,UR,UR+1,0,0,7);cx.fill();
 /* eyes */
 cx.fillStyle='#fff';
 cx.beginPath();cx.arc(u.x+u.face*3,u.y-2,2.4,0,7);cx.arc(u.x+u.face*3+u.face*3.4,u.y-2,2.4,0,7);cx.fill();
 cx.fillStyle='#111';
 const bl=(u.blink=(u.blink+1)%180)<6;
 if(!bl){cx.beginPath();cx.arc(u.x+u.face*3.6,u.y-2,1.1,0,7);cx.arc(u.x+u.face*3.6+u.face*3.4,u.y-2,1.1,0,7);cx.fill();}
 /* hp bar */
 const w=22;
 cx.fillStyle='rgba(0,0,0,.55)';cx.fillRect(u.x-w/2,u.y-UR-11,w,4);
 cx.fillStyle=u.hp>50?'#5fd35f':u.hp>25?'#ffc93c':'#ff5252';
 cx.fillRect(u.x-w/2,u.y-UR-11,w*u.hp/100,4);
 if(active){
   const bob=Math.sin(G.time*.15)*3;
   cx.fillStyle='#ffd76a';
   cx.beginPath();cx.moveTo(u.x,u.y-UR-18+bob);cx.lineTo(u.x-6,u.y-UR-27+bob);cx.lineTo(u.x+6,u.y-UR-27+bob);cx.fill();
 }
 cx.restore();
}
function drawAim(){
 const u=G.cur.u,wd=WEAPONS[G.cur.weapon],m=muzzle(u);
 if(wd.utility){
   cx.strokeStyle=G.cur.weapon==='teleport'?'rgba(176,106,255,.8)':'rgba(255,215,106,.8)';
   cx.setLineDash([6,6]);cx.lineWidth=2;
   cx.beginPath();cx.arc(mouse.x,mouse.y,14,0,7);cx.stroke();cx.setLineDash([]);
   cx.beginPath();cx.moveTo(mouse.x-20,mouse.y);cx.lineTo(mouse.x+20,mouse.y);cx.moveTo(mouse.x,mouse.y-20);cx.lineTo(mouse.x,mouse.y+20);cx.stroke();
   return;
 }
 /* crosshair */
 cx.strokeStyle='rgba(255,255,255,.9)';cx.lineWidth=1.5;
 cx.beginPath();cx.arc(m.x+Math.cos(m.a)*u.face*46,m.y-Math.sin(m.a)*46,8,0,7);cx.stroke();
 /* trajectory prediction */
 const pow=aim.charging?aim.power:.72;
 if(!wd.drop){
   const a=m.a,v=4.5+pow*13.5;
   let x=m.x,y=m.y,vx=Math.cos(a)*u.face*v,vy=-Math.sin(a)*v;
   cx.fillStyle='rgba(255,255,255,.55)';
   for(let i=0;i<110;i++){
     vy+=GRAV;vx+=G.wind*(wd.wind??.3);x+=vx;y+=vy;
     if(S(x,y)||y>WATER)break;
     if(i%4===0){cx.beginPath();cx.arc(x,y,1.8,0,7);cx.fill();}
   }
 }
 /* power bar */
 if(aim.charging){
   cx.fillStyle='rgba(0,0,0,.5)';cx.fillRect(u.x-24,u.y+UR+8,48,7);
   cx.fillStyle=`hsl(${120-aim.power*120},90%,55%)`;cx.fillRect(u.x-23,u.y+UR+9,46*aim.power,5);
 }
}
/* ============================== HUD ============================== */
function buildChips(){
 const el=document.getElementById('teamChips');el.innerHTML='';
 G.teams.forEach((t,i)=>{
   const d=document.createElement('div');d.className='chip';d.id='chip'+i;
   d.innerHTML=`<div class="nm"><span style="color:${t.col}">${t.name}</span><span class="cnt"></span></div><div class="bar"><i style="background:${t.col}"></i></div>`;
   el.appendChild(d);
 });
}
function updateHUD(){
 G.teams.forEach((t,i)=>{
   const d=document.getElementById('chip'+i);if(!d)return;
   const alive=t.units.filter(u=>u.alive);
   const hp=t.units.reduce((s,u)=>s+Math.max(0,u.hp),0);
   d.classList.toggle('active',i===G.ti&&G.phase!=='over');
   d.classList.toggle('dead',alive.length===0);
   d.querySelector('.cnt').textContent=alive.length+'/4';
   d.querySelector('.bar i').style.width=(hp/400*100)+'%';
 });
 const frac=Math.max(0,G.timer/TURN_TIME);
 const tb=document.getElementById('timerBar');
 tb.style.width=(frac*100)+'%';
 document.getElementById('timerTxt').textContent=Math.ceil(Math.max(0,G.timer));
 document.getElementById('timerWrap').classList.toggle('urgent',G.timer<10&&(G.phase==='aim'||G.phase==='ai'));
 /* wind */
 const wa=document.getElementById('windArrow');
 wa.textContent=G.wind>=0?'➤':'➤';
 wa.style.transform=`scaleX(${G.wind>=0?1:-1}) scaleX(${1+Math.abs(G.wind)*.6})`;
 document.getElementById('windVal').textContent=Math.abs(G.wind).toFixed(1);
 if(G.msgT>0){G.msgT--;if(G.msgT===0)document.getElementById('msgBanner').style.opacity=0;}
 /* tick sounds */
 const sec=Math.ceil(G.timer);
 if((G.phase==='aim'||G.phase==='ai')&&sec!==G.lastTick){
   G.lastTick=sec;
   if(sec<=10&&sec>0)sfx.tick(sec<=5);
 }
}
function buildTray(){
 const el=document.getElementById('tray');el.innerHTML='';
 let lastCat='';
 WLIST.forEach((wk,i)=>{
   const wd=WEAPONS[wk];
   if(wd.cat!==lastCat){const s=document.createElement('div');s.className='wcat';s.textContent=wd.cat==='ind'?'ARC':wd.cat==='dir'?'DIRECT':wd.cat==='aoe'?'AOE':'UTILITY';el.appendChild(s);lastCat=wd.cat;}
   const b=document.createElement('div');b.className='wpn';b.id='wp_'+wk;
   b.innerHTML=`<span class="kk">${i+1}</span><span class="ic">${wd.ic}</span><span class="nm2">${wd.nm}</span><span class="am"></span>`;
   b.onclick=()=>{if(G.phase==='aim'||G.phase==='target')selectWeaponIdx(i);};
   el.appendChild(b);
 });
}
function updateTray(){
 WLIST.forEach(wk=>{
   const b=document.getElementById('wp_'+wk);if(!b||!G.cur)return;
   const am=ammoOf(G.cur.ti,wk);
   b.classList.toggle('dis',am<=0);
   b.classList.toggle('sel',G.cur.weapon===wk);
   b.querySelector('.am').textContent=am===Infinity?'∞':am;
 });
}
/* ============================== SETUP ============================== */
let selMap='island',selCnt=2,cpuFlags=[false,true,false,true];
function buildSetup(){
 const mo=document.getElementById('mapOpts');
 const maps=[...Object.keys(MAPS),'random'];
 mo.innerHTML='';
 maps.forEach(m=>{
   const b=document.createElement('button');b.className='opt'+(m===selMap?' on':'');
   b.textContent=m==='random'?'🎲 RANDOM':MAPS[m];
   b.onclick=()=>{selMap=m;buildSetup();sfx.click();};
   mo.appendChild(b);
 });
 const co=document.getElementById('cntOpts');co.innerHTML='';
 [2,3,4].forEach(n=>{
   const b=document.createElement('button');b.className='opt'+(n===selCnt?' on':'');
   b.textContent=n+' TEAMS';b.onclick=()=>{selCnt=n;buildSetup();sfx.click();};
   co.appendChild(b);
 });
 const tl=document.getElementById('teamList');tl.innerHTML='';
 for(let i=0;i<selCnt;i++){
   const r=document.createElement('div');r.className='teamRow';
   r.innerHTML=`<span class="dot" style="background:${TEAM_DEFS[i].col}"></span>
   <input type="text" value="${TEAM_DEFS[i].name}" maxlength="16" data-ti="${i}">
   <label><input type="checkbox" data-cpu="${i}" ${cpuFlags[i]?'checked':''}> CPU</label>`;
   tl.appendChild(r);
 }
}
document.getElementById('startBtn').onclick=()=>{
 audioInit();sfx.click();
 let mt=selMap;
 if(mt==='random')mt=['cave','island','twin'][rndi(0,2)];
 document.querySelectorAll('#teamList input[type=text]').forEach(inp=>{const i=+inp.dataset.ti;TEAM_DEFS[i].name=inp.value.trim()||TEAM_DEFS[i].name;});
 document.querySelectorAll('#teamList input[type=checkbox]').forEach(cb=>{cpuFlags[+cb.dataset.cpu]=cb.checked;});
 startGame(mt,selCnt);
};
document.getElementById('hoBtn').onclick=hoReady;
document.getElementById('againBtn').onclick=()=>{
 sfx.click();document.getElementById('gameover').classList.add('hidden');
 document.getElementById('setup').classList.remove('hidden');
 document.getElementById('tray').classList.add('hidden');
 G.phase='setup';
};
function startGame(mt,cnt){
 G.mapType=mt;G.teams=[];G.projectiles=[];G.particles=[];G.texts=[];G.chunks=[];
 genTerrain(mt);
 const spawns=findSpawns(cnt*4);
 for(let i=0;i<cnt;i++){
   const def=TEAM_DEFS[i];
   const units=[];
   for(let k=0;k<4;k++){
     const sp=spawns[i*4+k]||{x:rnd(100,W-100),y:100};
     const u=makeUnit(sp.x,groundY(sp.x,Math.max(0,sp.y-30))-UR-1,def);
     u.name='W'+(k+1);units.push(u);
   }
   G.teams.push({i,name:def.name,col:def.col,dark:def.dark,units,cpu:cpuFlags[i],
     ammo:{cluster:3,holy:1,dynamite:2,teleport:3,girder:2}});
 }
 G.ti=rndi(0,cnt-1);G.ti=(G.ti-1+cnt)%cnt;
 G.cam.x=W/2;G.cam.y=H/2;G.cam.z=1;
 buildChips();buildTray();
 document.getElementById('setup').classList.add('hidden');
 document.getElementById('tray').classList.remove('hidden');
 startTurn();
}
/* ============================== MAIN LOOP ============================== */
let lastT=0,acc=0;
function loop(ts){
 requestAnimationFrame(loop);
 const dt=Math.min(50,ts-lastT);lastT=ts;acc+=dt;
 while(acc>=16.66){
   acc-=16.66;
   step();
 }
 draw();
}
function step(){
 G.time++;
 if(G.freeze>0){G.freeze--;G.shake*=.9;updateCam();return;}
 G.shake*=.88;if(G.shake<.3)G.shake=0;
 G.wind=lerp(G.wind,G.windTarget,.01);
 if(G.windTarget!==0&&Math.random()<.002)G.windTarget=clamp(G.windTarget+rnd(-.4,.4),-1,1);
 if(G.phase==='aim')updateAimControl();
 if(G.phase==='ai')updateAI();
 if(G.phase==='aim'||G.phase==='ai'||G.phase==='fly'){
   for(let ti=0;ti<G.teams.length;ti++)for(const u of G.teams[ti].units)updateUnit(u,ti);
   updateProjectiles();
   if(G.phase==='fly')nextTurnCheck();
 }
 updateParticles();
 updateCam();
 updateHUD();
}
genTerrain('island');
buildSetup();
requestAnimationFrame(loop);
</script>
</body>
</html>
<!-- agent-meta {"model":"z-ai/glm-5.3-flash","provider":"openrouter","persona":"full","sessionId":"5a29faed-6a9c-4df0-bbdd-7f90771025cc","tokensIn":309844,"tokensOut":25611,"tokensTotal":335455,"cost":0.021329025000000005,"turns":11,"toolCalls":11,"failedToolCalls":0,"timestamp":"2026-08-27T02:18:41.692Z"} -->