diff --git a/src/game/game.ts b/src/game/game.ts index 2509745..01ce12b 100644 --- a/src/game/game.ts +++ b/src/game/game.ts @@ -53,6 +53,8 @@ interface Save { daily?: Record; /** The regulars remember: per-customer visit count and how last time went. */ customers?: Record; + /** Days S-graded under the Inspector's Shadow — no words, face and sound only. */ + shadowed?: Record; } /** @@ -90,6 +92,8 @@ export class Game { private dailyDate: string | null = null; /** A demonstration is playing — the judge performs, nothing counts. */ private demoMode = false; + /** The Inspector's Shadow: the kitchen's words go dark; you cook by eye and ear. */ + private shadowMode = false; private demoSnapshot: string | null = null; private rng = new Rng(20260716); private order!: Order; @@ -230,8 +234,9 @@ export class Game { this.kitchen.root.visible = false; this.ticket.hide(); eye.hide(); // he doesn't watch you read the menu - new Title(this.save, (day) => { + new Title(this.save, (day, shadow) => { audio.resume(); + this.shadowMode = !!shadow; if (day === 'daily') { this.startDaily(); return; @@ -617,6 +622,7 @@ export class Game { beginDay(): void { eye.show(); // he watches you cook + document.body.classList.toggle('shadow', this.shadowMode); const day = this.save.day; // Procedural days are seeded BY the day, not by a running stream: a reload // re-deals the identical order (no reroll-scumming), and the harness can @@ -1009,6 +1015,10 @@ export class Game { c.last = v.grade; c.best = Math.max(c.best, v.total); book[who] = c; + if (this.shadowMode && v.grade === 'S') { + (this.save.shadowed ??= {})[`day${this.order.day}`] = true; + extras.push('In the silence, too. I have nothing left to teach you. \u2605'); + } if (this.dailyDate) { const d = (this.save.daily ??= {}); d[this.dailyDate] = Math.max(d[this.dailyDate] ?? 0, v.total); @@ -1154,7 +1164,7 @@ 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 ?? {}, daily: s.daily ?? {} }; + 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 ?? {}, shadowed: s.shadowed ?? {} }; } } } catch { diff --git a/src/style.css b/src/style.css index c7bc2fe..1bb96ae 100644 --- a/src/style.css +++ b/src/style.css @@ -718,3 +718,17 @@ canvas#c { color: #6d4a1c; font-weight: 700; } + +/* THE INSPECTOR'S SHADOW: the kitchen's words go dark. The ticket (the ask) + and the controls hint survive - but no state words, no wobble words, no + gauges. The face in the corner and the sound are the whole read. */ +body.shadow .slicer-thick, +body.shadow .slicer-wobble, +body.shadow .gauge, +body.shadow .load { + visibility: hidden; +} +.shadow-btn.on { + border-color: #b98f4e; + color: #e8c98a; +} diff --git a/src/ui/title.ts b/src/ui/title.ts index 48f7c6f..5cc8e33 100644 --- a/src/ui/title.ts +++ b/src/ui/title.ts @@ -12,8 +12,8 @@ export class Title { private panel: Panel; constructor( - save: { day: number; maxDay: number; best: Record }, - onStart: (day?: number | 'daily') => void, + save: { day: number; maxDay: number; best: Record; shadowed?: Record }, + onStart: (day?: number | 'daily', shadow?: boolean) => void, ) { this.panel = new Panel('title'); const wrap = el('div', 'title-wrap', this.panel.root); @@ -24,9 +24,10 @@ 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'); + let shadow = false; const go = (day?: number | 'daily') => { this.panel.dispose(); - onStart(day); + onStart(day, shadow); }; btn.addEventListener('click', () => go()); const today = new Date().toISOString().slice(0, 10); @@ -34,6 +35,17 @@ export class Title { 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')); + // Hard mode, earned: once you've pulled an S he offers to stop talking. + // No words on the bench — you cook by the face, the sound, the food. + if (Object.values(save.best).some((b) => gradeOf(b) === 'S')) { + const sh = el('button', 'btn ghost shadow-btn', txt, "THE INSPECTOR'S SHADOW — he says nothing"); + sh.addEventListener('click', () => { + shadow = !shadow; + sh.classList.toggle('on', shadow); + sh.textContent = shadow ? "THE INSPECTOR'S SHADOW — armed. no words today" : "THE INSPECTOR'S SHADOW — he says nothing"; + }); + } + // The day ledger: one chip per reached day, best grade underneath. Click to // replay it (progress never regresses — maxDay remembers how far you got). if (save.maxDay > 1) { @@ -44,7 +56,8 @@ export class Title { el('span', 'day-num', chip, String(d)); const bestScore = save.best[`day${d}`]; const grade = bestScore !== undefined ? gradeOf(bestScore) : null; - const g = el('span', `day-grade${grade ? ` grade-${grade}` : ''}`, chip, grade ?? '·'); + const star = save.shadowed?.[`day${d}`] ? '★' : ''; + const g = el('span', `day-grade${grade ? ` grade-${grade}` : ''}`, chip, (grade ?? '·') + star); void g; chip.title = bestScore !== undefined ? `day ${d} — best ${bestScore.toFixed(1)}/10` : `day ${d} — not judged yet`; chip.addEventListener('click', () => go(d));