DOOM
thinkingcap qwen3.6 27b · middle

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 31,683 bytes · SHA-256 cbcdf0f9c24a
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>DOOMED DEPTHS - Retro FPS</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #000; overflow: hidden; cursor: none; font-family: monospace; }
canvas { display: block; width: 100vw; height: 100vh; image-rendering: pixelated; }
#overlay {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
display: flex; align-items: center; justify-content: center;
pointer-events: none; z-index: 10;
}
#overlay.active { pointer-events: all; }
.screen {
text-align: center; color: #fff; padding: 40px;
background: rgba(0,0,0,0.85); border: 3px solid #f00;
display: none; max-width: 600px;
}
.screen.visible { display: block; }
.screen h1 { font-size: 48px; color: #f00; text-shadow: 0 0 20px #f00; margin-bottom: 20px; letter-spacing: 4px; }
.screen h2 { font-size: 36px; color: #ff0; text-shadow: 0 0 15px #ff0; margin-bottom: 15px; }
.screen p { font-size: 18px; margin: 10px 0; color: #ccc; line-height: 1.6; }
.screen .key { color: #0f0; background: #222; padding: 2px 8px; border-radius: 3px; }
.btn {
display: inline-block; margin-top: 20px; padding: 15px 40px;
font-size: 24px; font-family: monospace; color: #fff; background: #a00;
border: 2px solid #f00; cursor: pointer; text-transform: uppercase;
letter-spacing: 3px; transition: all 0.2s;
}
.btn:hover { background: #f00; box-shadow: 0 0 20px #f00; }
#crosshair {
position: fixed; top: 50%; left: 50%; transform: translate(-50%,-50%);
width: 24px; height: 24px; pointer-events: none; z-index: 5; display: none;
}
#crosshair::before, #crosshair::after {
content: ''; position: absolute; background: rgba(255,255,0,0.7);
}
#crosshair::before { width: 2px; height: 100%; left: 50%; transform: translateX(-50%); }
#crosshair::after { height: 2px; width: 100%; top: 50%; transform: translateY(-50%); }
</style>
</head>
<body>
<canvas id="game"></canvas>
<div id="crosshair"></div>
<div id="overlay" class="active">
<div id="startScreen" class="screen visible">
<h1>DOOMED DEPTHS</h1>
<p>Navigate the dungeon. Kill all demons. Find the exit.</p>
<br>
<p><span class="key">W A S D</span> Move <span class="key">Mouse</span> Look</p>
<p><span class="key">Left Click</span> Fire <span class="key">1 2 3</span> Switch Weapon</p>
<p><span class="key">R</span> Reload <span class="key">Shift</span> Run</p>
<br>
<button class="btn" id="startBtn">ENTER THE DEPTHS</button>
</div>
<div id="deathScreen" class="screen">
<h1>YOU DIED</h1>
<p>The demons have claimed another soul...</p>
<p id="deathScore"></p>
<button class="btn" id="respawnBtn">RESPAWN</button>
</div>
<div id="winScreen" class="screen">
<h2 style="color:#0f0;text-shadow:0 0 15px #0f0;">VICTORY!</h2>
<p>You have conquered the depths and escaped alive!</p>
<p id="winScore"></p>
<button class="btn" id="playAgainBtn">PLAY AGAIN</button>
</div>
</div>
<script>
// ============ CONSTANTS ============
const SCREEN_W = 320;
const SCREEN_H = 200;
const FOV = Math.PI / 3;
const HALF_FOV = FOV / 2;
const NUM_RAYS = SCREEN_W;
const MAX_DEPTH = 24;
const TILE = 1;
// Map: 0=empty, 1-5=walls, 6=exit door (locked until all enemies dead)
const MAP_DATA = [
[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,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,2,2,2,0,0,0,3,3,0,0,0,0,4,4,4,0,0,0,0,0,1],
[1,0,0,2,0,0,0,0,0,0,3,0,0,0,0,0,0,4,0,0,0,0,0,1],
[1,0,0,2,0,0,0,0,0,0,0,0,0,5,5,0,0,0,0,0,0,0,0,1],
[1,0,0,0,0,0,0,0,0,0,0,0,0,5,5,0,0,0,0,0,3,3,0,1],
[1,0,0,0,0,0,2,2,2,0,0,0,0,0,0,0,0,0,0,0,3,0,0,1],
[1,0,0,0,0,0,2,0,0,0,0,0,0,0,0,4,4,0,0,0,3,0,0,1],
[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,4,0,0,0,0,0,0,1],
[1,0,0,3,3,0,0,0,0,5,5,5,0,0,0,0,0,0,2,2,2,0,0,1],
[1,0,0,3,0,0,0,0,0,5,0,5,0,0,0,0,0,0,2,0,0,0,0,1],
[1,0,0,0,0,0,0,0,0,0,0,0,0,3,3,0,0,0,0,0,0,0,0,1],
[1,0,0,0,4,4,0,0,0,0,0,0,0,3,3,0,0,5,5,5,0,0,0,1],
[1,0,0,0,4,4,0,0,2,2,0,0,0,0,0,0,0,5,0,0,0,0,0,1],
[1,0,0,0,0,0,0,0,2,2,0,0,0,0,0,4,4,0,0,0,3,3,0,1],
[1,0,0,5,5,5,0,0,0,0,0,0,0,0,0,4,4,0,0,0,3,0,0,1],
[1,0,0,5,0,0,0,0,0,0,3,3,3,0,0,0,0,0,0,0,0,0,0,1],
[1,0,0,0,0,0,0,0,0,0,3,0,0,0,2,2,0,0,5,5,0,0,0,1],
[1,0,0,0,0,0,4,4,4,0,0,0,0,0,2,2,0,0,5,5,0,6,0,1],
[1,0,0,0,0,0,4,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,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
];
const MAP_W = MAP_DATA[0].length;
const MAP_H = MAP_DATA.length;
// Wall colors per type
const WALL_COLORS = {
1: [120, 60, 60], // dark red brick
2: [80, 80, 100], // blue stone
3: [100, 90, 70], // brown
4: [70, 100, 70], // green mossy
5: [90, 60, 90], // purple
6: [200, 200, 50], // gold exit
};
// ============ GAME STATE ============
let canvas, ctx;
let gameState = 'menu'; // menu, playing, dead, win
let player = {};
let enemies = [];
let pickups = [];
let particles = [];
let projectiles = [];
let weaponAnim = 0;
let weaponFlash = 0;
let damageFlash = 0;
let score = 0;
let totalEnemies = 0;
let enemiesKilled = 0;
let exitUnlocked = false;
let mouseDX = 0;
let keysDown = {};
let lastTime = 0;
// Weapons config
const WEAPONS = [
{ name: 'Pistol', damage: 15, fireRate: 400, ammoType: 'bullets', ammoCost: 1, spread: 0.02, pellets: 1 },
{ name: 'Shotgun', damage: 12, fireRate: 800, ammoType: 'shells', ammoCost: 1, spread: 0.08, pellets: 5 },
{ name: 'Plasma', damage: 20, fireRate: 200, ammoType: 'cells', ammoCost: 1, spread: 0.03, pellets: 1, isEnergy: true },
];
// ============ INIT ============
function init() {
canvas = document.getElementById('game');
ctx = canvas.getContext('2d');
canvas.width = SCREEN_W;
canvas.height = SCREEN_H;
ctx.imageSmoothingEnabled = false;
setupInput();
resetGame();
}
function resetGame() {
player = {
x: 1.5, y: 1.5, angle: 0, pitch: 0,
health: 100, maxHealth: 100,
ammo: [20, 8, 30], // bullets, shells, cells
weapon: 0,
lastFire: 0,
speed: 0.04, runSpeed: 0.07,
};
enemies = [];
pickups = [];
particles = [];
projectiles = [];
score = 0;
enemiesKilled = 0;
exitUnlocked = false;
weaponAnim = 0;
weaponFlash = 0;
damageFlash = 0;
spawnEnemies();
spawnPickups();
}
function spawnEnemies() {
const positions = [
[4,4],[8,3],[12,5],[16,4],[20,3],
[3,9],[7,8],[11,7],[15,9],[19,8],
[5,12],[9,11],[13,13],[17,12],[21,11],
[4,16],[8,15],[12,17],[16,16],[20,15],
[6,19],[10,18],[14,20],[18,19],
];
for (const [x, y] of positions) {
if (MAP_DATA[y][x] === 0) {
enemies.push({
x: x + 0.5, y: y + 0.5,
health: 40 + Math.random() * 30,
maxHealth: 60,
speed: 0.012 + Math.random() * 0.008,
damage: 5 + Math.floor(Math.random() * 8),
attackRate: 1500 + Math.random() * 1000,
lastAttack: 0,
state: 'idle', // idle, chase, attack, hurt
alertRange: 6 + Math.random() * 3,
attackRange: 1.8,
type: Math.floor(Math.random() * 3),
hitTimer: 0,
});
}
}
totalEnemies = enemies.length;
}
function spawnPickups() {
const positions = [
// Health packs
...[2,6],[10,4],[18,7],[5,13],[14,10],[22,16],[8,19],[16,14],
// Ammo - bullets
...[3,3],[15,3],[7,15],[21,5],[11,18],
// Ammo - shells
...[6,6],[18,10],[10,16],[22,9],
// Ammo - cells
...[4,18],[16,7],[8,11],[20,13],
];
for (let i = 0; i < positions.length; i += 2) {
const x = positions[i];
const y = positions[i + 1] || positions[i];
if (MAP_DATA[y][x] === 0) {
let type, amount;
if (i % 6 < 2) { type = 'health'; amount = 25; }
else if (i % 6 < 4) { type = 'bullets'; amount = 8; }
else if (i % 6 < 5) { type = 'shells'; amount = 3; }
else { type = 'cells'; amount = 10; }
pickups.push({ x: x + 0.5, y: y + 0.5, type, amount });
}
}
}
// ============ INPUT ============
function setupInput() {
document.addEventListener('keydown', e => { keysDown[e.key.toLowerCase()] = true; });
document.addEventListener('keyup', e => { keysDown[e.key.toLowerCase()] = false; });
canvas.addEventListener('click', () => {
if (gameState === 'playing') {
canvas.requestPointerLock();
}
});
document.addEventListener('mousemove', e => {
if (document.pointerLockElement === canvas && gameState === 'playing') {
mouseDX += e.movementX;
}
});
document.addEventListener('mousedown', e => {
if (gameState === 'playing' && e.button === 0) {
fireWeapon();
}
});
document.getElementById('startBtn').addEventListener('click', startGame);
document.getElementById('respawnBtn').addEventListener('click', respawnPlayer);
document.getElementById('playAgainBtn').addEventListener('click', () => { resetGame(); startGame(); });
}
function startGame() {
gameState = 'playing';
hideAllScreens();
canvas.requestPointerLock();
lastTime = performance.now();
requestAnimationFrame(gameLoop);
}
function respawnPlayer() {
player.health = player.maxHealth;
player.x = 1.5;
player.y = 1.5;
player.angle = 0;
gameState = 'playing';
hideAllScreens();
canvas.requestPointerLock();
}
function hideAllScreens() {
document.querySelectorAll('.screen').forEach(s => s.classList.remove('visible'));
document.getElementById('overlay').classList.remove('active');
document.getElementById('crosshair').style.display = 'block';
}
// ============ GAME LOOP ============
function gameLoop(timestamp) {
if (gameState !== 'playing') return;
const dt = Math.min((timestamp - lastTime) / 16.67, 3); // normalize to ~60fps ticks
lastTime = timestamp;
update(dt);
render();
requestAnimationFrame(gameLoop);
}
// ============ UPDATE ============
function update(dt) {
// Mouse look
const sensitivity = 0.002 * dt;
player.angle += mouseDX * sensitivity;
mouseDX = 0;
// Pitch (vertical look)
if (keysDown['arrowup'] || keysDown['q']) player.pitch -= 3 * dt;
if (keysDown['arrowdown'] || keysDown['e']) player.pitch += 3 * dt;
player.pitch = Math.max(-60, Math.min(60, player.pitch));
// Weapon switch
if (keysDown['1']) player.weapon = 0;
if (keysDown['2']) player.weapon = 1;
if (keysDown['3']) player.weapon = 2;
// Movement
const speed = keysDown['shift'] ? player.runSpeed : player.speed;
let moveX = 0, moveY = 0;
const cosA = Math.cos(player.angle);
const sinA = Math.sin(player.angle);
if (keysDown['w']) { moveX += cosA * speed * dt; moveY += sinA * speed * dt; }
if (keysDown['s']) { moveX -= cosA * speed * dt; moveY -= sinA * speed * dt; }
if (keysDown['a']) { moveX += sinA * speed * dt; moveY -= cosA * speed * dt; }
if (keysDown['d']) { moveX -= sinA * speed * dt; moveY += cosA * speed * dt; }
// Collision
const margin = 0.2;
const newX = player.x + moveX;
const newY = player.y + moveY;
if (MAP_DATA[Math.floor(player.y)][Math.floor(newX + margin * Math.sign(moveX))] === 0) {
player.x = newX;
}
if (MAP_DATA[Math.floor(newY + margin * Math.sign(moveY))][Math.floor(player.x)] === 0) {
player.y = newY;
}
// Check exit
const tileX = Math.floor(player.x);
const tileY = Math.floor(player.y);
if (MAP_DATA[tileY][tileX] === 6 && exitUnlocked) {
winGame();
return;
}
// Weapon animation decay
weaponAnim = Math.max(0, weaponAnim - dt * 2);
weaponFlash = Math.max(0, weaponFlash - dt * 3);
damageFlash = Math.max(0, damageFlash - dt * 4);
// Update enemies
const now = performance.now();
for (const enemy of enemies) {
if (enemy.health <= 0) continue;
enemy.hitTimer = Math.max(0, enemy.hitTimer - dt);
const dx = player.x - enemy.x;
const dy = player.y - enemy.y;
const dist = Math.sqrt(dx * dx + dy * dy);
// Line of sight check (simplified)
let canSee = dist < enemy.alertRange && hasLineOfSight(enemy.x, enemy.y, player.x, player.y);
if (canSee) {
enemy.state = dist < enemy.attackRange ? 'attack' : 'chase';
} else {
enemy.state = 'idle';
}
if (enemy.state === 'chase') {
const moveSpeed = enemy.speed * dt;
const nx = dx / dist;
const ny = dy / dist;
const enX = enemy.x + nx * moveSpeed;
const enY = enemy.y + ny * moveSpeed;
if (MAP_DATA[Math.floor(enemy.y)][Math.floor(enX)] === 0) enemy.x = enX;
if (MAP_DATA[Math.floor(enY)][Math.floor(enemy.x)] === 0) enemy.y = enY;
}
if (enemy.state === 'attack' && now - enemy.lastAttack > enemy.attackRate) {
enemy.lastAttack = now;
player.health -= enemy.damage;
damageFlash = 1;
if (player.health <= 0) {
player.health = 0;
die();
return;
}
}
// Enemy-enemy collision avoidance
for (const other of enemies) {
if (other === enemy || other.health <= 0) continue;
const edx = enemy.x - other.x;
const edy = enemy.y - other.y;
const edist = Math.sqrt(edx * edx + edy * edy);
if (edist < 0.5 && edist > 0) {
enemy.x += (edx / edist) * 0.01 * dt;
enemy.y += (edy / edist) * 0.01 * dt;
}
}
}
// Update projectiles
for (let i = projectiles.length - 1; i >= 0; i--) {
const p = projectiles[i];
p.x += p.vx * dt;
p.y += p.vy * dt;
p.life -= dt;
if (p.life <= 0 || MAP_DATA[Math.floor(p.y)][Math.floor(p.x)] !== 0) {
spawnParticles(p.x, p.y, p.color, 3);
projectiles.splice(i, 1);
continue;
}
// Hit enemies
for (const enemy of enemies) {
if (enemy.health <= 0) continue;
const dx = p.x - enemy.x;
const dy = p.y - enemy.y;
if (dx * dx + dy * dy < 0.15) {
enemy.health -= p.damage;
enemy.hitTimer = 5;
spawnParticles(enemy.x, enemy.y, [255, 50, 50], 6);
projectiles.splice(i, 1);
if (enemy.health <= 0) {
enemiesKilled++;
score += 100;
spawnParticles(enemy.x, enemy.y, [255, 100, 0], 15);
if (enemiesKilled >= totalEnemies) exitUnlocked = true;
}
break;
}
}
}
// Update particles
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
p.x += p.vx * dt;
p.y += p.vy * dt;
p.life -= dt;
if (p.life <= 0) particles.splice(i, 1);
}
// Pickup collision
for (let i = pickups.length - 1; i >= 0; i--) {
const pk = pickups[i];
const dx = player.x - pk.x;
const dy = player.y - pk.y;
if (dx * dx + dy * dy < 0.25) {
if (pk.type === 'health') {
player.health = Math.min(player.maxHealth, player.health + pk.amount);
} else {
const ammoIdx = pk.type === 'bullets' ? 0 : pk.type === 'shells' ? 1 : 2;
player.ammo[ammoIdx] += pk.amount;
}
score += 10;
pickups.splice(i, 1);
}
}
}
function hasLineOfSight(x1, y1, x2, y2) {
const dx = x2 - x1;
const dy = y2 - y1;
const dist = Math.sqrt(dx * dx + dy * dy);
const steps = Math.ceil(dist * 8);
for (let i = 0; i < steps; i++) {
const t = i / steps;
const cx = x1 + dx * t;
const cy = y1 + dy * t;
if (MAP_DATA[Math.floor(cy)][Math.floor(cx)] !== 0) return false;
}
return true;
}
function fireWeapon() {
const now = performance.now();
const w = WEAPONS[player.weapon];
if (now - player.lastFire < w.fireRate) return;
if (player.ammo[w.ammoType] < w.ammoCost) return;
player.lastFire = now;
player.ammo[w.ammoType] -= w.ammoCost;
weaponAnim = 10;
weaponFlash = 3;
for (let p = 0; p < w.pellets; p++) {
const spreadAngle = player.angle + (Math.random() - 0.5) * w.spread * 2;
if (w.isEnergy) {
// Energy weapon: projectile
projectiles.push({
x: player.x, y: player.y,
vx: Math.cos(spreadAngle) * 0.15,
vy: Math.sin(spreadAngle) * 0.15,
damage: w.damage, life: 30, color: [0, 255, 255],
});
} else {
// Hitscan weapon
raycastHit(player.x, player.y, spreadAngle, MAX_DEPTH, w.damage);
}
}
}
function raycastHit(ox, oy, angle, maxDist, damage) {
const dx = Math.cos(angle);
const dy = Math.sin(angle);
let dist = 0;
while (dist < maxDist) {
dist += 0.05;
const cx = ox + dx * dist;
const cy = oy + dy * dist;
const tx = Math.floor(cx);
const ty = Math.floor(cy);
if (tx < 0 || tx >= MAP_W || ty < 0 || ty >= MAP_H) break;
if (MAP_DATA[ty][tx] !== 0) {
spawnParticles(cx, cy, [255, 200, 100], 3);
return;
}
// Check enemy hit
for (const enemy of enemies) {
if (enemy.health <= 0) continue;
const edx = cx - enemy.x;
const edy = cy - enemy.y;
if (edx * edx + edy * edy < 0.12) {
enemy.health -= damage;
enemy.hitTimer = 5;
spawnParticles(enemy.x, enemy.y, [255, 50, 50], 6);
if (enemy.health <= 0) {
enemiesKilled++;
score += 100;
spawnParticles(enemy.x, enemy.y, [255, 100, 0], 15);
if (enemiesKilled >= totalEnemies) exitUnlocked = true;
}
return;
}
}
}
}
function spawnParticles(x, y, color, count) {
for (let i = 0; i < count; i++) {
particles.push({
x, y,
vx: (Math.random() - 0.5) * 0.04,
vy: (Math.random() - 0.5) * 0.04,
life: 10 + Math.random() * 10,
color, size: 1 + Math.random(),
});
}
}
function die() {
gameState = 'dead';
document.exitPointerLock();
document.getElementById('deathScore').textContent = `Score: ${score} | Enemies slain: ${enemiesKilled}/${totalEnemies}`;
document.getElementById('deathScreen').classList.add('visible');
document.getElementById('overlay').classList.add('active');
document.getElementById('crosshair').style.display = 'none';
}
function winGame() {
gameState = 'win';
document.exitPointerLock();
score += player.health * 10;
document.getElementById('winScore').textContent = `Final Score: ${score}`;
document.getElementById('winScreen').classList.add('visible');
document.getElementById('overlay').classList.add('active');
document.getElementById('crosshair').style.display = 'none';
}
// ============ RENDERING ============
function render() {
// Sky gradient
const skyGrad = ctx.createLinearGradient(0, 0, 0, SCREEN_H / 2);
skyGrad.addColorStop(0, '#1a0505');
skyGrad.addColorStop(1, '#3a1515');
ctx.fillStyle = skyGrad;
ctx.fillRect(0, 0, SCREEN_W, SCREEN_H / 2);
// Floor gradient
const floorGrad = ctx.createLinearGradient(0, SCREEN_H / 2, 0, SCREEN_H);
floorGrad.addColorStop(0, '#1a1a1a');
floorGrad.addColorStop(1, '#333');
ctx.fillStyle = floorGrad;
ctx.fillRect(0, SCREEN_H / 2, SCREEN_W, SCREEN_H / 2);
// Raycasting
const zBuffer = new Float64Array(NUM_RAYS);
const stripW = 1;
for (let i = 0; i < NUM_RAYS; i++) {
const rayAngle = player.angle - HALF_FOV + (i / NUM_RAYS) * FOV;
const cosA = Math.cos(rayAngle);
const sinA = Math.sin(rayAngle);
let dist = 0;
let wallType = 1;
let side = 0; // 0=NS, 1=EW for shading
while (dist < MAX_DEPTH) {
dist += 0.02;
const testX = Math.floor(player.x + cosA * dist);
const testY = Math.floor(player.y + sinA * dist);
if (testX < 0 || testX >= MAP_W || testY < 0 || testY >= MAP_H) {
dist = MAX_DEPTH;
break;
}
const tile = MAP_DATA[testY][testX];
if (tile > 0) {
wallType = tile;
// Determine side for shading
const prevX = Math.floor(player.x + cosA * (dist - 0.02));
const prevY = Math.floor(player.y + sinA * (dist - 0.02));
if (prevX !== testX) side = 1;
dist = dist - 0.01; // slight correction
break;
}
}
zBuffer[i] = dist;
// Fix fisheye
const corrDist = dist * Math.cos(rayAngle - player.angle);
const wallHeight = Math.min(SCREEN_H * 2, SCREEN_H / corrDist);
const wallTop = (SCREEN_H - wallHeight) / 2 + player.pitch;
// Wall color with shading
const wc = WALL_COLORS[wallType] || [100, 100, 100];
const shade = side ? 0.7 : 1.0;
const fogFactor = Math.max(0.15, 1 - dist / MAX_DEPTH);
const r = Math.floor(wc[0] * shade * fogFactor);
const g = Math.floor(wc[1] * shade * fogFactor);
const b = Math.floor(wc[2] * shade * fogFactor);
ctx.fillStyle = `rgb(${r},${g},${b})`;
ctx.fillRect(i, wallTop, stripW, wallHeight);
// Wall edge highlight
if (wallHeight > 4) {
ctx.fillStyle = `rgba(255,255,255,${0.03 * fogFactor})`;
ctx.fillRect(i, wallTop, stripW, 1);
}
}
// Render sprites (enemies + pickups) using painter's algorithm
renderSprites(zBuffer);
// Particles
for (const p of particles) {
const dx = p.x - player.x;
const dy = p.y - player.y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist > MAX_DEPTH || dist < 0.1) continue;
const angle = Math.atan2(dy, dx) - player.angle;
let normAngle = angle;
while (normAngle > Math.PI) normAngle -= 2 * Math.PI;
while (normAngle < -Math.PI) normAngle += 2 * Math.PI;
if (Math.abs(normAngle) > HALF_FOV + 0.3) continue;
const screenX = SCREEN_W / 2 + (normAngle / HALF_FOV) * (SCREEN_W / 2);
const size = p.size * (SCREEN_H / dist) * 0.15;
const alpha = Math.min(1, p.life / 10);
ctx.fillStyle = `rgba(${p.color[0]},${p.color[1]},${p.color[2]},${alpha})`;
ctx.fillRect(screenX - size/2, SCREEN_H/2 + player.pitch - size/2, size, size);
}
// Weapon rendering
renderWeapon();
// Damage flash overlay
if (damageFlash > 0) {
ctx.fillStyle = `rgba(255,0,0,${damageFlash * 0.4})`;
ctx.fillRect(0, 0, SCREEN_W, SCREEN_H);
}
// HUD
renderHUD();
}
function renderSprites(zBuffer) {
// Collect all sprites with distances
const sprites = [];
for (const enemy of enemies) {
if (enemy.health <= 0) continue;
const dx = enemy.x - player.x;
const dy = enemy.y - player.y;
const dist = Math.sqrt(dx * dx + dy * dy);
sprites.push({ x: enemy.x, y: enemy.y, dist, type: 'enemy', ref: enemy });
}
for (const pk of pickups) {
const dx = pk.x - player.x;
const dy = pk.y - player.y;
const dist = Math.sqrt(dx * dx + dy * dy);
sprites.push({ x: pk.x, y: pk.y, dist, type: 'pickup', ref: pk });
}
// Sort back to front
sprites.sort((a, b) => b.dist - a.dist);
for (const sp of sprites) {
if (sp.dist > MAX_DEPTH || sp.dist < 0.15) continue;
const dx = sp.x - player.x;
const dy = sp.y - player.y;
let angle = Math.atan2(dy, dx) - player.angle;
while (angle > Math.PI) angle -= 2 * Math.PI;
while (angle < -Math.PI) angle += 2 * Math.PI;
if (Math.abs(angle) > HALF_FOV + 0.5) continue;
const screenX = SCREEN_W / 2 + (angle / HALF_FOV) * (SCREEN_W / 2);
const corrDist = sp.dist * Math.cos(angle);
const spriteH = (SCREEN_H / corrDist) * 0.7;
const spriteW = spriteH * 0.6;
if (sp.type === 'enemy') {
const enemy = sp.ref;
const fogFactor = Math.max(0.2, 1 - sp.dist / MAX_DEPTH);
const baseColor = enemy.hitTimer > 0 ? [255, 255, 255] :
enemy.type === 0 ? [180, 60, 40] :
enemy.type === 1 ? [60, 140, 60] : [140, 60, 140];
const r = Math.floor(baseColor[0] * fogFactor);
const g = Math.floor(baseColor[1] * fogFactor);
const b = Math.floor(baseColor[2] * fogFactor);
// Check z-buffer for visibility
const startCol = Math.max(0, Math.floor(screenX - spriteW / 2));
const endCol = Math.min(NUM_RAYS - 1, Math.floor(screenX + spriteW / 2));
let visible = false;
for (let c = startCol; c <= endCol; c++) {
if (corrDist < zBuffer[c]) { visible = true; break; }
}
if (!visible) continue;
const topY = (SCREEN_H - spriteH) / 2 + player.pitch;
// Body
ctx.fillStyle = `rgb(${r},${g},${b})`;
for (let c = startCol; c <= endCol; c++) {
if (corrDist < zBuffer[c]) {
ctx.fillRect(c, topY, 1, spriteH);
}
}
// Eyes
const eyeSize = Math.max(2, spriteW * 0.15);
const eyeY = topY + spriteH * 0.2;
ctx.fillStyle = enemy.state === 'attack' ? '#f00' : '#ff0';
ctx.fillRect(screenX - spriteW * 0.2, eyeY, eyeSize, eyeSize);
ctx.fillRect(screenX + spriteW * 0.1, eyeY, eyeSize, eyeSize);
// Health bar
if (spriteH > 15) {
const hbW = spriteW * 0.8;
const hbH = Math.max(2, spriteH * 0.04);
const hbX = screenX - hbW / 2;
const hbY = topY - hbH - 3;
ctx.fillStyle = '#300';
ctx.fillRect(hbX, hbY, hbW, hbH);
ctx.fillStyle = '#f00';
ctx.fillRect(hbX, hbY, hbW * (enemy.health / enemy.maxHealth), hbH);
}
} else if (sp.type === 'pickup') {
const pk = sp.ref;
const fogFactor = Math.max(0.3, 1 - sp.dist / MAX_DEPTH);
let color;
if (pk.type === 'health') color = [255, 50, 50];
else if (pk.type === 'bullets') color = [255, 200, 50];
else if (pk.type === 'shells') color = [100, 150, 255];
else color = [50, 255, 255];
const r = Math.floor(color[0] * fogFactor);
const g = Math.floor(color[1] * fogFactor);
const b = Math.floor(color[2] * fogFactor);
const startCol = Math.max(0, Math.floor(screenX - spriteW / 4));
const endCol = Math.min(NUM_RAYS - 1, Math.floor(screenX + spriteW / 4));
const topY = (SCREEN_H) / 2 + player.pitch - spriteH * 0.15;
for (let c = startCol; c <= endCol; c++) {
if (corrDist < zBuffer[c]) {
ctx.fillStyle = `rgb(${r},${g},${b})`;
ctx.fillRect(c, topY, 1, spriteH * 0.3);
}
}
// Glow effect
const glowSize = Math.max(2, spriteW * 0.15);
for (let c = startCol; c <= endCol; c++) {
if (corrDist < zBuffer[c]) {
ctx.fillStyle = `rgba(${r},${g},${b},0.3)`;
ctx.fillRect(c - glowSize, topY - glowSize, 1 + glowSize * 2, spriteH * 0.3 + glowSize * 2);
}
}
}
}
}
function renderWeapon() {
const w = WEAPONS[player.weapon];
const bobX = Math.sin(performance.now() / 200) * (keysDown['w'] || keysDown['s'] || keysDown['a'] || keysDown['d'] ? 4 : 1);
const bobY = Math.abs(Math.cos(performance.now() / 200)) * (keysDown['w'] || keysDown['s'] || keysDown['a'] || keysDown['d'] ? 6 : 2);
let wx = SCREEN_W / 2 - 15 + bobX;
let wy = SCREEN_H - 70 + bobY;
// Recoil
if (weaponAnim > 0) {
wy += weaponAnim * 3;
}
ctx.save();
if (player.weapon === 0) {
// Pistol
ctx.fillStyle = '#555';
ctx.fillRect(wx + 8, wy + 10, 14, 40);
ctx.fillStyle = '#444';
ctx.fillRect(wx + 6, wy + 30, 18, 25);
ctx.fillStyle = '#333';
ctx.fillRect(wx + 10, wy + 5, 10, 10);
// Muzzle flash
if (weaponFlash > 0) {
ctx.fillStyle = `rgba(255,200,50,${weaponFlash / 3})`;
ctx.beginPath();
ctx.arc(wx + 15, wy - 5, 8 * weaponFlash / 3, 0, Math.PI * 2);
ctx.fill();
}
} else if (player.weapon === 1) {
// Shotgun
ctx.fillStyle = '#654';
ctx.fillRect(wx + 2, wy + 5, 26, 35);
ctx.fillStyle = '#543';
ctx.fillRect(wx + 4, wy + 20, 10, 30);
ctx.fillRect(wx + 16, wy + 20, 10, 30);
ctx.fillStyle = '#765';
ctx.fillRect(wx + 8, wy, 14, 12);
if (weaponFlash > 0) {
ctx.fillStyle = `rgba(255,180,30,${weaponFlash / 3})`;
ctx.beginPath();
ctx.arc(wx + 9, wy - 8, 6 * weaponFlash / 3, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.arc(wx + 21, wy - 8, 6 * weaponFlash / 3, 0, Math.PI * 2);
ctx.fill();
}
} else {
// Plasma rifle
ctx.fillStyle = '#456';
ctx.fillRect(wx + 4, wy + 10, 22, 35);
ctx.fillStyle = '#567';
ctx.fillRect(wx + 8, wy + 5, 14, 12);
ctx.fillStyle = '#0ff';
ctx.fillRect(wx + 10, wy + 2, 10, 6);
if (weaponFlash > 0) {
ctx.fillStyle = `rgba(0,255,255,${weaponFlash / 3})`;
ctx.beginPath();
ctx.arc(wx + 15, wy - 5, 10 * weaponFlash / 3, 0, Math.PI * 2);
ctx.fill();
}
}
ctx.restore();
}
function renderHUD() {
// Bottom bar background
ctx.fillStyle = 'rgba(0,0,0,0.7)';
ctx.fillRect(0, SCREEN_H - 30, SCREEN_W, 30);
ctx.strokeStyle = '#555';
ctx.strokeRect(0, SCREEN_H - 30, SCREEN_W, 30);
// Health bar
const hbX = 10;
const hbY = SCREEN_H - 24;
const hbW = 80;
const healthPct = player.health / player.maxHealth;
ctx.fillStyle = '#300';
ctx.fillRect(hbX, hbY, hbW, 12);
const hColor = healthPct > 0.5 ? '#0a0' : healthPct > 0.25 ? '#aa0' : '#f00';
ctx.fillStyle = hColor;
ctx.fillRect(hbX, hbY, hbW * healthPct, 12);
ctx.strokeStyle = '#fff';
ctx.strokeRect(hbX, hbY, hbW, 12);
// Health text
ctx.fillStyle = '#fff';
ctx.font = '8px monospace';
ctx.fillText(`HP ${player.health}`, hbX + 3, hbY + 9);
// Weapon name
const wName = WEAPONS[player.weapon].name;
ctx.fillStyle = '#ff0';
ctx.font = 'bold 10px monospace';
ctx.textAlign = 'center';
ctx.fillText(wName.toUpperCase(), SCREEN_W / 2, SCREEN_H - 18);
// Ammo counts
const ammoNames = ['BUL', 'SHE', 'CEL'];
for (let i = 0; i < 3; i++) {
const ax = SCREEN_W - 95 + i * 32;
ctx.fillStyle = i === player.weapon ? '#ff0' : '#aaa';
ctx.font = `${i === player.weapon ? 'bold ' : ''}8px monospace`;
ctx.textAlign = 'left';
ctx.fillText(`${ammoNames[i]}:${player.ammo[i]}`, ax, SCREEN_H - 18);
}
// Score
ctx.fillStyle = '#fff';
ctx.font = 'bold 9px monospace';
ctx.textAlign = 'center';
ctx.fillText(`SCORE: ${score}`, SCREEN_W / 2, SCREEN_H - 6);
// Enemies remaining
const remaining = totalEnemies - enemiesKilled;
ctx.fillStyle = exitUnlocked ? '#0f0' : '#f80';
ctx.font = '8px monospace';
ctx.fillText(`DEMONS: ${remaining}`, SCREEN_W / 2, 14);
// Exit indicator
if (exitUnlocked) {
ctx.fillStyle = '#ff0';
ctx.font = 'bold 9px monospace';
ctx.fillText('EXIT UNLOCKED!', SCREEN_W / 2, 26);
}
// Minimap
renderMinimap();
}
function renderMinimap() {
const mmSize = 4;
const mmX = SCREEN_W - MAP_W * mmSize - 5;
const mmY = 5;
ctx.fillStyle = 'rgba(0,0,0,0.6)';
ctx.fillRect(mmX - 1, mmY - 1, MAP_W * mmSize + 2, MAP_H * mmSize + 2);
for (let y = 0; y < MAP_H; y++) {
for (let x = 0; x < MAP_W; x++) {
if (MAP_DATA[y][x] > 0) {
const wc = WALL_COLORS[MAP_DATA[y][x]] || [100,100,100];
ctx.fillStyle = `rgb(${wc[0]},${wc[1]},${wc[2]})`;
} else {
ctx.fillStyle = '#222';
}
ctx.fillRect(mmX + x * mmSize, mmY + y * mmSize, mmSize, mmSize);
}
}
// Enemies on minimap
for (const e of enemies) {
if (e.health <= 0) continue;
ctx.fillStyle = '#f00';
ctx.fillRect(mmX + Math.floor(e.x) * mmSize, mmY + Math.floor(e.y) * mmSize, mmSize, mmSize);
}
// Player on minimap
ctx.fillStyle = '#0f0';
const px = mmX + Math.floor(player.x) * mmSize;
const py = mmY + Math.floor(player.y) * mmSize;
ctx.fillRect(px - 1, py - 1, mmSize + 2, mmSize + 2);
// Player direction indicator
ctx.strokeStyle = '#0f0';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(px + mmSize/2, py + mmSize/2);
ctx.lineTo(px + mmSize/2 + Math.cos(player.angle) * 6, py + mmSize/2 + Math.sin(player.angle) * 6);
ctx.stroke();
}
// ============ START ============
window.addEventListener('load', init);
</script>
</body>
</html>
<!-- agent-meta {"model":"thinkingcap-qwen3.6-27b","provider":"lmstudio","persona":"middle","sessionId":"c9290a37-446b-4d77-8006-35b6a82150cc","tokensIn":14720,"tokensOut":12763,"tokensTotal":27483,"turns":2,"toolCalls":1,"failedToolCalls":0,"timestamp":"2026-07-18T22:32:19.592Z"} -->