DOOM
thinkingcap qwen3.6 27b · full

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