← DOOM results

DOOM

muse spark 1.2 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 48,524 bytes · SHA-256 65ff5a2a8abc
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>RETRIBUTION — Retro FPS</title>
<link href="https://fonts.googleapis.com/css2?family=Black+Ops+One&family=Share+Tech+Mono&family=Teko:wght@400;600;700&display=swap" rel="stylesheet">
<style>
*{margin:0;padding:0;box-sizing:border-box}
:root{--red:#ff2a2a;--amber:#ffb700;--cyan:#00e5ff;--bg:#0a0a0f}
body{background:#050508;color:#e8e8e8;font-family:'Share Tech Mono',monospace;overflow:hidden;height:100vh;height:100dvh;display:flex;flex-direction:column}
a{color:inherit}
/* HEADER */
.topbar{height:42px;background:#111118;border-bottom:2px solid #2a2a3a;display:flex;align-items:center;justify-content:space-between;padding:0 14px;flex-shrink:0;z-index:10}
.logo{font-family:'Black Ops One',cursive;font-size:22px;letter-spacing:2px;color:#ff2a2a;text-shadow:0 0 10px rgba(255,42,42,.6),2px 2px 0 #000}
.logo span{color:#ffb700}
.topstats{display:flex;gap:18px;align-items:center;font-size:11px;letter-spacing:1px}
.stat{display:flex;align-items:center;gap:6px}
.stat b{font-family:'Teko',sans-serif;font-size:18px;line-height:1}
.dot{width:8px;height:8px;border-radius:50%;background:var(--red);box-shadow:0 0 6px var(--red);animation:pulse 1s infinite}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.5}}
/* MAIN LAYOUT */
.main{flex:1;display:flex;overflow:hidden;min-height:0}
.left{flex:1;display:flex;flex-direction:column;min-width:0;background:#000;position:relative}
.canvas-wrap{flex:1;position:relative;overflow:hidden;background:#000;display:flex;align-items:center;justify-content:center}
#gameCanvas{image-rendering:pixelated;image-rendering:crisp-edges;width:100%;height:100%;display:block;max-width:100%;max-height:100%;object-fit:contain}
#uiCanvas{position:absolute;inset:0;width:100%;height:100%;pointer-events:none}
/* overlays */
.screen-overlay{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,.82);backdrop-filter:blur(2px);z-index:5}
.hidden{display:none !important}
.panel{background:linear-gradient(180deg,#1a1a24 0%,#0f0f14 100%);border:2px solid #3a3a4a;box-shadow:0 0 30px rgba(0,0,0,.9),inset 0 1px 0 rgba(255,255,255,.08);padding:28px 32px;max-width:520px;width:92%;text-align:center;position:relative}
.panel::before{content:'';position:absolute;top:-2px;left:-2px;right:-2px;height:4px;background:linear-gradient(90deg,var(--red),var(--amber),var(--red))}
.panel h1{font-family:'Black Ops One',cursive;font-size:38px;line-height:1;color:var(--red);text-shadow:2px 2px 0 #000,0 0 20px rgba(255,42,42,.5);margin-bottom:6px}
.panel h2{font-family:'Black Ops One',cursive;font-size:28px;color:#ffb700;margin-bottom:8px}
.panel p{font-size:12px;line-height:1.6;color:#aaa;margin:8px 0}
.kbd{display:inline-block;background:#2a2a3a;border:1px solid #444;border-bottom-width:3px;border-radius:4px;padding:2px 7px;font-size:11px;color:#fff;margin:0 1px}
.btn{display:inline-block;margin-top:14px;background:linear-gradient(180deg,#ff3b3b,#c00000);border:2px solid #ff6b6b;color:#fff;font-family:'Black Ops One',cursive;font-size:16px;letter-spacing:1px;padding:10px 28px;cursor:pointer;text-transform:uppercase;box-shadow:0 4px 0 #7a0000,0 6px 20px rgba(255,0,0,.3);transition:transform .08s,box-shadow .08s}
.btn:active{transform:translateY(3px);box-shadow:0 1px 0 #7a0000}
.btn.secondary{background:linear-gradient(180deg,#3a3a4a,#1e1e2a);border-color:#5a5a6a;box-shadow:0 4px 0 #111,0 6px 20px rgba(0,0,0,.4)}
.btn.secondary:active{box-shadow:0 1px 0 #111}
.stats-grid{display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px;margin:14px 0}
.stat-box{background:rgba(0,0,0,.5);border:1px solid #333;padding:10px}
.stat-box b{font-family:'Teko',sans-serif;font-size:22px;color:var(--amber);display:block;line-height:1}
.stat-box span{font-size:10px;color:#888;letter-spacing:1px}
/* HUD bottom */
.hud{height:108px;background:linear-gradient(180deg,#1c1c2a 0%,#0e0e14 100%);border-top:3px solid #2a2a3a;display:flex;align-items:stretch;flex-shrink:0;position:relative;overflow:hidden}
.hud::before{content:'';position:absolute;top:0;left:0;right:0;height:1px;background:linear-gradient(90deg,transparent,var(--red),transparent);opacity:.5}
.hud-face{width:110px;background:#0a0a0f;border-right:2px solid #2a2a3a;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;flex-shrink:0}
.face{width:64px;height:64px;background:radial-gradient(circle at 30% 30%,#3a3a4a,#0a0a0f);border:2px solid #444;border-radius:4px;display:flex;align-items:center;justify-content:center;font-size:32px;position:relative;overflow:hidden}
.face::after{content:'';position:absolute;inset:0;background:linear-gradient(180deg,transparent 60%,rgba(0,0,0,.5));pointer-events:none}
.health-bar{width:80px;height:8px;background:#000;border:1px solid #444;position:relative;overflow:hidden}
.health-fill{height:100%;background:linear-gradient(90deg,#ff2a2a,#ff6b6b);transition:width .3s}
.hud-weapons{flex:1;display:flex;gap:6px;padding:10px;align-items:center;justify-content:center}
.wslot{flex:1;max-width:150px;background:rgba(0,0,0,.6);border:2px solid #333;border-radius:6px;padding:8px 10px;display:flex;align-items:center;gap:10px;cursor:pointer;transition:all .15s;position:relative;overflow:hidden}
.wslot.active{border-color:var(--amber);background:linear-gradient(180deg,rgba(255,183,0,.15),rgba(0,0,0,.6));box-shadow:0 0 12px rgba(255,183,0,.3)}
.wslot.active::after{content:'';position:absolute;bottom:0;left:0;right:0;height:2px;background:var(--amber)}
.wicon{width:40px;height:40px;background:#111;border:1px solid #444;border-radius:4px;display:flex;align-items:center;justify-content:center;font-size:20px;flex-shrink:0}
.winfo{flex:1;min-width:0}
.winfo strong{font-family:'Teko',sans-serif;font-size:13px;line-height:1;display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.winfo span{font-size:9px;color:#888}
.ammo-count{font-family:'Teko',sans-serif;font-size:18px;color:var(--amber);line-height:1;flex-shrink:0}
.ammo-count.low{color:var(--red);animation:blink .5s infinite}
@keyframes blink{50%{opacity:.4}}
.hud-right{width:220px;border-left:2px solid #2a2a3a;display:flex;flex-direction:column;background:rgba(0,0,0,.3);flex-shrink:0}
.hud-row{display:flex;justify-content:space-between;align-items:center;padding:6px 10px;border-bottom:1px solid #1e1e2a;font-size:11px}
.hud-row b{font-family:'Teko',sans-serif;font-size:16px}
.minimap-wrap{flex:1;padding:6px;display:flex;align-items:center;justify-content:center;background:#000;position:relative;overflow:hidden}
#minimap{image-rendering:pixelated;width:100%;height:100%;max-height:64px;object-fit:contain;border:1px solid #222}
/* RIGHT SIDEBAR */
.sidebar{width:260px;background:#0f0f14;border-left:2px solid #2a2a3a;display:flex;flex-direction:column;flex-shrink:0;overflow:hidden}
.side-head{padding:12px 14px;border-bottom:1px solid #222;display:flex;align-items:center;justify-content:space-between}
.side-head h3{font-family:'Teko',sans-serif;font-size:14px;letter-spacing:2px;color:var(--amber)}
.side-body{flex:1;overflow:auto;padding:12px;display:flex;flex-direction:column;gap:12px}
.card{background:rgba(255,255,255,.04);border:1px solid #2a2a3a;border-radius:6px;padding:10px}
.card h4{font-size:10px;letter-spacing:1.5px;color:#888;margin-bottom:8px;display:flex;align-items:center;gap:6px}
.ctrl-grid{display:grid;grid-template-columns:1fr 1fr;gap:6px}
.ctrl{font-size:11px;background:#000;border:1px solid #222;border-radius:4px;padding:6px 8px;display:flex;justify-content:space-between;align-items:center}
.ctrl kbd{background:#1a1a2a;border:1px solid #333;border-bottom-width:2px;border-radius:3px;padding:1px 5px;font-size:10px}
.enemy-list{display:flex;flex-direction:column;gap:6px}
.enemy-row{display:flex;align-items:center;gap:8px;background:#000;border:1px solid #222;border-radius:4px;padding:6px 8px;font-size:11px}
.enemy-row .bar{flex:1;height:4px;background:#111;border-radius:2px;overflow:hidden}
.enemy-row .fill{height:100%;background:var(--red)}
.log{font-size:10px;line-height:1.5;color:#888;max-height:110px;overflow:auto;background:#000;border:1px solid #1a1a2a;border-radius:4px;padding:6px}
.log b{color:var(--amber)}
.crosshair{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);width:20px;height:20px;pointer-events:none;z-index:2;opacity:.9}
.crosshair::before,.crosshair::after{content:'';position:absolute;background:var(--amber);box-shadow:0 0 4px var(--amber)}
.crosshair::before{left:9px;top:0;width:2px;height:20px}
.crosshair::after{left:0;top:9px;width:20px;height:2px}
.crosshair i{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);width:6px;height:6px;border:1px solid var(--amber);border-radius:50%;background:rgba(255,183,0,.3)}
.hit-flash{position:absolute;inset:0;background:rgba(255,0,0,.25);pointer-events:none;opacity:0;transition:opacity .12s;z-index:3}
.hit-flash.on{opacity:1}
.muzzle{position:absolute;left:50%;bottom:22%;transform:translateX(-50%);width:80px;height:80px;pointer-events:none;z-index:2;opacity:0;transition:opacity .05s}
.muzzle.on{opacity:1}
.scanlines{position:absolute;inset:0;pointer-events:none;z-index:2;opacity:.07;background:repeating-linear-gradient(0deg,transparent,transparent 2px,rgba(255,255,255,.8) 2px,rgba(255,255,255,.8) 3px)}
.vignette{position:absolute;inset:0;pointer-events:none;z-index:2;background:radial-gradient(ellipse at center,transparent 60%,rgba(0,0,0,.7) 100%)}
@media(max-width:900px){
  .sidebar{display:none}
  .hud{height:96px}
  .hud-right{width:140px}
  .hud-face{width:80px}
  .face{width:48px;height:48px;font-size:24px}
  .winfo strong{font-size:12px}
  .wicon{width:32px;height:32px;font-size:16px}
}
@media(max-width:600px){
  .topstats span{display:none}
  .hud-weapons{gap:4px;padding:6px}
  .wslot{padding:6px}
}
</style>
</head>
<body>
<div class="topbar">
  <div class="logo">RETRIBUTION <span>1994</span></div>
  <div class="topstats">
    <div class="stat"><span class="dot"></span> SECTOR 7</div>
    <div class="stat">ENEMIES <b id="topEnemies" style="color:var(--red)">—</b></div>
    <div class="stat">TIME <b id="topTime" style="color:var(--cyan)">00:00</b></div>
    <div class="stat" style="gap:4px"><span style="opacity:.6">HP</span> <b id="topHp" style="color:#ff4d4d">100</b></div>
  </div>
</div>

<div class="main">
  <div class="left">
    <div class="canvas-wrap" id="canvasWrap">
      <canvas id="gameCanvas" width="640" height="360"></canvas>
      <canvas id="uiCanvas" width="640" height="360"></canvas>
      <div class="crosshair"><i></i></div>
      <div class="hit-flash" id="hitFlash"></div>
      <div class="scanlines"></div>
      <div class="vignette"></div>
      <canvas id="muzzle" class="muzzle" width="80" height="80"></canvas>

      <!-- START SCREEN -->
      <div class="screen-overlay" id="startScreen">
        <div class="panel">
          <div style="font-size:10px;letter-spacing:4px;color:var(--amber);margin-bottom:6px">ID SOFTWARE TRIBUTE • SHAREWARE EDITION</div>
          <h1>RETRIBUTION</h1>
          <div style="font-size:11px;letter-spacing:3px;color:#666;margin-bottom:14px">— ENTER THE COMPLEX —</div>
          <p>Demons have overrun the Phobos research facility. You are the last marine. <b style="color:#fff">Find the exit portal</b> after eliminating the horde. Don't die.</p>
          <div class="stats-grid">
            <div class="stat-box"><b>3</b><span>WEAPONS</span></div>
            <div class="stat-box"><b id="previewEnemies">8</b><span>ENEMIES</span></div>
            <div class="stat-box"><b>60s</b><span>PAR TIME</span></div>
          </div>
          <p style="font-size:10px;color:#666">WASD move • Mouse drag / ← → turn • Click shoot • 1-2-3 weapons • SHIFT sprint</p>
          <button class="btn" id="playBtn">▶ ENGAGE</button>
          <p style="font-size:10px;color:#555;margin-top:10px">Tip: Shotgun shreds at close range. Vulcan eats ammo. Watch your corners.</p>
        </div>
      </div>

      <!-- DEATH SCREEN -->
      <div class="screen-overlay hidden" id="deathScreen">
        <div class="panel" style="border-color:#5a0000">
          <h1 style="color:#ff2a2a;font-size:42px">YOU DIED</h1>
          <p style="color:#ff6b6b">Your corpse will be repurposed for experiments.</p>
          <div class="stats-grid">
            <div class="stat-box"><b id="deathKills">0</b><span>KILLS</span></div>
            <div class="stat-box"><b id="deathTime">00:00</b><span>TIME</span></div>
            <div class="stat-box"><b id="deathFound">0/2</b><span>SECRETS</span></div>
          </div>
          <button class="btn" id="respawnBtn">↻ RESPAWN</button>
          <button class="btn secondary" id="restartBtn2" style="margin-left:8px">RESTART</button>
        </div>
      </div>

      <!-- WIN SCREEN -->
      <div class="screen-overlay hidden" id="winScreen">
        <div class="panel" style="border-color:#b89600">
          <div style="font-size:42px">★</div>
          <h2>MISSION COMPLETE</h2>
          <p style="color:var(--amber)">Sector 7 secured. The portal is open. You live to fight again, marine.</p>
          <div class="stats-grid">
            <div class="stat-box"><b id="winKills">0/0</b><span>KILLS</span></div>
            <div class="stat-box"><b id="winTime">00:00</b><span>TIME</span></div>
            <div class="stat-box"><b id="winHealth">100</b><span>HEALTH LEFT</span></div>
          </div>
          <div id="rankText" style="font-family:'Black Ops One',cursive;font-size:20px;color:var(--amber);margin-top:6px"></div>
          <button class="btn" id="againBtn">PLAY AGAIN</button>
        </div>
      </div>

      <!-- PAUSE / MESSAGE -->
      <div id="centerMsg" style="position:absolute;top:18%;left:50%;transform:translateX(-50%);z-index:4;pointer-events:none;text-align:center"></div>
    </div>

    <div class="hud">
      <div class="hud-face">
        <div class="face" id="face">😤</div>
        <div style="font-size:9px;letter-spacing:1px;color:#888">HEALTH</div>
        <div class="health-bar"><div class="health-fill" id="healthFill" style="width:100%"></div></div>
        <div style="font-family:'Teko',sans-serif;font-size:16px;line-height:1" id="healthNum">100%</div>
      </div>
      <div class="hud-weapons" id="hudWeapons">
        <!-- injected -->
      </div>
      <div class="hud-right">
        <div class="hud-row"><span style="color:#888">ARMOR</span> <b id="armorNum" style="color:var(--cyan)">0</b></div>
        <div class="hud-row"><span style="color:#888">KILLS</span> <b id="killNum" style="color:var(--red)">0 / 0</b></div>
        <div class="minimap-wrap"><canvas id="minimap" width="100" height="100"></canvas></div>
      </div>
    </div>
  </div>

  <div class="sidebar">
    <div class="side-head"><h3>TACTICAL READOUT</h3><span style="font-size:10px;color:#555">MAP • LOG</span></div>
    <div class="side-body">
      <div class="card">
        <h4>⌖ CONTROLS</h4>
        <div class="ctrl-grid">
          <div class="ctrl"><span>Move</span><kbd>WASD</kbd></div>
          <div class="ctrl"><span>Turn</span><kbd>← →</kbd></div>
          <div class="ctrl"><span>Sprint</span><kbd>SHIFT</kbd></div>
          <div class="ctrl"><span>Shoot</span><kbd>CLICK</kbd></div>
          <div class="ctrl"><span>Weapons</span><kbd>1 2 3</kbd></div>
          <div class="ctrl"><span>Interact</span><kbd>E</kbd></div>
        </div>
      </div>
      <div class="card">
        <h4>👹 HOSTILES</h4>
        <div class="enemy-list" id="enemyList"></div>
      </div>
      <div class="card">
        <h4>📟 COMBAT LOG</h4>
        <div class="log" id="combatLog"><div><b>SYSTEM:</b> Facility breach detected. Good luck, marine.</div></div>
      </div>
      <div class="card" style="background:linear-gradient(135deg,rgba(255,42,42,.12),rgba(255,183,0,.08));border-color:#3a2a00">
        <h4 style="color:var(--amber)">◉ OBJECTIVE</h4>
        <div style="font-size:11px;line-height:1.5;color:#ccc">
          <div id="obj1">○ Eliminate all hostiles</div>
          <div id="obj2">○ Reach the EXIT portal (north-east)</div>
          <div style="margin-top:6px;font-size:10px;color:#888">Pickups: <span style="color:#4caf50">✚ Health</span> • <span style="color:var(--amber)">⬢ Ammo</span> • <span style="color:var(--cyan)">⬣ Armor</span></div>
        </div>
      </div>
    </div>
  </div>
</div>

<script>
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,0,0,0,0,1,0,1],
  [1,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1],
  [1,0,0,1,1,0,0,1,1,1,0,0,0,1,0,1],
  [1,0,0,1,0,0,0,0,0,1,0,0,0,0,0,1],
  [1,0,0,1,0,0,0,0,0,1,0,1,1,1,0,1],
  [1,0,0,0,0,0,1,1,0,0,0,1,0,0,0,1],
  [1,0,0,0,0,0,1,1,0,0,0,1,0,0,0,1],
  [1,1,1,0,0,0,0,0,0,0,0,0,0,1,0,1],
  [1,0,0,0,0,1,1,0,0,1,1,1,0,1,0,1],
  [1,0,0,0,0,1,1,0,0,0,0,0,0,0,0,1],
  [1,0,1,1,0,0,0,0,0,0,0,0,0,1,0,1],
  [1,0,1,0,0,0,0,0,0,1,0,1,0,1,0,1],
  [1,0,0,0,0,0,0,1,0,1,0,1,0,0,0,1],
  [1,0,0,0,0,0,0,1,0,0,0,1,0,0,9,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 TILE = 1;

const WEAPONS = [
  {id:0,name:'BLASTER', short:'P', icon:'◉', damage:18, pellets:1, spread:0.015, fireDelay:320, range:14, ammo:'light', use:1, color:'#00e5ff', sound:220},
  {id:1,name:'SHREDDER', short:'S', icon:'⫷', damage:9, pellets:7, spread:0.13, fireDelay:780, range:10, ammo:'shell', use:2, color:'#ff6b00', sound:110},
  {id:2,name:'VULCAN', short:'V', icon:'⚙', damage:11, pellets:1, spread:0.045, fireDelay:95, range:13, ammo:'heavy', use:1, color:'#ff2a2a', sound:440},
];

const ENEMY_DEFS = [
  {hp:35, speed:0.022, dmg:7, color:'#ff3b3b', char:'👹', name:'GRUNT', score:100},
  {hp:65, speed:0.016, dmg:12, color:'#ff7b00', char:'👺', name:'BRUISER', score:200},
  {hp:30, speed:0.032, dmg:5, color:'#c800ff', char:'👾', name:'WRAITH', score:150},
];

let player, enemies, pickups, particles, startTime, killCount, gameState, lastShot, keys, mouseDX, animFrame;
let exitOpen = false;

function resetGame(){
  player = {
    x: 2.5, y: 2.5, angle: 0.4,
    health:100, armor:0, maxHealth:100,
    ammo:{light:60, shell:18, heavy:90},
    weapon:0, bob:0, pitch:0
  };
  // enemies placements
  enemies = [
    {x:5.5,y:4.5, hp:35, maxHp:35, type:0, alive:true, cooldown:0, ang:0, hitFlash:0},
    {x:8.5,y:3.5, hp:65, maxHp:65, type:1, alive:true, cooldown:0, ang:0, hitFlash:0},
    {x:10.5,y:6.5, hp:30, maxHp:30, type:2, alive:true, cooldown:0, ang:0, hitFlash:0},
    {x:4.5,y:10.5, hp:35, maxHp:35, type:0, alive:true, cooldown:0, ang:0, hitFlash:0},
    {x:12.5,y:10.5, hp:65, maxHp:65, type:1, alive:true, cooldown:0, ang:0, hitFlash:0},
    {x:7.5,y:12.5, hp:30, maxHp:30, type:2, alive:true, cooldown:0, ang:0, hitFlash:0},
    {x:13.5,y:7.5, hp:35, maxHp:35, type:0, alive:true, cooldown:0, ang:0, hitFlash:0},
    {x:3.5,y:13.5, hp:35, maxHp:35, type:0, alive:true, cooldown:0, ang:0, hitFlash:0},
  ];
  pickups = [
    {x:3.5,y:5.5,type:'health', val:30, taken:false, bob:0},
    {x:9.5,y:5.5,type:'armor', val:35, taken:false, bob:1},
    {x:6.5,y:8.5,type:'ammo', ammo:'shell', val:12, taken:false, bob:2},
    {x:11.5,y:3.5,type:'ammo', ammo:'heavy', val:40, taken:false, bob:3},
    {x:2.5,y:10.5,type:'health', val:25, taken:false, bob:0.5},
    {x:10.5,y:13.5,type:'ammo', ammo:'light', val:30, taken:false, bob:1.5},
    {x:13.5,y:12.5,type:'health', val:30, taken:false, bob:2.2},
    {x:7.5,y:6.5,type:'armor', val:25, taken:false, bob:0.8},
  ];
  particles=[];
  killCount=0;
  startTime=performance.now();
  lastShot=0;
  exitOpen=false;
  gameState='playing';
  document.getElementById('winScreen').classList.add('hidden');
  document.getElementById('deathScreen').classList.add('hidden');
  updateHUD();
  log('SYSTEM: Sector 7 re-initialized. Weapons hot.', 'SYSTEM');
}

const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const uiCanvas = document.getElementById('uiCanvas');
const uctx = uiCanvas.getContext('2d');
const mini = document.getElementById('minimap');
const mctx = mini.getContext('2d');
const muzzleCanvas = document.getElementById('muzzle');
const mzctx = muzzleCanvas.getContext('2d');

keys = {};
mouseDX = 0;
let isDragging=false, lastMX=0;

window.addEventListener('keydown', e=>{
  keys[e.key.toLowerCase()] = true;
  if(['1','2','3'].includes(e.key)){
    player.weapon = parseInt(e.key)-1;
    updateHUD();
    showCenterMsg(`▶ ${WEAPONS[player.weapon].name}`, '#ffb700');
  }
  if(e.key.toLowerCase()==='e') tryInteract();
  if(e.key==='Escape' && gameState==='playing'){
    // toggle start overlay as pause
  }
});
window.addEventListener('keyup', e=> keys[e.key.toLowerCase()]=false);

canvas.addEventListener('mousedown', e=>{
  if(gameState!=='playing') return;
  isDragging=true; lastMX=e.clientX;
  shoot();
});
window.addEventListener('mouseup', ()=> isDragging=false);
canvas.addEventListener('mousemove', e=>{
  if(!isDragging || gameState!=='playing') return;
  let dx = e.clientX - lastMX; lastMX=e.clientX;
  player.angle += dx * 0.008;
});
canvas.addEventListener('touchstart', e=>{
  if(gameState!=='playing') return;
  isDragging=true; lastMX=e.touches[0].clientX;
  shoot();
},{passive:true});
canvas.addEventListener('touchmove', e=>{
  if(!isDragging) return;
  let dx = e.touches[0].clientX - lastMX; lastMX=e.touches[0].clientX;
  player.angle += dx*0.008;
},{passive:true});
window.addEventListener('touchend', ()=> isDragging=false);
canvas.addEventListener('click', ()=>{
  if(gameState==='playing') shoot();
});

// buttons
document.getElementById('playBtn').onclick=()=>{
  document.getElementById('startScreen').classList.add('hidden');
  resetGame();
  if(!animFrame) loop();
};
document.getElementById('respawnBtn').onclick=()=>{
  resetGame();
};
document.getElementById('restartBtn2').onclick=()=>{
  document.getElementById('deathScreen').classList.add('hidden');
  document.getElementById('startScreen').classList.remove('hidden');
};
document.getElementById('againBtn').onclick=()=>{
  document.getElementById('winScreen').classList.add('hidden');
  document.getElementById('startScreen').classList.remove('hidden');
};

function tryInteract(){
  // exit at 14,14
  let d = Math.hypot(player.x-14.5, player.y-14.5);
  if(d<1.4){
    if(killCount >= enemies.length){
      winGame();
    } else {
      showCenterMsg(`EXIT LOCKED — CLEAR HOSTILES (${killCount}/${enemies.length})`, '#ff2a2a');
      shake(6);
    }
  }
}

function showCenterMsg(text, color='#fff'){
  const el=document.getElementById('centerMsg');
  el.innerHTML=`<div style="background:rgba(0,0,0,.75);border:1px solid ${color};color:${color};font-family:'Black Ops One',cursive;font-size:14px;letter-spacing:1px;padding:8px 16px;box-shadow:0 0 12px ${color}66">${text}</div>`;
  setTimeout(()=> el.innerHTML='', 1600);
}
function log(msg, who=''){
  const l=document.getElementById('combatLog');
  const d=document.createElement('div');
  d.innerHTML = who? `<b>${who}:</b> ${msg}` : msg;
  l.appendChild(d); l.scrollTop=l.scrollHeight;
  while(l.children.length>40) l.removeChild(l.firstChild);
}
function shake(intensity){
  const w=document.getElementById('canvasWrap');
  w.animate([{transform:`translate(${intensity}px,${intensity/2}px)`},{transform:'translate(0,0)'}],{duration:120});
}

let audioCtx;
function beep(freq, dur=0.08, type='square', vol=0.2){
  try{
    if(!audioCtx) audioCtx=new (window.AudioContext||window.webkitAudioContext)();
    if(audioCtx.state==='suspended') audioCtx.resume();
    const o=audioCtx.createOscillator(), g=audioCtx.createGain();
    o.type=type; o.frequency.value=freq;
    g.gain.value=vol;
    o.connect(g); g.connect(audioCtx.destination);
    o.start();
    g.gain.exponentialRampToValueAtTime(0.0001, audioCtx.currentTime+dur);
    o.stop(audioCtx.currentTime+dur);
  }catch(e){}
}

function isWall(x,y){
  let ix=Math.floor(x), iy=Math.floor(y);
  if(iy<0||iy>=MAP_H||ix<0||ix>=MAP_W) return true;
  let v=MAP[iy][ix];
  return v===1 || (v===9 && !exitOpen);
}
function hasLOS(x0,y0,x1,y1){
  let dx=x1-x0, dy=y1-y0, dist=Math.hypot(dx,dy), steps=Math.ceil(dist*8);
  for(let i=1;i<steps;i++){
    let t=i/steps, x=x0+dx*t, y=y0+dy*t;
    if(isWall(x,y)) return false;
  }
  return true;
}

function shoot(){
  if(gameState!=='playing') return;
  let now=performance.now();
  let w=WEAPONS[player.weapon];
  if(now-lastShot < w.fireDelay) return;
  if(player.ammo[w.ammo] < w.use){
    showCenterMsg('OUT OF AMMO!', '#ff2a2a');
    beep(80,0.15,'square',0.15);
    lastShot=now;
    return;
  }
  player.ammo[w.ammo]-=w.use;
  lastShot=now;
  // muzzle flash
  flashMuzzle(w.color);
  beep(w.sound + Math.random()*40, w.id===2?0.06:0.12, w.id===1?'sawtooth':'square', 0.18);
  if(w.id===1) beep(w.sound*0.5,0.18,'square',0.12);
  // screen kick
  player.pitch = w.id===1? -0.04 : -0.015;
  // ray pellets
  let hitSomething=false;
  for(let p=0;p<w.pellets;p++){
    let spread = (Math.random()-0.5)*w.spread*2;
    let ang = player.angle + spread;
    // find closest enemy hit
    let best=null, bestDist=Infinity;
    for(let e of enemies){
      if(!e.alive) continue;
      // project enemy to ray
      let dx=e.x-player.x, dy=e.y-player.y;
      let dist=Math.hypot(dx,dy);
      if(dist>w.range) continue;
      let aTo = Math.atan2(dy,dx);
      let diff = Math.atan2(Math.sin(aTo-ang), Math.cos(aTo-ang));
      if(Math.abs(diff) > 0.35) continue; // ~20deg tolerance scaled by distance
      // LOS check
      if(!hasLOS(player.x, player.y, e.x, e.y)) continue;
      // wall distance along ray
      let wallDist = castSingleRay(ang);
      if(wallDist < dist) continue;
      if(dist < bestDist){ bestDist=dist; best=e; }
    }
    if(best){
      best.hp -= w.damage;
      best.hitFlash=1;
      hitSomething=true;
      // knockback
      let nx = Math.cos(ang)*0.12, ny=Math.sin(ang)*0.12;
      if(!isWall(best.x+nx, best.y)) best.x+=nx;
      if(!isWall(best.x, best.y+ny)) best.y+=ny;
      addParticles(best.x, best.y, best.alive?6:12, ENEMY_DEFS[best.type].color);
      if(best.hp<=0){
        best.alive=false; best.hp=0;
        killCount++;
        log(`Eliminated ${ENEMY_DEFS[best.type].name} with ${w.name}`, 'HIT');
        beep(180,0.2,'sawtooth',0.22); beep(90,0.3,'square',0.12);
        showCenterMsg(`KILL +${ENEMY_DEFS[best.type].score}`, '#ffb700');
        if(killCount===enemies.length){
          exitOpen=true;
          showCenterMsg('ALL HOSTILES CLEARED — EXIT OPEN!', '#00ff88');
          log('EXIT portal unlocked. Proceed to extraction.', 'SYSTEM');
          document.getElementById('obj1').innerHTML='● <span style="color:#4caf50">Hostiles eliminated ✓</span>';
          document.getElementById('obj2').innerHTML='● <span style="color:#ffb700">EXIT OPEN — go to north-east!</span>';
        }
      }
    } else {
      // wall hit particles
      let wx = player.x + Math.cos(ang)*Math.min(w.range, castSingleRay(ang)-0.05);
      let wy = player.y + Math.sin(ang)*Math.min(w.range, castSingleRay(ang)-0.05);
      if(castSingleRay(ang) < w.range) addParticles(wx,wy,3,'#888');
    }
  }
  if(hitSomething){
    // hit marker
    const hf=document.getElementById('hitFlash');
    hf.classList.remove('on'); void hf.offsetWidth; hf.classList.add('on');
    setTimeout(()=>hf.classList.remove('on'),120);
  }
  updateHUD();
}

function castSingleRay(angle){
  let posX=player.x, posY=player.y;
  let dirX=Math.cos(angle), dirY=Math.sin(angle);
  let mapX=Math.floor(posX), mapY=Math.floor(posY);
  let deltaDistX=Math.abs(1/dirX), deltaDistY=Math.abs(1/dirY);
  let stepX, stepY, sideDistX, sideDistY;
  if(dirX<0){stepX=-1; sideDistX=(posX-mapX)*deltaDistX;} else {stepX=1; sideDistX=(mapX+1-posX)*deltaDistX;}
  if(dirY<0){stepY=-1; sideDistY=(posY-mapY)*deltaDistY;} else {stepY=1; sideDistY=(mapY+1-posY)*deltaDistY;}
  let hit=false, side=0, dist=0;
  for(let i=0;i<32;i++){
    if(sideDistX<sideDistY){sideDistX+=deltaDistX; mapX+=stepX; side=0;} else {sideDistY+=deltaDistY; mapY+=stepY; side=1;}
    if(mapY<0||mapY>=MAP_H||mapX<0||mapX>=MAP_W){dist=30;break;}
    let v=MAP[mapY][mapX];
    if(v===1 || (v===9 && !exitOpen)){hit=true; break;}
  }
  if(hit){
    if(side===0) dist=(mapX - posX + (1-stepX)/2)/dirX;
    else dist=(mapY - posY + (1-stepY)/2)/dirY;
  } else dist=30;
  return Math.abs(dist);
}

function addParticles(x,y,count,color){
  for(let i=0;i<count;i++){
    particles.push({x,y, vx:(Math.random()-0.5)*0.12, vy:(Math.random()-0.5)*0.12, life:1, color, size: Math.random()*3+2});
  }
}

function flashMuzzle(color){
  mzctx.clearRect(0,0,80,80);
  mzctx.fillStyle=color;
  mzctx.shadowColor=color; mzctx.shadowBlur=12;
  mzctx.beginPath(); mzctx.arc(40,40, 18+Math.random()*8,0,Math.PI*2); mzctx.fill();
  mzctx.fillStyle='#fff'; mzctx.beginPath(); mzctx.arc(40,40,8,0,Math.PI*2); mzctx.fill();
  mzctx.shadowBlur=0;
  const el=document.getElementById('muzzle');
  el.classList.add('on');
  setTimeout(()=>{el.classList.remove('on'); mzctx.clearRect(0,0,80,80);},70);
}

function updateHUD(){
  document.getElementById('healthFill').style.width = Math.max(0,player.health)+'%';
  document.getElementById('healthNum').textContent = Math.max(0,Math.ceil(player.health))+'%';
  document.getElementById('healthNum').style.color = player.health<30?'#ff2a2a':player.health<60?'#ffb700':'#4caf50';
  document.getElementById('topHp').textContent = Math.max(0,Math.ceil(player.health));
  document.getElementById('topEnemies').textContent = `${killCount}/${enemies.length}`;
  document.getElementById('killNum').textContent = `${killCount} / ${enemies.length}`;
  document.getElementById('armorNum').textContent = player.armor;
  // face
  let face='😤';
  if(player.health<=0) face='💀';
  else if(player.health<25) face='😵';
  else if(player.health<55) face='😠';
  else if(killCount===enemies.length) face='😎';
  document.getElementById('face').textContent=face;
  // weapons
  const hw=document.getElementById('hudWeapons');
  hw.innerHTML='';
  WEAPONS.forEach(w=>{
    const ammo = player.ammo[w.ammo];
    const active = player.weapon===w.id;
    const div=document.createElement('div');
    div.className='wslot'+(active?' active':'');
    div.onclick=()=>{player.weapon=w.id; updateHUD();};
    div.innerHTML=`<div class="wicon" style="color:${w.color};border-color:${active?w.color:'#444'}">${w.icon}</div>
      <div class="winfo"><strong>${w.name}</strong><span>${w.ammo.toUpperCase()} • ${ammo}</span></div>
      <div class="ammo-count ${ammo<8?'low':''}">${ammo}</div>
      <div style="position:absolute;top:4px;right:6px;font-size:9px;color:#666">${w.id+1}</div>`;
    hw.appendChild(div);
  });
  // enemy list
  const el=document.getElementById('enemyList');
  el.innerHTML='';
  enemies.forEach(e=>{
    const d=ENEMY_DEFS[e.type];
    const row=document.createElement('div');
    row.className='enemy-row';
    row.style.opacity=e.alive?1:0.35;
    row.innerHTML=`<span style="font-size:16px">${e.alive?d.char:'☠'}</span>
      <span style="flex:1"><b style="color:${d.color}">${d.name}</b><br><span style="font-size:9px;color:#666">${e.alive?'HOSTILE':'ELIMINATED'}</span></span>
      <div style="width:60px"><div class="bar"><div class="fill" style="width:${Math.max(0,e.hp/d.maxHp*100)}%;background:${d.color}"></div></div><span style="font-size:9px;color:#888">${Math.max(0,Math.ceil(e.hp))} HP</span></div>`;
    el.appendChild(row);
  });
  document.getElementById('previewEnemies').textContent=enemies.length;
}

function winGame(){
  gameState='won';
  let elapsed = Math.floor((performance.now()-startTime)/1000);
  let m=String(Math.floor(elapsed/60)).padStart(2,'0'), s=String(elapsed%60).padStart(2,'0');
  document.getElementById('winKills').textContent=`${killCount}/${enemies.length}`;
  document.getElementById('winTime').textContent=`${m}:${s}`;
  document.getElementById('winHealth').textContent= Math.ceil(player.health);
  let rank = elapsed<35?'S — NIGHTMARE': elapsed<55?'A — ULTRA VIOLENCE': elapsed<75?'B — HURT ME PLENTY':'C — TOO YOUNG TO DIE';
  document.getElementById('rankText').textContent='RANK: '+rank;
  document.getElementById('winScreen').classList.remove('hidden');
  beep(440,0.2,'square',0.2); setTimeout(()=>beep(550,0.2,'square',0.2),180); setTimeout(()=>beep(660,0.4,'square',0.25),360);
}
function die(){
  gameState='dead';
  let elapsed=Math.floor((performance.now()-startTime)/1000);
  let m=String(Math.floor(elapsed/60)).padStart(2,'0'), s=String(elapsed%60).padStart(2,'0');
  document.getElementById('deathKills').textContent=killCount;
  document.getElementById('deathTime').textContent=`${m}:${s}`;
  document.getElementById('deathFound').textContent=`${killCount}/${enemies.length}`;
  document.getElementById('deathScreen').classList.remove('hidden');
  beep(120,0.6,'sawtooth',0.25);
}

// MAIN LOOP
let lastTime=performance.now();
function loop(){
  animFrame=requestAnimationFrame(loop);
  let now=performance.now();
  let dt = Math.min(32, now-lastTime)/1000;
  lastTime=now;
  if(gameState==='playing'){
    update(dt);
  }
  render();
  // timer
  if(gameState==='playing'){
    let e=Math.floor((now-startTime)/1000);
    let m=String(Math.floor(e/60)).padStart(2,'0'), s=String(e%60).padStart(2,'0');
    document.getElementById('topTime').textContent=`${m}:${s}`;
  }
}

function update(dt){
  // input movement
  let moveX=0, moveY=0, turn=0;
  if(keys['w']||keys['arrowup']) moveY+=1;
  if(keys['s']||keys['arrowdown']) moveY-=1;
  if(keys['a']) moveX-=1;
  if(keys['d']) moveX+=1;
  if(keys['arrowleft']) turn-=1;
  if(keys['arrowright']) turn+=1;
  let sprint = keys['shift']?1.6:1;
  let speed = 2.9 * sprint * dt;
  let strafeSpeed = 2.4 * sprint * dt;

  player.angle += turn * 1.9 * dt;

  // forward/back
  if(moveY!==0){
    let nx = player.x + Math.cos(player.angle)* moveY * speed;
    let ny = player.y + Math.sin(player.angle)* moveY * speed;
    if(!isWall(nx, player.y)) player.x=nx;
    if(!isWall(player.x, ny)) player.y=ny;
  }
  if(moveX!==0){
    let ang = player.angle + Math.PI/2;
    let nx = player.x + Math.cos(ang)* moveX * strafeSpeed;
    let ny = player.y + Math.sin(ang)* moveX * strafeSpeed;
    if(!isWall(nx, player.y)) player.x=nx;
    if(!isWall(player.x, ny)) player.y=ny;
  }

  // bob
  let moving = moveX!==0||moveY!==0;
  if(moving) player.bob += dt*10 * sprint;
  else player.bob += dt*2;
  if(player.pitch<0) player.pitch += dt*3; else player.pitch=0;

  // pickups
  pickups.forEach(p=>{
    if(p.taken) return;
    p.bob+=dt*2;
    if(Math.hypot(player.x-p.x, player.y-p.y)<0.7){
      p.taken=true;
      if(p.type==='health'){
        let before=player.health;
        player.health=Math.min(100, player.health+p.val);
        log(`+${Math.ceil(player.health-before)} HEALTH`, 'PICKUP');
        showCenterMsg(`HEALTH +${p.val}`, '#4caf50');
        beep(660,0.12,'sine',0.2);
      } else if(p.type==='armor'){
        player.armor=Math.min(100, player.armor+p.val);
        log(`+${p.val} ARMOR`, 'PICKUP');
        showCenterMsg(`ARMOR +${p.val}`, '#00e5ff');
        beep(500,0.12,'sine',0.2);
      } else if(p.type==='ammo'){
        player.ammo[p.ammo]+=p.val;
        log(`+${p.val} ${p.ammo.toUpperCase()}`, 'PICKUP');
        showCenterMsg(`AMMO +${p.val}`, '#ffb700');
        beep(400,0.12,'square',0.15);
      }
      updateHUD();
    }
  });

  // enemies AI
  enemies.forEach(e=>{
    if(!e.alive) { if(e.hitFlash>0) e.hitFlash-=dt*4; return; }
    if(e.hitFlash>0) e.hitFlash-=dt*4;
    e.cooldown-=dt;
    let dx=player.x-e.x, dy=player.y-e.y;
    let dist=Math.hypot(dx,dy);
    // move towards player if seen and not too close
    if(dist<9 && hasLOS(e.x,e.y, player.x, player.y)){
      e.ang = Math.atan2(dy,dx);
      if(dist>1.0){
        let spd=ENEMY_DEFS[e.type].speed* (60*dt*1.0) * (1 + (1 - e.hp/ENEMY_DEFS[e.type].hp)*0.3);
        // normalize
        let nx=e.x + Math.cos(e.ang)*spd;
        let ny=e.y + Math.sin(e.ang)*spd;
        if(!isWall(nx, e.y)) e.x=nx;
        if(!isWall(e.x, ny)) e.y=ny;
      }
      // attack
      if(dist<1.6 && e.cooldown<=0){
        let dmg=ENEMY_DEFS[e.type].dmg;
        let absorbed = Math.min(player.armor, dmg*0.5);
        player.armor-=absorbed;
        player.health-= (dmg-absorbed);
        e.cooldown= e.type===1?1.1: e.type===2?0.7:0.9;
        shake(4);
        document.getElementById('hitFlash').classList.add('on');
        setTimeout(()=>document.getElementById('hitFlash').classList.remove('on'),140);
        beep(90,0.18,'sawtooth',0.22);
        log(`${ENEMY_DEFS[e.type].name} hits for ${Math.ceil(dmg-absorbed)}`, 'DAMAGE');
        updateHUD();
        if(player.health<=0){
          player.health=0;
          die();
        }
      }
    } else {
      // wander slightly
      e.ang += (Math.random()-0.5)*0.04;
    }
  });

  // particles
  particles.forEach(p=>{ p.x+=p.vx; p.y+=p.vy; p.life-=dt*2.2; p.vy+=0.001; });
  particles=particles.filter(p=>p.life>0);

  // win check via exit proximity
  if(Math.hypot(player.x-14.5, player.y-14.5)<0.9 && killCount>=enemies.length){
    winGame();
  }

  // auto switch if out of ammo
  let w=WEAPONS[player.weapon];
  if(player.ammo[w.ammo]<=0){
    // find weapon with ammo
    for(let i=0;i<WEAPONS.length;i++) if(player.ammo[WEAPONS[i].ammo]>0){ player.weapon=i; updateHUD(); break; }
  }
}

function render(){
  const W=canvas.width, H=canvas.height;
  // sky + floor
  ctx.fillStyle='#1a1a2a';
  ctx.fillRect(0,0,W,H/2);
  // subtle sky gradient
  let grad=ctx.createLinearGradient(0,0,0,H/2);
  grad.addColorStop(0,'#0f0f1a'); grad.addColorStop(1,'#2a1a0f');
  ctx.fillStyle=grad; ctx.fillRect(0,0,W,H/2);
  ctx.fillStyle='#0e0e0e';
  ctx.fillRect(0,H/2,W,H/2);
  // floor lines
  ctx.strokeStyle='rgba(255,183,0,0.04)';
  ctx.lineWidth=1;
  for(let y=H/2;y<H;y+=12){
    ctx.beginPath(); ctx.moveTo(0,y); ctx.lineTo(W,y); ctx.stroke();
  }

  const numRays = 160; // low-res retro
  const stripW = W / numRays;
  const fov = Math.PI*0.66;
  let zBuffer = new Array(numRays);

  for(let i=0;i<numRays;i++){
    let rayAngle = player.angle - fov/2 + (i/numRays)*fov;
    let posX=player.x, posY=player.y;
    let dirX=Math.cos(rayAngle), dirY=Math.sin(rayAngle);
    let mapX=Math.floor(posX), mapY=Math.floor(posY);
    let deltaDistX=Math.abs(1/(dirX||0.0001)), deltaDistY=Math.abs(1/(dirY||0.0001));
    let stepX, stepY, sideDistX, sideDistY;
    if(dirX<0){stepX=-1; sideDistX=(posX-mapX)*deltaDistX;} else {stepX=1; sideDistX=(mapX+1-posX)*deltaDistX;}
    if(dirY<0){stepY=-1; sideDistY=(posY-mapY)*deltaDistY;} else {stepY=1; sideDistY=(mapY+1-posY)*deltaDistY;}
    let hit=false, side=0, hitVal=1;
    let hitX=0, hitY=0;
    for(let s=0;s<40;s++){
      if(sideDistX<sideDistY){sideDistX+=deltaDistX; mapX+=stepX; side=0;} else {sideDistY+=deltaDistY; mapY+=stepY; side=1;}
      if(mapY<0||mapY>=MAP_H||mapX<0||mapX>=MAP_W) break;
      let v=MAP[mapY][mapX];
      if(v===1 || (v===9 && !exitOpen)){hit=true; hitVal=v; hitX=mapX; hitY=mapY; break;}
    }
    let perpDist;
    if(hit){
      if(side===0) perpDist=(mapX - posX + (1-stepX)/2)/dirX;
      else perpDist=(mapY - posY + (1-stepY)/2)/dirY;
    } else perpDist=30;
    perpDist = Math.max(0.1, Math.abs(perpDist));
    // fish-eye correction
    perpDist *= Math.cos(rayAngle - player.angle);
    zBuffer[i]=perpDist;

    let lineH = Math.floor(H / perpDist);
    let drawStart = Math.floor(-lineH/2 + H/2 + player.pitch*H*1.2);
    let drawEnd = Math.floor(lineH/2 + H/2 + player.pitch*H*1.2);

    // wall shading
    let shade = Math.max(0.25, 1 - perpDist*0.09);
    let base;
    if(hitVal===9){
      base = exitOpen? '#00ff88' : '#ff2a2a';
      // pulsing exit
      if(exitOpen) shade = 0.9 + Math.sin(performance.now()*0.005)*0.1;
    } else {
      // checker variation
      let isAlt = (hitX+hitY)%2===0;
      if(side===1){
        base = isAlt? '#4a3a2a' : '#5a4a3a';
      } else {
        base = isAlt? '#6a5a4a' : '#7a6a5a';
      }
      // add vertical stripe texture
      let tex = ((hitX*7+hitY*13 + i*3)%10)/10;
      shade *= (0.85 + tex*0.3);
    }
    // convert base to rgb
    let r=parseInt(base.slice(1,3),16), g=parseInt(base.slice(3,5),16), b=parseInt(base.slice(5,7),16);
    r=Math.floor(r*shade); g=Math.floor(g*shade); b=Math.floor(b*shade);
    ctx.fillStyle=`rgb(${r},${g},${b})`;
    // draw strip with slight gap for scanline feel
    let x = Math.floor(i*stripW);
    ctx.fillRect(x, drawStart, Math.ceil(stripW)+1, drawEnd-drawStart);
    // highlight top edge
    if(hit){
      ctx.fillStyle=`rgba(255,255,255,${0.08*shade})`;
      ctx.fillRect(x, drawStart, Math.ceil(stripW)+1, 2);
      ctx.fillStyle=`rgba(0,0,0,${0.25*shade})`;
      ctx.fillRect(x, drawEnd-2, Math.ceil(stripW)+1, 2);
    }
    // floor shading per column (simple)
    let floorY = drawEnd;
    if(floorY < H){
      let floorShade = Math.max(0, 1 - perpDist*0.07);
      ctx.fillStyle=`rgba(${Math.floor(30*floorShade)},${Math.floor(24*floorShade)},${Math.floor(18*floorShade)},0.9)`;
      // already filled, but add distance fog overlay
      ctx.fillRect(x, floorY, Math.ceil(stripW)+1, H-floorY);
    }
  }

  // sprites: enemies + pickups + particles
  let sprites=[];
  enemies.forEach(e=>{
    if(!e.alive && e.hp<=0 && Math.random()<0.02) return; // fade dead? keep visible briefly
    let dx=e.x-player.x, dy=e.y-player.y;
    let dist=Math.hypot(dx,dy);
    sprites.push({x:e.x,y:e.y,dist, type:'enemy', obj:e});
  });
  pickups.forEach(p=>{
    if(p.taken) return;
    let dx=p.x-player.x, dy=p.y-player.y;
    sprites.push({x:p.x,y:p.y,dist:Math.hypot(dx,dy), type:'pickup', obj:p});
  });
  particles.forEach(p=>{
    sprites.push({x:p.x,y:p.y,dist:Math.hypot(p.x-player.x,p.y-player.y), type:'particle', obj:p});
  });
  // exit sprite
  sprites.push({x:14.5,y:14.5,dist:Math.hypot(14.5-player.x,14.5-player.y), type:'exit'});

  sprites.sort((a,b)=>b.dist-a.dist);

  sprites.forEach(s=>{
    let dx=s.x-player.x, dy=s.y-player.y;
    let angleTo = Math.atan2(dy,dx) - player.angle;
    // normalize to -PI..PI
    angleTo = Math.atan2(Math.sin(angleTo), Math.cos(angleTo));
    if(Math.abs(angleTo) > fov/2 + 0.3) return;
    // behind wall check via zBuffer
    let col = Math.floor((angleTo + fov/2)/fov * numRays);
    if(col<0||col>=numRays) return;
    if(s.dist > zBuffer[col] + 0.3) return;

    let size = Math.floor(H / s.dist);
    let x = Math.floor(W/2 + Math.tan(angleTo)* (W/ (2*Math.tan(fov/2))) );
    let y = Math.floor(H/2 + player.pitch*H*1.2);

    if(s.type==='enemy'){
      let e=s.obj;
      let h = Math.floor(size*0.9), w = Math.floor(h*0.7);
      let sx = x - w/2, sy = y - h/2 - Math.floor(size*0.1);
      // shadow
      ctx.fillStyle='rgba(0,0,0,0.4)';
      ctx.fillRect(sx+4, y+ h/2 -4, w-8, 6);
      // body
      let def=ENEMY_DEFS[e.type];
      let flash = e.hitFlash>0? '#fff' : def.color;
      // bob
      sy += Math.sin(performance.now()*0.004 + e.x*2)* Math.min(6, 4/s.dist);
      // draw sprite as colored rect with face
      ctx.fillStyle = e.alive? flash : '#333';
      if(!e.alive) ctx.globalAlpha=0.5;
      // outer
      ctx.fillRect(sx, sy, w, h);
      // inner highlight
      ctx.fillStyle = e.alive? (e.hitFlash>0?'#fff':'rgba(255,255,255,0.18)') : 'rgba(255,255,255,0.08)';
      ctx.fillRect(sx+2, sy+2, w-4, Math.floor(h*0.35));
      // face char
      ctx.fillStyle='#000';
      ctx.font=`${Math.floor(w*0.7)}px monospace`;
      ctx.textAlign='center'; ctx.textBaseline='middle';
      ctx.fillText(e.alive?def.char:'✕', x, sy+h*0.45);
      ctx.globalAlpha=1;
      // health bar
      if(e.alive && s.dist<6){
        let bw=w, bh=4;
        ctx.fillStyle='#000'; ctx.fillRect(sx, sy-8, bw, bh);
        ctx.fillStyle=def.color; ctx.fillRect(sx, sy-8, Math.floor(bw*e.hp/def.hp), bh);
      }
      // outline
      ctx.strokeStyle='rgba(0,0,0,0.6)'; ctx.lineWidth=1; ctx.strokeRect(sx,sy,w,h);
    } else if(s.type==='pickup'){
      let p=s.obj;
      let h=Math.floor(size*0.45), w=Math.floor(h*0.9);
      let sy = y - h/2 + Math.sin(p.bob)*6 - Math.floor(size*0.15);
      let sx = x - w/2;
      let col = p.type==='health'?'#4caf50': p.type==='armor'?'#00e5ff':'#ffb700';
      let ch = p.type==='health'?'✚': p.type==='armor'?'⬣':'⬢';
      // glow
      ctx.shadowColor=col; ctx.shadowBlur=10;
      ctx.fillStyle=col;
      ctx.fillRect(sx, sy, w, h);
      ctx.shadowBlur=0;
      ctx.fillStyle='#000'; ctx.font=`${Math.floor(w*0.6)}px monospace`; ctx.textAlign='center'; ctx.fillText(ch, x, sy+h*0.55);
      ctx.strokeStyle='rgba(0,0,0,0.5)'; ctx.strokeRect(sx,sy,w,h);
      // label
      if(s.dist<4){
        ctx.fillStyle='rgba(0,0,0,0.7)'; ctx.fillRect(x-22, sy-14,44,10);
        ctx.fillStyle='#fff'; ctx.font='7px monospace'; ctx.fillText(p.type.toUpperCase(), x, sy-7);
      }
    } else if(s.type==='particle'){
      let p=s.obj;
      let sz = Math.max(2, Math.floor((4/p.dist)*8 * p.life));
      ctx.globalAlpha=p.life;
      ctx.fillStyle=p.color;
      ctx.fillRect(x-sz/2, y-sz/2, sz, sz);
      ctx.globalAlpha=1;
    } else if(s.type==='exit'){
      let h=Math.floor(size*1.1), w=Math.floor(h*0.6);
      let sx=x-w/2, sy=y-h/2;
      let col = exitOpen?'#00ff88':'#ff2a2a';
      ctx.shadowColor=col; ctx.shadowBlur= exitOpen?18:6;
      ctx.fillStyle=col;
      ctx.fillRect(sx, sy, w, h);
      ctx.shadowBlur=0;
      ctx.fillStyle='#000'; ctx.font=`${Math.floor(w*0.5)}px monospace`; ctx.textAlign='center';
      ctx.fillText(exitOpen?'◉':'✕', x, sy+h*0.4);
      ctx.fillStyle='#fff'; ctx.font='bold 7px monospace'; ctx.fillText(exitOpen?'EXIT':'LOCKED', x, sy+h+10);
      ctx.strokeStyle=col; ctx.lineWidth=2; ctx.strokeRect(sx,sy,w,h);
    }
  });

  // weapon bob overlay (simple)
  // draw weapon at bottom center via uiCanvas
  uctx.clearRect(0,0,uiCanvas.width, uiCanvas.height);
  let w = WEAPONS[player.weapon];
  let bobX = Math.sin(player.bob)*4;
  let bobY = Math.cos(player.bob*2)*3;
  if(player.pitch) bobY += player.pitch*60;
  let wx = uiCanvas.width/2 + bobX;
  let wy = uiCanvas.height - 28 + bobY;
  // gun base
  uctx.fillStyle='rgba(0,0,0,0.5)';
  uctx.fillRect(wx-52, wy-18, 104, 36);
  uctx.fillStyle='#1a1a1a'; uctx.strokeStyle='#333'; uctx.lineWidth=2;
  uctx.fillRect(wx-48, wy-14, 96, 28); uctx.strokeRect(wx-48, wy-14, 96, 28);
  // barrel
  uctx.fillStyle=w.color; uctx.fillRect(wx-6, wy-28, 12, 18);
  uctx.fillStyle='#fff'; uctx.fillRect(wx-4, wy-30, 8, 6);
  // name
  uctx.fillStyle=w.color; uctx.font='bold 10px "Share Tech Mono"'; uctx.textAlign='center';
  uctx.fillText(w.name, wx, wy+6);
  uctx.fillStyle='#888'; uctx.font='7px monospace'; uctx.fillText(w.ammo.toUpperCase()+' '+player.ammo[w.ammo], wx, wy+14);

  // minimap
  mctx.clearRect(0,0,100,100);
  mctx.fillStyle='#0a0a0a'; mctx.fillRect(0,0,100,100);
  let scale=100/16;
  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='#3a3a4a'; mctx.fillRect(x*scale, y*scale, scale-0.5, scale-0.5); }
    else if(v===9){ mctx.fillStyle=exitOpen?'#00ff88':'#ff2a2a'; mctx.fillRect(x*scale, y*scale, scale-0.5, scale-0.5); }
  }
  pickups.forEach(p=>{ if(p.taken) return; mctx.fillStyle=p.type==='health'?'#4caf50':p.type==='armor'?'#00e5ff':'#ffb700'; mctx.beginPath(); mctx.arc(p.x*scale, p.y*scale, 2.2,0,Math.PI*2); mctx.fill(); });
  enemies.forEach(e=>{ if(!e.alive) return; mctx.fillStyle=ENEMY_DEFS[e.type].color; mctx.beginPath(); mctx.arc(e.x*scale, e.y*scale, 2.5,0,Math.PI*2); mctx.fill(); });
  // player
  mctx.fillStyle='#fff'; mctx.beginPath(); mctx.arc(player.x*scale, player.y*scale, 2.8,0,Math.PI*2); mctx.fill();
  mctx.strokeStyle='#ffb700'; mctx.lineWidth=1; mctx.beginPath(); mctx.moveTo(player.x*scale, player.y*scale); mctx.lineTo(player.x*scale+Math.cos(player.angle)*8, player.y*scale+Math.sin(player.angle)*8); mctx.stroke();
}

resetGame();
updateHUD();
loop();

// handle resize: keep canvas internal res
function resizeCanvas(){
  // keep internal 640x360, CSS scales
}
window.addEventListener('resize', resizeCanvas);
</script>
</body>
</html>
<!-- agent-meta {"model":"meta/muse-spark-1.2-contributor","provider":"openrouter","persona":"full","sessionId":"9d709727-cec7-455b-9523-c027c9d5d714","tokensIn":229934,"tokensOut":20382,"tokensTotal":250316,"cost":0.010603545999999998,"turns":10,"toolCalls":9,"failedToolCalls":0,"timestamp":"2026-08-23T00:33:50.122Z"} -->