import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { PLAYER_AGGRO_ID } from "./aggro"; import { abilitiesForCharacter } from "./abilityCatalog"; import { installCombatClock } from "./combatClock"; import { createDeathKnightRunes } from "./combatActors"; import { useCombatStore, PLAYER_AURA_ENTITY_ID } from "./combatStore"; import { clearMobRuntimeRegistry, setMobPosition } from "./mobRuntimeRegistry"; import { clearPartyRuntimeRegistry, registerPartyRuntimePosition, updatePartyRuntimeTargetVisibility } from "./partyRuntimeRegistry"; import { advanceDungeonPartyCombat, resetPartyRuntimeClock } from "./partyRuntime"; import { createConfiguredPartyMember, usePartyStore } from "./partyStore"; import { EMPTY_ITEM_STATS } from "./itemStats"; import { WOW335_DEATH_KNIGHT_BLOODY_VENGEANCE_TALENT_ID, } from "./wow335DeathKnightBloodyVengeanceRuntime"; const TARGET_ID = "bloody-vengeance-target"; const PARTY_SEED = "bloody-vengeance-party"; let restoreClock: (() => void) | undefined; function bloodStrike() { const found = abilitiesForCharacter("death-knight", null) .find((ability) => ability.name === "Blood Strike"); if (!found) throw new Error("Missing generated Death Knight Blood Strike."); return found; } function registerTarget() { useCombatStore.getState().registerMob(TARGET_ID, { name: "Bloody Vengeance target", maxHealth: 1_000_000, xpReward: 0, }); setMobPosition(TARGET_ID, [2, 0, 0]); useCombatStore.getState().selectMob(TARGET_ID); } function guaranteedCriticalBloodStrike(sourceActorId: string, now: number) { const strike = bloodStrike(); return useCombatStore.getState().damageMob( TARGET_ID, 100, now, { actorId: sourceActorId, role: "damage", abilityId: strike.id }, "physical", { attackKind: "melee", seed: `${sourceActorId}:${now}:bloody-vengeance-test`, abilityId: strike.id, criticalChanceBonusPercent: 100, rules: { canMiss: false, canDodge: false, canParry: false, canBlock: false, resistanceMode: "none", }, }, ); } describe("WoW Death Knight Bloody Vengeance combat integration", () => { beforeEach(() => { restoreClock = installCombatClock(() => 1_000); clearMobRuntimeRegistry(); clearPartyRuntimeRegistry(); resetPartyRuntimeClock(); useCombatStore.getState().initializeCharacter({ classId: "death-knight", level: 80, xp: 0 }); usePartyStore.getState().initializeParty( "wow335-death-knight-bloody-vengeance", 80, useCombatStore.getState().health, "tank", 5, ); useCombatStore.setState({ deathKnightRunes: createDeathKnightRunes(1_000) }); registerTarget(); }); afterEach(() => { restoreClock?.(); restoreClock = undefined; }); it("adds up to three stacks for player critical hits and refreshes the generated duration", () => { useCombatStore.setState((state) => ({ talentRanks: { ...state.talentRanks, [WOW335_DEATH_KNIGHT_BLOODY_VENGEANCE_TALENT_ID]: 3 }, })); const first = guaranteedCriticalBloodStrike(PLAYER_AGGRO_ID, 1_000); expect(first.combatResult?.critical).toBe(true); let aura = useCombatStore.getState().auras.find((candidate) => candidate.targetId === PLAYER_AURA_ENTITY_ID && candidate.definition.name === "Bloody Vengeance"); expect(aura).toMatchObject({ stacks: 1, appliedAt: 1_000, expiresAt: 31_000 }); for (const now of [2_000, 3_000, 4_000]) { expect(guaranteedCriticalBloodStrike(PLAYER_AGGRO_ID, now).combatResult?.critical).toBe(true); aura = useCombatStore.getState().auras.find((candidate) => candidate.targetId === PLAYER_AURA_ENTITY_ID && candidate.definition.name === "Bloody Vengeance"); } expect(aura).toMatchObject({ stacks: 3, appliedAt: 4_000, expiresAt: 34_000 }); }); it("uses a companion Death Knight's own rank and applies its aura to that companion", () => { const talentRanks = { [WOW335_DEATH_KNIGHT_BLOODY_VENGEANCE_TALENT_ID]: 2 }; const member = createConfiguredPartyMember({ name: "Bloody Vengeance DK", classId: "death-knight", specialization: "Blood", specializationId: "death-knight:blood", talentTreeId: "death-knight-blood", talentRanks, role: "damage", }, 80, 0, PARTY_SEED); usePartyStore.setState({ members: [{ ...member, gearStats: EMPTY_ITEM_STATS, talentRanks, runes: createDeathKnightRunes(1_000), }] }); useCombatStore.getState().synchronizeActors(); const result = guaranteedCriticalBloodStrike(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: 32_000, definition: expect.objectContaining({ name: "Bloody Vengeance", modifiers: [expect.objectContaining({ kind: "damage", direction: "dealt", value: 0.02, school: "physical" })], }), })); }); it("gives a talented companion critical-result contexts for auto attacks", () => { const talentRanks = { [WOW335_DEATH_KNIGHT_BLOODY_VENGEANCE_TALENT_ID]: 1 }; const member = createConfiguredPartyMember({ name: "Bloody Vengeance auto attacker", classId: "death-knight", specialization: "Blood", specializationId: "death-knight:blood", talentTreeId: "death-knight-blood", talentRanks, role: "damage", }, 80, 0, `${PARTY_SEED}:auto`); usePartyStore.setState({ members: [{ ...member, gearStats: { ...EMPTY_ITEM_STATS, criticalStrikeRating: 1_000_000, hitRating: 1_000_000, expertiseRating: 1_000_000, }, talentRanks, runes: createDeathKnightRunes(1_000), }] }); useCombatStore.getState().synchronizeActors(); registerPartyRuntimePosition(member.id, [0, 0, 0]); useCombatStore.getState().damageMob(TARGET_ID, 1, 100); updatePartyRuntimeTargetVisibility(member.id, TARGET_ID, true); advanceDungeonPartyCombat(1_000); expect(useCombatStore.getState().auras).toContainEqual(expect.objectContaining({ sourceId: member.id, targetId: member.id, definition: expect.objectContaining({ name: "Bloody Vengeance" }), })); }); });