DOOM
Qwen 3.7 Flash · typescript

Reviews & generation details
Reviews
No human review yet. Unreviewed does not mean unsuccessful.
Generation context
Imported historical artifact. Its exact original instructions and lineage are not known.
Generation prompt · exact text unavailable
See the current prompt for reference. It may differ from the instructions used to generate this artifact.
Inspect original source 48,475 bytes · SHA-256 5e956ed6bb4e
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Crimson Depths</title>
<style>
/* ── css: layout ── */
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; background: #000; font-family: 'Courier New', monospace; }
#app { position: relative; width: 100%; height: 100%; }
#game { display: block; width: 100%; height: 100%; image-rendering: pixelated; image-rendering: crisp-edges; cursor: none; }
#crosshair {
position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
color: rgba(255,255,255,0.7); font-size: 28px; font-weight: bold; pointer-events: none; z-index: 10;
text-shadow: 0 0 4px rgba(255,0,0,0.5);
}
#hud {
position: absolute; bottom: 0; left: 0; right: 0; height: 90px;
display: flex; align-items: center; justify-content: space-between;
padding: 0 30px; background: linear-gradient(transparent 0%, rgba(0,0,0,0.85) 40%);
color: #fff; font-family: 'Courier New', monospace; z-index: 5; pointer-events: none;
border-top: 2px solid rgba(255,68,68,0.3);
}
#hud > div { min-width: 120px; }
#health-bar {
font-size: 22px; color: #ff4444; font-weight: bold;
text-shadow: 0 0 10px rgba(255,0,0,0.3);
}
#ammo-count { font-size: 32px; color: #ffaa00; font-weight: bold; text-align: center; text-shadow: 0 0 10px rgba(255,170,0,0.3); }
#weapon-name { font-size: 18px; color: #ccc; text-align: right; text-shadow: 0 0 5px rgba(255,255,255,0.2); }
#minimap-canvas { border: 1px solid rgba(255,255,255,0.15); background: rgba(0,0,0,0.6); image-rendering: pixelated; }
#message-overlay {
position: absolute; top: 35%; left: 50%; transform: translate(-50%, -50%);
color: #ffcc00; font-family: 'Courier New', monospace; font-size: 24px; font-weight: bold;
text-shadow: 0 0 15px rgba(255,200,0,0.5); z-index: 15; pointer-events: none;
opacity: 1; transition: opacity 0.3s;
}
#start-screen {
position: absolute; inset: 0; display: flex; flex-direction: column;
align-items: center; justify-content: center; background: radial-gradient(ellipse at center, #1a0505 0%, #000 70%);
z-index: 25; cursor: pointer;
}
#start-screen h1 {
color: #ff3333; font-size: 56px; letter-spacing: 4px;
text-shadow: 0 0 30px rgba(255,0,0,0.5), 0 0 60px rgba(255,0,0,0.3);
margin-bottom: 10px;
}
#start-screen p { color: #aa6666; font-size: 18px; margin: 5px 0; }
#start-screen .blink { animation: blink 1.5s ease-in-out infinite; margin-top: 30px; color: #ff8888; font-size: 20px; }
@keyframes blink { 0%,100% { opacity: 1; } 50% { opacity: 0.3; } }
#death-screen, #win-screen {
position: absolute; inset: 0; display: flex; flex-direction: column;
align-items: center; justify-content: center; z-index: 20;
}
#death-screen { background: rgba(100,0,0,0.8); }
#death-screen h1 { color: #ff0000; font-size: 72px; text-shadow: 0 0 30px rgba(255,0,0,0.5); margin-bottom: 10px; }
#win-screen { background: rgba(0,50,0,0.85); }
#win-screen h1 { color: #44ff44; font-size: 52px; text-shadow: 0 0 30px rgba(0,255,0,0.5); margin-bottom: 10px; }
#win-screen button {
margin-top: 20px; padding: 14px 40px; font-family: 'Courier New', monospace; font-size: 20px;
background: #44ff44; color: #000; border: none; cursor: pointer; pointer-events: auto;
font-weight: bold; letter-spacing: 2px;
}
#win-screen button:hover { background: #66ff66; box-shadow: 0 0 20px rgba(0,255,0,0.5); }
.hidden { display: none !important; }
</style>
</head>
<body>
<div id="app">
<canvas id="game"></canvas>
<div id="hud">
<div id="health-bar"><span id="health-val">100</span>%</div>
<div id="ammo-count">20</div>
<div id="weapon-name">Pistol</div>
<canvas id="minimap-canvas" width="128" height="128"></canvas>
<div id="message-overlay" class="hidden"></div>
</div>
<div id="death-screen" class="hidden">
<h1>YOU DIED</h1>
<p>Click or press Space to respawn</p>
</div>
<div id="win-screen" class="hidden">
<h1>LEVEL COMPLETE!</h1>
<p>You escaped the Crimson Depths.</p>
<button id="restart-btn">Play Again</button>
</div>
<div id="crosshair">+</div>
<div id="start-screen">
<h1>CRIMSON DEPTHS</h1>
<p>WASD - Move | Mouse - Look | Click/Space - Shoot & Switch Weapon</p>
<p>R - Restart | Find the yellow exit door to escape!</p>
<div class="blink">[ CLICK TO ENTER ]</div>
</div></div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./game":"src/game.ts"},"src/game.ts":{"./map":"src/map.ts","./audio":"src/audio.ts","./input":"src/input.ts","./weapons":"src/weapons.ts"}};
function __require(id) {
if (__cache[id]) return __cache[id].exports;
var module = __cache[id] = { exports: {} };
var factory = __mods[id];
if (!factory) throw new Error("Module not found: " + id);
factory(module.exports, function (spec) {
var target = (__map[id] && __map[id][spec]) || spec;
return __require(target);
}, module);
return module.exports;
}
// ── module: src/main.ts ──
__mods["src/main.ts"] = function (exports, require, module) {
"use strict";
// Entry point. Runs after the DOM is parsed and after any classic
// <script> imports, so document queries and CDN globals are available.
// Import your other modules from here.
Object.defineProperty(exports, "__esModule", { value: true });
exports.start = start;
const game_1 = require("./game");
function start() {
(0, game_1.startGame)();
}
start();
};
// ── module: src/map.ts ──
__mods["src/map.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.WALL_COLORS = exports.WORLD_MAP = exports.MAP_HEIGHT = exports.MAP_WIDTH = void 0;
exports.getEntities = getEntities;
exports.getPlayerStart = getPlayerStart;
exports.isWall = isWall;
exports.isExit = isExit;
// Map definition - 2D grid
// 0=empty, 1-6=wall types (different colors), 7=exit door
exports.MAP_WIDTH = 32;
exports.MAP_HEIGHT = 32;
exports.WORLD_MAP = [
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 2, 2, 2, 0, 0, 0, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 4, 4, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 2, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 5, 5, 5, 0, 0, 1],
[1, 0, 0, 2, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 5, 5, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 6, 6, 6, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 4, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 3, 3, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 6, 0, 0, 1],
[1, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 1],
[1, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
];
exports.WALL_COLORS = {
1: [120, 40, 40], // Red brick
2: [60, 60, 120], // Blue stone
3: [80, 100, 50], // Green mossy
4: [140, 100, 30], // Brown wood
5: [100, 50, 100], // Purple dark
6: [50, 120, 120], // Cyan tech
};
function getEntities() {
return [
// Room 1 area
{ type: 'enemy', subtype: 'demon', x: 5.5, y: 5.5, hp: 30, maxHp: 30, speed: 0.015, damage: 8 },
{ type: 'enemy', subtype: 'demon', x: 10.5, y: 3.5, hp: 30, maxHp: 30, speed: 0.015, damage: 8 },
// Corridor enemies
{ type: 'enemy', subtype: 'soldier', x: 15.5, y: 9.5, hp: 20, maxHp: 20, speed: 0.02, damage: 12 },
{ type: 'enemy', subtype: 'soldier', x: 18.5, y: 8.5, hp: 20, maxHp: 20, speed: 0.02, damage: 12 },
{ type: 'enemy', subtype: 'demon', x: 22.5, y: 15.5, hp: 40, maxHp: 40, speed: 0.012, damage: 10 },
// Room 4
{ type: 'enemy', subtype: 'soldier', x: 28.5, y: 18.5, hp: 20, maxHp: 20, speed: 0.02, damage: 12 },
{ type: 'enemy', subtype: 'soldier', x: 25.5, y: 22.5, hp: 20, maxHp: 20, speed: 0.02, damage: 12 },
// Boss room area
{ type: 'enemy', subtype: 'demon', x: 20.5, y: 25.5, hp: 50, maxHp: 50, speed: 0.01, damage: 15 },
{ type: 'enemy', subtype: 'demon', x: 28.5, y: 28.5, hp: 30, maxHp: 30, speed: 0.015, damage: 10 },
{ type: 'enemy', subtype: 'soldier', x: 14.5, y: 27.5, hp: 20, maxHp: 20, speed: 0.02, damage: 12 },
// More scattered
{ type: 'enemy', subtype: 'soldier', x: 8.5, y: 20.5, hp: 20, maxHp: 20, speed: 0.02, damage: 10 },
{ type: 'enemy', subtype: 'demon', x: 26.5, y: 10.5, hp: 35, maxHp: 35, speed: 0.014, damage: 9 },
{ type: 'enemy', subtype: 'soldier', x: 3.5, y: 25.5, hp: 20, maxHp: 20, speed: 0.02, damage: 12 },
{ type: 'enemy', subtype: 'demon', x: 12.5, y: 29.5, hp: 30, maxHp: 30, speed: 0.015, damage: 8 },
// Health pickups
{ type: 'pickup', subtype: 'medkit', x: 3.5, y: 2.5, pickupType: 'health', amount: 25 },
{ type: 'pickup', subtype: 'medkit', x: 8.5, y: 7.5, pickupType: 'health', amount: 25 },
{ type: 'pickup', subtype: 'medkit', x: 17.5, y: 13.5, pickupType: 'health', amount: 50 },
{ type: 'pickup', subtype: 'medkit', x: 25.5, y: 12.5, pickupType: 'health', amount: 30 },
{ type: 'pickup', subtype: 'medkit', x: 29.5, y: 23.5, pickupType: 'health', amount: 50 },
{ type: 'pickup', subtype: 'medkit', x: 15.5, y: 25.5, pickupType: 'health', amount: 25 },
// Ammo pickups
{ type: 'pickup', subtype: 'shell', x: 6.5, y: 4.5, pickupType: 'ammo', amount: 10 },
{ type: 'pickup', subtype: 'shell', x: 13.5, y: 5.5, pickupType: 'ammo', amount: 15 },
{ type: 'pickup', subtype: 'shell', x: 20.5, y: 10.5, pickupType: 'ammo', amount: 10 },
{ type: 'pickup', subtype: 'shell', x: 27.5, y: 16.5, pickupType: 'ammo', amount: 20 },
{ type: 'pickup', subtype: 'shell', x: 3.5, y: 15.5, pickupType: 'ammo', amount: 15 },
{ type: 'pickup', subtype: 'shell', x: 22.5, y: 28.5, pickupType: 'ammo', amount: 10 },
];
}
function getPlayerStart() {
return { x: 2.5, y: 2.5 };
}
function isWall(x, y) {
const ix = Math.floor(x);
const iy = Math.floor(y);
if (ix < 0 || ix >= exports.MAP_WIDTH || iy < 0 || iy >= exports.MAP_HEIGHT)
return true;
return exports.WORLD_MAP[iy][ix] !== 0;
}
function isExit(x, y) {
const ix = Math.floor(x);
const iy = Math.floor(y);
if (ix < 0 || ix >= exports.MAP_WIDTH || iy < 0 || iy >= exports.MAP_HEIGHT)
return false;
return exports.WORLD_MAP[iy][ix] === 7;
}
};
// ── module: src/audio.ts ──
__mods["src/audio.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.playSound = playSound;
exports.playAmbient = playAmbient;
// Simple Web Audio API sound effects - no external assets needed
const ctx = new (window.AudioContext || window.webkitAudioContext)();
function playSound(type) {
if (ctx.state === 'suspended')
ctx.resume();
const t = ctx.currentTime;
switch (type) {
case 'pistol': {
const o = ctx.createOscillator();
const g = ctx.createGain();
o.type = 'sawtooth';
o.frequency.setValueAtTime(300, t);
o.frequency.exponentialRampToValueAtTime(80, t + 0.15);
g.gain.setValueAtTime(0.25, t);
g.gain.exponentialRampToValueAtTime(0.001, t + 0.15);
o.connect(g).connect(ctx.destination);
o.start(t);
o.stop(t + 0.15);
break;
}
case 'shotgun': {
// Noise burst for shotgun
const bufSize = ctx.sampleRate * 0.3;
const buf = ctx.createBuffer(1, bufSize, ctx.sampleRate);
const data = buf.getChannelData(0);
for (let i = 0; i < bufSize; i++)
data[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / bufSize, 2);
const src = ctx.createBufferSource();
const g = ctx.createGain();
src.buffer = buf;
g.gain.setValueAtTime(0.4, t);
g.gain.exponentialRampToValueAtTime(0.001, t + 0.3);
src.connect(g).connect(ctx.destination);
src.start(t);
// Low boom
const o = ctx.createOscillator();
const g2 = ctx.createGain();
o.type = 'sine';
o.frequency.setValueAtTime(150, t);
o.frequency.exponentialRampToValueAtTime(40, t + 0.2);
g2.gain.setValueAtTime(0.3, t);
g2.gain.exponentialRampToValueAtTime(0.001, t + 0.2);
o.connect(g2).connect(ctx.destination);
o.start(t);
o.stop(t + 0.2);
break;
}
case 'machinegun': {
const o = ctx.createOscillator();
const g = ctx.createGain();
o.type = 'square';
o.frequency.setValueAtTime(400, t);
o.frequency.exponentialRampToValueAtTime(150, t + 0.06);
g.gain.setValueAtTime(0.12, t);
g.gain.exponentialRampToValueAtTime(0.001, t + 0.06);
o.connect(g).connect(ctx.destination);
o.start(t);
o.stop(t + 0.06);
break;
}
case 'hit': {
const o = ctx.createOscillator();
const g = ctx.createGain();
o.type = 'sine';
o.frequency.setValueAtTime(200, t);
o.frequency.exponentialRampToValueAtTime(100, t + 0.1);
g.gain.setValueAtTime(0.2, t);
g.gain.exponentialRampToValueAtTime(0.001, t + 0.1);
o.connect(g).connect(ctx.destination);
o.start(t);
o.stop(t + 0.1);
break;
}
case 'pickup': {
const o = ctx.createOscillator();
const g = ctx.createGain();
o.type = 'sine';
o.frequency.setValueAtTime(440, t);
o.frequency.linearRampToValueAtTime(880, t + 0.15);
g.gain.setValueAtTime(0.2, t);
g.gain.exponentialRampToValueAtTime(0.001, t + 0.2);
o.connect(g).connect(ctx.destination);
o.start(t);
o.stop(t + 0.2);
break;
}
case 'enemy_die': {
const o = ctx.createOscillator();
const g = ctx.createGain();
o.type = 'sawtooth';
o.frequency.setValueAtTime(400, t);
o.frequency.exponentialRampToValueAtTime(50, t + 0.4);
g.gain.setValueAtTime(0.25, t);
g.gain.exponentialRampToValueAtTime(0.001, t + 0.4);
o.connect(g).connect(ctx.destination);
o.start(t);
o.stop(t + 0.4);
break;
}
case 'hurt': {
const o = ctx.createOscillator();
const g = ctx.createGain();
o.type = 'sawtooth';
o.frequency.setValueAtTime(150, t);
o.frequency.linearRampToValueAtTime(80, t + 0.2);
g.gain.setValueAtTime(0.3, t);
g.gain.exponentialRampToValueAtTime(0.001, t + 0.2);
o.connect(g).connect(ctx.destination);
o.start(t);
o.stop(t + 0.2);
break;
}
case 'exit': {
for (let i = 0; i < 3; i++) {
const o = ctx.createOscillator();
const g = ctx.createGain();
o.type = 'sine';
o.frequency.setValueAtTime(440 * Math.pow(1.26, i), t + i * 0.15);
g.gain.setValueAtTime(0.2, t + i * 0.15);
g.gain.exponentialRampToValueAtTime(0.001, t + i * 0.15 + 0.3);
o.connect(g).connect(ctx.destination);
o.start(t + i * 0.15);
o.stop(t + i * 0.15 + 0.3);
}
break;
}
case 'empty': {
const o = ctx.createOscillator();
const g = ctx.createGain();
o.type = 'triangle';
o.frequency.setValueAtTime(800, t);
g.gain.setValueAtTime(0.05, t);
g.gain.exponentialRampToValueAtTime(0.001, t + 0.05);
o.connect(g).connect(ctx.destination);
o.start(t);
o.stop(t + 0.05);
break;
}
}
}
function playAmbient() {
// Subtle low drone
const o = ctx.createOscillator();
const g = ctx.createGain();
o.type = 'sine';
o.frequency.value = 55;
g.gain.value = 0.03;
o.connect(g).connect(ctx.destination);
o.start();
}
};
// ── module: src/input.ts ──
__mods["src/input.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.initInput = initInput;
exports.getInput = getInput;
exports.resetWeaponCycle = resetWeaponCycle;
exports.getMouseDelta = getMouseDelta;
exports.consumeMouseY = consumeMouseY;
exports.setMouseClamp = setMouseClamp;
const keys = new Map();
let mouseX = 0;
let mouseY = 0;
let pointerLocked = false;
function initInput(canvas) {
document.addEventListener('keydown', (e) => {
keys.set(e.code, true);
// Prevent scrolling with space/arrows
if (['Space', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.code))
e.preventDefault();
});
document.addEventListener('keyup', (e) => {
keys.set(e.code, false);
});
canvas.addEventListener('click', () => {
if (!pointerLocked) {
canvas.requestPointerLock();
}
});
document.addEventListener('pointerlockchange', () => {
pointerLocked = document.pointerLockElement === canvas;
});
document.addEventListener('mousemove', (e) => {
if (pointerLocked) {
mouseX += e.movementX;
mouseY += e.movementY;
}
});
}
function getInput() {
const state = {
forward: keys.get('KeyW') === true || keys.get('ArrowUp') === true,
backward: keys.get('KeyS') === true || keys.get('ArrowDown') === true,
left: keys.get('KeyA') === true || keys.get('ArrowLeft') === true,
right: keys.get('KeyD') === true || keys.get('ArrowRight') === true,
strafeLeft: keys.get('KeyQ') === true,
strafeRight: keys.get('KeyE') === true,
shoot: keys.get('Space') === true,
weaponCycle: keys.has('KeyR'),
prevWeaponCycle: false,
};
return state;
}
function resetWeaponCycle() { }
function getMouseDelta() {
const val = mouseX;
mouseX = 0;
return val;
}
function consumeMouseY() {
const val = mouseY;
mouseY = Math.max(-200, Math.min(200, val)); // clamp look up/down
return val;
}
function setMouseClamp(val) { mouseY = val; }
};
// ── module: src/weapons.ts ──
__mods["src/weapons.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Weapon = exports.WEAPONS = exports.WEAPON_NAMES = void 0;
exports.WEAPON_NAMES = ['Pistol', 'Shotgun', 'Machine Gun'];
exports.WEAPONS = [
{ name: 'Pistol', damage: 15, fireRate: 400, ammoCost: 1, spread: 0.02, pellets: 1, reloadTime: 800 },
{ name: 'Shotgun', damage: 10, fireRate: 700, ammoCost: 1, spread: 0.15, pellets: 5, reloadTime: 1200 },
{ name: 'Machine Gun', damage: 8, fireRate: 90, ammoCost: 1, spread: 0.06, pellets: 1, reloadTime: 600 },
];
class Weapon {
constructor() {
this.currentIdx = 0;
this.lastFire = 0;
this.reloading = false;
this.reloadStart = 0;
this.muzzleFlash = 0;
}
get current() { return exports.WEAPONS[this.currentIdx]; }
switchNext() {
this.currentIdx = (this.currentIdx + 1) % exports.WEAPONS.length;
this.reloading = false;
}
tryFire(now, ammo) {
if (this.reloading)
return false;
if (ammo < this.current.ammoCost)
return false;
if (now - this.lastFire < this.current.fireRate)
return false;
this.lastFire = now;
this.muzzleFlash = 6;
return true;
}
update(now) {
if (this.muzzleFlash > 0)
this.muzzleFlash--;
if (this.reloading && now - this.reloadStart >= this.current.reloadTime) {
this.reloading = false;
}
}
}
exports.Weapon = Weapon;
};
// ── module: src/game.ts ──
__mods["src/game.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.startGame = startGame;
const map_1 = require("./map");
const audio_1 = require("./audio");
const input_1 = require("./input");
const weapons_1 = require("./weapons");
const MOVE_SPEED = 0.04;
const STRAFE_SPEED = 0.03;
const ROT_SPEED = 0.0035;
const CANVAS_W = 640;
const CANVAS_H = 360;
const FOV = Math.PI / 3;
const MAX_DEPTH = 20;
var GameState;
(function (GameState) {
GameState[GameState["PLAYING"] = 0] = "PLAYING";
GameState[GameState["DEAD"] = 1] = "DEAD";
GameState[GameState["WIN"] = 2] = "WIN";
})(GameState || (GameState = {}));
let zBuffer = new Float32Array(CANVAS_W);
let playerState = null;
let gameState = GameState.PLAYING;
function startGame() {
const canvas = document.getElementById('game');
canvas.width = CANVAS_W;
canvas.height = CANVAS_H;
const ctx = canvas.getContext('2d', { alpha: false });
const imgData = ctx.createImageData(CANVAS_W, CANVAS_H);
// Start screen
const startScreen = document.getElementById('start-screen');
if (startScreen)
startScreen.style.display = 'flex';
(0, input_1.initInput)(canvas);
// Hide start screen on click
const onClickStart = () => {
if (startScreen)
startScreen.style.display = 'none';
window.removeEventListener('click', onClickStart);
};
window.addEventListener('click', onClickStart);
let entities = resetEntities();
let pickupSprites = [];
let enemySprites = [];
let msgTimer = 0;
document.getElementById('death-screen').classList.add('hidden');
document.getElementById('win-screen').classList.add('hidden');
const restartHandler = () => {
if (gameState === GameState.DEAD || gameState === GameState.WIN) {
resetLevel(entities);
}
};
document.getElementById('restart-btn').addEventListener('click', restartHandler);
// Also allow any key/click to respawn after death
window.addEventListener('keydown', (e) => {
if ((gameState === GameState.DEAD || gameState === GameState.WIN) && e.code !== 'ControlLeft' && e.code !== 'AltLeft') {
restartHandler();
}
});
canvas.addEventListener('click', () => {
if (gameState === GameState.DEAD || gameState === GameState.WIN) {
restartHandler();
}
});
function resetEntities() {
return (0, map_1.getEntities)().map(e => ({ ...e }));
}
function resetLevel(entities) {
const s = (0, map_1.getPlayerStart)();
playerState = {
x: s.x, y: s.y, angle: 0,
health: 100, maxHealth: 100, ammo: 30,
weapon: new weapons_1.Weapon(), bobPhase: 0, damageFlash: 0, lastShoot: false, prevWeaponCycle: false,
};
for (const e of entities) {
if (e.type === 'enemy') {
e.hp = e.maxHp;
e.alive = true;
}
if (e.type === 'pickup')
e.collected = false;
}
gameState = GameState.PLAYING;
document.getElementById('death-screen').classList.add('hidden');
document.getElementById('win-screen').classList.add('hidden');
}
resetLevel(entities);
function canMove(nx, ny) {
const r = 0.2;
return !(0, map_1.isWall)(nx - r, ny - r) && !(0, map_1.isWall)(nx + r, ny - r) &&
!(0, map_1.isWall)(nx - r, ny + r) && !(0, map_1.isWall)(nx + r, ny + r);
}
function rebuildSprites() {
pickupSprites.length = 0;
enemySprites.length = 0;
for (const e of entities) {
if (e.type === 'pickup' && !e.collected)
pickupSprites.push({ x: e.x, y: e.y, entity: e });
if (e.type === 'enemy' && e.alive)
enemySprites.push({ x: e.x, y: e.y, entity: e });
}
}
rebuildSprites();
function sortSprites(sprites) {
const p = playerState;
const arr = sprites.map(s => ({ s, d: (s.x - p.x) ** 2 + (s.y - p.y) ** 2 }));
arr.sort((a, b) => b.d - a.d);
return arr.map(a => a.s);
}
function showMessage(text) {
const el = document.getElementById('message-overlay');
el.textContent = text;
el.classList.remove('hidden');
msgTimer = 2000;
}
function hideMessage() {
document.getElementById('message-overlay').classList.add('hidden');
}
function castWallRay(px, py, rayAngle, zBuf, x) {
const cosA = Math.cos(rayAngle);
const sinA = Math.sin(rayAngle);
const dcx = cosA || 1e-10;
const dcy = sinA || 1e-10;
let mapX = Math.floor(px);
let mapY = Math.floor(py);
const ddx = Math.abs(1 / dcx);
const ddy = Math.abs(1 / dcy);
let stepX, stepY;
let sdX, sdY;
let side = 0;
if (dcx < 0) {
stepX = -1;
sdX = (px - mapX) * ddx;
}
else {
stepX = 1;
sdX = (mapX + 1 - px) * ddx;
}
if (dcy < 0) {
stepY = -1;
sdY = (py - mapY) * ddy;
}
else {
stepY = 1;
sdY = (mapY + 1 - py) * ddy;
}
for (let i = 0; i < 64; i++) {
if (sdX < sdY) {
sdX += ddx;
mapX += stepX;
side = 0;
}
else {
sdY += ddy;
mapY += stepY;
side = 1;
}
if (mapX >= 0 && mapX < map_1.MAP_WIDTH && mapY >= 0 && mapY < map_1.MAP_HEIGHT) {
const wt = map_1.WORLD_MAP[mapY][mapX];
if (wt > 0) {
let pd;
if (side === 0)
pd = (mapX - px + (1 - stepX) / 2) / dcx;
else
pd = (mapY - py + (1 - stepY) / 2) / dcy;
zBuf[x] = pd;
return { hit: true, perpDist: pd, wallType: wt, side };
}
}
else
break;
}
zBuf[x] = MAX_DEPTH;
return { hit: false, perpDist: MAX_DEPTH, wallType: 0, side: 0 };
}
function drawSprite(ctx, sp, colorR, colorG, colorB, zBuf, halfH) {
const p = playerState;
const dx = sp.x - p.x;
const dy = sp.y - p.y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 0.2 || dist > MAX_DEPTH)
return;
let spriteAngle = Math.atan2(dy, dx) - p.angle;
while (spriteAngle > Math.PI)
spriteAngle -= 2 * Math.PI;
while (spriteAngle < -Math.PI)
spriteAngle += 2 * Math.PI;
if (Math.abs(spriteAngle) > FOV * 0.7)
return;
const screenX = (0.5 + spriteAngle / FOV) * CANVAS_W;
const spriteSize = Math.floor(CANVAS_H / dist);
if (spriteSize < 6)
return;
const xStart = Math.floor(screenX - spriteSize / 2);
const yStart = Math.floor(halfH - spriteSize / 2);
// Z-buffer check at center column
const colIdx = Math.max(0, Math.min(CANVAS_W - 1, Math.floor(screenX)));
if (dist >= zBuf[colIdx])
return;
const fog = Math.max(0.2, 1 - dist / MAX_DEPTH);
const r = Math.floor(colorR * fog);
const g = Math.floor(colorG * fog);
const b = Math.floor(colorB * fog);
ctx.save();
if (sp.entity.pickupType === 'health') {
// Health medkit - glowing green box
const s = spriteSize * 0.4;
const ox = screenX - s / 2, oy = yStart + spriteSize * 0.5;
ctx.fillStyle = `rgba(0,${200 * fog | 0},0,${fog})`;
ctx.fillRect(ox, oy, s, s * 0.7);
ctx.fillStyle = `rgba(255,255,255,${0.6 * fog})`;
ctx.fillRect(ox + s * 0.4, oy + s * 0.15, s * 0.2, s * 0.4);
ctx.fillRect(ox + s * 0.15, oy + s * 0.3, s * 0.5, s * 0.1);
}
else if (sp.entity.pickupType === 'ammo') {
// Ammo shell - orange cylinder
const s = spriteSize * 0.3;
const ox = screenX - s / 2, oy = yStart + spriteSize * 0.5;
ctx.fillStyle = `rgba(${200 * fog | 0},${150 * fog | 0},0,${fog})`;
ctx.beginPath();
ctx.ellipse(screenX, oy + s * 0.5, s * 0.5, s * 0.4, 0, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = `rgba(255,200,0,${0.5 * fog})`;
ctx.lineWidth = 1;
ctx.stroke();
}
else {
// Enemy - detailed figure
const bw = spriteSize * 0.5;
const bh = spriteSize * 0.35;
const headS = spriteSize * 0.28;
const legW = Math.max(2, spriteSize * 0.08);
// Shadow
ctx.fillStyle = `rgba(0,0,0,${0.3 * fog})`;
ctx.beginPath();
ctx.ellipse(screenX, yStart + spriteSize, spriteSize * 0.15, spriteSize * 0.04, 0, 0, Math.PI * 2);
ctx.fill();
// Legs
ctx.fillStyle = `rgb(${Math.floor(r * 0.5)},${Math.floor(g * 0.5)},${Math.floor(b * 0.5)})`;
ctx.fillRect(xStart + spriteSize * 0.35, yStart + spriteSize * 0.6, legW, spriteSize * 0.35);
ctx.fillRect(xStart + spriteSize * 0.58, yStart + spriteSize * 0.6, legW, spriteSize * 0.35);
// Body
ctx.fillStyle = `rgb(${r},${g},${b})`;
ctx.fillRect(xStart + (spriteSize - bw) / 2, yStart + spriteSize * 0.25, bw, bh);
// Shoulders/pads
ctx.fillStyle = `rgb(${Math.min(255, r + 20)},${Math.min(255, g + 10)},${Math.min(255, b + 10)})`;
ctx.fillRect(xStart + (spriteSize - bw) / 2 - 2, yStart + spriteSize * 0.25, 8, 6);
ctx.fillRect(xStart + spriteSize - (spriteSize - bw) / 2 - 6, yStart + spriteSize * 0.25, 8, 6);
// Head
ctx.fillStyle = `rgb(${Math.min(255, r + 50)},${Math.max(0, g - 30)},${Math.max(0, b - 20)})`;
ctx.beginPath();
ctx.arc(screenX, yStart + spriteSize * 0.18, headS / 2, 0, Math.PI * 2);
ctx.fill();
// Glowing eyes
ctx.fillStyle = `rgba(255,${80 * fog | 0},0,${fog})`;
ctx.beginPath();
ctx.arc(xStart + spriteSize * 0.38, yStart + spriteSize * 0.16, Math.max(1, headS * 0.15), 0, Math.PI * 2);
ctx.arc(xStart + spriteSize * 0.62, yStart + spriteSize * 0.16, Math.max(1, headS * 0.15), 0, Math.PI * 2);
ctx.fill();
// Mouth (growl)
ctx.strokeStyle = `rgba(200,0,0,${0.7 * fog})`;
ctx.lineWidth = Math.max(1, spriteSize * 0.015);
ctx.beginPath();
ctx.arc(screenX, yStart + spriteSize * 0.24, headS * 0.18, 0.1, Math.PI - 0.1);
ctx.stroke();
// Weapon/arm visible
ctx.fillStyle = `rgb(${Math.floor(r * 0.7)},${Math.floor(g * 0.7)},${Math.floor(b * 0.7)})`;
ctx.fillRect(xStart + spriteSize * 0.7, yStart + spriteSize * 0.35, spriteSize * 0.12, 3);
}
ctx.restore();
}
function updateEnemies(dt) {
for (const e of entities) {
if (e.type !== 'enemy' || !e.alive)
continue;
const p = playerState;
const dx = p.x - e.x, dy = p.y - e.y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist > 1.2 && dist < 14) {
const spd = (e.speed || 0.012) * Math.max(1, dt / 16);
const mx = (dx / dist) * spd;
const my = (dy / dist) * spd;
if (canMove(e.x + mx, e.y))
e.x += mx;
if (canMove(e.x, e.y + my))
e.y += my;
}
if (dist < 1.1) {
const now = performance.now();
const cd = (e.lastAttack || 0) + (e.attackCooldown || 1000);
if (now > cd) {
p.health -= e.damage || 5;
e.lastAttack = now;
p.damageFlash = 12;
(0, audio_1.playSound)('hurt');
if (p.health <= 0) {
p.health = 0;
gameState = GameState.DEAD;
document.getElementById('death-screen').classList.remove('hidden');
}
}
}
}
}
function checkPickups() {
for (const e of entities) {
if (e.type !== 'pickup' || e.collected)
continue;
const p = playerState;
const dx = p.x - e.x, dy = p.y - e.y;
if (dx * dx + dy * dy < 0.25) {
e.collected = true;
(0, audio_1.playSound)('pickup');
if (e.pickupType === 'health') {
p.health = Math.min(p.maxHealth, p.health + (e.amount || 10));
showMessage('+' + (e.amount || 10) + ' Health');
}
else if (e.pickupType === 'ammo') {
p.ammo += (e.amount || 5);
showMessage('+' + (e.amount || 5) + ' Ammo');
}
}
}
}
function shootWeapon(now) {
const p = playerState;
const w = p.weapon;
if (!w.tryFire(now, p.ammo))
return;
const def = w.current;
p.ammo -= def.ammoCost;
(0, audio_1.playSound)(def.name === 'Pistol' ? 'pistol' : def.name === 'Shotgun' ? 'shotgun' : 'machinegun');
for (let pellet = 0; pellet < def.pellets; pellet++) {
const spread = (Math.random() - 0.5) * def.spread;
const ra = p.angle + spread;
const dirX = Math.cos(ra), dirY = Math.sin(ra);
// Raycast to find closest enemy in direction
let bestEnemy = null;
let bestDist = Infinity;
for (const ent of entities) {
if (ent.type !== 'enemy' || !ent.alive)
continue;
const edx = ent.x - p.x, edy = ent.y - p.y;
const eDist = Math.sqrt(edx * edx + edy * edy);
if (eDist > bestDist || eDist > MAX_DEPTH)
continue;
const dot = (edx * dirX + edy * dirY) / eDist;
if (dot < 0.9)
continue;
// Wall check
let blocked = false;
for (let t = 0; t < eDist; t += 0.05) {
if ((0, map_1.isWall)(p.x + dirX * t, p.y + dirY * t)) {
blocked = true;
break;
}
}
if (blocked)
continue;
// Hit radius
const perpDist = Math.abs(edy * dirX - edx * dirY);
if (perpDist < 0.35 && eDist < bestDist) {
bestDist = eDist;
bestEnemy = ent;
}
}
if (bestEnemy) {
bestEnemy.hp = (bestEnemy.hp || 0) - def.damage;
(0, audio_1.playSound)('hit');
if ((bestEnemy.hp || 0) <= 0) {
bestEnemy.alive = false;
(0, audio_1.playSound)('enemy_die');
rebuildSprites();
}
}
}
}
function renderMinimap() {
const el = document.getElementById('minimap-canvas');
if (!el)
return;
const mctx = el.getContext('2d');
const ms = 3.8;
mctx.fillStyle = '#000';
mctx.fillRect(0, 0, 128, 128);
for (let y = 0; y < map_1.MAP_HEIGHT; y++) {
for (let x = 0; x < map_1.MAP_WIDTH; x++) {
if (map_1.WORLD_MAP[y][x] > 0) {
const c = map_1.WALL_COLORS[map_1.WORLD_MAP[y][x]] || [128, 128, 128];
mctx.fillStyle = `rgb(${c[0]},${c[1]},${c[2]})`;
mctx.fillRect(x * ms, y * ms, ms, ms);
}
}
}
// Enemies
for (const e of entities) {
if (e.type !== 'enemy' || !e.alive)
continue;
mctx.fillStyle = '#f00';
mctx.fillRect(e.x * ms - 1, e.y * ms - 1, 3, 3);
}
// Pickups
for (const e of entities) {
if (e.type !== 'pickup' || e.collected)
continue;
mctx.fillStyle = e.pickupType === 'health' ? '#0f0' : '#fa0';
mctx.fillRect(e.x * ms - 1, e.y * ms - 1, 2, 2);
}
// Player
const p = playerState;
mctx.fillStyle = '#fff';
mctx.beginPath();
mctx.arc(p.x * ms, p.y * ms, 2.5, 0, Math.PI * 2);
mctx.fill();
mctx.strokeStyle = '#ff0';
mctx.lineWidth = 1;
mctx.beginPath();
mctx.moveTo(p.x * ms, p.y * ms);
mctx.lineTo((p.x + Math.cos(p.angle) * 2) * ms, (p.y + Math.sin(p.angle) * 2) * ms);
mctx.stroke();
}
function renderWeaponView(ctx) {
const bob = Math.sin(playerState.bobPhase) * 4;
const wepIdx = playerState.weapon.currentIdx;
const ox = CANVAS_W / 2 + bob;
const oy = CANVAS_H + bob * 0.5;
ctx.save();
ctx.translate(ox, oy);
switch (wepIdx) {
case 0: // Pistol
ctx.fillStyle = '#666';
ctx.fillRect(-6, -55, 12, 45);
ctx.fillStyle = '#444';
ctx.fillRect(-4, -65, 8, 14);
ctx.fillStyle = '#555';
ctx.fillRect(-5, -15, 10, 12);
break;
case 1: // Shotgun
ctx.fillStyle = '#654';
ctx.fillRect(-10, -75, 20, 55);
ctx.fillStyle = '#543';
ctx.fillRect(-7, -88, 14, 18);
ctx.fillStyle = '#888';
ctx.fillRect(-3, -94, 6, 10);
break;
default: // Machine Gun
ctx.fillStyle = '#567';
ctx.fillRect(-8, -65, 16, 50);
ctx.fillStyle = '#456';
ctx.fillRect(-12, -75, 24, 14);
ctx.fillStyle = '#777';
ctx.fillRect(-2, -84, 4, 12);
ctx.fillStyle = '#444';
ctx.fillRect(-5, -20, 10, 18);
break;
}
// Muzzle flash
if (playerState.weapon.muzzleFlash > 0) {
const fi = playerState.weapon.muzzleFlash / 6;
const g = ctx.createRadialGradient(0, -95, 0, 0, -95, 40);
g.addColorStop(0, `rgba(255,220,80,${fi})`);
g.addColorStop(1, 'rgba(255,100,0,0)');
ctx.fillStyle = g;
ctx.fillRect(-40, -135, 80, 60);
}
ctx.restore();
}
function updateHUD() {
const p = playerState;
const hv = document.getElementById('health-val');
hv.textContent = String(Math.ceil(p.health));
hv.style.color = p.health > 60 ? '#f44' : p.health > 30 ? '#fa0' : '#f00';
document.getElementById('ammo-count').textContent = String(p.ammo);
document.getElementById('weapon-name').textContent = weapons_1.WEAPON_NAMES[p.weapon.currentIdx];
}
function frameLoop(time) {
requestAnimationFrame(frameLoop);
if (!playerState)
return;
const dt = Math.min(50, time - frameLastTime);
frameLastTime = time;
// Dead state
if (gameState === GameState.DEAD)
return;
if (gameState === GameState.WIN)
return;
const input = (0, input_1.getInput)();
const p = playerState;
p.angle += (0, input_1.getMouseDelta)() * ROT_SPEED;
let moving = false;
let dx = 0, dy = 0;
if (input.forward) {
dx += Math.cos(p.angle) * MOVE_SPEED;
dy += Math.sin(p.angle) * MOVE_SPEED;
moving = true;
}
if (input.backward) {
dx -= Math.cos(p.angle) * MOVE_SPEED;
dy -= Math.sin(p.angle) * MOVE_SPEED;
moving = true;
}
if (input.strafeLeft) {
dx += Math.cos(p.angle - Math.PI / 2) * STRAFE_SPEED;
dy += Math.sin(p.angle - Math.PI / 2) * STRAFE_SPEED;
moving = true;
}
if (input.strafeRight) {
dx += Math.cos(p.angle + Math.PI / 2) * STRAFE_SPEED;
dy += Math.sin(p.angle + Math.PI / 2) * STRAFE_SPEED;
moving = true;
}
if (canMove(p.x + dx, p.y))
p.x += dx;
if (canMove(p.x, p.y + dy))
p.y += dy;
p.bobPhase += moving ? dt * 0.009 : 0;
p.bobPhase *= moving ? 1 : 0.9;
p.weapon.update(performance.now());
p.damageFlash = Math.max(0, p.damageFlash - 1);
// Shooting
if (input.shoot && !p.lastShoot) {
shootWeapon(performance.now());
}
// Weapon cycling (R key)
if (input.weaponCycle && !p.prevWeaponCycle) {
p.weapon.switchNext();
}
p.prevWeaponCycle = input.weaponCycle;
p.lastShoot = input.shoot;
p.lastShoot = input.shoot;
// Check win
if ((0, map_1.isExit)(p.x, p.y)) {
gameState = GameState.WIN;
document.getElementById('win-screen').classList.remove('hidden');
(0, audio_1.playSound)('exit');
return;
}
updateEnemies(dt);
checkPickups();
rebuildSprites();
updateHUD();
if (msgTimer > 0) {
msgTimer -= dt;
if (msgTimer <= 0)
hideMessage();
}
// ---- RENDER ----
imgData.data.fill(0);
const headBob = Math.sin(p.bobPhase) * 2.5;
const halfH = CANVAS_H / 2 + headBob;
// Pre-compute row indices and stride
const stride = CANVAS_W * 4;
// First pass: draw background (ceiling + floor) for all rows
for (let y = 0; y < Math.min(CANVAS_H, halfH); y++) {
const row = y * stride;
const skyF = y / (CANVAS_H / 2);
for (let x = 0; x < CANVAS_W; x++) {
const i = row + x * 4;
imgData.data[i] = Math.floor(30 + skyF * 20);
imgData.data[i + 1] = Math.floor(15 + skyF * 10);
imgData.data[i + 2] = Math.floor(20 + skyF * 15);
imgData.data[i + 3] = 255;
}
}
for (let y = Math.max(0, halfH | 0); y < CANVAS_H; y++) {
const row = y * stride;
const ft = (y - halfH + CANVAS_H / 2) / (CANVAS_H / 2);
for (let x = 0; x < CANVAS_W; x++) {
const i = row + x * 4;
imgData.data[i] = Math.floor(40 + ft * 20);
imgData.data[i + 1] = Math.floor(30 + ft * 15);
imgData.data[i + 2] = Math.floor(25 + ft * 12);
imgData.data[i + 3] = 255;
}
}
// Second pass: overlay walls on columns where we hit
zBuffer = new Float32Array(CANVAS_W);
const baseAngle = p.angle - FOV / 2;
for (let x = 0; x < CANVAS_W; x++) {
const rayAngle = baseAngle + (x / CANVAS_W) * FOV;
const result = castWallRay(p.x, p.y, rayAngle, zBuffer, x);
if (result.hit) {
const { perpDist, wallType, side } = result;
const colors = map_1.WALL_COLORS[wallType] || [128, 128, 128];
const shade = side === 1 ? 0.65 : 1.0;
const fogT = Math.min(1, 1 - perpDist / MAX_DEPTH);
const darkness = shade * fogT;
const lh = Math.floor(CANVAS_H / perpDist);
const ds = Math.max(0, Math.floor(-lh / 2 + halfH));
const de = Math.min(CANVAS_H, Math.floor(lh / 2 + halfH));
for (let y = ds; y < de; y++) {
const ty = (y - ds) / (de - ds || 1);
const pat = (Math.floor(ty * 8) % 2 === 0) ? 1.0 : 0.88;
const idx = y * stride + x * 4;
imgData.data[idx] = Math.floor(colors[0] * darkness * pat);
imgData.data[idx + 1] = Math.floor(colors[1] * darkness * pat);
imgData.data[idx + 2] = Math.floor(colors[2] * darkness * pat);
imgData.data[idx + 3] = 255;
}
}
}
ctx.putImageData(imgData, 0, 0);
// Sprites drawn on top using canvas 2D API
for (const sp of sortSprites(enemySprites)) {
drawSprite(ctx, sp, 180, 40, 40, zBuffer, halfH);
}
for (const sp of sortSprites(pickupSprites)) {
const t = sp.entity.pickupType;
drawSprite(ctx, sp, t === 'health' ? 0 : 200, t === 'health' ? 200 : 150, 0, zBuffer, halfH);
}
// Damage flash
if (p.damageFlash > 0) {
ctx.fillStyle = `rgba(255,0,0,${p.damageFlash / 30})`;
ctx.fillRect(0, 0, CANVAS_W, CANVAS_H);
}
// Weapon
renderWeaponView(ctx);
// Minimap
renderMinimap();
}
requestAnimationFrame(frameLoop);
}
let frameLastTime = 0;
};
// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"qwen/qwen3.7-flash","provider":"openrouter","persona":"typescript","sessionId":"c127e91e-cda5-4948-ae52-ffc27bb5162f","tokensIn":5944773,"tokensOut":54269,"tokensTotal":5999042,"cost":0.21259059000000005,"turns":90,"toolCalls":89,"failedToolCalls":6,"timestamp":"2026-08-02T02:58:14.174Z"} -->