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 (
);
}
/**
* 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;