Update class mechanics, stat rulesets, and CoA reference catalogs

This commit is contained in:
phenom committed 2026-10-04 12:56:30 -04:00
1 parent b16821e92e
commit 39458d5de6
221 files changed
+2584962 -568

No files matched your search

+104
View File
@@ -6,6 +6,7 @@ import { installManastormShellRuntime } from "./manastormShellRuntime";
import { SHELVED_MODES_ENABLED } from "./releaseFeatures";
import { DUNGEON_MANIFEST } from "../game/dungeonManifest";
import { useGameStore } from "../game/store";
import { useCombatStore } from "../game/combatStore";
import { useManastormStore } from "../game/manastormStore";
import { createEmptyManastormProgress, recordManastormCheckpoint } from "../game/manastormProgress";
import {
@@ -21,6 +22,8 @@ import {
resolveBossAdminEntry,
} from "../game/manastormAdminConfig";
import { useManastormAdminStore } from "../game/manastormAdminStore";
import { createCharacter, listCharacters, type StorageLike } from "./accountRepository";
import { useOnlineGroupStore } from "./onlineGroupStore";
installDungeonShellRuntime();
installManastormShellRuntime();
@@ -62,6 +65,20 @@ function readyBossForMode(modeId: number) {
const readyLevelingBoss = readyBossForMode(0);
const readyEndgameBoss = readyBossForMode(2);
if (!readyLevelingBoss || !readyEndgameBoss) throw new Error("Shell tests require runnable Manastorm bosses for both modes.");
class MemoryStorage implements StorageLike {
private 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); }
}
class FailingWriteStorage extends MemoryStorage {
failWrites = false;
setItem(key: string, value: string) {
if (this.failWrites) throw new Error("Storage quota exceeded.");
super.setItem(key, value);
}
}
const readyTestConfig = {
...DEFAULT_MANASTORM_ADMIN_CONFIG,
bosses: {
@@ -84,9 +101,11 @@ function prepareSelectedCharacter(): void {
describe("shell store", () => {
afterEach(() => {
vi.unstubAllEnvs();
vi.unstubAllGlobals();
});
beforeEach(() => {
useOnlineGroupStore.getState().reset();
useShellStore.setState({
phase: "login",
session: null,
@@ -108,6 +127,91 @@ describe("shell store", () => {
expect(useShellStore.getState().notice).toMatch(/select or create/i);
});
it("saves a Hunter's selected family to the active character and local roster", () => {
const storage = new MemoryStorage();
vi.stubGlobal("window", { localStorage: storage });
const hunter = createCharacter({
ownerId: "offline-roster",
name: "Turtlekeeper",
categoryId: "wow",
raceId: "human",
classId: "hunter",
specializationId: "hunter:marksmanship",
gender: "female",
appearance: createDefaultAppearance(),
}, storage).character!;
useShellStore.setState({
session: { kind: "offline", ownerId: hunter.ownerId, displayName: "Offline" },
characters: [hunter],
activeCharacter: hunter,
});
expect(useShellStore.getState().saveActiveHunterPetFamily("turtle")).toBe(true);
expect(useShellStore.getState().activeCharacter?.hunterPetFamily).toBe("turtle");
expect(listCharacters(hunter.ownerId, storage)[0]?.hunterPetFamily).toBe("turtle");
expect(useShellStore.getState().saveActiveHunterPetFamily("cat")).toBe(true);
expect(useShellStore.getState().activeCharacter?.hunterPetFamily).toBe("cat");
expect(listCharacters(hunter.ownerId, storage)[0]?.hunterPetFamily).toBe("cat");
});
it("switches the selected RoM stat profile only from the main menu and reports storage failure", () => {
const storage = new FailingWriteStorage();
vi.stubGlobal("window", { localStorage: storage });
const romCharacter = createCharacter({
ownerId: "offline-roster",
name: "Runekeeper",
categoryId: "rom",
raceId: "rom-human",
classId: "rom-priest",
gender: "female",
appearance: createDefaultAppearance(),
}, storage).character!;
useShellStore.setState({
phase: "main-menu",
session: { kind: "offline", ownerId: romCharacter.ownerId, displayName: "Offline" },
characters: [romCharacter],
selectedCharacterId: romCharacter.id,
activeCharacter: null,
});
expect(useShellStore.getState().setSelectedCharacterStatRuleset("wow")).toBe(true);
expect(useShellStore.getState().characters[0]?.statRuleset).toBe("wow");
expect(listCharacters(romCharacter.ownerId, storage)[0]?.statRuleset).toBe("wow");
storage.failWrites = true;
expect(useShellStore.getState().setSelectedCharacterStatRuleset("rom")).toBe(false);
expect(useShellStore.getState().characters[0]?.statRuleset).toBe("wow");
expect(useShellStore.getState().notice).toMatch(/could not be saved/i);
storage.failWrites = false;
const previousMobs = useCombatStore.getState().mobs;
try {
useCombatStore.setState({ mobs: { "live-fight": { engaged: true, dead: false } as never } });
expect(useShellStore.getState().setSelectedCharacterStatRuleset("rom")).toBe(false);
expect(useShellStore.getState().notice).toMatch(/combat is active/i);
expect(listCharacters(romCharacter.ownerId, storage)[0]?.statRuleset).toBe("wow");
} finally {
useCombatStore.setState({ mobs: previousMobs });
}
useShellStore.setState({ activeCharacter: romCharacter });
expect(useShellStore.getState().setSelectedCharacterStatRuleset("rom")).toBe(false);
useShellStore.setState({ activeCharacter: null });
useOnlineGroupStore.setState({ snapshot: {
group: { activity: { id: "live-run" } } as never,
invitations: [],
} });
useShellStore.setState({ phase: "main-menu", notice: "" });
expect(useShellStore.getState().setSelectedCharacterStatRuleset("rom")).toBe(false);
expect(useShellStore.getState().characters[0]?.statRuleset).toBe("wow");
expect(useShellStore.getState().notice).toMatch(/live online group activity/i);
useOnlineGroupStore.getState().reset();
useShellStore.setState({ phase: "game", notice: "" });
expect(useShellStore.getState().setSelectedCharacterStatRuleset("rom")).toBe(false);
expect(useShellStore.getState().characters[0]?.statRuleset).toBe("wow");
expect(listCharacters(romCharacter.ownerId, storage)[0]?.statRuleset).toBe("wow");
});
it("routes a legacy WoW character through the mandatory specialization picker", () => {
prepareSelectedCharacter();
useShellStore.setState({ characters: [{ ...character, specializationId: null }] });