PURPOSE

Ship Playground tab for inspecting and editing planet definitions and the shared records each planet points at. Implements the Option D layout: an instance-side planet list with per-planet LAUNCH buttons that restart the sandbox world, and a base-side source editor with three independently push-able editors (planet, referenced biome, referenced level preset). Edits to shared biome and preset records are flagged with a usage-count warning so the operator knows how many planets a single push will affect.

OWNS

  • The PlanetsTab default export, a TabProps consumer that switches on side to render either the instance list or the base editor.
  • Local draft state for three independent edit surfaces: drafts (per-planet patches keyed by PlanetId), biomeDrafts (per-biome patches keyed by BiomeId), and presetDrafts (per-preset patches keyed by LevelPresetId).
  • The currently selected planet id (selectedPlanetId, defaults to PLANET_ORDER[0]).
  • Three local subcomponents: RecordPicker (typed select dropdown), ToggleRow (boolean ON/OFF button), and ReferenceEditor (collapsible card with header, optional usage-count warning banner, and a dedicated PUSH button per shared record).
  • Two static field schemas: BIOME_FIELDS (ten numeric biome knobs with min/max/step) and PRESET_FIELDS (nine numeric level-preset knobs).
  • Three push callbacks: pushPlanet, pushBiome, pushPreset. Each guards on a non-empty draft, calls pushStats with the appropriate target, and clears the matching draft slice on success.
  • The launchPlanet callback, which restarts the sandbox world via the mission bridge using the planet’s biome and (optional) level preset.
  • Memoized usage counts (biomeUsageCount, presetUsageCount) used to render the “affects N planets” warning when a shared record has more than one referent.

READS FROM

  • data/planet-config: PLANETS, PLANET_ORDER, and the types PlanetDef, PlanetId, BiomeId, BossId, PostProcessingMode.
  • engine/world/generation: the BIOMES registry and the BiomeConfig type.
  • data/level-presets: LEVEL_PRESETS, LEVEL_PRESET_NAMES, and the LevelPresetId type.
  • data/level-config: the LevelConfig type used to derive editable numeric preset fields.
  • data/bosses: the BOSS_DEFS registry, whose keys populate the boss picker.
  • screens/playground/PlaygroundShared: Panel, StatRow, PushButton, BTN_STYLE, LABEL_STYLE, and the TabProps interface.

PUSHES TO

  • services/playgroundPush via pushStats with three distinct targets:
    • { target: 'planet', id: selectedPlanetId, patch } for per-planet field edits.
    • { target: 'biome', id: currentBiomeId, patch } for the referenced biome record.
    • { target: 'level-preset', id: currentPresetId, patch } for the referenced level-preset record.
  • The mission bridge handle via the missionRef prop, calling sandboxRegenerateWorld(biome) and conditionally sandboxSetLevelConfig(LEVEL_PRESETS[preset]) from launchPlanet.

DOES NOT

  • Mutate PLANETS, BIOMES, LEVEL_PRESETS, or BOSS_DEFS at runtime — pushes go through the pushStats service and are applied by the dev-server plugin that rewrites the underlying data source files.
  • Persist drafts across mounts or tab switches; closing or remounting the tab discards unpushed edits.
  • Validate that biome/preset combinations are coherent — the user can repoint any planet at any biome or preset and the editor will display the result.
  • Show or edit non-numeric biome or preset fields (terrain pools, palette, pattern, terrainType, lanternPreset). The preset palette is displayed read-only beneath the numeric grid; the biome’s terrain pool is displayed in the editor subtitle.
  • Render anything other than the instance list when side === 'instance'; the right-hand editor only renders when side is unset or 'base'.
  • Provide a “revert draft” affordance — drafts clear only on a successful push, not on demand.

Signals

  • pushStats returning ok: true clears the matching slice of the local draft store (planet, biome, or preset) and resolves the corresponding onPush promise with true, which PushButton uses to flash its success state.
  • pushStats returning ok: false or throwing leaves the draft intact and resolves the promise with false; the try/catch swallows the exception so the UI never throws.
  • Selecting a different planet via either the left-panel row button or the right-panel pill button updates selectedPlanetId, which immediately rerouts the editor to the new planet’s draft slice and shifts the referenced biome/preset to whatever the newly selected planet points at.
  • The MissionHandle methods sandboxRegenerateWorld and sandboxSetLevelConfig are optional-chained; if the handle or methods are absent, LAUNCH is a no-op rather than an error.

Entry points

  • Rendered as one of the tabs inside the Ship Playground screen via the playground tab router; the parent passes missionRef, shipId, and side through TabProps.
  • side='instance' renders the planet list with LAUNCH buttons (left panel).
  • side='base' (or unset) renders the source editor with the planet picker, pointer pickers, per-planet field grid, and the two collapsible reference editors (right panel).

Pattern notes

  • Three independent draft maps with three independent PUSH buttons is the load-bearing structure: it lets the operator edit a planet’s fields, the biome it points at, and the preset it points at without conflating the pushes. Each PUSH targets a different file via a different pushStats target.
  • The editable numeric field schemas are derived at the type level using a mapped-type filter ({ [K in keyof T]: T[K] extends number ? K : never }[keyof T]) so the field arrays are constrained to actual numeric keys of BiomeConfig and LevelConfig. The min/max/step bounds are hand-tuned and live in the field array itself, not in the data layer.
  • RecordPicker stops keyboard events from bubbling (stopPropagation and stopImmediatePropagation on the native event) so the playground’s global key bindings do not fire while the user is typing in a select.
  • The “no preset” and “no boss” cases are modeled by prepending an empty-string sentinel to the options list and coercing the empty string back to undefined in the onChange handler, which keeps the pickers strictly typed without a null branch in the option list.
  • Usage-count warnings are only rendered when count > 1; a record with a single referent shows no banner. Counts are recomputed only on mount (empty dependency arrays) because PLANETS and PLANET_ORDER are module-level constants.
  • The ReferenceEditor collapsed-by-default pattern hides the PUSH button until the editor is opened, so the operator cannot accidentally push an empty patch for a shared record they did not intend to edit.
  • The boss line in the planet list falls back to the literal string 'iron_throne (fallback)' for display only when p.boss is undefined; the underlying data is left as undefined and the runtime fallback lives elsewhere.