← Worms results

Worms

thinkingcap qwen3.6 27b · 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 121,868 bytes · SHA-256 236e01c11c94
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Worms Artillery Battle</title>
<style>
/* ── css: layout ── */
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { 
  width: 100%; height: 100%; 
  overflow: hidden; 
  background: #0a0a1a;
  font-family: Arial, sans-serif;
}
#app {
  width: 100vw;
  height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
}
canvas {
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
  image-rendering: pixelated;
  cursor: crosshair;
}
</style>
</head>
<body>
<div id="app"></div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./game":"src/game.ts","./rendering":"src/rendering.ts"},"src/terrain.ts":{"./types":"src/types.ts"},"src/physics.ts":{"./types":"src/types.ts","./terrain":"src/terrain.ts"},"src/weapons.ts":{"./types":"src/types.ts"},"src/particles.ts":{"./types":"src/types.ts"},"src/ai.ts":{"./types":"src/types.ts","./physics":"src/physics.ts","./terrain":"src/terrain.ts","./weapons":"src/weapons.ts"},"src/camera.ts":{"./types":"src/types.ts"},"src/game.ts":{"./types":"src/types.ts","./terrain":"src/terrain.ts","./physics":"src/physics.ts","./weapons":"src/weapons.ts","./particles":"src/particles.ts","./sounds":"src/sounds.ts","./ai":"src/ai.ts","./camera":"src/camera.ts"},"src/rendering.ts":{"./game":"src/game.ts","./types":"src/types.ts","./weapons":"src/weapons.ts","./physics":"src/physics.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 game_1 = require("./game");
const rendering_1 = require("./rendering");
let game;
let renderer;
let lastTime = 0;
let animFrameId;
function start() {
    // Setup canvas
    const app = document.getElementById('app');
    if (!app)
        return;
    const canvas = document.createElement('canvas');
    canvas.width = 1280;
    canvas.height = 720;
    canvas.style.display = 'block';
    app.appendChild(canvas);
    // Initialize game
    game = new game_1.Game();
    game.canvas = canvas;
    game.ctx = canvas.getContext('2d');
    renderer = new rendering_1.Renderer(game);
    game.init();
    // Handle clicks on canvas
    canvas.addEventListener('click', (e) => {
        const rect = canvas.getBoundingClientRect();
        const scaleX = canvas.width / rect.width;
        const scaleY = canvas.height / rect.height;
        const mx = (e.clientX - rect.left) * scaleX;
        const my = (e.clientY - rect.top) * scaleY;
        if (game.phase === 'menu') {
            renderer.handleMenuClick(mx, my);
        }
        else if (game.phase === 'gameOver') {
            renderer.handleGameOverClick(mx, my);
        }
        else if (game.phase === 'weaponSelect') {
            renderer.handleWeaponPanelClick(mx, my);
        }
    });
    // Handle scroll for power adjustment
    canvas.addEventListener('wheel', (e) => {
        e.preventDefault();
        renderer.handlePowerScroll(e.deltaY);
    }, { passive: false });
    // Start game loop
    requestAnimationFrame(gameLoop);
}
function gameLoop(timestamp) {
    const dt = Math.min((timestamp - lastTime) / 16.67, 3); // Cap at ~3 frames worth of physics
    lastTime = timestamp;
    // Update
    game.update(dt);
    // Render
    renderer.render();
    animFrameId = requestAnimationFrame(gameLoop);
}
// Auto-start when loaded
start();
};

// ── module: src/types.ts ──
__mods["src/types.ts"] = function (exports, require, module) {
"use strict";
// Core type definitions for Worms Artillery Battle
Object.defineProperty(exports, "__esModule", { value: true });
exports.WORM_NAMES = exports.TEAM_NAMES = exports.TEAM_COLORS = void 0;
exports.TEAM_COLORS = ['#FF4444', '#44AAFF', '#44FF44', '#FFAA00'];
exports.TEAM_NAMES = ['Red Team', 'Blue Team', 'Green Team', 'Orange Team'];
exports.WORM_NAMES = [
    'Sgt. Blast', 'Lt. Boom', 'Cpl. Crunch', 'Pvt. Pop',
    'Maj. Mortar', 'Capt. Cannon', 'Gen. Grenade', 'Col. Charge',
    'Sgt. Spark', 'Lt. Lightning', 'Cpl. Crash', 'Pvt. Powder',
    'Maj. Missile', 'Capt. Cluster', 'Gen. Gunpowder', 'Col. Combustion'
];
};

// ── module: src/terrain.ts ──
__mods["src/terrain.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Terrain = void 0;
// Terrain is stored as a 2D grid: 0 = air, 1+ = solid (with color variation)
class Terrain {
    constructor(width, height) {
        this.imageData = null;
        this.width = width;
        this.height = height;
        this.grid = new Uint8Array(width * height);
    }
    get(x, y) {
        if (x < 0 || x >= this.width || y < 0 || y >= this.height)
            return 1; // edges are solid
        return this.grid[y * this.width + x];
    }
    set(x, y, val) {
        if (x < 0 || x >= this.width || y < 0 || y >= this.height)
            return;
        this.grid[y * this.width + x] = val;
    }
    isSolid(x, y) {
        return this.get(Math.floor(x), Math.floor(y)) > 0;
    }
    // Generate terrain based on shape type
    generate(shape) {
        const w = this.width;
        const h = this.height;
        this.grid.fill(0);
        switch (shape) {
            case 'island':
                this.generateIsland();
                break;
            case 'cave':
                this.generateCave();
                break;
            case 'twoIslands':
                this.generateTwoIslands();
                break;
        }
        // Fill edges as solid border
        for (let x = 0; x < w; x++) {
            this.set(x, 0, 2);
            this.set(x, h - 1, 2);
        }
        for (let y = 0; y < h; y++) {
            this.set(0, y, 2);
            this.set(w - 1, y, 2);
        }
    }
    generateIsland() {
        const w = this.width;
        const h = this.height;
        const baseY = Math.floor(h * 0.45);
        // Multi-octave noise for terrain height
        const heights = [];
        for (let x = 0; x < w; x++) {
            let val = 0;
            val += Math.sin(x * 0.008) * 60;
            val += Math.sin(x * 0.02 + 1.3) * 30;
            val += Math.sin(x * 0.05 + 2.7) * 15;
            val += Math.sin(x * 0.1 + 4.1) * 8;
            heights[x] = baseY - val;
        }
        // Fill terrain below height curve
        for (let x = 0; x < w; x++) {
            const surfaceY = Math.floor(heights[x]);
            for (let y = surfaceY; y < h; y++) {
                if (y === surfaceY)
                    this.set(x, y, 1); // grass layer
                else if (y < surfaceY + 8)
                    this.set(x, y, 3); // dirt
                else
                    this.set(x, y, 4); // rock
            }
        }
        // Smooth terrain edges
        for (let pass = 0; pass < 2; pass++) {
            const newGrid = new Uint8Array(this.grid);
            for (let x = 1; x < w - 1; x++) {
                for (let y = 1; y < h - 1; y++) {
                    if (this.get(x, y) === 0) {
                        let solidCount = 0;
                        for (let dy = -1; dy <= 1; dy++)
                            for (let dx = -1; dx <= 1; dx++)
                                if (this.get(x + dx, y + dy) > 0)
                                    solidCount++;
                        if (solidCount >= 5)
                            newGrid[y * w + x] = this.get(x, y + 1) || 3;
                    }
                }
            }
            this.grid.set(newGrid);
        }
    }
    generateCave() {
        const w = this.width;
        const h = this.height;
        // Fill everything solid first
        for (let y = 0; y < h; y++)
            for (let x = 0; x < w; x++)
                this.set(x, y, 4);
        // Carve out cave using noise threshold
        const carveY = Math.floor(h * 0.5);
        const caveHeight = Math.floor(h * 0.35);
        for (let x = 1; x < w - 1; x++) {
            let offset = 0;
            offset += Math.sin(x * 0.01) * 40;
            offset += Math.sin(x * 0.025 + 2) * 25;
            offset += Math.sin(x * 0.06 + 3) * 12;
            const topY = carveY - caveHeight / 2 + offset;
            const bottomY = carveY + caveHeight / 2 + offset;
            for (let y = Math.floor(topY); y < Math.floor(bottomY); y++) {
                this.set(x, y, 0);
            }
        }
        // Add some stalactites/stalagmites
        for (let x = 5; x < w - 5; x += Math.floor(Math.random() * 30) + 15) {
            const len = Math.floor(Math.random() * 40) + 10;
            let offset = 0;
            offset += Math.sin(x * 0.01) * 40;
            offset += Math.sin(x * 0.025 + 2) * 25;
            // Stalactite from ceiling
            const ceilY = Math.floor(carveY - caveHeight / 2 + offset);
            for (let y = ceilY; y < ceilY + len && y < h; y++) {
                if (this.get(x, y) === 0)
                    this.set(x, y, 4);
            }
            // Stalagmite from floor
            const floorY = Math.floor(carveY + caveHeight / 2 + offset);
            for (let y = floorY; y > floorY - len && y >= 0; y--) {
                if (this.get(x, y) === 0)
                    this.set(x, y, 4);
            }
        }
        // Smooth
        for (let pass = 0; pass < 3; pass++) {
            const newGrid = new Uint8Array(this.grid);
            for (let x = 1; x < w - 1; x++) {
                for (let y = 1; y < h - 1; y++) {
                    if (this.get(x, y) === 0) {
                        let solidCount = 0;
                        for (let dy = -1; dy <= 1; dy++)
                            for (let dx = -1; dx <= 1; dx++)
                                if (this.get(x + dx, y + dy) > 0)
                                    solidCount++;
                        if (solidCount >= 6)
                            newGrid[y * w + x] = 4;
                    }
                    else {
                        let airCount = 0;
                        for (let dy = -1; dy <= 1; dy++)
                            for (let dx = -1; dx <= 1; dx++)
                                if (this.get(x + dx, y + dy) === 0)
                                    airCount++;
                        if (airCount >= 6)
                            newGrid[y * w + x] = 0;
                    }
                }
            }
            this.grid.set(newGrid);
        }
        // Add grass on top surfaces
        for (let x = 1; x < w - 1; x++) {
            for (let y = 1; y < h - 1; y++) {
                if (this.get(x, y) === 4 && this.get(x, y - 1) === 0) {
                    this.set(x, y, 1);
                }
            }
        }
    }
    generateTwoIslands() {
        const w = this.width;
        const h = this.height;
        // Generate two separate island profiles
        for (let x = 0; x < w; x++) {
            let val1 = 0, val2 = 0;
            // Island 1 (left)
            const center1 = w * 0.3;
            const dist1 = Math.abs(x - center1);
            if (dist1 < w * 0.25) {
                val1 += Math.sin((x - center1) * 0.015 + 1) * 40;
                val1 += Math.sin((x - center1) * 0.04 + 2) * 20;
                val1 *= (1 - dist1 / (w * 0.25)); // taper edges
            }
            // Island 2 (right)
            const center2 = w * 0.7;
            const dist2 = Math.abs(x - center2);
            if (dist2 < w * 0.25) {
                val2 += Math.sin((x - center2) * 0.015 + 3) * 40;
                val2 += Math.sin((x - center2) * 0.04 + 4) * 20;
                val2 *= (1 - dist2 / (w * 0.25)); // taper edges
            }
            const baseY = h * 0.5;
            const surfaceY = Math.floor(baseY - Math.max(val1, val2));
            for (let y = surfaceY; y < h; y++) {
                if (y === surfaceY)
                    this.set(x, y, 1);
                else if (y < surfaceY + 8)
                    this.set(x, y, 3);
                else
                    this.set(x, y, 4);
            }
        }
        // Smooth edges
        for (let pass = 0; pass < 2; pass++) {
            const newGrid = new Uint8Array(this.grid);
            for (let x = 1; x < w - 1; x++) {
                for (let y = 1; y < h - 1; y++) {
                    if (this.get(x, y) === 0) {
                        let solidCount = 0;
                        for (let dy = -1; dy <= 1; dy++)
                            for (let dx = -1; dx <= 1; dx++)
                                if (this.get(x + dx, y + dy) > 0)
                                    solidCount++;
                        if (solidCount >= 5)
                            newGrid[y * w + x] = this.get(x, y + 1) || 3;
                    }
                }
            }
            this.grid.set(newGrid);
        }
    }
    // Destroy terrain in a circular area - returns destroyed pixels for debris
    destroy(cx, cy, radius) {
        const debris = [];
        const r2 = radius * radius;
        for (let dy = -Math.ceil(radius); dy <= Math.ceil(radius); dy++) {
            for (let dx = -Math.ceil(radius); dx <= Math.ceil(radius); dx++) {
                if (dx * dx + dy * dy > r2)
                    continue;
                const px = Math.floor(cx + dx);
                const py = Math.floor(cy + dy);
                if (this.get(px, py) > 0) {
                    debris.push({ x: px, y: py, type: this.get(px, py) });
                    this.set(px, py, 0);
                }
            }
        }
        return debris;
    }
    // Find surface Y at given X position (for unit placement)
    findSurfaceY(x) {
        const fx = Math.floor(Math.max(1, Math.min(this.width - 2, x)));
        for (let y = 0; y < this.height; y++) {
            if (this.get(fx, y) > 0)
                return y;
        }
        return this.height - 1;
    }
    // Find a safe spot near X position (not in water, on solid ground)
    findSafeSpot(x, waterLevel) {
        const fx = Math.floor(Math.max(5, Math.min(this.width - 6, x)));
        for (let y = 0; y < this.height; y++) {
            if (this.get(fx, y) > 0 && y < waterLevel - 2) {
                // Check it's a flat surface
                let solidLeft = false, solidRight = false;
                for (let dy = 1; dy <= 3; dy++) {
                    if (this.get(fx - 2, y + dy) > 0)
                        solidLeft = true;
                    if (this.get(fx + 2, y + dy) > 0)
                        solidRight = true;
                }
                if (solidLeft && solidRight)
                    return { x: fx, y: y - 1 };
            }
        }
        // Fallback: just find any surface above water
        for (let y = 0; y < waterLevel - 2; y++) {
            if (this.get(fx, y) > 0)
                return { x: fx, y: y - 1 };
        }
        return null;
    }
    // Get terrain color for rendering with variation
    static getTerrainColor(type, x = 0, y = 0) {
        const noise = ((x * 7 + y * 13) % 20) - 10;
        switch (type) {
            case 1: return `rgb(${60 + noise},${140 + noise},${55 + noise})`; // grass with variation
            case 2: return '#333333'; // border
            case 3: return `rgb(${139 + noise},${105 + noise},${20 + noise / 2})`; // dirt
            case 4: return `rgb(${100 + noise},${100 + noise},${100 + noise})`; // rock
            default: return 'transparent';
        }
    }
    // Render terrain to canvas context at given scale
    render(ctx, scale) {
        const w = this.width;
        const h = this.height;
        for (let y = 0; y < h; y++) {
            let currentType = -1;
            let startX = 0;
            for (let x = 0; x <= w; x++) {
                const type = x < w ? this.get(x, y) : -1;
                if (type !== currentType) {
                    if (currentType > 0 && x - startX >= 2) {
                        ctx.fillStyle = Terrain.getTerrainColor(currentType, Math.floor((startX + x) / 2), y);
                        ctx.fillRect(startX * scale, y * scale, (x - startX) * scale, scale);
                    }
                    currentType = type;
                    startX = x;
                }
            }
        }
    }
    // Apply gravity to loose terrain pixels after destruction
    settleGravity() {
        const w = this.width;
        const h = this.height;
        let changed = true;
        while (changed) {
            changed = false;
            for (let y = h - 2; y >= 0; y--) {
                for (let x = 1; x < w - 1; x++) {
                    const val = this.get(x, y);
                    if (val > 0 && this.get(x, y + 1) === 0) {
                        // Check if supported on sides
                        const leftSupport = this.get(x - 1, y) > 0 || this.get(x - 1, y + 1) > 0;
                        const rightSupport = this.get(x + 1, y) > 0 || this.get(x + 1, y + 1) > 0;
                        if (!leftSupport && !rightSupport) {
                            this.set(x, y + 1, val);
                            this.set(x, y, 0);
                            changed = true;
                        }
                        else if (val !== 4) { // rock doesn't fall as easily
                            const leftSolid = this.get(x - 1, y) > 0;
                            const rightSolid = this.get(x + 1, y) > 0;
                            if (!leftSolid && !rightSolid) {
                                this.set(x, y + 1, val);
                                this.set(x, y, 0);
                                changed = true;
                            }
                        }
                    }
                }
            }
        }
    }
}
exports.Terrain = Terrain;
};

// ── module: src/physics.ts ──
__mods["src/physics.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Physics = exports.FALL_DAMAGE_FACTOR = exports.MAX_FALL_DAMAGE_HEIGHT = exports.MOVE_SPEED = exports.JUMP_FORCE = exports.UNIT_RADIUS = exports.GRAVITY = void 0;
exports.GRAVITY = 0.35;
exports.UNIT_RADIUS = 6;
exports.JUMP_FORCE = -7;
exports.MOVE_SPEED = 2.5;
exports.MAX_FALL_DAMAGE_HEIGHT = 120; // pixels of fall before damage starts
exports.FALL_DAMAGE_FACTOR = 0.4;
class Physics {
    constructor(terrain) {
        this.terrain = terrain;
    }
    updateUnit(unit, dt, wind, waterLevel) {
        if (!unit.alive)
            return false;
        // Apply gravity
        unit.vel.y += exports.GRAVITY * dt;
        // Wind effect on airborne units (slight push)
        if (!unit.onGround) {
            unit.vel.x += wind * 0.01 * dt;
        }
        // Move X
        let newX = unit.pos.x + unit.vel.x * dt;
        const px = Math.floor(newX);
        const py = Math.floor(unit.pos.y);
        if (this.terrain.isSolid(px, py)) {
            // Try to slide along wall
            for (let step = 1; step <= Math.abs(Math.round(unit.vel.x * dt)); step++) {
                const testX = unit.pos.x + Math.sign(unit.vel.x) * step;
                if (!this.terrain.isSolid(Math.floor(testX), py)) {
                    newX = testX;
                    break;
                }
            }
            unit.vel.x = 0;
        }
        // Move Y
        let newY = unit.pos.y + unit.vel.y * dt;
        const newPx = Math.floor(newX);
        const newPy = Math.floor(newY);
        if (this.terrain.isSolid(newPx, newPy)) {
            if (unit.vel.y > 0) {
                // Landing - check for fall damage
                const fallDist = unit.pos.y - unit.fallStartY;
                if (fallDist > exports.MAX_FALL_DAMAGE_HEIGHT && !unit.inWater) {
                    const dmg = Math.floor((fallDist - exports.MAX_FALL_DAMAGE_HEIGHT) * exports.FALL_DAMAGE_FACTOR);
                    unit.hp -= dmg;
                    if (dmg > 0)
                        return true; // had fall damage
                }
                // Snap to surface
                while (this.terrain.isSolid(newPx, Math.floor(newY)) && newY > 0) {
                    newY--;
                }
            }
            else {
                // Hit ceiling
                while (this.terrain.isSolid(newPx, Math.floor(newY)) && newY < this.terrain.height - 1) {
                    newY++;
                }
            }
            unit.vel.y = 0;
            unit.onGround = true;
            unit.jumpCount = 0;
        }
        else {
            if (unit.onGround && unit.vel.y > 0) {
                unit.fallStartY = unit.pos.y;
            }
            unit.onGround = false;
        }
        // Water check
        const wasInWater = unit.inWater;
        unit.inWater = newY >= waterLevel - exports.UNIT_RADIUS;
        if (unit.inWater && !wasInWater) {
            // Just entered water - slow down
            unit.vel.y *= 0.3;
        }
        if (!unit.onGround && !this.terrain.isSolid(newPx, newPy)) {
            // Check if floating on water
            if (newY >= waterLevel - exports.UNIT_RADIUS) {
                newY = waterLevel - exports.UNIT_RADIUS - 1;
                unit.vel.y = 0;
                unit.onGround = true;
                unit.jumpCount = 0;
            }
        }
        unit.pos.x = newX;
        unit.pos.y = newY;
        // Keep in bounds
        unit.pos.x = Math.max(exports.UNIT_RADIUS, Math.min(this.terrain.width - exports.UNIT_RADIUS, unit.pos.x));
        unit.pos.y = Math.max(exports.UNIT_RADIUS, Math.min(this.terrain.height - exports.UNIT_RADIUS, unit.pos.y));
        return false;
    }
    updateProjectile(proj, dt) {
        if (!proj.alive)
            return false;
        proj.vel.y += proj.gravity * dt;
        const newX = proj.pos.x + proj.vel.x * dt;
        const newY = proj.pos.y + proj.vel.y * dt;
        // Trail
        proj.trail.push({ x: proj.pos.x, y: proj.pos.y });
        if (proj.trail.length > 20)
            proj.trail.shift();
        // Terrain collision
        if (this.terrain.isSolid(Math.floor(newX), Math.floor(newY))) {
            proj.pos.x = newX;
            proj.pos.y = newY;
            return true; // hit terrain
        }
        // Out of bounds
        if (newX < 0 || newX >= this.terrain.width || newY < 0 || newY >= this.terrain.height) {
            return true; // out of bounds - explode anyway
        }
        proj.pos.x = newX;
        proj.pos.y = newY;
        return false;
    }
    checkUnitCollision(unit, proj) {
        if (!unit.alive || !proj.alive)
            return false;
        const dx = unit.pos.x - proj.pos.x;
        const dy = unit.pos.y - proj.pos.y;
        return (dx * dx + dy * dy) < (exports.UNIT_RADIUS + 3) * (exports.UNIT_RADIUS + 3);
    }
    // Check if position is safe from environmental hazards
    assessRisk(x, y, waterLevel) {
        const risks = [];
        let totalRisk = 0;
        // Water proximity
        const distToWater = Math.max(0, waterLevel - y);
        if (distToWater < 30) {
            totalRisk += (1 - distToWater / 30) * 50;
            risks.push('near_water');
        }
        // Cliff edge check
        const fx = Math.floor(x);
        let groundBelow = false;
        for (let dy = 1; dy <= 20; dy++) {
            if (this.terrain.isSolid(fx, Math.floor(y) + dy)) {
                groundBelow = true;
                break;
            }
        }
        if (!groundBelow) {
            totalRisk += 40;
            risks.push('no_ground');
        }
        // Check for overhangs (cave danger)
        let airAbove = false;
        for (let dy = -1; dy >= -15; dy--) {
            if (!this.terrain.isSolid(fx, Math.floor(y) + dy)) {
                airAbove = true;
                break;
            }
        }
        return { risk: totalRisk, reasons: risks };
    }
}
exports.Physics = Physics;
};

// ── module: src/weapons.ts ──
__mods["src/weapons.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.WEAPONS = void 0;
exports.getWeaponById = getWeaponById;
exports.createAmmoPool = createAmmoPool;
exports.WEAPONS = [
    {
        id: 'rocket', name: 'Rocket', icon: '🚀', category: 'indirect',
        damage: 35, aoeRadius: 25, power: 10, ammo: 4,
        description: 'Classic rocket with moderate AoE'
    },
    {
        id: 'dynamite', name: 'Dynamite', icon: '🧨', category: 'indirect',
        damage: 50, aoeRadius: 30, power: 8, ammo: 2,
        description: 'Timed explosive - click to detonate'
    },
    {
        id: 'homing_missile', name: 'Homing Missile', icon: '🎯', category: 'indirect',
        damage: 40, aoeRadius: 20, power: 9, ammo: 2,
        description: 'Locks onto nearest enemy'
    },
    {
        id: 'minigun', name: 'Minigun', icon: '🔫', category: 'direct',
        damage: 3, aoeRadius: 5, power: 14, ammo: 60,
        description: 'Rapid fire direct hits'
    },
    {
        id: 'super_size', name: 'Super Size', icon: '💊', category: 'direct',
        damage: 0, aoeRadius: 0, power: 0, ammo: 1,
        description: 'Grow to giant size (trample enemies!)'
    },
    {
        id: 'cluster_bomb', name: 'Cluster Bomb', icon: '💣', category: 'aoe',
        damage: 25, aoeRadius: 35, power: 11, ammo: 1,
        description: 'Splits into sub-munitions mid-air'
    },
    {
        id: 'quake_grenade', name: 'Quake Grenade', icon: '🌋', category: 'aoe',
        damage: 20, aoeRadius: 60, power: 5, ammo: 1,
        description: 'Massive AoE terrain destruction'
    },
    {
        id: 'bananapower', name: 'Banana Power', icon: '🍌', category: 'direct',
        damage: 0, aoeRadius: 0, power: 0, ammo: 1,
        description: '+50% damage for this turn'
    },
];
function getWeaponById(id) {
    return exports.WEAPONS.find(w => w.id === id);
}
function createAmmoPool() {
    const pool = new Map();
    for (const w of exports.WEAPONS)
        pool.set(w.id, w.ammo);
    return pool;
}
};

// ── module: src/particles.ts ──
__mods["src/particles.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ParticleSystem = void 0;
class ParticleSystem {
    constructor() {
        this.particles = [];
        this.floatingTexts = [];
    }
    update(dt) {
        for (const p of this.particles) {
            p.x += p.vx * dt;
            p.y += p.vy * dt;
            if (p.type === 'debris' || p.type === 'spark') {
                p.vy += 0.2 * dt; // gravity on debris
            }
            p.life -= dt;
        }
        this.particles = this.particles.filter(p => p.life > 0);
        for (const ft of this.floatingTexts) {
            ft.y += ft.vy * dt;
            ft.life -= dt;
        }
        this.floatingTexts = this.floatingTexts.filter(ft => ft.life > 0);
    }
    spawnExplosion(x, y, radius) {
        const count = Math.floor(radius * 1.5);
        // Fire particles
        for (let i = 0; i < count; i++) {
            const angle = Math.random() * Math.PI * 2;
            const speed = Math.random() * radius * 0.3 + 1;
            this.particles.push({
                x, y,
                vx: Math.cos(angle) * speed,
                vy: Math.sin(angle) * speed - 1,
                life: 30 + Math.random() * 20,
                maxLife: 50,
                color: ['#FF4400', '#FF8800', '#FFCC00', '#FFFF00'][Math.floor(Math.random() * 4)],
                size: Math.random() * radius * 0.15 + 2,
                type: 'explosion'
            });
        }
        // Smoke particles
        for (let i = 0; i < count / 3; i++) {
            const angle = Math.random() * Math.PI * 2;
            const speed = Math.random() * radius * 0.15 + 0.5;
            this.particles.push({
                x, y,
                vx: Math.cos(angle) * speed,
                vy: -Math.random() * 2 - 0.5,
                life: 40 + Math.random() * 30,
                maxLife: 70,
                color: `rgba(80,80,80,${0.3 + Math.random() * 0.4})`,
                size: Math.random() * radius * 0.2 + 5,
                type: 'smoke'
            });
        }
        // Sparks
        for (let i = 0; i < count / 2; i++) {
            const angle = Math.random() * Math.PI * 2;
            const speed = Math.random() * radius * 0.5 + 3;
            this.particles.push({
                x, y,
                vx: Math.cos(angle) * speed,
                vy: Math.sin(angle) * speed - 2,
                life: 15 + Math.random() * 15,
                maxLife: 30,
                color: '#FFFFFF',
                size: 1 + Math.random() * 2,
                type: 'spark'
            });
        }
    }
    spawnDebris(x, y, count) {
        for (let i = 0; i < count; i++) {
            const angle = Math.random() * Math.PI * 2;
            const speed = Math.random() * 4 + 1;
            this.particles.push({
                x: x + (Math.random() - 0.5) * 10,
                y: y + (Math.random() - 0.5) * 10,
                vx: Math.cos(angle) * speed,
                vy: Math.sin(angle) * speed - 3,
                life: 40 + Math.random() * 20,
                maxLife: 60,
                color: ['#8B6914', '#666666', '#5a4a2a'][Math.floor(Math.random() * 3)],
                size: 2 + Math.random() * 4,
                type: 'debris'
            });
        }
    }
    spawnDust(x, y) {
        for (let i = 0; i < 8; i++) {
            this.particles.push({
                x: x + (Math.random() - 0.5) * 10,
                y,
                vx: (Math.random() - 0.5) * 2,
                vy: -Math.random() * 1.5,
                life: 20 + Math.random() * 10,
                maxLife: 30,
                color: 'rgba(180,160,120,0.6)',
                size: 2 + Math.random() * 3,
                type: 'dust'
            });
        }
    }
    spawnWaterSplash(x, y) {
        for (let i = 0; i < 15; i++) {
            const angle = -Math.PI + Math.random() * Math.PI; // upward arc
            const speed = Math.random() * 4 + 2;
            this.particles.push({
                x, y,
                vx: Math.cos(angle) * speed,
                vy: Math.sin(angle) * speed,
                life: 20 + Math.random() * 15,
                maxLife: 35,
                color: '#4488FF',
                size: 2 + Math.random() * 3,
                type: 'water'
            });
        }
    }
    addFloatingText(x, y, text, color = '#FFFFFF', size = 16) {
        this.floatingTexts.push({
            x, y, text,
            life: 60, maxLife: 60,
            color, size,
            vy: -1.5
        });
    }
    get particlesList() { return this.particles; }
    get floatingTextsList() { return this.floatingTexts; }
    render(ctx) {
        // Render particles
        for (const p of this.particles) {
            const alpha = Math.max(0, p.life / p.maxLife);
            ctx.globalAlpha = alpha;
            if (p.type === 'smoke') {
                ctx.fillStyle = p.color;
                ctx.beginPath();
                ctx.arc(p.x, p.y, p.size * (1 + (1 - alpha) * 0.5), 0, Math.PI * 2);
                ctx.fill();
            }
            else if (p.type === 'spark') {
                ctx.fillStyle = p.color;
                ctx.fillRect(p.x - p.size / 2, p.y - p.size / 2, p.size, p.size);
            }
            else {
                ctx.fillStyle = p.color;
                ctx.beginPath();
                ctx.arc(p.x, p.y, p.size * alpha, 0, Math.PI * 2);
                ctx.fill();
            }
        }
        ctx.globalAlpha = 1;
        // Render floating texts
        for (const ft of this.floatingTexts) {
            const alpha = Math.max(0, ft.life / ft.maxLife);
            ctx.globalAlpha = alpha;
            ctx.font = `bold ${ft.size}px Arial`;
            ctx.textAlign = 'center';
            // Shadow
            ctx.fillStyle = '#000';
            ctx.fillText(ft.text, ft.x + 1, ft.y + 1);
            // Text
            ctx.fillStyle = ft.color;
            ctx.fillText(ft.text, ft.x, ft.y);
        }
        ctx.globalAlpha = 1;
    }
}
exports.ParticleSystem = ParticleSystem;
};

// ── module: src/sounds.ts ──
__mods["src/sounds.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.SoundSystem = void 0;
// Procedural sound effects using Web Audio API
class SoundSystem {
    constructor() {
        this.ctx = null;
        this.masterGain = null;
    }
    init() {
        try {
            this.ctx = new (window.AudioContext || window.webkitAudioContext)();
            this.masterGain = this.ctx.createGain();
            this.masterGain.gain.value = 0.3;
            this.masterGain.connect(this.ctx.destination);
        }
        catch (e) {
            console.warn('Web Audio not available');
        }
    }
    resume() {
        if (this.ctx?.state === 'suspended')
            this.ctx.resume();
    }
    createOsc(type, freq, duration, volume = 0.3) {
        if (!this.ctx || !this.masterGain)
            return;
        const osc = this.ctx.createOscillator();
        const gain = this.ctx.createGain();
        osc.type = type;
        osc.frequency.setValueAtTime(freq, this.ctx.currentTime);
        gain.gain.setValueAtTime(volume, this.ctx.currentTime);
        gain.gain.exponentialRampToValueAtTime(0.01, this.ctx.currentTime + duration);
        osc.connect(gain);
        gain.connect(this.masterGain);
        osc.start();
        osc.stop(this.ctx.currentTime + duration);
    }
    createNoise(duration, volume = 0.2) {
        if (!this.ctx || !this.masterGain)
            return;
        const bufferSize = this.ctx.sampleRate * duration;
        const buffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate);
        const data = buffer.getChannelData(0);
        for (let i = 0; i < bufferSize; i++) {
            data[i] = (Math.random() * 2 - 1) * volume;
        }
        const source = this.ctx.createBufferSource();
        source.buffer = buffer;
        const gain = this.ctx.createGain();
        gain.gain.setValueAtTime(volume, this.ctx.currentTime);
        gain.gain.exponentialRampToValueAtTime(0.01, this.ctx.currentTime + duration);
        // Low-pass filter for explosion sound
        const filter = this.ctx.createBiquadFilter();
        filter.type = 'lowpass';
        filter.frequency.setValueAtTime(2000, this.ctx.currentTime);
        filter.frequency.exponentialRampToValueAtTime(100, this.ctx.currentTime + duration);
        source.connect(filter);
        filter.connect(gain);
        gain.connect(this.masterGain);
        source.start();
    }
    playExplosion(size) {
        const dur = size === 'large' ? 0.6 : size === 'medium' ? 0.4 : 0.2;
        this.createNoise(dur, size === 'large' ? 0.5 : 0.3);
        // Add a low boom
        this.createOsc('sine', size === 'large' ? 60 : 100, dur * 0.8, 0.4);
        if (size !== 'small') {
            setTimeout(() => this.createNoise(dur * 0.5, 0.2), 50);
        }
    }
    playLaunch() {
        if (!this.ctx || !this.masterGain)
            return;
        const osc = this.ctx.createOscillator();
        const gain = this.ctx.createGain();
        osc.type = 'sawtooth';
        osc.frequency.setValueAtTime(300, this.ctx.currentTime);
        osc.frequency.exponentialRampToValueAtTime(150, this.ctx.currentTime + 0.3);
        gain.gain.setValueAtTime(0.15, this.ctx.currentTime);
        gain.gain.exponentialRampToValueAtTime(0.01, this.ctx.currentTime + 0.3);
        osc.connect(gain);
        gain.connect(this.masterGain);
        osc.start();
        osc.stop(this.ctx.currentTime + 0.3);
    }
    playHit() {
        this.createOsc('square', 200, 0.1, 0.2);
        this.createNoise(0.08, 0.15);
    }
    playJump() {
        if (!this.ctx || !this.masterGain)
            return;
        const osc = this.ctx.createOscillator();
        const gain = this.ctx.createGain();
        osc.type = 'sine';
        osc.frequency.setValueAtTime(300, this.ctx.currentTime);
        osc.frequency.exponentialRampToValueAtTime(500, this.ctx.currentTime + 0.1);
        gain.gain.setValueAtTime(0.15, this.ctx.currentTime);
        gain.gain.exponentialRampToValueAtTime(0.01, this.ctx.currentTime + 0.15);
        osc.connect(gain);
        gain.connect(this.masterGain);
        osc.start();
        osc.stop(this.ctx.currentTime + 0.15);
    }
    playMenuClick() {
        this.createOsc('sine', 800, 0.05, 0.2);
    }
    playMenuHover() {
        this.createOsc('sine', 600, 0.03, 0.1);
    }
    playTurnStart() {
        if (!this.ctx || !this.masterGain)
            return;
        const notes = [523, 659, 784]; // C E G
        notes.forEach((freq, i) => {
            setTimeout(() => this.createOsc('sine', freq, 0.15, 0.2), i * 80);
        });
    }
    playDeath() {
        if (!this.ctx || !this.masterGain)
            return;
        const osc = this.ctx.createOscillator();
        const gain = this.ctx.createGain();
        osc.type = 'sawtooth';
        osc.frequency.setValueAtTime(400, this.ctx.currentTime);
        osc.frequency.exponentialRampToValueAtTime(50, this.ctx.currentTime + 0.8);
        gain.gain.setValueAtTime(0.3, this.ctx.currentTime);
        gain.gain.exponentialRampToValueAtTime(0.01, this.ctx.currentTime + 0.8);
        osc.connect(gain);
        gain.connect(this.masterGain);
        osc.start();
        osc.stop(this.ctx.currentTime + 0.8);
    }
    playWaterSplash() {
        this.createNoise(0.3, 0.2);
        this.createOsc('sine', 400, 0.15, 0.1);
    }
    playTick() {
        this.createOsc('square', 1000, 0.02, 0.1);
    }
    playPowerUp() {
        if (!this.ctx || !this.masterGain)
            return;
        const notes = [400, 500, 600, 800];
        notes.forEach((freq, i) => {
            setTimeout(() => this.createOsc('sine', freq, 0.1, 0.2), i * 60);
        });
    }
    playDamage(amount) {
        const freq = Math.max(100, 400 - amount * 5);
        this.createOsc('square', freq, 0.1, Math.min(0.3, amount * 0.01));
    }
    // Ticking sound for turn timer urgency
    playUrgentTick(intensity) {
        const freq = 800 + intensity * 400;
        this.createOsc('square', freq, 0.03, 0.15);
    }
}
exports.SoundSystem = SoundSystem;
};

// ── module: src/ai.ts ──
__mods["src/ai.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.AIController = void 0;
const weapons_1 = require("./weapons");
class AIController {
    constructor(physics, terrain, difficulty) {
        this.physics = physics;
        this.terrain = terrain;
        this.difficulty = difficulty;
        this.thinkTimer = 0;
        this.actionQueue = [];
        this.isThinking = false;
    }
    get accuracy() {
        switch (this.difficulty) {
            case 'easy': return 0.5;
            case 'medium': return 0.7;
            case 'hard': return 0.9;
        }
    }
    get reactionTime() {
        switch (this.difficulty) {
            case 'easy': return 60;
            case 'medium': return 40;
            case 'hard': return 25;
        }
    }
    // Main AI decision making for a turn
    async executeTurn(unit, teammates, enemies, ammoPool, wind, waterLevel, onAction) {
        if (!unit.alive || this.isThinking)
            return;
        this.isThinking = true;
        await this.delay(this.reactionTime);
        // Phase 1: Assess situation and move to safe position
        const risk = this.physics.assessRisk(unit.pos.x, unit.pos.y, waterLevel);
        if (risk.risk > 30) {
            await this.moveToSafeSpot(unit, teammates, ammoPool, wind, waterLevel, onAction);
        }
        await this.delay(this.reactionTime * 0.5);
        // Phase 2: Select weapon and target
        const weaponChoice = this.selectWeapon(enemies, unit, ammoPool, wind);
        if (weaponChoice) {
            onAction('selectWeapon', weaponChoice.id);
            await this.delay(this.reactionTime * 0.3);
            // Phase 3: Aim and fire
            const target = this.selectTarget(enemies, weaponChoice, unit);
            if (target) {
                const aimData = this.calculateAim(unit, target, weaponChoice, wind);
                onAction('aim', aimData);
                await this.delay(this.reactionTime * 0.5);
                // Dynamite: detonate manually
                if (weaponChoice.id === 'dynamite') {
                    await this.delay(30 + Math.random() * 20);
                    onAction('detonate');
                }
                else {
                    onAction('fire');
                }
            }
        }
        // Phase 4: Use utility items if available
        if (unit.hp < unit.maxHp * 0.3) {
            const hasBanana = ammoPool.get('bananapower') || 0 > 0;
            if (hasBanana && enemies.length > 0) {
                onAction('selectWeapon', 'bananapower');
                await this.delay(20);
                onAction('fire');
            }
        }
        // Phase 5: End turn
        onAction('endTurn');
        this.isThinking = false;
    }
    selectTarget(enemies, weapon, shooterUnit) {
        if (enemies.length === 0)
            return null;
        // Prioritize low HP targets for finishing blows
        const aliveEnemies = enemies.filter(e => e.alive);
        if (aliveEnemies.length === 0)
            return null;
        // Hard AI: target lowest HP enemy
        if (this.difficulty === 'hard') {
            return [...aliveEnemies].sort((a, b) => a.hp - b.hp)[0];
        }
        // Medium/easy: random with slight preference for closest  
        const refPos = shooterUnit.pos;
        const weights = aliveEnemies.map(e => 1 / Math.max(1, this.distanceTo(refPos, e.pos)));
        const totalWeight = weights.reduce((s, w) => s + w, 0);
        let r = Math.random() * totalWeight;
        for (let i = 0; i < aliveEnemies.length; i++) {
            r -= weights[i];
            if (r <= 0)
                return aliveEnemies[i];
        }
        return aliveEnemies[0];
    }
    selectWeapon(enemies, unit, ammoPool, wind) {
        const aliveEnemies = enemies.filter(e => e.alive);
        if (aliveEnemies.length === 0)
            return null;
        // Score each weapon
        let bestWeapon = null;
        let bestScore = -Infinity;
        for (const w of weapons_1.WEAPONS) {
            const ammoLeft = ammoPool.get(w.id) || 0;
            if (ammoLeft <= 0)
                continue;
            // Skip utility weapons unless needed
            if (w.damage === 0 && unit.hp >= unit.maxHp * 0.3)
                continue;
            let score = 0;
            // Distance-based scoring
            const minDist = Math.min(...aliveEnemies.map(e => this.distanceTo(unit.pos, e.pos)));
            if (w.category === 'direct' && w.id !== 'super_size') {
                score = minDist < 150 ? 80 : 20;
            }
            else if (w.category === 'indirect') {
                score = minDist > 50 ? 70 : 30;
            }
            else if (w.category === 'aoe') {
                // AoE is good when enemies are clustered
                const avgDist = aliveEnemies.reduce((s, e) => s + this.distanceTo(unit.pos, e.pos), 0) / aliveEnemies.length;
                score = avgDist < 200 ? 90 : 40;
            }
            // Wind penalty for indirect weapons
            if (w.category === 'indirect' && Math.abs(wind) > 3) {
                score -= Math.abs(wind) * 5;
            }
            // Accuracy factor
            score *= this.accuracy;
            if (score > bestScore) {
                bestScore = score;
                bestWeapon = w;
            }
        }
        return bestWeapon;
    }
    calculateAim(shooter, target, weapon, wind) {
        const dx = target.pos.x - shooter.pos.x;
        const dy = target.pos.y - shooter.pos.y;
        const dist = Math.sqrt(dx * dx + dy * dy);
        let angle, power;
        if (weapon.category === 'direct') {
            // Direct fire: aim straight at target
            angle = Math.atan2(dy, dx);
            power = weapon.power || 14;
        }
        else {
            // Indirect/aoe: parabolic trajectory
            // Calculate launch angle for given distance
            const g = 0.35;
            const v = weapon.power * (0.8 + Math.random() * 0.4);
            // Ideal angle for maximum range is 45 degrees
            let idealAngle = Math.PI / 4;
            if (dist > 0) {
                // Adjust based on distance and height difference
                const heightDiff = -dy; // positive means target is above shooter
                idealAngle = Math.atan2(v * v - Math.sqrt(Math.max(1, v * v * v * v - g * (g * dist * dist + 2 * heightDiff * v * v))), g * dist);
                // Clamp to reasonable range
                idealAngle = Math.max(0.3, Math.min(1.4, idealAngle));
            }
            angle = dx >= 0 ? -idealAngle : -(Math.PI - idealAngle);
            // Wind compensation
            const windCompensation = wind * 0.02;
            angle += windCompensation;
            power = v / weapon.power;
        }
        // Add inaccuracy based on difficulty
        const inaccuracy = (1 - this.accuracy) * 0.3;
        angle += (Math.random() - 0.5) * inaccuracy;
        return {
            angle: Math.max(-Math.PI / 2, Math.min(0, angle)), // Only upward angles
            power: Math.max(0.3, Math.min(1, power))
        };
    }
    async moveToSafeSpot(unit, teammates, ammoPool, wind, waterLevel, onAction) {
        // Find a safer position nearby
        const currentRisk = this.physics.assessRisk(unit.pos.x, unit.pos.y, waterLevel).risk;
        const directions = [-1, 1];
        for (const dir of directions.sort(() => Math.random() - 0.5)) {
            const targetX = unit.pos.x + dir * (30 + Math.random() * 40);
            const newRisk = this.physics.assessRisk(targetX, unit.pos.y, waterLevel).risk;
            if (newRisk < currentRisk) {
                // Move toward safer spot
                onAction('move', dir > 0 ? 'right' : 'left');
                await this.delay(20);
                // Jump if needed
                const surfaceY = this.terrain.findSurfaceY(targetX);
                if (surfaceY < unit.pos.y - 10) {
                    onAction('jump');
                    await this.delay(15);
                }
            }
        }
    }
    distanceTo(a, b) {
        return Math.sqrt((a.x - b.x) ** 2 + (a.y - b.y) ** 2);
    }
    unitPos() {
        // Placeholder - will be set by caller
        return { x: 0, y: 0 };
    }
    delay(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }
}
exports.AIController = AIController;
};

// ── module: src/camera.ts ──
__mods["src/camera.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Camera = void 0;
class Camera {
    constructor(canvasWidth, canvasHeight) {
        this.canvasWidth = canvasWidth;
        this.canvasHeight = canvasHeight;
        this.x = 0;
        this.y = 0;
        this.zoom = 1;
        this.targetX = 0;
        this.targetY = 0;
        this.targetZoom = 1;
        this.shakeAmount = 0;
        this.shakeDecay = 0.9;
    }
    follow(targetX, targetY, mapW, mapH) {
        this.targetX = targetX;
        this.targetY = targetY;
    }
    setZoom(zoom) {
        this.targetZoom = Math.max(0.3, Math.min(2, zoom));
    }
    addShake(amount) {
        this.shakeAmount = Math.min(this.shakeAmount + amount, 20);
    }
    update(smoothing = 0.08) {
        // Smooth follow
        this.x += (this.targetX - this.x) * smoothing;
        this.y += (this.targetY - this.y) * smoothing;
        // Smooth zoom
        this.zoom += (this.targetZoom - this.zoom) * smoothing * 0.5;
        // Screen shake
        if (this.shakeAmount > 0.1) {
            this.x += (Math.random() - 0.5) * this.shakeAmount;
            this.y += (Math.random() - 0.5) * this.shakeAmount;
            this.shakeAmount *= this.shakeDecay;
        }
        else {
            this.shakeAmount = 0;
        }
    }
    // Get transform for canvas context
    getTransform() {
        return {
            tx: this.canvasWidth / 2 - this.x * this.zoom,
            ty: this.canvasHeight / 2 - this.y * this.zoom,
            scale: this.zoom
        };
    }
    // Convert screen coords to world coords
    screenToWorld(sx, sy) {
        const t = this.getTransform();
        return {
            x: (sx - t.tx) / t.scale,
            y: (sy - t.ty) / t.scale
        };
    }
    // Convert world coords to screen coords
    worldToScreen(wx, wy) {
        const t = this.getTransform();
        return {
            x: wx * t.scale + t.tx,
            y: wy * t.scale + t.ty
        };
    }
    reset() {
        this.x = 0;
        this.y = 0;
        this.zoom = 1;
        this.targetX = 0;
        this.targetY = 0;
        this.targetZoom = 1;
        this.shakeAmount = 0;
    }
}
exports.Camera = Camera;
};

// ── module: src/game.ts ──
__mods["src/game.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Game = void 0;
const types_1 = require("./types");
const terrain_1 = require("./terrain");
const physics_1 = require("./physics");
const weapons_1 = require("./weapons");
const particles_1 = require("./particles");
const sounds_1 = require("./sounds");
const ai_1 = require("./ai");
const camera_1 = require("./camera");
class Game {
    constructor() {
        // Game state
        this.units = [];
        this.currentTeamIndex = 0;
        this.turnTime = 30;
        this.maxTurnTime = 30;
        this.wind = 0;
        this.phase = 'menu';
        this.mapWidth = 1200;
        this.mapHeight = 600;
        this.waterLevel = 580;
        // Turn state
        this.selectedWeaponIndex = 0;
        this.aimAngle = -Math.PI / 4;
        this.aimPower = 0.7;
        this.turnCount = 0;
        // Projectiles & effects
        this.projectiles = [];
        this.explosionEffects = [];
        this.impactFreezeTimer = 0;
        this.turnAnnounceTimer = 0;
        // Team config
        this.teamCount = 2;
        this.cpuTeams = new Set();
        this.terrainShape = 'island';
        this.difficulty = 'medium';
        // Ammo pools per team
        this.ammoPools = new Map();
        // Banana power bonus
        this.bananaBonus = new Map();
        // AI controllers
        this.aiControllers = new Map();
        // Input state
        this.keysDown = new Set();
        this.mousePos = { x: 0, y: 0 };
        this.mouseWorld = { x: 0, y: 0 };
        this.mouseClicked = false;
        this.mouseRightClicked = false;
        this.terrain = new terrain_1.Terrain(this.mapWidth, this.mapHeight);
        this.physics = new physics_1.Physics(this.terrain);
        this.particles = new particles_1.ParticleSystem();
        this.sounds = new sounds_1.SoundSystem();
        this.camera = new camera_1.Camera(1280, 720);
    }
    init() {
        this.sounds.init();
        this.setupInput();
    }
    startGame(teamCount, cpuTeams, shape, difficulty) {
        this.teamCount = teamCount;
        this.cpuTeams = cpuTeams;
        this.terrainShape = shape;
        this.difficulty = difficulty;
        // Generate terrain
        this.terrain.generate(shape);
        // Place units
        this.placeUnits();
        // Create ammo pools
        for (let i = 0; i < teamCount; i++) {
            this.ammoPools.set(i, (0, weapons_1.createAmmoPool)());
            this.bananaBonus.set(i, 1);
            if (cpuTeams.has(i)) {
                const ai = new ai_1.AIController(this.physics, this.terrain, this.difficulty);
                this.aiControllers.set(i, ai);
            }
        }
        // Random initial wind
        this.wind = (Math.random() - 0.5) * 6;
        this.phase = 'playing';
        this.turnCount = 0;
        this.currentTeamIndex = 0;
        this.startTurn();
    }
    placeUnits() {
        const positions = [];
        const teamSpacing = this.mapWidth / (this.teamCount + 1);
        for (let t = 0; t < this.teamCount; t++) {
            const centerX = teamSpacing * (t + 1);
            for (let u = 0; u < 4; u++) {
                const offsetX = (u - 1.5) * 25;
                const spotX = centerX + offsetX;
                const spot = this.terrain.findSafeSpot(spotX, this.waterLevel);
                if (spot) {
                    positions.push(spot);
                    this.units.push({
                        id: `team${t}_unit${u}`,
                        teamId: t,
                        name: types_1.WORM_NAMES[t * 4 + u] || `Worm ${u + 1}`,
                        pos: { x: spot.x, y: spot.y },
                        vel: { x: 0, y: 0 },
                        hp: 100,
                        maxHp: 100,
                        alive: true,
                        facing: u < 2 ? 1 : -1,
                        onGround: false,
                        jumpCount: 0,
                        fallStartY: spot.y,
                        inWater: false
                    });
                }
            }
        }
        // Center camera
        this.camera.targetX = this.mapWidth / 2;
        this.camera.targetY = this.mapHeight / 3;
    }
    startTurn() {
        const teamUnits = this.getTeamAliveUnits(this.currentTeamIndex);
        if (teamUnits.length === 0) {
            // Team eliminated - check for winner
            this.checkWinCondition();
            return;
        }
        this.turnTime = this.maxTurnTime;
        this.aimAngle = -Math.PI / 4;
        this.aimPower = 0.7;
        this.selectedWeaponIndex = 0;
        // Change wind slightly each turn
        this.wind += (Math.random() - 0.5) * 2;
        this.wind = Math.max(-8, Math.min(8, this.wind));
        // Reset banana bonus
        this.bananaBonus.set(this.currentTeamIndex, 1);
        this.turnCount++;
        this.phase = 'weaponSelect';
        this.turnAnnounceTimer = 90; // frames to show announcement
        this.sounds.playTurnStart();
        // If CPU team, start AI turn
        if (this.cpuTeams.has(this.currentTeamIndex)) {
            setTimeout(() => this.startCPUTurn(), 500);
        }
    }
    async startCPUTurn() {
        const unit = this.getCurrentUnit();
        if (!unit || !unit.alive)
            return;
        const teammates = this.getTeamAliveUnits(this.currentTeamIndex).filter(u => u.id !== unit.id);
        const enemies = [];
        for (let t = 0; t < this.teamCount; t++) {
            if (t !== this.currentTeamIndex) {
                enemies.push(...this.getTeamAliveUnits(t));
            }
        }
        const ai = this.aiControllers.get(this.currentTeamIndex);
        if (!ai)
            return;
        this.phase = 'firing';
        await ai.executeTurn(unit, teammates, enemies, this.ammoPools.get(this.currentTeamIndex), this.wind, this.waterLevel, (action, data) => {
            switch (action) {
                case 'selectWeapon':
                    const w = (0, weapons_1.getWeaponById)(data);
                    if (w)
                        this.selectedWeaponIndex = weapons_1.WEAPONS.indexOf(w);
                    break;
                case 'aim':
                    this.aimAngle = data.angle;
                    this.aimPower = data.power;
                    break;
                case 'fire':
                    this.fireProjectile();
                    break;
                case 'detonate':
                    this.detonateDynamite();
                    break;
                case 'endTurn':
                    this.endTurn();
                    break;
            }
        });
    }
    getCurrentUnit() {
        const teamUnits = this.getTeamAliveUnits(this.currentTeamIndex);
        return teamUnits.length > 0 ? teamUnits[0] : null;
    }
    getTeamAliveUnits(teamId) {
        return this.units.filter(u => u.teamId === teamId && u.alive);
    }
    checkWinCondition() {
        const aliveTeams = new Set();
        for (const unit of this.units) {
            if (unit.alive)
                aliveTeams.add(unit.teamId);
        }
        if (aliveTeams.size <= 1) {
            this.phase = 'gameOver';
        }
        else {
            // Skip eliminated teams
            do {
                this.currentTeamIndex = (this.currentTeamIndex + 1) % this.teamCount;
            } while (!aliveTeams.has(this.currentTeamIndex));
            this.startTurn();
        }
    }
    endTurn() {
        const aliveTeams = new Set();
        for (const unit of this.units) {
            if (unit.alive)
                aliveTeams.add(unit.teamId);
        }
        do {
            this.currentTeamIndex = (this.currentTeamIndex + 1) % this.teamCount;
        } while (!aliveTeams.has(this.currentTeamIndex));
        this.startTurn();
    }
    fireProjectile() {
        const unit = this.getCurrentUnit();
        if (!unit || !unit.alive)
            return;
        const weapon = weapons_1.WEAPONS[this.selectedWeaponIndex];
        const ammoPool = this.ammoPools.get(this.currentTeamIndex);
        if (!ammoPool || (ammoPool.get(weapon.id) || 0) <= 0)
            return;
        // Special weapons
        if (weapon.id === 'super_size') {
            unit.maxHp += 50;
            unit.hp = Math.min(unit.hp + 50, unit.maxHp);
            this.particles.addFloatingText(unit.pos.x, unit.pos.y - 20, 'SUPER SIZE!', '#FFD700', 20);
            this.sounds.playPowerUp();
            ammoPool.set(weapon.id, (ammoPool.get(weapon.id) || 1) - 1);
            return;
        }
        if (weapon.id === 'bananapower') {
            this.bananaBonus.set(this.currentTeamIndex, 1.5);
            this.particles.addFloatingText(unit.pos.x, unit.pos.y - 20, '+50% DAMAGE!', '#FFA500', 18);
            this.sounds.playPowerUp();
            ammoPool.set(weapon.id, (ammoPool.get(weapon.id) || 1) - 1);
            return;
        }
        // Launch projectile
        const launchAngle = unit.facing === 1 ? Math.PI + this.aimAngle : this.aimAngle;
        const speed = weapon.power * this.aimPower;
        // Muzzle flash particles
        this.particles.spawnDust(unit.pos.x, unit.pos.y - 5);
        this.projectiles.push({
            id: `proj_${Date.now()}`,
            pos: { x: unit.pos.x + Math.cos(launchAngle) * 10, y: unit.pos.y - 10 + Math.sin(launchAngle) * 10 },
            vel: {
                x: Math.cos(launchAngle) * speed + ((weapon.category === 'indirect' || weapon.category === 'aoe') ? this.wind * 0.1 : 0),
                y: Math.sin(launchAngle) * speed
            },
            weaponId: weapon.id,
            teamId: this.currentTeamIndex,
            alive: true,
            trail: [],
            gravity: physics_1.GRAVITY,
            isHoming: weapon.id === 'homing_missile',
            homingTarget: null,
            fuseTimer: weapon.id === 'dynamite' ? 180 : undefined,
        });
        ammoPool.set(weapon.id, (ammoPool.get(weapon.id) || 1) - 1);
        this.phase = 'firing';
        this.sounds.playLaunch();
    }
    detonateDynamite() {
        for (const proj of this.projectiles) {
            if (proj.alive && proj.weaponId === 'dynamite' && proj.teamId === this.currentTeamIndex) {
                this.explodeProjectile(proj);
            }
        }
    }
    explodeProjectile(proj) {
        const weapon = (0, weapons_1.getWeaponById)(proj.weaponId);
        if (!weapon)
            return;
        proj.alive = false;
        const damageMult = this.bananaBonus.get(this.currentTeamIndex) || 1;
        const damage = Math.floor(weapon.damage * damageMult);
        const radius = weapon.aoeRadius;
        // Cluster bomb: spawn sub-munitions
        if (weapon.id === 'cluster_bomb') {
            for (let i = 0; i < 5; i++) {
                this.projectiles.push({
                    id: `sub_${Date.now()}_${i}`,
                    pos: { x: proj.pos.x, y: proj.pos.y },
                    vel: {
                        x: (Math.random() - 0.5) * 6,
                        y: -Math.random() * 4 - 2
                    },
                    weaponId: 'cluster_sub',
                    teamId: this.currentTeamIndex,
                    alive: true,
                    trail: [],
                    gravity: physics_1.GRAVITY,
                    subDamage: Math.floor(damage / 3),
                    subRadius: radius * 0.5,
                });
            }
        }
        // Apply damage to units
        for (const unit of this.units) {
            if (!unit.alive)
                continue;
            const dx = unit.pos.x - proj.pos.x;
            const dy = unit.pos.y - proj.pos.y;
            const dist = Math.sqrt(dx * dx + dy * dy);
            if (dist < radius) {
                const falloff = 1 - dist / radius;
                const dmg = Math.floor(damage * falloff);
                // Friendly fire reduced by 50%
                const actualDmg = unit.teamId === this.currentTeamIndex ? Math.floor(dmg * 0.5) : dmg;
                if (actualDmg > 0) {
                    unit.hp -= actualDmg;
                    // Knockback
                    const kbStrength = radius * 0.3;
                    unit.vel.x += (dx / dist) * kbStrength * falloff;
                    unit.vel.y += (dy / dist) * kbStrength * falloff - 2;
                    this.particles.addFloatingText(unit.pos.x, unit.pos.y - 15, `-${actualDmg}`, '#FF4444', 18);
                    this.sounds.playDamage(actualDmg);
                    if (unit.hp <= 0) {
                        unit.alive = false;
                        this.particles.addFloatingText(unit.pos.x, unit.pos.y - 25, '💀', '#FF0000', 24);
                        this.sounds.playDeath();
                        // Check win condition after delay
                        setTimeout(() => {
                            if (this.phase !== 'gameOver')
                                this.checkWinCondition();
                        }, 1000);
                    }
                }
            }
        }
        // Destroy terrain
        const debris = this.terrain.destroy(proj.pos.x, proj.pos.y, radius);
        // Spawn particles
        const size = radius > 40 ? 'large' : radius > 25 ? 'medium' : 'small';
        this.particles.spawnExplosion(proj.pos.x, proj.pos.y, radius);
        this.particles.spawnDebris(proj.pos.x, proj.pos.y, Math.min(debris.length / 3, 30));
        this.sounds.playExplosion(size);
        // Water splash if near water level
        if (proj.pos.y > this.waterLevel - radius) {
            this.particles.spawnWaterSplash(proj.pos.x, this.waterLevel);
            this.sounds.playWaterSplash();
        }
        // Explosion effect for visual feedback
        this.explosionEffects.push({
            x: proj.pos.x, y: proj.pos.y,
            radius, progress: 0,
            shakeAmount: size === 'large' ? 12 : size === 'medium' ? 8 : 4,
            freezeFrames: size === 'large' ? 5 : size === 'medium' ? 3 : 0
        });
        // Screen shake and impact freeze
        this.camera.addShake(size === 'large' ? 12 : size === 'medium' ? 8 : 4);
        if (size === 'large') {
            this.impactFreezeTimer = 5;
        }
        // Settle terrain gravity
        this.terrain.settleGravity();
    }
    update(dt) {
        if (this.phase === 'menu' || this.phase === 'gameOver')
            return;
        // Impact freeze
        if (this.impactFreezeTimer > 0) {
            this.impactFreezeTimer--;
            return;
        }
        const effectiveDt = dt * (1 / 60);
        // Turn announcement countdown
        if (this.turnAnnounceTimer > 0) {
            this.turnAnnounceTimer--;
        }
        // Update turn timer
        if (this.phase === 'weaponSelect' || this.phase === 'aiming') {
            this.turnTime -= effectiveDt;
            // Urgent ticking sound
            if (this.turnTime < 5 && Math.floor(this.turnTime * 4) !== Math.floor((this.turnTime + effectiveDt) * 4)) {
                this.sounds.playUrgentTick(1 - this.turnTime / 5);
            }
            if (this.turnTime <= 0) {
                this.endTurn();
                return;
            }
        }
        // Update units
        for (const unit of this.units) {
            if (!unit.alive)
                continue;
            const hadFallDamage = this.physics.updateUnit(unit, effectiveDt, this.wind, this.waterLevel);
            if (hadFallDamage) {
                this.particles.addFloatingText(unit.pos.x, unit.pos.y - 15, 'FALL DAMAGE!', '#FF6600', 14);
                this.sounds.playHit();
                if (unit.hp <= 0 && unit.alive) {
                    unit.alive = false;
                    this.particles.addFloatingText(unit.pos.x, unit.pos.y - 25, '💀', '#FF0000', 24);
                    this.sounds.playDeath();
                }
            }
            // Drowning damage
            if (unit.inWater && unit.onGround) {
                unit.hp -= effectiveDt * 2;
                if (Math.floor(unit.pos.y) % 30 === 0) {
                    this.particles.spawnWaterSplash(unit.pos.x, unit.pos.y);
                }
                if (unit.hp <= 0 && unit.alive) {
                    unit.alive = false;
                    this.particles.addFloatingText(unit.pos.x, unit.pos.y - 25, 'DROWNED!', '#4488FF', 16);
                    this.sounds.playDeath();
                }
            }
            // Input handling for current team's unit (human only)
            if (!this.cpuTeams.has(this.currentTeamIndex) &&
                this.phase !== 'firing' && this.phase !== 'explosion') {
                let moved = false;
                if (this.keysDown.has('ArrowLeft') || this.keysDown.has('a')) {
                    unit.vel.x = -physics_1.MOVE_SPEED;
                    unit.facing = -1;
                    moved = true;
                }
                else if (this.keysDown.has('ArrowRight') || this.keysDown.has('d')) {
                    unit.vel.x = physics_1.MOVE_SPEED;
                    unit.facing = 1;
                    moved = true;
                }
                else {
                    unit.vel.x *= 0.8; // friction
                }
                if ((this.keysDown.has('ArrowUp') || this.keysDown.has('w') || this.keysDown.has(' ')) &&
                    unit.onGround && unit.jumpCount < 2) {
                    unit.vel.y = physics_1.JUMP_FORCE;
                    unit.jumpCount++;
                    this.particles.spawnDust(unit.pos.x, unit.pos.y + physics_1.UNIT_RADIUS);
                    this.sounds.playJump();
                }
            }
        }
        // Update projectiles
        for (const proj of this.projectiles) {
            if (!proj.alive)
                continue;
            // Homing missile logic
            if (proj.isHoming && !proj.homingTarget) {
                // Find nearest enemy
                let closest = null;
                let closestDist = Infinity;
                for (const unit of this.units) {
                    if (!unit.alive || unit.teamId === proj.teamId)
                        continue;
                    const d = Math.sqrt((unit.pos.x - proj.pos.x) ** 2 + (unit.pos.y - proj.pos.y) ** 2);
                    if (d < closestDist) {
                        closestDist = d;
                        closest = unit;
                    }
                }
                proj.homingTarget = closest;
            }
            if (proj.isHoming && proj.homingTarget?.alive) {
                const dx = proj.homingTarget.pos.x - proj.pos.x;
                const dy = proj.homingTarget.pos.y - proj.pos.y;
                const angle = Math.atan2(dy, dx);
                const speed = Math.sqrt(proj.vel.x ** 2 + proj.vel.y ** 2);
                // Gradually steer toward target
                const currentAngle = Math.atan2(proj.vel.y, proj.vel.x);
                let diff = angle - currentAngle;
                while (diff > Math.PI)
                    diff -= Math.PI * 2;
                while (diff < -Math.PI)
                    diff += Math.PI * 2;
                const turnRate = 0.05;
                const newAngle = currentAngle + Math.sign(diff) * Math.min(Math.abs(diff), turnRate);
                proj.vel.x = Math.cos(newAngle) * speed;
                proj.vel.y = Math.sin(newAngle) * speed;
            }
            // Dynamite fuse timer
            if (proj.fuseTimer !== undefined) {
                proj.fuseTimer -= effectiveDt;
                if (proj.fuseTimer <= 0) {
                    this.explodeProjectile(proj);
                    continue;
                }
            }
            const hit = this.physics.updateProjectile(proj, effectiveDt);
            // Check unit collision
            for (const unit of this.units) {
                if (!unit.alive)
                    continue;
                if (this.physics.checkUnitCollision(unit, proj)) {
                    this.explodeProjectile(proj);
                    break;
                }
            }
            if (hit && proj.alive) {
                this.explodeProjectile(proj);
            }
        }
        // Update particles
        this.particles.update(effectiveDt * 60);
        // Update explosion effects
        for (const eff of this.explosionEffects) {
            eff.progress += effectiveDt * 3;
        }
        this.explosionEffects = this.explosionEffects.filter(e => e.progress < 1);
        // Update camera to follow current unit
        const currentUnit = this.getCurrentUnit();
        if (currentUnit && currentUnit.alive) {
            this.camera.follow(currentUnit.pos.x, currentUnit.pos.y - 50, this.mapWidth, this.mapHeight);
        }
        this.camera.update(0.06);
        // Clean up dead projectiles
        this.projectiles = this.projectiles.filter(p => p.alive || p.trail.length > 0);
    }
    setupInput() {
        window.addEventListener('keydown', (e) => {
            this.keysDown.add(e.key);
            if (this.phase === 'weaponSelect') {
                if (e.key >= '1' && e.key <= '9') {
                    const idx = parseInt(e.key) - 1;
                    if (idx < weapons_1.WEAPONS.length) {
                        this.selectedWeaponIndex = idx;
                        this.sounds.playMenuClick();
                    }
                }
                if (e.key === 'Enter' || e.key === ' ') {
                    this.phase = 'aiming';
                    this.sounds.playMenuClick();
                }
            }
            else if (this.phase === 'aiming') {
                if (e.key === 'Enter' || e.key === ' ') {
                    this.fireProjectile();
                }
                if (e.key === 'Escape') {
                    this.endTurn();
                }
            }
        });
        window.addEventListener('keyup', (e) => {
            this.keysDown.delete(e.key);
        });
        // Mouse handling for aiming
        const handleMouseMove = (e) => {
            if (!this.canvas)
                return;
            const rect = this.canvas.getBoundingClientRect();
            this.mousePos.x = e.clientX - rect.left;
            this.mousePos.y = e.clientY - rect.top;
            // Convert to world coords for aiming
            const t = this.camera.getTransform();
            this.mouseWorld.x = (this.mousePos.x - t.tx) / t.scale;
            this.mouseWorld.y = (this.mousePos.y - t.ty) / t.scale;
            if (this.phase === 'aiming') {
                const unit = this.getCurrentUnit();
                if (unit && unit.alive) {
                    const dx = this.mouseWorld.x - unit.pos.x;
                    const dy = this.mouseWorld.y - unit.pos.y;
                    // Calculate angle from unit to mouse
                    let angle = Math.atan2(dy, dx);
                    // Only allow upward angles
                    if (angle > 0) {
                        angle = -Math.abs(angle);
                    }
                    this.aimAngle = Math.max(-Math.PI / 2 + 0.1, Math.min(-0.1, angle));
                }
            }
        };
        const handleClick = (e) => {
            if (!this.canvas)
                return;
            // Left click
            if (e.button === 0) {
                this.mouseClicked = true;
                if (this.phase === 'weaponSelect') {
                    // Check weapon button clicks - handled by HUD rendering
                }
                else if (this.phase === 'aiming') {
                    this.fireProjectile();
                }
            }
            // Right click
            if (e.button === 2) {
                this.mouseRightClicked = true;
                if (this.phase === 'aiming') {
                    this.endTurn();
                }
            }
        };
        window.addEventListener('mousemove', handleMouseMove);
        window.addEventListener('mousedown', handleClick);
        window.addEventListener('contextmenu', e => e.preventDefault());
    }
    // Getters for rendering
    get currentTeamColor() {
        return types_1.TEAM_COLORS[this.currentTeamIndex] || '#FFFFFF';
    }
    get weaponAmmoCount() {
        const pool = this.ammoPools.get(this.currentTeamIndex);
        if (!pool)
            return weapons_1.WEAPONS.map(() => 0);
        return weapons_1.WEAPONS.map(w => pool.get(w.id) || 0);
    }
}
exports.Game = Game;
};

// ── module: src/rendering.ts ──
__mods["src/rendering.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Renderer = void 0;
const types_1 = require("./types");
const weapons_1 = require("./weapons");
const physics_1 = require("./physics");
class Renderer {
    constructor(game) {
        this.game = game;
    }
    render() {
        const ctx = this.game.ctx;
        if (!ctx)
            return;
        const t = this.game.camera.getTransform();
        // Clear background with sky gradient
        const skyGrad = ctx.createLinearGradient(0, 0, 0, this.game.canvas.height);
        skyGrad.addColorStop(0, '#1a1a3e');
        skyGrad.addColorStop(0.4, '#2d4a7a');
        skyGrad.addColorStop(0.85, '#4a7ab5');
        skyGrad.addColorStop(1, '#6ba3d6');
        ctx.fillStyle = skyGrad;
        ctx.fillRect(0, 0, this.game.canvas.width, this.game.canvas.height);
        // Stars (subtle)
        if (this.game.phase !== 'menu') {
            this.drawStars(ctx);
        }
        ctx.save();
        ctx.translate(t.tx, t.ty);
        ctx.scale(t.scale, t.scale);
        // Render terrain
        this.game.terrain.render(ctx, 1);
        // Render grass tufts on surface
        this.renderGrassTufts(ctx);
        // Render water
        this.renderWater(ctx);
        // Render explosion effects (flash)
        for (const eff of this.game.explosionEffects) {
            this.renderExplosionFlash(ctx, eff);
        }
        // Render projectile trails and projectiles
        this.renderProjectiles(ctx);
        // Render units
        this.renderUnits(ctx);
        // Render particles
        this.game.particles.render(ctx);
        ctx.restore();
        // Turn announcement overlay
        if (this.game.turnAnnounceTimer > 0) {
            this.renderTurnAnnouncement(ctx);
        }
        // HUD (screen-space)
        if (this.game.phase !== 'menu') {
            this.renderHUD(ctx);
        }
        // Menu overlay
        if (this.game.phase === 'menu') {
            this.renderMenu(ctx);
        }
        // Game over overlay
        if (this.game.phase === 'gameOver') {
            this.renderGameOver(ctx);
        }
        // Vignette effect for atmosphere
        this.renderVignette(ctx);
    }
    renderVignette(ctx) {
        const w = this.game.canvas.width;
        const h = this.game.canvas.height;
        const grad = ctx.createRadialGradient(w / 2, h / 2, w * 0.3, w / 2, h / 2, w * 0.7);
        grad.addColorStop(0, 'rgba(0,0,0,0)');
        grad.addColorStop(1, 'rgba(0,0,0,0.4)');
        ctx.fillStyle = grad;
        ctx.fillRect(0, 0, w, h);
    }
    drawStars(ctx) {
        // Stars with twinkling effect
        const time = Date.now() * 0.001;
        for (let i = 0; i < 80; i++) {
            const x = ((i * 137 + 42) % this.game.mapWidth);
            const y = ((i * 97 + 13) % (this.game.mapHeight * 0.5));
            const size = (i % 3) + 1;
            const twinkle = 0.2 + Math.sin(time + i) * 0.15;
            ctx.fillStyle = `rgba(255,255,255,${twinkle})`;
            ctx.fillRect(x, y, size, size);
        }
        // Clouds
        this.drawClouds(ctx, time);
    }
    drawClouds(ctx, time) {
        const cloudAlpha = 0.15;
        for (let i = 0; i < 6; i++) {
            const cx = ((i * 237 + time * 8) % (this.game.mapWidth + 200)) - 100;
            const cy = 40 + i * 50 + Math.sin(i * 2.3) * 20;
            ctx.fillStyle = `rgba(200,220,255,${cloudAlpha})`;
            for (let j = 0; j < 5; j++) {
                const px = cx + j * 18 - 36;
                const py = cy + Math.sin(j * 1.5) * 8;
                ctx.beginPath();
                ctx.arc(px, py, 20 + Math.sin(i + j) * 5, 0, Math.PI * 2);
                ctx.fill();
            }
        }
    }
    renderGrassTufts(ctx) {
        const time = Date.now() * 0.001;
        // Draw grass tufts on terrain surface
        for (let x = 5; x < this.game.mapWidth - 5; x += 7) {
            const surfaceY = this.game.terrain.findSurfaceY(x);
            // Only draw if above water and not in cave ceiling area
            if (surfaceY > this.game.waterLevel - 10)
                continue;
            // Check it's a grass block
            if (this.game.terrain.get(x, surfaceY) !== 1)
                continue;
            const sway = Math.sin(time * 2 + x * 0.3) * 2;
            ctx.strokeStyle = '#5a9e48';
            ctx.lineWidth = 1;
            // Small grass tuft
            for (let g = -1; g <= 1; g++) {
                ctx.beginPath();
                ctx.moveTo(x + g, surfaceY);
                ctx.quadraticCurveTo(x + g + sway * 0.5, surfaceY - 4, x + g + sway, surfaceY - 6 - Math.random() * 2);
                ctx.stroke();
            }
        }
    }
    renderWater(ctx) {
        const waterY = this.game.waterLevel;
        // Water body
        const waterGrad = ctx.createLinearGradient(0, waterY, 0, this.game.mapHeight);
        waterGrad.addColorStop(0, 'rgba(30,80,160,0.7)');
        waterGrad.addColorStop(0.5, 'rgba(20,60,140,0.8)');
        waterGrad.addColorStop(1, 'rgba(10,40,100,0.9)');
        ctx.fillStyle = waterGrad;
        ctx.fillRect(0, waterY, this.game.mapWidth, this.game.mapHeight - waterY);
        // Water surface wave effect
        const time = Date.now() * 0.002;
        ctx.strokeStyle = 'rgba(100,180,255,0.4)';
        ctx.lineWidth = 2;
        ctx.beginPath();
        for (let x = 0; x < this.game.mapWidth; x += 3) {
            const waveY = waterY + Math.sin(x * 0.05 + time) * 2 + Math.sin(x * 0.1 + time * 1.5) * 1;
            if (x === 0)
                ctx.moveTo(x, waveY);
            else
                ctx.lineTo(x, waveY);
        }
        ctx.stroke();
        // Foam line
        ctx.strokeStyle = 'rgba(200,230,255,0.3)';
        ctx.lineWidth = 1;
        ctx.beginPath();
        for (let x = 0; x < this.game.mapWidth; x += 3) {
            const waveY = waterY + Math.sin(x * 0.08 + time * 0.7) * 1.5;
            if (x === 0)
                ctx.moveTo(x, waveY);
            else
                ctx.lineTo(x, waveY);
        }
        ctx.stroke();
    }
    renderExplosionFlash(ctx, eff) {
        const alpha = Math.max(0, 1 - eff.progress);
        // Expanding ring
        const ringRadius = eff.radius * (0.5 + eff.progress * 1.5);
        ctx.strokeStyle = `rgba(255,200,100,${alpha * 0.6})`;
        ctx.lineWidth = Math.max(1, 4 * alpha);
        ctx.beginPath();
        ctx.arc(eff.x, eff.y, ringRadius, 0, Math.PI * 2);
        ctx.stroke();
        // Inner bright flash (only at start)
        if (eff.progress < 0.3) {
            const innerAlpha = (1 - eff.progress / 0.3) * 0.9;
            const grad = ctx.createRadialGradient(eff.x, eff.y, 0, eff.x, eff.y, eff.radius);
            grad.addColorStop(0, `rgba(255,255,255,${innerAlpha})`);
            grad.addColorStop(0.3, `rgba(255,255,200,${innerAlpha * 0.8})`);
            grad.addColorStop(0.6, `rgba(255,150,50,${innerAlpha * 0.4})`);
            grad.addColorStop(1, 'rgba(255,80,0,0)');
            ctx.fillStyle = grad;
            ctx.beginPath();
            ctx.arc(eff.x, eff.y, eff.radius, 0, Math.PI * 2);
            ctx.fill();
        }
        // Outer glow (fades slower)
        const outerAlpha = alpha * 0.4;
        if (outerAlpha > 0.01) {
            const grad = ctx.createRadialGradient(eff.x, eff.y, 0, eff.x, eff.y, eff.radius * 2);
            grad.addColorStop(0, `rgba(255,150,50,${outerAlpha})`);
            grad.addColorStop(0.5, `rgba(255,80,20,${outerAlpha * 0.5})`);
            grad.addColorStop(1, 'rgba(255,40,0,0)');
            ctx.fillStyle = grad;
            ctx.beginPath();
            ctx.arc(eff.x, eff.y, eff.radius * 2, 0, Math.PI * 2);
            ctx.fill();
        }
    }
    renderProjectiles(ctx) {
        for (const proj of this.game.projectiles) {
            // Trail
            if (proj.trail.length > 1) {
                ctx.strokeStyle = 'rgba(255,200,100,0.4)';
                ctx.lineWidth = 2;
                ctx.beginPath();
                for (let i = 0; i < proj.trail.length; i++) {
                    const alpha = i / proj.trail.length;
                    if (i === 0)
                        ctx.moveTo(proj.trail[i].x, proj.trail[i].y);
                    else
                        ctx.lineTo(proj.trail[i].x, proj.trail[i].y);
                }
                ctx.stroke();
            }
            // Projectile body
            if (proj.alive) {
                const weapon = weapons_1.WEAPONS.find(w => w.id === proj.weaponId);
                ctx.save();
                ctx.translate(proj.pos.x, proj.pos.y);
                const angle = Math.atan2(proj.vel.y, proj.vel.x);
                ctx.rotate(angle);
                if (weapon?.id === 'rocket') {
                    // Rocket shape
                    ctx.fillStyle = '#FF6600';
                    ctx.beginPath();
                    ctx.ellipse(0, 0, 8, 3, 0, 0, Math.PI * 2);
                    ctx.fill();
                    // Flame
                    ctx.fillStyle = '#FFFF00';
                    ctx.beginPath();
                    ctx.moveTo(-6, -2);
                    ctx.lineTo(-12 - Math.random() * 4, 0);
                    ctx.lineTo(-6, 2);
                    ctx.fill();
                }
                else if (weapon?.id === 'dynamite') {
                    // Dynamite stick
                    ctx.fillStyle = '#CC3333';
                    ctx.fillRect(-5, -2, 10, 4);
                    // Fuse spark
                    ctx.fillStyle = '#FFFF00';
                    ctx.beginPath();
                    ctx.arc(6, 0, 2 + Math.random(), 0, Math.PI * 2);
                    ctx.fill();
                }
                else if (weapon?.id === 'homing_missile') {
                    // Missile shape
                    ctx.fillStyle = '#88CCFF';
                    ctx.beginPath();
                    ctx.moveTo(10, 0);
                    ctx.lineTo(-5, -4);
                    ctx.lineTo(-3, 0);
                    ctx.lineTo(-5, 4);
                    ctx.closePath();
                    ctx.fill();
                }
                else if (weapon?.id === 'minigun') {
                    // Bullet
                    ctx.fillStyle = '#FFFF00';
                    ctx.beginPath();
                    ctx.arc(0, 0, 2, 0, Math.PI * 2);
                    ctx.fill();
                }
                else {
                    // Generic projectile
                    ctx.fillStyle = '#FFAA44';
                    ctx.beginPath();
                    ctx.arc(0, 0, 4, 0, Math.PI * 2);
                    ctx.fill();
                }
                ctx.restore();
            }
        }
    }
    renderUnits(ctx) {
        const time = Date.now() * 0.003;
        for (const unit of this.game.units) {
            if (!unit.alive)
                continue;
            const color = types_1.TEAM_COLORS[unit.teamId] || '#FFFFFF';
            const isSuperSized = unit.maxHp > 150;
            const bodyScale = isSuperSized ? 2 : 1;
            ctx.save();
            ctx.translate(unit.pos.x, unit.pos.y);
            // Shadow (scales with size)
            ctx.fillStyle = 'rgba(0,0,0,0.3)';
            ctx.beginPath();
            ctx.ellipse(0, physics_1.UNIT_RADIUS * bodyScale + 2, physics_1.UNIT_RADIUS * 1.5 * bodyScale, 4 * bodyScale, 0, 0, Math.PI * 2);
            ctx.fill();
            // Worm body - segmented appearance
            const segments = isSuperSized ? 7 : 5;
            const segWidth = (physics_1.UNIT_RADIUS + 2) * bodyScale / segments;
            for (let s = 0; s < segments; s++) {
                const sx = (s - (segments - 1) / 2) * segWidth * 0.85;
                // Slight wave animation
                const waveY = Math.sin(time + s * 0.8) * 1.5 * bodyScale;
                // Segment color with gradient effect
                const brightness = 1 - Math.abs(s - (segments - 1) / 2) / segments * 0.3;
                ctx.fillStyle = this.adjustBrightness(color, brightness);
                ctx.beginPath();
                ctx.arc(sx, waveY, segWidth * 0.9, 0, Math.PI * 2);
                ctx.fill();
                // Segment outline
                ctx.strokeStyle = 'rgba(0,0,0,0.3)';
                ctx.lineWidth = 0.5;
                ctx.stroke();
            }
            // Head (front segment with face)
            const headX = unit.facing === 1 ? physics_1.UNIT_RADIUS * bodyScale : -physics_1.UNIT_RADIUS * bodyScale;
            // Eyes on head
            const eyeSize = 3 * bodyScale;
            ctx.fillStyle = '#FFFFFF';
            ctx.beginPath();
            ctx.arc(headX + 2, -2 * bodyScale, eyeSize, 0, Math.PI * 2);
            ctx.arc(headX - 2, -2 * bodyScale, eyeSize, 0, Math.PI * 2);
            ctx.fill();
            // Pupils (look in facing direction)
            const pupilOffset = unit.facing === 1 ? 1.5 : -1;
            ctx.fillStyle = '#000';
            ctx.beginPath();
            ctx.arc(headX + 2 + pupilOffset, -2 * bodyScale, eyeSize * 0.5, 0, Math.PI * 2);
            ctx.arc(headX - 2 + pupilOffset, -2 * bodyScale, eyeSize * 0.5, 0, Math.PI * 2);
            ctx.fill();
            // Mouth expression based on HP
            ctx.strokeStyle = '#000';
            ctx.lineWidth = 1;
            ctx.beginPath();
            if (unit.hp > unit.maxHp * 0.6) {
                // Happy smile
                ctx.arc(headX, 2 * bodyScale, 3 * bodyScale, 0.1, Math.PI - 0.1);
            }
            else if (unit.hp > unit.maxHp * 0.3) {
                // Neutral
                ctx.moveTo(headX - 2, 2 * bodyScale);
                ctx.lineTo(headX + 2, 2 * bodyScale);
            }
            else {
                // Worried frown
                ctx.arc(headX, 5 * bodyScale, 2.5 * bodyScale, Math.PI + 0.3, -0.3);
            }
            ctx.stroke();
            // Health bar above unit
            const hpWidth = physics_1.UNIT_RADIUS * 3 * bodyScale;
            const hpHeight = 4 * bodyScale;
            const hpY = -(physics_1.UNIT_RADIUS + 8) * bodyScale;
            // HP bar background with rounded corners
            ctx.fillStyle = 'rgba(0,0,0,0.6)';
            this.roundRect(ctx, -hpWidth / 2, hpY, hpWidth, hpHeight, 2);
            ctx.fill();
            const hpRatio = Math.max(0, unit.hp / unit.maxHp);
            const hpColor = hpRatio > 0.6 ? '#44FF44' : hpRatio > 0.3 ? '#FFFF44' : '#FF4444';
            ctx.fillStyle = hpColor;
            this.roundRect(ctx, -hpWidth / 2, hpY, hpWidth * Math.max(0.05, hpRatio), hpHeight, 2);
            ctx.fill();
            // HP text
            if (bodyScale > 1) {
                ctx.font = `bold ${8 * bodyScale}px Arial`;
                ctx.textAlign = 'center';
                ctx.fillStyle = '#FFF';
                ctx.fillText(`${Math.max(0, unit.hp)}`, 0, hpY + hpHeight - 1);
            }
            // Name tag
            ctx.font = `${10 * bodyScale}px Arial`;
            ctx.textAlign = 'center';
            ctx.fillStyle = '#FFFFFF';
            ctx.strokeStyle = '#000';
            ctx.lineWidth = 2;
            ctx.strokeText(unit.name, 0, hpY - 4);
            ctx.fillText(unit.name, 0, hpY - 4);
            // Active unit indicator (pulsing star)
            if (!this.game.cpuTeams.has(unit.teamId) &&
                this.game.getCurrentUnit()?.id === unit.id) {
                const starPulse = Math.sin(Date.now() * 0.005) * 0.3 + 0.7;
                ctx.fillStyle = `rgba(255,255,100,${starPulse})`;
                this.drawStar(ctx, 0, -(physics_1.UNIT_RADIUS + 18) * bodyScale, 6);
            }
            // Water indicator
            if (unit.inWater) {
                ctx.font = `${14 * bodyScale}px Arial`;
                ctx.fillText('💧', 0, -(physics_1.UNIT_RADIUS + 25) * bodyScale);
            }
            ctx.restore();
        }
    }
    adjustBrightness(hexColor, factor) {
        // Parse hex color and adjust brightness
        const r = parseInt(hexColor.slice(1, 3), 16);
        const g = parseInt(hexColor.slice(3, 5), 16);
        const b = parseInt(hexColor.slice(5, 7), 16);
        return `rgb(${Math.min(255, Math.floor(r * factor))},${Math.min(255, Math.floor(g * factor))},${Math.min(255, Math.floor(b * factor))})`;
    }
    roundRect(ctx, x, y, w, h, r) {
        ctx.beginPath();
        ctx.moveTo(x + r, y);
        ctx.lineTo(x + w - r, y);
        ctx.quadraticCurveTo(x + w, y, x + w, y + r);
        ctx.lineTo(x + w, y + h - r);
        ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
        ctx.lineTo(x + r, y + h);
        ctx.quadraticCurveTo(x, y + h, x, y + h - r);
        ctx.lineTo(x, y + r);
        ctx.quadraticCurveTo(x, y, x + r, y);
        ctx.closePath();
    }
    renderTurnAnnouncement(ctx) {
        const w = this.game.canvas.width;
        const h = this.game.canvas.height;
        const timerRatio = this.game.turnAnnounceTimer / 90;
        const alpha = Math.min(1, timerRatio * 3); // Fade in quickly, fade out slowly
        ctx.save();
        ctx.globalAlpha = alpha;
        // Dark overlay
        ctx.fillStyle = 'rgba(0,0,0,0.5)';
        ctx.fillRect(0, h / 2 - 60, w, 120);
        // Team color bar
        const teamColor = this.game.currentTeamColor;
        ctx.fillStyle = teamColor;
        ctx.globalAlpha = alpha * 0.3;
        ctx.fillRect(0, h / 2 - 60, w, 120);
        ctx.globalAlpha = alpha;
        // Team name
        const cpuLabel = this.game.cpuTeams.has(this.game.currentTeamIndex) ? ' (CPU)' : '';
        const teamNames = ['Red', 'Blue', 'Green', 'Orange'];
        ctx.font = 'bold 36px Arial';
        ctx.textAlign = 'center';
        ctx.fillStyle = '#000';
        ctx.fillText(`${teamNames[this.game.currentTeamIndex]} Team${cpuLabel}`, w / 2 + 2, h / 2 + 12);
        // Glow effect
        ctx.shadowColor = teamColor;
        ctx.shadowBlur = 20;
        ctx.fillStyle = '#FFFFFF';
        ctx.fillText(`${teamNames[this.game.currentTeamIndex]} Team${cpuLabel}`, w / 2, h / 2 + 10);
        ctx.shadowBlur = 0;
        // Turn number
        ctx.font = '16px Arial';
        ctx.fillStyle = '#CCCCCC';
        ctx.fillText(`Turn ${this.game.turnCount}`, w / 2, h / 2 + 40);
        ctx.restore();
    }
    drawStar(ctx, cx, cy, r) {
        const spikes = 5;
        const innerR = r * 0.4;
        ctx.beginPath();
        for (let i = 0; i < spikes * 2; i++) {
            const radius = i % 2 === 0 ? r : innerR;
            const angle = (i * Math.PI / spikes) - Math.PI / 2;
            const x = cx + Math.cos(angle) * radius;
            const y = cy + Math.sin(angle) * radius;
            if (i === 0)
                ctx.moveTo(x, y);
            else
                ctx.lineTo(x, y);
        }
        ctx.closePath();
        ctx.fill();
    }
    renderHUD(ctx) {
        const w = this.game.canvas.width;
        const h = this.game.canvas.height;
        // Top bar background
        const topGrad = ctx.createLinearGradient(0, 0, 0, 60);
        topGrad.addColorStop(0, 'rgba(0,0,0,0.7)');
        topGrad.addColorStop(1, 'rgba(0,0,0,0)');
        ctx.fillStyle = topGrad;
        ctx.fillRect(0, 0, w, 60);
        // Team indicators
        const teamBarWidth = Math.min(w * 0.4, 300);
        const teamBarX = (w - teamBarWidth) / 2;
        for (let i = 0; i < this.game.teamCount; i++) {
            const aliveUnits = this.game.getTeamAliveUnits(i).length;
            const isActive = i === this.game.currentTeamIndex;
            // Team box
            ctx.fillStyle = isActive ? types_1.TEAM_COLORS[i] : 'rgba(255,255,255,0.1)';
            if (isActive) {
                ctx.shadowColor = types_1.TEAM_COLORS[i];
                ctx.shadowBlur = 10;
            }
            const boxX = teamBarX + i * (teamBarWidth / this.game.teamCount);
            const boxW = teamBarWidth / this.game.teamCount - 4;
            ctx.fillRect(boxX, 5, boxW, 20);
            // Unit dots
            for (let u = 0; u < 4; u++) {
                const unit = this.game.units.find(un => un.teamId === i &&
                    parseInt(un.id.split('_')[1].replace('unit', '')) === u);
                ctx.fillStyle = unit?.alive ? types_1.TEAM_COLORS[i] : 'rgba(255,255,255,0.2)';
                ctx.beginPath();
                ctx.arc(boxX + 8 + u * 14, 15, 4, 0, Math.PI * 2);
                ctx.fill();
            }
            // CPU indicator
            if (this.game.cpuTeams.has(i)) {
                ctx.font = '9px Arial';
                ctx.fillStyle = '#FFA500';
                ctx.textAlign = 'center';
                ctx.fillText('CPU', boxX + boxW / 2, 38);
            }
            ctx.shadowBlur = 0;
        }
        // Turn timer
        const timerRatio = this.game.turnTime / this.game.maxTurnTime;
        const timerWidth = Math.min(w * 0.3, 250);
        const timerX = (w - timerWidth) / 2;
        // Timer bar background
        ctx.fillStyle = 'rgba(0,0,0,0.5)';
        ctx.fillRect(timerX, 48, timerWidth, 6);
        // Timer fill with color based on urgency
        const timerColor = timerRatio > 0.5 ? '#44FF44' : timerRatio > 0.25 ? '#FFFF44' : '#FF4444';
        ctx.fillStyle = timerColor;
        ctx.fillRect(timerX, 48, timerWidth * timerRatio, 6);
        // Timer text (pulsing when low)
        const pulseScale = this.game.turnTime < 5 ? 1 + Math.sin(Date.now() * 0.015) * 0.3 : 1;
        ctx.font = `bold ${Math.floor(14 * pulseScale)}px Arial`;
        ctx.textAlign = 'center';
        // Timer glow when urgent
        if (this.game.turnTime < 5) {
            ctx.shadowColor = timerColor;
            ctx.shadowBlur = 8 + Math.sin(Date.now() * 0.015) * 4;
        }
        ctx.fillStyle = timerColor;
        ctx.fillText(`${this.game.turnTime.toFixed(1)}s`, w / 2, 46);
        ctx.shadowBlur = 0;
        // Urgency warning text when very low
        if (this.game.turnTime < 3) {
            const warnAlpha = Math.sin(Date.now() * 0.01) * 0.5 + 0.5;
            ctx.font = 'bold 12px Arial';
            ctx.fillStyle = `rgba(255,50,50,${warnAlpha})`;
            ctx.fillText('⚠ TIME RUNNING OUT ⚠', w / 2, 38);
        }
        // Wind indicator
        this.renderWindIndicator(ctx, w - 80, 35);
        // Minimap
        this.renderMinimap(ctx, w, h);
        // Turn info display
        if (!this.game.cpuTeams.has(this.game.currentTeamIndex)) {
            const unit = this.game.getCurrentUnit();
            if (unit) {
                ctx.font = '12px Arial';
                ctx.textAlign = 'left';
                ctx.fillStyle = '#CCCCCC';
                ctx.fillText(`Turn ${this.game.turnCount} • ${unit.name}`, 10, 55);
            }
        }
        // Weapon selection panel (bottom)
        if (this.game.phase === 'weaponSelect' || this.game.phase === 'aiming') {
            this.renderWeaponPanel(ctx, w, h);
        }
        // Aiming UI
        if (this.game.phase === 'aiming') {
            this.renderAimUI(ctx);
        }
    }
    renderWindIndicator(ctx, x, y) {
        ctx.font = '10px Arial';
        ctx.textAlign = 'center';
        ctx.fillStyle = '#AAAAAA';
        ctx.fillText('WIND', x, y - 8);
        // Wind arrow
        const windStrength = Math.abs(this.game.wind);
        const windDir = this.game.wind >= 0 ? 1 : -1;
        ctx.strokeStyle = '#FFFFFF';
        ctx.lineWidth = 2;
        ctx.beginPath();
        ctx.moveTo(x - windStrength * 3 * windDir, y + 5);
        ctx.lineTo(x + windStrength * 3 * windDir, y + 5);
        ctx.stroke();
        // Arrow head
        if (windStrength > 0.5) {
            ctx.beginPath();
            const tipX = x + windStrength * 3 * windDir;
            ctx.moveTo(tipX, y + 5);
            ctx.lineTo(tipX - 4 * windDir, y + 2);
            ctx.lineTo(tipX - 4 * windDir, y + 8);
            ctx.closePath();
            ctx.fillStyle = '#FFFFFF';
            ctx.fill();
        }
        // Wind value
        ctx.font = '9px Arial';
        ctx.fillStyle = '#CCCCCC';
        ctx.fillText(windStrength.toFixed(1), x, y + 20);
    }
    renderWeaponPanel(ctx, w, h) {
        const panelH = 80;
        const panelY = h - panelH;
        // Panel background
        ctx.fillStyle = 'rgba(0,0,0,0.75)';
        ctx.fillRect(0, panelY, w, panelH);
        // Top border glow
        ctx.strokeStyle = this.game.currentTeamColor;
        ctx.lineWidth = 2;
        ctx.shadowColor = this.game.currentTeamColor;
        ctx.shadowBlur = 8;
        ctx.beginPath();
        ctx.moveTo(0, panelY);
        ctx.lineTo(w, panelY);
        ctx.stroke();
        ctx.shadowBlur = 0;
        const weaponW = Math.min(100, (w - 40) / weapons_1.WEAPONS.length);
        const startX = (w - weaponW * weapons_1.WEAPONS.length) / 2;
        for (let i = 0; i < weapons_1.WEAPONS.length; i++) {
            const wx = startX + i * weaponW;
            const wy = panelY + 5;
            const isSelected = this.game.selectedWeaponIndex === i;
            const ammoCount = this.game.weaponAmmoCount[i];
            // Weapon button background
            ctx.fillStyle = isSelected ? 'rgba(255,255,255,0.15)' : 'rgba(255,255,255,0.05)';
            if (isSelected) {
                ctx.strokeStyle = this.game.currentTeamColor;
                ctx.lineWidth = 2;
                ctx.strokeRect(wx + 2, wy + 2, weaponW - 4, panelH - 10);
            }
            // Weapon icon
            ctx.font = '24px Arial';
            ctx.textAlign = 'center';
            ctx.fillText(weapons_1.WEAPONS[i].icon, wx + weaponW / 2, wy + 30);
            // Weapon name
            ctx.font = '9px Arial';
            ctx.fillStyle = '#CCCCCC';
            ctx.fillText(weapons_1.WEAPONS[i].name, wx + weaponW / 2, wy + 48);
            // Ammo count
            ctx.font = 'bold 11px Arial';
            ctx.fillStyle = ammoCount > 0 ? '#FFFFFF' : '#666666';
            ctx.fillText(`${ammoCount}`, wx + weaponW / 2, wy + 63);
            // Store button bounds for click detection (we'll use a global)
            weapons_1.WEAPONS[i]._buttonRect = { x: wx, y: wy, w: weaponW, h: panelH - 10 };
        }
        // Weapon description for selected weapon
        const selWeapon = weapons_1.WEAPONS[this.game.selectedWeaponIndex];
        ctx.font = '10px Arial';
        ctx.fillStyle = '#AAAAAA';
        ctx.textAlign = 'center';
        ctx.fillText(selWeapon.description, w / 2, panelY - 5);
        // Instructions
        ctx.font = '9px Arial';
        ctx.fillStyle = '#666666';
        if (this.game.phase === 'weaponSelect') {
            ctx.fillText('Click weapon or press 1-9, then Enter/Space to aim', w / 2, panelY - 18);
        }
        else {
            ctx.fillText('Aim with mouse • Click/Enter to fire • Right-click/ESC to cancel', w / 2, panelY - 18);
        }
        // Category badge for selected weapon
        const catColors = { indirect: '#FFAA44', direct: '#44AAFF', aoe: '#FF4444' };
        ctx.font = 'bold 9px Arial';
        ctx.fillStyle = catColors[selWeapon.category] || '#FFFFFF';
        ctx.textAlign = 'left';
        ctx.fillText(selWeapon.category.toUpperCase(), startX, panelY + panelH - 5);
    }
    renderAimUI(ctx) {
        const unit = this.game.getCurrentUnit();
        if (!unit || !unit.alive)
            return;
        // Draw trajectory prediction line
        const weapon = weapons_1.WEAPONS[this.game.selectedWeaponIndex];
        if (weapon.category === 'direct')
            return; // No trajectory for direct weapons
        const launchAngle = unit.facing === 1 ? Math.PI + this.game.aimAngle : this.game.aimAngle;
        const speed = weapon.power * this.game.aimPower;
        ctx.strokeStyle = `rgba(255,255,255,0.3)`;
        ctx.lineWidth = 1;
        ctx.setLineDash([4, 4]);
        ctx.beginPath();
        let px = unit.pos.x;
        let py = unit.pos.y - 10;
        let vx = Math.cos(launchAngle) * speed + this.game.wind * 0.1;
        let vy = Math.sin(launchAngle) * speed;
        ctx.moveTo(px, py);
        for (let i = 0; i < 80; i++) {
            vy += 0.35; // gravity
            px += vx;
            py += vy;
            if (py > this.game.waterLevel || px < 0 || px > this.game.mapWidth)
                break;
            ctx.lineTo(px, py);
        }
        ctx.stroke();
        ctx.setLineDash([]);
        // Power indicator on the line
        const powerDotX = unit.pos.x + Math.cos(launchAngle) * speed * 15;
        const powerDotY = (unit.pos.y - 10) + Math.sin(launchAngle) * speed * 15;
        ctx.fillStyle = this.game.currentTeamColor;
        ctx.beginPath();
        ctx.arc(powerDotX, powerDotY, 4, 0, Math.PI * 2);
        ctx.fill();
        // Crosshair at mouse position (in world space)
        const mw = this.game.mouseWorld;
        ctx.strokeStyle = 'rgba(255,255,100,0.6)';
        ctx.lineWidth = 1;
        ctx.beginPath();
        ctx.arc(mw.x, mw.y, 8, 0, Math.PI * 2);
        ctx.stroke();
        ctx.beginPath();
        ctx.moveTo(mw.x - 12, mw.y);
        ctx.lineTo(mw.x + 12, mw.y);
        ctx.moveTo(mw.x, mw.y - 12);
        ctx.lineTo(mw.x, mw.y + 12);
        ctx.stroke();
        // Power meter on side of screen
        this.renderPowerMeter(ctx);
    }
    renderMinimap(ctx, w, h) {
        const mmW = 150;
        const mmH = 75;
        const mmX = w - mmW - 10;
        const mmY = h - mmH - 90;
        // Background
        ctx.fillStyle = 'rgba(0,0,0,0.6)';
        this.roundRect(ctx, mmX, mmY, mmW, mmH, 4);
        ctx.fill();
        ctx.strokeStyle = 'rgba(255,255,255,0.3)';
        ctx.lineWidth = 1;
        this.roundRect(ctx, mmX, mmY, mmW, mmH, 4);
        ctx.stroke();
        const scaleX = mmW / this.game.mapWidth;
        const scaleY = mmH / this.game.mapHeight;
        // Draw terrain outline (simplified)
        for (let x = 0; x < this.game.mapWidth; x += 8) {
            const surfaceY = this.game.terrain.findSurfaceY(x);
            ctx.fillStyle = 'rgba(100,150,80,0.4)';
            ctx.fillRect(mmX + x * scaleX, mmY + surfaceY * scaleY, Math.max(1, 8 * scaleX), (this.game.mapHeight - surfaceY) * scaleY);
        }
        // Water line
        ctx.fillStyle = 'rgba(50,100,200,0.3)';
        ctx.fillRect(mmX, mmY + this.game.waterLevel * scaleY, mmW, (this.game.mapHeight - this.game.waterLevel) * scaleY);
        // Draw units as dots
        for (const unit of this.game.units) {
            if (!unit.alive)
                continue;
            const color = types_1.TEAM_COLORS[unit.teamId];
            ctx.fillStyle = color;
            ctx.beginPath();
            ctx.arc(mmX + unit.pos.x * scaleX, mmY + unit.pos.y * scaleY, 2, 0, Math.PI * 2);
            ctx.fill();
        }
        // Camera viewport indicator
        const cam = this.game.camera;
        const viewW = (w / cam.zoom) * scaleX;
        const viewH = (h / cam.zoom) * scaleY;
        const viewX = mmX + cam.x * scaleX - viewW / 2;
        const viewY = mmY + cam.y * scaleY - viewH / 2;
        ctx.strokeStyle = 'rgba(255,255,255,0.6)';
        ctx.lineWidth = 1;
        ctx.strokeRect(viewX, viewY, viewW, viewH);
        // Label
        ctx.font = '8px Arial';
        ctx.fillStyle = '#AAAAAA';
        ctx.textAlign = 'center';
        ctx.fillText('MAP', mmX + mmW / 2, mmY - 3);
    }
    renderPowerMeter(ctx) {
        const x = 30;
        const y = 80;
        const w = 15;
        const h = 200;
        // Background
        ctx.fillStyle = 'rgba(0,0,0,0.5)';
        ctx.fillRect(x - 2, y - 2, w + 4, h + 4);
        // Fill based on power
        const fillH = h * this.game.aimPower;
        const grad = ctx.createLinearGradient(0, y + h, 0, y);
        grad.addColorStop(0, '#44FF44');
        grad.addColorStop(0.5, '#FFFF44');
        grad.addColorStop(1, '#FF4444');
        ctx.fillStyle = grad;
        ctx.fillRect(x, y + h - fillH, w, fillH);
        // Border
        ctx.strokeStyle = 'rgba(255,255,255,0.3)';
        ctx.lineWidth = 1;
        ctx.strokeRect(x - 2, y - 2, w + 4, h + 4);
        // Label
        ctx.font = '9px Arial';
        ctx.textAlign = 'center';
        ctx.fillStyle = '#FFFFFF';
        ctx.fillText('POWER', x + w / 2, y - 6);
        ctx.fillText(`${Math.floor(this.game.aimPower * 100)}%`, x + w / 2, y + h + 14);
        // Scroll wheel hint
        ctx.font = '8px Arial';
        ctx.fillStyle = '#888888';
        ctx.fillText('Scroll', x + w / 2, y + h + 26);
    }
    renderMenu(ctx) {
        const w = this.game.canvas.width;
        const h = this.game.canvas.height;
        const time = Date.now() * 0.001;
        // Animated background with gradient
        const bgGrad = ctx.createRadialGradient(w / 2, h / 2, 50, w / 2, h / 2, w * 0.7);
        bgGrad.addColorStop(0, '#1a1a3e');
        bgGrad.addColorStop(0.5, '#0d0d2b');
        bgGrad.addColorStop(1, '#050510');
        ctx.fillStyle = bgGrad;
        ctx.fillRect(0, 0, w, h);
        // Animated particles in background
        for (let i = 0; i < 30; i++) {
            const px = ((i * 97 + time * 20) % w);
            const py = ((i * 53 + Math.sin(time + i) * 30) % h);
            const size = 1 + Math.sin(time * 2 + i) * 0.5;
            ctx.fillStyle = `rgba(255,200,100,${0.1 + Math.sin(time + i * 0.7) * 0.05})`;
            ctx.beginPath();
            ctx.arc(px, py, size, 0, Math.PI * 2);
            ctx.fill();
        }
        // Dark overlay for readability
        ctx.fillStyle = 'rgba(0,0,0,0.4)';
        ctx.fillRect(0, 0, w, h);
        // Title
        ctx.textAlign = 'center';
        // Title shadow
        ctx.font = 'bold 48px Arial';
        ctx.fillStyle = '#000';
        ctx.fillText('WORMS BATTLE', w / 2 + 2, 122);
        // Title text with gradient
        const titleGrad = ctx.createLinearGradient(w / 2 - 150, 80, w / 2 + 150, 130);
        titleGrad.addColorStop(0, '#FF6B4A');
        titleGrad.addColorStop(0.5, '#FFD700');
        titleGrad.addColorStop(1, '#FF6B4A');
        ctx.fillStyle = titleGrad;
        ctx.fillText('WORMS BATTLE', w / 2, 120);
        // Subtitle
        ctx.font = '18px Arial';
        ctx.fillStyle = '#AAAAAA';
        ctx.fillText('Artillery Warfare', w / 2, 150);
        // Menu options
        const menuItems = [
            { label: '🎮  Start Game (2 Teams)', action: () => this.game.startGame(2, new Set(), this.game.terrainShape, this.game.difficulty) },
            { label: '🤖  vs CPU (1v1)', action: () => this.game.startGame(2, new Set([1]), this.game.terrainShape, this.game.difficulty) },
            { label: '⚔️  4 Teams Battle', action: () => this.game.startGame(4, new Set(), this.game.terrainShape, this.game.difficulty) },
            { label: '🧠  vs CPU (2v2)', action: () => this.game.startGame(4, new Set([2, 3]), this.game.terrainShape, this.game.difficulty) },
        ];
        const startY = 200;
        const itemHeight = 50;
        for (let i = 0; i < menuItems.length; i++) {
            const y = startY + i * (itemHeight + 15);
            // Button background
            ctx.fillStyle = 'rgba(255,255,255,0.08)';
            ctx.fillRect(w / 2 - 160, y, 320, itemHeight);
            // Hover highlight (we'll track mouse position)
            const mx = this.game.mousePos.x;
            const my = this.game.mousePos.y;
            if (mx > w / 2 - 160 && mx < w / 2 + 160 && my > y && my < y + itemHeight) {
                ctx.fillStyle = 'rgba(255,255,255,0.15)';
                ctx.fillRect(w / 2 - 160, y, 320, itemHeight);
                // Glow border
                ctx.strokeStyle = '#FFD700';
                ctx.lineWidth = 2;
                ctx.strokeRect(w / 2 - 160, y, 320, itemHeight);
            }
            // Button text
            ctx.font = '18px Arial';
            ctx.fillStyle = '#FFFFFF';
            ctx.textAlign = 'center';
            ctx.fillText(menuItems[i].label, w / 2, y + 32);
            // Store button bounds for click detection
            menuItems[i]._buttonRect = { x: w / 2 - 160, y, w: 320, h: itemHeight };
        }
        // Terrain selector
        ctx.font = '14px Arial';
        ctx.fillStyle = '#AAAAAA';
        ctx.fillText('Terrain:', w / 2, startY + menuItems.length * (itemHeight + 15) + 20);
        const terrainTypes = [
            { label: '🏝️ Island', value: 'island' },
            { label: '🕳️ Cave', value: 'cave' },
            { label: '⛰️ Two Islands', value: 'twoIslands' },
        ];
        const terrainY = startY + menuItems.length * (itemHeight + 15) + 35;
        for (let i = 0; i < terrainTypes.length; i++) {
            const tx = w / 2 - 140 + i * 100;
            ctx.fillStyle = this.game.terrainShape === terrainTypes[i].value ?
                'rgba(255,215,0,0.3)' : 'rgba(255,255,255,0.08)';
            ctx.fillRect(tx, terrainY, 80, 35);
            if (this.game.terrainShape === terrainTypes[i].value) {
                ctx.strokeStyle = '#FFD700';
                ctx.lineWidth = 1;
                ctx.strokeRect(tx, terrainY, 80, 35);
            }
            ctx.font = '12px Arial';
            ctx.fillStyle = '#FFFFFF';
            ctx.textAlign = 'center';
            ctx.fillText(terrainTypes[i].label, tx + 40, terrainY + 22);
            terrainTypes[i]._buttonRect = { x: tx, y: terrainY, w: 80, h: 35 };
        }
        // Difficulty selector
        ctx.font = '14px Arial';
        ctx.fillStyle = '#AAAAAA';
        ctx.fillText('Difficulty:', w / 2, terrainY + 55);
        const diffTypes = [
            { label: 'Easy', value: 'easy' },
            { label: 'Medium', value: 'medium' },
            { label: 'Hard', value: 'hard' },
        ];
        const diffY = terrainY + 70;
        for (let i = 0; i < diffTypes.length; i++) {
            const tx = w / 2 - 140 + i * 100;
            ctx.fillStyle = this.game.difficulty === diffTypes[i].value ?
                'rgba(255,215,0,0.3)' : 'rgba(255,255,255,0.08)';
            ctx.fillRect(tx, diffY, 80, 35);
            if (this.game.difficulty === diffTypes[i].value) {
                ctx.strokeStyle = '#FFD700';
                ctx.lineWidth = 1;
                ctx.strokeRect(tx, diffY, 80, 35);
            }
            ctx.font = '12px Arial';
            ctx.fillStyle = '#FFFFFF';
            ctx.textAlign = 'center';
            ctx.fillText(diffTypes[i].label, tx + 40, diffY + 22);
            diffTypes[i]._buttonRect = { x: tx, y: diffY, w: 80, h: 35 };
        }
        // Controls info
        ctx.font = '11px Arial';
        ctx.fillStyle = '#666666';
        ctx.fillText('Arrow Keys/WASD: Move | Space/Up: Jump | Scroll: Power', w / 2, h - 30);
        ctx.fillText('Click to fire | Right-click/ESC: End turn', w / 2, h - 15);
    }
    renderGameOver(ctx) {
        const w = this.game.canvas.width;
        const h = this.game.canvas.height;
        const time = Date.now() * 0.001;
        // Dark overlay with gradient
        const bgGrad = ctx.createRadialGradient(w / 2, h / 2, 50, w / 2, h / 2, w * 0.7);
        bgGrad.addColorStop(0, 'rgba(20,20,40,0.9)');
        bgGrad.addColorStop(1, 'rgba(5,5,15,0.95)');
        ctx.fillStyle = bgGrad;
        ctx.fillRect(0, 0, w, h);
        // Find winner
        let winnerTeam = -1;
        for (let t = 0; t < this.game.teamCount; t++) {
            if (this.game.getTeamAliveUnits(t).length > 0) {
                winnerTeam = t;
                break;
            }
        }
        ctx.textAlign = 'center';
        // Animated confetti particles for victory
        if (winnerTeam >= 0) {
            const teamColor = types_1.TEAM_COLORS[winnerTeam];
            // Confetti
            for (let i = 0; i < 40; i++) {
                const cx = ((i * 73 + time * 50) % w);
                const cy = ((i * 47 + Math.sin(time + i) * 20 + time * 30) % h);
                ctx.fillStyle = [teamColor, '#FFD700', '#FFFFFF', '#FF6B4A'][i % 4];
                ctx.globalAlpha = 0.5;
                ctx.fillRect(cx, cy, 4, 4);
            }
            ctx.globalAlpha = 1;
            // Winner announcement with glow
            ctx.font = 'bold 52px Arial';
            ctx.fillStyle = '#000';
            ctx.fillText('VICTORY!', w / 2 + 2, h / 2 - 48);
            ctx.shadowColor = teamColor;
            ctx.shadowBlur = 25;
            const titleGrad = ctx.createLinearGradient(w / 2 - 100, h / 2 - 60, w / 2 + 100, h / 2 - 30);
            titleGrad.addColorStop(0, teamColor);
            titleGrad.addColorStop(0.5, '#FFD700');
            titleGrad.addColorStop(1, teamColor);
            ctx.fillStyle = titleGrad;
            ctx.fillText('VICTORY!', w / 2, h / 2 - 50);
            ctx.shadowBlur = 0;
            // Team name with CPU label
            const cpuLabel = this.game.cpuTeams.has(winnerTeam) ? ' (CPU)' : '';
            ctx.font = 'bold 30px Arial';
            ctx.fillStyle = '#FFFFFF';
            ctx.fillText(`Team ${['Red', 'Blue', 'Green', 'Orange'][winnerTeam]}${cpuLabel}`, w / 2, h / 2 + 5);
            // Trophy with animation
            const trophyBounce = Math.sin(time * 3) * 5;
            ctx.font = '70px Arial';
            ctx.fillText('🏆', w / 2, h / 2 - 110 + trophyBounce);
            // Stats summary
            ctx.font = '14px Arial';
            ctx.fillStyle = '#AAAAAA';
            ctx.fillText(`Game lasted ${this.game.turnCount} turns`, w / 2, h / 2 + 35);
        }
        else {
            ctx.font = 'bold 48px Arial';
            ctx.fillStyle = '#FF4444';
            ctx.fillText('DRAW!', w / 2, h / 2 - 10);
        }
        // Restart button with hover effect
        const btnY = h / 2 + 65;
        const mx = this.game.mousePos.x;
        const my = this.game.mousePos.y;
        const isHovered = mx > w / 2 - 100 && mx < w / 2 + 100 && my > btnY && my < btnY + 45;
        ctx.fillStyle = isHovered ? 'rgba(255,215,0,0.3)' : 'rgba(255,255,255,0.1)';
        this.roundRect(ctx, w / 2 - 100, btnY, 200, 45, 8);
        ctx.fill();
        if (isHovered) {
            ctx.strokeStyle = '#FFD700';
            ctx.lineWidth = 2;
            this.roundRect(ctx, w / 2 - 100, btnY, 200, 45, 8);
            ctx.stroke();
        }
        ctx.font = 'bold 18px Arial';
        ctx.fillStyle = '#FFFFFF';
        ctx.fillText('Play Again', w / 2, btnY + 30);
        // Menu button
        const menuBtnY = btnY + 55;
        const isMenuHovered = mx > w / 2 - 100 && mx < w / 2 + 100 && my > menuBtnY && my < menuBtnY + 45;
        ctx.fillStyle = isMenuHovered ? 'rgba(255,255,255,0.2)' : 'rgba(255,255,255,0.05)';
        this.roundRect(ctx, w / 2 - 100, menuBtnY, 200, 45, 8);
        ctx.fill();
        if (isMenuHovered) {
            ctx.strokeStyle = '#FFFFFF';
            ctx.lineWidth = 1;
            this.roundRect(ctx, w / 2 - 100, menuBtnY, 200, 45, 8);
            ctx.stroke();
        }
        ctx.font = '16px Arial';
        ctx.fillStyle = '#CCCCCC';
        ctx.fillText('Main Menu', w / 2, menuBtnY + 30);
        // Store buttons for click detection
        this._restartButton = { x: w / 2 - 100, y: btnY, w: 200, h: 45 };
        this._menuButton = { x: w / 2 - 100, y: menuBtnY, w: 200, h: 45 };
    }
    // Handle menu clicks
    handleMenuClick(mx, my) {
        const menuItems = [
            { action: () => this.game.startGame(2, new Set(), this.game.terrainShape, this.game.difficulty), rect: null },
            { action: () => this.game.startGame(2, new Set([1]), this.game.terrainShape, this.game.difficulty), rect: null },
            { action: () => this.game.startGame(4, new Set(), this.game.terrainShape, this.game.difficulty), rect: null },
            { action: () => this.game.startGame(4, new Set([2, 3]), this.game.terrainShape, this.game.difficulty), rect: null },
        ];
        // Check menu buttons (we need to recalculate positions)
        const w = this.game.canvas.width;
        const h = this.game.canvas.height;
        const startY = 200;
        const itemHeight = 50;
        for (let i = 0; i < 4; i++) {
            const y = startY + i * (itemHeight + 15);
            if (mx > w / 2 - 160 && mx < w / 2 + 160 && my > y && my < y + itemHeight) {
                this.game.sounds.playMenuClick();
                menuItems[i].action();
                return;
            }
        }
        // Check terrain buttons
        const terrainTypes = [
            { value: 'island' }, { value: 'cave' }, { value: 'twoIslands' },
        ];
        const terrainY = startY + 4 * (itemHeight + 15) + 35;
        for (let i = 0; i < 3; i++) {
            const tx = w / 2 - 140 + i * 100;
            if (mx > tx && mx < tx + 80 && my > terrainY && my < terrainY + 35) {
                this.game.sounds.playMenuClick();
                this.game.terrainShape = terrainTypes[i].value;
                return;
            }
        }
        // Check difficulty buttons
        const diffTypes = [
            { value: 'easy' }, { value: 'medium' }, { value: 'hard' },
        ];
        const diffY = terrainY + 70;
        for (let i = 0; i < 3; i++) {
            const tx = w / 2 - 140 + i * 100;
            if (mx > tx && mx < tx + 80 && my > diffY && my < diffY + 35) {
                this.game.sounds.playMenuClick();
                this.game.difficulty = diffTypes[i].value;
                return;
            }
        }
    }
    handleGameOverClick(mx, my) {
        const w = this.game.canvas.width;
        const h = this.game.canvas.height;
        // Restart button
        const restartY = h / 2 + 65;
        if (mx > w / 2 - 100 && mx < w / 2 + 100 && my > restartY && my < restartY + 45) {
            this.game.sounds.playMenuClick();
            this.game.startGame(this.game.teamCount, new Set(this.game.cpuTeams), this.game.terrainShape, this.game.difficulty);
            return;
        }
        // Menu button
        const menuBtnY = restartY + 55;
        if (mx > w / 2 - 100 && mx < w / 2 + 100 && my > menuBtnY && my < menuBtnY + 45) {
            this.game.sounds.playMenuClick();
            this.game.phase = 'menu';
        }
    }
    handleWeaponPanelClick(mx, my) {
        if (this.game.phase !== 'weaponSelect')
            return;
        const w = this.game.canvas.width;
        const h = this.game.canvas.height;
        const panelH = 80;
        const panelY = h - panelH;
        if (my < panelY || my > h)
            return;
        const weaponW = Math.min(100, (w - 40) / weapons_1.WEAPONS.length);
        const startX = (w - weaponW * weapons_1.WEAPONS.length) / 2;
        for (let i = 0; i < weapons_1.WEAPONS.length; i++) {
            const wx = startX + i * weaponW;
            if (mx > wx && mx < wx + weaponW) {
                this.game.selectedWeaponIndex = i;
                this.game.sounds.playMenuClick();
                return;
            }
        }
    }
    handlePowerScroll(delta) {
        if (this.game.phase !== 'aiming')
            return;
        this.game.aimPower = Math.max(0.1, Math.min(1, this.game.aimPower - delta * 0.02));
    }
}
exports.Renderer = Renderer;
};

// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"thinkingcap-qwen3.6-27b","provider":"lmstudio","persona":"typescript","sessionId":"42238400-5229-4aed-974b-44511a39e143","tokensIn":2937067,"tokensOut":51369,"tokensTotal":2988436,"turns":67,"toolCalls":66,"failedToolCalls":1,"timestamp":"2026-07-31T16:45:20.515Z"} -->