← DOOM results

DOOM

Qwen 3.6 27B MTP GGUF UD Q5 K XL · 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 74,741 bytes · SHA-256 0461ec8eae60
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>DOOMED CORRIDORS - Retro FPS</title>
<style>
/* ── css: base ── */
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { 
  width: 100%; height: 100%; overflow: hidden; 
  background: #000; 
  font-family: 'Courier New', monospace;
}
canvas { 
  display: block; 
  width: 100%; height: 100%; 
  image-rendering: pixelated;
  cursor: none;
}
/* ── css: hud ── */
#hud {
  position: fixed; bottom: 0; left: 0; right: 0;
  height: 72px;
  background: linear-gradient(to top, #1a1a1a 0%, #2a2a2a 60%, transparent 100%);
  border-top: 2px solid #444;
  display: flex; justify-content: space-between; align-items: center;
  padding: 8px 24px;
  z-index: 10;
  pointer-events: none;
  color: #fff;
  font-size: 16px;
  font-weight: bold;
  text-shadow: 1px 1px 2px #000;
  font-family: 'Courier New', monospace;
}
#hud-left, #hud-right, #hud-center {
  display: flex; flex-direction: column; gap: 2px;
}
#hud-center { align-items: center; }
#hud-right { align-items: flex-end; }
#weapon-name {
  font-size: 22px;
  color: #ff8844;
  text-shadow: 0 0 10px rgba(255, 136, 68, 0.5);
}
#crosshair {
  position: fixed; top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  width: 24px; height: 24px;
  z-index: 11;
  pointer-events: none;
}
#crosshair::before, #crosshair::after {
  content: ''; position: absolute;
  background: rgba(255, 255, 255, 0.7);
}
#crosshair::before {
  width: 2px; height: 24px; left: 11px; top: 0;
}
#crosshair::after {
  width: 24px; height: 2px; top: 11px; left: 0;
}
#message-overlay {
  position: fixed; top: 0; left: 0; right: 0; bottom: 72px;
  display: flex; flex-direction: column;
  justify-content: center; align-items: center;
  z-index: 12;
  pointer-events: none;
  background: rgba(0,0,0,0.5);
  color: #ff4444;
  font-size: 40px;
  font-weight: bold;
  text-shadow: 2px 2px 4px #000;
  opacity: 0;
  transition: opacity 0.3s;
  text-align: center;
  line-height: 1.4;
  font-family: 'Courier New', monospace;
}
#message-overlay.visible { opacity: 1; }
#controls-hint {
  position: fixed; top: 8px; left: 50%; transform: translateX(-50%);
  color: rgba(255,255,255,0.4);
  font-size: 12px;
  z-index: 10;
  pointer-events: none;
  font-family: 'Courier New', monospace;
}
#damage-overlay {
  position: fixed; top: 0; left: 0; right: 0; bottom: 0;
  pointer-events: none;
  z-index: 9;
  border: 8px solid transparent;
  transition: border-color 0.1s;
}
#damage-overlay.hit { border-color: rgba(255,0,0,0.6); }
#game-over-overlay {
  position: fixed; top: 0; left: 0; right: 0; bottom: 0;
  z-index: 50;
  pointer-events: none;
  display: flex; flex-direction: column;
  justify-content: center; align-items: center;
  background: rgba(80, 0, 0, 0.4);
  opacity: 0;
  transition: opacity 0.5s;
}
#game-over-overlay.visible { opacity: 1; }
#game-over-overlay .respawn-timer {
  font-size: 24px;
  color: #ffaaaa;
  font-family: 'Courier New', monospace;
  margin-top: 10px;
}
/* ── css: title ── */
#title-screen {
  position: fixed; top: 0; left: 0; right: 0; bottom: 0;
  background: linear-gradient(to bottom, #000 0%, #1a0000 50%, #000 100%);
  display: flex; flex-direction: column;
  justify-content: center; align-items: center;
  z-index: 200;
  cursor: pointer;
}
#title-text {
  font-size: 64px;
  font-weight: bold;
  color: #cc0000;
  text-shadow: 0 0 20px #ff0000, 0 0 40px #880000, 3px 3px 0 #440000;
  letter-spacing: 8px;
  margin-bottom: 10px;
}
#subtitle {
  font-size: 20px;
  color: #884444;
  letter-spacing: 12px;
  margin-bottom: 60px;
}
#start-prompt {
  font-size: 24px;
  color: #ff4444;
  animation: pulse 1.5s ease-in-out infinite;
}
@keyframes pulse {
  0%, 100% { opacity: 0.4; }
  50% { opacity: 1; }
}
#controls-info {
  font-size: 14px;
  color: #666;
  margin-top: 30px;
  letter-spacing: 2px;
}
#vignette {
  position: fixed; top: 0; left: 0; right: 0; bottom: 0;
  pointer-events: none;
  z-index: 8;
}
/* ── css: gameover ── */
#game-over-overlay {
  position: fixed; top: 0; left: 0; right: 0; bottom: 0;
  z-index: 50;
  pointer-events: none;
  display: flex; flex-direction: column;
  justify-content: center; align-items: center;
  background: rgba(80, 0, 0, 0.3);
  opacity: 0;
  transition: opacity 0.5s;
}
#game-over-overlay.visible { opacity: 1; }
#death-text {
  font-size: 64px;
  color: #cc0000;
  font-weight: bold;
  text-shadow: 0 0 20px #ff0000, 3px 3px 0 #440000;
  font-family: 'Courier New', monospace;
  letter-spacing: 6px;
}
#game-over-overlay .respawn-timer {
  font-size: 20px;
  color: #ffaaaa;
  font-family: 'Courier New', monospace;
  margin-top: 20px;
}
</style>
</head>
<body>
<div id="damage-overlay"></div><div id="game-over-overlay">
  <div id="death-text">YOU DIED</div>
  <div class="respawn-timer" id="respawn-timer"></div>
</div>
<canvas id="game"></canvas>
<div id="hud">
  <div id="hud-left">
    <div id="health-display">❤️ 100</div>
    <div id="armor-display">🛡️ 0</div>
  </div>
  <div id="hud-center">
    <div id="weapon-name">PISTOL</div>
  </div>
  <div id="hud-right">
    <div id="ammo-display">🔫 20 / 99</div>
    <div id="score-display">💀 0 / 20</div>
  </div>
</div>
<div id="crosshair"></div>
<div id="message-overlay"></div>
<div id="controls-hint">WASD: Move | Mouse: Look | Click: Shoot | 1-3: Weapons | R: Reload</div>
<div id="title-screen">
  <div id="title-text">DOOMED CORRIDORS</div>
  <div id="subtitle">A Retro FPS</div>
  <div id="start-prompt">Click to Play</div>
  <div id="controls-info">WASD: Move | Mouse: Look | Click: Shoot | 1-3: Weapons</div>
</div>
<canvas id="vignette"></canvas>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./game":"src/game.ts","./renderer":"src/renderer.ts","./sprites":"src/sprites.ts","./weapons":"src/weapons.ts","./input":"src/input.ts","./audio":"src/audio.ts","./ai":"src/ai.ts","./map":"src/map.ts","./player":"src/player.ts"},"src/player.ts":{"./types":"src/types.ts"},"src/game.ts":{"./player":"src/player.ts","./types":"src/types.ts","./map":"src/map.ts"},"src/renderer.ts":{"./game":"src/game.ts","./map":"src/map.ts"},"src/sprites.ts":{"./game":"src/game.ts","./types":"src/types.ts"},"src/weapons.ts":{"./game":"src/game.ts","./types":"src/types.ts","./map":"src/map.ts","./audio":"src/audio.ts"},"src/input.ts":{"./game":"src/game.ts","./audio":"src/audio.ts"},"src/ai.ts":{"./game":"src/game.ts","./types":"src/types.ts","./map":"src/map.ts","./player":"src/player.ts","./audio":"src/audio.ts"}};
function __require(id) {
  if (__cache[id]) return __cache[id].exports;
  var module = __cache[id] = { exports: {} };
  var factory = __mods[id];
  if (!factory) throw new Error("Module not found: " + id);
  factory(module.exports, function (spec) {
    var target = (__map[id] && __map[id][spec]) || spec;
    return __require(target);
  }, module);
  return module.exports;
}

// ── module: src/main.ts ──
__mods["src/main.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.start = start;
const game_1 = require("./game");
const renderer_1 = require("./renderer");
const sprites_1 = require("./sprites");
const weapons_1 = require("./weapons");
const input_1 = require("./input");
const audio_1 = require("./audio");
const ai_1 = require("./ai");
const map_1 = require("./map");
const player_1 = require("./player");
let g;
let ws;
function start() {
    const canvas = document.getElementById('game');
    const ctx = canvas.getContext('2d');
    ctx.imageSmoothingEnabled = false;
    function resize() {
        canvas.width = window.innerWidth;
        canvas.height = window.innerHeight;
        if (g) {
            g.width = canvas.width;
            g.height = canvas.height;
            g.zBuffer = new Array(g.width);
        }
    }
    window.addEventListener('resize', resize);
    resize();
    g = (0, game_1.createGame)(canvas, ctx, canvas.width, canvas.height);
    ws = (0, weapons_1.createWeaponState)();
    (0, input_1.setupInput)(g);
    g.running = true;
    showMessage('DOOMED CORRIDORS', 3000);
    requestAnimationFrame(gameLoop);
}
function gameLoop(timestamp) {
    const dt = Math.min(50, timestamp - g.lastTime);
    g.lastTime = timestamp;
    if (g.running) {
        update(dt, timestamp);
    }
    renderFrame();
    requestAnimationFrame(gameLoop);
}
function update(dt, now) {
    g.levelTime += dt;
    // Handle weapon switch
    if (g._weaponSwitchTarget !== null) {
        (0, weapons_1.trySwitchWeapon)(g, ws, g._weaponSwitchTarget, now);
        g._weaponSwitchTarget = null;
    }
    (0, weapons_1.updateSwitch)(g, ws, now);
    (0, weapons_1.updateReload)(g, ws, now);
    // Mouse look
    const mouseSensitivity = 0.002;
    g.player.angle += g.mouse.dx * mouseSensitivity;
    g.player.pitch -= g.mouse.dy * mouseSensitivity * 0.7;
    g.player.pitch = Math.max(-0.6, Math.min(0.6, g.player.pitch));
    g.mouse.dx = 0;
    g.mouse.dy = 0;
    if (!g.player.alive) {
        g.player.deadTimer -= dt;
        const goOverlay = document.getElementById('game-over-overlay');
        const respawnTimer = document.getElementById('respawn-timer');
        if (goOverlay)
            goOverlay.classList.add('visible');
        if (respawnTimer)
            respawnTimer.textContent = `Respawning in ${Math.ceil(g.player.deadTimer / 1000)}...`;
        if (g.player.deadTimer <= 0) {
            (0, player_1.respawnPlayer)(g.player, 1.5, 1.5);
            if (goOverlay)
                goOverlay.classList.remove('visible');
            showMessage('RESPAWNED', 1500);
        }
        else if (g.player.deadTimer < 2800 && g.player.deadTimer > 2700) {
            (0, audio_1.playSound)('death');
        }
        return;
    }
    // Hide death overlay when alive
    const deathOverlay = document.getElementById('game-over-overlay');
    if (deathOverlay)
        deathOverlay.classList.remove('visible');
    if (g.won)
        return;
    // Movement
    const moveSpeed = g.keys.shift ? g.player.moveSpeed * 1.6 : g.player.moveSpeed;
    let moving = false;
    let moveX = 0, moveY = 0;
    if (g.keys.w) {
        moveX += Math.cos(g.player.angle);
        moveY += Math.sin(g.player.angle);
        moving = true;
    }
    if (g.keys.s) {
        moveX -= Math.cos(g.player.angle);
        moveY -= Math.sin(g.player.angle);
        moving = true;
    }
    if (g.keys.a) {
        moveX += Math.cos(g.player.angle - Math.PI / 2);
        moveY += Math.sin(g.player.angle - Math.PI / 2);
        moving = true;
    }
    if (g.keys.d) {
        moveX += Math.cos(g.player.angle + Math.PI / 2);
        moveY += Math.sin(g.player.angle + Math.PI / 2);
        moving = true;
    }
    // Normalize
    const mag = Math.sqrt(moveX * moveX + moveY * moveY);
    if (mag > 0) {
        moveX = (moveX / mag) * moveSpeed * (dt / 1000);
        moveY = (moveY / mag) * moveSpeed * (dt / 1000);
    }
    // Collision and movement
    const slideRadius = 0.2;
    // Try X movement
    const newX = g.player.x + moveX;
    let blockedX = false;
    for (let dy = -slideRadius; dy <= slideRadius; dy += slideRadius * 2) {
        const tileX = Math.floor(newX + dy);
        const tileY = Math.floor(g.player.y);
        if (tileX < 0 || tileX >= map_1.MAP_W || tileY < 0 || tileY >= map_1.MAP_H || map_1.MAP[tileY][tileX] > 0) {
            blockedX = true;
            break;
        }
    }
    if (!blockedX)
        g.player.x = newX;
    // Try Y movement
    const newY = g.player.y + moveY;
    let blockedY = false;
    for (let dx = -slideRadius; dx <= slideRadius; dx += slideRadius * 2) {
        const tileX = Math.floor(g.player.x + dx);
        const tileY = Math.floor(newY);
        if (tileX < 0 || tileX >= map_1.MAP_W || tileY < 0 || tileY >= map_1.MAP_H || map_1.MAP[tileY][tileX] > 0) {
            blockedY = true;
            break;
        }
    }
    if (!blockedY)
        g.player.y = newY;
    // Check exit tile (win condition)
    const pTileX = Math.floor(g.player.x);
    const pTileY = Math.floor(g.player.y);
    if (pTileX >= 0 && pTileX < map_1.MAP_W && pTileY >= 0 && pTileY < map_1.MAP_H && map_1.MAP[pTileY][pTileX] === 3) {
        if ((0, game_1.countAliveEnemies)(g) <= 0) {
            g.won = true;
            showMessage('YOU ESCAPED!\nAll enemies eliminated!\nTime: ' + Math.floor(g.levelTime / 1000) + 's', 999999);
            return;
        }
    }
    // Head bob
    if (moving) {
        g.player.bobPhase += dt * 0.008 * (g.keys.shift ? 1.3 : 1);
        g.player.bobAmount = Math.min(1, g.player.bobAmount + dt * 0.005);
    }
    else {
        g.player.bobAmount = Math.max(0, g.player.bobAmount - dt * 0.003);
    }
    // Muzzle flash
    g.player.muzzleFlash = Math.max(0, g.player.muzzleFlash - dt * 0.05);
    // Weapon anim
    if (g.weaponAnim > 0)
        g.weaponAnim = Math.max(0, g.weaponAnim - dt * 0.03);
    // Screen shake decay
    if (g.screenShake > 0) {
        g.screenShake *= 0.9;
        if (g.screenShake < 0.1)
            g.screenShake = 0;
    }
    // Manual reload on R
    if (g.keys.r && !ws.isReloading) {
        const w = weapons_1.WEAPONS[g.player.currentWeapon];
        // Give some ammo (from infinite reserve)
        const ammoType = w.ammoType;
        const maxAmmo = ammoType === 'pistol' ? 99 : ammoType === 'shotgun' ? 32 : 50;
        const reloadAmount = ammoType === 'pistol' ? 12 : ammoType === 'shotgun' ? 6 : 20;
        if (g.player.ammo[ammoType] < maxAmmo) {
            ws.isReloading = true;
            ws.reloadStart = now;
        }
    }
    // Mouse fire
    if (g.keys.mouse) {
        const w = weapons_1.WEAPONS[g.player.currentWeapon];
        if (w.autoFire || true) {
            (0, weapons_1.tryFire)(g, ws, now);
            if (!w.autoFire)
                g.keys.mouse = false;
        }
    }
    // Pickup check
    const pickupRange = 0.5;
    for (const p of g.pickups) {
        if (!p.active)
            continue;
        p.bobPhase += dt * 0.003;
        const dx = g.player.x - p.x;
        const dy = g.player.y - p.y;
        if (dx * dx + dy * dy < pickupRange * pickupRange) {
            p.active = false;
            (0, audio_1.playSound)('pickup');
            switch (p.type) {
                case 'health':
                    g.player.health = Math.min(g.player.maxHealth, g.player.health + 25);
                    break;
                case 'armor':
                    g.player.armor = Math.min(100, g.player.armor + 50);
                    break;
                case 'ammo_pistol':
                    g.player.ammo.pistol = Math.min(99, g.player.ammo.pistol + 12);
                    break;
                case 'ammo_shotgun':
                    g.player.ammo.shotgun = Math.min(32, g.player.ammo.shotgun + 6);
                    break;
                case 'ammo_rifle':
                    g.player.ammo.rifle = Math.min(50, g.player.ammo.rifle + 20);
                    break;
            }
        }
    }
    // Update enemies
    (0, ai_1.updateEnemies)(g, dt);
    // Update particles
    update: for (let i = g.particles.length - 1; i >= 0; i--) {
        const p = g.particles[i];
        p.x += p.vx;
        p.y += p.vy;
        p.z += p.vz;
        p.vz -= 0.0001;
        p.life -= dt;
        if (p.life <= 0) {
            g.particles.splice(i, 1);
        }
    }
    // Damage flash
    g.damageFlash = Math.max(0, g.damageFlash - dt);
    // Message timer
    if (g.messageTimer > 0) {
        g.messageTimer -= dt;
        if (g.messageTimer <= 0)
            clearMessage();
    }
    // Update HUD
    updateHUD();
    // Check win (all dead + at exit)
    if ((0, game_1.countAliveEnemies)(g) === 0 && !g.won && !g.allDeadMsgShown) {
        g.allDeadMsgShown = true;
        showMessage('ALL ENEMIES DEAD!\nFind the exit door!', 4000);
    }
}
function renderFrame() {
    const { ctx, width, height } = g;
    ctx.save();
    // Screen shake
    if (g.screenShake > 0) {
        const shakeX = (Math.random() - 0.5) * g.screenShake * 2;
        const shakeY = (Math.random() - 0.5) * g.screenShake * 2;
        ctx.translate(shakeX, shakeY);
        g.screenShake *= 0.85;
        if (g.screenShake < 0.1)
            g.screenShake = 0;
    }
    ctx.clearRect(-10, -10, width + 20, height + 20);
    // Render walls
    (0, renderer_1.render)(g);
    // Render sprites
    (0, sprites_1.renderSprites)(g);
    // Render particles
    renderParticles(g);
    // Render weapon
    (0, weapons_1.drawWeapon)(g, ws);
    // Damage overlay
    const dmgOverlay = document.getElementById('damage-overlay');
    if (g.damageFlash > 0) {
        dmgOverlay?.classList.add('hit');
    }
    else {
        dmgOverlay?.classList.remove('hit');
    }
    // Minimap
    drawMinimap(g);
    // Vignette
    drawVignette();
    ctx.restore();
}
function renderParticles(g) {
    const { ctx, width, height, player, zBuffer } = g;
    for (const p of g.particles) {
        const dx = p.x - player.x;
        const dy = p.y - player.y;
        const dist = Math.sqrt(dx * dx + dy * dy);
        let angle = Math.atan2(dy, dx) - player.angle;
        while (angle < -Math.PI)
            angle += 2 * Math.PI;
        while (angle > Math.PI)
            angle -= 2 * Math.PI;
        if (Math.abs(angle) > Math.PI / 2.2)
            continue;
        if (dist < 0.1)
            continue;
        const perpDist = dist * Math.cos(angle - player.angle);
        const screenX = width / 2 + (angle / (Math.PI / 3)) * width / 2;
        const screenY = height / 2 - (p.z - 0.3) * height / perpDist + player.pitch * height;
        const size = Math.max(1, p.size * height / perpDist / 100);
        const alpha = p.life / p.maxLife;
        const c = p.color.startsWith('#') ? p.color : p.color;
        ctx.globalAlpha = alpha;
        ctx.fillStyle = c;
        ctx.fillRect(screenX - size / 2, screenY - size / 2, size, size);
    }
    ctx.globalAlpha = 1;
}
function drawMinimap(g) {
    const { ctx, width, height, player } = g;
    const scale = 6;
    const mapPx = map_1.MAP_W * scale;
    const mapPy = map_1.MAP_H * scale;
    const offsetX = 10;
    const offsetY = 10;
    // Background
    ctx.fillStyle = 'rgba(0,0,0,0.6)';
    ctx.fillRect(offsetX - 2, offsetY - 2, mapPx + 4, mapPy + 4);
    // Walls
    for (let y = 0; y < map_1.MAP_H; y++) {
        for (let x = 0; x < map_1.MAP_W; x++) {
            if (map_1.MAP[y][x] === 0)
                continue;
            if (map_1.MAP[y][x] === 3)
                ctx.fillStyle = '#335533';
            else if (map_1.MAP[y][x] === 1)
                ctx.fillStyle = '#664433';
            else
                ctx.fillStyle = '#444455';
            ctx.fillRect(offsetX + x * scale, offsetY + y * scale, scale, scale);
        }
    }
    // Enemies
    for (const e of g.enemies) {
        if (e.state === 'dead')
            continue;
        ctx.fillStyle = e.type === 'boss' ? '#ff44ff' : e.type === 'soldier' ? '#4466ff' : '#ff4444';
        ctx.fillRect(offsetX + e.x * scale - 2, offsetY + e.y * scale - 2, 4, 4);
    }
    // Pickups
    for (const p of g.pickups) {
        if (!p.active)
            continue;
        ctx.fillStyle = p.type === 'health' ? '#ff3333' : p.type === 'armor' ? '#3366ff' : '#ffaa00';
        ctx.fillRect(offsetX + p.x * scale - 1, offsetY + p.y * scale - 1, 3, 3);
    }
    // Player
    ctx.fillStyle = '#00ff00';
    ctx.beginPath();
    ctx.arc(offsetX + player.x * scale, offsetY + player.y * scale, 3, 0, Math.PI * 2);
    ctx.fill();
    // Player direction
    ctx.strokeStyle = '#00ff00';
    ctx.lineWidth = 1;
    ctx.beginPath();
    ctx.moveTo(offsetX + player.x * scale, offsetY + player.y * scale);
    ctx.lineTo(offsetX + (player.x + Math.cos(player.angle) * 1.5) * scale, offsetY + (player.y + Math.sin(player.angle) * 1.5) * scale);
    ctx.stroke();
}
function updateHUD() {
    const p = g.player;
    const w = weapons_1.WEAPONS[p.currentWeapon];
    document.getElementById('health-display').textContent = `❤️ ${Math.ceil(p.health)}`;
    document.getElementById('armor-display').textContent = `🛡️ ${Math.ceil(p.armor)}`;
    document.getElementById('weapon-name').textContent = w.name + (ws.isReloading ? ' [RELOADING]' : '') + (ws.switching ? ' ...' : '');
    const ammoCount = p.ammo[w.ammoType] || 0;
    document.getElementById('ammo-display').textContent = `🔫 ${ammoCount}`;
    const alive = (0, game_1.countAliveEnemies)(g);
    document.getElementById('score-display').textContent = `💀 ${g.enemiesKilled} / ${g.totalEnemies}`;
    // Health color
    const healthEl = document.getElementById('health-display');
    if (p.health < 25)
        healthEl.style.color = '#ff0000';
    else if (p.health < 50)
        healthEl.style.color = '#ff8800';
    else
        healthEl.style.color = '#fff';
}
function showMessage(text, duration) {
    g.message = text;
    g.messageTimer = duration;
    const overlay = document.getElementById('message-overlay');
    overlay.textContent = text;
    overlay.classList.add('visible');
}
function clearMessage() {
    g.message = '';
    g.messageTimer = 0;
    const overlay = document.getElementById('message-overlay');
    overlay.classList.remove('visible');
}
function drawVignette() {
    const vignette = document.getElementById('vignette');
    if (!vignette)
        return;
    // Resize if needed
    if (vignette.width !== g.width || vignette.height !== g.height) {
        vignette.width = g.width;
        vignette.height = g.height;
    }
    const vCtx = vignette.getContext('2d');
    vCtx.clearRect(0, 0, vignette.width, vignette.height);
    // Vignette gradient
    const cx = vignette.width / 2;
    const cy = vignette.height / 2;
    const radius = Math.max(vignette.width, vignette.height) * 0.7;
    const grad = vCtx.createRadialGradient(cx, cy, radius * 0.4, cx, cy, radius);
    grad.addColorStop(0, 'rgba(0,0,0,0)');
    grad.addColorStop(0.6, 'rgba(0,0,0,0)');
    grad.addColorStop(1, 'rgba(0,0,0,0.6)');
    vCtx.fillStyle = grad;
    vCtx.fillRect(0, 0, vignette.width, vignette.height);
    // Red flash when low health
    if (g.player.health < 30 && g.player.alive) {
        const intensity = (1 - g.player.health / 30) * 0.3;
        const pulse = Math.sin(g.levelTime * 0.005) * 0.5 + 0.5;
        vCtx.fillStyle = `rgba(255,0,0,${intensity * pulse * 0.3})`;
        vCtx.fillRect(0, 0, vignette.width, vignette.height);
    }
}
start();
};

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

// ── module: src/map.ts ──
__mods["src/map.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.CEILING_COLOR = exports.FLOOR_COLOR = exports.WALL_COLORS = exports.MAP_H = exports.MAP_W = exports.MAP = exports.TILE = void 0;
// Tile types
exports.TILE = {
    EMPTY: 0,
    WALL: 1,
    WALL_DARK: 2,
    WALL_EXIT: 3,
};
// 24x18 dungeon map - more rooms and corridors
exports.MAP = [
    [2, 2, 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, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2],
    [2, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2],
    [2, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 2],
    [2, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 2],
    [2, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 2],
    [2, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 2],
    [2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 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, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2],
    [2, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 2],
    [2, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 2],
    [2, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 2],
    [2, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 2],
    [2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 2],
    [2, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 1, 0, 0, 2],
    [2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 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, 0, 0, 0, 0, 0, 0, 0, 3, 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, 2, 2],
];
exports.MAP_W = exports.MAP[0].length;
exports.MAP_H = exports.MAP.length;
// Wall colors per tile type [bright, dark]
exports.WALL_COLORS = {
    1: ['#885533', '#553322'], // brown brick
    2: ['#444455', '#222233'], // dark gray stone
    3: ['#335533', '#223322'], // green exit
};
// Floor/ceiling colors
exports.FLOOR_COLOR = '#333340';
exports.CEILING_COLOR = '#222228';
};

// ── module: src/player.ts ──
__mods["src/player.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.createPlayer = createPlayer;
exports.respawnPlayer = respawnPlayer;
exports.takeDamage = takeDamage;
function createPlayer() {
    return {
        x: 1.5,
        y: 1.5,
        angle: 0,
        pitch: 0,
        health: 100,
        maxHealth: 100,
        armor: 0,
        ammo: { pistol: 20, shotgun: 8, rifle: 0 },
        alive: true,
        deadTimer: 0,
        speed: 0,
        moveSpeed: 3.5,
        rotSpeed: 2.0,
        bobPhase: 0,
        bobAmount: 0,
        muzzleFlash: 0,
        weaponSwitching: false,
        currentWeapon: 0,
        keys: [],
    };
}
function respawnPlayer(p, x, y) {
    p.x = x;
    p.y = y;
    p.health = 100;
    p.armor = 0;
    p.alive = true;
    p.deadTimer = 0;
    p.ammo.pistol = 20;
    p.ammo.shotgun = 8;
    p.ammo.rifle = 12;
    p.currentWeapon = 0;
    p.speed = 0;
}
function takeDamage(p, amount) {
    if (!p.alive)
        return 0;
    let remaining = amount;
    if (p.armor > 0) {
        const armorAbsorb = Math.min(p.armor, remaining * 0.7);
        p.armor -= armorAbsorb;
        remaining -= armorAbsorb * 0.5;
    }
    p.health -= remaining;
    if (p.health <= 0) {
        p.health = 0;
        p.alive = false;
        p.deadTimer = 3000;
    }
    return amount;
}
};

// ── module: src/game.ts ──
__mods["src/game.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.createGame = createGame;
exports.countAliveEnemies = countAliveEnemies;
const player_1 = require("./player");
function spawnEnemies() {
    const enemies = [];
    const spawns = [
        // Grunts (front rooms)
        { x: 4.5, y: 2.5, type: 'grunt' },
        { x: 10.5, y: 2.5, type: 'grunt' },
        { x: 17.5, y: 2.5, type: 'grunt' },
        // Grunts (middle)
        { x: 4.5, y: 5.5, type: 'grunt' },
        { x: 12.5, y: 4.5, type: 'grunt' },
        { x: 20.5, y: 5.5, type: 'grunt' },
        // Soldiers (back rooms)
        { x: 8.5, y: 10.5, type: 'soldier' },
        { x: 14.5, y: 11.5, type: 'soldier' },
        { x: 4.5, y: 12.5, type: 'soldier' },
        // Soldiers (late game)
        { x: 10.5, y: 15.5, type: 'soldier' },
        { x: 19.5, y: 14.5, type: 'soldier' },
        // Boss (near exit)
        { x: 20.5, y: 16.5, type: 'boss' },
        // Extra grunt
        { x: 15.5, y: 8.5, type: 'grunt' },
    ];
    let id = 0;
    for (const s of spawns) {
        const base = s.type === 'grunt'
            ? { health: 30, maxHealth: 30, speed: 1.2, damage: 8, attackCooldown: 1000, attackRange: 1.5 }
            : s.type === 'soldier'
                ? { health: 60, maxHealth: 60, speed: 1.0, damage: 15, attackCooldown: 800, attackRange: 3.0 }
                : { health: 150, maxHealth: 150, speed: 0.8, damage: 25, attackCooldown: 600, attackRange: 2.0 };
        enemies.push({
            id: id++, x: s.x, y: s.y, type: s.type,
            ...base,
            state: 'patrol',
            animFrame: 0, animTimer: 0, hitTimer: 0,
            patrolAngle: Math.random() * Math.PI * 2,
            patrolTimer: Math.random() * 3000,
            z: 0,
        });
    }
    return enemies;
}
function spawnPickups() {
    return [
        { x: 1.5, y: 1.5, type: 'ammo_pistol', active: true, bobPhase: 0 },
        { x: 3.5, y: 2.5, type: 'health', active: true, bobPhase: 0 },
        { x: 6.5, y: 2.5, type: 'ammo_shotgun', active: true, bobPhase: Math.PI },
        { x: 11.5, y: 2.5, type: 'health', active: true, bobPhase: 0 },
        { x: 16.5, y: 2.5, type: 'ammo_pistol', active: true, bobPhase: Math.PI },
        { x: 3.5, y: 7.5, type: 'armor', active: true, bobPhase: 0 },
        { x: 8.5, y: 7.5, type: 'ammo_shotgun', active: true, bobPhase: Math.PI },
        { x: 15.5, y: 7.5, type: 'health', active: true, bobPhase: 0 },
        { x: 21.5, y: 7.5, type: 'ammo_pistol', active: true, bobPhase: Math.PI },
        { x: 5.5, y: 11.5, type: 'ammo_rifle', active: true, bobPhase: 0 },
        { x: 13.5, y: 11.5, type: 'health', active: true, bobPhase: Math.PI },
        { x: 20.5, y: 12.5, type: 'armor', active: true, bobPhase: 0 },
        { x: 8.5, y: 15.5, type: 'ammo_shotgun', active: true, bobPhase: Math.PI },
        { x: 15.5, y: 15.5, type: 'health', active: true, bobPhase: 0 },
        { x: 20.5, y: 15.5, type: 'ammo_rifle', active: true, bobPhase: Math.PI },
    ];
}
function createGame(canvas, ctx, width, height) {
    const textureCanvas = document.createElement('canvas');
    textureCanvas.width = 64;
    textureCanvas.height = 64;
    const textureCtx = textureCanvas.getContext('2d');
    return {
        player: (0, player_1.createPlayer)(),
        enemies: spawnEnemies(),
        pickups: spawnPickups(),
        particles: [],
        running: false,
        won: false,
        totalEnemies: 13,
        enemiesKilled: 0,
        message: '',
        messageTimer: 0,
        levelTime: 0,
        damageFlash: 0,
        reloading: 0,
        weaponAnim: 0,
        lastTime: performance.now(),
        keys: { w: false, a: false, s: false, d: false, shift: false, mouse: false, r: false },
        mouse: { x: 0, y: 0, dx: 0, dy: 0 },
        canvas, ctx, width, height,
        textureCanvas, textureCtx,
        zBuffer: new Array(width),
        minimapScale: 4,
        pointerLocked: false,
        _weaponSwitchTarget: null,
        allDeadMsgShown: false,
        screenShake: 0,
    };
}
function countAliveEnemies(g) {
    return g.enemies.filter(e => e.state !== 'dead').length;
}
};

// ── module: src/renderer.ts ──
__mods["src/renderer.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.render = render;
const map_1 = require("./map");
const FOV = Math.PI / 3;
const HALF_FOV = FOV / 2;
const MAX_DEPTH = 22;
function render(g) {
    const { player, ctx, width, height, zBuffer } = g;
    const numRays = Math.min(width, 640);
    const stripW = width / numRays;
    for (let i = 0; i < numRays; i++) {
        const rayAngle = player.angle - HALF_FOV + (i / numRays) * FOV;
        const sin = Math.sin(rayAngle);
        const cos = Math.cos(rayAngle);
        // DDA
        let mapX = Math.floor(player.x);
        let mapY = Math.floor(player.y);
        const deltaDistX = Math.abs(1 / cos) || 1e10;
        const deltaDistY = Math.abs(1 / sin) || 1e10;
        let stepX, stepY;
        let sideDistX, sideDistY;
        if (cos < 0) {
            stepX = -1;
            sideDistX = (player.x - mapX) * deltaDistX;
        }
        else {
            stepX = 1;
            sideDistX = (mapX + 1 - player.x) * deltaDistX;
        }
        if (sin < 0) {
            stepY = -1;
            sideDistY = (player.y - mapY) * deltaDistY;
        }
        else {
            stepY = 1;
            sideDistY = (mapY + 1 - player.y) * deltaDistY;
        }
        let side = 0;
        let dist = MAX_DEPTH;
        let tileType = 0;
        let hit = false;
        for (let step = 0; step < 64; step++) {
            if (sideDistX < sideDistY) {
                sideDistX += deltaDistX;
                mapX += stepX;
                side = 0;
            }
            else {
                sideDistY += deltaDistY;
                mapY += stepY;
                side = 1;
            }
            if (mapX < 0 || mapX >= map_1.MAP_W || mapY < 0 || mapY >= map_1.MAP_H)
                break;
            if (map_1.MAP[mapY][mapX] > 0) {
                hit = true;
                tileType = map_1.MAP[mapY][mapX];
                break;
            }
        }
        if (hit) {
            dist = side === 0
                ? (mapX - player.x + (1 - stepX) / 2) / cos
                : (mapY - player.y + (1 - stepY) / 2) / sin;
        }
        const perpDist = dist * Math.cos(rayAngle - player.angle);
        zBuffer[i] = perpDist;
        const wallHeight = Math.min(height, height / perpDist);
        const pitchOffset = player.pitch * height;
        const drawStart = Math.floor((height - wallHeight) / 2 + pitchOffset);
        const drawEnd = Math.min(height, Math.floor((height + wallHeight) / 2 + pitchOffset));
        // Wall X for texture
        let wallX;
        if (side === 0)
            wallX = player.y + dist * sin;
        else
            wallX = player.x + dist * cos;
        wallX = ((wallX % 1) + 1) % 1;
        // Fog
        const fog = Math.min(0.8, perpDist / MAX_DEPTH);
        // Draw ceiling as single gradient strip
        if (drawStart > 0) {
            const cGrad = ctx.createLinearGradient(0, 0, 0, drawStart);
            const cFog = Math.min(0.5, perpDist / MAX_DEPTH);
            cGrad.addColorStop(0, `rgb(${Math.floor(15 * (1 - cFog))},${Math.floor(15 * (1 - cFog))},${Math.floor(20 * (1 - cFog))})`);
            cGrad.addColorStop(1, `rgb(${Math.floor(22 * (1 - cFog))},${Math.floor(22 * (1 - cFog))},${Math.floor(28 * (1 - cFog))})`);
            ctx.fillStyle = cGrad;
            ctx.fillRect(Math.floor(i * stripW), 0, Math.ceil(stripW), drawStart);
        }
        // Draw wall strip
        for (let y = drawStart; y < drawEnd; y++) {
            const d = (y - (height - wallHeight) / 2 - pitchOffset) / wallHeight;
            const texY = Math.max(0, Math.min(63, Math.floor(d * 64)));
            const texX = Math.floor(wallX * 64);
            let r, gr, b;
            if (tileType === 1) {
                // Brick wall with mortar lines
                const brickH = 8;
                const brickW = 16;
                const row = Math.floor(texY / brickH);
                const brickX = ((texX + (row % 2) * 8) % brickW);
                const brickY = texY % brickH;
                const isMortar = brickX < 1 || brickY < 1;
                if (isMortar) {
                    r = 100;
                    gr = 90;
                    b = 80;
                }
                else {
                    const variation = ((texX * 3 + row * 7) % 15) - 7;
                    r = 136 + variation;
                    gr = 85 + variation / 2;
                    b = 51;
                }
                // Slight horizontal line highlight at top
                if (texY < 2) {
                    r += 10;
                    gr += 10;
                    b += 10;
                }
            }
            else if (tileType === 2) {
                // Stone wall
                const n = ((texX * 7 + texY * 13) % 31) / 31;
                const n2 = ((texX * 13 + texY * 7) % 17) / 17;
                r = 68 + n * 20 + n2 * 10;
                gr = 68 + n * 18;
                b = 85 + n * 15;
            }
            else if (tileType === 3) {
                // Exit door - glowing
                const isFrame = texX < 4 || texX > 60 || texY < 4 || texY > 60;
                if (isFrame) {
                    r = 40;
                    gr = 70;
                    b = 40;
                }
                else {
                    const stripeX = Math.floor(texX / 8);
                    const pulse = Math.sin(g.levelTime * 0.003) * 30 + 30;
                    r = 180 + pulse;
                    gr = 220 + pulse * 0.5;
                    b = 30;
                }
            }
            else {
                r = 128;
                gr = 128;
                b = 128;
            }
            if (side === 1) {
                r = Math.floor(r * 0.65);
                gr = Math.floor(gr * 0.65);
                b = Math.floor(b * 0.65);
            }
            r = Math.floor(r * (1 - fog));
            gr = Math.floor(gr * (1 - fog));
            b = Math.floor(b * (1 - fog));
            ctx.fillStyle = `rgb(${r},${gr},${b})`;
            ctx.fillRect(Math.floor(i * stripW), y, Math.ceil(stripW), 1);
        }
        // Draw floor as gradient strip
        if (drawEnd < height) {
            const fGrad = ctx.createLinearGradient(0, drawEnd, 0, height);
            const fFog = Math.min(0.5, perpDist / MAX_DEPTH);
            fGrad.addColorStop(0, `rgb(${Math.floor(51 * (1 - fFog))},${Math.floor(51 * (1 - fFog))},${Math.floor(64 * (1 - fFog))})`);
            fGrad.addColorStop(1, `rgb(${Math.floor(30 * (1 - fFog))},${Math.floor(30 * (1 - fFog))},${Math.floor(38 * (1 - fFog))})`);
            ctx.fillStyle = fGrad;
            ctx.fillRect(Math.floor(i * stripW), drawEnd, Math.ceil(stripW), height - drawEnd);
        }
    }
}
};

// ── module: src/sprites.ts ──
__mods["src/sprites.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.renderSprites = renderSprites;
// Draw a sprite (enemy or pickup) using billboard technique
function drawSprite(g, spriteX, spriteY, size, color, fog) {
    const { ctx, width, height, player, zBuffer } = g;
    const dx = spriteX - player.x;
    const dy = spriteY - player.y;
    const dist = Math.sqrt(dx * dx + dy * dy);
    // Angle from player
    let spriteAngle = Math.atan2(dy, dx) - player.angle;
    while (spriteAngle < -Math.PI)
        spriteAngle += 2 * Math.PI;
    while (spriteAngle > Math.PI)
        spriteAngle -= 2 * Math.PI;
    // Off screen?
    if (Math.abs(spriteAngle) > Math.PI / 2.2)
        return;
    const perpDist = dist * Math.cos(spriteAngle);
    if (perpDist < 0.1)
        return;
    const screenX = width / 2 + (spriteAngle / (Math.PI / 3)) * width / 2;
    const spriteHeight = Math.min(height, Math.floor(height / perpDist * size));
    const spriteWidth = spriteHeight;
    const pitchOffset = player.pitch * height;
    const drawStart = Math.floor((height - spriteHeight) / 2 + pitchOffset);
    const drawEnd = Math.floor((height + spriteHeight) / 2 + pitchOffset);
    const startStripe = Math.floor(screenX - spriteWidth / 2);
    const endStripe = Math.floor(screenX + spriteWidth / 2);
    const fogFactor = Math.min(0.7, perpDist / 20);
    // Parse color
    const c = parseColor(color);
    const r = Math.floor(c.r * (1 - fogFactor));
    const gr = Math.floor(c.g * (1 - fogFactor));
    const b = Math.floor(c.b * (1 - fogFactor));
    // Draw sprite with z-buffer check
    const stride = g.width / Math.min(g.width, 640);
    for (let stripe = startStripe; stripe < endStripe; stripe++) {
        if (stripe < 0 || stripe >= width)
            continue;
        const rayIdx = Math.floor(stripe / stride);
        if (rayIdx < 0 || rayIdx >= zBuffer.length)
            continue;
        if (zBuffer[rayIdx] < perpDist)
            continue;
        // Position within sprite
        const spriteNX = (stripe - startStripe) / (endStripe - startStripe);
        for (let y = drawStart; y < drawEnd; y++) {
            const spriteNY = (y - drawStart) / (drawEnd - drawStart);
            // Simple circle shape
            const cx = spriteNX - 0.5;
            const cy = spriteNY - 0.5;
            const d = Math.sqrt(cx * cx + cy * cy);
            if (d > 0.5)
                continue;
            ctx.fillStyle = `rgb(${r},${gr},${b})`;
            ctx.fillRect(stripe, y, Math.ceil(stride), 1);
        }
    }
}
function parseColor(color) {
    // Simple hex color parser
    if (color.startsWith('#')) {
        const v = parseInt(color.slice(1), 16);
        return { r: (v >> 16) & 255, g: (v >> 8) & 255, b: v & 255 };
    }
    const m = color.match(/(\d+)/g);
    if (m && m.length >= 3)
        return { r: +m[0], g: +m[1], b: +m[2] };
    return { r: 255, g: 0, b: 0 };
}
function renderSprites(g) {
    const sprites = [];
    for (const enemy of g.enemies) {
        if (enemy.state === 'dead')
            continue;
        const dx = enemy.x - g.player.x;
        const dy = enemy.y - g.player.y;
        sprites.push({
            dist: dx * dx + dy * dy,
            render: () => renderEnemy(g, enemy),
        });
    }
    for (const pickup of g.pickups) {
        if (!pickup.active)
            continue;
        const dx = pickup.x - g.player.x;
        const dy = pickup.y - g.player.y;
        sprites.push({
            dist: dx * dx + dy * dy,
            render: () => renderPickup(g, pickup),
        });
    }
    // Back to front
    sprites.sort((a, b) => b.dist - a.dist);
    for (const s of sprites)
        s.render();
}
function renderEnemy(g, e) {
    const { ctx, width, height, player, zBuffer } = g;
    const dx = e.x - player.x;
    const dy = e.y - player.y;
    const dist = Math.sqrt(dx * dx + dy * dy);
    let spriteAngle = Math.atan2(dy, dx) - player.angle;
    while (spriteAngle < -Math.PI)
        spriteAngle += 2 * Math.PI;
    while (spriteAngle > Math.PI)
        spriteAngle -= 2 * Math.PI;
    if (Math.abs(spriteAngle) > Math.PI / 2.2)
        return;
    const perpDist = dist * Math.cos(spriteAngle);
    if (perpDist < 0.1)
        return;
    const screenX = width / 2 + (spriteAngle / (Math.PI / 3)) * width / 2;
    const scale = e.type === 'boss' ? 1.4 : e.type === 'soldier' ? 1.1 : 1.0;
    const spriteHeight = Math.min(height, Math.floor(height / perpDist * 0.8 * scale));
    const spriteWidth = spriteHeight * 0.6;
    const pitchOffset = player.pitch * height;
    const drawStart = Math.floor((height - spriteHeight * 0.8) / 2 + pitchOffset - spriteHeight * 0.1);
    const drawEnd = Math.floor((height + spriteHeight * 0.8) / 2 + pitchOffset + spriteHeight * 0.2);
    const startStripe = Math.floor(screenX - spriteWidth / 2);
    const endStripe = Math.floor(screenX + spriteWidth / 2);
    const fogFactor = Math.min(0.7, perpDist / 20);
    const hitFlash = e.hitTimer > 0;
    // Colors per type
    let bodyColor, eyeColor;
    if (e.type === 'grunt') {
        bodyColor = hitFlash ? '#ffffff' : '#aa4444';
        eyeColor = '#ffff00';
    }
    else if (e.type === 'soldier') {
        bodyColor = hitFlash ? '#ffffff' : '#4466aa';
        eyeColor = '#ff4400';
    }
    else {
        bodyColor = hitFlash ? '#ffffff' : '#884488';
        eyeColor = '#ff0000';
    }
    const c = parseColor(bodyColor);
    const ec = parseColor(eyeColor);
    const r = Math.floor(c.r * (1 - fogFactor));
    const gr = Math.floor(c.g * (1 - fogFactor));
    const b = Math.floor(c.b * (1 - fogFactor));
    const er = Math.floor(ec.r * (1 - fogFactor));
    const eg = Math.floor(ec.g * (1 - fogFactor));
    const eb = Math.floor(ec.b * (1 - fogFactor));
    const stride = g.width / Math.min(g.width, 640);
    for (let stripe = startStripe; stripe < endStripe; stripe++) {
        if (stripe < 0 || stripe >= width)
            continue;
        const rayIdx = Math.floor(stripe / stride);
        if (rayIdx < 0 || rayIdx >= g.zBuffer.length)
            continue;
        if (g.zBuffer[rayIdx] < perpDist)
            continue;
        const spriteNX = (stripe - startStripe) / (endStripe - startStripe);
        for (let y = drawStart; y < drawEnd; y++) {
            const spriteNY = (y - drawStart) / (drawEnd - drawStart);
            // Head region (top 25%)
            const headTop = 0;
            const headBottom = 0.28;
            // Body region (28% to 70%)
            const bodyTop = 0.25;
            const bodyBottom = 0.72;
            // Legs (70% to 100%)
            const cx = spriteNX - 0.5;
            // Draw body as a rough shape
            let fill = false;
            if (spriteNY >= headTop && spriteNY < headBottom) {
                // Head - circular
                const headCX = cx;
                const headCY = spriteNY - 0.14;
                const headR = 0.28;
                if (headCX * headCX + headCY * headCY < headR * headR)
                    fill = true;
            }
            if (spriteNY >= bodyTop && spriteNY < bodyBottom) {
                const bodyW = 0.35 + (spriteNY - bodyTop) * 0.15;
                if (Math.abs(cx) < bodyW)
                    fill = true;
            }
            if (spriteNY >= bodyBottom) {
                // Legs - split
                const legSpread = 0.12 + (spriteNY - bodyBottom) * 0.3;
                if (Math.abs(cx) < legSpread + 0.06)
                    fill = true;
            }
            if (!fill)
                continue;
            // Eyes
            if (spriteNY > 0.12 && spriteNY < 0.2) {
                const eyeW = 0.06;
                const eyeLX = -0.12, eyeRX = 0.12;
                if (Math.abs(cx - eyeLX) < eyeW || Math.abs(cx - eyeRX) < eyeW) {
                    ctx.fillStyle = `rgb(${er},${eg},${eb})`;
                    ctx.fillRect(stripe, y, Math.ceil(stride), 1);
                    continue;
                }
            }
            // Mouth for boss
            if (e.type === 'boss' && spriteNY > 0.22 && spriteNY < 0.26 && Math.abs(cx) < 0.1) {
                ctx.fillStyle = `rgb(${Math.floor(200 * (1 - fogFactor))},${Math.floor(200 * (1 - fogFactor))},${Math.floor(200 * (1 - fogFactor))})`;
                ctx.fillRect(stripe, y, Math.ceil(stride), 1);
                continue;
            }
            // Body color
            // Add some shading
            const shade = 0.85 + Math.abs(cx) * 0.3;
            ctx.fillStyle = `rgb(${Math.floor(r * shade)},${Math.floor(gr * shade)},${Math.floor(b * shade)})`;
            ctx.fillRect(stripe, y, Math.ceil(stride), 1);
        }
    }
    // Health bar above enemy
    if (e.health < e.maxHealth && perpDist < 12) {
        const barWidth = spriteWidth * 0.8;
        const barHeight = Math.max(3, spriteHeight * 0.03);
        const barY = drawStart - barHeight - 4;
        const healthPct = e.health / e.maxHealth;
        if (barY > 0) {
            ctx.fillStyle = '#333';
            ctx.fillRect(Math.floor(screenX - barWidth / 2), barY, barWidth, barHeight);
            ctx.fillStyle = healthPct > 0.5 ? '#0f0' : healthPct > 0.25 ? '#ff0' : '#f00';
            ctx.fillRect(Math.floor(screenX - barWidth / 2), barY, Math.floor(barWidth * healthPct), barHeight);
        }
    }
}
function renderPickup(g, p) {
    const { ctx, width, height, player, zBuffer } = g;
    const dx = p.x - player.x;
    const dy = p.y - player.y;
    const dist = Math.sqrt(dx * dx + dy * dy);
    let spriteAngle = Math.atan2(dy, dx) - player.angle;
    while (spriteAngle < -Math.PI)
        spriteAngle += 2 * Math.PI;
    while (spriteAngle > Math.PI)
        spriteAngle -= 2 * Math.PI;
    if (Math.abs(spriteAngle) > Math.PI / 2.2)
        return;
    const perpDist = dist * Math.cos(spriteAngle);
    if (perpDist < 0.1)
        return;
    const screenX = width / 2 + (spriteAngle / (Math.PI / 3)) * width / 2;
    const bob = Math.sin(p.bobPhase) * 4;
    const spriteHeight = Math.floor(height / perpDist * 0.25);
    const spriteWidth = spriteHeight * 0.7;
    const pitchOffset = player.pitch * height;
    const drawStart = Math.floor((height) / 2 + pitchOffset - spriteHeight / 2 + bob);
    const drawEnd = Math.floor((height) / 2 + pitchOffset + spriteHeight / 2 + bob);
    const startStripe = Math.floor(screenX - spriteWidth / 2);
    const endStripe = Math.floor(screenX + spriteWidth / 2);
    const fogFactor = Math.min(0.7, perpDist / 20);
    let color;
    switch (p.type) {
        case 'health':
            color = '#ff3333';
            break;
        case 'armor':
            color = '#3366ff';
            break;
        case 'ammo_pistol':
            color = '#ffaa00';
            break;
        case 'ammo_shotgun':
            color = '#aa6600';
            break;
        case 'ammo_rifle':
            color = '#00cc00';
            break;
        default: color = '#ffff00';
    }
    const c = parseColor(color);
    const r = Math.floor(c.r * (1 - fogFactor));
    const gr = Math.floor(c.g * (1 - fogFactor));
    const b = Math.floor(c.b * (1 - fogFactor));
    const stride = g.width / Math.min(g.width, 640);
    // Glow effect
    const glowStart = startStripe - 2;
    const glowEnd = endStripe + 2;
    const glowHeight = (drawEnd - drawStart) + 2;
    for (let stripe = glowStart; stripe < glowEnd; stripe++) {
        if (stripe < 0 || stripe >= width)
            continue;
        const rayIdx = Math.floor(stripe / stride);
        if (rayIdx < 0 || rayIdx >= zBuffer.length)
            continue;
        if (zBuffer[rayIdx] < perpDist)
            continue;
        const spriteNX = (stripe - glowStart) / (glowEnd - glowStart);
        for (let y = drawStart - 1; y < drawEnd + 1; y++) {
            const spriteNY = (y - drawStart + 1) / glowHeight;
            const cx = spriteNX - 0.5;
            const cy = spriteNY - 0.5;
            const d = Math.sqrt(cx * cx + cy * cy);
            if (d > 0.5)
                continue;
            const alpha = 1 - d / 0.5;
            ctx.fillStyle = `rgba(${r},${gr},${b},${alpha * 0.5})`;
            ctx.fillRect(stripe, y, Math.ceil(stride), 1);
        }
    }
    // Core
    for (let stripe = startStripe; stripe < endStripe; stripe++) {
        if (stripe < 0 || stripe >= width)
            continue;
        const rayIdx = Math.floor(stripe / stride);
        if (rayIdx < 0 || rayIdx >= zBuffer.length)
            continue;
        if (zBuffer[rayIdx] < perpDist)
            continue;
        const spriteNX = (stripe - startStripe) / (endStripe - startStripe);
        for (let y = drawStart; y < drawEnd; y++) {
            const spriteNY = (y - drawStart) / (drawEnd - drawStart);
            const cx = spriteNX - 0.5;
            const cy = spriteNY - 0.5;
            const d = Math.sqrt(cx * cx + cy * cy);
            if (d > 0.45)
                continue;
            ctx.fillStyle = `rgb(${r},${gr},${b})`;
            ctx.fillRect(stripe, y, Math.ceil(stride), 1);
        }
    }
}
};

// ── module: src/weapons.ts ──
__mods["src/weapons.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.WEAPONS = void 0;
exports.createWeaponState = createWeaponState;
exports.tryFire = tryFire;
exports.tryReload = tryReload;
exports.updateReload = updateReload;
exports.trySwitchWeapon = trySwitchWeapon;
exports.updateSwitch = updateSwitch;
exports.drawWeapon = drawWeapon;
const map_1 = require("./map");
const audio_1 = require("./audio");
exports.WEAPONS = [
    {
        name: 'PISTOL',
        ammoType: 'pistol',
        damage: 15,
        fireRate: 400,
        spread: 0.02,
        pellets: 1,
        ammoPerShot: 1,
        reloadTime: 1000,
        maxClip: 99,
        melee: false,
        autoFire: false,
        color: '#ffaa00',
        size: 0.15,
    },
    {
        name: 'SHOTGUN',
        ammoType: 'shotgun',
        damage: 8,
        fireRate: 900,
        spread: 0.08,
        pellets: 5,
        ammoPerShot: 1,
        reloadTime: 1500,
        maxClip: 32,
        melee: false,
        autoFire: false,
        color: '#aa6600',
        size: 0.2,
    },
    {
        name: 'LASER RIFLE',
        ammoType: 'rifle',
        damage: 25,
        fireRate: 150,
        spread: 0.03,
        pellets: 1,
        ammoPerShot: 1,
        reloadTime: 2000,
        maxClip: 50,
        melee: false,
        autoFire: true,
        color: '#00ff00',
        size: 0.18,
    },
];
function createWeaponState() {
    return {
        lastShot: 0,
        lastSwitch: 0,
        switching: false,
        switchTarget: 0,
        isReloading: false,
        reloadStart: 0,
    };
}
function tryFire(g, ws, now) {
    const p = g.player;
    if (!p.alive || ws.switching || ws.isReloading)
        return false;
    const w = exports.WEAPONS[p.currentWeapon];
    const ammoCount = p.ammo[w.ammoType] || 0;
    if (now - ws.lastShot < w.fireRate)
        return false;
    if (ammoCount < w.ammoPerShot) {
        tryReload(g, ws, now);
        return false;
    }
    ws.lastShot = now;
    p.ammo[w.ammoType] -= w.ammoPerShot;
    p.muzzleFlash = 100;
    g.weaponAnim = 5;
    // Sound
    if (w.ammoType === 'pistol')
        (0, audio_1.playSound)('pistol');
    else if (w.ammoType === 'shotgun')
        (0, audio_1.playSound)('shotgun');
    else
        (0, audio_1.playSound)('rifle');
    // Fire pellets
    g.screenShake = w.ammoType === 'shotgun' ? 6 : w.ammoType === 'rifle' ? 2 : 3;
    for (let i = 0; i < w.pellets; i++) {
        const spread = (Math.random() - 0.5) * w.spread;
        const angle = p.angle + spread;
        // Raycast for hit
        fireBullet(g, angle, w.damage, w.ammoType);
    }
    return true;
}
function fireBullet(g, angle, damage, ammoType) {
    const p = g.player;
    const sin = Math.sin(angle);
    const cos = Math.cos(angle);
    // Check enemies in order of distance
    let closestHit = null;
    let closestDist = Infinity;
    for (const e of g.enemies) {
        if (e.state === 'dead')
            continue;
        const dx = e.x - p.x;
        const dy = e.y - p.y;
        const dist = Math.sqrt(dx * dx + dy * dy);
        if (dist >= closestDist)
            continue;
        // Angle to enemy
        let eAngle = Math.atan2(dy, dx) - angle;
        while (eAngle < -Math.PI)
            eAngle += 2 * Math.PI;
        while (eAngle > Math.PI)
            eAngle -= 2 * Math.PI;
        // Enemy hit radius (approximate)
        const hitRadius = e.type === 'boss' ? 0.5 : 0.35;
        if (Math.abs(eAngle) < hitRadius / dist) {
            // Check wall occlusion
            if (!wallBetween(p.x, p.y, e.x, e.y)) {
                closestHit = e;
                closestDist = dist;
            }
        }
    }
    if (closestHit) {
        closestHit.health -= damage;
        closestHit.hitTimer = 150;
        closestHit.state = 'chase';
        (0, audio_1.playSound)('hit');
        // Blood particles
        for (let i = 0; i < 3; i++) {
            g.particles.push({
                x: closestHit.x + (Math.random() - 0.5) * 0.3,
                y: closestHit.y + (Math.random() - 0.5) * 0.3,
                z: 0.3 + Math.random() * 0.4,
                vx: (Math.random() - 0.5) * 0.02,
                vy: (Math.random() - 0.5) * 0.02,
                vz: Math.random() * 0.02,
                life: 500,
                maxLife: 500,
                color: '#ff0000',
                size: 2 + Math.random() * 2,
            });
        }
        if (closestHit.health <= 0) {
            closestHit.state = 'dead';
            g.enemiesKilled++;
            (0, audio_1.playSound)('kill');
            // Death particles
            for (let i = 0; i < 8; i++) {
                g.particles.push({
                    x: closestHit.x + (Math.random() - 0.5) * 0.4,
                    y: closestHit.y + (Math.random() - 0.5) * 0.4,
                    z: Math.random() * 0.5,
                    vx: (Math.random() - 0.5) * 0.04,
                    vy: (Math.random() - 0.5) * 0.04,
                    vz: Math.random() * 0.03,
                    life: 800,
                    maxLife: 800,
                    color: closestHit.type === 'boss' ? '#ff00ff' : '#ff4400',
                    size: 3 + Math.random() * 3,
                });
            }
        }
    }
}
function wallBetween(x1, y1, x2, y2) {
    const dist = Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
    const steps = Math.ceil(dist * 10);
    for (let i = 1; i < steps; i++) {
        const t = i / steps;
        const cx = x1 + (x2 - x1) * t;
        const cy = y1 + (y2 - y1) * t;
        const mx = Math.floor(cx);
        const my = Math.floor(cy);
        if (mx >= 0 && mx < map_1.MAP_W && my >= 0 && my < map_1.MAP_H && map_1.MAP[my][mx] > 0) {
            return true;
        }
    }
    return false;
}
function tryReload(g, ws, now) {
    const p = g.player;
    if (ws.isReloading || ws.switching || !p.alive)
        return;
    if ((p.ammo[exports.WEAPONS[p.currentWeapon].ammoType] || 0) > 0)
        return; // has ammo, can't reload
    ws.isReloading = true;
    ws.reloadStart = now;
}
function updateReload(g, ws, now) {
    if (!ws.isReloading)
        return;
    const w = exports.WEAPONS[g.player.currentWeapon];
    if (now - ws.reloadStart >= w.reloadTime) {
        ws.isReloading = false;
        // Give some ammo
        const ammoType = w.ammoType;
        const maxAmmo = ammoType === 'pistol' ? 99 : ammoType === 'shotgun' ? 32 : 50;
        g.player.ammo[ammoType] = Math.min(maxAmmo, (g.player.ammo[ammoType] || 0) + (ammoType === 'pistol' ? 12 : ammoType === 'shotgun' ? 4 : 20));
    }
}
function trySwitchWeapon(g, ws, target, now) {
    if (ws.switching || target === g.player.currentWeapon)
        return;
    if (target < 0 || target >= exports.WEAPONS.length)
        return;
    // Check if player has this weapon
    const ammoType = exports.WEAPONS[target].ammoType;
    if (ammoType === 'rifle' && (g.player.ammo.rifle || 0) === 0 && target !== g.player.currentWeapon) {
        // Can only switch to rifle if they have ammo or already have it
        // Give rifle on first pickup
        return;
    }
    ws.switching = true;
    ws.switchTarget = target;
    ws.lastSwitch = now;
}
function updateSwitch(g, ws, now) {
    if (!ws.switching)
        return;
    if (now - ws.lastSwitch >= 300) {
        g.player.currentWeapon = ws.switchTarget;
        ws.switching = false;
        ws.isReloading = false;
    }
}
function drawWeapon(g, ws) {
    const { ctx, width, height, player } = g;
    const w = exports.WEAPONS[player.currentWeapon];
    const bobX = Math.sin(player.bobPhase) * player.bobAmount * 20;
    const bobY = Math.abs(Math.cos(player.bobPhase)) * player.bobAmount * 15;
    const fireOffset = g.weaponAnim > 0 ? -20 * (g.weaponAnim / 5) : 0;
    const switchOffset = ws.switching ? 50 : 0;
    const baseX = width / 2 + width * 0.2 + bobX;
    const baseY = height - height * 0.2 + bobY + fireOffset + switchOffset;
    const scale = Math.min(width, height) / 600;
    ctx.save();
    ctx.translate(baseX, baseY);
    ctx.scale(scale, scale);
    // Draw weapon based on type
    if (player.currentWeapon === 0) {
        drawPistol(ctx);
    }
    else if (player.currentWeapon === 1) {
        drawShotgun(ctx);
    }
    else {
        drawRifle(ctx);
    }
    // Muzzle flash
    if (player.muzzleFlash > 0) {
        const flashSize = 30 + Math.random() * 20;
        ctx.fillStyle = `rgba(255, 255, 100, ${player.muzzleFlash / 100})`;
        ctx.beginPath();
        ctx.arc(-40, -60, flashSize, 0, Math.PI * 2);
        ctx.fill();
        ctx.fillStyle = `rgba(255, 150, 50, ${player.muzzleFlash / 150})`;
        ctx.beginPath();
        ctx.arc(-40, -60, flashSize * 1.5, 0, Math.PI * 2);
        ctx.fill();
    }
    ctx.restore();
}
function drawPistol(ctx) {
    // Handle
    ctx.fillStyle = '#553322';
    ctx.fillRect(-10, 0, 20, 60);
    // Barrel
    ctx.fillStyle = '#444';
    ctx.fillRect(-8, -50, 16, 55);
    // Barrel tip
    ctx.fillStyle = '#333';
    ctx.fillRect(-6, -65, 12, 18);
    // Sight
    ctx.fillStyle = '#666';
    ctx.fillRect(-2, -70, 4, 8);
}
function drawShotgun(ctx) {
    // Stock
    ctx.fillStyle = '#553322';
    ctx.fillRect(-15, 10, 30, 50);
    // Body
    ctx.fillStyle = '#444';
    ctx.fillRect(-12, -40, 24, 55);
    // Barrel
    ctx.fillStyle = '#555';
    ctx.fillRect(-10, -70, 20, 35);
    // Pump
    ctx.fillStyle = '#553322';
    ctx.fillRect(-12, -35, 24, 12);
    // Sight
    ctx.fillStyle = '#666';
    ctx.fillRect(-2, -75, 4, 8);
}
function drawRifle(ctx) {
    // Body
    ctx.fillStyle = '#333344';
    ctx.fillRect(-14, -20, 28, 65);
    // Barrel
    ctx.fillStyle = '#444466';
    ctx.fillRect(-8, -65, 16, 50);
    // Energy core glow
    ctx.fillStyle = '#00ff44';
    ctx.fillRect(-6, -5, 12, 8);
    ctx.fillStyle = '#00ff88';
    ctx.fillRect(-4, -3, 8, 4);
    // Sight
    ctx.fillStyle = '#00ff44';
    ctx.fillRect(-3, -70, 6, 8);
    // Magazine
    ctx.fillStyle = '#222233';
    ctx.fillRect(-16, 10, 8, 30);
}
};

// ── module: src/input.ts ──
__mods["src/input.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.setupInput = setupInput;
const audio_1 = require("./audio");
function setupInput(g) {
    // Keyboard
    window.addEventListener('keydown', (e) => {
        switch (e.code) {
            case 'KeyW':
            case 'ArrowUp':
                g.keys.w = true;
                break;
            case 'KeyA':
            case 'ArrowLeft':
                g.keys.a = true;
                break;
            case 'KeyS':
            case 'ArrowDown':
                g.keys.s = true;
                break;
            case 'KeyD':
            case 'ArrowRight':
                g.keys.d = true;
                break;
            case 'ShiftLeft':
            case 'ShiftRight':
                g.keys.shift = true;
                break;
            case 'KeyR':
                g.keys.r = true;
                break;
            case 'Digit1':
                g._weaponSwitchTarget = 0;
                break;
            case 'Digit2':
                g._weaponSwitchTarget = 1;
                break;
            case 'Digit3':
                g._weaponSwitchTarget = 2;
                break;
        }
    });
    window.addEventListener('keyup', (e) => {
        switch (e.code) {
            case 'KeyW':
            case 'ArrowUp':
                g.keys.w = false;
                break;
            case 'KeyA':
            case 'ArrowLeft':
                g.keys.a = false;
                break;
            case 'KeyS':
            case 'ArrowDown':
                g.keys.s = false;
                break;
            case 'KeyD':
            case 'ArrowRight':
                g.keys.d = false;
                break;
            case 'ShiftLeft':
            case 'ShiftRight':
                g.keys.shift = false;
                break;
            case 'KeyR':
                g.keys.r = false;
                break;
        }
    });
    // Mouse
    g.canvas.addEventListener('click', () => {
        (0, audio_1.initAudio)();
        if (!document.pointerLockElement) {
            g.canvas.requestPointerLock();
            return;
        }
        g.keys.mouse = true;
    });
    document.addEventListener('pointerlockchange', () => {
        g.pointerLocked = !!document.pointerLockElement;
    });
    document.addEventListener('mousemove', (e) => {
        if (document.pointerLockElement === g.canvas) {
            g.mouse.dx += e.movementX;
            g.mouse.dy += e.movementY;
        }
    });
    // Title screen
    const titleScreen = document.getElementById('title-screen');
    const titleHide = () => {
        if (titleScreen)
            titleScreen.style.display = 'none';
        document.removeEventListener('keydown', titleKeyHandler);
        g.canvas.removeEventListener('click', titleClickHandler);
    };
    const titleKeyHandler = (e) => {
        if (e.code === 'Enter' || e.code === 'Space')
            titleHide();
    };
    const titleClickHandler = () => {
        (0, audio_1.initAudio)();
        titleHide();
        g.canvas.requestPointerLock();
    };
    document.addEventListener('keydown', titleKeyHandler);
    g.canvas.addEventListener('click', titleClickHandler);
}
};

// ── module: src/ai.ts ──
__mods["src/ai.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.updateEnemies = updateEnemies;
const map_1 = require("./map");
const player_1 = require("./player");
const audio_1 = require("./audio");
function updateEnemies(g, dt) {
    for (const e of g.enemies) {
        if (e.state === 'dead')
            continue;
        e.hitTimer = Math.max(0, e.hitTimer - dt);
        e.animTimer += dt;
        if (e.animTimer > 300) {
            e.animTimer = 0;
            e.animFrame = (e.animFrame + 1) % 4;
        }
        const dx = g.player.x - e.x;
        const dy = g.player.y - e.y;
        const dist = Math.sqrt(dx * dx + dy * dy);
        // State machine
        switch (e.state) {
            case 'idle':
            case 'patrol':
                if (dist < 8 && g.player.alive) {
                    e.state = 'chase';
                }
                else {
                    // Patrol behavior
                    e.patrolTimer -= dt;
                    if (e.patrolTimer <= 0) {
                        e.patrolAngle += (Math.random() - 0.5) * Math.PI;
                        e.patrolTimer = 2000 + Math.random() * 3000;
                    }
                    tryMove(e, Math.cos(e.patrolAngle), Math.sin(e.patrolAngle), e.speed * 0.3, dt);
                }
                break;
            case 'chase':
                if (!g.player.alive || dist > 15) {
                    e.state = 'patrol';
                    e.patrolTimer = 0;
                }
                else if (dist < e.attackRange) {
                    e.state = 'attack';
                }
                else {
                    // Move toward player
                    const nx = dx / dist;
                    const ny = dy / dist;
                    tryMove(e, nx, ny, e.speed, dt);
                }
                break;
            case 'attack':
                if (!g.player.alive || dist > e.attackRange * 2) {
                    e.state = 'chase';
                }
                else {
                    // Attack
                    e.attackCooldown -= dt;
                    if (e.attackCooldown <= 0 && g.player.alive) {
                        // Check line of sight
                        if (!wallBlocksLOS(e.x, e.y, g.player.x, g.player.y)) {
                            const accuracy = e.type === 'soldier' ? 0.85 : 0.9;
                            if (Math.random() < accuracy) {
                                const dmg = e.damage * (0.7 + Math.random() * 0.6);
                                (0, player_1.takeDamage)(g.player, dmg);
                                g.damageFlash = 200;
                                g.screenShake = 4;
                                (0, audio_1.playSound)('hurt');
                            }
                        }
                        e.attackCooldown = e.type === 'boss' ? 500 : e.type === 'soldier' ? 600 : 1000 + Math.random() * 500;
                    }
                    // Boss and soldiers move slowly toward player when in range
                    if (dist > e.attackRange * 0.8) {
                        const nx = dx / dist;
                        const ny = dy / dist;
                        tryMove(e, nx, ny, e.speed * 0.2, dt);
                    }
                    else if (dist < e.attackRange * 0.5) {
                        // Back away
                        const nx = -dx / dist;
                        const ny = -dy / dist;
                        tryMove(e, nx, ny, e.speed * 0.2, dt);
                    }
                }
                break;
        }
    }
}
function tryMove(e, vx, vy, speed, dt) {
    const moveX = vx * speed * (dt / 1000);
    const moveY = vy * speed * (dt / 1000);
    // Try X
    const newX = e.x + moveX;
    const tileX = Math.floor(newX);
    const tileY = Math.floor(e.y);
    if (tileX >= 0 && tileX < map_1.MAP_W && tileY >= 0 && tileY < map_1.MAP_H && map_1.MAP[tileY][tileX] === 0) {
        e.x = newX;
    }
    // Try Y
    const newTileX = Math.floor(e.x);
    const newTileY = Math.floor(e.y + moveY);
    if (newTileX >= 0 && newTileX < map_1.MAP_W && newTileY >= 0 && newTileY < map_1.MAP_H && map_1.MAP[newTileY][newTileX] === 0) {
        e.y = e.y + moveY;
    }
}
function wallBlocksLOS(x1, y1, x2, y2) {
    const dist = Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
    const steps = Math.ceil(dist * 8);
    for (let i = 1; i < steps; i++) {
        const t = i / steps;
        const cx = x1 + (x2 - x1) * t;
        const cy = y1 + (y2 - y1) * t;
        const mx = Math.floor(cx);
        const my = Math.floor(cy);
        if (mx >= 0 && mx < map_1.MAP_W && my >= 0 && my < map_1.MAP_H && map_1.MAP[my][mx] > 0) {
            return true;
        }
    }
    return false;
}
};

// ── module: src/audio.ts ──
__mods["src/audio.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.initAudio = initAudio;
exports.playSound = playSound;
// Procedural sound effects using Web Audio API
let audioCtx = null;
let audioEnabled = false;
function initAudio() {
    if (audioEnabled)
        return;
    audioEnabled = true;
    try {
        audioCtx = new (window.AudioContext || window.webkitAudioContext)();
    }
    catch (e) { /* Audio not supported */ }
}
function getAudioCtx() {
    if (!audioCtx)
        return null;
    if (audioCtx.state === 'suspended')
        audioCtx.resume();
    return audioCtx;
}
function playSound(type) {
    try {
        const ctx = getAudioCtx();
        if (!ctx)
            return;
        if (ctx.state === 'suspended')
            ctx.resume();
        switch (type) {
            case 'pistol':
                fireWeaponSound(ctx, 800, 0.08, 'sawtooth');
                break;
            case 'shotgun':
                fireWeaponSound(ctx, 300, 0.15, 'square');
                fireWeaponSound(ctx, 200, 0.2, 'sawtooth');
                break;
            case 'rifle':
                fireWeaponSound(ctx, 1200, 0.04, 'sawtooth');
                break;
            case 'hit':
                hitSound(ctx);
                break;
            case 'kill':
                killSound(ctx);
                break;
            case 'pickup':
                pickupSound(ctx);
                break;
            case 'hurt':
                hurtSound(ctx);
                break;
            case 'death':
                deathSound(ctx);
                break;
        }
    }
    catch (e) {
        // Audio not available
    }
}
function fireWeaponSound(ctx, freq, duration, type) {
    if (!ctx)
        return;
    const osc = ctx.createOscillator();
    const gain = ctx.createGain();
    osc.type = type;
    osc.frequency.setValueAtTime(freq, ctx.currentTime);
    osc.frequency.exponentialRampToValueAtTime(freq * 0.1, ctx.currentTime + duration);
    gain.gain.setValueAtTime(0.15, ctx.currentTime);
    gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + duration);
    osc.connect(gain);
    gain.connect(ctx.destination);
    osc.start(ctx.currentTime);
    osc.stop(ctx.currentTime + duration);
}
function hitSound(ctx) {
    if (!ctx)
        return;
    const osc = ctx.createOscillator();
    const gain = ctx.createGain();
    osc.type = 'square';
    osc.frequency.setValueAtTime(400, ctx.currentTime);
    osc.frequency.exponentialRampToValueAtTime(100, ctx.currentTime + 0.1);
    gain.gain.setValueAtTime(0.08, ctx.currentTime);
    gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.1);
    osc.connect(gain);
    gain.connect(ctx.destination);
    osc.start(ctx.currentTime);
    osc.stop(ctx.currentTime + 0.1);
}
function killSound(ctx) {
    if (!ctx)
        return;
    const osc = ctx.createOscillator();
    const gain = ctx.createGain();
    osc.type = 'sawtooth';
    osc.frequency.setValueAtTime(600, ctx.currentTime);
    osc.frequency.exponentialRampToValueAtTime(50, ctx.currentTime + 0.4);
    gain.gain.setValueAtTime(0.12, ctx.currentTime);
    gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.4);
    osc.connect(gain);
    gain.connect(ctx.destination);
    osc.start(ctx.currentTime);
    osc.stop(ctx.currentTime + 0.4);
}
function pickupSound(ctx) {
    if (!ctx)
        return;
    const osc = ctx.createOscillator();
    const gain = ctx.createGain();
    osc.type = 'sine';
    osc.frequency.setValueAtTime(600, ctx.currentTime);
    osc.frequency.setValueAtTime(800, ctx.currentTime + 0.05);
    osc.frequency.setValueAtTime(1000, ctx.currentTime + 0.1);
    gain.gain.setValueAtTime(0.08, ctx.currentTime);
    gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.15);
    osc.connect(gain);
    gain.connect(ctx.destination);
    osc.start(ctx.currentTime);
    osc.stop(ctx.currentTime + 0.15);
}
function hurtSound(ctx) {
    if (!ctx)
        return;
    const osc = ctx.createOscillator();
    const gain = ctx.createGain();
    osc.type = 'sawtooth';
    osc.frequency.setValueAtTime(300, ctx.currentTime);
    osc.frequency.exponentialRampToValueAtTime(150, ctx.currentTime + 0.2);
    gain.gain.setValueAtTime(0.1, ctx.currentTime);
    gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.2);
    osc.connect(gain);
    gain.connect(ctx.destination);
    osc.start(ctx.currentTime);
    osc.stop(ctx.currentTime + 0.2);
}
function deathSound(ctx) {
    if (!ctx)
        return;
    const osc = ctx.createOscillator();
    const gain = ctx.createGain();
    osc.type = 'sawtooth';
    osc.frequency.setValueAtTime(400, ctx.currentTime);
    osc.frequency.exponentialRampToValueAtTime(40, ctx.currentTime + 1.0);
    gain.gain.setValueAtTime(0.15, ctx.currentTime);
    gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 1.0);
    osc.connect(gain);
    gain.connect(ctx.destination);
    osc.start(ctx.currentTime);
    osc.stop(ctx.currentTime + 1.0);
}
};

// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"unsloth/Qwen3.6-27B-MTP-GGUF-UD-Q5_K_XL","provider":"brigid","persona":"typescript","sessionId":"c04c96af-8498-4776-82eb-ce2eb6e0a294","tokensIn":5053787,"tokensOut":52393,"tokensTotal":5106180,"cost":0,"turns":110,"toolCalls":117,"failedToolCalls":4,"timestamp":"2026-08-04T07:34:54.408Z"} -->