← Zombie FPS results

Zombie FPS

Swift Qwen3.8 27B GGUF · 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 56,340 bytes · SHA-256 a0b128ef6220
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>LAST CALL AT THE STARLITE</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
html,body{width:100%;height:100%;overflow:hidden;background:#000;font-family:'Courier New',Courier,monospace;color:#cfd6dd;user-select:none;cursor:crosshair}
#gl{position:fixed;inset:0;width:100vw;height:100vh;display:block}
#hud{position:fixed;inset:0;pointer-events:none;z-index:5;font-size:14px;letter-spacing:1px}
#topL{position:absolute;top:14px;left:18px}
#wave{font-size:22px;color:#ffd98a;text-shadow:0 0 8px rgba(255,180,60,.5)}
#zleft{color:#9fb4c8;font-size:13px}
#clock{position:absolute;top:14px;left:50%;transform:translateX(-50%);font-size:16px;color:#8fa8c0;text-shadow:0 0 6px rgba(120,160,220,.4);letter-spacing:2px}
#topR{position:absolute;top:14px;right:18px;text-align:right}
#topR div{font-size:13px;color:#9fb4c8}
#botL{position:absolute;bottom:18px;left:18px;display:flex;align-items:center;gap:10px}
#hpbar{width:220px;height:14px;border:1px solid #3a4a5a;background:rgba(10,14,18,.6)}
#hpfill{height:100%;width:100%;background:linear-gradient(90deg,#7a1a1a,#d43a2a);transition:width .15s}
#hptxt{font-size:18px;color:#e8f2ff}
#botR{position:absolute;bottom:18px;right:18px;text-align:right}
#wname{font-size:13px;color:#9fb4c8;letter-spacing:2px}
#ammo{font-size:34px;color:#e8f2ff;text-shadow:0 0 8px rgba(140,190,255,.5)}
#resv{font-size:15px;color:#8fa8c0}
#slots{position:absolute;bottom:18px;left:50%;transform:translateX(-50%);display:flex;gap:8px}
.slot{padding:4px 10px;border:1px solid #2a3a4a;background:rgba(8,12,16,.55);font-size:12px;color:#5a6a7a}
.slot.cur{border-color:#ffd98a;color:#ffd98a;box-shadow:0 0 8px rgba(255,180,60,.3)}
.slot.lock{opacity:.45}
.slot b{margin-right:4px}
#msg{position:absolute;bottom:120px;left:50%;transform:translateX(-50%);font-size:16px;color:#ffd98a;text-shadow:0 0 8px rgba(255,160,40,.6);opacity:0;transition:opacity .3s;white-space:nowrap}
#crosshair{position:fixed;left:50%;top:50%;z-index:6;pointer-events:none;width:0;height:0}
#crosshair i{position:absolute;background:#dff4ff;box-shadow:0 0 4px rgba(150,220,255,.8)}
.cl{width:2px;height:9px;left:-1px;top:-16px}
.cr{width:2px;height:9px;left:-1px;top:7px}
.ct{width:9px;height:2px;left:-16px;top:-1px}
.cb{width:9px;height:2px;left:7px;top:-1px}
#hitmark{position:fixed;left:50%;top:50%;z-index:6;pointer-events:none;width:22px;height:22px;transform:translate(-50%,-50%);opacity:0}
#hitmark::before,#hitmark::after{content:'';position:absolute;background:#fff;left:9px;top:0;width:3px;height:22px}
#hitmark::before{transform:rotate(45deg)}
#hitmark::after{transform:rotate(-45deg)}
#hitmark.hs::before,#hitmark.hs::after{background:#ff5544}
#vignette{position:fixed;inset:0;z-index:8;pointer-events:none;opacity:0;background:radial-gradient(ellipse at center,rgba(0,0,0,0) 45%,rgba(160,10,5,.65) 100%)}
#flash{position:fixed;inset:0;z-index:7;pointer-events:none;opacity:0;background:rgba(255,240,210,.5)}
#banner{position:fixed;top:20%;left:0;right:0;z-index:9;text-align:center;pointer-events:none;opacity:0}
#banner.show{animation:ban 2.6s ease-out forwards}
@keyframes ban{0%{opacity:0;transform:scale(1.6)}12%{opacity:1;transform:scale(1)}80%{opacity:1}100%{opacity:0;transform:scale(.96)}}
#btitle{font-size:52px;letter-spacing:10px;color:#ffd98a;text-shadow:0 0 18px rgba(255,150,40,.8),0 0 40px rgba(255,100,20,.4)}
#bsub{font-size:18px;letter-spacing:4px;color:#c8d4e0;margin-top:6px}
#cd{position:fixed;bottom:16%;left:50%;transform:translateX(-50%);z-index:9;pointer-events:none;font-size:26px;color:#9fd4ff;text-shadow:0 0 10px rgba(80,160,255,.6);letter-spacing:3px}
.screen{position:fixed;inset:0;z-index:20;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;background:radial-gradient(ellipse at 50% 40%,rgba(8,12,18,.78),rgba(2,3,6,.94))}
.hidden{display:none}
.t1{font-size:56px;letter-spacing:12px;color:#e8f2ff;text-shadow:0 0 18px rgba(150,200,255,.5)}
.t2{font-size:44px;letter-spacing:10px;margin-top:8px}
.neon{color:#ff5fa0;text-shadow:0 0 10px #ff2f7f,0 0 34px rgba(255,47,127,.6)}
.t3{margin-top:22px;font-size:15px;letter-spacing:2px;color:#9fb4c8}
.t4{margin-top:8px;font-size:13px;letter-spacing:2px;color:#6a7a8a}
.ctrls{margin-top:30px;display:flex;flex-wrap:wrap;gap:8px 18px;justify-content:center;max-width:660px;font-size:12px;color:#7a8a9a;letter-spacing:1px}
.ctrls span{border:1px solid #2a3a4a;padding:5px 9px;background:rgba(10,14,20,.5)}
.start{margin-top:38px;font-size:20px;letter-spacing:4px}
.neonflick{animation:nf 2.2s infinite}
@keyframes nf{0%,100%{opacity:1}92%{opacity:1}93%{opacity:.3}94%{opacity:1}96%{opacity:.5}97%{opacity:1}}
button{margin-top:30px;font-family:inherit;font-size:16px;letter-spacing:3px;padding:12px 34px;background:transparent;color:#ffd98a;border:1px solid #ffd98a;cursor:pointer}
button:hover{background:rgba(255,200,80,.12);box-shadow:0 0 14px rgba(255,180,60,.4)}
.sunrise{position:absolute;inset:0;background:linear-gradient(180deg,#2a3550 0%,#4a5a78 45%,#c88a5a 78%,#e8a86a 100%);opacity:.5}
.sun{position:absolute;left:50%;bottom:16%;width:160px;height:160px;margin-left:-80px;border-radius:50%;background:radial-gradient(circle,#fff2d0 0%,#ffcf8a 40%,rgba(255,170,90,0) 70%);animation:sunup 4s ease-out forwards}
@keyframes sunup{from{transform:translateY(130px)}to{transform:translateY(0)}}
</style>
</head>
<body>
<canvas id="gl"></canvas>
<div id="hud">
  <div id="topL"><div id="wave">WAVE —</div><div id="zleft">DEAD: 0</div></div>
  <div id="clock">3:10 A.M.</div>
  <div id="topR"><div id="kills">KILLS 0</div><div id="score">SCORE 0</div></div>
  <div id="botL"><div id="hpbar"><div id="hpfill"></div></div><div id="hptxt">100</div></div>
  <div id="botR"><div id="wname">PISTOL</div><div id="ammo">12</div><div id="resv">&infin;</div></div>
  <div id="slots">
    <div class="slot" id="slot0"><b>1</b>PISTOL</div>
    <div class="slot" id="slot1"><b>2</b>SHOTGUN</div>
    <div class="slot" id="slot2"><b>3</b>RIFLE</div>
  </div>
  <div id="msg"></div>
</div>
<div id="crosshair"><i class="cl"></i><i class="cr"></i><i class="ct"></i><i class="cb"></i></div>
<div id="hitmark"></div>
<div id="vignette"></div>
<div id="flash"></div>
<div id="banner"><div id="btitle"></div><div id="bsub"></div></div>
<div id="cd"></div>
<div id="title" class="screen">
  <div class="t1">LAST CALL</div>
  <div class="t2 neon">AT THE STARLITE</div>
  <div class="t3">3:10 A.M. &mdash; THE DEAD ARE COMING OUT OF THE FOG</div>
  <div class="t4">Survive 10 waves. Dawn at 6 A.M. The payphone rings between waves. It knows.</div>
  <div class="ctrls">
    <span>MOUSE aim</span><span>WASD move</span><span>SHIFT sprint</span><span>SPACE jump</span>
    <span>LMB fire</span><span>R reload</span><span>1/2/3 + WHEEL weapons</span><span>ESC pause</span>
  </div>
  <div class="start neonflick">&mdash; CLICK TO START &mdash;</div>
</div>
<div id="pauseScr" class="screen hidden">
  <div class="t2">PAUSED</div>
  <div class="t3">The fog waits. The dead do not.</div>
  <button id="resumeBtn">RESUME</button>
</div>
<div id="overScr" class="screen hidden">
  <div class="t1" style="color:#ff4444;text-shadow:0 0 14px #f00">YOU DIDN'T MAKE IT</div>
  <div class="t3" id="overStats"></div>
  <button id="restartBtn">TRY AGAIN</div>
</div>
<div id="winScr" class="screen hidden">
  <div class="sunrise"><div class="sun"></div></div>
  <div class="t1" style="color:#ffd98a;text-shadow:0 0 16px #ff9a3a">DAWN BREAKS</div>
  <div class="t3" id="winStats"></div>
  <button id="winBtn">PLAY AGAIN</button>
</div>
<script>
'use strict';
/* ============================== MATH ============================== */
const M = new Float32Array(16);
function trs(o,tx,ty,tz,ry,rx,sx,sy,sz){
  const cyw=Math.cos(ry),syw=Math.sin(ry),cxw=Math.cos(rx),sxw=Math.sin(rx);
  o[0]=cyw*sxw;o[1]=0;o[2]=-syw*sxw;o[3]=0;
  o[4]=sxw*syw*syw;o[5]=cxw*syw;o[6]=sxw*cxw*syw;o[7]=0;
  o[8]=cxw*syw*sz;o[9]=-sxw*sz;o[10]=cxw*cxw*sz;o[11]=0;
  o[12]=tx;o[13]=ty;o[14]=tz;o[15]=1;
  return o;
}
function m4mul(o,a,b){
  for(let c=0;c<4;c++){
    const b0=b[c*4],b1=b[c*4+1],b2=b[c*4+2],b3=b[c*4+3];
    o[c*4]  =a[0]*b0+a[4]*b1+a[8]*b2+a[12]*b3;
    o[c*4+1]=a[1]*b0+a[5]*b1+a[9]*b2+a[13]*b3;
    o[c*4+2]=a[2]*b0+a[6]*b1+a[10]*b2+a[14]*b3;
    o[c*4+3]=a[3]*b0+a[7]*b1+a[11]*b2+a[15]*b3;
  }
  return o;
}
function persp(o,fov,asp,n,f){
  const t=1/Math.tan(fov/2);
  o[0]=t/asp;o[1]=0;o[2]=0;o[3]=0;
  o[4]=0;o[5]=t;o[6]=0;o[7]=0;
  o[8]=0;o[9]=0;o[10]=(f+n)/(n-f);o[11]=-1;
  o[12]=0;o[13]=0;o[14]=2*f*n/(n-f);o[15]=0;
  return o;
}
function viewM_(o,ex,ey,ez,yaw,pitch){
  const cy=Math.cos(yaw),sy=Math.sin(yaw),cp=Math.cos(pitch),sp=Math.sin(pitch);
  o[0]=cy;o[1]=0;o[2]=-sy;o[3]=0;
  o[4]=sp*sy;o[5]=cp;o[6]=sp*cy;o[7]=0;
  o[8]=cp*sy;o[9]=0;o[10]=cp*cy;o[11]=0;
  o[12]=-(cy*ex-sy*ez);
  o[13]=-(sp*sy*ex+cp*ey+sp*cy*ez);
  o[14]=-(cp*sy*ex+cp*cy*ez);
  o[15]=1;
  return o;
}
function rayBox(ox,oy,oz,dx,dy,dz,bx,by,bz,hx,hy,hz){
  let tmin=0,tmax=1e9;
  if(Math.abs(dx)<1e-8){ if(ox<bx-hx||ox>bx+hx) return -1; }
  else{ let t1=(bx-hx-ox)/dx,t2=(bx+hx-ox)/dx; if(t1>t2){const q=t1;t1=t2;t2=q;} if(t1>tmin)tmin=t1; if(t2<tmax)tmax=t2; if(tmin>tmax) return -1; }
  if(Math.abs(dy)<1e-8){ if(oy<by-hy||oy>by+hy) return -1; }
  else{ let t1=(by-hy-oy)/dy,t2=(by+hy-oy)/dy; if(t1>t2){const q=t1;t1=t2;t2=q;} if(t1>tmin)tmin=t1; if(t2<tmax)tmax=t2; if(tmin>tmax) return -1; }
  if(Math.abs(dz)<1e-8){ if(oz<bz-hz||oz>bz+hz) return -1; }
  else{ let t1=(bz-hz-oz)/dz,t2=(bz+hz-oz)/dz; if(t1>t2){const q=t1;t1=t2;t2=q;} if(t1>tmin)tmin=t1; if(t2<tmax)tmax=t2; if(tmin>tmax) return -1; }
  return tmin;
}
/* ============================== GL SETUP ============================== */
const cv=document.getElementById('gl');
const gl=cv.getContext('webgl',{antialias:true,alpha:false});
let W=1,H=1,aspect=1;
function resize(){
  const dpr=Math.min(1.5,window.devicePixelRatio||1);
  cv.width=Math.floor(innerWidth*dpr); cv.height=Math.floor(innerHeight*dpr);
  W=cv.width; H=cv.height; aspect=W/H;
  gl.viewport(0,0,W,H);
}
addEventListener('resize',resize); resize();
function sh(type,src){const s=gl.createShader(type);gl.shaderSource(s,src);gl.compileShader(s);
  if(!gl.getShaderParameter(s,gl.COMPILE_STATUS))throw new Error(gl.getShaderInfoLog(s));return s;}
function prog(vs,fs){const p=gl.createProgram();gl.attachShader(p,sh(gl.VERTEX_SHADER,vs));gl.attachShader(p,sh(gl.FRAGMENT_SHADER,fs));gl.linkProgram(p);
  if(!gl.getProgramParameter(p,gl.LINK_STATUS))throw new Error(gl.getProgramInfoLog(p));return p;}
/* ---- world shader ---- */
const progW=prog(
`attribute vec3 aP;attribute vec3 aN;
uniform mat4 uVP,uModel;
varying vec3 vN,vW;
void main(){vec4 w=uModel*vec4(aP,1.0);vW=w.xyz;vN=mat3(uModel)*aN;gl_Position=uVP*w;}`,
`precision mediump float;
varying vec3 vN,vW;
uniform vec3 uColor,uEm,uCam,uSunDir,uSunCol,uAmb,uFogCol;
uniform vec3 uLP[6];uniform vec3 uLC[6];
uniform float uFogD;
void main(){
  vec3 N=normalize(vN);
  float sd=max(dot(N,-uSunDir),0.0);
  vec3 c=uColor*(uAmb+uSunCol*sd);
  for(int i=0;i<6;i++){
    vec3 L=uLP[i]-vW;
    float d=length(L);
    float att=1.0/(1.0+0.12*d+0.05*d*d);
    c+=uColor*uLC[i]*max(dot(N,L/d),0.0)*att;
  }
  c+=uEm;
  float fd=length(vW-uCam);
  float f=1.0-exp(-fd*fd*uFogD*uFogD);
  c=mix(c,uFogCol,clamp(f,0.0,1.0));
  gl_FragColor=vec4(c,1.0);
}`);
const uW={};
['uVP','uModel','uColor','uEm','uCam','uSunDir','uSunCol','uAmb','uFogCol','uFogD','uLP','uLC'].forEach(n=>uW[n]=gl.getUniformLocation(progW,n));
gl.useProgram(progW);
const CUBE_P=[],CUBE_N=[],CUBE_I=[];
[ {n:[1,0,0],c:[[.5,-.5,-.5],[.5,.5,-.5],[.5,.5,.5],[.5,-.5,.5]]},
  {n:[-1,0,0],c:[[-.5,-.5,.5],[-.5,.5,.5],[-.5,.5,-.5],[-.5,-.5,-.5]]},
  {n:[0,1,0],c:[[-.5,.5,-.5],[-.5,.5,.5],[.5,.5,.5],[.5,.5,-.5]]},
  {n:[0,-1,0],c:[[-.5,-.5,-.5],[.5,-.5,-.5],[.5,-.5,.5],[-.5,-.5,.5]]},
  {n:[0,0,1],c:[[-.5,-.5,.5],[.5,-.5,.5],[.5,.5,.5],[-.5,.5,.5]]},
  {n:[0,0,-1],c:[[.5,-.5,-.5],[-.5,-.5,-.5],[-.5,.5,-.5],[.5,.5,-.5]]} ]
.forEach(f=>{const b=CUBE_P.length/3;f.c.forEach(c=>{CUBE_P.push(c[0],c[1],c[2]);CUBE_N.push(f.n[0],f.n[1],f.n[2]);});[0,1,2,0,2,3].forEach(i=>CUBE_I.push(b+i));});
function buf(data,type){const b=gl.createBuffer();gl.bindBuffer(type||gl.ARRAY_BUFFER,b);gl.bufferData(type||gl.ARRAY_BUFFER,data,gl.STATIC_DRAW);return b;}
buf(new Float32Array(CUBE_P)); const cubeVB=gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER,cubeVB);
gl.bufferData(gl.ARRAY_BUFFER,new Float32Array(CUBE_P),gl.STATIC_DRAW);
const cubeNB=gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER,cubeNB);
gl.bufferData(gl.ARRAY_BUFFER,new Float32Array(CUBE_N),gl.STATIC_DRAW);
const cubeIB=gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER,cubeIB);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER,new Uint16Array(CUBE_I),gl.STATIC_DRAW);
const aPloc=gl.getAttribLocation(progW,'aP');
const aNloc=gl.getAttribLocation(progW,'aN');
function bindCube(){
  gl.bindBuffer(gl.ARRAY_BUFFER,cubeVB);gl.vertexAttribPointer(aPloc,3,gl.FLOAT,false,0,0);
  gl.bindBuffer(gl.ARRAY_BUFFER,cubeNB);gl.vertexAttribPointer(aNloc,3,gl.FLOAT,false,0,0);
  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER,cubeIB);
}
gl.enableVertexAttribArray(aPloc); gl.enableVertexAttribArray(aNloc);
gl.disable(gl.CULL_FACE); gl.enable(gl.DEPTH_TEST);
function drawBox(x,y,z,sx,sy,sz,ry,rx,c,e){
  trs(M,x,y,z,ry||0,rx||0,sx,sy,sz);
  gl.uniformMatrix4fv(uW.uModel,false,M);
  gl.uniform3f(uW.uColor,c[0],c[1],c[2]);
  gl.uniform3f(uW.uEm,e?e[0]:0,e?e[1]:0,e?e[2]:0);
  gl.drawElements(gl.TRIANGLES,36,gl.UNSIGNED_SHORT,0);
}
/* ---- sky shader ---- */
const progS=prog(
`attribute vec2 aP;varying vec2 vUV;
void main(){vUV=aP*0.5+0.5;gl_Position=vec4(aP,0.9999,1.0);}`,
`precision mediump float;varying vec2 vUV;
uniform vec3 uTop,uHor;uniform float uDawn;uniform vec2 uSunUV;
float hash(vec2 p){return fract(sin(dot(p,vec2(127.1,311.7)))*43758.5453);}
void main(){
  vec3 c=mix(uHor,uTop,smoothstep(0.0,0.85,vUV.y));
  float st=step(0.9986,hash(floor(vUV*vec2(170.0,95.0))))*(1.0-uDawn)*0.6;
  c+=vec3(st);
  float sun=uDawn*uDawn*uDawn;
  float d=length(vUV-uSunUV);
  c+=vec3(1.0,0.55,0.3)*sun*exp(-d*d*16.0)*0.7;
  c+=vec3(1.0,0.82,0.55)*sun*exp(-d*d*80.0)*1.1;
  gl_FragColor=vec4(c,1.0);
}`);
const uS={uTop:gl.getUniformLocation(progS,'uTop'),uHor:gl.getUniformLocation(progS,'uHor'),uDawn:gl.getUniformLocation(progS,'uDawn'),uSunUV:gl.getUniformLocation(progS,'uSunUV')};
const skyBuf=gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER,skyBuf);
gl.bufferData(gl.ARRAY_BUFFER,new Float32Array([-1,-1,3,-1,-1,3]),gl.STATIC_DRAW);
const aSky=gl.getAttribLocation(progS,'aP');
/* ---- point particle shader ---- */
const progP=prog(
`attribute vec3 aP;attribute float aS;attribute vec3 aC;
uniform mat4 uVP;uniform float uPx;varying vec3 vC;
void main(){vec4 w=uVP*vec4(aP,1.0);gl_Position=w;gl_PointSize=max(1.0,aS*uPx/max(0.1,length(aP.xyz)));vC=aC;}`,
`precision mediump float;varying vec3 vC;
void main(){float d=length(gl_PointCoord-0.5);if(d>0.5)discard;gl_FragColor=vec4(vC,1.0-d);}`);
const uPt={uVP:gl.getUniformLocation(progP,'uVP'),uPx:gl.getUniformLocation(progP,'uPx'),aP:gl.getAttribLocation(progP,'aP'),aS:gl.getAttribLocation(progP,'aS'),aC:gl.getAttribLocation(progP,'aC')};
const pPosB=gl.createBuffer(),pSizeB=gl.createBuffer(),pColB=gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER,pPosB);
gl.bindBuffer(gl.ARRAY_BUFFER,pSizeB);
gl.bindBuffer(gl.ARRAY_BUFFER,pColB);
/* ============================== WORLD ============================== */
const PROPS=[],COLS=[],LIGHTS=[],SPAWNS=[],PICKUP_SPOTS=[];
function B(x,y,z,sx,sy,sz,ry,rx,c,e){PROPS.push([x,y,z,sx,sy,sz,ry||0,rx||0,c,e||null]);}
function C(x,y,z,hx,hy,hz){COLS.push([x,y,z,hx,hy,hz]);}
function L(x,y,z,c){}
const COL={
  asphalt:[.10,.10,.115],line:[.42,.40,.34],fence:[.16,.17,.19],tree:[.05,.08,.06],trunk:[.12,.09,.07],
  motel:[.42,.38,.33],motelDk:[.30,.27,.24],roof:[.16,.14,.13],door:[.35,.22,.14],door2:[.24,.30,.34],
  walk:[.20,.20,.22],office:[.52,.47,.40],desk:[.30,.20,.12],tv:[.05,.05,.06],
  diner:[.55,.28,.25],dinerWin:[.62,.55,.45],counter:[.35,.22,.13],stool:[.30,.15,.12],booth:[.45,.30,.20],
  pool:[.32,.36,.36],poolWall:[.40,.44,.44],leaf:[.14,.18,.09],chair:[.55,.50,.45],
  car:[.30,.10,.10],carIn:[.12,.10,.10],wheel:[.05,.05,.05],
  pole:[.22,.22,.24],lamp:[.5,.45,.35],phone:[.20,.22,.30],vend:[.16,.18,.24],ice:[.35,.40,.45],
  sign:[.10,.10,.12],tire:[.08,.08,.08],bin:[.14,.22,.16],suit:[.40,.20,.10],paper:[.75,.75,.72],
  lampOn:[1,.75,.35],tvOn:[.7,.9,1],vendOn:[.5,.75,1],greenOn:[.2,1,.4],numOn:[1,.85,.5],
  vacOn:[.1,.9,.35],neon:[1,.35,.65],whiteOn:[1,.95,.85],flash:[1,.95,.7]
};
function buildWorld(){
  /* ground: lot */
  B(0,-.1,0,70,.2,46,0,0,COL.asphalt);
  /* parking lines */
  for(let i=0;i<8;i++)B(-24+i*6.5,-.01,-14,0.18,.02,8,0,0,COL.line);
  /* highway */
  B(0,-.12,-26,120,.2,8,0,0,[.09,.09,.10]);
  B(0,-.02,-26,120,.02,.15,0,0,COL.line);
  B(0,-.02,-29.5,120,.02,.15,0,0,COL.line);
  /* treeline */
  for(let i=0;i<16;i++){const a=-0.15+i*0.13;const x=Math.sin(a)*44-10,z=-Math.cos(a)*40;
    B(x,1.6,z,1.1,3.2,1.1,0,0,COL.trunk);B(x,4.2,z,2.6,2.6,2.6,0.6,0,COL.tree);B(x,6.2,z,1.7,1.9,1.7,0.6,0,COL.tree);}
  for(let i=0;i<10;i++){B(-38+i*3.4,3.4,-14+((i*7)%9),2.8,2.9,2.8,0.6,0,COL.tree);B(-38+i*3.4,5.4,-14+((i*7)%9),1.8,1.8,1.8,0.6,0,COL.tree);}
  for(let i=0;i<10;i++){B(-38+i*3.4,3.4,14-((i*5)%9),2.8,2.9,2.8,0.6,0,COL.tree);B(-38+i*3.4,5.4,14-((i*5)%9),1.8,1.8,1.8,0.6,0,COL.tree);}
  /* chain-link fence: posts + rail */
  const fpts=[];
  for(let x=-34;x<=34;x+=5)fpts.push([x,-22]);
  for(let z=-22;z<=22;z+=5)fpts.push([34,z]);
  for(let x=34;x>=-34;x-=5)fpts.push([x,22]);
  for(let z=22;z>=-22;z-=5)fpts.push([-34,z]);
  fpts.forEach(p=>{B(p[0],1,p[1],.14,2,.14,0,0,COL.fence);});
  for(let x=-34;x<34;x+=5){B(x,2,-22,5,.08,.08,0,0,COL.fence);B(x,1,-22,5,.08,.08,0,0,COL.fence);}
  for(let x=-34;x<34;x+=5){B(x,2,22,5,.08,.08,0,0,COL.fence);B(x,1,22,5,.08,.08,0,0,COL.fence);}
  for(let z=-22;z<22;z+=5){B(34,2,z,.08,.08,5,0,0,COL.fence);B(-34,2,z,.08,.08,5,0,0,COL.fence);B(34,1,z,.08,.08,5,0,0,COL.fence);B(-34,1,z,.08,.08,5,0,0,COL.fence);}
  /* streetlights (sodium pools) */
  [[-26,-12],[24,-14],[-2,-18]].forEach(p=>{
    B(p[0],3.5,p[1],.22,7,.22,0,0,COL.pole);B(p[0]+.8,6.9,p[1],1.8,.14,.14,0,0,COL.pole);
    B(p[0]+1.5,6.75,p[1],.5,.3,.3,0,0,COL.lamp,COL.lampOn);
    B(p[0]+1.5,.02,p[1],4.5,.04,4.5,0,0,[0,0,0],[.10,.07,.03]);
  });
  L(-24.5,6.7,-12,[1.1,.8,.45]);L(25.5,6.7,-14,[1.1,.8,.45]);
  /* ---- MOTEL ROW ---- */
  B(-11,2.25,12,26,4.5,8,0,0,COL.motel);
  C(-11,2.25,12,13,2.25,4);
  B(-11,4.62,12,27,.24,9,0,0,COL.roof);
  B(-11,4.4,17.2,26,.2,1,0,0,COL.motelDk);
  /* walkway */
  B(-11,.02,6.2,26,.06,1.8,0,0,COL.walk);
  B(-11,4.35,6.6,26.5,.16,.9,0,0,COL.motelDk); /* overhang */
  const roomX=[-21,-17,-13,-9,-5,-1];
  roomX.forEach((x,i)=>{
    const dc=(i%2?COL.door2:COL.door);
    B(x,1.1,8.05,1.1,2.2,.12,0,0,dc);
    B(x,2.25,8.02,.5,.3,.06,0,0,[0,0,0],COL.numOn); /* room number plate */
    if(i===2){ /* open room: door swung, interior visible */
      B(x+.62,1.1,8.55,1.1,2.2,.1,1.9,0,dc);
      B(x,1,11,1.6,2,5,0,0,[.10,.09,.08]);
      B(x-.3,.45,11.6,1.2,.5,2.2,0,0,[.45,.42,.40]); /* bed */
      B(x+.5,.35,10.4,.5,.7,.5,0,0,[.3,.25,.2]);
      B(x,2.6,10,0,.06,0,0,0,[0,0,0],[.35,.28,.15]);
      B(x+.9,.02,9.2,.9,.03,.7,0,0,[0,0,0],[.12,.10,.06]);
    }
  });
  /* suitcase spilled in open doorway */
  B(-13.6,.25,8.6,.6,.5,.4,0.4,0,COL.suit);
  B(-13.1,.12,8.9,.4,.2,.3,-.7,0,[.6,.55,.5]);
  B(-14.2,.1,9.1,.35,.18,.3,1.2,0,[.5,.3,.35]);
  /* ---- FRONT OFFICE ---- */
  B(8,2.25,10,8,4.5,8,0,0,COL.office);
  C(8,2.25,10,4,2.25,4);
  B(8,4.62,10,8.5,.24,8.5,0,0,COL.roof);
  B(8,3.2,6.02,3.4,1.6,.1,0,0,COL.dinerWin); /* lit window glass */
  B(8,3.2,6.03,3.4,1.6,.12,0,0,[0,0,0],[.5,.42,.25]);
  B(6.2,1.1,6.02,1.4,2.2,.1,0,0,COL.door); /* office door shut */
  /* interior */
  B(7,1,11.5,2.6,2,1,0,0,COL.desk);
  B(9.5,1.9,11.8,1.8,.9,.15,0,0,[.25,.2,.15],[.3,.25,.15]); /* key board */
  B(10.6,1.5,8.6,.9,1.1,.7,0,0,COL.tv);
  B(10.6,1.55,8.25,.7,.8,.05,0,0,[0,0,0],[.5,.7,1]); /* TV screen */
  B(5.5,.4,8,.8,.8,.8,0,0,[.3,.28,.25]);
  L(8,3,6.6,[1,.85,.55]);
  /* ---- DINER ---- */
  B(21,2.5,10,10,5,12,0,0,COL.diner);
  C(21,2.5,10,5,2.5,6);
  B(21,5.1,10,10.6,.2,12.6,0,0,COL.roof);
  B(21,3,4.02,4.5,2,.1,0,0,COL.dinerWin);
  B(21,3,4.03,4.5,2,.12,0,0,[0,0,0],[.55,.48,.30]);
  B(17.5,1.1,4.02,1.4,2.2,.1,0,0,COL.door);
  /* interior: counter + stools + booths */
  B(18,1,11,3,2,1.2,0,0,COL.counter);
  B(17.4,1.35,11,.5,.4,.5,0,0,[.2,.2,.22],[.25,.2,.08]); /* coffee pot on warmer */
  B(18.9,1.35,11,.8,.5,.6,0,0,[.15,.15,.18],[.3,.2,.25]); /* pie display case */
  [16.6,17.6,18.6].forEach(x=>{B(x,.6,10, .5,1.2,.5,0,0,COL.stool);B(x,1.25,10,.55,.1,.55,0,0,COL.stool);});
  B(24,1,8,1.4,.5,1,0,0,COL.booth);B(24,.55,7.3,.4,1.1,.4,0,0,COL.booth);B(24,.55,8.7,.4,1.1,.4,0,0,COL.booth);
  B(24.6,.35,8,.5,.2,.4,.3,0,[.7,.65,.6]); /* half-eaten meal plate */
  B(25.4,.5,12,1.4,.5,1,0,0,COL.booth);B(24.8,.55,11.4,.4,1.1,.4,0,0,COL.booth);B(25.8,.55,12.6,.4,1.1,.4,0,0,COL.booth);
  L(21,3.4,4.6,[1,.9,.6]);
  /* ---- POOL ---- */
  B(24,-1.45,-7,12,.3,10,0,0,COL.pool); /* bottom */
  B(18,-.65,-7,.3,1.4,10,0,0,COL.poolWall);B(30,-.65,-7,.3,1.4,10,0,0,COL.poolWall);
  B(24,-.65,-2,12,1.4,.3,0,0,COL.poolWall);B(24,-.65,-12,12,1.4,.3,0,0,COL.poolWall);
  B(18,.15,-7,.3,.6,10,0,0,COL.poolWall);B(30,.15,-7,.3,.6,10,0,0,COL.poolWall);
  B(24,.15,-2,12,.6,.3,0,0,COL.poolWall);
  C(18,.5,-7,.35,.5,5.2);C(30,.5,-7,.35,.5,5.2);C(24,.5,-12,6.2,.5,.35);
  /* ladder at east end */
  B(29.75,.9,-4.6,.08,2,.08,0,0,COL.pole);B(29.75,.9,-5.4,.08,2,.08,0,0,COL.pole);
  [0.3,0.7,1.1,1.5].forEach(y=>B(29.75,y,-5,.06,.06,.85,0,0,COL.pole));
  /* leaves & debris on bottom */
  [[21,-9,.4],[23,-6.5,.8],[26,-8.5,.2],[22,-4.5,1.1],[27,-5,.6],[24.5,-10,.9]].forEach(l=>B(l[0],-1.26,l[1],.5,.05,.35,l[2],0,COL.leaf));
  B(22.8,-1.2,-7.8,.8,.08,.5,1.2,0,[.3,.25,.2]); /* old pool noodle */
  B(25.5,-1.15,-9.6,.6,.5,.6,2.6,0,COL.chair); /* overturned lawn chair */
  B(25.5,-1.05,-10.1,.6,.15,.4,2.6,0,[.4,.35,.3]);
  /* ---- STATION WAGON ---- */
  B(-10,1.05,-6,2.1,1.1,4.6,0,0,COL.car);
  B(-10,2.05,-6.4,1.9,.9,2.4,0,0,COL.car);
  B(-10,2.1,-6.4,1.95,.55,2.45,0,0,COL.carIn);
  [[-11.2,-4.2],[ -8.8,-4.2],[-11.2,-7.8],[-8.8,-7.8]].forEach(w=>B(w[0],.45,w[1],.35,.9,.9,0,0,COL.wheel));
  B(-11.9,1.05,-6.5,1.9,1.05,.08,1.35,0,COL.car); /* driver door open */
  B(-10,.95,-3.65,.5,.3,.1,0,0,[0,0,0],COL.whiteOn);
  B(-9.0,.95,-3.65,.5,.3,.1,0,0,[0,0,0],COL.whiteOn);
  B(-10,.02,-2.5,3.2,.04,5.5,0,0,[0,0,0],[.09,.08,.05]); /* headlight pool */
  L(-10,1.3,-3.8,[.9,.85,.7]);
  C(-10,1.1,-6,1.35,1.15,2.5);
  /* ---- PAYPHONE ---- */
  B(14,1.2,2,.16,2.4,.16,0,0,COL.pole);
  B(14,1.5,1.85,.7,1.1,.4,0,0,COL.phone);
  B(14,1.5,1.63,.5,.8,.05,0,0,[0,0,0],[.4,.5,.6]);
  B(14.35,1.6,1.7,.12,.5,.12,0,0,COL.phone);
  C(14,1.2,2,.45,1.2,.45);
  /* ---- NO VACANCY sign ---- */
  B(8,1.4,4.2,.12,2.8,.12,0,0,COL.pole);
  B(7.45,3,4.2,.9,.5,.08,0,0,COL.sign,[0,0,0]); /* NO - dead */
  B(7.45,2.45,4.2,.9,.4,.08,0,0,COL.sign,COL.vacOn); /* VACANCY lit */
  /* ---- STARLITE sign ---- */
  B(2,5.5,-20,.3,11,.3,0,0,COL.pole);
  B(2,11.6,-20,7,.25,.9,0,0,COL.sign);
  B(2,12.6,-20,7,.25,.9,0,0,COL.sign);
  /* letters drawn dynamically (flicker) */
  /* ---- VENDING + ICE under overhang ---- */
  B(-14,1.1,6.3,1.1,2.2,1,0,0,COL.vend);
  B(-14,1.15,5.78,1,.9,.06,0,0,[0,0,0],COL.vendOn);
  C(-14,1.1,6.3,.6,1.1,.55);
  B(-11.2,1,6.3,1.3,2,1,0,0,COL.ice);
  B(-11.2,1.1,5.78,1.1,.8,.06,0,0,[0,0,0],[.25,.3,.35]);
  C(-11.2,1,6.3,.7,1,.55);
  /* ---- scattered life ---- */
  B(-17,.5,6.2,1,.8,.7,0.2,0,[.4,.38,.35]); /* laundry cart */
  B(-17,.95,6.2,.8,.3,.5,0.2,0,[.55,.5,.45]);
  C(-17,.5,6.2,.55,.5,.4);
  B(-2,1.1,-10,1.2,.15,.5,1.57,0,[.3,.12,.12]); /* child's bike on its side */
  B(-2,1.05,-10.8,.12,.5,.12,1.57,0,COL.pole);
  B(-2,1.05,-9.3,.12,.5,.12,1.57,0,COL.pole);
  B(12.5,.2,-8,.5,.4,.7,0,0,COL.suit); /* abandoned suitcase */
  B(16,.04,-16,.7,.05,.5,.8,0,COL.paper); /* dropped papers */
  B(16.3,.05,-15.6,.6,.05,.4,1.9,0,COL.paper);
  B(-28,.5,16,1,1,1.2,0,0,COL.bin);C(-28,.5,16,.5,.5,.6);
  B(15.5,.5,3.4,.9,.9,.5,0,0,COL.tire);B(15.5,1.4,3.4,.9,.9,.5,0,0,COL.tire); /* tire stack */
  C(15.5,1,3.4,.55,1,.3);
  B(28.5,.5,14,1.2,1,.6,0,0,[.3,.25,.2]); /* broken bench */
  B(-6,.4,-16,.8,.8,.8,0,0,[.25,.2,.15]); /* old tire barrier */
  C(-6,.4,-16,.45,.4,.45);
  B(31,.6,-16,.3,1.2,.3,0,0,COL.pole);B(31,1.35,-16,.5,.3,.4,0,0,[.4,.3,.2]); /* mailbox */
  /* flashlight on ground, still shining */
  B(-4,.15,-4,.5,.12,.15,.9,0,[.3,.3,.35]);
  B(-3.2,.03,-3.6,1.6,.03,1.1,0,0,[0,0,0],[.12,.11,.08]);
  B(3,.03,12,.9,.03,.7,0,0,[0,0,0],[.08,.07,.05]);
  /* spawn points */
  SPAWNS.push([0,-28],[-16,-28],[16,-28],[-32,-10],[-32,0],[-32,10],[32,-8],[32,2],[32,10],[-20,20],[0,20],[12,20]);
  /* pickup spots */
  PICKUP_SPOTS.push(
    {x:5.5,z:3.5,t:'pistol'},{x:-13,z:-3,t:'pistol'},{x:28,z:-8,t:'pistol'},{x:-2,z:14,t:'pistol'},
    {x:5.5,z:12,t:'shot'},{x:20,z:8,t:'shot'},
    {x:17.5,z:10.5,t:'rifle'},{x:29,z:14,t:'rifle'},
    {x:-16,z:4,t:'med'},{x:31,z:-14,t:'med'},{x:-22,z:14,t:'med'}
  );
}
/* dynamic props: STARLITE letters, newspaper, TV flicker */
const STARX=[-1.4,-.85,-.3,.25,.8,1.35,1.9,2.45];
let starDead=3; /* the "L" is dead */
function drawDynamic(t){
  /* STARLITE neon letters */
  const flick=Math.random()<0.06?0.25:1;
  const base=(0.75+0.25*Math.sin(t*23))*flick;
  for(let i=0;i<8;i++){
    let col=[COL.neon[0]*base,COL.neon[1]*base,COL.neon[2]*base];
    if(i===starDead){ /* dead letter: bursts on and off */
      const on=(Math.sin(t*3.1)>0.55)||(Math.random()<0.01);
      col=on?[.9,.3,.5]:[.12,.05,.08];
    }
    drawBox(2+STARX[i],12.1,-20,.42,.85,.12,0,0,[.05,.05,.08],col);
  }
  /* newspaper blowing across the lot */
  const nx=-8+((t*2.2)%26),nz=10+Math.sin(t*1.7)*3,ny=.06+Math.abs(Math.sin(t*3.1))*.1;
  drawBox(nx,ny,nz,.7,.02,.5,Math.sin(t*2.7)*.8,0,[.8,.8,.75],[.15,.15,.14]);
  drawBox(nx-.5,ny+.02,nz+.3,.5,.02,.4,Math.sin(t*2.3)*1.1,0,[.7,.7,.68],[.12,.12,.11]);
  /* TV static flicker */
  const tv=(.5+.5*Math.sin(t*17))*Math.random();
  drawBox(10.6,1.55,8.25,.7,.8,.05,0,0,[0,0,0],[.3*tv,.45*tv,.65*tv]);
  /* payphone glow pulses while ringing */
  if(G.phase==='break'&&G.ringing){
    const g=.5+.5*Math.sin(t*25);
    drawBox(14,1.5,1.63,.5,.8,.05,0,0,[0,0,0],[.3*g,.4*g,.55*g]);
  }
}
</script>
<script>
/* ============================== GAME STATE ============================== */
const G={
  mode:'title', /* title|play|break|over|win */
  wave:0,kills:0,score:0,
  phase:'title',phaseT:0,
  dawn:0, /* 0..1 over 10 waves */
  ringT:0,ringing:false,
  shake:0,hurt:0,
  time:0
};
const WEAPONS=[
  {name:'PISTOL',dmg:26,head:4,rate:.28,mag:12,res:Infinity,spread:.012,auto:false,
   pellets:1,knock:.6,recoil:.012,snd:'pistol',
   vm:{x:.24,y:-.26,z:-.45,rx:-.06}},
  {name:'SHOTGUN',dmg:13,head:3,rate:.85,mag:6,res:36,spread:.05,auto:false,
   pellets:8,knock:1.6,recoil:.045,snd:'shotgun',
   vm:{x:.26,y:-.27,z:-.5,rx:-.02}},
  {name:'RIFLE',dmg:22,head:4,rate:.11,mag:30,res:120,spread:.02,auto:true,
   pellets:1,knock:.5,recoil:.01,snd:'rifle',
   vm:{x:.22,y:-.24,z:-.55,rx:-.08}}
];
const owned=[true,false,false];
const wpn={cur:0,mag:12,reserve:Infinity,reloadT:0,cool:0,flash:0,anim:0};
const P={x:2,y:0,z:2,yaw:0,pitch:0,vy:0,hp:100,sprint:false,ground:true,bob:0,sway:0};
const keys={};
let zombies=[],particles=[],pickups=[];
const ZTYPES={
  shambler:{hp:60,speed:1.5,r:.55,h:1.75,dmg:12,head:.12,sc:100,col:[.22,.28,.20]},
  runner:{hp:30,speed:3.6,r:.45,h:1.7,dmg:16,head:.13,sc:150,col:[.30,.16,.14]},
  brute:{hp:300,speed:1.0,r:.85,h:2.4,dmg:30,head:.10,sc:400,col:[.16,.19,.15]}
};
/* ============================== AUDIO ============================== */
let AC=null,masterGain=null,ambNodes=[];
function initAudio(){
  if(AC)return;
  AC=new (window.AudioContext||window.webkitAudioContext)();
  masterGain=AC.createGain();masterGain.gain.value=.8;masterGain.connect(AC.destination);
  /* wind */
  const wb=AC.createBuffer(2,AC.sampleRate*2,AC.sampleRate);
  for(let c=0;c<2;c++){const d=wb.getChannelData(c);for(let i=0;i<d.length;i++)d[i]=Math.random()*2-1;}
  const wind=AC.createBufferSource();wind.buffer=wb;wind.loop=true;
  const wf=AC.createBiquadFilter();wf.type='lowpass';wf.frequency.value=320;
  const wg=AC.createGain();wg.gain.value=.05;
  wind.connect(wf);wf.connect(wg);wg.connect(masterGain);wind.start();
  /* neon buzz */
  const nb=AC.createOscillator();nb.type='sawtooth';nb.frequency.value=120;
  const ng=AC.createGain();ng.gain.value=.008;
  nb.connect(ng);ng.connect(masterGain);nb.start();
  const nb2=AC.createOscillator();nb2.type='sawtooth';nb2.frequency.value=240;
  const ng2=AC.createGain();ng2.gain.value=.004;nb2.connect(ng2);ng2.connect(masterGain);nb2.start();
  /* ice hum */
  const ib=AC.createOscillator();ib.type='triangle';ib.frequency.value=55;
  const ig=AC.createGain();ig.gain.value=.012;ib.connect(ig);ig.connect(masterGain);ib.start();
  ambNodes=[wind,ng,ng2,ig];
}
function env(g,t0,a,peak,dec){g.gain.setValueAtTime(0,t0);g.gain.linearRampToValueAtTime(peak,t0+a);g.gain.exponentialRampToValueAtTime(.0001,t0+a+dec);}
function burst(dur,freq,peak,type,q){
  if(!AC)return;const t0=AC.currentTime;
  const b=AC.createBuffer(1,AC.sampleRate*dur,AC.sampleRate);const d=b.getChannelData(0);
  for(let i=0;i<d.length;i++)d[i]=(Math.random()*2-1)*(1-i/d.length);
  const s=AC.createBufferSource();s.buffer=b;
  const f=AC.createBiquadFilter();f.type='lowpass';f.frequency.value=freq;f.Q.value=q||1;
  const g=AC.createGain();env(g,t0,.002,peak,dec=dur);
  s.connect(f);f.connect(g);g.connect(masterGain);s.start(t0);
}
function tone(freq,dur,peak,type,slideTo){
  if(!AC)return;const t0=AC.currentTime;
  const o=AC.createOscillator();o.type=type||'sine';o.frequency.setValueAtTime(freq,t0);
  if(slideTo)o.frequency.exponentialRampToValueAtTime(Math.max(20,slideTo),t0+dur);
  const g=AC.createGain();env(g,t0,.004,peak,dur);
  o.connect(g);g.connect(masterGain);o.start(t0);o.stop(t0+dur+.1);
}
function sfxShot(kind){
  if(!AC)return;
  if(kind==='pistol'){burst(.14,2400,.5,'square');tone(180,.08,.25,'square',60);}
  else if(kind==='shotgun'){burst(.28,1400,.8,'square');tone(90,.2,.4,'sawtooth',40);}
  else{burst(.1,3200,.45,'square');tone(240,.06,.2,'square',90);}
}
function sfxReload(kind){
  if(!AC)return;const t0=AC.currentTime;
  tone(900,.03,.15,'square');
  setTimeout(()=>tone(600,.03,.15,'square'),180);
  setTimeout(()=>tone(1200,.04,.2,'square'),kind==='pistol'?650:900);
}
function sfxEmpty(){if(!AC)return;tone(1500,.04,.18,'square');setTimeout(()=>tone(700,.05,.15,'square'),90);}
function sfxHit(head){
  if(!AC)return;
  burst(head?.12:.08,head?900:600,head?.5:.25,'square');
  if(head)tone(520,.09,.3,'sine',180);
}
function sfxDeath(kind){
  if(!AC)return;
  if(kind==='brute'){tone(70,.5,.5,'sawtooth',30);burst(.5,300,.4,'square');}
  else{tone(160,.35,.3,'sawtooth',45);burst(.25,500,.2,'square');}
}
function sfxHurt(){if(!AC)return;tone(110,.25,.4,'sawtooth',50);burst(.15,800,.25,'square');}
function sfxSting(){
  if(!AC)return;
  tone(55,1.4,.5,'sawtooth');tone(58,1.4,.4,'sawtooth');
  setTimeout(()=>tone(110,.8,.3,'sawtooth',55),300);
}
function sfxRing(){
  if(!AC)return;
  tone(1150,.35,.12,'sine');setTimeout(()=>tone(1150,.35,.12,'sine'),420);
}
function sfxPickup(){if(!AC)return;tone(660,.08,.2,'sine');setTimeout(()=>tone(990,.1,.2,'sine'),80);}
function sfxJump(){if(!AC)return;tone(220,.12,.1,'sine',140);}
function sfxScream(){if(!AC)return;tone(300+Math.random()*200,.4,.15,'sawtooth',120);}
/* zombie ambient proximity */
let groanT=0;
function updateZAudio(dt){
  if(!AC)return;
  let near=1e9;
  for(const z of zombies){const dx=z.x-P.x,dz=z.z-P.z;const d=Math.sqrt(dx*dx+dz*dz);if(d<near)near=d;}
  if(G.mode==='play'&&near<22){
    groanT-=dt;
    if(groanT<=0){
      groanT=1.5+Math.random()*2.5;
      const vol=.02+.12*(1-near/22);
      const z=zombies[0];
      let type='sawtooth',f=60+Math.random()*40;
      if(near<12&&z.type==='runner'){type='sawtooth';f=200+Math.random()*100;}
      if(z.type==='brute'){tone(45+Math.random()*15,.4,vol*1.5,'sawtooth',30);burst(.3,150,vol,'square');}
      tone(f,.6,vol,type,f*.6);
    }
  }
}
/* ============================== WAVES ============================== */
function waveComp(w){
  const comp=[];
  const total=4+w*2+Math.floor(w*w*.35);
  for(let i=0;i<total;i++){
    if(w>=6&&i%7===0)comp.push('brute');
    else if(w>=3&&i%3===1)comp.push('runner');
    else comp.push('shambler');
  }
  return comp;
}
let spawnQueue=[],spawnT=0;
function startWave(){
  G.wave++;
  G.mode='play';G.phase='wave';G.phaseT=0;
  spawnQueue=waveComp(G.wave);
  spawnT=.5;
  banner('WAVE '+G.wave, G.wave===10?'THE LAST OF THEM':'');
  sfxSting();
  G.ringing=false;
  updateHUD();
}
function startBreak(){
  G.mode='break';G.phase='break';G.phaseT=0;
  G.ringing=true;G.ringT=0;
  banner('WAVE CLEARED','RELOAD. RESUPPLY. MOVE.');
  respawnPickups();
  updateHUD();
}
function banner(t,s){
  const b=document.getElementById('banner');
  document.getElementById('btitle').textContent=t;
  document.getElementById('bsub').textContent=s||'';
  b.classList.remove('show');void b.offsetWidth;b.classList.add('show');
}
function respawnPickups(){
  pickups=[];
  for(const s of PICKUP_SPOTS){
    if(s.t==='shot'&&!owned[1])continue;
    if(s.t==='rifle'&&!owned[2])continue;
    if(s.t==='pistol'&&Math.random()<.55)continue;
    if(s.t==='med'&&Math.random()<.7)continue;
    if(s.t==='shot'&&Math.random()<.75)continue;
    if(s.t==='rifle'&&Math.random()<.7)continue;
    pickups.push({x:s.x,z:s.z,t:s.t,taken:false,ph:Math.random()*6});
  }
}
function spawnZombie(type){
  const t=ZTYPES[type];
  const sp=SPAWNS[Math.floor(Math.random()*SPAWNS.length)];
  const ang=Math.random()*6.28;
  const x=sp[0]+Math.sin(ang)*3,z=sp[1]+Math.cos(ang)*3;
  zombies.push({
    x,y:-t.h/2+.0,z,type,
    hp:t.hp,maxhp:t.hp,r:t.r,h:t.h,
    vx:0,vz:0,dead:false,
    dieT:0,diePh:Math.random()*6,
    hitT:0,headHitT:0,
    ph:Math.random()*6,
    stuckT:0,
    scale:type==='brute'?1.25:1
  });
  if(type==='runner'&&Math.random()<.5)sfxScream();
}
</script>
<script>
/* ============================== UPDATES ============================== */
function collide(x,z,r){
  for(const c of COLS){
    const dx=x-c[0],dz=z-c[2];
    if(Math.abs(dx)<c[3]+r&&Math.abs(dz)<c[5]+r){
      const ox=c[3]+r-Math.abs(dx),oz=c[5]+r-Math.abs(dz);
      if(ox<oz)x+=ox*Math.sign(dx||1);
      else z+=oz*Math.sign(dz||1);
    }
  }
  x=Math.max(-33,Math.min(33,x));
  z=Math.max(-21,Math.min(21,z));
  return [x,z];
}
function updatePlayer(dt){
  const sp=(keys['ShiftLeft']||keys['ShiftRight'])?6.2:4.2;
  let mx=0,mz=0;
  if(keys['KeyW'])mz+=1;if(keys['KeyS'])mz-=1;
  if(keys['KeyA'])mx-=1;if(keys['KeyD'])mx+=1;
  const l=Math.sqrt(mx*mx+mz*mz)||1;mx/=l;mz/=l;
  const cy=Math.cos(P.yaw),sy=Math.sin(P.yaw);
  P.x+=(mx*cy-mz*sy)*sp*dt;
  P.z+=(-mx*sy-mz*cy)*sp*dt;
  [P.x,P.z]=collide(P.x,P.z,.5);
  /* pool: drop down / climb out */
  const inPool=P.x>18.5&&P.x<29.5&&P.z>-11.5&&P.z<-2.5;
  const inLadder=Math.abs(P.x-29.75)<1.1&&Math.abs(P.z+5)<1.1;
  if(inPool&&!inLadder){
    P.y=Math.min(P.y,-1.0);
    if(P.y<=-1.0){P.ground=true;P.vy=0;}
  }else{
    P.y=Math.max(P.y,0);
    if(P.y>=0){P.ground=true;P.vy=0;}
  }
  if(!P.ground){P.vy-=22*dt;P.y+=P.vy*dt;}
  if(keys['Space']&&P.ground){P.vy=7.5;P.ground=false;sfxJump();}
  /* bob & sway */
  const moving=(mx||mz)&&P.ground;
  if(moving)P.bob+=dt*sp*1.6;
  P.bob*= (moving?1:0.9);
  P.sway+= (0-P.sway)*Math.min(1,dt*8);
  /* fire */
  if(mouseDown)tryFire();
  /* reload */
  if(wpn.reloadT>0){
    wpn.reloadT-=dt;
    if(wpn.reloadT<=0)finishReload();
  }
  wpn.cool-=dt;
  wpn.flash-=dt;
  wpn.anim=Math.max(0,wpn.anim-dt*3);
  /* hurt decay */
  G.hurt=Math.max(0,G.hurt-dt*1.4);
  G.shake=Math.max(0,G.shake-dt*2.2);
}
function tryFire(){
  if(wpn.cool>0||wpn.reloadT>0)return;
  const w=WEAPONS[wpn.cur];
  if(wpn.mag<=0){sfxEmpty();wpn.cool=.25;startReload();return;}
  wpn.mag--;wpn.cool=w.rate;wpn.flash=.05;wpn.anim=1;
  sfxShot(w.snd);
  G.shake=Math.min(.5,G.shake+w.recoil*2.2);
  P.pitch+=w.recoil*(0.7+Math.random()*.5);
  for(let p=0;p<w.pellets;p++){
    const sp=w.spread*(1+Math.random()*.6);
    const ang=(Math.random()-.5)*sp*2,ang2=(Math.random()-.5)*sp*2;
    const dir=aimDir();
    const dx=dir[0]+(Math.random()-.5)*sp,dy=dir[1]+(Math.random()-.5)*sp,dz=dir[2]+(Math.random()-.5)*sp;
    const l=Math.sqrt(dx*dx+dy*dy+dz*dz);
    fireRay(dx/l,dy/l,dz/l,w,p===0);
  }
  if(wpn.mag===0)startReload();
  updateHUD();
}
function aimDir(){
  const cp=Math.cos(P.pitch),sp=Math.sin(P.pitch),cy=Math.cos(P.yaw),sy=Math.sin(P.yaw);
  return [-sy,sp*cp,-cy*cp];
}
function fireRay(dx,dy,dz,w,primary){
  let best=30,bestZ=null,head=false;
  for(const z of zombies){
    if(z.dead)continue;
    const t=rayBox(P.x,P.y+1.5,P.z,dx,dy,dz,z.x,z.y,z.z,z.r*.6,z.h/2,z.r*.6);
    if(t>0&&t<best){
      best=t;bestZ=z;
      const hy=P.y+1.5+dy*t;
      head=hy>z.y+z.h*.72;
    }
  }
  for(const c of COLS){
    const t=rayBox(P.x,P.y+1.5,P.z,dx,dy,dz,c[0],c[1],c[2],c[3],c[4],c[5]);
    if(t>0&&t<best){best=t;bestZ=null;}
  }
  if(bestZ){
    let dmg=w.dmg*(primary?1:.8);
    if(head)dmg*=w.head;
    bestZ.hp-=dmg;bestZ.hitT=.18;bestZ.headHitT=head?.4:bestZ.headHitT;
    const kx=dx*w.knock,kz=dz*w.knock;
    bestZ.vx+=kx;bestZ.vz+=kz;
    sfxHit(head);
    hitmark(head);
    /* blood */
    const bx=P.x+dx*best,by=P.y+1.5+dy*best,bz=P.z+dz*best;
    for(let i=0;i<(head?10:5);i++)addPart(bx,by,bz,(Math.random()-.5)*2,(Math.random()-.2)*3,(Math.random()-.5)*2,.06,.3,.55,.05,1.2);
    if(bestZ.hp<=0)killZombie(bestZ,head);
  }else if(best<30){
    /* dust on walls */
    const bx=P.x+dx*best,by=P.y+1.5+dy*best,bz=P.z+dz*best;
    for(let i=0;i<3;i++)addPart(bx,by,bz,(Math.random()-.5),(Math.random()-.3),(Math.random()-.5),.04,.5,.45,.4,.25);
  }
  /* tracer */
  addTracer(P.x+dx*.6,P.y+1.5+dy*.6,P.z+dz*.6,dx,dy,dz,Math.min(best,30));
}
function killZombie(z,head){
  z.dead=true;z.dieT=0;
  G.kills++;G.score+=ZTYPES[z.type].sc*(head?2:1);
  sfxDeath(z.type);
  banner2=head?'HEADSHOT':'';
  updateHUD();
  if(head)scorePop('HEADSHOT +'+ZTYPES[z.type].sc*2,'#ff5544');
}
function updateZombies(dt){
  for(const z of zombies){
    if(z.dead){
      z.dieT+=dt;
      continue;
    }
    z.hitT=Math.max(0,z.hitT-dt);
    z.headHitT=Math.max(0,z.headHitT-dt);
    const dx=P.x-z.x,dz=P.z-z.z;
    const d=Math.sqrt(dx*dx+dz*dz)||1;
    const t=ZTYPES[z.type];
    const speed=t.speed*(z.hitT>0?.25:1);
    let nx=dx/d,nz=dz/d;
    /* obstacle avoidance: probe straight ahead, steer if blocked */
    const ax=z.x+nx*z.r+1.2,az=z.z+nz*z.r+1.2;
    if(inside(ax,az,.4)){
      for(const sa of [.6,-.6,1.1,-1.1,1.7,-1.7]){
        const ca=Math.cos(sa),sn=Math.sin(sa);
        const tx=nx*ca-nz*sn,tz=nx*sn+nz*ca;
        if(!inside(z.x+tx*(z.r+1.4),z.z+tz*(z.r+1.4),.4)){nx=tx;nz=tz;break;}
      }
    }
    z.vx+= (nx*speed-z.vx)*Math.min(1,dt*4);
    z.vz+= (nz*speed-z.vz)*Math.min(1,dt*4);
    z.x+=z.vx*dt;z.z+=z.vz*dt;
    [z.x,z.z]=collide(z.x,z.z,z.r);
    /* separation */
    for(const o of zombies){
      if(o===z||o.dead)continue;
      const ox=z.x-o.x,oz=z.z-o.z;
      const od=Math.sqrt(ox*ox+oz*oz);
      const min=z.r+o.r;
      if(od<min&&od>0){z.x+=ox/od*(min-od)*.5;z.z+=oz/od*(min-od)*.5;}
    }
    z.ph+=dt*speed*2.2;
    /* attack */
    if(d<z.r+.6&&z.hitT<=.1){
      z.attackT=(z.attackT||0);
      if((z.atkCd||0)<=0){
        z.atkCd=1.0;
        damagePlayer(t.dmg,z);
      }
    }
    z.atkCd=Math.max(0,(z.atkCd||0)-dt);
    /* brute footfalls */
    if(z.type==='brute'){
      z.stepT=(z.stepT||0)+dt*speed;
      if(z.stepT>1.4){z.stepT=0;if(d<25)tone(40,.3,.12*Math.max(0,1-d/25),'sawtooth',28);}
    }
  }
  zombies=zombies.filter(z=>!z.dead||z.dieT<1.6);
  updateZAudio(dt);
}
function inside(x,z,r){
  for(const c of COLS){
    if(Math.abs(x-c[0])<c[3]+r&&Math.abs(z-c[2])<c[5]+r)return true;
  }
  return false;
}
function damagePlayer(dmg,z){
  if(G.mode!=='play')return;
  P.hp-=dmg;G.hurt=Math.min(1,G.hurt+.45);G.shake=Math.min(.6,G.shake+.3);
  sfxHurt();
  const ang=Math.atan2(z.x-P.x,z.z-P.z);
  P.x+=Math.sin(ang)*.4;P.z+=Math.cos(ang)*.4;
  [P.x,P.z]=collide(P.x,P.z,.5);
  updateHUD();
  if(P.hp<=0)gameOver();
}
function startReload(){
  const w=WEAPONS[wpn.cur];
  if(wpn.reloadT>0||wpn.mag===w.mag)return;
  if(w.res===Infinity&&wpn.mag===w.mag)return;
  wpn.reloadT=w.snd==='pistol'?1.1:w.snd==='shotgun'?1.7:1.5;
  sfxReload(w.snd);
}
function finishReload(){
  const w=WEAPONS[wpn.cur];
  const need=w.mag-wpn.mag;
  const take=Math.min(need,w.res===Infinity?need:Math.floor(w.res));
  wpn.mag+=take;
  if(w.res!==Infinity)w.res-=take;
  updateHUD();
}
function switchWeapon(i){
  if(i===wpn.cur||!owned[i])return;
  wpn.cur=i;wpn.reloadT=0;wpn.cool=Math.max(wpn.cool,.2);
  updateHUD();
}
function updatePickups(dt){
  for(const p of pickups){
    if(p.taken)continue;
    p.ph+=dt*3;
    const dx=P.x-p.x,dz=P.z-p.z;
    if(dx*dx+dz*dz<1.4){
      p.taken=true;sfxPickup();
      const w=WEAPONS;
      if(p.t==='pistol'){wpn.mag=Math.min(w[0].mag,wpn.mag+6);scorePop('PISTOL AMMO','#ffd98a');}
      else if(p.t==='shot'){
        if(!owned[1]){owned[1]=true;scorePop('SHOTGUN ACQUIRED','#ffd98a');wpn.cur=1;wpn.mag=6;}
        else{w[1].res+=12;scorePop('SHOTGUN SHELLS','#ffd98a');}
      }
      else if(p.t==='rifle'){
        if(!owned[2]){owned[2]=true;scorePop('RIFLE ACQUIRED','#ffd98a');wpn.cur=2;wpn.mag=30;}
        else{w[2].res+=30;scorePop('RIFLE AMMO','#ffd98a');}
      }
      else if(p.t==='med'){P.hp=Math.min(100,P.hp+35);scorePop('FIRST AID +35','#7fd47f');}
      updateHUD();
    }
  }
}
let banner2='';
function updateParticles(dt){
  for(const p of particles){
    p.life-=dt;
    p.x+=p.vx*dt;p.y+=p.vy*dt;p.z+=p.vz*dt;
    p.vy-= (p.grav||9.8)*dt;
  }
  particles=particles.filter(p=>p.life>0);
}
function addPart(x,y,z,vx,vy,vz,s,r,g,b,life,grav){
  if(particles.length>600)return;
  particles.push({x,y,z,vx,vy,vz,s,r,g,b,life,max:life,grav:grav===undefined?9.8:grav});
}
const tracers=[];
function addTracer(x,y,z,dx,dy,dz,len){
  tracers.push({x,y,z,dx,dy,dz,len,life:.06});
}
function updateTracers(dt){tracers.forEach(t=>t.life-=dt);for(let i=tracers.length-1;i>=0;i--)if(tracers[i].life<=0)tracers.splice(i,1);}
function hitmark(head){
  const h=document.getElementById('hitmark');
  h.className=head?'hs':'';
  h.style.opacity=1;
  clearTimeout(hitmark._t);
  hitmark._t=setTimeout(()=>h.style.opacity=0,head?220:110);
}
let pops=[];
function scorePop(txt,col){
  const el=document.createElement('div');
  el.textContent=txt;
  el.style.cssText='position:fixed;left:50%;top:42%;transform:translateX(-50%);z-index:9;pointer-events:none;font-size:18px;letter-spacing:2px;color:'+col+';text-shadow:0 0 10px '+col+';font-family:Courier New,monospace;transition:all .9s ease-out;opacity:1';
  document.body.appendChild(el);
  requestAnimationFrame(()=>{el.style.top='34%';el.style.opacity=0;});
  setTimeout(()=>el.remove(),1000);
}
function updateClock(){
  const mins=190+G.dawn*190; /* 3:10 -> 6:20 */
  const h=Math.floor(mins/60),m=Math.floor(mins%60);
  document.getElementById('clock').textContent=h+':'+String(m).padStart(2,'0')+' A.M.';
}
function updateHUD(){
  document.getElementById('wave').textContent='WAVE '+(G.wave||'—')+' / 10';
  const rem=spawnQueue.length+zombies.filter(z=>!z.dead).length;
  document.getElementById('zleft').textContent=(G.mode==='play'?'ZOMBIES: ':'DEAD: ')+(G.mode==='play'?rem:G.kills);
  document.getElementById('kills').textContent='KILLS '+G.kills;
  document.getElementById('score').textContent='SCORE '+G.score;
  document.getElementById('hptxt').textContent=Math.max(0,Math.ceil(P.hp));
  document.getElementById('hpfill').style.width=Math.max(0,P.hp)+'%';
  const w=WEAPONS[wpn.cur];
  document.getElementById('wname').textContent=w.name;
  document.getElementById('ammo').textContent=wpn.mag;
  document.getElementById('resv').textContent=w.res===Infinity?'∞':Math.floor(w.res);
  for(let i=0;i<3;i++){
    const s=document.getElementById('slot'+i);
    s.className='slot'+(i===wpn.cur?' cur':'')+(!owned[i]?' lock':'');
  }
}
function gameOver(){
  G.mode='over';
  document.exitPointerLock&&document.exitPointerLock();
  document.getElementById('overStats').innerHTML=
    'You survived to <b style="color:#ffd98a">WAVE '+G.wave+'</b><br>KILLS: '+G.kills+' &nbsp; SCORE: '+G.score;
  showScreen('overScr');
}
function winGame(){
  G.mode='win';G.dawn=1;
  document.exitPointerLock&&document.exitPointerLock();
  document.getElementById('winStats').innerHTML=
    'The sun clears the treeline. The dead stop.<br><br>KILLS: '+G.kills+' &nbsp; SCORE: '+G.score+'<br><span style="color:#9fd4ff">You made it to dawn.</span>';
  showScreen('winScr');
}
function showScreen(id){
  ['title','pauseScr','overScr','winScr'].forEach(s=>document.getElementById(s).classList.add('hidden'));
  if(id)document.getElementById(id).classList.remove('hidden');
}
</script>
<script>
/* ============================== RENDER ============================== */
const VP=new Float32Array(16),VW=new Float32Array(16),PV=new Float32Array(16);
function renderSky(){
  gl.useProgram(progS);
  gl.depthMask(false);
  gl.bindBuffer(gl.ARRAY_BUFFER,skyBuf);
  gl.enableVertexAttribArray(aSky);
  gl.vertexAttribPointer(aSky,2,gl.FLOAT,false,0,0);
  const d=G.dawn;
  const top=[0.004+d*.34,0.006+d*.44,0.012+d*.62];
  const hor=[0.012+d*.83,0.014+d*.58,0.02+d*.43];
  gl.uniform3f(uS.uTop,top[0],top[1],top[2]);
  gl.uniform3f(uS.uHor,hor[0],hor[1],hor[2]);
  gl.uniform1f(uS.uDawn,d);
  /* sun uv: project sun dir (north -Z, rising) */
  const elev=.08+d*.45;
  const sd=[0,Math.sin(elev),-Math.cos(elev)];
  const cy=Math.cos(P.yaw),sy=Math.sin(P.yaw),cp=Math.cos(P.pitch),sp=Math.sin(P.pitch);
  const fx=-sy,fy=sp*cy,fz=-cp*cy;
  const ux=sy*sp,uy=cp,uz=-cy*sp;
  const vx=fx*sd[0]+fy*sd[1]+fz*sd[2];
  const vy=ux*sd[0]+uy*sd[1]+uz*sd[2];
  const tf=Math.tan(1.15/2);
  const ndcx=vx/(tf*aspect), ndcy=vy/tf;
  gl.uniform2f(uS.uSunUV,ndcx*.5+.5,ndcy*.5+.5);
  gl.drawArrays(gl.TRIANGLES,0,3);
  gl.depthMask(true);
}
function setLights(){
  const L6x=[-24.5,25.5,8,21,-10,10.6],L6y=[6.7,6.7,3,3.4,1.3,1.5],L6z=[-12,-14,6.6,4.6,-3.8,8.2];
  const C6=[[1.1,.8,.45],[1.1,.8,.45],[1,.85,.55],[1,.9,.6],[.9,.85,.7],[.5,.7,1]];
  for(let i=0;i<6;i++){
    gl.uniform3f(uW.uLP[0],L6x[i],L6y[i],L6z[i]);
    break;
  }
}
function renderWorld(){
  const d=G.dawn;
  const hor=[0.012+d*.83,0.014+d*.58,0.02+d*.43];
  const shake=G.shake;
  const ex=P.x+(Math.random()-.5)*shake,ey=P.y+1.6+(Math.random()-.5)*shake,ez=P.z+(Math.random()-.5)*shake;
  persp(PV,1.15,aspect,.08,220);
  viewM_(VW,ex,ey,ez,P.yaw,P.pitch);
  m4mul(VP,PV,VW);
  renderSky();
  gl.useProgram(progW);
  gl.uniformMatrix4fv(uW.uVP,false,VP);
  gl.uniform3f(uW.uCam,ex,ey,ez);
  gl.uniform3f(uW.uSunDir,0,.4,-1);
  gl.uniform3f(uW.uSunCol,.10+d*.75,.10+d*.55,.12+d*.35);
  gl.uniform3f(uW.uAmb,.035+d*.30,.038+d*.34,.05+d*.42);
  gl.uniform3f(uW.uFogCol,hor[0],hor[1],hor[2]);
  gl.uniform1f(uW.uFogD,.016+.004*(1-d));
  /* lights */
  const LX=[-24.5,25.5,8,21,-10,10.6],LY=[6.7,6.7,3.1,3.4,1.3,1.55],LZ=[-12,-14,6.6,4.6,-3.8,8.2];
  const LC=[[1.1,.8,.45],[1.1,.8,.45],[1,.85,.55],[1,.9,.6],[.9,.85,.7],[.45,.65,.9]];
  for(let i=0;i<6;i++){
    gl.uniform3f(uW.uLP[i],LX[i],LY[i],LZ[i]);
    gl.uniform3f(uW.uLC[i],LC[i][0],LC[i][1],LC[i][2]);
  }
  bindCube();
  for(const p of PROPS)drawBox(p[0],p[1],p[2],p[3],p[4],p[5],p[6],p[7],p[8],p[9]);
  drawDynamic(G.time);
  /* pickups */
  for(const p of pickups){
    if(p.taken)continue;
    const y=.35+Math.sin(p.ph)*.1;
    let c=[1,1,1],e=[.6,.5,.3];
    if(p.t==='med')c=[.2,.6,.25];
    if(p.t==='shot')c=[.7,.4,.15];
    if(p.t==='rifle')c=[.2,.3,.6];
    if(p.t==='pistol')c=[.6,.55,.3];
    drawBox(p.x,y,p.z,.35,.3,.35,p.ph*.5,0,c,e);
  }
  /* tracers as point sprites */
  renderParticles();
}
function renderParticles(){
  const n=particles.length;
  if(!n)return;
  gl.useProgram(progP);
  gl.uniformMatrix4fv(uPt.uVP,false,VP);
  gl.uniform1f(uPt.uPx,H*.5);
  const pos=new Float32Array(n*3),siz=new Float32Array(n),col=new Float32Array(n*3);
  for(let i=0;i<n;i++){
    const p=particles[i];
    pos[i*3]=p.x;pos[i*3+1]=p.y;pos[i*3+2]=p.z;
    siz[i]=p.s;
    col[i*3]=p.r;col[i*3+1]=p.g;col[i*3+2]=p.b;
  }
  gl.bindBuffer(gl.ARRAY_BUFFER,pPosB);gl.bufferData(gl.ARRAY_BUFFER,pos,gl.DYNAMIC_DRAW);
  gl.enableVertexAttribArray(uPt.aP);gl.vertexAttribPointer(uPt.aP,3,gl.FLOAT,false,0,0);
  gl.bindBuffer(gl.ARRAY_BUFFER,pSizeB);gl.bufferData(gl.ARRAY_BUFFER,siz,gl.DYNAMIC_DRAW);
  gl.enableVertexAttribArray(uPt.aS);gl.vertexAttribPointer(uPt.aS,1,gl.FLOAT,false,0,0);
  gl.bindBuffer(gl.ARRAY_BUFFER,pColB);gl.bufferData(gl.ARRAY_BUFFER,col,gl.DYNAMIC_DRAW);
  gl.enableVertexAttribArray(uPt.aC);gl.vertexAttribPointer(uPt.aC,3,gl.FLOAT,false,0,0);
  gl.drawArrays(gl.POINTS,0,n);
}
function vmM(tx,ty,tz,rx){
  const c=Math.cos(rx),s=Math.sin(rx);
  return [1,0,0,0, 0,c,s,0, 0,-s,c,0, tx,ty,tz,1];
}
function renderViewModel(){
  const w=WEAPONS[wpn.cur];
  bindCube();
  const bobX=Math.sin(P.bob)*.012,bobY=-Math.abs(Math.cos(P.bob))*.014;
  const rec=wpn.anim;
  const tx=w.vm.x+P.sway*.02+bobX, ty=w.vm.y+bobY-rec*.03, tz=w.vm.z-rec*.09;
  const rx=w.vm.rx+rec*.14;
  gl.useProgram(progW);
  gl.uniformMatrix4fv(uW.uVP,false,VP);
  const c=[.08,.08,.10];
  const draw=(ox,oy,oz,sx,sy,sz,ry,col,em)=>{
    trs(M,ox,oy,oz,ry||0,0,sx,sy,sz);
    m4mul(M,M,vmM(tx,ty,tz,rx));
    gl.uniformMatrix4fv(uW.uModel,false,M);
    gl.uniform3f(uW.uColor,col[0],col[1],col[2]);
    gl.uniform3f(uW.uEm,em?em[0]:0,em?em[1]:0,em?em[2]:0);
    gl.drawElements(gl.TRIANGLES,36,gl.UNSIGNED_SHORT,0);
  };
  if(wpn.cur===0){ /* pistol */
    draw(0,.02,0,.05,.045,.22,0,c);
    draw(0,-.04,.05,.04,.09,.05,.3,[.05,.05,.06]);
    draw(0,.05,-.1,.03,.02,.05,0,c);
  }else if(wpn.cur===1){ /* shotgun */
    draw(0,0,-.05,.045,.05,.55,0,c);
    draw(0,-.05,.1,.05,.07,.16,.2,[.25,.15,.08]);
    draw(0,.06,-.2,.025,.025,.3,0,c);
  }else{ /* rifle */
    draw(0,0,-.05,.04,.06,.5,0,c);
    draw(0,-.08,-.05,.035,.12,.06,.1,[.05,.05,.06]);
    draw(0,.05,-.28,.028,.028,.32,0,c);
    draw(0,-.03,.18,.05,.08,.12,0,[.2,.12,.06]);
  }
  /* muzzle flash */
  if(wpn.flash>0){
    const my=wpn.cur===1?.05:.06,mz=wpn.cur===2?-.46:wpn.cur===1?-.35:-.24;
    draw(0,my,mz,.09,.09,.09,0,[1,.85,.4],[3,2.4,1.2]);
  }
}
function renderZombies(){
  gl.useProgram(progW);
  gl.uniformMatrix4fv(uW.uVP,false,VP);
  bindCube();
  for(const z of zombies){
    const t=ZTYPES[z.type];
    const dying=z.dead;
    const fall=dying?Math.min(z.dieT*2.4,1.57):0;
    const dx=P.x-z.x,dz=P.z-z.z;
    const ry=Math.atan2(dx,dz);
    const sw=Math.sin(z.ph);
    const hit=z.hitT>0,headHit=z.headHitT>0;
    const flash=hit?[.9,.9,.9]:null;
    const body=flash||t.col;
    const headC=flash?[1,.9,.9]:[t.col[0]+.06,t.col[1]+.04,t.col[2]+.03];
    const s=z.scale;
    const h=z.h;
    const bx=z.x,by=z.y,bz=z.z;
    const draw=(ox,oy,oz,sx,sy,sz,ry2,rx2,col,em)=>{
      trs(M,ox,oy,oz,ry2||0,rx2||0,sx,sy,sz);
      const base=[1,0,0,0,0,1,0,0,0,0,1,0,bx,by,bz,1];
      m4mul(M,M,base);
      if(fall){
        const fc=Math.cos(fall),fs=Math.sin(fall);
        const rot=[1,0,0,0,0,fc,-fs,0,0,fs,fc,0,0,0,0,1];
        m4mul(M,M,rot);
      }
      gl.uniformMatrix4fv(uW.uModel,false,M);
      gl.uniform3f(uW.uColor,col[0],col[1],col[2]);
      gl.uniform3f(uW.uEm,em?em[0]:0,em?em[1]:0,em?em[2]:0);
      gl.drawElements(gl.TRIANGLES,36,gl.UNSIGNED_SHORT,0);
    };
    const bob=dying?0:Math.abs(Math.sin(z.ph))*.06;
    /* legs */
    draw(-z.r*.35*s,h*.22*s+bob,z.r*.1, z.r*.4*s,h*.44*s,z.r*.45*s, 0,sw*.7*(dying?0:1),body);
    draw(z.r*.35*s,h*.22*s+bob,z.r*.1, z.r*.4*s,h*.44*s,z.r*.45*s, 0,-sw*.7*(dying?0:1),body);
    /* torso */
    draw(0,h*.6*s+bob,0, z.r*.95*s,h*.42*s,z.r*.75*s, 0,Math.sin(z.ph*.5)*.06, body, headHit?[0,0,0]:null);
    /* arms */
    draw(-z.r*.65*s,h*.66*s+bob,z.r*.15, z.r*.32*s,h*.4*s,z.r*.32*s, 0,-sw*.9*(dying?0:1), body);
    draw(z.r*.65*s,h*.66*s+bob,z.r*.15, z.r*.32*s,h*.4*s,z.r*.32*s, 0,sw*.9*(dying?0:1), body);
    /* head */
    draw(0,h*.86*s+bob,z.r*.15, z.r*.62*s,h*.18*s,z.r*.62*s, 0,headHit?.3:0, headC, headHit?[1,.3,.2]:null);
  }
}
function render(){
  gl.clearColor(0,0,0,1);
  gl.clear(gl.COLOR_BUFFER_BIT|gl.DEPTH_BUFFER_BIT);
  renderWorld();
  renderZombies();
  renderViewModel();
}
/* ============================== MAIN LOOP ============================== */
let last=0;
function loop(ts){
  requestAnimationFrame(loop);
  const dt=Math.min(.05,(ts-last)/1000||.016);last=ts;
  G.time+=dt;
  if(G.mode==='play'||G.mode==='break'){
    updatePlayer(dt);
    updatePickups(dt);
    updateParticles(dt);
    updateTracers(dt);
    if(G.mode==='play'){
      updateZombies(dt);
      /* spawning */
      if(spawnQueue.length){
        spawnT-=dt;
        if(spawnT<=0){
          spawnT=Math.max(.18,1.0-G.wave*.07)*(0.6+Math.random()*.8);
          spawnZombie(spawnQueue.pop());
        }
      }else if(!zombies.some(z=>!z.dead)){
        if(G.wave>=10)winGame();
        else startBreak();
      }
      G.dawn=(G.wave-1)/10+Math.min(.09,((1-zombies.filter(z=>!z.dead).length)/Math.max(1,zombies.length))*.05);
    }else{
      G.phaseT+=dt;
      /* phone rings in break */
      G.ringT+=dt;
      if(G.ringT>2.4){G.ringT=0;sfxRing();}
      const rem=18-G.phaseT;
      const cd=document.getElementById('cd');
      cd.textContent='NEXT WAVE IN '+Math.max(0,Math.ceil(rem));
      if(rem<=0){
        cd.textContent='';
        G.ringing=false;
        startWave();
      }
    }
    updateClock();
    /* vignette */
    document.getElementById('vignette').style.opacity=G.hurt;
  }
  render();
}
/* ============================== INPUT ============================== */
let mouseDown=false;
addEventListener('keydown',e=>{
  keys[e.code]=true;
  if(e.code==='KeyR')startReload();
  if(e.code==='Digit1')switchWeapon(0);
  if(e.code==='Digit2')switchWeapon(1);
  if(e.code==='Digit3')switchWeapon(2);
});
addEventListener('keyup',e=>keys[e.code]=false);
addEventListener('mousedown',e=>{
  if(e.button!==0)return;
  if(G.mode==='title'){startGame();return;}
  if(document.pointerLockElement!==cv){
    if(G.mode==='play'||G.mode==='break')cv.requestPointerLock();
    return;
  }
  mouseDown=true;
});
addEventListener('mouseup',e=>{if(e.button===0)mouseDown=false;});
addEventListener('mousemove',e=>{
  if(document.pointerLockElement!==cv)return;
  P.yaw-=e.movementX*.0022;
  P.pitch-=e.movementY*.0022;
  P.pitch=Math.max(-1.4,Math.min(1.4,P.pitch));
  P.sway=Math.max(-.5,Math.min(.5,P.sway+e.movementX*.0004));
});
addEventListener('wheel',e=>{
  if(G.mode!=='play'&&G.mode!=='break')return;
  let n=wpn.cur+(e.deltaY>0?1:-1);
  if(n<0)n=2;if(n>2)n=0;
  if(owned[n])switchWeapon(n);
},{passive:true});
document.addEventListener('pointerlockchange',()=>{
  if(document.pointerLockElement!==cv&&(G.mode==='play'||G.mode==='break')){
    G.paused=true;
    showScreen('pauseScr');
  }else G.paused=false;
});
document.getElementById('resumeBtn').onclick=()=>{
  showScreen(null);
  cv.requestPointerLock();
};
function resetGame(){
  G.mode='break';G.wave=0;G.kills=0;G.score=0;G.dawn=0;G.phaseT=0;G.hurt=0;G.shake=0;
  G.ringing=true;G.ringT=0;
  P.x=2;P.y=0;P.z=2;P.yaw=0;P.pitch=0;P.vy=0;P.hp=100;
  owned[0]=true;owned[1]=false;owned[2]=false;
  wpn.cur=0;wpn.mag=12;wpn.cool=0;wpn.reloadT=0;wpn.flash=0;
  WEAPONS[1].res=36;WEAPONS[2].res=120;
  zombies=[];particles=[];tracers.length=0;spawnQueue=[];spawnT=0;
  respawnPickups();
  banner('3:10 A.M.','THEY ARE COMING');
  updateHUD();
}
function startGame(){
  initAudio();
  showScreen(null);
  resetGame();
  cv.requestPointerLock();
}
document.getElementById('restartBtn').onclick=startGame;
document.getElementById('winBtn').onclick=startGame;
/* go */
buildWorld();
respawnPickups();
updateHUD();
requestAnimationFrame(loop);
</script>
</body>
</html>