PURPOSE
Playground tab that exposes the run-level modifier (upgrade) system in two modes selected by the side prop. The instance (LEFT) side renders 14 modifier rows with +/- buttons to grant in-run upgrade levels plus RESET ALL / MAX ALL shortcuts. The base (RIGHT) side renders a modifier picker grid above an editable panel for the selected modifier’s metadata (name, description, icon) and per-effect base/k numeric fields, with a PUSH button that writes back to modifiers.ts.
OWNS
- React component
UpgradesTab(default export) implementing theTabPropsinterface fromPlaygroundShared. - Local helper component
ModifierPanelBodythat hosts the edit form for oneModifierTypeDef. - Local hook
useModifierPanelthat wires draft state, dirty tracking, reset, and the asyncpushcallback for a modifier. - Local helpers
toDraft(clones aModifierTypeDefinto an editableModifierDraft) andreadCurrentLevels(snapshotsgame.upgradeCountsfor all known modifiers). - Local constants
MIN_LEVEL(0) andMAX_LEVEL(20) bounding the per-modifier grant range. - Local UI state:
grantOpen,baseOpen(collapsible panel toggles), a tick counter used to force re-renders after mutatinggame.upgradeCounts, andselectedModfor the base-side picker.
READS FROM
MODIFIER_TYPESandMODIFIER_TYPE_MAPfrom../../data/modifiers— the canonical modifier catalog and id lookup.- Types
ModifierTypeDefandModifierEffectfrom../../data/modifiers. game.upgradeCountsfrom../../engine/core/state— the live in-run upgrade level dictionary.- Shared playground primitives from
./PlaygroundShared:Panel,BTN_STYLE,LABEL_STYLE,EditableTextRow,EditableNumberRow,PushButton,useDraft, plus theTabPropstype.
PUSHES TO
- Mutates
game.upgradeCounts[modId]directly to grant, reset, or max upgrade levels for the current run. Initializesgame.upgradeCountsto{}if absent before writing. - Invokes
pushStatsfrom../../services/playgroundPushwithtarget: 'modifiers', the modifierid, and apatchcontaining{ name, description, icon, effects }to persist edits tomodifiers.ts.
DOES NOT
- Does not modify the modifier catalog (
MODIFIER_TYPES) in memory; only the source file viapushStats. - Does not recompute or apply modifier effects on the ship — it only sets the level counts; effect application lives elsewhere in the upgrade pipeline.
- Does not touch the
missionRef(received as_missionRef) or restart the mission. - Does not persist the granted in-run levels across reloads;
upgradeCountsis run-scoped state. - Does not validate effect
statormodestrings; onlybaseandkare editable numerically, andstat/modeare shown read-only. - Does not clamp pushed
base/kvalues; clamping only applies to per-modifier level input viaMIN_LEVEL/MAX_LEVEL.
Signals
useStatetick (setTick) drives a manualrerendercallback after each mutation ofgame.upgradeCounts, since the store is mutated outside React.useDraft(fromPlaygroundShared) suppliesdraft,setDraft,reset, andisDirtyfor the base-edit form; aRESETbutton is conditionally rendered whenisDirtyis true.PushButtonreceives the asyncpushcallback; success reflects the boolean returned bypushStats(...).ok. Exceptions are swallowed and reported asfalse.useMemorecomputes theoriginaldraft snapshot whenever the selectedModifierTypeDefinstance changes.ModifierPanelBodyis keyed bym.idso switching the selected modifier remounts the panel and resets draft state.
Entry points
- Default export
UpgradesTabis registered by the playground shell and rendered withside: 'instance'for the left column andside: 'base'for the right column. - Selecting a modifier button in the base-side picker updates
selectedMod, which drives theMODIFIER_TYPE_MAP[selectedMod]lookup and the renderedModifierPanelBody.
Pattern notes
- Split-panel pattern: a single component returns different JSX depending on the
sideprop, sharing state declarations between branches but only one branch’s UI at a time. - Direct mutation of
game.upgradeCountswith a tick-bump rerender is the established playground convention for editing engine state from React. - The
instancebranch reads levels viareadCurrentLevels()each render so writes togame.upgradeCountsmade elsewhere also reflect. - Editable rows in the base panel compare
valueagainstsavedValueto highlight dirtiness; the dirty state is owned byuseDraft, not by row-level diffing. - The push payload is restricted to the four editable fields (
name,description,icon,effects);idand any otherModifierTypeDeffields are not sent. +and-buttons are disabled at the bounds (level <= MIN_LEVEL/level >= MAX_LEVEL) and dim their opacity for visual feedback.- Modifier-row icons render as text glyphs prefixed to the name; no image assets are loaded here.