diff --git a/src/game/game.ts b/src/game/game.ts index c1ca1e2..18043e8 100644 --- a/src/game/game.ts +++ b/src/game/game.ts @@ -48,6 +48,8 @@ interface Save { fridge?: StoredItemSave[]; /** Consecutive S grades. The judge notices. Breaking it, he notices more. */ streak?: number; + /** Best score per Daily Deal date (YYYY-MM-DD) — one shared deal per day. */ + daily?: Record; /** The regulars remember: per-customer visit count and how last time went. */ customers?: Record; } @@ -83,6 +85,8 @@ export class Game { private prepQueue: PrepStep[] = []; /** The pre-toast drawer trip on loaf days: you're after a bread knife. */ private knifeTrip = false; + /** The Daily Deal: everyone on Earth gets the same order today. */ + private dailyDate: string | null = null; private rng = new Rng(20260716); private order!: Order; /** Sim-time service clock — wall clock lies whenever the tab throttles. */ @@ -172,6 +176,13 @@ export class Game { this.save.fridge = this.pendingFridge; this.pendingFridge = null; } + if (this.dailyDate) { + // A daily doesn't advance the campaign — back to your regular day. + this.dailyDate = null; + writeSave(this.save); + this.beginDay(); + return; + } this.save.day++; this.save.maxDay = Math.max(this.save.maxDay, this.save.day); writeSave(this.save); @@ -207,6 +218,10 @@ export class Game { this.ticket.hide(); new Title(this.save, (day) => { audio.resume(); + if (day === 'daily') { + this.startDaily(); + return; + } if (day !== undefined && day !== this.save.day) { // Replaying an old day: move the cursor there, but maxDay holds the // real progress so the ledger never shrinks. @@ -515,6 +530,61 @@ export class Game { } } + /** + * THE DAILY DEAL: one shared order per real date — the seed IS the date, so + * everyone gets the same deal. Scored into save.daily[date]; the campaign's + * day counter and streaks are left alone. Deterministic: replaying today + * re-deals today. + */ + startDaily(): void { + const date = new Date().toISOString().slice(0, 10); + this.dailyDate = date; + let h = 2166136261; + for (const ch of date) { + h ^= ch.charCodeAt(0); + h = Math.imul(h, 16777619); + } + const rng = new Rng(h >>> 0); + // A virtual day number varies the ramps date-to-date without touching the save. + const virtualDay = 23 + (Math.abs(h) % 14); + const o = proceduralOrder(virtualDay, () => rng.next()); + o.who = `the daily — ${date}`; + this.order = o; + this.orderSeconds = 0; + this.prepResult = null; + this.juiceResult = null; + this.panIngredient = null; + this.lastBruschetta = null; + this.prepQueue = o.prep ? [...o.prep] : []; + this.timing = true; + this.temp = null; + this.hideTempToggle(); + const sl = this.judgeView.release(); + if (sl) this.kitchen.root.add(sl.mesh); + this.kitchen.applyOrder(o); + this.judgeView.root.visible = false; + this.renderTicket(); + if (this.prepQueue.length) { + this.runNextPrep(); + return; + } + if (o.grill) return this.enterGrill(); + if (o.pan) return this.enterPan(); + if (o.fridge) return this.enterFridge(); + if (o.steak) return this.enterSteak(); + if (o.eggs) return this.enterEggs(); + if (o.poach) return this.enterPoach(); + this.enterToastFlow(); + } + + /** The one-line shareable result for the current order's verdict. */ + private shareLine(v: Verdict): string { + const o = this.order; + const emoji = o.steak ? '🥩' : o.grill ? '🔥' : o.pan ? '🍳' : o.benedict ? '🍞🍳' : o.poach ? '🥚' : o.eggs ? '🥚' : o.fridge ? '🧊' : o.prep ? '🔪' : '🍞'; + const tag = this.dailyDate ? `daily ${this.dailyDate}` : `day ${o.day}`; + return `TOASTSIM ${tag} — ${emoji} ${v.grade} ${v.total.toFixed(1)}/10 · partly.party/toastsim`; + } + beginDay(): void { const day = this.save.day; // Procedural days are seeded BY the day, not by a running stream: a reload @@ -903,7 +973,13 @@ export class Game { c.last = v.grade; c.best = Math.max(c.best, v.total); book[who] = c; + if (this.dailyDate) { + const d = (this.save.daily ??= {}); + d[this.dailyDate] = Math.max(d[this.dailyDate] ?? 0, v.total); + extras.push('The Daily. Same deal for everyone today — come back tomorrow.'); + } this.judgeView.extraLines = extras; + this.judgeView.shareText = this.shareLine(v); } private renderTicket(): void { @@ -1037,13 +1113,13 @@ function loadSave(): Save { if (raw) { const s = JSON.parse(raw) as Partial; if (typeof s.day === 'number' && s.day >= 1) { - return { day: s.day, maxDay: Math.max(s.day, s.maxDay ?? s.day), total: s.total ?? 0, best: s.best ?? {}, fridge: s.fridge, streak: s.streak ?? 0, customers: s.customers ?? {} }; + return { day: s.day, maxDay: Math.max(s.day, s.maxDay ?? s.day), total: s.total ?? 0, best: s.best ?? {}, fridge: s.fridge, streak: s.streak ?? 0, customers: s.customers ?? {}, daily: s.daily ?? {} }; } } } catch { /* a corrupt save is not worth a crash */ } - return { day: 1, maxDay: 1, total: 0, best: {}, streak: 0, customers: {} }; + return { day: 1, maxDay: 1, total: 0, best: {}, streak: 0, customers: {}, daily: {} }; } function writeSave(s: Save): void { diff --git a/src/scenes/judge.ts b/src/scenes/judge.ts index 3fa34ee..8a6b015 100644 --- a/src/scenes/judge.ts +++ b/src/scenes/judge.ts @@ -32,6 +32,8 @@ export class JudgeView implements View { onNext: (() => void) | null = null; /** One-shot lines the game adds to this verdict (streaks, regulars' memory). */ extraLines: string[] = []; + /** The one-line result SHARE copies to the clipboard. */ + shareText = ''; /** What the polaroid prints under the photo. */ private lastCaption = { day: 0, grade: 'S', total: 0, line: '' }; @@ -85,6 +87,14 @@ export class JudgeView implements View { this.viewBtn.addEventListener('click', () => this.cycleHeatmap()); const polaroid = el('button', 'btn ghost', btns, 'POLAROID'); polaroid.addEventListener('click', () => this.takePolaroid()); + const share = el('button', 'btn ghost', btns, 'SHARE'); + share.addEventListener('click', () => { + if (!this.shareText) return; + void navigator.clipboard?.writeText(this.shareText).then( + () => { share.textContent = 'COPIED'; window.setTimeout(() => (share.textContent = 'SHARE'), 1400); }, + () => undefined, + ); + }); this.nextBtn = el('button', 'btn', btns, 'NEXT ORDER'); this.nextBtn.addEventListener('click', () => this.onNext?.()); } diff --git a/src/ui/title.ts b/src/ui/title.ts index 4900813..7e724a9 100644 --- a/src/ui/title.ts +++ b/src/ui/title.ts @@ -13,7 +13,7 @@ export class Title { constructor( save: { day: number; maxDay: number; best: Record }, - onStart: (day?: number) => void, + onStart: (day?: number | 'daily') => void, ) { this.panel = new Panel('title'); const wrap = el('div', 'title-wrap', this.panel.root); @@ -24,11 +24,15 @@ export class Title { el('h1', undefined, txt, 'TOASTSIM'); el('p', 'title-sub', txt, 'Bread goes in. You are judged.'); const btn = el('button', 'btn', txt, save.day > 1 ? `RESUME — DAY ${save.day}` : 'START DAY 1'); - const go = (day?: number) => { + const go = (day?: number | 'daily') => { this.panel.dispose(); onStart(day); }; btn.addEventListener('click', () => go()); + const today = new Date().toISOString().slice(0, 10); + const dailyBest = (save as { daily?: Record }).daily?.[today]; + const daily = el('button', 'btn ghost daily-btn', txt, dailyBest !== undefined ? `THE DAILY ✓ ${dailyBest.toFixed(1)}` : 'THE DAILY — same deal for everyone'); + daily.addEventListener('click', () => go('daily')); // The day ledger: one chip per reached day, best grade underneath. Click to // replay it (progress never regresses — maxDay remembers how far you got).