← Worms results

Worms

muse spark 1.2 contributor · 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 53,500 bytes · SHA-256 f52524c8606c
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WORMBLAST — Artillery Mayhem</title>
<link href="https://fonts.googleapis.com/css2?family=Fredoka:wght@500;700;800&family=Space+Mono:wght@700&family=Bangers&family=Luckiest+Guy&display=swap" rel="stylesheet">
<style>
*{margin:0;padding:0;box-sizing:border-box}
:root{--bg:#070d1f;--panel:#111d33;--border:#1e3a5f;--amber:#facc15;--lime:#a3e635;--cyan:#22d3ee}
body{background:#050a1a;color:#fff;font-family:'Fredoka',system-ui,sans-serif;overflow:hidden;height:100vh;display:flex;flex-direction:column}
.topbar{height:54px;background:linear-gradient(180deg,#0f2440 0%,#0a162c 100%);border-bottom:2px solid #1e3a5f;display:flex;align-items:center;justify-content:space-between;padding:0 14px;z-index:20;box-shadow:0 4px 20px rgba(0,0,0,.5)}
.logo{font-family:'Luckiest Guy',cursive;font-size:30px;letter-spacing:1px;color:#fff;text-shadow:0 3px 0 #000,0 0 18px rgba(250,204,21,.5);display:flex;align-items:center;gap:8px}
.logo span{color:#facc15}
.logo i{font-style:normal;color:#ff3b30;text-shadow:0 2px 0 #000}
.top-stats{display:flex;gap:8px;align-items:center}
.pill{background:rgba(0,0,0,.35);border:1px solid #1e3a5f;border-radius:999px;padding:5px 12px;display:flex;align-items:center;gap:8px;font-size:11px;font-weight:800;letter-spacing:.5px;backdrop-filter:blur(6px)}
.pill b{color:#facc15;font-family:'Space Mono',monospace}
.wind-pill{width:170px;justify-content:space-between}
.wind-bar{flex:1;height:8px;background:#0a162c;border-radius:999px;overflow:hidden;position:relative;margin:0 6px;border:1px solid #1e3a5f}
.wind-fill{position:absolute;top:0;bottom:0;background:linear-gradient(90deg,#ef4444,#facc15,#22d3ee);border-radius:999px;transition:all .4s}
.wind-arrow{position:absolute;top:50%;transform:translate(-50%,-50%);font-size:10px;filter:drop-shadow(0 1px 2px #000)}
.main{flex:1;display:flex;overflow:hidden;position:relative}
.left{width:320px;background:linear-gradient(180deg,#0f1d33 0%,#0a162c 100%);border-right:2px solid #1e3a5f;display:flex;flex-direction:column;overflow:hidden;box-shadow:4px 0 24px rgba(0,0,0,.4)}
.right{flex:1;position:relative;background:#020617;overflow:hidden}
canvas#game{position:absolute;inset:0;width:100%;height:100%;display:block}
#terrainCanvas{display:none}
.hud-overlay{position:absolute;inset:0;pointer-events:none}
.turn-banner{position:absolute;top:12px;left:50%;transform:translateX(-50%);background:linear-gradient(180deg,#fde047 0%,#facc15 50%,#eab308 100%);color:#422006;padding:6px 10px 6px 14px;border-radius:999px;font-family:'Luckiest Guy',cursive;font-size:18px;letter-spacing:.5px;box-shadow:0 5px 0 #854d0e,0 10px 24px rgba(0,0,0,.45),inset 0 1px 0 rgba(255,255,255,.6);display:flex;align-items:center;gap:10px;pointer-events:auto;border:2px solid #000;white-space:nowrap}
.turn-banner .dot{width:16px;height:16px;border-radius:50%;border:2px solid #000;box-shadow:inset 0 2px 4px rgba(255,255,255,.5)}
.timer-ring{width:44px;height:44px;border-radius:50%;background:conic-gradient(#06b6d4 var(--p,0%),#0f172a 0);display:grid;place-items:center;font-family:'Space Mono',monospace;font-weight:800;font-size:15px;color:#fff;border:2px solid #000;box-shadow:0 2px 0 #000,0 0 12px rgba(6,182,214,.5);flex-shrink:0}
.timer-ring.panic{animation:panic .35s infinite alternate}
@keyframes panic{0%{transform:scale(1) rotate(-2deg)}100%{transform:scale(1.15) rotate(2deg)}}
.weapon-bar{position:absolute;bottom:18px;left:50%;transform:translateX(-50%);display:flex;gap:7px;pointer-events:auto;flex-wrap:nowrap;max-width:96%;justify-content:center;padding:6px 10px;background:rgba(2,6,23,.55);backdrop-filter:blur(8px);border:1px solid rgba(255,255,255,.08);border-radius:16px}
.wbtn{width:62px;height:62px;border-radius:13px;background:linear-gradient(180deg,#1e293b,#0f172a);border:2px solid #334155;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:1px;cursor:pointer;transition:.15s;position:relative;box-shadow:0 4px 0 #000,0 6px 16px rgba(0,0,0,.4)}
.wbtn:hover{transform:translateY(-3px) scale(1.03);border-color:#facc15;box-shadow:0 6px 0 #000,0 10px 20px rgba(0,0,0,.5)}
.wbtn.active{background:linear-gradient(180deg,#fde047,#facc15);border-color:#000;color:#422006;box-shadow:0 4px 0 #854d0e,0 8px 20px rgba(250,204,21,.5);transform:translateY(-2px)}
.wbtn .ico{font-size:20px;filter:drop-shadow(0 1px 2px rgba(0,0,0,.5))}
.wbtn .lbl{font-size:7.5px;font-weight:800;letter-spacing:.5px;text-transform:uppercase;line-height:1}
.wbtn .cat{position:absolute;top:-7px;right:-6px;font-size:6.5px;padding:2px 5px;border-radius:999px;font-weight:800;color:#fff;border:1px solid #000;letter-spacing:.3px}
.cat-indirect{background:#a855f7}
.cat-direct{background:#ef4444}
.cat-aoe{background:#f97316}
.cat-util{background:#06b6d4;color:#000!important}
.ammo{position:absolute;bottom:2px;right:5px;font-size:8px;font-weight:800;opacity:.75;background:#000;color:#fff;padding:0 4px;border-radius:999px}
.power-meter{position:absolute;bottom:84px;left:50%;transform:translateX(-50%);width:300px;height:16px;background:#020617;border:2px solid #000;border-radius:999px;overflow:hidden;display:none;pointer-events:none;box-shadow:0 3px 0 #000}
.power-fill{height:100%;width:0%;background:linear-gradient(90deg,#22d3ee 0%,#a3e635 35%,#facc15 70%,#ef4444 100%);transition:width .05s;box-shadow:inset 0 2px 6px rgba(255,255,255,.4)}
.setup{padding:14px;display:flex;flex-direction:column;gap:12px;overflow:auto;scrollbar-width:thin}
.setup h2{font-family:'Luckiest Guy',cursive;font-size:24px;letter-spacing:.5px;color:#fff;text-shadow:0 2px 0 #000}
.setup h3{font-size:10px;letter-spacing:1.2px;text-transform:uppercase;color:#7dd3fc;margin-bottom:6px;font-weight:800}
.card{background:rgba(2,6,23,.7);border:1px solid #1e3a5f;border-radius:14px;padding:12px;backdrop-filter:blur(8px);box-shadow:inset 0 1px 0 rgba(255,255,255,.06)}
.row{display:flex;gap:8px;align-items:center}
.btn{border:2px solid #000;border-radius:999px;padding:10px 14px;font-weight:800;cursor:pointer;font-family:'Fredoka',sans-serif;transition:.15s;box-shadow:0 4px 0 #000;font-size:13px}
.btn:active{transform:translateY(2px);box-shadow:0 2px 0 #000}
.btn-primary{background:linear-gradient(180deg,#fde047,#facc15);color:#422006;border-color:#000;box-shadow:0 4px 0 #854d0e;text-shadow:0 1px 0 rgba(255,255,255,.5)}
.btn-primary:hover{filter:brightness(1.08);transform:translateY(-1px);box-shadow:0 5px 0 #854d0e}
.btn-dark{background:#1e293b;color:#e2e8f0;border-color:#334155;box-shadow:0 3px 0 #000}
.btn-ghost{background:transparent;color:#94a3b8;border:1px dashed #334155;box-shadow:none;font-size:11px}
.select{flex:1;background:#020617;color:#fff;border:1px solid #334155;border-radius:10px;padding:8px 10px;font-weight:700}
.team-row{display:flex;align-items:center;gap:8px;padding:8px;background:rgba(15,23,42,.8);border-radius:10px;border:1px solid #1e293b;margin-bottom:6px}
.team-dot{width:18px;height:18px;border-radius:50%;border:2px solid #000;box-shadow:inset 0 2px 4px rgba(255,255,255,.4)}
.team-row input[type="text"]{flex:1;background:transparent;border:none;color:#fff;font-weight:700;outline:none;width:80px;font-family:'Fredoka',sans-serif}
.toggle{width:38px;height:22px;background:#334155;border-radius:999px;position:relative;cursor:pointer;transition:.2s;border:1px solid #000}
.toggle.on{background:#a3e635;box-shadow:0 0 8px rgba(163,230,53,.5)}
.toggle::after{content:'';position:absolute;top:1px;left:1px;width:18px;height:18px;background:#fff;border-radius:50%;transition:.2s;box-shadow:0 1px 3px rgba(0,0,0,.4)}
.toggle.on::after{left:17px}
.terrain-opts{display:flex;gap:7px}
.t-opt{flex:1;height:62px;border-radius:11px;border:2px solid #1e3a5f;background:linear-gradient(180deg,#0f172a,#020617);display:flex;flex-direction:column;align-items:center;justify-content:center;gap:3px;cursor:pointer;font-size:10px;font-weight:800;transition:.15s}
.t-opt.active{border-color:#facc15;background:linear-gradient(180deg,#1e293b,#0f172a);box-shadow:0 0 12px rgba(250,204,21,.3),inset 0 1px 0 rgba(255,255,255,.1);transform:translateY(-1px)}
.t-opt span{font-size:18px}
.controls-info{font-size:11px;color:#94a3b8;line-height:1.5}
.controls-info kbd{background:#1e293b;border:1px solid #334155;border-bottom-width:2px;padding:1px 5px;border-radius:4px;font-family:'Space Mono',monospace;font-size:10px;color:#e2e8f0}
.health-grid{display:grid;grid-template-columns:1fr 1fr;gap:5px}
.worm-chip{display:flex;align-items:center;gap:5px;background:rgba(2,6,23,.8);border:1px solid #1e293b;border-radius:999px;padding:4px 7px;font-size:10px;font-weight:700;transition:.2s}
.worm-chip.active{border-color:currentColor;box-shadow:0 0 0 2px currentColor,0 0 10px currentColor;transform:scale(1.02)}
.hpbar{flex:1;height:5px;background:#0f172a;border-radius:999px;overflow:hidden;border:1px solid #000}
.hpbar i{display:block;height:100%;background:linear-gradient(90deg,#22c55e,#a3e635);transition:width .4s}
.floating-text{position:absolute;color:#fff;font-family:'Luckiest Guy',cursive;font-size:20px;text-shadow:0 2px 0 #000,0 0 8px rgba(0,0,0,.5);pointer-events:none}
.shake{animation:shake .35s}
@keyframes shake{0%,100%{transform:translate(0,0)}25%{transform:translate(-7px,4px)}50%{transform:translate(7px,-4px)}75%{transform:translate(-4px,-2px)}}
.menu-btn{display:none}
@media(max-width:900px){.left{position:absolute;inset:0;z-index:30;transform:translateX(-100%);transition:.32s cubic-bezier(.4,0,.2,1)}.left.open{transform:translateX(0)}.menu-btn{display:inline-flex}.topbar{height:48px}.weapon-bar{bottom:8px;gap:5px}.wbtn{width:52px;height:52px}.wbtn .ico{font-size:16px}}
</style>
</head>
<body>
<div class="topbar">
  <div class="logo">WORM<span>BLAST</span> <i>!</i></div>
  <div class="top-stats">
    <div class="pill wind-pill"><span style="color:#7dd3fc">WIND</span><div class="wind-bar"><div class="wind-fill" id="windFill"></div><div class="wind-arrow" id="windArrow">▶</div></div><b id="windVal">+2</b></div>
    <div class="pill">TURN <b id="turnInfo">—</b></div>
    <button class="btn btn-dark menu-btn" style="padding:6px 12px;font-size:12px" onclick="toggleMenu()">☰ MENU</button>
    <button class="btn btn-dark" style="padding:6px 12px;font-size:12px;display:none" id="deskMenuBtn" onclick="toggleMenu()">☰</button>
  </div>
</div>
<div class="main">
  <div class="left" id="leftPanel">
    <div class="setup">
      <h2>Battle Setup</h2>
      <div class="card">
        <h3>Terrain Shape</h3>
        <div class="terrain-opts">
          <div class="t-opt active" data-terrain="island" onclick="pickTerrain('island')"><span>🏝️</span>ISLAND</div>
          <div class="t-opt" data-terrain="two" onclick="pickTerrain('two')"><span>🏖️</span>TWIN</div>
          <div class="t-opt" data-terrain="cave" onclick="pickTerrain('cave')"><span>🕳️</span>CAVE</div>
        </div>
      </div>
      <div class="card">
        <h3>Teams (2-4)</h3>
        <div id="teamList"></div>
        <div class="row" style="margin-top:8px">
          <button class="btn btn-ghost" onclick="addTeam()" style="flex:1">+ ADD TEAM</button>
          <button class="btn btn-ghost" onclick="removeTeam()">− REMOVE</button>
        </div>
      </div>
      <div class="card">
        <h3>Match</h3>
        <div class="row" style="margin-bottom:10px"><label style="font-size:11px;font-weight:800;color:#7dd3fc">WIND</label><input type="range" id="windInput" min="-8" max="8" value="2" style="flex:1;accent-color:#facc15" oninput="updateWindPreview()"><span id="windPreview" style="font-weight:800;width:28px;text-align:center;background:#0f172a;border:1px solid #1e3a5f;border-radius:6px;padding:2px 0;font-size:12px">2</span></div>
        <button class="btn btn-primary" style="width:100%;font-size:15px;letter-spacing:.3px" onclick="startGame()">🚀 START BATTLE</button>
        <button class="btn btn-dark" style="width:100%;margin-top:8px" onclick="restartGame()">↻ RESTART</button>
      </div>
      <div class="card controls-info">
        <h3>Controls</h3>
        <div><kbd>A</kbd> <kbd>D</kbd> Move &nbsp;•&nbsp; <kbd>W</kbd> / <kbd>SPACE</kbd> Jump &nbsp;•&nbsp; Mouse Aim &nbsp;•&nbsp; Hold <kbd>SPACE</kbd> to charge & fire &nbsp;•&nbsp; <kbd>1-8</kbd> Weapons &nbsp;•&nbsp; <kbd>Q/E</kbd> Switch worm</div>
        <div style="margin-top:6px;color:#fde047;font-weight:700">💨 Wind pushes projectiles & worms — Water drowns! Watch fall damage.</div>
      </div>
      <div class="card">
        <h3>Teams Health</h3>
        <div id="healthGrid" class="health-grid"></div>
      </div>
    </div>
  </div>
  <div class="right" id="gameWrap">
    <canvas id="game" width="1280" height="720"></canvas>
    <canvas id="terrainCanvas" width="1280" height="720"></canvas>
    <div class="hud-overlay">
      <div class="turn-banner" id="turnBanner"><span class="dot" id="turnDot" style="background:#facc15"></span><span id="turnText">SETUP — PRESS START</span><div class="timer-ring" id="timerRing" style="--p:100%">30</div></div>
      <div class="power-meter" id="powerMeter"><div class="power-fill" id="powerFill"></div></div>
      <div class="weapon-bar" id="weaponBar"></div>
    </div>
  </div>
</div>

<script>
const canvas=document.getElementById('game'), ctx=canvas.getContext('2d');
const tCanvas=document.getElementById('terrainCanvas'), tCtx=tCanvas.getContext('2d', {willReadFrequently:true});
let W=1280,H=720;
const WATER_Y=H-68;
const GRAVITY=0.48;
const FRICTION=0.80;
let wind=2;
let terrainType='island';
let teams=[], worms=[], activeTeam=0, activeWormIdx=0, turnTime=30, turnTimer=null, gameState='setup';
let projectiles=[], particles=[], debris=[], floatTexts=[], camera={x:640,y:360,zoom:1,targetX:640,targetY:360};
let mouse={x:640,y:360}, charging=false, charge=0, angle=0;
let shakeTime=0, freezeFrames=0, windPhase=0;
let terrainImage=null;

const TEAM_COLORS=['#facc15','#22d3ee','#a3e635','#f472b6'];
const TEAM_NAMES=['SUN','AQUA','LIME','PINK'];
const WEAPONS=[
  {id:'bazooka',name:'Bazooka',ico:'🚀',cat:'indirect',dmg:48,rad:60,ammo:99,desc:'Parabolic rocket'},
  {id:'grenade',name:'Grenade',ico:'💣',cat:'indirect',dmg:58,rad:64,ammo:5,desc:'Bouncy 3s fuse'},
  {id:'shotgun',name:'Shotgun',ico:'🔫',cat:'direct',dmg:26,rad:0,ammo:4,desc:'2× instant hit'},
  {id:'sniper',name:'Sniper',ico:'🎯',cat:'direct',dmg:72,rad:0,ammo:3,desc:'Laser instant'},
  {id:'airstrike',name:'Airstrike',ico:'✈️',cat:'aoe',dmg:34,rad:54,ammo:2,desc:'5 bombs rain'},
  {id:'dynamite',name:'Dynamite',ico:'🧨',cat:'aoe',dmg:80,rad:98,ammo:2,desc:'Huge crater'},
  {id:'jetpack',name:'Jetpack',ico:'🎒',cat:'util',dmg:0,rad:0,ammo:2,desc:'Fly WASD'},
  {id:'teleport',name:'Teleport',ico:'🌀',cat:'util',dmg:0,rad:0,ammo:2,desc:'Click to warp'},
];
let selectedWeapon=0;

let audioCtx=null;
function beep(freq,dur,type='sine',vol=0.18){
  try{
    if(!audioCtx) audioCtx=new (window.AudioContext||window.webkitAudioContext)();
    if(audioCtx.state==='suspended') audioCtx.resume();
    const o=audioCtx.createOscillator(), g=audioCtx.createGain();
    o.type=type; o.frequency.value=freq; o.connect(g); g.connect(audioCtx.destination);
    g.gain.value=vol; o.start(); g.gain.exponentialRampToValueAtTime(0.01,audioCtx.currentTime+dur); o.stop(audioCtx.currentTime+dur);
  }catch(e){}
}
function sfx(name){
  if(name==='jump'){beep(480,0.12,'square',0.14); setTimeout(()=>beep(620,0.08,'sine',0.1),60);}
  if(name==='shoot') {beep(140,0.28,'sawtooth',0.22); setTimeout(()=>beep(90,0.2,'triangle',0.18),70);}
  if(name==='explosion'){beep(70,0.45,'sawtooth',0.32); setTimeout(()=>beep(35,0.55,'triangle',0.28),70); setTimeout(()=>beep(120,0.2,'square',0.15),120);}
  if(name==='tick') beep(880,0.07,'sine',0.11);
  if(name==='tickFast') beep(1180,0.05,'square',0.13);
  if(name==='select') beep(720,0.08,'sine',0.11);
  if(name==='hurt'){beep(220,0.18,'square',0.18); setTimeout(()=>beep(160,0.15,'sawtooth',0.14),80);}
  if(name==='charge'){beep(300,0.04,'sine',0.06);}
}

function initTeams(){
  teams=[
    {name:'SUN SQUAD',color:TEAM_COLORS[0],cpu:false},
    {name:'AQUA CREW',color:TEAM_COLORS[1],cpu:true},
  ];
  renderTeamList();
}
function renderTeamList(){
  const el=document.getElementById('teamList'); el.innerHTML='';
  teams.forEach((t,i)=>{
    const row=document.createElement('div'); row.className='team-row';
    row.innerHTML=`<div class="team-dot" style="background:${t.color}"></div>
      <input type="text" value="${t.name}" onchange="teams[${i}].name=this.value.toUpperCase()" maxlength="12">
      <span style="font-size:9px;font-weight:800;color:#7dd3fc;letter-spacing:.5px">CPU</span>
      <div class="toggle ${t.cpu?'on':''}" onclick="teams[${i}].cpu=!teams[${i}].cpu;this.classList.toggle('on');sfx('select')"></div>`;
    el.appendChild(row);
  });
  updateHealthGrid();
}
function addTeam(){ if(teams.length>=4) return; const c=TEAM_COLORS[teams.length]; teams.push({name:TEAM_NAMES[teams.length]+' TEAM',color:c,cpu:teams.length%2===1}); renderTeamList(); sfx('select')}
function removeTeam(){ if(teams.length<=2) return; teams.pop(); renderTeamList(); sfx('select')}
function pickTerrain(t){ terrainType=t; document.querySelectorAll('.t-opt').forEach(o=>o.classList.toggle('active',o.dataset.terrain===t)); sfx('select')}
function updateWindPreview(){ document.getElementById('windPreview').textContent=document.getElementById('windInput').value; }

class Worm{
  constructor(teamIdx, idx, x,y){
    this.teamIdx=teamIdx; this.idx=idx; this.x=x; this.y=y; this.vx=0; this.vy=0;
    this.w=18; this.h=22; this.hp=100; this.maxHp=100; this.alive=true;
    this.onGround=false; this.fallDist=0; this.drownT=0; this.facing=1;
    this.weapons={}; WEAPONS.forEach(w=>this.weapons[w.id]=w.ammo);
    this.hasJetpack=false; this.jetFuel=0; this.bob= Math.random()*Math.PI*2;
  }
  update(){
    if(!this.alive) return;
    // water physics - float & drown
    if(this.y+this.h/2 > WATER_Y){
      this.vy-=0.38;
      this.vx*=0.92;
      this.vy*=0.96;
      this.drownT++;
      if(this.drownT>85){ this.damage(3); this.drownT=72; spawnParticles(this.x,this.y-8,3,'#22d3ee'); }
      this.y+=Math.sin(Date.now()*0.004+this.bob)*0.35;
      // drift with wind on water
      this.x+= wind*0.04;
    } else {
      this.drownT=Math.max(0,this.drownT-2);
    }
    this.vx+=wind*0.007;
    if(!this.onGround) this.vy+=GRAVITY;
    let nx=this.x+this.vx, ny=this.y+this.vy;
    // vertical collision
    if(isSolid(nx, ny+this.h/2)){
      // find surface
      let found=false;
      for(let k=0;k<16;k++){
        if(!isSolid(nx, ny+this.h/2 -k)){ ny= ny -k; found=true; break; }
      }
      if(found){ this.vy=0; this.onGround=true; }
      else { this.vx*=-0.35; nx=this.x; this.vy*=0.5; }
    } else {
      this.onGround=false;
    }
    if(isSolid(nx+ Math.sign(this.vx)*this.w/2, ny)){
      this.vx*=-0.25; nx=this.x;
    }
    this.x=nx; this.y=ny;
    this.vx*= this.onGround? FRICTION:0.985;
    this.vy*=0.985;
    this.x=Math.max(18,Math.min(W-18,this.x));
    if(!this.onGround) this.fallDist+=Math.max(0,this.vy);
    else if(this.fallDist>20){ let dmg=Math.floor((this.fallDist-20)*1.9); if(dmg>0){this.damage(dmg); spawnFloatText(this.x,this.y-30,`-${dmg} FALL!`,'#f97316')} this.fallDist=0; }
    else this.fallDist*=0.88;
    if(this.y> H+50) {this.hp=0; this.alive=false;}
    if(this.hp<=0 && this.alive){ this.alive=false; spawnParticles(this.x,this.y,20,'#fff'); sfx('hurt'); }
  }
  damage(d){
    this.hp=Math.max(0,this.hp-d);
    spawnFloatText(this.x,this.y-28,`-${d}`,'#ff3b30');
    sfx('hurt');
    if(this.hp<=0) this.alive=false;
  }
}

function generateTerrain(){
  tCtx.clearRect(0,0,W,H);
  tCtx.save();
  if(terrainType==='island'){
    // base rock
    tCtx.fillStyle='#0f172a';
    tCtx.beginPath();
    tCtx.moveTo(0,H);
    for(let x=0;x<=W;x+=8){
      let nx=x/W;
      let h= 190 + Math.sin(nx*Math.PI)*170 + Math.sin(nx*7)*14 + Math.cos(nx*13)*8;
      let y=H-68 - h*0.62;
      if(x===0) tCtx.lineTo(x,y); else tCtx.lineTo(x,y);
    }
    tCtx.lineTo(W,H); tCtx.lineTo(0,H); tCtx.closePath(); tCtx.fill();
    // grass layer with highlight
    tCtx.fillStyle='#84cc16';
    tCtx.beginPath();
    for(let x=0;x<=W;x+=8){
      let nx=x/W; let h= 190 + Math.sin(nx*Math.PI)*170 + Math.sin(nx*7)*14;
      let y=H-68 - h*0.62;
      if(x===0) tCtx.moveTo(x,y); else tCtx.lineTo(x,y);
    }
    tCtx.lineTo(W, H-68 - 190*0.62 +22); tCtx.lineTo(0, H-68 - 190*0.62 +22); tCtx.closePath(); tCtx.fill();
    // lighter grass top
    tCtx.fillStyle='#a3e635';
    tCtx.beginPath();
    for(let x=0;x<=W;x+=8){
      let nx=x/W; let h= 190 + Math.sin(nx*Math.PI)*170 + Math.sin(nx*7)*14;
      let y=H-68 - h*0.62;
      if(x===0) tCtx.moveTo(x,y); else tCtx.lineTo(x,y);
    }
    tCtx.lineTo(W, H-68 - 190*0.62 +10); tCtx.lineTo(0, H-68 - 190*0.62 +10); tCtx.closePath(); tCtx.fill();
    // speckles
    tCtx.fillStyle='rgba(0,0,0,.14)';
    for(let i=0;i<70;i++){ let x=Math.random()*W, y= H-110 - Math.random()*110; if(isSolidApprox(x,y)) tCtx.fillRect(x,y,3,3)}
  } else if(terrainType==='two'){
    const drawIsland=(cx,w,h, col1,col2)=>{
      tCtx.fillStyle=col1;
      tCtx.beginPath();
      for(let x=cx-w/2;x<=cx+w/2;x+=7){
        let nx=(x-(cx-w/2))/w;
        let hh= Math.sin(nx*Math.PI)*h + Math.sin(nx*9)*10 + Math.cos(nx*5)*6;
        let y=H-68 - hh;
        if(x===cx-w/2) tCtx.moveTo(x,y); else tCtx.lineTo(x,y);
      }
      tCtx.lineTo(cx+w/2,H); tCtx.lineTo(cx-w/2,H); tCtx.closePath(); tCtx.fill();
      tCtx.fillStyle=col2;
      tCtx.beginPath();
      for(let x=cx-w/2;x<=cx+w/2;x+=7){
        let nx=(x-(cx-w/2))/w;
        let hh= Math.sin(nx*Math.PI)*h + Math.sin(nx*9)*10;
        let y=H-68 - hh;
        if(x===cx-w/2) tCtx.moveTo(x,y); else tCtx.lineTo(x,y);
      }
      tCtx.lineTo(cx+w/2,H-68 - h*0.1); tCtx.lineTo(cx-w/2,H-68 - h*0.1); tCtx.closePath(); tCtx.fill();
    };
    drawIsland(300,500,165,'#0f172a','#84cc16');
    drawIsland(980,500,155,'#0f172a','#65a30d');
    // bridge hint texture between islands stays water gap
  } else if(terrainType==='cave'){
    tCtx.fillStyle='#0f172a';
    tCtx.fillRect(0,0,W,H);
    tCtx.globalCompositeOperation='destination-out';
    tCtx.beginPath();
    tCtx.ellipse(W/2, H/2+38, 540, 205, 0,0,Math.PI*2); tCtx.fill();
    for(let i=0;i<7;i++){ let x=180+Math.random()*920, y= H/2+ Math.random()*140-20; tCtx.beginPath(); tCtx.arc(x,y,38+Math.random()*28,0,Math.PI*2); tCtx.fill(); }
    // stalactites
    for(let i=0;i<10;i++){ let x=120+i*110+Math.random()*40; tCtx.beginPath(); tCtx.moveTo(x, H/2-165); tCtx.lineTo(x-14, H/2-120); tCtx.lineTo(x+14, H/2-120); tCtx.closePath(); tCtx.fill(); }
    tCtx.globalCompositeOperation='source-over';
    tCtx.fillStyle='#1e293b';
    tCtx.beginPath();
    for(let x=0;x<=W;x+=10){
      let y= H/2+142 + Math.sin(x*0.018)*13 + Math.cos(x*0.04)*7;
      if(x===0) tCtx.moveTo(x,y); else tCtx.lineTo(x,y);
    }
    tCtx.lineTo(W,H); tCtx.lineTo(0,H); tCtx.closePath(); tCtx.fill();
    tCtx.fillStyle='#84cc16';
    tCtx.beginPath();
    for(let x=0;x<=W;x+=10){
      let y= H/2+142 + Math.sin(x*0.018)*13;
      if(x===0) tCtx.moveTo(x,y); else tCtx.lineTo(x,y);
    }
    tCtx.lineTo(W, H/2+152); tCtx.lineTo(0, H/2+152); tCtx.closePath(); tCtx.fill();
    tCtx.fillStyle='#a3e635';
    tCtx.beginPath();
    for(let x=0;x<=W;x+=10){
      let y= H/2+142 + Math.sin(x*0.018)*13;
      if(x===0) tCtx.moveTo(x,y); else tCtx.lineTo(x,y);
    }
    tCtx.lineTo(W, H/2+147); tCtx.lineTo(0, H/2+147); tCtx.closePath(); tCtx.fill();
  }
  tCtx.restore();
  cacheTerrain();
}
function isSolidApprox(x,y){
  // quick check for speckles without reading image
  return y > H-220;
}
function cacheTerrain(){
  terrainImage=tCtx.getImageData(0,0,W,H);
}
function isSolid(x,y){
  if(x<0||x>=W||y<0||y>=H) return false;
  if(y>WATER_Y+8) return false;
  const ix=Math.floor(x), iy=Math.floor(y);
  const idx=(iy*W+ix)*4+3;
  return terrainImage.data[idx]>12;
}
function destroyTerrain(cx,cy,rad){
  tCtx.globalCompositeOperation='destination-out';
  tCtx.beginPath(); tCtx.arc(cx,cy,rad,0,Math.PI*2); tCtx.fill();
  for(let i=0;i<5;i++){ let a=Math.random()*Math.PI*2, r=rad*0.55+Math.random()*rad*0.45; tCtx.beginPath(); tCtx.arc(cx+Math.cos(a)*r*0.65, cy+Math.sin(a)*r*0.65, 7+Math.random()*11,0,Math.PI*2); tCtx.fill(); }
  tCtx.globalCompositeOperation='source-over';
  cacheTerrain();
  for(let i=0;i<7;i++){
    debris.push({x:cx+(Math.random()-0.5)*rad*1.1, y:cy+(Math.random()-0.5)*rad*0.8, vx:(Math.random()-0.5)*7, vy:Math.random()*-4 -1, w:9+Math.random()*15, h:7+Math.random()*11, rot:Math.random()*Math.PI, vr:(Math.random()-0.5)*0.32, col: Math.random()>0.5?'#334155':'#475569'});
  }
}

function spawnParticles(x,y,count,col='#facc15'){
  for(let i=0;i<count;i++) particles.push({x,y, vx:(Math.random()-0.5)*9, vy:(Math.random()-0.5)*9 -2, life:1, decay:0.032+Math.random()*0.038, r:2+Math.random()*4.5, col});
}
function spawnFloatText(x,y,txt,col='#fff'){
  floatTexts.push({x,y,txt,col,life:1, vy:-1.4});
}

function fireWeapon(worm, weapon, ang, pow){
  const w=WEAPONS.find(v=>v.id===weapon);
  if(!w) return;
  if(worm.weapons[w.id]<=0 && w.ammo!==99) return;
  if(w.ammo!==99) worm.weapons[w.id]--;
  sfx('shoot');
  const speed= pow*0.56;
  const vx=Math.cos(ang)*speed + wind*0.16;
  const vy=Math.sin(ang)*speed;
  const sx=worm.x+ Math.cos(ang)*22, sy=worm.y+ Math.sin(ang)*10 -5;
  if(w.id==='bazooka'){
    projectiles.push({x:sx,y:sy,vx,vy,rad:w.rad,dmg:w.dmg,type:'bazooka',trail:[]});
  } else if(w.id==='grenade'){
    projectiles.push({x:sx,y:sy,vx:vx*0.88,vy:vy*0.88,rad:w.rad,dmg:w.dmg,type:'grenade',fuse:135,trail:[]});
  } else if(w.id==='shotgun'){
    for(let i=0;i<2;i++){
      let a=ang+(Math.random()-0.5)*0.16;
      let hit=raycast(sx,sy,Math.cos(a),Math.sin(a), 900);
      if(hit.hitWorm) hit.hitWorm.damage(w.dmg);
      else if(hit.hitTerrain) destroyTerrain(hit.x,hit.y,13);
      spawnParticles(hit.x,hit.y,11,'#facc15');
      particles.push({x:sx,y:sy, vx:0,vy:0, life:1, decay:0.22, r:2, col:'#fff', tracer:true, tx:hit.x, ty:hit.y});
    }
    shakeTime=8; freezeFrames=4;
    endTurnSoon();
  } else if(w.id==='sniper'){
    let hit=raycast(sx,sy,Math.cos(ang),Math.sin(ang),1200);
    if(hit.hitWorm) hit.hitWorm.damage(w.dmg);
    else if(hit.hitTerrain) destroyTerrain(hit.x,hit.y,14);
    spawnParticles(hit.x,hit.y,15,'#22d3ee');
    particles.push({x:sx,y:sy, vx:0,vy:0, life:1, decay:0.14, r:1, col:'#22d3ee', laser:{x1:sx,y1:sy,x2:hit.x,y2:hit.y}});
    shakeTime=14; freezeFrames=7;
    endTurnSoon();
  } else if(w.id==='airstrike'){
    for(let i=0;i<5;i++){
      let bx= worm.x + (Math.cos(ang)*210) + (i-2)*92 + (Math.random()-0.5)*24;
      setTimeout(()=>{
        projectiles.push({x:bx,y:42, vx:wind*0.12, vy:6.5, rad:54, dmg:34, type:'bazooka', trail:[]});
        spawnParticles(bx,42,6,'#f97316');
      }, i*200);
    }
    endTurnSoon(1750);
  } else if(w.id==='dynamite'){
    projectiles.push({x:worm.x,y:worm.y+12, vx:Math.cos(ang)*1.8, vy:-1.5, rad:98, dmg:80, type:'dynamite', fuse:85, trail:[]});
  } else if(w.id==='jetpack'){
    worm.hasJetpack=true; worm.jetFuel=165; sfx('select');
    // don't end turn immediately - allow flying
    setTimeout(()=>endTurnSoon(3200), 3200);
    updateWeaponBar(); return;
  } else if(w.id==='teleport'){
    return;
  }
  updateWeaponBar();
}
function raycast(x,y,dx,dy,max){
  let cx=x, cy=y;
  for(let i=0;i<max;i+=6){
    cx+=dx*6; cy+=dy*6;
    if(isSolid(cx,cy)) return {x:cx,y:cy,hitTerrain:true};
    for(let w of worms){ if(!w.alive) continue; if(Math.hypot(w.x-cx,w.y-cy)<17) return {x:cx,y:cy,hitWorm:w};}
    if(cy>WATER_Y) break;
  }
  return {x:cx,y:cy,hitTerrain:false};
}
function endTurnSoon(delay=1200){ setTimeout(()=>nextTurn(),delay); }

function startGame(){
  wind=parseInt(document.getElementById('windInput').value)||0;
  generateTerrain();
  worms=[];
  for(let t=0;t<teams.length;t++){
    for(let i=0;i<4;i++){
      let x= 90 + (t* (W-180)/Math.max(1,teams.length-1))*(teams.length>1?1:0) + (i*34) + (Math.random()*18-9);
      if(teams.length===1) x= 220 + i*90 + t*300;
      if(terrainType==='two'){
        // distribute across two islands
        if(t%2===0) x= 120 + Math.random()*300 + i*32;
        else x= 760 + Math.random()*300 + i*32;
      }
      // find ground
      let y=H-140;
      for(let yy=0;yy<H;yy++){ if(isSolid(x,yy+8)){ y=yy-13; break; } }
      if(y>H-80) y=H-100;
      // nudge if inside solid
      let tries=0;
      while(isSolid(x,y+10) && tries<20){ y-=8; tries++; }
      worms.push(new Worm(t,i,x,y));
    }
  }
  activeTeam=0; activeWormIdx=0;
  gameState='playing';
  turnTime=30;
  startTurn();
  document.getElementById('leftPanel').classList.remove('open');
  updateWeaponBar(); updateWindUI();
  sfx('select');
}
function restartGame(){ generateTerrain(); startGame(); }
function toggleMenu(){ document.getElementById('leftPanel').classList.toggle('open'); }

function getActiveWorm(){
  let teamWorms=worms.filter(w=>w.teamIdx===activeTeam && w.alive);
  if(teamWorms.length===0) return null;
  return teamWorms[activeWormIdx % teamWorms.length];
}
function startTurn(){
  let aliveTeams=teams.map((_,i)=> worms.some(w=>w.teamIdx===i && w.alive));
  let start=activeTeam;
  let guard=0;
  while(!aliveTeams[activeTeam] && guard<8){
    activeTeam=(activeTeam+1)%teams.length;
    if(activeTeam===start) { gameState='over'; return; }
    guard++;
  }
  let aw=getActiveWorm();
  if(!aw){ activeTeam=(activeTeam+1)%teams.length; startTurn(); return; }
  turnTime=30;
  charge=0; charging=false;
  updateTurnBanner();
  updateHealthGrid();
  if(teams[activeTeam].cpu){
    setTimeout(cpuTurn, 850);
  }
  if(turnTimer) clearInterval(turnTimer);
  turnTimer=setInterval(()=>{
    turnTime-=0.1;
    if(turnTime<=0){ clearInterval(turnTimer); nextTurn(); }
    updateTurnBanner();
    if(turnTime<10 && Math.floor(turnTime*10)%7===0) sfx('tickFast');
    else if(Math.abs(turnTime%1)<0.11 && turnTime<30) sfx('tick');
  },100);
}
function nextTurn(){
  if(turnTimer) clearInterval(turnTimer);
  let aliveCounts=teams.map((_,i)=> worms.filter(w=>w.teamIdx===i && w.alive).length);
  let teamsAlive=aliveCounts.filter(c=>c>0).length;
  if(teamsAlive<=1){
    gameState='over';
    let winnerIdx=aliveCounts.findIndex(c=>c>0);
    let winner=winnerIdx>=0?teams[winnerIdx]:null;
    document.getElementById('turnText').textContent= winner? `${winner.name} WINS! 🎉` : 'DRAW!';
    document.getElementById('turnDot').style.background=winner?winner.color:'#fff';
    beep(600,0.35,'sine',0.28); setTimeout(()=>beep(800,0.5,'sine',0.28),180); setTimeout(()=>beep(1000,0.6,'sine',0.3),380);
    return;
  }
  activeTeam=(activeTeam+1)%teams.length;
  activeWormIdx= (activeWormIdx+1)%4;
  startTurn();
}
function updateTurnBanner(){
  const aw=getActiveWorm();
  const t=teams[activeTeam];
  if(!t) return;
  document.getElementById('turnDot').style.background=t.color;
  document.getElementById('turnText').textContent= gameState==='over'? document.getElementById('turnText').textContent : `${t.name} • WORM ${aw?aw.idx+1:''}`;
  const ring=document.getElementById('timerRing');
  ring.textContent=Math.ceil(Math.max(0,turnTime));
  ring.style.setProperty('--p', (Math.max(0,turnTime)/30*100)+'%');
  ring.classList.toggle('panic', turnTime<8);
  let scale= turnTime<10? 1+ (10-turnTime)*0.045 :1;
  ring.style.transform=`scale(${scale})`;
  document.getElementById('turnInfo').textContent=`${activeTeam+1}/${teams.length}`;
}
function updateWindUI(){
  document.getElementById('windVal').textContent=(wind>0?'+':'')+wind;
  const fill=document.getElementById('windFill');
  const arrow=document.getElementById('windArrow');
  let pct=Math.abs(wind)/8*52;
  fill.style.width=pct+'%';
  if(wind>=0){ fill.style.left='50%'; fill.style.right='auto'; arrow.textContent='▶'; arrow.style.left=`calc(50% + ${pct/2}% + 4px)`; }
  else { fill.style.left=(50-pct)+'%'; arrow.textContent='◀'; arrow.style.left=`calc(50% - ${pct/2}% - 4px)`; }
  fill.style.background= wind===0? '#334155' : (wind>0? 'linear-gradient(90deg,#facc15,#22d3ee)':'linear-gradient(90deg,#22d3ee,#facc15)');
}
function updateHealthGrid(){
  const el=document.getElementById('healthGrid'); el.innerHTML='';
  teams.forEach((t,ti)=>{
    const teamWorms=worms.filter(w=>w.teamIdx===ti);
    teamWorms.forEach(w=>{
      const chip=document.createElement('div'); chip.className='worm-chip'+ (ti===activeTeam && w===getActiveWorm()?' active':'');
      chip.style.color=t.color;
      let hpCol= w.hp>55?'#22c55e': w.hp>28?'#facc15':'#ef4444';
      chip.innerHTML=`<span style="width:8px;height:8px;border-radius:50%;background:${t.color};display:inline-block;box-shadow:0 0 6px ${t.color}"></span> ${t.name.slice(0,3)}${w.idx+1} <div class="hpbar"><i style="width:${w.hp}%;background:${hpCol}"></i></div> <span style="font-size:10px;min-width:18px;text-align:right">${w.hp}</span>`;
      if(!w.alive) chip.style.opacity='0.35';
      el.appendChild(chip);
    });
  });
}
function updateWeaponBar(){
  const bar=document.getElementById('weaponBar'); bar.innerHTML='';
  WEAPONS.forEach((w,i)=>{
    const aw=getActiveWorm();
    let ammo= aw? aw.weapons[w.id] : w.ammo;
    const btn=document.createElement('button'); btn.className='wbtn'+(i===selectedWeapon?' active':'');
    btn.innerHTML=`<span class="cat cat-${w.cat}">${w.cat}</span><span class="ico">${w.ico}</span><span class="lbl">${w.name}</span><span class="ammo">${ammo===99?'∞':ammo}</span>`;
    btn.onclick=()=>{selectedWeapon=i; updateWeaponBar(); sfx('select');};
    if(aw && ammo<=0 && w.ammo!==99) btn.style.opacity='0.45';
    bar.appendChild(btn);
  });
}

// Input
canvas.addEventListener('mousemove',e=>{
  const rect=canvas.getBoundingClientRect();
  const scaleX=W/rect.width, scaleY=H/rect.height;
  let mx=(e.clientX-rect.left)*scaleX;
  let my=(e.clientY-rect.top)*scaleY;
  // convert screen to world via camera
  let worldX= (mx - W/2)/camera.zoom + camera.x;
  let worldY= (my - H/2)/camera.zoom + camera.y;
  mouse.x=mx; mouse.y=my;
  const aw=getActiveWorm(); if(!aw) return;
  angle=Math.atan2(worldY - aw.y, worldX - aw.x);
  if(aw) aw.facing= Math.cos(angle)>=0?1:-1;
});
canvas.addEventListener('mousedown',e=>{
  if(gameState!=='playing') return;
  const aw=getActiveWorm(); if(!aw || teams[activeTeam].cpu) return;
  const w=WEAPONS[selectedWeapon];
  if(w.id==='teleport'){
    const rect=canvas.getBoundingClientRect();
    const scaleX=W/rect.width, scaleY=H/rect.height;
    let mx=(e.clientX-rect.left)*scaleX, my=(e.clientY-rect.top)*scaleY;
    let worldX= (mx - W/2)/camera.zoom + camera.x;
    let worldY= (my - H/2)/camera.zoom + camera.y;
    for(let y=worldY;y<H;y++){ if(isSolid(worldX,y)){ aw.x=worldX; aw.y=y-14; aw.vx=0; aw.vy=0; break; } if(y> WATER_Y) {aw.x=worldX; aw.y=WATER_Y-14; break;}}
    aw.weapons['teleport']--; updateWeaponBar(); sfx('select'); spawnParticles(aw.x,aw.y,14,'#a78bfa'); endTurnSoon(600); return;
  }
  if(w.cat==='util' && w.id==='jetpack'){ return;}
  charging=true; charge=0;
});
canvas.addEventListener('mouseup',e=>{
  if(!charging) return; charging=false;
  const aw=getActiveWorm(); if(!aw) return;
  const w=WEAPONS[selectedWeapon];
  if(w.cat==='util') return;
  let p= Math.min(100, charge*1.85);
  if(p<8) return;
  fireWeapon(aw,w.id,angle,p);
  document.getElementById('powerMeter').style.display='none';
});
window.addEventListener('keydown',e=>{
  const aw=getActiveWorm();
  if(e.code==='Space'){
    if(gameState==='playing' && aw && !teams[activeTeam].cpu){
      const w=WEAPONS[selectedWeapon];
      if(w.cat!=='util' && !charging){ charging=true; charge=0; e.preventDefault(); }
    }
  }
  if(e.key==='w' || e.key==='W' || e.key==='ArrowUp'){
    if(aw && aw.onGround){ aw.vy=-9.8; aw.onGround=false; sfx('jump'); spawnParticles(aw.x,aw.y+10,5,'#a3e635'); }
  }
  if(e.key>='1' && e.key<='8'){ selectedWeapon=parseInt(e.key)-1; if(selectedWeapon<WEAPONS.length) {updateWeaponBar(); sfx('select')} }
  if(e.key==='q' || e.key==='Q'){ activeWormIdx=(activeWormIdx+3)%4; updateHealthGrid(); sfx('select')}
  if(e.key==='e' || e.key==='E'){ activeWormIdx=(activeWormIdx+1)%4; updateHealthGrid(); sfx('select')}
});
window.addEventListener('keyup',e=>{
  if(e.code==='Space' && charging){
    charging=false;
    const aw=getActiveWorm(); if(!aw) return;
    const w=WEAPONS[selectedWeapon];
    let p=Math.min(100, charge*1.85);
    if(p<8){ document.getElementById('powerMeter').style.display='none'; return; }
    fireWeapon(aw,w.id,angle,p);
    document.getElementById('powerMeter').style.display='none';
  }
});
const keys={};
window.addEventListener('keydown',e=>keys[e.key.toLowerCase()]=true);
window.addEventListener('keyup',e=>keys[e.key.toLowerCase()]=false);

function cpuTurn(){
  const aw=getActiveWorm(); if(!aw || gameState!=='playing') return;
  let enemies=worms.filter(w=>w.teamIdx!==activeTeam && w.alive);
  if(enemies.length===0) return;
  let best=null, bestScore=-Infinity;
  enemies.forEach(en=>{
    let dist=Math.hypot(en.x-aw.x, en.y-aw.y);
    let score= 110 - en.hp*0.55 - dist*0.09;
    let nearWater= en.y > WATER_Y-28 ? -32 :0;
    let cliffRisk= !isSolid(en.x, en.y+18) ? -22 :0;
    let windRisk= Math.abs(wind)>4 && Math.sign(wind)===Math.sign(en.x-aw.x) ? -12 :0;
    score+= nearWater+cliffRisk+windRisk;
    if(dist<75) score-=24;
    if(en.hp<35) score+=18;
    if(score>bestScore){bestScore=score; best=en;}
  });
  let dist=Math.hypot(best.x-aw.x, best.y-aw.y);
  let chosen='bazooka';
  if(dist<130 && aw.weapons['shotgun']>0) chosen='shotgun';
  else if(dist>360 && aw.weapons['airstrike']>0) chosen='airstrike';
  else if(dist<210 && aw.weapons['grenade']>0) chosen='grenade';
  else if(dist>260 && aw.weapons['sniper']>0) chosen='sniper';
  else if(dist<110 && aw.weapons['dynamite']>0) chosen='dynamite';
  if(aw.weapons[chosen]<=0) chosen='bazooka';
  selectedWeapon=WEAPONS.findIndex(w=>w.id===chosen);
  updateWeaponBar();
  let dx=best.x-aw.x, dy=best.y-aw.y;
  let baseAngle=Math.atan2(dy,dx);
  baseAngle+= -wind*0.016 * (dist/300);
  baseAngle+= (Math.random()-0.5)*0.11;
  angle=baseAngle;
  let powerEst= Math.min(96, 36 + dist*0.175 + Math.abs(wind)*2.2);
  powerEst+= (Math.random()-0.5)*10;
  let steps=18, step=0;
  let aimInt=setInterval(()=>{
    step++; angle= baseAngle + Math.sin(step*0.35)*0.018;
    if(step>=steps){
      clearInterval(aimInt);
      fireWeapon(aw,chosen,baseAngle, powerEst);
    }
  },42);
}

function loop(){
  requestAnimationFrame(loop);
  if(freezeFrames>0){ freezeFrames--; return; }
  if(charging){
    charge+=1.7; if(charge>55) charge=55;
    let p=Math.min(100, charge*1.85);
    document.getElementById('powerMeter').style.display='block';
    document.getElementById('powerFill').style.width=p+'%';
    if(Math.floor(charge)%10===0) sfx('charge');
  }
  worms.forEach(w=>w.update());
  const aw=getActiveWorm();
  if(aw && aw.hasJetpack && aw.jetFuel>0){
    if(keys['w']||keys[' ']||keys['arrowup']) {aw.vy-=0.58; spawnParticles(aw.x,aw.y+11,2,'#f97316'); if(Math.random()<0.3) spawnParticles(aw.x,aw.y+11,1,'#facc15');}
    if(keys['a']||keys['arrowleft']) aw.vx-=0.38;
    if(keys['d']||keys['arrowright']) aw.vx+=0.38;
    aw.jetFuel--;
    if(aw.jetFuel<=0) aw.hasJetpack=false;
  }
  if(aw && !teams[activeTeam].cpu && gameState==='playing' && !aw.hasJetpack){
    if(keys['a']||keys['arrowleft']){ aw.vx-=0.48; aw.facing=-1; }
    if(keys['d']||keys['arrowright']){ aw.vx+=0.48; aw.facing=1; }
  }
  for(let i=projectiles.length-1;i>=0;i--){
    let p=projectiles[i];
    p.trail.push({x:p.x,y:p.y}); if(p.trail.length>13) p.trail.shift();
    p.vx+=wind*0.022;
    p.vy+=GRAVITY*0.62;
    p.x+=p.vx; p.y+=p.vy;
    if(p.type==='grenade' || p.type==='dynamite'){
      p.fuse--;
      if(p.fuse<=0){ explode(p.x,p.y,p.rad,p.dmg); projectiles.splice(i,1); continue; }
      if(isSolid(p.x,p.y+5)){
        p.vy*=-0.46; p.vx*=0.72; p.y-=5;
        spawnParticles(p.x,p.y,3,'#94a3b8');
        if(Math.abs(p.vy)<1.2) p.vy=0;
      }
      if(isSolid(p.x+Math.sign(p.vx)*6,p.y)){ p.vx*=-0.62; }
      if(p.y>WATER_Y){ spawnParticles(p.x,WATER_Y,10,'#22d3ee'); explode(p.x,WATER_Y-6,p.rad*0.7,p.dmg*0.6); projectiles.splice(i,1); continue; }
    } else {
      if(isSolid(p.x,p.y) || p.y>WATER_Y){
        if(p.y>WATER_Y) { spawnParticles(p.x,WATER_Y,13,'#22d3ee'); projectiles.splice(i,1); continue; }
        explode(p.x,p.y,p.rad,p.dmg); projectiles.splice(i,1); continue;
      }
      for(let w of worms){ if(!w.alive) continue; if(w===aw && p.trail.length<5) continue; if(Math.hypot(w.x-p.x,w.y-p.y)<18){ explode(p.x,p.y,p.rad,p.dmg); projectiles.splice(i,1); break; } }
    }
    if(p.y>H+110) projectiles.splice(i,1);
  }
  for(let i=particles.length-1;i>=0;i--){
    let pt=particles[i];
    if(pt.laser){ pt.life-=pt.decay; if(pt.life<=0) particles.splice(i,1); continue; }
    if(pt.tracer){ pt.life-=pt.decay; if(pt.life<=0) particles.splice(i,1); continue; }
    pt.x+=pt.vx; pt.y+=pt.vy; pt.vy+=0.22; pt.life-=pt.decay; pt.vx*=0.985;
    if(pt.life<=0) particles.splice(i,1);
  }
  for(let i=debris.length-1;i>=0;i--){
    let d=debris[i]; d.vy+=GRAVITY*0.52; d.x+=d.vx; d.y+=d.vy; d.vx*=0.985; d.rot+=d.vr;
    if(isSolid(d.x,d.y+ d.h/2)){ d.vy*=-0.32; d.vx*=0.68; d.y-=2.5; if(Math.abs(d.vy)<0.9) d.vy=0; }
    if(d.y>H+60) debris.splice(i,1);
  }
  for(let i=floatTexts.length-1;i>=0;i--){ let f=floatTexts[i]; f.y+=f.vy; f.vy+=0.06; f.life-=0.018; if(f.life<=0) floatTexts.splice(i,1); }

  if(aw){
    camera.targetX=aw.x; camera.targetY=aw.y-70;
    // if projectile exists, lerp towards it
    if(projectiles.length>0){ let p=projectiles[0]; camera.targetX= (aw.x*0.35 + p.x*0.65); camera.targetY= (aw.y*0.35 + p.y*0.65); }
  } else {
    camera.targetX=W/2; camera.targetY=H/2;
  }
  camera.x+= (camera.targetX - camera.x)*0.075;
  camera.y+= (camera.targetY - camera.y)*0.075;
  let targetZoom= projectiles.length? 0.92 : 1;
  camera.zoom+= (targetZoom - camera.zoom)*0.045;
  if(shakeTime>0) shakeTime--;
  draw();
  updateHealthGrid();
}
function explode(x,y,rad,dmg){
  destroyTerrain(x,y,rad);
  spawnParticles(x,y,30,'#f97316'); spawnParticles(x,y,20,'#facc15'); spawnParticles(x,y,16,'#fef08a'); spawnParticles(x,y,12,'#1e293b');
  // ring
  particles.push({x,y, vx:0,vy:0, life:1, decay:0.08, r:8, col:'rgba(250,204,21,0.9)', ring:true, rad:rad});
  shakeTime= rad>80? 20:12;
  freezeFrames= rad>80? 9:4;
  sfx('explosion');
  worms.forEach(w=>{
    if(!w.alive) return;
    let d=Math.hypot(w.x-x,w.y-y);
    if(d<rad+20){
      let falloff=1 - d/(rad+24);
      let dam=Math.floor(dmg*falloff);
      if(dam>0) w.damage(dam);
      let ang=Math.atan2(w.y-y,w.x-x);
      w.vx+=Math.cos(ang)*falloff*11;
      w.vy+=Math.sin(ang)*falloff*9 -2.5;
    }
  });
  if(y> WATER_Y- rad) spawnParticles(x,WATER_Y,16,'#22d3ee');
  setTimeout(()=>{ if(projectiles.length===0) endTurnSoon(850); }, 650);
}

function draw(){
  ctx.save();
  if(shakeTime>0){
    ctx.translate((Math.random()-0.5)*shakeTime*1.3, (Math.random()-0.5)*shakeTime*1.3);
  }
  // sky gradient with dusk
  let grad=ctx.createLinearGradient(0,0,0,H);
  grad.addColorStop(0,'#020617');
  grad.addColorStop(0.35,'#0f2440');
  grad.addColorStop(0.65,'#1e3a5f');
  grad.addColorStop(1,'#38bdf8');
  ctx.fillStyle=grad; ctx.fillRect(0,0,W,H);
  // stars
  ctx.fillStyle='rgba(255,255,255,0.9)';
  for(let i=0;i<60;i++){ let sx=(i*211)%W, sy=(i*137)%220; let tw=Math.sin(Date.now()*0.003+i)*0.5+0.5; ctx.globalAlpha=0.4+tw*0.6; ctx.fillRect(sx,sy,1.5,1.5); }
  ctx.globalAlpha=1;
  // moon
  ctx.fillStyle='#fef08a'; ctx.beginPath(); ctx.arc(1020,110,34,0,Math.PI*2); ctx.fill();
  ctx.fillStyle='#fde047'; ctx.beginPath(); ctx.arc(1028,108,8,0,Math.PI*2); ctx.fill(); ctx.beginPath(); ctx.arc(1012,118,5,0,Math.PI*2); ctx.fill();
  ctx.fillStyle='rgba(254,240,138,0.18)'; ctx.beginPath(); ctx.arc(1020,110,52,0,Math.PI*2); ctx.fill();
  // clouds parallax
  ctx.fillStyle='rgba(255,255,255,0.07)';
  for(let i=0;i<5;i++){ let cx=((i*320 + windPhase*0.6)% (W+160))-80, cy=58+i*26 + Math.sin(i)*10; ctx.beginPath(); ctx.ellipse(cx,cy,86,20,0,0,Math.PI*2); ctx.fill(); ctx.beginPath(); ctx.ellipse(cx+28,cy-8,42,14,0,0,Math.PI*2); ctx.fill(); }
  windPhase+= wind*0.16;
  // camera
  ctx.translate(W/2, H/2);
  ctx.scale(camera.zoom,camera.zoom);
  ctx.translate(-camera.x, -camera.y);
  // terrain
  ctx.drawImage(tCanvas,0,0);
  // water with waves
  ctx.fillStyle='#0284c7';
  ctx.fillRect(0,WATER_Y,W, H-WATER_Y);
  ctx.fillStyle='#0ea5e9';
  ctx.beginPath();
  for(let x=0;x<=W;x+=6){ let wy=WATER_Y+ Math.sin(x*0.04 + Date.now()*0.004)*3 + Math.cos(x*0.08 - Date.now()*0.002)*2; if(x===0) ctx.moveTo(x,wy); else ctx.lineTo(x,wy); }
  ctx.lineTo(W,WATER_Y+8); ctx.lineTo(0,WATER_Y+8); ctx.closePath(); ctx.fill();
  ctx.fillStyle='rgba(255,255,255,0.22)';
  for(let x=0;x<W;x+=22){ let wy=WATER_Y+ Math.sin(x*0.05 + Date.now()*0.003)*3.5; ctx.fillRect(x,wy,16,2); }
  ctx.fillStyle='rgba(14,165,233,0.35)'; ctx.fillRect(0,WATER_Y-7,W,7);
  // debris
  debris.forEach(d=>{
    ctx.save(); ctx.translate(d.x,d.y); ctx.rotate(d.rot); ctx.fillStyle=d.col; ctx.strokeStyle='#000'; ctx.lineWidth=1.5; ctx.fillRect(-d.w/2,-d.h/2,d.w,d.h); ctx.strokeRect(-d.w/2,-d.h/2,d.w,d.h); ctx.restore();
  });
  // worms
  worms.forEach(w=>{
    if(!w.alive) return;
    ctx.save(); ctx.translate(w.x,w.y);
    // shadow
    ctx.fillStyle='rgba(0,0,0,0.28)'; ctx.beginPath(); ctx.ellipse(0,13,15,5.5,0,0,Math.PI*2); ctx.fill();
    let teamCol=teams[w.teamIdx].color;
    // feet
    ctx.fillStyle='#0f172a'; ctx.strokeStyle='#000'; ctx.lineWidth=1.2;
    ctx.beginPath(); ctx.roundRect(-8,8,6,6,2); ctx.fill(); ctx.stroke();
    ctx.beginPath(); ctx.roundRect(2,8,6,6,2); ctx.fill(); ctx.stroke();
    // body
    ctx.fillStyle=teamCol; ctx.beginPath(); ctx.roundRect(-12,-11,24,22,9); ctx.fill();
    ctx.strokeStyle='#000'; ctx.lineWidth=2.2; ctx.stroke();
    // highlight
    ctx.fillStyle='rgba(255,255,255,0.28)'; ctx.beginPath(); ctx.ellipse(-4,-6,8,5,0,0,Math.PI*2); ctx.fill();
    // belly
    ctx.fillStyle='rgba(255,255,255,0.92)'; ctx.beginPath(); ctx.ellipse(0,5,8,6.5,0,0,Math.PI*2); ctx.fill();
    // blush
    if(w.hp<45){ ctx.fillStyle='rgba(239,68,68,0.55)'; ctx.beginPath(); ctx.arc(-8,1,2.2,0,Math.PI*2); ctx.arc(8,1,2.2,0,Math.PI*2); ctx.fill(); }
    // eyes
    ctx.fillStyle='#fff'; ctx.beginPath(); ctx.arc(-5.5,-6,5.2,0,Math.PI*2); ctx.arc(5.5,-6,5.2,0,Math.PI*2); ctx.fill();
    ctx.strokeStyle='#000'; ctx.lineWidth=1; ctx.stroke();
    ctx.fillStyle='#000'; ctx.beginPath(); ctx.arc(-5.5 + w.facing*1.6,-6,2.4,0,Math.PI*2); ctx.arc(5.5 + w.facing*1.6,-6,2.4,0,Math.PI*2); ctx.fill();
    ctx.fillStyle='#fff'; ctx.beginPath(); ctx.arc(-4.5 + w.facing*1.6,-7,0.9,0,Math.PI*2); ctx.arc(6.5 + w.facing*1.6,-7,0.9,0,Math.PI*2); ctx.fill();
    // mouth
    if(w.hp<30){ ctx.strokeStyle='#000'; ctx.lineWidth=1.4; ctx.beginPath(); ctx.arc(0,2,3,0.15*Math.PI,0.85*Math.PI); ctx.stroke(); }
    else { ctx.fillStyle='#000'; ctx.beginPath(); ctx.ellipse(0,2,2.2,1.2,0,0,Math.PI*2); ctx.fill(); }
    // health bar
    ctx.fillStyle='#000'; ctx.fillRect(-15,-19,30,6);
    ctx.fillStyle='#0f172a'; ctx.fillRect(-14,-18,28,4);
    let hpPct=w.hp/100;
    let hpCol= w.hp>55?'#22c55e': w.hp>28?'#facc15':'#ef4444';
    ctx.fillStyle=hpCol; ctx.fillRect(-14,-18,28*hpPct,4);
    if(w.hp<100){ ctx.fillStyle='rgba(255,255,255,0.5)'; ctx.fillRect(-14,-18,28*hpPct,1.5); }
    // active arrow
    if(w===getActiveWorm()){
      ctx.fillStyle=teamCol; ctx.strokeStyle='#000'; ctx.lineWidth=1.5;
      ctx.beginPath(); ctx.moveTo(0,-26); ctx.lineTo(-7,-35); ctx.lineTo(7,-35); ctx.closePath(); ctx.fill(); ctx.stroke();
      // glow
      ctx.fillStyle=teamCol; ctx.globalAlpha=0.18; ctx.beginPath(); ctx.arc(0,0,22,0,Math.PI*2); ctx.fill(); ctx.globalAlpha=1;
    }
    // jetpack
    if(w.hasJetpack){
      ctx.fillStyle='#334155'; ctx.strokeStyle='#000'; ctx.lineWidth=1.5; ctx.beginPath(); ctx.roundRect(-10, -2, 5, 12,2); ctx.fill(); ctx.stroke();
      ctx.beginPath(); ctx.roundRect(5, -2, 5, 12,2); ctx.fill(); ctx.stroke();
      if(w.jetFuel>0 && (keys['w']||keys[' ']||keys['arrowup'])){
        ctx.fillStyle='#f97316'; ctx.beginPath(); ctx.moveTo(-7.5,10); ctx.lineTo(-5,18+Math.random()*7); ctx.lineTo(-2.5,10); ctx.closePath(); ctx.fill();
        ctx.beginPath(); ctx.moveTo(2.5,10); ctx.lineTo(5,18+Math.random()*7); ctx.lineTo(7.5,10); ctx.closePath(); ctx.fill();
        ctx.fillStyle='#facc15'; ctx.beginPath(); ctx.moveTo(-6,10); ctx.lineTo(-5,15); ctx.lineTo(-4,10); ctx.closePath(); ctx.fill();
        ctx.beginPath(); ctx.moveTo(4,10); ctx.lineTo(5,15); ctx.lineTo(6,10); ctx.closePath(); ctx.fill();
      }
    }
    // drown bubbles
    if(w.y+w.h/2 > WATER_Y){
      ctx.fillStyle='rgba(255,255,255,0.7)'; for(let i=0;i<3;i++){ let bx=(Math.random()-0.5)*10, by=-14-Math.random()*10; ctx.beginPath(); ctx.arc(bx,by,1.5,0,Math.PI*2); ctx.fill(); }
    }
    ctx.restore();
  });
  // projectiles
  projectiles.forEach(p=>{
    if(p.trail.length>1){
      ctx.strokeStyle= p.type==='grenade'?'rgba(148,163,184,0.9)':'rgba(249,115,22,0.85)';
      ctx.lineWidth=2.5; ctx.lineCap='round'; ctx.beginPath();
      p.trail.forEach((pt,i)=>{ if(i===0) ctx.moveTo(pt.x,pt.y); else ctx.lineTo(pt.x,pt.y); }); ctx.stroke();
    }
    ctx.fillStyle= p.type==='grenade'?'#1e293b':'#0f172a';
    ctx.beginPath(); ctx.arc(p.x,p.y,7,0,Math.PI*2); ctx.fill();
    ctx.strokeStyle= p.type==='grenade'?'#94a3b8':'#facc15'; ctx.lineWidth=2; ctx.stroke();
    ctx.fillStyle= p.type==='grenade'?'#e2e8f0':'#f97316'; ctx.beginPath(); ctx.arc(p.x-1.5,p.y-1.5,2,0,Math.PI*2); ctx.fill();
    if(p.fuse!==undefined){
      ctx.fillStyle='#fff'; ctx.font='700 9px Space Mono'; ctx.textAlign='center'; ctx.strokeStyle='#000'; ctx.lineWidth=3; ctx.strokeText(Math.ceil(p.fuse/30),p.x,p.y-12); ctx.fillText(Math.ceil(p.fuse/30),p.x,p.y-12);
    }
  });
  // particles
  particles.forEach(pt=>{
    if(pt.ring){
      ctx.strokeStyle=`rgba(250,204,21,${pt.life*0.6})`; ctx.lineWidth=3; ctx.beginPath(); ctx.arc(pt.x,pt.y, pt.rad*(1.6-pt.life*0.6),0,Math.PI*2); ctx.stroke(); return;
    }
    if(pt.laser){
      ctx.strokeStyle=`rgba(34,211,238,${pt.life})`; ctx.lineWidth=4; ctx.shadowColor='#22d3ee'; ctx.shadowBlur=8; ctx.beginPath(); ctx.moveTo(pt.laser.x1,pt.laser.y1); ctx.lineTo(pt.laser.x2,pt.laser.y2); ctx.stroke(); ctx.shadowBlur=0;
      ctx.fillStyle=`rgba(255,255,255,${pt.life})`; ctx.beginPath(); ctx.arc(pt.laser.x2,pt.laser.y2,7,0,Math.PI*2); ctx.fill();
      ctx.fillStyle=`rgba(34,211,238,${pt.life})`; ctx.beginPath(); ctx.arc(pt.laser.x2,pt.laser.y2,3,0,Math.PI*2); ctx.fill();
      return;
    }
    if(pt.tracer){
      ctx.strokeStyle=`rgba(255,255,255,${pt.life})`; ctx.lineWidth=2.5; ctx.beginPath(); ctx.moveTo(pt.x,pt.y); ctx.lineTo(pt.tx,pt.ty); ctx.stroke();
      ctx.fillStyle=`rgba(250,204,21,${pt.life})`; ctx.beginPath(); ctx.arc(pt.tx,pt.ty,4,0,Math.PI*2); ctx.fill(); return;
    }
    ctx.globalAlpha=pt.life; ctx.fillStyle=pt.col; ctx.beginPath(); ctx.arc(pt.x,pt.y,pt.r,0,Math.PI*2); ctx.fill(); ctx.globalAlpha=1;
  });
  // predictive trajectory
  if(charging && getActiveWorm() && !teams[activeTeam].cpu){
    const aw=getActiveWorm();
    let simX=aw.x+Math.cos(angle)*23, simY=aw.y+Math.sin(angle)*10-5;
    let simVX=Math.cos(angle)* (Math.min(100,charge*1.85)*0.56) + wind*0.16;
    let simVY=Math.sin(angle)* (Math.min(100,charge*1.85)*0.56);
    ctx.strokeStyle='rgba(250,204,21,0.95)'; ctx.setLineDash([7,7]); ctx.lineWidth=2.2; ctx.lineCap='round'; ctx.beginPath(); ctx.moveTo(simX,simY);
    let hitX=simX, hitY=simY;
    for(let i=0;i<44;i++){
      simVX+=wind*0.022; simVY+=GRAVITY*0.62; simX+=simVX*0.58; simY+=simVY*0.58;
      ctx.lineTo(simX,simY);
      hitX=simX; hitY=simY;
      if(isSolid(simX,simY) || simY>WATER_Y) break;
    }
    ctx.stroke(); ctx.setLineDash([]);
    // impact marker pulsing
    let pulse= 1+ Math.sin(Date.now()*0.012)*0.15;
    ctx.fillStyle='#facc15'; ctx.strokeStyle='#000'; ctx.lineWidth=2;
    ctx.beginPath(); ctx.arc(hitX,hitY,6*pulse,0,Math.PI*2); ctx.fill(); ctx.stroke();
    ctx.fillStyle='#fff'; ctx.beginPath(); ctx.arc(hitX,hitY,2,0,Math.PI*2); ctx.fill();
    // dotted aim line from worm
    ctx.strokeStyle='rgba(255,255,255,0.5)'; ctx.setLineDash([3,6]); ctx.lineWidth=1.2; ctx.beginPath(); ctx.moveTo(aw.x,aw.y-4); ctx.lineTo(aw.x+Math.cos(angle)*28, aw.y+Math.sin(angle)*10-5); ctx.stroke(); ctx.setLineDash([]);
  }
  // floating texts
  floatTexts.forEach(f=>{
    ctx.globalAlpha=f.life; ctx.fillStyle=f.col; ctx.font='800 20px Luckiest Guy'; ctx.textAlign='center'; ctx.strokeStyle='#000'; ctx.lineWidth=4; ctx.strokeText(f.txt,f.x,f.y); ctx.fillText(f.txt,f.x,f.y); ctx.globalAlpha=1;
  });
  ctx.restore();
}

initTeams();
generateTerrain();
updateWeaponBar();
updateWindUI();
updateHealthGrid();
canvas.width=W; canvas.height=H; tCanvas.width=W; tCanvas.height=H;
generateTerrain();
requestAnimationFrame(loop);
</script>
</body>
</html>
<!-- agent-meta {"model":"meta/muse-spark-1.2-contributor","provider":"openrouter","persona":"full","sessionId":"042736c6-08ee-4f32-883b-56ca28ec3598","tokensIn":1170700,"tokensOut":39909,"tokensTotal":1210609,"cost":0.042146936,"turns":23,"toolCalls":22,"failedToolCalls":1,"timestamp":"2026-08-23T02:30:54.390Z"} -->