195 lines
8.3 KiB
TypeScript
195 lines
8.3 KiB
TypeScript
import { beforeEach, describe, expect, it } from "vitest";
|
|
import { PLAYER_AGGRO_ID } from "./aggro";
|
|
import { clearPartyRuntimeRegistry, registerPartyRuntimePosition } from "./partyRuntimeRegistry";
|
|
import { advanceDungeonPartyCombat, resetPartyRuntimeClock } from "./partyRuntime";
|
|
import { createConfiguredPartyMember, usePartyStore, type PartyMember } from "./partyStore";
|
|
import { useCombatStore } from "./combatStore";
|
|
import { useDungeonSimulationStore } from "./dungeonSimulationStore";
|
|
import { ROM_ALL_ABILITIES } from "./romAbilityCatalog";
|
|
import { ROM_DRUID_ROGUE_SINISTER_REQUEST_RESOURCES } from "./romDruidRogueCurrentCoa";
|
|
import { abilityAtLevel, type ResourceType } from "./abilityCatalog";
|
|
|
|
const RESOURCE_TYPES = ROM_DRUID_ROGUE_SINISTER_REQUEST_RESOURCES as readonly ResourceType[];
|
|
const STARTING_VALUES: Readonly<Record<ResourceType, number>> = {
|
|
mana: 20,
|
|
rage: 10,
|
|
focus: 10,
|
|
energy: 10,
|
|
psi: 1,
|
|
"nature-power": 1,
|
|
health: 0,
|
|
"runic-power": 0,
|
|
"holy-power": 0,
|
|
chi: 0,
|
|
"soul-power": 0,
|
|
};
|
|
|
|
function memberWithPools(seed: string, resources = RESOURCE_TYPES, index = 0): PartyMember {
|
|
const member = createConfiguredPartyMember({
|
|
name: "Druid/Rogue",
|
|
classId: "rom-druid",
|
|
secondaryClassId: "rom-rogue",
|
|
raceId: "rom-elf",
|
|
specialization: "Druid/Rogue",
|
|
specializationId: "rom-druid:rom-rogue",
|
|
talentTreeId: "rom-druid-rom-rogue",
|
|
talentRanks: {},
|
|
role: "damage",
|
|
}, 80, index, seed);
|
|
const resourcePools = Object.fromEntries(
|
|
resources.map((resource) => [resource, STARTING_VALUES[resource]]),
|
|
) as Partial<Record<ResourceType, number>>;
|
|
return {
|
|
...member,
|
|
resource: resourcePools[member.resourceType] ?? member.resource,
|
|
resourcePools,
|
|
};
|
|
}
|
|
|
|
function sinisterRequestId(): string {
|
|
const request = ROM_ALL_ABILITIES.find((ability) => ability.dbcSpellId === 494023
|
|
&& ability.classId === "rom-druid" && ability.secondaryClassId === "rom-rogue");
|
|
if (!request) throw new Error("Druid/Rogue Sinister Request catalog row is missing.");
|
|
return request.id;
|
|
}
|
|
|
|
function assertPoolsAreFull(actorId: string, expectedResources: readonly ResourceType[]): void {
|
|
const resources = useCombatStore.getState().actors[actorId]?.resources;
|
|
expect(resources).toBeDefined();
|
|
for (const resource of expectedResources) {
|
|
expect(resources![resource]?.current).toBe(resources![resource]?.maximum);
|
|
}
|
|
}
|
|
|
|
describe("current CoA Druid/Rogue Sinister Request runtime", () => {
|
|
beforeEach(() => {
|
|
clearPartyRuntimeRegistry();
|
|
resetPartyRuntimeClock();
|
|
useDungeonSimulationStore.setState({ sessionMode: "play" });
|
|
useCombatStore.getState().initializeCharacter({
|
|
classId: "rom-druid",
|
|
secondaryClassId: "rom-rogue",
|
|
level: 80,
|
|
xp: 0,
|
|
});
|
|
useCombatStore.getState().setPlayerPosition([0, 0, 0]);
|
|
usePartyStore.getState().initializeParty("sinister-request-test", 80,
|
|
useCombatStore.getState().health, "damage", 3, "rom");
|
|
});
|
|
|
|
it("fills the player's and nearby companions' existing pools, includes radius edge, and leaves outside/absent pools alone", () => {
|
|
const nearby = memberWithPools("sinister-player-nearby", RESOURCE_TYPES, 0);
|
|
const edge = memberWithPools("sinister-player-edge", ["mana", "energy"], 1);
|
|
const outside = memberWithPools("sinister-player-outside", RESOURCE_TYPES, 2);
|
|
usePartyStore.setState({ members: [nearby, edge, outside] });
|
|
registerPartyRuntimePosition(nearby.id, [5, 0, 0]);
|
|
registerPartyRuntimePosition(edge.id, [15, 0, 0]);
|
|
registerPartyRuntimePosition(outside.id, [15.01, 0, 0]);
|
|
|
|
useCombatStore.setState((state) => {
|
|
const maxResourcePools = {
|
|
...state.maxResourcePools,
|
|
mana: 240,
|
|
rage: 100,
|
|
focus: 100,
|
|
energy: 100,
|
|
psi: 6,
|
|
"nature-power": 15,
|
|
};
|
|
const resourcePools = {
|
|
...state.resourcePools,
|
|
mana: 1,
|
|
rage: 1,
|
|
focus: 1,
|
|
energy: 1,
|
|
psi: 1,
|
|
"nature-power": 1,
|
|
};
|
|
return {
|
|
maxResourcePools,
|
|
resourcePools,
|
|
maxResource: 240,
|
|
resource: 1,
|
|
};
|
|
});
|
|
useCombatStore.getState().synchronizeActors();
|
|
|
|
const beforeOutside = { ...useCombatStore.getState().actors[outside.id].resources };
|
|
const abilityId = sinisterRequestId();
|
|
const catalogRow = ROM_ALL_ABILITIES.find((ability) => ability.id === abilityId)!;
|
|
expect(abilityAtLevel(catalogRow, 80).effects).toHaveLength(RESOURCE_TYPES.length);
|
|
expect(useCombatStore.getState()).toMatchObject({ classId: "rom-druid", secondaryClassId: "rom-rogue" });
|
|
const castResult = useCombatStore.getState().castAbility(abilityId, { targetActorId: null, now: 1_000 });
|
|
expect(castResult).toMatchObject({ ok: true });
|
|
|
|
assertPoolsAreFull(PLAYER_AGGRO_ID, RESOURCE_TYPES);
|
|
assertPoolsAreFull(nearby.id, RESOURCE_TYPES);
|
|
assertPoolsAreFull(edge.id, ["mana", "energy"]);
|
|
expect(useCombatStore.getState().actors[edge.id].resources.focus).toBeUndefined();
|
|
for (const resource of RESOURCE_TYPES) {
|
|
expect(useCombatStore.getState().actors[outside.id].resources[resource]).toEqual(beforeOutside[resource]);
|
|
}
|
|
|
|
const events = useCombatStore.getState().combatEvents.filter((event) => (
|
|
event.kind === "resource" && event.abilityId === abilityId
|
|
));
|
|
expect(events.filter((event) => event.targetActorId === PLAYER_AGGRO_ID)).toHaveLength(RESOURCE_TYPES.length);
|
|
expect(events.filter((event) => event.targetActorId === nearby.id)).toHaveLength(RESOURCE_TYPES.length);
|
|
expect(events.filter((event) => event.targetActorId === edge.id)).toHaveLength(2);
|
|
expect(events.some((event) => event.targetActorId === outside.id)).toBe(false);
|
|
expect(events.every((event) => event.sourceActorId === PLAYER_AGGRO_ID)).toBe(true);
|
|
});
|
|
|
|
it("applies a companion cast to the in-range party, caps each existing pool, and attributes resource events to the caster", () => {
|
|
const abilityId = sinisterRequestId();
|
|
const casterBase = memberWithPools("sinister-companion-caster", RESOURCE_TYPES, 0);
|
|
const caster = {
|
|
...casterBase,
|
|
activeCast: {
|
|
abilityId,
|
|
school: "nature" as const,
|
|
mode: "cast" as const,
|
|
startedAt: 0,
|
|
completesAt: 0,
|
|
originalDurationMs: 0,
|
|
pushbackHits: 0,
|
|
origin: [0, 0, 0] as const,
|
|
targetId: casterBase.id,
|
|
},
|
|
activeCastTargetId: casterBase.id,
|
|
status: "healing" as const,
|
|
};
|
|
const nearby = memberWithPools("sinister-companion-nearby", RESOURCE_TYPES.filter((resource) => resource !== "psi"), 1);
|
|
const edge = memberWithPools("sinister-companion-edge", ["mana", "rage"], 2);
|
|
const outside = memberWithPools("sinister-companion-outside", RESOURCE_TYPES, 3);
|
|
usePartyStore.setState({ members: [caster, nearby, edge, outside] });
|
|
registerPartyRuntimePosition(caster.id, [0, 0, 0]);
|
|
registerPartyRuntimePosition(nearby.id, [5, 0, 0]);
|
|
registerPartyRuntimePosition(edge.id, [15, 0, 0]);
|
|
registerPartyRuntimePosition(outside.id, [15.01, 0, 0]);
|
|
useCombatStore.getState().synchronizeActors();
|
|
const beforeOutside = { ...useCombatStore.getState().actors[outside.id].resources };
|
|
|
|
advanceDungeonPartyCombat(1_000);
|
|
|
|
assertPoolsAreFull(PLAYER_AGGRO_ID, ["mana"]);
|
|
assertPoolsAreFull(caster.id, RESOURCE_TYPES);
|
|
assertPoolsAreFull(nearby.id, RESOURCE_TYPES.filter((resource) => resource !== "psi"));
|
|
expect(useCombatStore.getState().actors[nearby.id].resources.psi).toBeUndefined();
|
|
assertPoolsAreFull(edge.id, ["mana", "rage"]);
|
|
for (const resource of RESOURCE_TYPES) {
|
|
expect(useCombatStore.getState().actors[outside.id].resources[resource]).toEqual(beforeOutside[resource]);
|
|
}
|
|
|
|
const events = useCombatStore.getState().combatEvents.filter((event) => (
|
|
event.kind === "resource" && event.abilityId === abilityId
|
|
));
|
|
expect(events.filter((event) => event.targetActorId === PLAYER_AGGRO_ID)).toHaveLength(1);
|
|
expect(events.filter((event) => event.targetActorId === caster.id)).toHaveLength(RESOURCE_TYPES.length);
|
|
expect(events.filter((event) => event.targetActorId === nearby.id)).toHaveLength(RESOURCE_TYPES.length - 1);
|
|
expect(events.filter((event) => event.targetActorId === edge.id)).toHaveLength(2);
|
|
expect(events.some((event) => event.targetActorId === outside.id)).toBe(false);
|
|
expect(events.every((event) => event.sourceActorId === caster.id)).toBe(true);
|
|
});
|
|
});
|