← DOOM results

DOOM

Qwen 3.6 27b MTP · composer

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 46,947 bytes · SHA-256 ccc2ef84d97e
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>DOOMED CORRIDORS - Retro FPS</title>
<style>
/* ── css: layout ── */
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; background: #000; font-family: 'Courier New', monospace; }
#app { position: relative; width: 100vw; height: 100vh; display: flex; justify-content: center; align-items: center; }
canvas#game { image-rendering: pixelated; image-rendering: crisp-edges; background: #000; cursor: none; }
/* ── css: hud ── */
#hud { position: absolute; bottom: 0; left: 0; right: 0; height: 80px; background: linear-gradient(to bottom, #1a1a2e, #0d0d1a); border-top: 3px solid #444; display: flex; align-items: center; justify-content: space-around; padding: 0 20px; z-index: 10; }
#health-bar { width: 200px; height: 24px; background: #333; border: 2px solid #666; border-radius: 4px; overflow: hidden; position: relative; }
#health-fill { height: 100%; background: linear-gradient(to right, #44cc44, #22aa22); transition: width 0.3s; width: 100%; }
#ammo-display { color: #ffaa00; font-size: 24px; text-shadow: 0 0 10px rgba(255,170,0,0.5); letter-spacing: 2px; }
#weapon-name { color: #ffffff; font-size: 28px; text-shadow: 0 0 15px rgba(255,255,255,0.3); letter-spacing: 4px; }
#score-display { color: #00ffaa; font-size: 24px; text-shadow: 0 0 10px rgba(0,255,170,0.5); letter-spacing: 2px; }
#level-display { color: #aaaaff; font-size: 18px; text-shadow: 0 0 10px rgba(170,170,255,0.5); letter-spacing: 2px; }
#lives-display { color: #ff4444; font-size: 20px; text-shadow: 0 0 10px rgba(255,68,68,0.5); letter-spacing: 2px; }
/* ── css: overlay ── */
#crosshair { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 24px; height: 24px; z-index: 10; pointer-events: none; }
#crosshair::before, #crosshair::after { content: ''; position: absolute; background: rgba(255,255,255,0.7); }
#crosshair::before { width: 2px; height: 100%; left: 50%; transform: translateX(-50%); }
#crosshair::after { height: 2px; width: 100%; top: 50%; transform: translateY(-50%); }
#message-overlay { position: absolute; top: 0; left: 0; right: 0; bottom: 80px; display: flex; align-items: center; justify-content: center; z-index: 20; pointer-events: none; opacity: 0; transition: opacity 0.5s; }
#message-overlay.visible { opacity: 1; }
#message-overlay .msg-text { color: #fff; font-size: 48px; text-shadow: 0 0 30px rgba(255,0,0,0.8), 3px 3px 0 #000; letter-spacing: 6px; text-align: center; }
#minimap-container { position: absolute; top: 10px; right: 10px; z-index: 10; border: 2px solid rgba(0,255,0,0.3); opacity: 0.7; }
canvas#minimap { image-rendering: pixelated; background: rgba(0,0,0,0.6); }
</style>
</head>
<body>
<canvas id="game"></canvas>
<div id="hud">
  <div id="health-bar"><div id="health-fill"></div></div>
  <div id="ammo-display">AMMO: <span id="ammo-count">0</span></div>
  <div id="weapon-name">PISTOL</div>
  <div id="score-display">SCORE: <span id="score-count">0</span></div>
  <div id="level-display">LEVEL 1</div>
<div id="lives-display">LIVES: <span id="lives-count">3</span></div></div>
<div id="crosshair"></div>
<div id="message-overlay"></div>
<div id="minimap-container"><canvas id="minimap"></canvas></div>
<script>
'use strict';

// ── const: SCREEN_W ──
const SCREEN_W = 640;

// ── const: SCREEN_H ──
const SCREEN_H = 480;

// ── const: MINIMAP_SIZE ──
const MINIMAP_SIZE = 120;

// ── const: TILE ──
const TILE = 1;

// ── const: FOV ──
const FOV = Math.PI / 3;

// ── const: HALF_FOV ──
const HALF_FOV = FOV / 2;

// ── const: NUM_RAYS ──
const NUM_RAYS = SCREEN_W;

// ── const: MAX_DEPTH ──
const MAX_DEPTH = 20;

// ── const: MOVE_SPEED ──
const MOVE_SPEED = 3.0;

// ── const: ROT_SPEED ──
const ROT_SPEED = 2.5;

// ── const: WEAPONS ──
// Weapon definitions
const WEAPONS = {
  pistol: { name: 'PISTOL', damage: 15, fireRate: 400, ammoCost: 1, range: 8, spread: 0.02, pellets: 1, color: '#ffcc00' },
  shotgun: { name: 'SHOTGUN', damage: 10, fireRate: 800, ammoCost: 3, range: 6, spread: 0.15, pellets: 5, color: '#ff6600' },
  plasma: { name: 'PLASMA', damage: 25, fireRate: 200, ammoCost: 2, range: 12, spread: 0.03, pellets: 1, color: '#00ffff' }
};

// ── const: ENEMY_TYPES ──
const ENEMY_TYPES = {
  soldier: { hp: 40, speed: 1.2, damage: 8, attackRange: 6, attackRate: 1500, color: '#cc3333', size: 0.3 },
  heavy: { hp: 80, speed: 0.8, damage: 15, attackRange: 5, attackRate: 2000, color: '#996633', size: 0.4 },
  demon: { hp: 60, speed: 2.0, damage: 12, attackRange: 1.5, attackRate: 800, color: '#cc0066', size: 0.35 }
};

// ── const: WALL_COLORS ──
const WALL_COLORS = {
  1: { r: 120, g: 120, b: 120 }, // gray stone
  2: { r: 140, g: 80, b: 60 },   // brown brick
  3: { r: 60, g: 100, b: 60 }    // green mossy
};

// ── function: castRay ──
function castRay(game, angle) {
  const sin = Math.sin(angle);
  const cos = Math.cos(angle);
  
  // DDA algorithm
  let mapX = Math.floor(game.player.x);
  let mapY = Math.floor(game.player.y);
  
  const deltaDistX = Math.abs(1 / (cos || 1e-10));
  const deltaDistY = Math.abs(1 / (sin || 1e-10));
  
  let stepX, stepY, sideDistX, sideDistY;
  
  if (cos < 0) {
    stepX = -1;
    sideDistX = (game.player.x - mapX) * deltaDistX;
  } else {
    stepX = 1;
    sideDistX = (mapX + 1 - game.player.x) * deltaDistX;
  }
  
  if (sin < 0) {
    stepY = -1;
    sideDistY = (game.player.y - mapY) * deltaDistY;
  } else {
    stepY = 1;
    sideDistY = (mapY + 1 - game.player.y) * deltaDistY;
  }
  
  let hit = false;
  let side = 0;
  let dist = 0;
  let wallType = 1;
  
  for (let i = 0; i < MAX_DEPTH * 2; i++) {
    if (sideDistX < sideDistY) {
      sideDistX += deltaDistX;
      mapX += stepX;
      side = 0;
    } else {
      sideDistY += deltaDistY;
      mapY += stepY;
      side = 1;
    }
    
    if (mapY >= 0 && mapY < game.map.length && mapX >= 0 && mapX < game.map[0].length) {
      if (game.map[mapY][mapX] > 0) {
        hit = true;
        wallType = game.map[mapY][mapX];
        break;
      }
    } else {
      hit = true;
      break;
    }
  }
  
  if (side === 0) {
    dist = sideDistX - deltaDistX;
  } else {
    dist = sideDistY - deltaDistY;
  }
  
  // Calculate exact wall X for texture mapping / shading
  let wallX;
  if (side === 0) {
    wallX = game.player.y + dist * sin;
  } else {
    wallX = game.player.x + dist * cos;
  }
  wallX -= Math.floor(wallX);
  
  return { dist, side, wallType, wallX, hit };
}

// ── function: renderSprites ──
function renderSprites(game) {
  const ctx = game.ctx;
  
  // Collect all sprites (enemies + pickups) with their distances
  const sprites = [];
  
  for (const enemy of game.enemies) {
    if (enemy.state === 'dying' && enemy.deathTimer > 1.0) continue;
    const dx = enemy.x - game.player.x;
    const dy = enemy.y - game.player.y;
    const dist = Math.sqrt(dx * dx + dy * dy);
    sprites.push({ x: enemy.x, y: enemy.y, dist, type: 'enemy', ref: enemy });
  }
  
  for (const pickup of game.pickups) {
    if (!pickup.active) continue;
    const dx = pickup.x - game.player.x;
    const dy = pickup.y - game.player.y;
    const dist = Math.sqrt(dx * dx + dy * dy);
    sprites.push({ x: pickup.x, y: pickup.y, dist, type: 'pickup', ref: pickup });
  }
  
  // Add exit portal
  if (game.exitPos) {
    const dx = game.exitPos.x - game.player.x;
    const dy = game.exitPos.y - game.player.y;
    const dist = Math.sqrt(dx * dx + dy * dy);
    sprites.push({ x: game.exitPos.x, y: game.exitPos.y, dist, type: 'exit', ref: null });
  }
  
  // Sort back to front
  sprites.sort((a, b) => b.dist - a.dist);
  
  for (const sprite of sprites) {
    const dx = sprite.x - game.player.x;
    const dy = sprite.y - game.player.y;
    
    // Transform with inverse camera matrix
    const invDet = 1.0 / (Math.cos(game.player.angle + Math.PI/2) * Math.sin(game.player.angle) - 
                           Math.sin(game.player.angle + Math.PI/2) * Math.cos(game.player.angle));
    
    const planeX = Math.cos(game.player.angle + Math.PI/2) * Math.tan(HALF_FOV);
    const planeY = Math.sin(game.player.angle + Math.PI/2) * Math.tan(HALF_FOV);
    
    // Simpler approach: calculate screen position directly
    let angle = Math.atan2(dy, dx) - game.player.angle;
    while (angle < -Math.PI) angle += 2 * Math.PI;
    while (angle > Math.PI) angle -= 2 * Math.PI;
    
    if (Math.abs(angle) > HALF_FOV + 0.3) continue; // Behind camera
    
    const perpDist = sprite.dist * Math.cos(angle);
    if (perpDist < 0.2) continue;
    
    const screenX = SCREEN_W / 2 + (angle / HALF_FOV) * (SCREEN_W / 2);
    const spriteHeight = Math.min(SCREEN_H, SCREEN_H / perpDist);
    const spriteWidth = spriteHeight * 0.7;
    
    if (sprite.type === 'enemy') {
      renderEnemySprite(ctx, sprite.ref, screenX, spriteHeight, spriteWidth, perpDist);
    } else if (sprite.type === 'pickup') {
      renderPickupSprite(ctx, sprite.ref, screenX, spriteHeight * 0.5, perpDist);
    } else if (sprite.type === 'exit') {
      renderExitPortal(ctx, screenX, spriteHeight, spriteWidth, perpDist);
    }
  }
}

// ── function: renderEnemySprite ──
function renderEnemySprite(ctx, enemy, screenX, height, width, dist) {
  const shade = Math.max(0.2, 1 - dist / MAX_DEPTH);
  
  // Check z-buffer for occlusion (center column)
  const col = Math.floor(screenX);
  if (col >= 0 && col < SCREEN_W) {
    if (dist > game.zBuffer[col] + 0.1) return;
  }
  
  const drawY = (SCREEN_H - height) / 2;
  const flashColor = enemy.hitFlash > 0 ? '#ffffff' : 
                     enemy.state === 'dying' ? `rgba(255,0,0,${1 - enemy.deathTimer})` : null;
  
  // Body
  ctx.save();
  if (flashColor) {
    ctx.globalAlpha = 0.7 + Math.sin(enemy.hitFlash * 20) * 0.3;
  }
  
  const bodyW = width * 0.6;
  const bodyH = height * 0.5;
  const headR = width * 0.2;
  const bodyX = screenX - bodyW / 2;
  const bodyY = drawY + height * 0.35;
  
  // Legs
  ctx.fillStyle = shadeColor('#444', shade);
  ctx.fillRect(bodyX + width * 0.1, bodyY + bodyH, bodyW * 0.3, height * 0.25);
  ctx.fillRect(bodyX + bodyW - width * 0.1 - bodyW * 0.3, bodyY + bodyH, bodyW * 0.3, height * 0.25);
  
  // Body
  const bodyColor = flashColor || shadeColor(enemy.color, shade);
  ctx.fillStyle = bodyColor;
  ctx.fillRect(bodyX, bodyY, bodyW, bodyH);
  
  // Head
  const headY = drawY + height * 0.15;
  ctx.beginPath();
  ctx.arc(screenX, headY, headR, 0, Math.PI * 2);
  ctx.fillStyle = flashColor || shadeColor('#ddaa88', shade);
  ctx.fill();
  
  // Eyes (menacing)
  if (!flashColor && enemy.state !== 'dying') {
    const eyeSize = headR * 0.3;
    ctx.fillStyle = shadeColor('#ff0000', shade);
    ctx.fillRect(screenX - headR * 0.5, headY - eyeSize * 0.5, eyeSize, eyeSize);
    ctx.fillRect(screenX + headR * 0.2, headY - eyeSize * 0.5, eyeSize, eyeSize);
  }
  
  // Arms
  const armW = width * 0.15;
  const armH = bodyH * 0.7;
  ctx.fillStyle = flashColor || shadeColor(enemy.color, shade * 0.8);
  ctx.fillRect(bodyX - armW, bodyY + bodyH * 0.1, armW, armH);
  ctx.fillRect(bodyX + bodyW, bodyY + bodyH * 0.1, armW, armH);
  
  // Health bar above enemy
  if (enemy.hp < enemy.maxHp && enemy.state !== 'dying') {
    const barW = width * 0.8;
    const barH = 4;
    const barX = screenX - barW / 2;
    const barY = drawY + height * 0.05;
    ctx.fillStyle = '#333';
    ctx.fillRect(barX, barY, barW, barH);
    ctx.fillStyle = enemy.hp > enemy.maxHp * 0.5 ? '#0f0' : 
                    enemy.hp > enemy.maxHp * 0.25 ? '#ff0' : '#f00';
    ctx.fillRect(barX, barY, barW * (enemy.hp / enemy.maxHp), barH);
  }
  
  ctx.restore();
}

// ── function: renderPickupSprite ──
function renderPickupSprite(ctx, pickup, screenX, size, dist) {
  const col = Math.floor(screenX);
  if (col >= 0 && col < SCREEN_W) {
    if (dist > game.zBuffer[col] + 0.1) return;
  }
  
  const shade = Math.max(0.3, 1 - dist / MAX_DEPTH);
  const bob = Math.sin(pickup.bobPhase) * size * 0.2;
  const drawY = SCREEN_H / 2 + bob;
  
  ctx.save();
  ctx.translate(screenX, drawY);
  
  // Glow effect
  const glowColor = pickup.type === 'health' ? 'rgba(0,255,0,' : 'rgba(255,170,0,';
  ctx.shadowColor = pickup.type === 'health' ? '#0f0' : '#fa0';
  ctx.shadowBlur = size * 0.5;
  
  if (pickup.type === 'health') {
    // Cross shape
    ctx.fillStyle = shadeColor('#ff3333', shade);
    const s = size * 0.4;
    ctx.fillRect(-s, -s * 0.2, s * 2, s * 0.4);
    ctx.fillRect(-s * 0.2, -s, s * 0.4, s * 2);
  } else {
    // Ammo box
    ctx.fillStyle = shadeColor('#ffaa00', shade);
    const s = size * 0.35;
    ctx.fillRect(-s, -s * 0.7, s * 2, s * 1.4);
    ctx.fillStyle = shadeColor('#cc8800', shade);
    ctx.fillRect(-s * 0.6, -s * 0.3, s * 1.2, s * 0.6);
  }
  
  ctx.restore();
}

// ── function: renderExitPortal ──
function renderExitPortal(ctx, screenX, height, width, dist) {
  const col = Math.floor(screenX);
  if (col >= 0 && col < SCREEN_W) {
    if (dist > game.zBuffer[col] + 0.1) return;
  }
  
  const shade = Math.max(0.3, 1 - dist / MAX_DEPTH);
  const drawY = (SCREEN_H - height) / 2;
  const pulse = Math.sin(performance.now() * 0.005) * 0.3 + 0.7;
  
  ctx.save();
  ctx.globalAlpha = shade * pulse;
  
  // Portal frame
  ctx.strokeStyle = '#00ffff';
  ctx.lineWidth = 3;
  ctx.shadowColor = '#00ffff';
  ctx.shadowBlur = 20;
  ctx.strokeRect(screenX - width/2, drawY + height * 0.1, width, height * 0.8);
  
  // Portal interior
  const grad = ctx.createRadialGradient(screenX, SCREEN_H / 2, 0, screenX, SCREEN_H / 2, width);
  grad.addColorStop(0, 'rgba(0,255,255,0.8)');
  grad.addColorStop(0.5, 'rgba(0,100,255,0.4)');
  grad.addColorStop(1, 'rgba(0,0,50,0.1)');
  ctx.fillStyle = grad;
  ctx.fillRect(screenX - width/2 + 3, drawY + height * 0.1 + 3, width - 6, height * 0.8 - 6);
  
  // EXIT text
  if (width > 40) {
    ctx.fillStyle = '#fff';
    ctx.font = `${Math.max(12, width * 0.15)}px monospace`;
    ctx.textAlign = 'center';
    ctx.fillText('EXIT', screenX, SCREEN_H / 2 + height * 0.05);
  }
  
  ctx.restore();
}

// ── function: shadeColor ──
function shadeColor(hex, factor) {
  const r = parseInt(hex.slice(1,3), 16);
  const g = parseInt(hex.slice(3,5), 16);
  const b = parseInt(hex.slice(5,7), 16);
  return `rgb(${Math.floor(r * factor)},${Math.floor(g * factor)},${Math.floor(b * factor)})`;
}

// ── function: renderWeapon ──
function renderWeapon(game) {
  const ctx = game.ctx;
  const p = game.player;
  const w = game.getWeapon();
  
  // Weapon bob
  const bobX = Math.sin(p.bobPhase) * 8;
  const bobY = Math.abs(Math.cos(p.bobPhase)) * 5;
  
  let shootOffset = 0;
  if (p.shootTimer > 0) {
    shootOffset = Math.sin((1 - p.shootTimer / 6) * Math.PI) * 20;
  }
  
  const baseX = SCREEN_W / 2 + bobX;
  const baseY = SCREEN_H - 80 + bobY + shootOffset;
  
  ctx.save();
  
  if (w.name === 'PISTOL') {
    // Pistol body
    ctx.fillStyle = '#555';
    ctx.fillRect(baseX - 12, baseY - 60, 24, 60);
    ctx.fillStyle = '#333';
    ctx.fillRect(baseX - 8, baseY - 55, 16, 50);
    // Barrel
    ctx.fillStyle = '#444';
    ctx.fillRect(baseX - 5, baseY - 90, 10, 35);
    // Grip
    ctx.fillStyle = '#654321';
    ctx.beginPath();
    ctx.moveTo(baseX - 10, baseY + bobY * 0.5);
    ctx.lineTo(baseX + 10, baseY + bobY * 0.5);
    ctx.lineTo(baseX + 8, baseY - 20);
    ctx.lineTo(baseX - 8, baseY - 20);
    ctx.fill();
    
  } else if (w.name === 'SHOTGUN') {
    // Shotgun body
    ctx.fillStyle = '#555';
    ctx.fillRect(baseX - 16, baseY - 70, 32, 70);
    ctx.fillStyle = '#444';
    ctx.fillRect(baseX - 12, baseY - 65, 24, 60);
    // Barrel (double)
    ctx.fillStyle = '#3a3a3a';
    ctx.fillRect(baseX - 10, baseY - 110, 8, 45);
    ctx.fillRect(baseX + 2, baseY - 110, 8, 45);
    // Stock
    ctx.fillStyle = '#654321';
    ctx.beginPath();
    ctx.moveTo(baseX - 14, baseY);
    ctx.lineTo(baseX + 14, baseY);
    ctx.lineTo(baseX + 10, baseY - 30);
    ctx.lineTo(baseX - 10, baseY - 30);
    ctx.fill();
    
  } else if (w.name === 'PLASMA') {
    // Plasma rifle body
    ctx.fillStyle = '#2a4a6a';
    ctx.fillRect(baseX - 18, baseY - 75, 36, 75);
    ctx.fillStyle = '#1a3a5a';
    ctx.fillRect(baseX - 14, baseY - 70, 28, 65);
    // Barrel
    ctx.fillStyle = '#3a6a9a';
    ctx.fillRect(baseX - 8, baseY - 105, 16, 35);
    // Energy core glow
    const glowIntensity = p.shootTimer > 0 ? 1 : 0.4 + Math.sin(performance.now() * 0.01) * 0.2;
    ctx.shadowColor = '#00ffff';
    ctx.shadowBlur = 15 * glowIntensity;
    ctx.fillStyle = `rgba(0,255,255,${glowIntensity})`;
    ctx.beginPath();
    ctx.arc(baseX, baseY - 40, 8, 0, Math.PI * 2);
    ctx.fill();
    // Stock
    ctx.shadowBlur = 0;
    ctx.fillStyle = '#1a3a5a';
    ctx.fillRect(baseX - 16, baseY - 10, 32, 20);
  }
  
  // Muzzle flash
  if (p.shootTimer > 0.7) {
    const flashSize = w.name === 'SHOTGUN' ? 40 : w.name === 'PLASMA' ? 25 : 20;
    ctx.shadowColor = w.color;
    ctx.shadowBlur = 30;
    ctx.fillStyle = w.color;
    ctx.beginPath();
    ctx.arc(baseX, baseY - (w.name === 'PISTOL' ? 90 : w.name === 'SHOTGUN' ? 110 : 105), flashSize * p.shootTimer, 0, Math.PI * 2);
    ctx.fill();
    
    // Flash rays
    for (let i = 0; i < 6; i++) {
      const a = (i / 6) * Math.PI * 2 + performance.now() * 0.01;
      ctx.strokeStyle = w.color;
      ctx.lineWidth = 2;
      ctx.beginPath();
      ctx.moveTo(baseX, baseY - (w.name === 'PISTOL' ? 90 : w.name === 'SHOTGUN' ? 110 : 105));
      ctx.lineTo(
        baseX + Math.cos(a) * flashSize * 2,
        baseY - (w.name === 'PISTOL' ? 90 : w.name === 'SHOTGUN' ? 110 : 105) + Math.sin(a) * flashSize * 2
      );
      ctx.stroke();
    }
  }
  
  ctx.restore();
}

// ── function: renderParticles ──
function renderParticles(game) {
  const ctx = game.ctx;
  
  for (const p of game.particles) {
    const dx = p.x - game.player.x;
    const dy = p.y - game.player.y;
    let angle = Math.atan2(dy, dx) - game.player.angle;
    while (angle < -Math.PI) angle += 2 * Math.PI;
    while (angle > Math.PI) angle -= 2 * Math.PI;
    
    if (Math.abs(angle) > HALF_FOV + 0.3) continue;
    
    const dist = Math.sqrt(dx * dx + dy * dy);
    if (dist < 0.1) continue;
    
    const screenX = SCREEN_W / 2 + (angle / HALF_FOV) * (SCREEN_W / 2);
    const size = Math.max(1, (SCREEN_H / dist) * p.size * 0.05);
    
    ctx.globalAlpha = p.life;
    ctx.fillStyle = p.color;
    ctx.fillRect(screenX - size/2, SCREEN_H/2 - size/2 + (p.vy || 0), size, size);
  }
  
  ctx.globalAlpha = 1;
}

// ── function: spawnParticles ──
function spawnParticles(game, x, y, color, count) {
  for (let i = 0; i < count; i++) {
    game.particles.push({
      x: x + (Math.random() - 0.5) * 0.3,
      y: y + (Math.random() - 0.5) * 0.3,
      vx: (Math.random() - 0.5) * 2,
      vy: Math.random() * -2,
      size: Math.random() * 3 + 1,
      color: color,
      life: 1,
      decay: 0.02 + Math.random() * 0.03
    });
  }
}

// ── function: updateParticles ──
function updateParticles(game) {
  for (let i = game.particles.length - 1; i >= 0; i--) {
    const p = game.particles[i];
    p.x += p.vx * 0.016;
    p.y += p.vy * 0.016;
    p.vy -= 5 * 0.016; // gravity
    p.life -= p.decay;
    if (p.life <= 0) game.particles.splice(i, 1);
  }
}

// ── function: renderDamageOverlay ──
function renderDamageOverlay(game) {
  const ctx = game.ctx;
  if (game.player.hurtFlash > 0) {
    ctx.fillStyle = `rgba(255,0,0,${game.player.hurtFlash * 0.4})`;
    ctx.fillRect(0, 0, SCREEN_W, SCREEN_H);
    
    // Vignette effect
    const grad = ctx.createRadialGradient(SCREEN_W/2, SCREEN_H/2, SCREEN_W*0.3, SCREEN_W/2, SCREEN_H/2, SCREEN_W*0.7);
    grad.addColorStop(0, 'rgba(255,0,0,0)');
    grad.addColorStop(1, `rgba(255,0,0,${game.player.hurtFlash * 0.6})`);
    ctx.fillStyle = grad;
    ctx.fillRect(0, 0, SCREEN_W, SCREEN_H);
  }
}

// ── function: renderMinimap ──
function renderMinimap(game) {
  const ctx = game.minimapCtx;
  const scale = MINIMAP_SIZE / Math.max(game.map.length, game.map[0].length);
  
  ctx.fillStyle = 'rgba(0,0,0,0.8)';
  ctx.fillRect(0, 0, MINIMAP_SIZE, MINIMAP_SIZE);
  
  // Draw map
  for (let y = 0; y < game.map.length; y++) {
    for (let x = 0; x < game.map[y].length; x++) {
      if (game.map[y][x] > 0) {
        const wc = WALL_COLORS[game.map[y][x]];
        ctx.fillStyle = `rgb(${wc.r},${wc.g},${wc.b})`;
        ctx.fillRect(x * scale, y * scale, scale + 1, scale + 1);
      }
    }
  }
  
  // Draw enemies
  for (const e of game.enemies) {
    if (e.state === 'dying') continue;
    ctx.fillStyle = '#ff0000';
    ctx.fillRect(e.x * scale - 2, e.y * scale - 2, 4, 4);
  }
  
  // Draw pickups
  for (const p of game.pickups) {
    if (!p.active) continue;
    ctx.fillStyle = p.type === 'health' ? '#00ff00' : '#ffaa00';
    ctx.fillRect(p.x * scale - 2, p.y * scale - 2, 4, 4);
  }
  
  // Draw exit
  if (game.exitPos) {
    ctx.fillStyle = '#00ffff';
    ctx.fillRect(game.exitPos.x * scale - 3, game.exitPos.y * scale - 3, 6, 6);
  }
  
  // Draw player
  const px = game.player.x * scale;
  const py = game.player.y * scale;
  ctx.fillStyle = '#00ff00';
  ctx.beginPath();
  ctx.arc(px, py, 3, 0, Math.PI * 2);
  ctx.fill();
  
  // Player direction
  ctx.strokeStyle = '#00ff00';
  ctx.lineWidth = 1;
  ctx.beginPath();
  ctx.moveTo(px, py);
  ctx.lineTo(px + Math.cos(game.player.angle) * 8, py + Math.sin(game.player.angle) * 8);
  ctx.stroke();
}

// ── function: isWall ──
function isWall(game, x, y) {
  const mx = Math.floor(x);
  const my = Math.floor(y);
  if (my < 0 || my >= game.map.length || mx < 0 || mx >= game.map[0].length) return true;
  return game.map[my][mx] > 0;
}

// ── function: canMove ──
function canMove(game, x, y, radius) {
  radius = radius || 0.2;
  return !isWall(game, x - radius, y - radius) &&
         !isWall(game, x + radius, y - radius) &&
         !isWall(game, x - radius, y + radius) &&
         !isWall(game, x + radius, y + radius);
}

// ── function: lineOfSight ──
function lineOfSight(game, 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 * 5);
  
  for (let i = 0; i <= steps; i++) {
    const t = i / steps;
    const x = x1 + dx * t;
    const y = y1 + dy * t;
    if (isWall(game, x, y)) return false;
  }
  return true;
}

// ── function: updatePlayer ──
function updatePlayer(game, dt) {
  const p = game.player;
  let moving = false;
  
  // Rotation from keys
  if (game.keys['ArrowLeft'] || game.keys['a']) {
    p.angle -= ROT_SPEED * dt;
  }
  if (game.keys['ArrowRight'] || game.keys['d']) {
    p.angle += ROT_SPEED * dt;
  }
  
  // Movement
  let moveX = 0, moveY = 0;
  const cos = Math.cos(p.angle);
  const sin = Math.sin(p.angle);
  
  if (game.keys['ArrowUp'] || game.keys['w']) {
    moveX += cos * MOVE_SPEED * dt;
    moveY += sin * MOVE_SPEED * dt;
    moving = true;
  }
  if (game.keys['ArrowDown'] || game.keys['s']) {
    moveX -= cos * MOVE_SPEED * dt;
    moveY -= sin * MOVE_SPEED * dt;
    moving = true;
  }
  
  // Strafe
  if (game.keys['q']) {
    moveX += sin * MOVE_SPEED * dt * 0.7;
    moveY -= cos * MOVE_SPEED * dt * 0.7;
    moving = true;
  }
  if (game.keys['e']) {
    moveX -= sin * MOVE_SPEED * dt * 0.7;
    moveY += cos * MOVE_SPEED * dt * 0.7;
    moving = true;
  }
  
  // Collision with sliding
  const newX = p.x + moveX;
  const newY = p.y + moveY;
  
  if (canMove(game, newX, p.y)) p.x = newX;
  if (canMove(game, p.x, newY)) p.y = newY;
  
  // Head bob
  if (moving) {
    p.bobPhase += dt * 10;
  } else {
    p.bobPhase *= 0.95;
  }
  
  // Hurt flash decay
  if (p.hurtFlash > 0) p.hurtFlash -= dt * 3;
  
  // Shoot timer decay
  if (p.shootTimer > 0) p.shootTimer -= dt * 2;
}

// ── function: shootWeapon ──
function shootWeapon(game) {
  const now = performance.now();
  const w = game.getWeapon();
  const p = game.player;
  
  if (now - p.lastShot < w.fireRate) return;
  if (p.ammo[p.weaponIndex] < w.ammoCost) return;
  
  p.lastShot = now;
  p.ammo[p.weaponIndex] -= w.ammoCost;
  p.shootTimer = 1.0;
  
  // Play sound effect via oscillator
  playSound(w.name === 'PLASMA' ? 800 : w.name === 'SHOTGUN' ? 150 : 400, 
            w.name === 'SHOTGUN' ? 200 : 100);
  
  for (let i = 0; i < w.pellets; i++) {
    const spreadAngle = p.angle + (Math.random() - 0.5) * w.spread;
    
    // Raycast to find hit
    let closestEnemy = null;
    let closestDist = w.range;
    
    for (const enemy of game.enemies) {
      if (enemy.state === 'dying') continue;
      
      const dx = enemy.x - p.x;
      const dy = enemy.y - p.y;
      const dist = Math.sqrt(dx * dx + dy * dy);
      
      if (dist > closestDist) continue;
      
      let angleToEnemy = Math.atan2(dy, dx) - spreadAngle;
      while (angleToEnemy < -Math.PI) angleToEnemy += 2 * Math.PI;
      while (angleToEnemy > Math.PI) angleToEnemy -= 2 * Math.PI;
      
      const hitWidth = enemy.size / dist;
      if (Math.abs(angleToEnemy) < hitWidth && lineOfSight(game, p.x, p.y, enemy.x, enemy.y)) {
        closestDist = dist;
        closestEnemy = enemy;
      }
    }
    
    if (closestEnemy) {
      const damageReduction = 1 - (closestDist / w.range) * 0.5;
      closestEnemy.hp -= w.damage * damageReduction;
      closestEnemy.hitFlash = 1;
      
      spawnParticles(game, closestEnemy.x, closestEnemy.y, '#ff4444', 3);
      
      if (closestEnemy.hp <= 0) {
        closestEnemy.state = 'dying';
        closestEnemy.deathTimer = 0;
        game.score += closestEnemy.type === 'heavy' ? 200 : 
                      closestEnemy.type === 'demon' ? 150 : 100;
        spawnParticles(game, closestEnemy.x, closestEnemy.y, '#ff8800', 10);
      } else {
        // Alert nearby enemies
        for (const e of game.enemies) {
          if (e.state === 'idle') {
            const d = Math.sqrt((e.x - p.x) ** 2 + (e.y - p.y) ** 2);
            if (d < 10) e.state = 'chasing';
          }
        }
      }
    } else {
      // Wall hit particles
      const ray = castRay(game, spreadAngle);
      if (ray.hit && ray.dist < w.range) {
        const hx = p.x + Math.cos(spreadAngle) * ray.dist;
        const hy = p.y + Math.sin(spreadAngle) * ray.dist;
        spawnParticles(game, hx, hy, '#ffaa00', 2);
      }
    }
  }
}

// ── function: updateEnemies ──
function updateEnemies(game, dt) {
  const now = performance.now();
  
  for (const enemy of game.enemies) {
    if (enemy.state === 'dying') {
      enemy.deathTimer += dt;
      continue;
    }
    
    if (enemy.hitFlash > 0) enemy.hitFlash -= dt * 3;
    
    const dx = game.player.x - enemy.x;
    const dy = game.player.y - enemy.y;
    const dist = Math.sqrt(dx * dx + dy * dy);
    
    // State machine
    if (enemy.state === 'idle') {
      // Random patrol movement
      if (Math.random() < 0.01) enemy.moveAngle += (Math.random() - 0.5) * Math.PI;
      
      const nx = enemy.x + Math.cos(enemy.moveAngle) * enemy.speed * dt * 0.3;
      const ny = enemy.y + Math.sin(enemy.moveAngle) * enemy.speed * dt * 0.3;
      if (canMove(game, nx, ny)) {
        enemy.x = nx;
        enemy.y = ny;
      } else {
        enemy.moveAngle += Math.PI;
      }
      
      // Alert on proximity or line of sight
      if (dist < enemy.alertRange && lineOfSight(game, enemy.x, enemy.y, game.player.x, game.player.y)) {
        enemy.state = 'chasing';
      }
    } else if (enemy.state === 'chasing') {
      // Move toward player
      const angleToPlayer = Math.atan2(dy, dx);
      
      if (dist > enemy.attackRange * 0.8) {
        const nx = enemy.x + Math.cos(angleToPlayer) * enemy.speed * dt;
        const ny = enemy.y + Math.sin(angleToPlayer) * enemy.speed * dt;
        
        // Simple pathfinding: try direct, then slide along walls
        if (canMove(game, nx, ny)) {
          enemy.x = nx;
          enemy.y = ny;
        } else if (canMove(game, nx, enemy.y)) {
          enemy.x = nx;
        } else if (canMove(game, enemy.x, ny)) {
          enemy.y = ny;
        }
      }
      
      // Attack when in range
      if (dist < enemy.attackRange && lineOfSight(game, enemy.x, enemy.y, game.player.x, game.player.y)) {
        if (now - enemy.lastAttack > enemy.attackRate) {
          enemy.lastAttack = now;
          game.player.health -= enemy.damage;
          game.player.hurtFlash = 1;
          
          // Alert other enemies too
          for (const e of game.enemies) {
            if (e.state === 'idle') {
              const d = Math.sqrt((e.x - game.player.x) ** 2 + (e.y - game.player.y) ** 2);
              if (d < 10) e.state = 'chasing';
            }
          }
        }
      }
    }
    
    // Check if player is still in range for chasing enemies
    if (enemy.state === 'chasing' && dist > enemy.alertRange * 2) {
      enemy.state = 'idle';
    }
  }
}

// ── function: updatePickups ──
function updatePickups(game, dt) {
  const p = game.player;
  
  for (const pickup of game.pickups) {
    if (!pickup.active) continue;
    
    pickup.bobPhase += dt * 3;
    
    const dx = p.x - pickup.x;
    const dy = p.y - pickup.y;
    const dist = Math.sqrt(dx * dx + dy * dy);
    
    if (dist < 0.5) {
      if (pickup.type === 'health') {
        p.health = Math.min(p.maxHealth, p.health + pickup.amount);
        playSound(600, 150);
      } else if (pickup.type === 'ammo') {
        // Give ammo to all weapons proportionally
        for (let i = 0; i < p.ammo.length; i++) {
          p.ammo[i] += Math.ceil(pickup.amount / 3);
        }
        playSound(500, 100);
      }
      pickup.active = false;
    }
  }
}

// ── function: showMessage ──
function showMessage(text, color, duration) {
  const overlay = document.getElementById('message-overlay');
  overlay.innerHTML = `<div class="msg-text" style="color:${color || '#fff'}">${text.replace(/\n/g, '<br>')}</div>`;
  overlay.classList.add('visible');
  
  if (duration < 90000) {
    setTimeout(() => overlay.classList.remove('visible'), duration);
  }
}

// ── let: audioCtx ──
// Simple sound synthesis
let audioCtx = null;

// ── function: playSound ──
function playSound(freq, duration) {
  try {
    if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
    const osc = audioCtx.createOscillator();
    const gain = audioCtx.createGain();
    osc.connect(gain);
    gain.connect(audioCtx.destination);
    osc.frequency.value = freq;
    osc.type = 'square';
    gain.gain.setValueAtTime(0.1, audioCtx.currentTime);
    gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + duration / 1000);
    osc.start();
    osc.stop(audioCtx.currentTime + duration / 1000);
  } catch(e) {}
}

// ── let: game ──
let game;

// ── function: gameLoop ──
function gameLoop() {
  const now = performance.now();
  const dt = Math.min((now - game.lastTime) / 1000, 0.05); // Cap delta time
  game.lastTime = now;
  
  if (game.gameState === 'playing') {
    updatePlayer(game, dt);
    updateEnemies(game, dt);
    updatePickups(game, dt);
    updateParticles(game);
    
    // Check player death
    if (game.player.health <= 0) {
      game.player.health = 0;
      handlePlayerDeath(game);
    }
    
    checkExit(game);
    updateHUD(game);
  }
  
  // Render
  renderWalls(game);
  renderSprites(game);
  renderParticles(game);
  renderWeapon(game);
  renderDamageOverlay(game);
  renderMinimap(game);
  
  requestAnimationFrame(gameLoop);
}

// ── function: checkExit ──
function checkExit(game) {
  if (!game.exitPos) return;
  
  const dx = game.player.x - game.exitPos.x;
  const dy = game.player.y - game.exitPos.y;
  const dist = Math.sqrt(dx * dx + dy * dy);
  
  if (dist < 0.8) {
    const aliveEnemies = game.enemies.filter(e => e.state !== 'dying');
    if (aliveEnemies.length === 0) {
      game.score += 500; // Level bonus
      
      if (game.currentLevel + 1 >= LEVELS.length) {
        // Game complete!
        game.gameState = 'won';
        showMessage(`🏆 VICTORY!\nFinal Score: ${game.score}\nPress R to play again`, '#ffdd00', 99999);
      } else {
        // Next level
        game.currentLevel++;
        showMessage(`LEVEL COMPLETE! +500\nScore: ${game.score}`, '#00ffaa', 2500);
        
        setTimeout(() => {
          game.loadLevel(game.currentLevel);
          document.getElementById('level-display').textContent = `LEVEL ${game.currentLevel + 1}`;
          updateHUD(game);
        }, 2500);
      }
    } else {
      showMessage(`KILL ALL ENEMIES FIRST! (${aliveEnemies.length} remaining)`, '#ff4444', 1500);
    }
  }
}

// ── function: renderWalls ──
function renderWalls(game) {
  const ctx = game.ctx;
  const stripWidth = SCREEN_W / NUM_RAYS;
  
  // Draw ceiling gradient
  const ceilGrad = ctx.createLinearGradient(0, 0, 0, SCREEN_H / 2);
  ceilGrad.addColorStop(0, '#0a0515');
  ceilGrad.addColorStop(1, '#1a1030');
  ctx.fillStyle = ceilGrad;
  ctx.fillRect(0, 0, SCREEN_W, SCREEN_H / 2);
  
  // Draw floor gradient
  const floorGrad = ctx.createLinearGradient(0, SCREEN_H / 2, 0, SCREEN_H);
  floorGrad.addColorStop(0, '#1a1510');
  floorGrad.addColorStop(1, '#332a20');
  ctx.fillStyle = floorGrad;
  ctx.fillRect(0, SCREEN_H / 2, SCREEN_W, SCREEN_H / 2);
  
  // Floor grid lines for depth perception
  ctx.strokeStyle = 'rgba(60,50,40,0.3)';
  ctx.lineWidth = 1;
  const floorStartY = SCREEN_H / 2;
  for (let d = 1; d <= MAX_DEPTH; d++) {
    const y = floorStartY + (SCREEN_H / 2) * (d / MAX_DEPTH);
    if (y > SCREEN_H) break;
    ctx.beginPath();
    ctx.moveTo(0, y);
    ctx.lineTo(SCREEN_W, y);
    ctx.stroke();
  }
  
  for (let i = 0; i < NUM_RAYS; i++) {
    const rayAngle = game.player.angle - HALF_FOV + (i / NUM_RAYS) * FOV;
    const ray = castRay(game, rayAngle);
    
    // Fix fisheye
    const perpDist = ray.dist * Math.cos(rayAngle - game.player.angle);
    game.zBuffer[i] = perpDist;
    
    if (!ray.hit || perpDist < 0.1) continue;
    
    const lineHeight = Math.min(SCREEN_H * 2, SCREEN_H / perpDist);
    const drawStart = (SCREEN_H - lineHeight) / 2;
    const drawEnd = drawStart + lineHeight;
    
    // Wall color with shading
    const wc = WALL_COLORS[ray.wallType] || { r: 128, g: 128, b: 128 };
    let shade = Math.max(0.15, 1 - perpDist / MAX_DEPTH);
    if (ray.side === 1) shade *= 0.7;
    
    // Brick/stone pattern using wallX
    const brickShade = (Math.floor(ray.wallX * 8) % 2 === 0) ? 1 : 0.92;
    shade *= brickShade;
    
    const r = Math.floor(wc.r * shade);
    const g = Math.floor(wc.g * shade);
    const b = Math.floor(wc.b * shade);
    
    // Main wall strip
    ctx.fillStyle = `rgb(${r},${g},${b})`;
    ctx.fillRect(Math.floor(i * stripWidth), drawStart, Math.ceil(stripWidth) + 1, lineHeight);
    
    // Horizontal mortar lines (every ~8th pixel of the wall height maps to a brick row)
    const brickHeightPx = lineHeight / 8;
    if (brickHeightPx > 2) {
      ctx.fillStyle = `rgba(0,0,0,${shade * 0.3})`;
      for (let bh = 1; bh < 8; bh++) {
        const brickY = drawStart + bh * brickHeightPx;
        if (brickY > SCREEN_H) break;
        ctx.fillRect(Math.floor(i * stripWidth), brickY, Math.ceil(stripWidth) + 1, Math.max(1, brickHeightPx * 0.15));
      }
    }
    
    // Top edge highlight
    ctx.fillStyle = `rgba(255,255,255,${shade * 0.08})`;
    ctx.fillRect(Math.floor(i * stripWidth), drawStart, Math.ceil(stripWidth) + 1, 2);
    
    // Bottom shadow
    ctx.fillStyle = `rgba(0,0,0,${Math.min(shade * 0.4, 0.5)})`;
    ctx.fillRect(Math.floor(i * stripWidth), drawEnd - 3, Math.ceil(stripWidth) + 1, 3);
    
    // Floor/wall junction shadow line
    if (drawEnd < SCREEN_H) {
      ctx.fillStyle = `rgba(0,0,0,${shade * 0.2})`;
      ctx.fillRect(Math.floor(i * stripWidth), drawEnd, Math.ceil(stripWidth) + 1, 4);
    }
  }
  
  // Ambient fog overlay at edges of screen
  const fogGrad = ctx.createRadialGradient(SCREEN_W/2, SCREEN_H/2, SCREEN_W*0.3, SCREEN_W/2, SCREEN_H/2, SCREEN_W*0.75);
  fogGrad.addColorStop(0, 'rgba(0,0,0,0)');
  fogGrad.addColorStop(1, 'rgba(0,0,0,0.4)');
  ctx.fillStyle = fogGrad;
  ctx.fillRect(0, 0, SCREEN_W, SCREEN_H);
}

// ── function: handlePlayerDeath ──
function handlePlayerDeath(game) {
  game.lives--;
  
  if (game.lives <= 0) {
    game.gameState = 'gameover';
    showMessage(`GAME OVER\nFinal Score: ${game.score}\nPress R to restart`, '#ff0000', 99999);
  } else {
    // Respawn at start position
    const level = LEVELS[game.currentLevel];
    game.player.health = Math.floor(game.player.maxHealth * 0.5);
    game.player.x = level.playerStart.x;
    game.player.y = level.playerStart.y;
    game.player.angle = level.playerStart.angle;
    
    // Restore some ammo
    for (let i = 0; i < game.player.ammo.length; i++) {
      game.player.ammo[i] += 5;
    }
    
    showMessage(`YOU DIED! Lives: ${game.lives}\nHealth & ammo restored`, '#ff4444', 2500);
    
    // Push enemies away from spawn point to prevent instant death
    for (const enemy of game.enemies) {
      if (enemy.state === 'dying') continue;
      const dx = enemy.x - game.player.x;
      const dy = enemy.y - game.player.y;
      const dist = Math.sqrt(dx * dx + dy * dy);
      if (dist < 3) {
        enemy.x += (dx / dist) * 4;
        enemy.y += (dy / dist) * 4;
        // Clamp to map bounds
        enemy.x = Math.max(1, Math.min(game.map[0].length - 2, enemy.x));
        enemy.y = Math.max(1, Math.min(game.map.length - 2, enemy.y));
      }
    }
    
    updateHUD(game);
  }
}

// ── class: GameState ──
class GameState {
  constructor() {
    this.canvas = document.getElementById('game');
    this.ctx = this.canvas.getContext('2d');
    this.minimapCanvas = document.getElementById('minimap');
    this.minimapCtx = this.minimapCanvas.getContext('2d');
    
    this.canvas.width = SCREEN_W;
    this.canvas.height = SCREEN_H;
    this.minimapCanvas.width = MINIMAP_SIZE;
    this.minimapCanvas.height = MINIMAP_SIZE;
    
    this.currentLevel = 0;
    this.score = 0;
    this.lives = 3;
    this.gameState = 'playing'; // playing, dead, won, gameover
    
    this.player = {
      x: 2.5, y: 2.5, angle: 0,
      health: 100, maxHealth: 100,
      ammo: [30, 8, 4], // pistol, shotgun, plasma
      weaponIndex: 0,
      lastShot: 0,
      bobPhase: 0,
      shooting: false,
      shootTimer: 0,
      hurtFlash: 0
    };
    
    this.enemies = [];
    this.pickups = [];
    this.particles = [];
    this.projectiles = [];
    this.map = [];
    this.exitPos = null;
    
    this.keys = {};
    this.mouseX = 0;
    this.lastTime = performance.now();
    
    this.zBuffer = new Float64Array(SCREEN_W);
    
    this.loadLevel(this.currentLevel);
  }
  
  loadLevel(levelIdx) {
    const level = LEVELS[levelIdx];
    this.map = level.map;
    this.player.x = level.playerStart.x;
    this.player.y = level.playerStart.y;
    this.player.angle = level.playerStart.angle;
    
    this.enemies = level.enemies.map(e => ({
      x: e.x, y: e.y, type: e.type,
      hp: ENEMY_TYPES[e.type].hp,
      maxHp: ENEMY_TYPES[e.type].hp,
      speed: ENEMY_TYPES[e.type].speed,
      damage: ENEMY_TYPES[e.type].damage,
      attackRange: ENEMY_TYPES[e.type].attackRange,
      attackRate: ENEMY_TYPES[e.type].attackRate,
      color: ENEMY_TYPES[e.type].color,
      size: ENEMY_TYPES[e.type].size,
      lastAttack: 0,
      state: 'idle', // idle, chasing, attacking, dying
      deathTimer: 0,
      alertRange: 8,
      moveAngle: Math.random() * Math.PI * 2,
      hitFlash: 0
    }));
    
    this.pickups = level.pickups.map(p => ({
      x: p.x, y: p.y, type: p.type, amount: p.amount, active: true, bobPhase: Math.random() * Math.PI * 2
    }));
    
    this.exitPos = { ...level.exitPos };
    this.particles = [];
    this.projectiles = [];
    this.gameState = 'playing';
    
    // Update level display
    document.getElementById('level-display').textContent = `LEVEL ${this.currentLevel + 1}`;
  }
  
  getWeapon() { return WEAPONS[this.getWeaponKey()]; }
  getWeaponKey() { 
    const keys = Object.keys(WEAPONS);
    return keys[this.player.weaponIndex % keys.length];
  }
}

// ── function: initGame ──
function initGame() {
  game = new GameState();
  
  // Input handling
  document.addEventListener('keydown', (e) => {
    game.keys[e.key] = true;
    
    if (game.gameState === 'playing') {
      // Weapon switching
      if (e.key === '1') game.player.weaponIndex = 0;
      if (e.key === '2') game.player.weaponIndex = 1;
      if (e.key === '3') game.player.weaponIndex = 2;
      
      // Shoot
      if (e.key === ' ') {
        e.preventDefault();
        shootWeapon(game);
      }
    }
    
    // Restart
    if ((e.key === 'r' || e.key === 'R')) {
      if (game.gameState === 'gameover' || game.gameState === 'won') {
        game.lives = 3;
        game.score = 0;
        game.currentLevel = 0;
        game.loadLevel(0);
        document.getElementById('message-overlay').classList.remove('visible');
        updateHUD(game);
      }
    }
    
    updateHUD(game);
  });
  
  document.addEventListener('keyup', (e) => {
    game.keys[e.key] = false;
  });
  
  // Mouse look
  const canvas = document.getElementById('game');
  canvas.addEventListener('click', () => {
    if (game.gameState === 'playing') {
      canvas.requestPointerLock();
    }
  });
  
  document.addEventListener('mousemove', (e) => {
    if (document.pointerLockElement === canvas && game.gameState === 'playing') {
      game.player.angle += e.movementX * 0.003;
    }
  });
  
  // Mouse click to shoot
  canvas.addEventListener('mousedown', (e) => {
    if (game.gameState === 'playing' && document.pointerLockElement === canvas) {
      shootWeapon(game);
    }
  });
  
  // Scroll to switch weapons
  canvas.addEventListener('wheel', (e) => {
    if (game.gameState !== 'playing') return;
    e.preventDefault();
    const weaponKeys = Object.keys(WEAPONS);
    if (e.deltaY > 0) {
      game.player.weaponIndex = (game.player.weaponIndex + 1) % weaponKeys.length;
    } else {
      game.player.weaponIndex = (game.player.weaponIndex - 1 + weaponKeys.length) % weaponKeys.length;
    }
    updateHUD(game);
  }, { passive: false });
  
  // Start intro message
  showMessage('DOOMED CORRIDORS\nClick to lock mouse • WASD move • Mouse aim\nSpace/Click shoot • 1-2-3 weapons', '#ffffff', 4000);
  
  updateHUD(game);
  gameLoop();
}

// ── const: LEVELS ──
const LEVELS = [
  {
    name: "THE LABYRINTH",
    map: [
      [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
      [1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,1],
      [1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,1],
      [1,0,2,2,0,0,0,0,0,3,3,3,0,0,0,0,0,1],
      [1,0,2,0,0,0,0,0,0,0,0,3,0,0,0,0,0,1],
      [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
      [1,1,1,0,0,0,1,1,0,1,1,0,0,1,1,1,1,1],
      [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
      [1,0,0,3,0,0,0,0,0,0,0,0,2,0,0,0,0,1],
      [1,0,0,3,3,0,0,0,0,0,0,2,2,0,0,0,0,1],
      [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
      [1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,1],
      [1,0,0,0,0,0,1,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]
    ],
    playerStart: { x: 2.5, y: 2.5, angle: Math.PI / 4 },
    enemies: [
      { x: 3.5, y: 4.5, type: 'soldier' },   // visible from spawn through open corridor
      { x: 8.5, y: 3.5, type: 'soldier' },
      { x: 12.5, y: 2.5, type: 'heavy' },
      { x: 4.5, y: 9.5, type: 'demon' },
      { x: 10.5, y: 8.5, type: 'soldier' },
      { x: 6.5, y: 7.5, type: 'heavy' },
    ],
    pickups: [
      { x: 4.5, y: 2.5, type: 'ammo', amount: 10 },
      { x: 13.5, y: 5.5, type: 'health', amount: 25 },
      { x: 7.5, y: 10.5, type: 'ammo', amount: 8 },
      { x: 2.5, y: 9.5, type: 'health', amount: 30 },
    ],
    exitPos: { x: 14.5, y: 12.5 }
  },
  {
    name: "THE FORTRESS",
    map: [
      [3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3],
      [3,0,0,0,2,0,0,0,0,1,0,0,0,0,2,0,0,3],
      [3,0,0,0,2,0,0,0,0,1,0,0,0,0,2,0,0,3],
      [3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3],
      [3,2,2,0,0,0,1,1,1,1,1,1,0,0,0,2,2,3],
      [3,0,0,0,0,0,1,0,0,0,0,1,0,0,0,0,0,3],
      [3,0,0,0,0,0,1,0,0,0,0,1,0,0,0,0,0,3],
      [3,0,0,2,0,0,0,0,0,0,0,0,0,0,2,0,0,3],
      [3,0,0,2,0,0,1,0,0,0,0,1,0,0,2,0,0,3],
      [3,0,0,0,0,0,1,0,0,0,0,1,0,0,0,0,0,3],
      [3,0,0,0,0,0,1,0,0,0,0,1,0,0,0,0,0,3],
      [3,2,2,0,0,0,0,0,0,0,0,0,0,0,0,2,2,3],
      [3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3],
      [3,0,0,0,2,0,0,0,0,1,0,0,0,0,2,0,0,3],
      [3,0,0,0,2,0,0,0,0,1,0,0,0,0,2,0,0,3],
      [3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3]
    ],
    playerStart: { x: 2.5, y: 2.5, angle: Math.PI / 4 },
    enemies: [
      { x: 3.5, y: 3.5, type: 'soldier' },   // visible from spawn
      { x: 9.5, y: 2.5, type: 'heavy' },
      { x: 14.5, y: 3.5, type: 'demon' },
      { x: 5.5, y: 7.5, type: 'soldier' },
      { x: 12.5, y: 8.5, type: 'heavy' },
      { x: 9.5, y: 6.5, type: 'demon' },
      { x: 3.5, y: 10.5, type: 'soldier' },
      { x: 14.5, y: 12.5, type: 'heavy' },
    ],
    pickups: [
      { x: 7.5, y: 3.5, type: 'ammo', amount: 12 },
      { x: 14.5, y: 6.5, type: 'health', amount: 30 },
      { x: 2.5, y: 8.5, type: 'ammo', amount: 10 },
      { x: 9.5, y: 9.5, type: 'health', amount: 40 },
      { x: 13.5, y: 12.5, type: 'ammo', amount: 8 },
    ],
    exitPos: { x: 14.5, y: 14.5 }
  }
];

// ── function: updateHUD ──
function updateHUD(game) {
  const p = game.player;
  document.getElementById('health-fill').style.width = `${(p.health / p.maxHealth) * 100}%`;
  
  if (p.health > 60) {
    document.getElementById('health-fill').style.background = 'linear-gradient(to right, #44cc44, #22aa22)';
  } else if (p.health > 30) {
    document.getElementById('health-fill').style.background = 'linear-gradient(to right, #cccc00, #aaaa00)';
  } else {
    document.getElementById('health-fill').style.background = 'linear-gradient(to right, #ff4444, #cc2222)';
  }
  
  const wKey = p.weaponIndex;
  document.getElementById('ammo-count').textContent = p.ammo[wKey];
  document.getElementById('weapon-name').textContent = game.getWeapon().name;
  document.getElementById('score-count').textContent = game.score;
  document.getElementById('lives-count').textContent = '♥'.repeat(Math.max(0, game.lives));
}

// ── main ──
initGame();
</script>
</body>
</html>
<!-- agent-meta {"model":"qwen3.6-27b-mtp","provider":"lmstudio","persona":"composer","sessionId":"a840b232-e433-4575-afd5-0181351d7d8c","tokensIn":1497411,"tokensOut":30422,"tokensTotal":1527833,"turns":55,"toolCalls":54,"failedToolCalls":1,"timestamp":"2026-07-16T04:55:17.340Z"} -->