planet-progression.ts

Per-planet 10-level XP reward track. Each planet has 10 cumulative XP thresholds and 10 typed rewards. XP feeds in from challenge bounties and base run completion XP.

Contract

  • Reward levels are 1-indexed (1–10).
  • xpThresholds[i] = cumulative XP to reach level i+2.
  • Players start at level 1 (0 XP). Thresholds gate levels 2–10.
  • Harder planets need more XP but their challenges award proportionally more.

Types

SymbolShape
PlanetRewardTypeunion: custom_event | ship_common | weapon_1 | ship_uncommon | alt_boss | ship_rare | planet_master | ship_epic | weapon_2 | global_buff
TrackRewardRarityunion: common | uncommon | rare | epic | legendary
PlanetTrackReward{ level, type, name, description, lockedLabel, emoji, placeholderId }
PlanetTrackDef{ planetId, xpThresholds: number[], baseRunXp: number, rewards: PlanetTrackReward[] }

Reward slot map (fixed per planet)

LevelTypeRarityLocked labelEmoji
1custom_eventcommonUnlock: 🏎️
2ship_commoncommonUnlock: New Ship🚀
3weapon_1uncommonUnlock: New Weapon⚔️
4ship_uncommonuncommonUnlock: New Ship🚀
5alt_bossrareUnlock: Secret Boss👹
6ship_rarerareUnlock: New Ship🚀
7planet_masterepicUnlock: Planet Master🌟
8ship_epicepicUnlock: New Ship🚀
9weapon_2legendaryUnlock: New Weapon⚔️
10global_bufflegendaryUnlock: Global Buff💎

Rarity → level mapping

TRACK_REWARD_RARITY[] indexed 0–9:

  • Levels 1–2 → common
  • Levels 3–4 → uncommon
  • Levels 5–6 → rare
  • Levels 7–8 → epic
  • Levels 9–10 → legendary

Color tables

TRACK_RARITY_COLORS (border/accent):

RarityHex
common#ffffff
uncommon#50ff78
rare#44aaff
epic#cc66ff
legendary#ffd228

TRACK_RARITY_BG (2-tone for collectibles + card headers):

Raritydarklight
common#2a2a2a#ffffff
uncommon#0a5c2a#d4ffd8
rare#0a2a6e#d8eaff
epic#3a0a5c#f0d8f8
legendary#a03000#ffe8b0

Base XP thresholds (Landing Site = 1.0× baseline)

BASE_THRESHOLDS = [100, 250, 500, 800, 1200, 1700, 2300, 3000, 4000, 5500]

Cumulative — index i gates level i+2. Level 10 cap = xpThresholds[9].

Per-planet XP multiplier (PLANET_XP_MULT)

Planet IDNameMultiplierBase run XP
12Landing Site1.0×10
21Sunrise City1.5×15
3Voidstar2.0×20
30Solaris1.0× (placeholder)10
31Speedway1.0× (placeholder)10
32Eden1.0× (placeholder)10
33Old Earth1.0× (placeholder)10
34Network Station1.0× (placeholder)10
35Delphi1.0× (placeholder)10
36Desolation1.0× (placeholder)10
37Obelisk1.0× (placeholder)10

xpThresholds is built per planet as BASE_THRESHOLDS.map(t => Math.round(t * mult)).

Per-planet reward naming

Each planet supplies six naming slots used by buildRewards():

Planeteventbossbuff
12 Landing SiteRacing EventSecret BossGlobal Defense Boost
21 Sunrise CityCity EventSecret BossGlobal Speed Boost
3 VoidstarVoid EventSecret BossGlobal Damage Boost
30 SolarisSolar EventSecret BossGlobal Fire Rate Boost
31 SpeedwaySpeed EventSecret BossGlobal Crit Boost
32 EdenEden-5 EventSecret BossGlobal HP Regen
33 Old EarthEarth EventSecret BossGlobal Shield Boost
34 Network StationNetwork EventSecret BossGlobal XP Boost
35 DelphiOracle EventSecret BossGlobal Luck Boost
36 DesolationWasteland EventSecret BossGlobal Armor Boost
37 ObeliskObelisk EventSecret BossGlobal Magnet Boost

All ship, w1, w2 slots are currently 'TBD' / 'TBD Weapon'. placeholderId is '' for every reward — to be assigned per planet later.

Build pipeline

  • buildRewards(planetId) → 10 PlanetTrackReward objects, slot-fixed, names interpolated from per-planet names[].
  • buildTrackDef(planetId){ planetId, xpThresholds, baseRunXp, rewards }.
  • PLANET_TRACKS: Record<number, PlanetTrackDef> — exported map, populated by calling buildTrackDef for all 11 planet IDs at module load.

Exported helpers

FunctionReturnsNotes
getPlanetTrack(planetId)PlanetTrackDefThrows Error("No planet track for planet ${planetId}") if missing.
getLevelForXp(planetId, xp)number (1–10)Iterates thresholds, bumps level for each crossed, clamps to 10.
getXpProgress(planetId, xp){ level, currentXp, nextThreshold, pct, totalXp }nextThreshold is the span (range) for the next level, not absolute. pct clamped 0–1. At level 10 returns { level: 10, currentXp: 0, nextThreshold: 0, pct: 1, totalXp: xp }.
getOverallPct(planetId, xp)number (0–1)xp / xpThresholds[9] clamped to 1. For the compact hub bar.

Level computation rules

getLevelForXp semantics:

  • Start at level = 1.
  • For each threshold xpThresholds[i], if xp >= thresholdlevel = i + 2, else break.
  • Final clamp: Math.min(level, 10).

getXpProgress semantics:

  • If level >= 10 → bar full, no further progress.
  • Else thresholdIdx = level - 1; prevThreshold = thresholdIdx > 0 ? xpThresholds[thresholdIdx - 1] : 0.
  • currentXp = xp - prevThreshold; range = nextThreshold - prevThreshold; pct = currentXp / range (clamped, 0 if range ≤ 0).
  • Returned nextThreshold field is the range, not the absolute threshold value.

Dependencies

  • PlanetId from ./planet-config.

EXTRACT-CANDIDATE

  • BASE_THRESHOLDS, PLANET_XP_MULT, BASE_RUN_XP → move into a data table (e.g. planet-progression-data.ts or merged into planet-config) so balance pass can edit numbers without touching logic.
  • Per-planet names map inside buildRewards → extract to a PLANET_REWARD_NAMES: Record<PlanetId, ...> constant alongside the other planet data tables; the function should be pure templating.
  • Reward slot template (the 10-row array literal) → extract to REWARD_SLOT_TEMPLATE constant; buildRewards becomes a .map interpolating n.* into the template, removing 11× near-duplicate object literals.
  • placeholderId field is '' everywhere — either wire up real ship/weapon IDs or delete the field until placeholders are resolved.
  • 'Secret Boss' is hardcoded across every planet — collapse the boss slot in names to a constant until alt-boss identities diverge.
  • getXpProgress returning nextThreshold as a range (not absolute) is a footgun named badly — rename to nextLevelXpRange or expose both nextThresholdAbs and nextThresholdRange.