Update Healer Man server

This commit is contained in:
phenom committed 2026-09-19 21:01:51 -04:00
1 parent f8314e7de1
commit 1fedb9368d
76 files changed
+20556 -2523

No files matched your search

+142
View File
@@ -143,6 +143,33 @@ describe("shared combat rotations", () => {
expect(abilityIsPrecombatPreparation(temporaryPet)).toBe(false);
});
it("uses an eligible defensive before a configured incoming boss mechanic", () => {
const defensive = ability("paladin", "Divine Protection");
const decision = evaluateCombatRotation(context({
classId: "paladin",
specializationId: "paladin:protection",
role: "tank",
primaryResource: "mana",
resources: { mana: 10_000 },
maximumResources: { mana: 10_000 },
abilities: [defensive],
incomingDefensiveMechanic: {
id: "tank-buster",
enabled: true,
bossKey: "test/boss",
bossName: "Test Boss",
bossServerEntry: 1,
attackId: "heavy-strike",
attackName: "Heavy Strike",
scope: "tank",
leadTimeMs: 2_500,
minimumCooldownSeconds: 20,
},
}));
expect(decision?.ability.name).toBe("Divine Protection");
expect(decision?.ruleId).toBe("preemptive-tank-buster");
});
it("gates representative signature abilities from every class by their exact talent node", () => {
for (const [classId, name] of Object.entries(SIGNATURES)) {
const signature = ability(classId as RotationContext["classId"], name);
@@ -178,6 +205,67 @@ describe("shared combat rotations", () => {
}
});
it("keeps every required Ready-spec mechanic in its learned Playerbots profile", () => {
const contracts = [
{ classId: "warrior" as const, specialization: "Protection", id: "warrior:protection", actions: ["Defensive Stance", "Taunt", "Shield Bash", "Shield Slam", "Revenge", "Devastate", "Thunder Clap", "Shield Block", "Shield Wall", "Last Stand", "Enraged Regeneration"] },
{ classId: "priest" as const, specialization: "Discipline", id: "priest:discipline", actions: ["Penance", "Power Word: Shield", "Flash Heal", "Greater Heal", "Renew", "Dispel Magic", "Cure Disease", "Resurrection", "Pain Suppression", "Inner Focus", "Power Infusion", "Shadowfiend"] },
{ classId: "hunter" as const, specialization: "Marksmanship", id: "hunter:marksmanship", actions: ["Call Pet", "Aspect of the Dragonhawk", "Serpent Sting", "Chimera Shot", "Aimed Shot", "Arcane Shot", "Steady Shot", "Kill Shot", "Rapid Fire", "Readiness", "Misdirection", "Feign Death", "Silencing Shot", "Disengage"] },
{ classId: "mage" as const, specialization: "Fire", id: "mage:fire", actions: ["Molten Armor", "Living Bomb", "Fireball", "Scorch", "Fire Blast", "Combustion", "Mirror Image", "Counterspell", "Fire Ward", "Frost Ward", "Ice Block", "Blink"] },
{ classId: "rogue" as const, specialization: "Assassination", id: "rogue:assassination", actions: ["Instant Poison", "Deadly Poison", "Mutilate", "Rupture", "Envenom", "Cold Blood", "Kick", "Tricks of the Trade", "Evasion", "Cloak of Shadows", "Vanish"] },
];
for (const contract of contracts) {
const profile = rotationProfileFor(contract.classId, contract.id)!;
const talents = runtimePartyTalentRanksForLevel(contract.classId, contract.specialization, 80);
for (const name of contract.actions) {
expect(profile.abilityPriority, `${contract.id}:${name} priority`).toContain(name);
expect(learnedAbilityForBuild(ability(contract.classId, name), 80, talents), `${contract.id}:${name} learned`)
.not.toBeNull();
}
}
});
it("executes Ready-spec taunts, dispels, resurrection, interrupts, threat redirects, and movement reactively", () => {
const warriorTalents = runtimePartyTalentRanksForLevel("warrior", "Protection", 80);
const taunt = learnedAbilityForBuild(ability("warrior", "Taunt"), 80, warriorTalents)!;
expect(evaluateCombatRotation(context({ classId: "warrior", specializationId: "warrior:protection", role: "tank", primaryResource: "rage", resources: { rage: 100 }, maximumResources: { rage: 100 }, abilities: [taunt], targetIsLoose: true }))?.ability.name)
.toBe("Taunt");
const priestTalents = runtimePartyTalentRanksForLevel("priest", "Discipline", 80);
const dispel = learnedAbilityForBuild(ability("priest", "Dispel Magic"), 80, priestTalents)!;
const resurrection = learnedAbilityForBuild(ability("priest", "Resurrection"), 80, priestTalents)!;
const priest = context({ classId: "priest", specializationId: "priest:discipline", role: "healer", primaryResource: "mana", resources: { mana: 10_000 }, maximumResources: { mana: 10_000 } });
expect(evaluateCombatRotation({ ...priest, abilities: [dispel], friendlyTargets: [{ id: "ally", health: 500, maxHealth: 1_000, needsDispel: true }] })?.ability.name)
.toBe("Dispel Magic");
expect(evaluateCombatRotation({ ...priest, abilities: [resurrection], friendlyTargets: [{ id: "ally", health: 0, maxHealth: 1_000, fallen: true }] })?.ability.name)
.toBe("Resurrection");
for (const row of [
{ classId: "hunter" as const, specializationId: "hunter:marksmanship", specialization: "Marksmanship", name: "Silencing Shot", resource: "mana" as const },
{ classId: "mage" as const, specializationId: "mage:fire", specialization: "Fire", name: "Counterspell", resource: "mana" as const },
{ classId: "rogue" as const, specializationId: "rogue:assassination", specialization: "Assassination", name: "Kick", resource: "energy" as const },
]) {
const learned = learnedAbilityForBuild(ability(row.classId, row.name), 80,
runtimePartyTalentRanksForLevel(row.classId, row.specialization, 80))!;
expect(evaluateCombatRotation(context({
classId: row.classId,
specializationId: row.specializationId,
primaryResource: row.resource,
resources: { [row.resource]: 10_000 },
maximumResources: { [row.resource]: 10_000 },
abilities: [learned],
hostileTarget: { id: "caster", health: 10_000, maxHealth: 10_000, activeCast: { interruptible: true } },
}))?.ability.name).toBe(row.name);
}
const hunterTalents = runtimePartyTalentRanksForLevel("hunter", "Marksmanship", 80);
const misdirection = learnedAbilityForBuild(ability("hunter", "Misdirection"), 80, hunterTalents)!;
const disengage = learnedAbilityForBuild(ability("hunter", "Disengage"), 80, hunterTalents)!;
const hunter = context({ classId: "hunter", specializationId: "hunter:marksmanship", primaryResource: "mana", resources: { mana: 10_000 }, maximumResources: { mana: 10_000 }, friendlyTargets: [{ id: "tank", health: 1_000, maxHealth: 1_000, role: "tank" }] });
expect(evaluateCombatRotation({ ...hunter, abilities: [misdirection], hostileTarget: { id: "boss", health: 100_000, maxHealth: 100_000, boss: true } }))
.toMatchObject({ ability: { name: "Misdirection" }, targetId: "tank" });
expect(evaluateCombatRotation({ ...hunter, abilities: [disengage], moving: true })?.ability.name).toBe("Disengage");
});
it("keeps specialization talent actions isolated", () => {
const furyTalents = runtimePartyTalentRanksForLevel("warrior", "Fury", 80);
const armsTalents = runtimePartyTalentRanksForLevel("warrior", "Arms", 80);
@@ -189,6 +277,60 @@ describe("shared combat rotations", () => {
expect(learnedAbilityForBuild(bloodthirst, 80, armsTalents)).toBeNull();
});
it("certifies major offensive and defensive cooldown decisions for the Ready five", () => {
const cases = [
{ classId: "warrior" as const, specialization: "Protection", specializationId: "warrior:protection", offensive: null, defensive: "Shield Wall", role: "tank" as const },
{ classId: "priest" as const, specialization: "Discipline", specializationId: "priest:discipline", offensive: "Power Infusion", defensive: "Pain Suppression", role: "healer" as const },
{ classId: "hunter" as const, specialization: "Marksmanship", specializationId: "hunter:marksmanship", offensive: "Rapid Fire", defensive: "Feign Death", role: "damage" as const },
{ classId: "mage" as const, specialization: "Fire", specializationId: "mage:fire", offensive: "Combustion", defensive: "Ice Block", role: "damage" as const },
{ classId: "rogue" as const, specialization: "Assassination", specializationId: "rogue:assassination", offensive: "Cold Blood", defensive: "Evasion", role: "damage" as const },
];
for (const row of cases) {
const talents = runtimePartyTalentRanksForLevel(row.classId, row.specialization, 80);
const learned = (name: string) => learnedAbilityForBuild(ability(row.classId, name), 80, talents)!;
const primaryResource = resourceProfileForClass(row.classId).type;
const shared = context({
classId: row.classId,
specializationId: row.specializationId,
specialization: row.specialization,
role: row.role,
primaryResource,
resources: { [primaryResource]: 100_000 },
maximumResources: { [primaryResource]: 100_000 },
hostileTarget: { id: "boss", health: 100_000, maxHealth: 100_000, boss: true },
friendlyTargets: [{ id: "tank", health: 1_000, maxHealth: 1_000, role: "tank" }],
});
if (row.offensive) {
const offensive = learned(row.offensive);
expect(offensive, `${row.specializationId}:${row.offensive}`).toBeDefined();
expect(evaluateCombatRotation({ ...shared, abilities: [offensive] })?.ability.name)
.toBe(row.offensive);
}
const defensive = learned(row.defensive);
expect(defensive, `${row.specializationId}:${row.defensive}`).toBeDefined();
const defensiveContext = row.defensive === "Feign Death"
? { ...shared, abilities: [defensive], hasDangerousThreat: true }
: { ...shared, abilities: [defensive], health: 250 };
expect(evaluateCombatRotation(defensiveContext)?.ability.name).toBe(row.defensive);
}
});
it("uses Marksmanship Readiness only on a durable target", () => {
const talents = runtimePartyTalentRanksForLevel("hunter", "Marksmanship", 80);
const readiness = learnedAbilityForBuild(ability("hunter", "Readiness"), 80, talents)!;
const base = context({
classId: "hunter",
specializationId: "hunter:marksmanship",
primaryResource: "mana",
resources: { mana: 10_000 },
maximumResources: { mana: 10_000 },
abilities: [readiness],
});
expect(evaluateCombatRotation(base)).toBeNull();
expect(evaluateCombatRotation({ ...base, hostileTarget: { id: "boss", health: 100_000, maxHealth: 100_000, boss: true } })?.ability.name)
.toBe("Readiness");
});
it("uses combo builders before finishers and actual combo points for the decision", () => {
const builder = ability("rogue", "Sinister Strike");
const finisher = ability("rogue", "Eviscerate");