← DOOM results

DOOM

ling 3.0 Flash · full

Initial view of DOOM

Reviews & generation details

Reviews

No human review yet. Unreviewed does not mean unsuccessful.

Generation context

Imported historical artifact. Its exact original instructions and lineage are not known.

Generation prompt · exact text unavailable

See the current prompt for reference. It may differ from the instructions used to generate this artifact.

Inspect original source 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"} -->