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