← Worms results

Worms

muse spark 1.2 · typescript

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 91,500 bytes · SHA-256 7bbcf8728557
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>POCKET WORMS — Tactical Artillery</title>
<style>
/* ── css: base ── */
@import url('https://fonts.googleapis.com/css2?family=Bangers&family=Nunito:wght@600;800;900&display=swap');
*{box-sizing:border-box;margin:0;padding:0}
:root{--bg:#0f172a;--panel:#1e293bcc;--accent:#facc15;--red:#ef4444;--blue:#38bdf8;--green:#4ade80;--yellow:#facc15;--purple:#a78bfa}
html,body{height:100%;background:#0b1020;overflow:hidden}
body{font-family:'Nunito',system-ui,sans-serif;color:#f1f5f9}
#app{position:relative;width:100vw;height:100vh;overflow:hidden;background:radial-gradient(1200px 600px at 50% 0%, #1e3a5f 0%, #0b1020 70%)}
#c{position:absolute;inset:0;width:100%;height:100%;display:block;cursor:crosshair}
.overlay{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background:rgba(5,10,24,0.78);backdrop-filter:blur(10px);z-index:20;transition:opacity .4s}
.overlay.hidden{opacity:0;pointer-events:none}
.setup-card{width:min(860px,96vw);background:linear-gradient(180deg,#1e293b 0%,#0f172a 100%);border:1px solid rgba(255,255,255,.12);border-radius:20px;padding:28px 28px 18px;box-shadow:0 20px 60px rgba(0,0,0,.6), inset 0 1px 0 rgba(255,255,255,.08)}
.setup-card.narrow{width:420px;text-align:center}
.logo{text-align:center;margin-bottom:18px}
.logo-icon{font-size:42px;filter:drop-shadow(0 4px 12px rgba(250,204,21,.5))}
.logo h1{font-family:'Bangers',cursive;font-size:52px;letter-spacing:3px;color:#facc15;text-shadow:0 3px 0 #a16207,0 6px 16px rgba(0,0,0,.4);line-height:1}
.tagline{font-size:11px;letter-spacing:3px;color:#94a3b8;font-weight:900;margin-top:4px}
.setup-grid{display:grid;grid-template-columns:1fr 1fr;gap:18px;margin:16px 0}
@media(max-width:720px){.setup-grid{grid-template-columns:1fr}}
.setup-section h3{font-size:11px;letter-spacing:2px;color:#94a3b8;margin-bottom:8px}
.pill-row{display:flex;gap:8px;flex-wrap:wrap}
.pill{padding:8px 12px;border-radius:999px;border:1px solid rgba(255,255,255,.12);background:rgba(255,255,255,.06);color:#cbd5e1;font-weight:800;font-size:11px;letter-spacing:.6px;cursor:pointer;transition:.2s}
.pill:hover{background:rgba(255,255,255,.12);transform:translateY(-1px)}
.pill.active{background:#facc15;color:#422006;border-color:#facc15;box-shadow:0 4px 14px rgba(250,204,21,.4)}
.team-setup{display:flex;flex-direction:column;gap:8px}
.team-row{display:flex;align-items:center;gap:8px;background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.08);border-radius:12px;padding:8px 10px}
.team-color{width:18px;height:18px;border-radius:50%;border:2px solid rgba(255,255,255,.3)}
.team-row select,.team-row input{flex:1;background:#0f172a;border:1px solid rgba(255,255,255,.12);color:#f1f5f9;border-radius:8px;padding:6px 8px;font-weight:800;font-size:12px}
.team-row .remove{width:28px;height:28px;border-radius:8px;border:0;background:rgba(239,68,68,.15);color:#f87171;cursor:pointer}
.mini-btn{margin-top:8px;padding:8px 12px;border-radius:999px;border:1px dashed rgba(255,255,255,.18);background:transparent;color:#94a3b8;font-weight:800;cursor:pointer}
.launch-btn{width:100%;margin-top:18px;padding:14px;border-radius:14px;border:0;background:linear-gradient(180deg,#facc15,#eab308);color:#422006;font-family:'Bangers',cursive;font-size:22px;letter-spacing:2px;cursor:pointer;box-shadow:0 8px 20px rgba(250,204,21,.35), inset 0 1px 0 rgba(255,255,255,.6);transition:transform .1s}
.launch-btn:hover{transform:translateY(-2px) scale(1.01)}
.launch-btn:active{transform:translateY(0)}
.hint{text-align:center;font-size:11px;color:#64748b;margin-top:10px}

/* HUD */
.hud{position:absolute;inset:0;pointer-events:none;z-index:5}
.hud.hidden{display:none}
.top-bar{position:absolute;top:14px;left:50%;transform:translateX(-50%);display:flex;align-items:center;gap:14px;pointer-events:auto}
.wind-widget{display:flex;align-items:center;gap:8px;background:rgba(15,23,42,.88);border:1px solid rgba(255,255,255,.12);border-radius:999px;padding:6px 12px;backdrop-filter:blur(8px)}
.wind-label{font-size:9px;letter-spacing:1.5px;color:#94a3b8;font-weight:900}
.wind-arrow-wrap{width:22px;height:22px;border-radius:50%;background:rgba(255,255,255,.08);display:grid;place-items:center}
#windArrow{display:inline-block;transition:transform .5s;color:#facc15}
.wind-val{font-weight:900;font-size:13px;min-width:28px;text-align:center}
.wind-bar{width:60px;height:6px;background:rgba(255,255,255,.12);border-radius:999px;overflow:hidden}
#windFill{height:100%;width:50%;background:linear-gradient(90deg,#38bdf8,#facc15,#ef4444);transition:width .4s}
.turn-banner{background:rgba(15,23,42,.88);border:1px solid rgba(255,255,255,.12);border-radius:999px;padding:8px 16px;display:flex;gap:6px;align-items:center;backdrop-filter:blur(8px);box-shadow:0 8px 20px rgba(0,0,0,.3)}
#turnTeamName{font-family:'Bangers',cursive;letter-spacing:1px;font-size:18px}
#turnWormName{font-size:12px;color:#cbd5e1}
.timer-widget{width:58px;height:58px;background:rgba(15,23,42,.9);border:2px solid rgba(255,255,255,.14);border-radius:50%;display:grid;place-items:center;position:relative;backdrop-filter:blur(8px);transition:transform .2s}
.timer-widget.panic{animation:panic 0.5s infinite alternate; border-color:#ef4444}
@keyframes panic{to{transform:scale(1.12); box-shadow:0 0 20px rgba(239,68,68,.6)}}
#timerVal{font-family:'Bangers',cursive;font-size:22px;line-height:1}
.timer-label{position:absolute;bottom:-8px;left:50%;transform:translateX(-50%);background:#ef4444;color:#fff;font-size:7px;letter-spacing:1px;padding:2px 6px;border-radius:999px;font-weight:900}
.left-panel{position:absolute;left:14px;top:80px;display:flex;flex-direction:column;gap:10px;pointer-events:auto;width:190px}
#teamRoster{display:flex;flex-direction:column;gap:8px}
.team-card{background:rgba(15,23,42,.86);border:1px solid rgba(255,255,255,.1);border-radius:14px;padding:10px;backdrop-filter:blur(8px)}
.team-card.active{border-color:rgba(250,204,21,.6);box-shadow:0 0 0 2px rgba(250,204,21,.2)}
.team-head{display:flex;align-items:center;gap:8px;font-weight:900;font-size:12px;letter-spacing:.6px}
.team-head i{width:10px;height:10px;border-radius:50%;display:block}
.worm-list{display:flex;gap:4px;margin-top:6px;flex-wrap:wrap}
.worm-chip{flex:1;min-width:42px;background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.08);border-radius:8px;padding:4px 6px;text-align:center;position:relative;overflow:hidden}
.worm-chip.active{outline:2px solid #facc15}
.worm-chip.dead{opacity:.35}
.worm-chip .hp{height:4px;background:rgba(255,255,255,.15);border-radius:999px;overflow:hidden;margin-top:4px}
.worm-chip .hp i{display:block;height:100%;background:linear-gradient(90deg,#4ade80,#22c55e);transition:width .3s}
.utility-row{display:flex;gap:6px}
.util-btn{flex:1;display:flex;flex-direction:column;align-items:center;gap:2px;padding:8px 4px;border-radius:12px;border:1px solid rgba(255,255,255,.12);background:rgba(15,23,42,.9);color:#e2e8f0;font-weight:900;font-size:10px;cursor:pointer;transition:.15s}
.util-btn:hover{transform:translateY(-1px);background:rgba(255,255,255,.1)}
.util-btn.active{background:#facc15;color:#422006;border-color:#facc15}
.util-btn:disabled{opacity:.35;pointer-events:none}
.weapon-dock{position:absolute;left:50%;bottom:18px;transform:translateX(-50%);display:flex;gap:8px;pointer-events:auto;align-items:end}
.wep{width:86px;background:rgba(15,23,42,.92);border:1px solid rgba(255,255,255,.12);border-radius:14px;padding:8px 6px 6px;text-align:center;cursor:pointer;transition:.15s;position:relative;backdrop-filter:blur(8px)}
.wep:hover{transform:translateY(-4px);border-color:rgba(255,255,255,.22)}
.wep.selected{border-color:#facc15;box-shadow:0 8px 20px rgba(250,204,21,.3), inset 0 0 0 1px rgba(250,204,21,.5);transform:translateY(-6px)}
.wep .cat{font-size:7px;letter-spacing:1px;font-weight:900;color:#94a3b8}
.wep .icon{font-size:22px;margin:2px 0}
.wep .name{font-size:10px;font-weight:900;line-height:1.1}
.wep .ammo{font-size:9px;color:#94a3b8}
.wep .sel-dot{position:absolute;top:-6px;right:-6px;width:18px;height:18px;border-radius:50%;background:#facc15;color:#422006;display:grid;place-items:center;font-size:10px;opacity:0;transform:scale(.6);transition:.2s}
.wep.selected .sel-dot{opacity:1;transform:scale(1)}
.bottom-help{position:absolute;bottom:8px;left:50%;transform:translateX(-50%);background:rgba(15,23,42,.7);border:1px solid rgba(255,255,255,.08);padding:6px 12px;border-radius:999px;font-size:10px;color:#94a3b8;letter-spacing:.4px;pointer-events:none}
.power-bar{position:absolute;left:50%;bottom:108px;transform:translateX(-50%);width:320px;height:18px;background:rgba(15,23,42,.9);border:1px solid rgba(255,255,255,.12);border-radius:999px;overflow:hidden;display:flex;align-items:center;pointer-events:none}
.power-bar.hidden{opacity:0}
#powerFill{height:100%;width:0%;background:linear-gradient(90deg,#22c55e,#facc15,#ef4444);transition:width .05s}
#powerLabel{position:absolute;inset:0;display:grid;place-items:center;font-weight:900;font-size:11px;letter-spacing:1px;text-shadow:0 1px 2px rgba(0,0,0,.6)}
.turn-overlay{position:absolute;inset:0;display:grid;place-items:center;background:rgba(250,204,21,.12);backdrop-filter:blur(1px);z-index:4;pointer-events:none}
.turn-overlay.hidden{display:none}
.turn-overlay span{font-family:'Bangers',cursive;font-size:56px;letter-spacing:4px;color:#fff;text-shadow:0 4px 0 rgba(0,0,0,.4),0 0 30px rgba(250,204,21,.8);animation:pop .6s cubic-bezier(.175,.885,.32,1.275)}
@keyframes pop{0%{transform:scale(.6) rotate(-3deg);opacity:0}100%{transform:scale(1) rotate(0);opacity:1}}
.damage-layer{position:absolute;inset:0;pointer-events:none}
.fct{position:absolute;font-family:'Bangers',cursive;font-size:22px;color:#fff;text-shadow:0 2px 0 rgba(0,0,0,.5);animation:fct 0.9s forwards}
@keyframes fct{0%{transform:translateY(0) scale(1);opacity:1}100%{transform:translateY(-42px) scale(1.2);opacity:0}}
</style>
</head>
<body>
<div id="app">
  <canvas id="c"></canvas>
  <div id="setup" class="overlay">
    <div class="setup-card">
      <div class="logo">
        <div class="logo-icon">🪱</div>
        <h1>POCKET WORMS</h1>
        <p class="tagline">TACTICAL ARTILLERY • HOT-SEAT MAYHEM</p>
      </div>
      <div class="setup-grid">
        <div class="setup-section">
          <h3>TERRAIN</h3>
          <div class="pill-row" id="terrainPills">
            <button class="pill active" data-v="island">SINGLE ISLAND</button>
            <button class="pill" data-v="twin">TWIN ISLES</button>
            <button class="pill" data-v="cave">CAVERN</button>
          </div>
          <h3 style="margin-top:14px">WIND</h3>
          <div class="pill-row" id="windPills">
            <button class="pill" data-v="calm">CALM</button>
            <button class="pill active" data-v="breeze">BREEZE</button>
            <button class="pill" data-v="storm">STORM</button>
          </div>
        </div>
        <div class="setup-section">
          <h3>TEAMS (2-4)</h3>
          <div id="teamSetup" class="team-setup"></div>
          <button id="addTeamBtn" class="mini-btn">+ ADD TEAM</button>
        </div>
      </div>
      <button id="launchBtn" class="launch-btn">⚡ LAUNCH BATTLE ⚡</button>
      <p class="hint">Hot-seat multiplayer • Mouse to aim • SPACE to charge &amp; fire</p>
    </div>
  </div>
  <div id="hud" class="hud hidden">
    <div class="top-bar">
      <div class="wind-widget">
        <span class="wind-label">WIND</span>
        <div class="wind-arrow-wrap"><span id="windArrow">➤</span></div>
        <span id="windVal" class="wind-val">0.0</span>
        <div class="wind-bar"><div id="windFill"></div></div>
      </div>
      <div class="turn-banner" id="turnBanner">
        <span id="turnTeamName">TEAM</span> <em id="turnWormName">'s worm</em>
      </div>
      <div class="timer-widget" id="timerWidget">
        <div class="timer-ring"><span id="timerVal">30</span></div>
        <div class="timer-label">SEC</div>
      </div>
    </div>
    <div class="left-panel">
      <div id="teamRoster"></div>
      <div class="utility-row">
        <button class="util-btn" data-util="jetpack" title="Jetpack">🚀<span>JET</span></button>
        <button class="util-btn" data-util="teleport" title="Teleport">🌀<span>TELE</span></button>
        <button class="util-btn" data-util="grapple" title="Grapple">🪝<span>HOOK</span></button>
      </div>
    </div>
    <div class="weapon-dock" id="weaponDock"></div>
    <div class="bottom-help">
      <span><b>A/D</b> Move • <b>W / SPACE</b> Jump • <b>Mouse</b> Aim • <b>Hold SPACE</b> Power • <b>Enter</b> Fire</span>
    </div>
    <div id="turnOverlay" class="turn-overlay hidden"><span id="turnOverlayText"></span></div>
    <div id="damageLayer" class="damage-layer"></div>
    <div id="powerBar" class="power-bar hidden"><div id="powerFill"></div><span id="powerLabel">0%</span></div>
  </div>
  <div id="gameOver" class="overlay hidden">
    <div class="setup-card narrow">
      <div class="logo"><h1 id="winnerTitle">VICTORY!</h1><p id="winnerSub">Team wins</p></div>
      <button id="replayBtn" class="launch-btn">PLAY AGAIN</button>
    </div>
  </div>
</div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./types":"src/types.ts","./audio":"src/audio.ts","./terrain":"src/terrain.ts","./particles":"src/particles.ts"},"src/particles.ts":{"./types":"src/types.ts","./terrain":"src/terrain.ts"}};
function __require(id) {
  if (__cache[id]) return __cache[id].exports;
  var module = __cache[id] = { exports: {} };
  var factory = __mods[id];
  if (!factory) throw new Error("Module not found: " + id);
  factory(module.exports, function (spec) {
    var target = (__map[id] && __map[id][spec]) || spec;
    return __require(target);
  }, module);
  return module.exports;
}

// ── module: src/main.ts ──
__mods["src/main.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const types_1 = require("./types");
const audio_1 = require("./audio");
const terrain_1 = require("./terrain");
const particles_1 = require("./particles");
// DOM
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
const setupEl = document.getElementById('setup');
const hudEl = document.getElementById('hud');
const weaponDock = document.getElementById('weaponDock');
const teamRosterEl = document.getElementById('teamRoster');
const windArrow = document.getElementById('windArrow');
const windValEl = document.getElementById('windVal');
const windFill = document.getElementById('windFill');
const turnTeamName = document.getElementById('turnTeamName');
const turnWormName = document.getElementById('turnWormName');
const timerVal = document.getElementById('timerVal');
const timerWidget = document.getElementById('timerWidget');
const turnBanner = document.getElementById('turnBanner');
const damageLayer = document.getElementById('damageLayer');
const turnOverlay = document.getElementById('turnOverlay');
const turnOverlayText = document.getElementById('turnOverlayText');
const powerBar = document.getElementById('powerBar');
const powerFill = document.getElementById('powerFill');
const powerLabel = document.getElementById('powerLabel');
const teamSetupEl = document.getElementById('teamSetup');
let teams = [];
let turnTeamIdx = 0;
let turnWormIdx = 0;
let wind = 0;
let windSetting = 'breeze';
let terrainType = 'island';
let gameState = 'setup';
let projectiles = [];
let selectedWeapon = types_1.WEAPONS[0].id;
let selectedUtil = null;
let utilUses = { jetpack: 2, teleport: 1, grapple: 2 };
let jetFuel = 0;
let isJetpacking = false;
let turnTime = 30;
let turnTimer = 30;
let camera = { x: terrain_1.W / 2, y: terrain_1.H / 2, scale: 1, targetScale: 1 };
let shake = 0;
let freeze = 0;
let mouse = { x: 0, y: 0, worldX: 0, worldY: 0, down: false };
let aimAngle = -0.35; // radians
let isCharging = false;
let charge = 0;
let firing = false;
let grapple = null;
// weapon ammo map per team
let teamAmmo = new Map();
const GRAVITY = 0.38;
const WIND_FORCE = 0.018;
function resize() {
    const dpr = Math.min(2, window.devicePixelRatio || 1);
    canvas.width = innerWidth * dpr;
    canvas.height = innerHeight * dpr;
    canvas.style.width = innerWidth + 'px';
    canvas.style.height = innerHeight + 'px';
    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
window.addEventListener('resize', resize);
resize();
(0, terrain_1.initTerrain)((x, y, w, h) => (0, particles_1.addDebris)(x, y, w, h));
// SETUP UI
let setupTeams = [
    { colorIdx: 0, name: 'CRIMSON', cpu: false },
    { colorIdx: 1, name: 'AQUA', cpu: true },
];
function renderTeamSetup() {
    teamSetupEl.innerHTML = '';
    setupTeams.forEach((t, i) => {
        const div = document.createElement('div');
        div.className = 'team-row';
        div.innerHTML = `
      <span class="team-color" style="background:${types_1.TEAM_COLORS[t.colorIdx].col}"></span>
      <input value="${t.name}" data-i="${i}" data-k="name" maxlength="10" />
      <select data-i="${i}" data-k="color">
        ${types_1.TEAM_COLORS.map((c, idx) => `<option value="${idx}" ${idx === t.colorIdx ? 'selected' : ''}>${c.name}</option>`).join('')}
      </select>
      <select data-i="${i}" data-k="cpu">
        <option value="0" ${!t.cpu ? 'selected' : ''}>Human</option>
        <option value="1" ${t.cpu ? 'selected' : ''}>CPU</option>
      </select>
      ${setupTeams.length > 2 ? `<button class="remove" data-rm="${i}">✕</button>` : ''}
    `;
        teamSetupEl.appendChild(div);
    });
    teamSetupEl.querySelectorAll('input,select').forEach(el => {
        el.addEventListener('change', (e) => {
            const t = e.target;
            const i = Number(t.dataset.i);
            const k = t.dataset.k;
            if (k === 'name')
                setupTeams[i].name = t.value.toUpperCase().slice(0, 10);
            if (k === 'color')
                setupTeams[i].colorIdx = Number(t.value);
            if (k === 'cpu')
                setupTeams[i].cpu = t.value === '1';
            renderTeamSetup();
        });
        el.addEventListener('mouseenter', () => audio_1.SFX.hover());
    });
    teamSetupEl.querySelectorAll('[data-rm]').forEach(b => {
        b.addEventListener('click', () => {
            const idx = Number(b.dataset.rm);
            setupTeams.splice(idx, 1);
            renderTeamSetup();
            audio_1.SFX.click();
        });
    });
}
renderTeamSetup();
document.getElementById('addTeamBtn').addEventListener('click', () => {
    if (setupTeams.length >= 4)
        return;
    const used = new Set(setupTeams.map(s => s.colorIdx));
    let free = types_1.TEAM_COLORS.findIndex((_, i) => !used.has(i));
    if (free < 0)
        free = setupTeams.length % types_1.TEAM_COLORS.length;
    setupTeams.push({ colorIdx: free, name: `TEAM ${setupTeams.length + 1}`, cpu: setupTeams.length >= 2 });
    renderTeamSetup();
    audio_1.SFX.click();
});
document.querySelectorAll('#terrainPills .pill').forEach(p => {
    p.addEventListener('click', () => {
        document.querySelectorAll('#terrainPills .pill').forEach(x => x.classList.remove('active'));
        p.classList.add('active');
        terrainType = p.dataset.v;
        audio_1.SFX.click();
    });
});
document.querySelectorAll('#windPills .pill').forEach(p => {
    p.addEventListener('click', () => {
        document.querySelectorAll('#windPills .pill').forEach(x => x.classList.remove('active'));
        p.classList.add('active');
        windSetting = p.dataset.v;
        audio_1.SFX.click();
    });
});
document.getElementById('launchBtn').addEventListener('click', launch);
document.getElementById('replayBtn').addEventListener('click', () => { location.reload(); });
function launch() {
    audio_1.SFX.menuHit();
    // build teams
    teams = [];
    setupTeams.forEach((s, idx) => {
        const col = types_1.TEAM_COLORS[s.colorIdx];
        const team = { id: idx, name: s.name, color: col, isCPU: s.cpu, worms: [], alive: true };
        teams.push(team);
        const am = new Map();
        types_1.WEAPONS.forEach(w => am.set(w.id, w.ammo === '∞' ? 999 : w.ammo));
        teamAmmo.set(String(idx), am);
    });
    (0, terrain_1.generate)(terrainType);
    const totalWorms = teams.length * 4;
    const spots = (0, terrain_1.findSpawnPositions)(totalWorms);
    let si = 0;
    teams.forEach(team => {
        for (let i = 0; i < 4; i++) {
            const p = spots[si++] || { x: 200 + Math.random() * (terrain_1.W - 400), y: 200 };
            const worm = { id: team.id * 10 + i, team: team.id, x: p.x, y: p.y, vx: 0, vy: 0, hp: 100, maxHp: 100, alive: true, name: `#${i + 1}`, grounded: false, fallDist: 0, drownT: 0 };
            // drop to ground
            let yy = p.y;
            while (!(0, terrain_1.isSolid)(worm.x, yy + 12) && yy < terrain_1.WATER_Y)
                yy++;
            worm.y = yy - 12;
            team.worms.push(worm);
        }
    });
    rollWind();
    buildWeaponDock();
    updateRoster();
    setupEl.classList.add('hidden');
    hudEl.classList.remove('hidden');
    nextTurn(true);
    gameState = 'playing';
}
function rollWind() {
    const mag = windSetting === 'calm' ? 1.2 : windSetting === 'breeze' ? 3.5 : 6;
    wind = (Math.random() * 2 - 1) * mag;
    const pct = (wind / mag + 1) / 2 * 100;
    windFill.style.width = pct + '%';
    windValEl.textContent = wind.toFixed(1);
    const deg = wind > 0 ? 0 : 180;
    windArrow.style.transform = `rotate(${deg}deg)`;
    windArrow.style.color = Math.abs(wind) > mag * 0.7 ? '#ef4444' : Math.abs(wind) > mag * 0.35 ? '#facc15' : '#4ade80';
}
function buildWeaponDock() {
    weaponDock.innerHTML = '';
    types_1.WEAPONS.forEach(w => {
        const d = document.createElement('button');
        d.className = 'wep';
        d.dataset.id = w.id;
        if (w.id === selectedWeapon)
            d.classList.add('selected');
        const ammo = teamAmmo.get(String(turnTeamIdx))?.get(w.id) ?? 0;
        const ammoTxt = w.ammo === '∞' ? '∞' : String(ammo);
        d.innerHTML = `
      <div class="cat">${w.cat.toUpperCase()}</div>
      <div class="icon">${w.icon}</div>
      <div class="name">${w.name}</div>
      <div class="ammo">${ammoTxt} • ${w.damage}DMG</div>
      <div class="sel-dot">✓</div>
    `;
        d.addEventListener('click', () => {
            const am = teamAmmo.get(String(turnTeamIdx))?.get(w.id) ?? 0;
            if (w.ammo !== '∞' && am <= 0)
                return;
            selectedWeapon = w.id;
            document.querySelectorAll('.wep').forEach(e => e.classList.remove('selected'));
            d.classList.add('selected');
            audio_1.SFX.click();
        });
        d.addEventListener('mouseenter', () => audio_1.SFX.hover());
        weaponDock.appendChild(d);
    });
}
function updateRoster() {
    teamRosterEl.innerHTML = '';
    teams.forEach((t, ti) => {
        const card = document.createElement('div');
        card.className = 'team-card' + (ti === turnTeamIdx && gameState !== 'gameover' ? ' active' : '');
        card.innerHTML = `
      <div class="team-head"><i style="background:${t.color.col}"></i> ${t.name} ${t.isCPU ? '<span style="font-size:9px;background:rgba(255,255,255,.12);padding:2px 6px;border-radius:999px">CPU</span>' : ''}</div>
      <div class="worm-list">${t.worms.map((w, wi) => {
            const isAct = ti === turnTeamIdx && wi === turnWormIdx && w.alive;
            return `<div class="worm-chip ${isAct ? 'active' : ''} ${!w.alive ? 'dead' : ''}">
          <div style="font-size:10px;font-weight:900">${w.name} ${!w.alive ? '💀' : ''}</div>
          <div style="font-size:10px">${w.hp}</div>
          <div class="hp"><i style="width:${Math.max(0, w.hp)}%; background:${w.hp > 50 ? 'linear-gradient(90deg,#4ade80,#22c55e)' : w.hp > 25 ? 'linear-gradient(90deg,#facc15,#f59e0b)' : 'linear-gradient(90deg,#ef4444,#dc2626)'}"></i></div>
        </div>`;
        }).join('')}</div>
    `;
        teamRosterEl.appendChild(card);
    });
    const cur = activeWorm();
    if (cur) {
        turnTeamName.textContent = teams[turnTeamIdx].name;
        turnTeamName.style.color = teams[turnTeamIdx].color.col;
        turnTeamName.style.textShadow = `0 1px 6px ${teams[turnTeamIdx].color.col}66`;
        turnWormName.textContent = `'s ${cur.name}`;
        turnBanner.style.borderColor = teams[turnTeamIdx].color.col + '55';
    }
}
function activeWorm() {
    const t = teams[turnTeamIdx];
    if (!t)
        return null;
    return t.worms[turnWormIdx] ?? null;
}
function nextTurn(first = false) {
    // find next alive worm
    let attempts = 0;
    while (attempts < 20) {
        turnWormIdx = (turnWormIdx + 1) % 4;
        if (turnWormIdx === 0) {
            turnTeamIdx = (turnTeamIdx + 1) % teams.length;
            // skip dead teams
            let skip = 0;
            while (!teams[turnTeamIdx].alive && skip < 8) {
                turnTeamIdx = (turnTeamIdx + 1) % teams.length;
                skip++;
            }
            rollWind();
        }
        const w = activeWorm();
        if (w && w.alive)
            break;
        attempts++;
        if (attempts > 12)
            break;
    }
    if (first) {
        turnTeamIdx = 0;
        turnWormIdx = 0; // ensure alive
        let aw = activeWorm();
        let c = 0;
        while (aw && !aw.alive && c < 20) {
            turnWormIdx = (turnWormIdx + 1) % 4;
            if (turnWormIdx === 0)
                turnTeamIdx = (turnTeamIdx + 1) % teams.length;
            aw = activeWorm();
            c++;
        }
    }
    turnTime = 30;
    turnTimer = 30;
    isCharging = false;
    charge = 0;
    firing = false;
    projectiles = [];
    selectedUtil = null;
    jetFuel = 0;
    isJetpacking = false;
    grapple = null;
    utilUses = { jetpack: 2, teleport: 1, grapple: 2 };
    document.querySelectorAll('.util-btn').forEach(b => b.classList.remove('active'));
    buildWeaponDock();
    updateRoster();
    showTurnOverlay(`${teams[turnTeamIdx].name}'s TURN`);
    audio_1.SFX.turn();
    // center camera on worm
    const aw = activeWorm();
    if (aw) {
        camera.x = aw.x;
        camera.y = aw.y - 120;
    }
    if (teams[turnTeamIdx].isCPU)
        setTimeout(cpuTurn, 900);
}
function showTurnOverlay(txt) {
    turnOverlayText.textContent = txt;
    turnOverlay.classList.remove('hidden');
    setTimeout(() => turnOverlay.classList.add('hidden'), 1100);
}
function showDamage(x, y, dmg, crit = false) {
    const el = document.createElement('div');
    el.className = 'fct';
    const rect = canvas.getBoundingClientRect();
    // convert world to screen
    const sx = (x - camera.x) * camera.scale + rect.width / 2;
    const sy = (y - camera.y) * camera.scale + rect.height / 2;
    el.style.left = sx + 'px';
    el.style.top = sy + 'px';
    el.style.color = crit ? '#facc15' : '#fff';
    el.textContent = `-${dmg}`;
    damageLayer.appendChild(el);
    setTimeout(() => el.remove(), 950);
}
// INPUT
const keys = new Set();
window.addEventListener('keydown', (e) => {
    if (gameState === 'setup')
        return;
    const k = e.key.toLowerCase();
    keys.add(k);
    if (k === ' ') {
        if (gameState === 'playing' && !isCharging && !teams[turnTeamIdx].isCPU) {
            // if jetpacking, already handled
            if (selectedUtil === 'jetpack' && jetFuel > 0)
                return;
            isCharging = true;
            charge = 0;
            audio_1.SFX.charge();
            e.preventDefault();
        }
    }
    if (k === 'enter') {
        if (isCharging)
            fireCharged();
    }
    if (k === 'w') {
        tryJump();
    }
    if (k === 'f1') {
        selectUtil('jetpack');
        e.preventDefault();
    }
    if (k === 'f2') {
        selectUtil('teleport');
        e.preventDefault();
    }
    if (k === 'f3') {
        selectUtil('grapple');
        e.preventDefault();
    }
});
window.addEventListener('keyup', (e) => {
    const k = e.key.toLowerCase();
    keys.delete(k);
    if (k === ' ' && isCharging)
        fireCharged();
});
canvas.addEventListener('mousemove', (e) => {
    const rect = canvas.getBoundingClientRect();
    mouse.x = e.clientX - rect.left;
    mouse.y = e.clientY - rect.top;
    mouse.worldX = (mouse.x - rect.width / 2) / camera.scale + camera.x;
    mouse.worldY = (mouse.y - rect.height / 2) / camera.scale + camera.y;
    const aw = activeWorm();
    if (aw && gameState === 'playing') {
        aimAngle = Math.atan2(mouse.worldY - aw.y, mouse.worldX - aw.x);
    }
});
canvas.addEventListener('mousedown', (e) => {
    if (gameState !== 'playing' || teams[turnTeamIdx].isCPU)
        return;
    if (selectedUtil === 'teleport') {
        const aw = activeWorm();
        if (!aw)
            return;
        if (utilUses.teleport <= 0)
            return;
        // teleport to mouse if not solid
        if (!(0, terrain_1.isSolid)(mouse.worldX, mouse.worldY) && mouse.worldY < terrain_1.WATER_Y - 10) {
            aw.x = mouse.worldX;
            aw.y = mouse.worldY;
            aw.vx = 0;
            aw.vy = 0;
            utilUses.teleport--;
            (0, particles_1.spawnDust)(aw.x, aw.y, 18);
            audio_1.SFX.water();
            selectedUtil = null;
            document.querySelectorAll('.util-btn').forEach(b => b.classList.remove('active'));
        }
        return;
    }
    if (selectedUtil === 'grapple') {
        if (utilUses.grapple <= 0)
            return;
        const aw = activeWorm();
        if (!aw)
            return;
        const hit = (0, terrain_1.rayTerrain)(aw.x, aw.y, mouse.worldX, mouse.worldY);
        if (hit.hit) {
            grapple = { x: hit.x, y: hit.y, active: true, len: Math.hypot(hit.x - aw.x, hit.y - aw.y), ang: Math.atan2(hit.y - aw.y, hit.x - aw.x) };
            utilUses.grapple--;
            audio_1.SFX.fire();
            selectedUtil = null;
            document.querySelectorAll('.util-btn').forEach(b => b.classList.remove('active'));
        }
        return;
    }
    isCharging = true;
    charge = 0;
});
canvas.addEventListener('mouseup', () => { if (isCharging)
    fireCharged(); });
document.querySelectorAll('.util-btn').forEach(b => {
    b.addEventListener('click', () => {
        const u = b.dataset.util;
        selectUtil(u);
        audio_1.SFX.click();
    });
});
function selectUtil(u) {
    if (gameState !== 'playing' || teams[turnTeamIdx].isCPU)
        return;
    if (selectedUtil === u) {
        selectedUtil = null;
        document.querySelectorAll('.util-btn').forEach(x => x.classList.remove('active'));
        return;
    }
    if (u === 'jetpack' && utilUses.jetpack <= 0)
        return;
    if (u === 'teleport' && utilUses.teleport <= 0)
        return;
    if (u === 'grapple' && utilUses.grapple <= 0)
        return;
    selectedUtil = u;
    document.querySelectorAll('.util-btn').forEach(x => x.classList.toggle('active', x.dataset.util === u));
    if (u === 'jetpack') {
        jetFuel = 110;
        isJetpacking = false;
    }
}
function tryJump() {
    const w = activeWorm();
    if (!w || !w.alive || gameState !== 'playing')
        return;
    if (!w.grounded)
        return;
    w.vy = -7.2;
    w.grounded = false;
    (0, particles_1.spawnDust)(w.x, w.y + 12, 5);
    audio_1.SFX.jump();
    // directional jump with A/D
    if (keys.has('a'))
        w.vx -= 3.5;
    if (keys.has('d'))
        w.vx += 3.5;
}
function fireCharged() {
    if (!isCharging || firing)
        return;
    isCharging = false;
    const pct = Math.min(1, charge);
    if (pct < 0.08) {
        charge = 0;
        return;
    }
    fireWeapon(selectedWeapon, pct);
    charge = 0;
    powerBar.classList.add('hidden');
}
function fireWeapon(id, power) {
    const aw = activeWorm();
    if (!aw)
        return;
    const def = types_1.WEAPONS.find(w => w.id === id);
    const amMap = teamAmmo.get(String(turnTeamIdx));
    const cur = amMap.get(id) ?? 0;
    if (def.ammo !== '∞' && cur <= 0)
        return;
    if (def.ammo !== '∞')
        amMap.set(id, cur - 1);
    buildWeaponDock();
    // compute velocity
    const speed = 4 + power * 16; // up to ~20
    const vx = Math.cos(aimAngle) * speed + wind * 0.06;
    const vy = Math.sin(aimAngle) * speed;
    const sx = aw.x + Math.cos(aimAngle) * 18, sy = aw.y - 6 + Math.sin(aimAngle) * 10;
    if (id === 'railgun') {
        // instant ray
        audio_1.SFX.rail();
        const endX = sx + Math.cos(aimAngle) * 2600, endY = sy + Math.sin(aimAngle) * 2600;
        const hit = (0, terrain_1.rayTerrain)(sx, sy, endX, endY);
        const hx = hit.hit ? hit.x : endX, hy = hit.hit ? hit.y : endY;
        // damage worms along line
        teams.forEach(t => t.worms.forEach(w => {
            if (!w.alive)
                return;
            // distance to line segment
            const d = distToSegment(w.x, w.y, sx, sy, hx, hy);
            if (d < 18) {
                applyDamage(w, def.damage, sx, sy);
            }
        }));
        if (hit.hit) {
            (0, terrain_1.carveCircle)(hit.x, hit.y, def.radius * 0.7);
            (0, particles_1.spawnExplosion)(hit.x, hit.y, 10, '#7dd3fc');
            addShake(8);
        }
        // visual line handled in draw
        projectiles.railLine = { x1: sx, y1: sy, x2: hx, y2: hy, life: 0.18 };
        setTimeout(() => endProjectileTurn(), 420);
        (0, particles_1.spawnMuzzle)(sx, sy, aimAngle);
        return;
    }
    if (id === 'shotgun') {
        audio_1.SFX.fire();
        firing = true;
        gameState = 'projectile';
        for (let i = 0; i < 5; i++) {
            const spread = (Math.random() - 0.5) * 0.28;
            const a = aimAngle + spread;
            const sp = speed * (0.9 + Math.random() * 0.2);
            projectiles.push({ x: sx, y: sy, vx: Math.cos(a) * sp, vy: Math.sin(a) * sp, r: 4, type: 'shotgun', alive: true, owner: turnTeamIdx });
        }
        (0, particles_1.spawnMuzzle)(sx, sy, aimAngle);
        return;
    }
    if (id === 'airstrike') {
        audio_1.SFX.fire();
        firing = true;
        gameState = 'projectile';
        const tx = mouse.worldX;
        for (let i = 0; i < 3; i++) {
            setTimeout(() => {
                const px = tx + (i - 1) * 110 + (Math.random() - 0.5) * 30;
                projectiles.push({ x: px, y: 60, vx: wind * 0.05 + (Math.random() - 0.5) * 1.2, vy: 6 + Math.random() * 2, r: 6, type: 'airstrike', alive: true, owner: turnTeamIdx });
            }, i * 220);
        }
        return;
    }
    if (id === 'holy') {
        audio_1.SFX.holy();
        firing = true;
        gameState = 'projectile';
        projectiles.push({ x: sx, y: sy, vx: Math.cos(aimAngle) * speed * 0.7, vy: Math.sin(aimAngle) * speed * 0.7, r: 8, type: 'holy', alive: true, bounce: 0, fuse: 3.2, owner: turnTeamIdx });
        (0, particles_1.spawnMuzzle)(sx, sy, aimAngle);
        return;
    }
    if (id === 'drill') {
        audio_1.SFX.fire();
        firing = true;
        gameState = 'projectile';
        // drill goes straight then explodes underground
        projectiles.push({ x: sx, y: sy, vx: Math.cos(aimAngle) * speed * 0.9, vy: Math.sin(aimAngle) * speed * 0.9, r: 5, type: 'drill', alive: true, owner: turnTeamIdx, bounce: 0 });
        (0, particles_1.spawnMuzzle)(sx, sy, aimAngle);
        return;
    }
    // bazooka / grenade default
    audio_1.SFX.fire();
    firing = true;
    gameState = 'projectile';
    const type = id === 'grenade' ? 'grenade' : 'bazooka';
    projectiles.push({ x: sx, y: sy, vx, vy, r: 6, type, alive: true, bounce: type === 'grenade' ? 0 : undefined, fuse: type === 'grenade' ? 3 : undefined, owner: turnTeamIdx });
    (0, particles_1.spawnMuzzle)(sx, sy, aimAngle);
}
function distToSegment(px, py, x1, y1, x2, y2) {
    const l2 = (x2 - x1) ** 2 + (y2 - y1) ** 2;
    if (l2 === 0)
        return Math.hypot(px - x1, py - y1);
    let t = ((px - x1) * (x2 - x1) + (py - y1) * (y2 - y1)) / l2;
    t = Math.max(0, Math.min(1, t));
    const x = x1 + t * (x2 - x1), y = y1 + t * (y2 - y1);
    return Math.hypot(px - x, py - y);
}
function applyDamage(w, dmg, fromX, fromY) {
    if (!w.alive)
        return;
    w.hp = Math.max(0, w.hp - dmg);
    showDamage(w.x, w.y, dmg, dmg > 40);
    audio_1.SFX.hurt();
    (0, particles_1.spawnDust)(w.x, w.y, 4);
    w.vx += (w.x - fromX) * 0.04;
    w.vy += -2 - Math.random() * 2;
    if (w.hp <= 0) {
        w.alive = false;
        (0, particles_1.spawnExplosion)(w.x, w.y, 12, '#ff4444');
    }
    updateRoster();
    checkWin();
}
function explosionAt(x, y, rad, dmg) {
    (0, terrain_1.carveCircle)(x, y, rad);
    (0, particles_1.spawnExplosion)(x, y, rad * 0.45);
    addShake(Math.min(18, rad * 0.16));
    impactFreeze(rad > 100 ? 180 : rad > 70 ? 110 : 0);
    // damage worms
    teams.forEach(t => t.worms.forEach(w => {
        if (!w.alive)
            return;
        const d = Math.hypot(w.x - x, w.y - y);
        if (d < rad + 22) {
            const fall = 1 - d / (rad + 22);
            const dd = Math.floor(dmg * fall);
            if (dd > 0)
                applyDamage(w, dd, x, y);
            // knockback
            const ang = Math.atan2(w.y - y, w.x - x);
            const force = (rad - d) * 0.08 + 4;
            w.vx += Math.cos(ang) * force;
            w.vy += Math.sin(ang) * force - 1;
        }
    }));
    // chain terrain falling handled by debris
}
function addShake(v) { shake = Math.max(shake, v); }
function impactFreeze(ms) {
    if (ms > 0)
        freeze = ms;
}
function checkWin() {
    const aliveTeams = teams.filter(t => t.worms.some(w => w.alive));
    if (aliveTeams.length <= 1 && gameState !== 'gameover') {
        gameState = 'gameover';
        const win = aliveTeams[0];
        document.getElementById('winnerTitle').textContent = win ? `${win.name} WINS!` : `DRAW!`;
        document.getElementById('winnerSub').textContent = win ? `Victory in the pocket!` : `All worms perished`;
        document.getElementById('gameOver').classList.remove('hidden');
        audio_1.SFX.win();
    }
}
function endProjectileTurn() {
    // small delay then next turn unless gameover
    if (gameState === 'gameover')
        return;
    gameState = 'playing';
    firing = false;
    projectiles = [];
    projectiles.railLine = null;
    // check if active worm died from self damage etc, still next turn
    setTimeout(() => nextTurn(), 700);
}
// CPU AI
function cpuTurn() {
    if (gameState !== 'playing' || !teams[turnTeamIdx].isCPU)
        return;
    const me = activeWorm();
    if (!me || !me.alive) {
        nextTurn();
        return;
    }
    // simple movement: if near cliff/water, move inward
    // evaluate targets
    const enemies = [];
    teams.forEach(t => { if (t.id !== turnTeamIdx)
        t.worms.forEach(w => { if (w.alive)
            enemies.push(w); }); });
    if (enemies.length === 0) {
        nextTurn();
        return;
    }
    // choose high value target: lowest hp or closest
    enemies.sort((a, b) => (a.hp - b.hp) || (Math.hypot(a.x - me.x, a.y - me.y) - Math.hypot(b.x - me.x, b.y - me.y)));
    const target = enemies[0];
    // risk assessment: avoid firing if target near water and wind pushes away? just basic
    // choose weapon with ammo
    const ammoMap = teamAmmo.get(String(turnTeamIdx));
    let candidates = types_1.WEAPONS.filter(w => {
        const a = ammoMap.get(w.id) ?? 0;
        return w.ammo === '∞' ? true : a > 0;
    });
    // prefer high damage if close, else bazooka
    const dist = Math.hypot(target.x - me.x, target.y - me.y);
    let chosen = candidates.find(c => c.id === 'bazooka') || candidates[0];
    if (dist > 600 && candidates.find(c => c.id === 'airstrike'))
        chosen = candidates.find(c => c.id === 'airstrike');
    else if (dist < 160 && candidates.find(c => c.id === 'shotgun'))
        chosen = candidates.find(c => c.id === 'shotgun');
    else if (dist < 250 && candidates.find(c => c.id === 'holy') && Math.random() < 0.3)
        chosen = candidates.find(c => c.id === 'holy');
    selectedWeapon = chosen.id;
    buildWeaponDock();
    // aim calculation: solve parabolic with wind compensation (simple iterative)
    const g = GRAVITY;
    const power = 0.78 + Math.random() * 0.18;
    const speed = 4 + power * 16;
    // estimate angle to hit target with wind
    // brute search angles
    let bestAng = aimAngle, bestErr = Infinity;
    for (let deg = -80; deg <= 50; deg += 2) {
        const a = deg * Math.PI / 180;
        // simulate trajectory quickly
        let sx = me.x, sy = me.y, vx = Math.cos(a) * speed + wind * 0.06, vy = Math.sin(a) * speed;
        let hit = false;
        let err = Infinity;
        for (let t = 0; t < 180; t++) {
            sx += vx;
            sy += vy;
            vy += g;
            vx += wind * WIND_FORCE * 0.25;
            if ((0, terrain_1.isSolid)(sx, sy)) {
                hit = true;
                break;
            }
            if (Math.hypot(sx - target.x, sy - target.y) < 42) {
                err = Math.hypot(sx - target.x, sy - target.y);
                break;
            }
            if (sy > terrain_1.WATER_Y + 40)
                break;
        }
        if (err < bestErr) {
            bestErr = err;
            bestAng = a;
        }
        if (bestErr < 18)
            break;
    }
    aimAngle = bestAng;
    // avoid hazard: if worm near water edge, step away
    let nearWater = false;
    for (let dx = -18; dx <= 18; dx += 9)
        if (!(0, terrain_1.isSolid)(me.x + dx, me.y + 22))
            nearWater = true;
    if (nearWater) {
        // move inward: if on left half move right else left
        const dir = me.x < terrain_1.W / 2 ? 1 : -1;
        me.vx += dir * 2.2;
        (0, particles_1.spawnDust)(me.x, me.y + 10, 3);
    }
    setTimeout(() => {
        if (gameState !== 'playing')
            return;
        isCharging = true;
        charge = power;
        // simulate charge bar
        setTimeout(() => fireWeapon(chosen.id, power), 650);
    }, 700);
}
// LOOP
let last = performance.now();
function loop(now) {
    const dt = Math.min(0.033, (now - last) / 1000);
    last = now;
    if (freeze > 0) {
        freeze -= dt * 1000;
        requestAnimationFrame(loop);
        draw();
        return;
    }
    update(dt);
    draw();
    requestAnimationFrame(loop);
}
function update(dt) {
    if (gameState === 'setup')
        return;
    // timer
    if (gameState === 'playing') {
        turnTimer -= dt;
        if (turnTimer <= 0) {
            turnTimer = 0;
            isCharging = false;
            charge = 0;
            nextTurn();
        }
        const pct = turnTimer / 30;
        timerVal.textContent = Math.ceil(turnTimer).toString();
        if (turnTimer < 8) {
            timerWidget.classList.add('panic');
            if (Math.floor(turnTimer * 2) % 2 === 0)
                audio_1.SFX.panicTick();
        }
        else if (turnTimer < 15) {
            timerWidget.classList.remove('panic');
            if (Math.floor(turnTimer) % 1 === 0 && Math.random() < 0.02)
                audio_1.SFX.tick(Math.floor(30 - turnTimer));
        }
        else
            timerWidget.classList.remove('panic');
        // scale timer based on urgency
        const scale = turnTimer < 8 ? 1 + (8 - turnTimer) * 0.06 : 1;
        timerWidget.style.transform = `scale(${scale})`;
        // weapon dock tension
        const dockScale = turnTimer < 8 ? 0.96 : 1;
        weaponDock.style.transform = `translateX(-50%) scale(${dockScale})`;
    }
    // charging
    if (isCharging) {
        charge = Math.min(1, charge + dt * 0.85);
        powerBar.classList.remove('hidden');
        powerFill.style.width = (charge * 100) + '%';
        powerLabel.textContent = Math.floor(charge * 100) + '%';
        const col = charge < 0.33 ? '#4ade80' : charge < 0.66 ? '#facc15' : '#ef4444';
        powerFill.style.background = col;
        // subtle shake
        if (charge > 0.9)
            addShake(1.2);
    }
    // worm physics
    teams.forEach(team => team.worms.forEach(w => {
        if (!w.alive)
            return;
        // water
        if (w.y > terrain_1.WATER_Y - 14) {
            w.drownT = (w.drownT || 0) + dt;
            w.y += 0.6; // float/sink slowly then drown
            w.vy += 0.08;
            (0, particles_1.spawnSplash)(w.x, terrain_1.WATER_Y);
            if (w.drownT > 1.1) {
                applyDamage(w, 14, w.x, w.y);
                w.drownT = 0;
                audio_1.SFX.water();
            }
            if (w.y > terrain_1.WATER_Y + 50) { // drowned
                w.hp = 0;
                w.alive = false;
                (0, particles_1.spawnSplash)(w.x, w.y);
                updateRoster();
                checkWin();
            }
        }
        else {
            w.drownT = 0;
        }
        // gravity & terrain collision
        w.vy += GRAVITY;
        w.vx *= 0.992; // air friction ; ground friction applied later
        // wind affects worm slightly when airborne
        if (!w.grounded)
            w.vx += wind * WIND_FORCE * 0.08 * dt * 60;
        let nx = w.x + w.vx, ny = w.y + w.vy;
        // vertical collision
        if ((0, terrain_1.isSolidCircle)(nx, ny + 10, 11)) {
            // try step up
            if ((0, terrain_1.isSolidCircle)(nx, ny + 6, 11) && !(0, terrain_1.isSolidCircle)(nx, ny - 4, 11)) {
                ny -= 4;
                w.vy = Math.min(w.vy, 0);
            }
            else {
                // land
                // find ground
                let gy = ny;
                let iter = 0;
                while ((0, terrain_1.isSolidCircle)(nx, gy + 10, 11) && iter < 10) {
                    gy -= 1;
                    iter++;
                }
                ny = gy;
                // fall damage
                if (w.vy > 9) {
                    const dmg = Math.floor((w.vy - 9) * 3.2);
                    if (dmg > 0)
                        applyDamage(w, Math.min(dmg, 38), w.x, w.y);
                    (0, particles_1.spawnDust)(w.x, ny + 12, 8);
                    w.vy *= -0.12;
                }
                else {
                    w.vy *= -0.18;
                    if (Math.abs(w.vy) < 0.6)
                        w.vy = 0;
                }
                if (Math.abs(w.vx) > 0.2)
                    (0, particles_1.spawnDust)(w.x, ny + 12, 1);
                w.grounded = true;
                w.fallDist = 0;
            }
        }
        else {
            w.grounded = false;
            w.fallDist += Math.max(0, w.vy);
            if (w.vy > 12)
                w.vy = 12;
        }
        // horizontal collision
        if ((0, terrain_1.isSolidCircle)(nx, ny + 4, 11)) {
            w.vx *= -0.3;
            nx = w.x;
        }
        w.x = nx;
        w.y = ny;
        // ground friction
        if (w.grounded)
            w.vx *= 0.86;
        // bounds
        if (w.x < 18) {
            w.x = 18;
            w.vx = 0;
        }
        if (w.x > terrain_1.W - 18) {
            w.x = terrain_1.W - 18;
            w.vx = 0;
        }
        if (w.y > terrain_1.H + 40) {
            w.alive = false;
            updateRoster();
        }
    }));
    // player movement
    const aw = activeWorm();
    if (aw && gameState === 'playing' && !teams[turnTeamIdx].isCPU) {
        const moveSpeed = 2.1;
        const beforeX = aw.x;
        if (keys.has('a') && !isCharging) {
            aw.vx -= 0.45;
            if (aw.grounded)
                aw.x -= moveSpeed;
        }
        if (keys.has('d') && !isCharging) {
            aw.vx += 0.45;
            if (aw.grounded)
                aw.x += moveSpeed;
        }
        if (Math.abs(aw.x - beforeX) > 0.8 && aw.grounded && Math.random() < 0.18) {
            (0, particles_1.spawnDust)(aw.x, aw.y + 12, 1);
            if (Math.random() < 0.25)
                audio_1.SFX.step();
        }
        // jetpack
        if (selectedUtil === 'jetpack' && jetFuel > 0) {
            if (keys.has('w') || keys.has(' ') || keys.has('arrowup')) {
                aw.vy -= 0.52;
                aw.vx += wind * WIND_FORCE * 0.05;
                jetFuel -= 1;
                isJetpacking = true;
                if (Math.random() < 0.5)
                    particles_1.particles.push({ x: aw.x + (Math.random() - 0.5) * 8, y: aw.y + 14, vx: (Math.random() - 0.5) * 1.2, vy: 2 + Math.random() * 2, life: 1, maxLife: 0.35, r: 2 + Math.random() * 2, col: Math.random() < 0.5 ? '#ff8c2a' : '#ffd23f', alpha: 1, type: 'spark' });
                if (Math.random() < 0.08)
                    audio_1.SFX.fire();
                if (jetFuel <= 0) {
                    selectedUtil = null;
                    utilUses.jetpack--;
                    document.querySelectorAll('.util-btn').forEach(b => b.classList.remove('active'));
                }
            }
            else
                isJetpacking = false;
        }
        // grapple physics
        if (grapple && grapple.active) {
            const dx = grapple.x - aw.x, dy = grapple.y - aw.y;
            const dist = Math.hypot(dx, dy);
            if (dist > grapple.len) {
                const ang = Math.atan2(dy, dx);
                const pull = (dist - grapple.len) * 0.08;
                aw.vx += Math.cos(ang) * pull;
                aw.vy += Math.sin(ang) * pull;
                // wind affects rope
                aw.vx += wind * 0.01;
            }
            // release if near
            if (dist < 22) {
                grapple = null;
            }
            if (keys.has(' ')) {
                grapple = null;
                audio_1.SFX.jump();
            }
        }
    }
    // projectiles
    if (projectiles.length > 0) {
        const toExplode = [];
        for (let i = projectiles.length - 1; i >= 0; i--) {
            const p = projectiles[i];
            if (!p.alive)
                continue;
            // special drill: goes through terrain then explodes
            if (p.type === 'drill') {
                p.vy += GRAVITY * 0.9;
                p.vx += wind * WIND_FORCE * 0.6;
                p.x += p.vx;
                p.y += p.vy;
                p.vy = Math.min(p.vy, 14);
                // if inside solid, continue drilling then explode
                if ((0, terrain_1.isSolid)(p.x, p.y)) {
                    // carve tunnel
                    (0, terrain_1.carveCircle)(p.x, p.y, 10);
                    p.vy += 0.6;
                    if (p.vy > 1.5 && Math.random() < 0.5)
                        (0, particles_1.spawnDust)(p.x, p.y, 1);
                    // after going 40px deep, explode
                    // we track distance drilled via bounce field
                    p.bounce += 1;
                    if (p.bounce > 28) {
                        toExplode.push({ x: p.x, y: p.y, rad: 44, dmg: 30 });
                        p.alive = false;
                        projectiles.splice(i, 1);
                    }
                }
                else {
                    // in air, still check water
                    if (p.y > terrain_1.WATER_Y) {
                        (0, particles_1.spawnSplash)(p.x, terrain_1.WATER_Y);
                        p.alive = false;
                        projectiles.splice(i, 1);
                        continue;
                    }
                    if (p.y > terrain_1.H + 100) {
                        p.alive = false;
                        projectiles.splice(i, 1);
                        continue;
                    }
                }
                continue;
            }
            p.vy += GRAVITY;
            p.vx += wind * WIND_FORCE;
            if (p.type === 'grenade' || p.type === 'holy') {
                p.fuse -= dt;
                if (p.fuse <= 0) {
                    toExplode.push({ x: p.x, y: p.y, rad: p.type === 'holy' ? 150 : 70, dmg: p.type === 'holy' ? 75 : 42 });
                    p.alive = false;
                    projectiles.splice(i, 1);
                    continue;
                }
            }
            const nx = p.x + p.vx, ny = p.y + p.vy;
            if ((0, terrain_1.isSolid)(nx, ny)) {
                if (p.type === 'grenade' && (p.bounce ?? 0) < 4) {
                    // bounce
                    // simple reflect: check horizontal vs vertical
                    const hitX = (0, terrain_1.isSolid)(nx, p.y), hitY = (0, terrain_1.isSolid)(p.x, ny);
                    if (hitX)
                        p.vx *= -0.62;
                    if (hitY)
                        p.vy *= -0.55;
                    else {
                        p.vx *= -0.5;
                        p.vy *= -0.5;
                    }
                    p.x += p.vx * 0.5;
                    p.y += p.vy * 0.5;
                    p.bounce++;
                    (0, particles_1.spawnDust)(p.x, p.y, 3);
                    continue;
                }
                else if (p.type === 'holy') {
                    p.vx *= -0.4;
                    p.vy *= -0.4;
                    p.bounce++;
                    continue;
                }
                else {
                    // explode
                    const def = types_1.WEAPONS.find(w => w.projectile === p.type) || types_1.WEAPONS[0];
                    const rad = p.type === 'holy' ? 150 : p.type === 'airstrike' ? 62 : def.radius;
                    const dmg = p.type === 'holy' ? 75 : p.type === 'airstrike' ? 32 : def.damage;
                    toExplode.push({ x: nx, y: ny, rad, dmg });
                    p.alive = false;
                    projectiles.splice(i, 1);
                    continue;
                }
            }
            p.x = nx;
            p.y = ny;
            // water splash
            if (p.y > terrain_1.WATER_Y) {
                (0, particles_1.spawnSplash)(p.x, terrain_1.WATER_Y);
                if (p.type === 'airstrike' || p.type === 'bazooka' || p.type === 'shotgun') {
                    toExplode.push({ x: p.x, y: p.y - 8, rad: p.type === 'shotgun' ? 18 : 48, dmg: p.type === 'shotgun' ? 14 : 22 });
                }
                p.alive = false;
                projectiles.splice(i, 1);
                continue;
            }
            if (p.y > terrain_1.H + 120 || p.x < -120 || p.x > terrain_1.W + 120) {
                p.alive = false;
                projectiles.splice(i, 1);
            }
        }
        toExplode.forEach(e => explosionAt(e.x, e.y, e.rad, e.dmg));
        if (projectiles.length === 0 && firing) {
            // small delay before ending turn
            if (toExplode.length > 0)
                setTimeout(() => endProjectileTurn(), 900);
            else if (gameState === 'projectile')
                setTimeout(() => endProjectileTurn(), 400);
        }
    }
    // rail line fade
    const rl = projectiles.railLine;
    if (rl) {
        rl.life -= dt;
        if (rl.life <= 0)
            projectiles.railLine = null;
    }
    (0, particles_1.updateParticles)(dt, GRAVITY, wind);
    // camera
    if (aw) {
        const targetX = projectiles.length > 0 ? (projectiles[0].x + aw.x) / 2 : aw.x;
        const targetY = projectiles.length > 0 ? (projectiles[0].y + aw.y) / 2 - 40 : aw.y - 90;
        camera.x += (targetX - camera.x) * 0.08;
        camera.y += (targetY - camera.y) * 0.08;
        // zoom based on projectile height
        let desiredScale = 1;
        if (projectiles.length > 0) {
            const h = Math.abs(projectiles[0].y - aw.y);
            desiredScale = Math.max(0.72, Math.min(1, 1 - h / 1200));
        }
        else if (isCharging)
            desiredScale = 0.92;
        camera.targetScale += (desiredScale - camera.targetScale) * 0.06;
        camera.scale += (camera.targetScale - camera.scale) * 0.08;
    }
    // clamp camera
    const viewW = innerWidth / camera.scale, viewH = innerHeight / camera.scale;
    camera.x = Math.max(viewW / 2, Math.min(terrain_1.W - viewW / 2, camera.x));
    camera.y = Math.max(viewH / 2 - 60, Math.min(terrain_1.H - viewH / 2 + 30, camera.y));
    if (shake > 0)
        shake *= Math.pow(0.86, dt * 60);
    if (shake < 0.12)
        shake = 0;
    // update hud ammo disables
    document.querySelectorAll('.wep').forEach(el => {
        const id = el.dataset.id;
        const am = teamAmmo.get(String(turnTeamIdx))?.get(id) ?? 0;
        const def = types_1.WEAPONS.find(w => w.id === id);
        if (def.ammo !== '∞' && am <= 0)
            el.style.opacity = '0.45';
        else
            el.style.opacity = '1';
    });
    // util btn disable
    document.querySelectorAll('.util-btn').forEach(b => {
        const u = b.dataset.util;
        const left = utilUses[u];
        b.disabled = left <= 0;
        b.style.opacity = left <= 0 ? '0.38' : '1';
    });
    // check if current worm died and still playing -> next turn after projectile done
    if (gameState === 'playing' && aw && !aw.alive) {
        setTimeout(() => nextTurn(), 600);
    }
    // check all teams alive flag
    teams.forEach(t => t.alive = t.worms.some(w => w.alive));
    updateRoster();
}
function draw() {
    const dpr = window.devicePixelRatio || 1;
    const w = innerWidth, h = innerHeight;
    ctx.save();
    ctx.clearRect(0, 0, w, h);
    // background sky gradient
    const sky = ctx.createLinearGradient(0, 0, 0, h);
    sky.addColorStop(0, '#0f1b33');
    sky.addColorStop(0.35, '#1e3a5f');
    sky.addColorStop(0.7, '#2a5a8a');
    sky.addColorStop(1, '#7fb8e6');
    ctx.fillStyle = sky;
    ctx.fillRect(0, 0, w, h);
    // sun
    ctx.fillStyle = 'rgba(255,240,180,0.9)';
    ctx.beginPath();
    ctx.arc(w * 0.82, 110, 42, 0, Math.PI * 2);
    ctx.fill();
    ctx.fillStyle = 'rgba(255,240,180,0.18)';
    ctx.beginPath();
    ctx.arc(w * 0.82, 110, 78, 0, Math.PI * 2);
    ctx.fill();
    // clouds parallax
    ctx.fillStyle = 'rgba(255,255,255,0.55)';
    for (let i = 0; i < 4; i++) {
        const cx = ((Date.now() * 0.02 + i * 300) % (w + 200)) - 100 + i * 70;
        const cy = 70 + i * 34;
        const sc = 0.9 + i * 0.15;
        drawCloud(cx, cy, sc);
    }
    // camera transform
    ctx.save();
    // screen shake
    const sxShake = (Math.random() - 0.5) * shake * (w / 80), syShake = (Math.random() - 0.5) * shake * (w / 80);
    ctx.translate(w / 2 + sxShake, h / 2 + syShake);
    ctx.scale(camera.scale, camera.scale);
    ctx.translate(-camera.x, -camera.y);
    // terrain canvas
    const tCanvas = (0, terrain_1.getCanvas)();
    if (tCanvas)
        ctx.drawImage(tCanvas, 0, 0);
    // water
    const waterGrad = ctx.createLinearGradient(0, terrain_1.WATER_Y, 0, terrain_1.H);
    waterGrad.addColorStop(0, 'rgba(56,189,248,0.55)');
    waterGrad.addColorStop(1, 'rgba(14,116,144,0.85)');
    ctx.fillStyle = waterGrad;
    ctx.fillRect(0, terrain_1.WATER_Y, terrain_1.W, terrain_1.H - terrain_1.WATER_Y);
    // water waveline
    ctx.strokeStyle = 'rgba(255,255,255,0.45)';
    ctx.lineWidth = 2;
    ctx.beginPath();
    for (let x = 0; x <= terrain_1.W; x += 10) {
        const wx = Math.sin(x * 0.02 + Date.now() * 0.003) * 4 + Math.sin(x * 0.05 + Date.now() * 0.005) * 2;
        if (x === 0)
            ctx.moveTo(x, terrain_1.WATER_Y + wx);
        else
            ctx.lineTo(x, terrain_1.WATER_Y + wx);
    }
    ctx.stroke();
    // subtle water shimmer
    ctx.fillStyle = 'rgba(255,255,255,0.07)';
    for (let x = 0; x < terrain_1.W; x += 36) {
        const wx = Math.sin(x * 0.03 + Date.now() * 0.002) * 6;
        ctx.fillRect(x, terrain_1.WATER_Y + 4 + wx, 22, 2);
    }
    // worms
    teams.forEach(team => {
        team.worms.forEach(w => {
            if (!w.alive)
                return;
            drawWorm(w, team);
        });
    });
    // debris
    particles_1.debris.forEach(d => {
        ctx.save();
        ctx.translate(d.x, d.y);
        ctx.rotate(d.rot);
        ctx.fillStyle = '#4a3520';
        ctx.fillRect(-d.w / 2, -d.h / 2, d.w, d.h);
        ctx.strokeStyle = 'rgba(0,0,0,0.25)';
        ctx.lineWidth = 1;
        ctx.strokeRect(-d.w / 2, -d.h / 2, d.w, d.h);
        ctx.restore();
    });
    // projectiles
    projectiles.forEach(p => {
        ctx.save();
        ctx.translate(p.x, p.y);
        if (p.type === 'grenade') {
            ctx.fillStyle = '#2f3a18';
            ctx.beginPath();
            ctx.arc(0, 0, 7, 0, Math.PI * 2);
            ctx.fill();
            ctx.fillStyle = '#8ab44b';
            ctx.fillRect(-4, -2, 8, 4);
        }
        else if (p.type === 'holy') {
            ctx.fillStyle = '#ffd700';
            ctx.beginPath();
            ctx.arc(0, 0, 9, 0, Math.PI * 2);
            ctx.fill();
            ctx.fillStyle = '#fff';
            ctx.font = '10px sans-serif';
            ctx.textAlign = 'center';
            ctx.textBaseline = 'middle';
            ctx.fillText('✝', 0, 1);
        }
        else if (p.type === 'drill') {
            ctx.rotate(Math.atan2(p.vy, p.vx));
            ctx.fillStyle = '#9ca3af';
            ctx.fillRect(-10, -4, 20, 8);
            ctx.fillStyle = '#ef4444';
            ctx.beginPath();
            ctx.moveTo(10, 0);
            ctx.lineTo(4, -6);
            ctx.lineTo(4, 6);
            ctx.closePath();
            ctx.fill();
        }
        else if (p.type === 'shotgun') {
            ctx.fillStyle = '#facc15';
            ctx.beginPath();
            ctx.arc(0, 0, 3.5, 0, Math.PI * 2);
            ctx.fill();
        }
        else if (p.type === 'airstrike') {
            ctx.fillStyle = '#334155';
            ctx.beginPath();
            ctx.ellipse(0, 0, 8, 12, 0, 0, Math.PI * 2);
            ctx.fill();
            ctx.fillStyle = '#ef4444';
            ctx.fillRect(-3, 6, 6, 6);
        }
        else { // bazooka
            const ang = Math.atan2(p.vy, p.vx);
            ctx.rotate(ang);
            ctx.fillStyle = '#1f2937';
            ctx.fillRect(-10, -4, 18, 8);
            ctx.fillStyle = '#ef4444';
            ctx.beginPath();
            ctx.moveTo(8, 0);
            ctx.lineTo(2, -6);
            ctx.lineTo(2, 6);
            ctx.closePath();
            ctx.fill();
        }
        ctx.restore();
    });
    // rail line
    const rl = projectiles.railLine;
    if (rl) {
        ctx.strokeStyle = `rgba(125,211,252,${rl.life * 4})`;
        ctx.lineWidth = 4;
        ctx.beginPath();
        ctx.moveTo(rl.x1, rl.y1);
        ctx.lineTo(rl.x2, rl.y2);
        ctx.stroke();
        ctx.strokeStyle = 'rgba(255,255,255,0.9)';
        ctx.lineWidth = 1.5;
        ctx.beginPath();
        ctx.moveTo(rl.x1, rl.y1);
        ctx.lineTo(rl.x2, rl.y2);
        ctx.stroke();
        ctx.fillStyle = 'rgba(125,211,252,0.6)';
        ctx.beginPath();
        ctx.arc(rl.x2, rl.y2, 8, 0, Math.PI * 2);
        ctx.fill();
    }
    // grapple rope
    if (grapple && activeWorm()) {
        const aw = activeWorm();
        ctx.strokeStyle = '#cbd5e1';
        ctx.lineWidth = 2;
        ctx.setLineDash([6, 6]);
        ctx.beginPath();
        ctx.moveTo(aw.x, aw.y);
        ctx.lineTo(grapple.x, grapple.y);
        ctx.stroke();
        ctx.setLineDash([]);
        ctx.fillStyle = '#e2e8f0';
        ctx.beginPath();
        ctx.arc(grapple.x, grapple.y, 5, 0, Math.PI * 2);
        ctx.fill();
    }
    // trajectory prediction
    if (gameState === 'playing' && !teams[turnTeamIdx].isCPU && activeWorm() && !isJetpacking && !grapple) {
        const aw = activeWorm();
        const def = types_1.WEAPONS.find(w => w.id === selectedWeapon);
        if (def.cat !== 'Direct' || def.id === 'shotgun') {
            const previewPower = isCharging ? charge : 0.62;
            const speed = 4 + previewPower * 16;
            let px = aw.x + Math.cos(aimAngle) * 18, py = aw.y - 6 + Math.sin(aimAngle) * 10;
            let vx = Math.cos(aimAngle) * speed + wind * 0.06, vy = Math.sin(aimAngle) * speed;
            const pts = [];
            for (let i = 0; i < 70; i++) {
                px += vx;
                py += vy;
                vy += GRAVITY;
                vx += wind * WIND_FORCE;
                pts.push({ x: px, y: py });
                if ((0, terrain_1.isSolid)(px, py) || py > terrain_1.WATER_Y)
                    break;
            }
            // dotted line
            ctx.strokeStyle = isCharging ? 'rgba(250,204,21,0.95)' : 'rgba(255,255,255,0.55)';
            ctx.lineWidth = 2;
            ctx.setLineDash([4, 8]);
            ctx.beginPath();
            pts.forEach((p, i) => i === 0 ? ctx.moveTo(p.x, p.y) : ctx.lineTo(p.x, p.y));
            ctx.stroke();
            ctx.setLineDash([]);
            // impact marker
            const last = pts[pts.length - 1];
            if (last) {
                ctx.strokeStyle = 'rgba(239,68,68,0.9)';
                ctx.lineWidth = 2;
                ctx.beginPath();
                ctx.arc(last.x, last.y, 12, 0, Math.PI * 2);
                ctx.stroke();
                ctx.beginPath();
                ctx.moveTo(last.x - 8, last.y);
                ctx.lineTo(last.x + 8, last.y);
                ctx.moveTo(last.x, last.y - 8);
                ctx.lineTo(last.x, last.y + 8);
                ctx.stroke();
                const r = def.radius;
                ctx.fillStyle = 'rgba(239,68,68,0.12)';
                ctx.beginPath();
                ctx.arc(last.x, last.y, r, 0, Math.PI * 2);
                ctx.fill();
            }
            // aim arrow at worm
            ctx.strokeStyle = 'rgba(255,255,255,0.9)';
            ctx.lineWidth = 2;
            ctx.beginPath();
            ctx.moveTo(aw.x, aw.y - 6);
            ctx.lineTo(aw.x + Math.cos(aimAngle) * 34, aw.y - 6 + Math.sin(aimAngle) * 34);
            ctx.stroke();
            ctx.fillStyle = '#fff';
            ctx.beginPath();
            ctx.arc(aw.x + Math.cos(aimAngle) * 38, aw.y - 6 + Math.sin(aimAngle) * 38, 4, 0, Math.PI * 2);
            ctx.fill();
        }
        else if (def.id === 'railgun') {
            // laser preview
            const sx = aw.x + Math.cos(aimAngle) * 18, sy = aw.y - 6 + Math.sin(aimAngle) * 10;
            const ex = sx + Math.cos(aimAngle) * 2600, ey = sy + Math.sin(aimAngle) * 2600;
            const hit = (0, terrain_1.rayTerrain)(sx, sy, ex, ey);
            ctx.strokeStyle = 'rgba(250,204,21,0.35)';
            ctx.lineWidth = 8;
            ctx.beginPath();
            ctx.moveTo(sx, sy);
            ctx.lineTo(hit.hit ? hit.x : ex, hit.hit ? hit.y : ey);
            ctx.stroke();
            ctx.strokeStyle = 'rgba(255,255,255,0.85)';
            ctx.lineWidth = 1.5;
            ctx.setLineDash([6, 6]);
            ctx.beginPath();
            ctx.moveTo(sx, sy);
            ctx.lineTo(hit.hit ? hit.x : ex, hit.hit ? hit.y : ey);
            ctx.stroke();
            ctx.setLineDash([]);
            if (hit.hit) {
                ctx.fillStyle = 'rgba(239,68,68,0.9)';
                ctx.beginPath();
                ctx.arc(hit.x, hit.y, 6, 0, Math.PI * 2);
                ctx.fill();
            }
        }
    }
    // particles
    particles_1.particles.forEach(p => {
        ctx.globalAlpha = Math.max(0, p.life);
        if (p.type === 'smoke') {
            ctx.fillStyle = p.col;
            ctx.beginPath();
            ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2);
            ctx.fill();
        }
        else if (p.type === 'spark') {
            ctx.fillStyle = p.col;
            ctx.fillRect(p.x - 1.2, p.y - 1.2, 2.4, 2.4);
        }
        else if (p.type === 'water') {
            ctx.fillStyle = p.col;
            ctx.beginPath();
            ctx.arc(p.x, p.y, p.r * 0.7, 0, Math.PI * 2);
            ctx.fill();
        }
        else {
            ctx.fillStyle = p.col;
            ctx.beginPath();
            ctx.arc(p.x, p.y, p.r * 0.6, 0, Math.PI * 2);
            ctx.fill();
        }
    });
    ctx.globalAlpha = 1;
    ctx.restore();
    // vignette
    const vg = ctx.createRadialGradient(w / 2, h / 2, Math.min(w, h) * 0.6, w / 2, h / 2, Math.max(w, h) * 0.9);
    vg.addColorStop(0, 'transparent');
    vg.addColorStop(1, 'rgba(0,0,0,0.28)');
    ctx.fillStyle = vg;
    ctx.fillRect(0, 0, w, h);
    ctx.restore();
    // power bar is DOM
}
function drawCloud(x, y, s) {
    ctx.beginPath();
    ctx.ellipse(x, y, 28 * s, 16 * s, 0, 0, Math.PI * 2);
    ctx.ellipse(x + 18 * s, y + 4 * s, 22 * s, 12 * s, 0, 0, Math.PI * 2);
    ctx.ellipse(x - 16 * s, y + 6 * s, 20 * s, 10 * s, 0, 0, Math.PI * 2);
    ctx.fill();
}
function drawWorm(w, team) {
    const isActive = teams[turnTeamIdx] === team && teams[turnTeamIdx].worms[turnWormIdx] === w;
    ctx.save();
    ctx.translate(w.x, w.y);
    // shadow
    ctx.fillStyle = 'rgba(0,0,0,0.22)';
    ctx.beginPath();
    ctx.ellipse(0, 14, 14, 5, 0, 0, Math.PI * 2);
    ctx.fill();
    // body
    const bob = Math.sin(Date.now() * 0.004 + w.id) * 1.2;
    ctx.translate(0, bob);
    // outline
    ctx.fillStyle = 'rgba(0,0,0,0.35)';
    ctx.beginPath();
    ctx.ellipse(0.8, 1.2, 16, 12, 0, 0, Math.PI * 2);
    ctx.fill();
    ctx.fillStyle = '#f8fafc'; // worm body
    // body segments
    ctx.beginPath();
    ctx.ellipse(0, 0, 14, 10, 0, 0, Math.PI * 2);
    ctx.fill();
    ctx.fillStyle = '#e2e8f0';
    ctx.beginPath();
    ctx.ellipse(-3, 2, 10, 7, 0, 0, Math.PI * 2);
    ctx.fill();
    // belly
    ctx.fillStyle = '#fef3c7';
    ctx.beginPath();
    ctx.ellipse(0, 5, 8, 4, 0, 0, Math.PI * 2);
    ctx.fill();
    // head highlight
    ctx.fillStyle = team.color.col;
    ctx.beginPath();
    ctx.arc(6, -4, 4, 0, Math.PI * 2);
    ctx.fill();
    // eyes
    ctx.fillStyle = '#0f172a';
    ctx.beginPath();
    ctx.arc(5, -6, 2.2, 0, Math.PI * 2);
    ctx.arc(9, -5, 2.2, 0, Math.PI * 2);
    ctx.fill();
    ctx.fillStyle = '#fff';
    ctx.beginPath();
    ctx.arc(5.6, -6.6, 0.9, 0, Math.PI * 2);
    ctx.arc(9.6, -5.6, 0.9, 0, Math.PI * 2);
    ctx.fill();
    // bandana if active
    if (isActive) {
        ctx.fillStyle = team.color.col;
        ctx.fillRect(-12, -10, 22, 4);
        ctx.fillStyle = '#fff';
        ctx.font = '700 5px sans-serif';
        ctx.fillText('★', -9, -7);
    }
    // weapon in hand direction
    const ang = isActive ? aimAngle : 0;
    ctx.save();
    ctx.rotate(ang);
    ctx.fillStyle = '#334155';
    ctx.fillRect(10, -2, 14, 3);
    ctx.fillStyle = '#1e293b';
    ctx.fillRect(18, -3, 4, 5);
    ctx.restore();
    // HP bar above
    ctx.translate(0, -20);
    const hpPct = w.hp / 100;
    ctx.fillStyle = 'rgba(0,0,0,0.55)';
    ctx.fillRect(-18, -4, 36, 6);
    ctx.fillStyle = hpPct > 0.5 ? '#4ade80' : hpPct > 0.25 ? '#facc15' : '#ef4444';
    ctx.fillRect(-17, -3, 34 * hpPct, 4);
    ctx.fillStyle = '#fff';
    ctx.font = '700 7px Nunito,sans-serif';
    ctx.textAlign = 'center';
    ctx.fillText(String(w.hp), 0, 0);
    ctx.restore();
}
// start loop
last = performance.now();
requestAnimationFrame(loop);
// expose for probe
window.game = { teams, get activeWorm() { return activeWorm(); }, get wind() { return wind; }, projectiles, particles: particles_1.particles };
};

// ── module: src/types.ts ──
__mods["src/types.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.WEAPONS = exports.TEAM_COLORS = void 0;
exports.TEAM_COLORS = [
    { name: 'Crimson', col: '#ef4444', light: '#fca5a5', bg: '#7f1d1d' },
    { name: 'Ocean', col: '#38bdf8', light: '#bae6fd', bg: '#0c4a6e' },
    { name: 'Verdant', col: '#4ade80', light: '#bbf7d0', bg: '#14532d' },
    { name: 'Solar', col: '#facc15', light: '#fef08a', bg: '#713f12' },
    { name: 'Violet', col: '#a78bfa', light: '#ddd6fe', bg: '#4c1d95' },
    { name: 'Coral', col: '#fb7185', light: '#fecdd3', bg: '#881337' },
];
exports.WEAPONS = [
    { id: 'bazooka', name: 'Bazooka', icon: '🚀', cat: 'Indirect', ammo: '∞', damage: 38, radius: 78, desc: 'Classic parabolic rocket', projectile: 'bazooka' },
    { id: 'grenade', name: 'Grenade', icon: '🍍', cat: 'Indirect', ammo: 3, damage: 42, radius: 70, desc: 'Bouncy 3s fuse', projectile: 'grenade' },
    { id: 'shotgun', name: 'Shotgun', icon: '💥', cat: 'Direct', ammo: 2, damage: 14, radius: 18, desc: '5-pellet spread', projectile: 'shotgun', count: 5 },
    { id: 'railgun', name: 'Railgun', icon: '⚡', cat: 'Direct', ammo: 1, damage: 45, radius: 22, desc: 'Instant laser beam', projectile: 'rail' },
    { id: 'airstrike', name: 'Airstrike', icon: '✈️', cat: 'AoE', ammo: 1, damage: 32, radius: 62, desc: '3 missiles from the sky', projectile: 'airstrike', count: 3 },
    { id: 'holy', name: 'Holy Nade', icon: '🙏', cat: 'AoE', ammo: 1, damage: 75, radius: 150, desc: 'Divine devastation', projectile: 'holy' },
    { id: 'drill', name: 'Bunker Buster', icon: '🪛', cat: 'AoE', ammo: 2, damage: 30, radius: 44, desc: 'Drills then erupts', projectile: 'drill' },
];
};

// ── module: src/audio.ts ──
__mods["src/audio.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.SFX = void 0;
let ctx = null;
function ac() {
    if (!ctx)
        ctx = new (window.AudioContext || window.webkitAudioContext)();
    if (ctx.state === 'suspended')
        ctx.resume();
    return ctx;
}
function tone(f, dur, type = 'sine', gain = 0.3, slideTo) {
    try {
        const c = ac();
        const o = c.createOscillator();
        const g = c.createGain();
        o.type = type;
        o.frequency.value = f;
        o.connect(g);
        g.connect(c.destination);
        g.gain.value = gain;
        g.gain.exponentialRampToValueAtTime(0.001, c.currentTime + dur);
        if (slideTo !== undefined)
            o.frequency.exponentialRampToValueAtTime(Math.max(20, slideTo), c.currentTime + dur * 0.9);
        o.start();
        o.stop(c.currentTime + dur);
    }
    catch { }
}
function noise(dur, gain = 0.3, filt = 1200) {
    try {
        const c = ac();
        const buffer = c.createBuffer(1, c.sampleRate * dur, c.sampleRate);
        const d = buffer.getChannelData(0);
        for (let i = 0; i < d.length; i++)
            d[i] = (Math.random() * 2 - 1) * 0.6;
        const src = c.createBufferSource();
        src.buffer = buffer;
        const bf = c.createBiquadFilter();
        bf.type = 'lowpass';
        bf.frequency.value = filt;
        const g = c.createGain();
        g.gain.value = gain;
        g.gain.exponentialRampToValueAtTime(0.001, c.currentTime + dur);
        src.connect(bf);
        bf.connect(g);
        g.connect(c.destination);
        src.start();
    }
    catch { }
}
exports.SFX = {
    click() { tone(900, 0.08, 'square', 0.15, 1200); },
    hover() { tone(1200, 0.05, 'sine', 0.08); },
    jump() { tone(300, 0.18, 'square', 0.25, 600); tone(500, 0.12, 'sine', 0.15); },
    step() { tone(180 + Math.random() * 80, 0.07, 'square', 0.09); },
    charge() { tone(440, 0.08, 'sine', 0.12); },
    fire() { tone(120, 0.35, 'square', 0.4, 40); noise(0.25, 0.25, 800); },
    rail() { tone(1200, 0.12, 'sawtooth', 0.3, 200); tone(1800, 0.2, 'square', 0.2); },
    explosion(s) { tone(80, 0.5 * s, 'square', 0.5, 30); noise(0.6 * s, 0.45, 600); setTimeout(() => noise(0.4 * s, 0.22, 400), 90); },
    holy() { tone(523, 0.4, 'sine', 0.3, 659); setTimeout(() => tone(659, 0.4, 'sine', 0.3, 784), 180); setTimeout(() => tone(784, 0.6, 'sine', 0.3, 1046), 360); noise(1.2, 0.18, 900); },
    water() { noise(0.5, 0.15, 1400); tone(220, 0.4, 'sine', 0.12, 180); },
    hurt() { tone(220, 0.18, 'sawtooth', 0.28, 140); },
    tick(t) { const f = 700 + t * 40; tone(f, 0.07, 'square', 0.14); },
    panicTick() { tone(1100, 0.06, 'square', 0.22); },
    win() { [0, 200, 400].forEach(d => setTimeout(() => tone(400 + d, 0.25, 'square', 0.22, 600 + d), d)); },
    turn() { tone(600, 0.18, 'sine', 0.18, 900); },
    menuHit() { tone(800, 0.09, 'square', 0.18, 1000); }
};
};

// ── module: src/terrain.ts ──
__mods["src/terrain.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.WATER_Y = exports.H = exports.W = void 0;
exports.initTerrain = initTerrain;
exports.getCanvas = getCanvas;
exports.isSolid = isSolid;
exports.isSolidCircle = isSolidCircle;
exports.carveCircle = carveCircle;
exports.generate = generate;
exports.findSpawnPositions = findSpawnPositions;
exports.rayTerrain = rayTerrain;
exports.W = 2200;
exports.H = 740;
exports.WATER_Y = exports.H - 78;
let solids; // W*H 0/1
let canvas;
let g;
let debrisCb = null;
function initTerrain(cb) {
    debrisCb = cb;
    canvas = document.createElement('canvas');
    canvas.width = exports.W;
    canvas.height = exports.H;
    g = canvas.getContext('2d');
    solids = new Uint8Array(exports.W * exports.H);
}
function getCanvas() { return canvas; }
function isSolid(x, y) {
    x = Math.floor(x);
    y = Math.floor(y);
    if (x < 0 || x >= exports.W || y < 0 || y >= exports.H)
        return x < 0 || x >= exports.W ? true : false;
    return solids[y * exports.W + x] === 1;
}
function isSolidCircle(x, y, r) {
    for (let a = 0; a < 8; a++) {
        const ang = a * Math.PI / 4;
        if (isSolid(x + Math.cos(ang) * r, y + Math.sin(ang) * r))
            return true;
    }
    return isSolid(x, y);
}
function carveCircle(cx, y, rad) {
    const r = Math.ceil(rad);
    for (let dy = -r; dy <= r; dy++) {
        for (let dx = -r; dx <= r; dx++) {
            if (dx * dx + dy * dy > rad * rad)
                continue;
            const x = cx + dx, yy = y + dy;
            if (x < 0 || x >= exports.W || yy < 0 || yy >= exports.H)
                continue;
            solids[yy * exports.W + x] = 0;
        }
    }
    // punch canvas
    g.globalCompositeOperation = 'destination-out';
    g.beginPath();
    g.arc(cx, y, rad, 0, Math.PI * 2);
    g.fill();
    g.globalCompositeOperation = 'source-over';
    // debris chunks from edge
    if (debrisCb) {
        const n = Math.floor(rad / 10) + 2;
        for (let i = 0; i < n; i++) {
            const ang = Math.random() * Math.PI * 2;
            const rr = rad * 0.7 + Math.random() * rad * 0.4;
            const sx = cx + Math.cos(ang) * rr * 0.5;
            const sy = y + Math.sin(ang) * rr * 0.5;
            debrisCb(sx, sy, 8 + Math.random() * 14, 7 + Math.random() * 10);
        }
    }
}
function generate(type) {
    solids.fill(0);
    g.clearRect(0, 0, exports.W, exports.H);
    // sky gradient already drawn in main render; here draw terrain fill
    // base fill
    if (type === 'island') {
        const pts = [];
        for (let x = 0; x <= exports.W; x += 12) {
            const t = x / exports.W;
            const hill = Math.sin(t * Math.PI) * 280 + Math.sin(t * 6) * 18 + (Math.random() - 0.5) * 10;
            const base = exports.H - 90 - hill * 0.95;
            pts.push([x, base]);
        }
        drawPoly(pts, '#3a2a14');
        addTexture();
    }
    else if (type === 'twin') {
        const pts = [];
        for (let x = 0; x <= exports.W; x += 10) {
            const t = x / exports.W;
            const l = Math.sin(t * Math.PI * 2) * 0.5 + 0.5; // not needed
            const twin = Math.max(0, Math.sin((t * 2) * Math.PI)) * 180 + 60 * Math.sin(t * Math.PI * 6) * 0; // placeholder
            const h1 = Math.sin(Math.PI * (t * 2)) * (t < 0.5 ? 220 : 0);
            const h2 = Math.sin(Math.PI * ((t - 0.5) * 2)) * (t >= 0.5 ? 210 : 0);
            const h = Math.max(h1, h2) + Math.sin(t * 8) * 12 + Math.sin(t * 15) * 6 + Math.sin(t * Math.PI) * 20;
            const y = exports.H - 95 - Math.max(0, h);
            pts.push([x, y]);
        }
        // add valley
        for (let i = 0; i < pts.length; i++) {
            const t = pts[i][0] / exports.W;
            if (t > 0.42 && t < 0.58)
                pts[i][1] = exports.H - 90 - ((Math.abs(t - 0.5) * 2) * 20); // dip to near water
        }
        drawPoly(pts, '#3a2a14');
        addTexture();
    }
    else { // cave
        // fill full with terrain
        g.fillStyle = '#2b1b0e';
        g.fillRect(0, 0, exports.W, exports.H);
        for (let y = 0; y < exports.H; y++)
            for (let x = 0; x < exports.W; x++)
                solids[y * exports.W + x] = 1;
        // carve main cavern ellipse wavy
        g.globalCompositeOperation = 'destination-out';
        const cx = exports.W / 2, cy = exports.H / 2 + 40;
        g.beginPath();
        // wavy ellipse
        const rx = exports.W * 0.42, ry = 220;
        const steps = 72;
        for (let i = 0; i <= steps; i++) {
            const a = i / steps * Math.PI * 2;
            const wave = Math.sin(a * 6) * 18 + Math.sin(a * 13) * 8 + (Math.random() - 0.5) * 6;
            const x = cx + Math.cos(a) * (rx + wave);
            const y = cy + Math.sin(a) * (ry + wave * 0.6);
            if (i === 0)
                g.moveTo(x, y);
            else
                g.lineTo(x, y);
        }
        g.closePath();
        g.fill();
        // clear solids inside shape: we will clear by point-in-polygon approx: instead iterate and check ellipse dist with wave approximation (simplify clear oval)
        for (let y = 0; y < exports.H; y++)
            for (let x = 0; x < exports.W; x++) {
                const dx = (x - cx) / rx, dy = (y - cy) / ry;
                if (dx * dx + dy * dy < 0.88 + Math.sin(Math.atan2(dy, dx) * 6) * 0.04)
                    solids[y * exports.W + x] = 0;
            }
        // carve openings bottom for water
        g.globalCompositeOperation = 'source-over';
        // ensure floor platform
        // add pillars
        g.fillStyle = '#3d2816';
        g.globalCompositeOperation = 'source-over';
        // keep solids for pillars - add two pillars
        for (let p = 0; p < 2; p++) {
            const px = p === 0 ? exports.W * 0.32 : exports.W * 0.68;
            const pw = 26 + Math.random() * 18;
            for (let y = cy + ry * 0.2; y < exports.H - 70; y++) {
                for (let dx = -pw / 2; dx < pw / 2; dx++) {
                    const x = Math.floor(px + dx + Math.sin(y * 0.05) * 4);
                    if (x >= 0 && x < exports.W)
                        solids[Math.floor(y) * exports.W + x] = 1;
                }
                g.fillRect(px - pw / 2 + Math.sin(y * 0.05) * 4, y, pw, 1);
            }
        }
        g.globalCompositeOperation = 'source-over';
        addTextureCave();
    }
    // ensure bottom solid under water
    for (let y = exports.WATER_Y + 28; y < exports.H; y++)
        for (let x = 0; x < exports.W; x++) {
            solids[y * exports.W + x] = 1;
        }
    // bottom draw
    g.fillStyle = '#1a1208';
    g.globalCompositeOperation = 'source-over';
    // re-draw bottom strip for visual
    // we already have texture; just ensure dark base
}
function drawPoly(pts, col) {
    g.fillStyle = col;
    g.beginPath();
    g.moveTo(0, exports.H);
    pts.forEach(([x, y]) => g.lineTo(x, y));
    g.lineTo(exports.W, exports.H);
    g.closePath();
    g.fill();
    // solids
    // rasterize: for each x find top y and fill below
    for (let i = 0; i < pts.length - 1; i++) {
        const [x0, y0] = pts[i], [x1, y1] = pts[i + 1];
        const steps = Math.ceil(x1 - x0);
        for (let s = 0; s <= steps; s++) {
            const t = s / steps, x = Math.floor(x0 + t * (x1 - x0)), yTop = y0 + t * (y1 - y0);
            for (let y = Math.floor(yTop); y < exports.H; y++)
                if (x >= 0 && x < exports.W && y >= 0 && y < exports.H)
                    solids[y * exports.W + x] = 1;
        }
    }
}
function addTexture() {
    // sprinkle lighter spots + grass
    g.fillStyle = 'rgba(92,64,22,0.35)';
    for (let i = 0; i < 6000; i++) {
        const x = Math.random() * exports.W, y = Math.random() * exports.H;
        if (isSolid(x, y) && y < exports.WATER_Y)
            g.fillRect(x, y, 2, 2);
    }
    // grass top
    g.strokeStyle = '#5a8a2a';
    g.lineWidth = 2;
    for (let x = 0; x < exports.W; x += 7) {
        // find top
        let top = -1;
        for (let y = 0; y < exports.H; y++)
            if (isSolid(x, y)) {
                top = y;
                break;
            }
        if (top >= 0 && top < exports.WATER_Y) {
            g.beginPath();
            g.moveTo(x, top);
            g.lineTo(x + (Math.random() - 0.5) * 4, top - 6 - Math.random() * 6);
            g.stroke();
        }
    }
    // darker shadow under
    g.fillStyle = 'rgba(0,0,0,0.12)';
    for (let x = 0; x < exports.W; x++) {
        let top = -1;
        for (let y = 0; y < exports.H; y++)
            if (isSolid(x, y)) {
                top = y;
                break;
            }
        if (top >= 0)
            g.fillRect(x, top, 1, 10);
    }
}
function addTextureCave() {
    // subtle noise
    g.fillStyle = 'rgba(255,255,255,0.04)';
    for (let i = 0; i < 8000; i++) {
        const x = Math.random() * exports.W, y = Math.random() * exports.H;
        if (isSolid(x, y))
            g.fillRect(x, y, 1, 1);
    }
}
function findSpawnPositions(n) {
    const res = [];
    const tries = 500;
    for (let k = 0; k < tries && res.length < n; k++) {
        const x = 120 + Math.random() * (exports.W - 240);
        // find top solid
        let yTop = -1;
        for (let y = 0; y < exports.H; y++)
            if (isSolid(x, y)) {
                yTop = y;
                break;
            }
        if (yTop < 0 || yTop > exports.WATER_Y - 40)
            continue;
        // check flatness: sample either side
        const yL = (() => { for (let y = 0; y < exports.H; y++)
            if (isSolid(x - 16, y))
                return y; return 9999; })(), yR = (() => { for (let y = 0; y < exports.H; y++)
            if (isSolid(x + 16, y))
                return y; return 9999; })();
        if (Math.abs(yTop - yL) > 22 || Math.abs(yTop - yR) > 22)
            continue;
        // not too close to others
        if (res.some(p => Math.abs(p.x - x) < 90))
            continue;
        res.push({ x, y: yTop - 14 });
    }
    // fallback if not enough
    let attempts = 0;
    while (res.length < n && attempts < 200) {
        const x = 120 + Math.random() * (exports.W - 240);
        let yTop = -1;
        for (let y = 0; y < exports.H; y++)
            if (isSolid(x, y)) {
                yTop = y;
                break;
            }
        if (yTop >= 0)
            res.push({ x, y: yTop - 14 });
        attempts++;
    }
    return res;
}
function rayTerrain(x0, y0, x1, y1) {
    const dx = x1 - x0, dy = y1 - y0;
    const len = Math.hypot(dx, dy);
    const steps = Math.ceil(len / 2);
    for (let i = 0; i <= steps; i++) {
        const t = i / steps;
        const x = x0 + dx * t, y = y0 + dy * t;
        if (isSolid(x, y))
            return { x, y, hit: true };
    }
    return { x: x1, y: y1, hit: false };
}
};

// ── module: src/particles.ts ──
__mods["src/particles.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.debris = exports.particles = void 0;
exports.spawnExplosion = spawnExplosion;
exports.spawnDust = spawnDust;
exports.spawnMuzzle = spawnMuzzle;
exports.spawnSplash = spawnSplash;
exports.addDebris = addDebris;
exports.updateParticles = updateParticles;
const terrain_1 = require("./terrain");
exports.particles = [];
exports.debris = [];
function spawnExplosion(x, y, power, color = '#ff6b35') {
    const n = Math.floor(power * 0.9) + 18;
    for (let i = 0; i < n; i++) {
        const ang = Math.random() * Math.PI * 2, sp = (Math.random() * power * 0.45) + 2;
        exports.particles.push({ x, y, vx: Math.cos(ang) * sp, vy: Math.sin(ang) * sp - Math.random() * 2, life: 1, maxLife: 0.5 + Math.random() * 0.5, r: 2 + Math.random() * 4, col: i % 3 === 0 ? '#222' : i % 3 === 1 ? color : '#ffcc33', alpha: 1, type: Math.random() < 0.3 ? 'smoke' : 'spark' });
    }
    for (let i = 0; i < 12; i++) {
        exports.particles.push({ x, y, vx: (Math.random() - 0.5) * power * 0.3, vy: (Math.random() - 0.5) * power * 0.3 - 1, life: 1, maxLife: 0.7 + Math.random() * 0.6, r: 4 + Math.random() * 6, col: 'rgba(60,60,60,0.7)', alpha: 0.6, type: 'smoke' });
    }
    // debris specks
    for (let i = 0; i < 14; i++) {
        exports.particles.push({ x, y, vx: (Math.random() - 0.5) * power * 0.4, vy: (Math.random() - 0.5) * power * 0.4 - 2, life: 1, maxLife: 0.8 + Math.random() * 0.5, r: 2 + Math.random() * 3, col: '#5c3a1a', alpha: 1, type: 'dust' });
    }
}
function spawnDust(x, y, n = 6) {
    for (let i = 0; i < n; i++)
        exports.particles.push({ x, y, vx: (Math.random() - 0.5) * 3, vy: -Math.random() * 2.5, life: 1, maxLife: 0.4 + Math.random() * 0.3, r: 1 + Math.random() * 2, col: '#c2b280', alpha: 0.8, type: 'dust' });
}
function spawnMuzzle(x, y, ang) {
    for (let i = 0; i < 6; i++)
        exports.particles.push({ x, y, vx: Math.cos(ang) * (4 + Math.random() * 4) + (Math.random() - 0.5) * 2, vy: Math.sin(ang) * (4 + Math.random() * 4) + (Math.random() - 0.5) * 2, life: 1, maxLife: 0.18, r: 2 + Math.random() * 2, col: '#ffec8b', alpha: 1, type: 'spark' });
}
function spawnSplash(x, y) {
    for (let i = 0; i < 10; i++)
        exports.particles.push({ x, y, vx: (Math.random() - 0.5) * 4, vy: -Math.random() * 4 - 1, life: 1, maxLife: 0.5, r: 2 + Math.random() * 3, col: 'rgba(120,200,255,0.9)', alpha: 0.9, type: 'water' });
}
function addDebris(x, y, w, h) {
    exports.debris.push({ x, y, vx: (Math.random() - 0.5) * 3, vy: -Math.random() * 2, w, h, rot: Math.random() * Math.PI, vr: (Math.random() - 0.5) * 0.3, life: 1 });
}
function updateParticles(dt, gravity, wind) {
    for (let i = exports.particles.length - 1; i >= 0; i--) {
        const p = exports.particles[i];
        p.x += p.vx * dt * 60;
        p.y += p.vy * dt * 60;
        p.vy += gravity * 0.35 * dt * 60 * (p.type === 'smoke' ? 0.25 : 1);
        p.vx += wind * 0.02 * dt * 60;
        if (p.type === 'smoke') {
            p.vx *= 0.99;
            p.vy *= 0.99;
            p.r += 0.08;
        }
        else if (p.type === 'spark')
            p.vy += 0.15;
        p.life -= dt / p.maxLife;
        if (p.type === 'dust' && (0, terrain_1.isSolid)(p.x, p.y))
            p.life -= 0.08;
        if (p.life <= 0)
            exports.particles.splice(i, 1);
    }
    for (let i = exports.debris.length - 1; i >= 0; i--) {
        const d = exports.debris[i];
        d.vy += gravity * 0.55 * dt * 60;
        d.vx += wind * 0.03 * dt * 60;
        d.vx *= 0.995;
        d.x += d.vx;
        d.y += d.vy;
        d.rot += d.vr;
        // ground collision
        if ((0, terrain_1.isSolid)(d.x, d.y + d.h / 2)) {
            d.y -= 1;
            d.vy *= -0.15;
            d.vx *= 0.72;
            d.vr *= 0.7;
            if (Math.abs(d.vy) < 0.5)
                d.vy = 0;
            if (Math.abs(d.vx) < 0.15)
                d.vx = 0;
            d.life -= 0.012;
            if (Math.random() < 0.03)
                spawnDust(d.x, d.y, 1);
        }
        if (d.y > 740)
            exports.debris.splice(i, 1);
        if (d.life <= 0 && Math.abs(d.vy) < 0.2 && Math.abs(d.vx) < 0.2)
            exports.debris.splice(i, 1);
    }
}
};

// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>