Files
healer-man/src/game/coaPeriodicRotationRuntime.test.ts
T
2026-10-06 22:25:34 -04:00

176 lines
7.4 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { abilitiesForClass } from "./abilityCatalog";
import { targetingRulesForAbility } from "./abilityRules";
import {
coaPeriodicActionMaintainedOnTarget,
usesCoaTargetedPeriodicMaintenance,
} from "./coaPeriodicRotationRuntime";
import { evaluateCombatRotation, type RotationContext } from "./combatRotation";
function coaPeriodicAbility(classId: "ranger" | "witch-doctor", name: string) {
const ability = abilitiesForClass(classId).find((candidate) => candidate.name === name
&& candidate.effects.some((effect) => effect.kind === "dot" || effect.kind === "hot"));
if (!ability) throw new Error(`Missing CoA ${classId} periodic ability ${name}.`);
return ability;
}
function contextFor(
classId: RotationContext["classId"],
ability: RotationContext["abilities"][number],
options: {
readonly selfId?: string;
readonly targetId?: string;
readonly targetHealth?: number;
readonly maxTargetHealth?: number;
readonly friendlyTargets?: RotationContext["friendlyTargets"];
readonly maintainedPeriodicEffects?: RotationContext["maintainedPeriodicEffects"];
} = {},
): RotationContext {
const primaryResource = classId === "ranger" ? "focus" : classId === "barbarian" ? "energy" : "mana";
return {
now: 5_000,
classId,
specializationId: classId === "ranger" ? "ranger:archery" : "witch-doctor:brewing",
role: classId === "witch-doctor" ? "healer" : "damage",
level: 60,
health: 1_000,
maxHealth: 1_000,
selfId: options.selfId ?? "coa-periodic-caster",
resources: { [primaryResource]: 100 },
maximumResources: { [primaryResource]: 100 },
primaryResource,
comboPoints: 0,
comboTargetId: null,
runes: [],
abilities: [ability],
hostileTarget: options.targetId && ability.target !== "friendly"
? { id: options.targetId, health: options.targetHealth ?? 800, maxHealth: options.maxTargetHealth ?? 1_000 }
: null,
friendlyTargets: options.friendlyTargets,
maintainedAbilityIds: new Set([ability.id]),
maintainedPeriodicEffects: options.maintainedPeriodicEffects,
};
}
describe("target-specific CoA periodic maintenance", () => {
it("keeps a live Ranger DoT on enemy A from blocking the same cast on enemy B", () => {
const serratedShot = coaPeriodicAbility("ranger", "Serrated Shot");
expect(serratedShot.source).toMatch(/^coa-/);
expect(serratedShot.target).toBe("hostile");
expect(targetingRulesForAbility(serratedShot).shape).toBe("unit");
const onA = contextFor("ranger", serratedShot, {
targetId: "enemy-b",
maintainedPeriodicEffects: [{
abilityId: serratedShot.id,
sourceActorId: "coa-periodic-caster",
targetId: "enemy-a",
remainingMs: 2_000,
}],
});
expect(onA.maintainedAbilityIds?.has(serratedShot.id)).toBe(true);
expect(usesCoaTargetedPeriodicMaintenance(serratedShot, onA)).toBe(true);
expect(coaPeriodicActionMaintainedOnTarget(serratedShot, onA, "enemy-b")).toBe(false);
expect(evaluateCombatRotation(onA)).toMatchObject({ ability: { id: serratedShot.id }, targetId: "enemy-b" });
const onB = contextFor("ranger", serratedShot, {
targetId: "enemy-b",
maintainedPeriodicEffects: [{
abilityId: serratedShot.id,
sourceActorId: "coa-periodic-caster",
targetId: "enemy-b",
remainingMs: 2_000,
}],
});
expect(coaPeriodicActionMaintainedOnTarget(serratedShot, onB, "enemy-b")).toBe(true);
expect(evaluateCombatRotation(onB)).toBeNull();
});
it("uses the intended friendly recipient and ignores foreign or expired HoTs", () => {
const mojoBeam = coaPeriodicAbility("witch-doctor", "Mojo Beam");
expect(mojoBeam.source).toMatch(/^coa-/);
expect(mojoBeam.target).toBe("friendly");
expect(targetingRulesForAbility(mojoBeam).shape).toBe("unit");
const casterOnlyHot = {
...mojoBeam,
effects: [{ kind: "hot" as const, coefficient: 1, ticks: 2, intervalMs: 1, recipient: "caster" as const }],
};
expect(usesCoaTargetedPeriodicMaintenance(casterOnlyHot)).toBe(false);
const allies = [
{ id: "ally-a", health: 700, maxHealth: 1_000, role: "tank" as const },
{ id: "ally-b", health: 400, maxHealth: 1_000, role: "damage" as const },
];
const onAllyA = contextFor("witch-doctor", mojoBeam, {
friendlyTargets: allies,
maintainedPeriodicEffects: [{
abilityId: mojoBeam.id,
sourceActorId: "coa-periodic-caster",
targetId: "ally-a",
remainingMs: 2_000,
}],
});
expect(onAllyA.friendlyTargets?.[1]?.id).toBe("ally-b");
expect(coaPeriodicActionMaintainedOnTarget(mojoBeam, onAllyA, "ally-b")).toBe(false);
expect(evaluateCombatRotation(onAllyA)).toMatchObject({ ability: { id: mojoBeam.id }, targetId: "ally-b" });
const foreign = contextFor("witch-doctor", mojoBeam, {
friendlyTargets: allies,
maintainedPeriodicEffects: [{
abilityId: mojoBeam.id,
sourceActorId: "another-witch-doctor",
targetId: "ally-b",
remainingMs: 2_000,
}],
});
expect(usesCoaTargetedPeriodicMaintenance(mojoBeam, foreign)).toBe(true);
expect(coaPeriodicActionMaintainedOnTarget(mojoBeam, foreign, "ally-b")).toBe(false);
expect(evaluateCombatRotation(foreign)).toMatchObject({ ability: { id: mojoBeam.id }, targetId: "ally-b" });
const onRecipient = contextFor("witch-doctor", mojoBeam, {
friendlyTargets: allies,
maintainedPeriodicEffects: [{
abilityId: mojoBeam.id,
sourceActorId: "coa-periodic-caster",
targetId: "ally-b",
remainingMs: 2_000,
}],
});
expect(evaluateCombatRotation(onRecipient)).toBeNull();
const expired = contextFor("witch-doctor", mojoBeam, {
friendlyTargets: allies,
maintainedPeriodicEffects: [{
abilityId: mojoBeam.id,
sourceActorId: "coa-periodic-caster",
targetId: "ally-b",
remainingMs: 0,
}],
});
expect(coaPeriodicActionMaintainedOnTarget(mojoBeam, expired, "ally-b")).toBe(false);
expect(evaluateCombatRotation(expired)).toMatchObject({ ability: { id: mojoBeam.id }, targetId: "ally-b" });
});
it("leaves non-CoA and area periodic abilities on their existing maintenance path", () => {
const moonfire = abilitiesForClass("druid").find((ability) => ability.name === "Moonfire");
if (!moonfire) throw new Error("Missing WoW Moonfire.");
const wow = contextFor("druid", moonfire, { targetId: "enemy-b" });
const wowMaintenance = {
...wow,
specializationId: "druid:balance",
maintainedPeriodicEffects: undefined,
} satisfies RotationContext;
expect(usesCoaTargetedPeriodicMaintenance(moonfire, wowMaintenance)).toBe(false);
expect(evaluateCombatRotation({ ...wowMaintenance, maintainedAbilityIds: undefined })?.ability.id).toBe(moonfire.id);
expect(evaluateCombatRotation({
...wowMaintenance,
maintainedAbilityIds: new Set([moonfire.id]),
})).toBeNull();
const areaHot = abilitiesForClass("witch-doctor").find((ability) => ability.name === "Splash Potion");
if (!areaHot) throw new Error("Missing CoA area periodic action Splash Potion.");
const areaContext = contextFor("witch-doctor", areaHot, { targetId: "enemy-b" });
expect(targetingRulesForAbility(areaHot).shape).not.toBe("unit");
expect(usesCoaTargetedPeriodicMaintenance(areaHot, areaContext)).toBe(false);
});
});