← Worms results

Worms

Qwen 3.6 27B MTP GGUF · 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 113,602 bytes · SHA-256 982b93cce29d
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Worm Warfare — Artillery Battle</title>
<style>
/* ── css: layout ── */
* { margin: 0; padding: 0; box-sizing: border-box; }
body { overflow: hidden; background: #0a0a1a; font-family: 'Segoe UI', Tahoma, sans-serif; user-select: none; }
#app { position: relative; width: 100vw; height: 100vh; }
#game { display: block; width: 100%; height: 100%; }
#overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; }
#hud-top {
  position: absolute; top: 0; left: 0; right: 0; display: flex; justify-content: center; align-items: center;
  gap: 24px; padding: 12px 24px; background: linear-gradient(to bottom, rgba(0,0,0,0.75), transparent);
}
#wind-indicator { color: #aad; font-size: 20px; font-weight: bold; text-shadow: 0 0 12px rgba(100,200,255,0.5); }
#timer-container {
  position: relative; width: 150px; height: 24px;
  background: rgba(0,0,0,0.6); border-radius: 12px; overflow: hidden;
  border: 2px solid rgba(255,255,255,0.3);
}
#timer-bar {
  height: 100%; background: linear-gradient(90deg, #4f4, #6f6);
  width: 100%; transition: width 0.1s linear;
}
#timer-text {
  position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
  color: #fff; font-size: 13px; font-weight: bold; text-shadow: 0 0 4px #000;
}
#turn-count { color: #ffd700; font-size: 18px; font-weight: bold; text-shadow: 0 0 8px rgba(255,215,0,0.4); }
#hud-bottom {
  position: absolute; bottom: 0; left: 0; right: 0; display: flex; align-items: flex-end;
  padding: 12px 24px; gap: 20px; background: linear-gradient(to top, rgba(0,0,0,0.85), transparent);
  pointer-events: auto;
}
#unit-info { display: flex; align-items: center; gap: 12px; min-width: 200px; }
#unit-name { color: #fff; font-size: 18px; font-weight: bold; text-shadow: 0 0 8px rgba(255,255,255,0.3); }
#unit-health-bar { width: 130px; height: 16px; background: rgba(0,0,0,0.5); border-radius: 8px; overflow: hidden; border: 1px solid rgba(255,255,255,0.2); }
#unit-health-fill { height: 100%; background: linear-gradient(90deg, #2ecc71, #4f4); transition: width 0.3s ease; width: 100%; }
#unit-health-text { color: #fff; font-size: 16px; font-weight: bold; min-width: 30px; text-shadow: 0 0 4px rgba(0,0,0,0.5); }
#weapon-selector { display: flex; gap: 8px; flex-wrap: wrap; }
.weapon-btn {
  background: rgba(255,255,255,0.1); border: 2px solid rgba(255,255,255,0.3); border-radius: 8px;
  color: #fff; padding: 8px 14px; cursor: pointer; font-size: 14px; transition: all 0.2s;
  pointer-events: auto; white-space: nowrap;
}
.weapon-btn:hover { background: rgba(255,255,255,0.2); border-color: rgba(255,255,255,0.6); transform: scale(1.05); }
.weapon-btn.selected { background: rgba(100,200,255,0.3); border-color: #4af; box-shadow: 0 0 12px rgba(68,170,255,0.5); }
.weapon-btn .w-icon { font-size: 20px; margin-right: 4px; }
#item-selector { display: flex; gap: 8px; }
.item-btn {
  background: rgba(100,255,100,0.1); border: 2px solid rgba(100,255,100,0.3); border-radius: 8px;
  color: #afa; padding: 8px 14px; cursor: pointer; font-size: 14px; transition: all 0.2s;
  pointer-events: auto; white-space: nowrap;
}
.item-btn:hover { background: rgba(100,255,100,0.2); }
.item-btn.selected { background: rgba(100,255,100,0.3); border-color: #4f4; }
.item-btn.used { opacity: 0.3; pointer-events: none; }
#aim-info {
  position: absolute; bottom: 80px; left: 50%; transform: translateX(-50%);
  display: flex; gap: 24px; color: #fff; font-size: 16px;
  text-shadow: 0 0 8px rgba(0,0,0,0.8); background: rgba(0,0,0,0.4);
  padding: 6px 20px; border-radius: 8px;
}
#menu-screen {
  position: absolute; top: 0; left: 0; width: 100%; height: 100%;
  background: linear-gradient(135deg, #1a0a2e, #16213e, #0f3460);
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  z-index: 100; pointer-events: auto;
}
#menu-screen h1 { font-size: 60px; color: #ffd700; text-shadow: 0 0 30px rgba(255,215,0,0.5); margin-bottom: 30px; }
#menu-buttons { display: flex; flex-direction: column; align-items: center; gap: 16px; }
#btn-start {
  background: linear-gradient(135deg, #f44, #f80); color: #fff; border: none; padding: 18px 52px;
  font-size: 26px; font-weight: bold; border-radius: 12px; cursor: pointer; transition: all 0.3s;
  box-shadow: 0 4px 20px rgba(255,100,0,0.4);
}
#btn-start:hover { transform: scale(1.1); box-shadow: 0 6px 30px rgba(255,100,0,0.6); }
#team-select { display: flex; gap: 24px; color: #ccc; font-size: 18px; margin-top: 10px; }
#team-select label { display: flex; align-items: center; gap: 8px; }
#team-select select {
  background: rgba(255,255,255,0.1); color: #fff; border: 1px solid rgba(255,255,255,0.3);
  padding: 8px 14px; border-radius: 6px; font-size: 15px;
}
#team-select select option { background: #16213e; color: #fff; }
#gameover-screen {
  position: absolute; top: 0; left: 0; width: 100%; height: 100%;
  background: rgba(0,0,0,0.85); display: flex; flex-direction: column; align-items: center; justify-content: center;
  z-index: 100; pointer-events: auto;
}
#gameover-screen h1 { font-size: 52px; color: #ffd700; margin-bottom: 30px; text-shadow: 0 0 20px rgba(255,215,0,0.5); }
#btn-restart {
  background: linear-gradient(135deg, #4f4, #0a0); color: #fff; border: none; padding: 16px 44px;
  font-size: 22px; font-weight: bold; border-radius: 10px; cursor: pointer; transition: all 0.3s;
}
#btn-restart:hover { transform: scale(1.1); }
#pause-overlay {
  position: absolute; top: 0; left: 0; width: 100%; height: 100%;
  background: rgba(0,0,0,0.6); display: flex; align-items: center; justify-content: center;
  z-index: 90; pointer-events: auto; backdrop-filter: blur(4px);
}
#pause-content { text-align: center; color: #fff; background: rgba(0,0,0,0.5); padding: 40px 60px; border-radius: 16px; border: 2px solid rgba(255,255,255,0.2); }
#pause-content h2 { font-size: 40px; margin-bottom: 16px; }
#pause-content p { font-size: 22px; margin-bottom: 20px; }
#btn-resume {
  background: linear-gradient(135deg, #4488ff, #44aaff); color: #fff; border: none; padding: 14px 40px;
  font-size: 20px; font-weight: bold; border-radius: 10px; cursor: pointer; transition: all 0.3s;
}
#btn-resume:hover { transform: scale(1.08); }

/* Move meter bar */
#move-meter-container {
  position: absolute; bottom: 140px; left: 50%; transform: translateX(-50%);
  width: 200px; height: 8px; background: rgba(0,0,0,0.5); border-radius: 4px;
  border: 1px solid rgba(255,255,255,0.2);
}
#move-meter-fill {
  height: 100%; background: linear-gradient(90deg, #4af, #6cf); border-radius: 3px;
  width: 100%; transition: width 0.1s linear;
}
</style>
</head>
<body>
<div id="app">
  <canvas id="game"></canvas>
  <div id="overlay">
    <div id="hud-top">
      <div id="wind-indicator">🌬️ <span id="wind-val">0</span></div>
      <div id="timer-container">
        <div id="timer-bar"></div>
        <span id="timer-text">15</span>
      </div>
      <div id="turn-count">#1</div>
    </div>
    <div id="hud-bottom">
      <div id="unit-info">
        <div id="unit-name">Worm A</div>
        <div id="unit-health-bar"><div id="unit-health-fill"></div></div>
        <span id="unit-health-text">100</span>
      </div>
      <div id="weapon-selector"></div>
      <div id="item-selector"></div>
    </div>
    <div id="move-meter-container"><div id="move-meter-fill"></div></div><div id="aim-info">
      <span id="power-display">Power: 50</span>
      <span id="angle-display">Angle: 45°</span>
    </div>
  </div>
  <div id="menu-screen">
    <h1>🪱 WORM WARFARE</h1>
    <div id="menu-buttons">
      <button id="btn-start">Start Game</button>
      <div id="team-select">
        <label>Teams:
          <select id="num-teams">
            <option value="2">2 Teams</option>
            <option value="3">3 Teams</option>
            <option value="4">4 Teams</option>
          </select>
        </label>
        <label>Mode:
          <select id="game-mode">
            <option value="cpu">vs CPU</option>
            <option value="hotseat">Hot-Seat</option>
          </select>
        </label>
        <label>Terrain:
          <select id="terrain-shape">
            <option value="single">Single Island</option>
            <option value="double">Double Islands</option>
            <option value="cave">Cave</option>
          </select>
        </label>
      </div>
    </div>
  </div>
  <div id="gameover-screen" style="display:none">
    <h1 id="winner-text">🏆 Team Wins!</h1>
    <button id="btn-restart">Play Again</button>
  </div>
  <div id="pause-overlay" style="display:none">
    <div id="pause-content">
      <h2>⏸️ PAUSED</h2>
      <p id="next-player-info">Next: Worm B</p>
      <button id="btn-resume">Continue</button>
    </div>
  </div>
</div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./types":"src/types.ts","./game":"src/game.ts","./terrain":"src/terrain.ts","./renderer":"src/renderer.ts","./sound":"src/sound.ts","./ai":"src/ai.ts"},"src/terrain.ts":{"./types":"src/types.ts"},"src/game.ts":{"./types":"src/types.ts"},"src/renderer.ts":{"./types":"src/types.ts","./sound":"src/sound.ts","./game":"src/game.ts"},"src/ai.ts":{"./types":"src/types.ts","./game":"src/game.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 });
exports.start = start;
const types_1 = require("./types");
const terrain_1 = require("./terrain");
const game_1 = require("./game");
const renderer_1 = require("./renderer");
const sound_1 = require("./sound");
const ai_1 = require("./ai");
let gs = null;
let lastTime = 0;
let keys = { left: false, right: false, up: false, down: false, jump: false, usedJump: false };
let mouseDown = false;
let aiFireQueued = false;
let aiItemQueued = false;
let turnTickTimer = 0;
let isPlayerTurn = false;
let gameOverHandled = false;
let turnPauseTimer = 0;
let betweenTurns = false;
let windChangeTimer = 0;
const canvas = document.getElementById('game');
(0, renderer_1.setRenderTarget)(canvas);
// Expose for debugging
if (typeof window !== 'undefined') {
    Object.defineProperty(window, 'gameState', {
        get: () => gs,
        configurable: true,
    });
}
// --- Menu ---
const menuScreen = document.getElementById('menu-screen');
const btnStart = document.getElementById('btn-start');
const btnRestart = document.getElementById('btn-restart');
const gameOverScreen = document.getElementById('gameover-screen');
const winnerText = document.getElementById('winner-text');
const pauseOverlay = document.getElementById('pause-overlay');
const btnResume = document.getElementById('btn-resume');
const nextPlayerInfo = document.getElementById('next-player-info');
btnStart.addEventListener('click', () => {
    sound_1.sound.init();
    sound_1.sound.playClick();
    startGame();
});
btnRestart.addEventListener('click', () => {
    sound_1.sound.playClick();
    gameOverScreen.style.display = 'none';
    menuScreen.style.display = 'flex';
});
btnResume.addEventListener('click', () => {
    sound_1.sound.playClick();
    gs.phase = 'playing';
    pauseOverlay.style.display = 'none';
    betweenTurns = false;
});
function startGame() {
    menuScreen.style.display = 'none';
    gameOverScreen.style.display = 'none';
    pauseOverlay.style.display = 'none';
    const numTeams = parseInt(document.getElementById('num-teams').value);
    const mode = document.getElementById('game-mode').value;
    const terrainShape = document.getElementById('terrain-shape').value;
    gs = (0, game_1.createGameState)(numTeams, mode === 'cpu', terrainShape);
    const td = (0, terrain_1.generateTerrain)(terrainShape);
    gs.terrainCanvas = td.canvas;
    gs.terrainCtx = td.ctx;
    gs.waterCanvas = td.waterCanvas;
    gs.waterCtx = td.waterCtx;
    gs.hasWater = td.hasWater;
    gs.waterLevel = td.waterLevel;
    gs.terrainShape = td.shape;
    (0, game_1.setTerrainData)(td.heights, td.shape, td.canvas);
    (0, game_1.placeUnitsOnTerrain)(gs);
    (0, ai_1.resetAI)();
    aiFireQueued = false;
    aiItemQueued = false;
    gameOverHandled = false;
    betweenTurns = false;
    turnPauseTimer = 0;
    const unit = (0, game_1.getCurrentUnit)(gs);
    if (unit) {
        isPlayerTurn = !gs.teams[unit.teamId].isCPU;
    }
    gs.phase = 'playing';
    // If first turn is CPU, don't show pause
    if (!isPlayerTurn) {
        pauseOverlay.style.display = 'none';
    }
    else {
        // Human turn - show pause
        if (unit) {
            const team = gs.teams[unit.teamId];
            nextPlayerInfo.textContent = `${team.name} - ${unit.name}`;
            nextPlayerInfo.style.color = team.color;
        }
        pauseOverlay.style.display = 'flex';
        gs.phase = 'paused';
    }
    lastTime = performance.now();
    requestAnimationFrame(gameLoop);
}
// --- Input ---
document.addEventListener('keydown', (e) => {
    if (!gs || gs.phase === 'menu' || gs.phase === 'gameover')
        return;
    switch (e.key) {
        case 'ArrowLeft':
        case 'a':
        case 'A':
            keys.left = true;
            break;
        case 'ArrowRight':
        case 'd':
        case 'D':
            keys.right = true;
            break;
        case 'ArrowUp':
        case 'w':
        case 'W':
            keys.up = true;
            break;
        case 'ArrowDown':
        case 's':
        case 'S':
            keys.down = true;
            break;
        case ' ':
        case 'z':
        case 'x':
            e.preventDefault();
            if (!keys.jump) {
                sound_1.sound.playJump();
                keys.jump = true;
                keys.usedJump = false;
            }
            break;
        case 'p':
        case 'Escape':
            if (gs.phase === 'playing') {
                gs.phase = 'paused';
                const u = (0, game_1.getCurrentUnit)(gs);
                if (u) {
                    const t = gs.teams[u.teamId];
                    nextPlayerInfo.textContent = `${t.name} - ${u.name}`;
                    nextPlayerInfo.style.color = t.color;
                }
                pauseOverlay.style.display = 'flex';
            }
            else if (gs.phase === 'paused' && !betweenTurns) {
                gs.phase = 'playing';
                pauseOverlay.style.display = 'none';
            }
            break;
    }
});
document.addEventListener('keyup', (e) => {
    switch (e.key) {
        case 'ArrowLeft':
        case 'a':
        case 'A':
            keys.left = false;
            break;
        case 'ArrowRight':
        case 'd':
        case 'D':
            keys.right = false;
            break;
        case 'ArrowUp':
        case 'w':
        case 'W':
            keys.up = false;
            break;
        case 'ArrowDown':
        case 's':
        case 'S':
            keys.down = false;
            break;
        case ' ':
        case 'z':
        case 'x':
            keys.jump = false;
            keys.usedJump = false;
            break;
    }
});
canvas.addEventListener('mousemove', (e) => {
    if (!gs)
        return;
    const rect = canvas.getBoundingClientRect();
    const mx = e.clientX - rect.left;
    const my = e.clientY - rect.top;
    const cw = rect.width, ch = rect.height;
    const cam = gs.camera;
    gs.mouseX = (mx - cw / 2) / cam.zoom + cam.x;
    gs.mouseY = (my - ch / 2) / cam.zoom + cam.y;
});
canvas.addEventListener('mousedown', (e) => {
    if (!gs || gs.phase !== 'playing')
        return;
    const unit = (0, game_1.getCurrentUnit)(gs);
    if (!unit || !unit.alive || gs.teams[unit.teamId].isCPU)
        return;
    sound_1.sound.init();
    mouseDown = true;
    gs.aimMode = 'power';
});
canvas.addEventListener('mouseup', (e) => {
    if (!gs || gs.aimMode !== 'power')
        return;
    const unit = (0, game_1.getCurrentUnit)(gs);
    if (!unit || !unit.alive || gs.teams[unit.teamId].isCPU)
        return;
    (0, game_1.fireWeapon)(gs, gs.powerCharge, getAngle());
    sound_1.sound.playFire(gs.selectedWeapon);
    gs.aimMode = null;
    gs.powerCharge = 50;
    mouseDown = false;
});
canvas.addEventListener('contextmenu', (e) => e.preventDefault());
function getAngle() {
    if (!gs)
        return 45;
    const unit = (0, game_1.getCurrentUnit)(gs);
    if (!unit)
        return 45;
    const dx = gs.mouseX - unit.x;
    const dy = gs.mouseY - (unit.y - 15);
    let angle = Math.atan2(-dy, dx) * 180 / Math.PI;
    return Math.max(0, Math.min(180, angle));
}
// --- Game Loop ---
function gameLoop(timestamp) {
    if (!gs)
        return;
    const dt = Math.min((timestamp - lastTime) / 1000, 0.05);
    lastTime = timestamp;
    if (gs.phase === 'playing') {
        update(dt);
    }
    (0, renderer_1.render)(gs);
    requestAnimationFrame(gameLoop);
}
function update(dt) {
    if (!gs)
        return;
    // Freeze time for impact
    if (gs.freezeTime > 0) {
        gs.freezeTime -= dt;
        (0, renderer_1.render)(gs);
        return;
    }
    // Screen shake decay
    if (gs.shakeTime > 0) {
        gs.shakeTime -= dt;
        gs.shakeIntensity *= 0.92;
    }
    else {
        gs.shakeIntensity = 0;
    }
    // Between-turn pause
    if (betweenTurns) {
        turnPauseTimer -= dt;
        if (turnPauseTimer <= 0) {
            betweenTurns = false;
            pauseOverlay.style.display = 'none';
        }
        return;
    }
    const unit = (0, game_1.getCurrentUnit)(gs);
    if (!unit)
        return;
    // Reset input keys each frame for clean state
    // (keys are already set by events)
    // Update current unit physics
    (0, game_1.updateUnit)(unit, dt, gs, keys);
    // Update other units (for water effects, debris, etc)
    for (const u of gs.units) {
        if (u.id !== unit.id && u.alive) {
            // Still apply gravity/water to others
            if (gs.hasWater && u.y + 12 > gs.waterLevel) {
                u.inWater = true;
                u.drownTimer += dt;
                if (u.drownTimer > 5) {
                    // Water damage handled in updateUnit, but let's apply it here too
                    u.hp -= 2 * dt;
                    if (u.hp <= 0) {
                        u.hp = 0;
                        u.alive = false;
                        gs.floatingTexts.push({
                            x: u.x, y: u.y - 20, text: 'Drowned!',
                            life: 2, maxLife: 2, color: '#4af', size: 14, vy: -40,
                        });
                    }
                }
            }
            else {
                u.inWater = false;
                u.drownTimer = Math.max(0, u.drownTimer - dt * 2);
            }
        }
    }
    // Turn timer
    gs.turnTimer -= dt;
    gs.moveMeter = Math.min(gs.moveMeterMax, gs.moveMeter + dt * 5);
    // Power charging
    if (gs.aimMode === 'power' && mouseDown) {
        gs.powerCharge += dt * 80;
        if (gs.powerCharge > 100)
            gs.powerCharge = 100;
    }
    // Urgent turn tick sound
    if (gs.turnTimer < 5 && gs.turnTimer > 0.5) {
        turnTickTimer -= dt;
        if (turnTickTimer <= 0) {
            const urgency = 1 - gs.turnTimer / 5;
            sound_1.sound.playTurnTick(urgency);
            turnTickTimer = Math.max(0.3, 1 - urgency * 0.7);
        }
    }
    // Final second beeps
    if (gs.turnTimer < 1 && gs.turnTimer > 0) {
        turnTickTimer -= dt;
        if (turnTickTimer <= 0) {
            sound_1.sound.playTurnTick(0.9);
            turnTickTimer = 0.15;
        }
    }
    // Time's up
    if (gs.turnTimer <= 0) {
        sound_1.sound.playTurnEnd();
        onTurnEnd();
        return;
    }
    // AI processing
    if (!isPlayerTurn && gs.phase === 'playing') {
        processAI(dt);
    }
    // Update projectiles
    (0, game_1.updateProjectiles)(dt, gs);
    // Update particles
    (0, game_1.updateParticles)(dt, gs);
    // Update debris
    (0, game_1.updateDebris)(dt, gs);
    // Update floating texts
    (0, game_1.updateFloatingTexts)(dt, gs);
    // Wind changes slowly
    windChangeTimer += dt;
    if (windChangeTimer > 30) {
        windChangeTimer = 0;
        gs.wind += (Math.random() - 0.5) * 30;
        gs.wind = Math.max(-200, Math.min(200, gs.wind));
    }
    // Check game over
    if (!gameOverHandled) {
        const winner = (0, game_1.checkGameOver)(gs);
        if (winner) {
            gameOverHandled = true;
            gs.phase = 'gameover';
            winnerText.textContent = `🏆 ${winner} Wins!`;
            gs.freezeTime = 2;
        }
    }
}
function onTurnEnd() {
    if (!gs)
        return;
    (0, game_1.advanceTurn)(gs);
    aiFireQueued = false;
    aiItemQueued = false;
    (0, ai_1.resetAI)();
    const newUnit = (0, game_1.getCurrentUnit)(gs);
    if (!newUnit)
        return;
    isPlayerTurn = !gs.teams[newUnit.teamId].isCPU;
    // If CPU turn, auto-continue after brief pause
    if (!isPlayerTurn) {
        betweenTurns = true;
        turnPauseTimer = 0.5;
        gs.phase = 'playing';
    }
    else {
        // Human turn - show pause
        const team = gs.teams[newUnit.teamId];
        nextPlayerInfo.textContent = `${team.name} - ${newUnit.name}`;
        nextPlayerInfo.style.color = team.color;
        pauseOverlay.style.display = 'flex';
        gs.phase = 'paused';
        betweenTurns = false;
    }
}
function processAI(dt) {
    if (!gs)
        return;
    const unit = (0, game_1.getCurrentUnit)(gs);
    if (!unit || !unit.alive)
        return;
    const aiAction = (0, ai_1.updateAI)(dt, gs);
    if (!aiAction)
        return;
    switch (aiAction.action) {
        case 'move':
            if (aiAction.moveDir !== undefined && aiAction.moveDir !== 0) {
                keys.left = aiAction.moveDir === -1;
                keys.right = aiAction.moveDir === 1;
                if (gs.moveMeter > 0) {
                    gs.moveMeter -= dt * 10;
                }
            }
            else {
                keys.left = false;
                keys.right = false;
            }
            break;
        case 'fire':
            if (!aiFireQueued && aiAction.fireAngle !== undefined && aiAction.firePower !== undefined) {
                // Set fake mouse pos for aim line
                gs.mouseX = unit.x + Math.cos(-aiAction.fireAngle * Math.PI / 180) * 300;
                gs.mouseY = unit.y - 15 + Math.sin(-aiAction.fireAngle * Math.PI / 180) * 300;
                (0, game_1.fireWeapon)(gs, aiAction.firePower, aiAction.fireAngle);
                sound_1.sound.playFire(gs.selectedWeapon);
                aiFireQueued = true;
                gs.aimMode = null;
            }
            break;
        case 'useItem':
            if (!aiItemQueued && aiAction.itemId) {
                if (!gs.usedItems.includes(aiAction.itemId)) {
                    applyAIItem(gs, aiAction.itemId);
                    gs.usedItems.push(aiAction.itemId);
                }
                aiItemQueued = true;
            }
            break;
        case 'steerMissile':
            if (aiAction.mouseX !== undefined && aiAction.mouseY !== undefined) {
                gs.mouseX = aiAction.mouseX;
                gs.mouseY = aiAction.mouseY;
            }
            break;
    }
}
function applyAIItem(gs, itemId) {
    const unit = (0, game_1.getCurrentUnit)(gs);
    if (!unit)
        return;
    switch (itemId) {
        case 'parachute':
            unit.itemActive = 'parachute';
            unit.maxJumps = 99;
            unit.itemTimer = 999;
            break;
        case 'jetpack':
            unit.itemActive = 'jetpack';
            unit.itemTimer = 3;
            break;
        case 'shield':
            unit.invincible = true;
            unit.invincibleTimer = 5;
            break;
        case 'speed':
            unit.itemActive = 'speed';
            unit.itemTimer = gs.turnTimer;
            break;
        case 'grappling':
            unit.itemActive = 'grappling';
            unit.itemTimer = 10;
            unit.maxJumps = 3;
            break;
        case 'extraTime':
            gs.turnTimer = Math.min(gs.turnTimer + 10, types_1.TURN_TIME);
            break;
    }
}
// --- Menu Animation ---
function drawMenuBg(context, w, h, time) {
    const grad = context.createLinearGradient(0, 0, w, h);
    grad.addColorStop(0, '#1a0a2e');
    grad.addColorStop(0.5, '#16213e');
    grad.addColorStop(1, '#0f3460');
    context.fillStyle = grad;
    context.fillRect(0, 0, w, h);
    context.fillStyle = 'rgba(255, 255, 255, 0.4)';
    for (let i = 0; i < 80; i++) {
        const sx = (i * 137.508 + Math.sin(time * 0.2 + i * 0.5) * 30) % w;
        const sy = (i * 73.123 + Math.cos(time * 0.15 + i * 0.3) * 20) % h;
        const size = 1 + Math.sin(time * 2 + i * 1.7) * 0.8;
        context.beginPath();
        context.arc(sx, sy, size, 0, Math.PI * 2);
        context.fill();
    }
    // Terrain silhouette
    context.fillStyle = 'rgba(40, 80, 60, 0.15)';
    context.beginPath();
    context.moveTo(0, h);
    for (let x = 0; x <= w; x += 8) {
        const y = h * 0.72 + Math.sin(x * 0.008 + time * 0.2) * 40 + Math.sin(x * 0.025) * 15;
        context.lineTo(x, y);
    }
    context.lineTo(w, h);
    context.closePath();
    context.fill();
}
// Menu animation loop (runs before game starts)
let menuAnimId = 0;
function start() {
    menuScreen.style.display = 'flex';
    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight;
    function menuAnim(ts) {
        if (gs && gs.phase !== 'menu')
            return;
        const ctx = canvas.getContext('2d');
        if (ctx)
            drawMenuBg(ctx, canvas.width, canvas.height, ts * 0.001);
        menuAnimId = requestAnimationFrame(menuAnim);
    }
    menuAnim(0);
}
};

// ── module: src/types.ts ──
__mods["src/types.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.WORM_NAMES = exports.TEAM_NAMES = exports.TEAM_COLORS = exports.ITEMS = exports.WEAPONS = exports.MOVE_SPEED = exports.JUMP_FORCE = exports.MAX_FALL_VEL = exports.WATER_DAMAGE = exports.FALL_DAMAGE_PER_PX = exports.FALL_DAMAGE_THRESHOLD = exports.TURN_TIME = exports.UNIT_MAX_HP = exports.UNIT_RADIUS = exports.GRAVITY = exports.WORLD_H = exports.WORLD_W = void 0;
exports.WORLD_W = 2000;
exports.WORLD_H = 800;
exports.GRAVITY = 980;
exports.UNIT_RADIUS = 12;
exports.UNIT_MAX_HP = 100;
exports.TURN_TIME = 15; // seconds
exports.FALL_DAMAGE_THRESHOLD = 300;
exports.FALL_DAMAGE_PER_PX = 0.15;
exports.WATER_DAMAGE = 2;
exports.MAX_FALL_VEL = 800;
exports.JUMP_FORCE = 380;
exports.MOVE_SPEED = 120;
exports.WEAPONS = [
    { id: 'grenade', name: 'Grenade', icon: '💣', category: 'indirect', desc: 'Standard explosive projectile' },
    { id: 'missile', name: 'Missile', icon: '🚀', category: 'indirect', desc: 'Guided explosive (click to steer)' },
    { id: 'shotgun', name: 'Shotgun', icon: '🔫', category: 'direct', desc: 'Instant-hit spread blast' },
    { id: 'cannon', name: 'Cannon', icon: '💥', category: 'indirect', desc: 'High damage, large explosion' },
    { id: 'cluster', name: 'Cluster', icon: '🎆', category: 'aoe', desc: 'Splits into sub-bombs' },
    { id: 'nuke', name: 'Nuke', icon: '☢️', category: 'aoe', desc: 'Massive area destruction' },
    { id: 'harpoon', name: 'Harpoon', icon: '🎯', category: 'direct', desc: 'Pulls target toward you' },
    { id: 'flamethrower', name: 'Flame', icon: '🔥', category: 'direct', desc: 'Close range fire stream' },
];
exports.ITEMS = [
    { id: 'parachute', name: 'Parachute', icon: '🪂', desc: 'Safe fall, extra jump' },
    { id: 'grappling', name: 'Grappler', icon: '🪝', desc: 'Swing and move fast' },
    { id: 'jetpack', name: 'Jetpack', icon: '✈️', desc: 'Fly for 3 seconds' },
    { id: 'shield', name: 'Shield', icon: '🛡️', desc: 'Invincibility for 5s' },
    { id: 'speed', name: 'Speed Boots', icon: '⚡', desc: '2x movement for turn' },
    { id: 'extraTime', name: 'Extra Time', icon: '⏰', desc: '+10 seconds' },
];
exports.TEAM_COLORS = ['#e74c3c', '#3498db', '#2ecc71', '#f39c12'];
exports.TEAM_NAMES = ['Red Fangs', 'Blue Sharks', 'Green Goblins', 'Orange Devils'];
exports.WORM_NAMES = ['Boris', 'Cuddles', 'Sir Flaps', 'Wormy', 'Daisy', 'Twitch', 'Fluffy', 'Nuts', 'Bertie', 'Gizmo'];
};

// ── module: src/terrain.ts ──
__mods["src/terrain.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.generateTerrain = generateTerrain;
exports.isTerrainAt = isTerrainAt;
exports.getSurfaceY = getSurfaceY;
exports.destroyTerrainCircle = destroyTerrainCircle;
exports.spawnDebrisFromExplosion = spawnDebrisFromExplosion;
const types_1 = require("./types");
// --- Simplex-like noise ---
function hash2D(x, y) {
    let n = Math.sin(x * 127.1 + y * 311.7) * 43758.5453;
    return n - Math.floor(n);
}
function smoothNoise(x, y = 0) {
    const ix = Math.floor(x), iy = Math.floor(y);
    const fx = x - ix, fy = y - iy;
    const sx = fx * fx * (3 - 2 * fx), sy = fy * fy * (3 - 2 * fy);
    return lerp(lerp(hash2D(ix, iy), hash2D(ix + 1, iy), sx), lerp(hash2D(ix, iy + 1), hash2D(ix + 1, iy + 1), sx), sy);
}
function octaveNoise(x, y, octaves) {
    let v = 0, a = 1, f = 1, m = 0;
    for (let i = 0; i < octaves; i++) {
        v += smoothNoise(x * f, y * f) * a;
        m += a;
        a *= 0.5;
        f *= 2;
    }
    return v / m;
}
function lerp(a, b, t) { return a + (b - a) * t; }
function generateTerrain(shape) {
    const canvas = new OffscreenCanvas(types_1.WORLD_W, types_1.WORLD_H);
    const ctx = canvas.getContext('2d', { willReadFrequently: true });
    const heights = new Float32Array(types_1.WORLD_W);
    // Generate heights
    switch (shape) {
        case 'single': {
            for (let x = 0; x < types_1.WORLD_W; x++) {
                const n1 = octaveNoise(x * 0.003, 0, 4) * 180;
                const n2 = octaveNoise(x * 0.01, 100, 3) * 80;
                const n3 = octaveNoise(x * 0.03, 200, 2) * 30;
                heights[x] = types_1.WORLD_H * 0.4 + n1 + n2 + n3;
                heights[x] = Math.max(HEIGHTS_MIN, Math.min(types_1.WORLD_H - 50, heights[x]));
            }
            break;
        }
        case 'double': {
            for (let x = 0; x < types_1.WORLD_W; x++) {
                const li = Math.exp(-Math.pow((x - 500) / 250, 2));
                const ri = Math.exp(-Math.pow((x - 1500) / 250, 2));
                const island = li + ri;
                const n = octaveNoise(x * 0.008, 0, 3) * 120 + octaveNoise(x * 0.03, 50, 2) * 30;
                heights[x] = types_1.WORLD_H * 0.35 + n * Math.max(0.15, island) + (1 - island) * 60;
                heights[x] = Math.max(HEIGHTS_MIN, Math.min(types_1.WORLD_H - 30, heights[x]));
            }
            break;
        }
        case 'cave': {
            for (let x = 0; x < types_1.WORLD_W; x++) {
                const cn = octaveNoise(x * 0.006, 0, 3) * 70;
                heights[x] = 100 + cn; // ceiling
            }
            break;
        }
    }
    // Render terrain
    const imgData = ctx.createImageData(types_1.WORLD_W, types_1.WORLD_H);
    const d = imgData.data;
    for (let x = 0; x < types_1.WORLD_W; x++) {
        for (let y = 0; y < types_1.WORLD_H; y++) {
            const i = (y * types_1.WORLD_W + x) * 4;
            let isTerrain = false;
            if (shape === 'cave') {
                const floorBase = types_1.WORLD_H * 0.8;
                const floorNoise = octaveNoise(x * 0.008, 200, 3) * 60;
                const floorH = floorBase + floorNoise;
                const ceilH = heights[x];
                isTerrain = y < ceilH || y > floorH;
                if (!isTerrain) {
                    // Cave interior
                    d[i] = 18;
                    d[i + 1] = 18;
                    d[i + 2] = 30;
                    d[i + 3] = 255;
                }
                else {
                    // Rock
                    const n = octaveNoise(x * 0.1, y * 0.1, 2) * 25;
                    d[i] = 55 + n;
                    d[i + 1] = 48 + n * 0.8;
                    d[i + 2] = 38 + n * 0.6;
                    d[i + 3] = 255;
                }
            }
            else {
                isTerrain = y >= heights[x];
                if (!isTerrain) {
                    // Sky gradient with subtle variation
                    const t = y / types_1.WORLD_H;
                    const noiseVal = octaveNoise(x * 0.002, y * 0.002, 2) * 5;
                    d[i] = 10 + t * 20 + noiseVal;
                    d[i + 1] = 14 + t * 30 + noiseVal;
                    d[i + 2] = 35 + t * 60 + noiseVal * 0.5;
                    d[i + 3] = 255;
                    // Stars
                    if (t < 0.4 && hash2D(x * 0.5, y * 0.5) > 0.98) {
                        d[i] = 180 + Math.random() * 75;
                        d[i + 1] = 180 + Math.random() * 75;
                        d[i + 2] = 200 + Math.random() * 55;
                    }
                    // Clouds
                    const cloud = smoothNoise(x * 0.003 + 50, y * 0.005 + 50);
                    if (t < 0.35 && cloud > 0.55) {
                        const cloudAlpha = (cloud - 0.55) * 4;
                        d[i] = d[i] + (200 - d[i]) * cloudAlpha * 0.3;
                        d[i + 1] = d[i + 1] + (210 - d[i + 1]) * cloudAlpha * 0.3;
                        d[i + 2] = d[i + 2] + (230 - d[i + 2]) * cloudAlpha * 0.3;
                    }
                }
                else {
                    const depth = y - heights[x];
                    const n = octaveNoise(x * 0.08, y * 0.08, 2) * 20;
                    if (depth < 4) {
                        d[i] = 34 + n * 2;
                        d[i + 1] = 140 + n * 2;
                        d[i + 2] = 34 + n;
                    }
                    else if (depth < 18) {
                        const t = (depth - 4) / 14;
                        d[i] = lerp(80, 110, t) + n;
                        d[i + 1] = lerp(70, 65, t) + n * 0.7;
                        d[i + 2] = lerp(35, 30, t) + n * 0.5;
                    }
                    else {
                        d[i] = 70 + n * 2;
                        d[i + 1] = 55 + n;
                        d[i + 2] = 35 + n * 0.5;
                    }
                    d[i + 3] = 255;
                }
            }
        }
    }
    ctx.putImageData(imgData, 0, 0);
    // Water
    let hasWater = false, waterLevel = 0;
    const waterCanvas = new OffscreenCanvas(types_1.WORLD_W, types_1.WORLD_H);
    const waterCtx = waterCanvas.getContext('2d');
    if (shape === 'double') {
        hasWater = true;
        waterLevel = types_1.WORLD_H * 0.58;
        waterCtx.fillStyle = 'rgba(20, 60, 140, 0.65)';
        waterCtx.fillRect(0, waterLevel, types_1.WORLD_W, types_1.WORLD_H - waterLevel);
        for (let x = 0; x < types_1.WORLD_W; x++) {
            const wave = Math.sin(x * 0.04) * 2;
            waterCtx.fillStyle = `rgba(50, 120, 200, 0.25)`;
            waterCtx.fillRect(x, waterLevel + wave, 1, 6);
        }
    }
    return { canvas, ctx, waterCanvas, waterCtx, heights, hasWater, waterLevel, shape };
}
const HEIGHTS_MIN = 100;
// --- Terrain API ---
function isTerrainAt(td, x, y) {
    const ix = Math.round(x), iy = Math.round(y);
    if (ix < 0 || ix >= types_1.WORLD_W || iy < 0 || iy >= types_1.WORLD_H)
        return true;
    const px = td.ctx.getImageData(ix, iy, 1, 1).data;
    if (td.shape === 'cave') {
        // Cave: terrain is dark rock (R>40), sky is very dark
        return px[0] > 40 && px[1] > 35;
    }
    // Surface: sky is dark (R<35), terrain is brighter
    return px[0] > 35 || px[1] > 30;
}
function getSurfaceY(td, x) {
    const ix = Math.round(x);
    if (ix < 0 || ix >= types_1.WORLD_W)
        return types_1.WORLD_H;
    if (td.shape === 'cave') {
        // Scan from bottom for floor
        for (let y = types_1.WORLD_H - 1; y >= 0; y--) {
            if (isTerrainAt(td, ix, y)) {
                // Go up to find top of floor
                for (let sy = y; sy >= 0; sy--) {
                    if (!isTerrainAt(td, ix, sy))
                        return sy + 1;
                }
                return 0;
            }
        }
        return types_1.WORLD_H;
    }
    return td.heights[ix];
}
function destroyTerrainCircle(td, cx, cy, radius) {
    // Destroy terrain in a circle
    const r = Math.round(radius);
    const x0 = Math.max(0, Math.round(cx - r));
    const y0 = Math.max(0, Math.round(cy - r));
    const x1 = Math.min(types_1.WORLD_W, Math.round(cx + r) + 1);
    const y1 = Math.min(types_1.WORLD_H, Math.round(cy + r) + 1);
    let destroyed = false;
    // Collect debris pixel regions
    const debrisRegions = [];
    for (let x = x0; x < x1; x++) {
        for (let y = y0; y < y1; y++) {
            const dx = x - cx, dy = y - cy;
            if (dx * dx + dy * dy < r * r) {
                if (isTerrainAt(td, x, y)) {
                    td.ctx.clearRect(x, y, 1, 1);
                    // Make sky color
                    const t = y / types_1.WORLD_H;
                    if (td.shape === 'cave') {
                        td.ctx.fillStyle = `rgb(${18 | 0},${18 | 0},${30 | 0})`;
                    }
                    else {
                        td.ctx.fillStyle = `rgb(${12 + t * 25 | 0},${16 + t * 35 | 0},${40 + t * 65 | 0})`;
                    }
                    td.ctx.fillRect(x, y, 1, 1);
                    destroyed = true;
                }
            }
        }
    }
    // Update height map after destruction
    if (destroyed) {
        updateHeightsAfterDestruction(td);
    }
    return [destroyed];
}
function updateHeightsAfterDestruction(td) {
    if (td.shape === 'cave')
        return;
    for (let x = 0; x < types_1.WORLD_W; x++) {
        // Scan from top to find new surface
        for (let y = 0; y < types_1.WORLD_H; y++) {
            const px = td.ctx.getImageData(x, y, 1, 1).data;
            if (px[0] > 35 || px[1] > 30) {
                td.heights[x] = y;
                break;
            }
        }
    }
}
// Create debris chunks from destroyed terrain
function spawnDebrisFromExplosion(td, cx, cy, radius) {
    const debris = [];
    const r = Math.round(radius * 1.2);
    // Spawn debris in ring around explosion
    for (let a = 0; a < Math.PI * 2; a += 0.3) {
        const dist = radius * (0.7 + Math.random() * 0.5);
        const dx = cx + Math.cos(a) * dist;
        const dy = cy + Math.sin(a) * dist;
        const ix = Math.round(dx), iy = Math.round(dy);
        if (ix >= 0 && ix < types_1.WORLD_W && iy >= 0 && iy < types_1.WORLD_H) {
            if (isTerrainAt(td, ix, iy)) {
                const size = 3 + Math.random() * 6;
                const n = octaveNoise(ix * 0.1, iy * 0.1, 1);
                const shade = n * 40;
                debris.push({
                    x: dx, y: dy,
                    vx: Math.cos(a) * (30 + Math.random() * 60),
                    vy: Math.sin(a) * (30 + Math.random() * 60) - 40,
                    w: size, h: size,
                    color: `rgb(${90 + shade | 0},${75 + shade | 0},${50 + shade * 0.5 | 0})`
                });
            }
        }
    }
    return debris;
}
};

// ── module: src/game.ts ──
__mods["src/game.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.createGameState = createGameState;
exports.placeUnitsOnTerrain = placeUnitsOnTerrain;
exports.setTerrainData = setTerrainData;
exports.rebuildTerrainCache = rebuildTerrainCache;
exports.getTerrainSurfaceY = getTerrainSurfaceY;
exports.getCurrentUnit = getCurrentUnit;
exports.updateUnit = updateUnit;
exports.isUnitInTerrain = isUnitInTerrain;
exports.takeDamage = takeDamage;
exports.fireWeapon = fireWeapon;
exports.updateProjectiles = updateProjectiles;
exports.createExplosion = createExplosion;
exports.updateParticles = updateParticles;
exports.updateDebris = updateDebris;
exports.updateFloatingTexts = updateFloatingTexts;
exports.advanceTurn = advanceTurn;
exports.checkGameOver = checkGameOver;
exports.handleInput = handleInput;
const types_1 = require("./types");
let idCounter = 0;
function uid() { return `id_${idCounter++}`; }
// --- Game Init ---
function createGameState(numTeams, isCPU, terrainShape) {
    const teams = [];
    for (let i = 0; i < numTeams; i++) {
        const wormColors = types_1.TEAM_COLORS[i] === '#e74c3c' ? ['#c0392b', '#e74c3c', '#ec7063', '#f1948a'] :
            types_1.TEAM_COLORS[i] === '#3498db' ? ['#2471a3', '#3498db', '#5dade2', '#85c1e9'] :
                types_1.TEAM_COLORS[i] === '#2ecc71' ? ['#1e8449', '#2ecc71', '#58d68d', '#82e0aa'] :
                    ['#b9770e', '#f39c12', '#f5b041', '#f8c471'];
        teams.push({
            name: types_1.TEAM_NAMES[i],
            color: types_1.TEAM_COLORS[i],
            wormColors,
            isCPU: isCPU,
        });
    }
    const units = [];
    let nameIdx = 0;
    for (let t = 0; t < numTeams; t++) {
        for (let u = 0; u < 4; u++) {
            units.push({
                id: uid(),
                teamId: t,
                name: types_1.WORM_NAMES[nameIdx++],
                hp: types_1.UNIT_MAX_HP,
                x: 0, y: 0, // Set after terrain generated
                vx: 0, vy: 0,
                onGround: false,
                facing: 1,
                color: teams[t].wormColors[u],
                alive: true,
                lastFallVel: 0,
                inWater: false,
                drownTimer: 0,
                invincible: false,
                invincibleTimer: 0,
                jumpCount: 0,
                maxJumps: 1,
                itemActive: null,
                itemTimer: 0,
            });
        }
    }
    // Items per turn: one weapon item
    const shuffledWeapons = [...types_1.WEAPONS].sort(() => Math.random() - 0.5);
    const turnWeapons = shuffledWeapons.slice(0, Math.min(5, shuffledWeapons.length));
    const turnItem = types_1.ITEMS[Math.floor(Math.random() * types_1.ITEMS.length)];
    return {
        phase: 'playing',
        units,
        teams,
        currentUnitIndex: 0,
        turnNumber: 1,
        turnTimer: types_1.TURN_TIME,
        wind: (Math.random() - 0.5) * 200,
        terrainCanvas: null,
        terrainCtx: null,
        waterCanvas: null,
        waterCtx: null,
        projectiles: [],
        particles: [],
        debris: [],
        floatingTexts: [],
        selectedWeapon: turnWeapons[0].id,
        selectedItem: turnItem.id,
        mouseX: 0,
        mouseY: 0,
        aimMode: 'aiming',
        powerCharge: 50,
        camera: { x: types_1.WORLD_W / 2, y: types_1.WORLD_H / 2, zoom: 0.8, targetX: types_1.WORLD_W / 2, targetY: types_1.WORLD_H / 2, targetZoom: 0.8 },
        shakeX: 0, shakeY: 0, shakeIntensity: 0, shakeTime: 0,
        freezeTime: 0,
        items: { weapon: turnWeapons, item: turnItem },
        usedItems: [],
        waterLevel: 0,
        hasWater: false,
        terrainShape,
        moveMeter: 100,
        moveMeterMax: 100,
    };
}
function placeUnitsOnTerrain(gs) {
    const td = {
        canvas: gs.terrainCanvas,
        ctx: gs.terrainCtx,
        waterCanvas: gs.waterCanvas,
        waterCtx: gs.waterCtx,
        heights: getTerrainHeightsSafe(gs),
        hasWater: gs.hasWater,
        waterLevel: gs.waterLevel,
        shape: gs.terrainShape,
    };
    const alivePerTeam = gs.units.map(u => u.teamId).reduce((acc, t) => {
        acc[t] = (acc[t] || 0) + 1;
        return acc;
    }, []);
    for (let t = 0; t < gs.teams.length; t++) {
        const teamUnits = gs.units.filter(u => u.teamId === t);
        const spread = types_1.WORLD_W / (gs.teams.length * 2);
        const baseX = spread * (t * 2 + 1);
        for (let i = 0; i < teamUnits.length; i++) {
            const u = teamUnits[i];
            const x = baseX + (i - 1.5) * 30;
            const surfaceY = getTerrainSurfaceY(gs, x);
            u.x = x;
            u.y = surfaceY - types_1.UNIT_RADIUS;
            u.alive = true;
            u.hp = types_1.UNIT_MAX_HP;
            u.vx = 0;
            u.vy = 0;
            u.onGround = false;
            u.invincible = true;
            u.invincibleTimer = 3; // 3 seconds invincibility on spawn
        }
    }
}
// We need a way to get height data since terrain.ts generates it
// Store it in a global-accessible way
let _terrainHeights = null;
let _terrainShape = '';
let _terrainCanvas = null;
let _terrainCache = null; // 0=sky, 1=terrain for fast lookups
let _terrainCacheW = 0;
function setTerrainData(heights, shape, canvas) {
    _terrainHeights = heights;
    _terrainShape = shape;
    _terrainCanvas = canvas;
    // Build terrain cache for fast lookups (every 2px)
    const step = 2;
    const cw = Math.ceil(types_1.WORLD_W / step);
    const ch = Math.ceil(types_1.WORLD_H / step);
    _terrainCache = new Uint8Array(cw * ch);
    _terrainCacheW = cw;
    if (canvas) {
        const ctx = canvas.getContext('2d');
        if (ctx) {
            const imgData = ctx.getImageData(0, 0, types_1.WORLD_W, types_1.WORLD_H);
            const d = imgData.data;
            for (let sy = 0; sy < ch; sy++) {
                for (let sx = 0; sx < cw; sx++) {
                    const px = sx * step;
                    const py = sy * step;
                    if (px < types_1.WORLD_W && py < types_1.WORLD_H) {
                        const i = (py * types_1.WORLD_W + px) * 4;
                        const isTerrain = shape === 'cave'
                            ? d[i] > 40 && d[i + 1] > 35
                            : d[i] > 35 || d[i + 1] > 30;
                        _terrainCache[sy * cw + sx] = isTerrain ? 1 : 0;
                    }
                }
            }
        }
    }
}
// Rebuild cache after terrain modification
function rebuildTerrainCache(gs) {
    if (!_terrainCanvas || !gs.terrainCtx)
        return;
    const step = 2;
    const cw = Math.ceil(types_1.WORLD_W / step);
    const ch = Math.ceil(types_1.WORLD_H / step);
    if (!_terrainCache || _terrainCacheW !== cw) {
        _terrainCache = new Uint8Array(cw * ch);
        _terrainCacheW = cw;
    }
    const imgData = gs.terrainCtx.getImageData(0, 0, types_1.WORLD_W, types_1.WORLD_H);
    const d = imgData.data;
    for (let sy = 0; sy < ch; sy++) {
        for (let sx = 0; sx < cw; sx++) {
            const px = sx * step;
            const py = sy * step;
            if (px < types_1.WORLD_W && py < types_1.WORLD_H) {
                const i = (py * types_1.WORLD_W + px) * 4;
                const isTerrain = gs.terrainShape === 'cave'
                    ? d[i] > 40 && d[i + 1] > 35
                    : d[i] > 35 || d[i + 1] > 30;
                _terrainCache[sy * cw + sx] = isTerrain ? 1 : 0;
            }
        }
    }
}
function getTerrainHeightsSafe(gs) {
    return _terrainHeights;
}
function getTerrainSurfaceY(gs, x) {
    if (!_terrainHeights)
        return types_1.WORLD_H * 0.5;
    const ix = Math.max(0, Math.min(types_1.WORLD_W - 1, Math.round(x)));
    if (_terrainShape === 'cave') {
        // For cave, floor is at ~80% of world height + noise
        return types_1.WORLD_H * 0.8;
    }
    return _terrainHeights[ix];
}
function getCurrentUnit(gs) {
    if (gs.currentUnitIndex >= gs.units.length)
        return null;
    return gs.units[gs.currentUnitIndex];
}
// --- Unit Update ---
function updateUnit(u, dt, gs, keys) {
    if (!u.alive)
        return;
    // Invincibility timer
    if (u.invincible) {
        u.invincibleTimer -= dt;
        if (u.invincibleTimer <= 0)
            u.invincible = false;
    }
    // Item effects
    if (u.itemActive) {
        u.itemTimer -= dt;
        if (u.itemTimer <= 0) {
            u.itemActive = null;
            if (u.itemActive === 'parachute')
                u.maxJumps = 1;
        }
    }
    const speedMult = u.itemActive === 'speed' ? 2 : 1;
    const speed = types_1.MOVE_SPEED * speedMult;
    // Movement
    let moveX = 0;
    if (keys.left)
        moveX = -1;
    if (keys.right)
        moveX = 1;
    // Check move meter
    const moveCost = Math.abs(moveX) * dt * 10;
    if (moveCost > gs.moveMeter) {
        moveX = 0;
    }
    else {
        gs.moveMeter -= moveCost;
    }
    u.vx = moveX * speed;
    u.facing = moveX === 0 ? u.facing : (moveX > 0 ? 1 : -1);
    // Wind effect on units
    u.vx += gs.wind * dt * 0.1;
    // Jetpack
    if (u.itemActive === 'jetpack' && u.itemTimer > 0) {
        u.vy -= 500 * dt;
        // Jetpack particles
        for (let i = 0; i < 3; i++) {
            gs.particles.push({
                x: u.x + (Math.random() - 0.5) * 6,
                y: u.y + 10,
                vx: (Math.random() - 0.5) * 30,
                vy: 50 + Math.random() * 40,
                life: 0.3 + Math.random() * 0.3,
                maxLife: 0.5,
                size: 3 + Math.random() * 3,
                color: `hsl(${20 + Math.random() * 20}, 100%, ${50 + Math.random() * 30}%)`,
                type: 'spark',
                gravity: 0,
                friction: 0.95,
            });
        }
    }
    // Apply gravity
    if (u.itemActive !== 'parachute' || !keys.up) {
        const gravMult = u.itemActive === 'parachute' ? 0.3 : 1;
        u.vy += types_1.GRAVITY * gravMult * dt;
    }
    // Wind on airborne
    if (!u.onGround) {
        u.vx += gs.wind * dt * 0.02;
    }
    // Clamp velocity
    u.vy = Math.min(u.vy, types_1.MAX_FALL_VEL);
    u.vx = Math.max(-300, Math.min(300, u.vx));
    // Move
    u.x += u.vx * dt;
    u.y += u.vy * dt;
    // World bounds
    if (u.x < types_1.UNIT_RADIUS) {
        u.x = types_1.UNIT_RADIUS;
        u.vx = 0;
    }
    if (u.x > types_1.WORLD_W - types_1.UNIT_RADIUS) {
        u.x = types_1.WORLD_W - types_1.UNIT_RADIUS;
        u.vx = 0;
    }
    // Water check
    if (gs.hasWater && u.y + types_1.UNIT_RADIUS > gs.waterLevel) {
        u.inWater = true;
        u.drownTimer += dt;
        u.vy *= 0.9; // Damping
        if (u.drownTimer > 5) {
            takeDamage(u, types_1.WATER_DAMAGE * dt, -1, gs);
        }
    }
    else {
        u.inWater = false;
        u.drownTimer = Math.max(0, u.drownTimer - dt * 2);
    }
    // Terrain collision
    handleTerrainCollision(u, gs);
    // Fall damage
    if (u.onGround && Math.abs(u.lastFallVel) > types_1.FALL_DAMAGE_THRESHOLD) {
        const dmg = (Math.abs(u.lastFallVel) - types_1.FALL_DAMAGE_THRESHOLD) * types_1.FALL_DAMAGE_PER_PX;
        if (dmg > 0 && !u.invincible) {
            takeDamage(u, dmg, -1, gs);
            gs.floatingTexts.push({
                x: u.x, y: u.y - 30,
                text: `Fall ${dmg.toFixed(0)}`,
                life: 1.5, maxLife: 1.5,
                color: '#ff8800', size: 16,
                vy: -40,
            });
        }
        u.lastFallVel = 0;
    }
}
function handleTerrainCollision(u, gs) {
    const wasOnGround = u.onGround;
    const feetY = u.y + types_1.UNIT_RADIUS;
    const leftX = u.x - types_1.UNIT_RADIUS + 2;
    const rightX = u.x + types_1.UNIT_RADIUS - 2;
    // Check if feet are in terrain
    const surfaceY = getTerrainSurfaceY(gs, u.x);
    const inTerrain = isUnitInTerrain(gs, u.x, feetY);
    if (inTerrain) {
        // Push unit up
        u.y = surfaceY - types_1.UNIT_RADIUS;
        if (u.vy > 0) {
            u.lastFallVel = u.vy;
        }
        u.vy = 0;
        u.onGround = true;
        u.jumpCount = 0;
        u.vx *= 0.7; // Friction
    }
    else {
        u.onGround = false;
        if (!wasOnGround) {
            u.lastFallVel = u.vy;
        }
    }
    // Wall collision
    const wallLeft = isUnitInTerrain(gs, leftX, u.y);
    const wallRight = isUnitInTerrain(gs, rightX, u.y);
    if (wallLeft)
        u.x = leftX + types_1.UNIT_RADIUS - 2;
    if (wallRight)
        u.x = rightX - types_1.UNIT_RADIUS + 2;
    // Fall off world
    if (u.y > types_1.WORLD_H + 50) {
        u.alive = false;
        u.hp = 0;
    }
}
function isUnitInTerrain(gs, x, y) {
    // Use terrain cache for fast lookup
    if (_terrainCache && _terrainCacheW > 0) {
        const sx = Math.max(0, Math.min(_terrainCacheW - 1, Math.floor(x / 2)));
        const sy = Math.max(0, Math.min(Math.ceil(types_1.WORLD_H / 2) - 1, Math.floor(y / 2)));
        return _terrainCache[sy * _terrainCacheW + sx] === 1;
    }
    // Fallback to pixel read
    const ix = Math.max(0, Math.min(types_1.WORLD_W - 1, Math.round(x)));
    const iy = Math.max(0, Math.min(types_1.WORLD_H - 1, Math.round(y)));
    if (!gs.terrainCtx)
        return false;
    try {
        const px = gs.terrainCtx.getImageData(ix, iy, 1, 1).data;
        if (gs.terrainShape === 'cave') {
            return px[0] > 40 && px[1] > 35;
        }
        return px[0] > 35 || px[1] > 30;
    }
    catch {
        return false;
    }
}
function takeDamage(u, amount, attackerTeamId, gs) {
    if (u.invincible || !u.alive)
        return;
    u.hp -= amount;
    if (u.hp <= 0) {
        u.hp = 0;
        u.alive = false;
        // Death particles
        for (let i = 0; i < 20; i++) {
            gs.particles.push({
                x: u.x, y: u.y,
                vx: (Math.random() - 0.5) * 200,
                vy: (Math.random() - 0.5) * 200 - 50,
                life: 0.5 + Math.random(), maxLife: 1.5,
                size: 3 + Math.random() * 5,
                color: u.color,
                type: 'explosion',
                gravity: 200, friction: 0.97,
            });
        }
    }
}
// --- Weapon Fire ---
function fireWeapon(gs, power, angle) {
    const unit = getCurrentUnit(gs);
    if (!unit || !unit.alive)
        return;
    const rad = -angle * Math.PI / 180;
    const baseSpeed = power * 4;
    switch (gs.selectedWeapon) {
        case 'grenade': {
            gs.projectiles.push({
                id: uid(), x: unit.x, y: unit.y - 15,
                vx: Math.cos(rad) * baseSpeed, vy: Math.sin(rad) * baseSpeed,
                weaponId: 'grenade', ownerTeamId: unit.teamId,
                alive: true, life: 8, trail: [],
                bounceCount: 0, maxBounces: 0, explodeOnTouch: false,
                gravity: types_1.GRAVITY, size: 5, color: '#444',
                rotation: 0, fuse: 0, arming: 30,
            });
            break;
        }
        case 'missile': {
            gs.projectiles.push({
                id: uid(), x: unit.x, y: unit.y - 15,
                vx: Math.cos(rad) * baseSpeed * 0.8, vy: Math.sin(rad) * baseSpeed * 0.8,
                weaponId: 'missile', ownerTeamId: unit.teamId,
                alive: true, life: 6, trail: [],
                bounceCount: 0, maxBounces: 0, explodeOnTouch: false,
                gravity: types_1.GRAVITY * 0.3, size: 4, color: '#f44',
                rotation: 0, fuse: 0, arming: 50,
            });
            break;
        }
        case 'shotgun': {
            for (let i = -3; i <= 3; i++) {
                const spread = i * 0.08;
                const pelletSpeed = baseSpeed * 1.5;
                gs.projectiles.push({
                    id: uid(), x: unit.x, y: unit.y - 15,
                    vx: Math.cos(rad + spread) * pelletSpeed, vy: Math.sin(rad + spread) * pelletSpeed,
                    weaponId: 'shotgun', ownerTeamId: unit.teamId,
                    alive: true, life: 1.5, trail: [],
                    bounceCount: 0, maxBounces: 0, explodeOnTouch: true,
                    gravity: types_1.GRAVITY, size: 3, color: '#fa0',
                    rotation: 0, fuse: 0, arming: 0,
                });
            }
            break;
        }
        case 'cannon': {
            gs.projectiles.push({
                id: uid(), x: unit.x, y: unit.y - 15,
                vx: Math.cos(rad) * baseSpeed * 0.7, vy: Math.sin(rad) * baseSpeed * 0.7,
                weaponId: 'cannon', ownerTeamId: unit.teamId,
                alive: true, life: 10, trail: [],
                bounceCount: 0, maxBounces: 0, explodeOnTouch: false,
                gravity: types_1.GRAVITY, size: 8, color: '#666',
                rotation: 0, fuse: 0, arming: 20,
            });
            break;
        }
        case 'cluster': {
            gs.projectiles.push({
                id: uid(), x: unit.x, y: unit.y - 15,
                vx: Math.cos(rad) * baseSpeed, vy: Math.sin(rad) * baseSpeed,
                weaponId: 'cluster', ownerTeamId: unit.teamId,
                alive: true, life: 8, trail: [],
                bounceCount: 0, maxBounces: 0, explodeOnTouch: false,
                gravity: types_1.GRAVITY, size: 6, color: '#f0f',
                rotation: 0, fuse: 0, arming: 80,
            });
            break;
        }
        case 'nuke': {
            gs.projectiles.push({
                id: uid(), x: unit.x, y: unit.y - 15,
                vx: Math.cos(rad) * baseSpeed * 0.5, vy: Math.sin(rad) * baseSpeed * 0.5,
                weaponId: 'nuke', ownerTeamId: unit.teamId,
                alive: true, life: 15, trail: [],
                bounceCount: 0, maxBounces: 0, explodeOnTouch: false,
                gravity: types_1.GRAVITY * 0.8, size: 12, color: '#0f0',
                rotation: 0, fuse: 0, arming: 40,
            });
            break;
        }
        case 'harpoon': {
            // Instant-hit line projectile
            const dirX = Math.cos(rad), dirY = Math.sin(rad);
            let hitX = unit.x + dirX * 600, hitY = unit.y + dirY * 600;
            // Check terrain intersection
            for (let d = 0; d < 600; d += 5) {
                const tx = unit.x + dirX * d, ty = unit.y + dirY * d;
                if (isUnitInTerrain(gs, tx, ty)) {
                    hitX = tx;
                    hitY = ty;
                    break;
                }
            }
            // Check unit hits
            for (const target of gs.units) {
                if (target.id === unit.id || !target.alive)
                    continue;
                const dx = target.x - unit.x, dy = target.y - (unit.y - 15);
                const dot = dx * dirX + dy * dirY;
                if (dot > 0 && dot < 600) {
                    const perpDist = Math.abs(dx * dirY - dy * dirX);
                    if (perpDist < types_1.UNIT_RADIUS * 2) {
                        hitX = target.x;
                        hitY = target.y;
                        // Pull effect
                        target.vx += (unit.x - target.x) * 3;
                        target.vy -= 200;
                        takeDamage(target, 20, unit.teamId, gs);
                        gs.floatingTexts.push({
                            x: target.x, y: target.y - 20,
                            text: '20', life: 1.5, maxLife: 1.5,
                            color: '#ff44ff', size: 20, vy: -50,
                        });
                    }
                }
            }
            // Create explosion at hit point
            createExplosion(gs, hitX, hitY, 40, 15, unit.teamId, '#ff0');
            break;
        }
        case 'flamethrower': {
            const flameCount = Math.ceil(power / 5);
            for (let i = 0; i < flameCount; i++) {
                const spread = (Math.random() - 0.5) * 0.5;
                const dist = 20 + Math.random() * power * 2;
                const fx = unit.x + Math.cos(rad + spread) * dist;
                const fy = unit.y - 15 + Math.sin(rad + spread) * dist;
                // Check unit hits
                for (const target of gs.units) {
                    if (target.id === unit.id || !target.alive)
                        continue;
                    const dx = target.x - fx, dy = target.y - fy;
                    if (dx * dx + dy * dy < 400) {
                        takeDamage(target, 1 + Math.random() * 2, unit.teamId, gs);
                    }
                }
                gs.particles.push({
                    x: fx, y: fy,
                    vx: (Math.random() - 0.5) * 40, vy: (Math.random() - 0.5) * 40,
                    life: 0.2 + Math.random() * 0.3, maxLife: 0.5,
                    size: 4 + Math.random() * 6,
                    color: `hsl(${Math.random() * 30}, 100%, ${50 + Math.random() * 30}%)`,
                    type: 'explosion', gravity: 0, friction: 0.9,
                });
            }
            break;
        }
    }
    // Recoil
    unit.vx -= Math.cos(rad) * 30;
    unit.vy -= Math.sin(rad) * 20;
}
// --- Projectile Update ---
function updateProjectiles(dt, gs) {
    for (const p of gs.projectiles) {
        if (!p.alive)
            continue;
        p.life -= dt;
        if (p.life <= 0) {
            p.alive = false;
            createExplosion(gs, p.x, p.y, weaponData(p.weaponId).aoe, weaponData(p.weaponId).damage, p.ownerTeamId, weaponData(p.weaponId).color);
            continue;
        }
        // Arming distance
        if (p.arming > 0) {
            p.arming -= Math.sqrt(p.vx * p.vx + p.vy * p.vy) * dt;
            continue;
        }
        // Wind effect
        p.vx += gs.wind * dt * 0.1;
        // Gravity
        p.vy += p.gravity * dt;
        // Missile steering
        if (p.weaponId === 'missile') {
            // Steer toward mouse
            const dx = gs.mouseX - p.x, dy = gs.mouseY - p.y;
            const dist = Math.sqrt(dx * dx + dy * dy);
            if (dist > 1) {
                const steer = 300 * dt;
                p.vx += (dx / dist) * steer;
                p.vy += (dy / dist) * steer;
            }
        }
        // Update rotation
        p.rotation = Math.atan2(p.vy, p.vx);
        // Trail
        p.trail.push({ x: p.x, y: p.y });
        if (p.trail.length > 30)
            p.trail.shift();
        p.x += p.vx * dt;
        p.y += p.vy * dt;
        // Out of bounds
        if (p.x < -50 || p.x > types_1.WORLD_W + 50 || p.y > types_1.WORLD_H + 50) {
            p.alive = false;
            continue;
        }
        // Terrain collision
        if (isUnitInTerrain(gs, p.x, p.y)) {
            if (p.explodeOnTouch || p.weaponId === 'nuke' || p.weaponId === 'cannon') {
                const data = weaponData(p.weaponId);
                createExplosion(gs, p.x, p.y, data.aoe, data.damage, p.ownerTeamId, data.color);
                p.alive = false;
            }
            else if (p.bounceCount < p.maxBounces) {
                p.bounceCount++;
                p.vy = -p.vy * 0.5;
                p.vx *= 0.7;
            }
            else {
                p.alive = false;
                createExplosion(gs, p.x, p.y, 40, 30, p.ownerTeamId, '#ff8');
            }
            continue;
        }
        // Unit collision
        for (const u of gs.units) {
            if (!u.alive || u.invincible)
                continue;
            const dx = u.x - p.x, dy = u.y - p.y;
            if (dx * dx + dy * dy < types_1.UNIT_RADIUS * types_1.UNIT_RADIUS) {
                const data = weaponData(p.weaponId);
                createExplosion(gs, p.x, p.y, data.aoe, data.damage, p.ownerTeamId, data.color);
                p.alive = false;
                break;
            }
        }
        // Cluster bomb splits at 50% life
        if (p.weaponId === 'cluster' && p.life < 4) {
            // Already split flag check via a hack: check if we're the main and life is low
            if (!p.id.includes('_sub')) {
                for (let i = 0; i < 5; i++) {
                    const a = (Math.PI * 2 * i) / 5 + Math.random() * 0.5;
                    gs.projectiles.push({
                        id: uid() + '_sub', x: p.x, y: p.y,
                        vx: Math.cos(a) * 80 + p.vx * 0.3, vy: Math.sin(a) * 80 + p.vy * 0.3,
                        weaponId: 'grenade', ownerTeamId: p.ownerTeamId,
                        alive: true, life: 3, trail: [],
                        bounceCount: 0, maxBounces: 0, explodeOnTouch: true,
                        gravity: types_1.GRAVITY, size: 3, color: '#f0f',
                        rotation: 0, fuse: 0, arming: 0,
                    });
                }
                p.alive = false;
            }
        }
        // Nuke self-destruct
        if (p.weaponId === 'nuke' && p.life < 1) {
            createExplosion(gs, p.x, p.y, 250, 150, p.ownerTeamId, '#ff0');
            p.alive = false;
        }
    }
    // Cleanup
    gs.projectiles = gs.projectiles.filter(p => p.alive);
}
function weaponData(id) {
    const map = {
        grenade: { aoe: 50, damage: 30, color: '#fa0' },
        missile: { aoe: 60, damage: 35, color: '#f44' },
        shotgun: { aoe: 15, damage: 8, color: '#fa0' },
        cannon: { aoe: 80, damage: 50, color: '#ff4' },
        cluster: { aoe: 40, damage: 20, color: '#f0f' },
        nuke: { aoe: 250, damage: 150, color: '#ff0' },
        harpoon: { aoe: 40, damage: 15, color: '#ff0' },
        flamethrower: { aoe: 0, damage: 0, color: '#fa0' },
    };
    return map[id] || { aoe: 50, damage: 30, color: '#fa0' };
}
// --- Explosions ---
function createExplosion(gs, x, y, radius, damage, ownerTeamId, color) {
    // Screen shake
    const shakeAmt = Math.min(radius * 0.15, 20);
    gs.shakeIntensity = shakeAmt;
    gs.shakeTime = 0.3 + radius * 0.002;
    // Freeze time for big explosions
    if (radius > 100) {
        gs.freezeTime = 0.15;
    }
    // Damage units
    for (const u of gs.units) {
        if (!u.alive || u.invincible)
            continue;
        const dx = u.x - x, dy = u.y - y;
        const dist = Math.sqrt(dx * dx + dy * dy);
        if (dist < radius + types_1.UNIT_RADIUS) {
            const falloff = 1 - Math.min(dist / (radius + types_1.UNIT_RADIUS), 1);
            const dmg = damage * falloff;
            takeDamage(u, dmg, ownerTeamId, gs);
            // Knockback
            const kb = radius * 0.8 * falloff;
            if (dist > 0) {
                u.vx += (dx / dist) * kb;
                u.vy += (dy / dist) * kb - kb * 0.3;
            }
            // Floating damage text
            gs.floatingTexts.push({
                x: u.x + (Math.random() - 0.5) * 10,
                y: u.y - 20,
                text: `${dmg.toFixed(0)}`,
                life: 1.5, maxLife: 1.5,
                color: ownerTeamId !== u.teamId ? '#ff4444' : '#ffaa00',
                size: 14 + dmg * 0.3,
                vy: -60,
            });
        }
    }
    // Explosion particles
    const pCount = Math.min(radius * 2, 100);
    for (let i = 0; i < pCount; i++) {
        const a = Math.random() * Math.PI * 2;
        const speed = Math.random() * radius * 2;
        gs.particles.push({
            x, y,
            vx: Math.cos(a) * speed, vy: Math.sin(a) * speed - Math.random() * 50,
            life: 0.5 + Math.random() * 1, maxLife: 1.5,
            size: 2 + Math.random() * (radius / 10),
            color,
            type: i % 3 === 0 ? 'smoke' : 'explosion',
            gravity: 100 + Math.random() * 100,
            friction: 0.97,
        });
    }
    // Smoke
    for (let i = 0; i < 10; i++) {
        gs.particles.push({
            x: x + (Math.random() - 0.5) * radius,
            y: y + (Math.random() - 0.5) * radius,
            vx: (Math.random() - 0.5) * 30,
            vy: -20 - Math.random() * 30,
            life: 1 + Math.random() * 2, maxLife: 3,
            size: 5 + Math.random() * 10,
            color: '#555',
            type: 'smoke',
            gravity: -10,
            friction: 0.99,
        });
    }
    // Spawn debris from terrain
    if (gs.terrainCtx) {
        const debrisData = spawnDebrisFromExplosionLocal(gs, x, y, radius);
        for (const db of debrisData) {
            gs.debris.push({
                x: db.x, y: db.y,
                w: db.w, h: db.h,
                vx: db.vx, vy: db.vy,
                rotation: 0, vr: (Math.random() - 0.5) * 10,
                pixels: null,
                settled: false,
                settleTime: 0,
                _color: db.color,
            });
        }
    }
    // Destroy terrain
    if (gs.terrainCtx) {
        destroyTerrainCircleLocal(gs, x, y, radius);
    }
}
function spawnDebrisFromExplosionLocal(gs, cx, cy, radius) {
    const debris = [];
    const r = Math.round(radius);
    for (let a = 0; a < Math.PI * 2; a += 0.4) {
        const dist = r * (0.5 + Math.random() * 0.8);
        const dx = cx + Math.cos(a) * dist;
        const dy = cy + Math.sin(a) * dist;
        const ix = Math.round(dx), iy = Math.round(dy);
        if (ix >= 0 && ix < types_1.WORLD_W && iy >= 0 && iy < types_1.WORLD_H) {
            try {
                const px = gs.terrainCtx.getImageData(ix, iy, 1, 1).data;
                const isTerrain = gs.terrainShape === 'cave'
                    ? px[0] > 40 && px[1] > 35
                    : px[0] > 35 || px[1] > 30;
                if (isTerrain) {
                    const size = 3 + Math.random() * 8;
                    debris.push({
                        x: dx, y: dy,
                        vx: Math.cos(a) * (40 + Math.random() * 80),
                        vy: Math.sin(a) * (40 + Math.random() * 80) - 60,
                        w: size, h: size,
                        color: `rgb(${px[0] | 0},${px[1] | 0},${px[2] | 0})`,
                    });
                }
            }
            catch { /* skip */ }
        }
    }
    return debris;
}
function destroyTerrainCircleLocal(gs, cx, cy, radius) {
    if (!gs.terrainCtx)
        return;
    // Use image data for batch modification
    let imgData;
    try {
        imgData = gs.terrainCtx.getImageData(0, 0, types_1.WORLD_W, types_1.WORLD_H);
    }
    catch {
        return;
    }
    const d = imgData.data;
    const r = Math.round(radius);
    const r2 = r * r;
    const x0 = Math.max(0, Math.round(cx - r));
    const y0 = Math.max(0, Math.round(cy - r));
    const x1 = Math.min(types_1.WORLD_W, Math.round(cx + r) + 1);
    const y1 = Math.min(types_1.WORLD_H, Math.round(cy + r) + 1);
    const isCave = gs.terrainShape === 'cave';
    for (let y = y0; y < y1; y++) {
        for (let x = x0; x < x1; x++) {
            const dx = x - cx, dy = y - cy;
            if (dx * dx + dy * dy < r2) {
                const i = (y * types_1.WORLD_W + x) * 4;
                const isTerrain = isCave
                    ? d[i] > 40 && d[i + 1] > 35
                    : d[i] > 35 || d[i + 1] > 30;
                if (isTerrain) {
                    d[i] = isCave ? 18 : (12 + y / types_1.WORLD_H * 25) | 0;
                    d[i + 1] = isCave ? 18 : (16 + y / types_1.WORLD_H * 35) | 0;
                    d[i + 2] = isCave ? 30 : (40 + y / types_1.WORLD_H * 65) | 0;
                    d[i + 3] = 255;
                }
            }
        }
    }
    gs.terrainCtx.putImageData(imgData, 0, 0);
    rebuildTerrainCache(gs);
}
// --- Particles ---
function updateParticles(dt, gs) {
    for (const p of gs.particles) {
        p.life -= dt;
        if (p.life <= 0)
            continue;
        p.vy += p.gravity * dt;
        p.vx *= p.friction;
        p.vy *= p.friction;
        p.x += p.vx * dt;
        p.y += p.vy * dt;
        // Terrain collision for debris particles
        if (p.type === 'debris' && gs.terrainCtx) {
            try {
                const px = gs.terrainCtx.getImageData(Math.round(p.x), Math.round(p.y), 1, 1).data;
                const isTerrain = gs.terrainShape === 'cave'
                    ? px[0] > 40 && px[1] > 35
                    : px[0] > 35 || px[1] > 30;
                if (isTerrain) {
                    p.life = 0;
                }
            }
            catch { /* skip */ }
        }
    }
    gs.particles = gs.particles.filter(p => p.life > 0);
}
// --- Debris ---
function updateDebris(dt, gs) {
    for (const d of gs.debris) {
        if (d.settled) {
            d.settleTime -= dt;
            if (d.settleTime <= 0)
                d._remove = true;
            continue;
        }
        d.vy += types_1.GRAVITY * 0.7 * dt;
        d.x += d.vx * dt;
        d.y += d.vy * dt;
        d.rotation += d.vr * dt;
        // Terrain collision
        if (gs.terrainCtx) {
            try {
                const px = gs.terrainCtx.getImageData(Math.round(d.x), Math.round(d.y + d.h), 1, 1).data;
                const isTerrain = gs.terrainShape === 'cave'
                    ? px[0] > 40 && px[1] > 35
                    : px[0] > 35 || px[1] > 30;
                if (isTerrain) {
                    d.y -= d.h;
                    d.vy = 0;
                    d.vx = 0;
                    d.vr = 0;
                    d.settled = true;
                    d.settleTime = 3;
                }
            }
            catch { /* skip */ }
        }
        if (d.y > types_1.WORLD_H + 100)
            d._remove = true;
    }
    gs.debris = gs.debris.filter(d => !d._remove);
}
// --- Floating Text ---
function updateFloatingTexts(dt, gs) {
    for (const ft of gs.floatingTexts) {
        ft.life -= dt;
        ft.y += ft.vy * dt;
    }
    gs.floatingTexts = gs.floatingTexts.filter(ft => ft.life > 0);
}
// --- Turn Management ---
function advanceTurn(gs) {
    // Find next alive unit
    let nextIdx = (gs.currentUnitIndex + 1) % gs.units.length;
    let safety = 0;
    while (!gs.units[nextIdx].alive && safety < gs.units.length) {
        nextIdx = (nextIdx + 1) % gs.units.length;
        safety++;
    }
    if (safety >= gs.units.length) {
        // All dead
        return;
    }
    // Check if we're back to start = new round
    if (nextIdx < gs.currentUnitIndex) {
        gs.turnNumber++;
        // Change wind
        gs.wind = (Math.random() - 0.5) * 200;
    }
    gs.currentUnitIndex = nextIdx;
    gs.turnTimer = types_1.TURN_TIME;
    gs.aimMode = 'aiming';
    gs.powerCharge = 50;
    gs.moveMeter = gs.moveMeterMax;
    // New items for this turn
    const shuffled = [...types_1.WEAPONS].sort(() => Math.random() - 0.5);
    gs.items.weapon = shuffled.slice(0, 5);
    gs.items.item = types_1.ITEMS[Math.floor(Math.random() * types_1.ITEMS.length)];
    gs.usedItems = [];
    const unit = getCurrentUnit(gs);
    if (unit) {
        gs.selectedWeapon = gs.items.weapon[0].id;
    }
}
// --- Check Game Over ---
function checkGameOver(gs) {
    const aliveTeams = new Set();
    for (const u of gs.units) {
        if (u.alive)
            aliveTeams.add(u.teamId);
    }
    if (aliveTeams.size <= 1) {
        const winner = [...aliveTeams][0];
        return gs.teams[winner].name;
    }
    return null;
}
function handleInput(gs, keys, mouseDown, click) {
    const unit = getCurrentUnit(gs);
    if (!unit || !unit.alive)
        return;
    if (gs.phase !== 'playing')
        return;
    // Jump
    if (keys.jump && !keys.usedJump) {
        if (unit.onGround || unit.jumpCount < unit.maxJumps) {
            unit.vy = -types_1.JUMP_FORCE;
            unit.onGround = false;
            unit.jumpCount++;
            keys.usedJump = true;
        }
    }
    if (!keys.jump)
        keys.usedJump = false;
    // Aiming
    if (gs.aimMode === 'aiming') {
        // Aim toward mouse
    }
}
};

// ── module: src/sound.ts ──
__mods["src/sound.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.sound = void 0;
// Simple synthesized sound effects using Web Audio API
class SoundManager {
    constructor() {
        this.ctx = null;
        this.masterGain = null;
    }
    init() {
        if (this.ctx)
            return;
        this.ctx = new AudioContext();
        this.masterGain = this.ctx.createGain();
        this.masterGain.gain.value = 0.3;
        this.masterGain.connect(this.ctx.destination);
    }
    getCtx() {
        if (!this.ctx)
            this.init();
        return this.ctx;
    }
    playOsc(freq, duration, type = 'square', volume = 0.2, freqEnd) {
        const ctx = this.getCtx();
        if (!ctx)
            return;
        const osc = ctx.createOscillator();
        const gain = ctx.createGain();
        osc.type = type;
        osc.frequency.setValueAtTime(freq, ctx.currentTime);
        if (freqEnd)
            osc.frequency.exponentialRampToValueAtTime(Math.max(freqEnd, 1), ctx.currentTime + duration);
        gain.gain.setValueAtTime(volume, ctx.currentTime);
        gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + duration);
        osc.connect(gain);
        gain.connect(this.masterGain);
        osc.start();
        osc.stop(ctx.currentTime + duration);
    }
    playClick() {
        this.playOsc(800, 0.05, 'square', 0.15, 600);
    }
    playExplosion(size = 1) {
        const ctx = this.getCtx();
        if (!ctx)
            return;
        // Noise burst
        const bufferSize = ctx.sampleRate * 0.3 * size;
        const buffer = ctx.createBuffer(1, bufferSize, ctx.sampleRate);
        const data = buffer.getChannelData(0);
        for (let i = 0; i < bufferSize; i++) {
            data[i] = (Math.random() * 2 - 1) * Math.exp(-i / (bufferSize * 0.1));
        }
        const source = ctx.createBufferSource();
        source.buffer = buffer;
        const gain = ctx.createGain();
        gain.gain.setValueAtTime(Math.min(size * 0.3, 0.5), ctx.currentTime);
        gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.3 * size);
        const filter = ctx.createBiquadFilter();
        filter.type = 'lowpass';
        filter.frequency.value = 800;
        source.connect(filter);
        filter.connect(gain);
        gain.connect(this.masterGain);
        source.start();
    }
    playJump() {
        this.playOsc(300, 0.15, 'sine', 0.2, 600);
    }
    playHit() {
        this.playOsc(200, 0.1, 'sawtooth', 0.15, 50);
    }
    playDeath() {
        this.playOsc(400, 0.5, 'sawtooth', 0.2, 80);
    }
    playFire(type) {
        switch (type) {
            case 'grenade':
                this.playOsc(200, 0.2, 'square', 0.15, 400);
                break;
            case 'missile':
                this.playOsc(150, 0.3, 'sawtooth', 0.1, 800);
                break;
            case 'shotgun':
                this.playOsc(100, 0.15, 'sawtooth', 0.25, 200);
                break;
            case 'cannon':
                this.playOsc(80, 0.4, 'square', 0.3, 40);
                break;
            case 'cluster':
                this.playOsc(500, 0.2, 'square', 0.15, 300);
                break;
            case 'nuke':
                this.playOsc(60, 1.0, 'sawtooth', 0.4, 20);
                break;
            case 'harpoon':
                this.playOsc(1000, 0.1, 'square', 0.1, 200);
                break;
            case 'flamethrower':
                const ctx = this.getCtx();
                if (!ctx)
                    return;
                const bufSize = ctx.sampleRate * 0.5;
                const buf = ctx.createBuffer(1, bufSize, ctx.sampleRate);
                const d = buf.getChannelData(0);
                for (let i = 0; i < bufSize; i++) {
                    d[i] = (Math.random() * 2 - 1) * Math.exp(-i / (bufSize * 0.3)) * Math.sin(i * 0.05);
                }
                const src = ctx.createBufferSource();
                src.buffer = buf;
                const g = ctx.createGain();
                g.gain.value = 0.15;
                src.connect(g);
                g.connect(this.masterGain);
                src.start();
                break;
        }
    }
    playTurnTick(urgency) {
        // urgency 0-1, higher = more urgent
        const freq = 800 + urgency * 2000;
        this.playOsc(freq, 0.05, 'square', 0.08 * (1 - urgency * 0.5));
    }
    playTurnEnd() {
        this.playOsc(400, 0.3, 'triangle', 0.2, 100);
    }
    playSelect() {
        this.playOsc(600, 0.08, 'sine', 0.15);
    }
    playDamage() {
        this.playOsc(150 + Math.random() * 100, 0.1, 'sawtooth', 0.15, 50);
    }
    playWater() {
        const ctx = this.getCtx();
        if (!ctx)
            return;
        const bufSize = ctx.sampleRate * 0.2;
        const buf = ctx.createBuffer(1, bufSize, ctx.sampleRate);
        const d = buf.getChannelData(0);
        for (let i = 0; i < bufSize; i++) {
            d[i] = (Math.random() * 2 - 1) * Math.exp(-i / (bufSize * 0.2)) * 0.3;
        }
        const src = ctx.createBufferSource();
        src.buffer = buf;
        const g = ctx.createGain();
        g.gain.value = 0.1;
        src.connect(g);
        g.connect(this.masterGain);
        src.start();
    }
}
exports.sound = new SoundManager();
};

// ── module: src/renderer.ts ──
__mods["src/renderer.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.setRenderTarget = setRenderTarget;
exports.render = render;
const types_1 = require("./types");
const sound_1 = require("./sound");
const game_1 = require("./game");
let rt = null;
let lastRenderTime = 0;
function setRenderTarget(canvas) {
    rt = { canvas, ctx: canvas.getContext('2d') };
}
function render(gs) {
    if (!rt)
        return;
    const { canvas, ctx } = rt;
    lastRenderTime = performance.now();
    const cw = canvas.clientWidth, ch = canvas.clientHeight;
    const dpr = window.devicePixelRatio || 1;
    if (canvas.width !== cw * dpr || canvas.height !== ch * dpr) {
        canvas.width = cw * dpr;
        canvas.height = ch * dpr;
    }
    ctx.save();
    ctx.scale(dpr, dpr);
    ctx.clearRect(0, 0, cw, ch);
    // --- Camera ---
    const cam = gs.camera;
    const unit = gs.units[gs.currentUnitIndex];
    cam.targetX = 0;
    cam.targetY = 0;
    if (unit && unit.alive) {
        cam.targetX = unit.x;
        cam.targetY = unit.y;
    }
    cam.x += (cam.targetX - cam.x) * 0.06;
    cam.y += (cam.targetY - cam.y) * 0.06;
    const idealZoom = Math.min(cw / types_1.WORLD_W, ch / types_1.WORLD_H) * 0.88;
    cam.targetZoom = Math.max(0.3, idealZoom);
    cam.zoom += (cam.targetZoom - cam.zoom) * 0.04;
    // Screen shake
    let shakeX = 0, shakeY = 0;
    if (gs.shakeTime > 0) {
        shakeX = (Math.random() - 0.5) * gs.shakeIntensity * 2;
        shakeY = (Math.random() - 0.5) * gs.shakeIntensity * 2;
    }
    // Apply camera
    ctx.translate(cw / 2, ch / 2);
    ctx.scale(cam.zoom, cam.zoom);
    ctx.translate(-cam.x - shakeX, -cam.y - shakeY);
    // --- Draw World ---
    if (gs.terrainCanvas) {
        ctx.drawImage(gs.terrainCanvas, 0, 0, types_1.WORLD_W, types_1.WORLD_H);
    }
    // Water
    if (gs.hasWater && gs.waterCanvas) {
        ctx.drawImage(gs.waterCanvas, 0, 0, types_1.WORLD_W, types_1.WORLD_H);
        // Animated water surface
        const now = Date.now() * 0.001;
        ctx.strokeStyle = 'rgba(80, 160, 240, 0.4)';
        ctx.lineWidth = 2;
        ctx.beginPath();
        for (let x = 0; x < types_1.WORLD_W; x += 4) {
            const wave = Math.sin(x * 0.03 + now * 2) * 3 + Math.sin(x * 0.07 + now * 3) * 1.5;
            if (x === 0)
                ctx.moveTo(x, gs.waterLevel + wave);
            else
                ctx.lineTo(x, gs.waterLevel + wave);
        }
        ctx.stroke();
    }
    // Debris
    for (const d of gs.debris) {
        ctx.save();
        ctx.translate(d.x, d.y);
        ctx.rotate(d.rotation);
        ctx.fillStyle = d._color || '#886644';
        ctx.fillRect(-d.w / 2, -d.h / 2, d.w, d.h);
        ctx.strokeStyle = 'rgba(0,0,0,0.3)';
        ctx.lineWidth = 0.5;
        ctx.strokeRect(-d.w / 2, -d.h / 2, d.w, d.h);
        ctx.restore();
    }
    // Projectile trails
    for (const p of gs.projectiles) {
        if (!p.alive || p.trail.length < 2)
            continue;
        ctx.strokeStyle = p.color;
        ctx.lineWidth = 1.5;
        ctx.globalAlpha = 0.3;
        ctx.beginPath();
        ctx.moveTo(p.trail[0].x, p.trail[0].y);
        for (let i = 1; i < p.trail.length; i++) {
            ctx.lineTo(p.trail[i].x, p.trail[i].y);
        }
        ctx.stroke();
        ctx.globalAlpha = 1;
    }
    // Projectiles
    for (const p of gs.projectiles) {
        if (!p.alive)
            continue;
        ctx.save();
        ctx.translate(p.x, p.y);
        ctx.rotate(p.rotation);
        // Glow
        ctx.shadowColor = p.color;
        ctx.shadowBlur = 8;
        ctx.fillStyle = p.color;
        ctx.beginPath();
        ctx.arc(0, 0, p.size, 0, Math.PI * 2);
        ctx.fill();
        ctx.shadowBlur = 0;
        // Inner highlight
        ctx.fillStyle = 'rgba(255,255,255,0.4)';
        ctx.beginPath();
        ctx.arc(-p.size * 0.2, -p.size * 0.2, p.size * 0.4, 0, Math.PI * 2);
        ctx.fill();
        ctx.restore();
        // Nuke glow
        if (p.weaponId === 'nuke') {
            const pulse = 0.5 + 0.5 * Math.sin(Date.now() * 0.01);
            ctx.fillStyle = `rgba(0, 255, 100, ${pulse * 0.25})`;
            ctx.beginPath();
            ctx.arc(p.x, p.y, p.size * 4, 0, Math.PI * 2);
            ctx.fill();
        }
    }
    // Units (worms)
    const activeUnit = gs.units[gs.currentUnitIndex];
    for (const u of gs.units) {
        if (!u.alive)
            continue;
        drawWorm(u, gs, u === activeUnit);
    }
    // Particles
    for (const p of gs.particles) {
        const alpha = Math.max(0, p.life / p.maxLife);
        ctx.globalAlpha = alpha;
        ctx.fillStyle = p.color;
        ctx.beginPath();
        if (p.type === 'smoke') {
            const s = p.size * (1 + (1 - alpha) * 0.8);
            ctx.arc(p.x, p.y, s, 0, Math.PI * 2);
        }
        else {
            ctx.arc(p.x, p.y, p.size * alpha, 0, Math.PI * 2);
        }
        ctx.fill();
    }
    ctx.globalAlpha = 1;
    // Floating texts
    for (const ft of gs.floatingTexts) {
        const alpha = ft.life / ft.maxLife;
        const scale = 1 + (1 - alpha) * 0.6;
        ctx.save();
        ctx.globalAlpha = alpha;
        ctx.fillStyle = ft.color;
        ctx.font = `bold ${Math.round(ft.size * scale)}px 'Segoe UI', sans-serif`;
        ctx.textAlign = 'center';
        ctx.strokeStyle = '#000';
        ctx.lineWidth = 3;
        ctx.strokeText(ft.text, ft.x, ft.y);
        ctx.fillText(ft.text, ft.x, ft.y);
        ctx.restore();
    }
    // Trajectory preview
    if (gs.aimMode === 'power' && unit && unit.alive && gs.phase === 'playing') {
        drawTrajectory(gs, unit);
    }
    if (unit && unit.alive && gs.phase === 'playing') {
        drawAimLine(gs, unit);
    }
    // Active unit indicator ring
    if (activeUnit && activeUnit.alive) {
        const ringTime = Date.now() * 0.003;
        const ringR = 22 + Math.sin(ringTime) * 3;
        ctx.strokeStyle = gs.teams[activeUnit.teamId].color + '80';
        ctx.lineWidth = 2;
        ctx.setLineDash([6, 4]);
        ctx.lineDashOffset = -ringTime * 10;
        ctx.beginPath();
        ctx.arc(activeUnit.x, activeUnit.y, ringR, 0, Math.PI * 2);
        ctx.stroke();
        ctx.setLineDash([]);
    }
    ctx.restore();
    // Freeze effect
    if (gs.freezeTime > 0) {
        ctx.fillStyle = `rgba(255, 255, 255, ${gs.freezeTime * 0.4})`;
        ctx.fillRect(0, 0, cw, ch);
    }
    updateHUD(gs, cw, ch);
}
function drawWorm(u, gs, isActive) {
    const ctx = rt.ctx;
    const x = u.x, y = u.y;
    const now = Date.now() * 0.001;
    const t = gs.teams[u.teamId];
    const isFallen = !u.onGround && u.vy > 100;
    const bodyWobble = Math.sin(now * 3 + u.id.charCodeAt(0)) * 2;
    ctx.save();
    ctx.translate(x, y);
    // Shadow
    ctx.fillStyle = 'rgba(0,0,0,0.15)';
    ctx.beginPath();
    ctx.ellipse(0, 14, 14, 4, 0, 0, Math.PI * 2);
    ctx.fill();
    // --- Worm body (elongated, slightly wobbly) ---
    const bodyColor = u.color;
    const darker = shadeColor(bodyColor, -30);
    const lighter = shadeColor(bodyColor, 30);
    // Body segments (drawn as overlapping ellipses for a worm look)
    for (let s = 2; s >= -2; s--) {
        const segY = s * 4;
        const segW = 10 - Math.abs(s) * 1.5;
        const segH = 7;
        const wobble = Math.sin(now * 4 + s * 0.8) * 0.8;
        const grad = ctx.createRadialGradient(0, segY, 0, 0, segY, segW);
        grad.addColorStop(0, lighter);
        grad.addColorStop(1, darker);
        ctx.fillStyle = grad;
        ctx.beginPath();
        ctx.ellipse(wobble, segY, segW, segH, 0, 0, Math.PI * 2);
        ctx.fill();
    }
    // Body outline
    ctx.strokeStyle = 'rgba(0,0,0,0.25)';
    ctx.lineWidth = 1;
    ctx.beginPath();
    ctx.ellipse(0, 0, 10, 12, 0, 0, Math.PI * 2);
    ctx.stroke();
    // --- Head ---
    const headY = -8;
    ctx.fillStyle = bodyColor;
    ctx.beginPath();
    ctx.ellipse(0, headY, 8, 6, 0, 0, Math.PI * 2);
    ctx.fill();
    ctx.strokeStyle = 'rgba(0,0,0,0.25)';
    ctx.stroke();
    // Highlight on head
    ctx.fillStyle = 'rgba(255,255,255,0.2)';
    ctx.beginPath();
    ctx.ellipse(-2, headY - 2, 4, 2, -0.3, 0, Math.PI * 2);
    ctx.fill();
    // --- Eyes ---
    const eyeY = headY - 1;
    const eyeSpread = 3;
    const pupilOff = u.facing * 1.2;
    // Eye whites
    ctx.fillStyle = '#fff';
    ctx.beginPath();
    ctx.ellipse(-eyeSpread, eyeY, 2.8, 3, 0, 0, Math.PI * 2);
    ctx.fill();
    ctx.beginPath();
    ctx.ellipse(eyeSpread, eyeY, 2.8, 3, 0, 0, Math.PI * 2);
    ctx.fill();
    // Pupils
    ctx.fillStyle = '#111';
    ctx.beginPath();
    ctx.arc(-eyeSpread + pupilOff, eyeY + 0.3, 1.4, 0, Math.PI * 2);
    ctx.fill();
    ctx.beginPath();
    ctx.arc(eyeSpread + pupilOff, eyeY + 0.3, 1.4, 0, Math.PI * 2);
    ctx.fill();
    // Mouth (changes based on state)
    ctx.strokeStyle = '#333';
    ctx.lineWidth = 1;
    ctx.beginPath();
    if (isFallen) {
        // Scared mouth (O shape)
        ctx.arc(0, eyeY + 5, 1.5, 0, Math.PI * 2);
        ctx.stroke();
    }
    else {
        // Happy mouth
        ctx.arc(u.facing * 2, eyeY + 5, 2, 0.1, Math.PI - 0.1);
        ctx.stroke();
    }
    // --- Legs ---
    if (u.onGround) {
        const legAnim = Math.sin(now * 6) * 2;
        const hasMove = gs.units[gs.currentUnitIndex]?.id === u.id;
        const moveAnim = hasMove ? Math.sin(now * 8) * 3 : 0;
        ctx.strokeStyle = bodyColor;
        ctx.lineWidth = 2.5;
        ctx.lineCap = 'round';
        // Left leg
        ctx.beginPath();
        ctx.moveTo(-5, 3);
        ctx.lineTo(-7 - moveAnim, 12 + legAnim);
        ctx.lineTo(-9, 13);
        ctx.stroke();
        // Right leg
        ctx.beginPath();
        ctx.moveTo(5, 3);
        ctx.lineTo(7 + moveAnim, 12 - legAnim);
        ctx.lineTo(9, 13);
        ctx.stroke();
    }
    else {
        // Airborne legs (tucked)
        ctx.strokeStyle = bodyColor;
        ctx.lineWidth = 2;
        ctx.lineCap = 'round';
        ctx.beginPath();
        ctx.moveTo(-4, 5);
        ctx.lineTo(-6, 9);
        ctx.stroke();
        ctx.beginPath();
        ctx.moveTo(4, 5);
        ctx.lineTo(6, 9);
        ctx.stroke();
    }
    // --- Hat (for active unit) ---
    if (isActive) {
        drawHat(ctx, 0, headY - 5, u.facing, t.color);
    }
    ctx.restore();
    // Name tag
    ctx.save();
    ctx.font = 'bold 11px sans-serif';
    ctx.textAlign = 'center';
    const nameY = y - 24;
    // Name background
    const nameW = ctx.measureText(u.name).width + 8;
    ctx.fillStyle = 'rgba(0,0,0,0.5)';
    roundRect(ctx, x - nameW / 2, nameY - 10, nameW, 14, 4);
    ctx.fill();
    ctx.fillStyle = isActive ? '#ffd700' : '#fff';
    ctx.strokeStyle = '#000';
    ctx.lineWidth = 2;
    ctx.strokeText(u.name, x, nameY);
    ctx.fillText(u.name, x, nameY);
    // HP bar
    const barW = 36, barH = 4;
    const barX = x - barW / 2;
    const barY = nameY + 4;
    ctx.fillStyle = 'rgba(0,0,0,0.5)';
    roundRect(ctx, barX, barY, barW, barH, 2);
    ctx.fill();
    const hpColor = u.hp > 60 ? '#2ecc71' : u.hp > 30 ? '#f39c12' : '#e74c3c';
    ctx.fillStyle = hpColor;
    roundRect(ctx, barX, barY, barW * Math.max(0, u.hp / 100), barH, 2);
    ctx.fill();
    ctx.restore();
    // Water effect
    if (u.inWater) {
        ctx.strokeStyle = 'rgba(150, 200, 255, 0.4)';
        ctx.lineWidth = 1;
        for (let i = 0; i < 4; i++) {
            const bx = x + Math.sin(now * 3 + i * 1.5) * 8;
            const by = y - 12 - ((now * 15 + i * 12) % 25);
            ctx.beginPath();
            ctx.arc(bx, by, 2, 0, Math.PI * 2);
            ctx.stroke();
        }
    }
    // Item icon
    if (u.itemActive) {
        const icons = {
            parachute: '🪂', jetpack: '✈️', shield: '🛡️', speed: '⚡', grappling: '🪝'
        };
        const icon = icons[u.itemActive];
        if (icon) {
            ctx.font = '14px sans-serif';
            ctx.fillText(icon, x + 16, y - 12);
        }
    }
}
function drawHat(ctx, x, y, facing, teamColor) {
    // Little party hat
    ctx.fillStyle = teamColor;
    ctx.beginPath();
    ctx.moveTo(x - 5, y);
    ctx.lineTo(x, y - 12);
    ctx.lineTo(x + 5, y);
    ctx.closePath();
    ctx.fill();
    ctx.strokeStyle = 'rgba(0,0,0,0.3)';
    ctx.lineWidth = 0.5;
    ctx.stroke();
    // Hat brim
    ctx.fillStyle = shadeColor(teamColor, -20);
    ctx.fillRect(x - 6, y - 1, 12, 2);
    // Pom-pom
    ctx.fillStyle = '#ffd700';
    ctx.beginPath();
    ctx.arc(x, y - 12, 2, 0, Math.PI * 2);
    ctx.fill();
}
function drawTrajectory(gs, unit) {
    const ctx = rt.ctx;
    const angle = getAimAngle(gs, unit);
    const rad = -angle * Math.PI / 180;
    const power = gs.powerCharge;
    const baseSpeed = power * 4;
    const vx = Math.cos(rad) * baseSpeed;
    const vy = Math.sin(rad) * baseSpeed;
    ctx.strokeStyle = 'rgba(255, 255, 255, 0.25)';
    ctx.lineWidth = 1.5;
    ctx.setLineDash([6, 6]);
    ctx.beginPath();
    let px = unit.x, py = unit.y - 15;
    const dt = 0.04;
    let pvx = vx, pvy = vy;
    ctx.moveTo(px, py);
    for (let i = 0; i < 100; i++) {
        pvx += gs.wind * dt * 0.1;
        pvy += types_1.GRAVITY * dt;
        px += pvx * dt;
        py += pvy * dt;
        if (px < 0 || px > types_1.WORLD_W || py > types_1.WORLD_H)
            break;
        if ((0, game_1.isUnitInTerrain)(gs, px, py))
            break;
        ctx.lineTo(px, py);
    }
    ctx.stroke();
    ctx.setLineDash([]);
    // Impact dot
    ctx.fillStyle = 'rgba(255, 200, 50, 0.4)';
    ctx.beginPath();
    ctx.arc(px, py, 4, 0, Math.PI * 2);
    ctx.fill();
}
function drawAimLine(gs, unit) {
    const ctx = rt.ctx;
    const angle = getAimAngle(gs, unit);
    const rad = -angle * Math.PI / 180;
    const len = 35 + gs.powerCharge * 0.4;
    ctx.strokeStyle = 'rgba(255, 200, 50, 0.6)';
    ctx.lineWidth = 2.5;
    ctx.beginPath();
    ctx.moveTo(unit.x, unit.y - 15);
    ctx.lineTo(unit.x + Math.cos(rad) * len, unit.y - 15 + Math.sin(rad) * len);
    ctx.stroke();
    // Arrow tip
    const ax = unit.x + Math.cos(rad) * len;
    const ay = unit.y - 15 + Math.sin(rad) * len;
    const tipLen = 6;
    ctx.strokeStyle = 'rgba(255, 200, 50, 0.6)';
    ctx.lineWidth = 2;
    ctx.beginPath();
    ctx.moveTo(ax, ay);
    ctx.lineTo(ax - Math.cos(rad - 0.4) * tipLen, ay - Math.sin(rad - 0.4) * tipLen);
    ctx.moveTo(ax, ay);
    ctx.lineTo(ax - Math.cos(rad + 0.4) * tipLen, ay - Math.sin(rad + 0.4) * tipLen);
    ctx.stroke();
}
function getAimAngle(gs, unit) {
    const dx = gs.mouseX - unit.x;
    const dy = gs.mouseY - (unit.y - 15);
    let angle = Math.atan2(-dy, dx) * 180 / Math.PI;
    return Math.max(0, Math.min(180, angle));
}
function roundRect(ctx, x, y, w, h, r) {
    if (w <= 0)
        return;
    ctx.beginPath();
    ctx.moveTo(x + r, y);
    ctx.lineTo(x + w - r, y);
    ctx.arcTo(x + w, y, x + w, y + r, r);
    ctx.lineTo(x + w, y + h - r);
    ctx.arcTo(x + w, y + h, x + w - r, y + h, r);
    ctx.lineTo(x + r, y + h);
    ctx.arcTo(x, y + h, x, y + h - r, r);
    ctx.lineTo(x, y + r);
    ctx.arcTo(x, y, x + r, y, r);
    ctx.closePath();
}
function shadeColor(color, percent) {
    // Parse hex or rgb color
    const num = parseInt(color.replace('#', ''), 16);
    const r = Math.min(255, Math.max(0, ((num >> 16) & 0xff) + percent));
    const g = Math.min(255, Math.max(0, ((num >> 8) & 0xff) + percent));
    const b = Math.min(255, Math.max(0, (num & 0xff) + percent));
    return `rgb(${r},${g},${b})`;
}
let _hudUnitId = '';
let _hudTurnNum = 0;
function updateHUD(gs, cw, _ch) {
    const unit = gs.units[gs.currentUnitIndex];
    const team = gs.teams[unit?.teamId ?? 0];
    // Rebuild weapons/items when unit or turn changes
    const needsRebuild = unit && (unit.id !== _hudUnitId || gs.turnNumber !== _hudTurnNum);
    if (needsRebuild) {
        _hudUnitId = unit.id;
        _hudTurnNum = gs.turnNumber;
        const wSel = document.getElementById('weapon-selector');
        if (wSel) {
            wSel.innerHTML = '';
            for (const w of gs.items.weapon) {
                const btn = document.createElement('button');
                btn.className = 'weapon-btn' + (w.id === gs.selectedWeapon ? ' selected' : '');
                btn.innerHTML = `<span class="w-icon">${w.icon}</span> ${w.name}`;
                btn.title = w.desc;
                btn.onclick = () => {
                    sound_1.sound.playSelect();
                    gs.selectedWeapon = w.id;
                    wSel.querySelectorAll('.weapon-btn').forEach(b => b.classList.remove('selected'));
                    btn.classList.add('selected');
                };
                wSel.appendChild(btn);
            }
        }
        const iSel = document.getElementById('item-selector');
        if (iSel) {
            iSel.innerHTML = '';
            const item = gs.items.item;
            const btn = document.createElement('button');
            btn.className = 'item-btn';
            btn.innerHTML = `${item.icon} ${item.name}`;
            btn.title = item.desc;
            btn.onclick = () => {
                if (gs.usedItems.includes(item.id))
                    return;
                sound_1.sound.playSelect();
                applyItem(gs, item);
                gs.usedItems.push(item.id);
                btn.classList.add('used');
            };
            iSel.appendChild(btn);
        }
    }
    else {
        // Just update selection highlight
        const wSel = document.getElementById('weapon-selector');
        if (wSel) {
            const btns = wSel.querySelectorAll('.weapon-btn');
            let idx = 0;
            for (const w of gs.items.weapon) {
                if (idx < btns.length) {
                    btns[idx].className = 'weapon-btn' + (w.id === gs.selectedWeapon ? ' selected' : '');
                }
                idx++;
            }
        }
    }
    // Wind
    const windEl = document.getElementById('wind-val');
    if (windEl) {
        const dir = gs.wind > 0 ? '→' : gs.wind < 0 ? '←' : '—';
        windEl.textContent = `🌬 ${dir} ${Math.abs(Math.round(gs.wind))}`;
    }
    // Timer
    const timerBar = document.getElementById('timer-bar');
    const timerText = document.getElementById('timer-text');
    if (timerBar && timerText) {
        const pct = Math.max(0, (gs.turnTimer / types_1.TURN_TIME) * 100);
        timerBar.style.width = `${pct}%`;
        timerBar.style.background = pct > 50
            ? 'linear-gradient(90deg, #2ecc71, #4f4)'
            : pct > 25
                ? 'linear-gradient(90deg, #e67e22, #f39c12)'
                : 'linear-gradient(90deg, #c0392b, #e74c3c)';
        timerText.textContent = `${Math.ceil(Math.max(0, gs.turnTimer))}`;
        const urgency = 1 - gs.turnTimer / types_1.TURN_TIME;
        if (urgency > 0.7) {
            timerBar.parentElement.style.transform = `scale(${1 + (urgency - 0.7) * 1.5})`;
        }
        else {
            timerBar.parentElement.style.transform = 'scale(1)';
        }
    }
    // Turn count
    const turnEl = document.getElementById('turn-count');
    if (turnEl)
        turnEl.textContent = `Turn #${gs.turnNumber}`;
    // Unit info
    if (unit) {
        const nameEl = document.getElementById('unit-name');
        const fillEl = document.getElementById('unit-health-fill');
        const hpEl = document.getElementById('unit-health-text');
        if (nameEl) {
            nameEl.textContent = `${unit.name}`;
            nameEl.style.color = team.color;
        }
        if (fillEl) {
            fillEl.style.width = `${Math.max(0, unit.hp)}%`;
            fillEl.style.background = unit.hp > 60
                ? 'linear-gradient(90deg, #2ecc71, #4f4)'
                : unit.hp > 30
                    ? 'linear-gradient(90deg, #e67e22, #f39c12)'
                    : 'linear-gradient(90deg, #c0392b, #e74c3c)';
        }
        if (hpEl)
            hpEl.textContent = `${Math.round(Math.max(0, unit.hp))}`;
        // Weapon selector
        const wSel = document.getElementById('weapon-selector');
        if (wSel && !wSel.dataset.built) {
            wSel.innerHTML = '';
            for (const w of gs.items.weapon) {
                const btn = document.createElement('button');
                btn.className = 'weapon-btn' + (w.id === gs.selectedWeapon ? ' selected' : '');
                btn.innerHTML = `<span class="w-icon">${w.icon}</span> ${w.name}`;
                btn.title = w.desc;
                btn.onclick = () => {
                    sound_1.sound.playSelect();
                    gs.selectedWeapon = w.id;
                    wSel.querySelectorAll('.weapon-btn').forEach(b => b.classList.remove('selected'));
                    btn.classList.add('selected');
                };
                wSel.appendChild(btn);
            }
            wSel.dataset.built = '1';
        }
        else if (wSel && wSel.dataset.built) {
            // Update selection highlight only
            const btns = wSel.querySelectorAll('.weapon-btn');
            let idx = 0;
            for (const w of gs.items.weapon) {
                if (idx < btns.length) {
                    btns[idx].className = 'weapon-btn' + (w.id === gs.selectedWeapon ? ' selected' : '');
                }
                idx++;
            }
        }
        // Item selector
        const iSel = document.getElementById('item-selector');
        if (iSel && !iSel.dataset.built) {
            iSel.innerHTML = '';
            const item = gs.items.item;
            const btn = document.createElement('button');
            btn.className = 'item-btn';
            btn.innerHTML = `${item.icon} ${item.name}`;
            btn.title = item.desc;
            btn.onclick = () => {
                if (gs.usedItems.includes(item.id))
                    return;
                sound_1.sound.playSelect();
                applyItem(gs, item);
                gs.usedItems.push(item.id);
                btn.classList.add('used');
            };
            iSel.appendChild(btn);
            iSel.dataset.built = '1';
        }
    }
    // Move meter
    const mmFill = document.getElementById('move-meter-fill');
    if (mmFill && unit) {
        const pct = (gs.moveMeter / gs.moveMeterMax) * 100;
        mmFill.style.width = `${pct}%`;
    }
    // Aim info
    if (unit && unit.alive) {
        const angle = getAimAngle(gs, unit);
        const pEl = document.getElementById('power-display');
        const aEl = document.getElementById('angle-display');
        if (pEl)
            pEl.textContent = `Power: ${Math.round(gs.powerCharge)}`;
        if (aEl)
            aEl.textContent = `Angle: ${Math.round(angle)}°`;
    }
}
function applyItem(gs, item) {
    const unit = gs.units[gs.currentUnitIndex];
    if (!unit)
        return;
    switch (item.id) {
        case 'parachute':
            unit.itemActive = 'parachute';
            unit.maxJumps = 99;
            unit.itemTimer = 999;
            break;
        case 'jetpack':
            unit.itemActive = 'jetpack';
            unit.itemTimer = 3;
            break;
        case 'shield':
            unit.invincible = true;
            unit.invincibleTimer = 5;
            break;
        case 'speed':
            unit.itemActive = 'speed';
            unit.itemTimer = gs.turnTimer;
            break;
        case 'grappling':
            unit.itemActive = 'grappling';
            unit.itemTimer = 10;
            unit.maxJumps = 3;
            break;
        case 'extraTime':
            gs.turnTimer = Math.min(gs.turnTimer + 10, types_1.TURN_TIME);
            break;
    }
}
};

// ── module: src/ai.ts ──
__mods["src/ai.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.resetAI = resetAI;
exports.updateAI = updateAI;
const types_1 = require("./types");
const game_1 = require("./game");
let aiDelay = 0;
let aiState = null;
function resetAI() {
    aiState = null;
    aiDelay = 0;
}
function updateAI(dt, gs) {
    const unit = (0, game_1.getCurrentUnit)(gs);
    const team = gs.teams[unit?.teamId ?? 0];
    if (!unit || !unit.alive || !team.isCPU)
        return null;
    if (!aiState || gs.currentUnitIndex !== (aiState._prevIdx ?? -1)) {
        aiState = {
            action: { action: 'wait' },
            timer: 0,
            phase: 'assess',
            targetUnit: null,
            bestAngle: 45,
            bestPower: 70,
            evaluated: false,
            _prevIdx: gs.currentUnitIndex,
        };
        aiDelay = 0.5 + Math.random() * 0.5; // Initial delay
    }
    aiState.timer += dt;
    // Delay for more natural feel
    if (aiDelay > 0) {
        aiDelay -= dt;
        return aiState.action;
    }
    // Phase transitions
    switch (aiState.phase) {
        case 'assess': {
            // Find best target and position
            const target = findBestTarget(gs, unit);
            aiState.targetUnit = target;
            if (target) {
                const bestShot = evaluateShots(gs, unit, target);
                aiState.bestAngle = bestShot.angle;
                aiState.bestPower = bestShot.power;
                aiState.assessedTarget = { x: target.x, y: target.y, teamId: target.teamId };
            }
            // Consider moving to better position
            const shouldMove = shouldAIMove(gs, unit);
            if (shouldMove) {
                aiState.phase = 'move';
                aiState.action = {
                    action: 'move',
                    moveDir: shouldMove,
                };
            }
            else {
                aiState.phase = 'aim';
                aiState.action = { action: 'wait' };
            }
            break;
        }
        case 'move': {
            if (aiState.timer > 1.5) {
                aiState.phase = 'aim';
                aiState.action = { action: 'wait' };
            }
            break;
        }
        case 'aim': {
            // Start firing after assessment
            if (aiState.timer > 0.8) {
                aiState.phase = 'fire';
                // Maybe use an item first
                if (!gs.usedItems.length && Math.random() < 0.3) {
                    // Consider jetpack or speed boots
                    const item = gs.items.item;
                    if (item.id === 'jetpack' || item.id === 'speed') {
                        aiState.action = { action: 'useItem', itemId: item.id };
                        gs.usedItems.push(item.id);
                        break;
                    }
                }
                aiState.action = {
                    action: 'fire',
                    fireAngle: aiState.bestAngle,
                    firePower: aiState.bestPower,
                };
            }
            break;
        }
        case 'fire': {
            // After firing, go idle
            aiState.phase = 'idle';
            aiState.action = { action: 'wait' };
            break;
        }
    }
    // Missile steering
    if (gs.selectedWeapon === 'missile') {
        for (const p of gs.projectiles) {
            if (p.weaponId === 'missile' && p.ownerTeamId === unit.teamId && p.alive) {
                if (aiState.targetUnit && aiState.targetUnit.alive) {
                    aiState.action = {
                        action: 'steerMissile',
                        mouseX: aiState.targetUnit.x,
                        mouseY: aiState.targetUnit.y,
                    };
                }
            }
        }
    }
    return aiState.action;
}
function findBestTarget(gs, unit) {
    let bestTarget = null;
    let bestScore = -Infinity;
    for (const other of gs.units) {
        if (!other.alive || other.teamId === unit.teamId)
            continue;
        const dx = other.x - unit.x;
        const dy = (other.y - 15) - (unit.y - 15);
        const dist = Math.sqrt(dx * dx + dy * dy);
        // Score: closer is better, facing matters
        let score = 1000 - dist * 0.5;
        // Prefer targets that are in cover (harder to hit = more valuable to remove)
        if (!isInCover(gs, other))
            score += 30;
        // Prefer low HP targets (easier to eliminate)
        const finalScore = score + (100 - other.hp) * 0.5;
        if (finalScore > bestScore) {
            bestScore = finalScore;
            bestTarget = other;
        }
    }
    return bestTarget;
}
function isInCover(gs, unit) {
    // Check if terrain blocks the shot
    const surfaceY = (0, game_1.getTerrainSurfaceY)(gs, unit.x);
    return unit.y + types_1.UNIT_RADIUS >= surfaceY - 5;
}
function evaluateShots(gs, shooter, target) {
    let best = { angle: 45, power: 70, score: -Infinity };
    const targetX = target.x;
    const targetY = target.y - 15;
    const startX = shooter.x;
    const startY = shooter.y - 15;
    // Test multiple angle/power combinations
    for (let angle = 15; angle <= 85; angle += 5) {
        for (let power = 20; power <= 100; power += 5) {
            const result = simulateProjectile(startX, startY, angle, power, gs.wind);
            if (result) {
                const dx = result.x - targetX;
                const dy = result.y - targetY;
                const dist = Math.sqrt(dx * dx + dy * dy);
                let evalScore = 1000 - dist;
                // Prefer shots that actually hit terrain near target
                if (result.hitTerrain)
                    evalScore += 50;
                if (evalScore > best.score) {
                    best = { angle, power, score: evalScore };
                }
            }
        }
    }
    // Add some randomness for variety
    best.angle += (Math.random() - 0.5) * 8;
    best.power += (Math.random() - 0.5) * 10;
    best.angle = Math.max(5, Math.min(90, best.angle));
    best.power = Math.max(10, Math.min(100, best.power));
    return best;
}
function simulateProjectile(x, y, angle, power, wind) {
    const rad = -angle * Math.PI / 180;
    const speed = power * 4;
    let vx = Math.cos(rad) * speed;
    let vy = Math.sin(rad) * speed;
    const dt = 0.05;
    for (let i = 0; i < 100; i++) {
        vx += wind * dt * 0.1;
        vy += types_1.GRAVITY * dt;
        x += vx * dt;
        y += vy * dt;
        if (x < 0 || x > types_1.WORLD_W)
            return null;
        if (y > types_1.WORLD_H)
            return { x, y: types_1.WORLD_H, hitTerrain: true };
        // Check terrain (simplified - would need terrain access for accuracy)
        // For now, just return trajectory endpoint
        if (vy > 0 && i > 10) {
            return { x, y, hitTerrain: true };
        }
    }
    return { x, y, hitTerrain: false };
}
function shouldAIMove(gs, unit) {
    const surfaceY = (0, game_1.getTerrainSurfaceY)(gs, unit.x);
    const onGround = unit.y + types_1.UNIT_RADIUS >= surfaceY - 5;
    if (!onGround)
        return 0;
    // Check if near edge
    const leftSafe = isSafePosition(gs, unit.x - 50);
    const rightSafe = isSafePosition(gs, unit.x + 50);
    if (!leftSafe && !rightSafe)
        return 0; // Nowhere safe to go
    // If near left edge, move right
    if (!leftSafe && rightSafe)
        return 1;
    // If near right edge, move left
    if (rightSafe === false && !leftSafe === false)
        return -1;
    // Avoid water
    if (gs.hasWater && unit.y + types_1.UNIT_RADIUS > gs.waterLevel - 20) {
        // Move to higher ground
        const leftHeight = (0, game_1.getTerrainSurfaceY)(gs, unit.x - 100);
        const rightHeight = (0, game_1.getTerrainSurfaceY)(gs, unit.x + 100);
        if (leftHeight < rightHeight)
            return -1;
        return 1;
    }
    return 0;
}
function isSafePosition(gs, x) {
    const surfaceY = (0, game_1.getTerrainSurfaceY)(gs, x);
    if (surfaceY > types_1.WORLD_H - 20)
        return false; // Edge of world
    if (gs.hasWater && surfaceY > gs.waterLevel)
        return false;
    return true;
}
};

// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"Qwen3.6-27B-MTP-GGUF","provider":"brigid","persona":"typescript","sessionId":"2d3e6717-cf75-4719-862c-cbc844418483","tokensIn":4001844,"tokensOut":66407,"tokensTotal":4068251,"cost":0,"turns":73,"toolCalls":72,"failedToolCalls":2,"timestamp":"2026-08-02T12:28:09.962Z"} -->