← DOOM results

DOOM

muse spark 1.1 · composer

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 36,121 bytes · SHA-256 c54691271e59
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>CORRIDOR 1993</title>
<style>
/* ── css: main ── */
@import url('https://fonts.googleapis.com/css2?family=VT323&family=JetBrains+Mono:wght@700&display=swap');
*{margin:0;padding:0;box-sizing:border-box}
html,body{background:#08080a;overflow:hidden;height:100%;width:100%;font-family:'JetBrains Mono',monospace;color:#e8e8e8;user-select:none}
#root{position:relative;width:100vw;height:100vh;display:flex;justify-content:center;align-items:center;background:radial-gradient(70% 70% at 50% 0%,#1e1e30 0%,#0a0a10 60%);}
#view{width:min(100vw, 177.77vh);height:min(100vh, 56.25vw);display:block;image-rendering:pixelated;box-shadow:0 0 0 1px #222, 0 0 80px #000 inset, 0 20px 60px rgba(0,0,0,.8);background:#000;cursor:none}
#ui{position:absolute;inset:0;pointer-events:none;display:flex;justify-content:center;align-items:center}
#topbar{position:absolute;top:0;left:50%;transform:translateX(-50%);width:min(100vw,177.77vh);display:flex;gap:18px;padding:10px 14px;background:linear-gradient(#111 0%,#0c0c0f 100%);border-bottom:2px solid #2a2a3a;box-shadow:0 4px 20px #000;z-index:30;font-family:'JetBrains Mono',monospace}
.tb{display:flex;align-items:center;gap:8px;font-size:13px;letter-spacing:.04em}
.lbl{color:#8a8aa8;font-size:10px;letter-spacing:.12em}
.bar{width:84px;height:10px;background:#000;border:1px solid #333;position:relative;overflow:hidden}
.bar i{display:block;height:100%;background:linear-gradient(90deg,#ff2a2a,#ff8a2a);width:100%;transition:width .15s}
#ammoTxt{font-size:20px;color:#ffe060;min-width:42px;text-align:right;font-family:'VT323',monospace}
#ammoType{font-size:9px;color:#777}
#wepIcons{display:flex;gap:8px}
#wepIcons b{font-size:11px;padding:2px 6px;border:1px solid #333;background:#15151a;opacity:.6;transition:.15s}
#wepIcons b.active{opacity:1;background:#ffe060;color:#000;border-color:#ffe060;box-shadow:0 0 8px #ffe06066}
#enemyCount{color:#ff5555;font-weight:700}
#cross{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);font-size:22px;color:#ffffffcc;text-shadow:0 0 6px #000,0 0 1px #000;line-height:1;z-index:10;mix-blend-mode:difference}
#centerMsg{position:absolute;left:50%;top:32%;transform:translateX(-50%);font-family:'VT323',monospace;font-size:28px;color:#fff;text-shadow:0 2px 0 #000,0 0 12px #000;opacity:0;transition:opacity .2s;z-index:15;pointer-events:none;text-align:center}
#centerMsg.show{opacity:1}
#minimap{position:absolute;left:12px;bottom:12px;width:128px;height:128px;border:2px solid #333;image-rendering:pixelated;background:#050508;z-index:12;box-shadow:0 0 20px #000}
#weaponLayer{position:absolute;bottom:0;left:50%;transform:translateX(-50%);width:min(100vw,177.77vh);height:52%;pointer-events:none;z-index:11;display:flex;justify-content:center;align-items:flex-end}
#wepCanvas{width:60%;max-width:420px;height:100%;image-rendering:pixelated}
#help{position:absolute;bottom:8px;left:50%;transform:translateX(-50%);font-size:9px;letter-spacing:.15em;color:#555;z-index:13}
#startScreen,#deadScreen,#winScreen{position:absolute;inset:0;background:radial-gradient(120% 80% at 50% 10%,#222235 0%,#0a0a12 55%,#000 100%);display:flex;justify-content:center;align-items:center;z-index:50;pointer-events:auto}
#deadScreen,#winScreen{display:none}
.card{background:linear-gradient(#1a1a28,#11111a);border:2px solid #3a3a5a;padding:28px 32px;width:520px;max-width:90vw;box-shadow:0 0 0 1px #000,0 0 60px #000,0 0 0 4px #000 inset;text-align:center}
.card h1{font-family:'VT323',monospace;font-size:58px;line-height:.9;letter-spacing:.06em;color:#ffe060;text-shadow:0 3px 0 #884400,0 0 30px #ffae0088;margin-bottom:6px}
.card .sub{font-size:10px;letter-spacing:.25em;color:#8a8ab0;margin-bottom:18px}
.lore{font-size:13px;line-height:1.5;color:#b8b8cc;background:#0006;padding:12px;border:1px solid #2a2a3a;margin-bottom:16px;text-align:left}
.kbd{display:flex;gap:10px;justify-content:center;flex-wrap:wrap;margin-bottom:18px}
.kbd span{background:#000;border:1px solid #444;padding:2px 6px;font-size:10px;color:#ffe060}
#playBtn,#replayBtn{background:#ffe060;color:#000;border:2px solid #000;font-family:'VT323',monospace;font-size:26px;padding:10px 24px;letter-spacing:.08em;cursor:pointer;box-shadow:0 4px 0 #aa7700,0 0 20px #ffe06088;transition:.07s}
#playBtn:active,#replayBtn:active{transform:translateY(2px);box-shadow:0 2px 0 #aa7700}
.wepList{margin-top:16px;font-size:10px;color:#666;letter-spacing:.1em}
.wepList i{color:#aaa;font-style:normal}
#deadScreen{flex-direction:column;gap:8px;background:#300a;backdrop-filter:blur(3px)}
#deadScreen h2,#winScreen h2{font-family:'VT323',monospace;font-size:66px;color:#ff2a2a;text-shadow:0 4px 0 #000,0 0 20px #ff000088}
#winScreen{flex-direction:column;background:radial-gradient(100% 100% at 50% 0%,#223322 0%,#020402 65%)}
#winScreen h2{color:#60ff80}
.score{margin-top:18px;font-family:'VT323',monospace;font-size:22px;color:#ffe060}
</style>
</head>
<body>
<div id="root">
  <canvas id="view" width="960" height="540"></canvas>
  <div id="ui">
    <div id="topbar">
      <div class="tb"><span class="lbl">HEALTH</span><div class="bar"><i id="hpFill"></i></div><span id="hpTxt">100</span></div>
      <div class="tb"><span class="lbl">AMMO</span><span id="ammoTxt">∞</span><em id="ammoType">9MM</em></div>
      <div class="tb"><span class="lbl">WEAPON</span><div id="wepIcons"><b data-w="0" class="active">1 PISTOL</b><b data-w="1">2 SHOTGUN</b><b data-w="2">3 CHAINGUN</b></div></div>
      <div class="tb"><span class="lbl">ENEMIES</span><span id="enemyCount">0</span></div>
    </div>
    <div id="cross">+</div>
    <div id="centerMsg"></div>
    <canvas id="minimap" width="128" height="128"></canvas>
    <div id="weaponLayer"><canvas id="wepCanvas" width="400" height="300"></canvas></div>
    <div id="help">WASD MOVE • MOUSE DRAG LOOK • CLICK SHOOT • 1 2 3 WEAPONS • SHIFT RUN • E USE</div>
    <div id="startScreen">
      <div class="card">
        <h1>CORRIDOR 1993</h1>
        <p class="sub">SECTOR 7 — DECOMMISSIONED BIO-LABS</p>
        <div class="lore">The Bio-Titan breached containment. Find GOLD KEYCARD, open exit door (red). 7 hostiles incl. TITAN. Ammo/Health on floor - get close. Classic keys.</div>
        <div class="kbd"><span>MOUSE</span> look / <span>WASD</span> move / <span>CLICK</span> shoot / <span>1-3</span> switch</div>
        <button id="playBtn">▶ INSERT COIN / PLAY</button>
        <div class="wepList"><span>ARSENAL:</span> <i>Pistol ∞</i> <i>Shotgun ×2 shell</i> <i>Chaingun fast</i></div>
      </div>
    </div>
    <div id="deadScreen"><h2>YOU DIED</h2><p>Respawning...</p></div>
    <div id="winScreen"><h2>MISSION COMPLETE</h2><p>Bio-Titan neutralized. Sector secured.</p><button id="replayBtn">PLAY AGAIN</button><div class="score" id="finalScore"></div></div>
  </div>
</div>
<script>
'use strict';

// ── const: TILE ──
const TILE=64;

// ── const: MAP_W ──
const MAP_W=16;

// ── const: MAP_H ──
const MAP_H=16;

// ── const: FOV ──
const FOV=1.0471975512;

// ── const: WALL_H ──
const WALL_H=64;

// ── const: TEX ──
const TEX=64;

// ── const: LEVEL_MAP ──
const LEVEL_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,1,0,0,0,0,0,0,1],
[1,0,0,0,0,2,2,0,1,0,0,3,3,0,0,1],
[1,0,0,1,0,0,0,0,0,0,0,3,0,0,0,1],
[1,0,0,1,0,0,0,0,0,0,0,3,0,0,0,1],
[1,0,0,1,1,0,0,2,2,0,0,0,0,1,0,1],
[1,0,0,0,0,0,0,2,0,0,0,0,0,1,0,1],
[1,0,0,0,0,0,0,0,0,1,1,1,0,1,0,1],
[1,0,1,1,0,0,0,0,0,1,0,0,0,0,0,1],
[1,0,1,0,0,3,3,0,0,1,0,0,0,2,0,1],
[1,0,0,0,0,3,0,0,0,0,0,1,0,2,0,1],
[1,0,0,0,0,3,0,0,1,0,0,1,0,0,0,1],
[1,0,2,2,0,0,0,0,1,0,0,1,0,10,9,1],
[1,0,2,0,0,0,1,0,1,0,0,0,0,0,0,1],
[1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,1],
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
];

// ── const: WEAPONS ──
const WEAPONS=[
  {name:'PISTOL', dmg:22, spread:0.015, pellets:1, fireRate:320, ammoType:'bullet', ammoCost:1, range:18, color:'#ffd55a', auto:false, infinite:true},
  {name:'SHOTGUN', dmg:11, spread:0.14, pellets:8, fireRate:850, ammoType:'shell', ammoCost:2, range:12, color:'#ff8a2a', auto:false},
  {name:'CHAINGUN', dmg:14, spread:0.06, pellets:1, fireRate:105, ammoType:'bullet', ammoCost:1, range:20, color:'#8affff', auto:true},
];

// ── const: ENEMY_TYPES ──
const ENEMY_TYPES=[
  {hp:35, speed:0.025, dmg:8, range:6, color:'#ff3a3a', scale:0.9, score:100},
  {hp:55, speed:0.018, dmg:14, range:8, color:'#ff8a18', scale:1.0, score:200},
  {hp:350, speed:0.012, dmg:22, range:12, color:'#44ff66', scale:1.8, score:1000},
];

// ── function: isWall ──
function isWall(x,y){
  const ix=Math.floor(x), iy=Math.floor(y);
  if(ix<0||iy<0||ix>=MAP_W||iy>=MAP_H) return 1;
  const v=LEVEL_MAP[iy][ix];
  if(v===0) return 0;
  if(v===10) return game && game.hasKey?0:1;
  if(v===9) return 2;
  return 1;
}

// ── function: wallTypeAt ──
function wallTypeAt(x,y){
  const ix=Math.floor(x), iy=Math.floor(y);
  if(ix<0||iy<0||ix>=MAP_W||iy>=MAP_H) return 1;
  return LEVEL_MAP[iy][ix];
}

// ── function: dist ──
function dist(a,b,c,d){ const dx=a-c, dy=b-d; return Math.sqrt(dx*dx+dy*dy); }

// ── function: castRay ──
function castRay(ox,oy,ang){
  let sin=Math.sin(ang), cos=Math.cos(ang);
  let depth=0, hit=0, side=0, hitX=0, hitY=0, tex=0, mapVal=0;
  let rx, ry;
  // DDA
  let mapX=Math.floor(ox), mapY=Math.floor(oy);
  let deltaDistX=Math.abs(1/cos), deltaDistY=Math.abs(1/sin);
  let stepX, stepY, sideDistX, sideDistY;
  if(cos<0){ stepX=-1; sideDistX=(ox-mapX)*deltaDistX; } else { stepX=1; sideDistX=(mapX+1-ox)*deltaDistX; }
  if(sin<0){ stepY=-1; sideDistY=(oy-mapY)*deltaDistY; } else { stepY=1; sideDistY=(mapY+1-oy)*deltaDistY; }
  let maxSteps=40;
  while(maxSteps--){
    if(sideDistX<sideDistY){ sideDistX+=deltaDistX; mapX+=stepX; side=0; }
    else { sideDistY+=deltaDistY; mapY+=stepY; side=1; }
    if(mapX<0||mapY<0||mapX>=MAP_W||mapY>=MAP_H) break;
    const v=LEVEL_MAP[mapY][mapX];
    if(v!==0){
      if(v===10 && game && game.hasKey){ continue; }
      hit=1; mapVal=v;
      if(side===0){ depth=sideDistX-deltaDistX; tex=oy+depth*sin; }
      else { depth=sideDistY-deltaDistY; tex=ox+depth*cos; }
      hitX=mapX; hitY=mapY;
      break;
    }
  }
  if(!hit) depth=40;
  tex=tex-Math.floor(tex);
  return {depth, hit, side, tex, mapVal, hitX, hitY};
}

// ── class: Player ──
class Player{
  constructor(){
    this.x=2.5; this.y=2.5; this.ang=-0.4;
    this.hp=100; this.maxHp=100;
    this.weapons=[true,false,false];
    this.ammo={bullet:60, shell:8};
    this.curWeap=0;
    this.hasKey=false;
    this.dead=false;
    this.hitFlash=0;
  }
  reset(){
    this.x=2.5; this.y=2.5; this.ang=-0.4;
    this.hp=100; this.dead=false; this.hasKey=false;
    this.ammo={bullet:60,shell:8};
    this.weapons=[true,false,false];
    this.curWeap=0;
    this.hitFlash=0;
  }
  takeDamage(d){
    if(this.dead) return;
    this.hp-=d;
    this.hitFlash=1;
    if(this.hp<=0){ this.hp=0; this.dead=true; game.onPlayerDead(); }
  }
  heal(v){ this.hp=Math.min(this.maxHp,this.hp+v); }
  move(dx,dy){
    let nx=this.x+dx, ny=this.y+dy;
    if(!isWall(nx,this.y)) this.x=nx;
    if(!isWall(this.x,ny)) this.y=ny;
  }
}

// ── class: Enemy ──
class Enemy{
  constructor(x,y,type){
    this.x=x; this.y=y; this.type=type;
    const t=ENEMY_TYPES[type];
    this.hp=t.hp; this.maxHp=t.hp;
    this.dead=false; this.hurt=0;
    this.state='idle';
    this.lastShot=0;
    this.ang=0;
    this.vx=0; this.vy=0;
    this.seen=0;
  }
  update(pl,dt){
    if(this.dead) return;
    if(this.hurt>0) this.hurt-=dt*0.004;
    const d=dist(this.x,this.y,pl.x,pl.y);
    // line of sight check
    let los=true;
    const steps=Math.ceil(d*4);
    for(let i=1;i<steps;i++){
      let ix=pl.x+(this.x-pl.x)*i/steps;
      let iy=pl.y+(this.y-pl.y)*i/steps;
      if(isWall(ix,iy)===1){ los=false; break; }
    }
    if(los && d<12) this.seen=2000;
    else if(this.seen>0) this.seen-=dt;

    if(d<0.7){ /* melee bump */ }

    // AI
    if(this.seen>0 && d>0.9){
      const ang=Math.atan2(pl.y-this.y, pl.x-this.x);
      const mv=ENEMY_TYPES[this.type].speed * (dt/16.6);
      const nx=this.x+Math.cos(ang)*mv;
      const ny=this.y+Math.sin(ang)*mv;
      if(!isWall(nx,this.y)) this.x=nx;
      if(!isWall(this.x,ny)) this.y=ny;
      this.ang=ang;
      // shoot
      if(los && d<ENEMY_TYPES[this.type].range){
        if(performance.now()-this.lastShot>1200+Math.random()*800){
          this.lastShot=performance.now();
          pl.takeDamage(ENEMY_TYPES[this.type].dmg + (Math.random()*6-3));
          game.spawnMuzzle(this.x,this.y);
          game.flash=0.2;
        }
      }
    } else {
      // wander slightly
      this.ang+= (Math.random()-0.5)*0.02;
    }
  }
  damage(d,ang){
    if(this.dead) return false;
    this.hp-=d; this.hurt=1; this.seen=3000;
    // knockback
    this.x+=Math.cos(ang)*0.12; this.y+=Math.sin(ang)*0.12;
    if(isWall(this.x,this.y)) { this.x-=Math.cos(ang)*0.15; this.y-=Math.sin(ang)*0.15; }
    if(this.hp<=0){
      this.dead=true; this.hp=0; game.score+=ENEMY_TYPES[this.type].score;
      if(this.type===2) game.win();
      return true;
    }
    return false;
  }
}

// ── class: Game ──
class Game{
  constructor(){
    this.pl=new Player();
    this.enemies=[];
    this.pickups=[];
    this.hasKey=false;
    this.score=0;
    this.flash=0;
    this.muzzleTimers=[];
    this.time=0;
    this.kills=0;
    this.started=false;
    this.won=false;
  }
  loadLevel(){
    this.enemies=[];
    this.pickups=[];
    this.hasKey=false; this.pl.hasKey=false; this.won=false; this.score=0;
    const data=[
      {x:4.5,y:3.5,type:0},
      {x:7.5,y:6.5,type:0},
      {x:11.5,y:3.5,type:1},
      {x:5.5,y:10.5,type:0},
      {x:10.5,y:11.5,type:1},
      {x:13.5,y:10.5,type:1},
      {x:14.5,y:14.5,type:2},
    ];
    data.forEach(e=>this.enemies.push(new Enemy(e.x,e.y,e.type)));
    const pups=[
      {x:3.5,y:4.5,type:'shell',amount:8},
      {x:6.5,y:2.5,type:'bullet',amount:30},
      {x:2.5,y:7.5,type:'med',amount:25},
      {x:12.5,y:5.5,type:'med',amount:25},
      {x:8.5,y:12.5,type:'shell',amount:12},
      {x:3.5,y:13.5,type:'bullet',amount:50},
      {x:9.5,y:4.5,type:'shotgun'},
      {x:13.5,y:3.5,type:'chaingun'},
      {x:11.5,y:12.5,type:'key'},
    ];
    pups.forEach(p=>this.pickups.push(Object.assign({taken:false},p)));
  }
  spawnMuzzle(x,y){ this.muzzleTimers.push({x,y,t:200}); }
  onPlayerDead(){
    document.getElementById('deadScreen').style.display='flex';
    setTimeout(()=>{ this.respawn(); },1800);
  }
  respawn(){
    document.getElementById('deadScreen').style.display='none';
    this.pl.reset();
    this.loadLevel();
    this.hasKey=false;
    showMsg('SYSTEM REBOOTED - SECTOR RESET',2000);
  }
  win(){
    if(this.won) return; this.won=true;
    document.getElementById('finalScore').textContent='SCORE: '+this.score+' | TIME: '+Math.floor(this.time/1000)+'s';
    document.getElementById('winScreen').style.display='flex';
  }
  tryPickup(){
    for(let p of this.pickups){
      if(p.taken) continue;
      if(dist(p.x,p.y,this.pl.x,this.pl.y)<0.7){
        p.taken=true;
        if(p.type==='bullet'){ this.pl.ammo.bullet+=p.amount; showMsg('+'+p.amount+' BULLETS',1200); }
        else if(p.type==='shell'){ this.pl.ammo.shell+=p.amount; showMsg('+'+p.amount+' SHELLS',1200); }
        else if(p.type==='med'){ this.pl.heal(p.amount); showMsg('+'+p.amount+' HEALTH',1200); }
        else if(p.type==='shotgun'){ this.pl.weapons[1]=true; this.pl.curWeap=1; showMsg('ACQUIRED: SHOTGUN',1800); }
        else if(p.type==='chaingun'){ this.pl.weapons[2]=true; this.pl.curWeap=2; showMsg('ACQUIRED: CHAINGUN',1800); }
        else if(p.type==='key'){ this.hasKey=true; this.pl.hasKey=true; showMsg('GOLD KEYCARD ACQUIRED - FIND RED EXIT',2500); }
      }
    }
    // check exit
    const tx=Math.floor(this.pl.x), ty=Math.floor(this.pl.y);
    if(tx>=0&&ty>=0&&tx<MAP_W&&ty<MAP_H){
      if(LEVEL_MAP[ty][tx]===9 && this.hasKey){
        // need boss dead?
        const bossAlive=this.enemies.some(e=>e.type===2 && !e.dead);
        if(!bossAlive) this.win();
        else showMsg('TITAN STILL ALIVE - HUNTER PROTOCOL ACTIVE',2000);
      } else if(LEVEL_MAP[ty][tx]===9 && !this.hasKey){
        showMsg('LOCKED - GOLD KEY REQUIRED',1500);
      }
    }
  }
}

// ── function: showMsg ──
function showMsg(txt,ms){
  const el=document.getElementById('centerMsg');
  el.textContent=txt; el.classList.add('show');
  clearTimeout(showMsg._t);
  showMsg._t=setTimeout(()=>el.classList.remove('show'),ms||1500);
}

// ── const: Input ──
const Input={
  keys:{},
  mouseX:0, mouseDown:false, mouseDownTime:0,
  init(){
    window.addEventListener('keydown',e=>{
      this.keys[e.code]=true;
      if(e.code==='Digit1') game.pl.curWeap=0;
      if(e.code==='Digit2' && game.pl.weapons[1]) game.pl.curWeap=1;
      if(e.code==='Digit3' && game.pl.weapons[2]) game.pl.curWeap=2;
      if(e.code==='KeyE') game.tryPickup();
    });
    window.addEventListener('keyup',e=>{ this.keys[e.code]=false; });
    const view=document.getElementById('view');
    view.addEventListener('mousedown',e=>{
      this.mouseDown=true; this.mouseDownTime=performance.now();
      if(game.started) view.requestPointerLock?.();
    });
    window.addEventListener('mouseup',()=>{ this.mouseDown=false; });
    window.addEventListener('mousemove',e=>{
      if(document.pointerLockElement){
        game.pl.ang+= e.movementX*0.0035;
      } else {
        if(this.mouseDown) game.pl.ang+= e.movementX*0.007;
      }
    });
    view.addEventListener('click',()=>{
      if(!game.started) return;
      if(document.pointerLockElement) {}
    });
  },
  isDown(c){ return !!this.keys[c]; }
};

// ── function: drawWeapon ──
function drawWeapon(ctx,pl,time,shootAnim){
  const wc=document.getElementById('wepCanvas');
  const wctx=wc.getContext('2d');
  wctx.clearRect(0,0,wc.width,wc.height);
  const wep=WEAPONS[pl.curWeap];
  // bob
  const bob=Math.sin(time*0.008)*2 + Math.cos(time*0.012)*1;
  const recoil=shootAnim*30;
  const cx=wc.width/2, cy=wc.height;
  wctx.save();
  wctx.translate(cx, cy+bob-recoil);
  // simple pixel gun drawings
  if(pl.curWeap===0){
    // pistol
    wctx.fillStyle='#1a1a1a'; wctx.fillRect(-26,-90,52,70);
    wctx.fillStyle='#2a2a2a'; wctx.fillRect(-22,-90,44,12);
    wctx.fillStyle=wep.color; wctx.fillRect(-20,-88,40,4);
    wctx.fillStyle='#111'; wctx.fillRect(-10,-22,20,24);
    if(shootAnim>0.2){ wctx.fillStyle='#ffffaa'; wctx.beginPath(); wctx.arc(0,-88,10+Math.random()*10,0,Math.PI*2); wctx.fill(); }
  } else if(pl.curWeap===1){
    wctx.fillStyle='#111'; wctx.fillRect(-36,-110,72,90);
    wctx.fillStyle='#3a2a1a'; wctx.fillRect(-32,-110,64,14);
    wctx.fillStyle='#222'; wctx.fillRect(-8,-20,16,20);
    // dual barrels
    wctx.fillStyle='#444'; wctx.fillRect(-20,-124,12,30); wctx.fillRect(8,-124,12,30);
    wctx.fillStyle=wep.color; wctx.fillRect(-20,-126,12,4); wctx.fillRect(8,-126,12,4);
    if(shootAnim>0.2){ wctx.fillStyle='#ffdd88'; wctx.beginPath(); wctx.arc(-14,-130,14,0,Math.PI*2); wctx.arc(14,-130,14,0,Math.PI*2); wctx.fill(); wctx.fillStyle='#fff'; wctx.beginPath(); wctx.arc(-14,-130,6,0,Math.PI*2); wctx.arc(14,-130,6,0,Math.PI*2); wctx.fill(); }
  } else {
    wctx.fillStyle='#1a1a1a'; wctx.fillRect(-50,-100,100,80);
    wctx.fillStyle='#333'; wctx.fillRect(-44,-100,88,20);
    // spinning barrels
    let rot=time*0.02;
    for(let i=0;i<6;i++){
      let a=rot + i*Math.PI/3;
      wctx.fillStyle=i%2?'#555':'#777';
      wctx.fillRect(Math.cos(a)*8-2, -140+Math.sin(a)*2, 6, 54);
    }
    wctx.fillStyle=wep.color; wctx.fillRect(-40,-98,80,6);
    if(shootAnim>0.05){ wctx.fillStyle='#aaffff'; wctx.beginPath(); wctx.arc(0,-142,12+Math.random()*12,0,Math.PI*2); wctx.fill(); }
  }
  wctx.restore();
  // ammo count on gun
  wctx.fillStyle='#0008'; wctx.fillRect(260, 220, 80, 26);
  wctx.fillStyle='#ffe060'; wctx.font='bold 18px VT323'; wctx.textAlign='left';
  let ammoStr = WEAPONS[pl.curWeap].infinite? '∞' : pl.ammo[WEAPONS[pl.curWeap].ammoType];
  wctx.fillText(ammoStr+' '+WEAPONS[pl.curWeap].name, 268, 238);
}

// ── function: renderScene ──
function renderScene(ctx,W,H){
  const pl=game.pl;
  const numRays=W;
  const zBuffer=new Array(numRays);
  // ceiling & floor gradient
  let ceilGrad=ctx.createLinearGradient(0,0,0,H/2);
  ceilGrad.addColorStop(0,'#1a1a28'); ceilGrad.addColorStop(1,'#0a0a12');
  ctx.fillStyle=ceilGrad; ctx.fillRect(0,0,W,H/2);
  let floorGrad=ctx.createLinearGradient(0,H/2,0,H);
  floorGrad.addColorStop(0,'#222218'); floorGrad.addColorStop(1,'#0f0d09');
  ctx.fillStyle=floorGrad; ctx.fillRect(0,H/2,W,H/2);
  // simple floor checker fake perspective lines
  ctx.fillStyle='#ffffff08';
  for(let y=H/2+20; y<H; y+=24){
    let w = (y-H/2)/ (H/2);
    // dimming
  }

  const sprites=[];

  // raycast walls
  for(let x=0;x<numRays;x++){
    const ang=pl.ang - FOV/2 + (x/numRays)*FOV;
    const ray=castRay(pl.x,pl.y,ang);
    let distFix=ray.depth*Math.cos(ang-pl.ang);
    if(distFix<0.001) distFix=0.001;
    zBuffer[x]=distFix;
    const lineH = (H/distFix) * 0.95;
    const y0 = (H - lineH)/2;
    const y1 = y0+lineH;
    // wall shading
    let base;
    if(ray.mapVal===9) base='#ff1a2a';
    else if(ray.mapVal===10) base='#ffcc00';
    else if(ray.mapVal===2) base='#5a6e7a';
    else if(ray.mapVal===3) base='#6a4a3a';
    else base='#8a9aac';

    // apply side shading and distance
    let shade=Math.max(0.15, 1 - distFix*0.08);
    if(ray.side===1) shade*=0.78;
    let col=shadeColor(base,shade);
    // texture vertical stripe illusion: add line
    ctx.fillStyle=col; ctx.fillRect(x, y0, 1, lineH);
    // highlight top edge
    if(ray.mapVal>0 && lineH>8){
      ctx.fillStyle=adjustBright(col,1.25);
      ctx.fillRect(x, y0, 1, 2);
      // bottom edge shadow
      ctx.fillStyle=adjustBright(col,0.5);
      ctx.fillRect(x, y1-2, 1, 2);
    }
    // small texture lines
    const tx=Math.floor(ray.tex*8);
    if(tx%2===0 && ray.mapVal!==9 && ray.mapVal!==10){
      ctx.fillStyle='rgba(0,0,0,'+(0.12*shade)+')';
      ctx.fillRect(x, y0, 1, lineH);
    }

    // store exit for overlay
    if(ray.mapVal===9){
      ctx.fillStyle='#ff000044';
      ctx.fillRect(x, y0, 1, lineH);
    }
  }

  // pickups as sprites
  for(let p of game.pickups){
    if(p.taken) continue;
    const dx=p.x-pl.x, dy=p.y-pl.y;
    let d=Math.sqrt(dx*dx+dy*dy);
    if(d>12) continue;
    let a=Math.atan2(dy,dx)-pl.ang;
    while(a<-Math.PI) a+=Math.PI*2;
    while(a>Math.PI) a-=Math.PI*2;
    if(Math.abs(a)>FOV/2+0.2) continue;
    // occlusion check not needed for small?
    sprites.push({x:p.x,y:p.y,d,ang:a,type:'pickup',obj:p});
  }
  for(let e of game.enemies){
    if(e.dead) continue;
    const dx=e.x-pl.x, dy=e.y-pl.y;
    let d=Math.sqrt(dx*dx+dy*dy);
    let a=Math.atan2(dy,dx)-pl.ang;
    while(a<-Math.PI) a+=Math.PI*2;
    while(a>Math.PI) a-=Math.PI*2;
    if(Math.abs(a)>FOV/2+0.3 && d>2) continue;
    sprites.push({x:e.x,y:e.y,d,ang:a,type:'enemy',obj:e});
  }
  // sort back to front
  sprites.sort((a,b)=>b.d-a.d);

  // draw sprites
  for(let s of sprites){
    let distF=s.d*Math.cos(s.ang);
    if(distF<0.15) continue;
    const spriteX = (W/2) + Math.tan(s.ang) * (W/2) / Math.tan(FOV/2) *0.8;
    // approximate projection
    const projH=H/distF;
    const idx=Math.floor((spriteX/W)*numRays);
    if(idx<0||idx>=numRays) continue;
    if(distF>zBuffer[idx]+0.15) continue; // occluded
    if(s.type==='pickup'){
      const size=projH*0.32;
      const y=H/2 - projH*0.15 + size/2 + Math.sin(game.time*0.004 + s.x*2)*6;
      drawPickup(ctx,spriteX,y,size,s.obj);
    } else {
      const scale=ENEMY_TYPES[s.obj.type].scale;
      const h=projH*scale*0.95;
      const w=h*0.72;
      const y= H/2 - h*0.15 + h*0.25;
      drawEnemy(ctx,spriteX,y,w,h,s.obj,distF);
    }
  }

  // damage vignette
  if(pl.hitFlash>0){
    ctx.fillStyle='rgba(255,20,20,'+(pl.hitFlash*0.45)+')';
    ctx.fillRect(0,0,W,H);
    ctx.strokeStyle='rgba(255,0,0,'+(pl.hitFlash*0.9)+')';
    ctx.lineWidth=12; ctx.strokeRect(0,0,W,H);
  }
  if(game.flash>0){
    ctx.fillStyle='rgba(255,255,200,'+(game.flash*0.15)+')';
    ctx.fillRect(0,0,W,H);
  }

  // crosshair already DOM but also draw hit marker?
  return zBuffer;
}

// ── function: shadeColor ──
function shadeColor(hex, factor){
  let r=parseInt(hex.slice(1,3),16), g=parseInt(hex.slice(3,5),16), b=parseInt(hex.slice(5,7),16);
  r=Math.floor(r*factor); g=Math.floor(g*factor); b=Math.floor(b*factor);
  r=Math.min(255,r); g=Math.min(255,g); b=Math.min(255,b);
  return 'rgb('+r+','+g+','+b+')';
}

// ── function: adjustBright ──
function adjustBright(hex, f){
  if(hex.startsWith('rgb')) return hex;
  return shadeColor(hex,f);
}

// ── function: drawPickup ──
function drawPickup(ctx,x,y,size,obj){
  ctx.save();
  ctx.translate(x,y);
  ctx.shadowColor='#000'; ctx.shadowBlur=8;
  if(obj.type==='med'){
    ctx.fillStyle='#111'; ctx.fillRect(-size/2,-size/2,size,size);
    ctx.fillStyle='#e33'; ctx.fillRect(-size/2,-size/2,size,size*0.35);
    ctx.fillStyle='#fff'; ctx.font=Math.floor(size*0.6)+'px VT323'; ctx.textAlign='center'; ctx.textBaseline='middle'; ctx.fillText('+',0,-1);
  } else if(obj.type==='bullet'){
    ctx.fillStyle='#c9a84c'; ctx.fillRect(-size*0.3,-size/2,size*0.6,size);
    ctx.fillStyle='#ffe060'; ctx.fillRect(-size*0.2,-size/2,size*0.4,size*0.2);
  } else if(obj.type==='shell'){
    ctx.fillStyle='#ff6a00'; ctx.beginPath(); ctx.arc(0,0,size*0.5,0,Math.PI*2); ctx.fill();
    ctx.fillStyle='#400'; ctx.beginPath(); ctx.arc(0,0,size*0.22,0,Math.PI*2); ctx.fill();
  } else if(obj.type==='key'){
    ctx.fillStyle='#ffd700'; ctx.fillRect(-size*0.5,-size*0.2,size,size*0.4);
    ctx.beginPath(); ctx.arc(size*0.35,0,size*0.28,0,Math.PI*2); ctx.fillStyle='#0000'; ctx.strokeStyle='#ffd700'; ctx.lineWidth=4; ctx.stroke(); 
    ctx.fillStyle='#ffd700'; ctx.beginPath(); ctx.arc(size*0.35,0,size*0.12,0,Math.PI*2); ctx.fill();
  } else if(obj.type==='shotgun' || obj.type==='chaingun'){
    ctx.fillStyle='#222'; ctx.fillRect(-size*0.7,-size*0.5,size*1.4,size*0.9);
    ctx.fillStyle=obj.type==='shotgun'?'#ff8a2a':'#8affff'; ctx.fillRect(-size*0.6,-size*0.4,size*1.2,size*0.2);
    ctx.fillStyle='#fff'; ctx.font=Math.floor(size*0.3)+'px monospace'; ctx.textAlign='center'; ctx.fillText(obj.type==='shotgun'?'BOOM':'CHAIN',0,size*0.45);
  }
  ctx.restore();
}

// ── function: drawEnemy ──
function drawEnemy(ctx,x,y,w,h,en,d){
  ctx.save();
  ctx.translate(x,y);
  const hurt=en.hurt>0;
  const t=ENEMY_TYPES[en.type];
  // shadow
  ctx.fillStyle='rgba(0,0,0,0.5)'; ctx.beginPath(); ctx.ellipse(0,h*0.48,w*0.45,h*0.08,0,0,Math.PI*2); ctx.fill();
  // body box
  let col = hurt ? '#ffffff' : t.color;
  ctx.fillStyle= hurt? '#fff' : '#1b1b1b';
  // legs
  let bob=Math.sin(game.time*0.01+en.x*3)*h*0.04;
  ctx.fillStyle='#111';
  ctx.fillRect(-w*0.22, h*0.2+bob, w*0.16, h*0.32);
  ctx.fillRect(w*0.06, h*0.2-bob, w*0.16, h*0.32);
  // torso
  ctx.fillStyle= hurt? '#fff' : t.color;
  ctx.fillRect(-w*0.31, -h*0.35, w*0.62, h*0.6);
  // armor shading
  ctx.fillStyle='rgba(0,0,0,0.35)'; ctx.fillRect(-w*0.31, -h*0.35, w*0.62, h*0.12);
  // head
  ctx.fillStyle= hurt? '#fff' : '#e8d0b0';
  ctx.fillRect(-w*0.22, -h*0.52, w*0.44, h*0.24);
  // eye glow
  ctx.fillStyle= en.type===2 ? '#00ff66' : '#ff0000';
  if(en.type===1){ ctx.fillRect(-w*0.18,-h*0.45,w*0.12,h*0.06); ctx.fillRect(w*0.06,-h*0.45,w*0.12,h*0.06); }
  else { ctx.fillRect(-w*0.16,-h*0.44,w*0.32,h*0.06); }
  // gun
  ctx.fillStyle='#222'; ctx.fillRect(-w*0.06, -h*0.1, w*0.5, h*0.12);
  if(game.muzzleTimers.some(m=>dist(m.x,m.y,en.x,en.y)<0.5 && m.t>0)){
    ctx.fillStyle='#ffff99'; ctx.beginPath(); ctx.arc(w*0.4,-h*0.04,w*0.18,0,Math.PI*2); ctx.fill();
  }
  // health bar for boss
  if(en.type===2){
    ctx.fillStyle='#000'; ctx.fillRect(-w*0.6,-h*0.75,w*1.2,h*0.08);
    ctx.fillStyle='#0f6'; ctx.fillRect(-w*0.6,-h*0.75,w*1.2*(en.hp/en.maxHp),h*0.08);
  }
  ctx.restore();
}

// ── function: renderMinimap ──
function renderMinimap(ctx){
  const m=ctx.canvas; const W=m.width, H=m.height;
  ctx.clearRect(0,0,W,H);
  ctx.fillStyle='#0a0a10'; ctx.fillRect(0,0,W,H);
  const cellW=W/MAP_W, cellH=H/MAP_H;
  for(let y=0;y<MAP_H;y++) for(let x=0;x<MAP_W;x++){
    const v=LEVEL_MAP[y][x];
    if(v===1){ ctx.fillStyle='#3a3a4a'; ctx.fillRect(x*cellW,y*cellH,cellW,cellH); }
    else if(v===2){ ctx.fillStyle='#4a6a7a'; ctx.fillRect(x*cellW,y*cellH,cellW,cellH); }
    else if(v===3){ ctx.fillStyle='#6a4a2a'; ctx.fillRect(x*cellW,y*cellH,cellW,cellH); }
    else if(v===9){ ctx.fillStyle='#ff1a2a'; ctx.fillRect(x*cellW,y*cellH,cellW,cellH); }
    else if(v===10){ if(!game.hasKey){ ctx.fillStyle='#ffcc00'; ctx.fillRect(x*cellW,y*cellH,cellW,cellH);} }
  }
  // pickups
  for(let p of game.pickups){
    if(p.taken) continue;
    ctx.fillStyle=p.type==='key'?'#ffd700':(p.type==='med'?'#ff4444':'#44ffaa');
    ctx.beginPath(); ctx.arc(p.x*cellW,p.y*cellH,2.5,0,Math.PI*2); ctx.fill();
  }
  for(let e of game.enemies){
    if(e.dead) continue;
    ctx.fillStyle=e.type===2?'#00ff66':'#ff3333';
    ctx.beginPath(); ctx.arc(e.x*cellW,e.y*cellH,e.type===2?4:2.2,0,Math.PI*2); ctx.fill();
  }
  ctx.fillStyle='#ffe060'; ctx.beginPath(); ctx.arc(game.pl.x*cellW,game.pl.y*cellH,3,0,Math.PI*2); ctx.fill();
  ctx.strokeStyle='#ffe060'; ctx.beginPath(); ctx.moveTo(game.pl.x*cellW,game.pl.y*cellH); ctx.lineTo(game.pl.x*cellW+Math.cos(game.pl.ang)*10, game.pl.y*cellH+Math.sin(game.pl.ang)*10); ctx.stroke();
}

// ── let: game ──
let game=null;

// ── let: lastShoot ──
let lastShoot=0;

// ── let: shootAnim ──
let shootAnim=0;

// ── let: raf ──
let raf=0;

// ── function: handleShooting ──
function handleShooting(dt){
  const pl=game.pl;
  const wep=WEAPONS[pl.curWeap];
  if(Input.mouseDown || Input.isDown('Space')){
    if(wep.auto || performance.now()-Input.mouseDownTime<180){
      tryShoot();
    }
  }
}

// ── function: tryShoot ──
function tryShoot(){
  const now=performance.now();
  if(now-lastShoot<WEAPONS[game.pl.curWeap].fireRate) return;
  const pl=game.pl;
  const wep=WEAPONS[pl.curWeap];
  if(!wep.infinite){
    if((pl.ammo[wep.ammoType]||0) < wep.ammoCost){
      showMsg('OUT OF AMMO - FIND AMMO',1200);
      lastShoot=now-200; return;
    }
    pl.ammo[wep.ammoType]-=wep.ammoCost;
  }
  lastShoot=now;
  shootAnim=1;
  game.flash=0.25;
  // raycast hitscan
  let hitSomething=false;
  for(let p=0;p<wep.pellets;p++){
    let spread=wep.spread*(p===0?0.25:1);
    let ang=pl.ang + (Math.random()-0.5)*spread;
    // check walls
    let ray=castRay(pl.x,pl.y,ang);
    let bestDist=ray.depth;
    // check enemies
    let hitEnemy=null; let hitDist=1e9;
    for(let e of game.enemies){
      if(e.dead) continue;
      // intersect as circle
      // project enemy onto ray
      let dx=e.x-pl.x, dy=e.y-pl.y;
      let proj= dx*Math.cos(ang)+dy*Math.sin(ang);
      if(proj<0.2||proj>bestDist-0.05||proj>wep.range) continue;
      let perp= Math.abs(-dx*Math.sin(ang)+dy*Math.cos(ang));
      let rad=0.36*ENEMY_TYPES[e.type].scale;
      if(perp<rad){
        if(proj<hitDist){ hitDist=proj; hitEnemy=e; }
      }
    }
    if(hitEnemy){
      const dmg=wep.dmg + (Math.random()*wep.dmg*0.3);
      if(hitEnemy.damage(dmg,ang)){
        game.kills++;
        showMsg(ENEMY_TYPES[hitEnemy.type].name? (ENEMY_TYPES[hitEnemy.type].score+' PTS') : 'ENEMY DOWN', 700);
      }
      hitSomething=true;
    } else {
      // wall hit puff maybe
      if(ray.depth<wep.range && ray.hit){
        // spark at hit pos
        game.spawnMuzzle(pl.x+Math.cos(ang)*ray.depth, pl.y+Math.sin(ang)*ray.depth);
      }
    }
  }
  // screenshake via hit
  if(navigator.vibrate) navigator.vibrate(10);
}

// ── main ──
const canvas=document.getElementById('view');
const ctx=canvas.getContext('2d');
const mini=document.getElementById('minimap');
const mctx=mini.getContext('2d');
const wepCv=document.getElementById('wepCanvas');

function resize(){
  const dpr=Math.min(window.devicePixelRatio||1,1.5);
  canvas.width=960; canvas.height=540;
}
resize();
window.addEventListener('resize',resize);

game=new Game();
game.loadLevel();
Input.init();

let lastT=performance.now();
function loop(t){
  let dt=Math.min(t-lastT,50); lastT=t;
  game.time+=dt;
  if(game.started && !game.pl.dead && !game.won){
    // input movement
    let moveSpeed=0.07*(Input.isDown('ShiftLeft')||Input.isDown('ShiftRight')?1.6:1)*(dt/16.6);
    let rotSpeed=0.035*(dt/16.6);
    if(Input.isDown('KeyA')) { // strafe left
      game.pl.move(Math.sin(game.pl.ang)*moveSpeed, -Math.cos(game.pl.ang)*moveSpeed);
    }
    if(Input.isDown('KeyD')) {
      game.pl.move(-Math.sin(game.pl.ang)*moveSpeed, Math.cos(game.pl.ang)*moveSpeed);
    }
    if(Input.isDown('KeyW')) {
      game.pl.move(Math.cos(game.pl.ang)*moveSpeed, Math.sin(game.pl.ang)*moveSpeed);
    }
    if(Input.isDown('KeyS')) {
      game.pl.move(-Math.cos(game.pl.ang)*moveSpeed*0.7, -Math.sin(game.pl.ang)*moveSpeed*0.7);
    }
    if(Input.isDown('ArrowLeft')) game.pl.ang-=rotSpeed;
    if(Input.isDown('ArrowRight')) game.pl.ang+=rotSpeed;

    // enemies
    for(let e of game.enemies) e.update(game.pl,dt);
    // pickups
    game.tryPickup();
    // shooting
    handleShooting(dt);
    // decay
    if(game.pl.hitFlash>0) game.pl.hitFlash=Math.max(0,game.pl.hitFlash-dt*0.003);
    if(game.flash>0) game.flash=Math.max(0,game.flash-dt*0.005);
    shootAnim=Math.max(0,shootAnim-dt*0.0045);
    // muzzle timers
    game.muzzleTimers.forEach(m=>m.t-=dt);
    game.muzzleTimers=game.muzzleTimers.filter(m=>m.t>0);
  }

  // render
  renderScene(ctx,canvas.width,canvas.height);
  drawWeapon(ctx,game.pl,t,shootAnim);
  renderMinimap(mctx);

  // HUD
  const hpFill=document.getElementById('hpFill');
  hpFill.style.width=game.pl.hp+'%';
  hpFill.style.background=game.pl.hp<30?'linear-gradient(90deg,#ff0000,#880000)': (game.pl.hp<60?'linear-gradient(90deg,#ffaa00,#ff5500)':'linear-gradient(90deg,#ff2a2a,#ff8a2a)');
  document.getElementById('hpTxt').textContent=Math.floor(game.pl.hp);
  const wep=WEAPONS[game.pl.curWeap];
  const ammoTxt=document.getElementById('ammoTxt');
  if(wep.infinite) ammoTxt.textContent='∞';
  else ammoTxt.textContent=game.pl.ammo[wep.ammoType]||0;
  document.getElementById('ammoType').textContent=wep.ammoType.toUpperCase();
  document.querySelectorAll('#wepIcons b').forEach(b=>{
    const id=parseInt(b.dataset.w);
    b.classList.toggle('active',id===game.pl.curWeap);
    b.style.opacity=game.pl.weapons[id]?'1':'0.35';
  });
  const alive=game.enemies.filter(e=>!e.dead).length;
  document.getElementById('enemyCount').textContent=alive;

  raf=requestAnimationFrame(loop);
}
loop(performance.now());

document.getElementById('playBtn').addEventListener('click',()=>{
  document.getElementById('startScreen').style.display='none';
  game.started=true;
  canvas.requestPointerLock?.();
  showMsg('FIND GOLD KEY - KILL BIO-TITAN',2800);
});
document.getElementById('replayBtn').addEventListener('click',()=>{
  document.getElementById('winScreen').style.display='none';
  game.pl.reset(); game.loadLevel(); game.started=true; showMsg('SECTOR RE-ENTERED',2000);
});
canvas.addEventListener('click',()=>{
  if(!game.started) return;
  if(!document.pointerLockElement) canvas.requestPointerLock?.();
  else { Input.mouseDown=true; Input.mouseDownTime=performance.now(); tryShoot(); }
});
window.addEventListener('blur',()=>{ Input.keys={}; });

// auto pickup proximity hint
setInterval(()=>{ if(game.started) game.tryPickup(); },200);
</script>
</body>
</html>
<!-- agent-meta {"model":"meta/muse-spark-1.1","provider":"openrouter","persona":"composer","sessionId":"c3684a99-52d1-4969-97ce-b0d14dbdc302","tokensIn":301472,"tokensOut":16983,"tokensTotal":318455,"turns":19,"toolCalls":19,"failedToolCalls":2,"timestamp":"2026-07-17T04:29:19.257Z"} -->