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.
The counter is the bottleneck, and I keep it saturated on purpose. Customers arrive as a Poisson stream at 0.4/s. The counter can only manage about 0.26/s, so the line outside is always full and the overflow gives up. The barista takes U(2.5, 4.5) s per drink, a mean of 3.5 s. The next customer then needs about 0.3 s to shuffle 28 px up to the counter, so a drink leaves the counter roughly every 3.8 s.
After collecting a drink, a customer holds it inside for about 15.6 s:
The 8-person cap splits as ~4 holders, ~1 walking in and ~3 queued, so the counter never starves.
A catastrophe can only start at the instant a unicorn is handed over while at least 2 of the other drinks still inside are unicorns. The count only ever rises one drink at a time, so every event happens at exactly 3.
How many other drinks are inside at that moment?
So there are 3 others about 45% of the time, 4 about 50%, and 5 about 5%.
The chance that at least two of those are unicorns:
Weighted, that is 0.45·0.074 + 0.50·0.132 + 0.05·0.196 ≈ 0.109. Multiply by the 1/6 chance that the new drink is itself a unicorn and you get ≈ 0.018 per customer, or about 1 catastrophe per 55 customers.
After each rebuild the shop reopens empty. The first two drinks can't trigger anything and the next few have fewer neighbours, which adds a few customers, so call it ≈55–60. That is about 3.5 minutes of simulated time plus the 19 s disaster itself.
The gap is roughly geometric, so its standard deviation is about as large as its mean. Gaps of 5 and of 180 are both perfectly normal. For the same reason, the average number of unicorns inside while the shop is open should settle near 4.1 ÷ 6 ≈ 0.68 (Little's law).
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>STARBUCKLE — Neon Unicorn Frappuccino Promotion</title>
<style>
:root{--bg:#0d0e18;--panel:#171a2b;--line:#2a2e48;--ink:#ecebf5;--dim:#9491b3;--pink:#ff4fd8;--cyan:#4ff3ff}
*{box-sizing:border-box}
html,body{margin:0;background:var(--bg);color:var(--ink);font:13px/1.4 system-ui,-apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif}
#wrap{display:flex;gap:12px;padding:12px;align-items:flex-start;max-width:1560px;margin:0 auto}
#stage{flex:1;min-width:0}
#c{width:100%;height:auto;aspect-ratio:1000/620;display:block;border-radius:10px;background:#000;box-shadow:0 0 0 1px var(--line)}
#controls{display:flex;flex-wrap:wrap;gap:10px;align-items:center;margin-top:10px;background:var(--panel);padding:10px;border-radius:10px}
button{background:#262a44;color:var(--ink);border:1px solid #3a3f66;border-radius:7px;padding:7px 12px;font:inherit;cursor:pointer}
button:hover{background:#30355a}
button.on{background:var(--pink);color:#1a0016;border-color:var(--pink);font-weight:700}
input[type=range]{width:220px;accent-color:var(--pink);vertical-align:middle}
#spd{font-variant-numeric:tabular-nums;min-width:52px;display:inline-block;font-weight:700}
#panel{width:350px;flex:none;background:var(--panel);border-radius:10px;padding:12px 14px}
h1{font-size:15px;margin:0 0 6px;letter-spacing:.5px}
h2{font-size:11px;margin:14px 0 6px;color:var(--dim);text-transform:uppercase;letter-spacing:1px}
table{width:100%;border-collapse:collapse}
td{padding:3px 0;border-bottom:1px solid var(--line);vertical-align:top}
td:last-child{text-align:right;font-variant-numeric:tabular-nums;font-weight:600;padding-left:8px}
.sub{color:var(--dim);font-weight:400;font-size:11px}
#meter{display:flex;gap:8px;align-items:center;margin:4px 0 8px}
#meter i{width:22px;height:22px;border-radius:50%;background:#262a44;border:2px solid #3a3f66;display:block}
#meter i.lit{background:radial-gradient(circle,#fff,#ff4fd8 45%,#7a2cff);border-color:#fff;box-shadow:0 0 12px #ff4fd8}
#status{font-weight:800;margin-left:auto;text-align:right}
#chart{width:100%;height:auto;display:block;border-radius:6px;margin-top:4px}
#log{list-style:none;margin:0;padding:0;max-height:220px;overflow:auto;font-size:12px}
#log li{padding:3px 0;border-bottom:1px solid var(--line);color:#cfcde0}
#log li.u{color:#ff9cf0}
#log li.w{color:#ffcf7a}
#log li.x{color:#fff;font-weight:700;background:linear-gradient(90deg,#5a1650,transparent)}
#log li.r{color:#8ff}
.note{color:var(--dim);font-size:11px;margin-top:6px}
@media (max-width:1000px){#wrap{flex-direction:column}#panel{width:100%}}
</style>
</head>
<body>
<div id="wrap">
<div id="stage">
<canvas id="c" width="1000" height="620"></canvas>
<div id="controls">
<button id="pause">⏸ Pause</button>
<label>Speed <input id="speed" type="range" min="0" max="1000" value="347"></label>
<span id="spd">2.0×</span>
<button data-s="1">1×</button><button data-s="5">5×</button><button data-s="20">20×</button><button data-s="100">100×</button>
<button id="turbo">⏩ Fast-forward to next disaster</button>
</div>
</div>
<div id="panel">
<h1>★ STARBUCKLE · Neon Unicorn Promotion</h1>
<div id="meter"><span class="sub">Unicorns inside</span><i></i><i></i><i></i><b id="status">NOMINAL</b></div>
<table id="stats"></table>
<h2>Unicorn rate vs. the truth</h2>
<canvas id="chart" width="640" height="230"></canvas>
<div class="note">Pink: cumulative observed rate. Dashed: true 1/6. Cyan funnel: ±2σ for that many drinks. Pink verticals: catastrophes.</div>
<h2>Incident log</h2>
<ul id="log"></ul>
</div>
</div>
<script>
(()=>{
'use strict';
const W=1000,H=620;
const cv=document.getElementById('c'),ctx=cv.getContext('2d');
const DPR=Math.min(2,window.devicePixelRatio||1);
cv.width=W*DPR;cv.height=H*DPR;
// ================= TUNABLES (these, and only these, set the odds) =================
const P_UNICORN=1/6; // per-Frappuccino chance, rolled once at the counter
const MAX_INSIDE=8;
const ARRIVAL_RATE=0.4; // Poisson arrivals / sim-second (deliberately > counter capacity)
const OUT_LINE_MAX=15; // beyond this, arrivals give up
const SERVICE_MIN=2.5,SERVICE_MAX=4.5; // barista time per drink (s)
const LINGER_MIN=7,LINGER_MAX=13; // seated time with drink (s)
const WALK=90; // px / sim-second (±15% per customer)
const ENTRY_GAP=0.5;
const EVENT_LEN=19; // catastrophe + rebuild, sim-seconds
const EVENT_MAX_SPEED=2.5; // catastrophes are played back at most this fast
const DT=1/60,MAX_STEPS=2500,G=900;
const PRED_GAP=55,PRED_AVGU=0.68;
// ================= helpers =================
const rnd=(a,b)=>a+Math.random()*(b-a);
const pick=a=>a[(Math.random()*a.length)|0];
const clamp=(v,a,b)=>v<a?a:v>b?b:v;
const hsl=(h,s,l,a=1)=>`hsla(${(((h%360)+360)%360).toFixed(1)},${s}%,${l}%,${a})`;
const ease=p=>p<=0?0:p>=1?1:1-Math.pow(1-p,3);
function circ(x,y,r){ctx.beginPath();ctx.arc(x,y,Math.max(.1,r),0,Math.PI*2);}
function ell(x,y,rx,ry,rot=0){ctx.beginPath();ctx.ellipse(x,y,Math.max(.1,rx),Math.max(.1,ry),rot,0,Math.PI*2);}
function rrect(x,y,w,h,r){ctx.beginPath();ctx.moveTo(x+r,y);ctx.arcTo(x+w,y,x+w,y+h,r);ctx.arcTo(x+w,y+h,x,y+h,r);ctx.arcTo(x,y+h,x,y,r);ctx.arcTo(x,y,x+w,y,r);ctx.closePath();}
function star(x,y,r){ctx.beginPath();for(let i=0;i<10;i++){const a=-Math.PI/2+i*Math.PI/5,rr=i%2?r*.45:r;ctx.lineTo(x+Math.cos(a)*rr,y+Math.sin(a)*rr);}ctx.closePath();ctx.fill();}
function line(a,b,c,d){ctx.beginPath();ctx.moveTo(a,b);ctx.lineTo(c,d);ctx.stroke();}
function nEmit(rate,dt){const x=rate*dt,f=Math.floor(x);return f+(Math.random()<x-f?1:0);}
function glitch(s){let o='';for(const ch of s)o+=(ch!==' '&&Math.random()<.14)?pick(['✦','★','▓','#','?','U','∞']):ch;return o;}
// ================= geometry =================
const SHOP={x0:360,x1:950,wallTop:70,floorTop:140,floorBot:450};
const DOOR_X0=400,DOOR_X1=462,SIDEWALK_Y=490,EXIT_Y=514;
const outSlot=i=>({x:392-i*24,y:SIDEWALK_Y});
const QSLOTS=[[790,224],[762,224],[734,224],[706,224],[678,226],[652,232],[628,246],[606,262],[586,280]];
const qSlot=i=>{const s=QSLOTS[Math.min(i,QSLOTS.length-1)];return{x:s[0],y:s[1]};};
const TABLES=[[480,250],[470,370],[600,345],[735,320],[870,300],[715,415],[870,410]];
const SPOTS=[];TABLES.forEach(t=>{SPOTS.push({x:t[0]-24,y:t[1],occ:false});SPOTS.push({x:t[0]+24,y:t[1],occ:false});});
const LIGHTS=[[500,290],[680,290],[860,350],[560,410]];
const PLANTS=[[380,165],[930,428]];
const BALK=['nope.','that line? no.',"I'll make instant",'is it always like this?','lol no','see you never'];
const SCREAM=['AAAA','my frap!','worth it','WHEEE','I have a meeting','5 stars','again!','my ears are colors'];
const UNI_SAY=['✦ ooh ✦','it hums…','why is it warm','it is looking at me','sparkly!','is this safe?','it tastes purple'];
const CROWD=['OMG','click click','is that included?','10/10','I just wanted a latte','encore!','do they have oat milk'];
const QUIPS=['Corporate reminds you this was statistically inevitable.','The Unicorn does not accept reward points.','Three is a crowd. Three unicorns is a crater.',
'Please enjoy your complimentary structural failure.','No dice were loaded in the making of this disaster.','Your order will be ready in 4–6 business eternities.',
'The barista has achieved a state of grace.','Legal says the fine print was "technically visible".'];
const REBUILD_Q=['now with 30% more load-bearing optimism','reinforced with extra whipped cream','insurance adjuster weeping softly',
'same floor plan, fewer questions','drywall sourced from the previous drywall','under new (identical) management'];
// ================= state =================
const S={t:0,customers:[],outLine:[],cq:[],
barista:{state:'idle',cur:null,timer:0,dur:0,hidden:false},
open:true,entryCd:0,arrivalCd:1,ev:null,prevU:0,lvl2T:0,flash:0,shake:0,die:null,
st:{served:0,uni:0,norm:0,cat:0,since:0,gaps:[],catAt:[],balk:0,refund:0,air:0,orbit:0,bar:0,two:0,near:0,
expo:0,open:0,drought:0,maxDrought:0,streak:0,maxStreak:0},
hist:[],histStride:1};
let VT=0;
let nid=1;
function mk(x,y){return{id:nid++,x,y,z:0,vx:0,vy:0,vz:0,spin:0,face:Math.PI/2,hue:rnd(0,360),sat:rnd(25,60),light:rnd(45,68),r:rnd(7,9),
speed:WALK*rnd(.85,1.15),state:'outLine',wp:[],goal:{x,y},atGoal:false,walking:false,inside:false,drink:null,timer:0,spot:null,
bob:rnd(0,6.3),bubble:null,bubbleT:0,dead:false,isBarista:false};}
function setGoal(c,x,y){if(c.goal.x!==x||c.goal.y!==y){c.goal={x,y};c.atGoal=false;}}
function spawnArrival(){
if(S.outLine.length>=OUT_LINE_MAX){const c=mk(-20,526);c.state='balk';c.goal={x:rnd(40,230),y:526};S.customers.push(c);return;}
const c=mk(-20,SIDEWALK_Y);S.outLine.push(c);S.customers.push(c);
}
for(let i=0;i<8;i++){const s=outSlot(i),c=mk(s.x,s.y);S.outLine.push(c);S.customers.push(c);}
function unicornInside(){let n=0;for(const c of S.customers)if(c.inside&&c.drink==='U')n++;return n;}
function insideCount(){let n=0;for(const c of S.customers)if(c.inside)n++;return n;}
// ================= movement =================
function moveTo(c,tx,ty,sp,dt){const dx=tx-c.x,dy=ty-c.y,d=Math.hypot(dx,dy),s=sp*dt;if(d<.001)return true;
c.face=Math.atan2(dy,dx);if(d<=s){c.x=tx;c.y=ty;return true;}c.x+=dx/d*s;c.y+=dy/d*s;c.walking=true;return false;}
function walk(c,dt){c.walking=false;
if(c.wp.length){if(moveTo(c,c.wp[0].x,c.wp[0].y,c.speed,dt))c.wp.shift();c.atGoal=false;return false;}
c.atGoal=moveTo(c,c.goal.x,c.goal.y,c.speed,dt);return c.atGoal;}
function updC(c,dt){
if(c.bubbleT>0){c.bubbleT-=dt;if(c.bubbleT<=0)c.bubble=null;}
switch(c.state){
case 'outLine':case 'toQueue':case 'ordering':walk(c,dt);break;
case 'toSpot':if(walk(c,dt)){c.state='linger';c.timer=rnd(LINGER_MIN,LINGER_MAX);c.face=rnd(0,6.28);}break;
case 'linger':c.timer-=dt;if(c.timer<=0){if(c.spot)c.spot.occ=false;c.spot=null;c.state='toExit';
c.wp=[{x:445,y:428},{x:445,y:478}];c.goal={x:1060,y:EXIT_Y};}break;
case 'toExit':walk(c,dt);if(c.inside&&c.y>SHOP.floorBot+2)c.inside=false;if(c.x>1040)c.dead=true;break;
case 'balk':if(walk(c,dt)){c.state='balkWait';c.timer=1.3;c.bubble=pick(BALK);c.bubbleT=1.3;S.st.balk++;}break;
case 'balkWait':c.timer-=dt;if(c.timer<=0){c.state='balkLeave';c.goal={x:-40,y:c.y};}break;
case 'balkLeave':walk(c,dt);if(c.x<-30)c.dead=true;break;
case 'frozen':c.walking=false;break;
case 'ragdoll':rag(c,dt);break;
case 'flee':walk(c,dt);if(c.x<-30||c.x>1030)c.dead=true;break;
}
}
function rag(c,dt){
c.x+=c.vx*dt;c.y+=c.vy*dt;c.z+=c.vz*dt;c.vz-=G*dt;c.face+=c.spin*dt;c.walking=false;
if(c.y<135){c.y=135;c.vy=Math.abs(c.vy)*.5;}if(c.y>612){c.y=612;c.vy=-Math.abs(c.vy)*.5;}
if(c.z<=0){c.z=0;
if(c.vz<-90){c.vz=-c.vz*.42;c.vx*=.6;c.vy*=.6;c.spin*=.6;}
else{c.vz=0;c.state='flee';c.speed=rnd(170,240);c.wp=[];c.goal={x:c.x<655?-50:1050,y:clamp(c.y+rnd(-30,30),470,605)};
c.bubble=pick(SCREAM);c.bubbleT=1.6;}}
}
// ================= counter & THE DIE =================
function serviceStep(dt){
const b=S.barista;
if(b.state==='idle'){
const h=S.cq[0];
if(h&&h.state==='toQueue'&&h.atGoal){b.state='making';b.cur=h;b.dur=b.timer=rnd(SERVICE_MIN,SERVICE_MAX);h.state='ordering';h.face=-Math.PI/2;}
}else if(b.state==='making'){
b.timer-=dt;
if(b.timer<=0){const c=b.cur;b.state='idle';b.cur=null;serve(c);}
}
}
function serve(c){
S.cq.shift();
// ====== THE ONLY ROLL THAT DECIDES ANYTHING. One independent 1-in-6 per Frappuccino, here, at the counter. ======
const unicorn=Math.random()<P_UNICORN;
c.drink=unicorn?'U':'N';
// cosmetic die shown above the counter: a unicorn shows 6, a plain drink shows 1-5 (chosen AFTER the roll)
S.die={face:unicorn?6:1+((Math.random()*5)|0),t:1.0,uni:unicorn};
const st=S.st;st.served++;st.since++;
if(unicorn){st.uni++;st.streak++;st.maxStreak=Math.max(st.maxStreak,st.streak);st.drought=0;}
else{st.norm++;st.drought++;st.maxDrought=Math.max(st.maxDrought,st.drought);st.streak=0;}
pushHist();
const free=SPOTS.filter(s=>!s.occ);const sp=free.length?pick(free):{x:600,y:380,occ:false};
sp.occ=true;c.spot=sp;c.state='toSpot';c.wp=[];c.goal={x:sp.x,y:sp.y};c.atGoal=false;
if(unicorn){
const u=unicornInside();
c.bubble=pick(UNI_SAY);c.bubbleT=1.8;
burst(790,200,30,null);
log(`${ts()} #${c.id} got a Neon Unicorn — ${u} inside`,'u');
if(u>=3)startEvent();
}else burst(790,200,4,40);
}
function pushHist(){const st=S.st;if(st.served%S.histStride===0){S.hist.push([st.served,st.uni/st.served]);
if(S.hist.length>700){S.hist=S.hist.filter((_,i)=>i%2===0);S.histStride*=2;}}}
// ================= catastrophe =================
function startEvent(){
const st=S.st;st.cat++;st.gaps.push(st.since);st.catAt.push(st.served);
const holders=S.customers.filter(c=>c.inside&&c.drink==='U');
const variant=st.cat===1?'explode':pick(['explode','launch','melt']);
S.ev={t:0,n:st.cat,variant,holders,flung:false,debris:[],quip:pick(QUIPS),rq:pick(REBUILD_Q),gap:st.since,lastBeat:-1};
st.since=0;
const b=S.barista;b.state='idle';b.cur=null;
S.open=false;
for(const c of S.customers)if(c.inside){c.state='frozen';c.wp=[];c.walking=false;}
S.flash=.8;
log(`${ts()} ☢ NEON UNICORN EVENT #${st.cat} after ${S.ev.gap} customers`,'x');
if(turbo)stopTurbo();
}
function flingAll(e){
const cx=655,cy=300,ex=e.variant==='explode';
const vict=S.customers.filter(c=>c.inside);
S.barista.hidden=true;
const bc=mk(800,158);Object.assign(bc,{isBarista:true,hue:150,sat:55,light:30,r:9});S.customers.push(bc);vict.push(bc);S.st.bar++;
for(const c of vict){
c.inside=false;if(!c.drink&&!c.isBarista)S.st.refund++;
if(c.spot){c.spot.occ=false;c.spot=null;}
c.state='ragdoll';c.wp=[];
const a=Math.atan2(c.y-cy,c.x-cx)+rnd(-.6,.6),sp=rnd(140,380)*(ex?1:.55);
c.vx=Math.cos(a)*sp;c.vy=Math.sin(a)*sp*.6;
c.vz=ex?((c.drink==='U'||c.isBarista)?rnd(1050,1450):rnd(380,1000)):rnd(160,480);
c.spin=rnd(-14,14);
if(!c.isBarista){S.st.air++;if(c.vz*c.vz/(2*G)>600)S.st.orbit++;}
c.bubble=null;
}
S.cq.length=0;SPOTS.forEach(s=>s.occ=false);
if(ex){spawnDebris(e);S.shake=26;S.flash=1;boom(655,300);}
else if(e.variant==='launch'){S.shake=14;S.flash=.6;}
else{S.shake=8;S.flash=.5;}
}
function spawnDebris(e){
const{x0,x1,floorTop,floorBot}=SHOP,cx=655,cy=300;
const add=(x,y,w,h,col,z,shape)=>{const a=Math.atan2(y-cy,x-cx)+rnd(-.4,.4),sp=rnd(120,420);
e.debris.push({x,y,z:z||0,w,h,col,shape:shape||'r',vx:Math.cos(a)*sp,vy:Math.sin(a)*sp*.7,vz:rnd(350,1000),a:rnd(0,6.3),va:rnd(-9,9),rest:false});};
for(let x=x0;x<x1;x+=42)add(x+21,floorTop-4,42,34,hsl(158,48,rnd(18,26)),30);
for(let y=floorTop+20;y<floorBot;y+=62)for(let x=x0+30;x<x1;x+=74)add(x+rnd(-10,10),y+rnd(-10,10),rnd(24,40),rnd(20,34),hsl(33,38,rnd(62,74)));
for(let x=620;x<940;x+=40)add(x+20,192,40,28,hsl(20,35,22));
TABLES.forEach(t=>add(t[0],t[1],36,36,hsl(25,40,40),0,'c'));
for(let x=x0;x<x1;x+=50)add(x+25,floorBot+6,50,12,hsl(158,40,20));
add(730,172,44,18,'#b9c0cc');add(520,105,170,26,'#f3ead7',40,'s');
}
function eventStep(dt){
const e=S.ev;e.t+=dt;const t=e.t;
if(t<2.5)for(const c of e.holders)c.z=Math.min(45,t*30);
if(!e.flung&&t>=2.5){e.flung=true;flingAll(e);}
for(const d of e.debris){if(d.rest)continue;d.x+=d.vx*dt;d.y+=d.vy*dt;d.z+=d.vz*dt;d.vz-=G*dt;d.a+=d.va*dt;
if(d.z<=0){d.z=0;if(d.vz<-80){d.vz=-d.vz*.35;d.vx*=.5;d.vy*=.5;d.va*=.5;}else{d.vz=0;d.rest=true;}}
d.y=clamp(d.y,135,612);}
if(t>=EVENT_LEN)endEvent();
}
function endEvent(){
S.ev=null;S.open=true;S.barista.hidden=false;S.barista.state='idle';S.barista.cur=null;S.prevU=0;S.lvl2T=0;S.entryCd=.5;
log(`${ts()} Shop rebuilt. Doors open. Nobody has learned anything.`,'r');
}
// ================= sim step =================
function step(dt){
S.t+=dt;
S.arrivalCd-=dt;
while(S.arrivalCd<=0){S.arrivalCd+=-Math.log(1-Math.random())/ARRIVAL_RATE;spawnArrival();}
S.outLine.forEach((c,i)=>{const s=outSlot(i);setGoal(c,s.x,s.y);});
S.cq.forEach((c,i)=>{const s=qSlot(i);setGoal(c,s.x,s.y);});
if(S.open){
S.entryCd-=dt;const h=S.outLine[0];
if(h&&S.entryCd<=0&&h.atGoal&&insideCount()<MAX_INSIDE){
S.outLine.shift();h.state='toQueue';h.inside=true;h.wp=[{x:425,y:474},{x:425,y:428}];h.atGoal=false;
S.cq.push(h);const q=qSlot(S.cq.length-1);h.goal={x:q.x,y:q.y};S.entryCd=ENTRY_GAP;}
serviceStep(dt);
}
for(const c of S.customers)updC(c,dt);
if(S.customers.some(c=>c.dead))S.customers=S.customers.filter(c=>!c.dead);
if(S.ev)eventStep(dt);
else{
const u=unicornInside();
if(u===2&&S.prevU<2){S.st.two++;log(`${ts()} ⚠ Two Neon Unicorns in the room. Lights flickering.`,'w');}
if(u<2&&S.prevU===2)S.st.near++;
S.prevU=u;S.lvl2T=u===2?S.lvl2T+dt:0;S.st.open+=dt;S.st.expo+=u*dt;
}
}
// ================= particles =================
const parts=[];
function addP(o){if(parts.length<2400)parts.push(o);}
function sp(x,y,vx,vy,life,h,size,k='s',g=0){addP({x,y,vx,vy,life,max:life,h,size,k,g,a:Math.random()*6,va:rnd(-8,8),grow:0});}
function ring(x,y,life,h,grow,w){addP({x,y,vx:0,vy:0,life,max:life,h,size:6,k:'r',g:0,a:0,va:0,grow,w:w||4});}
function burst(x,y,n,h){for(let i=0;i<n;i++){const a=rnd(0,6.283),s=rnd(40,180);sp(x,y,Math.cos(a)*s,Math.sin(a)*s,rnd(.4,1),h==null?rnd(0,360):h,rnd(1.5,3.5));}}
function boom(x,y){for(let i=0;i<420;i++){const a=rnd(0,6.283),s=rnd(80,900);
sp(x,y,Math.cos(a)*s,Math.sin(a)*s*.7,rnd(.6,2.2),rnd(0,360),rnd(2,6),Math.random()<.5?'s':'c',300);}
ring(x,y,1.2,300,900,10);ring(x,y,1.7,180,700,6);ring(x,y,.8,60,500,14);}
const cars=[];
function uniPos(e,t){const rise=ease((t-4.5)/1.6),dep=t>13?ease((t-13)/1.5):0;
return{x:655+Math.sin((t-4.5)*.9)*110*rise,y:395+(1-rise)*80-dep*520,sc:(1.05+Math.min(.25,(e.n-1)*.06))*(.25+.75*rise)*(1-dep*.85),rise,dep};}
function visuals(dt){
S.flash=Math.max(0,S.flash-dt*1.6);S.shake=Math.max(0,S.shake-dt*14);
if(S.die){S.die.t-=dt;if(S.die.t<=0)S.die=null;}
for(let i=parts.length-1;i>=0;i--){const p=parts[i];p.life-=dt;if(p.life<=0){parts[i]=parts[parts.length-1];parts.pop();continue;}
const drag=p.k==='s'?Math.pow(.35,dt):1;p.vx*=drag;p.vy*=drag;p.vy+=p.g*dt;p.x+=p.vx*dt;p.y+=p.vy*dt;p.a+=p.va*dt;}
if(Math.random()<dt*.35)cars.push({x:-90,y:566,vx:rnd(120,200),col:hsl(rnd(0,360),55,50),len:rnd(50,70)});
if(Math.random()<dt*.35)cars.push({x:W+90,y:600,vx:-rnd(120,200),col:hsl(rnd(0,360),55,50),len:rnd(50,70)});
for(let i=cars.length-1;i>=0;i--){cars[i].x+=cars[i].vx*dt;if(cars[i].x<-120||cars[i].x>W+120)cars.splice(i,1);}
if(dt<=0)return;
if(!S.ev){
const u=unicornInside();
if(u>=1)for(const c of S.customers)if(c.inside&&c.drink==='U'){const k=nEmit(u>=2?22:3,dt);
for(let i=0;i<k;i++)sp(c.x+rnd(-4,4),c.y-c.z-8,rnd(-30,30),rnd(-70,-20),rnd(.4,.9),rnd(0,360),rnd(1,2.5));}
if(u>=2){const k=nEmit(25+S.lvl2T*3,dt);for(let i=0;i<k;i++)sp(rnd(SHOP.x0,SHOP.x1),rnd(SHOP.floorTop,SHOP.floorBot),0,rnd(-20,-5),rnd(.3,.8),rnd(0,360),rnd(1,2));}
return;
}
const e=S.ev,t=e.t;
if(t<2.5&&e.holders.length){
let cx=0,cy=0;e.holders.forEach(c=>{cx+=c.x;cy+=c.y-c.z;});cx/=e.holders.length;cy/=e.holders.length;
const k=nEmit(170,dt);for(let i=0;i<k;i++){const a=rnd(0,6.283),r=rnd(200,420),x=cx+Math.cos(a)*r,y=cy+Math.sin(a)*r*.7;
sp(x,y,(cx-x)/.7,(cy-y)/.7,.7,rnd(0,360),rnd(1.5,3));}
}
if(e.variant==='launch'&&t>=2.5&&t<6){const dy=-Math.pow(t-2.5,2.3)*70,k=nEmit(320,dt);
for(let i=0;i<k;i++)sp(rnd(SHOP.x0,SHOP.x1),SHOP.floorBot+dy+14,rnd(-40,40),rnd(250,520),rnd(.3,.7),rnd(5,50),rnd(3,7),'f');}
if(e.variant==='melt'&&t>=2.5&&t<5){const k=nEmit(110,dt);
for(let i=0;i<k;i++)sp(rnd(SHOP.x0,SHOP.x1),rnd(SHOP.floorTop,SHOP.floorBot),rnd(-10,10),rnd(60,160),rnd(.6,1.2),rnd(270,340),rnd(3,6),'s',200);}
if(t>=4.5&&t<14.5){
const up=uniPos(e,t);
let k=nEmit(70,dt);for(let i=0;i<k;i++)sp(rnd(0,W),-10,rnd(-40,40),rnd(60,160),rnd(3,5),rnd(0,360),rnd(2,4),'c',40);
k=nEmit(4,dt);for(let i=0;i<k;i++)sp(up.x+rnd(-120,120),up.y-rnd(150,300)*up.sc,rnd(-30,30),rnd(-90,-40),2,rnd(0,360),22,'n');
const b=Math.floor(t*2);if(b!==e.lastBeat){e.lastBeat=b;ring(up.x,up.y,.9,VT*100,260,5);S.shake=Math.max(S.shake,4);}
if(Math.random()<dt*1.5&&S.outLine.length){const c=pick(S.outLine);c.bubble=pick(CROWD);c.bubbleT=1.4;}
}
if(t>=14.5&&t<18.5){const p=ease((t-14.5)/4),top=SHOP.wallTop-12,bot=SHOP.floorBot+20,yc=bot-(bot-top)*p;
const k=nEmit(60,dt);for(let i=0;i<k;i++)sp(rnd(SHOP.x0,SHOP.x1),yc,rnd(-80,80),rnd(-120,-20),rnd(.2,.5),rnd(30,55),rnd(1,2.5),'s',400);}
}
// ================= background =================
const bg=document.createElement('canvas');bg.width=W*DPR;bg.height=H*DPR;
(function(){const g=bg.getContext('2d');g.setTransform(DPR,0,0,DPR,0,0);
const gr=g.createLinearGradient(0,0,0,140);gr.addColorStop(0,'#0f1230');gr.addColorStop(1,'#43306a');g.fillStyle=gr;g.fillRect(0,0,W,140);
for(let i=0;i<70;i++){g.fillStyle=`rgba(255,255,255,${rnd(.2,.8)})`;g.fillRect(rnd(0,W),rnd(0,90),1.5,1.5);}
let x=0;while(x<W){const w=rnd(40,90),h=rnd(30,105);g.fillStyle=hsl(250,25,rnd(12,20));g.fillRect(x,135-h,w,h+5);
g.fillStyle='rgba(255,220,150,.5)';for(let yy=135-h+6;yy<128;yy+=10)for(let xx=x+5;xx<x+w-6;xx+=9)if(Math.random()<.35)g.fillRect(xx,yy,4,5);
x+=w+rnd(2,10);}
g.fillStyle='#283039';g.fillRect(0,132,W,328);
g.strokeStyle='rgba(255,255,255,.04)';g.lineWidth=1;for(let y=140;y<458;y+=24){g.beginPath();g.moveTo(0,y);g.lineTo(W,y);g.stroke();}
for(const[tx,ty]of[[80,220],[200,330],[120,400],[260,190],[40,300]]){g.fillStyle='rgba(0,0,0,.25)';g.beginPath();g.ellipse(tx,ty+14,26,9,0,0,6.283);g.fill();
g.fillStyle='#2f5d3a';g.beginPath();g.arc(tx,ty,24,0,6.283);g.fill();g.fillStyle='#3d7a4a';g.beginPath();g.arc(tx-6,ty-6,14,0,6.283);g.fill();}
g.fillStyle='#8e9099';g.fillRect(0,458,W,82);
g.fillStyle='rgba(0,0,0,.08)';for(let xx=0;xx<W;xx+=40)g.fillRect(xx,458,2,82);
g.fillStyle='#6b6d75';g.fillRect(0,536,W,5);
g.fillStyle='#26262d';g.fillRect(0,541,W,79);
g.fillStyle='#d8c44a';for(let xx=10;xx<W;xx+=60)g.fillRect(xx,580,32,3);
})();
// ================= drawing: shop =================
function drawShopAll(o){drawShopBase(o);drawBarista(o);drawCounter(o);}
function drawShopBase(o){
const{x0,x1,wallTop,floorTop,floorBot}=SHOP,lvl=o.lvl||0,hs=o.hs||0;
ctx.fillStyle=hsl(33+hs,38,72);ctx.fillRect(x0,floorTop,x1-x0,floorBot-floorTop);
ctx.fillStyle=hsl(33+hs,34,66);
for(let j=0,y=floorTop;y<floorBot;y+=30,j++)for(let i=0,x=x0;x<x1;x+=30,i++){if((i+j)&1)continue;ctx.fillRect(x,y,Math.min(30,x1-x),Math.min(30,floorBot-y));}
if(lvl>=1){ctx.fillStyle=hsl(VT*160,100,62,.85);ctx.fillRect(x0+240,floorTop+180,30,30);}
ctx.save();ctx.globalCompositeOperation='lighter';
LIGHTS.forEach((p,i)=>{let col='rgb(255,200,120)',a=.13;
if(lvl>=2){if(Math.random()<.35)return;col=hsl(VT*200+i*90,100,55);a=.4;}
const g=ctx.createRadialGradient(p[0],p[1],0,p[0],p[1],95);g.addColorStop(0,col);g.addColorStop(1,'rgba(0,0,0,0)');
ctx.globalAlpha=a;ctx.fillStyle=g;ctx.fillRect(p[0]-95,p[1]-95,190,190);});
ctx.restore();
TABLES.forEach((t,i)=>{const lev=lvl>=2?6+Math.sin(VT*3+i)*4:0;
ctx.fillStyle='rgba(0,0,0,.18)';ell(t[0],t[1]+5,18,7);ctx.fill();
ctx.fillStyle=hsl(25+hs,40,40);circ(t[0],t[1]-lev,17);ctx.fill();ctx.fillStyle=hsl(25+hs,40,48);circ(t[0]-4,t[1]-lev-4,9);ctx.fill();});
PLANTS.forEach((p,i)=>{const wrong=(lvl>=1&&i===0)||lvl>=2;ctx.fillStyle='#6b4630';circ(p[0],p[1],10);ctx.fill();
for(let k=0;k<6;k++){const a=k*1.047+(lvl>=2?VT*2:0);ctx.fillStyle=wrong?hsl(300+k*10+VT*30,90,60):hsl(130,45,35+k*2);circ(p[0]+Math.cos(a)*9,p[1]+Math.sin(a)*9-3,7);ctx.fill();}});
// back wall
ctx.fillStyle=hsl(158+hs,48,22);ctx.fillRect(x0,wallTop,x1-x0,floorTop-wallTop);
ctx.fillStyle=hsl(158+hs,40,14);ctx.fillRect(x0,floorTop-6,x1-x0,6);
// clock (runs backward at level >= 1)
const ccx=384,ccy=96;ctx.fillStyle='#f4efe4';circ(ccx,ccy,12);ctx.fill();ctx.strokeStyle='#222';ctx.lineWidth=1.5;ctx.stroke();
const dir=lvl>=1?-1:1,ang=dir*S.t*(Math.PI*2/60);
ctx.lineWidth=1.5;line(ccx,ccy,ccx+Math.sin(ang)*9,ccy-Math.cos(ang)*9);ctx.lineWidth=2.5;line(ccx,ccy,ccx+Math.sin(ang/12)*6,ccy-Math.cos(ang/12)*6);
// logo
ctx.fillStyle='#f3ead7';circ(425,104,20);ctx.fill();ctx.fillStyle=hsl(158+hs,48,22);circ(425,104,16);ctx.fill();
ctx.fillStyle=lvl>=1?hsl(VT*120,100,70):'#f3ead7';star(425,104,10);
ctx.fillStyle='#f3ead7';ctx.textAlign='left';ctx.textBaseline='middle';ctx.font='bold 18px system-ui,sans-serif';
ctx.fillText(lvl>=2?glitch('STARBUCKLE'):'STARBUCKLE',452,98);
ctx.font='10px system-ui,sans-serif';ctx.fillText('COFFEE · EST. RECENTLY',454,118);
// incident board
ctx.fillStyle='#111';rrect(604,78,92,54,4);ctx.fill();
ctx.fillStyle='#bbb';ctx.font='8px system-ui,sans-serif';ctx.textAlign='center';ctx.fillText('CUSTOMERS SINCE',650,88);ctx.fillText('LAST INCIDENT',650,98);
const sl=S.st.since;ctx.font='bold 20px ui-monospace,monospace';ctx.fillStyle=sl<10?'#ff5d5d':sl<40?'#ffd24a':'#7dff9a';ctx.fillText(String(sl),650,117);
// menu
ctx.fillStyle='#1a1a1a';rrect(704,78,236,54,4);ctx.fill();
ctx.fillStyle='#e9e2cf';ctx.font='10px ui-monospace,monospace';ctx.textAlign='left';
const L=['FRAPPUCCINO ........ $5.45','NEON UNICORN* ..... 1 in 6','*not liable for events'];
L.forEach((s,i)=>{ctx.fillStyle=i===1?(lvl>=1?hsl(VT*180,100,70):'#ff9cf0'):'#e9e2cf';ctx.fillText(lvl>=2?glitch(s):s,712,90+i*15);});
// walls
ctx.fillStyle='#14231d';
ctx.fillRect(x0-7,wallTop,7,floorBot-wallTop+9);ctx.fillRect(x1,wallTop,7,floorBot-wallTop+9);
ctx.fillRect(x0-7,floorBot,DOOR_X0-x0+7,9);ctx.fillRect(DOOR_X1,floorBot,x1-DOOR_X1+7,9);
ctx.fillStyle='rgba(160,220,255,.55)';for(let x=480;x<920;x+=90)ctx.fillRect(x,floorBot+2,60,5);
for(let x=x0-7,k=0;x<x1+7;x+=14,k++){if(x+14>DOOR_X0&&x<DOOR_X1)continue;ctx.fillStyle=k%2?'#f3ead7':hsl(158+hs,50,28);ctx.fillRect(x,floorBot+9,14,8);}
ctx.fillStyle='#5a3b2a';ctx.fillRect(DOOR_X0+4,floorBot-10,DOOR_X1-DOOR_X0-8,10);
if(lvl>=1){ctx.save();ctx.lineWidth=lvl>=2?4:2;ctx.strokeStyle=hsl(VT*(lvl>=2?240:40)+300,100,60,lvl>=2?.9:.45);
ctx.shadowColor=ctx.strokeStyle;ctx.shadowBlur=lvl>=2?26:12;ctx.strokeRect(x0-10,wallTop-3,x1-x0+20,floorBot-wallTop+23);ctx.restore();}
}
function drawBarista(o){
const b=S.barista;if(b.hidden)return;
const mk_=b.state==='making',x=800+(mk_?Math.sin(VT*40)*1.2:0),y=158;
ctx.fillStyle='rgba(0,0,0,.2)';ell(800,y+5,9,4);ctx.fill();
ctx.fillStyle=hsl(150,55,30);circ(x,y,9);ctx.fill();ctx.fillStyle='#fff';ctx.fillRect(x-4,y+1,8,5);
ctx.fillStyle='#111';circ(x-3,y+3,1.4);ctx.fill();circ(x+3,y+3,1.4);ctx.fill();
if(mk_){ctx.strokeStyle='#fff';ctx.lineWidth=2;ctx.beginPath();ctx.arc(x+18,y-6,6,-Math.PI/2,-Math.PI/2+Math.PI*2*(1-b.timer/b.dur));ctx.stroke();
drawDie(x-22,y-8,11,1+((VT*14)|0)%6,false,true);}
if(S.die)drawDie(790,150-(1-S.die.t)*14,13,S.die.face,S.die.uni,false,S.die.t);
}
function drawDie(x,y,s,face,uni,tumble,alpha){
ctx.save();ctx.globalAlpha=alpha==null?1:clamp(alpha*1.5,0,1);ctx.translate(x,y);if(tumble)ctx.rotate(VT*9);
if(uni){ctx.shadowBlur=14;ctx.shadowColor=hsl(VT*300,100,60);ctx.fillStyle=hsl(VT*300,100,70);}else ctx.fillStyle='#fafafa';
rrect(-s/2,-s/2,s,s,s*.2);ctx.fill();ctx.shadowBlur=0;ctx.fillStyle=uni?'#fff':'#222';
const q=s*.27,P={1:[[0,0]],2:[[-q,-q],[q,q]],3:[[-q,-q],[0,0],[q,q]],4:[[-q,-q],[q,-q],[-q,q],[q,q]],5:[[-q,-q],[q,-q],[0,0],[-q,q],[q,q]],6:[[-q,-q],[q,-q],[-q,0],[q,0],[-q,q],[q,q]]};
for(const[a,b]of P[face])circ(a,b,s*.09),ctx.fill();ctx.restore();
}
function drawCounter(o){
const hs=o.hs||0,lvl=o.lvl||0;
ctx.fillStyle=hsl(22+hs,38,24);ctx.fillRect(620,178,320,30);ctx.fillStyle=hsl(22+hs,30,36);ctx.fillRect(620,176,320,6);
ctx.fillStyle='rgba(200,235,255,.5)';ctx.fillRect(628,182,70,22);
for(let i=0;i<4;i++){ctx.fillStyle=hsl(35+i*12,60,55);circ(640+i*16,194,5);ctx.fill();}
ctx.fillStyle='#c3c9d3';ctx.fillRect(708,164,44,16);ctx.fillStyle='#555';ctx.fillRect(716,172,6,8);ctx.fillRect(736,172,6,8);
ctx.fillStyle='#333';ctx.fillRect(772,166,22,12);
ctx.fillStyle=lvl>=2?hsl(VT*300,100,60):'rgba(220,240,255,.75)';ctx.fillRect(880,158,14,22);ctx.fillStyle='#444';ctx.fillRect(878,178,18,4);
ctx.fillStyle='#f3ead7';ctx.font='bold 8px system-ui,sans-serif';ctx.textAlign='center';ctx.fillText('ORDER · PICK UP',790,198);
}
function drawCars(){for(const c of cars){ctx.fillStyle='rgba(0,0,0,.25)';ctx.fillRect(c.x-c.len/2+3,c.y+9,c.len,6);
ctx.fillStyle=c.col;rrect(c.x-c.len/2,c.y-12,c.len,24,6);ctx.fill();ctx.fillStyle='rgba(20,30,50,.7)';ctx.fillRect(c.x-c.len*.22,c.y-9,c.len*.44,18);
ctx.fillStyle='#ffe9a0';const fx=c.vx>0?c.x+c.len/2-3:c.x-c.len/2;ctx.fillRect(fx,c.y-9,3,5);ctx.fillRect(fx,c.y+4,3,5);}}
function drawAFrame(u){ctx.fillStyle='#2b1f18';ctx.fillRect(552,462,44,26);ctx.fillStyle='#111';ctx.fillRect(555,465,38,20);
ctx.fillStyle=u>=1||S.ev?hsl(VT*200,100,70):'#ff9cf0';ctx.font='bold 8px system-ui,sans-serif';ctx.textAlign='center';ctx.textBaseline='middle';
ctx.fillText('NEON',574,470);ctx.fillText('1 in 6!',574,480);}
// ================= drawing: customers =================
function drawCup(x,y,uni,id){
if(uni){ctx.save();const h=VT*200+id*50;ctx.shadowBlur=12;ctx.shadowColor=hsl(h,100,60);
const g=ctx.createLinearGradient(x,y-6,x,y+5);g.addColorStop(0,hsl(h,100,65));g.addColorStop(.5,hsl(h+120,100,60));g.addColorStop(1,hsl(h+240,100,60));
ctx.fillStyle=g;ctx.fillRect(x-3,y-5,6,10);ctx.fillStyle='#fff';circ(x,y-6.5,3.3);ctx.fill();
ctx.strokeStyle=hsl(h+60,100,75);ctx.lineWidth=1.2;line(x+1,y-8,x+3,y-13);ctx.restore();}
else{ctx.fillStyle='#e8d6b5';ctx.fillRect(x-3,y-5,6,10);ctx.fillStyle='#fff';circ(x,y-6.5,3);ctx.fill();
ctx.fillStyle='#1e5c45';ctx.fillRect(x-3,y-1,6,3);ctx.strokeStyle='#2e8b57';ctx.lineWidth=1.2;line(x+1,y-8,x+3,y-12);}
}
function bubble(x,y,txt,hot){ctx.save();ctx.font='bold 10px system-ui,sans-serif';const w=ctx.measureText(txt).width+10;
ctx.fillStyle=hot?'#ffe6fb':'rgba(255,255,255,.92)';rrect(x-w/2,y-16,w,14,5);ctx.fill();
ctx.beginPath();ctx.moveTo(x-3,y-2);ctx.lineTo(x,y+3);ctx.lineTo(x+3,y-2);ctx.fill();
ctx.fillStyle='#222';ctx.textAlign='center';ctx.textBaseline='middle';ctx.fillText(txt,x,y-9);ctx.restore();}
function drawC(c,u){
let z=c.z;const bob=c.walking?Math.abs(Math.sin(VT*11+c.bob))*2.2:0;
if(S.ev&&c.state==='outLine')z+=Math.abs(Math.sin(VT*8+c.bob))*9;
const x=c.x,y=c.y-z-bob,r=c.r;
ctx.fillStyle=`rgba(0,0,0,${(.28/(1+z/60)).toFixed(3)})`;ell(c.x,c.y+r*.55,r*(1+z/300),r*.42);ctx.fill();
if(c.drink==='U'&&c.inside&&u>=1){ctx.save();ctx.globalCompositeOperation='lighter';
ctx.fillStyle=hsl(VT*120+c.id*30,100,60,.35);circ(x,y,r+7+Math.sin(VT*6+c.bob)*2+(u>=2?4:0));ctx.fill();ctx.restore();}
let h=c.hue;if(!S.ev&&u>=2&&c.inside)h+=Math.sin(VT*2+c.id)*60;
ctx.fillStyle=hsl(h,c.sat,c.light);circ(x,y,r);ctx.fill();ctx.lineWidth=1.5;ctx.strokeStyle=hsl(h,c.sat,c.light-25);ctx.stroke();
if(c.isBarista){ctx.fillStyle='#fff';ctx.fillRect(x-4,y+1,8,5);}
const f=c.face;ctx.fillStyle='#161616';for(const s of[-.55,.55]){circ(x+Math.cos(f+s)*r*.55,y+Math.sin(f+s)*r*.55,1.5);ctx.fill();}
if(c.drink)drawCup(x+r*.95,y-2,c.drink==='U',c.id);
if(c.bubble)bubble(x,y-r-6,c.bubble,c.drink==='U');
}
function bolt(ax,ay,bx,by,col,w){ctx.beginPath();ctx.moveTo(ax,ay);const nx=-(by-ay),ny=bx-ax,L=Math.hypot(nx,ny)||1;
for(let i=1;i<10;i++){const p=i/10,o=rnd(-14,14);ctx.lineTo(ax+(bx-ax)*p+nx/L*o,ay+(by-ay)*p+ny/L*o);}
ctx.lineTo(bx,by);ctx.strokeStyle=col;ctx.lineWidth=w;ctx.stroke();}
function drawArcs(){const hs=S.customers.filter(c=>c.inside&&c.drink==='U');if(hs.length<2)return;
ctx.save();ctx.globalCompositeOperation='lighter';ctx.shadowBlur=14;
for(let i=0;i<hs.length;i++)for(let j=i+1;j<hs.length;j++){if(Math.random()<.35)continue;const a=hs[i],b=hs[j],col=hsl(VT*250+i*90,100,70);
ctx.shadowColor=col;bolt(a.x,a.y-a.z-4,b.x,b.y-b.z-4,col,1.5+Math.min(3,S.lvl2T*.2));}
ctx.restore();}
function drawResonance(){const e=S.ev,t=e.t,hs=e.holders;if(hs.length<2)return;
const pts=hs.map(c=>[c.x,c.y-c.z]);let cx=0,cy=0;pts.forEach(p=>{cx+=p[0];cy+=p[1];});cx/=pts.length;cy/=pts.length;
ctx.save();ctx.globalCompositeOperation='lighter';
ctx.beginPath();pts.forEach((p,i)=>i?ctx.lineTo(p[0],p[1]):ctx.moveTo(p[0],p[1]));ctx.closePath();ctx.fillStyle=hsl(VT*300,100,60,.12+t*.08);ctx.fill();
for(let i=0;i<pts.length;i++){const a=pts[i],b=pts[(i+1)%pts.length],col=hsl(VT*300+i*120,100,70);ctx.shadowBlur=20;ctx.shadowColor=col;bolt(a[0],a[1],b[0],b[1],col,2+t*1.5);}
const R=(t%.5)/.5*140;ctx.shadowBlur=0;ctx.strokeStyle=hsl(VT*300,100,70,1-R/140);ctx.lineWidth=3;circ(cx,cy,R);ctx.stroke();
ctx.restore();}
// ================= drawing: event =================
function drawCrater(e,a,t){
ctx.save();ctx.globalAlpha=a;const cx=655,cy=300,rx=300,ry=168,melt=e.variant==='melt';
ell(cx,cy,rx,ry);ctx.fillStyle=melt?hsl(300+Math.sin(VT)*30,80,25):'#0c0612';ctx.fill();
const dance=t>=4.5&&t<14.5;
if(dance||melt){ctx.save();ell(cx,cy,rx-6,ry-6);ctx.clip();const beat=Math.floor(t*2);
for(let i=0;i<16;i++)for(let j=0;j<9;j++){const on=((i*7+j*3+beat)%5)===0||((i+j+beat)%4===0);if(!on)continue;
ctx.fillStyle=hsl((i+j)*30+VT*120,100,55,dance?.55:.22);ctx.fillRect(cx-rx+i*38,cy-ry+j*38,36,36);}ctx.restore();}
ctx.lineWidth=6;ctx.strokeStyle=hsl(VT*150,100,60);ctx.shadowColor=ctx.strokeStyle;ctx.shadowBlur=25;ell(cx,cy,rx,ry);ctx.stroke();
ctx.restore();
}
function drawDebris(e,t){if(!e.debris.length)return;const a=t<14.5?1:clamp(1-(t-14.5)/3,0,1);if(a<=0)return;
ctx.save();ctx.globalAlpha=a;
for(const d of e.debris){ctx.fillStyle='rgba(0,0,0,.25)';ell(d.x,d.y+4,d.w*.45,d.w*.18);ctx.fill();
ctx.save();ctx.translate(d.x,d.y-d.z);ctx.rotate(d.a);ctx.fillStyle=d.col;
if(d.shape==='c'){circ(0,0,d.w/2);ctx.fill();}
else{ctx.fillRect(-d.w/2,-d.h/2,d.w,d.h);if(d.shape==='s'){ctx.fillStyle='#1e5c45';ctx.font='bold 16px system-ui,sans-serif';ctx.textAlign='center';ctx.textBaseline='middle';ctx.fillText('STARBUCKLE',0,0);}}
ctx.restore();}
ctx.restore();}
function drawUnicorn(x,y,sc,t,n,spin,dance){
const ph=t*Math.PI*4,hue=VT*70,face=(Math.floor(t/2)%2)?-1:1,horns=Math.min(n,7);
ctx.save();ctx.translate(x,y);
ctx.fillStyle='rgba(0,0,0,.35)';ell(0,0,130*sc,20*sc);ctx.fill();
ctx.rotate(spin||0);ctx.scale(sc*face,sc);ctx.lineCap='round';ctx.lineJoin='round';
const bob=Math.abs(Math.sin(ph/2))*24;
ctx.shadowBlur=18;
[[-62,0],[-38,Math.PI],[42,Math.PI/2],[66,Math.PI*1.5]].forEach(([hx,p],i)=>{
const s=Math.sin(ph/2+p),kick=i>=2?Math.max(0,s)*.9:0,a1=s*.45+kick,a2=a1*.5-kick*1.4;
const hipY=-125-bob,kx=hx+Math.sin(a1)*58,ky=hipY+Math.cos(a1)*58,fx=kx+Math.sin(a2)*62,fy=ky+Math.cos(a2)*62;
const col=hsl(hue+i*50,100,62);ctx.strokeStyle=col;ctx.shadowColor=col;ctx.lineWidth=18;
ctx.beginPath();ctx.moveTo(hx,hipY);ctx.lineTo(kx,ky);ctx.lineTo(fx,fy);ctx.stroke();ctx.fillStyle='#fff';circ(fx,fy,10);ctx.fill();});
ctx.translate(0,-bob);ctx.rotate(Math.sin(ph/2)*.07);
for(let i=0;i<10;i++){const tx=-105-i*13,ty=-160+i*7+Math.sin(ph/2-i*.7)*i*3,col=hsl(hue+i*36,100,60);ctx.fillStyle=col;ctx.shadowColor=col;circ(tx,ty,15-i*.9);ctx.fill();}
const g=ctx.createLinearGradient(-110,-210,110,-100);g.addColorStop(0,hsl(hue,100,62));g.addColorStop(.5,hsl(hue+120,100,62));g.addColorStop(1,hsl(hue+240,100,62));
ctx.fillStyle=g;ctx.shadowColor=hsl(hue+60,100,60);ctx.shadowBlur=35;ell(0,-155,112,60);ctx.fill();
ctx.shadowBlur=0;ctx.lineWidth=4;ctx.strokeStyle='rgba(255,255,255,.85)';ctx.stroke();
for(let i=0;i<5;i++){ctx.fillStyle=hsl(hue+200+i*30,100,85,.85);star(-60+i*30,-155+Math.sin(i*2+VT)*20,7);}
ctx.save();ctx.translate(78,-185);ctx.rotate(-.35+Math.sin(ph)*.2);
ctx.strokeStyle=hsl(hue+160,100,62);ctx.lineWidth=46;ctx.shadowBlur=20;ctx.shadowColor=ctx.strokeStyle;line(0,10,10,-80);
for(let i=0;i<8;i++){const p=i/7,mx=-22+p*10+Math.sin(ph+i)*4,my=15-p*100,col=hsl(hue+i*45+180,100,60);ctx.fillStyle=col;ctx.shadowColor=col;circ(mx-8,my,14);ctx.fill();}
ctx.save();ctx.translate(18,-100);ctx.rotate(.55);
ctx.fillStyle=hsl(hue+200,100,66);ctx.shadowColor=ctx.fillStyle;ctx.shadowBlur=25;ell(20,0,50,30);ctx.fill();
ctx.shadowBlur=0;ctx.strokeStyle='#fff';ctx.lineWidth=3;ctx.stroke();
ctx.fillStyle=hsl(hue+230,100,70);ctx.beginPath();ctx.moveTo(-8,-20);ctx.lineTo(-2,-46);ctx.lineTo(8,-24);ctx.fill();
ctx.fillStyle='#fff';circ(8,-8,8);ctx.fill();ctx.fillStyle='#000';circ(10,-8,4);ctx.fill();
if(n>=2){ctx.fillStyle='#050505';rrect(-6,-16,28,14,5);ctx.fill();ctx.fillStyle='rgba(255,255,255,.6)';ctx.fillRect(0,-13,7,2);}
ctx.fillStyle='rgba(0,0,0,.5)';circ(58,4,3);ctx.fill();
ctx.strokeStyle='#fff';ctx.lineWidth=3;ctx.beginPath();ctx.arc(44,12,10,.2,Math.PI-.4);ctx.stroke();
for(let k=0;k<horns;k++){ctx.save();ctx.translate(22,-26);ctx.rotate(.25+(k-(horns-1)/2)*.32);
const hg=ctx.createLinearGradient(0,0,0,-85);hg.addColorStop(0,'#fff');hg.addColorStop(1,hsl(hue+k*50,100,70));
ctx.fillStyle=hg;ctx.shadowBlur=25;ctx.shadowColor=hsl(hue+k*50,100,60);
ctx.beginPath();ctx.moveTo(-9,0);ctx.lineTo(9,0);ctx.lineTo(0,-85);ctx.closePath();ctx.fill();ctx.shadowBlur=0;
ctx.strokeStyle='rgba(120,0,160,.4)';ctx.lineWidth=2;for(let s=1;s<5;s++){const yy=-s*17,w=9*(1-s/5);line(-w,yy+4,w,yy-4);}
if(dance){ctx.save();ctx.globalCompositeOperation='lighter';const o=Math.sin(t*1.7+k*1.3)*.9;
ctx.strokeStyle=hsl(hue+k*60,100,60,.45);ctx.lineWidth=7;ctx.beginPath();ctx.moveTo(0,-85);ctx.lineTo(Math.sin(o)*2000,-85-Math.cos(o)*2000);ctx.stroke();
ctx.lineWidth=2;ctx.strokeStyle='rgba(255,255,255,.7)';ctx.stroke();ctx.restore();}
ctx.restore();}
ctx.restore();ctx.restore();ctx.restore();
}
function drawRebuild(e,t){
const p=ease((t-14.5)/4),top=SHOP.wallTop-12,bot=SHOP.floorBot+20,yc=bot-(bot-top)*p;
ctx.save();ctx.beginPath();ctx.rect(0,yc,W,H-yc);ctx.clip();drawShopAll({lvl:0});ctx.restore();
ctx.save();ctx.globalAlpha=.75*(1-p*.85);ctx.strokeStyle='#ffb347';ctx.lineWidth=2;
for(let x=SHOP.x0-8;x<=SHOP.x1+8;x+=42)line(x,top,x,bot);
let k=0;for(let y=top;y<=bot;y+=38){line(SHOP.x0-8,y,SHOP.x1+8,y);for(let x=SHOP.x0-8;x<SHOP.x1;x+=42){if((k++)%2)line(x,y,x+42,y+38);}}
ctx.restore();
ctx.strokeStyle='#ffd24a';ctx.lineWidth=3;line(SHOP.x0-8,yc,SHOP.x1+8,yc);
}
function drawEventWorld(){
const e=S.ev,t=e.t;
if(t<2.5){drawShopAll({lvl:3,hs:t*140});return;}
const cA=t<14.5?1:clamp(1-(t-14.5)/3,0,1);
if(cA>0)drawCrater(e,cA,t);
if(e.variant==='launch'&&t<6){const dy=-Math.pow(t-2.5,2.3)*70;ctx.save();ctx.translate(Math.sin(t*50)*3,dy);drawShopAll({lvl:3,hs:t*160});ctx.restore();}
if(e.variant==='melt'&&t<5){const p=(t-2.5)/2.5,by=SHOP.floorBot+10;ctx.save();ctx.translate(655,by);ctx.scale(1+.45*p*p,Math.max(.02,1-p));ctx.translate(-655,-by);drawShopAll({lvl:3,hs:t*260});ctx.restore();}
drawDebris(e,t);
if(t>=14.5)drawRebuild(e,t);
if(t>=4.5&&t<14.5){const up=uniPos(e,t);drawUnicorn(up.x,up.y,up.sc,t,e.n,up.dep*6,t<13);}
}
// ================= particles draw =================
function drawParticles(){
ctx.save();
for(const p of parts){if(p.k!=='c')continue;const f=clamp(p.life/p.max,0,1);ctx.globalAlpha=Math.min(1,f*2);ctx.fillStyle=hsl(p.h,95,60);
ctx.save();ctx.translate(p.x,p.y);ctx.rotate(p.a);ctx.fillRect(-p.size,-p.size*.5,p.size*2,p.size);ctx.restore();}
ctx.globalCompositeOperation='lighter';
for(const p of parts){const f=clamp(p.life/p.max,0,1);
if(p.k==='s'){ctx.globalAlpha=f;ctx.fillStyle=hsl(p.h,100,65);circ(p.x,p.y,p.size*(.5+f*.5));ctx.fill();}
else if(p.k==='f'){ctx.globalAlpha=f*.8;ctx.fillStyle=hsl(p.h+(1-f)*-20,100,50+f*20);circ(p.x,p.y,p.size*(1.6-f));ctx.fill();}
else if(p.k==='r'){ctx.globalAlpha=f;ctx.strokeStyle=hsl(p.h,100,65);ctx.lineWidth=p.w*f+1;circ(p.x,p.y,p.size+(1-f)*p.grow);ctx.stroke();}
else if(p.k==='n'){ctx.globalAlpha=f;ctx.fillStyle=hsl(p.h,100,70);ctx.font='bold 22px system-ui,sans-serif';ctx.textAlign='center';ctx.fillText(p.a>3?'♪':'♫',p.x,p.y);}}
ctx.restore();
}
// ================= overlays / HUD =================
function vignette(col,a){const g=ctx.createRadialGradient(W/2,H/2,H*.35,W/2,H/2,H*.95);g.addColorStop(0,'rgba(0,0,0,0)');g.addColorStop(1,col);
ctx.save();ctx.globalAlpha=a;ctx.fillStyle=g;ctx.fillRect(0,0,W,H);ctx.restore();}
function title(txt,col,size,y){ctx.save();ctx.font=`900 ${size}px system-ui,sans-serif`;ctx.textAlign='center';ctx.textBaseline='middle';
ctx.lineWidth=6;ctx.strokeStyle='rgba(0,0,0,.75)';ctx.strokeText(txt,580,y);ctx.shadowBlur=20;ctx.shadowColor=col;ctx.fillStyle=col;ctx.fillText(txt,580,y);ctx.restore();}
function sub(txt,y){ctx.save();ctx.font='600 14px system-ui,sans-serif';ctx.textAlign='center';ctx.textBaseline='middle';
ctx.lineWidth=4;ctx.strokeStyle='rgba(0,0,0,.8)';ctx.strokeText(txt,580,y);ctx.fillStyle='#fff';ctx.fillText(txt,580,y);ctx.restore();}
function overlays(u){
if(!S.ev){
if(u===1){vignette('rgba(255,79,216,1)',.10+.04*Math.sin(VT*2));sub('something about this room is slightly wrong',30);}
if(u===2){ctx.save();ctx.globalCompositeOperation='lighter';ctx.fillStyle=hsl(VT*90,100,50,.06+.05*Math.max(0,Math.sin(VT*9)));ctx.fillRect(0,0,W,H);ctx.restore();
vignette(hsl(VT*120,100,50),.25+.1*Math.sin(VT*7));
if(Math.floor(VT*5)%3)title('⚠ COINCIDENCE LEVEL: CRITICAL',hsl(VT*200+20,100,65),26,30);
sub('one more Neon Unicorn and this becomes an insurance matter',58);}
}else{
const e=S.ev,t=e.t;
if(t<2.5){ctx.save();ctx.globalCompositeOperation='lighter';ctx.fillStyle=hsl(VT*200,100,50,t*.06);ctx.fillRect(0,0,W,H);ctx.restore();
title('RESONANCE DETECTED',(Math.floor(VT*8)%2)?'#fff':'#ff4fd8',32,36);sub('three Neon Unicorns in one room',66);}
else if(t<14.5){const f=clamp((t-2.5)/.4,0,1);title(`☢ NEON UNICORN EVENT #${e.n} ☢`,hsl(VT*200,100,65),40*(.6+.4*f)+Math.sin(VT*12)*2,40);
sub(e.quip,72);if(t>5)sub(`after ${e.gap} customers · the Unicorn now has ${Math.min(e.n,7)} horn${e.n>1?'s':''}`,94);}
else{title('REBUILDING…','#ffd24a',30,36);sub(e.rq,66);}
}
if(S.flash>0){ctx.fillStyle=`rgba(255,255,255,${S.flash.toFixed(3)})`;ctx.fillRect(0,0,W,H);}
}
function statusOf(u){if(S.ev){const t=S.ev.t;return t<2.5?['RESONANCE','#ff4fd8']:t<14.5?['NEON UNICORN EVENT',hsl(VT*200,100,65)]:['REBUILDING','#ffd24a'];}
return u===0?['NOMINAL','#7dff9a']:u===1?['SLIGHTLY WRONG','#ffd24a']:['UNSTABLE COINCIDENCE','#ff6a3d'];}
function hud(u){
ctx.save();ctx.fillStyle='rgba(10,10,25,.75)';rrect(10,8,222,58,8);ctx.fill();
ctx.fillStyle='#bdb9e0';ctx.font='bold 10px system-ui,sans-serif';ctx.textAlign='left';ctx.textBaseline='alphabetic';ctx.fillText('NEON UNICORNS INSIDE',20,24);
for(let i=0;i<3;i++){const lit=i<u;circ(30+i*26,44,9);ctx.fillStyle=lit?hsl(VT*200+i*80,100,60):'#2a2d4a';
if(lit){ctx.shadowBlur=12;ctx.shadowColor=ctx.fillStyle;}ctx.fill();ctx.shadowBlur=0;}
const[s,col]=statusOf(u);ctx.fillStyle=col;ctx.font='bold 11px system-ui,sans-serif';ctx.fillText(s,106,40);
ctx.fillStyle='#9491b3';ctx.font='10px system-ui,sans-serif';
ctx.fillText(S.ev?'counter closed':u===2?'next unicorn = catastrophe':`served ${S.st.served}`,106,56);
ctx.textAlign='right';ctx.fillStyle='rgba(255,255,255,.75)';ctx.font='11px ui-monospace,monospace';
ctx.fillText(`t ${fmtT(S.t)} · ${paused?'PAUSED':spdText(S.ev?Math.min(speed,EVENT_MAX_SPEED):speed)} · line ${S.outLine.length}`,990,612);
ctx.restore();
}
// ================= main draw =================
function draw(){
ctx.setTransform(DPR,0,0,DPR,0,0);
ctx.drawImage(bg,0,0,W,H);
const u=S.ev?3:unicornInside();
let amp=S.shake;
if(!S.ev&&u===2)amp+=1.5+Math.min(5,S.lvl2T*.35);
if(S.ev&&S.ev.t<2.5)amp+=2+S.ev.t*5;
ctx.save();ctx.translate(rnd(-amp,amp),rnd(-amp,amp));
drawCars();
if(!S.ev)drawShopAll({lvl:u,hs:u>=2?Math.sin(VT*1.3)*45:0});else drawEventWorld();
drawAFrame(u);
const list=S.customers.slice().sort((a,b)=>a.y-b.y);
for(const c of list)drawC(c,u);
if(!S.ev&&u>=2)drawArcs();
if(S.ev&&S.ev.t<2.5)drawResonance();
drawParticles();
ctx.restore();
overlays(u);
hud(u);
}
// ================= panel =================
const ROWS=[['Sim time','t'],['Customers served','served'],['Frappuccinos: plain / neon','split'],
['Observed unicorn rate <span class=sub>(true 16.67%)</span>','rate'],['Luck z-score <span class=sub>(0 = perfectly fair)</span>','z'],
['Catastrophes','cat'],['Customers since last event','since'],[`Mean customers per event <span class=sub>(predicted ≈${PRED_GAP})</span>`,'gapm'],
['Shortest / longest gap','gapr'],['Last gaps','gapl'],['Times two unicorns coincided','two'],['Near misses <span class=sub>(2 → back to 1)</span>','near'],
[`Avg unicorns inside <span class=sub>(predicted ≈${PRED_AVGU})</span>`,'avgu'],['Neon exposure <span class=sub>(unicorn-seconds)</span>','expo'],
['Longest unicorn drought','drought'],['Longest unicorn streak','streak'],['Walked away from the line','balk'],
['Refunds <span class=sub>(launched before drink)</span>','refund'],['Customers airborne','air'],['…of whom reached low orbit','orbit'],
['Baristas launched','bar'],['Insurance premium','prem'],['Barista therapy hours','ther']];
const tb=document.getElementById('stats');
for(const[lab,id]of ROWS){const tr=document.createElement('tr');tr.innerHTML=`<td>${lab}</td><td id="s_${id}">—</td>`;tb.appendChild(tr);}
const cells={};ROWS.forEach(([,id])=>cells[id]=document.getElementById('s_'+id));
const set=(id,v)=>{v=String(v);if(cells[id].textContent!==v)cells[id].textContent=v;};
function fmtT(s){s=Math.floor(s);const h=Math.floor(s/3600),m=Math.floor(s/60)%60,ss=s%60;return(h?h+':'+String(m).padStart(2,'0'):m)+':'+String(ss).padStart(2,'0');}
function money(v){if(!isFinite(v))return'$∞ (uninsurable)';if(v>=1e15)return'$'+v.toExponential(2);return'$'+Math.round(v).toLocaleString('en-US');}
const meterEls=document.querySelectorAll('#meter i'),statusEl=document.getElementById('status');
function panel(){
const st=S.st,n=st.served;
set('t',fmtT(S.t));set('served',n);set('split',`${st.norm} / ${st.uni}`);
set('rate',n?(st.uni/n*100).toFixed(2)+'%':'—');
set('z',n?((st.uni-n/6)/Math.sqrt(n*5/36)).toFixed(2):'—');
set('cat',st.cat);set('since',st.since);
const g=st.gaps;set('gapm',g.length?(g.reduce((a,b)=>a+b,0)/g.length).toFixed(1):'—');
set('gapr',g.length?`${Math.min(...g)} / ${Math.max(...g)}`:'—');set('gapl',g.length?g.slice(-5).join(', '):'—');
set('two',st.two);set('near',st.near);set('avgu',st.open?(st.expo/st.open).toFixed(3):'—');set('expo',st.expo.toFixed(0));
set('drought',st.maxDrought+' drinks');set('streak',st.maxStreak+' in a row');set('balk',st.balk);set('refund',st.refund);
set('air',st.air);set('orbit',st.orbit);set('bar',st.bar);set('prem',money(1200*Math.pow(1.8,st.cat)));set('ther',(3*st.cat*(st.cat+1)/2).toLocaleString('en-US'));
const u=S.ev?3:unicornInside();meterEls.forEach((el,i)=>el.classList.toggle('lit',i<u));
const[s,col]=statusOf(u);statusEl.textContent=s;statusEl.style.color=col;
drawChart();
}
const chCv=document.getElementById('chart'),ch=chCv.getContext('2d');
function drawChart(){
const w=chCv.width,h=chCv.height,pl=48,pr=10,pt=12,pb=28;ch.fillStyle='#10121f';ch.fillRect(0,0,w,h);
const N=Math.max(20,S.st.served),Y=r=>pt+(1-r/.4)*(h-pt-pb),X=n=>pl+(n/N)*(w-pl-pr);
ch.strokeStyle='#23263d';ch.lineWidth=1;ch.fillStyle='#8c8aa6';ch.font='18px system-ui,sans-serif';ch.textAlign='left';
for(const r of[0,.1,.2,.3,.4]){ch.beginPath();ch.moveTo(pl,Y(r));ch.lineTo(w-pr,Y(r));ch.stroke();ch.fillText(Math.round(r*100)+'%',4,Y(r)+6);}
const p=1/6,q=5/6;ch.beginPath();
for(let i=0;i<=60;i++){const n=Math.max(1,N*i/60);ch.lineTo(X(n),Y(clamp(p+2*Math.sqrt(p*q/n),0,.4)));}
for(let i=60;i>=0;i--){const n=Math.max(1,N*i/60);ch.lineTo(X(n),Y(clamp(p-2*Math.sqrt(p*q/n),0,.4)));}
ch.closePath();ch.fillStyle='rgba(79,243,255,.10)';ch.fill();
ch.strokeStyle='rgba(255,79,216,.45)';ch.lineWidth=1.5;for(const n of S.st.catAt){ch.beginPath();ch.moveTo(X(n),pt);ch.lineTo(X(n),h-pb);ch.stroke();}
ch.setLineDash([8,6]);ch.strokeStyle='#fff';ch.lineWidth=1.5;ch.beginPath();ch.moveTo(pl,Y(p));ch.lineTo(w-pr,Y(p));ch.stroke();ch.setLineDash([]);
ch.strokeStyle='#ff4fd8';ch.lineWidth=3;ch.beginPath();S.hist.forEach(([n,r],i)=>{const x=X(n),y=Y(clamp(r,0,.4));i?ch.lineTo(x,y):ch.moveTo(x,y);});ch.stroke();
ch.fillStyle='#8c8aa6';ch.fillText('drinks served: '+S.st.served,pl,h-6);
}
const logEl=document.getElementById('log');
function ts(){return'['+fmtT(S.t)+']';}
function log(msg,cls){const li=document.createElement('li');li.textContent=msg;if(cls)li.className=cls;logEl.prepend(li);while(logEl.children.length>60)logEl.lastChild.remove();}
log('[0:00] Doors open. Promotion live: every Frappuccino has a 1-in-6 chance of being a Neon Unicorn.','r');
// ================= controls =================
const spEl=document.getElementById('speed'),spdLbl=document.getElementById('spd'),turboBtn=document.getElementById('turbo'),pauseBtn=document.getElementById('pause');
let speed=2,paused=false,turbo=false,preTurbo=2;
const s2v=s=>Math.round(1000*Math.log(s/.25)/Math.log(400)),v2s=v=>.25*Math.pow(400,v/1000);
const spdText=s=>(s<10?s.toFixed(s<1?2:1):Math.round(s))+'×';
function setSpeed(s){speed=clamp(s,.25,100);spEl.value=s2v(speed);spdLbl.textContent=spdText(speed);}
function stopTurbo(){turbo=false;turboBtn.classList.remove('on');setSpeed(preTurbo);}
spEl.addEventListener('input',()=>{if(turbo){turbo=false;turboBtn.classList.remove('on');}speed=v2s(+spEl.value);spdLbl.textContent=spdText(speed);});
document.querySelectorAll('[data-s]').forEach(b=>b.addEventListener('click',()=>{if(turbo){turbo=false;turboBtn.classList.remove('on');}setSpeed(+b.dataset.s);}));
turboBtn.addEventListener('click',()=>{if(turbo){stopTurbo();return;}if(S.ev)return;preTurbo=speed;turbo=true;turboBtn.classList.add('on');setSpeed(100);});
function togglePause(){paused=!paused;pauseBtn.textContent=paused?'▶ Resume':'⏸ Pause';}
pauseBtn.addEventListener('click',togglePause);
window.addEventListener('keydown',e=>{if(e.code==='Space'&&e.target===document.body){e.preventDefault();togglePause();}});
setSpeed(2);
// ================= loop =================
let last=performance.now(),acc=0,lastPanel=0;
function frame(now){
let rdt=(now-last)/1000;last=now;if(!(rdt>0))rdt=0;if(rdt>.1)rdt=.1;
let spd=paused?0:speed;if(S.ev)spd=Math.min(spd,EVENT_MAX_SPEED);
acc+=rdt*spd;let n=0;
while(acc>=DT){const had=!!S.ev;step(DT);acc-=DT;
if(!had&&S.ev){acc=Math.min(acc,DT*2);spd=Math.min(spd,EVENT_MAX_SPEED);}
if(++n>=MAX_STEPS){acc=0;break;}}
const vdt=rdt*Math.min(spd,3);VT+=vdt;
visuals(vdt);
draw();
if(now-lastPanel>250){lastPanel=now;panel();}
requestAnimationFrame(frame);
}
panel();
requestAnimationFrame(frame);
})();
</script>
</body>
</html>
First few minutes. It opens at 2× speed with eight customers already in line, placed there so you aren't watching an empty sidewalk. They file in one every half second. The first drink comes off the counter a few simulated seconds later.
The roll. Each drink is decided by a single Math.random() < 1/6 inside serve(), and nothing else reads, retries or conditions it.
The die. The little die over the counter is decoration, picked after the roll: 6 for a unicorn, a random 1–5 otherwise. The spinning die shown while the barista works is pure noise and can't tip you off.
The shop's reactions follow the count of unicorns currently inside:
At 2×, expect the first catastrophe somewhere around the 1–3 minute mark of real time, but it is dice, so it could come in 20 seconds or take ten minutes.
What I tuned or special-cased. I did not touch the dice at all. What I did tune:
What it does badly:
requestAnimationFrame stops and so does the simulation.