diff --git a/PLAYTEST_NOTES.md b/PLAYTEST_NOTES.md index 1df85b6a..1fbb6be1 100644 --- a/PLAYTEST_NOTES.md +++ b/PLAYTEST_NOTES.md @@ -3529,3 +3529,19 @@ Vite's existing large-chunk advisory remains informational. displayed `Server runtime ready · Python 3.14.0`, submitted a real Eck run, generated all reports, and completed desktop/mobile QA without browser errors. Evidence is in `playtest-artifacts/balance-lab/02-server-job-config.png`. + +## Party melee animation cadence (2026-09-16) + +- Party character animation now follows authoritative combat-presentation + phases instead of treating every `nextActionAt` retry timestamp as an attack. + Auto-attacks produce one ordinary attack event, while real abilities retain + their cast-start, release, and cancel identities. +- Warrior and Paladin auto-attacks now prefer the normal one-handed/two-handed + attack clips before special clips. Special/spinning clips remain available to + explicitly matched abilities rather than every weapon swing. +- The focused combat-animation suite passed 10 tests, all 47 avatar tests + passed, and the production build plus texture/performance and release-bundle + audits passed. The live level-three Stockades pass completed without browser + errors; all five bodies and their equipped weapon/staff silhouettes rendered + intact. Evidence is in + `playtest-artifacts/dungeon-health-scaling/stockades-level-three.png`. diff --git a/docs/COMBAT_ROTATION_MATRIX.md b/docs/COMBAT_ROTATION_MATRIX.md new file mode 100644 index 00000000..eae0a0f2 --- /dev/null +++ b/docs/COMBAT_ROTATION_MATRIX.md @@ -0,0 +1,65 @@ +# Combat rotation matrix + +Healer Man uses AzerothCore playerbots commit `531282e` as a read-only behavioral reference. The implementation is an independent TypeScript policy over Healer Man's executable catalog, not a port of playerbots C++. + +| Concern | Playerbots behavior translated into Healer Man | +| --- | --- | +| Core rotation | One deterministic priority profile for each of the 31 Wrath specializations. | +| Maintenance | DoTs, debuffs, buffs, pets, and summons are refreshed only after their existing effect ends. | +| Execute | Execute-category abilities move ahead of the normal profile below 20% target health. | +| Multiple targets | Cleave gains value at two hostiles; area damage moves ahead at three or more. | +| Healing | Critical, medium, and light triage bands begin below 30%, 60%, and 90% health. Area healing requires three injured allies. | +| Threat | Tanks interrupt first and taunt a hostile that is not attacking them; non-tanks drop dangerous threat before continuing their profile. | +| Control | Interrupts are reactive. Long crowd control is used only when an encounter requests control. | +| Defense | Mitigation is used below 55% actor health and receives emergency priority below 35%. | +| Resources | Low-resource generators are favored below 30%; spenders still have to pass runtime cost and rune validation. | +| Combo points | Builders run below four points; finishers require at least four points on the current target. | +| Major cooldowns | Offensive cooldowns of 60 seconds or longer are held for bosses or durable targets. | + +CoA and Runes of Magic builds use the same semantic rules when no classic profile applies. Runes of Magic candidates include primary, secondary-general, and primary/secondary elite skills through `abilitiesForCharacter`. + +## Classic specialization profiles + +The leftmost available action wins after reactive rules, learned-talent gating, resource checks, maintenance checks, execute/AoE adjustments, and major-cooldown conservation. Maintenance entries are intentionally listed first; an active aura or pet removes that entry from consideration. + +| Class | Specialization | Maintenance and core priority | +| --- | --- | --- | +| Warrior | Arms | Battle Stance; Mortal Strike; Rend; Overpower; Execute; Slam; Heroic Strike | +| Warrior | Fury | Berserker Stance; Bloodthirst; Whirlwind; Execute; Sunder Armor; Heroic Strike | +| Warrior | Protection | Defensive Stance; Shield Slam; Revenge; Devastate; Thunder Clap; Sunder Armor; Heroic Strike | +| Paladin | Holy | Concentration Aura; Blessing of Might; Holy Shock; Flash of Light; Holy Light; Sacred Shield; Judgement of Light | +| Paladin | Protection | Devotion Aura; Blessing of Might; Hammer of the Righteous; Shield of Righteousness; Holy Shield; Consecration; Judgement of Wisdom | +| Paladin | Retribution | Retribution Aura; Blessing of Might; Divine Storm; Crusader Strike; Judgement of Wisdom; Consecration; Exorcism; Hammer of Wrath | +| Hunter | Beast Mastery | Aspect of the Dragonhawk/Hawk; Call Pet; Kill Command; Bestial Wrath; Serpent Sting; Arcane Shot; Multi-Shot; Steady Shot | +| Hunter | Marksmanship | Aspect of the Dragonhawk/Hawk; Call Pet; Kill Shot; Chimera Shot; Aimed Shot; Serpent Sting; Arcane Shot; Steady Shot | +| Hunter | Survival | Aspect of the Dragonhawk/Hawk; Call Pet; Kill Shot; Explosive Shot; Black Arrow; Serpent Sting; Aimed Shot; Arcane Shot; Steady Shot | +| Rogue | Assassination | Envenom; Rupture; Mutilate; Eviscerate; Sinister Strike | +| Rogue | Combat | Killing Spree; Blade Flurry; Eviscerate; Sinister Strike | +| Rogue | Subtlety | Shadow Dance; Rupture; Eviscerate; Hemorrhage; Backstab | +| Priest | Discipline | Inner Fire; Penance; Power Word: Shield; Flash Heal; Greater Heal; Renew | +| Priest | Holy | Inner Fire; Circle of Healing; Prayer of Mending; Flash Heal; Greater Heal; Renew | +| Priest | Shadow | Shadowform; Inner Fire; Vampiric Touch; Devouring Plague; Shadow Word: Pain; Mind Blast; Mind Flay; Shadow Word: Death | +| Death Knight | Blood | Frost Presence (Blood Presence fallback); Heart Strike; Death Strike; Icy Touch; Plague Strike; Death Coil | +| Death Knight | Frost | Blood Presence; Howling Blast; Obliterate; Frost Strike; Icy Touch; Plague Strike | +| Death Knight | Unholy | Unholy Presence (Blood Presence fallback); Raise Dead; Scourge Strike; Death and Decay; Plague Strike; Icy Touch; Death Coil | +| Shaman | Elemental | Flametongue Weapon (Rockbiter fallback); Lightning Shield; Flame Shock; Lava Burst; Chain Lightning; Lightning Bolt | +| Shaman | Enhancement | Windfury Weapon (Rockbiter fallback); Lightning Shield; Feral Spirit; Stormstrike; Lava Lash; Earth Shock; Lightning Bolt | +| Shaman | Restoration | Water Shield; Rockbiter Weapon; Lightning Shield; Riptide; Chain Heal; Lesser Healing Wave; Healing Wave; Earth Shield | +| Mage | Arcane | Mage Armor; Arcane Barrage; Arcane Blast; Arcane Missiles | +| Mage | Fire | Molten Armor; Living Bomb; Pyroblast; Fireball; Scorch; Fire Blast | +| Mage | Frost | Frost Armor; Deep Freeze; Frostbolt; Ice Lance; Cone of Cold | +| Warlock | Affliction | Fel Armor; Felhunter; Haunt; Unstable Affliction; Corruption; Curse of Agony; Drain Soul; Shadow Bolt | +| Warlock | Demonology | Fel Armor; Felguard; Metamorphosis; Immolate; Corruption; Shadow Bolt | +| Warlock | Destruction | Fel Armor; Imp; Chaos Bolt; Conflagrate; Immolate; Incinerate; Shadow Bolt | +| Druid | Balance | Moonkin Form; Starfall; Moonfire; Insect Swarm; Starfire; Wrath | +| Druid | Feral Bear | Dire Bear/Bear Form; Mangle; Lacerate; Swipe; Maul | +| Druid | Feral Cat | Cat Form; Rip; Savage Roar; Mangle; Rake; Shred; Claw | +| Druid | Restoration | Tree of Life; Wild Growth; Swiftmend; Lifebloom; Rejuvenation; Regrowth; Healing Touch | + +## Ability audit classification + +Active abilities fall into one of three audit buckets: + +- **Conditional:** executable but selected only by a semantic trigger, such as interrupts, dispels, resurrection, threat control, movement, crowd control, executes, defensives, and long cooldowns. +- **Unsupported:** the catalog has only `unsupported`, `scripted`, or noncombat `utility` payloads, or represents a noncombat summon such as a scouting eye, ritual object, portal, or mount. These remain player-visible when valid but are deliberately excluded from autonomous rotations. +- **Rotation defect:** an executable learned ability with combat semantics that cannot be selected in its valid state. Profile-reference tests, shared-policy scenario tests, party damage audits, and the expanded-class audit treat these as failures. diff --git a/package-lock.json b/package-lock.json index bc50e30f..cb859927 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "healer-man", - "version": "0.1.48", + "version": "0.1.49", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "healer-man", - "version": "0.1.48", + "version": "0.1.49", "dependencies": { "@capacitor/android": "8.4.1", "@capacitor/core": "8.4.1", diff --git a/package.json b/package.json index 87764818..dc9a5398 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "healer-man", "private": true, - "version": "0.1.48", + "version": "0.1.49", "type": "module", "scripts": { "dev": "vite --host 0.0.0.0", diff --git a/public/assets/ui/spells/ability_devour.png b/public/assets/ui/spells/ability_devour.png new file mode 100644 index 00000000..9beddbaa Binary files /dev/null and b/public/assets/ui/spells/ability_devour.png differ diff --git a/public/assets/ui/spells/ability_druid_forceofnature.png b/public/assets/ui/spells/ability_druid_forceofnature.png new file mode 100644 index 00000000..4df22566 Binary files /dev/null and b/public/assets/ui/spells/ability_druid_forceofnature.png differ diff --git a/public/assets/ui/spells/ability_golemstormbolt.png b/public/assets/ui/spells/ability_golemstormbolt.png new file mode 100644 index 00000000..a62ebe1c Binary files /dev/null and b/public/assets/ui/spells/ability_golemstormbolt.png differ diff --git a/public/assets/ui/spells/ability_hunter_chimerashot2.png b/public/assets/ui/spells/ability_hunter_chimerashot2.png new file mode 100644 index 00000000..6d522df6 Binary files /dev/null and b/public/assets/ui/spells/ability_hunter_chimerashot2.png differ diff --git a/public/assets/ui/spells/ability_hunter_pet_bat.png b/public/assets/ui/spells/ability_hunter_pet_bat.png new file mode 100644 index 00000000..8608b4e5 Binary files /dev/null and b/public/assets/ui/spells/ability_hunter_pet_bat.png differ diff --git a/public/assets/ui/spells/ability_mage_deepfreeze.png b/public/assets/ui/spells/ability_mage_deepfreeze.png new file mode 100644 index 00000000..e99f6695 Binary files /dev/null and b/public/assets/ui/spells/ability_mage_deepfreeze.png differ diff --git a/public/assets/ui/spells/ability_paladin_divinestorm.png b/public/assets/ui/spells/ability_paladin_divinestorm.png new file mode 100644 index 00000000..89993551 Binary files /dev/null and b/public/assets/ui/spells/ability_paladin_divinestorm.png differ diff --git a/public/assets/ui/spells/ability_paladin_hammeroftherighteous.png b/public/assets/ui/spells/ability_paladin_hammeroftherighteous.png new file mode 100644 index 00000000..3d079dbf Binary files /dev/null and b/public/assets/ui/spells/ability_paladin_hammeroftherighteous.png differ diff --git a/public/assets/ui/spells/ability_rogue_murderspree.png b/public/assets/ui/spells/ability_rogue_murderspree.png new file mode 100644 index 00000000..edebdae7 Binary files /dev/null and b/public/assets/ui/spells/ability_rogue_murderspree.png differ diff --git a/public/assets/ui/spells/ability_rogue_shadowstep.png b/public/assets/ui/spells/ability_rogue_shadowstep.png new file mode 100644 index 00000000..f59536cd Binary files /dev/null and b/public/assets/ui/spells/ability_rogue_shadowstep.png differ diff --git a/public/assets/ui/spells/ability_shaman_lavalash.png b/public/assets/ui/spells/ability_shaman_lavalash.png new file mode 100644 index 00000000..0994122e Binary files /dev/null and b/public/assets/ui/spells/ability_shaman_lavalash.png differ diff --git a/public/assets/ui/spells/ability_theblackarrow.png b/public/assets/ui/spells/ability_theblackarrow.png new file mode 100644 index 00000000..6121007d Binary files /dev/null and b/public/assets/ui/spells/ability_theblackarrow.png differ diff --git a/public/assets/ui/spells/ability_warlock_demonicempowerment.png b/public/assets/ui/spells/ability_warlock_demonicempowerment.png new file mode 100644 index 00000000..3116e7a0 Binary files /dev/null and b/public/assets/ui/spells/ability_warlock_demonicempowerment.png differ diff --git a/public/assets/ui/spells/inv_armor_helm_plate_naxxramas_raidwarrior_c_01.png b/public/assets/ui/spells/inv_armor_helm_plate_naxxramas_raidwarrior_c_01.png new file mode 100644 index 00000000..0bc67dd0 Binary files /dev/null and b/public/assets/ui/spells/inv_armor_helm_plate_naxxramas_raidwarrior_c_01.png differ diff --git a/public/assets/ui/spells/inv_chest_leather_13.png b/public/assets/ui/spells/inv_chest_leather_13.png new file mode 100644 index 00000000..7e140298 Binary files /dev/null and b/public/assets/ui/spells/inv_chest_leather_13.png differ diff --git a/public/assets/ui/spells/inv_relics_idolofrejuvenation.png b/public/assets/ui/spells/inv_relics_idolofrejuvenation.png new file mode 100644 index 00000000..c1399db5 Binary files /dev/null and b/public/assets/ui/spells/inv_relics_idolofrejuvenation.png differ diff --git a/public/assets/ui/spells/inv_staff_15.png b/public/assets/ui/spells/inv_staff_15.png new file mode 100644 index 00000000..1077ea6c Binary files /dev/null and b/public/assets/ui/spells/inv_staff_15.png differ diff --git a/public/assets/ui/spells/inv_sword_07.png b/public/assets/ui/spells/inv_sword_07.png new file mode 100644 index 00000000..6b0b824b Binary files /dev/null and b/public/assets/ui/spells/inv_sword_07.png differ diff --git a/public/assets/ui/spells/spell_frost_coldhearted.png b/public/assets/ui/spells/spell_frost_coldhearted.png new file mode 100644 index 00000000..7ff1a024 Binary files /dev/null and b/public/assets/ui/spells/spell_frost_coldhearted.png differ diff --git a/public/assets/ui/spells/spell_holy_crusaderstrike.png b/public/assets/ui/spells/spell_holy_crusaderstrike.png new file mode 100644 index 00000000..7026c600 Binary files /dev/null and b/public/assets/ui/spells/spell_holy_crusaderstrike.png differ diff --git a/public/assets/ui/spells/spell_holy_divineillumination.png b/public/assets/ui/spells/spell_holy_divineillumination.png new file mode 100644 index 00000000..e755b5de Binary files /dev/null and b/public/assets/ui/spells/spell_holy_divineillumination.png differ diff --git a/public/assets/ui/spells/spell_holy_painsupression.png b/public/assets/ui/spells/spell_holy_painsupression.png new file mode 100644 index 00000000..c9659068 Binary files /dev/null and b/public/assets/ui/spells/spell_holy_painsupression.png differ diff --git a/public/assets/ui/spells/spell_nature_forceofnature.png b/public/assets/ui/spells/spell_nature_forceofnature.png new file mode 100644 index 00000000..dd91d7c7 Binary files /dev/null and b/public/assets/ui/spells/spell_nature_forceofnature.png differ diff --git a/public/assets/ui/spells/spell_shadow_curse.png b/public/assets/ui/spells/spell_shadow_curse.png new file mode 100644 index 00000000..bc29ccd1 Binary files /dev/null and b/public/assets/ui/spells/spell_shadow_curse.png differ diff --git a/public/assets/ui/spells/spell_shadow_dispersion.png b/public/assets/ui/spells/spell_shadow_dispersion.png new file mode 100644 index 00000000..8b16f3a0 Binary files /dev/null and b/public/assets/ui/spells/spell_shadow_dispersion.png differ diff --git a/public/assets/ui/spells/spell_shadow_psychichorrors.png b/public/assets/ui/spells/spell_shadow_psychichorrors.png new file mode 100644 index 00000000..b2be3fc5 Binary files /dev/null and b/public/assets/ui/spells/spell_shadow_psychichorrors.png differ diff --git a/public/assets/ui/spells/spell_shadow_summonfelguard.png b/public/assets/ui/spells/spell_shadow_summonfelguard.png new file mode 100644 index 00000000..e6bd3aba Binary files /dev/null and b/public/assets/ui/spells/spell_shadow_summonfelguard.png differ diff --git a/release-version.json b/release-version.json index e5101389..4cca4a45 100644 --- a/release-version.json +++ b/release-version.json @@ -1,4 +1,4 @@ { - "versionName": "0.1.48", - "versionCode": 1048 + "versionName": "0.1.49", + "versionCode": 1049 } diff --git a/scripts/arena-pipeline/combat-runtime.ts b/scripts/arena-pipeline/combat-runtime.ts index c9696298..ef62536b 100644 --- a/scripts/arena-pipeline/combat-runtime.ts +++ b/scripts/arena-pipeline/combat-runtime.ts @@ -5,7 +5,7 @@ import { registerCombatSpatialProvider } from "../../src/game/combatSpatial"; import { clearMobRuntimeRegistry, registerMobRuntime, getMobPosition } from "../../src/game/mobRuntimeRegistry"; import { clearPartyRuntimeRegistry, registerPartyRuntimePosition, getPartyRuntimePosition } from "../../src/game/partyRuntimeRegistry"; import { targetingRulesForAbility } from "../../src/game/abilityRules"; -import { resourceProfileForClass, abilityAtLevel, isAbilityUnlocked } from "../../src/game/abilityCatalog"; +import { resourceProfileForClass, learnedAbilityForBuild } from "../../src/game/abilityCatalog"; import { deriveCharacterStats } from "../../src/game/wotlkStats"; import { subscribeCombatPresentation } from "../../src/game/combatPresentation"; import { arenaDampening } from "../../server/arena-rules.mjs"; @@ -36,9 +36,10 @@ export function initializeArenaCharacter(actor: any, now: number) { actor.avatar = { classId: state.classId, raceId: seed.raceId ?? (actor.team === 1 ? "orc" : "human"), gender: (seed as any).gender ?? "male", appearance: (seed as any).appearance ?? createDefaultAppearance() }; actor.visualEquipment = equipmentItemsForOwner(state.equipment, state.inventory, PLAYER_EQUIPMENT_OWNER_ID); - const abilities = state.abilities.filter((ability: any) => !ability.passive && isAbilityUnlocked(ability, state.level) - && (ability.talentEntryId === undefined || (state.talentRanks[`coa-entry-${ability.talentEntryId}`] ?? 0) > 0)) - .map((ability: any) => { const resolved = abilityAtLevel(ability, state.level, ability.talentEntryId === undefined ? undefined : state.talentRanks[`coa-entry-${ability.talentEntryId}`]); return { ...resolved, targeting: targetingRulesForAbility(resolved) }; }); + const abilities = state.abilities.filter((ability: any) => !ability.passive) + .map((ability: any) => learnedAbilityForBuild(ability, state.level, state.talentRanks)) + .filter((ability: any) => ability !== null) + .map((ability: any) => ({ ...ability, targeting: targetingRulesForAbility(ability) })); actor.characterKit = { ...kit, seed, classId: state.classId, level: state.level, resource: { ...resourceProfileForClass(state.classId), maximum: state.maxResource, initial: state.resource }, abilities: Object.fromEntries(abilities.map((ability: any) => [ability.id, ability])), actionBindings: state.actionBindings }; diff --git a/scripts/balance-lab/runtime.ts b/scripts/balance-lab/runtime.ts index b9bcb142..488691d5 100644 --- a/scripts/balance-lab/runtime.ts +++ b/scripts/balance-lab/runtime.ts @@ -9,9 +9,8 @@ import { } from "../../src/app/characterCatalog"; import type { ContentCategoryId } from "../../src/app/contentCategories"; import { - abilitiesForClass, - abilityAtLevel, - isAbilityUnlocked, + abilitiesForCharacter, + learnedAbilityForBuild, preloadAbilityCatalogsForClasses, } from "../../src/game/abilityCatalog"; import { aiHintsForAbility } from "../../src/game/abilityRules"; @@ -44,7 +43,8 @@ import { type PartyMember, } from "../../src/game/partyStore"; import { specializationsForClass } from "../../src/game/specializationCatalog"; -import { BenchmarkController } from "../../src/game/benchmark/benchmarkController"; +import { BENCHMARK_POLICY_ID, BenchmarkController } from "../../src/game/benchmark/benchmarkController"; +import { COMBAT_ROTATION_VERSION } from "../../src/game/combatRotation"; import { loadSelectableBenchmarkBuilds, selectableBenchmarkBuildCatalog, @@ -157,6 +157,8 @@ function catalogPayload(source: PartyCatalogSource, level: number) { const builds = selectableBenchmarkBuildCatalog(level, classIdsForSource(source)); return { schemaVersion: 1, + policyId: BENCHMARK_POLICY_ID, + rotationVersion: COMBAT_ROTATION_VERSION, level: builds[0]?.level ?? level, source, builds: builds.map((build) => ({ @@ -243,14 +245,11 @@ function currentHealth(actorId: string): readonly [number, number] { } function availableAbilities(build: BenchmarkBuildDefinition) { - return abilitiesForClass(build.classId) - .filter((ability) => !ability.passive && isAbilityUnlocked(ability, build.level)) + return abilitiesForCharacter(build.classId, build.secondaryClassId) + .filter((ability) => !ability.passive) .map((ability) => { - const learnedRank = ability.talentEntryId === undefined - ? undefined - : build.talentRanks[`coa-entry-${ability.talentEntryId}`] ?? 0; - if (ability.talentEntryId !== undefined && !learnedRank) return null; - const ranked = abilityAtLevel(ability, build.level, learnedRank); + const ranked = learnedAbilityForBuild(ability, build.level, build.talentRanks); + if (!ranked) return null; if (ranked.effects.every((effect) => effect.kind === "unsupported" || effect.kind === "scripted")) return null; return { abilityId: ranked.id, @@ -573,6 +572,8 @@ async function simulateConfig(config: BalanceLabConfig, output: string): Promise schemaVersion: 1, generatedAt: new Date().toISOString(), engine: "live-typescript-runtime", + policyId: BENCHMARK_POLICY_ID, + rotationVersion: COMBAT_ROTATION_VERSION, config: { ...config, level: builds[0]?.level ?? config.level, diff --git a/scripts/balance_lab.py b/scripts/balance_lab.py index fe836061..63a43375 100644 --- a/scripts/balance_lab.py +++ b/scripts/balance_lab.py @@ -443,6 +443,8 @@ def aggregate(raw: dict[str, Any]) -> dict[str, Any]: "schemaVersion": 1, "generatedAt": raw["generatedAt"], "engine": raw["engine"], + "policyId": raw["policyId"], + "rotationVersion": raw["rotationVersion"], "config": raw["config"], "party": raw["party"], "summary": { @@ -508,6 +510,8 @@ def markdown_report(report: dict[str, Any]) -> str: "", f"Generated: {report['generatedAt']}", "", + f"Policy: `{report['policyId']}` · rotation: `{report['rotationVersion']}`", + "", f"Party source: **{config['source']}** · level **{config['level']}** · " f"{report['summary']['bosses']} bosses · {report['summary']['runs']} simulations", "", @@ -614,7 +618,7 @@ input {{ width:min(420px,90%); padding:9px 12px; margin:0 0 12px; color:inherit; th,td {{ padding:8px 10px; border-bottom:1px solid #21262d; text-align:right; white-space:nowrap; }} th {{ position:sticky; top:0; cursor:pointer; background:#161b22; }} th:first-child,td:first-child,th:nth-child(2),td:nth-child(2) {{ text-align:left; }} tr:hover {{ background:#161b22; }}

Healer Man Balance Lab

-

{html.escape(report['generatedAt'])} · level {report['config']['level']} {html.escape(report['config']['source'])} party · {report['summary']['bosses']} bosses · {report['summary']['runs']} simulations. Click a heading to sort; filter boxes keep every row accessible.

+

{html.escape(report['generatedAt'])} · policy {html.escape(report['policyId'])} · rotation {html.escape(report['rotationVersion'])} · level {report['config']['level']} {html.escape(report['config']['source'])} party · {report['summary']['bosses']} bosses · {report['summary']['runs']} simulations. Click a heading to sort; filter boxes keep every row accessible.

{member_table}{boss_table}