ShipPullScreen

PURPOSE

Bonus 1× ship pull awarded after every run. Sits between RevealScreen (credits) and Hub. Free pull — no pity, no wallet cost. Rolls a rarity tag from SHIP_RARITY_RATES, then picks a uniformly random hull from HULL_CLASSES. Grants locally via the inventory store and persists server-side through the grant_free_ship_pull RPC. Renders the reused CollectibleCard so the new XP/star state is visible immediately.

OWNS

  • STAGE constant — timing offsets (titleIn 0, cardIn 200, badgeIn 800, continueAt 1400 ms).
  • PullOutcome interface — hullClass, rarity, unlocked, xpGained.
  • rollRarity() — weighted random over SHIP_RARITY_RATES with common fallback for float drift.
  • pickHull() — uniform random hull from HULL_CLASSES.
  • ShipPullScreen component — full-screen overlay, staged reveal animation, CONTINUE button.
  • UnlockBadge subcomponent — renders either NEW SHIP UNLOCKED (when unlocked) or +{xpGained} STAR XP (when duplicate).
  • Local React state: outcome, showTitle, showCard, showBadge, showContinue.
  • didRollRef guard preventing duplicate rolls across re-renders.

READS FROM

  • useSessionStoremissionResult (gates entry; redirects to / if absent).
  • useInventoryStore.getState().grantShipPull(hullClass) — applies the grant locally and returns { unlocked, xpGained }.
  • @starship-survivors/data/shipsHULL_CLASSES, ShipRarity type.
  • @starship-survivors/data/pull-configSHIP_RARITY_RATES.
  • @starship-survivors/engine/rendering/card-themeCARD_SHADOW_CSS, RARITY_ACCENT.
  • @metagame/components/CollectibleCard — the rendered card.
  • react-router-domuseNavigate.

PUSHES TO

  • useInventoryStore.grantShipPull(hullClass) — mutates local inventory state with the new hull / XP.
  • invokeRpc('grant_free_ship_pull', { p_ship_id, p_rarity }) from @metagame/services/supabase — fire-and-forget Supabase RPC that inserts a player_entities row.
  • navigate('/') — exits to Hub when CONTINUE is pressed after the reveal completes.
  • navigate('/', { replace: true }) — bounce to Hub if mounted with no missionResult.

DOES NOT

  • Does not charge currency, consult pity counters, or read wallet state.
  • Does not award duplicates separately — the inventory store’s grantShipPull decides unlocked-vs-XP.
  • Does not block on the RPC; failure is logged via console.warn and the pull stays local-only (matches anonymous-play behavior elsewhere).
  • Does not run while missionResult is null — early returns prior to render.
  • Does not animate beyond the staged opacity/transform reveal; no canvas, no particles.
  • Does not write to sessionStore or clear missionResult.
  • Does not retry the RPC.

Signals

  • Mount with missionResult present triggers the single roll (guarded by didRollRef).
  • Four setTimeout calls drive staged reveal flags; cleared on unmount.
  • Background click handler skipToEnd immediately flips all four reveal flags to true.
  • CONTINUE button click: if reveal incomplete, calls skipToEnd; otherwise navigates to /. e.stopPropagation() prevents double-fire with the backdrop.

Entry points

  • Navigated to after RevealScreen in the post-run flow.
  • Renders nothing (null) until both missionResult and outcome are populated.
  • Exits via navigate('/') to the Hub once the user taps CONTINUE post-reveal.

Pattern notes

  • Component uses inline styles only; no CSS modules.
  • z-index: 300 full-viewport overlay with radial-gradient backdrop.
  • CollectibleCard is reused as-is from the Ships screen — full sprite, rarity frame, star row, XP bar. Glow tint comes from RARITY_ACCENT[outcome.rarity] applied as a drop-shadow filter on the card wrapper.
  • Animation timings are declared centrally in STAGE rather than scattered.
  • Rarity roll uses the same float-safety fallback (return 'common') as the paid weightedRoll to handle epsilon drift.
  • Local-first then server-persist pattern: inventory store updates synchronously so the card renders immediately; RPC reconciles on its own clock.
  • Anonymous users (no auth.uid()) cause the RPC to throw, caught and warned only — the pull persists for the session but does not survive reload, matching the rest of the app.
  • Single-roll guard pattern via useRef rather than dependency tricks.