Release Healer Man 0.1.32

This commit is contained in:
phenom committed 2026-09-12 11:07:25 -04:00
1 parent d3d6aaebdb
commit 6dcb1034e5
102 files changed
+10772 -788

No files matched your search

+10 -13
View File
@@ -1,11 +1,12 @@
import { lazy, Suspense, useEffect, useMemo, useState, type ReactNode } from "react";
import { BASE_CLASS_IDS, classById } from "./app/characterCatalog";
import { useShellStore } from "./app/shellStore";
import { isShelvedShellPhase, SHELVED_MODES_ENABLED } from "./app/releaseFeatures";
import { preloadAbilityCatalogsForClasses } from "./game/abilityCatalog";
import { useManastormAdminStore } from "./game/manastormAdminStore";
import { useAuthoritativeDualScreenSync, useForcedThorDisplays } from "./platform/useThorDualScreen";
import { LoginScreen } from "./ui/LoginScreen";
import { OnlineGroupCoordinator } from "./app/OnlineGroupCoordinator";
import { ShelvedModeRoute } from "@shelvedModeRoutes";
import { preloadTalentGameplayResources } from "./ui/talentGameplayResources";
import {
getLoadedGameRuntime,
@@ -19,11 +20,6 @@ const CharacterSelectScreen = lazy(() => import("./ui/CharacterSelectScreen").th
const ClassHallScreen = lazy(() => import("./ui/ClassHallScreen").then((module) => ({ default: module.ClassHallScreen })));
const DungeonSelectScreen = lazy(() => import("./ui/DungeonSelectScreen").then((module) => ({ default: module.DungeonSelectScreen })));
const MainMenuScreen = lazy(() => import("./ui/MainMenuScreen").then((module) => ({ default: module.MainMenuScreen })));
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 ShellTransition() {
return <div className="world-transition"><span>HM</span><strong>Opening expedition</strong><small>Preparing the next screen...</small></div>;
}
@@ -74,12 +70,14 @@ export default function App() {
useAuthoritativeDualScreenSync();
const phase = useShellStore((state) => state.phase);
const session = useShellStore((state) => state.session);
const refreshManastormConfig = useManastormAdminStore((state) => state.refresh);
const PreloadedGameRuntime = getLoadedGameRuntime();
useEffect(() => {
void refreshManastormConfig(session);
}, [refreshManastormConfig, session]);
if (!SHELVED_MODES_ENABLED && isShelvedShellPhase(phase)) {
useShellStore.getState().returnToMainMenu();
useShellStore.getState().setNotice("That game mode is temporarily unavailable.");
}
}, [phase]);
let screen = null;
if (phase === "login") screen = <LoginScreen />;
@@ -88,10 +86,9 @@ export default function App() {
else if (phase === "main-menu") screen = <Suspense fallback={<ShellTransition />}><MainMenuScreen /></Suspense>;
else if (phase === "class-hall") screen = <Suspense fallback={<ShellTransition />}><ClassHallScreen /></Suspense>;
else if (phase === "dungeons") screen = <Suspense fallback={<ShellTransition />}><DungeonSelectScreen /></Suspense>;
else if (phase === "manastorms") screen = <Suspense fallback={<ShellTransition />}><ManastormSelectScreen /></Suspense>;
else if (phase === "arenas") screen = <Suspense fallback={<ShellTransition />}><ArenaSelectScreen /></Suspense>;
else if (phase === "arena-game") screen = <Suspense fallback={<ShellTransition />}><ArenaRuntime /></Suspense>;
else if (phase === "gm-admin") screen = <Suspense fallback={<ShellTransition />}><GmAdminScreen /></Suspense>;
else if (isShelvedShellPhase(phase)) {
screen = <Suspense fallback={<ShellTransition />}><ShelvedModeRoute phase={phase} session={session} /></Suspense>;
}
else if (phase === "game") screen = PreloadedGameRuntime
? <PreloadedGameRuntime />
: <Suspense fallback={<ShellTransition />}><GameRuntime /></Suspense>;
+4 -10
View File
@@ -8,9 +8,6 @@ import { releaseDungeonGltfCache } from "./game/dungeonAssetCache";
import { clearDungeonRuntimeCaches } from "./game/dungeonRuntimeReset";
import { requireDungeonDefinition } from "./game/dungeonRegistry";
import { PartyBridge } from "./game/PartyBridge";
import { ManastormBridge } from "./game/ManastormBridge";
import { releaseActiveManastormStage } from "./game/manastormStageLoader";
import { lootCurrentManastormBoss } from "./game/manastormLoot";
import { usePartyStore } from "./game/partyStore";
import { useGameStore } from "./game/store";
import { GameScene } from "./scene/GameScene";
@@ -20,8 +17,6 @@ import { GameplayPanels } from "./ui/GameplayPanels";
import { MapPanel } from "./ui/MapPanel";
import { PauseMenu } from "./ui/PauseMenu";
import { SceneErrorBoundary } from "./ui/SceneErrorBoundary";
import { GmWorldEditor } from "./ui/GmWorldEditor";
import { GmReviewCard } from "./ui/GmReviewCard";
import { OnlineSessionBridge } from "./game/OnlineSessionBridge";
import { DungeonRunCheckpointBridge } from "./game/DungeonRunCheckpointBridge";
import { DungeonEncounterEffectBridge } from "./game/DungeonEncounterEffectBridge";
@@ -33,6 +28,7 @@ import { activeUiLayout, currentUiDevice } from "./game/uiLayoutStore";
import { useUiLayoutStore } from "./game/uiLayoutStore";
import { profileInputBindings } from "./game/keybindPreferences";
import { KeybindsPanel } from "./ui/KeybindsPanel";
import { lootShelvedBoss, releaseShelvedGameRuntime, ShelvedGameBridges } from "@shelvedGameRuntime";
function InputBridge() {
const overlay = useGameStore((state) => state.overlay);
@@ -57,7 +53,7 @@ function InputBridge() {
toggleInventory: () => useGameStore.getState().toggleOverlay("inventory"),
toggleSpellbook: () => useGameStore.getState().toggleOverlay("spellbook"),
toggleTalents: () => useGameStore.getState().toggleOverlay("talents"),
lootActiveBoss: () => interactWithActiveDungeonObject() || lootCurrentManastormBoss(),
lootActiveBoss: () => interactWithActiveDungeonObject() || lootShelvedBoss(),
castActionBinding: (layer, control) => {
const game = useGameStore.getState();
useCombatStore.getState().castActionBinding(layer, control, game.playerPosition, Date.now());
@@ -102,7 +98,7 @@ export default function GameRuntime() {
const activeDungeon = requireDungeonDefinition(useGameStore.getState().activeDungeonId);
clearDungeonRuntimeCaches();
releaseDungeonGltfCache(activeDungeon);
releaseActiveManastormStage();
releaseShelvedGameRuntime();
}, []);
return (
<DualDisplayFrame
@@ -117,7 +113,7 @@ export default function GameRuntime() {
<DungeonRunCheckpointBridge />
<CombatBridge />
<PartyBridge />
<ManastormBridge />
<ShelvedGameBridges />
<SceneErrorBoundary><GameScene /></SceneErrorBoundary>
<Hud />
<EditableHudElement id="interaction"><DungeonInteractionPrompt /></EditableHudElement>
@@ -126,8 +122,6 @@ export default function GameRuntime() {
<PauseMenu />
<UiLayoutEditor />
<KeybindsPanel />
<GmReviewCard />
<GmWorldEditor />
</main>
}
bottom={<LocalCompanionPanel />}
+44
View File
@@ -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);
});
});
+8 -4
View File
@@ -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);
+9
View File
@@ -0,0 +1,9 @@
export function ShelvedGameBridges() {
return null;
}
export function lootShelvedBoss(): boolean {
return false;
}
export function releaseShelvedGameRuntime(): void {}
+23
View File
@@ -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();
}
+8
View File
@@ -0,0 +1,8 @@
import type { PlayerSession, ShellPhase } from "./types";
export function ShelvedModeRoute(_props: {
readonly phase: ShellPhase;
readonly session: PlayerSession | null;
}) {
return null;
}
+33
View File
@@ -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();
}
+27
View File
@@ -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);
});
});
+23
View File
@@ -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"));
}
+28 -2
View File
@@ -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();
});
});
+16 -4
View File
@@ -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
View File
@@ -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." });
@@ -75,14 +75,24 @@
"detail": "green"
},
{
"name": "population accounting",
"name": "scripted population accounting",
"passed": true,
"detail": "249 = 56 + 193"
"detail": "44 = 44"
},
{
"name": "source population accounting",
"passed": true,
"detail": "249 = 40 + 209"
},
{
"name": "boss population accounting",
"passed": true,
"detail": "5 = 5"
},
{
"name": "combat nav projection",
"passed": true,
"detail": "3.385892"
"detail": "0.998158"
},
{
"name": "mechanics is an explicit review gate",
@@ -104,6 +114,51 @@
"passed": true,
"detail": "grafu-castle"
},
{
"name": "actor atmo exists",
"passed": true,
"detail": "public/assets/creatures/grafu-castle/atmo.glb"
},
{
"name": "actor atmo checksum",
"passed": true,
"detail": "9f7c1ad1c56ef51ee0511507f950ef97224ad86af561ded111cf1d5740c69999"
},
{
"name": "actor atmo size",
"passed": true,
"detail": "1981260"
},
{
"name": "actor barbo-brother exists",
"passed": true,
"detail": "public/assets/creatures/grafu-castle/barbo-brother.glb"
},
{
"name": "actor barbo-brother checksum",
"passed": true,
"detail": "af1cd20b7cc1d465c270860997f1245b2967005b1e5308b4a43401e983f883a4"
},
{
"name": "actor barbo-brother size",
"passed": true,
"detail": "2226000"
},
{
"name": "actor black-hat exists",
"passed": true,
"detail": "public/assets/creatures/grafu-castle/black-hat.glb"
},
{
"name": "actor black-hat checksum",
"passed": true,
"detail": "e3d5265c794b82d45c34bd7875e33c9f00680dc67c3cc18213f5df657f9ab76a"
},
{
"name": "actor black-hat size",
"passed": true,
"detail": "1965712"
},
{
"name": "actor black-hat-blue exists",
"passed": true,
@@ -112,12 +167,12 @@
{
"name": "actor black-hat-blue checksum",
"passed": true,
"detail": "bcd344260000235789ddb2452a285210000646659538e56e57857d9667969edd"
"detail": "e6b78de784549ce814f998c34b8e1aef42e452e26c7395ac74fa5db168413d2f"
},
{
"name": "actor black-hat-blue size",
"passed": true,
"detail": "1561332"
"detail": "1560524"
},
{
"name": "actor bunny exists",
@@ -127,12 +182,12 @@
{
"name": "actor bunny checksum",
"passed": true,
"detail": "6d1ed1424fc075b472224f6bf2cd7c1ea291d7605ad280ce8ae0570d060f4b3e"
"detail": "e29298726425e0109890fb0b33d770f39e051b7a43d50a666f6f6c46a3f2ee32"
},
{
"name": "actor bunny size",
"passed": true,
"detail": "540500"
"detail": "540408"
},
{
"name": "actor cereus-01 exists",
@@ -142,12 +197,12 @@
{
"name": "actor cereus-01 checksum",
"passed": true,
"detail": "324a9b2c338d08258979bbbbdad98e82bcb77f99907b3abbdfc817f4a9aa6610"
"detail": "a35ec70378f7323a1f3d50c3e84d5654eacd2888722944c83f9be18cf42eecfa"
},
{
"name": "actor cereus-01 size",
"passed": true,
"detail": "620616"
"detail": "620404"
},
{
"name": "actor controlled-steel-guardian exists",
@@ -157,12 +212,27 @@
{
"name": "actor controlled-steel-guardian checksum",
"passed": true,
"detail": "5d0e0f5d6b38c1cf799a30a3ab0718a405feaa84ae844375a15cff6161fa8b77"
"detail": "b772b72bffc44c4fcf36ff8aeaf8f2d0c435f220e5c9729ed7e1ce2aac70e8be"
},
{
"name": "actor controlled-steel-guardian size",
"passed": true,
"detail": "1231536"
"detail": "1232036"
},
{
"name": "actor cursed-steel-guardian exists",
"passed": true,
"detail": "public/assets/creatures/grafu-castle/cursed-steel-guardian.glb"
},
{
"name": "actor cursed-steel-guardian checksum",
"passed": true,
"detail": "3ec7660d4e1b475ec9ae290c304802dd4ba174048fac0781ae25474a718227fe"
},
{
"name": "actor cursed-steel-guardian size",
"passed": true,
"detail": "1151612"
},
{
"name": "actor elder-myconid exists",
@@ -172,12 +242,12 @@
{
"name": "actor elder-myconid checksum",
"passed": true,
"detail": "5e5097cf12bac9780fd9925aae0d6507ad9393ac2e6fd106b2ca9cdf03b2ecc1"
"detail": "ef0d1fd12aa130a1a23a21bb978f7859b434d27512fb5da12426f1c1ce34514c"
},
{
"name": "actor elder-myconid size",
"passed": true,
"detail": "1395280"
"detail": "1393472"
},
{
"name": "actor ghastly-clown-red exists",
@@ -187,12 +257,12 @@
{
"name": "actor ghastly-clown-red checksum",
"passed": true,
"detail": "20a6fa59f783c2f17260db9556689906079f6d0d95b835ec26d3e2a7a589f322"
"detail": "7f767b8fc224b63f923af6e7f29eda935a746ef6040bc489e4957c2650993ccc"
},
{
"name": "actor ghastly-clown-red size",
"passed": true,
"detail": "825156"
"detail": "824792"
},
{
"name": "actor grinning-clown exists",
@@ -202,12 +272,57 @@
{
"name": "actor grinning-clown checksum",
"passed": true,
"detail": "b53b78efe39b807bc8bbf7952501970cf74ee1483b891b6b61b0880e30b197b2"
"detail": "b93ffdcfcf86df01c348678c97dfb913e74da9cab2367e187a46fafb3d85040d"
},
{
"name": "actor grinning-clown size",
"passed": true,
"detail": "1374852"
"detail": "1374556"
},
{
"name": "actor hm-male-image-585704 exists",
"passed": true,
"detail": "public/assets/creatures/grafu-castle/hm-male-image-585704.glb"
},
{
"name": "actor hm-male-image-585704 checksum",
"passed": true,
"detail": "7659a1aa2227f98f2ab68d6ffa8b608fda51b9fa72bd987242392e2aca8e5291"
},
{
"name": "actor hm-male-image-585704 size",
"passed": true,
"detail": "9421384"
},
{
"name": "actor hm-male-image-585705 exists",
"passed": true,
"detail": "public/assets/creatures/grafu-castle/hm-male-image-585705.glb"
},
{
"name": "actor hm-male-image-585705 checksum",
"passed": true,
"detail": "2c43c2ae657b4e4347ff9e319080e8d9c44ccaf6c7cc15251230e02abe63c2cd"
},
{
"name": "actor hm-male-image-585705 size",
"passed": true,
"detail": "9569888"
},
{
"name": "actor karth exists",
"passed": true,
"detail": "public/assets/creatures/grafu-castle/karth.glb"
},
{
"name": "actor karth checksum",
"passed": true,
"detail": "ac96b24461b39cdfae7810097dbe6698f0d7ce1afe235df04d72144dceef893d"
},
{
"name": "actor karth size",
"passed": true,
"detail": "1993572"
},
{
"name": "actor karth-01 exists",
@@ -217,12 +332,12 @@
{
"name": "actor karth-01 checksum",
"passed": true,
"detail": "867b8c610f1cbb4bfc52ded7d8010abcb3585592ac501b1af0792a5721ba4d3c"
"detail": "ee1c1e6937338dff99dad4a8863a03ad7d864a55120271e996a44d6cb83f07ae"
},
{
"name": "actor karth-01 size",
"passed": true,
"detail": "1777388"
"detail": "1776716"
},
{
"name": "actor milk-scarab-blue exists",
@@ -232,12 +347,72 @@
{
"name": "actor milk-scarab-blue checksum",
"passed": true,
"detail": "2392d45c63b193f2266e7c0c6e9dcc192fd860caac354395bf02a634f046dae5"
"detail": "dde6938b52a45a77f07da63ca26172c66c49f3c10ceb10f6bda17aa3d0fba57d"
},
{
"name": "actor milk-scarab-blue size",
"passed": true,
"detail": "879492"
"detail": "879476"
},
{
"name": "actor mount-millenium-chimera-laby exists",
"passed": true,
"detail": "public/assets/creatures/grafu-castle/mount-millenium-chimera-laby.glb"
},
{
"name": "actor mount-millenium-chimera-laby checksum",
"passed": true,
"detail": "7cb4ae344d3a46094b96faebdc6274eace04eb894de64ee87ac7bb7a1309b321"
},
{
"name": "actor mount-millenium-chimera-laby size",
"passed": true,
"detail": "1217348"
},
{
"name": "actor enraged-big-bear-purple exists",
"passed": true,
"detail": "public/assets/creatures/grafu-castle/enraged-big-bear-purple.glb"
},
{
"name": "actor enraged-big-bear-purple checksum",
"passed": true,
"detail": "36738dc204f6d574f56ee41044d61ae88a885d0dc4b411f7ea70d76e84d48a4b"
},
{
"name": "actor enraged-big-bear-purple size",
"passed": true,
"detail": "935312"
},
{
"name": "actor enraged-big-bear-cream exists",
"passed": true,
"detail": "public/assets/creatures/grafu-castle/enraged-big-bear-cream.glb"
},
{
"name": "actor enraged-big-bear-cream checksum",
"passed": true,
"detail": "1e327d246fdcebf9b35891f37ccc3aed3ff7bfb4f2c7355c1381d7e0231e956c"
},
{
"name": "actor enraged-big-bear-cream size",
"passed": true,
"detail": "941160"
},
{
"name": "actor rafael-bakderr exists",
"passed": true,
"detail": "public/assets/creatures/grafu-castle/rafael-bakderr.glb"
},
{
"name": "actor rafael-bakderr checksum",
"passed": true,
"detail": "06e87d84a7cb3e15d610b0f18528cb1331c80db4597c67c49267aa4a627386dc"
},
{
"name": "actor rafael-bakderr size",
"passed": true,
"detail": "1705044"
},
{
"name": "Khronos validation",
@@ -11,8 +11,120 @@
"dungeonConfigId": "grafu_castle"
},
"configuredBossTemplateIds": [],
"reviewedBossTemplateIds": [
105204,
105224,
105351,
105542,
105543,
105616
],
"configuredBossPositions": {},
"bossRows": [],
"scriptedBossSpawns": [
{
"scriptedPlacementId": "cayus",
"sourceAnchorSpawnId": 41927224,
"controllerSpawnId": 41928351,
"templateId": 105616,
"name": "Cayus",
"sourcePosition": [
-348.7113952636719,
1254.5040283203125,
-999.6005249023438
],
"sourceScriptRoot": "Dy_Z19_BOSSN_AIC",
"objective": true,
"evidence": [
"Resource/luascript/02345.lua:CreateObjByFlag(105616,780696,1,1)"
]
},
{
"scriptedPlacementId": "ardmond",
"sourceAnchorSpawnId": 41926939,
"controllerSpawnId": 41928345,
"templateId": 105542,
"name": "Ardmond",
"sourcePosition": [
-1427.9969482421875,
1133.35498046875,
-1353.3699951171875
],
"sourceScriptRoot": "kg_105542_01",
"objective": true,
"evidence": [
"Resource/luascript/02324.lua:kg_105542_CreateBoss"
]
},
{
"scriptedPlacementId": "issac",
"sourceAnchorSpawnId": 41926939,
"controllerSpawnId": 41928345,
"templateId": 105543,
"name": "Issac",
"sourcePosition": [
-1427.9969482421875,
1133.35498046875,
-1313.3699951171875
],
"sourceScriptRoot": "kg_105542_01",
"objective": false,
"evidence": [
"Resource/luascript/02324.lua:kg_105542_CreateBoss"
]
},
{
"scriptedPlacementId": "boro-boro-brothers",
"sourceAnchorSpawnId": 41926924,
"controllerSpawnId": 41927347,
"templateId": 105224,
"name": "Boro-Boro Brothers",
"sourcePosition": [
-2172.174072265625,
1095.10400390625,
-22.212709426879883
],
"sourceScriptRoot": "ic_105224_01",
"objective": true,
"evidence": [
"Resource/luascript/02231.lua:CreateObjByFlag(105224,780775,1,1)"
]
},
{
"scriptedPlacementId": "conjurer-chapeaunoir",
"sourceAnchorSpawnId": 41926979,
"controllerSpawnId": 41927358,
"templateId": 105204,
"name": "Conjurer Chapeaunoir",
"sourcePosition": [
-1732.9649658203125,
914.618896484375,
1190.8310546875
],
"sourceScriptRoot": "cl_136_105205_N",
"objective": true,
"evidence": [
"Resource/luascript/02330.lua:CreateObjByFlag(105204,780767,0,1)"
]
},
{
"scriptedPlacementId": "larvanger-barkud",
"sourceAnchorSpawnId": 41927071,
"controllerSpawnId": 41927369,
"templateId": 105351,
"name": "Larvanger Barkud",
"sourcePosition": [
-305.8063049316406,
902.5551147460938,
941.4002075195312
],
"sourceScriptRoot": "un_zone19_boss5_allconreal_1_01",
"objective": true,
"evidence": [
"Resource/luascript/02297.lua:CreateObjByFlag(105351,780838,1,1)"
]
}
],
"luaSources": [],
"luaSignals": [],
"templateCandidates": [
@@ -94,6 +206,18 @@
"templateScripts": [],
"spells": []
},
{
"templateId": 105204,
"name": "Conjurer Chapeaunoir",
"classification": "boss",
"spawnCount": 0,
"autoPlots": [
"cl_136_105205_N"
],
"plotClassNames": [],
"templateScripts": [],
"spells": []
},
{
"templateId": 105205,
"name": "Template 105205",
@@ -109,7 +233,7 @@
{
"templateId": 105210,
"name": "Template 105210",
"classification": "combat",
"classification": "deferred-object",
"spawnCount": 1,
"autoPlots": [
"kg_105542_01"
@@ -204,10 +328,82 @@
"templateScripts": [],
"spells": []
},
{
"templateId": 105224,
"name": "Boro-Boro Brothers",
"classification": "boss",
"spawnCount": 0,
"autoPlots": [
"ic_105224_01"
],
"plotClassNames": [],
"templateScripts": [],
"spells": []
},
{
"templateId": 105229,
"name": "Smirking Castle Clown",
"classification": "combat",
"spawnCount": 0,
"autoPlots": [],
"plotClassNames": [],
"templateScripts": [],
"spells": []
},
{
"templateId": 105230,
"name": "Odd Castle Clown",
"classification": "combat",
"spawnCount": 0,
"autoPlots": [],
"plotClassNames": [],
"templateScripts": [],
"spells": []
},
{
"templateId": 105233,
"name": "Cursed Steel Soldier",
"classification": "combat",
"spawnCount": 0,
"autoPlots": [],
"plotClassNames": [],
"templateScripts": [],
"spells": []
},
{
"templateId": 105235,
"name": "Conjurer's Apprentice",
"classification": "combat",
"spawnCount": 0,
"autoPlots": [],
"plotClassNames": [],
"templateScripts": [],
"spells": []
},
{
"templateId": 105236,
"name": "Giant Enraged Bear",
"classification": "combat",
"spawnCount": 0,
"autoPlots": [],
"plotClassNames": [],
"templateScripts": [],
"spells": []
},
{
"templateId": 105237,
"name": "Manipulated Bear",
"classification": "combat",
"spawnCount": 0,
"autoPlots": [],
"plotClassNames": [],
"templateScripts": [],
"spells": []
},
{
"templateId": 105254,
"name": "Template 105254",
"classification": "combat",
"classification": "deferred-object",
"spawnCount": 10,
"autoPlots": [
"cl_136_01_01_N",
@@ -225,7 +421,7 @@
{
"templateId": 105258,
"name": "Template 105258",
"classification": "combat",
"classification": "deferred-object",
"spawnCount": 5,
"autoPlots": [
"cl_105258_1",
@@ -265,6 +461,42 @@
"templateScripts": [],
"spells": []
},
{
"templateId": 105351,
"name": "Larvanger Barkud",
"classification": "boss",
"spawnCount": 0,
"autoPlots": [
"un_zone19_boss5_allconreal_1_01"
],
"plotClassNames": [],
"templateScripts": [],
"spells": []
},
{
"templateId": 105542,
"name": "Ardmond",
"classification": "boss",
"spawnCount": 0,
"autoPlots": [
"kg_105542_01"
],
"plotClassNames": [],
"templateScripts": [],
"spells": []
},
{
"templateId": 105543,
"name": "Issac",
"classification": "boss",
"spawnCount": 0,
"autoPlots": [
"kg_105542_01"
],
"plotClassNames": [],
"templateScripts": [],
"spells": []
},
{
"templateId": 105596,
"name": "Smirking Castle Clown",
@@ -332,6 +564,16 @@
}
]
},
{
"templateId": 105599,
"name": "Cursed Steel Soldier",
"classification": "combat",
"spawnCount": 0,
"autoPlots": [],
"plotClassNames": [],
"templateScripts": [],
"spells": []
},
{
"templateId": 105600,
"name": "Juggler's Apprentice",
@@ -420,6 +662,18 @@
}
]
},
{
"templateId": 105616,
"name": "Cayus",
"classification": "boss",
"spawnCount": 0,
"autoPlots": [
"Dy_Z19_BOSSN_AIC"
],
"plotClassNames": [],
"templateScripts": [],
"spells": []
},
{
"templateId": 116434,
"name": "Skeleton of an Adventurer",
File diff suppressed because it is too large. Load diff
+42 -13
View File
@@ -2,7 +2,6 @@ import { readFileSync } from "node:fs";
import { gzipSync } from "node:zlib";
import { runInNewContext } from "node:vm";
import { describe, expect, it, vi } from "vitest";
import { ARENA_STAGES } from "../game/arenaStages";
import { BROWSER_CONTENT_CACHE_NAME } from "./contentManager";
const workerSource = readFileSync(
@@ -12,15 +11,22 @@ const workerSource = readFileSync(
function contentWorker(cached?: Response, bundledFiles: Record<string, unknown> = {}, object?: Response) {
const handlers = new Map<string, (event: unknown) => void>();
const bootstrapObjects = new Map<string, Response>();
const fetch = vi.fn(async (url: RequestInfo | URL) => String(url) === "/content/bootstrap-manifest.json"
? Response.json({ files: bundledFiles }) : object?.clone() ?? new Response("bundled arena", { status: 200 }));
? Response.json({ files: bundledFiles }) : object?.clone() ?? new Response("bundled dungeon", { status: 200 }));
const match = vi.fn(async (url: RequestInfo | URL) => String(url).includes("/content/cache-objects/") ? object?.clone() : cached?.clone());
const bootstrapMatch = vi.fn(async (url: RequestInfo | URL) => bootstrapObjects.get(String(url))?.clone());
const bootstrapPut = vi.fn(async (url: RequestInfo | URL, response: Response) => {
bootstrapObjects.set(String(url), response.clone());
});
runInNewContext(workerSource, {
self: {
location: { origin: "https://healer.test" },
addEventListener: (type: string, handler: (event: unknown) => void) => handlers.set(type, handler),
},
caches: { open: async () => ({ match }) },
caches: { open: async (name: string) => name === "healer-man-bootstrap-v1"
? { match: bootstrapMatch, put: bootstrapPut }
: { match } },
fetch,
URL,
Response,
@@ -30,6 +36,8 @@ function contentWorker(cached?: Response, bundledFiles: Record<string, unknown>
return {
fetch,
match,
bootstrapMatch,
bootstrapPut,
refresh: () => handlers.get("message")!({ data: { type: "refresh-content-bootstrap" } }),
request: (path: string) => {
let response: Promise<Response> | undefined;
@@ -43,20 +51,28 @@ function contentWorker(cached?: Response, bundledFiles: Record<string, unknown>
}
describe("browser content cache worker", () => {
it.each(ARENA_STAGES)("loads bundled $name assets without an optional content install", async (stage) => {
const worker = contentWorker();
for (const asset of stage.assets) {
const response = await worker.request(`${asset.url}?arena-runtime=1`);
it("loads starter-dungeon assets without an optional content install", async () => {
const paths = [
"/assets/game/dungeons/wailing-caverns/visual.glb",
"/assets/creatures/wailing-caverns/raptor.glb",
];
const files = Object.fromEntries(paths.map((path, index) => [
path,
{ url: `/content/bundled/${String(index + 1).repeat(64)}.glb` },
]));
const worker = contentWorker(undefined, files);
for (const path of paths) {
const response = await worker.request(`${path}?starter-runtime=1`);
expect(response?.status).toBe(200);
expect(await response?.text()).toBe("bundled arena");
expect(await response?.text()).toBe("bundled dungeon");
}
expect(worker.fetch).toHaveBeenCalledTimes(stage.assets.length + 1);
expect(worker.fetch).toHaveBeenCalledTimes(paths.length + 1);
});
it("prefers installed arena updates over bundled files, ignoring the runtime query", async () => {
const worker = contentWorker(new Response("updated arena"));
const response = await worker.request(`${ARENA_STAGES[0].assets[0].url}?arena-runtime=1`);
expect(await response?.text()).toBe("updated arena");
it("prefers installed dungeon updates over bundled files, ignoring the runtime query", async () => {
const worker = contentWorker(new Response("updated dungeon"));
const response = await worker.request("/assets/game/dungeons/wailing-caverns/visual.glb?starter-runtime=1");
expect(await response?.text()).toBe("updated dungeon");
expect(worker.match).toHaveBeenCalledWith(expect.any(Request), { ignoreSearch: true });
expect(worker.fetch).not.toHaveBeenCalled();
});
@@ -72,6 +88,7 @@ describe("browser content cache worker", () => {
it("serves only cached same-origin asset requests from the active content cache", () => {
expect(workerSource).toContain(`const CONTENT_CACHE_NAME = "${BROWSER_CONTENT_CACHE_NAME}";`);
expect(workerSource).toContain('const BOOTSTRAP_CACHE_NAME = "healer-man-bootstrap-v1";');
expect(workerSource).toContain('request.method !== "GET"');
expect(workerSource).toContain("url.origin !== self.location.origin");
expect(workerSource).toContain("MANAGED_ASSET_PREFIXES.some");
@@ -79,6 +96,7 @@ describe("browser content cache worker", () => {
expect(workerSource).toContain("if (!objectPath) return cached");
expect(workerSource).toContain("BUNDLED_ASSET_PATHS.has(url.pathname)");
expect(workerSource).toContain("BUNDLED_ASSET_PREFIXES.some");
expect(workerSource).not.toContain('"/assets/game/arenas/",');
expect(workerSource).not.toContain('"/assets/characters/wow335a-v1/textures/character/human/",');
expect(workerSource).toContain('status: 404');
expect(workerSource).not.toContain("cached ?? fetch(request)");
@@ -93,6 +111,17 @@ describe("browser content cache worker", () => {
expect(response?.headers.get("content-type")).toBe("model/gltf-binary");
});
it("persists immutable bootstrap objects after their first use", async () => {
const url = "/assets/game/dungeons/wailing-caverns/visual.glb";
const objectUrl = `/content/bundled/${"b".repeat(64)}.glb`;
const worker = contentWorker(undefined, { [url]: { url: objectUrl } }, new Response("persistent dungeon"));
expect(await (await worker.request(url))?.text()).toBe("persistent dungeon");
expect(await (await worker.request(url))?.text()).toBe("persistent dungeon");
expect(worker.fetch).toHaveBeenCalledTimes(2);
expect(worker.bootstrapPut).toHaveBeenCalledTimes(1);
expect(worker.bootstrapMatch).toHaveBeenCalledTimes(2);
});
it("resolves a tiny installed alias to one compressed shared object", async () => {
const original = "glTF installed original".repeat(100);
const worker = contentWorker(new Response(null, { headers: { "x-healer-man-content-object": "/content/cache-objects/shared.glb" } }), {},
+4 -21
View File
@@ -8,25 +8,11 @@ import {
type ContentManifestFile,
} from "./contentManifest";
import { NativeContentStorage } from "./nativeContentStorage";
import contentPolicy from "../../shared/content-policy.json";
export const CONTENT_STATE_KEY = "healer-man.content-state.v2";
const PRODUCTION_ORIGIN = "https://iwanttoheal.phenomrom.com";
const STATIC_ANDROID_BUNDLED_PACKS = new Set([
"core:ui",
"core:equipment",
"core:character-basic",
"core:runtime",
"core:arenas",
"dungeon:wailing-caverns",
]);
const STATIC_BROWSER_BUNDLED_PACKS = new Set([
"core:ui",
"core:equipment",
"core:character-basic",
"core:runtime",
"core:arenas",
"dungeon:wailing-caverns",
]);
const STATIC_BUNDLED_PACKS = new Set(contentPolicy.bootstrapPackIds);
export const BROWSER_CONTENT_CACHE_NAME = "healer-man-content-v1";
export const BROWSER_CONTENT_WORKER_URL = "/content-cache-sw.js";
@@ -292,9 +278,7 @@ function packReceiptForManifest(manifest: ContentManifest, packId: string): Inst
function bundledFiles(): Readonly<Record<string, { readonly sha256: string }>> {
if (snapshot.bundledManifest) return snapshot.bundledManifest.files;
if (!snapshot.manifest) return {};
const bundledPacks = Capacitor.isNativePlatform()
? STATIC_ANDROID_BUNDLED_PACKS
: STATIC_BROWSER_BUNDLED_PACKS;
const bundledPacks = STATIC_BUNDLED_PACKS;
return Object.fromEntries(Object.entries(snapshot.manifest.files).filter(([, entry]) => (
bundledPacks.has(entry.packId)
)));
@@ -324,8 +308,7 @@ export function resolveContentUrl(logicalUrl: string): string {
}
export function contentPackIsBundled(packId: string): boolean {
const defaults = Capacitor.isNativePlatform() ? STATIC_ANDROID_BUNDLED_PACKS : STATIC_BROWSER_BUNDLED_PACKS;
return defaults.has(packId)
return STATIC_BUNDLED_PACKS.has(packId)
|| Boolean(snapshot.bundledManifest?.packs.some((pack) => pack.id === packId && pack.bundledOnAndroid));
}
@@ -0,0 +1,5 @@
import type { WorldSpawn } from "./dungeonTypes";
export function shelvedDungeonSpawn(_dungeonId: string, requested?: WorldSpawn): WorldSpawn | undefined {
return requested;
}
@@ -0,0 +1,6 @@
import type { WorldSpawn } from "./dungeonTypes";
import { useManastormAdminStore } from "./manastormAdminStore";
export function shelvedDungeonSpawn(dungeonId: string, requested?: WorldSpawn): WorldSpawn | undefined {
return requested ?? useManastormAdminStore.getState().config.dungeonSpawns[dungeonId];
}
+1 -1
View File
@@ -1060,7 +1060,7 @@ export function resourceProfileForClass(classId: ClassId): ClassResourceProfile
return profile;
}
if (import.meta.env.MODE === "test") {
if (import.meta.env.MODE === "test" || import.meta.env.MODE === "shelved") {
await preloadAbilityCatalogsForClasses([
...BASE_CLASS_IDS,
...COA_CLASS_IDS,
+2 -2
View File
@@ -8,8 +8,8 @@ import { usePartyStore } from "./partyStore";
import { useGameStore, type GameMode } from "./store";
import { dungeonCanEnter, dungeonDefinitionById, type DungeonId } from "./dungeonRegistry";
import type { WorldSpawn } from "./dungeonTypes";
import { useManastormAdminStore } from "./manastormAdminStore";
import type { GmReviewTarget } from "./gmContentReview";
import { shelvedDungeonSpawn } from "@shelvedDungeonSession";
import {
activateDungeonEncounterDefinitions,
exportDungeonEncounterSnapshots,
@@ -92,7 +92,7 @@ export function activateDungeonSession(
}
useGameStore.getState().activateDungeon(dungeonId as DungeonId, difficultyId, {
gameMode: options.gameMode,
spawn: options.spawn ?? useManastormAdminStore.getState().config.dungeonSpawns[dungeonId],
spawn: shelvedDungeonSpawn(dungeonId, options.spawn),
gmTestMode: options.gmTestMode,
gmReviewTarget: options.gmReviewTarget,
});
+6 -1
View File
@@ -7,6 +7,7 @@ import { usePartyStore } from "./partyStore";
import { useGameStore } from "./store";
import { DEFAULT_MANASTORM_ADMIN_CONFIG } from "./manastormAdminConfig";
import { useManastormAdminStore } from "./manastormAdminStore";
import { SHELVED_MODES_ENABLED } from "../app/releaseFeatures";
import {
dungeonEncounterSnapshot,
importDungeonEncounterSnapshots,
@@ -79,7 +80,11 @@ describe("dungeon session asset eviction", () => {
config: { ...DEFAULT_MANASTORM_ADMIN_CONFIG, dungeonSpawns: { "wailing-caverns": spawn } },
});
expect(activateDungeonSession("wailing-caverns")).toBe(true);
expect(useGameStore.getState().activeSpawn).toEqual(spawn);
const definition = dungeonDefinitionById("wailing-caverns")!;
expect(useGameStore.getState().activeSpawn).toEqual(SHELVED_MODES_ENABLED ? spawn : {
footPosition: definition.entrance.footPosition,
yaw: definition.entrance.yaw,
});
});
it("hydrates a same-process checkpoint even when the reset revision is newer", () => {
@@ -15,6 +15,110 @@ export const GRAFU_CASTLE_MECHANICS_AUDIT = {
"loot": 0
},
"bossRows": [],
"scriptedBossSpawns": [
{
"scriptedPlacementId": "cayus",
"sourceAnchorSpawnId": 41927224,
"controllerSpawnId": 41928351,
"templateId": 105616,
"name": "Cayus",
"sourcePosition": [
-348.7113952636719,
1254.5040283203125,
-999.6005249023438
],
"sourceScriptRoot": "Dy_Z19_BOSSN_AIC",
"objective": true,
"evidence": [
"Resource/luascript/02345.lua:CreateObjByFlag(105616,780696,1,1)"
]
},
{
"scriptedPlacementId": "ardmond",
"sourceAnchorSpawnId": 41926939,
"controllerSpawnId": 41928345,
"templateId": 105542,
"name": "Ardmond",
"sourcePosition": [
-1427.9969482421875,
1133.35498046875,
-1353.3699951171875
],
"sourceScriptRoot": "kg_105542_01",
"objective": true,
"evidence": [
"Resource/luascript/02324.lua:kg_105542_CreateBoss"
]
},
{
"scriptedPlacementId": "issac",
"sourceAnchorSpawnId": 41926939,
"controllerSpawnId": 41928345,
"templateId": 105543,
"name": "Issac",
"sourcePosition": [
-1427.9969482421875,
1133.35498046875,
-1313.3699951171875
],
"sourceScriptRoot": "kg_105542_01",
"objective": false,
"evidence": [
"Resource/luascript/02324.lua:kg_105542_CreateBoss"
]
},
{
"scriptedPlacementId": "boro-boro-brothers",
"sourceAnchorSpawnId": 41926924,
"controllerSpawnId": 41927347,
"templateId": 105224,
"name": "Boro-Boro Brothers",
"sourcePosition": [
-2172.174072265625,
1095.10400390625,
-22.212709426879883
],
"sourceScriptRoot": "ic_105224_01",
"objective": true,
"evidence": [
"Resource/luascript/02231.lua:CreateObjByFlag(105224,780775,1,1)"
]
},
{
"scriptedPlacementId": "conjurer-chapeaunoir",
"sourceAnchorSpawnId": 41926979,
"controllerSpawnId": 41927358,
"templateId": 105204,
"name": "Conjurer Chapeaunoir",
"sourcePosition": [
-1732.9649658203125,
914.618896484375,
1190.8310546875
],
"sourceScriptRoot": "cl_136_105205_N",
"objective": true,
"evidence": [
"Resource/luascript/02330.lua:CreateObjByFlag(105204,780767,0,1)"
]
},
{
"scriptedPlacementId": "larvanger-barkud",
"sourceAnchorSpawnId": 41927071,
"controllerSpawnId": 41927369,
"templateId": 105351,
"name": "Larvanger Barkud",
"sourcePosition": [
-305.8063049316406,
902.5551147460938,
941.4002075195312
],
"sourceScriptRoot": "un_zone19_boss5_allconreal_1_01",
"objective": true,
"evidence": [
"Resource/luascript/02297.lua:CreateObjByFlag(105351,780838,1,1)"
]
}
],
"templateCandidates": [
{
"templateId": 102678,
@@ -94,6 +198,18 @@ export const GRAFU_CASTLE_MECHANICS_AUDIT = {
"templateScripts": [],
"spells": []
},
{
"templateId": 105204,
"name": "Conjurer Chapeaunoir",
"classification": "boss",
"spawnCount": 0,
"autoPlots": [
"cl_136_105205_N"
],
"plotClassNames": [],
"templateScripts": [],
"spells": []
},
{
"templateId": 105205,
"name": "Template 105205",
@@ -109,7 +225,7 @@ export const GRAFU_CASTLE_MECHANICS_AUDIT = {
{
"templateId": 105210,
"name": "Template 105210",
"classification": "combat",
"classification": "deferred-object",
"spawnCount": 1,
"autoPlots": [
"kg_105542_01"
@@ -204,10 +320,82 @@ export const GRAFU_CASTLE_MECHANICS_AUDIT = {
"templateScripts": [],
"spells": []
},
{
"templateId": 105224,
"name": "Boro-Boro Brothers",
"classification": "boss",
"spawnCount": 0,
"autoPlots": [
"ic_105224_01"
],
"plotClassNames": [],
"templateScripts": [],
"spells": []
},
{
"templateId": 105229,
"name": "Smirking Castle Clown",
"classification": "combat",
"spawnCount": 0,
"autoPlots": [],
"plotClassNames": [],
"templateScripts": [],
"spells": []
},
{
"templateId": 105230,
"name": "Odd Castle Clown",
"classification": "combat",
"spawnCount": 0,
"autoPlots": [],
"plotClassNames": [],
"templateScripts": [],
"spells": []
},
{
"templateId": 105233,
"name": "Cursed Steel Soldier",
"classification": "combat",
"spawnCount": 0,
"autoPlots": [],
"plotClassNames": [],
"templateScripts": [],
"spells": []
},
{
"templateId": 105235,
"name": "Conjurer's Apprentice",
"classification": "combat",
"spawnCount": 0,
"autoPlots": [],
"plotClassNames": [],
"templateScripts": [],
"spells": []
},
{
"templateId": 105236,
"name": "Giant Enraged Bear",
"classification": "combat",
"spawnCount": 0,
"autoPlots": [],
"plotClassNames": [],
"templateScripts": [],
"spells": []
},
{
"templateId": 105237,
"name": "Manipulated Bear",
"classification": "combat",
"spawnCount": 0,
"autoPlots": [],
"plotClassNames": [],
"templateScripts": [],
"spells": []
},
{
"templateId": 105254,
"name": "Template 105254",
"classification": "combat",
"classification": "deferred-object",
"spawnCount": 10,
"autoPlots": [
"cl_136_01_01_N",
@@ -225,7 +413,7 @@ export const GRAFU_CASTLE_MECHANICS_AUDIT = {
{
"templateId": 105258,
"name": "Template 105258",
"classification": "combat",
"classification": "deferred-object",
"spawnCount": 5,
"autoPlots": [
"cl_105258_1",
@@ -265,6 +453,42 @@ export const GRAFU_CASTLE_MECHANICS_AUDIT = {
"templateScripts": [],
"spells": []
},
{
"templateId": 105351,
"name": "Larvanger Barkud",
"classification": "boss",
"spawnCount": 0,
"autoPlots": [
"un_zone19_boss5_allconreal_1_01"
],
"plotClassNames": [],
"templateScripts": [],
"spells": []
},
{
"templateId": 105542,
"name": "Ardmond",
"classification": "boss",
"spawnCount": 0,
"autoPlots": [
"kg_105542_01"
],
"plotClassNames": [],
"templateScripts": [],
"spells": []
},
{
"templateId": 105543,
"name": "Issac",
"classification": "boss",
"spawnCount": 0,
"autoPlots": [
"kg_105542_01"
],
"plotClassNames": [],
"templateScripts": [],
"spells": []
},
{
"templateId": 105596,
"name": "Smirking Castle Clown",
@@ -332,6 +556,16 @@ export const GRAFU_CASTLE_MECHANICS_AUDIT = {
}
]
},
{
"templateId": 105599,
"name": "Cursed Steel Soldier",
"classification": "combat",
"spawnCount": 0,
"autoPlots": [],
"plotClassNames": [],
"templateScripts": [],
"spells": []
},
{
"templateId": 105600,
"name": "Juggler's Apprentice",
@@ -420,6 +654,18 @@ export const GRAFU_CASTLE_MECHANICS_AUDIT = {
}
]
},
{
"templateId": 105616,
"name": "Cayus",
"classification": "boss",
"spawnCount": 0,
"autoPlots": [
"Dy_Z19_BOSSN_AIC"
],
"plotClassNames": [],
"templateScripts": [],
"spells": []
},
{
"templateId": 116434,
"name": "Skeleton of an Adventurer",
File diff suppressed because it is too large. Load diff
+73
View File
@@ -0,0 +1,73 @@
import { describe, expect, it } from "vitest";
import {
GRAFU_CASTLE_BOSSES,
GRAFU_CASTLE_ENTITIES,
GRAFU_CASTLE_POPULATION_PROVENANCE,
GRAFU_CASTLE_STATIC_SPAWNS,
} from "./generated/grafuCastlePopulation.generated";
const CONTROLLER_ENTITY_IDS = [
"rw-grafu-castle-105210",
"rw-grafu-castle-105254",
"rw-grafu-castle-105258",
] as const;
const BOSS_ENTITY_IDS = [
"rw-grafu-castle-105204",
"rw-grafu-castle-105224",
"rw-grafu-castle-105351",
"rw-grafu-castle-105542",
"rw-grafu-castle-105543",
"rw-grafu-castle-105616",
] as const;
describe("Grafu Castle population", () => {
it("does not render Lua controllers as mushroom combat mobs", () => {
for (const controllerId of CONTROLLER_ENTITY_IDS) {
expect(GRAFU_CASTLE_ENTITIES).not.toHaveProperty(controllerId);
expect(GRAFU_CASTLE_STATIC_SPAWNS.some((spawn) => (
String(spawn.entityId) === controllerId
))).toBe(false);
}
});
it("materializes the source-authored patrol formations", () => {
const formationSpawns = GRAFU_CASTLE_STATIC_SPAWNS.filter((spawn) => (
spawn.id.startsWith("rw-grafu-castle-scripted-castle-patrol-")
));
expect(formationSpawns).toHaveLength(38);
expect(new Set(formationSpawns.map((spawn) => GRAFU_CASTLE_ENTITIES[spawn.entityId]?.name)))
.toEqual(new Set([
"Conjurer's Apprentice",
"Cursed Steel Soldier",
"Giant Enraged Bear",
"Manipulated Bear",
"Manipulated Steel Soldier",
"Odd Castle Clown",
"Smirking Castle Clown",
]));
});
it("ships all five normal encounters and both members of the duo", () => {
expect(GRAFU_CASTLE_BOSSES.map((boss) => boss.name)).toEqual([
"Cayus",
"Ardmond and Issac",
"Boro-Boro Brothers",
"Conjurer Chapeaunoir",
"Larvanger Barkud",
]);
expect(GRAFU_CASTLE_POPULATION_PROVENANCE.bossSpawnCount).toBe(5);
expect(GRAFU_CASTLE_POPULATION_PROVENANCE.scriptedSpawnCount).toBe(44);
for (const entityId of BOSS_ENTITY_IDS) {
expect(GRAFU_CASTLE_ENTITIES[entityId].kind).toBe("boss");
expect(GRAFU_CASTLE_ENTITIES[entityId].visual.model?.url).toMatch(
/^\/assets\/creatures\/grafu-castle\/.+\.glb$/,
);
expect(GRAFU_CASTLE_STATIC_SPAWNS.some((spawn) => spawn.entityId === entityId)).toBe(true);
}
for (const objective of GRAFU_CASTLE_BOSSES) {
expect(GRAFU_CASTLE_STATIC_SPAWNS.some((spawn) => spawn.id === objective.id)).toBe(true);
}
});
});
+1 -20
View File
@@ -478,23 +478,4 @@ export function isGameMasterSession(session: { readonly kind: string; readonly a
return Boolean(session?.kind === "account" && session.accessToken && session.roles?.includes("gm"));
}
export function derivePartySpawnSlots(spawn: WorldSpawn, partySize = 5): readonly WorldSpawn[] {
const size = Math.max(1, Math.min(5, Math.trunc(partySize)));
const offsets = [
[0, 0],
[-1.65, -1.8],
[1.65, -1.8],
[-3.05, -3.5],
[3.05, -3.5],
] as const;
const sin = Math.sin(spawn.yaw);
const cos = Math.cos(spawn.yaw);
return Object.freeze(offsets.slice(0, size).map(([lateral, forward]) => ({
footPosition: [
spawn.footPosition[0] + lateral * cos + forward * sin,
spawn.footPosition[1],
spawn.footPosition[2] - lateral * sin + forward * cos,
] as const,
yaw: spawn.yaw,
})));
}
export { derivePartySpawnSlots } from "./partySpawnSlots";
+22
View File
@@ -0,0 +1,22 @@
import type { WorldSpawn } from "./dungeonTypes";
export function derivePartySpawnSlots(spawn: WorldSpawn, partySize = 5): readonly WorldSpawn[] {
const size = Math.max(1, Math.min(5, Math.trunc(partySize)));
const offsets = [
[0, 0],
[-1.65, -1.8],
[1.65, -1.8],
[-3.05, -3.5],
[3.05, -3.5],
] as const;
const sin = Math.sin(spawn.yaw);
const cos = Math.cos(spawn.yaw);
return Object.freeze(offsets.slice(0, size).map(([lateral, forward]) => ({
footPosition: [
spawn.footPosition[0] + lateral * cos + forward * sin,
spawn.footPosition[1],
spawn.footPosition[2] - lateral * sin + forward * cos,
] as const,
yaw: spawn.yaw,
})));
}
+2 -1
View File
@@ -24,7 +24,7 @@ describe("modular RuneWaker boss mechanics", () => {
));
it("gives every packaged boss a catalog-backed special-mechanic loadout", () => {
expect(bosses).toHaveLength(56);
expect(bosses).toHaveLength(62);
for (const { dungeon, entity } of bosses) {
const label = `${dungeon.id}:${entity.id}`;
const authored = catalog.bosses[entity.id];
@@ -70,6 +70,7 @@ describe("modular RuneWaker boss mechanics", () => {
"rw-fa-100561",
"rw-fa-100562",
"rw-fa-100563",
"rw-grafu-castle-105351",
]);
});
});
+3 -3
View File
@@ -283,8 +283,8 @@ describe("RuneWaker dungeon creature animation coverage", () => {
}
expect(counts).toEqual({
configured: 268,
native: 267,
configured: 275,
native: 274,
poseOnly: 1,
dynamicProxy: 0,
});
@@ -301,7 +301,7 @@ describe("RuneWaker dungeon creature animation coverage", () => {
}
}
expect(runtimeCounts, "runtime proxies: " + runtimeProxyIds.join(", ")).toEqual({
native: 436,
native: 446,
poseOnly: 1,
proxy: 0,
});
+3 -1
View File
@@ -8,6 +8,7 @@ import { clearMobRuntimeRegistry, setMobPosition } from "../game/mobRuntimeRegis
import { usePartyStore } from "../game/partyStore";
import { useGameStore } from "../game/store";
import { currentCompanionSnapshot, executeCompanionCommand } from "./dualScreenSync";
import { SHELVED_MODES_ENABLED } from "../app/releaseFeatures";
describe("dual-screen party commands", () => {
beforeEach(() => {
@@ -147,7 +148,8 @@ describe("dual-screen party commands", () => {
executeCompanionCommand({ name: "returnToMainMenu" });
executeCompanionCommand({ name: "openManastorms" });
expect(useShellStore.getState().phase).toBe("manastorms");
expect(useShellStore.getState().phase).toBe(SHELVED_MODES_ENABLED ? "manastorms" : "main-menu");
if (!SHELVED_MODES_ENABLED) expect(useShellStore.getState().notice).toMatch(/temporarily unavailable/i);
executeCompanionCommand({ name: "returnToMainMenu" });
executeCompanionCommand({ name: "openDungeons" });
+2 -4
View File
@@ -26,7 +26,6 @@ import { PartyPopulation } from "./PartyPopulation";
import { GroundEffectTelegraphs } from "./GroundEffectTelegraphs";
import { PlayerRig } from "./PlayerRig";
import { useManastormStore } from "../game/manastormStore";
import { ManastormPortal } from "./ManastormPortal";
import {
encounterWorldIsReady,
encounterWorldKey,
@@ -44,7 +43,6 @@ import { WailingNaralexEvent } from "./WailingNaralexEvent";
import { GameGltfLoaderLifecycle } from "./useGameGLTF";
import { CombatEffects } from "./CombatEffects";
import { PLAYER_GRAVITY } from "../game/playerMovement";
import { GmPlacementMarkers } from "./GmPlacementMarkers";
import { useOnlineGroupStore } from "../app/onlineGroupStore";
import { useOnlineSessionStore } from "../app/onlineSessionStore";
import { OnlinePlayerPopulation } from "./OnlinePlayerPopulation";
@@ -59,6 +57,7 @@ import { DungeonWorldObjects } from "./DungeonWorldObjects";
import { ActivityAssetWarmup } from "./ActivityAssetWarmup";
import { resolveDungeonAssets, resolveManastormStageAssets } from "../game/dungeonAssets";
import { activeManastormStageAssetPackage } from "../game/manastormStageLoader";
import { ShelvedGameSceneFeatures } from "@shelvedGameScene";
import { shouldLoadPartyNavigation } from "../game/partyNavigationPolicy";
import {
activeDungeonCompletedObjectiveIds,
@@ -404,8 +403,7 @@ export function GameScene() {
allowedRuntimeIds={allowedRuntimeIds}
/>
<WailingNaralexEvent />
<ManastormPortal />
<GmPlacementMarkers />
<ShelvedGameSceneFeatures />
<CombatEffects active={!simulationBlocked} safeGraphics={safeGraphics} />
</>
)}
+13 -4
View File
@@ -50,6 +50,7 @@ import {
type OriginalCombatMeshGraph,
} from "./originalCombatMeshRuntime";
import { updateCombatEffectAnchorPosition } from "./combatEffectAnchorRegistry";
import { shouldUseGeneratedOriginalFallback } from "./combatEffectRuntime";
import { OriginalWowModel } from "./OriginalWowModel";
import { OriginalParticleGeometry } from "./OriginalParticleGeometry";
import {
@@ -99,9 +100,10 @@ const vertexShader = /* glsl */ `
vParticleAlpha = particleAlpha;
vParticleFrame = particleFrame;
vec4 viewPosition = modelViewMatrix * vec4(position, 1.0);
float inheritedScale = length(modelViewMatrix[0].xyz);
gl_Position = projectionMatrix * viewPosition;
gl_PointSize = clamp(
particleSize * viewportHeight / max(1.0, -viewPosition.z),
particleSize * inheritedScale * viewportHeight / max(1.0, -viewPosition.z),
1.0,
256.0
);
@@ -894,16 +896,23 @@ export const OriginalCombatEffect = memo(function OriginalCombatEffect({
const selection = loaded === null
? null
: selectCombatEffectRender("original", loaded.map((item) => item.asset));
const hasOriginalInstantKit = event.phase === "release" && event.instantCast === true
&& (["cast-start", "impact", "aura-start"] as const).some((phase) => (
combatOriginalPhaseAssets({ ...event, phase }).length > 0
));
const resolvedFallback = shouldUseGeneratedOriginalFallback(event, hasOriginalInstantKit)
? fallback
: null;
useEffect(() => {
if (
loaded === null
|| selection?.kind === "original"
|| fallback !== null
|| resolvedFallback !== null
|| completedMissingOriginalRef.current
) return;
completedMissingOriginalRef.current = true;
onDone();
}, [fallback, loaded, onDone, selection?.kind]);
}, [loaded, onDone, resolvedFallback, selection?.kind]);
if (loaded === null || selection === null) return null;
return selection.kind === "original" ? (
@@ -914,7 +923,7 @@ export const OriginalCombatEffect = memo(function OriginalCombatEffect({
loaded={loaded}
onDone={onDone}
/>
) : fallback;
) : resolvedFallback;
}, (previous, next) => (
previous.event === next.event
&& previous.alternateOriginalPhase === next.alternateOriginalPhase
+1 -1
View File
@@ -23,7 +23,7 @@ import { resolvedEquipmentItems } from "../game/heirloomEquipment";
import type { CharacterCombatAnimationEvent } from "../game/combatAnimation";
import { resolveDungeonAssets, resolveManastormStageAssets } from "../game/dungeonAssets";
import { activeManastormStageAssetPackage } from "../game/manastormStageLoader";
import { derivePartySpawnSlots } from "../game/manastormAdminConfig";
import { derivePartySpawnSlots } from "../game/partySpawnSlots";
import { requireDungeonDefinition } from "../game/dungeonRegistry";
import type { DungeonBossObjective } from "../game/dungeonTypes";
import { getMobPosition } from "../game/mobRuntimeRegistry";
+3
View File
@@ -0,0 +1,3 @@
export function ShelvedGameSceneFeatures() {
return null;
}
+11
View File
@@ -0,0 +1,11 @@
import { GmPlacementMarkers } from "./GmPlacementMarkers";
import { ManastormPortal } from "./ManastormPortal";
export function ShelvedGameSceneFeatures() {
return (
<>
<ManastormPortal />
<GmPlacementMarkers />
</>
);
}
+19
View File
@@ -7,6 +7,7 @@ import {
advanceCombatEffectFrame,
originalInstantCastEvent,
originalInstantTerminalEvent,
shouldUseGeneratedOriginalFallback,
updateCombatEffectQueue,
type ActiveCombatEffect,
} from "./combatEffectRuntime";
@@ -168,4 +169,22 @@ describe("combat-effect runtime budget", () => {
});
expect(originalInstantTerminalEvent(event(22, { phase: "impact" }))).toBeNull();
});
it("does not layer a generated release over an instant ability's original phases", () => {
expect(shouldUseGeneratedOriginalFallback(event(30, {
phase: "release",
instantCast: true,
}), true)).toBe(false);
expect(shouldUseGeneratedOriginalFallback(event(31, {
phase: "release",
instantCast: true,
}), false)).toBe(true);
expect(shouldUseGeneratedOriginalFallback(event(31, {
phase: "release",
}), true)).toBe(true);
expect(shouldUseGeneratedOriginalFallback(event(32, {
phase: "impact",
instantCast: true,
}), true)).toBe(true);
});
});
+9
View File
@@ -179,6 +179,15 @@ export function originalInstantTerminalEvent(
};
}
/** Instant resolutions immediately emit a terminal phase, so their missing release
* asset must not add a generated layer over the authentic cast and terminal kits. */
export function shouldUseGeneratedOriginalFallback(
event: CombatPresentationEvent,
hasOriginalInstantKit: boolean,
): boolean {
return event.phase !== "release" || !event.instantCast || !hasOriginalInstantKit;
}
export function addOriginalOnlyEffect(
current: readonly ActiveCombatEffect[],
event: CombatPresentationEvent,
+10
View File
@@ -5,6 +5,7 @@ import { Matrix4, Quaternion, Vector3 } from "three";
import { parseOriginalWowModelGraph, updateWowBoneMatrices, wowModelFrame, wowModelTextureUrls, wowParticleChild } from "./originalWowModelRuntime";
const manifest = JSON.parse(readFileSync(path.resolve("public/assets/game/combat-effects/manifest.json"), "utf8"));
const rendererSource = readFileSync(new URL("./OriginalCombatEffect.tsx", import.meta.url), "utf8");
const urls = [...new Set<string>(manifest.entries.filter((e: { source: string }) => e.source === "wow335")
.flatMap((e: { phases: Record<string, { effectGraphUrl?: string }[]> }) => Object.values(e.phases ?? {}).flat()
.map((a) => a.effectGraphUrl).filter(Boolean)))];
@@ -67,4 +68,13 @@ describe("all-class original WoW models", () => {
const dispel = parseOriginalWowModelGraph(graphs.find((g) => g.sourceModel === "spells/clense_base.m2"));
expect(wowParticleChild(dispel, "Spells\\Dispel_Low_Recursive.mdx")!.particleGraph!.particles).toHaveLength(1);
});
it("applies recursive parent scale to Purify and Cleanse point sprites", () => {
for (const sourceModel of ["spells/purify_base.m2", "spells/clense_base.m2"]) {
const graph = parseOriginalWowModelGraph(graphs.find((candidate) => candidate.sourceModel === sourceModel));
expect(graph.particleGraph!.particles.filter((particle) => particle.recursionModel)).toHaveLength(3);
}
expect(rendererSource).toContain("float inheritedScale = length(modelViewMatrix[0].xyz);");
expect(rendererSource).toContain("particleSize * inheritedScale * viewportHeight");
});
});
+5 -18
View File
@@ -1,4 +1,4 @@
import { lazy, Suspense, useState, type CSSProperties } from "react";
import { useState, type CSSProperties } from "react";
import { classById, FACTIONS, raceById } from "../app/characterCatalog";
import type { CharacterProfile } from "../app/types";
import { useShellStore } from "../app/shellStore";
@@ -28,14 +28,8 @@ import { ActionBar, type ActionBarItem, type ActionBarItems } from "./ActionBar"
import { DungeonMapGraphic } from "./DungeonMapGraphic";
import { ExperienceBar } from "./ExperienceBar";
import { BrandMark, FrontSurface } from "./FrontSurface";
import {
getLoadedCompanionManastormPanel,
loadCompanionManastormPanel,
} from "./gameplayResources";
const CompanionManastormPanel = lazy(() => loadCompanionManastormPanel().then((module) => ({
default: module.CompanionManastormPanel,
})));
import { ShelvedCompanionPanel } from "@shelvedCompanionPanel";
import { SHELVED_MODES_ENABLED } from "../app/releaseFeatures";
function safePercent(current: number, maximum: number): number {
if (!Number.isFinite(current) || !Number.isFinite(maximum) || maximum <= 0) return 0;
@@ -158,7 +152,6 @@ export function CompanionPanel({ snapshot, send, preferControllerBinds = false }
}) {
const selected = snapshot.characters.find((character) => character.id === snapshot.selectedCharacterId) ?? null;
const active = snapshot.activeCharacter ?? selected;
const PreloadedCompanionManastormPanel = getLoadedCompanionManastormPanel();
if (snapshot.phase === "login") {
return (
@@ -289,7 +282,7 @@ export function CompanionPanel({ snapshot, send, preferControllerBinds = false }
<footer className="companion-actions">
<button type="button" onClick={() => send({ name: "returnToCharacters" })}>Change character</button>
<button type="button" disabled={!selected} onClick={() => send({ name: "openDungeons" })}>Dungeons</button>
<button type="button" disabled={!selected} onClick={() => send({ name: "openManastorms" })}>Manastorms</button>
{SHELVED_MODES_ENABLED ? <button type="button" disabled={!selected} onClick={() => send({ name: "openManastorms" })}>Manastorms</button> : null}
</footer>
</FrontSurface>
);
@@ -300,13 +293,7 @@ export function CompanionPanel({ snapshot, send, preferControllerBinds = false }
}
if (snapshot.phase === "manastorms") {
return PreloadedCompanionManastormPanel ? (
<PreloadedCompanionManastormPanel selected={selected} send={send} />
) : (
<Suspense fallback={<FrontSurface bottom className="companion-panel" ariaLabel="Loading Manastorm companion screen"><span>Loading Manastorm data…</span></FrontSurface>}>
<CompanionManastormPanel selected={selected} send={send} />
</Suspense>
);
return <ShelvedCompanionPanel selected={selected} send={send} />;
}
const race = active ? raceById(active.raceId) : null;
+26 -8
View File
@@ -6,6 +6,14 @@ const content = vi.hoisted(() => ({
installed: true,
update: true,
bundled: false,
packs: [{
id: "characters:test",
label: "Test Characters",
bytes: 1024,
fileCount: 1,
bundledOnAndroid: false,
dependencies: [],
}],
install: vi.fn(async () => undefined),
remove: vi.fn(async () => undefined),
}));
@@ -29,14 +37,7 @@ vi.mock("../content/useContentManager", () => ({
checking: false,
manifest: {
version: "content-test",
packs: [{
id: "characters:test",
label: "Test Characters",
bytes: 1024,
fileCount: 1,
bundledOnAndroid: false,
dependencies: [],
}],
packs: content.packs,
},
bundledManifest: null,
installingPackId: null,
@@ -61,6 +62,10 @@ describe("browser Downloads actions", () => {
content.installed = true;
content.update = true;
content.bundled = false;
content.packs = [{
id: "characters:test", label: "Test Characters", bytes: 1024,
fileCount: 1, bundledOnAndroid: false, dependencies: [],
}];
});
it("offers an update action when an installed browser pack has changed", () => {
@@ -92,4 +97,17 @@ describe("browser Downloads actions", () => {
expect(markup).toContain("Included — always available");
expect(markup).not.toContain(">Remove</button>");
});
it("hides shelved and internal dependency packs from Downloads", () => {
content.packs = [
...content.packs,
{ id: "core:arenas", label: "Rated Arenas", bytes: 1, fileCount: 1, bundledOnAndroid: false, dependencies: [] },
{ id: "manastorm:deadmines", label: "Deadmines Manastorm", bytes: 1, fileCount: 1, bundledOnAndroid: false, dependencies: [] },
];
const markup = renderToStaticMarkup(createElement(ContentManagerScreen, { onClose: () => undefined }));
expect(markup).toContain("Test Characters");
expect(markup).not.toContain("Rated Arenas");
expect(markup).not.toContain("Deadmines Manastorm");
});
});
+5 -1
View File
@@ -12,6 +12,7 @@ import { useContentManagerSnapshot } from "../content/useContentManager";
import { DualDisplayFrame } from "../components/DualDisplayFrame";
import { LazyLocalCompanionPanel } from "./LazyLocalCompanionPanel";
import { BrandMark, FrontSurface } from "./FrontSurface";
import contentPolicy from "../../shared/content-policy.json";
function formatBytes(bytes: number): string {
if (bytes >= 1024 ** 3) return `${(bytes / 1024 ** 3).toFixed(2)} GB`;
@@ -23,7 +24,10 @@ function formatBytes(bytes: number): string {
export function ContentManagerScreen({ onClose }: { readonly onClose: () => void }) {
const state = useContentManagerSnapshot();
const native = Capacitor.isNativePlatform();
const packs = useMemo(() => state.manifest?.packs ?? state.bundledManifest?.packs ?? [], [state.bundledManifest, state.manifest]);
const packs = useMemo(() => (state.manifest?.packs ?? state.bundledManifest?.packs ?? []).filter((pack) => (
!contentPolicy.hiddenContentPackIds.includes(pack.id)
&& !contentPolicy.hiddenContentPackPrefixes.some((prefix) => pack.id.startsWith(prefix))
)), [state.bundledManifest, state.manifest]);
const progress = state.totalBytes > 0 ? Math.min(1, state.downloadedBytes / state.totalBytes) : 0;
return (
+2 -2
View File
@@ -30,7 +30,7 @@ import { EncounterTimeline } from "./EncounterTimeline";
import { PartyFrames } from "./PartyFrames";
import { TimedEffectStrip } from "./TimedEffectStrip";
import { AuraStrip } from "./AuraStrip";
import { ManastormStatus } from "./ManastormStatus";
import { ShelvedHud } from "@shelvedHud";
import { ThreatMeterWidget } from "./ThreatMeterWidget";
import { BossLootWindow } from "./BossLootWindow";
import { EditableHudElement } from "./EditableHudElement";
@@ -363,7 +363,7 @@ export function Hud() {
return (
<div className="hud" style={{ "--game-ui-scale": settings.uiScale } as CSSProperties}>
<ManastormStatus />
<ShelvedHud />
<BossLootWindow />
<EditableHudElement id="completion">{dungeonComplete && health > 0 ? (
<DungeonCompletePrompt dungeonTitle={dungeon.title} onFinish={finishDungeon} />
+11 -13
View File
@@ -8,8 +8,8 @@ import { LazyLocalCompanionPanel } from "./LazyLocalCompanionPanel";
import { ControllerButton } from "./ControllerButton";
import { ContentManagerScreen } from "./ContentManagerScreen";
import { BrandMark, ControllerLegend, FrontSurface } from "./FrontSurface";
import { isGameMasterSession } from "../game/manastormAdminConfig";
import { OnlineGroupPanel } from "./OnlineGroupPanel";
import { sessionCanUseShelvedGmTools, SHELVED_MODES_ENABLED } from "../app/releaseFeatures";
const GAME_MODES = [
{
@@ -19,20 +19,19 @@ const GAME_MODES = [
description: "Choose an expedition and lead your party into the depths.",
available: true,
},
{
id: "manastorms",
...(SHELVED_MODES_ENABLED ? [{
id: "manastorms" as const,
number: "02",
name: "Manastorms",
description: "Chain random boss encounters as the storm grows stronger and more rewarding.",
available: true,
},
{
id: "arenas",
}, {
id: "arenas" as const,
number: "03",
name: "Arenas",
description: "Rated 2v2 and 3v3 combat against players or rating-matched opponents.",
available: true,
},
}] : []),
{
id: "trials",
number: "04",
@@ -77,7 +76,7 @@ export function MainMenuScreen() {
const openGmAdmin = useShellStore((state) => state.openGmAdmin);
const character = activeCharacter ?? rosterCharacter;
const isRom = character?.categoryId === "rom";
const isGm = isGameMasterSession(session);
const isGm = sessionCanUseShelvedGmTools(session);
const actions = useMemo<MenuAction[]>(() => [
{
@@ -85,20 +84,19 @@ export function MainMenuScreen() {
run: openDungeons,
neighbors: { right: "mode-manastorms", down: "back-to-roster" },
},
{
...(SHELVED_MODES_ENABLED ? [{
id: "mode-manastorms",
run: openManastorms,
neighbors: { left: "mode-dungeons", down: "back-to-roster" },
},
{
}, {
id: "mode-arenas",
run: openArenas,
neighbors: { left: "mode-manastorms", down: "back-to-roster" },
},
}] : []),
{
id: "mode-downloads",
run: () => setContentManagerOpen(true),
neighbors: { left: "mode-manastorms", down: "back-to-roster" },
neighbors: { left: SHELVED_MODES_ENABLED ? "mode-manastorms" : "mode-dungeons", down: "back-to-roster" },
},
{
id: "back-to-roster",
+2 -2
View File
@@ -3,7 +3,7 @@ import { useShellStore } from "../app/shellStore";
import { useGameStore } from "../game/store";
import { useMenuController, type MenuAction } from "../input/useMenuController";
import { ControllerButton } from "./ControllerButton";
import { isGameMasterSession } from "../game/manastormAdminConfig";
import { sessionCanUseShelvedGmTools } from "../app/releaseFeatures";
import { useInputBindingLabels } from "../input/useInputBindingLabels";
import { INPUT_ACTION_BY_ID } from "../input/keybinds";
@@ -33,7 +33,7 @@ function PauseDialog() {
abandonManastormRun();
};
const skillMenuLabel = activeCharacter?.categoryId === "rom" ? "Skills" : "Talents";
const isGm = isGameMasterSession(session);
const isGm = sessionCanUseShelvedGmTools(session);
const actions = useMemo<MenuAction[]>(() => [
{ id: "pause-resume", run: resume },
@@ -0,0 +1,9 @@
import type { CharacterProfile } from "../app/types";
import type { CompanionCommand } from "../platform/dualScreenSync";
export function ShelvedCompanionPanel(_props: {
readonly selected: CharacterProfile | null;
readonly send: (command: CompanionCommand) => void;
}) {
return null;
}
+22
View File
@@ -0,0 +1,22 @@
import { lazy, Suspense } from "react";
import type { CharacterProfile } from "../app/types";
import type { CompanionCommand } from "../platform/dualScreenSync";
import { FrontSurface } from "./FrontSurface";
const CompanionManastormPanel = lazy(() => import("./CompanionManastormPanel").then((module) => ({
default: module.CompanionManastormPanel,
})));
export function ShelvedCompanionPanel({
selected,
send,
}: {
readonly selected: CharacterProfile | null;
readonly send: (command: CompanionCommand) => void;
}) {
return (
<Suspense fallback={<FrontSurface bottom className="companion-panel" ariaLabel="Loading Manastorm companion screen"><span>Loading Manastorm data…</span></FrontSurface>}>
<CompanionManastormPanel selected={selected} send={send} />
</Suspense>
);
}
+3
View File
@@ -0,0 +1,3 @@
export function ShelvedHud() {
return null;
}
+5
View File
@@ -0,0 +1,5 @@
import { ManastormStatus } from "./ManastormStatus";
export function ShelvedHud() {
return <ManastormStatus />;
}
+7 -2
View File
@@ -21,6 +21,10 @@ const companionPanelSource = readFileSync(
new URL("./CompanionPanel.tsx", import.meta.url),
"utf8",
);
const shelvedCompanionSource = readFileSync(
new URL("./ShelvedCompanionPanel.enabled.tsx", import.meta.url),
"utf8",
);
describe("gameplay panel loading integration", () => {
it("loads talent resources behind the main-menu catalog gate, not during gameplay", () => {
@@ -42,9 +46,10 @@ describe("gameplay panel loading integration", () => {
expect(gameplayResourcesSource).toContain('import("../GameRuntime")');
expect(gameplayResourcesSource).toContain('import("./PartyManagementPanel")');
expect(gameplayResourcesSource).toContain('import("./BossGuidePanel")');
expect(gameplayResourcesSource).toContain('import("./CompanionManastormPanel")');
expect(gameplayResourcesSource).not.toContain('import("./CompanionManastormPanel")');
expect(shelvedCompanionSource).toContain('import("./CompanionManastormPanel")');
expect(gameplayPanelsSource).toContain("<PreloadedPartyManagementPanel />");
expect(gameplayPanelsSource).toContain("<PreloadedBossGuidePanel />");
expect(companionPanelSource).toContain("<PreloadedCompanionManastormPanel");
expect(companionPanelSource).toContain("<ShelvedCompanionPanel");
});
});
-22
View File
@@ -4,8 +4,6 @@ let partyManagementPanelModule: Promise<typeof import("./PartyManagementPanel")>
let loadedPartyManagementPanel: typeof import("./PartyManagementPanel")["PartyManagementPanel"] | null = null;
let bossGuidePanelModule: Promise<typeof import("./BossGuidePanel")> | null = null;
let loadedBossGuidePanel: typeof import("./BossGuidePanel")["BossGuidePanel"] | null = null;
let companionManastormPanelModule: Promise<typeof import("./CompanionManastormPanel")> | null = null;
let loadedCompanionManastormPanel: typeof import("./CompanionManastormPanel")["CompanionManastormPanel"] | null = null;
export function loadGameRuntime() {
if (!gameRuntimeModule) {
@@ -64,31 +62,11 @@ export function getLoadedBossGuidePanel() {
return loadedBossGuidePanel;
}
export function loadCompanionManastormPanel() {
if (!companionManastormPanelModule) {
companionManastormPanelModule = import("./CompanionManastormPanel")
.then((module) => {
loadedCompanionManastormPanel = module.CompanionManastormPanel;
return module;
})
.catch((error: unknown) => {
companionManastormPanelModule = null;
throw error;
});
}
return companionManastormPanelModule;
}
export function getLoadedCompanionManastormPanel() {
return loadedCompanionManastormPanel;
}
export async function preloadSharedGameplayResources(originalMediaEnabled: boolean): Promise<void> {
await Promise.all([
loadGameRuntime(),
loadPartyManagementPanel(),
loadBossGuidePanel(),
loadCompanionManastormPanel(),
]);
if (!originalMediaEnabled) return;
const { ensureCombatMediaPack } = await import("../game/combatMediaPack");
+1
View File
@@ -5,6 +5,7 @@ interface ImportMetaEnv {
readonly VITE_CONTENT_BASE_URL?: string;
readonly VITE_CONTENT_MODE: "bundled" | "downloadable";
readonly VITE_APP_VERSION: string;
readonly VITE_SHELVED_MODES_ENABLED: "true" | "false";
}
interface ImportMeta {