DOOM
thinkingcap qwen3.6 27b · 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 36,656 bytes · SHA-256 b721bc4179ce
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>DOOMED DEPTHS - Retro FPS</title>
<style>
/* ── css: layout ── */
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #000; overflow: hidden; font-family: 'Courier New', monospace; user-select: none; }
#game-container { position: relative; width: 100vw; height: 100vh; display: flex; justify-content: center; align-items: center; }
#gameCanvas { display: block; image-rendering: pixelated; background: #000; }
#minimap { position: absolute; top: 10px; right: 10px; border: 2px solid #555; opacity: 0.8; }
/* ── css: hud ── */
#hud { position: absolute; bottom: 0; left: 0; right: 0; height: 80px; background: linear-gradient(to top, rgba(0,0,0,0.9), transparent); pointer-events: none; display: flex; align-items: center; justify-content: space-between; padding: 10px 20px; }
#health-bar { width: 200px; height: 24px; background: #333; border: 2px solid #666; border-radius: 4px; overflow: hidden; }
#health-fill { height: 100%; background: linear-gradient(to right, #f00, #0f0); transition: width 0.3s; width: 100%; }
#health-text { color: #fff; font-size: 24px; margin-left: 10px; text-shadow: 2px 2px #000; }
#weapon-info { display: flex; flex-direction: column; align-items: flex-end; }
#weapon-name { color: #ff0; font-size: 18px; text-shadow: 2px 2px #000; }
#ammo-count { color: #fff; font-size: 28px; text-shadow: 2px 2px #000; }
#crosshair { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 20px; height: 20px; 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%); }
/* ── css: overlay ── */
#overlay { position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.85); display: flex; flex-direction: column; justify-content: center; align-items: center; z-index: 10; cursor: pointer; }
#overlay.hidden { display: none; }
#overlay-title { color: #f00; font-size: 48px; text-shadow: 3px 3px #600, 0 0 20px #f00; margin-bottom: 20px; letter-spacing: 4px; }
#overlay-msg { color: #ccc; font-size: 20px; animation: pulse 1.5s infinite; }
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
</style>
</head>
<body>
<div id="game-container">
<canvas id="gameCanvas"></canvas>
<div id="hud">
<div id="health-bar"><div id="health-fill"></div></div>
<span id="health-text">100</span>
<div id="weapon-info">
<span id="weapon-name">PISTOL</span>
<span id="ammo-count">∞</span>
</div>
<div id="crosshair"></div>
<div id="kills-display" style="color:#f44;font-size:16px;text-shadow:2px 2px #000;">KILLS: <span id="kill-count">0</span>/<span id="total-enemies">13</span></div></div>
<div id="overlay" class="hidden">
<h1 id="overlay-title">DOOMED DEPTHS</h1>
<p id="overlay-msg">Click to start</p>
</div>
<canvas id="minimap"></canvas>
</div>
<script>
'use strict';
// ── const: SCREEN_W ──
const SCREEN_W = 640;
// ── const: SCREEN_H ──
const SCREEN_H = 360;
// ── const: MINIMAP_SIZE ──
const MINIMAP_SIZE = 128;
// ── 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.5;
// ── const: ROT_SPEED ──
const ROT_SPEED = 2.5;
// ── const: WALL_COLORS ──
// Wall colors by type
const WALL_COLORS = {
1: '#8B4513', // brown brick
2: '#696969', // gray stone
3: '#556B2F', // dark green
4: '#800020', // burgundy
};
// ── const: MAP ──
// Map: 0=empty, 1-4=walls, 9=exit
const MAP = [
[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,1],
[1,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,4,4,4,0,0,1],
[1,0,0,2,0,0,0,0,0,0,3,0,0,0,0,0,4,0,0,1],
[1,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0,1],
[1,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,2,2,0,0,3,3,0,0,0,0,0,0,1],
[1,0,0,0,0,0,0,2,0,0,0,0,3,0,0,4,4,0,0,1],
[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0,0,1],
[1,0,0,3,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
[1,0,0,3,0,0,0,4,4,0,0,2,2,0,0,0,0,0,0,1],
[1,0,0,0,0,0,0,4,0,0,0,0,2,0,0,3,3,0,0,1],
[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,0,0,1],
[1,0,0,0,0,2,2,0,0,0,0,0,0,0,0,0,0,0,0,1],
[1,0,0,4,0,2,0,0,0,3,3,0,0,4,4,0,0,9,0,1],
[1,0,0,4,0,0,0,0,0,0,3,0,0,4,0,0,0,0,0,1],
[1,0,0,4,4,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,1],
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
];
// ── const: MAP_H ──
const MAP_H = MAP.length;
// ── const: MAP_W ──
const MAP_W = MAP[0].length;
// ── const: WEAPONS ──
const WEAPONS = {
pistol: { name: 'PISTOL', damage: 15, fireRate: 400, ammoUse: 1, range: 8, spread: 0.02, color: '#ff0' },
shotgun: { name: 'SHOTGUN', damage: 10, fireRate: 800, ammoUse: 2, range: 6, spread: 0.08, pellets: 5, color: '#f80' },
plasma: { name: 'PLASMA', damage: 30, fireRate: 150, ammoUse: 1, range: 12, spread: 0.01, color: '#0ff' }
};
// ── const: MAX_AMMO ──
const MAX_AMMO = { pistol: Infinity, shotgun: 20, plasma: 40 };
// ── function: castRay ──
function castRay(px, py, angle) {
const sinA = Math.sin(angle);
const cosA = Math.cos(angle);
// DDA algorithm
let mapX = Math.floor(px);
let mapY = Math.floor(py);
const deltaDistX = Math.abs(1 / (cosA || 0.0001));
const deltaDistY = Math.abs(1 / (sinA || 0.0001));
let stepX, stepY, sideDistX, sideDistY;
if (cosA < 0) {
stepX = -1;
sideDistX = (px - mapX) * deltaDistX;
} else {
stepX = 1;
sideDistX = (mapX + 1 - px) * deltaDistX;
}
if (sinA < 0) {
stepY = -1;
sideDistY = (py - mapY) * deltaDistY;
} else {
stepY = 1;
sideDistY = (mapY + 1 - py) * deltaDistY;
}
let hit = false;
let side = 0; // 0=NS, 1=EW
let dist = 0;
let wallType = 0;
while (!hit) {
if (sideDistX < sideDistY) {
sideDistX += deltaDistX;
mapX += stepX;
side = 0;
} else {
sideDistY += deltaDistY;
mapY += stepY;
side = 1;
}
if (mapY >= 0 && mapY < MAP_H && mapX >= 0 && mapX < MAP_W) {
if (MAP[mapY][mapX] > 0) {
hit = true;
wallType = MAP[mapY][mapX];
}
} else {
hit = true;
dist = MAX_DEPTH;
}
}
if (side === 0) {
dist = sideDistX - deltaDistX;
} else {
dist = sideDistY - deltaDistY;
}
// Calculate exact hit position for texture coordinate
let wallX;
if (side === 0) {
wallX = py + dist * sinA;
} else {
wallX = px + dist * cosA;
}
wallX -= Math.floor(wallX);
return { dist, side, wallType, wallX };
}
// ── function: getMapCell ──
function getMapCell(x, y) {
const mx = Math.floor(x);
const my = Math.floor(y);
if (my < 0 || my >= MAP_H || mx < 0 || mx >= MAP_W) return 1;
return MAP[my][mx];
}
// ── function: isWalkable ──
function isWalkable(x, y) {
// Add small margin for player radius
const r = 0.2;
return getMapCell(x - r, y - r) === 0 &&
getMapCell(x + r, y - r) === 0 &&
getMapCell(x - r, y + r) === 0 &&
getMapCell(x + r, y + r) === 0;
}
// ── function: distanceBetween ──
function distanceBetween(x1, y1, x2, y2) {
return Math.sqrt((x2-x1)**2 + (y2-y1)**2);
}
// ── function: createPlayer ──
function createPlayer() {
return {
x: 1.5, y: 1.5,
angle: 0,
health: 100,
maxHealth: 100,
weapon: 'pistol',
ammo: { pistol: Infinity, shotgun: 8, plasma: 12 },
lastShot: 0,
shooting: false,
shootTimer: 0,
bobPhase: 0,
alive: true
};
}
// ── function: createEnemy ──
function createEnemy(x, y, type) {
return {
x, y, type,
health: type === 'imp' ? 40 : type === 'demon' ? 80 : 25,
maxHealth: type === 'imp' ? 40 : type === 'demon' ? 80 : 25,
speed: type === 'imp' ? 1.5 : type === 'demon' ? 2.5 : 3,
damage: type === 'imp' ? 8 : type === 'demon' ? 15 : 5,
attackRange: type === 'imp' ? 4 : 1.5,
attackCooldown: type === 'imp' ? 1000 : type === 'demon' ? 600 : 300,
lastAttack: 0,
alertDist: type === 'imp' ? 8 : 6,
alive: true,
hitFlash: 0,
color: type === 'imp' ? '#a52a2a' : type === 'demon' ? '#8b0000' : '#4a0',
size: type === 'demon' ? 0.6 : 0.4
};
}
// ── function: createPickup ──
function createPickup(x, y, type) {
return { x, y, type, collected: false };
}
// ── function: renderScene ──
function renderScene(ctx, player) {
const imgData = ctx.createImageData(SCREEN_W, SCREEN_H);
const data = imgData.data;
// Sky gradient (top half) and floor (bottom half)
for (let y = 0; y < SCREEN_H; y++) {
for (let x = 0; x < SCREEN_W; x++) {
const idx = (y * SCREEN_W + x) * 4;
if (y < SCREEN_H / 2) {
// Sky - dark gradient
const t = y / (SCREEN_H / 2);
data[idx] = Math.floor(10 + t * 30);
data[idx+1] = Math.floor(10 + t * 20);
data[idx+2] = Math.floor(40 + t * 50);
} else {
// Floor - dark gradient
const t = (y - SCREEN_H / 2) / (SCREEN_H / 2);
data[idx] = Math.floor(30 + t * 20);
data[idx+1] = Math.floor(25 + t * 15);
data[idx+2] = Math.floor(20 + t * 10);
}
data[idx+3] = 255;
}
}
// Cast rays and draw walls
const zBuffer = new Float64Array(SCREEN_W);
for (let col = 0; col < NUM_RAYS; col++) {
const rayAngle = player.angle - HALF_FOV + (col / NUM_RAYS) * FOV;
const ray = castRay(player.x, player.y, rayAngle);
// Fix fisheye
const perpDist = ray.dist * Math.cos(rayAngle - player.angle);
zBuffer[col] = perpDist;
if (ray.wallType === 0) continue;
const lineHeight = SCREEN_H / perpDist;
let drawStart = Math.floor(SCREEN_H / 2 - lineHeight / 2);
let drawEnd = Math.floor(SCREEN_H / 2 + lineHeight / 2);
// Get wall color with shading
const baseColor = WALL_COLORS[ray.wallType] || '#888';
const r = parseInt(baseColor.slice(1,3), 16);
const g = parseInt(baseColor.slice(3,5), 16);
const b = parseInt(baseColor.slice(5,7), 16);
// Distance shading and side shading
let shade = Math.max(0.15, 1 - perpDist / MAX_DEPTH);
if (ray.side === 1) shade *= 0.7;
for (let row = drawStart; row < drawEnd && row < SCREEN_H; row++) {
if (row < 0) continue;
const idx = (row * SCREEN_W + col) * 4;
// Add stripe pattern for texture effect
let texShade = shade;
const wallTexY = ((row - drawStart) / lineHeight);
if (Math.floor(wallTexY * 8) % 2 === 0 && ray.wallX > 0.1 && ray.wallX < 0.9) {
texShade *= 0.9;
}
data[idx] = Math.min(255, r * texShade);
data[idx+1] = Math.min(255, g * texShade);
data[idx+2] = Math.min(255, b * texShade);
}
}
ctx.putImageData(imgData, 0, 0);
return zBuffer;
}
// ── function: drawSprite ──
function drawSprite(ctx, sprite, player, zBuffer) {
if (!sprite.alive && sprite.type !== 'pickup') return;
const dx = sprite.x - player.x;
const dy = sprite.y - player.y;
const dist = Math.sqrt(dx*dx + dy*dy);
// Transform sprite relative to player direction
const invDet = 1.0 / (Math.cos(player.angle) * Math.cos(player.angle) + Math.sin(player.angle) * Math.sin(player.angle));
const transformX = Math.cos(player.angle) * dy - Math.sin(player.angle) * dx;
const transformY = Math.sin(player.angle) * dx + Math.cos(player.angle) * dy;
if (transformY <= 0.1) return; // Behind player
const spriteScreenX = Math.floor((SCREEN_W / 2) * (1 + transformX / transformY));
const spriteHeight = Math.abs(SCREEN_H / transformY);
const spriteWidth = spriteHeight * 0.7;
const drawStartX = Math.floor(spriteScreenX - spriteWidth / 2);
const drawEndX = Math.floor(spriteScreenX + spriteWidth / 2);
const drawStartY = Math.floor(SCREEN_H / 2 - spriteHeight / 2);
const drawEndY = Math.floor(SCREEN_H / 2 + spriteHeight / 2);
// Check z-buffer for visibility
let visible = false;
for (let x = Math.max(0, drawStartX); x < Math.min(SCREEN_W, drawEndX); x++) {
if (x >= 0 && x < SCREEN_W && transformY < zBuffer[x]) {
visible = true;
break;
}
}
if (!visible) return;
const shade = Math.max(0.2, 1 - dist / MAX_DEPTH);
ctx.save();
for (let x = Math.max(0, drawStartX); x < Math.min(SCREEN_W, drawEndX); x++) {
if (x >= SCREEN_W || transformY >= zBuffer[x]) continue;
const texU = (x - drawStartX) / (drawEndX - drawStartX);
// Draw sprite body as a simple shape
for (let y = Math.max(0, drawStartY); y < Math.min(SCREEN_H, drawEndY); y++) {
const texV = (y - drawStartY) / (drawEndY - drawStartY);
// Simple sprite rendering: body + head
let isBody = false;
if (sprite.type === 'pickup') {
// Diamond shape for pickups
const cx = Math.abs(texU - 0.5) * 2;
const cy = Math.abs(texV - 0.5) * 2;
isBody = (cx + cy < 1);
} else {
// Enemy body: wider at bottom, head on top
const bodyWidth = 0.3 + texV * 0.2;
if (texV > 0.3 && Math.abs(texU - 0.5) < bodyWidth) isBody = true;
if (texV <= 0.35 && Math.abs(texU - 0.5) < 0.18) isBody = true; // head
}
if (!isBody) continue;
let color;
if (sprite.type === 'pickup') {
if (sprite.type === 'health' || sprite.subtype === 'health') {
color = `rgb(${Math.floor(0*shade)},${Math.floor(255*shade)},${Math.floor(0*shade)})`;
} else {
color = `rgb(${Math.floor(255*shade)},${Math.floor(200*shade)},${Math.floor(0*shade)})`;
}
} else if (sprite.hitFlash > 0) {
color = '#fff';
} else {
const r = parseInt(sprite.color.slice(1,3), 16);
const g = parseInt(sprite.color.slice(3,5), 16);
const b = parseInt(sprite.color.slice(5,7), 16);
color = `rgb(${Math.floor(r*shade)},${Math.floor(g*shade)},${Math.floor(b*shade)})`;
}
ctx.fillStyle = color;
ctx.fillRect(x, y, 1, 1);
}
// Draw eyes for enemies
if (sprite.type !== 'pickup') {
const eyeY = Math.floor(SCREEN_H / 2 - spriteHeight * 0.35);
const eyeSize = Math.max(1, spriteWidth * 0.08);
ctx.fillStyle = `rgba(255,${sprite.hitFlash > 0 ? 0 : 100},0,${shade})`;
ctx.fillRect(spriteScreenX - Math.floor(spriteWidth*0.1), eyeY, eyeSize, eyeSize);
ctx.fillRect(spriteScreenX + Math.floor(spriteWidth*0.05), eyeY, eyeSize, eyeSize);
}
}
// Health bar above enemy
if (sprite.type !== 'pickup' && sprite.health < sprite.maxHealth) {
const barW = spriteWidth * 0.8;
const barH = Math.max(2, spriteHeight * 0.03);
const barX = spriteScreenX - barW / 2;
const barY = drawStartY - barH - 4;
ctx.fillStyle = '#300';
ctx.fillRect(barX, barY, barW, barH);
ctx.fillStyle = '#f00';
ctx.fillRect(barX, barY, barW * (sprite.health / sprite.maxHealth), barH);
}
ctx.restore();
}
// ── function: drawWeapon ──
function drawWeapon(ctx, player) {
const bobOffset = Math.sin(player.bobPhase) * 5;
const weaponData = WEAPONS[player.weapon];
// Weapon position at bottom center
const wx = SCREEN_W / 2 + bobOffset;
const wy = SCREEN_H - 10 + Math.abs(bobOffset);
ctx.save();
if (player.shootTimer > 0) {
// Muzzle flash
const flashSize = 30 * (player.shootTimer / WEAPONS[player.weapon].fireRate);
ctx.fillStyle = weaponData.color;
ctx.globalAlpha = player.shootTimer / WEAPONS[player.weapon].fireRate;
ctx.beginPath();
ctx.arc(wx, wy - 80, flashSize, 0, Math.PI * 2);
ctx.fill();
ctx.globalAlpha = 1;
// Bright center of flash
ctx.fillStyle = '#fff';
ctx.beginPath();
ctx.arc(wx, wy - 80, flashSize * 0.4, 0, Math.PI * 2);
ctx.fill();
}
// Draw weapon body
if (player.weapon === 'pistol') {
ctx.fillStyle = '#666';
ctx.fillRect(wx - 8, wy - 70, 16, 50);
ctx.fillStyle = '#444';
ctx.fillRect(wx - 12, wy - 30, 24, 30);
} else if (player.weapon === 'shotgun') {
ctx.fillStyle = '#8B4513';
ctx.fillRect(wx - 6, wy - 90, 12, 70);
ctx.fillStyle = '#555';
ctx.fillRect(wx - 10, wy - 30, 20, 30);
// Double barrel look
ctx.fillStyle = '#8B4513';
ctx.fillRect(wx - 10, wy - 90, 6, 70);
ctx.fillRect(wx + 4, wy - 90, 6, 70);
} else if (player.weapon === 'plasma') {
ctx.fillStyle = '#333';
ctx.fillRect(wx - 12, wy - 80, 24, 55);
// Glowing parts
ctx.fillStyle = weaponData.color;
ctx.globalAlpha = 0.7 + Math.sin(Date.now() * 0.01) * 0.3;
ctx.fillRect(wx - 6, wy - 85, 12, 10);
ctx.fillRect(wx - 4, wy - 50, 8, 20);
ctx.globalAlpha = 1;
}
ctx.restore();
}
// ── function: drawMinimap ──
function drawMinimap(ctx, player, enemies) {
const mctx = ctx;
const cellSize = MINIMAP_SIZE / MAP_W;
mctx.fillStyle = 'rgba(0,0,0,0.7)';
mctx.fillRect(0, 0, MINIMAP_SIZE, MINIMAP_SIZE);
// Draw map
for (let y = 0; y < MAP_H; y++) {
for (let x = 0; x < MAP_W; x++) {
if (MAP[y][x] > 0) {
mctx.fillStyle = WALL_COLORS[MAP[y][x]] || '#888';
mctx.fillRect(x * cellSize, y * cellSize, cellSize, cellSize);
}
}
}
// Draw enemies
for (const e of enemies) {
if (!e.alive) continue;
mctx.fillStyle = '#f00';
mctx.fillRect(e.x * cellSize - 1, e.y * cellSize - 1, 3, 3);
}
// Draw player
mctx.fillStyle = '#0f0';
mctx.beginPath();
mctx.arc(player.x * cellSize, player.y * cellSize, 2, 0, Math.PI * 2);
mctx.fill();
// Player direction indicator
mctx.strokeStyle = '#0f0';
mctx.lineWidth = 1;
mctx.beginPath();
mctx.moveTo(player.x * cellSize, player.y * cellSize);
mctx.lineTo(
(player.x + Math.cos(player.angle) * 0.5) * cellSize,
(player.y + Math.sin(player.angle) * 0.5) * cellSize
);
mctx.stroke();
}
// ── function: updateEnemies ──
function updateEnemies(enemies, player, dt) {
const now = Date.now();
for (const e of enemies) {
if (!e.alive) continue;
const dist = distanceBetween(player.x, player.y, e.x, e.y);
// Alert when player is close enough
if (dist < e.alertDist && player.alive) {
// Move toward player
const angle = Math.atan2(player.y - e.y, player.x - e.x);
const newX = e.x + Math.cos(angle) * e.speed * dt;
const newY = e.y + Math.sin(angle) * e.speed * dt;
if (isWalkable(newX, e.y)) e.x = newX;
if (isWalkable(e.x, newY)) e.y = newY;
// Attack when in range
if (dist < e.attackRange && now - e.lastAttack > e.attackCooldown) {
player.health -= e.damage;
e.lastAttack = now;
return e.damage; // damage taken
}
}
// Hit flash decay
if (e.hitFlash > 0) e.hitFlash -= dt * 3;
}
return 0;
}
// ── function: checkPickups ──
function checkPickups(player, pickups) {
for (const p of pickups) {
if (p.collected) continue;
const dist = distanceBetween(player.x, player.y, p.x, p.y);
if (dist < 0.5) {
p.collected = true;
// Play pickup sound
playSound('pickup');
if (p.type === 'health') {
player.health = Math.min(player.maxHealth, player.health + 25);
} else if (p.type === 'shotgun_ammo') {
player.ammo.shotgun = Math.min(MAX_AMMO.shotgun, player.ammo.shotgun + 8);
} else if (p.type === 'plasma_ammo') {
player.ammo.plasma = Math.min(MAX_AMMO.plasma, player.ammo.plasma + 12);
} else if (p.type === 'shotgun_weapon') {
player.weapon = 'shotgun';
player.ammo.shotgun = Math.min(MAX_AMMO.shotgun, player.ammo.shotgun + 8);
} else if (p.type === 'plasma_weapon') {
player.weapon = 'plasma';
player.ammo.plasma = Math.min(MAX_AMMO.plasma, player.ammo.plasma + 12);
}
}
}
}
// ── function: checkWinCondition ──
function checkWinCondition(player, enemies) {
// Win: all enemies dead AND reach exit tile (9)
const allDead = enemies.every(e => !e.alive);
if (!allDead) return false;
const cell = getMapCell(player.x, player.y);
return cell === 9;
}
// ── function: respawnPlayer ──
function respawnPlayer(player) {
player.health = player.maxHealth;
player.alive = true;
player.x = 1.5;
player.y = 1.5;
player.angle = 0;
// Keep weapons and ammo
}
// ── function: handleInput ──
function handleInput(player, dt) {
const moveX = Math.cos(player.angle);
const moveY = Math.sin(player.angle);
const strafeX = Math.cos(player.angle + Math.PI/2);
const strafeY = Math.sin(player.angle + Math.PI/2);
let moved = false;
if (keys['KeyW'] || keys['ArrowUp']) {
const nx = player.x + moveX * MOVE_SPEED * dt;
const ny = player.y + moveY * MOVE_SPEED * dt;
if (isWalkable(nx, player.y)) player.x = nx;
if (isWalkable(player.x, ny)) player.y = ny;
moved = true;
}
if (keys['KeyS'] || keys['ArrowDown']) {
const nx = player.x - moveX * MOVE_SPEED * dt;
const ny = player.y - moveY * MOVE_SPEED * dt;
if (isWalkable(nx, player.y)) player.x = nx;
if (isWalkable(player.x, ny)) player.y = ny;
moved = true;
}
if (keys['KeyA']) {
const nx = player.x - strafeX * MOVE_SPEED * dt;
const ny = player.y - strafeY * MOVE_SPEED * dt;
if (isWalkable(nx, player.y)) player.x = nx;
if (isWalkable(player.x, ny)) player.y = ny;
moved = true;
}
if (keys['KeyD']) {
const nx = player.x + strafeX * MOVE_SPEED * dt;
const ny = player.y + strafeY * MOVE_SPEED * dt;
if (isWalkable(nx, player.y)) player.x = nx;
if (isWalkable(player.x, ny)) player.y = ny;
moved = true;
}
// Rotation
if (keys['ArrowLeft']) player.angle -= ROT_SPEED * dt;
if (keys['ArrowRight']) player.angle += ROT_SPEED * dt;
// Head bob
if (moved) {
player.bobPhase += dt * 10;
} else {
player.bobPhase *= 0.9;
}
}
// ── function: handleMouseInput ──
function handleMouseInput(player, mouse) {
document.addEventListener('mousemove', (e) => {
if (document.pointerLockElement && gameRunning) {
player.angle += e.movementX * 0.003;
}
});
document.addEventListener('mousedown', (e) => {
if (!gameRunning) return;
if (!player.alive) return;
// Try to lock pointer on first click
if (!document.pointerLockElement) {
document.getElementById('gameCanvas').requestPointerLock();
return;
}
shootWeapon(player, enemies);
});
}
// ── function: showOverlay ──
function showOverlay(title, msg) {
const overlay = document.getElementById('overlay');
const titleEl = document.getElementById('overlay-title');
const msgEl = document.getElementById('overlay-msg');
if (titleEl) titleEl.textContent = title;
if (msgEl) msgEl.textContent = msg;
overlay.classList.remove('hidden');
}
// ── function: hideOverlay ──
function hideOverlay() {
document.getElementById('overlay').classList.add('hidden');
}
// ── function: updateHUD ──
function updateHUD(player) {
const healthFill = document.getElementById('health-fill');
const healthText = document.getElementById('health-text');
const weaponName = document.getElementById('weapon-name');
const ammoCount = document.getElementById('ammo-count');
const killCount = document.getElementById('kill-count');
if (healthFill && healthText && weaponName && ammoCount) {
const pct = Math.max(0, player.health / player.maxHealth * 100);
healthFill.style.width = pct + '%';
healthText.textContent = Math.ceil(player.health);
// Health bar color based on level
if (pct > 60) {
healthFill.style.background = 'linear-gradient(to right, #0a0, #0f0)';
} else if (pct > 30) {
healthFill.style.background = 'linear-gradient(to right, #aa0, #ff0)';
} else {
healthFill.style.background = 'linear-gradient(to right, #a00, #f00)';
}
weaponName.textContent = WEAPONS[player.weapon].name;
if (player.weapon === 'pistol') {
ammoCount.textContent = '∞';
} else {
const amt = player.ammo[player.weapon];
ammoCount.textContent = amt;
ammoCount.style.color = amt <= 3 ? '#f44' : '#fff';
}
}
if (killCount) {
killCount.textContent = enemies.filter(e => !e.alive).length;
}
}
// ── let: audioCtx ──
// Simple sound synthesis for weapon effects
let audioCtx = null;
// ── function: playSound ──
function playSound(type) {
try {
if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.connect(gain);
gain.connect(audioCtx.destination);
const now = audioCtx.currentTime;
if (type === 'pistol') {
osc.type = 'square';
osc.frequency.setValueAtTime(200, now);
osc.frequency.exponentialRampToValueAtTime(50, now + 0.1);
gain.gain.setValueAtTime(0.15, now);
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.1);
osc.start(now);
osc.stop(now + 0.1);
} else if (type === 'shotgun') {
// Noise-like burst using multiple oscillators
for (let i = 0; i < 3; i++) {
const o = audioCtx.createOscillator();
const g = audioCtx.createGain();
o.connect(g);
g.connect(audioCtx.destination);
o.type = 'sawtooth';
o.frequency.setValueAtTime(100 + Math.random()*50, now);
o.frequency.exponentialRampToValueAtTime(30, now + 0.2);
g.gain.setValueAtTime(0.12, now);
g.gain.exponentialRampToValueAtTime(0.001, now + 0.2);
o.start(now);
o.stop(now + 0.2);
}
} else if (type === 'plasma') {
osc.type = 'sine';
osc.frequency.setValueAtTime(800, now);
osc.frequency.exponentialRampToValueAtTime(200, now + 0.15);
gain.gain.setValueAtTime(0.1, now);
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.15);
osc.start(now);
osc.stop(now + 0.15);
} else if (type === 'hit') {
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(300, now);
osc.frequency.exponentialRampToValueAtTime(100, now + 0.08);
gain.gain.setValueAtTime(0.08, now);
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.08);
osc.start(now);
osc.stop(now + 0.08);
} else if (type === 'pickup') {
osc.type = 'sine';
osc.frequency.setValueAtTime(400, now);
osc.frequency.linearRampToValueAtTime(600, now + 0.1);
gain.gain.setValueAtTime(0.08, now);
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.15);
osc.start(now);
osc.stop(now + 0.15);
} else if (type === 'death') {
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(200, now);
osc.frequency.exponentialRampToValueAtTime(30, now + 0.5);
gain.gain.setValueAtTime(0.15, now);
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.5);
osc.start(now);
osc.stop(now + 0.5);
}
} catch(e) { /* Audio not available */ }
}
// ── function: shootWeapon ──
function shootWeapon(player, enemies) {
const now = Date.now();
const weapon = WEAPONS[player.weapon];
if (now - player.lastShot < weapon.fireRate) return false;
if (player.ammo[player.weapon] <= 0 && player.weapon !== 'pistol') return false;
player.lastShot = now;
player.shootTimer = 150;
// Play weapon sound
playSound(player.weapon);
if (player.weapon !== 'pistol') {
player.ammo[player.weapon] -= weapon.ammoUse;
}
const pellets = weapon.pellets || 1;
let hitAny = false;
for (let p = 0; p < pellets; p++) {
const spreadAngle = player.angle + (Math.random() - 0.5) * weapon.spread * 2;
let closestHit = null;
let closestDist = Infinity;
for (const e of enemies) {
if (!e.alive) continue;
const dx = e.x - player.x;
const dy = e.y - player.y;
const dist = Math.sqrt(dx*dx + dy*dy);
if (dist > weapon.range) continue;
const angleToEnemy = Math.atan2(dy, dx);
let angleDiff = spreadAngle - angleToEnemy;
while (angleDiff > Math.PI) angleDiff -= 2 * Math.PI;
while (angleDiff < -Math.PI) angleDiff += 2 * Math.PI;
const hitWidth = e.size / dist;
if (Math.abs(angleDiff) < hitWidth && dist < closestDist) {
const ray = castRay(player.x, player.y, angleToEnemy);
if (ray.dist > dist * 0.95) {
closestHit = e;
closestDist = dist;
}
}
}
if (closestHit) {
const dmgMult = 1 - (closestDist / weapon.range) * 0.3;
closestHit.health -= Math.floor(weapon.damage * dmgMult);
closestHit.hitFlash = 1;
if (closestHit.health <= 0) {
closestHit.alive = false;
}
hitAny = true;
}
}
return hitAny;
}
// ── function: drawDamageOverlay ──
function drawDamageOverlay(ctx, damage) {
if (damage <= 0) return;
const alpha = Math.min(0.6, damage / 50);
ctx.fillStyle = `rgba(255, 0, 0, ${alpha})`;
ctx.fillRect(0, 0, SCREEN_W, SCREEN_H);
}
// ── function: drawVignette ──
function drawVignette(ctx) {
const gradient = ctx.createRadialGradient(SCREEN_W/2, SCREEN_H/2, SCREEN_H*0.3, SCREEN_W/2, SCREEN_H/2, SCREEN_H*0.8);
gradient.addColorStop(0, 'rgba(0,0,0,0)');
gradient.addColorStop(1, 'rgba(0,0,0,0.5)');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, SCREEN_W, SCREEN_H);
}
// ── let: weaponNotifyTimer ──
// Weapon switch notification
let weaponNotifyTimer = 0;
// ── let: weaponNotifyText ──
let weaponNotifyText = '';
// ── function: notifyWeapon ──
function notifyWeapon(name) {
weaponNotifyText = name;
weaponNotifyTimer = 120;
}
// ── function: drawNotifications ──
function drawNotifications(ctx) {
if (weaponNotifyTimer > 0) {
const alpha = Math.min(1, weaponNotifyTimer / 30);
ctx.fillStyle = `rgba(255, 255, 0, ${alpha})`;
ctx.font = 'bold 24px Courier New';
ctx.textAlign = 'center';
ctx.fillText('>>> ' + weaponNotifyText + ' <<<', SCREEN_W/2, SCREEN_H * 0.3);
weaponNotifyTimer--;
}
}
// ── let: prevWeapon ──
// Track previous weapon for notifications
let prevWeapon = 'pistol';
// ── function: initInput ──
function initInput(keys, mouse) {
document.addEventListener('keydown', (e) => {
keys[e.code] = true;
if (e.code === 'Digit1') {
if (player.weapon !== 'pistol') { player.weapon = 'pistol'; notifyWeapon(WEAPONS.pistol.name); }
}
if (e.code === 'Digit2') {
if (player.weapon !== 'shotgun') { player.weapon = 'shotgun'; notifyWeapon(WEAPONS.shotgun.name); }
}
if (e.code === 'Digit3') {
if (player.weapon !== 'plasma') { player.weapon = 'plasma'; notifyWeapon(WEAPONS.plasma.name); }
}
if (['Space', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.code)) {
e.preventDefault();
}
});
document.addEventListener('keyup', (e) => {
keys[e.code] = false;
});
}
// ── main ──
// Setup canvases
const canvas = document.getElementById('gameCanvas');
canvas.width = SCREEN_W;
canvas.height = SCREEN_H;
canvas.style.width = '100vw';
canvas.style.height = 'auto';
canvas.style.maxHeight = 'calc(100vh - 80px)';
const ctx = canvas.getContext('2d');
ctx.imageSmoothingEnabled = false;
// Minimap
const minimapCanvas = document.getElementById('minimap');
minimapCanvas.width = MINIMAP_SIZE;
minimapCanvas.height = MINIMAP_SIZE;
const mctx = minimapCanvas.getContext('2d');
// Game state
let player = createPlayer();
let enemies = [];
let pickups = [];
let keys = {};
let mouse = { x: 0, y: 0 };
let gameRunning = false;
let gameOver = false;
let damageFlash = 0;
const totalEl = document.getElementById('total-enemies');
if (totalEl) totalEl.textContent = '13';
function spawnEnemies() {
const positions = [
{x:5.5,y:3.5,type:'soldier'},
{x:10.5,y:2.5,type:'imp'},
{x:15.5,y:4.5,type:'soldier'},
{x:8.5,y:7.5,type:'demon'},
{x:3.5,y:9.5,type:'imp'},
{x:12.5,y:8.5,type:'soldier'},
{x:6.5,y:11.5,type:'imp'},
{x:14.5,y:10.5,type:'demon'},
{x:9.5,y:13.5,type:'soldier'},
{x:4.5,y:15.5,type:'imp'},
{x:16.5,y:14.5,type:'soldier'},
{x:10.5,y:16.5,type:'demon'},
{x:7.5,y:17.5,type:'imp'},
];
enemies = positions.map(p => createEnemy(p.x, p.y, p.type));
}
function spawnPickups() {
const positions = [
{x:3.5,y:2.5,type:'health'},
{x:8.5,y:4.5,type:'shotgun_ammo'},
{x:14.5,y:2.5,type:'plasma_ammo'},
{x:6.5,y:7.5,type:'health'},
{x:10.5,y:9.5,type:'shotgun_weapon'},
{x:3.5,y:12.5,type:'plasma_ammo'},
{x:15.5,y:8.5,type:'health'},
{x:8.5,y:14.5,type:'shotgun_ammo'},
{x:12.5,y:16.5,type:'plasma_weapon'},
{x:5.5,y:17.5,type:'health'},
];
pickups = positions.map(p => createPickup(p.x, p.y, p.type));
}
function initGame() {
player = createPlayer();
spawnEnemies();
spawnPickups();
damageFlash = 0;
gameOver = false;
gameRunning = true;
hideOverlay();
canvas.addEventListener('click', () => {
if (!document.pointerLockElement && gameRunning) {
canvas.requestPointerLock();
}
});
}
initInput(keys, mouse);
showOverlay('DOOMED DEPTHS', 'Click to Start');
document.getElementById('overlay').addEventListener('click', () => {
if (!gameRunning || gameOver) initGame();
});
let lastTime = performance.now();
function gameLoop(timestamp) {
requestAnimationFrame(gameLoop);
const dt = Math.min((timestamp - lastTime) / 1000, 0.05);
lastTime = timestamp;
if (!gameRunning || gameOver) return;
handleInput(player, dt);
if (player.shootTimer > 0) player.shootTimer -= dt * 1000;
if (damageFlash > 0) damageFlash -= dt * 30;
const beforeKills = enemies.filter(e => !e.alive).length;
const dmgTaken = updateEnemies(enemies, player, dt);
if (dmgTaken > 0) {
damageFlash = Math.max(damageFlash, dmgTaken);
playSound('hit');
}
checkPickups(player, pickups);
const afterKills = enemies.filter(e => !e.alive).length;
if (afterKills > beforeKills) playSound('death');
if (player.health <= 0 && player.alive) {
player.alive = false;
playSound('death');
setTimeout(() => {
respawnPlayer(player);
spawnEnemies();
spawnPickups();
damageFlash = 0;
}, 2000);
}
if (checkWinCondition(player, enemies)) {
gameOver = true;
gameRunning = false;
showOverlay('VICTORY!', 'All demons slain! Click to play again');
}
// === RENDER ===
ctx.clearRect(0, 0, SCREEN_W, SCREEN_H);
if (player.alive) {
const zBuffer = renderScene(ctx, player);
const allSprites = [
...enemies.map(e => ({...e, dist: distanceBetween(player.x, player.y, e.x, e.y)})),
...pickups.filter(p => !p.collected).map(p => ({...p, type: 'pickup', subtype: p.type, dist: distanceBetween(player.x, player.y, p.x, p.y)}))
];
allSprites.sort((a, b) => b.dist - a.dist);
for (const sprite of allSprites) {
drawSprite(ctx, sprite, player, zBuffer);
}
drawWeapon(ctx, player);
} else {
ctx.fillStyle = 'rgba(139, 0, 0, 0.6)';
ctx.fillRect(0, 0, SCREEN_W, SCREEN_H);
ctx.fillStyle = '#fff';
ctx.font = 'bold 36px Courier New';
ctx.textAlign = 'center';
ctx.fillText('YOU DIED', SCREEN_W/2, SCREEN_H/2 - 10);
ctx.font = '18px Courier New';
ctx.fillText('Respawning...', SCREEN_W/2, SCREEN_H/2 + 25);
}
drawDamageOverlay(ctx, damageFlash);
drawVignette(ctx);
drawNotifications(ctx);
updateHUD(player);
drawMinimap(mctx, player, enemies);
}
requestAnimationFrame(gameLoop);
</script>
</body>
</html>
<!-- agent-meta {"model":"thinkingcap-qwen3.6-27b","provider":"lmstudio","persona":"composer","sessionId":"e1fda245-5e32-42f1-8c1a-a9ce291b7f2a","tokensIn":943421,"tokensOut":22930,"tokensTotal":966351,"turns":45,"toolCalls":44,"failedToolCalls":3,"timestamp":"2026-07-18T05:13:56.149Z"} -->