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> = { "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 { 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"); }); });