Files
healer-man/src/game/wow335PaladinRuntime.test.ts
T
2026-09-30 09:57:13 -04:00

796 lines
34 KiB
TypeScript

import { beforeEach, describe, expect, it } from "vitest";
import { clearMobRuntimeRegistry, setMobPosition } from "./mobRuntimeRegistry";
import { clearPartyRuntimeRegistry, updatePartyRuntimePosition } from "./partyRuntimeRegistry";
import { characterCombatDefender, PLAYER_AURA_ENTITY_ID, useCombatStore } from "./combatStore";
import { usePartyStore } from "./partyStore";
import { abilityTalentNodeId, preloadAbilityCatalogsForClasses } from "./abilityCatalog";
import { applyAura } from "./combatAuras";
import { EMPTY_ITEM_STATS } from "./itemStats";
import { WOW335_TALENT_NODES } from "./wow335Talents.generated";
function abilityId(name: string): string {
const ability = useCombatStore.getState().abilities.find((candidate) => candidate.name === name);
expect(ability, name).toBeDefined();
return ability!.id;
}
function unlockTalentAbility(name: string): string {
const ability = useCombatStore.getState().abilities.find((candidate) => candidate.name === name);
expect(ability, name).toBeDefined();
const nodeId = abilityTalentNodeId(ability!);
expect(nodeId, name).toBeTruthy();
useCombatStore.setState((state) => ({ talentRanks: { ...state.talentRanks, [nodeId!]: 1 } }));
return ability!.id;
}
function setTalentRank(name: string, rank: number): void {
const talent = WOW335_TALENT_NODES.find((candidate) => candidate.name === name);
expect(talent, name).toBeDefined();
useCombatStore.setState((state) => ({
talentRanks: { ...state.talentRanks, [talent!.id]: rank },
}));
}
describe("WoW 3.3.5 Paladin source-backed runtime mechanics", () => {
beforeEach(async () => {
clearMobRuntimeRegistry();
clearPartyRuntimeRegistry();
await preloadAbilityCatalogsForClasses(["paladin"]);
useCombatStore.getState().initializeCharacter({ classId: "paladin", level: 80 });
useCombatStore.getState().resetSettings();
useCombatStore.getState().setPlayerPosition([0, 0, 0]);
usePartyStore.getState().initializeParty("wow335-paladin-runtime-tests", 80, useCombatStore.getState().health);
useCombatStore.setState((state) => ({
resource: state.maxResource,
resourcePools: { ...state.resourcePools, mana: state.maxResourcePools.mana },
}));
});
it("blocks silence with Aura Mastery for its sourced 10-second window, then allows silence after expiry", () => {
const mobId = "paladin-aura-mastery-silence";
useCombatStore.getState().registerMob(mobId, {
name: "Silence attacker",
maxHealth: 10_000,
attacks: [{
id: "silence",
name: "Silence",
delivery: "area",
target: "primary",
school: "shadow",
animation: "cast",
castTimeMs: 0,
range: 30,
cooldownMs: 10_000,
initialCooldownMs: [0, 0],
repeatCooldownMs: [10_000, 10_000],
damageMultiplier: 0,
effect: { kind: "control", mechanic: "silence", durationMs: 3_000 },
}],
});
setMobPosition(mobId, [2, 0, 0]);
useCombatStore.getState().selectMob(mobId);
useCombatStore.getState().damageMob(mobId, 1, 1_000);
const activate = useCombatStore.getState().castAbility(unlockTalentAbility("Aura Mastery"), undefined, 1_000);
expect(activate).toMatchObject({ ok: true });
expect(useCombatStore.getState().auras.some((aura) => (
aura.targetId === PLAYER_AURA_ENTITY_ID
&& aura.definition.controlImmunities?.includes("silence")
&& aura.expiresAt === 11_000
))).toBe(true);
const partyMember = usePartyStore.getState().members[0]!;
expect(useCombatStore.getState().auras.some((aura) => (
aura.targetId === partyMember.id
&& aura.definition.controlImmunities?.includes("silence")
&& aura.expiresAt === 11_000
))).toBe(true);
useCombatStore.getState().advanceMobCombat(1_450);
expect(useCombatStore.getState().controlMechanic).toBeNull();
expect(useCombatStore.getState().schoolLockedUntil).toEqual({});
useCombatStore.getState().tick(0.1, 11_001);
expect(useCombatStore.getState().auras.some((aura) => (
aura.definition.controlImmunities?.includes("silence")
))).toBe(false);
useCombatStore.getState().advanceMobCombat(11_450);
expect(useCombatStore.getState().controlMechanic).toBe("silence");
expect(useCombatStore.getState().controlledUntil).toBe(14_450);
});
it("makes Aura Mastery immune to incoming interrupts while a spell is casting", () => {
const mobId = "paladin-aura-mastery-interrupt";
useCombatStore.getState().registerMob(mobId, {
name: "Interrupt attacker",
maxHealth: 10_000,
attacks: [{
id: "interrupt",
name: "Interrupt",
delivery: "area",
target: "primary",
school: "shadow",
animation: "cast",
castTimeMs: 0,
range: 30,
cooldownMs: 10_000,
initialCooldownMs: [0, 0],
repeatCooldownMs: [10_000, 10_000],
damageMultiplier: 0,
effect: { kind: "control", mechanic: "interrupt", durationMs: 3_000 },
}],
});
setMobPosition(mobId, [2, 0, 0]);
useCombatStore.getState().selectMob(mobId);
useCombatStore.getState().damageMob(mobId, 1, 1_000);
const activate = useCombatStore.getState().castAbility(unlockTalentAbility("Aura Mastery"), undefined, 1_000);
expect(activate).toMatchObject({ ok: true });
const playerAura = useCombatStore.getState().auras.find((aura) => (
aura.targetId === PLAYER_AURA_ENTITY_ID
&& aura.definition.controlImmunities?.includes("interrupt")
));
expect(playerAura).toBeDefined();
const member = usePartyStore.getState().members[0]!;
updatePartyRuntimePosition(member.id, 4, 0, 0);
useCombatStore.getState().synchronizeActors();
const holyLight = useCombatStore.getState().castAbility(abilityId("Holy Light"), {
targetActorId: member.id,
now: 2_500,
});
expect(holyLight).toMatchObject({ ok: true });
expect(useCombatStore.getState().activeCast).not.toBeNull();
useCombatStore.getState().advanceMobCombat(2_600);
expect(useCombatStore.getState().controlMechanic).toBeNull();
expect(useCombatStore.getState().activeCast).not.toBeNull();
});
it("doubles the active Paladin aura on each Aura Mastery recipient for the generated spell duration", () => {
const armor = () => characterCombatDefender(useCombatStore.getState()).armor ?? 0;
const baseArmor = armor();
expect(useCombatStore.getState().castAbility(abilityId("Devotion Aura"), undefined, 1_000))
.toMatchObject({ ok: true });
expect(armor() - baseArmor).toBe(1_205);
const mastery = useCombatStore.getState().castAbility(
unlockTalentAbility("Aura Mastery"),
undefined,
2_500,
);
expect(mastery).toMatchObject({ ok: true });
const partyMember = usePartyStore.getState().members[0]!;
const masteryAuras = useCombatStore.getState().auras.filter((aura) => (
aura.definition.utilityTags?.includes("wow335-aura-mastery-aura-effectiveness")
));
for (const targetId of [PLAYER_AURA_ENTITY_ID, partyMember.id]) {
expect(masteryAuras.some((aura) => aura.targetId === targetId && aura.expiresAt === 12_500)).toBe(true);
}
expect(armor() - baseArmor).toBe(2_410);
useCombatStore.getState().tick(0.1, 12_501);
expect(armor() - baseArmor).toBe(1_205);
});
it("lets Redemption target and revive a defeated party member at the rank's source health", () => {
const member = usePartyStore.getState().members[0]!;
usePartyStore.setState((party) => ({
members: party.members.map((candidate) => candidate.id === member.id
? { ...candidate, health: 0, status: "down" as const }
: candidate),
}));
updatePartyRuntimePosition(member.id, 4, 0, 0);
usePartyStore.getState().selectMember(member.id);
const cast = useCombatStore.getState().castAbility(abilityId("Redemption"), undefined, 1_000);
expect(cast).toMatchObject({ ok: true });
const activeCast = useCombatStore.getState().activeCast;
expect(activeCast?.targetId).toBe(member.id);
useCombatStore.getState().tick(0.1, activeCast!.completesAt);
expect(usePartyStore.getState().members.find((candidate) => candidate.id === member.id)?.health).toBe(1_760);
expect(useCombatStore.getState().combatEvents.some((event) => (
event.kind === "resurrection" && event.targetActorId === member.id
))).toBe(true);
});
it("blocks Redemption while any living enemy is engaged", () => {
const member = usePartyStore.getState().members[0]!;
usePartyStore.setState((party) => ({
members: party.members.map((candidate) => candidate.id === member.id
? { ...candidate, health: 0, status: "down" as const }
: candidate),
}));
updatePartyRuntimePosition(member.id, 4, 0, 0);
usePartyStore.getState().selectMember(member.id);
const targetId = "paladin-redemption-combat-gate";
useCombatStore.getState().registerMob(targetId, { name: "Engaged enemy", maxHealth: 10_000 });
setMobPosition(targetId, [4, 0, 0]);
useCombatStore.setState((state) => ({
mobs: { ...state.mobs, [targetId]: { ...state.mobs[targetId]!, engaged: true } },
}));
const result = useCombatStore.getState().castAbility(abilityId("Redemption"), undefined, 1_000);
expect(result).toMatchObject({ ok: false, reason: "condition-not-met" });
expect(useCombatStore.getState().activeCast).toBeNull();
expect(usePartyStore.getState().members.find((candidate) => candidate.id === member.id)?.health).toBe(0);
});
it("enforces Hammer of Wrath's 20% execute gate", () => {
const targetId = "paladin-hammer-gate-target";
useCombatStore.getState().registerMob(targetId, { name: "Target", maxHealth: 10_000 });
setMobPosition(targetId, [4, 0, 0]);
useCombatStore.setState((state) => ({
mobs: { ...state.mobs, [targetId]: { ...state.mobs[targetId]!, health: 2_001 } },
selectedTargetId: targetId,
}));
const result = useCombatStore.getState().castAbility(abilityId("Hammer of Wrath"), undefined, 1_000);
expect(result).toMatchObject({ ok: false, reason: "condition-not-met" });
});
it("requires a shield before Holy Shield can be activated", () => {
useCombatStore.setState({ equipment: {}, inventory: [] });
expect(useCombatStore.getState().abilities.find((candidate) => candidate.name === "Holy Shield")?.target).toBe("self");
const result = useCombatStore.getState().castAbility(unlockTalentAbility("Holy Shield"), undefined, 1_000);
expect(result).toMatchObject({ ok: false, reason: "condition-not-met" });
});
it("adds Redoubt's generated shield block chance only while a shield is equipped", () => {
setTalentRank("Redoubt", 2);
const withoutShield = characterCombatDefender(useCombatStore.getState()).blockPercent ?? 0;
const shieldId = "paladin-redoubt-block-test";
const shield = {
instanceId: shieldId,
itemId: 1,
itemKey: shieldId,
name: "Redoubt Test Shield",
quality: "uncommon" as const,
inventorySlot: "off-hand",
armorType: "shield",
weaponType: null,
itemLevel: 1,
sourceItemLevel: 1,
stats: { ...EMPTY_ITEM_STATS, blockValue: 40 },
quantity: 1,
sourceBossKey: "test",
sourceBossName: "Test",
acquiredAt: 1,
};
useCombatStore.setState((state) => ({
inventory: [shield],
equipment: { player: { "off-hand": shieldId } },
gearStats: { ...state.gearStats, blockValue: 40 },
}));
const withShield = characterCombatDefender(useCombatStore.getState()).blockPercent ?? 0;
expect(withShield - withoutShield).toBe(12);
});
it("procs Redoubt from a shield block with the generated rank values and duration", () => {
setTalentRank("Redoubt", 3);
const shieldId = "paladin-redoubt-proc-test";
const shield = {
instanceId: shieldId,
itemId: 1,
itemKey: shieldId,
name: "Redoubt Proc Shield",
quality: "uncommon" as const,
inventorySlot: "off-hand",
armorType: "shield",
weaponType: null,
itemLevel: 1,
sourceItemLevel: 1,
stats: { ...EMPTY_ITEM_STATS, blockRating: 1_000_000, blockValue: 40 },
quantity: 1,
sourceBossKey: "test",
sourceBossName: "Test",
acquiredAt: 1,
};
useCombatStore.setState((state) => ({
inventory: [shield],
equipment: { player: { "off-hand": shieldId } },
gearStats: { ...state.gearStats, blockRating: 1_000_000, blockValue: 40 },
}));
const mobId = "paladin-redoubt-proc-attacker";
useCombatStore.getState().registerMob(mobId, {
name: "Redoubt attacker",
level: 80,
maxHealth: 100_000,
attacks: [{
id: "redoubt-proc-melee",
name: "Melee attack",
delivery: "melee",
target: "primary",
school: "physical",
animation: "attack",
castTimeMs: 0,
range: 8,
cooldownMs: 500,
initialCooldownMs: [0, 0],
repeatCooldownMs: [500, 500],
damageMultiplier: 0.1,
}],
});
setMobPosition(mobId, [2, 0, 0]);
useCombatStore.getState().damageMob(mobId, 1, 1_000);
let redoubt = undefined as ReturnType<typeof useCombatStore.getState>["auras"][number] | undefined;
for (let now = 1_450; now <= 30_000; now += 250) {
useCombatStore.getState().advanceMobCombat(now);
redoubt = useCombatStore.getState().auras.find((aura) => (
aura.targetId === PLAYER_AURA_ENTITY_ID
&& aura.definition.utilityTags?.includes("wow335-paladin-redoubt")
));
if (redoubt) break;
}
expect(useCombatStore.getState().combatEvents.some((event) => (
event.kind === "damage" && event.targetActorId === PLAYER_AURA_ENTITY_ID && event.outcome === "block"
))).toBe(true);
expect(redoubt).toBeDefined();
expect(redoubt?.expiresAt).toBe(redoubt!.appliedAt + 10_000);
expect(redoubt?.definition.modifiers).toContainEqual({
kind: "damage", direction: "taken", operation: "percent", value: -0.06,
});
expect(redoubt?.definition.modifiers).toContainEqual({
kind: "stat", stat: "strength", operation: "percent", value: 0.1,
});
});
it("spends Holy Shield's eight source-defined charges on successful blocks", () => {
const shieldId = "paladin-holy-shield-charge-test";
const shield = {
instanceId: shieldId,
itemId: 1,
itemKey: shieldId,
name: "Test Shield",
quality: "uncommon" as const,
inventorySlot: "off-hand",
armorType: "shield",
weaponType: null,
itemLevel: 1,
sourceItemLevel: 1,
stats: { ...EMPTY_ITEM_STATS, blockRating: 1_000_000, blockValue: 40 },
quantity: 1,
sourceBossKey: "test",
sourceBossName: "Test",
acquiredAt: 1,
};
useCombatStore.setState((state) => ({
inventory: [shield],
equipment: { player: { "off-hand": shieldId } },
gearStats: { ...state.gearStats, blockRating: 1_000_000, blockValue: 40 },
}));
const targetId = "paladin-holy-shield-charge-target";
useCombatStore.getState().registerMob(targetId, {
name: "Melee attacker",
level: 80,
maxHealth: 100_000,
attacks: [{
id: "holy-shield-charge-melee",
name: "Melee attack",
delivery: "melee",
target: "primary",
school: "physical",
animation: "attack",
castTimeMs: 0,
range: 8,
cooldownMs: 500,
initialCooldownMs: [0, 0],
repeatCooldownMs: [500, 500],
damageMultiplier: 0.1,
}],
});
setMobPosition(targetId, [2, 0, 0]);
useCombatStore.getState().damageMob(targetId, 1, 1_000);
const holyShieldId = unlockTalentAbility("Holy Shield");
const cast = useCombatStore.getState().castAbility(holyShieldId, undefined, 1_000);
expect(cast, JSON.stringify(cast)).toMatchObject({ ok: true });
const findHolyShieldProc = () => useCombatStore.getState().auras.find((aura) => (
aura.targetId === PLAYER_AURA_ENTITY_ID
&& aura.definition.procs?.some((proc) => proc.id.endsWith(":holy-shield"))
));
expect(findHolyShieldProc()?.procStates[0]?.remainingCharges).toBe(8);
for (let now = 1_450; now <= 10_000; now += 250) {
useCombatStore.getState().advanceMobCombat(now);
if (findHolyShieldProc()?.procStates[0]?.remainingCharges === 0) break;
}
expect(findHolyShieldProc()?.procStates[0]?.remainingCharges).toBe(0);
});
it("procs three 8-second Reckoning charges from melee hits and adds one landed autoattack per charge", () => {
setTalentRank("Reckoning", 5);
const mobId = "paladin-reckoning-trigger";
useCombatStore.getState().registerMob(mobId, {
name: "Reckoning attacker",
level: 80,
maxHealth: 100_000,
attacks: [{
id: "reckoning-melee",
name: "Melee attack",
delivery: "melee",
target: "primary",
school: "physical",
animation: "attack",
castTimeMs: 0,
range: 8,
cooldownMs: 500,
initialCooldownMs: [0, 0],
repeatCooldownMs: [500, 500],
damageMultiplier: 0.1,
}],
});
setMobPosition(mobId, [2, 0, 0]);
useCombatStore.getState().damageMob(mobId, 1, 1_000);
const ensureMeleeHit = applyAura(useCombatStore.getState().auras, {
definition: {
id: "test:ensure-reckoning-melee-hit",
name: "Ensure incoming melee hit",
disposition: "buff",
durationMs: null,
modifiers: [{ kind: "stat", stat: "incoming-melee-hit", operation: "flat", value: 100 }],
},
sourceId: mobId,
targetId: PLAYER_AURA_ENTITY_ID,
now: 1_000,
}).auras;
useCombatStore.setState((state) => ({ auras: [...ensureMeleeHit] }));
let procAt: number | null = null;
for (let now = 1_450; now <= 30_000; now += 250) {
useCombatStore.getState().advanceMobCombat(now);
if (useCombatStore.getState().auras.some((aura) => (
aura.targetId === PLAYER_AURA_ENTITY_ID
&& aura.definition.utilityTags?.includes("wow335-paladin-reckoning")
))) {
procAt = now;
break;
}
}
expect(procAt).not.toBeNull();
const reckoning = useCombatStore.getState().auras.find((aura) => (
aura.targetId === PLAYER_AURA_ENTITY_ID
&& aura.definition.utilityTags?.includes("wow335-paladin-reckoning")
));
expect(reckoning).toMatchObject({ stacks: 3, expiresAt: procAt! + 8_000 });
useCombatStore.setState((state) => ({
autoAttackTargetId: mobId,
nextAutoAttackAt: procAt! + 100,
gearStats: { ...state.gearStats, hitRating: 1_000_000, expertiseRating: 1_000_000 },
}));
let autoAttackAt = procAt! + 100;
for (let charge = 2; charge >= 0; charge -= 1) {
useCombatStore.getState().tick(0.1, autoAttackAt);
const swingEvents = useCombatStore.getState().combatEvents.filter((event) => (
event.kind === "damage"
&& event.sourceActorId === PLAYER_AURA_ENTITY_ID
&& event.targetActorId === mobId
&& event.occurredAt === autoAttackAt
));
expect(swingEvents).toHaveLength(2);
const remaining = useCombatStore.getState().auras.find((aura) => (
aura.targetId === PLAYER_AURA_ENTITY_ID
&& aura.definition.utilityTags?.includes("wow335-paladin-reckoning")
));
if (charge > 0) expect(remaining?.stacks).toBe(charge);
else expect(remaining).toBeUndefined();
autoAttackAt = useCombatStore.getState().nextAutoAttackAt;
}
});
it("reflects the sourced rank percentage of incoming critical damage with Eye for an Eye", () => {
const mobId = "paladin-eye-for-an-eye-crit";
useCombatStore.getState().registerMob(mobId, {
name: "Critical attacker",
level: 80,
maxHealth: 100_000,
damageMultiplier: 1,
attacks: [{
id: "eye-crit-18",
name: "Critical attack",
delivery: "area",
target: "primary",
school: "fire",
animation: "cast",
castTimeMs: 0,
range: 30,
cooldownMs: 10_000,
initialCooldownMs: [0, 0],
repeatCooldownMs: [10_000, 10_000],
damageMultiplier: 1,
}],
});
setMobPosition(mobId, [2, 0, 0]);
useCombatStore.getState().selectMob(mobId);
useCombatStore.getState().damageMob(mobId, 1, 1_000);
const ensureSpellHit = applyAura([], {
definition: {
id: "test:ensure-incoming-spell-hit",
name: "Ensure incoming spell hit",
disposition: "buff",
durationMs: null,
modifiers: [{ kind: "stat", stat: "incoming-spell-hit", operation: "flat", value: 100 }],
},
sourceId: mobId,
targetId: PLAYER_AURA_ENTITY_ID,
now: 1_000,
}).auras;
useCombatStore.setState((state) => ({
talentRanks: { ...state.talentRanks, "talent-1632": 3 },
auras: [...state.auras, ...ensureSpellHit],
}));
// This seeded instant spell attack rolls inside the crit slice of the
// equal-level attack table, so the test does not rely on chance.
useCombatStore.getState().advanceMobCombat(1_450);
useCombatStore.getState().advanceMobCombat(3_100);
const events = useCombatStore.getState().combatEvents;
const incoming = events.find((event) => event.kind === "damage"
&& event.sourceActorId === mobId && event.targetActorId === PLAYER_AURA_ENTITY_ID);
expect(incoming?.critical, JSON.stringify(incoming)).toBe(true);
const reflected = events.find((event) => event.kind === "damage"
&& event.targetActorId === mobId && event.abilityId === "talent-1632");
expect(reflected?.rawAmount).toBe(Math.round((incoming?.effectiveAmount ?? 0) * 0.15));
});
it("bounces Avenger's Shield to three total targets without damage falloff", () => {
const shieldId = unlockTalentAbility("Avenger's Shield");
const targetIds = [
"paladin-avenger-target",
"paladin-avenger-bounce-a",
"paladin-avenger-bounce-b",
"paladin-avenger-bounce-c",
"paladin-avenger-out-of-range",
];
for (const id of targetIds) {
useCombatStore.getState().registerMob(id, { name: id, maxHealth: 1_000_000 });
}
setMobPosition(targetIds[0]!, [4, 0, 0]);
setMobPosition(targetIds[1]!, [10, 0, 0]);
setMobPosition(targetIds[2]!, [16, 0, 0]);
setMobPosition(targetIds[3]!, [22, 0, 0]);
setMobPosition(targetIds[4]!, [10, 0, 12]);
useCombatStore.getState().selectMob(targetIds[0]!);
const result = useCombatStore.getState().castAbility(shieldId, undefined, 1_000);
expect(result, JSON.stringify(result)).toMatchObject({ ok: true });
expect(result.affectedMobIds).toEqual(targetIds.slice(0, 3));
expect(result.affectedMobIds).not.toContain(targetIds[3]);
expect(result.affectedMobIds).not.toContain(targetIds[4]);
const damageEvents = useCombatStore.getState().combatEvents.filter((event) => (
event.kind === "damage" && event.abilityId === shieldId
&& result.affectedMobIds.includes(event.targetActorId)
));
expect(damageEvents).toHaveLength(3);
const normalizedRawDamage = damageEvents.map((event) => Math.round(event.rawAmount / (event.critical ? 2 : 1)));
expect(new Set(normalizedRawDamage).size).toBe(1);
});
it("applies a Greater Blessing to the selected ally's class group", () => {
const members = usePartyStore.getState().members;
const target = members[0]!;
usePartyStore.setState((party) => ({
members: party.members.map((candidate, index) => ({
...candidate,
classId: index < 2 ? "mage" : "priest",
})),
}));
for (const member of members) updatePartyRuntimePosition(member.id, 1, 0, 0);
usePartyStore.getState().selectMember(target.id);
const result = useCombatStore.getState().castAbility(abilityId("Greater Blessing of Kings"), undefined, 1_000);
expect(result).toMatchObject({ ok: true });
const blessedIds = new Set(useCombatStore.getState().auras
.filter((aura) => aura.definition.name.includes("Greater Blessing of Kings"))
.map((aura) => aura.targetId));
expect(blessedIds).toEqual(new Set([target.id, members[1]!.id]));
expect(blessedIds.has(PLAYER_AURA_ENTITY_ID)).toBe(false);
});
it("heals at most three party actors from Divine Storm damage", () => {
const members = usePartyStore.getState().members;
usePartyStore.setState((party) => ({
members: party.members.map((member) => ({ ...member, health: 1 })),
}));
members.forEach((member, index) => updatePartyRuntimePosition(member.id, index + 1, 0, 0));
useCombatStore.setState((state) => ({ health: Math.max(1, Math.round(state.maxHealth * 0.1)) }));
const targetId = "paladin-divine-storm-target";
const nearbyTargetId = "paladin-divine-storm-nearby-target";
const distantTargetId = "paladin-divine-storm-distant-target";
useCombatStore.getState().registerMob(targetId, { name: "Target", maxHealth: 100_000 });
useCombatStore.getState().registerMob(nearbyTargetId, { name: "Nearby Target", maxHealth: 100_000 });
useCombatStore.getState().registerMob(distantTargetId, { name: "Distant Target", maxHealth: 100_000 });
setMobPosition(targetId, [4, 0, 0]);
setMobPosition(nearbyTargetId, [7, 0, 0]);
setMobPosition(distantTargetId, [13, 0, 0]);
useCombatStore.setState((state) => ({ selectedTargetId: targetId }));
const divineStorm = unlockTalentAbility("Divine Storm");
const result = useCombatStore.getState().castAbility(divineStorm, { targetActorId: targetId, now: 1_000 });
expect(result, JSON.stringify(result)).toMatchObject({ ok: true });
expect(result.affectedMobIds).toEqual(expect.arrayContaining([targetId, nearbyTargetId]));
expect(result.affectedMobIds).not.toContain(distantTargetId);
const healEvents = useCombatStore.getState().combatEvents.filter((event) => (
event.kind === "healing" && event.abilityId === divineStorm && event.effectiveAmount > 0
));
expect(healEvents.length).toBeGreaterThan(0);
expect(healEvents.length).toBeLessThanOrEqual(3);
expect(healEvents.every((event) => event.rawAmount > 0)).toBe(true);
});
it("transfers direct healing to the active Beacon of Light target", () => {
const member = usePartyStore.getState().members[0]!;
updatePartyRuntimePosition(member.id, 4, 0, 0);
usePartyStore.setState((party) => ({
members: party.members.map((candidate) => candidate.id === member.id
? { ...candidate, health: 100 }
: candidate),
}));
usePartyStore.getState().selectMember(member.id);
const beaconResult = useCombatStore.getState().castAbility(unlockTalentAbility("Beacon of Light"), undefined, 1_000);
expect(beaconResult).toMatchObject({ ok: true });
const beaconCast = useCombatStore.getState().activeCast;
if (beaconCast) useCombatStore.getState().tick(0.1, beaconCast.completesAt);
expect(useCombatStore.getState().auras.some((aura) => (
aura.targetId === member.id && aura.definition.utilityTags?.includes("beacon-of-light")
))).toBe(true);
useCombatStore.getState().damagePlayer(5_000);
const beforeTransfer = usePartyStore.getState().members.find((candidate) => candidate.id === member.id)!.health;
const heal = useCombatStore.getState().castAbility(abilityId("Flash of Light"), undefined, 3_000);
expect(heal).toMatchObject({ ok: true });
const healCast = useCombatStore.getState().activeCast;
if (healCast) useCombatStore.getState().tick(0.1, healCast.completesAt);
const afterTransfer = usePartyStore.getState().members.find((candidate) => candidate.id === member.id)!.health;
expect(afterTransfer).toBeGreaterThan(beforeTransfer);
expect(useCombatStore.getState().auras.some((aura) => aura.targetId === member.id
&& aura.definition.utilityTags?.includes("beacon-of-light"))).toBe(true);
});
it("limits Divine Sacrifice to the remaining portion of its shared damage cap", () => {
const member = usePartyStore.getState().members[0]!;
updatePartyRuntimePosition(member.id, 0, 0, 0);
usePartyStore.setState((party) => ({
members: party.members.map((candidate) => candidate.id === member.id
? { ...candidate, health: candidate.maxHealth }
: candidate),
}));
usePartyStore.getState().selectMember(member.id);
const sacrifice = unlockTalentAbility("Divine Sacrifice");
expect(useCombatStore.getState().castAbility(sacrifice, undefined, 1_000).ok).toBe(true);
const cap = Math.round(useCombatStore.getState().maxHealth * 1.5);
const anchor = useCombatStore.getState().auras.find((aura) => (
aura.targetId === PLAYER_AURA_ENTITY_ID
&& aura.definition.utilityTags?.includes("wow335-paladin-divine-sacrifice")
));
expect(anchor?.definition.redirectDamageCapPercent).toBe(1.5);
expect(useCombatStore.getState().auras.some((aura) => (
aura.targetId === member.id && aura.definition.utilityTags?.includes("wow335-paladin-divine-sacrifice")
))).toBe(true);
useCombatStore.setState((state) => ({
auras: state.auras.map((aura) => aura.instanceId === anchor!.instanceId
? { ...aura, redirectedDamage: cap - 20 }
: aura),
}));
const mobId = "paladin-divine-sacrifice-partial";
useCombatStore.getState().registerMob(mobId, { name: "Cap test attacker", maxHealth: 100_000 });
setMobPosition(mobId, [2, 0, 0]);
useCombatStore.getState().damageMob(mobId, 1, 1_000, { actorId: member.id, role: "tank" });
useCombatStore.setState((state) => ({
mobs: { ...state.mobs, [mobId]: { ...state.mobs[mobId]!, attackDamage: 1_000 } },
}));
const playerBefore = useCombatStore.getState().health;
const memberBefore = usePartyStore.getState().members.find((candidate) => candidate.id === member.id)!.health;
useCombatStore.getState().advanceMobCombat(1_450);
useCombatStore.getState().advanceMobCombat(3_350);
expect(playerBefore - useCombatStore.getState().health).toBe(20);
expect(usePartyStore.getState().members.find((candidate) => candidate.id === member.id)!.health).toBeLessThan(memberBefore);
expect(useCombatStore.getState().auras.find((aura) => aura.instanceId === anchor!.instanceId)?.redirectedDamage).toBe(cap);
});
it("stops Divine Sacrifice redirects after the shared damage cap is exhausted", () => {
const member = usePartyStore.getState().members[0]!;
updatePartyRuntimePosition(member.id, 0, 0, 0);
usePartyStore.setState((party) => ({
members: party.members.map((candidate) => candidate.id === member.id
? { ...candidate, health: candidate.maxHealth }
: candidate),
}));
usePartyStore.getState().selectMember(member.id);
const sacrifice = unlockTalentAbility("Divine Sacrifice");
expect(useCombatStore.getState().castAbility(sacrifice, undefined, 1_000).ok).toBe(true);
const anchor = useCombatStore.getState().auras.find((aura) => (
aura.targetId === PLAYER_AURA_ENTITY_ID
&& aura.definition.utilityTags?.includes("wow335-paladin-divine-sacrifice")
));
expect(anchor).toBeDefined();
const cap = Math.round(useCombatStore.getState().maxHealth * 1.5);
useCombatStore.setState((state) => ({
auras: state.auras.map((aura) => aura.instanceId === anchor!.instanceId
? { ...aura, redirectedDamage: cap }
: aura),
}));
const mobId = "paladin-divine-sacrifice-exhausted";
useCombatStore.getState().registerMob(mobId, { name: "Cap test attacker", maxHealth: 100_000 });
setMobPosition(mobId, [2, 0, 0]);
useCombatStore.getState().damageMob(mobId, 1, 1_000, { actorId: member.id, role: "tank" });
useCombatStore.setState((state) => ({
mobs: { ...state.mobs, [mobId]: { ...state.mobs[mobId]!, attackDamage: 1_000 } },
}));
const playerBefore = useCombatStore.getState().health;
const memberBefore = usePartyStore.getState().members.find((candidate) => candidate.id === member.id)!.health;
useCombatStore.getState().advanceMobCombat(1_450);
const resolvedDamage = useCombatStore.getState().advanceMobCombat(3_350);
expect(resolvedDamage, JSON.stringify(useCombatStore.getState().mobs[mobId])).toBeGreaterThan(0);
expect(useCombatStore.getState().health).toBe(playerBefore);
expect(usePartyStore.getState().members.find((candidate) => candidate.id === member.id)!.health).toBeLessThan(memberBefore);
expect(useCombatStore.getState().auras.find((aura) => aura.instanceId === anchor!.instanceId)?.redirectedDamage).toBe(cap);
});
it("does not redirect damage from an attack that targets the Paladin", () => {
const sacrifice = unlockTalentAbility("Divine Sacrifice");
expect(useCombatStore.getState().castAbility(sacrifice, undefined, 1_000).ok).toBe(true);
const anchor = useCombatStore.getState().auras.find((aura) => (
aura.targetId === PLAYER_AURA_ENTITY_ID
&& aura.definition.utilityTags?.includes("wow335-paladin-divine-sacrifice")
));
expect(anchor).toBeDefined();
const mobId = "paladin-divine-sacrifice-self-hit";
useCombatStore.getState().registerMob(mobId, { name: "Self hit attacker", maxHealth: 100_000 });
setMobPosition(mobId, [2, 0, 0]);
useCombatStore.getState().damageMob(mobId, 1, 1_000);
useCombatStore.setState((state) => ({
mobs: { ...state.mobs, [mobId]: { ...state.mobs[mobId]!, attackDamage: 1_000 } },
}));
const playerBefore = useCombatStore.getState().health;
expect(useCombatStore.getState().advanceMobCombat(1_450)).toBeGreaterThan(0);
expect(useCombatStore.getState().health).toBeLessThan(playerBefore);
expect(useCombatStore.getState().auras.find((aura) => aura.instanceId === anchor!.instanceId)?.redirectedDamage).toBe(0);
});
it("makes the next eligible Divine Favor heal critical and consumes its aura", () => {
const maxHealth = useCombatStore.getState().maxHealth;
useCombatStore.getState().damagePlayer(Math.round(maxHealth * 0.4));
const activate = useCombatStore.getState().castAbility(unlockTalentAbility("Divine Favor"), undefined, 1_000);
expect(activate).toMatchObject({ ok: true });
expect(useCombatStore.getState().auras.some((aura) => (
aura.definition.utilityTags?.includes("wow335-divine-favor-next-holy-heal")
))).toBe(true);
const flashOfLight = abilityId("Flash of Light");
const heal = useCombatStore.getState().castAbility(flashOfLight, undefined, 3_000);
expect(heal).toMatchObject({ ok: true });
const activeCast = useCombatStore.getState().activeCast;
if (activeCast) useCombatStore.getState().tick(0.1, activeCast.completesAt);
expect(useCombatStore.getState().combatEvents.some((event) => (
event.kind === "healing" && event.abilityId === flashOfLight && event.critical
))).toBe(true);
expect(useCombatStore.getState().auras.some((aura) => (
aura.definition.utilityTags?.includes("wow335-divine-favor-next-holy-heal")
))).toBe(false);
});
});