Release Healer Man 0.1.32
This commit is contained in:
1 parent
d3d6aaebdb
commit
6dcb1034e5
102 files changed
+10772
-788
No files matched your search
@@ -1,6 +1,10 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { OnlineGroupActivity } from "./onlineGroupTypes";
|
||||
import { enterOnlineGroupActivity } from "./OnlineGroupCoordinator";
|
||||
import { SHELVED_MODES_ENABLED } from "./releaseFeatures";
|
||||
|
||||
const normalReleaseIt = SHELVED_MODES_ENABLED ? it.skip : it;
|
||||
const shelvedModeIt = SHELVED_MODES_ENABLED ? it : it.skip;
|
||||
|
||||
const DUNGEON_ACTIVITY: OnlineGroupActivity = {
|
||||
id: "activity-dungeon-1",
|
||||
@@ -12,6 +16,14 @@ const DUNGEON_ACTIVITY: OnlineGroupActivity = {
|
||||
startedAt: 1,
|
||||
};
|
||||
|
||||
const MANASTORM_ACTIVITY: OnlineGroupActivity = {
|
||||
...DUNGEON_ACTIVITY,
|
||||
id: "activity-manastorm-1",
|
||||
type: "manastorm",
|
||||
partySize: 3,
|
||||
startingLevel: 6,
|
||||
};
|
||||
|
||||
function entryDependencies(enterDungeon: () => boolean) {
|
||||
return {
|
||||
installDungeonRuntime: vi.fn(async () => undefined),
|
||||
@@ -75,4 +87,36 @@ describe("online group activity entry", () => {
|
||||
expect(dependencies.enterDungeon).not.toHaveBeenCalled();
|
||||
expect(consumeActivity).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
normalReleaseIt("does not load or enter a shelved group activity", async () => {
|
||||
const consumeActivity = vi.fn();
|
||||
const dependencies = entryDependencies(() => true);
|
||||
|
||||
await expect(enterOnlineGroupActivity(
|
||||
MANASTORM_ACTIVITY,
|
||||
"healer",
|
||||
consumeActivity,
|
||||
dependencies,
|
||||
)).resolves.toBe(false);
|
||||
|
||||
expect(dependencies.installManastormRuntime).not.toHaveBeenCalled();
|
||||
expect(dependencies.enterManastorm).not.toHaveBeenCalled();
|
||||
expect(consumeActivity).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
shelvedModeIt("restores Manastorm group entry in the developer mode", async () => {
|
||||
const consumeActivity = vi.fn();
|
||||
const dependencies = entryDependencies(() => true);
|
||||
|
||||
await expect(enterOnlineGroupActivity(
|
||||
MANASTORM_ACTIVITY,
|
||||
"healer",
|
||||
consumeActivity,
|
||||
dependencies,
|
||||
)).resolves.toBe(true);
|
||||
|
||||
expect(dependencies.installManastormRuntime).toHaveBeenCalledOnce();
|
||||
expect(dependencies.enterManastorm).toHaveBeenCalledWith("healer", 3, 6);
|
||||
expect(consumeActivity).toHaveBeenCalledWith(MANASTORM_ACTIVITY.id);
|
||||
});
|
||||
});
|
||||
@@ -6,9 +6,11 @@ import {
|
||||
defaultPartyRoleForClass,
|
||||
type PartyRole,
|
||||
} from "../game/partyRoles";
|
||||
import { isManastormPartySize, type ManastormPartySize } from "../game/manastormProgress";
|
||||
import type { ManastormPartySize } from "../game/manastormProgress";
|
||||
import type { OnlineGroupActivity } from "./onlineGroupTypes";
|
||||
import { useContentManagerSnapshot } from "../content/useContentManager";
|
||||
import { SHELVED_MODES_ENABLED } from "./releaseFeatures";
|
||||
import { installShelvedOnlineGroupRuntime } from "@shelvedOnlineGroupRuntime";
|
||||
|
||||
const GROUP_POLL_MS = 2_500;
|
||||
const ACTIVITY_JOIN_WINDOW_MS = 10 * 60 * 1000;
|
||||
@@ -27,8 +29,7 @@ const DEFAULT_ACTIVITY_ENTRY_DEPENDENCIES: OnlineGroupActivityEntryDependencies
|
||||
},
|
||||
enterDungeon: (dungeonId, role) => useShellStore.getState().enterDungeon(dungeonId, role),
|
||||
installManastormRuntime: async () => {
|
||||
const { installManastormShellRuntime } = await import("./manastormShellRuntime");
|
||||
installManastormShellRuntime();
|
||||
await installShelvedOnlineGroupRuntime();
|
||||
},
|
||||
enterManastorm: (role, partySize, startingLevel) => useShellStore.getState().enterManastorm(
|
||||
role,
|
||||
@@ -48,8 +49,11 @@ export async function enterOnlineGroupActivity(
|
||||
await dependencies.installDungeonRuntime();
|
||||
entered = dependencies.enterDungeon(activity.contentId, role);
|
||||
} else {
|
||||
if (!SHELVED_MODES_ENABLED) return false;
|
||||
await dependencies.installManastormRuntime();
|
||||
const partySize = isManastormPartySize(activity.partySize) ? activity.partySize : 5;
|
||||
const partySize = Number.isInteger(activity.partySize) && activity.partySize >= 1 && activity.partySize <= 5
|
||||
? activity.partySize as ManastormPartySize
|
||||
: 5;
|
||||
entered = dependencies.enterManastorm(role, partySize, activity.startingLevel ?? undefined);
|
||||
}
|
||||
if (entered) consumeActivity(activity.id);
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
export function ShelvedGameBridges() {
|
||||
return null;
|
||||
}
|
||||
|
||||
export function lootShelvedBoss(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
export function releaseShelvedGameRuntime(): void {}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ManastormBridge } from "../game/ManastormBridge";
|
||||
import { releaseActiveManastormStage } from "../game/manastormStageLoader";
|
||||
import { lootCurrentManastormBoss } from "../game/manastormLoot";
|
||||
import { GmReviewCard } from "../ui/GmReviewCard";
|
||||
import { GmWorldEditor } from "../ui/GmWorldEditor";
|
||||
|
||||
export function ShelvedGameBridges() {
|
||||
return (
|
||||
<>
|
||||
<ManastormBridge />
|
||||
<GmReviewCard />
|
||||
<GmWorldEditor />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function lootShelvedBoss(): boolean {
|
||||
return lootCurrentManastormBoss();
|
||||
}
|
||||
|
||||
export function releaseShelvedGameRuntime(): void {
|
||||
releaseActiveManastormStage();
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import type { PlayerSession, ShellPhase } from "./types";
|
||||
|
||||
export function ShelvedModeRoute(_props: {
|
||||
readonly phase: ShellPhase;
|
||||
readonly session: PlayerSession | null;
|
||||
}) {
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { lazy, Suspense, useEffect } from "react";
|
||||
import type { PlayerSession, ShellPhase } from "./types";
|
||||
import { useManastormAdminStore } from "../game/manastormAdminStore";
|
||||
|
||||
const ManastormSelectScreen = lazy(() => import("../ui/ManastormSelectScreen").then((module) => ({ default: module.ManastormSelectScreen })));
|
||||
const ArenaSelectScreen = lazy(() => import("../ui/ArenaSelectScreen").then((module) => ({ default: module.ArenaSelectScreen })));
|
||||
const ArenaRuntime = lazy(() => import("../ArenaRuntime"));
|
||||
const GmAdminScreen = lazy(() => import("../ui/GmAdminScreen").then((module) => ({ default: module.GmAdminScreen })));
|
||||
|
||||
function ShelvedTransition() {
|
||||
return <div className="world-transition"><span>HM</span><strong>Opening expedition</strong><small>Preparing the next screen...</small></div>;
|
||||
}
|
||||
|
||||
export function ShelvedModeRoute({
|
||||
phase,
|
||||
session,
|
||||
}: {
|
||||
readonly phase: ShellPhase;
|
||||
readonly session: PlayerSession | null;
|
||||
}) {
|
||||
const refreshManastormConfig = useManastormAdminStore((state) => state.refresh);
|
||||
|
||||
useEffect(() => {
|
||||
void refreshManastormConfig(session);
|
||||
}, [refreshManastormConfig, session]);
|
||||
|
||||
let screen = null;
|
||||
if (phase === "manastorms") screen = <ManastormSelectScreen />;
|
||||
else if (phase === "arenas") screen = <ArenaSelectScreen />;
|
||||
else if (phase === "arena-game") screen = <ArenaRuntime />;
|
||||
else if (phase === "gm-admin") screen = <GmAdminScreen />;
|
||||
return <Suspense fallback={<ShelvedTransition />}>{screen}</Suspense>;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export async function installShelvedOnlineGroupRuntime(): Promise<void> {}
|
||||
@@ -0,0 +1,4 @@
|
||||
export async function installShelvedOnlineGroupRuntime(): Promise<void> {
|
||||
const { installManastormShellRuntime } = await import("./manastormShellRuntime");
|
||||
installManastormShellRuntime();
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
isShelvedShellPhase,
|
||||
sessionCanUseShelvedGmTools,
|
||||
SHELVED_MODES_ENABLED,
|
||||
} from "./releaseFeatures";
|
||||
|
||||
describe("release feature boundary", () => {
|
||||
it("recognizes every legacy shelved phase", () => {
|
||||
expect(["manastorms", "arenas", "arena-game", "gm-admin"].every(isShelvedShellPhase)).toBe(true);
|
||||
expect(isShelvedShellPhase("main-menu")).toBe(false);
|
||||
expect(isShelvedShellPhase("game")).toBe(false);
|
||||
});
|
||||
|
||||
it("exposes GM tools only in a shelved build with an authenticated GM", () => {
|
||||
const session = {
|
||||
kind: "account",
|
||||
ownerId: "gm-account",
|
||||
displayName: "Phenom",
|
||||
accessToken: "token",
|
||||
roles: ["gm"],
|
||||
} as const;
|
||||
expect(sessionCanUseShelvedGmTools(session)).toBe(SHELVED_MODES_ENABLED);
|
||||
expect(sessionCanUseShelvedGmTools({ ...session, roles: [] })).toBe(false);
|
||||
expect(sessionCanUseShelvedGmTools({ ...session, accessToken: "" })).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
export const SHELVED_MODES_ENABLED = import.meta.env.VITE_SHELVED_MODES_ENABLED === "true";
|
||||
|
||||
export const SHELVED_SHELL_PHASES = Object.freeze([
|
||||
"manastorms",
|
||||
"arenas",
|
||||
"arena-game",
|
||||
"gm-admin",
|
||||
] as const);
|
||||
|
||||
export type ShelvedShellPhase = (typeof SHELVED_SHELL_PHASES)[number];
|
||||
|
||||
export function isShelvedShellPhase(value: string): value is ShelvedShellPhase {
|
||||
return (SHELVED_SHELL_PHASES as readonly string[]).includes(value);
|
||||
}
|
||||
|
||||
export function sessionCanUseShelvedGmTools(session: {
|
||||
readonly kind: string;
|
||||
readonly accessToken?: string;
|
||||
readonly roles?: readonly string[];
|
||||
} | null): boolean {
|
||||
return SHELVED_MODES_ENABLED
|
||||
&& Boolean(session?.kind === "account" && session.accessToken && session.roles?.includes("gm"));
|
||||
}
|
||||
@@ -2,6 +2,10 @@ import { describe, expect, it, vi } from "vitest";
|
||||
import { registerDungeonShellRuntime } from "./dungeonShellRuntimeBridge";
|
||||
import { registerManastormShellRuntime } from "./manastormShellRuntimeBridge";
|
||||
import { useShellStore } from "./shellStore";
|
||||
import { SHELVED_MODES_ENABLED } from "./releaseFeatures";
|
||||
|
||||
const normalReleaseIt = SHELVED_MODES_ENABLED ? it.skip : it;
|
||||
const shelvedModeIt = SHELVED_MODES_ENABLED ? it : it.skip;
|
||||
|
||||
describe("on-demand shell runtime delegation", () => {
|
||||
it("delegates dungeon entry after its phase runtime is installed", () => {
|
||||
@@ -12,7 +16,30 @@ describe("on-demand shell runtime delegation", () => {
|
||||
expect(enterDungeon).toHaveBeenCalledWith("wailing-caverns", "healer");
|
||||
});
|
||||
|
||||
it("delegates Manastorm operations after its phase runtime is installed", () => {
|
||||
normalReleaseIt("ignores registered Manastorm delegates in a normal release", () => {
|
||||
const enterManastorm = vi.fn(() => true);
|
||||
const saveSelectedLoadout = vi.fn(() => true);
|
||||
const abandonRun = vi.fn();
|
||||
const failRun = vi.fn();
|
||||
registerManastormShellRuntime({
|
||||
enterManastorm,
|
||||
saveSelectedLoadout,
|
||||
abandonRun,
|
||||
failRun,
|
||||
leaveActiveGame: vi.fn(),
|
||||
});
|
||||
|
||||
expect(useShellStore.getState().enterManastorm("healer", 3, 6)).toBe(false);
|
||||
expect(useShellStore.getState().saveSelectedManastormLoadout(["spell-1"])).toBe(false);
|
||||
useShellStore.getState().abandonManastormRun();
|
||||
useShellStore.getState().failActiveManastorm();
|
||||
expect(enterManastorm).not.toHaveBeenCalled();
|
||||
expect(saveSelectedLoadout).not.toHaveBeenCalled();
|
||||
expect(abandonRun).not.toHaveBeenCalled();
|
||||
expect(failRun).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
shelvedModeIt("delegates Manastorm operations after its phase runtime is installed", () => {
|
||||
const enterManastorm = vi.fn(() => true);
|
||||
const saveSelectedLoadout = vi.fn(() => true);
|
||||
const abandonRun = vi.fn();
|
||||
@@ -37,4 +64,3 @@ describe("on-demand shell runtime delegation", () => {
|
||||
expect(failRun).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ import { createDefaultAppearance } from "./characterCatalog";
|
||||
import { useShellStore } from "./shellStore";
|
||||
import { installDungeonShellRuntime } from "./dungeonShellRuntime";
|
||||
import { installManastormShellRuntime } from "./manastormShellRuntime";
|
||||
import { SHELVED_MODES_ENABLED } from "./releaseFeatures";
|
||||
import { DUNGEON_MANIFEST } from "../game/dungeonManifest";
|
||||
import { useGameStore } from "../game/store";
|
||||
import { useManastormStore } from "../game/manastormStore";
|
||||
@@ -23,6 +24,8 @@ import { useManastormAdminStore } from "../game/manastormAdminStore";
|
||||
|
||||
installDungeonShellRuntime();
|
||||
installManastormShellRuntime();
|
||||
const normalReleaseIt = SHELVED_MODES_ENABLED ? it.skip : it;
|
||||
const shelvedModeIt = SHELVED_MODES_ENABLED ? it : it.skip;
|
||||
|
||||
const character = {
|
||||
id: "character-1",
|
||||
@@ -254,7 +257,16 @@ describe("shell store", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("opens Manastorms, validates the role, and starts a fresh level-one run", () => {
|
||||
normalReleaseIt("keeps legacy shelved commands out of the normal shell", () => {
|
||||
prepareSelectedCharacter();
|
||||
expect(useShellStore.getState().openManastorms()).toBe(false);
|
||||
expect(useShellStore.getState().openArenas()).toBe(false);
|
||||
expect(useShellStore.getState().enterManastorm("healer")).toBe(false);
|
||||
expect(useShellStore.getState().phase).toBe("characters");
|
||||
expect(useShellStore.getState().notice).toMatch(/temporarily unavailable/i);
|
||||
});
|
||||
|
||||
shelvedModeIt("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");
|
||||
@@ -278,7 +290,7 @@ describe("shell store", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("starts each party size from only its own unlocked checkpoint", () => {
|
||||
shelvedModeIt("starts each party size from only its own unlocked checkpoint", () => {
|
||||
const progress = recordManastormCheckpoint(createEmptyManastormProgress(), 3, 5, 6);
|
||||
useShellStore.setState({
|
||||
phase: "manastorms",
|
||||
@@ -302,7 +314,7 @@ describe("shell store", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("selects end-game mode at the character cap and hands off an eligible checkpoint", () => {
|
||||
shelvedModeIt("selects end-game mode at the character cap and hands off an eligible checkpoint", () => {
|
||||
const progress = recordManastormCheckpoint(
|
||||
createEmptyManastormProgress(),
|
||||
3,
|
||||
@@ -333,7 +345,7 @@ describe("shell store", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("passes a persisted client-grounded spell selection into the run runtime", () => {
|
||||
shelvedModeIt("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(
|
||||
|
||||
+25
-5
@@ -56,7 +56,7 @@ import {
|
||||
logoutOnlineSession,
|
||||
refreshOnlineSession,
|
||||
} from "./onlineAccountClient";
|
||||
import { isGameMasterSession } from "../game/manastormAdminConfig";
|
||||
import { sessionCanUseShelvedGmTools, SHELVED_MODES_ENABLED } from "./releaseFeatures";
|
||||
import { useGameStore } from "../game/store";
|
||||
import {
|
||||
normalizeSpecializationId,
|
||||
@@ -324,6 +324,10 @@ export const useShellStore = create<ShellState>((set, get) => ({
|
||||
return true;
|
||||
},
|
||||
openManastorms: () => {
|
||||
if (!SHELVED_MODES_ENABLED) {
|
||||
set({ notice: "Manastorms are temporarily unavailable." });
|
||||
return false;
|
||||
}
|
||||
const { session, selectedCharacterId, characters } = get();
|
||||
if (!session || !selectedCharacterId || !characters.some((character) => character.id === selectedCharacterId)) {
|
||||
set({ phase: session ? "characters" : "login", notice: "Select or create a character first." });
|
||||
@@ -338,6 +342,10 @@ export const useShellStore = create<ShellState>((set, get) => ({
|
||||
return true;
|
||||
},
|
||||
openArenas: () => {
|
||||
if (!SHELVED_MODES_ENABLED) {
|
||||
set({ notice: "Arenas are temporarily unavailable." });
|
||||
return false;
|
||||
}
|
||||
const { session, selectedCharacterId, characters } = get();
|
||||
if (!session || !selectedCharacterId || !characters.some((character) => character.id === selectedCharacterId)) {
|
||||
set({ phase: session ? "characters" : "login", notice: "Select or create a character first." });
|
||||
@@ -348,7 +356,7 @@ export const useShellStore = create<ShellState>((set, get) => ({
|
||||
},
|
||||
openGmAdmin: () => {
|
||||
const { session, selectedCharacterId, characters } = get();
|
||||
if (!isGameMasterSession(session)) {
|
||||
if (!sessionCanUseShelvedGmTools(session)) {
|
||||
set({ notice: "Game Master access is required." });
|
||||
return false;
|
||||
}
|
||||
@@ -429,7 +437,7 @@ export const useShellStore = create<ShellState>((set, get) => ({
|
||||
},
|
||||
returnToMainMenu: () => {
|
||||
useGameStore.setState({ activeGmReviewTarget: null, gmTestMode: false });
|
||||
manastormShellRuntime()?.leaveActiveGame();
|
||||
if (SHELVED_MODES_ENABLED) manastormShellRuntime()?.leaveActiveGame();
|
||||
const { session, characters, activeCharacter, selectedCharacterId } = get();
|
||||
if (!session) {
|
||||
set({ phase: "login", activeCharacter: null });
|
||||
@@ -449,13 +457,21 @@ export const useShellStore = create<ShellState>((set, get) => ({
|
||||
return false;
|
||||
},
|
||||
enterManastorm: (requestedRole, requestedPartySize = 5, requestedStartingLevel) => {
|
||||
if (!SHELVED_MODES_ENABLED) {
|
||||
set({ notice: "Manastorms are temporarily unavailable." });
|
||||
return false;
|
||||
}
|
||||
const runtime = manastormShellRuntime();
|
||||
if (runtime) return runtime.enterManastorm(requestedRole, requestedPartySize, requestedStartingLevel);
|
||||
set({ notice: "Manastorm data is still loading. Try again in a moment." });
|
||||
return false;
|
||||
},
|
||||
abandonManastormRun: () => manastormShellRuntime()?.abandonRun(),
|
||||
failActiveManastorm: () => manastormShellRuntime()?.failRun(),
|
||||
abandonManastormRun: () => {
|
||||
if (SHELVED_MODES_ENABLED) manastormShellRuntime()?.abandonRun();
|
||||
},
|
||||
failActiveManastorm: () => {
|
||||
if (SHELVED_MODES_ENABLED) manastormShellRuntime()?.failRun();
|
||||
},
|
||||
enterWorld: () => get().enterDungeon(DEFAULT_DUNGEON_ID),
|
||||
returnToCharacters: () => {
|
||||
useGameStore.setState({ activeGmReviewTarget: null, gmTestMode: false });
|
||||
@@ -594,6 +610,10 @@ export const useShellStore = create<ShellState>((set, get) => ({
|
||||
}));
|
||||
},
|
||||
saveSelectedManastormLoadout: (loadout) => {
|
||||
if (!SHELVED_MODES_ENABLED) {
|
||||
set({ notice: "Manastorms are temporarily unavailable." });
|
||||
return false;
|
||||
}
|
||||
const runtime = manastormShellRuntime();
|
||||
if (runtime) return runtime.saveSelectedLoadout(loadout);
|
||||
set({ notice: "Manastorm data is still loading. Try again in a moment." });
|
||||
|
||||
Reference in new issue
Block a user