← DOOM results

DOOM

thinkingcap qwen3.6 27b · full

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 32,186 bytes · SHA-256 b618b85c8790
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>DUNGEON RAIDERS - 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; 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(to top, rgba(0,0,0,0.9), transparent); display: flex; align-items: flex-end; justify-content: space-between; padding: 10px 20px; z-index: 10; }
.hud-item { color: #fff; font-size: 18px; text-shadow: 0 0 8px currentColor; }
#health-bar, #armor-bar { width: 150px; height: 16px; background: #333; border: 2px solid #555; margin-top: 4px; }
#health-fill { height: 100%; background: linear-gradient(90deg, #f00, #0f0); transition: width 0.3s; }
#armor-fill { height: 100%; background: linear-gradient(90deg, #44f, #88f); transition: width 0.3s; }
#weapon-name { color: #ff0; font-size: 22px; text-align: center; }
#msg { position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%); color: #f00; font-size: 48px; text-shadow: 0 0 20px #f00, 0 0 40px #900; display: none; z-index: 20; 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: radial-gradient(ellipse at center, #1a0505 0%, #000 70%); display: flex; flex-direction: column; align-items: center; justify-content: center; z-index: 100; }
#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: 18px; margin: 5px 0; }
#start-btn { margin-top: 30px; padding: 15px 40px; background: #900; color: #fff; border: 2px solid #f00; font-family: monospace; font-size: 24px; cursor: pointer; pointer-events: all; }
#start-btn:hover { background: #c00; }
#damage-flash { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: radial-gradient(ellipse at center, transparent 40%, rgba(255,0,0,0.6)); opacity: 0; z-index: 9; transition: opacity 0.1s; }
#level-msg { position: absolute; top: 30%; left: 50%; transform: translate(-50%,-50%); color: #ff0; font-size: 36px; text-shadow: 0 0 20px #ff0; display: none; z-index: 20; }
</style>
</head>
<body>

<div id="start-screen">
<h1>DUNGEON RAIDERS</h1>
<p>WASD - Move | Mouse - Look | Click - Shoot</p>
<p>1/2/3 - Switch Weapons | E - Open Doors</p>
<p>Kill all enemies and find the exit!</p>
<button id="start-btn">ENTER THE DUNGEON</button>
</div>

<canvas id="game"></canvas>
<div id="crosshair">+</div>
<div id="damage-flash"></div>
<div id="hud">
  <div class="hud-item" style="color:#f44;">
    ❤ HEALTH
    <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" style="color:#ff0;font-size:28px;margin-top:4px;">12 / ∞</div>
  </div>
  <div class="hud-item" style="text-align:right;color:#4af;">
    🛡 ARMOR
    <div id="armor-bar"><div id="armor-fill" style="width:0%"></div></div>
    <div id="score-display" style="color:#ff0;margin-top:4px;">SCORE: 0</div>
  </div>
</div>
<canvas id="minimap" width="160" height="160"></canvas>
<div id="msg"></div>
<div id="level-msg"></div>

<script>
// ============ CONSTANTS ============
const W = 640, H = 360;
const TILE = 1;
const FOV = Math.PI / 3;
const HALF_FOV = FOV / 2;
const NUM_RAYS = W;
const MAX_DEPTH = 20;

// ============ MAP DATA ============
// 0=empty, 1-4=walls, 5=door, 9=exit
const LEVELS = [
  // Level 1 - The Dungeon Entrance
  {
    map: [
      [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
      [1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,1],
      [1,0,0,0,0,0,1,0,0,2,2,0,0,0,0,1],
      [1,0,0,3,0,0,5,0,0,2,0,0,0,4,0,1],
      [1,0,0,3,0,0,1,0,0,0,0,0,0,4,0,1],
      [1,0,0,0,0,0,1,0,0,0,5,0,0,0,0,1],
      [1,1,1,0,0,1,1,0,0,0,1,1,1,0,1,1],
      [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
      [1,0,2,2,0,0,1,0,3,3,0,0,1,0,0,1],
      [1,0,0,0,0,0,1,0,0,0,0,0,1,0,0,1],
      [1,0,2,2,0,0,5,0,3,3,0,0,5,0,0,1],
      [1,0,0,0,0,0,1,0,0,0,0,0,1,0,0,1],
      [1,0,0,0,0,0,1,1,0,0,1,1,1,0,0,1],
      [1,0,0,4,0,0,0,0,0,0,0,0,0,0,0,1],
      [1,0,0,4,0,0,0,0,2,2,0,0,9,0,0,1],
      [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]
    ],
    spawn: { x: 1.5, y: 1.5, angle: 0 },
    enemies: [
      {x:7.5,y:3.5,type:'grunt'},{x:12.5,y:2.5,type:'grunt'},
      {x:4.5,y:8.5,type:'soldier'},{x:10.5,y:9.5,type:'grunt'},
      {x:7.5,y:13.5,type:'soldier'}
    ],
    pickups: [
      {x:3.5,y:2.5,type:'health'},{x:13.5,y:4.5,type:'ammo_shotgun'},
      {x:8.5,y:7.5,type:'shotgun'},{x:2.5,y:10.5,type:'health'},
      {x:12.5,y:12.5,type:'ammo_plasma'},{x:6.5,y:14.5,type:'armor'}
    ]
  },
  // Level 2 - Deep Caverns
  {
    map: [
      [2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2],
      [2,0,0,0,3,0,0,0,0,4,0,0,0,0,0,2],
      [2,0,0,0,3,0,0,0,0,4,0,0,5,0,0,2],
      [2,0,0,0,0,0,1,1,0,0,0,0,0,0,0,2],
      [2,3,3,0,0,0,1,0,0,0,5,0,0,4,0,2],
      [2,0,0,0,0,0,0,0,0,0,0,0,0,4,0,2],
      [2,0,0,4,4,0,0,3,3,0,0,1,1,1,0,2],
      [2,0,0,4,0,0,0,0,0,0,0,1,0,0,0,2],
      [2,0,0,5,0,0,3,0,0,0,0,5,0,0,0,2],
      [2,0,0,4,0,0,3,0,0,0,0,1,0,0,0,2],
      [2,0,0,4,0,0,0,0,0,0,0,1,1,1,0,2],
      [2,5,0,0,0,0,0,3,3,0,0,0,0,0,0,2],
      [2,0,0,0,0,4,0,0,0,0,0,0,0,0,0,2],
      [2,0,0,0,0,4,0,0,5,0,0,3,3,0,0,2],
      [2,0,0,9,0,0,0,0,0,0,0,3,0,0,0,2],
      [2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2]
    ],
    spawn: { x: 1.5, y: 1.5, angle: 0 },
    enemies: [
      {x:5.5,y:3.5,type:'soldier'},{x:12.5,y:2.5,type:'grunt'},
      {x:8.5,y:6.5,type:'demon'},{x:3.5,y:9.5,type:'soldier'},
      {x:10.5,y:10.5,type:'soldier'},{x:7.5,y:13.5,type:'grunt'}
    ],
    pickups: [
      {x:6.5,y:2.5,type:'health'},{x:14.5,y:5.5,type:'ammo_shotgun'},
      {x:8.5,y:7.5,type:'plasma'},{x:3.5,y:10.5,type:'armor'},
      {x:12.5,y:12.5,type:'health'},{x:6.5,y:14.5,type:'ammo_plasma'}
    ]
  },
  // Level 3 - The Boss Chamber
  {
    map: [
      [4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4],
      [4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4],
      [4,0,3,3,0,0,1,1,1,1,0,0,3,3,0,4],
      [4,0,3,0,0,0,1,0,0,1,0,0,0,3,0,4],
      [4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4],
      [4,0,0,0,5,0,0,0,0,0,0,5,0,0,0,4],
      [4,0,1,1,0,0,0,0,0,0,0,0,1,1,0,4],
      [4,0,1,0,0,0,0,0,0,0,0,0,0,1,0,4],
      [4,0,1,0,0,0,0,0,0,0,0,0,0,1,0,4],
      [4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4],
      [4,0,1,1,0,0,0,0,0,0,0,0,1,1,0,4],
      [4,0,0,0,5,0,0,0,0,0,0,5,0,0,0,4],
      [4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4],
      [4,0,3,0,0,0,1,0,0,1,0,0,0,3,0,4],
      [4,0,3,3,0,0,1,1,9,1,0,0,3,3,0,4],
      [4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4]
    ],
    spawn: { x: 1.5, y: 1.5, angle: Math.PI/2 },
    enemies: [
      {x:7.5,y:7.5,type:'boss'},
      {x:3.5,y:4.5,type:'soldier'},{x:12.5,y:4.5,type:'soldier'},
      {x:3.5,y:10.5,type:'demon'},{x:12.5,y:10.5,type:'demon'}
    ],
    pickups: [
      {x:7.5,y:2.5,type:'health'},{x:2.5,y:7.5,type:'armor'},
      {x:13.5,y:7.5,type:'ammo_shotgun'},{x:7.5,y:12.5,type:'health'}
    ]
  }
];

// ============ GAME STATE ============
let canvas, ctx, miniCanvas, miniCtx;
let gameState = 'menu'; // menu, playing, dead, won, levelTransition
let currentLevel = 0;
let map, enemies, pickups;
let player = { x:1.5, y:1.5, angle:0, health:100, maxHealth:100, armor:0 };
let weapons = [
  { name:'PISTOL', ammo:[12,Infinity], fireRate:300, damage:15, spread:0.02, range:MAX_DEPTH, auto:false },
  { name:'SHOTGUN', ammo:[8,0], fireRate:600, damage:40, spread:0.08, range:10, pellets:5, auto:false },
  { name:'PLASMA', ammo:[30,0], fireRate:120, damage:10, spread:0.03, range:MAX_DEPTH, auto:true }
];
let currentWeapon = 0;
let score = 0;
let zBuffer = new Float64Array(NUM_RAYS);
let keys = {};
let mouseDX = 0;
let lastShot = 0;
let weaponBob = 0;
let weaponRecoil = 0;
let muzzleFlash = 0;
let damageFlashTimer = 0;
let shootAnimFrame = 0;

// ============ INIT ============
function init() {
    canvas = document.getElementById('game');
    ctx = canvas.getContext('2d');
    canvas.width = W;
    canvas.height = H;
    miniCanvas = document.getElementById('minimap');
    miniCtx = miniCanvas.getContext('2d');

    document.addEventListener('keydown', e => { keys[e.key.toLowerCase()] = true; if(e.key>='1'&&e.key<='3') switchWeapon(+e.key-1); });
    document.addEventListener('keyup', e => keys[e.key.toLowerCase()] = false);
    document.addEventListener('mousemove', e => { if(document.pointerLockElement===canvas) mouseDX += e.movementX; });
    canvas.addEventListener('mousedown', () => { if(gameState==='playing') shoot(); });

    document.getElementById('start-btn').addEventListener('click', startGame);
}

function startGame() {
    document.getElementById('start-screen').style.display = 'none';
    currentLevel = 0;
    player.health = 100;
    player.armor = 0;
    score = 0;
    weapons[0].ammo = [12, Infinity];
    weapons[1].ammo = [8, 0];
    weapons[2].ammo = [30, 0];
    currentWeapon = 0;
    loadLevel(0);
    canvas.requestPointerLock();
    gameState = 'playing';
    requestAnimationFrame(gameLoop);
}

function loadLevel(n) {
    const lvl = LEVELS[n];
    map = lvl.map.map(r => [...r]);
    enemies = lvl.enemies.map(e => ({...e, health: e.type==='boss'?200:e.type==='demon'?80:40, maxHealth: e.type==='boss'?200:e.type==='demon'?80:40, speed: e.type==='boss'?1.5:e.type==='demon'?2:1.2, attackRate: e.type==='boss'?800:1200, lastAttack:0, state:'idle', alertDist: 8}));
    pickups = lvl.pickups.map(p => ({...p, active:true}));
    player.x = lvl.spawn.x;
    player.y = lvl.spawn.y;
    player.angle = lvl.spawn.angle;
    
    showLevelMsg(`LEVEL ${n+1}`);
}

function showLevelMsg(text) {
    const el = document.getElementById('level-msg');
    el.textContent = text;
    el.style.display = 'block';
    setTimeout(() => el.style.display = 'none', 2000);
}

// ============ GAME LOOP ============
let lastTime = 0;
function gameLoop(ts) {
    const dt = Math.min((ts - lastTime)/1000, 0.05);
    lastTime = ts;

    if(gameState === 'playing') {
        update(dt);
    }
    render();
    requestAnimationFrame(gameLoop);
}

// ============ UPDATE ============
function update(dt) {
    // Mouse look
    player.angle += mouseDX * 0.002;
    mouseDX = 0;

    // Movement
    const moveSpeed = 3.5 * dt;
    let dx = 0, dy = 0;
    if(keys['w']) { dx += Math.cos(player.angle)*moveSpeed; dy += Math.sin(player.angle)*moveSpeed; }
    if(keys['s']) { dx -= Math.cos(player.angle)*moveSpeed; dy -= Math.sin(player.angle)*moveSpeed; }
    if(keys['a']) { dx += Math.cos(player.angle-Math.PI/2)*moveSpeed*0.7; dy += Math.sin(player.angle-Math.PI/2)*moveSpeed*0.7; }
    if(keys['d']) { dx -= Math.cos(player.angle-Math.PI/2)*moveSpeed*0.7; dy -= Math.sin(player.angle-Math.PI/2)*moveSpeed*0.7; }

    // Collision - check X and Y separately with proper wall detection
    const margin = 0.2;
    if(dx !== 0) {
        const nx = player.x + dx;
        const cx1 = Math.floor(nx + (dx > 0 ? margin : -margin));
        const cy1 = Math.floor(player.y);
        if(cy1>=0 && cy1<map.length && cx1>=0 && cx1<map[0].length) {
            if(map[cy1][cx1] === 0 || map[cy1][cx1] === 9) player.x = nx;
        } else if(cx1 < 0 || cx1 >= map[0].length) {
            // out of bounds = wall
        } else {
            player.x = nx;
        }
    }
    if(dy !== 0) {
        const ny = player.y + dy;
        const cx2 = Math.floor(player.x);
        const cy2 = Math.floor(ny + (dy > 0 ? margin : -margin));
        if(cy2>=0 && cy2<map.length && cx2>=0 && cx2<map[0].length) {
            if(map[cy2][cx2] === 0 || map[cy2][cx2] === 9) player.y = ny;
        } else if(cy2 < 0 || cy2 >= map.length) {
            // out of bounds = wall
        } else {
            player.y = ny;
        }
    }

    // Door interaction
    if(keys['e']) {
        const fx = Math.floor(player.x + Math.cos(player.angle)*1.5);
        const fy = Math.floor(player.y + Math.sin(player.angle)*1.5);
        if(fx>=0&&fx<map[0].length&&fy>=0&&fy<map.length && map[fy][fx]===5) {
            map[fy][fx] = 0; // open door
        }
    }

    // Exit check
    const ex = Math.floor(player.x);
    const ey = Math.floor(player.y);
    if(ey>=0&&ey<map.length&&ex>=0&&ex<map[0].length && map[ey][ex]===9) {
        currentLevel++;
        if(currentLevel >= LEVELS.length) {
            gameState = 'won';
            showMsg('YOU ESCAPED THE DUNGEON!\nFINAL SCORE: '+score);
        } else {
            loadLevel(currentLevel);
        }
    }

    // Weapon bob
    const moving = dx !== 0 || dy !== 0;
    if(moving) weaponBob += dt * 8;
    else weaponBob *= 0.9;

    // Auto-fire for plasma gun
    const w = weapons[currentWeapon];
    if(w.auto && mouseDown) {
        shoot();
    }

    // Weapon recoil decay
    weaponRecoil *= 0.85;
    muzzleFlash = Math.max(0, muzzleFlash - dt * 10);

    // Damage flash
    damageFlashTimer = Math.max(0, damageFlashTimer - dt);
    document.getElementById('damage-flash').style.opacity = damageFlashTimer > 0 ? '1' : '0';

    // Update enemies
    const now = performance.now();
    for(let e of enemies) {
        if(e.health <= 0) continue;
        const dist = Math.hypot(player.x - e.x, player.y - e.y);
        
        if(dist < e.alertDist) {
            e.state = 'chase';
            // Move toward player
            const angle = Math.atan2(player.y - e.y, player.x - e.x);
            const espeed = e.speed * dt;
            let edx = Math.cos(angle)*espeed;
            let edy = Math.sin(angle)*espeed;
            
            if(map[Math.floor(e.y)][Math.floor(e.x+edx)] === 0) e.x += edx;
            if(map[Math.floor(e.y+edy)][Math.floor(e.x)] === 0) e.y += edy;

            // Attack
            if(dist < 8 && now - e.lastAttack > e.attackRate) {
                e.lastAttack = now;
                const dmg = e.type==='boss' ? 25 : e.type==='demon' ? 15 : 10;
                dealDamage(dmg);
            }
        } else {
            e.state = 'idle';
        }
    }

    // Pickups
    for(let p of pickups) {
        if(!p.active) continue;
        const dist = Math.hypot(player.x - p.x, player.y - p.y);
        if(dist < 0.5) {
            p.active = false;
            applyPickup(p.type);
        }
    }

    // Check all enemies dead (for boss level win condition alternative)
    const aliveEnemies = enemies.filter(e => e.health > 0);
    
    updateHUD();
}

function switchWeapon(n) {
    if(n >= 0 && n < weapons.length) currentWeapon = n;
}

function shoot() {
    const w = weapons[currentWeapon];
    const now = performance.now();
    if(now - lastShot < w.fireRate) return;
    if(w.ammo[0] <= 0 && w.ammo[1] <= 0) return;
    
    lastShot = now;
    weaponRecoil = 8;
    muzzleFlash = 1;
    shootAnimFrame = 5;

    if(w.ammo[0] > 0) w.ammo[0]--;
    else w.ammo[1]--;

    const pellets = w.pellets || 1;
    for(let i = 0; i < pellets; i++) {
        const spread = (Math.random()-0.5) * w.spread;
        raycastHit(player.x, player.y, player.angle + spread, w.range);
    }
}

function raycastHit(ox, oy, angle, range) {
    const cosA = Math.cos(angle);
    const sinA = Math.sin(angle);
    
    // Check enemy hits first (closest)
    let closestEnemy = null;
    let closestDist = range;
    
    for(let e of enemies) {
        if(e.health <= 0) continue;
        const dx = e.x - ox;
        const dy = e.y - oy;
        const dist = Math.hypot(dx, dy);
        if(dist > range || dist >= closestDist) continue;
        
        // Check if enemy is in the ray direction (dot product)
        const dot = (dx*cosA + dy*sinA) / dist;
        if(dot > 0.95 && dist < closestDist) {
            closestDist = dist;
            closestEnemy = e;
        }
    }

    // Also check wall hit to stop ray
    let wallDist = MAX_DEPTH;
    let step = 0.1;
    for(let d = 0; d < range; d += step) {
        const cx = ox + cosA * d;
        const cy = oy + sinA * d;
        const mx = Math.floor(cx);
        const my = Math.floor(cy);
        if(my>=0 && my<map.length && mx>=0 && mx<map[0].length && map[my][mx] >= 1) {
            wallDist = d;
            break;
        }
    }

    if(closestEnemy && closestDist < wallDist) {
        const w = weapons[currentWeapon];
        closestEnemy.health -= w.damage;
        score += closestEnemy.type==='boss'?500:closestEnemy.type==='demon'?200:100;
        
        if(closestEnemy.health <= 0) {
            score += 50; // kill bonus
        }
    }
}

function dealDamage(dmg) {
    let actual = dmg;
    if(player.armor > 0) {
        const absorbed = Math.min(player.armor, dmg * 0.6);
        player.armor -= absorbed;
        actual -= absorbed;
    }
    player.health -= actual;
    damageFlashTimer = 0.3;

    if(player.health <= 0) {
        player.health = 0;
        gameState = 'dead';
        showMsg('YOU DIED\nPress R to respawn');
        if(keys['r']) respawn();
    }
}

function applyPickup(type) {
    switch(type) {
        case 'health': player.health = Math.min(player.maxHealth, player.health + 25); break;
        case 'armor': player.armor = Math.min(100, player.armor + 30); break;
        case 'ammo_pistol': weapons[0].ammo[1] += 12; break;
        case 'ammo_shotgun': weapons[1].ammo[1] += 8; break;
        case 'ammo_plasma': weapons[2].ammo[1] += 30; break;
        case 'shotgun': if(weapons[1].ammo[0]<=0) { weapons[1].ammo = [8, weapons[1].ammo[1]]; } else weapons[1].ammo[0] = Math.min(50, weapons[1].ammo[0]+4); break;
        case 'plasma': if(weapons[2].ammo[0]<=0) { weapons[2].ammo = [30, weapons[2].ammo[1]]; } else weapons[2].ammo[0] += 15; break;
    }
}

function respawn() {
    player.health = 100;
    player.armor = Math.max(0, player.armor - 20);
    loadLevel(currentLevel);
    gameState = 'playing';
    document.getElementById('msg').style.display = 'none';
}

function showMsg(text) {
    const el = document.getElementById('msg');
    el.innerHTML = text.replace(/\n/g,'<br>');
    el.style.display = 'block';
}

function updateHUD() {
    document.getElementById('health-fill').style.width = (player.health/player.maxHealth*100)+'%';
    document.getElementById('armor-fill').style.width = player.armor+'%';
    const w = weapons[currentWeapon];
    document.getElementById('weapon-name').textContent = w.name;
    if(w.ammo[1] === Infinity) {
        document.getElementById('ammo-display').textContent = w.ammo[0]+' / ∞';
    } else {
        document.getElementById('ammo-display').textContent = w.ammo[0]+' / '+w.ammo[1];
    }
    document.getElementById('score-display').textContent = 'SCORE: '+score;
}

// ============ RENDERING ============
function render() {
    // Sky and floor
    const skyGrad = ctx.createLinearGradient(0, 0, 0, H/2);
    skyGrad.addColorStop(0, '#1a0520');
    skyGrad.addColorStop(1, '#3a1540');
    ctx.fillStyle = skyGrad;
    ctx.fillRect(0, 0, W, H/2);

    const floorGrad = ctx.createLinearGradient(0, H/2, 0, H);
    floorGrad.addColorStop(0, '#2a2a2a');
    floorGrad.addColorStop(1, '#555');
    ctx.fillStyle = floorGrad;
    ctx.fillRect(0, H/2, W, H/2);

    // Raycasting
    const stripW = 1;
    for(let i = 0; i < NUM_RAYS; i++) {
        const rayAngle = player.angle - HALF_FOV + (i / NUM_RAYS) * FOV;
        const result = castRay(rayAngle);
        
        // Wall color based on type and distance
        const wallColors = {
            1: [120,60,60],   // red brick
            2: [80,80,100],   // blue stone
            3: [100,90,70],   // brown wood
            4: [70,70,70],    // grey metal
            5: [140,120,60],  // gold door
            9: [0,200,0]      // green exit
        };
        
        const wc = wallColors[result.wallType] || [100,100,100];
        const shade = Math.max(0.15, 1 - result.dist / MAX_DEPTH);
        const r = Math.floor(wc[0]*shade);
        const g = Math.floor(wc[1]*shade);
        const b = Math.floor(wc[2]*shade);

        // Vertical shading (darker at edges for pseudo-texture)
        const wallH = H / result.dist;
        const top = (H - wallH) / 2;
        
        ctx.fillStyle = `rgb(${r},${g},${b})`;
        ctx.fillRect(i, top, stripW+1, wallH);

        // Wall edge highlight
        if(result.side === 0) {
            ctx.fillStyle = `rgba(255,255,255,${shade*0.1})`;
            ctx.fillRect(i, top, stripW+1, wallH);
        }

        zBuffer[i] = result.dist;
    }

    // Render sprites (enemies + pickups)
    renderSprites();

    // Weapon rendering
    renderWeapon();

    // Minimap
    renderMinimap();
}

function castRay(angle) {
    const cosA = Math.cos(angle);
    const sinA = Math.sin(angle);
    
    let mapX = Math.floor(player.x);
    let mapY = Math.floor(player.y);
    
    const deltaDistX = Math.abs(1/cosA) || 1e10;
    const deltaDistY = Math.abs(1/sinA) || 1e10;
    
    let stepX, stepY, sideDistX, sideDistY;
    
    if(cosA < 0) { stepX = -1; sideDistX = (player.x - mapX) * deltaDistX; }
    else { stepX = 1; sideDistX = (mapX + 1 - player.x) * deltaDistX; }
    
    if(sinA < 0) { stepY = -1; sideDistY = (player.y - mapY) * deltaDistY; }
    else { stepY = 1; sideDistY = (mapY + 1 - player.y) * deltaDistY; }
    
    let side = 0;
    let hit = false;
    let wallType = 0;
    
    for(let i = 0; i < 64; i++) {
        if(sideDistX < sideDistY) {
            sideDistX += deltaDistX;
            mapX += stepX;
            side = 0;
        } else {
            sideDistY += deltaDistY;
            mapY += stepY;
            side = 1;
        }
        
        if(mapY >= 0 && mapY < map.length && mapX >= 0 && mapX < map[0].length) {
            if(map[mapY][mapX] > 0) {
                hit = true;
                wallType = map[mapY][mapX];
                break;
            }
        } else {
            hit = true;
            break;
        }
    }
    
    let dist;
    if(side === 0) dist = (mapX - player.x + (1-stepX)/2) / cosA;
    else dist = (mapY - player.y + (1-stepY)/2) / sinA;
    
    return { dist: Math.max(dist, 0.1), wallType, side };
}

function renderSprites() {
    // Collect all sprites
    let sprites = [];
    
    for(let e of enemies) {
        if(e.health <= 0) continue;
        const dx = e.x - player.x;
        const dy = e.y - player.y;
        const dist = Math.hypot(dx, dy);
        sprites.push({x:e.x, y:e.y, dist, type:'enemy', data:e});
    }
    
    for(let p of pickups) {
        if(!p.active) continue;
        const dx = p.x - player.x;
        const dy = p.y - player.y;
        const dist = Math.hypot(dx, dy);
        sprites.push({x:p.x, y:p.y, dist, type:'pickup', data:p});
    }
    
    // Sort back to front
    sprites.sort((a,b) => b.dist - a.dist);
    
    for(let s of sprites) {
        const dx = s.x - player.x;
        const dy = s.y - player.y;
        
        // Transform relative to camera
        const invDet = 1.0 / (Math.cos(player.angle + Math.PI/2) * Math.sin(player.angle) - 
                               Math.sin(player.angle + Math.PI/2) * Math.cos(player.angle));
        // Simplified: use direct angle calculation
        let relAngle = Math.atan2(dy, dx) - player.angle;
        while(relAngle > Math.PI) relAngle -= 2*Math.PI;
        while(relAngle < -Math.PI) relAngle += 2*Math.PI;
        
        if(Math.abs(relAngle) > HALF_FOV + 0.3) continue;
        
        const screenX = W/2 + (relAngle / HALF_FOV) * (W/2);
        const spriteH = H / s.dist;
        const spriteW = spriteH * 0.7;
        
        if(s.type === 'enemy') {
            drawEnemy(screenX, spriteH, spriteW, s.data, s.dist);
        } else {
            drawPickup(screenX, spriteH, s.data, s.dist);
        }
    }
}

function drawEnemy(sx, sh, sw, enemy, dist) {
    const shade = Math.max(0.2, 1 - dist/MAX_DEPTH);
    const top = (H - sh) / 2 + sh * 0.1;
    
    // Check z-buffer for visibility
    const startCol = Math.max(0, Math.floor(sx - sw/2));
    const endCol = Math.min(W-1, Math.floor(sx + sw/2));
    let visible = false;
    for(let c = startCol; c <= endCol; c++) {
        if(zBuffer[c] > dist) { visible = true; break; }
    }
    if(!visible) return;

    // Body colors by type
    const colors = {
        grunt: ['#8B4513', '#654321'],
        soldier: ['#4a4a6a', '#3a3a5a'],
        demon: ['#aa2222', '#881111'],
        boss: ['#cc0000', '#990000']
    };
    const [c1, c2] = colors[enemy.type] || ['#888','#666'];

    // Draw body
    for(let col = startCol; col <= endCol; col++) {
        if(zBuffer[col] <= dist) continue;
        
        const t = (col - startCol) / (endCol - startCol);
        const brightness = 1 - Math.abs(t - 0.5) * 0.4;
        
        // Head
        ctx.fillStyle = c1;
        const headSize = sw * 0.3;
        if(Math.abs(col - sx) < headSize/2) {
            ctx.fillRect(col, top, 1, sh*0.2);
        }
        
        // Body
        ctx.fillStyle = shade > 0 ? c1 : '#333';
        const bodyW = sw * 0.4;
        if(Math.abs(col - sx) < bodyW/2) {
            ctx.fillRect(col, top + sh*0.2, 1, sh*0.5);
        }
        
        // Legs
        ctx.fillStyle = c2;
        const legW = sw * 0.35;
        if(Math.abs(col - sx) < legW/2) {
            ctx.fillRect(col, top + sh*0.7, 1, sh*0.3);
        }
    }

    // Eyes (glowing red)
    const eyeY = top + sh * 0.08;
    const eyeSize = Math.max(2, sw * 0.06);
    ctx.fillStyle = `rgba(255,${Math.floor(shade*100)},0,${shade})`;
    ctx.fillRect(sx - sw*0.08, eyeY, eyeSize, eyeSize);
    ctx.fillRect(sx + sw*0.04, eyeY, eyeSize, eyeSize);

    // Health bar above enemy
    if(enemy.health < enemy.maxHealth) {
        const hbW = sw * 0.6;
        const hbH = 3;
        const hbX = sx - hbW/2;
        const hbY = top - 8;
        ctx.fillStyle = '#300';
        ctx.fillRect(hbX, hbY, hbW, hbH);
        ctx.fillStyle = '#f00';
        ctx.fillRect(hbX, hbY, hbW * (enemy.health/enemy.maxHealth), hbH);
    }

    // Boss crown/aura
    if(enemy.type === 'boss') {
        ctx.strokeStyle = `rgba(255,100,0,${shade*0.5})`;
        ctx.lineWidth = 2;
        ctx.beginPath();
        ctx.arc(sx, top + sh*0.35, sw*0.6, 0, Math.PI*2);
        ctx.stroke();
    }
}

function drawPickup(sx, sh, pickup, dist) {
    const shade = Math.max(0.3, 1 - dist/MAX_DEPTH);
    const size = Math.max(4, sh * 0.25);
    const bobY = Math.sin(performance.now()/300 + sx) * size * 0.3;
    const py = H/2 + sh*0.15 + bobY;

    // Check z-buffer
    if(zBuffer[Math.floor(sx)] <= dist) return;

    const colors = {
        health: '#f44', ammo_shotgun: '#fa0', ammo_plasma: '#4af',
        shotgun: '#ff0', plasma: '#f0f', armor: '#48f'
    };
    
    // Glow
    ctx.fillStyle = `rgba(${colors[pickup.type]==='#f44'?'255,68,68':colors[pickup.type]==='#fa0'?'255,170,0':'100,100,255'},${shade*0.3})`;
    ctx.beginPath();
    ctx.arc(sx, py, size*1.5, 0, Math.PI*2);
    ctx.fill();

    // Icon
    ctx.fillStyle = colors[pickup.type] || '#fff';
    if(pickup.type === 'health') {
        // Cross
        ctx.fillRect(sx - size/6, py - size/2, size/3, size);
        ctx.fillRect(sx - size/2, py - size/6, size, size/3);
    } else {
        // Diamond shape for items
        ctx.beginPath();
        ctx.moveTo(sx, py - size/2);
        ctx.lineTo(sx + size/2, py);
        ctx.lineTo(sx, py + size/2);
        ctx.lineTo(sx - size/2, py);
        ctx.closePath();
        ctx.fill();
    }
}

function renderWeapon() {
    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 - 160 + bobY;

    ctx.save();
    ctx.translate(wx, wy);

    if(w.name === 'PISTOL') {
        // Pistol body
        ctx.fillStyle = '#555';
        ctx.fillRect(20, 40, 30, 80);
        ctx.fillStyle = '#444';
        ctx.fillRect(25, 60, 20, 60);
        // Barrel
        ctx.fillStyle = '#333';
        ctx.fillRect(28, 10, 14, 40);
        // Grip
        ctx.fillStyle = '#654321';
        ctx.fillRect(22, 90, 26, 40);
    } else if(w.name === 'SHOTGUN') {
        // Shotgun body - wider
        ctx.fillStyle = '#5a4a3a';
        ctx.fillRect(10, 50, 50, 70);
        ctx.fillStyle = '#6b5a4a';
        ctx.fillRect(15, 20, 40, 40);
        // Barrel
        ctx.fillStyle = '#333';
        ctx.fillRect(20, -10, 12, 40);
        ctx.fillRect(38, -10, 12, 40);
        // Pump
        ctx.fillStyle = '#654321';
        ctx.fillRect(18, 30, 44, 15);
    } else {
        // Plasma gun - futuristic
        ctx.fillStyle = '#4a4a6a';
        ctx.fillRect(15, 30, 40, 90);
        ctx.fillStyle = '#5a5a8a';
        ctx.fillRect(20, 10, 30, 30);
        // Barrel glow
        const glow = Math.sin(performance.now()/200)*0.3+0.7;
        ctx.fillStyle = `rgba(0,${Math.floor(150*glow)},255,${glow})`;
        ctx.fillRect(25, -5, 20, 20);
        // Energy core
        ctx.fillStyle = `rgba(100,0,255,${glow*0.8})`;
        ctx.beginPath();
        ctx.arc(35, 60, 12, 0, Math.PI*2);
        ctx.fill();
    }

    // Muzzle flash
    if(muzzleFlash > 0) {
        const flashSize = muzzleFlash * 40;
        ctx.fillStyle = `rgba(255,${Math.floor(200*muzzleFlash)},0,${muzzleFlash})`;
        ctx.beginPath();
        ctx.arc(35, -10, flashSize, 0, Math.PI*2);
        ctx.fill();
    }

    ctx.restore();
}

function renderMinimap() {
    const scale = 10;
    miniCtx.fillStyle = 'rgba(0,0,0,0.8)';
    miniCtx.fillRect(0, 0, 160, 160);

    const offsetX = player.x * scale - 80;
    const offsetY = player.y * scale - 80;

    for(let y = 0; y < map.length; y++) {
        for(let x = 0; x < map[0].length; x++) {
            if(map[y][x] === 0) continue;
            const colors = {1:'#633',2:'#445',3:'#654',4:'#444',5:'#aa4',9:'#0a0'};
            miniCtx.fillStyle = colors[map[y][x]] || '#888';
            miniCtx.fillRect(x*scale - offsetX, y*scale - offsetY, scale-1, scale-1);
        }
    }

    // Enemies on minimap
    for(let e of enemies) {
        if(e.health <= 0) continue;
        miniCtx.fillStyle = '#f00';
        miniCtx.fillRect(e.x*scale - offsetX - 2, e.y*scale - offsetY - 2, 4, 4);
    }

    // Pickups on minimap
    for(let p of pickups) {
        if(!p.active) continue;
        miniCtx.fillStyle = '#ff0';
        miniCtx.fillRect(p.x*scale - offsetX - 1, p.y*scale - offsetY - 1, 3, 3);
    }

    // Player on minimap
    miniCtx.fillStyle = '#0f0';
    miniCtx.beginPath();
    miniCtx.arc(player.x*scale - offsetX, player.y*scale - offsetY, 3, 0, Math.PI*2);
    miniCtx.fill();

    // Direction indicator
    miniCtx.strokeStyle = '#0f0';
    miniCtx.lineWidth = 1;
    miniCtx.beginPath();
    miniCtx.moveTo(player.x*scale - offsetX, player.y*scale - offsetY);
    miniCtx.lineTo(
        (player.x + Math.cos(player.angle)*2)*scale - offsetX,
        (player.y + Math.sin(player.angle)*2)*scale - offsetY
    );
    miniCtx.stroke();
}

// Handle death/respawn key press during dead state
document.addEventListener('keydown', e => {
    if(gameState === 'dead' && e.key.toLowerCase() === 'r') respawn();
});

// Mouse state for auto-fire
let mouseDown = false;
document.addEventListener('mousedown', () => { mouseDown = true; });
document.addEventListener('mouseup', () => { mouseDown = false; });

// Re-request pointer lock on click
canvas?.addEventListener('click', () => {
    if(gameState === 'playing' && document.pointerLockElement !== canvas) {
        canvas.requestPointerLock();
    }
});

init();
</script>
</body>
</html>
<!-- agent-meta {"model":"thinkingcap-qwen3.6-27b","provider":"lmstudio","persona":"full","sessionId":"60ee8d8c-e938-43ab-842b-84695cdc033d","tokensIn":304492,"tokensOut":15445,"tokensTotal":319937,"turns":16,"toolCalls":15,"failedToolCalls":0,"timestamp":"2026-07-18T20:02:01.987Z"} -->