← DOOM results

DOOM

muse spark 1.3 contributor · 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 47,950 bytes · SHA-256 6d73dcb40f46
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>STEEL CORRIDOR — Retro FPS</title>
<style>
  *{margin:0;padding:0;box-sizing:border-box;-webkit-tap-highlight-color:transparent}
  html,body{width:100%;height:100%;background:#050505;overflow:hidden;font-family:'Courier New',monospace;user-select:none}
  #wrap{position:fixed;inset:0;display:flex;align-items:center;justify-content:center;background:radial-gradient(ellipse at center,#111 0%,#000 75%)}
  #frame{position:relative;width:min(100vw,177.7vh);height:min(56.25vw,100vh);max-width:1280px;max-height:720px;background:#000;box-shadow:0 0 80px rgba(255,40,20,.15),0 0 0 4px #1a1a1a,0 0 0 6px #000;overflow:hidden}
  #game{position:absolute;inset:0;width:100%;height:100%;image-rendering:pixelated;image-rendering:crisp-edges;cursor:crosshair}
  /* CRT overlays */
  #scan{position:absolute;inset:0;pointer-events:none;z-index:20;background:repeating-linear-gradient(0deg,rgba(0,0,0,.22) 0 1px,transparent 1px 3px);mix-blend-mode:multiply}
  #vig{position:absolute;inset:0;pointer-events:none;z-index:21;background:radial-gradient(ellipse at center,transparent 55%,rgba(0,0,0,.55) 100%)}
  #dmg{position:absolute;inset:0;pointer-events:none;z-index:22;opacity:0;background:radial-gradient(ellipse at center,transparent 35%,rgba(200,0,0,.75) 100%);transition:opacity .1s}
  #heal{position:absolute;inset:0;pointer-events:none;z-index:22;opacity:0;background:radial-gradient(ellipse at center,transparent 50%,rgba(0,255,120,.35) 100%);transition:opacity .3s}
  #flash{position:absolute;inset:0;pointer-events:none;z-index:23;opacity:0;background:#fff}
  /* HUD */
  #hud{position:absolute;left:0;right:0;bottom:0;z-index:30;pointer-events:none;display:flex;align-items:stretch;justify-content:space-between;padding:8px 10px 10px;background:linear-gradient(transparent,rgba(0,0,0,.75) 30%);font-weight:bold}
  .panel{background:rgba(10,10,12,.82);border:2px solid #3a3a3a;border-top-color:#666;border-left-color:#555;box-shadow:inset 0 0 12px #000,0 2px 0 #000;padding:6px 12px;min-width:110px;text-align:center}
  .panel .lbl{font-size:10px;letter-spacing:2px;color:#ff3b1f;text-shadow:0 0 6px rgba(255,60,20,.8)}
  .panel .val{font-size:26px;line-height:1;color:#f2f2f2;text-shadow:2px 2px 0 #000}
  #healthVal.low{color:#ff2222;animation:blink .5s infinite alternate}
  @keyframes blink{from{opacity:1}to{opacity:.4}}
  #ammoPanel .val{color:#ffd23f}
  #weapons{display:flex;gap:6px;align-items:center;justify-content:center}
  .wslot{width:52px;padding:4px 2px;border:2px solid #333;background:#0c0c0c;text-align:center;font-size:10px;color:#777;letter-spacing:1px}
  .wslot b{display:block;font-size:18px;line-height:1}
  .wslot.owned{color:#bbb;border-color:#555}
  .wslot.active{color:#ffd23f;border-color:#ffd23f;box-shadow:0 0 12px rgba(255,210,63,.5);background:#1a1505}
  .wslot.locked{opacity:.35}
  #topbar{position:absolute;top:0;left:0;right:0;z-index:30;display:flex;justify-content:space-between;align-items:flex-start;padding:10px;pointer-events:none}
  #msg{flex:1;text-align:center;font-size:15px;font-weight:bold;color:#ffd23f;text-shadow:2px 2px 0 #000,0 0 14px rgba(255,180,0,.6);letter-spacing:1px;min-height:22px}
  #kills{background:rgba(0,0,0,.7);border:2px solid #3a3a3a;padding:4px 10px;color:#ff5a3c;font-size:13px;font-weight:bold;letter-spacing:1px}
  #minimap{width:132px;height:132px;background:rgba(0,0,0,.78);border:2px solid #444;image-rendering:pixelated}
  #cross{position:absolute;left:50%;top:50%;z-index:29;transform:translate(-50%,-50%);pointer-events:none;font-size:22px;font-weight:bold;color:#fff;text-shadow:0 0 4px #000,1px 1px 0 #000;opacity:.9}
  #cross.hit{color:#ff2222;transform:translate(-50%,-50%) scale(1.5)}
  #hitmark{position:absolute;left:50%;top:50%;width:26px;height:26px;z-index:29;transform:translate(-50%,-50%) rotate(45deg);pointer-events:none;opacity:0;border:2px solid #fff;box-shadow:0 0 8px #ff0000}
  /* overlays */
  .screen{position:absolute;inset:0;z-index:40;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;background:rgba(0,0,0,.82);backdrop-filter:blur(2px);padding:20px}
  .hidden{display:none!important}
  h1.title{font-size:clamp(38px,7vw,84px);letter-spacing:4px;color:#ff3b1f;text-shadow:3px 3px 0 #5a0d00,0 0 30px rgba(255,60,20,.8),0 0 80px rgba(255,0,0,.4);font-weight:900;font-style:italic;transform:skew(-4deg);line-height:.95}
  h1.title span{color:#ffd23f;text-shadow:3px 3px 0 #5a3a00,0 0 30px rgba(255,210,63,.8)}
  .sub{color:#8aff8a;font-size:13px;letter-spacing:3px;margin:8px 0 4px}
  .story{max-width:560px;color:#bbb;font-size:13px;line-height:1.5;margin:12px 0}
  .story b{color:#ffd23f}.story i{color:#ff5a3c;font-style:normal}
  .btn{pointer-events:auto;cursor:pointer;margin-top:14px;background:linear-gradient(#ff5a2a,#b81e00);border:3px solid #ffd23f;color:#fff;font-family:inherit;font-weight:900;font-size:20px;letter-spacing:2px;padding:12px 42px;text-shadow:2px 2px 0 #000;box-shadow:0 4px 0 #5a0d00,0 0 24px rgba(255,60,20,.6);transform:skew(-4deg)}
  .btn:hover{filter:brightness(1.2)} .btn:active{transform:skew(-4deg) translateY(3px);box-shadow:0 1px 0 #5a0d00}
  .btn.ghost{background:#111;border-color:#555;box-shadow:none;font-size:14px;padding:8px 22px}
  .controls{display:flex;gap:18px;flex-wrap:wrap;justify-content:center;margin-top:14px;color:#888;font-size:11px;line-height:1.7}
  .controls b{color:#fff;background:#222;border:1px solid #555;padding:1px 7px;border-radius:3px;font-size:11px}
  #deathScreen{background:radial-gradient(ellipse at center,rgba(120,0,0,.85),rgba(0,0,0,.95))}
  #deathScreen h1{font-size:clamp(40px,8vw,90px);color:#ff1111;text-shadow:0 0 40px #f00,4px 4px 0 #300;font-style:italic;font-weight:900;transform:skew(-6deg)}
  #winScreen{background:radial-gradient(ellipse at center,rgba(0,80,20,.75),rgba(0,0,0,.94))}
  #winScreen h1{font-size:clamp(36px,7vw,76px);color:#ffd23f;text-shadow:0 0 40px rgba(255,210,63,.8),3px 3px 0 #5a3a00;font-weight:900;font-style:italic}
  .stats{display:flex;gap:14px;margin:14px 0;flex-wrap:wrap;justify-content:center}
  .stat{background:#0a0a0a;border:2px solid #444;padding:8px 18px;color:#fff;font-size:13px}
  .stat b{display:block;font-size:24px;color:#ffd23f}
  #dmgdir{position:absolute;z-index:24;inset:0;pointer-events:none;opacity:0}
  /* touch */
  #touch{position:absolute;inset:0;z-index:35;pointer-events:none;display:none}
  #touch button{pointer-events:auto;position:absolute;background:rgba(255,255,255,.08);border:2px solid rgba(255,255,255,.3);color:#fff;font-family:inherit;font-weight:bold;border-radius:12px;font-size:14px}
  #btnFire{right:18px;bottom:110px;width:92px;height:92px;border-radius:50%!important;background:rgba(255,40,20,.25)!important;border-color:#ff3b1f!important;font-size:16px!important}
  #btnWeap{right:122px;bottom:96px;width:60px;height:60px}
  #btnAct{right:30px;bottom:210px;width:60px;height:44px}
  #stick{left:18px;bottom:96px;width:120px;height:120px;border-radius:50%!important}
  @media (pointer:coarse){#touch{display:block}}
  #loadTip{position:absolute;bottom:76px;width:100%;text-align:center;z-index:31;color:#fff;font-size:12px;letter-spacing:1px;pointer-events:none;text-shadow:1px 1px 0 #000}
</style>
</head>
<body>
<div id="wrap">
<div id="frame">
  <canvas id="game" width="480" height="270"></canvas>
  <div id="scan"></div><div id="vig"></div><div id="dmg"></div><div id="heal"></div><div id="flash"></div>
  <div id="cross">+</div><div id="hitmark"></div>
  <div id="topbar">
    <canvas id="minimap" width="132" height="132"></canvas>
    <div id="msg"></div>
    <div id="kills">KILLS 0/12</div>
  </div>
  <div id="hud">
    <div class="panel"><div class="lbl">HEALTH</div><div class="val" id="healthVal">100</div></div>
    <div class="panel" id="weapons">
      <div class="wslot owned active" id="w1"><b>🔫</b>1 PISTOL</div>
      <div class="wslot locked" id="w2"><b>💥</b>2 SHOTGN</div>
      <div class="wslot locked" id="w3"><b>🔥</b>3 RAPID</div>
    </div>
    <div class="panel" id="ammoPanel"><div class="lbl">AMMO</div><div class="val"><span id="ammoBul">50</span><span style="font-size:13px;color:#888"> | </span><span id="ammoSh" style="color:#ff8c5a">8</span></div></div>
  </div>
  <div id="loadTip">CLICK TO LOCK MOUSE • WASD MOVE • CLICK SHOOT</div>

  <div id="touch">
    <button id="stick">◀ ▶</button>
    <button id="btnFire">FIRE</button>
    <button id="btnWeap">WPN</button>
    <button id="btnAct">OPEN</button>
  </div>

  <div id="titleScreen" class="screen">
    <div class="sub">SECTOR 7 BREACH // UAC-STYLE ACTION</div>
    <h1 class="title">STEEL<br><span>CORRIDOR</span></h1>
    <div class="story">The station is overrun. <b>12 hostiles</b> roam the corridors. Find the <b>SHOTGUN</b> and <b>RAPID CANNON</b>, stock <i>ammo &amp; medkits</i>, wipe them out — then escape through the <b>EXIT GATE</b>.</div>
    <button class="btn" id="startBtn">▶ START MISSION</button>
    <div class="controls">
      <div><b>W A S D</b> move<br><b>← →</b> / mouse turn</div>
      <div><b>CLICK</b> / SPACE shoot<br><b>1 2 3</b> weapons</div>
      <div><b>SHIFT</b> sprint<br><b>M</b> big map • <b>P</b> pause</div>
    </div>
    <div style="margin-top:10px;color:#555;font-size:11px">headphones recommended • procedural audio • no assets loaded</div>
  </div>

  <div id="deathScreen" class="screen hidden">
    <h1>YOU DIED</h1>
    <div class="story" id="deathInfo">The corridors claim another marine.</div>
    <button class="btn" id="respawnBtn">↻ RESPAWN</button>
    <button class="btn ghost" id="restartBtn">RESTART MISSION</button>
  </div>

  <div id="winScreen" class="screen hidden">
    <div class="sub">★ MISSION COMPLETE ★</div>
    <h1>ESCAPED!</h1>
    <div class="stats">
      <div class="stat">TIME<b id="stTime">0:00</b></div>
      <div class="stat">KILLS<b id="stKills">12/12</b></div>
      <div class="stat">ACCURACY<b id="stAcc">0%</b></div>
      <div class="stat">DEATHS<b id="stDeath">0</b></div>
    </div>
    <div class="story" id="rankText"></div>
    <button class="btn" id="againBtn">↻ PLAY AGAIN</button>
  </div>

  <div id="pauseScreen" class="screen hidden">
    <h1 class="title" style="font-size:48px">PAUSED</h1>
    <button class="btn" id="resumeBtn">▶ RESUME</button>
    <div class="controls"><div><b>MOUSE SENS</b> <input id="sens" type="range" min="1" max="10" value="4" style="width:120px;vertical-align:middle"></div>
    <div><b>QUALITY</b> <button class="btn ghost" id="qBtn" style="margin:0">HIGH</button></div></div>
  </div>
</div>
</div>
<script>
"use strict";
/* ================= STEEL CORRIDOR — single-file retro FPS ================= */
const W=480,H=270;
const cv=document.getElementById('game'),ctx=cv.getContext('2d');
ctx.imageSmoothingEnabled=false;
const mm=document.getElementById('minimap'),mmc=mm.getContext('2d');
const msgEl=document.getElementById('msg'),killsEl=document.getElementById('kills');
const healthEl=document.getElementById('healthVal'),bulEl=document.getElementById('ammoBul'),shEl=document.getElementById('ammoSh');
const dmgEl=document.getElementById('dmg'),healEl=document.getElementById('heal'),flashEl=document.getElementById('flash');
const crossEl=document.getElementById('cross'),hitEl=document.getElementById('hitmark');

/* ---------- MAP ---------- */
const MAP_STR=[
"########################",
"#......#......#.......E#",
"#.####.#..##..#.#####..#",
"#.#....#..#......#...#.#",
"#.#.####.#####.#.#.#.#.#",
"#.#.#......#...#...#...#",
"#.#.#.######.#######.###",
"#...#.#....#.#.....#...#",
"###.#.#.##.#.#.###.###.#",
"#...#...#..#...#.#.....#",
"#.#######.#####.#.#####.#",
"#.#.....#.#.....#.#...#.#",
"#.#.###.#.#.#####.#.#.#.#",
"#...#.#...#.....#...#.#.#",
"#.###.#########.#####.#.#",
"#.#...#.......#.....#...#",
"#.#.###.#####.#####.###.#",
"#.#.#...#...#.#...#.#...#",
"#.#.#.###.#.#.#.#.#.#.###",
"#...#.....#...#.#.#.#...#",
"#.#############.#.#.##..#",
"#.............#.#....#..#",
"#.###########.#.######..#",
"#P.........................".slice(0,24),
"########################"];
// fix row 23 length
MAP_STR[23]="#P..........#..........#";
let MW=24,MH=25;
let grid=[];
function buildGrid(){
  grid=[];
  for(let y=0;y<MH;y++){let row=[];let s=MAP_STR[y]||"#".repeat(MW);
    for(let x=0;x<MW;x++){let c=s[x]||'#';
      // force sealed border (keep EXIT gate walkable marker)
      if((x===0||y===0||x===MW-1||y===MH-1)&&c!=='E')c='#';
      if(c==='#')row.push(1+((x*7+y*13)%4===0?1:0)+((x+y)%9===0?2:0)); // varied wall ids 1..4
      else if(c==='E')row.push(5);
      else row.push(0);
    }grid.push(row);}
}
buildGrid();
function cellAt(x,y){x=Math.floor(x);y=Math.floor(y);if(x<0||y<0||x>=MW||y>=MH)return 1;return grid[y][x];}
function isSolid(x,y){let c=cellAt(x,y);return c!==0&&c!==5;} // 5 = EXIT gate, walkable
function hasLOS(x0,y0,x1,y1){
  let dx=x1-x0,dy=y1-y0;let d=Math.hypot(dx,dy);let steps=Math.ceil(d*8);
  for(let i=1;i<steps;i++){let t=i/steps;if(cellAt(x0+dx*t,y0+dy*t)>0)return false;}return true;
}
function nearestOpen(x,y){
  if(!isSolid(x,y))return {x,y};
  for(let r=.5;r<4;r+=.5)for(let a=0;a<12;a++){
    let nx=x+Math.cos(a/12*Math.PI*2)*r,ny=y+Math.sin(a/12*Math.PI*2)*r;
    if(nx>1&&ny>1&&nx<MW-1&&ny<MH-1&&!isSolid(nx,ny))return {x:Math.floor(nx)+.5,y:Math.floor(ny)+.5};
  }
  return {x:1.5,y:23.5};
}

/* ---------- TEXTURES (procedural) ---------- */
function mkTex(fn){let c=document.createElement('canvas');c.width=64;c.height=64;let g=c.getContext('2d');fn(g);return c;}
function noiseOver(g,n,a){for(let i=0;i<n;i++){let v=Math.random()>0.5?255:0;g.fillStyle=`rgba(${v},${v},${v},${(Math.random()*a).toFixed(3)})`;g.fillRect(Math.random()*64|0,Math.random()*64|0,2,2);}}
const wallTex={};
wallTex[1]=mkTex(g=>{g.fillStyle='#5a5a60';g.fillRect(0,0,64,64);g.fillStyle='#3d3d44';for(let y=0;y<64;y+=8)for(let x=0;x<64;x+=16){g.fillRect(x+((y/8)%2)*8,y,15,7);}g.fillStyle='rgba(0,0,0,.35)';for(let y=0;y<64;y+=8)g.fillRect(0,y,64,1);for(let x=0;x<64;x+=16)g.fillRect(x,0,1,64);noiseOver(g,90,.08);});
wallTex[2]=mkTex(g=>{g.fillStyle='#6b4a2e';g.fillRect(0,0,64,64);g.fillStyle='#4a2f1c';for(let y=0;y<64;y+=10)for(let x=0;x<64;x+=20){g.fillRect(x+((y/10)%2)*10,y,19,9);}g.fillStyle='#8a6238';for(let y=0;y<64;y+=10)g.fillRect(0,y,64,1);noiseOver(g,90,.08);g.fillStyle='rgba(0,0,0,.25)';g.fillRect(0,0,64,8);});
wallTex[3]=mkTex(g=>{g.fillStyle='#2a3a4a';g.fillRect(0,0,64,64);g.fillStyle='#1a2530';g.fillRect(4,4,56,56);g.fillStyle='#3d5a74';g.fillRect(8,8,48,20);g.fillStyle='#0ff';g.fillRect(10,12,20,3);g.fillStyle='#f80';g.fillRect(10,18,30,2);for(let i=0;i<6;i++){g.fillStyle=i%2?'#ff0':'#0f0';g.fillRect(10+i*7,34,4,10);}g.strokeStyle='#0ff';g.strokeRect(4.5,4.5,55,55);noiseOver(g,60,.08);});
wallTex[4]=mkTex(g=>{g.fillStyle='#3a4a3a';g.fillRect(0,0,64,64);g.fillStyle='#2a352a';for(let i=0;i<24;i++){g.fillRect(Math.random()*64|0,Math.random()*64|0,8,6);}g.fillStyle='#46583c';for(let i=0;i<30;i++){g.fillRect(Math.random()*64|0,Math.random()*64|0,3,3);}noiseOver(g,60,.06);});
wallTex[5]=mkTex(g=>{ // EXIT gate
  g.fillStyle='#301010';g.fillRect(0,0,64,64);
  g.fillStyle='#7a1010';g.fillRect(6,0,52,64);
  g.fillStyle='#a02020';for(let x=6;x<58;x+=8)g.fillRect(x,0,3,64);
  g.fillStyle='#000';g.fillRect(6,18,52,28);
  g.fillStyle='#ff2222';g.font='bold 13px monospace';g.textAlign='center';g.fillText('EXIT',32,37);
  g.fillStyle='#ffd23f';g.fillRect(6,16,52,2);g.fillRect(6,46,52,2);
});
wallTex[6]=mkTex(g=>{g.fillStyle='#222';g.fillRect(0,0,64,64);g.fillStyle='#444';g.fillRect(0,0,64,10);g.fillStyle='#666';g.fillRect(0,0,64,3);noiseOver(g,80,.2);});

/* ---------- SPRITES ---------- */
function mkSpr(w,h,fn){let c=document.createElement('canvas');c.width=w;c.height=h;let g=c.getContext('2d');g.imageSmoothingEnabled=false;fn(g,w,h);return c;}
function drawDemon(g,t,frame){
  // shadow
  g.fillStyle='rgba(0,0,0,.4)';g.beginPath();g.ellipse(32,58,20,5,0,0,7);g.fill();
  let bob=frame?2:0;
  if(t==='runt'){
    g.fillStyle='#2fae4e';g.fillRect(18,30-bob,28,26); // torso
    g.fillStyle='#1e7a34';g.fillRect(18,44-bob,28,6);
    g.fillStyle='#39d45e';g.beginPath();g.arc(32,22-bob,12,0,7);g.fill(); // head
    g.fillStyle='#ff0';g.fillRect(24,19-bob,5,5);g.fillRect(35,19-bob,5,5);
    g.fillStyle='#000';g.fillRect(26,20-bob,2,3);g.fillRect(37,20-bob,2,3);
    g.fillStyle='#fff';g.fillRect(27,34-bob,10,4);g.fillStyle='#000';for(let i=0;i<4;i++)g.fillRect(28+i*2,34-bob,1,4);
    g.fillStyle='#2fae4e';g.fillRect(10,32-bob,8,16);g.fillRect(46,32-bob,8,16); // arms
    g.fillStyle='#ff2222';g.fillRect(10,46-bob,8,4);g.fillRect(46,46-bob,8,4); // claws
    g.fillStyle='#1e7a34';g.fillRect(20,54,9,6);g.fillRect(35,54,9,6);
  }else if(t==='trooper'){
    g.fillStyle='#5a4632';g.fillRect(20,32-bob,24,22);
    g.fillStyle='#3d2f22';g.fillRect(20,44-bob,24,5);
    g.fillStyle='#c9a06a';g.beginPath();g.arc(32,22-bob,11,0,7);g.fill();
    g.fillStyle='#333';g.fillRect(20,12-bob,24,8); // helmet
    g.fillStyle='#222';g.fillRect(22,22-bob,20,4); // visor
    g.fillStyle='#f00';g.fillRect(24,23-bob,16,2);
    g.fillStyle='#5a4632';g.fillRect(10,34-bob,9,14);g.fillRect(45,34-bob,9,14);
    g.fillStyle='#222';g.fillRect(45,38-bob,10,6); // gun
    g.fillStyle='#777';g.fillRect(53,39-bob,4,3);
    g.fillStyle='#333';g.fillRect(22,54,8,6);g.fillRect(34,54,8,6);
  }else{ // brute
    g.fillStyle='#8a1a1a';g.fillRect(12,26-bob,40,30);
    g.fillStyle='#5a0d0d';g.fillRect(12,42-bob,40,7);
    g.fillStyle='#c92a2a';g.beginPath();g.arc(32,18-bob,14,0,7);g.fill();
    g.fillStyle='#ffdd00';g.fillRect(22,14-bob,7,7);g.fillRect(35,14-bob,7,7);
    g.fillStyle='#f00';g.fillRect(24,16-bob,3,4);g.fillRect(37,16-bob,3,4);
    g.fillStyle='#fff';g.fillRect(24,28-bob,16,5);
    g.fillStyle='#8a1a1a';g.fillRect(2,28-bob,12,20);g.fillRect(50,28-bob,12,20);
    g.fillStyle='#ddd';g.fillRect(4,44-bob,8,6);g.fillRect(52,44-bob,8,6);
    g.fillStyle='#5a0d0d';g.fillRect(16,54,12,6);g.fillRect(36,54,12,6);
    // horns
    g.fillStyle='#eee';g.beginPath();g.moveTo(20,10-bob);g.lineTo(12,0);g.lineTo(24,6-bob);g.fill();
    g.beginPath();g.moveTo(44,10-bob);g.lineTo(52,0);g.lineTo(40,6-bob);g.fill();
  }
}
const enemySpr={};
['runt','trooper','brute'].forEach(t=>{enemySpr[t]=[mkSpr(64,64,(g)=>drawDemon(g,t,0)),mkSpr(64,64,(g)=>drawDemon(g,t,1))];});
const deadSpr={};
['runt','trooper','brute'].forEach(t=>{deadSpr[t]=mkSpr(64,64,(g)=>{g.fillStyle='rgba(0,0,0,.4)';g.beginPath();g.ellipse(32,56,22,5,0,0,7);g.fill();g.fillStyle=t==='runt'?'#1e7a34':t==='trooper'?'#3d2f22':'#5a0d0d';g.fillRect(10,46,44,12);g.fillStyle='#a00';g.fillRect(6,50,52,6);g.fillRect(14,42,10,8);});});
const pickSpr={
  med:mkSpr(48,48,(g)=>{g.fillStyle='#ddd';g.fillRect(8,14,32,22);g.fillStyle='#a00';g.fillRect(8,14,32,6);g.fillStyle='#f00';g.fillRect(21,20,6,12);g.fillRect(15,24,18,5);g.fillStyle='#fff';g.fillRect(8,14,32,2);}),
  stim:mkSpr(48,48,(g)=>{g.fillStyle='#eee';g.fillRect(14,20,20,14);g.fillStyle='#f00';g.fillRect(22,23,4,8);g.fillRect(18,25,12,3);}),
  bul:mkSpr(48,48,(g)=>{g.fillStyle='#3a5a2a';g.fillRect(8,24,32,16);g.fillStyle='#5a7a3a';g.fillRect(8,24,32,4);g.fillStyle='#ffd23f';g.font='bold 9px monospace';g.textAlign='center';g.fillText('BULLETS',24,35);}),
  sh:mkSpr(48,48,(g)=>{g.fillStyle='#5a1a1a';g.fillRect(8,24,32,16);g.fillStyle='#a03a3a';g.fillRect(8,24,32,4);g.fillStyle='#fff';g.font='bold 9px monospace';g.textAlign='center';g.fillText('SHELLS',24,35);}),
  sg:mkSpr(64,48,(g)=>{g.fillStyle='#4a2f1c';g.fillRect(4,22,40,8);g.fillStyle='#888';g.fillRect(36,20,24,7);g.fillStyle='#333';g.fillRect(52,18,8,10);g.fillStyle='#ffd23f';g.font='bold 9px monospace';g.textAlign='center';g.fillText('SHOTGUN',32,42);}),
  cg:mkSpr(64,48,(g)=>{g.fillStyle='#222';g.fillRect(6,18,44,10);g.fillStyle='#666';g.fillRect(40,20,22,5);g.fillRect(20,28,10,12);g.fillStyle='#0ff';g.fillRect(10,20,14,3);g.fillStyle='#ffd23f';g.font='bold 9px monospace';g.textAlign='center';g.fillText('RAPID',32,44);}),
};
const exitSpr=mkSpr(64,96,(g)=>{let gr=g.createLinearGradient(0,0,0,96);gr.addColorStop(0,'#ff5a3c');gr.addColorStop(1,'#7a1010');g.fillStyle=gr;g.fillRect(14,4,36,88);g.fillStyle='#fff';g.globalAlpha=.5+Math.random()*.3;g.fillRect(22,4,8,88);g.globalAlpha=1;g.fillStyle='#000';g.fillRect(14,36,36,24);g.fillStyle='#ffd23f';g.font='bold 11px monospace';g.textAlign='center';g.fillText('EXIT',32,52);});

/* ---------- AUDIO ---------- */
let AC=null,muted=false;
function audio(){if(!AC){try{AC=new (window.AudioContext||window.webkitAudioContext)();}catch(e){}}if(AC&&AC.state==='suspended')AC.resume();return AC;}
function beep(f,d,type='square',v=.2,slide=0){if(muted)return;let a=audio();if(!a)return;let o=a.createOscillator(),g=a.createGain();o.type=type;o.frequency.setValueAtTime(f,a.currentTime);if(slide)o.frequency.exponentialRampToValueAtTime(Math.max(20,f+slide),a.currentTime+d);g.gain.setValueAtTime(v,a.currentTime);g.gain.exponentialRampToValueAtTime(.001,a.currentTime+d);o.connect(g);g.connect(a.destination);o.start();o.stop(a.currentTime+d);}
function noiseBurst(d=.2,v=.4,lp=2000){if(muted)return;let a=audio();if(!a)return;let len=a.sampleRate*d,buf=a.createBuffer(1,len,a.sampleRate),ch=buf.getChannelData(0);for(let i=0;i<len;i++)ch[i]=(Math.random()*2-1)*(1-i/len);let s=a.createBufferSource();s.buffer=buf;let f=a.createBiquadFilter();f.type='lowpass';f.frequency.value=lp;let g=a.createGain();g.gain.value=v;s.connect(f);f.connect(g);g.connect(a.destination);s.start();}
const SFX={
  pistol(){noiseBurst(.12,.5,4000);beep(220,.1,'square',.25,-150);},
  shotgun(){noiseBurst(.35,.7,2500);beep(120,.25,'sawtooth',.3,-80);},
  chaingun(){noiseBurst(.08,.35,5000);beep(300,.06,'square',.18,-120);},
  empty(){beep(900,.05,'square',.1);},
  hit(){beep(500,.05,'square',.12,-200);},
  kill(){noiseBurst(.25,.4,1200);beep(150,.3,'sawtooth',.25,-100);},
  hurt(){beep(90,.25,'sawtooth',.35,-40);noiseBurst(.15,.3,800);},
  pickup(){beep(660,.08,'square',.2);setTimeout(()=>beep(990,.12,'square',.2),70);},
  weapon(){beep(330,.1,'square',.25);setTimeout(()=>beep(495,.1,'square',.25),90);setTimeout(()=>beep(660,.15,'square',.25),180);},
  door(){beep(80,.5,'sawtooth',.25,60);noiseBurst(.4,.2,500);},
  win(){[523,659,784,1046].forEach((f,i)=>setTimeout(()=>beep(f,.25,'square',.25),i*150));},
  die(){beep(200,.8,'sawtooth',.3,-160);noiseBurst(.6,.4,600);},
  step(){noiseBurst(.04,.06,700);},
  alert(){beep(180,.15,'sawtooth',.18,80);}
};

/* ---------- GAME STATE ---------- */
const WEAPONS=[
  {name:'PISTOL',key:'1',dmg:16,cd:.32,pellets:1,spread:.025,ammo:'bul',auto:false,kick:4,range:14},
  {name:'SHOTGUN',key:'2',dmg:9,cd:.95,pellets:8,spread:.10,ammo:'sh',auto:false,kick:14,range:9},
  {name:'RAPID',key:'3',dmg:11,cd:.115,pellets:1,spread:.05,ammo:'bul',auto:true,kick:2.5,range:13},
];
const ETYPES={
  runt:{hp:30,speed:2.4,dmg:[4,11],cd:1.1,scale:.85,score:100},
  trooper:{hp:65,speed:1.7,dmg:[7,17],cd:1.5,scale:1.0,score:200},
  brute:{hp:140,speed:1.25,dmg:[14,28],cd:1.9,scale:1.3,score:400},
};
let G=null;
function spawnDefs(){
  return [
    {t:'runt',x:5.5,y:1.5},{t:'runt',x:10.5,y:1.5},{t:'trooper',x:17.5,y:1.5},
    {t:'runt',x:3.5,y:5.5},{t:'trooper',x:11.5,y:3.5},{t:'brute',x:19.5,y:3.5},
    {t:'trooper',x:5.5,y:11.5},{t:'runt',x:13.5,y:11.5},{t:'brute',x:17.5,y:13.5},
    {t:'trooper',x:7.5,y:19.5},{t:'runt',x:15.5,y:11.5},{t:'brute',x:10.5,y:21.5},
  ];
}
function pickupDefs(){
  return [
    {k:'sg',x:2.5,y:1.5,respawn:false},{k:'bul',x:4.5,y:1.5},{k:'stim',x:9.5,y:1.5},
    {k:'med',x:11.5,y:5.5},{k:'sh',x:16.5,y:3.5},{k:'bul',x:19.5,y:5.5},
    {k:'cg',x:17.5,y:11.5,respawn:false},{k:'med',x:5.5,y:9.5},{k:'sh',x:9.5,y:9.5},
    {k:'bul',x:13.5,y:9.5},{k:'med',x:17.5,y:15.5},{k:'sh',x:5.5,y:17.5},
    {k:'bul',x:11.5,y:15.5},{k:'med',x:15.5,y:19.5},{k:'sh',x:19.5,y:19.5},
    {k:'med',x:3.5,y:21.5},{k:'bul',x:7.5,y:21.5},{k:'stim',x:17.5,y:5.5},
    {k:'stim',x:5.5,y:13.5},{k:'sh',x:13.5,y:13.5},
  ];
}
function newGame(){
  G={
    state:'title',px:1.5,py:23.5,pa:Math.PI*1.5, // facing up (north)
    hp:100,maxhp:100,bul:50,sh:8,maxbul:200,maxsh:50,
    owned:[true,false,false],weap:0,cd:0,
    enemies:[],pickups:[],parts:[],floats:[],
    kills:0,total:12,shots:0,hits:0,deaths:0,
    t:0,startT:0,exitOpen:false,shake:0,bob:0,mflash:0,
    invuln:0,hitmarkT:0,msgT:0,stepT:0,animT:0,
    keys:{},firing:false,bigmap:false,paused:false,
    sens:4,quality:'HIGH',
  };
  // fix start pos: row23 col1 is P
  G.px=1.5;G.py=23.5;
  spawnDefs().forEach(s=>{let e=ETYPES[s.t];let o=nearestOpen(s.x,s.y);G.enemies.push({t:s.t,x:o.x,y:o.y,hp:e.hp,maxhp:e.hp,state:'idle',cd:Math.random(),anim:Math.random()*2,alerted:false,dead:false,dt:0,flash:0});});
  G.pickups=pickupDefs().map(p=>{let o=nearestOpen(p.x,p.y);return {...p,x:o.x,y:o.y,taken:false,respT:0,bob:Math.random()*6};});
  updateHUD();setMsg('FIND WEAPONS • KILL ALL 12 • ESCAPE',4);
}
function setMsg(t,dur=2.5){msgEl.textContent=t;G.msgT=dur;}
function updateHUD(){
  healthEl.textContent=Math.max(0,Math.ceil(G.hp));
  healthEl.classList.toggle('low',G.hp<=30);
  bulEl.textContent=G.bul;shEl.textContent=G.sh;
  killsEl.textContent=`KILLS ${G.kills}/${G.total}`;
  for(let i=0;i<3;i++){let el=document.getElementById('w'+(i+1));
    el.classList.toggle('owned',G.owned[i]);el.classList.toggle('locked',!G.owned[i]);el.classList.toggle('active',G.weap===i);}
}

/* ---------- INPUT ---------- */
const keys={};
addEventListener('keydown',e=>{
  keys[e.code]=true;
  if(!G)return;
  if(e.code==='Digit1'&&G.owned[0]){G.weap=0;SFX.weapon&&beep(440,.05,'square',.12);updateHUD();}
  if(e.code==='Digit2'&&G.owned[1]){G.weap=1;beep(440,.05,'square',.12);updateHUD();}
  if(e.code==='Digit3'&&G.owned[2]){G.weap=2;beep(440,.05,'square',.12);updateHUD();}
  if(e.code==='KeyM'){G.bigmap=!G.bigmap;}
  if(e.code==='KeyP'||e.code==='Escape'){if(G.state==='playing')togglePause();}
  if(e.code==='Space'){G.firing=true;e.preventDefault();}
});
addEventListener('keyup',e=>{keys[e.code]=false;if(e.code==='Space')G.firing=false;});
let locked=false;
cv.addEventListener('click',()=>{if(G&&G.state==='playing'&&!locked){cv.requestPointerLock&&cv.requestPointerLock();}});
document.addEventListener('pointerlockchange',()=>{locked=document.pointerLockElement===cv;document.getElementById('loadTip').style.display=locked?'none':'block';});
document.addEventListener('mousemove',e=>{if(locked&&G&&G.state==='playing'&&!G.paused){G.pa+=e.movementX*0.0022*G.sens/4;}});
cv.addEventListener('mousedown',e=>{if(e.button===0&&G){G.firing=true;if(G.state==='playing'&&!locked){cv.requestPointerLock&&cv.requestPointerLock();}}});
addEventListener('mouseup',e=>{if(e.button===0&&G)G.firing=false;});
// touch
let touchMove={x:0,y:0},touchLook=0;
(function(){
  const stick=document.getElementById('stick');let sid=null,sx=0,sy=0;
  stick.addEventListener('touchstart',e=>{e.preventDefault();let t=e.changedTouches[0];sid=t.identifier;sx=t.clientX;sy=t.clientY;},{passive:false});
  addEventListener('touchmove',e=>{for(let t of e.changedTouches){if(t.identifier===sid){touchMove.x=(t.clientX-sx)/40;touchMove.y=(t.clientY-sy)/40;}}},{passive:true});
  addEventListener('touchend',e=>{for(let t of e.changedTouches){if(t.identifier===sid){sid=null;touchMove.x=0;touchMove.y=0;}}});
  let lid=null,lx=0;
  addEventListener('touchstart',e=>{for(let t of e.changedTouches){if(t.clientX>innerWidth/2&&t.target===cv){lid=t.identifier;lx=t.clientX;}}},{passive:true});
  addEventListener('touchmove',e=>{for(let t of e.changedTouches){if(t.identifier===lid&&G){G.pa+=(t.clientX-lx)*0.008;lx=t.clientX;}}},{passive:true});
  document.getElementById('btnFire').addEventListener('touchstart',e=>{e.preventDefault();if(G)G.firing=true;},{passive:false});
  document.getElementById('btnFire').addEventListener('touchend',e=>{if(G)G.firing=false;});
  document.getElementById('btnWeap').addEventListener('touchstart',e=>{e.preventDefault();cycleWeapon();},{passive:false});
})();
function cycleWeapon(){for(let i=1;i<=3;i++){let n=(G.weap+i)%3;if(G.owned[n]){G.weap=n;updateHUD();break;}}}

/* ---------- SHOOTING ---------- */
function tryFire(dt){
  G.cd-=dt;
  if(!G.firing||G.cd>0||G.paused)return;
  let w=WEAPONS[G.weap];
  // ammo check
  if(w.ammo==='bul'&&G.bul<=0){SFX.empty();setMsg('NO BULLETS!',1);G.cd=.3;return;}
  if(w.ammo==='sh'&&G.sh<=0){SFX.empty();setMsg('NO SHELLS! FIND AMMO',1);G.cd=.3;return;}
  G.cd=w.cd;G.mflash=.07;G.shake=Math.min(12,G.shake+w.kick);
  if(w.ammo==='bul')G.bul--;else G.sh--;
  if(G.weap===0)SFX.pistol();else if(G.weap===1)SFX.shotgun();else SFX.chaingun();
  // hitscan pellets
  for(let p=0;p<w.pellets;p++){
    G.shots++;
    let spread=(Math.random()*2-1)*w.spread + (G.weap===1?(Math.random()*2-1)*w.spread:0);
    let ang=G.pa+spread;
    // find best enemy
    let best=null,bestD=w.range;
    for(let e of G.enemies){
      if(e.dead)continue;
      let dx=e.x-G.px,dy=e.y-G.py;let d=Math.hypot(dx,dy);
      if(d>w.range)continue;
      let ea=Math.atan2(dy,dx);let diff=((ea-ang+Math.PI*3)%(Math.PI*2))-Math.PI;
      let tol=Math.atan2(.35,d)+.02;
      if(Math.abs(diff)<tol){
        if(!hasLOS(G.px,G.py,e.x,e.y))continue;
        if(d<bestD){bestD=d;best=e;}
      }
    }
    if(best){
      G.hits++;
      let fall=bestD>6? .6 : 1;
      let dmg=Math.round(w.dmg*(0.85+Math.random()*0.3)*fall);
      best.hp-=dmg;best.flash=.12;best.alerted=true;
      // wake nearby
      for(let o of G.enemies){if(!o.dead&&Math.hypot(o.x-best.x,o.y-best.y)<4)o.alerted=true;}
      spawnBlood(best.x,best.y,6,Math.min(1,bestD/6));
      SFX.hit();
      G.hitmarkT=.15;
      if(best.hp<=0&&!best.dead){killEnemy(best);}
    }
  }
  updateHUD();
}
function killEnemy(e){
  e.dead=true;e.dt=0;G.kills++;
  SFX.kill();spawnBlood(e.x,e.y,22,1);
  G.shake=Math.min(10,G.shake+3);
  setMsg(['HOSTILE DOWN!','TARGET ELIMINATED!','NICE SHOT!','THREAT NEUTRALIZED!'][Math.random()*4|0],1.4);
  // drop chance
  if(Math.random()<.45){
    let k=Math.random()<.5?'stim':(Math.random()<.5?'bul':'sh');
    G.pickups.push({k,x:e.x+(Math.random()-.5)*.4,y:e.y+(Math.random()-.5)*.4,taken:false,respT:0,bob:0,drop:true});
  }
  if(G.kills>=G.total){G.exitOpen=true;SFX.door();setMsg('★ ALL CLEAR — EXIT GATE OPEN! ★',5);flashEl.style.opacity=.7;setTimeout(()=>flashEl.style.opacity=0,250);}
  updateHUD();
}
function spawnBlood(x,y,n,pw){
  for(let i=0;i<n;i++)G.parts.push({x,y,vx:(Math.random()-.5)*3*pw,vy:(Math.random()-.5)*3*pw,life:.4+Math.random()*.4,t:.4+Math.random()*.4,c:Math.random()<.7?'#c00':'#f40'});
}

/* ---------- PLAYER DAMAGE / DEATH ---------- */
function hurtPlayer(dmg,fromX,fromY){
  if(G.invuln>0||G.state!=='playing'||G.paused)return;
  G.hp-=dmg;SFX.hurt();
  G.shake=Math.min(16,G.shake+6);
  dmgEl.style.opacity=.9;setTimeout(()=>dmgEl.style.opacity=0,180);
  crossEl.classList.add('hit');setTimeout(()=>crossEl.classList.remove('hit'),150);
  if(G.hp<=0){G.hp=0;playerDie();}
  updateHUD();
}
function playerDie(){
  G.state='dead';G.deaths++;SFX.die();
  document.exitPointerLock&&document.exitPointerLock();
  document.getElementById('deathInfo').textContent=`Kills ${G.kills}/${G.total} • Survived ${fmtTime(G.t)} • Deaths: ${G.deaths}`;
  setTimeout(()=>{document.getElementById('deathScreen').classList.remove('hidden');},900);
}
function respawn(){
  document.getElementById('deathScreen').classList.add('hidden');
  G.px=1.5;G.py=23.5;G.pa=Math.PI*1.5;G.hp=100;
  G.bul=Math.max(G.bul,25);G.sh=Math.max(G.sh,4);
  G.invuln=2.5;G.state='playing';
  setMsg('REDEPLOYED — GET REVENGE!',2.5);
  updateHUD();
}

/* ---------- UPDATE ---------- */
function movePlayer(dt){
  let sp=(keys['ShiftLeft']||keys['ShiftRight'])?4.6:3.1;
  let f=0,s=0;
  if(keys['KeyW']||keys['ArrowUp'])f+=1;
  if(keys['KeyS']||keys['ArrowDown'])f-=1;
  if(keys['KeyA'])s-=1;if(keys['KeyD'])s+=1;
  if(keys['ArrowLeft'])G.pa-=2.4*dt;
  if(keys['ArrowRight'])G.pa+=2.4*dt;
  f+= -touchMove.y; s+= touchMove.x;
  let dx=Math.cos(G.pa)*f+Math.cos(G.pa+Math.PI/2)*s;
  let dy=Math.sin(G.pa)*f+Math.sin(G.pa+Math.PI/2)*s;
  let l=Math.hypot(dx,dy)||1;dx/=l;dy/=l;
  let mag=Math.min(1,Math.hypot(f,s));
  let nx=G.px+dx*sp*mag*dt, ny=G.py+dy*sp*mag*dt;
  let r=.22;
  if(!isSolid(nx+r*Math.sign(dx),G.py))G.px=nx;
  if(!isSolid(G.px,ny+r*Math.sign(dy)))G.py=ny;
  if(mag>.1){G.bob+=dt*9*mag;G.stepT-=dt;if(G.stepT<=0){G.stepT=.38;SFX.step();}}
  // pickups
  for(let p of G.pickups){
    if(p.taken)continue;
    if(Math.hypot(p.x-G.px,p.y-G.py)<.5){takePickup(p);}
  }
  // exit check
  let c=cellAt(G.px,G.py);
  if(c===5){
    if(G.exitOpen){winGame();}
    else if(G.msgT<=0){setMsg(`🔒 EXIT LOCKED — ${G.total-G.kills} HOSTILES REMAIN`,2);}
  }
  if(G.invuln>0)G.invuln-=dt;
  if(G.msgT>0)G.msgT-=dt;else if(msgEl.textContent&&G.state==='playing'&&G.msgT<=-2)msgEl.textContent='';
}
function takePickup(p){
  if(p.k==='med'){if(G.hp>=G.maxhp)return;G.hp=Math.min(G.maxhp,G.hp+40);healEl.style.opacity=1;setTimeout(()=>healEl.style.opacity=0,400);}
  else if(p.k==='stim'){if(G.hp>=G.maxhp)return;G.hp=Math.min(G.maxhp,G.hp+12);healEl.style.opacity=.7;setTimeout(()=>healEl.style.opacity=0,300);}
  else if(p.k==='bul'){if(G.bul>=G.maxbul)return;G.bul=Math.min(G.maxbul,G.bul+20);}
  else if(p.k==='sh'){if(G.sh>=G.maxsh)return;G.sh=Math.min(G.maxsh,G.sh+6);}
  else if(p.k==='sg'){if(G.owned[1]){if(G.sh>=G.maxsh)return;G.sh=Math.min(G.maxsh,G.sh+6);}else{G.owned[1]=true;G.weap=1;SFX.weapon();setMsg('★ SHOTGUN ACQUIRED — PRESS 2 ★',3);p.taken=true;updateHUD();return;}}
  else if(p.k==='cg'){if(G.owned[2]){if(G.bul>=G.maxbul)return;G.bul=Math.min(G.maxbul,G.bul+30);}else{G.owned[2]=true;G.weap=2;SFX.weapon();setMsg('★ RAPID CANNON ACQUIRED — PRESS 3 ★',3);p.taken=true;updateHUD();return;}}
  SFX.pickup();
  setMsg(p.k==='med'?'MEDKIT +40':p.k==='stim'?'+12 HP':p.k==='bul'?'+20 BULLETS':'+6 SHELLS',1.2);
  p.taken=true;updateHUD();
}
function updateEnemies(dt){
  for(let e of G.enemies){
    if(e.dead){e.dt+=dt;continue;}
    e.anim+=dt*6;if(e.flash>0)e.flash-=dt;if(e.cd>0)e.cd-=dt;
    let dx=G.px-e.x,dy=G.py-e.y,d=Math.hypot(dx,dy);
    let los=d<16&&hasLOS(e.x,e.y,G.px,G.py);
    if(los)e.alerted=true;
    if(!e.alerted)continue;
    let T=ETYPES[e.t];
    if(d<.7){ // melee bump
      if(e.cd<=0){e.cd=T.cd;hurtPlayer(Math.round(T.dmg[0]+Math.random()*(T.dmg[1]-T.dmg[0])),e.x,e.y);}
    }else if(los&&d<9){
      // strafe + approach
      let ang=Math.atan2(dy,dx);
      let want=d>3?1:0;
      let mx=Math.cos(ang)*T.speed*want*dt,my=Math.sin(ang)*T.speed*want*dt;
      // slight strafe
      let st=Math.sin(G.t*1.5+e.anim)*.5;
      mx+=Math.cos(ang+Math.PI/2)*st*dt;my+=Math.sin(ang+Math.PI/2)*st*dt;
      if(!isSolid(e.x+mx*3,e.y))e.x+=mx;
      if(!isSolid(e.x,e.y+my*3))e.y+=my;
      if(e.cd<=0){
        e.cd=T.cd*(0.9+Math.random()*.3);
        // enemy fires: hit chance falls with distance
        let hitch=e.t==='brute'?.55-d*.04:e.t==='trooper'?.45-d*.04:.35-d*.03;
        setTimeout(()=>{ // slight projectile delay feel
          if(e.dead||G.state!=='playing')return;
          if(Math.random()<Math.max(.1,hitch)){hurtPlayer(Math.round(T.dmg[0]+Math.random()*(T.dmg[1]-T.dmg[0])),e.x,e.y);
            // tracer flash on enemy
            e.flash=.1;
          }
        },180);
        e.flash=.15;
        beep2Enemy(e.t);
      }
    }else if(los){
      let ang=Math.atan2(dy,dx);
      let mx=Math.cos(ang)*T.speed*dt,my=Math.sin(ang)*T.speed*dt;
      if(!isSolid(e.x+mx*3,e.y))e.x+=mx;else if(!isSolid(e.x,e.y+my*3))e.y+=my;
      if(!isSolid(e.x,e.y+my*3))e.y+=my;
    }
  }
}
function beep2Enemy(t){if(muted)return;if(t==='brute')beep(70,.2,'sawtooth',.3);else if(t==='trooper')beep(200,.12,'square',.2,-60);else beep(350,.1,'square',.15,120);}
function fmtTime(s){s=Math.floor(s);return `${Math.floor(s/60)}:${String(s%60).padStart(2,'0')}`;}
function winGame(){
  G.state='win';SFX.win();
  document.exitPointerLock&&document.exitPointerLock();
  let acc=G.shots?Math.round(G.hits/G.shots*100):0;
  document.getElementById('stTime').textContent=fmtTime(G.t);
  document.getElementById('stKills').textContent=`${G.kills}/${G.total}`;
  document.getElementById('stAcc').textContent=acc+'%';
  document.getElementById('stDeath').textContent=G.deaths;
  let rank=acc>=60?'★ S — NIGHTMARE MARINE ★':acc>=40?'A — BADASS':acc>=25?'B — SOLID':G.deaths>2?'C — SURVIVOR':'C — TRIGGER-HAPPY';
  document.getElementById('rankText').innerHTML=`RANK: <b>${rank}</b> — shots ${G.shots}, hits ${G.hits}`;
  document.getElementById('winScreen').classList.remove('hidden');
}

/* ---------- RENDER ---------- */
let zbuf=new Float32Array(W);
let ceilGrad=null,floorGrad=null;
function makeGrads(){
  ceilGrad=ctx.createLinearGradient(0,0,0,H/2);ceilGrad.addColorStop(0,'#050508');ceilGrad.addColorStop(1,'#1a1a24');
  floorGrad=ctx.createLinearGradient(0,H/2,0,H);floorGrad.addColorStop(0,'#1c1a18');floorGrad.addColorStop(1,'#050505');
}
makeGrads();
function render(){
  // ceiling/floor
  ctx.fillStyle=ceilGrad;ctx.fillRect(0,0,W,H/2);
  ctx.fillStyle=floorGrad;ctx.fillRect(0,H/2,W,H-H/2);
  // floor detail lines (cheap perspective)
  ctx.fillStyle='rgba(255,255,255,.03)';
  for(let i=0;i<6;i++){let y=H/2+Math.pow(i/6,1.8)*(H/2);ctx.fillRect(0,y|0,W,1);}
  let dirX=Math.cos(G.pa),dirY=Math.sin(G.pa);
  let planeX=-dirY*0.73,planeY=dirX*0.73; // ~66° FOV
  let bobX=G.shake>0?(Math.random()-.5)*G.shake:0;
  let bobY=Math.sin(G.bob)*2+(G.shake>0?(Math.random()-.5)*G.shake:0);
  for(let x=0;x<W;x++){
    let cam=2*x/W-1;
    let rx=dirX+planeX*cam,ry=dirY+planeY*cam;
    let mx=G.px|0,my=G.py|0;
    let ddx=Math.abs(1/(rx||1e-9)),ddy=Math.abs(1/(ry||1e-9));
    let stepX,stepY,sdx,sdy;
    if(rx<0){stepX=-1;sdx=(G.px-mx)*ddx;}else{stepX=1;sdx=(mx+1-G.px)*ddx;}
    if(ry<0){stepY=-1;sdy=(G.py-my)*ddy;}else{stepY=1;sdy=(my+1-G.py)*ddy;}
    let side=0,hit=0,id=1,guard=0;
    while(!hit&&guard++<64){
      if(sdx<sdy){sdx+=ddx;mx+=stepX;side=0;}else{sdy+=ddy;my+=stepY;side=1;}
      if(mx<0||my<0||mx>=MW||my>=MH){hit=1;id=1;break;}
      id=grid[my][mx];if(id>0)hit=1;
    }
    let dist=side===0?sdx-ddx:sdy-ddy;
    dist=Math.max(.05,dist);
    zbuf[x]=dist;
    let lineH=Math.min(H*4,H/dist);
    let y0=(H-lineH)/2+bobY+bobX*cam*.5;
    let wallX=side===0?G.py+dist*ry:G.px+dist*rx;wallX-=Math.floor(wallX);
    let tex=wallTex[id]||wallTex[1];
    let tx=Math.floor(wallX*64);if((side===0&&rx>0)||(side===1&&ry<0))tx=63-tx;
    let shade=Math.max(.15,1-dist/14)*(side?0.72:1);
    // exit glow pulse
    if(id===5&&G.exitOpen)shade=1.3+Math.sin(G.t*6)*.3;
    ctx.drawImage(tex,tx,0,1,64,x,y0,1,lineH);
    ctx.fillStyle=`rgba(0,0,0,${(1-shade*.85).toFixed(2)})`;
    if(shade<1)ctx.fillRect(x,y0,1,lineH);
    if(shade>1){ctx.fillStyle=`rgba(255,80,30,${Math.min(.4,(shade-1)*.5)})`;ctx.fillRect(x,y0,1,lineH);}
  }
  renderSprites(bobX,bobY);
  renderParticles();
  drawWeapon(bobX,bobY);
  if(G.invuln>0&&G.state==='playing'){ctx.fillStyle=`rgba(0,200,255,${.06+Math.sin(G.t*10)*.03})`;ctx.fillRect(0,0,W,H);}
  if(G.bigmap)drawBigMap();
  drawMinimap();
}
function renderSprites(bobX,bobY){
  let items=[];
  for(let e of G.enemies){
    let dx=e.x-G.px,dy=e.y-G.py,d=Math.hypot(dx,dy);
    items.push({kind:'e',e,d});
  }
  for(let p of G.pickups){if(p.taken)continue;let dx=p.x-G.px,dy=p.y-G.py,d=Math.hypot(dx,dy);items.push({kind:'p',p,d});}
  // exit marker sprite
  items.push({kind:'x',x:21.5,y:1.5,d:Math.hypot(21.5-G.px,1.5-G.py)});
  items.sort((a,b)=>b.d-a.d);
  let dirX=Math.cos(G.pa),dirY=Math.sin(G.pa);
  let planeX=-dirY*0.73,planeY=dirX*0.73;
  for(let it of items){
    let sx,sy,img,scale=1,vc=0;
    if(it.kind==='e'){sx=it.e.x;sy=it.e.y;
      img=it.e.dead?deadSpr[it.e.t]:enemySpr[it.e.t][Math.floor(it.e.anim)%2];
      scale=ETYPES[it.e.t].scale;
      if(it.e.dead)vc=18;
    }else if(it.kind==='p'){sx=it.p.x;sy=it.p.y;img=pickSpr[it.p.k];scale=.55;vc=-6+Math.sin(G.t*3+it.p.bob)*4;}
    else{if(!G.exitOpen)continue;sx=it.x;sy=it.y;img=exitSpr;scale=1.1;}
    let rx=sx-G.px,ry=sy-G.py;
    let inv=1/(planeX*dirY-dirX*planeY);
    let tx=inv*(dirY*rx-dirX*ry),ty=inv*(-planeY*rx+planeX*ry);
    if(ty<.15)continue;
    let size=Math.abs(H/ty)*scale;
    let sx0=(W/2)*(1+tx/ty)+bobX;
    let sy0=H/2-ty*0+vc/ty*10+bobY - size*.08;
    let w=size*(img.width/64),h=size*(img.height/64);
    if(it.kind==='p'){w*=.8;h*=.8;sy0+=size*.18;}
    if(it.kind==='e'&&it.e.dead){h*=.55;sy0+=size*.22;}
    // clip per-column with zbuffer
    let x0=Math.max(0,Math.floor(sx0-w/2)),x1=Math.min(W-1,Math.ceil(sx0+w/2));
    let iw=img.width;
    for(let x=x0;x<=x1;x++){
      if(ty>=zbuf[x])continue;
      let u=Math.floor((x-(sx0-w/2))/w*iw);
      u=Math.max(0,Math.min(iw-1,u));
      ctx.drawImage(img,u,0,1,img.height,x,sy0,1,h);
    }
    // shade + flash
    if(it.kind==='e'&&!it.e.dead){
      let sh=Math.max(0,1-it.d/13);
      if(it.e.flash>0){ctx.fillStyle='rgba(255,255,255,.55)';let ww=Math.min(w,W);ctx.fillRect(Math.max(0,sx0-ww/2),sy0,Math.min(W,ww),h);}
      else if(sh<.6){ctx.fillStyle=`rgba(0,0,0,${(.55*(1-sh)).toFixed(2)})`;let ww=Math.min(w,W);if(ww>0)ctx.fillRect(Math.max(0,sx0-ww/2),sy0,Math.min(W,ww),h);}
      // hp bar for wounded
      if(it.e.hp<it.e.maxhp&&it.d<8){
        let bw=Math.min(40,w*.6);
        ctx.fillStyle='#000';ctx.fillRect(sx0-bw/2,sy0-5,bw,3);
        ctx.fillStyle='#f00';ctx.fillRect(sx0-bw/2,sy0-5,bw*(it.e.hp/it.e.maxhp),3);
      }
    }
  }
}
function renderParticles(){
  let dirX=Math.cos(G.pa),dirY=Math.sin(G.pa);
  let planeX=-dirY*0.73,planeY=dirX*0.73;
  let inv0=1/(planeX*dirY-dirX*planeY);
  for(let q of G.parts){
    let rx=q.x-G.px,ry=q.y-G.py;
    let tx=inv0*(dirY*rx-dirX*ry),ty=inv0*(-planeY*rx+planeX*ry);
    if(ty<.1||ty>6)continue;
    let sx=(W/2)*(1+tx/ty),sy=H/2+20/ty;
    if(sx<0||sx>=W||ty>=zbuf[sx|0])continue;
    ctx.fillStyle=q.c;ctx.globalAlpha=Math.max(0,q.life/q.t);
    ctx.fillRect(sx,sy,3,3);
    ctx.globalAlpha=1;
  }
}
function drawWeapon(bx,by){
  let w=G.weap,bob=Math.sin(G.bob)*4,down=G.cd>0?4:0;
  let cx=W/2+bx*.5,cy=H+by*.3+bob*.5+down;
  ctx.save();ctx.translate(cx,cy);
  if(G.mflash>0){ // muzzle flash
    ctx.fillStyle='rgba(255,220,100,.9)';
    ctx.beginPath();ctx.moveTo(-14,-78);ctx.lineTo(0,-110-Math.random()*20);ctx.lineTo(14,-78);ctx.fill();
    ctx.fillStyle='rgba(255,120,20,.7)';ctx.beginPath();ctx.arc(0,-84,16+Math.random()*8,0,7);ctx.fill();
  }
  if(w===0){ // pistol
    ctx.fillStyle='#222';ctx.fillRect(-10,-60,20,60);
    ctx.fillStyle='#555';ctx.fillRect(-7,-78,14,22);
    ctx.fillStyle='#111';ctx.fillRect(-3,-84,6,8);
    ctx.fillStyle='#7a5a3a';ctx.fillRect(-12,-44,24,20);
  }else if(w===1){ // shotgun double barrel
    ctx.fillStyle='#4a2f1c';ctx.fillRect(-16,-46,32,46);
    ctx.fillStyle='#777';ctx.fillRect(-11,-88,9,46);ctx.fillRect(2,-88,9,46);
    ctx.fillStyle='#333';ctx.fillRect(-11,-88,9,6);ctx.fillRect(2,-88,9,6);
    ctx.fillStyle='#222';ctx.fillRect(-18,-48,36,12);
  }else{ // rapid
    ctx.fillStyle='#333';ctx.fillRect(-12,-64,24,64);
    ctx.fillStyle='#666';ctx.fillRect(-6,-96,12,36);
    ctx.fillStyle='#111';ctx.fillRect(-9,-96,18,6);
    ctx.fillStyle='#0ff';ctx.fillRect(-6,-70,12,4);
    ctx.fillStyle='#222';ctx.fillRect(-20,-52,40,14);
  }
  // hands
  ctx.fillStyle='#c9a06a';ctx.fillRect(-22,-34,12,14);ctx.fillRect(10,-34,12,14);
  ctx.restore();
  // damage direction / low hp pulse
  if(G.hp<=30&&G.state==='playing'){ctx.fillStyle=`rgba(180,0,0,${.08+Math.sin(G.t*5)*.05})`;ctx.fillRect(0,0,W,H);}
}
function drawMinimap(){
  mmc.fillStyle='#000';mmc.fillRect(0,0,132,132);
  let s=132/MW;
  for(let y=0;y<MH;y++)for(let x=0;x<MW;x++){
    if(grid[y][x]>0){mmc.fillStyle=grid[y][x]===5?(G.exitOpen?'#ffd23f':'#f00'):'#555';mmc.fillRect(x*s,y*s,s+.5,s+.5);}
  }
  for(let p of G.pickups){if(p.taken)continue;mmc.fillStyle=p.k==='med'||p.k==='stim'?'#fff':p.k==='sg'||p.k==='cg'?'#0ff':'#fd3';mmc.fillRect(p.x*s-1,p.y*s-1,2.5,2.5);}
  for(let e of G.enemies){if(e.dead)continue;let d=Math.hypot(e.x-G.px,e.y-G.py);if(d>9)continue;mmc.fillStyle='#f00';mmc.fillRect(e.x*s-1.5,e.y*s-1.5,3,3);}
  // player arrow
  mmc.save();mmc.translate(G.px*s,G.py*s);mmc.rotate(G.pa);
  mmc.fillStyle='#0f0';mmc.beginPath();mmc.moveTo(5,0);mmc.lineTo(-3,-3.5);mmc.lineTo(-3,3.5);mmc.fill();mmc.restore();
  // exit marker
  mmc.fillStyle=G.exitOpen?'#ffd23f':'#f00';mmc.fillRect(21.5*s-2,1.5*s-2,4,4);
}
function drawBigMap(){
  ctx.fillStyle='rgba(0,0,0,.8)';ctx.fillRect(40,20,W-80,H-80);
  ctx.strokeStyle='#ffd23f';ctx.strokeRect(40.5,20.5,W-80,H-80);
  let s=Math.min((W-100)/MW,(H-110)/MH);
  let ox=W/2-MW*s/2,oy=45;
  for(let y=0;y<MH;y++)for(let x=0;x<MW;x++){if(grid[y][x]>0){ctx.fillStyle=grid[y][x]===5?'#f00':'#888';ctx.fillRect(ox+x*s,oy+y*s,s,s);}}
  for(let e of G.enemies){if(e.dead)continue;ctx.fillStyle='#f00';ctx.fillRect(ox+e.x*s-1,oy+e.y*s-1,2.5,2.5);}
  ctx.save();ctx.translate(ox+G.px*s,oy+G.py*s);ctx.rotate(G.pa);ctx.fillStyle='#0f0';ctx.beginPath();ctx.moveTo(5,0);ctx.lineTo(-3,-3);ctx.lineTo(-3,3);ctx.fill();ctx.restore();
  ctx.fillStyle='#ffd23f';ctx.font='8px monospace';ctx.textAlign='center';ctx.fillText('— AUTOMAP (M TO CLOSE) —',W/2,34);
}

/* ---------- LOOP ---------- */
let last=0;
function loop(ts){
  requestAnimationFrame(loop);
  if(!G||G.state==='title')return;
  let dt=Math.min(.05,(ts-last)/1000||.016);last=ts;
  if(G.state==='playing'&&!G.paused){
    G.t+=dt;G.animT+=dt;
    if(G.mflash>0)G.mflash-=dt;
    if(G.hitmarkT>0){G.hitmarkT-=dt;hitEl.style.opacity=G.hitmarkT>0?1:0;}
    if(G.shake>0)G.shake=Math.max(0,G.shake-dt*30);
    tryFire(dt);
    movePlayer(dt);
    updateEnemies(dt);
    // particles
    for(let i=G.parts.length-1;i>=0;i--){let q=G.parts[i];q.life-=dt;if(q.life<=0){G.parts.splice(i,1);continue;}q.x+=q.vx*dt;q.y+=q.vy*dt;}
    for(let p of G.pickups)p.bob+=dt*3;
  }
  render();
}

/* ---------- UI ---------- */
function togglePause(){
  G.paused=!G.paused;
  document.getElementById('pauseScreen').classList.toggle('hidden',!G.paused);
  if(G.paused)document.exitPointerLock&&document.exitPointerLock();
}
document.getElementById('startBtn').onclick=()=>{audio();newGame();G.state='playing';G.startT=performance.now();document.getElementById('titleScreen').classList.add('hidden');cv.requestPointerLock&&cv.requestPointerLock();last=performance.now();setMsg('MISSION START — FIND THE SHOTGUN!',3);};
document.getElementById('respawnBtn').onclick=()=>respawn();
document.getElementById('restartBtn').onclick=()=>{document.getElementById('deathScreen').classList.add('hidden');newGame();G.state='playing';last=performance.now();};
document.getElementById('againBtn').onclick=()=>{document.getElementById('winScreen').classList.add('hidden');newGame();G.state='playing';last=performance.now();};
document.getElementById('resumeBtn').onclick=()=>togglePause();
document.getElementById('sens').oninput=e=>{if(G)G.sens=+e.target.value;};
document.getElementById('qBtn').onclick=e=>{if(!G)return;G.quality=G.quality==='HIGH'?'LOW':'HIGH';e.target.textContent=G.quality;};
document.getElementById('btnWeap').onclick=()=>{if(G&&G.state==='playing')cycleWeapon();};
document.getElementById('btnAct').onclick=()=>{};

// idle title background render
newGame();
requestAnimationFrame(loop);
// render one frame behind title for vibe
(function titleBg(){if(G.state==='title'){G.t+=.016;G.pa+=.002;render();requestAnimationFrame(titleBg);}})();
</script>
</body>
</html>
<!-- agent-meta {"model":"meta/muse-spark-1.3-contributor","provider":"openrouter","persona":"full","sessionId":"e743d74d-9628-4068-ae55-aa107b74b2ea","tokensIn":648128,"tokensOut":32225,"tokensTotal":680353,"cost":0.024468581999999996,"turns":19,"toolCalls":18,"failedToolCalls":0,"timestamp":"2026-09-03T04:44:46.625Z"} -->