← Worms results

Worms

Claude haiku 5.5 · full

Initial view of Worms

Reviews & generation details

Reviews

No human review yet. Unreviewed does not mean unsuccessful.

Generation context

Task text captured before this run. Effective runtime system instructions were not captured.

Generation prompt · 517fda48a4
Create a fun and beautiful HTML artillery game similar to Worms. Expectation is a highly-polished game, go all out. 
Requirements: 
- 5+ weapons with varying effects/gameplay 
- Creative utility items 
- Procedurally generated terrain, 3 different shapes (cave, single island, two islands) 
- Destructible terrain (pixel-based or chunk-based subtraction) 
- Multiplayer support via hot-seat 
- CPU player support 
- 2-4 teams with 4 units each, 100 health per unit 
- Beautiful HUD - Visceral physics (gravity, velocity, friction, collision, fall damage, parabolic projectile arcs) 
- Unit movement limited by turn time, with jumping 
- Camera follows active worm 
- Particle effects system for explosions, dust/debris, etc. 
- Subtle screen shake on heavy explosions 
- Fun and quirky sounds effects for gameplay and tactile sounds for menu items

Advanced Physics & Mechanics Addendum:
The game must feature a global Wind Variable affecting projectile trajectories and unit movement. Implement Water Physics where units can drown or float. Terrain Destruction must result in physical, falling debris chunks.

Weaponry must be categorized into:

Indirect Projectiles (parabolic travel time),
Direct/Instant-hit,
High-AoE/Map-altering.
AI Behavior: CPU must use 'risk-assessment' logic to avoid environmental hazards (water/cliffs) and prioritize high-value targets.

Visual Polish ('Juice'): Include predictive aiming trajectory lines, dynamic camera zooming/smoothing, 'impact freeze' for large explosions, floating combat text for damage, and particle-based debris for all collisions. The turn timer must create escalating tension through UI scaling and sound tempo.
Inspect original source 81,847 bytes · SHA-256 0c1749389946
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>CRATERFALL — Artillery Worms</title>
<style>
  :root{
    --gold:#ffd166; --ink:#0d1321; --panel:rgba(12,18,34,.72); --edge:rgba(255,255,255,.12);
    --red:#ff4d5e; --green:#4ade80; --blue:#38bdf8;
    --font: "Trebuchet MS","Segoe UI",system-ui,sans-serif;
  }
  *{box-sizing:border-box;user-select:none}
  html,body{margin:0;height:100%;overflow:hidden;background:#05070d;font-family:var(--font);color:#f1f5f9}
  canvas#game{position:fixed;inset:0;width:100vw;height:100vh;display:block;cursor:crosshair}
  .hidden{display:none!important}
  .glass{background:var(--panel);border:1px solid var(--edge);border-radius:14px;box-shadow:0 10px 30px rgba(0,0,0,.45),inset 0 1px 0 rgba(255,255,255,.08);backdrop-filter:blur(8px)}

  /* ---------- MENU ---------- */
  #menu{position:fixed;inset:0;display:flex;align-items:center;justify-content:center;z-index:50;background:radial-gradient(ellipse at center,rgba(10,14,28,.35),rgba(3,5,12,.85))}
  #menu .card{width:min(860px,94vw);padding:28px 34px 26px;display:grid;grid-template-columns:1fr 1fr;gap:18px 30px;animation:pop .6s cubic-bezier(.2,1.4,.4,1)}
  #menu h1{grid-column:1/-1;margin:0;text-align:center;font-size:54px;letter-spacing:6px;font-weight:900;
    background:linear-gradient(180deg,#fff7d6,#ffd166 45%,#ff8a3d);-webkit-background-clip:text;background-clip:text;color:transparent;
    text-shadow:0 4px 0 rgba(0,0,0,.35);filter:drop-shadow(0 6px 14px rgba(255,140,60,.35))}
  #menu .sub{grid-column:1/-1;text-align:center;margin-top:-14px;color:#a8b3c7;font-size:13px;letter-spacing:3px;text-transform:uppercase}
  .sec{display:flex;flex-direction:column;gap:9px}
  .sec h3{margin:0 0 2px;font-size:12px;letter-spacing:2.5px;text-transform:uppercase;color:#94a3b8}
  .opts{display:flex;gap:6px;flex-wrap:wrap}
  .btn{font-family:inherit;font-size:14px;font-weight:700;color:#e2e8f0;background:linear-gradient(180deg,rgba(255,255,255,.12),rgba(255,255,255,.03));
    border:1px solid var(--edge);border-radius:10px;padding:9px 14px;cursor:pointer;transition:transform .12s,background .15s,box-shadow .15s,border-color .15s;position:relative}
  .btn:hover{transform:translateY(-2px);border-color:rgba(255,209,102,.6);box-shadow:0 6px 16px rgba(255,209,102,.15)}
  .btn:active{transform:translateY(1px) scale(.97)}
  .btn.on{background:linear-gradient(180deg,#ffe08a,#f5a524);color:#1b1305;border-color:#ffd166;box-shadow:0 4px 14px rgba(245,165,36,.45)}
  .btn.primary{font-size:18px;padding:14px 22px;letter-spacing:2px;text-transform:uppercase;width:100%;
    background:linear-gradient(180deg,#7cf0a6,#22c55e);color:#052e16;border-color:#86efac;box-shadow:0 6px 20px rgba(34,197,94,.4)}
  .btn.primary:hover{box-shadow:0 10px 28px rgba(34,197,94,.6)}
  .teamrow{display:flex;align-items:center;gap:8px;padding:6px 8px;border-radius:10px;background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.06)}
  .chip{width:16px;height:16px;border-radius:50%;box-shadow:0 0 0 2px rgba(255,255,255,.15),0 0 12px currentColor}
  .teamrow span.lbl{flex:1;font-weight:700;font-size:14px}
  .teamrow .btn{padding:5px 10px;font-size:12px}
  .menu-foot{grid-column:1/-1;display:flex;flex-direction:column;gap:10px;margin-top:4px}
  .hint{text-align:center;color:#94a3b8;font-size:12px;letter-spacing:.5px}

  /* ---------- HUD ---------- */
  #hud{position:fixed;inset:0;pointer-events:none;z-index:10}
  #hud .pane{position:absolute;padding:10px 12px;pointer-events:none}
  #teams{left:14px;top:14px;width:250px;display:flex;flex-direction:column;gap:7px}
  .tbar{position:relative;padding:7px 10px 8px;border-radius:10px;background:rgba(10,14,28,.7);border:1px solid rgba(255,255,255,.08);overflow:hidden;transition:transform .3s,opacity .3s}
  .tbar.active{transform:translateX(6px);border-color:rgba(255,255,255,.4);box-shadow:0 0 0 1px rgba(255,255,255,.1),0 6px 18px rgba(0,0,0,.4)}
  .tbar .name{display:flex;justify-content:space-between;font-size:13px;font-weight:800;letter-spacing:.5px;text-shadow:0 1px 2px #000}
  .tbar .hpbg{margin-top:5px;height:8px;border-radius:6px;background:rgba(255,255,255,.1);overflow:hidden}
  .tbar .hpfg{height:100%;border-radius:6px;transition:width .5s cubic-bezier(.2,.8,.2,1)}
  .tbar .pips{display:flex;gap:4px;margin-top:5px}
  .tbar .pip{flex:1;height:4px;border-radius:2px;background:rgba(255,255,255,.12);transition:background .4s}
  .tbar.dead{opacity:.35;filter:grayscale(1)}

  #wind{right:14px;top:14px;width:230px;text-align:center;padding:12px 14px}
  #wind .t{font-size:11px;letter-spacing:3px;color:#94a3b8;text-transform:uppercase}
  #wind .v{font-size:20px;font-weight:900;margin:2px 0 6px;color:var(--gold)}
  #windbar{position:relative;height:16px;border-radius:8px;background:rgba(255,255,255,.07);overflow:hidden}
  #windbar .mid{position:absolute;left:50%;top:0;bottom:0;width:2px;background:rgba(255,255,255,.4)}
  #windfill{position:absolute;top:2px;bottom:2px;border-radius:6px;background:linear-gradient(90deg,#60a5fa,#e0f2fe);transition:all .4s}

  #timer{left:50%;top:12px;transform:translateX(-50%);padding:0;width:104px;height:104px;display:flex;align-items:center;justify-content:center;border-radius:50%}
  #timer svg{position:absolute;inset:0;width:100%;height:100%;transform:rotate(-90deg)}
  #timer .ring{transition:stroke-dashoffset .25s linear,stroke .3s}
  #timer .num{font-size:40px;font-weight:900;font-variant-numeric:tabular-nums;text-shadow:0 3px 8px rgba(0,0,0,.6);transition:transform .15s}
  #timer.danger{animation:pulse .5s ease-in-out infinite}
  #timer.panic{animation:pulse .25s ease-in-out infinite}
  #timer.danger .num{color:#fca5a5}
  @keyframes pulse{0%,100%{transform:translateX(-50%) scale(1)}50%{transform:translateX(-50%) scale(1.09)}}

  #banner{left:50%;top:132px;transform:translateX(-50%);padding:9px 22px;font-size:20px;font-weight:800;letter-spacing:1px;white-space:nowrap;
    transition:opacity .4s,transform .4s;opacity:0}
  #banner.show{opacity:1;transform:translateX(-50%) translateY(0)}
  #banner .chip{display:inline-block;vertical-align:middle;margin-right:8px}

  #unit{right:14px;bottom:96px;width:250px;padding:10px 12px}
  #unit .n{font-weight:900;font-size:16px;text-shadow:0 2px 4px #000}
  #unit .k{font-size:11px;color:#94a3b8;letter-spacing:1.5px;text-transform:uppercase}
  #unit .hp{height:10px;border-radius:6px;background:rgba(255,255,255,.1);margin-top:6px;overflow:hidden}
  #unit .hpf{height:100%;transition:width .3s}

  #weapons{position:absolute;left:50%;bottom:14px;transform:translateX(-50%);display:flex;gap:6px;padding:8px 10px;pointer-events:auto;max-width:98vw;flex-wrap:wrap;justify-content:center}
  .wbtn{pointer-events:auto;width:86px;padding:7px 4px 6px;border-radius:11px;border:1px solid rgba(255,255,255,.1);background:linear-gradient(180deg,rgba(255,255,255,.1),rgba(255,255,255,.02));
    color:#e2e8f0;font-family:inherit;cursor:pointer;text-align:center;transition:transform .12s,border-color .15s,background .15s;position:relative}
  .wbtn:hover{transform:translateY(-3px);border-color:rgba(255,255,255,.4)}
  .wbtn.sel{border-color:var(--gold);background:linear-gradient(180deg,rgba(255,209,102,.35),rgba(255,209,102,.08));box-shadow:0 0 16px rgba(255,209,102,.35)}
  .wbtn .ic{font-size:21px;line-height:1.1}
  .wbtn .nm{font-size:10.5px;font-weight:700;letter-spacing:.3px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
  .wbtn .am{position:absolute;top:3px;right:5px;font-size:10px;font-weight:900;color:var(--gold)}
  .wbtn .cat{position:absolute;left:0;right:0;bottom:0;height:3px;border-radius:0 0 10px 10px}
  .wbtn.empty{opacity:.35;filter:grayscale(1);cursor:not-allowed}
  .wsep{width:2px;background:rgba(255,255,255,.15);border-radius:2px;margin:0 4px}
  #legend{left:14px;bottom:14px;padding:9px 12px;font-size:11.5px;line-height:1.6;color:#cbd5e1;width:auto}
  #legend b{color:var(--gold)}
  #legend .cat-i{color:#93c5fd}#legend .cat-d{color:#fca5a5}#legend .cat-h{color:#fdba74}

  #power{pointer-events:none;position:absolute;width:120px;height:12px;border-radius:7px;background:rgba(0,0,0,.5);border:1px solid rgba(255,255,255,.3);overflow:hidden;display:none}
  #powerfill{height:100%;width:0;background:linear-gradient(90deg,#4ade80,#facc15,#f97316,#ef4444)}

  /* ---------- OVERLAYS ---------- */
  .overlay{position:fixed;inset:0;z-index:60;display:flex;align-items:center;justify-content:center;background:rgba(3,5,12,.6);backdrop-filter:blur(3px)}
  .overlay .card{padding:26px 30px;text-align:center;min-width:340px;animation:pop .45s cubic-bezier(.2,1.4,.4,1)}
  .overlay h2{margin:0 0 6px;font-size:40px;font-weight:900;letter-spacing:3px}
  .overlay p{color:#cbd5e1;margin:8px 0 16px}
  .overlay .stack{display:flex;flex-direction:column;gap:9px}
  @keyframes pop{from{transform:scale(.85) translateY(14px);opacity:0}to{transform:none;opacity:1}}
  #winBig{font-size:46px!important;text-shadow:0 0 30px currentColor}

  #tutorial{left:50%;top:50%;transform:translate(-50%,-50%);z-index:20;pointer-events:none;font-size:14px;color:#e2e8f0;padding:14px 18px;text-align:left;line-height:1.7;opacity:0;transition:opacity .6s}
  #tutorial.show{opacity:1}
  #tutorial b{color:var(--gold)}

  @media (max-width:820px){
    #menu .card{grid-template-columns:1fr}
    #teams{width:190px}#wind{width:170px}#legend{display:none}
    #unit{display:none}
  }
</style>
</head>
<body>
<canvas id="game"></canvas>

<!-- MENU -->
<div id="menu">
  <div class="glass card">
    <h1>CRATERFALL</h1>
    <div class="sub">Artillery · Destruction · Mayhem</div>

    <div class="sec">
      <h3>Teams</h3>
      <div class="opts" id="teamCountOpts"></div>
      <div id="teamRows" style="display:flex;flex-direction:column;gap:6px"></div>
    </div>

    <div class="sec">
      <h3>Terrain</h3>
      <div class="opts" id="terrainOpts"></div>
      <h3 style="margin-top:8px">Wind</h3>
      <div class="opts" id="windOpts"></div>
      <h3 style="margin-top:8px">Turn Timer</h3>
      <div class="opts" id="timerOpts"></div>
    </div>

    <div class="menu-foot">
      <button class="btn primary" id="startBtn">▶ Start Battle</button>
      <div class="hint">← → move · ↑ ↓ aim · hold SPACE to charge & fire · J jump · W backflip · Q/E weapons · G rope · ESC pause</div>
    </div>
  </div>
</div>

<!-- HUD -->
<div id="hud" class="hidden">
  <div id="teams" class="pane"></div>
  <div id="wind" class="pane glass">
    <div class="t">Wind</div>
    <div class="v" id="windVal">0</div>
    <div id="windbar"><div class="mid"></div><div id="windfill"></div></div>
  </div>
  <div id="timer" class="pane glass">
    <svg viewBox="0 0 100 100">
      <circle cx="50" cy="50" r="44" fill="rgba(5,8,18,.6)" stroke="rgba(255,255,255,.12)" stroke-width="8"/>
      <circle class="ring" id="timerRing" cx="50" cy="50" r="44" fill="none" stroke="#4ade80" stroke-width="8" stroke-linecap="round" stroke-dasharray="276.46" stroke-dashoffset="0"/>
    </svg>
    <div class="num" id="timerNum">30</div>
  </div>
  <div id="banner" class="pane glass"></div>
  <div id="unit" class="pane glass">
    <div class="k">Active unit</div>
    <div class="n" id="unitName">—</div>
    <div class="hp"><div class="hpf" id="unitHp"></div></div>
  </div>
  <div id="legend" class="pane glass">
    <b>Controls</b><br>
    ← → walk · ↑ ↓ aim · J jump · W backflip<br>
    SPACE hold &amp; release · Q/E or 1–8 weapon<br>
    G ninja rope · Enter end turn · ESC pause<br>
    <span class="cat-i">■ Indirect</span> &nbsp;<span class="cat-d">■ Direct</span> &nbsp;<span class="cat-h">■ Map-altering</span>
  </div>
  <div id="weapons" class="glass"></div>
  <div id="power"><div id="powerfill"></div></div>
  <div id="tutorial" class="glass"></div>
</div>

<!-- PAUSE -->
<div id="pause" class="overlay hidden">
  <div class="glass card">
    <h2>Paused</h2>
    <p>Breathe. Plan. Blow something up.</p>
    <div class="stack">
      <button class="btn primary" id="resumeBtn">Resume</button>
      <button class="btn" id="pauseMenuBtn">Back to Menu</button>
    </div>
  </div>
</div>

<!-- GAME OVER -->
<div id="gameover" class="overlay hidden">
  <div class="glass card">
    <h2 id="winBig">VICTORY</h2>
    <p id="winText">—</p>
    <div class="stack">
      <button class="btn primary" id="rematchBtn">Rematch</button>
      <button class="btn" id="goMenuBtn">Main Menu</button>
    </div>
  </div>
</div>
<script>
/* ================= CRATERFALL ================= */
const $=id=>document.getElementById(id);
const cv=$('game'), ctx=cv.getContext('2d');
const WW=1600, WH=900, WATER=700, GRAV=900, WALK=70, MOVE_MAX=260, WIND_ACC=130;
let VW=1,VH=1,DPR=1;
const rnd=(a,b)=>a+Math.random()*(b-a);
const clamp=(v,a,b)=>v<a?a:v>b?b:v;
const lerp=(a,b,t)=>a+(b-a)*t;
const TEAM_COL=['#ff5d73','#4cc9f0','#9ef01a','#ffbe0b'];
const TEAM_DARK=['#8a1d33','#1d6f91','#4c7a0a','#8a5f00'];
const TEAM_NAMES=['Crimson Crew','Azure Squad','Lime Legion','Amber Army'];
const UNIT_NAMES=['Boris','Sprocket','Nibbles','Gus','Mabel','Taco','Wrench','Pickle','Zoey','Bramble','Otto','Juniper'];

/* ================= AUDIO ================= */
let AC=null, MASTER=null, NOISEBUF=null, sfxOn=true;
function actx(){
  if(!AC){ AC=new (window.AudioContext||window.webkitAudioContext)(); MASTER=AC.createGain(); MASTER.gain.value=0.6;
    const comp=AC.createDynamicsCompressor(); MASTER.connect(comp); comp.connect(AC.destination); }
  if(AC.state==='suspended') AC.resume();
  return AC;
}
function tone(f,d=0.12,type='square',v=0.15,slide=0,delay=0){
  if(!sfxOn) return; const a=actx(); const t=a.currentTime+delay;
  const o=a.createOscillator(), g=a.createGain();
  o.type=type; o.frequency.setValueAtTime(f,t);
  if(slide) o.frequency.exponentialRampToValueAtTime(Math.max(30,f+slide),t+d);
  g.gain.setValueAtTime(0.0001,t); g.gain.exponentialRampToValueAtTime(v,t+0.008); g.gain.exponentialRampToValueAtTime(0.0001,t+d);
  o.connect(g); g.connect(MASTER); o.start(t); o.stop(t+d+0.05);
}
function noise(d=0.3,v=0.5,freq=900,type='lowpass',delay=0){
  if(!sfxOn) return; const a=actx();
  if(!NOISEBUF){ NOISEBUF=a.createBuffer(1,(a.sampleRate*1.5)|0,a.sampleRate); const ch=NOISEBUF.getChannelData(0);
    for(let i=0;i<ch.length;i++) ch[i]=Math.random()*2-1; }
  const t=a.currentTime+delay; const s=a.createBufferSource(); s.buffer=NOISEBUF;
  const f=a.createBiquadFilter(); f.type=type; f.frequency.value=freq; f.Q.value=0.8;
  const g=a.createGain(); g.gain.setValueAtTime(v,t); g.gain.exponentialRampToValueAtTime(0.0001,t+d);
  s.connect(f); f.connect(g); g.connect(MASTER); s.start(t,Math.random()*0.5); s.stop(t+d+0.05);
}
const SFX={
  hover(){tone(880,0.04,'sine',0.05)},
  click(){tone(520,0.07,'triangle',0.2,260); tone(780,0.05,'sine',0.08,0,0.03)},
  select(){tone(660,0.05,'triangle',0.15); tone(990,0.08,'triangle',0.12,0,0.04)},
  start(){[523,659,784,1046].forEach((f,i)=>tone(f,0.12,'square',0.1,0,i*0.07))},
  jump(){tone(260,0.3,'sine',0.25,500)},
  backflip(){tone(300,0.4,'sine',0.25,700); tone(520,0.2,'triangle',0.1,300,0.1)},
  land(){noise(0.08,0.3,400,'lowpass'); tone(120,0.08,'sine',0.25,-40)},
  squelch(){tone(220,0.15,'sawtooth',0.12,-120); noise(0.1,0.2,500)},
  fire(){noise(0.25,0.5,2500,'highpass'); tone(190,0.2,'sawtooth',0.12,-120)},
  bounce(){tone(520,0.06,'triangle',0.15,-220)},
  explode(r=40){noise(0.35+r/300,0.9,Math.max(300,1400-r*8),'lowpass'); tone(110,0.5,'sine',0.5,-70); noise(0.12,0.4,4000,'highpass')},
  splash(){noise(0.5,0.45,1400,'bandpass'); [0,0.09,0.2,0.33].forEach(d=>tone(300+Math.random()*700,0.1,'sine',0.12,300,d))},
  hit(){tone(700,0.12,'sine',0.2,500); tone(1100,0.08,'triangle',0.1,300,0.05)},
  bat(){tone(220,0.1,'square',0.3,-160); noise(0.08,0.5,2000,'highpass')},
  tick(f=1200){tone(f,0.03,'square',0.07)},
  crate(){[659,784,988,1318].forEach((f,i)=>tone(f,0.1,'triangle',0.15,0,i*0.06))},
  death(){tone(400,0.7,'sawtooth',0.14,-300); tone(300,0.7,'triangle',0.12,-200,0.25)},
  bubble(){tone(300+Math.random()*600,0.12,'sine',0.12,400)},
  rope(){tone(380,0.35,'triangle',0.12,300)},
  whistle(){tone(1400,0.9,'sine',0.1,-1000)},
  beep(){tone(1000,0.05,'square',0.12)},
  cluster(){[0,0.05,0.1,0.15,0.2].forEach(d=>tone(800+Math.random()*600,0.06,'triangle',0.1,0,d))},
  win(){[523,659,784,659,784,1046].forEach((f,i)=>tone(f,0.18,'square',0.12,0,i*0.12))},
  lose(){[392,349,330,262].forEach((f,i)=>tone(f,0.25,'triangle',0.15,0,i*0.2))},
  shotgun(){noise(0.2,0.7,3000,'highpass'); noise(0.3,0.5,600,'lowpass',0.02)},
  sniper(){tone(1800,0.25,'sawtooth',0.1,-1400); noise(0.1,0.5,5000,'highpass')},
  ring(){[400,500,620].forEach((f,i)=>tone(f,0.1,'sine',0.15,0,i*0.05))},
  dynaBeep(){tone(1400,0.04,'square',0.1)}
};
function sfx(n,...a){ if(SFX[n]) { try{SFX[n](...a);}catch(e){} } }

/* ================= THEMES ================= */
const THEMES={
  island:{sky:['#1b2a4a','#f08a5d','#ffd29a'],far:'#3b4f7a',near:'#2a3d63',pal:{g:[120,205,85],d:[150,104,64],r:[104,92,88]},moon:true},
  twin:{sky:['#24163d','#e35d6a','#ffc27a'],far:'#5b2f5e',near:'#3a1f47',pal:{g:[140,210,90],d:[176,116,72],r:[120,100,92]},moon:false},
  cave:{sky:['#07050d','#1a1030','#2b1a40'],far:'#1b1229',near:'#120b1d',pal:{g:[70,180,130],d:[92,74,104],r:[60,50,78]},moon:false}
};
let theme=THEMES.island;

/* ================= WORLD STATE ================= */
let solid=null, tp=null, tCanvas=null, tctx=null, tImg=null, tBuf=null;
let units=[], teams=[], projs=[], chunks=[], parts=[], texts=[], crates=[], beams=[], marks=[];
let wind=0, turnTeam=-1, active=null, timeLeft=30, turnTime=30, paused=false;
let state='menu', sel=0, charge=0, charging=false, cpuPlan=null, cpuT=0, settleT=0, turnTimerTick=0;
let shake=0, hitstop=0, cam={x:WW/2,y:WH/2,z:1}, camZoomT=1, gameT=0, skyCanvas=null, teamSel=[];
let keys={}, mouseWorld=null;

/* ================= TERRAIN ================= */
function surfFn(type,ph){
  const base=WATER+70;
  const n=x=>14*Math.sin(x*0.011+ph[0])+7*Math.sin(x*0.031+ph[1])+3*Math.sin(x*0.083+ph[2]);
  const gs=(x,c,w,p)=>Math.exp(-Math.pow(Math.abs(x-c)/w,p));
  return x=>{
    let y=base;
    if(type==='island') y-=330*gs(x,WW/2+(ph[3]||0),250,2.2);
    else if(type==='twin') y-=300*Math.max(gs(x,WW*0.28,185,2.2),gs(x,WW*0.72,185,2.2));
    else if(type==='cave') y-=310*gs(x,WW/2,520,2);
    const amp=clamp((base-y)/120+0.25,0,1);
    return y+(x>10&&x<WW-10?n(x)*amp:0);
  };
}
function carveDisk(cx,cy,r){
  const x0=Math.max(0,Math.floor(cx-r)),x1=Math.min(WW-1,Math.ceil(cx+r));
  const y0=Math.max(0,Math.floor(cy-r)),y1=Math.min(WH-1,Math.ceil(cy+r));
  for(let y=y0;y<=y1;y++)for(let x=x0;x<=x1;x++){const dx=x-cx,dy=y-cy;if(dx*dx+dy*dy<=r*r)solid[y*WW+x]=0;}
}
function tunnel(x0,y0,x1,y1,r){
  const L=Math.hypot(x1-x0,y1-y0),n=Math.ceil(L/4);
  for(let i=0;i<=n;i++){const t=i/n;carveDisk(lerp(x0,x1,t),lerp(y0,y1,t),r);}
}
function genTerrain(type){
  const ph=[rnd(0,6),rnd(0,6),rnd(0,6),0];
  const sf=surfFn(type,ph);
  solid=new Uint8Array(WW*WH);
  for(let x=0;x<WW;x++){const s=Math.round(sf(x));for(let y=Math.max(0,s);y<WH;y++)solid[y*WW+x]=1;}
  if(type==='cave'){
    const cx=WW/2+rnd(-70,70), cy=sf(cx)+175;
    carveDisk(cx,cy,130);
    tunnel(cx,cy,cx-270,sf(cx-270)-14,26);
    tunnel(cx,cy,cx+260,sf(cx+260)-14,26);
    carveDisk(WW*0.2,WATER-70,62);
    carveDisk(WW*0.8,WATER-130,50);
    tunnel(WW*0.2,WATER-70,WW*0.2+40,sf(WW*0.2+40)-2,20);
  }
  tp=new Int16Array(WW);
  for(let x=0;x<WW;x++){let y=0;while(y<WH&&!solid[y*WW+x])y++;tp[x]=y;}
  tCanvas=document.createElement('canvas'); tCanvas.width=WW; tCanvas.height=WH;
  tctx=tCanvas.getContext('2d'); tImg=tctx.createImageData(WW,WH); tBuf=new Uint32Array(tImg.data.buffer);
  renderRegion(0,0,WW-1,WH-1); tctx.putImageData(tImg,0,0);
}
function pixel(x,y){
  const i=y*WW+x; if(!solid[i]) return 0;
  const depth=y-tp[x]; const p=theme.pal;
  const h=((Math.imul(x,73856093)^Math.imul(y,19349663))>>>0 & 255)/255;
  let c;
  if(depth<7) c=p.g.map(v=>v*(0.92+h*0.16)); // grass
  else if(depth<34) { const m=(h-0.5)*18; c=p.d.map(v=>v+m); }
  else { const f=1-Math.min(0.45,depth/500); c=p.r.map(v=>v*f*(0.9+h*0.2)); }
  if(depth<2) c=c.map(v=>Math.min(255,v+40));
  return (255<<24)|((c[2]&255)<<16)|((c[1]&255)<<8)|(c[0]&255);
}
function renderRegion(x0,y0,x1,y1){
  x0=clamp(x0,0,WW-1);x1=clamp(x1,0,WW-1);y0=clamp(y0,0,WH-1);y1=clamp(y1,0,WH-1);
  for(let y=y0;y<=y1;y++)for(let x=x0;x<=x1;x++)tBuf[y*WW+x]=pixel(x,y);
  tctx.putImageData(tImg,0,0,x0,y0,x1-x0+1,y1-y0+1);
}
function computeTop(){for(let x=0;x<WW;x++){let y=0;while(y<WH&&!solid[y*WW+x])y++;tp[x]=y;}}
function sol(x,y){
  x=Math.round(x);y=Math.round(y);
  if(x<0||x>=WW||y>=WH) return 1;
  if(y<0) return 0;
  return solid[y*WW+x];
}
function inWaterY(y){return y>WATER;}

/* carve with optional physical debris */
function carve(cx,cy,r,debris=true){
  const x0=clamp(Math.floor(cx-r-2),0,WW-1),x1=clamp(Math.ceil(cx+r+2),0,WW-1);
  const y0=clamp(Math.floor(cy-r-2),0,WH-1),y1=clamp(Math.ceil(cy+r+2),0,WH-1);
  const r2=r*r;
  for(let y=y0;y<=y1;y++)for(let x=x0;x<=x1;x++){
    const dx=x-cx,dy=y-cy; if(dx*dx+dy*dy>r2) continue;
    const i=y*WW+x; if(!solid[i]) continue;
    if(debris && chunks.length<170 && Math.random()<0.02 && (y<=tp[x]+40)){
      const c=tBuf[i]; spawnChunk(x,y,c,dx,dy,r);
    }
    solid[i]=0;
  }
  for(let x=x0;x<=x1;x++){
    if(tp[x]>=y0&&tp[x]<=y1){let y=tp[x];while(y<WH&&!solid[y*WW+x])y++;tp[x]=y;}
  }
  renderRegion(x0,y0,x1,clamp(y1+40,0,WH-1));
}
function spawnChunk(x,y,c,dx,dy,r){
  const d=Math.hypot(dx,dy)||1;
  const col=`rgb(${c&255},${(c>>8)&255},${(c>>16)&255})`;
  const sp=rnd(60,200)*(r/60+0.5);
  chunks.push({x:x+0.5,y:y+0.5,vx:dx/d*sp*0.6+rnd(-60,60),vy:dy/d*sp*0.6-rnd(0,120),s:rnd(2,5),c:col,rot:rnd(0,6),vr:rnd(-8,8),t:0,landed:false});
}
function updChunks(dt){
  for(let i=chunks.length-1;i>=0;i--){
    const c=chunks[i]; c.t+=dt;
    if(!c.landed){
      c.vy+=GRAV*0.9*dt;
      if(inWaterY(c.y)){c.vy*=1-2.5*dt;c.vx*=1-2*dt;c.vy-=180*dt;}
      const nx=c.x+c.vx*dt; if(!sol(nx,c.y)) c.x=nx; else c.vx*=-0.3;
      const ny=c.y+c.vy*dt;
      if(!sol(c.x,ny)) c.y=ny;
      else{
        if(Math.abs(c.vy)>120){ c.vy*=-0.38; c.vx*=0.7; c.vr*=0.5; sfx('bounce'); dust(c.x,c.y,2,c.c,1); }
        else{ c.vy=0;c.vx*=0.4;c.vr=0;c.landed=true; }
      }
      c.rot+=c.vr*dt;
    } else if(Math.random()<dt*2){
      if(!sol(c.x,c.y+c.s+1)) c.landed=false;
    }
    if(c.t>7||(c.landed&&c.t>4)) chunks.splice(i,1);
  }
}
/* ---------- CONFIG & MENU ---------- */
const cfg={teams:2,cpu:[false,true,true,true],terrain:'island',wind:'normal',timer:30};
const TERRAIN_OPTS=[['island','Single Isle'],['twin','Twin Isles'],['cave','Cave Realm']];
const WIND_OPTS=[['calm','Calm'],['normal','Breezy'],['storm','Stormy']];
const TIMER_OPTS=[[15,'15s'],[30,'30s'],[45,'45s']];
function mkBtn(parent,label,on,fn,cls=''){
  const b=document.createElement('button');
  b.className='btn '+cls+(on?' on':''); b.textContent=label;
  b.onmouseenter=()=>sfx('hover');
  b.onclick=()=>{sfx('select');fn(b);};
  parent.appendChild(b); return b;
}
function buildMenu(){
  const tc=$('teamCountOpts'); tc.innerHTML='';
  [2,3,4].forEach(n=>mkBtn(tc,n+' Teams',cfg.teams===n,()=>{cfg.teams=n;buildMenu();}));
  const tr=$('teamRows'); tr.innerHTML='';
  for(let t=0;t<cfg.teams;t++){
    const row=document.createElement('div'); row.className='teamrow';
    row.innerHTML=`<div class="chip" style="background:${TEAM_COL[t]};color:${TEAM_COL[t]}"></div><span class="lbl">${TEAM_NAMES[t]}</span>`;
    mkBtn(row,cfg.cpu[t]?'CPU':'Human',false,()=>{cfg.cpu[t]=!cfg.cpu[t];buildMenu();});
    tr.appendChild(row);
  }
  const to=$('terrainOpts'); to.innerHTML='';
  TERRAIN_OPTS.forEach(([k,l])=>mkBtn(to,l,cfg.terrain===k,()=>{cfg.terrain=k;buildMenu();}));
  const wo=$('windOpts'); wo.innerHTML='';
  WIND_OPTS.forEach(([k,l])=>mkBtn(wo,l,cfg.wind===k,()=>{cfg.wind=k;buildMenu();}));
  const ti=$('timerOpts'); ti.innerHTML='';
  TIMER_OPTS.forEach(([k,l])=>mkBtn(ti,l,cfg.timer===k,()=>{cfg.timer=k;buildMenu();}));
}
function shuffle(a){for(let i=a.length-1;i>0;i--){const j=(Math.random()*(i+1))|0;[a[i],a[j]]=[a[j],a[i]];}return a;}

/* ---------- WEAPONS ---------- */
const CAT={I:{name:'Indirect',col:'#93c5fd'},D:{name:'Direct',col:'#fca5a5'},H:{name:'Map-altering',col:'#fdba74'},U:{name:'Utility',col:'#86efac'}};
const WEAPONS=[
  {id:'bazooka', name:'Bazooka',  icon:'🚀', cat:'I', ammo:Infinity, dmg:50, r:40, g:0.55, speed:1.0, desc:'Arcing rocket. Boom on contact.'},
  {id:'grenade', name:'Grenade',  icon:'💣', cat:'I', ammo:3, dmg:55, r:44, g:1.0, speed:0.85, desc:'Bounces 3 times, 3s fuse.'},
  {id:'cluster', name:'Cluster',  icon:'🎆', cat:'I', ammo:2, dmg:30, r:24, g:1.0, speed:0.95, desc:'Splits into 5 bomblets at apex.'},
  {id:'sniper',  name:'Sniper',   icon:'🎯', cat:'D', ammo:3, dmg:60, r:0,  g:0,   speed:0,   desc:'Instant hitscan rifle.'},
  {id:'shotgun', name:'Shotgun',  icon:'🔫', cat:'D', ammo:4, dmg:18, r:0,  g:0,   speed:0,   desc:'Five instant pellets, short range.'},
  {id:'airstrike',name:'Airstrike',icon:'✈️', cat:'H', ammo:1, dmg:46, r:42, g:1.0, speed:0,   desc:'Plane carpet-bombs the target.'},
  {id:'drill',   name:'Drill',    icon:'⛏️', cat:'H', ammo:2, dmg:0,  r:0,  g:0,   speed:0,   desc:'Bores a tunnel straight ahead.'},
  {id:'meteor',  name:'Meteor',   icon:'☄️', cat:'H', ammo:1, dmg:85, r:100,g:0.9, speed:0,   desc:'Massive crater from orbit.'},
  {id:'rope',    name:'Ninja Rope',icon:'🪢',cat:'U', ammo:Infinity, dmg:0, r:0, g:0, speed:0, util:true, desc:'Grapple & swing. Space releases.'},
  {id:'teleport',name:'Teleport', icon:'🌀', cat:'U', ammo:1, dmg:0, r:0,  g:0,   speed:0, util:true, desc:'Warp along aim line. Ends turn.'},
  {id:'ring',    name:'Rubber Ring',icon:'🛟',cat:'U',ammo:1, dmg:0, r:0, g:0, speed:0, util:true, desc:'Float on water this turn. Free.'}
];
const WEP_MAX_SPEED=900;
let wepSel=0;

/* ---------- UNITS ---------- */
const UH=14; // unit height
function mkUnit(team,k,name){
  return {team,k,name,x:0,y:0,vx:0,vy:0,hp:100,maxhp:100,dir:1,aimA:0.6,
    grounded:true,fallY:null,dead:false,drown:0,float:false,rope:null,flash:0,bob:rnd(0,6),
    walkAnim:0,hopT:0,blink:rnd(1,3),blinkT:0,lastDmg:0,onWater:false,wasInWater:false,spd:0};
}
function slotXs(){
  const v=[];
  for(let x=40;x<WW-40;x+=2){ if(tp[x]<WATER-40 && Math.abs(tp[x]-tp[Math.min(WW-1,x+10)])<26 && Math.abs(tp[x]-tp[Math.max(0,x-10)])<26) v.push(x); }
  return v;
}
function spawnUnits(){
  units=[]; teams=[];
  for(let t=0;t<cfg.teams;t++) teams.push({id:t,name:TEAM_NAMES[t],col:TEAM_COL[t],dark:TEAM_DARK[t],cpu:cfg.cpu[t],
    units:[],ammo:WEAPONS.map(w=>w.ammo),alive:true,turnPtr:0,dmgDone:0});
  const names=shuffle(UNIT_NAMES.slice());
  const valid=slotXs();
  const N=cfg.teams*4;
  for(let s=0;s<N;s++){
    const k=Math.floor(s/cfg.teams), t=s%cfg.teams;
    const idx=Math.floor((s+0.5)/N*valid.length);
    const x=valid[Math.min(valid.length-1,idx)];
    const u=mkUnit(t,k,names[s%names.length]);
    u.x=x; u.y=tp[x]-1; u.dir=x<WW/2?1:-1; u.aimA=0.6;
    teams[t].units.push(u); units.push(u);
  }
}

/* ---------- PHYSICS HELPERS ---------- */
function dist(ax,ay,bx,by){return Math.hypot(ax-bx,ay-by);}
function unitCenter(u){return {x:u.x,y:u.y-UH/2-1};}
function aliveUnits(){return units.filter(u=>!u.dead);}
function nearestUnit(x,y,pred){
  let best=null,bd=1e9;
  for(const u of units){ if(u.dead||(pred&&!pred(u))) continue; const d=dist(u.x,u.y-7,x,y); if(d<bd){bd=d;best=u;} }
  return {u:best,d:bd};
}
function addText(x,y,text,col='#fff',size=18){ texts.push({x,y,text,col,size,t:0,life:1.1,vy:-60,vx:rnd(-12,12)}); }
function spark(x,y,n,col,spd=220,life=0.6,size=2.2){
  for(let i=0;i<n;i++){const a=rnd(0,Math.PI*2),s=rnd(0.2,1)*spd;
    parts.push({x,y,vx:Math.cos(a)*s,vy:Math.sin(a)*s,life:rnd(0.3,1)*life,max:life,col,size:rnd(size*0.6,size*1.3),g:520,type:'spark',drag:1.6});}
}
function smoke(x,y,n,spread=14,rise=40,size=10){
  for(let i=0;i<n;i++) parts.push({x:x+rnd(-spread,spread),y:y+rnd(-spread/2,spread/2),vx:rnd(-20,20),vy:-rnd(rise*0.5,rise*1.2),
    life:rnd(0.8,1.6),max:1.6,col:['#6b6b78','#4a4a57','#8a8493'][i%3],size:rnd(size*0.6,size*1.4),g:-10,type:'smoke',drag:0.8});
}
function dust(x,y,n,col='#c9a46b',power=1){
  for(let i=0;i<n;i++) parts.push({x:x+rnd(-4,4),y:y+rnd(-2,1),vx:rnd(-90,90)*power,vy:-rnd(20,140)*power,life:rnd(0.4,0.9),max:0.9,
    col:typeof col==='string'?col:'#c9a46b',size:rnd(2,4.5),g:420,type:'dust',drag:2});
}
function ring(x,y,r,col){parts.push({x,y,vx:0,vy:0,life:0.45,max:0.45,col,size:r,type:'ring',g:0,drag:0});}
function fireball(x,y,r){
  for(let i=0;i<Math.min(60,r*1.2);i++){const a=rnd(0,Math.PI*2),s=rnd(0,r*3);
    parts.push({x:x+Math.cos(a)*rnd(0,r*0.4),y:y+Math.sin(a)*rnd(0,r*0.4),vx:Math.cos(a)*s,vy:Math.sin(a)*s-30,life:rnd(0.4,0.9),max:0.9,
      col:['#fff7b0','#ffd166','#ff9f1c','#ff5d3a'][(Math.random()*4)|0],size:rnd(r*0.1,r*0.25),g:-40,type:'fire',drag:2.5});}
}
function damage(u,amt,why=''){
  if(u.dead||amt<=0) return;
  amt=Math.round(amt);
  u.hp=Math.max(0,u.hp-amt); u.flash=0.35; u.lastDmg=amt;
  addText(u.x,u.y-UH-20,'-'+amt,amt>=40?'#ff4d5e':'#ffd166',amt>=40?26:19);
  if(u.hp<=0) killUnit(u,why);
}
function heal(u,amt){
  if(u.dead) return; const before=u.hp; u.hp=Math.min(u.maxhp,u.hp+amt);
  addText(u.x,u.y-UH-20,'+'+(u.hp-before),'#4ade80',20); spark(u.x,u.y-8,14,'#4ade80',140,0.6);
}
function killUnit(u,why){
  if(u.dead) return; u.dead=true; u.hp=0; u.rope=null;
  const drowned=why==='drowned';
  if(drowned){ sfx('bubble'); for(let i=0;i<14;i++){ parts.push({x:u.x+rnd(-5,5),y:u.y-6+rnd(-4,4),vx:rnd(-8,8),vy:-rnd(40,80),life:1.2,max:1.2,col:'#bfe9ff',size:rnd(2,4),g:-30,type:'bubble',drag:0.5}); } }
  else { fireball(u.x,u.y-7,18); smoke(u.x,u.y-7,14,12,50,14); sfx('death'); shake=Math.max(shake,8); }
  for(let i=0;i<22;i++){ const c=TEAM_COL[u.team]; chunks.push({x:u.x,y:u.y-7,vx:rnd(-160,160),vy:rnd(-260,-60),s:rnd(2,4),c:i%2?c:'#f8f8f0',rot:0,vr:rnd(-10,10),t:0,landed:false}); }
  addText(u.x,u.y-UH-34,drowned?'DROWNED':'RIP',drowned?'#7dd3fc':'#fca5a5',20);
  const t=teams[u.team]; if(t.units.every(v=>v.dead)) t.alive=false;
  if(active===u) active=null;
  checkWinner();
}
/* ---------- UNIT PHYSICS ---------- */
function unitSolidAt(x,y){ return sol(x,y-1); }
function updUnit(u,dt){
  if(u.dead) return;
  u.flash=Math.max(0,u.flash-dt); u.blinkT+=dt; if(u.blinkT>u.blink){u.blinkT=0;u.blink=rnd(1.5,4);u.blinkAt=0.12;}
  if(u.blinkAt>0) u.blinkAt-=dt;
  if(u.rope){ updRope(u,dt); return; }
  const inW=u.y>WATER-2;
  // water: float or sink
  if(u.float && inW){ u.vx*=1-3*dt; u.vy=-clamp((u.y-(WATER+4))*3,-40,60); }
  else if(inW){ u.vy+=GRAV*0.25*dt; u.vy*=1-2*dt; u.vx*=1-3*dt; u.drown+=dt; if(u.drown>0.25){ u.drown=0; } }
  else { u.drown=0; u.vy+=GRAV*dt; }
  // horizontal (ground friction when grounded)
  if(u.grounded && Math.abs(u.vx)>0.01){ u.vx*=Math.pow(0.0008,dt); }
  if(!u.grounded && !inW) u.vx*=Math.pow(0.6,dt);
  // move with step-up collision
  let steps=Math.max(1,Math.ceil((Math.abs(u.vx)+Math.abs(u.vy))*dt/2));
  const sdt=dt/steps;
  for(let s=0;s<steps;s++){
    const nx=u.x+u.vx*sdt;
    if(!unitSolidAt(nx,u.y)||u.dead){ u.x=nx; }
    else {
      // try step up 6px
      let ok=false;
      for(let up=1;up<=6;up++){ if(!unitSolidAt(nx,u.y-up)){ u.x=nx;u.y-=up;ok=true;break; } }
      if(!ok){ if(u.grounded) {} u.vx*=-0.25; }
    }
    const ny=u.y+u.vy*sdt;
    if(u.vy>=0){
      if(sol(u.x,ny)){
        // landed
        const impact=u.vy; u.y=Math.floor(ny); while(sol(u.x,u.y)&&u.y>0) u.y--; // snap to surface
        if(!u.grounded && impact>0){ onLand(u,impact); }
        u.vy=0; u.grounded=true;
      } else { u.y=ny; u.grounded=false; }
    } else {
      if(sol(u.x,ny+UH)){ u.vy=0; } else u.y=ny;
    }
  }
  // step-down snap so walking downhill doesn't re-trigger landings
  if(u.grounded && !inW && !sol(u.x,u.y+1)){ for(let d=1;d<=6;d++){ if(sol(u.x,u.y+1+d)){ u.y+=d; break; } } }
  // grounded check
  if(!u.grounded && u.vy>=0 && sol(u.x,u.y+1)) { u.grounded=true; }
  if(u.grounded){
    if(sol(u.x,u.y+1)===0 && u.vy===0){ // fell off
      u.grounded=false; u.fallY=u.y;
    }
  }
  if(!u.grounded){ if(u.fallY===null) u.fallY=u.y; } else { u.fallY=null; }
  if(u.grounded && Math.abs(u.vx)>8) { u.walkAnim+=dt*Math.abs(u.vx)*0.08; }
  // world bounds
  if(u.x<6){u.x=6;u.vx=Math.abs(u.vx)*0.3;} if(u.x>WW-6){u.x=WW-6;u.vx=-Math.abs(u.vx)*0.3;}
  if(u.y>WH+20){ killUnit(u,'fell'); return; }
  // fallen into water -> drown check
  if(u.y>WATER+6 && !u.float){ /* keep sinking */ }
  if(u.y>WATER+UH+2 && !u.dead){ killUnit(u,'drowned'); return; }
  // water entry splash
  if(!u.wasInWater && u.y>WATER){ splash(u.x,WATER,u.vy); }
  if(u.wasInWater && u.y<=WATER-2 && !u.dead && u.vy<-20){ /* exit */ }
  u.wasInWater=u.y>WATER;
  if(u.grounded && u.vy===0 && u.fallY!==null){ u.fallY=null; }
  // slight wind push on airborne units
  if(!u.grounded && !inW){ u.vx+=wind*WIND_ACC*0.05*dt; }
  if(u.grounded && !inW){ u.vx+=0; }
}
function onLand(u,impact){
  const fall=(u.fallY!==null)?(u.y-u.fallY):0;
  const impactV=impact;
  if(impactV>260){ sfx('land'); dust(u.x,u.y,10,'#c9a46b',1.2); }
  else if(impactV>80){ sfx('land'); dust(u.x,u.y,5,'#c9a46b',0.6); }
  // fall damage
  const h=Math.max(0,impactV*impactV/(2*GRAV));
  if(h>44){ const dmg=Math.round((h-44)*0.55); if(dmg>0){ damage(u,dmg,'fall'); addText(u.x,u.y-UH-38,'OUCH','#fff',14); } dust(u.x,u.y,18,'#e7d3a4',1.5); smoke(u.x,u.y,6,8,20,8); shake=Math.max(shake,2.5); }
  if(u.y>WATER-2 && impactV>100) splash(u.x,WATER,impactV);
  u.lastFall=h;
}
function splash(x,y,v){
  sfx('splash'); const n=Math.round(clamp(Math.abs(v)/12,10,40));
  for(let i=0;i<n;i++){ parts.push({x:x+rnd(-14,14),y:y,vx:rnd(-80,80),vy:-rnd(60,240)*(0.4+Math.min(1,Math.abs(v)/400)),life:rnd(0.6,1.1),max:1.1,col:i%3?'#bfe9ff':'#7dd3fc',size:rnd(2,4.5),g:520,type:'bubble',drag:0.3}); }
  ring(x,y,22,'rgba(160,220,255,.7)');
}
/* rope physics: pivot-based swing */
function updRope(u,dt){
  const R=u.rope; R.t+=dt;
  if(!R.len) R.len=dist(u.x,u.y,R.px,R.py);
  // pendulum
  const dx=u.x-R.px, dy=u.y-R.py;
  const ang=Math.atan2(dx,dy);
  let aV=u.rav||0;
  aV+= (-GRAV/R.len*Math.sin(ang))*dt;
  aV*=Math.pow(0.995,dt*60);
  // pump (player holds left/right)
  if(keys.ArrowLeft) aV-=1.8*dt; if(keys.ArrowRight) aV+=1.8*dt;
  const na=ang+aV*dt*60*0.1;
  u.rav=aV;
  u.x=R.px+Math.sin(na)*R.len; u.y=R.py+Math.cos(na)*R.len;
  u.vx=0; u.vy=0; u.dir=u.x>R.px?1:-1; u.grounded=false;
  if(u.x<6) u.x=6; if(u.x>WW-6) u.x=WW-6;
  if(Math.random()<dt*20) parts.push({x:R.px,y:R.py,vx:0,vy:0,life:0.2,max:0.2,col:'rgba(255,255,255,.4)',size:1.2,g:0,type:'spark',drag:0});
}
/* ---------- PROJECTILES ---------- */
// projectile: {kind,x,y,vx,vy,t,wep,team,owner,bounces,fuse,trail:[]}
function fireProjectile(kind,x,y,vx,vy,opts={}){
  projs.push(Object.assign({kind,x,y,vx,vy,t:0,trail:[],bounces:0,fuse:0,owner:active,wep:WEAPONS[wepSel]},opts));
}
function updProjs(dt){
  for(let i=projs.length-1;i>=0;i--){
    const p=projs[i]; p.t+=dt;
    if(p.kind==='rocket'||p.kind==='grenade'||p.kind==='cluster'||p.kind==='meteor'||p.kind==='bomblet'){
      const g=p.kind==='meteor'?GRAV*1.6:GRAV;
      p.vy+=g*dt;
      p.vx+=wind*WIND_ACC*dt*(p.kind==='grenade'?0.8:1);
      const sub=Math.max(1,Math.ceil(Math.hypot(p.vx,p.vy)*dt/2));
      let hit=false;
      for(let s=0;s<sub&&!hit;s++){
        p.x+=p.vx*dt/sub; p.y+=p.vy*dt/sub;
        if(p.trail.length===0||dist(p.trail[p.trail.length-1].x,p.trail[p.trail.length-1].y,p.x,p.y)>5){ p.trail.push({x:p.x,y:p.y}); if(p.trail.length>26)p.trail.shift(); }
        if(p.kind!=='grenade' && (p.kind!=='bomblet')) { if(p.kind==='rocket'||p.kind==='cluster'||p.kind==='meteor'){ } }
        if(p.kind==='rocket'||p.kind==='cluster'||p.kind==='meteor'){ if(unitAt(p.x,p.y,p.owner) || sol(p.x,p.y)){ hit=true; } }
        else if(p.kind==='bomblet'){ if(sol(p.x,p.y)||unitAt(p.x,p.y,null)) hit=true; }
        if(p.y>WH+100||p.x<-200||p.x>WW+200){ hit=true; p.dead=true; }
        if(p.kind==='grenade'&&sol(p.x,p.y)){
          // bounce
          const nx=sol(p.x+p.vx*dt*2,p.y)?1:0, ny=sol(p.x,p.y+p.vy*dt*2)?1:0;
          if(nx) p.vx*=-0.55; if(ny||!nx){ if(p.vy>0) p.vy*=-0.5; }
          p.x-=p.vx*dt/sub; p.y-=p.vy*dt/sub;
          p.bounces++; sfx('bounce'); dust(p.x,p.y,4,'#c9a46b',0.8);
          if(p.bounces>3||Math.hypot(p.vx,p.vy)<22){ p.vx*=0.4; p.vy=0; p.fuse=Math.min(p.fuse,0.01); }
          hit=false; // keep going
          if(p.bounces>6){ hit=true; }
        }
      }
      if(p.kind==='grenade'){
        p.fuse+=dt; if(p.fuse>=3 && p.dead===undefined){ hit=true; }
        if(p.fuse>0.01 && Math.hypot(p.vx,p.vy)<25 && p.bounces>0){ p.fuseRest=(p.fuseRest||0)+dt; if(p.fuseRest>1.2) hit=true; }
      }
      if(p.kind==='cluster' && !p.split && p.vy>0 && p.t>0.2){ p.split=true; splitCluster(p); p.dead=true; }
      if(hit){ projs.splice(i,1); if(p.kind==='cluster' && p.split) continue; onProjBoom(p); }
    }
    else if(p.kind==='drill'){ /* handled as instant */ projs.splice(i,1); }
    else if(p.kind==='airbomb'){
      p.x+=p.vx*dt; p.t+=0; p.vy+=GRAV*0.6*dt; p.y+=p.vy*dt;
      if(sol(p.x,p.y)||p.y>WH){ projs.splice(i,1); explode(p.x,Math.min(p.y,WH-2),p.wep.r,p.wep.dmg,p.owner); }
    }
    else if(p.kind==='pellet'){ projs.splice(i,1); }
  }
}
function unitAt(x,y,except){
  for(const u of units){ if(u.dead||u===except) continue; if(Math.abs(u.x-x)<7 && y>u.y-UH-4 && y<u.y+2) return u; }
  return null;
}
function splitCluster(p){
  for(let i=0;i<5;i++){ const a=rnd(-0.9,0.9); const s=rnd(120,260);
    fireProjectile('bomblet',p.x,p.y,Math.sin(a)*s*0.8+p.vx*0.3,-Math.abs(Math.cos(a))*s*0.4+rnd(-40,20),{wep:WEAPONS[2],owner:p.owner,fuse:0,bounces:0});
  }
  sfx('cluster');
}
function onProjBoom(p){
  const w=p.wep||WEAPONS[1];
  if(p.kind==='bomblet'){ explode(p.x,p.y,w.r,w.dmg,p.owner); return; }
  if(p.kind==='cluster'){ explode(p.x,p.y,w.r,w.dmg,p.owner); return; }
  explode(p.x,p.y,w.r,w.dmg,p.owner);
}
/* ---------- EXPLOSION ---------- */
function explode(x,y,r,dmg,owner){
  if(r<=0) return;
  const big=r>=60;
  sfx('explode',r);
  carve(x,y,r,true);
  // damage units
  for(const u of units){
    if(u.dead) continue;
    const cy=u.y-7; const d=dist(x,y,u.x,cy);
    if(d<r+10){
      const f=clamp(1-d/(r+10),0,1);
      const dd=Math.round(dmg*(0.35+0.65*f));
      if(dd>0){ damage(u,dd,'blast'); }
      if(owner&&owner.team!==u.team) owner.dmgDone=(owner.dmgDone||0)+dd;
      const k=(r*2.6)*f; const dx=u.x-x, dy=(u.y-7)-y; const dl=Math.hypot(dx,dy)||1;
      u.vx+=dx/dl*k*0.9; u.vy+=dy/dl*k*1.1-70*f; u.grounded=false; u.fallY=u.fallY??u.y;
    }
  }
  // particles
  fireball(x,y,r*0.9);
  smoke(x,y,Math.round(r*0.6),r*0.6,40,r*0.28);
  for(let i=0;i<Math.round(r*0.8);i++){ const c=['#ffd166','#ff9f1c','#ff5d3a','#fff7b0'][i%4]; parts.push({x,y,vx:rnd(-1,1)*r*3,vy:rnd(-1,1)*r*3-40,life:rnd(0.3,0.7),max:0.7,col:c,size:rnd(1.5,3.5),g:500,type:'spark',drag:1.2}); }
  ring(x,y,r*1.15,'rgba(255,220,140,.9)');
  if(r>=24) ring(x,y,r*0.7,'rgba(255,255,255,.7)');
  // scorch debris for big
  if(big){ shake=Math.max(shake,r/12); hitstop=Math.max(hitstop,0.07+r/900); }
  // water hit
  if(y>WATER-r*0.3) splash(x,WATER,160);
  // flash
  flash=Math.max(flash,r/120);
  // dust
  dust(x,y,Math.round(r*0.4),'#c9a46b',1+r/120);
  checkCrates(x,y,r);
  addText(x,y-r-6,'','#fff',1); texts.pop();
}
let flash=0;
function checkCrates(){ }

/* ---------- RAYS ---------- */
function rayHit(x,y,dx,dy,maxd){
  for(let d=2; d<=maxd; d+=2){
    const px=x+dx*d, py=y+dy*d;
    if(py>WH) return {x:px,y:py,d,hit:'void'};
    if(sol(px,py)) return {x:px,y:py,d,hit:'ground'};
  }
  return {x:x+dx*maxd,y:y+dy*maxd,d:maxd,hit:null};
}
function groundAlong(u,dx,dy,maxd){ // point where aim ray meets terrain
  const r=rayHit(u.x+dx*9,u.y-UH/2-3+dy*9,dx,dy,maxd);
  return r;
}

/* ---------- TURN FLOW ---------- */
let turnPhase='control', turnFired=false, moveLeft=6, turnWaitT=0, bannerT=0, pending=[], gameEndT=-1, turnCount=0, crateCount=0;
let planes=[];
function fxLater(delay,fn){ pending.push({at:gameT+delay,fn}); }
function nextTurn(first=false){
  let nt=turnTeam;
  for(let k=1;k<=teams.length;k++){
    const c=(turnTeam+k+teams.length)%teams.length;
    if(teams[c].units.some(u=>!u.dead)){ nt=c; break; }
  }
  const t=teams[nt]; t.alive=true;
  const alive=t.units.filter(u=>!u.dead);
  const u=alive[t.turnPtr%alive.length]; t.turnPtr++;
  turnTeam=nt; active=u; turnCount++;
  timeLeft=turnTime; moveLeft=6; turnPhase='control'; turnFired=false;
  charging=false; charge=0; cpuPlan=null; cpuT=cfgCpuThink(t);
  // wind drifts each turn
  const mag={calm:0.15,normal:0.5,storm:1}[cfg.wind];
  wind=clamp(wind+rnd(-0.35,0.35)*mag, -mag*1.2, mag*1.2);
  if(!first && turnCount%3===0) spawnCrate();
  bannerT=2.2; showBanner(t,u);
  sfx('ring');
  for(const v of units) if(v.grounded) v.float=false;
  camTarget=null;
  if(!first) sfx('select');
  // water-proof reset for ring
  if(!first) for(const v of units) if(v.float===undefined) v.float=false;
}
function cfgCpuThink(t){ return t.cpu? 0.9 : 0; }
function showBanner(t,u){
  const b=$('banner'); b.innerHTML=`<span class="chip" style="background:${t.col};color:${t.col}"></span>${t.name.toUpperCase()} · <span style="color:${t.col}">${u.name}</span>`;
  b.classList.add('show');
  setTimeout(()=>b.classList.remove('show'),2000);
}
function settled(){
  if(projs.length||pending.length) return false;
  for(const u of units){ if(u.dead) continue; if(u.rope) return false; if(!(u.grounded||u.y>WATER-2)) return false; }
  return true;
}
function endTurnNow(){
  charging=false; charge=0;
  checkWinner();
  if(state!=='play') return;
  nextTurn(false);
}
function checkWinner(){
  if(state!=='play'||gameEndT>=0) return;
  const alive=teams.filter(t=>t.units.some(u=>!u.dead));
  if(alive.length<=1){ gameEndT=2.4; winner=alive[0]||null; }
}
let winner=null;

/* ---------- WEAPONS FIRE ---------- */
function fireWeapon(u,pw){
  const idx=wepSel, w=WEAPONS[idx], t=teams[u.team];
  if(t.ammo[idx]<=0) return;
  const dx=Math.cos(u.aimA)*u.dir, dy=-Math.sin(u.aimA);
  const mx=u.x+dx*9, my=u.y-UH/2-3+dy*9;
  const endsTurn=!(w.id==='rope'||w.id==='ring');
  if(w.id==='rope'){
    const r=rayHit(mx,my,dx,dy,700);
    if(r.hit!=='ground') { addText(u.x,u.y-UH-18,'MISS','#cbd5e1',14); sfx('click'); return; }
    if(t.ammo[idx]!==Infinity) t.ammo[idx]--;
    u.rope={px:r.x,py:r.y,len:0,t:0}; u.rav=0; sfx('rope');
    beams.push({x0:mx,y0:my,x1:r.x,y1:r.y,t:0,life:0.35,col:'#e2e8f0',rope:true});
    return;
  }
  if(w.id==='ring'){ if(t.ammo[idx]!==Infinity) t.ammo[idx]--; u.float=true; addText(u.x,u.y-UH-18,'FLOATIE!','#7dd3fc',18); sfx('ring'); return; }
  if(w.id==='teleport'){
    const r=rayHit(mx,my,dx,dy,900);
    if(r.hit!=='ground'){ sfx('click'); return; }
    if(t.ammo[idx]!==Infinity) t.ammo[idx]--;
    const px=r.x-dx*5, py=r.y-dy*5;
    spark(u.x,u.y-7,20,'#c4b5fd',200,0.6); parts.push({x:px,y:py,vx:0,vy:0,life:.45,max:.45,col:'#c4b5fd',size:26,type:'ring',g:0,drag:0});
    sfx('bubble'); u.x=clamp(px,8,WW-8); u.y=py; u.vx=0; u.vy=0; u.grounded=false; u.fallY=null;
    turnFired=true; turnPhase='wait'; turnWaitT=0; return;
  }
  if(t.ammo[idx]!==Infinity) t.ammo[idx]--;
  turnFired=true;
  if(w.id==='bazooka'||w.id==='grenade'||w.id==='cluster'){
    const sp=(190+pw*600);
    const kind=w.id==='bazooka'?'rocket':w.id;
    sfx('fire');
    fireProjectile(kind,mx,my,dx*sp,dy*sp,{wep:w,owner:u});
    smoke(mx,my,4,4,25,6);
  } else if(w.id==='meteor'||w.id==='airstrike'){
    const g=groundAlong(u,dx,dy,1000);
    const gx=clamp(g.x,30,WW-30);
    if(w.id==='meteor'){
      sfx('whistle');
      fxLater(0.6,()=>{ fireProjectile('meteor',gx,-120,0,260,{wep:w,owner:u}); });
      marks.push({x:gx,y:Math.max(0,tp[clamp(Math.round(gx),0,WW-1)]),t:0,life:1.2,col:'#ff5d3a'});
    } else {
      sfx('whistle');
      const dir=dx>=0?1:-1;
      planes.push({x:gx-520*dir,y:70,dir,t:0,drops:0,gx,life:2.2});
      for(let i=0;i<5;i++){
        fxLater(0.5+i*0.13,()=>{ fireProjectile('airbomb',gx+(i-2)*46,40,0,0,{wep:w,owner:u}); });
      }
    }
  } else if(w.id==='sniper'){
    sfx('sniper');
    const r=rayHit(mx,my,dx,dy,2200);
    let end={x:r.x,y:r.y}, hitU=null;
    for(let s=0;s<=r.d;s+=2){ const px=mx+dx*s, py=my+dy*s; const hu=unitAt(px,py,u); if(hu){hitU=hu;end={x:px,y:py};break;} }
    beams.push({x0:mx,y0:my,x1:end.x,y1:end.y,t:0,life:0.35,col:'#fff7b0',w:3});
    if(hitU){ damage(hitU,w.dmg,'blast'); hitU.vx+=dx*140; hitU.vy-=80; hitU.grounded=false; spark(end.x,end.y,26,'#fff7b0',260,0.5); shake=Math.max(shake,3); sfx('hit'); }
    else { spark(end.x,end.y,8,'#ffd166',160,0.3); }
    if(r.hit==='ground'&&!hitU) carve(end.x,end.y,5,true);
    smoke(mx,my,3,3,20,5);
  } else if(w.id==='shotgun'){
    sfx('shotgun');
    let total=0;
    for(let p=0;p<5;p++){
      const a=Math.atan2(dy,dx)+rnd(-0.1,0.1); const ddx=Math.cos(a), ddy=Math.sin(a);
      let end=null,hitU=null;
      for(let s=0;s<=340;s+=2){ const px=mx+ddx*s, py=my+ddy*s; const hu=unitAt(px,py,u); if(hu){hitU=hu;end={x:px,y:py};break;} if(sol(px,py)){end={x:px,y:py};break;} }
      if(!end) end={x:mx+ddx*340,y:my+ddy*340};
      beams.push({x0:mx,y0:my,x1:end.x,y1:end.y,t:0,life:0.2,col:'#ffe08a',w:1.2});
      if(hitU){ damage(hitU,w.dmg,'blast'); hitU.vx+=ddx*120; hitU.vy-=40; hitU.grounded=false; total++; spark(end.x,end.y,8,'#ff9f1c',180,0.35); }
      else if(sol(end.x,end.y)){ carve(end.x,end.y,4,false); dust(end.x,end.y,4,'#c9a46b',0.8); }
    }
    if(total) sfx('hit');
  } else if(w.id==='drill'){
    sfx('bat'); 
    const L=260, n=50;
    const ddx=u.dir; 
    for(let i=0;i<n;i++){
      const px=u.x+ddx*(i*L/n), py=u.y-UH/2;
      carve(px,py,13,true);
    }
    for(let i=0;i<=n;i++){ const px=u.x+ddx*(i*L/n); const py=u.y-UH/2; if(i%5===0) spark(px,py,3,'#c9a46b',120,0.4); }
    const ex=clamp(u.x+ddx*L,6,WW-6);
    beams.push({x0:u.x,y0:u.y-UH/2,x1:ex,y1:u.y-UH/2,t:0,life:0.35,col:'#c9a46b',w:12,fade:true});
    // unit pushed to the end of the bore
    fxLater(0.05,()=>{ u.x=ex; u.y=Math.min(u.y,tp[clamp(Math.round(ex),0,WW-1)]-1); u.vx=0; u.vy=0; u.grounded=false; });
    shake=Math.max(shake,4);
  }
  if(endsTurn){ turnPhase='wait'; turnWaitT=0; }
}

/* ---------- CONTROL (human/CPU share key-state path) ---------- */
let camTarget=null;
function updateControl(dt){
  const u=active; if(!u||u.dead||turnPhase!=='control') return;
  const t=teams[u.team];
  if(t.cpu){ updateCPU(u,dt); return; }
  // walk
  const left=keys.ArrowLeft, right=keys.ArrowRight;
  if(!u.rope){
    if(u.grounded){
      if(left!==right && moveLeft>0){
        u.dir=left?-1:1; u.vx=u.dir*96; moveLeft-=dt; u.walkAnim+=dt*8;
      }
    }
  }
  if(keys.ArrowUp) u.aimA=clamp(u.aimA+1.4*dt,-0.35,Math.PI/2+0.35);
  if(keys.ArrowDown) u.aimA=clamp(u.aimA-1.4*dt,-0.35,Math.PI/2+0.35);
  if(charging){ charge=clamp(charge+dt*0.85,0,1); }
  if(timeLeft<=0){ turnPhase='wait'; turnWaitT=0; timeoutFlag=true; }
}
let timeoutFlag=false;
function trajectoryPoints(u,pw,w){
  const dx=Math.cos(u.aimA)*u.dir, dy=-Math.sin(u.aimA);
  const sp=(190+pw*600)*(w.speed||1);
  let x=u.x+dx*9, y=u.y-UH/2-3+dy*9, vx=dx*sp, vy=dy*sp;
  const pts=[]; let t=0;
  const g=w.id==='meteor'?GRAV*1.6:GRAV;
  while(t<3.2){
    const dt=1/60; t+=dt;
    vy+=g*dt; vx+=wind*WIND_ACC*dt*(w.id==='grenade'?0.8:1);
    x+=vx*dt; y+=vy*dt;
    if(sol(x,y)||y>WH||x<0||x>WW) break;
    if(Math.round(t*60)%4===0) pts.push({x,y});
  }
  return {pts,end:{x,y}};
}
function fireHold(u){ // called on Space press
  const w=WEAPONS[wepSel], t=teams[u.team];
  if(w.id==='rope'&&u.rope){ releaseRope(u); return; }
  if(t.ammo[wepSel]<=0) { sfx('click'); return; }
  if(w.speed>0) { charging=true; charge=0.05; sfx('tick',600); }
  else fireWeapon(u,1);
}
function fireRelease(u){
  if(!charging) return;
  charging=false;
  const pw=clamp(charge,0.05,1);
  fireWeapon(u,pw);
  charge=0;
}
function releaseRope(u){
  const R=u.rope; if(!R) return;
  const a=Math.atan2(u.x-R.px,u.y-R.py);
  const v=(u.rav||0)*6*(R.len||1);
  u.vx=v*Math.cos(a)*1.0; u.vy=-v*Math.sin(a)*1.0;
  u.rope=null; u.grounded=false; u.fallY=u.y; sfx('bubble');
}
function jumpUnit(u,back){
  if(u.rope||!u.grounded) return;
  if(moveLeft<=0) return;
  if(back){ u.dir=-u.dir; u.vy=-330; u.vx=u.dir*120; moveLeft-=0.6; sfx('backflip'); }
  else { u.vy=-250; u.vx=u.dir*70; moveLeft-=0.5; sfx('jump'); }
  u.grounded=false; u.fallY=u.y;
  dust(u.x,u.y,6,'#c9a46b',0.8);
}
/* ---------- CPU: risk-assessment AI ---------- */
function simShot(u,pw,angle,wepIdx){
  // simulate trajectory and return landing point and damage score
  const w=WEAPONS[wepIdx];
  const dx=Math.cos(angle)*u.dir, dy=-Math.sin(angle);
  const sp=(190+pw*600)*(w.speed||1);
  let x=u.x+dx*9, y=u.y-UH/2-3+dy*9, vx=dx*sp, vy=dy*sp;
  const g=w.id==='meteor'?GRAV*1.6:GRAV;
  let t=0, bounce=0;
  while(t<4){
    const dt=1/60; t+=dt; vy+=g*dt; vx+=wind*WIND_ACC*dt*(w.id==='grenade'?0.8:1);
    x+=vx*dt; y+=vy*dt;
    if(x<0||x>WW||y>WH+50) return {x:clamp(x,0,WW),y:WH,score:-999,hit:false};
    if(w.id!=='grenade' && (sol(x,y)||unitAt(x,y,u))) break;
    if(w.id==='grenade' && sol(x,y)){ if(++bounce>=3) break; vy=-Math.abs(vy)*0.5; vx*=0.6; x-=vx*dt; y-=vy*dt; }
  }
  return {x,y,score:0,hit:true};
}
function scoreShot(u,land,w){
  const t=teams[u.team]; let s=0;
  const r=w.r||0;
  for(const v of units){
    if(v.dead) continue;
    const d=dist(land.x,land.y,v.x,v.y-7);
    const val=(v.team===u.team)?-1:1;
    const hp=v.hp;
    if(d<r+12){ const f=clamp(1-d/(r+12),0,1); const dmg=w.dmg*(0.35+0.65*f);
      const bonus=(hp-dmg<=0)?35:0; // kills are valuable
      s+=val*(Math.min(dmg,hp)+bonus); }
  }
  // self damage risk
  if(dist(land.x,land.y,u.x,u.y-7)<r+12) s-=w.dmg*1.6;
  // terrain/water hazard: don't blast self off cliffs or into water
  if(land.y>WATER) s-=0;
  return s;
}
function cpuChoosePlan(u){
  const t=teams[u.team];
  const opps=units.filter(v=>!v.dead&&v.team!==u.team);
  const cands=[];
  const ammoWeps=[0,1,2,3,4,5,6,7].filter(i=>t.ammo[i]>0);
  // Direct shots first (score by hitscan)
  for(const wi of ammoWeps){
    const w=WEAPONS[wi];
    if(w.id==='sniper'||w.id==='shotgun'){
      for(const v of opps){
        const dx=v.x-u.x; const dir=dx>=0?1:-1;
        const a=Math.atan2(v.y-7-(u.y-UH/2-3),Math.abs(dx))*0;
        // try angles to hit target via direct line
        const ang=Math.atan2((u.y-UH/2-3)-(v.y-7),Math.abs(dx)+1e-3);
        const sc=w.dmg*1.2+(v.hp<=w.dmg?30:0)+(v.team===u.team?-200:0);
        if(hasLine(u,v)) cands.push({wi,ang,pw:1,dir,score:sc,target:v,direct:true});
      }
    }
  }
  // Ballistic: sweep angle and power
  for(const wi of ammoWeps){
    const w=WEAPONS[wi];
    if(w.speed<=0) continue;
    for(let ang=0.1; ang<=1.45; ang+=0.09){
      for(let pw=0.25; pw<=1.0; pw+=0.07){
        const dirOk=true;
        const u2=Object.assign({},u,{aimA:ang});
        const land=simShot(u2,pw,ang,wi);
        if(!land.hit) continue;
        let sc=scoreShot(u,land,w);
        // hazard: penalise shots that drop into water after own unit reach
        if(sc<=0) continue;
        // prefer lower power (safer) when equal
        sc-=pw*2;
        cands.push({wi,ang,pw,score:sc,land,dir:u.dir,ballistic:true});
      }
    }
  }
  // Utility: airstrike/meteor/drill/teleport options
  if(t.ammo[5]>0){ let best=-1e9; for(const v of opps){ const sc=(scoreAir(v)); if(sc>best) best=sc; } if(best>0) cands.push({wi:5,ang:0,pw:0,score:best*0.9,utility:true,dir:u.dir}); }
  if(t.ammo[7]>0){ const sc=scoreAir(null,'meteor'); if(sc>0) cands.push({wi:7,ang:0,pw:0,score:sc*0.7,utility:true,dir:u.dir}); }
  cands.sort((a,b)=>b.score-a.score);
  return cands[0]||null;
}
function hasLine(u,v){
  const sx=u.x,sy=u.y-UH/2-3,ex=v.x,ey=v.y-7;
  const L=dist(sx,sy,ex,ey);
  for(let s=0;s<L;s+=3){ const px=sx+(ex-sx)*s/L, py=sy+(ey-sy)*s/L; if(sol(px,py)) return false; }
  return true;
}
function scoreAir(v,kind){
  if(kind==='meteor'){ let s=0; for(const o of units){ if(o.dead) continue; if(Math.abs(o.x-(v?v.x:0))<120) s+=(o.team===active.team?-40:40); } return s; }
  let s=0; for(const o of units){ if(o.dead) continue; if(Math.abs(o.x-v.x)<90) s+=(o.team===active.team?-40:40)+(o.hp<45?20:0); } return s;
}
/* hazard map: is walking to x safe? (no cliff drop > 90 or water) */
function cpuSafeStep(x,dir){
  const nx=clamp(Math.round(x+dir*12),2,WW-3);
  const drop=tp[nx]-tp[clamp(Math.round(x),0,WW-1)];
  if(tp[nx]>WATER-10) return false;     // water
  if(drop>60) return false;              // cliff
  return true;
}
function updateCPU(u,dt){
  const t=teams[u.team];
  if(cpuPlan===null){
    cpuT+=dt;
    if(cpuT<0.8){ return; }
    // plan: consider a retreat/hazard step first
    if(u.y>WATER-14 && !u.float){ cpuPlan={type:'escape'}; }
    else if(!cpuPlan){ const best=cpuChoosePlan(u); cpuPlan=best?{type:'fire',best}:{type:'skip'}; }
    cpuT=0;
    if(cpuPlan.type==='fire' && cpuPlan.best && cpuPlan.best.utility){ }
  }
  if(!cpuPlan) return;
  const p=cpuPlan;
  if(p.type==='escape'){
    if(!u.grounded){ return; }
    const left=cpuSafeStep(u.x,-1), right=cpuSafeStep(u.x,1);
    const toCenter=u.x<WW/2?1:-1; u.dir=toCenter; u.vx=u.dir*90;
    if(!left&&!right){ jumpUnit(u,false); }
    cpuPlan={type:'skip'}; cpuT=0; return;
  }
  if(p.type==='skip'){ cpuT+=dt; if(cpuT>0.4&&!turnFired){ turnPhase='wait'; turnWaitT=0; } return; }
  // fire plan
  const b=p.best;
  if(!b){ turnPhase='wait'; turnWaitT=0; cpuPlan=null; return; }
  cpuT+=dt;
  // aim step
  if(p.phase===undefined){ p.phase='aim'; p.tt=0; }
  if(p.phase==='aim'){
    if(b.direct || b.ballistic){
      const target=b.ang; const diff=target-u.aimA;
      u.aimA+=clamp(diff,-1.8*dt,1.8*dt);
      if(Math.abs(diff)<0.02){ p.phase='go'; }
    } else p.phase='go';
    // orient
    if(b.target){ u.dir=b.target.x>u.x?1:-1; }
    else if(b.ballistic) u.dir=u.dir;
    if(cpuT>5){p.phase='go';}
    return;
  }
  if(p.phase==='go'){
    p.tt+=dt;
    if(p.tt<0.4) return;
    wepSel=b.wi;
    if(WEAPONS[b.wi].speed>0){ charging=true; charge=0.05; const tgt=b.pw; const tk=setInterval(()=>{},0); clearInterval(tk); p.phase='charge'; p.tt=0; p.tgt=tgt; return; }
    fireWeapon(u,1); cpuPlan={type:'done'}; cpuT=0; return;
  }
  if(p.phase==='charge'){
    charge=clamp(charge+dt*0.85,0,1);
    if(charge>=p.tgt){ charging=false; fireWeapon(u,p.tgt); cpuPlan={type:'done'}; cpuT=0; }
  }
}
/* ---------- CRATES ---------- */
function spawnCrate(){
  const x=rnd(120,WW-120);
  const y=0;
  crates.push({x,y,vy:0,vx:0,kind:Math.random()<0.5?'health':'ammo',landed:false,t:0,bob:rnd(0,6)});
  addText(x,40,'SUPPLY DROP!','#86efac',20);
}
function updCrates(dt){
  for(let i=crates.length-1;i>=0;i--){
    const c=crates[i]; c.t+=dt; c.bob+=dt*3;
    if(!c.landed){
      c.vy+=GRAV*0.4*dt; c.y+=c.vy*dt;
      if(sol(c.x,c.y+8)){ c.landed=true; c.y=tp[clamp(Math.round(c.x),0,WW-1)]-8; sfx('bounce'); dust(c.x,c.y+8,8,'#c9a46b',1); }
      if(c.y>WH+30){ crates.splice(i,1); continue; }
    }
    for(const u of units){ if(u.dead) continue; if(Math.abs(u.x-c.x)<16&&Math.abs((u.y-7)-c.y)<22){
      if(c.kind==='health'){ heal(u,35); } else { const t=teams[u.team]; const cand=[1,2,3,4,6,8,9]; /* bonus ammo */ const k=[1,2,3,4][(Math.random()*4)|0]; t.ammo[k]+= (k===3?1:k===0?0:1); addText(u.x,u.y-UH-34,'+AMMO','#fde68a',18); }
      sfx('crate'); spark(c.x,c.y,24,'#fde68a',220,0.7); crates.splice(i,1); break; } }
  }
}
/* ---------- FLOW / INPUT / HUD ---------- */
let lastTick=-1;
function baseScale(){return Math.min(VW/1150,VH/640);}
function s2w(sx,sy){const s=baseScale()*cam.z;return{x:(sx-VW/2)/s+cam.x,y:(sy-VH/2)/s+cam.y};}
function w2s(x,y){const s=baseScale()*cam.z;return{x:VW/2+(x-cam.x)*s,y:VH/2+(y-cam.y)*s};}
function selectWeapon(i){ if(charging||state!=='play') return; if(wepSel!==i){ wepSel=i; sfx('click'); } updWeaponBar(); }
function isHuman(u){return !!u && !u.dead && !teams[u.team].cpu && turnPhase==='control' && state==='play';}
function togglePause(){
  if(state==='play'){ state='paused'; $('pause').classList.remove('hidden'); sfx('click'); }
  else if(state==='paused'){ state='play'; $('pause').classList.add('hidden'); sfx('select'); }
}
function showMenu(){
  state='menu';
  $('menu').classList.remove('hidden'); $('hud').classList.add('hidden');
  $('pause').classList.add('hidden'); $('gameover').classList.add('hidden');
  projs=[];parts=[];chunks=[];texts=[];crates=[];beams=[];planes=[];marks=[];pending=[];units=[];teams=[];active=null;
  theme=THEMES[cfg.terrain]; genTerrain(cfg.terrain); buildMenu();
  cam.x=WW/2; cam.y=WH/2; cam.z=1; gameEndT=-1; winner=null;
}
window.addEventListener('keydown',e=>{
  if(['Space','ArrowLeft','ArrowRight','ArrowUp','ArrowDown'].includes(e.code)){
    e.preventDefault();
    if(document.activeElement&&document.activeElement.blur) document.activeElement.blur();
  }
  if(e.code==='Escape'){ if(!e.repeat&&(state==='play'||state==='paused')) togglePause(); return; }
  if(state!=='play') return;
  keys[e.code]=true;
  if(e.repeat) return;
  const u=active, h=isHuman(u);
  if(e.code==='Space'&&h) fireHold(u);
  else if(e.code==='KeyJ'&&h) jumpUnit(u,false);
  else if(e.code==='KeyW'&&h) jumpUnit(u,true);
  else if(e.code==='Enter'&&h){ if(u.rope) releaseRope(u); charging=false; turnPhase='wait'; turnWaitT=0; }
  else if(e.code==='KeyQ') selectWeapon((wepSel+WEAPONS.length-1)%WEAPONS.length);
  else if(e.code==='KeyE') selectWeapon((wepSel+1)%WEAPONS.length);
  else if(e.code==='KeyG') selectWeapon(8);
  else if(/^Digit[1-9]$/.test(e.code)) selectWeapon(Math.min(WEAPONS.length-1,+e.code.slice(5)-1));
});
window.addEventListener('keyup',e=>{
  keys[e.code]=false;
  if(e.code==='Space'&&state==='play'&&active) fireRelease(active);
});
cv.addEventListener('mousemove',e=>{
  if(state!=='play'||!isHuman(active)||charging) return;
  const w=s2w(e.clientX,e.clientY), u=active;
  const dx=w.x-u.x, dy=(u.y-UH/2-3)-w.y;
  if(Math.hypot(dx,dy)>14 && !u.rope){
    if(Math.abs(dx)>4) u.dir=dx>=0?1:-1;
    u.aimA=clamp(Math.atan2(dy,Math.abs(dx)+0.001),-0.35,Math.PI/2+0.35);
  }
});
cv.addEventListener('mousedown',e=>{ if(e.button!==0||state!=='play') return; if(isHuman(active)) fireHold(active); });
window.addEventListener('mouseup',e=>{ if(e.button===0&&state==='play'&&active) fireRelease(active); });
function resize(){ VW=innerWidth; VH=innerHeight; DPR=Math.min(2,devicePixelRatio||1); cv.width=Math.round(VW*DPR); cv.height=Math.round(VH*DPR); }
window.addEventListener('resize',resize);

function buildHUD(){
  const T=$('teams'); T.innerHTML='';
  teams.forEach(t=>{
    const d=document.createElement('div'); d.className='tbar'; d.id='tb'+t.id;
    d.innerHTML=`<div class="name"><span style="color:${t.col}">${t.cpu?'🤖 ':''}${t.name}</span><span class="tot"></span></div>`+
      `<div class="hpbg"><div class="hpfg" style="background:linear-gradient(90deg,${t.dark},${t.col})"></div></div>`+
      `<div class="pips">${t.units.map(()=>'<div class="pip"></div>').join('')}</div>`;
    T.appendChild(d);
  });
  const W=$('weapons'); W.innerHTML='';
  WEAPONS.forEach((w,i)=>{
    const b=document.createElement('button'); b.className='wbtn'; b.id='wb'+i;
    b.innerHTML=`<div class="am"></div><div class="ic">${w.icon}</div><div class="nm">${w.name}</div><div class="cat" style="background:${CAT[w.cat].col}"></div>`;
    b.title=`[${i+1}] ${w.name} (${CAT[w.cat].name}) — ${w.desc}`;
    b.onmouseenter=()=>sfx('hover');
    b.onclick=()=>selectWeapon(i);
    W.appendChild(b);
  });
}
function updWeaponBar(){
  const tm=active?teams[active.team]:null;
  WEAPONS.forEach((w,i)=>{
    const b=$('wb'+i); if(!b) return;
    const a=tm?tm.ammo[i]:0;
    b.classList.toggle('sel',i===wepSel);
    b.classList.toggle('empty',a<=0&&w.ammo!==Infinity);
    b.querySelector('.am').textContent=w.ammo===Infinity?'∞':a;
  });
}
function startGame(){
  actx();
  theme=THEMES[cfg.terrain];
  turnTime=cfg.timer; timeLeft=turnTime;
  wind=cfg.wind==='calm'?rnd(-0.1,0.1):cfg.wind==='storm'?rnd(-0.9,0.9):rnd(-0.55,0.55);
  genTerrain(cfg.terrain);
  parts=[];projs=[];chunks=[];texts=[];crates=[];beams=[];marks=[];planes=[];pending=[];
  spawnUnits();
  wepSel=0; charge=0; charging=false; cpuPlan=null; cpuT=0; turnPhase='control';
  gameEndT=-1; winner=null; turnCount=0; turnTeam=-1; lastTick=-1; shake=0; flash=0; hitstop=0; camTarget=null;
  buildHUD();
  $('menu').classList.add('hidden'); $('hud').classList.remove('hidden');
  $('pause').classList.add('hidden'); $('gameover').classList.add('hidden');
  state='play';
  nextTurn(true);
  cam.x=active.x; cam.y=active.y; cam.z=1;
  sfx('start');
}
function nextTurnCheck(){ /* placeholder for hooks */ }
function updateTurnFlow(dt){
  if(gameEndT>=0) return;
  if(!active||active.dead){ if(turnPhase==='control'){ turnPhase='wait'; turnWaitT=0; } }
  if(turnPhase==='control'){
    timeLeft-=dt;
    if(timeLeft<=0){
      timeLeft=0; turnPhase='wait'; turnWaitT=0; charging=false;
      if(active) addText(active.x,active.y-UH-40,'TIME!','#fca5a5',22);
      sfx('beep');
      return;
    }
    updateControl(dt);
  } else if(turnPhase==='wait'){
    if(active&&active.rope) releaseRope(active);
    turnWaitT+=dt;
    if(turnWaitT>0.5&&(settled()||turnWaitT>14)) endTurnNow();
  }
}
function updateHUD(){
  teams.forEach(t=>{
    const d=$('tb'+t.id); if(!d) return;
    const hp=t.units.reduce((s,u)=>s+Math.max(0,u.hp),0), mx=t.units.length*100;
    d.querySelector('.hpfg').style.width=(hp/mx*100).toFixed(1)+'%';
    d.querySelector('.tot').textContent=hp+' HP';
    d.classList.toggle('dead',!t.units.some(u=>!u.dead));
    d.classList.toggle('active',turnTeam===t.id&&state==='play');
    const pips=d.querySelectorAll('.pip');
    t.units.forEach((u,i)=>{ pips[i].style.background=u.dead?'rgba(255,255,255,.06)':(u===active?'#fff':t.col); pips[i].style.boxShadow=u===active?'0 0 8px #fff':'none'; });
  });
  const pct=clamp(Math.abs(wind)/1.2,0,1)*50;
  $('windVal').textContent=Math.abs(wind)<0.03?'CALM':((wind>0?'→ ':'← ')+Math.round(Math.abs(wind)*50));
  const wf=$('windfill');
  wf.style.left=(wind>=0?50:50-pct)+'%'; wf.style.width=pct+'%';
  wf.style.background=wind>=0?'linear-gradient(90deg,#60a5fa,#e0f2fe)':'linear-gradient(90deg,#e0f2fe,#60a5fa)';
  const T=$('timer'), num=$('timerNum'), ringEl=$('timerRing');
  const live=state==='play'&&turnPhase==='control';
  const tl=Math.max(0,timeLeft);
  num.textContent=live?Math.ceil(tl):'';
  const frac=live?clamp(tl/turnTime,0,1):0;
  ringEl.style.strokeDashoffset=(276.46*(1-frac)).toFixed(2);
  ringEl.style.stroke=frac>0.5?'#4ade80':frac>0.25?'#facc15':'#ef4444';
  const hurry=live&&tl<=10;
  T.classList.toggle('danger',hurry&&tl>5); T.classList.toggle('panic',hurry&&tl<=5);
  num.style.transform=`scale(${(hurry?1+(1-tl/10)*0.35:1).toFixed(3)})`;
  num.style.color=hurry?'#fecaca':'#fff';
  if(live){
    const key=tl<=5?Math.floor(tl*2):(tl<=10?Math.ceil(tl):-1);
    if(key!==lastTick&&key>=0){ lastTick=key; sfx('tick',tl<=5?1600:1100); }
  }
  const un=$('unitName'), uh=$('unitHp');
  if(active&&!active.dead){
    const tm=teams[active.team];
    un.textContent=active.name+' · '+tm.name; un.style.color=tm.col;
    uh.style.width=active.hp+'%'; uh.style.background=`linear-gradient(90deg,${tm.dark},${tm.col})`;
  } else { un.textContent='—'; un.style.color='#fff'; uh.style.width='0%'; }
  updWeaponBar();
}
function update(dt){
  if(hitstop>0){ hitstop-=dt; dt*=0.12; }
  gameT+=dt;
  shake=Math.max(0,shake-dt*18);
  flash=Math.max(0,flash-dt*2.5);
  if(pending.length){
    const due=pending.filter(p=>gameT>=p.at);
    if(due.length){ pending=pending.filter(p=>gameT<p.at); due.forEach(p=>p.fn()); }
  }
  for(const u of units) updUnit(u,dt);
  updProjs(dt); updChunks(dt); updCrates(dt); updParts(dt); updTexts(dt);
  for(let i=beams.length-1;i>=0;i--){ beams[i].t+=dt; if(beams[i].t>beams[i].life) beams.splice(i,1); }
  for(let i=marks.length-1;i>=0;i--){ marks[i].t+=dt; if(marks[i].t>marks[i].life) marks.splice(i,1); }
  for(let i=planes.length-1;i>=0;i--){ const p=planes[i]; p.t+=dt; p.x+=p.dir*620*dt; if(p.t>p.life) planes.splice(i,1); }
  if(camTarget){ camTarget.t-=dt; if(camTarget.t<=0) camTarget=null; }
  if(state==='play') updateTurnFlow(dt);
  if(gameEndT>=0){ gameEndT-=dt; if(gameEndT<0) showGameOver(); }
  updateCamera(dt);
  updateHUD();
}
function updParts(dt){
  for(let i=parts.length-1;i>=0;i--){
    const p=parts[i]; p.life-=dt;
    if(p.life<=0){ parts.splice(i,1); continue; }
    if(p.type!=='ring'){
      const k=Math.exp(-(p.drag||0)*dt); p.vx*=k; p.vy*=k; p.vy+=(p.g||0)*dt;
      p.x+=p.vx*dt; p.y+=p.vy*dt;
    }
  }
  if(parts.length>1400) parts.splice(0,parts.length-1400);
}
function updTexts(dt){
  for(let i=texts.length-1;i>=0;i--){ const t=texts[i]; t.t+=dt; t.y+=t.vy*dt; t.vy*=Math.pow(0.5,dt); if(t.t>t.life) texts.splice(i,1); }
}
function updateCamera(dt){
  let fx,fy,tz=1;
  if(camTarget){ fx=camTarget.x; fy=camTarget.y; tz=0.95; }
  else if(projs.length){ const p=projs[projs.length-1]; fx=p.x; fy=p.y; tz=0.85; }
  else if(active&&!active.dead){ fx=active.x; fy=active.y-12; tz=charging?1.18:1.0; }
  else { fx=WW/2; fy=WH/2; tz=0.9; }
  if(state==='over'){ fx=WW/2; fy=WH/2; tz=0.82; }
  const k=1-Math.exp(-dt*4.5), kz=1-Math.exp(-dt*2.2);
  cam.x+=(fx-cam.x)*k; cam.y+=(fy-cam.y)*k; cam.z+=(tz-cam.z)*kz;
  cam.x=clamp(cam.x,0,WW); cam.y=clamp(cam.y,0,WH);
}
function showGameOver(){
  state='over';
  const w=winner, big=$('winBig'), txt=$('winText');
  const dmg=teams.map(t=>`${t.name} ${t.dmgDone}`).join(' · ');
  if(w){ big.textContent='VICTORY'; big.style.color=w.col; txt.textContent=`${w.name} rule the crater!  Damage — ${dmg}`; sfx('win'); }
  else { big.textContent='DRAW'; big.style.color='#e2e8f0'; txt.textContent='Everyone ended up in the drink.'; sfx('lose'); }
  $('gameover').classList.remove('hidden');
}
$('startBtn').onclick=()=>{ startGame(); };
$('resumeBtn').onclick=()=>togglePause();
$('pauseMenuBtn').onclick=()=>{ sfx('click'); showMenu(); };
$('rematchBtn').onclick=()=>{ startGame(); };
$('goMenuBtn').onclick=()=>{ sfx('click'); showMenu(); };
document.querySelectorAll('.overlay .btn, #startBtn').forEach(b=>b.addEventListener('mouseenter',()=>sfx('hover')));

/* ---------- RENDER ---------- */
let stars=[];
for(let i=0;i<120;i++) stars.push({x:Math.random(),y:Math.random()*0.6,r:rnd(0.4,1.6),ph:rnd(0,6)});
function drawSky(){
  const sk=theme.sky;
  const g=ctx.createLinearGradient(0,0,0,VH);
  g.addColorStop(0,sk[0]); g.addColorStop(0.62,sk[1]); g.addColorStop(1,sk[2]);
  ctx.fillStyle=g; ctx.fillRect(0,0,VW,VH);
  if(theme.moon){
    const mx=VW*0.78, my=VH*0.2;
    const mg=ctx.createRadialGradient(mx,my,10,mx,my,140);
    mg.addColorStop(0,'rgba(255,245,210,.35)'); mg.addColorStop(1,'rgba(255,245,210,0)');
    ctx.fillStyle=mg; ctx.fillRect(mx-160,my-160,320,320);
    ctx.fillStyle='#fff6d8'; ctx.beginPath(); ctx.arc(mx,my,34,0,Math.PI*2); ctx.fill();
    ctx.fillStyle='rgba(0,0,0,.08)'; ctx.beginPath(); ctx.arc(mx-10,my-6,7,0,7); ctx.arc(mx+12,my+8,5,0,7); ctx.fill();
  }
  if(theme===THEMES.cave){
    // crystals & stalactites
    ctx.fillStyle='rgba(120,255,200,.12)';
    for(let i=0;i<14;i++){ const x=(i+0.5)*VW/14; const h=30+((i*37)%60); ctx.beginPath(); ctx.moveTo(x-16,0); ctx.lineTo(x,h); ctx.lineTo(x+16,0); ctx.fill(); }
    const tw=gameT*0.5;
    for(const s of stars){ ctx.fillStyle=`rgba(180,255,230,${0.2+0.2*Math.sin(tw*2+s.ph)})`; ctx.beginPath(); ctx.arc(s.x*VW,s.y*VH,s.r,0,7); ctx.fill(); }
  } else {
    for(const s of stars){ const a=0.35+0.35*Math.sin(gameT*1.5+s.ph); ctx.fillStyle=`rgba(255,255,255,${a*(theme.moon?1:0.5)})`; ctx.beginPath(); ctx.arc(s.x*VW,s.y*VH,s.r,0,7); ctx.fill(); }
  }
  // parallax hills
  const layer=(col,amp,fr,off,base,par)=>{
    ctx.fillStyle=col; ctx.beginPath(); ctx.moveTo(0,VH);
    for(let px=0;px<=VW+16;px+=16){ const wx=px+cam.x*par; ctx.lineTo(px, base-amp*(0.6*Math.sin(wx*fr+off)+0.4*Math.sin(wx*fr*2.3+off*1.7))); }
    ctx.lineTo(VW,VH); ctx.closePath(); ctx.fill();
  };
  layer(theme.far,VH*0.13,0.0042,1.3,VH*0.72,-0.05);
  layer(theme.near,VH*0.09,0.0065,4.1,VH*0.84,-0.12);
}
function drawTerrainWorld(){ ctx.drawImage(tCanvas,0,0,WW,WH); }
function drawMarks(){
  for(const m of marks){
    const k=m.t/m.life, r=28*(1-k*0.4)+Math.sin(gameT*14)*2;
    ctx.strokeStyle=`rgba(255,93,58,${1-k})`; ctx.lineWidth=2.5;
    ctx.beginPath(); ctx.arc(m.x,m.y,r,0,7); ctx.stroke();
    ctx.beginPath(); ctx.moveTo(m.x-r,m.y); ctx.lineTo(m.x+r,m.y); ctx.moveTo(m.x,m.y-r); ctx.lineTo(m.x,m.y+r); ctx.stroke();
  }
}
function drawChunks(){
  for(const c of chunks){
    ctx.save(); ctx.translate(c.x,c.y); ctx.rotate(c.rot);
    ctx.fillStyle='rgba(0,0,0,.25)'; ctx.fillRect(-c.s/2+1,-c.s/2+2,c.s,c.s);
    ctx.fillStyle=c.c; ctx.fillRect(-c.s/2,-c.s/2,c.s,c.s);
    ctx.restore();
  }
}
function drawCrates(){
  for(const c of crates){
    const bob=c.landed?Math.sin(c.bob)*1.5:0;
    const x=c.x, y=c.y+bob;
    if(!c.landed){
      ctx.fillStyle='rgba(255,255,255,.9)';
      ctx.beginPath(); ctx.arc(x,y-22,14,Math.PI,0); ctx.fill();
      ctx.strokeStyle='rgba(255,255,255,.6)'; ctx.lineWidth=1;
      ctx.beginPath(); ctx.moveTo(x-14,y-22); ctx.lineTo(x,y-2); ctx.moveTo(x+14,y-22); ctx.lineTo(x,y-2); ctx.stroke();
    }
    const col=c.kind==='health'?'#22c55e':'#f59e0b';
    ctx.fillStyle='rgba(0,0,0,.35)'; ctx.fillRect(x-10,y-8,20,20);
    ctx.fillStyle=col; ctx.fillRect(x-11,y-10,22,20);
    ctx.fillStyle='rgba(255,255,255,.85)';
    if(c.kind==='health'){ ctx.fillRect(x-2,y-7,4,14); ctx.fillRect(x-7,y-2,14,4); }
    else { ctx.font='bold 13px sans-serif'; ctx.textAlign='center'; ctx.fillText('★',x,y+4); }
  }
}
function drawWorm(u){
  const t=teams[u.team], col=t.col;
  const moving=u.grounded&&Math.abs(u.vx)>8;
  const bob=moving?Math.abs(Math.sin(u.walkAnim*6))*2:0;
  const x=u.x, y=u.y-bob;
  ctx.save();
  if(u.rope){ ctx.strokeStyle='rgba(226,232,240,.85)'; ctx.lineWidth=1.6; ctx.beginPath(); ctx.moveTo(x,y-8); ctx.lineTo(u.rope.px,u.rope.py); ctx.stroke(); }
  // shadow / legs
  ctx.fillStyle='rgba(0,0,0,.35)'; ctx.beginPath(); ctx.ellipse(x,u.y+1,7,2.2,0,0,7); ctx.fill();
  const leg=moving?Math.sin(u.walkAnim*6)*2.5:0;
  ctx.fillStyle=t.dark;
  ctx.fillRect(x-4+leg*0.4,u.y-4,3,4); ctx.fillRect(x+1-leg*0.4,u.y-4,3,4);
  // body
  ctx.fillStyle=col; ctx.strokeStyle='rgba(0,0,0,.45)'; ctx.lineWidth=1.2;
  ctx.beginPath(); ctx.ellipse(x,y-8,7.5,7,0,0,7); ctx.fill(); ctx.stroke();
  // helmet stripe
  ctx.fillStyle=t.dark; ctx.beginPath(); ctx.ellipse(x,y-12.5,7.2,3.2,0,Math.PI,0); ctx.fill();
  // face
  const fx=x+u.dir*2.4;
  ctx.fillStyle='#fff'; ctx.beginPath(); ctx.arc(fx,y-8,2.6,0,7); ctx.fill();
  if(u.blinkAt>0){ ctx.strokeStyle='#0f172a'; ctx.beginPath(); ctx.moveTo(fx-1.6,y-8); ctx.lineTo(fx+1.6,y-8); ctx.stroke(); }
  else { ctx.fillStyle='#0f172a'; ctx.beginPath(); ctx.arc(fx+u.dir*0.8,y-8+(u.hp<35?0.6:0),1.2,0,7); ctx.fill(); }
  if(u.hp<35){ ctx.strokeStyle='#0f172a'; ctx.lineWidth=1; ctx.beginPath(); ctx.moveTo(x+u.dir*1,y-3.5); ctx.arc(x+u.dir*1,y-3.5,1.6,0,Math.PI); ctx.stroke(); }
  // hit flash
  if(u.flash>0){ ctx.globalAlpha=Math.min(1,u.flash*2.5); ctx.fillStyle='#fff'; ctx.beginPath(); ctx.ellipse(x,y-8,7.5,7,0,0,7); ctx.fill(); ctx.globalAlpha=1; }
  // float ring
  if(u.float){ ctx.strokeStyle='rgba(125,211,252,.9)'; ctx.lineWidth=2; ctx.beginPath(); ctx.ellipse(x,y-6,12,4,0,0,7); ctx.stroke(); }
  // HUD tags
  const isAct=(u===active&&state==='play');
  const tagY=y-26;
  ctx.font='bold 11px "Trebuchet MS",sans-serif'; ctx.textAlign='center';
  ctx.lineWidth=3; ctx.strokeStyle='rgba(0,0,0,.7)'; ctx.strokeText(u.name,x,tagY-4); ctx.fillStyle=isAct?'#fff':'#e2e8f0'; ctx.fillText(u.name,x,tagY-4);
  const bw=30, hpw=bw*u.hp/100;
  ctx.fillStyle='rgba(0,0,0,.55)'; ctx.fillRect(x-bw/2-1,tagY-1,bw+2,6);
  ctx.fillStyle=u.hp>50?'#4ade80':u.hp>25?'#facc15':'#ef4444'; ctx.fillRect(x-bw/2,tagY,hpw,4);
  if(isAct){
    const bounce=Math.sin(gameT*6)*2.5;
    ctx.fillStyle=col; ctx.strokeStyle='#fff'; ctx.lineWidth=1.5;
    ctx.beginPath(); ctx.moveTo(x-6,y-44+bounce); ctx.lineTo(x+6,y-44+bounce); ctx.lineTo(x,y-36+bounce); ctx.closePath(); ctx.fill(); ctx.stroke();
  }
  ctx.restore();
}
function drawAimGuide(){
  const u=active; if(!u||u.dead||state!=='play'||!isHuman(u)||u.rope) return;
  const w=WEAPONS[wepSel];
  const dx=Math.cos(u.aimA)*u.dir, dy=-Math.sin(u.aimA);
  const ox=u.x+dx*9, oy=u.y-UH/2-3+dy*9;
  if(w.id==='sniper'||w.id==='shotgun'||w.id==='drill'){
    const r=rayHit(ox,oy,dx,dy,w.id==='drill'?260:1200);
    ctx.setLineDash([6,6]); ctx.strokeStyle=w.id==='drill'?'rgba(201,164,107,.9)':'rgba(255,240,180,.85)'; ctx.lineWidth=1.5;
    ctx.beginPath(); ctx.moveTo(ox,oy); ctx.lineTo(r.x,r.y); ctx.stroke(); ctx.setLineDash([]);
    ctx.strokeStyle='#fff'; ctx.lineWidth=2; ctx.beginPath(); ctx.arc(r.x,r.y,6+Math.sin(gameT*10)*1.5,0,7); ctx.stroke();
    return;
  }
  if(w.speed<=0) return;
  const pw=charging?charge:0.6;
  const tr=trajectoryPoints(u,pw,w);
  for(let i=0;i<tr.pts.length;i++){
    const p=tr.pts[i], a=1-i/tr.pts.length;
    ctx.fillStyle=`rgba(255,255,255,${0.15+0.7*a})`; ctx.beginPath(); ctx.arc(p.x,p.y,2.2*(0.6+0.4*a),0,7); ctx.fill();
  }
  const e=tr.end, pulse=(Math.sin(gameT*8)+1)/2;
  ctx.strokeStyle=`rgba(255,93,58,${0.5+0.4*pulse})`; ctx.lineWidth=2;
  ctx.beginPath(); ctx.arc(e.x,e.y,w.r*(0.9+0.1*pulse)||10,0,7); ctx.stroke();
}
function drawProj(p){
  // trail
  const tr=p.trail;
  if(tr.length>1){
    for(let i=1;i<tr.length;i++){
      const a=i/tr.length;
      ctx.strokeStyle=p.kind==='meteor'?`rgba(255,140,60,${a*0.8})`:`rgba(230,230,230,${a*0.45})`;
      ctx.lineWidth=(p.kind==='meteor'?10:2)*a+0.5; ctx.beginPath(); ctx.moveTo(tr[i-1].x,tr[i-1].y); ctx.lineTo(tr[i].x,tr[i].y); ctx.stroke();
    }
  }
  const ang=Math.atan2(p.vy,p.vx);
  ctx.save(); ctx.translate(p.x,p.y);
  switch(p.kind){
    case 'rocket':
      ctx.rotate(ang);
      ctx.fillStyle='rgba(255,170,60,.9)'; ctx.beginPath(); ctx.ellipse(-9-rnd(0,3),0,5+rnd(0,2),2.4,0,0,7); ctx.fill();
      ctx.fillStyle='#e5e7eb'; ctx.fillRect(-6,-2.6,12,5.2);
      ctx.fillStyle='#ef4444'; ctx.fillRect(4,-2.6,3,5.2);
      break;
    case 'grenade':
      ctx.fillStyle='#4d7c0f'; ctx.beginPath(); ctx.arc(0,0,4.6,0,7); ctx.fill();
      ctx.fillStyle='#a3e635'; ctx.fillRect(-1.5,-6,3,2.5);
      if(Math.floor(p.t*6)%2){ ctx.fillStyle='#ef4444'; ctx.beginPath(); ctx.arc(0,-6,1.6,0,7); ctx.fill(); }
      break;
    case 'cluster':
      ctx.fillStyle='#fb923c'; ctx.beginPath(); ctx.arc(0,0,6,0,7); ctx.fill();
      ctx.fillStyle='#fde68a'; for(let i=0;i<4;i++){ ctx.fillRect(Math.cos(i*1.6)*3-1,Math.sin(i*1.6)*3-1,2,2); }
      break;
    case 'meteor': {
      const g=ctx.createRadialGradient(0,0,2,0,0,30);
      g.addColorStop(0,'#fff7b0'); g.addColorStop(0.35,'#ff9f1c'); g.addColorStop(1,'rgba(255,60,20,0)');
      ctx.fillStyle=g; ctx.beginPath(); ctx.arc(0,0,30,0,7); ctx.fill();
      ctx.fillStyle='#7c2d12'; ctx.beginPath(); ctx.arc(0,0,11,0,7); ctx.fill(); break; }
    case 'bomblet':
      ctx.fillStyle='#fde047'; ctx.beginPath(); ctx.arc(0,0,2.6,0,7); ctx.fill(); break;
    case 'airbomb':
      ctx.rotate(Math.PI/2+ang*0); ctx.fillStyle='#1f2937'; ctx.beginPath(); ctx.ellipse(0,0,3.4,6,0,0,7); ctx.fill();
      ctx.fillStyle='#ef4444'; ctx.fillRect(-1,4,2,2); break;
    default: ctx.fillStyle='#fff'; ctx.beginPath(); ctx.arc(0,0,3,0,7); ctx.fill();
  }
  ctx.restore();
}
function drawPlane(p){
  ctx.save(); ctx.translate(p.x,p.y); ctx.scale(p.dir,1);
  ctx.fillStyle='#334155'; ctx.beginPath(); ctx.moveTo(-40,0); ctx.lineTo(30,-4); ctx.lineTo(42,0); ctx.lineTo(30,4); ctx.lineTo(-40,6); ctx.closePath(); ctx.fill();
  ctx.fillStyle='#475569'; ctx.beginPath(); ctx.moveTo(-6,0); ctx.lineTo(-16,-22); ctx.lineTo(-22,-20); ctx.lineTo(-12,2); ctx.closePath(); ctx.fill();
  ctx.fillStyle='#ef4444'; ctx.beginPath(); ctx.arc(40,0,2,0,7); ctx.fill();
  ctx.restore();
}
function drawWater(){
  const y=WATER;
  const path=()=>{ ctx.beginPath(); ctx.moveTo(-400,WH+400);
    for(let x=-400;x<=WW+400;x+=18){ ctx.lineTo(x,y+Math.sin(x*0.018+gameT*2.2)*3.2+Math.sin(x*0.045-gameT*1.4)*2); }
    ctx.lineTo(WW+400,WH+400); ctx.closePath(); };
  const g=ctx.createLinearGradient(0,y,0,WH+300);
  g.addColorStop(0,'rgba(56,189,248,.72)'); g.addColorStop(1,'rgba(7,36,78,.96)');
  ctx.fillStyle=g; path(); ctx.fill();
  ctx.strokeStyle='rgba(224,247,255,.75)'; ctx.lineWidth=2;
  ctx.beginPath();
  for(let x=-400;x<=WW+400;x+=18){ const yy=y+Math.sin(x*0.018+gameT*2.2)*3.2+Math.sin(x*0.045-gameT*1.4)*2; x===-400?ctx.moveTo(x,yy):ctx.lineTo(x,yy); }
  ctx.stroke();
}
function drawParts(pre){
  for(const p of parts){
    const isPre=(p.type==='smoke'||p.type==='dust');
    if(isPre!==pre) continue;
    const k=clamp(p.life/p.max,0,1);
    if(p.type==='ring'){
      const r=p.size*(0.35+0.65*(1-k));
      ctx.strokeStyle=p.col; ctx.globalAlpha=k; ctx.lineWidth=3*k+0.5; ctx.beginPath(); ctx.arc(p.x,p.y,r,0,7); ctx.stroke(); ctx.globalAlpha=1; continue;
    }
    if(p.type==='smoke'){
      const r=p.size*(1.6-k*0.6); ctx.globalAlpha=0.5*k; ctx.fillStyle=p.col; ctx.beginPath(); ctx.arc(p.x,p.y,r,0,7); ctx.fill(); ctx.globalAlpha=1; continue;
    }
    if(p.type==='bubble'){
      ctx.globalAlpha=k; ctx.strokeStyle=p.col; ctx.lineWidth=1.2; ctx.beginPath(); ctx.arc(p.x,p.y,p.size,0,7); ctx.stroke(); ctx.globalAlpha=1; continue;
    }
    if(p.type==='fire'){
      ctx.globalAlpha=Math.min(1,k*1.4); ctx.fillStyle=p.col; ctx.beginPath(); ctx.arc(p.x,p.y,p.size*(0.5+k*0.7),0,7); ctx.fill(); ctx.globalAlpha=1; continue;
    }
    if(p.type==='spark'){
      const sp=Math.hypot(p.vx,p.vy); ctx.strokeStyle=p.col; ctx.globalAlpha=k; ctx.lineWidth=p.size*0.9;
      ctx.beginPath(); ctx.moveTo(p.x,p.y); const L=Math.min(7,sp*0.03); ctx.lineTo(p.x-p.vx/(sp||1)*L,p.y-p.vy/(sp||1)*L); ctx.stroke(); ctx.globalAlpha=1; continue;
    }
    ctx.globalAlpha=k; ctx.fillStyle=p.col; ctx.beginPath(); ctx.arc(p.x,p.y,p.size*(0.6+0.4*k),0,7); ctx.fill(); ctx.globalAlpha=1;
  }
}
function drawBeams(){
  for(const b of beams){
    const k=1-b.t/b.life;
    ctx.globalAlpha=k; ctx.strokeStyle=b.col; ctx.lineCap='round';
    ctx.lineWidth=b.rope?2:(b.w||2);
    ctx.beginPath(); ctx.moveTo(b.x0,b.y0); ctx.lineTo(b.x1,b.y1); ctx.stroke();
    if(!b.rope&&b.w>=3){ ctx.globalAlpha=k*0.35; ctx.lineWidth=(b.w||2)*3; ctx.stroke(); }
    ctx.globalAlpha=1;
  }
}
function drawTexts(){
  ctx.textAlign='center';
  for(const t of texts){
    const k=t.t/t.life; const a=clamp(1-k*1.1,0,1);
    const pop=1+Math.max(0,0.5-k*4)*0.9;
    ctx.font=`900 ${Math.round(t.size*pop)}px "Trebuchet MS",sans-serif`;
    ctx.globalAlpha=a; ctx.lineWidth=4; ctx.strokeStyle='rgba(0,0,0,.75)';
    ctx.strokeText(t.text,t.x+t.vx*t.t,t.y); ctx.fillStyle=t.col; ctx.fillText(t.text,t.x+t.vx*t.t,t.y);
    ctx.globalAlpha=1;
  }
}
function drawWind(){
  if(Math.abs(wind)<0.05) return;
  ctx.strokeStyle='rgba(255,255,255,.18)'; ctx.lineWidth=1.2;
  const dir=wind>0?1:-1, spd=Math.abs(wind)*900;
  for(let i=0;i<28;i++){
    const seed=i*97.13;
    const x=((seed+gameT*spd*0.9*dir)%(VW+200)+VW+200)%(VW+200)-100;
    const y=((seed*1.7)%VH);
    ctx.beginPath(); ctx.moveTo(x,y); ctx.lineTo(x-dir*(18+Math.abs(wind)*40),y+4); ctx.stroke();
  }
}
function render(){
  const DP=DPR;
  ctx.setTransform(DP,0,0,DP,0,0);
  drawSky();
  const s=baseScale()*cam.z;
  const shx=(Math.random()*2-1)*shake*2.2, shy=(Math.random()*2-1)*shake*2.2;
  ctx.setTransform(DP*s,0,0,DP*s,DP*(VW/2-cam.x*s+shx),DP*(VH/2-cam.y*s+shy));
  drawTerrainWorld();
  drawMarks();
  drawChunks();
  drawParts(true);
  for(const p of planes) drawPlane(p);
  drawCrates();
  for(const u of units) if(!u.dead && !(u===active && false)) drawWorm(u);
  drawAimGuide();
  drawBeams();
  for(const p of projs) drawProj(p);
  drawParts(false);
  drawWater();
  drawTexts();
  // screen-space overlays
  ctx.setTransform(DP,0,0,DP,0,0);
  drawWind();
  const vg=ctx.createRadialGradient(VW/2,VH/2,Math.min(VW,VH)*0.45,VW/2,VH/2,Math.max(VW,VH)*0.75);
  vg.addColorStop(0,'rgba(0,0,0,0)'); vg.addColorStop(1,'rgba(0,0,0,.55)');
  ctx.fillStyle=vg; ctx.fillRect(0,0,VW,VH);
  if(flash>0){ ctx.fillStyle=`rgba(255,246,220,${Math.min(0.5,flash*0.5)})`; ctx.fillRect(0,0,VW,VH); }
  if(hitstop>0.02){ ctx.fillStyle='rgba(255,255,255,.04)'; ctx.fillRect(0,0,VW,VH); }
  if(charging&&active&&isHuman(active)){ // power bar in screen space
    const p=w2s(active.x,active.y-40); const bw=120;
    ctx.fillStyle='rgba(0,0,0,.6)'; ctx.fillRect(p.x-bw/2-2,p.y-2,bw+4,14);
    const g=ctx.createLinearGradient(p.x-bw/2,0,p.x+bw/2,0);
    g.addColorStop(0,'#4ade80'); g.addColorStop(0.5,'#facc15'); g.addColorStop(1,'#ef4444');
    ctx.fillStyle=g; ctx.fillRect(p.x-bw/2,p.y,bw*charge,10);
    ctx.strokeStyle='rgba(255,255,255,.7)'; ctx.lineWidth=1; ctx.strokeRect(p.x-bw/2,p.y,bw,10);
  }
}

/* ---------- LOOP ---------- */
let lastFrame=performance.now();
function frame(now){
  const dt=Math.min(0.033,Math.max(0,(now-lastFrame)/1000)); lastFrame=now;
  if(state!=='paused') update(dt);
  render();
  requestAnimationFrame(frame);
}
resize();
showMenu();
requestAnimationFrame(frame);
</script>
</body>
</html>
<!-- agent-meta {"model":"anthropic/claude-haiku-5.5","provider":"openrouter","persona":"full","sessionId":"ce68250c-87bc-4f72-a6af-ac2d345d0f2d","tokensIn":2040637,"tokensOut":261605,"tokensTotal":2302242,"cost":0.2589944449999999,"turns":28,"toolCalls":34,"failedToolCalls":2,"timestamp":"2026-10-08T04:34:58.471Z"} -->