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 refs transitionRef, transitionStartTimeRef.
  • Drag-to-navigate state — dragX, isSnapping, refs touchOriginRef, dragLockedRef. Shared between touch and mouse drag paths.
  • Challenge Mode state — isChallengeMode (toggle), challengeUnlocked (per-planet localStorage flag challenge_unlocked_planet_<id>), showChallengeUnlockBanner (first-time banner gated by challenge_popup_shown_<id>).
  • Overlay open flags — showChallenges, showTrackViewer, toastMsg.
  • The goToPlanet, onSlideEnd, onDragStart, onDragMove, onMouseDragStart, onMouseDragMove, onDragEnd, dismissChallengeBanner, and getArchetype callbacks.
  • The nebula blackout animation driven by requestAnimationFrame (300ms fade-through-black, peaks at midpoint).
  • A literal currentChapter object built inline (eventsUnlocked: true, starsUnlocked: true, eventTier: 3, currentChapter: 1) — unused by the visible JSX but kept in the closure.

READS FROM

  • useSessionStoreresetSession, selectedShipId, setShip, setRunDef.
  • useInventoryStoreisUnlocked(selectedShipId) to decide whether to resolve a ShipDef for rarity tinting.
  • useOnboardingStoreprologueComplete.
  • data/nebula-archetypesARCHETYPES, PLANET_ARCHETYPES for the nebula background.
  • data/shipsRARITY_COLORS, ShipRarity, getShipDef (resolved at star level 1; rarity is intrinsic to the hull).
  • data/planet-progressionTRACK_RARITY_BG for light/dark rarity tints on the ship square border.
  • data/planet-configPLANETS, PLANET_ORDER (fixed order, no circular wrap).
  • localStorage — reads challenge_unlocked_planet_<id> and challenge_popup_shown_<id>; wrapped in try/catch so private mode disables the feature gracefully.
  • services/assembleRunServiceassembleRunDef called on LAUNCH.
  • react-router-domuseNavigate for 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; setShip is 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] and PLANET_ORDER[length-1] are hard endpoints; arrow buttons hide via hub-planet-arrow-hidden at boundaries and goToPlanet clamps 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).
  • slideDir is 'left' | 'right' | null'left' means the user navigated left (content exits right, new content enters from the left side).
  • dragX > window.innerWidth * 0.2 triggers 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 === 0 on down, e.buttons === 1 on 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; V32Shell provides the chrome bands.
  • Two hub-planet-container divs co-exist during a transition — one for the exiting planet (with hub-slide-exit-* class) and one for the entering planet (with hub-slide-enter-* class). onAnimationEnd clears slideDir, transitioning, and exitingPlanet.
  • Drag transform is applied directly via inline style on the entering container, and transition is enabled only when isSnapping is 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 blackout from 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-primary classes (Tick 77 medical-UI rollout); the first-time unlock banner uses MedPanel variant="doors" inside a MedPanelModal.
  • 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 a hub-toast div; nothing in this file currently sets it, so it stays inert until wired up.