1import { WIN_TIMING } from '../render/stage-win.js';23// The card starts after the winning-cell ring, and its action row is the last4// entrance beat. A confirmation before that beat settles the card once.5export const WIN_ENTRANCE_END = WIN_TIMING.cardDelay + WIN_TIMING.actions + 200;67/** Scene intent state; the engine remains the sole owner of the won result. */8export function createWinFlow({ clock, reducedMotion = false, onChange = () => {}, onContinue = () => {} } = {}) {9 if (!clock || typeof clock.now !== 'function' || typeof clock.setTimeout !== 'function')10 throw new TypeError('Win flow requires a presentation clock');11 let start = 0;12 let skip = false;13 let exitStart = 0;14 let exitTimer = null;15 let active = false;16 let generation = 0;1718 function cancel() {19 generation += 1;20 if (exitTimer !== null) clock.clearTimeout(exitTimer);21 exitTimer = null;22 active = false;23 exitStart = 0;24 }2526 function enter({ instant = false } = {}) {27 cancel();28 active = true;29 start = clock.now();30 skip = Boolean(instant || reducedMotion);31 onChange();32 }3334 function confirm() {35 if (!active) return false;36 if (exitStart) return true;37 if (!skip && !reducedMotion && clock.now() - start < WIN_ENTRANCE_END) {38 skip = true;39 onChange();40 return true;41 }42 // Zero is the renderer's "no exit" sentinel, including on the first frame.43 exitStart = Math.max(Number.EPSILON, clock.now());44 if (reducedMotion) {45 cancel();46 onContinue();47 return true;48 }49 const thisGeneration = generation;50 onChange();51 exitTimer = clock.setTimeout(() => {52 if (generation !== thisGeneration || !active) return;53 cancel();54 onContinue();55 }, WIN_TIMING.exit);56 return true;57 }5859 return Object.freeze({60 enter,61 confirm,62 cancel,63 get view() { return { winStart: start, winSkip: skip, winExitStart: exitStart }; },64 get active() { return active; },65 get exiting() { return Boolean(exitStart); },66 });67}