/** * SimState — root shape of the in-memory sim world. Structurally * compatible with V1Payload from src/save/migrations.ts (a SimState * round-trips to a V1Payload via the application layer). * * Wave 0 ships placeholder unknown[] for tiles/plants — Wave 1 (Plan 02-02) * fleshes them out with real interfaces in src/sim/garden/types.ts. * * BLOCKER 3 invariant — two distinct time fields with strict separation: * - lastTickAt: wall-clock milliseconds. Written ONLY by the application * layer at saveSync time (src/PhaserGame.tsx). The sim NEVER writes * this field. computeOfflineCatchup reads it as wall-clock ms. * - tickCount: monotonically-increasing sim-internal counter (one per * simulate() call). Used for STRY-10 narrative gating that must be * immune to wall-clock manipulation. The sim DOES write this field. * The application layer reads it but never writes it. */ export interface SimState { garden: { tiles: unknown[] }; plants: unknown[]; harvestedFragmentIds: string[]; /** Wall-clock milliseconds at last save. Written ONLY at saveSync. */ lastTickAt: number; /** Monotonic sim tick counter. Incremented by the sim; used for STRY-10. */ tickCount: number; unlockedPlantTypes: string[]; luraBeatProgress: { arrived: boolean; mid: boolean; farewell: boolean; pending: 'arrival' | 'mid' | 'farewell' | null; }; offlineEvents: unknown | null; settings: { musicVolume: number; ambientVolume: number; sfxVolume: number; persistenceToastShown: boolean; }; }