planet-config.ts

Single source of truth for every planet definition: biome, name, image, fog, difficulty, post-processing mode, enemy set, boss, level preset, destructible density. All other files import from here instead of maintaining their own local copies.

PlanetId numeric IDs are a stable contract shared with HubScreen, Supabase, and RunDefinition.context.planetId. Renumbering breaks save data and leaderboard rows.

Public types

TypeMembersNotes
PlanetId3 | 12 | 21 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37Closed union — adding a planet requires extending this literal. ID 99 (Obsidian Spire) is cast through as PlanetId for the data-only test entry.
BiomeIdlanding_site | sunrise_city | the_voidstar | obsidian_spire | delphi | old_earthWorld-generation style (terrain, shape pools, hub topology). Most planets reuse landing_site as a neutral base.
PostProcessingModedark | sunlitMastering preset applied during runs.
BossIdpacemaker | first_lady | cenotaph | iron_throne | spire | grand_procession | ringmaster | foreman | apexMust match a key in BOSS_DEFS (engine/boss/boss-defs.ts).

PlanetDef interface

FieldTypePurpose
idPlanetIdNumeric ID — matches display, Supabase, run context.
biomeBiomeIdWorld-gen style.
namestringDisplay name on hub screen. ALL-CAPS in data.
imagestringPath to planet image asset under /planets/.
buildingsAllowedbooleanWhether buildings can be placed on this planet.
postProcessingPostProcessingModeRun mastering preset.
fogAlphanumberAtmospheric fog noise overlay opacity. 0 = none (void), 0.65 = thick (city).
enemyCountMultnumberSpawner count multiplier. 1.0 = normal, 2.0 = Voidstar double pressure.
spawnGraceSecondsnumberGrace period before full spawner pressure. 0 = instant (Voidstar).
shadowOffsetMultnumberDrop-shadow Y-offset multiplier. 1.0 = default (14px desktop / 8px mobile); 2.0 = Sunrise City tall buildings.
enemySetEnemySetIdDetermines archetype + spawn pools. bugs = orb/charger/shooter/mortar; city = gunner/field/sniper/racer; suffixed variants bias one archetype.
isLeaderboard?booleanLeaderboard planet (shows leaderboard + tier rewards instead of challenges + progress bar). Voidstar (3) and Speedway (31).
levelPreset?LevelPresetIdReferences a LevelConfig preset for hub/spoke generation, visual style, etc. Falls back to 'derelict_station' when omitted.
boss?BossIdBoss that spawns at T-0. Falls back to 'iron_throne' when missing. Keyed per-planet (not per-index) so mapping is stable against PLANET_ORDER reorderings.
destructiblesDestructibleDensityPer-biome 0–100 sliders.

DestructibleDensity interface

Per-biome 0–100 sliders. 100 maps to the system-defined max density for that category; 0 disables entirely.

FieldPurpose
cratesWooden/scrap crates — break on ram, drop XP. Universally 40 across all current planets.
debrisFloating metal-debris cloud — clusters, deal % HP+shield damage on contact. Tuned per-planet for spatial variety (419).

Public exports

  • PLANET_ORDER: PlanetId[] — ordered list for the hub swiper. Index = swiper position. Current order: [12, 21, 3, 30, 31, 32, 33, 34, 35, 36, 37]. Planet 99 (Obsidian Spire) is intentionally absent — data-only test entry, not player-accessible.
  • PLANETS: Record<number, PlanetDef> — all definitions keyed by numeric planet ID.
  • getPlanet(id: number): PlanetDef | undefined — lookup helper. Returns undefined for unknown IDs.

Planet roster

11 player-accessible planets in PLANET_ORDER + 1 hidden test entry (id 99).

IDNameBiomePost-FXFogEnemy multGraceShadowEnemy setLevel presetBossLeaderboardCrates / Debris
12LANDING SITElanding_sitedark0.151.01s1.0bugsderelict_stationpacemaker40 / 4
21SUNRISE CITYsunrise_citysunlit0.781.01s2.0citysunrise_gridfirst_lady40 / 16
3THE VOIDSTARthe_voidstardark0.02.00s1.0bugsvoidstar_abysscenotaphyes40 / 14
30SOLARISlanding_sitesunlit0.201.01s1.0bugs_mortarsolar_flatsiron_throne40 / 11
31SPEEDWAYlanding_sitedark0.101.01s1.0bugs_shooterspeed_circuitspireyes40 / 19
32EDEN-5landing_sitesunlit0.201.01s1.0bugs_chargereden_grovegrand_procession40 / 10
33OLD EARTHold_earthdark0.251.01s1.0bugs_sniperold_earth_ruinsringmaster40 / 16
34NETWORK STATIONlanding_sitedark0.151.01s1.0bugs_fieldnetwork_gridforeman40 / 15
35DELPHIdelphisunlit0.151.01s1.0bugs_racerdelphi_crystalapex40 / 12
36DESOLATIONlanding_sitedark0.151.01s1.0bugs_heavydesolation_field— (fallback iron_throne)40 / 18
37OBELISKlanding_sitedark0.101.01s1.0bugs_mixedobelisk_pillars— (fallback iron_throne)40 / 13
99OBSIDIAN SPIREobsidian_spiredark0.051.51s1.5bugs(none, falls back)40 / 8

Per-planet flavor comments (data-side intent):

  • 30 Solaris — scorched amber plains, mortar pressure.
  • 31 Speedway — open circuits, shooter gauntlets.
  • 32 Eden-5 — lush growth, charger swarms.
  • 33 Old Earth — grey ruins, sniper invaders.
  • 34 Network Station — cyber-grid, field emitter rares.
  • 35 Delphi — crystal cave, racer swarms.
  • 36 Desolation — barren rock, mortar+shooter suppression.
  • 37 Obelisk — pillar maze, all-rare mix.
  • 99 Obsidian Spire — pure obsidian pillar terrain. Data-only definition for testing; placeholder image reuses Voidstar art.

Notable invariants

  • crates is currently constant at 40 across all 12 entries. Spatial variety comes entirely from the debris slider (range 4–19).
  • Only Voidstar (3) breaks enemyCountMult and spawnGraceSeconds for player-accessible planets (2.0 / 0). Obsidian Spire (99) bumps enemyCountMult to 1.5 but is not player-facing.
  • Only Sunrise City (21) uses shadowOffsetMult: 2.0. Obsidian Spire (99) uses 1.5. All other planets are 1.0.
  • fogAlpha: 0.78 for Sunrise City is the maximum across the roster — 0.0 (Voidstar) is the minimum. The PlanetDef JSDoc claims 0.65 for “thick (city)” but the data overrides upward.
  • Boss is omitted on Desolation (36) and Obelisk (37), both falling back to 'iron_throne' per the field comment.
  • PLANETS is declared Record<number, PlanetDef> (not Record<PlanetId, PlanetDef>) — this lets id 99 live in the map without expanding the PlanetId union, at the cost of looser key typing.

Imports / exports

  • Type imports: EnemySetId from ./enemies, LevelPresetId from ./level-presets.
  • Exported types: PlanetId, BiomeId, PostProcessingMode, BossId, PlanetDef, DestructibleDensity.
  • Exported values: PLANET_ORDER, PLANETS, getPlanet.

EXTRACT-CANDIDATE

  • Per-planet flavor descriptions (“scorched amber plains, mortar pressure”, etc.) live as JSDoc-style comments inside the PLANETS object literal. Hoist into a sibling PLANET_FLAVOR: Record<PlanetId, string> if any UI ever wants to surface them as tooltips or codex blurbs.
  • Fallback-boss policy ('iron_throne' when boss is missing) is documented in the boss? field comment but enforced elsewhere (boss runtime). Consider promoting to a named constant DEFAULT_BOSS_ID: BossId = 'iron_throne' exported from here so the rule lives next to the data.
  • Fallback-level-preset policy ('derelict_station' when levelPreset is missing) is similarly documented but enforced downstream. Same extraction opportunity: DEFAULT_LEVEL_PRESET_ID.
  • shadowOffsetMult base values (14px desktop / 8px mobile referenced in the field comment) live in the rendering layer, not here. The doc-string and the consumer should share a named constant.
  • crates: 40 repeated 12 times — if crate density genuinely never varies per planet, lift to a single DEFAULT_CRATE_DENSITY constant. If variation is planned, leave as-is.
  • The id-99 as PlanetId cast exists because Obsidian Spire is data-only but the map key system uses PlanetId. Either add 99 to the PlanetId union with a tag like // hidden: data-only, or split PLANETS into a public map and a _HIDDEN_PLANETS map to keep the union honest.