Files
healer-man/src/game/romFiveRotation.integration.test.ts
T
2026-10-04 13:13:35 -04:00

184 lines
7.2 KiB
TypeScript

import { describe, expect, it } from "vitest";
import type { ClassId, RomClassId } from "../app/characterCatalog";
import type { AbilityDefinition } from "./abilityCatalog";
import { abilitiesForCharacter, learnedAbilityForBuild } from "./abilityCatalog";
import { aiHintsForAbility } from "./abilityRules";
import { evaluateCombatRotation, type RotationContext } from "./combatRotation";
import { nativeRotationAbilityAllowed, nativeRotationProfileFor } from "./romFiveRotation";
import type { PartyRole } from "./partyRoles";
const PRIMARY_RESOURCE: Readonly<Record<RomClassId, "mana" | "rage" | "energy" | "focus">> = {
"rom-warrior": "rage",
"rom-scout": "focus",
"rom-rogue": "energy",
"rom-mage": "mana",
"rom-priest": "mana",
"rom-knight": "mana",
"rom-warden": "mana",
"rom-druid": "mana",
"rom-warlock": "focus",
"rom-champion": "rage",
};
function nativeContext(
classId: RomClassId,
secondaryClassId: RomClassId,
role: PartyRole,
overrides: Partial<RotationContext> = {},
): RotationContext {
const abilities = abilitiesForCharacter(classId, secondaryClassId)
.map((ability) => learnedAbilityForBuild(ability, 30, {}))
.filter((ability): ability is AbilityDefinition => Boolean(ability && !ability.passive));
const knownDamagingAbilityNames = new Set(abilities.filter((ability) => {
const categories = aiHintsForAbility(ability).categories;
return categories.includes("damage") || categories.includes("execute");
}).map((ability) => ability.name));
return {
now: 1_000,
classId,
secondaryClassId,
statRuleset: "rom",
role,
level: 30,
health: 100,
maxHealth: 100,
selfId: `${classId}-player`,
resources: { mana: 100, rage: 100, energy: 100, focus: 100 },
maximumResources: { mana: 100, rage: 100, energy: 100, focus: 100 },
primaryResource: PRIMARY_RESOURCE[classId],
comboPoints: 0,
comboTargetId: null,
runes: [],
abilities,
knownDamagingAbilityNames,
hostileTarget: { id: "target", health: 20_000, maxHealth: 20_000, boss: true },
hostileCount: 1,
friendlyTargets: [],
...overrides,
};
}
function wowWarriorContext(): RotationContext {
const abilities = abilitiesForCharacter("warrior", null)
.map((ability) => learnedAbilityForBuild(ability, 80, {}))
.filter((ability): ability is AbilityDefinition => Boolean(ability && !ability.passive));
return {
now: 1_000,
classId: "warrior",
statRuleset: "wow",
specializationId: "warrior:protection",
role: "tank",
level: 80,
health: 100_000,
maxHealth: 100_000,
selfId: "wow-warrior",
resources: { rage: 100 },
maximumResources: { rage: 100 },
primaryResource: "rage",
comboPoints: 0,
comboTargetId: null,
runes: [],
abilities,
knownDamagingAbilityNames: new Set(abilities.filter((ability) => {
const categories = aiHintsForAbility(ability).categories;
return categories.includes("damage") || categories.includes("execute");
}).map((ability) => ability.name)),
hostileTarget: { id: "wow-target", health: 500_000, maxHealth: 500_000, boss: true },
hostileCount: 1,
};
}
describe("native RoM five profiles on the shared rotation evaluator", () => {
it.each([
["rom-knight", "rom-scout", "tank"],
["rom-priest", "rom-knight", "healer"],
["rom-rogue", "rom-warrior", "damage"],
["rom-scout", "rom-rogue", "damage"],
["rom-mage", "rom-warlock", "damage"],
] as const)("selects a learned native action for %s/%s as %s at level 30", (primary, secondary, role) => {
const context = nativeContext(primary, secondary, role, {
...(role === "tank" ? { targetIsLoose: true } : {}),
...(role === "healer" ? {
friendlyTargets: [
{ id: "tank", health: 55, maxHealth: 100, role: "tank" },
{ id: "scout", health: 75, maxHealth: 100, role: "damage" },
],
} : {}),
});
const profile = nativeRotationProfileFor(context);
const decision = evaluateCombatRotation(context);
expect(profile).not.toBeNull();
expect(decision).not.toBeNull();
expect(decision?.profileId).toBe(profile?.id);
expect(context.abilities.some((ability) => ability.name === decision?.ability.name)).toBe(true);
});
it("uses Group Heal on multiple injured allies when the priest is healthy", () => {
const context = nativeContext("rom-priest", "rom-knight", "healer", {
friendlyTargets: [
{ id: "tank", health: 65, maxHealth: 100, role: "tank" },
{ id: "scout", health: 75, maxHealth: 100, role: "damage" },
],
});
const groupHeal = context.abilities.find((ability) => ability.name === "Group Heal");
expect(groupHeal).toBeDefined();
expect(nativeRotationAbilityAllowed(groupHeal!, context)).toBe(true);
const decision = evaluateCombatRotation({ ...context, abilities: [groupHeal!] });
expect(decision?.ability.name).toBe("Group Heal");
expect(decision?.targetId).toBe(context.selfId);
});
it("keeps Urgent Heal ahead of Group Heal when one ally is critical", () => {
const context = nativeContext("rom-priest", "rom-knight", "healer", {
friendlyTargets: [
{ id: "tank", health: 20, maxHealth: 100, role: "tank" },
{ id: "scout", health: 70, maxHealth: 100, role: "damage" },
],
});
const urgentHeal = context.abilities.find((ability) => ability.name === "Urgent Heal");
const groupHeal = context.abilities.find((ability) => ability.name === "Group Heal");
expect(urgentHeal).toBeDefined();
expect(groupHeal).toBeDefined();
const decision = evaluateCombatRotation({ ...context, abilities: [groupHeal!, urgentHeal!] });
expect(decision?.ability.name).toBe("Urgent Heal");
expect(decision?.targetId).toBe("tank");
});
it("refreshes the Scout/Rogue DoT only for the current caster, target, and expiry window", () => {
const context = nativeContext("rom-scout", "rom-rogue", "damage");
const vampireArrows = context.abilities.find((ability) => ability.name === "Vampire Arrows");
expect(vampireArrows).toBeDefined();
const maintained = (sourceActorId: string, targetId: string, remainingMs: number) => [{
abilityId: vampireArrows!.id,
sourceActorId,
targetId,
remainingMs,
}];
expect(nativeRotationAbilityAllowed(vampireArrows!, {
...context,
maintainedPeriodicEffects: maintained(context.selfId!, "target", 10_000),
})).toBe(false);
expect(nativeRotationAbilityAllowed(vampireArrows!, {
...context,
maintainedPeriodicEffects: maintained("other-scout", "target", 10_000),
})).toBe(true);
expect(nativeRotationAbilityAllowed(vampireArrows!, {
...context,
maintainedPeriodicEffects: maintained(context.selfId!, "other-target", 10_000),
})).toBe(true);
expect(nativeRotationAbilityAllowed(vampireArrows!, {
...context,
maintainedPeriodicEffects: maintained(context.selfId!, "target", 1_000),
})).toBe(true);
});
it("leaves the WoW class profile selector on its original path", () => {
const context = wowWarriorContext();
expect(nativeRotationProfileFor(context)).toBeNull();
const decision = evaluateCombatRotation(context);
expect(decision).not.toBeNull();
expect(decision?.profileId).toBe("classic:warrior:protection");
});
});