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
| Type | Members | Notes |
|---|---|---|
PlanetId | 3 | 12 | 21 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | Closed union — adding a planet requires extending this literal. ID 99 (Obsidian Spire) is cast through as PlanetId for the data-only test entry. |
BiomeId | landing_site | sunrise_city | the_voidstar | obsidian_spire | delphi | old_earth | World-generation style (terrain, shape pools, hub topology). Most planets reuse landing_site as a neutral base. |
PostProcessingMode | dark | sunlit | Mastering preset applied during runs. |
BossId | pacemaker | first_lady | cenotaph | iron_throne | spire | grand_procession | ringmaster | foreman | apex | Must match a key in BOSS_DEFS (engine/boss/boss-defs.ts). |
PlanetDef interface
| Field | Type | Purpose |
|---|---|---|
id | PlanetId | Numeric ID — matches display, Supabase, run context. |
biome | BiomeId | World-gen style. |
name | string | Display name on hub screen. ALL-CAPS in data. |
image | string | Path to planet image asset under /planets/. |
buildingsAllowed | boolean | Whether buildings can be placed on this planet. |
postProcessing | PostProcessingMode | Run mastering preset. |
fogAlpha | number | Atmospheric fog noise overlay opacity. 0 = none (void), 0.65 = thick (city). |
enemyCountMult | number | Spawner count multiplier. 1.0 = normal, 2.0 = Voidstar double pressure. |
spawnGraceSeconds | number | Grace period before full spawner pressure. 0 = instant (Voidstar). |
shadowOffsetMult | number | Drop-shadow Y-offset multiplier. 1.0 = default (14px desktop / 8px mobile); 2.0 = Sunrise City tall buildings. |
enemySet | EnemySetId | Determines archetype + spawn pools. bugs = orb/charger/shooter/mortar; city = gunner/field/sniper/racer; suffixed variants bias one archetype. |
isLeaderboard? | boolean | Leaderboard planet (shows leaderboard + tier rewards instead of challenges + progress bar). Voidstar (3) and Speedway (31). |
levelPreset? | LevelPresetId | References a LevelConfig preset for hub/spoke generation, visual style, etc. Falls back to 'derelict_station' when omitted. |
boss? | BossId | Boss 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. |
destructibles | DestructibleDensity | Per-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.
| Field | Purpose |
|---|---|
crates | Wooden/scrap crates — break on ram, drop XP. Universally 40 across all current planets. |
debris | Floating metal-debris cloud — clusters, deal % HP+shield damage on contact. Tuned per-planet for spatial variety (4–19). |
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]. Planet99(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. Returnsundefinedfor unknown IDs.
Planet roster
11 player-accessible planets in PLANET_ORDER + 1 hidden test entry (id 99).
| ID | Name | Biome | Post-FX | Fog | Enemy mult | Grace | Shadow | Enemy set | Level preset | Boss | Leaderboard | Crates / Debris |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 12 | LANDING SITE | landing_site | dark | 0.15 | 1.0 | 1s | 1.0 | bugs | derelict_station | pacemaker | — | 40 / 4 |
| 21 | SUNRISE CITY | sunrise_city | sunlit | 0.78 | 1.0 | 1s | 2.0 | city | sunrise_grid | first_lady | — | 40 / 16 |
| 3 | THE VOIDSTAR | the_voidstar | dark | 0.0 | 2.0 | 0s | 1.0 | bugs | voidstar_abyss | cenotaph | yes | 40 / 14 |
| 30 | SOLARIS | landing_site | sunlit | 0.20 | 1.0 | 1s | 1.0 | bugs_mortar | solar_flats | iron_throne | — | 40 / 11 |
| 31 | SPEEDWAY | landing_site | dark | 0.10 | 1.0 | 1s | 1.0 | bugs_shooter | speed_circuit | spire | yes | 40 / 19 |
| 32 | EDEN-5 | landing_site | sunlit | 0.20 | 1.0 | 1s | 1.0 | bugs_charger | eden_grove | grand_procession | — | 40 / 10 |
| 33 | OLD EARTH | old_earth | dark | 0.25 | 1.0 | 1s | 1.0 | bugs_sniper | old_earth_ruins | ringmaster | — | 40 / 16 |
| 34 | NETWORK STATION | landing_site | dark | 0.15 | 1.0 | 1s | 1.0 | bugs_field | network_grid | foreman | — | 40 / 15 |
| 35 | DELPHI | delphi | sunlit | 0.15 | 1.0 | 1s | 1.0 | bugs_racer | delphi_crystal | apex | — | 40 / 12 |
| 36 | DESOLATION | landing_site | dark | 0.15 | 1.0 | 1s | 1.0 | bugs_heavy | desolation_field | — (fallback iron_throne) | — | 40 / 18 |
| 37 | OBELISK | landing_site | dark | 0.10 | 1.0 | 1s | 1.0 | bugs_mixed | obelisk_pillars | — (fallback iron_throne) | — | 40 / 13 |
| 99 | OBSIDIAN SPIRE | obsidian_spire | dark | 0.05 | 1.5 | 1s | 1.5 | bugs | (none, falls back) | — | — | 40 / 8 |
Per-planet flavor comments (data-side intent):
30Solaris — scorched amber plains, mortar pressure.31Speedway — open circuits, shooter gauntlets.32Eden-5 — lush growth, charger swarms.33Old Earth — grey ruins, sniper invaders.34Network Station — cyber-grid, field emitter rares.35Delphi — crystal cave, racer swarms.36Desolation — barren rock, mortar+shooter suppression.37Obelisk — pillar maze, all-rare mix.99Obsidian Spire — pure obsidian pillar terrain. Data-only definition for testing; placeholder image reuses Voidstar art.
Notable invariants
cratesis currently constant at 40 across all 12 entries. Spatial variety comes entirely from thedebrisslider (range 4–19).- Only Voidstar (3) breaks
enemyCountMultandspawnGraceSecondsfor player-accessible planets (2.0/0). Obsidian Spire (99) bumpsenemyCountMultto1.5but is not player-facing. - Only Sunrise City (21) uses
shadowOffsetMult: 2.0. Obsidian Spire (99) uses1.5. All other planets are1.0. fogAlpha: 0.78for Sunrise City is the maximum across the roster —0.0(Voidstar) is the minimum. The PlanetDef JSDoc claims0.65for “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. PLANETSis declaredRecord<number, PlanetDef>(notRecord<PlanetId, PlanetDef>) — this lets id 99 live in the map without expanding thePlanetIdunion, at the cost of looser key typing.
Imports / exports
- Type imports:
EnemySetIdfrom./enemies,LevelPresetIdfrom./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
PLANETSobject literal. Hoist into a siblingPLANET_FLAVOR: Record<PlanetId, string>if any UI ever wants to surface them as tooltips or codex blurbs. - Fallback-boss policy (
'iron_throne'whenbossis missing) is documented in theboss?field comment but enforced elsewhere (boss runtime). Consider promoting to a named constantDEFAULT_BOSS_ID: BossId = 'iron_throne'exported from here so the rule lives next to the data. - Fallback-level-preset policy (
'derelict_station'whenlevelPresetis missing) is similarly documented but enforced downstream. Same extraction opportunity:DEFAULT_LEVEL_PRESET_ID. shadowOffsetMultbase 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: 40repeated 12 times — if crate density genuinely never varies per planet, lift to a singleDEFAULT_CRATE_DENSITYconstant. If variation is planned, leave as-is.- The id-99
as PlanetIdcast exists because Obsidian Spire is data-only but the map key system usesPlanetId. Either add99to thePlanetIdunion with a tag like// hidden: data-only, or splitPLANETSinto a public map and a_HIDDEN_PLANETSmap to keep the union honest.