← DOOM results

DOOM

ling 3.0 tiny:free · full

Initial view of DOOM

Browser check: Artifact runtime error. This is a technical check, not a quality review.

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 31,826 bytes · SHA-256 a0ca1a208990
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>RETRO SHOOTER</title>
<style>
  * { margin: 0; padding: 0; box-sizing: border-box; }
  body {
    background: #000;
    display: flex;
    justify-content: center;
    align-items: center;
    height: 100vh;
    overflow: hidden;
    font-family: monospace;
  }
  canvas {
    image-rendering: pixelated;
    image-rendering: crisp-edges;
    border: 3px solid #2a2a2a;
  }
  #gameContainer {
    position: relative;
  }
</style>
</head>
<body>
<div id="gameContainer">
  <canvas id="gameCanvas" width="800" height="600"></canvas>
</div>
<script>
// ============================================
// RETRO SHOOTER - A Retro FPS Game
// ============================================

const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const W = 800, H = 600;

// ---- Audio ----
const AudioCtx = window.AudioContext || window.webkitAudioContext;
let audioCtx;

function initAudio() {
  if (!audioCtx) audioCtx = new AudioCtx();
}

function playSound(type) {
  if (!audioCtx) return;
  const now = audioCtx.currentTime;
  const osc = audioCtx.createOscillator();
  const gain = audioCtx.createGain();
  osc.connect(gain);
  gain.connect(audioCtx.destination);

  switch(type) {
    case 'shoot':
      osc.type = 'square';
      osc.frequency.setValueAtTime(800, now);
      osc.frequency.exponentialRampToValueAtTime(200, now + 0.08);
      gain.gain.setValueAtTime(0.08, now);
      gain.gain.exponentialRampToValueAtTime(0.001, now + 0.08);
      osc.start(now);
      osc.stop(now + 0.08);
      break;
    case 'hit':
      osc.type = 'sawtooth';
      osc.frequency.setValueAtTime(200, now);
      osc.frequency.exponentialRampToValueAtTime(80, now + 0.1);
      gain.gain.setValueAtTime(0.12, now);
      gain.gain.exponentialRampToValueAtTime(0.001, now + 0.1);
      osc.start(now);
      osc.stop(now + 0.1);
      break;
    case 'pickup':
      osc.type = 'sine';
      osc.frequency.setValueAtTime(600, now);
      osc.frequency.exponentialRampToValueAtTime(1200, now + 0.15);
      gain.gain.setValueAtTime(0.06, 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(400, now);
      osc.frequency.exponentialRampToValueAtTime(50, now + 0.4);
      gain.gain.setValueAtTime(0.15, now);
      gain.gain.exponentialRampToValueAtTime(0.001, now + 0.4);
      osc.start(now);
      osc.stop(now + 0.4);
      break;
    case 'win':
      osc.type = 'sine';
      osc.frequency.setValueAtTime(400, now);
      osc.frequency.setValueAtTime(500, now + 0.1);
      osc.frequency.setValueAtTime(600, now + 0.2);
      osc.frequency.setValueAtTime(800, now + 0.3);
      gain.gain.setValueAtTime(0.08, now);
      gain.gain.exponentialRampToValueAtTime(0.001, now + 0.5);
      osc.start(now);
      osc.stop(now + 0.5);
      break;
    case 'respawn':
      osc.type = 'triangle';
      osc.frequency.setValueAtTime(300, now);
      osc.frequency.setValueAtTime(600, now + 0.1);
      gain.gain.setValueAtTime(0.06, now);
      gain.gain.exponentialRampToValueAtTime(0.001, now + 0.2);
      osc.start(now);
      osc.stop(now + 0.2);
      break;
  }
}

// ---- Game State ----
let state = 'MENU'; // MENU, PLAYING, PAUSED, DEATH, WIN
let player, enemies, bullets, pickups, particles, powerups;
let score, level, gameTime, deaths, kills;
let screenShake, shakeIntensity;
let spawnTimer, spawnInterval;
let levelEndShown, levelEndTimer;
let gameOverTimer, gameOverAlpha;
let highScore = parseInt(localStorage.getItem('retroShooterHigh') || '0');
let keys = {};
let mouse = { x: W/2, y: H/2, down: false };
let camX = 0, camY = 0;
let comboTimer, comboCount;
let lastTime = 0;
let flashAlpha = 0;
let flashColor = '#ff0000';

// ---- Weapons ----
const WEAPONS = [
  { name: 'Pistol', shots: 1, spread: 0, rate: 280, damage: 12, mag: 10, color: '#00ff41', bulletColor: '#00ff41', icon: '⚔' },
  { name: 'Shotgun', shots: 1, spread: 4, rate: 220, damage: 8, mag: 6, color: '#ff4400', bulletColor: '#ff4400', icon: '🔫' },
  { name: 'Minigun', shots: 1, spread: 0, rate: 45, damage: 6, mag: 60, color: '#00ccff', bulletColor: '#00ccff', icon: '🪓' },
];

// ---- Level Data ----
const LEVELS = [
  { name: 'Training Ground', enemyCount: 8, spawnEvery: 2000, bossHP: 80 },
  { name: 'Corridor', enemyCount: 15, spawnEvery: 1600, bossHP: 120 },
  { name: 'War Zone', enemyCount: 22, spawnEvery: 1200, bossHP: 160 },
  { name: 'The Pit', enemyCount: 30, spawnEvery: 1000, bossHP: 200 },
  { name: 'Nexus Prime', enemyCount: 40, spawnEvery: 800, bossHP: 300 },
];

// ---- Player ----
function createPlayer() {
  return {
    x: 100, y: 300,
    w: 20, h: 28,
    speed: 3.5,
    health: 100, maxHealth: 100,
    ammo: 0, currentWeapon: 0,
    bullets: [],
    invincible: 0,
    respawnTimer: 0,
    dead: false,
    lastShot: 0,
    alive: true,
  };
}

function createEnemy(x, y, type, level) {
  const config = {
    zombie: { hp: 20, speed: 1.2, size: 14, color: '#884422', score: 100 },
    mutant: { hp: 12, speed: 2.5, size: 10, color: '#8844cc', score: 150 },
    slime: { hp: 8, speed: 0.7, size: 16, color: '#44cc44', score: 80 },
    boss: { hp: 300, speed: 0.8, size: 36, color: '#ff2200', score: 1000 },
  };
  const c = config[type] || config.zombie;
  return {
    x, y,
    type,
    hp: c.hp,
    maxHp: c.hp,
    speed: c.speed + Math.random() * 0.5,
    size: c.size,
    color: c.color,
    score: c.score,
    dx: 0, dy: 0,
    shootTimer: 0,
    patrolDir: Math.random() * Math.PI * 2,
    patrolTimer: 0,
    alive: true,
    hitFlash: 0,
  };
}

function createBullet(x, y, angle, damage, color, speed, isEnemy = false) {
  return { x, y, angle, damage, color, speed, isEnemy, life: 60, trail: [] };
}

function createPickup(x, y, type) {
  return { x, y, type, pulse: 0, alive: true };
}

function createParticle(x, y, color, speed, life) {
  return {
    x, y,
    vx: (Math.random() - 0.5) * speed,
    vy: (Math.random() - 0.5) * speed,
    color,
    life,
    maxLife: life,
    size: Math.random() * 3 + 1,
  };
}

function createPowerUp(x, y, type) {
  return { x, y, type, pulse: 0, alive: true };
}

// ---- Input ----
document.addEventListener('keydown', e => {
  keys[e.key.toLowerCase()] = true;
  keys[e.code] = true;
  if (e.key === 'Escape' && state === 'PLAYING') {
    state = 'PAUSED';
  } else if (e.key === 'Escape' && state === 'PAUSED') {
    state = 'PLAYING';
  }
  if (e.key === '1') switchWeapon(0);
  if (e.key === '2') switchWeapon(1);
  if (e.key === '3') switchWeapon(2);
});
document.addEventListener('keyup', e => {
  keys[e.key.toLowerCase()] = false;
  keys[e.code] = false;
});
canvas.addEventListener('mousemove', e => {
  const rect = canvas.getBoundingClientRect();
  mouse.x = (e.clientX - rect.left) * (W / rect.width);
  mouse.y = (e.clientY - rect.top) * (H / rect.height);
});
canvas.addEventListener('mousedown', e => {
  mouse.down = true;
  initAudio();
});
canvas.addEventListener('mouseup', e => { mouse.down = false; });
canvas.addEventListener('contextmenu', e => e.preventDefault());

// ---- Weapon Switch ----
function switchWeapon(idx) {
  if (player.currentWeapon !== idx) {
    player.currentWeapon = idx;
    player.ammo = WEAPONS[idx].mag;
    playSound('pickup');
  }
}

// ---- Camera ----
function updateCamera() {
  camX += (player.x - W/2 - camX) * 0.1;
  camY += (player.y - H/2 - camY) * 0.1;
}

// ---- Spawning ----
function spawnEnemy() {
  const lvl = LEVELS[Math.min(level - 1, LEVELS.length - 1)];
  const count = lvl.enemyCount;
  const spawnEvery = lvl.spawnEvery;
  const typeRoll = Math.random();
  let type = 'zombie';
  if (typeRoll < 0.08) type = 'boss';
  else if (typeRoll < 0.2) type = 'mutant';
  else if (typeRoll < 0.35) type = 'slime';

  const side = Math.floor(Math.random() * 4);
  let x, y;
  if (side === 0) { x = player.x + 200 + Math.random() * 300; y = -20; }
  else if (side === 1) { x = -20; y = 200 + Math.random() * 300; }
  else if (side === 2) { x = player.x + 200 + Math.random() * 300; y = H + 20; }
  else { x = -20; y = 200 + Math.random() * 300; }

  const enemy = createEnemy(x, y, type, level);
  enemies.push(enemy);
}

// ---- Game Init ----
function initGame() {
  player = createPlayer();
  enemies = [];
  bullets = [];
  pickups = [];
  particles = [];
  powerups = [];
  score = 0;
  level = 1;
  gameTime = 0;
  deaths = 0;
  kills = 0;
  comboCount = 0;
  comboTimer = 0;
  screenShake = 0;
  shakeIntensity = 0;
  spawnTimer = 0;
  spawnInterval = 2000;
  levelEndShown = false;
  levelEndTimer = 0;
  gameOverTimer = 0;
  gameOverAlpha = 0;
  flashAlpha = 0;
  camX = 0;
  camY = 0;
}

function startLevel() {
  initGame();
  const lvl = LEVELS[Math.min(level - 1, LEVELS.length - 1)];
  player.x = 100;
  player.y = 300;
  player.ammo = WEAPONS[player.currentWeapon].mag;
  // Spawn initial enemies
  for (let i = 0; i < lvl.enemyCount; i++) {
    setTimeout(() => {
      const side = Math.floor(Math.random() * 4);
      let x, y;
      if (side === 0) { x = 100 + Math.random() * 200; y = -10; }
      else if (side === 1) { x = -10; y = 200 + Math.random() * 200; }
      else if (side === 2) { x = 100 + Math.random() * 200; y = H + 10; }
      else { x = -10; y = 200 + Math.random() * 200; }
      enemies.push(createEnemy(x, y, 'zombie', level));
    }, i * 400);
  }
  state = 'PLAYING';
}

// ---- Update ----
function update(dt) {
  if (state !== 'PLAYING') return;
  gameTime += dt;

  // ---- Player Movement ----
  if (!player.dead && player.alive) {
    let dx = 0, dy = 0;
    if (keys['w'] || keys['arrowup']) dy -= 1;
    if (keys['s'] || keys['arrowdown']) dy += 1;
    if (keys['a'] || keys['arrowleft']) dx -= 1;
    if (keys['d'] || keys['arrowright']) dx += 1;

    const speed = player.speed * (keys['shift'] ? 1.5 : 1);
    const len = Math.sqrt(dx*dx + dy*dy);
    if (len > 0) {
      dx /= len; dy /= len;
      player.x += dx * speed * dt;
      player.y += dy * speed * dt;
    }

    // Clamp to world bounds
    player.x = Math.max(0, Math.min(W - player.w, player.x));
    player.y = Math.max(0, Math.min(H - player.h, player.y));

    // Invincibility
    if (player.invincible > 0) player.invincible -= dt;

    // Shooting
    const weapon = WEAPONS[player.currentWeapon];
    const now = performance.now();
    if ((keys['z'] || mouse.down) && now - player.lastShot > weapon.rate) {
      player.lastShot = now;
      const spreadAngle = (Math.random() - 0.5) * weapon.spread * Math.PI / 180;
      const angle = Math.atan2(mouse.y - player.y, mouse.x - player.x) + spreadAngle;
      const bulletSpeed = weapon.rate > 100 ? 14 : 10;
      bullets.push(createBullet(
        player.x + player.w/2, player.y + player.h/2,
        angle, weapon.damage, weapon.bulletColor, bulletSpeed
      ));
      if (weapon.rate > 100 && weapon.mag > 50) {
        // Quick burst for minigun
        for (let i = 0; i < 3; i++) {
          const spreadAngle = (Math.random() - 0.5) * 0.3;
          bullets.push(createBullet(
            player.x + player.w/2, player.y + player.h/2,
            angle + spreadAngle, weapon.damage * 0.8, weapon.bulletColor, 12
          ));
        }
      }
    }
  }

  // ---- Camera ----
  updateCamera();

  // ---- Spawn Enemies ----
  spawnTimer += dt * 1000;
  if (spawnTimer >= spawnInterval) {
    spawnTimer = 0;
    spawnEnemy();
    // Also spawn ammo pickups occasionally
    if (Math.random() < 0.15) {
      pickups.push(createPickup(
        player.x + 200 + Math.random() * 200,
        player.y + 50 + Math.random() * 200,
        'ammo'
      ));
    }
    if (Math.random() < 0.1) {
      pickups.push(createPickup(
        player.x + 200 + Math.random() * 200,
        player.y + 50 + Math.random() * 200,
        'health'
      ));
    }
  }

  // ---- Update Bullets ----
  for (let i = bullets.length - 1; i >= 0; i--) {
    const b = bullets[i];
    b.x += Math.cos(b.angle) * b.speed * 60 * dt;
    b.y += Math.sin(b.angle) * b.speed * 60 * dt;
    b.life--;

    // Trail
    b.trail.push({ x: b.x, y: b.y, life: 8 });
    if (b.trail.length > 6) b.trail.shift();

    if (b.life <= 0) { bullets.splice(i, 1); continue; }

    // Collisions with enemies
    for (let j = enemies.length - 1; j >= 0; j--) {
      const e = enemies[j];
      if (!e.alive) continue;
      const dist = Math.hypot(b.x - e.x, b.y - e.y);
      if (dist < e.size + 4) {
        e.hp -= b.damage;
        e.hitFlash = 0.2;
        // Hit particles
        for (let k = 0; k < 3; k++) {
          particles.push(createParticle(b.x, b.y, '#ffffff', 60, 10));
        }
        playSound('hit');
        if (e.hp <= 0) {
          e.alive = false;
          score += e.score;
          kills++;
          comboCount++;
          comboTimer = 1.5;
          // Death particles
          for (let k = 0; k < 12; k++) {
            particles.push(createParticle(e.x, e.y, e.color, 100, 25));
          }
          playSound('hit');
          if (e.type === 'boss') {
            // Special boss death
            for (let k = 0; k < 20; k++) {
              particles.push(createParticle(e.x, e.y, '#ff0000', 150, 30));
            }
            flashAlpha = 1;
            flashColor = '#ff0000';
          }
          bullets.splice(i, 1);
          break;
        }
      }
    }

    // Collisions with pickups
    for (let j = pickups.length - 1; j >= 0; j--) {
      const p = pickups[j];
      if (!p.alive) continue;
      const dist = Math.hypot(b.x - p.x, b.y - p.y);
      if (dist < 16) {
        if (p.type === 'ammo') {
          player.ammo += 30;
          playSound('pickup');
          particles.push(createParticle(p.x, p.y, '#ffaa00', 50, 15));
        } else if (p.type === 'health') {
          player.health = Math.min(player.maxHealth, player.health + 25);
          playSound('pickup');
          particles.push(createParticle(p.x, p.y, '#44ff44', 50, 15));
        }
        pickups.splice(j, 1);
      }
    }
  }

  // ---- Update Enemies ----
  for (let i = enemies.length - 1; i >= 0; i--) {
    const e = enemies[i];
    if (!e.alive) { enemies.splice(i, 1); continue; }

    // HP reduction
    if (e.hitFlash > 0) e.hitFlash -= dt;

    // Boss behavior
    if (e.type === 'boss') {
      e.patrolTimer += dt;
      if (e.patrolTimer > 1.5) {
        e.patrolTimer = 0;
        e.patrolDir = (Math.random() - 0.5) * Math.PI * 2;
      }
      const targetX = player.x + (Math.random() - 0.5) * 100;
      const targetY = player.y + (Math.random() - 0.5) * 100;
      e.dx = targetX - e.x;
      e.dy = targetY - e.y;
      const dist = Math.hypot(e.dx, e.dy);
      if (dist > 0) {
        e.x += (e.dx / dist) * e.speed * 60 * dt;
        e.y += (e.dy / dist) * e.speed * 60 * dt;
      }
    } else {
      // Patrol or chase
      e.patrolTimer += dt;
      if (e.patrolTimer > 2.0) {
        e.patrolTimer = 0;
        e.patrolDir += (Math.random() - 0.5) * Math.PI;
      }
      e.x += Math.cos(e.patrolDir) * e.speed * 60 * dt;
      e.y += Math.sin(e.patrolDir) * e.speed * 60 * dt;
      // Move toward player when close enough
      const distToPlayer = Math.hypot(e.x - player.x, e.y - player.y);
      if (distToPlayer < 150) {
        e.dx = player.x - e.x;
        e.dy = player.y - e.y;
        const dist = Math.hypot(e.dx, e.dy);
        if (dist > 0) {
          e.x += (e.dx / dist) * e.speed * 60 * dt;
          e.y += (e.dy / dist) * e.speed * 60 * dt;
        }
      }
    }

    // Enemy shooting (boss and mutants)
    if (e.type === 'boss') {
      e.shootTimer += dt;
      if (e.shootTimer > 2.0) {
        e.shootTimer = 0;
        const angle = Math.atan2(player.y - e.y, player.x - e.x);
        bullets.push(createBullet(
          e.x - Math.cos(angle) * 16, e.y - Math.sin(angle) * 16,
          angle + (Math.random() - 0.5) * 0.2, 8, '#ff4400', 8
        ));
      }
    } else if (e.type === 'mutant') {
      e.shootTimer += dt;
      if (e.shootTimer > 1.5) {
        e.shootTimer = 0;
        const angle = Math.atan2(player.y - e.y, player.x - e.x);
        bullets.push(createBullet(
          e.x - Math.cos(angle) * 16, e.y - Math.sin(angle) * 16,
          angle + (Math.random() - 0.5) * 0.15, 6, '#ff4400', 8
        ));
      }
    }

    // Player collision
    if (!player.dead && player.alive) {
      const dist = Math.hypot(e.x - player.x, e.y - player.y);
      if (dist < e.size + player.w/2) {
        if (player.invincible <= 0) {
          player.health -= 25;
          player.invincible = 0.5;
          screenShake = 5;
          shakeIntensity = 5;
          playSound('hit');
          for (let k = 0; k < 5; k++) {
            particles.push(createParticle(player.x, player.y, '#ff0000', 80, 12));
          }
          if (player.health <= 0) {
            player.health = 0;
            player.dead = true;
            player.respawnTimer = 3;
            player.alive = false;
            deaths++;
            playSound('death');
          }
        }
      }
    }

    // Remove dead enemies
    if (!e.alive) enemies.splice(i, 1);
  }

  // ---- Update Particles ----
  for (let i = particles.length - 1; i >= 0; i--) {
    const p = particles[i];
    p.x += p.vx * dt * 60;
    p.y += p.vy * dt * 60;
    p.vx *= 0.95;
    p.vy *= 0.95;
    p.life -= dt;
    if (p.life <= 0) particles.splice(i, 1);
  }

  // ---- Update PowerUps ----
  for (let i = powerups.length - 1; i >= 0; i--) {
    const p = powerups[i];
    p.pulse += dt * 3;
    p.life -= dt;
    if (p.life <= 0) powerups.splice(i, 1);
  }

  // ---- Combo Timer ----
  if (comboTimer > 0) {
    comboTimer -= dt;
    if (comboTimer <= 0) comboCount = 0;
  }

  // ---- Level Check ----
  if (player.health > 0 && player.alive) {
    if (!levelEndShown && player.x > W - 300 && player.y > H - 300) {
      levelEndShown = true;
      levelEndTimer = 2;
      playSound('win');
    }
  }
  if (levelEndShown) {
    levelEndTimer -= dt;
    if (levelEndTimer <= 0) {
      nextLevel();
    }
  }

  // ---- Respawn ----
  if (player.dead) {
    player.respawnTimer -= dt;
    if (player.respawnTimer <= 0) {
      respawnPlayer();
    }
  }

  // ---- Screen Shake ----
  if (screenShake > 0) {
    screenShake -= dt;
    shakeIntensity *= 0.95;
  }

  // ---- Flash ----
  if (flashAlpha > 0) {
    flashAlpha -= dt * 2;
    if (flashAlpha < 0) flashAlpha = 0;
  }

  // ---- Update Player ----
  if (player.dead && player.alive) {
    player.respawnTimer -= dt;
    if (player.respawnTimer <= 0) respawnPlayer();
  }

  // ---- Check Win ----
  if (player.health <= 0 && player.dead) {
    // Already respawned
  }
}

function respawnPlayer() {
  player.x = 100;
  player.y = 300;
  player.health = 100;
  player.invincible = 3;
  player.dead = false;
  player.alive = true;
  player.ammo = WEAPONS[player.currentWeapon].mag;
  spawnTimer = 0;
  // Respawn particles
  for (let i = 0; i < 20; i++) {
    particles.push(createParticle(
      player.x + 10 + Math.random() * 5, player.y + 10 + Math.random() * 5,
      '#00ff41', 100, 15
    ));
  }
  playSound('respawn');
}

function nextLevel() {
  level++;
  startLevel();
}

// ---- Render ----
function render() {
  ctx.save();

  // Screen shake
  if (screenShake > 0) {
    const sx = (Math.random() - 0.5) * shakeIntensity * 2;
    const sy = (Math.random() - 0.5) * shakeIntensity * 2;
    ctx.translate(sx, sy);
  }

  // Background
  ctx.fillStyle = '#0a0a0a';
  ctx.fillRect(0, 0, W, H);

  // Grid
  ctx.strokeStyle = 'rgba(255,255,255,0.03)';
  ctx.lineWidth = 1;
  for (let x = 0; x < W; x += 32) {
    ctx.beginPath();
    ctx.moveTo(x, 0);
    ctx.lineTo(x, H);
    ctx.stroke();
  }
  for (let y = 0; y < H; y += 32) {
    ctx.beginPath();
    ctx.moveTo(0, y);
    ctx.lineTo(W, y);
    ctx.stroke();
  }

  // Particles (behind entities)
  for (const p of particles) {
    const alpha = p.life / p.maxLife;
    ctx.globalAlpha = alpha;
    ctx.fillStyle = p.color;
    ctx.fillRect(p.x - p.size/2, p.y - p.size/2, p.size, p.size);
  }
  ctx.globalAlpha = 1;

  // Only render game world if playing
  if (state === 'PLAYING' || state === 'DEATH' || state === 'WIN') {
    // ---- Draw pickups ----
    for (const p of pickups) {
      const pulse = Math.sin(p.pulse) * 0.3 + 0.7;
      ctx.globalAlpha = pulse;
      if (p.type === 'ammo') {
        // Ammo box
        ctx.fillStyle = '#ffaa00';
        ctx.fillRect(p.x - 10, p.y - 10, 20, 20);
        ctx.fillStyle = '#ff4400';
        ctx.font = '10px monospace';
        ctx.textAlign = 'center';
        ctx.fillText('30', p.x, p.y + 3);
      } else {
        // Health can
        ctx.fillStyle = '#44ff44';
        ctx.fillRect(p.x - 10, p.y - 10, 20, 20);
        ctx.fillStyle = '#00ff44';
        ctx.font = '10px monospace';
        ctx.textAlign = 'center';
        ctx.fillText('+25', p.x, p.y + 3);
      }
      ctx.globalAlpha = 1;
    }

    // ---- Draw enemies ----
    for (const e of enemies) {
      if (!e.alive) continue;
      ctx.fillStyle = e.color;
      ctx.fillRect(e.x - e.size/2, e.y - e.size/2, e.size, e.size);
      // Eyes for mutants
      if (e.type === 'mutant') {
        ctx.fillStyle = '#fff';
        ctx.fillRect(e.x - 3, e.y - e.size/2 + 2, 3, 3);
        ctx.fillRect(e.x + 1, e.y - e.size/2 + 2, 3, 3);
        ctx.fillStyle = '#000';
        ctx.fillRect(e.x - 2, e.y - e.size/2 + 3, 2, 2);
        ctx.fillRect(e.x + 2, e.y - e.size/2 + 3, 2, 2);
      }
      // Boss eyes
      if (e.type === 'boss') {
        ctx.fillStyle = '#ff0000';
        ctx.fillRect(e.x - 5, e.y - e.size/2 + 2, 5, 5);
        ctx.fillRect(e.x + 2, e.y - e.size/2 + 2, 5, 5);
      }
      // HP bar for bosses
      if (e.type === 'boss') {
        const barW = 30;
        ctx.fillStyle = '#333';
        ctx.fillRect(e.x - barW/2, e.y - e.size/2 - 6, barW, 3);
        ctx.fillStyle = '#ff0000';
        ctx.fillRect(e.x - barW/2, e.y - e.size/2 - 6, barW * (e.hp / e.maxHp), 3);
      }
      // Damage text
      if (e.hitFlash > 0) {
        ctx.fillStyle = '#fff';
        ctx.font = '8px monospace';
        ctx.textAlign = 'center';
        ctx.fillText('!', e.x, e.y - e.size/2 - 2);
      }
      // Name tag
      ctx.fillStyle = '#fff';
      ctx.font = '8px monospace';
      ctx.textAlign = 'center';
      ctx.fillText(e.type.toUpperCase(), e.x, e.y - e.size/2 - 8);
    }

    // ---- Draw player ----
    if (player.alive || player.dead) {
      if (player.dead && player.respawnTimer > 2) {
        // Dead player - blinking
        ctx.globalAlpha = 0.5;
        ctx.fillStyle = '#ff0000';
        ctx.fillRect(player.x - player.w/2, player.y - player.h/2, player.w, player.h);
        ctx.globalAlpha = 1;
      } else if (player.invincible > 0 && Math.floor(player.invincible * 10) % 2 === 0) {
        // Invincible - blinking
        ctx.globalAlpha = 0.4;
        ctx.fillStyle = '#00ff41';
        ctx.fillRect(player.x - player.w/2, player.y - player.h/2, player.w, player.h);
        ctx.globalAlpha = 1;
      } else {
        // Player body
        ctx.fillStyle = '#00ff41';
        ctx.fillRect(player.x - player.w/2, player.y - player.h/2, player.w, player.h);
        // Head
        ctx.fillStyle = '#00ff41';
        ctx.fillRect(player.x - 5, player.y - player.h/2 - 6, 10, 8);
        // Weapon
        const w = WEAPONS[player.currentWeapon];
        ctx.fillStyle = w.color;
        ctx.fillRect(player.x - 2, player.y - 2, 4, 10);
        // Legs
        ctx.fillStyle = '#004400';
        if (keys['w'] || keys['arrowup']) {
          ctx.fillRect(player.x - 5, player.y + 12, 4, 6);
          ctx.fillRect(player.x + 1, player.y + 12, 4, 6);
        } else {
          ctx.fillRect(player.x - 5, player.y + 12, 4, 6);
          ctx.fillRect(player.x + 1, player.y + 12, 4, 6);
        }
      }
    }

    // ---- Draw bullets ----
    for (const b of bullets) {
      ctx.fillStyle = b.color;
      ctx.fillRect(b.x - 2, b.y - 2, 4, 4);
      // Trail
      for (const t of b.trail) {
        ctx.globalAlpha = t.life / 8 * 0.3;
        ctx.fillStyle = b.color;
        ctx.fillRect(t.x - 1, t.y - 1, 2, 2);
      }
      ctx.globalAlpha = 1;
    }

    // ---- Draw power-ups ----
    for (const p of powerups) {
      const pulse = Math.sin(p.pulse) * 0.3 + 0.7;
      ctx.globalAlpha = pulse;
      const colors = { health: '#44ff44', shield: '#00ccff', ammo: '#ffaa00' };
      ctx.fillStyle = colors[p.type] || '#ffffff';
      ctx.fillRect(p.x - 10, p.y - 10, 20, 20);
      ctx.fillStyle = '#000';
      ctx.font = '8px monospace';
      ctx.textAlign = 'center';
      ctx.fillText(p.type === 'health' ? '+' : p.type === 'shield' ? '!' : '×', p.x, p.y + 3);
      ctx.globalAlpha = 1;
    }

    // ---- Draw HUD ----
    drawHUD();

    // ---- Level Name ----
    ctx.fillStyle = '#ffffff';
    ctx.font = '14px monospace';
    ctx.textAlign = 'left';
    ctx.fillText('LEVEL ' + level + ': ' + LEVELS[Math.min(level - 1, LEVELS.length - 1)].name, 10, 16);

    // ---- Boss indicator ----
    if (levelEndShown) {
      ctx.fillStyle = '#ffaa00';
      ctx.font = '12px monospace';
      ctx.textAlign = 'center';
      ctx.fillText('PRESS SPACE TO CONTINUE', W/2, H - 20);
    }
  }

  // ---- Menu Screen ----
  if (state === 'MENU') {
    drawMenu();
  }

  // ---- Pause Screen ----
  if (state === 'PAUSED') {
    drawPaused();
  }

  // ---- Death Screen ----
  if (state === 'DEATH') {
    drawDeath();
  }

  // ---- Win Screen ----
  if (state === 'WIN') {
    drawWin();
  }

  // ---- Flash effect ----
  if (flashAlpha > 0) {
    ctx.globalAlpha = flashAlpha;
    ctx.fillStyle = flashColor;
    ctx.fillRect(0, 0, W, H);
    ctx.globalAlpha = 1;
  }

  ctx.restore();
}

function drawHUD() {
  // Health bar
  const barW = 160;
  const barH = 16;
  const barX = 10;
  const barY = 10;
  ctx.fillStyle = '#222';
  ctx.fillRect(barX, barY, barW, barH);
  const hpRatio = player.health / player.maxHealth;
  ctx.fillStyle = hpRatio > 0.5 ? '#00ff41' : hpRatio > 0.25 ? '#ffaa00' : '#ff0000';
  ctx.fillRect(barX, barY, barW * hpRatio, barH);
  ctx.strokeStyle = '#555';
  ctx.lineWidth = 1;
  ctx.strokeRect(barX, barY, barW, barH);

  // Health text
  ctx.fillStyle = '#fff';
  ctx.font = '10px monospace';
  ctx.textAlign = 'left';
  ctx.fillText('HP ' + Math.ceil(player.health) + '/' + player.maxHealth, barX + 2, barY + 12);

  // Ammo
  const weapon = WEAPONS[player.currentWeapon];
  ctx.fillStyle = weapon.color;
  ctx.font = '10px monospace';
  ctx.fillText(weapon.name + ': ' + Math.ceil(player.ammo) + '/' + weapon.mag, barX, barY + 26);

  // Weapon indicator
  ctx.fillStyle = '#888';
  ctx.font = '8px monospace';
  ctx.fillText('[1:' + weapon.name + ' 2:' + WEAPONS[1].name + ' 3:' + WEAPONS[2].name + ']', barX, barY + 38);

  // Score
  ctx.fillStyle = '#fff';
  ctx.textAlign = 'right';
  ctx.fillText('SCORE: ' + score, W - 10, 16);
  ctx.fillText('KILLS: ' + kills, W - 10, 28);
  ctx.fillText('LEVEL: ' + level, W - 10, 40);

  // Combo
  if (comboCount > 1) {
    ctx.fillStyle = '#ffaa00';
    ctx.font = '10px monospace';
    ctx.textAlign = 'center';
    ctx.fillText('COMBO x' + comboCount + '!', W/2, 20);
  }

  // Level progress bar
  const lvl = LEVELS[Math.min(level - 1, LEVELS.length - 1)];
  const prog = (level - 1) / (LEVELS.length - 1);
  const progX = W/2 - 30;
  const progY = 52;
  ctx.fillStyle = '#333';
  ctx.fillRect(progX, progY, 60, 4);
  ctx.fillStyle = '#00ff41';
  ctx.fillRect(progX, progY, 60 * prog, 4);
  ctx.fillStyle = '#fff';
  ctx.font = '8px monospace';
  ctx.textAlign = 'center';
  ctx.fillText('PROGRESS', W/2, progY + 14);
}

function drawMenu() {
  // Dark background
  ctx.fillStyle = '#0a0a0a';
  ctx.fillRect(0, 0, W, H);

  // Title
  ctx.fillStyle = '#00ff41';
  ctx.font = 'bold 42px monospace';
  ctx.textAlign = 'center';
  ctx.fillText('RETRO SHOOTER', W/2, 130);

  // Subtitle
  ctx.fillStyle = '#ff0066';
  ctx.font = '14px monospace';
  ctx.fillText('A Retro FPS Experience', W/2, 165);

  // Start button
  const btnX = W/2 - 100;
  const btnY = 220;
  const btnW = 200;
  const btnH = 48;
  ctx.fillStyle = '#ff4400';
  ctx.fillRect(btnX, btnY, btnW, btnH);
  ctx.fillStyle = '#fff';
  ctx.font = '16px monospace';
  ctx.textAlign = 'center';
  ctx.fillText('PRESS ENTER TO START', W/2, btnY + 22);

  // Instructions
  ctx.fillStyle = '#888';
  ctx.font = '10px monospace';
  ctx.fillText('WASD: Move    Mouse: Aim    Click: Shoot', W/2, 340);
  ctx.fillText('1/2/3: Switch Weapons    ESC: Pause', W/2, 360);
  ctx.fillText('3 Weapons: Pistol | Shotgun | Minigun', W/2, 380);
  ctx.fillText('Collect Ammo & Health pickups!', W/2, 400);
  ctx.fillText('Defeat the boss to complete each level!', W/2, 420);

  // High Score
  ctx.fillStyle = '#ffaa00';
  ctx.font = '12px monospace';
  ctx.fillText('HIGH SCORE: ' + highScore, W/2, 480);

  // Controls summary
  ctx.fillStyle = '#666';
  ctx.font = '9px monospace';
  ctx.fillText('Retro Pixel Art | Web Audio Powered', W/2, 540);
}

function drawPaused() {
  ctx.fillStyle = 'rgba(0,0,0,0.7)';
  ctx.fillRect(0, 0, W, H);
  ctx.fillStyle = '#fff';
  ctx.font = 'bold 36px monospace';
  ctx.textAlign = 'center';
  ctx.fillText('PAUSED', W/2, H/2 - 20);
  ctx.font = '14px monospace';
  ctx.fillText('Press ESC to resume', W/2, H/2 + 20);
}

function drawDeath() {
  ctx.fillStyle = 'rgba(0,0,0,0.6)';
  ctx.fillRect(0, 0, W, H);
  ctx.fillStyle = '#ff0000';
  ctx.font = 'bold 48px monospace';
  ctx.textAlign = 'center';
  ctx.fillText('DEAD', W/2, H/2 - 20);
  ctx.font = '14px monospace';
  ctx.fillStyle = '#fff';
  ctx.fillText('Respawning in 3 seconds...', W/2, H/2 + 20);
}

function drawWin() {
  ctx.fillStyle = 'rgba(0,0,0,0.7)';
  ctx.fillRect(0, 0, W, H);
  ctx.fillStyle = '#ffaa00';
  ctx.font = 'bold 48px monospace';
  ctx.textAlign = 'center';
  ctx.fillText('LEVEL COMPLETE!', W/2, H/2 - 20);
  ctx.font = '14px monospace';
  ctx.fillStyle = '#fff';
  ctx.fillText('Score: ' + score + '  Kills: ' + kills, W/2, H/2 + 30);
  ctx.fillText('Press ENTER for next level', W/2, H/2 + 60);
}

// ---- Game Loop ----
function gameLoop(timestamp) {
  const dt = Math.min((timestamp - lastTime) / 1000, 0.05);
  lastTime = timestamp;

  if (state === 'PLAYING') {
    update(dt);
  }

  // Always render
  render();

  requestAnimationFrame(gameLoop);
}

// ---- Start ----
initGame();
lastTime = performance.now();
requestAnimationFrame(gameLoop);

// ---- Keyboard enter ----
document.addEventListener('keydown', e => {
  if (e.key === 'Enter') {
    if (state === 'MENU') {
      startLevel();
    } else if (state === 'DEATH') {
      respawnPlayer();
    } else if (state === 'WIN') {
      level++;
      startLevel();
    }
  }
  if (e.key === ' ') {
    if (state === 'DEATH' || state === 'WIN') {
      // Trigger respawn / level advance
      if (state === 'DEATH') respawnPlayer();
      if (state === 'WIN') {
        level++;
        startLevel();
      }
    }
  }
});

// ---- Handle window resize ----
function resizeCanvas() {
  const container = document.getElementById('gameContainer');
  const scale = Math.min(window.innerWidth / W, window.innerHeight / H);
  canvas.style.width = (W * scale) + 'px';
  canvas.style.height = (H * scale) + 'px';
}
resizeCanvas();
window.addEventListener('resize', resizeCanvas);
</script>
</body>
</html>
<!-- agent-meta {"model":"inclusionai/ling-3.0-tiny:free","provider":"openrouter","persona":"full","sessionId":"6332a0b3-f658-476b-8318-7a5a473aba1f","tokensIn":180688,"tokensOut":21784,"tokensTotal":202472,"cost":0,"turns":8,"toolCalls":7,"failedToolCalls":0,"timestamp":"2026-08-09T02:31:41.659Z"} -->