Update Healer Man server

This commit is contained in:
phenom committed 2026-09-19 21:01:51 -04:00
1 parent f8314e7de1
commit 1fedb9368d
76 files changed
+20556 -2523

No files matched your search

+33 -1
View File
@@ -1,5 +1,8 @@
import { describe, expect, it } from "vitest";
import { abilitiesForClass } from "../abilityCatalog";
import { rotationProfileFor } from "../combatRotation";
import { createConfiguredPartyMember } from "../partyStore";
import { specializationsForClass } from "../specializationCatalog";
import { runtimeNormalizeTalentRanks } from "../talentRuntimeCatalog";
import {
loadSelectableBenchmarkBuilds,
@@ -12,7 +15,15 @@ describe("balance-lab selectable builds", () => {
"warrior", "paladin", "hunter", "rogue", "priest", "death-knight",
"shaman", "mage", "warlock", "druid",
]);
expect(catalog).toHaveLength(31);
const specializations = new Map<string, typeof catalog>();
for (const build of catalog) {
const key = `${build.classId}:${build.orientation}:${build.role}`;
specializations.set(key, [...(specializations.get(key) ?? []), build]);
}
expect(specializations.size).toBe(31);
expect([...specializations.values()].every((profiles) => profiles.length >= 2)).toBe(true);
expect(catalog.filter((build) => build.defaultTalentBuild)).toHaveLength(31);
expect(catalog.every((build) => build.talentBuildId && build.talentBuildName && build.characterName)).toBe(true);
expect(catalog).toEqual(expect.arrayContaining([
expect.objectContaining({ classId: "hunter", orientation: "Beast Mastery", role: "damage" }),
expect.objectContaining({ classId: "priest", orientation: "Discipline", role: "healer" }),
@@ -29,6 +40,27 @@ describe("balance-lab selectable builds", () => {
expect(Object.keys(beastMastery?.talentRanks ?? {})).not.toHaveLength(0);
});
it("gives every selectable WoW specialization an executable priority rotation", () => {
const catalog = selectableBenchmarkBuildCatalog(80, [
"warrior", "paladin", "hunter", "rogue", "priest", "death-knight",
"shaman", "mage", "warlock", "druid",
]);
const checked = new Set<string>();
for (const build of catalog) {
const key = `${build.classId}:${build.orientation}`;
if (checked.has(key)) continue;
checked.add(key);
const specialization = specializationsForClass(build.classId)
.find((candidate) => candidate.displayName === build.orientation);
const profile = rotationProfileFor(build.classId, specialization?.id, build.orientation);
expect(profile, key).not.toBeNull();
expect(profile!.abilityPriority.length, key).toBeGreaterThan(0);
const abilityNames = new Set(abilitiesForClass(build.classId).map((ability) => ability.name));
expect(profile!.abilityPriority.some((name) => abilityNames.has(name)), key).toBe(true);
}
expect(checked.size).toBe(31);
});
it("keeps each CoA specialization tree selectable", async () => {
const builds = await loadSelectableBenchmarkBuilds(20, ["witch-doctor"]);
expect(builds.map((build) => `${build.orientation}:${build.role}`)).toEqual([
+21 -2
View File
@@ -2,7 +2,11 @@ import { describe, expect, it } from "vitest";
import { CLASSES } from "../../app/characterCatalog";
import { partyRolesForClass } from "../partyRoles";
import { runtimeNormalizeTalentRanks } from "../talentRuntimeCatalog";
import { loadCanonicalBenchmarkBuilds } from "./benchmarkBuilds";
import {
loadCanonicalBenchmarkBuilds,
loadSelectableBenchmarkBuilds,
selectableBenchmarkBuildCatalog,
} from "./benchmarkBuilds";
describe("canonical benchmark builds", () => {
it("covers every declared class-role pair exactly once with legal ranks", async () => {
@@ -17,7 +21,7 @@ describe("canonical benchmark builds", () => {
expect(new Set(actualPairs)).toEqual(new Set(expectedPairs));
for (const build of builds) {
expect(build.level).toBe(60);
expect(build.gearPresetId).toBe("coefficient-empty");
expect(build.gearPresetId).toBe("virtual-heirloom");
expect(build.inventory).toEqual([]);
expect(build.equipment).toEqual({});
expect(runtimeNormalizeTalentRanks(build.classId, build.level, build.talentRanks)).toEqual(build.talentRanks);
@@ -33,4 +37,19 @@ describe("canonical benchmark builds", () => {
"rom-priest:damage",
]);
});
it("keeps both named classic talent builds and marks Playerbots as the default", async () => {
const catalog = selectableBenchmarkBuildCatalog(60, ["mage"]);
const builds = await loadSelectableBenchmarkBuilds(60, ["mage"]);
expect(catalog).toHaveLength(6);
expect(builds).toHaveLength(6);
expect(new Set(builds.map((build) => build.characterName)).size).toBe(6);
for (const specialization of ["Arcane", "Fire", "Frost"]) {
const variants = builds.filter((build) => build.orientation === specialization);
expect(variants).toHaveLength(2);
expect(variants.filter((build) => build.defaultTalentBuild)).toHaveLength(1);
expect(variants.find((build) => build.defaultTalentBuild)?.talentBuildSource)
.toBe("azerothcore-playerbots@531282e");
}
}, 30_000);
});
+105 -15
View File
@@ -20,7 +20,13 @@ import {
import { aiHintsForAbility } from "../abilityRules";
import type { EquipmentAssignments } from "../equipment";
import { partyRolesForClass, type PartyRole } from "../partyRoles";
import { runtimePartyTalentRanksForLevel } from "../partyTalentRuntime";
import {
runtimeDefaultPartyTalentProfile,
runtimePartyTalentProvenanceForLevel,
runtimePartyTalentProfiles,
runtimePartyTalentRanksForLevel,
type PartyTalentRuntimeProfile,
} from "../partyTalentRuntime";
import { clampPlayerLevel } from "../progression";
import {
specializationsForClass,
@@ -38,9 +44,13 @@ import {
type TalentUiTree,
} from "../talentUiCatalog";
import type { BenchmarkBuildDefinition } from "./benchmarkTypes";
import {
readinessStatusFor,
type CompanionSpecReadinessStatus,
} from "../companionSpecReadiness";
export const CANONICAL_BENCHMARK_LEVEL = 60;
export const CANONICAL_BENCHMARK_GEAR_PRESET_ID = "coefficient-empty";
export const CANONICAL_BENCHMARK_GEAR_PRESET_ID = "virtual-heirloom";
const EMPTY_INVENTORY: readonly never[] = Object.freeze([]);
const EMPTY_EQUIPMENT: EquipmentAssignments = Object.freeze({});
@@ -164,9 +174,13 @@ function benchmarkVariantBuildId(
role: PartyRole,
orientation: string,
level: number,
talentProfile?: PartyTalentRuntimeProfile,
): string {
const variant = orientation.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
return `balance-${contentCategoryForClass(classId)}-${classId}-${role}-${variant}-level-${level}`;
const base = `balance-${contentCategoryForClass(classId)}-${classId}-${role}-${variant}-level-${level}`;
if (!talentProfile || talentProfile.defaultForSpecialization) return base;
const profile = talentProfile.id.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
return `${base}--${profile}`;
}
function commonBuildFields(classId: ClassId, role: PartyRole, level: number) {
@@ -192,10 +206,20 @@ function wowBuild(
if (!orientation) {
throw new Error(`No canonical WoW talent orientation exists for ${classId}:${role}.`);
}
const talentProfile = runtimeDefaultPartyTalentProfile(classId, orientation);
const talentProvenance = runtimePartyTalentProvenanceForLevel(talentProfile, level);
return Object.freeze({
...commonBuildFields(classId, role, level),
orientation,
talentRanks: runtimePartyTalentRanksForLevel(classId, orientation, level),
characterName: talentProfile.characterName,
talentBuildId: talentProfile.id,
talentBuildName: talentProfile.name,
talentBuildSource: `${talentProfile.source}@${talentProfile.sourceVersion}`,
talentBuildSourcePointCount: talentProvenance.sourcePointCount,
talentBuildSourcePointsRetained: talentProvenance.sourcePointsRetained,
talentBuildFallbackPointCount: talentProvenance.fallbackPointCount,
defaultTalentBuild: talentProfile.defaultForSpecialization,
talentRanks: runtimePartyTalentRanksForLevel(classId, orientation, level, talentProfile.id),
});
}
@@ -435,11 +459,18 @@ function selectableClassicBuild(
specialization: SpecializationDefinition,
level: number,
catalog: TalentUiCatalog | null,
talentProfile: PartyTalentRuntimeProfile,
): BenchmarkBuildDefinition {
const classId = specialization.classId as BaseClassId;
const talentProvenance = runtimePartyTalentProvenanceForLevel(talentProfile, level);
let talentRanks: RuntimeTalentRanks;
try {
talentRanks = runtimePartyTalentRanksForLevel(classId, specialization.displayName, level);
talentRanks = runtimePartyTalentRanksForLevel(
classId,
specialization.displayName,
level,
talentProfile.id,
);
} catch {
if (!catalog) throw new Error(`No talent catalog was loaded for ${classId}.`);
talentRanks = allocateSingleTreeTalentRanks(
@@ -451,8 +482,22 @@ function selectableClassicBuild(
}
return Object.freeze({
...commonBuildFields(classId, specialization.role, level),
id: benchmarkVariantBuildId(classId, specialization.role, specialization.displayName, level),
id: benchmarkVariantBuildId(
classId,
specialization.role,
specialization.displayName,
level,
talentProfile,
),
orientation: specialization.displayName,
characterName: talentProfile.characterName,
talentBuildId: talentProfile.id,
talentBuildName: talentProfile.name,
talentBuildSource: `${talentProfile.source}@${talentProfile.sourceVersion}`,
talentBuildSourcePointCount: talentProvenance.sourcePointCount,
talentBuildSourcePointsRetained: talentProvenance.sourcePointsRetained,
talentBuildFallbackPointCount: talentProvenance.fallbackPointCount,
defaultTalentBuild: talentProfile.defaultForSpecialization,
talentRanks,
});
}
@@ -486,7 +531,17 @@ export interface SelectableBenchmarkBuildCatalogEntry {
readonly classId: ClassId;
readonly role: PartyRole;
readonly orientation: string;
readonly specializationId?: string;
readonly readinessStatus?: CompanionSpecReadinessStatus;
readonly level: number;
readonly characterName?: string;
readonly talentBuildId?: string;
readonly talentBuildName?: string;
readonly talentBuildSource?: string;
readonly talentBuildSourcePointCount?: number | null;
readonly talentBuildSourcePointsRetained?: number | null;
readonly talentBuildFallbackPointCount?: number;
readonly defaultTalentBuild?: boolean;
}
/** Cheap metadata-only roster used by interactive tools before a build is selected. */
@@ -508,14 +563,42 @@ export function selectableBenchmarkBuildCatalog(
orientation: "Primary",
level: safeLevel,
}));
return specializationsForClass(classId).map((specialization) => ({
id: benchmarkVariantBuildId(classId, specialization.role, specialization.displayName, safeLevel),
source: contentCategoryForClass(classId),
classId,
role: specialization.role,
orientation: specialization.displayName,
level: safeLevel,
}));
return specializationsForClass(classId).flatMap((specialization) => {
const profiles = isBaseClassId(classId)
? runtimePartyTalentProfiles(classId, specialization.displayName)
: [undefined];
return profiles.map((talentProfile) => {
const talentProvenance = talentProfile
? runtimePartyTalentProvenanceForLevel(talentProfile, safeLevel)
: null;
return {
id: benchmarkVariantBuildId(
classId,
specialization.role,
specialization.displayName,
safeLevel,
talentProfile,
),
source: contentCategoryForClass(classId),
classId,
role: specialization.role,
orientation: specialization.displayName,
specializationId: isBaseClassId(classId) ? specialization.id : undefined,
readinessStatus: isBaseClassId(classId) ? readinessStatusFor(specialization.id) : undefined,
level: safeLevel,
characterName: talentProfile?.characterName,
talentBuildId: talentProfile?.id,
talentBuildName: talentProfile?.name,
talentBuildSource: talentProfile
? `${talentProfile.source}@${talentProfile.sourceVersion}`
: undefined,
talentBuildSourcePointCount: talentProvenance?.sourcePointCount,
talentBuildSourcePointsRetained: talentProvenance?.sourcePointsRetained,
talentBuildFallbackPointCount: talentProvenance?.fallbackPointCount,
defaultTalentBuild: talentProfile?.defaultForSpecialization,
};
});
});
});
}
@@ -561,7 +644,14 @@ export async function loadSelectableBenchmarkBuilds(
}
for (const specialization of specializationsForClass(classId)) {
if (isBaseClassId(classId)) {
builds.push(selectableClassicBuild(specialization, safeLevel, catalogs.get(classId) ?? null));
for (const talentProfile of runtimePartyTalentProfiles(classId, specialization.displayName)) {
builds.push(selectableClassicBuild(
specialization,
safeLevel,
catalogs.get(classId) ?? null,
talentProfile,
));
}
} else if (isCoaClassId(classId)) {
const catalog = catalogs.get(classId);
if (!catalog) throw new Error(`No loaded CoA talent catalog exists for ${classId}.`);
@@ -231,6 +231,7 @@ export class BenchmarkController {
health: member.health,
maxHealth: member.maxHealth,
fallen: member.health <= 0,
role: member.role,
})),
],
hostileCount: scenario.hostileTargets,
+8
View File
@@ -51,6 +51,14 @@ export interface BenchmarkBuildDefinition {
readonly role: PartyRole;
readonly level: number;
readonly orientation: string;
readonly characterName?: string;
readonly talentBuildId?: string;
readonly talentBuildName?: string;
readonly talentBuildSource?: string;
readonly talentBuildSourcePointCount?: number | null;
readonly talentBuildSourcePointsRetained?: number | null;
readonly talentBuildFallbackPointCount?: number;
readonly defaultTalentBuild?: boolean;
readonly talentRanks: Readonly<Record<string, number>>;
readonly inventory: readonly never[];
readonly equipment: EquipmentAssignments;