import { lazy, Suspense, useEffect } from 'react'; import { normalizePlayerProgressionState } from '../../data/playerProgression'; import { APP_RUNTIME_ROUTES, pushAppHistoryPath, } from '../../routing/appPageRoutes'; import { UI_CHROME } from '../../uiAssets'; import { useAuthUi } from '../auth/AuthUiContext'; import { RpgRuntimeStageRouter } from './RpgRuntimeStageRouter'; import type { RpgRuntimeShellProps as RpgRuntimeShellComponentProps } from './types'; import { useRpgRuntimeShellViewModel } from './useRpgRuntimeShellViewModel'; const RpgRuntimeCanvasStage = lazy(async () => { const module = await import('./RpgRuntimeCanvasStage'); return { default: module.RpgRuntimeCanvasStage, }; }); const RpgRuntimeOverlayHost = lazy(async () => { const module = await import('./RpgRuntimeOverlayHost'); return { default: module.RpgRuntimeOverlayHost, }; }); function RuntimeLayerLoadingFallback({ label }: { label: string }) { return (
{label}
); } /** * RPG 运行态总外壳。 * 这里承接运行时主布局、画布舞台、主阶段路由和 overlay host, * 保持原有 UI 结构不变,只把真实实现迁入 RPG 域目录。 */ export function RpgRuntimeShell({ session, story, entry, companions, audio, chrome, onExitRuntimePreview, showRuntimePreviewExit, }: RpgRuntimeShellComponentProps) { const authUi = useAuthUi(); const isPlatformShell = !session.gameState.worldType; const platformThemeClass = authUi?.platformTheme === 'dark' ? 'platform-theme--dark' : 'platform-theme--light'; const { gameState, isLoading, aiError, bottomTab, setBottomTab, isMapOpen, setIsMapOpen, } = session; const { displayedOptions, canRefreshOptions, handleRefreshOptions, handleNpcChatInput, refreshNpcChatOptions, exitNpcChat, handleMapTravelToScene, npcUi, characterChatUi, inventoryUi, battleRewardUi, questUi, npcChatQuestOfferUi, goalUi, } = story; const { hasSavedGame, savedSnapshot, handleContinueGame, handleStartNewGame, handleSaveAndExit, handleCustomWorldSelect, handleBackToWorldSelect, handleCharacterSelect, } = entry; const { companionRenderStates, onBenchCompanion, onActivateRosterCompanion } = companions; const { musicVolume, onMusicVolumeChange } = audio; const { selectionStage, setSelectionStage, overlayPanel, openOverlayPanel, closeOverlayPanel, selectedSceneEntity, setSelectedSceneEntity, openPartyMemberDetails, closeAdventureEntityModal, showTeamModal, openCampModal, closeCampModal, resetForSaveAndExit, shouldMountAdventureEntityModal, shouldMountCampModal, shouldMountMapModal, shouldMountCharacterChatModal, shouldMountNpcModals, visibleGameState, visibleCurrentStory, sceneTransitionPhase, sceneTransitionToken, setSceneTransitionDurations, isCharacterSelectionStage, shouldHideStoryOptions, hideSelectionHero, dialogueIndicator, characterChatSummaries, canvasCompanionRenderStates, adventureStatistics, handleSceneTransitionChoice, } = useRpgRuntimeShellViewModel({ session, story, companions, }); const playerProgression = normalizePlayerProgressionState( visibleGameState.playerProgression ?? null, ); const playerProgressionRatio = playerProgression.xpToNextLevel <= 0 ? 1 : Math.max( 0, Math.min( 1, playerProgression.currentLevelXp / playerProgression.xpToNextLevel, ), ); const canExitRuntimePreview = Boolean(gameState.worldType) && Boolean(showRuntimePreviewExit) && Boolean(onExitRuntimePreview); useEffect(() => { if (gameState.worldType && !gameState.playerCharacter) { pushAppHistoryPath(APP_RUNTIME_ROUTES['rpg-character-select']); return; } if (visibleGameState.playerCharacter && visibleCurrentStory) { pushAppHistoryPath(APP_RUNTIME_ROUTES['rpg-adventure']); } }, [ gameState.playerCharacter, gameState.worldType, visibleCurrentStory, visibleGameState.playerCharacter, ]); return (
{gameState.worldType ? ( }> ) : null} {visibleGameState.playerCharacter && !chrome?.hidePlayerLevelBadge && (
Lv {playerProgression.level}
)} {canExitRuntimePreview ? (
) : null} {gameState.worldType ? ( }> ) : null}
); } export type RpgRuntimeShellProps = RpgRuntimeShellComponentProps; export default RpgRuntimeShell;