← DOOM results

DOOM

Qwen 3.7 Flash · 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 48,475 bytes · SHA-256 5e956ed6bb4e
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Crimson Depths</title>
<style>
/* ── css: layout ── */
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; background: #000; font-family: 'Courier New', monospace; }
#app { position: relative; width: 100%; height: 100%; }
#game { display: block; width: 100%; height: 100%; image-rendering: pixelated; image-rendering: crisp-edges; cursor: none; }
#crosshair {
  position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
  color: rgba(255,255,255,0.7); font-size: 28px; font-weight: bold; pointer-events: none; z-index: 10;
  text-shadow: 0 0 4px rgba(255,0,0,0.5);
}
#hud {
  position: absolute; bottom: 0; left: 0; right: 0; height: 90px;
  display: flex; align-items: center; justify-content: space-between;
  padding: 0 30px; background: linear-gradient(transparent 0%, rgba(0,0,0,0.85) 40%);
  color: #fff; font-family: 'Courier New', monospace; z-index: 5; pointer-events: none;
  border-top: 2px solid rgba(255,68,68,0.3);
}
#hud > div { min-width: 120px; }
#health-bar { 
  font-size: 22px; color: #ff4444; font-weight: bold; 
  text-shadow: 0 0 10px rgba(255,0,0,0.3);
}
#ammo-count { font-size: 32px; color: #ffaa00; font-weight: bold; text-align: center; text-shadow: 0 0 10px rgba(255,170,0,0.3); }
#weapon-name { font-size: 18px; color: #ccc; text-align: right; text-shadow: 0 0 5px rgba(255,255,255,0.2); }
#minimap-canvas { border: 1px solid rgba(255,255,255,0.15); background: rgba(0,0,0,0.6); image-rendering: pixelated; }
#message-overlay {
  position: absolute; top: 35%; left: 50%; transform: translate(-50%, -50%);
  color: #ffcc00; font-family: 'Courier New', monospace; font-size: 24px; font-weight: bold;
  text-shadow: 0 0 15px rgba(255,200,0,0.5); z-index: 15; pointer-events: none;
  opacity: 1; transition: opacity 0.3s;
}
#start-screen {
  position: absolute; inset: 0; display: flex; flex-direction: column;
  align-items: center; justify-content: center; background: radial-gradient(ellipse at center, #1a0505 0%, #000 70%);
  z-index: 25; cursor: pointer;
}
#start-screen h1 { 
  color: #ff3333; font-size: 56px; letter-spacing: 4px; 
  text-shadow: 0 0 30px rgba(255,0,0,0.5), 0 0 60px rgba(255,0,0,0.3);
  margin-bottom: 10px;
}
#start-screen p { color: #aa6666; font-size: 18px; margin: 5px 0; }
#start-screen .blink { animation: blink 1.5s ease-in-out infinite; margin-top: 30px; color: #ff8888; font-size: 20px; }
@keyframes blink { 0%,100% { opacity: 1; } 50% { opacity: 0.3; } }
#death-screen, #win-screen {
  position: absolute; inset: 0; display: flex; flex-direction: column;
  align-items: center; justify-content: center; z-index: 20;
}
#death-screen { background: rgba(100,0,0,0.8); }
#death-screen h1 { color: #ff0000; font-size: 72px; text-shadow: 0 0 30px rgba(255,0,0,0.5); margin-bottom: 10px; }
#win-screen { background: rgba(0,50,0,0.85); }
#win-screen h1 { color: #44ff44; font-size: 52px; text-shadow: 0 0 30px rgba(0,255,0,0.5); margin-bottom: 10px; }
#win-screen button {
  margin-top: 20px; padding: 14px 40px; font-family: 'Courier New', monospace; font-size: 20px;
  background: #44ff44; color: #000; border: none; cursor: pointer; pointer-events: auto;
  font-weight: bold; letter-spacing: 2px;
}
#win-screen button:hover { background: #66ff66; box-shadow: 0 0 20px rgba(0,255,0,0.5); }
.hidden { display: none !important; }
</style>
</head>
<body>
<div id="app">
  <canvas id="game"></canvas>
  <div id="hud">
    <div id="health-bar"><span id="health-val">100</span>%</div>
    <div id="ammo-count">20</div>
    <div id="weapon-name">Pistol</div>
    <canvas id="minimap-canvas" width="128" height="128"></canvas>
    <div id="message-overlay" class="hidden"></div>
  </div>
  <div id="death-screen" class="hidden">
    <h1>YOU DIED</h1>
    <p>Click or press Space to respawn</p>
  </div>
  <div id="win-screen" class="hidden">
    <h1>LEVEL COMPLETE!</h1>
    <p>You escaped the Crimson Depths.</p>
    <button id="restart-btn">Play Again</button>
  </div>
  <div id="crosshair">+</div>
<div id="start-screen">
  <h1>CRIMSON DEPTHS</h1>
  <p>WASD - Move | Mouse - Look | Click/Space - Shoot &amp; Switch Weapon</p>
  <p>R - Restart | Find the yellow exit door to escape!</p>
  <div class="blink">[ CLICK TO ENTER ]</div>
</div></div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./game":"src/game.ts"},"src/game.ts":{"./map":"src/map.ts","./audio":"src/audio.ts","./input":"src/input.ts","./weapons":"src/weapons.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";
// Entry point. Runs after the DOM is parsed and after any classic
// <script> imports, so document queries and CDN globals are available.
// Import your other modules from here.
Object.defineProperty(exports, "__esModule", { value: true });
exports.start = start;
const game_1 = require("./game");
function start() {
    (0, game_1.startGame)();
}
start();
};

// ── module: src/map.ts ──
__mods["src/map.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.WALL_COLORS = exports.WORLD_MAP = exports.MAP_HEIGHT = exports.MAP_WIDTH = void 0;
exports.getEntities = getEntities;
exports.getPlayerStart = getPlayerStart;
exports.isWall = isWall;
exports.isExit = isExit;
// Map definition - 2D grid
// 0=empty, 1-6=wall types (different colors), 7=exit door
exports.MAP_WIDTH = 32;
exports.MAP_HEIGHT = 32;
exports.WORLD_MAP = [
    [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 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, 0, 0, 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, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
    [1, 0, 0, 2, 2, 2, 0, 0, 0, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 4, 4, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0, 1],
    [1, 0, 0, 2, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 5, 5, 5, 0, 0, 1],
    [1, 0, 0, 2, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 1],
    [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 1],
    [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 6, 0, 0, 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, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 5, 5, 0, 0, 0, 0, 0, 0, 0, 1],
    [1, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 1],
    [1, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 6, 6, 6, 0, 0, 1],
    [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 0, 1],
    [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 1],
    [1, 0, 0, 0, 0, 0, 0, 0, 4, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
    [1, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 3, 3, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
    [1, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0, 1],
    [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 1],
    [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 6, 0, 0, 1],
    [1, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 1],
    [1, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 1],
    [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
    [1, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
    [1, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
    [1, 0, 0, 0, 0, 0, 0, 0, 5, 5, 0, 0, 0, 0, 0, 0, 0, 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, 0, 0, 0, 0, 4, 4, 4, 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, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 1],
    [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 1],
    [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 1],
    [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 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, 0, 0, 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, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
    [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
];
exports.WALL_COLORS = {
    1: [120, 40, 40], // Red brick
    2: [60, 60, 120], // Blue stone
    3: [80, 100, 50], // Green mossy
    4: [140, 100, 30], // Brown wood
    5: [100, 50, 100], // Purple dark
    6: [50, 120, 120], // Cyan tech
};
function getEntities() {
    return [
        // Room 1 area
        { type: 'enemy', subtype: 'demon', x: 5.5, y: 5.5, hp: 30, maxHp: 30, speed: 0.015, damage: 8 },
        { type: 'enemy', subtype: 'demon', x: 10.5, y: 3.5, hp: 30, maxHp: 30, speed: 0.015, damage: 8 },
        // Corridor enemies
        { type: 'enemy', subtype: 'soldier', x: 15.5, y: 9.5, hp: 20, maxHp: 20, speed: 0.02, damage: 12 },
        { type: 'enemy', subtype: 'soldier', x: 18.5, y: 8.5, hp: 20, maxHp: 20, speed: 0.02, damage: 12 },
        { type: 'enemy', subtype: 'demon', x: 22.5, y: 15.5, hp: 40, maxHp: 40, speed: 0.012, damage: 10 },
        // Room 4
        { type: 'enemy', subtype: 'soldier', x: 28.5, y: 18.5, hp: 20, maxHp: 20, speed: 0.02, damage: 12 },
        { type: 'enemy', subtype: 'soldier', x: 25.5, y: 22.5, hp: 20, maxHp: 20, speed: 0.02, damage: 12 },
        // Boss room area
        { type: 'enemy', subtype: 'demon', x: 20.5, y: 25.5, hp: 50, maxHp: 50, speed: 0.01, damage: 15 },
        { type: 'enemy', subtype: 'demon', x: 28.5, y: 28.5, hp: 30, maxHp: 30, speed: 0.015, damage: 10 },
        { type: 'enemy', subtype: 'soldier', x: 14.5, y: 27.5, hp: 20, maxHp: 20, speed: 0.02, damage: 12 },
        // More scattered
        { type: 'enemy', subtype: 'soldier', x: 8.5, y: 20.5, hp: 20, maxHp: 20, speed: 0.02, damage: 10 },
        { type: 'enemy', subtype: 'demon', x: 26.5, y: 10.5, hp: 35, maxHp: 35, speed: 0.014, damage: 9 },
        { type: 'enemy', subtype: 'soldier', x: 3.5, y: 25.5, hp: 20, maxHp: 20, speed: 0.02, damage: 12 },
        { type: 'enemy', subtype: 'demon', x: 12.5, y: 29.5, hp: 30, maxHp: 30, speed: 0.015, damage: 8 },
        // Health pickups
        { type: 'pickup', subtype: 'medkit', x: 3.5, y: 2.5, pickupType: 'health', amount: 25 },
        { type: 'pickup', subtype: 'medkit', x: 8.5, y: 7.5, pickupType: 'health', amount: 25 },
        { type: 'pickup', subtype: 'medkit', x: 17.5, y: 13.5, pickupType: 'health', amount: 50 },
        { type: 'pickup', subtype: 'medkit', x: 25.5, y: 12.5, pickupType: 'health', amount: 30 },
        { type: 'pickup', subtype: 'medkit', x: 29.5, y: 23.5, pickupType: 'health', amount: 50 },
        { type: 'pickup', subtype: 'medkit', x: 15.5, y: 25.5, pickupType: 'health', amount: 25 },
        // Ammo pickups
        { type: 'pickup', subtype: 'shell', x: 6.5, y: 4.5, pickupType: 'ammo', amount: 10 },
        { type: 'pickup', subtype: 'shell', x: 13.5, y: 5.5, pickupType: 'ammo', amount: 15 },
        { type: 'pickup', subtype: 'shell', x: 20.5, y: 10.5, pickupType: 'ammo', amount: 10 },
        { type: 'pickup', subtype: 'shell', x: 27.5, y: 16.5, pickupType: 'ammo', amount: 20 },
        { type: 'pickup', subtype: 'shell', x: 3.5, y: 15.5, pickupType: 'ammo', amount: 15 },
        { type: 'pickup', subtype: 'shell', x: 22.5, y: 28.5, pickupType: 'ammo', amount: 10 },
    ];
}
function getPlayerStart() {
    return { x: 2.5, y: 2.5 };
}
function isWall(x, y) {
    const ix = Math.floor(x);
    const iy = Math.floor(y);
    if (ix < 0 || ix >= exports.MAP_WIDTH || iy < 0 || iy >= exports.MAP_HEIGHT)
        return true;
    return exports.WORLD_MAP[iy][ix] !== 0;
}
function isExit(x, y) {
    const ix = Math.floor(x);
    const iy = Math.floor(y);
    if (ix < 0 || ix >= exports.MAP_WIDTH || iy < 0 || iy >= exports.MAP_HEIGHT)
        return false;
    return exports.WORLD_MAP[iy][ix] === 7;
}
};

// ── module: src/audio.ts ──
__mods["src/audio.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.playSound = playSound;
exports.playAmbient = playAmbient;
// Simple Web Audio API sound effects - no external assets needed
const ctx = new (window.AudioContext || window.webkitAudioContext)();
function playSound(type) {
    if (ctx.state === 'suspended')
        ctx.resume();
    const t = ctx.currentTime;
    switch (type) {
        case 'pistol': {
            const o = ctx.createOscillator();
            const g = ctx.createGain();
            o.type = 'sawtooth';
            o.frequency.setValueAtTime(300, t);
            o.frequency.exponentialRampToValueAtTime(80, t + 0.15);
            g.gain.setValueAtTime(0.25, t);
            g.gain.exponentialRampToValueAtTime(0.001, t + 0.15);
            o.connect(g).connect(ctx.destination);
            o.start(t);
            o.stop(t + 0.15);
            break;
        }
        case 'shotgun': {
            // Noise burst for shotgun
            const bufSize = ctx.sampleRate * 0.3;
            const buf = ctx.createBuffer(1, bufSize, ctx.sampleRate);
            const data = buf.getChannelData(0);
            for (let i = 0; i < bufSize; i++)
                data[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / bufSize, 2);
            const src = ctx.createBufferSource();
            const g = ctx.createGain();
            src.buffer = buf;
            g.gain.setValueAtTime(0.4, t);
            g.gain.exponentialRampToValueAtTime(0.001, t + 0.3);
            src.connect(g).connect(ctx.destination);
            src.start(t);
            // Low boom
            const o = ctx.createOscillator();
            const g2 = ctx.createGain();
            o.type = 'sine';
            o.frequency.setValueAtTime(150, t);
            o.frequency.exponentialRampToValueAtTime(40, t + 0.2);
            g2.gain.setValueAtTime(0.3, t);
            g2.gain.exponentialRampToValueAtTime(0.001, t + 0.2);
            o.connect(g2).connect(ctx.destination);
            o.start(t);
            o.stop(t + 0.2);
            break;
        }
        case 'machinegun': {
            const o = ctx.createOscillator();
            const g = ctx.createGain();
            o.type = 'square';
            o.frequency.setValueAtTime(400, t);
            o.frequency.exponentialRampToValueAtTime(150, t + 0.06);
            g.gain.setValueAtTime(0.12, t);
            g.gain.exponentialRampToValueAtTime(0.001, t + 0.06);
            o.connect(g).connect(ctx.destination);
            o.start(t);
            o.stop(t + 0.06);
            break;
        }
        case 'hit': {
            const o = ctx.createOscillator();
            const g = ctx.createGain();
            o.type = 'sine';
            o.frequency.setValueAtTime(200, t);
            o.frequency.exponentialRampToValueAtTime(100, t + 0.1);
            g.gain.setValueAtTime(0.2, t);
            g.gain.exponentialRampToValueAtTime(0.001, t + 0.1);
            o.connect(g).connect(ctx.destination);
            o.start(t);
            o.stop(t + 0.1);
            break;
        }
        case 'pickup': {
            const o = ctx.createOscillator();
            const g = ctx.createGain();
            o.type = 'sine';
            o.frequency.setValueAtTime(440, t);
            o.frequency.linearRampToValueAtTime(880, t + 0.15);
            g.gain.setValueAtTime(0.2, t);
            g.gain.exponentialRampToValueAtTime(0.001, t + 0.2);
            o.connect(g).connect(ctx.destination);
            o.start(t);
            o.stop(t + 0.2);
            break;
        }
        case 'enemy_die': {
            const o = ctx.createOscillator();
            const g = ctx.createGain();
            o.type = 'sawtooth';
            o.frequency.setValueAtTime(400, t);
            o.frequency.exponentialRampToValueAtTime(50, t + 0.4);
            g.gain.setValueAtTime(0.25, t);
            g.gain.exponentialRampToValueAtTime(0.001, t + 0.4);
            o.connect(g).connect(ctx.destination);
            o.start(t);
            o.stop(t + 0.4);
            break;
        }
        case 'hurt': {
            const o = ctx.createOscillator();
            const g = ctx.createGain();
            o.type = 'sawtooth';
            o.frequency.setValueAtTime(150, t);
            o.frequency.linearRampToValueAtTime(80, t + 0.2);
            g.gain.setValueAtTime(0.3, t);
            g.gain.exponentialRampToValueAtTime(0.001, t + 0.2);
            o.connect(g).connect(ctx.destination);
            o.start(t);
            o.stop(t + 0.2);
            break;
        }
        case 'exit': {
            for (let i = 0; i < 3; i++) {
                const o = ctx.createOscillator();
                const g = ctx.createGain();
                o.type = 'sine';
                o.frequency.setValueAtTime(440 * Math.pow(1.26, i), t + i * 0.15);
                g.gain.setValueAtTime(0.2, t + i * 0.15);
                g.gain.exponentialRampToValueAtTime(0.001, t + i * 0.15 + 0.3);
                o.connect(g).connect(ctx.destination);
                o.start(t + i * 0.15);
                o.stop(t + i * 0.15 + 0.3);
            }
            break;
        }
        case 'empty': {
            const o = ctx.createOscillator();
            const g = ctx.createGain();
            o.type = 'triangle';
            o.frequency.setValueAtTime(800, t);
            g.gain.setValueAtTime(0.05, t);
            g.gain.exponentialRampToValueAtTime(0.001, t + 0.05);
            o.connect(g).connect(ctx.destination);
            o.start(t);
            o.stop(t + 0.05);
            break;
        }
    }
}
function playAmbient() {
    // Subtle low drone
    const o = ctx.createOscillator();
    const g = ctx.createGain();
    o.type = 'sine';
    o.frequency.value = 55;
    g.gain.value = 0.03;
    o.connect(g).connect(ctx.destination);
    o.start();
}
};

// ── module: src/input.ts ──
__mods["src/input.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.initInput = initInput;
exports.getInput = getInput;
exports.resetWeaponCycle = resetWeaponCycle;
exports.getMouseDelta = getMouseDelta;
exports.consumeMouseY = consumeMouseY;
exports.setMouseClamp = setMouseClamp;
const keys = new Map();
let mouseX = 0;
let mouseY = 0;
let pointerLocked = false;
function initInput(canvas) {
    document.addEventListener('keydown', (e) => {
        keys.set(e.code, true);
        // Prevent scrolling with space/arrows
        if (['Space', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.code))
            e.preventDefault();
    });
    document.addEventListener('keyup', (e) => {
        keys.set(e.code, false);
    });
    canvas.addEventListener('click', () => {
        if (!pointerLocked) {
            canvas.requestPointerLock();
        }
    });
    document.addEventListener('pointerlockchange', () => {
        pointerLocked = document.pointerLockElement === canvas;
    });
    document.addEventListener('mousemove', (e) => {
        if (pointerLocked) {
            mouseX += e.movementX;
            mouseY += e.movementY;
        }
    });
}
function getInput() {
    const state = {
        forward: keys.get('KeyW') === true || keys.get('ArrowUp') === true,
        backward: keys.get('KeyS') === true || keys.get('ArrowDown') === true,
        left: keys.get('KeyA') === true || keys.get('ArrowLeft') === true,
        right: keys.get('KeyD') === true || keys.get('ArrowRight') === true,
        strafeLeft: keys.get('KeyQ') === true,
        strafeRight: keys.get('KeyE') === true,
        shoot: keys.get('Space') === true,
        weaponCycle: keys.has('KeyR'),
        prevWeaponCycle: false,
    };
    return state;
}
function resetWeaponCycle() { }
function getMouseDelta() {
    const val = mouseX;
    mouseX = 0;
    return val;
}
function consumeMouseY() {
    const val = mouseY;
    mouseY = Math.max(-200, Math.min(200, val)); // clamp look up/down
    return val;
}
function setMouseClamp(val) { mouseY = val; }
};

// ── module: src/weapons.ts ──
__mods["src/weapons.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Weapon = exports.WEAPONS = exports.WEAPON_NAMES = void 0;
exports.WEAPON_NAMES = ['Pistol', 'Shotgun', 'Machine Gun'];
exports.WEAPONS = [
    { name: 'Pistol', damage: 15, fireRate: 400, ammoCost: 1, spread: 0.02, pellets: 1, reloadTime: 800 },
    { name: 'Shotgun', damage: 10, fireRate: 700, ammoCost: 1, spread: 0.15, pellets: 5, reloadTime: 1200 },
    { name: 'Machine Gun', damage: 8, fireRate: 90, ammoCost: 1, spread: 0.06, pellets: 1, reloadTime: 600 },
];
class Weapon {
    constructor() {
        this.currentIdx = 0;
        this.lastFire = 0;
        this.reloading = false;
        this.reloadStart = 0;
        this.muzzleFlash = 0;
    }
    get current() { return exports.WEAPONS[this.currentIdx]; }
    switchNext() {
        this.currentIdx = (this.currentIdx + 1) % exports.WEAPONS.length;
        this.reloading = false;
    }
    tryFire(now, ammo) {
        if (this.reloading)
            return false;
        if (ammo < this.current.ammoCost)
            return false;
        if (now - this.lastFire < this.current.fireRate)
            return false;
        this.lastFire = now;
        this.muzzleFlash = 6;
        return true;
    }
    update(now) {
        if (this.muzzleFlash > 0)
            this.muzzleFlash--;
        if (this.reloading && now - this.reloadStart >= this.current.reloadTime) {
            this.reloading = false;
        }
    }
}
exports.Weapon = Weapon;
};

// ── module: src/game.ts ──
__mods["src/game.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.startGame = startGame;
const map_1 = require("./map");
const audio_1 = require("./audio");
const input_1 = require("./input");
const weapons_1 = require("./weapons");
const MOVE_SPEED = 0.04;
const STRAFE_SPEED = 0.03;
const ROT_SPEED = 0.0035;
const CANVAS_W = 640;
const CANVAS_H = 360;
const FOV = Math.PI / 3;
const MAX_DEPTH = 20;
var GameState;
(function (GameState) {
    GameState[GameState["PLAYING"] = 0] = "PLAYING";
    GameState[GameState["DEAD"] = 1] = "DEAD";
    GameState[GameState["WIN"] = 2] = "WIN";
})(GameState || (GameState = {}));
let zBuffer = new Float32Array(CANVAS_W);
let playerState = null;
let gameState = GameState.PLAYING;
function startGame() {
    const canvas = document.getElementById('game');
    canvas.width = CANVAS_W;
    canvas.height = CANVAS_H;
    const ctx = canvas.getContext('2d', { alpha: false });
    const imgData = ctx.createImageData(CANVAS_W, CANVAS_H);
    // Start screen
    const startScreen = document.getElementById('start-screen');
    if (startScreen)
        startScreen.style.display = 'flex';
    (0, input_1.initInput)(canvas);
    // Hide start screen on click
    const onClickStart = () => {
        if (startScreen)
            startScreen.style.display = 'none';
        window.removeEventListener('click', onClickStart);
    };
    window.addEventListener('click', onClickStart);
    let entities = resetEntities();
    let pickupSprites = [];
    let enemySprites = [];
    let msgTimer = 0;
    document.getElementById('death-screen').classList.add('hidden');
    document.getElementById('win-screen').classList.add('hidden');
    const restartHandler = () => {
        if (gameState === GameState.DEAD || gameState === GameState.WIN) {
            resetLevel(entities);
        }
    };
    document.getElementById('restart-btn').addEventListener('click', restartHandler);
    // Also allow any key/click to respawn after death
    window.addEventListener('keydown', (e) => {
        if ((gameState === GameState.DEAD || gameState === GameState.WIN) && e.code !== 'ControlLeft' && e.code !== 'AltLeft') {
            restartHandler();
        }
    });
    canvas.addEventListener('click', () => {
        if (gameState === GameState.DEAD || gameState === GameState.WIN) {
            restartHandler();
        }
    });
    function resetEntities() {
        return (0, map_1.getEntities)().map(e => ({ ...e }));
    }
    function resetLevel(entities) {
        const s = (0, map_1.getPlayerStart)();
        playerState = {
            x: s.x, y: s.y, angle: 0,
            health: 100, maxHealth: 100, ammo: 30,
            weapon: new weapons_1.Weapon(), bobPhase: 0, damageFlash: 0, lastShoot: false, prevWeaponCycle: false,
        };
        for (const e of entities) {
            if (e.type === 'enemy') {
                e.hp = e.maxHp;
                e.alive = true;
            }
            if (e.type === 'pickup')
                e.collected = false;
        }
        gameState = GameState.PLAYING;
        document.getElementById('death-screen').classList.add('hidden');
        document.getElementById('win-screen').classList.add('hidden');
    }
    resetLevel(entities);
    function canMove(nx, ny) {
        const r = 0.2;
        return !(0, map_1.isWall)(nx - r, ny - r) && !(0, map_1.isWall)(nx + r, ny - r) &&
            !(0, map_1.isWall)(nx - r, ny + r) && !(0, map_1.isWall)(nx + r, ny + r);
    }
    function rebuildSprites() {
        pickupSprites.length = 0;
        enemySprites.length = 0;
        for (const e of entities) {
            if (e.type === 'pickup' && !e.collected)
                pickupSprites.push({ x: e.x, y: e.y, entity: e });
            if (e.type === 'enemy' && e.alive)
                enemySprites.push({ x: e.x, y: e.y, entity: e });
        }
    }
    rebuildSprites();
    function sortSprites(sprites) {
        const p = playerState;
        const arr = sprites.map(s => ({ s, d: (s.x - p.x) ** 2 + (s.y - p.y) ** 2 }));
        arr.sort((a, b) => b.d - a.d);
        return arr.map(a => a.s);
    }
    function showMessage(text) {
        const el = document.getElementById('message-overlay');
        el.textContent = text;
        el.classList.remove('hidden');
        msgTimer = 2000;
    }
    function hideMessage() {
        document.getElementById('message-overlay').classList.add('hidden');
    }
    function castWallRay(px, py, rayAngle, zBuf, x) {
        const cosA = Math.cos(rayAngle);
        const sinA = Math.sin(rayAngle);
        const dcx = cosA || 1e-10;
        const dcy = sinA || 1e-10;
        let mapX = Math.floor(px);
        let mapY = Math.floor(py);
        const ddx = Math.abs(1 / dcx);
        const ddy = Math.abs(1 / dcy);
        let stepX, stepY;
        let sdX, sdY;
        let side = 0;
        if (dcx < 0) {
            stepX = -1;
            sdX = (px - mapX) * ddx;
        }
        else {
            stepX = 1;
            sdX = (mapX + 1 - px) * ddx;
        }
        if (dcy < 0) {
            stepY = -1;
            sdY = (py - mapY) * ddy;
        }
        else {
            stepY = 1;
            sdY = (mapY + 1 - py) * ddy;
        }
        for (let i = 0; i < 64; i++) {
            if (sdX < sdY) {
                sdX += ddx;
                mapX += stepX;
                side = 0;
            }
            else {
                sdY += ddy;
                mapY += stepY;
                side = 1;
            }
            if (mapX >= 0 && mapX < map_1.MAP_WIDTH && mapY >= 0 && mapY < map_1.MAP_HEIGHT) {
                const wt = map_1.WORLD_MAP[mapY][mapX];
                if (wt > 0) {
                    let pd;
                    if (side === 0)
                        pd = (mapX - px + (1 - stepX) / 2) / dcx;
                    else
                        pd = (mapY - py + (1 - stepY) / 2) / dcy;
                    zBuf[x] = pd;
                    return { hit: true, perpDist: pd, wallType: wt, side };
                }
            }
            else
                break;
        }
        zBuf[x] = MAX_DEPTH;
        return { hit: false, perpDist: MAX_DEPTH, wallType: 0, side: 0 };
    }
    function drawSprite(ctx, sp, colorR, colorG, colorB, zBuf, halfH) {
        const p = playerState;
        const dx = sp.x - p.x;
        const dy = sp.y - p.y;
        const dist = Math.sqrt(dx * dx + dy * dy);
        if (dist < 0.2 || dist > MAX_DEPTH)
            return;
        let spriteAngle = Math.atan2(dy, dx) - p.angle;
        while (spriteAngle > Math.PI)
            spriteAngle -= 2 * Math.PI;
        while (spriteAngle < -Math.PI)
            spriteAngle += 2 * Math.PI;
        if (Math.abs(spriteAngle) > FOV * 0.7)
            return;
        const screenX = (0.5 + spriteAngle / FOV) * CANVAS_W;
        const spriteSize = Math.floor(CANVAS_H / dist);
        if (spriteSize < 6)
            return;
        const xStart = Math.floor(screenX - spriteSize / 2);
        const yStart = Math.floor(halfH - spriteSize / 2);
        // Z-buffer check at center column
        const colIdx = Math.max(0, Math.min(CANVAS_W - 1, Math.floor(screenX)));
        if (dist >= zBuf[colIdx])
            return;
        const fog = Math.max(0.2, 1 - dist / MAX_DEPTH);
        const r = Math.floor(colorR * fog);
        const g = Math.floor(colorG * fog);
        const b = Math.floor(colorB * fog);
        ctx.save();
        if (sp.entity.pickupType === 'health') {
            // Health medkit - glowing green box
            const s = spriteSize * 0.4;
            const ox = screenX - s / 2, oy = yStart + spriteSize * 0.5;
            ctx.fillStyle = `rgba(0,${200 * fog | 0},0,${fog})`;
            ctx.fillRect(ox, oy, s, s * 0.7);
            ctx.fillStyle = `rgba(255,255,255,${0.6 * fog})`;
            ctx.fillRect(ox + s * 0.4, oy + s * 0.15, s * 0.2, s * 0.4);
            ctx.fillRect(ox + s * 0.15, oy + s * 0.3, s * 0.5, s * 0.1);
        }
        else if (sp.entity.pickupType === 'ammo') {
            // Ammo shell - orange cylinder
            const s = spriteSize * 0.3;
            const ox = screenX - s / 2, oy = yStart + spriteSize * 0.5;
            ctx.fillStyle = `rgba(${200 * fog | 0},${150 * fog | 0},0,${fog})`;
            ctx.beginPath();
            ctx.ellipse(screenX, oy + s * 0.5, s * 0.5, s * 0.4, 0, 0, Math.PI * 2);
            ctx.fill();
            ctx.strokeStyle = `rgba(255,200,0,${0.5 * fog})`;
            ctx.lineWidth = 1;
            ctx.stroke();
        }
        else {
            // Enemy - detailed figure
            const bw = spriteSize * 0.5;
            const bh = spriteSize * 0.35;
            const headS = spriteSize * 0.28;
            const legW = Math.max(2, spriteSize * 0.08);
            // Shadow
            ctx.fillStyle = `rgba(0,0,0,${0.3 * fog})`;
            ctx.beginPath();
            ctx.ellipse(screenX, yStart + spriteSize, spriteSize * 0.15, spriteSize * 0.04, 0, 0, Math.PI * 2);
            ctx.fill();
            // Legs
            ctx.fillStyle = `rgb(${Math.floor(r * 0.5)},${Math.floor(g * 0.5)},${Math.floor(b * 0.5)})`;
            ctx.fillRect(xStart + spriteSize * 0.35, yStart + spriteSize * 0.6, legW, spriteSize * 0.35);
            ctx.fillRect(xStart + spriteSize * 0.58, yStart + spriteSize * 0.6, legW, spriteSize * 0.35);
            // Body
            ctx.fillStyle = `rgb(${r},${g},${b})`;
            ctx.fillRect(xStart + (spriteSize - bw) / 2, yStart + spriteSize * 0.25, bw, bh);
            // Shoulders/pads
            ctx.fillStyle = `rgb(${Math.min(255, r + 20)},${Math.min(255, g + 10)},${Math.min(255, b + 10)})`;
            ctx.fillRect(xStart + (spriteSize - bw) / 2 - 2, yStart + spriteSize * 0.25, 8, 6);
            ctx.fillRect(xStart + spriteSize - (spriteSize - bw) / 2 - 6, yStart + spriteSize * 0.25, 8, 6);
            // Head
            ctx.fillStyle = `rgb(${Math.min(255, r + 50)},${Math.max(0, g - 30)},${Math.max(0, b - 20)})`;
            ctx.beginPath();
            ctx.arc(screenX, yStart + spriteSize * 0.18, headS / 2, 0, Math.PI * 2);
            ctx.fill();
            // Glowing eyes
            ctx.fillStyle = `rgba(255,${80 * fog | 0},0,${fog})`;
            ctx.beginPath();
            ctx.arc(xStart + spriteSize * 0.38, yStart + spriteSize * 0.16, Math.max(1, headS * 0.15), 0, Math.PI * 2);
            ctx.arc(xStart + spriteSize * 0.62, yStart + spriteSize * 0.16, Math.max(1, headS * 0.15), 0, Math.PI * 2);
            ctx.fill();
            // Mouth (growl)
            ctx.strokeStyle = `rgba(200,0,0,${0.7 * fog})`;
            ctx.lineWidth = Math.max(1, spriteSize * 0.015);
            ctx.beginPath();
            ctx.arc(screenX, yStart + spriteSize * 0.24, headS * 0.18, 0.1, Math.PI - 0.1);
            ctx.stroke();
            // Weapon/arm visible
            ctx.fillStyle = `rgb(${Math.floor(r * 0.7)},${Math.floor(g * 0.7)},${Math.floor(b * 0.7)})`;
            ctx.fillRect(xStart + spriteSize * 0.7, yStart + spriteSize * 0.35, spriteSize * 0.12, 3);
        }
        ctx.restore();
    }
    function updateEnemies(dt) {
        for (const e of entities) {
            if (e.type !== 'enemy' || !e.alive)
                continue;
            const p = playerState;
            const dx = p.x - e.x, dy = p.y - e.y;
            const dist = Math.sqrt(dx * dx + dy * dy);
            if (dist > 1.2 && dist < 14) {
                const spd = (e.speed || 0.012) * Math.max(1, dt / 16);
                const mx = (dx / dist) * spd;
                const my = (dy / dist) * spd;
                if (canMove(e.x + mx, e.y))
                    e.x += mx;
                if (canMove(e.x, e.y + my))
                    e.y += my;
            }
            if (dist < 1.1) {
                const now = performance.now();
                const cd = (e.lastAttack || 0) + (e.attackCooldown || 1000);
                if (now > cd) {
                    p.health -= e.damage || 5;
                    e.lastAttack = now;
                    p.damageFlash = 12;
                    (0, audio_1.playSound)('hurt');
                    if (p.health <= 0) {
                        p.health = 0;
                        gameState = GameState.DEAD;
                        document.getElementById('death-screen').classList.remove('hidden');
                    }
                }
            }
        }
    }
    function checkPickups() {
        for (const e of entities) {
            if (e.type !== 'pickup' || e.collected)
                continue;
            const p = playerState;
            const dx = p.x - e.x, dy = p.y - e.y;
            if (dx * dx + dy * dy < 0.25) {
                e.collected = true;
                (0, audio_1.playSound)('pickup');
                if (e.pickupType === 'health') {
                    p.health = Math.min(p.maxHealth, p.health + (e.amount || 10));
                    showMessage('+' + (e.amount || 10) + ' Health');
                }
                else if (e.pickupType === 'ammo') {
                    p.ammo += (e.amount || 5);
                    showMessage('+' + (e.amount || 5) + ' Ammo');
                }
            }
        }
    }
    function shootWeapon(now) {
        const p = playerState;
        const w = p.weapon;
        if (!w.tryFire(now, p.ammo))
            return;
        const def = w.current;
        p.ammo -= def.ammoCost;
        (0, audio_1.playSound)(def.name === 'Pistol' ? 'pistol' : def.name === 'Shotgun' ? 'shotgun' : 'machinegun');
        for (let pellet = 0; pellet < def.pellets; pellet++) {
            const spread = (Math.random() - 0.5) * def.spread;
            const ra = p.angle + spread;
            const dirX = Math.cos(ra), dirY = Math.sin(ra);
            // Raycast to find closest enemy in direction
            let bestEnemy = null;
            let bestDist = Infinity;
            for (const ent of entities) {
                if (ent.type !== 'enemy' || !ent.alive)
                    continue;
                const edx = ent.x - p.x, edy = ent.y - p.y;
                const eDist = Math.sqrt(edx * edx + edy * edy);
                if (eDist > bestDist || eDist > MAX_DEPTH)
                    continue;
                const dot = (edx * dirX + edy * dirY) / eDist;
                if (dot < 0.9)
                    continue;
                // Wall check
                let blocked = false;
                for (let t = 0; t < eDist; t += 0.05) {
                    if ((0, map_1.isWall)(p.x + dirX * t, p.y + dirY * t)) {
                        blocked = true;
                        break;
                    }
                }
                if (blocked)
                    continue;
                // Hit radius
                const perpDist = Math.abs(edy * dirX - edx * dirY);
                if (perpDist < 0.35 && eDist < bestDist) {
                    bestDist = eDist;
                    bestEnemy = ent;
                }
            }
            if (bestEnemy) {
                bestEnemy.hp = (bestEnemy.hp || 0) - def.damage;
                (0, audio_1.playSound)('hit');
                if ((bestEnemy.hp || 0) <= 0) {
                    bestEnemy.alive = false;
                    (0, audio_1.playSound)('enemy_die');
                    rebuildSprites();
                }
            }
        }
    }
    function renderMinimap() {
        const el = document.getElementById('minimap-canvas');
        if (!el)
            return;
        const mctx = el.getContext('2d');
        const ms = 3.8;
        mctx.fillStyle = '#000';
        mctx.fillRect(0, 0, 128, 128);
        for (let y = 0; y < map_1.MAP_HEIGHT; y++) {
            for (let x = 0; x < map_1.MAP_WIDTH; x++) {
                if (map_1.WORLD_MAP[y][x] > 0) {
                    const c = map_1.WALL_COLORS[map_1.WORLD_MAP[y][x]] || [128, 128, 128];
                    mctx.fillStyle = `rgb(${c[0]},${c[1]},${c[2]})`;
                    mctx.fillRect(x * ms, y * ms, ms, ms);
                }
            }
        }
        // Enemies
        for (const e of entities) {
            if (e.type !== 'enemy' || !e.alive)
                continue;
            mctx.fillStyle = '#f00';
            mctx.fillRect(e.x * ms - 1, e.y * ms - 1, 3, 3);
        }
        // Pickups
        for (const e of entities) {
            if (e.type !== 'pickup' || e.collected)
                continue;
            mctx.fillStyle = e.pickupType === 'health' ? '#0f0' : '#fa0';
            mctx.fillRect(e.x * ms - 1, e.y * ms - 1, 2, 2);
        }
        // Player
        const p = playerState;
        mctx.fillStyle = '#fff';
        mctx.beginPath();
        mctx.arc(p.x * ms, p.y * ms, 2.5, 0, Math.PI * 2);
        mctx.fill();
        mctx.strokeStyle = '#ff0';
        mctx.lineWidth = 1;
        mctx.beginPath();
        mctx.moveTo(p.x * ms, p.y * ms);
        mctx.lineTo((p.x + Math.cos(p.angle) * 2) * ms, (p.y + Math.sin(p.angle) * 2) * ms);
        mctx.stroke();
    }
    function renderWeaponView(ctx) {
        const bob = Math.sin(playerState.bobPhase) * 4;
        const wepIdx = playerState.weapon.currentIdx;
        const ox = CANVAS_W / 2 + bob;
        const oy = CANVAS_H + bob * 0.5;
        ctx.save();
        ctx.translate(ox, oy);
        switch (wepIdx) {
            case 0: // Pistol
                ctx.fillStyle = '#666';
                ctx.fillRect(-6, -55, 12, 45);
                ctx.fillStyle = '#444';
                ctx.fillRect(-4, -65, 8, 14);
                ctx.fillStyle = '#555';
                ctx.fillRect(-5, -15, 10, 12);
                break;
            case 1: // Shotgun
                ctx.fillStyle = '#654';
                ctx.fillRect(-10, -75, 20, 55);
                ctx.fillStyle = '#543';
                ctx.fillRect(-7, -88, 14, 18);
                ctx.fillStyle = '#888';
                ctx.fillRect(-3, -94, 6, 10);
                break;
            default: // Machine Gun
                ctx.fillStyle = '#567';
                ctx.fillRect(-8, -65, 16, 50);
                ctx.fillStyle = '#456';
                ctx.fillRect(-12, -75, 24, 14);
                ctx.fillStyle = '#777';
                ctx.fillRect(-2, -84, 4, 12);
                ctx.fillStyle = '#444';
                ctx.fillRect(-5, -20, 10, 18);
                break;
        }
        // Muzzle flash
        if (playerState.weapon.muzzleFlash > 0) {
            const fi = playerState.weapon.muzzleFlash / 6;
            const g = ctx.createRadialGradient(0, -95, 0, 0, -95, 40);
            g.addColorStop(0, `rgba(255,220,80,${fi})`);
            g.addColorStop(1, 'rgba(255,100,0,0)');
            ctx.fillStyle = g;
            ctx.fillRect(-40, -135, 80, 60);
        }
        ctx.restore();
    }
    function updateHUD() {
        const p = playerState;
        const hv = document.getElementById('health-val');
        hv.textContent = String(Math.ceil(p.health));
        hv.style.color = p.health > 60 ? '#f44' : p.health > 30 ? '#fa0' : '#f00';
        document.getElementById('ammo-count').textContent = String(p.ammo);
        document.getElementById('weapon-name').textContent = weapons_1.WEAPON_NAMES[p.weapon.currentIdx];
    }
    function frameLoop(time) {
        requestAnimationFrame(frameLoop);
        if (!playerState)
            return;
        const dt = Math.min(50, time - frameLastTime);
        frameLastTime = time;
        // Dead state
        if (gameState === GameState.DEAD)
            return;
        if (gameState === GameState.WIN)
            return;
        const input = (0, input_1.getInput)();
        const p = playerState;
        p.angle += (0, input_1.getMouseDelta)() * ROT_SPEED;
        let moving = false;
        let dx = 0, dy = 0;
        if (input.forward) {
            dx += Math.cos(p.angle) * MOVE_SPEED;
            dy += Math.sin(p.angle) * MOVE_SPEED;
            moving = true;
        }
        if (input.backward) {
            dx -= Math.cos(p.angle) * MOVE_SPEED;
            dy -= Math.sin(p.angle) * MOVE_SPEED;
            moving = true;
        }
        if (input.strafeLeft) {
            dx += Math.cos(p.angle - Math.PI / 2) * STRAFE_SPEED;
            dy += Math.sin(p.angle - Math.PI / 2) * STRAFE_SPEED;
            moving = true;
        }
        if (input.strafeRight) {
            dx += Math.cos(p.angle + Math.PI / 2) * STRAFE_SPEED;
            dy += Math.sin(p.angle + Math.PI / 2) * STRAFE_SPEED;
            moving = true;
        }
        if (canMove(p.x + dx, p.y))
            p.x += dx;
        if (canMove(p.x, p.y + dy))
            p.y += dy;
        p.bobPhase += moving ? dt * 0.009 : 0;
        p.bobPhase *= moving ? 1 : 0.9;
        p.weapon.update(performance.now());
        p.damageFlash = Math.max(0, p.damageFlash - 1);
        // Shooting
        if (input.shoot && !p.lastShoot) {
            shootWeapon(performance.now());
        }
        // Weapon cycling (R key)
        if (input.weaponCycle && !p.prevWeaponCycle) {
            p.weapon.switchNext();
        }
        p.prevWeaponCycle = input.weaponCycle;
        p.lastShoot = input.shoot;
        p.lastShoot = input.shoot;
        // Check win
        if ((0, map_1.isExit)(p.x, p.y)) {
            gameState = GameState.WIN;
            document.getElementById('win-screen').classList.remove('hidden');
            (0, audio_1.playSound)('exit');
            return;
        }
        updateEnemies(dt);
        checkPickups();
        rebuildSprites();
        updateHUD();
        if (msgTimer > 0) {
            msgTimer -= dt;
            if (msgTimer <= 0)
                hideMessage();
        }
        // ---- RENDER ----
        imgData.data.fill(0);
        const headBob = Math.sin(p.bobPhase) * 2.5;
        const halfH = CANVAS_H / 2 + headBob;
        // Pre-compute row indices and stride
        const stride = CANVAS_W * 4;
        // First pass: draw background (ceiling + floor) for all rows
        for (let y = 0; y < Math.min(CANVAS_H, halfH); y++) {
            const row = y * stride;
            const skyF = y / (CANVAS_H / 2);
            for (let x = 0; x < CANVAS_W; x++) {
                const i = row + x * 4;
                imgData.data[i] = Math.floor(30 + skyF * 20);
                imgData.data[i + 1] = Math.floor(15 + skyF * 10);
                imgData.data[i + 2] = Math.floor(20 + skyF * 15);
                imgData.data[i + 3] = 255;
            }
        }
        for (let y = Math.max(0, halfH | 0); y < CANVAS_H; y++) {
            const row = y * stride;
            const ft = (y - halfH + CANVAS_H / 2) / (CANVAS_H / 2);
            for (let x = 0; x < CANVAS_W; x++) {
                const i = row + x * 4;
                imgData.data[i] = Math.floor(40 + ft * 20);
                imgData.data[i + 1] = Math.floor(30 + ft * 15);
                imgData.data[i + 2] = Math.floor(25 + ft * 12);
                imgData.data[i + 3] = 255;
            }
        }
        // Second pass: overlay walls on columns where we hit
        zBuffer = new Float32Array(CANVAS_W);
        const baseAngle = p.angle - FOV / 2;
        for (let x = 0; x < CANVAS_W; x++) {
            const rayAngle = baseAngle + (x / CANVAS_W) * FOV;
            const result = castWallRay(p.x, p.y, rayAngle, zBuffer, x);
            if (result.hit) {
                const { perpDist, wallType, side } = result;
                const colors = map_1.WALL_COLORS[wallType] || [128, 128, 128];
                const shade = side === 1 ? 0.65 : 1.0;
                const fogT = Math.min(1, 1 - perpDist / MAX_DEPTH);
                const darkness = shade * fogT;
                const lh = Math.floor(CANVAS_H / perpDist);
                const ds = Math.max(0, Math.floor(-lh / 2 + halfH));
                const de = Math.min(CANVAS_H, Math.floor(lh / 2 + halfH));
                for (let y = ds; y < de; y++) {
                    const ty = (y - ds) / (de - ds || 1);
                    const pat = (Math.floor(ty * 8) % 2 === 0) ? 1.0 : 0.88;
                    const idx = y * stride + x * 4;
                    imgData.data[idx] = Math.floor(colors[0] * darkness * pat);
                    imgData.data[idx + 1] = Math.floor(colors[1] * darkness * pat);
                    imgData.data[idx + 2] = Math.floor(colors[2] * darkness * pat);
                    imgData.data[idx + 3] = 255;
                }
            }
        }
        ctx.putImageData(imgData, 0, 0);
        // Sprites drawn on top using canvas 2D API
        for (const sp of sortSprites(enemySprites)) {
            drawSprite(ctx, sp, 180, 40, 40, zBuffer, halfH);
        }
        for (const sp of sortSprites(pickupSprites)) {
            const t = sp.entity.pickupType;
            drawSprite(ctx, sp, t === 'health' ? 0 : 200, t === 'health' ? 200 : 150, 0, zBuffer, halfH);
        }
        // Damage flash
        if (p.damageFlash > 0) {
            ctx.fillStyle = `rgba(255,0,0,${p.damageFlash / 30})`;
            ctx.fillRect(0, 0, CANVAS_W, CANVAS_H);
        }
        // Weapon
        renderWeaponView(ctx);
        // Minimap
        renderMinimap();
    }
    requestAnimationFrame(frameLoop);
}
let frameLastTime = 0;
};

// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"qwen/qwen3.7-flash","provider":"openrouter","persona":"typescript","sessionId":"c127e91e-cda5-4948-ae52-ffc27bb5162f","tokensIn":5944773,"tokensOut":54269,"tokensTotal":5999042,"cost":0.21259059000000005,"turns":90,"toolCalls":89,"failedToolCalls":6,"timestamp":"2026-08-02T02:58:14.174Z"} -->