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

264 lines
9.7 KiB
TypeScript

import { describe, expect, it } from "vitest";
import type { AbilityDefinition } from "./abilityCatalog";
import type { RotationContext } from "./combatRotation";
import {
NATIVE_ROTATION_PROFILES,
ROM_ROTATION_EFFECT_REFRESH_WINDOW_MS,
nativeRotationAbilityAllowed,
nativeRotationProfileFor,
} from "./romFiveRotation";
function context(overrides: Partial<RotationContext> = {}): RotationContext {
return {
now: 1_000,
classId: "rom-priest",
secondaryClassId: "rom-knight",
statRuleset: "rom",
role: "healer",
level: 30,
health: 100,
maxHealth: 100,
selfId: "serenna",
resources: { mana: 100 },
maximumResources: { mana: 100 },
primaryResource: "mana",
comboPoints: 0,
comboTargetId: null,
runes: [],
abilities: [],
hostileTarget: { id: "target", health: 500, maxHealth: 500 },
friendlyTargets: [],
hostileCount: 1,
...overrides,
};
}
function ability(
name: string,
overrides: Partial<AbilityDefinition> = {},
): AbilityDefinition {
return {
id: `test-${name}`,
classId: "rom-priest",
dbcSpellId: 1,
name,
unlockLevel: 1,
icon: "",
sigil: "",
description: "test fixture",
target: "friendly",
range: { min: 0, max: 30 },
castMode: "instant",
castTimeMs: 0,
cooldownMs: 0,
gcdMs: 0,
cost: { resource: "mana", amount: 0 },
effects: [],
...overrides,
};
}
describe("native RoM five rotation profiles", () => {
it("only resolves exact pair, role, RoM ruleset, and secondary-class levels", () => {
expect(nativeRotationProfileFor(context())).toMatchObject({
id: "rom-five:rom-priest+rom-knight:healer",
classId: "rom-priest",
secondaryClassId: "rom-knight",
role: "healer",
});
expect(nativeRotationProfileFor(context({ secondaryClassId: "rom-scout" }))).toBeNull();
expect(nativeRotationProfileFor(context({ classId: "rom-knight", secondaryClassId: "rom-scout" }))).toBeNull();
expect(nativeRotationProfileFor(context({ role: "damage" }))).toBeNull();
expect(nativeRotationProfileFor(context({ statRuleset: "wow" }))).toBeNull();
expect(nativeRotationProfileFor(context({ secondaryClassId: null }))).toBeNull();
expect(nativeRotationProfileFor(context({ level: 9 }))).toBeNull();
});
it("keeps the five ordered pairs and a damaging fallback tail for the same build", () => {
expect(NATIVE_ROTATION_PROFILES.map(({ classId, secondaryClassId, role }) => [
classId,
secondaryClassId,
role,
])).toEqual([
["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"],
]);
const profile = nativeRotationProfileFor(context({
knownDamagingAbilityNames: new Set(["Current CoA Ability", "Urgent Heal"]),
}));
expect(profile?.abilityPriority.slice(0, 2)).toEqual(["Urgent Heal", "Group Heal"]);
expect(profile?.abilityPriority.at(-1)).toBe("Current CoA Ability");
});
});
describe("native RoM rotation action guards", () => {
it("uses taunts only for a tank's actually loose target and gates shield spend defensively", () => {
const arrow = ability("Arrow of Vengeance", { classId: "rom-knight" });
expect(nativeRotationAbilityAllowed(arrow, context({
classId: "rom-knight",
secondaryClassId: "rom-scout",
role: "tank",
}))).toBe(false);
expect(nativeRotationAbilityAllowed(arrow, context({
classId: "rom-knight",
secondaryClassId: "rom-scout",
role: "tank",
targetIsLoose: true,
}))).toBe(true);
const shieldBash = ability("Truth Shield Bash", { classId: "rom-knight" });
expect(nativeRotationAbilityAllowed(shieldBash, context({
classId: "rom-knight",
secondaryClassId: "rom-scout",
role: "tank",
}))).toBe(false);
expect(nativeRotationAbilityAllowed(shieldBash, context({
classId: "rom-knight",
secondaryClassId: "rom-scout",
role: "tank",
incomingMeleeDanger: true,
}))).toBe(true);
});
it("keeps emergency single heals fast and requires multiple injured allies for Group Heal", () => {
const urgent = ability("Urgent Heal");
const group = ability("Group Heal");
const oneInjured = [{ id: "tank", health: 30, maxHealth: 100, role: "tank" as const }];
expect(nativeRotationAbilityAllowed(urgent, context({ friendlyTargets: oneInjured }))).toBe(true);
expect(nativeRotationAbilityAllowed(urgent, context({
friendlyTargets: [{ id: "tank", health: 70, maxHealth: 100, role: "tank" }],
}))).toBe(false);
expect(nativeRotationAbilityAllowed(group, context({ friendlyTargets: oneInjured }))).toBe(false);
expect(nativeRotationAbilityAllowed(group, context({
friendlyTargets: [
{ id: "tank", health: 70, maxHealth: 100, role: "tank" },
{ id: "scout", health: 80, maxHealth: 100, role: "damage" },
],
}))).toBe(true);
});
it("refreshes a target-owned periodic effect only inside its expiry window", () => {
const dot = ability("Soul Pain", {
classId: "rom-warlock",
secondaryClassId: "rom-mage",
target: "hostile",
effects: [{ kind: "dot", coefficient: 1, ticks: 12, intervalMs: 2_000, tag: "soul-pain" }],
});
const effect = (remainingMs: number, sourceActorId = "serenna", targetId = "target") => ({
abilityId: dot.id,
sourceActorId,
targetId,
remainingMs,
});
expect(nativeRotationAbilityAllowed(dot, context({
classId: "rom-mage",
secondaryClassId: "rom-warlock",
role: "damage",
selfId: "durnik",
maintainedPeriodicEffects: [effect(ROM_ROTATION_EFFECT_REFRESH_WINDOW_MS + 1, "durnik")],
}))).toBe(false);
expect(nativeRotationAbilityAllowed(dot, context({
classId: "rom-mage",
secondaryClassId: "rom-warlock",
role: "damage",
selfId: "durnik",
maintainedPeriodicEffects: [effect(ROM_ROTATION_EFFECT_REFRESH_WINDOW_MS, "durnik")],
}))).toBe(true);
expect(nativeRotationAbilityAllowed(dot, context({
classId: "rom-mage",
secondaryClassId: "rom-warlock",
role: "damage",
selfId: "durnik",
maintainedAbilityIds: new Set([dot.id]),
maintainedPeriodicEffects: [effect(10_000, "another-caster")],
}))).toBe(true);
expect(nativeRotationAbilityAllowed(dot, context({
classId: "rom-mage",
secondaryClassId: "rom-warlock",
role: "damage",
selfId: "durnik",
maintainedPeriodicEffects: [effect(10_000, "durnik", "another-target")],
}))).toBe(true);
expect(nativeRotationAbilityAllowed(dot, context({
classId: "rom-mage",
secondaryClassId: "rom-warlock",
role: "damage",
selfId: "durnik",
hostileTarget: { id: "small-target", health: 100, maxHealth: 100 },
}))).toBe(false);
});
it("does not spend old health-cost Sacrificial Cure data and only bursts on durable targets", () => {
const staleCure = ability("Sacrificial Cure", {
cost: { resource: "health", amount: 70, percentage: true },
});
expect(nativeRotationAbilityAllowed(staleCure, context({
friendlyTargets: [{ id: "tank", health: 10, maxHealth: 100 }],
}))).toBe(false);
const burst = ability("Assassins Rage", { classId: "rom-rogue" });
expect(nativeRotationAbilityAllowed(burst, context({
classId: "rom-rogue",
secondaryClassId: "rom-warrior",
role: "damage",
hostileTarget: { id: "small-mob", health: 150, maxHealth: 150 },
}))).toBe(false);
expect(nativeRotationAbilityAllowed(burst, context({
classId: "rom-rogue",
secondaryClassId: "rom-warrior",
role: "damage",
hostileTarget: { id: "boss", health: 10_000, maxHealth: 10_000, boss: true },
}))).toBe(true);
});
it("waits for Vampire Arrows before Deadly Poison Bite and applies AoE minimums", () => {
const bite = ability("Deadly Poison Bite", { classId: "rom-scout" });
const scout = {
classId: "rom-scout" as const,
secondaryClassId: "rom-rogue" as const,
role: "damage" as const,
selfId: "caelan",
};
expect(nativeRotationAbilityAllowed(bite, context(scout))).toBe(false);
expect(nativeRotationAbilityAllowed(bite, context({
...scout,
maintainedPeriodicEffects: [{
abilityId: "rom-scout-general-2",
sourceActorId: "caelan",
targetId: "target",
remainingMs: 5_000,
}],
}))).toBe(true);
const thunderstorm = ability("Thunderstorm", { classId: "rom-mage" });
const mage = { classId: "rom-mage" as const, secondaryClassId: "rom-warlock" as const, role: "damage" as const };
expect(nativeRotationAbilityAllowed(thunderstorm, context(mage))).toBe(false);
expect(nativeRotationAbilityAllowed(thunderstorm, context({ ...mage, hostileCount: 2 }))).toBe(true);
const areaAttack = ability("Current CoA area attack", {
classId: "rom-mage",
secondaryClassId: "rom-warlock",
target: "hostile",
effects: [{ kind: "damage", coefficient: 1 }],
targeting: { shape: "circle", radius: 8 },
});
expect(nativeRotationAbilityAllowed(areaAttack, context(mage))).toBe(false);
expect(nativeRotationAbilityAllowed(areaAttack, context({ ...mage, hostileCount: 2 }))).toBe(true);
});
it("leaves unsupported and pre-secondary builds to the existing generic fallback", () => {
const abilityUnderTest = ability("Holy Strike", { classId: "rom-knight" });
expect(nativeRotationAbilityAllowed(abilityUnderTest, context({ level: 9 }))).toBe(true);
expect(nativeRotationAbilityAllowed(abilityUnderTest, context({
classId: "rom-knight",
secondaryClassId: "rom-priest",
role: "tank",
}))).toBe(true);
});
});