189 lines
7.9 KiB
TypeScript
189 lines
7.9 KiB
TypeScript
import { beforeEach, describe, expect, it } from "vitest";
|
|
import { PLAYER_AGGRO_ID } from "./aggro";
|
|
import { useCombatStore } from "./combatStore";
|
|
import { clearMobRuntimeRegistry, setMobPosition } from "./mobRuntimeRegistry";
|
|
import { advanceDungeonPartyCombat, resetPartyRuntimeClock } from "./partyRuntime";
|
|
import { clearPartyRuntimeRegistry, registerPartyRuntimePosition } from "./partyRuntimeRegistry";
|
|
import { ROM_ALL_ABILITIES } from "./romAbilityCatalog";
|
|
import {
|
|
ROM_DRUID_MAGE_INSPIRATION_AURA_TAG,
|
|
romDruidMageInspirationApplySpendStack,
|
|
romDruidMageInspirationHastePercent,
|
|
} from "./romDruidMageInspirationRuntime";
|
|
import { createConfiguredPartyMember, usePartyStore } from "./partyStore";
|
|
|
|
const EARTH_CHAIN_ID = "rom-druid-rom-mage-elite-15";
|
|
const earthChain = ROM_ALL_ABILITIES.find((ability) => ability.id === EARTH_CHAIN_ID);
|
|
|
|
function inspirationAura(targetId: string) {
|
|
return useCombatStore.getState().auras.find((aura) => aura.targetId === targetId
|
|
&& aura.definition.utilityTags?.includes(ROM_DRUID_MAGE_INSPIRATION_AURA_TAG));
|
|
}
|
|
|
|
function priest(name: string, seed: string, index: number) {
|
|
return createConfiguredPartyMember({
|
|
name,
|
|
classId: "rom-priest",
|
|
specialization: "Primary",
|
|
role: "damage",
|
|
}, 45, index, seed);
|
|
}
|
|
|
|
describe("CoA Druid/Mage Inspiration player and companion integration", () => {
|
|
beforeEach(() => {
|
|
resetPartyRuntimeClock();
|
|
clearMobRuntimeRegistry();
|
|
clearPartyRuntimeRegistry();
|
|
useCombatStore.getState().initializeCharacter({
|
|
classId: "rom-priest",
|
|
raceId: "rom-human",
|
|
level: 45,
|
|
role: "healer",
|
|
});
|
|
usePartyStore.getState().initializeParty(
|
|
"inspiration-party-test", 45, useCombatStore.getState().health, "healer", 4, "rom",
|
|
);
|
|
});
|
|
|
|
it("awards one nearby-party stack for a player's completed Nature Power spend and applies cast haste", () => {
|
|
if (!earthChain) throw new Error("Missing generated Druid/Mage Earth Chain row");
|
|
useCombatStore.getState().initializeCharacter({
|
|
classId: "rom-druid",
|
|
secondaryClassId: "rom-mage",
|
|
raceId: "rom-elf",
|
|
level: 45,
|
|
role: "damage",
|
|
});
|
|
useCombatStore.getState().setPlayerPosition([0, 0, 0]);
|
|
|
|
const seed = "inspiration-player-spend";
|
|
const edge = priest("Range Edge", seed, 0);
|
|
const outside = priest("Outside Range", seed, 1);
|
|
const fallen = { ...priest("Fallen Ally", seed, 2), health: 0 };
|
|
usePartyStore.setState({ members: [edge, outside, fallen] });
|
|
registerPartyRuntimePosition(edge.id, [3, 4, 0]);
|
|
registerPartyRuntimePosition(outside.id, [5.01, 0, 0]);
|
|
registerPartyRuntimePosition(fallen.id, [1, 0, 0]);
|
|
useCombatStore.getState().synchronizeActors();
|
|
|
|
useCombatStore.setState((state) => ({
|
|
resourcePools: { ...state.resourcePools, "nature-power": 1 },
|
|
maxResourcePools: { ...state.maxResourcePools, "nature-power": 10 },
|
|
}));
|
|
const targetId = "inspiration-player-target";
|
|
useCombatStore.getState().registerMob(targetId, { name: "Target", level: 1, maxHealth: 100_000, xpReward: 0 });
|
|
setMobPosition(targetId, [2, 0, 0]);
|
|
useCombatStore.getState().selectMob(targetId);
|
|
|
|
expect(useCombatStore.getState().castAbility(EARTH_CHAIN_ID, {
|
|
origin: [0, 0, 0], targetActorId: targetId, now: 1_000,
|
|
}).ok).toBe(true);
|
|
const unbuffedCastDuration = useCombatStore.getState().activeCast?.originalDurationMs ?? 0;
|
|
expect(unbuffedCastDuration).toBeGreaterThan(0);
|
|
useCombatStore.getState().tick(2, 3_000);
|
|
|
|
expect(useCombatStore.getState().resourcePools["nature-power"]).toBe(0);
|
|
expect(inspirationAura(PLAYER_AGGRO_ID)).toMatchObject({ stacks: 1, expiresAt: 13_000 });
|
|
expect(inspirationAura(edge.id)?.stacks).toBe(1);
|
|
expect(inspirationAura(outside.id)).toBeUndefined();
|
|
expect(inspirationAura(fallen.id)).toBeUndefined();
|
|
|
|
expect(useCombatStore.getState().castAbility(EARTH_CHAIN_ID, {
|
|
origin: [0, 0, 0], targetActorId: targetId, now: 7_000,
|
|
}).ok).toBe(true);
|
|
expect(useCombatStore.getState().activeCast?.originalDurationMs).toBeLessThan(unbuffedCastDuration);
|
|
});
|
|
|
|
it("awards one caster-centered stack when a generated Druid/Mage companion spends Nature Power", () => {
|
|
if (!earthChain) throw new Error("Missing generated Druid/Mage Earth Chain row");
|
|
const seed = "inspiration-companion-spend";
|
|
const caster = createConfiguredPartyMember({
|
|
name: "Druid/Mage",
|
|
classId: "rom-druid",
|
|
secondaryClassId: "rom-mage",
|
|
specialization: "Primary",
|
|
role: "damage",
|
|
}, 45, 0, seed);
|
|
const outside = priest("Outside Range", seed, 1);
|
|
usePartyStore.setState({
|
|
members: [{
|
|
...caster,
|
|
resourcePools: { ...caster.resourcePools, "nature-power": 1 },
|
|
activeCast: {
|
|
abilityId: EARTH_CHAIN_ID,
|
|
school: "nature",
|
|
mode: "cast",
|
|
startedAt: 0,
|
|
completesAt: 1_000,
|
|
originalDurationMs: 1_000,
|
|
pushbackHits: 0,
|
|
origin: [0, 0, 0],
|
|
targetId: "inspiration-companion-target",
|
|
},
|
|
activeCastTargetId: "inspiration-companion-target",
|
|
status: "attacking",
|
|
}, outside],
|
|
command: "attack",
|
|
activeMobId: "inspiration-companion-target",
|
|
});
|
|
registerPartyRuntimePosition(caster.id, [0, 0, 0]);
|
|
registerPartyRuntimePosition(outside.id, [5.01, 0, 0]);
|
|
useCombatStore.getState().synchronizeActors();
|
|
useCombatStore.getState().registerMob("inspiration-companion-target", {
|
|
name: "Target", level: 1, maxHealth: 100_000, xpReward: 0,
|
|
});
|
|
setMobPosition("inspiration-companion-target", [2, 0, 0]);
|
|
useCombatStore.getState().damageMob("inspiration-companion-target", 1, 500);
|
|
|
|
advanceDungeonPartyCombat(1_000);
|
|
|
|
expect(usePartyStore.getState().members[0]?.resourcePools["nature-power"]).toBe(0);
|
|
expect(inspirationAura(caster.id)?.stacks).toBe(1);
|
|
expect(inspirationAura(PLAYER_AGGRO_ID)?.stacks).toBe(1);
|
|
expect(inspirationAura(outside.id)).toBeUndefined();
|
|
});
|
|
|
|
it("applies Inspiration spell haste only once to a Mage receiving the party buff", () => {
|
|
useCombatStore.getState().initializeCharacter({
|
|
classId: "rom-mage",
|
|
secondaryClassId: "rom-druid",
|
|
raceId: "rom-elf",
|
|
level: 45,
|
|
role: "damage",
|
|
});
|
|
useCombatStore.getState().setPlayerPosition([0, 0, 0]);
|
|
|
|
const targetId = "inspiration-mage-target";
|
|
useCombatStore.getState().registerMob(targetId, { name: "Target", level: 1, maxHealth: 100_000, xpReward: 0 });
|
|
setMobPosition(targetId, [2, 0, 0]);
|
|
useCombatStore.getState().selectMob(targetId);
|
|
const ability = useCombatStore.getState().abilities.find((candidate) => candidate.id === "rom-mage-primary-1");
|
|
expect(ability?.castTimeMs).toBe(3_000);
|
|
|
|
expect(useCombatStore.getState().castAbility("rom-mage-primary-1", {
|
|
origin: [0, 0, 0], targetActorId: targetId, now: 1_000,
|
|
}).ok).toBe(true);
|
|
const unbuffedDuration = useCombatStore.getState().activeCast?.originalDurationMs ?? 0;
|
|
expect(unbuffedDuration).toBeGreaterThan(0);
|
|
useCombatStore.getState().tick(3, 4_000);
|
|
useCombatStore.setState((state) => ({
|
|
auras: [...romDruidMageInspirationApplySpendStack(
|
|
state.auras, "druid-mage-source", [PLAYER_AGGRO_ID], 5_000,
|
|
)],
|
|
}));
|
|
expect(inspirationAura(PLAYER_AGGRO_ID)?.stacks).toBe(1);
|
|
expect(romDruidMageInspirationHastePercent(
|
|
useCombatStore.getState().auras, PLAYER_AGGRO_ID, 5_000,
|
|
)).toBe(2);
|
|
|
|
expect(useCombatStore.getState().castAbility("rom-mage-primary-1", {
|
|
origin: [0, 0, 0], targetActorId: targetId, now: 5_000,
|
|
}).ok).toBe(true);
|
|
expect(useCombatStore.getState().activeCast?.startedAt).toBe(5_000);
|
|
const buffedDuration = useCombatStore.getState().activeCast?.originalDurationMs ?? 0;
|
|
const durationReduction = 1 - buffedDuration / unbuffedDuration;
|
|
expect(durationReduction).toBeGreaterThan(0.01);
|
|
expect(durationReduction).toBeLessThan(0.03);
|
|
});
|
|
});
|