This commit is contained in:
291
src/components/rpg-runtime-shell/RpgRuntimeShell.tsx
Normal file
291
src/components/rpg-runtime-shell/RpgRuntimeShell.tsx
Normal file
@@ -0,0 +1,291 @@
|
||||
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,
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* RPG 运行态总外壳。
|
||||
* 这里承接运行时主布局、画布舞台、主阶段路由和 overlay host,
|
||||
* 保持原有 UI 结构不变,只把真实实现迁入 RPG 域目录。
|
||||
*/
|
||||
export function RpgRuntimeShell({
|
||||
session,
|
||||
story,
|
||||
entry,
|
||||
companions,
|
||||
audio,
|
||||
chrome,
|
||||
}: 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,
|
||||
),
|
||||
);
|
||||
|
||||
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 (
|
||||
<div
|
||||
className={`${isPlatformShell ? `platform-ui-shell platform-theme ${platformThemeClass} text-[var(--platform-text-strong)]` : 'fusion-pixel-app pixel-root-shell text-zinc-100'} flex h-screen max-h-screen flex-col overflow-hidden font-sans`}
|
||||
style={{
|
||||
backgroundImage: isPlatformShell
|
||||
? 'var(--platform-body-fill)'
|
||||
: `linear-gradient(rgba(8, 10, 14, 0.82), rgba(8, 10, 14, 0.82)), url("${UI_CHROME.appBackground}")`,
|
||||
backgroundPosition: isPlatformShell ? undefined : 'center',
|
||||
backgroundRepeat: isPlatformShell ? undefined : 'repeat',
|
||||
}}
|
||||
>
|
||||
{gameState.worldType ? (
|
||||
<Suspense fallback={null}>
|
||||
<RpgRuntimeCanvasStage
|
||||
gameState={gameState}
|
||||
visibleGameState={visibleGameState}
|
||||
hideSelectionHero={hideSelectionHero}
|
||||
canvasCompanionRenderStates={canvasCompanionRenderStates}
|
||||
dialogueIndicator={dialogueIndicator}
|
||||
sceneTransitionPhase={sceneTransitionPhase}
|
||||
sceneTransitionToken={sceneTransitionToken}
|
||||
setSelectedSceneEntity={setSelectedSceneEntity}
|
||||
setIsMapOpen={setIsMapOpen}
|
||||
setSceneTransitionDurations={setSceneTransitionDurations}
|
||||
/>
|
||||
</Suspense>
|
||||
) : null}
|
||||
|
||||
{visibleGameState.playerCharacter && !chrome?.hidePlayerLevelBadge && (
|
||||
<div
|
||||
className="pointer-events-none fixed z-[26] w-[4.5rem] drop-shadow-[0_2px_8px_rgba(0,0,0,0.75)]"
|
||||
style={{
|
||||
top: 'calc(env(safe-area-inset-top, 0px) + 0.65rem)',
|
||||
left: 'calc(env(safe-area-inset-left, 0px) + 0.7rem)',
|
||||
}}
|
||||
>
|
||||
<div className="flex items-end gap-1.5 text-amber-50">
|
||||
<span className="text-[10px] font-semibold uppercase leading-none tracking-[0.14em] text-amber-100/80">
|
||||
Lv
|
||||
</span>
|
||||
<span className="text-2xl font-black leading-none tracking-[-0.08em] text-white">
|
||||
{playerProgression.level}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-1 h-1 overflow-hidden rounded-full bg-black/45">
|
||||
<div
|
||||
className="h-full rounded-full bg-[linear-gradient(90deg,rgba(251,191,36,0.65),rgba(254,240,138,0.95))]"
|
||||
style={{
|
||||
width:
|
||||
playerProgressionRatio <= 0
|
||||
? '0%'
|
||||
: `${Math.max(8, playerProgressionRatio * 100)}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<RpgRuntimeStageRouter
|
||||
gameState={gameState}
|
||||
visibleGameState={visibleGameState}
|
||||
visibleCurrentStory={visibleCurrentStory}
|
||||
isLoading={isLoading}
|
||||
aiError={aiError}
|
||||
bottomTab={bottomTab}
|
||||
setBottomTab={setBottomTab}
|
||||
selectionStage={selectionStage}
|
||||
setSelectionStage={setSelectionStage}
|
||||
isCharacterSelectionStage={isCharacterSelectionStage}
|
||||
hasSavedGame={hasSavedGame}
|
||||
savedSnapshot={savedSnapshot}
|
||||
handleContinueGame={handleContinueGame}
|
||||
handleStartNewGame={handleStartNewGame}
|
||||
handleCustomWorldSelect={handleCustomWorldSelect}
|
||||
handleBackToWorldSelect={handleBackToWorldSelect}
|
||||
handleCharacterSelect={handleCharacterSelect}
|
||||
displayedOptions={displayedOptions}
|
||||
hideStoryOptions={shouldHideStoryOptions}
|
||||
canRefreshOptions={canRefreshOptions}
|
||||
handleRefreshOptions={handleRefreshOptions}
|
||||
refreshNpcChatOptions={refreshNpcChatOptions}
|
||||
handleSceneTransitionChoice={handleSceneTransitionChoice}
|
||||
handleNpcChatInput={handleNpcChatInput}
|
||||
exitNpcChat={exitNpcChat}
|
||||
characterChatUi={characterChatUi}
|
||||
inventoryUi={inventoryUi}
|
||||
battleRewardUi={battleRewardUi}
|
||||
questUi={questUi}
|
||||
npcChatQuestOfferUi={npcChatQuestOfferUi}
|
||||
goalUi={goalUi}
|
||||
companionRenderStates={companionRenderStates}
|
||||
characterChatSummaries={characterChatSummaries}
|
||||
openOverlayPanel={openOverlayPanel}
|
||||
openCampModal={openCampModal}
|
||||
openPartyMemberDetails={openPartyMemberDetails}
|
||||
adventureStatistics={adventureStatistics}
|
||||
musicVolume={musicVolume}
|
||||
onMusicVolumeChange={onMusicVolumeChange}
|
||||
resetForSaveAndExit={resetForSaveAndExit}
|
||||
handleSaveAndExit={handleSaveAndExit}
|
||||
/>
|
||||
|
||||
{gameState.worldType ? (
|
||||
<Suspense fallback={null}>
|
||||
<RpgRuntimeOverlayHost
|
||||
gameState={gameState}
|
||||
isLoading={isLoading}
|
||||
isMapOpen={isMapOpen}
|
||||
setIsMapOpen={setIsMapOpen}
|
||||
npcUi={npcUi}
|
||||
characterChatUi={characterChatUi}
|
||||
inventoryUi={inventoryUi}
|
||||
companionRenderStates={companionRenderStates}
|
||||
characterChatSummaries={characterChatSummaries}
|
||||
overlayPanel={overlayPanel}
|
||||
closeOverlayPanel={closeOverlayPanel}
|
||||
openCampModal={openCampModal}
|
||||
openPartyMemberDetails={openPartyMemberDetails}
|
||||
shouldMountAdventureEntityModal={shouldMountAdventureEntityModal}
|
||||
selectedSceneEntity={selectedSceneEntity}
|
||||
closeAdventureEntityModal={closeAdventureEntityModal}
|
||||
shouldMountCampModal={shouldMountCampModal}
|
||||
showTeamModal={showTeamModal}
|
||||
closeCampModal={closeCampModal}
|
||||
onBenchCompanion={onBenchCompanion}
|
||||
onActivateRosterCompanion={onActivateRosterCompanion}
|
||||
shouldMountMapModal={shouldMountMapModal}
|
||||
handleMapTravelToScene={handleMapTravelToScene}
|
||||
shouldMountCharacterChatModal={shouldMountCharacterChatModal}
|
||||
shouldMountNpcModals={shouldMountNpcModals}
|
||||
/>
|
||||
</Suspense>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export type RpgRuntimeShellProps = RpgRuntimeShellComponentProps;
|
||||
|
||||
export default RpgRuntimeShell;
|
||||
Reference in New Issue
Block a user