DOOM
ling 3.0 Flash · 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 43,352 bytes · SHA-256 a0221b79c29c
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>IRON STORM — Retro FPS</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #000; overflow: hidden; font-family: 'Courier New', monospace; cursor: none; }
canvas { display: block; image-rendering: pixelated; image-rendering: crisp-edges; }
#gameCanvas { position: absolute; top: 0; left: 0; }
#hud { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; }
.screen-overlay {
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
display: flex; flex-direction: column; align-items: center; justify-content: center;
background: rgba(0,0,0,0.85); z-index: 10; pointer-events: auto;
}
.screen-overlay.hidden { display: none; }
h1 { font-size: 48px; color: #ff3333; text-shadow: 0 0 20px #ff0000, 0 0 40px #ff0000; margin-bottom: 10px; letter-spacing: 4px; }
h2 { font-size: 28px; color: #ffaa00; text-shadow: 0 0 10px #ff8800; margin-bottom: 20px; }
p, .stat { color: #cccccc; font-size: 16px; margin: 4px 0; }
.highlight { color: #00ff88; }
.danger { color: #ff4444; }
.ammo { color: #ffaa00; }
.btn {
margin-top: 20px; padding: 12px 36px; font-family: 'Courier New', monospace;
font-size: 18px; background: #331111; color: #ff4444; border: 2px solid #ff4444;
cursor: pointer; letter-spacing: 2px; text-transform: uppercase;
}
.btn:hover { background: #552222; box-shadow: 0 0 15px #ff0000; }
.controls-info { margin-top: 30px; text-align: center; color: #888888; font-size: 13px; line-height: 1.8; }
.controls-info span { color: #aaaaaa; }
#weaponName { position: absolute; bottom: 100px; left: 50%; transform: translateX(-50%); color: #fff; font-size: 14px; text-shadow: 0 0 8px #000; }
#damageFlash { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; opacity: 0; transition: opacity 0.1s; }
#pickupNotify { position: absolute; top: 40%; left: 50%; transform: translateX(-50%); color: #00ff88; font-size: 20px; text-shadow: 0 0 10px #00ff88; opacity: 0; transition: opacity 0.3s; pointer-events: none; }
#minimap { position: absolute; top: 10px; right: 10px; border: 2px solid #333; }
#crosshair { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: #0f0; font-size: 24px; text-shadow: 0 0 4px #0f0; pointer-events: none; }
#gameOverReason { color: #ff4444; font-size: 20px; margin-bottom: 10px; }
#victoryText { color: #ffaa00; font-size: 24px; text-shadow: 0 0 15px #ff8800; margin-bottom: 15px; }
.score-display { color: #ffff00; font-size: 18px; }
.wave-display { color: #00ccff; font-size: 16px; }
#scanlines {
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
pointer-events: none; z-index: 5;
background: repeating-linear-gradient(
0deg,
rgba(0,0,0,0.08) 0px,
rgba(0,0,0,0.08) 1px,
transparent 1px,
transparent 3px
);
}
#vignette {
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
pointer-events: none; z-index: 4;
background: radial-gradient(ellipse at center, transparent 50%, rgba(0,0,0,0.6) 100%);
}
</style>
</head>
<body>
<canvas id="gameCanvas"></canvas>
<div id="hud">
<canvas id="minimap" width="160" height="160"></canvas>
<div id="crosshair">+</div>
<div id="damageFlash"></div>
<div id="pickupNotify"></div>
<div id="weaponName"></div>
</div>
<div id="scanlines"></div>
<div id="vignette"></div>
<!-- Title Screen -->
<div id="titleScreen" class="screen-overlay">
<h1>IRON STORM</h1>
<h2>RETRO FPS</h2>
<p class="stat">Eliminate all enemies to win</p>
<p class="stat"><span class="highlight">WASD</span> Move | <span class="highlight">MOUSE</span> Look | <span class="highlight">CLICK</span> Shoot</p>
<p class="stat"><span class="highlight">1</span> Pistol | <span class="highlight">2</span> Shotgun | <span class="highlight">3</span> Plasma</p>
<p class="stat"><span class="highlight">R</span> Reload | <span class="highlight">ESC</span> Pause</p>
<button class="btn" id="startBtn">INSERT COIN</button>
<div class="controls-info">
<span>Find health [+] and ammo boxes to survive</span><br>
<span>3 weapons | Health pickups | Ammo pickups | Death = respawn</span>
</div>
</div>
<!-- Game Over Screen -->
<div id="gameOverScreen" class="screen-overlay hidden">
<h1 style="color:#ff4444;">GAME OVER</h1>
<div id="gameOverReason" class="danger">You have fallen in battle.</div>
<p class="score-display">Score: <span id="finalScore">0</span></p>
<p class="stat">Enemies slain: <span id="finalKills">0</span></p>
<button class="btn" id="retryBtn">TRY AGAIN</button>
</div>
<!-- Victory Screen -->
<div id="victoryScreen" class="screen-overlay hidden">
<h1 style="color:#ffaa00;">MISSION COMPLETE</h1>
<div id="victoryText">All enemies eliminated!</div>
<p class="score-display">Final Score: <span id="victoryScore">0</span></p>
<p class="stat">Enemies slain: <span id="victoryKills">0</span></p>
<button class="btn" id="nextLevelBtn">NEXT LEVEL</button>
</div>
<!-- Pause Screen -->
<div id="pauseScreen" class="screen-overlay hidden">
<h2>PAUSED</h2>
<button class="btn" id="resumeBtn">RESUME</button>
</div>
<script>
// ==================== CONFIGURATION ====================
const TILE = 64;
const FOV = Math.PI / 3;
const HALF_FOV = FOV / 2;
const MAX_DEPTH = 20;
const MOVE_SPEED = 3.5;
const ROT_SPEED = 0.003;
const PLAYER_HEIGHT = 1.0;
const PLAYER_RADIUS = 0.3;
const MAX_HEALTH = 100;
const MAX_LIVES = 3;
// ==================== MAP ====================
// 0=empty, 1=gray wall, 2=brown wall, 3=dark wall, 4=red wall, 5=health, 6=ammo pistol, 7=ammo shotgun, 8=ammo plasma
const MAP_TEMPLATE = [
[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,0,0,3,3,0,0,0,0,0,4,4,0,0,0,0,1],
[1,0,0,2,0,0,0,0,0,0,3,0,0,0,0,0,0,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,0,0,0,5,0,0,0,0,0,0,0,0,0,6,0,0,0,0,0,0,1],
[1,0,0,0,0,0,0,0,0,0,0,0,0,7,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,3,3,0,0,0,0,0,0,0,0,0,0,0,0,4,4,0,0,0,1],
[1,0,0,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,4,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,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],
];
// ==================== ENEMY TYPES ====================
const ENEMY_TYPES = {
grunt: { color: '#44aa44', hp: 30, speed: 1.2, damage: 8, shootRate: 2000, size: 0.6, score: 100 },
sergeant: { color: '#cc4444', hp: 60, speed: 0.8, damage: 15, shootRate: 3000, size: 0.7, score: 200 },
officer: { color: '#4444cc', hp: 45, speed: 1.5, damage: 12, shootRate: 1500, size: 0.65, score: 150 },
};
// ==================== WEAPONS ====================
const WEAPONS = {
pistol: { name: 'PISTOL', ammoMax: 9999, reloadTime: 0, damage: 12, fireRate: 250, pellets: 1, spread: 0.02, color: '#cccccc', sound: 'pistol' },
shotgun: { name: 'SHOTGUN', ammoMax: 50, reloadTime: 800, damage: 8, fireRate: 700, pellets: 5, spread: 0.15, color: '#ff8800', sound: 'shotgun', ammoType: 'shell' },
plasma: { name: 'PLASMA', ammoMax: 200, reloadTime: 0, damage: 25, fireRate: 500, pellets: 1, spread: 0.01, color: '#00ffff', sound: 'plasma', ammoType: 'cell' },
};
// ==================== AUDIO ====================
let audioCtx = null;
function initAudio() {
if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
}
function playSound(type) {
if (!audioCtx) return;
try {
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.connect(gain);
gain.connect(audioCtx.destination);
const now = audioCtx.currentTime;
switch(type) {
case 'pistol':
osc.type = 'square';
osc.frequency.setValueAtTime(800, now);
osc.frequency.exponentialRampToValueAtTime(100, now + 0.08);
gain.gain.setValueAtTime(0.15, now);
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.08);
osc.start(now); osc.stop(now + 0.08);
break;
case 'shotgun':
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(200, now);
osc.frequency.exponentialRampToValueAtTime(30, now + 0.15);
gain.gain.setValueAtTime(0.25, now);
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.15);
osc.start(now); osc.stop(now + 0.15);
break;
case 'plasma':
osc.type = 'sine';
osc.frequency.setValueAtTime(1200, now);
osc.frequency.exponentialRampToValueAtTime(200, now + 0.2);
gain.gain.setValueAtTime(0.12, now);
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.2);
osc.start(now); osc.stop(now + 0.2);
break;
case 'hit':
osc.type = 'square';
osc.frequency.setValueAtTime(300, now);
osc.frequency.exponentialRampToValueAtTime(50, now + 0.05);
gain.gain.setValueAtTime(0.1, now);
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.05);
osc.start(now); osc.stop(now + 0.05);
break;
case 'enemyDie':
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(400, now);
osc.frequency.exponentialRampToValueAtTime(50, now + 0.3);
gain.gain.setValueAtTime(0.15, now);
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.3);
osc.start(now); osc.stop(now + 0.3);
break;
case 'pickup':
osc.type = 'sine';
osc.frequency.setValueAtTime(600, now);
osc.frequency.setValueAtTime(900, now + 0.05);
osc.frequency.setValueAtTime(1200, now + 0.1);
gain.gain.setValueAtTime(0.1, now);
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.15);
osc.start(now); osc.stop(now + 0.15);
break;
case 'playerHurt':
osc.type = 'square';
osc.frequency.setValueAtTime(150, now);
osc.frequency.exponentialRampToValueAtTime(50, now + 0.2);
gain.gain.setValueAtTime(0.15, now);
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.2);
osc.start(now); osc.stop(now + 0.2);
break;
case 'reload':
osc.type = 'triangle';
osc.frequency.setValueAtTime(400, now);
osc.frequency.setValueAtTime(600, now + 0.1);
gain.gain.setValueAtTime(0.08, now);
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.15);
osc.start(now); osc.stop(now + 0.15);
break;
case 'death':
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(600, now);
osc.frequency.exponentialRampToValueAtTime(30, now + 0.6);
gain.gain.setValueAtTime(0.2, now);
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.6);
osc.start(now); osc.stop(now + 0.6);
break;
case 'victory':
osc.type = 'sine';
osc.frequency.setValueAtTime(523, now);
osc.frequency.setValueAtTime(659, now + 0.15);
osc.frequency.setValueAtTime(784, now + 0.3);
osc.frequency.setValueAtTime(1047, now + 0.45);
gain.gain.setValueAtTime(0.15, now);
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.6);
osc.start(now); osc.stop(now + 0.6);
break;
}
} catch(e) {}
}
// ==================== GAME STATE ====================
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const mmCanvas = document.getElementById('minimap');
const mmCtx = mmCanvas.getContext('2d');
let W, H, scale;
function resize() {
W = window.innerWidth;
H = window.innerHeight;
canvas.width = W;
canvas.height = H;
scale = Math.min(W / 320, H / 200);
}
window.addEventListener('resize', resize);
resize();
let gameState = 'title'; // title, playing, paused, gameover, victory
let score = 0;
let kills = 0;
let level = 1;
let lives = MAX_LIVES;
// Player
let player = {};
let playerInvincible = 0;
// Enemies
let enemies = [];
// Projectiles
let projectiles = [];
// Particles
let particles = [];
// Pickups on map
let mapPickups = [];
// Current map
let currentMap = [];
// Weapon state
let currentWeapon = 'pistol';
let weaponAmmo = {};
let weaponCooldown = 0;
let reloading = false;
let reloadTimer = 0;
let muzzleFlash = 0;
// Damage flash
let damageFlashTimer = 0;
// Pickup notify
let pickupNotifyTimer = 0;
let pickupNotifyText = '';
// Screen shake
let shakeTimer = 0;
let shakeIntensity = 0;
// Time
let lastTime = 0;
let deltaTime = 0;
// Keys
const keys = {};
let mouseDown = false;
let mouseSensitivity = 0.002;
let pointerLocked = false;
// ==================== MAP GENERATION ====================
function generateMap(lvl) {
// Deep copy template
const map = MAP_TEMPLATE.map(row => [...row]);
// Add some random walls for variety
const rand = (seed) => {
let x = Math.sin(seed * 12.9898 + 78.233) * 43758.5453;
return x - Math.floor(x);
};
// Place extra walls based on level
const extraWalls = lvl * 3;
for (let i = 0; i < extraWalls; i++) {
const rx = Math.floor(rand(lvl * 100 + i) * 22) + 1;
const ry = Math.floor(rand(lvl * 200 + i * 7) * 18) + 1;
if (map[ry] && map[ry][rx] === 0) {
// Don't block spawn
if (rx > 2 || ry > 2) map[ry][rx] = 1;
}
}
return map;
}
function getMap() { return currentMap; }
function isWall(x, y) {
const map = getMap();
const mx = Math.floor(x);
const my = Math.floor(y);
if (my < 0 || my >= map.length || mx < 0 || mx >= map[0].length) return true;
return map[my][mx] !== 0;
}
// ==================== PLAYER INIT ====================
function initPlayer() {
player = {
x: 2.5, y: 2.5,
angle: 0,
health: MAX_HEALTH,
bobPhase: 0,
bobAmount: 0,
};
playerInvincible = 0;
// Reset weapons
weaponAmmo = {
pistol: 9999,
shotgun: 20,
plasma: 100,
};
currentWeapon = 'pistol';
weaponCooldown = 0;
reloading = false;
reloadTimer = 0;
}
// ==================== ENEMY SPAWNING ====================
function spawnEnemies(lvl) {
enemies = [];
const count = 3 + lvl * 2;
const rand = (seed) => {
let x = Math.sin(seed * 12.9898 + 78.233) * 43758.5453;
return x - Math.floor(x);
};
for (let i = 0; i < count; i++) {
let ex, ey, attempts = 0;
do {
ex = Math.floor(rand(lvl * 1000 + i * 37 + attempts) * 22) + 1;
ey = Math.floor(rand(lvl * 2000 + i * 53 + attempts) * 18) + 1;
attempts++;
} while ((isWall(ex, ey) || (Math.abs(ex - 2.5) < 2 && Math.abs(ey - 2.5) < 2)) && attempts < 50);
let type = 'grunt';
if (lvl >= 2 && rand(lvl * 500 + i) > 0.5) type = 'sergeant';
if (lvl >= 3 && rand(lvl * 700 + i) > 0.7) type = 'officer';
const et = ENEMY_TYPES[type];
enemies.push({
x: ex + 0.5, y: ey + 0.5,
type, ...et,
alive: true,
alertTimer: 0,
shootCooldown: 0,
hitFlash: 0,
angle: 0,
});
}
}
// ==================== PICKUPS ====================
function initPickups() {
mapPickups = [];
const map = getMap();
for (let y = 0; y < map.length; y++) {
for (let x = 0; x < map[0].length; x++) {
if (map[y][x] === 5) mapPickups.push({ x: x + 0.5, y: y + 0.5, type: 'health', collected: false, bobPhase: Math.random() * Math.PI * 2 });
if (map[y][x] === 6) mapPickups.push({ x: x + 0.5, y: y + 0.5, type: 'ammo_pistol', collected: false, bobPhase: Math.random() * Math.PI * 2 });
if (map[y][x] === 7) mapPickups.push({ x: x + 0.5, y: y + 0.5, type: 'ammo_shotgun', collected: false, bobPhase: Math.random() * Math.PI * 2 });
if (map[y][x] === 8) mapPickups.push({ x: x + 0.5, y: y + 0.5, type: 'ammo_plasma', collected: false, bobPhase: Math.random() * Math.PI * 2 });
}
}
}
// ==================== PARTICLES ====================
function addParticle(x, y, z, vx, vy, vz, color, life, size) {
particles.push({ x, y, z, vx, vy, vz, color, life, maxLife: life, size: size || 2 });
}
function addMuzzleParticles(x, y, angle, color) {
for (let i = 0; i < 6; i++) {
const a = angle + (Math.random() - 0.5) * 0.5;
const sp = 2 + Math.random() * 3;
addParticle(x, y, PLAYER_HEIGHT, Math.cos(a) * sp, Math.sin(a) * sp, (Math.random() - 0.5) * 2, color, 0.15, 3);
}
}
function addHitParticles(x, y, color) {
for (let i = 0; i < 8; i++) {
const a = Math.random() * Math.PI * 2;
const sp = 1 + Math.random() * 2;
addParticle(x, y, 0.5, Math.cos(a) * sp, Math.sin(a) * sp, 1 + Math.random() * 2, color, 0.3, 2);
}
}
function addDeathParticles(x, y, color) {
for (let i = 0; i < 15; i++) {
const a = Math.random() * Math.PI * 2;
const sp = 1 + Math.random() * 4;
addParticle(x, y, 0.5 + Math.random() * 0.5, Math.cos(a) * sp, Math.sin(a) * sp, 2 + Math.random() * 3, color, 0.5 + Math.random() * 0.5, 3);
}
}
function addPickupParticles(x, y) {
for (let i = 0; i < 10; i++) {
const a = Math.random() * Math.PI * 2;
const sp = 1 + Math.random() * 2;
addParticle(x, y, 0.5, Math.cos(a) * sp, Math.sin(a) * sp, 1 + Math.random(), '#00ff88', 0.4, 2);
}
}
// ==================== INPUT ====================
document.addEventListener('keydown', e => {
keys[e.key.toLowerCase()] = true;
if (e.key === 'Escape' && gameState === 'playing') {
gameState = 'paused';
document.getElementById('pauseScreen').classList.remove('hidden');
} else if (e.key === 'Escape' && gameState === 'paused') {
gameState = 'playing';
document.getElementById('pauseScreen').classList.add('hidden');
document.body.requestPointerLock();
}
if (gameState === 'playing') {
if (e.key === '1') switchWeapon('pistol');
if (e.key === '2') switchWeapon('shotgun');
if (e.key === '3') switchWeapon('plasma');
if (e.key.toLowerCase() === 'r') startReload();
}
e.preventDefault();
});
document.addEventListener('keyup', e => { keys[e.key.toLowerCase()] = false; });
canvas.addEventListener('mousedown', e => {
if (gameState === 'playing') {
initAudio();
if (pointerLocked) document.body.requestPointerLock();
shoot();
}
});
canvas.addEventListener('mousemove', e => {
if (pointerLocked && gameState === 'playing') {
player.angle += e.movementX * mouseSensitivity;
}
});
canvas.addEventListener('click', () => {
if (gameState === 'playing' && !pointerLocked) {
document.body.requestPointerLock();
}
});
document.addEventListener('pointerlockchange', () => {
pointerLocked = document.pointerLockElement === canvas;
});
// ==================== WEAPON SWITCHING ====================
function switchWeapon(w) {
if (currentWeapon === w) return;
currentWeapon = w;
weaponCooldown = 0;
reloading = false;
document.getElementById('weaponName').textContent = WEAPONS[w].name;
document.getElementById('weaponName').style.color = WEAPONS[w].color;
// Brief flash
setTimeout(() => { document.getElementById('weaponName').textContent = ''; }, 1500);
}
function startReload() {
const w = WEAPONS[currentWeapon];
if (currentWeapon === 'pistol') return; // Pistol never needs reload
if (reloading) return;
if (weaponAmmo[currentWeapon] >= w.ammoMax) return;
reloading = true;
reloadTimer = w.reloadTime / 1000;
playSound('reload');
}
// ==================== SHOOTING ====================
function shoot() {
if (gameState !== 'playing') return;
if (reloading) return;
const w = WEAPONS[currentWeapon];
if (weaponCooldown > 0) return;
// Check ammo
if (currentWeapon !== 'pistol') {
if (weaponAmmo[currentWeapon] <= 0) {
startReload();
return;
}
}
// Consume ammo
if (currentWeapon !== 'pistol') {
weaponAmmo[currentWeapon]--;
}
weaponCooldown = w.fireRate / 1000;
muzzleFlash = 0.08;
// Fire pellets
for (let p = 0; p < w.pellets; p++) {
const spread = (Math.random() - 0.5) * w.spread;
const projAngle = player.angle + spread;
projectiles.push({
x: player.x + Math.cos(player.angle) * 0.3,
y: player.y + Math.sin(player.angle) * 0.3,
vx: Math.cos(projAngle) * 12,
vy: Math.sin(projAngle) * 12,
damage: w.damage,
life: 0.5,
color: w.color,
friendly: true,
});
}
addMuzzleParticles(player.x, player.y, player.angle, w.color);
playSound(w.sound);
// Shotgun kick (screen shake)
if (currentWeapon === 'shotgun') {
shakeTimer = 0.1;
shakeIntensity = 5;
}
}
// ==================== UPDATE ====================
function update(dt) {
if (gameState !== 'playing') return;
// Delta time cap
dt = Math.min(dt, 0.05);
// Timers
if (weaponCooldown > 0) weaponCooldown -= dt;
if (muzzleFlash > 0) muzzleFlash -= dt;
if (damageFlashTimer > 0) damageFlashTimer -= dt;
if (pickupNotifyTimer > 0) pickupNotifyTimer -= dt;
if (shakeTimer > 0) shakeTimer -= dt;
if (reloadTimer > 0) {
reloadTimer -= dt;
if (reloadTimer <= 0) {
reloading = false;
const w = WEAPONS[currentWeapon];
if (currentWeapon === 'shotgun') weaponAmmo.shotgun = Math.min(weaponAmmo.shotgun + 5, w.ammoMax);
if (currentWeapon === 'plasma') weaponAmmo.plasma = Math.min(weaponAmmo.plasma + 50, w.ammoMax);
}
}
if (playerInvincible > 0) playerInvincible -= dt;
// Player movement
let dx = 0, dy = 0;
const spd = MOVE_SPEED * dt;
if (keys['w'] || keys['arrowup']) { dx += Math.cos(player.angle) * spd; dy += Math.sin(player.angle) * spd; }
if (keys['s'] || keys['arrowdown']) { dx -= Math.cos(player.angle) * spd; dy -= Math.sin(player.angle) * spd; }
if (keys['a']) { dx += Math.cos(player.angle - Math.PI / 2) * spd; dy += Math.sin(player.angle - Math.PI / 2) * spd; }
if (keys['d']) { dx += Math.cos(player.angle + Math.PI / 2) * spd; dy += Math.sin(player.angle + Math.PI / 2) * spd; }
// Collision
const newX = player.x + dx;
const newY = player.y + dy;
if (!isWall(newX + PLAYER_RADIUS, player.y) && !isWall(newX - PLAYER_RADIUS, player.y) &&
!isWall(newX, player.y + PLAYER_RADIUS) && !isWall(newX, player.y - PLAYER_RADIUS)) {
player.x = newX;
}
if (!isWall(player.x + PLAYER_RADIUS, newY) && !isWall(player.x - PLAYER_RADIUS, newY) &&
!isWall(player.x, newY + PLAYER_RADIUS) && !isWall(player.x, newY - PLAYER_RADIUS)) {
player.y = newY;
}
// Head bob
const moving = Math.abs(dx) > 0.001 || Math.abs(dy) > 0.001;
if (moving) {
player.bobPhase += dt * 10;
player.bobAmount = Math.sin(player.bobPhase) * 0.03;
} else {
player.bobAmount *= 0.9;
}
// Update projectiles
for (let i = projectiles.length - 1; i >= 0; i--) {
const p = projectiles[i];
p.x += p.vx * dt;
p.y += p.vy * dt;
p.life -= dt;
if (p.life <= 0 || isWall(p.x, p.y)) {
// Hit wall - add spark particles
addHitParticles(p.x, p.y, p.color);
projectiles.splice(i, 1);
continue;
}
// Check enemy collision
let hitEnemy = false;
for (let j = enemies.length - 1; j >= 0; j--) {
const e = enemies[j];
if (!e.alive) continue;
const dist = Math.hypot(p.x - e.x, p.y - e.y);
if (dist < e.size + 0.15) {
e.hp -= p.damage;
e.hitFlash = 0.1;
addHitParticles(e.x, e.y, '#ff4444');
playSound('hit');
if (e.hp <= 0) {
e.alive = false;
score += e.score;
kills++;
addDeathParticles(e.x, e.y, e.color);
playSound('enemyDie');
// Drop a random pickup
if (Math.random() < 0.3) {
const pickups = ['health', 'ammo_pistol', 'ammo_shotgun', 'ammo_plasma'];
mapPickups.push({ x: e.x, y: e.y, type: pickups[Math.floor(Math.random() * pickups.length)], collected: false, bobPhase: 0 });
}
}
hitEnemy = true;
break;
}
}
if (hitEnemy) {
projectiles.splice(i, 1);
continue;
}
}
// Update enemies
for (let i = enemies.length - 1; i >= 0; i--) {
const e = enemies[i];
if (!e.alive) continue;
if (e.hitFlash > 0) e.hitFlash -= dt;
// Check if player is visible (simple distance + angle check)
const dxp = player.x - e.x;
const dyp = player.y - e.y;
const dist = Math.hypot(dxp, dyp);
const angleToPlayer = Math.atan2(dyp, dxp);
let angleDiff = angleToPlayer - e.angle;
while (angleDiff > Math.PI) angleDiff -= Math.PI * 2;
while (angleDiff < -Math.PI) angleDiff += Math.PI * 2;
if (dist < 12) {
e.angle += angleDiff * 3 * dt;
// Move toward player
if (dist > 1.5) {
const moveX = Math.cos(e.angle) * e.speed * dt;
const moveY = Math.sin(e.angle) * e.speed * dt;
const nx = e.x + moveX;
const ny = e.y + moveY;
if (!isWall(nx, e.y)) e.x = nx;
if (!isWall(e.x, ny)) e.y = ny;
}
// Shoot at player
e.shootCooldown -= dt * 1000;
if (e.shootCooldown <= 0 && dist < 10) {
e.shootCooldown = e.shootRate;
// Enemy projectile
const eSpread = (Math.random() - 0.5) * 0.1;
const eAngle = e.angle + eSpread;
projectiles.push({
x: e.x, y: e.y,
vx: Math.cos(eAngle) * 8,
vy: Math.sin(eAngle) * 8,
damage: e.damage,
life: 1.5,
color: '#ff4444',
friendly: false,
});
}
}
}
// Check enemy projectiles hitting player
for (let i = projectiles.length - 1; i >= 0; i--) {
const p = projectiles[i];
if (p.friendly) continue;
const dist = Math.hypot(p.x - player.x, p.y - player.y);
if (dist < 0.4) {
if (playerInvincible > 0) {
projectiles.splice(i, 1);
continue;
}
player.health -= p.damage;
damageFlashTimer = 0.15;
shakeTimer = 0.1;
shakeIntensity = 3;
playSound('playerHurt');
projectiles.splice(i, 1);
if (player.health <= 0) {
playerDie();
}
}
}
// Check pickups
for (const pickup of mapPickups) {
if (pickup.collected) continue;
const dist = Math.hypot(player.x - pickup.x, player.y - pickup.y);
if (dist < 0.8) {
pickup.collected = true;
playSound('pickup');
addPickupParticles(pickup.x, pickup.y);
switch (pickup.type) {
case 'health':
player.health = Math.min(MAX_HEALTH, player.health + 25);
showPickupNotify('+25 HEALTH');
break;
case 'ammo_pistol':
// Pistol has infinite ammo, but give bonus
showPickupNotify('AMMO');
break;
case 'ammo_shotgun':
weaponAmmo.shotgun = Math.min(WEAPONS.shotgun.ammoMax, weaponAmmo.shotgun + 8);
showPickupNotify('+8 SHELLS');
break;
case 'ammo_plasma':
weaponAmmo.plasma = Math.min(WEAPONS.plasma.ammoMax, weaponAmmo.plasma + 25);
showPickupNotify('+25 CELLS');
break;
}
}
}
// Update particles
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
p.x += p.vx * dt;
p.y += p.vy * dt;
p.z += p.vz * dt;
p.life -= dt;
if (p.life <= 0) particles.splice(i, 1);
}
// Check win condition
const aliveEnemies = enemies.filter(e => e.alive).length;
if (aliveEnemies === 0) {
winLevel();
}
}
function playerDie() {
lives--;
playSound('death');
if (lives <= 0) {
gameState = 'gameover';
document.getElementById('finalScore').textContent = score;
document.getElementById('finalKills').textContent = kills;
document.getElementById('gameOverScreen').classList.remove('hidden');
document.exitPointerLock();
} else {
// Respawn with invincibility frames
player.health = MAX_HEALTH;
player.x = 2.5;
player.y = 2.5;
player.angle = 0;
playerInvincible = 2.0; // 2 seconds of invincibility
// Drop some ammo on death
weaponAmmo.shotgun = Math.max(0, weaponAmmo.shotgun - 5);
weaponAmmo.plasma = Math.max(0, weaponAmmo.plasma - 15);
showPickupNotify('RESPAWN');
}
}
function winLevel() {
playSound('victory');
gameState = 'victory';
score += lives * 500; // Bonus for remaining lives
document.getElementById('victoryScore').textContent = score;
document.getElementById('victoryKills').textContent = kills;
document.getElementById('victoryScreen').classList.remove('hidden');
document.exitPointerLock();
}
function showPickupNotify(text) {
pickupNotifyText = text;
pickupNotifyTimer = 1.5;
const el = document.getElementById('pickupNotify');
el.textContent = text;
el.style.opacity = '1';
setTimeout(() => { el.style.opacity = '0'; }, 1200);
}
// ==================== RAYCASTING ====================
function castRays() {
const map = getMap();
const numRays = Math.floor(W / 2);
const rayAngleStep = FOV / numRays;
const viewAngle = player.angle - HALF_FOV;
// Wall colors by type
const wallColors = {
1: { light: '#666666', dark: '#444444' },
2: { light: '#8B6914', dark: '#5C450E' },
3: { light: '#555555', dark: '#333333' },
4: { light: '#883333', dark: '#552222' },
};
for (let i = 0; i < numRays; i++) {
const rayAngle = viewAngle + i * rayAngleStep;
const sinA = Math.sin(rayAngle);
const cosA = Math.cos(rayAngle);
// DDA
let mapX = Math.floor(player.x);
let mapY = Math.floor(player.y);
const deltaDistX = Math.abs(1 / cosA) || 1e30;
const deltaDistY = Math.abs(1 / sinA) || 1e30;
let stepX, stepY, sideDistX, sideDistY;
if (cosA < 0) {
stepX = -1;
sideDistX = (player.x - mapX) * deltaDistX;
} else {
stepX = 1;
sideDistX = (mapX + 1.0 - player.x) * deltaDistX;
}
if (sinA < 0) {
stepY = -1;
sideDistY = (player.y - mapY) * deltaDistY;
} else {
stepY = 1;
sideDistY = (mapY + 1.0 - player.y) * deltaDistY;
}
let side = 0;
let dist = 0;
let wallType = 1;
let hit = false;
for (let d = 0; d < MAX_DEPTH * 4; d++) {
if (sideDistX < sideDistY) {
sideDistX += deltaDistX;
mapX += stepX;
side = 0;
} else {
sideDistY += deltaDistY;
mapY += stepY;
side = 1;
}
if (mapY < 0 || mapY >= map.length || mapX < 0 || mapX >= map[0].length) {
dist = MAX_DEPTH;
break;
}
wallType = map[mapY][mapX];
if (wallType > 0) {
hit = true;
break;
}
}
if (!hit) dist = MAX_DEPTH;
else {
if (side === 0) dist = (mapX - player.x + (1 - stepX) / 2) / cosA;
else dist = (mapY - player.y + (1 - stepY) / 2) / sinA;
}
// Fish-eye correction
dist = dist * Math.cos(rayAngle - player.angle);
if (dist < 0.01) dist = 0.01;
// Wall height
const lineHeight = H / dist;
const drawStart = Math.max(0, Math.floor(H / 2 - lineHeight / 2 + player.bobAmount * H * 0.1));
const drawEnd = Math.min(H, Math.floor(H / 2 + lineHeight / 2 + player.bobAmount * H * 0.1));
// Wall color with distance fog
const colors = wallColors[wallType] || wallColors[1];
let baseColor = side === 0 ? colors.light : colors.dark;
// Distance fog
const fogFactor = Math.min(1, dist / MAX_DEPTH);
const fogR = Math.floor(lerp(parseInt(baseColor.slice(1,3), 16), 0, fogFactor));
const fogG = Math.floor(lerp(parseInt(baseColor.slice(3,5), 16), 0, fogFactor));
const fogB = Math.floor(lerp(parseInt(baseColor.slice(5,7), 16), 0, fogFactor));
const fogColor = `rgb(${fogR},${fogG},${fogB})`;
// Draw column
const screenX = Math.floor(i * (W / numRays));
const colW = Math.ceil(W / numRays) + 1;
ctx.fillStyle = fogColor;
ctx.fillRect(screenX, drawStart, colW, drawEnd - drawStart);
// Wall edge shading (Y-bucket)
if (side === 1) {
ctx.fillStyle = 'rgba(0,0,0,0.15)';
ctx.fillRect(screenX, drawStart, colW, drawEnd - drawStart);
}
}
}
function lerp(a, b, t) { return a + (b - a) * t; }
// ==================== RENDER ====================
function render() {
ctx.clearRect(0, 0, W, H);
if (gameState === 'title') {
renderTitleBackground();
return;
}
// Screen shake
let shakeX = 0, shakeY = 0;
if (shakeTimer > 0) {
shakeX = (Math.random() - 0.5) * shakeIntensity * 2;
shakeY = (Math.random() - 0.5) * shakeIntensity * 2;
}
ctx.save();
ctx.translate(shakeX, shakeY);
// Draw ceiling gradient first (behind walls)
const ceilGrad = ctx.createLinearGradient(0, 0, 0, H / 2);
ceilGrad.addColorStop(0, '#1a1a2e');
ceilGrad.addColorStop(1, '#2a2a3e');
ctx.fillStyle = ceilGrad;
ctx.fillRect(0, 0, W, H / 2);
// Draw floor gradient first (behind walls)
const floorGrad = ctx.createLinearGradient(0, H / 2, 0, H);
floorGrad.addColorStop(0, '#3a3a2a');
floorGrad.addColorStop(1, '#1a1a0a');
ctx.fillStyle = floorGrad;
ctx.fillRect(0, H / 2, W, H / 2);
// Cast rays and draw walls on top
castRays();
// Draw 3D sprites (enemies)
renderSprites();
// Draw particles
renderParticles();
// Muzzle flash overlay
if (muzzleFlash > 0) {
ctx.fillStyle = `rgba(255,255,200,${muzzleFlash * 3})`;
ctx.fillRect(0, 0, W, H);
}
// Damage flash
if (damageFlashTimer > 0) {
ctx.fillStyle = `rgba(255,0,0,${damageFlashTimer * 3})`;
ctx.fillRect(0, 0, W, H);
}
ctx.restore();
// Draw HUD
renderHUD();
// Draw minimap
renderMinimap();
}
function renderTitleBackground() {
// Animated gradient background
const t = Date.now() / 1000;
const grad = ctx.createLinearGradient(0, 0, W, H);
grad.addColorStop(0, `hsl(${(t * 20) % 360}, 60%, 10%)`);
grad.addColorStop(0.5, `hsl(${(t * 20 + 120) % 360}, 60%, 5%)`);
grad.addColorStop(1, `hsl(${(t * 20 + 240) % 360}, 60%, 10%)`);
ctx.fillStyle = grad;
ctx.fillRect(0, 0, W, H);
// Animated grid lines
ctx.strokeStyle = 'rgba(255,50,50,0.1)';
ctx.lineWidth = 1;
for (let i = 0; i < 30; i++) {
const y = (i * 40 + t * 30) % H;
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(W, y);
ctx.stroke();
}
}
function renderSprites() {
// Collect visible sprites with depth
const sprites = [];
for (const e of enemies) {
if (!e.alive) continue;
const dx = e.x - player.x;
const dy = e.y - player.y;
const dist = Math.hypot(dx, dy);
if (dist > MAX_DEPTH) continue;
// Check if in FOV
const angle = Math.atan2(dy, dx);
let diff = angle - player.angle;
while (diff > Math.PI) diff -= Math.PI * 2;
while (diff < -Math.PI) diff += Math.PI * 2;
if (Math.abs(diff) < HALF_FOV + 0.2) {
sprites.push({ e, dist, angle: diff });
}
}
// Sort back to front
sprites.sort((a, b) => b.dist - a.dist);
for (const s of sprites) {
const e = s.e;
const screenX = W / 2 + (s.angle / HALF_FOV) * (W / 2);
const spriteHeight = H / s.dist;
const spriteWidth = spriteHeight * 0.6;
const drawY = H / 2 - spriteHeight / 2 + player.bobAmount * H * 0.1;
const drawX = screenX - spriteWidth / 2;
// Body
const bodyColor = e.hitFlash > 0 ? '#ffffff' : e.color;
ctx.fillStyle = bodyColor;
ctx.fillRect(drawX, drawY, spriteWidth, spriteHeight * 0.7);
// Head
const headH = spriteHeight * 0.3;
const headY = drawY;
const headW = spriteWidth * 0.5;
const headX = screenX - headW / 2;
ctx.fillStyle = e.hitFlash > 0 ? '#ffffff' : e.color;
ctx.fillRect(headX, headY, headW, headH);
// Eyes
if (s.dist < 8) {
ctx.fillStyle = '#ff0000';
const eyeSize = Math.max(2, spriteWidth * 0.06);
ctx.fillRect(headX + headW * 0.25, headY + headH * 0.3, eyeSize, eyeSize);
ctx.fillRect(headX + headW * 0.55, headY + headH * 0.3, eyeSize, eyeSize);
}
// Health bar
if (e.hp < e.maxHp) {
const barW = spriteWidth * 0.8;
const barH = 4;
const barX = screenX - barW / 2;
const barY = drawY - 8;
ctx.fillStyle = '#330000';
ctx.fillRect(barX, barY, barW, barH);
ctx.fillStyle = '#ff0000';
ctx.fillRect(barX, barY, barW * (e.hp / e.maxHp), barH);
}
// Distance shading (fog)
const fogFactor = Math.min(1, s.dist / MAX_DEPTH);
ctx.fillStyle = `rgba(0,0,0,${fogFactor * 0.7})`;
ctx.fillRect(drawX, drawY, spriteWidth, spriteHeight * 0.7);
}
}
function renderParticles() {
for (const p of particles) {
const alpha = p.life / p.maxLife;
const size = p.size * alpha;
ctx.fillStyle = p.color;
ctx.globalAlpha = alpha;
ctx.fillRect(p.x - size / 2, p.y - size / 2, size, size);
}
ctx.globalAlpha = 1;
}
function renderHUD() {
// Health bar
const hbW = 200, hbH = 20;
const hbX = 20, hbY = H - 60;
ctx.fillStyle = '#330000';
ctx.fillRect(hbX, hbY, hbW, hbH);
const hpPct = Math.max(0, player.health / MAX_HEALTH);
const hpColor = hpPct > 0.5 ? '#00cc44' : hpPct > 0.25 ? '#ccaa00' : '#cc0000';
ctx.fillStyle = hpColor;
ctx.fillRect(hbX, hbY, hbW * hpPct, hbH);
ctx.strokeStyle = '#666';
ctx.strokeRect(hbX, hbY, hbW, hbH);
ctx.fillStyle = '#fff';
ctx.font = '12px Courier New';
ctx.textAlign = 'center';
ctx.fillText(`HP ${Math.ceil(Math.max(0, player.health))}`, hbX + hbW / 2, hbY + 14);
// Lives
ctx.textAlign = 'left';
ctx.fillStyle = '#ff4444';
ctx.font = '14px Courier New';
let livesText = '';
for (let i = 0; i < MAX_LIVES; i++) {
livesText += i < lives ? '♥ ' : '♡ ';
}
ctx.fillText(livesText, hbX, hbY - 5);
// Ammo display
const ammoY = H - 90;
ctx.fillStyle = '#ccc';
ctx.font = '13px Courier New';
ctx.textAlign = 'left';
if (currentWeapon === 'pistol') {
ctx.fillText(`[${WEAPONS.pistol.name}] ∞`, 20, ammoY);
} else {
const ammo = weaponAmmo[currentWeapon];
const max = WEAPONS[currentWeapon].ammoMax;
ctx.fillStyle = ammo > 0 ? '#ffaa00' : '#ff4444';
ctx.fillText(`[${WEAPONS[currentWeapon].name}] ${ammo} / ${max}`, 20, ammoY);
if (reloading) {
ctx.fillStyle = '#ff8800';
ctx.fillText('RELOADING...', 20, ammoY - 14);
}
}
// Weapon icon area (bottom right)
const wIconX = W - 80, wIconY = H - 60;
ctx.fillStyle = WEAPONS[currentWeapon].color;
ctx.globalAlpha = 0.3;
ctx.fillRect(wIconX, wIconY, 60, 40);
ctx.globalAlpha = 1;
ctx.fillStyle = WEAPONS[currentWeapon].color;
ctx.font = '10px Courier New';
ctx.textAlign = 'center';
ctx.fillText(WEAPONS[currentWeapon].name, wIconX + 30, wIconY + 25);
// Score
ctx.fillStyle = '#ffff00';
ctx.font = '14px Courier New';
ctx.textAlign = 'right';
ctx.fillText(`SCORE: ${score}`, W - 20, 30);
ctx.fillStyle = '#00ccff';
ctx.fillText(`KILLS: ${kills}`, W - 20, 50);
ctx.fillStyle = '#ff8800';
ctx.fillText(`LEVEL: ${level}`, W - 20, 70);
}
function renderMinimap() {
const mmSize = 160;
const tileSize = mmSize / currentMap[0].length;
mmCtx.fillStyle = '#000';
mmCtx.fillRect(0, 0, mmSize, mmSize);
for (let y = 0; y < currentMap.length; y++) {
for (let x = 0; x < currentMap[0].length; x++) {
const t = currentMap[y][x];
if (t > 0 && t < 5) {
mmCtx.fillStyle = t === 1 ? '#555' : t === 2 ? '#8B6914' : t === 3 ? '#444' : '#883333';
mmCtx.fillRect(x * tileSize, y * tileSize, tileSize, tileSize);
}
}
}
// Pickups
for (const p of mapPickups) {
if (p.collected) continue;
mmCtx.fillStyle = p.type === 'health' ? '#00ff88' : '#ffaa00';
mmCtx.fillRect(p.x * tileSize - 1, p.y * tileSize - 1, 3, 3);
}
// Enemies
for (const e of enemies) {
if (!e.alive) continue;
mmCtx.fillStyle = '#ff4444';
mmCtx.fillRect(e.x * tileSize - 1, e.y * tileSize - 1, 3, 3);
}
// Player
mmCtx.fillStyle = '#00ff00';
mmCtx.beginPath();
mmCtx.arc(player.x * tileSize, player.y * tileSize, 3, 0, Math.PI * 2);
mmCtx.fill();
// Player direction
mmCtx.strokeStyle = '#0f0';
mmCtx.lineWidth = 1;
mmCtx.beginPath();
mmCtx.moveTo(player.x * tileSize, player.y * tileSize);
mmCtx.lineTo(player.x * tileSize + Math.cos(player.angle) * 8, player.y * tileSize + Math.sin(player.angle) * 8);
mmCtx.stroke();
}
// ==================== GAME FLOW ====================
function startGame() {
initAudio();
level = 1;
score = 0;
kills = 0;
lives = MAX_LIVES;
startLevel();
}
function startLevel() {
currentMap = generateMap(level);
initPlayer();
spawnEnemies(level);
initPickups();
projectiles = [];
particles = [];
gameState = 'playing';
document.getElementById('titleScreen').classList.add('hidden');
document.getElementById('gameOverScreen').classList.add('hidden');
document.getElementById('victoryScreen').classList.add('hidden');
document.getElementById('pauseScreen').classList.add('hidden');
document.getElementById('weaponName').textContent = WEAPONS.pistol.name;
document.getElementById('weaponName').style.color = WEAPONS.pistol.color;
document.body.requestPointerLock();
}
function nextLevel() {
level++;
currentMap = generateMap(level);
// Keep player health and ammo but reset position
player.health = Math.min(MAX_HEALTH, player.health + 20);
player.x = 2.5;
player.y = 2.5;
player.angle = 0;
enemies = [];
mapPickups = [];
projectiles = [];
particles = [];
spawnEnemies(level);
initPickups();
gameState = 'playing';
document.getElementById('victoryScreen').classList.add('hidden');
document.body.requestPointerLock();
}
// ==================== MAIN LOOP ====================
function gameLoop(timestamp) {
deltaTime = (timestamp - lastTime) / 1000;
lastTime = timestamp;
update(deltaTime);
render();
requestAnimationFrame(gameLoop);
}
// ==================== EVENT LISTENERS ====================
document.getElementById('startBtn').addEventListener('click', () => {
initAudio();
startGame();
});
document.getElementById('retryBtn').addEventListener('click', () => {
document.getElementById('gameOverScreen').classList.add('hidden');
startGame();
});
document.getElementById('nextLevelBtn').addEventListener('click', () => {
document.getElementById('victoryScreen').classList.add('hidden');
nextLevel();
});
document.getElementById('resumeBtn').addEventListener('click', () => {
document.getElementById('pauseScreen').classList.add('hidden');
gameState = 'playing';
document.body.requestPointerLock();
});
// ==================== INIT ====================
initPlayer();
currentMap = generateMap(1);
spawnEnemies(1);
initPickups();
lastTime = performance.now();
requestAnimationFrame(gameLoop);
// Prevent context menu
document.addEventListener('contextmenu', e => e.preventDefault());
</script>
</body>
</html>
<!-- agent-meta {"model":"inclusionai/ling-3.0-flash","provider":"openrouter","persona":"full","sessionId":"6d31364f-c728-4110-9d31-547c4fa8e0a6","tokensIn":838018,"tokensOut":25720,"tokensTotal":863738,"cost":0.006824907599999999,"turns":21,"toolCalls":20,"failedToolCalls":0,"timestamp":"2026-08-24T22:41:43.674Z"} -->