forked from phenom/healer-man
114 lines
5.6 KiB
TypeScript
114 lines
5.6 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
import { createDefaultAppearance } from "./characterCatalog";
|
|
import {
|
|
createCharacter,
|
|
listCharacters,
|
|
updateCharacterProgression,
|
|
type StorageLike,
|
|
} from "./accountRepository";
|
|
import { beginMainMenuCharacterEditing } from "./mainMenuCharacterEditing";
|
|
import { selectedCharacter, useShellStore } from "./shellStore";
|
|
import { useCombatStore } from "../game/combatStore";
|
|
import { talentNodesForTree } from "../game/talentCatalog";
|
|
|
|
class MemoryStorage implements StorageLike {
|
|
values = new Map<string, string>();
|
|
getItem(key: string) { return this.values.get(key) ?? null; }
|
|
setItem(key: string, value: string) { this.values.set(key, value); }
|
|
removeItem(key: string) { this.values.delete(key); }
|
|
}
|
|
|
|
describe("main-menu character editing", () => {
|
|
let storage: MemoryStorage;
|
|
let stop: (() => void) | null;
|
|
const holyTalent = talentNodesForTree("priest-holy")[0];
|
|
|
|
beforeEach(() => {
|
|
storage = new MemoryStorage();
|
|
stop = null;
|
|
const created = createCharacter({
|
|
ownerId: "offline-roster", name: "Menuhealer", classId: "priest",
|
|
raceId: "human", gender: "female", appearance: createDefaultAppearance(),
|
|
specializationId: "priest:holy",
|
|
}, storage).character!;
|
|
const character = updateCharacterProgression(created.ownerId, created.id, {
|
|
level: 17, experience: 123, talentRanks: {},
|
|
}, storage)!;
|
|
useShellStore.setState({
|
|
phase: "main-menu", activeCharacter: null, selectedCharacterId: character.id,
|
|
session: { kind: "offline", ownerId: character.ownerId, displayName: "Offline" },
|
|
characters: [character], notice: "",
|
|
});
|
|
useCombatStore.getState().initializeCharacter({ classId: "mage", level: 1 });
|
|
});
|
|
afterEach(() => { stop?.(); });
|
|
|
|
it("loads the selected character without rewriting their saved profile or starting a dungeon", () => {
|
|
const before = [...storage.values];
|
|
stop = beginMainMenuCharacterEditing(storage);
|
|
expect(stop).not.toBeNull();
|
|
expect(useCombatStore.getState()).toMatchObject({ classId: "priest", level: 17, xp: 123 });
|
|
expect([...storage.values]).toEqual(before);
|
|
expect(useShellStore.getState()).toMatchObject({ phase: "main-menu", activeCharacter: null });
|
|
});
|
|
|
|
it("persists talent allocation and reset, and reloads the saved build when reopened", () => {
|
|
stop = beginMainMenuCharacterEditing(storage);
|
|
expect(useCombatStore.getState().allocateTalent(holyTalent.id)).toBe(true);
|
|
expect(listCharacters("offline-roster", storage)[0].talentRanks[holyTalent.id]).toBe(1);
|
|
expect(selectedCharacter(useShellStore.getState())?.talentRanks[holyTalent.id]).toBe(1);
|
|
stop!();
|
|
stop = beginMainMenuCharacterEditing(storage);
|
|
expect(useCombatStore.getState().talentRanks[holyTalent.id]).toBe(1);
|
|
useCombatStore.getState().resetTalents();
|
|
expect(listCharacters("offline-roster", storage)[0].talentRanks).toEqual({});
|
|
expect(listCharacters("offline-roster", storage)[0]).toMatchObject({ level: 17, experience: 123 });
|
|
});
|
|
|
|
it("persists controller assignments and reloads them without touching progression or equipment", () => {
|
|
const profile = selectedCharacter(useShellStore.getState())!;
|
|
stop = beginMainMenuCharacterEditing(storage);
|
|
expect(useCombatStore.getState().setActionBinding("primary", "face-bottom", "priest-flash-heal")).toBe(true);
|
|
const saved = listCharacters("offline-roster", storage)[0];
|
|
expect(saved.actionBindings["primary:face-bottom"]).toBe("priest-flash-heal");
|
|
expect(saved).toMatchObject({ level: profile.level, experience: profile.experience,
|
|
talentRanks: profile.talentRanks, inventory: profile.inventory, equipment: profile.equipment });
|
|
stop!();
|
|
stop = beginMainMenuCharacterEditing(storage);
|
|
expect(useCombatStore.getState().actionBindings["primary:face-bottom"]).toBe("priest-flash-heal");
|
|
});
|
|
|
|
it.each(["phase", "owner", "selection", "closed"])("stops saving after the editing context changes: %s", (change) => {
|
|
stop = beginMainMenuCharacterEditing(storage);
|
|
const before = [...storage.values];
|
|
if (change === "phase") useShellStore.setState({ phase: "game" });
|
|
if (change === "owner") useShellStore.setState({ session: { kind: "offline", ownerId: "another-owner", displayName: "Other" } });
|
|
if (change === "selection") useShellStore.setState({ selectedCharacterId: null });
|
|
if (change === "closed") stop!();
|
|
useCombatStore.getState().allocateTalent(holyTalent.id);
|
|
useCombatStore.getState().setActionBinding("primary", "face-bottom", "priest-flash-heal");
|
|
expect([...storage.values]).toEqual(before);
|
|
});
|
|
|
|
it("never saves combat XP as a side effect of a talent edit", () => {
|
|
stop = beginMainMenuCharacterEditing(storage);
|
|
useCombatStore.setState({ xp: 999 });
|
|
useCombatStore.getState().allocateTalent(holyTalent.id);
|
|
expect(listCharacters("offline-roster", storage)[0].experience).toBe(123);
|
|
});
|
|
|
|
it("rejects editing without a selected, owned main-menu character", () => {
|
|
useShellStore.setState({ selectedCharacterId: null });
|
|
expect(beginMainMenuCharacterEditing(storage)).toBeNull();
|
|
expect(useCombatStore.getState().classId).toBe("mage");
|
|
});
|
|
|
|
it("reports storage failures instead of updating the roster as though the save succeeded", () => {
|
|
stop = beginMainMenuCharacterEditing(storage);
|
|
storage.setItem = () => { throw new Error("Storage full"); };
|
|
useCombatStore.getState().allocateTalent(holyTalent.id);
|
|
expect(useShellStore.getState().notice).toMatch(/could not save/i);
|
|
expect(selectedCharacter(useShellStore.getState())?.talentRanks).toEqual({});
|
|
});
|
|
});
|