← DOOM results

DOOM

muse spark 1.2 · 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 40,201 bytes · SHA-256 67f1570acf6e
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1">
<title>IRON CORRIDOR - Retro FPS</title>
<link href="https://fonts.googleapis.com/css2?family=VT323&family=Press+Start+2P&display=swap" rel="stylesheet">
<style>
*{margin:0;padding:0;box-sizing:border-box}
:root{--amber:#ffae00;--red:#ff1a1a;--green:#1aff66;--bg:#0a0a0a}
body{background:#000;color:#e8e8e8;font-family:'VT323',monospace;overflow:hidden;height:100vh;display:flex;flex-direction:column;user-select:none}
.topbar{height:32px;background:#1a1a1a;border-bottom:2px solid #333;display:flex;align-items:center;justify-content:space-between;padding:0 12px;font-size:14px;letter-spacing:1px}
.topbar b{color:var(--amber)}
.topbar span{opacity:.7}
.wrap{flex:1;display:flex;justify-content:center;align-items:center;background:radial-gradient(ellipse at center,#1e1e1e 0%,#000 80%);padding:8px;gap:12px}
.game-frame{position:relative;background:#000;border:3px solid #444;box-shadow:0 0 0 4px #111,0 0 40px rgba(255,174,0,.15);width:min(860px,96vw);display:flex;flex-direction:column}
#screen{width:100%;height:420px;display:block;image-rendering:pixelated;background:#2a2d34;cursor:crosshair}
@media(max-width:700px){#screen{height:52vw}}
.crt::after{content:"";position:absolute;inset:0;pointer-events:none;background:repeating-linear-gradient(0deg,rgba(0,0,0,.22) 0 2px,transparent 2px 4px),radial-gradient(ellipse at center,transparent 60%,rgba(0,0,0,.55) 100%);mix-blend-mode:multiply}
.scanline{position:absolute;inset:0;pointer-events:none;background:linear-gradient(transparent 50%,rgba(255,255,255,.02) 50%);background-size:100% 4px;opacity:.5}
.hud{height:96px;background:linear-gradient(#2b2b2b,#1a1a1a);border-top:3px solid var(--amber);display:grid;grid-template-columns:120px 1fr 140px 160px;gap:0;position:relative}
.hud>div{border-right:2px solid #333;padding:6px 8px;display:flex;flex-direction:column;justify-content:center}
.hud>div:last-child{border:0}
.hlabel{font-size:11px;letter-spacing:2px;color:#888;font-family:'Press Start 2P',monospace}
.hval{font-size:28px;line-height:1;color:var(--amber);text-shadow:0 0 8px rgba(255,174,0,.6)}
.hval.red{color:var(--red);text-shadow:0 0 8px rgba(255,26,26,.6)}
.hval.green{color:var(--green)}
.weapons{display:flex;gap:4px;margin-top:4px}
.wbtn{flex:1;border:2px solid #555;background:#111;color:#999;font-family:'Press Start 2P',monospace;font-size:8px;padding:6px 0;cursor:pointer}
.wbtn.active{border-color:var(--amber);color:var(--amber);background:#332600;box-shadow:0 0 8px rgba(255,174,0,.4)}
.wbtn small{display:block;font-family:'VT323',monospace;font-size:12px;opacity:.8}
.bars{margin-top:4px}
.bar{height:8px;background:#111;border:1px solid #333;position:relative;overflow:hidden;margin-top:2px}
.bar-fill{height:100%;transition:width .2s}
.bar-fill.health{background:linear-gradient(90deg,#ff1a1a,#ff6a00)}
.bar-fill.armor{background:linear-gradient(90deg,#1a8cff,#00ffd0)}
.minimap-wrap{display:flex;align-items:center;gap:6px}
#minimap{width:84px;height:84px;background:#000;border:2px solid #333;image-rendering:pixelated}
.right-panel{position:absolute;right:8px;top:8px;display:flex;flex-direction:column;gap:6px;z-index:5}
.pill{background:rgba(0,0,0,.7);border:1px solid var(--amber);color:var(--amber);font-size:11px;padding:4px 8px;letter-spacing:1px;display:flex;align-items:center;gap:6px}
.pill i{width:8px;height:8px;background:var(--amber);display:inline-block;animation:blink 1s steps(1) infinite}
@keyframes blink{50%{opacity:0}}
.overlay{position:absolute;inset:0;background:rgba(0,0,0,.78);display:flex;align-items:center;justify-content:center;z-index:10;backdrop-filter:blur(2px)}
.card{background:#111;border:3px solid var(--amber);box-shadow:0 0 30px rgba(255,174,0,.3),inset 0 0 20px rgba(255,174,0,.08);padding:22px;width:min(520px,92%);text-align:center}
.card h1{font-family:'Press Start 2P',monospace;font-size:20px;color:var(--amber);letter-spacing:2px;line-height:1.4}
.card h2{font-family:'Press Start 2P',monospace;font-size:14px;color:#fff;margin:8px 0}
.card p{opacity:.8;line-height:1.4;margin:10px 0;font-size:17px}
.btn{background:var(--amber);color:#000;border:0;font-family:'Press Start 2P',monospace;font-size:12px;padding:12px 18px;cursor:pointer;letter-spacing:1px;box-shadow:0 4px 0 #a66f00}
.btn:active{transform:translateY(2px);box-shadow:0 2px 0 #a66f00}
.btn.secondary{background:#333;color:#fff;box-shadow:0 4px 0 #111}
.kbd{display:inline-block;border:1px solid #555;background:#222;color:var(--amber);padding:1px 6px;font-size:13px;margin:0 1px}
.controls{width:220px;background:#0f0f0f;border:2px solid #333;padding:10px;display:flex;flex-direction:column;gap:8px;height:518px}
.controls h3{font-family:'Press Start 2P',monospace;font-size:10px;color:var(--amber);letter-spacing:1px;border-bottom:1px solid #333;padding-bottom:6px}
.ctrl-row{display:flex;justify-content:space-between;font-size:15px;opacity:.85}
.ctrl-row b{color:#fff}
.legend{display:flex;align-items:center;gap:6px;font-size:14px;margin-top:2px}
.dot{width:10px;height:10px;border:1px solid #000}
.toast{position:absolute;left:50%;top:18%;transform:translateX(-50%);background:var(--amber);color:#000;font-family:'Press Start 2P',monospace;font-size:10px;padding:6px 10px;z-index:6;display:none}
.damage-flash{position:absolute;inset:0;background:radial-gradient(circle,rgba(255,0,0,.45) 0%,transparent 70%);pointer-events:none;opacity:0;transition:opacity .15s;z-index:7}
#weaponSprite{position:absolute;left:50%;bottom:68px;transform:translateX(-50%);pointer-events:none;z-index:4;filter:drop-shadow(0 8px 12px rgba(0,0,0,.8))}
.hidden{display:none !important}
.mobile{display:none}
@media(max-width:860px){.controls{display:none}.hud{grid-template-columns:1fr 1fr 1fr 1fr;height:88px}.hval{font-size:22px}.hlabel{font-size:9px}.mobile{display:flex;gap:6px;position:absolute;bottom:98px;left:50%;transform:translateX(-50%);z-index:6}.mbtn{width:54px;height:42px;background:rgba(0,0,0,.6);border:1px solid #555;color:#fff;font-size:18px;display:grid;place-items:center}}
</style>
</head>
<body>
<div class="topbar"><div><b>◢ IRON CORRIDOR ◣</b> &nbsp; SECTOR 7 - DECOMMISSIONED</div><div><span>DIFFICULTY:</span> <b style="color:var(--red)">NIGHTMARE</b> <span style="margin-left:12px">⏺ REC</span></div></div>
<div class="wrap">
<div class="game-frame" id="frame">
<canvas id="screen" width="640" height="400"></canvas>
<div class="crt scanline"></div>
<div class="damage-flash" id="dmg"></div>
<div class="toast" id="toast">PICKED UP SHOTGUN</div>
<div class="right-panel">
<div class="pill"><i></i> <span id="killCount">0 / 9 HOSTILES</span></div>
<div class="pill" style="border-color:var(--green);color:var(--green)">◉ EXIT LOCKED</div>
</div>
<!-- weapon sprite SVG -->
<svg id="weaponSprite" width="260" height="140" viewBox="0 0 260 140"></svg>

<div class="mobile">
<button class="mbtn" id="mLeft">↺</button>
<button class="mbtn" id="mFwd">▲</button>
<button class="mbtn" id="mBack">▼</button>
<button class="mbtn" id="mRight">↻</button>
<button class="mbtn" id="mShoot" style="background:var(--amber);color:#000">⦿</button>
</div>

<div class="hud">
<div>
<div class="hlabel">HEALTH</div>
<div class="hval" id="hpVal">100</div>
<div class="bar"><div class="bar-fill health" id="hpBar" style="width:100%"></div></div>
</div>
<div>
<div class="hlabel">ARMOR & AMMO</div>
<div style="display:flex;gap:10px;align-items:baseline">
<div class="hval green" id="armorVal">0</div>
<span style="opacity:.5">|</span>
<div class="hval" id="ammoVal">50</div>
<span style="font-size:12px;opacity:.6" id="ammoName">9MM</span>
</div>
<div class="bar"><div class="bar-fill armor" id="armorBar" style="width:0%"></div></div>
</div>
<div>
<div class="hlabel">ARSENAL</div>
<div class="weapons">
<button class="wbtn active" data-w="0">1<small>PISTOL</small></button>
<button class="wbtn" data-w="1">2<small>SHOTGN</small></button>
<button class="wbtn" data-w="2">3<small>RIPPER</small></button>
</div>
</div>
<div>
<div class="hlabel">STATUS</div>
<div style="font-size:13px;line-height:1.2;margin-top:2px">
<div>FACE: <span id="face">◕‿◕</span></div>
<div>KILLS: <b id="kills">0</b> &nbsp;KEY: <b id="hasKey" style="color:#888">NO</b></div>
<div style="opacity:.6;font-size:11px;letter-spacing:1px" id="fps">60 FPS • 640×400</div>
</div>
</div>
</div>

<!-- START -->
<div class="overlay" id="startOverlay">
<div class="card">
<h1>IRON CORRIDOR</h1>
<p style="color:var(--amber);letter-spacing:3px;font-size:12px;font-family:'Press Start 2P',monospace">A RETRO FPS</p>
<p>UAC Sector 7 has gone dark. 9 hostiles remain. Find the <b style="color:var(--amber)">YELLOW KEYCARD</b> and reach the exit gate.</p>
<div style="text-align:left;background:#000;border:1px solid #333;padding:10px;font-size:14px;line-height:1.5;margin:12px 0">
<div><span class="kbd">WASD</span> Move &nbsp; <span class="kbd">◄ ►</span> or <span class="kbd">MOUSE DRAG</span> Look</div>
<div><span class="kbd">CLICK</span> / <span class="kbd">SPACE</span> Shoot &nbsp; <span class="kbd">1 2 3</span> Weapons</div>
<div><span class="kbd">SHIFT</span> Sprint</div>
</div>
<div style="display:flex;gap:8px;justify-content:center">
<button class="btn" id="playBtn">▶ INSERT COIN - PLAY</button>
</div>
<p style="font-size:12px;opacity:.5">Legal distinct homage • No demons were harmed • CRT recommended</p>
</div>
</div>

<!-- DEAD -->
<div class="overlay hidden" id="deadOverlay">
<div class="card" style="border-color:var(--red)">
<h1 style="color:var(--red)">YOU DIED</h1>
<p>Your corpse will be repurposed as corridor decoration.</p>
<p style="font-family:'Press Start 2P',monospace;font-size:10px;color:var(--red)" id="deadStats">KILLS 0/9</p>
<div style="display:flex;gap:8px;justify-content:center;margin-top:10px">
<button class="btn secondary" id="respawnBtn">↻ RESPAWN [R]</button>
<button class="btn" id="restartBtn">RESTART MISSION</button>
</div>
</div>
</div>

<!-- WIN -->
<div class="overlay hidden" id="winOverlay">
<div class="card" style="border-color:var(--green)">
<h1 style="color:var(--green)">SECTOR CLEAR</h1>
<h2>MISSION COMPLETE</h2>
<p id="winStats">All hostiles neutralized in 00:00</p>
<div style="font-size:40px">★ ★ ★</div>
<button class="btn" id="winRestart" style="background:var(--green)">PLAY AGAIN</button>
</div>
</div>
</div>

<div class="controls">
<h3>MINIMAP & INTEL</h3>
<canvas id="minimap" width="120" height="120"></canvas>
<div style="font-size:12px;opacity:.7;line-height:1.3">Yellow square = EXIT (locked until key). Compass shows facing. Red = hostiles, Green = health, Amber = ammo, Cyan = keycard.</div>
<h3 style="margin-top:6px">WEAPONS</h3>
<div class="ctrl-row"><b>1 PISTOL</b><span>20 DMG • 400ms</span></div>
<div class="legend"><span class="dot" style="background:#ffae00"></span> Infinite-ish • Accurate • Starter</div>
<div class="ctrl-row"><b>2 SHOTGUN</b><span>8×12 DMG</span></div>
<div class="legend"><span class="dot" style="background:#ff3b3b"></span> Close range brawler • 750ms</div>
<div class="ctrl-row"><b>3 RIPPER</b><span>14 DMG • 110ms</span></div>
<div class="legend"><span class="dot" style="background:#1aff66"></span> Bullet hose • Hold fire</div>
<h3 style="margin-top:6px">PICKUPS</h3>
<div class="legend"><span class="dot" style="background:#1aff66"></span> MEDKIT +25 HP</div>
<div class="legend"><span class="dot" style="background:#1a8cff"></span> ARMOR +25</div>
<div class="legend"><span class="dot" style="background:#ffae00"></span> AMMO BOX +20/+12/+50</div>
<div style="margin-top:auto;background:#1a1a1a;border:1px solid #333;padding:8px;font-size:13px;line-height:1.4">
<b style="color:var(--amber)">OBJECTIVE:</b> Kill or bypass hostiles, grab the cyan keycard in the north-east lab, then sprint to the yellow exit in the south-east corner.
</div>
</div>
</div>

<script>
const canvas=document.getElementById('screen'), ctx=canvas.getContext('2d');
const mini=document.getElementById('minimap'), mctx=mini.getContext('2d');
const hpVal=document.getElementById('hpVal'), hpBar=document.getElementById('hpBar'), armorVal=document.getElementById('armorVal'), armorBar=document.getElementById('armorBar');
const ammoVal=document.getElementById('ammoVal'), ammoName=document.getElementById('ammoName'), toast=document.getElementById('toast'), dmgFlash=document.getElementById('dmg');
const killCountEl=document.getElementById('killCount'), killsEl=document.getElementById('kills'), hasKeyEl=document.getElementById('hasKey'), faceEl=document.getElementById('face');
const weaponSvg=document.getElementById('weaponSprite');

// map: 1 wall, 0 floor, 9 exit door, 2 wall variant
const MAP = [
[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,1,0,0,0,0,0,1],
[1,0,0,0,0,0,0,0,0,1,0,0,0,0,0,1],
[1,0,0,1,1,1,0,0,0,1,1,1,0,1,0,1],
[1,0,0,1,0,0,0,0,0,0,0,0,0,1,0,1],
[1,0,0,1,0,0,0,1,1,1,0,0,0,1,0,1],
[1,0,0,0,0,0,0,1,0,0,0,1,0,0,0,1],
[1,0,0,0,0,0,0,1,0,0,0,1,1,1,0,1],
[1,1,1,1,0,1,0,0,0,1,0,0,0,0,0,1],
[1,0,0,0,0,1,0,0,0,1,0,1,1,1,0,1],
[1,0,0,0,0,1,0,0,0,0,0,0,0,0,0,1],
[1,0,1,1,0,1,1,1,1,0,1,1,0,1,0,1],
[1,0,0,0,0,0,0,0,0,0,0,1,0,0,9,1],
[1,0,1,1,1,0,1,1,1,1,0,1,0,1,0,1],
[1,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],
];
const MAP_W=16, MAP_H=16;
const EXIT_X=14.5, EXIT_Y=12.5;

let player={x:2.5,y:2.5, ang:0, hp:100, armor:0, ammo:[60,18,90], weapon:0, hasKey:false, kills:0, alive:true, shootCooldown:0, bob:0};
let startTime=performance.now();
const weapons=[
{name:'PISTOL', color:'#ffae00', dmg:22, cd:380, ammoIdx:0, short:'9MM'},
{name:'SHOTGUN', color:'#ff3b3b', dmg:11, cd:720, ammoIdx:1, short:'SHELLS', pellets:8},
{name:'RIPPER', color:'#1aff66', dmg:14, cd:110, ammoIdx:2, short:'CELLS'}
];

let enemies=[], pickups=[], particles=[];
let keys={}, mouseDown=false, mouseDrag=0, isPointerLocked=false;
let lastTime=0, animFrame, muzzle=0, screenShake=0;

function resetGame(){
 player={x:2.5,y:2.5, ang:0, hp:100, armor:0, ammo:[60,18,90], weapon:0, hasKey:false, kills:0, alive:true, shootCooldown:0, bob:0};
 enemies=[
  {x:5.5,y:3.5,hp:60, max:60, alive:true, cd:0, type:0},
  {x:9.5,y:4.5,hp:60, max:60, alive:true, cd:0, type:0},
  {x:4.5,y:8.5,hp:90, max:90, alive:true, cd:0, type:1},
  {x:10.5,y:7.5,hp:60, max:60, alive:true, cd:0, type:0},
  {x:13.5,y:2.5,hp:60, max:60, alive:true, cd:0, type:0},
  {x:7.5,y:10.5,hp:90, max:90, alive:true, cd:0, type:1},
  {x:13.5,y:10.5,hp:60, max:60, alive:true, cd:0, type:0},
  {x:3.5,y:13.5,hp:120, max:120, alive:true, cd:0, type:2}, // bruiser
  {x:8.5,y:13.5,hp:60, max:60, alive:true, cd:0, type:0},
 ];
 pickups=[
  {x:3.5,y:3.5,type:'health',taken:false},
  {x:6.5,y:6.5,type:'armor',taken:false},
  {x:12.5,y:6.5,type:'ammo',taken:false},
  {x:2.5,y:10.5,type:'health',taken:false},
  {x:11.5,y:9.5,type:'ammo',taken:false},
  {x:6.5,y:13.5,type:'health',taken:false},
  {x:9.5,y:12.5,type:'armor',taken:false},
  {x:13.5,y:4.5,type:'key',taken:false},
  {x:5.5,y:11.5,type:'shotgun',taken:false},
  {x:7.5,y:14.5,type:'ripper',taken:false},
 ];
 startTime=performance.now();
 updateHUD();
 hideOverlays();
 spawnToast('SECTOR 7 BREACH - GO!');
}
function hideOverlays(){ document.getElementById('startOverlay').classList.add('hidden'); document.getElementById('deadOverlay').classList.add('hidden'); document.getElementById('winOverlay').classList.add('hidden'); }

function isWall(x,y){
 let mx=Math.floor(x), my=Math.floor(y);
 if(mx<0||my<0||mx>=MAP_W||my>=MAP_H) return true;
 let v=MAP[my][mx];
 if(v===9) return !player.hasKey; // exit locked
 return v===1||v===2;
}
function hasLOS(x0,y0,x1,y1){
 let dx=x1-x0, dy=y1-y0, dist=Math.hypot(dx,dy), steps=Math.ceil(dist*10);
 for(let i=1;i<steps;i++){ let t=i/steps; if(isWall(x0+dx*t, y0+dy*t)) return false; }
 return true;
}

function spawnToast(t){
 toast.textContent=t; toast.style.display='block';
 clearTimeout(toast._t); toast._t=setTimeout(()=>toast.style.display='none',1800);
}
function hurtPlayer(dmg){
 if(!player.alive) return;
 let absorbed=Math.min(player.armor, dmg*0.5);
 player.armor-=absorbed; dmg-=absorbed;
 player.hp-=dmg;
 screenShake=6;
 dmgFlash.style.opacity='0.9'; setTimeout(()=>dmgFlash.style.opacity='0',120);
 if(player.hp<=0){ player.hp=0; player.alive=false; faceEl.textContent='✖‿✖'; document.getElementById('deadStats').textContent=`KILLS ${player.kills} / ${enemies.length} • AMMO ${player.ammo.join('/')}`; document.getElementById('deadOverlay').classList.remove('hidden'); playSound('die'); }
 updateHUD();
}
function heal(n){ player.hp=Math.min(100, player.hp+n); spawnToast(`+${n} HEALTH`); updateHUD(); playSound('pickup'); }
function addArmor(n){ player.armor=Math.min(100, player.armor+n); spawnToast(`+${n} ARMOR`); updateHUD(); playSound('pickup'); }
function addAmmo(a0,a1,a2){
 player.ammo[0]+=a0; player.ammo[1]+=a1; player.ammo[2]+=a2; spawnToast(`AMMO +${a0}/${a1}/${a2}`); updateHUD(); playSound('pickup');
}

function tryShoot(){
 if(!player.alive || player.shootCooldown>0) return;
 const w=weapons[player.weapon];
 if(w.ammoIdx>=0 && player.ammo[w.ammoIdx]<=0 && player.weapon!==0){ spawnToast('OUT OF AMMO!'); playSound('empty'); player.shootCooldown=200; return; }
 // consume
 if(w.ammoIdx>=0){
   if(player.weapon===1) player.ammo[w.ammoIdx]-=1;
   else if(player.weapon===2) player.ammo[w.ammoIdx]-=2;
   else player.ammo[w.ammoIdx]-=1;
   if(player.ammo[w.ammoIdx]<0) player.ammo[w.ammoIdx]=0;
 }
 player.shootCooldown=w.cd;
 muzzle= w.name==='SHOTGUN'? 9 : 6;
 screenShake+= w.name==='SHOTGUN'?5:2;
 playSound('shoot', w.name);
 // hitscan: find enemies in cone
 let hitSomething=false;
 let pellets = w.pellets||1;
 for(let p=0;p<pellets;p++){
  let spread = w.name==='SHOTGUN'? (Math.random()-0.5)*0.18 : w.name==='RIPPER'?(Math.random()-0.5)*0.08 : (Math.random()-0.5)*0.03;
  let ang = player.ang + spread;
  // find closest enemy along ray within tolerance
  let best=null, bestDist=Infinity;
  for(let e of enemies){ if(!e.alive) continue;
    let dx=e.x-player.x, dy=e.y-player.y, dist=Math.hypot(dx,dy);
    if(dist>9) continue;
    let aTo=Math.atan2(dy,dx), diff=Math.atan2(Math.sin(aTo-ang), Math.cos(aTo-ang));
    if(Math.abs(diff) < 0.28 && hasLOS(player.x,player.y,e.x,e.y)){
      if(dist<bestDist){ best=e; bestDist=dist; }
    }
  }
  if(best){
    let dmg = w.dmg + (Math.random()*6-3);
    if(w.name==='SHOTGUN' && bestDist>4) dmg*=0.6;
    best.hp-=dmg;
    hitSomething=true;
    // impulse
    best.x += Math.cos(ang)*0.12; best.y += Math.sin(ang)*0.12;
    if(best.x<1) best.x=1; if(best.y<1) best.y=1;
    if(isWall(best.x,best.y)){ best.x-=Math.cos(ang)*0.12; best.y-=Math.sin(ang)*0.12; }
    if(best.hp<=0){ best.alive=false; player.kills++; spawnToast('HOSTILE DOWN'); playSound('hit'); // particles
      for(let i=0;i<10;i++) particles.push({x:best.x,y:best.y,vx:(Math.random()-0.5)*0.15,vy:(Math.random()-0.5)*0.15,life:400,cl:'#ff3b3b'});
    } else {
      playSound('hit');
      for(let i=0;i<3;i++) particles.push({x:best.x,y:best.y,vx:(Math.random()-0.5)*0.08,vy:(Math.random()-0.5)*0.08,life:180,cl:'#ffae00'});
    }
  }
 }
 // wall puff if missed?
 if(!hitSomething){
  // check wall hit for feedback
 }
 updateHUD();
 // update kill counter pill
 document.querySelector('.right-panel .pill:last-child').innerHTML = player.hasKey ? '◉ EXIT OPEN — GO!' : '◉ EXIT LOCKED';
 document.querySelector('.right-panel .pill:last-child').style.borderColor = player.hasKey ? 'var(--green)' : 'var(--amber)';
 document.querySelector('.right-panel .pill:last-child').style.color = player.hasKey ? 'var(--green)' : 'var(--amber)';
 if(player.kills>=enemies.length && player.hasKey) { /* win handled on reaching exit */ }
}

function updateHUD(){
 hpVal.textContent=Math.ceil(player.hp); hpBar.style.width=player.hp+'%';
 hpVal.className='hval'+(player.hp<30?' red':'');
 faceEl.textContent = !player.alive ? '✖‿✖' : player.hp<30?'◎_◎' : player.hp<70?'◕‿◕':'◔‿◔';
 armorVal.textContent=Math.ceil(player.armor); armorBar.style.width=player.armor+'%';
 let w=weapons[player.weapon];
 ammoVal.textContent = player.ammo[w.ammoIdx];
 ammoVal.style.color=w.color;
 ammoName.textContent=w.short;
 document.querySelectorAll('.wbtn').forEach(b=>b.classList.toggle('active', parseInt(b.dataset.w)===player.weapon));
 killsEl.textContent=player.kills; hasKeyEl.textContent=player.hasKey?'YES':'NO'; hasKeyEl.style.color=player.hasKey?'var(--green)':'#888';
 killCountEl.textContent=`${player.kills} / ${enemies.length} HOSTILES`;
 if(player.weapon===0 && player.ammo[0]<=0) ammoVal.style.opacity=.5; else ammoVal.style.opacity=1;
}

// ---- INPUT ----
window.addEventListener('keydown',e=>{
 keys[e.code]=true;
 if(e.code==='Digit1') {player.weapon=0; updateHUD(); playSound('switch')}
 if(e.code==='Digit2') {player.weapon=1; updateHUD(); playSound('switch')}
 if(e.code==='Digit3') {player.weapon=2; updateHUD(); playSound('switch')}
 if(e.code==='Space') {tryShoot(); e.preventDefault();}
 if(e.code==='KeyR' && !player.alive){ respawn(); }
});
window.addEventListener('keyup',e=>keys[e.code]=false);
canvas.addEventListener('mousedown',e=>{
 if(!player.alive) return;
 if(document.getElementById('startOverlay').classList.contains('hidden')){
   mouseDown=true; tryShoot();
   // pointer lock on click
   canvas.requestPointerLock?.();
 }});
window.addEventListener('mouseup',()=>mouseDown=false);
canvas.addEventListener('click',()=>{ if(!isPointerLocked && document.getElementById('startOverlay').classList.contains('hidden')) canvas.requestPointerLock?.(); });
document.addEventListener('pointerlockchange',()=>isPointerLocked=document.pointerLockElement===canvas);
canvas.addEventListener('mousemove',e=>{
 if(isPointerLocked){ player.ang += e.movementX*0.003; }
 else if(mouseDown){ player.ang += e.movementX*0.005; }
});
let touchStartX=0;
canvas.addEventListener('touchstart',e=>{ touchStartX=e.touches[0].clientX; });
canvas.addEventListener('touchmove',e=>{
 let dx=e.touches[0].clientX-touchStartX; player.ang+=dx*0.008; touchStartX=e.touches[0].clientX; e.preventDefault();
},{passive:false});
canvas.addEventListener('touchend',()=>{tryShoot()});

// buttons
document.querySelectorAll('.wbtn').forEach(b=>b.addEventListener('click',()=>{player.weapon=parseInt(b.dataset.w); updateHUD();}));
document.getElementById('playBtn').addEventListener('click',()=>{ resetGame(); hideOverlays(); canvas.focus(); });
document.getElementById('respawnBtn').addEventListener('click',respawn);
document.getElementById('restartBtn').addEventListener('click',()=>resetGame());
document.getElementById('winRestart').addEventListener('click',()=>resetGame());
document.getElementById('mLeft').addEventListener('touchstart',e=>{keys['ArrowLeft']=true;e.preventDefault()});
document.getElementById('mLeft').addEventListener('touchend',()=>keys['ArrowLeft']=false);
document.getElementById('mRight').addEventListener('touchstart',e=>{keys['ArrowRight']=true;e.preventDefault()});
document.getElementById('mRight').addEventListener('touchend',()=>keys['ArrowRight']=false);
document.getElementById('mFwd').addEventListener('touchstart',e=>{keys['KeyW']=true;e.preventDefault()});
document.getElementById('mFwd').addEventListener('touchend',()=>keys['KeyW']=false);
document.getElementById('mBack').addEventListener('touchstart',e=>{keys['KeyS']=true;e.preventDefault()});
document.getElementById('mBack').addEventListener('touchend',()=>keys['KeyS']=false);
document.getElementById('mShoot').addEventListener('touchstart',e=>{tryShoot();e.preventDefault()});

function respawn(){
 player.hp=100; player.armor=Math.max(0,player.armor); player.x=2.5; player.y=2.5; player.alive=true; document.getElementById('deadOverlay').classList.add('hidden'); updateHUD(); spawnToast('RESPAWNED');
}

// ---- AUDIO (tiny synth) ----
let actx;
function playSound(type, w){
 try{
  actx=actx||new (window.AudioContext||window.webkitAudioContext)();
  let o=actx.createOscillator(), g=actx.createGain(); o.connect(g); g.connect(actx.destination);
  if(type==='shoot'){
   if(w==='SHOTGUN'){ o.frequency.value=180; o.type='square'; g.gain.value=0.2; o.frequency.exponentialRampToValueAtTime(40, actx.currentTime+0.15); g.gain.exponentialRampToValueAtTime(0.01, actx.currentTime+0.18); o.start(); o.stop(actx.currentTime+0.2); let n=actx.createBufferSource(); /* noise burst via second osc */
   } else if(w==='RIPPER'){ o.frequency.value=620; o.type='square'; g.gain.value=0.12; g.gain.exponentialRampToValueAtTime(0.01, actx.currentTime+0.08); o.start(); o.stop(actx.currentTime+0.09); }
   else { o.frequency.value=520; o.type='square'; g.gain.value=0.15; g.gain.exponentialRampToValueAtTime(0.01, actx.currentTime+0.12); o.start(); o.stop(actx.currentTime+0.13); }
  } else if(type==='hit'){ o.frequency.value=120; o.type='sawtooth'; g.gain.value=0.15; g.gain.exponentialRampToValueAtTime(0.01, actx.currentTime+0.12); o.start(); o.stop(actx.currentTime+0.13); }
  else if(type==='pickup'){ o.frequency.value=440; o.type='sine'; g.gain.value=0.2; o.frequency.linearRampToValueAtTime(880, actx.currentTime+0.12); g.gain.linearRampToValueAtTime(0, actx.currentTime+0.2); o.start(); o.stop(actx.currentTime+0.21); }
  else if(type==='die'){ o.frequency.value=200; o.type='sawtooth'; g.gain.value=0.3; o.frequency.linearRampToValueAtTime(30, actx.currentTime+0.6); g.gain.linearRampToValueAtTime(0, actx.currentTime+0.6); o.start(); o.stop(actx.currentTime+0.62); }
  else if(type==='switch'){ o.frequency.value=300; o.type='sine'; g.gain.value=0.1; o.frequency.linearRampToValueAtTime(600, actx.currentTime+0.08); g.gain.linearRampToValueAtTime(0, actx.currentTime+0.1); o.start(); o.stop(actx.currentTime+0.11); }
  else if(type==='empty'){ o.frequency.value=80; g.gain.value=0.2; o.start(); o.stop(actx.currentTime+0.08); }
 }catch(e){}
}

// ---- RENDER + GAME LOOP ----
const FOV=Math.PI/3;
function gameUpdate(dt){
 if(!player.alive) return;
 // movement
 let moveSpeed = (keys['ShiftLeft']||keys['ShiftRight']? 2.9 : 1.75) * dt;
 let rotSpeed = 1.9*dt;
 if(keys['ArrowLeft']||keys['KeyQ']) player.ang-=rotSpeed;
 if(keys['ArrowRight']||keys['KeyE']) player.ang+=rotSpeed;
 let forward=0, strafe=0;
 if(keys['KeyW']||keys['ArrowUp']) forward+=1;
 if(keys['KeyS']||keys['ArrowDown']) forward-=1;
 if(keys['KeyA']) strafe-=1;
 if(keys['KeyD']) strafe+=1;
 // normalize
 if(forward!==0||strafe!==0){
  let len=Math.hypot(forward,strafe); forward/=len; strafe/=len;
  let nx = player.x + Math.cos(player.ang)*forward*moveSpeed + Math.cos(player.ang+Math.PI/2)*strafe*moveSpeed;
  let ny = player.y + Math.sin(player.ang)*forward*moveSpeed + Math.sin(player.ang+Math.PI/2)*strafe*moveSpeed;
  if(!isWall(nx, player.y)) player.x=nx; if(!isWall(player.x, ny)) player.y=ny;
  player.bob+= dt* (keys['ShiftLeft']?14:9);
 } else player.bob+=dt*2;
 // cooldowns
 if(player.shootCooldown>0) player.shootCooldown-=dt*1000;
 if(mouseDown && player.shootCooldown<=0 && weapons[player.weapon].name==='RIPPER') tryShoot();
 if(muzzle>0) muzzle-=dt*30;
 if(screenShake>0) screenShake-=dt*18;

 // pickups
 for(let p of pickups){ if(p.taken) continue; if(Math.hypot(p.x-player.x,p.y-player.y)<0.7){
   p.taken=true;
   if(p.type==='health') heal(25);
   else if(p.type==='armor') addArmor(25);
   else if(p.type==='ammo') addAmmo(20,12,50);
   else if(p.type==='key'){ player.hasKey=true; spawnToast('YELLOW KEYCARD ACQUIRED — EXIT OPEN'); playSound('pickup'); hasKeyEl.textContent='YES'; hasKeyEl.style.color='var(--green)'; }
   else if(p.type==='shotgun'){ player.weapon=1; addAmmo(0,12,0); spawnToast('SHOTGUN ACQUIRED [2]'); updateHUD(); }
   else if(p.type==='ripper'){ player.weapon=2; addAmmo(0,0,60); spawnToast('RIPPER ACQUIRED [3]'); updateHUD(); }
 }}
 // win check: reach exit
 if(Math.hypot(player.x-EXIT_X, player.y-EXIT_Y)<0.8 && player.hasKey){
   let t=Math.floor((performance.now()-startTime)/1000); let mm=String(Math.floor(t/60)).padStart(2,'0'), ss=String(t%60).padStart(2,'0');
   document.getElementById('winStats').textContent=`Cleared in ${mm}:${ss} • Kills ${player.kills}/${enemies.length}`;
   document.getElementById('winOverlay').classList.remove('hidden'); playSound('pickup');
 }
 // enemies AI
 for(let e of enemies){ if(!e.alive) continue;
   let dx=player.x-e.x, dy=player.y-e.y, dist=Math.hypot(dx,dy);
   if(e.cd>0) e.cd-=dt*1000;
   if(dist<7 && hasLOS(e.x,e.y,player.x,player.y)){
     // move toward player if not too close
     if(dist>1.2){
       let step= (e.type===2?0.55:0.85)*dt;
       let nx=e.x + dx/dist*step, ny=e.y + dy/dist*step;
       if(!isWall(nx,e.y) && Math.hypot(nx-player.x, ny-player.y)>0.6) e.x=nx;
       if(!isWall(e.x,ny) && Math.hypot(nx-player.x, ny-player.y)>0.6) e.y=ny;
     }
     if(dist<5.5 && e.cd<=0){
       e.cd = e.type===2?900:1300;
       // enemy shoot
       let hitChance = dist<3?0.7:0.35;
       if(Math.random()<hitChance) hurtPlayer(e.type===2?18:10+Math.random()*8);
       else spawnToast('ENEMY MISSED');
     }
   }
 }
 // particles
 for(let i=particles.length-1;i>=0;i--){ let pt=particles[i]; pt.x+=pt.vx*dt*60; pt.y+=pt.vy*dt*60; pt.life-=dt*1000; if(pt.life<=0) particles.splice(i,1); }
}

function drawWorld(){
 const W=canvas.width, H=canvas.height;
 // shake
 ctx.save();
 if(screenShake>0){ ctx.translate((Math.random()-0.5)*screenShake, (Math.random()-0.5)*screenShake); }
 // ceiling / floor
 let grad=ctx.createLinearGradient(0,0,0,H);
 grad.addColorStop(0,'#1a1f2b'); grad.addColorStop(0.5,'#1e252e'); grad.addColorStop(0.5,'#2b2b22'); grad.addColorStop(1,'#3a3a2e');
 ctx.fillStyle=grad; ctx.fillRect(0,0,W,H);
 // sky tweak
 ctx.fillStyle='#0f141e'; ctx.fillRect(0,0,W,H*0.5);
 // horizon line
 ctx.fillStyle='#1a1a1a'; ctx.fillRect(0,H*0.5-1,W,2);

 let zBuffer=new Array(W);
 for(let x=0;x<W;x++){
  let rayAng = player.ang - FOV/2 + (x/W)*FOV;
  let rayDirX=Math.cos(rayAng), rayDirY=Math.sin(rayAng);
  let mapX=Math.floor(player.x), mapY=Math.floor(player.y);
  let deltaX=Math.abs(1/rayDirX), deltaY=Math.abs(1/rayDirY);
  let stepX, stepY, sideDistX, sideDistY;
  if(rayDirX<0){ stepX=-1; sideDistX=(player.x-mapX)*deltaX; } else { stepX=1; sideDistX=(mapX+1-player.x)*deltaX; }
  if(rayDirY<0){ stepY=-1; sideDistY=(player.y-mapY)*deltaY; } else { stepY=1; sideDistY=(mapY+1-player.y)*deltaY; }
  let hit=0, side=0, hitVal=1;
  let maxIter=40;
  while(hit===0 && maxIter-->0){
   if(sideDistX<sideDistY){ sideDistX+=deltaX; mapX+=stepX; side=0; } else { sideDistY+=deltaY; mapY+=stepY; side=1; }
   if(mapX<0||mapY<0||mapX>=MAP_W||mapY>=MAP_H){hit=1; break;}
   let v=MAP[mapY][mapX];
   if(v===1||v===9){ hit=1; hitVal=v; }
  }
  let perpDist;
  if(side===0) perpDist=(mapX - player.x + (1-stepX)/2)/rayDirX;
  else perpDist=(mapY - player.y + (1-stepY)/2)/rayDirY;
  perpDist = Math.max(0.15, perpDist * Math.cos(rayAng - player.ang)); // fix fish eye approx
  zBuffer[x]=perpDist;
  let lineH=Math.floor(H/perpDist);
  let y0=Math.floor(H/2 - lineH/2);
  let y1=y0+lineH;
  // shading
  let shade = Math.max(0.2, 1 - perpDist*0.13);
  if(side===1) shade*=0.78;
  let isExit = hitVal===9;
  // wall color
  let base = isExit ? {r:255,g:210,b:0} : side===1 ? {r:44,g:56,b:72} : {r:66,g:78,b:94};
  if(isExit){ base.r=255; base.g=195+Math.sin(performance.now()*0.005)*20; base.b=0; }
  // vertical texture stripes
  let stripe = (mapX+mapY)%2===0 ? 0.06:0;
  ctx.fillStyle=`rgba(${Math.floor(base.r*shade+stripe*60)},${Math.floor(base.g*shade+stripe*60)},${Math.floor(base.b*shade+stripe*60)},1)`;
  ctx.fillRect(x, y0, 1, lineH);
  // highlight edge
  if(x%2===0){ ctx.fillStyle=`rgba(255,255,255,${0.04*shade})`; ctx.fillRect(x, y0,1, lineH); }
  // floor shading (fake)
  let floorShade = 0.55;
  ctx.fillStyle=`rgba(22,22,18,${floorShade})`;
  // dark vignette handled later
 }
 // sprites
 let sprites=[];
 for(let e of enemies) if(e.alive) sprites.push({x:e.x,y:e.y, kind:'enemy', obj:e, dist:Math.hypot(e.x-player.x,e.y-player.y)});
 for(let p of pickups) if(!p.taken) sprites.push({x:p.x,y:p.y, kind:'pickup', obj:p, dist:Math.hypot(p.x-player.x,p.y-player.y)});
 for(let pt of particles) sprites.push({x:pt.x,y:pt.y,kind:'particle', obj:pt, dist:Math.hypot(pt.x-player.x,pt.y-player.y)});
 sprites.sort((a,b)=>b.dist-a.dist);
 for(let s of sprites){
  let dx=s.x-player.x, dy=s.y-player.y;
  let angTo=Math.atan2(dy,dx), diff=Math.atan2(Math.sin(angTo-player.ang), Math.cos(angTo-player.ang));
  if(Math.abs(diff)>FOV/2+0.2) continue;
  let perp = s.dist * Math.cos(diff);
  if(perp<0.2) continue;
  let sx = Math.floor((0.5 + diff/FOV)*W);
  let size = Math.floor(H/perp);
  let col = zBuffer[Math.max(0,Math.min(W-1,sx))];
  if(perp>col+0.15) continue;
  // draw billboard
  if(s.kind==='enemy'){
   let e=s.obj;
   let w=Math.floor(size*0.55), h=Math.floor(size*0.9);
   let x0=sx - w/2, y0=H/2 - h/2 + H/perp*0.08;
   // shadow
   ctx.fillStyle='rgba(0,0,0,0.4)'; ctx.fillRect(x0+4, y0+h-6, w-8, 6);
   // body
   let c = e.type===2 ? '#7a0f0f' : e.type===1 ? '#8a3b00' : '#3a4a3a';
   let healthPct=e.hp/e.max;
   // simple pixel enemy: rectangle + head
   ctx.fillStyle=c; ctx.fillRect(x0, y0+ h*0.25, w, h*0.6);
   ctx.fillStyle= e.type===2?'#ff3b3b':'#c2b8a3'; ctx.fillRect(x0+w*0.18, y0, w*0.64, h*0.38);
   // eyes
   ctx.fillStyle=e.cd<200?'#ff0000':'#ffae00'; ctx.fillRect(x0+w*0.28, y0+h*0.12, w*0.14, h*0.08); ctx.fillRect(x0+w*0.60, y0+h*0.12, w*0.14, h*0.08);
   // teeth / gun
   ctx.fillStyle='#111'; ctx.fillRect(x0+w*0.33, y0+h*0.26, w*0.34, h*0.06);
   ctx.fillStyle='#1a1a1a'; ctx.fillRect(x0+w*0.42, y0+h*0.55, w*0.16, h*0.22);
   // health bar
   ctx.fillStyle='#000'; ctx.fillRect(x0, y0-6, w, 4);
   ctx.fillStyle=healthPct>0.5?'#1aff66':healthPct>0.25?'#ffae00':'#ff1a1a'; ctx.fillRect(x0+1, y0-5, (w-2)*healthPct, 2);
  } else if(s.kind==='pickup'){
   let p=s.obj;
   let size2=Math.floor(size*0.35);
   let x0=sx-size2/2, y0=H/2 - size2/2 + 6;
   let colMap={health:'#1aff66', armor:'#1a8cff', ammo:'#ffae00', key:'#00ffd0', shotgun:'#ff3b3b', ripper:'#b56cff'};
   let col2=colMap[p.type]||'#fff';
   // float
   y0+= Math.sin(performance.now()*0.004 + s.x*2)*6;
   ctx.fillStyle=col2; ctx.fillRect(x0, y0, size2, size2);
   ctx.fillStyle='rgba(255,255,255,0.85)'; ctx.fillRect(x0+3, y0+3, size2*0.28, size2*0.28);
   ctx.fillStyle='#000'; ctx.font=`${Math.max(8,size2*0.28)}px monospace`; ctx.textAlign='center'; ctx.fillText(p.type==='health'?'+':p.type==='key'?'K':p.type==='armor'?'A':'•', x0+size2/2, y0+size2*0.75);
   // glow
   ctx.shadowColor=col2; ctx.shadowBlur=10; ctx.strokeStyle=col2; ctx.strokeRect(x0,y0,size2,size2); ctx.shadowBlur=0;
  } else if(s.kind==='particle'){
   let sz=Math.max(2, Math.floor(6/perp));
   ctx.fillStyle=s.obj.cl; ctx.fillRect(sx, H/2 - H/perp*0.1, sz, sz);
  }
 }
 ctx.restore();
 // crosshair
 ctx.fillStyle='rgba(255,174,0,0.9)'; ctx.fillRect(W/2-1, H/2-8, 2, 6); ctx.fillRect(W/2-1, H/2+4, 2,6); ctx.fillRect(W/2-8,H/2-1,6,2); ctx.fillRect(W/2+4,H/2-1,6,2);
 ctx.fillStyle='#000'; ctx.fillRect(W/2-0.5,H/2-0.5,1,1);
 drawWeapon();
 drawMinimap();
}

function drawWeapon(){
 const w=weapons[player.weapon];
 weaponSvg.innerHTML='';
 let bobX=Math.sin(player.bob*0.7)*4, bobY=Math.abs(Math.cos(player.bob*0.7))*4;
 let flash = muzzle>0;
 let g=document.createElementNS('http://www.w3.org/2000/svg','g');
 g.setAttribute('transform',`translate(${130+bobX},${74+bobY})`);
 // muzzle flash
 if(flash){
  let f=document.createElementNS('http://www.w3.org/2000/svg','polygon');
  f.setAttribute('points','0,-32 18,-12 8,-6 14,8 0,2 -14,8 -8,-6 -18,-12');
  f.setAttribute('fill','#fff'); f.setAttribute('opacity','0.95');
  g.appendChild(f);
  let f2=document.createElementNS('http://www.w3.org/2000/svg','polygon');
  f2.setAttribute('points','0,-28 12,-10 6,-4 10,6 0,0 -10,6 -6,-4 -12,-10');
  f2.setAttribute('fill',w.color); g.appendChild(f2);
 }
 // weapon body by type
 let barrel=document.createElementNS('http://www.w3.org/2000/svg','rect');
 if(w.name==='PISTOL'){
  barrel.setAttribute('x','-10'); barrel.setAttribute('y','-18'); barrel.setAttribute('width','20'); barrel.setAttribute('height','42'); barrel.setAttribute('rx','2');
  barrel.setAttribute('fill','#2b2b2b'); barrel.setAttribute('stroke','#111'); g.appendChild(barrel);
  let slide=document.createElementNS('http://www.w3.org/2000/svg','rect'); slide.setAttribute('x','-13'); slide.setAttribute('y','-22'); slide.setAttribute('width','26'); slide.setAttribute('height','14'); slide.setAttribute('fill','#1a1a1a'); g.appendChild(slide);
 } else if(w.name==='SHOTGUN'){
  barrel.setAttribute('x','-14'); barrel.setAttribute('y','-28'); barrel.setAttribute('width','28'); barrel.setAttribute('height','56'); barrel.setAttribute('rx','3'); barrel.setAttribute('fill','#3a2a1a'); g.appendChild(barrel);
  let met=document.createElementNS('http://www.w3.org/2000/svg','rect'); met.setAttribute('x','-11'); met.setAttribute('y','-30'); met.setAttribute('width','22'); met.setAttribute('height','34'); met.setAttribute('fill','#333'); g.appendChild(met);
 } else {
  barrel.setAttribute('x','-16'); barrel.setAttribute('y','-22'); barrel.setAttribute('width','32'); barrel.setAttribute('height','48'); barrel.setAttribute('fill','#222'); g.appendChild(barrel);
  for(let i=0;i<3;i++){ let r=document.createElementNS('http://www.w3.org/2000/svg','rect'); r.setAttribute('x', -12+i*9); r.setAttribute('y','-18'); r.setAttribute('width','6'); r.setAttribute('height','36'); r.setAttribute('fill', i%2?'#444':'#666'); g.appendChild(r); }
 }
 let grip=document.createElementNS('http://www.w3.org/2000/svg','rect'); grip.setAttribute('x','-9'); grip.setAttribute('y','24'); grip.setAttribute('width','18'); grip.setAttribute('height','28'); grip.setAttribute('fill','#0f0f0f'); grip.setAttribute('transform','rotate(12)'); g.appendChild(grip);
 weaponSvg.appendChild(g);
}

function drawMinimap(){
 const s=12, scale=7.5;
 mctx.fillStyle='#0a0a0a'; mctx.fillRect(0,0,120,120);
 for(let y=0;y<MAP_H;y++) for(let x=0;x<MAP_W;x++){
  let v=MAP[y][x];
  if(v===1) {mctx.fillStyle='#333'; mctx.fillRect(x*s, y*s, s-1, s-1);}
  else if(v===9){ mctx.fillStyle=player.hasKey?'#1aff66':'#ffae00'; mctx.fillRect(x*s, y*s, s-1,s-1); mctx.fillStyle='#000'; mctx.font='7px monospace'; mctx.fillText('EXIT', x*s+1, y*s+7); }
 }
 // pickups
 for(let p of pickups){ if(p.taken) continue; let c={health:'#1aff66',armor:'#1a8cff',ammo:'#ffae00',key:'#00ffd0',shotgun:'#ff6a00',ripper:'#b56cff'}[p.type]; mctx.fillStyle=c; mctx.fillRect(p.x*s-2,p.y*s-2,4,4); }
 for(let e of enemies){ if(!e.alive) continue; mctx.fillStyle='#ff1a1a'; mctx.beginPath(); mctx.arc(e.x*s, e.y*s, 3,0,Math.PI*2); mctx.fill(); }
 // player
 mctx.fillStyle='#fff'; mctx.beginPath(); mctx.arc(player.x*s, player.y*s, 3.5,0,Math.PI*2); mctx.fill();
 mctx.strokeStyle='#fff'; mctx.beginPath(); mctx.moveTo(player.x*s, player.y*s); mctx.lineTo(player.x*s+Math.cos(player.ang)*10, player.y*s+Math.sin(player.ang)*10); mctx.stroke();
}

let lastFpsUpdate=0, frameCount=0;
function loop(t){
 requestAnimationFrame(loop);
 let dt=Math.min(0.05, (t-lastTime)/1000); lastTime=t;
 if(document.getElementById('startOverlay').classList.contains('hidden')){
   gameUpdate(dt);
   drawWorld();
   frameCount++;
   if(t-lastFpsUpdate>500){ document.getElementById('fps').textContent=`${Math.round(frameCount*1000/(t-lastFpsUpdate))} FPS • 640×400`; frameCount=0; lastFpsUpdate=t; }
 } else {
   // attract mode: slowly rotate view
   player.ang+=dt*0.2;
   drawWorld();
 }
}
resetGame();
document.getElementById('startOverlay').classList.remove('hidden');
hideOverlays(); document.getElementById('startOverlay').classList.remove('hidden'); // ensure start visible
// keep game state but start overlay covers
player.x=2.5; player.y=2.5;
lastTime=performance.now();
requestAnimationFrame(loop);

// prevent context menu
canvas.addEventListener('contextmenu',e=>e.preventDefault());
</script>
</body>
</html>
<!-- agent-meta {"model":"meta/muse-spark-1.2","provider":"openrouter","persona":"basic","sessionId":"ff2e48cb-65b6-4b8b-99c9-6426b614e716","tokensIn":223,"tokensOut":15666,"tokensTotal":15889,"cost":0.06685925,"turns":1,"toolCalls":0,"failedToolCalls":0,"timestamp":"2026-08-06T15:08:50.930Z"} -->