import { INGREDIENTS, type IngredientId } from './ingredients'; /** * The temperature clock, generalized from butter softness (M2) to any perishable * that drifts on the bench. * * Butter softness was always a static dial the order handed you. This is that * dial made real: a cheese sat in the fridge is cold and firm; left on the bench * it warms toward soft in real time, on its own `softensInSec`. Two perishables, * two opposite needs, and that is the whole planning game of the bruschetta: * * brie needsSoft — spreadable by assembly time. Get it OUT early or it * places like a stone. * parmesan needsCold — shaves clean only cold (the M13 grater rule: warm * cheese smears). Leave it IN until you need it. * * You set each state at order start and can revisit it — for a price in service * time, because opening the fridge again mid-service costs you (the game charges * the seconds; this sim just flips the state). The pressure is felt through the * ticket hint and the clock, never a tutorial: by the time you assemble, the * cheese is however warm your planning left it, and the judge tastes it. * * Pure and time-stepped — no three.js — so the harness can warm a cheese headless * and read exactly how ready it is. */ export type TempState = 'fridge' | 'bench'; export interface Perishable { id: IngredientId; name: string; state: TempState; /** 0 = fridge-cold and firm, 1 = out-all-morning warm and soft. */ temp: number; /** Wants to be warm/soft by assembly (brie). */ needsSoft: boolean; /** Wants to be cold/firm at assembly (parmesan). */ needsCold: boolean; /** Seconds on the bench to drift a full 0→1. Fridge cools at the same rate. */ softensInSec: number; } export interface TempReadiness { /** The perishable's current 0..1 warmth. */ temp: number; /** 0..1 — how well it meets its need right now. */ score: number; /** The readout word, in the judge's own thresholds. */ word: string; } /** A perishable starts cold in the fridge — you decide what comes out. */ function makePerishable(id: IngredientId): Perishable { const ing = INGREDIENTS[id]; return { id, name: ing.name, state: 'fridge', temp: 0, needsSoft: !!ing.temp?.needsSoft, needsCold: !!ing.temp?.needsCold, softensInSec: ing.temp?.softensInSec ?? 90, }; } export class TempClock { readonly items: Perishable[]; constructor(ids: IngredientId[]) { this.items = ids.map(makePerishable); } get(id: IngredientId): Perishable | undefined { return this.items.find((p) => p.id === id); } /** Flip fridge↔bench. The game charges the service-time cost; this just flips. */ toggle(id: IngredientId): void { const p = this.get(id); if (p) p.state = p.state === 'fridge' ? 'bench' : 'fridge'; } setState(id: IngredientId, state: TempState): void { const p = this.get(id); if (p) p.state = state; } /** Drift every perishable one tick toward its state's target temperature. */ step(dt: number): void { if (dt <= 0) return; for (const p of this.items) { const target = p.state === 'bench' ? 1 : 0; const rate = dt / p.softensInSec; if (p.temp < target) p.temp = Math.min(target, p.temp + rate); else if (p.temp > target) p.temp = Math.max(target, p.temp - rate); } } /** How ready one perishable is for assembly, scored against its need. */ readiness(id: IngredientId): TempReadiness { const p = this.get(id); if (!p) return { temp: 0, score: 1, word: 'n/a' }; return readinessOf(p); } /** * The one number The Bench temperature row reads: the whole plan's readiness, * dragged down to its worst perishable — one cold-hard brie or one sweaty * parmesan is enough to hear about it. */ overall(): { score: number; worst: Perishable; word: string } { let worst = this.items[0]; let worstScore = 1; for (const p of this.items) { const s = readinessOf(p).score; if (s < worstScore) { worstScore = s; worst = p; } } return { score: worstScore, worst, word: readinessOf(worst).word }; } } const clamp01 = (v: number) => (v < 0 ? 0 : v > 1 ? 1 : v); function readinessOf(p: Perishable): TempReadiness { if (p.needsSoft) { // Soft is the whole point: cold-hard scores nothing, spreadable scores full. const score = clamp01((p.temp - 0.12) / 0.45); const word = p.temp < 0.2 ? 'fridge-hard' : p.temp < 0.5 ? 'softening' : 'spreadable'; return { temp: p.temp, score, word }; } if (p.needsCold) { // Cold shaves clean; warm smears (the grater's lesson, one clock over). const score = clamp01(1 - (p.temp - 0.2) / 0.45); const word = p.temp < 0.25 ? 'cold' : p.temp < 0.55 ? 'sweating' : 'smearing'; return { temp: p.temp, score, word }; } return { temp: p.temp, score: 1, word: 'fine' }; }