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-sideENEMY BASE STATSpanel.selectedEnemy— type id of the enemy whose preview is shown on LEFT (defaults toENEMY_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.archetypeDrafts—Record<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_GROUPSandARCHETYPE_NAMES, computed once fromENEMY_TYPESviagetArchetypeGroups().
READS FROM
../../data/enemies—ENEMY_TYPE_MAP,ENEMY_TYPES, typeEnemyTypeDef../PlaygroundShared—Panel,Section,StatRow,PushButton,BTN_STYLE,LABEL_STYLE, and typeTabProps.TabProps.missionRef— used to callgetCameraTransform,spawnEnemyAt, andclearEnemieson 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 whenaoeRadius != null; Blast/Fuse block whenblastRadius != 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 theCLEAR ALLbutton.../../services/playgroundPush#pushStats— invoked bypushArchetypewith{ target: 'enemy-archetype', id: selectedArchetype, patch }. Onr.ok === truethe draft for that archetype is removed fromarchetypeDrafts. Returnsfalseon 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 nocommonVariantOfArchfor 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 oneSectionper archetype (uppercased name, collapsible viaarchOpen), each with one+1 <rarity-3-letter>button per variant tinted byt.tint; plus a redCLEAR ALLbutton. - LEFT panel:
LAST SPAWNED(always open) with a<select>of all enemy types showingname (rarity)and a stat readout:HP,Speed,XP,Archetype,Radius(one decimal). - RIGHT panel:
ENEMY BASE STATS(collapsible viabaseOpen) with anextraslot renderingPushButton(onClick=pushArchetype), a helper label, archetype picker buttons (yellow#fbbf24outline + tinted background when selected), a baseline header (Baseline = <archetype> (common variant)tinted bycommonVariantOfArch.tint), andStatRowcontrols forHP/Speed/Radius/XP/Orbit Radius, plus optionalAoE Radius/AoE Cooldown/AoE DamageandBlast Radius/Fuse Timeblocks.
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
sideprop, sharing alluseState/useMemo/useCallbackhooks 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:
spawnEnemycallsgetCameraTransform()first and silently no-ops when the camera or mission handle is unavailable. - The LAST SPAWNED
<select>stopskeydownpropagation (both React synthetic and nativestopImmediatePropagation) 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
falseso thePushButtoncan 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_STYLEconstants fromPlaygroundShared.