forked from phenom/healer-man
Update Healer Man server
This commit is contained in:
1 parent
39458d5de6
commit
6cc1b9217f
203 files changed
+133929
-31271
No files matched your search
@@ -0,0 +1,263 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user