HomeResolveController
PURPOSE
Hub-screen controller that drives the 'resolve' phase of a reward batch. Sequentially animates one AnimatedBar per ProgressDelta, then advances the orchestrator. Render-null outside 'resolve'. Mounted inside HubScreen only.
OWNS
- Local React state
activeBarIndex: number— index of the bar currently animating;-1means idle / not rendering. sortedDeltasref — frozen copy ofactiveBatch.progresssorted byTRACK_ORDER, snapshot at resolve start.- Module-local
TRACK_ORDER: ProgressTrack[](currently['journey']) and thetrackSortKeyhelper. - Module-local
defaultGetXpForLevelcurve passed to everyAnimatedBar. - Module-local
trackColor(track)mapping (per-track bar color). - Container
<div>styling (padding: '8px 16px').
READS FROM
useRewardQueueStore—phase,activeBatch,surfaceReady(selector subscriptions);getState().activeBatchinsidehandleBarCompleteto read the live batch without resubscribing.ProgressDelta/ProgressTrackfrom../data/reward-types(type-only).activeBatch.progress— source of bar definitions. Readsdelta.track,delta.trackId, and (viaAnimatedBar)delta.before/delta.after. Does NOT read live store progression values.
PUSHES TO
useDisplayOverrideStore.getState().releaseProgress(progressKey(track, trackId))— released once per bar as it completes.useRewardQueueStore’ssurfaceReady()— called once on mount to drain deferred resolve backlog.advanceOrFinish(batch, 'resolve')from./RewardOrchestrator— called when all bars finish, OR immediately ifactiveBatch.progressis empty.<AnimatedBar />children — one per delta, withonCompletewired only on the bar whose index equalsactiveBarIndex; preceding bars receiveskip={true}so they render in their final state without re-animating.
DOES NOT
- Does not mutate
activeBatchor store-side progression values directly. - Does not animate concurrently — bars fill sequentially in
TRACK_ORDER. - Does not handle the
'milestones'or'rewards'phases — those are sibling phases reached only viaadvanceOrFinish. - Does not render when
phase !== 'resolve', whenactiveBatchis null, or whenactiveBarIndex < 0. - Does not acquire display overrides — only releases them. Acquisition happens upstream when the batch enters the queue.
- Does not compute XP curves from live data — uses the flat
defaultGetXpForLevelplaceholder. - Does not own the lifecycle of
'journey'-class progression — only the visual reveal.
Signals
| Signal | Source | Effect |
|---|---|---|
| Mount | React | Calls surfaceReady() once. |
phase / activeBatch change | Zustand selector | Re-sorts deltas and sets activeBarIndex = 0, or -1 if not in resolve. Empty progress array short-circuits to advanceOrFinish. |
AnimatedBar.onComplete | Active child bar only | handleBarComplete — releases that track’s override, increments activeBarIndex, or finishes via advanceOrFinish(batch, 'resolve') when no bars remain. |
Entry points
- Exported function component
HomeResolveController()— only public surface. - Mounted by
HubScreen. No props.
Pattern notes
- Sequential bars via index gating. Every delta renders an
AnimatedBar, butonCompleteis bound to a no-op for non-active bars. Bars atidx < activeBarIndexgetskip={true}(final-state, no animation); the bar atidx === activeBarIndexis the one that ticks; bars beyond are present but inert. This keeps mount identity stable across the sequence — the key is`${delta.track}-${delta.trackId ?? ''}`. - Snapshot-then-iterate. Deltas are copied into a ref at resolve start (
[...activeBatch.progress].sort(...)) so later store mutations toactiveBatch.progresscannot reshape the in-flight sequence. - Imperative store read inside the callback.
handleBarCompletereadsuseRewardQueueStore.getState().activeBatchrather than the closed-over selector value, so the callback always sees the current batch and isn’t invalidated by stale closures. - Empty-progress fast path. A batch with
progress.length === 0is forwarded toadvanceOrFinishwithout rendering any bars — keeps resolve from blocking the queue. - Release-only contract on
displayOverrideStore. Acquire happens elsewhere; this controller is the canonical release site for'resolve'-phase progress overrides. TRACK_ORDERis extensible. Unknown tracks sort to999(end of list) so adding a track without updating order still renders deterministically at the tail.
EXTRACT-CANDIDATE: The pattern “render N children sequentially, gate onComplete to one at a time, skip earlier ones” recurs in any phase controller that consumes a batch list (resolve, milestones, rewards). Worth lifting to a useSequentialReveal(items, onAllDone) helper if a second controller adopts it.
EXTRACT-CANDIDATE: defaultGetXpForLevel is a flat 100-XP-per-level placeholder marked for replacement by real progression curves. When the real curve lands it should live in a shared progression module (data table + lookup) rather than inline in this file — gameplay numbers belong on a balance page, not in a UI controller.
EXTRACT-CANDIDATE: trackColor maps ProgressTrack → CSS color. As soon as a second track ships, this should move next to the ProgressTrack type (or to a small track-presentation.ts) so the UI palette has one canonical source.