import { create, type StoreApi, type UseBoundStore } from "zustand"; import { createDungeonEncounterSnapshot, reduceDungeonEncounter, registerDungeonEncounterProvider, resetDungeonEncounterSnapshot, validateDungeonEncounterDefinition, } from "./dungeonEncounterRuntime"; import type { DungeonEncounterDefinition, DungeonEncounterEffect, DungeonEncounterEvent, DungeonEncounterResetScope, DungeonEncounterSnapshot, } from "./dungeonEncounterTypes"; export interface DungeonEncounterStoreState extends DungeonEncounterSnapshot { readonly dispatch: (event: DungeonEncounterEvent) => readonly DungeonEncounterEffect[]; readonly reset: (scope?: DungeonEncounterResetScope, now?: number) => void; } function serializableSnapshot(state: DungeonEncounterStoreState): DungeonEncounterSnapshot { return { schemaVersion: state.schemaVersion, encounterId: state.encounterId, phase: state.phase, phaseStartedAt: state.phaseStartedAt, flags: state.flags, counters: state.counters, timers: state.timers, worldObjectStates: state.worldObjectStates, transitionStates: state.transitionStates, activeSpawnGroupIds: state.activeSpawnGroupIds, completedObjectiveIds: state.completedObjectiveIds, consumedActionIds: state.consumedActionIds, checkpointId: state.checkpointId, revision: state.revision, }; } export function createDungeonEncounterStore( definition: DungeonEncounterDefinition, ): UseBoundStore> { validateDungeonEncounterDefinition(definition); const useStore = create((set, get) => ({ ...createDungeonEncounterSnapshot(definition), dispatch: (event) => { const snapshot = serializableSnapshot(get()); const result = reduceDungeonEncounter(definition, snapshot, event); if (result.snapshot !== snapshot) set(result.snapshot); return result.effects; }, reset: (scope = "dungeon", now = 0) => { set(resetDungeonEncounterSnapshot(definition, serializableSnapshot(get()), scope, now)); }, })); const provider = { definition, encounterId: definition.id, dungeonId: definition.dungeonId, getSnapshot: () => serializableSnapshot(useStore.getState()), dispatch: (event: DungeonEncounterEvent) => useStore.getState().dispatch(event), reset: (scope: DungeonEncounterResetScope, now = 0) => useStore.getState().reset(scope, now), replaceSnapshot: (snapshot: DungeonEncounterSnapshot) => { if (snapshot.encounterId !== definition.id) { throw new Error(`Cannot hydrate ${definition.id} from ${snapshot.encounterId}`); } useStore.setState(snapshot); }, }; registerDungeonEncounterProvider(provider); return useStore; }