114 lines
6.4 KiB
TypeScript
114 lines
6.4 KiB
TypeScript
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 { 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,
|
|
loadGameRuntime,
|
|
preloadSharedGameplayResources,
|
|
} from "./ui/gameplayResources";
|
|
import { useCompanionCooldownPolicyStore } from "./game/companionCooldownPolicyStore";
|
|
import { useCompanionSpecReadinessStore } from "./game/companionSpecReadinessStore";
|
|
import { usePartyBuffPolicyStore } from "./game/partyBuffPolicyStore";
|
|
|
|
const GameRuntime = lazy(loadGameRuntime);
|
|
const CharacterCreateScreen = lazy(() => import("./ui/CharacterCreateScreen").then((module) => ({ default: module.CharacterCreateScreen })));
|
|
const CharacterSelectScreen = lazy(() => import("./ui/CharacterSelectScreen").then((module) => ({ default: module.CharacterSelectScreen })));
|
|
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 DungeonSimulationAdminScreen = lazy(() => import("./ui/DungeonSimulationAdminScreen").then((module) => ({ default: module.DungeonSimulationAdminScreen })));
|
|
function ShellTransition() {
|
|
return <div className="world-transition"><span>HM</span><strong>Opening expedition</strong><small>Preparing the next screen...</small></div>;
|
|
}
|
|
|
|
function RuntimeCatalogGate({ children, preloadGameplay }: { readonly children: ReactNode; readonly preloadGameplay: boolean }) {
|
|
const character = useShellStore((state) => (
|
|
state.characters.find((candidate) => candidate.id === state.selectedCharacterId) ?? null
|
|
));
|
|
const classIds = useMemo(() => {
|
|
if (!character) return [];
|
|
const definition = classById(character.classId);
|
|
if (definition.mode === "conquest") return [...BASE_CLASS_IDS, character.classId];
|
|
return [character.classId, ...(character.secondaryClassId ? [character.secondaryClassId] : [])];
|
|
}, [character]);
|
|
const talentClassId = character && !character.classId.startsWith("rom-")
|
|
? character.classId
|
|
: null;
|
|
const originalMediaEnabled = useShellStore((state) => state.combatPresentationSettings.originalMediaEnabled);
|
|
const requestKey = classIds.length
|
|
? `${classIds.join(":")}|${preloadGameplay ? `game:${originalMediaEnabled}` : "shell"}`
|
|
: "";
|
|
const [loadedKey, setLoadedKey] = useState("");
|
|
const [failedKey, setFailedKey] = useState("");
|
|
|
|
useEffect(() => {
|
|
if (!requestKey) return;
|
|
let active = true;
|
|
setFailedKey("");
|
|
void Promise.all([
|
|
preloadAbilityCatalogsForClasses(classIds),
|
|
preloadGameplay && talentClassId ? preloadTalentGameplayResources(talentClassId) : Promise.resolve(),
|
|
preloadGameplay ? preloadSharedGameplayResources(originalMediaEnabled) : Promise.resolve(),
|
|
]).then(() => {
|
|
if (active) setLoadedKey(requestKey);
|
|
}).catch(() => {
|
|
if (active) setFailedKey(requestKey);
|
|
});
|
|
return () => { active = false; };
|
|
}, [classIds, originalMediaEnabled, preloadGameplay, requestKey, talentClassId]);
|
|
|
|
if (!requestKey || loadedKey === requestKey) return children;
|
|
if (failedKey === requestKey) {
|
|
return <div className="world-transition"><span>!</span><strong>Catalog unavailable</strong><small>Reload the app to retry loading combat data.</small></div>;
|
|
}
|
|
return <ShellTransition />;
|
|
}
|
|
|
|
export default function App() {
|
|
useForcedThorDisplays();
|
|
useAuthoritativeDualScreenSync();
|
|
const phase = useShellStore((state) => state.phase);
|
|
const session = useShellStore((state) => state.session);
|
|
const PreloadedGameRuntime = getLoadedGameRuntime();
|
|
const refreshCooldownPolicy = useCompanionCooldownPolicyStore((state) => state.refresh);
|
|
const refreshSpecReadiness = useCompanionSpecReadinessStore((state) => state.refresh);
|
|
const refreshPartyBuffPolicy = usePartyBuffPolicyStore((state) => state.refresh);
|
|
|
|
useEffect(() => { void refreshCooldownPolicy(session); }, [refreshCooldownPolicy, session]);
|
|
useEffect(() => { void refreshSpecReadiness(session); }, [refreshSpecReadiness, session]);
|
|
useEffect(() => { void refreshPartyBuffPolicy(session); }, [refreshPartyBuffPolicy, session]);
|
|
|
|
useEffect(() => {
|
|
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 />;
|
|
else if (phase === "characters") screen = <Suspense fallback={<ShellTransition />}><CharacterSelectScreen /></Suspense>;
|
|
else if (phase === "create-character") screen = <Suspense fallback={<ShellTransition />}><CharacterCreateScreen /></Suspense>;
|
|
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 === "gm-admin") screen = <Suspense fallback={<ShellTransition />}><DungeonSimulationAdminScreen /></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>;
|
|
const requiresRuntimeCatalog = !["login", "characters", "create-character"].includes(phase);
|
|
return <><OnlineGroupCoordinator />{requiresRuntimeCatalog
|
|
? <RuntimeCatalogGate preloadGameplay={phase === "game"}>{screen}</RuntimeCatalogGate>
|
|
: screen}</>;
|
|
}
|