DOOM
Qwen 3.6 35b a3b · full

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 42,492 bytes · SHA-256 dc7c390cd83e
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DEEP HALLS - Retro FPS</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #000; overflow: hidden; font-family: 'Courier New', monospace; }
canvas { display: block; image-rendering: pixelated; }
#gameCanvas { width: 100vw; height: 100vh; }
#hud {
position: fixed; bottom: 0; left: 0; right: 0;
height: 80px; background: linear-gradient(to bottom, #1a1a2e, #0a0a15);
border-top: 3px solid #444; display: flex; align-items: center;
justify-content: center; gap: 40px; z-index: 10;
font-family: 'Courier New', monospace;
}
.hud-item {
display: flex; flex-direction: column; align-items: center;
color: #aaa; font-size: 12px; text-transform: uppercase; letter-spacing: 2px;
}
.hud-value {
font-size: 36px; font-weight: bold; margin-top: 4px;
text-shadow: 0 0 10px currentColor;
}
.health-val { color: #4f4; }
.ammo-val { color: #ff4; }
.weapon-val { color: #f44; }
#overlay {
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
display: flex; flex-direction: column; align-items: center;
justify-content: center; z-index: 100; pointer-events: none;
}
#overlay.active { pointer-events: all; }
#overlay h1 {
font-size: 64px; color: #f44; text-shadow: 0 0 30px #f00, 0 0 60px #900;
margin-bottom: 20px; letter-spacing: 8px;
}
#overlay p {
font-size: 18px; color: #aaa; margin: 8px 0;
}
#overlay .btn {
margin-top: 30px; padding: 15px 40px; font-size: 20px;
background: #333; color: #fff; border: 2px solid #666;
cursor: pointer; font-family: 'Courier New', monospace;
text-transform: uppercase; letter-spacing: 3px;
transition: all 0.2s;
}
#overlay .btn:hover { background: #555; border-color: #f44; color: #f44; }
#minimap {
position: fixed; top: 10px; right: 10px; z-index: 10;
border: 2px solid #444; opacity: 0.7;
}
#crosshair {
position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
z-index: 10; pointer-events: none; color: #0f0; font-size: 24px;
text-shadow: 0 0 5px #0f0;
}
#damage-flash {
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
pointer-events: none; z-index: 50; opacity: 0;
background: radial-gradient(ellipse at center, transparent 50%, rgba(255,0,0,0.6) 100%);
transition: opacity 0.1s;
}
#pickup-msg {
position: fixed; top: 60%; left: 50%; transform: translateX(-50%);
z-index: 10; color: #ff0; font-size: 16px; font-family: 'Courier New', monospace;
opacity: 0; transition: opacity 0.3s; text-shadow: 0 0 10px #ff0;
}
#weapon-sprite {
position: fixed; bottom: 80px; left: 50%; transform: translateX(-50%);
z-index: 10; pointer-events: none;
}
</style>
</head>
<body>
<canvas id="gameCanvas"></canvas>
<canvas id="minimap" width="160" height="160"></canvas>
<div id="crosshair">+</div>
<div id="damage-flash"></div>
<div id="pickup-msg"></div>
<div id="hud">
<div class="hud-item">
<span>Health</span>
<span class="hud-value health-val" id="hudHealth">100</span>
</div>
<div class="hud-item">
<span>Ammo</span>
<span class="hud-value ammo-val" id="hudAmmo">50</span>
</div>
<div class="hud-item">
<span>Weapon</span>
<span class="hud-value weapon-val" id="hudWeapon">PISTOL</span>
</div>
</div>
<div id="overlay" class="active">
<h1>DEEP HALLS</h1>
<p>WASD - Move | Mouse - Look | Click - Shoot</p>
<p>1/2/3 - Switch Weapons | R - Reload</p>
<p>Find the EXIT door to escape the deep halls.</p>
<p id="gameMsg"></p>
<button class="btn" id="startBtn">ENTER</button>
</div>
<script>
// ============================================================
// DEEP HALLS - A Retro FPS
// ============================================================
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const miniCanvas = document.getElementById('minimap');
const miniCtx = miniCanvas.getContext('2d');
const hudHealth = document.getElementById('hudHealth');
const hudAmmo = document.getElementById('hudAmmo');
const hudWeapon = document.getElementById('hudWeapon');
const overlay = document.getElementById('overlay');
const startBtn = document.getElementById('startBtn');
const damageFlash = document.getElementById('damage-flash');
const pickupMsg = document.getElementById('pickup-msg');
// --- CONSTANTS ---
const SCREEN_W = 640, SCREEN_H = 360;
const FOV = Math.PI / 3;
const HALF_FOV = FOV / 2;
const TILE = 64;
const WALL_HEIGHT = 1.2;
const MOVE_SPEED = 3.0;
const STRAFE_SPEED = 2.5;
const MOUSE_SENS = 0.002;
const MAX_ENEMIES = 12;
const MAX_PICKUPS = 15;
// --- MAP ---
const 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,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,2,2,0,0,0,3,3,0,0,0,0,3,3,0,0,0,0,0,0,0,1],
[1,0,0,2,2,0,0,0,3,3,0,0,0,0,3,3,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,4,4,0,0,0,0,0,0,0,0,4,4,0,0,0,0,0,1],
[1,0,0,0,0,0,4,4,0,0,0,0,0,0,0,0,4,4,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,0,0,0,0,0,5,5,5,5,5,0,0,0,0,0,3,0,0,0,1],
[1,0,0,3,0,0,0,0,0,5,0,0,0,5,0,0,0,0,0,3,0,0,0,1],
[1,0,0,3,0,0,0,0,0,5,0,0,0,5,0,0,0,0,0,3,0,0,0,1],
[1,0,0,3,0,0,0,0,0,5,0,0,0,5,0,0,0,0,0,3,0,0,0,1],
[1,0,0,3,0,0,0,0,0,5,5,5,5,5,0,0,0,0,0,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,2,2,0,0,0,0,0,0,0,0,2,2,0,0,0,0,0,1],
[1,0,0,0,0,0,2,2,0,0,0,0,0,0,0,0,2,2,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,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,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],
];
const MAP_W = MAP[0].length;
const MAP_H = MAP.length;
// Wall colors
const WALL_COLORS = {
1: { r: 100, g: 100, b: 100 }, // gray stone
2: { r: 120, g: 60, b: 60 }, // red brick
3: { r: 60, g: 100, b: 60 }, // green moss
4: { r: 80, g: 80, b: 120 }, // blue stone
5: { r: 140, g: 120, b: 60 }, // gold trim
};
// --- AUDIO ---
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
function playSound(type) {
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.connect(gain);
gain.connect(audioCtx.destination);
const t = audioCtx.currentTime;
switch(type) {
case 'pistol':
osc.type = 'square';
osc.frequency.setValueAtTime(200, t);
osc.frequency.exponentialRampToValueAtTime(50, t + 0.1);
gain.gain.setValueAtTime(0.3, t);
gain.gain.exponentialRampToValueAtTime(0.001, t + 0.15);
osc.start(t); osc.stop(t + 0.15);
break;
case 'shotgun':
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(150, t);
osc.frequency.exponentialRampToValueAtTime(30, t + 0.2);
gain.gain.setValueAtTime(0.4, t);
gain.gain.exponentialRampToValueAtTime(0.001, t + 0.25);
osc.start(t); osc.stop(t + 0.25);
// add noise burst
const buf = audioCtx.createBuffer(1, audioCtx.sampleRate * 0.1, audioCtx.sampleRate);
const data = buf.getChannelData(0);
for (let i = 0; i < data.length; i++) data[i] = (Math.random() * 2 - 1) * 0.5;
const noise = audioCtx.createBufferSource();
noise.buffer = buf;
const ng = audioCtx.createGain();
ng.gain.setValueAtTime(0.3, t);
ng.gain.exponentialRampToValueAtTime(0.001, t + 0.1);
noise.connect(ng); ng.connect(audioCtx.destination);
noise.start(t); noise.stop(t + 0.1);
break;
case 'plasma':
osc.type = 'sine';
osc.frequency.setValueAtTime(800, t);
osc.frequency.exponentialRampToValueAtTime(200, t + 0.15);
gain.gain.setValueAtTime(0.2, t);
gain.gain.exponentialRampToValueAtTime(0.001, t + 0.2);
osc.start(t); osc.stop(t + 0.2);
break;
case 'hit':
osc.type = 'sine';
osc.frequency.setValueAtTime(400, t);
osc.frequency.exponentialRampToValueAtTime(100, t + 0.05);
gain.gain.setValueAtTime(0.15, t);
gain.gain.exponentialRampToValueAtTime(0.001, t + 0.08);
osc.start(t); osc.stop(t + 0.08);
break;
case 'enemy_die':
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(300, t);
osc.frequency.exponentialRampToValueAtTime(50, t + 0.4);
gain.gain.setValueAtTime(0.2, t);
gain.gain.exponentialRampToValueAtTime(0.001, t + 0.4);
osc.start(t); osc.stop(t + 0.4);
break;
case 'pickup':
osc.type = 'sine';
osc.frequency.setValueAtTime(400, t);
osc.frequency.exponentialRampToValueAtTime(800, t + 0.15);
gain.gain.setValueAtTime(0.15, t);
gain.gain.exponentialRampToValueAtTime(0.001, t + 0.2);
osc.start(t); osc.stop(t + 0.2);
break;
case 'hurt':
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(100, t);
osc.frequency.exponentialRampToValueAtTime(50, t + 0.1);
gain.gain.setValueAtTime(0.2, t);
gain.gain.exponentialRampToValueAtTime(0.001, t + 0.15);
osc.start(t); osc.stop(t + 0.15);
break;
case 'win':
osc.type = 'sine';
osc.frequency.setValueAtTime(523, t);
gain.gain.setValueAtTime(0.2, t);
osc.start(t);
const osc2 = audioCtx.createOscillator();
const g2 = audioCtx.createGain();
osc2.connect(g2); g2.connect(audioCtx.destination);
osc2.type = 'sine';
osc2.frequency.setValueAtTime(659, t + 0.2);
g2.gain.setValueAtTime(0.2, t + 0.2);
osc2.start(t + 0.2);
const osc3 = audioCtx.createOscillator();
const g3 = audioCtx.createGain();
osc3.connect(g3); g3.connect(audioCtx.destination);
osc3.type = 'sine';
osc3.frequency.setValueAtTime(784, t + 0.4);
g3.gain.setValueAtTime(0.2, t + 0.4);
osc3.start(t + 0.4);
gain.gain.exponentialRampToValueAtTime(0.001, t + 0.6);
osc.stop(t + 0.6);
osc2.stop(t + 0.6);
osc3.stop(t + 0.6);
break;
case 'death':
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(200, t);
osc.frequency.exponentialRampToValueAtTime(30, t + 0.8);
gain.gain.setValueAtTime(0.3, t);
gain.gain.exponentialRampToValueAtTime(0.001, t + 0.8);
osc.start(t); osc.stop(t + 0.8);
break;
}
}
// --- WEAPONS ---
const WEAPONS = {
pistol: {
name: 'PISTOL', ammoType: 'bullets', baseDmg: 15, fireRate: 300,
maxAmmo: 999, reloadTime: 500, spread: 0.02, pellets: 1,
color: '#888', muzzleFlash: '#ff0'
},
shotgun: {
name: 'SHOTGUN', ammoType: 'shells', baseDmg: 12, fireRate: 800,
maxAmmo: 50, reloadTime: 1500, spread: 0.08, pellets: 6,
color: '#a64', muzzleFlash: '#f80'
},
plasma: {
name: 'PLASMA', ammoType: 'cells', baseDmg: 25, fireRate: 150,
maxAmmo: 200, reloadTime: 800, spread: 0.015, pellets: 1,
color: '#4ff', muzzleFlash: '#0ff'
}
};
// --- GAME STATE ---
let game = {
running: false,
won: false,
dead: false,
player: {
x: 1.5, y: 1.5, angle: 0, health: 100, maxHealth: 100,
weapon: 'pistol', ammo: { bullets: 36, shells: 8, cells: 50 },
bobPhase: 0, bobAmount: 0, recoil: 0, muzzleFlash: 0,
speed: 0
},
enemies: [],
pickups: [],
projectiles: [],
particles: [],
keys: {},
mouseDown: false,
lastShot: 0,
reloadTimer: 0,
doorPositions: [],
exitPos: { x: 22.5, y: 22.5 },
lastTime: 0,
zBuffer: new Float32Array(SCREEN_W),
camera: null,
spriteCache: null,
enemyCache: null,
pickupCache: null,
};
// --- TEXTURE GENERATION ---
function generateTextures() {
const textures = {};
const sizes = [64, 64, 64, 64, 64]; // one texture per wall type
for (let type = 1; type <= 5; type++) {
const c = document.createElement('canvas');
c.width = 64; c.height = 64;
const tctx = c.getContext('2d');
const col = WALL_COLORS[type];
// Base
tctx.fillStyle = `rgb(${col.r},${col.g},${col.b})`;
tctx.fillRect(0, 0, 64, 64);
// Brick/stone pattern
const brickH = 16, brickW = 32;
for (let row = 0; row < 4; row++) {
const offset = row % 2 === 0 ? 0 : brickW / 2;
for (let col2 = -1; col2 < 3; col2++) {
const x = col2 * brickW + offset;
const y = row * brickH;
// Brick face
const r = col.r + (Math.random() - 0.5) * 20;
const g = col.g + (Math.random() - 0.5) * 20;
const b = col.b + (Math.random() - 0.5) * 20;
tctx.fillStyle = `rgb(${r|0},${g|0},${b|0})`;
tctx.fillRect(x + 1, y + 1, brickW - 2, brickH - 2);
// Highlight
tctx.fillStyle = `rgba(255,255,255,0.08)`;
tctx.fillRect(x + 1, y + 1, brickW - 2, 2);
// Shadow
tctx.fillStyle = `rgba(0,0,0,0.15)`;
tctx.fillRect(x + 1, y + brickH - 3, brickW - 2, 2);
}
}
// Add noise
const imgData = tctx.getImageData(0, 0, 64, 64);
for (let i = 0; i < imgData.data.length; i += 4) {
const n = (Math.random() - 0.5) * 15;
imgData.data[i] += n;
imgData.data[i+1] += n;
imgData.data[i+2] += n;
}
tctx.putImageData(imgData, 0, 0);
textures[type] = tctx.getImageData(0, 0, 64, 64);
}
return textures;
}
// --- ENEMY GENERATION ---
function generateEnemySprites() {
const c = document.createElement('canvas');
c.width = 64; c.height = 64;
const tctx = c.getContext('2d');
// Demon-like enemy
// Body
tctx.fillStyle = '#a33';
tctx.beginPath();
tctx.ellipse(32, 40, 18, 20, 0, 0, Math.PI * 2);
tctx.fill();
// Head
tctx.fillStyle = '#c44';
tctx.beginPath();
tctx.arc(32, 18, 14, 0, Math.PI * 2);
tctx.fill();
// Eyes (glowing red)
tctx.fillStyle = '#ff0';
tctx.beginPath();
tctx.ellipse(26, 16, 4, 3, 0, 0, Math.PI * 2);
tctx.fill();
tctx.beginPath();
tctx.ellipse(38, 16, 4, 3, 0, 0, Math.PI * 2);
tctx.fill();
tctx.fillStyle = '#f00';
tctx.beginPath();
tctx.ellipse(26, 16, 2, 2, 0, 0, Math.PI * 2);
tctx.fill();
tctx.beginPath();
tctx.ellipse(38, 16, 2, 2, 0, 0, Math.PI * 2);
tctx.fill();
// Horns
tctx.fillStyle = '#822';
tctx.beginPath();
tctx.moveTo(22, 10); tctx.lineTo(18, 0); tctx.lineTo(26, 8);
tctx.fill();
tctx.beginPath();
tctx.moveTo(42, 10); tctx.lineTo(46, 0); tctx.lineTo(38, 8);
tctx.fill();
// Mouth
tctx.fillStyle = '#600';
tctx.fillRect(26, 24, 12, 5);
// Teeth
tctx.fillStyle = '#fff';
for (let i = 0; i < 4; i++) {
tctx.fillRect(27 + i * 3, 24, 2, 3);
}
// Arms
tctx.fillStyle = '#a33';
tctx.fillRect(8, 30, 10, 18);
tctx.fillRect(46, 30, 10, 18);
// Legs
tctx.fillStyle = '#822';
tctx.fillRect(24, 52, 8, 12);
tctx.fillRect(34, 52, 8, 12);
return tctx.getImageData(0, 0, 64, 64);
}
// --- PICKUP GENERATION ---
function generatePickupSprites() {
const sprites = {};
// Health pack
const hc = document.createElement('canvas');
hc.width = 32; hc.height = 32;
const hctx = hc.getContext('2d');
hctx.fillStyle = '#fff';
hctx.fillRect(6, 4, 20, 24);
hctx.fillStyle = '#f00';
hctx.fillRect(13, 8, 6, 16);
hctx.fillRect(8, 13, 16, 6);
sprites.health = hctx.getImageData(0, 0, 32, 32);
// Ammo box
const ac = document.createElement('canvas');
ac.width = 32; ac.height = 32;
const actx = ac.getContext('2d');
actx.fillStyle = '#654';
actx.fillRect(4, 10, 24, 18);
actx.fillStyle = '#876';
actx.fillRect(6, 12, 20, 14);
actx.fillStyle = '#ff0';
actx.font = 'bold 10px monospace';
actx.textAlign = 'center';
actx.fillText('A', 16, 23);
sprites.ammo = actx.getImageData(0, 0, 32, 32);
// Shell box
const sc = document.createElement('canvas');
sc.width = 32; sc.height = 32;
const sctx = sc.getContext('2d');
sctx.fillStyle = '#554';
sctx.fillRect(4, 10, 24, 18);
sctx.fillStyle = '#765';
sctx.fillRect(6, 12, 20, 14);
sctx.fillStyle = '#f80';
sctx.font = 'bold 10px monospace';
sctx.textAlign = 'center';
sctx.fillText('S', 16, 23);
sprites.shells = sctx.getImageData(0, 0, 32, 32);
// Cell pack
const cc = document.createElement('canvas');
cc.width = 32; cc.height = 32;
const cctx2 = cc.getContext('2d');
cctx2.fillStyle = '#0a0a0a';
cctx2.fillRect(4, 10, 24, 18);
cctx2.fillStyle = '#0f0';
cctx2.fillRect(8, 14, 16, 10);
cctx2.fillStyle = '#0ff';
cctx2.font = 'bold 10px monospace';
cctx2.textAlign = 'center';
cctx2.fillText('C', 16, 23);
sprites.cells = cctx2.getImageData(0, 0, 32, 32);
// Exit door (gold)
const ec = document.createElement('canvas');
ec.width = 64; ec.height = 64;
const etctx = ec.getContext('2d');
etctx.fillStyle = '#da0';
etctx.fillRect(16, 4, 32, 56);
etctx.fillStyle = '#fa0';
etctx.fillRect(20, 8, 24, 48);
etctx.fillStyle = '#ff0';
etctx.font = 'bold 12px monospace';
etctx.textAlign = 'center';
etctx.fillText('EXIT', 32, 38);
sprites.exit = etctx.getImageData(0, 0, 64, 64);
return sprites;
}
// --- LEVEL INIT ---
function initLevel() {
game.enemies = [];
game.pickups = [];
game.projectiles = [];
game.particles = [];
// Spawn enemies in open areas
const enemyPositions = [
{x: 5.5, y: 5.5}, {x: 10.5, y: 3.5}, {x: 15.5, y: 5.5},
{x: 20.5, y: 3.5}, {x: 3.5, y: 10.5}, {x: 7.5, y: 15.5},
{x: 12.5, y: 12.5}, {x: 18.5, y: 15.5}, {x: 22.5, y: 10.5},
{x: 5.5, y: 20.5}, {x: 10.5, y: 22.5}, {x: 16.5, y: 20.5},
];
for (let i = 0; i < enemyPositions.length; i++) {
game.enemies.push({
x: enemyPositions[i].x,
y: enemyPositions[i].y,
health: 40 + Math.random() * 30,
maxHealth: 70,
state: 'patrol', // patrol, chase, attack, dead
alertDist: 8,
attackDist: 2,
attackTimer: 0,
attackRate: 1500,
damage: 8 + Math.random() * 7,
speed: 1.2 + Math.random() * 0.8,
patrolAngle: Math.random() * Math.PI * 2,
patrolTimer: 0,
hitFlash: 0,
deadTimer: 0,
type: 'demon',
});
}
// Spawn pickups
const pickupPositions = [
// health
{x: 2.5, y: 12.5, type: 'health', amount: 25},
{x: 12.5, y: 7.5, type: 'health', amount: 25},
{x: 22.5, y: 17.5, type: 'health', amount: 50},
{x: 8.5, y: 21.5, type: 'health', amount: 25},
// ammo
{x: 9.5, y: 11.5, type: 'ammo', ammoType: 'bullets', amount: 12},
{x: 14.5, y: 11.5, type: 'ammo', ammoType: 'shells', amount: 8},
{x: 18.5, y: 7.5, type: 'ammo', ammoType: 'cells', amount: 20},
{x: 3.5, y: 17.5, type: 'ammo', ammoType: 'bullets', amount: 12},
{x: 22.5, y: 5.5, type: 'ammo', ammoType: 'shells', amount: 8},
{x: 12.5, y: 22.5, type: 'ammo', ammoType: 'cells', amount: 20},
// extra
{x: 7.5, y: 7.5, type: 'health', amount: 25},
{x: 17.5, y: 17.5, type: 'ammo', ammoType: 'bullets', amount: 12},
{x: 21.5, y: 21.5, type: 'ammo', ammoType: 'shells', amount: 8},
{x: 4.5, y: 4.5, type: 'ammo', ammoType: 'cells', amount: 20},
{x: 11.5, y: 17.5, type: 'health', amount: 25},
];
for (const p of pickupPositions) {
game.pickups.push({
x: p.x, y: p.y,
type: p.type,
amount: p.amount,
ammoType: p.ammoType || null,
active: true,
bobPhase: Math.random() * Math.PI * 2,
scale: 1,
});
}
// Set exit door position
game.exitPos = { x: 22.5, y: 22.5 };
}
function resetPlayer() {
game.player.x = 1.5;
game.player.y = 1.5;
game.player.angle = 0;
game.player.health = 100;
game.player.weapon = 'pistol';
game.player.ammo = { bullets: 36, shells: 8, cells: 50 };
game.player.recoil = 0;
game.player.muzzleFlash = 0;
game.player.speed = 0;
game.player.bobPhase = 0;
game.player.bobAmount = 0;
}
// --- RAYCASTING ENGINE ---
function castRay(ox, oy, angle) {
const sin = Math.sin(angle);
const cos = Math.cos(angle);
// DDA
let mapX = Math.floor(ox);
let mapY = Math.floor(oy);
const deltaDistX = Math.abs(1 / cos) || 1e10;
const deltaDistY = Math.abs(1 / sin) || 1e10;
let stepX, stepY, sideDistX, sideDistY;
if (cos > 0) {
stepX = 1;
sideDistX = (mapX + 1 - ox) * deltaDistX;
} else {
stepX = -1;
sideDistX = (ox - mapX) * deltaDistX;
}
if (sin > 0) {
stepY = 1;
sideDistY = (mapY + 1 - oy) * deltaDistY;
} else {
stepY = -1;
sideDistY = (oy - mapY) * deltaDistY;
}
let hit = 0, side = 0;
while (hit === 0) {
if (sideDistX < sideDistY) {
sideDistX += deltaDistX;
mapX += stepX;
side = 0;
} else {
sideDistY += deltaDistY;
mapY += stepY;
side = 1;
}
if (mapX < 0 || mapX >= MAP_W || mapY < 0 || mapY >= MAP_H) {
hit = 1; break;
}
if (MAP[mapY][mapX] > 0) {
hit = MAP[mapY][mapX];
}
}
let perpDist;
if (side === 0) {
perpDist = (mapX - ox + (1 - stepX) / 2) / cos;
} else {
perpDist = (mapY - oy + (1 - stepY) / 2) / sin;
}
// Wall X (texture coordinate)
let wallX;
if (side === 0) {
wallX = oy + perpDist * sin;
} else {
wallX = ox + perpDist * cos;
}
wallX -= Math.floor(wallX);
return { perpDist, hit, side, wallX, mapX, mapY };
}
function render() {
const w = canvas.width, h = canvas.height;
const imgData = ctx.createImageData(w, h);
const data = imgData.data;
const player = game.player;
const startAngle = player.angle - HALF_FOV;
// Floor/ceiling colors
const ceilR = 20, ceilG = 20, ceilB = 30;
const floorR = 40, floorG = 35, floorB = 30;
// Cast rays
for (let x = 0; x < w; x++) {
const rayAngle = startAngle + x * FOV / w;
const ray = castRay(player.x, player.y, rayAngle);
// Fix fisheye
const dist = ray.perpDist * Math.cos(rayAngle - player.angle);
game.zBuffer[x] = dist;
const wallH = Math.min(h * 2, (TILE * WALL_HEIGHT) / dist);
const wallTop = (h - wallH) / 2;
// Wall texture
const tex = game.textureCache[ray.hit];
const texX = Math.floor(ray.wallX * 64) & 63;
const shade = ray.side === 1 ? 0.6 : 1.0;
const fogFactor = Math.min(1, 8 / dist); // distance fog
for (let y = 0; y < h; y++) {
const idx = (y * w + x) * 4;
if (y < wallTop) {
// Ceiling
data[idx] = ceilR;
data[idx+1] = ceilG;
data[idx+2] = ceilB;
} else if (y < wallTop + wallH) {
// Wall
const texY = Math.floor(((y - wallTop) / wallH) * 64) & 63;
const ti = (texY * 64 + texX) * 4;
data[idx] = Math.floor(tex.data[ti] * shade * fogFactor);
data[idx+1] = Math.floor(tex.data[ti+1] * shade * fogFactor);
data[idx+2] = Math.floor(tex.data[ti+2] * shade * fogFactor);
} else {
// Floor
const floorFog = Math.min(1, 10 / dist);
data[idx] = Math.floor(floorR * floorFog);
data[idx+1] = Math.floor(floorG * floorFog);
data[idx+2] = Math.floor(floorB * floorFog);
}
data[idx+3] = 255;
}
}
ctx.putImageData(imgData, 0, 0);
// --- SPRITE RENDERING ---
renderSprites(w, h, data);
// --- WEAPON SPRITE ---
renderWeapon(w, h);
// --- HUD OVERLAY ---
renderOverlay(w, h);
}
function renderSprites(w, h, data) {
const player = game.player;
// Collect all sprites
let sprites = [];
for (const e of game.enemies) {
if (e.state === 'dead' && e.deadTimer > 2) continue;
const dx = e.x - player.x;
const dy = e.y - player.y;
const dist = Math.sqrt(dx * dx + dy * dy);
sprites.push({ ...e, dist, type: 'enemy', sprite: game.enemyCache });
}
for (const p of game.pickups) {
if (!p.active) continue;
const dx = p.x - player.x;
const dy = p.y - player.y;
const dist = Math.sqrt(dx * dx + dy * dy);
let spriteData = null;
if (p.type === 'health') spriteData = game.pickupCache?.health;
else if (p.type === 'ammo') spriteData = game.pickupCache?.[p.ammoType];
sprites.push({ ...p, dist, spriteType: 'pickup', sprite: spriteData });
}
// Sort back to front
sprites.sort((a, b) => b.dist - a.dist);
for (const sp of sprites) {
const dx = sp.x - player.x;
const dy = sp.y - player.y;
// Transform relative to camera
const invDet = 1.0 / (Math.cos(player.angle + Math.PI/2) * Math.sin(player.angle) - Math.sin(player.angle + Math.PI/2) * Math.cos(player.angle));
const planeX = Math.cos(player.angle + Math.PI/2) * Math.tan(FOV/2);
const planeY = Math.sin(player.angle + Math.PI/2) * Math.tan(FOV/2);
const transformX = planeX * dy - Math.cos(player.angle) * dx;
const transformY = Math.sin(player.angle) * dx - Math.sin(player.angle + Math.PI/2) * dy;
// Actually use simpler transform
const cosA = Math.cos(player.angle);
const sinA = Math.sin(player.angle);
const camX = dx * cosA + dy * sinA;
const camY = -dx * sinA + dy * cosA;
if (camY <= 0.1) continue; // behind player
const spriteScreenX = w / 2 * (1 + camX / camY);
const spriteH = Math.abs(h / camY);
const spriteW = spriteH;
const drawStartY = -spriteH / 2 + h / 2;
const drawEndY = spriteH / 2 + h / 2;
const sprite = sp.type === 'pickup' && sp.type === 'health' ? game.pickupCache?.health :
sp.type === 'pickup' && sp.type === 'ammo' ? game.pickupCache?.[sp.ammoType] :
sp.type === 'pickup' && sp.type === 'exit' ? game.pickupCache?.exit :
game.enemyCache;
if (!sprite) continue;
const drawStartX = Math.floor(-spriteW / 2 + spriteScreenX);
const drawEndX = Math.floor(spriteW / 2 + spriteScreenX);
const fogFactor = Math.min(1, 8 / sp.dist);
const bobOffset = sp.type === 'pickup' ? Math.sin(sp.bobPhase) * 5 : 0;
for (let sx = drawStartX; sx < drawEndX; sx++) {
if (sx < 0 || sx >= w) continue;
if (camY > game.zBuffer[sx]) continue; // behind wall
const texX = Math.floor(((sx - drawStartX) / (drawEndX - drawStartX)) * 64);
for (let sy = Math.max(0, drawStartY); sy < Math.min(h, drawEndY); sy++) {
const texY = Math.floor(((sy - drawStartY - bobOffset) / spriteH) * 64);
if (texY < 0 || texY >= 64) continue;
const ti = (texY * 64 + texX) * 4;
if (sprite.data[ti + 3] < 128) continue; // transparent
const idx = (sy * w + sx) * 4;
const shade = sp.type === 'enemy' ? (sp.hitFlash > 0 ? 2.5 : fogFactor) : fogFactor;
data[idx] = Math.min(255, Math.floor(sprite.data[ti] * shade));
data[idx+1] = Math.min(255, Math.floor(sprite.data[ti+1] * shade));
data[idx+2] = Math.min(255, Math.floor(sprite.data[ti+2] * shade));
}
}
}
}
function renderWeapon(w, h) {
const player = game.player;
const bobX = Math.sin(player.bobPhase) * player.bobAmount * 10;
const bobY = Math.abs(Math.cos(player.bobPhase)) * player.bobAmount * 5;
const weaponY = h - 120 + bobY;
const weaponX = w / 2 - 60 + bobX;
// Draw weapon based on type
ctx.save();
if (player.weapon === 'pistol') {
// Pistol body
ctx.fillStyle = '#555';
ctx.fillRect(weaponX + 40, weaponY + 20, 40, 80);
ctx.fillStyle = '#444';
ctx.fillRect(weaponX + 35, weaponY + 100, 50, 20);
// Barrel
ctx.fillStyle = '#666';
ctx.fillRect(weaponX + 48, weaponY - 10, 24, 40);
// Grip
ctx.fillStyle = '#333';
ctx.fillRect(weaponX + 38, weaponY + 80, 30, 40);
// Grip lines
ctx.strokeStyle = '#555';
ctx.lineWidth = 1;
for (let i = 0; i < 4; i++) {
ctx.beginPath();
ctx.moveTo(weaponX + 42, weaponY + 85 + i * 8);
ctx.lineTo(weaponX + 64, weaponY + 85 + i * 8);
ctx.stroke();
}
} else if (player.weapon === 'shotgun') {
// Shotgun body
ctx.fillStyle = '#543';
ctx.fillRect(weaponX + 20, weaponY + 30, 80, 30);
// Barrel
ctx.fillStyle = '#666';
ctx.fillRect(weaponX + 10, weaponY - 20, 20, 70);
ctx.fillRect(weaponX + 35, weaponY - 15, 20, 65);
// Pump
ctx.fillStyle = '#432';
ctx.fillRect(weaponX + 15, weaponY + 55, 30, 15);
// Stock
ctx.fillStyle = '#543';
ctx.fillRect(weaponX + 55, weaponY + 55, 45, 50);
} else if (player.weapon === 'plasma') {
// Plasma gun body
ctx.fillStyle = '#345';
ctx.fillRect(weaponX + 30, weaponY + 20, 60, 70);
// Barrel
ctx.fillStyle = '#456';
ctx.fillRect(weaponX + 45, weaponY - 15, 30, 50);
// Energy core (glowing)
const glowAlpha = 0.5 + Math.sin(Date.now() * 0.01) * 0.3;
ctx.fillStyle = `rgba(0, 255, 255, ${glowAlpha})`;
ctx.fillRect(weaponX + 50, weaponY + 10, 20, 20);
// Grip
ctx.fillStyle = '#234';
ctx.fillRect(weaponX + 35, weaponY + 80, 35, 35);
}
// Muzzle flash
if (player.muzzleFlash > 0) {
const flashSize = 30 + player.muzzleFlash * 20;
const colors = WEAPONS[player.weapon].muzzleFlash;
ctx.fillStyle = colors;
ctx.globalAlpha = player.muzzleFlash;
ctx.beginPath();
ctx.arc(weaponX + 60, weaponY - 15, flashSize, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#fff';
ctx.beginPath();
ctx.arc(weaponX + 60, weaponY - 15, flashSize * 0.4, 0, Math.PI * 2);
ctx.fill();
ctx.globalAlpha = 1;
}
ctx.restore();
}
function renderOverlay(w, h) {
// Minimap
const mmScale = 6;
const mmW = MAP_W * mmScale;
const mmH = MAP_H * mmScale;
miniCanvas.width = mmW;
miniCanvas.height = mmH;
miniCtx.fillStyle = '#000';
miniCtx.fillRect(0, 0, mmW, mmH);
for (let y = 0; y < MAP_H; y++) {
for (let x = 0; x < MAP_W; x++) {
if (MAP[y][x] > 0) {
const col = WALL_COLORS[MAP[y][x]];
miniCtx.fillStyle = `rgb(${col.r},${col.g},${col.b})`;
miniCtx.fillRect(x * mmScale, y * mmScale, mmScale, mmScale);
}
}
}
// Exit marker
miniCtx.fillStyle = '#ff0';
miniCtx.fillRect(game.exitPos.x * mmScale - 2, game.exitPos.y * mmScale - 2, 4, 4);
// Pickups
for (const p of game.pickups) {
if (!p.active) continue;
miniCtx.fillStyle = p.type === 'health' ? '#0f0' : '#ff0';
miniCtx.fillRect(p.x * mmScale - 1, p.y * mmScale - 1, 2, 2);
}
// Enemies
for (const e of game.enemies) {
if (e.state === 'dead') continue;
miniCtx.fillStyle = e.state === 'chase' ? '#f00' : '#f80';
miniCtx.fillRect(e.x * mmScale - 2, e.y * mmScale - 2, 4, 4);
}
// Player
miniCtx.fillStyle = '#0ff';
miniCtx.beginPath();
miniCtx.arc(game.player.x * mmScale, game.player.y * mmScale, 3, 0, Math.PI * 2);
miniCtx.fill();
// Direction
miniCtx.strokeStyle = '#0ff';
miniCtx.lineWidth = 1;
miniCtx.beginPath();
miniCtx.moveTo(game.player.x * mmScale, game.player.y * mmScale);
miniCtx.lineTo(
(game.player.x + Math.cos(game.player.angle) * 1.5) * mmScale,
(game.player.y + Math.sin(game.player.angle) * 1.5) * mmScale
);
miniCtx.stroke();
// Reload indicator
if (game.reloadTimer > 0) {
ctx.fillStyle = '#ff0';
ctx.font = '14px monospace';
ctx.textAlign = 'center';
ctx.fillText('RELOADING...', w/2, h/2 + 40);
}
}
// --- ENEMY AI ---
function updateEnemies(dt) {
const player = game.player;
for (const e of game.enemies) {
if (e.state === 'dead') {
e.deadTimer += dt;
continue;
}
const dx = player.x - e.x;
const dy = player.y - e.y;
const dist = Math.sqrt(dx * dx + dy * dy);
const angleToPlayer = Math.atan2(dy, dx);
e.hitFlash = Math.max(0, e.hitFlash - dt * 5);
if (e.state === 'patrol') {
// Patrol behavior
e.patrolTimer -= dt;
if (e.patrolTimer <= 0) {
e.patrolAngle = Math.random() * Math.PI * 2;
e.patrolTimer = 2 + Math.random() * 3;
}
const moveX = Math.cos(e.patrolAngle) * e.speed * dt;
const moveY = Math.sin(e.patrolAngle) * e.speed * dt;
if (!isWall(e.x + moveX, e.y) && !isWall(e.x + moveX, e.y + 0.3)) {
e.x += moveX;
} else {
e.patrolAngle = Math.random() * Math.PI * 2;
}
if (!isWall(e.x, e.y + moveY) && !isWall(e.x + 0.3, e.y + moveY)) {
e.y += moveY;
} else {
e.patrolAngle = Math.random() * Math.PI * 2;
}
// Detect player
if (dist < e.alertDist) {
// Check line of sight
if (hasLineOfSight(e.x, e.y, player.x, player.y)) {
e.state = 'chase';
}
}
} else if (e.state === 'chase') {
if (dist > e.attackDist) {
// Move toward player
const angle = Math.atan2(dy, dx);
const moveX = Math.cos(angle) * e.speed * dt;
const moveY = Math.sin(angle) * e.speed * dt;
if (!isWall(e.x + moveX, e.y)) e.x += moveX;
if (!isWall(e.x, e.y + moveY)) e.y += moveY;
}
if (dist < e.alertDist * 1.5) {
e.state = 'chase';
} else if (dist > 15) {
e.state = 'patrol';
}
// Attack
if (dist < e.attackDist && hasLineOfSight(e.x, e.y, player.x, player.y)) {
e.attackTimer -= dt;
if (e.attackTimer <= 0) {
e.attackTimer = e.attackRate / 1000;
damagePlayer(e.damage);
playSound('hit');
}
}
} else if (e.state === 'attack') {
e.attackTimer -= dt;
if (e.attackTimer <= 0) {
if (dist < e.attackDist * 1.5 && hasLineOfSight(e.x, e.y, player.x, player.y)) {
damagePlayer(e.damage);
playSound('hit');
e.attackTimer = e.attackRate / 1000;
} else {
e.state = 'chase';
}
}
}
}
}
function isWall(x, y) {
const mx = Math.floor(x);
const my = Math.floor(y);
if (mx < 0 || mx >= MAP_W || my < 0 || my >= MAP_H) return true;
return MAP[my][mx] > 0;
}
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 x = x1 + dx * t;
const y = y1 + dy * t;
if (isWall(x, y)) return false;
}
return true;
}
// --- PLAYER ---
function damagePlayer(amount) {
const player = game.player;
player.health -= amount;
damageFlash.style.opacity = '0.5';
setTimeout(() => damageFlash.style.opacity = '0', 100);
playSound('hurt');
if (player.health <= 0) {
player.health = 0;
playerDeath();
}
}
function playerDeath() {
game.dead = true;
playSound('death');
setTimeout(() => {
overlay.classList.add('active');
overlay.querySelector('h1').textContent = 'YOU DIED';
overlay.querySelector('h1').style.color = '#f00';
document.getElementById('gameMsg').textContent = '';
startBtn.textContent = 'RESPAWN';
}, 1000);
}
function playerWin() {
game.won = true;
game.running = false;
playSound('win');
overlay.classList.add('active');
overlay.querySelector('h1').textContent = 'ESCAPED!';
overlay.querySelector('h1').style.color = '#0f0';
document.getElementById('gameMsg').textContent = 'You escaped the deep halls!';
startBtn.textContent = 'PLAY AGAIN';
}
function respawnPlayer() {
resetPlayer();
game.dead = false;
game.won = false;
game.running = true;
overlay.classList.remove('active');
initLevel();
}
// --- PICKUPS ---
function checkPickups() {
const player = game.player;
for (const p of game.pickups) {
if (!p.active) continue;
const dx = p.x - player.x;
const dy = p.y - player.y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 0.6) {
if (p.type === 'health') {
const healed = Math.min(p.amount, player.maxHealth - player.health);
if (healed > 0) {
player.health += healed;
p.active = false;
playSound('pickup');
showPickupMsg(`+${healed} HEALTH`);
}
} else if (p.type === 'ammo') {
const weapon = WEAPONS[player.weapon];
if (p.ammoType === weapon.ammoType) {
player.ammo[p.ammoType] += p.amount;
p.active = false;
playSound('pickup');
showPickupMsg(`+${p.amount} ${weapon.name.toUpperCase()} AMMO`);
}
}
}
}
}
function checkExit() {
const player = game.player;
const dx = game.exitPos.x - player.x;
const dy = game.exitPos.y - player.y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 0.8) {
playerWin();
}
}
function showPickupMsg(text) {
pickupMsg.textContent = text;
pickupMsg.style.opacity = '1';
setTimeout(() => pickupMsg.style.opacity = '0', 1500);
}
// --- COMBAT ---
function shoot() {
const player = game.player;
const now = performance.now();
const weapon = WEAPONS[player.weapon];
const ammoKey = weapon.ammoType;
if (player.reloadTimer > 0) return;
if (now - game.lastShot < weapon.fireRate) return;
if (player.ammo[ammoKey] <= 0) return;
game.lastShot = now;
player.ammo[ammoKey]--;
player.recoil = 1;
player.muzzleFlash = 1;
playSound(player.weapon);
// Hit detection
for (let i = 0; i < weapon.pellets; i++) {
const spread = (Math.random() - 0.5) * weapon.spread;
const rayAngle = player.angle + spread;
const ray = castRay(player.x, player.y, rayAngle);
// Check enemy hits
for (const e of game.enemies) {
if (e.state === 'dead') continue;
const dx = e.x - player.x;
const dy = e.y - player.y;
const dist = Math.sqrt(dx * dx + dy * dy);
// Check if enemy is in the ray path
const enemyAngle = Math.atan2(dy, dx);
let angleDiff = enemyAngle - rayAngle;
while (angleDiff > Math.PI) angleDiff -= Math.PI * 2;
while (angleDiff < -Math.PI) angleDiff += Math.PI * 2;
const hitWidth = 0.3 / dist; // apparent width
if (Math.abs(angleDiff) < hitWidth && dist < ray.perpDist) {
const dmg = weapon.baseDmg + Math.random() * 10;
e.health -= dmg;
e.hitFlash = 1;
if (e.health <= 0) {
e.state = 'dead';
e.deadTimer = 0;
playSound('enemy_die');
}
break;
}
}
}
}
// --- MOVEMENT ---
function movePlayer(dt) {
const player = game.player;
let moveX = 0, moveY = 0;
let moving = false;
if (game.keys['KeyW'] || game.keys['ArrowUp']) {
moveX += Math.cos(player.angle) * MOVE_SPEED * dt;
moveY += Math.sin(player.angle) * MOVE_SPEED * dt;
moving = true;
}
if (game.keys['KeyS'] || game.keys['ArrowDown']) {
moveX -= Math.cos(player.angle) * MOVE_SPEED * dt;
moveY -= Math.sin(player.angle) * MOVE_SPEED * dt;
moving = true;
}
if (game.keys['KeyA'] || game.keys['ArrowLeft']) {
moveX += Math.cos(player.angle - Math.PI/2) * STRAFE_SPEED * dt;
moveY += Math.sin(player.angle - Math.PI/2) * STRAFE_SPEED * dt;
moving = true;
}
if (game.keys['KeyD'] || game.keys['ArrowRight']) {
moveX += Math.cos(player.angle + Math.PI/2) * STRAFE_SPEED * dt;
moveY += Math.sin(player.angle + Math.PI/2) * STRAFE_SPEED * dt;
moving = true;
}
// Collision detection with sliding
const margin = 0.2;
if (!isWall(player.x + moveX + (moveX > 0 ? margin : -margin), player.y)) {
player.x += moveX;
}
if (!isWall(player.x, player.y + moveY + (moveY > 0 ? margin : -margin))) {
player.y += moveY;
}
player.speed = moving ? 1 : 0;
player.bobPhase += player.speed * dt * 8;
player.bobAmount = Math.min(1, player.bobAmount + dt * 5) * player.speed;
// Recoil recovery
player.recoil = Math.max(0, player.recoil - dt * 5);
player.muzzleFlash = Math.max(0, player.muzzleFlash - dt * 8);
// Reload timer
if (game.reloadTimer > 0) {
game.reloadTimer -= dt;
if (game.reloadTimer <= 0) {
game.reloadTimer = 0;
showPickupMsg('READY');
}
}
}
// --- HUD UPDATE ---
function updateHUD() {
const player = game.player;
hudHealth.textContent = Math.ceil(player.health);
hudHealth.style.color = player.health > 50 ? '#4f4' : player.health > 25 ? '#ff4' : '#f44';
hudAmmo.textContent = player.ammo[WEAPONS[player.weapon].ammoType];
hudWeapon.textContent = WEAPONS[player.weapon].name;
}
// --- MAIN LOOP ---
function gameLoop(timestamp) {
if (!game.running) return;
const dt = Math.min((timestamp - game.lastTime) / 1000, 0.05);
game.lastTime = timestamp;
// Update
movePlayer(dt);
updateEnemies(dt);
checkPickups();
updateHUD();
// Render
canvas.width = SCREEN_W;
canvas.height = SCREEN_H;
ctx.imageSmoothingEnabled = false;
render();
requestAnimationFrame(gameLoop);
}
// --- INPUT ---
document.addEventListener('keydown', (e) => {
game.keys[e.code] = true;
// Weapon switching
if (e.code === 'Digit1') {
game.player.weapon = 'pistol';
playSound('hit');
}
if (e.code === 'Digit2') {
game.player.weapon = 'shotgun';
playSound('hit');
}
if (e.code === 'Digit3') {
game.player.weapon = 'plasma';
playSound('hit');
}
// Reload
if (e.code === 'KeyR' && game.reloadTimer <= 0) {
game.reloadTimer = WEAPONS[game.player.weapon].reloadTime / 1000;
}
});
document.addEventListener('keyup', (e) => {
game.keys[e.code] = false;
});
document.addEventListener('mousemove', (e) => {
if (document.pointerLockElement === canvas) {
game.player.angle += e.movementX * MOUSE_SENS;
}
});
canvas.addEventListener('mousedown', (e) => {
if (game.running) {
canvas.requestPointerLock();
game.mouseDown = true;
shoot();
}
});
canvas.addEventListener('mouseup', () => {
game.mouseDown = false;
});
canvas.addEventListener('click', () => {
if (game.running) {
canvas.requestPointerLock();
}
});
// --- START ---
startBtn.addEventListener('click', () => {
audioCtx.resume();
if (game.won || game.dead) {
respawnPlayer();
} else {
game.running = true;
game.lastTime = performance.now();
overlay.classList.remove('active');
initLevel();
requestAnimationFrame(gameLoop);
}
});
// --- INIT ---
game.textureCache = generateTextures();
game.enemyCache = generateEnemySprites();
game.pickupCache = generatePickupSprites();
resetPlayer();
initLevel();
// Initial render
canvas.width = SCREEN_W;
canvas.height = SCREEN_H;
ctx.imageSmoothingEnabled = false;
render();
updateHUD();
// Show instructions
document.getElementById('gameMsg').textContent = 'Find the EXIT door to escape.';
</script>
</body>
</html>
<!-- agent-meta {"model":"qwen/qwen3.6-35b-a3b","provider":"lmstudio","persona":"full","sessionId":"5f334e15-3e38-47ab-bcc7-eb294a029755","tokensIn":742391,"tokensOut":20249,"tokensTotal":762640,"turns":19,"toolCalls":18,"failedToolCalls":1,"timestamp":"2026-07-21T01:05:03.652Z"} -->