PURPOSE

Playground tab that controls hub/spoke pattern generation, terrain settings, enemy spawning, and a build mode for placing terrain blockers and triggers. Instance-side (left panel) edits affect the current test run only; base-side (right panel) edits rewrite the shared level-presets.ts source via the playground push pipeline.

OWNS

  • Section open/closed state: patternOpen, spawnOpen, buildOpen.
  • Spawning runtime knobs not stored in LevelConfig: spawnerOn, spawnRate, knobs (enemyHpMult / enemyDamageMult / enemySpeedMult / enemyCountMult).
  • Build mode UX state: buildSubMode (terrain | trigger), selectedTriggerType, triggerRadius.
  • Debug toggles: zoneDebug, wireframe.
  • Latest level-generation stats snapshot returned by the mission.
  • LevelPresetEditor (nested component) owns selectedId and a per-preset drafts map of pending Partial<LevelConfig> patches.

READS FROM

  • DEFAULT_LEVEL_CONFIG from ../../data/level-config as the fallback when no levelConfig prop is supplied.
  • LEVEL_PRESETS, LEVEL_PRESET_NAMES, LevelPresetId from ../../data/level-presets.
  • Types LevelConfig, PatternType, TerrainTypeId, TriggerType from ../../data/level-config.
  • Panel, StatRow, PushButton, BTN_STYLE, LABEL_STYLE, TabProps from ./PlaygroundShared.
  • missionRef.current?.getCameraTransform() to anchor placed terrain and triggers at the camera position.
  • missionRef.current?.sandboxGetLevelStats?.() for the post-regen stats readout.
  • missionRef.current?.sandboxGetTerrainCount() and sandboxGetTriggerCount() for undo and counter UI.

PUSHES TO

  • setLevelConfig (setLc) prop — instance-side slider/select edits patch the active LevelConfig via a set(patch) helper.
  • setBuildMode, setSelectedShape props — bubble build-mode UI selections up to the playground host.
  • Mission ref methods on the active run:
    • setSpawnerEnabled(boolean)
    • sandboxSetSpawnRate(value)
    • setWorldKnobs(knobs)
    • sandboxSetLevelConfig(config) (only on preset load or REGENERATE WORLD click)
    • sandboxSetZoneDebug(next)
    • sandboxSetHubSpokeWireframe(next)
    • sandboxPlaceTerrain(shape, x, y, size, rotation)
    • sandboxRemoveTerrain(index)
    • sandboxAddTrigger({ id, type, x, y, radius, oneShot, payload })
    • sandboxUndoTrigger()
    • clearEnemies()
  • pushStats from ../../services/playgroundPush with target: 'level-preset' to rewrite level-presets.ts from the base-side editor.

DOES NOT

  • Does not regenerate the world automatically on slider changes. Pattern, terrain, and seed edits accumulate in React state until the user clicks REGENERATE WORLD or loads a preset.
  • Does not own LevelConfig storage — lc/setLc are received as props.
  • Does not own buildMode or selectedShape — those are received as props from the playground host with local fallbacks for when they are absent.
  • Does not edit non-numeric LevelConfig fields in the base-side editor (notes line states these are edited via source for now).
  • Does not implement spawn-one controls — comments explicitly direct the user to the ENEMIES tab.
  • Does not render a wilds fog slider; comment notes the fog system was deleted because it caused checkerboard grid artifacts.

Signals

  • Preset button click in LEVEL GENERATION calls loadPreset(id) which replaces LevelConfig and immediately invokes sandboxSetLevelConfig(preset).
  • World mode toggle between precomputed and scripted swaps the body of the LEVEL GENERATION panel; scripted mode shows an explanatory stub instead of geometry controls.
  • Pattern selection (chaotic, rings, grid, zigzag) gates which geometry sliders render under hub/spoke.
  • randomizeSeed writes a new integer seed into LevelConfig but does not regenerate until REGENERATE WORLD is clicked.
  • fetchStats posts a 200ms delayed read of sandboxGetLevelStats after a regen click to surface hub/spoke/terrain/event/grid/bake stats.
  • Spawner ON/OFF chip in the SPAWNING panel header toggles setSpawnerEnabled and stops event propagation so the panel doesn’t collapse.
  • Spawn Pressure slider routes through changeSpawnRate which mirrors spawnRate state and sandboxSetSpawnRate in one step.
  • Knob sliders (HP / Damage / Speed / Max Enemy Scale) all flow through patchKnob which merges into the local knobs object and calls setWorldKnobs with the merged value.
  • CLEAR ALL button on SPAWNING calls clearEnemies().
  • BUILD MODE panel shows sub-mode selector (TERRAIN / TRIGGER) only when buildMode is on.
  • Terrain sub-mode PLACE button reads camera transform and places the selected shape at camera position offset by +200 on x.
  • Terrain UNDO removes the last terrain entry by index = count − 1.
  • Trigger sub-mode PLACE constructs a payload that branches by trigger type: spawn_wave uses a hard-coded enemy mix (3 shooter_common + 2 orb_common), dialogue uses a hard-coded test string and 3-second duration, others get {}.

Entry points

  • Default export LevelTab is the React component consumed by the playground host (matches the TabProps contract from PlaygroundShared).
  • side prop branches the render: 'base' returns the nested LevelPresetEditor only; any other value (default 'instance') returns the full instance-side panel stack (LEVEL GENERATION + SPAWNING + BUILD MODE).
  • LevelPresetEditor is a private nested component, not exported.

Pattern notes

  • Module-scope PATTERNS, TERRAIN_TYPES, PRESET_IDS, and PRESET_NUMERIC_FIELDS are typed constant arrays used for declarative button/select rendering.
  • set is a useCallback-wrapped patcher that wraps setLc and falls back silently when setLc is undefined.
  • The instance-side panel deliberately decouples slider state from world regeneration — only REGENERATE WORLD or preset load invokes sandboxSetLevelConfig. This is documented in an inline comment.
  • The base-side LevelPresetEditor keeps a per-preset draft map keyed by LevelPresetId so switching presets preserves unsaved edits per id; the draft is cleared for the active preset only on a successful push.
  • pushPreset returns a boolean to satisfy PushButton’s contract; failures swallow the exception and return false.
  • Build-mode and selected-shape props are defaulted with ?? false and ?? 'blocker_rect' and no-op setters so the component renders safely without a host that wires them up.
  • Inline styles dominate the file; shared style tokens (BTN_STYLE, LABEL_STYLE) come from PlaygroundShared and per-state colors (orange #f97316 for build, blue #60a5fa for selection, green #22c55e for active toggles, purple #a855f7 for triggers, cyan #38bdf8 for wireframe, violet #7c3aed for zone debug) are hard-coded at use sites.
  • The terrain select element stops both React-synthetic and native keyboard propagation to prevent the playground’s global key handlers from intercepting option-list navigation.