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_CHANCEconstant — the per-drop probability that a rolled mod is uncommon rather than common (0.30; remainder is common).DropRollinterface —{ 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.
- First attempt (
rollMissionDrops(count)— producescountDropRollrecords by uniformly samplingMOD_TEMPLATESand rolling rarity againstUNCOMMON_CHANCE. UsesMath.random().applyMissionDrops(drops)— commits a pre-rolled drop list into the Mod Grid store: callsaddDrop(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;rollMissionDropssamples its indices uniformly.useModGridStore(../stores/modGridStore) — read via.getState()insideapplyMissionDropsto obtainaddDropandpersist.
PUSHES TO
useModGridStore— appends one inventory entry per drop viaaddDrop, then callspersist()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/tierPBare — 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 emptyMOD_TEMPLATES, or a malformedDropRollwill surface as a runtime error from downstream code rather than be defended against here. - Roll rarities other than
commonoruncommon. 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
templateIdmay 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[]), andapplyMissionDrops(uidstring[]). - The side effect of
addDrop+persistonuseModGridStore, 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 throughunseen-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 ofcomputeDropCount(or any caller-chosen count) to produce the roll list.applyMissionDrops(drops: DropRoll[]): string[]— called with a roll list (typically the output ofrollMissionDrops) to commit drops into inventory and flag them unseen. Returns the new uids in the same order as the input drops.DropRollandUNCOMMON_CHANCEare 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
applyMissionDropscalls) because each stage is a plain function over plain data. - Pure / impure split is explicit:
computeDropCountandrollMissionDropsare pure on their inputs (moduloMath.random()in the roller);applyMissionDropsis the only function that touches the store. - The “first attempt gives the middle outcome” rule (
tierPB <= 0returns 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
tierReachedandtierPBas 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, soapplyMissionDropsis callable from non-React contexts (engine code, headless tests). markModsUnseenis called once with the full uid batch rather than per-drop, andpersist()is called once after all drops have been added, so a single mission’s drops are flushed atomically.- Uid ordering:
applyMissionDropspreserves the order of itsdropsinput in the returned uid array, allowing callers to correlate roll metadata to inventory entries by index.