Files
healer-man/src/game/romMageWarriorRuntime.test.ts
T
2026-09-30 09:57:13 -04:00

190 lines
8.5 KiB
TypeScript

import { beforeEach, describe, expect, it } from "vitest";
import { abilitiesForCharacter, abilityById } from "./abilityCatalog";
import { PLAYER_AGGRO_ID } from "./aggro";
import { useCombatStore } from "./combatStore";
import { clearMobRuntimeRegistry, setMobPosition } from "./mobRuntimeRegistry";
import {
advanceDungeonPartyCombat,
partyCastCanMoveDuringMeditationPath,
resetPartyRuntimeClock,
} from "./partyRuntime";
import {
clearPartyRuntimeRegistry,
registerPartyRuntimePosition,
updatePartyRuntimePosition,
updatePartyRuntimeTargetVisibility,
} from "./partyRuntimeRegistry";
import { createConfiguredPartyMember, usePartyStore } from "./partyStore";
import {
ROM_MAGE_WARRIOR_MEDITATION_PATH_ACTIVE_TAG,
ROM_MAGE_WARRIOR_MEDITATION_PATH_LOCKOUT_TAG,
} from "./romMageWarriorRuntime";
const TARGET_ID = "meditation-path-target";
function initializeMageWarrior(): void {
useCombatStore.getState().initializeCharacter({
classId: "rom-mage",
secondaryClassId: "rom-warrior",
raceId: "rom-human",
level: 80,
});
useCombatStore.setState((state) => ({
resource: 1_000,
maxResource: 1_000,
resourcePools: { ...state.resourcePools, mana: 1_000 },
maxResourcePools: { ...state.maxResourcePools, mana: 1_000 },
}));
useCombatStore.getState().setPlayerPosition([0, 0, 0]);
}
function registerTarget(): void {
useCombatStore.getState().registerMob(TARGET_ID, { name: TARGET_ID, maxHealth: 1_000_000, xpReward: 0 });
setMobPosition(TARGET_ID, [3, 0, 0]);
useCombatStore.getState().selectMob(TARGET_ID);
}
function auraWithTag(tag: string, now: number) {
return useCombatStore.getState().auras.find((aura) => aura.targetId === PLAYER_AGGRO_ID
&& (aura.expiresAt === null || aura.expiresAt > now)
&& aura.definition.utilityTags?.includes(tag));
}
describe("RoM Mage/Warrior Meditation Path", () => {
beforeEach(() => {
clearMobRuntimeRegistry();
clearPartyRuntimeRegistry();
resetPartyRuntimeClock();
initializeMageWarrior();
});
it.each([
["rom-mage-general-0", 490204, "Fireball"],
["rom-mage-general-1", 490212, "Lightning"],
["rom-mage-primary-4", 490242, "Discharge"],
["rom-mage-primary-6", 490239, "Phoenix"],
] as const)("grants 7 seconds of mobile casting after %s (%i, %s)", (abilityId, spellId, _name) => {
registerTarget();
const state = useCombatStore.getState();
expect(state.abilities.some((ability) => ability.dbcSpellId === 499588 && ability.passive)).toBe(true);
expect(abilityById(abilityId)?.dbcSpellId).toBe(spellId);
expect(state.castAbility(abilityId, undefined, 1_000)).toMatchObject({ ok: true });
expect(auraWithTag(ROM_MAGE_WARRIOR_MEDITATION_PATH_ACTIVE_TAG, 1_000)?.expiresAt).toBe(8_000);
expect(auraWithTag(ROM_MAGE_WARRIOR_MEDITATION_PATH_LOCKOUT_TAG, 1_000)?.expiresAt).toBe(16_000);
});
it("does not proc on Flame and does not retrigger during the 15-second lockout", () => {
registerTarget();
const flame = abilityById("rom-mage-primary-1");
const fireball = abilityById("rom-mage-general-0");
if (!flame || !fireball) throw new Error("Missing local Mage ability rows");
expect(useCombatStore.getState().castAbility(flame.id, undefined, 1_000)).toMatchObject({ ok: true });
useCombatStore.getState().tick(0.1, 4_100);
expect(auraWithTag(ROM_MAGE_WARRIOR_MEDITATION_PATH_ACTIVE_TAG, 4_100)).toBeUndefined();
expect(auraWithTag(ROM_MAGE_WARRIOR_MEDITATION_PATH_LOCKOUT_TAG, 4_100)).toBeUndefined();
expect(useCombatStore.getState().castAbility(fireball.id, undefined, 6_000)).toMatchObject({ ok: true });
expect(auraWithTag(ROM_MAGE_WARRIOR_MEDITATION_PATH_ACTIVE_TAG, 6_000)?.expiresAt).toBe(13_000);
expect(useCombatStore.getState().castAbility(fireball.id, undefined, 14_000)).toMatchObject({ ok: true });
expect(auraWithTag(ROM_MAGE_WARRIOR_MEDITATION_PATH_ACTIVE_TAG, 14_000)).toBeUndefined();
expect(auraWithTag(ROM_MAGE_WARRIOR_MEDITATION_PATH_LOCKOUT_TAG, 14_000)?.expiresAt).toBe(21_000);
expect(useCombatStore.getState().castAbility(fireball.id, undefined, 21_000)).toMatchObject({ ok: true });
expect(auraWithTag(ROM_MAGE_WARRIOR_MEDITATION_PATH_ACTIVE_TAG, 21_000)?.expiresAt).toBe(28_000);
});
it("lets a cast started during Meditation Path continue after movement", () => {
registerTarget();
const fireball = abilityById("rom-mage-general-0");
const flame = abilityById("rom-mage-primary-1");
if (!fireball || !flame) throw new Error("Missing local Mage ability rows");
expect(useCombatStore.getState().castAbility(fireball.id, undefined, 1_000)).toMatchObject({ ok: true });
expect(auraWithTag(ROM_MAGE_WARRIOR_MEDITATION_PATH_ACTIVE_TAG, 1_000)).toBeDefined();
expect(useCombatStore.getState().castAbility(flame.id, undefined, 2_600)).toMatchObject({ ok: true });
expect(useCombatStore.getState().activeCast).toMatchObject({ abilityId: flame.id, canCastWhileMoving: true });
useCombatStore.getState().setPlayerPosition([1, 0, 0]);
expect(useCombatStore.getState().activeCast).toMatchObject({ abilityId: flame.id, canCastWhileMoving: true });
useCombatStore.getState().tick(0.1, 5_700);
expect(useCombatStore.getState().activeCast).toBeNull();
expect(useCombatStore.getState().combatEvents.some((event) => event.kind === "cast"
&& event.abilityId === flame.id && event.outcome === "completed")).toBe(true);
expect(auraWithTag(ROM_MAGE_WARRIOR_MEDITATION_PATH_ACTIVE_TAG, 5_700)?.expiresAt).toBe(8_000);
});
it("grants the companion aura after a supported cast and preserves movement casting", () => {
const seed = "meditation-path-party";
const partyMember = createConfiguredPartyMember({
name: "Meditation Mage",
classId: "rom-mage",
secondaryClassId: "rom-warrior",
specialization: "Primary",
role: "damage",
}, 80, 0, seed);
const fireball = abilityById("rom-mage-general-0");
const flame = abilityById("rom-mage-primary-1");
if (!fireball || !flame) throw new Error("Missing local Mage ability rows");
usePartyStore.getState().initializeParty(seed, 80, useCombatStore.getState().health, "damage", 2, "rom");
usePartyStore.getState().setCommand("attack");
usePartyStore.setState({ members: [{
...partyMember,
resource: 10_000,
maxResource: 10_000,
resourcePools: { ...partyMember.resourcePools, mana: 10_000 },
activeCast: {
abilityId: fireball.id,
school: "fire",
mode: "cast",
startedAt: 500,
completesAt: 1_000,
originalDurationMs: 500,
pushbackHits: 0,
origin: [0, 0, 0],
targetId: TARGET_ID,
},
activeCastTargetId: TARGET_ID,
status: "attacking",
}] });
registerPartyRuntimePosition(partyMember.id, [0, 0, 0]);
registerTarget();
useCombatStore.getState().damageMob(TARGET_ID, 1, 900);
updatePartyRuntimeTargetVisibility(partyMember.id, TARGET_ID, true);
useCombatStore.getState().synchronizeActors();
advanceDungeonPartyCombat(1_000);
const partyAuras = useCombatStore.getState().auras;
expect(partyAuras.find((aura) => aura.targetId === partyMember.id
&& aura.definition.utilityTags?.includes(ROM_MAGE_WARRIOR_MEDITATION_PATH_ACTIVE_TAG))?.expiresAt).toBe(8_000);
expect(partyAuras.find((aura) => aura.targetId === partyMember.id
&& aura.definition.utilityTags?.includes(ROM_MAGE_WARRIOR_MEDITATION_PATH_LOCKOUT_TAG))?.expiresAt).toBe(16_000);
const canMoveDuringCast = partyCastCanMoveDuringMeditationPath(partyMember, flame, 7_000);
expect(canMoveDuringCast).toBe(true);
usePartyStore.setState({ members: usePartyStore.getState().members.map((member) => member.id === partyMember.id
? { ...member, activeCast: {
abilityId: flame.id,
school: "fire",
mode: "cast",
startedAt: 7_000,
completesAt: 10_000,
originalDurationMs: 3_000,
pushbackHits: 0,
canCastWhileMoving: canMoveDuringCast,
origin: [0, 0, 0],
targetId: TARGET_ID,
}, activeCastTargetId: TARGET_ID, status: "attacking" }
: member) });
useCombatStore.getState().synchronizeActors();
advanceDungeonPartyCombat(7_500);
updatePartyRuntimePosition(partyMember.id, 1, 0, 0);
advanceDungeonPartyCombat(10_000);
expect(usePartyStore.getState().members.find((member) => member.id === partyMember.id)?.activeCast).toBeNull();
expect(useCombatStore.getState().combatEvents.some((event) => event.sourceActorId === partyMember.id
&& event.abilityId === flame.id && event.outcome === "hit")).toBe(true);
});
});