Zombie FPS
space bunny alpha · 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 35,103 bytes · SHA-256 e2aecb155f30
<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>LAST CALL AT THE STARLITE</title><style>
*{box-sizing:border-box}html,body{margin:0;width:100%;height:100%;overflow:hidden;background:#030507;color:#e8e1cf;font-family:Arial,sans-serif}canvas{display:block;width:100%;height:100%}#hud{position:fixed;inset:0;pointer-events:none;text-shadow:0 2px 3px #000}#cross{position:absolute;left:50%;top:50%;width:16px;height:16px;transform:translate(-50%,-50%)}#cross:before,#cross:after{content:"";position:absolute;background:#e8f0dfaa}#cross:before{width:16px;height:2px;top:7px}#cross:after{height:16px;width:2px;left:7px}#cross i{position:absolute;width:4px;height:4px;background:#e44;left:6px;top:6px;border-radius:50%}.panel{position:absolute;background:#080c10b8;border:1px solid #c6d5d044;padding:9px 13px;backdrop-filter:blur(3px)}#wave{left:24px;top:22px;min-width:165px}#wave b{font-size:25px;color:#ffcf74}#wave small{display:block;color:#b7c0bd;letter-spacing:2px}#threat{right:24px;top:22px;text-align:right}#threat b{font-size:25px;color:#d95649}#stats{right:24px;bottom:24px;text-align:right;font-size:15px;line-height:1.8}#ammo{left:24px;bottom:24px;min-width:185px}#ammo strong{font-size:30px}#ammo span{color:#a9b1af}#health{height:7px;background:#301313;margin-top:7px;width:180px}#health i{display:block;height:100%;background:#d34a3d;transition:.2s}#weapon{letter-spacing:2px;color:#e4c17b;margin-bottom:4px}#weaponName{left:50%;bottom:24px;transform:translateX(-50%);font-size:13px;letter-spacing:3px}.vignette{position:absolute;inset:0;box-shadow:inset 0 0 130px 30px #9a0000;opacity:0;transition:.08s}.scan{position:absolute;inset:0;opacity:.12;background:repeating-linear-gradient(0deg,transparent 0 3px,#000 4px)}#banner{position:absolute;left:50%;top:25%;transform:translateX(-50%);text-align:center;opacity:0;transition:.25s;width:100%}#banner strong{display:block;font-size:44px;letter-spacing:8px;color:#f4dca2}#banner span{color:#e65c4e;letter-spacing:4px}#sub{position:absolute;left:50%;top:39%;transform:translateX(-50%);font-size:18px;color:#ffd580;text-shadow:0 0 12px #000;opacity:0}#prompt{position:absolute;left:50%;top:58%;transform:translateX(-50%);background:#111a;color:white;padding:7px 12px;display:none}#weaponModel{position:absolute;right:19%;bottom:13%;width:250px;height:125px;transform-origin:50% 80%;filter:drop-shadow(0 8px 8px #000)}.screen{position:fixed;inset:0;display:flex;align-items:center;justify-content:center;background:radial-gradient(circle at 50% 40%,#0c1b25f5,#020304f8);z-index:5}.card{width:min(720px,90vw);text-align:center;padding:34px}.neon{color:#ff876c;font-size:clamp(42px,8vw,80px);letter-spacing:7px;margin:0;text-shadow:0 0 8px #ff3b26,0 0 32px #d71920,3px 0 #23bde066,-3px 0 #23bde066}.subtitle{letter-spacing:7px;color:#9ec4d2;margin:5px 0 30px}.brief{max-width:610px;margin:auto;color:#b7bec0;line-height:1.6}.keys{display:grid;grid-template-columns:repeat(4,1fr);gap:8px;margin:28px auto;max-width:540px;font-size:12px;color:#8f9b9e}.keys b{display:block;color:#f0d9a9;border:1px solid #766b55;padding:5px;margin-bottom:4px}.cta{display:inline-block;border:1px solid #d4a85c;padding:13px 25px;letter-spacing:3px;color:#f7d89a;cursor:pointer;margin-top:14px;background:#b8792510}.cta:hover{background:#b8792538}#paused .card{background:#050809cc;padding:50px;box-shadow:0 0 80px #000}#end h1{letter-spacing:6px}.hide{display:none!important}#loading{position:fixed;bottom:8px;right:12px;color:#678;font-size:11px}@media(max-width:700px){#weaponModel{right:8%;width:190px}.panel{transform:scale(.85)}.keys{grid-template-columns:repeat(2,1fr)}}
</style></head><body><canvas id="c"></canvas>
<div id="hud" class="hide"><div id="cross"><i></i></div><div class="vignette" id="dmg"></div><div class="scan"></div><div class="panel" id="wave"><small>THE NIGHT SHIFT</small><b id="waveNum">WAVE 1</b><small id="remaining">0 HOSTILES</small></div><div class="panel" id="threat"><small>THREAT</small><b id="clock">3:10 AM</b><small id="phase">HOLD</small></div><div class="panel" id="ammo"><div id="weapon">SERVICE AUTOMATIC</div><strong id="mag">12</strong><span> / 12</span><div id="health"><i></i></div></div><div id="stats">KILLS <b id="kills">0</b><br>SCORE <b id="score">0</b></div><div class="panel" id="weaponName">1 — SIDEARM</div><div id="banner"><strong></strong><span></span></div><div id="sub"></div><div id="prompt"></div><svg id="weaponModel" viewBox="0 0 250 125"><g id="gun"><path fill="#111417" stroke="#555b5e" stroke-width="2" d="M18 40h192v18H125l-14 17H55v26H30L18 82z"/><path fill="#24282b" d="M22 45h180v7H22zM70 77h32l-5 35H55z"/><path fill="#4e5559" d="M120 51h92v8h-92zM48 44h28v9H48z"/></g><circle id="flash" cx="213" cy="46" r="14" fill="#fff2a5" opacity="0"/></svg></div>
<div id="title" class="screen"><div class="card"><h1 class="neon">STARLITE</h1><div class="subtitle">LAST CALL — 3:10 A.M.</div><div class="brief">The highway is empty. The phones are ringing. Something is coming out of the trees.<br><br>Survive <b style="color:#f5d38f">10 waves</b> until sunrise. Find weapons and ammunition. Listen for the payphone during each break.</div><div class="keys"><div><b>W A S D</b>Move</div><div><b>MOUSE</b>Aim</div><div><b>LEFT CLICK</b>Fire</div><div><b>SHIFT</b>Sprint</div><div><b>R</b>Reload</div><div><b>1 2 3</b>Weapons</div><div><b>WHEEL</b>Switch</div><div><b>ESC</b>Pause</div></div><div class="cta" id="start">CLICK TO HOLD THE LOT</div><div style="margin-top:20px;color:#68777d;font-size:11px;letter-spacing:2px">HEADPHONES RECOMMENDED · CLICK CAPTURES THE MOUSE</div></div></div>
<div id="paused" class="screen hide"><div class="card"><div class="subtitle">THE LINE IS DEAD</div><h1>PAUSED</h1><div class="cta" id="resume">RETURN TO THE NIGHT</div></div></div>
<div id="end" class="screen hide"><div class="card"><div class="subtitle" id="endSub"></div><h1 id="endTitle"></h1><p id="endStats" class="brief"></p><div class="cta" id="restart">TRY AGAIN</div></div></div><div id="loading">PROCEDURAL AUDIO / WEBGL</div>
<script>
'use strict';
const $=id=>document.getElementById(id), canvas=$('c'), gl=canvas.getContext('webgl',{antialias:true,alpha:false});if(!gl){alert('WebGL is required');process.exit()}
const VSH=`attribute vec3 aP,aN,aC;uniform mat4 u,uMVP;uniform mat4 uL;varying vec3 vN,vW,vC;void main(){vec4 w=u*vec4(aP,1.);vW=w.xyz;vN=normalize(mat3(u)*aN);vC=aC;gl_Position=uMVP*vec4(aP,1.);}`;
const FSH=`precision highp float;varying vec3 vN,vW,vC;uniform vec3 uCam,uFog,uSky;uniform float uFlash,uDawn;void main(){vec3 n=normalize(vN),l=normalize(vec3(-.45,.8,.2));float dif=max(dot(n,l),0.);float hemi=.28+.22*n.y;vec3 c=vC*(hemi+dif*.72);float fl=pow(max(dot(reflect(-l,n),normalize(uCam-vW)),0.),18.);c+=vec3(.22,.17,.08)*fl;float lamp=exp(-length(vW.xz-vec2(-13.,-4.))*.16)*max(n.y*.5+.5,.1);c+=vec3(.5,.22,.05)*lamp*.7;vec3 amb=mix(vec3(.12,.17,.23),vec3(.35,.43,.55),uDawn);c+=vC*amb;c=mix(c,vec3(1.,.8,.35),uFlash);float fog=1.-exp(-length(uCam-vW)*mix(.036,.018,uDawn));fog=clamp(fog,0.,.92);vec3 fogc=mix(uFog,uSky,uDawn*.8);gl_FragColor=vec4(mix(c,fogc,fog),1.);}`;
function sh(t,s){let x=gl.createShader(t);gl.shaderSource(x,s);gl.compileShader(x);if(!gl.getShaderParameter(x,gl.COMPILE_STATUS))throw Error(gl.getShaderInfoLog(x));return x}const prog=gl.createProgram();gl.attachShader(prog,sh(gl.VERTEX_SHADER,VSH));gl.attachShader(prog,sh(gl.FRAGMENT_SHADER,FSH));gl.linkProgram(prog);gl.useProgram(prog);gl.enable(gl.DEPTH_TEST);gl.enable(gl.CULL_FACE);
const A={p:gl.getAttribLocation(prog,'aP'),n:gl.getAttribLocation(prog,'aN'),c:gl.getAttribLocation(prog,'aC')},U={u:gl.getUniformLocation(prog,'u'),mvp:gl.getUniformLocation(prog,'uMVP'),L:gl.getUniformLocation(prog,'uL'),cam:gl.getUniformLocation(prog,'uCam'),fog:gl.getUniformLocation(prog,'uFog'),sky:gl.getUniformLocation(prog,'uSky'),flash:gl.getUniformLocation(prog,'uFlash'),dawn:gl.getUniformLocation(prog,'uDawn')};
const I={u:'u',mvp:'uMVP',L:'uL',cam:'uCam',fog:'uFog',sky:'uSky',flash:'uFlash',dawn:'uDawn'};
const vadd=(a,b)=>[a[0]+b[0],a[1]+b[1],a[2]+b[2]],vsub=(a,b)=>[a[0]-b[0],a[1]-b[1],a[2]-b[2]],vscale=(a,s)=>[a[0]*s,a[1]*s,a[2]*s],dot=(a,b)=>a[0]*b[0]+a[1]*b[1]+a[2]*b[2],cross=(a,b)=>[a[1]*b[2]-a[2]*b[1],a[2]*b[0]-a[0]*b[2],a[0]*b[1]-a[1]*b[0]],norm=a=>vscale(a,1/(Math.hypot(...a)||1)),mixv=(a,b,t)=>a.map((x,i)=>x+(b[i]-x)*t);function matMul(a,b){let o=new Float32Array(16);for(let c=0;c<4;c++)for(let r=0;r<4;r++)for(let k=0;k<4;k++)o[c*4+r]+=a[k*4+r]*b[c*4+k];return o}function perspective(f,a,n,z){let m=new Float32Array(16);m[0]=1/Math.tan(f/2)/a;m[5]=1/Math.tan(f/2);m[10]=(z+n)/(n-z);m[11]=-1;m[14]=2*z*n/(n-z);return m}function model(x,y,z,sx,sy,sz,ry=0,rx=0,rz=0){let a=Math.cos(rx/2),b=Math.sin(rx/2),c=Math.cos(ry/2),d=Math.sin(ry/2),e=Math.cos(rz/2),f=Math.sin(rz/2),qx=a*d*e+b*c*f,qy=a*c*f-b*d*e,qz=a*d*f+b*c*e,qw=a*c*e-b*d*f,R=[[1-2*(qy*qy+qz*qz),2*(qx*qy-qz*qw),2*(qx*qz+qy*qw)],[2*(qx*qy+qz*qw),1-2*(qx*qx+qz*qz),2*(qy*qz-qx*qw)],[2*(qx*qz-qy*qw),2*(qy*qz+qx*qw),1-2*(qx*qx+qy*qy)]];return new Float32Array([R[0][0]*sx,R[1][0]*sx,R[2][0]*sx,0,R[0][1]*sy,R[1][1]*sy,R[2][1]*sy,0,R[0][2]*sz,R[1][2]*sz,R[2][2]*sz,0,x,y,z,1])}
function mesh(v,n,c,idx){if(!c){idx=n;n=[];c=[];for(let i=0;i<idx.length;i+=3){let a=idx[i]*3,b=idx[i+1]*3,d=idx[i+2]*3,q=norm(vsub(vsub([v[b],v[b+1],v[b+2]],[v[a],v[a+1],v[a+2]]),vsub([v[d],v[d+1],v[d+2]],[v[a],v[a+1],v[a+2]])));for(let j of[a,b,d]){n.push(q[0],q[1],q[2]);c.push(1,1,1)}}}let x=gl.createBuffer();gl.bindBuffer(gl.ARRAY_BUFFER,x);gl.bufferData(gl.ARRAY_BUFFER,new Float32Array([...v,...n,...c]),gl.STATIC_DRAW);let q=gl.createBuffer();gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER,q);gl.bufferData(gl.ELEMENT_ARRAY_BUFFER,new Uint16Array(idx),gl.STATIC_DRAW);return{x,q,count:idx.length}}
function cube(){return mesh([-1,-1,1,1,-1,1,1,1,1,-1,1,1, 1,-1,-1,-1,-1,-1,-1,1,-1,1,1,-1, 0,1,0,0,-1,0,0,0,1,0,0,0,-1, 0,0,1,0,0,-1,0,-1,0,0,1],[0,1,2,0,2,3,4,5,6,4,6,7,8,9,10,8,10,11,12,14,13,12,15,14,16,18,17,16,19,18,20,22,21,20,23,22])}function cyl(sides=12){let v=[],n=[],c=[],idx=[];for(let i=0;i<=sides;i++){let a=i/sides*Math.PI*2,x=Math.cos(a),z=Math.sin(a);v.push(x,-1,z,x,1,z);n.push(x,0,z,x,0,z);c.push(1,1,1,1,1,1)}for(let i=0;i<sides;i++){let a=i*2;idx.push(a,a+1,a+2,a+1,a+3,a+2)}return mesh(v,n,c,idx)}function sphere(rings=8,sides=10){let v=[],n=[],c=[],idx=[];for(let y=0;y<=rings;y++)for(let x=0;x<=sides;x++){let p=y/rings*Math.PI,a=x/sides*Math.PI*2,nx=Math.sin(p)*Math.cos(a),ny=Math.cos(p),nz=Math.sin(p)*Math.sin(a);v.push(nx,ny,nz);n.push(nx,ny,nz);c.push(1,1,1)}for(let y=0;y<rings;y++)for(let x=0;x<sides;x++){let a=y*(sides+1)+x,b=a+sides+1;idx.push(a,b,a+1,a+1,b,b+1)}return mesh(v,n,c,idx)}const GEO={box:cube(),cyl:cyl(),sphere:sphere()};
let objects=[],colliders=[],pickups=[],lights=[];function add(type,pos,scale,col,opts={}){let o={type,pos,scale,col,ry:opts.ry||0,rx:opts.rx||0,rz:opts.rz||0,em:opts.em||0};objects.push(o);return o}function box(x,y,z,sx,sy,sz,c,o={}){return add('box',[x,y,z],[sx,sy,sz],c,o)}function solid(x,y,z,sx,sy,sz){colliders.push({x,z,hx:sx/2,hz:sz/2,top:y+sy/2})}function textSign(x,y,z,w,h,txt,color,vertical=false){let o=add('box',[x,y,z],[w,h,.12],color,{em:1});o.text=txt;o.vertical=vertical;return o}
let time=0,started=false,paused=false,dead=false,wave=0,phase='break',breakTime=18,spawnQueue=[],zombies=[],effects=[],kills=0,score=0,health=100,dawn=0,phaseTime=0,phone=0,shake=0,muzzle=0;let keys={};let cam={x:0,y:1.7,z:19,yaw:0,pitch:0,bob:0};let last=performance.now();
function buildWorld(){objects=[];colliders=[];pickups=[];lights=[];box(0,-.12,0,72,.2,62,[.11,.12,.115]); // asphalt
for(let x=-30;x<=30;x+=5){if(Math.abs(x)>7)box(x,.002,10,.08,.006,28,[.32,.3,.24])}for(let z=-2;z<26;z+=4)box(-7,.003,z,25,.006,.07,[.35,.33,.25]);
// motel and office row
box(0,2.7,-18,48,5.4,4,[.39,.31,.25]);box(0,5.6,-18,50,.35,5,[.19,.2,.19]);for(let i=0;i<9;i++){let x=-20+i*5;box(x,1.25,-15.85,.12,2.5,3.3,[.11,.12,.13]);if(i==2){box(x-1.05,1.25,-16,.13,2.5,3.3,[.11,.12,.13]);box(x+1.05,1.25,-16,.13,2.5,3.3,[.11,.12,.13])}textSign(x,2.75,-15.65,.55,.32,'0'+(i+1),[.82,.67,.3],1);box(x+.15,1.2,-15.45,.18,.05,.1,[.75,.55,.2])}for(let i=0;i<7;i++)box(-23+i*7,2.7,-14.2,.28,5.4,3.4,[.34,.29,.25]);box(0,5.1,-12,50,.28,4.5,[.21,.23,.22]);
// rooms visibly open
for(let z=0;z<7;z++){box(-12,2.6,-12+z*.8,6,5.2,.12,[.45,.4,.34]);box(-15,2.6,-10,.12,5.2,5,[.38,.32,.27]);box(-8.7,1.1,-10,1.2,2.2,.5,[.2,.15,.11]);box(-8.7,2.1,-10,1.4,.2,.6,[.65,.58,.4]);box(-10,1.25,-10,1,2.5,.12,[.2,.22,.2])}
box(-8,1.25,7,2.2,2.5,.15,[.2,.15,.1]); // office open
textSign(-8,2.6,7,2.7,.4,'FRONT OFFICE',[.65,.9,.8]);
box(-10,.65,5,4,.2,1,[.28,.17,.1]);box(-10,1.25,4.5,4,1.2,.15,[.2,.12,.08]); // desk
box(-11,1.2,4.4,1.1,.65,.06,[.05,.06,.06]); // static tv
for(let i=0;i<5;i++)box(-12+i*.38,2,4.4,.25,.18,.08,[.35,.18,.08]);
box(-9,1.7,4.45,1.5,1,.12,[.2,.13,.08]); for(let i=0;i<8;i++)box(-9.5+i*.14,1.7+(i%2)*.12,4.34,.04,.16,.04,[.75,.55,.18]);
box(-11,2,5.6,1.2,1.5,.1,[.1,.12,.13]); // key board
// diner
box(15,2.7,10,13,5.4,10,[.35,.31,.28]);box(15,.08,10,13,.16,10,[.22,.25,.24]);box(15,2.7,5,13,5.4,.18,[.38,.34,.3]);box(8.5,2.7,10,.18,5.4,10,[.38,.34,.3]);box(21.5,2.7,10,.18,5.4,10,[.38,.34,.3]);box(15,2.7,15,13,5.4,.18,[.38,.34,.3]);
textSign(15,3.9,4.82,5,.75,'STARLITE DINER',[.9,.45,.16]);
box(17,.6,9,7,.25,1.1,[.55,.3,.14]);for(let i=0;i<5;i++){box(15.5+i*1.3,1,9,.8,.2,.7,[.7,.36,.15]);box(15.5+i*1.3,.55,8.5,.12,.7,.12,[.25,.2,.15])}add('cyl',[19.5,1.05,8.15],[.22,.3,.22],[.25,.28,.28]);add('cyl',[19.5,1.25,8.15],[.16,.18,.16],[.42,.46,.43]);add('cyl',[17.8,1.2,9],[.45,.28,.45],[.78,.75,.58],{rx:Math.PI/2}); // pie
for(let z=8;z<13;z+=2.5){box(20.5,.35,z,2.2,.5,1.3,[.36,.14,.12]);box(20.1,.8,z+1.4,2.2,.9,.25,[.4,.17,.13]);box(20.5,.65,z-.55,1.6,.08,.7,[.78,.73,.6]);box(20.1,.74,z-.35,.5,.08,.45,[.45,.2,.12])}
// station wagon
function car(x,z,ry=0){let g=(lx,y,lz,sx,sy,sz,c)=>box(x+lx*Math.cos(ry)-lz*Math.sin(ry),y,z+lx*Math.sin(ry)+lz*Math.cos(ry),sx,sy,sz,c,{ry});g(0,.55,0,2,.65,4.1,[.2,.25,.24]);g(0,1.1,.25,1.75,.65,2.15,[.12,.17,.18]);for(let a of [-1,1])for(let b of [-1.4,1.35]){add('cyl',[x+a*1.02*Math.cos(ry)-b*Math.sin(ry),.38,z+a*1.02*Math.sin(ry)+b*Math.cos(ry)],[.4,.16,.4],[.05,.05,.05],{rz:Math.PI/2,ry})}g(-.95,1,0,.08,.75,1.4,[.35,.28,.2],0,0,.35);g(0,.55,2.08,1.8,.35,.12,[.7,.68,.58]);lights.push({x:x,z:z+8,type:'car',col:[1,.75,.3],r:10})}car(-14,7,.12);solid(-14,1,7,2.2,2,4.4);
// pool
box(-22,-1.25,4,12,.3,7,[.18,.31,.34]);for(let x of [-28,-16])box(x,-.35,4,.3,1.8,7,[.62,.62,.55]);for(let z of [.5,7.5])box(-22,-.35,z,12,1.8,.3,[.62,.62,.55]);for(let i=0;i<20;i++)box(-27+Math.random()*10,-1.05,.9+Math.random()*6,.12,.04,.28,[[.4,.22,.08],[.2,.12,.06],[.5,.45,.22]][i%3],{ry:Math.random()*6});let chair=add('box',[-20,-.65,3.5],[.45,.1,.65],[.55,.44,.22],{ry:.5,rz:1.2});for(let i=0;i<30;i++)add('box',[-25+Math.random()*6,-.1+Math.random()*3,8+Math.random()*17],[.1,.03,.4],[.32,.27,.2],{ry:Math.random()*6});
// pool ladder
for(let y=-.9;y<.8;y+=.38)box(-15.82,y,1.35,.12,.08,.9,[.55,.58,.56]);box(-15.78,-.1,1,.12,2,.12,[.55,.58,.56]);box(-15.78,-.1,1.7,.12,2,.12,[.55,.58,.56]);
// neon
for(let x of [-17,-7])box(x,3,-11,.25,6,.25,[.2,.2,.2]);textSign(-12,7,-11,14,2,'STARLITE',[.7,.06,.03]);textSign(-17.7,3,-12,1.1,2.8,'8',[.95,.18,.04]);
// fence/trees
for(let z=-27;z<=27;z+=3){box(-34,1.3,z,.08,2.6,.08,[.25,.3,.28]);box(34,1.3,z,.08,2.6,.08,[.25,.3,.28])}for(let z=-27;z<=27;z+=.7)box(-34,2,z,.025,2,.025,[.18,.22,.2]);for(let z=-27;z<=27;z+=.7)box(34,2,z,.025,2,.025,[.18,.22,.2]);for(let z=-27;z<=27;z+=3)box(0,.65,z,68,1.3,.06,[.14,.19,.18]);for(let i=0;i<55;i++){let x=-40+Math.random()*80,z=-31+Math.random()*8;add('cyl',[x,2+Math.random()*3,z],[1,3+Math.random()*2,1],[.025,.045,.035],{ry:Math.random()*6})}for(let i=0;i<18;i++)add('cyl',[-32+Math.random()*5,.5,-25+Math.random()*50],[.12,1,.12],[.17,.2,.18],{rz:(Math.random()-.5)*.4});
// payphone, lights and props
box(-6,1.3,5.5,1.1,2.6,.6,[.28,.32,.28]);box(-6,1.75,5.16,.75,1,.12,[.05,.05,.05]);box(-6,1.2,5.08,.55,.18,.08,[.8,.7,.4],{em:1});textSign(-6,2.85,5.1,.8,.25,'PHONE',[.8,.8,.3]);
for(let x of [-20,-4,18]){box(x,4.8,12,.18,9.6,.18,[.16,.18,.17]);box(x,9.35,12,2,.2,.5,[.2,.21,.19]);box(x,9.18,12,1.6,.08,.35,[1,.66,.25],{em:1})}
box(-1,1.3,-13.8,1.1,2.6,.8,[.35,.45,.46]);box(-1,1.4,-13.34,.85,.9,.06,[.2,.5,.65],{em:.7});box(1.1,1.3,-13.8,1.3,2.6,.7,[.5,.2,.16]);box(1.1,1.45,-13.4,1,1.1,.06,[.55,.14,.09],{em:.5});
// lived-in
let bike=add('cyl',[4,.3,13],[.45,.05,.45],[.12,.15,.16],{rz:Math.PI/2,ry:.3});add('cyl',[3.3,.3,13],[.45,.05,.45],[.12,.15,.16],{rz:Math.PI/2,ry:.3});add('box',[3.65,.4,13],[.8,.12,.25],[.5,.16,.12],{ry:.3});
box(-9,1.5,6,.6,.12,.4,[.35,.18,.1]); // suitcase
box(-3,.5,16,1.5,.8,.9,[.3,.34,.36]);box(-3,.92,16,1.4,.06,.85,[.4,.44,.45]);for(let i=0;i<3;i++)add('cyl',[-3+i*.3,.55,16.47],[.04,.12,.04],[.06,.07,.08],{rz:Math.PI/2});
// cart
box(2,1.1,-13,1.5,1,.8,[.45,.46,.42]);for(let x of [1.3,2.7])add('cyl',[x,.25,-13],[.22,.08,.22],[.08,.08,.08],{rz:Math.PI/2});for(let i=0;i<7;i++)box(1.3+i*.2,1.25,-13,.13,.7,.65,[[.5,.5,.42],[.2,.35,.5],[.45,.3,.2]][i%3]);
// newspaper
let paper=box(10,.03,6,.5,.02,.7,[.65,.63,.5],{ry:.5});paper.news=true;
// barriers
for(let i=0;i<5;i++){box(0, .7,22+i*2,.35,1.2,.35,[.55,.3,.15]);box(0,1.05,22+i*2,.4,.25,.4,[.7,.7,.65])}
// perimeter wall collision
solid(-35,2,0,1,4,60);solid(35,2,0,1,4,60);solid(0,2,-30,70,4,1);solid(0,2,30,70,4,1);
// pickups
pickup('pistol',-13,6,'station wagon');pickup('shotgun',-13,8,'OPEN DRIVER DOOR');pickup('rifle',-9,5,'BEHIND THE COUNTER');
pickup('ammo',3,8,'12 ROUNDS');pickup('ammo',-26,8,'BY THE POOL');pickup('health',18,5,'IN THE DINER');pickup('ammo',25,10,'OFFICE SUPPLIES');
}
function pickup(type,x,z,label){pickups.push({type,x,z,label,active:true,phase:Math.random()*6})}
function resetPickups(){pickups.forEach(p=>p.active=true)}buildWorld();
function draw(o,vp){if(vp[1]<-2)return;let m=model(...o.pos,...o.scale,o.ry,o.rx,o.rz),g=GEO[o.type],col=o.col;if(o.text){col=[.05,.05,.05]}gl.uniformMatrix4fv(U.u,false,m);gl.uniformMatrix4fv(U.mvp,false,matMul(vp,m));gl.uniform3fv(U.cam,camxyz());gl.uniform3f(U.fog,.035,.055,.068);gl.uniform3f(U.sky,.1,.16,.21);gl.uniform1f(U.dawn,dawn);gl.uniform1f(U.flash,o.em?1:0);gl.bindBuffer(gl.ARRAY_BUFFER,g.x);gl.vertexAttribPointer(A.p,3,gl.FLOAT,false,36,0);gl.vertexAttribPointer(A.n,3,gl.FLOAT,false,36,12);gl.vertexAttribPointer(A.c,3,gl.FLOAT,false,36,24);gl.enableVertexAttribArray(A.p);gl.enableVertexAttribArray(A.n);gl.enableVertexAttribArray(A.c);gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER,g.q);gl.drawElements(gl.TRIANGLES,g.count,gl.UNSIGNED_SHORT,0)}
function camxyz(){return[cam.x+(Math.random()-.5)*shake,cam.y+(Math.random()-.5)*shake,cam.z]}
function view(){let cp=Math.cos(cam.pitch),s=Math.sin(cam.pitch);return new Float32Array([Math.cos(cam.yaw)*cp,0,-Math.sin(cam.yaw)*cp,0,0,1,0,0,s,cp,Math.cos(cam.yaw),0,-cam.x,-cam.y,-cam.z,1])}
function resize(){let d=Math.min(devicePixelRatio,1.6);canvas.width=innerWidth*d;canvas.height=innerHeight*d;gl.viewport(0,0,canvas.width,canvas.height)}addEventListener('resize',resize);resize();
// gameplay, procedural sound, and effects
const weapons=[
{name:'SIDEARM',mag:12,reserve:Infinity,damage:32,rate:.25,spread:.012,type:'pistol',sound:220},
{name:'PUMP SHOTGUN',mag:8,reserve:32,damage:20,rate:.72,spread:.07,pellets:8,type:'shotgun',sound:110},
{name:'MOTEL .308',mag:10,reserve:40,damage:76,rate:.52,spread:.018,type:'rifle',sound:160}];
let current=0,unlocked=[true,false,false],mag=[12,0,0],reserve=[Infinity,0,0],reload=0,fireCD=0,recoil=0,bob=0,moveSpeed=0,lastShot=0;
const spawns=[[-31,-22],[31,-22],[-32,16],[32,18],[0,-27],[20,-27],[-5,-27]];
function initGame(){wave=0;phase='break';breakTime=18;spawnQueue=[];zombies=[];effects=[];kills=score=0;health=100;dawn=0;current=0;unlocked=[true,false,false];mag=[12,0,0];reserve=[Infinity,0,0];cam={x:0,y:1.7,z:19,yaw:0,pitch:0,bob:0};resetPickups();startWave();}
function startWave(){wave++;phase='fight';$('phase').textContent='HOSTILES INBOUND';$('waveNum').textContent='WAVE '+wave;showBanner('WAVE '+wave,wave<3?'THEY HEARD THE ENGINES':wave<6?'MORE THAN ONE ROAD':'NOTHING IS LEFT OUTSIDE');audio.sting();if(wave>1)phone=0;let total=4+wave*2,runners=wave>=3?Math.floor(wave*.8):0,brutes=wave>=6?Math.floor((wave-3)/2):0;for(let i=0;i<total;i++)spawnQueue.push(i<brutes?'brute':i<runners?'runner':'shambler');shuffle(spawnQueue)}
function shuffle(a){for(let i=a.length-1;i;i--){let j=Math.floor(Math.random()*(i+1));[a[i],a[j]]=[a[j],a[i]]}}
function spawnZombie(type){let p=spawns[Math.floor(Math.random()*spawns.length)];zombies.push({type,x:p[0]+(Math.random()-.5)*2,z:p[1]+(Math.random()-.5)*2,y:type==='brute'?0:-.18,hp:type==='brute'?320:type==='runner'?65:110,max:type==='brute'?320:type==='runner'?65:110,phase:Math.random()*6,flinch:0,fall:0,dead:false,attack:0});groan(type,50)}
function showBanner(a,b){let e=$('banner');e.children[0].textContent=a;e.children[1].textContent=b;e.style.opacity=1;setTimeout(()=>e.style.opacity=0,2400)}
function aimRay(){let cp=Math.cos(cam.pitch);return[-Math.sin(cam.yaw)*cp,Math.sin(cam.pitch),-Math.cos(cam.yaw)*cp]}
function move(dt){let f=(keys.KeyW?1:0)-(keys.KeyS?1:0),s=(keys.KeyD?1:0)-(keys.KeyA?1:0),len=Math.hypot(f,s)||1;let v=(keys.ShiftLeft?7:4.1),dx=(-Math.sin(cam.yaw)*f+Math.cos(cam.yaw)*s)/len*v*dt,dz=(-Math.cos(cam.yaw)*f-Math.sin(cam.yaw)*s)/len*v*dt;let oldx=cam.x,oldz=cam.z;if(!blocked(cam.x+dx,cam.z))cam.x+=dx;if(!blocked(cam.x,cam.z+dz))cam.z+=dz;let inPool=cam.x>-27.6&&cam.x<-16.4&&cam.z>.8&&cam.z<7.2, targetY=inPool?.48:1.7;cam.y+=(targetY-cam.y)*Math.min(1,dt*5);moveSpeed=Math.min(1,Math.hypot(cam.x-oldx,cam.z-oldz)/(dt*v||1)*2);cam.bob+=moveSpeed*dt*10;if(moveSpeed>.05){if(Math.floor(cam.bob/Math.PI)!==Math.floor((cam.bob-Math.PI*moveSpeed)/Math.PI))audio.step()}}
function blocked(x,z){x=Math.max(-33,Math.min(33,x));z=Math.max(-28,Math.min(28,z));for(let c of colliders)if(x>c.x-c.hx-.3&&x<c.x+c.hx+.3&&z>c.z-c.hz-.3&&z<c.z+c.hz+.3)return true;return false}
function updateZombies(dt){for(let z of zombies){if(z.dead){z.fall+=dt*3.2;if(z.fall>1)z.remove=true;continue}let dx=cam.x-z.x,dz=cam.z-z.z,d=Math.hypot(dx,dz),speed=z.type==='runner'?3.55+wave*.09:z.type==='brute'?1.12:.75+wave*.045;z.phase+=dt*(z.type==='runner'?10:6);if(d>1.15&&z.flinch<=0){let ax=dx/d,az=dz/d,sx=z.x,sz=z.z;if(!blocked(z.x+ax*speed*dt,z.z))z.x+=ax*speed*dt;if(!blocked(z.x,z.z+az*speed*dt))z.z+=az*speed*dt;z.yaw=Math.atan2(dx,dz);if(z.type==='runner'&&Math.random()<dt*1.5)groan('runner',d)}if(d<1.5){z.attack-=dt;if(z.attack<=0){hurt(z.type==='brute'?19:z.type==='runner'?10:7);z.attack=1.05;if(z.type==='brute')audio.heavy()}}z.flinch=Math.max(0,z.flinch-dt)}zombies=zombies.filter(z=>!z.remove)}
function zombieParts(z){let dead=z.fall>0,fall=Math.sin(Math.min(1,z.fall)*Math.PI/2)*1.25,y=z.y+fall*(z.type==='brute'?1.15:.9),lean=dead?fall*.6:Math.sin(z.phase)*.05,scale=z.type==='brute'?1.35:z.type==='runner'?.9:1,col=z.type==='brute'?[.19,.22,.18]:z.type==='runner'?[.3,.12,.1]:[.31,.36,.28];let list=[];list.push({type:'box',pos:[z.x,y+(1.05*scale),z.z],scale:[.52*scale,.72*scale,.3*scale],col,ry:z.yaw,rz:lean});list.push({type:'sphere',pos:[z.x,y+(1.72*scale),z.z],scale:[.32*scale,.37*scale,.31*scale],col:[.48,.55,.4],ry:z.yaw,rz:lean});list.push({type:'box',pos:[z.x,y+(1.4*scale),z.z+.16*scale],scale:[.12*scale,.12*scale,.03],col:[.9,.05,.02],ry:z.yaw,rz:lean});for(let side of [-1,1]){list.push({type:'box',pos:[z.x+Math.cos(z.yaw)*side*.38*scale,y+(.95+Math.sin(z.phase+side)*.18)*scale,z.z-Math.sin(z.yaw)*side*.38*scale],scale:[.14*scale,.65*scale,.14*scale],col,ry:z.yaw,rz:Math.sin(z.phase+side)*.25});list.push({type:'box',pos:[z.x+Math.cos(z.yaw)*side*.19*scale,y+.35*scale,z.z-Math.sin(z.yaw)*side*.19*scale],scale:[.16*scale,.45*scale,.17*scale],col:[.16,.18,.16],ry:z.yaw})}return list}
function raySphere(o,d,c,r){let oc=vsub(o,c),b=dot(oc,d),cc=dot(oc,oc)-r*r,h=b*b-cc;if(h<0)return -1;let t=-b-Math.sqrt(h);return t<0?0:t}
function shoot(){
if(!started||paused||dead||fireCD>0||reload>0)return;
const w=weapons[current];
if(mag[current]<=0){audio.click();fireCD=.25;return}
mag[current]--; fireCD=w.rate; recoil=Math.min(1,recoil+.7); muzzle=.055; audio.gun(current);
const d=aimRay(), origin=[cam.x,cam.y,cam.z];
for(let p=0;p<(w.pellets||1);p++){
const dd=norm([d[0]+(Math.random()-.5)*w.spread*2,d[1]+(Math.random()-.5)*w.spread*2,d[2]+(Math.random()-.5)*w.spread*2]);
let hit=null,best=55;
for(const z of zombies){
if(z.dead)continue;
const t=raySphere(origin,dd,[z.x,z.y+1.5*(z.type==='brute'?1.35:.95),z.z],z.type==='brute'?.8:.65);
if(t>0&&t<best){best=t;hit=z}
}
if(hit){
const head=raySphere(origin,dd,[hit.x,hit.y+1.65*(hit.type==='brute'?1.35:.95),hit.z],hit.type==='brute'?.38:.33);
const hs=head>0&&head<=best+.05;
damage(hit,hs?w.damage*(w.type==='rifle'?2.8:2.4):w.damage,dd); audio.impact(hs);
}
effects.push({type:'tracer',from:[origin[0],origin[1]-.2,origin[2]],to:vadd(origin,vscale(dd,best)),life:.07});
}
}
function damage(z,n,dir){if(z.dead)return;z.hp-=n;z.flinch=z.type==='brute'?.09:.25;z.x+=dir[0]*.12;z.z+=dir[2]*.12;score+=n|0;effects.push({type:'blood',x:z.x,y:z.y+1.35,z:z.z,life:.5});if(z.hp<=0){z.dead=true;kills++;score+=z.type==='brute'?300:z.type==='runner'?150:100;audio.death(z.type);groan(z.type,3)}}
function reloadGun(){if(!started||paused||dead||reload>0)return;let w=weapons[current];if(mag[current]>=w.mag)return;if(reserve[current]<=0)return;reload=current===1?1.8:1.25;audio.reload(current);$('prompt').style.display='none'}
function finishReload(){let w=weapons[current],n=Math.min(w.mag-mag[current],reserve[current]);mag[current]+=n;if(reserve[current]!==Infinity)reserve[current]-=n;reload=0;updateHUD()}
function switchWeapon(n){if(n<0||n>2||!unlocked[n]||current===n)return;current=n;reload=0;fireCD=.15;updateHUD();audio.click()}
function hurt(n){if(dead)return;health-=n;shake=Math.min(.28,shake+.09);$('dmg').style.opacity=.65;setTimeout(()=>$('dmg').style.opacity=0,110);audio.hurt();if(health<=0){health=0;endGame(false)}}
function updatePickups(){for(let p of pickups){if(!p.active)continue;if(Math.hypot(cam.x-p.x,cam.z-p.z)<1.5&&Math.abs(cam.y-1.7)<2){if(p.type==='pistol'||p.type==='shotgun'||p.type==='rifle'){let i=weapons.findIndex(w=>w.type===p.type);unlocked[i]=true;if(i===2){mag[2]=weapons[2].mag;reserve[2]+=20}}else if(p.type==='ammo'){for(let i=0;i<3;i++)if(unlocked[i]&&reserve[i]!==Infinity)reserve[i]+=Math.round((i===1?6:12)*(1+wave*.12))}else if(p.type==='health')health=Math.min(100,health+35);p.active=false;audio.pickup();$('prompt').textContent='+ SUPPLIES';$('prompt').style.display='block';setTimeout(()=>$('prompt').style.display='none',1200)}}}
function updateHUD(){$('mag').textContent=mag[current];$('ammo').querySelector('span').textContent=' / '+weapons[current].mag+(reserve[current]===Infinity?' · ∞':' / '+reserve[current]);$('weapon').textContent=weapons[current].name;$('weaponName').textContent=['1 — SIDEARM','2 — SHOTGUN','3 — .308 RIFLE'][current];$('health').firstElementChild.style.width=health+'%';$('kills').textContent=kills;$('score').textContent=score;$('remaining').textContent=zombies.filter(z=>!z.dead).length+spawnQueue.length+' HOSTILES';let min=10+Math.floor((wave-1)*5+phaseTime*.05);$('clock').textContent=(3+Math.floor(min/60))+':'+String(min%60).padStart(2,'0')+' AM';$('phase').textContent=phase==='break'?'NEXT WAVE IN '+Math.ceil(breakTime)+'s':'WAVE ACTIVE'}
function endGame(win){dead=true;phase=win?'win':'dead';document.exitPointerLock?.();$('hud').classList.add('hide');$('end').classList.remove('hide');$('endTitle').textContent=win?'DAWN':'YOU DIDN’T ANSWER';$('endSub').textContent=win?'THE STARLITE SURVIVED THE NIGHT':'THE MOTEL KEEPS ANOTHER SECRET';$('endStats').innerHTML='You reached <b style="color:#e4b86b">Wave '+wave+' of 10</b><br>Zombies put down: <b style="color:#e4b86b">'+kills+'</b><br>Final score: <b style="color:#e4b86b">'+score+'</b>'}
function togglePause(v){if(!started||dead)return;paused=v??!paused;$('paused').classList.toggle('hide',!paused);if(paused)document.exitPointerLock?.();else canvas.requestPointerLock()}
canvas.addEventListener('click',()=>{if(started&&!paused&&!dead)canvas.requestPointerLock()});document.addEventListener('mousemove',e=>{if(document.pointerLockElement===canvas&&!paused){cam.yaw+=e.movementX*.0022;cam.pitch=Math.max(-1.35,Math.min(1.35,cam.pitch-e.movementY*.0022))}});$('start').onclick=()=>{audio.init();$('title').classList.add('hide');$('hud').classList.remove('hide');started=true;initGame();canvas.requestPointerLock()};$('resume').onclick=()=>togglePause(false);$('restart').onclick=()=>{location.reload()};
document.addEventListener('pointerlockchange',()=>{if(started&&!dead&&document.pointerLockElement!==canvas)togglePause(true)});addEventListener('keydown',e=>{keys[e.code]=true;if(e.code==='KeyR')reloadGun();if(e.code==='Digit1')switchWeapon(0);if(e.code==='Digit2')switchWeapon(1);if(e.code==='Digit3')switchWeapon(2);if(e.code==='Escape'&&started)togglePause();if(['Space','KeyW','KeyA','KeyS','KeyD'].includes(e.code))e.preventDefault()});addEventListener('keyup',e=>keys[e.code]=false);addEventListener('mousedown',e=>{if(e.button===0)shoot()});addEventListener('wheel',e=>{if(unlocked.some((x,i)=>x&&i!==current)){let d=e.deltaY>0?1:-1;for(let i=1;i<=3;i++){let n=(current+d*i+3)%3;if(unlocked[n]){switchWeapon(n);break}}}});
addEventListener('blur',()=>{keys={};if(started&&!paused)togglePause(true)});
// All sound is synthesized: no files or network assets.
const audio={ctx:null,master:null,init(){if(this.ctx)return;this.ctx=new(window.AudioContext||window.webkitAudioContext)();this.master=this.ctx.createGain();this.master.gain.value=.32;this.master.connect(this.ctx.destination);this.ambient()},tone(f,d,type='sine',vol=.2,end=f){let t=this.ctx.currentTime,o=this.ctx.createOscillator(),g=this.ctx.createGain();o.type=type;o.frequency.setValueAtTime(f,t);o.frequency.exponentialRampToValueAtTime(Math.max(20,end),t+d);g.gain.setValueAtTime(vol,t);g.gain.exponentialRampToValueAtTime(.001,t+d);o.connect(g);g.connect(this.master);o.start(t);o.stop(t+d)},noise(d,vol,cut=1800,type='lowpass'){let t=this.ctx.currentTime,n=Math.floor(this.ctx.sampleRate*d),b=this.ctx.createBuffer(1,n,this.ctx.sampleRate),a=b.getChannelData(0);for(let i=0;i<n;i++)a[i]=(Math.random()*2-1)*Math.pow(1-i/n,2);let s=this.ctx.createBufferSource(),f=this.ctx.createBiquadFilter(),g=this.ctx.createGain();s.buffer=b;f.type=type;f.frequency.value=cut;g.gain.value=vol;s.connect(f);f.connect(g);g.connect(this.master);s.start()},gun(i){let f=[230,95,145][i];this.noise(.09,.65,5000);this.tone(f,.13,'sawtooth',.34,f*.25);this.tone(70,.22,'square',.25,32)},reload(i){setTimeout(()=>this.click(),80);setTimeout(()=>this.click(),i===1?600:380)},click(){this.noise(.035,.18,5500,'highpass')},step(){this.noise(.05,.08,220)},pickup(){this.tone(540,.12,'sine',.12,780)},hurt(){this.noise(.16,.3,700);this.tone(90,.2,'sawtooth',.12,55)},impact(head){this.noise(.1,head?.5:.3,head?500:900);if(head)this.tone(70,.14,'square',.22,35)},sting(){this.tone(150,.7,'sawtooth',.17,48);this.noise(.8,.12,300)},death(type){this.tone(type==='brute'?60:100,.5,'sawtooth',.14,35)},heavy(){this.tone(45,.3,'square',.22,25)},groan(type,dist){let vol=Math.max(.015,.2/(1+dist*.055));this.tone(type==='runner'?170:type==='brute'?48:75,.7,'sawtooth',vol,type==='runner'?330:45)},ambient(){let c=this.ctx;let b=c.createBuffer(1,c.sampleRate*2,c.sampleRate),d=b.getChannelData(0);for(let i=0;i<d.length;i++)d[i]=(Math.random()*2-1)*.025;let s=c.createBufferSource();s.buffer=b;s.loop=true;let f=c.createBiquadFilter();f.type='lowpass';f.frequency.value=320;let g=c.createGain();g.gain.value=.12;s.connect(f);f.connect(g);g.connect(this.master);s.start()}};
function drawTextSigns(vp){// A dead neon letter and a broken segment buzz on the sign face.
if(Math.sin(time*9)>.35)draw({type:'box',pos:[-17.3,7,-10.73],scale:[.055,1.25,.04],col:[1,.03,.01],em:1},vp);
draw({type:'box',pos:[-18.1,7,-10.72],scale:[.18,1.2,.035],col:[.008,.008,.008],em:1},vp);
}
function fixedView(){let cy=Math.cos(cam.yaw),sy=Math.sin(cam.yaw),cp=Math.cos(cam.pitch),sp=Math.sin(cam.pitch);return new Float32Array([cy,0,-sy,0,sy*sp,cp,cy*sp,0,sy*cp,-sp,cy*cp,0,-cam.x,-cam.y,-cam.z,1])}
function tick(now){requestAnimationFrame(tick);let dt=Math.min(.035,(now-last)/1000);last=now;time+=dt;if(started&&!paused&&!dead){phaseTime+=dt;fireCD-=dt;muzzle=Math.max(0,muzzle-dt);recoil=Math.max(0,recoil-dt*2.5);shake*=Math.pow(.02,dt);if(phase==='break'){breakTime-=dt;phone+=dt;if(Math.floor(phone*2)!==Math.floor((phone-dt)*2)&&phone<17.5)audio.tone(1050,.13,'sine',.11,960);if(breakTime<=0)startWave()}else{while(spawnQueue.length&&zombies.filter(z=>!z.dead).length+spawnQueue.length<4+Math.ceil(wave*.8))spawnZombie(spawnQueue.pop());if(!spawnQueue.length&&!zombies.some(z=>!z.dead)){if(wave===10)endGame(true);else{phase='break';breakTime=18;resetPickups();showBanner('THE LINE IS RINGING','RELOAD · RESUPPLY · MOVE');dawn=Math.min(1,wave/10);audio.tone(850,.7,'sine',.08,1250)}}}move(dt);updateZombies(dt);updatePickups();if(reload>0){reload-=dt;if(reload<=0)finishReload()}dawn=Math.max(dawn,wave/10);updateHUD()}
if(!paused&&!dead){cam.y=1.7+Math.sin(cam.bob)*.035*moveSpeed;cam.x+=Math.cos(cam.yaw)*recoil*.025;cam.z-=Math.sin(cam.yaw)*recoil*.025}gl.clearColor(.025+dawn*.16,.035+dawn*.19,.05+dawn*.23,1);gl.clear(gl.COLOR_BUFFER_BIT|gl.DEPTH_BUFFER_BIT);let vp=matMul(perspective(1.25,canvas.width/canvas.height,.05,110),fixedView());drawTextSigns(vp);for(let o of objects){if(o.news)o.pos[0]+=dt*.6;if(o.pos[1]<-2)continue;draw(o,vp)}for(let p of pickups)if(p.active){let bob2=Math.sin(time*3+p.phase)*.08;let c=p.type==='health'?[.7,.13,.1]:p.type==='ammo'?[.5,.35,.12]:[.2,.3,.32];draw({type:'box',pos:[p.x,1.0+bob2,p.z],scale:[.28,.25,.2],col:c,ry:time*.8,em:1},vp);draw({type:'box',pos:[p.x,.9+bob2,p.z],scale:[.3,.3,.22],col:c,ry:-time*.8,em:1},vp)}for(let z of zombies)for(let p of zombieParts(z))draw(p,vp);effects=effects.filter(e=>(e.life-=dt)>0);for(let e of effects)if(e.type==='tracer')draw({type:'box',pos:mixv(e.from,e.to,.5),scale:[.015,.015,Math.hypot(...vsub(e.to,e.from))],col:[1,.65,.22],ry:Math.atan2(e.to[0]-e.from[0],e.to[2]-e.from[2]),rx:Math.acos(vsub(e.to,e.from)[1]/Math.hypot(...vsub(e.to,e.from)))},vp);$('flash').setAttribute('opacity',muzzle>0?1:0);$('gun').setAttribute('transform','translate(0 '+(moveSpeed?Math.sin(cam.bob)*5*moveSpeed:0)+') rotate('+(recoil*-7)+' 25 45)');}
requestAnimationFrame(tick);
</script></body></html>
<!-- agent-meta {"model":"stealth/space-bunny-alpha","provider":"openrouter","persona":"full","sessionId":"432e1ad0-60a7-4e3a-aeb0-a7b22e0f5186","tokensIn":2839600,"tokensOut":29368,"tokensTotal":2868968,"cost":0,"turns":50,"toolCalls":55,"failedToolCalls":2,"timestamp":"2026-09-23T18:01:13.833Z"} -->