Release Healer Man 0.1.12

This commit is contained in:
phenom committed 2026-09-01 14:35:56 -04:00
1 parent 13970d03c8
commit 65e24f644e
201 files changed
+5061 -123

No files matched your search

+23 -1
View File
@@ -139,7 +139,7 @@ describe("local profile repository", () => {
});
});
it("migrates v1/v2 databases and stores combat presentation settings by owner", async () => {
it("migrates legacy databases and stores combat presentation settings by owner", async () => {
for (const version of [1, 2]) {
const storage = new MemoryStorage();
storage.setItem(PROFILE_STORAGE_KEYS.database, JSON.stringify({
@@ -152,11 +152,33 @@ describe("local profile repository", () => {
expect(JSON.parse(storage.getItem(PROFILE_STORAGE_KEYS.database)!).version).toBe(3);
}
const legacyV3Storage = new MemoryStorage();
legacyV3Storage.setItem(PROFILE_STORAGE_KEYS.database, JSON.stringify({
version: 3,
accounts: [],
rosters: { "offline-roster": [] },
presentationSettingsByOwner: {
"offline-roster": {
vfxQuality: "low",
vfxEnabled: { player: true, party: false, enemy: true },
audioEnabled: { player: true, party: true, enemy: false },
audioVolume: { player: 0.75, party: 0.4, enemy: 0.2 },
},
},
}));
expect(loadCombatPresentationSettings("offline-roster", legacyV3Storage)).toMatchObject({
vfxMode: "generated",
vfxQuality: "low",
});
expect(JSON.parse(legacyV3Storage.getItem(PROFILE_STORAGE_KEYS.database)!)
.presentationSettingsByOwner["offline-roster"].vfxMode).toBe("generated");
const storage = new MemoryStorage();
const firstOwner = (await createLocalAccount("EffectsOne", "eight-plus", storage)).session!.ownerId;
const secondOwner = (await createLocalAccount("EffectsTwo", "eight-plus", storage)).session!.ownerId;
const customized = updateCombatPresentationSettings(firstOwner, {
...DEFAULT_COMBAT_PRESENTATION_SETTINGS,
vfxMode: "original",
vfxQuality: "low",
vfxEnabled: { player: true, party: false, enemy: true },
audioEnabled: { player: true, party: true, enemy: false },
+10 -5
View File
@@ -209,14 +209,19 @@ function readDatabase(storage: StorageLike | null = browserLocalStorage()): Prof
&& !Array.isArray(parsed.presentationSettingsByOwner)
? parsed.presentationSettingsByOwner as Record<string, unknown>
: {};
let presentationSettingsMigrated = false;
const presentationSettingsByOwner = Object.fromEntries(
Object.entries(rawPresentationSettings).map(([ownerId, settings]) => [
ownerId,
normalizeCombatPresentationSettings(settings),
]),
Object.entries(rawPresentationSettings).map(([ownerId, settings]) => {
const normalized = normalizeCombatPresentationSettings(settings);
const storedMode = settings && typeof settings === "object" && !Array.isArray(settings)
? (settings as { vfxMode?: unknown }).vfxMode
: undefined;
if (storedMode !== normalized.vfxMode) presentationSettingsMigrated = true;
return [ownerId, normalized];
}),
);
const database: ProfileDatabase = { version: 3, accounts, rosters, presentationSettingsByOwner };
if (parsed.version !== 3) writeDatabase(database, storage);
if (parsed.version !== 3 || presentationSettingsMigrated) writeDatabase(database, storage);
return database;
} catch {
return emptyDatabase();
+117
View File
@@ -0,0 +1,117 @@
import { beforeEach, describe, expect, it } from "vitest";
import { createDefaultAppearance } from "./characterCatalog";
import { enterRandomGmReview } from "./gmManastormRuntime";
import { useShellStore } from "./shellStore";
import { DUNGEON_CATALOG } from "../game/dungeonCatalog";
import {
DEFAULT_MANASTORM_ADMIN_CONFIG,
resolvedBossAdminCatalog,
type ManastormAdminConfig,
} from "../game/manastormAdminConfig";
import { useManastormAdminStore } from "../game/manastormAdminStore";
import { useManastormStore } from "../game/manastormStore";
import { useGameStore } from "../game/store";
const character = {
id: "gm-character",
ownerId: "gm-account",
name: "Verifier",
raceId: "human" as const,
classId: "priest" as const,
gender: "female" as const,
appearance: createDefaultAppearance(),
level: 20,
experience: 0,
talentRanks: {},
actionBindings: {},
inventory: [],
equipment: {},
location: "GM Tools",
createdAt: 1,
lastPlayedAt: 1,
};
function prepareGm(config: ManastormAdminConfig): void {
useShellStore.setState({
phase: "gm-admin",
session: {
kind: "account",
ownerId: "gm-account",
displayName: "Game Master",
accessToken: "gm-token",
roles: ["gm"],
},
characters: [character],
selectedCharacterId: character.id,
activeCharacter: null,
notice: "",
});
useManastormAdminStore.setState({ config, draft: config, dirty: false, saving: false, error: null });
}
describe("GM random review runtime", () => {
beforeEach(() => {
useGameStore.getState().resetAtEntrance();
useManastormStore.getState().resetRun();
useGameStore.setState({ activeGmReviewTarget: null, gmTestMode: false });
});
it("launches the exact remaining dungeon in GM test mode", () => {
const available = DUNGEON_CATALOG.filter((dungeon) => dungeon.available);
const selected = available[0];
const config: ManastormAdminConfig = {
...DEFAULT_MANASTORM_ADMIN_CONFIG,
dungeons: Object.fromEntries(available.map((dungeon) => [
dungeon.id,
{ status: dungeon.id === selected.id ? "needs-tested" : "ready" },
])),
};
prepareGm(config);
expect(enterRandomGmReview("dungeon")).toBe(true);
expect(useShellStore.getState()).toMatchObject({ phase: "game", activeDungeonId: selected.id });
expect(useGameStore.getState()).toMatchObject({
gmTestMode: true,
activeGmReviewTarget: { kind: "dungeon", dungeonId: selected.id },
});
});
it("allows a runnable Not ready Manastorm in an isolated GM review", () => {
const catalog = resolvedBossAdminCatalog(DEFAULT_MANASTORM_ADMIN_CONFIG);
const selected = catalog.find((entry) => entry.runnable && entry.source.includes("installed-manastorm"));
expect(selected).toBeTruthy();
const config: ManastormAdminConfig = {
...DEFAULT_MANASTORM_ADMIN_CONFIG,
bosses: Object.fromEntries(catalog.map((entry) => [
entry.key,
{ status: entry.key === selected!.key ? "not-ready" : "ready" },
])),
};
prepareGm(config);
expect(enterRandomGmReview("manastorm")).toBe(true);
expect(useGameStore.getState()).toMatchObject({
gameMode: "manastorm",
gmTestMode: true,
activeGmReviewTarget: { kind: "manastorm", bossKey: selected!.key },
});
expect(useManastormStore.getState().currentEncounter?.bossName).toBe(selected!.name);
});
it("rejects non-GM sessions and clears review context when leaving", () => {
prepareGm(DEFAULT_MANASTORM_ADMIN_CONFIG);
useShellStore.setState({
session: { kind: "offline", ownerId: "gm-account", displayName: "Offline", roles: ["gm"] },
});
expect(enterRandomGmReview("dungeon")).toBe(false);
expect(useGameStore.getState().activeGmReviewTarget).toBeNull();
prepareGm(DEFAULT_MANASTORM_ADMIN_CONFIG);
useGameStore.setState({
gmTestMode: true,
activeGmReviewTarget: { kind: "dungeon", dungeonId: DUNGEON_CATALOG[0].id },
});
useShellStore.getState().returnToMainMenu();
expect(useGameStore.getState()).toMatchObject({ activeGmReviewTarget: null, gmTestMode: false });
});
});
+59 -6
View File
@@ -5,34 +5,87 @@ import {
isGameMasterSession,
resolvedBossAdminCatalog,
} from "../game/manastormAdminConfig";
import {
drawRandomGmReviewTarget,
type GmReviewKind,
type GmReviewTarget,
} from "../game/gmContentReview";
import { useManastormAdminStore } from "../game/manastormAdminStore";
import { INSTALLED_MANASTORM_RUNTIME_CATALOG } from "../game/manastormInstalledCatalog";
import { installedManastormStageBinding } from "../game/manastormInstalledStages";
import { registerManastormStageBindingResolver } from "../game/manastormStagePopulation";
import { activateManastormStage } from "../game/manastormStageLoader";
import { activateManastormStage, releaseActiveManastormStage } from "../game/manastormStageLoader";
import { manastormModeForCharacterLevel, setManastormCatalog } from "../game/manastorm";
import { createEmptyManastormProgress, manastormModeProgress } from "../game/manastormProgress";
import { manastormActiveSpellLoadoutModel } from "../game/manastormSpellLoadout";
import { useManastormStore } from "../game/manastormStore";
import { defaultPartyRoleForClass } from "../game/partyRoles";
import { useGameStore } from "../game/store";
import { activateDungeonSession } from "../game/dungeonSession";
import type { CharacterProfile } from "./types";
export function enterGmManastormTest(bossKey: string): boolean {
function activeGmCharacter(): CharacterProfile | null {
const shell = useShellStore.getState();
if (!isGameMasterSession(shell.session) || !shell.selectedCharacterId) {
useShellStore.setState({ notice: "Game Master access is required." });
return false;
return null;
}
const activeCharacter = touchCharacter(shell.session!.ownerId, shell.selectedCharacterId)
?? shell.characters.find((character) => character.id === shell.selectedCharacterId)
?? null;
if (!activeCharacter) {
useShellStore.setState({ notice: "Select a character before testing a boss." });
useShellStore.setState({ notice: "Select a character before testing content." });
return null;
}
return activeCharacter;
}
function enterGmDungeonTest(target: Extract<GmReviewTarget, { readonly kind: "dungeon" }>): boolean {
const activeCharacter = activeGmCharacter();
if (!activeCharacter) return false;
useManastormStore.getState().resetRun();
releaseActiveManastormStage();
if (!activateDungeonSession(target.dungeonId, undefined, {
gmTestMode: true,
gmReviewTarget: target,
})) {
useShellStore.setState({ notice: "That dungeon did not pass its offline runtime gate." });
return false;
}
useGameStore.getState().setCompanionOpen(false);
useShellStore.setState({
phase: "game",
activeCharacter,
activeDungeonId: target.dungeonId,
activeDungeonRole: defaultPartyRoleForClass(activeCharacter.classId),
notice: "GM dungeon review",
});
return true;
}
export function enterRandomGmReview(kind: GmReviewKind): boolean {
const admin = useManastormAdminStore.getState();
if (admin.dirty || admin.saving) {
useShellStore.setState({ notice: "Save or discard GM changes before testing another target." });
return false;
}
const previous = useGameStore.getState().activeGmReviewTarget;
const target = drawRandomGmReviewTarget(kind, admin.config, previous);
if (!target) {
const label = kind === "dungeon" ? "dungeons" : kind === "boss" ? "bosses" : "Manastorm encounters";
useShellStore.setState({ notice: `All runnable ${label} have been marked Ready.` });
return false;
}
if (target.kind === "dungeon") return enterGmDungeonTest(target);
return enterGmManastormTest(target.bossKey, target);
}
export function enterGmManastormTest(bossKey: string, reviewTarget: GmReviewTarget | null = null): boolean {
const activeCharacter = activeGmCharacter();
if (!activeCharacter) return false;
const adminConfig = useManastormAdminStore.getState().config;
const entry = resolvedBossAdminCatalog(adminConfig).find((candidate) => candidate.key === bossKey);
if (!entry || entry.status === "not-ready" || !entry.runnable || !entry.stage) {
if (!entry || !entry.runnable || !entry.stage) {
useShellStore.setState({ notice: entry?.validationIssues[0] ?? "That boss is not available for GM testing." });
return false;
}
@@ -84,7 +137,7 @@ export function enterGmManastormTest(bossKey: string): boolean {
useShellStore.setState({ notice: "The configured boss could not be prepared for testing." });
return false;
}
if (!activateManastormStage(encounter, undefined, false, true).activated) {
if (!activateManastormStage(encounter, undefined, false, true, reviewTarget).activated) {
useManastormStore.getState().resetRun();
useShellStore.setState({ notice: "The configured dungeon or stage package is not loadable." });
return false;
+3
View File
@@ -166,6 +166,7 @@ function abandonRun(): void {
const state = useManastormStore.getState();
const completed = state.completed;
state.resetRun();
useGameStore.setState({ activeGmReviewTarget: null, gmTestMode: false });
useShellStore.setState({
phase: "manastorms",
activeCharacter: null,
@@ -179,6 +180,7 @@ function failRun(): void {
const state = useManastormStore.getState();
if (state.status !== "failed" && !state.failRun()) return;
useGameStore.getState().setPaused(true);
useGameStore.setState({ activeGmReviewTarget: null, gmTestMode: false });
useShellStore.setState({
phase: "manastorms",
activeCharacter: null,
@@ -190,6 +192,7 @@ function leaveActiveGame(): void {
if (useGameStore.getState().gameMode !== "manastorm") return;
useManastormStore.getState().resetRun();
releaseActiveManastormStage();
useGameStore.setState({ activeGmReviewTarget: null, gmTestMode: false });
}
function saveSelectedLoadout(loadout: readonly string[]): boolean {
+4
View File
@@ -54,6 +54,7 @@ import {
refreshOnlineSession,
} from "./onlineAccountClient";
import { isGameMasterSession } from "../game/manastormAdminConfig";
import { useGameStore } from "../game/store";
export interface ShellState {
phase: ShellPhase;
@@ -164,6 +165,7 @@ export const useShellStore = create<ShellState>((set, get) => ({
});
},
signOut: () => {
useGameStore.setState({ activeGmReviewTarget: null, gmTestMode: false });
const session = get().session;
void logoutOnlineSession(session);
clearSession();
@@ -352,6 +354,7 @@ export const useShellStore = create<ShellState>((set, get) => ({
return true;
},
returnToMainMenu: () => {
useGameStore.setState({ activeGmReviewTarget: null, gmTestMode: false });
manastormShellRuntime()?.leaveActiveGame();
const { session, characters, activeCharacter, selectedCharacterId } = get();
if (!session) {
@@ -381,6 +384,7 @@ export const useShellStore = create<ShellState>((set, get) => ({
failActiveManastorm: () => manastormShellRuntime()?.failRun(),
enterWorld: () => get().enterDungeon(DEFAULT_DUNGEON_ID),
returnToCharacters: () => {
useGameStore.setState({ activeGmReviewTarget: null, gmTestMode: false });
const session = get().session;
if (!session) {
set({ phase: "login", activeCharacter: null });