DOOM
Qwen 3.6 27b MTP · composer

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 46,947 bytes · SHA-256 ccc2ef84d97e
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>DOOMED CORRIDORS - Retro FPS</title>
<style>
/* ── css: layout ── */
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; background: #000; font-family: 'Courier New', monospace; }
#app { position: relative; width: 100vw; height: 100vh; display: flex; justify-content: center; align-items: center; }
canvas#game { image-rendering: pixelated; image-rendering: crisp-edges; background: #000; cursor: none; }
/* ── css: hud ── */
#hud { position: absolute; bottom: 0; left: 0; right: 0; height: 80px; background: linear-gradient(to bottom, #1a1a2e, #0d0d1a); border-top: 3px solid #444; display: flex; align-items: center; justify-content: space-around; padding: 0 20px; z-index: 10; }
#health-bar { width: 200px; height: 24px; background: #333; border: 2px solid #666; border-radius: 4px; overflow: hidden; position: relative; }
#health-fill { height: 100%; background: linear-gradient(to right, #44cc44, #22aa22); transition: width 0.3s; width: 100%; }
#ammo-display { color: #ffaa00; font-size: 24px; text-shadow: 0 0 10px rgba(255,170,0,0.5); letter-spacing: 2px; }
#weapon-name { color: #ffffff; font-size: 28px; text-shadow: 0 0 15px rgba(255,255,255,0.3); letter-spacing: 4px; }
#score-display { color: #00ffaa; font-size: 24px; text-shadow: 0 0 10px rgba(0,255,170,0.5); letter-spacing: 2px; }
#level-display { color: #aaaaff; font-size: 18px; text-shadow: 0 0 10px rgba(170,170,255,0.5); letter-spacing: 2px; }
#lives-display { color: #ff4444; font-size: 20px; text-shadow: 0 0 10px rgba(255,68,68,0.5); letter-spacing: 2px; }
/* ── css: overlay ── */
#crosshair { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 24px; height: 24px; z-index: 10; pointer-events: none; }
#crosshair::before, #crosshair::after { content: ''; position: absolute; background: rgba(255,255,255,0.7); }
#crosshair::before { width: 2px; height: 100%; left: 50%; transform: translateX(-50%); }
#crosshair::after { height: 2px; width: 100%; top: 50%; transform: translateY(-50%); }
#message-overlay { position: absolute; top: 0; left: 0; right: 0; bottom: 80px; display: flex; align-items: center; justify-content: center; z-index: 20; pointer-events: none; opacity: 0; transition: opacity 0.5s; }
#message-overlay.visible { opacity: 1; }
#message-overlay .msg-text { color: #fff; font-size: 48px; text-shadow: 0 0 30px rgba(255,0,0,0.8), 3px 3px 0 #000; letter-spacing: 6px; text-align: center; }
#minimap-container { position: absolute; top: 10px; right: 10px; z-index: 10; border: 2px solid rgba(0,255,0,0.3); opacity: 0.7; }
canvas#minimap { image-rendering: pixelated; background: rgba(0,0,0,0.6); }
</style>
</head>
<body>
<canvas id="game"></canvas>
<div id="hud">
<div id="health-bar"><div id="health-fill"></div></div>
<div id="ammo-display">AMMO: <span id="ammo-count">0</span></div>
<div id="weapon-name">PISTOL</div>
<div id="score-display">SCORE: <span id="score-count">0</span></div>
<div id="level-display">LEVEL 1</div>
<div id="lives-display">LIVES: <span id="lives-count">3</span></div></div>
<div id="crosshair"></div>
<div id="message-overlay"></div>
<div id="minimap-container"><canvas id="minimap"></canvas></div>
<script>
'use strict';
// ── const: SCREEN_W ──
const SCREEN_W = 640;
// ── const: SCREEN_H ──
const SCREEN_H = 480;
// ── const: MINIMAP_SIZE ──
const MINIMAP_SIZE = 120;
// ── const: TILE ──
const TILE = 1;
// ── const: FOV ──
const FOV = Math.PI / 3;
// ── const: HALF_FOV ──
const HALF_FOV = FOV / 2;
// ── const: NUM_RAYS ──
const NUM_RAYS = SCREEN_W;
// ── const: MAX_DEPTH ──
const MAX_DEPTH = 20;
// ── const: MOVE_SPEED ──
const MOVE_SPEED = 3.0;
// ── const: ROT_SPEED ──
const ROT_SPEED = 2.5;
// ── const: WEAPONS ──
// Weapon definitions
const WEAPONS = {
pistol: { name: 'PISTOL', damage: 15, fireRate: 400, ammoCost: 1, range: 8, spread: 0.02, pellets: 1, color: '#ffcc00' },
shotgun: { name: 'SHOTGUN', damage: 10, fireRate: 800, ammoCost: 3, range: 6, spread: 0.15, pellets: 5, color: '#ff6600' },
plasma: { name: 'PLASMA', damage: 25, fireRate: 200, ammoCost: 2, range: 12, spread: 0.03, pellets: 1, color: '#00ffff' }
};
// ── const: ENEMY_TYPES ──
const ENEMY_TYPES = {
soldier: { hp: 40, speed: 1.2, damage: 8, attackRange: 6, attackRate: 1500, color: '#cc3333', size: 0.3 },
heavy: { hp: 80, speed: 0.8, damage: 15, attackRange: 5, attackRate: 2000, color: '#996633', size: 0.4 },
demon: { hp: 60, speed: 2.0, damage: 12, attackRange: 1.5, attackRate: 800, color: '#cc0066', size: 0.35 }
};
// ── const: WALL_COLORS ──
const WALL_COLORS = {
1: { r: 120, g: 120, b: 120 }, // gray stone
2: { r: 140, g: 80, b: 60 }, // brown brick
3: { r: 60, g: 100, b: 60 } // green mossy
};
// ── function: castRay ──
function castRay(game, angle) {
const sin = Math.sin(angle);
const cos = Math.cos(angle);
// DDA algorithm
let mapX = Math.floor(game.player.x);
let mapY = Math.floor(game.player.y);
const deltaDistX = Math.abs(1 / (cos || 1e-10));
const deltaDistY = Math.abs(1 / (sin || 1e-10));
let stepX, stepY, sideDistX, sideDistY;
if (cos < 0) {
stepX = -1;
sideDistX = (game.player.x - mapX) * deltaDistX;
} else {
stepX = 1;
sideDistX = (mapX + 1 - game.player.x) * deltaDistX;
}
if (sin < 0) {
stepY = -1;
sideDistY = (game.player.y - mapY) * deltaDistY;
} else {
stepY = 1;
sideDistY = (mapY + 1 - game.player.y) * deltaDistY;
}
let hit = false;
let side = 0;
let dist = 0;
let wallType = 1;
for (let i = 0; i < MAX_DEPTH * 2; i++) {
if (sideDistX < sideDistY) {
sideDistX += deltaDistX;
mapX += stepX;
side = 0;
} else {
sideDistY += deltaDistY;
mapY += stepY;
side = 1;
}
if (mapY >= 0 && mapY < game.map.length && mapX >= 0 && mapX < game.map[0].length) {
if (game.map[mapY][mapX] > 0) {
hit = true;
wallType = game.map[mapY][mapX];
break;
}
} else {
hit = true;
break;
}
}
if (side === 0) {
dist = sideDistX - deltaDistX;
} else {
dist = sideDistY - deltaDistY;
}
// Calculate exact wall X for texture mapping / shading
let wallX;
if (side === 0) {
wallX = game.player.y + dist * sin;
} else {
wallX = game.player.x + dist * cos;
}
wallX -= Math.floor(wallX);
return { dist, side, wallType, wallX, hit };
}
// ── function: renderSprites ──
function renderSprites(game) {
const ctx = game.ctx;
// Collect all sprites (enemies + pickups) with their distances
const sprites = [];
for (const enemy of game.enemies) {
if (enemy.state === 'dying' && enemy.deathTimer > 1.0) continue;
const dx = enemy.x - game.player.x;
const dy = enemy.y - game.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 pickup of game.pickups) {
if (!pickup.active) continue;
const dx = pickup.x - game.player.x;
const dy = pickup.y - game.player.y;
const dist = Math.sqrt(dx * dx + dy * dy);
sprites.push({ x: pickup.x, y: pickup.y, dist, type: 'pickup', ref: pickup });
}
// Add exit portal
if (game.exitPos) {
const dx = game.exitPos.x - game.player.x;
const dy = game.exitPos.y - game.player.y;
const dist = Math.sqrt(dx * dx + dy * dy);
sprites.push({ x: game.exitPos.x, y: game.exitPos.y, dist, type: 'exit', ref: null });
}
// Sort back to front
sprites.sort((a, b) => b.dist - a.dist);
for (const sprite of sprites) {
const dx = sprite.x - game.player.x;
const dy = sprite.y - game.player.y;
// Transform with inverse camera matrix
const invDet = 1.0 / (Math.cos(game.player.angle + Math.PI/2) * Math.sin(game.player.angle) -
Math.sin(game.player.angle + Math.PI/2) * Math.cos(game.player.angle));
const planeX = Math.cos(game.player.angle + Math.PI/2) * Math.tan(HALF_FOV);
const planeY = Math.sin(game.player.angle + Math.PI/2) * Math.tan(HALF_FOV);
// Simpler approach: calculate screen position directly
let angle = Math.atan2(dy, dx) - game.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.3) continue; // Behind camera
const perpDist = sprite.dist * Math.cos(angle);
if (perpDist < 0.2) continue;
const screenX = SCREEN_W / 2 + (angle / HALF_FOV) * (SCREEN_W / 2);
const spriteHeight = Math.min(SCREEN_H, SCREEN_H / perpDist);
const spriteWidth = spriteHeight * 0.7;
if (sprite.type === 'enemy') {
renderEnemySprite(ctx, sprite.ref, screenX, spriteHeight, spriteWidth, perpDist);
} else if (sprite.type === 'pickup') {
renderPickupSprite(ctx, sprite.ref, screenX, spriteHeight * 0.5, perpDist);
} else if (sprite.type === 'exit') {
renderExitPortal(ctx, screenX, spriteHeight, spriteWidth, perpDist);
}
}
}
// ── function: renderEnemySprite ──
function renderEnemySprite(ctx, enemy, screenX, height, width, dist) {
const shade = Math.max(0.2, 1 - dist / MAX_DEPTH);
// Check z-buffer for occlusion (center column)
const col = Math.floor(screenX);
if (col >= 0 && col < SCREEN_W) {
if (dist > game.zBuffer[col] + 0.1) return;
}
const drawY = (SCREEN_H - height) / 2;
const flashColor = enemy.hitFlash > 0 ? '#ffffff' :
enemy.state === 'dying' ? `rgba(255,0,0,${1 - enemy.deathTimer})` : null;
// Body
ctx.save();
if (flashColor) {
ctx.globalAlpha = 0.7 + Math.sin(enemy.hitFlash * 20) * 0.3;
}
const bodyW = width * 0.6;
const bodyH = height * 0.5;
const headR = width * 0.2;
const bodyX = screenX - bodyW / 2;
const bodyY = drawY + height * 0.35;
// Legs
ctx.fillStyle = shadeColor('#444', shade);
ctx.fillRect(bodyX + width * 0.1, bodyY + bodyH, bodyW * 0.3, height * 0.25);
ctx.fillRect(bodyX + bodyW - width * 0.1 - bodyW * 0.3, bodyY + bodyH, bodyW * 0.3, height * 0.25);
// Body
const bodyColor = flashColor || shadeColor(enemy.color, shade);
ctx.fillStyle = bodyColor;
ctx.fillRect(bodyX, bodyY, bodyW, bodyH);
// Head
const headY = drawY + height * 0.15;
ctx.beginPath();
ctx.arc(screenX, headY, headR, 0, Math.PI * 2);
ctx.fillStyle = flashColor || shadeColor('#ddaa88', shade);
ctx.fill();
// Eyes (menacing)
if (!flashColor && enemy.state !== 'dying') {
const eyeSize = headR * 0.3;
ctx.fillStyle = shadeColor('#ff0000', shade);
ctx.fillRect(screenX - headR * 0.5, headY - eyeSize * 0.5, eyeSize, eyeSize);
ctx.fillRect(screenX + headR * 0.2, headY - eyeSize * 0.5, eyeSize, eyeSize);
}
// Arms
const armW = width * 0.15;
const armH = bodyH * 0.7;
ctx.fillStyle = flashColor || shadeColor(enemy.color, shade * 0.8);
ctx.fillRect(bodyX - armW, bodyY + bodyH * 0.1, armW, armH);
ctx.fillRect(bodyX + bodyW, bodyY + bodyH * 0.1, armW, armH);
// Health bar above enemy
if (enemy.hp < enemy.maxHp && enemy.state !== 'dying') {
const barW = width * 0.8;
const barH = 4;
const barX = screenX - barW / 2;
const barY = drawY + height * 0.05;
ctx.fillStyle = '#333';
ctx.fillRect(barX, barY, barW, barH);
ctx.fillStyle = enemy.hp > enemy.maxHp * 0.5 ? '#0f0' :
enemy.hp > enemy.maxHp * 0.25 ? '#ff0' : '#f00';
ctx.fillRect(barX, barY, barW * (enemy.hp / enemy.maxHp), barH);
}
ctx.restore();
}
// ── function: renderPickupSprite ──
function renderPickupSprite(ctx, pickup, screenX, size, dist) {
const col = Math.floor(screenX);
if (col >= 0 && col < SCREEN_W) {
if (dist > game.zBuffer[col] + 0.1) return;
}
const shade = Math.max(0.3, 1 - dist / MAX_DEPTH);
const bob = Math.sin(pickup.bobPhase) * size * 0.2;
const drawY = SCREEN_H / 2 + bob;
ctx.save();
ctx.translate(screenX, drawY);
// Glow effect
const glowColor = pickup.type === 'health' ? 'rgba(0,255,0,' : 'rgba(255,170,0,';
ctx.shadowColor = pickup.type === 'health' ? '#0f0' : '#fa0';
ctx.shadowBlur = size * 0.5;
if (pickup.type === 'health') {
// Cross shape
ctx.fillStyle = shadeColor('#ff3333', shade);
const s = size * 0.4;
ctx.fillRect(-s, -s * 0.2, s * 2, s * 0.4);
ctx.fillRect(-s * 0.2, -s, s * 0.4, s * 2);
} else {
// Ammo box
ctx.fillStyle = shadeColor('#ffaa00', shade);
const s = size * 0.35;
ctx.fillRect(-s, -s * 0.7, s * 2, s * 1.4);
ctx.fillStyle = shadeColor('#cc8800', shade);
ctx.fillRect(-s * 0.6, -s * 0.3, s * 1.2, s * 0.6);
}
ctx.restore();
}
// ── function: renderExitPortal ──
function renderExitPortal(ctx, screenX, height, width, dist) {
const col = Math.floor(screenX);
if (col >= 0 && col < SCREEN_W) {
if (dist > game.zBuffer[col] + 0.1) return;
}
const shade = Math.max(0.3, 1 - dist / MAX_DEPTH);
const drawY = (SCREEN_H - height) / 2;
const pulse = Math.sin(performance.now() * 0.005) * 0.3 + 0.7;
ctx.save();
ctx.globalAlpha = shade * pulse;
// Portal frame
ctx.strokeStyle = '#00ffff';
ctx.lineWidth = 3;
ctx.shadowColor = '#00ffff';
ctx.shadowBlur = 20;
ctx.strokeRect(screenX - width/2, drawY + height * 0.1, width, height * 0.8);
// Portal interior
const grad = ctx.createRadialGradient(screenX, SCREEN_H / 2, 0, screenX, SCREEN_H / 2, width);
grad.addColorStop(0, 'rgba(0,255,255,0.8)');
grad.addColorStop(0.5, 'rgba(0,100,255,0.4)');
grad.addColorStop(1, 'rgba(0,0,50,0.1)');
ctx.fillStyle = grad;
ctx.fillRect(screenX - width/2 + 3, drawY + height * 0.1 + 3, width - 6, height * 0.8 - 6);
// EXIT text
if (width > 40) {
ctx.fillStyle = '#fff';
ctx.font = `${Math.max(12, width * 0.15)}px monospace`;
ctx.textAlign = 'center';
ctx.fillText('EXIT', screenX, SCREEN_H / 2 + height * 0.05);
}
ctx.restore();
}
// ── function: shadeColor ──
function shadeColor(hex, factor) {
const r = parseInt(hex.slice(1,3), 16);
const g = parseInt(hex.slice(3,5), 16);
const b = parseInt(hex.slice(5,7), 16);
return `rgb(${Math.floor(r * factor)},${Math.floor(g * factor)},${Math.floor(b * factor)})`;
}
// ── function: renderWeapon ──
function renderWeapon(game) {
const ctx = game.ctx;
const p = game.player;
const w = game.getWeapon();
// Weapon bob
const bobX = Math.sin(p.bobPhase) * 8;
const bobY = Math.abs(Math.cos(p.bobPhase)) * 5;
let shootOffset = 0;
if (p.shootTimer > 0) {
shootOffset = Math.sin((1 - p.shootTimer / 6) * Math.PI) * 20;
}
const baseX = SCREEN_W / 2 + bobX;
const baseY = SCREEN_H - 80 + bobY + shootOffset;
ctx.save();
if (w.name === 'PISTOL') {
// Pistol body
ctx.fillStyle = '#555';
ctx.fillRect(baseX - 12, baseY - 60, 24, 60);
ctx.fillStyle = '#333';
ctx.fillRect(baseX - 8, baseY - 55, 16, 50);
// Barrel
ctx.fillStyle = '#444';
ctx.fillRect(baseX - 5, baseY - 90, 10, 35);
// Grip
ctx.fillStyle = '#654321';
ctx.beginPath();
ctx.moveTo(baseX - 10, baseY + bobY * 0.5);
ctx.lineTo(baseX + 10, baseY + bobY * 0.5);
ctx.lineTo(baseX + 8, baseY - 20);
ctx.lineTo(baseX - 8, baseY - 20);
ctx.fill();
} else if (w.name === 'SHOTGUN') {
// Shotgun body
ctx.fillStyle = '#555';
ctx.fillRect(baseX - 16, baseY - 70, 32, 70);
ctx.fillStyle = '#444';
ctx.fillRect(baseX - 12, baseY - 65, 24, 60);
// Barrel (double)
ctx.fillStyle = '#3a3a3a';
ctx.fillRect(baseX - 10, baseY - 110, 8, 45);
ctx.fillRect(baseX + 2, baseY - 110, 8, 45);
// Stock
ctx.fillStyle = '#654321';
ctx.beginPath();
ctx.moveTo(baseX - 14, baseY);
ctx.lineTo(baseX + 14, baseY);
ctx.lineTo(baseX + 10, baseY - 30);
ctx.lineTo(baseX - 10, baseY - 30);
ctx.fill();
} else if (w.name === 'PLASMA') {
// Plasma rifle body
ctx.fillStyle = '#2a4a6a';
ctx.fillRect(baseX - 18, baseY - 75, 36, 75);
ctx.fillStyle = '#1a3a5a';
ctx.fillRect(baseX - 14, baseY - 70, 28, 65);
// Barrel
ctx.fillStyle = '#3a6a9a';
ctx.fillRect(baseX - 8, baseY - 105, 16, 35);
// Energy core glow
const glowIntensity = p.shootTimer > 0 ? 1 : 0.4 + Math.sin(performance.now() * 0.01) * 0.2;
ctx.shadowColor = '#00ffff';
ctx.shadowBlur = 15 * glowIntensity;
ctx.fillStyle = `rgba(0,255,255,${glowIntensity})`;
ctx.beginPath();
ctx.arc(baseX, baseY - 40, 8, 0, Math.PI * 2);
ctx.fill();
// Stock
ctx.shadowBlur = 0;
ctx.fillStyle = '#1a3a5a';
ctx.fillRect(baseX - 16, baseY - 10, 32, 20);
}
// Muzzle flash
if (p.shootTimer > 0.7) {
const flashSize = w.name === 'SHOTGUN' ? 40 : w.name === 'PLASMA' ? 25 : 20;
ctx.shadowColor = w.color;
ctx.shadowBlur = 30;
ctx.fillStyle = w.color;
ctx.beginPath();
ctx.arc(baseX, baseY - (w.name === 'PISTOL' ? 90 : w.name === 'SHOTGUN' ? 110 : 105), flashSize * p.shootTimer, 0, Math.PI * 2);
ctx.fill();
// Flash rays
for (let i = 0; i < 6; i++) {
const a = (i / 6) * Math.PI * 2 + performance.now() * 0.01;
ctx.strokeStyle = w.color;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(baseX, baseY - (w.name === 'PISTOL' ? 90 : w.name === 'SHOTGUN' ? 110 : 105));
ctx.lineTo(
baseX + Math.cos(a) * flashSize * 2,
baseY - (w.name === 'PISTOL' ? 90 : w.name === 'SHOTGUN' ? 110 : 105) + Math.sin(a) * flashSize * 2
);
ctx.stroke();
}
}
ctx.restore();
}
// ── function: renderParticles ──
function renderParticles(game) {
const ctx = game.ctx;
for (const p of game.particles) {
const dx = p.x - game.player.x;
const dy = p.y - game.player.y;
let angle = Math.atan2(dy, dx) - game.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.3) continue;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 0.1) continue;
const screenX = SCREEN_W / 2 + (angle / HALF_FOV) * (SCREEN_W / 2);
const size = Math.max(1, (SCREEN_H / dist) * p.size * 0.05);
ctx.globalAlpha = p.life;
ctx.fillStyle = p.color;
ctx.fillRect(screenX - size/2, SCREEN_H/2 - size/2 + (p.vy || 0), size, size);
}
ctx.globalAlpha = 1;
}
// ── function: spawnParticles ──
function spawnParticles(game, x, y, color, count) {
for (let i = 0; i < count; i++) {
game.particles.push({
x: x + (Math.random() - 0.5) * 0.3,
y: y + (Math.random() - 0.5) * 0.3,
vx: (Math.random() - 0.5) * 2,
vy: Math.random() * -2,
size: Math.random() * 3 + 1,
color: color,
life: 1,
decay: 0.02 + Math.random() * 0.03
});
}
}
// ── function: updateParticles ──
function updateParticles(game) {
for (let i = game.particles.length - 1; i >= 0; i--) {
const p = game.particles[i];
p.x += p.vx * 0.016;
p.y += p.vy * 0.016;
p.vy -= 5 * 0.016; // gravity
p.life -= p.decay;
if (p.life <= 0) game.particles.splice(i, 1);
}
}
// ── function: renderDamageOverlay ──
function renderDamageOverlay(game) {
const ctx = game.ctx;
if (game.player.hurtFlash > 0) {
ctx.fillStyle = `rgba(255,0,0,${game.player.hurtFlash * 0.4})`;
ctx.fillRect(0, 0, SCREEN_W, SCREEN_H);
// Vignette effect
const grad = ctx.createRadialGradient(SCREEN_W/2, SCREEN_H/2, SCREEN_W*0.3, SCREEN_W/2, SCREEN_H/2, SCREEN_W*0.7);
grad.addColorStop(0, 'rgba(255,0,0,0)');
grad.addColorStop(1, `rgba(255,0,0,${game.player.hurtFlash * 0.6})`);
ctx.fillStyle = grad;
ctx.fillRect(0, 0, SCREEN_W, SCREEN_H);
}
}
// ── function: renderMinimap ──
function renderMinimap(game) {
const ctx = game.minimapCtx;
const scale = MINIMAP_SIZE / Math.max(game.map.length, game.map[0].length);
ctx.fillStyle = 'rgba(0,0,0,0.8)';
ctx.fillRect(0, 0, MINIMAP_SIZE, MINIMAP_SIZE);
// Draw map
for (let y = 0; y < game.map.length; y++) {
for (let x = 0; x < game.map[y].length; x++) {
if (game.map[y][x] > 0) {
const wc = WALL_COLORS[game.map[y][x]];
ctx.fillStyle = `rgb(${wc.r},${wc.g},${wc.b})`;
ctx.fillRect(x * scale, y * scale, scale + 1, scale + 1);
}
}
}
// Draw enemies
for (const e of game.enemies) {
if (e.state === 'dying') continue;
ctx.fillStyle = '#ff0000';
ctx.fillRect(e.x * scale - 2, e.y * scale - 2, 4, 4);
}
// Draw pickups
for (const p of game.pickups) {
if (!p.active) continue;
ctx.fillStyle = p.type === 'health' ? '#00ff00' : '#ffaa00';
ctx.fillRect(p.x * scale - 2, p.y * scale - 2, 4, 4);
}
// Draw exit
if (game.exitPos) {
ctx.fillStyle = '#00ffff';
ctx.fillRect(game.exitPos.x * scale - 3, game.exitPos.y * scale - 3, 6, 6);
}
// Draw player
const px = game.player.x * scale;
const py = game.player.y * scale;
ctx.fillStyle = '#00ff00';
ctx.beginPath();
ctx.arc(px, py, 3, 0, Math.PI * 2);
ctx.fill();
// Player direction
ctx.strokeStyle = '#00ff00';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(px, py);
ctx.lineTo(px + Math.cos(game.player.angle) * 8, py + Math.sin(game.player.angle) * 8);
ctx.stroke();
}
// ── function: isWall ──
function isWall(game, x, y) {
const mx = Math.floor(x);
const my = Math.floor(y);
if (my < 0 || my >= game.map.length || mx < 0 || mx >= game.map[0].length) return true;
return game.map[my][mx] > 0;
}
// ── function: canMove ──
function canMove(game, x, y, radius) {
radius = radius || 0.2;
return !isWall(game, x - radius, y - radius) &&
!isWall(game, x + radius, y - radius) &&
!isWall(game, x - radius, y + radius) &&
!isWall(game, x + radius, y + radius);
}
// ── function: lineOfSight ──
function lineOfSight(game, 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 * 5);
for (let i = 0; i <= steps; i++) {
const t = i / steps;
const x = x1 + dx * t;
const y = y1 + dy * t;
if (isWall(game, x, y)) return false;
}
return true;
}
// ── function: updatePlayer ──
function updatePlayer(game, dt) {
const p = game.player;
let moving = false;
// Rotation from keys
if (game.keys['ArrowLeft'] || game.keys['a']) {
p.angle -= ROT_SPEED * dt;
}
if (game.keys['ArrowRight'] || game.keys['d']) {
p.angle += ROT_SPEED * dt;
}
// Movement
let moveX = 0, moveY = 0;
const cos = Math.cos(p.angle);
const sin = Math.sin(p.angle);
if (game.keys['ArrowUp'] || game.keys['w']) {
moveX += cos * MOVE_SPEED * dt;
moveY += sin * MOVE_SPEED * dt;
moving = true;
}
if (game.keys['ArrowDown'] || game.keys['s']) {
moveX -= cos * MOVE_SPEED * dt;
moveY -= sin * MOVE_SPEED * dt;
moving = true;
}
// Strafe
if (game.keys['q']) {
moveX += sin * MOVE_SPEED * dt * 0.7;
moveY -= cos * MOVE_SPEED * dt * 0.7;
moving = true;
}
if (game.keys['e']) {
moveX -= sin * MOVE_SPEED * dt * 0.7;
moveY += cos * MOVE_SPEED * dt * 0.7;
moving = true;
}
// Collision with sliding
const newX = p.x + moveX;
const newY = p.y + moveY;
if (canMove(game, newX, p.y)) p.x = newX;
if (canMove(game, p.x, newY)) p.y = newY;
// Head bob
if (moving) {
p.bobPhase += dt * 10;
} else {
p.bobPhase *= 0.95;
}
// Hurt flash decay
if (p.hurtFlash > 0) p.hurtFlash -= dt * 3;
// Shoot timer decay
if (p.shootTimer > 0) p.shootTimer -= dt * 2;
}
// ── function: shootWeapon ──
function shootWeapon(game) {
const now = performance.now();
const w = game.getWeapon();
const p = game.player;
if (now - p.lastShot < w.fireRate) return;
if (p.ammo[p.weaponIndex] < w.ammoCost) return;
p.lastShot = now;
p.ammo[p.weaponIndex] -= w.ammoCost;
p.shootTimer = 1.0;
// Play sound effect via oscillator
playSound(w.name === 'PLASMA' ? 800 : w.name === 'SHOTGUN' ? 150 : 400,
w.name === 'SHOTGUN' ? 200 : 100);
for (let i = 0; i < w.pellets; i++) {
const spreadAngle = p.angle + (Math.random() - 0.5) * w.spread;
// Raycast to find hit
let closestEnemy = null;
let closestDist = w.range;
for (const enemy of game.enemies) {
if (enemy.state === 'dying') continue;
const dx = enemy.x - p.x;
const dy = enemy.y - p.y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist > closestDist) continue;
let angleToEnemy = Math.atan2(dy, dx) - spreadAngle;
while (angleToEnemy < -Math.PI) angleToEnemy += 2 * Math.PI;
while (angleToEnemy > Math.PI) angleToEnemy -= 2 * Math.PI;
const hitWidth = enemy.size / dist;
if (Math.abs(angleToEnemy) < hitWidth && lineOfSight(game, p.x, p.y, enemy.x, enemy.y)) {
closestDist = dist;
closestEnemy = enemy;
}
}
if (closestEnemy) {
const damageReduction = 1 - (closestDist / w.range) * 0.5;
closestEnemy.hp -= w.damage * damageReduction;
closestEnemy.hitFlash = 1;
spawnParticles(game, closestEnemy.x, closestEnemy.y, '#ff4444', 3);
if (closestEnemy.hp <= 0) {
closestEnemy.state = 'dying';
closestEnemy.deathTimer = 0;
game.score += closestEnemy.type === 'heavy' ? 200 :
closestEnemy.type === 'demon' ? 150 : 100;
spawnParticles(game, closestEnemy.x, closestEnemy.y, '#ff8800', 10);
} else {
// Alert nearby enemies
for (const e of game.enemies) {
if (e.state === 'idle') {
const d = Math.sqrt((e.x - p.x) ** 2 + (e.y - p.y) ** 2);
if (d < 10) e.state = 'chasing';
}
}
}
} else {
// Wall hit particles
const ray = castRay(game, spreadAngle);
if (ray.hit && ray.dist < w.range) {
const hx = p.x + Math.cos(spreadAngle) * ray.dist;
const hy = p.y + Math.sin(spreadAngle) * ray.dist;
spawnParticles(game, hx, hy, '#ffaa00', 2);
}
}
}
}
// ── function: updateEnemies ──
function updateEnemies(game, dt) {
const now = performance.now();
for (const enemy of game.enemies) {
if (enemy.state === 'dying') {
enemy.deathTimer += dt;
continue;
}
if (enemy.hitFlash > 0) enemy.hitFlash -= dt * 3;
const dx = game.player.x - enemy.x;
const dy = game.player.y - enemy.y;
const dist = Math.sqrt(dx * dx + dy * dy);
// State machine
if (enemy.state === 'idle') {
// Random patrol movement
if (Math.random() < 0.01) enemy.moveAngle += (Math.random() - 0.5) * Math.PI;
const nx = enemy.x + Math.cos(enemy.moveAngle) * enemy.speed * dt * 0.3;
const ny = enemy.y + Math.sin(enemy.moveAngle) * enemy.speed * dt * 0.3;
if (canMove(game, nx, ny)) {
enemy.x = nx;
enemy.y = ny;
} else {
enemy.moveAngle += Math.PI;
}
// Alert on proximity or line of sight
if (dist < enemy.alertRange && lineOfSight(game, enemy.x, enemy.y, game.player.x, game.player.y)) {
enemy.state = 'chasing';
}
} else if (enemy.state === 'chasing') {
// Move toward player
const angleToPlayer = Math.atan2(dy, dx);
if (dist > enemy.attackRange * 0.8) {
const nx = enemy.x + Math.cos(angleToPlayer) * enemy.speed * dt;
const ny = enemy.y + Math.sin(angleToPlayer) * enemy.speed * dt;
// Simple pathfinding: try direct, then slide along walls
if (canMove(game, nx, ny)) {
enemy.x = nx;
enemy.y = ny;
} else if (canMove(game, nx, enemy.y)) {
enemy.x = nx;
} else if (canMove(game, enemy.x, ny)) {
enemy.y = ny;
}
}
// Attack when in range
if (dist < enemy.attackRange && lineOfSight(game, enemy.x, enemy.y, game.player.x, game.player.y)) {
if (now - enemy.lastAttack > enemy.attackRate) {
enemy.lastAttack = now;
game.player.health -= enemy.damage;
game.player.hurtFlash = 1;
// Alert other enemies too
for (const e of game.enemies) {
if (e.state === 'idle') {
const d = Math.sqrt((e.x - game.player.x) ** 2 + (e.y - game.player.y) ** 2);
if (d < 10) e.state = 'chasing';
}
}
}
}
}
// Check if player is still in range for chasing enemies
if (enemy.state === 'chasing' && dist > enemy.alertRange * 2) {
enemy.state = 'idle';
}
}
}
// ── function: updatePickups ──
function updatePickups(game, dt) {
const p = game.player;
for (const pickup of game.pickups) {
if (!pickup.active) continue;
pickup.bobPhase += dt * 3;
const dx = p.x - pickup.x;
const dy = p.y - pickup.y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 0.5) {
if (pickup.type === 'health') {
p.health = Math.min(p.maxHealth, p.health + pickup.amount);
playSound(600, 150);
} else if (pickup.type === 'ammo') {
// Give ammo to all weapons proportionally
for (let i = 0; i < p.ammo.length; i++) {
p.ammo[i] += Math.ceil(pickup.amount / 3);
}
playSound(500, 100);
}
pickup.active = false;
}
}
}
// ── function: showMessage ──
function showMessage(text, color, duration) {
const overlay = document.getElementById('message-overlay');
overlay.innerHTML = `<div class="msg-text" style="color:${color || '#fff'}">${text.replace(/\n/g, '<br>')}</div>`;
overlay.classList.add('visible');
if (duration < 90000) {
setTimeout(() => overlay.classList.remove('visible'), duration);
}
}
// ── let: audioCtx ──
// Simple sound synthesis
let audioCtx = null;
// ── function: playSound ──
function playSound(freq, duration) {
try {
if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.frequency.value = freq;
osc.type = 'square';
gain.gain.setValueAtTime(0.1, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + duration / 1000);
osc.start();
osc.stop(audioCtx.currentTime + duration / 1000);
} catch(e) {}
}
// ── let: game ──
let game;
// ── function: gameLoop ──
function gameLoop() {
const now = performance.now();
const dt = Math.min((now - game.lastTime) / 1000, 0.05); // Cap delta time
game.lastTime = now;
if (game.gameState === 'playing') {
updatePlayer(game, dt);
updateEnemies(game, dt);
updatePickups(game, dt);
updateParticles(game);
// Check player death
if (game.player.health <= 0) {
game.player.health = 0;
handlePlayerDeath(game);
}
checkExit(game);
updateHUD(game);
}
// Render
renderWalls(game);
renderSprites(game);
renderParticles(game);
renderWeapon(game);
renderDamageOverlay(game);
renderMinimap(game);
requestAnimationFrame(gameLoop);
}
// ── function: checkExit ──
function checkExit(game) {
if (!game.exitPos) return;
const dx = game.player.x - game.exitPos.x;
const dy = game.player.y - game.exitPos.y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 0.8) {
const aliveEnemies = game.enemies.filter(e => e.state !== 'dying');
if (aliveEnemies.length === 0) {
game.score += 500; // Level bonus
if (game.currentLevel + 1 >= LEVELS.length) {
// Game complete!
game.gameState = 'won';
showMessage(`🏆 VICTORY!\nFinal Score: ${game.score}\nPress R to play again`, '#ffdd00', 99999);
} else {
// Next level
game.currentLevel++;
showMessage(`LEVEL COMPLETE! +500\nScore: ${game.score}`, '#00ffaa', 2500);
setTimeout(() => {
game.loadLevel(game.currentLevel);
document.getElementById('level-display').textContent = `LEVEL ${game.currentLevel + 1}`;
updateHUD(game);
}, 2500);
}
} else {
showMessage(`KILL ALL ENEMIES FIRST! (${aliveEnemies.length} remaining)`, '#ff4444', 1500);
}
}
}
// ── function: renderWalls ──
function renderWalls(game) {
const ctx = game.ctx;
const stripWidth = SCREEN_W / NUM_RAYS;
// Draw ceiling gradient
const ceilGrad = ctx.createLinearGradient(0, 0, 0, SCREEN_H / 2);
ceilGrad.addColorStop(0, '#0a0515');
ceilGrad.addColorStop(1, '#1a1030');
ctx.fillStyle = ceilGrad;
ctx.fillRect(0, 0, SCREEN_W, SCREEN_H / 2);
// Draw floor gradient
const floorGrad = ctx.createLinearGradient(0, SCREEN_H / 2, 0, SCREEN_H);
floorGrad.addColorStop(0, '#1a1510');
floorGrad.addColorStop(1, '#332a20');
ctx.fillStyle = floorGrad;
ctx.fillRect(0, SCREEN_H / 2, SCREEN_W, SCREEN_H / 2);
// Floor grid lines for depth perception
ctx.strokeStyle = 'rgba(60,50,40,0.3)';
ctx.lineWidth = 1;
const floorStartY = SCREEN_H / 2;
for (let d = 1; d <= MAX_DEPTH; d++) {
const y = floorStartY + (SCREEN_H / 2) * (d / MAX_DEPTH);
if (y > SCREEN_H) break;
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(SCREEN_W, y);
ctx.stroke();
}
for (let i = 0; i < NUM_RAYS; i++) {
const rayAngle = game.player.angle - HALF_FOV + (i / NUM_RAYS) * FOV;
const ray = castRay(game, rayAngle);
// Fix fisheye
const perpDist = ray.dist * Math.cos(rayAngle - game.player.angle);
game.zBuffer[i] = perpDist;
if (!ray.hit || perpDist < 0.1) continue;
const lineHeight = Math.min(SCREEN_H * 2, SCREEN_H / perpDist);
const drawStart = (SCREEN_H - lineHeight) / 2;
const drawEnd = drawStart + lineHeight;
// Wall color with shading
const wc = WALL_COLORS[ray.wallType] || { r: 128, g: 128, b: 128 };
let shade = Math.max(0.15, 1 - perpDist / MAX_DEPTH);
if (ray.side === 1) shade *= 0.7;
// Brick/stone pattern using wallX
const brickShade = (Math.floor(ray.wallX * 8) % 2 === 0) ? 1 : 0.92;
shade *= brickShade;
const r = Math.floor(wc.r * shade);
const g = Math.floor(wc.g * shade);
const b = Math.floor(wc.b * shade);
// Main wall strip
ctx.fillStyle = `rgb(${r},${g},${b})`;
ctx.fillRect(Math.floor(i * stripWidth), drawStart, Math.ceil(stripWidth) + 1, lineHeight);
// Horizontal mortar lines (every ~8th pixel of the wall height maps to a brick row)
const brickHeightPx = lineHeight / 8;
if (brickHeightPx > 2) {
ctx.fillStyle = `rgba(0,0,0,${shade * 0.3})`;
for (let bh = 1; bh < 8; bh++) {
const brickY = drawStart + bh * brickHeightPx;
if (brickY > SCREEN_H) break;
ctx.fillRect(Math.floor(i * stripWidth), brickY, Math.ceil(stripWidth) + 1, Math.max(1, brickHeightPx * 0.15));
}
}
// Top edge highlight
ctx.fillStyle = `rgba(255,255,255,${shade * 0.08})`;
ctx.fillRect(Math.floor(i * stripWidth), drawStart, Math.ceil(stripWidth) + 1, 2);
// Bottom shadow
ctx.fillStyle = `rgba(0,0,0,${Math.min(shade * 0.4, 0.5)})`;
ctx.fillRect(Math.floor(i * stripWidth), drawEnd - 3, Math.ceil(stripWidth) + 1, 3);
// Floor/wall junction shadow line
if (drawEnd < SCREEN_H) {
ctx.fillStyle = `rgba(0,0,0,${shade * 0.2})`;
ctx.fillRect(Math.floor(i * stripWidth), drawEnd, Math.ceil(stripWidth) + 1, 4);
}
}
// Ambient fog overlay at edges of screen
const fogGrad = ctx.createRadialGradient(SCREEN_W/2, SCREEN_H/2, SCREEN_W*0.3, SCREEN_W/2, SCREEN_H/2, SCREEN_W*0.75);
fogGrad.addColorStop(0, 'rgba(0,0,0,0)');
fogGrad.addColorStop(1, 'rgba(0,0,0,0.4)');
ctx.fillStyle = fogGrad;
ctx.fillRect(0, 0, SCREEN_W, SCREEN_H);
}
// ── function: handlePlayerDeath ──
function handlePlayerDeath(game) {
game.lives--;
if (game.lives <= 0) {
game.gameState = 'gameover';
showMessage(`GAME OVER\nFinal Score: ${game.score}\nPress R to restart`, '#ff0000', 99999);
} else {
// Respawn at start position
const level = LEVELS[game.currentLevel];
game.player.health = Math.floor(game.player.maxHealth * 0.5);
game.player.x = level.playerStart.x;
game.player.y = level.playerStart.y;
game.player.angle = level.playerStart.angle;
// Restore some ammo
for (let i = 0; i < game.player.ammo.length; i++) {
game.player.ammo[i] += 5;
}
showMessage(`YOU DIED! Lives: ${game.lives}\nHealth & ammo restored`, '#ff4444', 2500);
// Push enemies away from spawn point to prevent instant death
for (const enemy of game.enemies) {
if (enemy.state === 'dying') continue;
const dx = enemy.x - game.player.x;
const dy = enemy.y - game.player.y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 3) {
enemy.x += (dx / dist) * 4;
enemy.y += (dy / dist) * 4;
// Clamp to map bounds
enemy.x = Math.max(1, Math.min(game.map[0].length - 2, enemy.x));
enemy.y = Math.max(1, Math.min(game.map.length - 2, enemy.y));
}
}
updateHUD(game);
}
}
// ── class: GameState ──
class GameState {
constructor() {
this.canvas = document.getElementById('game');
this.ctx = this.canvas.getContext('2d');
this.minimapCanvas = document.getElementById('minimap');
this.minimapCtx = this.minimapCanvas.getContext('2d');
this.canvas.width = SCREEN_W;
this.canvas.height = SCREEN_H;
this.minimapCanvas.width = MINIMAP_SIZE;
this.minimapCanvas.height = MINIMAP_SIZE;
this.currentLevel = 0;
this.score = 0;
this.lives = 3;
this.gameState = 'playing'; // playing, dead, won, gameover
this.player = {
x: 2.5, y: 2.5, angle: 0,
health: 100, maxHealth: 100,
ammo: [30, 8, 4], // pistol, shotgun, plasma
weaponIndex: 0,
lastShot: 0,
bobPhase: 0,
shooting: false,
shootTimer: 0,
hurtFlash: 0
};
this.enemies = [];
this.pickups = [];
this.particles = [];
this.projectiles = [];
this.map = [];
this.exitPos = null;
this.keys = {};
this.mouseX = 0;
this.lastTime = performance.now();
this.zBuffer = new Float64Array(SCREEN_W);
this.loadLevel(this.currentLevel);
}
loadLevel(levelIdx) {
const level = LEVELS[levelIdx];
this.map = level.map;
this.player.x = level.playerStart.x;
this.player.y = level.playerStart.y;
this.player.angle = level.playerStart.angle;
this.enemies = level.enemies.map(e => ({
x: e.x, y: e.y, type: e.type,
hp: ENEMY_TYPES[e.type].hp,
maxHp: ENEMY_TYPES[e.type].hp,
speed: ENEMY_TYPES[e.type].speed,
damage: ENEMY_TYPES[e.type].damage,
attackRange: ENEMY_TYPES[e.type].attackRange,
attackRate: ENEMY_TYPES[e.type].attackRate,
color: ENEMY_TYPES[e.type].color,
size: ENEMY_TYPES[e.type].size,
lastAttack: 0,
state: 'idle', // idle, chasing, attacking, dying
deathTimer: 0,
alertRange: 8,
moveAngle: Math.random() * Math.PI * 2,
hitFlash: 0
}));
this.pickups = level.pickups.map(p => ({
x: p.x, y: p.y, type: p.type, amount: p.amount, active: true, bobPhase: Math.random() * Math.PI * 2
}));
this.exitPos = { ...level.exitPos };
this.particles = [];
this.projectiles = [];
this.gameState = 'playing';
// Update level display
document.getElementById('level-display').textContent = `LEVEL ${this.currentLevel + 1}`;
}
getWeapon() { return WEAPONS[this.getWeaponKey()]; }
getWeaponKey() {
const keys = Object.keys(WEAPONS);
return keys[this.player.weaponIndex % keys.length];
}
}
// ── function: initGame ──
function initGame() {
game = new GameState();
// Input handling
document.addEventListener('keydown', (e) => {
game.keys[e.key] = true;
if (game.gameState === 'playing') {
// Weapon switching
if (e.key === '1') game.player.weaponIndex = 0;
if (e.key === '2') game.player.weaponIndex = 1;
if (e.key === '3') game.player.weaponIndex = 2;
// Shoot
if (e.key === ' ') {
e.preventDefault();
shootWeapon(game);
}
}
// Restart
if ((e.key === 'r' || e.key === 'R')) {
if (game.gameState === 'gameover' || game.gameState === 'won') {
game.lives = 3;
game.score = 0;
game.currentLevel = 0;
game.loadLevel(0);
document.getElementById('message-overlay').classList.remove('visible');
updateHUD(game);
}
}
updateHUD(game);
});
document.addEventListener('keyup', (e) => {
game.keys[e.key] = false;
});
// Mouse look
const canvas = document.getElementById('game');
canvas.addEventListener('click', () => {
if (game.gameState === 'playing') {
canvas.requestPointerLock();
}
});
document.addEventListener('mousemove', (e) => {
if (document.pointerLockElement === canvas && game.gameState === 'playing') {
game.player.angle += e.movementX * 0.003;
}
});
// Mouse click to shoot
canvas.addEventListener('mousedown', (e) => {
if (game.gameState === 'playing' && document.pointerLockElement === canvas) {
shootWeapon(game);
}
});
// Scroll to switch weapons
canvas.addEventListener('wheel', (e) => {
if (game.gameState !== 'playing') return;
e.preventDefault();
const weaponKeys = Object.keys(WEAPONS);
if (e.deltaY > 0) {
game.player.weaponIndex = (game.player.weaponIndex + 1) % weaponKeys.length;
} else {
game.player.weaponIndex = (game.player.weaponIndex - 1 + weaponKeys.length) % weaponKeys.length;
}
updateHUD(game);
}, { passive: false });
// Start intro message
showMessage('DOOMED CORRIDORS\nClick to lock mouse • WASD move • Mouse aim\nSpace/Click shoot • 1-2-3 weapons', '#ffffff', 4000);
updateHUD(game);
gameLoop();
}
// ── const: LEVELS ──
const LEVELS = [
{
name: "THE LABYRINTH",
map: [
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
[1,0,0,0,0,0,1,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,1],
[1,0,2,2,0,0,0,0,0,3,3,3,0,0,0,0,0,1],
[1,0,2,0,0,0,0,0,0,0,0,3,0,0,0,0,0,1],
[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
[1,1,1,0,0,0,1,1,0,1,1,0,0,1,1,1,1,1],
[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
[1,0,0,3,0,0,0,0,0,0,0,0,2,0,0,0,0,1],
[1,0,0,3,3,0,0,0,0,0,0,2,2,0,0,0,0,1],
[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,1],
[1,0,0,0,0,0,1,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]
],
playerStart: { x: 2.5, y: 2.5, angle: Math.PI / 4 },
enemies: [
{ x: 3.5, y: 4.5, type: 'soldier' }, // visible from spawn through open corridor
{ x: 8.5, y: 3.5, type: 'soldier' },
{ x: 12.5, y: 2.5, type: 'heavy' },
{ x: 4.5, y: 9.5, type: 'demon' },
{ x: 10.5, y: 8.5, type: 'soldier' },
{ x: 6.5, y: 7.5, type: 'heavy' },
],
pickups: [
{ x: 4.5, y: 2.5, type: 'ammo', amount: 10 },
{ x: 13.5, y: 5.5, type: 'health', amount: 25 },
{ x: 7.5, y: 10.5, type: 'ammo', amount: 8 },
{ x: 2.5, y: 9.5, type: 'health', amount: 30 },
],
exitPos: { x: 14.5, y: 12.5 }
},
{
name: "THE FORTRESS",
map: [
[3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3],
[3,0,0,0,2,0,0,0,0,1,0,0,0,0,2,0,0,3],
[3,0,0,0,2,0,0,0,0,1,0,0,0,0,2,0,0,3],
[3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3],
[3,2,2,0,0,0,1,1,1,1,1,1,0,0,0,2,2,3],
[3,0,0,0,0,0,1,0,0,0,0,1,0,0,0,0,0,3],
[3,0,0,0,0,0,1,0,0,0,0,1,0,0,0,0,0,3],
[3,0,0,2,0,0,0,0,0,0,0,0,0,0,2,0,0,3],
[3,0,0,2,0,0,1,0,0,0,0,1,0,0,2,0,0,3],
[3,0,0,0,0,0,1,0,0,0,0,1,0,0,0,0,0,3],
[3,0,0,0,0,0,1,0,0,0,0,1,0,0,0,0,0,3],
[3,2,2,0,0,0,0,0,0,0,0,0,0,0,0,2,2,3],
[3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3],
[3,0,0,0,2,0,0,0,0,1,0,0,0,0,2,0,0,3],
[3,0,0,0,2,0,0,0,0,1,0,0,0,0,2,0,0,3],
[3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3]
],
playerStart: { x: 2.5, y: 2.5, angle: Math.PI / 4 },
enemies: [
{ x: 3.5, y: 3.5, type: 'soldier' }, // visible from spawn
{ x: 9.5, y: 2.5, type: 'heavy' },
{ x: 14.5, y: 3.5, type: 'demon' },
{ x: 5.5, y: 7.5, type: 'soldier' },
{ x: 12.5, y: 8.5, type: 'heavy' },
{ x: 9.5, y: 6.5, type: 'demon' },
{ x: 3.5, y: 10.5, type: 'soldier' },
{ x: 14.5, y: 12.5, type: 'heavy' },
],
pickups: [
{ x: 7.5, y: 3.5, type: 'ammo', amount: 12 },
{ x: 14.5, y: 6.5, type: 'health', amount: 30 },
{ x: 2.5, y: 8.5, type: 'ammo', amount: 10 },
{ x: 9.5, y: 9.5, type: 'health', amount: 40 },
{ x: 13.5, y: 12.5, type: 'ammo', amount: 8 },
],
exitPos: { x: 14.5, y: 14.5 }
}
];
// ── function: updateHUD ──
function updateHUD(game) {
const p = game.player;
document.getElementById('health-fill').style.width = `${(p.health / p.maxHealth) * 100}%`;
if (p.health > 60) {
document.getElementById('health-fill').style.background = 'linear-gradient(to right, #44cc44, #22aa22)';
} else if (p.health > 30) {
document.getElementById('health-fill').style.background = 'linear-gradient(to right, #cccc00, #aaaa00)';
} else {
document.getElementById('health-fill').style.background = 'linear-gradient(to right, #ff4444, #cc2222)';
}
const wKey = p.weaponIndex;
document.getElementById('ammo-count').textContent = p.ammo[wKey];
document.getElementById('weapon-name').textContent = game.getWeapon().name;
document.getElementById('score-count').textContent = game.score;
document.getElementById('lives-count').textContent = '♥'.repeat(Math.max(0, game.lives));
}
// ── main ──
initGame();
</script>
</body>
</html>
<!-- agent-meta {"model":"qwen3.6-27b-mtp","provider":"lmstudio","persona":"composer","sessionId":"a840b232-e433-4575-afd5-0181351d7d8c","tokensIn":1497411,"tokensOut":30422,"tokensTotal":1527833,"turns":55,"toolCalls":54,"failedToolCalls":1,"timestamp":"2026-07-16T04:55:17.340Z"} -->