PURPOSE

Playground tab that drives the in-engine scenario-runner harness. Hosts three modes — Weapon Gauntlet, Artifact Matrix, and AABB — exposing buttons that kick off batched live-game test runs and renders their progress and result tables. Default mode on mount is the Weapon Gauntlet.

OWNS

  • Default export TestRunnerTab({ missionRef }) — the tab’s root component.
  • Local component state: runnerState (AABB), matrixState (full matrix), weaponGauntletState (weapon gauntlet), selectedArtifacts (Set<string> of artifact IDs), viewMode ('aabb' | 'matrix' | 'weapons').
  • Click handlers: runWeaponGauntlet, toggleArtifact, selectAll, selectNone, runABAll, runABSelected, runTierSweep, runFullMatrix.
  • Derived flag isRunning (true while runner status is 'running' for either AABB or matrix).
  • Private subcomponents WeaponGauntletPanel, WeaponMatrixGrid, MatrixGrid, MatrixRow.
  • Pure helpers deltaPctColor(pct, triggered), fmtPct(pct, triggered), and the SCENARIO_LABELS record.
  • Module constants SPEED_MULT = 4, WEAPON_GAUNTLET_LEVELS = [1, 5, 10, 15, 20], WEAPON_GAUNTLET_SPEED = 8, WEAPON_GAUNTLET_DURATION = 20.

READS FROM

  • ./PlaygroundSharedBTN_STYLE for all button styling.
  • ../../data/artifactsARTIFACT_DEFS (id, name, icon) for the selector grid and select-all action.
  • ../../testing/scenario-runnerscenarioRunner singleton, plus types WeaponGauntletState and WeaponGauntletResult.
  • ../../testing/scenario-typesRunnerState, MatrixState, ScenarioType, ArtifactMatrixResult.
  • ../../testing/artifact-scenariosSCENARIO_TEMPLATE_LIST (imported; not referenced at render time in this file).
  • Prop missionRef: React.RefObject<any> — the live mission ref piped into the runner on mount.

PUSHES TO

  • scenarioRunner.setMissionRef(missionRef) — hands the runner the live mission handle.
  • scenarioRunner.onUpdate(setRunnerState) — subscribes to AABB progress and results.
  • scenarioRunner.onMatrixUpdate(setMatrixState) — subscribes to matrix progress and results.
  • scenarioRunner.onWeaponGauntletUpdate(setWeaponGauntletState) — subscribes to weapon gauntlet progress and results.
  • scenarioRunner.runWeaponGauntlet(WEAPON_GAUNTLET_SPEED, WEAPON_GAUNTLET_DURATION) from the WEAPONS panel.
  • scenarioRunner.runABSuite(SPEED_MULT) from ▶ AABB ALL.
  • scenarioRunner.runABSelected([...selectedArtifacts], SPEED_MULT) from ▶ AABB SELECTED.
  • scenarioRunner.runTierSweep(id, SPEED_MULT) from ▶ TIER SWEEP (exactly one artifact must be selected).
  • scenarioRunner.runFullMatrix(SPEED_MULT) from ▶ FULL MATRIX.
  • scenarioRunner.cancel() from every mode’s ✕ CANCEL button while a run is in flight.

DOES NOT

  • Does not own scenario logic, simulation stepping, weapon definitions, or artifact effects — only the runner orchestrates those.
  • Does not persist results; everything lives in component state and disappears with the tab unmount.
  • Does not write to telemetry, Supabase, Sentry, or any cloud surface.
  • Does not mutate selectedArtifacts after a run completes; selection is independent of run lifecycle.
  • Does not validate that missionRef.current exists; passes the ref through to the runner unconditionally.
  • Does not gate buttons on each other across modes — switching tabs does not cancel an in-flight run.

Signals

  • runnerState.status === 'running' and matrixState?.status === 'running' drive the shared isRunning lockout for cross-mode buttons.
  • weaponGauntletState?.status === 'running' additionally disables the weapon gauntlet button via the isRunning prop passed to WeaponGauntletPanel.
  • Tier sweep button is enabled only when selectedArtifacts.size === 1.
  • AABB selected button is enabled only when selectedArtifacts.size > 0.
  • Matrix progress block reads matrixState.progress.{currentArtifact, currentTier, currentScenario, phase, current, total}.
  • AABB progress block reads runnerState.{currentScenarioId, currentRun, currentStep, totalSteps}.
  • Weapon panel completion banner is gated on state?.status === 'complete', showing state.results.length and state.wallTimeMs / 1000.
  • Result row color in AABB list: red when r.reason includes 'BROKEN', orange when it includes 'NOISY', green on r.status === 'pass', otherwise red.
  • Matrix scaling-column color: red when avgScaling > 3, yellow when > 2, otherwise green.

Entry points

  • Imported and rendered by the Playground screen as one of the playground tabs (consumes missionRef from that parent).
  • Side-effect subscriptions registered once via useEffect([missionRef]) — re-subscribes if the parent ever swaps the ref.

Pattern notes

  • Three orthogonal runner streams (AABB / matrix / weapon gauntlet) are kept in three independent useState slots, each fed by its own onXxxUpdate callback. The runner is treated as a singleton observable.
  • All run-triggering handlers are async but discard the resolved value; result rendering is driven entirely by the subscription callbacks, not by awaiting the promise.
  • Mode switching is purely visual — the conditional render blocks share no DOM and reset no state when toggled.
  • The artifact selector grid is hidden in 'weapons' mode (viewMode !== 'weapons') because it is meaningless without an AABB or matrix run target.
  • deltaPctColor thresholds (gray / green / yellow / orange / red at 0.20 / 0.05 / 0.0 / negative) and WeaponMatrixGrid DPS thresholds (red / orange / yellow / green / purple at 5% / 15% / 40% / 75% / top) are inline magic numbers — not pulled from a constants module.
  • WeaponMatrixGrid preserves weapon insertion order via Map iteration, matching the order the runner emits results in.
  • MatrixRow computes avgScaling per artifact as the mean of scalingFactor across the three scenario sweeps, defaulting missing sweeps to 1.
  • SCENARIO_TEMPLATE_LIST is imported but unused at render time; it is kept in the import graph alongside the other scenario-types imports.
  • Numeric formatting helper fmt inside WeaponMatrixGrid collapses values >= 1000 to 1.2k-style strings; otherwise rounds to integer.
  • Tooltip on each weapon cell carries the full breakdown (DPS, kills, total damage, durationSec) so the visual grid stays compact.