import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { PLAYER_AGGRO_ID } from "./aggro"; import { installCombatClock } from "./combatClock"; import { useCombatStore, PLAYER_AURA_ENTITY_ID } from "./combatStore"; import { clearMobRuntimeRegistry, setMobPosition } from "./mobRuntimeRegistry"; import { createConfiguredPartyMember, usePartyStore } from "./partyStore"; import { EMPTY_ITEM_STATS } from "./itemStats"; import { WOW335_PALADIN_VENGEANCE_AURA_TAG, WOW335_PALADIN_VENGEANCE_DAMAGE_ABILITY_IDS, WOW335_PALADIN_VENGEANCE_TALENT_ID, } from "./wow335PaladinVengeanceRuntime"; const TARGET_ID = "paladin-vengeance-target"; let restoreClock: (() => void) | undefined; function registerTarget(id = TARGET_ID) { useCombatStore.getState().registerMob(id, { name: "Vengeance target", maxHealth: 1_000_000, xpReward: 0 }); setMobPosition(id, [2, 0, 0]); } function criticalHit(actorId: string, now: number) { return useCombatStore.getState().damageMob(TARGET_ID, 100, now, { actorId, role: "damage", abilityId: WOW335_PALADIN_VENGEANCE_DAMAGE_ABILITY_IDS["Crusader Strike"] }, "physical", { attackKind: "melee", seed: `${actorId}:${now}:vengeance-test`, abilityId: WOW335_PALADIN_VENGEANCE_DAMAGE_ABILITY_IDS["Crusader Strike"], criticalChanceBonusPercent: 100, rules: { canMiss: false, canDodge: false, canParry: false, canBlock: false, resistanceMode: "none" }, }); } describe("WoW Paladin Vengeance combat integration", () => { beforeEach(() => { restoreClock = installCombatClock(() => 1_000); clearMobRuntimeRegistry(); useCombatStore.getState().initializeCharacter({ classId: "paladin", level: 80, xp: 0 }); usePartyStore.getState().initializeParty("wow335-paladin-vengeance", 80, useCombatStore.getState().health, "damage", 5); registerTarget(); }); afterEach(() => { restoreClock?.(); restoreClock = undefined; }); it("applies a stack after player crits and uses existing stacks only on the next qualifying hit", () => { useCombatStore.setState((state) => ({ talentRanks: { ...state.talentRanks, [WOW335_PALADIN_VENGEANCE_TALENT_ID]: 3 }, })); const first = criticalHit(PLAYER_AGGRO_ID, 1_000); expect(first.combatResult?.critical).toBe(true); expect(useCombatStore.getState().auras.find((aura) => aura.targetId === PLAYER_AURA_ENTITY_ID && aura.definition.utilityTags?.includes(WOW335_PALADIN_VENGEANCE_AURA_TAG))) .toMatchObject({ stacks: 1, appliedAt: 1_000, expiresAt: 16_000 }); const withStack = useCombatStore.getState().damageMob(TARGET_ID, 100, 2_000, { actorId: PLAYER_AGGRO_ID, role: "damage", abilityId: WOW335_PALADIN_VENGEANCE_DAMAGE_ABILITY_IDS["Crusader Strike"] }, "physical", { attackKind: "melee", seed: "vengeance-followup", abilityId: WOW335_PALADIN_VENGEANCE_DAMAGE_ABILITY_IDS["Crusader Strike"], rules: { canMiss: false, canDodge: false, canParry: false, canBlock: false, canCrit: false, resistanceMode: "none" }, }); expect(withStack.amount).toBeGreaterThan(0); expect(useCombatStore.getState().auras.find((aura) => aura.targetId === PLAYER_AURA_ENTITY_ID && aura.definition.utilityTags?.includes(WOW335_PALADIN_VENGEANCE_AURA_TAG))?.stacks).toBe(1); criticalHit(PLAYER_AGGRO_ID, 3_000); expect(useCombatStore.getState().auras.find((aura) => aura.targetId === PLAYER_AURA_ENTITY_ID && aura.definition.utilityTags?.includes(WOW335_PALADIN_VENGEANCE_AURA_TAG))) .toMatchObject({ stacks: 2, appliedAt: 3_000, expiresAt: 18_000 }); registerTarget("paladin-vengeance-control"); useCombatStore.setState((state) => ({ auras: state.auras.filter((aura) => !aura.definition.utilityTags?.includes( WOW335_PALADIN_VENGEANCE_AURA_TAG, )) })); const withoutStack = useCombatStore.getState().damageMob("paladin-vengeance-control", 100, 2_000, { actorId: PLAYER_AGGRO_ID, role: "damage", abilityId: WOW335_PALADIN_VENGEANCE_DAMAGE_ABILITY_IDS["Crusader Strike"] }, "physical", { attackKind: "melee", seed: "vengeance-followup", abilityId: WOW335_PALADIN_VENGEANCE_DAMAGE_ABILITY_IDS["Crusader Strike"], rules: { canMiss: false, canDodge: false, canParry: false, canBlock: false, canCrit: false, resistanceMode: "none" }, }); expect(withStack.amount).toBeGreaterThan(withoutStack.amount); }); it("uses the companion Paladin's own rank and owner-scoped stack aura", () => { const talentRanks = { [WOW335_PALADIN_VENGEANCE_TALENT_ID]: 2 }; const member = createConfiguredPartyMember({ name: "Vengeance Paladin", classId: "paladin", specialization: "Retribution", specializationId: "paladin:retribution", talentTreeId: "paladin-retribution", talentRanks, role: "damage", }, 80, 0, "paladin-vengeance-companion"); usePartyStore.setState({ members: [{ ...member, gearStats: EMPTY_ITEM_STATS, talentRanks }] }); useCombatStore.getState().synchronizeActors(); const result = criticalHit(member.id, 2_000); expect(result.combatResult?.critical).toBe(true); expect(useCombatStore.getState().auras).toContainEqual(expect.objectContaining({ sourceId: member.id, targetId: member.id, appliedAt: 2_000, expiresAt: 17_000, stacks: 1, definition: expect.objectContaining({ utilityTags: [WOW335_PALADIN_VENGEANCE_AURA_TAG] }), })); }); });