Files
healer-man/docs/BALANCE_LAB.md
T
2026-09-20 21:50:50 -04:00

642 lines
33 KiB
Markdown

# Boss and party balance lab
`scripts/balance_lab.py` runs a chosen World of Warcraft, Conquest of Azeroth,
or Runes of Magic five-player party against the authored boss catalog. It is an
interactive Python controller and report generator, not a second combat model.
The actual fights execute through `scripts/balance-lab/runtime.ts`, which imports
the game's live TypeScript combat store, party AI, abilities, talents, gear,
threat, mitigation, healing, boss scaling, and enemy attacks.
This split is intentional. Balance formulas stay in the game runtime. Python
owns selection, repeatable run configuration, aggregation, CSV/Markdown/HTML
output, and outlier flags. Do not copy combat coefficients into the Python
script.
## Quick start
Install the normal project dependencies, then start the guided prompt:
```powershell
npm run balance:lab
```
The prompt selects:
1. one random party, five manual members, or an automatic multi-party sweep;
2. a WoW, CoA, or RoM party catalog; automatic mode only uses entries marked
Ready (plus Beta when explicitly enabled);
3. a level from 1 through 80;
4. random Ready parties, guaranteed coverage of all Ready class/spec builds,
or a searchable targeted selection that may include Needs Testing builds;
5. how many unique 1-tank/1-healer/3-damage configurations to generate per
source when random selection is used;
6. all bosses, WoW bosses, RoM bosses, or a searchable multi-boss selection;
7. how many deterministic simulations to run for every party/boss pairing; and
8. the maximum duration of each fight.
Automatic class/spec selection has three strategies:
- **Random parties** preserves the original seeded behavior and generates up
to the requested number of unique parties per source. If a Ready/Beta pool
can form fewer unique 1/1/3 rosters, Balance Lab runs each available roster
once and reports the cap instead of failing or duplicating identical data.
- **All Ready specs** creates the minimum deterministic set of valid parties
needed to include every Ready build for the selected source at least once.
At present this is the five certified WoW specs; CoA and RoM have no Ready
entries and therefore do not participate in automatic runs.
- **Specific specs** treats each selected build as required coverage and fills
the remaining tank, healer, and damage slots with distinct compatible builds.
When alternatives exist, required builds are not reused as fillers.
Coverage parties always contain one tank, one healer, and three different
damage builds. Their composition is deterministic for the party seed, and no
build is duplicated inside one party.
The active member is driven by the shared benchmark policy while the other four
use the live companion AI. The healer is the default active member. Use
`--active-slot` to change it. Use `--seat-sweep` to run the same composition
five times with each seat active exactly once. Seat sweeps produce dedicated
`npc_class_specs_across_parties.csv` and
`npc_class_abilities_across_parties.csv` artifacts, so companion comparisons
exclude the active benchmark controller.
Classic specializations expose two named, non-destructive talent profiles: the
default PvE build translated from the pinned AzerothCore Playerbots
configuration at commit `531282e`, and the prior Healer Man executable-first
build. Each profile has a stable generated character name and unique build ID.
Adding another profile therefore does not overwrite either existing build.
Random parties and unselected filler seats use the declared default profile;
named alternatives remain available to specific selection and full coverage.
Run `npm run talents:generate` after intentionally changing the Playerbots
mapping or source; the checked-in snapshot keeps production independent from
the historical checkout. Runtime selection mirrors PlayerbotFactory: it starts
with the closest configured link at or below the tested level (or the first
future link at low levels), then consumes later links if talent points remain;
level 80 starts from the level-80 link. Profile metadata records the applicable
source point count, ranks retained after mapping to the imported Healer Man
talent catalog, and legal fallback points at the tested level. This makes
patched-tree mismatch visible instead of presenting the translation as a
literal Blizzard build.
## GM app and server execution
A server-authorized GM sees **Balance Lab** directly underneath **GM Tools** on
the main menu. That screen exposes the automatic sweep controls: party source,
a single level or an all-brackets schedule, class/spec selection strategy, a
searchable spec picker, simulations per party/boss, duration, random seed, boss
category, and a searchable multi-boss picker. The bracket schedules cover level
1 plus every 10 levels, level 1 plus every 5 levels, or every level from 1
through 80. Random mode exposes the requested parties per source; coverage
modes show the derived minimum party count before submission.
The optional **NPC seat audit** multiplies each generated composition across all
five active-seat assignments and makes in-app class/ability analysis prefer the
NPC-only report tables.
**Run on server** sends one allow-listed configuration to the authenticated GM
API. **Run all level brackets** submits the complete schedule atomically and
creates one independently persisted job and report per level. The bounded worker
pool runs several jobs at the same time, launches the same Python controller and
live TypeScript runtime used by the command line, and writes artifacts under:
```text
DATA_DIR/balance-lab/jobs/<job-id>/
```
The browser polls lightweight status records; it does not execute combat ticks.
Closing the browser does not stop a running server job. Completed jobs and their
metadata remain in **Run history** across server restarts, with the newest run
selected automatically. The history shows the most recent 100 jobs and can be
searched or filtered by status. A job that was still queued or running when the
server stopped is restored as failed (it cannot safely resume a half-finished
deterministic sweep) and can be rerun.
Each job directory contains a private `job.json` status record alongside its
report artifacts. On startup, `server/balance-lab-jobs.mjs` rebuilds history from
those records and treats a directory containing `suite-report.json` as complete.
Completed server-job directories from the pre-history implementation are
imported from `suite-report.json` plus `suite-config.json`, then receive a new
`job.json`; their requester is shown as `unknown (legacy run)` because that was
not previously recorded.
Do not add `job.json` to the artifact allow-list: it is server state, not a GM
download. The standalone `npm run balance:lab` path remains the way to spend the
local development device's CPU.
## In-app analysis and previous runs
Select any completed entry in **Run history**, then choose **View graphs and
filterable data**. The app reads that run's `suite-report.json` through the
authenticated artifact endpoint and caches it for the current screen session.
The dashboard includes:
- boss damage to the whole party (incoming DTPS);
- party damage applied directly to each boss;
- average successful kill time;
- boss DPS and damage taken for every class/specialization;
- effective healing done and effective healing received per second for every
class/specialization;
- complete boss tables with pressure versus median, total incoming damage,
defeat/wipe rates, deaths, and health;
- complete class/spec tables with total and boss DPS, healing done/received per
second, DTPS, absolute damage taken, healing done/received and absorb totals,
deaths, source/role-median tuning flags, and a direct ability-damage
drill-down for every build; and
- a complete Abilities tab with learned/available abilities (including zero-use
rows), exact runtime IDs, AI categories, active-run coverage, casts, hits,
casts per minute, effective/raw damage, DPS, damage share, damage per hit,
healing, overheal, absorbs, and cooldown; and
- complete party-composition tables with output, incoming pressure, outcomes,
and deaths.
The overview graphs intentionally show at most the 15 highest visible values so
labels remain readable. They never discard report data: the Bosses, Class/spec,
and Parties tabs retain every matching row. Search, party-source, boss-source,
role, metric, and sort-direction controls filter the display without modifying
the saved report. **View ability damage** on a class/spec row opens the
Abilities tab with that exact build selected, damaging abilities ranked by
effective damage, and its casts, hits, DPS, damage share, and damage-per-hit
details visible in the table. **Show all specs** clears that drill-down.
Server requirements:
- the normal checkout and development dependencies must be present because the
combat bridge uses `vite-node`;
- Python 3 must be installed, or `BALANCE_LAB_PYTHON` must name its executable;
and
- the signed-in account must have the server-issued `gm` role.
The GM screen calls `/api/gm/balance-lab/runtime` when it opens and shows the
resolved Python version. If Python 3 is unavailable, **Run on server** is
disabled and no job is queued. On the canonical TrueNAS deployment, the service
command installs `python3`, prints `python3 --version` during startup, and sets
`BALANCE_LAB_PYTHON=python3`; use the exact YAML in `DEPLOYMENT.md` rather than
the older Node-only command.
`BALANCE_LAB_MAX_CONCURRENT` defaults to four and is capped at 16.
`BALANCE_LAB_MAX_QUEUED` defaults to 100 and is capped at 200, which leaves room
for an 80-job every-level sweep. Raising concurrency permits more parallel
server processes but can make both the game server and simulations less
responsive. A batch contains at most 80 jobs and is validated against the
remaining queue capacity before any of its jobs are created. Subprocess
arguments are passed as an array without a shell, artifact names are
allow-listed, and every job and artifact endpoint authenticates the account's
server-issued GM role.
Each Python worker starts the TypeScript bridge with `vite-node --mode
balance-lab`. That dedicated mode disables Vite's public directory and ignores
all filesystem-watch paths because a headless, one-shot simulation does not
need hot reload. Keep this isolation in place when changing `vite.config.ts`:
parallel workers must not consume one watcher per shipped asset. If an older
deployment exhausts the host watcher limit, the job now reports that specific
startup failure instead of only the subprocess exit code; deploy the no-watch
runtime fix rather than increasing the host's watcher allowance.
## Local graphical runs
On Windows, double-click `scripts/Launch Balance Lab GUI.cmd`. The launcher
starts the local Balance Lab service, opens the existing Balance Lab screen in
the default browser, and keeps the worker service alive in its command window.
Leave that window open until queued simulations finish. The equivalent terminal
command is:
```powershell
npm run balance:gui
```
This development-only mode binds Vite to `127.0.0.1`, accepts API requests only
from the loopback interface, and does not require an online account or GM login.
It uses the same Python controller, TypeScript combat runtime, configuration
form, progress UI, report dashboard, and artifact downloads as the server GM
tool. Job history and reports persist beneath
`artifacts/balance-lab-gui-runtime`; closing the browser does not stop work, but
closing the launcher does. Production and ordinary development modes do not
expose these unauthenticated local routes.
Use **All brackets** in the GUI for every-10-level, every-5-level, or every-level
runs. Use **Run locally** for one level. The runtime status at the top confirms
that Python is available before a run is submitted.
## Repeatable command-line runs
Run the same every-10-level schedule used by the GM screen entirely on the
local development machine:
```powershell
npm run balance:lab:brackets
```
The bracket runner launches the existing Python controller and live TypeScript
runtime; it does not contain a second combat implementation. By default it
tests every WoW specialization at levels 1, 10, 20, and so on through 80, uses
three seeds, allows 80 seconds per fight, and runs up to four level processes
concurrently. Normal Balance Lab options can narrow the run:
```powershell
npm run balance:lab:brackets -- `
--levels 40,50,60,70,80 `
--simulations-per-party 5 `
--boss "stormwind-stockade/entry-1717" `
--boss "stormwind-stockade/entry-1663"
```
Use `--step 5` or `--step 1` for the other GM schedules, `--jobs 1` when the
machine should remain responsive, and `--output-dir <path>` for a stable
comparison location. `npm run balance:lab:regression -- <options>` runs the
same suite but returns a failing exit code when configured pressure or
durability limits are exceeded. The default checks flag boss durability below
20 or above 70 boss-damage seconds, bracket-to-bracket boss-health growth more
than 1.5 times party boss-DPS growth once the preceding bracket reaches that
minimum window, missing incoming damage, and local raw runs where healers are
not required often enough or answer fewer than 60% of five-second pressure
windows.
The bracket root contains `bracket-summary.json`, `.csv`, and `.md`. These show
boss health beside direct boss DPS, incoming DTPS, effective healer HPS,
mitigation, healing coverage, five-second pressure windows answered by healer
throughput, outcomes, and growth relative to the previous tested level.
`heirloom-summary.json` and `.csv` record the resolved item level,
slot count, attributes, ratings, armor, and weapon DPS for every tested build.
Existing reports can be re-analyzed without rerunning combat:
```powershell
python scripts/balance_lab_brackets.py --analyze-existing artifacts/balance-lab-brackets/<run>
```
Balance Lab builds use the `virtual-heirloom` preset. Empty persisted inventory
and equipment assignments mean that no earned drops override the virtual set;
they do not mean that the character is ungeared. Virtual heirlooms fill every
compatible slot with specialization-specific synthetic stats, scale through
level 79, and remain at item level 79 for every party member at level 80. They
are not literal Blizzard heirloom item templates: their custom stat budget is
normalized slightly above the best compatible same-level item in the authored
WotLK-derived loot catalog. The saved catalog and heirloom summary make that
resolved loadout explicit for each local run.
Damage and healer heirloom profiles include both critical-strike rating and
haste rating. The WotLK stat resolver converts those ratings at the simulated
character level into the appropriate melee, ranged, or spell percentages.
Protection tank heirlooms intentionally spend that budget on defense,
avoidance, block, stamina, and expertise instead.
Run selected brackets as a companion-only seat audit:
```powershell
npm run balance:lab:npc-audit -- --levels 20,40,60,80 --simulations-per-party 1
```
NPC raw rows include the named talent build and character, selected rotation
profile/rule/reason counts, profile priority for every available ability,
missing profile actions, GCD uptime, and sampled resource starvation/capping.
Use these diagnostics before changing class coefficients.
## Companion cooldown policy
GM Tools includes a revisioned **Companion cooldown policy** editor. Generic
rules control normal defensive, emergency defensive, healer mitigation, and
major offensive cooldown thresholds. Mechanic rules bind an authored boss
attack to tank-only or party-wide pre-emptive mitigation with configurable lead
time and minimum defensive cooldown length.
Clients load the published policy from `/api/companion-cooldown-policy`; GM
reads/writes use `/api/gm/companion-cooldown-policy`. Server Balance Lab jobs
snapshot the published policy before launch. Local runs use built-in defaults
unless `--cooldown-policy` points to a JSON policy file.
## Companion specialization readiness
The readiness catalog shows every WoW and CoA specialization plus each RoM
class/role profile as Ready, Beta, or Needs Testing. The revisioned GM policy
currently publishes the 31 WoW specialization statuses; all CoA and RoM entries
are fixed at Needs Testing until their certification work begins. Shipped defaults certify only Protection
Warrior/Aldren, Discipline Priest/Quorin, Marksmanship Hunter/Liora, Fire
Mage/Xavian, and Assassination Rogue/Orin. The main menu, Dungeon setup, and
Balance Lab link to the full Companion Readiness catalog. Players can inspect
it; authenticated GMs can edit and atomically publish the complete policy.
Published policies must retain at least one Ready tank, one Ready healer, and
three distinct Ready damage specializations.
Random and all-eligible runs use Ready specs by default across every source.
Because no CoA or RoM entry is currently Ready, those catalogs contribute no
automatic parties. **Include Beta Specs** adds Beta specs and is saved per
device; explicit targeted diagnostics may still select a Needs Testing spec,
but `--ready-only` rejects those targets for certification and regression runs.
Filler seats always come from the eligible Ready/Beta pool.
Server jobs snapshot the published policy and beta choice. Local GUI runs use
the revisioned loopback policy saved under
`artifacts/balance-lab-gui-runtime`; corrupt or missing policy files fall back
to shipped defaults.
CLI parity is available through `--ready-only`, `--include-beta-specs`, and
`--companion-readiness-policy <json>`. The JSON may be the published envelope
or its policy config.
The standard bracket, regression, and NPC-audit npm commands pass
`--ready-only`. For focused code validation use `npm run test:ready-five`; do
not use a full-repository or all-class balance sweep unless the project owner
explicitly expands the certification scope.
List exact party build IDs at a level:
```powershell
python scripts/balance_lab.py --source wow --level 60 --list-members
```
List boss keys:
```powershell
python scripts/balance_lab.py --source wow --level 60 --list-bosses
```
Run a random party against every authored boss:
```powershell
python scripts/balance_lab.py --source coa --level 60 --party-mode random --party-seed 42 --seeds 1,2,3 --boss-category all
```
Generate 25 unique WoW parties and run five simulations per party against all
bosses:
```powershell
python scripts/balance_lab.py `
--source wow `
--level 60 `
--party-mode automatic `
--party-configurations 25 `
--simulations-per-party 5 `
--boss-category all
```
Run every currently Ready source against only RoM bosses. Until a CoA or RoM
build is promoted, `--source all` still produces only the five Ready WoW specs:
```powershell
python scripts/balance_lab.py `
--source all `
--level 60 `
--party-mode automatic `
--party-configurations 10 `
--simulations-per-party 3 `
--boss-category rom
```
Automatically test a selected group of bosses:
```powershell
python scripts/balance_lab.py `
--source coa `
--party-mode automatic `
--party-configurations 20 `
--simulations-per-party 5 `
--boss "gundrak/entry-29932" `
--boss "utgarde-keep/entry-23954"
```
Run an exact party against Eck only:
```powershell
python scripts/balance_lab.py `
--source wow `
--level 40 `
--party-mode manual `
--member "warrior:Protection" `
--member "priest:Holy" `
--member "mage:Fire" `
--member "rogue:Combat" `
--member "hunter:Marksmanship" `
--active-slot 2 `
--boss "gundrak/entry-29932" `
--seeds 1,2,3 `
--duration 60
```
Manual selectors accept an exact generated build ID or a unique
`class-id:specialization`/`class-name:specialization` value. Repeat `--boss` to
run any selected subset. Omit it to run every boss in the selected boss
category. `--seeds 4,8,15` is an advanced exact-seed override for
`--simulations-per-party`.
## Output
Each run creates `artifacts/balance-lab/<timestamp>/` unless `--output-dir` is
provided.
| File | Contents |
| --- | --- |
| `report.html` | Searchable and sortable tables containing every selected member and every boss. |
| `report.md` | Human-readable complete party and boss tables. |
| `report.json` | Aggregated machine-readable report. |
| `raw-runs.json` | Lossless per-seed, per-boss, per-member, and per-ability runtime results. |
| `bosses.csv` | One aggregate row for every boss, ordered by party damage taken per second. |
| `party_members.csv` | One aggregate row for every selected party member. |
| `member_by_boss.csv` | Every party member against every boss. |
| `member_abilities.csv` | Every observed player/companion ability for every run. |
| `boss_abilities.csv` | Every observed boss ability for every run. |
| `runs.csv` | One row per boss and deterministic seed. |
| `config.json` | Exact rerunnable selection. |
| `catalog.json` | Party and boss choices shown to the controller. |
No report is truncated to a top-N list.
Automatic mode creates a complete report directory for every generated party
under `parties/<party-id>/` and adds these suite-level files:
| File | Contents |
| --- | --- |
| `suite-report.html` | Searchable/sortable tables for every tested class/spec, party configuration, and boss. |
| `suite-report.md` / `suite-report.json` | Complete human- and machine-readable suite summaries. |
| `party_configurations.csv` | Every generated party, its full composition, party DPS, DTPS, deaths, and outcomes. |
| `class_specs_across_parties.csv` | Every class/spec observed across every configuration, compared with its source/role median. |
| `class_abilities_across_parties.csv` | Every learned/available and observed class/spec ability, including unused rows, usage frequency, damage contribution, healing, and absorbs. |
| `npc_class_specs_across_parties.csv` | NPC-only class/spec results, excluding each run's active benchmark-controlled seat. |
| `npc_class_abilities_across_parties.csv` | NPC-only ability, rotation-priority, decision-count, and missing-profile-action results. |
| `class_spec_by_boss.csv` | Every observed class/spec broken down against every boss it fought. |
| `bosses_across_parties.csv` | Every boss aggregated across all generated parties. |
| `boss_by_party.csv` | Every boss and party-configuration pairing. |
| `suite_runs.csv` | Every individual seed run with its party configuration ID. |
| `suite-config.json` | The exact automatic-generation seed, counts, filters, and generated party build IDs. |
## Metrics and interpretation
- Boss `incomingDtps` is effective damage received by all five members divided
by active fight time. Damage from summoned hostile actors is included.
- `averageIncomingDamage` is total healer workload and therefore includes fight
length. Read it beside DTPS, wipe rate, deaths, and time to defeat.
- Member `dps` includes damage to the boss and adds. `bossDps` includes only
direct boss damage. Suite class/spec `dtps` is damage taken per active second;
`totalDamageTaken` is the absolute healer/survivability burden across every
included run.
- Member `hps` is effective healing done per active second and excludes
overhealing. `hpsReceived` is effective healing received per active second,
attributed from each healing event's target (including self-heals).
`totalHealing` and `totalHealingReceived` are the corresponding absolute
values across the included runs. Reports created before target attribution
was added remain readable but require a rerun to populate healing received.
- Member and ability `absorbs` count shield capacity granted by both direct
shield effects and named absorb auras such as Power Word: Shield. Member
`deathEvents` counts each alive-to-dead transition, so a member that is
resurrected and defeated again contributes another death.
- Ability `activeRuns` counts simulations with at least one cast, completed
non-throughput release, hit, damage, healing, overheal, or absorb event.
Completed resource, aura, pet, and summon actions are recorded as casts even
though they create no immediate throughput event. `usageRate` divides that by
every run for that build; `damageShare` divides the ability's effective
damage by the class/spec's total effective damage. `available · unused` means
the ability was learned and executable at the tested level/talent setup but
emitted no combat event. This is a review signal, not proof of an AI defect:
resurrection,
execute, interrupt, dispel, emergency, and long-cooldown abilities can be
legitimately unused when their condition never occurs.
- Companion AI currently applies several instant attacks directly and therefore
may emit hits/damage without a separate cast event. Use **Active runs** and
**Hits** as the authoritative usage indicators; use **Casts** and **Casts/min**
as additional detail, especially for the active-player benchmark controller
and non-throughput actions.
- Boss pressure flags in a party report compare DTPS with the non-zero median
boss for that exact party. Suite boss flags compare across all generated
parties. Suite class/spec flags compare DPS with every observed spec from the
same source and role. `high` begins at 1.25 times the median and `very
high` at 1.5; `low` begins at 0.75 and `very low` at 0.5.
- Flags are triage signals, not automatic tuning decisions. A boss can have high
DTPS but low total damage because it dies quickly. A member can have low boss
DPS because it spends time healing, tanking, or damaging adds.
The isolated boss room deliberately removes campaign-final health promotion so
all bosses use the same testing-room durability rule at the chosen level.
Runtime IDs include the simulation seed, which deterministically varies the
game's stable target and cooldown rolls.
Shared curves remain the baseline, but confirmed encounter outliers belong in
`src/game/bossBalanceOverrides.ts`. Each source-controlled entry identifies one
dungeon boss and may apply at one exact level, across a bounded level bracket,
or at every level. Health and damage multipliers are independent and compose;
isolated `boss-testing:` rooms resolve back to their source dungeon so the same
exception is exercised by Balance Lab and live dungeon runs. Record the audit
evidence in the entry reason and prefer this layer over another global curve
change when the problem is confined to particular encounters.
## Coverage and limitations
The selectable roster includes every authored WoW and CoA specialization plus
every supported RoM class-role pair, but automatic certification is Ready-only.
Needs Testing entries remain selectable solely for an intentional targeted
diagnostic. Existing generated party talent plans are used when available.
Missing WoW plans are legally allocated through their matching live talent
tree; CoA builds allocate the chosen specialization tree and class tree through
the live essence/prerequisite rules.
Headless simulation executes combat-store boss attacks and their authored
damage, healing, control, aura, summon, threat, and timing effects. It does not
mount the Three.js scene, play visuals or sounds, or run scene-owned world
objects. Encounter rules that require movement through the original dungeon,
clicking an object, or a renderer-owned trigger require a live Boss Testing
playtest in addition to this numeric audit.
One selected member must occupy the active player slot because the production
runtime distinguishes the player from four companions. The report marks that
member. When a suspected class outlier could be policy-sensitive, rerun with a
different `--active-slot`; do not compare active and companion results without
noting that control-policy difference.
Companion builds initialize every resource pool required by their learned
abilities, not only the class's primary pool. Secondary Energy and Focus
regenerate, and any companion with a Rage pool gains Rage from landed weapon
attacks; this is what makes druid Cat and Bear rotations executable while Mana
remains the class's primary resource. A completed companion cast resolves the
member's currently learned rank before applying its effects, so cast-time spell
damage and healing cannot fall back to rank one.
`playerbots-inspired-v6-reactive-ready-five` keeps level-10-and-up non-healer builds on their
specialization's authored damage priority whenever that build knows a profile
damage action. Reactive interrupts, taunts, control, and defenses still take
precedence. Bear and Cat actions are isolated to their matching Feral specs;
Overpower and Victory Rush require an explicitly modeled trigger, while
Protection companions derive Revenge's five-second trigger from incoming
dodge, parry, and block combat events; Drain Soul, Hammer of Wrath, and Kill
Shot obey their target-health gates.
Off-profile control, interrupt, and debuff buttons cannot consume ordinary
rotation GCDs after their reactive conditions have been checked. Incidental DBC
aura categories cannot outrank explicit profile actions. Rage and Runic Power
generated during an action persist without erasing that action's cooldown/GCD;
short-range melee abilities use companion combat reach; and pets move every
combat tick while retaining their own attack cadence. Untargeted cooldown
resets exclude the source ability, Discipline mitigation/absorbs precede direct
triage and moderate injuries receive a pending-HoT-aware periodic-heal pass,
and Assassination banks Energy for Mutilate while using Envenom as its primary
four-plus-point finisher, matching the pinned Playerbots strategy. Rogue weapon
hits now execute the maintained Instant/Deadly coatings: Deadly Poison stacks a
12-second DoT to five source-specific doses, Envenom uses one dose per combo
point for Nature damage and grants its temporary poison-frequency buff, Mutilate
offers two poison application opportunities, and Shiv guarantees the off-hand
Deadly application at four-plus combo points only while the pending Envenom
lacks enough doses. Envenom normally consumes the used doses; Orin's pinned 3/3
Master Poisoner build preserves them 100% of the time as specified by the
talent.
Ready DPS pack logic is deliberately thresholded and cluster-aware:
Marksmanship uses Multi-Shot at two nearby engaged targets and Volley at three,
while Fire uses Flamestrike and Assassination uses Fan of Knives at three.
Volley's six Arcane pulses are recovered from its DBC trigger child, Fan of
Knives is centered on the Rogue, active ground effects are not replaced early,
and companion AoE does not pull unengaged enemies.
## Code map
- `scripts/balance_lab.py`: prompts, command-line parsing, random/manual/automatic
party selection, searchable boss selection, process orchestration, aggregates,
flags, and all party/suite report formats.
- `scripts/balance-lab/runtime.ts`: live headless fight setup, controlled clock,
exact party construction, boss execution, and lossless event attribution.
- `src/ui/BalanceLabScreen.tsx`: GM form, searchable boss selection, persistent
run-history selection, progress display, report loading, and server or local
artifact opening/downloading.
- `src/ui/BalanceLabResults.tsx`: in-app graphs, report filters, sorting, summary
cards, and complete boss/class-spec/party tables. Add new dashboard metrics
here only after they exist in `suite-report.json`.
- `src/app/balanceLabClient.ts`: typed Balance Lab API client with authenticated
server and loopback-local execution targets.
- `server/balance-lab-jobs.mjs`: validated single and batch job specifications,
bounded parallel process queue, progress parsing, `job.json`
persistence/startup restoration, report-summary extraction, and artifact
allow-list.
- `server/local-balance-lab-api.mjs`: loopback-only, no-login development API
that owns the local job service and persists GUI runs under `artifacts/`.
- `server/server.mjs`: GM-authenticated runtime-preflight, job, status, and
artifact HTTP routes.
- `src/game/benchmark/benchmarkBuilds.ts`: selectable spec builds and legal
talent allocation. The older canonical cohort remains unchanged.
- `src/game/partyStore.ts`: `createConfiguredPartyMember`, the narrow factory
that preserves production stats, equipment, all learned-ability resource
pools, and appearance for an exact companion.
- `src/game/bossTesting.ts`: the authoritative all-boss catalog and isolated
room construction.
- `src/game/bossBalanceOverrides.ts`: audited boss-specific health and damage
exceptions, optionally scoped to an exact level or level bracket.
When extending the tool, add fields to `raw-runs.json` first, aggregate them in
Python second, and document the metric here. Keep formula ownership in `src/game`
and keep reports complete rather than introducing top-N filtering.
## Validation
For tool changes, run at minimum:
```powershell
python -m py_compile scripts/balance_lab.py
python -m py_compile scripts/balance_lab_brackets.py
python -m unittest scripts.test_balance_lab scripts.test_balance_lab_brackets
node --test server/local-balance-lab-api.test.mjs server/balance-lab-jobs.test.mjs server/server.test.mjs
python scripts/balance_lab.py --source wow --level 20 --party-mode automatic --party-configurations 2 --boss "gundrak/entry-29932" --simulations-per-party 1 --duration 5 --output-dir artifacts/balance-lab-smoke
npm run test:ready-five
npm run build
npm run balance:lab:playtest
npm run release:check
```
The Playwright smoke test uses an isolated temporary database, logs in as a GM,
checks that Balance Lab is below GM Tools, runs one real Eck server simulation,
verifies all eleven suite artifacts, exercises the overview and ability graphs
plus every complete table, checks history, role, class/spec, and unused-ability
filtering, and captures desktop/mobile screenshots
under `playtest-artifacts/balance-lab/`. The server unit tests separately restart
the job service and prove that completed history is restored while interrupted
work is marked failed. Use a longer all-boss run before making production tuning
changes.