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.

StateDwell (s)ConstantVisual signature
noneIdle. No reveal active. updateRewardState early-returns. Last frame fires cinematic.onEnd exactly once.
announce0.4ANNOUNCE_TIMECards exist at final positions but alpha = 0 — banner / backdrop / cinematic drawBackdrop paints in. Bridge frame; cinematics start their reveal here.
slot_intro2.5 (overridable)DEFAULT_SLOT_INTRO_TIME / SLOT_INTRO_TIMESlot-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_lock0.05SLOT_LOCK_TIMEMinimal beat between the slot intro ending and the shine. Cards pulse 1 + 0.05 * sin(stateTime * 20) at locked positions.
shine0.10SHINE_TIMEBrief post-lock shine sweep across each card before interaction opens.
showingopenInteractable. 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.
fadeout0.6FADE_OUT_TIMECards 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_center0.38FLY_CENTER_TIMESelected 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.
burst0.72BURST_TIMESelected 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_showvariableUPGRADE_ITEM_TIME * n + UPGRADE_HOLD_TIMEBalatro-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_showmodule-drivenMod 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 reset
  • slot_intro (>= SLOT_INTRO_TIME) → slot_lock, timer reset
  • slot_lock (>= SLOT_LOCK_TIME) → shine, timer reset
  • shine (>= SHINE_TIME) → showing, timer reset, showingTimer = 0
  • showing is exited by card selection (or shouldAutoPick) — pickReward or equivalent sets state to fadeout, fly_center, upgrade_show, or merge_show depending on reward type
  • fadeout (>= FADE_OUT_TIME) → none, clears selection / vanish / banish targeting
  • fly_center (>= FLY_CENTER_TIME) → burst, spawns particles, plays burst chime
  • burst (>= BURST_TIME) → none

Non-linear entry points

  • startRewardReveal(family, level?) — full pipeline from announce. Resets SLOT_INTRO_TIME to default, clears vanish state, fires cinematic.onStart.
  • startUpgradeShowOnly(family) — jumps straight to upgrade_show, skipping announce and the card phases. Used by the Event Reward artifact for auto-applied upgrades. Caller must have already populated _upgradeItems via prepareUpgradeShow.
  • Mod merge — enters merge_show directly 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 routing
  • engine/rendering/hud.ts — state machine, dwell constants, card position math, particle update
  • engine/rendering/reward-cinematics/registry.tsRewardState mirror, CinematicContext, CinematicModule contract