PURPOSE

Zustand store that holds “frozen” before-values for wallet, counter, and progress displays during reward presentation so the UI can show pre-reward state while animations and chips play, then release back to live store values once each animation completes. Currently a stub: lockForBatch, releaseProgress, and releaseCounter are no-ops; only releaseAll and the walletOverrides field are functional. Exported alongside the store is a progressKey helper that composes a track plus optional trackId into a stable override key.

OWNS

  • walletOverrides: Map<string, number> | null — frozen wallet display values keyed by currency name (gems, credits); null when no override is active.
  • The override lifecycle API surface: lockForBatch(batch), releaseProgress(key), releaseCounter(counter), releaseAll().
  • The progressKey(track, trackId?) key-composition function.

READS FROM

  • RewardBatch type from ../data/reward-types (parameter type for lockForBatch).
  • Nothing else at runtime — the store is self-contained.

PUSHES TO

  • React subscribers via Zustand selectors. V32Shell reads walletOverrides and substitutes walletOverrides?.get('gems') ?? liveGems (and same for credits) so the hub HUD shows the frozen value while a reward batch is mid-presentation.

DOES NOT

  • Mutate walletStore or inventoryStore — it only holds an overlay value; the underlying wallet/inventory stores are updated by the reward finalizer pipeline separately.
  • Track progress-bar or counter override values yet — the stub does not populate any progress/counter map, so consumers currently fall back to live values.
  • Drive animations or timing — it only freezes/unfreezes display values; RewardOrchestrator and HomeResolveController decide when to call the release methods.

Signals

  • walletOverrides flips from null to a populated Map when lockForBatch is wired, and back to null on releaseAll. Selector subscribers re-render on change.

Entry points

  • useDisplayOverrideStore — Zustand hook; called as a selector (useDisplayOverrideStore(s => s.walletOverrides) in V32Shell) or imperatively (useDisplayOverrideStore.getState().lockForBatch(batch) in reward-finalizers).
  • progressKey(track, trackId?) — used by HomeResolveController to build the key passed to releaseProgress after a bar animation finishes.
  • lockForBatch(batch) — called from reward-finalizers at the end of each finalize path, guarded by !isNoOpBatch(batch), before enqueueing the batch onto rewardQueueStore.
  • releaseCounter(counter) — called from RewardOrchestrator after the collect phase, iterating the batch’s counters deltas.
  • releaseProgress(key) — called from HomeResolveController once a progress bar animation completes for a given track delta.
  • releaseAll() — called from RewardOrchestrator when the queue drains (next phase is null).

Pattern notes

  • Stub status is intentional and load-bearing: the call sites in reward-finalizers, RewardOrchestrator, and HomeResolveController already expect the lifecycle, so the eventual non-stub implementation slots in without changing callers.
  • The store is the chosen seam for the “display vs. truth” split — live stores (walletStore, inventoryStore, progress tracks) are always current; this overlay layer is the only place that lies to the UI, and it lies only for the duration of a reward presentation.
  • Override keys are namespaced by domain: currency keys are bare strings (gems, credits); progress keys go through progressKey(track, trackId?) which yields track or track:trackId. Counter keys use the delta.counter string directly.
  • Imperative getState() access is used from non-React code paths (finalizers, advance functions); React components use the hook with a selector to get reactivity.
  • releaseAll is the only mutator currently implemented and unconditionally clears walletOverrides to null; it does not touch progress/counter maps because none exist on the state yet.