PURPOSE

Playground tab for spawning, previewing, and tuning enemies. Split by side prop: instance renders the LEFT panel (per-archetype spawn-one buttons grouped/collapsible + last-spawned preview); base renders the RIGHT panel (archetype picker, editable baseline stats, rarity-variant preview, PUSH button). The right-side selected archetype is independent of the left-side preview selection — PUSH edits the archetype BASELINE and the five rarity variants derive from RARITY_MULTS at assemble time.

OWNS

  • baseOpen — collapse state for the RIGHT-side ENEMY BASE STATS panel.
  • selectedEnemy — type id of the enemy whose preview is shown on LEFT (defaults to ENEMY_TYPES[0]?.id ?? 'orb_common').
  • archOpen — per-archetype open/closed map for LEFT-side grouped spawn sections.
  • selectedArchetype — which archetype the RIGHT-side baseline editor is editing.
  • archetypeDraftsRecord<archetype, Record<statKey, number>> of pending baseline overrides. Drafts do not apply live; they ship in the PUSH payload and clear on successful push.
  • Module-level constants ARCHETYPE_GROUPS and ARCHETYPE_NAMES, computed once from ENEMY_TYPES via getArchetypeGroups().

READS FROM

  • ../../data/enemiesENEMY_TYPE_MAP, ENEMY_TYPES, type EnemyTypeDef.
  • ./PlaygroundSharedPanel, Section, StatRow, PushButton, BTN_STYLE, LABEL_STYLE, and type TabProps.
  • TabProps.missionRef — used to call getCameraTransform, spawnEnemyAt, and clearEnemies on the active mission handle.
  • TabProps.side'instance' | 'base' (defaults to 'instance'), selects which panel set is rendered.
  • The “common” variant of the selected archetype (commonVariantOfArch) is used as the baseline display source for editable stats and to gate optional stat rows (AoE block when aoeRadius != null; Blast/Fuse block when blastRadius != null).

PUSHES TO

  • missionRef.current?.spawnEnemyAt(typeId, x, y) — spawned at the camera position offset by 300 units along a random angle (Math.random() * Math.PI * 2).
  • missionRef.current?.clearEnemies() — wired to the CLEAR ALL button.
  • ../../services/playgroundPush#pushStats — invoked by pushArchetype with { target: 'enemy-archetype', id: selectedArchetype, patch }. On r.ok === true the draft for that archetype is removed from archetypeDrafts. Returns false on empty patch, network throw, or non-ok result.

DOES NOT

  • Does not apply drafts live to running enemies — edits only ship via PUSH.
  • Does not edit per-rarity variants directly — only the archetype baseline; rarity variants derive from RARITY_MULTS.
  • Does not couple the LEFT preview selection (selectedEnemy) to the RIGHT editor selection (selectedArchetype).
  • Does not render anything when side === 'base' and there is no commonVariantOfArch for the chosen archetype (the stat block and baseline-tint header are skipped).
  • Does not persist archOpen, baseOpen, drafts, or selections across remounts — all state is local React state.
  • Does not handle keyboard input beyond stopping propagation on the LAST SPAWNED <select> element.

Signals

  • LEFT panel: SPAWN ENEMIES (always open, non-toggleable) containing one Section per archetype (uppercased name, collapsible via archOpen), each with one +1 <rarity-3-letter> button per variant tinted by t.tint; plus a red CLEAR ALL button.
  • LEFT panel: LAST SPAWNED (always open) with a <select> of all enemy types showing name (rarity) and a stat readout: HP, Speed, XP, Archetype, Radius (one decimal).
  • RIGHT panel: ENEMY BASE STATS (collapsible via baseOpen) with an extra slot rendering PushButton(onClick=pushArchetype), a helper label, archetype picker buttons (yellow #fbbf24 outline + tinted background when selected), a baseline header (Baseline = <archetype> (common variant) tinted by commonVariantOfArch.tint), and StatRow controls for HP / Speed / Radius / XP / Orbit Radius, plus optional AoE Radius / AoE Cooldown / AoE Damage and Blast Radius / Fuse Time blocks.

Entry points

  • Default export EnemiesTab({ missionRef, side = 'instance' }: TabProps) — React functional component, rendered by the Playground screen for both panel sides.

Pattern notes

  • Side-routed rendering: a single component returns one of two distinct JSX trees based on the side prop, sharing all useState / useMemo / useCallback hooks above the branch so both panels stay logically coupled even though they render separately.
  • Drafts vs. live stats: edits accumulate in archetypeDrafts[archetype] keyed by stat name. effectiveStat(key) falls back through draft common-variant value 0, so the UI shows the in-progress patch without mutating game data.
  • Optional stat rows are gated on the presence of fields on the common variant (aoeRadius != null, blastRadius != null) — archetypes without those fields render fewer rows.
  • Archetype grouping is computed once at module load by getArchetypeGroups(), not on each render.
  • Camera-relative spawn placement: spawnEnemy calls getCameraTransform() first and silently no-ops when the camera or mission handle is unavailable.
  • The LAST SPAWNED <select> stops keydown propagation (both React synthetic and native stopImmediatePropagation) to prevent global hotkeys from firing while the dropdown has focus.
  • PUSH is async and returns a boolean; the catch silently swallows errors and reports false so the PushButton can drive its own success/error UI without throwing.
  • Inline styles only — no class names — keeping the tab self-contained and themable via BTN_STYLE/LABEL_STYLE constants from PlaygroundShared.