DOOM
Ornith 1.5 9B GGUF · full

Browser check: Artifact runtime error. This is a technical check, not a quality review.
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 29,959 bytes · SHA-256 e02085534b60
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>RED ZONE — Retro FPS</title>
<style>
* { margin:0; padding:0; box-sizing:border-box; }
html,body { height:100%; background:#000; overflow:hidden; font-family:'Courier New',monospace; }
#wrap { position:fixed; inset:0; display:flex; align-items:center; justify-content:center; background:#050505; }
canvas { display:block; image-rendering:pixelated; }
#game { position:absolute; }
#overlay {
position:absolute; inset:0; display:flex; flex-direction:column; align-items:center; justify-content:center;
background:radial-gradient(circle at 50% 40%, #2a0000, #000 70%); color:#e03020; text-align:center;
z-index:10; user-select:none;
}
#overlay h1 { font-size:64px; letter-spacing:8px; text-shadow:0 0 20px #f00, 4px 4px 0 #400; margin-bottom:6px; }
#overlay h2 { font-size:22px; color:#ffcf30; letter-spacing:3px; margin-bottom:28px; text-shadow:0 0 10px #a00; }
#overlay p { color:#c8c8c8; font-size:15px; line-height:1.9; max-width:520px; margin-bottom:24px; }
#overlay button {
background:#7a0f00; color:#ffd0c0; border:2px solid #d03020; padding:14px 40px; font-size:20px;
font-family:inherit; letter-spacing:3px; cursor:pointer; text-transform:uppercase;
box-shadow:0 0 18px rgba(255,40,20,0.5); transition:.15s;
}
#overlay button:hover { background:#a01800; color:#fff; box-shadow:0 0 28px rgba(255,60,30,0.8); }
.hidden { display:none !important; }
#win h1 { color:#30e060; text-shadow:0 0 24px #0f0, 4px 4px 0 #040; }
#win p { color:#90ffcf; }
.key { display:inline-block; background:#333; border:1px solid #666; border-radius:4px; padding:1px 8px; color:#fff; font-size:13px; }
</style>
</head>
<body>
<div id="wrap">
<canvas id="game"></canvas>
<div id="overlay">
<h1>RED ZONE</h1>
<h2>OPERATION: PURGE</h2>
<p>Hostiles overrun the facility. Clear every wave, grab supplies, and reach the <span style="color:#ffcf30">EXIT</span> to win.<br><br>
<span class="key">WASDC</span> move <span class="key">Arrows</span> turn <span class="key">Mouse</span> aim <span class="key">LMB</span> fire<br>
<span class="key">1 2 3</span> switch weapon <span class="key">Shift</span> sprint</p>
<button class="deploy-btn">DEPLOY</button>
</div>
</div>
<script>
"use strict";
(function(){
/* ============================================================
RED ZONE — a retro raycasting FPS
============================================================ */
const canvas = document.getElementById('game');
const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
const overlay = document.getElementById('overlay');
const startBtn = document.querySelector('.deploy-btn');
let W = 640, H = 360;
function fit(){
const maxW = Math.min(window.innerWidth, 1000);
const maxH = window.innerHeight;
let w = maxW;
let h = w * (H/W);
if(h > maxH){ h = maxH; w = h*(W/H); }
canvas.style.width = Math.floor(w)+'px';
canvas.style.height = Math.floor(h)+'px';
canvas.width = W; canvas.height = H;
}
window.addEventListener('resize', fit); fit();
/* ---------- WebGL setup (fixed-function shader) ---------- */
let program, buffer, proj, NULL_TEX;
const STRIDE = 20;
function initGL(){
const vs = `
attribute vec2 aPos; attribute vec2 aUV; attribute float aDepth;
varying float vDepth; varying vec2 vUV;
uniform mat4 uMVP;
void main(){ gl_Position = uMVP * vec4(aPos,0.0,1.0); vDepth=aDepth; vUV=aUV; }`;
const fs = `
precision mediump float;
varying float vDepth; varying vec2 vUV;
uniform sampler2D uTex; uniform float uTime; uniform vec4 uColor; uniform float uTint; uniform float uFog;
void main(){
vec4 c = texture2D(uTex, vUV);
c.rgb *= uColor.rgb * uTint;
float d = clamp(vDepth,0.0,1.0);
vec3 fog = mix(c.rgb, vec3(0.01), uFog);
gl_FragColor = vec4(fog, 1.0);
}`;
function compile(type, src){
const s = gl.createShader(type); gl.shaderSource(s, src); gl.compileShader(s);
if(!gl.getShaderParameter(s, gl.COMPILE_STATUS)) console.error(gl.getShaderInfoLog(s));
return s;
}
program = gl.createProgram();
gl.attachShader(program, compile(gl.VERTEX_SHADER, vs));
gl.attachShader(program, compile(gl.FRAGMENT_SHADER, fs));
gl.linkProgram(program);
if(!gl.getProgramParameter(program, gl.LINK_STATUS)) console.error(gl.getProgramInfoLog(program));
gl.useProgram(program);
buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.enableVertexAttribArray(0);
gl.enableVertexAttribArray(1);
gl.enableVertexAttribArray(2);
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, STRIDE, 0);
gl.vertexAttribPointer(1, 2, gl.FLOAT, false, STRIDE, 8);
gl.vertexAttribPointer(2, 1, gl.FLOAT, false, STRIDE, 16);
proj = gl.getUniformLocation(program, 'uMVP');
NULL_TEX = gl.createTexture();
function bTex(tex){ gl.bindTexture(tex, NULL_TEX); }
}
/* ---------- Map ---------- */
const MAP = [
"XXXXXXXXXXXXXXXXXXXXXXXX",
"X.........X...........XX",
"X.........X...........XX",
"X...S1....X..........5X",
"X...X..X..X..........XX",
"X...X....X...........XX",
"X.....X..X...........XX",
"X.....X.XXXX....5....XX",
"X......X.........X...XX",
"X......X.........X...XX",
"X......X.........X...XX",
"X......X....E.....X...XX",
"X......X....XXX...X...XX",
"X......X..........X...XX",
"X......X..........X...XX",
"X......X..........X...XX",
"X.....S2.XXXXXXXXX...XX",
"X.........X.......6...XX",
"X.........X.......XXX.XX",
"X.........X...........X",
"XXXXXXXXXXXXXXXXXXXXXXEX",
"XXXXXXXXXXXXXXXXXXXXXXXX",
];
const MH = MAP.length, MW = MAP[0].length;
const map = [];
for(let y=0;y<MH;y++){ const row=MAP[y]; for(let x=0;x<MW;x++) map[y*MW+x] = row[x]==='X'?1:0; }
function X(y,x){ if(y<0||y>=MH||x<0||x>=MW) return 1; return map[y*MW+x]; }
const pickups = {};
function findPickup(type){ for(let y=0;y<MH;y++) for(let x=0;x<MW;x++){ const ch=MAP[y][x]; if(ch===type){ pickups[type]={x:x+0.5,y:y+0.5,taken:false}; return pickups[type]; } } return null; }
const P1=findPickup('S1'), P2=findPickup('S2');
const EXIT = { x:MW-0.5, y:MH-2 };
/* ---------- Player ---------- */
const player = {
x:2.5, y:2.5, dir:0, hp:100, maxhp:100,
weapon:0, ammo:[60,24,9], maxammo:[200,60,20],
speed:3.2, bob:0, dead:false,
kills:0, wavesCleared:0,
};
const WEAPONS = [
{ name:'Pistol', dmg:22, spread:0.02, rate:380, mag:60, reserve:60, auto:false, reload:700, pellets:1, sound:'pist' },
{ name:'Shotgun', dmg:14, spread:0.22, rate:620, mag:24, reserve:24, auto:false, reload:950, pellets:8, sound:'shot' },
{ name:'Plasma', dmg:30, spread:0.05, rate:150, mag:9, reserve:9, auto:true, reload:1100, pellets:3, sound:'plaz' },
];
let muzzle=0, gunFlash=0, damageFlash=0, shake=0;
/* ---------- Input ---------- */
const keys = {};
addEventListener('keydown', e=>{
keys[e.code]=true;
if(gameState!=='play') return;
if(e.code==='Digit1') player.weapon=0;
if(e.code==='Digit2') player.weapon=1;
if(e.code==='Digit3') player.weapon=2;
if(e.code==='KeyR') reload();
if(['Space','ArrowUp','ArrowDown','KeyW','KeyA','KeyS','KeyD'].includes(e.code)) e.preventDefault();
});
addEventListener('keyup', e=> keys[e.code]=false);
canvas.addEventListener('mousedown', e=>{ if(gameState==='play' && e.button===0) firing=true; });
addEventListener('mouseup', ()=> firing=false);
canvas.addEventListener('contextmenu', e=>e.preventDefault());
let mouseDX=0;
canvas.addEventListener('mousemove', e=>{
if(gameState==='play' && document.pointerLockElement===canvas) mouseDX += e.movementX;
});
function requestLock(){ canvas.requestPointerLock(); }
/* ---------- Audio (WebAudio, procedural) ---------- */
let actx=null;
function initAudio(){ if(!actx){ actx=new (window.AudioContext||window.webkitAudioContext)(); } if(actx.state==='suspended') actx.resume(); }
function blip(freq0,freq1,dur,type,vol,when){
if(!actx) return; const t=actx.currentTime+(when||0);
const o=actx.createOscillator(), g=actx.createGain();
o.type=type||'sawtooth'; o.connect(g); g.connect(actx.destination);
g.gain.setValueAtTime(vol, t);
g.gain.exponentialRampToValueAtTime(0.0006, t+dur);
o.frequency.setValueAtTime(freq0,t); o.frequency.exponentialRampToValueAtTime(Math.max(40,freq1), t+dur);
o.start(t); o.stop(t+dur+0.02);
}
function noise(dur,vol){ if(!actx) return; const t=actx.currentTime; const n=actx.createBuffer(1,Math.floor(actx.sampleRate*dur),actx.sampleRate); const d=n.getChannelData(0); for(let i=0;i<d.length;i++) d[i]=(Math.random()*2-1)*(1-i/d.length); const s=actx.createBufferSource(); s.buffer=n; const g=actx.createGain(); g.gain.value=vol; s.connect(g); g.connect(actx.destination); s.start(t); }
function sfx(name){
if(name==='pist'){ blip(320,90,0.12,'square',0.14); noise(0.05,0.06); }
else if(name==='shot'){ noise(0.12,0.16); blip(180,50,0.18,'sawtooth',0.1); }
else if(name==='plaz'){ blip(900,200,0.14,'triangle',0.1); noise(0.04,0.04); }
else if(name==='hit'){ blip(1400,600,0.07,'square',0.1); }
else if(name==='kill'){ blip(400,80,0.28,'sawtooth',0.13); noise(0.08,0.05); }
else if(name==='hurt'){ blip(300,70,0.25,'sawtooth',0.16); }
else if(name==='pickup'){ blip(660,1320,0.12,'square',0.1); blip(990,1480,0.1,'sine',0.07); }
else if(name==='reload'){ blip(440,220,0.08,'sine',0.06,0.05); blip(330,165,0.08,'sine',0.06,0.2); }
else if(name==='win'){ [523,659,784,1046].forEach((f,i)=>blip(f,f*0.5,0.3,'triangle',0.1,i*0.12)); }
else if(name==='lose'){ [400,300,200,120].forEach((f,i)=>blip(f,f*0.8,0.4,'sawtooth',0.12,i*0.18)); }
}
/* ---------- Particles ---------- */
let particles=[];
function addParticle(x,y,z,vx,vy,vz,life,color,size){ particles.push({x,y,z,vx,vy,vz,life,maxLife:life,color,size}); }
/* ---------- Enemy types ---------- */
const ENEMY_TYPES = {
grunt: {hp:55, speed:1.05, dmg:8, range:8, atkCd:500, color:'#c0302a', r:0.28, pts:100, sound:'grunt'},
brute: {hp:130,speed:0.8, dmg:16, range:6, atkCd:800, color:'#8a1a10', r:0.36, pts:200, sound:'brut'},
fast: {hp:35, speed:1.9, dmg:6, range:7, atkCd:350, color:'#30a040', r:0.22, pts:120, sound:'fast'},
};
let enemies=[];
function spawnWave(n){
const list=[];
for(let i=0;i<n;i++){
let tx,ty,tries=0;
do { tx=Math.random()*MW; ty=Math.random()*MH; tries++; }
while(X(Math.round(ty),Math.round(tx)) && tries<40);
let type='grunt';
const roll=Math.random();
if(roll<0.35) type='fast'; else if(roll<0.6) type='brute';
const def=ENEMY_TYPES[type];
list.push({ type, x:tx, y:ty, hp:def.hp, def, alive:true, atkT:0, hitT:0, hurt:0 });
}
enemies.push(...list);
}
function waveDef(){ return Math.min(3 + player.wavesCleared*2, 16); }
/* ---------- Game state ---------- */
let gameState='menu';
let firing=false, fireCd=0, reloading=false, reloadT=0;
let lastTime=0;
/* ---------- Projection ---------- */
function aspect(){ return W/H; }
function project(px,py,pz){
const cos=Math.cos(player.dir), sin=Math.sin(player.dir);
const rx=(px-player.x)*cos - (py-player.y)*sin;
const ry=(px-player.x)*sin + (py-player.y)*cos;
const invW = 1/(pz+0.2);
const sx = (rx*invW)*0.5*aspect() + 0.5;
const sy = -(ry*invW)*0.5 + 0.5;
return {x:sx, y:sy, depth:pz};
}
/* ---------- Raycaster ---------- */
const WALL_COLORS = [
{base:[0.45,0.42,0.4], edge:[0.15,0.14,0.12], fog:0.09},
{base:[0.55,0.45,0.3], edge:[0.2,0.16,0.12], fog:0.085},
{base:[0.35,0.38,0.5], edge:[0.12,0.13,0.2], fog:0.095},
];
function render(){
gl.viewport(0,0,W,H);
const mvp = new Float32Array([1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1]);
gl.uniformMatrix4fv(proj, false, mvp);
gl.clearColor(0.01,0.01,0.012,1); gl.clear(gl.COLOR_BUFFER_BIT);
gl.activeTexture(gl.TEXTURE0);
gl.uniform1i(gl.getUniformLocation(program,'uTex'), 0);
gl.uniform1f(gl.getUniformLocation(program,'uTime'), 0);
gl.uniform4f(gl.getUniformLocation(program,'uColor'), 1,1,1,1);
gl.uniform1f(gl.getUniformLocation(program,'uTint'), 1);
gl.uniform1f(gl.getUniformLocation(program,'uFog'), 1);
}
function castWall(dx,dy){
let mapX=Math.floor(player.x), mapY=Math.floor(player.y);
const deltaX=Math.abs(1/Math.abs(dx||1e-9)), deltaY=Math.abs(1/Math.abs(dy||1e-9));
let stepX,stepY,timex,timey;
if(dx<0){ stepX=-1; timex=(player.x-mapX)*deltaX; } else { stepX=1; timex=(mapX+1-player.x)*deltaX; }
if(dy<0){ stepY=-1; timey=(player.y-mapY)*deltaY; } else { stepY=1; timey=(mapY+1-player.y)*deltaY; }
let side=0, hit=0, dist=0, step=0;
for(step=0; step<64; step++){
if (timex<timey) { timex+=deltaX; mapX+=stepX; side=0; } else { timey+=deltaY; mapY+=stepY; side=1; }
if(mapX<0||mapY<0||mapX>=MW||mapY>=MH){ hit=1; break; }
if(map[mapY*MW+mapX]){ hit=1; break; }
}
if(!hit) return null;
dist = side===0 ? (timey-deltaY) : (timex-deltaX);
if(dist<0.02) dist=0.02;
let wallX = player.x + dx*dist, wallY = player.y + dy*dist;
const texX = Math.floor((wallX-Math.floor(wallX))*64);
return { dist, side, texX };
}
/* ---------- Sprite sorting + drawing ---------- */
function spriteSort(){
const list=[...enemies.filter(e=>e.alive)];
if(P1 && !P1.taken) list.push({isPickup:true, key:P1, x:P1.x, y:P1.y});
if(P2 && !P2.taken) list.push({isPickup:true, key:P2, x:P2.x, y:P2.y});
list.forEach(e=>{ const dx=e.x-player.x, dy=e.y-player.y; e.depth=Math.hypot(dx,dy); });
list.sort((a,b)=>b.depth-a.depth);
return list;
}
function mvp(){ return new Float32Array([1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1]); }
function drawSprites(){
const sprites=spriteSort();
if(!sprites.length) return;
const pw = Math.max(1, Math.floor((W*0.7)/(sprites[0].depth*0.6)));
const view = pw*0.9;
for(const s of sprites){
const dx=s.x-player.x, dy=s.y-player.y;
let ang=Math.atan2(dx,dy);
let diff=ang-player.dir;
while(diff>Math.PI) diff-=2*Math.PI; while(diff<-Math.PI) diff+=2*Math.PI;
if(Math.abs(diff)>1.35) continue;
const dist=s.depth;
const proj=project(s.x,s.y,dist);
const sx=proj.x, sy=proj.y;
const w=view/Math.cos(diff)/dist;
const h=w*aspect();
if(s.isPickup) drawPickup(s, sx, sy, w, h);
else drawEnemy(s, sx, sy, w, h);
}
}
function makeCtxCanvas(w,h){ const c=document.createElement('canvas'); c.width=w; c.height=h; return c.getContext('2d'); }
function uploadSprite(sx, sy, w, h, c){
gl.bindTexture(gl.createTexture());
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, c);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.uniform1f(gl.getUniformLocation(program,'uTint'), 1);
gl.uniform1f(gl.getUniformLocation(program,'uFog'), 1);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
bTex(NULL_TEX);
}
function lighten(hex,amt){
const n=parseInt(hex.slice(1),16);
const r=Math.min(255,(n>>16)+255*amt), g=Math.min(255,((n>>8)&255)+255*amt), b=Math.min(255,(n&255)+255*amt);
return `rgb(${r|0},${g|0},${b|0})`;
}
function roundRect(g,x,y,w,h,r){ if(w<0){x+=w;w=-w;} if(h<0){y+=h;h=-h;} r=Math.min(r,w/2,h/2); g.beginPath(); g.moveTo(x+r,y); g.arcTo(x+w,y,x+w,y+h,r); g.arcTo(x+w,y+h,x,y+h,r); g.arcTo(x,y+h,x,y,r); g.arcTo(x,y,x+w,y,r); g.closePath(); }
function drawEnemy(e, sx, sy, w, h){
const g=makeCtxCanvas(Math.max(1,w|0),Math.max(1,h|0));
const cw=w, ch=h; const t=performance.now()*0.006;
const bob=Math.sin(t*2 + e.x*3 + e.y*5)*0.06;
const ox=cw/2, top=sy-ch*(0.9+bob), bot=sy+ch*0.15;
const hurt = e.hitT>0;
g.fillStyle='rgba(0,0,0,0.4)';
g.beginPath(); g.ellipse(ox, bot, cw*0.45, cw*0.15, 0,0,7); g.fill();
const legSwing=Math.sin(t*3+e.x)*cw*0.08;
g.strokeStyle=hurt?'#ff8080':e.def.color; g.lineWidth=Math.max(2,cw*0.1); g.lineCap='round';
g.beginPath();
g.moveTo(ox-cw*0.15, bot-ch*0.25); g.lineTo(ox-cw*0.22+legSwing, bot);
g.moveTo(ox+cw*0.15, bot-ch*0.25); g.lineTo(ox+cw*0.22-legSwing, bot); g.stroke();
const bw=cw*0.5, bh=ch*0.5;
g.fillStyle=hurt?'#ff8080':e.def.color;
roundRect(g, ox-bw/2, top+bh*0.25, bw, bh*0.72, cw*0.12); g.fill();
g.fillStyle=hurt?'#ffd0c0':lighten(e.def.color,0.35);
roundRect(g, ox-bw*0.28, top+bh*0.35, bw*0.56, bh*0.4, cw*0.08); g.fill();
g.strokeStyle=hurt?'#ff9090':e.def.color; g.lineWidth=Math.max(2,cw*0.08);
g.beginPath();
g.moveTo(ox-bw*0.35, top+bh*0.4); g.lineTo(ox-bw*0.45, top+bh*0.75);
g.moveTo(ox+bw*0.35, top+bh*0.4); g.lineTo(ox+bw*0.45, top+bh*0.75); g.stroke();
const hr=cw*0.22, hy=top+bh*0.22;
g.fillStyle=hurt?'#ffaaaa':e.def.color;
g.beginPath(); g.arc(ox, hy, hr, 0,7); g.fill();
g.fillStyle='#ff3020';
const ex=cw*0.1;
g.beginPath(); g.arc(ox-ex, hy-hr*0.05, hr*0.22,0,7); g.arc(ox+ex, hy-hr*0.05, hr*0.22,0,7); g.fill();
g.fillStyle='#ffef80';
g.beginPath(); g.arc(ox-ex, hy-hr*0.05, hr*0.1,0,7); g.arc(ox+ex, hy-hr*0.05, hr*0.1,0,7); g.fill();
if(e.hp < e.def.hp){
const bw2=cw*0.6, bxx=ox-bw2/2, bhy=top-hy-hr-10;
g.fillStyle='#000'; roundRect(g,bxx-2,bhy-2,bw2+4,8,4); g.fill();
g.fillStyle='#30a040'; roundRect(g,bxx,bhy,bw2*(e.hp/e.def.hp),6,3); g.fill();
}
uploadSprite(sx, sy, w, h, g.canvas);
}
function drawPickup(p, sx, sy, w, h){
const g=makeCtxCanvas(Math.max(1,w|0),Math.max(1,h|0));
const cw=w, ch=h; const cx=cw/2;
const bob=Math.sin(performance.now()*0.005)*ch*0.05;
const ox=cx, oy=sy-ch*0.5+bob;
const grad=g.createRadialGradient(cx,cy,2,cx,cy,cw*0.7);
grad.addColorStop(0,'rgba(255,220,120,0.5)'); grad.addColorStop(1,'rgba(255,220,120,0)');
g.fillStyle=grad; g.fillRect(0,0,cw,ch);
const col=p.key==='S1'?'#40d0ff':'#ff4060';
g.strokeStyle=col; g.fillStyle=col; g.lineWidth=Math.max(2,cw*0.06);
if(p.key==='S1'){
roundRect(g, cx-cw*0.28, oy-ch*0.28, cw*0.56, ch*0.56, cw*0.1); g.strokeRect(cx-cw*0.28, oy-ch*0.28, cw*0.56, ch*0.56);
g.fillStyle=col; g.font='bold '+(cw*0.2)+'px monospace'; g.textAlign='center'; g.fillText('A', cx, oy+ch*0.1);
} else {
roundRect(g, cx-cw*0.28, oy-ch*0.28, cw*0.56, ch*0.56, cw*0.1); g.strokeRect(cx-cw*0.28, oy-ch*0.28, cw*0.56, ch*0.56);
g.fillStyle=col; g.font='bold '+(cw*0.32)+'px monospace'; g.textAlign='center'; g.textBaseline='middle';
g.fillText('+', cx, oy+ch*0.05);
}
g.textBaseline='alphabetic';
uploadSprite(sx, sy, w, h, g.canvas);
}
/* ---------- Gun ---------- */
let gunCanvas=null;
function drawGun(){
if(!gunCanvas){
gunCanvas=document.createElement('canvas'); gunCanvas.width=160; gunCanvas.height=200;
const g=gunCanvas.getContext('2d');
g.fillStyle='#2a2a30'; roundRect(g,40,70,80,110,8); g.fill();
g.fillStyle='#3a3a42'; roundRect(g,48,80,64,90,6); g.fill();
g.fillStyle='#1a1a20'; roundRect(g,60,70,40,20,4); g.fill();
g.fillStyle='#33333c'; roundRect(g,66,10,50,70,6); g.fill();
g.fillStyle='#111'; roundRect(g,70,6,42,16,4); g.fill();
g.fillStyle='#222'; roundRect(g,78,150,30,45,6); g.fill();
g.fillStyle='#555'; roundRect(g,40,90,80,12,4); g.fill();
g.fillStyle='#0a0a0a'; roundRect(g,80,4,40,12,3); g.fill();
}
const w=gunCanvas.width, h=gunCanvas.height;
const bob=Math.sin(player.bob)*6;
const recoil=gunFlash*25;
const gx=W/2 - w/2, gy=H - h + 40 + bob + recoil;
gl.bindTexture(gl.createTexture());
gl.texImage2D(gl.TEXTURE_2D,0,gl.RGBA,gl.RGBA,gl.UNSIGNED_BYTE,gunCanvas);
gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_MIN_FILTER,gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_WRAP_S,gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_WRAP_T,gl.CLAMP_TO_EDGE);
gl.uniformMatrix4fv(proj,false,mvp());
gl.uniform1f(gl.getUniformLocation(program,'uTint'),1);
gl.uniform1f(gl.getUniformLocation(program,'uFog'),1);
gl.drawArrays(gl.TRIANGLE_STRIP,0,4);
bTex(NULL_TEX);
}
/* ---------- HUD ---------- */
const hudCanvas=document.createElement('canvas');
const hctx=hudCanvas.getContext('2d');
function drawHUD(){
hctx.clearRect(0,0,W,H);
const w=WEAPONS[player.weapon];
hctx.strokeStyle='rgba(255,60,40,0.8)'; hctx.lineWidth=2;
hctx.beginPath(); hctx.moveTo(W/2-10,H/2); hctx.lineTo(W/2+10,H/2);
hctx.moveTo(W/2,H/2-10); hctx.lineTo(W/2,H/2+10); hctx.stroke();
hctx.strokeStyle='rgba(255,255,255,0.3)'; hctx.beginPath(); hctx.arc(W/2,H/2,12,0,7); hctx.stroke();
hctx.fillStyle='#d0d0d0'; hctx.font='bold 16px monospace'; hctx.textAlign='right';
hctx.fillText(w.name, W-14, 22);
hctx.fillStyle= player.ammo[player.weapon]>0?'#ffe050':'#ff4040';
hctx.fillText('['+player.ammo[player.weapon]+']', W-14, 44);
hctx.fillStyle='#888'; hctx.fillText('RES '+player.maxammo[player.weapon], W-14, 62);
for(let i=0;i<3;i++){
const sel=i===player.weapon;
hctx.fillStyle=sel?'rgba(255,80,40,0.35)':'rgba(80,80,80,0.25)';
roundRect(hctx, 10+i*26, H-34, 22, 22, 4); hctx.fill();
hctx.fillStyle=sel?'#fff':'#777'; hctx.font='10px monospace'; hctx.textAlign='center';
hctx.fillText(i+1, 10+i*26+11, H-15);
}
drawMinimap();
hctx.fillStyle='#ffcf30'; hctx.font='bold 14px monospace'; hctx.textAlign='left';
hctx.fillText('WAVE '+(player.wavesCleared+1), 12, 20);
hctx.fillStyle='#aaa'; hctx.fillText('KILLS '+player.kills, 12, 38);
}
function drawMinimap(){
const s=Math.min((W-40)/MW,(H-96)/MH);
const ox=10, oy=H-96;
hctx.fillStyle='rgba(0,0,0,0.55)';
roundRect(hctx, ox-4, oy-4, MW*s+8, MH*s+8, 4); hctx.fill();
hctx.fillStyle='rgba(80,90,110,0.8)';
for(let y=0;y<MH;y++) for(let x=0;x<MW;x++) if(map[y*MW+x]===1){ hctx.fillRect(ox+x*s, oy+y*s, Math.max(1,s*0.9), Math.max(1,s*0.9)); }
if(P1&& !P1.taken){ hctx.fillStyle='#40d0ff'; hctx.fillRect(ox+P1.x*s-2, oy+P1.y*s-2,4,4); }
if(P2&& !P2.taken){ hctx.fillStyle='#ff4060'; hctx.fillRect(ox+P2.x*s-2, oy+P2.y*s-2,4,4); }
hctx.fillStyle='#30e060'; hctx.fillRect(ox+EXIT.x*s-2, oy+EXIT.y*s-2,4,4);
hctx.fillStyle='#ff3020';
for(const e of enemies){ if(e.alive){ hctx.beginPath(); hctx.arc(ox+e.x*s, oy+e.y*s, Math.max(1.5,s),0,7); hctx.fill(); } }
hctx.fillStyle='#ffcf30'; hctx.beginPath(); hctx.arc(ox+player.x*s, oy+player.y*s, 3,0,7); hctx.fill();
hctx.strokeStyle='#ffcf30'; hctx.lineWidth=1; hctx.beginPath(); hctx.moveTo(ox+player.x*s, oy+player.y*s);
hctx.lineTo(ox+player.x*s+Math.sin(player.dir)*8, oy+player.y*s+Math.cos(player.dir)*8); hctx.stroke();
}
/* ---------- Main loop ---------- */
function loop(t){
const dt=Math.min(0.05,(t-lastTime)/1000)||0; lastTime=t;
if(gameState==='play') update(dt);
render();
drawSprites();
drawGun();
drawHUD();
requestAnimationFrame(loop);
}
/* ---------- Update ---------- */
function update(dt){
updatePlayer(dt);
updateWeapons(dt);
updateEnemies(dt);
updateParticles(dt);
updateEffects(dt);
checkGameState();
}
function updatePlayer(dt){
const sprint = keys['ShiftLeft']||keys['ShiftRight'];
const spd = player.speed*(sprint?1.6:1)*(damageFlash>0?0.6:1);
let move=0, turn=0;
if(keys['KeyW']||keys['ArrowUp']) move=1;
if(keys['KeyS']||keys['ArrowDown']) move=-0.6;
if(keys['KeyA']) turn=-1;
if(keys['KeyD']) turn=1;
if(mouseDX!==0){ turn += mouseDX*0.002; mouseDX*=0.6; }
player.dir += turn*dt;
const cos=Math.cos(player.dir), sin=Math.sin(player.dir);
let vx=0, vy=0;
if(move){
vx += cos*spd; vy += sin*spd;
if(keys['KeyA']) { vx -= sin*spd; vy -= cos*spd; }
if(keys['KeyD']) { vx += sin*spd; vy += cos*spd; }
player.bob += (Math.hypot(vx,vy)/spd)*0.02;
}
tryMove(player.x + vx*dt, player.y, 0.25);
tryMove(player.x, player.y + vy*dt, 0.25);
}
function tryMove(nx, ny, pad){
const r=0.25;
if(nx>1+r && nx<MW-1-r && !map[Math.floor(player.y)*MW+Math.floor(nx)]) player.x=nx;
if(ny>1+r && ny<MH-1-r && !map[Math.floor(ny)*MW+Math.floor(player.x)]) player.y=ny;
}
function updateWeapons(dt){
const w=WEAPONS[player.weapon];
fireCd-=dt;
if(firing && !reloading && player.ammo[player.weapon]>0 && fireCd<=0){
fireCd=1/w.rate;
shoot();
}
if(reloading){
reloadT-=dt*1000;
if(reloadT<=0){ reloading=false; player.ammo[player.weapon]=w.mag; sfx('reload'); }
}
}
function shoot(){
const w=WEAPONS[player.weapon];
if(player.ammo[player.weapon]<=0){ reloading=true; reloadT=w.reload; return; }
player.ammo[player.weapon]--;
muzzle=1; gunFlash=1; shake=6; sfx(w.sound); muzzleBurst();
const pellets = w.pellets;
for(let i=0;i<pellets;i++){
const a=player.dir + (Math.random()*2-1)*w.spread;
const cos=Math.cos(a), sin=Math.sin(a);
let best=null, bestDist=1e9;
for(const e of enemies){ if(!e.alive) continue;
const dx=e.x-player.x, dy=e.y-player.y; const d=Math.hypot(dx,dy);
const da=Math.atan2(dx,dy)-a;
if(Math.abs(da)<0.15 && d<bestDist){
const angT=Math.abs(da)/0.15, sizeT=1-angT;
if(d*e.def.r > (dx*cos+dy*sin)){ best=e; bestDist=d; }
}
}
if(best){ damageEnemy(best, w.dmg*(0.8+Math.random()*0.4)); }
}
if(player.ammo[player.weapon]===0){ reloading=true; reloadT=w.reload; }
}
function reload(){
const w=WEAPONS[player.weapon];
if(reloading||player.ammo[player.weapon]<w.mag||player.maxammo[player.weapon]<=0) return;
reloading=true; reloadT=w.reload; sfx('reload');
}
function updateEnemies(dt){
for(const e of enemies){
if(!e.alive) continue;
if(e.hitT>0) e.hitT-=dt;
const dx=player.x-e.x, dy=player.y-e.y;
const dist=Math.hypot(dx,dy);
const lineOfSight = dist<14 && !wallBetween(e.x,e.y,player.x,player.y);
if(dist> e.def.range*0.8 && lineOfSight){
let mx=dx/dist, my=dy/dist, step=e.def.speed*dt;
let nx=e.x+mx*step, ny=e.y+my*step;
if(!blocked(e.x+mx*step, e.y, e.def.r)) e.x=nx;
if(!blocked(e.x, e.y+my*step, e.def.r)) e.y=ny;
} else if(dist<e.def.range*0.8){
const a=Math.atan2(dy,dx)+Math.PI/2;
let nx=e.x+Math.cos(a)*e.def.speed*0.4*dt;
let ny=e.y+Math.sin(a)*e.def.speed*0.4*dt;
if(!blocked(nx,e.y,e.def.r)) e.x=nx;
if(!blocked(e.x,ny,e.def.r)) e.y=ny;
}
e.atkT-=dt*1000;
if(dist<1.6 && lineOfSight && e.atkT<=0){
e.atkT=e.def.atkCd;
if(dist<1.3){
damagePlayer(e.def.dmg);
} else {
player.hp -= e.def.dmg*0.35; damageFlash=0.3; sfx('hurt');
}
}
if(e.hp<=0 && e.alive){ e.alive=false; player.kills++; sfx('kill'); spawnDeath(e); }
}
}
function blocked(nx,ny,r){
const d1=Math.hypot(nx-player.x,ny-player.y), da1=Math.atan2(nx-player.x,ny-player.y)-player.dir;
if(d1<1.4 && Math.abs(((da1+Math.PI*3)%(Math.PI*2))-Math.PI)<=0.9){ return true; }
return false;
}
function wallBetween(ax,ay,bx,by){
const steps=Math.ceil(Math.hypot(ax-bx,ay-by)/0.2);
for(let i=1;i<steps;i++){
const t=i/steps;
if(map[Math.floor(ay+(by-ay)*t)*MW+Math.floor(ax+(bx-ax)*t)]) return true;
}
return false;
}
function damageEnemy(e,dmg){ e.hp-=dmg; e.hitT=0.15; }
function spawnDeath(e){
const col=e.def.color;
for(let i=0;i<12;i++){
const a=Math.random()*7, sp=1+Math.random()*3;
addParticle(e.x,e.y,0.3, Math.cos(a)*sp, Math.sin(a)*sp, 0.5+Math.random()*0.5, 0, col, 3+Math.random()*3);
}
}
function damagePlayer(dmg){
if(player.dead) return;
player.hp-=dmg; damageFlash=0.4; shake=10; sfx('hurt');
if(player.hp<=0){ player.hp=0; die(); }
}
function die(){
if(player.dead) return;
player.dead=true; gameState='dead'; sfx('lose');
overlay.classList.remove('hidden');
overlay.classList.remove('win');
overlay.innerHTML='<h1 style="color:#e03020;text-shadow:0 0 24px #f00,4px 4px 0 #400">YOU DIED</h1><h2 style="color:#ffcf30">WAVE '+(player.wavesCleared+1)+' — KILLS '+player.kills+'</h2><p>You were overrun by the hostiles.</p><button class="respawn-btn">RESPAWN</button>';
document.getElementById('respawnBtn').onclick=()=>{ location.reload(); };
document.exitPointerLock();
}
function checkGameState(){
const anyEnemy=enemies.some(e=>e.alive);
if(!anyEnemy && !P1.taken && !P2.taken){
if(Math.abs(player.x-EXIT.x)<=1.2 && Math.abs(player.y-EXIT.y)<=1.2){ win(); return; }
}
if(!anyEnemy && (P1.taken||P2.taken)){
if(Math.abs(player.x-EXIT.x)<=1.2 && Math.abs(player.y-EXIT.y)<=1.2){ win(); return; }
}
}
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.vx*=0.92; p.vy*=0.92;
}
}
function updateEffects(dt){ muzzle*=Math.pow(0.001,dt); if(muzzle<0.01)muzzle=0; gunFlash*=Math.pow(0.0008,dt); if(gunFlash<0.01)gunFlash=0; shake*=Math.pow(0.0001,dt); if(shake<0.3)shake=0; damageFlash=Math.max(0,damageFlash-dt); }
function win(){
if(gameState!=='play') return;
gameState='win'; sfx('win');
overlay.classList.remove('hidden'); overlay.classList.add('win');
overlay.innerHTML='<h1 style="color:#30e060;text-shadow:0 0 24px #0f0,4px 4px 0 #040">FACILITY CLEARED</h1><h2 style="color:#ffcf30">YOU WIN!</h2><p>All hostiles neutralized. Exfiltration complete.<br><br>Final kills: <span style="color:#fff">'+player.kills+'</span> Waves cleared: <span style="color:#fff">'+player.wavesCleared+'</span></p><button class="again-btn">PLAY AGAIN</button>';
document.getElementById('againBtn').onclick=()=>{ location.reload(); };
document.exitPointerLock();
}
/* ---------- start ---------- */
function startGame(){
if(!startBtn) return;
initAudio();
if(!P1) findPickup('S1'); if(!P2) findPickup('S2');
spawnWave(waveDef());
player.dead=false; gameState='play';
overlay.classList.add('hidden');
requestLock();
}
startBtn.addEventListener('click', startGame);
document.addEventListener('click', function onEvt(){
if(gameState==='dead' && document.getElementById('respawnBtn')){ startGame(); }
else if(gameState==='win' && document.getElementById('againBtn')){ location.reload(); }
});
initGL();
requestAnimationFrame(loop);
})();
</script>
</body>
</html>