← DOOM results

DOOM

inkling · 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 22,029 bytes · SHA-256 221e4641b868
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CYBER SHRINE — Retro FPS</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=VT323&display=swap');

:root {
  --neon-cyan: #00f3ff;
  --neon-magenta: #ff00aa;
  --neon-green: #00ff9d;
  --dark-bg: #050511;
  --dark-wall: #0a0a18;
  --hud-text: #e0e0ff;
}

* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; background: var(--dark-bg); color: var(--hud-text); font-family: 'VT323', monospace; user-select: none; }
#gameCanvas { display: block; image-rendering: pixelated; }
#hud { position: fixed; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 10; }
.hud-top { position: absolute; top: 16px; left: 50%; transform: translateX(-50%); display: flex; gap: 24px; align-items: center; background: rgba(5,5,17,0.85); border: 2px solid var(--neon-cyan); padding: 8px 20px; border-radius: 4px; box-shadow: 0 0 15px rgba(0,243,255,0.25); }
.hud-top .label { font-family: 'Orbitron', sans-serif; font-weight: 700; letter-spacing: 2px; color: var(--neon-magenta); text-shadow: 0 0 6px var(--neon-magenta); font-size: 1.1rem; }
.hud-top .stat { font-family: 'VT323', monospace; font-size: 1.4rem; color: var(--hud-text); min-width: 70px; text-align: center; }
.hud-top .stat .icon { display: inline-block; width: 14px; height: 14px; border: 2px solid; border-radius: 50%; margin-right: 4px; vertical-align: middle; }
.hud-bottom { position: absolute; bottom: 16px; left: 50%; transform: translateX(-50%); display: flex; gap: 16px; align-items: center; background: rgba(5,5,17,0.85); border: 2px solid var(--neon-green); padding: 10px 20px; border-radius: 4px; box-shadow: 0 0 15px rgba(0,255,157,0.25); }
.weapon-slot { display: flex; align-items: center; gap: 8px; padding: 6px 10px; border-radius: 3px; border: 1px solid transparent; transition: .1s; }
.weapon-slot.active { border-color: var(--neon-cyan); background: rgba(0,243,255,0.1); box-shadow: 0 0 10px rgba(0,243,255,0.2); }
.weapon-slot .name { font-family: 'Orbitron', sans-serif; font-weight: 700; letter-spacing: 1px; color: var(--hud-text); font-size: 0.85rem; }
.weapon-slot .ammo { font-family: 'VT323', monospace; font-size: 1.1rem; color: var(--neon-green); }
#crosshair { position: absolute; top: 50%; left: 50%; width: 24px; height: 24px; transform: translate(-50%, -50%); pointer-events: none; z-index: 5; }
#crosshair::before, #crosshair::after { content: ''; position: absolute; background: var(--neon-cyan); }
#crosshair::before { width: 2px; height: 100%; left: 50%; transform: translateX(-50%); box-shadow: 0 0 6px var(--neon-cyan); }
#crosshair::after { height: 2px; width: 100%; top: 50%; transform: translateY(-50%); box-shadow: 0 0 6px var(--neon-cyan); }
#overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 20; }
#screen-text { position: absolute; top: 45%; left: 50%; transform: translate(-50%, -50%); text-align: center; opacity: 0; transition: opacity 0.3s; }
#screen-text h1 { font-family: 'Orbitron', sans-serif; font-size: 4rem; letter-spacing: 8px; text-transform: uppercase; text-shadow: 0 0 20px var(--neon-magenta); color: var(--neon-magenta); }
#screen-text p { font-family: 'VT323', monospace; font-size: 2rem; color: var(--neon-green); margin-top: 12px; text-shadow: 0 0 10px var(--neon-green); }
#start-screen { position: fixed; inset: 0; background: var(--dark-bg); display: flex; flex-direction: column; align-items: center; justify-content: center; z-index: 30; transition: opacity 0.6s; }
#start-screen h1 { font-family: 'Orbitron', sans-serif; font-size: 5rem; letter-spacing: 10px; color: var(--neon-cyan); text-shadow: 0 0 30px var(--neon-cyan), 0 0 60px var(--neon-cyan); animation: pulse 2s infinite alternate; }
#start-screen .sub { font-family: 'VT323', monospace; font-size: 1.8rem; color: var(--neon-magenta); margin: 8px 0 40px; text-shadow: 0 0 15px var(--neon-magenta); }
#start-screen button { font-family: 'Orbitron', sans-serif; font-size: 1.2rem; padding: 14px 36px; background: transparent; border: 2px solid var(--neon-green); color: var(--neon-green); cursor: pointer; text-transform: uppercase; letter-spacing: 3px; box-shadow: 0 0 15px var(--neon-green); transition: background 0.2s, color 0.2s; }
#start-screen button:hover { background: var(--neon-green); color: var(--dark-bg); box-shadow: 0 0 30px var(--neon-green); }
#start-screen .controls { margin-top: 30px; color: #889; font-size: 1rem; line-height: 1.6; }
#start-screen .controls b { color: var(--neon-cyan); }
@keyframes pulse { from { text-shadow: 0 0 20px var(--neon-cyan), 0 0 40px var(--neon-cyan); } to { text-shadow: 0 0 40px var(--neon-cyan), 0 0 80px var(--neon-cyan); } }
</style>
</head>
<body>
<div id="start-screen">
  <h1>CYBER SHRINE</h1>
  <div class="sub">A Retro FPS Experience</div>
  <button onclick="startGame()">Initiate Sequence</button>
  <div class="controls">
    <b>WASD</b> — Move &nbsp;|&nbsp; <b>MOUSE</b> — Aim &nbsp;|&nbsp; <b>LEFT CLICK</b> — Shoot<br>
    <b>1 2 3</b> — Weapons &nbsp;|&nbsp; Pick up ammo, health, and reach the exit.
  </div>
</div>

<div id="hud">
  <div class="hud-top">
    <div class="label">CYBER SHRINE</div>
    <div class="stat"><span class="icon" style="border-color: var(--neon-magenta); background: var(--neon-magenta);"></span><span id="hp">100</span></div>
    <div class="stat"><span class="icon" style="border-color: var(--neon-green);"></span><span id="score">0</span></div>
  </div>
  <div id="crosshair"></div>
  <div class="hud-bottom">
    <div class="weapon-slot active" data-w="0">
      <div class="name">NEON PISTOL</div>
      <div class="ammo" id="ammo-0">∞</div>
    </div>
    <div class="weapon-slot" data-w="1">
      <div class="name">SHADOW SHOT</div>
      <div class="ammo" id="ammo-1">20</div>
    </div>
    <div class="weapon-slot" data-w="2">
      <div class="name">VOID BLAST</div>
      <div class="ammo" id="ammo-2">5</div>
    </div>
  </div>
  <div id="overlay"></div>
  <div id="screen-text">
    <h1 id="txt-title">GAME OVER</h1>
    <p id="txt-sub">Respawning...</p>
  </div>
</div>

<canvas id="gameCanvas"></canvas>

<script>
// Game config
const W = 14; // map width
const H = 14; // map height
const TILE = 64; // world units per tile
const FOV = Math.PI / 3; // field of view
const NUM_RAYS = 320; // resolution
const RAY_STEP = Math.PI / 3 / NUM_RAYS;

// Map layout: 1=wall, 0=empty, 2=player start, 3=exit, 4=enemy, 5=health, 6=ammo-pistol(not needed), 7=shotgun ammo, 8=rocket ammo
// We'll generate programmatically for variety but use a fixed seed-like layout for reliability.
let map = [];
function buildMap() {
  map = [];
  // Initialize empty
  for(let y=0; y<H; y++) {
    let row = [];
    for(let x=0; x<W; x++) row.push(0);
    map.push(row);
  }
  // Walls around border
  for(let i=0; i<W; i++) { map[0][i] = 1; map[H-1][i] = 1; map[i][0] = 1; map[i][W-1] = 1; }
  // Internal walls and rooms
  const layout = [
    [2,0,1,1,1,1,1,0,1,1,1,0,3,0],
    [0,0,0,0,0,0,0,0,0,0,0,0,0,0],
    [1,1,1,0,1,1,1,1,1,0,1,0,1,0],
    [0,0,0,0,0,0,0,0,1,0,0,0,0,0],
    [1,0,1,1,1,1,0,1,1,0,1,1,1,0],
    [0,0,0,0,0,0,0,0,0,0,0,0,0,0],
    [1,1,1,1,1,0,1,1,1,0,0,0,1,0],
    [0,0,0,0,0,0,0,0,1,0,1,1,0,0],
    [1,1,1,0,1,1,1,0,1,1,1,0,1,0],
    [0,0,0,0,0,0,0,0,0,0,0,0,0,0],
    [1,0,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],
    [1,1,0,1,1,0,1,1,1,0,1,1,1,0],
    [0,0,4,0,1,0,0,0,0,0,0,0,0,0],
  ];
  for(let y=1; y<H-1; y++) {
    for(let x=1; x<W-1; x++) {
      const v = layout[y-1][x-1];
      if(v>=0 && v<=4) map[y][x] = v; // keep walls and objects
      else map[y][x] = 0;
    }
  }
  // Place some pickups and enemies manually in empty spots for fun
  const extras = [
    [1,7,5], [5,3,5], [7,6,5], [10,8,5],
    [3,9,7], [9,3,7], [11,4,8], [6,11,8],
    [2,5,0], [8,2,0], [12,10,0], // ammo not needed for pistol
  ];
  for(const [x,y,t] of extras) {
    if(map[y][x] === 0) map[y][x] = t;
  }
  // Force start and exit
  map[1][1] = 2; // player start
  map[1][W-2] = 3; // exit
  map[13][2] = 4; // enemy
  map[6][11] = 4; // enemy
  return map;
}

buildMap();

// Game state
let player = { x: 1.5, y: 1.5, angle: 0, hp: 100, score: 0, dead: false, respawnTimer: 0 };
let weapons = [
  { name: 'NEON PISTOL', ammo: Infinity, damage: 15, rate: 600, color: '#00f3ff', sprite: null },
  { name: 'SHADOW SHOT', ammo: 20, damage: 40, rate: 900, color: '#ff00aa', sprite: null },
  { name: 'VOID BLAST', ammo: 5, damage: 120, rate: 1500, color: '#00ff9d', sprite: null },
];
let currentWeapon = 0;
let keys = {};
let mouseX = 0;
let mouseDown = false;
let lastShot = 0;
let pickupFlash = 0;
let overlayAlpha = 0;
let overlayText = { title: '', sub: '' };
let win = false;
let loopStarted = false;

const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');

function resize() {
  canvas.width = window.innerWidth;
  canvas.height = window.innerHeight;
}
window.addEventListener('resize', resize);
resize();

// Input
window.addEventListener('keydown', e => {
  keys[e.key.toLowerCase()] = true;
  if (e.key === '1') currentWeapon = 0;
  if (e.key === '2') currentWeapon = 1;
  if (e.key === '3') currentWeapon = 2;
  if (e.key === 'r' || e.key === 'R') restartLevel();
});
window.addEventListener('keyup', e => keys[e.key.toLowerCase()] = false);

window.addEventListener('mousemove', e => {
  mouseX = e.movementX || 0;
  if (document.pointerLockElement) mouseX *= 0.002; // sensitivity
});
window.addEventListener('mousedown', () => { mouseDown = true; if (!document.pointerLockElement) canvas.requestPointerLock(); });
window.addEventListener('mouseup', () => mouseDown = false);
window.addEventListener('click', () => { if (!document.pointerLockElement) canvas.requestPointerLock(); });

function startGame() {
  if(loopStarted) return;
  loopStarted = true;
  document.getElementById('start-screen').style.opacity = '0';
  document.getElementById('start-screen').style.pointerEvents = 'none';
  canvas.requestPointerLock();
  setTimeout(() => { document.getElementById('start-screen').style.display = 'none'; }, 600);
  gameLoop();
}

function restartLevel() {
  buildMap();
  player.x = 1.5; player.y = 1.5; player.angle = 0; player.hp = 100; player.dead = false; player.respawnTimer = 0;
  weapons[0].ammo = Infinity; weapons[1].ammo = 20; weapons[2].ammo = 5;
  currentWeapon = 0; win = false; overlayAlpha = 0;
}

// Raycasting renderer
function render() {
  ctx.fillStyle = '#050511';
  ctx.fillRect(0, 0, canvas.width, canvas.height);

  // Sky / ceiling gradient
  const skyGrad = ctx.createLinearGradient(0, 0, 0, canvas.height / 2);
  skyGrad.addColorStop(0, '#08082a');
  skyGrad.addColorStop(1, '#0a0a18');
  ctx.fillStyle = skyGrad;
  ctx.fillRect(0, 0, canvas.width, canvas.height * 0.45);

  // Floor
  ctx.fillStyle = '#080510';
  ctx.fillRect(0, canvas.height * 0.45, canvas.width, canvas.height * 0.55);
  // Grid texture on floor
  for (let y = 0; y < canvas.height * 0.55; y += 32) {
    for (let x = 0; x < canvas.width; x += 32) {
      if ((Math.floor(x / 32) + Math.floor(y / 32)) % 2 === 0) {
        ctx.fillStyle = 'rgba(0,243,255,0.03)';
        ctx.fillRect(x, canvas.height * 0.45 + y, 32, 32);
      }
    }
  }

  // Raycasting walls
  const screenDist = (canvas.width / 2) / Math.tan(FOV / 2);
  for (let i = 0; i < NUM_RAYS; i++) {
    const rayAngle = player.angle - FOV / 2 + (i / NUM_RAYS) * FOV;
    let distance = 0;
    let hitWall = false;
    let wallType = 0;
    let hitX = 0, hitY = 0;

    while (!hitWall && distance < 20) {
      distance += 0.05;
      const rx = player.x + Math.cos(rayAngle) * distance;
      const ry = player.y + Math.sin(rayAngle) * distance;
      const mx = Math.floor(rx);
      const my = Math.floor(ry);
      if (mx < 0 || my < 0 || mx >= W || my >= H) { hitWall = true; wallType = 0; break; }
      if (map[my][mx] > 0 && map[my][mx] !== 2) {
        hitWall = true; wallType = map[my][mx]; hitX = rx; hitY = ry;
      }
    }

    const sliceHeight = Math.min(canvas.height, (TILE / distance) * screenDist * 0.8);
    const shade = Math.max(0.1, 1 - distance / 14);
    const xPos = i * (canvas.width / NUM_RAYS);
    const wallColor = wallType === 1 ? `rgba(30, 35, 60, ${shade})` : (wallType === 3 ? `rgba(0, 255, 157, ${shade+0.3})` : `rgba(20, 20, 40, ${shade})`);
    ctx.fillStyle = wallColor;
    ctx.fillRect(xPos, (canvas.height / 2) - (sliceHeight / 2), (canvas.width / NUM_RAYS) + 1, sliceHeight);
    // Wall edge outline for retro feel
    if (distance < 8) {
      ctx.strokeStyle = `rgba(0, 243, 255, ${shade * 0.4})`;
      ctx.lineWidth = 1;
      ctx.strokeRect(xPos, (canvas.height / 2) - (sliceHeight / 2), (canvas.width / NUM_RAYS) + 1, sliceHeight);
    }

    // Sprite drawing: collect sprites visible in this ray slice
    // We'll do a second pass for sprites sorted by distance
  }

  // Collect sprites
  const sprites = [];
  for(let y=0; y<H; y++) {
    for(let x=0; x<W; x++) {
      const v = map[y][x];
      if(v===3 || v===5 || v===7 || v===8 || v===4) {
        const dx = x + 0.5 - player.x;
        const dy = y + 0.5 - player.y;
        const dist = Math.sqrt(dx*dx + dy*dy);
        const angleToSprite = Math.atan2(dy, dx) - player.angle;
        // Only include if in front
        if(dist < 12) sprites.push({ x: x+0.5, y: y+0.5, type: v, dist, ang: angleToSprite, screenDist: dist });
      }
    }
  }
  sprites.sort((a,b) => b.screenDist - a.screenDist);

  // Draw sprites
  for(const s of sprites) {
    // Check if sprite is roughly in view
    const spriteScreenX = (s.ang / FOV + 0.5) * canvas.width;
    if(spriteScreenX < 0 || spriteScreenX > canvas.width) continue;
    // Check visibility roughly (not occluded by a closer wall at same angle - simplified)
    const size = Math.min(canvas.height, (0.6 / s.screenDist) * screenDist);
    const drawY = (canvas.height / 2) - (size / 2);
    const drawX = spriteScreenX - (size / 2);

    if(s.type === 3) { // exit portal
      const grad = ctx.createRadialGradient(drawX + size/2, drawY + size/2, 0, drawX + size/2, drawY + size/2, size/2);
      grad.addColorStop(0, 'rgba(0,255,157,0.9)');
      grad.addColorStop(1, 'rgba(0,255,157,0)');
      ctx.fillStyle = grad;
      ctx.fillRect(drawX, drawY, size, size);
      ctx.strokeStyle = '#00ff9d';
      ctx.lineWidth = 3;
      ctx.strokeRect(drawX+size*0.15, drawY+size*0.15, size*0.7, size*0.7);
    } else if(s.type === 5) { // health
      ctx.fillStyle = `rgba(255, 50, 120, 0.9)`;
      ctx.beginPath();
      ctx.arc(drawX + size/2, drawY + size/2, size/2, 0, Math.PI*2);
      ctx.fill();
      ctx.fillStyle = '#fff';
      ctx.font = `${size*0.6}px 'VT323', monospace`;
      ctx.fillText('+', drawX + size*0.25, drawY + size*0.7);
    } else if(s.type === 7) { // shotgun ammo
      ctx.fillStyle = `rgba(255, 0, 170, 0.9)`;
      ctx.fillRect(drawX + size*0.2, drawY + size*0.2, size*0.6, size*0.6);
      ctx.fillStyle = '#fff';
      ctx.font = `${size*0.4}px 'VT323', monospace`;
      ctx.fillText('S', drawX + size*0.35, drawY + size*0.55);
    } else if(s.type === 8) { // rocket ammo
      ctx.fillStyle = `rgba(0, 255, 157, 0.9)`;
      ctx.fillRect(drawX + size*0.2, drawY + size*0.2, size*0.6, size*0.6);
      ctx.fillStyle = '#fff';
      ctx.font = `${size*0.4}px 'VT323', monospace`;
      ctx.fillText('R', drawX + size*0.35, drawY + size*0.55);
    } else if(s.type === 4) { // enemy
      // Draw enemy sprite
      const cx = drawX + size/2;
      const cy = drawY + size/2;
      // Body
      ctx.fillStyle = `rgba(255, 0, 100, 0.9)`;
      ctx.fillRect(drawX + size*0.2, drawY + size*0.3, size*0.6, size*0.5);
      // Head
      ctx.fillStyle = '#ff0064';
      ctx.beginPath();
      ctx.arc(cx, drawY + size*0.25, size*0.25, 0, Math.PI*2);
      ctx.fill();
      // Eyes
      ctx.fillStyle = '#fff';
      ctx.beginPath(); ctx.arc(cx - size*0.1, drawY + size*0.2, size*0.06, 0, Math.PI*2); ctx.fill();
      ctx.beginPath(); ctx.arc(cx + size*0.1, drawY + size*0.2, size*0.06, 0, Math.PI*2); ctx.fill();
    }
  }

  // Crosshair and flash
  if(pickupFlash > 0) {
    pickupFlash -= 0.05;
    ctx.fillStyle = `rgba(0, 255, 157, ${pickupFlash})`;
    ctx.fillRect(0, 0, canvas.width, canvas.height);
  }

  // Weapon flash at bottom
  if(lastShot > 0) {
    lastShot -= 0.02;
    ctx.fillStyle = weapons[currentWeapon].color;
    ctx.globalAlpha = lastShot;
    ctx.fillRect(canvas.width/2 - 50, canvas.height - 120, 100, 80);
    ctx.globalAlpha = 1;
  }
}

function update() {
  if(player.dead) {
    player.respawnTimer--;
    if(player.respawnTimer <= 0) {
      player.dead = false; player.hp = 100; player.x = 1.5; player.y = 1.5; player.angle = 0;
      overlayAlpha = 0;
      document.getElementById('txt-title').textContent = 'RESPAWNED';
      document.getElementById('txt-sub').textContent = 'Good luck, runner.';
    }
    return;
  }
  if(win) return;

  // Rotate
  player.angle += mouseX;
  player.angle = player.angle % (Math.PI * 2);

  // Move
  let nx = player.x; let ny = player.y;
  if(keys['w']) { nx += Math.cos(player.angle) * 0.06; ny += Math.sin(player.angle) * 0.06; }
  if(keys['s']) { nx -= Math.cos(player.angle) * 0.06; ny -= Math.sin(player.angle) * 0.06; }
  if(keys['a']) { nx += Math.sin(player.angle) * 0.06; ny -= Math.cos(player.angle) * 0.06; }
  if(keys['d']) { nx -= Math.sin(player.angle) * 0.06; ny += Math.cos(player.angle) * 0.06; }
  // Wall collision
  if(map[Math.floor(ny)][Math.floor(nx)] === 0 || map[Math.floor(ny)][Math.floor(nx)] > 2) {
    player.x = nx; player.y = ny;
  }

  // Pickup collisions
  const px = Math.floor(player.x); const py = Math.floor(player.y);
  const val = map[py][px];
  if(val === 5) { // health
    player.hp = Math.min(100, player.hp + 25);
    map[py][px] = 0;
    pickupFlash = 1;
  } else if(val === 7) { // shotgun ammo
    weapons[1].ammo += 5;
    map[py][px] = 0;
    pickupFlash = 1;
  } else if(val === 8) { // rocket ammo
    weapons[2].ammo += 2;
    map[py][px] = 0;
    pickupFlash = 1;
  } else if(val === 3) { // exit
    win = true;
    overlayAlpha = 1;
    document.getElementById('txt-title').textContent = 'VICTORY';
    document.getElementById('txt-sub').textContent = 'You escaped the Shrine.';
    document.getElementById('screen-text').style.opacity = 1;
  }

  // Enemy update (very simple AI)
  for(let y=0; y<H; y++) {
    for(let x=0; x<W; x++) {
      if(map[y][x] === 4) {
        // Simple move toward player if close
        const dx = player.x - (x + 0.5); const dy = player.y - (y + 0.5);
        const dist = Math.sqrt(dx*dx + dy*dy);
        if(dist < 6 && dist > 0.5) {
          const nx = x + 0.5 + (dx/dist)*0.04;
          const ny = y + 0.5 + (dy/dist)*0.04;
          const mx = Math.floor(nx); const my = Math.floor(ny);
          if(map[my][mx] === 0 || map[my][mx] > 2) {
            map[y][x] = 0; map[Math.floor(ny)][Math.floor(nx)] = 4;
          }
        }
        // Attack if close
        if(dist < 1.2 && !player.dead) {
          player.hp -= 0.8;
          if(player.hp <= 0) {
            player.hp = 0; player.dead = true; player.respawnTimer = 180; // 3 sec
            overlayAlpha = 1;
            document.getElementById('txt-title').textContent = 'CRITICAL FAILURE';
            document.getElementById('txt-sub').textContent = 'Respawning...';
            document.getElementById('screen-text').style.opacity = 1;
          }
        }
      }
    }
  }

  // Shooting
  if(mouseDown && !player.dead && !win && Date.now() - lastShot > weapons[currentWeapon].rate) {
    lastShot = Date.now();
    if(weapons[currentWeapon].ammo !== Infinity) {
      if(weapons[currentWeapon].ammo > 0) weapons[currentWeapon].ammo--;
      else { lastShot = 0; return; }
    }
    // Shoot ray
    let shotDist = 0; let hitEnemy = false; let enemyDist = Infinity; let hitEnemyData = null;
    while(shotDist < 12) {
      shotDist += 0.05;
      const rx = player.x + Math.cos(player.angle) * shotDist;
      const ry = player.y + Math.sin(player.angle) * shotDist;
      const mx = Math.floor(rx); const my = Math.floor(ry);
      if(mx < 0 || my < 0 || mx >= W || my >= H) break;
      if(map[my][mx] === 4) {
        const edx = rx - (mx+0.5); const edy = ry - (my+0.5);
        const edist = Math.sqrt(edx*edx + edy*edy);
        if(edist < 0.6) { hitEnemy = true; hitEnemyData = { mx, my }; break; }
      }
      if(map[my][mx] === 1) break;
    }
    if(hitEnemy && hitEnemyData) {
      map[hitEnemyData.my][hitEnemyData.mx] = 0; // kill enemy (remove from map by setting empty since we don't have enemy list separately - but simple is fine)
      player.score += 100;
    }
  }

  // Update HUD
  document.getElementById('hp').textContent = Math.max(0, Math.floor(player.hp));
  document.getElementById('score').textContent = player.score;
  document.getElementById('ammo-0').textContent = '∞';
  document.getElementById('ammo-1').textContent = weapons[1].ammo;
  document.getElementById('ammo-2').textContent = weapons[2].ammo;

  // Weapon slot styling
  document.querySelectorAll('.weapon-slot').forEach(s => {
    s.classList.remove('active');
    if(parseInt(s.dataset.w) === currentWeapon) s.classList.add('active');
  });

  // Overlay fade
  if(overlayAlpha > 0 && !player.dead && !win) overlayAlpha -= 0.02;
  document.getElementById('screen-text').style.opacity = overlayAlpha;
}

function gameLoop() {
  update();
  render();
  requestAnimationFrame(gameLoop);
}

// Initialize HUD
update();
render();
</script>
</body>
</html>
<!-- agent-meta {"model":"thinkingmachines/inkling","provider":"openrouter","persona":"full","sessionId":"945a5081-5d23-4a65-aa66-3389e41f5ddc","tokensIn":140155,"tokensOut":10835,"tokensTotal":150990,"turns":9,"toolCalls":8,"failedToolCalls":0,"timestamp":"2026-07-18T21:32:17.258Z"} -->