Cinematic State Machine
The reward-reveal cinematic is a single state machine driven by rewardState
(typed RewardState) and a per-state stopwatch rewardTimer. Every frame,
updateRewardState(dt) adds dt to rewardTimer and, when a state’s dwell
duration is exceeded, advances to the next state and resets the timer to 0.
The whole reveal pipeline — banner, slot machine, card hand, autopick, fly
to center, particle burst, Balatro-style upgrade animation, mod merge —
hangs off this one variable. See also: cinematic-pipeline.
RewardState union
The RewardState type is the canonical contract — defined in
engine/rendering/hud.ts and mirrored in
engine/rendering/reward-cinematics/registry.ts (duplicated to avoid a
circular import; the two definitions must be kept in sync).
type RewardState =
| 'none' | 'announce' | 'slot_intro' | 'slot_lock' | 'shine'
| 'showing' | 'fadeout' | 'fly_center' | 'burst'
| 'upgrade_show' | 'merge_show';State catalog
Each state has a fixed visual signature and a dwell duration. Most durations
are module-level constants in hud.ts. slot_intro is the exception — it
defaults to DEFAULT_SLOT_INTRO_TIME (2.5 s) but cinematic modules may
override it via setSlotIntroDuration(seconds) to size the dwell to the
hand’s rarity. The override is reset to default on every fresh
startRewardReveal() call.
| State | Dwell (s) | Constant | Visual signature |
|---|---|---|---|
none | — | — | Idle. No reveal active. updateRewardState early-returns. Last frame fires cinematic.onEnd exactly once. |
announce | 0.4 | ANNOUNCE_TIME | Cards exist at final positions but alpha = 0 — banner / backdrop / cinematic drawBackdrop paints in. Bridge frame; cinematics start their reveal here. |
slot_intro | 2.5 (overridable) | DEFAULT_SLOT_INTRO_TIME / SLOT_INTRO_TIME | Slot-machine reveal. Each card flies in from a CARD_ORIGINS corner ([-1.4, 0.2], [0, -1.4], [1.4, 0.2] in screen-relative units) with staggered arrival times CARD_ARRIVE = [0.05, 0.23, 0.41], travel CARD_TRAVEL = 0.22, and _cardEase overshoot (1.08). Alpha ramps min(1, t*2), scale 0.6 → 1. |
slot_lock | 0.05 | SLOT_LOCK_TIME | Minimal beat between the slot intro ending and the shine. Cards pulse 1 + 0.05 * sin(stateTime * 20) at locked positions. |
shine | 0.10 | SHINE_TIME | Brief post-lock shine sweep across each card before interaction opens. |
showing | open | — | Interactable. Player taps a card or the autopick timer fires. showingTimer runs in parallel (gated by _autopickPaused): at AUTOPICK_IDLE = 5.0 s the countdown shows; at AUTOPICK_IDLE + AUTOPICK_COUNTDOWN = 10.0 s, shouldAutoPick() returns true. Reroll / banish UI is live. |
fadeout | 0.6 | FADE_OUT_TIME | Cards fade out (alpha = 1 - t/FADE_OUT_TIME) and grow slightly (scale = 1 + t*0.5). Terminal — on completion, rewardState returns to none, selection clears, vanish state resets, game.banishTargeting = false. |
fly_center | 0.38 | FLY_CENTER_TIME | Selected card eases (ease-in-out, t<0.5 ? 2t² : 1 - 2(1-t)²) to top-center (destX = (W - cardW)/2, destY = H * 0.06); unselected cards shrink and fade (alpha = 1 - 2t, scale = 1 - 0.3t). Plays for picked cards before the burst phase. |
burst | 0.72 | BURST_TIME | Selected card sits at top-center; gold-dust particles (32 spawn for upgrades, none for new-weapon coin flip) explode from the card center, drift for 0.15 s, then attract toward the HUD slot at _burstTargetX, _burstTargetY with growing force (min(800, 400 + rewardTimer * 600)) and 0.92 drag per frame. MicroSfx.playCardBurst plays once on entry. Cleans up on dwell completion. |
upgrade_show | variable | UPGRADE_ITEM_TIME * n + UPGRADE_HOLD_TIME | Balatro-style sequential upgrade reveal. Per-item duration scales with item count: 1–3 items get the full UPGRADE_ITEM_REF_TIME = 2.20 s; 4+ items cap total at ~6 s (max(0.55, 6.0 / n)). Each item’s badge flies from its HUD slot to screen center (overshoot ease), holds with breathing, optionally liquid-gold-fills bottom-to-top for level-ups, flips to the new number, shrinks, and returns. NEW items skip the gold fill. No backdrop, no banner, no dim overlay — just the moving HUD badges and a geyser. Drives _slotAnims so the live HUD badges are the things that animate. |
merge_show | module-driven | — | Mod merge cinematic. Drawing and exit conditions live in the active _mergeCin cinematic module; hud.ts defers to the registered module for layout, particles, and termination. |
Linear progression
The canonical happy path for an interactive reveal:
none → announce → slot_intro → slot_lock → shine → showing
→ fly_center → burst → none
Transitions inside updateRewardState:
announce(>=ANNOUNCE_TIME) →slot_intro, timer resetslot_intro(>=SLOT_INTRO_TIME) →slot_lock, timer resetslot_lock(>=SLOT_LOCK_TIME) →shine, timer resetshine(>=SHINE_TIME) →showing, timer reset,showingTimer = 0showingis exited by card selection (orshouldAutoPick) —pickRewardor equivalent sets state tofadeout,fly_center,upgrade_show, ormerge_showdepending on reward typefadeout(>=FADE_OUT_TIME) →none, clears selection / vanish / banish targetingfly_center(>=FLY_CENTER_TIME) →burst, spawns particles, plays burst chimeburst(>=BURST_TIME) →none
Non-linear entry points
startRewardReveal(family, level?)— full pipeline fromannounce. ResetsSLOT_INTRO_TIMEto default, clears vanish state, firescinematic.onStart.startUpgradeShowOnly(family)— jumps straight toupgrade_show, skippingannounceand the card phases. Used by the Event Reward artifact for auto-applied upgrades. Caller must have already populated_upgradeItemsviaprepareUpgradeShow.- Mod merge — enters
merge_showdirectly from the merge trigger path.
Cinematic dispatch
updateRewardState tracks the previous state in _prevRewardState. On every
transition where rewardState !== _prevRewardState, the active cinematic
module (resolved via getCinematicFor(rewardFamily)) receives an
onStateTransition(fromState, toState, ctx) call with a freshly built
CinematicContext. On return to none, onEnd(ctx) fires exactly once.
The context snapshot carries time, dt (= _lastRewardDt), the current
state, stateTimer (= rewardTimer), family, choices, selectedIndex,
viewport dimensions, and card layout. Cinematic modules read these to drive
per-frame behavior without touching hud.ts internals.
Cross-references
cinematic-pipeline— module registry, hook lifecycle, family routingengine/rendering/hud.ts— state machine, dwell constants, card position math, particle updateengine/rendering/reward-cinematics/registry.ts—RewardStatemirror,CinematicContext,CinematicModulecontract