27:18Gemini 4 Hydrogen: Cheaper Tokens, More Expensive Answers?
Nov 13, 2026 · YouTube
18:40ERNIE 5.5 Flash Preview vs GLM 5.3 Flash: Baidu's new model is very impressive!
Nov 11, 2026 · YouTube
36:22Fable 5.2 robbing me blind, plus: Nex AGI's N2.6 vs GLM 5.3 Flash
Nov 11, 2026 · YouTube
14:03Gemini 4 Carbon: Can it handle my challenges? (First look with one-shot coding tests)
Nov 10, 2026 · YouTube
47:51Ranking major U.S. Open-Weights Models. (Where does Ripple 65B fit in?)
Nov 6, 2026 · YouTube
10:57"Crouton Alpha": the latest super-efficient coding model by Meta?
Nov 3, 2026 · YouTube
38:58Testing Particle-1: a post-trained DeepSeek V4.1 Flash specializing in coding and efficiency
Oct 28, 2026 · YouTube
22:14GPT-6.1 Terra Pro vs Terra: How Much Better Is Pro?
Oct 27, 2026 · YouTube
29:55Kimi K3.1 Pro Looked Incredible. Then I Kept Testing.
Oct 26, 2026 · YouTube
31:28Is this company reselling GPT Sol 6.1 at a loss (for non-agentic/text-only)?🤔 Is this sustainable?
Oct 20, 2026 · YouTube
17:58Nemotron 3.5 Super VL vs Opus 5.5: Testing NVIDIA’s new coding juggernaut
Oct 15, 2026 · YouTube
16:04Testing a *massive* new open-weights model 🤯: Muse Spark Open (vs. Nemotron, Opus 5.5, and more)
Oct 15, 2026 · YouTube
16:43Gemini 4 Argon: Great 🐈 Animation, but a broken escalator. (Coding challenge vs Opus 5.5, Sol 6.1)
Oct 13, 2026 · YouTube
16:37Meta's new "Muse Wonder" AI is impressive... but can it compete with Anthropic's models?
Oct 12, 2026 · YouTube
11:12WOW... Haiku 5.5 is very impressive. RIP OpenAI?
Oct 7, 2026 · YouTube
22:47Mistral Large 4 Is Cheap. But Is It Good Enough to Compete?
Oct 6, 2026 · YouTube
22:02Is Pareto 26.10 Preview Just Routing to GPT Sol? I Tested It Against Sol 6.1 Pro
Oct 1, 2026 · YouTube
11:22I Tested Sonnet 5.5 Against Opus 5.5. Is It Actually Cheaper?
Sep 28, 2026 · YouTube
You write simulations where the interesting behavior is supposed to come out of the dice rather than be animated by hand.
Build me a single HTML page: a Starbucks-style coffee shop running a bizarre promotion, simulated live.
data: URIs are fine.localStorage, sessionStorage, cookies or indexedDB — sandboxed frame, they throw.html block.One paragraph, before the file. Given your own timings — how long a customer stays inside holding a drink, how fast the counter serves — roughly how many customers should pass through between catastrophes? Show the working, not just the number.
The single fenced html block.
Having written it, what does the page do in its first few minutes? Name anything you tuned, special-cased or nudged. Say what it does badly — I am going to leave it running and watch the counters.
Each drink is rolled at the counter with a single d6, and a 1 makes a unicorn, so p = 1/6. The counter takes 2–4 s per customer (mean 3 s) plus about 0.25 s for the next person to step up. With arrivals every 2.6 s on average, the counter is always the bottleneck, so it hands out a drink every ≈3.25 s, or about 0.31 per second. A customer holds the drink for 8–14 s from hand-off (mean 11 s), then walks from their seat to the door, about 2 s more before they stop counting as "inside". That makes ≈13 s in the room per drink. By Little's law, about 0.31 × 13 ≈ 4 drinks are held at any moment. A catastrophe happens when a new unicorn is rolled while exactly two unicorns are already held; if three were already held, the event would have fired earlier. Treating the ≈4 drinks in hand as independent 1/6 trials, P(exactly 2 of 4) = 6·(1/6)²·(5/6)² ≈ 0.116. The hazard per drink is therefore (1/6)·0.116 ≈ 0.0193, so the mean wait is ≈ 1/0.0193 ≈ 52 customers. An emptied shop after each reset has fewer drinks in hand for its first ~13 s, which adds a couple more. So I expect about 50–55 customers between catastrophes, roughly 2¾ minutes of simulated time (≈50 × 3.25 s) plus ≈19 s for the event itself. The spread is roughly geometric: median ≈ 36, about 1 in 4 events within 15 customers, and about 1 in 8 gaps longer than 100. Ten events will still average anywhere from about 35 to about 70.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Moonbeans Coffee — The Neon Unicorn Promotion</title>
<style>
*{box-sizing:border-box}
html,body{margin:0;height:100%;background:#07040f;color:#e9e4ff;font:13px/1.35 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;overflow:hidden}
#app{display:flex;height:100%}
#stage{position:relative;flex:1;min-width:0;background:#000}
#cv{position:absolute;left:0;top:0;width:100%;height:100%;display:block}
#panel{width:316px;flex:none;overflow-y:auto;padding:10px 12px 16px;background:linear-gradient(#150d2b,#0c0719);border-left:1px solid #3b2a70}
h1{margin:0 0 2px;font-size:17px;letter-spacing:.12em;font-weight:800}
.rb{background:linear-gradient(90deg,#ff4fd8,#ffd84f,#4fffb0,#4fb4ff,#c04fff);-webkit-background-clip:text;background-clip:text;color:transparent}
.sub{color:#a99fd4;font-size:12px;margin-bottom:8px}
.box{background:#1a1236;border:1px solid #33265f;border-radius:8px;padding:8px 10px;margin:8px 0}
.lbl{font-size:10px;text-transform:uppercase;letter-spacing:.12em;color:#8f84c4;margin-bottom:4px}
.big{font-size:30px;font-weight:800;line-height:1}
.big .of{font-size:12px;font-weight:500;color:#8f84c4}
.pips{display:flex;gap:6px;margin:6px 0 4px}
.pips i{width:22px;height:22px;border-radius:50%;background:#2a2050;border:1px solid #47387f}
.pips i.on{background:conic-gradient(#ff4fd8,#ffd84f,#4fffb0,#4fb4ff,#c04fff,#ff4fd8);box-shadow:0 0 12px #ff4fd8;border-color:#fff}
.kt{font-size:12px;color:#cfc6ff;min-height:1.3em}
#roomBox.k1{border-color:#8a63ff}
#roomBox.k2{border-color:#ffd84f;animation:sh .25s infinite}
#roomBox.k3{border-color:#ff3b6b;animation:sh .1s infinite;background:#3a0d2e}
@keyframes sh{0%{transform:translate(0,0)}25%{transform:translate(1.5px,-1px)}50%{transform:translate(-1px,1.5px)}75%{transform:translate(1px,1px)}100%{transform:translate(0,0)}}
table{width:100%;border-collapse:collapse}
td{padding:2px 0;border-bottom:1px solid #241a48;vertical-align:top}
td:first-child{color:#a99fd4;padding-right:6px}
td:last-child{text-align:right;font-variant-numeric:tabular-nums;font-weight:700;white-space:nowrap}
.dim{color:#8f84c4;font-size:11px}
.rateRow{font-size:22px;font-weight:800}
.bar{position:relative;height:10px;background:#2a2050;border-radius:5px;margin-top:6px;overflow:hidden}
#rFill{height:100%;width:0;background:linear-gradient(90deg,#6d5bd0,#ff4fd8)}
.bar .tick{position:absolute;top:-2px;bottom:-2px;width:2px;background:#fff}
.dice{display:flex;gap:4px;height:74px}
.dice .col{flex:1;display:flex;flex-direction:column;align-items:center;min-width:0}
.dice .n{font-size:10px;color:#a99fd4}
.dice .b{flex:1;width:100%;background:#241a48;border-radius:3px;position:relative;overflow:hidden}
.dice .b>div{position:absolute;left:0;right:0;bottom:0;height:0;background:#6d5bd0}
.dice .uni .b>div{background:linear-gradient(#ff4fd8,#ffd84f,#4fffb0,#4fb4ff)}
.dice .f{font-weight:800;font-size:11px}
#dlog{display:flex;flex-wrap:wrap;gap:2px}
#dlog i{width:8px;height:8px;border-radius:50%;background:#5a4a78}
#dlog i.u{background:conic-gradient(#ff4fd8,#ffd84f,#4fffb0,#4fb4ff,#c04fff,#ff4fd8);box-shadow:0 0 5px #ff4fd8}
#spd{display:flex;flex-wrap:wrap;gap:4px}
button{font:inherit;font-weight:700;background:#2a2050;color:#e9e4ff;border:1px solid #47387f;border-radius:6px;padding:4px 8px;cursor:pointer}
button:hover{background:#3a2c6e}
button.on{background:#7a4cff;border-color:#fff}
select{font:inherit;background:#2a2050;color:#e9e4ff;border:1px solid #47387f;border-radius:6px;padding:3px;width:100%;margin-top:6px}
ol{margin:0;padding-left:20px;color:#cfc6ff}
.foot{font-size:11px;color:#8f84c4;margin-top:8px}
@media (max-width:860px){#app{flex-direction:column}#panel{width:auto;height:46%;border-left:0;border-top:1px solid #3b2a70}#stage{flex:1 1 54%}}
</style>
</head>
<body>
<div id="app">
<div id="stage"><canvas id="cv"></canvas></div>
<aside id="panel">
<h1>MOONBEANS COFFEE</h1>
<div class="sub">Promotion: every Frappuccino has a <b>1-in-6</b> chance of being a <b class="rb">Neon Unicorn</b>. Three in the room at once is, officially, "unlikely".</div>
<div class="box" id="roomBox">
<div class="lbl">Neon Unicorns currently inside</div>
<div class="big"><span id="kN">0</span><span class="of"> 3 = disaster</span></div>
<div class="pips"><i></i><i></i><i></i></div>
<div class="kt" id="kTxt">All quiet.</div>
</div>
<div class="box"><table id="tbl"></table></div>
<div class="box">
<div class="lbl">Unicorn rate (observed vs true)</div>
<div class="rateRow" id="rObs">—</div>
<div class="dim" id="rSig">true 16.67%</div>
<div class="bar"><div id="rFill"></div><div class="tick" style="left:50%"></div></div>
<div class="dim" style="margin-top:2px">white tick = true 1/6 (bar spans 0–33%)</div>
</div>
<div class="box">
<div class="lbl">The dice: rolls per face (fair die = flat; face 1 = unicorn)</div>
<div class="dice" id="dice"></div>
</div>
<div class="box">
<div class="lbl">Last 60 drinks (rainbow = unicorn)</div>
<div id="dlog"></div>
</div>
<div class="box">
<div class="lbl">Simulation speed</div>
<div id="spd"></div>
<select id="slow">
<option value="0">Never auto-slow (skip cutscenes)</option>
<option value="1" selected>Slow to 1× during disasters</option>
<option value="2">Slow during disasters + at 2 unicorns</option>
</select>
</div>
<div class="box">
<div class="lbl">Disaster log</div>
<ol id="evlog"><li class="dim">None yet. Give it time.</li></ol>
</div>
<div class="foot">A drink is a unicorn iff a d6 rolled at the counter shows 1. Nothing else touches the dice. Space = pause.</div>
</aside>
</div>
<script>
(()=>{
'use strict';
/* ================= helpers ================= */
const TAU=Math.PI*2;
const clamp=(v,a,b)=>v<a?a:v>b?b:v;
const lerp=(a,b,t)=>a+(b-a)*t;
const rnd=(a,b)=>a+Math.random()*(b-a);
const smooth=t=>{t=clamp(t,0,1);return t*t*(3-2*t);};
const easeOutBack=t=>{t=clamp(t,0,1)-1;return t*t*(2.70158*t+1.70158)+1;};
const easeOutElastic=t=>{t=clamp(t,0,1);if(t===0||t===1)return t;return Math.pow(2,-9*t)*Math.sin((t*10-0.75)*(TAU/3))+1;};
function hsl(h,s,l,a){h=Math.round(((h%360)+360)%360);return a===undefined?`hsl(${h},${s}%,${l}%)`:`hsla(${h},${s}%,${l}%,${clamp(a,0,1).toFixed(3)})`;}
const hrand=n=>{const x=Math.sin(n*127.1+311.7)*43758.5453;return x-Math.floor(x);};
const $=id=>document.getElementById(id);
/* ================= constants ================= */
const CAP=8; // max customers inside
const ARRIVAL_MEAN=2.6, LINE_MAX=14; // arrivals (s), line length at which people give up
const DOOR_X=380, FLOOR=516, LINE_Y=506, LANE_Y=546, QUEUE_Y=500, ORDER_X=770, QUEUE_GAP=26;
const LINE_X0=DOOR_X-46, LINE_GAP=24;
const SEATS=[[430,440],[470,478],[520,446],[570,474],[620,442],[665,474],[715,442],[505,500]];
const DT=1/60;
const T_EXPL=1.8,T_HATCH=2.4,T_DANCE=4.2,T_LAUNCH=12.6,T_REB=14.2,REB_LEN=4.6,T_END=T_REB+REB_LEN;
const BPS=132/60;
const BLAST={x:660,y:400}, REST_Y=578;
const NAMES=['MOONBEANS','MOONBEANS 2','MOONBEANS: REBORN','MOONBEANS (UNICORN-PROOF)','NEW MOONBEANS','MOONBEANS ...AGAIN','MOONBEANS 6: DOOMSDAY','MOONBEANS (PRE-OWNED)'];
const nameFor=n=>n<NAMES.length?NAMES[n]:'MOONBEANS #'+(n+1);
/* ================= state ================= */
const S={served:0,uni:0,events:0,streak:0,longest:0,intervals:[],near:0,brinkT:0,launched:0,baristas:0,balked:0,
drought:0,longDrought:0,run:0,longRun:0,exited:0,cost:0,simT:0,dice:[0,0,0,0,0,0],log:[],evlog:[],logDirty:true};
const W={phase:'open',t:0,nextArr:0.5,doorCd:0,cust:[],line:[],queue:[],seats:new Array(SEATS.length).fill(null),
nid:1,K:0,prevK:0,die:null,ordering:null,holders:[],names:0,signName:NAMES[0]};
const fx={glow:0,unst:0,unstT:0,flash:0,flashHue:-1,shake:0};
const ev={t:0,n:0,exploded:false,hatched:false,psy:0,hue:0,punch:0,acc:0,cf:0,fw:0,ringT:0};
const bar={x:848,y:424,alive:true,launch:false,vx:0,vy:0,rot:0,spin:0,hue:30,work:0};
const P=[],R=[];
let rt=0, speed=2, paused=false, slowMode=1, FAST=false, clockA=0;
let c=null;
/* ================= canvas ================= */
const cv=$('cv'), ctxMain=cv.getContext('2d');
const off=document.createElement('canvas'), octx=off.getContext('2d');
let CW=2,CH=2,DPR=1,VS=1,VX=0,VY=0;
function resize(){
const r=cv.parentElement.getBoundingClientRect();
DPR=Math.min(window.devicePixelRatio||1,2);
CW=Math.max(2,Math.floor(r.width*DPR));CH=Math.max(2,Math.floor(r.height*DPR));
cv.width=CW;cv.height=CH;off.width=CW;off.height=CH;
VS=Math.min(CW/1000,CH/600);VX=(CW-1000*VS)/2;VY=(CH-600*VS)/2;
}
window.addEventListener('resize',resize);resize();
/* ================= shop chunks (the building is a pile of pieces) ================= */
function buildChunks(){
const ch=[];
const add=o=>{o.ox=0;o.oy=0;o.rot=0;o.vx=0;o.vy=0;o.vr=0;o.jx=0;o.jy=0;o.state='home';ch.push(o);};
const cols=11,rows=7,cw=550/cols,rh=322/rows;
for(let r=0;r<rows;r++)for(let q=0;q<cols;q++)
add({k:'wall',layer:0,x:386+q*cw,y:190+r*rh,w:cw,h:rh,col:hsl(26+rnd(-3,5),38,26+6*(1-r/rows)+rnd(0,3))});
for(let i=0;i<14;i++)add({k:'floor',layer:0,x:372+i*(578/14),y:FLOOR,w:578/14,h:18});
for(let i=0;i<6;i++)add({k:'roof',layer:2,x:362+i*100,y:150,w:100,h:40,i});
add({k:'sign',layer:2,x:540,y:100,w:240,h:46});
add({k:'pillar',layer:2,x:372,y:190,w:14,h:131});
add({k:'pillar',layer:2,x:372,y:321,w:14,h:131});
for(let i=0;i<3;i++)add({k:'pillar',layer:2,x:936,y:190+i*109,w:14,h:109});
add({k:'menu',layer:1,x:412,y:212,w:132,h:82});
add({k:'clock',layer:1,x:563,y:218,w:44,h:44});
add({k:'window',layer:1,x:626,y:212,w:96,h:112});
add({k:'window',layer:1,x:742,y:212,w:96,h:112});
add({k:'espresso',layer:1,x:884,y:388,w:52,h:44});
add({k:'counter',layer:2,x:800,y:432,w:140,h:84});
for(const s of SEATS)add({k:'table',layer:1,x:s[0]+6,y:s[1]+6,w:34,h:14});
return ch;
}
let chunks=buildChunks();
/* ================= scenery data ================= */
const STARS=Array.from({length:90},()=>({x:rnd(-300,1300),y:rnd(-250,400),r:rnd(.6,1.8),p:rnd(0,6.28)}));
const SKY=[];
(()=>{let x=-400;while(x<1400){const w=rnd(50,110),h=rnd(60,240);
SKY.push({x,w,h,win:Array.from({length:Math.floor(w*h/1400)},()=>[rnd(4,w-10),rnd(6,h-12),Math.random()<.55])});x+=w+rnd(0,20);}})();
/* ================= particles ================= */
function emit(o){
if(P.length>2000)return;
const p=Object.assign({x:0,y:0,vx:0,vy:0,g:0,drag:0,life:1,size:3,hue:0,type:'spark',rot:0,vr:0},o);
p.max=p.life;P.push(p);
}
function updateParticles(dt){
let j=0;
for(let i=0;i<P.length;i++){
const p=P[i];p.life-=dt;
if(p.life>0){
p.vy+=p.g*dt;
if(p.drag){const d=Math.pow(p.drag,dt*60);p.vx*=d;p.vy*=d;}
p.x+=p.vx*dt;p.y+=p.vy*dt;p.rot+=p.vr*dt;P[j++]=p;
}
}
P.length=j;
j=0;
for(let i=0;i<R.length;i++){const r=R[i];r.life-=dt;r.r+=r.vr*dt;if(r.life>0)R[j++]=r;}
R.length=j;
}
function burst(x,y,n,spd,hue,life){
for(let i=0;i<n;i++){const a=rnd(0,TAU),s=rnd(spd*.3,spd);
emit({x,y,vx:Math.cos(a)*s,vy:Math.sin(a)*s,drag:.96,life:rnd(life*.5,life),size:rnd(1.5,3.5),hue:hue+rnd(-40,40),type:'spark'});}
}
function firework(){
const x=rnd(80,920),y=rnd(50,260),h=rnd(0,360),n=46;
for(let i=0;i<n;i++){const a=i/n*TAU,sp=rnd(120,330);
emit({x,y,vx:Math.cos(a)*sp,vy:Math.sin(a)*sp,g:180,drag:.97,life:rnd(.9,1.5),size:3.2,hue:h+rnd(-30,30)});}
}
/* ================= customers ================= */
function newCustomer(x,y,state){
const u={id:W.nid++,x,y,px:x,py:y,face:1,hue:Math.floor(rnd(0,360)),state,inside:false,drink:null,timer:0,hold:0,seat:-1,
wp:rnd(0,6),mv:false,pop:1,rot:0,spin:0,vx:0,vy:0,g:0,kv:0,hoverT:0,dead:false};
W.cust.push(u);return u;
}
function arrive(){
if(W.line.length>=LINE_MAX){S.balked++;const b=newCustomer(-30,LANE_Y,'balk');b.hue=210;return;}
const u=newCustomer(-30,LINE_Y,'line');W.line.push(u);
}
function moveTo(u,tx,ty,sp,dt){
const dx=tx-u.x,dy=ty-u.y,d=Math.hypot(dx,dy);
if(d<0.001)return true;
if(dx>0.5)u.face=1;else if(dx<-0.5)u.face=-1;
u.mv=true;const st=sp*dt;
if(d<=st){u.x=tx;u.y=ty;u.wp+=d*0.3;return true;}
u.x+=dx/d*st;u.y+=dy/d*st;u.wp+=st*0.3;return false;
}
function countK(){
const h=[];for(const u of W.cust)if(u.inside&&u.drink&&u.drink.uni)h.push(u);
W.holders=h;return h.length;
}
function admit(){
if(W.doorCd>0||!W.line.length)return;
let inside=0;for(const u of W.cust)if(u.inside)inside++;
if(inside>=CAP)return;
const f=W.line[0];if(f.x<LINE_X0-8)return;
W.line.shift();f.inside=true;f.state='toQueue';W.queue.push(f);W.doorCd=0.8;
}
/* ---- THE ROLL. This is the only place a drink's fate is decided. ---- */
function serve(u){
const die=1+Math.floor(Math.random()*6); // one fair d6 per drink, rolled at the counter
const uni=(die===1); // 1 => Neon Unicorn. P = 1/6. No other input.
S.served++;S.streak++;if(S.streak>S.longest)S.longest=S.streak;
S.dice[die-1]++;S.log.push(uni?1:0);if(S.log.length>60)S.log.shift();S.logDirty=true;
if(uni){S.uni++;S.run++;if(S.run>S.longRun)S.longRun=S.run;S.drought=0;}
else{S.run=0;S.drought++;if(S.drought>S.longDrought)S.longDrought=S.drought;}
u.drink={uni,die,seed:Math.random()*6};
u.pop=-0.5;
const qi=W.queue.indexOf(u);if(qi>=0)W.queue.splice(qi,1);
W.ordering=null;
const free=[];for(let i=0;i<SEATS.length;i++)if(!W.seats[i])free.push(i);
const si=free.length?free[Math.floor(Math.random()*free.length)]:0;
W.seats[si]=u;u.seat=si;
u.hold=rnd(8,14);u.state='toSeat';
W.die={face:die,t:0};
if(uni&&!FAST){
burst(ORDER_X+16,QUEUE_Y-14,34,260,rnd(0,360),.9);
R.push({x:ORDER_X+16,y:QUEUE_Y-14,r:4,vr:160,life:.7,max:.7,hue:rnd(0,360),w:6});
}
}
function updateCustomers(dt){
let dead=false;
for(const u of W.cust){
u.mv=false;u.pop=Math.min(1,u.pop+dt*2.5);
switch(u.state){
case 'line':{
if(u.kv){u.x+=u.kv*dt;u.kv*=Math.pow(0.03,dt);if(Math.abs(u.kv)<2)u.kv=0;}
const i=W.line.indexOf(u);moveTo(u,LINE_X0-Math.max(i,0)*LINE_GAP,LINE_Y,90,dt);break;}
case 'toQueue':{
const i=W.queue.indexOf(u);
const arr=moveTo(u,ORDER_X-Math.max(i,0)*QUEUE_GAP,QUEUE_Y,100,dt);
if(i===0&&arr){u.state='ordering';u.timer=rnd(2,4);u.face=1;W.ordering=u;}
break;}
case 'ordering':u.timer-=dt;bar.work=1;if(u.timer<=0)serve(u);break;
case 'toSeat':case 'sitting':{
u.hold-=dt;
if(u.hold<=0){W.seats[u.seat]=null;u.seat=-1;u.state='exiting';break;}
if(u.state==='toSeat'){const s=SEATS[u.seat];if(moveTo(u,s[0],s[1],100,dt))u.state='sitting';}
break;}
case 'exiting':
moveTo(u,DOOR_X-2,LINE_Y,100,dt);
if(u.x<=DOOR_X){u.inside=false;if(u.drink&&u.drink.uni)S.exited++;u.state='leaving';}
break;
case 'leaving':
if(u.x>DOOR_X-44)moveTo(u,DOOR_X-44,LANE_Y,100,dt);else moveTo(u,-80,LANE_Y,100,dt);
if(u.x<-70){u.dead=true;dead=true;}
break;
case 'balk':if(moveTo(u,50,LANE_Y,100,dt)){u.state='balkwait';u.timer=1.4;}break;
case 'balkwait':u.timer-=dt;if(u.timer<=0)u.state='leaving';break;
case 'hover':u.hoverT+=dt;break;
case 'launched':
u.x+=u.vx*dt;u.y+=u.vy*dt;u.rot+=u.spin*dt;
if(u.y<-300||u.x<-400||u.x>1400){u.dead=true;dead=true;}
break;
}
}
if(dead)W.cust=W.cust.filter(u=>!u.dead);
}
/* ================= event machinery ================= */
function startEvent(){
W.phase='event';ev.t=0;ev.n=++S.events;ev.exploded=false;ev.hatched=false;ev.acc=0;ev.ringT=0;
S.intervals.push(S.streak);
S.evlog.unshift({n:ev.n,cust:S.streak,t:S.simT});if(S.evlog.length>6)S.evlog.pop();
S.streak=0;W.prevK=0;
S.cost+=Math.round(rnd(0.4,2.6)*100)*10000;
for(const u of W.cust)if(u.inside){u.state='hover';u.hoverT=0;}
}
function explode(){
ev.exploded=true;fx.flash=1;fx.flashHue=-1;fx.shake=1;ev.punch=1;
for(const u of W.cust){
if(u.state==='hover'){
u.state='launched';u.inside=false;S.launched++;
u.vx=(u.x-BLAST.x)*rnd(.5,1.2)+rnd(-80,80);u.vy=-rnd(450,900);u.g=0;u.spin=rnd(-14,14)||6;
}
}
W.queue.length=0;W.seats.fill(null);W.ordering=null;W.die=null;
if(bar.alive){bar.alive=false;bar.launch=true;bar.vx=rnd(-120,220);bar.vy=-820;bar.spin=9;S.baristas++;}
for(const k of chunks){
k.state='fly';k.jx=0;k.jy=0;
const cx=k.x+k.w/2,cy=k.y+k.h/2,dx=cx-BLAST.x,dy=cy-BLAST.y,d=Math.hypot(dx,dy)||1;
const sp=rnd(250,750)*(1.1-Math.min(d,400)/800);
k.vx=dx/d*sp+rnd(-60,60);k.vy=dy/d*sp-rnd(200,500);k.vr=rnd(-8,8);
}
for(const u of W.line)u.kv=-rnd(80,200);
for(let i=0;i<5;i++)R.push({x:BLAST.x,y:BLAST.y,r:10-i*45,vr:900+i*80,life:1.3,max:1.3,hue:i*70,w:16});
for(let i=0;i<260;i++)emit({x:BLAST.x+rnd(-120,120),y:BLAST.y+rnd(-80,80),vx:rnd(-900,900),vy:rnd(-1000,300),g:700,drag:.99,life:rnd(1,2.6),size:rnd(4,9),hue:rnd(0,360),type:'conf',vr:rnd(-12,12),rot:rnd(0,6)});
for(let i=0;i<220;i++){const a=rnd(0,TAU),s=rnd(200,1200);
emit({x:BLAST.x,y:BLAST.y,vx:Math.cos(a)*s,vy:Math.sin(a)*s,drag:.95,life:rnd(.5,1.5),size:rnd(2,6),hue:rnd(0,360)});}
}
function startRebuild(){
W.phase='rebuild';
for(const k of chunks){
k.state='ret';k.sx=k.ox;k.sy=k.oy;
k.sr=((k.rot+Math.PI)%TAU+TAU)%TAU-Math.PI;
k.delay=((FLOOR+20)-(k.y+k.h))/330*2.6+rnd(0,.5);
}
}
function finishRebuild(){
for(const k of chunks){k.state='home';k.ox=k.oy=k.rot=0;k.jx=k.jy=0;}
W.phase='open';W.doorCd=1.5;W.K=0;W.prevK=0;W.holders=[];
bar.alive=true;bar.launch=false;bar.x=848;bar.y=424;bar.rot=0;bar.hue=rnd(0,360);
W.names++;W.signName=nameFor(W.names);
fx.unstT=0;ev.psy=0;ev.punch=0;
}
function beatPulse(){const bt=(ev.t-T_DANCE)*BPS;const f=bt-Math.floor(bt);return Math.pow(1-f,3);}
const UP={vis:false,x:660,y:REST_Y+6,s:0,xs:1,rot:0,hop:0,rear:0,ph:0,amp:.5,kick:0,bob:0,sq:1,hue:0,t:0};
function updateUnicorn(dt){
const t=ev.t;UP.t=t;UP.hue=(ev.hue*1.3)%360;
const T_FLY=T_LAUNCH+0.8;
if(t<T_HATCH||t>T_FLY+2.3){UP.vis=false;return;}
UP.vis=true;
const a=1-Math.pow(0.001,dt);
if(t<T_DANCE){
const k=clamp((t-T_HATCH)/(T_DANCE-T_HATCH),0,1);
UP.s=1.25*easeOutElastic(k);UP.x=660;UP.y=REST_Y+6+(1-smooth(k*1.6))*40;
UP.xs=1;UP.rot=0;UP.hop=0;UP.rear=0;UP.kick=0;UP.bob=0;UP.sq=1+0.06*Math.sin(k*20);UP.ph=t*4;UP.amp=.3;
}else if(t<T_LAUNCH){
const bt=(t-T_DANCE)*BPS,m=Math.floor(bt/8)%4,fr=(bt%8)/8;
let hop=0,rear=0,kick=0,bob=0,amp=.6,x=660,xs=1,ph=bt*Math.PI;
switch(m){
case 0:hop=Math.abs(Math.sin(bt*Math.PI))*40;bob=Math.sin(bt*Math.PI*2)*.18;break;
case 1:rear=.1+.42*(.5+.5*Math.sin(bt*Math.PI-Math.PI/2));kick=1;bob=-.2;amp=1;break;
case 2:xs=Math.cos(fr*TAU);hop=Math.abs(Math.sin(bt*Math.PI))*55;bob=Math.sin(bt*Math.PI)*.25;break;
case 3:x=660+Math.sin(fr*TAU)*150;amp=.35;bob=Math.sin(bt*Math.PI*2)*.1;hop=Math.abs(Math.sin(bt*Math.PI*2))*8;ph=bt*Math.PI*3;break;
}
UP.s=1.25;UP.y=REST_Y+6;UP.hop=hop;UP.xs=xs;UP.rot=0;UP.sq=1;UP.ph=ph;
UP.rear=lerp(UP.rear,rear,a);UP.kick=lerp(UP.kick,kick,a);UP.bob=lerp(UP.bob,bob,a);UP.amp=lerp(UP.amp,amp,a);UP.x=lerp(UP.x,x,a);
}else if(t<T_FLY){
const k=(t-T_LAUNCH)/0.8;
UP.sq=1-0.2*Math.sin(k*Math.PI);UP.rear=lerp(UP.rear,-0.12,a);UP.hop=0;UP.xs=1;UP.kick=0;UP.amp=.3;UP.x=lerp(UP.x,660,a);UP.rot=0;UP.s=1.25;UP.y=REST_Y+6;
}else{
const tt=t-T_FLY;
UP.x=660+tt*tt*300+tt*60;UP.y=REST_Y+6-(tt*tt*380+tt*120);UP.rot=-0.5*Math.min(1,tt*1.5);
UP.s=1.25*(1-0.25*tt);UP.sq=1;UP.hop=0;UP.rear=.2;UP.kick=0;UP.amp=1.1;UP.ph=t*22;UP.bob=-.1;UP.xs=1;
if(!FAST&&Math.random()<dt*120)emit({x:UP.x-60*UP.s,y:UP.y-190*UP.s,vx:-rnd(50,150),vy:rnd(-30,60),life:rnd(.5,1),size:rnd(3,7),hue:rnd(0,360)});
}
}
const CAPS=[[0,'THREE NEON UNICORNS. ONE ROOM.'],[0.9,'THE COINCIDENCE IS BECOMING A PROBLEM'],[T_EXPL,'ORDER UP!!!'],
[3.0,'SOMETHING IS HATCHING OUT OF THE ESPRESSO MACHINE'],[T_DANCE+0.2,'IT IS TALL. IT IS DARK. IT HAS A HORN. (SIZE: VENTI)'],
[6.0,'THE HORN IS A LASER NOW'],[7.4,'MOONBEANS IS NOW A NIGHTCLUB'],[8.8,'BARISTA: "I DIDN\'T SIGN UP FOR THIS" (LAUNCHED)'],
[10.2,'PLEASE DO NOT FEED THE UNICORN CARAMEL'],[11.6,'TIP JAR: EATEN'],[T_LAUNCH+.2,'IT HAS ORDERED ITSELF A TAKEAWAY'],
[T_REB,'REBUILDING... THE INSURER IS WEEPING']];
function eventStep(dt){
ev.t+=dt;const t=ev.t;
ev.hue=(ev.hue+dt*140)%360;
ev.punch=Math.max(0,ev.punch-dt*1.8);
ev.psy=t<T_EXPL?0:t<T_REB?smooth((t-T_EXPL)/0.9):1-smooth((t-T_REB)/2.4);
if(t<T_EXPL){
const k=t/T_EXPL;
ev.acc+=dt*(40+260*k);
while(ev.acc>=1){ev.acc--;
const ang=rnd(0,TAU),r=rnd(320,520),sx=BLAST.x+Math.cos(ang)*r,sy=BLAST.y+Math.sin(ang)*r*.7;
emit({x:sx,y:sy,vx:(BLAST.x-sx)*2.2,vy:(BLAST.y-sy)*2.2,life:.45,size:rnd(2,4.5),hue:rnd(0,360)});}
ev.ringT-=dt;if(ev.ringT<=0){ev.ringT=.35;R.push({x:BLAST.x,y:BLAST.y,r:380,vr:-600,life:.6,max:.6,hue:rnd(0,360),w:8});}
for(const u of W.cust)if(u.state==='hover'&&u.drink&&u.drink.uni&&Math.random()<dt*60)
emit({x:u.x+u.face*13,y:u.y-30,vx:rnd(-30,30),vy:-rnd(100,300),life:.8,size:rnd(2,4),hue:rnd(0,360)});
if(Math.floor(t*(8+14*k))%2===0&&Math.random()<dt*20){fx.flash=Math.max(fx.flash,.3);fx.flashHue=rnd(0,360);}
}
if(!ev.exploded&&t>=T_EXPL)explode();
if(!ev.hatched&&t>=T_HATCH){ev.hatched=true;fx.flash=.7;fx.flashHue=-1;fx.shake=Math.max(fx.shake,.6);
for(let i=0;i<3;i++)R.push({x:660,y:REST_Y,r:10-i*30,vr:700,life:1.2,max:1.2,hue:i*120,w:14});}
if(t>T_HATCH&&t<T_LAUNCH+1){
ev.cf+=dt*(FAST?10:38);
while(ev.cf>=1){ev.cf--;emit({x:rnd(-50,1050),y:-20,vx:rnd(-40,40),vy:rnd(100,260),life:4,size:rnd(3,6),hue:rnd(0,360),type:'conf',vr:rnd(-8,8),rot:rnd(0,6)});}
ev.fw-=dt;if(t>T_DANCE-.6&&ev.fw<=0){ev.fw=rnd(.25,.6);firework();}
}
updateUnicorn(dt);
if(W.phase==='event'&&t>=T_REB)startRebuild();
if(W.phase==='rebuild'&&t>=T_END)finishRebuild();
}
/* ================= per-step update ================= */
function updateFx(dt){
let g=0,u=0;
if(W.phase==='open'){
g=W.K>=1?1:0;
if(W.K===2){fx.unstT+=dt;u=Math.min(1,.35+fx.unstT/7);}else fx.unstT=Math.max(0,fx.unstT-dt*3);
}else{g=1;u=ev.t<T_EXPL?1:0;}
fx.glow+=(g-fx.glow)*Math.min(1,dt*2.5);
fx.unst+=(u-fx.unst)*Math.min(1,dt*(u>fx.unst?3:1.5));
fx.flash=Math.max(0,fx.flash-dt*3);
fx.shake*=Math.pow(0.02,dt);
if(fx.unst>.2&&W.phase==='open'&&Math.random()<dt*10*fx.unst){fx.flash=Math.max(fx.flash,rnd(.15,.4)*fx.unst);fx.flashHue=rnd(0,360);}
if(!FAST&&W.phase==='open'){
const rate=1.5+16*fx.unst;
for(const u2 of W.holders)if(Math.random()<rate*dt)
emit({x:u2.x+u2.face*13+rnd(-4,4),y:u2.y-4,vx:rnd(-20,20),vy:-rnd(30,90)-60*fx.unst,g:-10,life:rnd(.6,1.2),size:rnd(1.5,3),hue:rnd(0,360)});
if(fx.unst>.3&&Math.random()<dt*20*fx.unst)
emit({x:rnd(390,930),y:rnd(300,510),vx:rnd(-30,30),vy:-rnd(40,140),life:rnd(.8,1.6),size:rnd(1.5,3.5),hue:rnd(0,360)});
}
}
function updateChunks(dt){
for(const k of chunks){
if(k.state==='fly'){
k.vy+=1100*dt;k.ox+=k.vx*dt;k.oy+=k.vy*dt;k.rot+=k.vr*dt;
const cx=k.x+k.w/2+k.ox,cy=k.y+k.h/2+k.oy;
if((cx<-120&&k.vx<0)||(cx>1120&&k.vx>0))k.vx*=-0.4;
const fl=REST_Y-Math.min(k.w,k.h)*0.4-k.h/2;
if(cy>fl+k.h/2-Math.min(k.w,k.h)*0.4+ (k.h/2) - k.h/2 && k.vy>0 && (k.y+k.h/2+k.oy)>REST_Y-Math.min(k.w,k.h)*0.4){
k.oy-=(k.y+k.h/2+k.oy)-(REST_Y-Math.min(k.w,k.h)*0.4);
k.vy*=-0.38;k.vx*=0.72;k.vr*=0.55;
if(Math.abs(k.vy)<60){k.state='rest';k.vx=k.vy=k.vr=0;}
}
}else if(k.state==='ret'){
const r=ev.t-T_REB-k.delay;
if(r>=0){
const pr=clamp(r/0.9,0,1),e=1-Math.pow(1-pr,3);
k.ox=k.sx*(1-e);k.oy=k.sy*(1-e)-Math.sin(pr*Math.PI)*70;k.rot=k.sr*(1-e);
if(pr>=1){k.state='home';k.ox=k.oy=k.rot=0;
if(!FAST&&Math.random()<.3)burst(k.x+k.w/2,k.y+k.h/2,4,120,rnd(0,360),.5);}
}
}else if(k.state==='home'){
if(fx.unst>.02&&W.phase==='open'){
k.jx=(Math.random()-.5)*3.2*fx.unst;
k.jy=(Math.random()-.5)*3.2*fx.unst-(k.k==='table'?fx.unst*(8+6*Math.sin(rt*3+k.x)):0);
}else{k.jx=0;k.jy=0;}
}
}
}
function step(dt){
W.t+=dt;S.simT+=dt;
W.nextArr-=dt;
if(W.nextArr<=0){arrive();W.nextArr=-Math.log(1-Math.random())*ARRIVAL_MEAN;}
W.doorCd-=dt;
if(W.phase==='open')admit();
updateCustomers(dt);
bar.work=Math.max(0,bar.work-dt*4);
if(bar.launch){bar.x+=bar.vx*dt;bar.y+=bar.vy*dt;bar.rot+=bar.spin*dt;if(bar.y<-300)bar.launch=false;}
if(W.phase==='open'){
W.K=countK();
if(W.K===2)S.brinkT+=dt;
if(W.prevK===2&&W.K<2)S.near++;
W.prevK=W.K;
if(W.K>=3)startEvent(); // the ONLY trigger: three unicorn-holders inside at once
}
updateFx(dt);updateChunks(dt);updateParticles(dt);
clockA+=dt*.35*(fx.glow>.3?-1:1)*(1+25*fx.unst)*(W.phase==='open'?1:0);
if(W.phase!=='open')eventStep(dt);
if(W.die){W.die.t+=dt;if(W.die.t>2.2)W.die=null;}
}
/* ================= drawing primitives ================= */
function rr(x,y,w,h,r){c.beginPath();c.moveTo(x+r,y);c.arcTo(x+w,y,x+w,y+h,r);c.arcTo(x+w,y+h,x,y+h,r);c.arcTo(x,y+h,x,y,r);c.arcTo(x,y,x+w,y,r);c.closePath();}
function dot(x,y,r){c.beginPath();c.arc(x,y,r,0,TAU);c.fill();}
function spark4(x,y,r){c.beginPath();c.moveTo(x,y-r);c.quadraticCurveTo(x,y,x+r,y);c.quadraticCurveTo(x,y,x,y+r);c.quadraticCurveTo(x,y,x-r,y);c.quadraticCurveTo(x,y,x,y-r);c.fill();}
const GCH='#%&@?$*+=';
function garble(s,amt){const q=Math.floor(rt*10);let o='';for(let i=0;i<s.length;i++)o+=(s[i]!==' '&&hrand(q*57+i)<amt)?GCH[Math.floor(hrand(q*13+i*7)*GCH.length)]:s[i];return o;}
const gtxt=s=>fx.unst>.05?garble(s,.12*fx.unst):s;
function fitText(txt,maxW,size,weight){c.font=`${weight} ${size}px system-ui,sans-serif`;const w=c.measureText(txt).width;if(w>maxW)c.font=`${weight} ${(size*maxW/w).toFixed(2)}px system-ui,sans-serif`;}
function signText(){
let s=W.signName;
if(fx.unst>.05)return garble(s,.25*fx.unst+.05);
if(fx.glow>.4){
const pat=[0,1,0,0,2,0,3,0,0,4],v=pat[Math.floor(rt/1.3)%pat.length];
if(v===1)s=s.replace(/O/g,'0');else if(v===2)s=s.replace(/S(?!.*S)/,'Z');else if(v===3)s=s.split('').reverse().join('');else if(v===4)s=s.replace('E','3');
}
return s;
}
const PIPS={1:[[0,0]],2:[[-1,-1],[1,1]],3:[[-1,-1],[0,0],[1,1]],4:[[-1,-1],[1,-1],[-1,1],[1,1]],5:[[-1,-1],[1,-1],[0,0],[-1,1],[1,1]],6:[[-1,-1],[1,-1],[-1,0],[1,0],[-1,1],[1,1]]};
/* ================= drawing: chunks ================= */
function drawChunk(k){
const w=k.w,h=k.h;
c.save();
c.translate(k.x+w/2+k.ox+k.jx,k.y+h/2+k.oy+k.jy);
if(k.rot)c.rotate(k.rot);
const x=-w/2,y=-h/2;
switch(k.k){
case 'wall':{
c.fillStyle=k.col;c.fillRect(x,y,w+.6,h+.6);
c.strokeStyle='rgba(0,0,0,.22)';c.lineWidth=1;c.strokeRect(x+.5,y+.5,w-1,h-1);
if(fx.glow>.02||fx.unst>.02){
const a=.05*fx.glow+.26*fx.unst*(.5+.5*Math.sin(rt*5-k.x*.02-k.y*.03));
c.fillStyle=hsl(rt*80+k.x*.3,100,60,a);c.fillRect(x,y,w+.6,h+.6);
}
break;}
case 'floor':c.fillStyle='#6a4429';c.fillRect(x,y,w+.6,h);c.fillStyle='#8b5a37';c.fillRect(x,y,w+.6,3);c.strokeStyle='rgba(0,0,0,.3)';c.strokeRect(x+.5,y+.5,w-1,h-1);break;
case 'pillar':c.fillStyle='#242833';c.fillRect(x,y,w,h+.6);c.fillStyle='#3a4050';c.fillRect(x+2,y,3,h);break;
case 'roof':{
for(let i=0;i<5;i++){c.fillStyle=i%2?'#efe6cf':'#0f6b46';c.fillRect(x+i*20,y,20.6,h-8);}
for(let i=0;i<5;i++){c.fillStyle=i%2?'#efe6cf':'#0f6b46';c.beginPath();c.arc(x+10+i*20,y+h-8,10,0,Math.PI);c.fill();}
for(let i=0;i<3;i++){
const bx=x+17+i*33,on=fx.unst>.02?((Math.floor(rt*8)+i+k.i)%3===0):false;
c.fillStyle=on?hsl(rt*300+k.i*60+i*90,100,65):(fx.glow>.02?hsl(rt*60+k.i*40,80,60,.8):'#ffe9a8');
dot(bx,y+h-3,3.2);
}
break;}
case 'sign':{
if(fx.glow>.02||fx.unst>.02){c.fillStyle=hsl(rt*80,100,60,.22*fx.glow+.25*fx.unst);rr(x-6,y-6,w+12,h+12,12);c.fill();}
c.fillStyle='#0d3b2b';rr(x,y,w,h,8);c.fill();c.lineWidth=3;c.strokeStyle='#c9a55a';c.stroke();
c.fillStyle='#22a06b';dot(x+26,0,16);c.fillStyle='#f4efe0';dot(x+26,0,11);c.fillStyle='#22a06b';dot(x+31,-3,10);
c.textAlign='center';c.textBaseline='middle';
const st=signText();c.fillStyle='#f4efe0';fitText(st,180,19,'800');c.fillText(st,x+w/2+19,-6);
c.fillStyle='#f0c674';c.font='700 8.5px system-ui,sans-serif';c.fillText(gtxt('NEON UNICORN FRAPPS - 1 IN 6*'),x+w/2+19,12);
break;}
case 'menu':{
c.fillStyle='#16241d';c.fillRect(x,y,w,h);c.strokeStyle='#8a6238';c.lineWidth=4;c.strokeRect(x,y,w,h);
c.textAlign='center';c.textBaseline='middle';c.fillStyle='#e9e4d0';
c.font='800 11px system-ui,sans-serif';c.fillText(gtxt('FRAPPUCCINO'),0,y+13);
c.font='600 9px system-ui,sans-serif';c.fillText(gtxt('CARAMEL ........ $7'),0,y+29);c.fillText(gtxt('MOCHA .......... $7'),0,y+41);
c.fillStyle=hsl(rt*200,100,70);c.font='800 9px system-ui,sans-serif';c.fillText(gtxt('NEON UNICORN* 1 IN 6'),0,y+57);
c.fillStyle='#8fa';c.font='600 7px system-ui,sans-serif';c.fillText(gtxt('*side effects may include events'),0,y+71);
break;}
case 'clock':{
c.fillStyle='#f3ecd8';dot(0,0,w/2);c.strokeStyle='#5a3a22';c.lineWidth=3;c.beginPath();c.arc(0,0,w/2-1,0,TAU);c.stroke();
c.strokeStyle='#222';c.lineWidth=1;
for(let i=0;i<12;i++){const a=i/12*TAU;c.beginPath();c.moveTo(Math.cos(a)*16,Math.sin(a)*16);c.lineTo(Math.cos(a)*19,Math.sin(a)*19);c.stroke();}
c.lineWidth=2;c.beginPath();c.moveTo(0,0);c.lineTo(Math.cos(clockA-1.2)*14,Math.sin(clockA-1.2)*14);c.stroke();
c.lineWidth=2.6;c.beginPath();c.moveTo(0,0);c.lineTo(Math.cos(clockA/12+.8)*9,Math.sin(clockA/12+.8)*9);c.stroke();
break;}
case 'window':{
const g=c.createLinearGradient(0,y,0,y+h);g.addColorStop(0,'#101a3d');g.addColorStop(1,'#2a2560');c.fillStyle=g;c.fillRect(x,y,w,h);
if(fx.glow>.02||fx.unst>.02){c.fillStyle=hsl(rt*60,100,60,.18*fx.glow+.3*fx.unst);c.fillRect(x,y,w,h);}
c.fillStyle='rgba(255,255,255,.07)';c.beginPath();c.moveTo(x,y+h*.6);c.lineTo(x+w*.5,y);c.lineTo(x+w*.8,y);c.lineTo(x,y+h*.9);c.fill();
c.strokeStyle='#d8cbb0';c.lineWidth=4;c.strokeRect(x,y,w,h);c.lineWidth=2.5;c.beginPath();c.moveTo(0,y);c.lineTo(0,y+h);c.moveTo(x,0);c.lineTo(x+w,0);c.stroke();
break;}
case 'espresso':{
c.fillStyle='#b9bfc9';c.fillRect(x,y,w,h);c.fillStyle='#7d8593';c.fillRect(x,y+h-12,w,12);c.fillStyle='#2a2f38';c.fillRect(x+6,y+8,w-12,12);
c.fillStyle=(fx.glow>.02)?hsl(rt*120,100,60):'#e33';dot(x+w-10,y+30,3);
c.fillStyle='#555';c.fillRect(x+10,y+h-2,6,4);c.fillRect(x+w-16,y+h-2,6,4);
break;}
case 'counter':{
c.fillStyle='#7a4a2b';c.fillRect(x,y,w,h+.6);c.fillStyle='#c9925a';c.fillRect(x-2,y,w+4,8);
c.strokeStyle='rgba(0,0,0,.25)';c.lineWidth=1;for(let i=1;i<5;i++){c.beginPath();c.moveTo(x+i*w/5,y+8);c.lineTo(x+i*w/5,y+h);c.stroke();}
c.fillStyle='rgba(220,240,255,.7)';c.fillRect(x+8,y-12,10,12);c.fillStyle='#e0b040';c.fillRect(x+9,y-5,8,4);
break;}
case 'table':c.fillStyle='#5b3a26';c.beginPath();c.ellipse(0,0,w/2,h/2,0,0,TAU);c.fill();c.fillStyle='#7a5238';c.beginPath();c.ellipse(0,-2,w/2-3,h/2-3,0,0,TAU);c.fill();break;
}
c.restore();
}
function drawChunks(layer){for(const k of chunks)if(k.layer===layer)drawChunk(k);}
/* ================= drawing: sky / street ================= */
function drawSky(){
const g=c.createLinearGradient(0,-300,0,520);g.addColorStop(0,'#0a0620');g.addColorStop(.6,'#2b1450');g.addColorStop(1,'#5b2a72');
c.fillStyle=g;c.fillRect(-3000,-3000,7000,3600);
c.fillStyle='#fff';
for(const s of STARS){c.globalAlpha=.4+.5*Math.sin(rt*2+s.p);dot(s.x,s.y,s.r);}
c.globalAlpha=1;
c.fillStyle='#f4efe0';dot(140,90,34);c.fillStyle='rgba(0,0,0,.08)';dot(128,80,8);dot(152,102,5);dot(150,76,4);
if(ev.psy>.02){c.fillStyle=hsl(ev.hue,100,60,.6*ev.psy);dot(140,90,34);c.strokeStyle=hsl(ev.hue+120,100,80,.8*ev.psy);c.lineWidth=1.5;
c.beginPath();c.moveTo(106,90);c.lineTo(174,90);c.moveTo(140,56);c.lineTo(140,124);c.stroke();}
for(const b of SKY){
c.fillStyle='#171034';c.fillRect(b.x,FLOOR-b.h,b.w,b.h);
c.fillStyle='rgba(255,217,138,.55)';
for(const w of b.win)if(w[2])c.fillRect(b.x+w[0],FLOOR-b.h+w[1],4,5);
}
if(ev.psy>.01){
const a=ev.psy;
const g2=c.createLinearGradient(0,-300,0,520);
g2.addColorStop(0,hsl(ev.hue,90,35,.8*a));g2.addColorStop(.5,hsl(ev.hue+120,90,45,.7*a));g2.addColorStop(1,hsl(ev.hue+240,90,55,.8*a));
c.fillStyle=g2;c.fillRect(-3000,-3000,7000,3600);
c.globalCompositeOperation='lighter';
const n=20,rot=ev.t*.5;
for(let i=0;i<n;i+=2){
const a0=rot+i/n*TAU,a1=rot+(i+1)/n*TAU;
c.fillStyle=hsl(ev.hue+i*18,100,60,.16*a);c.beginPath();c.moveTo(660,330);
c.lineTo(660+Math.cos(a0)*2500,330+Math.sin(a0)*2500);c.lineTo(660+Math.cos(a1)*2500,330+Math.sin(a1)*2500);c.closePath();c.fill();
}
c.globalCompositeOperation='source-over';
}
}
function drawStreet(){
c.fillStyle='#3a3350';c.fillRect(-3000,FLOOR,7000,46);
c.fillStyle='#4b4468';c.fillRect(-3000,FLOOR,7000,4);
c.fillStyle='#6a6285';c.fillRect(-3000,559,7000,4);
c.fillStyle='#1b1826';c.fillRect(-3000,563,7000,3000);
c.fillStyle='#8b7f3a';for(let x=-200;x<1300;x+=90)c.fillRect(x,586,46,4);
// lamp
c.fillStyle='#22202e';c.fillRect(982,380,5,140);c.fillRect(972,376,20,5);
c.fillStyle='rgba(255,230,150,.9)';dot(973,384,5);
c.globalCompositeOperation='lighter';c.fillStyle='rgba(255,220,130,.12)';dot(973,390,46);c.globalCompositeOperation='source-over';
if(fx.glow>.02&&W.phase==='open'){
c.globalCompositeOperation='lighter';c.fillStyle=hsl(rt*90,100,60,.2*fx.glow+.12*fx.unst);
c.beginPath();c.ellipse(660,570,330,16,0,0,TAU);c.fill();c.globalCompositeOperation='source-over';
}
if(ev.psy>.05){
const bt=(ev.t-T_DANCE)*BPS;
for(let i=0;i<16;i++){const on=(((i+Math.floor(bt))%2)+2)%2===0;c.fillStyle=hsl(ev.hue+i*22,100,on?55:28,.85*ev.psy);c.fillRect(140+i*48,566,46,34);}
}
}
/* ================= drawing: people ================= */
function liftOf(u){return u.state==='hover'?Math.min(1,u.hoverT*1.6)*28+Math.sin(u.hoverT*7+u.id)*3:0;}
function drawCup(x,y,sc,d){
c.save();c.translate(x,y);c.scale(sc,sc);
let hb=0;
if(d.uni){
hb=rt*140+d.seed*60;
c.globalCompositeOperation='lighter';
const g=c.createRadialGradient(0,-2,1,0,-2,24);g.addColorStop(0,hsl(hb,100,60,.55));g.addColorStop(1,hsl(hb+90,100,50,0));
c.fillStyle=g;dot(0,-2,24);c.globalCompositeOperation='source-over';
const cg=c.createLinearGradient(0,-6,0,7);cg.addColorStop(0,hsl(hb,100,62));cg.addColorStop(.5,hsl(hb+100,100,60));cg.addColorStop(1,hsl(hb+200,100,58));
c.fillStyle=cg;
}else c.fillStyle='#d9b78c';
c.beginPath();c.moveTo(-4.5,-5);c.lineTo(4.5,-5);c.lineTo(3.2,6);c.lineTo(-3.2,6);c.closePath();c.fill();
if(!d.uni){c.fillStyle='#7a4a2b';c.fillRect(-4,-1,8,4);}
c.fillStyle=d.uni?'#fff':'#fff8ea';c.beginPath();c.ellipse(0,-6,5,2.6,0,0,TAU);c.fill();
c.strokeStyle=d.uni?hsl(hb+180,100,60):'#ff5a8a';c.lineWidth=1.4;c.beginPath();c.moveTo(1,-7);c.lineTo(3,-13);c.stroke();
if(d.uni){c.fillStyle='#fff';spark4(6*Math.sin(rt*5+d.seed),-10,2.5+Math.sin(rt*9));}
c.restore();
}
function drawCustomer(u){
const R0=10;
let rot=u.rot||0,lift=0;
const panicked=W.phase!=='open'&&(u.state==='line'||u.state==='leaving'||u.state==='balk'||u.state==='balkwait');
if(u.state==='hover'){lift=liftOf(u);rot=Math.sin(u.hoverT*4+u.id)*.4;}
if(panicked){
if(W.phase==='event'&&ev.t>=T_DANCE&&ev.t<T_LAUNCH+.5)lift=Math.abs(Math.sin((ev.t-T_DANCE)*BPS*Math