DOOM
Ornith 1.5 35B A3B · full

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 42,116 bytes · SHA-256 5fa489be82e0
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>HAZARD: Sector 7 — Retro FPS</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body {
width: 100%; height: 100%;
background: #000;
overflow: hidden;
font-family: 'Courier New', monospace;
cursor: default;
user-select: none;
}
#wrap {
position: relative;
width: 100vw; height: 100vh;
display: flex; align-items: center; justify-content: center;
}
canvas#game {
background: #000;
image-rendering: pixelated;
image-rendering: crisp-edges;
box-shadow: 0 0 40px rgba(0,0,0,0.9), inset 0 0 20px rgba(0,0,0,0.8);
}
/* Overlays */
.overlay {
position: absolute; inset: 0;
display: flex; flex-direction: column;
align-items: center; justify-content: center;
text-align: center; color: #fff;
background: radial-gradient(ellipse at center, rgba(30,0,0,0.75), rgba(0,0,0,0.95));
z-index: 20;
}
.overlay.hidden { display: none; }
.title {
font-size: 64px; font-weight: bold;
letter-spacing: 6px;
color: #ff3b3b;
text-shadow: 0 0 20px #ff0000, 3px 3px 0 #7a0000;
margin-bottom: 6px;
font-family: Impact, 'Courier New', monospace;
}
.subtitle {
font-size: 20px; letter-spacing: 8px;
color: #ffcc44; text-shadow: 0 0 10px #aa7700;
margin-bottom: 40px;
}
.hint {
font-size: 16px; color: #cccccc; line-height: 1.9;
background: rgba(0,0,0,0.5); padding: 20px 30px;
border: 1px solid #552222; border-radius: 4px;
max-width: 520px;
}
.hint b { color: #ffcc44; }
.btn {
margin-top: 34px;
font-family: 'Courier New', monospace;
font-size: 22px; font-weight: bold;
letter-spacing: 2px;
color: #100; background: #ffcc44;
border: none; padding: 14px 42px;
cursor: pointer; border-radius: 3px;
box-shadow: 0 5px 0 #996b11;
transition: transform .05s;
}
.btn:hover { background: #ffd968; }
.btn:active { transform: translateY(4px); box-shadow: 0 1px 0 #996b11; }
.result { font-size: 56px; font-weight: bold; letter-spacing: 4px; margin-bottom: 10px; }
.win { color: #4dff7a; text-shadow: 0 0 24px #00ff55; }
.lose { color: #ff4d4d; text-shadow: 0 0 24px #ff0000; }
.stat { font-size: 18px; color: #ddd; margin: 4px; }
#hintKey {
position: absolute; bottom: 14px; left: 50%; transform: translateX(-50%);
color: #666; font-size: 12px; z-index: 5; letter-spacing: 1px;
}
.flash {
position: absolute; inset: 0; z-index: 15; pointer-events: none;
background: #f00; opacity: 0;
}
</style>
</head>
<body>
<div id="wrap">
<canvas id="game"></canvas>
<div id="damageFlash" class="flash"></div>
<div id="menu" class="overlay">
<div class="title">HAZARD</div>
<div class="subtitle">SECTOR 7</div>
<div class="hint">
<b>WASD</b> move · <b>Mouse</b> look · <b>Left Click</b> fire<br>
<b>Space</b> jump · <b>1/2/3</b> switch weapon · <b>R</b> reload<br><br>
Clear <b>SECTOR 7</b> of hostiles. Eliminate every monster and reach the <b>EXIT</b> gate.<br>
Grab <span style="color:#4dff7a">+ health</span> and <span style="color:#ffcc44">✦ ammo</span> packs along the way.
</div>
<button class="btn" id="startBtn">ENTER SECTOR 7</button>
</div>
<div id="result" class="overlay hidden">
<div class="result" id="resultText"></div>
<div class="stat" id="resultStat"></div>
<button class="btn" id="againBtn">PLAY AGAIN</button>
</div>
</div>
<script>
"use strict";
(function () {
// ============================================================
// HAZARD: SECTOR 7 — a raycasting retro FPS
// ============================================================
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
// Logical resolution (rendered then scaled up for pixel look)
const W = 640, H = 400;
canvas.width = W; canvas.height = H;
function fitToScreen() {
const s = Math.min(window.innerWidth / W, window.innerHeight / H);
canvas.style.width = (W * s) + 'px';
canvas.style.height = (H * s) + 'px';
}
window.addEventListener('resize', fitToScreen);
fitToScreen();
// ---------- Audio (WebAudio, procedural) ----------
let AC = null;
function initAudio() {
if (!AC) {
AC = new (window.AudioContext || window.webkitAudioContext)();
}
if (AC.state === 'suspended') AC.resume();
}
function tone(freq, dur, type = 'square', vol = 0.15, slideTo = null) {
if (!AC) return;
const t = AC.currentTime;
const o = AC.createOscillator();
const g = AC.createGain();
o.type = type; o.frequency.setValueAtTime(freq, t);
if (slideTo) o.frequency.exponentialRampToValueAtTime(Math.max(1, slideTo), t + dur);
g.gain.setValueAtTime(vol, t);
g.gain.exponentialRampToValueAtTime(0.0001, t + dur);
o.connect(g); g.connect(AC.destination);
o.start(t); o.stop(t + dur + 0.02);
}
function noise(dur, vol = 0.3) {
if (!AC) return;
const t = AC.currentTime;
const buf = AC.createBuffer(1, AC.sampleRate * dur, AC.sampleRate);
const d = buf.getChannelData(0);
for (let i = 0; i < d.length; i++) d[i] = (Math.random() * 2 - 1) * (1 - i / d.length);
const src = AC.createBufferSource(); src.buffer = buf;
const g = AC.createGain(); g.gain.value = vol;
const filt = AC.createBiquadFilter(); filt.type = 'lowpass'; filt.frequency.value = 1200;
src.connect(filt); filt.connect(g); g.connect(AC.destination);
src.start(t);
}
const SFX = {
pistol() { tone(680, 0.09, 'square', 0.16, 180); noise(0.06, 0.12); },
smg() { tone(520, 0.05, 'square', 0.11, 140); noise(0.04, 0.09); },
shotgun(){ tone(220, 0.18, 'sawtooth', 0.22, 60); noise(0.14, 0.28); },
hurt() { tone(160, 0.18, 'sawtooth', 0.22, 60); },
kill() { tone(300, 0.12, 'square', 0.18, 90); noise(0.1, 0.14); },
pickup() { tone(880, 0.08, 'sine', 0.16, 1320); tone(1320, 0.1, 'sine', 0.14, 1760); },
empty() { tone(120, 0.06, 'square', 0.1, 80); },
reload() { tone(300, 0.08, 'triangle', 0.12, 440); tone(500, 0.08, 'triangle', 0.12, 700); },
gunswap(){ tone(420, 0.07, 'triangle', 0.12, 600); },
win() { [523,659,784,1046].forEach((f,i)=>setTimeout(()=>tone(f,0.25,'square',0.18),i*120)); },
lose() { [400,320,250,180].forEach((f,i)=>setTimeout(()=>tone(f,0.3,'sawtooth',0.18),i*160)); },
enemyhurt(){ tone(400, 0.08, 'square', 0.12, 200); },
door() { tone(140, 0.3, 'sawtooth', 0.15, 440); },
};
// ---------- Map ----------
// 1..4 wall types (color variants), 0 floor/air. 9 = exit gate (walkable, triggers win)
const MAP = [
"1111111111111111111111111",
"1000000001000000000000001",
"1022222001003333000044441",
"1020002001003003000040041",
"1020222200003000002222001",
"1020200000003004002002001",
"1000200001000004002002001",
"1110200001001114000000001",
"1000000001001000000444401",
"1033333000001000000400001",
"1030003001111000000400001",
"1030003001000000000400901",
"1000003001000111110004001",
"1044443000001000010004001",
"1000000001111000010004001",
"1111111111111111111111111",
];
const MAP_H = MAP.length, MAP_W = MAP[0].length;
function cell(x, y) {
if (x < 0 || y < 0 || x >= MAP_W || y >= MAP_H) return 1;
return parseInt(MAP[y | 0][x | 0], 10);
}
function isWall(x, y) { return cell(x, y) > 0 && cell(x, y) !== 9; }
// locate the exit gate (cell value 9) and spawn the player at a floor tile
function locateExit() {
for (let y = 0; y < MAP_H; y++) {
for (let x = 0; x < MAP_W; x++) {
if (cell(x, y) === 9) { exitPos = { x: x + 0.5, y: y + 0.5 }; return; }
}
}
exitPos = { x: 0, y: 0 };
}
// ---------- Entities ----------
let enemies = [], pickups = [], particles = [];
let exitPos = { x: 0, y: 0 };
// Spawn points for enemies (grid coords)
const ENEMY_SPAWNS = [
{ x: 3.5, y: 4.5 }, { x: 10.5, y: 3.5 }, { x: 6.5, y: 8.5 },
{ x: 21.5, y: 4.5 }, { x: 14.5, y: 6.5 }, { x: 4.5, y: 11.5 },
{ x: 20.5, y: 11.5 }, { x: 9.5, y: 13.5 }, { x: 15.5, y: 13.5 },
{ x: 22.5, y: 13.5 }, { x: 7.5, y: 6.5 }, { x: 18.5, y: 8.5 },
];
const PICKUP_SPAWNS = [
{ x: 2.5, y: 2.5, type: 'health' }, { x: 12.5, y: 5.5, type: 'ammo' },
{ x: 5.5, y: 12.5, type: 'health' }, { x: 19.5, y: 3.5, type: 'ammo' },
{ x: 24.5, y: 9.5, type: 'health' }, { x: 10.5, y: 10.5, type: 'ammo' },
{ x: 3.5, y: 8.5, type: 'ammo' }, { x: 22.5, y: 6.5, type: 'health' },
];
// ---------- Weapons ----------
const WEAPONS = [
{ name: 'PISTOL', mag: Infinity, maxMag: Infinity, reserve: Infinity, dmg: 1, rate: 260, spread: 0.0, auto: false, sound: 'pistol', pellets: 1, kick: 0.03 },
{ name: 'SMG', mag: 40, maxMag: 40, reserve: 200, dmg: 0.8, rate: 85, spread: 0.09, auto: true, sound: 'smg', pellets: 1, kick: 0.02 },
{ name: 'SHOTGUN', mag: 8, maxMag: 8, reserve: 40, dmg: 0.7, rate: 620, spread: 0.22, auto: false, sound: 'shotgun', pellets: 8, kick: 0.08 },
];
// ---------- Player state ----------
let player = null;
function freshPlayer() {
return {
x: 2.5, y: 2.5, dir: 0.6,
health: 100, maxHealth: 100,
dead: false,
weapons: [
{ ...WEAPONS[0], mag: WEAPONS[0].mag, reserve: WEAPONS[0].reserve },
{ ...WEAPONS[1], mag: WEAPONS[1].mag, reserve: WEAPONS[1].reserve },
{ ...WEAPONS[2], mag: WEAPONS[2].mag, reserve: WEAPONS[2].reserve },
],
cur: 0,
jumpT: 0, jumpVel: 0, jumpHeight: 0,
hurtT: 0, muzzleT: 0, fireT: 0, bobT: 0, bobY: 0,
reloading: 0, hitFlash: 0,
};
}
// ---------- Game state ----------
let state = 'menu'; // menu, playing, dead, win
let kills = 0, totalEnemies = ENEMY_SPAWNS.length;
let lastTime = 0, dt = 0;
let msgText = '', msgTimer = 0, msgColor = '#fff';
const keys = {};
let mouseLocked = false;
function resetGame() {
locateExit();
player = freshPlayer();
// ensure spawn is a walkable tile
while (isWall(player.x, player.y)) { player.x += 0.5; if (player.x > MAP_W - 1) { player.x = 0.5; player.y += 0.5; } }
enemies = ENEMY_SPAWNS.map((s, i) => makeEnemy(s.x, s.y, i));
pickups = PICKUP_SPAWNS.map(p => ({ x: p.x, y: p.y, type: p.type, t: 0, bob: Math.random() * 6 }));
particles = [];
kills = 0;
state = 'playing';
player.msg = '';
msgText = ''; msgTimer = 0;
document.getElementById('menu').classList.add('hidden');
document.getElementById('result').classList.add('hidden');
if (canvas.requestPointerLock) canvas.requestPointerLock();
requestAnimationFrame(loop);
}
// ---------- Enemies ----------
function makeEnemy(x, y, id) {
return {
x, y, id, alive: true,
health: 30, maxHealth: 30,
speed: 1.6 + Math.random() * 0.6,
fireT: 300 + Math.random() * 800,
hurtT: 0, deathT: 0,
state: 'idle', // idle, chase, attack, hurt
lastSeen: 0,
painFlash: 0,
anim: 0,
};
}
function enemySpeed(e) {
return e.speed * (e.health < e.maxHealth * 0.4 ? 1.25 : 1.0);
}
// ---------- Line of sight ----------
function hasLOS(x0, y0, x1, y1) {
const dx = x1 - x0, dy = y1 - y0;
const dist = Math.hypot(dx, dy);
const steps = Math.ceil(dist * 8);
for (let i = 1; i < steps; i++) {
const t = i / steps;
if (isWall(x0 + dx * t, y0 + dy * t)) return false;
}
return true;
}
// ---------- Input ----------
window.addEventListener('keydown', e => {
keys[e.code] = true;
if (state === 'playing' && !player.dead) {
if (e.code === 'Digit1') switchWeapon(0);
if (e.code === 'Digit2') switchWeapon(1);
if (e.code === 'Digit3') switchWeapon(2);
if (e.code === 'KeyR') reload();
if (e.code === 'Space') { e.preventDefault(); jump(); }
}
});
window.addEventListener('keyup', e => { keys[e.code] = false; });
canvas.addEventListener('click', () => {
if (state === 'playing' && !mouseLocked) {
if (canvas.requestPointerLock) canvas.requestPointerLock();
}
});
document.addEventListener('pointerlockchange', () => {
mouseLocked = (document.pointerLockElement === canvas);
});
document.addEventListener('mousemove', e => {
if (mouseLocked && state === 'playing' && !player.dead) {
player.dir += e.movementX * 0.0022;
}
});
document.addEventListener('mousedown', e => {
if (e.button === 0) { keys['fire'] = true; tryFire(); }
});
document.addEventListener('mouseup', e => {
if (e.button === 0) keys['fire'] = false;
});
function switchWeapon(idx) {
if (idx === player.cur || state !== 'playing') return;
player.cur = idx; player.reloading = 0;
SFX.gunswap();
}
function reload() {
const w = player.weapons[player.cur];
if (w.mag === w.maxMag || w.reserve === 0 || w.reserve === Infinity || player.reloading) return;
player.reloading = 900; // ms
SFX.reload();
}
function jump() {
if (player.jumpHeight <= 0.01) {
player.jumpVel = 1.9;
}
}
// ---------- Firing ----------
let lastFireTime = 0;
function tryFire() {
if (state !== 'playing' || player.dead) return;
const w = player.weapons[player.cur];
if (player.reloading > 0) return;
const now = performance.now();
if (now - lastFireTime < w.rate) return;
if (w.mag === 0) {
SFX.empty();
if (w.reserve > 0 && w.maxMag !== Infinity) { player.reloading = 900; SFX.reload(); }
return;
}
lastFireTime = now;
player.fireT = w.rate;
player.muzzleT = 60;
if (w.mag !== Infinity) w.mag--;
SFX[w.sound]();
applyRecoil(w.kick);
// Hitscan: multiple pellets for shotgun
for (let p = 0; p < w.pellets; p++) {
const spread = (Math.random() - 0.5) * 2 * w.spread;
hitscan(player.x, player.y, player.dir + spread, w.dmg);
}
}
function applyRecoil(kick) {
player.dir += kick * (Math.random() - 0.3);
}
function hitscan(ox, oy, angle, dmg) {
const dx = Math.cos(angle), dy = Math.sin(angle);
// Find nearest enemy along ray (clamped by wall distance)
let bestDist = Infinity, best = null;
for (const e of enemies) {
if (!e.alive) continue;
// project enemy center onto ray
const ex = e.x - ox, ey = e.y - oy;
const t = ex * dx + ey * dy;
if (t <= 0) continue;
const cx = ox + dx * t, cy = oy + dy * t;
const distToRay = Math.hypot(cx - e.x, cy - e.y);
if (distToRay < 0.35 && t < bestDist) {
if (hasLOS(ox, oy, e.x, e.y)) { bestDist = t; best = e; }
}
}
// muzzle flash particle toward nearest wall too, but we mainly hit enemy
if (best) {
damageEnemy(best, dmg);
}
}
function damageEnemy(e, dmg) {
e.health -= dmg;
e.painFlash = 120;
e.state = 'chase';
e.lastSeen = performance.now();
SFX.enemyhurt();
if (e.health <= 0) {
e.alive = false; e.state = 'dead'; e.deathT = performance.now();
kills++;
SFX.kill();
spawnBlood(e.x, e.y, 14);
}
}
function spawnBlood(x, y, n) {
for (let i = 0; i < n; i++) {
particles.push({
x, y, vx: (Math.random() - 0.5) * 2, vy: (Math.random() - 0.5) * 2,
life: 400 + Math.random() * 300, max: 700, color: '#b00', size: 2 + Math.random() * 2,
});
}
}
// ---------- Player damage / death ----------
function damagePlayer(dmg) {
if (player.dead) return;
player.health -= dmg;
player.hitFlash = 120;
player.hurtT = 150;
SFX.hurt();
flashDamage();
if (player.health <= 0) {
player.health = 0;
die();
}
}
function die() {
player.dead = true;
state = 'dead';
if (document.pointerLockElement) document.exitPointerLock();
SFX.lose();
setTimeout(showResult, 1400);
}
function win() {
if (state !== 'playing') return;
state = 'win';
if (document.pointerLockElement) document.exitPointerLock();
SFX.win();
setTimeout(showResult, 1200);
}
function showResult() {
const rt = document.getElementById('resultText');
const rs = document.getElementById('resultStat');
const won = state === 'win';
rt.textContent = won ? 'MISSION ACCOMPLISHED' : 'YOU DIED';
rt.className = 'result ' + (won ? 'win' : 'lose');
rs.textContent = won
? `Hostiles eliminated: ${kills}/${totalEnemies} · Health remaining: ${Math.ceil(player.health)}`
: `Hostiles eliminated: ${kills}/${totalEnemies}`;
document.getElementById('result').classList.remove('hidden');
}
function respawn() {
resetGame();
}
function flashDamage() {
const f = document.getElementById('damageFlash');
f.style.transition = 'none';
f.style.opacity = '0.55';
requestAnimationFrame(() => {
f.style.transition = 'opacity .35s';
f.style.opacity = '0';
});
}
// ---------- Update ----------
function update(dtMs) {
const dtSec = dtMs / 1000;
// timers
if (player.muzzleT > 0) player.muzzleT -= dtMs;
if (player.fireT > 0) player.fireT -= dtMs;
if (player.hurtT > 0) player.hurtT -= dtMs;
if (player.hitFlash > 0) player.hitFlash -= dtMs;
if (msgTimer > 0) { msgTimer -= dtMs; if (msgTimer <= 0) msgText = ''; }
// auto-fire for automatic weapons
if (keys['fire'] && player.weapons[player.cur].auto) tryFire();
// reload handling
if (player.reloading > 0) {
player.reloading -= dtMs;
if (player.reloading <= 0) finishReload();
}
// jump physics
if (player.jumpT > 0 || player.jumpHeight > 0.01) {
player.jumpHeight += player.jumpVel * dtSec;
player.jumpVel -= 6.5 * dtSec;
if (player.jumpHeight <= 0) { player.jumpHeight = 0; player.jumpVel = 0; player.jumpT = 0; }
else player.jumpT = 1;
}
// Movement
const moveSpeed = 3.2 * dtSec;
const cos = Math.cos(player.dir), sin = Math.sin(player.dir);
let mx = 0, my = 0;
if (keys['KeyW'] || keys['ArrowUp']) { mx += cos; my += sin; }
if (keys['KeyS'] || keys['ArrowDown']) { mx -= cos; my -= sin; }
if (keys['KeyA']) { mx += sin; my -= cos; }
if (keys['KeyD']) { mx -= sin; my += cos; }
if (keys['ArrowLeft']) player.dir -= 2.4 * dtSec;
if (keys['ArrowRight']) player.dir += 2.4 * dtSec;
const len = Math.hypot(mx, my);
if (len > 0) {
mx = mx / len * moveSpeed; my = my / len * moveSpeed;
player.bobT += dtSec * 10;
}
// collide (axis separated)
const rad = 0.28;
if (!isWall(player.x + mx + Math.sign(mx) * rad, player.y)) player.x += mx;
if (!isWall(player.x, player.y + my + Math.sign(my) * rad)) player.y += my;
// step-based bob
player.bobY = Math.sin(player.bobT) * 0.03 * (len > 0 ? 1 : 0);
// pickups
for (const pk of pickups) {
pk.bob += dtSec * 4;
if (Math.hypot(player.x - pk.x, player.y - pk.y) < 0.4) {
if (pk.type === 'health') {
if (player.health >= player.maxHealth) continue;
player.health = Math.min(player.maxHealth, player.health + 35);
SFX.pickup();
showMsg('+35 HEALTH', '#4dff7a');
pk.active = false;
} else {
for (const w of player.weapons) {
if (w.reserve !== Infinity) w.reserve = Math.min(w.maxMag * 4, w.reserve + w.maxMag);
}
player.weapons[0].mag = player.weapons[0].maxMag; // pistol always full
SFX.pickup();
showMsg('AMMO SUPPLIED', '#ffcc44');
pk.active = false;
}
}
}
pickups = pickups.filter(p => p.active !== false);
// enemies AI
const now = performance.now();
for (const e of enemies) {
if (e.alive) {
e.painFlash = Math.max(0, e.painFlash - dtMs);
e.anim += dtSec * 6;
const dx = player.x - e.x, dy = player.y - e.y;
const dist = Math.hypot(dx, dy);
const see = hasLOS(e.x, e.y, player.x, player.y) && dist < 12;
if (see) { e.lastSeen = now; e.state = 'chase'; }
else if (now - e.lastSeen > 2500) e.state = 'idle';
if (e.state === 'chase') {
const ang = Math.atan2(dy, dx);
const sp = enemySpeed(e) * dtSec;
const nx = e.x + Math.cos(ang) * sp, ny = e.y + Math.sin(ang) * sp;
if (!isWall(nx, e.y)) e.x = nx;
if (!isWall(e.x, ny)) e.y = ny;
// attack
e.fireT -= dtMs;
if (see && e.fireT <= 0 && dist < 10) {
e.fireT = 900 + Math.random() * 600;
// projectile hits player unless dodging
damagePlayer(6 + Math.random() * 6);
// visible tracer handled in render
e.tracer = { t: 80, ax: player.x, ay: player.y };
}
} else if (e.state === 'idle') {
// idle wander
e.wander = (e.wander || 0) - dtMs;
if (e.wander <= 0) { e.wander = 1500 + Math.random() * 2000; e.wanderAng = Math.random() * 6.28; }
const nx = e.x + Math.cos(e.wanderAng) * enemySpeed(e) * 0.4 * dtSec;
const ny = e.y + Math.sin(e.wanderAng) * enemySpeed(e) * 0.4 * dtSec;
if (!isWall(nx, e.y)) e.x = nx; if (!isWall(e.x, ny)) e.y = ny;
}
} else {
// death animation timer cleanup
}
if (e.tracer) { e.tracer.t -= dtMs; if (e.tracer.t <= 0) delete e.tracer; }
}
// particles
for (const pt of particles) {
pt.life -= dtMs;
pt.x += pt.vx * dtSec; pt.y += pt.vy * dtSec;
pt.vy += 5 * dtSec;
}
particles = particles.filter(p => p.life > 0);
// win check: all dead AND reached exit
const remaining = enemies.filter(e => e.alive).length;
if (remaining === 0) {
if (Math.hypot(player.x - exitPos.x, player.y - exitPos.y) < 0.6) {
win();
} else if (msgTimer <= 0) {
showMsg('ALL HOSTILES DOWN — REACH THE EXIT!', '#ffcc44', 4000);
}
} else if (msgTimer <= 0 && kills > 0) {
// nothing
}
}
function finishReload() {
const w = player.weapons[player.cur];
if (w.mag === w.maxMag || w.reserve === 0 || w.reserve === Infinity) return;
const need = w.maxMag - w.mag;
const take = Math.min(need, w.reserve);
w.mag += take; w.reserve -= take;
}
function showMsg(t, color, dur = 2500) { msgText = t; msgColor = color || '#fff'; msgTimer = dur; }
// ---------- Rendering ----------
const zBuffer = new Float32Array(W);
function render() {
// ceiling + floor gradient
const cg = ctx.createLinearGradient(0, 0, 0, H / 2);
cg.addColorStop(0, '#2a2a33'); cg.addColorStop(1, '#12121a');
ctx.fillStyle = cg; ctx.fillRect(0, 0, W, H / 2);
const fg = ctx.createLinearGradient(0, H / 2, 0, H);
fg.addColorStop(0, '#3a2f24'); fg.addColorStop(1, '#1a1410');
ctx.fillStyle = fg; ctx.fillRect(0, H / 2, W, H / 2);
const p = player;
const fov = Math.PI / 3;
const jump = p.jumpHeight;
// Walls
for (let x = 0; x < W; x++) {
const camX = 2 * x / W - 1;
const rayX = p.dir - Math.tan(camX) * fov; // careful, see below
// cast
}
// Proper raycast:
const dirX = Math.cos(p.dir), dirY = Math.sin(p.dir);
const planeLen = Math.tan(fov) * 0.9;
const planeX = -dirY * planeLen, planeY = dirX * planeLen; // camera plane perpendicular
for (let x = 0; x < W; x++) {
const camX = 2 * x / W - 1;
const rayDirX = dirX + planeX * camX;
const rayDirY = dirY + planeY * camX;
let mapX = p.x | 0, mapY = p.y | 0;
const deltaX = Math.abs(1 / rayDirX), deltaY = Math.abs(1 / rayDirY);
let stepX, stepY, sideX, sideY;
if (rayDirX < 0) { stepX = -1; sideX = (p.x - mapX) * deltaX; }
else { stepX = 1; sideX = (mapX + 1 - p.x) * deltaX; }
if (rayDirY < 0) { stepY = -1; sideY = (p.y - mapY) * deltaY; }
else { stepY = 1; sideY = (mapY + 1 - p.y) * deltaY; }
let hit = 0, side = 0, dist = 0, cellVal = 0, wallX = 0;
let guard = 0;
while (hit === 0 && guard++ < 64) {
if (sideX < sideY) { sideX += deltaX; mapX += stepX; side = 0; }
else { sideY += deltaY; mapY += stepY; side = 1; }
cellVal = cell(mapX, mapY);
if (cellVal > 0) hit = 1;
}
if (side === 0) dist = (mapX - p.x + (1 - stepX) / 2) / rayDirX;
else dist = (mapY - p.y + (1 - stepY) / 2) / rayDirY;
zBuffer[x] = dist;
if (cellVal === 9) continue; // exit gate handled as floor tile (walkable)
const lineH = Math.min(H * 4, (H / dist));
const y0 = (H - lineH) / 2 + jump * H / dist;
const y1 = (H + lineH) / 2 + jump * H / dist;
// wall color by type + shading
const shade = side === 1 ? 0.68 : 1.0;
const fog = Math.max(0.25, 1 - dist / 14);
const col = wallColor(cellVal, shade * fog);
ctx.fillStyle = col;
ctx.fillRect(x, y0, 1, lineH);
}
// Draw exit gate glow if visible
drawExitGate(dirX, dirY, planeX, planeY, fov, jump);
// Sprites (enemies + pickups), sorted far→near
const sprites = [];
for (const e of enemies) sprites.push({ x: e.x, y: e.y, ref: e, kind: 'enemy' });
for (const pk of pickups) sprites.push({ x: pk.x, y: pk.y, ref: pk, kind: 'pickup' });
sprites.forEach(s => s.dist = (s.x - p.x) ** 2 + (s.y - p.y) ** 2);
sprites.sort((a, b) => b.dist - a.dist);
for (const s of sprites) {
drawSprite(s, p, dirX, dirY, planeX, planeY, fov, zBuffer, jump);
}
// Particles (3D-ish)
for (const pt of particles) {
drawParticle(pt, p, fov, planeX, planeY, dirX, dirY);
}
drawHUD();
drawMinimap();
drawWeapon();
drawCrosshair();
}
function wallColor(v, shade) {
const base = {
1: [120, 60, 55], // brick red
2: [110, 110, 125], // steel grey
3: [90, 95, 120], // blue steel
4: [120, 95, 60], // bronze/brown
}[v] || [120, 60, 55];
const r = Math.round(base[0] * shade);
const g = Math.round(base[1] * shade);
const b = Math.round(base[2] * shade);
return `rgb(${r},${g},${b})`;
}
function drawExitGate(dirX, dirY, planeX, planeY, fov, jump) {
// exit at grid cell 9 (value 9). Find it
if (exitPos.x === 0 && exitPos.y === 0) return;
const dx = exitPos.x - player.x, dy = exitPos.y - player.y;
// relative to dir
const invDet = 1.0; // plane and dir orthonormal-ish; use dot
const planeDot = dirX * planeX + dirY * planeY; // ~0
const tX = dx, tY = dy;
const fwd = dx * dirX + dy * dirY;
const right = dx * planeX + dy * planeY;
if (fwd <= 0.1) return;
const screenX = 0.5 + (right / fwd) / (2 * Math.tan(fov));
const scale = H / fwd;
if (screenX < -0.5 || screenX > 1.5) return;
const y0 = (H - scale) / 2 + jump * H / fwd;
// glow
ctx.save();
const grd = ctx.createRadialGradient(screenX * W, y0 + scale / 2, 4, screenX * W, y0 + scale / 2, scale / 1.5);
grd.addColorStop(0, 'rgba(80,255,120,0.9)');
grd.addColorStop(1, 'rgba(20,120,50,0)');
ctx.fillStyle = grd;
ctx.fillRect(screenX * W - scale, y0 - scale / 3, scale * 2, scale * 1.6);
ctx.fillStyle = '#2f2';
ctx.fillRect(screenX * W - scale / 3, y0 + scale / 4, scale / 1.5, scale / 2);
ctx.fillStyle = '#0a0';
ctx.font = 'bold ' + Math.floor(scale / 5) + 'px Courier New';
ctx.textAlign = 'center';
ctx.fillText('EXIT', screenX * W, y0 + scale / 4 - 4);
ctx.restore();
}
// ---- Sprite drawing with simple hand-drawn sprites ----
function drawSprite(s, p, dirX, dirY, planeX, planeY, fov, zBuffer, jump) {
const relX = s.x - p.x, relY = s.y - p.y;
const invDet = 1.0 / (planeX * dirY - dirX * planeY);
const transformX = invDet * (dirY * relX - dirX * relY);
const transformY = invDet * (-planeY * relX + planeX * relY);
if (transformY <= 0.1) return; // behind camera
const screenX = (W / 2) * (1 + transformX / transformY);
const scale = H / transformY;
const pitch = jump * H / transformY; // jump raises sprites
if (s.kind === 'enemy') drawEnemySprite(s.ref, screenX, scale, transformY, zBuffer, pitch);
else drawPickupSprite(s.ref, screenX, scale, transformY, zBuffer, pitch);
}
function drawEnemySprite(e, screenX, scale, depth, zBuffer, pitch) {
const w = scale * 0.7;
const h = scale;
const x = screenX - w / 2;
const y = (H - h) / 2 + pitch;
const frac = scale / W; // per-column sample spacing
// clip by zBuffer
const leftCol = Math.max(0, Math.floor(x));
const rightCol = Math.min(W - 1, Math.ceil(x + w));
let anyVisible = false;
for (let c = leftCol; c <= rightCol; c++) {
if (depth < zBuffer[c]) { anyVisible = true; break; }
}
if (!anyVisible) return;
ctx.save();
ctx.beginPath();
ctx.rect(Math.max(0, x), 0, Math.min(w, W - Math.max(0, x)), H);
ctx.clip();
// death pose: lie down
const dying = !e.alive;
const flash = e.painFlash > 0;
const walk = Math.sin(e.anim) * w * 0.06;
// body color
const skin = flash ? '#fff' : '#9c4a3a';
const body = flash ? '#fff' : '#5a2d2d';
const dark = flash ? '#ddd' : '#3a1a1a';
// draw columns for a pseudo-3D flat look
if (dying) {
// fallen
ctx.fillStyle = dark;
ctx.fillRect(x, y + h * 0.62, w, h * 0.16);
ctx.fillStyle = skin;
ctx.beginPath(); ctx.arc(x + w * 0.25, y + h * 0.62, w * 0.14, 0, 6.28); ctx.fill();
} else {
// legs
ctx.fillStyle = dark;
ctx.fillRect(x + w * 0.22, y + h * 0.62, w * 0.2, h * 0.22 + walk);
ctx.fillRect(x + w * 0.58, y + h * 0.62, w * 0.2, h * 0.22 - walk);
// torso
ctx.fillStyle = body;
ctx.fillRect(x + w * 0.18, y + h * 0.28, w * 0.64, h * 0.38);
// arms
ctx.fillStyle = skin;
ctx.fillRect(x + w * 0.06, y + h * 0.30, w * 0.14, h * 0.30 + walk * 0.5);
ctx.fillRect(x + w * 0.80, y + h * 0.30, w * 0.14, h * 0.30 - walk * 0.5);
// head
ctx.fillStyle = skin;
ctx.beginPath();
ctx.arc(x + w * 0.5, y + h * 0.20, w * 0.20, 0, 6.28);
ctx.fill();
// eyes (glowing)
ctx.fillStyle = flash ? '#ff0' : '#ff3020';
ctx.beginPath();
ctx.arc(x + w * 0.42, y + h * 0.19, w * 0.045, 0, 6.28);
ctx.arc(x + w * 0.58, y + h * 0.19, w * 0.045, 0, 6.28);
ctx.fill();
// mouth / teeth
ctx.fillStyle = dark;
ctx.fillRect(x + w * 0.44, y + h * 0.26, w * 0.12, h * 0.03);
// health bar above if hurt
if (e.health < e.maxHealth) {
ctx.fillStyle = '#000';
ctx.fillRect(x + w * 0.2, y + h * 0.06, w * 0.6, h * 0.02);
ctx.fillStyle = '#f33';
ctx.fillRect(x + w * 0.2, y + h * 0.06, w * 0.6 * (e.health / e.maxHealth), h * 0.02);
}
}
ctx.restore();
}
function drawPickupSprite(pk, screenX, scale, depth, zBuffer, pitch) {
const s = scale * 0.35;
const x = screenX, y = H / 2 + pitch + scale * 0.18 + Math.sin(pk.bob) * s * 0.4;
// check column center
const cx = Math.round(screenX);
if (cx < 0 || cx >= W || depth >= zBuffer[cx]) return;
ctx.save();
if (pk.type === 'health') {
// green medkit
ctx.fillStyle = '#0a0';
ctx.strokeStyle = '#050';
ctx.lineWidth = Math.max(1, s * 0.06);
ctx.fillRect(x - s * 0.6, y - s * 0.5, s * 1.2, s * 1.0);
ctx.strokeRect(x - s * 0.6, y - s * 0.5, s * 1.2, s * 1.0);
ctx.fillStyle = '#fff';
ctx.fillRect(x - s * 0.08, y - s * 0.3, s * 0.16, s * 0.6);
ctx.fillRect(x - s * 0.3, y - s * 0.08, s * 0.6, s * 0.16);
} else {
// ammo box (yellow)
ctx.fillStyle = '#da0';
ctx.strokeStyle = '#860';
ctx.lineWidth = Math.max(1, s * 0.06);
ctx.fillRect(x - s * 0.6, y - s * 0.4, s * 1.2, s * 0.85);
ctx.strokeRect(x - s * 0.6, y - s * 0.4, s * 1.2, s * 0.85);
ctx.fillStyle = '#640';
ctx.fillRect(x - s * 0.5, y - s * 0.3, s * 1.0, s * 0.12);
ctx.fillStyle = '#fff';
ctx.font = `bold ${Math.floor(s * 0.7)}px Courier New`;
ctx.textAlign = 'center';
ctx.fillText('✦', x, y + s * 0.28);
}
ctx.restore();
}
function drawParticle(pt, p, fov, planeX, planeY, dirX, dirY) {
const relX = pt.x - p.x, relY = pt.y - p.y;
const invDet = 1.0 / (planeX * dirY - dirX * planeY);
const tx = invDet * (dirY * relX - dirX * relY);
const ty = invDet * (-planeY * relX + planeX * relY);
if (ty <= 0.05) return;
const sx = (W / 2) * (1 + tx / ty);
const sz = Math.max(1, (H / ty) * 0.02);
const sy = H / 2 - (ty === 0 ? 0 : 0) + H / ty * 0.0; // particles at chest height
const alpha = Math.max(0, pt.life / pt.max);
ctx.fillStyle = `rgba(180,20,20,${alpha})`;
ctx.fillRect(sx - sz / 2, sy - sz / 2, sz, sz);
}
// ---------- HUD ----------
function drawHUD() {
// HUD background band
ctx.fillStyle = 'rgba(20,10,10,0.82)';
ctx.fillRect(0, H - 46, W, 46);
ctx.strokeStyle = '#6b2b2b';
ctx.lineWidth = 2;
ctx.strokeRect(1, H - 46, W - 2, 44);
// Health (left)
ctx.textAlign = 'left';
ctx.font = 'bold 13px Courier New';
ctx.fillStyle = '#888';
ctx.fillText('HEALTH', 18, H - 30);
ctx.font = 'bold 30px Courier New';
ctx.fillStyle = player.health > 30 ? '#4dff7a' : '#ff4d4d';
ctx.fillText(Math.ceil(player.health) + '%', 18, H - 5);
// health bar
ctx.fillStyle = '#300';
ctx.fillRect(96, H - 34, 130, 12);
const hp = Math.max(0, player.health / player.maxHealth);
ctx.fillStyle = hp > 0.5 ? '#3f6' : hp > 0.25 ? '#fb0' : '#f33';
ctx.fillRect(96, H - 34, 130 * hp, 12);
// Ammo (right)
const w = player.weapons[player.cur];
ctx.textAlign = 'right';
ctx.font = 'bold 13px Courier New';
ctx.fillStyle = '#888';
ctx.fillText(w.name, W - 18, H - 30);
ctx.font = 'bold 30px Courier New';
ctx.fillStyle = (w.mag === 0) ? '#ff4d4d' : '#ffcc44';
const ammoStr = w.mag === Infinity ? '∞' : (w.mag + ' / ' + w.reserve);
ctx.fillText(ammoStr, W - 18, H - 5);
if (player.reloading > 0) {
ctx.fillStyle = '#fff';
ctx.font = 'bold 16px Courier New';
ctx.textAlign = 'center';
ctx.fillText('RELOADING...', W / 2, H - 20);
}
// Kill counter (center-top)
ctx.textAlign = 'center';
ctx.font = 'bold 16px Courier New';
ctx.fillStyle = '#ddd';
ctx.fillText('KILLS ' + kills + ' / ' + totalEnemies, W / 2, 26);
const remaining = enemies.filter(e => e.alive).length;
ctx.font = '12px Courier New';
ctx.fillStyle = remaining === 0 ? '#4dff7a' : '#ffcc44';
ctx.fillText(remaining === 0 ? 'EXIT UNLOCKED' : remaining + ' REMAINING', W / 2, 42);
// current message
if (msgTimer > 0) {
ctx.globalAlpha = Math.min(1, msgTimer / 400);
ctx.font = 'bold 20px Courier New';
ctx.fillStyle = msgColor;
ctx.textAlign = 'center';
ctx.fillText(msgText, W / 2, H / 2 - 60);
ctx.globalAlpha = 1;
}
// weapon indicators (bottom center)
for (let i = 0; i < 3; i++) {
const wx = W / 2 - 60 + i * 40;
ctx.fillStyle = i === player.cur ? '#ffcc44' : '#555';
ctx.font = 'bold 11px Courier New';
ctx.textAlign = 'center';
ctx.fillText((i + 1) + '', wx, H - 12);
}
// hit flash
if (player.hitFlash > 0) {
ctx.fillStyle = `rgba(255,0,0,${(player.hitFlash / 120) * 0.35})`;
ctx.fillRect(0, 0, W, H);
}
}
function drawMinimap() {
const size = 120, cellS = size / MAP_W;
const mx = W - size - 10, my = 10;
ctx.globalAlpha = 0.75;
ctx.fillStyle = '#000';
ctx.fillRect(mx, my, size, size * (MAP_H / MAP_W));
for (let y = 0; y < MAP_H; y++) {
for (let x = 0; x < MAP_W; x++) {
const c = cell(x, y);
if (c === 9) ctx.fillStyle = '#2f2';
else if (c > 0) ctx.fillStyle = wallColor(c, 0.9);
else ctx.fillStyle = '#1a1a1a';
ctx.fillRect(mx + x * cellS, my + y * cellS, cellS + 0.5, cellS + 0.5);
}
}
// enemies
for (const e of enemies) {
ctx.fillStyle = e.alive ? '#f44' : '#822';
ctx.fillRect(mx + e.x * cellS - 1, my + e.y * cellS - 1, 2.5, 2.5);
}
// pickups
for (const pk of pickups) {
ctx.fillStyle = pk.type === 'health' ? '#4dff7a' : '#ffcc44';
ctx.fillRect(mx + pk.x * cellS - 1, my + pk.y * cellS - 1, 2.5, 2.5);
}
// player
ctx.fillStyle = '#fff';
ctx.fillRect(mx + player.x * cellS - 1.5, my + player.y * cellS - 1.5, 3, 3);
ctx.strokeStyle = '#fff';
ctx.beginPath();
ctx.moveTo(mx + player.x * cellS, my + player.y * cellS);
ctx.lineTo(mx + player.x * cellS + Math.cos(player.dir) * 6, my + player.y * cellS + Math.sin(player.dir) * 6);
ctx.stroke();
ctx.globalAlpha = 1;
}
// ---------- Weapon view (first person) ----------
function drawWeapon() {
const w = player.weapons[player.cur];
const bobX = Math.sin(player.bobT) * 8 * (player.bobY > 0 ? 1 : 0);
const bobY = Math.abs(Math.cos(player.bobT)) * 6 * (player.bobY > 0 ? 1 : 0);
const kick = player.fireT > 0 ? (player.fireT / 60) * 26 : 0;
const reloading = player.reloading > 0;
const relRot = reloading ? Math.sin((900 - player.reloading) / 900 * Math.PI) * 0.5 : 0;
const cx = W / 2 + bobX;
const baseY = H + 20; // offscreen bottom
const topY = baseY - kick + bobY;
ctx.save();
ctx.translate(cx, topY);
ctx.rotate(relRot);
if (w.name === 'SHOTGUN') drawShotgun();
else if (w.name === 'SMG') drawSMG();
else drawPistol();
// muzzle flash
if (player.muzzleT > 0) {
const a = player.muzzleT / 60;
ctx.save();
const fx = w.name === 'SHOTGUN' ? 0 : (w.name === 'SMG' ? -4 : 6);
ctx.translate(fx, -150);
ctx.fillStyle = `rgba(255,220,120,${a})`;
ctx.beginPath();
for (let i = 0; i < 10; i++) {
const ang = (i / 10) * 6.28;
const r = 30 + Math.random() * 26;
ctx.lineTo(Math.cos(ang) * r, Math.sin(ang) * r);
}
ctx.closePath(); ctx.fill();
ctx.fillStyle = `rgba(255,255,220,${a})`;
ctx.beginPath(); ctx.arc(0, 0, 14, 0, 6.28); ctx.fill();
ctx.restore();
}
ctx.restore();
}
function drawPistol() {
// barrel
ctx.fillStyle = '#2b2b2f';
ctx.fillRect(-8, -150, 16, 90);
ctx.fillStyle = '#1a1a1d';
ctx.fillRect(-6, -150, 4, 90);
// slide
ctx.fillStyle = '#3a3a40';
ctx.fillRect(-14, -66, 28, 30);
// grip
ctx.fillStyle = '#4a3a2a';
ctx.beginPath();
ctx.moveTo(-14, -36); ctx.lineTo(14, -36);
ctx.lineTo(18, 40); ctx.lineTo(-18, 40); ctx.closePath();
ctx.fill();
// hand
ctx.fillStyle = '#c68b5a';
ctx.fillRect(-22, -30, 44, 72);
ctx.fillStyle = '#a8704a';
ctx.fillRect(-22, 30, 44, 14);
}
function drawSMG() {
// barrel
ctx.fillStyle = '#242428';
ctx.fillRect(-7, -150, 14, 80);
// suppressor-ish
ctx.fillStyle = '#333';
ctx.fillRect(-10, -150, 20, 24);
// body
ctx.fillStyle = '#38383d';
ctx.fillRect(-20, -72, 40, 40);
// magazine
ctx.fillStyle = '#222';
ctx.fillRect(-10, -34, 14, 40);
// grip
ctx.fillStyle = '#4a3a2a';
ctx.beginPath();
ctx.moveTo(-14, -34); ctx.lineTo(14, -34);
ctx.lineTo(16, 44); ctx.lineTo(-16, 44); ctx.closePath();
ctx.fill();
// hand
ctx.fillStyle = '#c68b5a';
ctx.fillRect(-24, -30, 48, 74);
ctx.fillStyle = '#a8704a';
ctx.fillRect(-24, 30, 48, 14);
}
function drawShotgun() {
// double barrel
ctx.fillStyle = '#2a2a2e';
ctx.fillRect(-18, -155, 12, 95);
ctx.fillRect(6, -155, 12, 95);
ctx.fillStyle = '#19191c';
ctx.fillRect(-17, -155, 3, 95);
ctx.fillRect(7, -155, 3, 95);
// pump / body
ctx.fillStyle = '#4a3a2a';
ctx.fillRect(-22, -64, 44, 34);
ctx.fillStyle = '#333';
ctx.fillRect(-16, -32, 32, 12);
// grip
ctx.fillStyle = '#5a4530';
ctx.beginPath();
ctx.moveTo(-16, -32); ctx.lineTo(16, -32);
ctx.lineTo(20, 46); ctx.lineTo(-20, 46); ctx.closePath();
ctx.fill();
// hand
ctx.fillStyle = '#c68b5a';
ctx.fillRect(-26, -28, 52, 74);
ctx.fillStyle = '#a8704a';
ctx.fillRect(-26, 30, 52, 14);
}
function drawCrosshair() {
if (state !== 'playing' || player.dead) return;
const cx = W / 2, cy = H / 2;
ctx.strokeStyle = 'rgba(255,255,255,0.8)';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(cx - 10, cy); ctx.lineTo(cx - 3, cy);
ctx.moveTo(cx + 3, cy); ctx.lineTo(cx + 10, cy);
ctx.moveTo(cx, cy - 10); ctx.lineTo(cx, cy - 3);
ctx.moveTo(cx, cy + 3); ctx.lineTo(cx, cy + 10);
ctx.stroke();
ctx.fillStyle = 'rgba(255,80,80,0.9)';
ctx.beginPath(); ctx.arc(cx, cy, 1.6, 0, 6.28); ctx.fill();
}
// ---------- Main loop ----------
function loop(t) {
if (state !== 'playing' && state !== 'dead') return;
dt = t - lastTime; lastTime = t;
if (dt > 50) dt = 50;
if (state === 'playing') update(dt);
render();
if (state === 'playing' || state === 'dead') requestAnimationFrame(loop);
}
// ---------- Boot ----------
document.getElementById('startBtn').addEventListener('click', () => {
initAudio();
resetGame();
});
document.getElementById('againBtn').addEventListener('click', () => {
initAudio();
resetGame();
});
// draw a static preview behind the menu
function menuPreview() {
if (state !== 'menu') return;
player = freshPlayer();
render();
requestAnimationFrame(menuPreview);
}
menuPreview();
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"bartowski/Ornith-1.5-35B-A3B","provider":"brigid","persona":"full","sessionId":"2cdf9827-912a-4518-b7c7-767b1466aae4","tokensIn":2110921,"tokensOut":37964,"tokensTotal":2148885,"cost":0,"turns":47,"toolCalls":46,"failedToolCalls":2,"timestamp":"2026-08-23T19:05:01.047Z"} -->