reward-cards.ts
Definitions for every card surfaced in the post-run reward screen and on the level reward track, plus the rollers and tables that turn a mission outcome into a list of resolved rewards.
Purpose
One module owns three concerns:
- The catalog of reward-card definitions (visual identity + payload type).
- The post-run roller that produces a list of resolved cards from a mission result.
- The level reward track — five staged one-time chests claimed by accumulating run progress.
Premium currency (Warp Crystals) and Pull Tickets are explicitly excluded from the non-premium pool here.
Types
| Type | Members / shape |
|---|---|
RewardCardType | 'credits' | 'ship_pull' | 'star_xp' | 'level_xp' | 'mod_drop' |
RewardCardRarity | 'common' | 'uncommon' | 'rare' | 'epic' | 'legendary' |
RewardCardDef | { id, name, type, rarity, description, icon, color } |
ResolvedReward | { card: RewardCardDef, amount: number } |
LevelChestReward | { threshold: number /* 0-100 */, rewards: Array<{ cardId, amount }> } |
RewardCardDef.type drives both icon selection and destination routing (currency pouch vs. collection). RewardCardDef.rarity drives reveal-animation intensity (god rays, particles), not drop weight — drop logic lives in the roller, not the def.
Exports
| Name | Kind | Shape | Role |
|---|---|---|---|
RewardCardType | type | union of 5 string literals | card category |
RewardCardRarity | type | union of 5 string literals | reveal-animation intensity |
RewardCardDef | interface | see above | catalog row shape |
REWARD_CARDS | const array | RewardCardDef[] | catalog, source of truth |
REWARD_CARD_MAP | const record | Record<string, RewardCardDef> | id → def fast lookup (built from REWARD_CARDS) |
ResolvedReward | interface | { card, amount } | reward-screen payload |
STAR_XP_BASE | const number | 3 | base star-XP grant |
STAR_XP_DAILY_MULT | const number | 10 | first-win-of-day multiplier |
MIN_PULL_BY_TIER | const record | Record<string, RewardCardRarity> | minimum guaranteed pull rarity per tier |
LevelChestReward | interface | see above | level-track chest shape |
LEVEL_REWARD_TRACK | const array | LevelChestReward[] (length 5) | the staged chest ladder |
LEVEL_TRACK_POINTS_PER_RUN | const record | Record<string, number> | points added per run by tier |
rollPostRunRewards | function | (tier, survived, isFirstWinOfDay) => ResolvedReward[] | resolves a mission outcome into reward cards |
Catalog contract
REWARD_CARDS is a flat array of RewardCardDef. Every entry referenced by LEVEL_REWARD_TRACK.rewards[].cardId or by the rollPostRunRewards body must have a matching id in this array — the map is populated by iterating the array once at module load.
REWARD_CARD_MAP is built immediately after the array literal and is the only intended lookup surface for consumers. Adding a new card means appending one entry to REWARD_CARDS; no other registration step is required.
Performance tiers
Three lookup tables key off the same tier string ('bronze' | 'silver' | 'gold' | 'diamond') supplied by MissionResult:
| Table | Maps to |
|---|---|
MIN_PULL_BY_TIER | minimum RewardCardRarity for the guaranteed pull |
LEVEL_TRACK_POINTS_PER_RUN | points added to the level reward track per run |
CREDITS_RANGES (module-local) | [min, max] credits inclusive range |
rollPostRunRewards falls back to bronze when an unknown tier string arrives, and 'common' when MIN_PULL_BY_TIER lookup misses. Both fallbacks are silent.
rollPostRunRewards contract
rollPostRunRewards(tier: string, survived: boolean, isFirstWinOfDay: boolean): ResolvedReward[]Returns an ordered list, always in this order:
- Credits — always. Amount is
Math.round(min + Math.random() * (max - min))fromCREDITS_RANGES[tier]. Card is alwayscredits_small. - Ship pull — only when
survived === true. Card ispull_uncommonif the tier’s minimum rarity is'uncommon', otherwisepull_common. The roller never emits rare or epic pull cards itself — rare pulls come from the level track. - Star XP — always. If
isFirstWinOfDay && survived, emits thestar_xp_dailycard withSTAR_XP_BASE * STAR_XP_DAILY_MULT. Otherwise emitsstar_xp_basewithSTAR_XP_BASE.
Notes on the contract:
- The function uses
Math.random()directly — output is non-deterministic and not seeded. - The first-win bonus is gated on both
isFirstWinOfDayandsurvived. A first-win loss gets the base card. - The output list contains 2 entries on a loss, 3 on a win.
level_xpandmod_dropcards exist in the catalog but are not emitted by this function — they are populated elsewhere (level recap / post-mission inventory).
Level reward track
LEVEL_REWARD_TRACK is a fixed five-entry ladder at thresholds 20 / 40 / 60 / 80 / 100. Each entry lists one or more { cardId, amount } rewards granted when the chest is opened. Progress is filled in by accumulating LEVEL_TRACK_POINTS_PER_RUN[tier] per completed run (comments document ~20 bronze runs → ~6 diamond runs to fill the 100-point track). Track is permanently complete once the threshold-100 chest is claimed.
The track and rollPostRunRewards are independent: a single completed run can both contribute points to the track and emit post-run rewards.
Invariants worth knowing
- Warp Crystals are never produced by this module. The roller and the track only reference IDs present in
REWARD_CARDS, which intentionally omits the premium currency. - A completed level guarantees at least one ship pull, sourced via
MIN_PULL_BY_TIER(the doc comment promises “min 1 common per completed level”; the implementation gates onsurvived). STAR_XP_BASEis intentionally stingy. First-win-of-day applies a flatSTAR_XP_DAILY_MULT(10x) on top.- Card identity is the
idstring. Display fields (name,icon,color) are presentational — consumers should not branch on them. - The level-XP card (
level_xp) and mod-drop card (mod_drop_common) are part of the catalog but have no roller in this file.
Consumers (typical)
- Post-run reward screen — calls
rollPostRunRewardsand renders the returnedResolvedReward[]. - Level reward track UI — iterates
LEVEL_REWARD_TRACK, resolves eachcardIdviaREWARD_CARD_MAP. - Mission-completion flow — adds
LEVEL_TRACK_POINTS_PER_RUN[tier]to the track’s progress on completion.
EXTRACT-CANDIDATE
CREDITS_RANGESis module-local but tier-shaped. It mirrors the publicMIN_PULL_BY_TIER/LEVEL_TRACK_POINTS_PER_RUNshape and is the only tier table not exported. Promoting it to an export (or merging the three into a singleREWARDS_BY_TIERrecord) would centralize tier balance in one surface.- Performance tier string is untyped. All three tables and
rollPostRunRewardsacceptstringfor the tier. APerformanceTier = 'bronze' | 'silver' | 'gold' | 'diamond'union (likely already defined whereMissionResultlives) would eliminate the silent fallbacks at lookup-miss. rollPostRunRewardsusesMath.random()directly. No injected RNG / seed, so the roller can’t be deterministically tested or replayed. Threading an RNG argument (or pulling from the game’s seeded RNG) would make this unit-testable.- Catalog ↔ roller coupling is by string id.
'pull_uncommon','pull_common','credits_small','star_xp_base','star_xp_daily'are hard-coded in the roller body. Surfacing these as exportedconstids (or grouping pull cards by rarity in a small lookup) would catch rename drift at compile time. LEVEL_REWARD_TRACKmixes data and structure. The thresholds are evenly spaced (20/40/60/80/100) and could be derived; only therewardsarrays are real data. Splitting payouts from thresholds (or moving the whole table to JSON/data) would let designers tune chests without touching TS.level_xpandmod_drop_commonare orphan catalog rows in this file. They are emitted by other modules — a cross-reference comment or relocating them next to their emitting site would make ownership explicit.