import { beforeEach, describe, expect, it } from "vitest"; import { abilityAtLevel, abilitiesForClass, abilityTalentNodeId } from "./abilityCatalog"; import { resolvedAbilityCosts, useCombatStore, PLAYER_AURA_ENTITY_ID } from "./combatStore"; import { clearPartyRuntimeRegistry, registerPartyRuntimePosition } from "./partyRuntimeRegistry"; import { usePartyStore } from "./partyStore"; import { PLAYER_AGGRO_ID } from "./aggro"; import { clearMobRuntimeRegistry, setMobPosition } from "./mobRuntimeRegistry"; import { TALENT_NODES } from "./talentCatalog"; import { talentModifiersForRanks } from "./talentModifiers"; import { DEFAULT_ENEMY_ATTACK } from "./enemyAttacks"; function priestAbility(name: string) { const ability = abilitiesForClass("priest").find((candidate) => candidate.name === name); expect(ability, name).toBeDefined(); return ability!; } function unlock(name: string): void { const nodeId = abilityTalentNodeId(priestAbility(name)); expect(nodeId, `${name} talent node`).toBeTruthy(); useCombatStore.setState((state) => ({ talentRanks: { ...state.talentRanks, [nodeId!]: 1 } })); } function setPriestTalentRanks(ranks: Record, criticalChance?: number): void { const modifiers = talentModifiersForRanks(ranks, "priest"); useCombatStore.setState({ talentRanks: ranks, talentModifiers: criticalChance === undefined ? modifiers : [...modifiers, { kind: "critical-chance-percent", amount: criticalChance, unit: "percent", scope: "your spells", sourceTalentId: "test:priest-critical-chance", sourceTalentName: "Test critical chance", rank: 1, description: "Test-only critical chance modifier.", }], }); } function setMemberHealth(memberId: string, health: number, maxHealth: number): void { usePartyStore.setState((state) => ({ members: state.members.map((member) => member.id === memberId ? { ...member, health, maxHealth } : member), })); useCombatStore.setState((state) => ({ actors: { ...state.actors, [memberId]: { ...state.actors[memberId]!, health, maxHealth, alive: health > 0 }, }, })); } function castFriendly(name: string, targetId: string, now: number) { useCombatStore.getState().selectFriendlyActor(targetId); const result = useCombatStore.getState().castAbility(priestAbility(name).id, { targetActorId: targetId, now }); const activeCast = useCombatStore.getState().activeCast; if (result.ok && activeCast?.abilityId === priestAbility(name).id) { useCombatStore.getState().tick((activeCast.completesAt - now) / 1_000, activeCast.completesAt); } return result; } function castHostile(name: string, targetId: string, now: number) { useCombatStore.getState().selectMob(targetId); const result = useCombatStore.getState().castAbility(priestAbility(name).id, { targetActorId: targetId, now }); const activeCast = useCombatStore.getState().activeCast; if (result.ok && activeCast?.abilityId === priestAbility(name).id) { useCombatStore.getState().tick((activeCast.completesAt - now) / 1_000, activeCast.completesAt); } return result; } function stableUnit(seed: string): number { let hash = 2166136261; for (const character of seed) { hash ^= character.charCodeAt(0); hash = Math.imul(hash, 16777619); } return (hash >>> 0) / 0x1_0000_0000; } function painAndSufferingMindBlastStart(targetId: string, rank: number, shouldProc: boolean, earliest: number): number { const mindBlast = priestAbility("Mind Blast"); const talent = TALENT_NODES.find((node) => node.id === "talent-1909")!; const chance = rank > 0 ? (talent.rankEffects[rank - 1]?.procChance ?? 0) / 100 : 0; for (let start = earliest; start < earliest + 12_000; start += 100) { const completesAt = start + mindBlast.castTimeMs; const proc = stableUnit( `${PLAYER_AGGRO_ID}:${mindBlast.id}:${targetId}:${completesAt}:wow335-priest-pain-and-suffering`, ) < chance; if (proc === shouldProc) return start; } throw new Error(`Could not find a seeded Pain and Suffering ${shouldProc ? "proc" : "failure"}.`); } function shadowWordPainEffect(targetId: string, sourceActorId = PLAYER_AGGRO_ID) { return useCombatStore.getState().effects.find((effect) => effect.kind === "dot" && effect.sourceAbilityId === priestAbility("Shadow Word: Pain").id && effect.sourceActorId === sourceActorId && effect.targetId === targetId); } describe("WoW Priest mechanics", () => { beforeEach(() => { clearMobRuntimeRegistry(); clearPartyRuntimeRegistry(); useCombatStore.getState().initializeCharacter({ classId: "priest", level: 80 }); useCombatStore.getState().setPlayerPosition([0, 0, 0]); usePartyStore.getState().initializeParty("wow-priest-mechanics", 80, useCombatStore.getState().health); usePartyStore.getState().selectMember(null); useCombatStore.getState().synchronizeActors(); usePartyStore.getState().members.forEach((member, index) => { registerPartyRuntimePosition(member.id, [index + 2, 0, 0]); }); }); it("uses self targeting for Desperate Prayer and dual relationship for Dispel Magic", () => { const desperatePrayer = priestAbility("Desperate Prayer"); const dispelMagic = priestAbility("Dispel Magic"); const rank988 = abilityAtLevel(dispelMagic, 38); expect(desperatePrayer.target).toBe("self"); expect(dispelMagic.target).toBe("any"); expect(rank988.dbcSpellId).toBe(988); expect(rank988.effects).toContainEqual(expect.objectContaining({ kind: "dispel", mode: "cleanse", relationship: "target", categories: ["magic"], maxCount: 2, })); }); it("lets Dispel Magic rank 988 cleanse a party member's magic debuff", () => { const member = usePartyStore.getState().members[0]!; const carrier = priestAbility("Prayer of Fortitude"); expect(useCombatStore.getState().applyActorAura(PLAYER_AGGRO_ID, member.id, carrier, { kind: "apply-aura", aura: { id: "test:priest-magic-debuff", name: "Test magic debuff", disposition: "debuff", dispelCategory: "magic", durationMs: 30_000, maxStacks: 1, stackBehavior: "refresh", }, sourceEffectType: 6, sourceAuraType: 4, miscValue: 0, triggerSpellId: 0, }, 1_000)).toBe(true); expect(useCombatStore.getState().selectFriendlyActor(member.id)).toBe(true); const result = useCombatStore.getState().castAbility(priestAbility("Dispel Magic").id, { targetActorId: member.id, now: 2_000, }); expect(result.ok, JSON.stringify(result)).toBe(true); expect(useCombatStore.getState().auras.some((aura) => aura.definition.id === "test:priest-magic-debuff")).toBe(false); }); it("casts Desperate Prayer on the priest and applies Pain Suppression's threat reduction to its ally", () => { unlock("Desperate Prayer"); useCombatStore.getState().damagePlayer(500); const playerBefore = useCombatStore.getState().health; const desperatePrayer = useCombatStore.getState().castAbility(priestAbility("Desperate Prayer").id, [0, 0, 0], 1_000); expect(desperatePrayer.ok, JSON.stringify(desperatePrayer)).toBe(true); expect(useCombatStore.getState().health).toBeGreaterThan(playerBefore); const member = usePartyStore.getState().members[0]!; unlock("Pain Suppression"); useCombatStore.getState().registerMob("priest-threat-test", { name: "Threat test", maxHealth: 1_000, xpReward: 0 }); setMobPosition("priest-threat-test", [4, 0, 0]); useCombatStore.setState((state) => ({ mobs: { ...state.mobs, "priest-threat-test": { ...state.mobs["priest-threat-test"]!, engaged: true, threatByActor: { [member.id]: 100 }, targetActorId: member.id, } }, })); useCombatStore.getState().selectFriendlyActor(member.id); const result = useCombatStore.getState().castAbility(priestAbility("Pain Suppression").id, { targetActorId: member.id, now: 3_000, }); expect(result.ok, JSON.stringify(result)).toBe(true); expect(useCombatStore.getState().mobs["priest-threat-test"]?.threatByActor[member.id]).toBe(60); }); it("applies Prayer of Fortitude to the priest and all party members", () => { const result = useCombatStore.getState().castAbility(priestAbility("Prayer of Fortitude").id, [0, 0, 0], 1_000); expect(result.ok, JSON.stringify(result)).toBe(true); const recipients = useCombatStore.getState().auras .filter((aura) => aura.definition.id.includes(":48162:")) .map((aura) => aura.targetId); expect(recipients).toContain(PLAYER_AURA_ENTITY_ID); for (const member of usePartyStore.getState().members) expect(recipients).toContain(member.id); }); it("reduces only magic damage while an eligible Test of Faith cast is active", () => { const talent = TALENT_NODES.find((node) => node.name === "Test of Faith"); expect(talent).toBeDefined(); const ranks = { [talent!.id]: 1 }; let castId = 0; const damageTaken = (rank: number, school: "physical" | "shadow", abilityName: string, expired = false): number => { const now = Date.now(); const maxHealth = useCombatStore.getState().maxHealth; const selectedRanks = rank > 0 ? { [talent!.id]: rank } : {}; const completesAt = expired ? now - 1 : now + 2_000; useCombatStore.setState({ health: maxHealth, shield: 0, auras: [], talentRanks: selectedRanks, talentModifiers: talentModifiersForRanks(selectedRanks, "priest"), activeCast: { id: ++castId, abilityId: priestAbility(abilityName).id, targetId: null, mode: "cast", startedAt: now - 500, completesAt, originalDurationMs: 2_500, pushbackHits: 0, school: "holy", canCastWhileMoving: false, origin: [0, 0, 0], groundPosition: null, totalTicks: 1, ticksCompleted: 0, nextTickAt: completesAt, }, }); useCombatStore.getState().damagePlayer(1_000, school); return maxHealth - useCombatStore.getState().health; }; const baselineMagic = damageTaken(0, "shadow", "Flash Heal"); expect(damageTaken(1, "shadow", "Flash Heal")).toBe(Math.round(baselineMagic * 0.9)); expect(damageTaken(1, "physical", "Flash Heal")).toBe(damageTaken(0, "physical", "Flash Heal")); expect(damageTaken(1, "shadow", "Prayer of Fortitude")).toBe(baselineMagic); expect(damageTaken(1, "shadow", "Flash Heal", true)).toBe(baselineMagic); expect(ranks[talent!.id]).toBe(1); }); it("applies Test of Faith to magic hits resolved through enemy combat", () => { const talent = TALENT_NODES.find((node) => node.name === "Test of Faith"); expect(talent).toBeDefined(); const incomingHit = (rank: number): number => { useCombatStore.getState().initializeCharacter({ classId: "priest", level: 80 }); const selectedRanks = rank > 0 ? { [talent!.id]: rank } : {}; useCombatStore.setState({ talentRanks: selectedRanks, talentModifiers: talentModifiersForRanks(selectedRanks, "priest"), activeCast: { id: 1, abilityId: priestAbility("Flash Heal").id, targetId: null, mode: "cast", startedAt: 1_000, completesAt: 8_000, originalDurationMs: 7_000, pushbackHits: 0, school: "holy", canCastWhileMoving: false, origin: [0, 0, 0], groundPosition: null, totalTicks: 1, ticksCompleted: 0, nextTickAt: 8_000, }, }); const mobId = "priest-test-faith-magic-hit"; useCombatStore.getState().registerMob(mobId, { name: "Test of Faith magic hit", maxHealth: 100_000, xpReward: 0, attacks: [{ ...DEFAULT_ENEMY_ATTACK, id: "priest-test-shadow-cast", name: "Shadow cast", delivery: "projectile", animation: "cast", school: "shadow", range: 30, castTimeMs: 0, }], }); setMobPosition(mobId, [8, 0, 0]); useCombatStore.getState().damageMob(mobId, 1, 1_000); const before = useCombatStore.getState().health; useCombatStore.getState().advanceMobCombat(1_450, [0, 0, 0]); return before - useCombatStore.getState().health; }; const baseline = incomingHit(0); expect(baseline).toBeGreaterThan(0); expect(incomingHit(1)).toBe(Math.round(baseline * 0.9)); }); it("applies Test of Faith only to spells whose target is at or below half health", () => { const talent = TALENT_NODES.find((node) => node.name === "Test of Faith"); expect(talent).toBeDefined(); const ranks = { [talent!.id]: 3 }; expect(talentModifiersForRanks(ranks, "priest")).not.toContainEqual(expect.objectContaining({ kind: "healing-percent", sourceTalentName: "Test of Faith", })); expect(talentModifiersForRanks(ranks, "priest")).not.toContainEqual(expect.objectContaining({ kind: "damage-percent", sourceTalentName: "Test of Faith", })); const member = usePartyStore.getState().members[0]!; const prepareTarget = (health: number) => { usePartyStore.setState((state) => ({ members: state.members.map((candidate) => candidate.id === member.id ? { ...candidate, health, maxHealth: 20_000 } : candidate), })); useCombatStore.setState((state) => ({ actors: { ...state.actors, [member.id]: { ...state.actors[member.id]!, health, maxHealth: 20_000, alive: true }, }, })); }; const castFlashHeal = (health: number, now: number, withTalent: boolean) => { prepareTarget(health); useCombatStore.setState({ talentRanks: withTalent ? ranks : {}, talentModifiers: talentModifiersForRanks(withTalent ? ranks : {}, "priest"), }); useCombatStore.getState().selectFriendlyActor(member.id); const result = useCombatStore.getState().castAbility(priestAbility("Flash Heal").id, { targetActorId: member.id, now, }); expect(result.ok, JSON.stringify(result)).toBe(true); const completesAt = useCombatStore.getState().activeCast?.completesAt; if (completesAt) useCombatStore.getState().tick((completesAt - now) / 1_000, completesAt); const event = useCombatStore.getState().combatEvents.filter((candidate) => ( candidate.kind === "healing" && candidate.abilityId === priestAbility("Flash Heal").id && candidate.targetActorId === member.id )).at(-1); expect(event).toBeDefined(); return event!.rawAmount / (event!.critical ? 1.5 : 1); }; const base = castFlashHeal(9_000, 1_000, false); const boosted = castFlashHeal(9_000, 4_000, true); const aboveThreshold = castFlashHeal(11_000, 7_000, true); expect(boosted).toBeCloseTo(base * 1.12, 0); expect(aboveThreshold).toBeCloseTo(base, 0); const smite = priestAbility("Smite"); useCombatStore.getState().registerMob("priest-test-faith-damage", { name: "Test of Faith target", maxHealth: 20_000, xpReward: 0, }); setMobPosition("priest-test-faith-damage", [4, 0, 0]); const castSmite = (health: number, now: number, withTalent: boolean) => { useCombatStore.setState((state) => ({ talentRanks: withTalent ? ranks : {}, talentModifiers: talentModifiersForRanks(withTalent ? ranks : {}, "priest"), mobs: { ...state.mobs, "priest-test-faith-damage": { ...state.mobs["priest-test-faith-damage"]!, health, maxHealth: 20_000, dead: false, }, }, })); useCombatStore.getState().selectMob("priest-test-faith-damage"); const result = useCombatStore.getState().castAbility(smite.id, [0, 0, 0], now); expect(result.ok, JSON.stringify(result)).toBe(true); const completesAt = useCombatStore.getState().activeCast?.completesAt; if (completesAt) useCombatStore.getState().tick((completesAt - now) / 1_000, completesAt); const event = useCombatStore.getState().combatEvents.filter((candidate) => ( candidate.kind === "damage" && candidate.abilityId === smite.id && candidate.targetActorId === "priest-test-faith-damage" )).at(-1); expect(event).toBeDefined(); return event!.rawAmount / (event!.critical ? 1.5 : 1); }; const baseDamage = castSmite(9_000, 10_000, false); const lowHealthDamage = castSmite(9_000, 13_000, true); const healthyDamage = castSmite(11_000, 16_000, true); expect(lowHealthDamage).toBeCloseTo(baseDamage * 1.12, 0); expect(healthyDamage).toBeCloseTo(baseDamage, 0); }); it("keeps proc-based Priest talent values out of passive talent modifiers", () => { const procTalentNames = ["Holy Concentration", "Serendipity", "Shadow Weaving"]; const ranks = Object.fromEntries(procTalentNames.map((name) => { const talent = TALENT_NODES.find((node) => node.name === name); expect(talent, name).toBeDefined(); return [talent!.id, talent!.maxRank]; })); const modifiers = talentModifiersForRanks(ranks, "priest"); for (const name of procTalentNames) { expect(modifiers.filter((modifier) => modifier.sourceTalentName === name), name).toEqual([]); } }); it("limits Inner Focus to its three named spells and empowers Greater Heal", () => { const talent = TALENT_NODES.find((node) => node.name === "Inner Focus"); expect(talent).toBeDefined(); setPriestTalentRanks({ [talent!.id]: 1 }, 75); useCombatStore.setState((state) => ({ resource: 0, resourcePools: { ...state.resourcePools, mana: 0 }, })); useCombatStore.getState().registerMob("priest-inner-focus-damage", { name: "Inner Focus target", maxHealth: 100_000, xpReward: 0, }); setMobPosition("priest-inner-focus-damage", [4, 0, 0]); expect(useCombatStore.getState().castAbility(priestAbility("Inner Focus").id, undefined, 1_000).ok).toBe(true); expect(useCombatStore.getState().castAbility(priestAbility("Smite").id, { targetActorId: "priest-inner-focus-damage", now: 1_001, })).toMatchObject({ ok: false, reason: "not-enough-resource" }); expect(useCombatStore.getState().auras.some((aura) => ( aura.definition.utilityTags?.includes("next-heal-free-critical") ))).toBe(true); const manaSpentAt = useCombatStore.getState().lastResourceSpentAt.mana; const member = usePartyStore.getState().members[0]!; setMemberHealth(member.id, 1, member.maxHealth); useCombatStore.getState().selectFriendlyActor(member.id); const greaterHeal = priestAbility("Greater Heal"); const cast = useCombatStore.getState().castAbility(greaterHeal.id, { targetActorId: member.id, now: 1_002, }); expect(cast.ok, JSON.stringify(cast)).toBe(true); const activeCast = useCombatStore.getState().activeCast!; expect(activeCast.completesAt - activeCast.startedAt).toBe(Math.round(greaterHeal.castTimeMs * 0.5)); useCombatStore.getState().tick((activeCast.completesAt - activeCast.startedAt) / 1_000, activeCast.completesAt); expect(useCombatStore.getState().lastResourceSpentAt.mana).toBe(manaSpentAt); expect(useCombatStore.getState().auras.some((aura) => ( aura.definition.utilityTags?.includes("next-heal-free-critical") ))).toBe(false); const heal = useCombatStore.getState().combatEvents.filter((event) => ( event.kind === "healing" && event.abilityId === greaterHeal.id && event.targetActorId === member.id )).at(-1)!; expect(heal.critical).toBe(true); }); it("gives Inner Focus's Mind Blast rider and reduces Power Word: Shield's Weakened Soul", () => { const talent = TALENT_NODES.find((node) => node.name === "Inner Focus")!; setPriestTalentRanks({ [talent.id]: 1 }); useCombatStore.setState((state) => ({ resource: 0, resourcePools: { ...state.resourcePools, mana: 0 }, })); useCombatStore.getState().registerMob("priest-inner-focus-mind-blast", { name: "Inner Focus Mind Blast target", maxHealth: 100_000, xpReward: 0, }); setMobPosition("priest-inner-focus-mind-blast", [4, 0, 0]); expect(useCombatStore.getState().castAbility(priestAbility("Inner Focus").id, undefined, 1_000).ok).toBe(true); const mindBlast = priestAbility("Mind Blast"); expect(useCombatStore.getState().castAbility(mindBlast.id, { targetActorId: "priest-inner-focus-mind-blast", now: 1_001, }).ok).toBe(true); expect(useCombatStore.getState().activeCast?.completesAt).toBe(1_001 + mindBlast.castTimeMs / 2); useCombatStore.getState().tick(mindBlast.castTimeMs / 2_000, 1_001 + mindBlast.castTimeMs / 2); expect(useCombatStore.getState().mobs["priest-inner-focus-mind-blast"]?.statuses).toContainEqual(expect.objectContaining({ kind: "slow", magnitude: 0.7, endsAt: 1_001 + mindBlast.castTimeMs / 2 + 5_000, })); const member = usePartyStore.getState().members[0]!; useCombatStore.setState((state) => ({ cooldowns: { ...state.cooldowns, [priestAbility("Inner Focus").id]: 0 }, })); expect(useCombatStore.getState().castAbility(priestAbility("Inner Focus").id, undefined, 8_000).ok).toBe(true); expect(castFriendly("Power Word: Shield", member.id, 8_001).ok).toBe(true); const weakenedSoul = useCombatStore.getState().auras.find((aura) => ( aura.targetId === member.id && aura.definition.id === "wow335:priest:weakened-soul" )); expect(weakenedSoul?.expiresAt).toBe(28_001); }); it.each([ { rank: 1, percent: 0.04 }, { rank: 2, percent: 0.07 }, { rank: 3, percent: 0.1 }, ])("grants Divine Aegis rank $rank from a critical Holy heal, including reduced overheal", ({ rank, percent }) => { const talent = TALENT_NODES.find((node) => node.name === "Divine Aegis")!; const ranks = { [talent.id]: rank }; setPriestTalentRanks(ranks, 100); const member = usePartyStore.getState().members[0]!; setMemberHealth(member.id, 1, 20_000); const result = castFriendly("Flash Heal", member.id, 1_000); expect(result.ok, JSON.stringify(result)).toBe(true); const heal = useCombatStore.getState().combatEvents.filter((event) => ( event.kind === "healing" && event.abilityId === priestAbility("Flash Heal").id && event.targetActorId === member.id )).at(-1)!; expect(heal.critical).toBe(true); const expected = Math.min(2_000, Math.round((heal.effectiveAmount + heal.overheal * 0.25) * percent * 2)); const aegis = useCombatStore.getState().auras.find((aura) => aura.targetId === member.id && aura.definition.utilityTags?.includes("wow335-priest-divine-aegis")); expect(aegis?.absorbs.reduce((total, absorb) => total + absorb.remaining, 0)).toBe(expected); expect(aegis?.expiresAt).toBe(heal.occurredAt + 6_000); }); it("caps a target's Divine Aegis absorb at ten percent maximum health", () => { const talent = TALENT_NODES.find((node) => node.name === "Divine Aegis")!; setPriestTalentRanks({ [talent.id]: 3 }, 100); const member = usePartyStore.getState().members[0]!; setMemberHealth(member.id, 1, 500); expect(castFriendly("Flash Heal", member.id, 1_000).ok).toBe(true); const aegis = useCombatStore.getState().auras.find((aura) => aura.targetId === member.id && aura.definition.utilityTags?.includes("wow335-priest-divine-aegis")); expect(aegis?.absorbs.reduce((total, absorb) => total + absorb.remaining, 0)).toBeLessThanOrEqual(50); }); it("makes Surge of Light's next Smite instant, free, stronger, and unable to crit", () => { const talent = TALENT_NODES.find((node) => node.name === "Surge of Light")!; useCombatStore.getState().registerMob("priest-surge-test", { name: "Surge target", maxHealth: 200_000, xpReward: 0 }); setMobPosition("priest-surge-test", [4, 0, 0]); setPriestTalentRanks({}, -100); expect(castHostile("Smite", "priest-surge-test", 1_000).ok).toBe(true); const baseline = useCombatStore.getState().combatEvents.filter((event) => ( event.kind === "damage" && event.abilityId === priestAbility("Smite").id && event.targetActorId === "priest-surge-test" )).at(-1)!; expect(baseline.critical).toBe(false); setPriestTalentRanks({ [talent.id]: 2 }, 100); const member = usePartyStore.getState().members[0]!; let procAt = 0; for (let attempt = 0; attempt < 16; attempt += 1) { const now = 6_000 + attempt * 4_000; setMemberHealth(member.id, 1, 20_000); expect(castFriendly("Flash Heal", member.id, now).ok).toBe(true); if (useCombatStore.getState().auras.some((aura) => aura.definition.utilityTags?.includes("wow335-priest-surge-of-light"))) { procAt = now + priestAbility("Flash Heal").castTimeMs; break; } } expect(procAt).toBeGreaterThan(0); const manaBefore = useCombatStore.getState().resource; expect(castHostile("Smite", "priest-surge-test", procAt + 4_000).ok).toBe(true); expect(useCombatStore.getState().activeCast).toBeNull(); expect(useCombatStore.getState().resource).toBe(manaBefore); const surged = useCombatStore.getState().combatEvents.filter((event) => ( event.kind === "damage" && event.abilityId === priestAbility("Smite").id && event.targetActorId === "priest-surge-test" )).at(-1)!; expect(surged.critical).toBe(false); expect(surged.rawAmount).toBe(Math.round(baseline.rawAmount * 1.6)); }); it("also triggers Surge of Light from critical direct damage and spends it on Flash Heal", () => { const talent = TALENT_NODES.find((node) => node.name === "Surge of Light")!; useCombatStore.getState().registerMob("priest-surge-crit-test", { name: "Surge critical target", maxHealth: 1_000_000, xpReward: 0, }); setMobPosition("priest-surge-crit-test", [4, 0, 0]); setPriestTalentRanks({ [talent.id]: 2 }, 100); let procAt = 0; for (let attempt = 0; attempt < 16; attempt += 1) { const now = 1_000 + attempt * 4_000; expect(castHostile("Smite", "priest-surge-crit-test", now).ok).toBe(true); if (useCombatStore.getState().auras.some((aura) => aura.definition.utilityTags?.includes("wow335-priest-surge-of-light"))) { procAt = now + priestAbility("Smite").castTimeMs; break; } } expect(procAt).toBeGreaterThan(0); const member = usePartyStore.getState().members[0]!; setMemberHealth(member.id, 1, 20_000); setPriestTalentRanks({}, 100); const manaBefore = useCombatStore.getState().resource; expect(castFriendly("Flash Heal", member.id, procAt + 4_000).ok).toBe(true); expect(useCombatStore.getState().activeCast).toBeNull(); expect(useCombatStore.getState().resource).toBe(manaBefore); expect(useCombatStore.getState().auras.some((aura) => aura.definition.utilityTags?.includes("wow335-priest-surge-of-light"))).toBe(false); }); it("stacks Serendipity to three charges and consumes all charges on Greater Heal", () => { const talent = TALENT_NODES.find((node) => node.name === "Serendipity")!; const ranks = { [talent.id]: 3 }; setPriestTalentRanks(ranks); const member = usePartyStore.getState().members[0]!; for (const now of [1_000, 5_000, 9_000]) { setMemberHealth(member.id, 1, 20_000); expect(castFriendly("Flash Heal", member.id, now).ok).toBe(true); } const procAura = useCombatStore.getState().auras.find((aura) => aura.definition.utilityTags?.includes("wow335-priest-serendipity")); expect(procAura?.stacks).toBe(3); const greaterHeal = abilityAtLevel(priestAbility("Greater Heal"), 80); useCombatStore.setState((state) => ({ resource: 5_000, maxResource: 5_000, resourcePools: { ...state.resourcePools, mana: 5_000 }, maxResourcePools: { ...state.maxResourcePools, mana: 5_000 }, gearStats: { ...state.gearStats, spirit: 0, mp5: 0 }, })); const baseCost = resolvedAbilityCosts(greaterHeal, [], useCombatStore.getState()) .find((cost) => cost.resource === "mana")!.amount; const manaBefore = useCombatStore.getState().resource; useCombatStore.getState().selectFriendlyActor(member.id); const result = useCombatStore.getState().castAbility(greaterHeal.id, { targetActorId: member.id, now: 14_000 }); expect(result.ok, JSON.stringify(result)).toBe(true); const cast = useCombatStore.getState().activeCast!; expect(cast.completesAt - cast.startedAt).toBe(Math.round(greaterHeal.castTimeMs * 0.55)); expect(useCombatStore.getState().auras.find((aura) => aura.definition.utilityTags?.includes("wow335-priest-serendipity"))?.stacks).toBe(3); useCombatStore.getState().castAbility(greaterHeal.id, { targetActorId: member.id, now: cast.completesAt, completingCast: cast, }); expect(useCombatStore.getState().auras.some((aura) => aura.definition.utilityTags?.includes("wow335-priest-serendipity"))).toBe(false); expect(manaBefore - useCombatStore.getState().resource).toBe(Math.round(baseCost * 0.55)); }); it("adds Shadow Weaving on Shadow damage and increases later ticks up to five stacks", () => { const talent = TALENT_NODES.find((node) => node.name === "Shadow Weaving")!; useCombatStore.getState().registerMob("priest-shadow-weaving-test", { name: "Shadow Weaving target", maxHealth: 200_000, xpReward: 0, }); setMobPosition("priest-shadow-weaving-test", [4, 0, 0]); setPriestTalentRanks({ [talent.id]: 3 }, -100); expect(castHostile("Shadow Word: Pain", "priest-shadow-weaving-test", 1_000).ok).toBe(true); useCombatStore.getState().tick(18, 19_000); const ticks = useCombatStore.getState().combatEvents.filter((event) => ( event.kind === "damage" && event.abilityId === priestAbility("Shadow Word: Pain").id && event.targetActorId === "priest-shadow-weaving-test" )); expect(ticks.length).toBeGreaterThanOrEqual(2); expect(ticks[1]!.rawAmount).toBe(Math.round(ticks[0]!.rawAmount * 1.02)); expect(useCombatStore.getState().auras.find((aura) => aura.definition.utilityTags?.includes("wow335-priest-shadow-weaving"))?.stacks).toBe(5); }); it("uses generated Pain and Suffering proc chances to refresh Shadow Word: Pain", () => { const talent = TALENT_NODES.find((node) => node.id === "talent-1909")!; expect(talent.rankEffects.map((rank) => rank.procChance)).toEqual([33, 66, 100]); const cases = [ { rank: 0, shouldProc: false }, { rank: 1, shouldProc: false }, { rank: 1, shouldProc: true }, { rank: 2, shouldProc: false }, { rank: 2, shouldProc: true }, { rank: 3, shouldProc: true }, ]; const shadowWordPain = abilityAtLevel(priestAbility("Shadow Word: Pain"), 80); const dotDefinition = shadowWordPain.effects.find((effect) => effect.kind === "dot")!; expect(dotDefinition.kind).toBe("dot"); cases.forEach(({ rank, shouldProc }, index) => { const targetId = `priest-pain-and-suffering-rank-${rank}-${index}`; useCombatStore.getState().registerMob(targetId, { name: "Pain and Suffering target", maxHealth: 1_000_000, xpReward: 0, }); setMobPosition(targetId, [4, 0, 0]); setPriestTalentRanks(rank ? { [talent.id]: rank } : {}); const appliedAt = 1_000 + index * 25_000; expect(castHostile("Shadow Word: Pain", targetId, appliedAt).ok).toBe(true); const before = shadowWordPainEffect(targetId)!; expect(before).toBeDefined(); const expirationBefore = before.nextTickAt + (before.remainingTicks - 1) * before.intervalMs; const mindBlast = priestAbility("Mind Blast"); const castAt = painAndSufferingMindBlastStart(targetId, rank, shouldProc, appliedAt + 2_000); expect(castHostile("Mind Blast", targetId, castAt).ok).toBe(true); const completedCast = useCombatStore.getState().combatEvents.filter((event) => ( event.kind === "cast" && event.abilityId === mindBlast.id && event.targetActorId === targetId )).at(-1)!; const after = shadowWordPainEffect(targetId)!; const expirationAfter = after.nextTickAt + (after.remainingTicks - 1) * after.intervalMs; if (shouldProc) { expect(after.remainingTicks).toBe(dotDefinition.ticks); expect(after.nextTickAt).toBe(completedCast.occurredAt + after.intervalMs); expect(expirationAfter).toBe(completedCast.occurredAt + dotDefinition.ticks * dotDefinition.intervalMs); } else { expect(expirationAfter).toBe(expirationBefore); } }); }); it("refreshes only its own Shadow Word: Pain on the eligible cast target", () => { const talent = TALENT_NODES.find((node) => node.id === "talent-1909")!; setPriestTalentRanks({ [talent.id]: 3 }); for (const targetId of ["priest-pain-scope-primary", "priest-pain-scope-other"]) { useCombatStore.getState().registerMob(targetId, { name: "Pain scope target", maxHealth: 1_000_000, xpReward: 0, }); setMobPosition(targetId, [4, 0, 0]); } expect(castHostile("Shadow Word: Pain", "priest-pain-scope-primary", 1_000).ok).toBe(true); expect(castHostile("Shadow Word: Pain", "priest-pain-scope-other", 5_000).ok).toBe(true); const ownedPrimary = shadowWordPainEffect("priest-pain-scope-primary")!; const ownedOther = shadowWordPainEffect("priest-pain-scope-other")!; const foreignPrimary = { ...ownedPrimary, id: "foreign-priest-shadow-word-pain", sourceActorId: "other-priest" }; useCombatStore.setState((state) => ({ effects: [...state.effects, foreignPrimary] })); const endOtherBefore = ownedOther.nextTickAt + (ownedOther.remainingTicks - 1) * ownedOther.intervalMs; const endForeignBefore = foreignPrimary.nextTickAt + (foreignPrimary.remainingTicks - 1) * foreignPrimary.intervalMs; expect(castHostile("Mind Blast", "priest-pain-scope-primary", 10_000).ok).toBe(true); const ownAfter = shadowWordPainEffect("priest-pain-scope-primary")!; const otherAfter = shadowWordPainEffect("priest-pain-scope-other")!; const foreignAfter = useCombatStore.getState().effects.find((effect) => effect.id === foreignPrimary.id)!; expect(ownAfter.remainingTicks).toBe(6); expect(otherAfter.nextTickAt + (otherAfter.remainingTicks - 1) * otherAfter.intervalMs).toBe(endOtherBefore); expect(foreignAfter.nextTickAt + (foreignAfter.remainingTicks - 1) * foreignAfter.intervalMs).toBe(endForeignBefore); }); it("does not refresh for unrelated casts or when the target has no Shadow Word: Pain", () => { const talent = TALENT_NODES.find((node) => node.id === "talent-1909")!; setPriestTalentRanks({ [talent.id]: 3 }); for (const targetId of ["priest-pain-unrelated", "priest-pain-absent"]) { useCombatStore.getState().registerMob(targetId, { name: "Pain trigger guard target", maxHealth: 1_000_000, xpReward: 0, }); setMobPosition(targetId, [4, 0, 0]); } expect(castHostile("Shadow Word: Pain", "priest-pain-unrelated", 1_000).ok).toBe(true); const dot = shadowWordPainEffect("priest-pain-unrelated")!; const endBefore = dot.nextTickAt + (dot.remainingTicks - 1) * dot.intervalMs; expect(castHostile("Smite", "priest-pain-unrelated", 5_000).ok).toBe(true); const afterSmite = shadowWordPainEffect("priest-pain-unrelated")!; expect(afterSmite.nextTickAt + (afterSmite.remainingTicks - 1) * afterSmite.intervalMs).toBe(endBefore); expect(castHostile("Mind Blast", "priest-pain-absent", 10_000).ok).toBe(true); expect(shadowWordPainEffect("priest-pain-absent")).toBeUndefined(); }); it.each(["Mind Flay", "Mind Sear", "Mind Blast"])("refreshes Shadow Word: Pain after a %s cast", (castName) => { const talent = TALENT_NODES.find((node) => node.id === "talent-1909")!; const castAbilityNodeId = abilityTalentNodeId(priestAbility(castName)); setPriestTalentRanks({ [talent.id]: 3, ...(castAbilityNodeId ? { [castAbilityNodeId]: 1 } : {}), }); const targetId = `priest-pain-and-suffering-${castName.toLowerCase().replaceAll(" ", "-")}`; useCombatStore.getState().registerMob(targetId, { name: "Pain and Suffering eligible cast target", maxHealth: 1_000_000, xpReward: 0, }); setMobPosition(targetId, [4, 0, 0]); expect(castHostile("Shadow Word: Pain", targetId, 1_000).ok).toBe(true); useCombatStore.getState().tick(9, 10_000); const before = shadowWordPainEffect(targetId)!; expect(before.remainingTicks).toBeLessThan(6); const castResult = castHostile(castName, targetId, 11_000); expect(castResult.ok, JSON.stringify(castResult)).toBe(true); const cast = useCombatStore.getState().combatEvents.filter((event) => ( event.kind === "cast" && event.abilityId === priestAbility(castName).id && event.targetActorId === targetId )).at(-1)!; const after = shadowWordPainEffect(targetId)!; expect(after.remainingTicks).toBe(6); expect(after.nextTickAt).toBe(cast.occurredAt + after.intervalMs); expect(after.tickAmounts).toEqual(before.tickAmounts); expect(after.basePoints).toBe(before.basePoints); expect(after.intervalMs).toBe(before.intervalMs); expect(after.damageSchool).toBe(before.damageSchool); expect(after.id).toBe(before.id); }); it("procs Holy Concentration on a critical heal and extends nearby Renew by at most six seconds", () => { const talent = TALENT_NODES.find((node) => node.name === "Holy Concentration")!; setPriestTalentRanks({ [talent.id]: 3 }, 100); const [first, second] = usePartyStore.getState().members; expect(castFriendly("Renew", first!.id, 1_000).ok).toBe(true); expect(castFriendly("Renew", second!.id, 4_000).ok).toBe(true); const beforeExtensions = new Map(useCombatStore.getState().effects.filter((effect) => effect.kind === "hot" && effect.sourceAbilityId === priestAbility("Renew").id) .map((effect) => [effect.targetId ?? PLAYER_AURA_ENTITY_ID, effect.nextTickAt + (effect.remainingTicks - 1) * effect.intervalMs])); setMemberHealth(second!.id, 1, second!.maxHealth); expect(castFriendly("Flash Heal", second!.id, 7_000).ok).toBe(true); const concentration = useCombatStore.getState().auras.find((aura) => aura.definition.utilityTags?.includes("wow335-priest-holy-concentration")); expect(concentration?.expiresAt).toBeGreaterThan(0); expect(concentration?.definition.modifiers).toContainEqual(expect.objectContaining({ kind: "stat", stat: "spirit", operation: "percent", value: 0.3, })); let renewEffects = useCombatStore.getState().effects.filter((effect) => effect.kind === "hot" && effect.sourceAbilityId === priestAbility("Renew").id); for (const effect of renewEffects) { const target = effect.targetId ?? PLAYER_AURA_ENTITY_ID; expect(effect.extensionTicks).toBe(1); expect(effect.nextTickAt + (effect.remainingTicks - 1) * effect.intervalMs).toBe(beforeExtensions.get(target)! + 3_000); } setMemberHealth(second!.id, 1, second!.maxHealth); expect(castFriendly("Flash Heal", second!.id, 11_000).ok).toBe(true); renewEffects = useCombatStore.getState().effects.filter((effect) => effect.kind === "hot" && effect.sourceAbilityId === priestAbility("Renew").id); for (const effect of renewEffects) expect(effect.extensionTicks).toBe(2); setMemberHealth(second!.id, 1, second!.maxHealth); expect(castFriendly("Flash Heal", second!.id, 15_000).ok).toBe(true); renewEffects = useCombatStore.getState().effects.filter((effect) => effect.kind === "hot" && effect.sourceAbilityId === priestAbility("Renew").id); for (const effect of renewEffects) expect(effect.extensionTicks).toBe(2); }); it("gives the three Prayer buffs party scope and Pain Suppression its target threat reduction", () => { for (const name of ["Prayer of Fortitude", "Prayer of Shadow Protection", "Prayer of Spirit"]) { const rank = abilityAtLevel(priestAbility(name), 80); expect(rank.effects.some((effect) => effect.kind === "apply-aura" && effect.scope === "party"), name).toBe(true); } const painSuppression = abilityAtLevel(priestAbility("Pain Suppression"), 80); expect(painSuppression.effects).toContainEqual({ kind: "threat", mode: "percent", amount: -40, recipient: "ability-target", }); }); it("summons a nonattacking Lightwell and heals for six seconds per click", () => { unlock("Lightwell"); const ability = priestAbility("Lightwell"); expect(useCombatStore.getState().castAbility(ability.id, [0, 0, 0], 1_000).ok).toBe(true); const completion = useCombatStore.getState().activeCast?.completesAt; expect(completion).toBeDefined(); useCombatStore.getState().tick(1, completion!); const well = useCombatStore.getState().summons.find((summon) => summon.sourceAbilityId === ability.id); expect(well).toMatchObject({ clickCharges: 10, expiresAt: completion! + 180_000, attackDamage: 0 }); useCombatStore.getState().damagePlayer(500); const healthBefore = useCombatStore.getState().health; const clickAmount = useCombatStore.getState().clickLightwell(well!.id, PLAYER_AURA_ENTITY_ID, completion! + 1); expect(clickAmount).toBe(1_540 * 3); expect(useCombatStore.getState().health).toBe(healthBefore); expect(useCombatStore.getState().effects).toContainEqual(expect.objectContaining({ kind: "hot", sourceAbilityId: ability.id, remainingTicks: 3, intervalMs: 2_000, coefficient: 0, basePoints: 1_540, nextTickAt: completion! + 2_001, })); useCombatStore.getState().tick(1, completion! + 1_001); expect(useCombatStore.getState().health).toBe(healthBefore); expect(useCombatStore.getState().effects.find((effect) => effect.sourceAbilityId === ability.id)?.remainingTicks).toBe(3); useCombatStore.getState().tick(1, completion! + 2_001); expect(useCombatStore.getState().health).toBeGreaterThan(healthBefore); expect(useCombatStore.getState().effects.find((effect) => effect.sourceAbilityId === ability.id)?.remainingTicks).toBe(2); useCombatStore.getState().tick(4, completion! + 6_001); expect(useCombatStore.getState().summons.find((summon) => summon.id === well!.id)?.clickCharges).toBe(9); for (let click = 1; click < 10; click += 1) { useCombatStore.getState().clickLightwell(well!.id, PLAYER_AURA_ENTITY_ID, completion! + 1 + click); } expect(useCombatStore.getState().summons.some((summon) => summon.id === well!.id)).toBe(false); expect(useCombatStore.getState().clickLightwell(well!.id, PLAYER_AURA_ENTITY_ID, completion! + 20)).toBe(0); }); it("uses the sourced per-tick spell-power coefficient for Lightwell ranks with a bonus row", () => { useCombatStore.getState().initializeCharacter({ classId: "priest", level: 75 }); useCombatStore.setState((state) => ({ gearStats: { ...state.gearStats, spellPower: 300 } })); usePartyStore.getState().initializeParty("wow-priest-lightwell-coefficient", 75, useCombatStore.getState().health); useCombatStore.getState().synchronizeActors(); unlock("Lightwell"); const ability = priestAbility("Lightwell"); expect(useCombatStore.getState().castAbility(ability.id, [0, 0, 0], 1_000).ok).toBe(true); const completion = useCombatStore.getState().activeCast?.completesAt; expect(completion).toBeDefined(); useCombatStore.getState().tick(1, completion!); const well = useCombatStore.getState().summons.find((summon) => summon.sourceAbilityId === ability.id); expect(well?.creatureId).toBe(31893); expect(useCombatStore.getState().clickLightwell(well!.id, PLAYER_AURA_ENTITY_ID, completion! + 1)).toBe(1_405 * 3); expect(useCombatStore.getState().effects).toContainEqual(expect.objectContaining({ sourceAbilityId: ability.id, basePoints: 1_305, coefficient: 0.3333, remainingTicks: 3, intervalMs: 2_000, })); }); it("cancels a Lightwell HoT after cumulative effective damage reaches 30% of recipient max health", () => { unlock("Lightwell"); const ability = priestAbility("Lightwell"); expect(useCombatStore.getState().castAbility(ability.id, [0, 0, 0], 1_000).ok).toBe(true); const completion = useCombatStore.getState().activeCast?.completesAt; expect(completion).toBeDefined(); useCombatStore.getState().tick(1, completion!); const well = useCombatStore.getState().summons.find((summon) => summon.sourceAbilityId === ability.id); expect(well).toBeDefined(); const maxHealth = useCombatStore.getState().maxHealth; useCombatStore.setState({ health: maxHealth, shield: 0 }); useCombatStore.getState().clickLightwell(well!.id, PLAYER_AURA_ENTITY_ID, completion! + 1); const threshold = maxHealth * 0.3; const firstDamage = useCombatStore.getState().damagePlayer(Math.floor(maxHealth * 0.2), "shadow", undefined, completion! + 2); const afterFirstHit = useCombatStore.getState().effects.find((effect) => effect.kind === "hot" && effect.sourceAbilityId === ability.id); expect(firstDamage).toBeGreaterThan(0); expect(firstDamage).toBeLessThan(threshold); expect(afterFirstHit?.lightwellDamageCancellation).toEqual({ threshold, accumulatedDamage: firstDamage }); const secondDamage = useCombatStore.getState().damagePlayer(Math.floor(maxHealth * 0.2), "shadow", undefined, completion! + 3); expect(firstDamage + secondDamage).toBeGreaterThanOrEqual(threshold); expect(useCombatStore.getState().effects.some((effect) => effect.kind === "hot" && effect.sourceAbilityId === ability.id)).toBe(false); useCombatStore.getState().clickLightwell(well!.id, PLAYER_AURA_ENTITY_ID, completion! + 4); expect(useCombatStore.getState().effects.find((effect) => effect.kind === "hot" && effect.sourceAbilityId === ability.id)?.lightwellDamageCancellation).toEqual({ threshold, accumulatedDamage: 0, }); }); it("does not count fully absorbed damage and starts a fresh threshold after the HoT expires", () => { unlock("Lightwell"); const ability = priestAbility("Lightwell"); expect(useCombatStore.getState().castAbility(ability.id, [0, 0, 0], 1_000).ok).toBe(true); const completion = useCombatStore.getState().activeCast?.completesAt; expect(completion).toBeDefined(); useCombatStore.getState().tick(1, completion!); const well = useCombatStore.getState().summons.find((summon) => summon.sourceAbilityId === ability.id); expect(well).toBeDefined(); useCombatStore.getState().clickLightwell(well!.id, PLAYER_AURA_ENTITY_ID, completion! + 1); useCombatStore.setState({ shield: 2_000 }); expect(useCombatStore.getState().damagePlayer(1_000, "shadow", undefined, completion! + 2)).toBe(0); expect(useCombatStore.getState().effects.find((effect) => effect.kind === "hot" && effect.sourceAbilityId === ability.id)?.lightwellDamageCancellation?.accumulatedDamage).toBe(0); useCombatStore.getState().tick(1, completion! + 6_001); expect(useCombatStore.getState().effects.some((effect) => effect.kind === "hot" && effect.sourceAbilityId === ability.id)).toBe(false); useCombatStore.getState().clickLightwell(well!.id, PLAYER_AURA_ENTITY_ID, completion! + 6_002); expect(useCombatStore.getState().effects.find((effect) => effect.kind === "hot" && effect.sourceAbilityId === ability.id)?.lightwellDamageCancellation?.accumulatedDamage).toBe(0); }); it("tracks Lightwell damage separately for each recipient and on resolved enemy hits", () => { unlock("Lightwell"); const ability = priestAbility("Lightwell"); expect(useCombatStore.getState().castAbility(ability.id, [0, 0, 0], 1_000).ok).toBe(true); const completion = useCombatStore.getState().activeCast?.completesAt; expect(completion).toBeDefined(); useCombatStore.getState().tick(1, completion!); const well = useCombatStore.getState().summons.find((summon) => summon.sourceAbilityId === ability.id); expect(well).toBeDefined(); const member = usePartyStore.getState().members[0]!; useCombatStore.getState().clickLightwell(well!.id, PLAYER_AURA_ENTITY_ID, completion! + 1); useCombatStore.getState().clickLightwell(well!.id, member.id, completion! + 1); useCombatStore.getState().registerMob("priest-lightwell-hit", { name: "Lightwell damage test", maxHealth: 100_000, xpReward: 0, attacks: [{ ...DEFAULT_ENEMY_ATTACK, id: "priest-lightwell-shadow-cast", name: "Shadow cast", delivery: "projectile", animation: "cast", school: "shadow", range: 30, castTimeMs: 0, }], }); setMobPosition("priest-lightwell-hit", [8, 0, 0]); useCombatStore.getState().damageMob("priest-lightwell-hit", 1, completion! + 1); const beforeHit = useCombatStore.getState().health; useCombatStore.getState().advanceMobCombat(completion! + 451, [0, 0, 0]); const appliedDamage = beforeHit - useCombatStore.getState().health; const effects = useCombatStore.getState().effects.filter((effect) => effect.kind === "hot" && effect.sourceAbilityId === ability.id); expect(appliedDamage).toBeGreaterThan(0); expect(effects.find((effect) => effect.targetId === null)?.lightwellDamageCancellation?.accumulatedDamage) .toBe(appliedDamage); expect(effects.find((effect) => effect.targetId === member.id)?.lightwellDamageCancellation?.accumulatedDamage) .toBe(0); }); it("counts resolved enemy attacks against a party member toward that member's Lightwell cancellation", () => { unlock("Lightwell"); const ability = priestAbility("Lightwell"); expect(useCombatStore.getState().castAbility(ability.id, [0, 0, 0], 1_000).ok).toBe(true); const completion = useCombatStore.getState().activeCast?.completesAt; expect(completion).toBeDefined(); useCombatStore.getState().tick(1, completion!); const well = useCombatStore.getState().summons.find((summon) => summon.sourceAbilityId === ability.id); expect(well).toBeDefined(); const member = usePartyStore.getState().members[0]!; setMemberHealth(member.id, 100_000, 100_000); useCombatStore.getState().clickLightwell(well!.id, member.id, completion! + 1); useCombatStore.getState().registerMob("priest-lightwell-party-hit", { name: "Lightwell party damage test", maxHealth: 100_000, xpReward: 0, attacks: [{ ...DEFAULT_ENEMY_ATTACK, id: "priest-lightwell-party-strike", name: "Party strike", range: 30, cooldownMs: 1_900, }], }); setMobPosition("priest-lightwell-party-hit", [4, 0, 0]); useCombatStore.getState().damageMob("priest-lightwell-party-hit", 1, completion! + 1, { actorId: member.id, role: member.role, }); useCombatStore.getState().advanceMobCombat(completion! + 451, [0, 0, 0]); const damagedMember = usePartyStore.getState().members.find((candidate) => candidate.id === member.id)!; const effectiveDamage = 100_000 - damagedMember.health; const memberHot = useCombatStore.getState().effects.find((effect) => effect.kind === "hot" && effect.sourceAbilityId === ability.id && effect.targetId === member.id); expect(effectiveDamage).toBeGreaterThan(0); expect(memberHot?.lightwellDamageCancellation?.accumulatedDamage).toBe(effectiveDamage); }); it("applies Spell Warding to incoming magic damage by rank and leaves physical damage unchanged", () => { const incomingDamageAtRank = (rank: number, school: "physical" | "shadow"): number => { setPriestTalentRanks(rank > 0 ? { "talent-411": rank } : {}); useCombatStore.setState({ health: 1_000_000, maxHealth: 1_000_000, shield: 0 }); const before = useCombatStore.getState().health; useCombatStore.getState().damagePlayer(10_000, school, 80, 1_000 + rank); return before - useCombatStore.getState().health; }; const magicDamage = [0, 1, 2, 3].map((rank) => incomingDamageAtRank(rank, "shadow")); expect(magicDamage[0]).toBeGreaterThan(magicDamage[1]!); expect(magicDamage[1]).toBeGreaterThan(magicDamage[2]!); expect(magicDamage[2]).toBeGreaterThan(magicDamage[3]!); expect(magicDamage[3]).toBeCloseTo(magicDamage[0]! * 0.97, -1); const physicalDamage = [0, 1, 2, 3].map((rank) => incomingDamageAtRank(rank, "physical")); expect(physicalDamage).toEqual([physicalDamage[0], physicalDamage[0], physicalDamage[0], physicalDamage[0]]); }); it("applies Spell Warding to resolved enemy spell attacks without reducing enemy melee hits", () => { const resolvedEnemyDamageAtRank = (rank: number, school: "physical" | "shadow"): number => { clearMobRuntimeRegistry(); clearPartyRuntimeRegistry(); useCombatStore.getState().initializeCharacter({ classId: "priest", level: 80 }); useCombatStore.getState().setPlayerPosition([0, 0, 0]); usePartyStore.getState().initializeParty("wow-priest-spell-warding", 80, useCombatStore.getState().health); usePartyStore.getState().selectMember(null); useCombatStore.getState().synchronizeActors(); setPriestTalentRanks(rank > 0 ? { "talent-411": rank } : {}); const mobId = "priest-spell-warding-attacker"; const attackId = "priest-spell-warding-attack"; useCombatStore.getState().registerMob(mobId, { name: "Spell Warding attacker", maxHealth: 100_000, xpReward: 0, attacks: [{ ...DEFAULT_ENEMY_ATTACK, id: attackId, delivery: school === "physical" ? "melee" : "projectile", animation: school === "physical" ? "attack" : "cast", school, range: 30, castTimeMs: 0, }], }); setMobPosition(mobId, [8, 0, 0]); useCombatStore.getState().damageMob(mobId, 1, 1_000); const before = useCombatStore.getState().health; useCombatStore.getState().advanceMobCombat(1_450, [0, 0, 0]); return before - useCombatStore.getState().health; }; const baseSpellHit = resolvedEnemyDamageAtRank(0, "shadow"); const wardedSpellHit = resolvedEnemyDamageAtRank(3, "shadow"); expect(baseSpellHit).toBeGreaterThan(0); expect(wardedSpellHit).toBeLessThan(baseSpellHit); const baseMeleeHit = resolvedEnemyDamageAtRank(0, "physical"); const wardedMeleeHit = resolvedEnemyDamageAtRank(3, "physical"); expect(baseMeleeHit).toBeGreaterThan(0); expect(wardedMeleeHit).toBe(baseMeleeHit); }); });