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:

  1. The catalog of reward-card definitions (visual identity + payload type).
  2. The post-run roller that produces a list of resolved cards from a mission result.
  3. 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

TypeMembers / 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

NameKindShapeRole
RewardCardTypetypeunion of 5 string literalscard category
RewardCardRaritytypeunion of 5 string literalsreveal-animation intensity
RewardCardDefinterfacesee abovecatalog row shape
REWARD_CARDSconst arrayRewardCardDef[]catalog, source of truth
REWARD_CARD_MAPconst recordRecord<string, RewardCardDef>id → def fast lookup (built from REWARD_CARDS)
ResolvedRewardinterface{ card, amount }reward-screen payload
STAR_XP_BASEconst number3base star-XP grant
STAR_XP_DAILY_MULTconst number10first-win-of-day multiplier
MIN_PULL_BY_TIERconst recordRecord<string, RewardCardRarity>minimum guaranteed pull rarity per tier
LevelChestRewardinterfacesee abovelevel-track chest shape
LEVEL_REWARD_TRACKconst arrayLevelChestReward[] (length 5)the staged chest ladder
LEVEL_TRACK_POINTS_PER_RUNconst recordRecord<string, number>points added per run by tier
rollPostRunRewardsfunction(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:

TableMaps to
MIN_PULL_BY_TIERminimum RewardCardRarity for the guaranteed pull
LEVEL_TRACK_POINTS_PER_RUNpoints 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:

  1. Credits — always. Amount is Math.round(min + Math.random() * (max - min)) from CREDITS_RANGES[tier]. Card is always credits_small.
  2. Ship pull — only when survived === true. Card is pull_uncommon if the tier’s minimum rarity is 'uncommon', otherwise pull_common. The roller never emits rare or epic pull cards itself — rare pulls come from the level track.
  3. Star XP — always. If isFirstWinOfDay && survived, emits the star_xp_daily card with STAR_XP_BASE * STAR_XP_DAILY_MULT. Otherwise emits star_xp_base with STAR_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 isFirstWinOfDay and survived. A first-win loss gets the base card.
  • The output list contains 2 entries on a loss, 3 on a win.
  • level_xp and mod_drop cards 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 on survived).
  • STAR_XP_BASE is intentionally stingy. First-win-of-day applies a flat STAR_XP_DAILY_MULT (10x) on top.
  • Card identity is the id string. 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 rollPostRunRewards and renders the returned ResolvedReward[].
  • Level reward track UI — iterates LEVEL_REWARD_TRACK, resolves each cardId via REWARD_CARD_MAP.
  • Mission-completion flow — adds LEVEL_TRACK_POINTS_PER_RUN[tier] to the track’s progress on completion.

EXTRACT-CANDIDATE

  • CREDITS_RANGES is module-local but tier-shaped. It mirrors the public MIN_PULL_BY_TIER / LEVEL_TRACK_POINTS_PER_RUN shape and is the only tier table not exported. Promoting it to an export (or merging the three into a single REWARDS_BY_TIER record) would centralize tier balance in one surface.
  • Performance tier string is untyped. All three tables and rollPostRunRewards accept string for the tier. A PerformanceTier = 'bronze' | 'silver' | 'gold' | 'diamond' union (likely already defined where MissionResult lives) would eliminate the silent fallbacks at lookup-miss.
  • rollPostRunRewards uses Math.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 exported const ids (or grouping pull cards by rarity in a small lookup) would catch rename drift at compile time.
  • LEVEL_REWARD_TRACK mixes data and structure. The thresholds are evenly spaced (20/40/60/80/100) and could be derived; only the rewards arrays are real data. Splitting payouts from thresholds (or moving the whole table to JSON/data) would let designers tune chests without touching TS.
  • level_xp and mod_drop_common are 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.