boss-gauntlet-runner

PURPOSE

Dev-only harness that drives the live sandbox MissionHandle through every entry in BOSS_GAUNTLET_ORDER (or a single boss) under one of four modes (smoke / balance / survival / stress). Reuses the existing mission + engine rAF render loop, calling sandboxResetForTest() and sandboxSpawnBoss() between bosses for a clean slate. Injects scripted DPS directly on the boss anchor’s hp field (bypassing weapons) so the kill rate is deterministic and player loadout doesn’t contaminate the test. Samples telemetry on a frame-stride into per-boss BossGauntletReports and emits live progress to a registered listener.

OWNS

  • Module-level singletons: _missionRef, _cancelled, _onProgress, _lastReport.
  • The runner’s wall-clock pacing loop (while (true) { … await _wait(16); } in _runOneBoss).
  • Per-boss scripted-damage application (_applyScriptedDamage) — writes directly to enemy.hp on the first alive isBoss && !_dying enemy.
  • Telemetry frame construction (BossTelemetryFrame) — built from world / ship / game state plus getBossVfxPassMs() / bossVfxLayerCount().
  • Pass/fail decision logic (_decideOutcome) — mode-specific, plus universal NaN / exception gates.
  • Empty-report seeding (_emptyReport) so the UI can render 'queued' rows before any boss starts.
  • window.__bossGauntlet console API (run, runSingle, getReport, cancel).

READS FROM

  • BOSS_DEFS (../data/bosses) — boss def lookup; displayName copied into report.
  • BOSS_GAUNTLET_MODE_CONFIGS, BOSS_GAUNTLET_ORDER, PROJECTILE_PERF_GATE, TELEMETRY_SAMPLE_EVERY_N_FRAMES (./boss-gauntlet-types) — all tuning lives there, not here.
  • game.time, game.stats.damageTaken (../engine/core/state) — baseline + elapsed.
  • ship.weapons, ship.hp, ship.hpMax, ship.shieldMax, ship.alive, ship.invulnerable, ship.invulnTimer plus dev-only flags ship._gauntletPlayerDamageDisabled, ship._dontStuckInvuln, ship._invulnWallTime.
  • world.enemies (alive scan for boss anchor, bar HP rollup, leaked-sharing check), world.enemyBullets, world.playerBullets.
  • getBossVfxPassMs() and bossVfxLayerCount() (../engine/vfx/boss-layers) — perf samples.
  • Sig (../engine/core/signals) — subscribes to 'boss_kill' per run with priority 50.

PUSHES TO

  • MissionHandle API on the active sandbox mission ref: sandboxResetForTest(), sandboxSpawnBoss(defId, 0), setGodMode(), setWorldKnobs({ enemyDamageMult }), patchShipStats({ hpMax, shieldMax }), fullHeal().
  • Mutates ship.weapons (stash + restore), ship._gauntletPlayerDamageDisabled, ship._dontStuckInvuln, ship._invulnWallTime, ship.invulnerable, ship.invulnTimer on teardown.
  • Direct enemy.hp writes on the boss anchor (and sets _dying when hp hits 0); does NOT route through the weapon / damage pipeline.
  • Listener callback registered via setProgressListener — fires on every status change and every telemetry-sampled frame.
  • window.__bossGauntlet (browser global) — exposes run, runSingle, getReport, cancel for console use.

DOES NOT

  • Does NOT create a fresh Mission per boss — reuses the existing sandbox mission via _missionRef.
  • Does NOT pump frames manually — wall-clock-paced via await _wait(16); the engine’s main rAF loop keeps rendering.
  • Does NOT use the player’s weapons against the boss — ship.weapons is cleared and _gauntletPlayerDamageDisabled is set so artifact/echo damage is also suppressed.
  • Does NOT route scripted damage through the bullet / weapon system — direct hp write on the boss anchor.
  • Does NOT define mode tuning — all numeric thresholds (timeoutSec, playerDpsToBoss, godMode, playerLevel, enemyDamageMult) live in BOSS_GAUNTLET_MODE_CONFIGS.
  • Does NOT compute true ability fires — abilitiesFiredCount is a rising-edge heuristic on world.enemyBullets.length.
  • Does NOT persist reports — _lastReport is in-memory only; consumer is responsible for surfacing it.

Signals

  • Subscribes (per run): Sig.on('boss_kill', killHandler, 50) — handler flips a closure-local bossKilled and breaks the wait loop. Unsubscribed in the finally block.

Entry points

  • setMissionRef(ref) — caller (Ship Playground / dev UI) injects the live { current: MissionHandle | null } ref. Required before any run.
  • setProgressListener(cb | null) — registers a UI listener for BossGauntletProgress events.
  • getLastReport(): BossGauntletRunReport | null — read-only accessor for the last completed run.
  • cancel(): void — flips _cancelled; the per-boss loop checks each tick and marks remaining bosses 'skipped'.
  • runGauntlet(mode): Promise<BossGauntletRunReport> — full BOSS_GAUNTLET_ORDER sequence.
  • runSingleBoss(bossId, mode): Promise<BossGauntletRunReport> — single-boss variant; same code path via _runSequence([bossId], mode).
  • window.__bossGauntlet.{run,runSingle,getReport,cancel} — browser console mirror of the same surface.

Pattern notes

  • Mission reuse, sandbox reset. Per-boss isolation is achieved by calling sandboxResetForTest() before spawn and again after the wait loop exits. The post-run reset is best-effort (wrapped in try {} catch {}) so a teardown failure can’t break the whole sequence.
  • Stuck-invuln watchdog bypass ordering matters. ship._dontStuckInvuln = cfg.godMode and ship._invulnWallTime = 0 are set BEFORE _configurePlayerForMode calls setGodMode(true). Skipping this seeds _invulnWallTime while the watchdog is live and risks a stale warning on later runs.
  • Weapon stash + belt-and-suspenders flag. ship.weapons.slice() is held in a local, weapons array is emptied, and _gauntletPlayerDamageDisabled = true is set as a redundant gate because clearing weapons alone doesn’t suppress artifact-tagged damage (Personal Space, Echo Generator). All three are restored in the post-run block AND on the early-exit spawn-error path.
  • Rising-edge heuristic for abilities. abilitiesEstimate increments whenever world.enemyBullets.length is greater than the prior sample. Coarse — meant for smoke-mode liveness, not balance accounting.
  • Bar HP rollup. _readBarHp() sums hp / hpMax across all alive enemies with isBoss || sharesHealthWithBoss; sharingCount > 0 is the proof-of-spawn signal (bossSpawned).
  • Mode-specific pass/fail in one switch. _decideOutcome runs after universal gates (exceptions, NaN bar HP) and then branches per mode. smoke expects the boss to survive the timeout (it’s deliberately under-damaged); balance / survival require a kill; stress only requires no breakage.
  • Live-report mutation pattern. liveReports[index] = { ...report } shallow-clones into the array each sample so the UI sees an updated snapshot via _emit without external consumers needing to subscribe to the underlying report.
  • pass field patched on emit. _emit rewrites r.pass = r.status === 'passed' on every emit so listeners don’t need to recompute. Final canonical status remains report.status.

EXTRACT-CANDIDATE

  • Stuck-invuln watchdog bypass protocol (_dontStuckInvuln = true; _invulnWallTime = 0; before setGodMode(true)) is duplicated between the pre-mission setup block and _configurePlayerForMode. Anywhere else in the codebase that toggles long-form invuln (god-mode console toggles, cinematic invuln) likely needs the same dance — worth a shared helper in engine/core/state or wherever god-mode is centrally toggled.
  • Player-damage-suppression bundle (clear ship.weapons, set _gauntletPlayerDamageDisabled, set _dontStuckInvuln) and its inverse teardown is a self-contained “dev-only no-damage player” mode. If any other harness (replay viewer, cinematic, screenshot tooling) needs the same, extract enterPlayerDamageDisabled() / exitPlayerDamageDisabled(stashed) to testing/player-damage-disabled.ts.
  • Scripted-damage-on-anchor primitive (_applyScriptedDamage) is generic — any future test harness that wants deterministic kill rates without weapons can reuse it. Candidate to live alongside sandboxSpawnBoss in the bridge as sandboxApplyScriptedBossDamage(amount).