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

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