Compare commits
2 Commits
2085f68655
...
afe2bf174b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
afe2bf174b | ||
|
|
3f91bedc7f |
@ -249,8 +249,18 @@ door), shut the door, three days compress-pass (items recolour as they spoil), a
|
|||||||
`judgeFridge` scores THE FRIDGE (The Cold / Hygiene) + The Waste with the hygiene-
|
`judgeFridge` scores THE FRIDGE (The Cold / Hygiene) + The Waste with the hygiene-
|
||||||
inspector line bank. Verified live: good storage (raw low, perishables cold, RTE
|
inspector line bank. Verified live: good storage (raw low, perishables cold, RTE
|
||||||
high) grades **S (10.0)**; bad storage (raw high in the warm door, crammed) grades
|
high) grades **S (10.0)**; bad storage (raw high in the warm door, crammed) grades
|
||||||
**C (3.5)** — 38% off, 38% dripped on, 3 wasted. Cross-day carryover (fridge ages
|
**C (3.5)** — 38% off, 38% dripped on, 3 wasted.
|
||||||
in the save between days) and the FIFO pull-for-a-cook are the remaining polish.
|
|
||||||
|
**CROSS-DAY LOOP SHIPPED**: the fridge persists in the save (`serializeStore`/
|
||||||
|
`restoreStore`) — what you stock on day 17 is still there on day 18. Day 18 "the
|
||||||
|
lunch rush" pan-sears **the very mushrooms from your own fridge** (`fetch` FIFO);
|
||||||
|
the fetched `usableQuality` is a judged row ("The Ingredient") and a genuinely off
|
||||||
|
one HARD-CAPS the dish — no sear saves rot. Verified: identical cook (72%/57% sear),
|
||||||
|
well-stored mushroom → **7.4/10**, badly-stored (warm door) → same sear, off
|
||||||
|
ingredient → **3.0/10 (F)**; plain pan day unaffected (8.0). Judge: "No sear saves a
|
||||||
|
bad mushroom. The fridge did this, not the flame." Remaining polish: per-day-transition
|
||||||
|
ageing (currently the fridge day's own 3-day compress does the ageing) and the fetch
|
||||||
|
wired into the grill/other cooks too.
|
||||||
|
|
||||||
|
|
||||||
The cure is time you *want*; the cold chain is time you *fight*. `src/sim/coldchain.ts`
|
The cure is time you *want*; the cold chain is time you *fight*. `src/sim/coldchain.ts`
|
||||||
|
|||||||
29
src/dev.ts
29
src/dev.ts
@ -517,6 +517,35 @@ export function installDevHarness(app: App, game: Game): void {
|
|||||||
return { fuel, wind, sheltered, mean: +mean.toFixed(3), stdev: +stdev.toFixed(4) };
|
return { fuel, wind, sheltered, mean: +mean.toFixed(3), stdev: +stdev.toFixed(4) };
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** The cross-day arc: stock the fridge on day 17 (well or badly), then cook
|
||||||
|
* those very mushrooms on day 18. Bad storage → an off mushroom caps the sear. */
|
||||||
|
coldChainArc(goodStorage = true) {
|
||||||
|
harness.fridgeDay(goodStorage); // day 17: stock + 3 days + serve
|
||||||
|
run(5);
|
||||||
|
// Advance to day 18 (the judge's NEXT button bumps the day + beginDay).
|
||||||
|
const g = game as unknown as { save: { day: number; fridge?: unknown[] } };
|
||||||
|
const storedCount = g.save.fridge?.length ?? 0;
|
||||||
|
game.setDay(18);
|
||||||
|
run(6);
|
||||||
|
const atPan = app.currentView === game.panView ? 'pan' : 'other';
|
||||||
|
const fetched = (game as unknown as { panIngredient: { quality: number; word: string } | null }).panIngredient;
|
||||||
|
// Cook it well and serve — only the fetched ingredient's quality varies.
|
||||||
|
const pv = game.panView as unknown as { session: import('./sim/pan').PanSession };
|
||||||
|
const s = pv.session;
|
||||||
|
if (s) {
|
||||||
|
setPanKnob(s, 7); addButter(s);
|
||||||
|
let guard = 0; while (butterState(s) !== 'foaming' && guard++ < 60 * 30) run(1);
|
||||||
|
addFood(s, 'button_mushroom', 'A');
|
||||||
|
for (let i = 0; i < 60 * 8; i++) run(1);
|
||||||
|
flip(s);
|
||||||
|
for (let i = 0; i < 60 * 9; i++) run(1);
|
||||||
|
tap('Enter'); run(5);
|
||||||
|
}
|
||||||
|
const rows = [...document.querySelectorAll('.crit')].map((e) => e.textContent);
|
||||||
|
const gradeEl = document.querySelector('.judge-grade, .grade');
|
||||||
|
return { storedCount, atPan, fetched, grade: gradeEl ? gradeEl.textContent : null, rows };
|
||||||
|
},
|
||||||
|
|
||||||
/** Play the whole day-17 fridge order: route in, put the delivery away
|
/** Play the whole day-17 fridge order: route in, put the delivery away
|
||||||
* (well or badly), shut the door (3 days), serve, read the scorecard. */
|
* (well or badly), shut the door (3 days), serve, read the scorecard. */
|
||||||
fridgeDay(good = true) {
|
fridgeDay(good = true) {
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import { OvenView } from '../scenes/oven';
|
|||||||
import { GrillView } from '../scenes/grill';
|
import { GrillView } from '../scenes/grill';
|
||||||
import { PanView } from '../scenes/pan';
|
import { PanView } from '../scenes/pan';
|
||||||
import { FridgeView } from '../scenes/fridge';
|
import { FridgeView } from '../scenes/fridge';
|
||||||
|
import { serializeStore, restoreStore, fetch as fetchStock, usableQuality, type StoredItemSave } from '../sim/coldchain';
|
||||||
import { AssemblyView } from '../scenes/assembly';
|
import { AssemblyView } from '../scenes/assembly';
|
||||||
import type { CutPattern } from '../sim/cutting';
|
import type { CutPattern } from '../sim/cutting';
|
||||||
import type { IngredientId } from '../sim/ingredients';
|
import type { IngredientId } from '../sim/ingredients';
|
||||||
@ -36,6 +37,9 @@ interface Save {
|
|||||||
day: number;
|
day: number;
|
||||||
total: number;
|
total: number;
|
||||||
best: Record<string, number>;
|
best: Record<string, number>;
|
||||||
|
/** The cold store, persisted: what you stocked on a fridge day is still there
|
||||||
|
* (and older) when a later cook reaches in for it. */
|
||||||
|
fridge?: StoredItemSave[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -139,6 +143,12 @@ export class Game {
|
|||||||
this.kitchen.flash(cut.knife === 'the_best_thing' ? 'cut with The Best Thing' : 'now toast it');
|
this.kitchen.flash(cut.knife === 'the_best_thing' ? 'cut with The Best Thing' : 'now toast it');
|
||||||
};
|
};
|
||||||
this.judgeView.onNext = () => {
|
this.judgeView.onNext = () => {
|
||||||
|
// Commit any fridge consumption atomically with the day advance: a reload
|
||||||
|
// before this point re-derives the same fetch, so the cap can't be dodged.
|
||||||
|
if (this.pendingFridge) {
|
||||||
|
this.save.fridge = this.pendingFridge;
|
||||||
|
this.pendingFridge = null;
|
||||||
|
}
|
||||||
this.save.day++;
|
this.save.day++;
|
||||||
writeSave(this.save);
|
writeSave(this.save);
|
||||||
this.beginDay();
|
this.beginDay();
|
||||||
@ -537,6 +547,9 @@ export class Game {
|
|||||||
this.timing = false;
|
this.timing = false;
|
||||||
const seconds = this.orderSeconds;
|
const seconds = this.orderSeconds;
|
||||||
const slice = this.kitchen.currentSlice;
|
const slice = this.kitchen.currentSlice;
|
||||||
|
// Persist the stock exactly as you left it — a later cook reaches into THIS
|
||||||
|
// fridge, at whatever freshness your storage (and the days) left each item.
|
||||||
|
this.save.fridge = serializeStore(result.store);
|
||||||
const v = judgeFridge(result, this.order, seconds);
|
const v = judgeFridge(result, this.order, seconds);
|
||||||
this.save.total += v.total;
|
this.save.total += v.total;
|
||||||
const key = `day${this.order.day}`;
|
const key = `day${this.order.day}`;
|
||||||
@ -551,8 +564,28 @@ export class Game {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** The pan day (M17): a fuel-aware hob — butter it, foam it, flip it, serve. */
|
/** The pan day (M17): a fuel-aware hob — butter it, foam it, flip it, serve. */
|
||||||
|
/** When a pan day fetches its food from the fridge, what came out. */
|
||||||
|
private panIngredient: { quality: number; word: string } | null = null;
|
||||||
|
/** The fridge stock AFTER this day's fetch, held until the day actually advances
|
||||||
|
* (committed in onNext) — so a mid-service reload re-fetches the same item and
|
||||||
|
* can't dodge the rot-cap or silently lose the ingredient. */
|
||||||
|
private pendingFridge: StoredItemSave[] | null = null;
|
||||||
|
|
||||||
private enterPan(): void {
|
private enterPan(): void {
|
||||||
const p = this.order.pan!;
|
const p = this.order.pan!;
|
||||||
|
this.panIngredient = null;
|
||||||
|
this.pendingFridge = null;
|
||||||
|
if (p.fromFridge && this.save.fridge?.length) {
|
||||||
|
// Reach into the fridge you stocked: FIFO, at whatever freshness your storage
|
||||||
|
// left it. The consumed stock is HELD (pendingFridge), not written yet.
|
||||||
|
const store = restoreStore(this.save.fridge);
|
||||||
|
const got = fetchStock(store, p.food);
|
||||||
|
if (got) {
|
||||||
|
const q = usableQuality(got);
|
||||||
|
this.panIngredient = { quality: q.quality, word: q.word };
|
||||||
|
this.pendingFridge = serializeStore(store);
|
||||||
|
}
|
||||||
|
}
|
||||||
this.pan.reset(p.food, p.fuel);
|
this.pan.reset(p.food, p.fuel);
|
||||||
this.hideAllScenes();
|
this.hideAllScenes();
|
||||||
this.pan.root.visible = true;
|
this.pan.root.visible = true;
|
||||||
@ -568,7 +601,7 @@ export class Game {
|
|||||||
this.timing = false;
|
this.timing = false;
|
||||||
const seconds = this.orderSeconds;
|
const seconds = this.orderSeconds;
|
||||||
const slice = this.kitchen.currentSlice;
|
const slice = this.kitchen.currentSlice;
|
||||||
const v = judgePan(result, this.order, seconds);
|
const v = judgePan(result, this.order, seconds, this.panIngredient ?? undefined);
|
||||||
this.save.total += v.total;
|
this.save.total += v.total;
|
||||||
const key = `day${this.order.day}`;
|
const key = `day${this.order.day}`;
|
||||||
this.save.best[key] = Math.max(this.save.best[key] ?? 0, v.total);
|
this.save.best[key] = Math.max(this.save.best[key] ?? 0, v.total);
|
||||||
@ -762,7 +795,7 @@ 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, total: s.total ?? 0, best: s.best ?? {} };
|
return { day: s.day, total: s.total ?? 0, best: s.best ?? {}, fridge: s.fridge };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@ -550,7 +550,7 @@ export function judgeFridge(h: ReturnType<typeof storeHealth>, order: Order, sec
|
|||||||
// M17 — The pan. Did both faces land a proper sear (the flip), did you cook in
|
// M17 — The pan. Did both faces land a proper sear (the flip), did you cook in
|
||||||
// good butter not burnt (the bitterness), and is the finished piece even.
|
// good butter not burnt (the bitterness), and is the finished piece even.
|
||||||
|
|
||||||
export function judgePan(r: PanResult, order: Order, seconds: number): Verdict {
|
export function judgePan(r: PanResult, order: Order, seconds: number, ingredient?: { quality: number; word: string }): Verdict {
|
||||||
const seared = clamp01(r.doneScore);
|
const seared = clamp01(r.doneScore);
|
||||||
const criteria: Criterion[] = [
|
const criteria: Criterion[] = [
|
||||||
{
|
{
|
||||||
@ -585,13 +585,32 @@ export function judgePan(r: PanResult, order: Order, seconds: number): Verdict {
|
|||||||
detail: `${Math.round(seconds)}s`,
|
detail: `${Math.round(seconds)}s`,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
// If the mushroom came out of the fridge, its freshness is judged on its own
|
||||||
|
// row — and a genuinely off ingredient CAPS the whole dish, because no sear
|
||||||
|
// saves rot. You can't cook your way out of bad storage.
|
||||||
|
if (ingredient) {
|
||||||
|
// Scored on the freshness BAND, not raw quality: a genuinely fresh ingredient
|
||||||
|
// is full marks (so perfect storage still leaves S on the table — the floors
|
||||||
|
// law), use-soon is a real dent, off/dangerous is near-zero and hard-caps below.
|
||||||
|
const bandScore = ingredient.word === 'fresh' ? 1 : ingredient.word === 'use soon' ? 0.55 : ingredient.word === 'off' ? 0.2 : 0;
|
||||||
|
criteria.push({
|
||||||
|
key: 'ingredient',
|
||||||
|
group: 'pan',
|
||||||
|
label: 'The Ingredient',
|
||||||
|
score: bandScore,
|
||||||
|
weight: 1.2,
|
||||||
|
detail: `off the shelf — ${ingredient.word}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
let sum = 0;
|
let sum = 0;
|
||||||
let wsum = 0;
|
let wsum = 0;
|
||||||
for (const c of criteria) {
|
for (const c of criteria) {
|
||||||
sum += c.score * c.weight;
|
sum += c.score * c.weight;
|
||||||
wsum += c.weight;
|
wsum += c.weight;
|
||||||
}
|
}
|
||||||
const total = (sum / wsum) * 10;
|
let total = (sum / wsum) * 10;
|
||||||
|
// A dangerous/off ingredient hard-caps the total — you served it, after all.
|
||||||
|
if (ingredient && ingredient.quality < 0.4) total = Math.min(total, 3 + ingredient.quality * 5);
|
||||||
const rankable = criteria.filter((c) => c.key !== 'time');
|
const rankable = criteria.filter((c) => c.key !== 'time');
|
||||||
const sorted = [...rankable].sort((a, b) => a.score - b.score);
|
const sorted = [...rankable].sort((a, b) => a.score - b.score);
|
||||||
void order;
|
void order;
|
||||||
|
|||||||
@ -367,6 +367,22 @@ Object.assign(BANK, {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// The fetched ingredient — the cook who reads a use-by date.
|
||||||
|
Object.assign(BANK, {
|
||||||
|
ingredient: {
|
||||||
|
bad: [
|
||||||
|
'This was off before it hit the pan. You stored it in the door, didn\'t you. Three days ago.',
|
||||||
|
'No sear saves a bad mushroom. The fridge did this, not the flame.',
|
||||||
|
'You cooked something that had already given up. I can taste the fridge door.',
|
||||||
|
'Fresh is a choice you make days early. You made the other one.',
|
||||||
|
],
|
||||||
|
good: [
|
||||||
|
'The mushroom was still perfect. Cold, back of the fridge, used in time. That is the whole craft before the pan even lights.',
|
||||||
|
'Fresh off the shelf. You store like someone who respects the ingredient.',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// The pan — the butter snob.
|
// The pan — the butter snob.
|
||||||
Object.assign(BANK, {
|
Object.assign(BANK, {
|
||||||
pansear: {
|
pansear: {
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { CUT_BANDS, type CutClass } from '../sim/slicing';
|
|||||||
import type { CutPattern } from '../sim/cutting';
|
import type { CutPattern } from '../sim/cutting';
|
||||||
import type { IngredientId } from '../sim/ingredients';
|
import type { IngredientId } from '../sim/ingredients';
|
||||||
import type { JuicerId } from '../sim/juicing';
|
import type { JuicerId } from '../sim/juicing';
|
||||||
|
import { FRIDGE_MUSHROOM_ID } from '../sim/coldchain';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A stop at the prep bench before the toaster. M11 shipped the 'cut' kind;
|
* A stop at the prep bench before the toaster. M11 shipped the 'cut' kind;
|
||||||
@ -100,8 +101,10 @@ export interface Order {
|
|||||||
* the whole day to the outdoor grill instead of the toaster. */
|
* the whole day to the outdoor grill instead of the toaster. */
|
||||||
grill?: string[];
|
grill?: string[];
|
||||||
/** The pan (M17): the food to sear, and the fuel the hob runs on. Presence
|
/** The pan (M17): the food to sear, and the fuel the hob runs on. Presence
|
||||||
* sends the day to the pan — butter it, foam it, flip it, don't burn it. */
|
* sends the day to the pan — butter it, foam it, flip it, don't burn it.
|
||||||
pan?: { food: string; fuel: string };
|
* `fromFridge` pulls the food out of your stored stock — how you stocked it
|
||||||
|
* days ago decides how fresh it is now. */
|
||||||
|
pan?: { food: string; fuel: string; fromFridge?: boolean };
|
||||||
/** The cold chain: a delivery to put away. Presence sends the day to the
|
/** The cold chain: a delivery to put away. Presence sends the day to the
|
||||||
* fridge — raw low, perishables cold, don't cram it, then three days pass. */
|
* fridge — raw low, perishables cold, don't cram it, then three days pass. */
|
||||||
fridge?: boolean;
|
fridge?: boolean;
|
||||||
@ -401,6 +404,21 @@ export const DAYS: Order[] = [
|
|||||||
text: 'The order came in. Put it away before the inspector does his rounds — raw meat down low where it can\'t drip, the perishables in the cold at the back, and don\'t cram one shelf or nothing breathes. Three days till he looks.',
|
text: 'The order came in. Put it away before the inspector does his rounds — raw meat down low where it can\'t drip, the perishables in the cold at the back, and don\'t cram one shelf or nothing breathes. Three days till he looks.',
|
||||||
fridge: true,
|
fridge: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
day: 18,
|
||||||
|
brand: 'the bakery loaf',
|
||||||
|
bread: 'white',
|
||||||
|
spread: 'butter',
|
||||||
|
amount: 'normal',
|
||||||
|
browning: 0.55,
|
||||||
|
browningName: 'golden',
|
||||||
|
noChar: true,
|
||||||
|
tool: 'butter_knife',
|
||||||
|
butterSoftness: 0.5,
|
||||||
|
who: 'the lunch rush',
|
||||||
|
text: 'Pan-sear the mushrooms — the ones from your own fridge. Whatever shape they\'re in now is the shape you stored them in. Butter, foam, both faces. No excuses about the delivery; that was your fridge.',
|
||||||
|
pan: { food: FRIDGE_MUSHROOM_ID, fuel: 'gas', fromFridge: true },
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
/** Days beyond the script — keep going, with everything cranked. */
|
/** Days beyond the script — keep going, with everything cranked. */
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import {
|
|||||||
coldStep,
|
coldStep,
|
||||||
storeHealth,
|
storeHealth,
|
||||||
ZONE_COLD,
|
ZONE_COLD,
|
||||||
|
FRIDGE_MUSHROOM_ID,
|
||||||
} from '../sim/coldchain';
|
} from '../sim/coldchain';
|
||||||
import { el, Panel } from '../ui/hud';
|
import { el, Panel } from '../ui/hud';
|
||||||
|
|
||||||
@ -151,6 +152,7 @@ export class FridgeView implements View {
|
|||||||
{ id: 'salad', perish: 1, rte: true, color: 0x5aa84a },
|
{ id: 'salad', perish: 1, rte: true, color: 0x5aa84a },
|
||||||
{ id: 'cheese', perish: 0.7, rte: true, color: 0xe6c34e },
|
{ id: 'cheese', perish: 0.7, rte: true, color: 0xe6c34e },
|
||||||
{ id: 'berries', perish: 1.6, rte: true, age: 1, color: 0x8a3d78 },
|
{ id: 'berries', perish: 1.6, rte: true, age: 1, color: 0x8a3d78 },
|
||||||
|
{ id: FRIDGE_MUSHROOM_ID, perish: 1.3, color: 0xd8cbb0 },
|
||||||
{ id: 'onion', perish: 0.3, color: 0xb99a5b },
|
{ id: 'onion', perish: 0.3, color: 0xb99a5b },
|
||||||
];
|
];
|
||||||
spec.forEach((sp, i) => {
|
spec.forEach((sp, i) => {
|
||||||
|
|||||||
@ -50,6 +50,10 @@ export const FRESH = 0.7;
|
|||||||
export const USE_SOON = 0.4;
|
export const USE_SOON = 0.4;
|
||||||
export const OFF = 0.15;
|
export const OFF = 0.15;
|
||||||
|
|
||||||
|
/** The mushroom that ties the fridge day (delivery) to the pan day (fetch). One
|
||||||
|
* id, so a rename can't silently uncouple the two days. */
|
||||||
|
export const FRIDGE_MUSHROOM_ID = 'button_mushroom';
|
||||||
|
|
||||||
/** Base spoilage per DAY at bench temp, before the item's own perishability.
|
/** Base spoilage per DAY at bench temp, before the item's own perishability.
|
||||||
* Spoilage lives on the day clock, not the second clock — food goes off across
|
* Spoilage lives on the day clock, not the second clock — food goes off across
|
||||||
* days (the cure system's twin), and a service leaves things out for a fraction
|
* days (the cure system's twin), and a service leaves things out for a fraction
|
||||||
@ -183,11 +187,15 @@ export function freshnessWord(it: StoredItem): FreshWord {
|
|||||||
* a serve you do not walk back.
|
* a serve you do not walk back.
|
||||||
*/
|
*/
|
||||||
export function usableQuality(it: StoredItem): { quality: number; safe: boolean; word: FreshWord } {
|
export function usableQuality(it: StoredItem): { quality: number; safe: boolean; word: FreshWord } {
|
||||||
const word = freshnessWord(it);
|
|
||||||
let q = it.freshness;
|
let q = it.freshness;
|
||||||
if (it.frozen) q *= 0.82; // thawed texture
|
if (it.frozen) q *= 0.78; // thawed texture — steep enough that a 3-day freeze on a
|
||||||
|
// mid-perishable no longer beats the coldest fridge shelf.
|
||||||
if (it.contaminated) q *= 0.4;
|
if (it.contaminated) q *= 0.4;
|
||||||
return { quality: Math.max(0, q), safe: it.freshness > OFF && !it.contaminated, word };
|
q = Math.max(0, q);
|
||||||
|
// The word describes what you're ACTUALLY handed (post-thaw, post-drip), not the
|
||||||
|
// raw freshness — a frozen item can read "fresh" on the shelf and "use soon" on the plate.
|
||||||
|
const word: FreshWord = q >= FRESH ? 'fresh' : q >= USE_SOON ? 'use soon' : q >= OFF ? 'off' : 'dangerous';
|
||||||
|
return { quality: q, safe: it.freshness > OFF && !it.contaminated, word };
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The inspector's storage read: hygiene, rotation, and what's gone off on your watch. */
|
/** The inspector's storage read: hygiene, rotation, and what's gone off on your watch. */
|
||||||
@ -211,3 +219,37 @@ export function storeHealth(s: ColdStore): { fresh: number; off: number; contami
|
|||||||
waste: s.wasteCount,
|
waste: s.wasteCount,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** A plain snapshot of the stock for the save file (no Rng, no functions). */
|
||||||
|
export type StoredItemSave = Omit<StoredItem, never>;
|
||||||
|
|
||||||
|
/** Flatten every zone's items to a plain array — each item already knows its zone. */
|
||||||
|
export function serializeStore(s: ColdStore): StoredItemSave[] {
|
||||||
|
const out: StoredItemSave[] = [];
|
||||||
|
for (const zone of Object.keys(s.zones) as ZoneId[]) for (const it of s.zones[zone]) out.push({ ...it });
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Rebuild a store from a save snapshot (a fresh Rng — spoilage is deterministic).
|
||||||
|
* Corruption-tolerant: a garbled save (unknown zone, non-array, junk item) yields
|
||||||
|
* an empty/partial store rather than an uncaught crash — the save intent of loadSave. */
|
||||||
|
export function restoreStore(items: StoredItemSave[], seed = 20260720): ColdStore {
|
||||||
|
const s = newColdStore(seed);
|
||||||
|
if (Array.isArray(items)) for (const it of items) if (it && s.zones[it.zone]) s.zones[it.zone].push({ ...it });
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Take one item of `id` out of the store, oldest-first (FIFO) across the whole
|
||||||
|
* fridge — the cook reaches in for stock. Returns null if you've none. */
|
||||||
|
export function fetch(s: ColdStore, id: string): StoredItem | null {
|
||||||
|
let found: { zone: ZoneId; idx: number; it: StoredItem } | null = null;
|
||||||
|
for (const zone of Object.keys(s.zones) as ZoneId[]) {
|
||||||
|
const arr = s.zones[zone];
|
||||||
|
for (let i = 0; i < arr.length; i++) {
|
||||||
|
if (arr[i].id === id && (!found || arr[i].age > found.it.age)) found = { zone, idx: i, it: arr[i] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!found) return null;
|
||||||
|
s.zones[found.zone].splice(found.idx, 1);
|
||||||
|
return found.it;
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user