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
STAGEconstant — timing offsets (titleIn 0, cardIn 200, badgeIn 800, continueAt 1400 ms).PullOutcomeinterface —hullClass,rarity,unlocked,xpGained.rollRarity()— weighted random overSHIP_RARITY_RATESwithcommonfallback for float drift.pickHull()— uniform random hull fromHULL_CLASSES.ShipPullScreencomponent — full-screen overlay, staged reveal animation, CONTINUE button.UnlockBadgesubcomponent — renders eitherNEW SHIP UNLOCKED(whenunlocked) or+{xpGained} STAR XP(when duplicate).- Local React state:
outcome,showTitle,showCard,showBadge,showContinue. didRollRefguard preventing duplicate rolls across re-renders.
READS FROM
useSessionStore—missionResult(gates entry; redirects to/if absent).useInventoryStore.getState().grantShipPull(hullClass)— applies the grant locally and returns{ unlocked, xpGained }.@starship-survivors/data/ships—HULL_CLASSES,ShipRaritytype.@starship-survivors/data/pull-config—SHIP_RARITY_RATES.@starship-survivors/engine/rendering/card-theme—CARD_SHADOW_CSS,RARITY_ACCENT.@metagame/components/CollectibleCard— the rendered card.react-router-dom—useNavigate.
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 aplayer_entitiesrow.navigate('/')— exits to Hub when CONTINUE is pressed after the reveal completes.navigate('/', { replace: true })— bounce to Hub if mounted with nomissionResult.
DOES NOT
- Does not charge currency, consult pity counters, or read wallet state.
- Does not award duplicates separately — the inventory store’s
grantShipPulldecides unlocked-vs-XP. - Does not block on the RPC; failure is logged via
console.warnand the pull stays local-only (matches anonymous-play behavior elsewhere). - Does not run while
missionResultis null — early returns prior to render. - Does not animate beyond the staged opacity/transform reveal; no canvas, no particles.
- Does not write to
sessionStoreor clearmissionResult. - Does not retry the RPC.
Signals
- Mount with
missionResultpresent triggers the single roll (guarded bydidRollRef). - Four
setTimeoutcalls drive staged reveal flags; cleared on unmount. - Background click handler
skipToEndimmediately 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
RevealScreenin the post-run flow. - Renders nothing (
null) until bothmissionResultandoutcomeare 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: 300full-viewport overlay with radial-gradient backdrop.CollectibleCardis reused as-is from the Ships screen — full sprite, rarity frame, star row, XP bar. Glow tint comes fromRARITY_ACCENT[outcome.rarity]applied as adrop-shadowfilter on the card wrapper.- Animation timings are declared centrally in
STAGErather than scattered. - Rarity roll uses the same float-safety fallback (
return 'common') as the paidweightedRollto 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
useRefrather than dependency tricks.