Files
healer-man/src/game/romWarlockMageCoaRuntime.integration.test.ts
T
2026-10-02 10:38:12 -04:00

282 lines
11 KiB
TypeScript

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);
});
});