Files
healer-man/src/game/manastormSpellLoadout.test.ts
T
2026-08-14 15:56:39 -04:00

150 lines
6.1 KiB
TypeScript

import { beforeEach, describe, expect, it } from "vitest";
import { actionBindingId } from "./actionBindings";
import { abilitiesForClass } from "./abilityCatalog";
import { useCombatStore } from "./combatStore";
import {
activateManastormActiveSpellLoadout,
manastormActiveSpellLoadoutModel,
resolveManastormActiveSpellLoadout,
validateManastormActiveSpellLoadout,
} from "./manastormSpellLoadout";
import { useManastormStore } from "./manastormStore";
import { useGameStore } from "./store";
describe("Manastorm active-spell loadouts", () => {
beforeEach(() => {
useCombatStore.getState().initializeCharacter({ classId: "priest", level: 20 });
useGameStore.setState({ gameMode: "dungeon" });
useManastormStore.getState().resetRun();
});
it("offers only unlocked active spells grounded in the imported client catalog", () => {
const model = manastormActiveSpellLoadoutModel("priest", 20);
expect(model.options.length).toBeGreaterThan(4);
expect(model.options.every((option) => (
option.unlockLevel <= 20
&& option.dbcSpellId > 0
&& option.source === "wow335-progression"
))).toBe(true);
expect(model.provenance).toMatchObject({
model: "local-client-spell-selection",
fidelity: "substitution",
exactServerParity: false,
source: "character-client-ability-catalog",
});
expect(model.provenance.note).toMatch(/no authoritative spell-choice table/i);
});
it("filters unlearned tree spells and resolves learned DBC ranks", () => {
const withoutTalent = manastormActiveSpellLoadoutModel("stormbringer", 60, {});
expect(withoutTalent.options.every((option) => option.source !== "coa-tree")).toBe(true);
const treeAbility = abilitiesForClass("stormbringer")
.find((ability) => (
ability.source === "coa-tree"
&& !ability.passive
&& ability.unlockLevel <= 60
))!;
expect(treeAbility).toBeDefined();
const allStormbringer = manastormActiveSpellLoadoutModel(
"stormbringer",
60,
{ [`coa-entry-${treeAbility.talentEntryId}`]: 1 },
);
const learned = allStormbringer.options.filter((option) => option.source === "coa-tree");
expect(learned.map((option) => option.id)).toContain(treeAbility.id);
expect(learned.every((option) => option.dbcSpellId > 0)).toBe(true);
});
it("rejects oversized, duplicate, and unavailable selections", () => {
const model = manastormActiveSpellLoadoutModel("priest", 20);
const ids = model.options.map((option) => option.id);
expect(validateManastormActiveSpellLoadout(ids.slice(0, 5), model.options, 4))
.toMatchObject({ ok: false, error: "Select no more than 4 active spells." });
expect(validateManastormActiveSpellLoadout([ids[0], ids[0]], model.options, 4))
.toMatchObject({ ok: false, error: `${ids[0]} is selected more than once.` });
expect(validateManastormActiveSpellLoadout(["server-only-spell"], model.options, 4))
.toMatchObject({ ok: false, error: expect.stringMatching(/not an available client-grounded spell/) });
});
it("validates the persisted selection again when a run starts", () => {
const model = manastormActiveSpellLoadoutModel("priest", 20);
const selectedIds = model.options.slice(0, 2).map((option) => option.id);
useManastormStore.getState().startRun({
activeSpellLoadout: {
available: model.options,
selectedIds,
maxSelections: model.maxSelections,
provenance: model.provenance,
},
random: () => 0,
});
expect(useManastormStore.getState().activeSpellLoadout).toMatchObject({
selectedIds,
validationError: null,
provenance: { exactServerParity: false },
});
expect(useManastormStore.getState().setActiveSpellLoadout(["server-only-spell"])).toBe(false);
expect(useManastormStore.getState().activeSpellLoadout.selectedIds).toEqual(selectedIds);
const invalid = resolveManastormActiveSpellLoadout({
available: model.options,
selectedIds: ["server-only-spell"],
maxSelections: model.maxSelections,
provenance: model.provenance,
});
expect(invalid).toMatchObject({
selectedIds: [],
selected: [],
validationError: expect.stringMatching(/not an available client-grounded spell/),
});
useManastormStore.getState().startRun({
activeSpellLoadout: {
available: model.options,
selectedIds: ["server-only-spell"],
maxSelections: model.maxSelections,
provenance: model.provenance,
},
random: () => 0,
});
expect(useManastormStore.getState().message).toMatch(/selection ignored/i);
});
it("pins selected spells to temporary base controls only in Manastorm mode", () => {
const model = manastormActiveSpellLoadoutModel("priest", 20);
const selectedIds = model.options.slice(0, 2).map((option) => option.id);
const loadout = resolveManastormActiveSpellLoadout({
available: model.options,
selectedIds,
maxSelections: model.maxSelections,
provenance: model.provenance,
});
const ordinaryBindings = useCombatStore.getState().actionBindings;
expect(activateManastormActiveSpellLoadout(loadout)).toBe(false);
expect(useCombatStore.getState().actionBindings).toEqual(ordinaryBindings);
useGameStore.setState({ gameMode: "manastorm" });
expect(activateManastormActiveSpellLoadout(loadout)).toBe(true);
expect(useCombatStore.getState().actionBindings).toMatchObject({
[actionBindingId("primary", "face-bottom")]: selectedIds[0],
[actionBindingId("primary", "face-left")]: selectedIds[1],
[actionBindingId("primary", "face-top")]: null,
[actionBindingId("primary", "face-right")]: null,
});
useCombatStore.getState().initializeCharacter({ classId: "priest", level: 20 });
const defaults = useCombatStore.getState().actionBindings;
expect(activateManastormActiveSpellLoadout(resolveManastormActiveSpellLoadout({
available: model.options,
selectedIds: [],
maxSelections: model.maxSelections,
provenance: model.provenance,
}))).toBe(true);
expect(useCombatStore.getState().actionBindings).toEqual(defaults);
});
});