← Zombie FPS results

Zombie FPS

Mimo v2.6 Flash · 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 126,776 bytes · SHA-256 a54478c59cc9
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=no"/>
<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;color:#dfe6e9;
  font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;user-select:none;-webkit-user-select:none;cursor:default}
canvas{display:block;position:fixed;left:0;top:0;width:100%;height:100%}
#hud{position:fixed;inset:0;pointer-events:none;z-index:10;overflow:hidden}
.mono{font-family:"SF Mono",Menlo,Consolas,"Courier New",monospace}
/* ---------- panels ---------- */
.panel{position:absolute;padding:10px 14px;background:rgba(6,9,14,.55);border:1px solid rgba(140,180,220,.18);
  border-left:3px solid rgba(255,150,60,.55);backdrop-filter:blur(3px);letter-spacing:.06em;text-shadow:0 2px 6px #000}
.lbl{font-size:10px;letter-spacing:.28em;color:#7f93a8;text-transform:uppercase}
.big{font-size:26px;font-weight:700;line-height:1.05;color:#f2f6fa}
#tl{left:18px;top:16px;min-width:172px}
#tr{right:18px;top:16px;text-align:right;border-left:1px solid rgba(140,180,220,.18);border-right:3px solid rgba(255,150,60,.55)}
#tr .row{font-size:13px;letter-spacing:.16em;color:#b9c8d8;margin:3px 0}
#tr .row b{color:#ffd9a0;font-size:17px;font-weight:700;margin-left:8px}
/* ---------- health ---------- */
#hpWrap{position:absolute;left:18px;bottom:20px;width:270px}
#hpTop{display:flex;justify-content:space-between;align-items:flex-end;margin-bottom:5px}
#hpText{font-size:30px;font-weight:700;color:#ff6b5a;text-shadow:0 0 14px rgba(255,60,40,.55);line-height:1}
#hpBarBg{height:13px;background:rgba(0,0,0,.62);border:1px solid rgba(255,255,255,.16);position:relative;overflow:hidden}
#hpBar{height:100%;width:100%;background:linear-gradient(90deg,#ff3b2f,#ff8b4a);transition:width .18s ease}
#hpTick{position:absolute;top:0;bottom:0;width:2px;background:rgba(255,255,255,.25)}
/* ---------- ammo ---------- */
#amWrap{position:absolute;right:18px;bottom:20px;text-align:right}
#wname{font-size:12px;letter-spacing:.3em;color:#8fa6bd;text-transform:uppercase}
#ammo{font-size:40px;font-weight:700;letter-spacing:.02em;line-height:1;color:#eaf2f8;text-shadow:0 0 18px rgba(120,190,255,.35)}
#ammo.low{color:#ff7a5a}
#ammo .res{font-size:20px;color:#8fa6bd}
#slots{margin-top:7px;display:flex;gap:6px;justify-content:flex-end}
.slot{font-size:11px;padding:3px 9px;border:1px solid rgba(255,255,255,.16);color:#6d8095;background:rgba(0,0,0,.4);letter-spacing:.1em}
.slot.on{color:#0a0d12;background:#ffce7a;border-color:#ffce7a;font-weight:700}
.slot.off{opacity:.32;text-decoration:line-through}
/* ---------- crosshair ---------- */
#cross{position:absolute;left:50%;top:50%;width:0;height:0}
#cross i{position:absolute;left:-1px;top:-1px;background:rgba(232,246,255,.92);box-shadow:0 0 4px rgba(0,0,0,.9);display:block}
#cross .h{width:9px;height:2px;top:-1px}
#cross .v{width:2px;height:9px;left:-1px}
#cross .dot{width:2px;height:2px;left:-1px;top:-1px;border-radius:50%;background:rgba(255,255,255,.9)}
#hitmark{position:absolute;left:50%;top:50%;width:0;height:0;opacity:0;transform:scale(1.5)}
#hitmark i{position:absolute;width:12px;height:2px;background:#fff;box-shadow:0 0 6px #000}
#hitmark.hs i{background:#ff9b3d}
#hitmark i:nth-child(1){transform:translate(-14px,-14px) rotate(45deg)}
#hitmark i:nth-child(2){transform:translate(2px,-14px) rotate(-45deg)}
#hitmark i:nth-child(3){transform:translate(-14px,12px) rotate(-45deg)}
#hitmark i:nth-child(4){transform:translate(2px,12px) rotate(45deg)}
@keyframes hm{0%{opacity:1;transform:scale(1.35)}100%{opacity:0;transform:scale(.85)}}
#hitmark.go{animation:hm .22s ease-out}
/* ---------- banners ---------- */
#banner{position:absolute;left:0;right:0;top:26%;text-align:center;opacity:0;transform:translateY(14px)}
#banner .t1{font-size:74px;font-weight:800;letter-spacing:.22em;color:#fff;
  text-shadow:0 0 34px rgba(255,90,40,.75),0 6px 22px #000}
#banner .t2{font-size:15px;letter-spacing:.5em;color:#ffb066;margin-top:6px}
@keyframes bn{0%{opacity:0;transform:translateY(20px) scale(.94)}14%{opacity:1;transform:translateY(0) scale(1)}
  76%{opacity:1}100%{opacity:0;transform:translateY(-16px)}}
#banner.go{animation:bn 2.6s ease-out forwards}
#subbanner{position:absolute;left:0;right:0;top:44%;text-align:center;font-size:20px;letter-spacing:.34em;
  color:#9fe3ff;text-shadow:0 0 16px rgba(0,0,0,.9);opacity:0;transition:opacity .3s}
#subbanner.go{opacity:1}
#breakBox{position:absolute;left:50%;top:15px;transform:translateX(-50%);text-align:center;padding:8px 22px;
  background:rgba(6,9,14,.6);border:1px solid rgba(120,220,255,.35);display:none}
#breakBox .lbl{color:#6fd0ff}
#btCount{font-size:34px;font-weight:700;color:#bfefff;line-height:1.05}
#btNote{font-size:11px;letter-spacing:.24em;color:#ffd08a;margin-top:3px}
/* ---------- prompt / messages ---------- */
#prompt{position:absolute;left:50%;bottom:26%;transform:translateX(-50%);font-size:15px;letter-spacing:.16em;
  color:#ffe6b8;background:rgba(0,0,0,.55);border:1px solid rgba(255,210,140,.4);padding:7px 16px;opacity:0;transition:opacity .15s}
#prompt.go{opacity:1}
#msgs{position:absolute;left:50%;bottom:34%;transform:translateX(-50%);text-align:center}
.msg{font-size:15px;letter-spacing:.2em;color:#c9f4ff;margin-top:5px;text-shadow:0 2px 8px #000;animation:mfade 2.6s forwards}
@keyframes mfade{0%{opacity:0;transform:translateY(8px)}12%{opacity:1;transform:none}72%{opacity:1}100%{opacity:0}}
/* ---------- overlays ---------- */
#vig{position:absolute;inset:0;opacity:0;transition:opacity .16s;
  background:radial-gradient(ellipse at center,rgba(255,0,0,0) 32%,rgba(150,0,0,.55) 74%,rgba(90,0,0,.95) 100%)}
#lowhp{position:absolute;inset:0;opacity:0;background:radial-gradient(ellipse at center,rgba(255,0,0,0) 46%,rgba(190,0,0,.6) 100%)}
#lowhp.beat{animation:beat 1.15s infinite}
@keyframes beat{0%,100%{opacity:.18}42%{opacity:.55}}
#dfash{position:absolute;inset:0;background:rgba(255,40,20,.34);opacity:0;transition:opacity .28s}
#fade{position:absolute;inset:0;background:#000;opacity:0;transition:opacity .8s;pointer-events:none}
.screen{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;
  text-align:center;pointer-events:auto;background:radial-gradient(ellipse at center,rgba(4,6,10,.42),rgba(0,0,0,.88));
  opacity:1;transition:opacity .5s}
.screen.hide{opacity:0;pointer-events:none}
.kicker{font-size:12px;letter-spacing:.62em;color:#ff8b4a;margin-bottom:14px}
.title{font-family:Georgia,"Times New Roman",serif;font-size:clamp(38px,7.4vw,96px);line-height:.98;color:#fff;
  letter-spacing:.04em;text-shadow:0 0 44px rgba(255,60,140,.5),0 0 90px rgba(60,140,255,.35),0 8px 30px #000}
.title em{font-style:normal;color:#ff5f9e;text-shadow:0 0 30px rgba(255,60,140,.9),0 0 70px rgba(255,60,140,.5)}
.sub{margin-top:16px;font-size:14px;letter-spacing:.4em;color:#93a9bd}
.cta{margin-top:34px;font-size:17px;letter-spacing:.3em;color:#0a0d12;background:#ffce7a;padding:13px 34px;font-weight:700;
  box-shadow:0 0 40px rgba(255,200,120,.4);animation:pulse 1.7s infinite}
@keyframes pulse{0%,100%{transform:scale(1);filter:brightness(1)}50%{transform:scale(1.035);filter:brightness(1.18)}}
.keys{margin-top:30px;display:grid;grid-template-columns:auto auto;gap:7px 26px;font-size:12.5px;letter-spacing:.1em;
  color:#9fb2c6;text-align:left}
.keys b{color:#ffd9a0;font-weight:700;letter-spacing:.14em}
.stat{display:flex;gap:38px;margin-top:26px;flex-wrap:wrap;justify-content:center}
.stat div{min-width:118px}
.stat .n{font-size:44px;font-weight:800;color:#fff;line-height:1}
.stat .k{font-size:10px;letter-spacing:.3em;color:#7f93a8;margin-top:6px}
.dead .n{color:#ff5a4a}
.btn{margin-top:30px;font-size:14px;letter-spacing:.26em;color:#0a0d12;background:#ffce7a;padding:12px 30px;
  font-weight:700;cursor:pointer;pointer-events:auto;border:0}
.btn:hover{filter:brightness(1.15)}
#scrWin{background:linear-gradient(180deg,rgba(10,14,26,.5),rgba(255,140,60,.28))}
#scrWin .title{text-shadow:0 0 40px rgba(255,190,90,.8),0 8px 30px #000}
.hint{position:absolute;bottom:22px;font-size:11px;letter-spacing:.24em;color:#63788c}
#err{position:fixed;left:0;right:0;bottom:0;padding:10px 14px;background:#300;color:#ffb3b3;font-size:12px;
  font-family:monospace;white-space:pre-wrap;z-index:99;display:none;max-height:42%;overflow:auto}
</style>
</head>
<body>
<canvas id="c"></canvas>
<div id="hud">
  <div id="vig"></div><div id="lowhp"></div><div id="dfash"></div>
  <div id="cross">
    <i class="h" id="cl"></i><i class="h" id="cr"></i><i class="v" id="cu"></i><i class="v" id="cd"></i><i class="dot"></i>
  </div>
  <div id="hitmark"><i></i><i></i><i></i><i></i></div>
  <div class="panel" id="tl">
    <div class="lbl">Wave</div><div class="big"><span id="waveN">1</span><span style="font-size:15px;color:#7f93a8"> / 10</span></div>
    <div class="lbl" style="margin-top:7px">Zombies remaining</div>
    <div class="big" style="color:#ff9a5c"><span id="zLeft">0</span></div>
  </div>
  <div class="panel" id="tr">
    <div class="row">SCORE<b id="score">0</b></div>
    <div class="row">KILLS<b id="kills">0</b></div>
    <div class="row">HEADSHOTS<b id="hs">0</b></div>
  </div>
  <div id="breakBox">
    <div class="lbl">Break &mdash; next wave</div>
    <div id="btCount">15</div>
    <div id="btNote">&#9742; THE PHONE IS RINGING</div>
  </div>
  <div id="hpWrap">
    <div id="hpTop"><span class="lbl">Condition</span><span id="hpText">100</span></div>
    <div id="hpBarBg"><div id="hpBar"></div></div>
  </div>
  <div id="amWrap">
    <div id="wname">Pistol</div>
    <div id="ammo">12 <span class="res">&infin;</span></div>
    <div id="slots"><span class="slot on" id="s1">1 PISTOL</span><span class="slot off" id="s2">2 SHOTGUN</span><span class="slot off" id="s3">3 RIFLE</span></div>
  </div>
  <div id="banner"><div class="t1"></div><div class="t2"></div></div>
  <div id="subbanner"></div>
  <div id="prompt"></div>
  <div id="msgs"></div>
  <div id="fade"></div>

  <div class="screen" id="scrTitle">
    <div class="kicker">3:10 A.M. &nbsp;&middot;&nbsp; ROUTE 9 &nbsp;&middot;&nbsp; NO ONE CHECKED OUT</div>
    <div class="title">LAST CALL<br/>AT THE <em>STARLITE</em></div>
    <div class="sub">HOLD THE LOT UNTIL DAWN &mdash; 10 WAVES</div>
    <div class="cta">CLICK TO START</div>
    <div class="keys">
      <span><b>MOUSE</b></span><span>look &amp; aim</span>
      <span><b>W A S D</b></span><span>move &nbsp;&middot;&nbsp; <b>SHIFT</b> sprint</span>
      <span><b>LMB</b></span><span>fire &nbsp;&middot;&nbsp; <b>R</b> reload</span>
      <span><b>1 2 3 / WHEEL</b></span><span>switch weapon</span>
      <span><b>ESC</b></span><span>pause</span>
    </div>
    <div class="hint">SHOTGUN IN THE STATION WAGON &middot; RIFLE BEHIND THE OFFICE COUNTER</div>
  </div>

  <div class="screen hide" id="scrPause">
    <div class="kicker">PAUSED</div>
    <div class="title" style="font-size:52px">THE NIGHT WAITS</div>
    <div class="keys" style="margin-top:26px">
      <span><b>MOUSE</b></span><span>look &amp; aim</span>
      <span><b>W A S D</b></span><span>move &nbsp;&middot;&nbsp; <b>SHIFT</b> sprint</span>
      <span><b>LMB / R</b></span><span>fire / reload</span>
      <span><b>1 2 3 / WHEEL</b></span><span>switch weapon</span>
    </div>
    <button class="btn" id="btnResume">CLICK TO RESUME</button>
  </div>

  <div class="screen hide dead" id="scrDead">
    <div class="kicker">YOU DIDN'T MAKE IT TO MORNING</div>
    <div class="title" style="font-size:64px;color:#ff5a4a">DEAD AT<br/>THE STARLITE</div>
    <div class="stat">
      <div><div class="n" id="dWave">1</div><div class="k">WAVE REACHED</div></div>
      <div><div class="n" id="dKills">0</div><div class="k">KILLS</div></div>
      <div><div class="n" id="dHs">0</div><div class="k">HEADSHOTS</div></div>
      <div><div class="n" id="dScore">0</div><div class="k">SCORE</div></div>
    </div>
    <button class="btn" id="btnRetry">TRY AGAIN</button>
    <div class="hint">THE PAYPHONE IS STILL RINGING</div>
  </div>

  <div class="screen hide" id="scrWin">
    <div class="kicker" style="color:#ffd9a0">06:02 A.M. &nbsp;&middot;&nbsp; THE SKY IS COMING UP</div>
    <div class="title">YOU MADE<br/>IT to <em>DAWN</em></div>
    <div class="stat">
      <div><div class="n">10</div><div class="k">WAVES SURVIVED</div></div>
      <div><div class="n" id="wKills">0</div><div class="k">KILLS</div></div>
      <div><div class="n" id="wHs">0</div><div class="k">HEADSHOTS</div></div>
      <div><div class="n" id="wScore">0</div><div class="k">SCORE</div></div>
    </div>
    <button class="btn" id="btnAgain">PLAY AGAIN</button>
  </div>
</div>
<div id="err"></div>
<script>
'use strict';
/* =========================================================================
   LAST CALL AT THE STARLITE — single-file WebGL FPS
   ========================================================================= */
const $=id=>document.getElementById(id);
const errBox=$('err');
function showErr(e){ errBox.style.display='block'; errBox.textContent+=(e&&e.stack?e.stack:e)+'\n'; console.error(e); }
window.addEventListener('error',e=>showErr(e.error||e.message));
/* ------------------------------ MATH ------------------------------ */
function m4(){return new Float32Array([1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1]);}
function m4mul(a,b){const o=new Float32Array(16);
  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 m4chain(){let m=arguments[0];for(let i=1;i<arguments.length;i++)m=m4mul(m,arguments[i]);return m;}
function m4t(x,y,z){const m=m4();m[12]=x;m[13]=y;m[14]=z;return m;}
function m4s(x,y,z){const m=m4();m[0]=x;m[5]=y;m[10]=z;return m;}
function m4rx(a){const c=Math.cos(a),s=Math.sin(a),m=m4();m[5]=c;m[6]=s;m[9]=-s;m[10]=c;return m;}
function m4ry(a){const c=Math.cos(a),s=Math.sin(a),m=m4();m[0]=c;m[2]=-s;m[8]=s;m[10]=c;return m;}
function m4rz(a){const c=Math.cos(a),s=Math.sin(a),m=m4();m[0]=c;m[1]=s;m[4]=-s;m[5]=c;return m;}
function m4persp(fovy,asp,n,f){const t=1/Math.tan(fovy/2),m=new Float32Array(16);
  m[0]=t/asp;m[5]=t;m[10]=(f+n)/(n-f);m[11]=-1;m[14]=2*f*n/(n-f);return m;}
function clamp(v,a,b){return v<a?a:(v>b?b:v);}
function lerp(a,b,t){return a+(b-a)*t;}
function rnd(a,b){return a+Math.random()*(b-a);}
function rndi(a,b){return Math.floor(rnd(a,b+1));}
function dist2(ax,az,bx,bz){const dx=ax-bx,dz=az-bz;return dx*dx+dz*dz;}
const TAU=Math.PI*2;

/* ------------------------------ GL ------------------------------ */
const canvas=$('c');
const gl=canvas.getContext('webgl',{antialias:true,alpha:false,powerPreference:'high-performance'})
       ||canvas.getContext('experimental-webgl');
if(!gl){showErr('WebGL unavailable');}
const MAX_L=gl?Math.min(8,Math.max(4,Math.floor((gl.getParameter(gl.MAX_FRAGMENT_UNIFORM_VECTORS)-22)/3))):4;

function compile(src,type){const s=gl.createShader(type);gl.shaderSource(s,src);gl.compileShader(s);
  if(!gl.getShaderParameter(s,gl.COMPILE_STATUS))showErr('shader: '+gl.getShaderInfoLog(s)+'\n'+src);return s;}
function program(vs,fs){const p=gl.createProgram();gl.attachShader(p,compile(vs,gl.VERTEX_SHADER));
  gl.attachShader(p,compile(fs,gl.FRAGMENT_SHADER));gl.linkProgram(p);
  if(!gl.getProgramParameter(p,gl.LINK_STATUS))showErr('link: '+gl.getProgramInfoLog(p));return p;}

/* --------------------------- WORLD SHADER --------------------------- */
const VS=`
attribute vec3 aPos; attribute vec3 aNrm; attribute vec2 aUV; attribute vec3 aCol; attribute float aEmi;
uniform mat4 uProj,uView,uModel;
varying vec3 vN,vW,vC; varying vec2 vUV; varying float vE;
void main(){ vec4 w=uModel*vec4(aPos,1.0); vW=w.xyz; vN=mat3(uModel)*aNrm; vUV=aUV; vC=aCol; vE=aEmi;
  gl_Position=uProj*uView*w; }`;
const FS=`
precision mediump float;
varying vec3 vN,vW,vC; varying vec2 vUV; varying float vE;
uniform sampler2D uTex;
uniform vec4 uLP[${MAX_L}]; uniform vec3 uLD[${MAX_L}]; uniform vec3 uLC[${MAX_L}];
uniform int uLN;
uniform vec3 uAmb,uFog,uCam; uniform float uFogD,uEmisM,uEmisA,uTintM,uAlpha;
uniform vec3 uTint;
void main(){
  vec4 t=texture2D(uTex,vUV);
  if(t.a<0.14) discard;
  vec3 base=t.rgb*mix(vC,uTint,uTintM);
  vec3 n=normalize(vN);
  vec3 lit=uAmb;
  for(int i=0;i<${MAX_L};i++){
    if(i>=uLN) break;
    vec3 d=uLP[i].xyz-vW; float dd=length(d); vec3 dn=d/max(dd,0.001);
    float att=1.0/(1.0+0.045*dd+0.0075*dd*dd);
    if(uLP[i].w>0.0){ float cs=dot(-dn,uLD[i]); att*=smoothstep(uLP[i].w,1.0,cs); }
    float nd=max(dot(n,dn),0.0);
    lit+=uLC[i]*att*nd;
  }
  vec3 col=base*lit + base*(vE*uEmisM) + base*uEmisA;
  float fd=distance(vW,uCam);
  float f=1.0-exp(-uFogD*fd*fd);
  col=mix(col,uFog,clamp(f,0.0,1.0));
  gl_FragColor=vec4(col,t.a*uAlpha);
}`;
const PROG=program(VS,FS);
const U={};
['uProj','uView','uModel','uTex','uLN','uAmb','uFog','uCam','uFogD','uEmisM','uEmisA','uTintM','uAlpha','uTint',
 'uLP','uLD','uLC'].forEach(k=>U[k]=gl.getUniformLocation(PROG,k));
const A={};
['aPos','aNrm','aUV','aCol','aEmi'].forEach(k=>A[k]=gl.getAttribLocation(PROG,k));

/* ----------------------------- SKY SHADER ----------------------------- */
const SVS=`attribute vec2 aP; varying vec2 vU; void main(){vU=aP*0.5+0.5; gl_Position=vec4(aP,0.0,1.0);}`;
const SFS=`precision mediump float;
varying vec2 vU; uniform vec3 uCamR,uCamU,uCamF; uniform float uTan,uAsp,uDawn,uT;
float h31(vec3 p){return fract(sin(dot(p,vec3(12.9898,78.233,37.719)))*43758.5453);}
void main(){
  vec2 nd=vU*2.0-1.0;
  vec3 dir=normalize(uCamR*(nd.x*uTan*uAsp)+uCamU*(nd.y*uTan)+uCamF);
  float up=clamp(dir.y*1.6+0.16,0.0,1.0);
  vec3 nightT=vec3(0.012,0.017,0.035), nightH=vec3(0.055,0.070,0.095);
  vec3 dawnT=vec3(0.24,0.34,0.47), dawnH=vec3(0.93,0.60,0.36);
  vec3 top=mix(nightT,dawnT,uDawn), hor=mix(nightH,dawnH,uDawn);
  vec3 col=mix(hor,top,pow(up,0.75));
  // stars
  if(uDawn<0.95){
    vec3 g=floor(dir*140.0); float s=h31(g);
    float star=smoothstep(0.9975,1.0,s)*pow(max(dir.y,0.0),0.4);
    float tw=0.6+0.4*sin(uT*3.0+s*90.0);
    col+=vec3(0.85,0.9,1.0)*star*tw*(1.0-uDawn)*1.4;
  }
  // dawn sun near horizon
  if(uDawn>0.55){
    vec3 sd=normalize(vec3(0.42, (uDawn-0.62)*0.55, 0.86));
    float a=max(dot(dir,sd),0.0);
    col+=vec3(1.0,0.62,0.28)*pow(a,90.0)*1.6*step(0.6,uDawn);
    col+=vec3(1.0,0.55,0.25)*pow(a,7.0)*0.55*step(0.6,uDawn);
  }
  // moon
  vec3 md=normalize(vec3(-0.5,0.42,-0.75));
  float ma=max(dot(dir,md),0.0);
  col+=vec3(0.72,0.78,0.9)*pow(ma,1400.0)*(1.0-uDawn)*0.9;
  col+=vec3(0.35,0.42,0.55)*pow(ma,60.0)*(1.0-uDawn)*0.10;
  gl_FragColor=vec4(col,1.0);
}`;
const SKY=program(SVS,SFS);
const SU={};['uCamR','uCamU','uCamF','uTan','uAsp','uDawn','uT'].forEach(k=>SU[k]=gl.getUniformLocation(SKY,k));
const SA_P=gl.getAttribLocation(SKY,'aP');
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);

gl.enable(gl.DEPTH_TEST);
gl.enable(gl.CULL_FACE);
gl.cullFace(gl.BACK);
gl.clearColor(0.02,0.03,0.05,1);
/* ========================= TEXTURE ATLAS ========================= */
const ATLAS=1024,TS=128,GN=8;
const actx=document.createElement('canvas'); actx.width=actx.height=ATLAS;
const TX=actx.getContext('2d');
const TILES={}; let tcount=0;
function T(name,fn){const i=tcount++;TILES[name]=i;
  TX.save();TX.translate((i%GN)*TS,Math.floor(i/GN)*TS);TX.beginPath();TX.rect(0,0,TS,TS);TX.clip();
  try{fn(TX);}catch(e){showErr('tile '+name+': '+e);}
  TX.restore();return i;}
function tuv(name){const i=TILES[name]||0,x=(i%GN)*TS,y=Math.floor(i/GN)*TS,p=1.6;
  return [(x+p)/ATLAS,(y+p)/ATLAS,(x+TS-p)/ATLAS,(y+TS-p)/ATLAS];}
function speck(c,n,col,a){for(let i=0;i<n;i++){c.globalAlpha=a*Math.random();c.fillStyle=col;
  const s=1+Math.random()*3.4;c.fillRect(Math.random()*TS,Math.random()*TS,s,s);}c.globalAlpha=1;}
function blotch(c,n,col,a,rmax){for(let i=0;i<n;i++){c.globalAlpha=a*Math.random();c.fillStyle=col;
  c.beginPath();c.arc(Math.random()*TS,Math.random()*TS,2+Math.random()*rmax,0,TAU);c.fill();}c.globalAlpha=1;}
function cracks(c,n,col){c.strokeStyle=col;c.lineWidth=1;
  for(let i=0;i<n;i++){let x=Math.random()*TS,y=Math.random()*TS;c.beginPath();c.moveTo(x,y);
    for(let j=0;j<5;j++){x+=rnd(-18,18);y+=rnd(-18,18);c.lineTo(x,y);}c.stroke();}}
function neonText(c,txt,y,size,col,blur){
  c.font='900 '+size+'px "Arial Black",Impact,sans-serif';c.textAlign='center';c.textBaseline='middle';
  c.shadowColor=col;c.shadowBlur=blur||16;c.fillStyle=col;c.fillText(txt,TS/2,y);
  c.shadowBlur=0;c.fillStyle='rgba(255,255,255,.85)';c.font='900 '+(size*0.5)+'px "Arial Black",sans-serif';
  c.globalAlpha=.5;c.fillText(txt,TS/2,y-size*0.04);c.globalAlpha=1;}

T('white',c=>{c.fillStyle='#fff';c.fillRect(0,0,TS,TS);speck(c,140,'#eee',.5);});
T('asphalt',c=>{c.fillStyle='#23252a';c.fillRect(0,0,TS,TS);speck(c,900,'#3a3d43',.8);
  speck(c,500,'#15161a',.9);blotch(c,14,'#191b1f',.7,14);cracks(c,7,'#101114');
  blotch(c,6,'#31343a',.5,10);});
T('concrete',c=>{c.fillStyle='#63625c';c.fillRect(0,0,TS,TS);speck(c,700,'#787770',.7);
  speck(c,400,'#4a4944',.8);blotch(c,10,'#55544e',.6,16);cracks(c,4,'#3d3c38');});
T('brick',c=>{c.fillStyle='#5d544a';c.fillRect(0,0,TS,TS);
  for(let r=0;r<8;r++){for(let i=0;i<4;i++){const off=(r%2)?16:0;
    c.fillStyle=['#7b4636','#713f31','#834d3b','#6b3d2f'][(r+i)%4];
    c.fillRect(i*32+off+1.5,r*16+1.5,29,13);}}
  speck(c,300,'#00000022',.6);blotch(c,8,'#4b3a30',.5,12);});
T('stucco',c=>{c.fillStyle='#a89b85';c.fillRect(0,0,TS,TS);speck(c,900,'#b8ab95',.7);
  speck(c,500,'#8d8270',.7);for(let i=0;i<10;i++){c.globalAlpha=.16;c.fillStyle='#6b6355';
  c.fillRect(Math.random()*TS,0,2+Math.random()*5,TS);}c.globalAlpha=1;blotch(c,7,'#7d7365',.5,16);});
T('roof',c=>{c.fillStyle='#2a2b2e';c.fillRect(0,0,TS,TS);speck(c,1100,'#3a3c40',.9);
  speck(c,600,'#1b1c1f',.9);});
T('carpet',c=>{c.fillStyle='#54382f';c.fillRect(0,0,TS,TS);speck(c,1400,'#63443a',.8);
  speck(c,700,'#3e281f',.8);blotch(c,9,'#2e1d17',.5,20);});
T('dinerfloor',c=>{c.fillStyle='#e8e4da';c.fillRect(0,0,TS,TS);
  for(let y=0;y<4;y++)for(let x=0;x<4;x++){if((x+y)%2){c.fillStyle='#2b2b30';c.fillRect(x*32,y*32,32,32);}}
  c.strokeStyle='#8d8a82';c.lineWidth=1;for(let i=0;i<=4;i++){c.beginPath();c.moveTo(i*32,0);c.lineTo(i*32,TS);c.stroke();
    c.beginPath();c.moveTo(0,i*32);c.lineTo(TS,i*32);c.stroke();}
  speck(c,200,'#00000022',.6);});
T('ceil',c=>{c.fillStyle='#c9c9c2';c.fillRect(0,0,TS,TS);speck(c,400,'#b5b5ae',.7);
  c.strokeStyle='#9a9a92';c.lineWidth=2;for(let i=0;i<=2;i++){c.beginPath();c.moveTo(i*64,0);c.lineTo(i*64,TS);c.stroke();
    c.beginPath();c.moveTo(0,i*64);c.lineTo(TS,i*64);c.stroke();}
  c.fillStyle='#8e8e86';for(let i=0;i<4;i++)c.fillRect(10+i*8,96,4,22);});
T('wallpaper',c=>{c.fillStyle='#b7a98d';c.fillRect(0,0,TS,TS);
  for(let i=0;i<16;i++){c.fillStyle=i%2?'#a89a7e':'#c0b298';c.fillRect(i*8,0,4,TS);}
  c.fillStyle='#6f5f4a';c.fillRect(0,96,TS,4);speck(c,250,'#8d8069',.6);});
T('tilewall',c=>{c.fillStyle='#dcdcd6';c.fillRect(0,0,TS,TS);
  c.fillStyle='#b9b9b1';for(let r=0;r<8;r++)for(let i=0;i<5;i++){const o=(r%2)?13:0;
    c.fillRect(i*26+o+1,r*16+1,24,14);}
  speck(c,160,'#00000018',.7);});
T('woodfloor',c=>{c.fillStyle='#7a5636';c.fillRect(0,0,TS,TS);
  for(let r=0;r<8;r++){c.fillStyle=['#835c3a','#6f4c30','#8b6441','#775334'][r%4];c.fillRect(0,r*16,TS,15);
    c.strokeStyle='#5a3d26';c.lineWidth=1;c.beginPath();c.moveTo(0,r*16+15.5);c.lineTo(TS,r*16+15.5);c.stroke();
    for(let g=0;g<14;g++){c.globalAlpha=.25;c.strokeStyle='#4e351f';c.beginPath();
      const y=r*16+Math.random()*14;c.moveTo(0,y);c.lineTo(TS,y+rnd(-3,3));c.stroke();}c.globalAlpha=1;}});
const DOORNUM=['101','102','103','104','105','106'];
DOORNUM.forEach((num,k)=>T('door'+k,c=>{
  const base=['#6d452c','#7a4e30','#5f3d28','#7f5535','#68432a','#75502f'][k];
  c.fillStyle=base;c.fillRect(0,0,TS,TS);
  for(let i=0;i<6;i++){c.strokeStyle='rgba(0,0,0,.35)';c.lineWidth=1.4;
    c.beginPath();c.moveTo(0,i*22);c.lineTo(TS,i*22+rnd(-2,2));c.stroke();}
  c.fillStyle='rgba(0,0,0,.28)';c.fillRect(14,14,TS-28,44);c.fillRect(14,74,TS-28,42);
  c.fillStyle='#c9a24a';c.fillRect(34,26,60,26);
  c.fillStyle='#241a10';c.font='bold 20px Georgia,serif';c.textAlign='center';c.textBaseline='middle';
  c.fillText(num,64,40);
  c.fillStyle='#d8b45c';c.beginPath();c.arc(TS-24,74,7,0,TAU);c.fill();
  c.fillStyle='#8a6a2a';c.beginPath();c.arc(TS-24,74,3,0,TAU);c.fill();
  speck(c,220,'rgba(0,0,0,.2)',.7);}));
T('poolfloor',c=>{c.fillStyle='#8f9086';c.fillRect(0,0,TS,TS);speck(c,700,'#9d9e93',.7);
  speck(c,500,'#75766d',.8);blotch(c,12,'#6c6d63',.6,18);
  for(let i=0;i<26;i++){c.save();c.translate(Math.random()*TS,Math.random()*TS);
    c.rotate(Math.random()*TAU);c.globalAlpha=.85;
    c.fillStyle=['#6d6134','#4f5a2c','#7a5a2a','#5b4a25'][i%4];
    c.beginPath();c.ellipse(0,0,rnd(3,7),rnd(1.6,3.4),0,0,TAU);c.fill();c.restore();}
  c.globalAlpha=1;for(let i=0;i<7;i++){c.fillStyle='#5d5e55';c.fillRect(Math.random()*TS,Math.random()*TS,rnd(4,14),rnd(2,5));}
  cracks(c,6,'#5a5b52');});
T('pooltile',c=>{c.fillStyle='#3f7f9a';c.fillRect(0,0,TS,TS);
  for(let y=0;y<8;y++)for(let x=0;x<8;x++){c.fillStyle=(x+y)%2?'#4f93ad':'#58a0bb';c.fillRect(x*16+1,y*16+1,14,14);}
  speck(c,200,'#ffffff22',.6);});
T('chain',c=>{c.clearRect(0,0,TS,TS);c.strokeStyle='rgba(178,186,192,.95)';c.lineWidth=2.4;
  for(let i=-TS;i<TS*2;i+=16){c.beginPath();c.moveTo(i,0);c.lineTo(i+TS,TS);c.stroke();
    c.beginPath();c.moveTo(i,TS);c.lineTo(i+TS,0);c.stroke();}
  c.strokeStyle='rgba(120,128,134,.55)';c.lineWidth=1;
  for(let i=-TS;i<TS*2;i+=16){c.beginPath();c.moveTo(i+4,0);c.lineTo(i+TS+4,TS);c.stroke();}});
T('foliage',c=>{c.clearRect(0,0,TS,TS);
  for(let i=0;i<180;i++){const x=Math.random()*TS,y=Math.random()*TS;
    c.fillStyle=['#0d1a12','#122416','#0a1410','#16301c','#0f2015'][i%5];
    c.beginPath();c.ellipse(x,y,rnd(4,13),rnd(3,9),Math.random()*TAU,0,TAU);c.fill();}});
const LETS=['S','T','A','R','L','I','T','E'];
LETS.forEach((ch,k)=>T('let'+k,c=>{c.clearRect(0,0,TS,TS);neonText(c,ch,TS/2,104,'#ff4f9b',26);
  c.globalAlpha=.9;neonText(c,ch,TS/2,104,'#ff9ecb',10);c.globalAlpha=1;}));
T('star',c=>{c.clearRect(0,0,TS,TS);c.save();c.translate(TS/2,TS/2);
  c.shadowColor='#49d8ff';c.shadowBlur=24;c.strokeStyle='#8fe9ff';c.lineWidth=7;c.lineJoin='round';
  c.beginPath();for(let i=0;i<10;i++){const a=-Math.PI/2+i*Math.PI/5,r=i%2?18:48;
    c[i?'lineTo':'moveTo'](Math.cos(a)*r,Math.sin(a)*r);}c.closePath();c.stroke();
  c.fillStyle='rgba(180,245,255,.5)';c.fill();c.restore();});
T('no',c=>{c.clearRect(0,0,TS,TS);
  c.font='900 74px "Arial Black",Impact,sans-serif';c.textAlign='center';c.textBaseline='middle';
  c.fillStyle='#3a1418';c.fillText('NO',TS/2,TS/2);c.strokeStyle='#5c2026';c.lineWidth=2;
  c.strokeText('NO',TS/2,TS/2);});
T('vac',c=>{c.clearRect(0,0,TS,TS);neonText(c,'VACANCY',TS/2,44,'#ff2b4a',22);});
T('static',c=>{c.fillStyle='#787878';c.fillRect(0,0,TS,TS);});
T('vending',c=>{c.fillStyle='#1b1c22';c.fillRect(0,0,TS,TS);
  const g=c.createLinearGradient(0,0,0,TS);g.addColorStop(0,'#ff3b30');g.addColorStop(.5,'#e02b22');g.addColorStop(1,'#8f1a14');
  c.fillStyle=g;c.fillRect(6,6,TS-12,TS-12);
  c.fillStyle='#ffffff';c.font='900 26px "Arial Black",sans-serif';c.textAlign='center';
  c.fillText('COLA',TS/2,34);
  for(let r=0;r<3;r++)for(let i=0;i<4;i++){c.fillStyle='rgba(255,255,255,.85)';
    c.fillRect(14+i*26,50+r*22,20,16);c.fillStyle='rgba(0,0,0,.35)';c.fillRect(14+i*26,50+r*22,20,4);}
  c.fillStyle='#15161a';c.fillRect(14,TS-30,TS-28,20);
  c.fillStyle='#ffd45e';c.fillRect(TS-34,TS-34,20,10);
  speck(c,120,'#ffffff33',.7);});
T('iceface',c=>{c.fillStyle='#dfe6ea';c.fillRect(0,0,TS,TS);
  c.fillStyle='#2f6fb0';c.fillRect(0,0,TS,34);
  c.fillStyle='#fff';c.font='900 24px "Arial Black",sans-serif';c.textAlign='center';c.fillText('ICE',TS/2,25);
  c.fillStyle='#b9c4cb';for(let i=0;i<9;i++)c.fillRect(14,46+i*8,TS-28,4);
  c.fillStyle='#8c979e';c.fillRect(14,TS-30,TS-28,20);
  speck(c,140,'#00000018',.7);});
T('keybd',c=>{c.fillStyle='#4a3320';c.fillRect(0,0,TS,TS);
  c.fillStyle='#3a2718';c.fillRect(6,6,TS-12,TS-12);
  c.fillStyle='#c9a24a';c.font='bold 13px Georgia,serif';c.textAlign='center';
  c.fillText('ROOM KEYS',TS/2,22);
  for(let r=0;r<3;r++)for(let i=0;i<5;i++){const x=18+i*23,y=44+r*28;
    c.strokeStyle='#b8944a';c.lineWidth=2;c.beginPath();c.moveTo(x,y);c.lineTo(x,y+9);c.stroke();
    c.fillStyle=(r*5+i)%4?'#d8c07a':'#9a7b3a';c.beginPath();c.arc(x,y+13,5,0,TAU);c.fill();
    c.fillStyle='#2a1d12';c.font='7px monospace';c.fillText(String(101+r*5+i),x,y+15);}});
T('menu',c=>{c.fillStyle='#171a1c';c.fillRect(0,0,TS,TS);
  c.fillStyle='#ffd45e';c.font='900 22px "Arial Black",sans-serif';c.textAlign='center';
  c.fillText('TONIGHT',TS/2,26);
  c.strokeStyle='#ffd45e';c.lineWidth=1;c.beginPath();c.moveTo(14,34);c.lineTo(TS-14,34);c.stroke();
  const items=['CHILI .......... 65c','APPLE PIE ....... 40c','COFFEE .......... 10c','BLT ............. 55c','MILK SHAKE ...... 45c'];
  c.textAlign='left';c.fillStyle='#e8e2d2';c.font='11px monospace';
  items.forEach((s,i)=>c.fillText(s,14,54+i*16));
  c.fillStyle='#8fd8ff';c.font='9px monospace';c.fillText('OPEN ALL NIGHT',14,TS-14);});
T('news',c=>{c.fillStyle='#cfc9b8';c.fillRect(0,0,TS,TS);
  c.fillStyle='#20242a';c.font='900 17px Georgia,serif';c.textAlign='left';
  c.fillText('ROUTE 9 GAZETTE',6,20);
  c.fillStyle='#3a3f46';c.font='900 13px Georgia,serif';c.fillText('POWER CUTS SPREAD',6,38);
  c.fillStyle='#5a5f66';c.font='8px serif';
  for(let i=0;i<13;i++)c.fillRect(6,48+i*6,TS-12-rnd(0,26),2.4);
  c.fillStyle='#7a7f86';c.fillRect(78,50,44,34);speck(c,200,'#00000022',.6);});
T('metal',c=>{const g=c.createLinearGradient(0,0,0,TS);g.addColorStop(0,'#9aa1a7');g.addColorStop(.5,'#7d858c');
  g.addColorStop(1,'#8f969c');c.fillStyle=g;c.fillRect(0,0,TS,TS);
  for(let i=0;i<160;i++){c.globalAlpha=.12;c.strokeStyle=Math.random()>.5?'#fff':'#000';
    c.beginPath();const y=Math.random()*TS;c.moveTo(0,y);c.lineTo(TS,y);c.stroke();}c.globalAlpha=1;});
T('glass',c=>{c.fillStyle='rgba(150,190,215,.30)';c.fillRect(0,0,TS,TS);
  c.fillStyle='rgba(255,255,255,.20)';c.beginPath();c.moveTo(10,TS);c.lineTo(70,0);c.lineTo(96,0);c.lineTo(36,TS);
  c.closePath();c.fill();c.fillStyle='rgba(255,255,255,.10)';c.fillRect(0,0,TS,3);});
T('open24',c=>{c.clearRect(0,0,TS,TS);neonText(c,'OPEN',TS/2,54,'#38e8ff',24);
  c.font='900 22px "Arial Black",sans-serif';c.textAlign='center';c.shadowColor='#ff3b5c';
  c.shadowBlur=18;c.fillStyle='#ff8fa4';c.fillText('24 HRS',TS/2,100);c.shadowBlur=0;});
T('notice',c=>{c.fillStyle='#e9e4d6';c.fillRect(0,0,TS,TS);
  c.fillStyle='#2a2f36';c.font='900 15px Georgia,serif';c.textAlign='center';
  c.fillText('MOTOR COURT',TS/2,22);c.fillText('RULES',TS/2,40);
  c.strokeStyle='#8a8578';c.beginPath();c.moveTo(12,48);c.lineTo(TS-12,48);c.stroke();
  c.textAlign='left';c.fillStyle='#4a4f56';c.font='9px serif';
  ['QUIET AFTER 10 P.M.','NO SWIMMING AFTER DARK','POOL IS DRAINED','KEYS AT OFFICE',
   'DO NOT FEED THE','RACCOONS'].forEach((s,i)=>c.fillText('• '+s,12,64+i*13));
  speck(c,120,'#00000018',.6);});
T('blood',c=>{c.clearRect(0,0,TS,TS);
  for(let i=0;i<40;i++){const x=TS/2+rnd(-42,42),y=TS/2+rnd(-42,42);
    c.globalAlpha=rnd(.35,.9);c.fillStyle=['#5a0d0d','#6d1212','#420808'][i%3];
    c.beginPath();c.ellipse(x,y,rnd(2,16),rnd(2,13),Math.random()*TAU,0,TAU);c.fill();}
  c.globalAlpha=1;});
T('weeds',c=>{c.clearRect(0,0,TS,TS);
  for(let i=0;i<48;i++){const x=Math.random()*TS;
    c.strokeStyle=['#3c4a2a','#4d5c33','#2f3b22'][i%3];c.lineWidth=rnd(1,2.6);
    c.beginPath();c.moveTo(x,TS);c.quadraticCurveTo(x+rnd(-10,10),TS*rnd(.4,.7),x+rnd(-16,16),rnd(4,50));c.stroke();}});
T('hazard',c=>{c.fillStyle='#f2c518';c.fillRect(0,0,TS,TS);
  c.strokeStyle='#17181c';c.lineWidth=14;
  for(let i=-TS;i<TS*2;i+=34){c.beginPath();c.moveTo(i,0);c.lineTo(i+TS,TS);c.stroke();}
  speck(c,120,'#00000022',.6);});
T('grass',c=>{c.fillStyle='#232c1c';c.fillRect(0,0,TS,TS);speck(c,900,'#2d3a23',.9);
  speck(c,600,'#1a2116',.9);speck(c,250,'#3b4a2c',.7);});
T('gravel',c=>{c.fillStyle='#4a483f';c.fillRect(0,0,TS,TS);
  for(let i=0;i<300;i++){c.fillStyle=['#5a584d','#3b3a33','#6a675b','#2f2e28'][i%4];
    c.beginPath();c.arc(Math.random()*TS,Math.random()*TS,rnd(1.5,5),0,TAU);c.fill();}});
T('cloth',c=>{c.fillStyle='#b8434a';c.fillRect(0,0,TS,TS);
  for(let i=0;i<10;i++){c.fillStyle=['#c9505a','#8f3a41','#d9636b'][i%3];c.fillRect(0,i*13,TS,7);}
  speck(c,300,'#00000022',.7);});
T('clockface',c=>{c.fillStyle='#efeae0';c.beginPath();c.arc(TS/2,TS/2,60,0,TAU);c.fill();
  c.strokeStyle='#2a2f36';c.lineWidth=3;c.stroke();
  c.fillStyle='#2a2f36';for(let i=0;i<12;i++){const a=i/12*TAU;
    c.beginPath();c.arc(TS/2+Math.cos(a)*48,TS/2+Math.sin(a)*48,3,0,TAU);c.fill();}
  c.lineWidth=4;c.beginPath();c.moveTo(TS/2,TS/2);c.lineTo(TS/2+22,TS/2-16);c.stroke();
  c.lineWidth=3;c.beginPath();c.moveTo(TS/2,TS/2);c.lineTo(TS/2-8,TS/2-40);c.stroke();});
T('rust',c=>{c.fillStyle='#6b4a34';c.fillRect(0,0,TS,TS);speck(c,700,'#8a5b38',.8);
  speck(c,500,'#4a3222',.8);blotch(c,14,'#7d4b2c',.7,16);});
T('darkpanel',c=>{c.fillStyle='#111318';c.fillRect(0,0,TS,TS);speck(c,300,'#1a1d24',.8);});
T('dinerwall',c=>{c.fillStyle='#e6ddcb';c.fillRect(0,0,TS,TS);
  c.fillStyle='#b8342e';c.fillRect(0,72,TS,44);
  c.fillStyle='#8f2a25';c.fillRect(0,72,TS,5);c.fillRect(0,111,TS,5);
  speck(c,200,'#00000018',.6);});
T('officefloor',c=>{c.fillStyle='#b9ad93';c.fillRect(0,0,TS,TS);
  for(let y=0;y<4;y++)for(let x=0;x<4;x++){c.fillStyle=(x+y)%2?'#c4b79c':'#a89c85';
    c.fillRect(x*32,y*32,32,32);}
  speck(c,300,'#00000020',.6);blotch(c,6,'#8e836e',.5,16);});
T('paper',c=>{c.fillStyle='#f2eee4';c.fillRect(0,0,TS,TS);
  c.strokeStyle='#b9c8d8';c.lineWidth=1;for(let i=1;i<14;i++){c.beginPath();c.moveTo(8,i*9);
    c.lineTo(TS-8,i*9);c.stroke();}
  c.fillStyle='#3a4048';c.font='9px serif';c.fillText('STARLITE MOTOR COURT — GUEST LEDGER',8,10);});
T('line',c=>{c.clearRect(0,0,TS,TS);
  for(let i=0;i<70;i++){c.globalAlpha=rnd(.35,1);c.fillStyle='#d8d4c4';
    c.fillRect(rnd(0,TS),rnd(0,TS),rnd(3,14),rnd(3,12));}c.globalAlpha=1;});
T('window',c=>{const g=c.createLinearGradient(0,0,TS,TS);
  g.addColorStop(0,'#1b2836');g.addColorStop(.5,'#0d141c');g.addColorStop(1,'#243444');
  c.fillStyle=g;c.fillRect(0,0,TS,TS);
  c.fillStyle='rgba(160,200,235,.14)';c.beginPath();c.moveTo(0,TS);c.lineTo(74,0);c.lineTo(104,0);
  c.lineTo(30,TS);c.closePath();c.fill();
  c.fillStyle='rgba(255,255,255,.06)';c.fillRect(0,0,TS,2);});
T('towel',c=>{c.fillStyle='#dcd6c8';c.fillRect(0,0,TS,TS);speck(c,400,'#c9c3b5',.8);
  c.fillStyle='#9fb7c9';c.fillRect(0,20,TS,10);c.fillRect(0,96,TS,10);});
T('sign2',c=>{c.clearRect(0,0,TS,TS);
  c.font='900 30px "Arial Black",Impact,sans-serif';c.textAlign='center';c.textBaseline='middle';
  c.shadowColor='#ff7ac0';c.shadowBlur=14;c.fillStyle='#ffd9ef';
  c.fillText('MOTOR',TS/2,44);c.fillText('COURT',TS/2,88);c.shadowBlur=0;});

const atlasTex=gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D,atlasTex);
gl.texImage2D(gl.TEXTURE_2D,0,gl.RGBA,gl.RGBA,gl.UNSIGNED_BYTE,actx);
gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_WRAP_S,gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_WRAP_T,gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_MIN_FILTER,gl.LINEAR_MIPMAP_LINEAR);
gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_MAG_FILTER,gl.LINEAR);
gl.generateMipmap(gl.TEXTURE_2D);
const STAT_TILE=TILES['static'];
const sctx=document.createElement('canvas'); sctx.width=sctx.height=TS;
const sSX=sctx.getContext('2d');
function updateStatic(){
  const img=sSX.createImageData(TS,TS);const d=img.data;
  for(let i=0;i<d.length;i+=4){const v=(Math.random()*255)|0;
    const band=((i/4/TS)|0)%3===0?0.72:1;
    d[i]=d[i+1]=d[i+2]=v*band;d[i+3]=255;}
  sSX.putImageData(img,0,0);
  gl.bindTexture(gl.TEXTURE_2D,atlasTex);
  gl.texSubImage2D(gl.TEXTURE_2D,0,(STAT_TILE%GN)*TS,Math.floor(STAT_TILE/GN)*TS,
    gl.RGBA,gl.UNSIGNED_BYTE,sctx);
  gl.generateMipmap(gl.TEXTURE_2D);
}
updateStatic();

/* ========================= GEOMETRY BUILDERS ========================= */
const FACES=[
 {n:[1,0,0], v:[[1,-1,1],[1,-1,-1],[1,1,-1],[1,1,1]]},
 {n:[-1,0,0],v:[[-1,-1,-1],[-1,-1,1],[-1,1,1],[-1,1,-1]]},
 {n:[0,1,0], v:[[-1,1,1],[1,1,1],[1,1,-1],[-1,1,-1]]},
 {n:[0,-1,0],v:[[-1,-1,-1],[1,-1,-1],[1,-1,1],[-1,-1,1]]},
 {n:[0,0,1], v:[[-1,-1,1],[1,-1,1],[1,1,1],[-1,1,1]]},
 {n:[0,0,-1],v:[[1,-1,-1],[-1,-1,-1],[-1,1,-1],[1,1,-1]]}];
function rot3(v,rx,ry,rz){
  let x=v[0],y=v[1],z=v[2],c,s,t;
  if(rx){c=Math.cos(rx);s=Math.sin(rx);t=y*c-z*s;z=y*s+z*c;y=t;}
  if(ry){c=Math.cos(ry);s=Math.sin(ry);t=x*c+z*s;z=-x*s+z*c;x=t;}
  if(rz){c=Math.cos(rz);s=Math.sin(rz);t=x*c-y*s;y=x*s+y*c;x=t;}
  return [x,y,z];}

function Builder(){this.p=[];this.n=[];this.t=[];this.c=[];this.e=[];this.idx=[];this.v=0;this.meshes=[];}
Builder.prototype.flush=function(){
  if(this.v>0){this.meshes.push(makeMesh(this.p,this.n,this.t,this.c,this.e,this.idx));}
  this.p=[];this.n=[];this.t=[];this.c=[];this.e=[];this.idx=[];this.v=0;};
Builder.prototype.quad=function(a,b,d,e,o){
  o=o||{};const col=o.c||[1,1,1],uv=o.uv||tuv(o.tile||'white'),em=o.e||0;
  const u1=[b[0]-a[0],b[1]-a[1],b[2]-a[2]],u2=[d[0]-a[0],d[1]-a[1],d[2]-a[2]];
  let nx=u1[1]*u2[2]-u1[2]*u2[1],ny=u1[2]*u2[0]-u1[0]*u2[2],nz=u1[0]*u2[1]-u1[1]*u2[0];
  const L=Math.hypot(nx,ny,nz)||1;nx/=L;ny/=L;nz/=L;
  if(o.n){nx=o.n[0];ny=o.n[1];nz=o.n[2];}
  const pts=[a,b,d,e],us=[[0,1],[1,1],[1,0],[0,0]];
  const base=this.v;
  for(let i=0;i<4;i++){this.p.push(pts[i][0],pts[i][1],pts[i][2]);this.n.push(nx,ny,nz);
    const uu=us[i];
    this.t.push(uv[0]+(uv[2]-uv[0])*uu[0],uv[1]+(uv[3]-uv[1])*uu[1]);
    this.c.push(col[0],col[1],col[2]);this.e.push(em);this.v++;}
  this.idx.push(base,base+1,base+2,base,base+2,base+3);
  if(this.v>60000)this.flush();};
Builder.prototype.box=function(cx,cy,cz,sx,sy,sz,o){
  o=o||{};const col=o.c||[1,1,1],uv=o.uv||tuv(o.tile||'white'),em=o.e||0;
  const rx=o.rx||0,ry=o.ry||0,rz=o.rz||0,hx=sx/2,hy=sy/2,hz=sz/2;
  const faces=FACES;
  for(let f=0;f<6;f++){
    if(o.faces&&!o.faces[f])continue;
    const F=faces[f],n=rot3(F.n,rx,ry,rz),base=this.v;
    for(let i=0;i<4;i++){const lv=F.v[i];
      const p=rot3([lv[0]*hx,lv[1]*hy,lv[2]*hz],rx,ry,rz);
      this.p.push(cx+p[0],cy+p[1],cz+p[2]);this.n.push(n[0],n[1],n[2]);
      const uu=[[0,1],[1,1],[1,0],[0,0]][i];
      this.t.push(uv[0]+(uv[2]-uv[0])*uu[0],uv[1]+(uv[3]-uv[1])*uu[1]);
      this.c.push(col[0],col[1],col[2]);this.e.push(em);this.v++;}
    this.idx.push(base,base+1,base+2,base,base+2,base+3);
    if(this.v>60000)this.flush();}};
Builder.prototype.tri=function(a,b,d,o){
  o=o||{};const col=o.c||[1,1,1],uv=o.uv||tuv(o.tile||'white'),em=o.e||0;
  const u1=[b[0]-a[0],b[1]-a[1],b[2]-a[2]],u2=[d[0]-a[0],d[1]-a[1],d[2]-a[2]];
  let nx=u1[1]*u2[2]-u1[2]*u2[1],ny=u1[2]*u2[0]-u1[0]*u2[2],nz=u1[0]*u2[1]-u1[1]*u2[0];
  const L=Math.hypot(nx,ny,nz)||1;nx/=L;ny/=L;nz/=L;
  const pts=[a,b,d],us=[[0,1],[1,1],[0.5,0.5]],base=this.v;
  for(let i=0;i<3;i++){const p=pts[i],uu=us[i];
    this.p.push(p[0],p[1],p[2]);this.n.push(nx,ny,nz);
    this.t.push(uv[0]+(uv[2]-uv[0])*uu[0],uv[1]+(uv[3]-uv[1])*uu[1]);
    this.c.push(col[0],col[1],col[2]);this.e.push(em);this.v++;}
  this.idx.push(base,base+1,base+2);
  if(this.v>60000)this.flush();};
Builder.prototype.cyl=function(cx,cy,cz,r,h,seg,o){
  o=o||{};const col=o.c||[1,1,1],uv=o.uv||tuv(o.tile||'white'),em=o.e||0;
  const r2=(o.r2!==undefined)?o.r2:r, axis=o.axis||'y', cap=(o.cap!==false);
  const P=(a,yy,rr)=>{let x=Math.cos(a)*rr,y=yy,z=Math.sin(a)*rr;
    if(axis==='x'){const t=x;x=y;y=z;z=t;} else if(axis==='z'){const t=z;z=y;y=x;x=t;}
    if(o.ry){const c=Math.cos(o.ry),s=Math.sin(o.ry);const nx=x*c+z*s,nz=-x*s+z*c;x=nx;z=nz;}
    return [cx+x,cy+y,cz+z];};
  for(let i=0;i<seg;i++){
    const a0=i/seg*TAU,a1=(i+1)/seg*TAU;
    this.quad(P(a0,h/2,r2),P(a1,h/2,r2),P(a1,-h/2,r),P(a0,-h/2,r),{c:col,uv:uv,e:em});
    if(cap){
      this.tri(P(a1,h/2,r2),P(a0,h/2,r2),[cx,cy+h/2,cz],{c:col,uv:uv,e:em});
      this.tri(P(a0,-h/2,r),P(a1,-h/2,r),[cx,cy-h/2,cz],{c:col,uv:uv,e:em});
    }}
  if(this.v>60000)this.flush();};
Builder.prototype.mesh=function(){this.flush();return this.meshes;};

function makeMesh(p,n,t,c,e,idx){
  const m={bufs:[],count:idx.length};
  const mk=data=>{const b=gl.createBuffer();gl.bindBuffer(gl.ARRAY_BUFFER,b);
    gl.bufferData(gl.ARRAY_BUFFER,new Float32Array(data),gl.STATIC_DRAW);return b;};
  m.bufs.push(mk(p),mk(n),mk(t),mk(c),mk(e));
  m.ib=gl.createBuffer();gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER,m.ib);
  let data;
  if(idx.length&&Math.max.apply(null,idx)>65535){data=new Uint32Array(idx);m.idxType=gl.UNSIGNED_INT;}
  else{data=new Uint16Array(idx);m.idxType=gl.UNSIGNED_SHORT;}
  gl.bufferData(gl.ELEMENT_ARRAY_BUFFER,data,gl.STATIC_DRAW);
  return m;}
if(gl)gl.getExtension('OES_element_index_uint');

let curProg=null;
function useProg(){ if(curProg===PROG)return; curProg=PROG; gl.useProgram(PROG);
  gl.enableVertexAttribArray(A.aPos);gl.enableVertexAttribArray(A.aNrm);
  gl.enableVertexAttribArray(A.aUV);gl.enableVertexAttribArray(A.aCol);gl.enableVertexAttribArray(A.aEmi);
  gl.uniform1i(U.uTex,0); gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D,atlasTex);}
function drawMesh(m,model,tint,tintM,emisA,emisM,alpha){
  if(!m||!m.count)return;
  useProg();
  const B=m.bufs;
  gl.bindBuffer(gl.ARRAY_BUFFER,B[0]);gl.vertexAttribPointer(A.aPos,3,gl.FLOAT,false,0,0);
  gl.bindBuffer(gl.ARRAY_BUFFER,B[1]);gl.vertexAttribPointer(A.aNrm,3,gl.FLOAT,false,0,0);
  gl.bindBuffer(gl.ARRAY_BUFFER,B[2]);gl.vertexAttribPointer(A.aUV,2,gl.FLOAT,false,0,0);
  gl.bindBuffer(gl.ARRAY_BUFFER,B[3]);gl.vertexAttribPointer(A.aCol,3,gl.FLOAT,false,0,0);
  gl.bindBuffer(gl.ARRAY_BUFFER,B[4]);gl.vertexAttribPointer(A.aEmi,1,gl.FLOAT,false,0,0);
  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER,m.ib);
  gl.uniformMatrix4fv(U.uModel,false,model||M4I);
  if(tint){gl.uniform3f(U.uTint,tint[0],tint[1],tint[2]);}
  gl.uniform1f(U.uTintM,tintM?1:0);
  gl.uniform1f(U.uEmisA,emisA||0);
  gl.uniform1f(U.uEmisM,emisM===undefined?1:emisM);
  gl.uniform1f(U.uAlpha,alpha===undefined?1:alpha);
  gl.drawElements(gl.TRIANGLES,m.count,m.idxType,0);}
const M4I=m4();

/* ===================== WORLD DATA / COLLISION ===================== */
const COLL=[];      // {x0,x1,z0,z1,y0,y1,block,walk}
const LIGHTS=[];    // {p:[x,y,z], c:[r,g,b], i:intensity, spot:{d:[x,y,z],cos}|null, mod:string, on:true}
const POOL={x0:-32,x1:-8,z0:8,z1:24,f:-2.6};
function inR(r,x,z){return x>=r.x0&&x<=r.x1&&z>=r.z0&&z<=r.z1;}
function addCol(x0,x1,z0,z1,y0,y1,o){o=o||{};
  const c={x0:Math.min(x0,x1),x1:Math.max(x0,x1),z0:Math.min(z0,z1),z1:Math.max(z0,z1),
    y0:y0,y1:y1,block:o.block!==false,walk:!!o.walk};
  COLL.push(c);return c;}
function addLight(x,y,z,r,g,b,i,o){o=o||{};
  const L={p:[x,y,z],c:[r,g,b],i:i,spot:o.spot||null,mod:o.mod||'steady',on:true,base:i,phase:Math.random()*100};
  LIGHTS.push(L);return L;}
function baseGround(x,z){return inR(POOL,x,z)?POOL.f:0;}
function groundAt(x,z,feet){
  let g=baseGround(x,z);
  for(let i=0;i<COLL.length;i++){const c=COLL[i];
    if(!c.walk)continue;
    if(x<c.x0||x>c.x1||z<c.z0||z>c.z1)continue;
    if(c.y1<=feet+0.62&&c.y1>g)g=c.y1;}
  return g;}
function blocked(c,feet,step){return c.block&&c.y1>feet+step&&c.y0<feet+1.85;}
function tryMove(x,z,dx,dz,rad,feet,step){
  let nx=x+dx;
  for(let i=0;i<COLL.length;i++){const c=COLL[i];
    if(!blocked(c,feet,step))continue;
    if(nx>c.x0-rad&&nx<c.x1+rad&&z>c.z0-rad&&z<c.z1+rad){
      nx=dx>0?c.x0-rad:c.x1+rad;}}
  let nz=z+dz;
  for(let i=0;i<COLL.length;i++){const c=COLL[i];
    if(!blocked(c,feet,step))continue;
    if(nx>c.x0-rad&&nx<c.x1+rad&&nz>c.z0-rad&&nz<c.z1+rad){
      nz=dz>0?c.z0-rad:c.z1+rad;}}
  return [nx,nz];}
function solidAhead(x,z,dx,dz,rad,feet,step){
  const px=x+dx,pz=z+dz;
  for(let i=0;i<COLL.length;i++){const c=COLL[i];
    if(!blocked(c,feet,step))continue;
    if(px>c.x0-rad&&px<c.x1+rad&&pz>c.z0-rad&&pz<c.z1+rad)return true;}
  return false;}
/* ========================= WORLD BUILD ========================= */
let RIFLE_SPAWN=[-40.2,1.24,-21.9];
const WORLD=[];           // static meshes
const EMIS=[];            // meshes needing their own emissive draw
function bake(b){const ms=b.mesh();for(const i of ms)WORLD.push(i);}
function slab(x0,z0,x1,z1,top,thick,o){o=o||{};
  B.box((x0+x1)/2,top-thick/2,(z0+z1)/2,x1-x0,thick,z1-z0,o);}
function wallX(x0,x1,z,t,y0,y1,o){o=o||{};
  if(x1<x0){const s=x0;x0=x1;x1=s;}
  B.box((x0+x1)/2,(y0+y1)/2,z,x1-x0,y1-y0,t,o);
  if(o.nc)return; addCol(x0,x1,z-t/2,z+t/2,y0,y1,{block:o.block!==false,walk:o.walk});}
function wallZ(z0,z1,x,t,y0,y1,o){o=o||{};
  if(z1<z0){const s=z0;z0=z1;z1=s;}
  B.box(x,(y0+y1)/2,(z0+z1)/2,t,y1-y0,z1-z0,o);
  if(o.nc)return; addCol(x-t/2,x+t/2,z0,z1,y0,y1,{block:o.block!==false,walk:o.walk});}
const B=new Builder();

/* ---- base ground / asphalt lot (cut open around the pool) ---- */
slab(-130,-130,130,130,0,0.5,{tile:'grass',c:[0.72,0.8,0.66]});
slab(-54,-44,54,POOL.z0,0.03,0.4,{tile:'asphalt',c:[0.95,0.97,1]});
slab(-54,POOL.z1,54,44,0.03,0.4,{tile:'asphalt',c:[0.95,0.97,1]});
slab(-54,POOL.z0,POOL.x0,POOL.z1,0.03,0.4,{tile:'asphalt',c:[0.95,0.97,1]});
slab(POOL.x1,POOL.z0,54,POOL.z1,0.03,0.4,{tile:'asphalt',c:[0.95,0.97,1]});
/* highway south of the fence */
slab(-130,47,130,58,0.04,0.4,{tile:'asphalt',c:[0.8,0.82,0.86]});
for(let x=-126;x<126;x+=9)B.box(x,0.06,52.5,5,0.02,0.3,{tile:'white',c:[0.9,0.86,0.6]});
slab(-130,44,130,47,0.0,0.3,{tile:'gravel',c:[0.8,0.8,0.78]});
/* faded parking stalls */
for(let x=-24;x<=24;x+=3.2)B.box(x,0.05,-11.5,0.13,0.02,5.4,{tile:'line'});
for(let x=-24;x<=24;x+=3.2)B.box(x,0.05,-19,0.13,0.02,4.4,{tile:'line'});
/* puddles, weeds, blood */
[[16,4,3.4,2.2],[-30,-6,2.6,1.6],[38,18,4.2,2.4],[-6,26,2.2,3.0]].forEach(p=>{
  B.quad([p[0]-p[2],0.055,p[1]-p[3]],[p[0]+p[2],0.055,p[1]-p[3]],
         [p[0]+p[2],0.055,p[1]+p[3]],[p[0]-p[2],0.055,p[1]+p[3]],
         {tile:'window',c:[0.7,0.8,0.95],e:0.12});});
for(let i=0;i<70;i++){const x=rnd(-52,52),z=rnd(-42,42);
  const nearEdge=(Math.abs(x)>44||Math.abs(z)>34);
  if(!nearEdge&&Math.random()<0.7)continue;
  B.quad([x-0.5,0.06,z-0.5],[x+0.5,0.06,z-0.5],[x+0.5,0.06,z+0.5],[x-0.5,0.06,z+0.5],
    {tile:'weeds',c:[0.8,0.85,0.7]});}
[[-11,-21.5,1.6,1.1],[8,-17.5,1.3,0.9],[-24,6,1.8,1.3],[30,10,1.5,1.0],[-2,-3,2.2,1.4]]
.forEach(p=>{B.quad([p[0]-p[2],0.07,p[1]-p[3]],[p[0]+p[2],0.07,p[1]-p[3]],
  [p[0]+p[2],0.07,p[1]+p[3]],[p[0]-p[2],0.07,p[1]+p[3]],{tile:'blood',c:[0.75,0.5,0.5]});});

/* ---- chain-link fence ---- */
const FX=54,FZ=44,FH=2.55;
function fenceRun(ax,az,bx,bz,gaps){
  const len=Math.hypot(bx-ax,bz-az),n=Math.max(1,Math.round(len/6));
  const dx=(bx-ax)/len,dz=(bz-az)/len,ry=Math.atan2(-dz,dx);
  for(let i=0;i<n;i++){
    const t0=i/n,t1=(i+1)/n,s=(t0+t1)/2;
    let skip=false;
    if(gaps)for(const g of gaps)if(s>g[0]&&s<g[1])skip=true;
    const cx=ax+dx*len*s,cz=az+dz*len*s;
    if(!skip){
      B.box(cx,FH/2,cz,6.05,FH,0.05,{tile:'chain',ry:ry,c:[1,1,1]});
      addCol(cx-Math.abs(dx)*3.2,cx+Math.abs(dx)*3.2,
             cz-Math.abs(dz)*3.2,cz+Math.abs(dz)*3.2,0,FH,{});}
    else{
      addCol(cx-Math.abs(dx)*3.2,cx+Math.abs(dx)*3.2,
             cz-Math.abs(dz)*3.2,cz+Math.abs(dz)*3.2,0,FH,{});
      B.box(cx,FH*0.42,cz,6.0,FH*0.85,0.05,{tile:'chain',ry:ry,rz:skip?0.34:0,c:[0.85,0.86,0.88]});}
    B.cyl(ax+dx*len*(i/n),FH/2,az+dz*len*(i/n),0.09,FH+0.3,6,{c:[0.5,0.53,0.55],tile:'metal'});
  }
  B.cyl(bx,FH/2,bz,0.09,FH+0.3,6,{c:[0.5,0.53,0.55],tile:'metal'});
  B.box((ax+bx)/2,FH-0.06,(az+bz)/2,len,0.07,0.07,{tile:'metal',ry:ry,c:[0.55,0.58,0.6]});
  B.box((ax+bx)/2,0.12,(az+bz)/2,len,0.07,0.07,{tile:'metal',ry:ry,c:[0.55,0.58,0.6]});
}
fenceRun(-FX,-FZ,FX,-FZ,[[0.44,0.53]]);           // north (gap at breezeway side)
fenceRun(FX,-FZ,FX,FZ,[[0.56,0.64]]);             // east
fenceRun(FX,FZ,-FX,FZ,[[0.60,0.68],[0.28,0.34]]); // south
fenceRun(-FX,FZ,-FX,-FZ,[[0.36,0.44]]);           // west
/* bent fence panels pushed down in the gaps */
B.box(20.5,0.16,FZ,6,0.07,3.6,{tile:'chain',c:[0.9,0.9,0.9],ry:0.05});
B.box(-15.1,0.16,FZ,6,0.07,3.6,{tile:'chain',c:[0.9,0.9,0.9],ry:-0.08});
B.box(FX,0.16,11.4,3.6,0.07,6,{tile:'chain',c:[0.9,0.9,0.9],ry:0.06});
B.box(-FX,0.16,8.8,3.6,0.07,6,{tile:'chain',c:[0.9,0.9,0.9],ry:-0.05});
/* ---- treeline ---- */
function tree(x,z,s){
  B.cyl(x,1.4*s,z,0.22*s,2.8*s,5,{c:[0.22,0.19,0.16],tile:'rust'});
  for(let k=0;k<3;k++){const y=(2.2+k*1.5)*s,sc=(2.9-k*0.5)*s;
    B.quad([x-sc,y-sc*0.75,z],[x+sc,y-sc*0.75,z],[x+sc,y+sc*0.75,z],[x-sc,y+sc*0.75,z],
      {tile:'foliage',c:[0.75,0.85,0.75]});
    B.quad([x,y-sc*0.75,z-sc],[x,y-sc*0.75,z+sc],[x,y+sc*0.75,z+sc],[x,y+sc*0.75,z-sc],
      {tile:'foliage',c:[0.7,0.8,0.7]});}}
for(let i=0;i<150;i++){
  const a=i/150*TAU+rnd(-0.02,0.02), r=rnd(62,96);
  const x=Math.cos(a)*r,z=Math.sin(a)*r;
  if(z>44&&z<60)continue;
  tree(x,z,rnd(1.1,2.1));}
for(let i=0;i<40;i++){const x=rnd(-58,58),z=rnd(-70,-50);tree(x,z,rnd(1.2,2.2));}

/* ============================ MOTEL ROW ============================ */
const DOORW=1.5,DOORH=2.35;
function doorPanel(hx,hz,len,ang,height,o){
  const a=ang,dx=-Math.cos(a),dz=Math.sin(a);
  B.box(hx+dx*len/2,0.05+height/2,hz+dz*len/2,len,height,0.09,Object.assign({ry:a},o));}
const ROOMS=[];
function motelBlock(x0,x1,defs){
  const w=(x1-x0)/defs.length;
  wallX(x0,x1,-36.2,0.4,0,3.4,{tile:'stucco',c:[1,1,1]});          // back wall
  wallZ(-36,-22,x0,0.34,0,3.4,{tile:'stucco'});                      // west end
  wallZ(-36,-22,x1,0.34,0,3.4,{tile:'stucco'});                      // east end
  for(let i=0;i<defs.length;i++){
    const rx0=x0+i*w,rx1=rx0+w,dc=(rx0+rx1)/2,d0=dc-DOORW/2,d1=dc+DOORW/2,def=defs[i];
    wallX(rx0,d0,-22,0.4,0,3.4,{tile:'stucco'});
    wallX(d1,rx1,-22,0.4,0,3.4,{tile:'stucco'});
    wallX(d0,d1,-22,0.4,DOORH,3.4,{tile:'stucco'});
    if(i>0)wallZ(-36,-22,rx0,0.3,0,3.4,{tile:'wallpaper',c:[0.95,0.93,0.9]});
    slab(rx0,-36,rx1,-22.2,0.03,0.1,{tile:'carpet',c:[1,1,1]});
    slab(rx0,-36,rx1,-22.2,3.34,0.12,{tile:'ceil',c:[1,1,1]});
    /* door */
    const dtile='door'+i;
    if(def.open===1){ doorPanel(d0,-22.02,DOORW,1.32,DOORH,{tile:dtile,c:[1,1,1]});
      addCol(d0-0.1,d0+1.5,-23.6,-21.9,0,DOORH,{});}
    else if(def.open===2){ doorPanel(d0,-22.02,DOORW,0.34,DOORH,{tile:dtile,c:[1,1,1]});}
    else{ B.box(dc,DOORH/2,-22,DOORW,DOORH,0.1,{tile:dtile,c:[1,1,1]});
      addCol(d0,d1,-22.3,-21.7,0,DOORH,{});}
    /* room number plate above the door */
    B.box(dc,2.62,-21.74,0.5,0.24,0.04,{tile:'metal',c:[0.9,0.8,0.5],e:0.1});
    /* wall lamp */
    B.box(dc,2.86,-21.66,0.34,0.16,0.2,{tile:'white',c:[1,0.95,0.8],e:0.9});
    if(i%2===0)addLight(dc,2.7,-21.2,1,0.86,0.62,1.15,{mod:'steady'});
    /* interior dressing */
    const bx=rx0+w*0.62;
    B.box(bx,0.32,-33.6,2.0,0.5,1.6,{tile:'woodfloor',c:[0.55,0.4,0.3]});
    B.box(bx,0.66,-33.6,1.9,0.34,1.5,{tile:'white',c:[0.92,0.9,0.88]});
    B.box(bx,0.86,-34.2,0.7,0.16,0.4,{tile:'white',c:[0.95,0.95,0.92]});
    addCol(bx-1,bx+1,-34.4,-32.8,0,0.9,{});
    B.box(rx0+1.1,0.3,-31.6,0.7,0.6,0.7,{tile:'woodfloor',c:[0.5,0.36,0.26]});
    B.box(rx0+1.1,0.78,-31.6,0.26,0.36,0.26,{tile:'white',c:[1,0.92,0.7],e:0.6});
    B.box(rx0+1.1,0.98,-31.6,0.5,0.06,0.5,{tile:'white',c:[1,0.9,0.65],e:0.5});
    B.box(rx0+w-1.3,1.7,-35.9,1.1,0.8,0.06,{tile:'news',c:[0.8,0.78,0.72]});
    B.box(rx0+w*0.3,0.06,-27,1.6,0.03,1.2,{tile:'cloth',c:[0.7,0.5,0.5]});
    if(def.light)addLight(dc,2.6,-30,0.72,0.86,1.0,2.4,{});
    ROOMS.push({x0:rx0,x1:rx1,c:dc,def:def});
  }
}
motelBlock(-34,-6,[{},{},{open:1,light:1}]);
motelBlock(4,26,[{},{},{open:2}]);
/* breezeway floor + roof (gap x[-6,4]) */
slab(-6,-36.4,4,-22,0.04,0.14,{tile:'concrete',c:[0.9,0.9,0.9]});
addCol(-6,4,-36,-22,0,0.04,{block:false,walk:true});
/* roof over the whole row */
slab(-34.6,-36.6,26.6,-21.4,3.62,0.3,{tile:'roof',c:[1,1,1]});
slab(-34.6,-36.6,26.6,-21.4,3.34,0.14,{tile:'darkpanel',c:[0.6,0.6,0.62]});
/* back-of-motel clutter */
B.box(-20,0.5,-38.4,1.4,1.0,1.0,{tile:'rust',c:[0.7,0.7,0.72]});
B.box(-18.4,0.35,-38.9,0.9,0.7,0.7,{tile:'darkpanel',c:[0.5,0.5,0.55]});
B.cyl(14,0.55,-38.6,0.42,1.1,10,{tile:'rust',c:[0.6,0.4,0.3]});
B.box(6,0.35,-37.6,1.6,0.7,1.0,{tile:'woodfloor',c:[0.5,0.4,0.3]});
for(let i=0;i<6;i++)B.box(rnd(-30,24),0.06,rnd(-41,-37),rnd(.3,.7),0.05,rnd(.3,.6),
  {tile:'cloth',c:[rnd(.5,1),rnd(.5,1),rnd(.5,1)]});

/* ==================== WALKWAY + OVERHANG ==================== */
slab(-34.5,-22,26.5,-16.4,0.16,0.18,{tile:'concrete',c:[1,1,1]});
addCol(-34.5,26.5,-22,-16.4,0,0.16,{block:false,walk:true});
slab(-35,-23.8,27,-16.2,3.16,0.22,{tile:'metal',c:[0.55,0.56,0.6]});
B.box(1,3.0,-16.3,62,0.34,0.2,{tile:'stucco',c:[0.8,0.78,0.74]});
for(let x=-33.5;x<=26;x+=7.4){
  B.cyl(x,1.58,-16.75,0.13,3.16,8,{tile:'metal',c:[0.5,0.52,0.55]});
  B.box(x,0.1,-16.75,0.4,0.2,0.4,{tile:'metal',c:[0.45,0.47,0.5]});}
/* fluorescent strip lights under the overhang */
[[-24,'steady'],[-4,'buzz'],[14,'flicker']].forEach(s=>{
  B.box(s[0],3.0,-19.5,3.2,0.1,0.24,{tile:'white',c:[0.86,0.94,1],e:1});
  addLight(s[0],2.85,-19.5,0.72,0.86,1.0,2.1,{mod:s[1]});});
/* ice + vending under the overhang */
B.box(-30.4,0.85,-21.3,1.5,1.7,0.95,{tile:'iceface',c:[1,1,1]});
B.box(-30.4,1.78,-21.3,1.56,0.16,1.0,{tile:'metal',c:[0.7,0.72,0.75]});
addCol(-31.2,-29.6,-21.8,-20.8,0,1.7,{});
addLight(-30.4,1.6,-20.4,0.7,0.85,1.0,1.1,{});
B.box(-28.4,0.9,-21.3,1.15,1.8,0.9,{tile:'darkpanel',c:[0.6,0.6,0.65]});
B.quad([-28.95,1.72,-20.83],[-27.85,1.72,-20.83],[-27.85,0.35,-20.83],[-28.95,0.35,-20.83],
  {tile:'vending',c:[1,1,1],e:1,n:[0,0,1]});
addCol(-29,-27.8,-21.8,-20.8,0,1.8,{});
addLight(-28.4,1.2,-20.5,1.0,0.55,0.5,2.2,{mod:'buzz'});
/* abandoned laundry cart half-way along the walkway */
(function(){const x=6.5,z=-18.6;
  B.box(x,0.62,z,1.25,0.9,0.8,{tile:'chain',c:[0.75,0.78,0.8]});
  B.box(x,1.1,z,1.3,0.1,0.86,{tile:'metal',c:[0.6,0.62,0.65]});
  for(let i=0;i<7;i++)B.box(x+rnd(-.5,.5),1.16+i*0.07,z+rnd(-.3,.3),rnd(.4,.8),0.16,rnd(.3,.6),
    {tile:'cloth',c:[rnd(.6,1),rnd(.6,1),rnd(.6,1)],ry:rnd(0,3)});
  [[-.55,-.32],[.55,-.32],[-.55,.32],[.55,.32]].forEach(o=>
    B.cyl(x+o[0],0.1,z+o[1],0.1,0.2,8,{tile:'darkpanel',c:[.3,.3,.3],axis:'z'}));
  B.box(1.9,0.06,z+0.4,0.9,0.1,0.7,{tile:'cloth',c:[0.6,0.65,0.8],ry:0.6});
  addCol(x-.7,x+.7,z-.5,z+.5,0,1.2,{});})();
/* dropped flashlight still shining */
(function(){const x=15.5,z=-17.6;
  B.cyl(x,0.24,z,0.075,0.36,8,{tile:'metal',c:[0.8,0.75,0.4],axis:'x',ry:0.7});
  B.cyl(x+0.14,0.24,z+0.12,0.1,0.06,8,{tile:'white',c:[1,1,0.9],e:1,axis:'x',ry:0.7});
  addLight(x+0.4,0.3,z+0.5,1,0.95,0.8,1.6,{spot:{d:[0.7,0.1,0.7],cos:0.55}});})();
/* hose reel + welcome mats + trash */
B.cyl(-2,0.45,-21.4,0.4,0.9,10,{tile:'rust',c:[0.5,0.6,0.4]});
ROOMS.forEach(r=>B.box(r.c,0.2,-21.3,1.1,0.04,0.7,{tile:'darkpanel',c:[0.4,0.3,0.3]}));

/* ============================ OFFICE ============================ */
const OX0=-46,OX1=-34,OZ0=-30,OZ1=-14,OH=3.6;
slab(OX0,OZ0,OX1,OZ1,0.05,0.16,{tile:'officefloor',c:[1,1,1]});
addCol(OX0,OX1,OZ0,OZ1,0,0.05,{block:false,walk:true});
slab(OX0-0.4,OZ0-0.4,OX1+0.4,OZ1+0.4,OH+0.24,0.3,{tile:'roof',c:[0.9,0.9,0.9]});
slab(OX0,OZ0,OX1,OZ1,OH-0.1,0.12,{tile:'ceil',c:[1,1,1]});
wallX(OX0,OX1,OZ0,0.4,0,OH,{tile:'stucco',c:[0.95,0.92,0.86]});        // north
wallZ(OZ0,OZ1,OX0,0.4,0,OH,{tile:'stucco',c:[0.95,0.92,0.86]});         // west
wallZ(OZ0,OZ1,OX1,0.4,0,OH,{tile:'stucco',c:[0.95,0.92,0.86]});         // east
/* south (front) wall with door + window */
const ODOOR=[-40.6,-39.1],OWIN=[-45.4,-41.6];
wallX(OX0,OWIN[0],OZ1,0.4,0,OH,{tile:'stucco',c:[0.95,0.92,0.86]});
wallX(OWIN[0],OWIN[1],OZ1,0.4,0,1.0,{tile:'stucco',c:[0.95,0.92,0.86]});
wallX(OWIN[0],OWIN[1],OZ1,0.4,2.55,OH,{tile:'stucco',c:[0.95,0.92,0.86]});
wallX(OWIN[1],ODOOR[0],OZ1,0.4,0,OH,{tile:'stucco',c:[0.95,0.92,0.86]});
wallX(ODOOR[0],ODOOR[1],OZ1,0.4,DOORH,OH,{tile:'stucco',c:[0.95,0.92,0.86]});
wallX(ODOOR[1],OX1,OZ1,0.4,0,OH,{tile:'stucco',c:[0.95,0.92,0.86]});
doorPanel(ODOOR[0],OZ1-0.02,1.5,1.45,DOORH,{tile:'glass',c:[1,1,1]});
/* lit window: warm glow pane + blinds */
B.quad([OWIN[0]+0.1,2.5,OZ1-0.16],[OWIN[1]-0.1,2.5,OZ1-0.16],
       [OWIN[1]-0.1,1.05,OZ1-0.16],[OWIN[0]+0.1,1.05,OZ1-0.16],
  {tile:'white',c:[1,0.86,0.6],e:1,n:[0,0,1]});
for(let i=0;i<7;i++)B.box(-43.5,1.15+i*0.2,OZ1-0.1,3.6,0.07,0.1,
  {tile:'white',c:[0.85,0.8,0.72],e:0.15});
addLight(-43.4,2.0,-13.4,1.0,0.78,0.5,3.0,{spot:{d:[0,-0.35,1],cos:0.5}});
addLight(-42.5,2.6,-22,1.0,0.9,0.72,2.6,{mod:'flicker'});
/* interior: front desk, key board, TV, chair, filing cabinet */
B.box(-40.5,0.55,-21.6,7.4,1.1,1.15,{tile:'woodfloor',c:[0.62,0.46,0.32]});
B.box(-40.5,1.14,-21.6,7.7,0.1,1.3,{tile:'darkpanel',c:[0.5,0.42,0.36]});
addCol(-44.2,-36.8,-22.25,-20.95,0,1.2,{});
B.box(-41.5,1.22,-21.4,0.9,0.05,0.65,{tile:'paper',c:[1,1,1]});
B.box(-38.6,1.28,-21.5,0.3,0.22,0.3,{tile:'white',c:[0.9,0.88,0.85]});
B.box(-43.4,1.3,-21.5,0.4,0.3,0.35,{tile:'darkpanel',c:[0.6,0.6,0.6]});
/* key board on the west wall */
B.box(OX0+0.26,1.85,-25.5,0.1,1.15,1.75,{tile:'keybd',c:[1,1,1],e:0.35});
B.box(OX0+0.3,1.85,-25.5,0.06,1.25,1.85,{tile:'woodfloor',c:[0.5,0.4,0.3]});
addLight(-44.8,1.9,-25.5,1.0,0.85,0.6,1.4,{});
/* television showing static */
(function(){const x=-37.4,z=-27.6;
  B.box(x,0.45,z,1.5,0.9,0.75,{tile:'woodfloor',c:[0.5,0.4,0.3]});
  B.box(x,1.25,z,1.35,0.85,0.7,{tile:'darkpanel',c:[0.55,0.55,0.58]});
  B.quad([x-0.55,1.58,z+0.36],[x+0.55,1.58,z+0.36],[x+0.55,0.95,z+0.36],[x-0.55,0.95,z+0.36],
    {tile:'static',c:[1,1,1],e:1,n:[0,0,1]});
  B.box(x,1.72,z+0.3,0.5,0.06,0.3,{tile:'metal',c:[0.6,0.6,0.6]});
  addCol(x-0.8,x+0.8,z-0.5,z+0.5,0,1.7,{});
  addLight(x,1.4,z+1.1,0.7,0.8,1.0,2.0,{mod:'tv'});})();
B.box(-44.6,0.5,-17.4,1.0,1.0,1.0,{tile:'metal',c:[0.55,0.57,0.6]});
addCol(-45.1,-44.1,-17.9,-16.9,0,1.0,{});
B.cyl(-42,0.45,-19.6,0.34,0.9,10,{tile:'darkpanel',c:[0.3,0.3,0.34]});
B.box(-42,1.0,-19.6,0.6,0.12,0.6,{tile:'darkpanel',c:[0.3,0.3,0.34]});
B.box(-40.5,3.3,-25,0.7,0.7,0.08,{tile:'clockface',c:[1,1,1],e:0.1});
B.box(-44,1.5,-29.7,1.6,1.1,0.5,{tile:'notice',c:[1,1,1],e:0.05});
/* NO VACANCY in the office window (the NO is dead) */
B.quad([-45.2,3.05,-13.78],[-43.6,3.05,-13.78],[-43.6,2.45,-13.78],[-45.2,2.45,-13.78],
  {tile:'no',c:[1,1,1],e:0.05,n:[0,0,1]});
B.quad([-43.4,3.05,-13.78],[-39.6,3.05,-13.78],[-39.6,2.45,-13.78],[-43.4,2.45,-13.78],
  {tile:'vac',c:[1,1,1],e:1,n:[0,0,1]});
addLight(-41.5,2.7,-13.2,1.0,0.2,0.25,1.6,{mod:'neon'});
/* rifle behind the counter (pickup spawn) */
RIFLE_SPAWN=[-40.2,1.24,-21.9];
/* ============================ PAYPHONE ============================ */
const PHONE={x:-33.45,y:0,z:-17.2};
(function(){const x=PHONE.x,z=PHONE.z;
  B.box(x,1.35,z,0.34,1.5,0.62,{tile:'metal',c:[0.42,0.45,0.5]});
  B.box(x+0.06,2.2,z,0.5,0.35,0.8,{tile:'metal',c:[0.35,0.38,0.42]});
  B.box(x+0.2,1.55,z-0.16,0.1,0.5,0.28,{tile:'darkpanel',c:[0.2,0.2,0.22]});
  B.box(x+0.24,1.62,z+0.2,0.12,0.34,0.14,{tile:'darkpanel',c:[0.15,0.15,0.17]});
  B.box(x+0.16,1.05,z,0.3,0.06,0.5,{tile:'paper',c:[0.9,0.88,0.8]});
  B.box(x+0.02,2.42,z,0.36,0.1,0.7,{tile:'white',c:[0.9,0.9,0.85],e:0.5});
  B.quad([x+0.19,1.98,z+0.32],[x+0.19,1.98,z-0.32],[x+0.19,1.72,z-0.32],[x+0.19,1.72,z+0.32],
    {tile:'open24',c:[0.7,0.8,1],e:0.5,n:[1,0,0]});
  addCol(x-0.3,x+0.3,z-0.45,z+0.45,0,2.4,{});
  addLight(x+0.6,2.3,z,0.8,0.9,1.0,1.2,{mod:'buzz'});
  PHONE.hand=[x+0.24,1.62,z+0.2];})();
/* ============================ DINER ============================ */
const DX0=30,DX1=48,DZ0=-26,DZ1=-4,DH=3.6;
slab(DX0,DZ0,DX1,DZ1,0.06,0.16,{tile:'dinerfloor',c:[1,1,1]});
addCol(DX0,DX1,DZ0,DZ1,0,0.06,{block:false,walk:true});
slab(DX0-0.4,DZ0-0.4,DX1+0.4,DZ1+0.4,DH+0.26,0.32,{tile:'roof',c:[0.95,0.95,0.95]});
slab(DX0,DZ0,DX1,DZ1,DH-0.1,0.12,{tile:'ceil',c:[1,1,1]});
function winSegX(x0,x1,z,y0,y1){ // window opening in a wall running along X
  if(y0>0.01)wallX(x0,x1,z,0.4,0,y0,{tile:'dinerwall'});
  if(y1<DH)wallX(x0,x1,z,0.4,y1,DH,{tile:'dinerwall'});
  B.quad([x0+0.05,y1-0.05,z-0.14],[x1-0.05,y1-0.05,z-0.14],
         [x1-0.05,y0+0.05,z-0.14],[x0+0.05,y0+0.05,z-0.14],
    {tile:'glass',c:[0.8,0.9,1],e:0.06,n:[0,0,-1]});
  B.quad([x0+0.05,y1-0.05,z+0.14],[x1-0.05,y1-0.05,z+0.14],
         [x1-0.05,y0+0.05,z+0.14],[x0+0.05,y0+0.05,z+0.14],
    {tile:'glass',c:[0.8,0.9,1],e:0.06,n:[0,0,1]});
  B.box((x0+x1)/2,(y0+y1)/2,z,0.07,y1-y0,0.44,{tile:'dinerwall',c:[0.7,0.6,0.55]});
}
function winSegZ(z0,z1,x,y0,y1){
  if(y0>0.01)wallZ(z0,z1,x,0.4,0,y0,{tile:'dinerwall'});
  if(y1<DH)wallZ(z0,z1,x,0.4,y1,DH,{tile:'dinerwall'});
  B.quad([x-0.14,y1-0.05,z0+0.05],[x-0.14,y1-0.05,z1-0.05],
         [x-0.14,y0+0.05,z1-0.05],[x-0.14,y0+0.05,z0+0.05],
    {tile:'glass',c:[0.8,0.9,1],e:0.06,n:[-1,0,0]});
  B.quad([x+0.14,y1-0.05,z0+0.05],[x+0.14,y1-0.05,z1-0.05],
         [x+0.14,y0+0.05,z1-0.05],[x+0.14,y0+0.05,z0+0.05],
    {tile:'glass',c:[0.8,0.9,1],e:0.06,n:[1,0,0]});
  B.box(x,(y0+y1)/2,(z0+z1)/2,0.44,y1-y0,0.07,{tile:'dinerwall',c:[0.7,0.6,0.55]});
}
wallX(DX0,DX1,DZ0,0.4,0,DH,{tile:'dinerwall'});                 // north (kitchen wall)
wallZ(DZ0,DZ1,DX1,0.4,0,DH,{tile:'dinerwall'});                 // east
/* south wall with a big window */
winSegX(33,45,DZ1,0.95,2.65);
wallX(DX0,33,DZ1,0.4,0,DH,{tile:'dinerwall'});
wallX(45,DX1,DZ1,0.4,0,DH,{tile:'dinerwall'});
/* west wall: windows + entrance */
wallZ(DZ0,-24,DX0,0.4,0,DH,{tile:'dinerwall'});
winSegZ(-24,-17,DX0,0.95,2.65);
wallZ(-17,-16,DX0,0.4,0,DH,{tile:'dinerwall'});
wallZ(-16,-14.5,DX0,0.4,DOORH,DH,{tile:'dinerwall'});
wallZ(-14.5,-13,DX0,0.4,0,DH,{tile:'dinerwall'});
winSegZ(-13,-6,DX0,0.95,2.65);
wallZ(-6,DZ1,DX0,0.4,0,DH,{tile:'dinerwall'});
doorPanel(DX0+0.02,-16,1.5,-1.5,DOORH,{tile:'metal',c:[0.7,0.7,0.72]});
/* OPEN 24 HRS neon in the west window */
B.quad([DX0-0.24,2.5,-21.6],[DX0-0.24,2.5,-18.4],[DX0-0.24,1.3,-18.4],[DX0-0.24,1.3,-21.6],
  {tile:'open24',c:[1,1,1],e:1,n:[-1,0,0]});
addLight(DX0-1.2,2.0,-20,0.2,0.8,1.0,2.4,{mod:'neon'});
/* counter + stools */
B.box(45,0.5,-16,3.2,1.0,14,{tile:'dinerwall',c:[0.85,0.8,0.75]});
B.box(45,1.06,-16,3.5,0.12,14.3,{tile:'metal',c:[0.9,0.9,0.92]});
addCol(43.4,46.6,-23.1,-8.9,0,1.15,{});
B.box(47.4,0.5,-16,1.0,1.0,14,{tile:'tilewall',c:[1,1,1]});
B.box(47.4,1.06,-16,1.2,0.12,14.2,{tile:'metal',c:[0.85,0.86,0.88]});
addCol(46.8,48,-23.1,-8.9,0,1.15,{});
for(let i=0;i<6;i++){const z=-22+i*2.7;
  B.cyl(42.4,0.36,z,0.05,0.72,8,{tile:'metal',c:[0.7,0.72,0.75]});
  B.cyl(42.4,0.05,z,0.26,0.06,10,{tile:'metal',c:[0.55,0.57,0.6]});
  B.cyl(42.4,0.76,z,0.24,0.1,12,{c:[0.62,0.16,0.16],tile:'cloth'});
  addCol(42.1,42.7,z-0.3,z+0.3,0,0.85,{});}
/* booths along the west windows */
function booth(zc){
  B.box(31.4,0.5,zc,0.75,1.0,2.3,{c:[0.5,0.14,0.14],tile:'cloth'});
  B.box(31.4,1.02,zc,0.8,0.14,2.3,{c:[0.62,0.18,0.18],tile:'cloth'});
  B.box(33.6,0.5,zc,0.75,1.0,2.3,{c:[0.5,0.14,0.14],tile:'cloth'});
  B.box(33.6,1.02,zc,0.8,0.14,2.3,{c:[0.62,0.18,0.18],tile:'cloth'});
  B.box(32.5,0.74,zc,1.5,0.08,1.7,{tile:'metal',c:[0.92,0.92,0.9]});
  B.box(32.5,0.36,zc,0.2,0.72,0.2,{tile:'metal',c:[0.6,0.62,0.65]});
  addCol(31,31.9,zc-1.15,zc+1.15,0,1.1,{});
  addCol(33.2,34.1,zc-1.15,zc+1.15,0,1.1,{});
  addCol(31.7,33.3,zc-0.85,zc+0.85,0,0.8,{});
}
booth(-21.9);booth(-18.7);booth(-11.4);booth(-8.2);
/* half-eaten meal in the first booth */
(function(){const z=-21.9;
  B.cyl(32.5,0.83,z-0.3,0.22,0.05,12,{tile:'white',c:[0.95,0.95,0.92]});
  B.box(32.5,0.88,z-0.3,0.24,0.1,0.2,{c:[0.7,0.5,0.25],tile:'paper'});
  B.cyl(32.6,0.9,z+0.4,0.1,0.2,10,{tile:'white',c:[0.9,0.88,0.85]});
  B.cyl(32.6,0.86,z+0.4,0.085,0.16,10,{c:[0.28,0.16,0.08],tile:'darkpanel'});
  B.box(32.2,0.85,z+0.05,0.3,0.06,0.24,{tile:'paper',c:[0.9,0.85,0.8],ry:0.4});
  B.cyl(32.9,0.86,z-0.6,0.09,0.14,8,{c:[0.55,0.15,0.12],tile:'cloth'});})();
/* pie display case on the counter */
(function(){const x=44.6,z=-13.4;
  B.box(x,1.18,z,1.5,0.14,0.95,{tile:'metal',c:[0.8,0.8,0.82]});
  B.box(x,1.55,z,1.44,0.6,0.9,{tile:'glass',c:[1,1,1],e:0.25});
  B.box(x,1.88,z,1.5,0.1,0.95,{tile:'metal',c:[0.8,0.8,0.82]});
  B.cyl(x-0.3,1.34,z,0.28,0.16,14,{c:[0.85,0.68,0.35],tile:'paper'});
  B.cyl(x+0.35,1.34,z,0.28,0.16,14,{c:[0.7,0.4,0.45],tile:'paper'});
  addLight(x,1.7,z,1.0,0.85,0.6,1.0,{});})();
/* coffee pot still on its warmer */
(function(){const x=47.4,z=-20;
  B.box(x,1.16,z,0.7,0.1,0.55,{tile:'darkpanel',c:[0.3,0.3,0.32]});
  B.box(x,1.24,z,0.5,0.07,0.42,{tile:'white',c:[1,0.4,0.15],e:1});
  B.cyl(x,1.5,z,0.19,0.5,12,{tile:'glass',c:[0.9,0.95,1],e:0.15});
  B.cyl(x,1.42,z,0.16,0.3,12,{c:[0.22,0.12,0.06],tile:'darkpanel'});
  B.box(x+0.24,1.55,z,0.1,0.22,0.06,{c:[0.2,0.2,0.22],tile:'darkpanel',ry:0.4});
  addLight(x,1.4,z+0.4,1.0,0.6,0.3,1.2,{});
  B.cyl(x,1.3,z+0.7,0.1,0.16,10,{tile:'white',c:[0.95,0.93,0.9]});
  B.cyl(x,1.3,z-0.7,0.1,0.16,10,{tile:'white',c:[0.95,0.93,0.9]});})();
/* register, napkins, menu board, jukebox, wet floor sign, tipped stool */
B.box(45.4,1.35,-9.6,0.7,0.5,0.6,{tile:'metal',c:[0.6,0.62,0.66]});
B.box(45.4,1.62,-9.6,0.5,0.16,0.45,{tile:'darkpanel',c:[0.35,0.36,0.4]});
for(let i=0;i<4;i++)B.box(44.2,1.2,-19+i*2.4,0.24,0.16,0.14,{tile:'metal',c:[0.85,0.86,0.88]});
B.quad([33,3.1,DZ0+0.24],[45,3.1,DZ0+0.24],[45,1.7,DZ0+0.24],[33,1.7,DZ0+0.24],
  {tile:'menu',c:[1,1,1],e:0.55,n:[0,0,1]});
addLight(39,2.6,-24,1.0,0.85,0.6,2.0,{spot:{d:[0,-1,0.2],cos:0.7}});
addLight(39,2.6,-12,1.0,0.88,0.66,2.2,{spot:{d:[0,-1,0],cos:0.75}});
addLight(36,2.4,-19,1.0,0.9,0.7,1.6,{spot:{d:[0,-1,0],cos:0.8}});
/* jukebox */
(function(){const x=46.6,z=-24.4;
  B.box(x,0.7,z,1.1,1.4,0.8,{tile:'woodfloor',c:[0.55,0.35,0.25]});
  B.quad([x-0.5,1.32,z+0.42],[x+0.5,1.32,z+0.42],[x+0.5,0.75,z+0.42],[x-0.5,0.75,z+0.42],
    {tile:'open24',c:[1,0.8,0.6],e:0.7,n:[0,0,1]});
  addLight(x,1.2,z+0.7,1.0,0.5,0.25,1.0,{mod:'buzz'});})();
(function(){const x=36,z=-5.6;
  B.box(x,0.42,z,0.06,0.8,0.6,{tile:'hazard',c:[1,1,1],ry:0.5});
  B.box(x+0.2,0.42,z,0.06,0.8,0.6,{tile:'hazard',c:[1,1,1],ry:-0.5});})();
B.cyl(41.5,0.4,-5.4,0.24,0.8,10,{tile:'metal',c:[0.6,0.62,0.65],rz:1.4});
B.box(41.9,0.16,-5.4,0.5,0.1,0.5,{c:[0.62,0.16,0.16],tile:'cloth'});
/* kitchen doorway glow */
B.quad([41,2.4,DZ0+0.22],[44,2.4,DZ0+0.22],[44,0.1,DZ0+0.22],[41,0.1,DZ0+0.22],
  {tile:'white',c:[0.9,0.95,1],e:0.7,n:[0,0,1]});
addLight(42.5,2.2,-25,0.8,0.9,1.0,1.6,{spot:{d:[0,-0.3,1],cos:0.6}});

/* ============================ POOL ============================ */
slab(-34,6,-6,8,0.07,0.25,{tile:'concrete',c:[1,1,1]});
slab(-34,24,-6,26,0.07,0.25,{tile:'concrete',c:[1,1,1]});
slab(-34,8,-32,24,0.07,0.25,{tile:'concrete',c:[1,1,1]});
slab(-8,8,-6,24,0.07,0.25,{tile:'concrete',c:[1,1,1]});
slab(POOL.x0,POOL.z0,POOL.x1,POOL.z1,POOL.f,0.35,{tile:'poolfloor',c:[1,1,1]});
/* pool walls (block from inside, step-overable from the deck) */
function poolWall(x0,x1,z0,z1){
  B.box((x0+x1)/2,(POOL.f+0.16)/2,(z0+z1)/2,x1-x0,0.16-POOL.f,z1-z0,{tile:'pooltile',c:[1,1,1]});
  addCol(x0,x1,z0,z1,POOL.f,0.16,{});}
poolWall(-32,-16,7.9,8.25);
poolWall(-12,-8,7.9,8.25);
poolWall(-32,-8,23.75,24.1);
poolWall(-32.1,-31.75,8,24);
poolWall(-8.25,-7.9,8,24);
/* coping edge */
B.box(-24,0.11,7.85,16.4,0.14,0.3,{tile:'concrete',c:[1,1,1]});
B.box(-24,0.11,24.15,24.4,0.14,0.3,{tile:'concrete',c:[1,1,1]});
/* steps down into the pool (north side) */
for(let i=0;i<4;i++){
  const z0=8+i*0.8,z1=z0+0.8,top=-0.52*(i+1);
  B.box(-14,(POOL.f+top)/2,(z0+z1)/2,4,top-POOL.f,z1-z0,{tile:'pooltile',c:[1,1,1]});
  addCol(-16,-12,z0,z1,POOL.f,top,{block:false,walk:true});}
/* ladder on the east wall */
(function(){const x=-8.55,z=15.6;
  [z-0.35,z+0.35].forEach(zz=>{B.cyl(x,POOL.f+1.6,zz,0.05,3.4,8,{tile:'metal',c:[0.75,0.78,0.8]});
    B.cyl(x+0.15,3.35,zz,0.05,0.5,8,{tile:'metal',c:[0.75,0.78,0.8],axis:'z'});});
  for(let i=0;i<6;i++)B.cyl(x,POOL.f+0.4+i*0.55,z,0.04,0.7,6,{tile:'metal',c:[0.7,0.73,0.76],axis:'z'});
  addCol(-8.9,-8.3,z-0.5,z+0.5,POOL.f,1.2,{block:false});})();
const LADDER={x:-8.6,z:15.6};
/* overturned lawn chair + leaves & debris on the pool floor */
(function(){const x=-22,z=18,y=POOL.f+0.1,a=0.7,c=Math.cos(a),s=Math.sin(a);
  const put=(lx,ly,lz,sx,sy,sz,o)=>{o=o||{};
    B.box(x+lx*c+lz*s,y+ly,z-lx*s+lz*c,sx,sy,sz,Object.assign({ry:a},o));};
  put(0,0.04,0,0.66,0.08,0.66,{tile:'cloth',c:[0.8,0.78,0.66]});
  put(0,0.06,-0.62,0.66,0.08,0.62,{tile:'cloth',c:[0.8,0.78,0.66]});
  put(0.3,0.02,0.3,0.06,0.06,0.7,{tile:'metal',c:[0.7,0.7,0.72]});
  put(-0.3,0.02,0.3,0.06,0.06,0.7,{tile:'metal',c:[0.7,0.7,0.72]});
  put(0.3,0.22,-0.3,0.06,0.5,0.06,{tile:'metal',c:[0.7,0.7,0.72],rz:0.5});
  put(-0.3,0.22,-0.3,0.06,0.5,0.06,{tile:'metal',c:[0.7,0.7,0.72],rz:0.5});
  put(0.34,0.2,0.2,0.06,0.44,0.06,{tile:'metal',c:[0.7,0.7,0.72],rz:-0.4});
  put(-0.34,0.2,0.2,0.06,0.44,0.06,{tile:'metal',c:[0.7,0.7,0.72],rz:-0.4});})();
for(let i=0;i<46;i++){const x=rnd(-31,-9),z=rnd(9,23);
  B.quad([x-0.28,POOL.f+0.03,z-0.22],[x+0.28,POOL.f+0.03,z-0.22],
         [x+0.28,POOL.f+0.03,z+0.22],[x-0.28,POOL.f+0.03,z+0.22],
    {tile:'weeds',c:[rnd(0.7,1.1),rnd(0.6,0.95),rnd(0.4,0.7)]});}
for(let i=0;i<16;i++){const x=rnd(-31,-9),z=rnd(9,23);
  B.box(x,POOL.f+0.04,z,rnd(0.1,0.4),0.05,rnd(0.1,0.3),
    {tile:'paper',c:[rnd(.6,1),rnd(.6,.9),rnd(.6,.8)],ry:rnd(0,3)});}
/* pool deck props: ladder sign, towel, crate, chair on deck */
B.box(-33,1.1,10,0.08,1.6,1.0,{tile:'notice',c:[1,1,1],e:0.05});
B.box(-33,0.3,10,0.5,0.6,1.2,{tile:'metal',c:[0.5,0.5,0.52]});
B.box(-10,0.1,25.2,1.6,0.06,0.9,{tile:'cloth',c:[0.7,0.85,0.95],ry:0.3});
B.box(-28,0.5,25.2,1.1,0.9,0.9,{tile:'woodfloor',c:[0.6,0.5,0.35]});
B.box(-28,1.0,25.2,1.15,0.12,0.95,{tile:'woodfloor',c:[0.5,0.42,0.3]});
addCol(-28.6,-27.4,24.7,25.7,0,1.0,{});
/* child's bike on its side by the pool */
(function(){const x=-4.2,z=12.5,a=1.5,c=Math.cos(a),s=Math.sin(a);
  const put=(lx,ly,lz,sx,sy,sz,o)=>{o=o||{};
    B.box(x+lx*c+lz*s,0.16+ly,z-lx*s+lz*c,sx,sy,sz,Object.assign({ry:a},o));};
  [-0.62,0.62].forEach(lx=>{
    B.cyl(x+lx*c,0.2,z-lx*s,0.32,0.09,14,{tile:'darkpanel',c:[0.15,0.15,0.17],axis:'y',rz:1.5});
    B.cyl(x+lx*c,0.2,z-lx*s,0.12,0.12,8,{tile:'metal',c:[0.7,0.7,0.72],axis:'y',rz:1.5});});
  put(0,0.2,0,1.3,0.09,0.09,{c:[0.85,0.25,0.25],tile:'metal'});
  put(0.3,0.4,0,0.09,0.4,0.09,{c:[0.85,0.25,0.25],tile:'metal'});
  put(-0.3,0.4,0,0.09,0.4,0.09,{c:[0.85,0.25,0.25],tile:'metal'});
  put(0.62,0.5,0,0.09,0.5,0.09,{c:[0.8,0.8,0.82],tile:'metal'});
  put(0.72,0.72,0,0.09,0.09,0.5,{c:[0.8,0.8,0.82],tile:'metal'});
  put(-0.1,0.5,0,0.3,0.07,0.16,{c:[0.2,0.2,0.24],tile:'darkpanel'});
  addCol(x-0.9,x+0.9,z-0.9,z+0.9,0,0.5,{});})();

/* ======================= STATION WAGON ======================= */
const CAR={x:-4,z:5,ry:0.82};
(function(){
  const cx=CAR.x,cz=CAR.z,ry=CAR.ry,co=Math.cos(ry),si=Math.sin(ry);
  const P=(lx,lz)=>[cx+lx*co+lz*si, cz-lx*si+lz*co];
  const put=(lx,ly,lz,sx,sy,sz,o)=>{o=o||{};
    const rr=(o.ry===undefined)?ry:o.ry;
    B.box(cx+lx*co+lz*si, ly, cz-lx*si+lz*co, sx,sy,sz, Object.assign({},o,{ry:rr}));};
  const body=[0.30,0.36,0.32];
  put(0,0.62,0,4.9,0.52,1.88,{tile:'metal',c:body});
  put(1.55,0.98,0,1.9,0.34,1.84,{tile:'metal',c:body});
  put(-1.7,0.98,0,1.5,0.34,1.84,{tile:'metal',c:body});
  put(-0.45,1.3,0,2.7,0.6,1.78,{tile:'window',c:[0.55,0.66,0.78]});
  put(-0.45,1.66,0,2.85,0.12,1.86,{tile:'metal',c:[0.26,0.3,0.28]});
  [[1.0,0.87],[1.0,-0.87],[-1.7,0.87],[-1.7,-0.87]].forEach(p=>
    put(p[0],1.32,p[1],0.12,0.62,0.12,{tile:'metal',c:body}));
  put(-0.45,1.05,0,2.7,0.1,1.7,{tile:'darkpanel',c:[0.3,0.3,0.32]});
  /* wheels */
  [[1.62,0.95],[1.62,-0.95],[-1.62,0.95],[-1.62,-0.95]].forEach(w=>{
    const q=P(w[0],w[1]);
    B.cyl(q[0],0.37,q[1],0.37,0.26,14,{tile:'darkpanel',c:[0.12,0.12,0.14],axis:'z',ry:ry});
    B.cyl(q[0],0.37,q[1],0.17,0.3,10,{tile:'metal',c:[0.6,0.6,0.62],axis:'z',ry:ry});});
  /* driver's door swung open */
  const da=1.15,dl=1.25;
  put(1.15-Math.cos(da)*dl*0.5,0.95,0.94+Math.sin(da)*dl*0.5,dl,0.98,0.09,
    {ry:ry+da,tile:'metal',c:body});
  put(1.15-Math.cos(da)*dl*0.5,1.28,0.94+Math.sin(da)*dl*0.5,dl*0.7,0.3,0.05,
    {ry:ry+da,tile:'window',c:[0.5,0.6,0.72]});
  /* headlights / taillights */
  [[0.62],[-0.62]].forEach(h=>{
    const q=P(2.44,h[0]);
    put(2.44,0.8,h[0],0.16,0.24,0.42,{tile:'white',c:[1,0.97,0.85],e:1});
    const d=[Math.cos(ry),-0.16,-Math.sin(ry)];
    addLight(q[0],0.82,q[1],1.0,0.94,0.76,5.6,{spot:{d:d,cos:0.56}});});
  [[0.7],[-0.7]].forEach(h=>put(-2.46,0.85,h[0],0.12,0.2,0.34,
    {tile:'white',c:[1,0.12,0.08],e:1}));
  const dome=P(-0.4,0);
  addLight(dome[0],1.45,dome[1],1.0,0.85,0.6,1.4,{});
  /* roof rack + luggage + road map */
  put(-0.45,1.78,0,2.4,0.07,0.07,{tile:'metal',c:[0.5,0.5,0.52]});
  put(-1.2,1.9,0,1.1,0.3,1.2,{tile:'cloth',c:[0.5,0.42,0.3],ry:ry+0.1});
  put(0.4,1.12,0.4,0.5,0.04,0.35,{tile:'paper',c:[0.95,0.92,0.85],ry:ry+0.4});
  put(-0.45,1.1,-0.5,0.4,0.12,0.3,{tile:'darkpanel',c:[0.2,0.2,0.22]});
  /* colliders: two boxes along the body */
  [[1.2],[-1.2]].forEach(h=>{
    const q=P(h[0],0),hx=Math.abs(co)*2.5+Math.abs(si)*0.95,hz=Math.abs(si)*2.5+Math.abs(co)*0.95;
    addCol(q[0]-hx,q[0]+hx,q[1]-hz,q[1]+hz,0,1.7,{});});
  /* open door collider */
  const dq=P(1.15-Math.cos(da)*dl*0.5,0.94+Math.sin(da)*dl*0.5);
  addCol(dq[0]-0.7,dq[0]+0.7,dq[1]-0.7,dq[1]+0.7,0,1.5,{});
})();
const SHOT_SPAWN=(function(){const co=Math.cos(CAR.ry),si=Math.sin(CAR.ry);
  const lx=1.0,lz=1.75;return [CAR.x+lx*co+lz*si, 0.9, CAR.z-lx*si+lz*co];})();
/* cooler + gas can beside the car */
B.box(-6.6,0.3,7.4,0.7,0.6,0.5,{tile:'white',c:[0.85,0.9,0.95],ry:0.3});
B.box(-6.6,0.62,7.4,0.74,0.08,0.54,{c:[0.2,0.5,0.85],tile:'metal',ry:0.3});
B.box(-1.6,0.28,7.6,0.4,0.5,0.32,{c:[0.8,0.2,0.15],tile:'metal',ry:-0.4});
addCol(-7,-6.2,7.1,7.7,0,0.7,{});addCol(-1.9,-1.3,7.3,7.9,0,0.6,{});

/* ==================== NEON STARLITE SIGN ==================== */
const SIGN={x:34,z:33,a:Math.atan2(-34,-33)};
(function(){
  const a=SIGN.a,co=Math.cos(a),si=Math.sin(a);
  const W=(lx,ly,lz)=>[SIGN.x+lx*co+lz*si, ly, SIGN.z-lx*si+lz*co];
  B.cyl(SIGN.x,4.4,SIGN.z,0.4,8.8,10,{tile:'metal',c:[0.35,0.36,0.4]});
  B.cyl(SIGN.x,0.2,SIGN.z,1.4,0.4,12,{tile:'concrete',c:[0.8,0.8,0.78]});
  addCol(SIGN.x-1,SIGN.x+1,SIGN.z-1,SIGN.z+1,0,8,{});
  const c=W(0,8.6,0);
  B.box(c[0],8.6,c[2],11.4,3.4,0.5,{tile:'darkpanel',c:[0.5,0.5,0.55],ry:a});
  B.box(c[0],8.6,c[2],11.8,3.8,0.34,{tile:'metal',c:[0.28,0.29,0.33],ry:a});
  /* letters, each its own mesh so one can flicker on its own */
  for(let k=0;k<8;k++){
    const lx=-4.45+k*1.28, y=8.95, lz=0.3;
    const p0=W(lx-0.56,y-1.05,lz),p1=W(lx+0.56,y-1.05,lz),
          p2=W(lx+0.56,y+1.05,lz),p3=W(lx-0.56,y+1.05,lz);
    const lb=new Builder();
    lb.quad(p0,p1,p2,p3,{tile:'let'+k,c:[1,1,1],e:1});
    EMIS.push({mesh:lb.mesh()[0],i:1,on:true,letter:k});}
  const st=W(0,11.1,0.3);
  const sb=new Builder();
  sb.quad([st[0]-1.1,st[1]-1.1,st[2]],[st[0]+1.1,st[1]-1.1,st[2]],
          [st[0]+1.1,st[1]+1.1,st[2]],[st[0]-1.1,st[1]+1.1,st[2]],{tile:'star',c:[1,1,1],e:1});
  EMIS.push({mesh:sb.mesh()[0],i:1,on:true,letter:-1});
  const f=W(0,6.9,0.28);
  const fb=new Builder();
  fb.quad([f[0]-4.2,f[1]-0.5,f[2]],[f[0]+4.2,f[1]-0.5,f[2]],
          [f[0]+4.2,f[1]+0.5,f[2]],[f[0]-4.2,f[1]+0.5,f[2]],{tile:'sign2',c:[1,1,1],e:1});
  EMIS.push({mesh:fb.mesh()[0],i:0.7,on:true,letter:-2});
  addLight(SIGN.x,9,SIGN.z,1.0,0.25,0.6,3.4,{mod:'neon'});
  addLight(SIGN.x,7.4,SIGN.z,0.9,0.4,0.8,1.8,{mod:'neon'});
})();
/* ==================== STREET LIGHTS ==================== */
function street(x,z,arm){
  B.cyl(x,3.5,z,0.15,7,8,{tile:'metal',c:[0.42,0.44,0.47]});
  B.cyl(x,0.16,z,0.55,0.32,10,{tile:'concrete',c:[0.75,0.75,0.72]});
  const ax=x+arm*1.6;
  B.box((x+ax)/2,6.95,z,1.7,0.14,0.14,{tile:'metal',c:[0.42,0.44,0.47]});
  B.box(ax,6.75,z,1.0,0.3,0.6,{tile:'metal',c:[0.35,0.36,0.4]});
  B.box(ax,6.58,z,0.86,0.08,0.5,{tile:'white',c:[1,0.82,0.45],e:1});
  addLight(ax,6.5,z,1.0,0.62,0.22,6.0,{spot:{d:[0,-1,0],cos:0.62}});
  addCol(x-0.4,x+0.4,z-0.4,z+0.4,0,7,{});
  for(let i=0;i<7;i++)B.box(x+rnd(-1.2,1.2),0.06,z+rnd(-1.2,1.2),rnd(.4,.9),0.03,rnd(.4,.9),
    {tile:'weeds',c:[0.7,0.75,0.6]});}
street(-45,16,1);street(4,34,-1);street(47,26,-1);street(24,-13,1);
/* ==================== LOT CLUTTER ==================== */
B.cyl(-48,0.55,-8,0.42,1.1,12,{tile:'rust',c:[0.6,0.6,0.62]});
addCol(-48.5,-47.5,-8.5,-7.5,0,1.1,{});
B.cyl(49,0.5,-2,0.4,1.0,12,{tile:'darkpanel',c:[0.3,0.32,0.36]});
addCol(48.5,49.5,-2.5,-1.5,0,1.0,{});
B.box(44,0.35,34,0.5,0.7,0.5,{c:[0.9,0.4,0.1],tile:'hazard'});
B.box(-40,0.4,30,1.2,0.8,0.9,{tile:'woodfloor',c:[0.6,0.5,0.36],ry:0.4});
B.box(-40,0.85,30,1.25,0.12,0.95,{tile:'woodfloor',c:[0.5,0.42,0.3],ry:0.4});
addCol(-40.7,-39.3,29.5,30.5,0,0.9,{});
for(let i=0;i<9;i++){const x=rnd(-50,50),z=rnd(-40,40);
  if(inR(POOL,x,z))continue;
  B.box(x,0.12,z,rnd(.2,.4),rnd(.15,.3),rnd(.2,.4),
    {tile:'darkpanel',c:[rnd(.3,.6),rnd(.3,.6),rnd(.3,.6)],ry:rnd(0,3)});}
/* suitcase spilled open in the doorway of room 105 */
(function(){const r=ROOMS[4],x=r.c+0.1,z=-22.9;
  B.box(x,0.16,z,0.9,0.3,0.6,{tile:'cloth',c:[0.45,0.3,0.22],ry:0.25});
  B.box(x-0.1,0.42,z-0.45,0.9,0.06,0.55,{tile:'cloth',c:[0.5,0.34,0.25],ry:0.25,rx:-1.1});
  for(let i=0;i<6;i++)B.box(x+rnd(-.7,.7),0.06,z+rnd(-1.6,.6),rnd(.25,.5),0.08,rnd(.2,.4),
    {tile:'cloth',c:[rnd(.5,1),rnd(.5,1),rnd(.5,1)],ry:rnd(0,3)});})();
/* welcome sign board near the office door */
B.box(-37,1.5,-12.6,1.4,1.0,0.1,{tile:'notice',c:[1,1,1],e:0.06});
B.cyl(-37,0.5,-12.6,0.07,1.0,8,{tile:'metal',c:[0.5,0.5,0.52]});

bake(B);
/* ========================= PROCEDURAL AUDIO ========================= */
const AUD={ctx:null,master:null,noiseBuf:null,phoneOn:false,phoneTimer:null,amb:{},muted:false};
function aInit(){
  if(AUD.ctx)return;
  const C=window.AudioContext||window.webkitAudioContext; if(!C)return;
  let ctx; try{ctx=new C();}catch(e){showErr('audio: '+e);return;}
  AUD.ctx=ctx;
  const master=ctx.createGain(); master.gain.value=0.8;
  const comp=ctx.createDynamicsCompressor();
  comp.threshold.value=-16; comp.ratio.value=7; comp.knee.value=18;
  master.connect(comp); comp.connect(ctx.destination); AUD.master=master;
  const len=Math.floor(ctx.sampleRate*2.5);
  const buf=ctx.createBuffer(1,len,ctx.sampleRate); const d=buf.getChannelData(0);
  for(let i=0;i<len;i++)d[i]=Math.random()*2-1;
  AUD.noiseBuf=buf;
  startAmbient();
}
function aResume(){ if(AUD.ctx&&AUD.ctx.state==='suspended')AUD.ctx.resume(); }
function panNode(v){ const ctx=AUD.ctx;
  if(ctx.createStereoPanner){const p=ctx.createStereoPanner();p.pan.value=clamp(v,-1,1);return p;}
  return ctx.createGain();}
function playNoise(o){ const ctx=AUD.ctx; if(!ctx||AUD.muted)return;
  o=o||{}; const dur=o.dur||0.2, t=ctx.currentTime+(o.delay||0);
  const src=ctx.createBufferSource(); src.buffer=AUD.noiseBuf; src.loop=true;
  src.playbackRate.value=o.rate||1;
  const flt=ctx.createBiquadFilter(); flt.type=o.type||'bandpass';
  flt.frequency.setValueAtTime(o.f0||2000,t); flt.Q.value=o.q||0.9;
  if(o.f1)flt.frequency.exponentialRampToValueAtTime(Math.max(o.f1,25),t+dur);
  const g=ctx.createGain(); const gn=(o.gain||0.4);
  g.gain.setValueAtTime(0.0001,t);
  g.gain.linearRampToValueAtTime(gn,t+(o.attack||0.006));
  g.gain.exponentialRampToValueAtTime(0.0006,t+dur);
  const p=panNode(o.pan||0);
  src.connect(flt);flt.connect(g);g.connect(p);p.connect(AUD.master);
  src.start(t);src.stop(t+dur+0.08);}
function playTone(o){ const ctx=AUD.ctx; if(!ctx||AUD.muted)return;
  o=o||{}; const dur=o.dur||0.2, t=ctx.currentTime+(o.delay||0);
  const osc=ctx.createOscillator(); osc.type=o.type||'sine';
  osc.frequency.setValueAtTime(o.f0||440,t);
  if(o.f1)osc.frequency.exponentialRampToValueAtTime(Math.max(o.f1,20),t+dur);
  const g=ctx.createGain(); const gn=(o.gain||0.25);
  g.gain.setValueAtTime(0.0001,t);
  g.gain.linearRampToValueAtTime(gn,t+(o.attack||0.01));
  g.gain.setValueAtTime(gn,t+dur*(o.hold||0.55));
  g.gain.exponentialRampToValueAtTime(0.0006,t+dur);
  const p=panNode(o.pan||0);
  osc.connect(g);g.connect(p);p.connect(AUD.master);
  osc.start(t);osc.stop(t+dur+0.05);}
function startAmbient(){
  const ctx=AUD.ctx;
  /* wind bed */
  const ws=ctx.createBufferSource(); ws.buffer=AUD.noiseBuf; ws.loop=true;
  const wf=ctx.createBiquadFilter(); wf.type='lowpass'; wf.frequency.value=340; wf.Q.value=0.6;
  const wg=ctx.createGain(); wg.gain.value=0.075;
  const lfo=ctx.createOscillator(); lfo.frequency.value=0.07;
  const lg=ctx.createGain(); lg.gain.value=0.045;
  lfo.connect(lg); lg.connect(wg.gain);
  ws.connect(wf); wf.connect(wg); wg.connect(AUD.master);
  ws.start(); lfo.start();
  /* neon buzz */
  [120,181].forEach((f,i)=>{const o=ctx.createOscillator();o.type='sawtooth';o.frequency.value=f;
    const fl=ctx.createBiquadFilter();fl.type='bandpass';fl.frequency.value=f*2.2;fl.Q.value=5;
    const g=ctx.createGain();g.gain.value=i?0.006:0.012;
    o.connect(fl);fl.connect(g);g.connect(AUD.master);o.start();AUD.amb['buzz'+i]=g;});
  /* ice machine hum */
  const io=ctx.createOscillator(); io.type='triangle'; io.frequency.value=61;
  const ig=ctx.createGain(); ig.gain.value=0.03;
  io.connect(ig); ig.connect(AUD.master); io.start();
  const ir=ctx.createBufferSource(); ir.buffer=AUD.noiseBuf; ir.loop=true;
  const irf=ctx.createBiquadFilter(); irf.type='bandpass'; irf.frequency.value=760; irf.Q.value=1.4;
  const irg=ctx.createGain(); irg.gain.value=0.016;
  ir.connect(irf);irf.connect(irg);irg.connect(AUD.master); ir.start();
}
/* -------- gunplay -------- */
function sfxShot(kind){
  if(kind==='pistol'){
    playNoise({dur:0.17,type:'bandpass',f0:4600,f1:520,q:0.7,gain:0.5});
    playNoise({dur:0.1,type:'lowpass',f0:1400,f1:220,gain:0.55});
    playTone({f0:230,f1:58,dur:0.15,type:'triangle',gain:0.42});
    playNoise({dur:0.035,type:'highpass',f0:5200,gain:0.3});
  }else if(kind==='shotgun'){
    playNoise({dur:0.34,type:'bandpass',f0:2600,f1:340,q:0.5,gain:0.72});
    playNoise({dur:0.24,type:'lowpass',f0:900,f1:130,gain:0.8});
    playTone({f0:120,f1:34,dur:0.32,type:'triangle',gain:0.62});
    playNoise({dur:0.05,type:'highpass',f0:3600,gain:0.35});
    playNoise({dur:0.5,type:'bandpass',f0:700,f1:260,q:1.2,gain:0.14,delay:0.12});
  }else{
    playNoise({dur:0.11,type:'bandpass',f0:6200,f1:900,q:0.6,gain:0.46});
    playNoise({dur:0.08,type:'lowpass',f0:2400,f1:400,gain:0.5});
    playTone({f0:320,f1:70,dur:0.1,type:'sawtooth',gain:0.34});
    playNoise({dur:0.03,type:'highpass',f0:7000,gain:0.28});
  }}
function sfxReload(stage){
  if(stage==='mag'){ playNoise({dur:0.06,type:'bandpass',f0:2400,f1:1400,q:2,gain:0.35});
    playTone({f0:520,f1:380,dur:0.06,type:'square',gain:0.12});}
  else if(stage==='slide'){ playNoise({dur:0.1,type:'bandpass',f0:3400,f1:1200,q:1.4,gain:0.42});
    playTone({f0:280,f1:180,dur:0.08,type:'square',gain:0.16});}
  else { playNoise({dur:0.04,type:'highpass',f0:4200,gain:0.4}); }}
function sfxEmpty(){ playNoise({dur:0.045,type:'bandpass',f0:3200,f1:2400,q:3,gain:0.36});
  playTone({f0:1400,f1:900,dur:0.05,type:'square',gain:0.1});}
function sfxImpact(head){
  if(head){ playTone({f0:150,f1:52,dur:0.2,type:'sine',gain:0.6});
    playNoise({dur:0.17,type:'lowpass',f0:1500,f1:260,gain:0.55});
    playNoise({dur:0.09,type:'bandpass',f0:900,f1:400,q:1.5,gain:0.35});}
  else { playNoise({dur:0.1,type:'lowpass',f0:1800,f1:420,gain:0.34});
    playTone({f0:210,f1:110,dur:0.07,type:'sine',gain:0.2});}}
function sfxWall(){ playNoise({dur:0.07,type:'bandpass',f0:2600,f1:900,q:1.6,gain:0.22});}
function sfxHurt(){ playNoise({dur:0.26,type:'lowpass',f0:900,f1:180,gain:0.5});
  playTone({f0:110,f1:60,dur:0.3,type:'sawtooth',gain:0.2});
  playTone({f0:70,f1:44,dur:0.36,type:'sine',gain:0.35});}
function sfxGroan(type,vol,panv){
  if(vol<=0.01)return;
  if(type==='runner'){
    playTone({f0:380,f1:170,dur:0.4,type:'sawtooth',gain:0.30*vol,pan:panv});
    playNoise({dur:0.38,type:'bandpass',f0:2000,f1:800,q:1.6,gain:0.30*vol,pan:panv});
    playTone({f0:240,f1:120,dur:0.3,type:'square',gain:0.12*vol,pan:panv,delay:0.05});
  }else if(type==='brute'){
    playTone({f0:54,f1:34,dur:1.5,type:'sine',gain:0.55*vol,pan:panv});
    playTone({f0:84,f1:56,dur:1.5,type:'triangle',gain:0.2*vol,pan:panv});
    playNoise({dur:1.2,type:'lowpass',f0:520,f1:220,gain:0.3*vol,pan:panv});
  }else{
    playTone({f0:98,f1:70,dur:1.2,type:'sawtooth',gain:0.24*vol,pan:panv});
    playNoise({dur:1.0,type:'lowpass',f0:800,f1:320,gain:0.26*vol,pan:panv});
    playTone({f0:132,f1:96,dur:0.7,type:'triangle',gain:0.1*vol,pan:panv,delay:0.15});}}
function sfxFoot(type){ 
  if(type==='brute'){ playTone({f0:46,f1:26,dur:0.34,type:'sine',gain:0.55});
    playNoise({dur:0.16,type:'lowpass',f0:340,f1:90,gain:0.4});}
  else{ playNoise({dur:0.1,type:'lowpass',f0:700,f1:200,gain:0.16});}}
function sfxDeath(type){ 
  if(type==='brute'){playTone({f0:64,f1:24,dur:1.4,type:'sawtooth',gain:0.4});
    playNoise({dur:0.7,type:'lowpass',f0:800,f1:120,gain:0.5});}
  else{playTone({f0:150,f1:44,dur:0.75,type:'sawtooth',gain:0.3});
    playNoise({dur:0.5,type:'lowpass',f0:1400,f1:200,gain:0.4});}}
function sfxSting(){
  [110,138.6,164.8,220].forEach((f,i)=>
    playTone({f0:f,f1:f*0.72,dur:1.9,type:'sawtooth',gain:0.11,delay:i*0.045}));
  playTone({f0:58,f1:32,dur:2.2,type:'sine',gain:0.42});
  playNoise({dur:1.3,type:'lowpass',f0:2600,f1:180,gain:0.3});
  playNoise({dur:0.5,type:'bandpass',f0:1800,f1:420,q:1.2,gain:0.2,delay:0.05});}
function sfxWaveClear(){
  playTone({f0:392,dur:0.4,type:'triangle',gain:0.2});
  playTone({f0:523,dur:0.5,type:'triangle',gain:0.2,delay:0.14});
  playTone({f0:659,dur:0.9,type:'triangle',gain:0.18,delay:0.28});}
function sfxPickup(kind){
  if(kind==='med'){playTone({f0:520,f1:780,dur:0.22,type:'sine',gain:0.3});
    playTone({f0:780,f1:1040,dur:0.3,type:'sine',gain:0.2,delay:0.1});}
  else{playTone({f0:660,f1:990,dur:0.14,type:'square',gain:0.16});
    playTone({f0:990,f1:1320,dur:0.2,type:'square',gain:0.13,delay:0.08});}}
function sfxWeapon(){ playTone({f0:220,f1:440,dur:0.16,type:'sawtooth',gain:0.22});
  playNoise({dur:0.2,type:'bandpass',f0:2600,f1:900,q:1.4,gain:0.3});
  playTone({f0:440,f1:660,dur:0.3,type:'triangle',gain:0.18,delay:0.1});}
function sfxPhoneBurst(){
  [0,1].forEach(()=>{});
  playTone({f0:440,dur:0.85,type:'sine',gain:0.15,hold:0.7});
  playTone({f0:480,dur:0.85,type:'sine',gain:0.15,hold:0.7});
  playTone({f0:1580,dur:0.5,type:'sine',gain:0.035,hold:0.5});
  playTone({f0:2350,dur:0.4,type:'sine',gain:0.02,hold:0.4});}
function phoneRing(on){
  if(on===AUD.phoneOn)return; AUD.phoneOn=on;
  if(AUD.phoneTimer){clearTimeout(AUD.phoneTimer);AUD.phoneTimer=null;}
  if(!on||!AUD.ctx)return;
  const loop=()=>{ if(!AUD.phoneOn)return; sfxPhoneBurst();
    AUD.phoneTimer=setTimeout(()=>{ if(!AUD.phoneOn)return; sfxPhoneBurst();
      AUD.phoneTimer=setTimeout(loop,1500); },950); };
  loop();}
function sfxBuzzLetter(on){ if(on)playNoise({dur:0.09,type:'bandpass',f0:3600,f1:2600,q:6,gain:0.09});}
function sfxZombieBite(){ playNoise({dur:0.22,type:'bandpass',f0:1200,f1:500,q:1.2,gain:0.4});
  playTone({f0:260,f1:90,dur:0.25,type:'sawtooth',gain:0.25});}
/* ========================= PLAYER + WEAPONS ========================= */
const EYE=1.68, PR=0.42, STEP=0.62;
const p={x:6,y:EYE,z:-9,yaw:0.43,pitch:-0.02,vy:0,hp:100,r:PR,
  bob:0,bobAmt:0,shake:0,vig:0,dflash:0,hurtcd:0,climb:0,speed:0,
  reloading:0,relStage:'',cool:0,flash:0,cur:0,spread:0,recoil:0,kickX:0};
const WEAPONS=[
 {key:'pistol',name:'PISTOL',magSize:12,mag:12,res:Infinity,res0:Infinity,dmg:34,rate:0.17,
  spread:0.010,pellets:1,auto:false,reload:1.15,kick:0.035,snd:'pistol',range:140,
  muzzle:-0.20,unlock:true,slot:'9MM'},
 {key:'shotgun',name:'SHOTGUN',magSize:6,mag:0,res:0,res0:30,dmg:17,rate:0.8,
  spread:0.055,pellets:9,auto:false,reload:2.5,kick:0.12,snd:'shotgun',range:60,
  muzzle:-0.72,unlock:false,slot:'12GA'},
 {key:'rifle',name:'RIFLE',magSize:30,mag:0,res:0,res0:150,dmg:27,rate:0.095,
  spread:0.014,pellets:1,auto:true,reload:1.95,kick:0.03,snd:'rifle',range:150,
  muzzle:-0.62,unlock:false,slot:'5.56'}];
const held={fire:false};
const keys={};

/* ---------------- input ---------------- */
document.addEventListener('keydown',e=>{
  if(e.repeat)return; keys[e.code]=true;
  if(e.code==='KeyR')reload();
  if(e.code==='Digit1')switchTo(0);
  if(e.code==='Digit2')switchTo(1);
  if(e.code==='Digit3')switchTo(2);
  if(e.code==='Escape'&&(S.mode==='play'))pauseGame();
  if(['Space','ArrowUp','ArrowDown','ArrowLeft','ArrowRight'].indexOf(e.code)>=0)e.preventDefault();
});
document.addEventListener('keyup',e=>{keys[e.code]=false;});
canvas.addEventListener('mousedown',e=>{
  if(e.button!==0)return;
  if(S.mode==='title'){startGame();return;}
  if(S.mode==='pause'){resumeGame();return;}
  if(S.mode==='dead'||S.mode==='win'){restartGame();return;}
  if(S.mode==='play'){held.fire=true; if(!WEAPONS[p.cur].auto)fire();}});
document.addEventListener('mouseup',e=>{if(e.button===0)held.fire=false;});
document.addEventListener('mousemove',e=>{
  if(document.pointerLockElement!==canvas)return;
  const s=0.0022;
  p.yaw-=e.movementX*s; p.pitch-=e.movementY*s;
  p.pitch=clamp(p.pitch,-1.5,1.5);});
document.addEventListener('wheel',e=>{
  if(S.mode!=='play')return;
  const dir=e.deltaY>0?1:-1;
  let n=p.cur;
  for(let i=0;i<3;i++){n=(n+dir+3)%3;if(WEAPONS[n].unlock)break;}
  switchTo(n);},{passive:true});
document.addEventListener('contextmenu',e=>e.preventDefault());
function lockPointer(){ if(canvas.requestPointerLock)canvas.requestPointerLock(); }
let everLocked=false;
document.addEventListener('pointerlockchange',()=>{
  if(document.pointerLockElement===canvas)everLocked=true;
  else if(everLocked&&S.mode==='play')pauseGame();});

function switchTo(i){
  if(i===p.cur||!WEAPONS[i].unlock||p.reloading>0)return;
  p.cur=i; p.cool=Math.max(p.cool,0.22); p.reloading=0;
  p.kickX=0.22; sfxReload('mag'); updateAmmoHUD();}
function reload(){
  const w=WEAPONS[p.cur];
  if(S.mode!=='play'||p.reloading>0)return;
  if(w.mag>=w.magSize)return;
  if(w.res<=0){ sfxEmpty(); msg('NO '+w.slot+' AMMUNITION'); return; }
  p.reloading=w.reload; p.relStage='mag'; sfxReload('mag');}
function finishReload(){
  const w=WEAPONS[p.cur];
  const need=w.magSize-w.mag;
  const take=(w.res===Infinity)?need:Math.min(need,w.res);
  w.mag+=take; if(w.res!==Infinity)w.res-=take;
  sfxReload('slide'); updateAmmoHUD();}
function fire(){
  if(S.mode!=='play')return;
  const w=WEAPONS[p.cur];
  if(p.reloading>0||p.cool>0)return;
  if(w.mag<=0){ sfxEmpty(); p.cool=0.32; return; }
  w.mag--; p.cool=w.rate;
  p.recoil+=w.kick*3.2; p.pitch+=w.kick*0.55*rnd(-0.4,1);
  p.shake=Math.min(0.6,p.shake+w.kick*3.5);
  p.spread=Math.min(1,p.spread+0.55);
  p.flash=0.055;
  sfxShot(w.snd);
  const fwd=camFwd(), rgt=camRight(), up=[0,1,0];
  const ox=p.x+fwd[0]*0.1, oy=p.y+fwd[1]*0.1-0.03, oz=p.z+fwd[2]*0.1;
  for(let i=0;i<w.pellets;i++){
    const sx=rnd(-1,1)*w.spread*(1+p.spread*1.2), sy=rnd(-1,1)*w.spread*(1+p.spread*1.2);
    let dx=fwd[0]+rgt[0]*sx+up[0]*sy, dy=fwd[1]+rgt[1]*sx+up[1]*sy, dz=fwd[2]+rgt[2]*sx+up[2]*sy;
    const L=Math.hypot(dx,dy,dz); dx/=L;dy/=L;dz/=L;
    shootRay([ox,oy,oz],[dx,dy,dz],w.dmg,w);
  }
  muzzleT=0.05;
  if(w.mag===0&&w.res>0)setTimeout(()=>{if(S.mode==='play'&&WEAPONS[p.cur]===w&&w.mag===0)reload();},260);
  updateAmmoHUD();}
function camFwd(){const cp=Math.cos(p.pitch);
  return [-Math.sin(p.yaw)*cp, Math.sin(p.pitch), -Math.cos(p.yaw)*cp];}
function camRight(){return [Math.cos(p.yaw),0,-Math.sin(p.yaw)];}

/* ---------------- ray hits ---------------- */
function rayBox(o,d,bx){ // bx=[x0,x1,y0,y1,z0,z1]
  let t0=0,t1=1e9;
  const oc=[o[0],o[1],o[2]], dc=[d[0],d[1],d[2]];
  for(let a=0;a<3;a++){
    const lo=bx[a*2],hi=bx[a*2+1];
    if(Math.abs(dc[a])<1e-7){ if(oc[a]<lo||oc[a]>hi)return null; continue; }
    let ta=(lo-oc[a])/dc[a], tb=(hi-oc[a])/dc[a];
    if(ta>tb){const s=ta;ta=tb;tb=s;}
    if(ta>t0)t0=ta; if(tb<t1)t1=tb;
    if(t0>t1)return null;}
  return t0;}
function rayWorld(o,d){
  let best=1e9;
  for(let i=0;i<COLL.length;i++){const c=COLL[i];
    const t=rayBox(o,d,[c.x0,c.x1,c.y0,c.y1,c.z0,c.z1]);
    if(t!==null&&t<best&&t>0.05)best=t;}
  if(d[1]<-1e-5){
    let t=(0-o[1])/d[1]; if(t>0.05&&t<best)best=t;
    t=(POOL.f-o[1])/d[1];
    if(t>0.05&&t<best&&inR(POOL,o[0]+d[0]*t,o[2]+d[2]*t))best=t;}
  return best;}
function zLocal(z,o,d){
  const c=Math.cos(z.yaw),s=Math.sin(z.yaw);
  const px=o[0]-z.x,pz=o[2]-z.z,py=o[1]-z.fy;
  return {o:[px*c-pz*s, py, px*s+pz*c],
          d:[d[0]*c-d[2]*s, d[1], d[0]*s+d[2]*c]};}
function zBoxes(z){
  const s=z.s, hy=z.headY;
  return [
    {part:'head', b:[-0.16*s,0.16*s, hy-0.17*s, hy+0.17*s, -0.17*s,0.17*s]},
    {part:'body', b:[-0.27*s,0.27*s, z.bodyY-0.36*s, z.bodyY+0.34*s, -0.2*s,0.22*s]},
    {part:'legs', b:[-0.24*s,0.24*s, 0.0, z.bodyY-0.34*s, -0.19*s,0.19*s]}];}
function shootRay(o,d,dmg,w){
  const wt=rayWorld(o,d);
  let best=null,bestT=1e9,bestPart=null;
  for(let i=0;i<ZOMB.length;i++){
    const z=ZOMB[i]; if(z.dead)continue;
    const dx2=z.x-o[0],dz2=z.z-o[2];
    if(dx2*dx2+dz2*dz2>9000)continue;
    const L=zLocal(z,o,d);
    const bs=zBoxes(z);
    for(let k=0;k<3;k++){
      const t=rayBox(L.o,L.d,bs[k].b);
      if(t!==null&&t>0&&t<bestT){bestT=t;best=z;bestPart=bs[k].part;}
    }}
  if(best&&bestT<wt){
    const hp=[o[0]+d[0]*bestT,o[1]+d[1]*bestT,o[2]+d[2]*bestT];
    damageZombie(best,bestPart,dmg,d,w,hp);
  }else if(wt<1e8){
    const hp=[o[0]+d[0]*wt,o[1]+d[1]*wt,o[2]+d[2]*wt];
    spawnBurst(hp[0],hp[1],hp[2],5,[0.7,0.65,0.55],3.5,0.05);
    sfxWall();
    TRACERS.push({a:[o[0]+d[0]*0.4,o[1]+d[1]*0.4,o[2]+d[2]*0.4],b:hp,t:0.05,c:[1,0.85,0.5]});
  }}
/* ---------------- HUD helpers ---------------- */
function msg(t){
  const d=document.createElement('div'); d.className='msg'; d.textContent=t;
  $('msgs').appendChild(d); setTimeout(()=>{if(d.parentNode)d.parentNode.removeChild(d);},2700);}
function banner(t1,t2,cls){
  const b=$('banner'); b.classList.remove('go'); void b.offsetWidth;
  b.querySelector('.t1').textContent=t1;
  b.querySelector('.t2').textContent=t2||'';
  b.classList.add('go');}
function subbanner(t){const s=$('subbanner');s.textContent=t;s.classList.add('go');}
function hitmark(head){
  const h=$('hitmark'); h.classList.toggle('hs',!!head);
  h.classList.remove('go'); void h.offsetWidth; h.classList.add('go');}
function updateAmmoHUD(){
  const w=WEAPONS[p.cur];
  $('wname').textContent=w.name;
  const res=(w.res===Infinity)?'&infin;':w.res;
  $('ammo').innerHTML=w.mag+' <span class="res">/ '+res+'</span>';
  $('ammo').classList.toggle('low',w.mag<=Math.ceil(w.magSize*0.3));
  ['s1','s2','s3'].forEach((id,i)=>{
    const el=$(id); el.classList.toggle('on',i===p.cur);
    el.classList.toggle('off',!WEAPONS[i].unlock);});}

/* ===================== VIEWMODEL ===================== */
function buildGun(parts){
  const g=new Builder();
  parts.forEach(a=>{const o=a[6]||{};
    g.box(a[0],a[1],a[2],a[3],a[4],a[5],o);});
  return g.mesh()[0];}
const GUNS=[
  buildGun([
    [0,0.035,-0.07,0.056,0.05,0.24,{tile:'metal',c:[0.32,0.33,0.36]}],
    [0,-0.005,-0.06,0.05,0.035,0.22,{tile:'darkpanel',c:[0.4,0.4,0.44]}],
    [0,-0.075,0.035,0.046,0.12,0.062,{tile:'woodfloor',c:[0.5,0.36,0.24],rx:-0.22}],
    [0,-0.05,-0.02,0.03,0.012,0.085,{tile:'darkpanel',c:[0.3,0.3,0.32]}],
    [0,0.005,-0.03,0.016,0.03,0.03,{tile:'darkpanel',c:[0.25,0.25,0.27]}],
    [0,0.068,-0.16,0.012,0.02,0.02,{tile:'darkpanel',c:[0.2,0.2,0.22]}],
    [0,0.068,0.0,0.014,0.022,0.02,{tile:'darkpanel',c:[0.2,0.2,0.22]}],
    [0.0,0.035,-0.185,0.05,0.045,0.03,{tile:'metal',c:[0.25,0.26,0.28]}]]),
  buildGun([
    [0,0.03,-0.34,0.046,0.046,0.66,{tile:'metal',c:[0.3,0.31,0.34]}],
    [0,-0.015,-0.3,0.04,0.04,0.56,{tile:'metal',c:[0.34,0.35,0.38]}],
    [0,0.0,0.02,0.062,0.09,0.22,{tile:'metal',c:[0.32,0.33,0.36]}],
    [0,-0.02,-0.2,0.075,0.07,0.16,{tile:'woodfloor',c:[0.55,0.38,0.24]}],
    [0,-0.045,0.24,0.05,0.1,0.34,{tile:'woodfloor',c:[0.5,0.34,0.22],rx:0.16}],
    [0,-0.075,0.06,0.046,0.1,0.06,{tile:'woodfloor',c:[0.5,0.34,0.22],rx:-0.2}],
    [0,0.075,0.06,0.02,0.03,0.05,{tile:'darkpanel',c:[0.22,0.22,0.24]}]]),
  buildGun([
    [0,0.035,-0.3,0.05,0.055,0.62,{tile:'metal',c:[0.28,0.29,0.32]}],
    [0,0.0,0.0,0.07,0.1,0.3,{tile:'metal',c:[0.34,0.35,0.38]}],
    [0,-0.09,-0.01,0.05,0.14,0.07,{tile:'woodfloor',c:[0.45,0.3,0.2],rx:-0.24}],
    [0,-0.05,-0.14,0.05,0.16,0.1,{tile:'darkpanel',c:[0.3,0.3,0.3],rx:0.35}],
    [0,0.0,0.24,0.05,0.1,0.3,{tile:'woodfloor',c:[0.48,0.32,0.2],rx:0.1}],
    [0,0.08,-0.5,0.016,0.04,0.03,{tile:'darkpanel',c:[0.2,0.2,0.22]}],
    [0,0.075,-0.06,0.02,0.035,0.03,{tile:'darkpanel',c:[0.2,0.2,0.22]}]])];
const FLASHM=buildGun([[0,0,0,0.3,0.3,0.06,{tile:'white',c:[1,0.9,0.6],e:1}]]);
let muzzleT=0;

function drawViewmodel(){
  if(S.mode!=='play')return;
  useProg();
  gl.clear(gl.DEPTH_BUFFER_BIT);
  gl.uniformMatrix4fv(U.uView,false,M4I);
  gl.uniform1f(U.uFogD,0);
  gl.uniform3f(U.uCam,0,0,0);
  gl.uniform3f(U.uAmb,0.34,0.36,0.42);
  LP.fill(0);LC.fill(0);LD.fill(0);
  LP[0]=0.55;LP[1]=0.5;LP[2]=0.35;
  LC[0]=1.0;LC[1]=0.93;LC[2]=0.82;
  LP[4]=-0.6;LP[5]=-0.1;LP[6]=-0.2;
  LC[3]=0.3;LC[4]=0.42;LC[5]=0.6;
  gl.uniform4fv(U.uLP,LP); gl.uniform3fv(U.uLC,LC); gl.uniform3fv(U.uLD,LD);
  gl.uniform1i(U.uLN,2);
  const w=WEAPONS[p.cur];
  const bx=0.235+p.kickX*0.05, by=-0.225-Math.abs(Math.sin(p.bob))*0.008-p.recoil*0.01;
  const bz=-0.56+p.recoil*0.07;
  let m=m4chain(m4t(bx+Math.sin(p.bob*0.5)*0.012,by+Math.sin(p.bob)*0.012,bz),
    m4ry(-0.05+p.kickX*0.12), m4rx(p.recoil*0.22-0.02), m4rz(0.03-p.kickX*0.06), m4s(1,1,1));
  if(p.reloading>0){
    const t=1-p.reloading/w.reload;
    const dip=Math.sin(clamp(t,0,1)*Math.PI);
    m=m4chain(m4t(bx,by-dip*0.16,bz+dip*0.05),m4rx(dip*0.8),m4ry(-0.3*dip),m4rz(dip*0.35));}
  drawMesh(GUNS[p.cur],m,null,0,0,1,1);
  if(muzzleT>0){
    const fm=m4chain(m, m4t(0,0.035,w.muzzle), m4s(1,1,1));
    drawMesh(FLASHM,fm,null,0,1.6,1,1);}
}
/* ========================= ZOMBIES ========================= */
const ZOMB=[],PARTS=[],TRACERS=[],MOTHS=[];
const CUBE=(function(){const g=new Builder();
  g.box(0,0,0,1,1,1,{tile:'white',c:[1,1,1]});return g.mesh()[0];})();
const ZTYPES={
  shambler:{hp:100,spd:1.5,s:1.0,headY:1.62,bodyY:1.12,dmg:9,atk:1.15,score:100,r:0.44,
    skin:[0.55,0.6,0.47],name:'SHAMBLER'},
  runner:{hp:64,spd:4.35,s:0.97,headY:1.56,bodyY:1.06,dmg:7,atk:0.72,score:150,r:0.4,
    skin:[0.68,0.55,0.5],name:'RUNNER'},
  brute:{hp:440,spd:1.28,s:1.6,headY:1.6,bodyY:1.12,dmg:22,atk:1.5,score:400,r:0.78,
    skin:[0.42,0.5,0.36],name:'BRUTE'}};
const ZDRAW={
  shambler:{torso:[0.46,0.6,0.3],armW:0.15,reach:1.45,legW:0.17},
  runner:{torso:[0.42,0.56,0.28],armW:0.14,reach:1.75,legW:0.16},
  brute:{torso:[0.82,0.92,0.52],armW:0.28,reach:1.2,legW:0.3}};
const SPAWNS=[
 {p:[-44,-41],w:[[-44,-41],[-49,-41],[-50,-24],[-50,-4],[-44,2]]},
 {p:[-50,22],w:[[-50,22],[-46,10],[-42,4]]},
 {p:[-22,41],w:[[-22,41],[-8,37],[-2,28],[0,18]]},
 {p:[16,41],w:[[16,41],[14,33],[10,24],[8,14]]},
 {p:[50,24],w:[[50,24],[46,16],[44,8],[40,2]]},
 {p:[51,-30],w:[[51,-30],[51,-6],[46,0]]},
 {p:[28,-41],w:[[28,-41],[28,-28],[28,-14],[26,-6]]},
 {p:[0,-41],w:[[0,-41],[0,-30],[0,-20],[0,-12]]},
 {p:[-52,38],w:[[-52,38],[-46,30],[-40,24],[-36,16],[-36,6]]},
 {p:[52,40],w:[[52,40],[46,34],[40,26],[36,16],[34,6]]}];
const STAIR={x:-14,z:7.0};
const SHIRTS=[[0.35,0.4,0.5],[0.5,0.3,0.28],[0.3,0.45,0.4],[0.55,0.5,0.35],[0.4,0.3,0.45],
  [0.6,0.6,0.62],[0.28,0.34,0.42],[0.5,0.42,0.3]];
const PANTS=[[0.24,0.26,0.32],[0.3,0.28,0.24],[0.2,0.24,0.28],[0.34,0.3,0.26]];
function pickType(w){
  const r=Math.random();
  if(w>=6&&r<0.05+(w-6)*0.022)return 'brute';
  if(w>=3&&r<0.2+(w-3)*0.05)return 'runner';
  return 'shambler';}
function spawnZombie(type,wave){
  const t=ZTYPES[type],sp=SPAWNS[rndi(0,SPAWNS.length-1)];
  const x=sp.p[0]+rnd(-2,2),z=sp.p[1]+rnd(-2,2);
  const z0={type:type,x:x,z:z,fy:0,yaw:0,hp:t.hp,maxhp:t.hp,s:t.s,spd:t.spd*rnd(0.88,1.12),
    headY:t.headY,bodyY:t.bodyY,dmg:t.dmg,atk:t.atk,score:t.score,r:t.r,
    wp:0,path:sp.w,dead:false,deathT:0,fall:0,fallAx:'x',sink:0,
    flinch:0,kb:[0,0],anim:rnd(0,6),atkCd:rnd(0,0.6),groanT:rnd(0.5,5),footT:0,
    lunge:0,hurtT:0,shirt:SHIRTS[rndi(0,SHIRTS.length-1)],pants:PANTS[rndi(0,PANTS.length-1)],
    skin:t.skin,wave:wave};
  z0.fy=groundAt(x,z,0);
  ZOMB.push(z0);
  return z0;}
function aliveCount(){let n=0;for(let i=0;i<ZOMB.length;i++)if(!ZOMB[i].dead)n++;return n;}
function damageZombie(z,part,dmg,d,w,hp){
  if(z.dead)return;
  const mult=part==='head'?3.3:(part==='legs'?0.72:1);
  const head=part==='head';
  const dmg2=dmg*mult;
  z.hp-=dmg2;
  z.hurtT=0.22;
  z.flinch=Math.min(1.2,z.flinch+dmg2/(head?120:70));
  const kl=Math.min(3.2,dmg2*0.03);
  z.kb[0]+=d[0]*kl; z.kb[1]+=d[2]*kl;
  const n=head?9:5;
  spawnBurst(hp[0],hp[1],hp[2],n,[0.42,0.03,0.03],head?5.5:3.5,head?0.075:0.055);
  sfxImpact(head);
  if(S.mode==='play')hitmark(head);
  if(z.hp<=0)killZombie(z,head,d);}
function killZombie(z,head,d){
  z.dead=true; z.deathT=0; z.fall=0;
  z.fallAx=Math.random()<0.5?'x':'z';
  z.flDir=(Math.random()<0.5)?1:-1;
  S.kills++; if(head)S.hs++;
  S.score+=head?z.score+75:z.score;
  sfxDeath(z.type);
  spawnBurst(z.x,z.fy+1.2*z.s,z.z,14,[0.4,0.03,0.03],4.5,0.07);
  if(head)spawnBurst(z.x,z.fy+1.55*z.s,z.z,10,[0.5,0.06,0.05],6,0.06);
  updateScoreHUD();}
function damagePlayer(a,src){
  if(S.mode!=='play'||p.hp<=0)return;
  p.hp-=a;
  p.shake=Math.min(1.1,p.shake+0.45);
  p.vig=1; p.dflash=1;
  sfxHurt();
  if(p.hp<=0){p.hp=0;die();}
  updateHealthHUD();}
const ANGS=[0,0.45,-0.45,0.95,-0.95,1.5,-1.5,2.1,-2.1,2.7,-2.7];
function updateZombies(dt){
  const px=p.x,pz=p.z;
  const pInPool=inR(POOL,px,pz)&&(p.y-EYE)<-0.5;
  for(let i=ZOMB.length-1;i>=0;i--){
    const z=ZOMB[i];
    if(z.dead){
      z.deathT+=dt;
      const k=clamp(z.deathT/0.62,0,1);
      z.fall=(1-Math.pow(1-k,3))*(z.flDir||1)*1.5;
      if(z.deathT>11)z.sink+=dt*0.42;
      if(z.deathT>15){ZOMB.splice(i,1);}
      continue;}
    z.flinch=Math.max(0,z.flinch-dt*2.6);
    z.hurtT=Math.max(0,z.hurtT-dt);
    z.lunge=Math.max(0,z.lunge-dt*3);
    z.atkCd-=dt;
    const dxp=px-z.x,dzp=pz-z.z,dp=Math.hypot(dxp,dzp);
    /* voice + footsteps */
    z.groanT-=dt;
    if(z.groanT<=0){z.groanT=rnd(3.5,9);
      const vol=clamp(1-dp/30,0,1);
      if(vol>0.03){const rn=camRight();
        const pan=clamp((dxp*rn[0]+dzp*rn[2])/Math.max(dp,1),-1,1);
        sfxGroan(z.type,vol*0.95,pan);}}
    if(dp<26){z.footT-=dt*z.spd;
      if(z.footT<=0){z.footT=1.6;sfxFoot(z.type);}}
    /* pick target */
    let tx,tz;
    const zInPool=inR(POOL,z.x,z.z)&&z.fy<-0.5;
    if(zInPool&&!pInPool){tx=STAIR.x;tz=STAIR.z;}
    else{
      let wp=z.path[z.wp];
      while(z.wp<z.path.length-1&&dist2(z.x,z.z,z.path[z.wp][0],z.path[z.wp][1])<10)z.wp++;
      wp=z.path[Math.min(z.wp,z.path.length-1)];
      if(dp<16||z.wp>=z.path.length-1){tx=px;tz=pz;}
      else{tx=wp[0];tz=wp[1];}}
    /* steering */
    let dx=tx-z.x,dz=tz-z.z;const L=Math.hypot(dx,dz)||1;dx/=L;dz/=L;
    let bx=dx,bz=dz;
    for(let a=0;a<ANGS.length;a++){
      const c=Math.cos(ANGS[a]),s=Math.sin(ANGS[a]);
      const nx=dx*c-dz*s,nz=dx*s+dz*c;
      if(!solidAhead(z.x,z.z,nx*1.7,nz*1.7,z.r,z.fy,0.5)){bx=nx;bz=nz;break;}}
    /* separation */
    let sx=0,sz2=0;
    for(let j=0;j<ZOMB.length;j++){
      if(j===i)continue;const o=ZOMB[j];if(o.dead)continue;
      const ox=z.x-o.x,oz=z.z-o.z,d2=ox*ox+oz*oz;
      const minr=(z.r+o.r)*1.15;
      if(d2<minr*minr&&d2>0.0001){const dd=Math.sqrt(d2);
        sx+=ox/dd*(minr-dd);sz2+=oz/dd*(minr-dd);}}
    const speed=z.spd*(z.lunge>0?1.5:1);
    let mvx=(bx*speed+z.kb[0]+sx*3.0)*dt;
    let mvz=(bz*speed+z.kb[1]+sz2*3.0)*dt;
    z.kb[0]*=Math.exp(-5*dt); z.kb[1]*=Math.exp(-5*dt);
    const res=tryMove(z.x,z.z,mvx,mvz,z.r,z.fy,0.5);
    z.x=res[0];z.z=res[1];
    z.fy=groundAt(z.x,z.z,z.fy);
    /* facing + animation */
    const want=Math.atan2(-(bx),-(bz));
    let diff=want-z.yaw; while(diff>Math.PI)diff-=TAU; while(diff<-Math.PI)diff+=TAU;
    z.yaw+=clamp(diff,-4.5*dt,4.5*dt);
    z.anim+=dt*(2.0+z.spd*1.5);
    /* attack */
    const reach=1.15+z.r;
    if(dp<reach&&z.atkCd<=0){
      z.atkCd=z.atk; z.lunge=1;
      damagePlayer(z.dmg,z.type);
      sfxZombieBite();}}
  if(ZOMB.length>64){
    for(let i=0;i<ZOMB.length;i++)if(ZOMB[i].dead&&ZOMB[i].deathT>6){ZOMB.splice(i,1);break;}}}
function drawZombie(z){
  const s=z.s,d=ZDRAW[z.type];
  let base;
  if(z.dead){
    base=m4chain(m4t(z.x,z.fy+0.14*s+z.sink,z.z),m4ry(z.yaw),
      z.fallAx==='x'?m4rx(z.fall):m4rz(z.fall));}
  else{
    const lean=(z.type==='shambler'?0.14:(z.type==='runner'?0.3:0.1))+z.flinch*0.14+z.lunge*0.18;
    base=m4chain(m4t(z.x,z.fy,z.z),m4ry(z.yaw),m4rx(lean));}
  const ph=Math.sin(z.anim),ph2=Math.sin(z.anim+Math.PI);
  const P=(lx,ly,lz,sx,sy,sz,col,extra,em)=>{
    let m=m4chain(base,m4t(lx,ly,lz));
    if(extra)m=m4mul(m,extra);
    m=m4mul(m,m4s(sx,sy,sz));
    drawMesh(CUBE,m,col,1,em||0,1,1);};
  const tx=d.torso[0]*s,ty=d.torso[1]*s,tz=d.torso[2]*s;
  const hurt=z.hurtT>0;
  const skin=hurt?[z.skin[0]*1.35+0.2,z.skin[1]*0.75,z.skin[2]*0.7]:z.skin;
  /* legs */
  const lw=d.legW*s;
  P(-0.13*s,0.42*s,ph*0.16*s,lw,0.84*s,lw*1.15,z.pants,m4rx(ph*0.55));
  P(0.13*s,0.42*s,ph2*0.16*s,lw,0.84*s,lw*1.15,z.pants,m4rx(ph2*0.55));
  P(-0.13*s,0.045*s,ph*0.16*s-0.05*s,lw*1.1,0.09*s,lw*2,[0.1,0.1,0.12]);
  P(0.13*s,0.045*s,ph2*0.16*s-0.05*s,lw*1.1,0.09*s,lw*2,[0.1,0.1,0.12]);
  /* torso */
  const fl=z.flinch;
  const torsoM=m4chain(m4t(0,z.bodyY,0),m4rx(-fl*0.26));
  let m=m4chain(base,torsoM,m4s(1,1,1));
  m=m4mul(m,m4s(tx,ty,tz));
  drawMesh(CUBE,m,hurt?[0.85,0.4,0.4]:z.shirt,1,0,1,1);
  /* chest blood */
  m=m4chain(base,torsoM,m4t(0.02*s,0.06*s,-tz*0.51),m4s(tx*0.42,ty*0.4,0.02));
  drawMesh(CUBE,m,[0.3,0.04,0.04],1,0,1,1);
  /* arms reaching forward */
  const aw=d.armW*s;
  [-1,1].forEach(sd=>{
    const swing=(z.type==='runner'?ph:0)*0.12;
    const am=m4chain(m4t(sd*(tx*0.5+aw*0.4),z.bodyY+ty*0.34,-tz*0.12),
      m4rx(-d.reach+swing+fl*0.5),m4rz(sd*0.12));
    let mm=m4mul(base,am); mm=m4mul(mm,m4t(0,-0.3*s,0)); mm=m4mul(mm,m4s(aw,0.62*s,aw));
    drawMesh(CUBE,mm,skin,1,0,1,1);
    mm=m4mul(base,am); mm=m4mul(mm,m4t(0,-0.62*s,0)); mm=m4mul(mm,m4s(aw*1.15,0.17*s,aw*1.15));
    drawMesh(CUBE,mm,skin,1,0,1,1);});
  /* head */
  const hm=m4chain(m4t(0,z.headY,-0.03*s),m4rx(-0.15+fl*0.6-z.lunge*0.3),m4ry(Math.sin(z.anim*0.4)*0.07));
  let h=m4mul(base,hm); h=m4mul(h,m4s(0.27*s,0.3*s,0.28*s));
  drawMesh(CUBE,h,skin,1,0,1,1);
  let j=m4chain(base,hm,m4t(0,-0.16*s,0.02*s),m4rx(0.4+z.lunge*0.5),m4s(0.2*s,0.11*s,0.2*s));
  drawMesh(CUBE,j,[0.35,0.14,0.13],1,0,1,1);
  /* matted hair */
  let hr=m4chain(base,hm,m4t(0,0.15*s,0.02*s),m4s(0.29*s,0.11*s,0.3*s));
  drawMesh(CUBE,hr,[0.1,0.09,0.08],1,0,1,1);
  /* eyes (help readability in the murk) */
  [-1,1].forEach(sd=>{
    let e=m4chain(base,hm,m4t(sd*0.07*s,0.03*s,-0.145*s),m4s(0.05*s,0.045*s,0.02));
    drawMesh(CUBE,e,z.type==='runner'?[1,0.45,0.25]:[0.55,0.85,0.6],1,0.45,1,1);});}

/* ========================= PARTICLES / TRACERS ========================= */
function spawnBurst(x,y,z,n,col,spd,size){
  for(let i=0;i<n;i++){
    if(PARTS.length>460)break;
    PARTS.push({x:x,y:y,z:z,
      vx:rnd(-spd,spd)*0.55,vy:rnd(-spd,spd)*0.55+1.4,vz:rnd(-spd,spd)*0.55,
      life:rnd(0.5,1.3),t:0,size:rnd(size*0.6,size*1.5),col:col,g:rnd(7,11)});}}
function updateParticles(dt){
  for(let i=PARTS.length-1;i>=0;i--){
    const q=PARTS[i]; q.t+=dt;
    if(q.t>q.life){PARTS.splice(i,1);continue;}
    q.vy-=q.g*dt; q.x+=q.vx*dt; q.y+=q.vy*dt; q.z+=q.vz*dt;
    const g=baseGround(q.x,q.z);
    if(q.y<g+0.02){q.y=g+0.02;q.vy*=-0.2;q.vx*=0.5;q.vz*=0.5;}}
  for(let i=TRACERS.length-1;i>=0;i--){TRACERS[i].t-=dt;if(TRACERS[i].t<=0)TRACERS.splice(i,1);}}
function drawParticles(){
  for(let i=0;i<PARTS.length;i++){const q=PARTS[i];
    const k=1-q.t/q.life;
    drawMesh(CUBE,m4chain(m4t(q.x,q.y,q.z),m4s(q.size*k,q.size*k,q.size*k)),q.col,1,0.1*k,1,1);}
  for(let i=0;i<TRACERS.length;i++){const T=TRACERS[i];
    const d=[T.b[0]-T.a[0],T.b[1]-T.a[1],T.b[2]-T.a[2]];
    const L=Math.hypot(d[0],d[1],d[2])||1;
    const dx=d[0]/L,dy=d[1]/L,dz=d[2]/L;
    const cb=Math.hypot(dx,dz);
    const m=m4chain(m4t((T.a[0]+T.b[0])/2,(T.a[1]+T.b[1])/2,(T.a[2]+T.b[2])/2),
      m4ry(Math.atan2(dx,dz)),m4rx(-Math.asin(clamp(dy,-1,1))),m4s(0.035,0.035,L));
    drawMesh(CUBE,m,T.c,1,0.9,1,1);}}

/* ========================= PICKUPS ========================= */
const PICKUPS=[
 {x:-39,z:-20.5,y:0.1,type:'med'},
 {x:44,z:-16,y:1.2,type:'shells'},
 {x:0,z:-18.5,y:0.25,type:'ammo'},
 {x:-4,z:11,y:0.25,type:'med'},
 {x:22,z:8,y:0.25,type:'ammo'},
 {x:7.7,z:-19.5,y:0.3,type:'rifle'},
 {x:-30,z:-6,y:0.25,type:'shells'},
 {x:40,z:-8,y:0.25,type:'med'},
 {x:-12,z:-4,y:0.25,type:'ammo'}];
PICKUPS.forEach(k=>{k.taken=false;k.bob=rnd(0,6);});
const PCOL={med:[1,0.25,0.25],shells:[1,0.75,0.2],rifle:[0.3,1,0.5],ammo:[0.4,0.8,1]};
function respawnPickups(){
  PICKUPS.forEach(k=>{ if(!k.taken)return;
    if(Math.random()<0.65){k.taken=false;}});}
function updatePickups(dt){
  for(let i=0;i<PICKUPS.length;i++){
    const k=PICKUPS[i]; if(k.taken)continue;
    k.bob+=dt*2.2;
    if(dist2(p.x,p.z,k.x,k.z)<1.9&&Math.abs((p.y-EYE)-k.y)<2.2){
      if(k.type==='med'){
        if(p.hp>=100)continue;
        p.hp=Math.min(100,p.hp+32); updateHealthHUD();
        msg('FIRST-AID +32'); sfxPickup('med');}
      else if(k.type==='shells'){WEAPONS[1].res+=14;msg('+14 SHELLS');sfxPickup('ammo');}
      else if(k.type==='rifle'){WEAPONS[2].res+=45;msg('+45 RIFLE ROUNDS');sfxPickup('ammo');}
      else{WEAPONS[1].res+=6;WEAPONS[2].res+=24;msg('+AMMUNITION');sfxPickup('ammo');}
      k.taken=true; updateAmmoHUD();}}
  /* weapon pickups */
  if(!WEAPONS[1].unlock&&dist2(p.x,p.z,SHOT_SPAWN[0],SHOT_SPAWN[2])<3.0){
    WEAPONS[1].unlock=true;WEAPONS[1].mag=6;WEAPONS[1].res=WEAPONS[1].res0;
    msg('PICKED UP: SHOTGUN — press 2'); sfxWeapon(); switchTo(1); S.score+=250;}
  if(!WEAPONS[2].unlock&&dist2(p.x,p.z,RIFLE_SPAWN[0],RIFLE_SPAWN[2])<2.6){
    WEAPONS[2].unlock=true;WEAPONS[2].mag=30;WEAPONS[2].res=WEAPONS[2].res0;
    msg('PICKED UP: RIFLE — press 3'); sfxWeapon(); switchTo(2); S.score+=250;}
  updateScoreHUD();}
function drawPickups(t){
  for(let i=0;i<PICKUPS.length;i++){
    const k=PICKUPS[i]; if(k.taken)continue;
    const y=k.y+Math.sin(k.bob)*0.07;
    const col=PCOL[k.type];
    const isMed=k.type==='med';
    drawMesh(CUBE,m4chain(m4t(k.x,y,k.z),m4ry(t*1.2),m4s(isMed?0.34:0.4,isMed?0.3:0.24,isMed?0.3:0.34)),col,1,0.75,1,1);
    drawMesh(CUBE,m4chain(m4t(k.x,y,k.z),m4ry(t*1.2),
      m4s(isMed?0.36:0.42,isMed?0.09:0.08,isMed?0.12:0.1)),[1,1,1],1,0.9,1,1);}
  [SHOT_SPAWN,RIFLE_SPAWN].forEach((sp,i)=>{
    if(WEAPONS[i+1].unlock)return;
    const y=sp[1]+Math.sin(t*2+i)*0.05;
    drawMesh(CUBE,m4chain(m4t(sp[0],y,sp[2]),m4ry(t*0.8+i),m4rz(0.1),m4s(0.9,0.14,0.16)),
      i?[0.35,0.32,0.26]:[0.3,0.3,0.33],1,0.15,1,1);});}

/* ========================= AMBIENT LIFE ========================= */
(function(){
  const spots=[[-45,6.6,16],[4,6.6,34],[-43.4,2.6,-13.4],[-30.4,2.6,-20.5],[-28.4,2.4,-20.5],
    [14,3,-19.5],[-4,1.9,5]];
  spots.forEach(sp=>{for(let i=0;i<7;i++)
    MOTHS.push({c:sp,r:rnd(0.35,1.0),a:rnd(0,6.3),sp:rnd(1.4,3.4),h:rnd(-0.5,0.5),ph:rnd(0,6)});});
})();
function drawMoths(t){
  for(let i=0;i<MOTHS.length;i++){const m=MOTHS[i];
    const a=m.a+t*m.sp;
    const x=m.c[0]+Math.cos(a)*m.r, y=m.c[1]+Math.sin(t*2.2+m.ph)*0.35+m.h,
          z=m.c[2]+Math.sin(a*1.3)*m.r;
    drawMesh(CUBE,m4chain(m4t(x,y,z),m4s(0.045,0.045,0.045)),[1,0.95,0.75],1,0.7,1,1);}}
const PAPER={x:-20,z:2,y:0.1,a:0};
function drawPaper(dt,t){
  PAPER.x+=dt*3.2; PAPER.z+=Math.sin(t*0.7)*dt*1.6; PAPER.a+=dt*2.6;
  if(PAPER.x>52){PAPER.x=-52;PAPER.z=rnd(-30,30);}
  if(PAPER.x>-34&&PAPER.x<-6&&PAPER.z>8&&PAPER.z<24){PAPER.z=6;}
  drawMesh(CUBE,m4chain(m4t(PAPER.x,0.12+Math.abs(Math.sin(t*3.1))*0.14,PAPER.z),
    m4ry(PAPER.a),m4rx(Math.sin(t*3.1)*0.5),m4s(0.34,0.015,0.46)),[0.93,0.9,0.84],1,0.05,1,1);}
/* ========================= GAME STATE / WAVES ========================= */
const S={mode:'title',wave:0,toSpawn:0,spawnT:0,inBreak:false,breakT:0,breakMax:16,
  kills:0,hs:0,score:0,dawn:0,dawnT:0,time:0,shakeS:0};
const TOUR=[
 {p:[13,3.8,20],q:[34,7.6,33]},   /* the neon sign across the lot */
 {p:[17,2.5,-3],q:[-6,2.0,-21]},  /* motel row + walkway */
 {p:[-1,3.0,31],q:[-24,-1.2,15]}, /* the drained pool */
 {p:[21,2.7,7],q:[39,1.6,-13]},   /* the diner */
 {p:[-27,2.5,-3],q:[-42,1.8,-17]} /* office, NO VACANCY, payphone */
];
const WAVE_N=[6,9,13,16,20,24,28,32,36,40];
const WAVE_ALIVE=[8,10,12,14,16,18,20,22,24,26];
const WAVE_GAP=[1.75,1.6,1.45,1.3,1.18,1.06,0.96,0.86,0.76,0.64];
function show(id,on){$(id).classList.toggle('hide',!on);}
function updateHealthHUD(){
  const v=Math.max(0,Math.ceil(p.hp));
  $('hpBar').style.width=v+'%'; $('hpText').textContent=v;
  $('lowhp').classList.toggle('beat',v<=35);
  $('lowhp').style.opacity=v<=35?1:0;}
function updateScoreHUD(){
  $('score').textContent=S.score; $('kills').textContent=S.kills; $('hs').textContent=S.hs;}
function updateWaveHUD(){
  $('waveN').textContent=Math.max(1,S.wave);
  $('zLeft').textContent=S.toSpawn+aliveCount();}
function startWave(n){
  S.wave=n; S.inBreak=false;
  S.toSpawn=WAVE_N[n-1]; S.spawnT=0.7;
  phoneRing(false);
  $('breakBox').style.display='none';
  banner('WAVE '+n,n===10?'LAST CALL — HOLD TILL DAWN':(n===6?'SOMETHING BIG IS COMING':'THEY KEEP COMING'));
  sfxSting();
  if(n>=3)msg('YOU HEAR RUNNING IN THE DARK');
  if(n>=6)msg('SOMETHING HEAVY IS MOVING OUT THERE');
  updateWaveHUD();}
function beginBreak(){
  S.inBreak=true; S.breakMax=rnd(15,20); S.breakT=S.breakMax;
  phoneRing(true);
  banner('WAVE '+S.wave+' CLEARED','THE PHONE IS RINGING');
  sfxWaveClear(); respawnPickups();
  $('breakBox').style.display='block';
  subbanner('RELOAD — RESUPPLY — REPOSITION');}
function winGame(){
  S.mode='win'; S.dawnT=1; phoneRing(false);
  $('breakBox').style.display='none'; subbanner('');
  $('wKills').textContent=S.kills; $('wHs').textContent=S.hs; $('wScore').textContent=S.score;
  show('scrWin',true);
  if(document.exitPointerLock)document.exitPointerLock();
  sfxWaveClear(); setTimeout(sfxWaveClear,420);}
function die(){
  S.mode='dead'; phoneRing(false);
  $('breakBox').style.display='none'; subbanner('');
  $('dWave').textContent=S.wave; $('dKills').textContent=S.kills;
  $('dHs').textContent=S.hs; $('dScore').textContent=S.score;
  show('scrDead',true);
  if(document.exitPointerLock)document.exitPointerLock();}
function startGame(){
  aInit(); aResume(); lockPointer();
  resetGame(); S.mode='play';
  show('scrTitle',false); show('scrPause',false);
  startWave(1); updateHealthHUD(); updateAmmoHUD(); updateScoreHUD();}
function resetGame(){
  ZOMB.length=0; PARTS.length=0; TRACERS.length=0;
  p.x=6;p.y=EYE;p.z=-9;p.yaw=0.43;p.pitch=-0.02;p.vy=0;p.hp=100;
  p.reloading=0;p.cool=0;p.cur=0;p.recoil=0;p.spread=0;p.shake=0;p.vig=0;p.climb=0;
  p.kickX=0;p.bob=0;p.bobAmt=0;p.hurtcd=0;
  WEAPONS[0].mag=12;WEAPONS[0].res=Infinity;WEAPONS[0].unlock=true;
  WEAPONS[1].mag=0;WEAPONS[1].res=0;WEAPONS[1].unlock=false;
  WEAPONS[2].mag=0;WEAPONS[2].res=0;WEAPONS[2].unlock=false;
  PICKUPS.forEach(k=>{k.taken=false;});
  S.wave=0;S.kills=0;S.hs=0;S.score=0;S.inBreak=false;S.toSpawn=0;S.dawn=0;S.dawnT=0;S.time=0;}
function restartGame(){
  show('scrDead',false); show('scrWin',false); show('scrTitle',false); show('scrPause',false);
  startGame();}
function pauseGame(){
  if(S.mode!=='play')return; S.mode='pause'; show('scrPause',true);
  if(document.exitPointerLock)document.exitPointerLock();}
function resumeGame(){
  if(S.mode!=='pause')return; S.mode='play'; show('scrPause',false);
  aResume(); lockPointer();}
function updateWaves(dt){
  if(S.inBreak){
    S.breakT-=dt;
    $('btCount').textContent=Math.max(0,Math.ceil(S.breakT));
    if(S.breakT<=0){ $('breakBox').style.display='none'; subbanner('');
      startWave(S.wave+1);}
    return;}
  if(S.toSpawn>0){
    const alive=aliveCount();
    S.spawnT-=dt;
    if(S.spawnT<=0&&alive<WAVE_ALIVE[S.wave-1]){
      spawnZombie(pickType(S.wave),S.wave);
      S.toSpawn--;
      S.spawnT=WAVE_GAP[S.wave-1]*rnd(0.6,1.35);
      if(Math.random()<0.18&&S.toSpawn>0){spawnZombie(pickType(S.wave),S.wave);S.toSpawn--;}}
  }else if(aliveCount()===0){
    if(S.wave>=10)winGame(); else beginBreak();}
  updateWaveHUD();}

/* ========================= PLAYER UPDATE ========================= */
function updatePlayer(dt){
  p.cool=Math.max(0,p.cool-dt);
  if(p.reloading>0){p.reloading-=dt;if(p.reloading<=0){p.reloading=0;finishReload();}}
  if(held.fire&&WEAPONS[p.cur].auto)fire();
  p.recoil*=Math.exp(-9*dt);
  p.kickX*=Math.exp(-7*dt);
  p.spread=Math.max(0,p.spread-dt*2.4);
  p.shake=Math.max(0,p.shake-dt*2.4);
  p.vig=Math.max(0,p.vig-dt*1.5);
  p.dflash=Math.max(0,p.dflash-dt*3.2);
  muzzleT=Math.max(0,muzzleT-dt);
  let mf=0,ms=0;
  if(keys.KeyW||keys.ArrowUp)mf+=1;
  if(keys.KeyS||keys.ArrowDown)mf-=1;
  if(keys.KeyD||keys.ArrowRight)ms+=1;
  if(keys.KeyA||keys.ArrowLeft)ms-=1;
  const feet=p.y-EYE;
  const onLadder=inR(POOL,p.x,p.z)&&dist2(p.x,p.z,LADDER.x,LADDER.z)<3.0&&keys.KeyW&&feet<-0.3;
  if(onLadder)p.climb=1;
  if(p.climb){
    p.vy=0; p.y+=2.1*dt;
    if(p.y-EYE>=-0.02){p.climb=0;p.x=-7.2;p.z=LADDER.z;}
    else if(!keys.KeyW)p.climb=0;
  }
  const len=Math.hypot(mf,ms);
  if(len>0&&!p.climb){
    const sy=Math.sin(p.yaw),cy=Math.cos(p.yaw);
    const fx=-sy*fy0(mf),fz=-cy*mf,rx=cy*ms,rz=-sy*ms;
    let dx=(fx*0+(-sy*mf))+(cy*ms), dz=(-cy*mf)+(-sy*ms);
    const L=Math.hypot(dx,dz)||1;dx/=L;dz/=L;
    const sprint=(keys.ShiftLeft||keys.ShiftRight)&&mf>0;
    const spd=sprint?7.0:4.5;
    const r=tryMove(p.x,p.z,dx*spd*dt,dz*spd*dt,PR,feet,STEP);
    p.x=r[0];p.z=r[1];
    p.speed=spd;
    p.bobAmt+=( (sprint?1.35:1) - p.bobAmt)*Math.min(1,dt*8);
    p.bob+=dt*(sprint?13.5:9.5);
  }else{p.speed=0;p.bobAmt+= (0-p.bobAmt)*Math.min(1,dt*6);p.bob+=dt*2.4;}
  /* gravity */
  const g=groundAt(p.x,p.z,p.y-EYE);
  if(!p.climb){
    p.vy-=19*dt; p.y+=p.vy*dt;
    if(p.y-EYE<=g){ if(p.vy<-8)sfxFoot('brute'); p.y=g+EYE; p.vy=0; }
    else if(p.y-EYE>g+0.02&&p.vy===0)p.vy=0;}
  /* ground safety */
  if(p.y-EYE<g){p.y=g+EYE;p.vy=0;}
}
function fy0(v){return v;}

/* ========================= RENDER ========================= */
const IDENT=m4();
let PROJ=null,ASPECT=1,FOV=1.31;
const LP=new Float32Array(MAX_L*4),LC=new Float32Array(MAX_L*3),LD=new Float32Array(MAX_L*3);
function resize(){
  const dpr=Math.min(window.devicePixelRatio||1,1.75);
  canvas.width=Math.floor(innerWidth*dpr); canvas.height=Math.floor(innerHeight*dpr);
  canvas.style.width=innerWidth+'px'; canvas.style.height=innerHeight+'px';
  ASPECT=canvas.width/canvas.height;
  PROJ=m4persp(FOV,ASPECT,0.06,320);}
window.addEventListener('resize',resize); resize();
function h1(n){const s=Math.sin(n*127.1)*43758.5453;return s-Math.floor(s);}
function lightMod(l,t){
  switch(l.mod){
    case 'flicker':{const n=h1(Math.floor(t*6)+l.phase);
      return n<0.1?0.18:0.72+0.28*Math.sin(t*10.5+l.phase);}
    case 'buzz':{return h1(Math.floor(t*22)+l.phase)<0.05?0.3:1;}
    case 'neon':{const a=h1(Math.floor(t*2.6)+l.phase)<0.2?0.14:1;
      const b=h1(Math.floor(t*15)+l.phase)<0.05?0.1:1;
      return a*b*(0.78+0.22*Math.sin(t*29+l.phase));}
    case 'tv':{return 0.45+0.65*h1(Math.floor(t*26)+l.phase);}
    default:return 1;}}
function updateSign(t){
  for(let i=0;i<EMIS.length;i++){
    const e=EMIS[i];
    if(e.letter===5){ // the dead "I"
      const k=h1(Math.floor(t*7.3));
      const fast=0.55+0.45*Math.sin(t*41);
      e.mul=(k<0.42?0.05:1)*fast;
      if(k<0.06&&Math.random()<0.2)sfxBuzzLetter(true);
    }else if(e.letter===-1){ e.mul=0.8+0.2*Math.sin(t*3); }
    else if(e.letter===-2){ e.mul=0.9+0.1*Math.sin(t*7); }
    else{ e.mul=0.9+0.1*Math.sin(t*23+i); }}
  if(phoneRingState()){}
}
function phoneRingState(){return AUD.phoneOn;}
function updateLights(t){
  const cand=[];
  for(let i=0;i<LIGHTS.length;i++){
    const l=LIGHTS[i]; if(!l.on)continue;
    const m=lightMod(l,t); const ii=l.base*m;
    if(ii<0.05)continue;
    const d=dist2(l.p[0],l.p[2],p.x,p.z)+(l.p[1]-p.y)*(l.p[1]-p.y);
    cand.push([ii/(1+d*0.045),l,ii]);}
  cand.sort((a,b)=>b[0]-a[0]);
  const n=Math.min(MAX_L,cand.length);
  LP.fill(0);LC.fill(0);LD.fill(0);
  for(let i=0;i<n;i++){
    const l=cand[i][1],ii=cand[i][2];
    LP[i*4]=l.p[0];LP[i*4+1]=l.p[1];LP[i*4+2]=l.p[2];LP[i*4+3]=l.spot?l.spot.cos:0;
    LC[i*3]=l.c[0]*ii;LC[i*3+1]=l.c[1]*ii;LC[i*3+2]=l.c[2]*ii;
    if(l.spot){const d=l.spot.d,L=Math.hypot(d[0],d[1],d[2])||1;
      LD[i*3]=d[0]/L;LD[i*3+1]=d[1]/L;LD[i*3+2]=d[2]/L;}}
  gl.uniform1i(U.uLN,n);
  gl.uniform4fv(U.uLP,LP);gl.uniform3fv(U.uLC,LC);gl.uniform3fv(U.uLD,LD);}
function skyColors(){
  const d=S.dawn;
  return {
    fog:[lerp(0.055,0.86,d*0.9),lerp(0.07,0.56,d*0.9),lerp(0.095,0.34,d*0.9)],
    amb:[lerp(0.095,0.32,d),lerp(0.108,0.35,d),lerp(0.145,0.43,d)],
    fogD:lerp(0.00052,0.00022,d)};}
function render(dt,t){
  gl.viewport(0,0,canvas.width,canvas.height);
  const c=skyColors();
  gl.clearColor(c.fog[0],c.fog[1],c.fog[2],1);
  gl.clear(gl.COLOR_BUFFER_BIT|gl.DEPTH_BUFFER_BIT);
  const sy=Math.sin(p.yaw),cy=Math.cos(p.yaw),cp=Math.cos(p.pitch),sp=Math.sin(p.pitch);
  const fwd=[-sy*cp,sp,-cy*cp], rgt=[cy,0,-sy], up=[sp*sy,cp,sp*cy];
  /* sky */
  gl.disable(gl.DEPTH_TEST); gl.depthMask(false);
  gl.useProgram(SKY);
  gl.bindBuffer(gl.ARRAY_BUFFER,skyBuf);
  gl.enableVertexAttribArray(SA_P);
  gl.vertexAttribPointer(SA_P,2,gl.FLOAT,false,0,0);
  gl.uniform3f(SU.uCamR,rgt[0],rgt[1],rgt[2]);
  gl.uniform3f(SU.uCamU,up[0],up[1],up[2]);
  gl.uniform3f(SU.uCamF,fwd[0],fwd[1],fwd[2]);
  gl.uniform1f(SU.uTan,1/Math.tan(FOV/2));
  gl.uniform1f(SU.uAsp,ASPECT);
  gl.uniform1f(SU.uDawn,S.dawn);
  gl.uniform1f(SU.uT,t);
  gl.drawArrays(gl.TRIANGLES,0,3);
  gl.disableVertexAttribArray(SA_P);
  gl.enable(gl.DEPTH_TEST); gl.depthMask(true);
  /* world (attribute locations are global state — re-enable after sky pass) */
  curProg=null; useProg();
  const sh=p.shake;
  const vy=p.pitch+(Math.random()-0.5)*sh*0.075;
  const yw=p.yaw+(Math.random()-0.5)*sh*0.075;
  const bobY=Math.sin(p.bob)*0.035*p.bobAmt;
  const view=m4chain(m4rx(-vy),m4ry(-yw),m4t(-p.x,-(p.y+bobY),-p.z));
  gl.uniformMatrix4fv(U.uProj,false,PROJ);
  gl.uniformMatrix4fv(U.uView,false,view);
  gl.uniform3f(U.uCam,p.x,p.y,p.z);
  gl.uniform3f(U.uAmb,c.amb[0],c.amb[1],c.amb[2]);
  gl.uniform3f(U.uFog,c.fog[0],c.fog[1],c.fog[2]);
  gl.uniform1f(U.uFogD,c.fogD);
  updateLights(t);
  for(let i=0;i<WORLD.length;i++)drawMesh(WORLD[i],M4I,null,0,0,1,1);
  for(let i=0;i<EMIS.length;i++){const e=EMIS[i];
    drawMesh(e.mesh,M4I,null,0,0,Math.max(0.02,e.mul||e.i||1),1);}
  for(let i=0;i<ZOMB.length;i++)drawZombie(ZOMB[i]);
  drawPickups(t); drawMoths(t); drawPaper(dt,t); drawParticles();
  drawViewmodel();}
/* ========================= MAIN LOOP ========================= */
let lastT=0;
function frame(ts){
  requestAnimationFrame(frame);
  const t=ts/1000;
  let dt=lastT?Math.min(0.05,t-lastT):0.016;
  lastT=t; S.time=t;
  try{
    if(S.mode==='title'){
      const T=7,tt=(t/T)%TOUR.length,i=Math.floor(tt),f=tt-i;
      const a=TOUR[i],b=TOUR[(i+1)%TOUR.length];
      const k=(function(u){u=clamp(u,0,1);return u*u*(3-2*u);})((f-0.68)/0.32);
      p.x=lerp(a.p[0],b.p[0],k)+Math.sin(t*0.31)*0.5;
      p.y=lerp(a.p[1],b.p[1],k)+Math.sin(t*0.23)*0.22;
      p.z=lerp(a.p[2],b.p[2],k)+Math.cos(t*0.27)*0.5;
      const tx=lerp(a.q[0],b.q[0],k),ty=lerp(a.q[1],b.q[1],k),tz=lerp(a.q[2],b.q[2],k);
      S.dawn=0.05;
      updateSign(t); updateParticles(dt); updateZombies(0.0001);
      const dx=tx-p.x,dz=tz-p.z;
      p.yaw=Math.atan2(-dx,-dz);
      p.pitch=Math.atan2(ty-p.y,Math.hypot(dx,dz));
    }else if(S.mode==='play'){
      updatePlayer(dt); updateZombies(dt); updateWaves(dt); updatePickups(dt);
      updateParticles(dt); updateSign(t);
      S.dawn=Math.pow(clamp((S.wave-1)/10,0,1),1.25)*0.8;
      updateCrosshair(); updatePrompt();
      $('vig').style.opacity=clamp(p.vig*0.9,0,1);
      $('dfash').style.opacity=clamp(p.dflash*0.6,0,1);
    }else if(S.mode==='dead'){
      p.pitch+=(0.9-p.pitch)*Math.min(1,dt*2);
      p.y+=(1.0-p.y)*Math.min(1,dt*1.6);
      p.shake=Math.max(0,p.shake-dt);
      updateParticles(dt); updateZombies(dt); updateSign(t);
      $('vig').style.opacity=clamp(Math.max(p.vig,0.55),0,1);
      $('dfash').style.opacity=0;
    }else if(S.mode==='win'){
      S.dawn+=(1-S.dawn)*Math.min(1,dt*0.35);
      p.yaw+=dt*0.045; p.pitch+=(0.16-p.pitch)*Math.min(1,dt*0.8);
      p.y=2.6; updateSign(t); updateParticles(dt);
      $('vig').style.opacity=0; $('dfash').style.opacity=0;
    }else{ /* pause */
      updateSign(t);}
    render(dt,t);
  }catch(e){showErr(e);}}
function updateCrosshair(){
  const gap=7+p.spread*26+(p.speed>5?3:0)+(p.reloading>0?14:0);
  $('cl').style.transform='translateX('+(-gap-9)+'px)';
  $('cr').style.transform='translateX('+(gap)+'px)';
  $('cu').style.transform='translateY('+(-gap-9)+'px)';
  $('cd').style.transform='translateY('+(gap)+'px)';}
function updatePrompt(){
  let txt='';
  if(!WEAPONS[1].unlock&&dist2(p.x,p.z,SHOT_SPAWN[0],SHOT_SPAWN[2])<9)txt='SHOTGUN IN THE STATION WAGON — WALK UP TO IT';
  else if(!WEAPONS[2].unlock&&dist2(p.x,p.z,RIFLE_SPAWN[0],RIFLE_SPAWN[2])<7)txt='RIFLE BEHIND THE OFFICE COUNTER';
  else if(p.reloading>0)txt='RELOADING…';
  else if(WEAPONS[p.cur].mag===0&&WEAPONS[p.cur].res<=0)txt='NO AMMO — PRESS 1 FOR PISTOL';
  const el=$('prompt');
  if(txt){el.textContent=txt;el.classList.add('go');}else el.classList.remove('go');}
/* ========================= INIT ========================= */
document.addEventListener('mousedown',e=>{
  if(e.button!==0)return;
  if(S.mode==='title'){startGame();return;}
  if(S.mode==='pause'){resumeGame();return;}
  if(S.mode==='dead'||S.mode==='win'){restartGame();return;}});
window.addEventListener('mouseup',e=>{if(e.button===0)held.fire=false;});
updateHealthHUD(); updateAmmoHUD(); updateScoreHUD(); updateWaveHUD();
if(location.search.indexOf('play')>=0){startGame();}
requestAnimationFrame(frame);
</script>
</body>
</html>