THE DAILY DEAL + SHARE: one deal for everyone, a result you can paste

THE DAILY — a button on the title deals one shared order per real date:
the seed IS the date (FNV hash), so everyone on Earth gets the same deal
today, station and all. Scored into save.daily[date] (best-of), the
campaign day counter untouched — NEXT after a daily returns you to your
regular day. The title button becomes 'THE DAILY ✓ 9.9' once played.
Judge closes with 'The Daily. Same deal for everyone today — come back
tomorrow.'

SHARE — a button on every verdict copies a one-line result to the
clipboard: 'TOASTSIM daily 2026-07-20 — 🥚 S 9.9/10 · partly.party/
toastsim' (station emoji per order; campaign days read 'day N'). The
wordle-paste for toast.

Verified live: daily deals deterministically (same date → same order,
today's is a poach), played to S 9.9, best saved, share text exact,
NEXT preserved campaign day 20, title shows the checkmark.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
type-two 2026-07-20 16:44:41 +10:00
parent f74ee61bbd
commit 56389dabab
3 changed files with 94 additions and 4 deletions

View File

@ -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<string, number>;
/** The regulars remember: per-customer visit count and how last time went. */
customers?: Record<string, { visits: number; last: Grade; best: number }>;
}
@ -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<Save>;
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 {

View File

@ -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?.());
}

View File

@ -13,7 +13,7 @@ export class Title {
constructor(
save: { day: number; maxDay: number; best: Record<string, number> },
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<string, number> }).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).