← DOOM results

DOOM

Qwen 3.8 · typescript

Initial view of DOOM

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 68,192 bytes · SHA-256 cdfdc1a84f54
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>HELLFORGE — Retro Raycaster FPS</title>
<style>
/* ── css: layout ── */
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { height: 100%; }
body {
  background: #0a0a0f;
  color: #e8e0c8;
  font-family: "Courier New", ui-monospace, monospace;
  display: flex; align-items: center; justify-content: center;
  overflow: hidden;
  -webkit-user-select: none; user-select: none;
}
#app {
  width: min(96vw, 1024px);
  display: flex; flex-direction: column; gap: 10px;
}
#screen {
  position: relative;
  width: 100%;
  aspect-ratio: 8 / 5;
  background: #000;
  border: 3px solid #3a3128;
  box-shadow: 0 0 0 2px #14100c, 0 0 42px rgba(255, 90, 30, 0.13), inset 0 0 34px rgba(0,0,0,0.75);
  cursor: crosshair;
}
#screen.locking { cursor: none; }
#game { position: absolute; inset: 0; width: 100%; height: 100%; image-rendering: pixelated; display: block; }
.crt {
  position: absolute; inset: 0; pointer-events: none; z-index: 5;
  background:
    repeating-linear-gradient(0deg, rgba(0,0,0,0.16) 0 1px, transparent 1px 3px),
    radial-gradient(ellipse at center, transparent 55%, rgba(0,0,0,0.5) 100%);
  mix-blend-mode: multiply;
}
.flash {
  position: absolute; inset: 0; z-index: 6; pointer-events: none;
  background: rgba(255, 40, 10, 0.55); opacity: 0;
  transition: opacity 0.28s ease-out;
}
.flash.show { opacity: 1; transition: opacity 0.04s; }
#dmg {
  position: absolute; inset: 0; z-index: 6; pointer-events: none; opacity: 0;
  background: radial-gradient(ellipse at center, transparent 45%, rgba(190, 20, 10, 0.55) 100%);
  transition: opacity 0.15s;
}
#banner {
  position: absolute; top: 16%; left: 0; right: 0; z-index: 7; text-align: center;
  font-weight: bold; letter-spacing: 5px; font-size: clamp(16px, 2.6vw, 30px);
  color: #ffd23e; text-shadow: 0 0 12px rgba(255, 170, 40, 0.7), 3px 3px 0 #401800;
  opacity: 0; pointer-events: none; transition: opacity 0.35s;
}
#banner.show { opacity: 1; }
#hint {
  position: absolute; bottom: 8px; left: 0; right: 0; z-index: 7; text-align: center;
  font-size: 12px; letter-spacing: 2px; color: #cbb98a; opacity: 0.85; pointer-events: none;
}
#title { font-size: clamp(13px, 1.8vw, 19px); letter-spacing: 4px; font-weight: bold; color: #ff5b2e; text-shadow: 2px 2px 0 #2b0a00; }
#title span { color: #e8e0c8; }
/* ── css: hud ── */
#hud {
  display: grid; grid-template-columns: 1fr auto 1fr; align-items: stretch; gap: 10px;
  font-weight: bold;
}
.panel {
  background: #191410;
  border: 2px solid #4a3a26;
  box-shadow: inset 0 0 14px rgba(0,0,0,0.8);
  padding: 6px 12px;
  display: flex; flex-direction: column; justify-content: center; gap: 3px;
  min-height: 64px;
}
.panel .row { display: flex; align-items: baseline; gap: 10px; justify-content: space-between; }
.panel .lbl { font-size: 11px; letter-spacing: 3px; color: #9b8a62; }
.panel .val { font-size: clamp(20px, 3vw, 32px); letter-spacing: 2px; color: #e8e0c8; text-shadow: 2px 2px 0 #000; }
#hp-low { color: #ff3b28 !important; animation: blink 0.6s steps(2) infinite; }
#ammo-low { color: #ff3b28 !important; }
#ammo-empty { color: #8a2a1a !important; }
@keyframes blink { 50% { opacity: 0.35; } }
#mid { display: flex; flex-direction: column; gap: 4px; min-width: 210px; }
#weapons { display: flex; gap: 6px; justify-content: center; }
.wbtn {
  width: 42px; height: 34px; display: flex; align-items: center; justify-content: center;
  font-size: 18px; background: #14100c; border: 2px solid #4a3a26; color: #8a7a52;
}
.wbtn.on { background: #332514; color: #ffd23e; border-color: #ffb347; box-shadow: 0 0 10px rgba(255,170,50,0.35); }
#objrow {
  display: flex; justify-content: space-between; font-size: 12px; letter-spacing: 2px;
  color: #cbb98a; padding: 0 4px;
}
#score { color: #ffd23e; }
#timer { color: #7fd4c8; }
#controls { font-size: 10px; letter-spacing: 2px; color: #6f6248; text-align: center; }
</style>
</head>
<body>
<div id="screen">
  <canvas id="game" width="320" height="200"></canvas>
  <div class="crt"></div>
  <div id="dmg"></div>
  <div class="flash" id="flash"></div>
  <div id="banner"></div>
  <div id="hint">CLICK TO DEPLOY</div>
</div>
<div id="hud">
  <div class="panel">
    <div class="row"><span class="lbl">HEALTH</span><span class="val" id="hp">100</span></div>
    <div class="row"><span class="lbl">AMMO</span><span class="val" id="ammo">50</span></div>
  </div>
  <div id="mid">
    <div id="weapons"></div>
    <div id="objrow"><span id="obj">WAVE 1</span><span id="score">0</span><span id="timer">5:00</span></div>
  </div>
  <div class="panel">
    <div class="row"><span class="lbl">KILLS</span><span class="val" id="kills">0</span></div>
    <div class="row"><span class="lbl">BEST</span><span class="val" id="best">0</span></div>
  </div>
</div>
<div id="title">HELLFORGE <span>// click to deploy, mouse to aim, [1-3] weapons, [E] portal</span></div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./game":"src/game.ts","./types":"src/types.ts"},"src/world.ts":{"./types":"src/types.ts"},"src/enemy.ts":{"./types":"src/types.ts","./world":"src/world.ts","./audio":"src/audio.ts"},"src/render.ts":{"./types":"src/types.ts","./world":"src/world.ts"},"src/game.ts":{"./types":"src/types.ts","./world":"src/world.ts","./enemy":"src/enemy.ts","./render":"src/render.ts","./audio":"src/audio.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 types_1 = require("./types");
function $(id) {
    return document.getElementById(id);
}
function start() {
    const canvas = $('game');
    const ctx = canvas.getContext('2d', { willReadFrequently: true });
    ctx.imageSmoothingEnabled = false;
    const weapons = $('weapons');
    for (const w of types_1.WEAPONS) {
        const b = document.createElement('div');
        b.className = 'wbtn';
        b.textContent = w.icon;
        b.title = w.name;
        weapons.appendChild(b);
    }
    const el = {
        screen: $('screen'),
        canvas,
        ctx,
        hp: $('hp'),
        ammo: $('ammo'),
        kills: $('kills'),
        best: $('best'),
        obj: $('obj'),
        score: $('score'),
        timer: $('timer'),
        banner: $('banner'),
        dmg: $('dmg'),
        flash: $('flash'),
        hint: $('hint'),
        weapons,
    };
    const game = new game_1.Game(el);
    (0, game_1.attachInput)(game);
    window.__game = game;
    let last = performance.now();
    const frame = (now) => {
        const dt = Math.min(0.05, (now - last) / 1000);
        last = now;
        game.update(dt);
        requestAnimationFrame(frame);
    };
    requestAnimationFrame(frame);
}
start();
};

// ── module: src/types.ts ──
__mods["src/types.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.WEAPONS = exports.ECOL = exports.EMAXHP = exports.FOG_MAX = exports.WTOP = exports.GROUND = exports.CELL = exports.FH = exports.FW = exports.H = exports.W = void 0;
exports.W = 320;
exports.H = 200;
exports.FW = 8; // wall grid width (cols)
exports.FH = 8; // wall grid height (rows)
exports.CELL = 40; // world units per cell
exports.GROUND = 66; // wall base line px
exports.WTOP = 15;
exports.FOG_MAX = 7.5;
exports.EMAXHP = [40, 120, 30];
exports.ECOL = [0, 1, 2]; // enemy tint palette per kind
exports.WEAPONS = [
    { name: 'PISTOL', icon: '✦', auto: false, rate: 0.26, dmg: [22, 48], spread: 0.03, mag: Infinity, reload: 0, sound: 'pistol' },
    { name: 'SCATTER', icon: '♣', auto: true, rate: 0.78, dmg: [14, 20], spread: 0.085, mag: 8, reload: 1.2, pellets: 7, sound: 'shot' },
    { name: 'HEAVY', icon: '◆', auto: true, rate: 0.13, dmg: [7, 12], spread: 0.055, mag: 45, reload: 1.6, sound: 'heavy' },
];
};

// ── module: src/world.ts ──
__mods["src/world.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.cover = exports.pickups = exports.enemySpawns = exports.spawn = exports.portalCells = exports.grid = void 0;
exports.cellAt = cellAt;
exports.isSolid = isSolid;
exports.portalActive = portalActive;
exports.openPortal = openPortal;
exports.portalCenter = portalCenter;
exports.moveBody = moveBody;
exports.blockedCircle = blockedCircle;
exports.lineOfSight = lineOfSight;
const types_1 = require("./types");
const L = (c, r) => r * types_1.FW + c;
// map legend: 0 floor, 1 stone, 2 brick, 3 hell, 4 metal
const MAPS = [
    [1, 1, 1, 1, 3, 1, 1, 1],
    [1, 0, 0, 0, 0, 0, 0, 1],
    [1, 0, 2, 0, 0, 0, 0, 1],
    [1, 0, 0, 0, 0, 0, 0, 1],
    [1, 0, 0, 0, 0, 0, 0, 1],
    [1, 0, 0, 0, 0, 2, 0, 1],
    [1, 0, 0, 0, 0, 0, 0, 1],
    [1, 1, 1, 1, 1, 1, 1, 1],
];
exports.grid = MAPS.flat().map(v => (v === 3 ? 3 : v)); // portal cells handled dynamically
exports.portalCells = [];
for (let i = 0; i < exports.grid.length; i++)
    if (MAPS.flat()[i] === 3)
        exports.portalCells.push(i);
exports.spawn = { x: 1.5, y: 6.8 };
// enemy spawn points (grid coords) — corners of the arena
exports.enemySpawns = [
    { x: 1.35, y: 1.35 }, { x: 6.65, y: 1.35 }, { x: 1.35, y: 6.65 }, { x: 6.65, y: 6.65 },
    { x: 1.4, y: 4 }, { x: 6.6, y: 4 }, { x: 4, y: 1.4 }, { x: 4, y: 6.6 },
];
exports.pickups = [
    { x: 3.5, y: 1.5, kind: 'h' },
    { x: 4.5, y: 4.0, kind: 'a' },
    { x: 2.0, y: 3.5, kind: 'a' },
    { x: 5.0, y: 6.0, kind: 'h' },
    { x: 3.0, y: 6.5, kind: 'a' },
];
exports.cover = [
    { x: 2, y: 2, r: 0.55 }, { x: 5, y: 2, r: 0.55 },
    { x: 2, y: 5, r: 0.55 }, { x: 5, y: 5, r: 0.55 },
];
function cellAt(x, y) {
    const c = x | 0, r = y | 0;
    if (c < 0 || r < 0 || c >= types_1.FW || r >= types_1.FH)
        return 1;
    return exports.grid[r * types_1.FW + c];
}
function isSolid(x, y) {
    return cellAt(x, y) !== 0;
}
function portalActive() {
    return exports.portalCells.length > 0;
}
function openPortal(on) {
    for (const i of exports.portalCells)
        exports.grid[i] = on ? 0 : 3;
}
function portalCenter() {
    let sx = 0, sy = 0, n = 0;
    for (const i of exports.portalCells) {
        sx += (i % types_1.FW) + 0.5;
        sy += ((i / types_1.FW) | 0) + 0.5;
        n++;
    }
    return { x: sx / n, y: sy / n };
}
// move with sliding collision; radius r. returns final pos + whether any axis blocked.
function moveBody(px, py, dx, dy, r) {
    let blocked = false;
    if (dx !== 0) {
        const s = Math.sign(dx);
        const tx = px + dx - s * r;
        if (isSolid(tx, py - r) || isSolid(tx, py + r))
            blocked = true;
        else
            px += dx;
    }
    if (dy !== 0) {
        const s = Math.sign(dy);
        const ty = py + dy - s * r;
        if (isSolid(px - r, ty) || isSolid(px + r, ty))
            blocked = true;
        else
            py += dy;
    }
    return { x: px, y: py, blocked };
}
// simple circle collision against walls + cover blocks
function blockedCircle(x, y, r) {
    if (isSolid(x - r, y - r) || isSolid(x + r, y - r) || isSolid(x - r, y + r) || isSolid(x + r, y + r))
        return true;
    for (const c of exports.cover) {
        const dx = x - c.x, dy = y - c.y;
        if (dx * dx + dy * dy < (c.r + r) * (c.r + r))
            return true;
    }
    return false;
}
// true if any wall/cover between (x0,y0)-(x1,y1)
function lineOfSight(x0, y0, x1, y1) {
    const dx = x1 - x0, dy = y1 - y0;
    const dist = Math.hypot(dx, dy);
    const steps = Math.ceil(dist / 0.08);
    for (let i = 1; i < steps; i++) {
        const t = i / steps;
        if (isSolid(x0 + dx * t, y0 + dy * t))
            return false;
        // cover blocks (sample only near them is fine for coarse steps)
        for (const c of exports.cover) {
            const px = x0 + dx * t, py = y0 + dy * t;
            const cx = px - c.x, cy = py - c.y;
            if (cx * cx + cy * cy < c.r * c.r)
                return false;
        }
    }
    return true;
}
};

// ── module: src/audio.ts ──
__mods["src/audio.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.sfx = void 0;
exports.unlock = unlock;
exports.toggleMute = toggleMute;
// tiny procedural synth — no assets
let ac = null;
let master = null;
let muted = false;
function ctx() {
    if (ac)
        return ac;
    try {
        const AC = window.AudioContext || window.webkitAudioContext;
        ac = new AC();
        master = ac.createGain();
        master.gain.value = 0.5;
        master.connect(ac.destination);
    }
    catch {
        ac = null;
    }
    return ac;
}
function unlock() {
    const a = ctx();
    if (a && a.state === 'suspended')
        void a.resume();
}
function toggleMute() {
    muted = !muted;
    if (master)
        master.gain.value = muted ? 0 : 0.5;
    return muted;
}
function tone(freq, dur, type, vol, slide = 0, delay = 0) {
    const a = ctx();
    if (!a || !master || muted)
        return;
    const t0 = a.currentTime + delay;
    const o = a.createOscillator();
    const g = a.createGain();
    o.type = type;
    o.frequency.setValueAtTime(freq, t0);
    if (slide !== 0)
        o.frequency.exponentialRampToValueAtTime(Math.max(30, freq + slide), t0 + dur);
    g.gain.setValueAtTime(vol, t0);
    g.gain.exponentialRampToValueAtTime(0.001, t0 + dur);
    o.connect(g);
    g.connect(master);
    o.start(t0);
    o.stop(t0 + dur + 0.02);
}
function noise(dur, vol, filterFreq = 1200, delay = 0) {
    const a = ctx();
    if (!a || !master || muted)
        return;
    const t0 = a.currentTime + delay;
    const n = Math.floor(a.sampleRate * dur);
    const buf = a.createBuffer(1, n, a.sampleRate);
    const d = buf.getChannelData(0);
    for (let i = 0; i < n; i++)
        d[i] = (Math.random() * 2 - 1) * (1 - i / n);
    const src = a.createBufferSource();
    src.buffer = buf;
    const f = a.createBiquadFilter();
    f.type = 'lowpass';
    f.frequency.value = filterFreq;
    const g = a.createGain();
    g.gain.setValueAtTime(vol, t0);
    g.gain.exponentialRampToValueAtTime(0.001, t0 + dur);
    src.connect(f);
    f.connect(g);
    g.connect(master);
    src.start(t0);
}
exports.sfx = {
    pistol() { tone(320, 0.12, 'square', 0.25, -220); noise(0.08, 0.2, 2400); },
    shot() { noise(0.22, 0.5, 1600); tone(140, 0.18, 'sawtooth', 0.3, -90); },
    heavy() { noise(0.09, 0.45, 2600); tone(210, 0.1, 'square', 0.22, -140); },
    empty() { tone(90, 0.05, 'square', 0.15); },
    reload() { tone(500, 0.06, 'square', 0.12, 200); tone(700, 0.06, 'square', 0.12, 200, 0.12); },
    hurt() { tone(160, 0.2, 'sawtooth', 0.3, -80); noise(0.12, 0.25, 800); },
    enemyHit() { tone(240, 0.07, 'square', 0.14, -90); },
    enemyDie() { tone(110, 0.3, 'sawtooth', 0.3, -70); noise(0.2, 0.2, 600); },
    pickup() { tone(660, 0.08, 'square', 0.18, 0); tone(990, 0.1, 'square', 0.18, 0, 0.08); },
    lunge() { tone(90, 0.25, 'sawtooth', 0.3, 60); },
    portalOpen() { tone(80, 1.1, 'sine', 0.4, 220); tone(160, 1.1, 'sine', 0.25, 300, 0.1); },
    portalGo() { tone(120, 0.9, 'sine', 0.5, 500); tone(240, 0.9, 'sine', 0.3, 700, 0.15); },
    wave() { tone(220, 0.15, 'square', 0.2, 0); tone(330, 0.15, 'square', 0.2, 0, 0.15); tone(440, 0.25, 'square', 0.2, 0, 0.3); },
    die() { tone(200, 0.8, 'sawtooth', 0.4, -160); noise(0.5, 0.3, 500); },
    respawn() { tone(330, 0.12, 'square', 0.2, 0); tone(495, 0.12, 'square', 0.2, 0, 0.1); tone(660, 0.2, 'square', 0.2, 0, 0.2); },
};
};

// ── module: src/enemy.ts ──
__mods["src/enemy.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ENEMY_CFG = void 0;
exports.makeEnemy = makeEnemy;
exports.pickSpawn = pickSpawn;
exports.damageEnemy = damageEnemy;
exports.updateEnemies = updateEnemies;
exports.updateBullets = updateBullets;
exports.updatePickups = updatePickups;
exports.updateParticles = updateParticles;
const types_1 = require("./types");
const world_1 = require("./world");
const audio_1 = require("./audio");
exports.ENEMY_CFG = [
    { speed: 1.9, dmg: 8, atkRange: 0.75, shootRange: 5.6, shootCd: 1.7, r: 0.3, hpMult: 1, score: 100 },
    { speed: 1.0, dmg: 16, atkRange: 0.9, shootRange: 0, shootCd: 0, r: 0.42, hpMult: 1, score: 300 },
    { speed: 3.1, dmg: 10, atkRange: 0.7, shootRange: 0, shootCd: 0, r: 0.26, hpMult: 0.75, score: 150, lungeCd: 1.9, lungeSpeed: 8.5 },
];
function makeEnemy(kind, x, y, wave) {
    const cfg = exports.ENEMY_CFG[kind];
    const hp = Math.round(types_1.EMAXHP[kind] * cfg.hpMult * (1 + (wave - 1) * 0.12));
    return { x, y, hp, maxHp: hp, kind, cd: 0.8 + Math.random() * 1.2, hurt: 0, phase: Math.random() * 10, r: cfg.r, dead: false, lunge: 0 };
}
function pickSpawn(px, py) {
    let best = world_1.enemySpawns[0];
    let bd = -1;
    for (let i = 0; i < 5; i++) {
        const c = world_1.enemySpawns[(Math.random() * world_1.enemySpawns.length) | 0];
        const d = (c.x - px) * (c.x - px) + (c.y - py) * (c.y - py);
        if (d > bd) {
            bd = d;
            best = c;
        }
    }
    return { x: best.x, y: best.y };
}
// damage an enemy; returns true if killed
function damageEnemy(e, dmg, fromX, fromY, parts) {
    e.hp -= dmg;
    e.hurt = 0.12;
    audio_1.sfx.enemyHit();
    for (let i = 0; i < 5; i++) {
        const a = Math.atan2(e.y - fromY, e.x - fromX) + (Math.random() - 0.5) * 1.6;
        parts.push({ x: e.x, y: e.y, px: e.x, py: e.y, vx: Math.cos(a) * 14, vy: Math.sin(a) * 14, life: 0.3, max: 0.3, col: e.kind === 2 ? 3 : 4, size: 1 });
    }
    if (e.hp <= 0) {
        e.dead = true;
        audio_1.sfx.enemyDie();
        for (let i = 0; i < 12; i++) {
            const a = Math.random() * Math.PI * 2;
            parts.push({ x: e.x, y: e.y, px: e.x, py: e.y, vx: Math.cos(a) * 20, vy: Math.sin(a) * 20, life: 0.5, max: 0.5, col: e.kind === 2 ? 3 : 4, size: 1 + Math.random() });
        }
        return true;
    }
    return false;
}
// update all enemies. returns damage dealt to player this tick.
function updateEnemies(es, dt, px, py, playerAlive, parts, bullets) {
    let dmg = 0;
    if (!playerAlive)
        return 0;
    for (const e of es) {
        if (e.dead)
            continue;
        e.phase += dt * 6;
        if (e.hurt > 0)
            e.hurt -= dt;
        if (e.cd > 0)
            e.cd -= dt;
        const cfg = exports.ENEMY_CFG[e.kind];
        const dx = px - e.x, dy = py - e.y;
        const dist = Math.hypot(dx, dy);
        const ux = dist > 0.001 ? dx / dist : 0;
        const uy = dist > 0.001 ? dy / dist : 0;
        // --- movement ---
        let mx = 0, my = 0;
        let speed = cfg.speed * (1 + e.phase * 0);
        if (e.lunge > 0) {
            e.lunge -= dt;
            speed = exports.ENEMY_CFG[2].lungeSpeed;
            mx = ux;
            my = uy;
        }
        else {
            const want = dist > cfg.atkRange;
            if (want) {
                mx = ux;
                my = uy;
            }
            // separation from other enemies
            for (const o of es) {
                if (o === e || o.dead)
                    continue;
                const ox = e.x - o.x, oy = e.y - o.y;
                const od = Math.hypot(ox, oy);
                const min = (e.r + o.r) * 1.6;
                if (od > 0.001 && od < min) {
                    mx += (ox / od) * 0.8;
                    my += (oy / od) * 0.8;
                }
            }
        }
        if (mx !== 0 || my !== 0) {
            const ml = Math.hypot(mx, my);
            const res = (0, world_1.moveBody)(e.x, e.y, (mx / ml) * speed * dt, (my / ml) * speed * dt, e.r);
            e.x = res.x;
            e.y = res.y;
        }
        // --- attacks ---
        if (e.lunge <= 0) {
            if (dist < cfg.atkRange) {
                if (e.cd <= 0) {
                    e.cd = 1.0;
                    dmg += cfg.dmg;
                    audio_1.sfx.hurt();
                }
            }
            else if (cfg.shootRange > 0 && e.kind === 0 && e.cd <= 0 && dist < cfg.shootRange && (0, world_1.lineOfSight)(e.x, e.y, px, py)) {
                e.cd = cfg.shootCd;
                const a = Math.atan2(dy, dx) + (Math.random() - 0.5) * 0.09;
                bullets.push({ x: e.x + ux * 0.3, y: e.y + uy * 0.3, vx: Math.cos(a) * 11, vy: Math.sin(a) * 11, life: 1.4 });
                audio_1.sfx.enemyHit();
            }
            else if (e.kind === 2 && e.cd <= 0 && dist < 4.2 && dist > 1.2 && (0, world_1.lineOfSight)(e.x, e.y, px, py)) {
                e.cd = exports.ENEMY_CFG[2].lungeCd;
                e.lunge = 0.35;
                audio_1.sfx.lunge();
            }
        }
    }
    return dmg;
}
function updateBullets(bs, dt, es, px, py, parts, playerAlive) {
    let dmg = 0;
    for (const b of bs) {
        b.x += b.vx * dt;
        b.y += b.vy * dt;
        b.life -= dt;
        if (b.life <= 0)
            continue;
        if ((0, world_1.blockedCircle)(b.x, b.y, 0.05)) {
            b.life = 0;
            for (let i = 0; i < 4; i++) {
                const a = Math.random() * 6.28;
                parts.push({ x: b.x, y: b.y, px: b.x, py: b.y, vx: Math.cos(a) * 10, vy: Math.sin(a) * 10, life: 0.25, max: 0.25, col: 3, size: 0.7 });
            }
            continue;
        }
        for (const e of es) {
            if (e.dead)
                continue;
            const dx = e.x - b.x, dy = e.y - b.y;
            if (dx * dx + dy * dy < (e.r + 0.12) * (e.r + 0.12)) {
                b.life = 0;
                damageEnemy(e, 12, b.x - b.vx * 0.05, b.y - b.vy * 0.05, parts);
                break;
            }
        }
        if (b.life > 0 && playerAlive) {
            const dx = px - b.x, dy = py - b.y;
            if (dx * dx + dy * dy < 0.22 * 0.22) {
                b.life = 0;
                dmg += 9;
            }
        }
    }
    return dmg;
}
function updatePickups(ps, dt, px, py, onPick) {
    for (const p of ps) {
        p.t += dt;
        if (p.gone)
            continue;
        const dx = px - p.x, dy = py - p.y;
        if (dx * dx + dy * dy < 0.5 * 0.5 && onPick(p.kind)) {
            p.gone = true;
            audio_1.sfx.pickup();
        }
    }
}
function updateParticles(ps, dt) {
    for (const p of ps) {
        if (p.life <= 0)
            continue;
        p.px = p.x;
        p.py = p.y;
        p.x += p.vx * dt;
        p.y += p.vy * dt;
        p.vx *= 0.9;
        p.vy *= 0.9;
        p.life -= dt;
    }
}
};

// ── module: src/render.ts ──
__mods["src/render.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.render = render;
exports.renderMenu = renderMenu;
const types_1 = require("./types");
const world_1 = require("./world");
const FOGR = 8, FOGG = 5, FOGB = 14;
// ---------- textures (RGB triples, 64x64) ----------
function makeTex(draw) {
    const c = document.createElement('canvas');
    c.width = 64;
    c.height = 64;
    const ctx = c.getContext('2d');
    draw(ctx);
    return ctx.getImageData(0, 0, 64, 64).data;
}
function texStone() {
    return makeTex(c => {
        c.fillStyle = '#8a8472';
        c.fillRect(0, 0, 64, 64);
        c.fillStyle = '#6b6454';
        for (let y = 0; y < 64; y += 22) {
            c.fillRect(0, y, 64, 3);
            for (let x = (y / 22) % 2 === 0 ? 0 : 17; x < 64; x += 34)
                c.fillRect(x, y, 3, 22);
        }
        c.fillStyle = 'rgba(0,0,0,0.2)';
        for (let i = 0; i < 130; i++)
            c.fillRect((Math.random() * 64) | 0, (Math.random() * 64) | 0, 1, 1);
        c.fillStyle = 'rgba(255,255,240,0.1)';
        for (let i = 0; i < 60; i++)
            c.fillRect((Math.random() * 64) | 0, (Math.random() * 64) | 0, 1, 1);
    });
}
function texBrick() {
    return makeTex(c => {
        c.fillStyle = '#9a4c35';
        c.fillRect(0, 0, 64, 64);
        c.fillStyle = '#582e1f';
        for (let y = 0; y < 64; y += 16) {
            c.fillRect(0, y, 64, 3);
            for (let x = ((y / 16) % 2) * 16; x < 64; x += 32)
                c.fillRect(x, y, 3, 16);
        }
        c.fillStyle = 'rgba(255,170,130,0.14)';
        for (let y = 3; y < 64; y += 16)
            for (let x = 0; x < 64; x += 32)
                c.fillRect(x + 4, y, 24, 10);
    });
}
function texMetal() {
    return makeTex(c => {
        c.fillStyle = '#626c86';
        c.fillRect(0, 0, 64, 64);
        c.fillStyle = '#464f6a';
        for (let y = 0; y < 64; y += 32)
            c.fillRect(0, y, 64, 4);
        c.fillStyle = '#7f8aab';
        for (let y = 8; y < 64; y += 32)
            for (let x = 4; x < 64; x += 12) {
                c.beginPath();
                c.arc(x, y, 2, 0, 7);
                c.fill();
            }
        c.fillStyle = 'rgba(0,0,0,0.3)';
        c.fillRect(0, 30, 64, 2);
    });
}
function texPortal() {
    return makeTex(c => {
        const g = c.createLinearGradient(0, 0, 64, 0);
        g.addColorStop(0, '#221834');
        g.addColorStop(0.5, '#4b3468');
        g.addColorStop(1, '#221834');
        c.fillStyle = g;
        c.fillRect(0, 0, 64, 64);
        c.fillStyle = '#8a5cff';
        c.fillRect(0, 0, 5, 64);
        c.fillRect(59, 0, 5, 64);
        c.fillStyle = '#cfa6ff';
        for (let y = 5; y < 64; y += 10) {
            c.fillRect(1, y, 3, 4);
            c.fillRect(60, y, 3, 4);
        }
        c.fillStyle = 'rgba(140,90,255,0.35)';
        c.fillRect(6, 0, 8, 64);
        c.fillRect(50, 0, 8, 64);
    });
}
const TEX = [texStone(), texStone(), texBrick(), texPortal(), texMetal()];
// ---------- base layer (sky + floor) ----------
const base = document.createElement('canvas');
base.width = types_1.W;
base.height = types_1.H;
{
    const c = base.getContext('2d');
    const sky = c.createLinearGradient(0, 0, 0, types_1.GROUND);
    sky.addColorStop(0, '#191224');
    sky.addColorStop(1, '#0d0a14');
    c.fillStyle = sky;
    c.fillRect(0, 0, types_1.W, types_1.GROUND);
    const fl = c.createLinearGradient(0, types_1.GROUND, 0, types_1.H);
    fl.addColorStop(0, '#26211a');
    fl.addColorStop(0.35, '#151109');
    fl.addColorStop(1, '#080604');
    c.fillStyle = fl;
    c.fillRect(0, types_1.GROUND, types_1.W, types_1.H - types_1.GROUND);
}
// wall buffer — single putImageData per frame
const wall = new ImageData(types_1.W, types_1.H);
{
    const d = wall.data;
    for (let i = 0; i < d.length; i += 4) {
        d[i] = FOGR;
        d[i + 1] = FOGG;
        d[i + 2] = FOGB;
        d[i + 3] = 255;
    }
}
const zbuf = new Float32Array(types_1.W);
// ---------- render ----------
function render(ctx, s, t) {
    ctx.drawImage(base, 0, 0);
    const dirX = Math.cos(s.pa), dirY = Math.sin(s.pa);
    const planeX = -dirY * 0.66, planeY = dirX * 0.66;
    let camX = s.px, camY = s.py;
    if (s.shake > 0.01) {
        camX += (Math.random() - 0.5) * s.shake * 0.09;
        camY += (Math.random() - 0.5) * s.shake * 0.09;
    }
    const wd = wall.data;
    // reset wall buffer to fog
    for (let i = 3; i < wd.length; i += 4) {
        wd[i] = FOGR;
        wd[i + 1] = FOGG;
        wd[i + 2] = FOGB;
    }
    const lineH0 = types_1.H * 1.02;
    for (let x = 0; x < types_1.W; x++) {
        const cam = 2 * x / types_1.W - 1;
        const rdX = dirX + planeX * cam;
        const rdY = dirY + planeY * cam;
        let mapX = camX | 0, mapY = camY | 0;
        const dDX = rdX === 0 ? 1e30 : Math.abs(1 / rdX);
        const dDY = rdY === 0 ? 1e30 : Math.abs(1 / rdY);
        let stepX, stepY, sideX, sideY;
        if (rdX < 0) {
            stepX = -1;
            sideX = (camX - mapX) * dDX;
        }
        else {
            stepX = 1;
            sideX = (mapX + 1 - camX) * dDX;
        }
        if (rdY < 0) {
            stepY = -1;
            sideY = (camY - mapY) * dDY;
        }
        else {
            stepY = 1;
            sideY = (mapY + 1 - camY) * dDY;
        }
        let side = 0, tile = 0, guard = 0;
        while (guard++ < 24) {
            if (sideX < sideY) {
                sideX += dDX;
                mapX += stepX;
                side = 0;
            }
            else {
                sideY += dDY;
                mapY += stepY;
                side = 1;
            }
            if (mapX < 0 || mapY < 0 || mapX >= types_1.FW || mapY >= types_1.FH) {
                tile = 1;
                break;
            }
            tile = world_1.grid[mapY * types_1.FW + mapX];
            if (tile !== 0)
                break;
        }
        if (tile === 0) {
            zbuf[x] = 1e9;
            continue;
        }
        const per = side === 0 ? dDX : dDY;
        zbuf[x] = per;
        const lineH = lineH0 / Math.max(per, 0.02);
        const dStart = types_1.GROUND - lineH / 2;
        const yTop = Math.max(0, dStart | 0);
        const yBot = Math.min(types_1.H, (dStart + lineH) | 0);
        const tex = TEX[tile] || TEX[1];
        let wallX = side === 0 ? camY + per * rdY : camX + per * rdX;
        wallX -= Math.floor(wallX);
        let texX = (wallX * 64) | 0;
        if ((side === 0 && rdX > 0) || (side === 1 && rdX < 0))
            texX = 63 - texX;
        const step = 64 / lineH;
        const texPos0 = (yTop - dStart) * step;
        const sideMul = side === 0 ? 1 : 0.68;
        const fog = Math.min(1, per / types_1.FOG_MAX);
        const fogMul = 1 - fog * 0.85;
        for (let y = yTop; y < yBot; y++) {
            const texY = ((texPos0 + (y - yTop) * step) & 63) * 64 + texX;
            const i = (y * types_1.W + x) * 4;
            const m = sideMul * fogMul;
            wd[i] = tex[texY] * m;
            wd[i + 1] = tex[texY + 1] * m;
            wd[i + 2] = tex[texY + 2] * m;
        }
    }
    ctx.putImageData(wall, 0, 0);
    // --- portal glow ---
    if (s.portalOpen) {
        const pc = (0, world_1.portalCenter)();
        const rx = pc.x - camX, ry = pc.y - camY;
        const dist = Math.hypot(rx, ry);
        const a = Math.atan2(ry, rx) - s.pa;
        const rel = Math.atan2(Math.sin(a), Math.cos(a));
        const sx = types_1.W / 2 + (rel / (2 * 0.9)) * types_1.W * 0.55;
        if (sx > -60 && sx < types_1.W + 60 && zbuf[types_1.W / 2 | 0] > dist - 0.5) {
            const rad = Math.min(50, 180 / dist);
            const pulse = 0.55 + 0.45 * Math.sin(t * 5);
            const g = ctx.createRadialGradient(sx, types_1.GROUND - 10, 1, sx, types_1.GROUND - 10, rad);
            g.addColorStop(0, `rgba(190,140,255,${0.55 * pulse})`);
            g.addColorStop(1, 'rgba(120,70,220,0)');
            ctx.fillStyle = g;
            ctx.fillRect(sx - rad, types_1.GROUND - 10 - rad, rad * 2, rad * 2);
        }
    }
    const sprs = [];
    for (const e of s.enemies)
        if (!e.dead)
            sprs.push({ x: e.x, y: e.y, z: 0, kind: 'e', data: e });
    for (const p of s.pickups)
        if (!p.gone)
            sprs.push({ x: p.x, y: p.y, z: 0, kind: 'p', data: p });
    for (const b of s.bullets)
        sprs.push({ x: b.x, y: b.y, z: 0, kind: 'b', data: b });
    for (const pt of s.particles)
        if (pt.life > 0)
            sprs.push({ x: pt.x, y: pt.y, z: 0, kind: 't', data: pt });
    for (const c of world_1.cover)
        sprs.push({ x: c.x, y: c.y, z: 0, kind: 'c', data: c });
    for (const sp of sprs) {
        const dx = sp.x - camX, dy = sp.y - camY;
        sp.z = dx * dx + dy * dy;
    }
    sprs.sort((a, b) => b.z - a.z);
    for (const sp of sprs) {
        const rx = sp.x - camX, ry = sp.y - camY;
        const inv = 1 / (planeX * dirY - dirX * planeY);
        const tX = inv * (dirY * rx - dirX * ry);
        const tY = inv * (-planeY * rx + planeX * ry);
        if (tY <= 0.08)
            continue;
        const sx = (types_1.W / 2) * (1 + tX / tY);
        const dist = Math.hypot(rx, ry);
        const fog = Math.min(1, dist / types_1.FOG_MAX);
        if (sx - 40 < 0 && sx + 40 < 0)
            continue;
        if (sp.kind === 'e')
            drawEnemy(ctx, tY, sx, sp.data, fog, t);
        else if (sp.kind === 'p')
            drawPickup(ctx, tY, sx, sp.data, fog, t);
        else if (sp.kind === 'b')
            drawOrb(ctx, tY, sx, fog);
        else if (sp.kind === 't')
            drawParticle(ctx, tY, sx, sp.data, fog);
        else
            drawCover(ctx, tY, sx, sp.data, fog);
    }
    drawWeapon(ctx, s);
    if (s.pain > 0) {
        ctx.fillStyle = `rgba(200,20,10,${Math.min(0.5, s.pain * 0.8)})`;
        ctx.fillRect(0, 0, types_1.W, types_1.H);
    }
    if (s.dead > 0) {
        ctx.fillStyle = `rgba(120,0,0,${0.55 * Math.min(1, s.dead)})`;
        ctx.fillRect(0, 0, types_1.W, types_1.H);
    }
}
function fogOK(x, dist) {
    const xi = Math.max(0, Math.min(types_1.W - 1, x | 0));
    return zbuf[xi] > dist - 0.3;
}
function mix3(r, g, b, f) {
    return `rgb(${(r * (1 - f)) | 0},${(g * (1 - f)) | 0},${(b * (1 - f)) | 0})`;
}
function drawEnemy(ctx, tY, sx, e, fog, t) {
    const k = e.kind === 1 ? 0.82 : e.kind === 2 ? 0.42 : 0.55;
    const h = (types_1.H / tY) * k;
    if (h < 4)
        return;
    const w = h * 0.78;
    const bob = Math.sin(e.phase) * h * 0.05;
    const x0 = sx - w / 2;
    const yTop = types_1.GROUND - h + bob;
    if (!fogOK(sx, tY))
        return;
    ctx.save();
    ctx.beginPath();
    const cx = Math.max(0, x0 | 0), cw = Math.min(types_1.W, x0 + w) - cx;
    if (cw <= 0) {
        ctx.restore();
        return;
    }
    ctx.rect(cx, 0, cw, types_1.H);
    ctx.clip();
    const cxx = sx, base = types_1.GROUND + bob * 0.3;
    const dark = fog * 0.6;
    ctx.globalAlpha = Math.max(0.25, 1 - fog * 0.8);
    const hurt = e.hurt > 0;
    if (e.kind === 0) {
        const bodyC = hurt ? '#e8e8e8' : mix3(155, 42, 30, dark);
        const hoodC = hurt ? '#ffffff' : mix3(98, 26, 22, dark);
        ctx.fillStyle = bodyC;
        ctx.fillRect(cxx - w * 0.28, yTop + h * 0.3, w * 0.56, h * 0.6);
        ctx.fillStyle = hoodC;
        ctx.beginPath();
        ctx.moveTo(cxx - w * 0.3, yTop + h * 0.42);
        ctx.lineTo(cxx, yTop);
        ctx.lineTo(cxx + w * 0.3, yTop + h * 0.42);
        ctx.closePath();
        ctx.fill();
        const eye = 1 + Math.sin(t * 7 + e.phase) * 0.4;
        ctx.fillStyle = hurt ? '#400' : '#ffd23e';
        ctx.fillRect(cxx - w * 0.15, yTop + h * 0.2, eye * 2, eye * 2.4);
        ctx.fillRect(cxx + w * 0.05, yTop + h * 0.2, eye * 2, eye * 2.4);
        const armSw = Math.sin(e.phase * 1.4) * w * 0.08;
        ctx.fillStyle = hoodC;
        ctx.fillRect(cxx - w * 0.42, yTop + h * 0.36 + armSw, w * 0.14, h * 0.4);
        ctx.fillRect(cxx + w * 0.28, yTop + h * 0.36 - armSw, w * 0.14, h * 0.4);
        const legSw = Math.sin(e.phase * 1.4) * w * 0.1;
        ctx.fillRect(cxx - w * 0.2 + legSw, base - h * 0.32, w * 0.16, h * 0.32);
        ctx.fillRect(cxx + w * 0.04 - legSw, base - h * 0.32, w * 0.16, h * 0.32);
    }
    else if (e.kind === 1) {
        const bodyC = hurt ? '#ffffff' : mix3(122, 66, 44, dark);
        const hornC = hurt ? '#ffffff' : mix3(214, 194, 122, dark);
        ctx.fillStyle = bodyC;
        ctx.fillRect(cxx - w * 0.4, yTop + h * 0.24, w * 0.8, h * 0.55);
        ctx.fillRect(cxx - w * 0.2, yTop + h * 0.08, w * 0.4, h * 0.24);
        ctx.fillStyle = hornC;
        ctx.beginPath();
        ctx.moveTo(cxx - w * 0.2, yTop + h * 0.14);
        ctx.lineTo(cxx - w * 0.34, yTop - h * 0.02);
        ctx.lineTo(cxx - w * 0.1, yTop + h * 0.2);
        ctx.moveTo(cxx + w * 0.2, yTop + h * 0.14);
        ctx.lineTo(cxx + w * 0.34, yTop - h * 0.02);
        ctx.lineTo(cxx + w * 0.1, yTop + h * 0.2);
        ctx.fill();
        ctx.fillStyle = hurt ? '#600' : '#ff4020';
        ctx.fillRect(cxx - w * 0.13, yTop + h * 0.14, w * 0.09, h * 0.05);
        ctx.fillRect(cxx + w * 0.04, yTop + h * 0.14, w * 0.09, h * 0.05);
        ctx.fillStyle = bodyC;
        ctx.fillRect(cxx - w * 0.56, yTop + h * 0.28, w * 0.18, h * 0.5);
        ctx.fillRect(cxx + w * 0.38, yTop + h * 0.28, w * 0.18, h * 0.5);
        ctx.fillRect(cxx - w * 0.3, base - h * 0.3, w * 0.22, h * 0.3);
        ctx.fillRect(cxx + w * 0.08, base - h * 0.3, w * 0.22, h * 0.3);
        if (e.hp < e.maxHp) {
            ctx.globalAlpha = 1;
            ctx.fillStyle = '#000';
            ctx.fillRect(cxx - w * 0.35, yTop - 4, w * 0.7, 3);
            ctx.fillStyle = '#ff4020';
            ctx.fillRect(cxx - w * 0.34, yTop - 3.4, w * 0.68 * (e.hp / e.maxHp), 2);
        }
    }
    else {
        const bodyC = hurt ? '#ffffff' : mix3(202, 172, 62, dark);
        ctx.fillStyle = bodyC;
        ctx.beginPath();
        ctx.ellipse(cxx, yTop + h * 0.55, w * 0.34, h * 0.34, 0, 0, 7);
        ctx.fill();
        ctx.fillStyle = hurt ? '#600' : '#ff2020';
        ctx.fillRect(cxx - w * 0.16, yTop + h * 0.42, w * 0.1, h * 0.13);
        ctx.fillRect(cxx + w * 0.06, yTop + h * 0.42, w * 0.1, h * 0.13);
        ctx.strokeStyle = hurt ? '#fff' : mix3(122, 92, 30, dark);
        ctx.lineWidth = Math.max(1, h * 0.06);
        const flap = Math.sin(t * 14 + e.phase) * w * 0.14;
        ctx.beginPath();
        ctx.moveTo(cxx - w * 0.3, yTop + h * 0.5);
        ctx.lineTo(cxx - w * 0.52, yTop + h * 0.34 - flap);
        ctx.moveTo(cxx + w * 0.3, yTop + h * 0.5);
        ctx.lineTo(cxx + w * 0.52, yTop + h * 0.34 - flap);
        ctx.stroke();
    }
    ctx.restore();
}
function drawPickup(ctx, tY, sx, p, fog, t) {
    const h = (types_1.H / tY) * 0.3;
    if (h < 3 || !fogOK(sx, tY))
        return;
    ctx.save();
    ctx.globalAlpha = Math.max(0.3, 1 - fog * 0.8);
    const bob = Math.sin(t * 3 + p.x * 5) * h * 0.12;
    const cy = types_1.GROUND - h * 0.6 + bob;
    if (p.kind === 'h') {
        ctx.fillStyle = 'rgba(255,80,60,0.3)';
        ctx.beginPath();
        ctx.ellipse(sx, types_1.GROUND + 1, h * 0.4, h * 0.1, 0, 0, 7);
        ctx.fill();
        ctx.fillStyle = '#7a1410';
        ctx.fillRect(sx - h * 0.22, cy - h * 0.5, h * 0.44, h * 0.6);
        ctx.fillStyle = '#ff3828';
        ctx.fillRect(sx - h * 0.16, cy - h * 0.42, h * 0.32, h * 0.42);
        ctx.fillStyle = '#c8b090';
        ctx.fillRect(sx - h * 0.1, cy - h * 0.62, h * 0.2, h * 0.14);
    }
    else {
        ctx.fillStyle = 'rgba(255,200,60,0.25)';
        ctx.beginPath();
        ctx.ellipse(sx, types_1.GROUND + 1, h * 0.4, h * 0.1, 0, 0, 7);
        ctx.fill();
        ctx.fillStyle = '#5a4a20';
        ctx.fillRect(sx - h * 0.28, cy - h * 0.32, h * 0.56, h * 0.42);
        ctx.fillStyle = '#ffd23e';
        ctx.fillRect(sx - h * 0.2, cy - h * 0.24, h * 0.4, h * 0.1);
        ctx.fillStyle = '#8a7440';
        ctx.fillRect(sx - h * 0.28, cy - h * 0.32, h * 0.56, h * 0.08);
    }
    ctx.restore();
}
function drawOrb(ctx, tY, sx, fog) {
    const h = (types_1.H / tY) * 0.12;
    if (h < 2 || !fogOK(sx, tY))
        return;
    ctx.save();
    ctx.globalAlpha = Math.max(0.3, 1 - fog * 0.8);
    const y = types_1.GROUND - h * 0.8;
    ctx.fillStyle = '#fff0c0';
    ctx.fillRect(sx - h / 2, y - h / 2, h, h);
    ctx.fillStyle = 'rgba(255,120,30,0.5)';
    ctx.fillRect(sx - h, y - h, h * 2, h * 2);
    ctx.restore();
}
function drawParticle(ctx, tY, sx, p, fog) {
    const h = (types_1.H / tY) * 0.1;
    if (!fogOK(sx, tY))
        return;
    ctx.save();
    ctx.globalAlpha = (p.life / p.max) * (1 - fog * 0.7);
    const cols = ['#ff5020', '#c02010', '#ff8030', '#ffb040', '#e03828'];
    ctx.fillStyle = cols[p.col] || cols[0];
    const sz = Math.max(1, h * p.size);
    ctx.fillRect(sx - sz / 2, types_1.GROUND - h, sz, sz);
    ctx.restore();
}
function drawCover(ctx, tY, sx, c, fog) {
    const h = (types_1.H / tY) * 0.34;
    if (h < 3 || !fogOK(sx, tY))
        return;
    const w = (types_1.H / tY) * c.r * 2.4;
    const x = sx - w / 2;
    ctx.save();
    ctx.globalAlpha = Math.max(0.3, 1 - fog * 0.8);
    const top = types_1.GROUND - h;
    ctx.fillStyle = mix3(112, 98, 80, fog * 0.7);
    ctx.fillRect(x, top, w, h);
    ctx.fillStyle = mix3(76, 68, 56, fog * 0.7);
    ctx.fillRect(x, top + h * 0.35, w, h * 0.12);
    ctx.fillStyle = mix3(92, 82, 66, fog * 0.7);
    ctx.fillRect(x, top, w, h * 0.16);
    ctx.fillStyle = 'rgba(0,0,0,0.35)';
    ctx.fillRect(x + w * 0.3, top + h * 0.55, w * 0.14, h * 0.2);
    ctx.restore();
}
// ---------- weapon viewmodel ----------
function drawWeapon(ctx, s) {
    if (s.dead > 0)
        return;
    const bobX = Math.sin(s.bobT) * (s.moving ? 4 : 1.5);
    const bobY = Math.abs(Math.cos(s.bobT)) * (s.moving ? 3 : 1.5);
    const dip = s.swing * 13;
    const cx = types_1.W / 2 + bobX + 13;
    const baseY = types_1.H + bobY + dip;
    const wid = s.weaponId;
    if (wid < 0)
        return;
    ctx.save();
    if (wid === 0) {
        ctx.fillStyle = '#3c3c46';
        ctx.fillRect(cx - 5, baseY - 23, 10, 23);
        ctx.fillStyle = '#585862';
        ctx.fillRect(cx - 3, baseY - 29, 6, 7);
        ctx.fillStyle = '#28282e';
        ctx.fillRect(cx - 7, baseY - 7, 14, 7);
        ctx.fillStyle = '#b5806a';
        ctx.fillRect(cx - 6, baseY - 10, 12, 11);
    }
    else if (wid === 1) {
        ctx.fillStyle = '#4c3c2a';
        ctx.fillRect(cx - 7, baseY - 33, 14, 33);
        ctx.fillStyle = '#705c3e';
        ctx.fillRect(cx - 7, baseY - 33, 14, 5);
        ctx.fillStyle = '#35353e';
        ctx.fillRect(cx - 5, baseY - 38, 4, 8);
        ctx.fillRect(cx + 1, baseY - 38, 4, 8);
        ctx.fillStyle = '#b5806a';
        ctx.fillRect(cx - 8, baseY - 13, 16, 14);
    }
    else {
        ctx.fillStyle = '#303642';
        ctx.fillRect(cx - 10, baseY - 27, 20, 27);
        ctx.fillStyle = '#4a526c';
        ctx.fillRect(cx - 10, baseY - 31, 20, 5);
        ctx.fillStyle = '#1d2129';
        ctx.fillRect(cx - 6, baseY - 39, 5, 10);
        ctx.fillRect(cx + 1, baseY - 39, 5, 10);
        ctx.fillStyle = '#ffd23e';
        ctx.fillRect(cx - 9, baseY - 20, 3, 2);
        ctx.fillStyle = '#b5806a';
        ctx.fillRect(cx - 11, baseY - 12, 22, 13);
    }
    if (s.muzzle > 0) {
        const my = baseY - (wid === 0 ? 30 : wid === 1 ? 40 : 42);
        const mr = 7 + s.muzzle * 8;
        const g = ctx.createRadialGradient(cx, my, 0, cx, my, mr);
        g.addColorStop(0, `rgba(255,240,180,${s.muzzle})`);
        g.addColorStop(0.5, `rgba(255,150,40,${s.muzzle * 0.8})`);
        g.addColorStop(1, 'rgba(255,80,10,0)');
        ctx.fillStyle = g;
        ctx.fillRect(cx - mr, my - mr, mr * 2, mr * 2);
    }
    ctx.restore();
}
function renderMenu(ctx, t) {
    const px = 4 + Math.cos(t * 0.22) * 2.3;
    const py = 4 + Math.sin(t * 0.22) * 2.3;
    const pa = Math.atan2(4 - py, 4 - px);
    render(ctx, {
        px, py, pa, enemies: [], pickups: [], particles: [], bullets: [],
        weaponId: -1, swing: 0, bobT: t, moving: true, muzzle: 0,
        portalOpen: true, dead: 0, pain: 0, shake: 0,
    }, t);
}
};

// ── module: src/game.ts ──
__mods["src/game.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Game = void 0;
exports.attachInput = attachInput;
const types_1 = require("./types");
const world_1 = require("./world");
const types_2 = require("./types");
const enemy_1 = require("./enemy");
const render_1 = require("./render");
const audio_1 = require("./audio");
class Game {
    constructor(el) {
        this.phase = 'menu';
        this.enemies = [];
        this.pickups = [];
        this.particles = [];
        this.bullets = [];
        this.locked = false;
        this.keys = {};
        this.t = 0;
        this.bannerTimer = 0;
        this.winT = 0;
        this.loseT = 0;
        this.best = 0;
        this.hudCache = '';
        this.onPickup = (kind) => {
            const p = this.player;
            if (p.dead > 0)
                return false;
            if (kind === 'h') {
                if (p.hp >= 100)
                    return false;
                p.hp = Math.min(100, p.hp + 25);
                return true;
            }
            const def = types_1.WEAPONS[1];
            const cap = 24 + 2 * Math.max(0, p.cur); // generous
            // ammo always useful: add to both mag weapons
            const w1 = p.weapons[1], w2 = p.weapons[2];
            if (w1.ammo >= 24 * 2 && w2.ammo >= 90)
                return false;
            w1.ammo = Math.min(w1.ammo + 8, 24 * 2);
            w2.ammo = Math.min(w2.ammo + 30, 180);
            void cap;
            void def;
            return true;
        };
        this.el = el;
        this.ctx = el.ctx;
        this.player = this.freshPlayer();
        this.stats = this.freshStats();
        this.pickups = [];
        try {
            this.best = Number(localStorage.getItem('hellforge_best') || 0);
        }
        catch {
            this.best = 0;
        }
        this.scene = {
            px: 0, py: 0, pa: 0, enemies: this.enemies, pickups: this.pickups,
            particles: this.particles, bullets: this.bullets,
            weaponId: 0, swing: 0, bobT: 0, moving: false, muzzle: 0,
            portalOpen: false, dead: 0, pain: 0, shake: 0,
        };
    }
    freshPlayer() {
        return {
            x: world_1.spawn.x, y: world_1.spawn.y, a: 0,
            hp: 100,
            weapons: [
                { id: 0, ammo: Infinity, reloading: 0, cooldown: 0 },
                { id: 1, ammo: 24, reloading: 0, cooldown: 0 },
                { id: 2, ammo: 90, reloading: 0, cooldown: 0 },
            ],
            cur: 0, fireHeld: false, swing: 0, pain: 0, dead: 0, respawn: 0, bob: 0, moving: false, speed: 0, dash: 0,
        };
    }
    freshStats() {
        return {
            kills: 0, score: 0, wave: 1, timeLeft: 300, portalOpen: false, portalUsed: false,
            killsNeeded: 0, enemiesLeft: 0, spawnQueue: [], spawnTimer: 0,
        };
    }
    // ---------- flow ----------
    start() {
        (0, audio_1.unlock)();
        this.player = this.freshPlayer();
        this.stats = this.freshStats();
        this.enemies = [];
        this.bullets = [];
        this.particles = [];
        this.respawnPickups();
        this.phase = 'play';
        this.startWave(1, true);
        this.el.hint.textContent = '';
    }
    respawnPickups() {
        const defs = [
            { x: 3.5, y: 1.5, kind: 'h' }, { x: 4.5, y: 4.0, kind: 'a' },
            { x: 2.0, y: 3.5, kind: 'a' }, { x: 5.0, y: 6.0, kind: 'h' },
            { x: 3.0, y: 6.5, kind: 'a' }, { x: 1.5, y: 2.5, kind: 'h' },
        ];
        this.pickups = defs.map(d => ({ x: d.x, y: d.y, kind: d.kind, t: Math.random() * 6, gone: false }));
    }
    waveComp(wave) {
        const q = [];
        const n = 3 + wave * 2;
        for (let i = 0; i < n; i++) {
            let kind = 0;
            const roll = Math.random();
            if (wave >= 2 && roll < 0.22)
                kind = 1;
            else if (wave >= 3 && roll >= 0.22 && roll < 0.52)
                kind = 2;
            else if (roll >= 0.22 && roll < 0.5)
                kind = 2;
            q.push({ kind });
        }
        if (wave >= 3)
            q.push({ kind: 1 }, { kind: 1 });
        return q;
    }
    startWave(wave, first) {
        this.stats.wave = wave;
        this.stats.spawnQueue = this.waveComp(wave);
        this.stats.enemiesLeft = this.stats.spawnQueue.length;
        this.stats.killsNeeded = Math.ceil(this.stats.spawnQueue.length * (wave <= 2 ? 0.8 : 0.65));
        this.stats.spawnTimer = first ? 1.2 : 1.6;
        this.stats.portalOpen = false;
        (0, world_1.openPortal)(false);
        this.banner(`WAVE ${wave}`, 2.2);
        audio_1.sfx.wave();
    }
    banner(txt, dur) {
        this.el.banner.textContent = txt;
        this.el.banner.classList.add('show');
        this.bannerTimer = dur;
    }
    damagePlayer(dmg) {
        const p = this.player;
        if (p.dead > 0 || p.respawn > 0 || this.phase !== 'play')
            return;
        p.hp -= dmg;
        p.pain = 0.3;
        this.el.dmg.style.opacity = '1';
        setTimeout(() => { this.el.dmg.style.opacity = '0'; }, 140);
        if (p.hp <= 0) {
            p.hp = 0;
            p.dead = 0.001; // start death sequence
            audio_1.sfx.die();
        }
    }
    respawnPlayer() {
        const p = this.player;
        p.x = world_1.spawn.x;
        p.y = world_1.spawn.y;
        p.a = 0;
        p.hp = Math.max(50, Math.round(p.hp * 0.5) || 50);
        if (p.hp < 50)
            p.hp = 50;
        p.dead = 0;
        p.respawn = 0;
        p.pain = 0;
        // clear nearby enemies
        for (const e of this.enemies) {
            const d = Math.hypot(e.x - p.x, e.y - p.y);
            if (d < 3)
                e.dead = true;
        }
        this.stats.score = Math.max(0, this.stats.score - 150);
        this.banner('RE-DEPLOYED  (-150)', 1.8);
        audio_1.sfx.respawn();
    }
    killEnemy(e) {
        const idx = this.enemies.indexOf(e);
        if (idx >= 0)
            this.enemies.splice(idx, 1);
        this.stats.kills++;
        this.stats.score += enemy_1.ENEMY_CFG[e.kind].score;
        this.stats.enemiesLeft--;
        // chance to drop pickup
        const roll = Math.random();
        if (roll < 0.34) {
            this.pickups.push({ x: e.x, y: e.y, kind: roll < 0.17 ? 'a' : 'h', t: 0, gone: false });
        }
    }
    checkWave() {
        const s = this.stats;
        const alive = this.enemies.length;
        if (!s.portalOpen && alive === 0 && s.spawnQueue.length === 0) {
            // wave cleared → open portal
            s.portalOpen = true;
            (0, world_1.openPortal)(true);
            this.banner('PORTAL OPEN — [E] TO ESCAPE', 3.2);
            audio_1.sfx.portalOpen();
        }
    }
    fireWeapon(dt) {
        const p = this.player;
        const w = p.weapons[p.cur];
        const def = types_1.WEAPONS[p.cur];
        if (w.reloading > 0) {
            w.reloading -= dt;
            if (w.reloading <= 0) {
                w.reloading = 0;
                audio_1.sfx.reload();
            }
            return;
        }
        if (w.cooldown > 0) {
            w.cooldown -= dt;
            return;
        }
        if (!p.fireHeld && !def.auto)
            return;
        if (w.ammo <= 0) {
            if (p.fireHeld) {
                audio_1.sfx.empty();
                w.cooldown = 0.3;
            }
            return;
        }
        // fire!
        w.cooldown = def.rate;
        if (def.mag !== Infinity)
            w.ammo--;
        p.swing = 1;
        this.scene.muzzle = 1;
        const pellets = def.pellets || 1;
        audio_1.sfx[def.sound]();
        this.scene.shake = Math.min(1, this.scene.shake + (def.name === 'SCATTER' ? 0.9 : def.name === 'HEAVY' ? 0.35 : 0.3));
        for (let i = 0; i < pellets; i++) {
            const a = p.a + (Math.random() - 0.5) * 2 * def.spread;
            const dmg = def.dmg[0] + Math.random() * (def.dmg[1] - def.dmg[0]);
            // hitscan
            const hx = p.x + Math.cos(a) * 0.2, hy = p.y + Math.sin(a) * 0.2;
            let hit = null;
            let hitDist = 99;
            for (const e of this.enemies) {
                if (e.dead)
                    continue;
                const rx = e.x - hx, ry = e.y - hy;
                const proj = rx * Math.cos(a) + ry * Math.sin(a);
                if (proj < 0.3)
                    continue;
                const perp = Math.abs(rx * Math.sin(a) - ry * Math.cos(a));
                if (perp < e.r + 0.1 && proj < hitDist && (0, world_1.lineOfSight)(hx, hy, e.x, e.y)) {
                    hit = e;
                    hitDist = proj;
                }
            }
            if (hit) {
                (0, enemy_1.damageEnemy)(hit, dmg, hx, hy, this.particles);
            }
            else {
                // spawn impact spark at wall
                const res = this.raycastWall(hx, hy, a, 9);
                this.particles.push({ x: res.x, y: res.y, px: res.x, py: res.y, vx: -Math.cos(a) * 8, vy: -Math.sin(a) * 8, life: 0.2, max: 0.2, col: 1, size: 0.7 });
            }
        }
        if (def.mag !== Infinity && w.ammo <= 0) {
            w.reloading = def.reload;
            audio_1.sfx.reload();
        }
    }
    raycastWall(x, y, a, maxDist) {
        let ddx = Math.abs(1 / (Math.cos(a) || 1e-9));
        let ddy = Math.abs(1 / (Math.sin(a) || 1e-9));
        let mx = x | 0, my = y | 0;
        const stx = Math.cos(a) < 0 ? -1 : 1;
        const sty = Math.sin(a) < 0 ? -1 : 1;
        let sdx = (stx < 0 ? x - mx : mx + 1 - x) * ddx;
        let sdy = (sty < 0 ? y - my : my + 1 - y) * ddy;
        let side = 0;
        for (let i = 0; i < 40; i++) {
            if (sdx < sdy) {
                sdx += ddx;
                mx += stx;
                side = 0;
            }
            else {
                sdy += ddy;
                my += sty;
                side = 1;
            }
            const d = side === 0 ? (mx - x + (1 - stx) / 2) * ddx * stx : (my - y + (1 - sty) / 2) * ddy * sty;
            const dist = side === 0 ? sdx - ddx : sdy - ddy;
            if ((0, world_1.cellAt)(mx, my) !== 0 || dist > maxDist) {
                return side === 0
                    ? { x: x + Math.cos(a) * dist, y: y + Math.sin(a) * dist }
                    : { x: x + Math.cos(a) * dist, y: y + Math.sin(a) * dist };
            }
        }
        return { x: x + Math.cos(a) * maxDist, y: y + Math.sin(a) * maxDist };
    }
    update(dt) {
        this.t += dt;
        if (this.bannerTimer > 0) {
            this.bannerTimer -= dt;
            if (this.bannerTimer <= 0)
                this.el.banner.classList.remove('show');
        }
        if (this.phase === 'menu' || this.phase === 'over') {
            (0, render_1.renderMenu)(this.ctx, this.t);
            const c = this.ctx;
            c.textAlign = 'center';
            c.fillStyle = '#ff5b2e';
            c.font = 'bold 30px "Courier New", monospace';
            c.fillText('H E L L F O R G E', 160, 62);
            c.fillStyle = '#cbb98a';
            c.font = '9px "Courier New", monospace';
            c.fillText('SURVIVE THE WAVES — REACH THE PORTAL', 160, 80);
            c.fillStyle = '#7fd4c8';
            c.fillText('WASD MOVE · MOUSE AIM · CLICK FIRE · 1-3 WEAPONS · E ESCAPE', 160, 176);
            c.fillStyle = this.best > 0 ? '#ffd23e' : '#6f6248';
            c.fillText(this.best > 0 ? `BEST SCORE ${this.best}` : 'CLICK TO DEPLOY', 160, 190);
            c.textAlign = 'left';
            this.drawHud();
            return;
        }
        const p = this.player;
        const s = this.stats;
        // ----- death / respawn -----
        if (p.dead > 0) {
            p.dead += dt;
            if (p.dead > 1.4) {
                p.respawn = 1.2;
                p.dead = 0;
                this.respawnPlayer();
            }
        }
        this.scene.dead = p.dead > 0 ? Math.min(1, p.dead / 0.8) : 0;
        if (p.respawn > 0)
            p.respawn -= dt;
        const alive = p.dead === 0 && p.respawn === 0;
        // ----- player movement -----
        p.moving = false;
        if (alive) {
            const k = this.keys;
            if (k['ArrowLeft'])
                p.a -= 2.8 * dt;
            if (k['ArrowRight'])
                p.a += 2.8 * dt;
            let mx = 0, my = 0;
            if (k['KeyW'] || k['ArrowUp']) {
                mx += Math.cos(p.a);
                my += Math.sin(p.a);
            }
            if (k['KeyS'] || k['ArrowDown']) {
                mx -= Math.cos(p.a);
                my -= Math.sin(p.a);
            }
            if (k['KeyA']) {
                mx += Math.sin(p.a);
                my -= Math.cos(p.a);
            }
            if (k['KeyD']) {
                mx -= Math.sin(p.a);
                my += Math.cos(p.a);
            }
            const l = Math.hypot(mx, my);
            if (l > 0) {
                const spd = 3.4 * (k['ShiftLeft'] || k['ShiftRight'] ? 1.55 : 1);
                mx /= l;
                my /= l;
                const res = this.slide(p.x, p.y, mx * spd * dt, my * spd * dt, 0.28);
                p.x = res.x;
                p.y = res.y;
                p.moving = true;
                p.bob += dt * (k['ShiftLeft'] || k['ShiftRight'] ? 13 : 9);
            }
            // firing
            this.fireWeapon(dt);
            // portal use
            if (k['KeyE'] && s.portalOpen) {
                const pc = (0, world_1.portalCenter)();
                const d = Math.hypot(pc.x - p.x, pc.y - p.y);
                if (d < 1.1)
                    this.win();
            }
        }
        p.swing = Math.max(0, p.swing - dt * 5);
        if (p.pain > 0)
            p.pain -= dt;
        // ----- enemies -----
        const dmg = (0, enemy_1.updateEnemies)(this.enemies, dt, p.x, p.y, alive, this.particles, this.bullets);
        if (dmg > 0) {
            this.damagePlayer(dmg);
            this.scene.shake = Math.min(1.2, this.scene.shake + 0.7);
        }
        const bdmg = (0, enemy_1.updateBullets)(this.bullets, dt, this.enemies, p.x, p.y, this.particles, alive);
        if (bdmg > 0)
            this.damagePlayer(bdmg);
        // dead enemy cleanup (non-killed path: e.dead set elsewhere too)
        for (let i = this.enemies.length - 1; i >= 0; i--) {
            if (this.enemies[i].dead) {
                const e = this.enemies[i];
                this.enemies.splice(i, 1);
                this.stats.kills++;
                this.stats.score += enemy_1.ENEMY_CFG[e.kind].score;
                this.stats.enemiesLeft--;
                const roll = Math.random();
                if (roll < 0.34)
                    this.pickups.push({ x: e.x, y: e.y, kind: roll < 0.17 ? 'a' : 'h', t: 0, gone: false });
            }
        }
        // ----- spawn queue -----
        if (s.spawnQueue.length > 0) {
            s.spawnTimer -= dt;
            if (s.spawnTimer <= 0) {
                const c = s.spawnQueue.pop();
                const pos = (0, enemy_1.pickSpawn)(p.x, p.y);
                if (!(0, world_1.blockedCircle)(pos.x, pos.y, 0.3))
                    this.enemies.push((0, enemy_1.makeEnemy)(c.kind, pos.x, pos.y, s.wave));
                s.spawnTimer = 1.1 + Math.random() * 0.9;
            }
        }
        // ----- pickups / particles / timer -----
        (0, enemy_1.updatePickups)(this.pickups, dt, p.x, p.y, this.onPickup);
        (0, enemy_1.updateParticles)(this.particles, dt);
        this.bullets = this.bullets.filter(b => b.life > 0);
        this.particles = this.particles.filter(pp => pp.life > 0);
        if (alive)
            s.timeLeft -= dt;
        // ----- win/lose -----
        if (alive && !s.portalUsed && s.timeLeft <= 0) {
            this.lose();
        }
        if (this.phase === 'play' && !s.portalUsed)
            this.checkWave();
        // win/lose overlays advance
        if (this.winT > 0)
            this.winT += dt;
        if (this.loseT > 0)
            this.loseT += dt;
        // ----- render -----
        this.scene.px = p.x;
        this.scene.py = p.y;
        this.scene.pa = p.a;
        this.scene.weaponId = alive ? p.cur : -1;
        this.scene.swing = p.swing;
        this.scene.bobT = p.bob;
        this.scene.moving = p.moving;
        this.scene.muzzle = Math.max(0, this.scene.muzzle - dt * 9);
        this.scene.portalOpen = s.portalOpen;
        this.scene.pain = p.pain;
        this.scene.shake = Math.max(0, this.scene.shake - dt * 3);
        (0, render_1.render)(this.ctx, this.scene, this.t);
        if (p.dead > 0) {
            const c = this.ctx;
            c.fillStyle = '#fff';
            c.font = 'bold 17px "Courier New", monospace';
            c.textAlign = 'center';
            c.fillText('TAKEN DOWN', W2 / 2, 95);
            c.font = '9px "Courier New", monospace';
            c.fillStyle = '#ff8a6a';
            c.fillText('re-deploying…', W2 / 2, 108);
            c.textAlign = 'left';
        }
        if (this.winT > 0 || this.loseT > 0)
            this.drawEndOverlay();
        this.drawHud();
    }
    slide(x, y, dx, dy, r) {
        let blocked = false;
        if (dx !== 0) {
            const s = Math.sign(dx);
            if (!this.circSolid(x + dx - s * r, y - r) && !this.circSolid(x + dx - s * r, y + r))
                x += dx;
            else
                blocked = true;
        }
        if (dy !== 0 && !blocked) {
            const s = Math.sign(dy);
            if (!this.circSolid(x - r, y + dy - s * r) && !this.circSolid(x + r, y + dy - s * r))
                y += dy;
        }
        else if (dy !== 0) {
            const s = Math.sign(dy);
            if (!this.circSolid(x - r, y + dy - s * r) && !this.circSolid(x + r, y + dy - s * r))
                y += dy;
        }
        return { x, y };
    }
    circSolid(x, y) {
        if (x < 0 || y < 0 || x >= types_2.FW || y >= types_2.FH)
            return true;
        if ((0, world_1.cellAt)(x, y) !== 0)
            return true;
        return false;
    }
    win() {
        if (this.stats.portalUsed)
            return;
        this.stats.portalUsed = true;
        this.stats.score += Math.round(this.stats.timeLeft) * 5 + this.player.hp * 2;
        (0, world_1.openPortal)(false);
        audio_1.sfx.portalGo();
        this.el.flash.classList.add('show');
        setTimeout(() => this.el.flash.classList.remove('show'), 500);
        if (this.stats.score > this.best) {
            this.best = this.stats.score;
            try {
                localStorage.setItem('hellforge_best', String(this.best));
            }
            catch { /* noop */ }
        }
        this.winT = 0.001;
    }
    lose() {
        this.loseT = 0.001;
        if (this.stats.score > this.best) {
            this.best = this.stats.score;
            try {
                localStorage.setItem('hellforge_best', String(this.best));
            }
            catch { /* noop */ }
        }
    }
    drawEndOverlay() {
        const c = this.ctx;
        c.fillStyle = 'rgba(5,3,8,0.78)';
        c.fillRect(0, 0, W2, H2);
        c.textAlign = 'center';
        if (this.winT > 0) {
            c.fillStyle = '#ffd23e';
            c.font = 'bold 20px "Courier New", monospace';
            c.fillText('ESCAPED THE FORGE', W2 / 2, 80);
            c.fillStyle = '#e8e0c8';
            c.font = '11px "Courier New", monospace';
            c.fillText(`SCORE ${this.stats.score}   KILLS ${this.stats.kills}`, W2 / 2, 100);
            c.fillText(`WAVES ${this.stats.wave}   BEST ${this.best}`, W2 / 2, 114);
        }
        else {
            c.fillStyle = '#ff4020';
            c.font = 'bold 20px "Courier New", monospace';
            c.fillText('THE FORGE CONSUMES YOU', W2 / 2, 80);
            c.fillStyle = '#e8e0c8';
            c.font = '11px "Courier New", monospace';
            c.fillText(`SCORE ${this.stats.score}   KILLS ${this.stats.kills}   WAVE ${this.stats.wave}`, W2 / 2, 100);
            c.fillText(`BEST ${this.best}`, W2 / 2, 114);
        }
        if (this.winT > 1.1 || this.loseT > 1.1) {
            c.fillStyle = '#9b8a62';
            c.font = '9px "Courier New", monospace';
            c.fillText('[ENTER / CLICK] RE-ENTER THE FORGE', W2 / 2, 140);
        }
        c.textAlign = 'left';
    }
    drawHud() {
        const el = this.el;
        const p = this.player;
        const s = this.stats;
        const w = p.weapons[p.cur];
        const ammoTxt = p.cur === 0 ? '∞' : String(w.ammo);
        const mm = Math.max(0, s.timeLeft);
        const tTxt = `${(mm / 60) | 0}:${String((mm % 60) | 0).padStart(2, '0')}`;
        const obj = this.phase === 'menu' ? 'STANDBY' : s.portalOpen ? 'OPEN PORTAL [E]' : `WAVE ${s.wave}  •  ${s.kills}/${s.killsNeeded} KILLS`;
        const hpLow = p.hp <= 30;
        const ammoEmpty = p.cur !== 0 && w.ammo <= 0;
        const key = [p.hp, ammoTxt, obj, s.score, tTxt, s.kills, this.best, this.phase, p.cur, w.reloading > 0, hpLow, ammoEmpty, this.winT > 0, this.loseT > 0].join('|');
        if (key === this.hudCache)
            return;
        this.hudCache = key;
        el.hp.textContent = String(Math.max(0, p.hp));
        el.hp.classList.toggle('hp-low', hpLow);
        el.ammo.textContent = ammoTxt;
        el.ammo.classList.toggle('ammo-empty', ammoEmpty);
        el.kills.textContent = String(s.kills);
        el.best.textContent = String(this.best);
        el.obj.textContent = obj;
        el.score.textContent = String(s.score);
        el.timer.textContent = tTxt;
        const btns = el.weapons.children;
        for (let i = 0; i < btns.length; i++) {
            btns[i].classList.toggle('on', i === p.cur);
        }
    }
}
exports.Game = Game;
const W2 = 320;
const H2 = 200;
function attachInput(g) {
    const cv = g.el.canvas;
    const screen = g.el.screen;
    const onMouseDown = (e) => {
        (0, audio_1.unlock)();
        if (g.phase === 'menu') {
            g.start();
            return;
        }
        if (g.phase === 'over') {
            if (g.winT > 1.1 || g.loseT > 1.1)
                g.start();
            return;
        }
        if (!g.locked) {
            if (screen.requestPointerLock)
                screen.requestPointerLock();
            return;
        }
        g.player.fireHeld = true;
        if (e.button === 0) {
            const def = types_1.WEAPONS[g.player.cur];
            if (!def.auto)
                g.player.fireHeld = true;
        }
    };
    const onMouseUp = () => { g.player.fireHeld = false; };
    const onMove = (e) => {
        if (!g.locked)
            return;
        g.player.a += e.movementX * 0.0024;
    };
    const onKeyDown = (e) => {
        (0, audio_1.unlock)();
        g.keys[e.code] = true;
        if (e.code === 'Digit1')
            g.player.cur = 0;
        if (e.code === 'Digit2')
            g.player.cur = 1;
        if (e.code === 'Digit3')
            g.player.cur = 2;
        if (e.code === 'KeyM') {
            const m = (0, audio_1.toggleMute)();
            g.banner(m ? 'MUTED' : 'SOUND ON', 1);
        }
        if (e.code === 'Enter' || e.code === 'Space') {
            if (g.phase === 'menu')
                g.start();
            else if (g.phase === 'over' && (g.winT > 1.1 || g.loseT > 1.1))
                g.start();
        }
        if (['Space', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.code))
            e.preventDefault();
    };
    const onKeyUp = (e) => { g.keys[e.code] = false; };
    const onLockChange = () => {
        g.locked = document.pointerLockElement === screen;
        screen.classList.toggle('locking', g.locked);
        g.el.hint.textContent = g.locked ? '' : (g.phase === 'play' ? 'CLICK TO CAPTURE AIM' : '');
        if (!g.locked)
            g.player.fireHeld = false;
    };
    screen.addEventListener('mousedown', onMouseDown);
    window.addEventListener('mouseup', onMouseUp);
    window.addEventListener('mousemove', onMove);
    window.addEventListener('keydown', onKeyDown);
    window.addEventListener('keyup', onKeyUp);
    document.addEventListener('pointerlockchange', onLockChange);
    void cv;
}
};

// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"Qwen3.8","provider":"brigid","persona":"typescript","sessionId":"7ac5c576-e839-438c-b7a2-26011c9b555f","tokensIn":4189312,"tokensOut":91424,"tokensTotal":4280736,"cost":0,"turns":75,"toolCalls":87,"failedToolCalls":0,"timestamp":"2026-09-09T02:04:47.244Z"} -->