PURPOSE

Implements the levelUpSlotModule cinematic — a three-reel slot-machine reveal for level-up reward cards. Three blurred reels spin, each ticks common → legendary with ascending chimes, then locks left-to-right at its true rarity with a chunk SFX, ring pulse, white flash, and star burst. After lock, draw falls through to hud.ts default card render so the reveal matches the post-cinematic painted card (no double-render).

OWNS

  • Module-level per-reveal state: _capturedChoices, _targetRarity, _rampDuration, _lockAt, _slotTime, _lastTickIdx, _lockedFired, _burstSpawned, _leverFired, _prevState, _cardRect.
  • _stars particle pool (capped at STAR_POOL_CAP = 60).
  • Tuning constants: RARITY_LADDER, PRE_SPIN_TIME, INTER_LOCK_PAUSE, RAMP_BY_RARITY, DURATION_BUFFER, RARITY_BURST_COUNT.
  • Reel rendering helpers paintSpinningReel, roundRect, _starPath.
  • Schedule planner planLockSchedule, ticker function reelState, rarity coercion coerceRarity.
  • Star burst spawner spawnStarBurst.
  • The exported levelUpSlotModule: CinematicModule.

READS FROM

  • ./registry — types CinematicModule, CinematicContext, RewardState.
  • ../card-themeRARITY_GRADIENT, RARITY_ACCENT, paintCardGradient, Rarity type.
  • ./audioplayCinematicTone (for slot-tick and slot-chunk cues).
  • ../hudsetSlotIntroDuration to grow the shared slot_intro state to fit the planned schedule.
  • ../../audio/sample-sfxSampleSfx.playLevelUp, SampleSfx.playReward.
  • ../../core/config/_accessibilityisSkipRewardAnimations short-circuit.
  • From ctx: ctx.state, ctx.stateTimer, ctx.dt, ctx.choices, ctx.uiScale.

PUSHES TO

  • setSlotIntroDuration(plan.total + DURATION_BUFFER) — resizes hud’s slot_intro state once choices are captured (or to 0.05 when accessibility skip is on).
  • playCinematicTone('slot-tick', pitch) and playCinematicTone('slot-chunk', pitch, dur) for ticker and lock SFX.
  • SampleSfx.playLevelUp() on the lever-down (announce → slot_intro transition).
  • SampleSfx.playReward() at slot_lock → shine transition.
  • Direct CanvasRenderingContext2D draws via drawCardOverride (reel painting) and drawOverlay (lock flash, ring pulse, star particles).

DOES NOT

  • Does not own card layout math — hud.ts owns positioning; this module only stashes the rect passed into drawCardOverride via _cardRect.
  • Does not paint the final revealed card — returns false from drawCardOverride once a reel is locked or the cinematic enters showing / fadeout / fly_center / burst, so hud’s default card paint takes over.
  • Does not advance the cinematic state machine — only reads ctx.state and responds to onStateTransition.
  • Does not overshoot rarity — the ladder walks common → target and stops; never climbs higher than the actual outcome.
  • Does not tick _slotTime during announce — the shared 2-second darken intro is excluded from the slot anim clock.
  • Does not run any animation work when isSkipRewardAnimations() is true (other than setting a 0.05s slot_intro duration in onStart).

Signals

  • Audio:
    • slot-chunk at 110 Hz, 0.14s — lever-down on announce → slot_intro.
    • slot-tick ascending pitch (base 380 Hz + 60 Hz per ladder step + 80 Hz per card slot) on each ticker bump while spinning.
    • slot-chunk lock pitch (140 Hz + 12 Hz per rarity index, dur 0.12s + 0.02s per rarity index) when each reel locks.
    • SampleSfx.playLevelUp() on the lever-down moment.
    • SampleSfx.playReward() on slot_lock → shine.
  • Visual:
    • Reel: rarity-tinted gradient, four scrolling motion-blur bands, top/bottom darkening, ghosted bobbing ”?” glyph, rarity-accent border.
    • Lock: white flash overlay (0.12s, alpha 0.45), rarity-accent ring pulse (0.35s, expands from 0.35× to 0.95× card max dim, thins from 4px).
    • Star burst: 10–20 five-pointed stars per card (count by rarity), spawned at card edge, additive lighter composite, ~0.65–0.95s lifespan, fast-in/slow-out fade with bright white core.
  • State capture: _cardRect[i] stashed each frame from drawCardOverride so drawOverlay and the deferred star-burst spawn can hit the correct screen coordinates without duplicating hud’s layout.

Entry points

  • levelUpSlotModule.id = 'level-up-slot' — registry key.
  • onStart(ctx) — resets module state; sets slot_intro to 0.05s when skip-reward-animations is on.
  • onUpdate(ctx) — captures choices on first frame they appear, plans schedule via planLockSchedule, resizes slot_intro, accumulates _slotTime outside announce, fires tick/lock SFX, spawns deferred star bursts, advances/decays star particles.
  • onStateTransition(from, to, ctx) — fires lever-down chunk + level-up sample on announce → slot_intro; fires reward sample on slot_lock → shine.
  • drawCardOverride(ctx2d, cardIndex, x, y, w, h, reward, alpha, scale, isSelected, ctx) — stashes card rect, returns false after lock or in non-slot states (delegates to hud’s default paint), otherwise paints the spinning reel and returns true.
  • drawOverlay(ctx2d, ctx) — paints ring pulse + white flash per locked card, then star particles with additive blending.
  • onEnd(ctx) — calls resetState().

Pattern notes

  • Timing contract. Documented at the top of the file: announce 0.40s, slot_intro 0.50s (resized via setSlotIntroDuration), slot_lock 0.15s, shine 0.30s, showing ∞. Lock times are absolute seconds since slot_intro began and the slot clock excludes announce.
  • No compression. planLockSchedule accumulates PRE_SPIN_TIME + sum(ramps) + INTER_LOCK_PAUSE between locks. The required duration is pushed into hud via setSlotIntroDuration rather than crushing rarities into a fixed window — common reveals stay snappy, all-legendary hands get full dwell.
  • Never overshoot target. reelState walks common up to target and stops; the ladder index is Math.min(targetIdx, Math.floor(p * steps)) with steps = targetIdx + 1.
  • Choices captured lazily. onStart fires before ctx.choices is populated, so onUpdate snapshots _capturedChoices on its first frame where ctx.choices.length > 0 and only then plans the schedule.
  • Single-source draw. Once a reel locks (or state is showing/later), drawCardOverride returns false so the player’s real card (gradient + badge + icon + name) is painted by hud.ts rather than re-drawn here — eliminates pop-in mismatch on the lock frame.
  • Star burst is deferred. Spawn happens in onUpdate only after both rs.locked is true and _cardRect[i] has been stashed by a prior drawCardOverride call.
  • Particle pool discipline. Swap-remove iteration on _stars, drag Math.pow(0.92, dt * 60) for framerate-stable decay, cap at STAR_POOL_CAP.
  • Accessibility skip. Every lifecycle method early-returns when isSkipRewardAnimations() is true, so the cinematic effectively collapses to a near-instant slot_intro and hud takes over immediately.
  • roundRect polyfill. Local rounded-rect path matches hud’s silhouette so reel and final card line up exactly.