feat(02-01): BigQty + scheduler + sim foundations

- Install zustand@^5.0.0 + break_eternity.js@^2.1.3 as dependencies
- BigQty immutable wrapper around Decimal (D-31): factories,
  arithmetic, comparison, JSON round-trip, saturating coercion
- formatHumanReadable for K/M/B/T/scientific HUD readouts (UX-11)
- Clock interface + wallClock + FakeClock — only file in src/sim/
  allowed to read Date.now() (D-33)
- drainTicks fixed-timestep accumulator: refuses negative deltas
  (CORE-11), clamps at MAX_OFFLINE_MS=24h (CORE-03), TICK_MS=200
- computeOfflineCatchup pure descriptor for offline boundaries
- SimState root shape with BLOCKER 3 split: lastTickAt
  (wall-clock, app-layer-only) + tickCount (sim-internal monotonic)
- 52 tests across big-qty / format / clock / tick / catchup all green
This commit is contained in:
2026-05-09 09:14:10 -04:00
parent 5ddaabcdc1
commit 58db53227c
16 changed files with 801 additions and 4 deletions
+40
View File
@@ -0,0 +1,40 @@
/**
* 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;
};
}