← Worms results

Worms

ox alpha · 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 26,587 bytes · SHA-256 14ec3d601237
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>BOOM WORMS — Artillery Mayhem</title>
<style>
  * { margin:0; padding:0; box-sizing:border-box; }
  html,body { width:100%; height:100%; overflow:hidden; background:#0a0e1a;
    font-family:'Trebuchet MS','Segoe UI',sans-serif; user-select:none; }
  #game { position:absolute; inset:0; display:block; cursor:crosshair; }
  /* ---------- HUD ---------- */
  #hud { position:absolute; inset:0; pointer-events:none; }
  .panel { position:absolute; background:linear-gradient(160deg,rgba(16,22,40,.92),rgba(10,14,26,.88));
    border:1px solid rgba(120,160,255,.25); border-radius:12px;
    box-shadow:0 4px 24px rgba(0,0,0,.55), inset 0 1px 0 rgba(255,255,255,.08);
    backdrop-filter:blur(6px); pointer-events:auto; }
  /* team cards */
  #teams { top:12px; left:12px; display:flex; flex-direction:column; gap:8px; background:none; border:none; box-shadow:none; }
  .teamCard { min-width:190px; padding:7px 10px; border-radius:10px; position:relative;
    background:linear-gradient(160deg,rgba(16,22,40,.92),rgba(10,14,26,.85));
    border:1px solid rgba(120,160,255,.2); transition:transform .25s, box-shadow .25s, opacity .3s; opacity:.55; }
  .teamCard.active { transform:scale(1.06); opacity:1; box-shadow:0 0 18px var(--glow); border-color:var(--col); }
  .teamName { font-weight:bold; font-size:13px; letter-spacing:.5px; color:var(--col); display:flex; justify-content:space-between; }
  .teamName .cpuTag { font-size:9px; opacity:.7; background:rgba(255,255,255,.1); padding:1px 5px; border-radius:6px; }
  .wrow { display:flex; gap:4px; margin-top:5px; }
  .whp { flex:1; height:9px; border-radius:4px; background:rgba(255,255,255,.09); overflow:hidden; position:relative; }
  .whpfill { height:100%; border-radius:4px; transition:width .35s cubic-bezier(.2,.8,.3,1); background:var(--col);
    box-shadow:0 0 6px var(--col); }
  .whp.dead::after { content:'✝'; position:absolute; inset:0; font-size:8px; line-height:9px; text-align:center; color:#666; }
  .whp.activeslot { outline:2px solid rgba(255,255,255,.75); outline-offset:1px; }
  /* wind */
  #windPanel { top:12px; left:50%; transform:translateX(-50%); padding:8px 18px; display:flex; align-items:center; gap:12px; }
  #windLabel { font-size:11px; letter-spacing:2px; color:#8fa8d8; }
  #windBar { width:170px; height:14px; border-radius:7px; background:rgba(255,255,255,.07); position:relative; overflow:hidden; }
  #windBar::after { content:''; position:absolute; left:50%; top:0; bottom:0; width:2px; background:rgba(255,255,255,.3); }
  #windFill { position:absolute; top:2px; bottom:2px; left:50%; background:linear-gradient(90deg,#5eead4,#93c5fd,#fca5a5); border-radius:5px; transition:all .5s; }
  #windArrows { font-size:13px; color:#bcd; letter-spacing:1px; min-width:70px; text-align:center; }
  /* timer */
  #timerPanel { top:64px; left:50%; transform:translateX(-50%); padding:4px 20px; text-align:center; }
  #turnLabel { font-size:11px; letter-spacing:2px; color:#9fb4dd; margin-top:-2px; }
  #timerText { font-size:30px; font-weight:bold; color:#fff; text-shadow:0 0 12px currentColor; transition:color .3s; }
  /* weapon dock */
  #weaponDock { bottom:14px; left:50%; transform:translateX(-50%); display:flex; gap:8px; padding:10px 12px; }
  .wslot { width:74px; padding:7px 4px 6px; border-radius:9px; text-align:center; cursor:pointer;
    background:rgba(255,255,255,.05); border:1px solid rgba(255,255,255,.12); transition:all .15s; position:relative; }
  .wslot:hover { background:rgba(255,255,255,.13); transform:translateY(-3px); }
  .wslot.sel { background:rgba(120,180,255,.22); border-color:#8ab8ff; box-shadow:0 0 12px rgba(120,180,255,.5); transform:translateY(-4px); }
  .wslot.empty { opacity:.28; pointer-events:none; }
  .wicon { font-size:24px; line-height:1; }
  .wname { font-size:9px; color:#cfe0ff; margin-top:3px; letter-spacing:.3px; white-space:nowrap; }
  .wammo { position:absolute; top:-6px; right:-5px; background:#e2483f; color:#fff; font-size:9px; font-weight:bold;
    min-width:16px; height:16px; line-height:16px; border-radius:8px; padding:0 3px; box-shadow:0 1px 4px rgba(0,0,0,.5); }
  .wcat { font-size:7px; color:#7d92bb; letter-spacing:1px; margin-top:1px; }
  /* power meter */
  #powerWrap { position:absolute; left:50%; bottom:110px; transform:translateX(-50%); width:260px; height:16px;
    background:rgba(0,0,0,.55); border-radius:8px; border:1px solid rgba(255,255,255,.25); overflow:hidden; display:none; }
  #powerFill { height:100%; width:0%; background:linear-gradient(90deg,#4ade80,#facc15,#f87171); }
  /* messages */
  #banner { position:absolute; top:34%; left:50%; transform:translate(-50%,-50%) scale(.6); font-size:52px; font-weight:bold;
    letter-spacing:3px; color:#fff; text-shadow:0 0 30px var(--bc,#8ab8ff), 0 4px 8px rgba(0,0,0,.7);
    opacity:0; transition:all .35s cubic-bezier(.2,1.4,.4,1); white-space:nowrap; }
  #banner.show { opacity:1; transform:translate(-50%,-50%) scale(1); }
  #hint { position:absolute; bottom:118px; left:50%; transform:translateX(-50%); font-size:12px; color:rgba(200,215,245,.65);
    letter-spacing:.5px; text-shadow:0 1px 3px #000; }
  /* menu */
  #menu { position:absolute; inset:0; display:flex; align-items:center; justify-content:center;
    background:radial-gradient(ellipse at 50% 30%,#1b2a4d 0%,#0a0e1a 70%); z-index:20; }
  #menuBox { text-align:center; max-width:640px; padding:36px 44px; }
  #menuTitle { font-size:58px; font-weight:bold; letter-spacing:4px; color:#ffd94d;
    text-shadow:0 0 30px rgba(255,200,60,.7), 0 5px 0 #a05a12, 0 8px 20px rgba(0,0,0,.8); animation:bob 2.4s ease-in-out infinite; }
  @keyframes bob { 0%,100%{transform:translateY(0)} 50%{transform:translateY(-8px)} }
  #menuSub { color:#8fa8d8; letter-spacing:3px; font-size:13px; margin:6px 0 26px; }
  .mrow { margin:14px 0; color:#cfe0ff; font-size:14px; }
  .mrow label { margin-right:12px; color:#9fb4dd; letter-spacing:1px; }
  .seg { display:inline-flex; gap:4px; }
  .seg button { padding:7px 15px; border-radius:8px; border:1px solid rgba(140,170,230,.3); background:rgba(255,255,255,.06);
    color:#cfe0ff; font-size:13px; cursor:pointer; font-family:inherit; transition:all .12s; }
  .seg button:hover { background:rgba(255,255,255,.15); transform:translateY(-1px); }
  .seg button.on { background:#3b82f6; border-color:#93c5fd; color:#fff; box-shadow:0 0 10px rgba(59,130,246,.6); }
  .cpusel button { padding:5px 9px; font-size:12px; }
  #startBtn { margin-top:26px; padding:14px 54px; font-size:22px; font-weight:bold; letter-spacing:3px; border-radius:12px;
    border:none; cursor:pointer; color:#20140a; background:linear-gradient(180deg,#ffe27a,#f5a623);
    box-shadow:0 5px 0 #a05a12, 0 10px 24px rgba(245,166,35,.4); transition:transform .1s, box-shadow .1s; font-family:inherit; }
  #startBtn:hover { transform:translateY(-2px); box-shadow:0 7px 0 #a05a12, 0 14px 28px rgba(245,166,35,.5); }
  #startBtn:active { transform:translateY(3px); box-shadow:0 2px 0 #a05a12; }
</style>
</head>
<body>
<canvas id="game"></canvas>
<div id="hud">
  <div id="teams" class="panel"></div>
  <div id="windPanel" class="panel">
    <span id="windLabel">WIND</span>
    <div id="windBar"><div id="windFill"></div></div>
    <span id="windArrows"></span>
  </div>
  <div id="timerPanel" class="panel"><div id="timerText">45</div><div id="turnLabel">TEAM NAME</div></div>
  <div id="weaponDock" class="panel"></div>
  <div id="powerWrap"><div id="powerFill"></div></div>
  <div id="banner"></div>
  <div id="hint">← → move · ↑ jump · hold CLICK to charge · release to fire · scroll = zoom</div>
</div>
<div id="menu">
  <div id="menuBox">
    <div id="menuTitle">💣 BOOM WORMS</div>
    <div id="menuSub">DESTRUCTIBLE TERRAIN ARTILLERY MAYHEM</div>
    <div class="mrow"><label>TEAMS</label><span class="seg" id="teamCount"></span></div>
    <div class="mrow"><label>OPPONENTS</label><span class="seg cpusel" id="cpuMode">
      <button data-v="cpu" class="on">🤖 CPU</button><button data-v="hot">🔥 HOT-SEAT</button><button data-v="mix">🎲 MIXED</button></span></div>
    <div class="mrow"><label>TERRAIN</label><span class="seg" id="terrSel">
      <button data-v="island" class="on">🏝 ISLAND</button><button data-v="twin">🏝🏝 TWIN ISLES</button><button data-v="cave">🕳 CAVE</button><button data-v="random">❓ RANDOM</button></span></div>
    <button id="startBtn">START BATTLE</button>
  </div>
</div>
<script>
'use strict';
/* ============================================================
   BOOM WORMS — single-file artillery game
   ============================================================ */
const WORLD_W = 1920, WORLD_H = 1080, WATER_Y = WORLD_H - 70;
const GRAV = 0.16, WORM_R = 8;
const cvs = document.getElementById('game'), ctx = cvs.getContext('2d');
let VW = innerWidth, VH = innerHeight;
function resize(){ VW=cvs.width=innerWidth; VH=cvs.height=innerHeight; }
addEventListener('resize', resize); resize();

/* ---------------- utils ---------------- */
const rnd=(a,b)=>a+Math.random()*(b-a), rndi=(a,b)=>Math.floor(rnd(a,b+1));
const clamp=(v,a,b)=>v<a?a:v>b?b:v, lerp=(a,b,t)=>a+(b-a)*t;
const dist=(x1,y1,x2,y2)=>Math.hypot(x2-x1,y2-y1);
function valueNoise(seed){ // tiny deterministic noise
  const g=new Float32Array(512); let s=seed;
  const rand=()=>{ s=(s*16807)%2147483647; return s/2147483647; };
  for(let i=0;i<512;i++)g[i]=rand();
  return (x,scale=1)=>{
    x*=scale; const i=Math.floor(x), f=x-i, u=f*f*(3-2*f);
    return lerp(g[i&511],g[(i+1)&511],u);
  };
}
/* ---------------- AUDIO (synth SFX) ---------------- */
const AudioSys = (()=>{
  let ac=null, master=null, tempoTick=0;
  function ensure(){ if(!ac){ ac=new (window.AudioContext||window.webkitAudioContext)();
    master=ac.createGain(); master.gain.value=.5; master.connect(ac.destination);} 
    if(ac.state==='suspended')ac.resume(); }
  function env(g,t,a,d,peak=1){ g.gain.setValueAtTime(0,t); g.gain.linearRampToValueAtTime(peak,t+a);
    g.gain.exponentialRampToValueAtTime(.001,t+a+d); }
  function tone(freq,dur,type='square',vol=.2,slide=0){
    ensure(); const t=ac.currentTime,o=ac.createOscillator(),g=ac.createGain();
    o.type=type; o.frequency.setValueAtTime(freq,t);
    if(slide)o.frequency.exponentialRampToValueAtTime(Math.max(20,freq+slide),t+dur);
    env(g,t,.005,dur,vol); o.connect(g); g.connect(master); o.start(t); o.stop(t+dur+.05);
  }
  function noise(dur,vol=.3,filt=800,q=1){
    ensure(); const t=ac.currentTime, len=ac.sampleRate*dur, buf=ac.createBuffer(1,len,ac.sampleRate),
      d=buf.getChannelData(0);
    for(let i=0;i<len;i++)d[i]=(Math.random()*2-1)*(1-i/len);
    const src=ac.createBufferSource(); src.buffer=buf;
    const f=ac.createBiquadFilter(); f.type='lowpass'; f.frequency.value=filt; f.Q.value=q;
    const g=ac.createGain(); env(g,t,.005,dur,vol);
    src.connect(f); f.connect(g); g.connect(master); src.start(t);
  }
  return {
    ensure,
    ui(){ tone(rnd(600,700),.06,'triangle',.12,200); },
    uiBig(){ tone(300,.12,'triangle',.16,250); setTimeout(()=>tone(500,.1,'triangle',.14,150),70); },
    jump(){ tone(280,.14,'square',.1,320); },
    walk(){ noise(.04,.05,600); },
    shoot(){ noise(.12,.25,2200); tone(220,.15,'sawtooth',.14,-140); },
    bounce(){ tone(rnd(380,480),.07,'square',.1,-120); },
    boom(big){ noise(big?.9:.5,big?.55:.38,big?420:900,.8); tone(60,big?.7:.4,'sine',.4,-40);
      if(big)setTimeout(()=>noise(.6,.3,300),120); },
    splash(){ noise(.5,.3,500,2); tone(400,.3,'sine',.12,-300); },
    hurt(){ tone(rnd(300,360),.16,'sawtooth',.16,-180); },
    die(){ tone(400,.4,'sawtooth',.18,-350); },
    tick(fast){ tone(fast?rnd(900,1100):750,.045,'square',fast?.14:.09); },
    charge(){ tone(200+tempoTick*14,.03,'square',.05); tempoTick=(tempoTick+1)%30; },
    tp(){ tone(900,.25,'sine',.15,-650); },
    build(){ tone(180,.1,'square',.14,60); setTimeout(()=>tone(260,.1,'square',.12,80),90); },
    win(){ [440,554,659,880].forEach((f,i)=>setTimeout(()=>tone(f,.3,'triangle',.2),i*140)); }
  };
})();

/* ---------------- TERRAIN ---------------- */
const Terrain = {
  sol: new Uint8Array(WORLD_W*WORLD_H),
  cvs: document.createElement('canvas'),
  ctx: null,
  init(shape){
    this.cvs.width=WORLD_W; this.cvs.height=WORLD_H;
    this.ctx=this.cvs.getContext('2d');
    const S=this.sol; S.fill(0);
    const n1=valueNoise(rndi(1,99999)), n2=valueNoise(rndi(1,99999));
    if(shape==='cave'){
      // solid rock, carve caverns + surface valley
      S.fill(1);
      for(let x=0;x<WORLD_W;x++){
        const surf = 260 + n1(x,.006)*220 + n2(x,.02)*40;
        for(let y=0;y<surf;y++) S[y*WORLD_W+x]=0;
      }
      for(let i=0;i<26;i++){ // carve tunnels
        let cx=rnd(0,WORLD_W), cy=rnd(340,WORLD_H-160), r=rnd(30,80);
        const dx=rnd(-1,1), dy=rnd(-.4,.4);
        for(let t=0;t<rndi(30,90);t++){
          cx+=dx*22+rnd(-8,8); cy+=dy*22+rnd(-8,8);
          this.carveOnly(cx,cy,r*(0.7+n2(t,.3)*.6));
        }
      }
    } else {
      const centers = shape==='twin' ? [[.28,300],[.72,320]] : [[.5,330]];
      for(const [fx,hh] of centers){
        const cx=fx*WORLD_W;
        for(let x=0;x<WORLD_W;x++){
          const d=Math.abs(x-cx)/(WORLD_W*.42);
          if(d>1)continue;
          const h = hh*(Math.cos(d*Math.PI/2)**.7) + n1(x,.015)*70 + n2(x,.05)*25;
          for(let y=WORLD_H-h;y<WORLD_H;y++) S[y*WORLD_W+x]=1;
        }
      }
    }
    this.paint();
  },
  carveOnly(x,y,r){
    x|=0;y|=0;
    for(let dy=-r;dy<=r;dy++)for(let dx=-r;dx<=r;dx++){
      if(dx*dx+dy*dy>r*r)continue;
      const px=(x+dx)|0, py=(y+dy)|0;
      if(px>=0&&px<WORLD_W&&py>=0&&py<WORLD_H) this.sol[py*WORLD_W+px]=0;
    }
  },
  paint(){
    const c=this.ctx, img=c.createImageData(WORLD_W,WORLD_H), d=img.data;
    for(let y=0;y<WORLD_H;y++)for(let x=0;x<WORLD_W;x++){
      const i=y*WORLD_W+x; if(!this.sol[i])continue;
      const o=i*4;
      // grass on top edge, dirt gradient below
      const top=!this.sol[i-WORLD_W];
      let r,g,b;
      if(top){ r=76; g=175+rndi(-14,14); b=60; }
      else {
        const dep=clamp(y/WORLD_H,0,1);
        r=110-dep*35+rndi(-10,10); g=72-dep*24+rndi(-8,8); b=42-dep*14+rndi(-6,6);
      }
      d[o]=r;d[o+1]=g;d[o+2]=b;d[o+3]=255;
    }
    c.putImageData(img,0,0);
  },
  solid(x,y){
    x|=0;y|=0;
    if(x<0||x>=WORLD_W||y<0)return false;
    if(y>=WORLD_H)return false;
    return !!this.sol[y*WORLD_W+x];
  },
  destroy(x,y,r,noDebris){
    x|=0;y|=0;
    let removed=0;
    for(let dy=-r;dy<=r;dy+=1)for(let dx=-r;dx<=r;dx+=1){
      if(dx*dx+dy*dy>r*r)continue;
      const px=x+dx,py=y+dy;
      if(px<0||px>=WORLD_W||py<0||py>=WORLD_H)continue;
      const i=py*WORLD_W+px;
      if(this.sol[i]){ this.sol[i]=0; removed++; }
    }
    // clear pixels
    const c=this.ctx; c.save(); c.globalCompositeOperation='destination-out';
    c.beginPath(); c.arc(x,y,r,0,7); c.fill(); c.restore();
    // scorch rim
    c.save(); c.strokeStyle='rgba(35,20,10,.55)'; c.lineWidth=3;
    c.beginPath(); c.arc(x,y,r+1,0,7); c.stroke(); c.restore();
    return removed;
  },
  addGirder(x,y,w,h){
    const c=this.ctx; c.fillStyle='#8a6a3a';
    c.fillRect(x-w/2,y-h/2,w,h);
    c.fillStyle='rgba(0,0,0,.25)';
    for(let gx=x-w/2;gx<x+w/2;gx+=12)c.fillRect(gx,y-h/2,2,h);
    const x0=(x-w/2)|0,x1=(x+w/2)|0,y0=(y-h/2)|0,y1=(y+h/2)|0;
    for(let py=y0;py<=y1;py++)for(let px=x0;px<=x1;px++)
      if(px>=0&&px<WORLD_W&&py>=0&&py<WORLD_H)this.sol[py*WORLD_W+px]=1;
  },
  groundY(x,fromY){ // first solid y at column from fromY downward
    for(let y=Math.max(0,fromY|0);y<WORLD_H;y++)if(this.solid(x,y))return y;
    return WORLD_H;
  }
};

/* ---------------- PARTICLES / DEBRIS / TEXT ---------------- */
const particles=[], debris=[], floaters=[], projectiles=[], girders=[];
function spawnParticles(x,y,n,opt={}){
  for(let i=0;i<n;i++){
    const a=opt.ang!==undefined?opt.ang+rnd(-(opt.spread||3.14),(opt.spread||3.14)):rnd(0,Math.PI*2);
    const sp=rnd(opt.spMin??1,opt.spMax??5);
    particles.push({x,y,vx:Math.cos(a)*sp+(opt.vx||0),vy:Math.sin(a)*sp+(opt.vy||0),
      life:rnd(.4,opt.life??1),age:0,size:rnd(1.5,opt.size??4),
      col:opt.col||(opt.fire?['#fff3b0','#ffb347','#ff6b35','#d43e2a'][rndi(0,3)]:'#9a7b52'),
      grav:opt.grav??GRAV*.7, fade:opt.fade??true});
  }
}
function spawnChunkDebris(x,y,r){
  const n=clamp((r*r)/900|0,2,9);
  for(let i=0;i<n;i++){
    const s=rnd(5,13);
    debris.push({x:x+rnd(-r,r)*.6,y:y+rnd(-r,r)*.6,vx:rnd(-3,3),vy:rnd(-6,-1),
      rot:rnd(0,6.28),vr:rnd(-.2,.2),w:s,h:s*rnd(.5,1),life:rnd(1.2,2.4),age:0});
  }
}
function floatText(x,y,text,col='#fff',size=16){
  floaters.push({x,y,text,col,size,age:0,life:1.4});
}

/* ---------------- GAME STATE ---------------- */
const G = {
  teams:[], worms:[], activeTeam:0, activeWorm:0,
  state:'menu', // menu | aim | charge | projectile | settle | gameover
  wind:0, turnTime:45, timeLeft:45,
  cam:{x:WORLD_W/2,y:WORLD_H/2,zoom:.8,tz:.8},
  shake:0, freeze:0, selectedWeapon:0,
  aimAngle:0, power:0, charging:false, mouse:{x:0,y:0,down:false},
  hoverTeleport:false, winner:null, tickAcc:0, lastTickT:99,
};
const TEAM_DEFS=[
  {name:'RED RAIDERS', col:'#ff5a52', glow:'rgba(255,90,82,.6)', body:'#e04a42'},
  {name:'BLUE BARRAGE',col:'#4da3ff', glow:'rgba(77,163,255,.6)', body:'#3f8fe0'},
  {name:'GREEN GOONS', col:'#5ede6a', glow:'rgba(94,222,106,.6)', body:'#46c452'},
  {name:'GOLD GRUBS',  col:'#ffd23d', glow:'rgba(255,210,61,.6)', body:'#eab828'},
];
const WORM_NAMES=['Boggy','Dibbles','Spadge','Clagnut','Chuck','Norris','Rambo','Sarge','Noodle','Wiggles','Squirm','Mudkip','Tunnel','Bazza','Sheila','Bruce'];

/* ---------------- WEAPONS ---------------- */
const WEAPONS=[
 {id:'bazooka', name:'BAZOOKA', icon:'🚀', cat:'INDIRECT', ammo:-1, radius:42, dmg:55, grav:GRAV, windF:1, bounce:0, fuse:0, knock:7, big:true},
 {id:'grenade', name:'GRENADE', icon:'💣', cat:'INDIRECT', ammo:-1, radius:38, dmg:50, grav:GRAV, windF:.3, bounce:.45, fuse:3, knock:6},
 {id:'cluster', name:'CLUSTER', icon:'🧨', cat:'INDIRECT', ammo:3, radius:30, dmg:32, grav:GRAV, windF:1, bounce:0, fuse:0, knock:5, cluster:6},
 {id:'shotgun', name:'SHOTGUN', icon:'🔫', cat:'DIRECT', ammo:5, radius:20, dmg:27, hitscan:true, knock:4, shots:2},
 {id:'dynamite',name:'DYNAMITE',icon:'🧯', cat:'THROWN', ammo:2, radius:48, dmg:65, drop:true, fuse:3.5, knock:9, big:true},
 {id:'holy',    name:'HOLY GRENADE',icon:'🐑',cat:'HIGH-AOE',ammo:1, radius:95, dmg:95, grav:GRAV, windF:.2, bounce:.35, fuse:4, knock:14, big:true, holy:true},
 {id:'teleport',name:'TELEPORT', icon:'✨', cat:'UTILITY', ammo:2, utility:'tp'},
 {id:'girder',  name:'GIRDER',  icon:'📏', cat:'UTILITY', ammo:2, utility:'girder'},
];

/* ---------------- WORMS ---------------- */
function makeWorm(team,x,y,name){
  return {team,name,x,y,vx:0,vy:0,hp:100,dead:false,facing:1,grounded:false,
    fallStart:null,onWater:false,breath:2.5,anim:rnd(0,9),aimA:0};
}
function spawnTeams(nTeams,cpuMode){
  G.teams=[]; G.worms=[];
  const names=[...WORM_NAMES].sort(()=>Math.random()-.5);
  for(let t=0;t<nTeams;t++){
    const isCpu = cpuMode==='cpu' || (cpuMode==='mix' && t>0);
    G.teams.push({...TEAM_DEFS[t], cpu:isCpu, worms:[]});
    // spread spawn points
    for(let w=0;w<4;w++){
      let x,tries=0;
      do{ x=rnd(80,WORLD_W-80); tries++; }
      while(tries<60 && (Terrain.groundY(x,0)>=WORLD_H-40 ||
        G.worms.some(o=>dist(o.x,o.y,x,0)<70)));
      const gy=Terrain.groundY(x,0);
      const wm=makeWorm(t,x,gy-WORM_R-1,names.pop()||('Worm'+t+w));
      G.teams[t].worms.push(wm); G.worms.push(wm);
    }
  }
}
function curTeam(){ return G.teams[G.activeTeam]; }
function curWorm(){ return curTeam().worms[G.activeWorm]; }
function aliveWorms(){ return G.worms.filter(w=>!w.dead); }

/* ---------------- PHYSICS: WORM MOVEMENT ---------------- */
function moveWorm(w,dx){
  const dir=Math.sign(dx);
  if(dir)w.facing=dir;
  const step=Math.abs(dx);
  for(let i=0;i<step;i++){
    const nx=w.x+dir*1;
    // try walk; climb up to 7px, drop any
    let placed=false;
    for(let up=0;up<=7;up++){
      if(!bodyBlocked(nx,w.y-up)){ w.x=nx; w.y-=up; placed=true; break; }
    }
    if(!placed){ break; }
    // gravity snap down
    let dy=0; while(dy<6 && !bodyBlocked(w.x,w.y+1)){ w.y++; dy++; }
  }
}
function bodyBlocked(x,y){ // circle test
  for(let a=0;a<8;a++){
    const an=a/8*Math.PI*2;
    if(Terrain.solid(x+Math.cos(an)*WORM_R*.8,y+Math.sin(an)*WORM_R*.8))return true;
  }
  return Terrain.solid(x,y+WORM_R);
}
function wormPhysics(w,dt){
  // gravity & integrate
  w.vy+=GRAV*(dt*60);
  w.x+=w.vx*(dt*60); w.y+=w.vy*(dt*60);
  // water
  if(w.y>WATER_Y+4){
    if(!w.onWater){ AudioSys.splash(); spawnParticles(w.x,WATER_Y,14,{col:'#bfe6ff',spMax:3,grav:.05}); }
    w.onWater=true;
    // buoyancy: floats briefly then drowns
    w.vy=Math.min(w.vy,-.35); w.vx*=.95;
    w.breath-=dt;
    if(w.breath<=0 && !w.dead){ damage(w,rndi(20,30)*dt*3|0||1,'💦',false,true); }
    w.grounded=false;
    return;
  } else if(w.onWater){ w.onWater=false; w.breath=2.5; }
  // world bounds
  w.x=clamp(w.x,WORM_R,WORLD_W-WORM_R);
  if(w.y>WORLD_H+100){ damage(w,999,'💀'); return; }
  // collision resolve
  if(bodyBlocked(w.x,w.y)){
    // land: push up out of ground
    let pushed=false;
    for(let k=0;k<WORM_R*2;k++){ w.y--; if(!bodyBlocked(w.x,w.y)){pushed=true;break;} }
    if(!pushed){ w.x-=w.vx*(dt*60); w.y+=w.vy*(dt*60)+1; }
    // fall damage
    if(w.fallStart!==null){
      const fall=w.y-w.fallStart;
      if(fall>85 && !w.dead){
        const dmg=Math.min(45,((fall-85)/8)|0);
        if(dmg>0){ damage(w,dmg,'🤕'); spawnParticles(w.x,w.y,10,{col:'#9a7b52'}); }
        else AudioSys.walk();
      }
      w.fallStart=null;
    }
    w.vy=0; w.vx*=.55;
    if(Math.abs(w.vx)<.15)w.vx=0;
    w.grounded=true;
  } else {
    if(w.grounded && w.vy>0.5){} 
    if(!w.grounded && w.fallStart===null && w.vy>0) w.fallStart=w.y;
    if(w.vy>0 && w.fallStart===null) w.fallStart=w.y;
    w.grounded=false;
  }
  if(w.grounded && Math.abs(w.vx)>0){ w.vx*=.82; }
}
function jump(w){
  if(!w.grounded)return;
  w.vy=-4.6; w.vx=w.facing*2.4; w.grounded=false; w.fallStart=null;
  AudioSys.jump(); spawnParticles(w.x,w.y+WORM_R,6,{col:'#9a7b52',spMax:2});
}

/* ---------------- DAMAGE / EXPLOSIONS ---------------- */
function damage(w,amt,label,silentKnock,noSound){
  if(w.dead)return;
  amt=Math.round(amt);
  w.hp-=amt;
  floatText(w.x+rnd(-6,6),w.y-16,(amt>=0?'-':'+')+Math.abs(amt)+' '+(label||''),amt>=0?'#ff6a5e':'#7dff8a',amt>=30?22:15);
  if(!noSound&&amt>0)AudioSys.hurt();
  if(w.hp<=0){ w.hp=0; killWorm(w); }
  updateHUD();
}
function killWorm(w){
  if(w.dead)return; w.dead=true;
  AudioSys.die();
  spawnParticles(w.x,w.y,26,{fire:true,spMax:6});
  floatText(w.x,w.y-24,'💥 R.I.P.',"#ffdd55",18);
  explosion(w.x,w.y,28,20,4,false);
  checkWin();
}
function explosion(x,y,radius,maxDmg,knock,big=true){
  AudioSys.boom(big);
  spawnParticles(x,y,big?46:24,{fire:true,spMax:big?8:5,size:big?5:3});
  spawnParticles(x,y,big?22:12,{col:'#5b4632',spMax:big?6:4,size:big?5:3});
  spawnParticles(x,y,10,{col:'#fff',spMax:big?9:6,life:.25,size:big?6:4,grav:0});
  spawnChunkDebris(x,y,radius);
  const removed=Terrain.destroy(x,y,radius);
  G.shake=Math.min(22,G.shake+(big?14:6));
  if(big)G.freeze=Math.max(G.freeze,.13);
  // ring flash
  particles.push({x,y,vx:0,vy:0,life:.3,age:0,size:radius,col:'#fff',grav:0,ring:true});
  // damage worms
  for(const w of aliveWorms()){
    const d=dist(x,y,w.x,w.y);
    if(d<radius+20){
      const f=clamp(1-d/(radius+20),0,1);
      const dmg=maxDmg*f*(d<radius?1:.5);
      if(dmg>1)damage(w,dmg,'');
      // knockback
      const ang=Math.atan2(w.y-y,w.x-x);
      const kb=(knock*f)*(w.grounded?.8:1);
      w.vx+=Math.cos(ang)*kb; w.vy+=Math.sin(ang)*kb-1.2;
      w.grounded=false; w.fallStart=null;
    }
  }
  // nudge projectiles/debris
  for(const p of projectiles){ const d=dist(x,y,p.x,p.y); if(d<radius*1.4){const a=Math.atan2(p.y-y,p.x-x);p.vx+=Math.cos(a)*3;p.vy+=Math.sin(a)*3;} }
  settleCheck();
}

/* ---------------- PROJECTILES ---------------- */
function fireProjectile(opt){
  projectiles.push({x:opt.x,y:opt.y,vx:opt.vx,vy:opt.vy,
    wpn:opt.wpn,fuse:opt.wpn.fuse||0,age:0,shotsLeft:opt.shots||0,trail:[],holyPrayed:false});
}
function updateProjectiles(dt){
  for(let i=projectiles.length-1;i>=0;i--){
    const p=projectiles[i], W=p.wpn;
    p.age+=dt;
    if(W.grav)p.vy+=W.grav*(dt*60);
    p.vx+=G.wind*0.004*(W.windF||0)*(dt*60);
    const steps=Math.ceil(Math.max(Math.abs(p.vx),Math.abs(p.vy))*(dt*60)/3)||1;
    let hit=false;
    for(let s=0;s<steps&&!hit;s++){
      p.x+=p.vx*(dt*60)/steps; p.y+=p.vy*(dt*60)/steps;
      if(p.x<0||p.x>WORLD_W){ hit=true; break; }
      // water
      if(p.y>WATER_Y){
        AudioSys.splash(); spawnParticles(p.x,WATER_Y,12,{col:'#bfe6ff',spMax:3,grav:.06,ang:-Math.PI/2,spread:1});
        projectiles.splice(i,1); splashText(p); hit=true; break;
      }
      if(Terrain.solid(p.x,p.y)){
        if(W.bounce>0){
          // reflect: find normal approx
          const nx=Terrain.solid(p.x-p.vx,p.y)?-1:0, ny=nx===0?-1:0;
          if(nx){p.vx=-p.vx*W.bounce;} if(ny){p.vy=-p.vy*W.bounce;}
          p.x-= p.vx*dt*2; p.y-=p.vy*dt*2;
          if(Math.hypot(p.vx,p.vy)>1.2)AudioSys.bounce();
          p.vx*=.92;
        } else { hit=true; }
      }
      // direct worm contact
      if(!hit && W.hitscan!==true){
        for(const w of aliveWorms()){
          if(w===p.owner)continue;
          if(dist(p.x,p.y,w.x,w.y)<WORM_R+3){ hit=true; break; }
        }
      }
    }
    p.trail.push({x:p.x,y:p.y}); if(p.trail.length>14)p.trail.shift();
    if(hit){
      projectiles.splice(i,1);
      detonate(p);
      continue;
    }
    if(p.fuse>0 && p.age>=p.fuse){ projectiles.splice(i,1); detonate(p,true); continue; }
    if(p.age>12){ projectiles.splice(i,1); endTurnSoon(); }
  }
}
function splashText(p){ floatText(p.x,WATER_Y-10,'💧 sploosh!','#9fdcff',13); settleCheck(); }
function detonate(p,airburst){
  const W=p.wpn;
  if(W.cluster){
    explosion(p.x,p.y,W.radius,W.dmg,false);
    for(let i=0;i<W.cluster;i++){
      projectiles.push({x:p.x,y:p.y-4,vx:rnd(-3.5,3.5),vy:rnd(-6,-2),wpn:{...W,cluster:0,id:'bomblit',name:'bomblet',radius:22,dmg:22,knock:4},fuse:rnd(.6,1.4),age:0,trail:[]});
    }
    return;
  }
  explosion(p.x,p.y,W.radius,W.dmg,W.knock,W.big);
  if(W.hitscan){ /* shotgun handled separately */ }
}