DOOM
Ornith 1.0 35B GGUF UD IQ4 NL · typescript

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 48,062 bytes · SHA-256 930c9675f5da
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>DOOM-LITE — Retro FPS</title>
<style>
/* ── css: game ── */
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; background: #000; }
#game-container {
width: 100vw; height: 100vh; position: relative;
background: #000;
display: flex; justify-content: center; align-items: center;
}
#game-canvas {
width: 100%; height: 100%;
image-rendering: pixelated;
image-rendering: -moz-crisp-edges;
image-rendering: crisp-edges;
cursor: crosshair;
display: block;
}
#instructions {
position: absolute; top: 50%; left: 50%;
transform: translate(-50%, -50%);
text-align: center; color: #fff;
font-family: monospace; z-index: 10; cursor: pointer;
user-select: none;
}
#instructions h1 {
font-size: 48px; color: #f44;
text-shadow: 0 0 20px #f00; margin: 0 0 20px;
}
#instructions p {
font-size: 18px; color: #aaa; margin: 8px;
}
#overlay {
position: absolute; top: 0; left: 0;
width: 100%; height: 100%;
display: flex; justify-content: center; align-items: center;
background: rgba(0,0,0,0.85);
z-index: 20;
}
</style>
</head>
<body>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {};
function __require(id) {
if (__cache[id]) return __cache[id].exports;
var module = __cache[id] = { exports: {} };
var factory = __mods[id];
if (!factory) throw new Error("Module not found: " + id);
factory(module.exports, function (spec) {
var target = (__map[id] && __map[id][spec]) || spec;
return __require(target);
}, module);
return module.exports;
}
// ── module: src/main.ts ──
__mods["src/main.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.start = start;
// =============================================================================
// Constants & Types
// =============================================================================
const SCREEN_W = 640, SCREEN_H = 480;
const MAP_SIZE = 24;
const FOV = Math.PI / 3, HALF_FOV = FOV / 2, NUM_RAYS = 320;
const TOTAL_KEYS = 3;
const PLAYER_SPEED = 3.2, PLAYER_RADIUS = 0.22;
const WEAPONS = [
{ name: 'PISTOL', damage: 25, fireRate: 250, spread: 0.015, pellets: 1, type: 'pistol' },
{ name: 'SHOTGUN', damage: 14, fireRate: 750, spread: 0.08, pellets: 7, type: 'shotgun' },
{ name: 'ROCKET', damage: 60, fireRate: 1200, spread: 0.005, pellets: 1, type: 'rocket' },
];
const ENEMY_DEFS = {
imp: { health: 30, speed: 1.8, damage: 12, atkRange: 0.9, atkCD: 800, color: '#dd3333', eyesColor: '#ffff00', size: 0.35 },
demon: { health: 70, speed: 2.5, damage: 25, atkRange: 1.0, atkCD: 600, color: '#cc00cc', eyesColor: '#00ff00', size: 0.45 },
knight: { health: 150, speed: 0.8, damage: 40, atkRange: 1.2, atkCD: 1200, color: '#3366cc', eyesColor: '#ff0000', size: 0.5 },
};
const ENEMY_SPAWNS = [
{ x: 8.5, y: 8.5, type: 'imp' },
{ x: 15.5, y: 8.5, type: 'imp' },
{ x: 20.5, y: 10.5, type: 'imp' },
{ x: 10.5, y: 14.5, type: 'demon' },
{ x: 5.5, y: 18.5, type: 'imp' },
{ x: 18.5, y: 14.5, type: 'demon' },
{ x: 8.5, y: 4.5, type: 'imp' },
{ x: 15.5, y: 18.5, type: 'knight' },
{ x: 20.5, y: 6.5, type: 'imp' },
{ x: 12.5, y: 6.5, type: 'demon' },
{ x: 3.5, y: 18.5, type: 'imp' },
{ x: 18.5, y: 4.5, type: 'demon' },
];
const PICKUP_SPAWNS = [
{ x: 4.5, y: 4.5, type: 'health' },
{ x: 8.5, y: 4.5, type: 'ammo' },
{ x: 12.5, y: 2.5, type: 'key' },
{ x: 16.5, y: 4.5, type: 'health' },
{ x: 20.5, y: 6.5, type: 'ammo' },
{ x: 6.5, y: 8.5, type: 'key' },
{ x: 18.5, y: 8.5, type: 'health' },
{ x: 10.5, y: 10.5, type: 'ammo' },
{ x: 4.5, y: 14.5, type: 'health' },
{ x: 14.5, y: 14.5, type: 'key' },
{ x: 20.5, y: 14.5, type: 'ammo' },
{ x: 8.5, y: 18.5, type: 'health' },
{ x: 16.5, y: 18.5, type: 'ammo' },
{ x: 3.5, y: 20.5, type: 'health' },
{ x: 20.5, y: 20.5, type: 'key' },
];
const MAP = [
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 1],
[1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1],
[1, 0, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1],
[1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 0, 0, 1],
[1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1],
[1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 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, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
];
const EXIT_POS = { x: 22.5, y: 22.5 };
function isWall(mx, my) {
if (mx < 0 || mx >= 24 || my < 0 || my >= 24)
return true;
return MAP[my][mx] > 0;
}
function canMove(nx, ny) {
const r = PLAYER_RADIUS;
for (let dy = -r; dy <= r + 0.01; dy += r) {
for (let dx = -r; dx <= r + 0.01; dx += r) {
if (isWall(Math.floor(nx + dx), Math.floor(ny + dy)))
return false;
}
}
return true;
}
const player = {
x: 1.5, y: 1.5, angle: 0,
health: 100, maxHealth: 100,
ammo: 50, weaponIdx: 0,
alive: true, respawnT: 0,
shootAnim: 0, shootT: 0,
bobPhase: 0, damageFlash: 0,
};
const gameState = {
started: false, gameWon: false, gameLost: false,
keysCollected: 0, kills: 0, score: 0, time: 0,
};
const input = {
forward: false, backward: false, strafeLeft: false, strafeRight: false,
shoot: false, switchWeapon: false, lastWeaponSwitch: 0,
mouseDX: 0,
};
let enemies = [];
let pickups = [];
let projectiles = [];
let shakeAmount = 0;
// =============================================================================
// Initialization
// =============================================================================
function initGame() {
player.x = 1.5;
player.y = 1.5;
player.angle = 0;
player.health = 100;
player.ammo = 50;
player.weaponIdx = 0;
player.alive = true;
player.shootAnim = 0;
player.shootT = 0;
player.bobPhase = 0;
player.damageFlash = 0;
player.respawnT = 0;
gameState.started = true;
gameState.gameWon = false;
gameState.gameLost = false;
gameState.keysCollected = 0;
gameState.kills = 0;
gameState.score = 0;
gameState.time = 0;
enemies = [];
pickups = [];
projectiles = [];
for (const s of ENEMY_SPAWNS) {
const d = ENEMY_DEFS[s.type];
if (!d)
continue;
enemies.push({
x: s.x, y: s.y, defId: s.type,
health: d.health, maxHealth: d.health,
state: 'idle', animT: Math.random() * 6.28,
attackT: 0, deathT: 0, dmgFlash: 0,
spawnX: s.x, spawnY: s.y,
angle: Math.random() * 6.28,
kbX: 0, kbY: 0,
});
}
for (const s of PICKUP_SPAWNS) {
pickups.push({ x: s.x, y: s.y, type: s.type, active: true, animT: Math.random() * 6.28 });
}
}
// =============================================================================
// Input
// =============================================================================
window.addEventListener('keydown', (e) => {
switch (e.code) {
case 'KeyW':
case 'ArrowUp':
input.forward = true;
e.preventDefault();
break;
case 'KeyS':
case 'ArrowDown':
input.backward = true;
e.preventDefault();
break;
case 'KeyA':
case 'ArrowLeft':
input.strafeLeft = true;
e.preventDefault();
break;
case 'KeyD':
case 'ArrowRight':
input.strafeRight = true;
e.preventDefault();
break;
case 'KeyQ':
input.switchWeapon = true;
break;
case 'Space':
input.shoot = true;
e.preventDefault();
break;
case 'Digit1':
case 'Numpad1':
player.weaponIdx = 0;
break;
case 'Digit2':
case 'Numpad2':
player.weaponIdx = 1;
break;
case 'Digit3':
case 'Numpad3':
player.weaponIdx = 2;
break;
}
});
window.addEventListener('keyup', (e) => {
switch (e.code) {
case 'KeyW':
case 'ArrowUp':
input.forward = false;
break;
case 'KeyS':
case 'ArrowDown':
input.backward = false;
break;
case 'KeyA':
case 'ArrowLeft':
input.strafeLeft = false;
break;
case 'KeyD':
case 'ArrowRight':
input.strafeRight = false;
break;
case 'KeyQ':
input.switchWeapon = false;
break;
case 'Space':
input.shoot = false;
break;
}
});
document.addEventListener('mousemove', (e) => {
input.mouseDX += e.movementX;
});
// =============================================================================
// Rendering
// =============================================================================
let canvas;
let ctx;
let imgData;
let pixels;
let zBuffer;
function initRender(c) {
canvas = c;
ctx = c.getContext('2d', { willReadFrequently: true });
canvas.width = SCREEN_W;
canvas.height = SCREEN_H;
imgData = ctx.createImageData(SCREEN_W, SCREEN_H);
pixels = imgData.data;
zBuffer = new Float32Array(SCREEN_W);
}
function render() {
const w = SCREEN_W, h = SCREEN_H;
pixels.fill(0);
zBuffer.fill(Infinity);
const px = player.x, py = player.y, pa = player.angle;
const bob = Math.sin(player.bobPhase) * (player.alive ? 4 : 0);
const cos = Math.cos(pa), sin = Math.sin(pa);
// Screen shake
let ssx = 0, ssy = 0;
if (shakeAmount > 0) {
ssx = (Math.random() - 0.5) * shakeAmount * 2.5;
ssy = (Math.random() - 0.5) * shakeAmount * 2.5;
shakeAmount *= 0.85;
if (shakeAmount < 0.3)
shakeAmount = 0;
}
// Ceiling
for (let y = 0; y < Math.floor(h / 2); y++) {
let idx = y * w * 4;
for (let x = 0; x < w; x++) {
pixels[idx] = 8;
pixels[idx + 1] = 8;
pixels[idx + 2] = 24;
pixels[idx + 3] = 255;
idx += 4;
}
}
// Floor
for (let y = Math.floor(h / 2); y < h; y++) {
const dist = (y - h / 2) / (h * 0.5);
const s = 1 - dist * 0.5;
let idx = y * w * 4;
for (let x = 0; x < w; x++) {
pixels[idx] = 26 * s | 0;
pixels[idx + 1] = 26 * s | 0;
pixels[idx + 2] = 46 * s | 0;
pixels[idx + 3] = 255;
idx += 4;
}
}
// Raycast walls
const sw = Math.max(1, w / NUM_RAYS | 0);
for (let col = 0; col < w; col += sw) {
const ri = Math.floor(col / w * NUM_RAYS);
const ra = pa - HALF_FOV + ri / NUM_RAYS * FOV;
const rdx = Math.cos(ra), rdy = Math.sin(ra);
let mx = Math.floor(px), my = Math.floor(py);
const ddx = Math.abs(1 / rdx), ddy = Math.abs(1 / rdy);
let sx, sy, sdx, sdy;
if (rdx < 0) {
sx = -1;
sdx = (px - mx) * ddx;
}
else {
sx = 1;
sdx = (mx + 1 - px) * ddx;
}
if (rdy < 0) {
sy = -1;
sdy = (py - my) * ddy;
}
else {
sy = 1;
sdy = (my + 1 - py) * ddy;
}
let hit = false, side = 0, wt = 1;
while (!hit) {
if (sdx < sdy) {
sdx += ddx;
mx += sx;
side = 0;
}
else {
sdy += ddy;
my += sy;
side = 1;
}
if (mx < 0 || mx >= 24 || my < 0 || my >= 24) {
hit = true;
break;
}
if (MAP[my][mx] > 0) {
hit = true;
wt = MAP[my][mx];
}
}
let pd;
if (side === 0)
pd = Math.abs((mx - px + (1 - sx) / 2) / rdx);
else
pd = Math.abs((my - py + (1 - sy) / 2) / rdy);
pd = Math.max(pd, 0.05);
for (let s2 = 0; s2 < sw && col + s2 < w; s2++)
zBuffer[col + s2] = pd;
const lh = Math.floor(h / pd);
const ds = (h - lh) / 2 + bob;
let cr, cg, cb;
if (wt === 2) {
cr = 0x66;
cg = 0x66;
cb = 0x88;
}
else {
const c = [0x8B, 0x69, 0x14];
cr = c[0];
cg = c[1];
cb = c[2];
}
const ds2 = 1 / (1 + pd * 0.12);
const ss = (side === 1 ? 0.7 : 1) * ds2;
for (let y = Math.max(0, ds | 0); y < Math.min(h, ds + lh); y++) {
const idx = (y * w + col) * 4;
pixels[idx] = cr * ss | 0;
pixels[idx + 1] = cg * ss | 0;
pixels[idx + 2] = cb * ss | 0;
pixels[idx + 3] = 255;
if (sw > 1) {
for (let s3 = 1; s3 < sw && col + s3 < w; s3++) {
const i2 = (y * w + col + s3) * 4;
pixels[i2] = pixels[idx];
pixels[i2 + 1] = pixels[idx + 1];
pixels[i2 + 2] = pixels[idx + 2];
pixels[i2 + 3] = 255;
}
}
}
}
// Sprites
const sprites = [];
for (let i = 0; i < pickups.length; i++) {
if (!pickups[i].active)
continue;
sprites.push({ x: pickups[i].x, y: pickups[i].y, kind: 'pickup', i });
}
for (let i = 0; i < enemies.length; i++) {
if (enemies[i].state === 'dead' && enemies[i].deathT > 1500)
continue;
sprites.push({ x: enemies[i].x, y: enemies[i].y, kind: 'enemy', i });
}
for (let i = 0; i < projectiles.length; i++) {
sprites.push({ x: projectiles[i].x, y: projectiles[i].y, kind: 'proj', i });
}
sprites.sort((a, b) => {
const da = (a.x - px) ** 2 + (a.y - py) ** 2;
const db = (b.x - px) ** 2 + (b.y - py) ** 2;
return db - da;
});
for (const sp of sprites) {
const dx = sp.x - px, dy = sp.y - py;
const plX = -sin * Math.tan(HALF_FOV), plY = cos * Math.tan(HALF_FOV);
const dX = cos, dY = sin;
const inv = 1 / (plX * dY - dX * plY);
const tx = inv * (dY * dx - dX * dy);
const ty = inv * (-plY * dx + plX * dy);
if (ty <= 0.2)
continue;
const sX = Math.floor(w / 2 * (1 + tx / ty));
const sH = Math.abs(Math.floor(h / ty));
const sW = sH;
const sY = Math.floor((h - sH) / 2 + bob);
drawSprite(sp, sX, sY, sH, sW, ty, w, h);
}
// Weapon
drawWeapon(w, h, bob);
// Damage vignette
if (player.damageFlash > 0) {
const i2 = Math.min(player.damageFlash * 0.35, 0.6);
const gr = ctx.createRadialGradient(w / 2, h / 2, h * 0.3, w / 2, h / 2, h * 0.8);
gr.addColorStop(0, 'rgba(255,0,0,0)');
gr.addColorStop(1, `rgba(255,0,0,${i2})`);
ctx.fillStyle = gr;
ctx.fillRect(0, 0, w, h);
}
// Muzzle flash
if (player.shootAnim > 10) {
const cx = w / 2 + (Math.random() - 0.5) * 12;
const cy = h - h * 0.22 + (Math.random() - 0.5) * 12;
const gr = ctx.createRadialGradient(cx, cy, 0, cx, cy, 45);
gr.addColorStop(0, 'rgba(255,255,200,0.6)');
gr.addColorStop(0.5, 'rgba(255,150,0,0.25)');
gr.addColorStop(1, 'rgba(255,50,0,0)');
ctx.fillStyle = gr;
ctx.fillRect(0, 0, w, h);
}
// Apply shake
ctx.setTransform(1, 0, 0, 1, ssx, ssy);
ctx.putImageData(imgData, 0, 0);
ctx.setTransform(1, 0, 0, 1, 0, 0);
drawMinimap();
drawHUD();
drawCrosshair(w, h);
}
function drawSprite(sp, screenX, screenY, height, width, depth, w, h) {
if (height < 2)
return;
if (sp.kind === 'enemy') {
const e = enemies[sp.i];
const d = ENEMY_DEFS[e.defId];
if (!d)
return;
const sh = Math.max(0.2, 1 / (1 + depth * 0.08));
const r = parseInt(d.color.slice(1, 3), 16) * sh | 0;
const g = parseInt(d.color.slice(3, 5), 16) * sh | 0;
const b = parseInt(d.color.slice(5, 7), 16) * sh | 0;
for (let y = Math.max(0, screenY); y < Math.min(h, screenY + height); y++) {
const rY = (y - screenY) / height;
for (let x = Math.max(0, screenX - width / 2); x < Math.min(w, screenX + width / 2); x++) {
if (depth > zBuffer[x])
continue;
const dx = x / width - 0.5, dy = rY - 0.5;
if (Math.sqrt(dx * dx + dy * dy) > 0.45)
continue;
const idx = (y * w + x) * 4;
if (e.dmgFlash > 0) {
pixels[idx] = 255;
pixels[idx + 1] = 255;
pixels[idx + 2] = 255;
}
else {
const sh2 = Math.max(0.3, 1 - Math.abs(Math.sqrt(dx * dx + dy * dy) - 0.3) * 2);
pixels[idx] = r * sh2 | 0;
pixels[idx + 1] = g * sh2 | 0;
pixels[idx + 2] = b * sh2 | 0;
}
pixels[idx + 3] = 255;
}
}
// Eyes
const eyeY = screenY + height * 0.35 | 0;
const eyeS = Math.max(1, Math.floor(width * 0.07));
const eyeR = parseInt(d.eyesColor.slice(1, 3), 16);
const eyeG = parseInt(d.eyesColor.slice(3, 5), 16);
const eyeB = parseInt(d.eyesColor.slice(5, 7), 16);
for (const exOff of [-0.15, 0.15]) {
const ex = screenX + width * exOff | 0;
for (let ey = eyeY - eyeS; ey <= eyeY + eyeS; ey++) {
for (let ex2 = ex - eyeS; ex2 <= ex + eyeS; ex2++) {
if (ex2 < 0 || ex2 >= w || ey < 0 || ey >= h)
continue;
if (depth > zBuffer[ex2])
continue;
const dist2 = Math.sqrt(((ex2 - ex) / eyeS) ** 2 + ((ey - eyeY) / eyeS) ** 2);
if (dist2 <= 1) {
const idx = (ey * w + ex2) * 4;
pixels[idx] = eyeR;
pixels[idx + 1] = eyeG;
pixels[idx + 2] = eyeB;
pixels[idx + 3] = 255;
}
}
}
}
// Health bar
if (e.state !== 'dead') {
const bW = Math.max(4, Math.floor(width * 0.6));
const bH = Math.max(2, Math.floor(height * 0.05));
const bX = Math.floor(screenX - bW / 2);
const bY = Math.max(0, screenY - bH - 4);
if (bY > 0) {
for (let y = bY; y < bY + bH && y < h; y++) {
for (let x = bX; x < bX + bW && x < w; x++) {
if (depth > zBuffer[x])
continue;
const idx = (y * w + x) * 4;
pixels[idx] = 0x33;
pixels[idx + 1] = 0x33;
pixels[idx + 2] = 0x33;
pixels[idx + 3] = 255;
}
}
const hp = Math.max(0, e.health / e.maxHealth);
const hpW = Math.floor(bW * hp);
const hpColor = hp > 0.5 ? 0x00ff00 : hp > 0.25 ? 0xffff00 : 0xff0000;
const hr = (hpColor >> 16) & 0xff, hg = (hpColor >> 8) & 0xff, hb = hpColor & 0xff;
for (let y = bY; y < bY + bH && y < h; y++) {
for (let x = bX; x < bX + hpW && x < w; x++) {
if (depth > zBuffer[x])
continue;
const idx = (y * w + x) * 4;
pixels[idx] = hr;
pixels[idx + 1] = hg;
pixels[idx + 2] = hb;
pixels[idx + 3] = 255;
}
}
}
}
// Death fade
if (e.state === 'dead') {
const f = Math.max(0, 1 - e.deathT / 1500);
for (let y = Math.max(0, screenY); y < Math.min(h, screenY + height); y++) {
for (let x = Math.max(0, screenX - width / 2); x < Math.min(w, screenX + width / 2); x++) {
if (depth > zBuffer[x])
continue;
const idx = (y * w + x) * 4;
pixels[idx] = pixels[idx] * f | 0;
pixels[idx + 1] = pixels[idx + 1] * f | 0;
pixels[idx + 2] = pixels[idx + 2] * f | 0;
}
}
}
}
else if (sp.kind === 'pickup') {
const p = pickups[sp.i];
const sz = Math.floor(height * 0.35);
const bobO = Math.sin(p.animT) * sz * 0.25;
const py2 = screenY + height * 0.35 + bobO;
let cr, cg, cb;
switch (p.type) {
case 'health':
cr = 0;
cg = 255;
cb = 68;
break;
case 'ammo':
cr = 255;
cg = 136;
cb = 0;
break;
default:
cr = 255;
cg = 221;
cb = 0;
}
for (let y = Math.max(0, Math.floor(py2 - sz)); y < Math.min(h, Math.floor(py2 + sz)); y++) {
for (let x = Math.max(0, Math.floor(screenX - sz)); x < Math.min(w, Math.floor(screenX + sz)); x++) {
if (depth > zBuffer[x])
continue;
const dx = (x - screenX) / sz, dy = (y - py2) / sz;
if (dx * dx + dy * dy > 0.25)
continue;
const idx = (y * w + x) * 4;
pixels[idx] = cr;
pixels[idx + 1] = cg;
pixels[idx + 2] = cb;
pixels[idx + 3] = 255;
}
}
if (p.type === 'health') {
const th = Math.max(1, Math.floor(sz * 0.2)), ln = Math.floor(sz * 0.45);
const pickupY = screenY + height * 0.35 + Math.sin(p.animT) * sz * 0.25;
for (let y2 = pickupY - ln; y2 <= pickupY + ln; y2++) {
for (let t = -th; t <= th; t++) {
const px2 = screenX + t;
if (px2 < 0 || px2 >= w || y2 < 0 || y2 >= h || depth > zBuffer[px2])
continue;
const idx = (y2 * w + px2) * 4;
pixels[idx] = 255;
pixels[idx + 1] = 255;
pixels[idx + 2] = 255;
pixels[idx + 3] = 255;
}
}
for (let x2 = screenX - ln; x2 <= screenX + ln; x2++) {
for (let t = -th; t <= th; t++) {
const py2 = pickupY + t;
if (py2 < 0 || py2 >= h || x2 < 0 || x2 >= w || depth > zBuffer[x2])
continue;
const idx = (py2 * w + x2) * 4;
pixels[idx] = 255;
pixels[idx + 1] = 255;
pixels[idx + 2] = 255;
pixels[idx + 3] = 255;
}
}
}
}
else if (sp.kind === 'proj') {
const sz = Math.max(2, Math.floor(80 / depth));
for (let y = Math.max(0, screenY - sz); y < Math.min(h, screenY + sz); y++) {
for (let x = Math.max(0, screenX - sz); x < Math.min(w, screenX + sz); x++) {
if (depth > zBuffer[x])
continue;
const dx = (x - screenX) / sz, dy = (y - screenY) / sz;
if (dx * dx + dy * dy > 1)
continue;
const idx = (y * w + x) * 4;
pixels[idx] = 255;
pixels[idx + 1] = 68;
pixels[idx + 2] = 0;
pixels[idx + 3] = 255;
}
}
}
}
function drawWeapon(w, h, bob) {
const recoil = player.shootAnim > 0 ? Math.sin(player.shootAnim * 0.5) * 12 : 0;
switch (player.weaponIdx) {
case 0: {
const x = w / 2 - 40, y = h - 160 + recoil;
ctx.fillStyle = '#555';
ctx.fillRect(x + 25, y + 10, 12, 50);
ctx.fillStyle = '#444';
ctx.fillRect(x + 18, y + 50, 28, 25);
ctx.fillStyle = '#333';
ctx.fillRect(x + 22, y + 70, 20, 35);
if (player.shootAnim > 10) {
ctx.fillStyle = `rgba(255,${150 + Math.random() * 100 | 0},0,${0.5 + Math.random() * 0.5})`;
ctx.beginPath();
ctx.arc(x + 31, y + 5, 10 + Math.random() * 8, 0, Math.PI * 2);
ctx.fill();
}
break;
}
case 1: {
const x = w / 2 - 50, y = h - 170 + recoil;
ctx.fillStyle = '#444';
ctx.fillRect(x + 20, y + 10, 14, 60);
ctx.fillRect(x + 45, y + 10, 14, 60);
ctx.fillStyle = '#555';
ctx.fillRect(x + 10, y + 60, 65, 25);
ctx.fillStyle = '#654321';
ctx.fillRect(x, y + 80, 85, 35);
ctx.fillStyle = '#333';
ctx.fillRect(x + 22, y + 50, 40, 10);
if (player.shootAnim > 10) {
ctx.fillStyle = `rgba(255,${100 + Math.random() * 100 | 0},0,${0.7 + Math.random() * 0.3})`;
ctx.beginPath();
ctx.arc(x + 27, y + 8, 12 + Math.random() * 6, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.arc(x + 52, y + 8, 12 + Math.random() * 6, 0, Math.PI * 2);
ctx.fill();
}
break;
}
case 2: {
const x = w / 2 - 55, y = h - 165 + recoil;
ctx.fillStyle = '#3a3a3a';
ctx.fillRect(x + 25, y + 15, 70, 30);
ctx.fillStyle = '#cc3300';
ctx.beginPath();
ctx.moveTo(x + 25, y + 15);
ctx.lineTo(x + 10, y + 30);
ctx.lineTo(x + 25, y + 45);
ctx.fill();
ctx.fillStyle = '#555';
ctx.fillRect(x + 90, y + 12, 15, 6);
ctx.fillRect(x + 90, y + 42, 15, 6);
ctx.fillStyle = '#444';
ctx.fillRect(x + 30, y + 40, 25, 40);
if (player.shootAnim > 10) {
ctx.fillStyle = `rgba(255,50,0,${0.8 + Math.random() * 0.2})`;
ctx.beginPath();
ctx.arc(x + 8, y + 30, 15 + Math.random() * 10, 0, Math.PI * 2);
ctx.fill();
}
break;
}
}
}
function drawMinimap() {
const sc = 4;
const mx = SCREEN_W - 24 * sc - 10, my = 10;
ctx.save();
ctx.globalAlpha = 0.65;
ctx.fillStyle = '#000';
ctx.fillRect(mx - 1, my - 1, 24 * sc + 2, 24 * sc + 2);
for (let y = 0; y < 24; y++) {
for (let x = 0; x < 24; x++) {
if (MAP[y][x] > 0) {
ctx.fillStyle = '#555';
ctx.fillRect(mx + x * sc, my + y * sc, sc, sc);
}
}
}
ctx.fillStyle = '#0f0';
ctx.fillRect(mx + player.x * sc - 2, my + player.y * sc - 2, 4, 4);
ctx.strokeStyle = '#0f0';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(mx + player.x * sc, my + player.y * sc);
ctx.lineTo(mx + (player.x + Math.cos(player.angle) * 2) * sc, my + (player.y + Math.sin(player.angle) * 2) * sc);
ctx.stroke();
for (const e of enemies) {
if (e.state === 'dead')
continue;
ctx.fillStyle = '#f00';
ctx.fillRect(mx + e.x * sc - 1, my + e.y * sc - 1, 3, 3);
}
for (const p of pickups) {
if (!p.active)
continue;
ctx.fillStyle = p.type === 'health' ? '#0f0' : p.type === 'ammo' ? '#f80' : '#ff0';
ctx.fillRect(mx + p.x * sc - 1, my + p.y * sc - 1, 2, 2);
}
ctx.restore();
}
function drawHUD() {
ctx.fillStyle = 'rgba(0,0,0,0.6)';
ctx.fillRect(8, SCREEN_H - 30, 140, 22);
const hp = player.health / player.maxHealth;
const hc = hp > 0.5 ? '#00ff44' : hp > 0.25 ? '#ffaa00' : '#ff3333';
ctx.fillStyle = hc;
ctx.fillRect(10, SCREEN_H - 28, 136 * hp, 18);
ctx.fillStyle = '#fff';
ctx.font = 'bold 13px monospace';
ctx.textAlign = 'center';
ctx.fillText(`${Math.ceil(player.health)}`, 78, SCREEN_H - 14);
ctx.textAlign = 'left';
ctx.fillStyle = '#aaa';
ctx.font = '9px monospace';
ctx.fillText('HEALTH', 14, SCREEN_H - 32);
ctx.textAlign = 'right';
ctx.fillStyle = '#aaa';
ctx.font = '9px monospace';
ctx.fillText('AMMO', SCREEN_W - 12, SCREEN_H - 32);
ctx.fillStyle = player.ammo > 10 ? '#ffaa00' : '#ff3333';
ctx.font = 'bold 16px monospace';
ctx.fillText(`${player.ammo}`, SCREEN_W - 12, SCREEN_H - 14);
const wn = ['PISTOL', 'SHOTGUN', 'ROCKET'];
ctx.textAlign = 'center';
ctx.fillStyle = '#aaa';
ctx.font = '9px monospace';
ctx.fillText('WEAPON', SCREEN_W / 2, SCREEN_H - 32);
ctx.fillStyle = '#fff';
ctx.font = 'bold 12px monospace';
ctx.fillText(wn[player.weaponIdx], SCREEN_W / 2, SCREEN_H - 14);
ctx.textAlign = 'left';
ctx.fillStyle = '#aaa';
ctx.font = '9px monospace';
ctx.fillText('KEYS', 14, 18);
for (let i = 0; i < 3; i++) {
const kx = 55 + i * 22;
ctx.fillStyle = i < gameState.keysCollected ? '#ffdd00' : '#333';
ctx.beginPath();
ctx.arc(kx, 13, 5, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = i < gameState.keysCollected ? '#ffdd00' : '#555';
ctx.fillRect(kx - 1, 16, 2, 5);
}
ctx.textAlign = 'right';
ctx.fillStyle = '#aaa';
ctx.font = '9px monospace';
ctx.fillText('KILLS', SCREEN_W - 12, 18);
ctx.fillStyle = '#ff4444';
ctx.font = 'bold 11px monospace';
ctx.fillText(`${gameState.kills}`, SCREEN_W - 12, 31);
ctx.textAlign = 'center';
for (let i = 0; i < 3; i++) {
const sx = SCREEN_W / 2 - 75 + i * 75;
ctx.fillStyle = i === player.weaponIdx ? 'rgba(255,255,255,0.15)' : 'rgba(0,0,0,0.3)';
ctx.fillRect(sx - 28, SCREEN_H - 6, 56, 14);
ctx.fillStyle = i === player.weaponIdx ? '#fff' : '#555';
ctx.font = '9px monospace';
ctx.fillText(`${i + 1}:${wn[i]}`, sx, SCREEN_H + 2);
}
}
function drawCrosshair(w, h) {
const cx = w / 2, cy = h / 2;
ctx.strokeStyle = 'rgba(255,255,255,0.5)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(cx - 7, cy);
ctx.lineTo(cx - 3, cy);
ctx.moveTo(cx + 3, cy);
ctx.lineTo(cx + 7, cy);
ctx.moveTo(cx, cy - 7);
ctx.lineTo(cx, cy - 3);
ctx.moveTo(cx, cy + 3);
ctx.lineTo(cx, cy + 7);
ctx.stroke();
}
// =============================================================================
// Game Logic
// =============================================================================
function update(dt) {
gameState.time += dt;
const mdx = input.mouseDX;
input.mouseDX = 0;
player.angle += mdx * 0.003;
const rs = 2.5 * dt / 1000;
if (input.rotateLeft)
player.angle -= rs;
if (input.rotateRight)
player.angle += rs;
if (input.switchWeapon && performance.now() - input.lastWeaponSwitch > 200) {
input.switchWeapon = false;
input.lastWeaponSwitch = performance.now();
player.weaponIdx = (player.weaponIdx + 1) % 3;
player.shootAnim = 0;
}
if (player.alive) {
const ms = PLAYER_SPEED * dt / 1000;
let dx = 0, dy = 0;
if (input.forward) {
dx += Math.cos(player.angle) * ms;
dy += Math.sin(player.angle) * ms;
}
if (input.backward) {
dx -= Math.cos(player.angle) * ms * 0.6;
dy -= Math.sin(player.angle) * ms * 0.6;
}
if (input.strafeLeft) {
dx += Math.sin(player.angle) * ms;
dy -= Math.cos(player.angle) * ms;
}
if (input.strafeRight) {
dx -= Math.sin(player.angle) * ms;
dy += Math.cos(player.angle) * ms;
}
if (dx !== 0 || dy !== 0)
player.bobPhase += dt * 0.008;
let nx = player.x + dx, ny = player.y + dy;
if (canMove(nx, player.y))
player.x = nx;
if (canMove(player.x, ny))
player.y = ny;
}
handleShoot(dt);
if (player.shootAnim > 0)
player.shootAnim -= dt * 0.015;
if (player.damageFlash > 0)
player.damageFlash -= dt * 0.003;
if (!player.alive) {
player.respawnT -= dt;
if (player.respawnT <= 0) {
player.alive = true;
player.health = 100;
player.ammo = Math.max(player.ammo, 12);
player.x = 1.5;
player.y = 1.5;
player.angle = 0;
}
}
// Update enemies
for (const e of enemies) {
if (e.state === 'dead') {
e.deathT += dt;
continue;
}
e.attackT += dt;
if (e.dmgFlash > 0)
e.dmgFlash -= dt * 0.004;
if (Math.abs(e.kbX) > 0.005 || Math.abs(e.kbY) > 0.005) {
const nx = e.x + e.kbX * dt / 16, ny = e.y + e.kbY * dt / 16;
if (canMove(nx, ny)) {
e.x = nx;
e.y = ny;
}
e.kbX *= 0.85;
e.kbY *= 0.85;
continue;
}
const edx = player.x - e.x, edy = player.y - e.y;
const dist = Math.sqrt(edx * edx + edy * edy);
const def = ENEMY_DEFS[e.defId];
if (!def)
continue;
e.angle = Math.atan2(edy, edx);
if (dist > def.atkRange * 2.5)
e.state = 'idle';
else if (dist < def.atkRange) {
e.state = 'attack';
if (e.attackT >= def.atkCD) {
e.attackT = 0;
if (player.alive && player.damageFlash <= 0) {
player.health -= def.damage;
player.damageFlash = 2;
shakeAmount = Math.max(shakeAmount, 6);
if (player.health <= 0) {
player.health = 0;
player.alive = false;
player.respawnT = 2500;
}
}
}
}
else {
e.state = 'chase';
const spd = def.speed * dt / 1000;
const nx = e.x + (edx / dist) * spd, ny = e.y + (edy / dist) * spd;
if (canMove(nx, ny)) {
e.x = nx;
e.y = ny;
}
else if (canMove(nx, e.y))
e.x = nx;
else if (canMove(e.x, ny))
e.y = ny;
}
}
// Pickups
for (const p of pickups) {
if (!p.active)
continue;
p.animT += dt * 0.004;
const dx = player.x - p.x, dy = player.y - p.y;
if (dx * dx + dy * dy < 0.36) {
p.active = false;
switch (p.type) {
case 'health':
player.health = Math.min(player.health + 25, player.maxHealth);
break;
case 'ammo': {
const wpn = WEAPONS[player.weaponIdx];
player.ammo = wpn && wpn.type === 'rocket'
? Math.min(player.ammo + 1, 12)
: Math.min(player.ammo + 12, 99);
break;
}
case 'key':
gameState.keysCollected++;
if (gameState.keysCollected >= TOTAL_KEYS) {
gameState.gameWon = true;
gameState.score = Math.max(0, 10000 - Math.floor(gameState.time / 1000) * 100) + gameState.kills * 200;
}
break;
}
}
}
// Projectiles
const pms = dt / 16;
for (let i = projectiles.length - 1; i >= 0; i--) {
const p = projectiles[i];
p.life -= dt;
const nx = p.x + p.dx * pms, ny = p.y + p.dy * pms;
p.trail.push([p.x, p.y]);
if (p.trail.length > 10)
p.trail.shift();
if (isWall(Math.floor(nx), Math.floor(ny))) {
boom(p.x, p.y, 3);
projectiles.splice(i, 1);
continue;
}
p.x = nx;
p.y = ny;
for (const e of enemies) {
if (e.state === 'dead')
continue;
const edx2 = e.x - p.x, edy2 = e.y - p.y;
if (Math.sqrt(edx2 * edx2 + edy2 * edy2) < 0.5) {
e.health -= p.damage;
e.dmgFlash = 1;
const a = Math.atan2(e.y - p.y, e.x - p.x);
e.kbX = Math.cos(a) * p.damage * 0.02;
e.kbY = Math.sin(a) * p.damage * 0.02;
boom(p.x, p.y, 3);
if (e.health <= 0) {
e.state = 'dead';
e.deathT = 0;
gameState.kills++;
}
projectiles.splice(i, 1);
break;
}
}
if (p.life <= 0 && projectiles[i] === p) {
boom(p.x, p.y, 2);
projectiles.splice(i, 1);
}
}
// Win check
if (player.alive && !gameState.gameWon) {
const edx = player.x - EXIT_POS.x, edy = player.y - EXIT_POS.y;
if (Math.sqrt(edx * edx + edy * edy) < 0.8 && gameState.keysCollected >= TOTAL_KEYS) {
gameState.gameWon = true;
gameState.score = Math.max(0, 10000 - Math.floor(gameState.time / 1000) * 100) + gameState.kills * 200;
}
}
}
function handleShoot(dt) {
const wp = WEAPONS[player.weaponIdx];
if (!wp || !input.shoot || !player.alive)
return;
player.shootT += dt;
if (player.shootT < wp.fireRate)
return;
if (wp.type === 'rocket') {
if (player.ammo < 1)
return;
player.ammo--;
player.shootT = 0;
player.shootAnim = 25;
const spd = 8, spr = (Math.random() - 0.5) * wp.spread;
const a = player.angle + spr;
projectiles.push({ x: player.x, y: player.y, dx: Math.cos(a) * spd, dy: Math.sin(a) * spd, damage: wp.damage, life: 2000, trail: [] });
shakeAmount = Math.max(shakeAmount, 8);
}
else {
if (player.ammo < 1)
return;
player.ammo -= 1;
player.shootT = 0;
player.shootAnim = 25;
for (let p = 0; p < wp.pellets; p++) {
const a = player.angle + (Math.random() - 0.5) * wp.spread * 2;
const rdx = Math.cos(a), rdy = Math.sin(a);
let emx = Math.floor(player.x), emy = Math.floor(player.y);
const sX = rdx > 0 ? 1 : -1, sY = rdy > 0 ? 1 : -1;
const ddX = Math.abs(1 / rdx), ddY = Math.abs(1 / rdy);
let sDx = rdx < 0 ? (player.x - emx) * ddX : (emx + 1 - player.x) * ddX;
let sDy = rdy < 0 ? (player.y - emy) * ddY : (emy + 1 - player.y) * ddY;
let clE = null, clD = Infinity, wD = Infinity;
while (true) {
if (sDx < sDy) {
sDx += ddX;
emx += sX;
}
else {
sDy += ddY;
emy += sY;
}
const cd = Math.max(sDx, sDy);
if (cd > wD && clE)
break;
if (emx < 0 || emx >= 24 || emy < 0 || emy >= 24) {
wD = cd;
break;
}
if (MAP[emy] && MAP[emy][emx] > 0) {
wD = cd;
break;
}
for (const e of enemies) {
if (e.state === 'dead')
continue;
const edx2 = e.x - player.x, edy2 = e.y - player.y;
const dist = Math.sqrt(edx2 * edx2 + edy2 * edy2);
if (Math.abs(edy2 * rdx - edx2 * rdy) < 0.3 && dist < clD && dist < 20) {
clD = dist;
clE = e;
}
}
}
if (clE) {
clE.health -= wp.damage;
clE.dmgFlash = 1;
const pa = Math.atan2(clE.y - player.y, clE.x - player.x);
clE.kbX = Math.cos(pa) * wp.damage * 0.02;
clE.kbY = Math.sin(pa) * wp.damage * 0.02;
if (clE.health <= 0) {
clE.state = 'dead';
clE.deathT = 0;
gameState.kills++;
}
}
}
shakeAmount = Math.max(shakeAmount, wp.type === 'shotgun' ? 5 : 2);
}
}
function boom(x, y, radius) {
for (const e of enemies) {
if (e.state === 'dead')
continue;
const edx = e.x - x, edy = e.y - y;
const dist = Math.sqrt(edx * edx + edy * edy);
if (dist < radius) {
e.health -= 30 * (1 - dist / radius);
e.dmgFlash = 1;
}
}
shakeAmount = Math.max(shakeAmount, 8);
}
// =============================================================================
// Entry Point
// =============================================================================
function start() {
// Set title
document.title = 'DOOM-LITE — Retro FPS';
// Create DOM structure
document.body.style.margin = '0';
document.body.style.overflow = 'hidden';
document.body.style.background = '#000';
document.body.style.display = 'flex';
document.body.style.justifyContent = 'center';
document.body.style.alignItems = 'center';
document.body.style.height = '100vh';
// Game container
const gameContainer = document.createElement('div');
gameContainer.id = 'game-container';
gameContainer.style.position = 'relative';
gameContainer.style.width = '100vw';
gameContainer.style.height = '100vh';
gameContainer.style.display = 'flex';
gameContainer.style.justifyContent = 'center';
gameContainer.style.alignItems = 'center';
gameContainer.style.background = '#111';
document.body.appendChild(gameContainer);
// Canvas
const c = document.createElement('canvas');
c.id = 'game-canvas';
c.tabIndex = 1;
c.style.width = '100%';
c.style.height = '100%';
c.style.objectFit = 'contain';
c.style.imageRendering = 'pixelated';
gameContainer.appendChild(c);
// Instructions overlay
const instructions = document.createElement('div');
instructions.id = 'instructions';
instructions.style.position = 'absolute';
instructions.style.top = '50%';
instructions.style.left = '50%';
instructions.style.transform = 'translate(-50%, -50%)';
instructions.style.textAlign = 'center';
instructions.style.color = '#fff';
instructions.style.fontFamily = 'monospace';
instructions.style.zIndex = '10';
instructions.style.cursor = 'pointer';
instructions.innerHTML = `
<h1 style="font-size: 48px; color: #f44; text-shadow: 0 0 20px #f00; margin: 0 0 20px;">🔥 DOOM-LITE 🔥</h1>
<p style="font-size: 18px; color: #aaa; margin: 8px;">Click to start</p>
<p style="font-size: 14px; color: #888; margin-top: 20px;">WASD to move • Mouse to look • Q to switch weapon • SPACE to shoot</p>
<p style="font-size: 12px; color: #666; margin-top: 10px;">Collect 3 keys and reach the exit • Find the keys!</p>
`;
gameContainer.appendChild(instructions);
c.addEventListener('click', () => {
c.requestPointerLock();
instructions.style.display = 'none';
});
document.addEventListener('pointerlockchange', () => {
c.style.cursor = document.pointerLockElement === c ? 'none' : 'default';
});
// Init and start
initRender(c);
initGame();
let lastTime = performance.now();
function loop(now) {
const dt = Math.min(now - lastTime, 50);
lastTime = now;
if (gameState.started && !gameState.gameWon)
update(dt);
render();
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
// Check win
setInterval(() => {
if (gameState.gameWon) {
const overlay = document.createElement('div');
overlay.id = 'overlay';
overlay.style.position = 'absolute';
overlay.style.top = '0';
overlay.style.left = '0';
overlay.style.width = '100%';
overlay.style.height = '100%';
overlay.style.display = 'flex';
overlay.style.justifyContent = 'center';
overlay.style.alignItems = 'center';
overlay.style.background = 'rgba(0,0,0,0.8)';
overlay.style.zIndex = '20';
overlay.innerHTML = `
<div style="text-align: center; font-family: monospace;">
<h1 style="font-size: 48px; color: #0f0; text-shadow: 0 0 20px #0f0; margin: 0 0 20px;">🎉 LEVEL COMPLETE!</h1>
<div style="font-size: 20px; color: #aaa; line-height: 1.8;">
<p>Time: ${Math.floor(gameState.time / 1000)}s</p>
<p>Kills: ${gameState.kills}</p>
<p>Score: ${gameState.score}</p>
</div>
<button id="restart-btn" style="margin-top: 20px; padding: 12px 24px; font-size: 18px; font-family: monospace; background: #f44; color: #fff; border: 2px solid #f66; cursor: pointer;">PLAY AGAIN</button>
</div>
`;
gameContainer.appendChild(overlay);
document.getElementById('restart-btn')?.addEventListener('click', () => {
overlay.remove();
initGame();
if (document.pointerLockElement)
document.exitPointerLock();
c.requestPointerLock();
});
}
}, 200);
}
};
// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"unsloth/Ornith-1.0-35B-GGUF-UD-IQ4_NL","provider":"brigid","persona":"typescript","sessionId":"f4ee86e9-03be-48f3-a5a2-286c7e69baf7","tokensIn":3198049,"tokensOut":128951,"tokensTotal":3327000,"cost":0,"turns":30,"toolCalls":105,"failedToolCalls":7,"timestamp":"2026-08-04T17:05:41.624Z"} -->