Update Healer Man server
This commit is contained in:
1 parent
4c768fbec6
commit
b16821e92e
330 files changed
+35406
-1760
No files matched your search
@@ -0,0 +1,281 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user