PURPOSE
Playground tab that exposes both a per-run loadout swap (LEFT/instance) and an authoring surface for weapon base stats and scaling curves (RIGHT/base). The side prop selects which half renders; both render inside a collapsible Panel. Instance side hot-swaps the live ship loadout for the current run only. Base side edits the canonical WeaponCoreSpec values and pushes them through pushStats to rewrite the source data tables.
OWNS
- Local equip-slot state: a four-entry
SlotState[](each{ weaponId, level }), seeded withrifle@1and threeEMPTY_SLOTs. - Per-side open/closed state (
instanceOpen,baseOpen). - Picker state on the base side:
selectedWeapon(defaults toWEAPON_ORDER[0]) andpreviewLevel(1..20). - Pending override map
baseOverrides: Record<weaponId, Record<dottedKey, number>>capturing unsaved slider edits keyed by"damage.base","fireRate.scaling", or a flat legendary key. - The two top-level callbacks:
applyWeapons(instance APPLY) andpushWeaponStats(base PUSH). - Inline
WeaponSlotEditorsubcomponent and the helpersweaponStatValue/weaponStatScaling. - Static authoring metadata:
SCALING_CURVES,BASE_STATS,LEGENDARY_KNOBS.
READS FROM
../../data/weapons—WEAPON_MAP,WEAPON_ORDER,getWeaponStatAtLevel,resolveWeaponRarity, and the typesWeaponCoreSpec/ScalingCurve../PlaygroundShared—Panel,StatRow,PushButton,BTN_STYLE,LABEL_STYLE, and the sharedTabPropstype.WEAPON_MAP[selectedWeapon]for preview values, family/curve display, damage tags, behavior, legendary flags, merge parents, and cadence pattern (read via(w as any)to reach optional fields).getWeaponStatAtLevelfor the previewed damage / fire rate / range atpreviewLevel.
PUSHES TO
missionRef.current?.setWeapons(weapons)— bridge call that swaps the live loadout. Only fires whenweapons.length > 0; empty slots are filtered out.pushStats({ target: 'weapon', id: selectedWeapon, patch })from../../services/playgroundPush, wherepatchis the flat override map translated into{ path: string[], value }entries (dotted keys split into nested paths for the source rewriter).- On a successful push (
r.ok), clearsbaseOverrides[selectedWeapon]for that weapon.
DOES NOT
- Does not persist instance state across renders, sessions, or runs — slot edits live in component state only.
- Does not mutate
WEAPON_MAP,WEAPON_ORDER, orWeaponCoreSpecrecords directly; all writes go throughpushStats. - Does not write to the player’s saved profile, account, or any Zustand store.
- Does not bake the previewed level into the live loadout —
previewLevelis base-side only and never flows into APPLY. - Does not validate weapon IDs beyond the
<select>options sourced fromWEAPON_ORDER. - Does not interact with the engine loop, rendering, or input pipelines outside the
missionRefbridge. - Does not edit the active scaling curve — the curve buttons on the base side render the current selection but have no
onClickwired up. - Does not surface the
LEGENDARY_KNOBSorBASE_STATSrows for fields the selected weapon does not define; both lists filter to the actually-present keys. - Does not handle async errors verbosely —
pushWeaponStatsswallows thrown errors and returnsfalse.
Signals
- APPLY click (instance side,
applyWeapons): collects non-empty slots into{ id, level }[]and callsmissionRef.current?.setWeapons(...). The click handler also callse.stopPropagation()so the Panel header does not toggle. - PUSH click (base side, via
<PushButton onClick={pushWeaponStats} />): translates pending overrides forselectedWeaponinto nested-path patches and awaitspushStats; clears overrides for that weapon on success. <select>and<input type="number">onChangehandlers update slot weapon/level; level is clamped[1, 20]andNaNis coerced to1.- All key events on weapon-picker
<select>, slot<select>, and slot level<input>are explicitly stopped withe.stopPropagation()pluse.nativeEvent.stopImmediatePropagation()so playground hotkeys do not steal them. - Base-stat
StatRowonChangewrites intobaseOverrides[selectedWeapon][{sub}];onResetdeletes that key. - Legendary-knob
StatRowonChangewrites flatbaseOverrides[selectedWeapon][key];onResetdeletes that key. - Preview-level range input updates
previewLevel, which re-computes the displayed damage / fire rate / range viagetWeaponStatAtLevel.
Entry points
- Default export
WeaponsTab(props: TabProps)— rendered twice by the playground shell, once withside="instance"and once withside="base"(default"instance"per the destructured default). WeaponSlotEditor({ slot, index, onChange })— internal subcomponent used by the instance branch only.
Pattern notes
- Single-component, dual-mode rendering keyed on
side: the instance branch returns early; the base branch is the default return. - Stats are read uniformly via the
{ base, scaling }object shape; legendary knobs are read as flatnumberfields, andBASE_STATS/LEGENDARY_KNOBSrows are filtered to only those a weapon actually defines. - Dotted override keys (
"damage.base","fireRate.scaling") are flattened during editing and split into nested paths only at push time, matching the source rewriter’s expected patch shape. - Optional
WeaponCoreSpecfields (scalingCurve,damageTag,secondaryDamageTag,behavior,isLegendary,mergeParents,cadencePattern,cadenceStepSec, legendary knobs) are accessed through(w as any)to avoid widening the public type. - Number inputs use
stepper slider, clamp client-side, and the preview values display rounded damage / range and atoFixed(2)fire rate. - The scaling-curve button row is presentational only — selection state visually reflects
(w as any).scalingCurve ?? 'linear', but the buttons have no click handler. - Panel
extraslot is used for the per-side action button (APPLY on instance,<PushButton>on base) and click events stop propagation so the panel does not collapse.