← Zombie FPS results

Zombie FPS

GPT 6.1 Sol · full

Initial view of Zombie FPS

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 40,230 bytes · SHA-256 f5a6f3fa376a
<!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}body{margin:0;background:#05090c;color:#e9e4ce;font-family:Arial,sans-serif;overflow:hidden}canvas{position:absolute;inset:0;width:100%;height:100%}#hud{position:absolute;inset:0;pointer-events:none;display:none;text-shadow:0 2px 5px #000}.top{position:absolute;left:36px;right:36px;top:25px;display:flex;justify-content:space-between;align-items:start}.brand{font-size:12px;letter-spacing:4px;color:#b1bbb8}.wave{margin-top:8px;font-size:27px;font-weight:bold;letter-spacing:2px}.sub{font-size:12px;letter-spacing:2px;color:#c3ba9c;margin-top:8px}.time{text-align:right;color:#d0d7d4;font-family:monospace;font-size:18px}.bottom{position:absolute;bottom:32px;left:36px;right:36px;display:flex;justify-content:space-between;align-items:end}.label{color:#9faea9;font-size:10px;letter-spacing:3px}.value{font-size:34px;font-weight:bold;margin-top:6px}.bar{height:4px;width:180px;background:#34423e;margin-top:8px}.bar i{display:block;background:#b9ccb0;height:100%;width:100%}.ammo{text-align:right}.ammo .value{font-family:monospace}.controls{position:absolute;bottom:16px;left:50%;transform:translateX(-50%);font-size:10px;color:#8a9e96;letter-spacing:1px;white-space:nowrap}#cross{position:absolute;left:50%;top:50%;width:20px;height:20px;transform:translate(-50%,-50%)}#cross:before,#cross:after{content:'';position:absolute;background:#cbdaceaa}#cross:before{width:2px;height:20px;left:9px}#cross:after{width:20px;height:2px;top:9px}#hit{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%) rotate(45deg);font-size:38px;color:#fff;opacity:0}#hurt{position:absolute;inset:0;pointer-events:none;box-shadow:inset 0 0 130px 45px #a20c13;opacity:0}#banner{position:absolute;top:27%;width:100%;text-align:center;font-size:32px;letter-spacing:6px;color:#f4dbc1;opacity:0}#notice{position:absolute;top:64%;width:100%;text-align:center;color:#e7d19c;letter-spacing:2px;font-size:13px}#overlay{position:absolute;inset:0;display:flex;justify-content:center;align-items:center;background:linear-gradient(90deg,#02090edb,#06101688 65%,#02090e55)}.panel{width:min(650px,90vw);padding:40px}.eyebrow{color:#d2ae74;letter-spacing:5px;font-size:11px;margin-bottom:23px}.title{font-family:Georgia,serif;font-size:clamp(48px,7vw,82px);line-height:.94;letter-spacing:-2px;color:#eee8d8;text-shadow:0 0 40px #7adbd32b}.title em{font-style:normal;color:#a8d7cd}.tag{font-size:12px;letter-spacing:4px;margin:25px 0;color:#ceab86}.desc{color:#aabbb6;line-height:1.8;font-size:14px;max-width:440px}button{background:#cee1cb;border:0;padding:17px 30px;letter-spacing:3px;font-weight:bold;color:#162c29;margin:22px 0 18px;cursor:pointer}button:hover{background:#fff2cc}.keys{color:#91a19b;font-size:11px;line-height:2;letter-spacing:1px}.footer{position:absolute;bottom:22px;right:30px;font-size:10px;letter-spacing:3px;color:#76958d}#error{position:absolute;top:10px;color:white}
</style></head><body><canvas id="scene"></canvas><canvas id="weapon"></canvas><div id="hud"><div class="top"><div><div class="brand">STARLITE MOTOR COURT</div><div class="wave" id="wave">WAVE 01 / 10</div><div class="sub" id="remaining">6 HOSTILES REMAIN</div></div><div class="time"><span id="clock">03:10 AM</span><div class="sub" id="kills">0 KILLS · 0 POINTS</div></div></div><div id="cross"></div><div id="hit">+</div><div id="banner"></div><div id="notice"></div><div class="bottom"><div><div class="label">VITAL SIGNS</div><div class="value"><span id="health">100</span> <span style="font-size:12px;color:#94aaa2">HP</span></div><div class="bar"><i id="healthbar"></i></div></div><div class="ammo"><div class="label" id="gunname">SERVICE PISTOL</div><div class="value"><span id="mag">12</span><span style="color:#758980;font-size:22px"> / <span id="reserve">∞</span></span></div><div class="sub" id="reloadtext">9 MM · SEMI AUTO</div></div></div><div class="controls">WASD MOVE &nbsp; SHIFT SPRINT &nbsp; R RELOAD &nbsp; 1 / 2 / 3 WEAPONS &nbsp; ESC PAUSE</div></div><div id="hurt"></div><div id="overlay"><div class="panel"><div class="eyebrow">A ROADSIDE SURVIVAL STORY</div><div class="title" id="title">LAST CALL<br>AT THE <em>STARLITE</em></div><div class="tag" id="tag">3:10 A.M. · NO VACANCY. NO WAY OUT.</div><div class="desc" id="desc">The coffee is warm. The rooms are empty.<br>Something is moving beyond the fence.<br>Hold the motor court through ten waves. Live to see dawn.</div><button id="start">CLICK TO SURVIVE →</button><div class="keys">MOUSE Aim &nbsp; · &nbsp; LEFT CLICK Fire &nbsp; · &nbsp; WASD Move<br>SHIFT Sprint &nbsp; · &nbsp; R Reload &nbsp; · &nbsp; 1–3 / WHEEL Switch<br>Walk over supplies and weapons to collect. Pool steps face the lot.</div></div><div class="footer">HEADSHOTS COUNT. KEEP MOVING.</div></div><script>
'use strict';
// Self-contained WebGL renderer: flat-shaded geometry, atmospheric depth fog.
const canvas=document.getElementById('scene'),gl=canvas.getContext('webgl',{antialias:true});
const $=id=>document.getElementById(id);const wc=$('weapon'),ctx=wc.getContext('2d');
const vs=`attribute vec3 p;attribute vec3 n;attribute vec3 c;uniform mat4 vp;uniform vec3 eye;varying vec3 col;varying float dist;void main(){gl_Position=vp*vec4(p,1.);dist=distance(p,eye);float l=.35+.65*max(dot(n,normalize(vec3(-.4,1.,.3))),0.);vec3 light=vec3(0.);light+=vec3(1.,.56,.22)*2.5/(1.+distance(p,vec3(-18.,5.,8.))*distance(p,vec3(-18.,5.,8.))*.18);light+=vec3(1.,.56,.22)*2.5/(1.+distance(p,vec3(20.,5.,4.))*distance(p,vec3(20.,5.,4.))*.18);light+=vec3(.35,.83,.86)*2.3/(1.+distance(p,vec3(-7.,2.8,-18.))*distance(p,vec3(-7.,2.8,-18.))*.24);light+=vec3(.85,.63,.34)*2.0/(1.+distance(p,vec3(-27.,2.,-12.))*distance(p,vec3(-27.,2.,-12.))*.15);light+=vec3(.64,.83,.75)*1.8/(1.+distance(p,vec3(24.,3.,-13.))*distance(p,vec3(24.,3.,-13.))*.12);float glow=step(.77,max(c.r,max(c.g,c.b)));col=c*(l+light)+c*glow*.55;}`;
const fs=`precision mediump float;varying vec3 col;varying float dist;uniform vec3 fog;uniform float dawn;void main(){float f=1.-exp(-dist*dist*.00064);gl_FragColor=vec4(mix(col*(.75+dawn*.6),fog,clamp(f,0.,.97)),1.);}`;
function shader(type,s){let a=gl.createShader(type);gl.shaderSource(a,s);gl.compileShader(a);return a}const program=gl.createProgram();gl.attachShader(program,shader(gl.VERTEX_SHADER,vs));gl.attachShader(program,shader(gl.FRAGMENT_SHADER,fs));gl.linkProgram(program);gl.useProgram(program);const buffer=gl.createBuffer();gl.bindBuffer(gl.ARRAY_BUFFER,buffer);for(let [name,off] of [['p',0],['n',12],['c',24]]){let a=gl.getAttribLocation(program,name);gl.enableVertexAttribArray(a);gl.vertexAttribPointer(a,3,gl.FLOAT,false,36,off)}const uniforms={};for(let n of ['vp','eye','fog','dawn'])uniforms[n]=gl.getUniformLocation(program,n);gl.enable(gl.DEPTH_TEST);
const C={asphalt:[.11,.15,.15],wall:[.43,.43,.34],trim:[.18,.35,.33],orange:[1,.55,.19],blue:[.4,.8,.88],pink:[1,.23,.43],white:[.79,.8,.71]};let verts=[],world=[],solids=[];let building=[];const texts=[];let rndSeed=37;function rand(){rndSeed=(rndSeed*1664525+1013904223)>>>0;return rndSeed/4294967296}function color(c,k=1){return c.map(v=>v*k)}
function quad(a,b,c,d,n,col){for(let v of [a,b,c,a,c,d])verts.push(...v,...n,...col)}
function box(x,y,z,w,h,d,col,rot=0){let co=Math.cos(rot),si=Math.sin(rot);function v(a,b,c){return[x+a*co-c*si,y+b,z+a*si+c*co]}let a=w/2,b=d/2;quad(v(-a,0,b),v(a,0,b),v(a,h,b),v(-a,h,b),[-si,0,co],col);quad(v(a,0,-b),v(-a,0,-b),v(-a,h,-b),v(a,h,-b),[si,0,-co],col);quad(v(-a,0,-b),v(-a,0,b),v(-a,h,b),v(-a,h,-b),[-co,0,-si],col);quad(v(a,0,b),v(a,0,-b),v(a,h,-b),v(a,h,b),[co,0,si],col);quad(v(-a,h,b),v(a,h,b),v(a,h,-b),v(-a,h,-b),[0,1,0],col);quad(v(-a,0,-b),v(a,0,-b),v(a,0,b),v(-a,0,b),[0,-1,0],col)}
function add(x,y,z,w,h,d,c,solid=false,rot=0){box(x,y,z,w,h,d,c,rot);if(solid)solids.push({x,z,w:w/2,d:d/2,h})}function line(a,b,width,c){let dx=b[0]-a[0],dz=b[2]-a[2],dy=b[1]-a[1];if(Math.abs(dy)>Math.hypot(dx,dz))box(a[0],Math.min(a[1],b[1]),a[2],width,Math.abs(dy),width,c);else box((a[0]+b[0])/2,(a[1]+b[1])/2,(a[2]+b[2])/2,Math.hypot(dx,dz),width,width,c,Math.atan2(dz,dx))}
// Voxel stroke lettering is actual world geometry, not a screen label.
const font={A:['010','101','111','101','101'],B:['110','101','110','101','110'],C:['111','100','100','100','111'],D:['110','101','101','101','110'],E:['111','100','110','100','111'],F:['111','100','110','100','100'],G:['111','100','101','101','111'],H:['101','101','111','101','101'],I:['111','010','010','010','111'],K:['101','101','110','101','101'],L:['100','100','100','100','111'],M:['101','111','111','101','101'],N:['101','111','111','111','101'],O:['111','101','101','101','111'],P:['110','101','110','100','100'],R:['110','101','110','101','101'],S:['111','100','111','001','111'],T:['111','010','010','010','010'],U:['101','101','101','101','111'],V:['101','101','101','101','010'],W:['101','101','111','111','101'],Y:['101','101','010','010','010'],'0':['111','101','101','101','111'],'1':['010','110','010','010','111'],'2':['110','001','010','100','111'],'3':['110','001','010','001','110'],'4':['101','101','111','001','001'],'5':['111','100','110','001','110'],'6':['111','100','111','101','111'],'7':['111','001','010','010','010'],'8':['111','101','111','101','111'],'9':['111','101','111','001','111']};
function text(s,x,y,z,size,c){let start=x-s.length*size*2;for(let i=0;i<s.length;i++){let f=font[s[i]];if(!f)continue;for(let r=0;r<5;r++)for(let k=0;k<3;k++)if(f[r][k]==='1')box(start+i*size*4+k*size,y+(4-r)*size,z,size*.75,size*.78,.035,c)}}
function stool(x,z){add(x,0,z,.12,.7,.12,[.3,.35,.34]);add(x,.65,z,.65,.12,.65,[.65,.15,.12])}
function build(){verts=[];solids=[];add(0,-.2,0,66,.2,54,C.asphalt); // pool ground is assembled separately below
// carve pool hole by covering ground with dedicated recessed mesh (ground excluded in pool footprint)
verts=[];add(-19,-.2,0,28,.2,54,C.asphalt);add(24,-.2,0,18,.2,54,C.asphalt);add(5,-.2,-9.5,20,.2,35,C.asphalt);add(5,-.2,24,20,.2,6,C.asphalt);
for(let x=-25;x<=25;x+=5){add(x,.01,2,.09,.015,6,[.45,.43,.3]);add(x,.01,-4,4,.015,.08,[.45,.43,.3])}for(let i=0;i<80;i++){let x=rand()*64-32,z=rand()*50-25;if(x>-5&&x<15&&z>8&&z<21)continue;add(x,.025,z,.035,.008,rand()*2+.4,[.05,.075,.075],false,rand()*6)}
// motel: six rooms, accessible open room 104
for(let i=0;i<6;i++){let x=-25+i*6;if(i!==3)add(x,0,-19,6,3.5,.3,C.wall,true);add(x,0,-24,6,3.5,.3,C.wall,true);add(x-3,0,-21.5,.2,3.5,5,C.wall,true);add(x,3.5,-21,6.2,.2,9,C.trim);let open=i===3; // remove front wall in open room
if(open){solids.pop();solids.pop(); // restore back/side only safely below
solids.push({x,z:-24,w:3,d:.15},{x:x-3,z:-21.5,w:.1,d:2.5});add(x,0,-19.02,1.05,2.55,.12,[.18,.24,.22],false,-1.3);add(x-2,0,-19,2,3.5,.3,C.wall,true);add(x+2,0,-19,2,3.5,.3,C.wall,true);add(x,2.7,-19,2, .8,.3,C.wall);add(x,-.03,-21,5.8,.04,5,[.3,.31,.27]);add(x+1,0,-22,1.8,.5,2.4,[.3,.2,.19]);add(x+1,.5,-22,1.8,.18,2.4,[.64,.62,.47]);add(x+1,.7,-22.8,1.3,.15,.5,C.white);add(x-1,0,-22.8,.8,.8,.7,C.trim);add(x-1,.8,-22.8,.3,.4,.3,C.orange);add(x-1,.05,-20,.8,.3,.5,[.35,.19,.13]);}else{add(x,0,-18.78,1.1,2.6,.12,[.12,.24,.22]);add(x+.4,1.1,-18.65,.06,.08,.08,C.orange);add(x+1.8,1.3,-18.8,1.5,1.05,.08,[.13,.22,.23]);add(x+1.8,1.8,-18.7,1.5,.035,.04,C.trim)}text('10'+(i+1),x,2.8,-18.72,.1,C.white);add(x,2.9,-18.45,.55,.13,.25,i===3?C.blue:C.orange);add(x-3,0,-16,.12,3.5,.12,C.trim,true)}add(-10,-.02,-17,36,.08,4,[.29,.31,.27]);
// ice and soda machines
add(-21,0,-16.8,1.4,1.8,.9,[.7,.74,.7],true);text('ICE',-21,1.15,-16.32,.12,C.blue);add(-21,.3,-16.3,1,.45,.08,[.16,.2,.21]);add(-19,0,-16.8,1.1,2.1,.9,[.15,.2,.23],true);add(-19, .4,-16.32,.78,1.45,.04,[.22,.66,.59]);text('COLA',-19,1.35,-16.28,.06,C.white);for(let i=0;i<4;i++)add(-18.72,.55+i*.22,-16.25,.12,.09,.05,C.orange);
// laundry cart / scattered towels
add(-12,.25,-16,.85,.8,.7,[.48,.52,.47]);for(let x of [-12.35,-11.65])add(x,.06,-16,.16,.2,.16,[.08,.1,.1]);add(-12,1,-16,.8,.1,.7,C.white);add(-13,.02,-15,.65,.03,.45,[.53,.56,.5]);
// office at west; open front entrance
add(-26,0,-7.5,12,3.8,.25,C.wall,true);add(-31.8,0,-12,.25,3.8,9,C.wall,true);add(-20.2,0,-12,.25,3.8,9,C.wall,true);add(-26,0,-16.5,12,3.8,.25,C.wall,true);add(-26,3.8,-12,12.4,.25,10,C.trim); // front wall split doorway
// replace front full collider with windows and door gap
solids=solids.filter(s=>!(s.x===-26&&s.z===-7.5));add(-29,0,-7.4,6,1,.2,C.trim,true);add(-29,2.7,-7.4,6,1.1,.2,C.wall);add(-21.2,0,-7.4,2,3.8,.2,C.wall,true);add(-29,1,-7.42,5.4,1.7,.035,[.2,.43,.38]); // translucent-like bright window
text('OFFICE',-26,3.05,-7.2,.14,C.orange);add(-26,.01,-12,11,.03,9,[.3,.3,.24]);add(-27,0,-13.7,6,1.05,1,[.39,.26,.16],true);add(-27,1.05,-13.7,6,.09,1.1,[.68,.54,.34]);add(-27.3,1.16,-13.5,.6,.25,.45,[.17,.21,.18]);add(-29,1.18,-13.5,.5,.03,.4,C.white); // ledger
add(-27,1.3,-16.2,3,1.7,.1,[.24,.18,.12]);for(let i=0;i<12;i++){add(-28.2+(i%6)*.45,1.6+Math.floor(i/6)*.6,-16.05,.045,.2,.04,C.orange)}text('KEYS',-27,2.7,-16,.1,C.white);add(-30.3,0,-15.2,1,.9,.7,[.25,.19,.13]);add(-30.3,.9,-15.2,1, .75,.65,[.14,.16,.15]);add(-30.3,1.05,-14.85,.8,.45,.04,C.blue);add(-23,0,-15.5,.65,1,.65,[.2,.24,.21]);add(-23,1,-15.5,.75,.08,.7,C.orange);
// payphone outside office
add(-21.4,0,-6,.16,1.5,.16,C.trim);add(-21.4,1.3,-6,.7,1.1,.4,[.23,.34,.37]);add(-21.4,1.4,-5.76,.54,.8,.04,[.08,.13,.14]);text('PHONE',-21.4,2.1,-5.72,.038,C.blue);add(-21.6,1.6,-5.66,.12,.45,.09,[.03,.04,.04]);for(let i=0;i<9;i++)add(-21.35+(i%3)*.055,1.5+Math.floor(i/3)*.08,-5.65,.03,.035,.02,C.white);
// diner on east, open front
add(24,0,-20,15,4,.25,[.5,.49,.4],true);add(31.4,0,-13,.25,4,14,C.wall,true);add(16.6,0,-13,.25,4,14,C.wall,true);add(24,4,-13,15.5,.3,15,[.23,.38,.37]);add(20,0,-6,7,1,.2,[.46,.14,.12],true);add(29.5,0,-6,4,1,.2,[.46,.14,.12],true);add(24,3,-6,15,1,.2,C.trim);text('DINER',24,3.2,-5.8,.18,C.orange);add(24,.02,-13,14,.03,13,[.37,.4,.36]);for(let i=0;i<7;i++)for(let j=0;j<6;j++)add(17.5+i*2,.06,-7-j*2,1,.02,1,[.24,.29,.27]);add(21,0,-14,1.2,1.1,8,[.49,.2,.14],true);add(21,1.1,-14,1.6,.12,8,C.white);for(let z=-17;z<=-10;z+=2)stool(19,z);
// pie case, pies, coffee warmer and coffee pot
add(21,1.24,-11,1.1,.07,1.3,[.22,.27,.24]);for(let x of [20.5,21.5])add(x,1.3,-11,.05,.65,1.3,C.blue);add(21,1.93,-11,1.1,.05,1.3,C.blue);for(let z of [-11.35,-10.65]){add(21,1.32,z,.65,.1,.5,[.75,.48,.2]);add(21,1.44,z,.4,.05,.4,[.5,.26,.11])}add(21,1.24,-16,.7,.18,.6,[.14,.17,.17]);add(21,1.42,-16,.45,.38,.4,[.24,.12,.05]);add(21,1.8,-16,.45,.06,.4,[.08,.09,.09]);add(21.35,1.5,-16,.08,.25,.14,C.white);add(21,1.22,-15.5,.06,.02,.07,C.orange);
for(let z of [-17,-12,-8]){add(28,0,z,1.8,.55,.6,[.52,.17,.13],true);add(28,.55,z+.25,1.8,.7,.2,[.52,.17,.13]);add(28,0,z-2,1.8,.55,.6,[.52,.17,.13],true);add(28,.55,z-2.25,1.8,.7,.2,[.52,.17,.13]);add(28,0,z-1,.12,.8,.12,[.34,.37,.35]);add(28,.8,z-1,2,.1,1,C.white);add(28,.91,z-1,.45,.035,.4,[.75,.7,.5]);add(28.2,.95,z-1,.15,.05,.13,[.49,.23,.1]);add(27.3,.91,z-1,.14,.19,.14,C.white)}
// recessed pool  x -5..15 z 8..21
add(5,-2.25,14.5,20,.2,13,[.24,.37,.34]);add(-5,-2,14.5,.3,2,13,[.36,.55,.51]);add(15,-2,14.5,.3,2,13,[.36,.55,.51]);add(5,-2,21,20,2,.3,[.36,.55,.51]);add(0,-2,8,10,2,.3,[.36,.55,.51]);add(12,-2,8,6,2,.3,[.36,.55,.51]);for(let x of [-5,15])add(x,0,14.5,.6,.12,13.5,C.white);add(5,0,21,20,.12,.6,C.white);add(0,0,8,10,.12,.6,C.white);add(12,0,8,6,.12,.6,C.white);for(let i=0;i<6;i++)add(7,-i*.34-.34,8+i*.55,3,.34,.6,[.46,.59,.55]);for(let i=0;i<45;i++)add(rand()*18-4,-2.02,9+rand()*11,.1+rand()*.22,.025,.15,[.29+rand()*.12,.21,.07],false,rand()*6); // overturned lawn chair
add(2,-1.9,17,1.5,.13,.6,C.white,false,.5);add(2,-1.85,17,.13,.8,.7,C.trim,false,.5);add(2.6,-1.85,17,.13,.8,.7,C.trim,false,.5);add(3,-2,18,.6,.04,.3,[.6,.5,.24]);text('POOL',5,.18,21.35,.1,C.blue);
// wagon and open door, headlight beams painted into atmosphere ground
add(-11,0,-1,2.6,.65,5,[.22,.35,.29],true);add(-11,.65,-1.5,2.4,.75,3.1,[.28,.4,.31]);add(-11,.95,0,2.15,.43,.09,[.09,.21,.22]);add(-11,1.4,-1.5,2.5,.12,3.2,[.3,.4,.3]);for(let x of [-12.3,-9.7])for(let z of [-2.8,.6])add(x,.05,z,.3,.6,.75,[.04,.045,.045]);add(-12.8,.45,.1,1.3,.9,.15,[.27,.4,.31],true,-.9);for(let x of [-11.85,-10.15]){add(x,.5,1.55,.6,.25,.06,[1,.96,.63]);quad([x-.2,.035,1.6],[x+.2,.035,1.6],[x+2.5,.035,11],[x-2.5,.035,11],[0,1,0],[.25,.25,.15])}add(-11,.58,1.6,1,.15,.07,[.13,.14,.13]);
// perimeter chain link and treeline
for(let side=0;side<4;side++){let horiz=side<2,len=horiz?64:52;for(let k=0;k<=len;k+=4){let x=horiz?k-32:(side===2?-32:32),z=horiz?(side===0?-26:26):k-26;add(x,0,z,.07,2.6,.07,[.26,.33,.32]);}for(let k=0;k<len;k+=.8){let x=horiz?k-32:(side===2?-32:32),z=horiz?(side===0?-26:26):k-26;for(let h=0;h<2.2;h+=.55){line([x,h,z],[x+(horiz?.8:0),h+.5,z+(horiz?0:.8)],.018,[.22,.3,.29]);line([x,h+.5,z],[x+(horiz?.8:0),h,z+(horiz?0:.8)],.018,[.22,.3,.29])}}}
for(let i=0;i<90;i++){let a=rand()*Math.PI*2,x=Math.cos(a)*(39+rand()*10),z=Math.sin(a)*(34+rand()*10);add(x,0,z,.5,7+rand()*5,.5,[.075,.105,.085]);for(let j=0;j<3;j++)add(x,3+j*1.6,z,4-j*.7,2,4-j*.7,[.06,.12,.09])}
// neon sign structure, street lamps
add(-5,0,23,.25,7,.25,C.trim);add(-5,5.8,23,12,2.3,.25,[.08,.13,.13]);text('MOTOR COURT',-5,5.98,23.2,.12,C.white);text('VACANCY',-5,5.35,23.2,.085,C.orange); // STARLITE animated later
for(let [x,z] of [[-18,8],[20,4],[-3,-14]]){add(x,0,z,.15,6,.15,[.24,.28,.26]);add(x+.5,5.8,z,1.4,.15,.4,[.4,.39,.29]);add(x+.5,5.73,z,1,.04,.3,C.orange);add(x, .04,z,5,.015,5,[.21,.2,.12]);}
// story props: trash, fallen bicycle, spilled travel bag, tables
add(-23,0,-5, .75,.9,.75,[.22,.27,.21],true);add(-23,.92,-5,.8,.05,.8,C.trim);for(let i=0;i<15;i++)add(-22+rand()*5,.04,-4+rand()*3,.25,.015,.18,[.5,.49,.36],false,rand()*6);for(let x of [-3,-1.8]){add(x,.05,4,.1,.5,.5,[.07,.08,.08],false,1.57)}line([-3,.25,4],[-2,.12,4.4],.08,[.5,.2,.15]);line([-2,.12,4.4],[-1.8,.3,4],.08,[.5,.2,.15]);add(11,0,-16,1,.35,.7,[.35,.2,.13]);add(11,.35,-16,1,.08,.7,[.42,.28,.18],false,.5);add(11.7,.03,-15.6,.6,.06,.4,[.38,.5,.53]);add(-7,.04,7,.22,.18,.6,[.13,.16,.15]);quad([-7,.035,7],[-6.8,.035,7],[0,.035,9],[-1,.035,11],[0,1,0],[.22,.28,.24]);
world=new Float32Array(verts)}build();
let player={x:0,y:1.7,z:3,yaw:0,pitch:0,hp:100},keys={},state='title',paused=false,wave=0,kills=0,score=0,zombies=[],corpses=[],particles=[],pickups=[],pending=0,spawnClock=0,breakTime=0,gameTime=0,last=0,shotTimer=0,reloading=0,recoil=0,hurt=0,hit=0,bannerTime=0,noticeTime=0,walk=0,groanTimer=0,ringTimer=0;
const guns=[{name:'SERVICE PISTOL',mag:12,cap:12,reserve:Infinity,damage:34,rate:.24,spread:.007,reload:1.25,owned:true},{name:'PUMP SHOTGUN',mag:6,cap:6,reserve:30,damage:23,rate:.78,spread:.065,reload:2.1,owned:false},{name:'AUTOMATIC RIFLE',mag:24,cap:24,reserve:96,damage:27,rate:.115,spread:.012,reload:1.7,owned:false}];let gun=0,firing=false;let ac=null;
function tone(f,d,v=.12,type='sine',end=30){if(!ac)return;let o=ac.createOscillator(),g=ac.createGain();o.type=type;o.frequency.setValueAtTime(f,ac.currentTime);o.frequency.exponentialRampToValueAtTime(Math.max(5,end),ac.currentTime+d);g.gain.setValueAtTime(v,ac.currentTime);g.gain.exponentialRampToValueAtTime(.001,ac.currentTime+d);o.connect(g);g.connect(ac.destination);o.start();o.stop(ac.currentTime+d)}function noise(d,v,low=1500){if(!ac)return;let b=ac.createBuffer(1,ac.sampleRate*d,ac.sampleRate),a=b.getChannelData(0);for(let i=0;i<a.length;i++)a[i]=(Math.random()*2-1)*(1-i/a.length);let s=ac.createBufferSource(),f=ac.createBiquadFilter(),g=ac.createGain();s.buffer=b;f.type='lowpass';f.frequency.value=low;g.gain.value=v;s.connect(f);f.connect(g);g.connect(ac.destination);s.start()}
function initAudio(){if(ac){ac.resume();return}ac=new (window.AudioContext||window.webkitAudioContext)();let b=ac.createBuffer(1,ac.sampleRate*3,ac.sampleRate),a=b.getChannelData(0);for(let i=0;i<a.length;i++)a[i]=Math.random()*2-1;let s=ac.createBufferSource(),f=ac.createBiquadFilter(),g=ac.createGain();s.buffer=b;s.loop=true;f.type='lowpass';f.frequency.value=210;g.gain.value=.045;s.connect(f);f.connect(g);g.connect(ac.destination);s.start();for(let f of [60,119]){let o=ac.createOscillator(),v=ac.createGain();o.frequency.value=f;v.gain.value=.008;o.connect(v);v.connect(ac.destination);o.start()}}
function notify(s){$('notice').textContent=s;noticeTime=3.5}function announce(s){$('banner').textContent=s;bannerTime=3.2}
function supplies(){pickups=pickups.filter(p=>p.kind==='weapon');for(let [x,z] of [[-3,-15],[-23,-10],[27,-9],[7,15],[-10,2],[18,8]])pickups.push({x,z,kind:'ammo'});for(let [x,z] of [[-27,-11],[28,-17],[0,19]])pickups.push({x,z,kind:'health'});if(!guns[1].owned&&!pickups.some(p=>p.weapon===1))pickups.push({x:-13.5,z:1,kind:'weapon',weapon:1});if(!guns[2].owned&&!pickups.some(p=>p.weapon===2))pickups.push({x:-25,z:-15,kind:'weapon',weapon:2})}
function startWave(){wave++;pending=5+wave*3;spawnClock=.1;breakTime=0;state='wave';announce('WAVE '+String(wave).padStart(2,'0')+' — '+(wave===10?'LAST CALL':wave<3?'THEY FOUND YOU':wave<6?'KEEP THE LIGHTS ON':'HOLD UNTIL DAWN'));tone(130,1,.18,'sawtooth',35);setTimeout(()=>tone(195,.5,.1,'triangle',90),180)}
function reset(){player={x:0,y:1.7,z:3,yaw:0,pitch:0,hp:100};wave=kills=score=0;zombies=[];corpses=[];particles=[];pickups=[];pending=0;gameTime=0;gun=0;for(let g of guns){g.mag=g.cap;g.owned=g===guns[0];g.reserve=g===guns[0]?Infinity:g===guns[1]?30:96}reloading=0;shotTimer=0;hurt=0;recoil=0;hit=0;firing=false;keys={};ringTimer=0;paused=false;supplies();startWave();$('hud').style.display='block';$('overlay').style.display='none'}
function lock(){canvas.requestPointerLock?.()}
$('start').onclick=()=>{initAudio();if(state==='title'||state==='dead'||state==='win')reset();else{paused=false;$('overlay').style.display='none'}lock()};
document.addEventListener('pointerlockchange',()=>{if(document.pointerLockElement!==canvas&&(state==='wave'||state==='break')&&!paused)pause()});function pause(){paused=true;firing=false;keys={};$('overlay').style.display='flex';$('title').innerHTML='STILL<br><em>BREATHING?</em>';$('tag').textContent='THE NIGHT CAN WAIT.';$('desc').textContent='Take a breath. Your position is safe while paused.';$('start').textContent='RESUME →'}
document.addEventListener('mousemove',e=>{if(document.pointerLockElement===canvas&&!paused){player.yaw+=e.movementX*.0022;player.pitch=Math.max(-1.35,Math.min(1.35,player.pitch-e.movementY*.0022))}});document.addEventListener('keydown',e=>{keys[e.code]=true;if(e.code==='KeyR')reload();if(['Digit1','Digit2','Digit3'].includes(e.code))switchGun(+e.code.slice(-1)-1);if(e.code==='Escape'&&!paused&&state!=='title')pause();if(['Space','ArrowUp','ArrowDown'].includes(e.code))e.preventDefault()});document.addEventListener('keyup',e=>keys[e.code]=false);document.addEventListener('mousedown',e=>{if(e.button===0&&!paused&&(state==='wave'||state==='break')){if(document.pointerLockElement!==canvas)lock();firing=true;shoot()}});document.addEventListener('mouseup',()=>firing=false);document.addEventListener('wheel',e=>{if(paused||state==='title')return;let n=gun;for(let i=0;i<3;i++){n=(n+(e.deltaY>0?1:2))%3;if(guns[n].owned)break}switchGun(n)},{passive:true});window.addEventListener('blur',()=>{if(!paused&&(state==='wave'||state==='break'))pause()});
function switchGun(n){if(guns[n].owned&&n!==gun){gun=n;reloading=0;recoil=.15;tone(400,.06,.03,'square',180)}else if(!guns[n].owned)notify(n===1?'SHOTGUN — BESIDE THE WAGON':'RIFLE — BEHIND THE OFFICE COUNTER')}
function reload(){let g=guns[gun];if(paused||reloading||g.mag===g.cap||g.reserve<=0)return;reloading=g.reload;tone(350,.1,.08,'square',120);notify('RELOADING')}
function blocked(x,z,r=.32){if(x<-31||x>31||z<-25||z>25)return true;return solids.some(s=>Math.abs(x-s.x)<s.w+r&&Math.abs(z-s.z)<s.d+r)}
function floorAt(x,z){if(x>-4.85&&x<14.85&&z>8&&z<20.85){if(x>5.5&&x<8.5&&z<11.3)return -Math.min(2.04,(Math.floor((z-8)/.55)+1)*.34);return -2.04}return 0}
function movePlayer(dx,dz){let nx=player.x+dx,nz=player.z+dz; // cannot jump straight out of deep pool
let f=floorAt(player.x,player.z),nf=floorAt(nx,nz);if(nf-f>.4)return;if(!blocked(nx,player.z))player.x=nx;if(!blocked(player.x,nz))player.z=nz}
// Navigation on a shared half-metre grid; paths route around architecture and the pool.
const gridStep=1,gw=63,gh=51;let flow=new Int16Array(gw*gh),nav=new Uint8Array(gw*gh),flowTimer=0;
function cell(x,z){return Math.max(0,Math.min(gh-1,Math.round(z+25)))*gw+Math.max(0,Math.min(gw-1,Math.round(x+31)))}
for(let iz=0;iz<gh;iz++)for(let ix=0;ix<gw;ix++){let x=ix-31,z=iz-25;nav[iz*gw+ix]=blocked(x,z,.45)|| (x>-5.2&&x<15.2&&z>7.5&&z<21.3)?0:1;if(x>=6&&x<=8&&z>=8&&z<=11)nav[iz*gw+ix]=1}
function computeFlow(){flow.fill(-1);let tx=player.x,tz=player.z;if(floorAt(tx,tz)<-.4){tx=7;tz=7}let root=cell(tx,tz);let q=[root];flow[root]=0;for(let h=0;h<q.length;h++){let c=q[h],ix=c%gw;for(let n of [ix>0?c-1:-1,ix<gw-1?c+1:-1,c-gw,c+gw])if(n>=0&&n<flow.length&&nav[n]&&flow[n]===-1){flow[n]=flow[c]+1;q.push(n)}}}
const spawns=[[-30,22],[30,22],[-30,4],[30,2],[-18,24],[19,24],[11,-24],[-8,24]];
function spawn(){let choices=spawns.filter(p=>{let dx=p[0]-player.x,dz=p[1]-player.z,dist=Math.hypot(dx,dz),dot=(dx*Math.sin(player.yaw)-dz*Math.cos(player.yaw))/dist;return dist>18&&(dot<.4||obstruct([player.x,player.y,player.z],[dx/dist,0,dz/dist],dist))});let p=(choices.length?choices:spawns)[Math.floor(rand()*(choices.length||spawns.length))];let type=wave>=6&&rand()<.16?'brute':wave>=3&&rand()<.28?'runner':'shambler';let brute=type==='brute',runner=type==='runner';zombies.push({x:p[0]+rand()*.6,z:p[1]+rand()*.6,y:0,type,h:brute?2.75:runner?1.6:1.95,hp:brute?340:runner?55:85+wave*3,speed:brute?.68+wave*.03:runner?2.25+wave*.06:.7+wave*.065,hit:0,attack:0,phase:rand()*6,age:0});pending--}
function raySphere(o,d,c,r){let a=c.map((v,i)=>v-o[i]),t=a.reduce((s,v,i)=>s+v*d[i],0);if(t<0)return Infinity;let dist=a.reduce((s,v)=>s+v*v,0)-t*t;if(dist>r*r)return Infinity;return t-Math.sqrt(r*r-dist)}
function obstruct(o,d,t){for(let s of solids){let mn=[s.x-s.w,0,s.z-s.d],mx=[s.x+s.w,s.h||3.6,s.z+s.d],lo=0,hi=t;for(let k=0;k<3;k++){if(Math.abs(d[k])<.00001){if(o[k]<mn[k]||o[k]>mx[k]){hi=-1;break}}else{let a=(mn[k]-o[k])/d[k],b=(mx[k]-o[k])/d[k];lo=Math.max(lo,Math.min(a,b));hi=Math.min(hi,Math.max(a,b))}}if(hi>=lo&&lo<t&&lo>.1)return true}return false}
function shoot(){if(paused||shotTimer>0||reloading||!(state==='wave'||state==='break'))return;let g=guns[gun];shotTimer=g.rate;if(g.mag<=0){tone(180,.045,.08,'square',100);notify('EMPTY — PRESS R');return}g.mag--;recoil=gun===1?.28:gun===2?.09:.16;noise(gun===1?.32:.13,gun===1?.55:.3,gun===2?2400:1300);tone(gun===1?85:gun===2?150:210,.12,.22,'triangle',35);let o=[player.x,player.y,player.z];for(let pellet=0;pellet<(gun===1?9:1);pellet++){let yaw=player.yaw+(rand()-.5)*g.spread,pitch=player.pitch+(rand()-.5)*g.spread,d=[Math.sin(yaw)*Math.cos(pitch),Math.sin(pitch),-Math.cos(yaw)*Math.cos(pitch)];let target=null,best=60,head=false;for(let z of zombies){let ht=raySphere(o,d,[z.x,z.y+z.h-.19,z.z],z.type==='brute'?.32:.25),bt=raySphere(o,d,[z.x,z.y+z.h*.49,z.z],z.type==='brute'?.65:.47);let t=Math.min(ht,bt);if(t<best&&!obstruct(o,d,t)){best=t;target=z;head=ht<=bt}}if(target){target.hp-=g.damage*(head?2.8:1);target.hit=.35;target.x+=d[0]*.18;target.z+=d[2]*.18;hit=.2;$('hit').style.color=head?'#ffc875':'#fff';tone(head?160:85,.09,.09,'triangle',40);noise(.08,.1,400);for(let k=0;k<5;k++)particles.push({x:target.x,y:target.y+(head?target.h-.2:target.h*.6),z:target.z,vx:(rand()-.5)*3,vy:rand()*2,vz:(rand()-.5)*3,life:.45,c:head?[.85,.38,.15]:[.5,.06,.07]});if(target.hp<=0){kills++;score+=head?150:100;corpses.push({...target,dead:0});zombies.splice(zombies.indexOf(target),1)}}}}
function finish(win){state=win?'win':'dead';paused=true;firing=false;document.exitPointerLock?.();$('overlay').style.display='flex';$('title').innerHTML=win?'YOU MADE<br>IT TO <em>DAWN.</em>':'THE LAST<br><em>LIGHT GOES OUT.</em>';$('tag').textContent=win?'6:00 A.M. · THE STARLITE IS STILL STANDING.':'WAVE '+wave+' · THE NIGHT TAKES ANOTHER.';$('desc').textContent=kills+' kills · '+score+' points. '+(win?'Somewhere on the highway, an engine turns over. You are not alone.':'The payphone will ring for someone else.');$('start').textContent='TRY ANOTHER NIGHT →'}
function update(dt){gameTime+=dt;shotTimer=Math.max(0,shotTimer-dt);recoil=Math.max(0,recoil-dt*1.1);hurt=Math.max(0,hurt-dt*.7);hit=Math.max(0,hit-dt);bannerTime=Math.max(0,bannerTime-dt);noticeTime=Math.max(0,noticeTime-dt);if(reloading>0){reloading-=dt;if(reloading<=0){let g=guns[gun],n=Math.min(g.cap-g.mag,g.reserve);g.mag+=n;g.reserve-=n;tone(700,.07,.05,'square',220)}}if(firing)shoot();let dx=(keys.KeyD?1:0)-(keys.KeyA?1:0),dz=(keys.KeyW?1:0)-(keys.KeyS?1:0),len=Math.hypot(dx,dz),speed=keys.ShiftLeft||keys.ShiftRight?5.1:3.15;if(len){dx/=len;dz/=len;movePlayer((Math.cos(player.yaw)*dx+Math.sin(player.yaw)*dz)*speed*dt,(Math.sin(player.yaw)*dx-Math.cos(player.yaw)*dz)*speed*dt);walk+=dt*speed*2.2}player.y+=(floorAt(player.x,player.z)+1.7-player.y)*Math.min(1,dt*12);
flowTimer-=dt;if(flowTimer<0){computeFlow();flowTimer=.45}if(state==='wave'){spawnClock-=dt;if(pending>0&&spawnClock<=0){spawn();spawnClock=Math.max(.25,1.7-wave*.115)}if(!pending&&!zombies.length){if(wave===10){finish(true);return}state='break';breakTime=18;supplies();player.hp=Math.min(100,player.hp+15);announce('LOT CLEAR — CATCH YOUR BREATH');notify('SUPPLIES RESTOCKED · +15 HEALTH')}}else if(state==='break'){breakTime-=dt;ringTimer-=dt;if(ringTimer<0){let volume=.05+.08/(1+Math.hypot(player.x+21.4,player.z+6)*.15);tone(880,.22,volume,'sine',880);setTimeout(()=>{if(state==='break'&&!paused)tone(1100,.25,volume,'sine',1100)},270);ringTimer=2.6}if(breakTime<=0)startWave()}
for(let z of zombies){z.age+=dt;z.hit=Math.max(0,z.hit-dt);z.attack=Math.max(0,z.attack-dt);let dist=Math.hypot(z.x-player.x,z.z-player.z),tx=player.x,tz=player.z;if(floorAt(player.x,player.z)<-.4&&floorAt(z.x,z.z)>=-.4){tx=7;tz=9}let c=cell(z.x,z.z);if(dist>1.4&&floorAt(z.x,z.z)>=-.4&&(flow[c]>0||blocked((z.x+tx)/2,(z.z+tz)/2,.35))){let ix=c%gw,best=c;for(let n of [ix>0?c-1:-1,ix<gw-1?c+1:-1,c-gw,c+gw])if(n>=0&&n<flow.length&&flow[n]>=0&&(flow[best]<0||flow[n]<flow[best]))best=n;if(best!==c){tx=best%gw-31;tz=Math.floor(best/gw)-25}}let a=Math.atan2(tx-z.x,tz-z.z);z.angle=a;let sp=z.speed*dt*(z.hit>0?.25:1);if(dist>.85){let nx=z.x+Math.sin(a)*sp,nz=z.z+Math.cos(a)*sp;if(!blocked(nx,z.z,.35))z.x=nx;if(!blocked(z.x,nz,.35))z.z=nz}z.y+=(floorAt(z.x,z.z)-z.y)*Math.min(1,dt*10);if(dist<1.3&&Math.abs(player.y-(z.y+1.7))<1.4&&z.attack<=0){player.hp-=z.type==='brute'?22:z.type==='runner'?8:11;hurt=1;z.attack=z.type==='runner'?.8:1.25;noise(.2,.17,380);tone(70,.2,.13,'sawtooth',30);if(player.hp<=0){player.hp=0;finish(false);return}}}
groanTimer-=dt;if(groanTimer<0&&zombies.length){let z=zombies.reduce((a,b)=>Math.hypot(a.x-player.x,a.z-player.z)<Math.hypot(b.x-player.x,b.z-player.z)?a:b),v=.12/(1+Math.hypot(z.x-player.x,z.z-player.z)*.3);tone(z.type==='runner'?230: z.type==='brute'?45:85,.6,v,'sawtooth',z.type==='runner'?70:35);if(z.type==='brute')noise(.2,v*2,140);groanTimer=1.5+rand()*1.5}
for(let p of pickups){if(Math.hypot(player.x-p.x,player.z-p.z)<1.15){if(p.kind==='weapon'){guns[p.weapon].owned=true;gun=p.weapon;notify(guns[gun].name+' ACQUIRED')}else if(p.kind==='health'){if(player.hp>=100)continue;player.hp=Math.min(100,player.hp+35);notify('+35 HEALTH')}else{guns[1].reserve+=12;guns[2].reserve+=36;notify('AMMUNITION COLLECTED')}tone(620,.15,.06,'sine',900);pickups.splice(pickups.indexOf(p),1)}}for(let c of corpses)c.dead+=dt;corpses=corpses.filter(c=>c.dead<24);for(let p of particles){p.life-=dt;p.x+=p.vx*dt;p.y+=p.vy*dt;p.z+=p.vz*dt;p.vy-=7*dt}particles=particles.filter(p=>p.life>0);
}
function mul(a,b){let r=new Float32Array(16);for(let i=0;i<4;i++)for(let j=0;j<4;j++)for(let k=0;k<4;k++)r[i+j*4]+=a[i+k*4]*b[k+j*4];return r}
function camera(){let yaw=player.yaw,p=player.pitch+(hurt>0?Math.sin(gameTime*45)*hurt*.008:0),sy=Math.sin(yaw),cy=Math.cos(yaw),sp=Math.sin(p),cp=Math.cos(p),eye=[player.x,player.y+(Math.sin(walk)*.025),player.z];let right=[cy,0,sy],up=[-sy*sp,cp,cy*sp],back=[-sy*cp,-sp,cy*cp],m=new Float32Array([right[0],up[0],back[0],0,right[1],up[1],back[1],0,right[2],up[2],back[2],0,-right.reduce((s,v,i)=>s+v*eye[i],0),-up.reduce((s,v,i)=>s+v*eye[i],0),-back.reduce((s,v,i)=>s+v*eye[i],0),1]);let f=1/Math.tan(.57),aspect=canvas.width/canvas.height,n=.06,far=120,pr=new Float32Array([f/aspect,0,0,0,0,f,0,0,0,0,(far+n)/(n-far),-1,0,0,2*far*n/(n-far),0]);return {vp:mul(pr,m),eye}}
function zombieMesh(z,dead=false){let brute=z.type==='brute',runner=z.type==='runner',h=z.h,w=brute?.85:.5,a=z.angle||0,col=brute?[.38,.24,.18]:runner?[.43,.23,.22]:[.22,.34,.28],skin=brute?[.48,.42,.3]:[.45,.51,.36],swing=Math.sin(z.age* (runner?13:5)+z.phase)*.24;if(z.hit>0)skin=[.8,.49,.35];if(dead){let fall=Math.min(1,z.dead*2.8);box(z.x,z.y,z.z,w,h*(1-fall)+.18,h*fall+.3,color(col,.65),a);box(z.x+Math.sin(a)*h*.48*fall,z.y+.16,z.z+Math.cos(a)*h*.48*fall,.38,.3,.38,skin);return}box(z.x,z.y+h*.38,z.z,w,h*.43,brute?.6:.35,col,a);box(z.x,z.y+h-.4,z.z,.38*(brute?1.5:1),.43*(brute?1.3:1),.38,skin,a);box(z.x,z.y+h*.76,z.z,.22,.15,.22,skin);for(let side of [-1,1]){let lx=z.x+Math.cos(a)*side*w*.28,lz=z.z-Math.sin(a)*side*w*.28;box(lx,z.y+.05,lz+side*swing,.19*(brute?1.4:1),h*.38,.23,[.14,.19,.18],a);let ax=z.x+Math.cos(a)*side*w*.65,az=z.z-Math.sin(a)*side*w*.65;box(ax,z.y+h*.43,az+Math.cos(a)*.2,.16*(brute?1.8:1),h*.31,.22,col,a);box(ax,z.y+h*.43,az+Math.cos(a)*.3,.17,.16,.2,skin,a)}for(let side of [-1,1])box(z.x+Math.cos(a)*side*.095+Math.sin(a)*.2,z.y+h-.16,z.z-Math.sin(a)*side*.095+Math.cos(a)*.2,.06,.04,.055,runner?[1,.3,.12]:[.74,.75,.35],a)}
function render(){let dawn=state==='win'?1:Math.max(0,(wave-1)/10),fog=[.018+dawn*.17,.037+dawn*.2,.045+dawn*.24];gl.clearColor(...fog,1);gl.clear(gl.COLOR_BUFFER_BIT|gl.DEPTH_BUFFER_BIT);let c=camera();gl.uniformMatrix4fv(uniforms.vp,false,c.vp);gl.uniform3fv(uniforms.eye,c.eye);gl.uniform3fv(uniforms.fog,fog);gl.uniform1f(uniforms.dawn,dawn);gl.bufferData(gl.ARRAY_BUFFER,world,gl.STATIC_DRAW);gl.drawArrays(gl.TRIANGLES,0,world.length/9);verts=[];
// Flickering tube sign: L is dead, T buzzes intermittently.
let label='STARLITE';for(let i=0;i<label.length;i++)text(label[i],-10.5+i*1.55,6.8,23.2,.34,i===4?[.15,.2,.18]:(i===5&&Math.sin(gameTime*32)>.5)?[.2,.2,.17]:C.pink);
// Office TV static, shimmering illumination and moths
let tv= .45+Math.sin(gameTime*31)*.2;box(-30.3,1.06,-14.81,.78,.44,.01,[tv,tv+.1,tv+.13]);for(let i=0;i<16;i++)box(-30.63+(i%4)*.17,1.1+Math.floor(i/4)*.09,-14.79,.16,.045,.01,[rand()*.8,rand()*.9,rand()*.8]);box(-29.9,1.12,-13.7,.5,.015,.8,[tv*.6,tv*.7,tv*.8]);for(let i=0;i<8;i++)box(-3+Math.sin(gameTime*3+i)*.3,2.8+Math.cos(gameTime*2+i)*.25,-14+Math.cos(gameTime*4+i)*.25,.035,.025,.03,C.white);
for(let z of zombies)zombieMesh(z);for(let z of corpses)zombieMesh(z,true);for(let p of particles)box(p.x,p.y,p.z,.07,.07,.07,p.c);
for(let p of pickups){let y=floorAt(p.x,p.z)+.15+Math.sin(gameTime*2)*.05,col=p.kind==='health'?[.73,.79,.65]:p.kind==='weapon'?[.66,.52,.23]:[.3,.46,.26];box(p.x,y,p.z,p.kind==='weapon'?1:.6,.3,.45,col);if(p.kind==='health'){box(p.x,y+.31,p.z,.12,.02,.35,[.75,.14,.14]);box(p.x,y+.31,p.z,.35,.02,.12,[.75,.14,.14])}else if(p.kind==='weapon'){box(p.x,y+.32,p.z,1.1,.13,.15,[.1,.14,.14]);box(p.x-.35,y+.2,p.z,.2,.12,.18,[.3,.2,.11])}else for(let i=0;i<3;i++)box(p.x-.17+i*.17,y+.31,p.z,.06,.08,.25,C.orange)}
// phone light during intermission
if(state==='break'&&Math.sin(gameTime*8)>0)box(-21.4,2.27,-5.74,.3,.055,.02,C.orange);
gl.bufferData(gl.ARRAY_BUFFER,new Float32Array(verts),gl.DYNAMIC_DRAW);gl.drawArrays(gl.TRIANGLES,0,verts.length/9);drawGun();hud()}
function drawGun(){let W=wc.width,H=wc.height;ctx.clearRect(0,0,W,H);if(state==='title')return;ctx.save();let s=H/720;ctx.translate(W*.61+Math.sin(walk)*7*s,H+Math.cos(walk*2)*5*s);ctx.scale(s,s);let reloadShift=reloading>0?Math.sin(Math.PI*Math.min(1,reloading/guns[gun].reload))*180:0;ctx.translate(recoil*80,recoil*210+reloadShift);ctx.rotate(-recoil*.3+(reloading>0?-.35:0));
// gloved forearms, detailed steel receiver and barrel
ctx.fillStyle='#27362e';ctx.beginPath();ctx.moveTo(-15,-110);ctx.lineTo(75,-130);ctx.lineTo(190,40);ctx.lineTo(25,40);ctx.fill();ctx.fillStyle='#576255';ctx.beginPath();ctx.moveTo(5,-100);ctx.lineTo(18,-163);ctx.lineTo(54,-161);ctx.lineTo(81,-95);ctx.fill();let grad=ctx.createLinearGradient(-65,-260,75,-110);grad.addColorStop(0,'#708078');grad.addColorStop(.3,'#243632');grad.addColorStop(1,'#101a1a');ctx.fillStyle=grad;
let tipX=-39,tipY=gun===0?-257:gun===1?-315:-306;
if(gun===0){ctx.beginPath();ctx.moveTo(-69,-252);ctx.lineTo(-25,-280);ctx.lineTo(52,-176);ctx.lineTo(44,-125);ctx.lineTo(6,-118);ctx.lineTo(-13,-192);ctx.lineTo(-58,-205);ctx.fill();ctx.fillStyle='#82948b';ctx.beginPath();ctx.moveTo(-69,-252);ctx.lineTo(-25,-280);ctx.lineTo(-20,-265);ctx.lineTo(-61,-238);ctx.fill();ctx.fillStyle='#111e1c';ctx.fillRect(10,-181,35,66);ctx.fillStyle='#78897e';for(let i=0;i<6;i++)ctx.fillRect(9,-177+i*5,31,1);ctx.fillStyle='#c9cab1';ctx.fillRect(-30,-279,5,5)}else{ctx.beginPath();ctx.moveTo(-53,tipY);ctx.lineTo(-23,tipY-12);ctx.lineTo(39,-210);ctx.lineTo(75,-114);ctx.lineTo(33,-91);ctx.lineTo(-12,-190);ctx.fill();ctx.fillStyle=gun===1?'#5f4430':'#16231f';ctx.beginPath();ctx.moveTo(-27,-244);ctx.lineTo(7,-252);ctx.lineTo(40,-188);ctx.lineTo(5,-165);ctx.fill();ctx.fillStyle='#7b8b7e';ctx.fillRect(-43,tipY-8,14,5);if(gun===2){ctx.fillStyle='#0c1516';ctx.fillRect(13,-175,27,75);ctx.strokeStyle='#637369';ctx.lineWidth=4;ctx.strokeRect(-22,-290,25,18)}for(let i=0;i<7;i++){ctx.strokeStyle='#111d1b';ctx.beginPath();ctx.moveTo(-10+i*4,-232+i*7);ctx.lineTo(10+i*4,-240+i*7);ctx.stroke()}}
if(recoil> (gun===1?.2:gun===0?.1:.045)&&!reloading){ctx.save();ctx.translate(tipX,tipY);ctx.fillStyle=gun===2?'#fff4ae':'#ffe1a1';ctx.shadowColor='#ffb43f';ctx.shadowBlur=40;ctx.beginPath();for(let i=0;i<12;i++){let a=i*Math.PI/6,r=i%2?14:45+Math.random()*30;ctx.lineTo(Math.cos(a)*r,Math.sin(a)*r)}ctx.closePath();ctx.fill();ctx.restore()}ctx.restore()}
function hud(){let g=guns[gun];$('wave').textContent='WAVE '+String(wave).padStart(2,'0')+' / 10';$('remaining').textContent=state==='break'?'NEXT WAVE IN '+Math.ceil(breakTime)+'s · PHONE RINGING':(zombies.length+pending)+' HOSTILES REMAIN';$('health').textContent=Math.ceil(player.hp);$('healthbar').style.width=player.hp+'%';$('mag').textContent=g.mag;$('reserve').textContent=g.reserve===Infinity?'∞':g.reserve;$('gunname').textContent=g.name;$('reloadtext').textContent=reloading>0?'RELOADING…':gun===0?'9 MM · SEMI AUTO':gun===1?'12 GA · PUMP ACTION':'5.56 MM · AUTOMATIC';$('kills').textContent=kills+' KILLS · '+score+' POINTS';let minutes=190+Math.round((wave-1)/9*164);$('clock').textContent=state==='win'?'06:00 AM':String(Math.floor(minutes/60)).padStart(2,'0')+':'+String(minutes%60).padStart(2,'0')+' AM';$('hurt').style.opacity=hurt*.7;$('hit').style.opacity=hit>0?1:0;$('banner').style.opacity=Math.min(1,bannerTime);if(noticeTime<=0)$('notice').textContent=''}
function resize(){canvas.width=wc.width=innerWidth*Math.min(devicePixelRatio,1.5);canvas.height=wc.height=innerHeight*Math.min(devicePixelRatio,1.5);gl.viewport(0,0,canvas.width,canvas.height)}window.addEventListener('resize',resize);resize();
function loop(t){let dt=Math.min(.04,(t-last)/1000||.016);last=t;if(!paused&&(state==='wave'||state==='break'))update(dt);if(state==='title'){player.x=3;player.z=15;player.y=1.8;player.yaw=-.2+Math.sin(t*.00008)*.15;player.pitch=.02;gameTime=t/1000}render();requestAnimationFrame(loop)}requestAnimationFrame(loop);
</script></body></html>
<!-- agent-meta {"model":"openai/gpt-6.1-sol","provider":"openrouter","persona":"full","sessionId":"9f2bac9d-8673-4356-97d9-05c803caef9c","tokensIn":364168,"tokensOut":19093,"tokensTotal":383261,"cost":0.30267950000000005,"turns":15,"toolCalls":14,"failedToolCalls":1,"timestamp":"2026-09-29T19:51:28.256Z"} -->