# 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 can select all three; 3. a level from 1 through 80; 4. how many unique 1-tank/1-healer/3-damage configurations to generate per source; 5. all bosses, WoW bosses, RoM bosses, or a searchable multi-boss selection; 6. how many deterministic simulations to run for every party/boss pairing; and 7. the maximum duration of each fight. 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. ## 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, level, unique parties per source, simulations per party/boss, duration, random seed, boss category, and a searchable multi-boss picker. **Run on server** sends only that allow-listed configuration to the authenticated GM API. The server places it in a one-at-a-time queue, launches the same Python controller and live TypeScript runtime used by the command line, and writes artifacts under: ```text DATA_DIR/balance-lab/jobs// ``` 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; - 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, HPS, DTPS, absolute damage taken, damage/healing/absorb totals, deaths, and source/role-median tuning flags; 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. 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 `1` and is capped at four. `BALANCE_LAB_MAX_QUEUED` defaults to eight and is capped at 50. Raising concurrency permits parallel server processes but can make both the game server and simulations less responsive. Requests are capped before launch, 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. ## Repeatable command-line runs 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 ``` Generate ten parties for each of WoW, CoA, and RoM, but test only RoM bosses: ```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//` 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//` 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. | | `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. - Ability `activeRuns` counts simulations with at least one cast, hit, damage, healing, overheal, or absorb 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. - 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. ## Coverage and limitations The selectable roster includes every authored WoW and CoA specialization plus every supported RoM class-role pair. 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. ## 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 authenticated 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 GM Balance Lab API client. - `server/balance-lab-jobs.mjs`: validated job specifications, bounded one-at-a-time process queue, progress parsing, `job.json` persistence/startup restoration, report-summary extraction, and artifact allow-list. - `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, resources, and appearance for an exact companion. - `src/game/bossTesting.ts`: the authoritative all-boss catalog and isolated room construction. 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 unittest scripts.test_balance_lab node --test 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 test -- --run src/game/benchmark src/game/partyStore.test.ts src/game/partyRuntime.test.ts 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.