← DOOM results

DOOM

Qwen 3.6 27b MTP · 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 71,840 bytes · SHA-256 4167549295f7
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>SHADOW CORRIDORS - Retro FPS</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; }
#game-container { position: relative; width: 100vw; height: 100vh; display: flex; justify-content: center; align-items: center; }
#game { image-rendering: pixelated; cursor: none; background: #000; }
#minimap { position: absolute; top: 8px; right: 8px; border: 2px solid #555; opacity: 0.7; image-rendering: pixelated; }

/* HUD */
#hud {
  position: absolute; bottom: 0; left: 0; right: 0; height: 64px;
  background: linear-gradient(to top, #1a1a2e 0%, #16213e 70%, transparent 100%);
  display: flex; align-items: center; justify-content: space-between; padding: 8px 20px;
  border-top: 2px solid #4a4a6a; z-index: 10;
}
#health-bar { width: 200px; height: 20px; background: #333; border: 2px solid #555; border-radius: 2px; overflow: hidden; }
#health-fill { height: 100%; background: linear-gradient(to right, #ff0000, #cc0000); transition: width 0.2s; width: 100%; }
#ammo-display { color: #ffaa00; font-size: 18px; text-shadow: 0 0 8px rgba(255,170,0,0.5); letter-spacing: 2px; }
#weapon-name { color: #ffffff; font-size: 22px; font-weight: bold; text-shadow: 0 0 10px rgba(255,255,255,0.3); letter-spacing: 4px; }
#score-display { color: #00ff88; font-size: 16px; text-shadow: 0 0 8px rgba(0,255,136,0.5); }
#level-display { color: #aaaaff; font-size: 14px; letter-spacing: 2px; }

/* Overlay */
#overlay {
  position: absolute; top: 0; left: 0; right: 0; bottom: 0;
  background: rgba(0,0,0,0.85); display: flex; flex-direction: column;
  justify-content: center; align-items: center; z-index: 20;
}
#overlay.hidden { display: none; }
#overlay-title { color: #ff4444; font-size: 48px; text-shadow: 0 0 20px rgba(255,68,68,0.8); margin-bottom: 10px; letter-spacing: 6px; }
#overlay-subtitle { color: #aaa; font-size: 18px; margin-bottom: 30px; text-align: center; line-height: 1.6; }
#overlay-btn {
  background: #cc0000; color: white; border: none; padding: 12px 40px;
  font-size: 20px; cursor: pointer; font-family: 'Courier New', monospace;
  letter-spacing: 3px; text-transform: uppercase; transition: all 0.2s;
}
#overlay-btn:hover { background: #ff2222; transform: scale(1.05); box-shadow: 0 0 20px rgba(255,0,0,0.5); }

/* Crosshair */
.crosshair { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); pointer-events: none; z-index: 15; }
/* ── css: crosshair ── */
#crosshair {
  position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
  pointer-events: none; z-index: 15; color: rgba(255,255,255,0.7);
  font-size: 24px; font-weight: bold; text-shadow: 0 0 4px rgba(0,0,0,0.8);
  line-height: 1; user-select: none;
}

/* Scanline effect overlay */
#game-container::after {
  content: ''; position: absolute; top: 0; left: 0; right: 0; bottom: 0;
  pointer-events: none; z-index: 5;
  background: repeating-linear-gradient(
    0deg,
    transparent,
    transparent 2px,
    rgba(0,0,0,0.03) 2px,
    rgba(0,0,0,0.03) 4px
  );
}

/* Kill feed */
#kill-feed {
  position: absolute; top: 10px; left: 10px; z-index: 15; pointer-events: none;
  font-family: 'Courier New', monospace; font-size: 12px; color: #ff8844;
}
.kill-msg {
  opacity: 1; transition: opacity 0.5s; margin-bottom: 4px;
  text-shadow: 0 0 6px rgba(0,0,0,0.8);
}
</style>
</head>
<body>
<div id="game-container">
  <canvas id="game"></canvas>
  <div id="hud">
    <div id="health-bar"><div id="health-fill"></div></div>
    <div id="ammo-display">AMMO: --</div>
    <div id="weapon-name">PISTOL</div>
    <div id="score-display">SCORE: 0</div>
    <div id="level-display">LEVEL: 1/3</div>
  </div>
  <div id="overlay" class="hidden">
    <div id="overlay-title"></div>
    <div id="overlay-subtitle"></div>
    <button id="overlay-btn">CONTINUE</button>
  </div>
  <canvas id="minimap"></canvas>
<div id="crosshair" class="crosshair">+</div><div id="kill-feed"></div></div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./types":"src/types.ts","./input":"src/input.ts","./renderer":"src/renderer.ts","./logic":"src/logic.ts","./game":"src/game.ts","./sounds":"src/sounds.ts"},"src/maps.ts":{"./types":"src/types.ts"},"src/game.ts":{"./types":"src/types.ts","./maps":"src/maps.ts"},"src/input.ts":{"./types":"src/types.ts"},"src/renderer.ts":{"./types":"src/types.ts","./maps":"src/maps.ts"},"src/logic.ts":{"./types":"src/types.ts","./maps":"src/maps.ts","./sounds":"src/sounds.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 types_1 = require("./types");
const input_1 = require("./input");
const renderer_1 = require("./renderer");
const logic_1 = require("./logic");
const game_1 = require("./game");
const sounds_1 = require("./sounds");
// DOM elements
const gameCanvas = document.getElementById('game');
const minimapCanvas = document.getElementById('minimap');
const healthFill = document.getElementById('health-fill');
const ammoDisplay = document.getElementById('ammo-display');
const weaponName = document.getElementById('weapon-name');
const scoreDisplay = document.getElementById('score-display');
const levelDisplay = document.getElementById('level-display');
const overlay = document.getElementById('overlay');
const overlayTitle = document.getElementById('overlay-title');
const overlaySubtitle = document.getElementById('overlay-subtitle');
const overlayBtn = document.getElementById('overlay-btn');
// Game state
let gameState = (0, game_1.initLevel)(0);
let damageFlash = 0;
let lastTime = performance.now();
let fireHeld = false;
let prevPickupStates = [];
let prevEnemyAlive = [];
const killFeedEl = document.getElementById('kill-feed');
const killMessages = [];
function addKillMessage(type) {
    const names = { grunt: 'GRUNT', soldier: 'SOLDIER', boss: 'BOSS' };
    const msg = document.createElement('div');
    msg.className = 'kill-msg';
    msg.textContent = `☠ ${names[type] || 'ENEMY'} ELIMINATED`;
    killFeedEl.appendChild(msg);
    killMessages.push({ el: msg, timer: 3000 });
    while (killMessages.length > 5) {
        const old = killMessages.shift();
        old.el.remove();
    }
}
// Render target
const rt = (0, renderer_1.createRenderTarget)(gameCanvas);
function resizeCanvas() {
    const container = document.getElementById('game-container');
    const cw = container.clientWidth || window.innerWidth;
    const ch = container.clientHeight || window.innerHeight;
    const scale = Math.min(cw / types_1.RENDER_W, ch / types_1.RENDER_H);
    gameCanvas.style.width = `${Math.floor(types_1.RENDER_W * scale)}px`;
    gameCanvas.style.height = `${Math.floor(types_1.RENDER_H * scale)}px`;
}
window.addEventListener('resize', resizeCanvas);
resizeCanvas();
(0, input_1.initInput)(gameCanvas);
window.__gameKeys = input_1.keys;
function showOverlay(title, subtitle, btnText) {
    overlay.classList.remove('hidden');
    overlayTitle.textContent = title;
    overlaySubtitle.innerHTML = subtitle;
    overlayBtn.textContent = btnText;
}
function hideOverlay() {
    overlay.classList.add('hidden');
}
showOverlay('SHADOW CORRIDORS', '<span style="color:#ff8844;font-size:14px">A Retro FPS Adventure</span><br><br>' +
    '<b>WASD</b> — Move &nbsp;|&nbsp; <b>Mouse</b> — Look<br>' +
    '<b>Left Click / Space</b> — Fire<br>' +
    '<b>1</b> Pistol (∞ ammo) &nbsp; <b>2</b> Shotgun &nbsp; <b>3</b> Rifle<br><br>' +
    'Eliminate enemies, collect pickups,<br>and find the exit portal in each level!', 'START GAME');
overlayBtn.addEventListener('click', () => {
    (0, sounds_1.initAudio)();
    if (gameState.gameState === 'dead') {
        gameState.deathTimer = 0;
        return;
    }
    gameCanvas.focus();
    gameCanvas.requestPointerLock();
});
gameCanvas.addEventListener('click', () => { (0, sounds_1.initAudio)(); });
function updateHUD() {
    const p = gameState.player;
    healthFill.style.width = `${(p.health / p.maxHealth) * 100}%`;
    if (p.health > 60)
        healthFill.style.background = 'linear-gradient(to right, #44cc44, #22aa22)';
    else if (p.health > 30)
        healthFill.style.background = 'linear-gradient(to right, #cccc00, #aaaa00)';
    else
        healthFill.style.background = 'linear-gradient(to right, #ff4444, #cc0000)';
    const weapon = types_1.WEAPONS[p.weaponIndex];
    ammoDisplay.textContent = weapon.ammoCost === 0 ? 'AMMO: ∞' : `AMMO: ${p.ammo[p.weaponIndex]}`;
    weaponName.textContent = weapon.name;
    scoreDisplay.textContent = `SCORE: ${p.score}`;
    levelDisplay.textContent = `LEVEL: ${gameState.level}/${gameState.totalLevels}`;
    // Nearby pickup indicator
    let nearbyPickup = '';
    for (const pk of gameState.pickups) {
        if (pk.collected)
            continue;
        const d = Math.sqrt((p.x - pk.x) ** 2 + (p.y - pk.y) ** 2);
        if (d < 3) {
            const names = { health: '♥ HEALTH', ammo_shotgun: '⊕ SHOTGUN AMMO', ammo_rifle: '⊕ RIFLE AMMO' };
            nearbyPickup = `${names[pk.type]} (${Math.ceil(d)}m)`;
            break;
        }
    }
    let pickupIndicator = document.getElementById('pickup-indicator');
    if (nearbyPickup) {
        if (!pickupIndicator) {
            pickupIndicator = document.createElement('div');
            pickupIndicator.id = 'pickup-indicator';
            pickupIndicator.style.cssText = 'position:absolute;bottom:70px;left:50%;transform:translateX(-50%);color:#ffcc44;font-size:13px;font-family:Courier New,monospace;text-shadow:0 0 8px rgba(255,200,68,0.5);z-index:15;pointer-events:none;';
            document.getElementById('game-container').appendChild(pickupIndicator);
        }
        pickupIndicator.textContent = nearbyPickup;
    }
    else if (pickupIndicator) {
        pickupIndicator.remove();
    }
}
function gameLoop(now) {
    const dt = Math.min(0.05, (now - lastTime) / 1000);
    lastTime = now;
    if (gameState.gameState === 'playing') {
        const mouseDelta = (0, input_1.getMouseDelta)();
        (0, input_1.resetMouseDelta)();
        const oldHealth = gameState.player.health;
        (0, logic_1.updatePlayer)(gameState, dt, input_1.keys, mouseDelta);
        if (input_1.keys.fire && !fireHeld) {
            (0, logic_1.tryShoot)(gameState, now);
            fireHeld = true;
        }
        if (!input_1.keys.fire)
            fireHeld = false;
        if (input_1.keys.weapon1)
            (0, logic_1.switchWeapon)(gameState, 0);
        if (input_1.keys.weapon2)
            (0, logic_1.switchWeapon)(gameState, 1);
        if (input_1.keys.weapon3)
            (0, logic_1.switchWeapon)(gameState, 2);
        (0, logic_1.updateEnemies)(gameState, dt, now);
        if (gameState.player.health < oldHealth) {
            damageFlash = 300;
        }
        if (prevPickupStates.length !== gameState.pickups.length) {
            prevPickupStates = new Array(gameState.pickups.length).fill(false);
        }
        (0, logic_1.updatePickups)(gameState);
        for (let i = 0; i < gameState.pickups.length; i++) {
            if (!prevPickupStates[i] && !gameState.pickups[i].collected) {
                (0, sounds_1.playPickup)();
            }
            prevPickupStates[i] = gameState.pickups[i].collected;
        }
        if (prevEnemyAlive.length !== gameState.enemies.length) {
            prevEnemyAlive = new Array(gameState.enemies.length).fill(true);
        }
        for (let i = 0; i < gameState.enemies.length; i++) {
            if (prevEnemyAlive[i] && !gameState.enemies[i].alive) {
                addKillMessage(gameState.enemies[i].type);
            }
            prevEnemyAlive[i] = gameState.enemies[i].alive;
        }
        for (let i = killMessages.length - 1; i >= 0; i--) {
            killMessages[i].timer -= dt * 1000;
            if (killMessages[i].timer <= 0) {
                killMessages[i].el.style.opacity = '0';
                setTimeout(() => killMessages[i].el.remove(), 500);
                killMessages.splice(i, 1);
            }
            else if (killMessages[i].timer < 1000) {
                killMessages[i].el.style.opacity = String(killMessages[i].timer / 1000);
            }
        }
        if (damageFlash > 0)
            damageFlash -= dt * 1000;
    }
    let justDied = false;
    if (gameState.gameState === 'dead') {
        if (!justDied)
            (0, sounds_1.playDeath)();
        justDied = true;
        gameState.deathTimer -= dt * 1000;
        damageFlash = Math.max(damageFlash, 200);
        if (gameState.deathTimer <= 0) {
            (0, game_1.respawnPlayer)(gameState);
            hideOverlay();
            gameCanvas.requestPointerLock();
        }
        else {
            showOverlay('YOU DIED', `Score: ${gameState.player.score}<br>Respawning in ${Math.ceil(gameState.deathTimer / 1000)}s...<br><span style="color:#888;font-size:12px">(Click to respawn faster)</span>`, 'RESPAWN');
        }
    }
    let justCompletedLevel = false;
    if (gameState.gameState === 'levelComplete' && !justCompletedLevel) {
        (0, sounds_1.playLevelComplete)();
        justCompletedLevel = true;
    }
    if (gameState.gameState === 'levelComplete') {
        if (gameState.level >= gameState.totalLevels) {
            gameState.gameState = 'won';
            showOverlay('VICTORY!', `You escaped all ${gameState.totalLevels} levels!<br>Final Score: ${gameState.player.score}`, 'PLAY AGAIN');
            overlayBtn.onclick = () => {
                gameState = (0, game_1.initLevel)(0);
                hideOverlay();
                gameCanvas.requestPointerLock();
            };
        }
        else {
            showOverlay(`LEVEL ${gameState.level} COMPLETE`, `Score: ${gameState.player.score}<br>Get ready for the next level...`, 'NEXT LEVEL');
            overlayBtn.onclick = () => {
                (0, game_1.nextLevel)(gameState);
                hideOverlay();
                gameCanvas.requestPointerLock();
            };
        }
    }
    const playerRef = gameState.player;
    let screenShake = 0;
    if (playerRef.shooting && playerRef.shootTimer > 150) {
        const weapon = types_1.WEAPONS[playerRef.weaponIndex];
        screenShake = weapon.name === 'SHOTGUN' ? 4 : weapon.name === 'RIFLE' ? 2 : 1;
    }
    if (damageFlash > 100)
        screenShake += damageFlash / 50;
    const p = gameState.player;
    (0, renderer_1.renderFrame)(rt, gameState.map, gameState.mapWidth, gameState.mapHeight, p.x + (Math.random() - 0.5) * screenShake * 0.02, p.y + (Math.random() - 0.5) * screenShake * 0.02, p.angle, gameState.enemies, gameState.pickups, gameState.exitPos, p.shooting, p.shootTimer, p.weaponIndex, damageFlash, screenShake);
    (0, renderer_1.renderMinimap)(minimapCanvas, gameState.map, p.x, p.y, p.angle, gameState.enemies, gameState.pickups, gameState.exitPos);
    updateHUD();
    requestAnimationFrame(gameLoop);
}
requestAnimationFrame(gameLoop);
};

// ── module: src/types.ts ──
__mods["src/types.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RENDER_H = exports.RENDER_W = exports.FLOOR = exports.WALL = exports.WEAPONS = exports.MAX_DEPTH = exports.FOV = exports.TILE = void 0;
// Game constants
exports.TILE = 1; // tile size in world units
exports.FOV = Math.PI / 3; // field of view (60 degrees)
exports.MAX_DEPTH = 24; // max ray cast distance
exports.WEAPONS = [
    { name: 'PISTOL', damage: 15, fireRate: 400, ammoCost: 0, spread: 0.02, pellets: 1, range: 20, reloadTime: 300 },
    { name: 'SHOTGUN', damage: 12, fireRate: 800, ammoCost: 1, spread: 0.15, pellets: 6, range: 12, reloadTime: 600 },
    { name: 'RIFLE', damage: 25, fireRate: 150, ammoCost: 1, spread: 0.03, pellets: 1, range: 30, reloadTime: 400 },
];
// Map tile types
exports.WALL = 1;
exports.FLOOR = 0;
// Render constants
exports.RENDER_W = 640;
exports.RENDER_H = 480;
};

// ── module: src/maps.ts ──
__mods["src/maps.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.LEVELS = void 0;
exports.getLevel = getLevel;
exports.getWallColor = getWallColor;
exports.isWall = isWall;
exports.dist = dist;
// Map levels - 0 = empty, 1+ = wall (different numbers = different wall colors)
exports.LEVELS = [
    // Level 1 - The Corridors
    [
        [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, 1],
        [1, 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, 3, 3, 0, 0, 0, 0, 4, 4, 4, 0, 0, 1],
        [1, 0, 0, 2, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 4, 0, 0, 1],
        [1, 0, 0, 2, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 4, 0, 0, 1],
        [1, 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, 3, 3, 3, 0, 0, 2, 2, 2, 0, 0, 0, 0, 0, 1],
        [1, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 1],
        [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 1],
        [1, 0, 0, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 3, 0, 0, 1],
        [1, 0, 0, 4, 0, 0, 0, 0, 2, 2, 2, 0, 0, 0, 0, 0, 3, 0, 0, 1],
        [1, 0, 0, 4, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 1],
        [1, 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, 4, 4, 4, 0, 0, 0, 0, 0, 0, 1],
        [1, 0, 0, 3, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 2, 2, 2, 0, 1],
        [1, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 1],
        [1, 0, 0, 3, 3, 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, 1],
        [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
    ],
    // Level 2 - The Dungeon
    [
        [2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2],
        [2, 0, 0, 0, 3, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 4, 0, 0, 0, 0, 2],
        [2, 0, 0, 0, 3, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 2],
        [2, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2],
        [2, 0, 0, 0, 0, 0, 0, 4, 4, 0, 0, 0, 0, 3, 3, 3, 0, 0, 0, 0, 0, 2],
        [2, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 2],
        [2, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 4, 0, 0, 0, 2],
        [2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 2],
        [2, 1, 0, 0, 3, 3, 0, 0, 0, 2, 2, 2, 0, 0, 0, 0, 0, 4, 0, 0, 0, 2],
        [2, 0, 0, 0, 3, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2],
        [2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 4, 4, 0, 0, 0, 0, 0, 2],
        [2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 3, 0, 0, 2],
        [2, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 2],
        [2, 0, 1, 0, 0, 0, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 2],
        [2, 0, 0, 0, 0, 0, 4, 0, 0, 0, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2],
        [2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 2],
        [2, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 2],
        [2, 0, 4, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2],
        [2, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0, 2],
        [2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 3, 3, 0, 0, 0, 0, 2],
        [2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 2],
        [2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2],
    ],
    // Level 3 - The Fortress (final)
    [
        [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3],
        [3, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 4, 4, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 3],
        [3, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 3],
        [3, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0, 3],
        [3, 0, 1, 1, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 1, 1, 0, 0, 3],
        [3, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 3],
        [3, 0, 0, 0, 0, 0, 4, 4, 0, 0, 0, 0, 0, 0, 0, 4, 4, 0, 0, 0, 0, 0, 0, 3],
        [3, 0, 0, 0, 0, 0, 4, 0, 0, 0, 1, 1, 1, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 3],
        [3, 0, 2, 2, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 3],
        [3, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 3],
        [3, 0, 0, 0, 0, 1, 1, 0, 0, 4, 4, 0, 4, 4, 0, 0, 1, 1, 0, 0, 0, 0, 0, 3],
        [3, 0, 0, 0, 0, 1, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 1, 0, 0, 0, 0, 0, 3],
        [3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3],
        [3, 0, 0, 0, 0, 1, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 1, 0, 0, 0, 0, 0, 3],
        [3, 0, 0, 0, 0, 1, 1, 0, 0, 4, 4, 0, 4, 4, 0, 0, 1, 1, 0, 0, 0, 0, 0, 3],
        [3, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 3],
        [3, 0, 2, 2, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 3],
        [3, 0, 0, 0, 0, 0, 4, 0, 0, 0, 1, 1, 1, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 3],
        [3, 0, 0, 0, 0, 0, 4, 4, 0, 0, 0, 0, 0, 0, 0, 4, 4, 0, 0, 0, 0, 0, 0, 3],
        [3, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 3],
        [3, 0, 1, 1, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 1, 1, 0, 0, 3],
        [3, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0, 3],
        [3, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 3],
        [3, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 4, 4, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 3],
        [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3],
    ],
];
function getLevel(level) {
    const m = exports.LEVELS[level] || exports.LEVELS[0];
    return { map: m.map(r => [...r]), width: m[0].length, height: m.length };
}
// Wall colors by tile type (for rendering)
function getWallColor(tileType) {
    switch (tileType) {
        case 1: return [120, 80, 60]; // brown brick
        case 2: return [80, 80, 90]; // gray stone
        case 3: return [60, 50, 70]; // dark purple
        case 4: return [100, 70, 40]; // orange brick
        default: return [100, 100, 100];
    }
}
function isWall(map, x, y) {
    const tx = Math.floor(x);
    const ty = Math.floor(y);
    if (tx < 0 || tx >= map[0].length || ty < 0 || ty >= map.length)
        return true;
    return map[ty][tx] !== 0;
}
function dist(x1, y1, x2, y2) {
    return Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
}
};

// ── module: src/game.ts ──
__mods["src/game.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.createGameState = createGameState;
exports.respawnPlayer = respawnPlayer;
exports.nextLevel = nextLevel;
exports.isExit = isExit;
exports.markExit = markExit;
exports.initLevel = initLevel;
const maps_1 = require("./maps");
const TOTAL_LEVELS = 3;
function createPlayer() {
    return {
        x: 1.5, y: 1.5, angle: 0,
        health: 100, maxHealth: 100,
        weaponIndex: 0,
        ammo: [999, 20, 30], // pistol unlimited, shotgun 20, rifle 30
        score: 0, alive: true,
        lastShot: 0, shooting: false, shootTimer: 0,
    };
}
function findEmptySpot(map, tries = 100) {
    for (let i = 0; i < tries; i++) {
        const x = Math.floor(Math.random() * map[0].length);
        const y = Math.floor(Math.random() * map.length);
        if (map[y][x] === 0)
            return { x: x + 0.5, y: y + 0.5 };
    }
    return null;
}
function spawnEnemies(map, level) {
    const enemies = [];
    let id = 0;
    const count = 6 + level * 4; // more enemies each level
    for (let i = 0; i < count; i++) {
        const pos = findEmptySpot(map);
        if (!pos)
            continue;
        if ((0, maps_1.dist)(pos.x, pos.y, 1.5, 1.5) < 4)
            continue; // not too close to spawn
        let type;
        if (level === 2 && i === count - 1) {
            type = 'boss';
        }
        else if (Math.random() < 0.3 + level * 0.15) {
            type = 'soldier';
        }
        else {
            type = 'grunt';
        }
        const e = {
            id: id++, x: pos.x, y: pos.y,
            health: type === 'boss' ? 200 : type === 'soldier' ? 60 : 35,
            maxHealth: type === 'boss' ? 200 : type === 'soldier' ? 60 : 35,
            speed: type === 'boss' ? 1.2 : type === 'soldier' ? 1.8 : 1.0,
            damage: type === 'boss' ? 20 : type === 'soldier' ? 12 : 8,
            attackRange: type === 'boss' ? 3 : 2.5,
            attackRate: type === 'boss' ? 600 : 1000,
            lastAttack: 0, type, alive: true,
            alertDist: type === 'soldier' ? 10 : 8,
            textureOffset: Math.random() * 100,
        };
        enemies.push(e);
    }
    return enemies;
}
function spawnPickups(map, level) {
    const pickups = [];
    let id = 0;
    const count = 4 + level * 2;
    for (let i = 0; i < count; i++) {
        const pos = findEmptySpot(map);
        if (!pos)
            continue;
        const r = Math.random();
        let type;
        if (r < 0.4)
            type = 'health';
        else if (r < 0.7)
            type = 'ammo_shotgun';
        else
            type = 'ammo_rifle';
        pickups.push({ id: id++, x: pos.x, y: pos.y, type, collected: false });
    }
    return pickups;
}
function findExit(map) {
    // Find a spot far from player spawn
    let bestX = map[0].length - 2;
    let bestY = map.length - 2;
    while (map[bestY][bestX] !== 0) {
        bestX--;
        if (bestX < 1) {
            bestX = map[0].length - 2;
            bestY--;
        }
    }
    return { x: bestX + 0.5, y: bestY + 0.5 };
}
function createGameState(level) {
    const lvl = (0, maps_1.getLevel)(level);
    const exitPos = findExit(lvl.map);
    // Place a special tile for the exit (tile type 9)
    const etx = Math.floor(exitPos.x - 0.5);
    const ety = Math.floor(exitPos.y - 0.5);
    if (lvl.map[ety] && lvl.map[ety][etx] !== undefined) {
        // Mark exit with a special value stored separately
    }
    return {
        player: createPlayer(),
        enemies: spawnEnemies(lvl.map, level),
        pickups: spawnPickups(lvl.map, level),
        map: lvl.map,
        mapWidth: lvl.width,
        mapHeight: lvl.height,
        level: level + 1,
        totalLevels: TOTAL_LEVELS,
        keysCollected: 0,
        keysRequired: 0,
        exitPos,
        gameState: 'playing',
        deathTimer: 0,
    };
}
function respawnPlayer(state) {
    state.player.health = Math.min(state.player.maxHealth, state.player.health + 50);
    state.player.alive = true;
    state.player.x = 1.5;
    state.player.y = 1.5;
    state.player.angle = 0;
    state.gameState = 'playing';
}
function nextLevel(state) {
    const newLvl = (0, maps_1.getLevel)(state.level);
    // Preserve score and ammo
    const oldScore = state.player.score;
    const oldAmmo = [...state.player.ammo];
    const oldWeapon = state.player.weaponIndex;
    const oldHealth = Math.min(100, state.player.health + 25);
    state.map = newLvl.map;
    state.mapWidth = newLvl.width;
    state.mapHeight = newLvl.height;
    state.level++;
    state.enemies = spawnEnemies(newLvl.map, state.level - 1);
    state.pickups = spawnPickups(newLvl.map, state.level - 1);
    state.exitPos = findExit(newLvl.map);
    state.player.x = 1.5;
    state.player.y = 1.5;
    state.player.angle = 0;
    state.player.health = oldHealth;
    state.player.score = oldScore;
    state.player.ammo = oldAmmo;
    state.player.weaponIndex = oldWeapon;
    state.gameState = 'playing';
}
function isExit(map, x, y) {
    const tx = Math.floor(x);
    const ty = Math.floor(y);
    if (tx < 0 || tx >= map[0].length || ty < 0 || ty >= map.length)
        return false;
    // Exit is marked with tile value 9
    return map[ty][tx] === 9;
}
// Mark exit on a map
function markExit(map, x, y) {
    const tx = Math.floor(x - 0.5);
    const ty = Math.floor(y - 0.5);
    if (tx >= 0 && tx < map[0].length && ty >= 0 && ty < map.length) {
        map[ty][tx] = 9;
    }
}
// Override createGameState to mark exit
function initLevel(level) {
    const state = createGameState(level);
    markExit(state.map, state.exitPos.x, state.exitPos.y);
    return state;
}
};

// ── module: src/input.ts ──
__mods["src/input.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.keys = void 0;
exports.getMouseDelta = getMouseDelta;
exports.resetMouseDelta = resetMouseDelta;
exports.initInput = initInput;
exports.keys = {
    forward: false, backward: false, left: false, right: false,
    strafeLeft: false, strafeRight: false, fire: false,
    weapon1: false, weapon2: false, weapon3: false,
};
let mouseDX = 0;
function getMouseDelta() { return mouseDX; }
function resetMouseDelta() { mouseDX = 0; }
const keyMap = {
    'KeyW': 'forward', 'ArrowUp': 'forward',
    'KeyS': 'backward', 'ArrowDown': 'backward',
    'KeyA': 'left', 'ArrowLeft': 'left',
    'KeyD': 'right', 'ArrowRight': 'right',
    'Q': 'strafeLeft', 'E': 'strafeRight',
    'Space': 'fire', 'ControlLeft': 'fire', 'ControlRight': 'fire',
    'Digit1': 'weapon1', 'Numpad1': 'weapon1',
    'Digit2': 'weapon2', 'Numpad2': 'weapon2',
    'Digit3': 'weapon3', 'Numpad3': 'weapon3',
};
window.addEventListener('keydown', (e) => {
    const k = keyMap[e.code];
    if (k)
        exports.keys[k] = true;
});
window.addEventListener('keyup', (e) => {
    const k = keyMap[e.code];
    if (k)
        exports.keys[k] = false;
});
// Mouse movement for looking around
let lastMouseX = 0;
function initInput(canvas) {
    canvas.addEventListener('click', () => {
        canvas.requestPointerLock();
    });
    document.addEventListener('mousemove', (e) => {
        if (document.pointerLockElement === canvas) {
            mouseDX += e.movementX;
        }
    });
    // Prevent context menu on right click
    canvas.addEventListener('contextmenu', (e) => e.preventDefault());
}
};

// ── module: src/renderer.ts ──
__mods["src/renderer.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.createRenderTarget = createRenderTarget;
exports.renderFrame = renderFrame;
exports.renderMinimap = renderMinimap;
const types_1 = require("./types");
const maps_1 = require("./maps");
function createRenderTarget(canvas) {
    const ctx = canvas.getContext('2d', { alpha: false });
    canvas.width = types_1.RENDER_W;
    canvas.height = types_1.RENDER_H;
    const imageData = ctx.createImageData(types_1.RENDER_W, types_1.RENDER_H);
    return { canvas, ctx, imageData, pixels: imageData.data };
}
function renderFrame(rt, map, mapW, mapH, px, py, angle, enemies, pickups, exitPos, shooting, shootTimer, weaponIndex, damageFlash, screenShake) {
    const { pixels, ctx } = rt;
    const halfH = types_1.RENDER_H / 2;
    const tanHalfFov = Math.tan(types_1.FOV / 2);
    // Apply screen shake offset
    const shakeX = (Math.random() - 0.5) * screenShake;
    const shakeY = (Math.random() - 0.5) * screenShake;
    ctx.save();
    ctx.translate(shakeX, shakeY);
    // Clear to dark ceiling and floor
    for (let y = 0; y < types_1.RENDER_H; y++) {
        for (let x = 0; x < types_1.RENDER_W; x++) {
            const i = (y * types_1.RENDER_W + x) << 2;
            if (y < halfH) {
                // Ceiling - dark gradient
                const t = 1.0 - y / halfH;
                pixels[i] = Math.floor(30 * (1 - t));
                pixels[i + 1] = Math.floor(25 * (1 - t));
                pixels[i + 2] = Math.floor(40 * (1 - t));
            }
            else {
                // Floor - dark gradient
                const t = (y - halfH) / halfH;
                pixels[i] = Math.floor(40 + 30 * t);
                pixels[i + 1] = Math.floor(35 + 25 * t);
                pixels[i + 2] = Math.floor(30 + 20 * t);
            }
            pixels[i + 3] = 255;
        }
    }
    // Raycasting for walls
    const zBuffer = new Float32Array(types_1.RENDER_W);
    for (let col = 0; col < types_1.RENDER_W; col++) {
        const rayAngle = angle - types_1.FOV / 2 + (col / types_1.RENDER_W) * types_1.FOV;
        const sinA = Math.sin(rayAngle);
        const cosA = Math.cos(rayAngle);
        // DDA algorithm
        let mapX = Math.floor(px);
        let mapY = Math.floor(py);
        const deltaDistX = Math.abs(1 / cosA) || 1e10;
        const deltaDistY = Math.abs(1 / sinA) || 1e10;
        let stepX, stepY;
        let sideDistX, sideDistY;
        if (cosA < 0) {
            stepX = -1;
            sideDistX = (px - mapX) * deltaDistX;
        }
        else {
            stepX = 1;
            sideDistX = (mapX + 1 - px) * deltaDistX;
        }
        if (sinA < 0) {
            stepY = -1;
            sideDistY = (py - mapY) * deltaDistY;
        }
        else {
            stepY = 1;
            sideDistY = (mapY + 1 - py) * deltaDistY;
        }
        let hit = false;
        let side = 0; // 0 = x-side, 1 = y-side
        let dist = 0;
        let tileType = 1;
        for (let step = 0; step < types_1.MAX_DEPTH * 4; step++) {
            if (sideDistX < sideDistY) {
                sideDistX += deltaDistX;
                mapX += stepX;
                side = 0;
            }
            else {
                sideDistY += deltaDistY;
                mapY += stepY;
                side = 1;
            }
            if (mapX < 0 || mapX >= mapW || mapY < 0 || mapY >= mapH)
                break;
            const tile = map[mapY][mapX];
            if (tile !== 0) {
                hit = true;
                tileType = tile;
                break;
            }
        }
        if (hit) {
            // Calculate perpendicular distance to avoid fisheye
            if (side === 0) {
                dist = (mapX - px + (1 - stepX) / 2) / cosA;
            }
            else {
                dist = (mapY - py + (1 - stepY) / 2) / sinA;
            }
            const perpDist = dist * Math.cos(rayAngle - angle);
            zBuffer[col] = perpDist;
            let screenPerpDist = perpDist;
            if (screenPerpDist < 0.1)
                screenPerpDist = 0.1;
            // Wall height on screen
            const lineHeight = Math.floor(types_1.RENDER_H / perpDist);
            const drawStart = Math.max(0, Math.floor(halfH - lineHeight / 2));
            const drawEnd = Math.min(types_1.RENDER_H - 1, Math.floor(halfH + lineHeight / 2));
            // Get wall color
            let [r, g, b] = (0, maps_1.getWallColor)(tileType);
            // Darken y-sides
            if (side === 1) {
                r *= 0.7;
                g *= 0.7;
                b *= 0.7;
            }
            // Distance fog
            const fogFactor = Math.min(1, perpDist / types_1.MAX_DEPTH);
            r = Math.floor(r * (1 - fogFactor * 0.8));
            g = Math.floor(g * (1 - fogFactor * 0.8));
            b = Math.floor(b * (1 - fogFactor * 0.8));
            // Draw wall strip with horizontal stripe pattern for texture effect
            const texX = side === 0 ? py + dist * sinA : px + dist * cosA;
            const texFrac = texX - Math.floor(texX);
            for (let row = drawStart; row <= drawEnd; row++) {
                const d = row - (halfH - lineHeight / 2);
                const texY = d / lineHeight;
                // Simple texture pattern
                let shade = 1.0;
                if ((Math.floor(texY * 8) + Math.floor(texX * 4)) % 3 === 0)
                    shade *= 0.85;
                if (texY < 0.02 || texY > 0.98)
                    shade *= 0.7; // darker edges
                const pi = (row * types_1.RENDER_W + col) << 2;
                pixels[pi] = Math.floor(r * shade);
                pixels[pi + 1] = Math.floor(g * shade);
                pixels[pi + 2] = Math.floor(b * shade);
            }
        }
        else {
            zBuffer[col] = types_1.MAX_DEPTH;
        }
    }
    const sprites = [];
    for (const e of enemies) {
        if (!e.alive)
            continue;
        const dx = e.x - px;
        const dy = e.y - py;
        sprites.push({ x: e.x, y: e.y, dist: Math.sqrt(dx * dx + dy * dy), data: e });
    }
    for (const p of pickups) {
        if (p.collected)
            continue;
        const dx = p.x - px;
        const dy = p.y - py;
        sprites.push({ x: p.x, y: p.y, dist: Math.sqrt(dx * dx + dy * dy), data: p });
    }
    // Sort back to front
    sprites.sort((a, b) => b.dist - a.dist);
    for (const sprite of sprites) {
        const dx = sprite.x - px;
        const dy = sprite.y - py;
        // Transform with inverse camera matrix
        const invDet = 1.0 / (Math.cos(angle + Math.PI / 2) * Math.sin(angle) - Math.sin(angle + Math.PI / 2) * Math.cos(angle));
        // Simplified: use angle directly
        const cosA = Math.cos(angle);
        const sinA = Math.sin(angle);
        const planeX = Math.cos(angle + Math.PI / 2) * tanHalfFov;
        const planeY = Math.sin(angle + Math.PI / 2) * tanHalfFov;
        // Transform sprite position relative to camera
        const transformX = cosA * dy - sinA * dx;
        const transformY = -planeY * dx + cosA * dy; // simplified depth
        if (transformX <= 0.1)
            continue; // behind player
        const spriteScreenX = Math.floor((types_1.RENDER_W / 2) * (1 + (-dx * cosA + dy * sinA) / transformX));
        // Actually let me redo this properly...
        // Proper sprite transformation
        const camX = -dx * Math.cos(angle) + dy * Math.sin(angle); // depth
        const camY = dx * Math.sin(angle) + dy * Math.cos(angle); // lateral (flipped for screen coords)
        if (camX <= 0.1)
            continue;
        const spriteScale = types_1.RENDER_H / camX;
        const spriteScreenCenterX = Math.floor((types_1.RENDER_W / 2) * (1 - camY / camX));
        // Determine sprite size and color based on type
        let spriteW, spriteH;
        let colors;
        const isEnemy = 'health' in sprite.data && 'type' in sprite.data;
        if (isEnemy) {
            const e = sprite.data;
            const size = e.type === 'boss' ? 1.5 : 1.0;
            spriteW = Math.floor(spriteScale * 0.6 * size);
            spriteH = Math.floor(spriteScale * 0.9 * size);
            if (e.type === 'grunt')
                colors = [[180, 60, 40], [150, 50, 30]];
            else if (e.type === 'soldier')
                colors = [[60, 120, 60], [50, 100, 50]];
            else
                colors = [[180, 40, 120], [150, 30, 100]]; // boss
            // Draw enemy body with detailed sprite shape
            const startX = Math.max(0, spriteScreenCenterX - Math.floor(spriteW / 2));
            const endX = Math.min(types_1.RENDER_W - 1, spriteScreenCenterX + Math.floor(spriteW / 2));
            const startY = Math.max(0, halfH - Math.floor(spriteH / 2));
            const endY = Math.min(types_1.RENDER_H - 1, halfH + Math.floor(spriteH / 2));
            for (let sx = startX; sx <= endX; sx++) {
                if (camX >= zBuffer[sx])
                    continue;
                const localX = (sx - spriteScreenCenterX) / (spriteW / 2);
                for (let sy = startY; sy <= endY; sy++) {
                    if (camX >= zBuffer[sx])
                        continue;
                    const localY = (sy - halfH) / (spriteH / 2);
                    let inBody = false;
                    let isEye = false;
                    let isMouth = false;
                    // Head region: top third
                    if (localY < -0.15) {
                        const headRadius = 0.38 + Math.max(0, -(localY + 0.15)) * 0.2;
                        inBody = localX * localX + (localY + 0.4) * (localY + 0.4) < headRadius * headRadius;
                        // Eyes - two bright spots on the face
                        if (inBody && localY > -0.35 && localY < -0.2) {
                            const eyeDist = Math.abs(Math.abs(localX) - 0.15);
                            if (eyeDist < 0.06)
                                isEye = true;
                        }
                        // Mouth
                        if (inBody && localY > -0.18 && localY < -0.12) {
                            if (Math.abs(localX) < 0.15)
                                isMouth = true;
                        }
                    }
                    // Torso: middle section
                    else if (localY <= 0.4) {
                        const torsoWidth = 0.35 + Math.abs(localY - 0.12) * 0.15;
                        inBody = Math.abs(localX) < torsoWidth;
                        // Arms on sides
                        if (!inBody && localY > -0.05 && localY < 0.3) {
                            const armDist = Math.abs(Math.abs(localX) - 0.48);
                            if (armDist < 0.12)
                                inBody = true;
                        }
                    }
                    // Legs: bottom section
                    else {
                        const legSpread = 0.15 + (localY - 0.4) * 0.3;
                        const leftLeg = Math.abs(localX + legSpread) < 0.12;
                        const rightLeg = Math.abs(localX - legSpread) < 0.12;
                        inBody = leftLeg || rightLeg;
                    }
                    if (inBody) {
                        const pi = (sy * types_1.RENDER_W + sx) << 2;
                        let [cr, cg, cb] = colors[0];
                        // Distance fog
                        const fogFactor = Math.min(1, camX / types_1.MAX_DEPTH);
                        if (isEye) {
                            // Glowing eyes
                            cr = 255;
                            cg = 255;
                            cb = 200;
                        }
                        else if (isMouth) {
                            // Dark mouth
                            cr = Math.floor(cr * 0.3);
                            cg = Math.floor(cg * 0.3);
                            cb = Math.floor(cb * 0.3);
                        }
                        pixels[pi] = Math.floor(cr * (1 - fogFactor * 0.8));
                        pixels[pi + 1] = Math.floor(cg * (1 - fogFactor * 0.8));
                        pixels[pi + 2] = Math.floor(cb * (1 - fogFactor * 0.8));
                        // Health bar on top of enemy
                        if (sy === startY && sx % 3 === 0) {
                            const healthPct = e.health / e.maxHealth;
                            pixels[pi] = Math.floor(255 * healthPct);
                            pixels[pi + 1] = Math.floor(255 * (1 - healthPct));
                            pixels[pi + 2] = 0;
                        }
                    }
                }
            }
        }
        else {
            // Pickup rendering
            const p = sprite.data;
            spriteW = Math.floor(spriteScale * 0.3);
            spriteH = Math.floor(spriteScale * 0.3);
            let color;
            if (p.type === 'health')
                color = [255, 60, 60];
            else if (p.type === 'ammo_shotgun')
                color = [255, 180, 40];
            else
                color = [60, 180, 255];
            const startX = Math.max(0, spriteScreenCenterX - Math.floor(spriteW / 2));
            const endX = Math.min(types_1.RENDER_W - 1, spriteScreenCenterX + Math.floor(spriteW / 2));
            const startY = Math.max(0, halfH + Math.floor(spriteH * 0.3) - Math.floor(spriteH / 2));
            const endY = Math.min(types_1.RENDER_H - 1, halfH + Math.floor(spriteH * 0.3) + Math.floor(spriteH / 2));
            // Bobbing animation
            const bobOffset = Math.sin(Date.now() / 300 + p.id) * spriteScale * 0.05;
            for (let sx = startX; sx <= endX; sx++) {
                if (camX >= zBuffer[sx])
                    continue;
                const bOff = Math.floor(bobOffset);
                for (let sy = startY + bOff; sy <= endY + bOff; sy++) {
                    if (sy < 0 || sy >= types_1.RENDER_H)
                        continue;
                    const pi = (sy * types_1.RENDER_W + sx) << 2;
                    // Diamond shape for pickup
                    const cx = Math.abs(sx - spriteScreenCenterX) / (spriteW / 2);
                    const cy = Math.abs(sy - halfH - spriteH * 0.3) / (spriteH / 2);
                    if (cx + cy < 1) {
                        // Glow effect
                        const glow = 1 - (cx + cy) * 0.5;
                        pixels[pi] = Math.min(255, color[0] + glow * 80);
                        pixels[pi + 1] = Math.min(255, color[1] + glow * 40);
                        pixels[pi + 2] = Math.min(255, color[2] + glow * 40);
                    }
                }
            }
        }
    }
    // Render exit portal
    {
        const dx = exitPos.x - px;
        const dy = exitPos.y - py;
        const camX = -dx * Math.cos(angle) + dy * Math.sin(angle);
        const camY = dx * Math.sin(angle) + dy * Math.cos(angle);
        if (camX > 0.1) {
            const spriteScale = types_1.RENDER_H / camX;
            const exitW = Math.floor(spriteScale * 0.5);
            const exitH = Math.floor(spriteScale * 0.8);
            const screenCenterX = Math.floor((types_1.RENDER_W / 2) * (1 - camY / camX));
            const eHalfW = Math.floor(exitW / 2);
            const eHalfH = Math.floor(exitH / 2);
            for (let sx = Math.max(0, screenCenterX - eHalfW); sx < Math.min(types_1.RENDER_W - 1, screenCenterX + eHalfW); sx++) {
                if (camX >= zBuffer[sx])
                    continue;
                const localX = Math.abs(sx - screenCenterX) / (exitW / 2);
                for (let sy = halfH - eHalfH; sy < halfH + eHalfH; sy++) {
                    if (sy < 0 || sy >= types_1.RENDER_H)
                        continue;
                    const localY = Math.abs(sy - halfH) / (exitH / 2);
                    // Portal effect - swirling colors
                    const t = Date.now() / 500 + sx * 0.1 + sy * 0.1;
                    if (localX < 0.9 && localY < 0.9) {
                        const pi = (sy * types_1.RENDER_W + sx) << 2;
                        pixels[pi] = Math.floor(50 + 100 * Math.sin(t));
                        pixels[pi + 1] = Math.floor(50 + 100 * Math.cos(t * 0.7));
                        pixels[pi + 2] = Math.floor(150 + 100 * Math.sin(t * 1.3));
                        // Bright border
                        if (localX > 0.8 || localY > 0.8) {
                            pixels[pi] = 200;
                            pixels[pi + 1] = 200;
                            pixels[pi + 2] = 255;
                        }
                    }
                }
            }
        }
    }
    // Weapon rendering (bottom center)
    renderWeapon(rt, weaponIndex, shooting, shootTimer);
    // Damage flash overlay
    if (damageFlash > 0) {
        const alpha = Math.min(1, damageFlash / 300);
        for (let y = 0; y < types_1.RENDER_H; y++) {
            for (let x = 0; x < types_1.RENDER_W; x++) {
                const i = (y * types_1.RENDER_W + x) << 2;
                pixels[i] = Math.min(255, pixels[i] + Math.floor(180 * alpha));
                pixels[i + 1] = Math.floor(pixels[i + 1] * (1 - alpha * 0.7));
                pixels[i + 2] = Math.floor(pixels[i + 2] * (1 - alpha * 0.7));
            }
        }
    }
    // Vignette effect
    for (let y = 0; y < types_1.RENDER_H; y++) {
        for (let x = 0; x < types_1.RENDER_W; x++) {
            const nx = (x / types_1.RENDER_W - 0.5) * 2;
            const ny = (y / types_1.RENDER_H - 0.5) * 2;
            const vignette = 1 - (nx * nx + ny * ny) * 0.3;
            const i = (y * types_1.RENDER_W + x) << 2;
            pixels[i] = Math.floor(pixels[i] * vignette);
            pixels[i + 1] = Math.floor(pixels[i + 1] * vignette);
            pixels[i + 2] = Math.floor(pixels[i + 2] * vignette);
        }
    }
    ctx.restore();
    rt.ctx.putImageData(rt.imageData, 0, 0);
    // Draw exit proximity indicator on canvas overlay
    const distToExit = Math.sqrt((exitPos.x - px) ** 2 + (exitPos.y - py) ** 2);
    if (distToExit < 15 && distToExit > 1.5) {
        ctx.fillStyle = `rgba(100, 200, 255, ${Math.min(0.8, (15 - distToExit) / 14)})`;
        ctx.font = 'bold 16px Courier New';
        ctx.textAlign = 'center';
        const exitAngle = Math.atan2(exitPos.y - py, exitPos.x - px);
        let angleDiff = exitAngle - angle;
        while (angleDiff > Math.PI)
            angleDiff -= Math.PI * 2;
        while (angleDiff < -Math.PI)
            angleDiff += Math.PI * 2;
        const arrowX = types_1.RENDER_W / 2 + Math.sin(angleDiff) * 100;
        ctx.fillText(`EXIT ${Math.ceil(distToExit)}m`, arrowX, 30);
        ctx.font = '24px Courier New';
        ctx.fillText('▼', arrowX, 52);
    }
}
function renderWeapon(rt, weaponIndex, shooting, shootTimer) {
    const { pixels } = rt;
    const cx = types_1.RENDER_W / 2;
    const baseY = types_1.RENDER_H;
    const bobX = Math.sin(Date.now() / 300) * 4;
    const bobY = Math.abs(Math.cos(Date.now() / 300)) * 3;
    let recoilOffset = 0;
    if (shooting && shootTimer > 0) {
        recoilOffset = Math.min(20, shootTimer * 0.15);
    }
    const weaponY = baseY + bobY - recoilOffset;
    // Draw weapon based on type
    if (weaponIndex === 0) {
        // Pistol
        drawPistol(pixels, cx + bobX, weaponY);
    }
    else if (weaponIndex === 1) {
        // Shotgun
        drawShotgun(pixels, cx + bobX, weaponY);
    }
    else {
        // Rifle
        drawRifle(pixels, cx + bobX, weaponY);
    }
    // Muzzle flash
    if (shooting && shootTimer > 150) {
        const flashSize = Math.floor(30 * (shootTimer - 150) / 100);
        for (let dy = -flashSize; dy <= flashSize; dy++) {
            for (let dx = -flashSize; dx <= flashSize; dx++) {
                if (dx * dx + dy * dy > flashSize * flashSize)
                    continue;
                const sx = cx + bobX + dx;
                const sy = weaponY - 100 + dy;
                if (sx >= 0 && sx < types_1.RENDER_W && sy >= 0 && sy < types_1.RENDER_H) {
                    const i = (sy * types_1.RENDER_W + sx) << 2;
                    const brightness = 1 - Math.sqrt(dx * dx + dy * dy) / flashSize;
                    pixels[i] = 255;
                    pixels[i + 1] = Math.floor(200 * brightness);
                    pixels[i + 2] = Math.floor(50 * brightness);
                }
            }
        }
    }
}
function drawPistol(pixels, cx, baseY) {
    const w = types_1.RENDER_W;
    // Barrel
    for (let y = baseY - 100; y < baseY - 40; y++) {
        for (let x = cx - 8; x <= cx + 8; x++) {
            if (y >= 0 && y < types_1.RENDER_H) {
                const i = (y * w + x) << 2;
                pixels[i] = 60;
                pixels[i + 1] = 55;
                pixels[i + 2] = 50;
            }
        }
    }
    // Handle
    for (let y = baseY - 40; y < baseY; y++) {
        for (let x = cx - 12; x <= cx + 6; x++) {
            if (y >= 0 && y < types_1.RENDER_H) {
                const i = (y * w + x) << 2;
                pixels[i] = 80;
                pixels[i + 1] = 50;
                pixels[i + 2] = 30;
            }
        }
    }
}
function drawShotgun(pixels, cx, baseY) {
    const w = types_1.RENDER_W;
    // Double barrel
    for (let y = baseY - 120; y < baseY - 30; y++) {
        for (let x = cx - 18; x <= cx + 18; x++) {
            if (y >= 0 && y < types_1.RENDER_H) {
                const i = (y * w + x) << 2;
                pixels[i] = 50;
                pixels[i + 1] = 45;
                pixels[i + 2] = 40;
            }
        }
    }
    // Stock
    for (let y = baseY - 30; y < baseY; y++) {
        for (let x = cx - 20; x <= cx + 15; x++) {
            if (y >= 0 && y < types_1.RENDER_H) {
                const i = (y * w + x) << 2;
                pixels[i] = 90;
                pixels[i + 1] = 60;
                pixels[i + 2] = 30;
            }
        }
    }
}
function drawRifle(pixels, cx, baseY) {
    const w = types_1.RENDER_W;
    // Long barrel
    for (let y = baseY - 130; y < baseY - 25; y++) {
        for (let x = cx - 6; x <= cx + 6; x++) {
            if (y >= 0 && y < types_1.RENDER_H) {
                const i = (y * w + x) << 2;
                pixels[i] = 45;
                pixels[i + 1] = 50;
                pixels[i + 2] = 55;
            }
        }
    }
    // Body
    for (let y = baseY - 30; y < baseY; y++) {
        for (let x = cx - 14; x <= cx + 14; x++) {
            if (y >= 0 && y < types_1.RENDER_H) {
                const i = (y * w + x) << 2;
                pixels[i] = 55;
                pixels[i + 1] = 60;
                pixels[i + 2] = 65;
            }
        }
    }
}
function renderMinimap(canvas, map, px, py, angle, enemies, pickups, exitPos) {
    const ctx = canvas.getContext('2d');
    const scale = 6;
    const mw = map[0].length * scale;
    const mh = map.length * scale;
    canvas.width = mw;
    canvas.height = mh;
    // Background
    ctx.fillStyle = '#111';
    ctx.fillRect(0, 0, mw, mh);
    // Walls
    for (let y = 0; y < map.length; y++) {
        for (let x = 0; x < map[0].length; x++) {
            if (map[y][x] !== 0) {
                const [r, g, b] = (0, maps_1.getWallColor)(map[y][x]);
                ctx.fillStyle = `rgb(${r},${g},${b})`;
                ctx.fillRect(x * scale, y * scale, scale, scale);
            }
            else if (map[y][x] === 9) {
                // Exit portal on minimap
                const t = Date.now() / 300;
                ctx.fillStyle = `hsl(${(t * 60) % 360}, 80%, 60%)`;
                ctx.fillRect(x * scale, y * scale, scale, scale);
            }
        }
    }
    // Pickups
    for (const p of pickups) {
        if (p.collected)
            continue;
        ctx.fillStyle = p.type === 'health' ? '#ff4444' : p.type === 'ammo_shotgun' ? '#ffaa00' : '#44aaff';
        ctx.fillRect(p.x * scale - 2, p.y * scale - 2, 4, 4);
    }
    // Enemies
    for (const e of enemies) {
        if (!e.alive)
            continue;
        ctx.fillStyle = e.type === 'boss' ? '#ff00ff' : e.type === 'soldier' ? '#00ff00' : '#ff6644';
        const size = e.type === 'boss' ? 5 : 3;
        ctx.fillRect(e.x * scale - size / 2, e.y * scale - size / 2, size, size);
    }
    // Player
    ctx.fillStyle = '#ffffff';
    ctx.beginPath();
    ctx.arc(px * scale, py * scale, 3, 0, Math.PI * 2);
    ctx.fill();
    // Direction indicator
    ctx.strokeStyle = '#ffff00';
    ctx.lineWidth = 1.5;
    ctx.beginPath();
    ctx.moveTo(px * scale, py * scale);
    ctx.lineTo((px + Math.cos(angle) * 2) * scale, (py + Math.sin(angle) * 2) * scale);
    ctx.stroke();
}
};

// ── module: src/logic.ts ──
__mods["src/logic.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.updatePlayer = updatePlayer;
exports.updateShooting = updateShooting;
exports.tryShoot = tryShoot;
exports.updateEnemies = updateEnemies;
exports.updatePickups = updatePickups;
exports.switchWeapon = switchWeapon;
const types_1 = require("./types");
const maps_1 = require("./maps");
const sounds_1 = require("./sounds");
const PLAYER_SPEED = 3.5; // tiles per second
const ROTATION_SPEED = 2.0; // radians per second
const PLAYER_RADIUS = 0.25;
function updatePlayer(state, dt, keys, mouseDelta) {
    const p = state.player;
    if (!p.alive)
        return;
    // Rotation from mouse
    p.angle += mouseDelta * ROTATION_SPEED * 0.003;
    // Keyboard rotation (slower)
    if (keys.left)
        p.angle -= ROTATION_SPEED * dt;
    if (keys.right)
        p.angle += ROTATION_SPEED * dt;
    // Movement
    let dx = 0, dy = 0;
    const cosA = Math.cos(p.angle);
    const sinA = Math.sin(p.angle);
    if (keys.forward) {
        dx += cosA;
        dy += sinA;
    }
    if (keys.backward) {
        dx -= cosA;
        dy -= sinA;
    }
    if (keys.strafeLeft) {
        dx += sinA;
        dy -= cosA;
    }
    if (keys.strafeRight) {
        dx -= sinA;
        dy += cosA;
    }
    // Normalize diagonal movement
    const len = Math.sqrt(dx * dx + dy * dy);
    if (len > 0) {
        dx = (dx / len) * PLAYER_SPEED * dt;
        dy = (dy / len) * PLAYER_SPEED * dt;
    }
    // Collision detection with sliding
    const newX = p.x + dx;
    const newY = p.y + dy;
    if (!(0, maps_1.isWall)(state.map, newX + (dx > 0 ? PLAYER_RADIUS : -PLAYER_RADIUS), p.y)) {
        p.x = newX;
    }
    if (!(0, maps_1.isWall)(state.map, p.x, newY + (dy > 0 ? PLAYER_RADIUS : -PLAYER_RADIUS))) {
        p.y = newY;
    }
    // Check exit
    const distToExit = (0, maps_1.dist)(p.x, p.y, state.exitPos.x, state.exitPos.y);
    if (distToExit < 0.8) {
        state.gameState = 'levelComplete';
    }
}
function updateShooting(state, now) {
    const p = state.player;
    if (!p.alive || state.gameState !== 'playing')
        return;
    const weapon = types_1.WEAPONS[p.weaponIndex];
    // Update shoot timer
    if (p.shootTimer > 0) {
        p.shootTimer -= 16; // approximate frame time
        if (p.shootTimer <= 0) {
            p.shooting = false;
            p.shootTimer = 0;
        }
    }
    // Check weapon switch
    const keys = window.__gameKeys;
}
function tryShoot(state, now) {
    const p = state.player;
    if (!p.alive)
        return;
    const weapon = types_1.WEAPONS[p.weaponIndex];
    // Check fire rate
    if (now - p.lastShot < weapon.fireRate)
        return;
    // Check ammo
    if (weapon.ammoCost > 0 && p.ammo[p.weaponIndex] <= 0)
        return;
    // Fire!
    (0, sounds_1.playShoot)(p.weaponIndex);
    p.lastShot = now;
    p.shooting = true;
    p.shootTimer = weapon.reloadTime;
    if (weapon.ammoCost > 0) {
        p.ammo[p.weaponIndex] -= weapon.ammoCost;
    }
    // Hit detection - raycast for each pellet
    for (let i = 0; i < weapon.pellets; i++) {
        const spreadAngle = (Math.random() - 0.5) * weapon.spread;
        const shootAngle = p.angle + spreadAngle;
        const sinA = Math.sin(shootAngle);
        const cosA = Math.cos(shootAngle);
        // Find closest enemy in this direction within range
        let closestDist = weapon.range;
        let hitEnemy = null;
        for (const e of state.enemies) {
            if (!e.alive)
                continue;
            const dx = e.x - p.x;
            const dy = e.y - p.y;
            const distToE = Math.sqrt(dx * dx + dy * dy);
            if (distToE > closestDist)
                continue;
            // Check if enemy is in the line of fire
            // Project enemy position onto ray direction
            const proj = dx * cosA + dy * sinA;
            if (proj < 0 || proj > distToE)
                continue;
            // Perpendicular distance from ray to enemy center
            const perpDist = Math.abs(dx * sinA - dy * cosA);
            const hitRadius = e.type === 'boss' ? 0.6 : 0.4;
            if (perpDist < hitRadius) {
                // Check wall between player and enemy
                if (!wallBetween(state.map, p.x, p.y, e.x, e.y)) {
                    closestDist = distToE;
                    hitEnemy = e;
                }
            }
        }
        if (hitEnemy) {
            (0, sounds_1.playHit)();
            const dmg = weapon.damage + Math.floor(Math.random() * 5);
            hitEnemy.health -= dmg;
            if (hitEnemy.health <= 0) {
                (0, sounds_1.playEnemyDeath)();
                hitEnemy.alive = false;
                p.score += hitEnemy.type === 'boss' ? 500 : hitEnemy.type === 'soldier' ? 100 : 50;
            }
        }
    }
}
function wallBetween(map, x1, y1, x2, y2) {
    const steps = Math.ceil(Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2) * 4);
    for (let i = 0; i <= steps; i++) {
        const t = i / steps;
        const x = x1 + (x2 - x1) * t;
        const y = y1 + (y2 - y1) * t;
        if ((0, maps_1.isWall)(map, x, y))
            return true;
    }
    return false;
}
function updateEnemies(state, dt, now) {
    const p = state.player;
    if (!p.alive)
        return;
    for (const e of state.enemies) {
        if (!e.alive)
            continue;
        const dx = p.x - e.x;
        const dy = p.y - e.y;
        const distToPlayer = Math.sqrt(dx * dx + dy * dy);
        // Alert check
        if (distToPlayer > e.alertDist)
            continue;
        // Move toward player
        if (distToPlayer > e.attackRange * 0.7) {
            const moveAngle = Math.atan2(dy, dx);
            const speed = e.speed * dt;
            const newX = e.x + Math.cos(moveAngle) * speed;
            const newY = e.y + Math.sin(moveAngle) * speed;
            // Simple collision - try to slide around walls
            if (!(0, maps_1.isWall)(state.map, newX, e.y)) {
                e.x = newX;
            }
            else if (!(0, maps_1.isWall)(state.map, e.x, newY)) {
                e.y = newY;
            }
            else {
                // Try diagonal
                const diagX = e.x + Math.cos(moveAngle) * speed * 0.7;
                const diagY = e.y + Math.sin(moveAngle + (Math.random() > 0.5 ? 1 : -1) * 0.5) * speed * 0.7;
                if (!(0, maps_1.isWall)(state.map, diagX, diagY)) {
                    e.x = diagX;
                    e.y = diagY;
                }
            }
        }
        // Attack player
        if (distToPlayer < e.attackRange && now - e.lastAttack > e.attackRate) {
            e.lastAttack = now;
            (0, sounds_1.playDamage)();
            p.health -= e.damage + Math.floor(Math.random() * 5);
            if (p.health <= 0) {
                p.health = 0;
                p.alive = false;
                state.gameState = 'dead';
                state.deathTimer = 2000; // 2 second respawn delay
            }
        }
    }
}
function updatePickups(state) {
    const p = state.player;
    if (!p.alive)
        return;
    for (const pickup of state.pickups) {
        if (pickup.collected)
            continue;
        const distToP = (0, maps_1.dist)(p.x, p.y, pickup.x, pickup.y);
        if (distToP > 0.6)
            continue;
        pickup.collected = true;
        switch (pickup.type) {
            case 'health':
                p.health = Math.min(p.maxHealth, p.health + 25);
                break;
            case 'ammo_shotgun':
                p.ammo[1] += 8;
                break;
            case 'ammo_rifle':
                p.ammo[2] += 10;
                break;
        }
    }
}
function switchWeapon(state, index) {
    if (index >= 0 && index < types_1.WEAPONS.length) {
        state.player.weaponIndex = index;
    }
}
};

// ── module: src/sounds.ts ──
__mods["src/sounds.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.initAudio = initAudio;
exports.playShoot = playShoot;
exports.playHit = playHit;
exports.playEnemyDeath = playEnemyDeath;
exports.playPickup = playPickup;
exports.playDamage = playDamage;
exports.playLevelComplete = playLevelComplete;
exports.playDeath = playDeath;
// Simple synthesized sound effects using Web Audio API
let audioCtx = null;
function initAudio() {
    if (!audioCtx) {
        audioCtx = new (window.AudioContext || window.webkitAudioContext)();
    }
    else if (audioCtx.state === 'suspended') {
        audioCtx.resume();
    }
}
function safePlay(fn) {
    try {
        const ctx = audioCtx;
        if (!ctx || ctx.state === 'suspended')
            return;
        fn(ctx, ctx.currentTime);
    }
    catch (_e) { /* Audio not available */ }
}
function playShoot(weaponIndex) {
    safePlay((ctx, now) => {
        if (weaponIndex === 0) {
            // Pistol - sharp crack
            const osc = ctx.createOscillator();
            const gain = ctx.createGain();
            osc.type = 'square';
            osc.frequency.setValueAtTime(800, now);
            osc.frequency.exponentialRampToValueAtTime(200, now + 0.1);
            gain.gain.setValueAtTime(0.3, now);
            gain.gain.exponentialRampToValueAtTime(0.001, now + 0.15);
            osc.connect(gain).connect(ctx.destination);
            osc.start(now);
            osc.stop(now + 0.15);
            const bufferSize = ctx.sampleRate * 0.05;
            const buffer = ctx.createBuffer(1, bufferSize, ctx.sampleRate);
            const data = buffer.getChannelData(0);
            for (let i = 0; i < bufferSize; i++)
                data[i] = Math.random() * 2 - 1;
            const noise = ctx.createBufferSource();
            noise.buffer = buffer;
            const nGain = ctx.createGain();
            nGain.gain.setValueAtTime(0.4, now);
            nGain.gain.exponentialRampToValueAtTime(0.001, now + 0.05);
            noise.connect(nGain).connect(ctx.destination);
            noise.start(now);
        }
        else if (weaponIndex === 1) {
            // Shotgun - loud boom with rumble
            const osc = ctx.createOscillator();
            const gain = ctx.createGain();
            osc.type = 'sawtooth';
            osc.frequency.setValueAtTime(300, now);
            osc.frequency.exponentialRampToValueAtTime(50, now + 0.2);
            gain.gain.setValueAtTime(0.5, now);
            gain.gain.exponentialRampToValueAtTime(0.001, now + 0.3);
            osc.connect(gain).connect(ctx.destination);
            osc.start(now);
            osc.stop(now + 0.3);
            const bufferSize = ctx.sampleRate * 0.15;
            const buffer = ctx.createBuffer(1, bufferSize, ctx.sampleRate);
            const data = buffer.getChannelData(0);
            for (let i = 0; i < bufferSize; i++)
                data[i] = Math.random() * 2 - 1;
            const noise = ctx.createBufferSource();
            noise.buffer = buffer;
            const nGain = ctx.createGain();
            nGain.gain.setValueAtTime(0.6, now);
            nGain.gain.exponentialRampToValueAtTime(0.001, now + 0.2);
            noise.connect(nGain).connect(ctx.destination);
            noise.start(now);
        }
        else {
            // Rifle - rapid crack
            const osc = ctx.createOscillator();
            const gain = ctx.createGain();
            osc.type = 'square';
            osc.frequency.setValueAtTime(1200, now);
            osc.frequency.exponentialRampToValueAtTime(400, now + 0.06);
            gain.gain.setValueAtTime(0.35, now);
            gain.gain.exponentialRampToValueAtTime(0.001, now + 0.08);
            osc.connect(gain).connect(ctx.destination);
            osc.start(now);
            osc.stop(now + 0.08);
            const bufferSize = ctx.sampleRate * 0.03;
            const buffer = ctx.createBuffer(1, bufferSize, ctx.sampleRate);
            const data = buffer.getChannelData(0);
            for (let i = 0; i < bufferSize; i++)
                data[i] = Math.random() * 2 - 1;
            const noise = ctx.createBufferSource();
            noise.buffer = buffer;
            const nGain = ctx.createGain();
            nGain.gain.setValueAtTime(0.3, now);
            nGain.gain.exponentialRampToValueAtTime(0.001, now + 0.04);
            noise.connect(nGain).connect(ctx.destination);
            noise.start(now);
        }
    });
}
function playHit() {
    safePlay((ctx, now) => {
        const osc = ctx.createOscillator();
        const gain = ctx.createGain();
        osc.type = 'triangle';
        osc.frequency.setValueAtTime(400, now);
        osc.frequency.exponentialRampToValueAtTime(150, now + 0.1);
        gain.gain.setValueAtTime(0.2, now);
        gain.gain.exponentialRampToValueAtTime(0.001, now + 0.12);
        osc.connect(gain).connect(ctx.destination);
        osc.start(now);
        osc.stop(now + 0.12);
    });
}
function playEnemyDeath() {
    safePlay((ctx, now) => {
        const osc = ctx.createOscillator();
        const gain = ctx.createGain();
        osc.type = 'sawtooth';
        osc.frequency.setValueAtTime(500, now);
        osc.frequency.exponentialRampToValueAtTime(80, now + 0.4);
        gain.gain.setValueAtTime(0.25, now);
        gain.gain.exponentialRampToValueAtTime(0.001, now + 0.4);
        osc.connect(gain).connect(ctx.destination);
        osc.start(now);
        osc.stop(now + 0.4);
    });
}
function playPickup() {
    safePlay((ctx, now) => {
        [523, 659, 784].forEach((freq, i) => {
            const osc = ctx.createOscillator();
            const gain = ctx.createGain();
            osc.type = 'sine';
            osc.frequency.setValueAtTime(freq, now + i * 0.08);
            gain.gain.setValueAtTime(0, now);
            gain.gain.linearRampToValueAtTime(0.2, now + i * 0.08);
            gain.gain.exponentialRampToValueAtTime(0.001, now + i * 0.08 + 0.15);
            osc.connect(gain).connect(ctx.destination);
            osc.start(now + i * 0.08);
            osc.stop(now + i * 0.08 + 0.15);
        });
    });
}
function playDamage() {
    safePlay((ctx, now) => {
        const osc = ctx.createOscillator();
        const gain = ctx.createGain();
        osc.type = 'square';
        osc.frequency.setValueAtTime(150, now);
        osc.frequency.linearRampToValueAtTime(100, now + 0.15);
        gain.gain.setValueAtTime(0.2, now);
        gain.gain.exponentialRampToValueAtTime(0.001, now + 0.15);
        osc.connect(gain).connect(ctx.destination);
        osc.start(now);
        osc.stop(now + 0.15);
    });
}
function playLevelComplete() {
    safePlay((ctx, now) => {
        [523, 659, 784, 1047].forEach((freq, i) => {
            const osc = ctx.createOscillator();
            const gain = ctx.createGain();
            osc.type = 'square';
            osc.frequency.setValueAtTime(freq, now + i * 0.15);
            gain.gain.setValueAtTime(0, now);
            gain.gain.linearRampToValueAtTime(0.15, now + i * 0.15);
            gain.gain.exponentialRampToValueAtTime(0.001, now + i * 0.15 + 0.3);
            osc.connect(gain).connect(ctx.destination);
            osc.start(now + i * 0.15);
            osc.stop(now + i * 0.15 + 0.3);
        });
    });
}
function playDeath() {
    safePlay((ctx, now) => {
        const osc = ctx.createOscillator();
        const gain = ctx.createGain();
        osc.type = 'sawtooth';
        osc.frequency.setValueAtTime(600, now);
        osc.frequency.exponentialRampToValueAtTime(40, now + 1.0);
        gain.gain.setValueAtTime(0.3, now);
        gain.gain.exponentialRampToValueAtTime(0.001, now + 1.0);
        osc.connect(gain).connect(ctx.destination);
        osc.start(now);
        osc.stop(now + 1.0);
    });
}
};

// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"qwen3.6-27b-mtp","provider":"lmstudio","persona":"typescript","sessionId":"9ee1540e-bcd2-44b1-94b2-15e8f972a1f0","tokensIn":3722438,"tokensOut":47773,"tokensTotal":3770211,"turns":84,"toolCalls":83,"failedToolCalls":5,"timestamp":"2026-07-30T20:02:49.786Z"} -->