DOOM
inkling · basic

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"} -->