Release Healer Man 0.1.50

This commit is contained in:
phenom committed 2026-09-17 13:33:55 -04:00
1 parent 717d12e5af
commit 79153ca40d
11 files changed
+89 -38

No files matched your search

+16 -9
View File
@@ -90,6 +90,7 @@ interface MutableMemberMetrics {
healing: number;
rawHealing: number;
overheal: number;
healingReceived: number;
absorbs: number;
damageTaken: number;
rawDamageTaken: number;
@@ -286,6 +287,8 @@ function serializedMember(
rawHealing: metrics.rawHealing,
overheal: metrics.overheal,
hps: metrics.healing / Math.max(0.1, activeSeconds),
healingReceived: metrics.healingReceived,
hpsReceived: metrics.healingReceived / Math.max(0.1, activeSeconds),
absorbs: metrics.absorbs,
damageTaken: metrics.damageTaken,
rawDamageTaken: metrics.rawDamageTaken,
@@ -356,6 +359,7 @@ async function runBoss(
healing: 0,
rawHealing: 0,
overheal: 0,
healingReceived: 0,
absorbs: 0,
damageTaken: 0,
rawDamageTaken: 0,
@@ -395,15 +399,18 @@ async function runBoss(
ability.hits += 1;
bossAbilities.set(key, ability);
}
} else if (event.kind === "healing" && source) {
source.rawHealing += event.rawAmount;
source.healing += event.effectiveAmount;
source.overheal += event.overheal;
const ability = abilityMetrics(source, event.abilityId);
ability.rawHealing += event.rawAmount;
ability.effectiveHealing += event.effectiveAmount;
ability.overheal += event.overheal;
ability.hits += 1;
} else if (event.kind === "healing") {
if (source) {
source.rawHealing += event.rawAmount;
source.healing += event.effectiveAmount;
source.overheal += event.overheal;
const ability = abilityMetrics(source, event.abilityId);
ability.rawHealing += event.rawAmount;
ability.effectiveHealing += event.effectiveAmount;
ability.overheal += event.overheal;
ability.hits += 1;
}
if (target) target.healingReceived += event.effectiveAmount;
} else if (event.kind === "absorb" && source) {
source.absorbs += event.effectiveAmount;
abilityMetrics(source, event.abilityId).absorbs += event.effectiveAmount;
+27 -15
View File
@@ -320,7 +320,7 @@ def interactive_arguments(
def weighted_rate(rows: Iterable[dict[str, Any]], amount: str) -> float:
materialized = list(rows)
seconds = sum(float(row["elapsedSeconds"]) for row in materialized)
return sum(float(row[amount]) for row in materialized) / max(0.1, seconds)
return sum(float(row.get(amount, 0)) for row in materialized) / max(0.1, seconds)
def mean(values: Iterable[float]) -> float:
@@ -402,9 +402,11 @@ def aggregate(raw: dict[str, Any]) -> dict[str, Any]:
"dps": weighted_rate(rows, "damage"),
"bossDps": weighted_rate(rows, "bossDamage"),
"hps": weighted_rate(rows, "healing"),
"hpsReceived": weighted_rate(rows, "healingReceived"),
"dtps": weighted_rate(rows, "damageTaken"),
"totalDamage": sum(row["damage"] for row in rows),
"totalHealing": sum(row["healing"] for row in rows),
"totalHealingReceived": sum(row.get("healingReceived", 0) for row in rows),
"totalAbsorbs": sum(row["absorbs"] for row in rows),
"deathEvents": sum(row["deathEvents"] for row in rows),
"minimumHealthPercent": min(row["minimumHealthPercent"] for row in rows),
@@ -435,6 +437,7 @@ def aggregate(raw: dict[str, Any]) -> dict[str, Any]:
"dps": weighted_rate(rows, "damage"),
"bossDps": weighted_rate(rows, "bossDamage"),
"hps": weighted_rate(rows, "healing"),
"hpsReceived": weighted_rate(rows, "healingReceived"),
"dtps": weighted_rate(rows, "damageTaken"),
"deathEvents": sum(row["deathEvents"] for row in rows),
})
@@ -520,15 +523,16 @@ def markdown_report(report: dict[str, Any]) -> str:
"",
"## Every party member",
"",
"| Slot | Member | Role | DPS | Boss DPS | HPS | DTPS | Role median | Flag | Death events |",
"| ---: | --- | --- | ---: | ---: | ---: | ---: | ---: | --- | ---: |",
"| Slot | Member | Role | DPS | Boss DPS | HPS done | HPS received | DTPS | Role median | Flag | Death events |",
"| ---: | --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | --- | ---: |",
]
for member in report["members"]:
marker = " (active)" if member["activePlayer"] else ""
lines.append(
f"| {member['slot']} | {member['className']} — {member['specialization']}{marker} | "
f"{member['role']} | {format_number(member['dps'])} | {format_number(member['bossDps'])} | "
f"{format_number(member['hps'])} | {format_number(member['dtps'])} | "
f"{format_number(member['hps'])} | {format_number(member['hpsReceived'])} | "
f"{format_number(member['dtps'])} | "
f"{format_ratio(member['damageVsRoleMedian'])} | {member['damageFlag']} | {member['deathEvents']} |"
)
lines.extend([
@@ -592,12 +596,14 @@ def html_report(report: dict[str, Any]) -> str:
"dps": format_number(row["dps"]),
"bossDps": format_number(row["bossDps"]),
"hps": format_number(row["hps"]),
"hpsReceived": format_number(row["hpsReceived"]),
"dtps": format_number(row["dtps"]),
"damageVsRoleMedian": format_number(row["damageVsRoleMedian"], 2),
} for row in report["members"]]
member_table = html_table("members", "Every party member", members, (
("slot", "Slot"), ("member", "Member"), ("role", "Role"), ("dps", "DPS"),
("bossDps", "Boss DPS"), ("hps", "HPS"), ("dtps", "DTPS"),
("bossDps", "Boss DPS"), ("hps", "HPS done"), ("hpsReceived", "HPS received"),
("dtps", "DTPS"),
("damageVsRoleMedian", "Role median ×"), ("damageFlag", "Flag"), ("deathEvents", "Death events"),
))
boss_table = html_table("bosses", "Every boss — highest pressure first", bosses, (
@@ -639,12 +645,12 @@ def create_reports(raw_path: Path, output_dir: Path) -> dict[str, Any]:
))
write_csv(output_dir / "party_members.csv", report["members"], (
"slot", "activePlayer", "classId", "className", "specialization", "role", "dps", "bossDps",
"hps", "dtps", "damageVsRoleMedian", "damageFlag", "totalDamage", "totalHealing",
"totalAbsorbs", "deathEvents", "minimumHealthPercent",
"hps", "hpsReceived", "dtps", "damageVsRoleMedian", "damageFlag", "totalDamage", "totalHealing",
"totalHealingReceived", "totalAbsorbs", "deathEvents", "minimumHealthPercent",
))
write_csv(output_dir / "member_by_boss.csv", report["memberByBoss"], (
"bossKey", "bossName", "dungeonTitle", "slot", "className", "specialization", "role",
"dps", "bossDps", "hps", "dtps", "deathEvents",
"dps", "bossDps", "hps", "hpsReceived", "dtps", "deathEvents",
))
run_rows = [{
"policyId": raw["policyId"],
@@ -688,10 +694,12 @@ def aggregate_class_specs(
"dps": weighted_rate(observations, "damage"),
"bossDps": weighted_rate(observations, "bossDamage"),
"hps": weighted_rate(observations, "healing"),
"hpsReceived": weighted_rate(observations, "healingReceived"),
"dtps": weighted_rate(observations, "damageTaken"),
"totalDamage": sum(row["damage"] for row in observations),
"totalDamageTaken": sum(row["damageTaken"] for row in observations),
"totalHealing": sum(row["healing"] for row in observations),
"totalHealingReceived": sum(row.get("healingReceived", 0) for row in observations),
"totalAbsorbs": sum(row["absorbs"] for row in observations),
"deathEvents": sum(row["deathEvents"] for row in observations),
})
@@ -805,14 +813,15 @@ def suite_markdown(report: dict[str, Any]) -> str:
"",
"## Every tested class/spec",
"",
"| Source | Class/spec | Role | Parties | Runs | DPS | Boss DPS | HPS | DTPS | Total taken | Role median | Flag | Deaths |",
"| --- | --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | --- | ---: |",
"| Source | Class/spec | Role | Parties | Runs | DPS | Boss DPS | HPS done | HPS received | DTPS | Total taken | Role median | Flag | Deaths |",
"| --- | --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | --- | ---: |",
]
for row in report["classSpecs"]:
lines.append(
f"| {row['source']} | {row['className']} — {row['specialization']} | {row['role']} | "
f"{row['partyConfigurations']} | {row['runs']} | {format_number(row['dps'])} | "
f"{format_number(row['bossDps'])} | {format_number(row['hps'])} | {format_number(row['dtps'])} | "
f"{format_number(row['bossDps'])} | {format_number(row['hps'])} | "
f"{format_number(row['hpsReceived'])} | {format_number(row['dtps'])} | "
f"{format_number(row['totalDamageTaken'])} | "
f"{format_ratio(row['damageVsSourceRoleMedian'])} | {row['damageFlag']} | {row['deathEvents']} |"
)
@@ -873,6 +882,7 @@ def suite_html(report: dict[str, Any]) -> str:
"dps": format_number(row["dps"]),
"bossDps": format_number(row["bossDps"]),
"hps": format_number(row["hps"]),
"hpsReceived": format_number(row["hpsReceived"]),
"dtps": format_number(row["dtps"]),
"totalDamageTaken": format_number(row["totalDamageTaken"]),
"damageVsSourceRoleMedian": format_ratio(row["damageVsSourceRoleMedian"]),
@@ -911,7 +921,8 @@ def suite_html(report: dict[str, Any]) -> str:
class_table = html_table("class-specs", "Every tested class/spec", class_specs, (
("source", "Source"), ("classSpec", "Class/spec"), ("role", "Role"),
("partyConfigurations", "Parties"), ("runs", "Runs"), ("dps", "DPS"),
("bossDps", "Boss DPS"), ("hps", "HPS"), ("dtps", "DTPS"),
("bossDps", "Boss DPS"), ("hps", "HPS done"), ("hpsReceived", "HPS received"),
("dtps", "DTPS"),
("totalDamageTaken", "Total taken"), ("damageVsSourceRoleMedian", "Role median"),
("damageFlag", "Flag"), ("deathEvents", "Deaths"),
))
@@ -1064,6 +1075,7 @@ def create_suite_reports(entries: Sequence[dict[str, Any]], output_dir: Path, le
"dps": weighted_rate(observations, "damage"),
"bossDps": weighted_rate(observations, "bossDamage"),
"hps": weighted_rate(observations, "healing"),
"hpsReceived": weighted_rate(observations, "healingReceived"),
"dtps": weighted_rate(observations, "damageTaken"),
"totalDamageTaken": sum(row["damageTaken"] for row in observations),
"deathEvents": sum(row["deathEvents"] for row in observations),
@@ -1106,8 +1118,8 @@ def create_suite_reports(entries: Sequence[dict[str, Any]], output_dir: Path, le
))
write_csv(output_dir / "class_specs_across_parties.csv", class_specs, (
"source", "buildId", "classId", "className", "specialization", "role", "partyConfigurations",
"runs", "dps", "bossDps", "hps", "dtps", "damageVsSourceRoleMedian", "damageFlag",
"totalDamage", "totalDamageTaken", "totalHealing", "totalAbsorbs", "deathEvents",
"runs", "dps", "bossDps", "hps", "hpsReceived", "dtps", "damageVsSourceRoleMedian", "damageFlag",
"totalDamage", "totalDamageTaken", "totalHealing", "totalHealingReceived", "totalAbsorbs", "deathEvents",
))
write_csv(output_dir / "class_abilities_across_parties.csv", class_abilities, (
"source", "buildId", "classId", "className", "specialization", "role", "abilityId",
@@ -1129,7 +1141,7 @@ def create_suite_reports(entries: Sequence[dict[str, Any]], output_dir: Path, le
))
write_csv(output_dir / "class_spec_by_boss.csv", class_spec_by_boss, (
"source", "buildId", "classId", "className", "specialization", "role", "bossKey", "bossName",
"dungeonTitle", "partyConfigurations", "runs", "dps", "bossDps", "hps", "dtps",
"dungeonTitle", "partyConfigurations", "runs", "dps", "bossDps", "hps", "hpsReceived", "dtps",
"totalDamageTaken", "deathEvents",
))
write_csv(output_dir / "suite_runs.csv", compact_runs, (
+5 -3
View File
@@ -66,23 +66,25 @@ class BalanceLabControllerTests(unittest.TestCase):
self.assertEqual([boss["key"] for boss in search_bosses("forsaken", bosses)], ["abbey/duke"])
self.assertEqual([boss["key"] for boss in search_bosses("rom", bosses)], ["abbey/duke"])
def test_class_spec_aggregate_keeps_rate_and_absolute_damage_taken(self) -> None:
def test_class_spec_aggregate_keeps_damage_and_healing_received_metrics(self) -> None:
key = ("wow", "priest-holy", "priest", "Priest", "Holy", "healer")
rows = aggregate_class_specs({key: [
{
"partyConfigurationId": "wow-party-001", "elapsedSeconds": 10,
"damage": 200, "bossDamage": 180, "healing": 500,
"damage": 200, "bossDamage": 180, "healing": 500, "healingReceived": 300,
"damageTaken": 120, "absorbs": 25, "deathEvents": 0,
},
{
"partyConfigurationId": "wow-party-002", "elapsedSeconds": 20,
"damage": 400, "bossDamage": 360, "healing": 900,
"damage": 400, "bossDamage": 360, "healing": 900, "healingReceived": 600,
"damageTaken": 330, "absorbs": 50, "deathEvents": 1,
},
]})
self.assertEqual(len(rows), 1)
self.assertEqual(rows[0]["dtps"], 15)
self.assertEqual(rows[0]["totalDamageTaken"], 450)
self.assertEqual(rows[0]["hpsReceived"], 30)
self.assertEqual(rows[0]["totalHealingReceived"], 900)
def test_ability_aggregate_keeps_used_and_available_but_unused_rows(self) -> None:
class_key = ("wow", "mage-fire", "mage", "Mage", "Fire", "damage")