Zombie FPS
GPT 6 Sol · full

Reviews & generation details
Reviews
No human review yet. Unreviewed does not mean unsuccessful.
Generation context
Imported historical artifact. Its exact original instructions and lineage are not known.
Generation prompt · exact text unavailable
See the current prompt for reference. It may differ from the instructions used to generate this artifact.
Inspect original source 36,719 bytes · SHA-256 7d96d25b82a9
<!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>
*{box-sizing:border-box}html,body{margin:0;width:100%;height:100%;overflow:hidden;background:#03070c;color:#e8e6d7;font-family:Arial,Helvetica,sans-serif}canvas{position:absolute;inset:0;width:100%;height:100%;display:block}#game{background:#071018}#fx{pointer-events:none}#hud{position:absolute;inset:0;pointer-events:none;text-shadow:0 2px 5px #000;font-family:'Arial Narrow',Arial,sans-serif;display:none}#top{position:absolute;top:20px;left:24px;display:flex;gap:18px;align-items:center}#waveBadge{font-size:13px;letter-spacing:3px;color:#ffbf8a;border:1px solid #af623c;background:#180d0ab8;padding:8px 12px}#remaining{font-size:13px;letter-spacing:2px;color:#d5dadd}#score{position:absolute;top:27px;right:26px;font-size:13px;letter-spacing:2px;color:#b3bfba}#bottom{position:absolute;bottom:25px;left:26px;right:26px;display:flex;align-items:end;justify-content:space-between}#healthBlock{width:190px}#healthLabel{font-size:11px;letter-spacing:3px;color:#d0aaa0;margin-bottom:7px}#healthTrack{height:8px;border:1px solid #b26653;background:#1c191b}#healthFill{width:100%;height:100%;background:linear-gradient(90deg,#9b342a,#e8875e);box-shadow:0 0 12px #d45039}#ammoBlock{text-align:right}#weaponName{font-size:12px;letter-spacing:3px;color:#cdb999}#ammo{font-size:38px;font-weight:bold;line-height:1.1;color:#f2e9d3}#ammo small{font-size:17px;color:#9aa9a9;font-weight:normal}#crosshair{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);width:24px;height:24px}#crosshair:before,#crosshair:after{content:'';position:absolute;background:#e4dfce;box-shadow:0 0 4px #000}#crosshair:before{width:2px;height:6px;left:11px;top:0;box-shadow:0 18px #e4dfce}#crosshair:after{height:2px;width:6px;top:11px;left:0;box-shadow:18px 0 #e4dfce}#crosshair.hit:before,#crosshair.hit:after{background:#ff694f}#banner{position:absolute;top:18%;left:0;width:100%;text-align:center;font-size:34px;letter-spacing:9px;color:#ffe8bd;opacity:0;transition:opacity .25s;font-weight:700;text-shadow:0 0 25px #e05c39,0 4px 10px #000}#tip{position:absolute;bottom:82px;left:50%;transform:translateX(-50%);font-size:12px;letter-spacing:2px;color:#e5d4ad;opacity:0;white-space:nowrap}#clock{position:absolute;top:22px;left:50%;transform:translateX(-50%);font-size:12px;letter-spacing:3px;color:#99aeb9}#overlay{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background:radial-gradient(ellipse at 50% 40%,#0f1a21c9,#020409f4 75%);text-align:center;z-index:10}#panel{max-width:620px;padding:35px 22px}#eyebrow{letter-spacing:7px;font-size:12px;color:#f99f70;margin-bottom:15px}h1{font-size:clamp(42px,7vw,88px);line-height:.88;letter-spacing:-3px;margin:0 0 20px;color:#ffdcc1;text-shadow:0 0 30px #e44b35,0 3px 0 #852b26}h1 span{display:block;color:#fff2d5;font-size:.53em;letter-spacing:10px;margin:8px 0 0}#desc{font-size:15px;color:#b6c3c5;line-height:1.7;margin:24px auto;max-width:510px}#start{border:1px solid #e18f68;background:#a23e2a;color:#fff1db;padding:16px 39px;letter-spacing:4px;font-weight:bold;font-size:13px;cursor:pointer;box-shadow:0 0 25px #bd4b3c66}#start:hover{background:#c45838}#controls{color:#84959b;font-size:11px;line-height:2;letter-spacing:1px;margin-top:24px}#danger{position:absolute;inset:0;pointer-events:none;background:radial-gradient(ellipse at center,transparent 42%,#a00b13 100%);opacity:0;mix-blend-mode:screen}#flash{position:absolute;inset:0;background:#fff;opacity:0;pointer-events:none}#minimap{display:none}</style></head><body><canvas id="game"></canvas><canvas id="fx"></canvas><div id="hud"><div id="top"><div id="waveBadge">WAVE <span id="wave">01</span> / 10</div><div id="remaining">0 HOSTILES REMAIN</div></div><div id="clock">03:10 AM · UNTIL DAWN</div><div id="score">KILLS 000</div><div id="banner"></div><div id="tip"></div><div id="crosshair"></div><div id="bottom"><div id="healthBlock"><div id="healthLabel">VITALS <span id="hpText">100</span></div><div id="healthTrack"><div id="healthFill"></div></div></div><div id="ammoBlock"><div id="weaponName">M1911 PISTOL</div><div id="ammo">12 <small>/ ∞</small></div></div></div></div><div id="danger"></div><div id="flash"></div><div id="overlay"><div id="panel"><div id="eyebrow">3:10 A.M. · ROUTE 16 · NO SIGNAL</div><h1>LAST CALL<span>AT THE STARLITE</span></h1><div id="desc">The lights are still on. The guests are not.<br>Hold the motor court until sunrise. Ten waves between you and morning.</div><button id="start">CLICK TO SURVIVE</button><div id="controls">WASD MOVE · MOUSE AIM & FIRE · SHIFT SPRINT · R RELOAD<br>1 / 2 / 3 OR SCROLL SWITCH WEAPONS · ESC PAUSE<br>SHOTGUN: STATION WAGON · RIFLE: OFFICE COUNTER</div></div></div><script>
'use strict';
const canvas=document.getElementById('game'),ctx=canvas.getContext('2d',{alpha:false});const fx=document.getElementById('fx'),fctx=fx.getContext('2d');let W=innerWidth,H=innerHeight,DPR=1;function resize(){DPR=Math.min(devicePixelRatio||1,1.5);W=innerWidth;H=innerHeight;canvas.width=Math.round(W*DPR);canvas.height=Math.round(H*DPR);fx.width=canvas.width;fx.height=Math.round(H*DPR);ctx.setTransform(DPR,0,0,DPR,0,0);fctx.setTransform(DPR,0,0,DPR,0,0)}
addEventListener('resize',resize);resize();
const $=id=>document.getElementById(id), TAU=Math.PI*2, clamp=(v,a,b)=>Math.max(a,Math.min(b,v)),rand=(a,b)=>a+Math.random()*(b-a),dist=(a,b,c,d)=>Math.hypot(a-c,b-d);
let state='title',time=0,last=0,wave=0,remaining=0,spawned=0,spawnTimer=0,breakTime=0,bannerTime=0,tipTime=0,kills=0,shake=0,hurt=0,fireFlash=0,hitTime=0,phoneTime=0,sky=0,keys={},mouseDown=false,zombies=[],corpses=[],particles=[],pickups=[],objects=[],colliders=[],lights=[],decals=[];
const P={x:0,z:-2,y:1.65,yaw:0,pitch:0,hp:100,walk:0,weapon:0,ammo:[12,6,24],reserve:[Infinity,30,90],owned:[true,false,false],reload:0,fire:0,recoil:0,step:0};
const guns=[{name:'M1911 PISTOL',mag:12,delay:.26,damage:37,spread:.014,reload:1.3,pellets:1},{name:'12-GAUGE SHOTGUN',mag:6,delay:.85,damage:25,spread:.095,reload:2.1,pellets:8},{name:'SERVICE RIFLE',mag:24,delay:.095,damage:22,spread:.022,reload:1.7,pellets:1}];
function box(x,y,z,w,h,d,color,opts={}){objects.push({type:'box',x,y,z,w,h,d,color,...opts});if(opts.solid)colliders.push({x:x-w/2,z:z-d/2,w,d})}function sign(text,x,y,z,size,color,opts={}){objects.push({type:'sign',text,x,y,z,size,color,...opts})}function light(x,y,z,color,r=5){lights.push({x,y,z,color,r})}function prop(x,y,z,w,h,d,color,opts={}){box(x,y,z,w,h,d,color,opts)}
function world(){objects=[];colliders=[];lights=[];decals=[];
// Asphalt, pavement, stripes and edge of the highway.
for(let gx=-24;gx<=24;gx+=4)for(let gz=-20;gz<=20;gz+=4){if(gx>=8&&gx<=16&&gz>=-16&&gz<=-8)continue;box(gx,-.13,gz,3.99,.19,3.99,(Math.abs(gx*7+gz*3)%5===0?'#202c2f':'#1d292c'),{top:(Math.abs(gx+gz*3)%7===0?'#2b3435':'#273235')});}box(0,-.16,-25,56,.12,3,'#303435');
for(let i=-24;i<=24;i+=7){box(i,-.075,-24.9,3,.012,.08,'#a09064')}
for(let i=-19;i<=19;i+=7){for(let side of [-1,1])box(i,-.026,side*4.2,.09,.015,4.2,'#8e8771')}
for(let i=0;i<32;i++){let x=rand(-24,24),z=rand(-21,18);box(x,-.023,z,rand(.25,.9),.005,.014,'#070f12',{rotation:rand(-1,1)})}
// Fence posts and chain-link rails, warning signs.
for(let x=-26;x<=26;x+=2){for(let z of [-23,24]){box(x,1.05,z,.055,2.15,.055,'#677677');box(x,.72,z,1.95,.024,.018,'#566263');box(x,1.86,z,1.95,.024,.018,'#566263')}}
for(let z=-23;z<=24;z+=2){for(let x of [-26,26]){box(x,1.05,z,.055,2.15,.055,'#657372');box(x,1.82,z,.02,.025,1.98,'#566263')}}
sign('KEEP OUT',-25.7,1.45,2,.21,'#cc8b6b');sign('ROUTE 16',12,1,-24,.3,'#ddd2a2');
// Motel row: room walls separated by columns and doors; one room open.
box(-1,2.65,19.25,30,.4,9.5,'#343639',{top:'#424748'});box(-1,1.4,23.6,30,2.8,.35,'#454948');box(-1,.12,14.85,30,.19,2.4,'#4c5050',{top:'#676967'});
for(let i=0;i<6;i++){let x=-13.5+i*5;box(x-2.48,1.38,19.1,.2,2.7,8.4,'#4b4b48',{solid:true});if(i===2){box(x-1.74,1.5,17.1,1.25,3,.23,'#585959',{solid:true});box(x+1.74,1.5,17.1,1.25,3,.23,'#585959',{solid:true});box(x,2.8,17.1,2.25,.4,.23,'#585959');box(x,1.3,23.1,4.5,2.6,.12,'#59615e');box(x-.9,.47,20.4,1.45,.65,2.3,'#857d6b');box(x-.9,.83,20.4,1.34,.12,1.9,'#c6c3ac');box(x+.95,.38,19.4,.55,.65,.45,'#746e60');box(x+.95,.76,19.4,.45,.12,.35,'#a6aca1');box(x+.35,.035,17.95,.62,.06,.38,'#98816d');}else box(x,1.5,17.1,4.75,3,.23,'#585959',{solid:true});box(x,1.53,17.0,1.55,2.55,.055,i===2?'#11191d':'#354045');if(i===2){box(x-.9,1.6,17.02,.16,2.6,.15,'#82948d');box(x+1,1.6,17.3,.08,2.6,1.1,'#73817a');box(x,0.07,20.1,3.3,.03,2.1,'#495253');box(x-.5,.35,20.7,1.6,.5,.9,'#696058');sign('OPEN',x,2.2,17,.17,'#b7d4cd')}else{box(x+.54,1.2,16.93,.075,.075,.04,'#a3a394')}sign(String(101+i),x-.65,2.24,16.91,.18,'#e6d7ad');box(x+1.65,1.53,16.92,.75,.85,.05,'#283940');box(x+1.65,1.53,16.87,.56,.62,.02,'#071920');light(x,2.55,15.7,i===2?'#98d5cf':'#e9ac65',5)}
box(-1,2.98,15.7,30,.15,2,'#3b4242',{top:'#565b5b'});box(-1,3.1,15.7,30,.08,2,'#aaa389');
// Office shell, glass frontage, desk and key cubbies.
box(-20,2.85,17.2,11,.38,12,'#444847',{top:'#4e5554'});box(-25.3,1.5,17.2,.35,3,11.9,'#4b4f4c',{solid:true});box(-20,1.5,23,.35,3,11,'#4e5350',{solid:true});box(-20,1.5,22.95,10.5,3,.3,'#4b5151',{solid:true});box(-23.5,1.55,11.4,3.4,3,.2,'#485250',{solid:true});box(-17.1,1.55,11.4,4.7,3,.18,'#4b5250',{solid:true});box(-21.8,1.75,11.35,1.1,2.15,.07,'#7b9a9c');box(-22.45,1.4,11.32,.06,2.8,.07,'#1c282c');box(-20,1.32,14.2,4.6,1.05,1.2,'#675748',{solid:true});box(-20,1.9,14.2,5,.14,1.5,'#98816b');for(let i=0;i<5;i++)for(let j=0;j<3;j++)box(-24.4+i*.23,1.5+j*.3,19,.12,.18,.06,'#a88763');sign('ROOM KEYS',-23.65,2.4,19,.17,'#f1d4a7');box(-17.1,1.72,20,.62,.62,.55,'#363b3d');box(-17.1,1.73,19.7,.5,.48,.03,'#a2bbb7',{tv:true});light(-19,2.55,17,'#c5ded2',7);sign('FRONT OFFICE',-20.8,2.75,11.18,.37,'#eac5a2');
// Payphone: exposed by the entrance.
box(-15.2,1.1,10.9,.7,2.1,.45,'#334950');box(-15.2,1.5,10.62,.53,.62,.08,'#bec7bd');box(-15.35,1.5,10.55,.15,.39,.1,'#283239');box(-15.16,.95,10.56,.26,.23,.07,'#b5aaa0');sign('PHONE',-15.2,2,10.55,.15,'#d3e0d7');
// Diner, passable central doorway.
box(20,2.8,16,11,.4,14,'#55524e',{top:'#555b5c'});box(25.3,1.45,16,.35,2.9,13.8,'#4b5352',{solid:true});box(14.7,1.45,16,.32,2.9,13.8,'#525452',{solid:true});box(20,1.45,23,10.7,2.9,.3,'#53504b',{solid:true});box(16,1.45,9.2,2.8,2.9,.2,'#414d4c',{solid:true});box(23.5,1.45,9.2,3.4,2.9,.2,'#414d4c',{solid:true});box(17.9,1.45,9.17,1.1,2.8,.045,'#a2c5c3');box(21.5,1.45,9.17,2.1,2.4,.04,'#869f9b');box(21.5,1.45,9.13,.06,2.8,.04,'#2c3b3c');
box(22,1.05,18,1.5,1,7.2,'#a38970',{solid:true});box(22,1.57,18,1.7,.13,7.5,'#d1b9a0');for(let z=15;z<=21;z+=1.8){box(20.5,.52,z,.48,.22,.48,'#a53932');box(20.5,.3,z,.08,.5,.08,'#a5aaa3')}
for(let z of [12.5,20.5]){box(16.5,.45,z,2.15,.8,1,'#683b36');box(16.5,1,z,1.7,.12,1.2,'#b89c7e');box(15.55,.65,z, .42,.7,1.8,'#8f493d');box(17.45,.65,z,.42,.7,1.8,'#8f493d')}
box(22.2,1.86,16.3,.36,.48,.35,'#323d3d');box(22.2,2.14,16.3,.27,.09,.27,'#9ca39d');box(23.2,1.88,13.9,1.15,.48,.7,'#a6b6ad');sign('FRESH PIE',23.2,1.93,13.51,.13,'#e2ab8f');box(16.7,1.1,20.4,.25,.08,.25,'#ece5d0');box(16.65,1.21,20.4,.08,.15,.08,'#ddd8c2');light(19,2.55,15,'#e4c6a0',8);sign('24 HOUR DINER',19.7,3.05,9.13,.38,'#f6aa86');
// Pool: sunken floor, coping, steps and ladder.
box(12,-1.1,-12,13,.11,11,'#33575b',{top:'#294e55'});for(let x of [5.3,18.7])box(x,-.28,-12,.45,.65,12,'#6e8382');for(let z of [-18,-6])box(12,-.28,z,14,.65,.45,'#798986');for(let i=0;i<4;i++)box(7.1+i*.5,-.84+i*.22,-7.5,.5,.25,2.25,'#678385');box(17.5,.1,-9,.07,1.1,.07,'#aab3ad');box(18,.1,-9,.07,1.1,.07,'#aab3ad');box(17.75,.58,-9,.55,.07,.07,'#aab3ad');sign('POOL CLOSED',12,1.35,-6.3,.28,'#dcb8a5');
for(let i=0;i<29;i++){let x=rand(6.5,17.5),z=rand(-17,-7);box(x,-1.02,z,rand(.1,.3),.012,.06,i%3?'#706c46':'#534d37')}
box(14,-.93,-13.5,1.4,.06,.64,'#ded5b6',{rotation:.7});box(14.6,-.55,-13.6,.07,.75,.06,'#b1a996');box(13.4,-.65,-13.6,.07,.55,.06,'#b1a996');box(10,-1.02,-15,.36,.06,.34,'#4a4239');
// Station wagon and open driver door, headlights.
box(-8,.64,-5,2.8,.65,5,'#697b76',{solid:true,top:'#849188'});box(-8,1.25,-5.5,2.45,.7,2.8,'#576966',{top:'#748987'});box(-8,1.2,-6.93,2.2,.6,.025,'#263b43');box(-8,1.15,-4.1,2.2,.5,.026,'#354a4e');for(let x of [-9.4,-6.6])for(let z of [-6.6,-3.3]){box(x,.34,z,.19,.56,.9,'#141819');box(x,.37,z,.2,.22,.37,'#88908b')}
box(-5.95,.76,-5.2,.13,1.35,1.75,'#788b84',{rotation:.65});sign('SHOTGUN',-5.7,1.5,-5.3,.16,'#f0cf90');box(-8.95,.67,-7.54,.58,.25,.08,'#efe5b6',{headlight:true});box(-7.05,.67,-7.54,.58,.25,.08,'#efe5b6',{headlight:true});light(-8,.6,-8,'#eacfa2',10);
// Ice & vending machines, overhang on motel walkway.
box(9.5,1,15.55,1.5,2,1.1,'#b7c8c3',{solid:true});sign('ICE',9.5,1.75,14.94,.32,'#294c61');box(12.2,1.15,15.55,1.6,2.3,1.1,'#a33435',{solid:true});box(12.2,1.32,14.96,1.12,1.55,.05,'#8b282c',{vending:true});sign('COLD DRINKS',12.2,2.09,14.87,.17,'#ffe3b2');light(12.2,1.7,14.7,'#e8515b',4);
// Lampposts, cast-off possessions, lot details.
for(let [x,z] of [[-18,-12],[4,7],[22,-21]]){box(x,2.6,z,.15,5.2,.15,'#555c5b');box(x,5.16,z,.8,.18,.8,'#bba57c');light(x,4.9,z,'#e9a258',10)}
box(-18,.6,2,.95,1.15,.7,'#756455');box(-18,.31,2.6,1,.15,.3,'#413d36');box(-13,.35,15.1,1.2,.7,.8,'#92958c');box(-13,.81,15.1,1.3,.1,.85,'#b8b9ac');box(-3,.12,8,.8,.1,.55,'#b3a794');box(2,.23,-14,.65,.42,.08,'#d6b66e');box(1.7,.23,-14.2,.65,.42,.08,'#d6b66e');box(3,.04,6,.45,.04,.32,'#aca297');sign('NO VACANCY',-2,3.55,15.2,.3,'#e87562');
for(let i=0;i<48;i++){let x=rand(-33,33),z=rand(-33,33);if(Math.abs(x)<26&&Math.abs(z)<24)continue;box(x,rand(2,4),z,rand(1,2.5),rand(4,8),rand(1,2),'#172523')}
}
// Perspective software renderer: textured-looking solid geometry rendered directly, with billboards for creatures.
function project(x,y,z){let dx=x-P.x,dz=z-P.z,cy=Math.cos(P.yaw),sy=Math.sin(P.yaw),depth=dx*sy+dz*cy,right=dx*cy-dz*sy;let yp=(y-P.y)*Math.cos(P.pitch)+depth*Math.sin(P.pitch);depth=depth*Math.cos(P.pitch)-(y-P.y)*Math.sin(P.pitch);if(depth<.14)return null;let f=Math.min(W*1.0,H*1.3);return {x:W/2+right*f/depth,y:H/2-yp*f/depth,d:depth,s:f/depth}}
function shade(hex,mul){let c=hex.replace('#','');if(c.length!==6)return hex;let r=clamp(parseInt(c.slice(0,2),16)*mul,0,255)|0,g=clamp(parseInt(c.slice(2,4),16)*mul,0,255)|0,b=clamp(parseInt(c.slice(4,6),16)*mul,0,255)|0;return `rgb(${r},${g},${b})`}
function illumination(x,z){let b=.48+sky*.3;for(let l of lights){let r=dist(x,z,l.x,l.z)/l.r;if(r<1)b+=.55*(1-r)*(1-r)}return clamp(b,.27,1.35)}
function poly(points,color,alpha=1){if(points.some(p=>!p))return;ctx.globalAlpha=alpha;ctx.fillStyle=color;ctx.beginPath();ctx.moveTo(points[0].x,points[0].y);for(let i=1;i<points.length;i++)ctx.lineTo(points[i].x,points[i].y);ctx.closePath();ctx.fill();ctx.globalAlpha=1}
function drawBox(o){let {x,y,z,w,h,d}=o;let rot=o.rotation||0,c=Math.cos(rot),s=Math.sin(rot),v=[];for(let yy of [-h/2,h/2])for(let zz of [-d/2,d/2])for(let xx of [-w/2,w/2])v.push(project(x+xx*c-zz*s,y+yy,z+xx*s+zz*c));let mul=illumination(x,z),fog=clamp((dist(x,z,P.x,P.z)-13)/47,0,.85),color=o.color;function face(idx,brightness,col){let p=idx.map(i=>v[i]);if(p.some(q=>!q))return;poly(p,shade(col||color,mul*brightness),1-fog*.72);if(fog>.05)poly(p,`rgb(${Math.round(24+sky*36)},${Math.round(39+sky*44)},${Math.round(48+sky*50)})`,fog*.53)}let dx=P.x-x,dz=P.z-z;face([0,1,3,2],.77);if(dx>0)face([0,2,6,4],.77);else face([1,3,7,5],.58);if(dz>0)face([2,3,7,6],.84);else face([0,1,5,4],.65);face([4,5,7,6],1.12,o.top||color);
if(o.headlight||o.vending||o.tv){let q=project(x,y,z),r=project(x,y+h*.4,z);if(q){let radius=clamp(q.s*(o.vending?1.6:1),4,210);let grad=ctx.createRadialGradient(q.x,q.y,0,q.x,q.y,radius);let rgb=o.vending?'239,66,69':o.tv?'180,215,216':'255,224,166';grad.addColorStop(0,`rgba(${rgb},.34)`);grad.addColorStop(1,`rgba(${rgb},0)`);ctx.fillStyle=grad;ctx.fillRect(q.x-radius,q.y-radius,radius*2,radius*2)}}}
function drawSign(o){let p=project(o.x,o.y,o.z);if(!p)return;let sz=clamp(p.s*o.size,2,150);ctx.save();ctx.textAlign='center';ctx.font=`bold ${sz}px Arial`;ctx.shadowColor=o.color;ctx.shadowBlur=o.text.includes('STARLITE')?22:8;ctx.fillStyle=o.color;ctx.globalAlpha=clamp(1-(p.d-19)/63,.1,1);ctx.fillText(o.text,p.x,p.y);ctx.restore()}
function line3(a,b,color,width=1){let p=project(...a),q=project(...b);if(!p||!q)return;ctx.strokeStyle=color;ctx.lineWidth=width;ctx.beginPath();ctx.moveTo(p.x,p.y);ctx.lineTo(q.x,q.y);ctx.stroke()}
function drawZombie(z){let pos=project(z.x,z.y,z.z);if(!pos)return;let scale=pos.s*(z.type==='brute'?1.32:z.type==='runner'?.86:1),opacity=clamp((56-pos.d)/27,.06,1);if(opacity<=0)return;ctx.save();ctx.translate(pos.x,pos.y);ctx.scale(scale,scale);ctx.globalAlpha=opacity;let fall=z.dead?Math.min(1,z.deadTime*2):0;ctx.rotate((z.dead?1.28*fall:0)+Math.sin(time*3+z.phase)*.035);let step=Math.sin(time*(z.type==='runner'?15:7)+z.phase)*(.18+z.speed*.02)*(z.dead?0:1);let hit=z.flinch>0;let coat=z.type==='brute'?'#333a35':z.type==='runner'?'#6e4442':'#505d59',skin=z.type==='brute'?'#728875':'#87978b';ctx.fillStyle='rgba(0,0,0,.32)';ctx.beginPath();ctx.ellipse(0,0,.42,.13,0,0,TAU);ctx.fill();
ctx.fillStyle='#222b2b';ctx.save();ctx.rotate(step*.4);ctx.fillRect(-.29,-.73,.22,.76);ctx.restore();ctx.save();ctx.rotate(-step*.4);ctx.fillRect(.08,-.73,.22,.76);ctx.restore();ctx.fillStyle=hit?'#ad6660':coat;ctx.beginPath();ctx.moveTo(-.36,-1.7);ctx.lineTo(.36,-1.7);ctx.lineTo(.42,-.66);ctx.lineTo(-.37,-.66);ctx.fill();ctx.fillStyle=skin;ctx.fillRect(-.12,-1.85,.24,.24);ctx.fillStyle=hit?'#dc8b73':'#859286';ctx.beginPath();ctx.ellipse(0,-2.04,.235,.285,0,0,TAU);ctx.fill();ctx.fillStyle='#241e20';ctx.fillRect(-.17,-2.08,.09,.045);ctx.fillRect(.08,-2.08,.09,.045);ctx.fillStyle='#b45042';ctx.fillRect(-.085,-1.91,.16,.04);ctx.strokeStyle=skin;ctx.lineWidth=.15;ctx.beginPath();ctx.moveTo(-.33,-1.58);ctx.lineTo(-.46,-1.03+step*.2);ctx.moveTo(.33,-1.58);ctx.lineTo(.48,-1.06-step*.2);ctx.stroke();if(z.type==='brute'){ctx.fillStyle='#354039';ctx.fillRect(-.55,-1.64,.21,.57);ctx.fillRect(.34,-1.64,.21,.57)}ctx.restore();if(z.hp<z.max&&z.hp>0){ctx.fillStyle='#3e1313';ctx.fillRect(pos.x-scale*.3,pos.y-scale*2.52,scale*.6,3);ctx.fillStyle='#c9503e';ctx.fillRect(pos.x-scale*.3,pos.y-scale*2.52,scale*.6*z.hp/z.max,3)}}
function drawPickup(p){if(!p.active)return;let pos=project(p.x,.25+Math.sin(time*3+p.x)*.08,p.z);if(!pos)return;let sz=clamp(pos.s*.37,3,52);ctx.save();ctx.translate(pos.x,pos.y);ctx.fillStyle=p.kind==='med'?'#b5e0c1':p.kind==='shotgun'?'#f3bf77':p.kind==='rifle'?'#e1ca9b':'#d1b181';ctx.shadowBlur=18;ctx.shadowColor=ctx.fillStyle;ctx.fillRect(-sz/2,-sz/2,sz,sz);ctx.shadowBlur=0;ctx.fillStyle='#243b3b';ctx.font=`bold ${Math.max(7,sz*.65)}px Arial`;ctx.textAlign='center';ctx.textBaseline='middle';ctx.fillText(p.kind==='med'?'+':p.kind==='shotgun'?'Ⅱ':p.kind==='rifle'?'Ⅲ':'▤',0,0);ctx.restore()}
function drawWorld(){sky=clamp((wave-1)/10+(wave===10?.09:0),0,1);let grad=ctx.createLinearGradient(0,0,0,H);grad.addColorStop(0,`rgb(${Math.round(4+sky*83)},${Math.round(10+sky*99)},${Math.round(19+sky*112)})`);grad.addColorStop(.65,`rgb(${Math.round(12+sky*89)},${Math.round(27+sky*88)},${Math.round(37+sky*97)})`);grad.addColorStop(1,'#1b2426');ctx.fillStyle=grad;ctx.fillRect(0,0,W,H);
let horizon=H/2+Math.tan(P.pitch)*H*.9;ctx.fillStyle=`rgba(15,30,35,${.4-sky*.18})`;ctx.fillRect(0,horizon,W,H-horizon);ctx.save();ctx.globalAlpha=1-sky*.9;for(let i=0;i<85;i++){let sx=((i*7919+Math.floor(P.yaw*120)*2)%10000+10000)%10000/10000*W,sy=((i*2371+137)%10000)/10000*Math.max(0,horizon*.68);ctx.fillStyle=i%8===0?'#cfccbe':'#718594';ctx.fillRect(sx,sy,i%9===0?2:1,i%9===0?2:1)}ctx.restore();
let all=objects.map(o=>({o,d:dist(o.x,o.z,P.x,P.z)}));all.push(...zombies.map(o=>({o,d:dist(o.x,o.z,P.x,P.z),zombie:true})));all.push(...pickups.map(o=>({o,d:dist(o.x,o.z,P.x,P.z),pickup:true})));all.sort((a,b)=>b.d-a.d);for(let item of all){if(item.d>66)continue;if(item.zombie)drawZombie(item.o);else if(item.pickup)drawPickup(item.o);else if(item.o.type==='box')drawBox(item.o);else drawSign(item.o)}
// Neon pole and flashing letters on the motel roof.
let lit=Math.sin(time*13)>-.35;line3([1,3.1,18],[1,6.2,18],'#767e79',5);let np=project(1,6.55,18);if(np){ctx.save();ctx.font=`bold ${clamp(np.s*.8,9,95)}px Arial`;ctx.textAlign='left';let parts=['STAR','L','ITE'],total=ctx.measureText('STARLITE').width,px=np.x-total/2;ctx.shadowBlur=24;ctx.shadowColor='#fd4970';for(let i=0;i<3;i++){ctx.fillStyle=i===1&&!lit?'#44323a':'#ff7790';ctx.fillText(parts[i],px,np.y);px+=ctx.measureText(parts[i]).width}ctx.restore()}
let fog=ctx.createLinearGradient(0,H*.26,0,H);fog.addColorStop(0,`rgba(87,111,124,${.045+sky*.028})`);fog.addColorStop(1,`rgba(68,90,99,${.12+sky*.025})`);ctx.fillStyle=fog;ctx.fillRect(0,H*.26,W,H*.74);
}
function resetPickups(){pickups=[{x:-5.2,z:-5.2,kind:'shotgun',active:!P.owned[1]},{x:-19.8,z:15,kind:'rifle',active:!P.owned[2]},{x:-13,z:8,kind:'ammo',active:true},{x:3,z:5,kind:'ammo',active:true},{x:17,z:13,kind:'ammo',active:true},{x:-21,z:-14,kind:'med',active:true},{x:9,z:-14,kind:'med',active:true},{x:2,z:15,kind:'med',active:true}]}
function collides(x,z,r=.28,enemy=false){if(x<-25+r||x>25-r||z<-22+r||z>23-r)return true;for(let c of colliders)if(x>c.x-r&&x<c.x+c.w+r&&z>c.z-r&&z<c.z+c.d+r)return true;if(enemy&&x>5.2&&x<18.8&&z>-18&&z<-6)return true;return false}
function moveEntity(o,dx,dz,r=.3,enemy=false){if(!collides(o.x+dx,o.z,r,enemy))o.x+=dx;if(!collides(o.x,o.z+dz,r,enemy))o.z+=dz}
const spawnLocations=[[-24,-17],[-24,1],[-23,6],[-2,-21],[23,-18],[23,4],[18,6],[-12,-21],[4,10]];
function spawnZombie(){let spots=spawnLocations.filter(s=>dist(s[0],s[1],P.x,P.z)>16);let s=spots[Math.floor(Math.random()*spots.length)]||spawnLocations[0];let type=wave>=6&&Math.random()<.12+(wave-6)*.025?'brute':wave>=3&&Math.random()<.19+(wave-3)*.03?'runner':'shambler';let max=type==='brute'?270+wave*15:type==='runner'?48+wave*3:85+wave*8;zombies.push({x:s[0]+rand(-1.4,1.4),z:s[1]+rand(-1.4,1.4),y:0,type,hp:max,max,speed:type==='runner'?2.5+wave*.12:type==='brute'?.65+wave*.045:1.02+wave*.075,phase:rand(0,TAU),flinch:0,attack:0,dead:false,deadTime:0,groan:rand(2,8),stuck:0,angle:0})}
function startWave(n){wave=n;remaining=5+wave*3+(wave>5?(wave-5)*2:0);spawned=0;spawnTimer=1.1;breakTime=0;phoneTime=0;banner('WAVE '+String(wave).padStart(2,'0')+' — THEY ARE COMING',3);sound('wave');updateHud()}
function banner(text,seconds=2){$('banner').textContent=text;$('banner').style.opacity='1';bannerTime=seconds}
function tip(text,seconds=2){$('tip').textContent=text;$('tip').style.opacity='1';tipTime=seconds}
function begin(){state='playing';$('hud').style.display='block';$('overlay').style.display='none';document.body.requestPointerLock?.();if(!audio)setupAudio();if(wave===0){world();resetPickups();startWave(1)}last=performance.now()}
function finish(win){state=win?'win':'dead';document.exitPointerLock?.();$('overlay').style.display='flex';$('eyebrow').textContent=win?'06:02 A.M. · THE SUN IS RISING':'SIGNAL LOST · NO SURVIVORS';$('panel').querySelector('h1').innerHTML=win?'DAWN<span>AT THE STARLITE</span>':'LAST CALL<span>AT THE STARLITE</span>';$('desc').innerHTML=win?`You made it through all ten waves.<br>${kills} infected put to rest. Morning finally came.`:`You made it to wave ${wave}.<br>${kills} infected put to rest before the darkness took you.`;$('start').textContent='TRY AGAIN';$('controls').style.display='none';sound(win?'win':'death')}
function restart(){wave=0;kills=0;time=0;zombies=[];corpses=[];particles=[];Object.assign(P,{x:0,z:-2,y:1.65,yaw:0,pitch:0,hp:100,walk:0,weapon:0,ammo:[12,6,24],reserve:[Infinity,30,90],owned:[true,false,false],reload:0,fire:0,recoil:0,step:0});$('controls').style.display='block';begin()}
$('start').onclick=()=>{if(state==='dead'||state==='win')restart();else if(state==='paused'){state='playing';$('overlay').style.display='none';document.body.requestPointerLock?.()}else begin()};
document.addEventListener('pointerlockchange',()=>{if(!document.pointerLockElement&&state==='playing'){state='paused';$('overlay').style.display='flex';$('eyebrow').textContent='PAUSED · THE NIGHT WAITS';$('panel').querySelector('h1').innerHTML='LAST CALL<span>AT THE STARLITE</span>';$('desc').textContent='Take a breath. They will still be here.';$('start').textContent='RESUME';$('controls').style.display='block'}});
document.addEventListener('mousemove',e=>{if(state!=='playing'||!document.pointerLockElement)return;P.yaw+=e.movementX*.0023;P.pitch=clamp(P.pitch-e.movementY*.0023,-1.27,1.27)});
document.addEventListener('keydown',e=>{keys[e.code]=true;if(['Space','ArrowUp','ArrowDown','ArrowLeft','ArrowRight'].includes(e.code))e.preventDefault();if(state!=='playing')return;if(e.code==='KeyR')reload();if(['Digit1','Digit2','Digit3'].includes(e.code))switchGun(Number(e.code.slice(-1))-1);if(e.code==='Escape'){state='paused';document.exitPointerLock?.()}});document.addEventListener('keyup',e=>keys[e.code]=false);document.addEventListener('mousedown',e=>{if(e.button===0){mouseDown=true;if(state==='playing'&&document.pointerLockElement)fire()}});document.addEventListener('mouseup',e=>{if(e.button===0)mouseDown=false});document.addEventListener('wheel',e=>{if(state!=='playing')return;let next=P.weapon;for(let i=0;i<3;i++){next=(next+(e.deltaY>0?1:2))%3;if(P.owned[next]){switchGun(next);break}}e.preventDefault()},{passive:false});
function switchGun(n){if(!P.owned[n]){tip(n===1?'SHOTGUN IN THE STATION WAGON':'RIFLE BEHIND THE OFFICE COUNTER');return}if(P.weapon===n)return;P.weapon=n;P.reload=0;P.fire=.18;P.recoil=.5;sound('switch');updateHud()}
function reload(){let g=guns[P.weapon],a=P.weapon;if(P.reload>0||P.ammo[a]>=g.mag||P.reserve[a]<=0)return;P.reload=g.reload;sound('reload');tip('RELOADING...',.9)}
function fire(){let a=P.weapon,g=guns[a];if(P.reload>0||P.fire>0)return;if(P.ammo[a]<=0){P.fire=.22;sound('empty');tip('R TO RELOAD',1);return}P.fire=g.delay;P.ammo[a]--;P.recoil=a===1?1.8:a===2?.65:1;P.pitch=clamp(P.pitch+(a===1?.025:a===2?.006:.014),-1.27,1.27);fireFlash=.13;shake=a===1?7:2.8;sound(['pistol','shotgun','rifle'][a]);let hits=new Set();for(let i=0;i<g.pellets;i++){let sx=W/2+rand(-1,1)*g.spread*W*.5,sy=H/2+rand(-1,1)*g.spread*H*.5;let targets=zombies.filter(z=>!z.dead).map(z=>{let feet=project(z.x,z.y,z.z),head=project(z.x,z.y+1.85*(z.type==='brute'?1.3:z.type==='runner'?.86:1),z.z);return {z,feet,head}}).filter(t=>t.feet&&t.head).sort((a,b)=>a.feet.d-b.feet.d);for(let t of targets){let z=t.z,sc=t.feet.s*(z.type==='brute'?1.3:z.type==='runner'?.86:1),hx=t.head.x,hy=t.head.y,fx=t.feet.x,fy=t.feet.y;let headshot=Math.hypot((sx-hx)/(.25*sc),(sy-hy)/(.3*sc))<1;let body=(sx>fx-.46*sc&&sx<fx+.46*sc&&sy>hy-.12*sc&&sy<fy+.1*sc);if(headshot||body){let damage=g.damage*(headshot?2.5:1)*(a===1&&hits.has(z)?.25:1);hits.add(z);z.hp-=damage;z.flinch=.23;z.x+=Math.sin(P.yaw)*(a===1?.33:.12);z.z+=Math.cos(P.yaw)*(a===1?.33:.12);hitTime=.14;sound(headshot?'head':'hit');particles.push({x:z.x,y:z.y+(headshot?1.9:1.2),z:z.z,life:.5,head:headshot});if(headshot)tip('✕ HEADSHOT',.65);if(z.hp<=0)kill(z,headshot);break}}}
if(P.ammo[a]===0&&P.reserve[a]>0)setTimeout(()=>{if(state==='playing'&&P.ammo[a]===0)reload()},280);updateHud()}
function kill(z,head){z.dead=true;z.deadTime=0;kills++;remaining=Math.max(0,remaining-1);if(head)shake+=1;corpses.push(z);if(corpses.length>35){let old=corpses.shift();zombies.splice(zombies.indexOf(old),1)}updateHud()}
function hurtPlayer(damage){P.hp=Math.max(0,P.hp-damage);hurt=.9;shake=8;sound('hurt');updateHud();if(P.hp<=0)finish(false)}
function updateHud(){$('wave').textContent=String(wave).padStart(2,'0');$('remaining').textContent=breakTime>0?`NEXT WAVE IN ${Math.ceil(breakTime)}s`:remaining+' HOSTILES REMAIN';$('score').textContent='KILLS '+String(kills).padStart(3,'0');$('hpText').textContent=Math.ceil(P.hp);$('healthFill').style.width=P.hp+'%';$('weaponName').textContent=guns[P.weapon].name;$('ammo').innerHTML=P.ammo[P.weapon]+` <small>/ ${P.weapon===0?'∞':P.reserve[P.weapon]}</small>`;let minute=10+Math.round((wave-1)/10*170);$('clock').textContent=`${String(3+Math.floor(minute/60)).padStart(2,'0')}:${String(minute%60).padStart(2,'0')} AM · UNTIL DAWN`}
let audio=null,noiseBuffer=null,ambient=[];function setupAudio(){try{audio=new (window.AudioContext||window.webkitAudioContext)();noiseBuffer=audio.createBuffer(1,audio.sampleRate*2,audio.sampleRate);let d=noiseBuffer.getChannelData(0);for(let i=0;i<d.length;i++)d[i]=Math.random()*2-1;for(let [freq,vol] of [[62,.012],[118,.007]]){let o=audio.createOscillator(),gain=audio.createGain();o.type='sawtooth';o.frequency.value=freq;gain.gain.value=vol;o.connect(gain).connect(audio.destination);o.start();ambient.push(o)}}catch(e){}}
function tone(freq,dur,vol=.08,type='sine',slide=0){if(!audio)return;let t=audio.currentTime,o=audio.createOscillator(),g=audio.createGain();o.type=type;o.frequency.setValueAtTime(Math.max(15,freq),t);if(slide)o.frequency.exponentialRampToValueAtTime(Math.max(15,freq+slide),t+dur);g.gain.setValueAtTime(vol,t);g.gain.exponentialRampToValueAtTime(.0001,t+dur);o.connect(g).connect(audio.destination);o.start(t);o.stop(t+dur+.02)}
function noise(dur,vol,low=350,high=1900){if(!audio)return;let t=audio.currentTime,source=audio.createBufferSource(),filter=audio.createBiquadFilter(),gain=audio.createGain();source.buffer=noiseBuffer;filter.type='bandpass';filter.frequency.value=(low+high)/2;filter.Q.value=.35;gain.gain.setValueAtTime(vol,t);gain.gain.exponentialRampToValueAtTime(.001,t+dur);source.connect(filter).connect(gain).connect(audio.destination);source.start(t);source.stop(t+dur+.01)}
function sound(name,vol=1){if(!audio)return;switch(name){case'pistol':noise(.18,.22*vol,120,1700);tone(110,.14,.15*vol,'sawtooth',-85);break;case'shotgun':noise(.4,.35*vol,60,1100);tone(70,.3,.26*vol,'sawtooth',-47);break;case'rifle':noise(.11,.14*vol,350,2400);tone(155,.09,.1*vol,'square',-115);break;case'reload':tone(810,.06,.065,'square',-330);setTimeout(()=>tone(580,.09,.07,'square',-360),400);break;case'empty':tone(490,.045,.07,'square',-290);break;case'switch':tone(250,.1,.035,'triangle',-120);break;case'hit':noise(.09,.09,100,780);tone(130,.08,.05,'triangle',-70);break;case'head':noise(.19,.19,100,900);tone(290,.15,.095,'sawtooth',-250);break;case'hurt':noise(.25,.16,80,650);tone(95,.35,.14,'sawtooth',-50);break;case'groan':tone(rand(75,120),.5,.025*vol,'sawtooth',-35);break;case'snarl':noise(.24,.055*vol,230,900);tone(215,.18,.04*vol,'sawtooth',-110);break;case'step':tone(55,.13,.08*vol,'sine',-22);break;case'phone':tone(460,.19,.046,'sine');setTimeout(()=>tone(590,.22,.046,'sine'),200);break;case'wave':tone(220,.55,.11,'sawtooth',-125);setTimeout(()=>tone(145,.66,.1,'sawtooth',-100),220);break;case'death':tone(165,1,.13,'sawtooth',-120);break;case'win':tone(350,.5,.09);setTimeout(()=>tone(520,.7,.09),300);break}}
function update(dt){time+=dt;if(state!=='playing')return;P.fire=Math.max(0,P.fire-dt);P.recoil=Math.max(0,P.recoil-dt*5);shake=Math.max(0,shake-dt*20);hurt=Math.max(0,hurt-dt*1.3);fireFlash=Math.max(0,fireFlash-dt);hitTime=Math.max(0,hitTime-dt);if(P.reload>0){P.reload-=dt;if(P.reload<=0){let a=P.weapon,need=guns[a].mag-P.ammo[a],give=Math.min(need,P.reserve[a]);P.ammo[a]+=give;if(Number.isFinite(P.reserve[a]))P.reserve[a]-=give;sound('switch');updateHud()}}
if(mouseDown&&P.weapon===2)fire();let dx=(keys.KeyD?1:0)-(keys.KeyA?1:0),dz=(keys.KeyW?1:0)-(keys.KeyS?1:0),moving=dx||dz;if(moving){let len=Math.hypot(dx,dz),s=(keys.ShiftLeft||keys.ShiftRight?6.2:4.1)*dt;dx/=len;dz/=len;moveEntity(P,(dx*Math.cos(P.yaw)+dz*Math.sin(P.yaw))*s,(dz*Math.cos(P.yaw)-dx*Math.sin(P.yaw))*s,.32);P.walk+=dt*(keys.ShiftLeft?15:10);P.step+=dt;if(P.step>.49){P.step=0;sound('step',.3)}}P.y=1.65+(P.x>5.7&&P.x<18.3&&P.z>-17.5&&P.z<-6.5?-.98:0);
for(let p of pickups)if(p.active&&dist(P.x,P.z,p.x,p.z)<1.18){p.active=false;if(p.kind==='med'){P.hp=Math.min(100,P.hp+35);tip('+35 HEALTH')}else if(p.kind==='ammo'){P.reserve[1]+=12;P.reserve[2]+=35;tip('AMMUNITION RESTOCKED')}else if(p.kind==='shotgun'){P.owned[1]=true;P.weapon=1;tip('12-GAUGE SHOTGUN ACQUIRED',3);sound('reload')}else if(p.kind==='rifle'){P.owned[2]=true;P.weapon=2;tip('SERVICE RIFLE ACQUIRED',3);sound('reload')}updateHud()}
if(bannerTime>0){bannerTime-=dt;if(bannerTime<=0)$('banner').style.opacity='0'}if(tipTime>0){tipTime-=dt;if(tipTime<=0)$('tip').style.opacity='0'}$('danger').style.opacity=String(hurt*.46+Math.max(0,(40-P.hp)/200));$('flash').style.opacity=String(fireFlash*.6);$('crosshair').classList.toggle('hit',hitTime>0);
if(breakTime>0){breakTime-=dt;phoneTime-=dt;if(phoneTime<=0){sound('phone');phoneTime=2.7}if(breakTime<=0){if(wave===10)finish(true);else startWave(wave+1)}updateHud()}else{if(spawned<5+wave*3+(wave>5?(wave-5)*2:0)){spawnTimer-=dt;if(spawnTimer<=0){spawnZombie();spawned++;spawnTimer=clamp(2.15-wave*.13,.55,2.2)*rand(.7,1.3)}}if(remaining===0&&spawned>=5+wave*3+(wave>5?(wave-5)*2:0)){if(wave===10){finish(true);return}breakTime=17;resetPickups();banner('WAVE CLEARED · BREATHE',2.8);P.hp=Math.min(100,P.hp+12);updateHud()}}
for(let z of zombies){if(z.dead){z.deadTime+=dt;continue}z.flinch=Math.max(0,z.flinch-dt);z.attack-=dt;z.groan-=dt;let dis=dist(P.x,P.z,z.x,z.z),angle=Math.atan2(P.x-z.x,P.z-z.z),speed=z.speed*(z.flinch>0?.25:1);if(dis>1.08){let moved=false;for(let off of [0,.55,-.55,1.15,-1.15,2.1,-2.1]){let a=angle+off,tx=z.x+Math.sin(a)*speed*dt,tz=z.z+Math.cos(a)*speed*dt;if(!collides(tx,tz,z.type==='brute'?.46:.28,true)){z.x=tx;z.z=tz;moved=true;break}}if(!moved){z.x+=Math.sin(angle+Math.PI/2)*speed*dt*.5;z.z+=Math.cos(angle+Math.PI/2)*speed*dt*.5}}else if(z.attack<=0){hurtPlayer(z.type==='brute'?25:z.type==='runner'?9:12);z.attack=z.type==='runner'?.9:1.5}
if(z.groan<=0&&dis<19){sound(z.type==='runner'?'snarl':z.type==='brute'?'step':'groan',clamp((19-dis)/13,.1,1));z.groan=rand(z.type==='runner'?2:3,7)}if(z.type==='brute'&&dis<12&&Math.random()<dt*.7)sound('step',clamp((12-dis)/12,.1,.8))}
for(let p of particles)p.life-=dt;particles=particles.filter(p=>p.life>0);
}
function viewGun(){if(state!=='playing'&&state!=='paused')return;let a=P.weapon,bob=Math.sin(P.walk)*Math.min(.012,Math.abs(P.step)*.05),re=P.recoil,rl=P.reload>0?Math.sin((1-P.reload/guns[a].reload)*Math.PI):0,x=W*.53+Math.sin(P.walk*.5)*W*.003,y=H*(.83+bob+rl*.26)+re*H*.025;ctx.save();ctx.translate(x,y);ctx.rotate(-re*.1+rl*.33);let scale=Math.min(W/1100,H/740)*1.05;ctx.scale(scale,scale);ctx.fillStyle='#151e22';ctx.beginPath();ctx.moveTo(-85,140);ctx.lineTo(68,140);ctx.lineTo(32,23);ctx.lineTo(-35,23);ctx.fill();ctx.fillStyle='#202b2e';ctx.fillRect(-29,14,74,66);ctx.fillStyle=a===1?'#635546':a===2?'#3d4141':'#495051';ctx.fillRect(-34,-5,a===1?185:a===2?225:105,a===1?38:a===2?25:34);ctx.fillStyle='#101a1e';ctx.fillRect(a===1?145:a===2?185:70,-4,a===1?60:a===2?80:55,a===1?24:17);ctx.fillStyle='#899390';ctx.fillRect(-33,-6,a===1?158:a===2?210:97,5);ctx.fillStyle='#0c1114';ctx.fillRect(-22,29,45,12);ctx.fillStyle='#343a3a';ctx.fillRect(-10,-19,30,17);ctx.fillStyle='#1a2326';ctx.fillRect(-5,-23,6,9);ctx.fillRect(a===1?174:a===2?223:105,-21,7,18);ctx.fillStyle='#33302c';ctx.beginPath();ctx.moveTo(10,85);ctx.lineTo(64,91);ctx.lineTo(97,150);ctx.lineTo(-43,150);ctx.fill();if(fireFlash>0){ctx.globalAlpha=fireFlash/.13;ctx.fillStyle='#fff1b7';ctx.shadowColor='#ff8c32';ctx.shadowBlur=37;let mx=a===1?205:a===2?264:125;ctx.beginPath();ctx.moveTo(mx,-1);ctx.lineTo(mx+30,-25);ctx.lineTo(mx+73,7);ctx.lineTo(mx+29,25);ctx.fill()}ctx.restore()}
function drawEffects(){fctx.clearRect(0,0,W,H);if(shake>0){/* feedback uses slight screen perturbation on the weapon and red vignette */}for(let p of particles){let pr=project(p.x,p.y,p.z);if(!pr)continue;fctx.fillStyle=p.head?'#ed7a50':'#9d3735';for(let i=0;i<5;i++){let t=(.5-p.life),ex=Math.sin(i*2.4+p.x)*t*pr.s*.7,ey=Math.cos(i*3+p.z)*t*pr.s*.6;fctx.fillRect(pr.x+ex,pr.y+ey,Math.max(2,pr.s*.025),Math.max(2,pr.s*.025))}}}
function loop(now){let dt=Math.min(.05,(now-last)/1000||.016);last=now;update(dt);drawWorld();viewGun();drawEffects();requestAnimationFrame(loop)}world();sign('STARLITE',1,6.6,18,.55,'#fa6282');requestAnimationFrame(loop);
</script></body></html>
<!-- agent-meta {"model":"openai/gpt-6-sol","provider":"openrouter","persona":"full","sessionId":"0df987a0-0c70-40e5-9755-3c0083d69449","tokensIn":411210,"tokensOut":22936,"tokensTotal":434146,"cost":0.37880869999999994,"turns":21,"toolCalls":20,"failedToolCalls":1,"timestamp":"2026-09-23T14:55:33.371Z"} -->