← DOOM results

DOOM

Qwen 3.8 · 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 41,854 bytes · SHA-256 d5ef84fa304c
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>GRIDFALL — a retro raycast slayer</title>
<style>
:root{--grn:#5dff5d;--amb:#ffd23e;--red:#ff5040;--cyn:#3fe0d0;}
*{box-sizing:border-box;user-select:none;-webkit-user-select:none;}
html,body{margin:0;height:100%;background:#04050a;overflow:hidden;font-family:'Courier New',Courier,monospace;}
#stage{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);width:min(97vw,calc(97vh*1.6));aspect-ratio:16/10;box-shadow:0 0 80px rgba(0,0,0,.9);}
#game{width:100%;height:100%;display:block;background:#000;image-rendering:pixelated;image-rendering:crisp-edges;cursor:crosshair;}
.fx{position:absolute;inset:0;pointer-events:none;}
#fx-scan{background:repeating-linear-gradient(0deg,rgba(0,0,0,.13) 0 1px,transparent 1px 3px);z-index:3;}
#fx-vig{background:radial-gradient(ellipse at 50% 50%,transparent 52%,rgba(0,0,0,.55) 100%);z-index:3;}
#dmg{background:radial-gradient(ellipse at 50% 50%,rgba(255,30,20,.03) 35%,rgba(255,30,20,.4));opacity:0;transition:opacity .25s;z-index:4;}
#cross{position:absolute;left:50%;top:50%;width:0;height:0;pointer-events:none;z-index:5;transition:opacity .3s;}
#cross::before,#cross::after{content:'';position:absolute;background:#8dff7a;box-shadow:0 0 4px rgba(120,255,110,.8);}
#cross::before{width:11px;height:2px;left:-5px;top:-1px;}
#cross::after{width:2px;height:11px;left:-1px;top:-5px;}
#cross.hit::before,#cross.hit::after{background:#fff;box-shadow:0 0 6px #fff;}
#hud{position:absolute;left:0;right:0;bottom:0;display:flex;justify-content:space-between;align-items:flex-end;padding:8px 14px;z-index:6;pointer-events:none;font-weight:bold;letter-spacing:1px;transition:opacity .3s;}
.hud-col{display:flex;flex-direction:column;gap:5px;}
.hud-lab{font-size:10px;opacity:.9;}
.hud-num{font-size:22px;line-height:1;text-shadow:0 0 8px currentColor;}
#hpbar{width:120px;height:6px;background:rgba(255,255,255,.14);margin-top:2px;}
#hpfill{height:100%;width:100%;background:var(--grn);box-shadow:0 0 6px var(--grn);transition:width .15s;}
#wslots{display:flex;gap:4px;}
.wslot{font-size:10px;padding:2px 7px;border:1px solid rgba(255,255,255,.25);color:rgba(255,255,255,.45);}
.wslot.on{color:#0a0c10;background:var(--amb);border-color:var(--amb);box-shadow:0 0 8px rgba(255,210,62,.5);}
#cores{display:flex;gap:7px;}
.core{width:11px;height:11px;transform:rotate(45deg);border:1px solid rgba(63,224,208,.5);background:transparent;}
.core.lit{background:var(--cyn);box-shadow:0 0 8px var(--cyn);border-color:var(--cyn);}
#toast{position:absolute;top:16%;left:50%;transform:translateX(-50%);color:#8affd9;text-shadow:0 0 10px rgba(63,224,208,.9);font-size:16px;letter-spacing:3px;font-weight:bold;opacity:0;transition:opacity .3s;z-index:7;pointer-events:none;text-align:center;white-space:nowrap;}
#toast.show{opacity:1;}
.overlay{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:16px;background:rgba(4,5,10,.87);z-index:10;color:#cfd6dc;text-align:center;}
.hidden{display:none!important;}
.title{font-size:52px;letter-spacing:14px;color:var(--red);text-shadow:0 0 18px rgba(255,80,64,.7),0 4px 0 #4a0f0a;margin:0;}
.sub{font-size:12px;letter-spacing:6px;color:#7f8a99;margin-top:-10px;}
.rules{font-size:12px;line-height:2.1;color:#9aa5b5;letter-spacing:2px;}
.rules b{color:#e6ebf2;}
.btn{pointer-events:auto;cursor:pointer;font-family:inherit;font-weight:bold;font-size:16px;letter-spacing:4px;padding:12px 34px;background:transparent;color:var(--grn);border:2px solid var(--grn);box-shadow:0 0 12px rgba(93,255,93,.35),inset 0 0 12px rgba(93,255,93,.12);transition:all .15s;}
.btn:hover{background:var(--grn);color:#04120a;box-shadow:0 0 26px rgba(93,255,93,.7);}
.stats{font-size:14px;line-height:2.2;color:#9aa5b5;letter-spacing:2px;}
.stats b{color:var(--amb);}
#dead h2{font-size:44px;letter-spacing:12px;color:var(--red);text-shadow:0 0 20px rgba(255,80,64,.8);margin:0;}
</style>
</head>
<body>
<div id="stage">
  <canvas id="game" width="320" height="200"></canvas>
  <div class="fx" id="fx-scan"></div>
  <div class="fx" id="fx-vig"></div>
  <div class="fx" id="dmg"></div>
  <div id="cross"></div>
  <div id="hud">
    <div class="hud-col" style="color:var(--grn)">
      <div><span class="hud-lab">HEALTH</span> <span class="hud-num" id="hp">100</span></div>
      <div id="hpbar"><div id="hpfill"></div></div>
    </div>
    <div class="hud-col" style="color:var(--amb);align-items:center">
      <div id="wslots">
        <span class="wslot on" id="ws0">1 P-9</span><span class="wslot" id="ws1">2 SG</span><span class="wslot" id="ws2">3 MG</span>
      </div>
      <div class="hud-num" id="ammo" style="font-size:18px">∞</div>
      <div class="hud-lab" id="wname">P-9 SIDEARM</div>
    </div>
    <div class="hud-col" style="color:var(--cyn);align-items:flex-end">
      <div id="cores"><span class="core" id="c0"></span><span class="core" id="c1"></span><span class="core" id="c2"></span></div>
      <div class="hud-lab" style="color:#cfd6dc">KILLS <span id="kills">0/13</span>&nbsp;&nbsp;TIME <span id="time">0:00</span></div>
    </div>
  </div>
  <div id="toast"></div>
  <div class="overlay" id="menu">
    <h1 class="title">GRIDFALL</h1>
    <div class="sub">A RETRO RAYCAST SLAYER</div>
    <div class="rules">
      <b>WASD / ARROWS</b> MOVE &nbsp;&middot;&nbsp; <b>MOUSE / Q E</b> LOOK &nbsp;&middot;&nbsp; <b>SHIFT</b> SPRINT<br>
      <b>CLICK / SPACE</b> FIRE &nbsp;&middot;&nbsp; <b>1 2 3 / WHEEL</b> WEAPONS &nbsp;&middot;&nbsp; <b>M</b> MUTE<br>
      RECOVER THE <b>3 REACTOR CORES</b> &mdash; BREACH THE EXIT DOOR &mdash; SURVIVE
    </div>
    <button class="btn" id="startBtn">START MISSION</button>
    <div class="sub" style="margin-top:6px">BEST WITH MOUSE + KEYBOARD &middot; SOUND ON</div>
  </div>
  <div class="overlay hidden" id="pause">
    <div class="sub" style="font-size:22px;letter-spacing:8px">PAUSED</div>
    <div class="rules">CLICK TO RESUME</div>
  </div>
  <div class="overlay hidden" id="dead">
    <h2>YOU DIED</h2>
    <div class="sub">RESPAWNING&hellip;</div>
  </div>
  <div class="overlay hidden" id="win">
    <h1 class="title" style="color:var(--grn);text-shadow:0 0 18px rgba(93,255,93,.7),0 4px 0 #0a3a12">SECTOR CLEARED</h1>
    <div class="stats" id="winStats"></div>
    <button class="btn" id="againBtn">PLAY AGAIN</button>
  </div>
</div>
<script>
'use strict';
/* ================= DOM ================= */
const cv=document.getElementById('game'),ctx=cv.getContext('2d');
ctx.imageSmoothingEnabled=false;
const $=id=>document.getElementById(id);
const el={hp:$('hp'),hpfill:$('hpfill'),ammo:$('ammo'),wname:$('wname'),kills:$('kills'),time:$('time'),
  toast:$('toast'),dmg:$('dmg'),cross:$('cross'),hud:$('hud'),
  menu:$('menu'),pause:$('pause'),dead:$('dead'),win:$('win'),winStats:$('winStats'),
  cores:[$('c0'),$('c1'),$('c2')],slots:[$('ws0'),$('ws1'),$('ws2')]};
const BW=320,BH=200;
/* ================= utils ================= */
const TAU=Math.PI*2;
const clamp=(v,a,b)=>v<a?a:v>b?b:v;
const rand=(a=1,b)=>b===undefined?Math.random()*a:a+Math.random()*(b-a);
function hash2(x,y){const s=Math.sin(x*127.1+y*311.7)*43758.5453;return s-Math.floor(s);}
/* ================= audio ================= */
let AC=null,master=null,muted=false,noiseBuf=null;
function audioInit(){
  if(AC)return;
  try{
    AC=new (window.AudioContext||window.webkitAudioContext)();
    master=AC.createGain();master.gain.value=0.45;master.connect(AC.destination);
    noiseBuf=AC.createBuffer(1,AC.sampleRate*0.5,AC.sampleRate);
    const d=noiseBuf.getChannelData(0);
    for(let i=0;i<d.length;i++)d[i]=Math.random()*2-1;
  }catch(e){AC=null;}
}
function beep(f0,f1,dur,type='square',vol=0.3,delay=0){
  if(!AC||muted)return;
  const t=AC.currentTime+delay;
  const o=AC.createOscillator(),g=AC.createGain();
  o.type=type;o.frequency.setValueAtTime(Math.max(1,f0),t);
  o.frequency.exponentialRampToValueAtTime(Math.max(1,f1),t+dur);
  g.gain.setValueAtTime(vol,t);g.gain.exponentialRampToValueAtTime(0.001,t+dur);
  o.connect(g);g.connect(master);o.start(t);o.stop(t+dur+0.05);
}
function noise(dur,vol,fc=1000,delay=0){
  if(!AC||muted)return;
  const t=AC.currentTime+delay;
  const s=AC.createBufferSource();s.buffer=noiseBuf;s.loop=true;
  const f=AC.createBiquadFilter();f.type='lowpass';f.frequency.value=fc;
  const g=AC.createGain();g.gain.setValueAtTime(vol,t);g.gain.exponentialRampToValueAtTime(0.001,t+dur);
  s.connect(f);f.connect(g);g.connect(master);s.start(t);s.stop(t+dur+0.05);
}
const sfx={
  pistol(){noise(0.07,0.5,2200);beep(240,70,0.08,'square',0.25);},
  shotgun(){noise(0.28,0.9,800);beep(130,40,0.22,'sawtooth',0.4);},
  mg(){noise(0.05,0.4,2600);beep(320,100,0.05,'square',0.2);},
  click(){beep(900,600,0.04,'square',0.12);},
  hit(){beep(170,90,0.07,'square',0.25);},
  edie(){beep(300,45,0.4,'sawtooth',0.35);noise(0.3,0.25,500);},
  hurt(){beep(120,55,0.25,'sawtooth',0.5);},
  pdie(){beep(220,28,1.1,'sawtooth',0.5);noise(0.8,0.4,300);},
  pickup(){beep(660,990,0.12,'square',0.25);},
  ammo(){beep(440,660,0.1,'square',0.22);},
  core(){beep(440,880,0.15,'square',0.3);beep(880,1760,0.25,'square',0.3,0.12);beep(1320,1760,0.3,'square',0.2,0.28);},
  door(){noise(1.0,0.5,250);beep(55,28,1.0,'sawtooth',0.4);},
  bolt(){beep(500,200,0.12,'square',0.18);},
  win(){[523,659,784,1046,1318].forEach((f,i)=>beep(f,f,0.22,'square',0.3,i*0.13));}
};
/* ================= map ================= */
const MW=24,MH=24;
let grid=[];
function buildMap(){
  grid=Array.from({length:MH},()=>Array(MW).fill(1));
  const carve=(x0,y0,x1,y1)=>{for(let y=y0;y<=y1;y++)for(let x=x0;x<=x1;x++)grid[y][x]=0;};
  carve(1,17,7,22);   // start room
  carve(3,12,4,16);   // corridor north
  carve(4,8,11,16);   // central hall
  carve(8,3,9,7);     // corridor to top
  carve(4,1,17,7);    // top room
  carve(19,1,22,7);   // exit chamber
  carve(14,9,22,16);  // right room
  carve(14,18,22,22); // bottom-right room
  carve(8,19,13,20);  // bottom corridor
  carve(20,8,21,8);   // chamber -> right
  carve(17,17,18,17); // right -> bottom-right
  grid[3][18]=5;      // exit door
  for(let y=0;y<MH;y++)for(let x=0;x<MW;x++){
    const c=grid[y][x];
    if(c===0||c===5)continue;
    grid[y][x]=wallType(x,y);
  }
}
function wallType(x,y){
  if(y<8&&x>=4&&x<=22)return 2;
  if(x>=14&&y>=8&&y<=17)return 4;
  if(x<=7&&y>=17)return 3;
  return 1;
}
const cellAt=(x,y)=>{
  if(x<0||y<0||x>=MW||y>=MH)return 1;
  const c=grid[y|0][x|0];
  if(c===5&&typeof state!=='undefined'&&state.doorOpen)return 0; // open exit door
  return c;
};
const isSolid=(x,y)=>cellAt(x,y)>0;
function castRay(px,py,ang){
  const dx=Math.cos(ang),dy=Math.sin(ang);
  let mx=px|0,my=py|0;
  const ddx=dx===0?1e30:Math.abs(1/dx),ddy=dy===0?1e30:Math.abs(1/dy);
  let sx,sdx,sy,sdy;
  if(dx<0){sx=-1;sdx=(px-mx)*ddx;}else{sx=1;sdx=(mx+1-px)*ddx;}
  if(dy<0){sy=-1;sdy=(py-my)*ddy;}else{sy=1;sdy=(my+1-py)*ddy;}
  for(let i=0;i<64;i++){
    let side;
    if(sdx<sdy){mx+=sx;side=0;}else{my+=sy;side=1;}
    const c=cellAt(mx,my);
    if(c>0){
      const dist=side===0?sdx:sdy;
      let wallX=side===0?py+dist*dy:px+dist*dx;
      wallX-=Math.floor(wallX);
      return {dist,side,type:c,wallX};
    }
    if(side===0)sdx+=ddx;else sdy+=ddy;
  }
  return {dist:40,side:0,type:1,wallX:0};
}
function hasLOS(ax,ay,bx,by){
  const d=Math.hypot(bx-ax,by-ay);
  if(d<0.01)return true;
  const h=castRay(ax,ay,Math.atan2(by-ay,bx-ax));
  return h.dist>=d-0.08;
}
function moveCircle(e,dx,dy){
  const r=0.28;
  let nx=e.x+dx;
  if(!isSolid(nx+r,e.y)&&!isSolid(nx-r,e.y)&&!isSolid(nx,e.y+r)&&!isSolid(nx,e.y-r))e.x=nx;
  let ny=e.y+dy;
  if(!isSolid(e.x,ny+r)&&!isSolid(e.x,ny-r)&&!isSolid(e.x+r,ny)&&!isSolid(e.x-r,ny))e.y=ny;
}
</script>
<script>
'use strict';
/* ================= state ================= */
const state={
  mode:'menu', // menu | play | dead | win
  t:0, elapsed:0,
  px:4.5,py:20.5,ang:Math.PI/2,
  hp:100, maxhp:100,
  weapon:0, ammo:[Infinity,32,120],
  bob:0, bobT:0, fireCd:0, recoil:0, muzzle:0,
  cores:0, kills:0,
  alert:false,
  grace:3,
  shake:0, dmgFlash:0,
  respawnT:0, winT:0,
  enemies:[], items:[], parts:[],
  spawnQueue:[], spawnT:3,
  doorOpen:false, doorAnim:0,
  toastT:0,
  hitT:0,
};
const WEAPONS=[
  {name:'P-9 SIDEARM',  auto:false, rate:0.26, dmg:34, spread:0.012, pellets:1,  kick:0.05, shake:0.2,  ammo:0,  sfx:'pistol',  reach:20},
  {name:'SCATTERGUN',   auto:false, rate:0.75, dmg:11, spread:0.07,  pellets:6,  kick:0.16, shake:1.0,  ammo:1,  sfx:'shotgun', reach:20},
  {name:'MAULER MG',    auto:true,  rate:0.085,dmg:15, spread:0.035, pellets:1,  kick:0.03, shake:0.35, ammo:2,  sfx:'mg',      reach:24},
];
const EDEF={
  grunt: {hp:55, spd:1.5,  dmg:7,  r:0.3,  scale:0.55, atk:0.9,  color:'#8a4a2f', name:'SPITTER'},
  brute: {hp:180,spd:1.15, dmg:18, r:0.38, scale:0.75, atk:1.0,  color:'#4a5d3a', name:'BRUTE'},
  crawler:{hp:32, spd:2.0,  dmg:5,  r:0.24, scale:0.4,  atk:1.1,  color:'#7a3a6e', name:'CRAWLER'},
  boss:  {hp:900,spd:1.5,  dmg:24, r:0.5,  scale:1.0,  atk:1.2,  color:'#5d1f1f', name:'WRAITH'},
};
/* ================= world setup ================= */
function resetWorld(){
  buildMap();
  state.px=4.5;state.py=20.5;state.ang=-Math.PI/2;
  state.hp=100;state.cores=0;state.kills=0;state.elapsed=0;
  state.weapon=0;state.ammo=[Infinity,32,120];
  state.enemies=[];state.items=[];state.parts=[];state.spawnQueue=[];
  state.doorOpen=false;state.doorAnim=0;state.spawnT=2.5;
  state.bossSpawned=false;
  state.alert=false;
  state.grace=3;
  state.shake=0;state.dmgFlash=0;state.recoil=0;state.muzzle=0;state.fireCd=0;
  // enemies
  const E=(t,x,y)=>state.enemies.push(makeEnemy(t,x,y));
  E('grunt',8.5,9.5);E('grunt',10.5,10.5);E('grunt',12.5,3.5);
  E('grunt',15.5,5.5);E('grunt',20.5,4.5);
  E('brute',5.5,4.5);E('brute',18.5,13.5);
  E('crawler',9.5,20.2);E('crawler',12,19.5);E('crawler',16.5,20.5);E('crawler',20.5,19.5);
  E('crawler',6.5,10.5);E('brute',20.5,20.5);
  // pickups
  const I=(x,y,k,v)=>state.items.push({x,y,kind:k,amt:v,t:rand(TAU),dead:false});
  I(2.5,18.5,'health',25);I(9.5,15.5,'health',25);I(21.5,2.5,'health',25);
  I(15.5,21.5,'health',25);I(5.5,2.5,'health',50);
  I(7.5,15.5,'ammo',12);I(21.5,6.5,'ammo',12);I(19.5,19.5,'ammo',24);
  I(6.5,5.5,'shot',8);I(15.5,2.5,'shot',8);
  I(11.5,11.5,'mg',40);I(17.5,15.5,'mg',40);
  I(10.5,20.5,'shot',6);
  I(19.5,10.5,'core',1);I(5.5,19.5,'core',1);I(2.5,4.5,'core',1);
  // elite spawn queue
  state.spawnQueue=[
    {t:20, type:'brute',  x:6.5,y:2.5,  msg:'REINFORCEMENTS DETECTED'},
    {t:38, type:'brute',  x:21.5,y:21.5,msg:'REINFORCEMENTS DETECTED'},
    {t:56, type:'crawler',x:12.5,y:3.5, msg:'SWARM INBOUND'},
    {t:56, type:'crawler',x:12.5,y:3.5, msg:''},
    {t:75, type:'brute',  x:2.5,y:21.5, msg:'REINFORCEMENTS DETECTED'},
  ];
}
function makeEnemy(type,x,y){
  const d=EDEF[type];
  return {type,x,y,hp:d.hp,maxhp:d.hp,spd:d.spd,dmg:d.dmg,r:d.r,scale:d.scale,
    atkT:d.atk*rand(0.5,1),flash:0,dead:false,deadT:0,seed:rand(100),
    state:'idle',los:false,shootT:0,boltT:0,charge:0,hurlT:0,
    boss:type==='boss',summonT:8,phase2:false};
}
/* ================= toast ================= */
function toast(msg,dur=2.2){el.toast.textContent=msg;el.toast.classList.add('show');state.toastT=dur;}
/* ================= player actions ================= */
function switchWeapon(i){
  if(i===state.weapon)return;
  state.weapon=i;sfx.click();
  el.slots.forEach((s,j)=>s.classList.toggle('on',j===i));
  updateHUD();
}
function tryFire(){
  const w=WEAPONS[state.weapon];
  if(state.fireCd>0)return;
  if(state.ammo[state.weapon]<=0){sfx.click();state.fireCd=0.25;return;}
  if(state.ammo[state.weapon]!==Infinity)state.ammo[state.weapon]--;
  state.fireCd=w.rate;
  state.recoil=1;state.muzzle=1;
  state.alert=true;
  state.ang+=rand(-w.spread*0.4,w.spread*0.4);
  state.shake=Math.max(state.shake,w.shake);
  sfx[w.sfx]();
  for(let p=0;p<w.pellets;p++){
    const a=state.ang+rand(-w.spread,w.spread);
    fireRay(a,w.dmg);
  }
  spawnMuzzleParts();
  updateHUD();
}
function fireRay(ang,dmg){
  let best=null,bd=1e9;
  for(const e of state.enemies){
    if(e.dead)continue;
    const ex=e.x-state.px,ey=e.y-state.py;
    const t=ex*Math.cos(ang)+ey*Math.sin(ang);
    if(t<0.2||t>bd)continue;
    const px2=state.px+Math.cos(ang)*t,py2=state.py+Math.sin(ang)*t;
    const d=Math.hypot(e.x-px2,e.y-py2);
    if(d<e.r+0.12){
      const wd=castRay(state.px,state.py,ang);
      if(wd.dist<t-0.15)continue;
      best=e;bd=t;
    }
  }
  if(best){
    best.hp-=dmg;best.flash=1;
    state.hitT=0.12;
    sfx.hit();
    for(let i=0;i<3;i++)spawnPart(best.x,best.y,'#ff5040',1.2);
    if(best.hp<=0)killEnemy(best);
  } else {
    // spark on wall
    const wd=castRay(state.px,state.py,ang);
    const wx=state.px+Math.cos(ang)*wd.dist,wy=state.py+Math.sin(ang)*wd.dist;
    for(let i=0;i<2;i++)spawnPart(wx,wy,'#ffd23e',1.5);
  }
}
function killEnemy(e){
  e.dead=true;e.deadT=0.5;
  state.kills++;
  sfx.edie();
  for(let i=0;i<14;i++)spawnPart(e.x,e.y,'#ff5040',2.5);
  for(let i=0;i<6;i++)spawnPart(e.x,e.y,'#ffd23e',2);
  // drops
  const r=Math.random();
  if(e.type==='grunt'&&r<0.22)state.items.push({x:e.x,y:e.y,kind:'health',amt:15,t:0,dead:false});
  else if(e.type==='grunt'&&r<0.5)state.items.push({x:e.x,y:e.y,kind:'ammo',amt:8,t:0,dead:false});
  else if((e.type==='brute'||e.type==='crawler')&&r<0.3)state.items.push({x:e.x,y:e.y,kind:'health',amt:20,t:0,dead:false});
  if(e.boss){
    state.items.push({x:e.x,y:e.y,kind:'health',amt:50,t:0,dead:false});
    state.items.push({x:e.x+0.3,y:e.y,kind:'ammo',amt:20,t:0,dead:false});
  }
  if(state.kills%4===0&&Math.random()<0.5)state.items.push({x:e.x,y:e.y,kind:'shot',amt:4,t:0,dead:false});
  updateHUD();
}
function damagePlayer(dmg){
  if(state.mode!=='play')return;
  state.hp-=dmg;
  state.dmgFlash=1;
  state.shake=Math.max(state.shake,0.8);
  sfx.hurt();
  if(state.hp<=0){
    state.hp=0;
    diePlayer();
  }
  updateHUD();
}
function diePlayer(){
  state.mode='dead';
  state.respawnT=1.6;
  sfx.pdie();
  for(let i=0;i<20;i++)spawnPart(state.px,state.py,'#ff5040',3);
  el.dead.classList.remove('hidden');
  el.hud.style.opacity=0.25;
}
function respawn(){
  state.px=4.5;state.py=20.5;state.ang=-Math.PI/2;
  state.hp=100;
  state.mode='play';
  el.dead.classList.add('hidden');
  el.hud.style.opacity=1;
  state.dmgFlash=1;
  state.grace=3;
  toast('BACK IN THE FIGHT');
  updateHUD();
}
function winGame(){
  state.mode='win';
  state.winT=1.2;
  sfx.win();
  const m=Math.floor(state.elapsed/60),s=Math.floor(state.elapsed%60);
  el.winStats.innerHTML=`TIME <b>${m}:${String(s).padStart(2,'0')}</b> &middot; KILLS <b>${state.kills}</b><br>REACTOR CORES <b>3/3</b> &middot; GRID SECURED`;
  el.win.classList.remove('hidden');
}
/* ================= particles ================= */
function spawnPart(x,y,c,spd){
  state.parts.push({x,y,vx:rand(-1,1)*spd,vy:rand(-1,1)*spd,life:rand(0.25,0.6),max:0.6,c});
  if(state.parts.length>220)state.parts.shift();
}
function spawnMuzzleParts(){
  for(let i=0;i<4;i++)state.parts.push({x:state.px+Math.cos(state.ang)*0.3,y:state.py+Math.sin(state.ang)*0.3,
    vx:Math.cos(state.ang)*3+rand(-1,1),vy:Math.sin(state.ang)*3+rand(-1,1),life:0.15,max:0.15,c:'#ffd23e'});
}
function boltHit(p){ // enemy projectile impact
  for(let i=0;i<6;i++)spawnPart(p.x,p.y,'#3fe0d0',2);
}
/* ================= enemy AI ================= */
function updateEnemies(dt){
  for(const e of state.enemies){
    if(e.dead){e.deadT-=dt;continue;}
    e.flash=Math.max(0,e.flash-dt*5);
    const dx=state.px-e.x,dy=state.py-e.y;
    const d=Math.hypot(dx,dy);
    const ang=Math.atan2(dy,dx);
    e.los=state.mode==='play'&&d<14&&hasLOS(e.x,e.y,state.px,state.py);
    const def=EDEF[e.type];
    if(e.boss){
      updateBoss(e,dt,d,ang,def);
      continue;
    }
    if(e.type==='crawler'){
      // chase once alerted (player fired or crawler hurt) or close
      const wake=state.alert||e.flash>0||d<3.5;
      if(wake&&d>0.55){
        moveCircle(e,Math.cos(ang)*def.spd*dt,Math.sin(ang)*def.spd*dt);
      } else if(!wake&&d>1.5){
        // idle skitter
        const wa=ang+Math.sin(state.t*2+e.seed)*1.4;
        moveCircle(e,Math.cos(wa)*def.spd*dt*0.35,Math.sin(wa)*def.spd*dt*0.35);
      }
      e.atkT-=dt;
      if(d<1.0&&e.atkT<=0){
        e.atkT=def.atk;
        damagePlayer(def.dmg);
        for(let i=0;i<5;i++)spawnPart(state.px,state.py,'#ff5040',1.5);
      }
      continue;
    }
    // grunt / brute: ranged
    if(e.los&&d<11){
      e.state='chase';
      if(e.type==='brute'&&d>3.2){
        moveCircle(e,Math.cos(ang)*def.spd*dt,Math.sin(ang)*def.spd*dt);
      } else if(d<2.2){
        moveCircle(e,-Math.cos(ang)*def.spd*dt*0.6,-Math.sin(ang)*def.spd*dt*0.6);
      }
      e.shootT-=dt;
      if(e.shootT<=0&&state.grace<=0){
        e.shootT=def.atk*rand(0.8,1.4);
        sfx.bolt();
        const spd=7;
        state.parts.push({bolt:true,x:e.x,y:e.y,vx:Math.cos(ang)*spd+rand(-0.4,0.4),vy:Math.sin(ang)*spd+rand(-0.4,0.4),life:1.6,max:1.6,c:'#3fe0d0',dmg:def.dmg});
      }
    } else {
      e.state='idle';
      if(e.los===false&&d<9){
        // wander toward player without LOS
        const wa=ang+Math.sin(state.t*1.3+e.seed)*0.8;
        moveCircle(e,Math.cos(wa)*def.spd*dt*0.7,Math.sin(wa)*def.spd*dt*0.7);
      }
    }
  }
  // bolts
  for(const p of state.parts){
    if(!p.bolt)continue;
    p.x+=p.vx*dt;p.y+=p.vy*dt;
    if(isSolid(p.x,p.y)){p.life=0;boltHit(p);continue;}
    if(state.mode==='play'&&Math.hypot(p.x-state.px,p.y-state.py)<0.35){
      p.life=0;
      damagePlayer(p.dmg);
    }
  }
}
function updateBoss(e,dt,d,ang,def){
  e.phase2=e.hp<e.maxhp*0.5;
  const spd=def.spd*(e.phase2?1.45:1);
  // charge
  e.charge-=dt;
  if(e.charge<=0&&!e.phase2){
    e.charge=6+rand(3);
    if(e.los&&d>3&&d<10){e.dashAng=ang;e.dashT=0.9;}
  }
  if(e.dashT>0){
    e.dashT-=dt;
    moveCircle(e,Math.cos(e.dashAng)*spd*3.2*dt,Math.sin(e.dashAng)*spd*3.2*dt);
    if(d<0.9){damagePlayer(26);e.dashT=0;}
  } else if(d>2.4){
    moveCircle(e,Math.cos(ang)*spd*dt,Math.sin(ang)*spd*dt);
  }
  // dual bolts
  e.shootT-=dt;
  if(e.shootT<=0&&e.los&&d<12){
    e.shootT=e.phase2?0.55:0.85;
    sfx.bolt();
    const n=e.phase2?3:2;
    for(let i=0;i<n;i++){
      const a=ang+(i-(n-1)/2)*0.16;
      state.parts.push({bolt:true,x:e.x,y:e.y,vx:Math.cos(a)*7.5,vy:Math.sin(a)*7.5,life:1.4,max:1.4,c:'#3fe0d0',dmg:16});
    }
  }
  // melee
  e.atkT-=dt;
  if(d<0.95&&e.atkT<=0){e.atkT=def.atk;damagePlayer(def.dmg);}
  // summon
  e.summonT-=dt;
  if(e.summonT<=0&&state.enemies.filter(x=>!x.dead).length<16){
    e.summonT=10;
    toast('THE WRAITH SUMMONS');
    for(let i=0;i<2;i++){
      const a=rand(TAU);
      const sx=clamp(e.x+Math.cos(a)*2.2,1,23),sy=clamp(e.y+Math.sin(a)*2.2,1,23);
      if(!isSolid(sx,sy))state.enemies.push(makeEnemy('crawler',sx,sy));
    }
  }
}
/* ================= pickups ================= */
function updateItems(dt){
  for(const it of state.items){
    if(it.dead)continue;
    it.t+=dt;
    const d=Math.hypot(it.x-state.px,it.y-state.py);
    if(d<0.6&&state.mode==='play'){
      it.dead=true;
      applyPickup(it);
    }
  }
}
function applyPickup(it){
  switch(it.kind){
    case 'health':
      if(state.hp>=state.maxhp){it.dead=false;return;}
      state.hp=Math.min(state.maxhp,state.hp+it.amt);
      sfx.pickup();toast('+'+it.amt+' HEALTH');break;
    case 'ammo':
      state.ammo[1]+=it.amt;state.ammo[2]+=it.amt;
      sfx.ammo();toast('+'+it.amt+' AMMO');break;
    case 'shot':
      state.ammo[1]+=it.amt;
      sfx.ammo();toast('+'+it.amt+' SHELLS');break;
    case 'mg':
      state.ammo[2]+=it.amt;
      sfx.ammo();toast('+'+it.amt+' MG ROUNDS');break;
    case 'core':
      state.cores++;
      sfx.core();
      toast('REACTOR CORE RECOVERED '+state.cores+'/3');
      for(let i=0;i<20;i++)spawnPart(it.x,it.y,'#3fe0d0',2.5);
      if(state.cores>=3){
        state.doorOpen=true;
        sfx.door();
        setTimeout(()=>{if(state.mode==='play')toast('EXIT DOOR OPEN — BREACH IT');},900);
      }
      break;
  }
  updateHUD();
}
/* ================= HUD ================= */
function updateHUD(){
  el.hp.textContent=Math.max(0,Math.ceil(state.hp));
  const pct=clamp(state.hp/state.maxhp,0,1);
  el.hpfill.style.width=(pct*100)+'%';
  el.hpfill.style.background=pct>0.5?'var(--grn)':pct>0.25?'var(--amb)':'var(--red)';
  el.hpfill.style.boxShadow='0 0 6px '+(pct>0.5?'var(--grn)':pct>0.25?'var(--amb)':'var(--red)');
  const a=state.ammo[state.weapon];
  el.ammo.textContent=a===Infinity?'∞':a;
  el.wname.textContent=WEAPONS[state.weapon].name;
  el.kills.textContent=state.kills+'/'+(13+(state.bossSpawned?1:0));
  const m=Math.floor(state.elapsed/60),s=Math.floor(state.elapsed%60);
  el.time.textContent=m+':'+String(s).padStart(2,'0');
  el.cores.forEach((c,i)=>c.classList.toggle('lit',i<state.cores));
}
</script>
<script>
'use strict';
/* ================= rendering ================= */
const WALLPAL={
  1:{base:'#46505a',dark:'#2e353d',edge:'#5f6b76',name:'concrete'},
  2:{base:'#54463c',dark:'#3a312a',edge:'#6d5d4e',name:'rust'},
  3:{base:'#45505a',dark:'#2e3840',edge:'#5a6873',name:'steel'},
  4:{base:'#4a5242',dark:'#333a2e',edge:'#616b54',name:'bio'},
  5:{base:'#5d1f1f',dark:'#3a1212',edge:'#8a2f2f',name:'door'},
};
let zbuf=new Float32Array(BW);
function drawWalls(shx,shy){
  const fov=66*Math.PI/180;
  const half=fov/2;
  for(let x=0;x<BW;x++){
    const camX=2*x/BW-1;
    const ang=state.ang-Math.PI/2+Math.atan(camX);
    const r=castRay(state.px,state.py,ang);
    const perp=r.dist*Math.cos(ang-state.ang);
    if(perp<0.05)continue;
    zbuf[x]=perp;
    const h=Math.min(BH*2.2,(BH*1.05)/perp);
    const y0=BH/2-h/2+shy;
    const pal=WALLPAL[r.type]||WALLPAL[1];
    // base column
    ctx.fillStyle=pal.base;
    ctx.fillRect(x,y0|0,1,h|0);
    // side shading
    if(r.side===1){ctx.fillStyle='rgba(0,0,0,0.28)';ctx.fillRect(x,y0|0,1,h|0);}
    // texture bands by wallX
    const t=r.wallX;
    ctx.fillStyle=pal.edge;
    if(t<0.06||t>0.94)ctx.fillRect(x,y0|0,1,h|0);
    // horizontal seams
    ctx.fillStyle=pal.dark;
    ctx.fillRect(x,(y0+h*0.33)|0,1,Math.max(1,(h*0.04)|0));
    ctx.fillRect(x,(y0+h*0.66)|0,1,Math.max(1,(h*0.04)|0));
    // vertical panel line
    if((t*2|0)%2===0){ctx.fillStyle='rgba(0,0,0,0.15)';ctx.fillRect(x,y0|0,1,h|0);}
    // distance fog
    const fog=clamp(perp/20,0,0.65);
    if(fog>0.02){ctx.fillStyle='rgba(5,7,12,'+fog.toFixed(3)+')';ctx.fillRect(x,y0|0,1,h|0);}
    // exit door glow
    if(r.type===5){
      const open=state.doorAnim;
      const gh=h*(0.25+0.5*open);
      ctx.fillStyle='rgba(63,224,208,'+(0.15+0.45*open+0.1*Math.sin(state.t*4)).toFixed(3)+')';
      ctx.fillRect(x,(BH/2-gh/2+shy)|0,1,gh|0);
    }
  }
}
function drawFloorCeil(shy){
  // ceiling
  ctx.fillStyle='#0c0e13';
  ctx.fillRect(0,0,BW,(BH/2+shy)|0);
  // floor
  ctx.fillStyle='#161a20';
  ctx.fillRect(0,(BH/2+shy)|0,BW,BH);
  // floor light strip near player (fake)
  const g=ctx.createLinearGradient(0,BH,0,BH/2+shy);
  g.addColorStop(0,'rgba(63,224,208,0.07)');
  g.addColorStop(1,'rgba(0,0,0,0)');
  ctx.fillStyle=g;
  ctx.fillRect(0,(BH/2+shy)|0,BW,BH);
}
function drawSprites(){
  // collect drawables: enemies + items + parts (non-bolt)
  const list=[];
  for(const e of state.enemies){
    if(e.dead&&e.deadT<=0)continue;
    list.push({kind:'enemy',e,d:(e.x-state.px)**2+(e.y-state.py)**2});
  }
  for(const it of state.items){
    if(it.dead)continue;
    list.push({kind:'item',it,d:(it.x-state.px)**2+(it.y-state.py)**2});
  }
  for(const p of state.parts){
    if(p.bolt)continue;
    list.push({kind:'part',p,d:(p.x-state.px)**2+(p.y-state.py)**2});
  }
  list.sort((a,b)=>b.d-a.d);
  const fov=66*Math.PI/180;
  for(const o of list){
    const ex=(o.kind==='enemy'?o.e.x:o.kind==='item'?o.it.x:o.p.x)-state.px;
    const ey=(o.kind==='enemy'?o.e.y:o.kind==='item'?o.it.y:o.p.y)-state.py;
    const dx=Math.cos(state.ang)*ex+Math.sin(state.ang)*ey;
    const dy=-Math.sin(state.ang)*ex+Math.cos(state.ang)*ey;
    if(dx<0.1)continue;
    const sx=(0.5+dy/(dx*Math.tan(fov/2)))*BW;
    if(sx<-40||sx>BW+40)continue;
    const colX=clamp(sx|0,0,BW-1);
    if(zbuf[colX]<dx)continue;
    if(o.kind==='enemy')drawEnemy(o.e,dx,sx);
    else if(o.kind==='item')drawItem(o.it,dx,sx);
    else drawPart(o.p,dx,sx);
  }
}
function drawEnemy(e,dist,sx){
  const def=EDEF[e.type];
  const h=(BH*0.92*def.scale)/dist;
  const bobY=Math.sin(state.t*6+e.seed)*h*0.03;
  const y0=BH/2+h*0.06+bobY;
  const w=h*0.62;
  const x=sx-w/2;
  const dying=e.dead;
  const alpha=dying?clamp(e.deadT/0.5,0,1):1;
  ctx.globalAlpha=alpha;
  const bodyC=e.flash>0?'#ffffff':def.color;
  const darkC=e.flash>0?'#dddddd':shade(def.color,-30);
  if(e.boss)drawBoss(e,x,y0,w,h,dist);
  else if(e.type==='crawler')drawCrawler(e,x,y0,w,h);
  else drawHumanoid(e,x,y0,w,h,bodyC,darkC);
  ctx.globalAlpha=1;
  // hp bar
  if(!dying&&e.hp<e.maxhp){
    const bw=clamp(w,10,40);
    ctx.fillStyle='rgba(0,0,0,0.6)';
    ctx.fillRect(sx-bw/2,y0-6,bw,3);
    ctx.fillStyle=e.boss?'#ff5040':'#5dff5d';
    ctx.fillRect(sx-bw/2,y0-6,bw*clamp(e.hp/e.maxhp,0,1),3);
  }
}
function shade(hex,amt){
  const n=parseInt(hex.slice(1),16);
  let r=(n>>16)+amt,g=((n>>8)&255)+amt,b=(n&255)+amt;
  r=clamp(r,0,255);g=clamp(g,0,255);b=clamp(b,0,255);
  return 'rgb('+r+','+g+','+b+')';
}
function drawHumanoid(e,x,y0,w,h,bodyC,darkC){
  const legs=Math.sin(state.t*8+e.seed)>0;
  // legs
  ctx.fillStyle=darkC;
  const lw=w*0.22;
  ctx.fillRect(x+w*0.22,y0+h*0.62+(legs?0:h*0.03),lw,h*0.38);
  ctx.fillRect(x+w*0.56,y0+h*0.62+(legs?h*0.03:0),lw,h*0.38);
  // torso
  ctx.fillStyle=bodyC;
  ctx.fillRect(x+w*0.18,y0+h*0.3,w*0.64,h*0.36);
  // shoulder pads
  ctx.fillStyle=darkC;
  ctx.fillRect(x+w*0.1,y0+h*0.3,w*0.14,h*0.14);
  ctx.fillRect(x+w*0.76,y0+h*0.3,w*0.14,h*0.14);
  // head
  ctx.fillStyle=bodyC;
  ctx.fillRect(x+w*0.3,y0,w*0.4,h*0.22);
  // visor
  ctx.fillStyle=e.los?'#3fe0d0':'#173a3a';
  ctx.fillRect(x+w*0.36,y0+h*0.06,w*0.28,h*0.07);
  // gun
  ctx.fillStyle='#22262c';
  ctx.fillRect(x+w*0.42,y0+h*0.42,w*0.16,h*0.2);
  // brute extra armor
  if(e.type==='brute'){
    ctx.fillStyle=darkC;
    ctx.fillRect(x+w*0.24,y0+h*0.34,w*0.52,h*0.1);
  }
}
function drawCrawler(e,x,y0,w,h){
  const bodyC=e.flash>0?'#ffffff':EDEF.crawler.color;
  const darkC=e.flash>0?'#dddddd':shade(EDEF.crawler.color,-30);
  const legP=Math.sin(state.t*14+e.seed);
  // legs
  ctx.fillStyle=darkC;
  ctx.fillRect(x+w*0.06,y0+h*0.55+legP*h*0.06,w*0.14,h*0.4);
  ctx.fillRect(x+w*0.3,y0+h*0.6- legP*h*0.06,w*0.14,h*0.42);
  ctx.fillRect(x+w*0.56,y0+h*0.55+legP*h*0.06,w*0.14,h*0.4);
  ctx.fillRect(x+w*0.78,y0+h*0.6-legP*h*0.06,w*0.12,h*0.4);
  // body
  ctx.fillStyle=bodyC;
  ctx.fillRect(x+w*0.12,y0+h*0.3,w*0.76,h*0.36);
  // eyes
  ctx.fillStyle=e.los?'#ff5040':'#5a1a1a';
  ctx.fillRect(x+w*0.24,y0+h*0.36,w*0.16,h*0.1);
  ctx.fillRect(x+w*0.6,y0+h*0.36,w*0.16,h*0.1);
}
function drawBoss(e,x,y0,w,h){
  const bodyC=e.flash>0?'#ffffff':(e.phase2?'#7a1f1f':'#5d1f1f');
  const darkC=e.flash>0?'#dddddd':'#3a1212';
  const pulse=0.5+0.5*Math.sin(state.t*(e.phase2?10:4));
  // aura
  ctx.fillStyle='rgba(255,80,64,'+(0.12+0.1*pulse).toFixed(3)+')';
  ctx.fillRect(x-w*0.1,y0-h*0.05,w*1.2,h*1.05);
  // body
  ctx.fillStyle=bodyC;
  ctx.fillRect(x+w*0.14,y0+h*0.22,w*0.72,h*0.72);
  // armor plates
  ctx.fillStyle=darkC;
  ctx.fillRect(x+w*0.1,y0+h*0.3,w*0.18,h*0.2);
  ctx.fillRect(x+w*0.72,y0+h*0.3,w*0.18,h*0.2);
  ctx.fillRect(x+w*0.3,y0+h*0.55,w*0.4,h*0.12);
  // crown
  ctx.fillStyle=darkC;
  ctx.fillRect(x+w*0.2,y0,w*0.6,h*0.14);
  ctx.fillRect(x+w*0.28,y0-h*0.06,w*0.08,h*0.08);
  ctx.fillRect(x+w*0.46,y0-h*0.09,w*0.08,h*0.1);
  ctx.fillRect(x+w*0.64,y0-h*0.06,w*0.08,h*0.08);
  // eyes
  ctx.fillStyle='rgba(255,210,62,'+(0.6+0.4*pulse).toFixed(3)+')';
  ctx.fillRect(x+w*0.3,y0+h*0.16,w*0.16,h*0.08);
  ctx.fillRect(x+w*0.54,y0+h*0.16,w*0.16,h*0.08);
  // core
  ctx.fillStyle='rgba(63,224,208,'+(0.5+0.5*pulse).toFixed(3)+')';
  ctx.fillRect(x+w*0.42,y0+h*0.62,w*0.16,h*0.16);
}
function drawItem(it,dist,sx){
  const h=(BH*0.28)/dist;
  const y0=BH/2+h*0.55+Math.sin(it.t*3)*h*0.08;
  const w=h*0.7;
  const x=sx-w/2;
  const pulse=0.7+0.3*Math.sin(it.t*4);
  if(it.kind==='health'){
    ctx.fillStyle='rgba(255,80,64,'+(0.25*pulse).toFixed(3)+')';
    ctx.fillRect(x-2,y0-2,w+4,h+4);
    ctx.fillStyle='#e8e8e8';
    ctx.fillRect(x,y0,w,h);
    ctx.fillStyle='#ff5040';
    ctx.fillRect(x+w*0.2,y0+h*0.3,w*0.6,h*0.4);
    ctx.fillRect(x+w*0.3,y0+h*0.2,w*0.4,h*0.6);
  } else if(it.kind==='core'){
    ctx.fillStyle='rgba(63,224,208,'+(0.3*pulse).toFixed(3)+')';
    ctx.fillRect(x-3,y0-3,w+6,h+6);
    ctx.save();
    ctx.translate(sx,y0+h/2);
    ctx.rotate(it.t*1.5);
    ctx.fillStyle='#3fe0d0';
    ctx.fillRect(-w*0.3,-h*0.3,w*0.6,h*0.6);
    ctx.fillStyle='#bffcf4';
    ctx.fillRect(-w*0.15,-h*0.15,w*0.3,h*0.3);
    ctx.restore();
  } else {
    // ammo boxes
    const col=it.kind==='shot'?'#ffd23e':'#8a97a5';
    ctx.fillStyle='rgba(0,0,0,0.4)';
    ctx.fillRect(x-1,y0+h-2,w+2,3);
    ctx.fillStyle='#2a2f36';
    ctx.fillRect(x,y0+h*0.35,w,h*0.65);
    ctx.fillStyle=col;
    ctx.fillRect(x+w*0.15,y0+h*0.45,w*0.16,h*0.4);
    ctx.fillRect(x+w*0.42,y0+h*0.45,w*0.16,h*0.4);
    ctx.fillRect(x+w*0.69,y0+h*0.45,w*0.16,h*0.4);
  }
}
function drawPart(p,dist,sx){
  const s=clamp(14/dist,1,10);
  ctx.globalAlpha=clamp(p.life/p.max,0,1);
  ctx.fillStyle=p.c;
  ctx.fillRect(sx-s/2,BH/2-s/2,s,s);
  ctx.globalAlpha=1;
}
function drawBolts(){
  const fov=66*Math.PI/180;
  for(const p of state.parts){
    if(!p.bolt)continue;
    const dx=Math.cos(state.ang)*(p.x-state.px)+Math.sin(state.ang)*(p.y-state.py);
    const dy=-Math.sin(state.ang)*(p.x-state.px)+Math.cos(state.ang)*(p.y-state.py);
    if(dx<0.1)continue;
    const sx=(0.5+dy/(dx*Math.tan(fov/2)))*BW;
    if(sx<0||sx>BW)continue;
    const colX=clamp(sx|0,0,BW-1);
    if(zbuf[colX]<dx)continue;
    const s=clamp(10/dx,1.5,8);
    ctx.fillStyle='rgba(63,224,208,0.35)';
    ctx.fillRect(sx-s,BH/2-s,s*2,s*2);
    ctx.fillStyle='#bffcf4';
    ctx.fillRect(sx-s/2,BH/2-s/2,s,s);
  }
}
/* weapon sprites */
function drawWeapon(shx,shy){
  const w=state.weapon;
  const bobX=Math.sin(state.bob)*4;
  const bobY=Math.abs(Math.cos(state.bob))*3+state.recoil*8;
  const ox=BW/2+bobX+shx,oy=BH-2+bobY+shy;
  ctx.save();
  if(w===0){ // P-9 sidearm
    ctx.fillStyle='#1d2126';
    ctx.fillRect(ox-8,oy-26,16,26);
    ctx.fillStyle='#2c333b';
    ctx.fillRect(ox-5,oy-30,10,8);
    ctx.fillStyle='#3a4450';
    ctx.fillRect(ox-3,oy-32,6,4);
    ctx.fillStyle='#14171b';
    ctx.fillRect(ox-2,oy-33,4,3);
    if(state.muzzle>0.5){
      ctx.fillStyle='rgba(255,210,62,0.9)';
      ctx.fillRect(ox-6,oy-44,12,10);
      ctx.fillStyle='#fff';
      ctx.fillRect(ox-3,oy-41,6,5);
    }
  } else if(w===1){ // scattergun
    ctx.fillStyle='#23282e';
    ctx.fillRect(ox-12,oy-22,24,22);
    ctx.fillStyle='#313943';
    ctx.fillRect(ox-8,oy-34,16,14);
    ctx.fillStyle='#454f5c';
    ctx.fillRect(ox-4,oy-38,8,6);
    ctx.fillStyle='#14171b';
    ctx.fillRect(ox-3,oy-39,6,3);
    ctx.fillStyle='#3a2c1c';
    ctx.fillRect(ox-10,oy-12,20,4);
    if(state.muzzle>0.4){
      ctx.fillStyle='rgba(255,160,40,0.9)';
      ctx.fillRect(ox-12,oy-52,24,14);
      ctx.fillStyle='#ffe9b0';
      ctx.fillRect(ox-6,oy-48,12,7);
    }
  } else { // mauler MG
    ctx.fillStyle='#20252c';
    ctx.fillRect(ox-16,oy-18,32,18);
    ctx.fillStyle='#2c333c';
    ctx.fillRect(ox-10,oy-30,20,14);
    ctx.fillStyle='#3d4652';
    ctx.fillRect(ox-5,oy-34,10,6);
    ctx.fillStyle='#14171b';
    ctx.fillRect(ox-2,oy-35,4,3);
    // ammo drum
    ctx.fillStyle='#39424e';
    ctx.fillRect(ox-6,oy-8,12,8);
    ctx.fillStyle='#ffd23e';
    ctx.fillRect(ox-4,oy-6,8,2);
    if(state.muzzle>0.5){
      ctx.fillStyle='rgba(255,210,62,0.85)';
      ctx.fillRect(ox-8,oy-44,16,10);
      ctx.fillStyle='#fff';
      ctx.fillRect(ox-4,oy-41,8,5);
    }
  }
  ctx.restore();
}
/* ================= input ================= */
const keys={};
let firing=false;
document.addEventListener('keydown',e=>{
  keys[e.code]=true;
  if(e.code==='Space'){e.preventDefault();firing=true;}
  if(e.code==='Digit1')switchWeapon(0);
  if(e.code==='Digit2')switchWeapon(1);
  if(e.code==='Digit3')switchWeapon(2);
  if(e.code==='KeyM'){muted=!muted;}
  if(e.code==='Escape'&&state.mode==='play'){state.mode='pause';el.pause.classList.remove('hidden');}
  else if(e.code==='Escape'&&state.mode==='pause'){state.mode='play';el.pause.classList.add('hidden');}
});
document.addEventListener('keyup',e=>{
  keys[e.code]=false;
  if(e.code==='Space')firing=false;
});
cv.addEventListener('mousedown',e=>{
  if(state.mode==='play'){firing=true;tryFire();}
  else if(state.mode==='pause'){state.mode='play';el.pause.classList.add('hidden');}
});
document.addEventListener('mouseup',()=>firing=false);
cv.addEventListener('contextmenu',e=>e.preventDefault());
cv.addEventListener('wheel',e=>{
  if(state.mode!=='play')return;
  const dir=e.deltaY>0?1:-1;
  switchWeapon((state.weapon+dir+3)%3);
},{passive:true});
document.addEventListener('mousemove',e=>{
  if(state.mode!=='play')return;
  if(document.pointerLockElement===cv){
    state.ang+=e.movementX*0.0022;
  } else {
    const r=cv.getBoundingClientRect();
    const mx=(e.clientX-r.left)/r.width;
    if(mx>0.62)state.ang+=0.02;
    else if(mx<0.38)state.ang-=0.02;
  }
});
$('startBtn').addEventListener('click',()=>{
  audioInit();
  if(AC&&AC.state==='suspended')AC.resume();
  resetWorld();
  state.mode='play';
  el.menu.classList.add('hidden');
  el.win.classList.add('hidden');
  el.hud.style.opacity=1;
  el.slots.forEach((s,j)=>s.classList.toggle('on',j===0));
  updateHUD();
  toast('RECOVER 3 REACTOR CORES');
  try{cv.requestPointerLock&&cv.requestPointerLock();}catch(err){}
});
$('againBtn').addEventListener('click',()=>$('startBtn').click());
/* ================= main loop ================= */
let last=performance.now();
function frame(now){
  const dt=Math.min(0.05,(now-last)/1000);
  last=now;
  state.t+=dt;
  if(state.mode==='play'){
    state.elapsed+=dt;
    state.grace=Math.max(0,state.grace-dt);
    updatePlay(dt);
  } else if(state.mode==='dead'){
    state.respawnT-=dt;
    if(state.respawnT<=0)respawn();
  } else if(state.mode==='win'){
    state.winT-=dt;
  }
  // toast decay
  if(state.toastT>0){
    state.toastT-=dt;
    if(state.toastT<=0)el.toast.classList.remove('show');
  }
  // hit crosshair flash
  if(state.hitT>0){state.hitT-=dt;el.cross.classList.add('hit');}
  else el.cross.classList.remove('hit');
  // damage flash
  state.dmgFlash=Math.max(0,state.dmgFlash-dt*2.2);
  el.dmg.style.opacity=state.dmgFlash.toFixed(2);
  // shake decay
  state.shake=Math.max(0,state.shake-dt*3);
  // recoil / muzzle decay
  state.recoil=Math.max(0,state.recoil-dt*6);
  state.muzzle=Math.max(0,state.muzzle-dt*10);
  // door anim
  if(state.doorOpen)state.doorAnim=Math.min(1,state.doorAnim+dt*1.5);
  // spawn queue
  if(state.mode==='play'){
    for(let i=state.spawnQueue.length-1;i>=0;i--){
      const q=state.spawnQueue[i];
      if(state.elapsed>=q.t){
        state.spawnQueue.splice(i,1);
        if(!isSolid(q.x,q.y)){
          state.enemies.push(makeEnemy(q.type,q.x,q.y));
          if(q.msg)toast(q.msg);
        }
      }
    }
  }
  // particles
  for(const p of state.parts){
    if(p.bolt)continue;
    p.x+=p.vx*dt;p.y+=p.vy*dt;p.life-=dt;
  }
  state.parts=state.parts.filter(p=>p.life>0);
  render();
  requestAnimationFrame(frame);
}
function updatePlay(dt){
  // movement
  const sprint=keys['ShiftLeft']||keys['ShiftRight']?1.5:1;
  let mx=0,my=0;
  const sp=3.2*sprint;
  if(keys['KeyW']||keys['ArrowUp']){mx+=Math.cos(state.ang);my+=Math.sin(state.ang);}
  if(keys['KeyS']||keys['ArrowDown']){mx-=Math.cos(state.ang);my-=Math.sin(state.ang);}
  if(keys['KeyA']){mx+=Math.sin(state.ang);my-=Math.cos(state.ang);}
  if(keys['KeyD']){mx-=Math.sin(state.ang);my+=Math.cos(state.ang);}
  const ml=Math.hypot(mx,my);
  if(ml>0.01){
    mx/=ml;my/=ml;
    moveCircle(state,mx*sp*dt,my*sp*dt);
    state.bobT+=dt*sprint;
    state.bob=state.bobT*9;
  }
  // look keys
  if(keys['KeyQ']||keys['ArrowLeft'])state.ang-=2.4*dt;
  if(keys['KeyE']||keys['ArrowRight'])state.ang+=2.4*dt;
  // fire
  state.fireCd-=dt;
  const w=WEAPONS[state.weapon];
  if(firing&&w.auto)tryFire();
  // enemies & items
  updateEnemies(dt);
  updateItems(dt);
  // boss spawn: all cores collected — guards the exit
  if(state.cores>=3&&!state.bossSpawned){
    state.bossSpawned=true;
    state.enemies.push(makeEnemy('boss',20.5,2.5));
    toast('WARNING: WRAITH EMERGING',3);
    sfx.door();
  }
  // door exit check
  if(state.doorOpen&&Math.abs(state.px-18.5)<0.5&&Math.abs(state.py-3.5)<0.5){
    winGame();
  }
  updateHUD();
}
function render(){
  const shx=(Math.random()*2-1)*state.shake*3;
  const shy=(Math.random()*2-1)*state.shake*3;
  ctx.fillStyle='#000';
  ctx.fillRect(0,0,BW,BH);
  drawFloorCeil(shy*0.5);
  drawWalls(shx,shy*0.5);
  drawSprites();
  drawBolts();
  if(state.mode!=='menu')drawWeapon(shx,shy);
  // low hp vignette pulse
  if(state.hp<30&&state.mode==='play'){
    ctx.fillStyle='rgba(255,30,20,'+(0.07+0.05*Math.sin(state.t*6)).toFixed(3)+')';
    ctx.fillRect(0,0,BW,BH);
  }
}
/* ================= boot ================= */
buildMap();
resetWorld();
updateHUD();
requestAnimationFrame(frame);
</script>
</body>
</html>
<!-- agent-meta {"model":"Qwen3.8","provider":"brigid","persona":"full","sessionId":"67514458-1545-4277-81ba-e8e2a0c8bc87","tokensIn":7912295,"tokensOut":118911,"tokensTotal":8031206,"cost":0,"turns":74,"toolCalls":98,"failedToolCalls":1,"timestamp":"2026-08-31T18:54:01.546Z"} -->