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
PlanetsTabdefault export, aTabPropsconsumer that switches onsideto render either the instance list or the base editor. - Local draft state for three independent edit surfaces:
drafts(per-planet patches keyed byPlanetId),biomeDrafts(per-biome patches keyed byBiomeId), andpresetDrafts(per-preset patches keyed byLevelPresetId). - The currently selected planet id (
selectedPlanetId, defaults toPLANET_ORDER[0]). - Three local subcomponents:
RecordPicker(typed select dropdown),ToggleRow(boolean ON/OFF button), andReferenceEditor(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) andPRESET_FIELDS(nine numeric level-preset knobs). - Three push callbacks:
pushPlanet,pushBiome,pushPreset. Each guards on a non-empty draft, callspushStatswith the appropriatetarget, and clears the matching draft slice on success. - The
launchPlanetcallback, 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 typesPlanetDef,PlanetId,BiomeId,BossId,PostProcessingMode.engine/world/generation: theBIOMESregistry and theBiomeConfigtype.data/level-presets:LEVEL_PRESETS,LEVEL_PRESET_NAMES, and theLevelPresetIdtype.data/level-config: theLevelConfigtype used to derive editable numeric preset fields.data/bosses: theBOSS_DEFSregistry, whose keys populate the boss picker.screens/playground/PlaygroundShared:Panel,StatRow,PushButton,BTN_STYLE,LABEL_STYLE, and theTabPropsinterface.
PUSHES TO
services/playgroundPushviapushStatswith 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
missionRefprop, callingsandboxRegenerateWorld(biome)and conditionallysandboxSetLevelConfig(LEVEL_PRESETS[preset])fromlaunchPlanet.
DOES NOT
- Mutate
PLANETS,BIOMES,LEVEL_PRESETS, orBOSS_DEFSat runtime — pushes go through thepushStatsservice 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 whensideis unset or'base'. - Provide a “revert draft” affordance — drafts clear only on a successful push, not on demand.
Signals
pushStatsreturningok: trueclears the matching slice of the local draft store (planet, biome, or preset) and resolves the correspondingonPushpromise withtrue, whichPushButtonuses to flash its success state.pushStatsreturningok: falseor throwing leaves the draft intact and resolves the promise withfalse; thetry/catchswallows 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
MissionHandlemethodssandboxRegenerateWorldandsandboxSetLevelConfigare 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, andsidethroughTabProps. 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
pushStatstarget. - 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 ofBiomeConfigandLevelConfig. The min/max/step bounds are hand-tuned and live in the field array itself, not in the data layer. RecordPickerstops keyboard events from bubbling (stopPropagationandstopImmediatePropagationon 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
undefinedin 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
PLANETSandPLANET_ORDERare module-level constants. - The
ReferenceEditorcollapsed-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 whenp.bossis undefined; the underlying data is left asundefinedand the runtime fallback lives elsewhere.