THE SHADOW: hard mode is him saying nothing
Once you have pulled a single S, the title offers THE INSPECTOR'S SHADOW: arm it and the kitchen's words go dark for the whole run — no state words, no wobble words, no browning gauge. The ticket still asks and the controls hint survives, but the read is the face in the corner, the sound of the pan, and the food itself. Every diegetic channel we built was practice for playing without the subtitles. Take an S with the words off and the ledger chip earns a star (S★), the save remembers it (shadowed, per day, survives loadSave's corruption filter — which was quietly dropping unknown fields; fixed), and he gives you the only compliment he has: 'In the silence, too. I have nothing left to teach you. ★' Verified live: toggle arms, body.shadow hides the words (computed visibility checked), S-in-silence writes shadowed.day20, star renders on the chip after reload. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
2a01a6e3a0
commit
a8ae26f2d4
@ -53,6 +53,8 @@ interface Save {
|
||||
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 }>;
|
||||
/** Days S-graded under the Inspector's Shadow — no words, face and sound only. */
|
||||
shadowed?: Record<string, boolean>;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -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<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 ?? {}, 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 {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -12,8 +12,8 @@ export class Title {
|
||||
private panel: Panel;
|
||||
|
||||
constructor(
|
||||
save: { day: number; maxDay: number; best: Record<string, number> },
|
||||
onStart: (day?: number | 'daily') => void,
|
||||
save: { day: number; maxDay: number; best: Record<string, number>; shadowed?: Record<string, boolean> },
|
||||
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));
|
||||
|
||||
Loading…
Reference in New Issue
Block a user