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.
I use a 7.0 sim-second in-shop drink-holding lifetime and a counter handoff every 1.8 sim-seconds while a queue exists; after a handoff, the previous cups still inside are aged 1.8, 3.6 and 5.4 seconds, while the 7.2-second-old one has just left, so each roll effectively checks a four-drink window. With an independent d6 per drink and face 1 = Neon Unicorn, p = 1/6, the single-window chance of 3-or-more is C(4,3)p³(1−p)+p⁴ = 20/1296+1/1296 = 21/1296 ≈ 1.62%, which naively suggests 1/0.0162 ≈ 62 drinks, but the windows overlap and every rebuild starts with zero neon holders; solving E_s = 1 + qE_{s,0} + pE_{s,1} over the eight “last three roll” bit states gives a clean-start mean of 56,478/521 ≈ 108 served customers to the first 3-in-room hit, i.e. about 108 × 1.8 ≈ 195 sim-seconds of active selling, plus a no-roll rebuild pause; speed only compresses wall-clock time, not the true 1/6 chance, so the practical answer is “order of 100 customers between catastrophes, with a very wide tail.”
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
<title>Siren-ish Coffee: Neon Unicorn Promotion</title>
<style>
* { box-sizing: border-box; }
html, body, #app {
margin: 0;
width: 100%;
height: 100%;
overflow: hidden;
background: #070611;
}
body {
color: #eafff8;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
}
#c {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
display: block;
}
#hud {
position: absolute;
top: 14px;
left: 14px;
width: 372px;
max-width: calc(100% - 28px);
max-height: calc(100% - 28px);
overflow: auto;
padding: 12px;
border: 1px solid rgba(126,255,221,.28);
border-radius: 14px;
background: linear-gradient(145deg, rgba(7,12,24,.88), rgba(18,8,34,.84));
box-shadow: 0 0 32px rgba(0,255,190,.14), inset 0 0 24px rgba(255,0,220,.06);
pointer-events: auto;
}
.brand { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
.logo {
display: grid;
place-items: center;
width: 38px;
height: 38px;
border-radius: 50%;
color: #071d16;
background: radial-gradient(circle, #aaffdd 0 35%, #16b877 36% 72%, #063d2c 73%);
box-shadow: 0 0 15px #35ffbd;
font-weight: 900;
font-size: 22px;
}
h1 { margin: 0; font-size: 15px; letter-spacing: .08em; }
.tagline { margin: 2px 0 0; color: #8fcabb; font-size: 10px; line-height: 1.25; }
#status {
margin: 8px 0 6px;
padding: 7px 9px;
border: 1px solid rgba(255,255,255,.13);
border-radius: 9px;
background: rgba(255,255,255,.045);
color: #c7fff0;
font-size: 12px;
font-weight: 800;
letter-spacing: .03em;
}
#ticker {
min-height: 32px;
margin: 0 0 8px;
padding: 7px 9px;
border-left: 3px solid #46ffd1;
border-radius: 7px;
background: rgba(50,255,200,.06);
color: #d7fff4;
font-size: 10px;
line-height: 1.35;
}
.stats {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 6px;
}
.stat {
min-width: 0;
padding: 7px 8px;
border: 1px solid rgba(255,255,255,.10);
border-radius: 9px;
background: rgba(0,0,0,.20);
}
.stat span {
display: block;
overflow: hidden;
color: #87aaad;
font-size: 8px;
letter-spacing: .07em;
white-space: nowrap;
text-overflow: ellipsis;
}
.stat b {
display: block;
margin-top: 3px;
overflow: hidden;
color: #fff;
font-size: 13px;
white-space: nowrap;
text-overflow: ellipsis;
}
#controls {
display: flex;
align-items: center;
gap: 8px;
margin-top: 9px;
padding: 8px;
border: 1px solid rgba(126,255,221,.18);
border-radius: 10px;
background: rgba(0,255,180,.045);
font-size: 10px;
}
#controls label { flex: 1; color: #b9ffec; }
#speed { width: 100%; margin-top: 5px; accent-color: #45ffc8; }
#speedLabel { color: #fff; font-size: 12px; }
button {
padding: 7px 10px;
border: 1px solid rgba(255,255,255,.22);
border-radius: 8px;
color: #eafff8;
background: rgba(255,255,255,.08);
font: inherit;
cursor: pointer;
}
button:hover { background: rgba(255,255,255,.16); }
.rules {
margin-top: 8px;
color: #9bc5be;
font-size: 9px;
line-height: 1.35;
}
.rules strong { color: #fff; }
#cornerBadge {
position: absolute;
right: 16px;
bottom: 14px;
padding: 8px 11px;
border: 1px solid rgba(255,80,220,.35);
border-radius: 999px;
color: #ffd8fa;
background: rgba(24,4,31,.72);
box-shadow: 0 0 18px rgba(255,0,200,.18);
font-size: 10px;
letter-spacing: .08em;
pointer-events: none;
}
@media (max-width: 720px) {
#hud { left: 9px; top: 9px; width: 392px; transform: scale(.82); transform-origin: top left; }
#cornerBadge { display: none; }
}
</style>
</head>
<body>
<div id="app">
<canvas id="c" aria-label="A live geometric simulation of a coffee shop and its unsafe neon drink promotion"></canvas>
<section id="hud">
<div class="brand">
<div class="logo">✦</div>
<div>
<h1>SIREN-ISH COFFEE</h1>
<p class="tagline">Frappuccino compliance simulation · “definitely not a curse”</p>
</div>
</div>
<div id="status">REALITY: nominal, legally speaking</div>
<div id="ticker">Promotion opened. Corporate confidence is currently a number.</div>
<div class="stats">
<div class="stat"><span>CUSTOMERS SERVED</span><b id="served">0</b></div>
<div class="stat"><span>NEON UNICORNS PRODUCED</span><b id="neon">0</b></div>
<div class="stat"><span>OBSERVED / TRUE</span><b id="rate">— / 16.667%</b></div>
<div class="stat"><span>CATASTROPHES</span><b id="cats">0</b></div>
<div class="stat"><span>INSIDE HOLDING NEON / 3</span><b id="pressure">0 / 3</b></div>
<div class="stat"><span>INSIDE CAP / OUTSIDE LINE</span><b id="occupancy">0/8 · 0</b></div>
<div class="stat"><span>LAST COUNTER D6</span><b id="die">—</b></div>
<div class="stat"><span>NEAR MISSES: 2 AT ONCE</span><b id="near">0</b></div>
<div class="stat"><span>UNSTABLE COINCIDENCE TIME</span><b id="unstable">0.0s</b></div>
<div class="stat"><span>LONGEST NORMAL STREAK</span><b id="dry">0</b></div>
<div class="stat"><span>LINE WALKOUTS</span><b id="walkouts">0</b></div>
<div class="stat"><span>MEAN CUPS / EVENT</span><b id="avg">—</b></div>
<div class="stat"><span>SERVED SINCE EVENT</span><b id="since">0</b></div>
<div class="stat"><span>CORPORATE CONFIDENCE</span><b id="confidence">100%</b></div>
<div class="stat"><span>LIABILITY GLITTER</span><b id="glitter">0 kg</b></div>
</div>
<div id="controls">
<label>FAST-FORWARD TO NEXT DISASTER
<input id="speed" type="range" min="0" max="6" step="1" value="2">
</label>
<b id="speedLabel">1×</b>
<button id="pause" type="button">PAUSE</button>
</div>
<div class="rules">
<strong>ODDS:</strong> one independent counter d6 per Frappuccino; face 1 produces a Neon Unicorn.
<strong>EVENT TRIGGER:</strong> the count of customers currently inside holding a Neon Unicorn reaching 3.
No pity, no quota, no loaded die, no event schedule. Visual randomness is cosmetic only.
</div>
</section>
<div id="cornerBadge">LIVE DICE · NO SCRIPTED CATASTROPHE</div>
</div>
<script>
(function () {
'use strict';
const $ = function (id) { return document.getElementById(id); };
const canvas = $('c');
const ctx = canvas.getContext('2d');
const W = 1200;
const H = 700;
const TAU = Math.PI * 2;
const TRUE_P = 1 / 6;
const SERVE_INTERVAL = 1.8;
const HOLD_TIME = 7.0;
const ARRIVAL_INTERVAL = 1.74;
const ADMIT_INTERVAL = 0.30;
const MAX_INSIDE = 8;
const MAX_LINE = 60;
const CAT_DURATION = 10.8;
const SPEEDS = [0.25, 0.5, 1, 2, 4, 8, 16];
let speedIndex = 2;
let paused = false;
let dpr = 1;
const SHOP = { x: 300, y: 170, w: 660, h: 400 };
const COUNTER_Q0 = { x: 810, y: 485 };
const EXIT = { x: 600, y: 610 };
let time = 0;
let spawnTimer = 0;
let admitTimer = 0;
let serviceTimer = 0;
let ambientCarry = 0;
let nextId = 1;
let outside = [];
let queue = [];
let holders = [];
let particles = [];
let floaters = [];
let currentNeon = 0;
let lastPressure = 0;
let lastTicker = '';
const cat = { active: false, t: 0, count: 0, cleared: false };
const stats = {
served: 0,
neon: 0,
catastrophes: 0,
nearMisses: 0,
longestDry: 0,
dry: 0,
lastDie: 0,
lastNeon: false,
lastEventServed: 0,
eventCupsSum: 0,
eventCupsCount: 0,
walkouts: 0,
unstableSeconds: 0,
maxNeon: 0,
glitter: 0
};
function resize() {
dpr = Math.min(2, window.devicePixelRatio || 1);
canvas.width = Math.max(1, Math.floor(window.innerWidth * dpr));
canvas.height = Math.max(1, Math.floor(window.innerHeight * dpr));
}
window.addEventListener('resize', resize);
resize();
const speedInput = $('speed');
speedInput.max = String(SPEEDS.length - 1);
speedInput.value = String(speedIndex);
speedInput.addEventListener('input', function () {
speedIndex = clamp(Number(speedInput.value), 0, SPEEDS.length - 1);
updateHud();
});
$('pause').addEventListener('click', function () {
paused = !paused;
$('pause').textContent = paused ? 'RESUME' : 'PAUSE';
});
function clamp(v, a, b) { return v < a ? a : (v > b ? b : v); }
function mix(a, b, t) { return a + (b - a) * t; }
function ease(t) { t = clamp(t, 0, 1); return t * t * (3 - 2 * t); }
function fract(n) { const x = Math.sin(n * 12.9898) * 43758.5453; return x - Math.floor(x); }
function hsla(h, s, l, a) {
h = ((h % 360) + 360) % 360;
if (a === undefined) a = 1;
return 'hsla(' + h + ',' + s + '%,' + l + '%,' + a + ')';
}
function fmt(n) {
return String(Math.round(n)).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}
function pathRoundRect(x, y, w, h, r) {
r = Math.max(0, Math.min(r, Math.abs(w) / 2, Math.abs(h) / 2));
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.lineTo(x + w - r, y);
ctx.quadraticCurveTo(x + w, y, x + w, y + r);
ctx.lineTo(x + w, y + h - r);
ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
ctx.lineTo(x + r, y + h);
ctx.quadraticCurveTo(x, y + h, x, y + h - r);
ctx.lineTo(x, y + r);
ctx.quadraticCurveTo(x, y, x + r, y);
ctx.closePath();
}
function roundRect(x, y, w, h, r, fill, stroke, lw) {
pathRoundRect(x, y, w, h, r);
if (fill) { ctx.fillStyle = fill; ctx.fill(); }
if (stroke) { ctx.strokeStyle = stroke; ctx.lineWidth = lw || 1; ctx.stroke(); }
}
function starPath(x, y, r1, r2, points) {
ctx.beginPath();
for (let i = 0; i < points * 2; i++) {
const a = -Math.PI / 2 + i * Math.PI / points;
const r = i % 2 === 0 ? r2 : r1;
const px = x + Math.cos(a) * r;
const py = y + Math.sin(a) * r;
if (i === 0) ctx.moveTo(px, py); else ctx.lineTo(px, py);
}
ctx.closePath();
}
function moveToward(o, tx, ty, dt, speed) {
const dx = tx - o.x;
const dy = ty - o.y;
const d = Math.sqrt(dx * dx + dy * dy);
if (d < 0.001) return;
const k = Math.min(1, speed * dt / d);
o.x += dx * k;
o.y += dy * k;
}
function outsideTarget(i) {
const per = 11;
const row = Math.floor(i / per);
const col = i % per;
const x = 530 - col * 46 - row * 9;
const y = Math.min(682, 610 + row * 25);
return { x: Math.max(22, x), y: y };
}
function queueTarget(i) {
const x = clamp(810 - i * 55, 335, 810);
const y = 485 + (i % 2) * 7;
return { x: x, y: y };
}
const HOLDER_SPOTS = [
{ x: 370, y: 350 }, { x: 465, y: 380 }, { x: 550, y: 335 },
{ x: 390, y: 470 }, { x: 510, y: 455 }, { x: 665, y: 345 },
{ x: 680, y: 470 }, { x: 335, y: 410 }
];
function holderSpot(id) {
const s = HOLDER_SPOTS[id % HOLDER_SPOTS.length];
return {
x: s.x + ((id * 17) % 13) - 6,
y: s.y + ((id * 11) % 9) - 4
};
}
function makeCustomer() {
const id = nextId++;
return {
id: id,
hue: (id * 67 + 21) % 360,
phase: fract(id * 91.7) * TAU,
x: outsideTarget(0).x,
y: outsideTarget(0).y,
neon: false,
holdLeft: 0,
holdTotal: HOLD_TIME,
sx: 0,
sy: 0,
state: 'outside'
};
}
function say(text, hue) {
lastTicker = text;
const t = $('ticker');
t.textContent = text;
t.style.borderColor = hsla(hue === undefined ? 160 : hue, 100, 65, .9);
t.style.background = hsla(hue === undefined ? 160 : hue, 100, 55, .07);
}
function addFloater(x, y, text, hue, life, size) {
if (floaters.length > 42) floaters.shift();
floaters.push({
x: x, y: y, text: text, hue: hue === undefined ? 160 : hue,
life: life || 1.5, maxLife: life || 1.5,
vy: -28 - Math.random() * 16,
size: size || 15
});
}
function addParticle(p) {
if (particles.length > 900) particles.shift();
particles.push(p);
}
function spawnCustomer() {
if (outside.length >= MAX_LINE) {
stats.walkouts++;
return;
}
const c = makeCustomer();
const p = outsideTarget(outside.length);
c.x = p.x + 14;
c.y = p.y + 8;
outside.push(c);
}
function seed() {
for (let i = 0; i < 22; i++) {
const c = makeCustomer();
const p = outsideTarget(i);
c.x = p.x;
c.y = p.y;
outside.push(c);
}
say('Promotion opened. Legal has already stopped reading email.', 150);
}
function admitOne() {
if (outside.length === 0) return false;
if (queue.length + holders.length >= MAX_INSIDE) return false;
const c = outside.shift();
c.state = 'queue';
queue.push(c);
return true;
}
function countNeonHolders() {
let n = 0;
for (let i = 0; i < holders.length; i++) if (holders[i].neon) n++;
return n;
}
function cupBurst(neon, x, y, id) {
const n = neon ? 24 : 5;
for (let i = 0; i < n; i++) {
const a = Math.random() * TAU;
const sp = (neon ? 60 : 24) + Math.random() * (neon ? 170 : 50);
addParticle({
x: x, y: y,
vx: Math.cos(a) * sp,
vy: Math.sin(a) * sp - (neon ? 55 : 12),
g: neon ? 30 : 90,
life: neon ? 1.3 + Math.random() * .8 : .45 + Math.random() * .35,
maxLife: neon ? 2.1 : .8,
size: neon ? 2 + Math.random() * 4 : 2 + Math.random() * 2,
hue: neon ? (time * 150 + id * 47 + i * 19) % 360 : 34,
kind: neon ? 'spark' : 'crumb',
rot: Math.random() * TAU,
vr: (Math.random() - .5) * 5
});
}
}
function catastropheBurst() {
stats.glitter += 12.5;
for (let i = 0; i < 260; i++) {
const x = 300 + Math.random() * 660;
const y = 170 + Math.random() * 400;
const a = Math.random() * TAU;
const sp = 80 + Math.random() * 560;
addParticle({
x: x, y: y,
vx: Math.cos(a) * sp,
vy: Math.sin(a) * sp - 120 - Math.random() * 250,
g: 220 + Math.random() * 220,
life: 1.5 + Math.random() * 2.4,
maxLife: 3.9,
size: 2 + Math.random() * 9,
hue: (i * 37 + Math.random() * 80 + time * 120) % 360,
kind: i % 4 === 0 ? 'debris' : (i % 3 === 0 ? 'confetti' : 'spark'),
rot: Math.random() * TAU,
vr: (Math.random() - .5) * 12
});
}
}
function spawnAmbient(pressure) {
const catMode = cat.active;
const x = catMode ? 600 + (Math.random() - .5) * 500 : SHOP.x + Math.random() * SHOP.w;
const y = catMode ? 300 + (Math.random() - .5) * 300 : 250 + Math.random() * 300;
const hot = pressure >= 2;
addParticle({
x: x, y: y,
vx: (Math.random() - .5) * (hot ? 90 : 25),
vy: -20 - Math.random() * (hot ? 110 : 32),
g: hot ? 20 : -5,
life: .7 + Math.random() * (catMode ? 2.0 : 1.1),
maxLife: catMode ? 2.7 : 1.8,
size: 1 + Math.random() * (catMode ? 6 : 3),
hue: (time * (catMode ? 220 : 100) + Math.random() * 120) % 360,
kind: catMode && Math.random() < .2 ? 'confetti' : 'spark',
rot: Math.random() * TAU,
vr: (Math.random() - .5) * 5
});
}
function updateParticles(dt) {
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
p.life -= dt;
if (p.life <= 0) {
particles.splice(i, 1);
continue;
}
p.vy += (p.g || 0) * dt;
p.x += p.vx * dt;
p.y += p.vy * dt;
p.rot += (p.vr || 0) * dt;
if (p.y > H + 60) p.life = Math.min(p.life, .15);
}
for (let i = floaters.length - 1; i >= 0; i--) {
const f = floaters[i];
f.life -= dt;
f.y += f.vy * dt;
if (f.life <= 0) floaters.splice(i, 1);
}
const pressure = cat.active ? 3 : currentNeon;
const rate = pressure >= 3 ? 62 : pressure === 2 ? 22 : pressure === 1 ? 2.2 : 0;
ambientCarry += rate * dt;
while (ambientCarry >= 1) {
spawnAmbient(pressure);
ambientCarry -= 1;
}
}
function updateOutside(dt) {
for (let i = 0; i < outside.length; i++) {
const p = outsideTarget(i);
moveToward(outside[i], p.x, p.y, dt, 210);
}
}
function updateQueue(dt) {
for (let i = 0; i < queue.length; i++) {
const p = queueTarget(i);
moveToward(queue[i], p.x, p.y, dt, 380);
}
}
function updateHolders(dt) {
const survivors = [];
for (let i = 0; i < holders.length; i++) {
const h = holders[i];
h.holdLeft -= dt;
if (h.holdLeft <= 0) {
if (h.neon) {
addParticle({
x: h.x, y: h.y, vx: (Math.random() - .5) * 40, vy: -50,
g: 20, life: .5, maxLife: .5, size: 3,
hue: (time * 140 + h.id * 37) % 360, kind: 'spark',
rot: 0, vr: 0
});
}
continue;
}
const p = 1 - h.holdLeft / HOLD_TIME;
const spot = holderSpot(h.id);
let tx, ty;
if (p < .18) {
const k = ease(p / .18);
tx = mix(h.sx, spot.x, k);
ty = mix(h.sy, spot.y, k);
} else if (p < .70) {
tx = spot.x + Math.sin(time * 2.2 + h.phase) * 3;
ty = spot.y + Math.cos(time * 2.7 + h.phase) * 2;
} else {
const k = ease((p - .70) / .30);
tx = mix(spot.x, EXIT.x, k);
ty = mix(spot.y, EXIT.y, k);
}
h.x = tx;
h.y = ty + Math.sin(time * 8 + h.phase) * (h.neon ? 2.5 : .8);
survivors.push(h);
}
holders = survivors;
}
function triggerCatastrophe(n) {
if (cat.active) return;
cat.active = true;
cat.t = 0;
cat.count = n;
cat.cleared = false;
serviceTimer = 0;
currentNeon = n;
lastPressure = n;
stats.catastrophes++;
const cycle = stats.served - stats.lastEventServed;
stats.eventCupsSum += cycle;
stats.eventCupsCount++;
stats.lastEventServed = stats.served;
catastropheBurst();
addFloater(600, 170, 'THREE IN ROOM · ZERO IN POLICY', 320, 2.4, 22);
say('EVENT: ' + n + ' in-room Neon Unicorns. The building has resigned.', 320);
}
function syncReality() {
if (cat.active) {
currentNeon = cat.count;
return;
}
const n = countNeonHolders();
currentNeon = n;
stats.maxNeon = Math.max(stats.maxNeon, n);
if (n === 1 && lastPressure < 1) {
addFloater(510, 245, 'tiny wrongness', 300, 1.6, 14);
say('One Neon Unicorn inside. A framed scone now has a halo.', 300);
}
if (n === 2 && lastPressure < 2) {
stats.nearMisses++;
addFloater(520, 235, 'COINCIDENCE?', 42, 1.8, 18);
say('Two at once. Reality has started flashing and asking for a manager.', 42);
}
if (n < 2 && lastPressure >= 2 && !cat.active) {
say('The building exhales. Nobody apologizes.', 160);
}
lastPressure = n;
if (n >= 3) triggerCatastrophe(n);
}
function serveFront() {
if (queue.length === 0 || cat.active) return;
const c = queue.shift();
const die = 1 + Math.floor(Math.random() * 6);
const neon = die === 1;
stats.served++;
stats.lastDie = die;
stats.lastNeon = neon;
if (neon) {
stats.neon++;
stats.dry = 0;
stats.glitter += .035;
} else {
stats.dry++;
stats.longestDry = Math.max(stats.longestDry, stats.dry);
}
c.state = 'holding';
c.neon = neon;
c.holdLeft = HOLD_TIME;
c.holdTotal = HOLD_TIME;
c.sx = c.x;
c.sy = c.y;
holders.push(c);
cupBurst(neon, 838, 442, c.id);
if (neon) {
addFloater(835, 382, 'D6=1 NEON UNICORN FRAP', (time * 180 + c.id * 31) % 360, 1.8, 15);
say('Counter roll: face 1. A glowing cup leaves the counter, physically and legally.', 310);
} else {
addFloater(835, 390, 'D6=' + die + ' regular frap', 38, 1.15, 13);
}
syncReality();
}
function updateCat(dt) {
cat.t += dt;
if (!cat.cleared && cat.t > 1.25) {
const everyone = queue.concat(holders);
for (let i = 0; i < everyone.length; i++) {
const c = everyone[i];
addParticle({
x: c.x, y: c.y,
vx: (Math.random() - .5) * 260,
vy: -80 - Math.random() * 220,
g: 240, life: 1.4 + Math.random(), maxLife: 2.4,
size: 3 + Math.random() * 5,
hue: (c.id * 43 + time * 150) % 360,
kind: 'spark', rot: Math.random() * TAU, vr: (Math.random() - .5) * 8
});
}
queue.length = 0;
holders.length = 0;
cat.cleared = true;
}
if (cat.t >= CAT_DURATION) {
cat.active = false;
cat.t = 0;
cat.count = 0;
cat.cleared = false;
queue.length = 0;
holders.length = 0;
serviceTimer = 0;
admitTimer = 0;
currentNeon = 0;
lastPressure = 0;
say('Shop rebuilt from liability waivers. The promotion continues, inexplicably.', 135);
}
}
function update(dt) {
time += dt;
updateParticles(dt);
updateOutside(dt);
if (cat.active) {
updateCat(dt);
return;
}
spawnTimer += dt;
while (spawnTimer >= ARRIVAL_INTERVAL) {
spawnCustomer();
spawnTimer -= ARRIVAL_INTERVAL;
}
updateQueue(dt);
updateHolders(dt);
admitTimer += dt;
while (queue.length + holders.length < MAX_INSIDE &&
outside.length > 0 &&
admitTimer >= ADMIT_INTERVAL) {
if (admitOne()) admitTimer -= ADMIT_INTERVAL;
else break;
}
admitTimer = Math.min(admitTimer, ADMIT_INTERVAL * 4);
if (queue.length > 0) serviceTimer += dt;
else serviceTimer = 0;
while (!cat.active && queue.length > 0 && serviceTimer >= SERVE_INTERVAL) {
serviceTimer -= SERVE_INTERVAL;
serveFront();
if (cat.active) {
serviceTimer = 0;
break;
}
}
syncReality();
if (currentNeon === 2 && !cat.active) stats.unstableSeconds += dt;
}
function drawBackground() {
const warp = cat.active ? clamp(cat.t / 2, 0, 1) : currentNeon * .11;
const g = ctx.createLinearGradient(0, 0, 0, H);
g.addColorStop(0, hsla(242 + warp * 65, 62, 7 + warp * 4, 1));
g.addColorStop(.55, hsla(258 + warp * 50, 62, 11 + warp * 5, 1));
g.addColorStop(1, '#070611');
ctx.fillStyle = g;
ctx.fillRect(0, 0, W, H);
ctx.save();
for (let i = 0; i < 95; i++) {
const x = fract(i * 12.13) * W;
const y = 15 + fract(i * 4.77) * 270;
const tw = .25 + .55 * (.5 + .5 * Math.sin(time * (1 + fract(i) * 2) + i));
ctx.globalAlpha = tw;
ctx.fillStyle = hsla((i * 31 + time * 40) % 360, 80, 78, .8);
ctx.fillRect(x, y, 1.6 + fract(i * 9) * 2, 1.6 + fract(i * 3) * 2);
}
ctx.globalAlpha = 1;
for (let i = 0; i < 18; i++) {
const x = i * 72 - 20;
const h = 35 + fract(i * 7.3) * 75;
ctx.fillStyle = 'rgba(9,12,24,.85)';
ctx.fillRect(x, 570 - h, 52, h);
ctx.fillStyle = 'rgba(70,255,200,.08)';
for (let j = 0; j < 3; j++) ctx.fillRect(x + 8 + j * 14, 570 - h + 12, 4, 7);
}
ctx.restore();
const ground = ctx.createLinearGradient(0, 565, 0, H);
ground.addColorStop(0, '#171729');
ground.addColorStop(1, '#090813');
ctx.fillStyle = ground;
ctx.fillRect(0, 565, W, H - 565);
ctx.strokeStyle = 'rgba(140,180,190,.10)';
ctx.lineWidth = 1;
for (let x = -200; x < W + 250; x += 85) {
ctx.beginPath();
ctx.moveTo(x, 570);
ctx.lineTo(x + 90, H);
ctx.stroke();
}
for (let y = 590; y < H; y += 28) {
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(W, y);
ctx.stroke();
}
ctx.fillStyle = '#ffe89a';
ctx.font = '700 12px ui-monospace, monospace';
ctx.textAlign = 'center';
ctx.fillText('NO PARKING FOR REALITIES', 865, 650);
ctx.textAlign = 'left';
}
function drawRealityAura(pressure) {
if (pressure <= 0) return;
const p = cat.active ? 3 : pressure;
const alpha = p === 1 ? .035 : p === 2 ? .10 + .05 * Math.sin(time * 15) : .16;
const radius = p === 1 ? 300 : p === 2 ? 450 : 650;
const g = ctx.createRadialGradient(640, 370, 20, 640, 370, radius);
g.addColorStop(0, hsla((time * 130 + 300) % 360, 100, 60, alpha));
g.addColorStop(.55, hsla((time * 90 + 185) % 360, 100, 55, alpha * .45));
g.addColorStop(1, hsla((time * 70 + 40) % 360, 100, 50, 0));
ctx.fillStyle = g;
ctx.fillRect(0, 0, W, H);
}
function drawDie(x, y, face, neon) {
const s = 42;
ctx.save();
ctx.translate(x, y);
ctx.rotate(Math.sin(time * 2.1) * .035);
ctx.shadowColor = neon ? hsla(time * 190, 100, 60, .95) : 'rgba(0,0,0,.5)';
ctx.shadowBlur = neon ? 22 : 6;
roundRect(-s / 2, -s / 2, s, s, 7, neon ? '#fffaff' : '#f3ecd9', neon ? '#ff4fea' : '#3d3c46', 2);
ctx.shadowBlur = 0;
const coords = [
[-1,-1], [0,-1], [1,-1],
[-1, 0], [0, 0], [1, 0],
[-1, 1], [0, 1], [1, 1]
];
const faces = {
1: [4], 2: [0,8], 3: [0,4,8],
4: [0,2,6,8], 5: [0,2,4,6,8], 6: [0,2,3,5,6,8]
};
if (face >= 1 && face <= 6) {
ctx.fillStyle = neon ? hsla(time * 180, 100, 45, 1) : '#17222a';
const arr = faces[face];
for (let i = 0; i < arr.length; i++) {
const c = coords[arr[i]];
ctx.beginPath();
ctx.arc(c[0] * 11, c[1] * 11, 3.7, 0, TAU);
ctx.fill();
}
} else {
ctx.fillStyle = '#27313b';
ctx.font = '900 22px ui-monospace, monospace';
ctx.textAlign = 'center';
ctx.fillText('?', 0, 8);
}
ctx.restore();
ctx.save();
ctx.font = '700 11px ui-monospace, monospace';
ctx.textAlign = 'center';
ctx.fillStyle = neon ? '#ffb6fa' : '#bfe9dc';
ctx.fillText('COUNTER D6 · FACE 1 = NEON', x, y + s / 2 + 17);
ctx.restore();
}
function drawBarista(pressure) {
ctx.save();
ctx.translate(880, 340);
const bounce = Math.sin(time * 3) * (pressure === 2 ? 4 : 1);
ctx.fillStyle = '#f2c9a6';
ctx.beginPath();
ctx.arc(0, bounce, 18, 0, TAU);
ctx.fill();
ctx.strokeStyle = '#2c2734';
ctx.lineWidth = 2;
ctx.stroke();
ctx.fillStyle = '#0b6b49';
ctx.fillRect(-14, 17 + bounce, 28, 34);
ctx.fillStyle = pressure === 2 ? '#fff' : '#17222a';
ctx.beginPath(); ctx.arc(-6, -2 + bounce, 2, 0, TAU); ctx.fill();
ctx.beginPath(); ctx.arc(6, -2 + bounce, 2, 0, TAU); ctx.fill();
ctx.strokeStyle = '#17222a';
ctx.beginPath();
if (pressure === 2) ctx.arc(0, 7 + bounce, 6, 0, Math.PI);
else { ctx.moveTo(-5, 8 + bounce); ctx.lineTo(5, 8 + bounce); }
ctx.stroke();
ctx.restore();
ctx.save();
ctx.font = '700 10px ui-monospace, monospace';
ctx.fillStyle = 'rgba(232,255,248,.78)';
ctx.textAlign = 'center';
ctx.fillText('ONE CUP · ONE DIE', 880, 385);
ctx.restore();
}
function drawNormalShop(pressure) {
const p = clamp(pressure, 0, 2);
let sx = 0, sy = 0;
if (p === 2) {
const amp = 3 + 3 * (.5 + .5 * Math.sin(time * 21));
sx = Math.sin(time * 37) * amp + Math.sin(time * 13) * amp * .4;
sy = Math.cos(time * 29) * amp * .45;
}
ctx.save();
ctx.translate(sx, sy);
if (p === 2) {
ctx.filter = 'hue-rotate(' + Math.sin(time * 4) * 85 + 'deg) saturate(' + (145 + 55 * Math.sin(time * 7)) + '%)';
}
if (p >= 1) {
ctx.save();
ctx.shadowColor = p === 1 ? '#ff43d0' : hsla(time * 220, 100, 60, 1);
ctx.shadowBlur = p === 1 ? 18 : 42;
pathRoundRect(SHOP.x, SHOP.y, SHOP.w, SHOP.h, 12);
ctx.strokeStyle = p === 1 ? 'rgba(255,70,220,.45)' : hsla(time * 220, 100, 65, .78);
ctx.lineWidth = p === 1 ? 3 : 7;
ctx.stroke();
ctx.restore();
}
ctx.fillStyle = 'rgba(0,0,0,.38)';
ctx.fillRect(SHOP.x + 10, SHOP.y + 14, SHOP.w, SHOP.h);
const wall = p === 2 ? hsla(35 + time * 75, 45, 76, 1) : '#eee2cc';
roundRect(SHOP.x, SHOP.y, SHOP.w, SHOP.h, 11, wall, '#26382f', 3);
ctx.fillStyle = '#0b5c42';
ctx.fillRect(SHOP.x - 10, 145, SHOP.w + 20, 55);
for (let i = 0; i < 14; i++) {
ctx.fillStyle = (i % 2 === 0)
? (p === 2 ? hsla(time * 160 + i * 21, 90, 55, 1) : '#167653')
: (p === 2 ? hsla(time * 160 + i * 37 + 180, 90, 65, 1) : '#f3e6cc');
ctx.fillRect(SHOP.x - 10 + i * ((SHOP.w + 20) / 14), 145, (SHOP.w + 20) / 14, 46);
}
ctx.fillStyle = 'rgba(0,0,0,.18)';
ctx.fillRect(SHOP.x - 10, 191, SHOP.w + 20, 9);
roundRect(430, 94, 400, 42, 18, '#073e31', '#baffdf', 2);
ctx.save();
ctx.textAlign = 'center';
ctx.fillStyle = '#d9fff0';
ctx.font = '900 18px ui-monospace, monospace';
ctx.fillText('SIREN-ISH COFFEE', 630, 121);
ctx.font = '700 10px ui-monospace, monospace';
ctx.fillStyle = '#8ee9c8';
ctx.fillText('FRAPPUCCINO PROMOTION · PLEASE REMAIN CALM', 630, 136);
ctx.restore();
const logoX = 392, logoY = 115;
ctx.save();
ctx.translate(logoX, logoY);
ctx.fillStyle = '#0a6b4d';
ctx.beginPath(); ctx.arc(0, 0, 29, 0, TAU); ctx.fill();
ctx.strokeStyle = '#e5fff4'; ctx.lineWidth = 3; ctx.stroke();
ctx.fillStyle = '#d9fff0';
starPath(0, 0, 8, 18, 8);
ctx.fill();
ctx.restore();
ctx.fillStyle = 'rgba(12,42,42,.76)';
roundRect(325, 265, 250, 210, 6, 'rgba(12,42,42,.78)', '#35544c', 3);
roundRect(690, 265, 245, 210, 6, 'rgba(12,42,42,.78)', '#35544c', 3);
roundRect(590, 348, 88, 222, 4, 'rgba(10,30,38,.72)', '#2a5149', 3);
ctx.strokeStyle = 'rgba(210,255,240,.28)';
ctx.lineWidth = 2;
for (let x = 340; x < 575; x += 58) { ctx.beginPath(); ctx.moveTo(x, 270); ctx.lineTo(x, 470); ctx.stroke(); }
for (let x = 705; x < 935; x += 58) { ctx.beginPath(); ctx.moveTo(x, 270); ctx.lineTo(x, 470); ctx.stroke(); }
ctx.fillStyle = '#2b231d';
ctx.fillRect(750, 420, 178, 62);
ctx.fillStyle = '#b98a57';
ctx.fillRect(745, 410, 188, 16);
ctx.fillStyle = '#0c7a55';
ctx.fillRect(756, 426, 62, 48);
ctx.fillStyle = '#d9fff0';
ctx.font = '900 13px ui-monospace, monospace';
ctx.textAlign = 'center';
ctx.fillText('ORDER / D6 / EXIT', 839, 451);
ctx.textAlign = 'left';
drawBarista(p);
drawDie(884, 365, stats.lastDie, stats.lastNeon);
ctx.fillStyle = 'rgba(255,255,255,.45)';
ctx.fillRect(601, 358, 12, 196);
ctx.fillStyle = '#b7f7de';
ctx.beginPath(); ctx.arc(678, 465, 5, 0, TAU); ctx.fill();
ctx.fillStyle = '#0b5c42';
ctx.fillRect(584, 548, 100, 22);
ctx.fillStyle = '#e9fff4';
ctx.font = '800 10px ui-monospace, monospace';
ctx.textAlign = 'center';
ctx.fillText('ENTRANCE / EXIT', 634, 563);
ctx.textAlign = 'left';
if (p >= 1) {
ctx.save();
ctx.translate(382, 305);
ctx.rotate(.055 + (p === 2 ? Math.sin(time * 5) * .05 : 0));
roundRect(-42, -28, 84, 56, 4, '#3a2a23', '#ffd36a', 2);
ctx.fillStyle = '#ffd36a';
ctx.font = '700 9px ui-monospace, monospace';
ctx.textAlign = 'center';
ctx.fillText(p === 1 ? 'WIFI: MOO' : 'WIFI: neigh', 0, -5);
ctx.fillStyle = p === 1 ? '#ff7ae6' : hsla(time * 180, 100, 60, 1);
ctx.beginPath();
ctx.moveTo(0, -1); ctx.lineTo(8, -20); ctx.lineTo(14, 0);
ctx.closePath(); ctx.fill();
ctx.restore();
}
if (p === 2) {
const flash = .13 + .18 * (.5 + .