← DOOM results

DOOM

inkling · basic

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 16,320 bytes · SHA-256 e53a9387d510
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>VOID STRIKE - Retro FPS</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap');
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { height: 100%; background: #0a0a12; overflow: hidden; font-family: 'Press Start 2P', monospace; user-select: none; }
canvas { display: block; width: 100%; height: 100vh; image-rendering: pixelated; image-rendering: crisp-edges; }
.hud { position: absolute; top: 16px; left: 16px; color: #0ff; text-shadow: 2px 2px 0 #000, -1px -1px 0 #fff; font-size: 10px; line-height: 1.8; pointer-events: none; z-index: 10; }
.hud h1 { font-size: 12px; color: #f00; margin-bottom: 4px; letter-spacing: 2px; }
.hud .bar { width: 140px; height: 10px; background: #222; border: 2px solid #555; margin: 2px 0; position: relative; }
.hud .fill { height: 100%; background: #0f0; }
.hud .fill.low { background: #f0f; }
.hud .fill.crit { background: #f00; }
.overlay-msg { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: #fff; font-size: 28px; text-align: center; text-shadow: 4px 4px 0 #f00, -2px -2px 0 #000; letter-spacing: 4px; pointer-events: none; opacity: 0; transition: opacity 0.2s; z-index: 20; }
.overlay-msg.show { opacity: 1; }
.controls { position: absolute; bottom: 8px; left: 0; width: 100%; text-align: center; color: #666; font-size: 8px; pointer-events: none; z-index: 10; }
.crosshair { position: absolute; top: 50%; left: 50%; width: 12px; height: 12px; transform: translate(-50%, -50%); border: 2px solid rgba(255,255,255,0.5); border-radius: 50%; pointer-events: none; z-index: 15; box-shadow: 0 0 0 1px #000; }
</style>
</head>
<body>

<canvas id="game"></canvas>
<div class="crosshair"></div>
<div class="hud">
  <h1>VOID STRIKE</h1>
  <div>HEALTH <div class="bar"><div class="fill" id="barHp" style="width:100%"></div></div></div>
  <div>AMMO <div class="bar"><div class="fill" id="barAmmo" style="width:100%"></div></div></div>
  <div style="margin-top:4px;">WEAPON: <span id="wName">BLASTER</span></div>
  <div>STATUS: <span id="status">ACTIVE</span></div>
</div>
<div class="overlay-msg" id="msg"></div>
<div class="controls">WASD MOVE | MOUSE LOOK | 1-2-3 WEAPONS | SPACE/CLICK FIRE</div>

<script>
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const msgEl = document.getElementById('msg');

// Size
const W = 800;
const H = 480;
canvas.width = W; canvas.height = H;

// Audio context for retro beeps
const AudioCtx = window.AudioContext || window.webkitAudioContext;
const audioCtx = new AudioCtx();
function beep(freq, type='square', dur=0.1, vol=0.1) {
  try {
    const o = audioCtx.createOscillator();
    const g = audioCtx.createGain();
    o.type = type;
    o.frequency.setValueAtTime(freq, audioCtx.currentTime);
    o.connect(g);
    g.connect(audioCtx.destination);
    g.gain.setValueAtTime(vol, audioCtx.currentTime);
    g.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + dur);
    o.start();
    o.stop(audioCtx.currentTime + dur);
  } catch(e){}
}

// Map: 1=wall, values are tile types
// 14 columns x 13 rows
const MAP = [
  [1,1,1,1,1,1,1,1,1,1,1,1,1,1],
  [1,6,0,0,0,0,0,0,0,0,0,0,0,1],
  [1,0,0,1,1,0,0,0,1,0,1,0,0,1],
  [1,0,0,0,0,0,1,0,0,0,0,0,4,1],
  [1,1,0,1,1,1,0,0,1,1,0,0,0,1],
  [1,0,0,0,0,0,0,0,0,0,0,1,0,1],
  [1,0,1,1,1,1,1,1,1,0,0,0,0,1],
  [1,0,0,0,0,0,2,0,0,1,1,1,0,1],
  [1,1,1,0,1,1,1,1,0,0,0,0,0,1],
  [1,0,0,0,0,0,0,0,0,1,0,3,0,1],
  [1,1,1,1,1,1,1,1,1,1,1,1,5,1],
  [1,0,0,0,0,0,0,0,0,0,0,0,0,1],
  [1,1,1,1,1,1,1,1,1,1,1,1,1,1]
];

// State
let player = { x: 1.5, y: 1.5, dir: 0, planeX: 0, planeY: 0.66 };
let health = 100;
let ammo = { scatter: 20, pulse: 5 }; // index 1 and 2
let weaponIdx = 0;
const weapons = [
  { name: 'BLASTER', dmg: 10, range: 5, ammoUsed: 0, color: '#0ff', sound: [880, 0.05] },
  { name: 'SCATTER', dmg: 25, range: 4, ammoUsed: 1, color: '#f0f', sound: [330, 0.08] },
  { name: 'PULSE', dmg: 50, range: 6, ammoUsed: 2, color: '#0f0', sound: [110, 0.12] }
];
let entities = [];
let gameState = 'playing'; // playing, dead, won

function initEntities() {
  entities = [];
  // Enemy at row 7 col 6
  entities.push({ type: 'enemy', x: 6.5, y: 6.5, hp: 30, dead: false });
  // Ammo pickup at row 3 col 12
  entities.push({ type: 'ammo', x: 12.5, y: 3.5, collected: false });
  // Health pickup at row 9 col 11
  entities.push({ type: 'health', x: 11.5, y: 9.5, collected: false });
  // Exit at row 10 col 12
  entities.push({ type: 'exit', x: 12.5, y: 10.5 });
}

function respawn() {
  player.x = 1.5; player.y = 1.5; player.dir = 0; player.planeX = 0; player.planeY = 0.66;
  health = 100;
  ammo = { scatter: 20, pulse: 5 };
  weaponIdx = 0;
  gameState = 'playing';
  msgEl.classList.remove('show');
  initEntities();
}

initEntities();

function resetFull() {
  respawn();
}

function msg(text, delay=2000) {
  msgEl.textContent = text;
  msgEl.classList.add('show');
  if (delay > 0) setTimeout(() => msgEl.classList.remove('show'), delay);
}

function updateHUD() {
  document.getElementById('wName').textContent = weapons[weaponIdx].name;
  document.getElementById('status').textContent = gameState === 'won' ? 'VICTORY' : gameState === 'dead' ? 'DEAD' : 'ACTIVE';
  const hpPct = Math.max(0, health);
  const hpColor = hpPct > 50 ? '' : (hpPct > 20 ? 'low' : 'crit');
  document.getElementById('barHp').style.width = hpPct + '%';
  document.getElementById('barHp').className = 'fill ' + hpColor;

  const w = weapons[weaponIdx];
  let ammoVal = 999;
  if (w.ammoUsed === 1) ammoVal = ammo.scatter;
  if (w.ammoUsed === 2) ammoVal = ammo.pulse;
  const ammoPct = Math.min(100, (ammoVal / 30) * 100);
  document.getElementById('barAmmo').style.width = ammoPct + '%';
}

// Inputs
const keys = {};
window.addEventListener('keydown', e => {
  keys[e.key.toLowerCase()] = true;
  if ('123'.includes(e.key)) weaponIdx = parseInt(e.key)-1;
  if (e.key === 'r' && gameState === 'dead') respawn();
  if (e.key === ' ' && gameState === 'playing') shoot();
});
window.addEventListener('keyup', e => keys[e.key.toLowerCase()] = false);

canvas.addEventListener('mousemove', e => {
  if (document.pointerLockElement !== canvas) return;
  const sens = 0.003;
  player.dir += e.movementX * sens;
  player.planeX = -Math.sin(player.dir) * 0.66;
  player.planeY = Math.cos(player.dir) * 0.66;
});
canvas.addEventListener('click', () => {
  if (document.pointerLockElement !== canvas) canvas.requestPointerLock();
  if (gameState === 'playing') shoot();
});
document.addEventListener('pointerlockchange', () => {
  if (document.pointerLockElement !== canvas) keys['mouse'] = false;
});
document.addEventListener('click', () => {
  if (document.pointerLockElement !== canvas) canvas.requestPointerLock();
});

// Shooting
function shoot() {
  if (gameState !== 'playing') return;
  const w = weapons[weaponIdx];
  if (w.ammoUsed > 0) {
    if (w.ammoUsed === 1 && ammo.scatter <= 0) return;
    if (w.ammoUsed === 2 && ammo.pulse <= 0) return;
  }
  if (w.ammoUsed === 1) ammo.scatter--;
  if (w.ammoUsed === 2) ammo.pulse--;

  beep(w.sound[0], 'square', w.sound[1], 0.15);

  // Hitscan closest enemy in front
  let closest = null;
  let minD = w.range;
  for (const e of entities) {
    if (e.type !== 'enemy' || e.dead) continue;
    const dx = e.x - player.x;
    const dy = e.y - player.y;
    const dist = Math.sqrt(dx*dx + dy*dy);
    if (dist > w.range) continue;
    const angle = Math.atan2(dy, dx) - player.dir;
    let a = angle;
    while (a > Math.PI) a -= 2*Math.PI;
    while (a < -Math.PI) a += 2*Math.PI;
    if (Math.abs(a) < 0.4) {
      if (dist < minD) { minD = dist; closest = e; }
    }
  }
  if (closest) {
    closest.hp -= w.dmg;
    beep(200 + Math.random()*400, 'sine', 0.05, 0.1);
    if (closest.hp <= 0) {
      closest.dead = true;
      beep(80, 'triangle', 0.3, 0.1);
    }
  }
  updateHUD();
}

// Update
function update() {
  if (gameState !== 'playing') return;

  // Movement
  const moveSpd = 0.05;
  const rotSpd = 0.04;
  if (keys['w']) {
    player.x += Math.cos(player.dir) * moveSpd;
    player.y += Math.sin(player.dir) * moveSpd;
  }
  if (keys['s']) {
    player.x -= Math.cos(player.dir) * moveSpd;
    player.y -= Math.sin(player.dir) * moveSpd;
  }
  if (keys['a']) {
    player.dir -= rotSpd;
    player.planeX = -Math.sin(player.dir) * 0.66;
    player.planeY = Math.cos(player.dir) * 0.66;
  }
  if (keys['d']) {
    player.dir += rotSpd;
    player.planeX = -Math.sin(player.dir) * 0.66;
    player.planeY = Math.cos(player.dir) * 0.66;
  }

  // Wall collision
  if (MAP[Math.floor(player.y)][Math.floor(player.x)] === 1) {
    player.x -= Math.cos(player.dir) * 0.01;
    player.y -= Math.sin(player.dir) * 0.01;
  }

  // Enemy AI
  for (const e of entities) {
    if (e.type !== 'enemy' || e.dead) continue;
    const dx = player.x - e.x;
    const dy = player.y - e.y;
    const dist = Math.sqrt(dx*dx + dy*dy);
    if (dist < 0.8) {
      health -= 1;
      beep(60, 'sine', 0.1, 0.05);
      if (health <= 0) {
        health = 0;
        gameState = 'dead';
        msg('YOU DIED', 2500);
        setTimeout(respawn, 1500);
      }
    }
    if (dist > 0.8 && dist < 6) {
      e.x += (dx / dist) * 0.02;
      e.y += (dy / dist) * 0.02;
      if (MAP[Math.floor(e.y)][Math.floor(e.x)] === 1) {
        e.x -= (dx / dist) * 0.02;
        e.y -= (dy / dist) * 0.02;
      }
    }
  }

  // Pickups
  for (const e of entities) {
    if (e.collected) continue;
    const dx = player.x - e.x;
    const dy = player.y - e.y;
    if (Math.sqrt(dx*dx + dy*dy) < 0.8) {
      if (e.type === 'ammo') {
        ammo.scatter += 15;
        ammo.pulse += 5;
        beep(660, 'square', 0.1, 0.1);
        msg('AMMO PICKED');
      } else if (e.type === 'health') {
        health = Math.min(100, health + 25);
        beep(880, 'sine', 0.2, 0.1);
        msg('HEALTH +25');
      }
      e.collected = true;
    }
  }

  // Win condition
  const exit = entities.find(e => e.type === 'exit');
  if (exit && !exit.collected) {
    const dx = player.x - exit.x;
    const dy = player.y - exit.y;
    if (Math.sqrt(dx*dx + dy*dy) < 0.7) {
      exit.collected = true;
      gameState = 'won';
      msg('MISSION COMPLETE!', 5000);
      beep(880, 'triangle', 0.3, 0.1);
      setTimeout(() => {
        msg('PRESS R TO RETRY', 0);
      }, 3000);
    }
  }

  updateHUD();
}

// Draw
function render() {
  ctx.fillStyle = '#0a0a12';
  ctx.fillRect(0, 0, W, H);

  // Raycasting
  const planeX = player.planeX;
  const planeY = player.planeY;
  const dirX = Math.cos(player.dir);
  const dirY = Math.sin(player.dir);

  for (let x = 0; x < W; x++) {
    const cameraX = 2 * x / W - 1;
    const rayDirX = dirX + planeX * cameraX;
    const rayDirY = dirY + planeY * cameraX;

    let mapX = Math.floor(player.x);
    let mapY = Math.floor(player.y);

    let sideDistX, sideDistY, deltaDistX, deltaDistY;
    deltaDistX = Math.abs(1 / rayDirX);
    deltaDistY = Math.abs(1 / rayDirY);
    let hit = 0;
    let side = 0;
    let stepX, stepY;
    if (rayDirX < 0) { stepX = -1; sideDistX = (player.x - mapX) * deltaDistX; }
    else { stepX = 1; sideDistX = (mapX + 1.0 - player.x) * deltaDistX; }
    if (rayDirY < 0) { stepY = -1; sideDistY = (player.y - mapY) * deltaDistY; }
    else { stepY = 1; sideDistY = (mapY + 1.0 - player.y) * deltaDistY; }

    while (hit === 0) {
      if (sideDistX < sideDistY) {
        sideDistX += deltaDistX;
        mapX += stepX;
        side = 0;
      } else {
        sideDistY += deltaDistY;
        mapY += stepY;
        side = 1;
      }
      if (MAP[mapY] && MAP[mapY][mapX] > 0) hit = 1;
    }

    const perpWallDist = (side === 0) ? (mapX - player.x + (1 - stepX) / 2) / rayDirX : (mapY - player.y + (1 - stepY) / 2) / rayDirY;
    perpWallDist = Math.abs(perpWallDist);

    const lineHeight = Math.abs(H / perpWallDist);
    const drawStart = -lineHeight / 2 + H / 2;
    const drawEnd = lineHeight / 2 + H / 2;

    // Ceiling
    ctx.fillStyle = '#222233';
    ctx.fillRect(x, 0, 1, Math.max(0, drawStart));

    // Floor
    ctx.fillStyle = '#333344';
    ctx.fillRect(x, Math.min(H, drawEnd), 1, Math.max(0, H - drawEnd));

    // Wall
    const distShade = Math.max(0.3, 1 - perpWallDist / 8);
    const r = 160 * distShade;
    const g = 20 * distShade;
    const b = 20 * distShade;
    const color = `rgb(${r},${g},${b})`;
    const darkColor = `rgb(${r*0.6},${g*0.6},${b*0.6})`;
    ctx.fillStyle = side === 1 ? darkColor : color;
    ctx.fillRect(x, Math.max(0, drawStart), 1, Math.min(H, drawEnd) - Math.max(0, drawStart));
  }

  // Entities (sorted by distance)
  const sorted = entities.slice().sort((a, b) => {
    const da = Math.sqrt(Math.pow(a.x - player.x, 2) + Math.pow(a.y - player.y, 2));
    const db = Math.sqrt(Math.pow(b.x - player.x, 2) + Math.pow(b.y - player.y, 2));
    return db - da;
  });

  // Sprite projection setup
  const invDet = 1.0 / (planeX * dirY - dirX * planeY); // actually plane is perpendicular so det = -const, but we'll compute properly
  // Using the standard projection formula
  for (const e of sorted) {
    if (e.collected && e.type !== 'enemy' && e.type !== 'exit') continue;
    if (e.type === 'enemy' && e.dead) continue;

    const dx = e.x - player.x;
    const dy = e.y - player.y;
    const distance = Math.sqrt(dx*dx + dy*dy);
    if (distance < 0.5) continue;

    // Project
    const invDetReal = 1.0 / (planeX * dirY - dirX * planeY + 0.00001); // prevent div0
    const transformX = invDetReal * (dirY * dx - dirX * dy);
    const transformY = invDetReal * (-planeY * dx + planeX * dy);
    const spriteScreenX = Math.floor((W / 2) * (1 + transformX / transformY));

    const height = Math.abs(Math.floor(200 / distance));
    const width = Math.abs(Math.floor(height * 1.2));
    const drawY = Math.floor((H / 2) - height / 2);

    // Draw sprite
    if (e.type === 'enemy') {
      ctx.fillStyle = e.hp < 15 ? '#f0f' : '#f00';
      ctx.fillRect(spriteScreenX - width/2, drawY, width, height);
      // Simple face
      ctx.fillStyle = '#fff';
      const eyeSize = Math.max(2, Math.floor(width / 6));
      ctx.fillRect(spriteScreenX - width/4, drawY + height/4, eyeSize, eyeSize);
      ctx.fillRect(spriteScreenX + width/8, drawY + height/4, eyeSize, eyeSize);
      ctx.fillStyle = '#f00';
      ctx.fillRect(spriteScreenX - width/8, drawY + height/2.2, width/4, height/6);
    } else if (e.type === 'ammo') {
      if (!e.collected) {
        ctx.fillStyle = '#ff0';
        ctx.fillRect(spriteScreenX - 6, drawY, 12, 12);
        ctx.fillStyle = '#fff';
        ctx.font = '8px "Press Start 2P"';
        ctx.textAlign = 'center';
        ctx.fillText('A', spriteScreenX, drawY + 18);
      }
    } else if (e.type === 'health') {
      if (!e.collected) {
        ctx.fillStyle = '#0f0';
        ctx.fillRect(spriteScreenX - 8, drawY + 4, 16, 16);
        ctx.fillStyle = '#fff';
        ctx.fillRect(spriteScreenX - 6, drawY + 8, 12, 8);
      }
    } else if (e.type === 'exit') {
      const pulse = Math.sin(Date.now() / 200) * 20 + 30;
      ctx.fillStyle = `rgba(0,255,128,${0.4 + Math.sin(Date.now()/300)*0.4})`;
      ctx.fillRect(spriteScreenX - width/2, drawY + height/2 - pulse/2, width, pulse);
      ctx.fillStyle = '#fff';
      ctx.font = '8px "Press Start 2P"';
      ctx.textAlign = 'center';
      ctx.fillText('EXIT', spriteScreenX, drawY + height/2 + 20);
    }
  }

  // Weapon sprite at bottom
  const w = weapons[weaponIdx];
  const gunY = H - 80;
  const gunX = W/2;
  ctx.fillStyle = w.color;
  ctx.fillRect(gunX - 10, gunY + 40, 20, 30); // barrel
  ctx.fillRect(gunX - 20, gunY + 30, 40, 10); // handle
  ctx.fillStyle = '#fff';
  ctx.font = '10px "Press Start 2P"';
  ctx.textAlign = 'center';
  ctx.fillText(w.name, gunX, gunY);
}

function loop() {
  update();
  render();
  requestAnimationFrame(loop);
}
loop();
</script>
</body>
</html>
<!-- agent-meta {"model":"thinkingmachines/inkling","provider":"openrouter","persona":"basic","sessionId":"4d9dba2a-7033-415f-b782-79bbe9a08fb4","tokensIn":240,"tokensOut":15544,"tokensTotal":15784,"turns":1,"toolCalls":0,"failedToolCalls":0,"timestamp":"2026-07-18T20:25:05.008Z"} -->