import { beforeEach, describe, expect, it } from "vitest"; import { createDefaultAppearance } from "./characterCatalog"; import { useShellStore } from "./shellStore"; import { installDungeonShellRuntime } from "./dungeonShellRuntime"; import { installManastormShellRuntime } from "./manastormShellRuntime"; import { DUNGEON_MANIFEST } from "../game/dungeonManifest"; import { useGameStore } from "../game/store"; import { useManastormStore } from "../game/manastormStore"; import { createEmptyManastormProgress, recordManastormCheckpoint } from "../game/manastormProgress"; import { manastormActiveSpellLoadoutModel, } from "../game/manastormSpellLoadout"; import { mergeManastormUnlockedContent, setManastormLoadout, } from "../game/manastormProgress"; import { DEFAULT_MANASTORM_ADMIN_CONFIG, UNIFIED_MANASTORM_BOSS_CATALOG, resolveBossAdminEntry, } from "../game/manastormAdminConfig"; import { useManastormAdminStore } from "../game/manastormAdminStore"; installDungeonShellRuntime(); installManastormShellRuntime(); const character = { id: "character-1", ownerId: "offline-roster", name: "Remedy", raceId: "human" as const, classId: "priest" as const, gender: "female" as const, appearance: createDefaultAppearance(), level: 1, experience: 0, talentRanks: {}, actionBindings: {}, inventory: [], equipment: {}, location: "Wailing Caverns Entrance", createdAt: 1, lastPlayedAt: 1, }; function readyBossForMode(modeId: number) { return UNIFIED_MANASTORM_BOSS_CATALOG.find((entry) => ( (entry.baseStage?.unlockLevel ?? 1) <= 1 && (entry.baseStage?.minimumPartySize ?? 1) <= 1 && (entry.baseStage?.maximumPartySize ?? 5) >= 5 && (!entry.baseStage?.modeIds?.length || entry.baseStage.modeIds.includes(modeId)) && resolveBossAdminEntry(entry, { ...DEFAULT_MANASTORM_ADMIN_CONFIG, bosses: { [entry.key]: { status: "ready" } }, }).runnable )); } const readyLevelingBoss = readyBossForMode(0); const readyEndgameBoss = readyBossForMode(2); if (!readyLevelingBoss || !readyEndgameBoss) throw new Error("Shell tests require runnable Manastorm bosses for both modes."); const readyTestConfig = { ...DEFAULT_MANASTORM_ADMIN_CONFIG, bosses: { [readyLevelingBoss.key]: { status: "ready" as const }, [readyEndgameBoss.key]: { status: "ready" as const }, }, }; function prepareSelectedCharacter(): void { useShellStore.setState({ phase: "characters", session: { kind: "offline", ownerId: "offline-roster", displayName: "Offline" }, characters: [character], selectedCharacterId: character.id, activeCharacter: null, notice: "", }); } describe("shell store", () => { beforeEach(() => { useShellStore.setState({ phase: "login", session: null, characters: [], selectedCharacterId: null, activeCharacter: null, activeDungeonRole: "healer", notice: "", }); useGameStore.getState().resetAtEntrance(); useManastormStore.getState().resetRun(); useManastormAdminStore.setState({ config: readyTestConfig, draft: readyTestConfig, dirty: false }); }); it("does not open the character hub or a dungeon without a selected character", () => { expect(useShellStore.getState().openMainMenu()).toBe(false); expect(useShellStore.getState().enterDungeon(DUNGEON_MANIFEST.id)).toBe(false); expect(useShellStore.getState().phase).toBe("login"); expect(useShellStore.getState().notice).toMatch(/select or create/i); }); it("opens the main menu and dungeon list without resetting gameplay", () => { prepareSelectedCharacter(); useGameStore.setState({ paused: true, mapOpen: true, companionOpen: true, distanceTravelled: 42 }); expect(useShellStore.getState().openMainMenu()).toBe(true); expect(useShellStore.getState()).toMatchObject({ phase: "main-menu", activeCharacter: null }); expect(useGameStore.getState()).toMatchObject({ paused: true, mapOpen: true, companionOpen: true, distanceTravelled: 42, }); expect(useShellStore.getState().openDungeons()).toBe(true); expect(useShellStore.getState().phase).toBe("dungeons"); expect(useGameStore.getState().distanceTravelled).toBe(42); }); it("mounts the game and resets it only after choosing the available dungeon", () => { prepareSelectedCharacter(); useShellStore.getState().openMainMenu(); useShellStore.getState().openDungeons(); useGameStore.setState({ paused: true, mapOpen: true, companionOpen: true, distanceTravelled: 42 }); expect(useShellStore.getState().enterDungeon(DUNGEON_MANIFEST.id, "damage")).toBe(true); expect(useShellStore.getState()).toMatchObject({ phase: "game", activeCharacter: character, activeDungeonRole: "damage", }); expect(useGameStore.getState()).toMatchObject({ paused: false, mapOpen: false, companionOpen: false, distanceTravelled: 0, }); }); it("rejects a role the selected character's class cannot fill", () => { prepareSelectedCharacter(); expect(useShellStore.getState().enterDungeon(DUNGEON_MANIFEST.id, "tank")).toBe(false); expect(useShellStore.getState().phase).toBe("characters"); expect(useShellStore.getState().notice).toMatch(/cannot enter as tank/i); }); it("rejects an unknown dungeon without resetting or leaving the list", () => { prepareSelectedCharacter(); useShellStore.getState().openMainMenu(); useShellStore.getState().openDungeons(); useGameStore.setState({ distanceTravelled: 42 }); expect(useShellStore.getState().enterDungeon("missing-dungeon")).toBe(false); expect(useShellStore.getState().phase).toBe("dungeons"); expect(useShellStore.getState().notice).toMatch(/not available/i); expect(useGameStore.getState().distanceTravelled).toBe(42); }); it("returns from gameplay to the main menu with the same character selected", () => { prepareSelectedCharacter(); expect(useShellStore.getState().enterDungeon(DUNGEON_MANIFEST.id)).toBe(true); useShellStore.getState().returnToMainMenu(); expect(useShellStore.getState()).toMatchObject({ phase: "main-menu", selectedCharacterId: character.id, activeCharacter: null, notice: "", }); }); it("keeps the original single-dungeon entry point as a compatibility alias", () => { prepareSelectedCharacter(); expect(useShellStore.getState().enterWorld()).toBe(true); expect(useShellStore.getState().phase).toBe("game"); }); it("returns cleanly from the creator without touching game state", () => { useShellStore.getState().openCreator(); expect(useShellStore.getState().phase).toBe("create-character"); useShellStore.getState().cancelCreator(); expect(useShellStore.getState().phase).toBe("characters"); }); it("normalizes creator gender to the models shipped for an extended race", () => { useShellStore.getState().openCreator(); useShellStore.getState().updateDraft({ raceId: "vrykul" }); expect(useShellStore.getState().draft).toMatchObject({ raceId: "vrykul", gender: "male" }); useShellStore.getState().updateDraft({ gender: "female" }); expect(useShellStore.getState().draft.gender).toBe("male"); }); it("resets the creator to a valid draft when switching among WoW, RoM, and CoA", () => { useShellStore.getState().openCreator(); useShellStore.getState().updateDraft({ name: "Category Keeper", categoryId: "rom" }); expect(useShellStore.getState().draft).toMatchObject({ name: "Categorykeep", categoryId: "rom", raceId: "rom-human", classId: "rom-priest", gender: "female", }); useShellStore.getState().updateDraft({ raceId: "rom-dwarf", gender: "female", classId: "rom-champion" }); expect(useShellStore.getState().draft).toMatchObject({ raceId: "rom-dwarf", classId: "rom-champion", gender: "male", }); useShellStore.getState().updateDraft({ categoryId: "coa" }); expect(useShellStore.getState().draft).toMatchObject({ name: "Categorykeep", categoryId: "coa", raceId: "human", classId: "barbarian", gender: "female", }); useShellStore.getState().updateDraft({ classId: "priest" }); expect(useShellStore.getState().draft.classId).toBe("barbarian"); useShellStore.getState().updateDraft({ categoryId: "wow" }); expect(useShellStore.getState().draft).toMatchObject({ name: "Categorykeep", categoryId: "wow", raceId: "human", classId: "priest", gender: "female", }); }); it("opens Manastorms, validates the role, and starts a fresh level-one run", () => { prepareSelectedCharacter(); expect(useShellStore.getState().openManastorms()).toBe(true); expect(useShellStore.getState().phase).toBe("manastorms"); expect(useShellStore.getState().enterManastorm("tank")).toBe(false); expect(useShellStore.getState().notice).toMatch(/cannot enter as tank/i); expect(useShellStore.getState().enterManastorm("healer")).toBe(true); expect(useShellStore.getState()).toMatchObject({ phase: "game", activeDungeonRole: "healer", }); expect(useGameStore.getState()).toMatchObject({ gameMode: "manastorm" }); expect(useManastormStore.getState()).toMatchObject({ status: "entering", phase: "enter", level: 1, completed: 0, }); expect(useGameStore.getState().playerPosition).toEqual( useManastormStore.getState().currentEncounter?.playerSpawn.footPosition, ); }); it("starts each party size from only its own unlocked checkpoint", () => { const progress = recordManastormCheckpoint(createEmptyManastormProgress(), 3, 5, 6); useShellStore.setState({ phase: "manastorms", session: { kind: "offline", ownerId: "checkpoint-owner", displayName: "Offline" }, characters: [{ ...character, id: "checkpoint-character", ownerId: "checkpoint-owner", manastormProgress: progress, }], selectedCharacterId: "checkpoint-character", activeCharacter: null, notice: "", }); expect(useShellStore.getState().enterManastorm("healer", 3, 6)).toBe(true); expect(useManastormStore.getState()).toMatchObject({ partySize: 3, level: 6, checkpointLevel: 6, }); }); it("selects end-game mode at the character cap and hands off an eligible checkpoint", () => { const progress = recordManastormCheckpoint( createEmptyManastormProgress(), 3, 105, 106, ); useShellStore.setState({ phase: "manastorms", session: { kind: "offline", ownerId: "endgame-owner", displayName: "Offline" }, characters: [{ ...character, id: "endgame-character", ownerId: "endgame-owner", level: 60, manastormProgress: progress, }], selectedCharacterId: "endgame-character", activeCharacter: null, notice: "", }); expect(useShellStore.getState().enterManastorm("healer", 3, 106)).toBe(true); expect(useManastormStore.getState()).toMatchObject({ partySize: 3, modeId: 2, level: 106, checkpointLevel: 106, }); }); it("passes a persisted client-grounded spell selection into the run runtime", () => { const model = manastormActiveSpellLoadoutModel(character.classId, character.level); const selectedIds = model.options.slice(0, 2).map((option) => option.id); const unlocked = mergeManastormUnlockedContent( createEmptyManastormProgress(), model.options.map((option) => option.id), ); const progress = setManastormLoadout(unlocked, selectedIds, model.maxSelections).progress; useShellStore.setState({ phase: "manastorms", session: { kind: "offline", ownerId: character.ownerId, displayName: "Offline" }, characters: [{ ...character, manastormProgress: progress }], selectedCharacterId: character.id, activeCharacter: null, notice: "", }); expect(useShellStore.getState().enterManastorm("healer", 2, 1)).toBe(true); expect(useManastormStore.getState().activeSpellLoadout).toMatchObject({ selectedIds, validationError: null, provenance: { fidelity: "substitution", exactServerParity: false, }, }); }); });