← DOOM results

DOOM

Qwen 3.6 35b a3b · 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 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"} -->