← DOOM results

DOOM

Qwen 3.8 27B GGUF · 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 67,980 bytes · SHA-256 74aef9ca47a8
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>HELLHOLE — A Raycast Dungeon Crawl</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Press+Start+2P&amp;display=swap">
<style>
/* ── css: base ── */
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { height: 100%; }
body {
  background: #040506;
  overflow: hidden;
  font-family: 'Press Start 2P', 'Courier New', monospace;
  color: #cfe8cf;
}
#app { position: fixed; inset: 0; display: flex; align-items: center; justify-content: center; }
#stage {
  position: relative;
  width: min(100vw, 160vh);
  aspect-ratio: 480 / 300;
  background: #000;
  box-shadow: 0 0 80px rgba(0, 40, 10, 0.8);
}
#game {
  position: absolute; inset: 0; width: 100%; height: 100%;
  image-rendering: pixelated; image-rendering: crisp-edges;
  display: block;
}
#scanlines {
  position: absolute; inset: 0; pointer-events: none; z-index: 2;
  background: repeating-linear-gradient(to bottom, rgba(0,0,0,0.18) 0 1px, transparent 1px 3px);
  opacity: 0.55;
}
#crt {
  position: absolute; inset: 0; pointer-events: none; z-index: 2;
  background: radial-gradient(ellipse at center, transparent 52%, rgba(0,0,0,0.55) 100%);
}
/* ── css: hud ── */
#hud { position: absolute; inset: 0; pointer-events: none; z-index: 3; }
#msg {
  position: absolute; top: 1.4%; left: 50%; transform: translateX(-50%);
  font-size: clamp(8px, 1.15vmin, 13px); color: #9fe89f;
  text-shadow: 0 0 8px rgba(80,255,120,0.5);
  text-align: center; white-space: nowrap; letter-spacing: 1px;
}
#radarwrap { position: absolute; top: 1.4%; right: 1.2%; text-align: center; }
#radar {
  width: clamp(58px, 9.5vmin, 108px); height: auto;
  image-rendering: pixelated;
  border: 2px solid rgba(120,200,120,0.35);
  background: rgba(0,10,4,0.55);
}
#kills { margin-top: 6px; font-size: clamp(7px, 1vmin, 11px); color: #e89f9f; text-shadow: 0 0 6px rgba(255,80,80,0.4); }
#bottom {
  position: absolute; left: 0; right: 0; bottom: 0;
  display: flex; align-items: flex-end; justify-content: space-between;
  padding: 10px 12px; gap: 12px;
  background: linear-gradient(to top, rgba(0,0,0,0.55), transparent);
}
#hpbox { display: flex; align-items: center; gap: 8px; font-size: clamp(7px, 1vmin, 11px); }
.lbl { color: #8fbf8f; }
#hpbar { width: clamp(70px, 12vmin, 150px); height: 12px; border: 2px solid #4a6a4a; background: rgba(0,20,0,0.6); }
#hpfill { height: 100%; width: 100%; background: linear-gradient(to right, #39d353, #8aff9a); transition: width 0.15s; }
#hpfill.low { background: linear-gradient(to right, #d33939, #ff8a8a); }
#hptxt { color: #d8f8d8; min-width: 3.5em; }
#wlist { display: flex; gap: 8px; }
.wslot {
  border: 2px solid #3a4a3a; background: rgba(0,10,0,0.55);
  padding: 6px 8px; text-align: center; min-width: clamp(70px, 10vmin, 110px);
}
.wslot .wn { display: block; font-size: clamp(6px, 0.9vmin, 10px); color: #7fa07f; margin-bottom: 5px; }
.wslot .wa { font-size: clamp(10px, 1.6vmin, 18px); color: #c8e8c8; }
.wslot.active { border-color: #8aff9a; box-shadow: 0 0 10px rgba(80,255,120,0.25); }
.wslot.active .wa { color: #eaffea; }
/* ── css: overlay ── */
#overlay {
  position: absolute; inset: 0; z-index: 5;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: clamp(10px, 2.4vmin, 26px);
  background: rgba(2,4,3,0.78);
  pointer-events: auto; cursor: pointer; text-align: center;
}
#overlay.hidden { display: none; }
#ovTitle {
  font-size: clamp(20px, 5vmin, 54px); color: #e8f8e8; letter-spacing: 4px;
  text-shadow: 0 0 18px rgba(120,255,140,0.45), 3px 3px 0 #1a3a1a;
}
#ovTitle.red { color: #ff6a5a; text-shadow: 0 0 18px rgba(255,80,60,0.5), 3px 3px 0 #3a1210; }
#ovSub { font-size: clamp(9px, 1.6vmin, 16px); color: #9fe89f; letter-spacing: 2px; }
#ovText { font-size: clamp(8px, 1.25vmin, 13px); color: #b8d8b8; line-height: 2.2; }
.blink { animation: blink 1.1s steps(2) infinite; color: #eaffea; }
@keyframes blink { 50% { opacity: 0.25; } }
</style>
</head>
<body>
<div id="stage">
    <canvas id="game" width="480" height="300"></canvas>
    <div id="scanlines"></div>
    <div id="crt"></div>
    <div id="hud">
      <div id="msg"></div>
      <div id="radarwrap">
        <canvas id="radar" width="120" height="120"></canvas>
        <div id="kills">KILLS 0/8</div>
      </div>
      <div id="bottom">
        <div id="hpbox">
          <span class="lbl">HEALTH</span>
          <div id="hpbar"><div id="hpfill"></div></div>
          <span id="hptxt">100%</span>
        </div>
        <div id="wlist">
          <div class="wslot" id="w0"><span class="wn">1·P-9 SIDEARM</span><span class="wa" id="w0a">∞</span></div>
          <div class="wslot" id="w1"><span class="wn">2·SPES-12</span><span class="wa" id="w1a">8</span></div>
          <div class="wslot" id="w2"><span class="wn">3·RG-100</span><span class="wa" id="w2a">60</span></div>
        </div>
      </div>
    </div>
    <div id="overlay">
      <div id="ovTitle">HELLHOLE</div>
      <div id="ovSub">A RAYCAST DUNGEON CRAWL</div>
      <div id="ovText"></div>
    </div>
  </div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./game":"src/game.ts","./renderer":"src/renderer.ts","./input":"src/input.ts","./audio":"src/audio.ts"},"src/enemies.ts":{"./types":"src/types.ts","./map":"src/map.ts","./audio":"src/audio.ts"},"src/game.ts":{"./types":"src/types.ts","./map":"src/map.ts","./enemies":"src/enemies.ts","./weapons":"src/weapons.ts","./audio":"src/audio.ts","./input":"src/input.ts"},"src/renderer.ts":{"./game":"src/game.ts","./map":"src/map.ts","./types":"src/types.ts"}};
function __require(id) {
  if (__cache[id]) return __cache[id].exports;
  var module = __cache[id] = { exports: {} };
  var factory = __mods[id];
  if (!factory) throw new Error("Module not found: " + id);
  factory(module.exports, function (spec) {
    var target = (__map[id] && __map[id][spec]) || spec;
    return __require(target);
  }, module);
  return module.exports;
}

// ── module: src/main.ts ──
__mods["src/main.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const game_1 = require("./game");
const renderer_1 = require("./renderer");
const input_1 = require("./input");
const audio_1 = require("./audio");
const canvas = document.getElementById('game');
const radar = document.getElementById('radar');
const overlay = document.getElementById('overlay');
const ovTitle = document.getElementById('ovTitle');
const ovSub = document.getElementById('ovSub');
const ovText = document.getElementById('ovText');
const msgEl = document.getElementById('msg');
const hpFill = document.getElementById('hpfill');
const hpTxt = document.getElementById('hptxt');
const killsEl = document.getElementById('kills');
const wSlots = [
    document.getElementById('w0'),
    document.getElementById('w1'),
    document.getElementById('w2'),
];
const wAmmo = [
    document.getElementById('w0a'),
    document.getElementById('w1a'),
    document.getElementById('w2a'),
];
const input = new input_1.Input();
input.attach();
const game = new game_1.Game(input);
const renderer = new renderer_1.Renderer(canvas, radar);
window.__game = game;
window.__input = input;
window.__renderer = renderer;
let overlayVisible = true;
let lastOverlayMode = '';
function fmt(n) {
    return Math.max(0, Math.round(n)).toString();
}
function showOverlay(mode, kills, total) {
    overlay.classList.remove('hidden');
    overlayVisible = true;
    if (mode === 'title') {
        ovTitle.textContent = 'HELLHOLE';
        ovTitle.classList.remove('red');
        ovSub.textContent = 'A RAYCAST DUNGEON CRAWL';
        ovText.innerHTML =
            'THE PIT HAS OPENED. 8 DEMONS HOLD THE HALLS.<br>' +
                'SLAY THEM ALL, THEN ESCAPE THROUGH THE GATE.<br><br>' +
                'WASD MOVE &nbsp;·&nbsp; ARROWS / MOUSE TURN &nbsp;·&nbsp; CLICK / SPACE FIRE<br>' +
                '1 2 3 WEAPONS &nbsp;·&nbsp; SHIFT SPRINT<br><br>' +
                '<span class="blink">CLICK TO DESCEND</span>';
    }
    else if (mode === 'dead') {
        ovTitle.textContent = 'YOU DIED';
        ovTitle.classList.add('red');
        ovSub.textContent = 'THE PIT CLAIMS ANOTHER SOUL';
        ovText.innerHTML =
            `DEMONS SLAIN: ${kills} / ${total}<br><br>` +
                '<span class="blink">CLICK TO RESPAWN</span>';
    }
    else {
        ovTitle.textContent = 'ESCAPED';
        ovTitle.classList.remove('red');
        ovSub.textContent = 'THE GATE SHUTS BEHIND YOU... FOR NOW';
        ovText.innerHTML =
            `ALL ${total} DEMONS SLAIN. HELLHOLE IS CLEAR.<br><br>` +
                '<span class="blink">CLICK TO PLAY AGAIN</span>';
    }
}
function hideOverlay() {
    overlay.classList.add('hidden');
    overlayVisible = false;
}
function updateHUD() {
    const p = game.player;
    const frac = Math.max(0, p.hp / p.maxHp);
    hpFill.style.width = `${Math.round(frac * 100)}%`;
    hpFill.classList.toggle('low', p.hp <= 30);
    hpTxt.textContent = `${Math.round(p.hp)}%`;
    killsEl.textContent = `KILLS ${game.state.kills}/${game.state.totalKills}`;
    for (let i = 0; i < 3; i++) {
        wSlots[i].classList.toggle('active', i === game.weaponIdx);
        const w = game.weapons[i];
        wAmmo[i].textContent = w.infinite ? '∞' : fmt(w.ammo);
    }
    msgEl.textContent = game.msgTimer > 0 ? game.msgText : game.state.gateOpen && game.state.mode === 'playing' ? 'THE GATE IS OPEN — ESCAPE!' : '';
}
overlay.addEventListener('pointerdown', (e) => {
    e.stopPropagation();
    (0, audio_1.initAudio)();
    const mode = game.state.mode;
    if (mode === 'title' || mode === 'win') {
        game.startRun();
        hideOverlay();
    }
    else if (mode === 'dead') {
        game.respawn();
        game.state.mode = 'playing';
        hideOverlay();
    }
});
let last = performance.now();
function frame(now) {
    const dt = Math.min(0.05, (now - last) / 1000);
    last = now;
    game.update(dt);
    renderer.render(game, dt);
    // overlay state sync
    const m = game.state.mode;
    if (m === 'title' && !overlayVisible)
        showOverlay('title', game.state.kills, game.state.totalKills);
    if (m === 'dead' && game.state.deadAt > 1.0 && !overlayVisible)
        showOverlay('dead', game.state.kills, game.state.totalKills);
    if (m === 'win' && game.state.wonAt > 1.5 && !overlayVisible)
        showOverlay('win', game.state.kills, game.state.totalKills);
    if (m === 'playing' && overlayVisible)
        hideOverlay();
    updateHUD();
    requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
};

// ── module: src/types.ts ──
__mods["src/types.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
};

// ── module: src/audio.ts ──
__mods["src/audio.ts"] = function (exports, require, module) {
"use strict";
// Tiny WebAudio synth: all sounds generated, no assets.
Object.defineProperty(exports, "__esModule", { value: true });
exports.sfx = void 0;
exports.initAudio = initAudio;
let ac = null;
let master = null;
function initAudio() {
    if (!ac) {
        const AC = window.AudioContext || window.webkitAudioContext;
        ac = new AC();
        master = ac.createGain();
        master.gain.value = 0.5;
        master.connect(ac.destination);
    }
    if (ac.state === 'suspended')
        void ac.resume();
}
function env(g, t0, a, d, peak, sustain = 0) {
    g.gain.setValueAtTime(0.0001, t0);
    g.gain.linearRampToValueAtTime(peak, t0 + a);
    g.gain.setTargetAtTime(sustain, t0 + a, d);
}
function osc(type, f0, f1, dur, peak, slideDelay = 0) {
    if (!ac || !master)
        return;
    const t = ac.currentTime;
    const o = ac.createOscillator();
    const g = ac.createGain();
    o.type = type;
    o.frequency.setValueAtTime(Math.max(1, f0), t);
    if (f1 !== f0)
        o.frequency.exponentialRampToValueAtTime(Math.max(1, f1), t + slideDelay + dur);
    env(g, t, 0.005, dur * 0.6, peak);
    o.connect(g).connect(master);
    o.start(t);
    o.stop(t + slideDelay + dur + 0.25);
}
function noise(dur, peak, filterFreq, q = 0.8) {
    if (!ac || !master)
        return;
    const t = ac.currentTime;
    const len = Math.max(1, Math.floor(ac.sampleRate * dur));
    const buf = ac.createBuffer(1, len, ac.sampleRate);
    const data = buf.getChannelData(0);
    for (let i = 0; i < len; i++)
        data[i] = (Math.random() * 2 - 1) * (1 - i / len);
    const src = ac.createBufferSource();
    src.buffer = buf;
    const f = ac.createBiquadFilter();
    f.type = 'lowpass';
    f.frequency.value = filterFreq;
    f.Q.value = q;
    const g = ac.createGain();
    env(g, t, 0.003, dur * 0.5, peak);
    src.connect(f).connect(g).connect(master);
    src.start(t);
}
exports.sfx = {
    fire0() {
        osc('square', 620, 240, 0.07, 0.2);
        noise(0.05, 0.15, 4000);
    },
    fire1() {
        osc('sawtooth', 180, 60, 0.14, 0.4);
        noise(0.18, 0.5, 900);
    },
    fire2() {
        osc('sawtooth', 140, 50, 0.25, 0.35);
        noise(0.3, 0.25, 600, 0.4);
    },
    dry() {
        osc('square', 1400, 900, 0.04, 0.06);
    },
    hurt() {
        osc('sawtooth', 200, 70, 0.25, 0.35);
    },
    die() {
        osc('square', 300, 50, 0.7, 0.3);
        osc('sawtooth', 150, 30, 0.9, 0.25);
    },
    kill() {
        osc('square', 240, 900, 0.1, 0.15);
        noise(0.08, 0.2, 3000);
    },
    pickup() {
        osc('square', 880, 1320, 0.09, 0.16);
    },
    gate() {
        osc('sawtooth', 90, 90, 0.6, 0.3, 0);
        osc('square', 440, 880, 0.5, 0.12);
        osc('square', 880, 1760, 0.5, 0.08);
    },
    step() {
        noise(0.04, 0.06, 500);
    },
};
};

// ── module: src/input.ts ──
__mods["src/input.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Input = void 0;
class Input {
    constructor() {
        this.keys = new Set();
        // edge-triggered events, consumed once per frame by the game
        this.fired = false;
        this.sprint = false;
        this.slotPressed = 0;
        this.wheelAcc = 0;
        this.mouseAcc = 0;
        this.onMove = (e) => {
            this.mouseAcc += (e.movementX || 0);
        };
        this.onKeyDown = (e) => {
            const k = e.key.toLowerCase();
            if (['arrowup', 'arrowdown', 'arrowleft', 'arrowright', ' '].includes(k))
                e.preventDefault();
            this.keys.add(k);
            if (k === ' ' || k === 'j')
                this.fired = true;
            if (k === '1' || k === '2' || k === '3')
                this.slotPressed = parseInt(k, 10);
        };
        this.onKeyUp = (e) => {
            this.keys.delete(e.key.toLowerCase());
        };
        this.onWheel = (e) => {
            e.preventDefault();
            this.wheelAcc += Math.sign(e.deltaY);
        };
        this.onClick = () => {
            this.fired = true;
        };
        this.onBlur = () => {
            this.keys.clear();
        };
    }
    attach() {
        window.addEventListener('keydown', this.onKeyDown);
        window.addEventListener('keyup', this.onKeyUp);
        window.addEventListener('wheel', this.onWheel, { passive: false });
        window.addEventListener('blur', this.onBlur);
        window.addEventListener('pointerdown', this.onClick);
        window.addEventListener('pointermove', this.onMove);
    }
    detach() {
        window.removeEventListener('keydown', this.onKeyDown);
        window.removeEventListener('keyup', this.onKeyUp);
        window.removeEventListener('wheel', this.onWheel);
        window.removeEventListener('blur', this.onBlur);
        window.removeEventListener('pointerdown', this.onClick);
        window.removeEventListener('pointermove', this.onMove);
    }
    consumeMouse() {
        const m = this.mouseAcc;
        this.mouseAcc = 0;
        return m;
    }
    down(k) {
        return this.keys.has(k.toLowerCase());
    }
    consumeFire() {
        const f = this.fired;
        this.fired = false;
        return f;
    }
    consumeSlot() {
        const s = this.slotPressed;
        this.slotPressed = 0;
        return s;
    }
    // returns +1/-1 weapon-slot scroll and zeroes the accumulator
    consumeWheel() {
        const w = Math.max(-1, Math.min(1, this.wheelAcc));
        this.wheelAcc = 0;
        return w;
    }
}
exports.Input = Input;
};

// ── module: src/map.ts ──
__mods["src/map.ts"] = function (exports, require, module) {
"use strict";
// 16x16 dungeon. 0 floor, 1-4 wall materials, 5 sealed gate (becomes 0 when open).
Object.defineProperty(exports, "__esModule", { value: true });
exports.SPAWNS = exports.WORLD = exports.MH = exports.MW = void 0;
exports.cell = cell;
exports.isSolid = isSolid;
exports.collideCircle = collideCircle;
exports.MW = 16;
exports.MH = 16;
exports.WORLD = [
    [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
    [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
    [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
    [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
    [1, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0, 3, 3, 0, 1],
    [1, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 1],
    [1, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 1],
    [1, 0, 0, 0, 0, 0, 4, 4, 4, 0, 0, 0, 0, 0, 0, 1],
    [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
    [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
    [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
    [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
    [1, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 1],
    [1, 0, 0, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 1],
    [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
    [1, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 1],
];
function cell(x, y) {
    if (x < 0 || y < 0 || x >= exports.MW || y >= exports.MH)
        return 1;
    return exports.WORLD[Math.floor(y)][Math.floor(x)];
}
function isSolid(x, y, openGate) {
    const c = cell(x, y);
    if (c === 0)
        return false;
    if (c === 5)
        return !openGate;
    return true;
}
// Circle (px, py, r) vs the 4 neighboring solid cells — returns push vector.
function collideCircle(px, py, r, openGate) {
    let pushX = 0;
    let pushY = 0;
    const minCx = Math.floor(px - r);
    const maxCx = Math.floor(px + r);
    const minCy = Math.floor(py - r);
    const maxCy = Math.floor(py + r);
    for (let cy = minCy; cy <= maxCy; cy++) {
        for (let cx = minCx; cx <= maxCx; cx++) {
            if (!isSolid(cx + 0.5, cy + 0.5, openGate))
                continue;
            const nx = Math.max(cx + 0.5 - r, Math.min(px, cx + 0.5 + r));
            const ny = Math.max(cy + 0.5 - r, Math.min(py, cy + 0.5 + r));
            const dx = px - nx;
            const dy = py - ny;
            const d2 = dx * dx + dy * dy;
            if (d2 < r * r) {
                const d = Math.sqrt(d2) || 0.0001;
                pushX += (dx / d) * (r - d);
                pushY += (dy / d) * (r - d);
            }
        }
    }
    return { pushX, pushY };
}
exports.SPAWNS = [
    { x: 2.5, y: 4.5, kind: 'imp' },
    { x: 12.5, y: 5.5, kind: 'imp' },
    { x: 5.5, y: 9.5, kind: 'imp' },
    { x: 10.5, y: 10.5, kind: 'imp' },
    { x: 8.5, y: 3.5, kind: 'imp' },
    { x: 13.5, y: 9.5, kind: 'grunt' },
    { x: 2.5, y: 12.5, kind: 'grunt' },
    { x: 8.5, y: 13.5, kind: 'grunt' },
    { x: 3.5, y: 9.5, kind: 'medkit' },
    { x: 12.5, y: 11.5, kind: 'medkit' },
    { x: 9.5, y: 6.5, kind: 'medkit' },
    { x: 6.5, y: 3.5, kind: 'ammo' },
    { x: 10.5, y: 13.5, kind: 'ammo' },
    { x: 2.5, y: 6.5, kind: 'ammo' },
    { x: 13.5, y: 10.5, kind: 'ammo' },
    { x: 8.5, y: 14.5, kind: 'gate' },
];
};

// ── module: src/enemies.ts ──
__mods["src/enemies.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.PLAYER_RADIUS = exports.ENEMY_STATS = void 0;
exports.makeEnemy = makeEnemy;
exports.makePickups = makePickups;
exports.lineOfSight = lineOfSight;
exports.updateEnemy = updateEnemy;
exports.findPath = findPath;
exports.wallColor = wallColor;
exports.shade = shade;
const map_1 = require("./map");
const audio_1 = require("./audio");
const ENEMY_DEF = {
    imp: { hp: 30, speed: 1.7, dmg: 7, range: 1.0, windup: 0.5, cooldown: 1.0, seed: 1 },
    grunt: { hp: 90, speed: 1.15, dmg: 13, range: 1.15, windup: 0.9, cooldown: 1.7, seed: 1 },
    boss: { hp: 340, speed: 1.5, dmg: 18, range: 1.5, windup: 0.8, cooldown: 1.3, seed: 5 },
};
exports.ENEMY_STATS = ENEMY_DEF;
function makeEnemy(kind, x, y) {
    const d = ENEMY_DEF[kind];
    return {
        kind,
        x,
        y,
        hp: d.hp,
        maxHp: d.hp,
        speed: d.speed,
        dmg: d.dmg,
        state: 'idle',
        atkCd: 0,
        pain: 0,
        flash: 0,
        deadAt: 0,
        seed: d.seed,
        slideA: 0,
        stuckT: 0,
    };
}
function makePickups() {
    return map_1.SPAWNS.map((s) => ({
        kind: s.kind,
        x: s.x,
        y: s.y,
        t: Math.random() * 10,
        taken: false,
    }));
}
exports.PLAYER_RADIUS = 0.24;
function lineOfSight(ax, ay, bx, by, openGate) {
    const dx = bx - ax;
    const dy = by - ay;
    const dist = Math.hypot(dx, dy);
    const steps = Math.max(1, Math.ceil(dist / 0.12));
    for (let i = 1; i < steps; i++) {
        const t = i / steps;
        if ((0, map_1.isSolid)(ax + dx * t, ay + dy * t, openGate))
            return false;
    }
    return true;
}
function moveWithCollision(e, vx, vy, r, openGate) {
    e.x += vx;
    e.y += vy;
    const p = (0, map_1.collideCircle)(e.x, e.y, r, openGate);
    e.x += p.pushX;
    e.y += p.pushY;
}
function separateEnemies(enemies, self) {
    for (const o of enemies) {
        if (o === self || o.state === 'dead')
            continue;
        const dx = o.x - self.x;
        const dy = o.y - self.y;
        const d2 = dx * dx + dy * dy;
        const min = 0.55;
        if (d2 > 0.0001 && d2 < min * min) {
            const d = Math.sqrt(d2);
            const push = (min - d) / 2;
            self.x -= (dx / d) * push;
            self.y -= (dy / d) * push;
        }
    }
}
// dt in seconds
function updateEnemy(e, dt, player, state, enemies, texts) {
    e.flash = Math.max(0, e.flash - dt);
    if (e.state === 'dead') {
        e.deadAt += dt;
        return;
    }
    const def = ENEMY_DEF[e.kind];
    const dx = player.x - e.x;
    const dy = player.y - e.y;
    const dist = Math.hypot(dx, dy) || 0.0001;
    const los = lineOfSight(e.x, e.y, player.x, player.y, state.gateOpen);
    if (e.state === 'idle') {
        if (los && dist < (e.kind === 'boss' ? 9 : 6))
            e.state = 'chase';
        return;
    }
    e.atkCd -= dt;
    if (e.pain > 0) {
        e.pain -= dt;
        return; // stagger: no movement
    }
    if (e.state === 'chase') {
        if (e.atkCd <= 0 && dist < def.range) {
            e.state = 'attack';
            e.atkCd = def.windup; // seconds until the strike lands
        }
        else {
            // move toward player, stop at attack range
            if (dist > def.range * 0.75) {
                let ang = Math.atan2(dy, dx);
                if (e.stuckT > 0) {
                    e.stuckT -= dt;
                    ang = e.slideA;
                }
                const bx = e.x, by = e.y;
                e.x += Math.cos(ang) * e.speed * dt;
                e.y += Math.sin(ang) * e.speed * dt;
                // if mostly blocked by geometry, slide around it
                const moved = Math.hypot(e.x - bx, e.y - by);
                if (e.stuckT <= 0 && moved < e.speed * dt * 0.3) {
                    e.stuckT = 0.8;
                    e.slideA = Math.atan2(dy, dx) + (Math.random() < 0.5 ? 1 : -1) * (Math.PI / 2);
                }
            }
            separateEnemies(enemies, e);
            const p = (0, map_1.collideCircle)(e.x, e.y, 0.28, state.gateOpen);
            e.x += p.pushX;
            e.y += p.pushY;
            if (dist < 0.6) {
                e.x -= (dx / dist) * (0.6 - dist) * 0.5;
                e.y -= (dy / dist) * (0.6 - dist) * 0.5;
            }
        }
    }
    else if (e.state === 'attack') {
        if (e.atkCd <= 0) {
            // strike
            if (dist < def.range * 1.2 && los && state.invuln <= 0) {
                player.hp -= e.dmg;
                state.hurtFlash = 0.5;
                state.shake = Math.max(state.shake, 0.3);
                audio_1.sfx.hurt();
                texts.push({ x: player.x, y: player.y, vy: 0.6, t: 0.8, text: `-${e.dmg}`, color: '#ff6a5a' });
            }
            e.atkCd = def.cooldown;
            e.state = 'chase';
        }
    }
}
// simple BFS on the 16x16 grid, returns path as list of cell centers
function findPath(sx, sy, tx, ty, openGate) {
    const scx = Math.floor(sx);
    const scy = Math.floor(sy);
    const tcx = Math.floor(tx);
    const tcy = Math.floor(ty);
    if (scx === tcx && scy === tcy)
        return [];
    const key = (x, y) => y * map_1.MW + x;
    const prev = new Map();
    const visited = new Set();
    const queue = [key(scx, scy)];
    visited.add(key(scx, scy));
    const dirs = [
        [1, 0],
        [-1, 0],
        [0, 1],
        [0, -1],
    ];
    let found = false;
    while (queue.length > 0) {
        const cur = queue.shift();
        const cx = cur % map_1.MW;
        const cy = Math.floor(cur / map_1.MW);
        if (cx === tcx && cy === tcy) {
            found = true;
            break;
        }
        for (const [ox, oy] of dirs) {
            const nx = cx + ox;
            const ny = cy + oy;
            if (nx < 0 || ny < 0 || nx >= map_1.MW || ny >= map_1.MH)
                continue;
            if ((0, map_1.isSolid)(nx + 0.5, ny + 0.5, openGate))
                continue;
            const nk = key(nx, ny);
            if (visited.has(nk))
                continue;
            visited.add(nk);
            prev.set(nk, cur);
            queue.push(nk);
        }
    }
    if (!found)
        return [];
    const path = [];
    let cur = key(tcx, tcy);
    while (cur !== key(scx, scy)) {
        const cx = cur % map_1.MW;
        const cy = Math.floor(cur / map_1.MW);
        path.unshift({ x: cx + 0.5, y: cy + 0.5 });
        cur = prev.get(cur);
    }
    return path;
}
function wallColor(c) {
    switch (c) {
        case 2:
            return { r: 158, g: 128, b: 96 };
        case 3:
            return { r: 96, g: 108, b: 130 };
        case 4:
            return { r: 130, g: 88, b: 84 };
        case 5:
            return { r: 200, g: 168, b: 48 };
        default:
            return { r: 122, g: 96, b: 92 };
    }
}
function shade(c, side, dist) {
    const base = wallColor(c);
    const fog = Math.max(0.18, 1 - dist / 11);
    const sideMul = side ? 0.66 : 1;
    const m = fog * sideMul;
    return { r: base.r * m, g: base.g * m, b: base.b * m };
}
};

// ── module: src/weapons.ts ──
__mods["src/weapons.ts"] = function (exports, require, module) {
"use strict";
// Three weapons: P-9 sidearm (infinite), SPE-12 shotgun, RG-100 raygun.
Object.defineProperty(exports, "__esModule", { value: true });
exports.makeWeapons = makeWeapons;
exports.fireShot = fireShot;
exports.ammoPickupAmount = ammoPickupAmount;
function makeWeapons() {
    return [
        { name: 'P-9 SIDEARM', short: 'P9', dmg: 12, pellets: 1, spread: 0.015, auto: false, interval: 0.24, maxAmmo: 999, ammo: 999, infinite: true },
        { name: 'SPES-12', short: 'S-12', dmg: 11, pellets: 8, spread: 0.1, auto: false, interval: 0.7, maxAmmo: 32, ammo: 8, infinite: false },
        { name: 'RG-100', short: 'RG', dmg: 9, pellets: 1, spread: 0.02, auto: true, interval: 0.09, maxAmmo: 200, ammo: 60, infinite: false },
    ];
}
function fireShot(w) {
    const shots = [];
    for (let i = 0; i < w.pellets; i++) {
        const off = w.pellets > 1
            ? (i / (w.pellets - 1) - 0.5) * w.spread * 2 + (Math.random() - 0.5) * w.spread * 0.5
            : (Math.random() - 0.5) * w.spread;
        shots.push({ dmg: w.dmg, angle: off, pierce: w.pellets === 1 && w.interval < 0.2 });
    }
    return shots;
}
function ammoPickupAmount() {
    return 12; // shotgun shells, shared with raygun
}
};

// ── module: src/game.ts ──
__mods["src/game.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Game = exports.PH = exports.PW = void 0;
const map_1 = require("./map");
const enemies_1 = require("./enemies");
const weapons_1 = require("./weapons");
const audio_1 = require("./audio");
exports.PW = 480;
exports.PH = 300;
const FOV = Math.PI / 3;
const PLAYER_SPEED = 3.4;
const SPRINT_MULT = 1.45;
const ROT_SPEED = 2.6;
const MAX_HP = 100;
const RESPAWN_INVULN = 2.0;
class Game {
    constructor(input) {
        this.gateCells = [];
        this.msgText = '';
        this.msgTimer = 0;
        this.input = input;
        this.state = {
            mode: 'title',
            time: 0,
            kills: 0,
            totalKills: 0,
            gateOpen: false,
            wonAt: 0,
            deadAt: 0,
            shake: 0,
            muzzle: 0,
            hitFlash: 0,
            hurtFlash: 0,
            invuln: 0,
        };
        this.player = { x: 8.5, y: 8.5, angle: 0, hp: MAX_HP, maxHp: MAX_HP, vx: 0, vy: 0, moving: false, bob: 0 };
        this.weapons = (0, weapons_1.makeWeapons)();
        this.weaponIdx = 0;
        this.fireCd = 0;
        this.enemies = [];
        this.pickups = [];
        this.texts = [];
        this.boss = (0, enemies_1.makeEnemy)('boss', 8.5, 2.2);
        this.bossSpawned = false;
        this.stepTimer = 0;
        this.hudTimer = 0;
    }
    startRun() {
        (0, audio_1.initAudio)();
        this.state.mode = 'playing';
        this.state.kills = 0;
        this.state.gateOpen = false;
        this.state.time = 0;
        this.state.shake = 0;
        this.state.muzzle = 0;
        this.state.hitFlash = 0;
        this.state.hurtFlash = 0;
        this.state.invuln = 0;
        this.state.wonAt = 0;
        this.state.deadAt = 0;
        this.player.x = 8.5;
        this.player.y = 8.5;
        this.player.angle = Math.PI; // face west down the open hall
        this.player.hp = MAX_HP;
        this.weapons = (0, weapons_1.makeWeapons)();
        this.weaponIdx = 0;
        this.fireCd = 0;
        this.bossSpawned = false;
        this.texts = [];
        // gate cells: row 15 (and any 5-cell found)
        this.gateCells = [];
        for (let y = 0; y < map_1.MH; y++) {
            for (let x = 0; x < map_1.MW; x++) {
                if ((0, map_1.cell)(x, y) === 5)
                    this.gateCells.push({ x, y });
            }
        }
        // spawn everything from defs
        this.enemies = [];
        this.pickups = [];
        for (const s of map_1.SPAWNS) {
            if (s.kind === 'imp' || s.kind === 'grunt' || s.kind === 'boss') {
                this.enemies.push((0, enemies_1.makeEnemy)(s.kind, s.x, s.y));
            }
            else if (s.kind === 'gate') {
                // gate marker (not an entity; gate is handled by cells)
            }
            else {
                this.pickups.push({ kind: s.kind, x: s.x, y: s.y, t: Math.random() * 10, taken: false });
            }
        }
        this.state.totalKills = this.enemies.length;
        this.showMsg('KILL ALL DEMONS TO OPEN THE GATE', 3.5);
    }
    respawn() {
        this.player.x = 8.5;
        this.player.y = 8.5;
        this.player.angle = Math.PI;
        this.player.hp = this.player.maxHp;
        this.state.invuln = RESPAWN_INVULN;
        this.state.hurtFlash = 0;
        // push nearby enemies back so the player isn't instantly re-killed
        for (const e of this.enemies) {
            if (e.state === 'dead')
                continue;
            const dx = e.x - this.player.x;
            const dy = e.y - this.player.y;
            const d = Math.hypot(dx, dy) || 0.001;
            if (d < 3.5) {
                e.x += (dx / d) * (3.6 - d);
                e.y += (dy / d) * (3.6 - d);
                e.state = 'idle';
                e.pain = 1.0;
            }
        }
        this.showMsg('REBORN... KEEP YOUR WARDING', 2.0);
    }
    showMsg(t, dur) {
        this.msgText = t;
        this.msgTimer = dur;
    }
    damagePlayer(dmg) {
        if (this.state.invuln > 0 || this.state.mode !== 'playing')
            return;
        this.player.hp -= dmg;
        this.state.hurtFlash = 0.5;
        this.state.shake = Math.max(this.state.shake, 0.3);
        if (this.player.hp <= 0) {
            this.player.hp = 0;
            this.state.mode = 'dead';
            this.state.deadAt = 0;
            audio_1.sfx.die();
        }
    }
    openGate() {
        this.state.gateOpen = true;
        audio_1.sfx.gate();
        this.showMsg('THE GATE IS OPEN — ESCAPE SOUTHWEST!', 4);
    }
    update(dt) {
        const s = this.state;
        s.time += dt;
        s.shake = Math.max(0, s.shake - dt * 1.4);
        s.muzzle = Math.max(0, s.muzzle - dt * 4);
        s.hitFlash = Math.max(0, s.hitFlash - dt);
        s.hurtFlash = Math.max(0, s.hurtFlash - dt);
        s.invuln = Math.max(0, s.invuln - dt);
        if (this.msgTimer > 0) {
            this.msgTimer -= dt;
            if (this.msgTimer <= 0)
                this.msgText = '';
        }
        for (const t of this.texts) {
            t.t -= dt;
            t.y += t.vy * dt;
        }
        this.texts = this.texts.filter((t) => t.t > 0);
        if (s.mode === 'dead') {
            s.deadAt += dt;
            return;
        }
        if (s.mode === 'win') {
            s.wonAt += dt;
            return;
        }
        if (s.mode !== 'playing')
            return;
        // ---- player movement
        const p = this.player;
        let mx = 0;
        let my = 0;
        const k = this.input.keys;
        const sprint = this.input.down('shift');
        if (k.has('w') || k.has('arrowup'))
            my -= 1;
        if (k.has('s') || k.has('arrowdown'))
            my += 1;
        if (k.has('a') || k.has('arrowleft'))
            mx -= 1;
        if (k.has('d') || k.has('arrowright'))
            mx += 1;
        if (k.has('arrowleft'))
            p.angle -= ROT_SPEED * dt;
        if (k.has('arrowright'))
            p.angle += ROT_SPEED * dt;
        if (k.has('q'))
            p.angle -= ROT_SPEED * 0.8 * dt;
        if (k.has('e'))
            p.angle += ROT_SPEED * 0.8 * dt;
        p.angle += this.input.consumeMouse() * 0.0028;
        const speed = PLAYER_SPEED * (sprint ? SPRINT_MULT : 1);
        if (mx !== 0 || my !== 0) {
            const inv = 1 / Math.hypot(mx, my);
            const dirX = Math.cos(p.angle);
            const dirY = Math.sin(p.angle);
            const rx = -dirY;
            const ry = dirX;
            p.vx = (dirX * -my + rx * mx) * speed * inv;
            p.vy = (dirY * -my + ry * mx) * speed * inv;
            p.x += p.vx * dt;
            p.y += p.vy * dt;
            const col = (0, map_1.collideCircle)(p.x, p.y, enemies_1.PLAYER_RADIUS, s.gateOpen);
            p.x += col.pushX;
            p.y += col.pushY;
            p.moving = true;
            p.bob += dt * (sprint ? 13 : 9);
            this.stepTimer -= dt;
            if (this.stepTimer <= 0) {
                audio_1.sfx.step();
                this.stepTimer = 0.32;
            }
        }
        else {
            p.vx = 0;
            p.vy = 0;
            p.moving = false;
        }
        // ---- weapons: slot switching (edge-triggered)
        const w = this.weapons[this.weaponIdx];
        const slot = this.input.consumeSlot();
        if (slot >= 1 && slot <= 3 && slot !== this.weaponIdx + 1) {
            this.weaponIdx = slot - 1;
            this.fireCd = Math.max(this.fireCd, 0.12);
        }
        const wheel = this.input.consumeWheel();
        if (wheel !== 0) {
            this.weaponIdx = (this.weaponIdx + wheel + 3) % 3;
            this.fireCd = Math.max(this.fireCd, 0.12);
        }
        // ---- firing
        this.fireCd -= dt;
        const wantFire = this.input.down(' ') || this.input.down('j') || this.input.down('m') || this.input.consumeFire();
        if (wantFire && this.fireCd <= 0) {
            if (!w.infinite && w.ammo <= 0) {
                audio_1.sfx.dry();
                this.fireCd = 0.25;
                // auto-fall back to pistol if possible
                if (this.weaponIdx !== 0 && this.weapons[0].ammo > 0) {
                    this.weaponIdx = 0;
                }
            }
            else {
                if (!w.infinite)
                    w.ammo--;
                this.fireCd = w.interval;
                s.muzzle = 1;
                const shots = (0, weapons_1.fireShot)(w);
                for (const sh of shots)
                    this.fireRay(p.angle + sh.angle, sh.dmg, sh.pierce);
                if (this.weaponIdx === 1)
                    s.shake = Math.max(s.shake, 0.12);
                if (this.weaponIdx === 2)
                    s.shake = Math.max(s.shake, 0.08);
            }
        }
        // ---- pickups
        for (const pk of this.pickups) {
            if (pk.taken)
                continue;
            pk.t += dt;
            const dx = pk.x - p.x;
            const dy = pk.y - p.y;
            if (dx * dx + dy * dy < 0.42 * 0.42) {
                if (pk.kind === 'medkit') {
                    if (p.hp >= p.maxHp)
                        continue;
                    p.hp = Math.min(p.maxHp, p.hp + 40);
                    pk.taken = true;
                    audio_1.sfx.pickup();
                    this.texts.push({ x: p.x, y: p.y, vy: 0.6, t: 0.9, text: '+40', color: '#7dffa0' });
                }
                else if (pk.kind === 'ammo') {
                    // split evenly between the two non-infinite weapons
                    const amt = (0, weapons_1.ammoPickupAmount)();
                    const others = this.weapons.filter((x) => !x.infinite);
                    const half = Math.ceil(amt / 2);
                    let added = 0;
                    for (let i = 0; i < others.length; i++) {
                        const give = i === 0 ? half : amt - half;
                        const before = others[i].ammo;
                        others[i].ammo = Math.min(others[i].maxAmmo, before + give);
                        added += others[i].ammo - before;
                    }
                    pk.taken = true;
                    audio_1.sfx.pickup();
                    this.texts.push({ x: p.x, y: p.y, vy: 0.6, t: 0.9, text: `+${added} AMMO`, color: '#ffd977' });
                }
            }
        }
        // ---- boss spawn when all regular demons fall
        if (!this.bossSpawned) {
            const aliveRegular = this.enemies.some((e) => e.kind !== 'boss' && e.state !== 'dead');
            if (!aliveRegular) {
                this.bossSpawned = true;
                this.enemies.push(this.boss);
                this.state.totalKills = this.enemies.length;
                audio_1.sfx.gate();
                this.showMsg('A CHAMPION OF THE PIT APPEARS!', 3.2);
            }
        }
        // ---- enemies
        for (const e of this.enemies) {
            (0, enemies_1.updateEnemy)(e, dt, p, s, this.enemies, this.texts);
        }
        this.enemies = this.enemies.filter((e) => e.state !== 'dead' || e.deadAt < 1.6);
        // gate opens when every enemy (incl. boss) is dead
        if (!s.gateOpen && this.enemies.every((e) => e.state === 'dead')) {
            this.openGate();
        }
        // death check
        if (this.player.hp <= 0) {
            this.state.mode = 'dead';
            this.state.deadAt = 0;
            audio_1.sfx.die();
        }
        // win: reach the gate row once it's open
        if (s.gateOpen && Math.floor(p.y) >= map_1.MH - 1) {
            s.mode = 'win';
            s.wonAt = 0;
        }
    }
    fireRay(angle, dmg, pierce) {
        const p = this.player;
        const s = this.state;
        const dx = Math.cos(angle);
        const dy = Math.sin(angle);
        // wall distance
        let wallDist = 100;
        {
            const steps = Math.ceil(100 / 0.05);
            for (let i = 1; i < steps; i++) {
                const t = i * 0.05;
                if ((0, map_1.isSolid)(p.x + dx * t, p.y + dy * t, s.gateOpen)) {
                    wallDist = t;
                    break;
                }
            }
        }
        let hitEnemy = null;
        let hitDist = wallDist;
        for (const e of this.enemies) {
            if (e.state === 'dead')
                continue;
            const relx = e.x - p.x;
            const rely = e.y - p.y;
            const t = relx * dx + rely * dy;
            if (t < 0 || t > hitDist)
                continue;
            const perp = Math.abs(relx * dy - rely * dx);
            const r = e.kind === 'boss' ? 0.42 : 0.3;
            if (perp < r) {
                hitDist = t;
                hitEnemy = e;
            }
        }
        if (hitEnemy) {
            hitEnemy.hp -= dmg;
            hitEnemy.flash = 0.12;
            hitEnemy.pain = 0.15;
            hitEnemy.state = 'chase';
            this.texts.push({
                x: hitEnemy.x,
                y: hitEnemy.y,
                vy: 0.5,
                t: 0.5,
                text: `-${dmg}`,
                color: '#ffe9a0',
            });
            if (hitEnemy.hp <= 0) {
                hitEnemy.state = 'dead';
                hitEnemy.deadAt = 0;
                this.state.kills++;
                audio_1.sfx.kill();
            }
        }
        else if (!pierce && wallDist < 30) {
            // wall spark
            this.texts.push({
                x: p.x + dx * wallDist,
                y: p.y + dy * wallDist,
                vy: -0.2,
                t: 0.25,
                text: '*',
                color: '#cfc7a8',
            });
        }
        // pierce continues past first hit for the raygun
    }
}
exports.Game = Game;
};

// ── module: src/renderer.ts ──
__mods["src/renderer.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Renderer = void 0;
const game_1 = require("./game");
const map_1 = require("./map");
class Renderer {
    constructor(canvas, radar) {
        this.wallTop = new Float32Array(game_1.PW);
        this.wallBot = new Float32Array(game_1.PW);
        this.time = 0;
        this.ctx = canvas.getContext('2d');
        this.off = document.createElement('canvas');
        this.off.width = game_1.PW;
        this.off.height = game_1.PH;
        this.octx = this.off.getContext('2d');
        this.img = this.octx.createImageData(game_1.PW, game_1.PH);
        this.zbuf = { arr: new Float32Array(game_1.PW) };
        this.radar = radar;
        this.rctx = radar.getContext('2d');
    }
    render(game, dt) {
        this.time += dt;
        const s = game.state;
        const p = game.player;
        const px = p.x + (Math.random() - 0.5) * s.shake * 0.12;
        const py = p.y + (Math.random() - 0.5) * s.shake * 0.12;
        const dirX = Math.cos(p.angle);
        const dirY = Math.sin(p.angle);
        const planeX = -dirY * Math.tan(FOV_HALF);
        const planeY = dirX * Math.tan(FOV_HALF);
        const data = this.img.data;
        const zbuf = this.zbuf.arr;
        // ---- cast walls
        for (let x = 0; x < game_1.PW; x++) {
            const cameraX = (2 * x) / game_1.PW - 1;
            const rdX = dirX + planeX * cameraX;
            const rdY = dirY + planeY * cameraX;
            let mapX = Math.floor(px);
            let mapY = Math.floor(py);
            const deltaX = Math.abs(1 / (rdX || 1e-9));
            const deltaY = Math.abs(1 / (rdY || 1e-9));
            let stepX, stepY, sideX, sideY;
            if (rdX < 0) {
                stepX = -1;
                sideX = (px - mapX) * deltaX;
            }
            else {
                stepX = 1;
                sideX = (mapX + 1 - px) * deltaX;
            }
            if (rdY < 0) {
                stepY = -1;
                sideY = (py - mapY) * deltaY;
            }
            else {
                stepY = 1;
                sideY = (mapY + 1 - py) * deltaY;
            }
            let side = 0;
            let hit = 0;
            for (let i = 0; i < 96; i++) {
                if (sideX < sideY) {
                    sideX += deltaX;
                    mapX += stepX;
                    side = 0;
                }
                else {
                    sideY += deltaY;
                    mapY += stepY;
                    side = 1;
                }
                const c = (0, map_1.cell)(mapX, mapY);
                if (c !== 0) {
                    hit = c;
                    break;
                }
            }
            let perp = side === 0 ? sideX - deltaX : sideY - deltaY;
            if (perp < 0.01)
                perp = 0.01;
            zbuf[x] = perp;
            const lineH = Math.min(game_1.PH * 3, Math.floor(game_1.PH / perp));
            const y0 = Math.max(0, Math.floor(game_1.PH / 2 - lineH / 2));
            const y1 = Math.min(game_1.PH, Math.ceil(game_1.PH / 2 + lineH / 2));
            this.wallTop[x] = y0;
            this.wallBot[x] = y1;
            // wall texture coord
            let wallX = side === 0 ? py + perp * rdY : px + perp * rdX;
            wallX -= Math.floor(wallX);
            const tex = wallX * 8;
            // base color with shading
            const fog = Math.max(0.15, 1 - perp / 10.5);
            const shadeMul = (side === 1 ? 0.62 : 1) * fog;
            let r, g, b;
            switch (hit) {
                case 2:
                    r = 158;
                    g = 128;
                    b = 96;
                    break;
                case 3:
                    r = 96;
                    g = 108;
                    b = 130;
                    break;
                case 4:
                    r = 130;
                    g = 88;
                    b = 84;
                    break;
                case 5: {
                    // gate: sealed = pulsing gold, open = bright green escape portal
                    const pulse = 0.72 + 0.28 * Math.sin(this.time * 5);
                    const fog2 = Math.max(0.35, fog);
                    if (s.gateOpen) {
                        r = 110 * pulse * fog2 + 60;
                        g = 235 * fog2;
                        b = 130 * pulse * fog2 + 40;
                    }
                    else {
                        r = 240 * pulse * fog2;
                        g = 195 * pulse * fog2;
                        b = 60 * pulse * fog2;
                    }
                    if (tex > 3.2 && tex < 4.8) {
                        r *= 0.35;
                        g *= 0.35;
                        b *= 0.35;
                    }
                    this.fillColumn(data, x, y0, y1, r, g, b);
                    continue;
                }
                default:
                    r = 122;
                    g = 96;
                    b = 92;
                    break;
            }
            r *= shadeMul;
            g *= shadeMul;
            b *= shadeMul;
            // texture stripes
            if (tex > 3.2 && tex < 4.8) {
                r *= 0.72;
                g *= 0.72;
                b *= 0.72;
            }
            if (tex > 6.4 && tex < 6.8) {
                r *= 1.25;
                g *= 1.25;
                b *= 1.25;
            }
            this.fillColumn(data, x, y0, y1, r, g, b);
        }
        // ---- floor/ceiling (skip wall columns)
        this.drawFloorCeiling(data, px, py, dirX, dirY, planeX, planeY);
        // ---- sprites
        const sprites = [];
        for (const e of game.enemies)
            sprites.push({ x: e.x, y: e.y, kind: 'enemy', ref: e, t: this.time + e.seed });
        for (const pk of game.pickups) {
            if (pk.taken)
                continue;
            sprites.push({ x: pk.x, y: pk.y, kind: 'pickup', ref: pk, t: this.time + pk.t });
        }
        for (const f of game.texts) {
            sprites.push({ x: f.x, y: f.y, kind: 'text', ref: f, t: this.time });
        }
        sprites.sort((a, b) => (b.x - px) ** 2 + (b.y - py) ** 2 - ((a.x - px) ** 2 + (a.y - py) ** 2));
        const invDet = 1 / (planeX * dirY - dirX * planeY);
        for (const sp of sprites) {
            const sx = sp.x - px;
            const sy = sp.y - py;
            const tx = invDet * (dirY * sx - dirX * sy);
            const ty = invDet * (-planeY * sx + planeX * sy); // depth
            if (ty <= 0.1)
                continue;
            const screenX = (game_1.PW / 2) * (1 + tx / ty);
            if (sp.kind === 'text') {
                this.drawTextSprite(data, screenX, ty, sp.ref);
                continue;
            }
            if (sp.kind === 'enemy') {
                this.drawEnemy(data, zbuf, screenX, ty, sp.ref);
            }
            else {
                this.drawPickup(data, zbuf, screenX, ty, sp.ref, sp.t);
            }
        }
        // blit
        this.octx.putImageData(this.img, 0, 0);
        this.drawWeapon(this.octx, game);
        this.drawCrosshair(this.octx, game);
        this.drawDamage(this.octx, s);
        this.ctx.imageSmoothingEnabled = false;
        this.ctx.clearRect(0, 0, game_1.PW, game_1.PH);
        this.ctx.drawImage(this.off, 0, 0, game_1.PW, game_1.PH, 0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
        this.drawRadar(game);
    }
    fillColumn(data, x, y0, y1, r, g, b) {
        const ri = Math.round(r) & 255;
        const gi = Math.round(g) & 255;
        const bi = Math.round(b) & 255;
        for (let y = y0; y < y1; y++) {
            const i = (y * game_1.PW + x) * 4;
            data[i] = ri;
            data[i + 1] = gi;
            data[i + 2] = bi;
            data[i + 3] = 255;
        }
    }
    drawFloorCeiling(data, px, py, dirX, dirY, planeX, planeY) {
        const h = game_1.PH / 2;
        for (let y = 0; y < game_1.PH; y++) {
            const isFloor = y > h;
            const dist = h / (Math.abs(y - h) || 1);
            const fog = Math.max(0.12, 1 - dist / 11);
            const fr = Math.floor(dist * 2);
            for (let x = 0; x < game_1.PW; x++) {
                if (y >= this.wallTop[x] && y < this.wallBot[x])
                    continue;
                const camX = (2 * x) / game_1.PW - 1;
                const wx = px + (dirX + planeX * camX) * dist;
                const wy = py + (dirY + planeY * camX) * dist;
                const stripe = (Math.floor(wx * 2) + Math.floor(wy * 2)) & 1;
                let r, g, b;
                if (isFloor) {
                    if (stripe) {
                        r = 43;
                        g = 41;
                        b = 38;
                    }
                    else {
                        r = 30;
                        g = 28;
                        b = 26;
                    }
                }
                else {
                    if (stripe) {
                        r = 17;
                        g = 16;
                        b = 17;
                    }
                    else {
                        r = 11;
                        g = 10;
                        b = 11;
                    }
                }
                const i = (y * game_1.PW + x) * 4;
                data[i] = (r * fog) & 255;
                data[i + 1] = (g * fog) & 255;
                data[i + 2] = (b * fog) & 255;
                data[i + 3] = 255;
            }
        }
    }
    // draw a sprite rectangle per column with zbuffer occlusion
    blitRect(data, zbuf, cx, ty, w, hgt, yOff, color) {
        const x0 = Math.max(0, Math.floor(cx - w / 2));
        const x1 = Math.min(game_1.PW - 1, Math.ceil(cx + w / 2));
        const yTop = Math.floor(game_1.PH / 2 - hgt / 2 + yOff);
        const yBot = Math.min(game_1.PH - 1, Math.ceil(game_1.PH / 2 + hgt / 2 + yOff));
        for (let x = x0; x <= x1; x++) {
            if (zbuf[x] < ty)
                continue; // occluded
            const u = (x - (cx - w / 2)) / w;
            for (let y = yTop; y <= yBot; y++) {
                const v = (y - (game_1.PH / 2 - hgt / 2 + yOff)) / hgt;
                if (v < 0 || v > 1)
                    continue;
                const [r, g, b, a] = color(u, v);
                if (a <= 0)
                    continue;
                const i = (y * game_1.PW + x) * 4;
                data[i] = r;
                data[i + 1] = g;
                data[i + 2] = b;
                data[i + 3] = 255;
            }
        }
    }
    drawEnemy(data, zbuf, cx, ty, e) {
        const scale = game_1.PH / ty;
        const w = e.kind === 'boss' ? 0.85 * scale : 0.6 * scale;
        const hgt = e.kind === 'boss' ? 1.5 * scale : 1.05 * scale;
        const fog = Math.max(0.18, 1 - ty / 10);
        const flash = e.flash > 0;
        const walking = Math.sin(this.time * 8 + e.seed * 7);
        const yOff = (walking * 0.03 * scale) - (e.state === 'dead' ? (e.deadAt / 1.6) * hgt * 0.5 : 0);
        const alpha = e.state === 'dead' ? Math.max(0, 1 - e.deadAt / 1.6) : 1;
        let bodyR, bodyG, bodyB, eyeR, eyeG, eyeB;
        if (e.kind === 'imp') {
            bodyR = 186;
            bodyG = 74;
            bodyB = 52;
            eyeR = 255;
            eyeG = 220;
            eyeB = 80;
        }
        else if (e.kind === 'grunt') {
            bodyR = 82;
            bodyG = 126;
            bodyB = 100;
            eyeR = 255;
            eyeG = 90;
            eyeB = 60;
        }
        else {
            bodyR = 120;
            bodyG = 30;
            bodyB = 60;
            eyeR = 120;
            eyeG = 255;
            eyeB = 160;
        }
        if (flash) {
            bodyR = 255;
            bodyG = 255;
            bodyB = 255;
        }
        const hr = bodyR * fog, hg = bodyG * fog, hb = bodyB * fog;
        const er = eyeR * fog, eg = eyeG * fog, eb = eyeB * fog;
        const hornR = bodyR * 0.7 * fog;
        this.blitRect(data, zbuf, cx, ty, w, hgt, yOff, (u, v) => {
            // head
            if (v < 0.32) {
                const dx = u - 0.5;
                const dy = (v - 0.16) / 0.16;
                if (dx * dx * 4 + dy * dy < 1) {
                    // eyes
                    const ex = u - 0.5;
                    const ey = (v - 0.18) / 0.05;
                    if (ex * ex * 6 + ey * ey < 0.55 && v > 0.14 && v < 0.22)
                        return [Math.round(er), Math.round(eg), Math.round(eb), 255];
                    return [Math.round(hr), Math.round(hg), Math.round(hb), 255];
                }
                // horns
                const hx = Math.abs(u - 0.5) - 0.28;
                const hy = (v - 0.05) / 0.1;
                if (hx > -0.05 && hx < 0.12 && hy > -1 && hy < 1 && v < 0.12)
                    return [Math.round(hornR), Math.round(hornR * 0.9), Math.round(hornR * 0.8), 255];
                return [0, 0, 0, 0];
            }
            // body
            const bw = e.kind === 'boss' ? 0.4 : 0.3;
            const dx = (u - 0.5) / bw;
            const dy = (v - 0.62) / 0.36;
            if (dx * dx + dy * dy < 1)
                return [Math.round(hr * 0.9), Math.round(hg * 0.9), Math.round(hb * 0.9), 255];
            // arms
            const ax = Math.abs(u - 0.5) - (e.kind === 'boss' ? 0.46 : 0.38);
            const ay = (v - 0.55) / 0.2;
            if (ax > -0.06 && ax < 0.1 && ay > -1 && ay < 1)
                return [Math.round(hr), Math.round(hg), Math.round(hb), 255];
            return [0, 0, 0, 0];
        });
        // HP bar for damaged enemies
        if (e.state !== 'dead' && e.hp < e.maxHp) {
            const bw = Math.min(w * 0.8, 40 / (ty * 0.5 + 0.6));
            const bx = cx - bw / 2;
            const by = game_1.PH / 2 - hgt / 2 + yOff - 4;
            const frac = Math.max(0, e.hp / e.maxHp);
            for (let x = Math.max(0, Math.floor(bx)); x < Math.min(game_1.PW, Math.ceil(bx + bw)); x++) {
                if (zbuf[x] < ty)
                    continue;
                for (let y = Math.max(0, Math.floor(by)); y < Math.min(game_1.PH, by + 2); y++) {
                    const i = (y * game_1.PW + x) * 4;
                    const u = (x - bx) / bw;
                    if (u < frac) {
                        data[i] = 220;
                        data[i + 1] = 60;
                        data[i + 2] = 60;
                    }
                    else {
                        data[i] = 40;
                        data[i + 1] = 40;
                        data[i + 2] = 40;
                    }
                    data[i + 3] = 255;
                }
            }
        }
        void alpha;
    }
    drawPickup(data, zbuf, cx, ty, pk, t) {
        const scale = game_1.PH / ty;
        const w = 0.34 * scale;
        const hgt = 0.34 * scale;
        const bob = Math.sin(t * 3) * 0.05 * scale;
        const yOff = hgt * 0.25 + bob;
        const fog = Math.max(0.25, 1 - ty / 10);
        if (pk.kind === 'medkit') {
            this.blitRect(data, zbuf, cx, ty, w, hgt, yOff, (u, v) => {
                if (v > 0.2 && v < 0.85 && u > 0.1 && u < 0.9) {
                    // white box with red cross
                    const cross = (Math.abs(u - 0.5) < 0.1 && v > 0.35 && v < 0.7) || (Math.abs(v - 0.5) < 0.1 && u > 0.35 && u < 0.65);
                    if (cross)
                        return [Math.round(220 * fog), Math.round(50 * fog), Math.round(50 * fog), 255];
                    return [Math.round(230 * fog), Math.round(230 * fog), Math.round(230 * fog), 255];
                }
                return [0, 0, 0, 0];
            });
        }
        else {
            this.blitRect(data, zbuf, cx, ty, w, hgt, yOff, (u, v) => {
                if (v > 0.25 && v < 0.85 && u > 0.2 && u < 0.8) {
                    // ammo box: brown with yellow bands
                    const band = (v > 0.42 && v < 0.58);
                    if (band)
                        return [Math.round(230 * fog), Math.round(190 * fog), Math.round(60 * fog), 255];
                    return [Math.round(140 * fog), Math.round(100 * fog), Math.round(60 * fog), 255];
                }
                return [0, 0, 0, 0];
            });
        }
    }
    drawTextSprite(data, cx, ty, f) {
        const size = 7;
        const chars = f.text;
        const w = chars.length * size;
        const x0 = Math.round(cx - w / 2);
        const scale = game_1.PH / ty;
        let y = Math.round(game_1.PH / 2 + 0.35 * scale - (0.8 - f.t) * 22);
        y = Math.max(2, Math.min(game_1.PH - size - 2, y));
        const col = this.hex(f.color);
        const alpha = Math.max(0, Math.min(1, f.t / 0.4));
        const cr = Math.round(col[0] * alpha), cg = Math.round(col[1] * alpha), cb = Math.round(col[2] * alpha);
        for (let i = 0; i < chars.length; i++) {
            const ch = chars[i];
            const ox = x0 + i * size;
            for (let vy = 0; vy < 7; vy++) {
                const row = FONT[ch] ? FONT[ch][vy] : 0;
                for (let vx = 0; vx < 5; vx++) {
                    if (!(row >> vx & 1))
                        continue;
                    const xx = ox + vx;
                    const yy = y + vy;
                    if (xx < 0 || xx >= game_1.PW || yy < 0 || yy >= game_1.PH)
                        continue;
                    if (this.zbuf.arr[xx] < ty)
                        continue;
                    const idx = (yy * game_1.PW + xx) * 4;
                    data[idx] = cr;
                    data[idx + 1] = cg;
                    data[idx + 2] = cb;
                    data[idx + 3] = 255;
                }
            }
        }
    }
    hex(h) {
        const v = parseInt(h.slice(1), 16);
        return [(v >> 16) & 255, (v >> 8) & 255, v & 255];
    }
    drawWeapon(ctx, game) {
        const p = game.player;
        const s = game.state;
        const w = game.weapons[game.weaponIdx];
        const bobX = Math.sin(p.bob) * 6 * (p.moving ? 1 : 0.3);
        const bobY = Math.abs(Math.cos(p.bob)) * 4 * (p.moving ? 1 : 0.3);
        const recoil = s.muzzle * 8;
        const bx = game_1.PW / 2 + bobX + 20;
        const by = game_1.PH - 20 + bobY + recoil;
        ctx.save();
        ctx.imageSmoothingEnabled = false;
        if (game.weaponIdx === 0) {
            // pistol
            this.prim(ctx, 'rect', bx - 8, by - 26, 16, 26, '#3a3f46');
            this.prim(ctx, 'rect', bx - 12, by - 40, 24, 16, '#4a4f58');
            this.prim(ctx, 'rect', bx - 3, by - 46, 6, 8, '#22252a');
            this.prim(ctx, 'rect', bx - 8, by - 8, 16, 6, '#2c2f34');
        }
        else if (game.weaponIdx === 1) {
            // shotgun
            this.prim(ctx, 'rect', bx - 10, by - 30, 20, 30, '#5a4632');
            this.prim(ctx, 'rect', bx - 14, by - 52, 28, 24, '#3c3a38');
            this.prim(ctx, 'rect', bx - 5, by - 60, 10, 10, '#232323');
            this.prim(ctx, 'rect', bx - 14, by - 40, 28, 4, '#8a6a3a');
        }
        else {
            // raygun
            this.prim(ctx, 'rect', bx - 12, by - 28, 24, 28, '#2a4a4a');
            this.prim(ctx, 'rect', bx - 16, by - 44, 32, 18, '#1e3a3a');
            this.prim(ctx, 'rect', bx - 4, by - 52, 8, 10, '#39f5c8');
            const glow = 0.5 + 0.5 * Math.sin(this.time * 6);
            ctx.globalAlpha = 0.5 + glow * 0.4;
            this.prim(ctx, 'rect', bx - 2, by - 58, 4, 6, '#a8ffe8');
            ctx.globalAlpha = 1;
        }
        // muzzle flash
        if (s.muzzle > 0.4) {
            const fx = bx;
            const fy = by - 62;
            const r = 10 + s.muzzle * 12;
            ctx.globalAlpha = s.muzzle * 0.9;
            ctx.fillStyle = '#ffe9a0';
            ctx.beginPath();
            ctx.moveTo(fx, fy - r);
            ctx.lineTo(fx + r * 0.6, fy);
            ctx.lineTo(fx, fy + r * 0.4);
            ctx.lineTo(fx - r * 0.6, fy);
            ctx.closePath();
            ctx.fill();
            ctx.fillStyle = '#fff';
            ctx.beginPath();
            ctx.arc(fx, fy, r * 0.35, 0, Math.PI * 2);
            ctx.fill();
            ctx.globalAlpha = 1;
        }
        ctx.restore();
        void w;
    }
    prim(ctx, kind, x, y, w, h, c) {
        ctx.fillStyle = c;
        ctx.fillRect(Math.round(x), Math.round(y), Math.round(w), Math.round(h));
    }
    drawCrosshair(ctx, game) {
        const s = game.state;
        const cx = game_1.PW / 2;
        const cy = game_1.PH / 2;
        const gap = 4 + s.muzzle * 6;
        ctx.fillStyle = 'rgba(200,255,210,0.85)';
        ctx.fillRect(cx - 1, cy - gap - 4, 2, 4);
        ctx.fillRect(cx - 1, cy + gap, 2, 4);
        ctx.fillRect(cx - gap - 4, cy - 1, 4, 2);
        ctx.fillRect(cx + gap, cy - 1, 4, 2);
        ctx.fillRect(cx - 1, cy - 1, 2, 2);
    }
    drawDamage(ctx, s) {
        if (s.hurtFlash > 0) {
            ctx.fillStyle = `rgba(200,30,20,${s.hurtFlash * 0.4})`;
            ctx.fillRect(0, 0, game_1.PW, game_1.PH);
        }
    }
    drawRadar(game) {
        const ctx = this.rctx;
        const W = this.radar.width;
        const H = this.radar.height;
        ctx.clearRect(0, 0, W, H);
        const scale = W / 9; // show ~9x9 cells around player
        const p = game.player;
        const toR = (wx, wy) => {
            const dx = wx - p.x;
            const dy = wy - p.y;
            const ca = Math.cos(-p.angle), sa = Math.sin(-p.angle);
            const rx = dx * ca - dy * sa;
            const ry = dx * sa + dy * ca;
            return [W / 2 + rx * scale, H / 2 + ry * scale];
        };
        // walls
        for (let y = 0; y < map_1.MH; y++) {
            for (let x = 0; x < map_1.MW; x++) {
                const c = (0, map_1.cell)(x, y);
                if (c === 0)
                    continue;
                if (c === 5 && game.state.gateOpen)
                    continue;
                const [rx, ry] = toR(x + 0.5, y + 0.5);
                if (rx < 0 || ry < 0 || rx > W || ry > H)
                    continue;
                ctx.fillStyle = c === 5 ? '#e8c840' : '#5a6a5a';
                ctx.fillRect(rx - 2, ry - 2, 4, 4);
            }
        }
        // pickups
        for (const pk of game.pickups) {
            if (pk.taken)
                continue;
            const [rx, ry] = toR(pk.x, pk.y);
            if (rx < 0 || ry < 0 || rx > W || ry > H)
                continue;
            ctx.fillStyle = pk.kind === 'medkit' ? '#7dffa0' : '#ffd977';
            ctx.fillRect(rx - 1, ry - 1, 2, 2);
        }
        // enemies
        for (const e of game.enemies) {
            if (e.state === 'dead')
                continue;
            const [rx, ry] = toR(e.x, e.y);
            if (rx < 0 || ry < 0 || rx > W || ry > H)
                continue;
            ctx.fillStyle = e.kind === 'boss' ? '#ff5a8a' : '#ff6a5a';
            const r = e.kind === 'boss' ? 4 : 2.5;
            ctx.beginPath();
            ctx.arc(rx, ry, r, 0, Math.PI * 2);
            ctx.fill();
        }
        // player triangle
        ctx.save();
        ctx.translate(W / 2, H / 2);
        ctx.rotate(-p.angle - Math.PI / 2);
        ctx.fillStyle = '#eaffea';
        ctx.beginPath();
        ctx.moveTo(0, -6);
        ctx.lineTo(4, 5);
        ctx.lineTo(-4, 5);
        ctx.closePath();
        ctx.fill();
        ctx.restore();
    }
}
exports.Renderer = Renderer;
const FOV_HALF = Math.PI / 6;
// 5x7 bitmap font (each char -> 7 row-bitmasks, 5 wide)
const FONT = {
    '0': [14, 17, 17, 17, 17, 17, 14],
    '1': [4, 12, 4, 4, 4, 4, 14],
    '2': [14, 17, 1, 2, 4, 8, 31],
    '3': [31, 4, 2, 4, 1, 17, 14],
    '4': [2, 6, 10, 18, 31, 4, 4],
    '5': [31, 8, 14, 1, 1, 17, 14],
    '6': [6, 8, 14, 17, 17, 17, 14],
    '7': [31, 1, 2, 4, 8, 8, 8],
    '8': [14, 17, 17, 14, 17, 17, 14],
    '9': [14, 17, 17, 15, 1, 2, 12],
    'A': [14, 17, 17, 31, 17, 17, 17],
    'B': [30, 16, 30, 16, 16, 16, 30],
    'C': [14, 17, 16, 16, 16, 17, 14],
    'D': [30, 16, 16, 16, 16, 16, 30],
    'E': [31, 16, 14, 16, 16, 16, 31],
    'F': [31, 16, 14, 16, 16, 16, 16],
    'G': [14, 17, 16, 19, 17, 17, 14],
    'H': [17, 17, 17, 31, 17, 17, 17],
    'I': [14, 4, 4, 4, 4, 4, 14],
    'J': [7, 2, 2, 2, 2, 18, 12],
    'K': [17, 18, 20, 24, 20, 18, 17],
    'L': [16, 16, 16, 16, 16, 16, 31],
    'M': [17, 27, 21, 21, 17, 17, 17],
    'N': [17, 25, 21, 19, 17, 17, 17],
    'O': [14, 17, 17, 17, 17, 17, 14],
    'P': [30, 16, 16, 30, 16, 16, 16],
    'Q': [14, 17, 17, 17, 21, 18, 11],
    'R': [30, 16, 16, 30, 20, 18, 17],
    'S': [15, 16, 14, 2, 1, 17, 14],
    'T': [31, 4, 4, 4, 4, 4, 4],
    'U': [17, 17, 17, 17, 17, 17, 14],
    'V': [17, 17, 17, 17, 10, 4, 4],
    'W': [17, 17, 17, 21, 21, 21, 10],
    'X': [17, 17, 10, 4, 10, 17, 17],
    'Y': [17, 17, 10, 4, 4, 4, 4],
    'Z': [31, 1, 2, 4, 8, 16, 31],
    '+': [0, 4, 4, 31, 4, 4, 0],
    '-': [0, 0, 0, 31, 0, 0, 0],
    '*': [0, 9, 14, 31, 14, 9, 0],
    ' ': [0, 0, 0, 0, 0, 0, 0],
};
};

// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"unsloth/Qwen3.8-27B-GGUF","provider":"brigid","persona":"typescript","sessionId":"364c6c1f-4390-48a8-a16d-b9fa70862314","tokensIn":4415486,"tokensOut":171930,"tokensTotal":4587416,"cost":0,"turns":82,"toolCalls":118,"failedToolCalls":0,"timestamp":"2026-08-15T07:17:19.095Z"} -->