HubScreen
PURPOSE
The Home / Launch screen of the metagame. Renders the V32-styled hub where the player picks a planet, optionally toggles Challenge Mode, and taps LAUNCH to start a run. Hosts the live WebGL nebula background, the sun effect, the planet swiper, the per-planet progress bar (or leaderboard tier track), the LAUNCH CTA, and overlay surfaces for the challenge popover, planet track viewer, and Challenge Mode first-time unlock banner.
OWNS
- Planet swiper state —
planetIndex,transitioning,slideDir,exitingPlanet,blackout, plus refstransitionRef,transitionStartTimeRef. - Drag-to-navigate state —
dragX,isSnapping, refstouchOriginRef,dragLockedRef. Shared between touch and mouse drag paths. - Challenge Mode state —
isChallengeMode(toggle),challengeUnlocked(per-planet localStorage flagchallenge_unlocked_planet_<id>),showChallengeUnlockBanner(first-time banner gated bychallenge_popup_shown_<id>). - Overlay open flags —
showChallenges,showTrackViewer,toastMsg. - The
goToPlanet,onSlideEnd,onDragStart,onDragMove,onMouseDragStart,onMouseDragMove,onDragEnd,dismissChallengeBanner, andgetArchetypecallbacks. - The nebula blackout animation driven by
requestAnimationFrame(300ms fade-through-black, peaks at midpoint). - A literal
currentChapterobject built inline (eventsUnlocked: true,starsUnlocked: true,eventTier: 3,currentChapter: 1) — unused by the visible JSX but kept in the closure.
READS FROM
useSessionStore—resetSession,selectedShipId,setShip,setRunDef.useInventoryStore—isUnlocked(selectedShipId)to decide whether to resolve aShipDeffor rarity tinting.useOnboardingStore—prologueComplete.data/nebula-archetypes—ARCHETYPES,PLANET_ARCHETYPESfor the nebula background.data/ships—RARITY_COLORS,ShipRarity,getShipDef(resolved at star level 1; rarity is intrinsic to the hull).data/planet-progression—TRACK_RARITY_BGfor light/dark rarity tints on the ship square border.data/planet-config—PLANETS,PLANET_ORDER(fixed order, no circular wrap).localStorage— readschallenge_unlocked_planet_<id>andchallenge_popup_shown_<id>; wrapped in try/catch so private mode disables the feature gracefully.services/assembleRunService—assembleRunDefcalled on LAUNCH.react-router-dom—useNavigatefor the post-LAUNCH route change.
PUSHES TO
useSessionStore.setRunDef(runDef)on LAUNCH with the assembled run definition (ship id, planet id, challenge flag).useSessionStore.resetSession()on mount.useNavigate()('/games/starship-survivors/play')on LAUNCH.localStorage.setItem('challenge_popup_shown_<id>', '1')when the Challenge Mode unlock banner is dismissed; wrapped in try/catch.
DOES NOT
- Does not write to the inventory store. Ship selection writes happen elsewhere;
setShipis read from the session store but not invoked in this file. - Does not mutate Challenge Mode unlock state — only reads it from localStorage. Unlock is written by the engine on first clear of a planet’s normal-mode final boss.
- Does not loop or wrap the planet list.
PLANET_ORDER[0]andPLANET_ORDER[length-1]are hard endpoints; arrow buttons hide viahub-planet-arrow-hiddenat boundaries andgoToPlanetclamps the index. - Does not show Challenge Mode UI on leaderboard planets (
PLANETS[id].isLeaderboard) — Voidstar and Speedway are their own ranked tracks. - Does not render building cards (a strip referenced in the file header comment but absent from the current JSX).
- Does not validate ship selection before LAUNCH — assembly trusts
selectedShipId.
Signals
transitioning && Date.now() - transitionStartTimeRef.current < 150— input lockout window for swipe/click. Past the 150ms midpoint a new transition can pre-empt the current one (rapid flick-through).slideDiris'left' | 'right' | null—'left'means the user navigated left (content exits right, new content enters from the left side).dragX > window.innerWidth * 0.2triggers navigation; below threshold springs back with a 300ms cubic-bezier transition.- 8px dead zone before drag-lock; if the gesture is predominantly vertical (
|dy| >= |dx|), the touch is released so native scroll takes over. - Arrow buttons stop touch/mouse propagation so a tap on them never trips the drag handler.
- Mouse drag requires left button (
e.button === 0on down,e.buttons === 1on move) and shares all refs with the touch path.
Entry points
- Rendered by the metagame router under the home/hub route.
- Exported as the named function component
HubScreen— no default export. - Wrapped in
<V32Shell hideChrome={showTrackViewer}>so the shell’s top bar and bottom tabs hide when the planet track viewer overlay is open.
Pattern notes
- Layout is bottom-aligned: nebula + sun + planet viewer occupy the upper region; the LAUNCH button and Challenge Mode toggle live in the bottom shadow layer;
V32Shellprovides the chrome bands. - Two
hub-planet-containerdivs co-exist during a transition — one for the exiting planet (withhub-slide-exit-*class) and one for the entering planet (withhub-slide-enter-*class).onAnimationEndclearsslideDir,transitioning, andexitingPlanet. - Drag transform is applied directly via inline
styleon the entering container, andtransitionis enabled only whenisSnappingis true. During an active drag, transition is'none'so the planet tracks the finger. - Nebula blackout is decoupled from the CSS slide: an rAF loop drives
blackoutfrom 0 → 1 → 0 over 300ms (triangular ramp) and is passed to<NebulaBackground blackoutProgress={...}>. - Rarity tinting: ship rarity comes from the ★1
ShipDef(rarity is intrinsic to the hull, star level does not change color). Falls back to'#e2e8f0'/'#fff'/'#2a2a2a'when no ship is unlocked. - Challenge Mode toggle uses
med-btn/med-btn-primaryclasses (Tick 77 medical-UI rollout); the first-time unlock banner usesMedPanelvariant="doors"inside aMedPanelModal. - Reward-pipeline animations are driven by
<HomeResolveController />rendered as a sibling to the visible content — it has no UI of its own here. - Leaderboard planets render a 10-slot
hub-tier-track(placeholder reward emoji) above the planet sprite and a<LeaderboardWrapper>overlay below it; normal planets render<PlanetProgressBar>and no overlay. - Toast (
toastMsg) is rendered as ahub-toastdiv; nothing in this file currently sets it, so it stays inert until wired up.