import { beforeEach, describe, expect, it } from "vitest"; import { abilitiesForCharacter, learnedAbilityForBuild } from "./abilityCatalog"; import { resolveDamageValue as resolveAuraDamageValue } from "./combatAuras"; import { clearMobRuntimeRegistry, setMobPosition } from "./mobRuntimeRegistry"; import { PLAYER_AGGRO_ID } from "./aggro"; import { useCombatStore } from "./combatStore"; import { clearPartyRuntimeRegistry, registerPartyRuntimePosition } from "./partyRuntimeRegistry"; import { advanceDungeonPartyCombat, resetPartyRuntimeClock } from "./partyRuntime"; import { createConfiguredPartyMember, usePartyStore } from "./partyStore"; import { EMPTY_ITEM_STATS, type WeaponStats } from "./itemStats"; import type { InventoryItem } from "./lootTypes"; const OFFHAND_ID = "coa-gods-sword-test-offhand"; const OFFHAND: WeaponStats = { damageMin: 100, damageMax: 300, damageSchool: "physical", speedMs: 2_000, }; function offhandItem(instanceId = OFFHAND_ID): InventoryItem { return { instanceId, itemId: 1, itemKey: instanceId, name: "Test Off-hand Dagger", quality: "uncommon", inventorySlot: "off-hand", armorType: null, weaponType: "mace", itemLevel: 1, sourceItemLevel: 1, stats: { ...EMPTY_ITEM_STATS, meleeWeapon: OFFHAND, hitRating: 1_000_000 }, quantity: 1, sourceBossKey: "test", sourceBossName: "Test", acquiredAt: 0, }; } function registerTarget(id: string): void { useCombatStore.getState().registerMob(id, { name: id, level: 1, maxHealth: 1_000_000, xpReward: 0, }); setMobPosition(id, [2, 0, 0]); useCombatStore.getState().selectMob(id); } function expectedLightRiderAmount(actorId: string): number { const auras = useCombatStore.getState().auras.filter((aura) => aura.targetId === actorId); return Math.round(resolveAuraDamageValue(350, auras, { direction: "dealt", school: "holy", attackKind: "spell", })); } describe("current CoA Knight/Rogue God's Sword runtime", () => { beforeEach(() => { clearMobRuntimeRegistry(); clearPartyRuntimeRegistry(); resetPartyRuntimeClock(); useCombatStore.getState().initializeCharacter({ classId: "rom-knight", secondaryClassId: "rom-rogue", raceId: "rom-human", level: 80, xp: 0, }); usePartyStore.getState().initializeParty("coa-gods-sword-runtime", 80, useCombatStore.getState().health); }); it("adds one separate 350% off-hand-DPS Light hit to a landed player physical ability", () => { const item = offhandItem(); useCombatStore.getState().initializeCharacter({ classId: "rom-knight", secondaryClassId: "rom-rogue", raceId: "rom-human", level: 80, xp: 0, inventory: [item], equipment: { player: { "off-hand": item.instanceId } }, }); registerTarget("coa-gods-sword-player-target"); const ability = useCombatStore.getState().abilities.find((candidate) => candidate.dbcSpellId === 490148)!; expect(useCombatStore.getState().abilities.find((candidate) => candidate.dbcSpellId === 491621)) .toMatchObject({ passive: true, unlockLevel: 15 }); expect(useCombatStore.getState().castAbility(ability.id, undefined, 1_000)).toMatchObject({ ok: true }); const events = useCombatStore.getState().combatEvents.filter((event) => ( event.kind === "damage" && event.sourceActorId === PLAYER_AGGRO_ID && event.targetActorId === "coa-gods-sword-player-target" && event.abilityId === ability.id )); expect(events.some((event) => event.school === "holy" && event.rawAmount !== 350 && event.effectiveAmount > 0)).toBe(true); expect(events.filter((event) => event.school === "holy" && event.rawAmount === expectedLightRiderAmount(PLAYER_AGGRO_ID))) .toHaveLength(1); }); it("does not add the CoA rider before the passive is learned or without an off-hand weapon", () => { const simulate = (level: number, equipOffhand: boolean) => { const item = offhandItem(`coa-gods-sword-gate-${level}-${equipOffhand}`); useCombatStore.getState().initializeCharacter({ classId: "rom-knight", secondaryClassId: "rom-rogue", raceId: "rom-human", level, xp: 0, ...(equipOffhand ? { inventory: [item], equipment: { player: { "off-hand": item.instanceId } }, } : {}), }); const targetId = `coa-gods-sword-gate-target-${level}-${equipOffhand}`; registerTarget(targetId); const ability = useCombatStore.getState().abilities.find((candidate) => candidate.dbcSpellId === 490148)!; useCombatStore.getState().castAbility(ability.id, undefined, 1_000); return useCombatStore.getState().combatEvents.filter((event) => ( event.kind === "damage" && event.targetActorId === targetId && event.abilityId === ability.id && event.school === "holy" && event.rawAmount === 350 )); }; expect(simulate(14, true)).toHaveLength(0); expect(simulate(80, false)).toHaveLength(0); }); it("adds the same landed-hit rider for a generated Knight/Rogue companion", () => { useCombatStore.getState().initializeCharacter({ classId: "rom-mage", level: 80, xp: 0 }); const seed = "coa-gods-sword-party"; const companion = createConfiguredPartyMember({ name: "Knight/Rogue", classId: "rom-knight", secondaryClassId: "rom-rogue", raceId: "rom-human", specialization: "Knight/Rogue", role: "damage", }, 80, 0, seed); const sourceAbility = abilitiesForCharacter("rom-knight", "rom-rogue") .find((candidate) => candidate.dbcSpellId === 490148)!; const ability = learnedAbilityForBuild(sourceAbility, 80, companion.talentRanks)!; const targetId = "coa-gods-sword-companion-target"; const item = { ...offhandItem(`${companion.id}:${OFFHAND_ID}`), }; usePartyStore.getState().initializeParty(seed, 80, useCombatStore.getState().health, "damage", 2, "rom"); usePartyStore.setState({ members: [{ ...companion, gearStats: { ...EMPTY_ITEM_STATS, hitRating: 1_000_000 }, globalCooldownEndsAt: 0, nextActionAt: 0, activeCast: { abilityId: ability.id, school: "physical", mode: "cast", startedAt: 1_000, completesAt: 1_000, originalDurationMs: 0, pushbackHits: 0, origin: [0, 0, 0], targetId, }, activeCastTargetId: targetId, status: "attacking", }], command: "attack", activeMobId: targetId, }); useCombatStore.setState((state) => ({ inventory: [...state.inventory, item], equipment: { ...state.equipment, [companion.id]: { "off-hand": item.instanceId } }, })); registerPartyRuntimePosition(companion.id, [0, 0, 0]); useCombatStore.getState().synchronizeActors(); registerTarget(targetId); advanceDungeonPartyCombat(1_000); const events = useCombatStore.getState().combatEvents.filter((event) => ( event.kind === "damage" && event.sourceActorId === companion.id && event.targetActorId === targetId && event.abilityId === ability.id )); expect(events.some((event) => event.school === "holy" && event.rawAmount !== 350 && event.effectiveAmount > 0)).toBe(true); expect(events.filter((event) => event.school === "holy" && event.rawAmount === expectedLightRiderAmount(companion.id))) .toHaveLength(1); }); });