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.
At steady state, the outside line keeps the 8-person shop full, the barista completes one Frappuccino every $1.2\text{ s}$ (a throughput of $0.833\text{ drinks/s}$), and each served customer stays inside holding their drink for $7.2\text{ s}$ before walking out the exit. By Little’s Law, $7.2 / 1.2 = 6$ customers are inside holding drinks at any moment (leaving the remaining 2 of the 8 inside slots for the customer currently being served and the next customer walking in and waiting). Consequently, at the exact instant a fresh drink is handed out at the counter, the previous $5$ served customers are still inside the shop with their drinks. For that new drink to trigger a catastrophe, it must roll a Neon Unicorn ($1/6$) while exactly 2 of the $5$ previous drink-holders also hold Neon Unicorns (since 3 would have already triggered an earlier event): $\frac{1}{6} \times \binom{5}{2}\left(\frac{1}{6}\right)^2\left(\frac{5}{6}\right)^3 = \frac{1}{6} \times \frac{10 \times 125}{7776} = \frac{1250}{46656} \approx 0.0268$ per customer, whose reciprocal is $37.3$ customers. Factoring in the post-rebuild warm-up (the first 2 customers into an empty shop cannot hit 3) and the mild anti-clumping from earlier windows surviving below 3, roughly 40–42 customers pass through the counter between catastrophes—about 50 seconds of simulation time at 1x speed (plus the ~5.4-second explosion and rebuild sequence).
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MOONBUCKS — Neon Unicorn Frappuccino Simulator</title>
<style>
:root {
--bg: #0b0e12;
--panel: #131820;
--panel-border: #232c3a;
--text: #e6edf3;
--muted: #8b9bb0;
--green: #00704a;
--green-bright: #1db954;
--pink: #ff2a85;
--cyan: #00f0ff;
--yellow: #ffe600;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
background: var(--bg);
color: var(--text);
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
min-height: 100vh;
display: flex;
flex-direction: column;
overflow-x: hidden;
}
header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 18px;
background: #0f141b;
border-bottom: 1px solid var(--panel-border);
gap: 12px;
flex-wrap: wrap;
}
.brand {
display: flex;
align-items: center;
gap: 10px;
}
.logo-dot {
width: 28px;
height: 28px;
border-radius: 50%;
background: var(--green);
border: 2px solid #d7f3e5;
display: grid;
place-items: center;
font-weight: 900;
font-size: 14px;
color: #fff;
box-shadow: 0 0 12px rgba(0, 112, 74, 0.5);
}
.brand h1 {
font-size: 16px;
letter-spacing: 0.08em;
font-weight: 800;
}
.brand .sub {
font-size: 12px;
color: var(--muted);
}
.promo-pill {
font-size: 12px;
font-weight: 700;
padding: 5px 12px;
border-radius: 999px;
background: linear-gradient(90deg, rgba(255,42,133,0.18), rgba(0,240,255,0.18));
border: 1px solid rgba(255,42,133,0.45);
color: #ffd6eb;
letter-spacing: 0.03em;
}
.main {
flex: 1;
display: grid;
grid-template-columns: 1fr 340px;
gap: 14px;
padding: 14px;
max-width: 1440px;
width: 100%;
margin: 0 auto;
}
@media (max-width: 980px) {
.main {
grid-template-columns: 1fr;
}
}
.stage-wrap {
background: #0d1117;
border: 1px solid var(--panel-border);
border-radius: 12px;
padding: 10px;
display: flex;
flex-direction: column;
gap: 8px;
position: relative;
}
canvas {
width: 100%;
height: auto;
aspect-ratio: 900 / 620;
border-radius: 8px;
display: block;
background: #10151c;
}
.controls-bar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
flex-wrap: wrap;
padding: 4px 4px 0;
}
.speed-group {
display: flex;
align-items: center;
gap: 6px;
flex-wrap: wrap;
}
.speed-label {
font-size: 12px;
color: var(--muted);
font-weight: 600;
margin-right: 4px;
}
button {
background: #1b222d;
color: var(--text);
border: 1px solid #2b3646;
border-radius: 6px;
padding: 6px 10px;
font-size: 12px;
font-weight: 700;
cursor: pointer;
transition: background 0.12s, border-color 0.12s, transform 0.08s;
}
button:hover { background: #252f3d; border-color: #3f4e63; }
button:active { transform: translateY(1px); }
button.active {
background: var(--green);
border-color: var(--green-bright);
color: #fff;
}
button.warn {
border-color: rgba(255, 42, 133, 0.5);
}
.sidebar {
display: flex;
flex-direction: column;
gap: 12px;
}
.card {
background: var(--panel);
border: 1px solid var(--panel-border);
border-radius: 10px;
padding: 12px 14px;
}
.card-title {
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--muted);
font-weight: 700;
margin-bottom: 10px;
display: flex;
justify-content: space-between;
align-items: center;
}
.unicorn-meter {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
}
.pip {
flex: 1;
height: 18px;
border-radius: 5px;
background: #1b222d;
border: 1px solid #2c3748;
transition: all 0.15s;
}
.pip.lit-1 {
background: linear-gradient(90deg, #00f0ff, #40ffaa);
border-color: #9fffe0;
box-shadow: 0 0 12px rgba(0, 240, 255, 0.6);
}
.pip.lit-2 {
background: linear-gradient(90deg, #ff2a85, #ffe600);
border-color: #fff0a8;
box-shadow: 0 0 16px rgba(255, 42, 133, 0.85);
}
.pip.lit-3 {
background: linear-gradient(90deg, #ff2a85, #ffe600, #00f0ff);
border-color: #ffffff;
box-shadow: 0 0 22px rgba(255, 42, 133, 1);
}
.threat-status {
font-size: 13px;
font-weight: 700;
padding: 6px 8px;
border-radius: 6px;
background: #18202a;
text-align: center;
}
.stat-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
.stat {
background: #0f141b;
border: 1px solid #1e2633;
border-radius: 7px;
padding: 8px 10px;
}
.stat .k {
font-size: 11px;
color: var(--muted);
margin-bottom: 2px;
}
.stat .v {
font-size: 18px;
font-weight: 800;
font-variant-numeric: tabular-nums;
}
.stat .subv {
font-size: 11px;
color: var(--muted);
font-variant-numeric: tabular-nums;
margin-top: 2px;
}
.stat.wide {
grid-column: span 2;
}
.rate-bar-wrap {
margin-top: 6px;
height: 8px;
background: #1c2430;
border-radius: 999px;
position: relative;
overflow: hidden;
}
.rate-bar-fill {
height: 100%;
background: linear-gradient(90deg, var(--cyan), var(--pink));
width: 16.67%;
transition: width 0.2s;
}
.rate-target {
position: absolute;
left: 50%;
top: 0;
bottom: 0;
width: 2px;
background: #ffffff;
opacity: 0.85;
}
.log-list {
list-style: none;
display: flex;
flex-direction: column;
gap: 5px;
max-height: 170px;
overflow-y: auto;
font-size: 12px;
}
.log-list li {
padding: 5px 7px;
border-radius: 5px;
background: #0f141b;
border-left: 3px solid #2e3b4e;
color: #c9d5e3;
line-height: 1.3;
}
.log-list li.u { border-left-color: var(--cyan); }
.log-list li.n { border-left-color: var(--yellow); }
.log-list li.e { border-left-color: var(--pink); background: rgba(255,42,133,0.12); color: #ffd9ec; font-weight: 600; }
</style>
</head>
<body>
<header>
<div class="brand">
<div class="logo-dot">M</div>
<div>
<h1>MOONBUCKS COFFEE CO.</h1>
<div class="sub">Store #404 • Max Occupancy: 8 Customers • Live Stochastic Simulation</div>
</div>
</div>
<div class="promo-pill">PROMO: EVERY FRAPPUCCINO HAS A 1-IN-6 CHANCE OF BEING A NEON UNICORN™</div>
</header>
<div class="main">
<div class="stage-wrap">
<canvas id="simCanvas" width="900" height="620"></canvas>
<div class="controls-bar">
<div class="speed-group" id="speedButtons">
<span class="speed-label">SIM SPEED:</span>
<button data-speed="0.5">0.5x</button>
<button data-speed="1" class="active">1x</button>
<button data-speed="2">2x</button>
<button data-speed="5">5x</button>
<button data-speed="10">10x</button>
<button data-speed="25">25x</button>
</div>
<div class="speed-group">
<button id="pauseBtn">Pause</button>
<button id="soundBtn">Sound: Off</button>
<button id="resetStatsBtn" class="warn">Reset All</button>
</div>
</div>
</div>
<div class="sidebar">
<div class="card">
<div class="card-title">
<span>Unicorns Currently Inside</span>
<span id="insideRatioText">0 / 8 inside</span>
</div>
<div class="unicorn-meter">
<div class="pip" id="pip1"></div>
<div class="pip" id="pip2"></div>
<div class="pip" id="pip3"></div>
</div>
<div class="threat-status" id="threatStatus">CALM • 0 Neon Unicorns inside</div>
</div>
<div class="card">
<div class="card-title">
<span>Live Dice Telemetry</span>
<span id="simClock">00:00</span>
</div>
<div class="stat-grid">
<div class="stat">
<div class="k">Customers Served</div>
<div class="v" id="statServed">0</div>
<div class="subv" id="statLine">Outside line: 0</div>
</div>
<div class="stat">
<div class="k">Neon Unicorns</div>
<div class="v" id="statUnicorns" style="color:#ff6bb5">0</div>
<div class="subv" id="statSince">0 served since event</div>
</div>
<div class="stat wide">
<div class="k">Observed Unicorn Rate vs. True 1-in-6 (16.67%)</div>
<div style="display:flex;justify-content:space-between;align-items:baseline;">
<div class="v" id="statRate">0.00%</div>
<div class="subv" id="statLuck">Awaiting first pour</div>
</div>
<div class="rate-bar-wrap" title="Center tick = true 16.67% (scale 0% to 33.33%)">
<div class="rate-bar-fill" id="rateBar"></div>
<div class="rate-target"></div>
</div>
</div>
<div class="stat">
<div class="k">Catastrophes</div>
<div class="v" id="statEvents" style="color:#ffe600">0</div>
<div class="subv" id="statAvgGap">Avg gap: —</div>
</div>
<div class="stat">
<div class="k">Near Misses (2→1)</div>
<div class="v" id="statNearMiss" style="color:#00f0ff">0</div>
<div class="subv" id="statShortest">Shortest gap: —</div>
</div>
<div class="stat">
<div class="k">Property Damage</div>
<div class="v" id="statDamage">$0</div>
<div class="subv" id="statLongest">Longest calm: —</div>
</div>
<div class="stat">
<div class="k">Barista Morale</div>
<div class="v" id="statMorale" style="font-size:14px;padding-top:3px;">Chill</div>
<div class="subv" id="statSips">0 sips taken</div>
</div>
</div>
</div>
<div class="card" style="flex:1;min-height:150px;">
<div class="card-title">
<span>Incident Feed</span>
<span>Pure 1/6 RNG</span>
</div>
<ul class="log-list" id="logList">
<li>Shop opened. Mystery Frappuccino promo is live.</li>
</ul>
</div>
</div>
</div>
<script>
(() => {
const canvas = document.getElementById('simCanvas');
const ctx = canvas.getContext('2d');
const W = 900, H = 620;
// High-DPI crispness
function setupCanvasDPI() {
const dpr = Math.min(window.devicePixelRatio || 1, 2);
canvas.width = W * dpr;
canvas.height = H * dpr;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
setupCanvasDPI();
window.addEventListener('resize', setupCanvasDPI);
// Audio synthesizer (Web Audio API, no external assets)
let soundEnabled = false;
let audioCtx = null;
function ensureAudio() {
if (!soundEnabled) return null;
try {
if (!audioCtx) {
const AC = window.AudioContext || window.webkitAudioContext;
if (AC) audioCtx = new AC();
}
if (audioCtx && audioCtx.state === 'suspended') audioCtx.resume();
return audioCtx;
} catch (e) {
return null;
}
}
function playTone(freq, dur, type = 'sine', vol = 0.06, slideTo = null) {
const ac = ensureAudio();
if (!ac) return;
try {
const now = ac.currentTime;
const osc = ac.createOscillator();
const gain = ac.createGain();
osc.type = type;
osc.frequency.setValueAtTime(freq, now);
if (slideTo) osc.frequency.exponentialRampToValueAtTime(slideTo, now + dur);
gain.gain.setValueAtTime(vol, now);
gain.gain.exponentialRampToValueAtTime(0.0001, now + dur);
osc.connect(gain);
gain.connect(ac.destination);
osc.start(now);
osc.stop(now + dur);
} catch (e) {}
}
function playUnicornSound(countInside) {
if (countInside === 1) {
playTone(523.25, 0.14, 'triangle', 0.06, 784);
} else if (countInside === 2) {
playTone(622.25, 0.12, 'sawtooth', 0.06, 932);
setTimeout(() => playTone(880, 0.18, 'square', 0.04, 1174), 70);
}
}
function playCatastropheSound() {
const ac = ensureAudio();
if (!ac) return;
playTone(140, 0.9, 'sawtooth', 0.12, 34);
playTone(220, 0.6, 'square', 0.07, 55);
const notes = [523.25, 659.25, 783.99, 1046.5, 1318.5, 1567.98];
notes.forEach((n, i) => {
setTimeout(() => playTone(n, 0.25, 'triangle', 0.06, n * 1.12), 140 + i * 95);
});
}
// Geometry constants
const SHOP = { x: 175, y: 48, w: 690, h: 525 };
const ENTRANCE = { x: 175, y: 455 };
const EXIT_DOOR = { x: 740, y: 573 };
const COUNTER = { x: 270, y: 86, w: 350, h: 64 };
const ORDER_SPOT = { x: 430, y: 185 };
// Inside queue positions leading to ORDER_SPOT (up to 8 inside)
const QUEUE_SLOTS = [
{ x: 430, y: 185 },
{ x: 386, y: 224 },
{ x: 342, y: 263 },
{ x: 298, y: 302 },
{ x: 256, y: 346 },
{ x: 222, y: 394 },
{ x: 202, y: 435 },
{ x: 192, y: 455 }
];
// Outside sidewalk line slots (max 14 waiting outside)
const OUTSIDE_SLOTS = [];
for (let i = 0; i < 14; i++) {
if (i < 4) {
OUTSIDE_SLOTS.push({ x: 140 - i * 28, y: 455 });
} else {
OUTSIDE_SLOTS.push({ x: 56, y: 425 - (i - 4) * 32 });
}
}
// Lounge tables & hang-out spots for customers holding drinks
const TABLES = [
{ x: 590, y: 275, r: 24 },
{ x: 750, y: 265, r: 24 },
{ x: 640, y: 425, r: 24 }
];
const LOUNGE_SPOTS = [
{ x: 550, y: 255 }, { x: 630, y: 255 }, { x: 590, y: 318 },
{ x: 710, y: 245 }, { x: 790, y: 245 }, { x: 750, y: 308 },
{ x: 600, y: 405 }, { x: 680, y: 405 }, { x: 640, y: 468 },
{ x: 795, y: 390 }, { x: 515, y: 360 }, { x: 795, y: 165 }
];
const NORMAL_DRINK_COLORS = ['#c68b59', '#9b6b43', '#7fb069', '#d4a373', '#a98467'];
const SHIRT_COLORS = ['#7c98b3', '#95a5a6', '#b8a99a', '#8ab0ab', '#a89f91', '#9db4c0', '#c2b280'];
// Simulation parameters (explicitly match "The odds")
const MAX_INSIDE = 8;
const SERVICE_TIME = 1.2; // seconds per Frappuccino at the counter
const HOLD_DURATION = 7.2; // seconds inside holding drink after served
const UNICORN_PROB = 1 / 6; // pure independent 1-in-6 roll at counter
// State
let simSpeed = 1;
let paused = false;
let simTime = 0;
let visualTime = 0;
let nextCustId = 1;
let outsideQueue = [];
let insideCustomers = [];
let particles = [];
let floatingTexts = [];
let debris = [];
let panicDots = [];
let logs = [];
// Phase: 'RUNNING' | 'EVENT' | 'REBUILD'
let phase = 'RUNNING';
let phaseTimer = 0;
let doorCooldown = 0;
let spawnTimer = 0.2;
let baristaPos = { x: 430, y: 118 };
let shakeIntensity = 0;
let flashAlpha = 0;
let alarmBeepTimer = 0;
// Stats
const stats = {
served: 0,
unicorns: 0,
events: 0,
nearMisses: 0,
sinceLastEvent: 0,
longestCalm: 0,
shortestGap: null,
totalSips: 0,
eventGaps: []
};
function addLog(msg, cls = '') {
logs.unshift({ msg, cls });
if (logs.length > 24) logs.pop();
renderLogs();
}
function renderLogs() {
const ul = document.getElementById('logList');
ul.innerHTML = '';
for (const item of logs) {
const li = document.createElement('li');
if (item.cls) li.className = item.cls;
li.textContent = item.msg;
ul.appendChild(li);
}
}
function makeCustomer(x, y) {
return {
id: nextCustId++,
x, y,
vx: 0, vy: 0,
r: 11,
color: SHIRT_COLORS[Math.floor(Math.random() * SHIRT_COLORS.length)],
state: 'outside', // 'outside' | 'queue' | 'holding'
serveTimer: 0,
holdTimer: 0,
hasDrink: false,
isUnicorn: false,
drinkColor: '#ffffff',
loungeIndex: -1,
sipPhase: Math.random() * Math.PI * 2,
bobPhase: Math.random() * Math.PI * 2,
sipCounted: false
};
}
function seedInitialLine() {
for (let i = 0; i < 8; i++) {
const slot = OUTSIDE_SLOTS[i];
outsideQueue.push(makeCustomer(slot.x, slot.y));
}
}
function countUnicornsInside() {
let c = 0;
for (const cust of insideCustomers) {
if (cust.hasDrink && cust.isUnicorn) c++;
}
return c;
}
function pickLoungeSpot() {
const used = new Set();
for (const c of insideCustomers) {
if (c.loungeIndex >= 0) used.add(c.loungeIndex);
}
const free = [];
for (let i = 0; i < LOUNGE_SPOTS.length; i++) {
if (!used.has(i)) free.push(i);
}
if (free.length > 0) return free[Math.floor(Math.random() * free.length)];
return Math.floor(Math.random() * LOUNGE_SPOTS.length);
}
function spawnParticle(x, y, vx, vy, color, size, life, glow = false) {
if (particles.length > 500) return;
particles.push({ x, y, vx, vy, color, size, life, maxLife: life, glow });
}
function spawnFloatText(x, y, text, color, size = 14) {
floatingTexts.push({ x, y, text, color, size, life: 1.5, maxLife: 1.5 });
}
function moveToward(entity, tx, ty, speed, dt) {
const dx = tx - entity.x;
const dy = ty - entity.y;
const dist = Math.hypot(dx, dy);
if (dist < 0.001) return 0;
const step = Math.min(dist, speed * dt);
entity.x += (dx / dist) * step;
entity.y += (dy / dist) * step;
return dist;
}
function triggerCatastrophe(triggerCustomer) {
phase = 'EVENT';
phaseTimer = 0;
flashAlpha = 1;
shakeIntensity = 22;
stats.events++;
stats.eventGaps.push(stats.sinceLastEvent);
if (stats.sinceLastEvent > stats.longestCalm) stats.longestCalm = stats.sinceLastEvent;
if (stats.shortestGap === null || stats.sinceLastEvent < stats.shortestGap) {
stats.shortestGap = stats.sinceLastEvent;
}
addLog(
`EVENT #${stats.events}! Cust #${triggerCustomer.id} rolled 3rd Neon Unicorn inside (${stats.sinceLastEvent} served since last).`,
'e'
);
stats.sinceLastEvent = 0;
playCatastropheSound();
// Shatter shop structures into flying debris
debris = [];
const pieces = [
// Walls
{ x: SHOP.x, y: SHOP.y, w: 230, h: 10, c: '#3b4a5e' },
{ x: SHOP.x + 230, y: SHOP.y, w: 230, h: 10, c: '#3b4a5e' },
{ x: SHOP.x + 460, y: SHOP.y, w: 230, h: 10, c: '#3b4a5e' },
{ x: SHOP.x, y: SHOP.y + SHOP.h - 8, w: 260, h: 10, c: '#3b4a5e' },
{ x: SHOP.x + 280, y: SHOP.y + SHOP.h - 8, w: 260, h: 10, c: '#3b4a5e' },
{ x: SHOP.x + SHOP.w - 8, y: SHOP.y, w: 10, h: 260, c: '#3b4a5e' },
{ x: SHOP.x + SHOP.w - 8, y: SHOP.y + 260, w: 10, h: 265, c: '#3b4a5e' },
// Counter pieces
{ x: COUNTER.x, y: COUNTER.y, w: 115, h: 58, c: '#00704a' },
{ x: COUNTER.x + 115, y: COUNTER.y, w: 115, h: 58, c: '#00704a' },
{ x: COUNTER.x + 230, y: COUNTER.y, w: 120, h: 58, c: '#00704a' },
// Espresso machine & blenders
{ x: 355, y: 95, w: 34, h: 24, c: '#c4cdd5' },
{ x: 405, y: 95, w: 26, h: 26, c: '#ff2a85' }
];
for (const t of TABLES) {
pieces.push({ x: t.x - t.r, y: t.y - t.r, w: t.r * 2, h: t.r * 2, c: '#8d6e63' });
}
for (let i = 0; i < 18; i++) {
pieces.push({
x: SHOP.x + 60 + Math.random() * (SHOP.w - 120),
y: SHOP.y + 60 + Math.random() * (SHOP.h - 120),
w: 14 + Math.random() * 24,
h: 10 + Math.random() * 18,
c: ['#ff2a85', '#00f0ff', '#ffe600', '#00704a', '#4a5568'][i % 5]
});
}
const cx = 480, cy = 285;
for (const p of pieces) {
const px = p.x + p.w / 2;
const py = p.y + p.h / 2;
const ang = Math.atan2(py - cy, px - cx) + (Math.random() - 0.5) * 0.6;
const spd = 140 + Math.random() * 340;
debris.push({
x: px, y: py, w: p.w, h: p.h,
vx: Math.cos(ang) * spd,
vy: Math.sin(ang) * spd,
rot: 0,
vrot: (Math.random() - 0.5) * 11,
color: p.c
});
}
// Fleeing customers + ascending unicorn holders
panicDots = [];
for (const c of insideCustomers) {
const ang = Math.atan2(c.y - cy, c.x - cx) + (Math.random() - 0.5) * 0.5;
const spd = c.isUnicorn ? 90 : 240 + Math.random() * 180;
panicDots.push({
x: c.x, y: c.y,
vx: Math.cos(ang) * spd,
vy: c.isUnicorn ? -120 - Math.random() * 80 : Math.sin(ang) * spd,
r: c.r,
color: c.color,
isUnicorn: c.isUnicorn
});
}
// Barista also blasts off
panicDots.push({
x: baristaPos.x, y: baristaPos.y,
vx: (Math.random() - 0.5) * 200,
vy: -220,
r: 13,
color: '#00704a',
isUnicorn: false
});
insideCustomers = [];
// Massive neon starburst
for (let i = 0; i < 140; i++) {
const ang = Math.random() * Math.PI * 2;
const spd = 80 + Math.random() * 440;
const hue = (i * 19) % 360;
spawnParticle(
triggerCustomer.x, triggerCustomer.y,
Math.cos(ang) * spd, Math.sin(ang) * spd,
`hsl(${hue}, 100%, 62%)`,
3 + Math.random() * 5,
1.2 + Math.random() * 1.4,
true
);
}
}
function stepRunning(dt) {
simTime += dt;
if (doorCooldown > 0) doorCooldown -= dt;
// Outside arrival stream (keeps sidewalk line lively)
spawnTimer -= dt;
if (spawnTimer <= 0) {
spawnTimer = 0.85 + Math.random() * 0.45; // mean 1.075s
if (outsideQueue.length < OUTSIDE_SLOTS.length) {
outsideQueue.push(makeCustomer(56, 645));
}
}
// Admit from outside queue if inside < MAX_INSIDE
if (insideCustomers.length < MAX_INSIDE && outsideQueue.length > 0 && doorCooldown <= 0) {
const newcomer = outsideQueue.shift();
newcomer.state = 'queue';
newcomer.x = ENTRANCE.x - 4;
newcomer.y = ENTRANCE.y;
insideCustomers.push(newcomer);
doorCooldown = 0.22;
}
// Move outside queue forward smoothly
for (let i = 0; i < outsideQueue.length; i++) {
const c = outsideQueue[i];
const slot = OUTSIDE_SLOTS[i];
const dist = moveToward(c, slot.x, slot.y, 155, dt);
if (dist > 2) c.bobPhase += dt * 11;
}
// Separate inside customers into queueing vs holding
const queueGroup = [];
const holdingGroup = [];
for (const c of insideCustomers) {
if (c.state === 'queue') queueGroup.push(c);
else holdingGroup.push(c);
}
// Update queueing customers & barista service
for (let i = 0; i < queueGroup.length; i++) {
const c = queueGroup[i];
const slot = QUEUE_SLOTS[Math.min(i, QUEUE_SLOTS.length - 1)];
const dist = moveToward(c, slot.x, slot.y, 185, dt);
if (dist > 2) c.bobPhase += dt * 12;
if (i === 0) {
// Barista starts blending as customer steps up to counter
if (dist < 75) {
c.serveTimer += dt;
baristaPos.x += (ORDER_SPOT.x + Math.sin(simTime * 8) * 14 - baristaPos.x) * Math.min(1, dt * 10);
}
if (c.serveTimer >= SERVICE_TIME && dist < 18) {
// Serve Frappuccino and roll the independent 1-in-6 die!
c.hasDrink = true;
c.state = 'holding';
c.holdTimer = 0;
c.loungeIndex = pickLoungeSpot();
stats.served++;
stats.sinceLastEvent++;
if (stats.sinceLastEvent > stats.longestCalm) stats.longestCalm = stats.sinceLastEvent;
const rolledUnicorn = Math.random() < UNICORN_PROB;
c.isUnicorn = rolledUnicorn;
if (rolledUnicorn) {
stats.unicorns++;
const uNow = countUnicornsInside();
addLog(`Cust #${c.id} rolled a NEON UNICORN! (${uNow}/3 inside)`, 'u');
spawnFloatText(c.x, c.y - 18, 'NEON UNICORN!', '#ff4da6', 14);
for (let p = 0; p < 22; p++) {
const a = Math.random() * Math.PI * 2;
const s = 45 + Math.random() * 110;
spawnParticle(c.x, c.y, Math.cos(a) * s, Math.sin(a) * s, `hsl(${(p * 30) % 360}, 100%, 60%)`, 3.5, 0.7, true);
}
if (uNow >= 3) {
triggerCatastrophe(c);
return;
} else {
playUnicornSound(uNow);
}
} else {
c.drinkColor = NORMAL_DRINK_COLORS[Math.floor(Math.random() * NORMAL_DRINK_COLORS.length)];
}
}
}
}
if (queueGroup.length === 0) {
baristaPos.x += (ORDER_SPOT.x - baristaPos.x) * Math.min(1, dt * 5);
}
// Update drink-holding customers inside
const unicornsBeforeExits = countUnicornsInside();
let exitedUnicorn = false;
for (let i = holdingGroup.length - 1; i >= 0; i--) {
const c = holdingGroup[i];
c.holdTimer += dt;
c.sipPhase += dt * 4.5;
const spot = LOUNGE_SPOTS[c.loungeIndex >= 0 ? c.loungeIndex : 0];
if (c.holdTimer < HOLD_DURATION - 1.5) {
const d = moveToward(c, spot.x, spot.y, 150, dt);
if (d > 2) c.bobPhase += dt * 11;
else if (!c.sipCounted && c.holdTimer > 2.5) {
c.sipCounted = true;
stats.totalSips++;
}
} else {
// Walk smoothly to exit door arriving right at HOLD_DURATION
const rem = Math.max(0.05, HOLD_DURATION - c.holdTimer);
const distToExit = Math.hypot(EXIT_DOOR.x - c.x, EXIT_DOOR.y - c.y);
const neededSpeed = Math.max(120, distToExit / rem);
moveToward(c, EXIT_DOOR.x, EXIT_DOOR.y, neededSpeed, dt);
c.bobPhase += dt * 12;
}
if (c.isUnicorn && Math.random() < dt * 14) {
const hue = (visualTime * 220 + Math.random() * 80) % 360;
spawnParticle(
c.x + (Math.random() - 0.5) * 14,
c.y + (Math.random() - 0.5) * 14,
(Math.random() - 0.5) * 35,
-20 - Math.random() * 45,
`hsl(${hue}, 100%, 62%)`,
2.6, 0.65, true
);
}
if (c.holdTimer >= HOLD_DURATION) {
if (c.isUnicorn) exitedUnicorn = true;
const idx = insideCustomers.indexOf(c);
if (idx !== -1) insideCustomers.splice(idx, 1);
}
}
const unicornsAfterExits = countUnicornsInside();
if (exitedUnicorn && unicornsBeforeExits === 2 && unicornsAfterExits === 1) {
stats.nearMisses++;
spawnFloatText(EXIT_DOOR.x - 40, EXIT_DOOR.y - 25, 'PHEW! CRISIS AVERTED', '#00f0ff', 13);
addLog('Near miss! A Unicorn holder left just in time (2 -> 1).', 'n');
}
// Ambient shop stress particles at 1 or 2 unicorns inside
const uCount = unicornsAfterExits;
if (uCount === 1 && Math.random() < dt * 4) {
spawnParticle(372, 102, (Math.random() - 0.5) * 20, -25 - Math.random() * 20, '#c04dff', 2.5, 0.8, true);
} else if (uCount === 2) {
if (Math.random() < dt * 28) {
const rx = SHOP.x + 30 + Math.random() * (SHOP.w - 60);
const ry = SHOP.y + 30 + Math.random() * (SHOP.h - 60);
const hue = (visualTime * 300 + Math.random() * 120) % 360;
spawnParticle(rx, ry, (Math.random() - 0.5) * 60, -40 - Math.random() * 70, `hsl(${hue}, 100%, 65%)`, 3, 0.7, true);
}
alarmBeepTimer -= dt;
if (alarmBeepTimer <= 0) {
alarmBeepTimer = 0.55;
playTone(740, 0.08, 'square', 0.022);
}
}
}
function stepEvent(dt) {
phaseTimer += dt;
if (flashAlpha > 0) flashAlpha = Math.max(0, flashAlpha - dt * 1.9);
shakeIntensity = phaseTimer < 1.2 ? 20 * (1 - phaseTimer / 1.2) : 6 + Math.sin(visualTime * 30) * 3;
// Move debris
for (const d of debris) {
d.x += d.vx * dt;
d.y += d.vy * dt;
d.vx *= Math.pow(0.28, dt);
d.vy *= Math.pow(0.28, dt);
d.rot += d.vrot * dt;
}
// Move fleeing dots
for (const p of panicDots) {
p.x += p.vx * dt;
p.y += p.vy * dt;
}
// Dancing giant unicorn firework sparks
if (phaseTimer > 0.6 && phaseTimer < 4.3 && Math.random() < dt * 55) {
const ux = 480 + Math.sin(phaseTimer * 3.8) * 110;
const uy = 295 - Math.abs(Math.sin(phaseTimer * 6.5)) * 55;
const a = Math.random() * Math.PI * 2;
const s = 90 + Math.random() * 240;
const hue = (visualTime * 360 + Math.random() * 90) % 360;
spawnParticle(ux, uy, Math.cos(a) * s, Math.sin(a) * s, `hsl(${hue}, 100%, 62%)`, 3.5, 0.85, true);
}
if (phaseTimer >= 4.4) {
phase = 'REBUILD';
phaseTimer = 0;
debris = [];
panicDots = [];
baristaPos = { x: ORDER_SPOT.x, y: 118 };
spawnFloatText(480, 290, 'REBUILDING MOONBUCKS...', '#1db954', 20);
}
}
function stepRebuild(dt) {
phaseTimer += dt;
shakeIntensity = 0;
if (phaseTimer >= 1.0) {
phase = 'RUNNING';
phaseTimer = 0;
doorCooldown = 0;
addLog('Store #404 rebuilt! Mystery promo resumes.');
}
}
function stepParticlesAndText(dt) {
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
p.x += p.vx * dt;
p.y += p.vy * dt;
p.life -= dt;
if (p.life <= 0) particles.splice(i, 1);
}
for (let i = floatingTexts.length - 1; i >= 0; i--) {
const t = floatingTexts[i];
t.y -= 28 * dt;
t.life -= dt;
if (t.life <= 0) floatingTexts.splice(i, 1);
}
}
// Drawing helpers
function drawCustomerDot(c, panicMode = false) {
const bob = Math.sin(c.bobPhase) * 1.5;
const jitterX = (c.isUnicorn || panicMode) ? (Math.random() - 0.5) * (c.isUnicorn ? 2.8 : 2.0) : 0;
const jitterY = (c.isUnicorn || panicMode) ? (Math.random() - 0.5) * (c.isUnicorn ? 2.8 : 2.0) : 0;
const cx = c.x + jitterX;
const cy = c.y + bob + jitterY;
ctx.save();
if (c.isUnicorn) {
const hue = (visualTime * 260 + c.id * 47) % 360;
const g = ctx.createRadialGradient(cx, cy, 4, cx, cy, 34);
g.addColorStop(0, `hsla(${hue}, 100%, 62%, 0.55)`);
g.addColorStop(1, `hsla(${(hue + 90) % 360}, 100%, 50%, 0)`);
ctx.fillStyle = g;
ctx.beginPath();
ctx.arc(cx, cy, 34, 0, Math.PI * 2);
ctx.fill();
}
// Shadow
ctx.fillStyle = 'rgba(0,0,0,0.35)';
ctx.beginPath();
ctx.ellipse(cx, cy + c.r * 0.85, c.r * 0.85, c.r * 0.4, 0, 0, Math.PI * 2);
ctx.fill();
// Body circle
ctx.fillStyle = c.isUnicorn ? `hsl(${(visualTime * 220 + c.id * 35) % 360}, 90%, 72%)` : c.color;
ctx.strokeStyle = c.isUnicorn ? '#ffffff' : '#0c1015';
ctx.lineWidth = c.isUnicorn ? 2.4 : 2;
ctx.beginPath();
ctx.arc(cx, cy, c.r, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
// Drink cup held
if (c.hasDrink) {
const sipOffset = Math.sin(c.sipPhase) * 1.5;
const dx = cx + 9;
const dy = cy - 4 + sipOffset;
if (c.isUnicorn) {
const cupHue = (visualTime * 360 + c.id * 60) % 360;
ctx.fillStyle = `hsl(${cupHue}, 100%, 60%)`;
ctx.strokeStyle = '#fff';
ctx.lineWidth = 1.6;
ctx.beginPath();
ctx.arc(dx, dy, 5.8, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
// Tiny neon straw
ctx.strokeStyle = `hsl(${(cupHue + 180) % 360}, 100%, 75%)`;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(dx, dy);
ctx.lineTo(dx + 3, dy - 7);
ctx.stroke();
} else {
ctx.fillStyle = c.drinkColor;
ctx.strokeStyle = '#f5ebe0';
ctx.lineWidth = 1.3;
ctx.beginPath();
ctx.arc(dx, dy, 4.6, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
}
}
ctx.restore();
}
function drawGiantDancingUnicorn(t) {
const bounce = Math.abs(Math.sin(t * 6.5)) * 52;
const sway = Math.sin(t * 3.8) * 105;
const ux = 490 + sway;
const uy = 315 - bounce;
const beat = Math.sin(t * 13);
const hueBase = (t * 280) % 360;
ctx.save();
ctx.translate(ux, uy);
// Rave spotlights & laser beams from horn
const hornTipX = 68;
const hornTipY = -108 + beat * 6;
for (let i = 0; i < 6; i++) {
const ang = t * 2.7 + (i * Math.PI) / 3;
const lx = hornTipX + Math.cos(ang) * 700;
const ly = hornTipY + Math.sin(ang) * 700;
ctx.strokeStyle = `hsla(${(hueBase + i * 55) % 360}, 100%, 62%, 0.42)`;
ctx.lineWidth = 5;
ctx.beginPath();
ctx.moveTo(hornTipX, hornTipY);
ctx.lineTo(lx, ly);
ctx.stroke();
}
// Concentric rave aura rings
for (let r = 0; r < 3; r++) {
const rad = ((t * 180 + r * 70) % 210) + 40;
ctx.strokeStyle = `hsla(${(hueBase + r * 90) % 360}, 100%, 65%, ${1 - rad / 250})`;
ctx.lineWidth = 4;
ctx.beginPath();
ctx.arc(0, 0, rad, 0, Math.PI * 2);
ctx.stroke();
}
// Flowing rainbow tail
for (let s = 0; s < 5; s++) {
ctx.strokeStyle = `hsl(${(hueBase + s * 55) % 360}, 100%, 60%)`;
ctx.lineWidth = 7;
ctx.lineCap = 'round';
ctx.beginPath();
ctx.moveTo(-64, -5 + s * 3);
ctx.bezierCurveTo(
-115, -25 + Math.sin(t * 10 + s) * 26,
-140, 25 + Math.cos(t * 9 + s) * 28,
-175, -10 + Math.sin(t * 12 + s) * 24
);
ctx.stroke();
}
// Flapping geometric wings
const flap = Math.sin(t * 14) * 32;
ctx.fillStyle = `hsla(${(hueBase + 160) % 360}, 100%, 65%, 0.72)`;
ctx.strokeStyle = '#ffffff';
ctx.lineWidth = 2.5;
ctx.beginPath();
ctx.moveTo(-10, -24);
ctx.lineTo(-95, -95 + flap);
ctx.lineTo(-42, -55 + flap * 0.5);
ctx.lineTo(-80, -35 + flap * 0.4);
ctx.closePath();
ctx.fill();
ctx.stroke();
// Articulated dancing legs
const legKick1 = Math.sin(t * 13) * 26;
const legKick2 = Math.cos(t * 13) * 26;
const legs = [
{ bx: -44, by: 26, kx: -55 + legKick1, ky: 62, fx: -42 + legKick1 * 1.4, fy: 95 },
{ bx: -24, by: 28, kx: -18 - legKick2, ky: 62, fx: -25 - legKick2 * 1.4, fy: 96 },
{ bx: 28, by: 26, kx: 40 + legKick2, ky: 58, fx: 58 + legKick2 * 1.3, fy: 90 },
{ bx: 44, by: 22, kx: 62 - legKick1, ky: 52, fx: 75 - legKick1 * 1.3, fy: 82 }
];
ctx.lineWidth = 11;
ctx.lineCap = 'round';
legs.forEach((l, idx) => {
ctx.strokeStyle = `hsl(${(hueBase + idx * 45) % 360}, 100%, 68%)`;
ctx.beginPath();
ctx.moveTo(l.bx, l.by);
ctx.lineTo(l.kx, l.ky);
ctx.lineTo(l.fx, l.fy);
ctx.stroke();
// Glowing hoof
ctx.fillStyle = '#ffffff';
ctx.beginPath();
ctx.arc(l.fx, l.fy, 6, 0, Math.PI * 2);
ctx.fill();
});
// Sleek geometric torso & neck
const bodyGrad = ctx.createLinearGradient(-70, -30, 70, 30);
bodyGrad.addColorStop(0, `hsl(${hueBase}, 100%, 65%)`);
bodyGrad.addColorStop(0.5, `hsl(${(hueBase + 110) % 360}, 100%, 68%)`);
bodyGrad.addColorStop(1, `hsl(${(hueBase + 220) % 360}, 100%, 65%)`);
ctx.fillStyle = bodyGrad;
ctx.strokeStyle = '#ffffff';
ctx.lineWidth = 3;
ctx.beginPath();
ctx.ellipse(0, 4, 68, 34, -0.08, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
// Neck + Head bobbing
ctx.save();
ctx.translate(45, -18 + beat * 6);
ctx.rotate(beat * 0.14);
// Rainbow mane spikes
for (let m = 0; m < 5; m++) {
ctx.fillStyle = `hsl(${(hueBase + m * 50) % 360}, 100%, 60%)`;
ctx.beginPath();
ctx.moveTo(-18 - m * 4, -8 - m * 8);
ctx.lineTo(-48 - m * 5, -18 - m * 7);
ctx.lineTo(-22 - m * 3, -20 - m * 8);
ctx.closePath();
ctx.fill();
}
// Neck + Head polygon
ctx.fillStyle = `hsl(${(hueBase + 190) % 360}, 100%, 72%)`;
ctx.strokeStyle = '#ffffff';
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(-22, 24);
ctx.lineTo(-10, -45);
ctx.lineTo(20, -64);
ctx.lineTo(54, -44);
ctx.lineTo(48, -24);
ctx.lineTo(14, -22);
ctx.lineTo(10, 20);
ctx.closePath();
ctx.fill();
ctx.stroke();
// Glowing Eye
ctx.fillStyle = '#ffffff';
ctx.beginPath();
ctx.arc(16, -44, 6, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#ff007f';
ctx.beginPath();
ctx.arc(17.5, -44, 3, 0, Math.PI * 2);
ctx.fill();
// Giant Spiral Horn
const hGrad = ctx.createLinearGradient(8, -56, 26, -128);
hGrad.addColorStop(0, '#ffe600');
hGrad.addColorStop(0.5, '#ff2a85');
hGrad.addColorStop(1, '#00f0ff');
ctx.fillStyle = hGrad;
ctx.beginPath();
ctx.moveTo(4, -56);
ctx.lineTo(26, -124);
ctx.lineTo(22, -52);
ctx.closePath();
ctx.fill();
ctx.stroke();
ctx.restore();
// Orbiting Giant Unicorn Frappuccino
const cupX = Math.cos(t * 4.5) * 145;
const cupY = Math.sin(t * 4.5) * 60 - 20;
ctx.fillStyle = `hsl(${(hueBase + 90) % 360}, 100%, 60%)`;
ctx.strokeStyle = '#ffffff';
ctx.lineWidth = 2.5;
ctx.beginPath();
ctx.moveTo(cupX - 12, cupY - 12);
ctx.lineTo(cupX + 12, cupY - 12);
ctx.lineTo(cupX + 9, cupY + 18);
ctx.lineTo(cupX - 9, cupY + 18);
ctx.closePath();
ctx.fill();
ctx.stroke();
// Whipped cream dome
ctx.fillStyle = '#ffffff';
ctx.beginPath();
ctx.arc(cupX, cupY - 14, 11, Math.PI, 0);
ctx.fill();
ctx.restore();
}
function render() {
ctx.save();
// Screen shake during 2 unicorns or EVENT
const uCount = countUnicornsInside();
let sx = 0, sy = 0;
if (phase === 'RUNNING' && uCount === 2) {
sx = (Math.random() - 0.5) * 6;
sy = (Math.random() - 0.5) * 6;
} else if (phase === 'EVENT') {
sx = (Math.random() - 0.5) * shakeIntensity;
sy = (Math.random() - 0.5) * shakeIntensity;
}
ctx.translate(sx, sy);
// Sidewalk & exterior
ctx.fillStyle = (phase === 'RUNNING' && uCount === 2)
? `hsl(${(visualTime * 180) % 360}, 28%, 12%)`
: '#11161d';
ctx.fillRect(0, 0, W, H);
// Sidewalk paving grid
ctx.strokeStyle = 'rgba(255,255,255,0.04)';
ctx.lineWidth = 1;
for (let x = 0; x < SHOP.x; x += 35) {
ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, H); ctx.stroke();
}
for (let y = 0; y < H; y += 35) {
ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(SHOP.x, y); ctx.stroke();
}
// Outside promo A-frame sign
ctx.save();
ctx.translate(105, 520);
ctx.fillStyle = '#1b2430';
ctx.strokeStyle = uCount >= 1 ? '#ff2a85' : '#00704a';
ctx.lineWidth = 2;
ctx.fillRect(-44, -22, 88, 44);
ctx.strokeRect(-44, -22, 88, 44);
ctx.fillStyle = '#e6edf3';
ctx.font = 'bold 9px system-ui, sans-serif';
ctx.textAlign = 'center';
ctx.fillText('1-IN-6 CHANCE:', 0, -5);
ctx.fillStyle = uCount >= 1 ? `hsl(${(visualTime * 240) % 360}, 100%, 68%)` : '#ff6bb5';
ctx.fillText('NEON UNICORN!', 0, 10);
ctx.restore();
// Outside stanchion velvet rope
ctx.strokeStyle = '#7f1d3a';
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(32, 476);
ctx.lineTo(155, 476);
ctx.stroke();
// Draw shop building if RUNNING or REBUILD
if (phase === 'RUNNING' || phase === 'REBUILD') {
const rebuildScale = phase === 'REBUILD' ? Math.min(1, phaseTimer / 0.8) : 1;
ctx.save();
if (phase === 'REBUILD') {
ctx.globalAlpha = rebuildScale;
}
// Floor color reacts to unicorn count
let floorCol = '#1c232d';
if (uCount === 1) {
floorCol = '#201e2e'; // subtle purplish wrongness
} else if (uCount === 2) {
const pulse = Math.sin(visualTime * 16) * 0.5 + 0.5;
floorCol = pulse > 0.5 ? '#2b132b' : '#122833';
}
ctx.fillStyle = floorCol;
ctx.fillRect(SHOP.x, SHOP.y, SHOP.w, SHOP.h);
// Floor planks / tiles
ctx.strokeStyle = uCount === 2
? `hsla(${(visualTime * 320) % 360}, 85%, 55%, 0.16)`
: uCount === 1
? 'rgba(200, 110, 255, 0.08)'
: 'rgba(255, 255, 255, 0.04)';
ctx.lineWidth = 1;
for (let x = SHOP.x; x < SHOP.x + SHOP.w; x += 46) {
ctx.beginPath(); ctx.moveTo(x, SHOP.y); ctx.lineTo(x, SHOP.y + SHOP.h); ctx.stroke();
}
for (let y = SHOP.y; y < SHOP.y + SHOP.h; y += 46) {
ctx.beginPath(); ctx.moveTo(SHOP.x, y); ctx.lineTo(SHOP.x + SHOP.w, y); ctx.stroke();
}
// Subtle Level 1 wrongness: creeping neon corner vignette + floor logo horn
if (uCount === 1) {
const vg = ctx.createRadialGradient(520, 310, 140, 520, 310, 420);
vg.addColorStop(0, 'rgba(0,0,0,0)');
vg.addColorStop(1, 'rgba(205, 45, 255, 0.17)');
ctx.fillStyle = vg;
ctx.fillRect(SHOP.x, SHOP.y, SHOP.w, SHOP.h);
}
// Level 2 flashing disco lights + arc tether between the 2 unicorn holders
if (uCount === 2) {
const strobeHue = (visualTime * 420) % 360;
const sg = ctx.createRadialGradient(480, 280, 20, 480, 280, 390);
sg.addColorStop(0, `hsla(${strobeHue}, 100%, 55%, 0.22)`);
sg.addColorStop(1, `hsla(${(strobeHue + 160) % 360}, 100%, 45%, 0.15)`);
ctx.fillStyle = sg;
ctx.fillRect(SHOP.x, SHOP.y, SHOP.w, SHOP.h);
// Sweeping spotlight beams
for (let b = 0; b < 2; b++) {
const ang = visualTime * (b === 0 ? 3.2 : -2.6);
const bx = b === 0 ? SHOP.x + 20 : SHOP.x + SHOP.w - 20;
const by = SHOP.y + 20;
ctx.fillStyle = `hsla(${(strobeHue + b * 140) % 360}, 100%, 60%, 0.11)`;
ctx.beginPath();
ctx.moveTo(bx, by);
ctx.lineTo(bx + Math.cos(ang - 0.22) * 700, by + Math.sin(ang - 0.22) * 700);
ctx.lineTo(bx + Math.cos(ang + 0.22) * 700, by + Math.sin(ang + 0.22) * 700);
ctx.closePath();
ctx.fill();
}
}
// Moonbucks floor medallion (develops a horn at 1+ unicorns!)
ctx.save();
ctx.translate(490, 345);
ctx.strokeStyle = uCount >= 1 ? `hsla(${(visualTime * 200) % 360}, 90%, 60%, 0.35)` : 'rgba(0, 112, 74, 0.28)';
ctx.lineWidth = 4;
ctx.beginPath();
ctx.arc(0, 0, 46, 0, Math.PI * 2);
ctx.stroke();
if (uCount >= 1) {
// Eerie unicorn horn emerging from logo circle
ctx.fillStyle = uCount === 2 ? 'rgba(255,42,133,0.45)' : 'rgba(190,80,255,0.28)';
ctx.beginPath();
ctx.moveTo(-7, -45);
ctx.lineTo(0, -76);
ctx.lineTo(7, -45);
ctx.closePath();
ctx.fill();
}
ctx.fillStyle = uCount >= 1 ? 'rgba(255,150,240,0.22)' : 'rgba(0, 112, 74, 0.25)';
ctx.font = 'bold 12px system-ui, sans-serif';
ctx.textAlign = 'center';
ctx.fillText('MOONBUCKS', 0, 4);
ctx.restore();
// Queue guide dots inside
ctx.fillStyle = 'rgba(255,255,255,0.06)';
for (let i = 1; i < 6; i++) {
ctx.beginPath();
ctx.arc(QUEUE_SLOTS[i].x, QUEUE_SLOTS[i].y, 4, 0, Math.PI * 2);
ctx.fill();
}
// Lounge tables & chairs
for (const t of TABLES) {
// Chairs
ctx.fillStyle = '#293444';
const offsets = [[-32, -16], [32, -16], [0, 36]];
for (const [ox, oy] of offsets) {
ctx.beginPath();
ctx.arc(t.x + ox, t.y + oy, 7, 0, Math.PI * 2);
ctx.fill();
}
// Table
const wobble = uCount === 2 ? Math.sin(visualTime * 20 + t.x) * 2.5 : 0;
ctx.fillStyle = uCount === 2 ? '#6f42c1' : '#5c4738';
ctx.strokeStyle = '#9c7a61';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.arc(t.x + wobble, t.y, t.r, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
}
// Counter
ctx.fillStyle = uCount === 2 ? '#3f1d5c' : '#005b3c';
ctx.strokeStyle = uCount >= 1 ? '#64dfdf' : '#1db954';
ctx.lineWidth = 2.5;
ctx.fillRect(COUNTER.x, COUNTER.y, COUNTER.w, COUNTER.h);
ctx.strokeRect(COUNTER.x, COUNTER.y, COUNTER.w, COUNTER.h);
// Espresso & Blender station on counter
ctx.fillStyle = '#9aa8b8';
ctx.fillRect(345, 96, 36, 22);
// Blender pitcher
const activeCust = insideCustomers.find(c => c.state === 'queue');
const blending = activeCust && activeCust.serveTimer > 0;
ctx.fillStyle = blending ? '#ff6bb5' : '#4a5a6e';
ctx.beginPath();
ctx.arc(408, 107, 11, 0, Math.PI * 2);
ctx.fill();
if (blending) {
ctx.strokeStyle = '#ffe600';
ctx.lineWidth = 3;
ctx.beginPath();
ctx.arc(408, 107, 15, -Math.PI / 2, -Math.PI / 2 + (activeCust.serveTimer / SERVICE_TIME) * Math.PI * 2);
ctx.stroke();
}
// Barista
const bJitter = uCount === 2 ? (Math.random() - 0.5) * 3 : 0;
ctx.fillStyle = uCount === 2 ? '#ff4da6' : '#00704a';
ctx.strokeStyle = '#e6edf3';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.arc(baristaPos.x + bJitter, baristaPos.y + bJitter, 12, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 9px system-ui, sans-serif';
ctx.textAlign = 'center';
ctx.fillText('BARISTA', baristaPos.x, baristaPos.y - 16);
// Walls & Doors
ctx.strokeStyle = uCount === 2 ? `hsl(${(visualTime * 300) % 360}, 100%, 60%)` : '#46566b';
ctx.lineWidth = 7;
ctx.lineCap = 'square';
// Top wall
ctx.beginPath(); ctx.moveTo(SHOP.x, SHOP.y); ctx.lineTo(SHOP.x + SHOP.w, SHOP.y); ctx.stroke();
// Right wall
ctx.beginPath(); ctx.moveTo(SHOP.x + SHOP.w, SHOP.y); ctx.lineTo(SHOP.x + SHOP.w, SHOP.y + SHOP.h); ctx.stroke();
// Left wall (gap at ENTRANCE y=430..480)
ctx.beginPath(); ctx.moveTo(SHOP.x, SHOP.y); ctx.lineTo(SHOP.x, ENTRANCE.y - 26); ctx.stroke();
ctx.beginPath(); ctx.moveTo(SHOP.x, ENTRANCE.y + 26); ctx.lineTo(SHOP.x, SHOP.y + SHOP.h); ctx.stroke();
// Bottom wall (gap at EXIT_DOOR x=714..766)
ctx.beginPath(); ctx.moveTo(SHOP.x, SHOP.y + SHOP.h); ctx.lineTo(EXIT_DOOR.x - 26, SHOP.y + SHOP.h); ctx.stroke();
ctx.beginPath(); ctx.moveTo(EXIT_DOOR.x + 26, SHOP.y + SHOP.h); ctx.lineTo(SHOP.x + SHOP.w, SHOP.y + SHOP.h); ctx.stroke();
// Door frames & labels
ctx.strokeStyle = '#00f0ff';
ctx.lineWidth = 2;
ctx.beginPath(); ctx.moveTo(ENTRANCE.x, ENTRANCE.y - 24); ctx.lineTo(ENTRANCE.x + 14, ENTRANCE.y - 10); ctx.stroke();
ctx.beginPath(); ctx.moveTo(EXIT_DOOR.x - 24, EXIT_DOOR.y); ctx.lineTo(EXIT_DOOR.x - 10, EXIT_DOOR.y + 14); ctx.stroke();
ctx.fillStyle = '#8b9bb0';
ctx.font = 'bold 10px system-ui, sans-serif';
ctx.textAlign = 'left';
ctx.fillText('IN ->', ENTRANCE.x + 8, ENTRANCE.y + 4);
ctx.textAlign = 'center';
ctx.fillText('EXIT ↓', EXIT_DOOR.x, EXIT_DOOR.y + 26);
// Inside occupancy badge
ctx.fillStyle = 'rgba(13, 17, 23, 0.85)';
ctx.fillRect(SHOP.x + 14, SHOP.y + 12, 128, 24);
ctx.strokeStyle = '#2d3a4b';
ctx.lineWidth = 1;
ctx.strokeRect(SHOP.x + 14, SHOP.y + 12, 128, 24);
ctx.fillStyle = '#e6edf3';
ctx.font = 'bold 11px system-ui, sans-serif';
ctx.textAlign = 'left';
ctx.fillText(`INSIDE: ${insideCustomers.length} / ${MAX_INSIDE}`, SHOP.x + 22, SHOP.y + 28);
// Energy arc tether if 2 unicorns inside
if (uCount === 2) {
const uHolders = insideCustomers.filter(c => c.hasDrink && c.isUnicorn);
if (uHolders.length >= 2) {
const a = uHolders[0], b = uHolders[1];
ctx.strokeStyle = `hsl(${(visualTime * 400) % 360}, 100%, 70%)`;
ctx.lineWidth = 2.5;
ctx.beginPath();
ctx.moveTo(a.x, a.y);
for (let s = 1; s < 6; s++) {
const frac = s / 6;
const px = a.x + (b.x - a.x) * frac + (Math.random() - 0.5) * 18;
const py = a.y + (b.y - a.y) * frac + (Math.random() - 0.5) * 18;
ctx.lineTo(px, py);
}
ctx.lineTo(b.x, b.y);
ctx.stroke();
}
}
ctx.restore();
}
// Outside queue customers
for (const c of outsideQueue) {
drawCustomerDot(c, false);
}
// Inside customers
if (phase === 'RUNNING') {
for (const c of insideCustomers) {
drawCustomerDot(c, uCount === 2);
}
}
// Catastrophe crater, debris, panic dots & GIANT DANCING UNICORN
if (phase === 'EVENT') {
// Glowing neon crater
const cg = ctx.createRadialGradient(485, 300, 10, 485, 300, 270);
cg.addColorStop(0, `hsla(${(visualTime * 300) % 360}, 100%, 55%, 0.5)`);
cg.addColorStop(1, 'rgba(0,0,0,0)');
ctx.fillStyle = cg;
ctx.beginPath();
ctx.arc(485, 300, 270, 0, Math.PI * 2);
ctx.fill();
// Flying debris
for (const d of debris) {
ctx.save();
ctx.translate(d.x, d.y);
ctx.rotate(d.rot);
ctx.fillStyle = d.color;
ctx.strokeStyle = 'rgba(255,255,255,0.45)';
ctx.lineWidth = 1.5;
ctx.fillRect(-d.w / 2, -d.h / 2, d.w, d.h);
ctx.strokeRect(-d.w / 2, -d.h / 2, d.w, d.h);
ctx.restore();
}
// Fleeing customer dots
for (const p of panicDots) {
ctx.fillStyle = p.isUnicorn ? `hsl(${(visualTime * 360) % 360}, 100%, 65%)` : p.color;
ctx.strokeStyle = '#ffffff';
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
}
// Giant Dancing Neon Unicorn!
if (phaseTimer > 0.35) {
drawGiantDancingUnicorn(phaseTimer);
}
// Event banner
ctx.save();
ctx.textAlign = 'center';
ctx.font = '900 32px system-ui, sans-serif';
const txtHue = (visualTime * 420) % 360;
ctx.fillStyle = '#000';
ctx.fillText(`NEON UNICORN EVENT #${stats.events}!`, W / 2 + 3, 62);
ctx.fillStyle = `hsl(${txtHue}, 100%, 65%)`;
ctx.fillText(`NEON UNICORN EVENT #${stats.events}!`, W / 2, 59);
ctx.font = 'bold 14px system-ui, sans-serif';
ctx.fillStyle = '#ffffff';
ctx.fillText('THREE UNICORNS IN THE ROOM — REALITY COMPROMISED', W / 2, 84);
ctx.restore();
}
// Level 2 hazard warning border
if (phase === 'RUNNING' && uCount === 2) {
ctx.strokeStyle = Math.sin(visualTime * 22) > 0 ? '#ff2a85' : '#ffe600';
ctx.lineWidth = 6;
ctx.strokeRect(3, 3, W - 6, H - 6);
ctx.fillStyle = 'rgba(255, 42, 133, 0.88)';
ctx.fillRect(W / 2 - 205, 10, 410, 26);
ctx.fillStyle = '#ffffff';
ctx.font = '900 12px system-ui, sans-serif';
ctx.textAlign = 'center';
ctx.fillText('CRITICAL DENSITY: 2 UNICORNS INSIDE — ONE MORE TRIGGERS EVENT!', W / 2, 27);
}
// Particles
for (const p of particles) {
const alpha = Math.max(0, p.life / p.maxLife);
ctx.globalAlpha = alpha;
ctx.fillStyle = p.color;
ctx.beginPath();
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
ctx.fill();
}
ctx.globalAlpha = 1;
// Floating toast texts
for (const t of floatingTexts) {
ctx.globalAlpha = Math.max(0, t.life / t.maxLife);
ctx.font = `900 ${t.size}px system-ui, sans-serif`;
ctx.textAlign = 'center';
ctx.fillStyle = '#000';
ctx.fillText(t.text, t.x + 1.5, t.y + 1.5);
ctx.fillStyle = t.color;
ctx.fillText(t.text, t.x, t.y);
}
ctx.globalAlpha = 1;
// Initial detonation flash
if (flashAlpha > 0) {
ctx.fillStyle = `rgba(255, 220, 255, ${flashAlpha})`;
ctx.fillRect(0, 0, W, H);
}
ctx.restore();
}
// UI Sync
function updateUI() {
const uCount = phase === 'EVENT' ? 3 : countUnicornsInside();
const p1 = document.getElementById('pip1');
const p2 = document.getElementById('pip2');
const p3 = document.getElementById('pip3');
p1.className = 'pip' + (uCount >= 1 ? (uCount === 1 ? ' lit-1' : uCount === 2 ? ' lit-2' : ' lit-3') : '');
p2.className = 'pip' + (uCount >= 2 ? (uCount === 2 ? ' lit-2' : ' lit-3') : '');
p3.className = 'pip' + (uCount >= 3 ? ' lit-3' : '');
const threat = document.getElementById('threatStatus');
if (phase === 'EVENT') {
threat.textContent = 'CATASTROPHE • NEON UNICORN EVENT!';
threat.style.color = '#ff6bb5';
} else if (phase === 'REBUILD') {
threat.textContent = 'REBUILDING • Sweeping up glitter...';
threat.style.color = '#1db954';
} else if (uCount === 0) {
threat.textContent = 'CALM • 0 Neon Unicorns inside';
threat.style.color = '#8b9bb0';
} else if (uCount === 1) {
threat.textContent = 'SUBTLE WRONGNESS • 1 Neon Unicorn inside';
threat.style.color = '#00f0ff';
} else {
threat.textContent = 'UNSTABLE! • 2 Neon Unicorns inside!';
threat.style.color = '#ffe600';
}
document.getElementById('insideRatioText').textContent = `${insideCustomers.length} / ${MAX_INSIDE} inside`;
document.getElementById('statServed').textContent = stats.served.toLocaleString();
document.getElementById('statLine').textContent = `Outside line: ${outsideQueue.length}`;
document.getElementById('statUnicorns').textContent = stats.unicorns.toLocaleString();
document.getElementById('statSince').textContent = `${stats.sinceLastEvent} served since event`;
const obsRate = stats.served > 0 ? (stats.unicorns / stats.served) * 100 : 0;
document.getElementById('statRate').textContent = `${obsRate.toFixed(2)}%`;
const barPct = Math.min(100, (obsRate / 33.3333) * 100);
document.getElementById('rateBar').style.width = `${barPct}%`;
if (stats.served > 0) {
const diff = obsRate - 16.6667;
const sign = diff >= 0 ? '+' : '';
document.getElementById('statLuck').textContent = `${sign}${diff.toFixed(2)}% vs 16.67%`;
} else {
document.getElementById('statLuck').textContent = 'Awaiting first pour';
}
document.getElementById('statEvents').textContent = stats.events.toLocaleString();
if (stats.eventGaps.length > 0) {
const avg = stats.eventGaps.reduce((a, b) => a + b, 0) / stats.eventGaps.length;
document.getElementById('statAvgGap').textContent = `Avg gap: ${avg.toFixed(1)} cust`;
} else {
document.getElementById('statAvgGap').textContent = 'Avg gap: —';
}
document.getElementById('statNearMiss').textContent = stats.nearMisses.toLocaleString();
document.getElementById('statShortest').textContent = stats.shortestGap !== null
? `Shortest gap: ${stats.shortestGap} cust`
: 'Shortest gap: —';
document.getElementById('statDamage').textContent = `$${(stats.events * 42500).toLocaleString()}`;
document.getElementById('statLongest').textContent = `Longest calm: ${stats.longestCalm} cust`;
document.getElementById('statSips').textContent = `${stats.totalSips.toLocaleString()} sips taken`;
let morale = 'Chill';
if (phase === 'EVENT') morale = 'Vaporized';
else if (uCount === 2) morale = 'Panicking';
else if (uCount === 1) morale = 'Uneasy';
else if (stats.events >= 3) morale = 'Numb';
document.getElementById('statMorale').textContent = morale;
const mins = Math.floor(simTime / 60);
const secs = Math.floor(simTime % 60);
document.getElementById('simClock').textContent =
`${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
}
// Controls
document.getElementById('speedButtons').addEventListener('click', (e) => {
const btn = e.target.closest('button[data-speed]');
if (!btn) return;
simSpeed = parseFloat(btn.dataset.speed);
for (const b of document.querySelectorAll('#speedButtons button')) b.classList.remove('active');
btn.classList.add('active');
});
const pauseBtn = document.getElementById('pauseBtn');
pauseBtn.addEventListener('click', () => {
paused = !paused;
pauseBtn.textContent = paused ? 'Resume' : 'Pause';
pauseBtn.classList.toggle('active', paused);
});
const soundBtn = document.getElementById('soundBtn');
soundBtn.addEventListener('click', () => {
soundEnabled = !soundEnabled;
soundBtn.textContent = soundEnabled ? 'Sound: On' : 'Sound: Off';
soundBtn.classList.toggle('active', soundEnabled);
if (soundEnabled) playTone(440, 0.08, 'sine', 0.05);
});
document.getElementById('resetStatsBtn').addEventListener('click', () => {
simTime = 0;
nextCustId = 1;
outsideQueue = [];
insideCustomers = [];
particles = [];
floatingTexts = [];
debris = [];
panicDots = [];
logs = [];
phase = 'RUNNING';
phaseTimer = 0;
doorCooldown = 0;
spawnTimer = 0.2;
shakeIntensity = 0;
flashAlpha = 0;
stats.served = 0;
stats.unicorns = 0;
stats.events = 0;
stats.nearMisses = 0;
stats.sinceLastEvent = 0;
stats.longestCalm = 0;
stats.shortestGap = null;
stats.totalSips = 0;
stats.eventGaps = [];
seedInitialLine();
addLog('Simulation reset. Mystery promo is live.');
updateUI();
});
// Fixed-timestep loop so fast-forwarding never breaks occupancy or dice logic
let lastTs = performance.now();
let accum = 0;
let uiAccum = 0;
const FIXED_DT = 1 / 60;
function frame(now) {
const rawDt = Math.min(0.05, (now - lastTs) / 1000);
lastTs = now;
if (!paused) {
visualTime += rawDt;
// During explosions at high simSpeed, scale animation smoothly so it's still visible
const effectiveSpeed = phase === 'RUNNING'
? simSpeed
: (simSpeed <= 2 ? simSpeed : Math.min(simSpeed * 0.5, 8));
accum += rawDt * effectiveSpeed;
let steps = 0;
const maxSteps = 220;
while (accum >= FIXED_DT && steps < maxSteps) {
accum -= FIXED_DT;
steps++;
if (phase === 'RUNNING') stepRunning(FIXED_DT);
else if (phase === 'EVENT') stepEvent(FIXED_DT);
else if (phase === 'REBUILD') stepRebuild(FIXED_DT);
stepParticlesAndText(FIXED_DT);
}
if (steps >= maxSteps) accum = 0;
}
render();
uiAccum += rawDt;
if (uiAccum >= 0.08) {
uiAccum = 0;
updateUI();
}
requestAnimationFrame(frame);
}
seedInitialLine();
updateUI();
requestAnimationFrame(frame);
})();
</script>
</body>
</html>
In the first few minutes at 1x speed, an initial line of customers files through the door up to the 8-customer limit, settles into a steady cadence of one Frappuccino blended every 1.2 seconds, and leaves the excess customers queued along the outside sidewalk rope. Almost immediately the room spends most of its time in the 1-unicorn state (subtle purple floor tint, logo horn, espresso sparks) punctuated by tense 2-unicorn stretches (strobing lights, shaking building, hazard border, energy arc connecting the two unicorn holders) that frequently resolve in a "PHEW! CRISIS AVERTED" near-miss when one holder finishes their 7.2-second drink and exits. Typically within the first minute or two—or every couple of seconds at 10x–25x—a third Neon Unicorn lands while two are still inside, shattering the counter, walls, and tables into spinning debris, scattering the dots, and summoning the giant dancing neon unicorn with laser beams before the shop rebuilds and resumes from zero.
No odds were nudged: every drink is decided purely by Math.random() < 1 / 6 at the counter with no timer, quota, or pity mechanic. Two pacing details were tuned explicitly: first, the barista’s 1.2s service timer begins as soon as the front customer steps within 75px of the counter (while they finish walking the last few pixels) so queue step-up time doesn’t silently inflate the interval between pours; second, during explosions and rebuilds at high fast-forward speeds (>2x), the catastrophe animation speed is capped (min(simSpeed * 0.5, 8)) so the giant unicorn remains visible instead of vanishing in a single frame.
What it does badly: because the outside sidewalk queue is capped at 14 spots and new arrivals spawn slightly faster (~1.075s average) than the barista serves (1.2s), the outside line stays near-full and effectively stops growing once it hits 14 rather than modeling impatient customers leaving; customers inside walk straight toward their queue or table coordinates without collision avoidance, so dots briefly clip past each other in the center aisle; and because catastrophe rebuilds clear everyone out of the shop immediately, the very first 7–8 customers after a rebuild enter slightly staggered before the steady-state 6-drink-holder window locks back in.