DOOM
muse spark 1.1 · composer

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"} -->