import { type HeatSource, newHeatSource, setKnob, heatStep } from './heatsource'; /** * THE POACH — almost a whole game, and treated like one. * * Four acts, none skippable: * * THE WATER — the pot chases the knob on the fuel's lag (the heat-source * bone). The poach wants the SHIVER — that band where the surface * trembles but nothing rolls. Too cool and the white disperses into * rags; a rolling boil and it tears the egg apart while it cooks. * THE VORTEX — swirl the water first. A good vortex wraps the white around * the yolk as it lands; drop into still water and the white walks off * on its own. The vortex DECAYS — swirl, then drop, not swirl and * admire. * THE DROP — from low, into the eye of the spin. Height splats it. * THE SET — white sets first, then the yolk starts to firm. Lift when the * white is set and the yolk still trembles; every second past that is * a second toward a boiled egg in disguise. Then DRAIN it on the * spoon — nobody wants poach water on the plate. * * The wobble test at the pass reads yolkSet: the judge presses it, on camera. * "It should tremble. Like you, right now." * * Pure — no three.js — the harness poaches headless. */ /** The shiver: where a poach wants the water. Above BOIL it tears. */ export const SHIVER_LO = 5.8; export const SHIVER_HI = 7.4; export const BOIL_AT = 8.2; /** White is SET past here (snotty below ~0.6). */ export const WHITE_SET = 0.85; /** Yolk still trembles below here; past it you've boiled an egg in disguise. */ export const YOLK_TREMBLE = 0.35; /** How fast the vortex dies once you stop stirring. */ const VORTEX_DECAY = 0.09; export interface PoachSession { src: HeatSource; /** Vortex strength 0..1 — built by swirling, decays on its own. */ vortex: number; dropped: boolean; lifted: boolean; /** 0 raw .. 1 fully set. */ whiteSet: number; /** 0 liquid .. 1 hard. The wobble test reads this. */ yolkSet: number; /** White lost to rags — still water, a high drop, or a rolling boil. */ rags: number; /** Poach water still clinging — drain on the spoon before the pass. */ cling: number; /** Vortex strength at the moment of the drop (the judged number). */ vortexAtDrop: number; tempAtDrop: number; seconds: number; } export function newPoachSession(fuel = 'gas'): PoachSession { return { src: newHeatSource(fuel), vortex: 0, dropped: false, lifted: false, whiteSet: 0, yolkSet: 0, rags: 0, cling: 1, vortexAtDrop: 0, tempAtDrop: 0, seconds: 0, }; } export function setPotKnob(s: PoachSession, k: number): void { setKnob(s.src, k); } /** Stir: add angular sweep (radians this frame) into the vortex. */ export function swirl(s: PoachSession, sweep: number): void { if (s.dropped) { // Stirring a dropped egg is vandalism — it rags the white directly. s.rags = Math.min(1, s.rags + Math.abs(sweep) * 0.06); return; } s.vortex = Math.min(1, s.vortex + Math.abs(sweep) * 0.055); } /** * Drop the egg from `height` (0 rim-low .. 1 show-off). The vortex catches the * white; still water and altitude both cost you form, immediately. */ export function dropEgg(s: PoachSession, height = 0): void { if (s.dropped) return; s.dropped = true; s.vortexAtDrop = s.vortex; s.tempAtDrop = s.src.output; s.rags = Math.min(1, s.rags + (1 - s.vortex) * 0.45 + height * 0.4); // Cold water at the drop lets the white walk before any heat can catch it. if (s.src.output < SHIVER_LO) s.rags = Math.min(1, s.rags + (SHIVER_LO - s.src.output) * 0.12); } /** Lift it out on the spoon. The set stops; the drain begins. */ export function liftEgg(s: PoachSession): void { if (s.dropped) s.lifted = true; } /** Hold it on the spoon: cling drains away. Patience, again. */ export function drainStep(s: PoachSession, dt: number): void { if (s.lifted) s.cling = Math.max(0, s.cling - dt / 4); } export function poachStep(s: PoachSession, dt: number): void { heatStep(s.src, dt); s.vortex = Math.max(0, s.vortex - VORTEX_DECAY * dt); const t = s.src.output; if (s.dropped && !s.lifted) { // The white sets fastest in the shiver; cooler is slower (and it rags on // the way in); a boil sets it fast but TEARS it the whole time. const inBand = t >= SHIVER_LO && t <= SHIVER_HI; const rate = inBand ? 0.055 : t > SHIVER_HI ? 0.07 : t > 4 ? 0.03 : 0.012; s.whiteSet = Math.min(1, s.whiteSet + rate * dt); if (t > BOIL_AT) s.rags = Math.min(1, s.rags + (t - BOIL_AT) * 0.045 * dt); // The yolk only starts once the white is mostly there. if (s.whiteSet > 0.75) s.yolkSet = Math.min(1, s.yolkSet + 0.035 * (inBand ? 1 : 1.4) * dt); } s.seconds += dt; } export interface PoachResult { /** Set, not snotty. */ whiteScore: number; /** The wobble: liquid yolk full marks, hard yolk none. */ yolkScore: number; /** One neat comet, not a pot of rags. */ formScore: number; /** Drained on the spoon, or served with its own puddle. */ drainScore: number; whiteSet: number; yolkSet: number; rags: number; cling: number; vortexAtDrop: number; wobbleWord: string; } const clamp01 = (v: number) => (v < 0 ? 0 : v > 1 ? 1 : v); export function poachResult(s: PoachSession): PoachResult { const whiteScore = !s.dropped ? 0 : clamp01((s.whiteSet - 0.55) / (WHITE_SET - 0.55)); const yolkScore = !s.dropped ? 0 : clamp01(1 - Math.max(0, s.yolkSet - YOLK_TREMBLE) / 0.4); const formScore = clamp01(1 - s.rags * 1.3); const drainScore = clamp01(1 - s.cling * 0.9); const wobbleWord = !s.dropped ? 'there is no egg' : s.yolkSet < 0.15 && s.whiteSet < WHITE_SET ? 'it collapses — raw inside' : s.yolkSet <= YOLK_TREMBLE ? 'it TREMBLES — perfect' : s.yolkSet < 0.6 ? 'a stiff little nod' : 'it does not move at all'; return { whiteScore, yolkScore, formScore, drainScore, whiteSet: s.whiteSet, yolkSet: s.yolkSet, rags: s.rags, cling: s.cling, vortexAtDrop: s.vortexAtDrop, wobbleWord, }; } /** The water's diegetic tell — never a thermometer. */ export function waterWord(s: PoachSession): string { const t = s.src.output; if (t < 3) return 'still water'; if (t < SHIVER_LO) return 'warming — not yet'; if (t <= SHIVER_HI) return 'a gentle SHIVER — now'; if (t <= BOIL_AT) return 'simmering hard'; return 'ROLLING — it will tear'; }