THE MOTHER: a sourdough starter that lives in your save and holds a grudge
After five days survived, a jar appears on the title shelf. She wants feeding — one click — every new day you reach, and the stamps are maxDay based so replaying old days for the S never starves her. Fed within a day, toaster-days earn +0.2 and the line 'The starter is lively — the crumb held. (She did that, not you.)'. Three days forgotten and the jar sulks, and the crumb knows. Five and she goes still: the button becomes a funeral, and one click begins THE MOTHER (second of her name) — she forgives, mostly. The deaths counter never resets. Station days (grill, pan, cold, steak, eggs, poach) are honestly out of her reach. Also hardened loadSave against non-finite numbers: NaN and Infinity stringify to null, and one bad number was enough to rot every derived stat downstream. Every numeric field now refuses them on the way in. Verified live: jar born fed at maxDay, starved to a funeral, resurrected as second-of-her-name (deaths=1 persisted), lively bonus fired on a bruschetta (9.6 -> 9.8 with the line), sulk line at gap 3, save numbers intact after the full arc. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
9eb1c2db3a
commit
ef66e1aa7a
@ -55,6 +55,10 @@ interface Save {
|
||||
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>;
|
||||
/** THE MOTHER: the sourdough starter. Appears after day 5, wants feeding
|
||||
* every new day you reach, and remembers exactly how long you forgot her.
|
||||
* `fed`/`born` are maxDay stamps so replaying old days never starves her. */
|
||||
starter?: { born: number; fed: number; deaths: number };
|
||||
}
|
||||
|
||||
/**
|
||||
@ -234,6 +238,11 @@ export class Game {
|
||||
this.kitchen.root.visible = false;
|
||||
this.ticket.hide();
|
||||
eye.hide(); // he doesn't watch you read the menu
|
||||
// The jar arrives once the kitchen has survived five days.
|
||||
if (!this.save.starter && this.save.maxDay >= 5) {
|
||||
this.save.starter = { born: this.save.maxDay, fed: this.save.maxDay, deaths: 0 };
|
||||
writeSave(this.save);
|
||||
}
|
||||
new Title(this.save, (day, shadow) => {
|
||||
audio.resume();
|
||||
this.shadowMode = !!shadow;
|
||||
@ -248,6 +257,11 @@ export class Game {
|
||||
writeSave(this.save);
|
||||
}
|
||||
this.beginDay();
|
||||
}, () => {
|
||||
// Feeding day: the jar mutated in place; make it stick and let her sing.
|
||||
writeSave(this.save);
|
||||
audio.resume();
|
||||
audio.blip(0.3);
|
||||
});
|
||||
}
|
||||
|
||||
@ -1006,6 +1020,19 @@ export class Game {
|
||||
else if (v.grade === 'F') extras.push('The Critic: I will be gentle in print. This kitchen has suffered enough today.');
|
||||
else extras.push(`The Critic: the ${v.worst.label.toLowerCase()} betrayed you. It will be the headline.`);
|
||||
}
|
||||
// THE MOTHER: a lively starter firms the crumb on toaster days. A dead
|
||||
// one is on the shelf where he can see it, and he can smell a coward.
|
||||
const st = this.save.starter;
|
||||
if (st) {
|
||||
const gap = this.save.maxDay - st.fed;
|
||||
const toastDay = !this.order.grill && !this.order.pan && !this.order.fridge && !this.order.steak && !this.order.eggs && !this.order.poach;
|
||||
if (gap >= 5) extras.push('The jar on the shelf has gone still. You know what you did.');
|
||||
else if (gap <= 1 && toastDay) {
|
||||
v.total = Math.min(10, Math.round((v.total + 0.2) * 10) / 10);
|
||||
v.grade = gradeOf(v.total);
|
||||
extras.push('The starter is lively — the crumb held. (+0.2. She did that, not you.)');
|
||||
} else if (gap >= 3 && toastDay) extras.push('The jar says nothing today. It is sulking, and the crumb knows.');
|
||||
}
|
||||
// The streak. He notices. He hates that he notices.
|
||||
const prev = this.save.streak ?? 0;
|
||||
if (v.grade === 'S') {
|
||||
@ -1178,8 +1205,15 @@ function loadSave(): Save {
|
||||
const raw = localStorage.getItem(SAVE_KEY);
|
||||
if (raw) {
|
||||
const s = JSON.parse(raw) as Partial<Save>;
|
||||
// NaN/Infinity stringify to null and null arithmetic breeds more NaN —
|
||||
// one bad number would otherwise rot the whole save. Refuse them here.
|
||||
const num = (x: unknown, d: number): number => (typeof x === 'number' && Number.isFinite(x) ? x : d);
|
||||
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 ?? {}, shadowed: s.shadowed ?? {} };
|
||||
const day = num(s.day, 1);
|
||||
const maxDay = Math.max(day, num(s.maxDay, day));
|
||||
const st = s.starter;
|
||||
const starter = st && Number.isFinite(st.born) && Number.isFinite(st.fed) ? { born: Math.min(st.born, maxDay), fed: Math.min(st.fed, maxDay), deaths: num(st.deaths, 0) } : undefined;
|
||||
return { day, maxDay, total: num(s.total, 0), best: s.best ?? {}, fridge: s.fridge, streak: num(s.streak, 0), customers: s.customers ?? {}, daily: s.daily ?? {}, shadowed: s.shadowed ?? {}, starter };
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
|
||||
@ -800,3 +800,9 @@ body.shadow .load {
|
||||
|
||||
/* No keys while you read the menu - the title disposes itself on start. */
|
||||
body:has(#ui > .title) #touchpad { display: none; }
|
||||
|
||||
.jar-btn.fed {
|
||||
border-color: #7da05f;
|
||||
color: #b8d89a;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
@ -12,8 +12,9 @@ export class Title {
|
||||
private panel: Panel;
|
||||
|
||||
constructor(
|
||||
save: { day: number; maxDay: number; best: Record<string, number>; shadowed?: Record<string, boolean> },
|
||||
save: { day: number; maxDay: number; best: Record<string, number>; shadowed?: Record<string, boolean>; starter?: { born: number; fed: number; deaths: number } },
|
||||
onStart: (day?: number | 'daily', shadow?: boolean) => void,
|
||||
onFeed?: () => void,
|
||||
) {
|
||||
this.panel = new Panel('title');
|
||||
const wrap = el('div', 'title-wrap', this.panel.root);
|
||||
@ -46,6 +47,32 @@ export class Title {
|
||||
});
|
||||
}
|
||||
|
||||
// THE MOTHER: the sourdough starter. A pet that lives in the save and
|
||||
// holds a grudge. Feeding is one click; forgetting is a funeral.
|
||||
if (save.starter) {
|
||||
const st = save.starter;
|
||||
const jar = el('button', 'btn ghost jar-btn', txt, '');
|
||||
const label = () => {
|
||||
const gap = save.maxDay - st.fed;
|
||||
const name = st.deaths > 0 ? `THE MOTHER (${['second', 'third', 'fourth', 'umpteenth'][Math.min(st.deaths - 1, 3)]} of her name)` : 'THE MOTHER';
|
||||
if (gap >= 5) return `\u{1FAD9} ${name} has gone still. begin again \u2014 she forgives, mostly`;
|
||||
if (gap === 0) return `\u{1FAD9} ${name} \u2014 fed. bubbling like gossip`;
|
||||
if (gap === 1) return `\u{1FAD9} ${name} \u2014 peckish. FEED HER`;
|
||||
if (gap === 4) return `\u{1FAD9} ${name} \u2014 one day from the end. FEED HER NOW`;
|
||||
return `\u{1FAD9} ${name} \u2014 hungry (${gap} days). the crumb suffers`;
|
||||
};
|
||||
jar.textContent = label();
|
||||
jar.addEventListener('click', () => {
|
||||
const gap = save.maxDay - st.fed;
|
||||
if (gap >= 5) { st.deaths++; st.born = save.maxDay; }
|
||||
st.fed = save.maxDay;
|
||||
onFeed?.();
|
||||
jar.textContent = label();
|
||||
jar.classList.add('fed');
|
||||
window.setTimeout(() => jar.classList.remove('fed'), 700);
|
||||
});
|
||||
}
|
||||
|
||||
// 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) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user