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:
parent
f74ee61bbd
commit
56389dabab
@ -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 {
|
||||
|
||||
@ -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?.());
|
||||
}
|
||||
|
||||
@ -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).
|
||||
|
||||
Loading…
Reference in New Issue
Block a user