← DOOM results

DOOM

Qwen 3.6 27b · 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 55,765 bytes · SHA-256 db37623e331d
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>DUNGEON RAIDERS - Retro FPS</title>
<style>
/* ── css: layout ── */
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
  background: #000;
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100vh;
  overflow: hidden;
  font-family: 'Courier New', monospace;
  user-select: none;
}
#app {
  position: relative;
  width: 960px;
  height: 600px;
}
#game {
  display: block;
  image-rendering: pixelated;
  image-rendering: crisp-edges;
}
/* ── css: hud ── */
#hud {
  position: absolute;
  bottom: 0;
  left: 0;
  width: 960px;
  height: 80px;
  background: linear-gradient(to bottom, #333, #111);
  border-top: 3px solid #555;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 20px;
  z-index: 10;
}
#health-bar {
  display: flex;
  align-items: center;
  gap: 8px;
  color: #f44;
  font-size: 24px;
  font-weight: bold;
}
#health-bar::before {
  content: '❤️';
  font-size: 28px;
}
#weapon-info {
  display: flex;
  flex-direction: column;
  align-items: center;
  color: #ff0;
  font-size: 18px;
  font-weight: bold;
}
#weapon-name { font-size: 22px; }
#ammo-val { color: #fa0; }
#ammo-slots {
  display: flex;
  gap: 15px;
  align-items: center;
}
.ammo-slot {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 4px 8px;
  border-radius: 4px;
  border: 1px solid #444;
  min-width: 50px;
}
.ammo-slot.active {
  border-color: #ff0;
  background: rgba(255,255,0,0.1);
}
.ammo-slot .slot-name {
  font-size: 10px;
  color: #888;
}
.ammo-slot .slot-count {
  font-size: 16px;
  color: #fff;
  font-weight: bold;
}
#message {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  color: #fff;
  font-size: 36px;
  font-weight: bold;
  text-shadow: 2px 2px 4px #000;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.3s;
  z-index: 20;
  text-align: center;
}
#message.show { opacity: 1; }
#overlay {
  position: absolute;
  top: 0;
  left: 0;
  width: 960px;
  height: 520px;
  background: rgba(0,0,0,0.85);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  z-index: 30;
  cursor: pointer;
}
#overlay-title {
  font-size: 48px;
  color: #f44;
  font-weight: bold;
  text-shadow: 3px 3px 6px #000;
  margin-bottom: 20px;
}
#overlay-sub {
  font-size: 24px;
  color: #ff0;
  animation: blink 1s infinite;
  margin-bottom: 30px;
}
#overlay-instructions {
  font-size: 14px;
  color: #aaa;
  text-align: center;
  line-height: 1.8;
  margin-top: 10px;
}
@keyframes blink {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.3; }
}
</style>
</head>
<body>
<canvas id="game" width="960" height="600"></canvas><div id="message"></div>
<div id="hud">
  <div id="health-bar"><span id="health-val">100</span></div>
  <div id="ammo-slots">
    <div class="ammo-slot active" id="slot-0"><span class="slot-name">PISTOL</span><span class="slot-count" id="ammo-0">12</span></div>
    <div class="ammo-slot" id="slot-1"><span class="slot-name">SHOTGUN</span><span class="slot-count" id="ammo-1">8</span></div>
    <div class="ammo-slot" id="slot-2"><span class="slot-name">MACHINEGUN</span><span class="slot-count" id="ammo-2">30</span></div>
  </div>
  <div id="weapon-info">
    <span id="weapon-name">PISTOL</span>
    <span id="ammo-val">Kills: 0/11</span>
  </div>
</div>
<div id="overlay">
  <div id="overlay-title">DUNGEON RAIDERS</div>
  <div id="overlay-sub">Click to Start</div><div id="overlay-instructions">
  WASD / Arrow Keys — Move &amp; Turn<br>
  Q/E — Strafe Left/Right<br>
  Mouse — Look (click to lock)<br>
  Space / Click — Shoot<br>
  1/2/3 — Switch Weapon<br><br>
  Kill all enemies, then reach the exit (yellow on minimap)!
</div>
</div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./types":"src/types.ts","./map":"src/map.ts","./player":"src/player.ts","./enemies":"src/enemies.ts","./pickups":"src/pickups.ts","./input":"src/input.ts","./render":"src/render.ts","./sound":"src/sound.ts"},"src/map.ts":{"./types":"src/types.ts"},"src/raycast.ts":{"./map":"src/map.ts"},"src/player.ts":{"./types":"src/types.ts","./map":"src/map.ts"},"src/enemies.ts":{"./types":"src/types.ts","./map":"src/map.ts","./raycast":"src/raycast.ts"},"src/pickups.ts":{"./types":"src/types.ts"},"src/render.ts":{"./types":"src/types.ts","./raycast":"src/raycast.ts","./enemies":"src/enemies.ts","./pickups":"src/pickups.ts","./player":"src/player.ts","./map":"src/map.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 map_1 = require("./map");
const player_1 = require("./player");
const enemies_1 = require("./enemies");
const pickups_1 = require("./pickups");
const input_1 = require("./input");
const render_1 = require("./render");
const sound_1 = require("./sound");
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const overlay = document.getElementById('overlay');
const overlayTitle = document.getElementById('overlay-title');
const overlaySub = document.getElementById('overlay-sub');
const healthVal = document.getElementById('health-val');
const weaponName = document.getElementById('weapon-name');
const ammoVal = document.getElementById('ammo-val');
const messageEl = document.getElementById('message');
let player = (0, player_1.createPlayer)();
let enemies = [];
let pickups = [];
let gameState;
let input;
let messageTimer = 0;
let currentMessage = '';
let lastTime = 0;
let running = false;
function initGame() {
    player = (0, player_1.createPlayer)();
    enemies = (0, map_1.spawnEnemies)();
    pickups = (0, map_1.spawnPickups)();
    gameState = {
        phase: 'menu',
        kills: 0,
        totalEnemies: (0, enemies_1.countAlive)(enemies),
        deathCount: 0,
        level: 1,
    };
    currentMessage = '';
    messageTimer = 0;
}
function showMessage(text, duration = 1200) {
    currentMessage = text;
    messageTimer = duration;
}
function updateHUD() {
    healthVal.textContent = Math.ceil(player.health).toString();
    weaponName.textContent = types_1.WEAPONS[player.weapon].name;
    ammoVal.textContent = `Kills: ${gameState.kills}/${gameState.totalEnemies}`;
    if (player.health > 50) {
        healthVal.style.color = '#f44';
    }
    else if (player.health > 25) {
        healthVal.style.color = '#fa0';
    }
    else {
        healthVal.style.color = '#f00';
    }
    // Update ammo slots
    for (let i = 0; i < 3; i++) {
        const slot = document.getElementById(`slot-${i}`);
        const count = document.getElementById(`ammo-${i}`);
        if (slot && count) {
            count.textContent = player.ammo[i].toString();
            if (i === player.weapon) {
                slot.classList.add('active');
            }
            else {
                slot.classList.remove('active');
            }
        }
    }
}
function handleShooting() {
    if (!player.shooting)
        return;
    // Check ammo
    if (player.ammo[player.weapon] <= 0) {
        // Try to switch to a weapon with ammo
        let found = false;
        for (let i = 0; i < types_1.WEAPONS.length; i++) {
            if (player.ammo[i] > 0) {
                player.weapon = i;
                found = true;
                break;
            }
        }
        if (!found) {
            showMessage('No ammo!');
            return;
        }
    }
    const w = types_1.WEAPONS[player.weapon];
    player.ammo[player.weapon] -= w.ammoCost;
    player.weaponAnim = 1;
    const pellets = w.pellets;
    for (let i = 0; i < pellets; i++) {
        const spread = (Math.random() - 0.5) * w.spread;
        const shotAngle = player.angle + spread;
        const hit = (0, enemies_1.checkShotHit)(enemies, player.x, player.y, shotAngle, w.range);
        if (hit) {
            if ((0, enemies_1.damageEnemy)(hit.enemy, w.damage)) {
                gameState.kills++;
                (0, sound_1.playSound)('enemy_die');
                showMessage(`Enemy killed! (${gameState.kills}/${gameState.totalEnemies})`);
            }
            else {
                (0, sound_1.playSound)('hit');
            }
        }
    }
}
function handleWinCondition() {
    // Win: all enemies dead AND reach the exit
    if (gameState.kills >= gameState.totalEnemies) {
        if ((0, map_1.isExit)(player.x, player.y)) {
            (0, sound_1.playSound)('door');
            gameState.phase = 'won';
            running = false;
            overlayTitle.textContent = 'VICTORY!';
            overlayTitle.style.color = '#0f0';
            overlaySub.textContent = `Deaths: ${gameState.deathCount} | Click to Play Again`;
            overlay.style.display = 'flex';
        }
        else {
            showMessage('All clear! Reach the exit (yellow on minimap)!');
        }
    }
}
function handleDeath() {
    if (!player.alive) {
        (0, sound_1.playSound)('death');
        gameState.deathCount++;
        gameState.phase = 'dead';
        setTimeout(() => {
            (0, player_1.respawnPlayer)(player);
            // Respawn enemies that were killed
            const freshEnemies = (0, map_1.spawnEnemies)();
            for (let i = 0; i < enemies.length; i++) {
                if (!enemies[i].alive && freshEnemies[i]) {
                    enemies[i] = freshEnemies[i];
                }
            }
            gameState.kills = 0;
            gameState.phase = 'playing';
            running = true;
            showMessage('Respawned!');
            overlay.style.display = 'none';
        }, 1500);
        overlayTitle.textContent = 'YOU DIED';
        overlayTitle.style.color = '#f00';
        overlaySub.textContent = 'Respawning...';
        overlay.style.display = 'flex';
        running = false;
    }
}
function gameLoop(timestamp) {
    if (!running)
        return;
    const dt = Math.min(3, (timestamp - lastTime) / 16.67);
    lastTime = timestamp;
    // Input processing
    let dx = 0;
    let dy = 0;
    if (input.forward)
        dy += 1;
    if (input.backward)
        dy -= 1;
    if (input.strafeLeft)
        dx -= 1;
    if (input.strafeRight)
        dx += 1;
    // Mouse look
    const mouseDelta = (0, input_1.consumeMouseDelta)(input);
    (0, player_1.rotatePlayer)(player, mouseDelta);
    if (input.left)
        (0, player_1.rotatePlayer)(player, -1);
    if (input.right)
        (0, player_1.rotatePlayer)(player, 1);
    if (input.switchUp) {
        (0, player_1.switchWeapon)(player, 1);
        input.switchUp = false;
    }
    if (input.switchDown) {
        (0, player_1.switchWeapon)(player, -1);
        input.switchDown = false;
    }
    // Move player
    (0, player_1.movePlayer)(player, dx, dy, dt);
    // Update timers
    (0, player_1.updatePlayerTimers)(player, dt);
    // Weapon animation decay
    if (player.weaponAnim > 0) {
        player.weaponAnim -= dt * 0.15;
        if (player.weaponAnim < 0)
            player.weaponAnim = 0;
    }
    // Shooting
    if (input.shoot) {
        if ((0, player_1.tryShoot)(player, timestamp)) {
            // Play weapon sound
            const soundNames = ['pistol', 'shotgun', 'machinegun'];
            (0, sound_1.playSound)(soundNames[player.weapon]);
            handleShooting();
        }
    }
    // Update enemies
    (0, enemies_1.updateEnemies)(enemies, player, dt);
    // Check pickups
    const pickupMsg = (0, pickups_1.checkPickupCollision)(player, pickups);
    if (pickupMsg) {
        showMessage(pickupMsg);
        (0, sound_1.playSound)('pickup');
    }
    // Message timer
    if (messageTimer > 0) {
        messageTimer -= 16.67 * dt;
    }
    else {
        currentMessage = '';
    }
    // Check death
    if (!player.alive) {
        handleDeath();
        return;
    }
    // Check win
    handleWinCondition();
    // Update HUD
    updateHUD();
    // Render
    const msg = messageTimer > 0 ? currentMessage : null;
    (0, render_1.render)(ctx, player, enemies, pickups, canvas.width, canvas.height, msg);
    requestAnimationFrame(gameLoop);
}
function startGame() {
    initGame();
    input = (0, input_1.setupInput)(canvas);
    gameState.phase = 'playing';
    running = true;
    overlay.style.display = 'none';
    lastTime = performance.now();
    requestAnimationFrame(gameLoop);
}
// Initial setup
initGame();
(0, render_1.render)(ctx, player, enemies, pickups, canvas.width, canvas.height, null);
overlay.addEventListener('click', () => {
    if (gameState.phase === 'menu' || gameState.phase === 'won') {
        startGame();
    }
});
// Also allow keyboard start
document.addEventListener('keydown', (e) => {
    if ((e.code === 'Space' || e.code === 'Enter') && (gameState.phase === 'menu' || gameState.phase === 'won')) {
        startGame();
    }
});
};

// ── module: src/types.ts ──
__mods["src/types.ts"] = function (exports, require, module) {
"use strict";
// Core types for DUNGEON RAIDERS
Object.defineProperty(exports, "__esModule", { value: true });
exports.ENEMY_TYPES = exports.WEAPONS = void 0;
exports.WEAPONS = [
    { name: 'PISTOL', damage: 15, fireRate: 15, ammoCost: 1, spread: 0.02, pellets: 1, range: 20 },
    { name: 'SHOTGUN', damage: 10, fireRate: 30, ammoCost: 1, spread: 0.15, pellets: 5, range: 15 },
    { name: 'MACHINE GUN', damage: 8, fireRate: 5, ammoCost: 1, spread: 0.05, pellets: 1, range: 18 },
];
exports.ENEMY_TYPES = {
    grunt: { health: 40, speed: 0.025, damage: 8, alertRange: 8, color: '#4a4' },
    soldier: { health: 80, speed: 0.02, damage: 15, alertRange: 10, color: '#a44' },
    boss: { health: 200, speed: 0.015, damage: 25, alertRange: 12, color: '#a4a' },
};
};

// ── module: src/map.ts ──
__mods["src/map.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.WALL_COLORS = exports.MAP_H = exports.MAP_W = exports.MAP = void 0;
exports.getTile = getTile;
exports.isWall = isWall;
exports.isExit = isExit;
exports.spawnEnemies = spawnEnemies;
exports.spawnPickups = spawnPickups;
// 24x24 map. 0=empty, 1-5=wall types, 6=exit door
exports.MAP = [
    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
    1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1,
    1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1,
    1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
    1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0, 3, 3, 3, 0, 0, 1,
    1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0, 3, 0, 3, 0, 0, 1,
    1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 3, 0, 0, 1,
    1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
    1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 4, 4, 4, 0, 0, 1, 1, 1, 1, 0, 0, 1,
    1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 4, 0, 4, 0, 0, 1, 0, 0, 1, 0, 0, 1,
    1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1,
    1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
    1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
    1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
    1, 0, 0, 3, 3, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1,
    1, 0, 0, 3, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1,
    1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 2, 2, 2, 0, 0, 1, 0, 0, 0, 0, 0, 1,
    1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 1,
    1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 2, 0, 2, 0, 0, 0, 3, 3, 0, 0, 0, 1,
    1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 3, 0, 0, 0, 1,
    1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
    1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 1,
    1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
];
exports.MAP_W = 24;
exports.MAP_H = 24;
// Wall colors per type [r, g, b]
exports.WALL_COLORS = [
    [80, 80, 80], // 0: empty (not used)
    [150, 75, 75], // 1: red brick
    [75, 120, 75], // 2: green stone
    [75, 75, 150], // 3: blue stone
    [150, 130, 75], // 4: gold
    [100, 80, 120], // 5: purple
    [200, 200, 50], // 6: exit (yellow)
];
function getTile(x, y) {
    const ix = Math.floor(x);
    const iy = Math.floor(y);
    if (ix < 0 || ix >= exports.MAP_W || iy < 0 || iy >= exports.MAP_H)
        return 1;
    return exports.MAP[iy * exports.MAP_W + ix];
}
function isWall(x, y) {
    const t = getTile(x, y);
    return t >= 1 && t <= 5;
}
function isExit(x, y) {
    return getTile(x, y) === 6;
}
// Spawn enemies at fixed positions
function spawnEnemies() {
    const enemies = [
        // Grunts scattered around
        { x: 5.5, y: 3.5, health: 40, maxHealth: 40, type: 'grunt', alive: true, state: 'idle', attackTimer: 0, alertRange: 8, speed: 0.025, damage: 8, hitFlash: 0 },
        { x: 10.5, y: 5.5, health: 40, maxHealth: 40, type: 'grunt', alive: true, state: 'idle', attackTimer: 0, alertRange: 8, speed: 0.025, damage: 8, hitFlash: 0 },
        { x: 18.5, y: 4.5, health: 40, maxHealth: 40, type: 'grunt', alive: true, state: 'idle', attackTimer: 0, alertRange: 8, speed: 0.025, damage: 8, hitFlash: 0 },
        { x: 8.5, y: 12.5, health: 40, maxHealth: 40, type: 'grunt', alive: true, state: 'idle', attackTimer: 0, alertRange: 8, speed: 0.025, damage: 8, hitFlash: 0 },
        { x: 15.5, y: 15.5, health: 40, maxHealth: 40, type: 'grunt', alive: true, state: 'idle', attackTimer: 0, alertRange: 8, speed: 0.025, damage: 8, hitFlash: 0 },
        // Soldiers in tougher spots
        { x: 4.5, y: 8.5, health: 80, maxHealth: 80, type: 'soldier', alive: true, state: 'idle', attackTimer: 0, alertRange: 10, speed: 0.02, damage: 15, hitFlash: 0 },
        { x: 12.5, y: 10.5, health: 80, maxHealth: 80, type: 'soldier', alive: true, state: 'idle', attackTimer: 0, alertRange: 10, speed: 0.02, damage: 15, hitFlash: 0 },
        { x: 20.5, y: 12.5, health: 80, maxHealth: 80, type: 'soldier', alive: true, state: 'idle', attackTimer: 0, alertRange: 10, speed: 0.02, damage: 15, hitFlash: 0 },
        { x: 10.5, y: 17.5, health: 80, maxHealth: 80, type: 'soldier', alive: true, state: 'idle', attackTimer: 0, alertRange: 10, speed: 0.02, damage: 15, hitFlash: 0 },
        { x: 18.5, y: 18.5, health: 80, maxHealth: 80, type: 'soldier', alive: true, state: 'idle', attackTimer: 0, alertRange: 10, speed: 0.02, damage: 15, hitFlash: 0 },
        // Boss near the exit
        { x: 21.5, y: 21.5, health: 200, maxHealth: 200, type: 'boss', alive: true, state: 'idle', attackTimer: 0, alertRange: 12, speed: 0.015, damage: 25, hitFlash: 0 },
    ];
    return enemies;
}
// Spawn pickups
function spawnPickups() {
    return [
        { x: 3.5, y: 5.5, type: 'health', collected: false },
        { x: 7.5, y: 1.5, type: 'ammo_pistol', collected: false },
        { x: 15.5, y: 3.5, type: 'ammo_shotgun', collected: false },
        { x: 20.5, y: 7.5, type: 'health', collected: false },
        { x: 1.5, y: 10.5, type: 'ammo_machinegun', collected: false },
        { x: 11.5, y: 13.5, type: 'health', collected: false },
        { x: 5.5, y: 16.5, type: 'ammo_shotgun', collected: false },
        { x: 16.5, y: 19.5, type: 'ammo_pistol', collected: false },
        { x: 22.5, y: 15.5, type: 'health', collected: false },
        { x: 1.5, y: 20.5, type: 'ammo_machinegun', collected: false },
        { x: 10.5, y: 8.5, type: 'ammo_shotgun', collected: false },
        { x: 14.5, y: 20.5, type: 'health', collected: false },
    ];
}
};

// ── module: src/raycast.ts ──
__mods["src/raycast.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.castRay = castRay;
exports.getWallColor = getWallColor;
exports.getFloorCeilingColor = getFloorCeilingColor;
exports.hasLineOfSight = hasLineOfSight;
const map_1 = require("./map");
function castRay(px, py, angle, maxDist = 30) {
    const sin = Math.sin(angle);
    const cos = Math.cos(angle);
    // Current map cell
    let mapX = Math.floor(px);
    let mapY = Math.floor(py);
    // Length of ray from one x-side to next x-side
    const deltaDistX = Math.abs(1 / cos) || 1e10;
    const deltaDistY = Math.abs(1 / sin) || 1e10;
    // Direction each step goes
    let stepX;
    let stepY;
    let sideDistX;
    let sideDistY;
    if (cos < 0) {
        stepX = -1;
        sideDistX = (px - mapX) * deltaDistX;
    }
    else {
        stepX = 1;
        sideDistX = (mapX + 1 - px) * deltaDistX;
    }
    if (sin < 0) {
        stepY = -1;
        sideDistY = (py - mapY) * deltaDistY;
    }
    else {
        stepY = 1;
        sideDistY = (mapY + 1 - py) * deltaDistY;
    }
    let side = 0;
    let depth = 0;
    for (let i = 0; i < 200; i++) {
        if (sideDistX < sideDistY) {
            sideDistX += deltaDistX;
            mapX += stepX;
            side = 0;
        }
        else {
            sideDistY += deltaDistY;
            mapY += stepY;
            side = 1;
        }
        depth++;
        const tile = (0, map_1.getTile)(mapX, mapY);
        if (tile >= 1 && tile <= 6) {
            // Calculate actual distance
            let perpDist;
            let wallX;
            if (side === 0) {
                perpDist = (mapX - px + (1 - stepX) / 2) / cos;
                wallX = py + perpDist * sin;
            }
            else {
                perpDist = (mapY - py + (1 - stepY) / 2) / sin;
                wallX = px + perpDist * cos;
            }
            wallX -= Math.floor(wallX);
            return {
                dist: perpDist,
                wallType: tile,
                side,
                hitX: wallX,
            };
        }
    }
    return { dist: maxDist, wallType: 0, side: 0, hitX: 0 };
}
function getWallColor(wallType, side, dist) {
    const base = map_1.WALL_COLORS[wallType] || map_1.WALL_COLORS[1];
    const shade = side === 1 ? 0.7 : 1.0;
    // Fog: darken with distance
    const fog = Math.max(0.15, 1 - dist / 20);
    return [
        Math.floor(base[0] * shade * fog),
        Math.floor(base[1] * shade * fog),
        Math.floor(base[2] * shade * fog),
    ];
}
function getFloorCeilingColor(dist) {
    const fog = Math.max(0.1, 1 - dist / 18);
    const floorR = Math.floor(60 * fog);
    const floorG = Math.floor(50 * fog);
    const floorB = Math.floor(40 * fog);
    const ceilR = Math.floor(40 * fog);
    const ceilG = Math.floor(35 * fog);
    const ceilB = Math.floor(50 * fog);
    return [floorR, floorG, floorB, ceilR, ceilG, ceilB];
}
// Check line of sight (simplified - check if any wall blocks the path)
function hasLineOfSight(x1, y1, x2, y2) {
    const dx = x2 - x1;
    const dy = y2 - y1;
    const dist = Math.sqrt(dx * dx + dy * dy);
    const steps = Math.ceil(dist * 4);
    for (let i = 0; i < steps; i++) {
        const t = i / steps;
        const cx = x1 + dx * t;
        const cy = y1 + dy * t;
        if ((0, map_1.isWall)(cx, cy))
            return false;
    }
    return true;
}
};

// ── 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.movePlayer = movePlayer;
exports.rotatePlayer = rotatePlayer;
exports.tryShoot = tryShoot;
exports.updatePlayerTimers = updatePlayerTimers;
exports.getBobOffset = getBobOffset;
exports.switchWeapon = switchWeapon;
const types_1 = require("./types");
const map_1 = require("./map");
function createPlayer() {
    return {
        x: 2.5,
        y: 2.5,
        angle: 0.5,
        health: 100,
        maxHealth: 100,
        alive: true,
        weapon: 0,
        ammo: [12, 8, 30], // pistol, shotgun, machine gun
        shooting: false,
        shootTimer: 0,
        bobPhase: 0,
        damageFlash: 0,
        weaponAnim: 0,
    };
}
function respawnPlayer(p) {
    p.x = 2.5;
    p.y = 2.5;
    p.angle = 0.5;
    p.health = 100;
    p.alive = true;
    p.weapon = 0;
    p.ammo = [12, 8, 30];
    p.shooting = false;
    p.shootTimer = 0;
    p.damageFlash = 0;
    p.weaponAnim = 0;
}
function movePlayer(p, dx, dy, dt) {
    if (!p.alive)
        return;
    const speed = 0.05 * dt;
    const cos = Math.cos(p.angle);
    const sin = Math.sin(p.angle);
    // Forward/back
    let moveX = cos * dy * speed;
    let moveY = sin * dy * speed;
    // Strafe
    moveX += -sin * dx * speed;
    moveY += cos * dx * speed;
    // Collision detection with sliding
    const margin = 0.2;
    const newX = p.x + moveX;
    const newY = p.y + moveY;
    if (!(0, map_1.isWall)(newX + (moveX > 0 ? margin : -margin), p.y)) {
        p.x = newX;
    }
    if (!(0, map_1.isWall)(p.x, newY + (moveY > 0 ? margin : -margin))) {
        p.y = newY;
    }
    // Head bob
    if (dx !== 0 || dy !== 0) {
        p.bobPhase += 0.12 * dt;
    }
}
function rotatePlayer(p, da) {
    if (!p.alive)
        return;
    p.angle += da * 0.03;
}
function tryShoot(p, frame) {
    if (!p.alive)
        return false;
    if (p.shootTimer > 0)
        return false;
    const w = types_1.WEAPONS[p.weapon];
    p.shootTimer = w.fireRate;
    p.shooting = true;
    return true;
}
function updatePlayerTimers(p, dt) {
    if (p.shootTimer > 0) {
        p.shootTimer -= dt;
        if (p.shootTimer <= 0) {
            p.shooting = false;
            p.shootTimer = 0;
        }
    }
    if (p.damageFlash > 0) {
        p.damageFlash -= dt * 2;
    }
}
function getBobOffset(p) {
    return Math.sin(p.bobPhase) * 5;
}
function switchWeapon(p, dir) {
    if (!p.alive)
        return;
    p.weapon = (p.weapon + dir + types_1.WEAPONS.length) % types_1.WEAPONS.length;
}
};

// ── module: src/enemies.ts ──
__mods["src/enemies.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.updateEnemies = updateEnemies;
exports.damageEnemy = damageEnemy;
exports.getEnemyColor = getEnemyColor;
exports.countAlive = countAlive;
exports.checkShotHit = checkShotHit;
const types_1 = require("./types");
const map_1 = require("./map");
const raycast_1 = require("./raycast");
function updateEnemies(enemies, player, dt) {
    for (const e of enemies) {
        if (!e.alive)
            continue;
        // Hit flash decay
        if (e.hitFlash > 0)
            e.hitFlash -= dt * 3;
        const dx = player.x - e.x;
        const dy = player.y - e.y;
        const dist = Math.sqrt(dx * dx + dy * dy);
        // Detection
        if (dist < e.alertRange && player.alive) {
            if ((0, raycast_1.hasLineOfSight)(e.x, e.y, player.x, player.y)) {
                e.state = 'chase';
            }
        }
        if (e.state === 'idle')
            continue;
        // Chase player
        if (dist > 1.0) {
            const nx = dx / dist;
            const ny = dy / dist;
            const speed = e.speed * dt;
            const newX = e.x + nx * speed;
            const newY = e.y + ny * speed;
            // Simple collision - try each axis separately
            if (!(0, map_1.isWall)(newX, e.y))
                e.x = newX;
            if (!(0, map_1.isWall)(e.x, newY))
                e.y = newY;
        }
        else {
            e.state = 'attack';
        }
        // Attack player
        if (e.state === 'attack' && dist < 1.5) {
            e.attackTimer += dt;
            if (e.attackTimer > 60) {
                e.attackTimer = 0;
                if (player.alive) {
                    player.health -= e.damage;
                    player.damageFlash = 1;
                    if (player.health <= 0) {
                        player.health = 0;
                        player.alive = false;
                    }
                }
            }
        }
    }
}
function damageEnemy(e, damage) {
    if (!e.alive)
        return false;
    e.health -= damage;
    e.hitFlash = 1;
    if (e.health <= 0) {
        e.health = 0;
        e.alive = false;
        return true;
    }
    return false;
}
function getEnemyColor(e) {
    if (e.hitFlash > 0)
        return '#fff';
    return types_1.ENEMY_TYPES[e.type].color;
}
function countAlive(enemies) {
    return enemies.filter(e => e.alive).length;
}
// Check if a shot at a given angle and distance hits an enemy
function checkShotHit(enemies, px, py, angle, maxDist) {
    const cos = Math.cos(angle);
    const sin = Math.sin(angle);
    let hit = null;
    for (const e of enemies) {
        if (!e.alive)
            continue;
        const dx = e.x - px;
        const dy = e.y - py;
        const dist = Math.sqrt(dx * dx + dy * dy);
        if (dist > maxDist)
            continue;
        // Project enemy onto ray direction
        const dot = dx * cos + dy * sin;
        if (dot < 0)
            continue; // behind
        // Perpendicular distance
        const perpDist = Math.abs(dx * sin - dy * cos);
        // Enemy hitbox radius (smaller for grunts, larger for boss)
        const radius = e.type === 'boss' ? 0.5 : 0.35;
        if (perpDist < radius && dot < dist) {
            if (!hit || dot < hit.dist) {
                hit = { enemy: e, dist: dot };
            }
        }
    }
    return hit;
}
};

// ── module: src/pickups.ts ──
__mods["src/pickups.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.PICKUP_LABELS = exports.PICKUP_COLORS = void 0;
exports.checkPickupCollision = checkPickupCollision;
exports.countRemaining = countRemaining;
exports.PICKUP_COLORS = {
    health: '#f44',
    ammo_pistol: '#ff0',
    ammo_shotgun: '#f80',
    ammo_machinegun: '#0ff',
};
exports.PICKUP_LABELS = {
    health: '+HP',
    ammo_pistol: 'P',
    ammo_shotgun: 'S',
    ammo_machinegun: 'M',
};
function checkPickupCollision(player, pickups) {
    for (const p of pickups) {
        if (p.collected)
            continue;
        const dx = player.x - p.x;
        const dy = player.y - p.y;
        const dist = Math.sqrt(dx * dx + dy * dy);
        if (dist < 0.5) {
            p.collected = true;
            switch (p.type) {
                case 'health':
                    player.health = Math.min(player.maxHealth, player.health + 25);
                    return 'Health +25!';
                case 'ammo_pistol':
                    return 'Pistol Ammo!';
                case 'ammo_shotgun':
                    return 'Shotgun Ammo!';
                case 'ammo_machinegun':
                    return 'Machine Gun Ammo!';
            }
        }
    }
    return null;
}
function countRemaining(pickups) {
    return pickups.filter(p => !p.collected).length;
}
};

// ── module: src/input.ts ──
__mods["src/input.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.createInput = createInput;
exports.setupInput = setupInput;
exports.consumeMouseDelta = consumeMouseDelta;
function createInput() {
    return {
        forward: false,
        backward: false,
        left: false,
        right: false,
        strafeLeft: false,
        strafeRight: false,
        shoot: false,
        switchUp: false,
        switchDown: false,
    };
}
function setupInput(canvas) {
    const input = createInput();
    const keyMap = {
        KeyW: 'forward',
        ArrowUp: 'forward',
        KeyS: 'backward',
        ArrowDown: 'backward',
        ArrowLeft: 'left',
        KeyA: 'left',
        ArrowRight: 'right',
        KeyD: 'right',
        KeyQ: 'strafeLeft',
        KeyE: 'strafeRight',
        Space: 'shoot',
        Digit1: 'switchUp',
        Digit2: 'switchUp',
        Digit3: 'switchDown',
    };
    canvas.addEventListener('contextmenu', e => e.preventDefault());
    canvas.addEventListener('keydown', e => {
        const key = e.code;
        if (key in keyMap) {
            input[keyMap[key]] = true;
        }
    });
    canvas.addEventListener('keyup', e => {
        const key = e.code;
        if (key in keyMap) {
            input[keyMap[key]] = false;
        }
    });
    // Mouse movement for looking
    canvas.addEventListener('mousedown', (e) => {
        if (e.button === 0) {
            canvas.requestPointerLock?.();
            input.shoot = true;
        }
    });
    canvas.addEventListener('mouseup', (e) => {
        if (e.button === 0) {
            input.shoot = false;
        }
    });
    document.addEventListener('mousemove', e => {
        if (document.pointerLockElement === canvas) {
            // Mouse look handled in game loop
            input._mouseDX = (input._mouseDX || 0) + e.movementX;
        }
    });
    return input;
}
function consumeMouseDelta(input) {
    const dx = input._mouseDX || 0;
    input._mouseDX = 0;
    return dx;
}
};

// ── module: src/render.ts ──
__mods["src/render.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.render = render;
const types_1 = require("./types");
const raycast_1 = require("./raycast");
const enemies_1 = require("./enemies");
const pickups_1 = require("./pickups");
const player_1 = require("./player");
const map_1 = require("./map");
// Helper: shade a hex color by a factor
function shadeColor(hex, factor) {
    const num = parseInt(hex.replace('#', ''), 16);
    const r = Math.min(255, Math.max(0, ((num >> 16) & 0xff) * factor));
    const g = Math.min(255, Math.max(0, ((num >> 8) & 0xff) * factor));
    const b = Math.min(255, Math.max(0, (num & 0xff) * factor));
    return `rgb(${Math.floor(r)},${Math.floor(g)},${Math.floor(b)})`;
}
const FOV = Math.PI / 3;
const NUM_RAYS = 480; // half the canvas width for performance
function render(ctx, player, enemies, pickups, width, height, message) {
    const viewHeight = height - 80; // leave room for HUD
    const halfH = viewHeight / 2;
    const bob = (0, player_1.getBobOffset)(player);
    // Clear sky
    ctx.fillStyle = '#1a1a2e';
    ctx.fillRect(0, 0, width, viewHeight);
    // Cast rays and draw walls
    const zBuffer = [];
    for (let i = 0; i < NUM_RAYS; i++) {
        const rayAngle = player.angle - FOV / 2 + (i / NUM_RAYS) * FOV;
        const ray = (0, raycast_1.castRay)(player.x, player.y, rayAngle);
        // Fix fisheye
        const perpDist = ray.dist * Math.cos(rayAngle - player.angle);
        zBuffer[i] = perpDist;
        const wallHeight = Math.min(viewHeight, (viewHeight / perpDist) * 0.8);
        const wallTop = halfH - wallHeight / 2 + bob;
        const [r, g, b] = (0, raycast_1.getWallColor)(ray.wallType, ray.side, ray.dist);
        const x = (i / NUM_RAYS) * width;
        const colW = Math.ceil(width / NUM_RAYS) + 1;
        // Draw wall with texture stripe pattern
        const stripeSize = Math.max(2, wallHeight / 20);
        for (let s = 0; s < wallHeight; s += stripeSize) {
            const stripeY = wallTop + s;
            const stripeH = Math.min(stripeSize, wallHeight - s);
            // Alternating stripe shading for brick/stone texture
            const stripeIdx = Math.floor((s - bob) / stripeSize);
            const shade = (stripeIdx % 2 === 0) ? 1.0 : 0.85;
            // Edge darkening for texture effect
            const edgeFade = (ray.hitX < 0.05 || ray.hitX > 0.95) ? 0.7 : 1.0;
            ctx.fillStyle = `rgb(${Math.floor(r * shade * edgeFade)},${Math.floor(g * shade * edgeFade)},${Math.floor(b * shade * edgeFade)})`;
            ctx.fillRect(x, stripeY, colW, stripeH);
        }
        // Floor with distance fog
        const floorFog = Math.max(0.05, 1 - perpDist / 18);
        const floorR = Math.floor(55 * floorFog);
        const floorG = Math.floor(45 * floorFog);
        const floorB = Math.floor(35 * floorFog);
        ctx.fillStyle = `rgb(${floorR},${floorG},${floorB})`;
        ctx.fillRect(x, wallTop + wallHeight, colW, viewHeight - wallTop - wallHeight);
        // Floor grid lines (subtle)
        const gridSpacing = 0.5;
        const gridY1 = player.y + (Math.floor(player.y / gridSpacing) + 1) * gridSpacing;
        const gridDist1 = (gridY1 - player.y) / Math.sin(FOV / 2 + Math.abs(rayAngle - player.angle));
        if (gridDist1 > 0 && gridDist1 < perpDist) {
            const screenY1 = halfH + viewHeight / (2 * gridDist1 * Math.cos(rayAngle - player.angle)) + bob;
            if (screenY1 > wallTop + wallHeight && screenY1 < viewHeight) {
                ctx.fillStyle = `rgba(80, 70, 60, ${0.3 * floorFog})`;
                ctx.fillRect(x, screenY1, colW, 1);
            }
        }
        // Ceiling
        const ceilFog = Math.max(0.05, 1 - perpDist / 15);
        ctx.fillStyle = `rgb(${Math.floor(30 * ceilFog)},${Math.floor(28 * ceilFog)},${Math.floor(40 * ceilFog)})`;
        ctx.fillRect(x, 0, colW, wallTop);
    }
    // Draw sprites (enemies and pickups)
    drawSprites(ctx, player, enemies, pickups, zBuffer, width, viewHeight, halfH, bob);
    // Draw crosshair
    drawCrosshair(ctx, width, viewHeight);
    // Draw weapon
    drawWeapon(ctx, player, width, viewHeight, bob);
    // Vignette effect
    const gradient = ctx.createRadialGradient(width / 2, viewHeight / 2, viewHeight * 0.3, width / 2, viewHeight / 2, viewHeight * 0.9);
    gradient.addColorStop(0, 'rgba(0,0,0,0)');
    gradient.addColorStop(1, 'rgba(0,0,0,0.5)');
    ctx.fillStyle = gradient;
    ctx.fillRect(0, 0, width, viewHeight);
    // Damage flash
    if (player.damageFlash > 0) {
        ctx.fillStyle = `rgba(255, 0, 0, ${player.damageFlash * 0.3})`;
        ctx.fillRect(0, 0, width, viewHeight);
    }
    // Message
    if (message) {
        ctx.fillStyle = '#fff';
        ctx.font = 'bold 24px Courier New';
        ctx.textAlign = 'center';
        ctx.fillText(message, width / 2, 60);
    }
    // Minimap
    drawMinimap(ctx, player, enemies, width, viewHeight);
}
function drawCrosshair(ctx, width, viewHeight) {
    const cx = width / 2;
    const cy = viewHeight / 2;
    const size = 12;
    const gap = 4;
    ctx.strokeStyle = '#0f0';
    ctx.lineWidth = 2;
    ctx.shadowColor = '#0f0';
    ctx.shadowBlur = 4;
    // Top
    ctx.beginPath();
    ctx.moveTo(cx, cy - gap);
    ctx.lineTo(cx, cy - size);
    ctx.stroke();
    // Bottom
    ctx.beginPath();
    ctx.moveTo(cx, cy + gap);
    ctx.lineTo(cx, cy + size);
    ctx.stroke();
    // Left
    ctx.beginPath();
    ctx.moveTo(cx - gap, cy);
    ctx.lineTo(cx - size, cy);
    ctx.stroke();
    // Right
    ctx.beginPath();
    ctx.moveTo(cx + gap, cy);
    ctx.lineTo(cx + size, cy);
    ctx.stroke();
    ctx.shadowBlur = 0;
}
function drawSprites(ctx, player, enemies, pickups, zBuffer, width, viewHeight, halfH, bob) {
    const sprites = [];
    for (const e of enemies) {
        if (!e.alive)
            continue;
        const dx = e.x - player.x;
        const dy = e.y - player.y;
        sprites.push({ x: e.x, y: e.y, dist: Math.sqrt(dx * dx + dy * dy), type: 'enemy', data: e });
    }
    for (const p of pickups) {
        if (p.collected)
            continue;
        const dx = p.x - player.x;
        const dy = p.y - player.y;
        sprites.push({ x: p.x, y: p.y, dist: Math.sqrt(dx * dx + dy * dy), type: 'pickup', data: p });
    }
    // Sort by distance (far first)
    sprites.sort((a, b) => b.dist - a.dist);
    for (const sprite of sprites) {
        const dx = sprite.x - player.x;
        const dy = sprite.y - player.y;
        // Transform relative to player angle
        const cos = Math.cos(player.angle);
        const sin = Math.sin(player.angle);
        const transformX = dx * cos + dy * sin;
        const transformY = -dx * sin + dy * cos;
        if (transformX <= 0.1)
            continue; // behind player
        const screenX = (0.5 + transformY / transformX * 0.5) * width;
        const spriteHeight = Math.min(viewHeight, (viewHeight / transformX) * 0.6);
        const spriteWidth = spriteHeight * 0.7;
        // Check z-buffer
        const bufIdx = Math.floor((screenX / width) * (zBuffer.length - 1));
        if (bufIdx >= 0 && bufIdx < zBuffer.length && zBuffer[bufIdx] < transformX * 0.8)
            continue;
        const spriteTop = halfH - spriteHeight / 2 + bob;
        if (sprite.type === 'enemy') {
            const enemy = sprite.data;
            const color = (0, enemies_1.getEnemyColor)(enemy);
            // Animated idle bob
            const idleBob = Math.sin(Date.now() / 400 + sprite.x * 10) * 3;
            // Legs
            const legW = spriteWidth * 0.2;
            const legH = spriteHeight * 0.25;
            const legY = spriteTop + spriteHeight * 0.7 + idleBob;
            ctx.fillStyle = shadeColor(color, 0.6);
            ctx.fillRect(screenX - spriteWidth * 0.3, legY, legW, legH);
            ctx.fillRect(screenX + spriteWidth * 0.1, legY, legW, legH);
            // Body/torso
            const bodyW = spriteWidth * 0.7;
            const bodyH = spriteHeight * 0.4;
            const bodyY = spriteTop + spriteHeight * 0.3 + idleBob;
            ctx.fillStyle = color;
            ctx.fillRect(screenX - bodyW / 2, bodyY, bodyW, bodyH);
            // Body detail - belt
            ctx.fillStyle = shadeColor(color, 0.5);
            ctx.fillRect(screenX - bodyW / 2, bodyY + bodyH * 0.6, bodyW, 3);
            // Arms
            const armW = spriteWidth * 0.15;
            const armH = spriteHeight * 0.35;
            const armY = bodyY + 5;
            ctx.fillStyle = shadeColor(color, 0.8);
            ctx.fillRect(screenX - bodyW / 2 - armW, armY, armW, armH);
            ctx.fillRect(screenX + bodyW / 2, armY, armW, armH);
            // Head
            const headR = spriteWidth * 0.25;
            const headY = spriteTop + spriteHeight * 0.18 + idleBob;
            ctx.fillStyle = shadeColor(color, 1.1);
            ctx.beginPath();
            ctx.arc(screenX, headY, headR, 0, Math.PI * 2);
            ctx.fill();
            // Eyes (menacing)
            const eyeSize = headR * 0.25;
            ctx.fillStyle = enemy.state === 'attack' ? '#f00' : '#ff0';
            ctx.fillRect(screenX - headR * 0.4, headY - headR * 0.15, eyeSize, eyeSize);
            ctx.fillRect(screenX + headR * 0.15, headY - headR * 0.15, eyeSize, eyeSize);
            // Mouth
            if (enemy.state === 'attack') {
                ctx.fillStyle = '#000';
                ctx.fillRect(screenX - headR * 0.2, headY + headR * 0.2, headR * 0.4, headR * 0.15);
            }
            // Boss crown/helmet
            if (enemy.type === 'boss') {
                ctx.fillStyle = '#ff0';
                ctx.fillRect(screenX - headR * 0.8, headY - headR * 1.1, headR * 1.6, headR * 0.3);
                ctx.fillRect(screenX - headR * 0.5, headY - headR * 1.3, headR * 0.3, headR * 0.3);
                ctx.fillRect(screenX + headR * 0.2, headY - headR * 1.3, headR * 0.3, headR * 0.3);
            }
            // Health bar
            if (enemy.health < enemy.maxHealth) {
                const barW = spriteWidth * 0.8;
                const barH = Math.max(3, spriteHeight * 0.03);
                const barX = screenX - barW / 2;
                const barY = spriteTop - 10 + idleBob;
                ctx.fillStyle = '#300';
                ctx.fillRect(barX, barY, barW, barH);
                const hpPct = enemy.health / enemy.maxHealth;
                ctx.fillStyle = hpPct > 0.5 ? '#0f0' : hpPct > 0.25 ? '#ff0' : '#f00';
                ctx.fillRect(barX, barY, barW * hpPct, barH);
            }
        }
        else {
            const pickup = sprite.data;
            const color = pickups_1.PICKUP_COLORS[pickup.type];
            // Pickup bobbing
            const bobY = Math.sin(Date.now() / 300) * 5;
            ctx.fillStyle = color;
            ctx.beginPath();
            ctx.arc(screenX, spriteTop + spriteHeight * 0.5 + bobY, spriteWidth * 0.25, 0, Math.PI * 2);
            ctx.fill();
            // Glow
            ctx.fillStyle = color + '44';
            ctx.beginPath();
            ctx.arc(screenX, spriteTop + spriteHeight * 0.5 + bobY, spriteWidth * 0.4, 0, Math.PI * 2);
            ctx.fill();
            // Label
            ctx.fillStyle = '#fff';
            ctx.font = '10px Courier New';
            ctx.textAlign = 'center';
            ctx.fillText(pickups_1.PICKUP_LABELS[pickup.type], screenX, spriteTop + spriteHeight * 0.3);
        }
    }
}
function drawWeapon(ctx, player, width, viewHeight, bob) {
    const w = types_1.WEAPONS[player.weapon];
    const weaponX = width / 2 + Math.cos(player.bobPhase) * 8;
    const weaponY = viewHeight - 80 + bob;
    const recoil = player.weaponAnim * 30;
    // Muzzle flash
    if (player.weaponAnim > 0.5) {
        const flashSize = player.weaponAnim * 30;
        ctx.fillStyle = `rgba(255, 255, 100, ${player.weaponAnim})`;
        ctx.beginPath();
        ctx.arc(weaponX, weaponY - 70 - recoil, flashSize, 0, Math.PI * 2);
        ctx.fill();
        ctx.fillStyle = `rgba(255, 255, 255, ${player.weaponAnim})`;
        ctx.beginPath();
        ctx.arc(weaponX, weaponY - 70 - recoil, flashSize * 0.5, 0, Math.PI * 2);
        ctx.fill();
    }
    const wy = weaponY - recoil;
    // Weapon body
    switch (player.weapon) {
        case 0: // Pistol
            ctx.fillStyle = '#777';
            ctx.fillRect(weaponX - 8, wy - 30, 16, 55);
            ctx.fillStyle = '#555';
            ctx.fillRect(weaponX - 12, wy + 10, 24, 35);
            ctx.fillStyle = '#888';
            ctx.fillRect(weaponX - 6, wy - 28, 12, 5);
            break;
        case 1: // Shotgun
            ctx.fillStyle = '#666';
            ctx.fillRect(weaponX - 12, wy - 50, 24, 75);
            ctx.fillStyle = '#555';
            ctx.fillRect(weaponX - 16, wy + 5, 32, 45);
            ctx.fillStyle = '#444';
            ctx.fillRect(weaponX - 6, wy - 55, 12, 18);
            // Pump detail
            ctx.fillStyle = '#777';
            ctx.fillRect(weaponX - 10, wy - 20, 20, 8);
            break;
        case 2: // Machine gun
            ctx.fillStyle = '#666';
            ctx.fillRect(weaponX - 10, wy - 40, 20, 65);
            ctx.fillStyle = '#555';
            ctx.fillRect(weaponX - 14, wy + 5, 28, 40);
            ctx.fillStyle = '#444';
            ctx.fillRect(weaponX - 4, wy - 45, 8, 15);
            // Magazine
            ctx.fillStyle = '#777';
            ctx.fillRect(weaponX - 16, wy - 20, 6, 25);
            // Barrel
            ctx.fillStyle = '#888';
            ctx.fillRect(weaponX - 3, wy - 50, 6, 12);
            break;
    }
}
function drawMinimap(ctx, player, enemies, width, _viewHeight) {
    const mapSize = 120;
    const cellSize = mapSize / map_1.MAP_W;
    const offsetX = width - mapSize - 10;
    const offsetY = 10;
    // Background
    ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
    ctx.fillRect(offsetX - 2, offsetY - 2, mapSize + 4, mapSize + 4);
    // Walls
    for (let y = 0; y < map_1.MAP_H; y++) {
        for (let x = 0; x < map_1.MAP_W; x++) {
            const tile = map_1.MAP[y * map_1.MAP_W + x];
            if (tile >= 1) {
                if (tile === 6) {
                    ctx.fillStyle = '#ff0';
                }
                else {
                    ctx.fillStyle = '#555';
                }
                ctx.fillRect(offsetX + x * cellSize, offsetY + y * cellSize, cellSize, cellSize);
            }
        }
    }
    // Player direction indicator
    ctx.fillStyle = '#0f0';
    ctx.beginPath();
    ctx.arc(offsetX + player.x * cellSize, offsetY + player.y * cellSize, 3, 0, Math.PI * 2);
    ctx.fill();
    // Direction line
    ctx.strokeStyle = '#0f0';
    ctx.lineWidth = 1;
    ctx.beginPath();
    ctx.moveTo(offsetX + player.x * cellSize, offsetY + player.y * cellSize);
    ctx.lineTo(offsetX + (player.x + Math.cos(player.angle) * 1.5) * cellSize, offsetY + (player.y + Math.sin(player.angle) * 1.5) * cellSize);
    ctx.stroke();
    // Enemies
    for (const e of enemies) {
        if (!e.alive)
            continue;
        ctx.fillStyle = '#f00';
        ctx.fillRect(offsetX + e.x * cellSize - 2, offsetY + e.y * cellSize - 2, 4, 4);
    }
}
};

// ── module: src/sound.ts ──
__mods["src/sound.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.playSound = playSound;
// Simple synthesized sound effects using Web Audio API
let audioCtx = null;
function getAudioCtx() {
    if (!audioCtx) {
        try {
            audioCtx = new (window.AudioContext || window.webkitAudioContext)();
        }
        catch {
            return null;
        }
    }
    return audioCtx;
}
function playSound(type) {
    const ctx = getAudioCtx();
    if (!ctx)
        return;
    const now = ctx.currentTime;
    switch (type) {
        case 'pistol': {
            const osc = ctx.createOscillator();
            const gain = ctx.createGain();
            osc.type = 'sawtooth';
            osc.frequency.setValueAtTime(200, now);
            osc.frequency.exponentialRampToValueAtTime(50, now + 0.15);
            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);
            // Noise burst
            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) * 0.5;
            const noise = ctx.createBufferSource();
            const noiseGain = ctx.createGain();
            noise.buffer = buffer;
            noiseGain.gain.setValueAtTime(0.4, now);
            noiseGain.gain.exponentialRampToValueAtTime(0.001, now + 0.05);
            noise.connect(noiseGain).connect(ctx.destination);
            noise.start(now);
            break;
        }
        case 'shotgun': {
            const osc = ctx.createOscillator();
            const gain = ctx.createGain();
            osc.type = 'sawtooth';
            osc.frequency.setValueAtTime(100, now);
            osc.frequency.exponentialRampToValueAtTime(30, now + 0.3);
            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.1;
            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();
            const noiseGain = ctx.createGain();
            noise.buffer = buffer;
            noiseGain.gain.setValueAtTime(0.6, now);
            noiseGain.gain.exponentialRampToValueAtTime(0.001, now + 0.1);
            noise.connect(noiseGain).connect(ctx.destination);
            noise.start(now);
            break;
        }
        case 'machinegun': {
            const osc = ctx.createOscillator();
            const gain = ctx.createGain();
            osc.type = 'square';
            osc.frequency.setValueAtTime(300, now);
            osc.frequency.exponentialRampToValueAtTime(80, now + 0.08);
            gain.gain.setValueAtTime(0.2, now);
            gain.gain.exponentialRampToValueAtTime(0.001, now + 0.08);
            osc.connect(gain).connect(ctx.destination);
            osc.start(now);
            osc.stop(now + 0.08);
            break;
        }
        case 'hit': {
            const osc = ctx.createOscillator();
            const gain = ctx.createGain();
            osc.type = 'square';
            osc.frequency.setValueAtTime(400, now);
            osc.frequency.exponentialRampToValueAtTime(100, now + 0.1);
            gain.gain.setValueAtTime(0.15, now);
            gain.gain.exponentialRampToValueAtTime(0.001, now + 0.1);
            osc.connect(gain).connect(ctx.destination);
            osc.start(now);
            osc.stop(now + 0.1);
            break;
        }
        case 'pickup': {
            const osc = ctx.createOscillator();
            const gain = ctx.createGain();
            osc.type = 'sine';
            osc.frequency.setValueAtTime(440, now);
            osc.frequency.setValueAtTime(660, now + 0.1);
            osc.frequency.setValueAtTime(880, now + 0.2);
            gain.gain.setValueAtTime(0.2, now);
            gain.gain.exponentialRampToValueAtTime(0.001, now + 0.3);
            osc.connect(gain).connect(ctx.destination);
            osc.start(now);
            osc.stop(now + 0.3);
            break;
        }
        case 'death': {
            const osc = ctx.createOscillator();
            const gain = ctx.createGain();
            osc.type = 'sawtooth';
            osc.frequency.setValueAtTime(300, now);
            osc.frequency.exponentialRampToValueAtTime(20, now + 1);
            gain.gain.setValueAtTime(0.3, now);
            gain.gain.exponentialRampToValueAtTime(0.001, now + 1);
            osc.connect(gain).connect(ctx.destination);
            osc.start(now);
            osc.stop(now + 1);
            break;
        }
        case 'enemy_die': {
            const osc = ctx.createOscillator();
            const gain = ctx.createGain();
            osc.type = 'sawtooth';
            osc.frequency.setValueAtTime(200, now);
            osc.frequency.exponentialRampToValueAtTime(30, now + 0.5);
            gain.gain.setValueAtTime(0.2, now);
            gain.gain.exponentialRampToValueAtTime(0.001, now + 0.5);
            osc.connect(gain).connect(ctx.destination);
            osc.start(now);
            osc.stop(now + 0.5);
            break;
        }
        case 'door': {
            const osc = ctx.createOscillator();
            const gain = ctx.createGain();
            osc.type = 'sine';
            osc.frequency.setValueAtTime(500, now);
            osc.frequency.setValueAtTime(700, now + 0.15);
            osc.frequency.setValueAtTime(900, now + 0.3);
            osc.frequency.setValueAtTime(1100, now + 0.45);
            gain.gain.setValueAtTime(0.2, now);
            gain.gain.exponentialRampToValueAtTime(0.001, now + 0.6);
            osc.connect(gain).connect(ctx.destination);
            osc.start(now);
            osc.stop(now + 0.6);
            break;
        }
    }
}
};

// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"qwen/qwen3.6-27b","provider":"lmstudio","persona":"typescript","sessionId":"946e9fa9-9fb4-4766-b3c9-c93e5a796d64","tokensIn":2438418,"tokensOut":33915,"tokensTotal":2472333,"cost":0,"turns":76,"toolCalls":75,"failedToolCalls":1,"timestamp":"2026-08-01T06:26:01.958Z"} -->