Files
healer-man/src/game/romMageMechanics.test.ts
T
2026-09-28 22:46:45 -04:00

366 lines
17 KiB
TypeScript

import { beforeEach, describe, expect, it } from "vitest";
import { abilityById, resourceProfileForClass } from "./abilityCatalog";
import { PLAYER_AGGRO_ID } from "./aggro";
import { applyAura, resolveResourceValue } from "./combatAuras";
import { useCombatStore } from "./combatStore";
import { clearMobRuntimeRegistry, setMobPosition } from "./mobRuntimeRegistry";
import {
ROM_MAGE_WARLOCK_PASSIVE_TAGS,
romMageWarlockCastEffects,
} from "./romMageWarlockOverrides";
import { applyTalentModifierValue } from "./talentModifiers";
function initializeMage(secondaryClassId?: "rom-druid" | "rom-warden" | "rom-champion", level = 80): void {
useCombatStore.getState().initializeCharacter({
classId: "rom-mage",
secondaryClassId,
raceId: "rom-human",
level,
});
}
function addTarget(id: string, x: number, z = 0): void {
useCombatStore.getState().registerMob(id, { name: id, maxHealth: 1_000_000 });
setMobPosition(id, [x, 0, z]);
}
describe("RoM Mage mechanics", () => {
beforeEach(() => {
clearMobRuntimeRegistry();
useCombatStore.setState({ playerPosition: [0, 0, 0] });
});
it("maps Elemental Catalysis mana percentage regeneration against maximum mana", () => {
const catalysis = abilityById("rom-mage-primary-0")!;
expect(catalysis.effects).toContainEqual(expect.objectContaining({
kind: "apply-aura",
aura: expect.objectContaining({
durationMs: 20_000,
modifiers: expect.arrayContaining([
expect.objectContaining({
kind: "stat",
stat: "mana-regeneration-max-percent-per-second",
operation: "flat",
value: 0.01,
}),
]),
}),
}));
initializeMage(undefined, 60);
addTarget("catalysis-combat", 3);
useCombatStore.setState((state) => ({
...state,
resource: state.maxResource / 2,
resourcePools: { ...state.resourcePools, mana: state.maxResourcePools.mana! / 2 },
lastResourceSpentAt: { mana: 1_000 },
mobs: { ...state.mobs, "catalysis-combat": { ...state.mobs["catalysis-combat"]!, engaged: true } },
}));
const before = useCombatStore.getState().resourcePools.mana!;
expect(useCombatStore.getState().castAbility(catalysis.id, undefined, 1_000)).toMatchObject({ ok: true });
expect(useCombatStore.getState().auras.some((aura) => aura.targetId === PLAYER_AGGRO_ID
&& aura.definition.modifiers?.some((modifier) => modifier.kind === "stat"
&& modifier.stat === "mana-regeneration-max-percent-per-second" && modifier.value === 0.01))).toBe(true);
useCombatStore.getState().tick(1, 2_000);
useCombatStore.getState().tick(1, 3_000);
const restored = useCombatStore.getState().resourcePools.mana! - before;
expect(restored).toBeGreaterThanOrEqual(useCombatStore.getState().maxResourcePools.mana! * 0.02);
});
it("uses rank-two Catalysis's explicit 20-second damage and spell-speed payload without adding a mana tick", () => {
initializeMage(undefined, 65);
const catalysis = abilityById("rom-mage-primary-0")!;
const rankTwo = catalysis.ranks?.find((rank) => rank.spellId === 497971);
expect(rankTwo).toBeDefined();
expect(rankTwo?.effects).toEqual(expect.arrayContaining([
expect.objectContaining({
kind: "apply-aura",
aura: expect.objectContaining({
durationMs: 20_000,
modifiers: [expect.objectContaining({ kind: "damage", direction: "dealt", attackKind: "spell", value: 0.2 })],
}),
}),
expect.objectContaining({
kind: "apply-aura",
aura: expect.objectContaining({
durationMs: 20_000,
modifiers: [expect.objectContaining({ kind: "stat", stat: "spell-haste", value: 50 })],
}),
}),
]));
expect(rankTwo?.effects.some((effect) => effect.kind === "apply-aura"
&& effect.aura.modifiers?.some((modifier) => modifier.kind === "stat"
&& modifier.stat === "mana-regeneration-max-percent-per-second"))).toBe(false);
addTarget("catalysis-rank-two-combat", 3);
useCombatStore.setState((state) => ({
...state,
lastResourceSpentAt: { mana: 1_000 },
mobs: { ...state.mobs, "catalysis-rank-two-combat": { ...state.mobs["catalysis-rank-two-combat"]!, engaged: true } },
}));
const result = useCombatStore.getState().castAbility(catalysis.id, undefined, 1_000);
expect(result, JSON.stringify(result)).toMatchObject({ ok: true });
expect(useCombatStore.getState().auras.some((aura) => aura.targetId === PLAYER_AGGRO_ID
&& aura.definition.id.includes("497971")
&& aura.definition.modifiers?.some((modifier) => modifier.kind === "damage"
&& modifier.attackKind === "spell" && modifier.value === 0.2))).toBe(true);
});
it("applies rank-two Catalysis spell haste to Mage cast time while rank one stays unchanged", () => {
const flame = abilityById("rom-mage-primary-1")!;
expect(flame).toMatchObject({ name: "Flame", castTimeMs: 3_000 });
initializeMage(undefined, 65);
addTarget("catalysis-haste-rank-two-baseline", 3);
useCombatStore.getState().selectMob("catalysis-haste-rank-two-baseline");
expect(useCombatStore.getState().castAbility(flame.id, undefined, 1_000)).toMatchObject({ ok: true });
const rankTwoBaselineMs = useCombatStore.getState().activeCast!.originalDurationMs;
initializeMage(undefined, 65);
clearMobRuntimeRegistry();
addTarget("catalysis-haste-rank-two-target", 3);
useCombatStore.getState().selectMob("catalysis-haste-rank-two-target");
expect(useCombatStore.getState().castAbility("rom-mage-primary-0", undefined, 3_000)).toMatchObject({ ok: true });
expect(useCombatStore.getState().auras.some((aura) => aura.targetId === PLAYER_AGGRO_ID
&& aura.definition.id.includes("497971")
&& aura.definition.modifiers?.some((modifier) => modifier.kind === "stat"
&& modifier.stat === "spell-haste" && modifier.value === 50))).toBe(true);
expect(useCombatStore.getState().castAbility(flame.id, undefined, 5_000)).toMatchObject({ ok: true });
expect(useCombatStore.getState().activeCast).toMatchObject({
abilityId: flame.id,
startedAt: 5_000,
originalDurationMs: Math.round(rankTwoBaselineMs / 1.5),
completesAt: 5_000 + Math.round(rankTwoBaselineMs / 1.5),
});
initializeMage(undefined, 60);
clearMobRuntimeRegistry();
addTarget("catalysis-haste-rank-one-baseline", 3);
useCombatStore.getState().selectMob("catalysis-haste-rank-one-baseline");
expect(useCombatStore.getState().castAbility(flame.id, undefined, 7_000)).toMatchObject({ ok: true });
const rankOneBaselineMs = useCombatStore.getState().activeCast!.originalDurationMs;
initializeMage(undefined, 60);
clearMobRuntimeRegistry();
addTarget("catalysis-haste-rank-one-target", 3);
useCombatStore.getState().selectMob("catalysis-haste-rank-one-target");
expect(useCombatStore.getState().castAbility("rom-mage-primary-0", undefined, 9_000)).toMatchObject({ ok: true });
expect(useCombatStore.getState().auras.some((aura) => aura.targetId === PLAYER_AGGRO_ID
&& aura.definition.modifiers?.some((modifier) => modifier.kind === "stat"
&& modifier.stat === "spell-haste"))).toBe(false);
expect(useCombatStore.getState().castAbility(flame.id, undefined, 11_000)).toMatchObject({ ok: true });
expect(useCombatStore.getState().activeCast).toMatchObject({
abilityId: flame.id,
startedAt: 11_000,
originalDurationMs: rankOneBaselineMs,
completesAt: 11_000 + rankOneBaselineMs,
});
});
it("does not apply a RoM spell-haste aura to another class's cast time", () => {
useCombatStore.getState().initializeCharacter({ classId: "priest", raceId: "human", level: 80 });
addTarget("non-mage-haste-baseline", 3);
useCombatStore.getState().selectMob("non-mage-haste-baseline");
expect(useCombatStore.getState().castAbility("priest-smite", undefined, 1_000)).toMatchObject({ ok: true });
const priestBaselineMs = useCombatStore.getState().activeCast!.originalDurationMs;
useCombatStore.getState().initializeCharacter({ classId: "priest", raceId: "human", level: 80 });
clearMobRuntimeRegistry();
addTarget("non-mage-haste-target", 3);
useCombatStore.getState().selectMob("non-mage-haste-target");
useCombatStore.setState((state) => ({
auras: applyAura(state.auras, {
definition: {
id: "rom-catalysis-non-mage-test",
name: "Test Catalysis haste",
disposition: "buff",
durationMs: 20_000,
modifiers: [{ kind: "stat", stat: "spell-haste", operation: "flat", value: 50 }],
},
sourceId: PLAYER_AGGRO_ID,
targetId: PLAYER_AGGRO_ID,
now: 3_000,
}).auras,
}));
expect(useCombatStore.getState().castAbility("priest-smite", undefined, 3_000)).toMatchObject({ ok: true });
expect(useCombatStore.getState().activeCast).toMatchObject({
abilityId: "priest-smite",
originalDurationMs: priestBaselineMs,
completesAt: 3_000 + priestBaselineMs,
});
});
it("leaves ordinary mana regeneration unchanged without the Catalysis aura", () => {
useCombatStore.getState().initializeCharacter({ classId: "priest", raceId: "human", level: 80 });
addTarget("ordinary-mana-combat", 3);
useCombatStore.setState((state) => ({
...state,
resource: state.maxResource / 2,
resourcePools: { ...state.resourcePools, mana: state.maxResourcePools.mana! / 2 },
lastResourceSpentAt: { mana: 1_000 },
mobs: { ...state.mobs, "ordinary-mana-combat": { ...state.mobs["ordinary-mana-combat"]!, engaged: true } },
}));
const beforeState = useCombatStore.getState();
const before = beforeState.resourcePools.mana!;
const playerAuras = beforeState.auras.filter((aura) => aura.targetId === PLAYER_AGGRO_ID);
const baseRegeneration = applyTalentModifierValue(
resourceProfileForClass("priest").regenerationPerSecond
+ beforeState.gearStats.mp5 / 5
+ beforeState.gearStats.spirit * 0.02,
beforeState.talentModifiers,
"resource-regeneration-percent",
);
const expectedPerSecond = resolveResourceValue(
resolveResourceValue(baseRegeneration, playerAuras, "mana", "regeneration"),
playerAuras,
"power",
"regeneration",
);
expect(beforeState.auras.some((aura) => aura.definition.modifiers?.some((modifier) => (
modifier.kind === "stat" && modifier.stat === "mana-regeneration-max-percent-per-second"
)))).toBe(false);
useCombatStore.getState().tick(1, 2_000);
useCombatStore.getState().tick(1, 3_000);
expect(useCombatStore.getState().resourcePools.mana! - before).toBeCloseTo(expectedPerSecond * 2);
});
it("casts Phoenix down a capped line and leaves other line spells' extension intact", () => {
initializeMage();
for (const [id, x, z] of [
["phoenix-target", 4, 0],
["phoenix-path", 8, 0],
["phoenix-off-line", 7, 2],
["phoenix-beyond", 12.1, 0],
] as const) addTarget(id, x, z);
useCombatStore.getState().selectMob("phoenix-target");
expect(abilityById("rom-mage-primary-6")).toMatchObject({
target: "hostile",
range: { max: 12 },
targeting: { shape: "line", radius: 12 },
});
expect(useCombatStore.getState().castAbility("rom-mage-primary-6", undefined, 1_000))
.toMatchObject({ ok: true, affectedMobIds: ["phoenix-target", "phoenix-path"] });
useCombatStore.getState().initializeCharacter({ classId: "rom-scout", raceId: "rom-elf", level: 60 });
clearMobRuntimeRegistry();
for (const [id, x] of [["arrow-target", 3], ["arrow-path", 7], ["arrow-beyond", 7.1]] as const) {
addTarget(id, x);
}
useCombatStore.getState().selectMob("arrow-target");
expect(useCombatStore.getState().castAbility("rom-scout-primary-5", undefined, 4_000))
.toMatchObject({ ok: true, affectedMobIds: ["arrow-target", "arrow-path"] });
});
it("doubles only Fireball and Flame on the 10% Eruption proc and runs Control Flame once", () => {
const fireball = abilityById("rom-mage-general-0")!;
const tags = new Set([
ROM_MAGE_WARLOCK_PASSIVE_TAGS.eruption,
ROM_MAGE_WARLOCK_PASSIVE_TAGS.controlFlame,
]);
const procced = romMageWarlockCastEffects(fireball, fireball.effects, tags, 0.099);
const ordinary = romMageWarlockCastEffects(fireball, fireball.effects, tags, 0.1);
expect(procced.filter((effect) => effect.kind === "damage")).toEqual(
fireball.effects.filter((effect) => effect.kind === "damage").map((effect) => effect.kind === "damage"
? expect.objectContaining({
coefficient: effect.coefficient * 2,
basePoints: (effect.basePoints ?? 0) * 2,
pointsPerLevel: (effect.pointsPerLevel ?? 0) * 2,
})
: effect),
);
expect(ordinary.filter((effect) => effect.kind === "damage")).toEqual(
fireball.effects.filter((effect) => effect.kind === "damage"),
);
expect(procced).toEqual(expect.arrayContaining([
expect.objectContaining({
kind: "apply-aura",
aura: expect.objectContaining({
id: "rom-mage-druid-control-flame-buff",
durationMs: 12_000,
modifiers: [expect.objectContaining({ stat: "magical-attack", value: 0.08 })],
}),
}),
expect.objectContaining({
kind: "apply-aura",
aura: expect.objectContaining({
id: "rom-mage-druid-control-flame-cooldown",
durationMs: 20_000,
}),
}),
]));
expect(romMageWarlockCastEffects(fireball, fireball.effects, new Set([
...tags,
ROM_MAGE_WARLOCK_PASSIVE_TAGS.controlFlameCooldown,
]), 0.099).some((effect) => effect.kind === "apply-aura"
&& effect.aura.id === "rom-mage-druid-control-flame-buff")).toBe(false);
});
it("applies Eruption and Control Flame when a Fireball proc succeeds", () => {
initializeMage("rom-druid");
addTarget("eruption-target", 3);
useCombatStore.getState().selectMob("eruption-target");
const before = useCombatStore.getState().mobs["eruption-target"]!.health;
expect(useCombatStore.getState().auras.some((aura) => aura.targetId === PLAYER_AGGRO_ID
&& aura.definition.utilityTags?.includes(ROM_MAGE_WARLOCK_PASSIVE_TAGS.eruption))).toBe(true);
expect(useCombatStore.getState().castAbility("rom-mage-general-0", undefined, 1_002)).toMatchObject({ ok: true });
expect(useCombatStore.getState().mobs["eruption-target"]!.health).toBeLessThan(before);
expect(useCombatStore.getState().auras.some((aura) => aura.definition.id === "rom-mage-druid-control-flame-buff"))
.toBe(true);
const flameBuff = useCombatStore.getState().auras.find((aura) => aura.definition.id === "rom-mage-druid-control-flame-buff");
expect(flameBuff?.definition.modifiers).toEqual([
expect.objectContaining({ kind: "stat", stat: "magical-attack", operation: "percent", value: 0.08 }),
]);
});
it("reduces Fireball cooldown by one second with Flame Follower", () => {
initializeMage("rom-warden");
addTarget("follower-target", 3);
useCombatStore.getState().selectMob("follower-target");
expect(useCombatStore.getState().auras.some((aura) => aura.targetId === PLAYER_AGGRO_ID
&& aura.definition.utilityTags?.includes(ROM_MAGE_WARLOCK_PASSIVE_TAGS.flameFollower))).toBe(true);
expect(useCombatStore.getState().castAbility("rom-mage-general-0", undefined, 1_000)).toMatchObject({ ok: true });
expect(useCombatStore.getState().cooldowns["rom-mage-general-0"]).toBe(6_000);
});
it("resets only Lightning and Discharge with Brain Shock", () => {
initializeMage("rom-champion");
useCombatStore.setState((state) => ({
cooldowns: {
...state.cooldowns,
"rom-mage-general-1": 8_000,
"rom-mage-primary-4": 10_000,
"rom-mage-general-0": 9_000,
},
resourcePools: { ...state.resourcePools, rage: 100 },
}));
expect(useCombatStore.getState().castAbility("rom-mage-rom-champion-elite-20", undefined, 1_000))
.toMatchObject({ ok: true });
expect(useCombatStore.getState().cooldowns).toMatchObject({
"rom-mage-general-1": 0,
"rom-mage-primary-4": 0,
"rom-mage-general-0": 9_000,
});
});
it("reduces Electrostatic Charge cooldown by 30 seconds with Mage/Champion expertise", () => {
initializeMage(undefined, 80);
expect(useCombatStore.getState().castAbility("rom-mage-primary-2", undefined, 1_000)).toMatchObject({ ok: true });
expect(useCombatStore.getState().cooldowns["rom-mage-primary-2"]).toBe(121_000);
initializeMage("rom-champion", 40);
expect(useCombatStore.getState().castAbility("rom-mage-primary-2", undefined, 1_000)).toMatchObject({ ok: true });
expect(useCombatStore.getState().cooldowns["rom-mage-primary-2"]).toBe(121_000);
initializeMage("rom-champion", 80);
expect(useCombatStore.getState().castAbility("rom-mage-primary-2", undefined, 1_000)).toMatchObject({ ok: true });
expect(useCombatStore.getState().cooldowns["rom-mage-primary-2"]).toBe(91_000);
});
it("keeps Mage Fire Ward party-scoped", () => {
const fireWard = abilityById("rom-mage-general-5")!;
expect(fireWard.effects.some((effect) => effect.kind === "apply-aura" && effect.scope === "party"))
.toBe(true);
});
});