challenges.ts

Per-planet challenge definitions with rarity tiers. Generates a flat array of 45 challenges (15 per planet × 3 active planets) at module load. Rewards are warp crystals (gems) plus planet XP bounty.

Purpose

  • Defines the shape of a challenge: planet binding, category, rarity, scope, condition, reward.
  • Hand-tunes targets per planet so harder planets demand more.
  • Generates the canonical CHALLENGES array and lookup helpers for store and UI consumers (future challengeStore, future HubScreen cards).

Public API

Types

NameKindShape
ChallengeRarityunion'common' | 'uncommon' | 'rare' | 'epic' | 'legendary'
ChallengeCategoryunion'tier' | 'kills' | 'events'
ChallengeConditiontagged union{ type: 'reach_tier' | 'kill_count' | 'events_completed'; target: number }
ChallengeScopeunion'run' | 'lifetime'
ChallengeDefinterface{ id, planetId, category, rarity, name, description, scope, condition, reward: { gems, xp } }

ID format: {planetShort}_{category}_{rarity} (e.g. ls_kills_common).

Exports

SymbolTypeDescription
CHALLENGE_RARITIESChallengeRarity[]Ordered rarity list (common → legendary).
CHALLENGESChallengeDef[]Flat array, all 45 challenges. Ordered by planet then category.
STARTER_WEAPONSreadonly ['rifle', 'shotgun']Always-in-pool weapons regardless of challenge completion.
getChallengesForPlanet(planetId)(PlanetId) => ChallengeDef[]Filter by planet.
getChallengesByCategory(planetId, category)(PlanetId, ChallengeCategory) => ChallengeDef[]Filter by planet and category.
getChallengeById(id)(string) => ChallengeDef | undefinedO(1) lookup via internal CHALLENGE_BY_ID map.

Reward Tables

Gems per rarity (shared across categories)

RarityGems
Common5
Uncommon15
Rare40
Epic100
Legendary300

XP bounty base (Landing Site values; scaled per planet)

RarityBase XP
Common30
Uncommon60
Rare150
Epic400
Legendary800

XP awarded = round(XP_BOUNTY_BASE[rarity] * PLANET_XP_BOUNTY_MULT[planetId]).

Planet XP bounty multipliers

Planet IDPlanetMultiplier
12Landing Site1.0
21Sunrise City1.5
3Voidstar2.0
30–37Solaris, Speedway, Eden, Old Earth, Network Station, Delphi, Desolation, Obelisk1.0 (placeholder)

Per-Planet Targets

Targets are hand-tuned. Format: [Common, Uncommon, Rare, Epic, Legendary].

Landing Site (id 12, short ls) — easiest

CategoryTargets
tier3, 5, 8, 12, 20
kills50, 150, 300, 2 000, 10 000
events1, 3, 5, 25, 100

Sunrise City (id 21, short sc) — medium

CategoryTargets
tier4, 6, 10, 15, 25
kills75, 200, 500, 3 000, 15 000
events2, 4, 6, 30, 120

Voidstar (id 3, short vs) — hardest, 2× enemy pressure

CategoryTargets
tier5, 8, 12, 18, 30
kills100, 300, 750, 5 000, 25 000
events2, 5, 8, 40, 150

Placeholder planets (30–37) use LANDING_SITE_TARGETS until tuned.

Scope Rules

CategoryCommonUncommonRareEpicLegendary
tierrunrunrunrunrun
killsrunrunrunlifetimelifetime
eventsrunrunrunlifetimelifetime

tier is always per-run (you reach a tier within one run). kills and events flip to lifetime at epic+.

Description string suffix is rendered from scope:

  • 'run'" in a single run"
  • 'lifetime'" total"

Names by Category × Rarity

RarityTierKillsEvents
CommonWarm UpPest ControlFirst Contact
UncommonProving GroundScrapperOpportunist
RareDeep RunAce PilotTrailblazer
EpicEnduranceWar MachineVeteran Explorer
LegendaryUnstoppableExtinction EventCartographer

Planet Short Codes (for ID generation)

IDShortPlanet
12lsLanding Site
21scSunrise City
3vsVoidstar
30soSolaris
31spSpeedway
32edEden
33oeOld Earth
34nsNetwork Station
35dpDelphi
36dsDesolation
37obObelisk

Generation Flow

  1. buildPlanetChallenges(planetId) looks up PLANET_SHORT, PLANET_TARGETS, and PLANET_XP_BOUNTY_MULT for the planet.
  2. Loops i = 0..4 over CHALLENGE_RARITIES.
  3. For each rarity, computes gems = GEMS[rarity] and xp = round(XP_BOUNTY_BASE[rarity] * xpMult).
  4. Pushes three ChallengeDef entries — one each for tier, kills, events — with the rarity’s target and the appropriate scope.
  5. Returns 15 challenges per planet.
  6. CHALLENGES concatenates the output for planets 12, 21, 3 → 45 total.
  7. CHALLENGE_BY_ID indexes the flat list at module load for O(1) lookup.

scopeLabel(scope) returns the description suffix. Event descriptions pluralize via t.events[i] > 1 ? 's' : ''. Kill counts render with toLocaleString() for thousands separators.

Dependencies

  • import type { PlanetId } from './planet-config' — only type import. No runtime coupling.
  • Targets and XP multipliers are local constants; the file is self-contained at runtime.

Consumers (declared in source comments)

  • Future challengeStore — tracks completion state.
  • Future HubScreen UI — challenge display cards.

Notes / Gotchas

  • The XP multiplier table comment claims it “matches the threshold scaling in planet-progression.ts” — placeholder planets all default to 1.0 and LANDING_SITE_TARGETS, so once those planets ship they need both a real PLANET_XP_BOUNTY_MULT value and a real PlanetTargets entry.
  • Only planets 12, 21, 3 are currently built into CHALLENGES despite tables and short codes existing for 30–37. Activating a new planet requires adding a ...buildPlanetChallenges(<id>) line to the CHALLENGES array.
  • STARTER_WEAPONS lives in this file but is unrelated to the challenge generator — it is a sibling export documenting weapons always available regardless of challenge completion.
  • CHALLENGE_BY_ID is module-private; lookups must go through getChallengeById.

EXTRACT-CANDIDATE

  • Rarity → reward mapping (GEMS, XP_BOUNTY_BASE) duplicates a pattern likely needed elsewhere for any rarity-driven reward system (loot, missions). Consider hoisting to a shared rarity-rewards.ts if a second consumer appears.
  • Planet short-code table (PLANET_SHORT) overlaps with PLANET_XP_BOUNTY_MULT and the placeholder planet list — both want to know “which planets exist and at what difficulty.” A unified planet-registry.ts keyed by PlanetId would deduplicate.
  • Scope-label suffix logic (scopeLabel) and the pluralization inline in the events description are description-builder concerns. If more categories or localized strings arrive, factor into a challenge-copy.ts formatter.
  • Per-planet PlanetTargets table is currently inlined as three named constants plus a placeholder fallback. When all 11 planets ship, a single Record<PlanetId, PlanetTargets> literal with no fallback would catch missing tunings at type-check time.