DOOM
thinkingcap qwen3.6 27b · basic

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 28,135 bytes · SHA-256 ad7858f2e0c9
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>DUNGEON RAID - Retro FPS</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #000; overflow: hidden; cursor: none; font-family: monospace; }
canvas { display: block; width: 100vw; height: 100vh; image-rendering: pixelated; }
#ui { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; }
#crosshair { position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%); color: #0f0; font-size: 24px; text-shadow: 0 0 4px #0f0; z-index: 10; }
#hud { position: absolute; bottom: 0; left: 0; width: 100%; height: 80px; background: linear-gradient(transparent, rgba(0,0,0,0.9)); display: flex; justify-content: space-between; align-items: flex-end; padding: 10px 20px; z-index: 10; }
.hud-item { color: #fff; font-size: 14px; text-shadow: 0 0 8px currentColor; }
#health-bar { width: 200px; height: 20px; border: 2px solid #f00; background: #300; margin-top: 4px; }
#health-fill { height: 100%; background: linear-gradient(90deg, #f00, #ff0); transition: width 0.3s; }
#weapon-name { color: #ff0; font-size: 20px; font-weight: bold; }
#ammo-display { color: #0ff; font-size: 18px; }
#msg { position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%); color: #f00; font-size: 48px; font-weight: bold; text-shadow: 0 0 20px #f00, 0 0 40px #900; z-index: 20; display: none; text-align: center; }
#minimap { position: absolute; top: 10px; right: 10px; border: 2px solid #0f0; opacity: 0.7; z-index: 10; }
#start-screen { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.95); display: flex; flex-direction: column; align-items: center; justify-content: center; z-index: 100; cursor: default; }
#start-screen h1 { color: #f00; font-size: 64px; text-shadow: 0 0 30px #f00, 0 0 60px #900; margin-bottom: 20px; letter-spacing: 8px; }
#start-screen p { color: #aaa; font-size: 16px; margin: 5px 0; }
#start-btn { margin-top: 30px; padding: 15px 40px; background: #900; color: #fff; border: 2px solid #f00; font-family: monospace; font-size: 20px; cursor: pointer; pointer-events: all; }
#start-btn:hover { background: #c00; }
#death-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: rgba(139,0,0,0.6); z-index: 50; display: none; pointer-events: none; }
#win-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,80,0,0.7); z-index: 50; display: none; pointer-events: none; }
</style>
</head>
<body>
<canvas id="game"></canvas>
<div id="ui">
<div id="crosshair">+</div>
<div id="hud">
<div class="hud-item"><span style="color:#f00;font-size:18px;">♥ HEALTH</span><br><div id="health-bar"><div id="health-fill" style="width:100%"></div></div></div>
<div class="hud-item" style="text-align:center;"><div id="weapon-name">PISTOL</div><div id="ammo-display">AMMO: 50</div></div>
<div class="hud-item" style="text-align:right;"><span style="color:#ff0;">ENEMIES LEFT: <span id="enemy-count">0</span></span><br><span style="color:#f80;font-size:12px;">KILLS: <span id="kill-count">0</span></span></div>
</div>
<canvas id="minimap" width="160" height="160"></canvas>
<div id="msg"></div>
<div id="death-overlay"></div>
<div id="win-overlay"></div>
</div>
<div id="start-screen">
<h1>DUNGEON RAID</h1>
<p>WASD - Move | Mouse - Look | Click - Shoot</p>
<p>1/2/3 - Switch Weapons | R - Reload</p>
<p>Kill all demons to escape!</p>
<button id="start-btn">ENTER THE DUNGEON</button>
</div>
<script>
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const miniCanvas = document.getElementById('minimap');
const miniCtx = miniCanvas.getContext('2d');
const W = 640, H = 480;
canvas.width = W; canvas.height = H;
// Audio context
let audioCtx;
function initAudio() {
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
}
function playSound(freq, duration, type='square', vol=0.15) {
if (!audioCtx) return;
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = type;
osc.frequency.setValueAtTime(freq, audioCtx.currentTime);
osc.frequency.exponentialRampToValueAtTime(freq * 0.5, audioCtx.currentTime + duration);
gain.gain.setValueAtTime(vol, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + duration);
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start();
osc.stop(audioCtx.currentTime + duration);
}
function playShootSound(type) {
if (type === 0) { playSound(800, 0.1, 'square', 0.2); }
else if (type === 1) { playSound(200, 0.3, 'sawtooth', 0.3); playSound(150, 0.4, 'square', 0.2); }
else { playSound(600, 0.05, 'square', 0.15); setTimeout(() => playSound(700, 0.05, 'square', 0.1), 30); }
}
function playHitSound() { playSound(300, 0.15, 'sawtooth', 0.2); }
function playPickupSound() { playSound(600, 0.1, 'sine', 0.15); setTimeout(() => playSound(900, 0.1, 'sine', 0.15), 80); }
function playDeathSound() { playSound(200, 0.5, 'sawtooth', 0.3); playSound(100, 0.6, 'square', 0.2); }
function playEnemyDieSound() { playSound(400, 0.2, 'sawtooth', 0.2); setTimeout(() => playSound(200, 0.3, 'square', 0.15), 100); }
// Map: 0=empty, 1-4=wall types
const MAP = [
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
[1,0,0,2,2,2,0,0,0,3,3,0,0,0,0,0,4,4,4,0,0,0,0,1],
[1,0,0,2,0,0,0,0,0,0,3,0,0,0,0,0,0,0,4,0,0,0,0,1],
[1,0,0,2,0,0,0,0,0,0,3,0,0,0,0,0,0,0,4,0,0,0,0,1],
[1,0,0,0,0,0,0,0,0,0,0,0,0,2,2,2,2,0,0,0,0,0,0,1],
[1,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,3,3,0,1],
[1,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,3,0,0,1],
[1,1,1,0,0,0,4,4,4,4,0,0,0,2,0,0,0,0,0,0,3,0,0,1],
[1,0,0,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
[1,0,0,0,0,0,4,0,0,0,0,0,0,0,0,3,3,3,0,0,0,0,0,1],
[1,0,0,2,2,0,0,0,0,0,0,0,0,0,0,3,0,0,0,0,4,4,0,1],
[1,0,0,2,0,0,0,0,0,0,0,0,0,0,0,3,0,0,0,0,4,0,0,1],
[1,0,0,2,0,0,0,0,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,2,2,2,0,0,0,0,0,0,0,0,0,0,1],
[1,0,0,0,0,0,0,0,0,0,2,0,0,0,0,4,4,4,0,0,0,0,0,1],
[1,0,0,3,3,3,0,0,0,0,2,0,0,0,0,4,0,0,0,0,0,0,0,1],
[1,0,0,3,0,0,0,0,0,0,0,0,0,0,0,4,0,0,0,2,2,2,0,1],
[1,0,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,1],
[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,1],
[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
];
const MAP_W = MAP[0].length;
const MAP_H = MAP.length;
// Wall colors
const wallColors = {
1: ['#667', '#556'],
2: ['#844', '#733'],
3: ['#484', '#373'],
4: ['#448', '#337']
};
// Player
let player = { x: 2, y: 2, angle: 0, health: 100, maxHealth: 100 };
const FOV = Math.PI / 3;
const MOVE_SPEED = 3.0;
const ROT_SPEED = 0.003;
// Weapons
const weapons = [
{ name: 'PISTOL', ammo: 50, maxAmmo: 99, damage: 20, fireRate: 400, lastShot: 0, color: '#ff0' },
{ name: 'SHOTGUN', ammo: 15, maxAmmo: 30, damage: 60, fireRate: 800, lastShot: 0, color: '#f80' },
{ name: 'MACHINE GUN', ammo: 120, maxAmmo: 200, damage: 12, fireRate: 100, lastShot: 0, color: '#0ff' }
];
let currentWeapon = 0;
// Enemies
let enemies = [];
const ENEMY_TYPES = [
{ name: 'Imp', health: 40, speed: 1.5, damage: 8, color: '#f44', size: 0.3 },
{ name: 'Demon', health: 80, speed: 2.0, damage: 15, color: '#a0f', size: 0.35 },
{ name: 'Cacodemon', health: 120, speed: 1.2, damage: 20, color: '#f80', size: 0.4 }
];
function spawnEnemies() {
enemies = [];
const positions = [
{x:5,y:3},{x:10,y:5},{x:15,y:3},{x:20,y:5},
{x:8,y:8},{x:14,y:7},{x:18,y:9},{x:5,y:11},
{x:12,y:12},{x:20,y:11},{x:3,y:15},{x:10,y:16},
{x:16,y:14},{x:21,y:17},{x:8,y:19},{x:15,y:18},
{x:20,y:20},{x:5,y:21},{x:12,y:21},{x:18,y:22}
];
positions.forEach((p, i) => {
const type = ENEMY_TYPES[i % 3];
enemies.push({
x: p.x + 0.5, y: p.y + 0.5,
health: type.health, maxHealth: type.health,
speed: type.speed, damage: type.damage,
color: type.color, size: type.size,
alive: true, attackTimer: 0, hitFlash: 0,
type: i % 3
});
});
}
// Pickups
let pickups = [];
function spawnPickups() {
pickups = [
{x:7,y:2,type:'health',amount:25,color:'#f44'},
{x:12,y:4,type:'ammo0',amount:15,color:'#ff0'},
{x:18,y:6,type:'ammo1',amount:5,color:'#f80'},
{x:3,y:9,type:'health',amount:30,color:'#f44'},
{x:21,y:8,type:'ammo2',amount:40,color:'#0ff'},
{x:10,y:10,type:'health',amount:25,color:'#f44'},
{x:6,y:13,type:'ammo0',amount:20,color:'#ff0'},
{x:16,y:12,type:'ammo1',amount:8,color:'#f80'},
{x:22,y:15,type:'health',amount:40,color:'#f44'},
{x:9,y:17,type:'ammo2',amount:30,color:'#0ff'},
{x:14,y:19,type:'health',amount:25,color:'#f44'},
{x:4,y:20,type:'ammo0',amount:15,color:'#ff0'},
{x:19,y:21,type:'ammo1',amount:6,color:'#f80'},
];
}
// Game state
let gameState = 'menu'; // menu, playing, dead, won
let kills = 0;
let deathCount = 0;
let weaponBob = 0;
let weaponRecoil = 0;
let screenShake = 0;
let damageFlash = 0;
// Input
const keys = {};
let mouseDX = 0;
let pointerLocked = false;
document.addEventListener('keydown', e => { keys[e.key.toLowerCase()] = true; });
document.addEventListener('keyup', e => { keys[e.key.toLowerCase()] = false; });
document.addEventListener('mousemove', e => { if (pointerLocked) mouseDX += e.movementX; });
document.addEventListener('mousedown', () => { if (gameState === 'playing') shoot(); });
// Weapon switching
document.addEventListener('keydown', e => {
if (e.key === '1') currentWeapon = 0;
if (e.key === '2') currentWeapon = 1;
if (e.key === '3') currentWeapon = 2;
});
function shoot() {
const w = weapons[currentWeapon];
const now = Date.now();
if (now - w.lastShot < w.fireRate || w.ammo <= 0) return;
w.ammo--;
w.lastShot = now;
weaponRecoil = 8;
screenShake = 4;
playShootSound(currentWeapon);
// Raycast for hit detection
const rayAngle = player.angle;
const sin = Math.sin(rayAngle), cos = Math.cos(rayAngle);
let dist = 0;
const step = 0.05;
let hitEnemy = null;
let hitDist = Infinity;
for (let d = 0; d < 20; d += step) {
const cx = player.x + cos * d;
const cy = player.y + sin * d;
// Check wall collision
if (MAP[Math.floor(cy)] && MAP[Math.floor(cy)][Math.floor(cx)] > 0) break;
// Check enemy hit
for (const e of enemies) {
if (!e.alive) continue;
const dx = cx - e.x, dy = cy - e.y;
if (dx*dx + dy*dy < e.size * e.size) {
if (d < hitDist) { hitDist = d; hitEnemy = e; }
}
}
}
if (hitEnemy) {
const dmgMult = currentWeapon === 1 ? 1.5 : 1;
hitEnemy.health -= w.damage * dmgMult;
hitEnemy.hitFlash = 0.3;
playHitSound();
if (hitEnemy.health <= 0) {
hitEnemy.alive = false;
kills++;
playEnemyDieSound();
// Chance to drop pickup
if (Math.random() < 0.4) {
const types = ['health', 'ammo0', 'ammo1', 'ammo2'];
pickups.push({
x: hitEnemy.x, y: hitEnemy.y,
type: types[Math.floor(Math.random()*types.length)],
amount: Math.floor(Math.random()*20)+10,
color: '#fff'
});
}
}
}
}
function respawnPlayer() {
player.health = 50;
player.x = 2 + Math.random() * 3;
player.y = 2 + Math.random() * 3;
weapons[0].ammo = Math.min(weapons[0].maxAmmo, weapons[0].ammo + 10);
deathCount++;
}
function checkWinCondition() {
const aliveEnemies = enemies.filter(e => e.alive).length;
if (aliveEnemies === 0 && gameState === 'playing') {
gameState = 'won';
document.getElementById('win-overlay').style.display = 'block';
document.getElementById('msg').textContent = 'VICTORY!\nAll demons slain!';
document.getElementById('msg').style.color = '#0f0';
document.getElementById('msg').style.textShadow = '0 0 20px #0f0, 0 0 40px #090';
document.getElementById('msg').style.display = 'block';
}
}
// Raycasting
function castRay(angle) {
const sin = Math.sin(angle), cos = Math.cos(angle);
let mapX = Math.floor(player.x), mapY = Math.floor(player.y);
const deltaDistX = Math.abs(1 / (cos || 0.0001));
const deltaDistY = Math.abs(1 / (sin || 0.0001));
let stepX, stepY, sideDistX, sideDistY;
if (cos < 0) { stepX = -1; sideDistX = (player.x - mapX) * deltaDistX; }
else { stepX = 1; sideDistX = (mapX + 1 - player.x) * deltaDistX; }
if (sin < 0) { stepY = -1; sideDistY = (player.y - mapY) * deltaDistY; }
else { stepY = 1; sideDistY = (mapY + 1 - player.y) * deltaDistY; }
let side = 0, hit = 0;
while (!hit && side < 30) {
if (sideDistX < sideDistY) {
sideDistX += deltaDistX;
mapX += stepX;
side = 0;
} else {
sideDistY += deltaDistY;
mapY += stepY;
side = 1;
}
if (mapX >= 0 && mapX < MAP_W && mapY >= 0 && mapY < MAP_H) {
if (MAP[mapY][mapX] > 0) hit = MAP[mapY][mapX];
} else break;
}
let perpDist;
if (side === 0) perpDist = sideDistX - deltaDistX;
else perpDist = sideDistY - deltaDistY;
return { dist: Math.max(perpDist, 0.01), wallType: hit, side };
}
// Render
function render() {
// Screen shake offset
const shakeX = screenShake > 0 ? (Math.random()-0.5) * screenShake : 0;
const shakeY = screenShake > 0 ? (Math.random()-0.5) * screenShake : 0;
ctx.save();
ctx.translate(shakeX, shakeY);
// Ceiling and floor
const ceilGrad = ctx.createLinearGradient(0, 0, 0, H/2);
ceilGrad.addColorStop(0, '#111');
ceilGrad.addColorStop(1, '#333');
ctx.fillStyle = ceilGrad;
ctx.fillRect(0, 0, W, H/2);
const floorGrad = ctx.createLinearGradient(0, H/2, 0, H);
floorGrad.addColorStop(0, '#444');
floorGrad.addColorStop(1, '#222');
ctx.fillStyle = floorGrad;
ctx.fillRect(0, H/2, W, H/2);
// Raycast walls
const zBuffer = new Float32Array(W);
const stripW = 2;
for (let x = 0; x < W; x += stripW) {
const rayAngle = player.angle - FOV/2 + (x/W) * FOV;
const ray = castRay(rayAngle);
zBuffer[x] = ray.dist;
const lineHeight = H / ray.dist;
const drawStart = Math.max(0, (H - lineHeight) / 2);
const drawEnd = Math.min(H, (H + lineHeight) / 2);
// Wall color with shading
const shade = Math.max(0.15, 1 - ray.dist / 15);
const wc = wallColors[ray.wallType] || wallColors[1];
const baseColor = ray.side === 1 ? wc[1] : wc[0];
// Parse hex color and apply shade
const r = parseInt(baseColor.slice(0,1).repeat(2), 16);
const g = parseInt(baseColor.slice(1,2).repeat(2), 16);
const b = parseInt(baseColor.slice(2,3).repeat(2), 16);
ctx.fillStyle = `rgb(${Math.floor(r*shade)},${Math.floor(g*shade)},${Math.floor(b*shade)})`;
ctx.fillRect(x, drawStart, stripW, drawEnd - drawStart);
// Wall edge highlight
if (ray.dist < 2) {
ctx.fillStyle = `rgba(255,255,255,${0.1 * shade})`;
ctx.fillRect(x, drawStart, stripW, 2);
}
}
// Sprite rendering (enemies and pickups)
const sprites = [];
enemies.forEach(e => {
if (!e.alive) return;
const dx = e.x - player.x, dy = e.y - player.y;
const dist = Math.sqrt(dx*dx + dy*dy);
sprites.push({ x: e.x, y: e.y, dist, type: 'enemy', data: e });
});
pickups.forEach((p, i) => {
const dx = p.x - player.x, dy = p.y - player.y;
const dist = Math.sqrt(dx*dx + dy*dy);
sprites.push({ x: p.x, y: p.y, dist, type: 'pickup', data: p, index: i });
});
// Sort by distance (far first)
sprites.sort((a, b) => b.dist - a.dist);
for (const sprite of sprites) {
const dx = sprite.x - player.x;
const dy = sprite.y - player.y;
let angle = Math.atan2(dy, dx) - player.angle;
while (angle < -Math.PI) angle += 2*Math.PI;
while (angle > Math.PI) angle -= 2*Math.PI;
if (Math.abs(angle) > FOV * 0.7) continue;
const screenX = W/2 + (angle / (FOV/2)) * (W/2);
const size = H / sprite.dist;
// Check z-buffer for visibility
const bufIdx = Math.floor(Math.max(0, Math.min(W-1, screenX)));
if (sprite.dist < zBuffer[bufIdx]) {
if (sprite.type === 'enemy') {
drawEnemy(screenX, size, sprite.data);
} else {
drawPickup(screenX, size, sprite.data);
}
}
}
// Damage flash
if (damageFlash > 0) {
ctx.fillStyle = `rgba(255,0,0,${damageFlash * 0.4})`;
ctx.fillRect(0, 0, W, H);
}
// Weapon rendering
drawWeapon();
ctx.restore();
}
function drawEnemy(sx, size, enemy) {
const w = size * 0.5;
const h = size * 0.7;
const yOff = H/2 - h*0.1 + Math.sin(Date.now()*0.003)*size*0.02;
// Body
let color = enemy.color;
if (enemy.hitFlash > 0) color = '#fff';
ctx.fillStyle = color;
ctx.fillRect(sx - w/2, yOff - h/2, w, h);
// Eyes
const eyeSize = Math.max(3, size * 0.06);
ctx.fillStyle = enemy.type === 1 ? '#ff0' : '#f00';
ctx.fillRect(sx - w*0.25, yOff - h*0.3, eyeSize, eyeSize);
ctx.fillRect(sx + w*0.15, yOff - h*0.3, eyeSize, eyeSize);
// Health bar
if (enemy.health < enemy.maxHealth) {
const barW = w * 0.8;
const barH = Math.max(2, size * 0.03);
ctx.fillStyle = '#400';
ctx.fillRect(sx - barW/2, yOff - h/2 - barH - 4, barW, barH);
ctx.fillStyle = '#f00';
ctx.fillRect(sx - barW/2, yOff - h/2 - barH - 4, barW * (enemy.health/enemy.maxHealth), barH);
}
}
function drawPickup(sx, size, pickup) {
const s = Math.min(size * 0.3, 40);
const yOff = H/2 + size*0.15;
const bob = Math.sin(Date.now()*0.005)*s*0.15;
ctx.fillStyle = pickup.color;
ctx.shadowColor = pickup.color;
ctx.shadowBlur = 10;
if (pickup.type === 'health') {
// Cross shape
const t = s * 0.3;
ctx.fillRect(sx - t/2, yOff + bob - s*0.4, t, s*0.8);
ctx.fillRect(sx - s*0.4, yOff + bob - t/2, s*0.8, t);
} else {
// Ammo box
ctx.fillRect(sx - s*0.35, yOff + bob - s*0.35, s*0.7, s*0.7);
ctx.fillStyle = '#fff';
ctx.font = `${Math.max(8,s*0.4)}px monospace`;
ctx.textAlign = 'center';
const label = pickup.type === 'ammo0' ? 'P' : pickup.type === 'ammo1' ? 'S' : 'M';
ctx.fillText(label, sx, yOff + bob + s*0.15);
}
ctx.shadowBlur = 0;
}
function drawWeapon() {
const w = weapons[currentWeapon];
const bobX = Math.sin(weaponBob) * 8;
const bobY = Math.abs(Math.cos(weaponBob)) * 6 + weaponRecoil;
const wx = W/2 - 40 + bobX;
const wy = H - 180 + bobY;
ctx.save();
if (currentWeapon === 0) {
// Pistol
ctx.fillStyle = '#666';
ctx.fillRect(wx, wy, 30, 80);
ctx.fillStyle = '#555';
ctx.fillRect(wx+5, wy-20, 20, 40);
ctx.fillStyle = '#444';
ctx.fillRect(wx+10, wy+60, 10, 30);
// Muzzle flash
if (weaponRecoil > 4) {
ctx.fillStyle = '#ff8';
ctx.beginPath();
ctx.arc(wx+15, wy-25, 15, 0, Math.PI*2);
ctx.fill();
}
} else if (currentWeapon === 1) {
// Shotgun
ctx.fillStyle = '#654';
ctx.fillRect(wx-5, wy+10, 40, 70);
ctx.fillStyle = '#555';
ctx.fillRect(wx, wy-30, 12, 50);
ctx.fillRect(wx+18, wy-30, 12, 50);
ctx.fillStyle = '#444';
ctx.fillRect(wx+5, wy+70, 30, 25);
if (weaponRecoil > 4) {
ctx.fillStyle = '#f80';
ctx.beginPath();
ctx.arc(wx+10, wy-35, 12, 0, Math.PI*2);
ctx.fill();
ctx.beginPath();
ctx.arc(wx+26, wy-35, 12, 0, Math.PI*2);
ctx.fill();
}
} else {
// Machine gun
ctx.fillStyle = '#777';
ctx.fillRect(wx-5, wy, 45, 85);
ctx.fillStyle = '#666';
ctx.fillRect(wx, wy-25, 15, 35);
ctx.fillRect(wx+20, wy-25, 15, 35);
ctx.fillStyle = '#555';
ctx.fillRect(wx+5, wy+75, 35, 20);
// Magazine
ctx.fillStyle = '#888';
ctx.fillRect(wx+35, wy+10, 10, 40);
if (weaponRecoil > 4) {
ctx.fillStyle = '#0ff';
ctx.beginPath();
ctx.arc(wx+7, wy-30, 10, 0, Math.PI*2);
ctx.fill();
ctx.beginPath();
ctx.arc(wx+27, wy-30, 10, 0, Math.PI*2);
ctx.fill();
}
}
ctx.restore();
}
function drawMinimap() {
const scale = 6;
miniCtx.fillStyle = '#000';
miniCtx.fillRect(0, 0, 160, 160);
for (let y = 0; y < MAP_H; y++) {
for (let x = 0; x < MAP_W; x++) {
if (MAP[y][x] > 0) {
miniCtx.fillStyle = '#444';
miniCtx.fillRect(x*scale, y*scale, scale, scale);
}
}
}
// Enemies
enemies.forEach(e => {
if (!e.alive) return;
miniCtx.fillStyle = '#f00';
miniCtx.fillRect(e.x*scale-1, e.y*scale-1, 3, 3);
});
// Pickups
pickups.forEach(p => {
miniCtx.fillStyle = p.type === 'health' ? '#0f0' : '#ff0';
miniCtx.fillRect(p.x*scale-1, p.y*scale-1, 2, 2);
});
// Player
miniCtx.fillStyle = '#0f0';
miniCtx.beginPath();
miniCtx.arc(player.x*scale, player.y*scale, 3, 0, Math.PI*2);
miniCtx.fill();
// Direction indicator
miniCtx.strokeStyle = '#0f0';
miniCtx.lineWidth = 1;
miniCtx.beginPath();
miniCtx.moveTo(player.x*scale, player.y*scale);
miniCtx.lineTo(
(player.x + Math.cos(player.angle)*2)*scale,
(player.y + Math.sin(player.angle)*2)*scale
);
miniCtx.stroke();
}
function updateHUD() {
const w = weapons[currentWeapon];
document.getElementById('health-fill').style.width = (player.health / player.maxHealth * 100) + '%';
document.getElementById('weapon-name').textContent = w.name;
document.getElementById('ammo-display').textContent = `AMMO: ${w.ammo}`;
document.getElementById('enemy-count').textContent = enemies.filter(e => e.alive).length;
document.getElementById('kill-count').textContent = kills;
}
// Collision detection
function isWall(x, y) {
const mx = Math.floor(x), my = Math.floor(y);
if (mx < 0 || mx >= MAP_W || my < 0 || my >= MAP_H) return true;
return MAP[my][mx] > 0;
}
function canMove(x, y) {
const r = 0.2;
return !isWall(x-r,y-r) && !isWall(x+r,y-r) && !isWall(x-r,y+r) && !isWall(x+r,y+r);
}
// Enemy AI
function updateEnemies(dt) {
enemies.forEach(e => {
if (!e.alive) return;
const dx = player.x - e.x, dy = player.y - e.y;
const dist = Math.sqrt(dx*dx + dy*dy);
// Hit flash decay
if (e.hitFlash > 0) e.hitFlash -= dt;
// Attack timer
if (e.attackTimer > 0) e.attackTimer -= dt;
if (dist < 12) {
// Move toward player
const moveX = (dx / dist) * e.speed * dt;
const moveY = (dy / dist) * e.speed * dt;
if (canMove(e.x + moveX, e.y)) e.x += moveX;
if (canMove(e.x, e.y + moveY)) e.y += moveY;
// Attack player
if (dist < 1.2 && e.attackTimer <= 0) {
player.health -= e.damage;
damageFlash = 0.5;
screenShake = 6;
playSound(150, 0.2, 'sawtooth', 0.2);
e.attackTimer = 1.0;
if (player.health <= 0) {
player.health = 0;
gameState = 'dead';
document.getElementById('death-overlay').style.display = 'block';
playDeathSound();
setTimeout(() => {
respawnPlayer();
gameState = 'playing';
document.getElementById('death-overlay').style.display = 'none';
}, 2000);
}
}
}
});
}
// Pickup collection
function updatePickups() {
for (let i = pickups.length - 1; i >= 0; i--) {
const p = pickups[i];
const dx = player.x - p.x, dy = player.y - p.y;
if (dx*dx + dy*dy < 0.5) {
if (p.type === 'health') {
player.health = Math.min(player.maxHealth, player.health + p.amount);
} else {
const wIdx = parseInt(p.type.slice(4));
weapons[wIdx].ammo = Math.min(weapons[wIdx].maxAmmo, weapons[wIdx].ammo + p.amount);
}
playPickupSound();
pickups.splice(i, 1);
}
}
}
// Main game loop
let lastTime = 0;
function gameLoop(timestamp) {
const dt = Math.min((timestamp - lastTime) / 1000, 0.05);
lastTime = timestamp;
if (gameState === 'playing') {
// Mouse look
player.angle += mouseDX * ROT_SPEED;
mouseDX = 0;
// Movement
let moveX = 0, moveY = 0;
const cos = Math.cos(player.angle), sin = Math.sin(player.angle);
if (keys['w']) { moveX += cos; moveY += sin; }
if (keys['s']) { moveX -= cos; moveY -= sin; }
if (keys['a']) { moveX += sin; moveY -= cos; }
if (keys['d']) { moveX -= sin; moveY += cos; }
// Normalize diagonal movement
const len = Math.sqrt(moveX*moveX + moveY*moveY);
if (len > 0) {
moveX = (moveX/len) * MOVE_SPEED * dt;
moveY = (moveY/len) * MOVE_SPEED * dt;
// Collision with sliding
if (canMove(player.x + moveX, player.y)) player.x += moveX;
if (canMove(player.x, player.y + moveY)) player.y += moveY;
weaponBob += dt * 8;
}
// Decay effects
if (weaponRecoil > 0) weaponRecoil -= dt * 30;
if (screenShake > 0) screenShake -= dt * 20;
if (damageFlash > 0) damageFlash -= dt * 3;
updateEnemies(dt);
updatePickups();
}
render();
drawMinimap();
updateHUD();
requestAnimationFrame(gameLoop);
}
// Start game
document.getElementById('start-btn').addEventListener('click', () => {
document.getElementById('start-screen').style.display = 'none';
initAudio();
spawnEnemies();
spawnPickups();
gameState = 'playing';
canvas.requestPointerLock();
document.addEventListener('pointerlockchange', () => {
pointerLocked = !!document.pointerLockElement;
});
requestAnimationFrame(gameLoop);
});
// Click to re-lock pointer
canvas.addEventListener('click', () => {
if (gameState === 'playing' && !pointerLocked) {
canvas.requestPointerLock();
}
});
</script>
</html>
<!-- agent-meta {"model":"thinkingcap-qwen3.6-27b","provider":"lmstudio","persona":"basic","sessionId":"70084cd4-5987-459b-bdf2-0ab8acc64ffc","tokensIn":234,"tokensOut":10742,"tokensTotal":10976,"turns":1,"toolCalls":0,"failedToolCalls":0,"timestamp":"2026-07-19T01:36:33.081Z"} -->