PURPOSE

Interactive post-run stats explorer. Reachable only by direct URL (/games/starship-survivors/stats) — no longer part of the live post-run flow as of v5.122 (the flow is now Game Over → /reveal → /ship-pull → Hub). Reads the in-memory MissionResult from sessionStore and renders a mobile-first portrait layout with a top outcome banner, a scrollable tabbed content area, and a bottom tab pill bar plus an EXIT button. Tabs are Overview, Weapons, Enemies, and History.

OWNS

  • The exported component RunStatsScreen (fixed-position fullscreen overlay at zIndex 300).
  • Local state: activeTab ('overview' | 'weapons' | 'enemies' | 'history') and a savedRef guard so the run is saved to history exactly once per mount.
  • Internal subcomponents: OutcomeBanner, OverviewTab, NewArtifactUnlocksPanel, WeaponsTab, EnemiesTab, HistoryTab, BottomControls, StatCard, Bar, ComparisonBadge, ComparisonRow, PBBadge, MiniStat.
  • Local helpers: formatTime (M:SS or H:MM:SS) and formatNumber (k / M suffixes).
  • Shared inline style constants PANEL, LABEL, VALUE.
  • Per-tab derived data: sorted/merged weapon stats (with name, color, level, accuracy, dps, damage share); enemy entries grouped by archetype or rarity; per-stat comparison rows against the running average; recent-score bar trend for the last five runs; personal-best diff computation.

READS FROM

  • useSessionStore selector s => s.missionResult — the canonical MissionResult for this run.
  • useArtifactUnlocksStore.getState() — invoked to push newly-legendary artifacts and per-artifact best-tier history into the persistent unlock store.
  • ARTIFACT_MAP from ../data/artifacts — to resolve unlocked artifact ids to display names.
  • WEAPON_MAP from ../data/weapons — to resolve weapon ids to display name and color.
  • ENEMY_TYPE_MAP, RARITY_TINTS, type EnemyRarity from ../data/enemies — to resolve enemy ids to name, archetype, rarity, tint.
  • getRunHistory, getRunAverages, getPersonalBests, type RunHistorySummary from ../data/run-history — full local-storage run history, averages computed on prior runs (history.slice(1)), and rolled-up personal bests.
  • MissionResult type from ../data/mission-result.
  • useNavigate from react-router-dom.

PUSHES TO

  • saveRunToHistory(result) from ../data/run-history — fired once on mount, gated by savedRef.
  • useArtifactUnlocksStore.getState().unlockMany(ids) — flushes result.progression.newlyUnlockedArtifactIds so newly-legendary artifacts are selectable as starting artifacts in the hub.
  • useArtifactUnlocksStore.getState().recordBestTierMany(bestTier) — flushes result.progression.artifactBestTierThisRun so the picker can show progress on not-yet-legendary artifacts.
  • navigate('/', { replace: true }) — on mount when result is missing (page reload clears in-memory state).
  • navigate('/') — when the user taps EXIT TO HUB in BottomControls.

DOES NOT

  • Does not run any game-loop logic, spawn entities, or touch the engine/canvas; it is pure metagame React UI.
  • Does not write directly to localStorage; persistence is delegated to run-history and artifactUnlocksStore.
  • Does not mutate MissionResult, the session store, or any other store beyond the artifact-unlock flushes described above.
  • Does not award currency, XP, or other rewards — those occur upstream in the reveal flow.
  • Does not subscribe to any per-frame engine signal; rendering is React-driven from store state.
  • Does not handle hardware-back, swipe gestures, or keyboard navigation; tab switching and exit are tap-only.
  • Does not request, fetch, or post anything to the server; all data is read from local stores.
  • Is no longer part of the live post-run flow — entering it requires the direct URL.

Signals

  • useEffect on result → redirects to / with replace: true when result is null.
  • useEffect on result (with savedRef guard) → calls saveRunToHistory, then conditionally calls unlockMany and recordBestTierMany on the artifact-unlocks store.
  • useMemo for history (empty deps), averages (depends on history), personalBests (depends on history), plus per-tab memos for sorted weapon entries, grouped enemy entries, and recent-score series.
  • Tab button onClicksetActiveTab(tab.id).
  • Group-toggle button onClick in EnemiesTabsetGroupBy('archetype' | 'type') (the label “Rarity” maps to the 'type' mode internally).
  • Exit button onClicknavigate('/').

Entry points

  • URL route /games/starship-survivors/stats — the only way to land on the screen post-v5.122.
  • Direct mount via the metagame router; no other component links here in the current build. The doc comment notes a future Hub-side “Last Run” link could make it reachable again.

Pattern notes

  • All styling is inline CSS via shared PANEL / LABEL / VALUE objects plus clamp() font sizes for mobile responsiveness; no external CSS modules or theme tokens.
  • env(safe-area-inset-top) and env(safe-area-inset-bottom) are added to top banner and bottom controls padding for iOS notch/home-indicator safety.
  • The component is rendered as a fixed inset: 0 overlay at zIndex: 300 with its own background gradient, so it covers whatever is mounted underneath.
  • savedRef = useRef(false) is the idempotency gate that ensures saveRunToHistory and the artifact-unlock flushes fire exactly once even if React re-runs the effect.
  • averages deliberately excludes the current run by passing history.slice(1) to getRunAverages — the just-saved run sits at index 0 in history.
  • ComparisonBadge treats deltas under 3% as neutral (“avg”), positive deltas as green up-arrow, negative as red down-arrow; null/zero averages render nothing.
  • Weapons are merged from result.combat.weaponStats with result.progression.weaponsAtEnd to recover the end-of-run level; accuracy is hits / shots * 100, DPS is dmg / runTime with runTime floored to at least 1 second; the top weapon is tagged with a crown.
  • Enemies tab groups by archetype by default; the “Rarity” toggle re-bins the same entries by rarity; per-row proportion bars are normalized against totalKills.
  • History tab shows a personal-bests banner only if at least one record was tied or beaten this run; comparison rows colour current values green when higher than average and red when lower (no metric-direction awareness — e.g., damage-taken-higher still renders green).
  • Recent-score sparkline uses bar heights scaled against Math.max(...recent.map(r => r.score), 1); the latest run is gold, older runs are grey.
  • Fonts mix 'Cal Sans', cursive for headings/labels and 'Space Grotesk', sans-serif for weapon/artifact body text.