DOOM
Qwen 3.6 35b a3b · 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 35,451 bytes · SHA-256 1681b3ab0263
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>DOOMSDAY — 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; }
#game { display: block; width: 100vw; height: 100vh; image-rendering: pixelated; image-rendering: crisp-edges; }
#hud {
position: fixed; bottom: 0; left: 0; right: 0;
height: 70px; background: linear-gradient(to bottom, #333, #111);
border-top: 3px solid #555; display: flex; align-items: center;
padding: 0 30px; gap: 30px; z-index: 10;
}
#health-bar { display: flex; align-items: center; gap: 8px; }
#health-label { color: #aaa; font-size: 11px; letter-spacing: 2px; }
.bar-bg { width: 150px; height: 14px; background: #222; border: 1px solid #555; }
.bar-fill { height: 100%; background: linear-gradient(to right, #c00, #f44); transition: width 0.2s; }
#health-value { color: #f44; font-size: 20px; font-weight: bold; min-width: 40px; text-align: right; }
#ammo-bar { display: flex; align-items: center; gap: 8px; }
#ammo-label { color: #aaa; font-size: 11px; letter-spacing: 2px; }
#ammo-value { color: #ff0; font-size: 20px; font-weight: bold; }
#weapon-name { color: #0f0; font-size: 16px; letter-spacing: 3px; margin-left: auto; }
#weapon-icons { display: flex; gap: 6px; }
.wpn-icon { color: #555; font-size: 14px; padding: 4px 8px; border: 1px solid #444; cursor: pointer; }
.wpn-icon.active { color: #0f0; border-color: #0f0; background: rgba(0,255,0,0.1); }
#crosshair {
position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
width: 24px; height: 24px; z-index: 5; pointer-events: none;
}
#crosshair::before, #crosshair::after {
content: ''; position: absolute; background: rgba(255,255,255,0.6);
}
#crosshair::before { width: 2px; height: 100%; left: 50%; transform: translateX(-50%); }
#crosshair::after { height: 2px; width: 100%; top: 50%; transform: translateY(-50%); }
#damage-overlay {
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
pointer-events: none; z-index: 4; opacity: 0;
background: radial-gradient(ellipse at center, transparent 40%, rgba(255,0,0,0.5) 100%);
transition: opacity 0.1s;
}
#damage-overlay.hit { opacity: 1; }
.overlay {
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
display: flex; flex-direction: column; align-items: center; justify-content: center;
z-index: 100; background: rgba(0,0,0,0.85); color: #fff;
}
.overlay.hidden { display: none; }
.overlay h1 { font-size: 48px; letter-spacing: 8px; margin-bottom: 20px; }
.overlay p { font-size: 16px; color: #aaa; margin: 6px 0; }
.overlay button {
margin-top: 30px; padding: 12px 40px; font-size: 18px; font-family: 'Courier New', monospace;
background: #c00; color: #fff; border: 2px solid #f44; cursor: pointer;
letter-spacing: 4px; transition: background 0.2s;
}
.overlay button:hover { background: #f44; }
#death-screen h1 { color: #c00; }
#win-screen h1 { color: #0f0; }
</style>
</head>
<body>
<canvas id="game"></canvas>
<canvas id="minimap" width="150" height="150"></canvas>
<div id="hud">
<div class="hud-section" id="health-section">
<span class="hud-label">HEALTH</span>
<div id="health-bar-container"><div id="health-fill"></div></div>
<span id="health-value" class="hud-value">100</span>
</div>
<div class="hud-section" id="ammo-section">
<span class="hud-label">AMMO</span>
<span id="ammo-value" class="hud-value">50</span>
</div>
<div class="hud-section" id="score-section">
<span class="hud-label">SCORE</span>
<span id="score-value" class="hud-value">0</span>
</div>
<div class="hud-section" id="weapon-section">
<span class="hud-label">WEAPON</span>
<div id="weapon-name">PISTOL</div>
<div id="weapon-icons">
<span class="wpn-icon" data-idx="0">1</span>
<span class="wpn-icon" data-idx="1">2</span>
<span class="wpn-icon" data-idx="2">3</span>
</div>
</div>
</div>
<div id="crosshair"></div>
<div id="damage-overlay"></div>
<div id="death-screen" class="overlay hidden">
<h1>YOU DIED</h1>
<p>Score: <span id="death-score">0</span></p>
<p>Respawning in 3 seconds...</p>
</div><div id="win-screen" class="overlay hidden">
<h1>MISSION COMPLETE</h1>
<p>All enemies eliminated!</p>
<p class="subtitle">Final Score: <span id="win-score">0</span></p>
<button id="restart-btn">PLAY AGAIN</button>
</div>
<div id="start-screen" class="overlay">
<h1>DOOMSDAY</h1>
<p>WASD — Move | Mouse — Look | Click — Shoot</p>
<p>1/2/3 — Weapons | E — Use Pickup</p>
<p class="subtitle">Survive the dungeon. Eliminate all enemies.</p>
<button id="start-btn">START GAME</button>
</div>
<script>
'use strict';
// ── const: MAP_W ──
const MAP_W = 24;
// ── const: MAP_H ──
const MAP_H = 24;
// ── const: TILE ──
const TILE = 1;
// ── const: WORLD_MAP ──
const WORLD_MAP = [
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
[1,0,0,0,0,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,3,0,0,0,0,0,4,4,4,0,0,0,1],
[1,0,0,2,0,0,0,0,0,0,0,3,0,0,0,0,0,0,0,4,0,0,0,1],
[1,0,0,2,0,0,0,0,0,0,0,3,0,0,0,0,0,0,0,4,0,0,0,1],
[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
[1,0,0,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,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,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,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,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,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,0,0,1],
[1,0,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,0,0,1],
[1,0,0,3,3,3,0,0,0,4,4,4,4,0,0,0,0,0,0,3,3,3,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: WALL_COLORS ──
const WALL_COLORS = {
1: { r: 120, g: 120, b: 120 },
2: { r: 140, g: 60, b: 60 },
3: { r: 60, g: 100, b: 140 },
4: { r: 80, g: 140, b: 60 },
};
// ── const: WEAPONS ──
const WEAPONS = [
{ name: 'PISTOL', damage: 20, fireRate: 300, ammoPerShot: 1, maxAmmo: 999, spread: 0, auto: false },
{ name: 'SHOTGUN', damage: 60, fireRate: 700, ammoPerShot: 5, maxAmmo: 200, spread: 0.15, auto: false, pellets: 5 },
{ name: 'PLASMA', damage: 15, fireRate: 100, ammoPerShot: 2, maxAmmo: 500, spread: 0.02, auto: true },
];
// ── const: ENEMY_TYPES ──
const ENEMY_TYPES = {
grunt: { hp: 50, speed: 0.015, damage: 8, color: '#a44', size: 0.3, attackRange: 6, attackRate: 1000, score: 100 },
heavy: { hp: 120, speed: 0.008, damage: 15, color: '#a80', size: 0.4, attackRange: 8, attackRate: 1500, score: 200 },
elite: { hp: 200, speed: 0.02, damage: 25, color: '#a0a', size: 0.35, attackRange: 10, attackRate: 800, score: 500 },
};
// ── const: PICKUP_TYPES ──
const PICKUP_TYPES = {
health: { color: '#f00', size: 0.2, amount: 25, symbol: '+' },
ammo: { color: '#ff0', size: 0.15, amount: 20, symbol: 'A' },
armor: { color: '#00f', size: 0.2, amount: 50, symbol: 'H' },
};
// ── function: createPlayer ──
function createPlayer() {
return {
x: 2.5, y: 2.5,
angle: 0,
health: 100,
maxHealth: 100,
armor: 0,
ammo: 50,
weapon: 0,
lastShot: 0,
isDead: false,
bobPhase: 0,
bobAmount: 0,
};
}
// ── function: createEnemy ──
function createEnemy(type, x, y) {
const def = ENEMY_TYPES[type];
return {
type, x, y,
hp: def.hp, maxHp: def.hp,
speed: def.speed,
damage: def.damage,
color: def.color,
size: def.size,
attackRange: def.attackRange,
attackRate: def.attackRate,
lastAttack: 0,
alive: true,
alert: false,
alertTimer: 0,
moveTimer: 0,
targetX: x, targetY: y,
hitFlash: 0,
};
}
// ── function: createPickup ──
function createPickup(type, x, y) {
const def = PICKUP_TYPES[type];
return {
type, x, y,
size: def.size,
color: def.color,
amount: def.amount,
symbol: def.symbol,
alive: true,
bobPhase: Math.random() * Math.PI * 2,
};
}
// ── function: createGameState ──
function createGameState() {
return {
player: createPlayer(),
enemies: [],
pickups: [],
running: false,
won: false,
deathTimer: 0,
totalScore: 0,
kills: 0,
totalEnemies: 0,
};
}
// ── function: castRay ──
function castRay(player, angle) {
const sin = Math.sin(angle);
const cos = Math.cos(angle);
const dx = cos;
const dy = sin;
let mapX = Math.floor(player.x);
let mapY = Math.floor(player.y);
const stepX = cos >= 0 ? 1 : -1;
const stepY = sin >= 0 ? 1 : -1;
let tMaxX = cos >= 0
? (mapX + 1 - player.x) / cos
: (player.x - mapX) / (-cos);
let tMaxY = sin >= 0
? (mapY + 1 - player.y) / sin
: (player.y - mapY) / (-sin);
const tStepX = stepX >= 0 ? 1 / cos : -1 / cos;
const tStepY = stepY >= 0 ? 1 / sin : -1 / sin;
let side = 0;
let hit = false;
let dist = 0;
for (let i = 0; i < 64; i++) {
if (tMaxX < tMaxY) {
tMaxX += tStepX;
mapX += stepX;
side = 0;
} else {
tMaxY += tStepY;
mapY += stepY;
side = 1;
}
if (mapX < 0 || mapX >= MAP_W || mapY < 0 || mapY >= MAP_H) break;
if (WORLD_MAP[mapY][mapX] > 0) {
hit = true;
break;
}
}
if (side === 0) {
dist = (mapX - player.x + (1 - stepX) / 2) / cos;
} else {
dist = (mapY - player.y + (1 - stepY) / 2) / sin;
}
let wallX;
if (side === 0) {
wallX = player.y + dist * sin;
} else {
wallX = player.x + dist * cos;
}
wallX -= Math.floor(wallX);
return { dist, side, wallX, mapX, mapY, hit };
}
// ── function: renderGame ──
function renderGame(canvas, ctx, player, enemies, pickups, dt) {
const W = canvas.width;
const H = canvas.height;
const FOV = Math.PI / 3;
const NUM_RAYS = Math.min(W, 640);
const stripWidth = W / NUM_RAYS;
// Sky gradient
const skyGrad = ctx.createLinearGradient(0, 0, 0, H / 2);
skyGrad.addColorStop(0, '#112');
skyGrad.addColorStop(1, '#335');
ctx.fillStyle = skyGrad;
ctx.fillRect(0, 0, W, H / 2);
// Floor gradient
const floorGrad = ctx.createLinearGradient(0, H / 2, 0, H);
floorGrad.addColorStop(0, '#333');
floorGrad.addColorStop(1, '#555');
ctx.fillStyle = floorGrad;
ctx.fillRect(0, H / 2, W, H / 2);
// Cast rays
const zBuffer = new Float32Array(NUM_RAYS);
for (let i = 0; i < NUM_RAYS; i++) {
const rayAngle = player.angle - FOV / 2 + (i / NUM_RAYS) * FOV;
const ray = castRay(player, rayAngle);
const perpDist = ray.dist * Math.cos(rayAngle - player.angle);
zBuffer[i] = perpDist;
if (!ray.hit) continue;
const lineHeight = H / perpDist;
const drawStart = (H - lineHeight) / 2;
const wc = WALL_COLORS[WORLD_MAP[ray.mapY][ray.mapX]];
const brightness = Math.max(0.15, 1 - perpDist / 16);
const shade = ray.side === 1 ? 0.7 : 1.0;
const r = Math.floor(wc.r * brightness * shade);
const g = Math.floor(wc.g * brightness * shade);
const b = Math.floor(wc.b * brightness * shade);
// Add texture-like vertical stripes
const stripePattern = (ray.wallX * 8) % 1;
const texShade = stripePattern < 0.1 ? 0.85 : 1;
ctx.fillStyle = `rgb(${Math.floor(r * texShade)},${Math.floor(g * texShade)},${Math.floor(b * texShade)})`;
ctx.fillRect(i * stripWidth, drawStart, stripWidth + 1, lineHeight);
// Wall top/bottom edge highlight
if (lineHeight > 4) {
ctx.fillStyle = `rgba(255,255,255,${0.08 * brightness})`;
ctx.fillRect(i * stripWidth, drawStart, stripWidth + 1, 2);
ctx.fillStyle = `rgba(0,0,0,${0.2 * brightness})`;
ctx.fillRect(i * stripWidth, drawStart + lineHeight - 2, stripWidth + 1, 2);
}
}
// Render sprites (enemies + pickups) with z-buffer
renderSprites(canvas, ctx, player, enemies, pickups, zBuffer, NUM_RAYS, stripWidth, dt);
}
// ── function: renderSprites ──
function renderSprites(canvas, ctx, player, enemies, pickups, zBuffer, numRays, stripWidth, dt) {
const W = canvas.width;
const H = canvas.height;
const FOV = Math.PI / 3;
const allSprites = [];
for (const e of enemies) {
if (!e.alive) continue;
const dist = Math.sqrt((e.x - player.x) ** 2 + (e.y - player.y) ** 2);
allSprites.push({ ...e, spriteType: 'enemy', dist });
}
for (const p of pickups) {
if (!p.alive) continue;
const dist = Math.sqrt((p.x - player.x) ** 2 + (p.y - player.y) ** 2);
allSprites.push({ ...p, spriteType: 'pickup', dist });
}
allSprites.sort((a, b) => b.dist - a.dist);
for (const sprite of allSprites) {
const dx = sprite.x - player.x;
const dy = sprite.y - player.y;
let spriteAngle = Math.atan2(dy, dx) - player.angle;
while (spriteAngle < -Math.PI) spriteAngle += 2 * Math.PI;
while (spriteAngle > Math.PI) spriteAngle -= 2 * Math.PI;
if (Math.abs(spriteAngle) > FOV * 0.7) continue;
const perpDist = sprite.dist * Math.cos(spriteAngle);
if (perpDist < 0.3) continue;
const screenX = W / 2 + (spriteAngle / (FOV / 2)) * (W / 2);
const spriteHeight = (H / perpDist) * 0.8;
const spriteWidth = spriteHeight * 0.6;
const drawStartY = (H - spriteHeight) / 2;
if (sprite.spriteType === 'enemy') {
drawEnemy(ctx, screenX, drawStartY, spriteWidth, spriteHeight, sprite, perpDist, zBuffer, numRays, stripWidth);
} else if (sprite.spriteType === 'pickup') {
drawPickup(ctx, screenX, drawStartY, spriteWidth, spriteHeight, sprite, perpDist, zBuffer, numRays, stripWidth);
}
}
}
// ── function: drawEnemy ──
function drawEnemy(ctx, x, y, w, h, enemy, dist, zBuffer, numRays, stripWidth) {
const brightness = Math.max(0.2, 1 - dist / 14);
const flash = enemy.hitFlash > 0;
const flashVal = flash ? 255 : Math.floor(160 * brightness);
const flashVal2 = flash ? 255 : Math.floor(60 * brightness);
const flashVal3 = flash ? 255 : Math.floor(60 * brightness);
const startStrip = Math.max(0, Math.floor((x - w / 2) / stripWidth));
const endStrip = Math.min(numRays - 1, Math.floor((x + w / 2) / stripWidth));
let visible = false;
for (let s = startStrip; s <= endStrip; s++) {
if (zBuffer[s] > dist) { visible = true; break; }
}
if (!visible) return;
const bodyTop = y + h * 0.15;
const bodyH = h * 0.55;
// Body with shading
ctx.fillStyle = `rgb(${flashVal},${flashVal2},${flashVal3})`;
ctx.fillRect(x - w * 0.3, bodyTop, w * 0.6, bodyH);
// Darker belly
ctx.fillStyle = `rgba(0,0,0,${0.15 * brightness})`;
ctx.fillRect(x - w * 0.15, bodyTop + bodyH * 0.4, w * 0.3, bodyH * 0.3);
// Head
const headR = h * 0.25;
const headY = bodyTop - headR * 0.5;
ctx.beginPath();
ctx.arc(x, headY, headR, 0, Math.PI * 2);
const headColor = flash ? 'rgb(255,255,255)' : `rgb(${Math.floor(180 * brightness)},${Math.floor(140 * brightness)},${Math.floor(100 * brightness)})`;
ctx.fillStyle = headColor;
ctx.fill();
// Eyes
const eyeSize = headR * 0.2;
const eyeY = headY - headR * 0.1;
ctx.fillStyle = flash ? 'rgb(255,0,0)' : `rgb(255,${Math.floor(100 * brightness)},0)`;
ctx.fillRect(x - headR * 0.4 - eyeSize / 2, eyeY, eyeSize, eyeSize);
ctx.fillRect(x + headR * 0.4 - eyeSize / 2, eyeY, eyeSize, eyeSize);
// Legs
const legH = h * 0.25;
const legY = bodyTop + bodyH;
const legColor = flash ? 'rgb(200,200,200)' : `rgb(${Math.floor(80 * brightness)},${Math.floor(80 * brightness)},${Math.floor(80 * brightness)})`;
ctx.fillStyle = legColor;
ctx.fillRect(x - w * 0.25, legY, w * 0.12, legH);
ctx.fillRect(x + w * 0.13, legY, w * 0.12, legH);
// HP bar
if (enemy.hp < enemy.maxHp) {
const barW = w * 0.6;
const barH = 4;
const barY = headY - headR - 10;
ctx.fillStyle = '#300';
ctx.fillRect(x - barW / 2, barY, barW, barH);
ctx.fillStyle = '#f00';
ctx.fillRect(x - barW / 2, barY, barW * (enemy.hp / enemy.maxHp), barH);
}
}
// ── function: drawPickup ──
function drawPickup(ctx, x, y, w, h, pickup, dist, zBuffer, numRays, stripWidth) {
const brightness = Math.max(0.3, 1 - dist / 12);
const bob = Math.sin(pickup.bobPhase) * h * 0.03;
const startStrip = Math.max(0, Math.floor((x - w / 2) / stripWidth));
const endStrip = Math.min(numRays - 1, Math.floor((x + w / 2) / stripWidth));
let visible = false;
for (let s = startStrip; s <= endStrip; s++) {
if (zBuffer[s] > dist) { visible = true; break; }
}
if (!visible) return;
const size = w * 0.5;
const py = y + h * 0.4 + bob;
// Glow
ctx.shadowColor = pickup.color;
ctx.shadowBlur = 10;
ctx.fillStyle = `rgba(${hexToRgb(pickup.color)},${0.3 * brightness})`;
ctx.beginPath();
ctx.arc(x, py, size * 1.2, 0, Math.PI * 2);
ctx.fill();
// Core
ctx.fillStyle = `rgba(${hexToRgb(pickup.color)},${brightness})`;
ctx.beginPath();
ctx.arc(x, py, size * 0.5, 0, Math.PI * 2);
ctx.fill();
// Symbol
ctx.shadowBlur = 0;
ctx.fillStyle = `rgba(255,255,255,${brightness})`;
ctx.font = `bold ${Math.floor(size * 0.8)}px Courier New`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(pickup.symbol, x, py);
}
// ── function: hexToRgb ──
function hexToRgb(hex) {
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 `${r},${g},${b}`;
}
// ── function: updateEnemies ──
function updateEnemies(enemies, player, dt, now) {
const moveDir = (ex, ey, tx, ty) => {
const dx = tx - ex;
const dy = ty - ey;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 0.1) return { x: ex, y: ey };
const nx = dx / dist;
const ny = dy / dist;
const step = 0.03;
const newX = ex + nx * step;
const newY = ey + ny * step;
const margin = 0.2;
if (WORLD_MAP[Math.floor(ey)][Math.floor(ex + nx * step)] === 0 &&
Math.abs(Math.floor(ex + nx * step) - ex) < margin) {
// Check wall collision
}
if (WORLD_MAP[Math.floor(newY)][Math.floor(newX)] === 0) {
return { x: newX, y: newY };
}
// Try sliding
if (WORLD_MAP[Math.floor(newY)][Math.floor(ex)] === 0) {
return { x: ex, y: newY };
}
if (WORLD_MAP[Math.floor(ey)][Math.floor(newX)] === 0) {
return { x: newX, y: ey };
}
return { x: ex, y: ey };
};
for (const e of enemies) {
if (!e.alive) continue;
if (e.hitFlash > 0) e.hitFlash -= dt * 5;
const dx = player.x - e.x;
const dy = player.y - e.y;
const dist = Math.sqrt(dx * dx + dy * dy);
// Alert check
if (!e.alert) {
if (dist < 8) {
e.alert = true;
e.alertTimer = now + 5000;
}
continue;
}
// Lose alert after time
if (now > e.alertTimer && dist > 10) {
e.alert = false;
continue;
}
// Move toward player
e.moveTimer -= dt * 1000;
if (e.moveTimer <= 0) {
e.moveTimer = 300 + Math.random() * 400;
if (dist > 1.5) {
const result = moveDir(e.x, e.y, player.x, player.y);
e.x = result.x;
e.y = result.y;
}
}
// Attack
if (dist < e.attackRange) {
e.lastAttack -= dt * 1000;
if (e.lastAttack <= 0) {
e.lastAttack = e.attackRate;
// Check line of sight
const los = checkLineOfSight(e.x, e.y, player.x, player.y);
if (los) {
damagePlayer(e.damage, now);
}
}
}
}
}
// ── function: checkLineOfSight ──
function checkLineOfSight(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 * 10);
for (let i = 0; i <= steps; i++) {
const t = i / steps;
const mx = Math.floor(x1 + dx * t);
const my = Math.floor(y1 + dy * t);
if (mx >= 0 && mx < MAP_W && my >= 0 && my < MAP_H) {
if (WORLD_MAP[my][mx] > 0) return false;
}
}
return true;
}
// ── let: damageFlashTimer ──
let damageFlashTimer = 0;
// ── function: damagePlayer ──
function damagePlayer(amount, now) {
damageFlashTimer = now + 200;
if (game.player.armor > 0) {
const armorAbsorb = Math.min(game.player.armor, amount * 0.5);
game.player.armor -= armorAbsorb;
amount -= armorAbsorb;
}
game.player.health -= amount;
if (game.player.health <= 0) {
game.player.health = 0;
game.player.isDead = true;
game.deathTimer = 3000;
gameRunning = false;
}
}
// ── function: fireWeapon ──
function fireWeapon(game) {
const { player, enemies } = game;
const weapon = WEAPONS[player.weapon];
const now = performance.now();
if (now - player.lastShot < weapon.fireRate) return;
if (player.ammo < weapon.ammoPerShot) return;
player.lastShot = now;
player.ammo -= weapon.ammoPerShot;
const pellets = weapon.pellets || 1;
for (let p = 0; p < pellets; p++) {
const spread = (Math.random() - 0.5) * weapon.spread;
const rayAngle = player.angle + spread;
const ray = castRay(player, rayAngle);
// Check enemy hits
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);
let spriteAngle = Math.atan2(dy, dx) - player.angle;
while (spriteAngle < -Math.PI) spriteAngle += 2 * Math.PI;
while (spriteAngle > Math.PI) spriteAngle -= 2 * Math.PI;
const hitWidth = Math.atan2(e.size, dist);
if (Math.abs(spriteAngle) < hitWidth && dist < ray.dist) {
const dmg = weapon.damage / pellets;
e.hp -= dmg;
e.hitFlash = 1;
e.alert = true;
e.alertTimer = now + 10000;
if (e.hp <= 0) {
e.alive = false;
game.kills++;
game.totalScore += ENEMY_TYPES[e.type].score;
}
break;
}
}
}
}
// ── function: updatePickups ──
function updatePickups(game, dt, now) {
const { player, pickups, enemies } = game;
for (const p of pickups) {
if (!p.alive) continue;
p.bobPhase += dt * 3;
const dx = player.x - p.x;
const dy = player.y - p.y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 0.5) {
if (p.type === 'health' && player.health < player.maxHealth) {
player.health = Math.min(player.maxHealth, player.health + p.amount);
p.alive = false;
} else if (p.type === 'ammo') {
player.ammo += p.amount;
p.alive = false;
} else if (p.type === 'armor' && player.armor < 100) {
player.armor = Math.min(100, player.armor + p.amount);
p.alive = false;
}
}
}
// Respawn pickups occasionally
if (Math.random() < dt * 0.05) {
const types = ['health', 'ammo', 'armor'];
const type = types[Math.floor(Math.random() * types.length)];
let px, py, attempts = 0;
do {
px = 2 + Math.floor(Math.random() * (MAP_W - 4));
py = 2 + Math.floor(Math.random() * (MAP_H - 4));
attempts++;
} while ((WORLD_MAP[py][px] > 0 || attempts < 50) && attempts < 100);
if (attempts < 100 && WORLD_MAP[py][px] === 0) {
pickups.push(createPickup(type, px + 0.5, py + 0.5));
}
}
// Spawn enemies periodically
if (enemies.filter(e => e.alive).length < 8 && Math.random() < dt * 0.1) {
const types = ['grunt', 'grunt', 'grunt', 'heavy', 'elite'];
const type = types[Math.floor(Math.random() * types.length)];
let ex, ey, attempts = 0;
do {
ex = 2 + Math.floor(Math.random() * (MAP_W - 4));
ey = 2 + Math.floor(Math.random() * (MAP_H - 4));
attempts++;
} while ((WORLD_MAP[ey][ex] > 0 || attempts < 50) && attempts < 100);
if (attempts < 100 && WORLD_MAP[ey][ex] === 0) {
enemies.push(createEnemy(type, ex + 0.5, ey + 0.5));
}
}
}
// ── function: checkWinCondition ──
function checkWinCondition(game) {
const aliveEnemies = game.enemies.filter(e => e.alive).length;
return aliveEnemies === 0;
}
// ── function: respawnPlayer ──
function respawnPlayer(game) {
game.player = createPlayer();
game.player.x = 2.5;
game.player.y = 2.5;
game.player.angle = 0;
}
// ── main ──
let game = createGameState();
let keys = {};
let mouseDown = false;
let lastTime = 0;
let gameRunning = false;
let deathRespawnTimer = 0;
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const minimapCanvas = document.getElementById('minimap');
const mctx = minimapCanvas.getContext('2d');
function resizeCanvas() {
canvas.width = Math.min(window.innerWidth, 960);
canvas.height = Math.min(window.innerHeight - 75, 720);
}
function initGame() {
game = createGameState();
game.enemies = [];
game.pickups = [];
const enemySpawns = [
{ type: 'grunt', x: 7.5, y: 7.5 },
{ type: 'grunt', x: 20.5, y: 7.5 },
{ type: 'heavy', x: 22.5, y: 14.5 },
{ type: 'grunt', x: 7.5, y: 20.5 },
{ type: 'elite', x: 18.5, y: 24.5 },
{ type: 'grunt', x: 12.5, y: 16.5 },
{ type: 'heavy', x: 2.5, y: 16.5 },
{ type: 'grunt', x: 25.5, y: 24.5 },
{ type: 'grunt', x: 4.5, y: 26.5 },
{ type: 'heavy', x: 25.5, y: 4.5 },
];
for (const es of enemySpawns) game.enemies.push(createEnemy(es.type, es.x, es.y));
game.totalEnemies = game.enemies.length;
const pickupSpawns = [
{ type: 'health', x: 4.5, y: 4.5 },
{ type: 'ammo', x: 21.5, y: 4.5 },
{ type: 'armor', x: 12.5, y: 12.5 },
{ type: 'health', x: 22.5, y: 22.5 },
{ type: 'ammo', x: 3.5, y: 24.5 },
{ type: 'health', x: 26.5, y: 16.5 },
];
for (const ps of pickupSpawns) game.pickups.push(createPickup(ps.type, ps.x, ps.y));
game.running = true;
game.won = false;
game.deathTimer = 0;
game.player.isDead = false;
}
function update(dt, now) {
if (!game.running || game.won) return;
if (game.player.isDead) {
game.deathTimer -= dt * 1000;
document.getElementById('death-screen').classList.remove('hidden');
document.getElementById('death-score').textContent = game.totalScore;
if (game.deathTimer <= 0) {
respawnPlayer(game);
game.deathTimer = 3000;
}
return;
}
const player = game.player;
const moveSpeed = 0.04;
let moving = false;
if (keys['KeyW'] || keys['ArrowUp']) {
const nx = player.x + Math.cos(player.angle) * moveSpeed;
const ny = player.y + Math.sin(player.angle) * moveSpeed;
if (WORLD_MAP[Math.floor(player.y)][Math.floor(nx)] === 0) player.x = nx;
if (WORLD_MAP[Math.floor(ny)][Math.floor(player.x)] === 0) player.y = ny;
moving = true;
}
if (keys['KeyS'] || keys['ArrowDown']) {
const nx = player.x - Math.cos(player.angle) * moveSpeed;
const ny = player.y - Math.sin(player.angle) * moveSpeed;
if (WORLD_MAP[Math.floor(player.y)][Math.floor(nx)] === 0) player.x = nx;
if (WORLD_MAP[Math.floor(ny)][Math.floor(player.x)] === 0) player.y = ny;
moving = true;
}
if (keys['KeyA'] || keys['ArrowLeft']) {
const nx = player.x + Math.cos(player.angle - Math.PI / 2) * moveSpeed;
const ny = player.y + Math.sin(player.angle - Math.PI / 2) * moveSpeed;
if (WORLD_MAP[Math.floor(player.y)][Math.floor(nx)] === 0) player.x = nx;
if (WORLD_MAP[Math.floor(ny)][Math.floor(player.x)] === 0) player.y = ny;
moving = true;
}
if (keys['KeyD'] || keys['ArrowRight']) {
const nx = player.x + Math.cos(player.angle + Math.PI / 2) * moveSpeed;
const ny = player.y + Math.sin(player.angle + Math.PI / 2) * moveSpeed;
if (WORLD_MAP[Math.floor(player.y)][Math.floor(nx)] === 0) player.x = nx;
if (WORLD_MAP[Math.floor(ny)][Math.floor(player.x)] === 0) player.y = ny;
moving = true;
}
if (keys['Digit1']) { player.weapon = 0; keys['Digit1'] = false; }
if (keys['Digit2']) { player.weapon = 1; keys['Digit2'] = false; }
if (keys['Digit3']) { player.weapon = 2; keys['Digit3'] = false; }
if (mouseDown) {
const weapon = WEAPONS[player.weapon];
if (weapon.auto || !player._wasMouseDown) fireWeapon(game);
}
player._wasMouseDown = mouseDown;
if (moving) {
player.bobPhase += dt * 8;
player.bobAmount = Math.min(1, player.bobAmount + dt * 5);
} else {
player.bobAmount = Math.max(0, player.bobAmount - dt * 3);
}
updateEnemies(game.enemies, player, dt, now);
updatePickups(game, dt, now);
if (checkWinCondition(game)) {
game.won = true;
gameRunning = false;
document.getElementById('win-score').textContent = game.totalScore;
document.getElementById('win-screen').classList.remove('hidden');
}
updateHUD();
}
function render(now) {
const player = game.player;
ctx.fillStyle = '#000';
ctx.fillRect(0, 0, canvas.width, canvas.height);
const bobY = Math.sin(player.bobPhase) * 5 * player.bobAmount;
const bobX = Math.cos(player.bobPhase / 2) * 3 * player.bobAmount;
ctx.save();
ctx.translate(bobX, bobY);
renderGame(canvas, ctx, player, game.enemies, game.pickups, 1 / 60);
ctx.restore();
const overlay = document.getElementById('damage-overlay');
if (damageFlashTimer > now) overlay.classList.add('hit');
else overlay.classList.remove('hit');
drawGun(ctx, now);
drawMinimap();
}
function drawGun(ctx, now) {
const W = canvas.width, H = canvas.height;
const bobY = Math.sin(game.player.bobPhase) * 3 * game.player.bobAmount;
const bobX = Math.cos(game.player.bobPhase / 2) * 2 * game.player.bobAmount;
const gunX = W / 2 + bobX;
const gunY = H - 10 + bobY;
if (now - game.player.lastShot < 80) {
const flashSize = 30 + Math.random() * 20;
ctx.fillStyle = `rgba(255,${150 + Math.random() * 105},0,${0.8 - (now - game.player.lastShot) / 100})`;
ctx.beginPath(); ctx.arc(gunX, gunY - 80, flashSize, 0, Math.PI * 2); ctx.fill();
ctx.fillStyle = `rgba(255,255,200,${0.6 - (now - game.player.lastShot) / 120})`;
ctx.beginPath(); ctx.arc(gunX, gunY - 80, flashSize * 0.5, 0, Math.PI * 2); ctx.fill();
}
ctx.fillStyle = '#444';
if (game.player.weapon === 0) {
ctx.fillRect(gunX - 15, gunY - 70, 30, 70);
ctx.fillStyle = '#333'; ctx.fillRect(gunX - 8, gunY - 75, 16, 10);
} else if (game.player.weapon === 1) {
ctx.fillRect(gunX - 20, gunY - 80, 40, 80);
ctx.fillStyle = '#555'; ctx.fillRect(gunX - 18, gunY - 85, 36, 8);
ctx.fillStyle = '#333'; ctx.fillRect(gunX - 6, gunY - 90, 12, 15);
} else {
ctx.fillRect(gunX - 22, gunY - 75, 44, 75);
ctx.fillStyle = '#0af'; ctx.fillRect(gunX - 5, gunY - 95, 10, 25);
ctx.fillStyle = `rgba(0,170,255,${0.3 + Math.sin(now * 0.01) * 0.2})`;
ctx.beginPath(); ctx.arc(gunX, gunY - 95, 8, 0, Math.PI * 2); ctx.fill();
}
}
function drawMinimap() {
const mw = minimapCanvas.width, mh = minimapCanvas.height;
const scale = mw / MAP_W;
mctx.fillStyle = 'rgba(0,0,0,0.8)';
mctx.fillRect(0, 0, mw, mh);
for (let y = 0; y < MAP_H; y++) {
for (let x = 0; x < MAP_W; x++) {
if (WORLD_MAP[y][x] > 0) {
const c = WALL_COLORS[WORLD_MAP[y][x]];
mctx.fillStyle = `rgb(${c.r},${c.g},${c.b})`;
mctx.fillRect(x * scale, y * scale, scale + 0.5, scale + 0.5);
} else {
mctx.fillStyle = '#222';
mctx.fillRect(x * scale, y * scale, scale + 0.5, scale + 0.5);
}
}
}
const p = game.player;
mctx.fillStyle = '#0f0';
mctx.beginPath();
mctx.arc(p.x * scale, p.y * scale, 3, 0, Math.PI * 2);
mctx.fill();
mctx.strokeStyle = '#0f0';
mctx.lineWidth = 1.5;
mctx.beginPath();
mctx.moveTo(p.x * scale, p.y * scale);
mctx.lineTo((p.x + Math.cos(p.angle) * 1.5) * scale, (p.y + Math.sin(p.angle) * 1.5) * scale);
mctx.stroke();
for (const e of game.enemies) {
if (!e.alive) continue;
mctx.fillStyle = e.alert ? '#f00' : '#a00';
mctx.fillRect(e.x * scale - 1.5, e.y * scale - 1.5, 3, 3);
}
for (const pk of game.pickups) {
if (!pk.alive) continue;
mctx.fillStyle = pk.color;
mctx.fillRect(pk.x * scale - 1, pk.y * scale - 1, 2, 2);
}
}
function updateHUD() {
const player = game.player;
const healthFill = document.getElementById('health-fill');
const healthVal = document.getElementById('health-value');
const ammoVal = document.getElementById('ammo-value');
const scoreVal = document.getElementById('score-value');
const weaponName = document.getElementById('weapon-name');
healthFill.style.width = `${Math.max(0, (player.health / player.maxHealth) * 100)}%`;
healthVal.textContent = Math.floor(player.health);
ammoVal.textContent = player.ammo;
scoreVal.textContent = game.totalScore;
weaponName.textContent = WEAPONS[player.weapon].name;
document.querySelectorAll('.wpn-icon').forEach((icon, i) => {
icon.classList.toggle('active', i === player.weapon);
});
healthFill.classList.remove('low', 'critical');
if (player.health <= 25) healthFill.classList.add('critical');
else if (player.health <= 50) healthFill.classList.add('low');
if (player.health > 60) {
healthFill.style.background = 'linear-gradient(to right, #0a0, #4f4)';
healthVal.style.color = '#4f4';
} else if (player.health > 30) {
healthFill.style.background = 'linear-gradient(to right, #aa0, #ff4)';
healthVal.style.color = '#ff4';
} else {
healthFill.style.background = 'linear-gradient(to right, #c00, #f44)';
healthVal.style.color = '#f44';
}
}
function gameLoop(timestamp) {
if (!lastTime) lastTime = timestamp;
const dt = Math.min((timestamp - lastTime) / 1000, 0.05);
lastTime = timestamp;
if (gameRunning) {
update(dt, timestamp);
render(timestamp);
}
requestAnimationFrame(gameLoop);
}
document.addEventListener('keydown', (e) => { keys[e.code] = true; });
document.addEventListener('keyup', (e) => { keys[e.code] = false; });
document.addEventListener('mousedown', () => { mouseDown = true; });
document.addEventListener('mouseup', () => { mouseDown = false; });
document.addEventListener('mousemove', (e) => {
if (!gameRunning) return;
game.player.angle += e.movementX * 0.002;
});
document.getElementById('start-btn').addEventListener('click', () => {
document.getElementById('start-screen').classList.add('hidden');
canvas.requestPointerLock();
initGame();
gameRunning = true;
});
document.getElementById('restart-btn').addEventListener('click', () => {
document.getElementById('win-screen').classList.add('hidden');
canvas.requestPointerLock();
initGame();
gameRunning = true;
});
canvas.addEventListener('click', () => {
if (gameRunning) canvas.requestPointerLock();
});
window.addEventListener('resize', resizeCanvas);
resizeCanvas();
requestAnimationFrame(gameLoop);
</script>
</body>
</html>
<!-- agent-meta {"model":"qwen/qwen3.6-35b-a3b","provider":"lmstudio","persona":"composer","sessionId":"f8dca7a8-6006-4d56-834b-b4e1681437d9","tokensIn":3077997,"tokensOut":45720,"tokensTotal":3123717,"turns":76,"toolCalls":76,"failedToolCalls":7,"timestamp":"2026-07-20T22:04:11.264Z"} -->