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["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); }); });