DOOM
Deepseek v4.1 Flash · full

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 52,985 bytes · SHA-256 bf0b3c8df2bf
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=no">
<title>CRIMSON SECTOR</title>
<style>
*{box-sizing:border-box;-webkit-user-select:none;user-select:none}
html,body{margin:0;padding:0;height:100%;background:#050508;overflow:hidden;
font-family:"Courier New",ui-monospace,monospace;color:#f2d9a8}
#stage{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);
width:min(100vw,calc(100vh*16/9));height:min(100vh,calc(100vw*9/16));
background:#000;overflow:hidden;
box-shadow:0 0 90px rgba(255,70,0,.12),0 0 0 2px #1c0e07,0 0 0 4px #000}
canvas#screen{display:block;width:100%;height:100%;image-rendering:pixelated;image-rendering:crisp-edges}
/* CRT layers */
#crt{position:absolute;inset:0;pointer-events:none;z-index:4;
background:repeating-linear-gradient(0deg,rgba(0,0,0,.3) 0px,rgba(0,0,0,.3) 1px,rgba(255,255,255,.02) 1px,rgba(255,255,255,.02) 3px);
mix-blend-mode:multiply;opacity:.45}
#vig{position:absolute;inset:0;pointer-events:none;z-index:4;
box-shadow:inset 0 0 90px 8px rgba(0,0,0,.72),inset 0 0 30px rgba(150,30,0,.18)}
/* ---------- HUD ---------- */
#hud{position:absolute;left:0;right:0;bottom:0;height:14.5%;z-index:3;
display:flex;align-items:stretch;gap:2px;padding:0 4px 4px 4px;
background:linear-gradient(180deg,rgba(24,14,10,.86),rgba(8,5,6,.97));
border-top:2px solid #ff5a1e;box-shadow:0 -6px 26px rgba(0,0,0,.8),inset 0 1px 0 rgba(255,140,60,.25)}
.cell{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;
background:linear-gradient(180deg,rgba(60,30,14,.35),rgba(0,0,0,.35));
border:1px solid rgba(255,120,40,.18);border-radius:2px;min-width:0;padding:1px 2px}
.cell .lbl{font-size:clamp(6px,1.05vw,11px);letter-spacing:.14em;color:#c98a5e;opacity:.95}
.cell .val{font-weight:bold;font-size:clamp(13px,2.5vw,27px);line-height:1.05;letter-spacing:.03em;
text-shadow:0 0 10px currentColor;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.val.hp{color:#ff4030}.val.ar{color:#54d0ff}.val.am{color:#ffc63a}.val.wp{color:#ffd8a0}.val.kl{color:#b9f36b;font-size:clamp(11px,2vw,22px)}
.val.low{animation:blink .5s steps(2,end) infinite}
@keyframes blink{50%{opacity:.35}}
#msg{position:absolute;left:0;right:0;bottom:15.5%;z-index:3;text-align:center;pointer-events:none;
font-size:clamp(9px,1.5vw,17px);letter-spacing:.16em;color:#ffcf6a;
text-shadow:0 0 12px rgba(255,120,0,.9),0 1px 0 #000;opacity:0;transition:opacity .25s}
#msg.show{opacity:1}
/* ---------- overlays ---------- */
.ov{position:absolute;inset:0;z-index:6;display:none;flex-direction:column;align-items:center;justify-content:center;
text-align:center;padding:4%;
background:radial-gradient(ellipse at 50% 46%, rgba(66,16,2,.52) 0%, rgba(16,5,3,.88) 55%, rgba(0,0,0,.96) 100%);
cursor:pointer}
.ov.show{display:flex}
.ov h1{margin:0;font-family:"Arial Black",Impact,"Segoe UI",sans-serif;font-weight:900;
font-size:clamp(26px,7.4vw,84px);line-height:1;letter-spacing:.06em;color:#ff5a1e;
text-shadow:0 0 16px rgba(255,80,0,.85),0 0 60px rgba(255,20,0,.55),0 4px 0 #4b0f00,0 8px 22px #000}
.ov h1.blue{color:#5bd8ff;text-shadow:0 0 16px rgba(60,200,255,.85),0 0 60px rgba(0,130,255,.5),0 4px 0 #001d33,0 8px 22px #000}
.ov h1.green{color:#8dff62;text-shadow:0 0 16px rgba(120,255,90,.85),0 0 60px rgba(0,200,60,.5),0 4px 0 #0b2a00,0 8px 22px #000}
.ov .sub{margin-top:.6em;font-size:clamp(9px,1.7vw,18px);letter-spacing:.34em;color:#ffb166}
.keys{margin-top:1.4em;display:grid;grid-template-columns:auto auto;gap:.32em 1.1em;
font-size:clamp(8px,1.35vw,14px);color:#d8c39b;letter-spacing:.08em}
.keys b{color:#ff8b3d;font-weight:bold}
.prompt{margin-top:1.5em;font-size:clamp(10px,1.9vw,20px);letter-spacing:.2em;color:#fff2d6;
animation:pulse 1.15s ease-in-out infinite}
@keyframes pulse{50%{opacity:.35}}
.stats{margin-top:1.2em;color:#e8cf9e;font-size:clamp(9px,1.5vw,16px);letter-spacing:.12em;line-height:1.9;
background:rgba(0,0,0,.45);border:1px solid rgba(255,120,40,.28);padding:.7em 1.4em;border-radius:3px}
.stats span{color:#ffd066;font-weight:bold}
</style>
</head>
<body>
<div id="stage">
<canvas id="screen"></canvas>
<div id="crt"></div>
<div id="vig"></div>
<div id="msg"></div>
<div id="hud">
<div class="cell"><div class="lbl">HEALTH</div><div class="val hp" id="hHealth">100</div></div>
<div class="cell"><div class="lbl">ARMOR</div><div class="val ar" id="hArmor">0</div></div>
<div class="cell"><div class="lbl">WEAPON</div><div class="val wp" id="hWeapon">PISTOL</div></div>
<div class="cell"><div class="lbl">AMMO</div><div class="val am" id="hAmmo">∞</div></div>
<div class="cell"><div class="lbl">HOSTILES</div><div class="val kl" id="hKills">0/0</div></div>
</div>
<div class="ov show" id="ovTitle">
<h1>CRIMSON SECTOR</h1>
<div class="sub">SECTOR 7 · PURGE PROTOCOL</div>
<div class="keys">
<b>W A S D</b><span>move / strafe</span>
<b>MOUSE</b><span>look (← → also turn)</span>
<b>CLICK / SPACE</b><span>fire</span>
<b>1 2 3 / WHEEL</b><span>switch weapon</span>
<b>SHIFT</b><span>sprint</span>
<b>TAB</b><span>map <b>M</b> mute</span>
</div>
<div class="prompt">CLICK TO DEPLOY</div>
</div>
<div class="ov" id="ovPause"><h1 class="blue">PAUSED</h1><div class="prompt">CLICK TO RESUME</div></div>
<div class="ov" id="ovDead">
<h1>YOU ARE DEAD</h1>
<div class="sub">THE SECTOR CLAIMS ANOTHER</div>
<div class="prompt">CLICK OR PRESS R TO REDEPLOY</div>
</div>
<div class="ov" id="ovWin">
<h1 class="green">SECTOR CLEARED</h1>
<div class="sub">PURGE PROTOCOL COMPLETE</div>
<div class="stats" id="winStats"></div>
<div class="prompt">CLICK TO REDEPLOY</div>
</div>
</div>
<script>
(function(){
'use strict';
/* ============================================================
CRIMSON SECTOR - a raycasting first person shooter
============================================================ */
/* ---------------- utils ---------------- */
const clamp=(v,a,b)=>v<a?a:(v>b?b:v);
const lerp=(a,b,t)=>a+(b-a)*t;
let _seed=987654321;
function rnd(){_seed=(_seed*1664525+1013904223)&0x7fffffff;return _seed/0x7fffffff;}
function mk(w,h){const c=document.createElement('canvas');c.width=w;c.height=h;return c;}
function dist2(ax,ay,bx,by){const dx=ax-bx,dy=ay-by;return dx*dx+dy*dy;}
/* ---------------- render canvas ---------------- */
const RW=480,RH=270,TEX=64,SHADES=9,FOV=0.72;
const cv=document.getElementById('screen');
cv.width=RW;cv.height=RH;
const ctx=cv.getContext('2d',{alpha:false});
ctx.imageSmoothingEnabled=false;
const stage=document.getElementById('stage');
const msgEl=document.getElementById('msg');
const ovTitle=document.getElementById('ovTitle'),ovPause=document.getElementById('ovPause');
const ovDead=document.getElementById('ovDead'),ovWin=document.getElementById('ovWin');
const elHealth=document.getElementById('hHealth'),elArmor=document.getElementById('hArmor');
const elWeapon=document.getElementById('hWeapon'),elAmmo=document.getElementById('hAmmo');
const elKills=document.getElementById('hKills'),elWinStats=document.getElementById('winStats');
/* ---------------- texture generation ---------------- */
function shadeVariants(src,base){
const out=[];
for(let i=0;i<SHADES;i++){
const t=mk(src.width,src.height),g=t.getContext('2d');
g.drawImage(src,0,0);
const b=i/(SHADES-1);
if(b>0){
g.globalCompositeOperation='source-atop';
g.fillStyle='rgba(8,5,12,'+(b*0.87).toFixed(3)+')';
g.fillRect(0,0,t.width,t.height);
}
out.push(t);
}
return out;
}
function texStone(){
const c=mk(TEX,TEX),g=c.getContext('2d');
g.fillStyle='#6c6c78';g.fillRect(0,0,TEX,TEX);
for(let y=0;y<TEX;y+=16){
for(let x=0;x<TEX;x+=32){
const off=((y/16)%2)*16;
g.fillStyle=rnd()>.5?'#787884':'#666672';
g.fillRect(x+off,y,32,16);
}
}
g.fillStyle='rgba(0,0,0,.55)';
for(let y=0;y<TEX;y+=16)g.fillRect(0,y,TEX,2);
for(let y=0;y<TEX;y+=16){const off=((y/16)%2)*16;for(let x=0;x<TEX;x+=32)g.fillRect(x+off,y,2,16);}
g.fillStyle='rgba(255,255,255,.07)';
for(let y=2;y<TEX;y+=16)g.fillRect(0,y,TEX,1);
for(let i=0;i<700;i++){const s=rnd()>.6?2:1;
g.fillStyle=rnd()>.5?'rgba(0,0,0,.13)':'rgba(255,255,255,.07)';
g.fillRect(rnd()*TEX|0,rnd()*TEX|0,s,s);}
return c;
}
function texBrick(){
const c=mk(TEX,TEX),g=c.getContext('2d');
g.fillStyle='#241110';g.fillRect(0,0,TEX,TEX);
const bw=16,bh=8;
for(let y=0;y<TEX;y+=bh){
const off=((y/bh)%2)*8;
for(let x=-bw;x<TEX;x+=bw){
const v=rnd();
g.fillStyle=v>.65?'#96392a':(v>.3?'#832f21':'#702619');
g.fillRect(x+off+1,y+1,bw-2,bh-2);
g.fillStyle='rgba(255,180,120,.10)';
g.fillRect(x+off+1,y+1,bw-2,1);
}
}
for(let i=0;i<500;i++){
g.fillStyle=rnd()>.5?'rgba(0,0,0,.16)':'rgba(255,150,80,.07)';
g.fillRect(rnd()*TEX|0,rnd()*TEX|0,2,2);
}
return c;
}
function texTech(){
const c=mk(TEX,TEX),g=c.getContext('2d');
g.fillStyle='#3b4856';g.fillRect(0,0,TEX,TEX);
g.fillStyle='#465566';g.fillRect(2,2,28,60);g.fillStyle='#313d4a';g.fillRect(34,2,28,60);
g.fillStyle='rgba(0,0,0,.6)';g.fillRect(31,0,3,TEX);
g.fillStyle='#20262e';g.fillRect(0,0,TEX,3);g.fillRect(0,30,TEX,3);g.fillRect(0,61,TEX,3);
for(let y=8;y<TEX;y+=14){
g.fillStyle='#8c9aa8';g.fillRect(36,y,3,3);g.fillRect(58,y,3,3);
g.fillStyle='#131820';g.fillRect(37,y+1,2,2);g.fillRect(59,y+1,2,2);
}
g.fillStyle='rgba(60,220,255,.55)';g.fillRect(8,6,2,52);
g.fillStyle='rgba(60,220,255,.18)';g.fillRect(5,6,8,52);
g.fillStyle='rgba(255,170,60,.5)';g.fillRect(20,44,2,14);
for(let i=0;i<420;i++){
g.fillStyle=rnd()>.5?'rgba(0,0,0,.14)':'rgba(190,220,255,.05)';
g.fillRect(rnd()*TEX|0,rnd()*TEX|0,1+(rnd()*2|0),1);
}
return c;
}
const WALLTEX=[null,shadeVariants(texStone()),shadeVariants(texBrick()),shadeVariants(texTech())];
/* ---------------- pixel sprite helper ---------------- */
function px(rows,pal,scale){
const w=rows[0].length,h=rows.length;
const c=mk(w*scale,h*scale),g=c.getContext('2d');
for(let y=0;y<h;y++){
const row=rows[y];
for(let x=0;x<w;x++){
const ch=row[x];
if(ch==='.'||ch===undefined)continue;
const col=pal[ch];if(!col)continue;
g.fillStyle=col;g.fillRect(x*scale,y*scale,scale,scale);
}
}
return c;
}
function flashVariant(src,r,gc,b){
const c=mk(src.width,src.height),g=c.getContext('2d');
g.drawImage(src,0,0);
g.globalCompositeOperation='source-atop';
g.fillStyle='rgba('+r+','+gc+','+b+',0.72)';
g.fillRect(0,0,c.width,c.height);
return c;
}
/* ---------------- enemy sprites ---------------- */
const IMP_ART=[
"o....oo....o",
".o..obbo..o.",
"..oobbbboo..",
".obbbbbbbbo.",
".obeebbeebo.",
".obbbbbbbbo.",
".obbwwwwbbo.",
"..obbbbbbo..",
"..obbbbbbo..",
".obbbbbbbbo.",
"obboobboobbo",
"ob..obbo..bo",
"o...o..o...o",
"....o..o...."
];
const SOLDIER_ART=[
"....oooo....",
"...oaaaao...",
"..oaaaaaao..",
"..ovvvvvvo..",
"..oaaaaaao..",
"..oaaaaaao..",
".oaaaaaaaao.",
".oaalaalaao.",
".oaaaaagaao.",
".ooaaaaggoo.",
"...oaaaao...",
"..oa...ao...",
"..oa...ao...",
"..oo...oo...",
"..oo...oo..."
];
const BRUTE_ART=[
".....oooo.....",
"....obbbbo....",
"....obeebo....",
"....obbbbo....",
"...obbbbbbo...",
"..obbbbbbbbo..",
".obbbbbbbbbbo.",
"obobbllllbbobo",
"obbbbbbbbbbbbo",
".obbbbbbbbbbo.",
".obbwwwwwwbbo.",
".obbbbbbbbbbo.",
"..obbbbbbbbo..",
"..obbbbbbbbo..",
".obbo.oo.obbo.",
".obo...oo..obo",
".oo........oo."
];
const IMP=px(IMP_ART,{o:'#361008',b:'#8f3517',e:'#ffe14d',w:'#f5e9c9'},4);
const SOLDIER=px(SOLDIER_ART,{o:'#141c26',a:'#4a6b8a',l:'#7fa3c4',v:'#3ce8ff',g:'#24242c'},4);
const BRUTE=px(BRUTE_ART,{o:'#1a2412',b:'#3d6b2c',l:'#63a03f',e:'#ff5a24',w:'#e9e2c2'},4);
function spriteSet(src,hWorld){
return {tex:src,shades:shadeVariants(src,0),flash:flashVariant(src,255,240,220),
hurt:flashVariant(src,255,60,20),w:src.width,h:src.height,hw:hWorld};
}
const SPR={
imp:spriteSet(IMP,0.92),
soldier:spriteSet(SOLDIER,1.0),
brute:spriteSet(BRUTE,1.55)
};
/* ---------------- pickup sprites ---------------- */
function pickupTex(kind){
const S=16,scale=3;
const c=mk(S*scale,S*scale),g=c.getContext('2d');
g.scale(scale,scale);
const box=(x,y,w,h,a,b)=>{g.fillStyle=a;g.fillRect(x,y,w,h);g.fillStyle=b;g.fillRect(x+1,y+1,w-2,1);};
if(kind==='health'){
box(2,4,12,10,'#e8e8ee','#ffffff');
g.fillStyle='#c8121a';g.fillRect(7,6,2,7);g.fillRect(5,8,6,2);
g.fillStyle='#7a7a86';g.fillRect(2,4,12,1);
}else if(kind==='bullets'){
box(2,5,12,9,'#3c3a30','#6b6754');
for(let i=0;i<4;i++){g.fillStyle='#e8c25a';g.fillRect(3+i*3,7,2,5);g.fillStyle='#a3822c';g.fillRect(3+i*3,11,2,1);}
}else if(kind==='shells'){
box(2,5,12,9,'#3c2a1a','#6b4a2c');
for(let i=0;i<3;i++){g.fillStyle='#c8202a';g.fillRect(3+i*4,6,3,5);g.fillStyle='#e0c060';g.fillRect(3+i*4,10,3,2);}
}else if(kind==='cells'){
box(3,3,10,12,'#1d3346','#39597a');
g.fillStyle='#39e0ff';g.fillRect(5,5,6,7);
g.fillStyle='#d9fbff';g.fillRect(6,6,2,5);
g.fillStyle='#7a8fa0';g.fillRect(6,2,4,2);
}else if(kind==='armor'){
g.fillStyle='#1c3a18';g.beginPath();g.moveTo(8,1);g.lineTo(14,4);g.lineTo(12,13);g.lineTo(8,15);
g.lineTo(4,13);g.lineTo(2,4);g.closePath();g.fill();
g.fillStyle='#4fd45a';g.beginPath();g.moveTo(8,3);g.lineTo(12,5);g.lineTo(10.5,12);g.lineTo(8,13.5);
g.lineTo(5.5,12);g.lineTo(4,5);g.closePath();g.fill();
g.fillStyle='rgba(220,255,220,.65)';g.fillRect(7,4,1.5,8);
}else if(kind==='shotgun'){
g.fillStyle='#8a5a2a';g.fillRect(2,10,7,4);
g.fillStyle='#5b5f6b';g.fillRect(4,4,11,3);g.fillRect(4,8,11,3);
g.fillStyle='#2a2d36';g.fillRect(4,4,11,1);g.fillRect(4,8,11,1);
g.fillStyle='#c9ccd6';g.fillRect(13,4,2,3);g.fillRect(13,8,2,3);
}else if(kind==='chaingun'){
g.fillStyle='#2a2e38';g.fillRect(2,9,9,5);
g.fillStyle='#6b7280';g.fillRect(6,3,9,8);
for(let i=0;i<4;i++){g.fillStyle='#aeb6c4';g.fillRect(7+i*2,2,1,9);}
g.fillStyle='#ffb020';g.fillRect(3,11,2,2);
}
return c;
}
const PICK={
health:spriteSet(pickupTex('health'),0.42),
bullets:spriteSet(pickupTex('bullets'),0.36),
shells:spriteSet(pickupTex('shells'),0.36),
cells:spriteSet(pickupTex('cells'),0.40),
armor:spriteSet(pickupTex('armor'),0.48),
shotgun:spriteSet(pickupTex('shotgun'),0.5),
chaingun:spriteSet(pickupTex('chaingun'),0.5)
};
/* ---------------- projectile + exit textures ---------------- */
function orbTex(c1,c2){
const S=24,c=mk(S,S),g=c.getContext('2d');
const gr=g.createRadialGradient(S/2,S/2,0,S/2,S/2,S/2);
gr.addColorStop(0,'#ffffff');gr.addColorStop(0.28,c1);gr.addColorStop(0.65,c2);gr.addColorStop(1,'rgba(0,0,0,0)');
g.fillStyle=gr;g.fillRect(0,0,S,S);
return c;
}
const ORB_FIRE=orbTex('#ffd24d','#ff3a08');
const ORB_PLASMA=orbTex('#a8f0ff','#1d6bff');
const EXIT_TEX=(function(){
const S=48,c=mk(S,64),g=c.getContext('2d');
const gr=g.createLinearGradient(0,0,0,64);
gr.addColorStop(0,'rgba(120,255,150,.15)');gr.addColorStop(0.5,'rgba(90,255,130,.85)');gr.addColorStop(1,'rgba(20,255,120,.12)');
g.fillStyle=gr;g.fillRect(10,0,28,64);
g.fillStyle='rgba(230,255,230,.75)';
for(let i=0;i<14;i++){g.fillRect(10+rnd()*26|0,rnd()*64|0,2,4+rnd()*8|0);}
return c;
})();
/* ---------------- map ---------------- */
const MW=24,MH=24;
const grid=new Uint8Array(MW*MH);
function setT(x,y,v){if(x>=0&&x<MW&&y>=0&&y<MH)grid[y*MW+x]=v;}
function tileAt(x,y){
const mx=Math.floor(x),my=Math.floor(y);
if(mx<0||my<0||mx>=MW||my>=MH)return 1;
return grid[my*MW+mx];
}
function fillR(x0,y0,x1,y1,v){for(let y=y0;y<=y1;y++)for(let x=x0;x<=x1;x++)setT(x,y,v);}
function buildMap(){
grid.fill(0);
fillR(0,0,MW-1,0,3);fillR(0,MH-1,MW-1,MH-1,3);
fillR(0,0,0,MH-1,3);fillR(MW-1,0,MW-1,MH-1,3);
// left / right dividers with doorways
fillR(6,1,6,10,1);fillR(6,14,6,22,1);
fillR(17,1,17,10,1);fillR(17,14,17,22,1);
// central vault (exit)
fillR(10,8,14,8,2);fillR(10,16,14,16,2);
fillR(10,8,10,16,2);fillR(14,8,14,16,2);
setT(12,8,0);setT(12,16,0);
// cover / pillars
fillR(2,4,3,4,1);fillR(2,19,3,19,1);
fillR(20,4,21,4,1);fillR(20,19,21,19,1);
fillR(8,2,8,3,1);fillR(15,2,16,2,1);
fillR(8,20,8,21,1);fillR(15,21,16,21,1);
fillR(3,8,4,8,3);fillR(19,8,20,8,3);
fillR(3,16,4,16,3);fillR(19,16,20,16,3);
// small side rooms in the left band
fillR(1,2,1,2,1);
}
const EXIT={x:12.5,y:12.5,r:0.75};
const PLAYER_START={x:2.5,y:12.5,a:0};
/* ---------------- entity storage ---------------- */
let enemies=[],pickups=[],projectiles=[],particles=[];
const SPAWN_ENEMIES=[
[4.5,3.5,'imp'],[21.5,4.5,'soldier'],[8.5,4.5,'imp'],[12.5,5.5,'soldier'],
[15.5,3.5,'imp'],[21.5,11.5,'soldier'],[8.5,12.5,'brute'],[16.5,19.5,'imp'],
[3.5,20.5,'soldier'],[12.5,20.5,'brute'],[20.5,20.5,'soldier'],[12.5,2.5,'imp'],
[8.5,17.5,'imp'],[16.5,9.5,'soldier'],[4.5,16.5,'imp'],[21.5,16.5,'imp']
];
const SPAWN_PICKUPS=[
[3.5,6.5,'health'],[20.5,6.5,'health'],[12.5,21.5,'health'],[7.5,19.5,'health'],
[4.5,12.5,'bullets'],[21.5,13.5,'bullets'],[8.5,6.5,'bullets'],[16.5,6.5,'bullets'],
[3.5,9.5,'shells'],[20.5,9.5,'shells'],[12.5,3.5,'shells'],[9.5,21.5,'shells'],
[2.5,20.5,'cells'],[22.5,2.5,'cells'],[12.5,7.5,'cells'],[12.5,17.5,'cells'],
[2.5,3.5,'shotgun'],[22.5,21.5,'chaingun'],[22.5,11.5,'armor'],[3.5,13.5,'armor'],
[8.5,9.5,'armor'],[16.5,15.5,'health']
];
function makeEnemy(x,y,type){
const T=ETYPES[type];
return {x:x,y:y,type:type,hp:T.hp,maxhp:T.hp,r:T.r,height:T.height,
awake:false,cd:0.6+rnd()*0.8,state:'idle',animT:rnd()*6,hurt:0,
sx:x,sy:y,dying:0,strafe:rnd()>.5?1:-1,strafeT:0};
}
function resetWorld(){
buildMap();
enemies=SPAWN_ENEMIES.map(s=>makeEnemy(s[0],s[1],s[2]));
pickups=SPAWN_PICKUPS.map(s=>({x:s[0],y:s[1],kind:s[2],t:rnd()*6,gone:false}));
projectiles=[];particles=[];
miniTex=null;
totalEnemies=enemies.length;
}
let totalEnemies=0;
/* ---------------- player ---------------- */
const WEAPONS={
pistol:{name:'PISTOL',dmg:16,rate:0.30,spread:0.014,pellets:1,ammo:null,refire:true,sfx:'pistol',kick:5},
shotgun:{name:'SHOTGUN',dmg:12,rate:0.80,spread:0.105,pellets:7,ammo:'shells',refire:true,sfx:'shotgun',kick:14},
chaingun:{name:'CHAIN GUN',dmg:10,rate:0.085,spread:0.055,pellets:1,ammo:'cells',refire:true,sfx:'chaingun',kick:6}
};
const ETYPES={
imp:{hp:38,speed:1.75,r:0.32,height:0.85,dmg:11,melee:1.35,cd:0.95,pJitter:0.045,
fireRange:10,keep:1.2,score:10,projSpeed:5.2,projDmg:11,sight:15},
soldier:{hp:58,speed:1.25,r:0.32,height:1.0,dmg:9,melee:1.4,cd:1.5,pJitter:0.09,
fireRange:12,keep:5.0,score:15,projSpeed:8.5,projDmg:10,sight:16},
brute:{hp:140,speed:1.05,r:0.46,height:1.55,dmg:24,melee:1.7,cd:1.4,pJitter:0.06,
fireRange:9,keep:1.4,score:30,projSpeed:4.2,projDmg:16,sight:14,heavy:true}
};
const P={x:PLAYER_START.x,y:PLAYER_START.y,a:PLAYER_START.a,
hp:100,maxhp:100,armor:0,weapon:'pistol',
owned:{pistol:true,shotgun:false,chaingun:false},
ammo:{shells:8,cells:0},
bob:0,recoil:0,cd:0,hurt:0,pickupFlash:0,flash:0,spin:0,
dead:false,deaths:0,kills:0,time:0,unlocked:false,shake:0};
let state='title'; // title | playing | paused | dead | won
let msgTimer=0, minimap=true, lastHitMarker=0;
/* ---------------- audio ---------------- */
const AU={ctx:null,master:null,muted:false,noise:null};
function initAudio(){
if(AU.ctx)return;
const AC=window.AudioContext||window.webkitAudioContext;
if(!AC)return;
AU.ctx=new AC();
AU.master=AU.ctx.createGain();
AU.master.gain.value=0.5;
AU.master.connect(AU.ctx.destination);
const len=AU.ctx.sampleRate*1.0;
const buf=AU.ctx.createBuffer(1,len,AU.ctx.sampleRate);
const d=buf.getChannelData(0);
for(let i=0;i<len;i++)d[i]=Math.random()*2-1;
AU.noise=buf;
}
function tone(f0,f1,dur,type,vol,delay){
if(!AU.ctx||AU.muted)return;
const t=AU.ctx.currentTime+(delay||0);
const o=AU.ctx.createOscillator(),g=AU.ctx.createGain();
o.type=type||'square';o.frequency.setValueAtTime(f0,t);
if(f1&&f1!==f0)o.frequency.exponentialRampToValueAtTime(Math.max(20,f1),t+dur);
g.gain.setValueAtTime(0.0001,t);
g.gain.exponentialRampToValueAtTime(vol||0.08,t+0.008);
g.gain.exponentialRampToValueAtTime(0.0001,t+dur);
o.connect(g);g.connect(AU.master);o.start(t);o.stop(t+dur+0.05);
}
function noiseHit(dur,freq,q,vol,delay,type){
if(!AU.ctx||AU.muted)return;
const t=AU.ctx.currentTime+(delay||0);
const s=AU.ctx.createBufferSource();s.buffer=AU.noise;
const f=AU.ctx.createBiquadFilter();f.type=type||'lowpass';f.frequency.value=freq;f.Q.value=q||1;
const g=AU.ctx.createGain();
g.gain.setValueAtTime(vol,t);
g.gain.exponentialRampToValueAtTime(0.0001,t+dur);
s.connect(f);f.connect(g);g.connect(AU.master);s.start(t);s.stop(t+dur+0.02);
}
function sfx(name){
if(!AU.ctx||AU.muted)return;
switch(name){
case 'pistol': noiseHit(0.09,2600,1.2,0.22);tone(420,90,0.09,'square',0.06);break;
case 'shotgun':noiseHit(0.32,1500,0.9,0.32);noiseHit(0.15,500,0.7,0.2);tone(160,50,0.22,'sawtooth',0.09);break;
case 'chaingun':noiseHit(0.06,3200,1.0,0.15);tone(300,140,0.05,'square',0.045);break;
case 'empty': tone(180,120,0.06,'square',0.05);break;
case 'ehit': noiseHit(0.07,900,1,0.16);tone(300,160,0.07,'square',0.04);break;
case 'edie': tone(180,42,0.5,'sawtooth',0.1);noiseHit(0.4,700,0.7,0.14);break;
case 'ehurt':tone(220,140,0.1,'square',0.05);break;
case 'eshoot':tone(760,220,0.13,'sawtooth',0.07);break;
case 'fire': noiseHit(0.3,600,0.8,0.12,null,'bandpass');break;
case 'pickup':tone(680,690,0.07,'triangle',0.09);tone(1020,1030,0.09,'triangle',0.08,0.07);break;
case 'weapon':tone(300,310,0.09,'square',0.09);tone(500,510,0.09,'square',0.08,0.09);tone(760,770,0.12,'square',0.07,0.18);break;
case 'phurt': noiseHit(0.26,700,0.8,0.26);tone(220,80,0.2,'sawtooth',0.1);break;
case 'armor': tone(400,900,0.14,'triangle',0.08);break;
case 'growl': noiseHit(0.5,320,1.4,0.13);tone(120,70,0.45,'sawtooth',0.07);break;
case 'unlock':
[523,659,784,1046].forEach((f,i)=>tone(f,f,0.22,'triangle',0.1,i*0.12));break;
case 'win':
[523,659,784,1046,1318,1568].forEach((f,i)=>tone(f,f,0.4,'triangle',0.11,i*0.16));break;
case 'dead':
[400,300,220,150,90].forEach((f,i)=>tone(f,f*0.7,0.5,'sawtooth',0.12,i*0.2));break;
}
}
/* ---------------- input ---------------- */
const keys={};
let locked=false,mouseDown=false,wheelAcc=0;
function setState(s){
state=s;
ovTitle.classList.toggle('show',s==='title');
ovPause.classList.toggle('show',s==='paused');
ovDead.classList.toggle('show',s==='dead');
ovWin.classList.toggle('show',s==='won');
}
function showMsg(text,dur){
msgEl.textContent=text;msgEl.classList.add('show');
msgTimer=dur||2.6;
}
document.addEventListener('keydown',e=>{
keys[e.code]=true;
if(e.code==='Tab')e.preventDefault();
if(e.code==='Space')e.preventDefault();
if(e.code==='KeyM'){AU.muted=!AU.muted;showMsg(AU.muted?'AUDIO MUTED':'AUDIO ON',1.2);}
if(e.code==='KeyP'&&state==='playing'){setState('paused');if(document.exitPointerLock)document.exitPointerLock();}
if(e.code==='Tab'&&state==='playing'){minimap=!minimap;}
if(state==='playing'){
if(e.code==='Digit1')selectWeapon('pistol');
if(e.code==='Digit2')selectWeapon('shotgun');
if(e.code==='Digit3')selectWeapon('chaingun');
if(e.code==='KeyQ')cycleWeapon(1);
}
if(e.code==='KeyR'&&(state==='dead'))respawn();
});
document.addEventListener('keyup',e=>{keys[e.code]=false;});
document.addEventListener('mousemove',e=>{
if(locked&&state==='playing')P.a+=e.movementX*0.0026;
});
cv.addEventListener('mousedown',e=>{if(e.button===0)mouseDown=true;});
window.addEventListener('mouseup',()=>{mouseDown=false;});
cv.addEventListener('wheel',e=>{
if(state==='playing'){e.preventDefault();cycleWeapon(e.deltaY>0?1:-1);}
},{passive:false});
document.addEventListener('pointerlockchange',()=>{
const was=locked;
locked=(document.pointerLockElement===cv);
if(was&&!locked&&state==='playing'){setState('paused');}
});
function requestLock(){try{cv.requestPointerLock();}catch(err){}}
function clickHandler(){
initAudio();
if(AU.ctx&&AU.ctx.state==='suspended')AU.ctx.resume();
if(state==='title'){startGame();}
else if(state==='paused'){setState('playing');requestLock();}
else if(state==='dead'){respawn();}
else if(state==='won'){startGame();}
else if(state==='playing'){requestLock();}
}
stage.addEventListener('click',clickHandler);
/* ---------------- player helpers ---------------- */
function blocked(x,y,r){
if(tileAt(x-r,y-r)||tileAt(x+r,y-r)||tileAt(x-r,y+r)||tileAt(x+r,y+r))return true;
if(tileAt(x,y-r)||tileAt(x,y+r)||tileAt(x-r,y)||tileAt(x+r,y))return true;
return false;
}
function applyDamage(hp){
const absorbed=Math.min(P.armor,hp*0.5);
P.armor-=absorbed;hp-=absorbed;
P.hp-=hp;P.hurt=0.6;P.shake=6;
if(P.hp<=0){P.hp=0;die();}
else sfx('phurt');
}
function die(){
if(P.dead)return;
P.dead=true;P.deaths++;
sfx('dead');
if(document.exitPointerLock)document.exitPointerLock();
setState('dead');
}
function respawn(){
P.dead=false;P.hp=P.maxhp;P.armor=0;P.hurt=0;
P.x=PLAYER_START.x;P.y=PLAYER_START.y;P.a=PLAYER_START.a;
if(P.ammo.shells<6)P.ammo.shells=6;
if(P.owned.chaingun&&P.ammo.cells<25)P.ammo.cells=25;
// reset living enemies to their posts so the spawn is safe
enemies.forEach(e=>{
if(e.hp<=0)return;
e.x=e.sx;e.y=e.sy;e.awake=false;e.cd=1.2+rnd();e.state='idle';e.hurt=0;
});
projectiles.length=0;
setState('playing');
requestLock();
showMsg('REDEPLOYED',1.6);
}
function startGame(){
resetWorld();
P.x=PLAYER_START.x;P.y=PLAYER_START.y;P.a=PLAYER_START.a;
P.hp=100;P.armor=0;P.weapon='pistol';
P.owned={pistol:true,shotgun:false,chaingun:false};
P.ammo={shells:8,cells:0};
P.kills=0;P.deaths=0;P.time=0;P.dead=false;P.unlocked=false;P.hurt=0;
projectiles.length=0;particles.length=0;
setState('playing');
requestLock();
showMsg('FIND AND PURGE ALL HOSTILES',3.0);
}
function selectWeapon(w){
if(!P.owned[w]||P.weapon===w)return;
P.weapon=w;P.cd=Math.max(P.cd,0.22);
sfx('weapon');showMsg(WEAPONS[w].name,1.0);
}
function cycleWeapon(dir){
const list=['pistol','shotgun','chaingun'].filter(w=>P.owned[w]);
const i=list.indexOf(P.weapon);
selectWeapon(list[(i+dir+list.length)%list.length]);
}
/* ---------------- world queries ---------------- */
function wallDist(x,y,dx,dy,maxD){
let mx=Math.floor(x),my=Math.floor(y);
const ddx=dx===0?1e30:Math.abs(1/dx),ddy=dy===0?1e30:Math.abs(1/dy);
let stepX,stepY,sdx,sdy;
if(dx<0){stepX=-1;sdx=(x-mx)*ddx;}else{stepX=1;sdx=(mx+1-x)*ddx;}
if(dy<0){stepY=-1;sdy=(y-my)*ddy;}else{stepY=1;sdy=(my+1-y)*ddy;}
let side=0,guard=0,d=0;
while(guard++<256){
if(sdx<sdy){d=sdx;sdx+=ddx;mx+=stepX;side=0;}
else{d=sdy;sdy+=ddy;my+=stepY;side=1;}
if(d>maxD)return maxD;
if(tileAt(mx,my))return Math.max(0.0001,d);
}
return maxD;
}
function lineClear(x0,y0,x1,y1){
const dx=x1-x0,dy=y1-y0;
const d=Math.hypot(dx,dy);
if(d<0.001)return true;
const steps=Math.ceil(d/0.14);
for(let i=1;i<steps;i++){
const t=i/steps;
if(tileAt(x0+dx*t,y0+dy*t))return false;
}
return true;
}
function enemyAtRay(ox,oy,dx,dy,maxT){
let best=null,bestT=maxT;
for(const e of enemies){
if(e.hp<=0)continue;
const cx=e.x-ox,cy=e.y-oy;
const proj=cx*dx+cy*dy;
if(proj<0||proj>bestT)continue;
const perp2=cx*cx+cy*cy-proj*proj;
const rr=e.r*e.r*1.25;
if(perp2>rr)continue;
const t=proj-Math.sqrt(Math.max(0,rr-perp2));
if(t<bestT){bestT=t;best=e;}
}
return best?{e:best,t:bestT}:null;
}
/* ---------------- combat ---------------- */
function spawnParticles(x,y,z,n,color,spd,size,life){
for(let i=0;i<n;i++){
const a=rnd()*Math.PI*2,sp=rnd()*spd;
particles.push({x:x,y:y,z:z,vx:Math.cos(a)*sp,vy:Math.sin(a)*sp,vz:rnd()*spd*0.9*0.5+0.4,
life:life,max:life,color:color,size:size*(0.6+rnd()*0.8)});
}
}
function damageEnemy(e,dmg,hitX,hitY,hitZ){
e.hp-=dmg;e.hurt=0.14;
spawnParticles(hitX,hitY,hitZ||0.5,5,'#b81414',2.2,0.05,0.42);
if(e.hp<=0){
e.hp=0;P.kills++;sfx('edie');
spawnParticles(e.x,e.y,0.5,16,'#8c1010',3.0,0.07,0.7);
if(P.kills>=totalEnemies&&!P.unlocked){
P.unlocked=true;sfx('unlock');
showMsg('ALL HOSTILES DOWN - EXIT UNLOCKED',4.0);
}
}else{
sfx('ehit');
if(!e.awake){e.awake=true;}
}
}
function fireWeapon(){
const W=WEAPONS[P.weapon];
if(W.ammo&&P.ammo[W.ammo]<=0){sfx('empty');showMsg('OUT OF AMMO - SWITCH WEAPON',1.2);P.cd=0.45;return;}
if(W.ammo)P.ammo[W.ammo]--;
P.cd=W.rate;P.recoil=Math.min(1,P.recoil+1);P.flash=0.06;P.shake=W.kick;
sfx(W.sfx);
for(let i=0;i<W.pellets;i++){
const ang=P.a+(rnd()-0.5)*W.spread*2;
const dx=Math.cos(ang),dy=Math.sin(ang);
const wd=wallDist(P.x,P.y,dx,dy,30);
const hit=enemyAtRay(P.x,P.y,dx,dy,wd);
if(hit){
const hx=P.x+dx*hit.t,hy=P.y+dy*hit.t;
damageEnemy(hit.e,W.dmg,hx,hy,0.45+rnd()*0.5);
if(i===0)lastHitMarker=0.16;
}else{
if(wd<30){
const hx=P.x+dx*wd*0.99,hy=P.y+dy*wd*0.99;
spawnParticles(hx,hy,0.5,3,'#ffcf6a',1.6,0.035,0.22);
}
}
}
}
/* ---------------- enemies ---------------- */
function enemyShoot(e,type){
const T=ETYPES[e.type];
const ang=Math.atan2(P.y-e.y,P.x-e.x)+(rnd()-0.5)*T.pJitter*2;
const sp=T.projSpeed;
projectiles.push({x:e.x+Math.cos(ang)*0.4,y:e.y+Math.sin(ang)*0.4,
vx:Math.cos(ang)*sp,vy:Math.sin(ang)*sp,dmg:T.projDmg,life:4.0,t:0,
kind:type,size:type==='fire'?0.30:(type==='heavy'?0.5:0.22),h:0.55});
sfx('eshoot');
}
function updateEnemies(dt){
for(const e of enemies){
if(e.hp<=0){e.deadT=(e.deadT||0)+dt;continue;}
e.animT+=dt;
if(e.hurt>0)e.hurt-=dt;
if(e.cd>0)e.cd-=dt;
const T=ETYPES[e.type];
const dx=P.x-e.x,dy=P.y-e.y;
const d=Math.hypot(dx,dy)||0.001;
const los=d<T.sight&&lineClear(e.x,e.y,P.x,P.y);
if(!e.awake){
if(los&&!P.dead){e.awake=true;sfx('growl');}
else continue;
}
if(P.dead){e.state='idle';continue;}
const nx=dx/d,ny=dy/d;
// ---- decide movement ----
let mx=0,my=0;
if(d>T.keep+0.35){mx=nx;my=ny;}
else if(d<T.keep-0.35){mx=-nx;my=-ny;}
e.strafeT-=dt;
if(e.strafeT<=0){e.strafeT=0.8+rnd()*1.6;e.strafe=rnd()>.5?1:-1;}
if(los&&d<T.fireRange*0.8){
mx+=-ny*e.strafe*0.75;my+=nx*e.strafe*0.75;
}
const ml=Math.hypot(mx,my);
if(ml>0.01){
mx/=ml;my/=ml;
const sp=T.speed*dt;
const px2=e.x+mx*sp,py2=e.y+my*sp;
if(!blocked(px2,e.y,e.r))e.x=px2;
if(!blocked(e.x,py2,e.r))e.y=py2;
}
// separation so they do not stack
for(const o of enemies){
if(o===e||o.hp<=0)continue;
const ox=e.x-o.x,oy=e.y-o.y;
const od=Math.hypot(ox,oy);
const minD=e.r+o.r+0.05;
if(od>0.0001&&od<minD){
const push=(minD-od)*0.5;
const px3=e.x+(ox/od)*push,py3=e.y+(oy/od)*push;
if(!blocked(px3,e.y,e.r))e.x=px3;
if(!blocked(e.x,py3,e.r))e.y=py3;
}
}
// ---- attack ----
if(e.cd<=0){
if(d<T.melee){
e.cd=T.cd+T.cd*0.3;e.attackAnim=0.25;
applyDamage(T.dmg);
}else if(los&&d<T.fireRange&&e.cd<=0){
e.cd=T.cd*(0.85+rnd()*0.4);e.attackAnim=0.25;
if(e.type==='imp')enemyShoot(e,'fire');
else if(e.type==='brute')enemyShoot(e,rnd()>0.45?'heavy':'fire');
else enemyShoot(e,'plasma');
}
}
if(e.attackAnim>0)e.attackAnim-=dt;
}
}
/* ---------------- projectiles / particles / pickups ---------------- */
function updateProjectiles(dt){
for(let i=projectiles.length-1;i>=0;i--){
const p=projectiles[i];
p.t+=dt;p.life-=dt;
const steps=Math.max(1,Math.ceil(Math.hypot(p.vx,p.vy)*dt/0.25));
let dead=false;
for(let s=0;s<steps;s++){
const nx=p.x+p.vx*dt/steps,ny=p.y+p.vy*dt/steps;
if(tileAt(nx,ny)){
spawnParticles(nx,ny,0.55,8,p.kind==='plasma'?'#9ff0ff':'#ffb03a',2.2,0.05,0.35);
dead=true;break;
}
p.x=nx;p.y=ny;
if(!P.dead&&dist2(p.x,p.y,P.x,P.y)<0.16*0.16+0.05){
applyDamage(p.dmg);dead=true;break;
}
}
if(dead||p.life<=0)projectiles.splice(i,1);
}
}
function updateParticles(dt){
for(let i=particles.length-1;i>=0;i--){
const p=particles[i];
p.life-=dt;
if(p.life<=0){particles.splice(i,1);continue;}
p.x+=p.vx*dt;p.y+=p.vy*dt;p.vz-=9.5*dt;p.z+=p.vz*dt;
if(p.z<0.06){p.z=0.06;p.vz*=-0.32;p.vx*=0.6;p.vy*=0.6;}
p.vx*=0.965;p.vy*=0.965;
}
}
function updatePickups(dt){
for(const pk of pickups){
if(pk.gone)continue;
pk.t+=dt;
const d2=dist2(pk.x,pk.y,P.x,P.y);
if(d2<0.45*0.45){
let taken=true,txt='';
switch(pk.kind){
case 'health': if(P.hp>=P.maxhp)taken=false; else {P.hp=Math.min(P.maxhp,P.hp+25);txt='+25 HEALTH';} break;
case 'armor': if(P.armor>=100)taken=false; else {P.armor=Math.min(100,P.armor+50);txt='+50 ARMOR';sfx('armor');} break;
case 'bullets': if(P.owned.shotgun&&P.ammo.shells>=60)taken=false; else {P.ammo.shells=Math.min(60,P.ammo.shells+8);txt='+8 SHELLS';} break;
case 'shells': P.ammo.shells=Math.min(99,P.ammo.shells+8);txt='+8 SHELLS';break;
case 'cells': P.ammo.cells=Math.min(200,P.ammo.cells+45);txt='+45 CELLS';break;
case 'shotgun': P.owned.shotgun=true;P.ammo.shells=Math.min(99,P.ammo.shells+10);txt='SHOTGUN ACQUIRED';selectWeapon('shotgun');break;
case 'chaingun': P.owned.chaingun=true;P.ammo.cells=Math.min(200,P.ammo.cells+70);txt='CHAIN GUN ACQUIRED';selectWeapon('chaingun');break;
}
if(taken){
pk.gone=true;
if(pk.kind!=='armor'&&pk.kind!=='shotgun'&&pk.kind!=='chaingun')sfx('pickup');
if(pk.kind==='shotgun'||pk.kind==='chaingun')sfx('weapon');
if(txt)showMsg(txt,1.6);
P.pickupFlash=0.25;
spawnParticles(pk.x,pk.y,0.4,8,'#ffe08a',1.4,0.04,0.4);
}
}
}
}
/* ---------------- player update ---------------- */
function updatePlayer(dt){
if(P.dead)return;
const run=(keys['ShiftLeft']||keys['ShiftRight'])?1.65:1;
const spd=2.9*run*dt;
let fx=0,fy=0;
const dxx=Math.cos(P.a),dyy=Math.sin(P.a);
if(keys['KeyW']||keys['ArrowUp']){fx+=dxx;fy+=dyy;}
if(keys['KeyS']||keys['ArrowDown']){fx-=dxx;fy-=dyy;}
if(keys['KeyD']){fx+=-dyy;fy+=dxx;}
if(keys['KeyA']){fx+=dyy;fy+=-dxx;}
if(keys['ArrowLeft'])P.a-=2.1*dt;
if(keys['ArrowRight'])P.a+=2.1*dt;
const ml=Math.hypot(fx,fy);
let moving=false;
if(ml>0.01){
fx/=ml;fy/=ml;moving=true;
const nx=P.x+fx*spd,ny=P.y+fy*spd;
if(!blocked(nx,P.y,0.22))P.x=nx;
if(!blocked(P.x,ny,0.22))P.y=ny;
}
P.bob+=dt*(moving?9.5*run:2.2);
if(!moving)P.bob=lerp(P.bob,Math.round(P.bob/Math.PI)*Math.PI,Math.min(1,dt*4));
if(P.hurt>0)P.hurt-=dt;
if(P.pickupFlash>0)P.pickupFlash-=dt;
if(P.flash>0)P.flash-=dt;
if(P.shake>0)P.shake=Math.max(0,P.shake-dt*32);
P.recoil=Math.max(0,P.recoil-dt*6.5);
P.spin+=dt*18;
// firing
if(P.cd>0)P.cd-=dt;
if((mouseDown||keys['Space']||keys['ControlLeft'])&&P.cd<=0)fireWeapon();
// exit
if(!P.unlocked&&P.kills>=totalEnemies&&totalEnemies>0){P.unlocked=true;}
if(P.unlocked&&dist2(P.x,P.y,EXIT.x,EXIT.y)<EXIT.r*EXIT.r)winGame();
}
function winGame(){
if(state!=='playing')return;
setState('won');
sfx('win');
if(document.exitPointerLock)document.exitPointerLock();
const t=P.time;
const mm=Math.floor(t/60),ss=Math.floor(t%60);
elWinStats.innerHTML=
'TIME <span>'+mm+':'+(ss<10?'0':'')+ss+'</span><br>'+
'HOSTILES PURGED <span>'+P.kills+'/'+totalEnemies+'</span><br>'+
'DEATHS <span>'+P.deaths+'</span><br>'+
'RANK <span>'+(P.deaths===0?'FLAWLESS':(P.deaths<3?'VETERAN':'SURVIVOR'))+'</span>';
}
/* ---------------- renderer ---------------- */
const zbuf=new Float32Array(RW);
const gradCache={valid:false,ceil:null,floor:null};
let horizon=RH>>1;
function shIdx(d,extra){
return clamp(Math.floor(d/2.6)+(extra||0),0,SHADES-1);
}
function render(dt){
const dirX=Math.cos(P.a),dirY=Math.sin(P.a);
const planeX=-dirY*FOV,planeY=dirX*FOV;
ctx.save();
const sk=P.shake>0?P.shake*0.35:0;
const shx=(rnd()-0.5)*sk,shy=(rnd()-0.5)*sk*0.6;
ctx.translate(shx,shy);
// ---- ceiling + floor ----
if(!gradCache.valid){
const gc=ctx.createLinearGradient(0,0,0,horizon);
gc.addColorStop(0,'#2a3044');gc.addColorStop(0.55,'#1a1e2b');gc.addColorStop(1,'#0b0d14');
const gf=ctx.createLinearGradient(0,horizon,0,RH);
gf.addColorStop(0,'#121216');gf.addColorStop(0.35,'#2b2420');gf.addColorStop(1,'#4a3d2e');
gradCache.ceil=gc;gradCache.floor=gf;gradCache.valid=true;
}
ctx.fillStyle=gradCache.ceil;ctx.fillRect(-10,-10,RW+20,horizon+10);
ctx.fillStyle=gradCache.floor;ctx.fillRect(-10,horizon,RW+20,RH-horizon+10);
// ---- walls (DDA, 2px columns for speed) ----
const CW=2;
for(let x=0;x<RW;x+=CW){
const camX=(2*(x+CW*0.5)/RW)-1;
const rdx=dirX+planeX*camX,rdy=dirY+planeY*camX;
let mx=Math.floor(P.x),my=Math.floor(P.y);
const ddx=rdx===0?1e30:Math.abs(1/rdx),ddy=rdy===0?1e30:Math.abs(1/rdy);
let stepX,stepY,sdx,sdy;
if(rdx<0){stepX=-1;sdx=(P.x-mx)*ddx;}else{stepX=1;sdx=(mx+1-P.x)*ddx;}
if(rdy<0){stepY=-1;sdy=(P.y-my)*ddy;}else{stepY=1;sdy=(my+1-P.y)*ddy;}
let side=0,hit=0,guard=0;
while(guard++<220){
if(sdx<sdy){sdx+=ddx;mx+=stepX;side=0;}
else{sdy+=ddy;my+=stepY;side=1;}
const t=tileAt(mx,my);
if(t){hit=t;break;}
}
if(!hit){zbuf[x]=1e30;if(x+CW<RW)zbuf[x+1]=1e30;continue;}
const perp=Math.max(0.05,side===0?(sdx-ddx):(sdy-ddy));
zbuf[x]=perp;if(x+CW<RW)zbuf[x+1]=perp;
const lh=RH/perp;
const y0=horizon-lh/2;
let wallX=side===0?(P.y+perp*rdy):(P.x+perp*rdx);
wallX-=Math.floor(wallX);
let tx=(wallX*TEX)|0;
if(side===0&&rdx>0)tx=TEX-tx-1;
if(side===1&&rdy<0)tx=TEX-tx-1;
tx=clamp(tx,0,TEX-1);
const tex=WALLTEX[hit][shIdx(perp,side===1?1:0)];
ctx.drawImage(tex,tx,0,1,TEX,x,y0,CW,lh);
}
// ---- spawn particles/sprites list ----
const draws=[];
for(const pk of pickups){
if(pk.gone)continue;
const S=PICK[pk.kind];
if(S)draws.push({x:pk.x,y:pk.y,d2:dist2(pk.x,pk.y,P.x,P.y),kind:'pick',S:S,
off:0.16+Math.sin(pk.t*2.6)*0.07,alpha:1,squash:1});
}
for(const e of enemies){
const S=SPR[e.type];
const d2v=dist2(e.x,e.y,P.x,P.y);
const dying=e.hp<=0;
const squash=dying?clamp(1-(e.deadT||0)*2.4,0.14,1):1;
draws.push({x:e.x,y:e.y,d2:d2v,kind:'enemy',S:S,e:e,
off:0,alpha:dying?clamp(1-(e.deadT||0)*0.35,0.55,1):1,squash:squash,
bob:dying?0:Math.sin(e.animT*5)*0.012});
}
for(const pr of projectiles){
draws.push({x:pr.x,y:pr.y,d2:dist2(pr.x,pr.y,P.x,P.y),kind:'proj',pr:pr});
}
draws.push({x:EXIT.x,y:EXIT.y,d2:dist2(EXIT.x,EXIT.y,P.x,P.y),kind:'exit',S:null,off:0,alpha:1,squash:1});
draws.sort((a,b)=>b.d2-a.d2);
for(const d of draws){
if(d.kind==='exit'){drawExit(dirX,dirY,planeX,planeY);continue;}
if(d.kind==='proj'){drawOrb(dirX,dirY,planeX,planeY,d.pr);continue;}
if(d.kind==='pick'){
drawBillboard(dirX,dirY,planeX,planeY,d.x,d.y,d.S.hw,d.S.shades[shIdx(Math.sqrt(d.d2))],d.off,d.alpha,d.squash,null);
}else{
const e=d.e;
const si=shIdx(Math.sqrt(d.d2));
let tex;
if(e.hp<=0)tex=d.S.shades[Math.min(SHADES-1,si+3)];
else if(e.hurt>0)tex=d.S.flash;
else if(e.attackAnim>0)tex=d.S.hurt;
else tex=d.S.shades[si];
drawBillboard(dirX,dirY,planeX,planeY,d.x,d.y,d.S.hw,tex,d.bob,d.alpha,d.squash,e);
}
}
// ---- particles ----
ctx.globalCompositeOperation='lighter';
for(const p of particles){
const sx=p.x-P.x,sy=p.y-P.y;
const invDet=1/(planeX*dirY-dirX*planeY);
const tX=invDet*(dirY*sx-dirX*sy);
const tY=invDet*(-planeY*sx+planeX*sy);
if(tY<0.2)continue;
const scrX=(RW/2)*(1+tX/tY);
if(scrX<0||scrX>=RW)continue;
if(zbuf[scrX|0]<tY-0.08)continue;
const scrY=horizon+(0.5-p.z)*(RH/tY);
const s=Math.max(1,(p.size*RH)/tY);
ctx.globalAlpha=clamp(p.life/p.max,0,1)*0.95;
ctx.fillStyle=p.color;
ctx.fillRect(scrX-s/2,scrY-s/2,s,s);
}
ctx.globalAlpha=1;
ctx.globalCompositeOperation='source-over';
ctx.restore();
// ---- damage / pickup flashes ----
if(P.hurt>0){ctx.fillStyle='rgba(190,10,0,'+(P.hurt*0.5)+')';ctx.fillRect(0,0,RW,RH);}
if(P.pickupFlash>0){ctx.fillStyle='rgba(255,215,110,'+(P.pickupFlash*0.28)+')';ctx.fillRect(0,0,RW,RH);}
// ---- weapon + HUD ----
if(state!=='title')drawWeapon();
drawCrosshair();
if(minimap&&(state==='playing'||state==='paused'))drawMinimap();
}
function drawOrb(dirX,dirY,planeX,planeY,pj){
const tex=pj.kind==='plasma'?ORB_PLASMA:ORB_FIRE;
const sx=pj.x-P.x,sy=pj.y-P.y;
const invDet=1/(planeX*dirY-dirX*planeY);
const tX=invDet*(dirY*sx-dirX*sy);
const tY=invDet*(-planeY*sx+planeX*sy);
if(tY<0.25)return;
const scrX=(RW/2)*(1+tX/tY);
if(scrX<-40||scrX>RW+40)return;
const c=clamp(Math.round(scrX),0,RW-1);
if(zbuf[c]<tY-0.2)return;
const size=Math.max(2,(pj.size*2.1*RH)/tY);
const cy=horizon+(0.5-0.55)*(RH/tY);
ctx.globalCompositeOperation='lighter';
ctx.drawImage(tex,scrX-size/2,cy-size/2,size,size);
ctx.globalCompositeOperation='source-over';
}
function drawBillboard(dirX,dirY,planeX,planeY,wx,wy,hWorld,tex,offWorld,alpha,squash,e){
const sx=wx-P.x,sy=wy-P.y;
const invDet=1/(planeX*dirY-dirX*planeY);
const tX=invDet*(dirY*sx-dirX*sy);
const tY=invDet*(-planeY*sx+planeX*sy);
if(tY<0.25)return;
const scrX=(RW/2)*(1+tX/tY);
const hpx=(hWorld*RH)/tY*squash;
const wpx=hpx*(tex.width/tex.height);
const bottom=horizon+(0.5-offWorld)*(RH/tY);
const top=bottom-hpx;
const x0=scrX-wpx/2,x1=scrX+wpx/2;
const c0=Math.max(0,Math.ceil(x0)),c1=Math.min(RW-1,Math.floor(x1));
if(c0>c1)return;
let clearAll=true;
for(let x=c0;x<=c1;x++){if(zbuf[x]<=tY){clearAll=false;break;}}
ctx.globalAlpha=alpha;
if(clearAll){
ctx.drawImage(tex,x0,top,wpx,hpx);
}else{
let runStart=-1;
for(let x=c0;x<=c1+1;x++){
const vis=(x<=c1&&zbuf[x]>tY);
if(vis){
if(runStart<0)runStart=x;
}else if(runStart>=0){
const u0=clamp((runStart-x0)/wpx,0,1);
const u1=clamp((x-x0)/wpx,0,1);
const sw=(u1-u0)*tex.width;
if(sw>0.02)ctx.drawImage(tex,u0*tex.width,0,sw,tex.height,runStart,top,x-runStart,hpx);
runStart=-1;
}
}
}
ctx.globalAlpha=1;
}
function drawExit(dirX,dirY,planeX,planeY){
const sx=EXIT.x-P.x,sy=EXIT.y-P.y;
const invDet=1/(planeX*dirY-dirX*planeY);
const tX=invDet*(dirY*sx-dirX*sy);
const tY=invDet*(-planeY*sx+planeX*sy);
if(tY<0.3)return;
const scrX=(RW/2)*(1+tX/tY);
const hpx=(1.85*RH)/tY;
const wpx=hpx*(EXIT_TEX.width/EXIT_TEX.height);
const bottom=horizon+(0.42*RH)/tY;
const top=bottom-hpx;
const pulse=P.unlocked?0.46+0.18*Math.sin(P.time*6):0.17+0.05*Math.sin(P.time*2.4);
const fade=clamp(2.4/tY,0.45,1);
const col=P.unlocked?'#6dff8c':'#ff4a2a';
const x0=scrX-wpx/2,x1=scrX+wpx/2;
const c0=Math.max(0,Math.ceil(x0)),c1=Math.min(RW-1,Math.floor(x1));
if(c0>c1)return;
ctx.globalCompositeOperation='lighter';
ctx.globalAlpha=clamp(pulse*fade,0,1)*0.9;
let eRun=-1;
for(let x=c0;x<=c1+1;x++){
const vis=(x<=c1&&zbuf[x]>tY);
if(vis){
if(eRun<0)eRun=x;
}else if(eRun>=0){
const eu0=clamp((eRun-x0)/wpx,0,1);
const eu1=clamp((x-x0)/wpx,0,1);
const esw=(eu1-eu0)*EXIT_TEX.width;
if(esw>0.02)ctx.drawImage(EXIT_TEX,eu0*EXIT_TEX.width,0,esw,EXIT_TEX.height,eRun,top,x-eRun,hpx);
eRun=-1;
}
}
// glow blob
ctx.globalAlpha=clamp(pulse,0,1)*fade*(P.unlocked?0.5:0.2);
const gy=horizon+(0.2*RH)/tY;
const gr=ctx.createRadialGradient(scrX,gy,1,scrX,gy,Math.max(6,wpx*0.7));
gr.addColorStop(0,col);gr.addColorStop(1,'rgba(0,0,0,0)');
ctx.fillStyle=gr;
ctx.fillRect(scrX-wpx,gy-wpx,Math.max(12,wpx*2),Math.max(12,wpx*2));
ctx.globalAlpha=1;
ctx.globalCompositeOperation='source-over';
}
function drawCrosshair(){
const cx=RW/2|0,cy=horizon|0;
const hit=lastHitMarker>0;
ctx.fillStyle=hit?'rgba(255,90,60,.95)':'rgba(255,220,150,.8)';
ctx.fillRect(cx-4,cy,3,1);ctx.fillRect(cx+2,cy,3,1);
ctx.fillRect(cx,cy-4,1,3);ctx.fillRect(cx,cy+2,1,3);
ctx.fillRect(cx,cy,1,1);
}
/* ---------------- weapon view models ---------------- */
function drawWeapon(){
const bobA=Math.sin(P.bob)*6,bobB=Math.abs(Math.cos(P.bob))*5.5;
const rec=P.recoil*16;
const sk=P.shake*0.5;
const r1=(rnd()-0.5)*sk,r2=(rnd()-0.5)*sk;
const g=ctx;
let cx,by,sc;
if(P.weapon==='pistol'){sc=0.55;cx=RW*0.64+bobA+r1;by=RH-40+bobB+rec+r2;}
else if(P.weapon==='shotgun'){sc=0.56;cx=RW*0.50+bobA*0.7+r1;by=RH-38+bobB+rec+r2;}
else{sc=0.55;cx=RW*0.55+bobA*0.55+r1;by=RH-38+bobB+rec*0.8+r2;}
g.save();g.translate(cx,by);g.scale(sc,sc);
if(P.weapon==='pistol'){
g.fillStyle='#243418';g.fillRect(-42,-22,56,28); // forearm
g.fillStyle='#3f5a34';g.fillRect(-40,-24,52,28);
g.fillStyle='#4d6d40';g.fillRect(-40,-24,52,5);
g.fillStyle='#2e4423';g.fillRect(-40,-52,46,30); // fist
g.fillStyle='#466b38';g.fillRect(-38,-50,42,26);
g.fillStyle='#578041';g.fillRect(-38,-50,42,4);
g.fillStyle='#2b3d20';g.fillRect(-26,-44,6,16);g.fillRect(-12,-44,6,16);
g.fillStyle='#0d1015';g.fillRect(-30,-78,32,44); // grip
g.fillStyle='#2a3039';g.fillRect(-28,-76,28,40);
g.fillStyle='#3b434f';g.fillRect(-28,-76,28,4);
g.fillStyle='#161a20';g.fillRect(-28,-56,28,3);
g.fillStyle='#0d1015';g.fillRect(-34,-118,64,46); // slide
g.fillStyle='#59616f';g.fillRect(-32,-116,60,42);
g.fillStyle='#7d8697';g.fillRect(-32,-116,60,7);
g.fillStyle='#414855';g.fillRect(-32,-82,60,8);
g.fillStyle='#4a5260';g.fillRect(6,-110,18,5);g.fillRect(6,-101,18,5);g.fillRect(6,-92,18,5);
g.fillStyle='#0d1015';g.fillRect(-14,-150,30,36); // barrel
g.fillStyle='#4a5260';g.fillRect(-12,-148,26,34);
g.fillStyle='#6d7686';g.fillRect(-12,-148,7,34);
g.fillStyle='#05070a';g.fillRect(-9,-152,20,8);
g.fillStyle='#2a3039';g.fillRect(-30,-126,9,12); // sight
g.fillStyle='#8b94a4';g.fillRect(-29,-125,7,10);
}else if(P.weapon==='shotgun'){
g.fillStyle='#3a2c1e';g.fillRect(-48,-42,60,34); // stock
g.fillStyle='#553f28';g.fillRect(-46,-40,56,30);
g.fillStyle='#6d4d2b';g.fillRect(-46,-40,56,5);
g.fillStyle='#33373f';g.fillRect(-46,-74,58,34); // receiver
g.fillStyle='#4d515c';g.fillRect(-46,-74,58,7);
g.fillStyle='#1b1f26';g.fillRect(-16,-68,12,24); // ejection port
g.fillStyle='#3d434e';g.fillRect(-42,-136,34,64);g.fillRect(-6,-136,34,64);
g.fillStyle='#69707e';g.fillRect(-40,-136,7,64);g.fillRect(-4,-136,7,64);
g.fillStyle='#858d9d';g.fillRect(-42,-136,34,5);g.fillRect(-6,-136,34,5);
g.fillStyle='#0b0d11';g.fillRect(-38,-140,26,8);g.fillRect(-2,-140,26,8);
g.fillStyle='#2a2e36';g.fillRect(-9,-132,10,60); // rib
g.fillStyle='#6b4a2a';g.fillRect(-48,-100,74,22); // pump
g.fillStyle='#7d5932';g.fillRect(-48,-100,74,5);
g.fillStyle='#3f5a34';g.fillRect(-62,-92,26,32); // hands
g.fillStyle='#4d6d40';g.fillRect(-62,-92,26,5);
g.fillStyle='#3f5a34';g.fillRect(12,-68,32,42);
g.fillStyle='#4d6d40';g.fillRect(12,-68,32,5);
}else{
const rot=P.spin;
g.fillStyle='#232a20';g.fillRect(-66,-52,46,50); // ammo drum
g.fillStyle='#36462e';g.fillRect(-64,-50,42,46);
g.fillStyle='#4b6136';g.fillRect(-64,-50,42,5);
g.fillStyle='#ffb020';g.fillRect(-58,-14,5,5);
g.fillStyle='#2b3038';g.fillRect(-42,-82,78,62); // housing
g.fillStyle='#3d444f';g.fillRect(-38,-80,70,56);
g.fillStyle='#555d6b';g.fillRect(-38,-80,70,5);
g.fillStyle='#4a5260';g.fillRect(-28,-112,54,18); // shroud
g.fillStyle='#6d7686';g.fillRect(-28,-112,54,4);
for(let i=0;i<6;i++){
const a=rot+i*Math.PI/3;
const bxp=Math.cos(a)*13,byp=-128+Math.sin(a)*13;
g.fillStyle='#3c424e';g.fillRect(bxp-5,byp-18,11,34);
g.fillStyle='#8b94a4';g.fillRect(bxp-5,byp-18,3,34);
g.fillStyle='#0a0c10';g.fillRect(bxp-4,byp-16,8,5);
}
g.fillStyle='#3f5a34';g.fillRect(-44,-70,28,34); // hands
g.fillStyle='#4d6d40';g.fillRect(-44,-70,28,5);
g.fillStyle='#3f5a34';g.fillRect(12,-70,30,40);
g.fillStyle='#4d6d40';g.fillRect(12,-70,30,5);
}
g.restore();
if(P.flash>0){
const t=P.flash/0.06;
const mx=cx,my=by-(P.weapon==='pistol'?84:(P.weapon==='shotgun'?86:84));
ctx.save();
ctx.globalCompositeOperation='lighter';
ctx.globalAlpha=clamp(t,0,1);
const R=P.weapon==='shotgun'?30:(P.weapon==='chaingun'?22:18);
const gr=ctx.createRadialGradient(mx,my,1,mx,my,R*1.7);
gr.addColorStop(0,'rgba(255,255,220,1)');
gr.addColorStop(0.35,'rgba(255,190,80,.85)');
gr.addColorStop(1,'rgba(255,80,0,0)');
ctx.fillStyle=gr;
ctx.beginPath();ctx.arc(mx,my,R*1.7,0,6.2832);ctx.fill();
ctx.fillStyle='rgba(255,240,190,.95)';
ctx.beginPath();
for(let i=0;i<8;i++){
const a=i*Math.PI/4+0.3;
const rr=(i%2?R*0.35:R)*1.1;
const px4=mx+Math.cos(a)*rr,py4=my+Math.sin(a)*rr;
if(i===0)ctx.moveTo(px4,py4);else ctx.lineTo(px4,py4);
}
ctx.closePath();ctx.fill();
ctx.restore();
}
}
/* ---------------- minimap ---------------- */
const MMS=3;
let miniTex=null;
function buildMiniTex(){
const ox=0,oy=0;
const c=mk(MW*MMS+2,MH*MMS+2),g=c.getContext('2d');
g.fillStyle='#05070a';g.fillRect(0,0,c.width,c.height);
for(let y=0;y<MH;y++)for(let x=0;x<MW;x++){
const t=grid[y*MW+x];
if(!t)continue;
g.fillStyle=t===1?'#4c5560':(t===2?'#6b3526':'#38505e');
g.fillRect(x*MMS+1,y*MMS+1,MMS-0.5,MMS-0.5);
}
miniTex=c;
}
function drawMinimap(){
const S=MMS,ox=6,oy=6;
if(!miniTex)buildMiniTex();
ctx.save();
ctx.globalAlpha=0.78;
ctx.drawImage(miniTex,ox-1,oy-1);
ctx.globalAlpha=0.98;
ctx.fillStyle=P.unlocked?'#6dff8c':'#ff4a2a';
ctx.fillRect(ox+EXIT.x*S-2,oy+EXIT.y*S-2,5,5);
for(const e of enemies){
if(e.hp<=0)continue;
if(dist2(e.x,e.y,P.x,P.y)>100&&e.awake===false)continue;
ctx.fillStyle=e.awake?'#ff3b25':'#8a2a1a';
ctx.fillRect(ox+e.x*S-1,oy+e.y*S-1,3,3);
}
for(const pk of pickups){
if(pk.gone)continue;
ctx.fillStyle='#ffd45a';
ctx.fillRect(ox+pk.x*S-0.5,oy+pk.y*S-0.5,2,2);
}
ctx.fillStyle='#ffffff';
ctx.fillRect(ox+P.x*S-1.5,oy+P.y*S-1.5,3,3);
ctx.strokeStyle='#ffe9a8';ctx.lineWidth=1;
ctx.beginPath();ctx.moveTo(ox+P.x*S,oy+P.y*S);
ctx.lineTo(ox+(P.x+Math.cos(P.a)*3.2)*S,oy+(P.y+Math.sin(P.a)*3.2)*S);ctx.stroke();
ctx.restore();
}
/* ---------------- HUD ---------------- */
function updateHUD(){
elHealth.textContent=Math.max(0,Math.ceil(P.hp));
elArmor.textContent=Math.max(0,Math.ceil(P.armor));
elWeapon.textContent=WEAPONS[P.weapon].name;
const W=WEAPONS[P.weapon];
elAmmo.textContent=W.ammo?(P.ammo[W.ammo]|0):'\u221E';
elKills.textContent=P.kills+'/'+totalEnemies;
elHealth.classList.toggle('low',P.hp<=30);
}
/* ---------------- main loop ---------------- */
let last=performance.now();
function frame(now){
const dt=Math.min(0.05,Math.max(0,(now-last)/1000));
last=now;
if(state==='playing'){
P.time+=dt;
updatePlayer(dt);
updateEnemies(dt);
updateProjectiles(dt);
updateParticles(dt);
updatePickups(dt);
}else if(state==='title'){
P.a+=dt*0.14;
for(const e of enemies)e.animT+=dt;
updateParticles(dt);
P.hurt=0;
}else if(state==='paused'||state==='dead'||state==='won'){
updateParticles(dt);
}
if(lastHitMarker>0)lastHitMarker-=dt;
if(msgTimer>0){msgTimer-=dt;if(msgTimer<=0)msgEl.classList.remove('show');}
render(dt);
updateHUD();
requestAnimationFrame(frame);
}
/* ---------------- boot ---------------- */
resetWorld();
P.a=PLAYER_START.a;
requestAnimationFrame(frame);
window.CRIMSON={
player:P,get enemies(){return enemies;},get pickups(){return pickups;},
get state(){return state;},get total(){return totalEnemies;},
get projectiles(){return projectiles;},get particles(){return particles;},
grid:grid,MW:MW,MH:MH,EXIT:EXIT,
selectWeapon:selectWeapon,startGame:startGame,respawn:respawn,setState:setState,
applyDamage:applyDamage,fireWeapon:fireWeapon
};
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"deepseek/deepseek-v4.1-flash","provider":"openrouter","persona":"full","sessionId":"6606c4d4-b7bd-4486-9551-95f263d39d3a","tokensIn":15896501,"tokensOut":112021,"tokensTotal":16008522,"cost":0.13999162199999993,"turns":97,"toolCalls":96,"failedToolCalls":3,"timestamp":"2026-09-10T17:01:52.052Z"} -->