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[];
|
fridge?: StoredItemSave[];
|
||||||
/** Consecutive S grades. The judge notices. Breaking it, he notices more. */
|
/** Consecutive S grades. The judge notices. Breaking it, he notices more. */
|
||||||
streak?: number;
|
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. */
|
/** The regulars remember: per-customer visit count and how last time went. */
|
||||||
customers?: Record<string, { visits: number; last: Grade; best: number }>;
|
customers?: Record<string, { visits: number; last: Grade; best: number }>;
|
||||||
}
|
}
|
||||||
@ -83,6 +85,8 @@ export class Game {
|
|||||||
private prepQueue: PrepStep[] = [];
|
private prepQueue: PrepStep[] = [];
|
||||||
/** The pre-toast drawer trip on loaf days: you're after a bread knife. */
|
/** The pre-toast drawer trip on loaf days: you're after a bread knife. */
|
||||||
private knifeTrip = false;
|
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 rng = new Rng(20260716);
|
||||||
private order!: Order;
|
private order!: Order;
|
||||||
/** Sim-time service clock — wall clock lies whenever the tab throttles. */
|
/** Sim-time service clock — wall clock lies whenever the tab throttles. */
|
||||||
@ -172,6 +176,13 @@ export class Game {
|
|||||||
this.save.fridge = this.pendingFridge;
|
this.save.fridge = this.pendingFridge;
|
||||||
this.pendingFridge = null;
|
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.day++;
|
||||||
this.save.maxDay = Math.max(this.save.maxDay, this.save.day);
|
this.save.maxDay = Math.max(this.save.maxDay, this.save.day);
|
||||||
writeSave(this.save);
|
writeSave(this.save);
|
||||||
@ -207,6 +218,10 @@ export class Game {
|
|||||||
this.ticket.hide();
|
this.ticket.hide();
|
||||||
new Title(this.save, (day) => {
|
new Title(this.save, (day) => {
|
||||||
audio.resume();
|
audio.resume();
|
||||||
|
if (day === 'daily') {
|
||||||
|
this.startDaily();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (day !== undefined && day !== this.save.day) {
|
if (day !== undefined && day !== this.save.day) {
|
||||||
// Replaying an old day: move the cursor there, but maxDay holds the
|
// Replaying an old day: move the cursor there, but maxDay holds the
|
||||||
// real progress so the ledger never shrinks.
|
// 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 {
|
beginDay(): void {
|
||||||
const day = this.save.day;
|
const day = this.save.day;
|
||||||
// Procedural days are seeded BY the day, not by a running stream: a reload
|
// 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.last = v.grade;
|
||||||
c.best = Math.max(c.best, v.total);
|
c.best = Math.max(c.best, v.total);
|
||||||
book[who] = c;
|
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.extraLines = extras;
|
||||||
|
this.judgeView.shareText = this.shareLine(v);
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderTicket(): void {
|
private renderTicket(): void {
|
||||||
@ -1037,13 +1113,13 @@ function loadSave(): Save {
|
|||||||
if (raw) {
|
if (raw) {
|
||||||
const s = JSON.parse(raw) as Partial<Save>;
|
const s = JSON.parse(raw) as Partial<Save>;
|
||||||
if (typeof s.day === 'number' && s.day >= 1) {
|
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 {
|
} catch {
|
||||||
/* a corrupt save is not worth a crash */
|
/* 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 {
|
function writeSave(s: Save): void {
|
||||||
|
|||||||
@ -32,6 +32,8 @@ export class JudgeView implements View {
|
|||||||
onNext: (() => void) | null = null;
|
onNext: (() => void) | null = null;
|
||||||
/** One-shot lines the game adds to this verdict (streaks, regulars' memory). */
|
/** One-shot lines the game adds to this verdict (streaks, regulars' memory). */
|
||||||
extraLines: string[] = [];
|
extraLines: string[] = [];
|
||||||
|
/** The one-line result SHARE copies to the clipboard. */
|
||||||
|
shareText = '';
|
||||||
/** What the polaroid prints under the photo. */
|
/** What the polaroid prints under the photo. */
|
||||||
private lastCaption = { day: 0, grade: 'S', total: 0, line: '' };
|
private lastCaption = { day: 0, grade: 'S', total: 0, line: '' };
|
||||||
|
|
||||||
@ -85,6 +87,14 @@ export class JudgeView implements View {
|
|||||||
this.viewBtn.addEventListener('click', () => this.cycleHeatmap());
|
this.viewBtn.addEventListener('click', () => this.cycleHeatmap());
|
||||||
const polaroid = el('button', 'btn ghost', btns, 'POLAROID');
|
const polaroid = el('button', 'btn ghost', btns, 'POLAROID');
|
||||||
polaroid.addEventListener('click', () => this.takePolaroid());
|
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 = el('button', 'btn', btns, 'NEXT ORDER');
|
||||||
this.nextBtn.addEventListener('click', () => this.onNext?.());
|
this.nextBtn.addEventListener('click', () => this.onNext?.());
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,7 +13,7 @@ export class Title {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
save: { day: number; maxDay: number; best: Record<string, number> },
|
save: { day: number; maxDay: number; best: Record<string, number> },
|
||||||
onStart: (day?: number) => void,
|
onStart: (day?: number | 'daily') => void,
|
||||||
) {
|
) {
|
||||||
this.panel = new Panel('title');
|
this.panel = new Panel('title');
|
||||||
const wrap = el('div', 'title-wrap', this.panel.root);
|
const wrap = el('div', 'title-wrap', this.panel.root);
|
||||||
@ -24,11 +24,15 @@ export class Title {
|
|||||||
el('h1', undefined, txt, 'TOASTSIM');
|
el('h1', undefined, txt, 'TOASTSIM');
|
||||||
el('p', 'title-sub', txt, 'Bread goes in. You are judged.');
|
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 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();
|
this.panel.dispose();
|
||||||
onStart(day);
|
onStart(day);
|
||||||
};
|
};
|
||||||
btn.addEventListener('click', () => go());
|
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
|
// The day ledger: one chip per reached day, best grade underneath. Click to
|
||||||
// replay it (progress never regresses — maxDay remembers how far you got).
|
// replay it (progress never regresses — maxDay remembers how far you got).
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user