← DOOM results

DOOM

thinkingcap qwen3.6 27b · basic

Initial view of DOOM

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"} -->