import { beforeEach, describe, expect, it } from "vitest"; import { abilitiesForCharacter, abilityById } from "./abilityCatalog"; import { PLAYER_AGGRO_ID } from "./aggro"; import { useCombatStore } from "./combatStore"; import { clearMobRuntimeRegistry, setMobPosition } from "./mobRuntimeRegistry"; import { advanceDungeonPartyCombat, resetPartyRuntimeClock, } from "./partyRuntime"; import { clearPartyRuntimeRegistry, registerPartyRuntimePosition, updatePartyRuntimeTargetVisibility, } from "./partyRuntimeRegistry"; import { createConfiguredPartyMember, usePartyStore } from "./partyStore"; import { ROM_SCOUT_PRIEST_DRUMS_OF_WAR, ROM_SCOUT_PRIEST_DRUMS_OF_WAR_AURA, } from "./romScoutPriestOverrides"; const TARGET_ID = "drums-of-war-target"; function initializeScoutPriest(): void { useCombatStore.getState().initializeCharacter({ classId: "rom-scout", secondaryClassId: "rom-priest", raceId: "rom-human", level: 80, }); } function registerTarget(): void { useCombatStore.getState().registerMob(TARGET_ID, { name: TARGET_ID, maxHealth: 1_000_000, xpReward: 0, }); setMobPosition(TARGET_ID, [3, 0, 0]); useCombatStore.getState().selectMob(TARGET_ID); } function shot() { const ability = useCombatStore.getState().abilities.find((candidate) => ( candidate.source === "runewaker" && candidate.dbcSpellId === ROM_SCOUT_PRIEST_DRUMS_OF_WAR.triggerSpellId )); if (!ability) throw new Error("RuneWaker Shot (490423) is missing from Scout/Priest abilities."); return ability; } function landPlayerShot(now: number, seed: string) { const ability = shot(); return useCombatStore.getState().damageMob( TARGET_ID, 100, now, { actorId: PLAYER_AGGRO_ID, role: "damage", abilityId: ability.id }, "physical", { attackKind: "ranged", abilityId: ability.id, seed, rules: { canMiss: false, canDodge: false, canParry: false, canBlock: false }, }, ); } function activeDrumsAura(actorId: string, now: number) { return useCombatStore.getState().auras.find((aura) => ( aura.targetId === actorId && aura.sourceId === PLAYER_AGGRO_ID && aura.definition.id === ROM_SCOUT_PRIEST_DRUMS_OF_WAR_AURA.id && (aura.expiresAt === null || aura.expiresAt > now) )); } describe("RoM Scout/Priest Drums of War integration", () => { beforeEach(() => { clearMobRuntimeRegistry(); clearPartyRuntimeRegistry(); resetPartyRuntimeClock(); initializeScoutPriest(); }); it("applies the local critical-stat aura after a landed Shot and respects its per-caster ICD", () => { registerTarget(); const passive = useCombatStore.getState().abilities.find((ability) => ability.dbcSpellId === 493010); expect(passive).toMatchObject({ classId: "rom-scout", secondaryClassId: "rom-priest", passive: true }); const first = landPlayerShot(1_000, "drums-player-first"); expect(first.combatResult?.landed).toBe(true); expect(activeDrumsAura(PLAYER_AGGRO_ID, 1_000)).toMatchObject({ expiresAt: 9_000, definition: { modifiers: [ { kind: "stat", stat: "physical-critical-strike", operation: "flat", value: 20 }, { kind: "stat", stat: "magical-critical-strike", operation: "flat", value: 20 }, ], }, }); expect(useCombatStore.getState().cooldowns[`${ROM_SCOUT_PRIEST_DRUMS_OF_WAR.cooldownKey}:${PLAYER_AGGRO_ID}`]) .toBe(16_000); landPlayerShot(5_000, "drums-player-on-icd"); expect(activeDrumsAura(PLAYER_AGGRO_ID, 5_000)?.expiresAt).toBe(9_000); landPlayerShot(16_000, "drums-player-ready-again"); expect(activeDrumsAura(PLAYER_AGGRO_ID, 16_000)?.expiresAt).toBe(24_000); }); it("applies only to living party members within 50 yards of the caster", () => { const near = createConfiguredPartyMember({ name: "Near Ally", classId: "priest", specialization: "Discipline", role: "healer", }, 80, 0, "drums-near-party-member"); const far = createConfiguredPartyMember({ name: "Far Ally", classId: "warrior", specialization: "Arms", role: "damage", }, 80, 1, "drums-far-party-member"); usePartyStore.setState({ members: [near, far] }); useCombatStore.getState().synchronizeActors(); registerPartyRuntimePosition(near.id, [40, 0, 0]); registerPartyRuntimePosition(far.id, [51, 0, 0]); registerTarget(); landPlayerShot(1_000, "drums-player-party-radius"); expect(activeDrumsAura(PLAYER_AGGRO_ID, 1_000)).toBeDefined(); expect(useCombatStore.getState().auras.some((aura) => aura.targetId === near.id && aura.sourceId === PLAYER_AGGRO_ID && aura.definition.id === ROM_SCOUT_PRIEST_DRUMS_OF_WAR_AURA.id)).toBe(true); expect(useCombatStore.getState().auras.some((aura) => aura.targetId === far.id && aura.sourceId === PLAYER_AGGRO_ID && aura.definition.id === ROM_SCOUT_PRIEST_DRUMS_OF_WAR_AURA.id)).toBe(false); }); it("passes party Shot through the ranged hit-resolution path before proc application", () => { registerTarget(); const playerHit = landPlayerShot(1_000, "drums-player-independent-icd"); expect(playerHit.combatResult?.landed).toBe(true); const seed = "drums-party-scout"; const member = createConfiguredPartyMember({ name: "Scout Priest", classId: "rom-scout", secondaryClassId: "rom-priest", specialization: "Primary", role: "damage", }, 80, 0, seed); const partyShot = abilitiesForCharacter(member.classId, member.secondaryClassId) .find((ability) => ability.dbcSpellId === ROM_SCOUT_PRIEST_DRUMS_OF_WAR.triggerSpellId); if (!partyShot) throw new Error("RuneWaker Shot (490423) is missing from party Scout/Priest abilities."); const passive = abilitiesForCharacter(member.classId, member.secondaryClassId) .find((ability) => ability.dbcSpellId === ROM_SCOUT_PRIEST_DRUMS_OF_WAR.passiveSpellId); if (!passive) throw new Error("Drums of War (493010) is missing from party Scout/Priest abilities."); expect(abilityById(partyShot.id)?.dbcSpellId).toBe(490_423); usePartyStore.getState().initializeParty(seed, 80, useCombatStore.getState().health, "damage", 2, "rom"); usePartyStore.getState().setCommand("attack"); usePartyStore.setState({ members: [{ ...member, resource: 10_000, maxResource: 10_000, resourcePools: { ...member.resourcePools, "nature-power": 10_000, mana: 10_000 }, activeCast: { abilityId: partyShot.id, school: "physical", mode: "cast", startedAt: 500, completesAt: 3_500, originalDurationMs: 3_000, pushbackHits: 0, origin: [0, 0, 0], targetId: TARGET_ID, }, activeCastTargetId: TARGET_ID, status: "attacking", }] }); registerPartyRuntimePosition(member.id, [0, 0, 0]); registerTarget(); useCombatStore.getState().damageMob(TARGET_ID, 1, 3_400); updatePartyRuntimeTargetVisibility(member.id, TARGET_ID, true); useCombatStore.getState().synchronizeActors(); advanceDungeonPartyCombat(3_500); expect(useCombatStore.getState().auras.some((aura) => aura.sourceId === member.id && aura.targetId === member.id && aura.definition.id === ROM_SCOUT_PRIEST_DRUMS_OF_WAR_AURA.id && aura.expiresAt === 11_500)).toBe(true); expect(useCombatStore.getState().cooldowns[`${ROM_SCOUT_PRIEST_DRUMS_OF_WAR.cooldownKey}:${member.id}`]) .toBe(18_500); expect(activeDrumsAura(PLAYER_AGGRO_ID, 3_500)?.expiresAt).toBe(9_000); }); });