import { beforeAll, beforeEach, describe, expect, it } from "vitest"; import { PLAYER_AGGRO_ID } from "./aggro"; import { abilitiesForCharacter, preloadAbilityCatalogsForClasses } from "./abilityCatalog"; import { applyAura } from "./combatAuras"; import { clearMobRuntimeRegistry, setMobPosition } from "./mobRuntimeRegistry"; import { clearPartyRuntimeRegistry, registerPartyRuntimePosition, updatePartyRuntimeTargetVisibility, } from "./partyRuntimeRegistry"; import { advanceDungeonPartyCombat, resetPartyRuntimeClock } from "./partyRuntime"; import { useCombatStore, PLAYER_AURA_ENTITY_ID } from "./combatStore"; import { createConfiguredPartyMember, usePartyStore } from "./partyStore"; import { ROM_WARLOCK_MAGE_WILLPOWER_BLADE_STATE_TAG } from "./romWarlockMageSoulBrandSting"; import { ROM_WARLOCK_PRIEST_SPREADING_PAIN_CRIT_TAG } from "./romWarlockPriestSpreadingPain"; import { partyAbilityAmount, partyAbilityIsReady } from "./partyAbilityAi"; const SOUL_BRAND_TARGET = "rom-warlock-mage-soul-brand-target"; const CRIT_TARGET = "rom-warlock-mage-spreading-pain-target"; const STING_TARGET = "rom-warlock-mage-soul-brand-sting-target"; function prepareTarget(id: string, name: string): void { useCombatStore.getState().registerMob(id, { name, maxHealth: 100_000 }); setMobPosition(id, [5, 0, 0]); } function applyStateAura(actorId: string, tag: string, auraId: string, name: string, now: number): void { const current = useCombatStore.getState(); const aura = applyAura(current.auras, { definition: { id: auraId, name, disposition: "buff", durationMs: 30_000, utilityTags: [tag] }, sourceId: actorId, targetId: actorId, now, }); useCombatStore.setState({ auras: [...aura.auras] }); } function damageSpell(targetId: string, sourceActorId: string, school: "fire" | "shadow", now: number, periodic = false) { return useCombatStore.getState().damageMob( targetId, 1_000, now, { actorId: sourceActorId, role: "damage", abilityId: "coa-runtime-test-spell" }, school, { attackKind: "spell", abilityId: "coa-runtime-test-spell", seed: `${sourceActorId}:${school}:${now}`, periodic, rules: { canMiss: false, canDodge: false, canParry: false, canBlock: false }, }, ); } describe("current CoA Warlock hybrid runtime hooks", () => { beforeAll(async () => { await preloadAbilityCatalogsForClasses(["rom-warlock"]); }); beforeEach(() => { clearMobRuntimeRegistry(); clearPartyRuntimeRegistry(); resetPartyRuntimeClock(); useCombatStore.getState().initializeCharacter({ classId: "rom-warlock", secondaryClassId: "rom-mage", raceId: "rom-dwarf", level: 80, }); }); it("procs Soul Brand on player direct and periodic Fire damage, stacks to four, and restores 1 Psi per landed event", () => { prepareTarget(SOUL_BRAND_TARGET, "Soul Brand target"); useCombatStore.setState((state) => ({ resourcePools: { ...state.resourcePools, psi: 1 }, maxResourcePools: { ...state.maxResourcePools, psi: 6 }, })); expect(damageSpell(SOUL_BRAND_TARGET, PLAYER_AGGRO_ID, "fire", 1_000).amount).toBeGreaterThan(0); expect(damageSpell(SOUL_BRAND_TARGET, PLAYER_AGGRO_ID, "fire", 2_000).amount).toBeGreaterThan(0); expect(damageSpell(SOUL_BRAND_TARGET, PLAYER_AGGRO_ID, "shadow", 3_000).amount).toBeGreaterThan(0); expect(damageSpell(SOUL_BRAND_TARGET, PLAYER_AGGRO_ID, "fire", 4_000, true).amount).toBeGreaterThan(0); const state = useCombatStore.getState(); expect(state.auras.find((aura) => aura.targetId === SOUL_BRAND_TARGET && aura.definition.id === "rom-warlock-mage-soul-brand-498705")?.stacks).toBe(3); expect(state.resourcePools.psi).toBe(4); }); it("gates Soul Brand Sting for both player and companion until the tagged Willpower Blade state is active", () => { const ability = abilitiesForCharacter("rom-warlock", "rom-mage") .find((candidate) => candidate.dbcSpellId === 498710)!; useCombatStore.setState((state) => ({ resourcePools: { ...state.resourcePools, psi: 4 }, maxResourcePools: { ...state.maxResourcePools, psi: 6 }, })); expect(useCombatStore.getState().castAbility(ability.id, { now: 1_000 })).toMatchObject({ ok: false, reason: "condition-not-met", }); prepareTarget(STING_TARGET, "Soul Brand Sting target"); usePartyStore.getState().initializeParty( "rom-warlock-mage-gate-party", 80, useCombatStore.getState().health, "damage", 2, "rom", ); const member = createConfiguredPartyMember({ name: "Warlock Mage", classId: "rom-warlock", secondaryClassId: "rom-mage", raceId: "rom-dwarf", specialization: "Warlock", role: "damage", }, 80, 0, "rom-warlock-mage-gate-party"); const partyMember = { ...member, resourcePools: { ...member.resourcePools, psi: 4 }, cooldowns: { ...member.cooldowns, [ability.id]: 0 }, }; const stingDamage = ability.effects.find((effect) => effect.kind === "damage"); if (stingDamage?.kind === "damage") { expect(partyAbilityAmount(partyMember, ability, stingDamage)).toBeGreaterThan(3_737.5); } usePartyStore.setState({ members: [partyMember] }); expect(partyAbilityIsReady(partyMember, ability, 1_000)).toBe(false); applyStateAura(PLAYER_AURA_ENTITY_ID, ROM_WARLOCK_MAGE_WILLPOWER_BLADE_STATE_TAG, "local-willpower-blade", "Willpower Blade", 1_100); applyStateAura(member.id, ROM_WARLOCK_MAGE_WILLPOWER_BLADE_STATE_TAG, "companion-willpower-blade", "Willpower Blade", 1_100); expect(partyAbilityIsReady(partyMember, ability, 1_200)).toBe(true); expect(useCombatStore.getState().castAbility(ability.id, { targetActorId: STING_TARGET, now: 1_200, })).toMatchObject({ ok: true }); const activeCast = useCombatStore.getState().activeCast; expect(activeCast?.abilityId).toBe(ability.id); const completedCast = useCombatStore.getState().castAbility(ability.id, { targetActorId: STING_TARGET, now: 2_300, completingCast: activeCast ?? undefined, }); expect(completedCast.ok).toBe(true); expect(completedCast.damage).toBeGreaterThan(7_475); }); it("applies Spreading Pain's +25% critical bonus only to magical spells for its exact Warlock/Priest build", () => { useCombatStore.getState().initializeCharacter({ classId: "rom-warlock", secondaryClassId: "rom-priest", raceId: "rom-dwarf", level: 80, }); prepareTarget(CRIT_TARGET, "Spreading Pain target"); const hit = (now: number) => useCombatStore.getState().damageMob( CRIT_TARGET, 1_000, now, { actorId: PLAYER_AGGRO_ID, role: "damage", abilityId: "coa-critical-spell" }, "fire", { attackKind: "spell", abilityId: "coa-critical-spell", seed: `spreading-pain:${now}`, criticalChanceBonusPercent: 100, rules: { canMiss: false, canDodge: false, canParry: false, canBlock: false }, }, ); const baseline = hit(2_000); expect(baseline.combatResult?.critical).toBe(true); applyStateAura(PLAYER_AURA_ENTITY_ID, ROM_WARLOCK_PRIEST_SPREADING_PAIN_CRIT_TAG, "spreading-pain-critical", "Spreading Pain: Magical Critical Bonus", 2_100); const buffed = hit(2_200); expect(buffed.combatResult?.critical).toBe(true); expect(buffed.amount).toBe(Math.round(baseline.amount * 1.125)); }); it("uses the same Spreading Pain critical bonus for generated companion spells", () => { useCombatStore.getState().initializeCharacter({ classId: "rom-priest", raceId: "rom-dwarf", level: 80 }); usePartyStore.getState().initializeParty( "rom-warlock-priest-spreading-pain-party", 80, useCombatStore.getState().health, "damage", 2, "rom", ); const member = createConfiguredPartyMember({ name: "Warlock Priest", classId: "rom-warlock", secondaryClassId: "rom-priest", raceId: "rom-dwarf", specialization: "Warlock", role: "damage", }, 80, 0, "rom-warlock-priest-spreading-pain-party"); usePartyStore.setState({ members: [member] }); useCombatStore.getState().synchronizeActors(); prepareTarget("rom-warlock-priest-crit-target", "Companion crit target"); const hit = (now: number) => useCombatStore.getState().damageMob( "rom-warlock-priest-crit-target", 1_000, now, { actorId: member.id, role: "damage", abilityId: "coa-party-critical-spell" }, "fire", { attackKind: "spell", abilityId: "coa-party-critical-spell", seed: `companion-spreading-pain:${now}`, criticalChanceBonusPercent: 100, rules: { canMiss: false, canDodge: false, canParry: false, canBlock: false }, }, ); const baseline = hit(2_000); expect(baseline.combatResult?.critical).toBe(true); applyStateAura(member.id, ROM_WARLOCK_PRIEST_SPREADING_PAIN_CRIT_TAG, "companion-spreading-pain-critical", "Spreading Pain: Magical Critical Bonus", 2_100); const buffed = hit(2_200); expect(buffed.combatResult?.critical).toBe(true); expect(buffed.amount).toBe(Math.round(baseline.amount * 1.125)); }); it("routes deterministic companion Fire hits through Soul Brand's post-landed hook", () => { useCombatStore.getState().initializeCharacter({ classId: "rom-priest", raceId: "rom-dwarf", level: 80 }); usePartyStore.getState().initializeParty( "rom-warlock-mage-soul-brand-party", 80, useCombatStore.getState().health, "damage", 2, "rom", ); const member = createConfiguredPartyMember({ name: "Warlock Mage", classId: "rom-warlock", secondaryClassId: "rom-mage", raceId: "rom-dwarf", specialization: "Warlock", role: "damage", }, 80, 0, "rom-warlock-mage-soul-brand-party"); const pairAbilities = abilitiesForCharacter("rom-warlock", "rom-mage"); const flameStrike = pairAbilities.find((ability) => ability.dbcSpellId === 498697)!; const cooldowns = Object.fromEntries(pairAbilities.map((ability) => [ ability.id, ability.id === flameStrike.id ? 0 : 100_000, ])); const runtimeMember = { ...member, cooldowns, resourcePools: { ...member.resourcePools, psi: 3 }, resource: member.resourceType === "psi" ? 3 : member.resource, nextActionAt: 0, globalCooldownEndsAt: 0, }; usePartyStore.setState({ command: "attack", activeMobId: SOUL_BRAND_TARGET, memberTargetIds: { [member.id]: SOUL_BRAND_TARGET }, members: [runtimeMember] }); useCombatStore.getState().synchronizeActors(); registerPartyRuntimePosition(member.id, [0, 0, 0]); prepareTarget(SOUL_BRAND_TARGET, "Companion Soul Brand target"); useCombatStore.getState().damageMob(SOUL_BRAND_TARGET, 100, 900); updatePartyRuntimeTargetVisibility(member.id, SOUL_BRAND_TARGET, true); for (const now of [1_000, 1_300, 3_500, 3_800, 6_000]) advanceDungeonPartyCombat(now); expect(damageSpell(SOUL_BRAND_TARGET, member.id, "fire", 7_000, true).amount).toBeGreaterThan(0); const targetAura = useCombatStore.getState().auras.find((aura) => ( aura.targetId === SOUL_BRAND_TARGET && aura.sourceId === member.id && aura.definition.id === "rom-warlock-mage-soul-brand-498705" )); expect(targetAura?.stacks).toBe(2); expect(usePartyStore.getState().members.find((candidate) => candidate.id === member.id)?.resourcePools.psi) .toBeGreaterThan(3); }); });