PURPOSE

Gacha-style reveal screen that sits between ShipPullScreen and the Hub. Rolls grid-mod drops earned during the just-completed mission, persists them to the player’s inventory, and stages a timed reveal animation with a stat inspector popover. Auto-skips to the Hub when there is no missionResult in session or when the drop count resolves to zero.

OWNS

  • The mounted overlay (position: fixed, zIndex: 300) with radial gradient backdrop, the title element, the row of drop cards, and the bottom action buttons.
  • Drop roll lifecycle: the one-shot mount-time roll, application of those rolls to inventory, and the local drops state holding the resulting DropRoll[].
  • Reveal staging state: showTitle, revealedCount, showButtons, plus the array of setTimeout handles cleared on unmount.
  • The skip-to-end behavior that collapses all stages instantly when the background is tapped.
  • The drop inspector popover (zIndex: 350) and its inspect state.
  • Internal helper components DropCard, ShapeMini, DropPopover and the pillButton style factory.
  • Local STAT_LABEL map and formatStatValue helper (mirrors UpgradesTab; intentionally not extracted).
  • STAGE timing constants: titleIn 0ms, firstReveal 400ms, revealStagger 600ms, buttonsAfterLast 400ms.

READS FROM

  • useSessionStoremissionResult (used for both the redirect guard and the drop-count inputs).
  • getTierPB(nodeId) from @starship-survivors/data/run-history — prior personal-best tier for the node.
  • computeDropCount(tierReached, tierPB) from @starship-survivors/services/mission-drops.
  • MOD_TEMPLATES_BY_ID and RARITY_COLORS from @starship-survivors/data/mods.
  • resolveShape from @starship-survivors/data/mods/_types for ShapeMini cell grids.
  • CARD_SHADOW_CSS from @starship-survivors/engine/rendering/card-theme for the primary button shadow.
  • React Router’s useNavigate for redirects.

PUSHES TO

  • rollMissionDrops(dropCount) — produces the DropRoll[] rendered on screen.
  • applyMissionDrops(rolled) — persists the rolls into the player inventory (called once, guarded by didRollRef).
  • Navigation:
    • navigate('/', { replace: true }) when missionResult is absent or dropCount === 0.
    • navigate('/') from the CONTINUE button.
    • navigate('/games/starship-survivors/ships?tab=upgrades&highlight=new') from the VIEW IN MENU button.

DOES NOT

  • Roll or grant any rewards other than grid-mod drops (no XP, currency, ship pulls, or mission-progress writes — those happen upstream in ShipPullScreen / mission resolution).
  • Render anything when missionResult is null or when drops.length === 0 — returns null and lets the redirect effect handle navigation.
  • Re-roll drops on re-render: didRollRef ensures rollMissionDrops + applyMissionDrops run exactly once per mount.
  • Validate or de-duplicate templates; trusts whatever rollMissionDrops returns.
  • Persist any UI state across navigation — staging timers, inspect, and drops are all local and reset on mount.
  • Block backgrounding/tab-hide: timers continue running while hidden (no visibility-handling).

Signals

  • showTitle: boolean — fades the “LOOT RECOVERED” header in at STAGE.titleIn.
  • revealedCount: number — number of cards currently revealed; advanced via setRevealedCount(n => Math.max(n, i + 1)) so skip-to-end can monotonically jump ahead.
  • showButtons: boolean — toggles opacity and pointerEvents for the bottom button row; also drives the disabled attribute on both buttons.
  • inspect: DropRoll | null — non-null mounts the DropPopover.
  • drops: DropRoll[] — driven once by the roll effect; an empty array short-circuits render to null.
  • didRollRef: MutableRefObject<boolean> — guards single-execution of the roll.

Entry points

  • React route in the metagame shell that renders <LootBagScreen /> after a completed mission (the screen reads missionResult rather than route params; entered when navigation lands on the loot-bag URL with a populated session).
  • Mount flow: redirect-guard effect → one-shot roll effect (or redirect on zero drops) → staging-timer effect → user input (tap card / tap background / press button).

Outgoing entry points (where this screen sends the user):

  • Hub root / — on missing missionResult, on zero drops, and from the CONTINUE button.
  • Ships upgrades tab /games/starship-survivors/ships?tab=upgrades&highlight=new — from VIEW IN MENU.

Pattern notes

  • The outer container’s onClick={skipToEnd} is the catch-all; revealed DropCards and both buttons call e.stopPropagation() so taps on interactive elements don’t also collapse the staging.
  • Unrevealed cards intentionally do not stop propagation, so tapping a hidden card still triggers skipToEnd.
  • skipToEnd sets all staging flags to their terminal values rather than canceling timers; the timers stay in flight but are no-ops because state setters use Math.max or are idempotent.
  • Both navigation redirects use replace: true to keep the loot-bag screen out of the browser history.
  • Stat rendering convention: keys ending in Pct and the special-case damageReduction are stored as fractions and rendered as +N%; everything else is rendered as +N.
  • ShapeMini derives a cell size as Math.min(10, Math.floor(56 / max(w, h))) to keep the mod’s shape grid inside the card regardless of template bounding box.
  • pillButton uses all: 'unset' to neutralize default button styles before reapplying the visual treatment; primary variant pulls its drop shadow from the shared CARD_SHADOW_CSS token.
  • The popover uses a two-layer click structure: the outer scrim calls onClose, the inner card calls e.stopPropagation() so taps inside don’t dismiss.
  • STAT_LABEL is duplicated with UpgradesTab by design — comment explicitly defers extraction until a third caller exists.