PURPOSE

Post-mission Mod Grid drop service. Decides how many mod drops a finished run is worth (relative to the player’s per-planet personal best tier), rolls each drop’s template and rarity, and pushes the results into the player’s Mod Grid inventory flagged “unseen” so the metagame UI can render a NEW badge on the Upgrades tab.

OWNS

  • UNCOMMON_CHANCE constant — the per-drop probability that a rolled mod is uncommon rather than common (0.30; remainder is common).
  • DropRoll interface — { templateId: string; rarity: 'common' | 'uncommon' }. The intermediate roll record before a drop is materialized into the inventory.
  • computeDropCount(tierReached, tierPB) — pure function from this-run tier and the stored per-planet PB tier to the drop count for the run.
    • First attempt (tierPB <= 0): 2 drops.
    • Met or exceeded PB (tierReached >= tierPB): 3 drops.
    • One short of PB (tierReached === tierPB - 1): 2 drops.
    • Otherwise: 1 drop.
  • rollMissionDrops(count) — produces count DropRoll records by uniformly sampling MOD_TEMPLATES and rolling rarity against UNCOMMON_CHANCE. Uses Math.random().
  • applyMissionDrops(drops) — commits a pre-rolled drop list into the Mod Grid store: calls addDrop(templateId, rarity) per entry, collects the returned uids in input order, marks all uids unseen, and persists the store. Returns the uid array.

READS FROM

  • MOD_TEMPLATES (../data/mods) — the master mod template list; rollMissionDrops samples its indices uniformly.
  • useModGridStore (../stores/modGridStore) — read via .getState() inside applyMissionDrops to obtain addDrop and persist.

PUSHES TO

  • useModGridStore — appends one inventory entry per drop via addDrop, then calls persist() to flush the store.
  • markModsUnseen (../data/unseen-mods) — receives the newly-created uid array so the Upgrades tab can render NEW badges until the player views each mod.

DOES NOT

  • Decide when a run is over or what tierReached/tierPB are — callers supply both. The service does not read run state, planet state, or PB storage.
  • Award currencies, XP, ship unlocks, achievements, or any reward other than Mod Grid drops.
  • Render UI, schedule animations, or play audio. It is silent and synchronous.
  • Validate inputs. A negative count, an empty MOD_TEMPLATES, or a malformed DropRoll will surface as a runtime error from downstream code rather than be defended against here.
  • Roll rarities other than common or uncommon. Rare, epic, legendary, etc. are not produced by this service.
  • Seed or expose its RNG. Math.random() is used directly; rolls are not deterministic or replayable.
  • Deduplicate template picks. The same templateId may be rolled multiple times in one call.

Signals

The module exports no events, observables, or subscriptions. Its only outbound signals are:

  • The return values of computeDropCount (number), rollMissionDrops (DropRoll[]), and applyMissionDrops (uid string[]).
  • The side effect of addDrop + persist on useModGridStore, which downstream subscribers (the Mod Grid UI, the Upgrades tab badge) observe through their own store bindings.
  • The unseen flag set on the returned uids via markModsUnseen, which the Upgrades tab reads through unseen-mods.

Entry points

  • computeDropCount(tierReached: number, tierPB: number): number — called by the post-mission flow once a run resolves to determine how many drops to roll.
  • rollMissionDrops(count: number): DropRoll[] — called with the result of computeDropCount (or any caller-chosen count) to produce the roll list.
  • applyMissionDrops(drops: DropRoll[]): string[] — called with a roll list (typically the output of rollMissionDrops) to commit drops into inventory and flag them unseen. Returns the new uids in the same order as the input drops.
  • DropRoll and UNCOMMON_CHANCE are exported for type sharing and for callers that need to know the rarity probability without re-rolling.

Pattern notes

  • Three-stage pipeline by design: count → roll → apply. Callers can interpose between stages (for example, log rolls, mutate them in tests, or split into multiple applyMissionDrops calls) because each stage is a plain function over plain data.
  • Pure / impure split is explicit: computeDropCount and rollMissionDrops are pure on their inputs (modulo Math.random() in the roller); applyMissionDrops is the only function that touches the store.
  • The “first attempt gives the middle outcome” rule (tierPB <= 0 returns 2) keeps the drop count from collapsing to 1 on a player’s very first run on a planet, where no PB exists yet.
  • The PB comparison is integer-tier-based: the function reads tierReached and tierPB as plain numbers and does not consult any sub-tier progress, in-run difficulty modifiers, or partial completion.
  • Rarity weighting is implemented as a single threshold (Math.random() < UNCOMMON_CHANCE) rather than a weight table; adding a new rarity tier would require restructuring this branch rather than extending a data table.
  • The store is accessed via useModGridStore.getState() rather than as a hook, so applyMissionDrops is callable from non-React contexts (engine code, headless tests).
  • markModsUnseen is called once with the full uid batch rather than per-drop, and persist() is called once after all drops have been added, so a single mission’s drops are flushed atomically.
  • Uid ordering: applyMissionDrops preserves the order of its drops input in the returned uid array, allowing callers to correlate roll metadata to inventory entries by index.