Files
healer-man/src/app/shellStore.test.ts
T
2026-08-18 16:22:19 -04:00

339 lines
12 KiB
TypeScript

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