import { lazy, Suspense, useCallback, useEffect, useRef, useState, } from 'react'; import { useAuthUi } from './components/auth/AuthUiContext'; import { PlatformEntryFlowShell } from './components/platform-entry/PlatformEntryFlowShell'; import { getInitialPlatformDesktopLayout } from './components/platform-entry/platformEntryResponsive'; import type { CustomWorldRuntimeLaunchOptions, SelectionStage, } from './components/platform-entry/platformEntryTypes'; import { useHostNavigationCanGoBack } from './hooks/useHostNavigationCanGoBack'; import type { HydratedSavedGameSnapshot } from './persistence/runtimeSnapshotTypes'; import { APP_RUNTIME_ROUTES, isAppHistoryState, normalizeAppPath, pushAppHistoryPath, readPublicWorkCodeFromLocationSearch, replaceAppHistoryPath, resolveInitialSelectionStageFromPath, resolvePathForSelectionStage, shouldRedirectEditorCanvasWithoutProject, } from './routing/appPageRoutes'; import type { RpgRuntimeAppIntent } from './RpgRuntimeApp'; import { resolveAppTitleForSelectionStage, syncAppTitle, } from './services/appTitle'; import { refreshNativeAppHostRuntime, subscribeHostRuntimeChange, } from './services/host-bridge/hostBridge'; import type { CustomWorldProfile } from './types'; const RpgRuntimeApp = lazy(async () => { const module = await import('./RpgRuntimeApp'); return { default: module.RpgRuntimeApp, }; }); function RuntimeLoadingFallback() { return (
正在启动
); } function isRpgRuntimeRoute(pathname: string) { const normalizedPath = normalizeAppPath(pathname); return ( normalizedPath === APP_RUNTIME_ROUTES['rpg-character-select'] || normalizedPath === APP_RUNTIME_ROUTES['rpg-adventure'] ); } function resolveInitialAppSelectionStage() { if ( shouldRedirectEditorCanvasWithoutProject( window.location.pathname, window.location.search, ) ) { replaceAppHistoryPath('/creation'); return 'creation-home'; } return resolveInitialSelectionStageFromPath( window.location.pathname, getInitialPlatformDesktopLayout(), ); } export default function App() { const authUi = useAuthUi(); const runtimeIntentTokenRef = useRef(0); const hasHostNavigationAnchorRef = useRef( isAppHistoryState(window.history.state), ); const hostNavigation = useHostNavigationCanGoBack(); const [runtimeIntent, setRuntimeIntent] = useState(null); const [, setHostRuntimeRevision] = useState(0); const [isRuntimeActive, setIsRuntimeActive] = useState(() => isRpgRuntimeRoute(window.location.pathname), ); const [selectionStage, setRawSelectionStage] = useState( resolveInitialAppSelectionStage, ); const [runtimeReturnStage, setRuntimeReturnStage] = useState('platform'); const [initialPublicWorkCode] = useState(() => readPublicWorkCodeFromLocationSearch(window.location.search), ); const setSelectionStage = useCallback( (stage: SelectionStage, options?: { path?: string }) => { setRawSelectionStage(stage); pushAppHistoryPath(options?.path ?? resolvePathForSelectionStage(stage)); }, [], ); useEffect(() => { const unsubscribe = subscribeHostRuntimeChange(() => { setHostRuntimeRevision((revision) => revision + 1); }); void refreshNativeAppHostRuntime(); return unsubscribe; }, []); useEffect(() => { const syncStageFromHistory = () => { hasHostNavigationAnchorRef.current = isAppHistoryState( window.history.state, ); if ( shouldRedirectEditorCanvasWithoutProject( window.location.pathname, window.location.search, ) ) { replaceAppHistoryPath('/creation'); setIsRuntimeActive(false); setRawSelectionStage('creation-home'); return; } if (isRpgRuntimeRoute(window.location.pathname)) { setIsRuntimeActive(true); return; } setIsRuntimeActive(false); setRawSelectionStage( resolveInitialSelectionStageFromPath( window.location.pathname, getInitialPlatformDesktopLayout(), ), ); }; window.addEventListener('popstate', syncStageFromHistory); return () => window.removeEventListener('popstate', syncStageFromHistory); }, []); useEffect(() => { if ( !hostNavigation.isSupported || hostNavigation.canGoBack || isRuntimeActive || selectionStage === 'platform' || isAppHistoryState(window.history.state) || hasHostNavigationAnchorRef.current ) { return; } const currentPath = normalizeAppPath(window.location.pathname); const currentSearch = window.location.search; hasHostNavigationAnchorRef.current = true; replaceAppHistoryPath('/'); pushAppHistoryPath(`${currentPath}${currentSearch}`); }, [ hostNavigation.canGoBack, hostNavigation.isSupported, isRuntimeActive, selectionStage, ]); const createRuntimeIntent = useCallback( (intent: Omit) => { runtimeIntentTokenRef.current += 1; setRuntimeIntent({ ...intent, token: runtimeIntentTokenRef.current, }); setIsRuntimeActive(true); }, [], ); const handleContinueGame = useCallback( (snapshot?: HydratedSavedGameSnapshot | null) => { createRuntimeIntent({ kind: 'snapshot', snapshot: snapshot ?? null, }); }, [createRuntimeIntent], ); const handleCustomWorldSelect = useCallback( ( customWorldProfile: CustomWorldProfile, options?: CustomWorldRuntimeLaunchOptions, ) => { // 中文注释:作品测试需要在结束测试后精确返回启动它的结果页; // 正式进入世界仍保持既有平台首页返回语义。 setRuntimeReturnStage(options?.returnStage ?? 'platform'); createRuntimeIntent({ kind: 'custom-world', profile: customWorldProfile, mode: options?.mode ?? 'play', disablePersistence: options?.disablePersistence, exitToResult: options?.returnStage === 'custom-world-result', }); }, [createRuntimeIntent], ); const platformThemeClass = authUi?.platformTheme === 'dark' ? 'platform-theme--dark' : 'platform-theme--light'; const isImageEditorStage = selectionStage === 'image-editor'; const platformShellSurfaceClass = isImageEditorStage ? 'bg-white p-0' : 'bg-[image:var(--platform-body-fill)] p-2 sm:p-4'; useEffect(() => { syncAppTitle( isRuntimeActive ? 'RPG 运行中 - 陶泥儿' : resolveAppTitleForSelectionStage(selectionStage), ); }, [isRuntimeActive, selectionStage]); if (isRuntimeActive) { return ( }> { setIsRuntimeActive(false); setSelectionStage(runtimeReturnStage); }} /> ); } return (
{}} handleCustomWorldSelect={handleCustomWorldSelect} />
); }