Files
Genarrative/src/components/rpg-runtime-panels/RpgRuntimePanelRouter.tsx
2026-04-29 20:56:59 +08:00

308 lines
11 KiB
TypeScript

import { lazy, Suspense } from 'react';
import type {
BattleRewardUi,
CharacterChatUi,
GoalFlowUi,
InventoryFlowUi,
NpcChatQuestOfferUi,
QuestFlowUi,
} from '../../hooks/rpg-runtime-story';
import type { BottomTab } from '../../hooks/rpg-session';
import type {
CompanionRenderState,
GameState,
StoryMoment,
StoryOption,
} from '../../types';
import { getNineSliceStyle, TAB_ICONS, UI_CHROME } from '../../uiAssets';
import type { GameCanvasEntitySelection } from '../GameCanvas';
import { PixelIcon } from '../PixelIcon';
import { PanelLoadingFallback } from '../rpg-runtime-shell/rpgRuntimeLoaders';
import type { RpgAdventureStatistics } from '../rpg-runtime-shell/types';
const RpgAdventurePanel = lazy(async () => {
const module = await import('./RpgAdventurePanel');
return {
default: module.RpgAdventurePanel,
};
});
const CharacterPanel = lazy(async () => {
const module = await import('../CharacterPanel');
return {
default: module.CharacterPanel,
};
});
const InventoryPanel = lazy(async () => {
const module = await import('../InventoryPanel');
return {
default: module.InventoryPanel,
};
});
export interface RpgRuntimePanelRouterProps {
visibleGameState: GameState;
visibleCurrentStory: StoryMoment;
isLoading: boolean;
aiError: string | null;
bottomTab: BottomTab;
setBottomTab: (tab: BottomTab) => void;
displayedOptions: StoryOption[];
hideStoryOptions: boolean;
canRefreshOptions: boolean;
handleRefreshOptions: () => void;
refreshNpcChatOptions: () => boolean;
handleSceneTransitionChoice: (option: StoryOption) => void;
handleNpcChatInput: (input: string) => boolean;
exitNpcChat: () => boolean;
characterChatUi: CharacterChatUi;
inventoryUi: InventoryFlowUi;
battleRewardUi: BattleRewardUi;
questUi: QuestFlowUi;
npcChatQuestOfferUi: NpcChatQuestOfferUi;
goalUi: GoalFlowUi;
companionRenderStates: CompanionRenderState[];
characterChatSummaries: Record<string, string>;
openOverlayPanel: (panel: 'character' | 'inventory') => void;
openCampModal: () => void;
openPartyMemberDetails: (selection: GameCanvasEntitySelection) => void;
adventureStatistics: RpgAdventureStatistics;
musicVolume: number;
onMusicVolumeChange: (value: number) => void;
onSaveAndExit: () => void;
}
/**
* RPG 运行态主面板路由器。
* 只负责冒险 / 角色 / 背包三个主标签的切换和装配。
*/
export function RpgRuntimePanelRouter({
visibleGameState,
visibleCurrentStory,
isLoading,
aiError,
bottomTab,
setBottomTab,
displayedOptions,
hideStoryOptions,
canRefreshOptions,
handleRefreshOptions,
refreshNpcChatOptions,
handleSceneTransitionChoice,
handleNpcChatInput,
exitNpcChat,
characterChatUi,
inventoryUi,
battleRewardUi,
questUi,
npcChatQuestOfferUi,
goalUi,
companionRenderStates,
characterChatSummaries,
openOverlayPanel,
openCampModal,
openPartyMemberDetails,
adventureStatistics,
musicVolume,
onMusicVolumeChange,
onSaveAndExit,
}: RpgRuntimePanelRouterProps) {
const playerCharacter = visibleGameState.playerCharacter;
if (!playerCharacter) {
return null;
}
return (
<>
<div className="story-top-tabs mb-3 grid grid-cols-3 gap-2 sm:gap-3">
<button
onClick={() => setBottomTab('character')}
className={`pixel-nine-slice pixel-pressable pixel-tab-button ${bottomTab === 'character' ? 'pixel-tab-button--active text-white' : 'text-zinc-300'}`}
style={getNineSliceStyle(
bottomTab === 'character'
? UI_CHROME.tabActive
: UI_CHROME.tabInactive,
{ paddingX: 10, paddingY: 8 },
)}
>
<span className="pixel-tab-button__inner">
<PixelIcon
src={
bottomTab === 'character'
? TAB_ICONS.character.active
: TAB_ICONS.character.inactive
}
className={`pixel-tab-button__icon ${bottomTab === 'character' ? 'opacity-100' : 'opacity-70'}`}
/>
<span className="pixel-tab-button__label"></span>
</span>
</button>
<button
onClick={() => setBottomTab('adventure')}
className={`pixel-nine-slice pixel-pressable pixel-tab-button ${bottomTab === 'adventure' ? 'pixel-tab-button--active text-white' : 'text-zinc-300'}`}
style={getNineSliceStyle(
bottomTab === 'adventure'
? UI_CHROME.tabActive
: UI_CHROME.tabInactive,
{ paddingX: 10, paddingY: 8 },
)}
>
<span className="pixel-tab-button__inner">
<PixelIcon
src={
bottomTab === 'adventure'
? TAB_ICONS.adventure.active
: TAB_ICONS.adventure.inactive
}
className={`pixel-tab-button__icon ${bottomTab === 'adventure' ? 'opacity-100' : 'opacity-70'}`}
/>
<span className="pixel-tab-button__label"></span>
</span>
</button>
<button
onClick={() => setBottomTab('inventory')}
className={`pixel-nine-slice pixel-pressable pixel-tab-button ${bottomTab === 'inventory' ? 'pixel-tab-button--active text-white' : 'text-zinc-300'}`}
style={getNineSliceStyle(
bottomTab === 'inventory'
? UI_CHROME.tabActive
: UI_CHROME.tabInactive,
{ paddingX: 10, paddingY: 8 },
)}
>
<span className="pixel-tab-button__inner">
<PixelIcon
src={
bottomTab === 'inventory'
? TAB_ICONS.inventory.active
: TAB_ICONS.inventory.inactive
}
className={`pixel-tab-button__icon ${bottomTab === 'inventory' ? 'opacity-100' : 'opacity-70'}`}
/>
<span className="pixel-tab-button__label"></span>
</span>
</button>
</div>
{bottomTab === 'character' && (
<Suspense fallback={<PanelLoadingFallback label="正在加载角色面板" />}>
<CharacterPanel
worldType={visibleGameState.worldType}
customWorldProfile={visibleGameState.customWorldProfile}
playerCharacter={playerCharacter}
playerProgression={visibleGameState.playerProgression ?? null}
playerHp={visibleGameState.playerHp}
playerMaxHp={visibleGameState.playerMaxHp}
playerMana={visibleGameState.playerMana}
playerMaxMana={visibleGameState.playerMaxMana}
playerEquipment={visibleGameState.playerEquipment}
activeBuildBuffs={visibleGameState.activeBuildBuffs}
companionRenderStates={companionRenderStates}
npcStates={visibleGameState.npcStates}
companionArcStates={
visibleGameState.storyEngineMemory?.companionArcStates ?? []
}
companionResolutions={
visibleGameState.storyEngineMemory?.companionResolutions ?? []
}
onOpenCamp={openCampModal}
onOpenCharacterChat={characterChatUi.openChat}
chatSummaries={characterChatSummaries}
onInspectMember={openPartyMemberDetails}
/>
</Suspense>
)}
{bottomTab === 'adventure' && (
<Suspense fallback={<PanelLoadingFallback label="正在加载冒险面板" />}>
<RpgAdventurePanel
aiError={aiError}
currentStory={visibleCurrentStory}
isLoading={isLoading}
displayedOptions={displayedOptions}
hideOptions={hideStoryOptions}
canRefreshOptions={
visibleCurrentStory.npcChatState
? visibleCurrentStory.options.length > 1
: canRefreshOptions
}
onRefreshOptions={() => {
if (visibleCurrentStory.npcChatState) {
refreshNpcChatOptions();
return;
}
handleRefreshOptions();
}}
onChoice={handleSceneTransitionChoice}
onSubmitNpcChatInput={handleNpcChatInput}
onExitNpcChat={exitNpcChat}
onOpenCharacter={() => openOverlayPanel('character')}
onOpenInventory={() => openOverlayPanel('inventory')}
playerCharacter={playerCharacter}
worldType={visibleGameState.worldType}
quests={visibleGameState.quests}
questUi={questUi}
npcChatQuestOfferUi={npcChatQuestOfferUi}
goalStack={goalUi.goalStack}
goalPulse={goalUi.pulse}
onDismissGoalPulse={goalUi.dismissPulse}
battleRewardUi={battleRewardUi}
playerHp={visibleGameState.playerHp}
playerMaxHp={visibleGameState.playerMaxHp}
playerMana={visibleGameState.playerMana}
playerMaxMana={visibleGameState.playerMaxMana}
playerSkillCooldowns={visibleGameState.playerSkillCooldowns}
inBattle={visibleGameState.inBattle}
currentNpcBattleMode={visibleGameState.currentNpcBattleMode}
chapterState={visibleGameState.chapterState ?? null}
journeyBeat={
visibleGameState.storyEngineMemory?.currentJourneyBeat ?? null
}
statistics={adventureStatistics}
musicVolume={musicVolume}
onMusicVolumeChange={onMusicVolumeChange}
onSaveAndExit={onSaveAndExit}
/>
</Suspense>
)}
{bottomTab === 'inventory' && (
<Suspense fallback={<PanelLoadingFallback label="正在加载背包面板" />}>
<InventoryPanel
playerCharacter={playerCharacter}
worldType={visibleGameState.worldType}
playerInventory={visibleGameState.playerInventory}
playerCurrency={visibleGameState.playerCurrency}
playerHp={visibleGameState.playerHp}
playerMaxHp={visibleGameState.playerMaxHp}
playerMana={visibleGameState.playerMana}
playerMaxMana={visibleGameState.playerMaxMana}
inBattle={visibleGameState.inBattle}
currencyText={inventoryUi.currencyText}
backpackItems={inventoryUi.backpackItems}
equipmentSlots={inventoryUi.equipmentSlots}
onUseItem={inventoryUi.useInventoryItem}
onEquipItem={inventoryUi.equipInventoryItem}
forgeRecipes={inventoryUi.forgeRecipes}
onCraftRecipe={inventoryUi.craftRecipe}
onDismantleItem={inventoryUi.dismantleItem}
onReforgeItem={inventoryUi.reforgeItem}
narrativeCodex={
visibleGameState.storyEngineMemory?.narrativeCodex ?? []
}
narrativeQaReport={
visibleGameState.storyEngineMemory?.narrativeQaReport ?? null
}
/>
</Suspense>
)}
</>
);
}
export default RpgRuntimePanelRouter;