Files
Genarrative/src/components/game-shell/GameShellOverlays.tsx
高物 1c72066bab
Some checks failed
CI / verify (push) Has been cancelled
1
2026-04-20 15:45:14 +08:00

312 lines
10 KiB
TypeScript

import { AnimatePresence, motion } from 'motion/react';
import { lazy, Suspense } from 'react';
import type {
CharacterChatUi,
InventoryFlowUi,
StoryGenerationNpcUi,
} from '../../hooks/useStoryGeneration';
import type { CompanionRenderState, GameState } from '../../types';
import { CHROME_ICONS, getNineSliceStyle, UI_CHROME } from '../../uiAssets';
import type { GameCanvasEntitySelection } from '../GameCanvas';
import { PixelIcon } from '../PixelIcon';
import { ModalLoadingFallback, PanelLoadingFallback } from './GameShellLoaders';
const AdventureEntityModal = lazy(async () => {
const module = await import('../AdventureEntityModal');
return {
default: module.AdventureEntityModal,
};
});
const CharacterChatModal = lazy(async () => {
const module = await import('../CharacterChatModal');
return {
default: module.CharacterChatModal,
};
});
const CompanionCampModal = lazy(async () => {
const module = await import('../CompanionCampModal');
return {
default: module.CompanionCampModal,
};
});
const MapModal = lazy(async () => {
const module = await import('../MapModal');
return {
default: module.MapModal,
};
});
const NpcModals = lazy(async () => {
const module = await import('../NpcModals');
return {
default: module.NpcModals,
};
});
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 function GameShellOverlays({
gameState,
isLoading,
isMapOpen,
setIsMapOpen,
npcUi,
characterChatUi,
inventoryUi,
companionRenderStates,
characterChatSummaries,
overlayPanel,
closeOverlayPanel,
openCampModal,
openPartyMemberDetails,
shouldMountAdventureEntityModal,
selectedSceneEntity,
closeAdventureEntityModal,
shouldMountCampModal,
showTeamModal,
closeCampModal,
onBenchCompanion,
onActivateRosterCompanion,
shouldMountMapModal,
handleMapTravelToScene,
shouldMountCharacterChatModal,
shouldMountNpcModals,
}: {
gameState: GameState;
isLoading: boolean;
isMapOpen: boolean;
setIsMapOpen: (open: boolean) => void;
npcUi: StoryGenerationNpcUi;
characterChatUi: CharacterChatUi;
inventoryUi: InventoryFlowUi;
companionRenderStates: CompanionRenderState[];
characterChatSummaries: Record<string, string>;
overlayPanel: 'character' | 'inventory' | null;
closeOverlayPanel: () => void;
openCampModal: () => void;
openPartyMemberDetails: (selection: GameCanvasEntitySelection) => void;
shouldMountAdventureEntityModal: boolean;
selectedSceneEntity: GameCanvasEntitySelection | null;
closeAdventureEntityModal: () => void;
shouldMountCampModal: boolean;
showTeamModal: boolean;
closeCampModal: () => void;
onBenchCompanion: (npcId: string) => void;
onActivateRosterCompanion: (npcId: string, swapNpcId?: string | null) => void;
shouldMountMapModal: boolean;
handleMapTravelToScene: (sceneId: string) => boolean;
shouldMountCharacterChatModal: boolean;
shouldMountNpcModals: boolean;
}) {
return (
<>
{shouldMountAdventureEntityModal && (
<Suspense
fallback={
<ModalLoadingFallback
label="正在加载冒险详情..."
onClose={closeAdventureEntityModal}
/>
}
>
<AdventureEntityModal
selection={selectedSceneEntity}
gameState={gameState}
onClose={closeAdventureEntityModal}
onOpenCharacterChat={characterChatUi.openChat}
/>
</Suspense>
)}
<AnimatePresence>
{overlayPanel && gameState.playerCharacter && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 z-[65] flex items-center justify-center bg-black/70 p-4 backdrop-blur-sm"
onClick={closeOverlayPanel}
>
<motion.div
initial={{ opacity: 0, scale: 0.96, y: 8 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.96, y: 8 }}
transition={{ duration: 0.18, ease: 'easeOut' }}
className="pixel-nine-slice pixel-modal-shell flex max-h-[min(92vh,60rem)] w-full max-w-5xl flex-col overflow-hidden shadow-[0_24px_80px_rgba(0,0,0,0.55)]"
style={getNineSliceStyle(UI_CHROME.modalPanel)}
onClick={(event) => event.stopPropagation()}
>
<div className="relative border-b border-white/10 px-4 py-3 sm:px-5 sm:py-4">
<div className="min-w-0 pr-10 text-sm font-semibold text-white">
{overlayPanel === 'character' ? '队伍' : '背包'}
</div>
<button
type="button"
onClick={closeOverlayPanel}
className="absolute right-4 top-3 p-1 text-zinc-400 transition-colors hover:text-white sm:right-5 sm:top-4"
>
<PixelIcon src={CHROME_ICONS.close} className="h-4 w-4" />
</button>
</div>
<div className="flex min-h-0 flex-1 p-5">
{overlayPanel === 'character' ? (
<Suspense
fallback={<PanelLoadingFallback label="正在加载队伍面板" />}
>
<CharacterPanel
worldType={gameState.worldType}
customWorldProfile={gameState.customWorldProfile}
playerCharacter={gameState.playerCharacter}
playerProgression={gameState.playerProgression ?? null}
playerHp={gameState.playerHp}
playerMaxHp={gameState.playerMaxHp}
playerMana={gameState.playerMana}
playerMaxMana={gameState.playerMaxMana}
playerEquipment={gameState.playerEquipment}
activeBuildBuffs={gameState.activeBuildBuffs}
companionRenderStates={companionRenderStates}
npcStates={gameState.npcStates}
quests={gameState.quests}
onOpenCamp={() => {
closeOverlayPanel();
openCampModal();
}}
onOpenCharacterChat={(target) => {
closeOverlayPanel();
characterChatUi.openChat(target);
}}
chatSummaries={characterChatSummaries}
onInspectMember={openPartyMemberDetails}
/>
</Suspense>
) : (
<Suspense
fallback={<PanelLoadingFallback label="正在加载背包面板" />}
>
<InventoryPanel
playerCharacter={gameState.playerCharacter}
worldType={gameState.worldType}
playerInventory={gameState.playerInventory}
playerCurrency={gameState.playerCurrency}
playerHp={gameState.playerHp}
playerMaxHp={gameState.playerMaxHp}
playerMana={gameState.playerMana}
playerMaxMana={gameState.playerMaxMana}
inBattle={gameState.inBattle}
onUseItem={inventoryUi.useInventoryItem}
onEquipItem={inventoryUi.equipInventoryItem}
forgeRecipes={inventoryUi.forgeRecipes}
onCraftRecipe={inventoryUi.craftRecipe}
onDismantleItem={inventoryUi.dismantleItem}
onReforgeItem={inventoryUi.reforgeItem}
/>
</Suspense>
)}
</div>
</motion.div>
</motion.div>
)}
</AnimatePresence>
{shouldMountCampModal && (
<Suspense
fallback={
<ModalLoadingFallback
label="正在加载队伍营地..."
onClose={closeCampModal}
/>
}
>
<CompanionCampModal
isOpen={showTeamModal}
playerCharacter={gameState.playerCharacter}
companions={gameState.companions}
roster={gameState.roster}
inBattle={gameState.inBattle}
onClose={closeCampModal}
onBenchCompanion={onBenchCompanion}
onActivateCompanion={onActivateRosterCompanion}
/>
</Suspense>
)}
{shouldMountMapModal && (
<Suspense
fallback={
<ModalLoadingFallback
label="正在加载地图..."
onClose={() => setIsMapOpen(false)}
/>
}
>
<MapModal
isOpen={isMapOpen}
currentScenePreset={gameState.currentScenePreset}
worldType={gameState.worldType}
canTravel={!gameState.inBattle && !isLoading}
onTravelToScene={(scene) => {
const triggered = handleMapTravelToScene(scene.id);
if (triggered) {
setIsMapOpen(false);
}
}}
isTraveling={isLoading}
onClose={() => setIsMapOpen(false)}
/>
</Suspense>
)}
{shouldMountCharacterChatModal && (
<Suspense
fallback={
<ModalLoadingFallback
label="正在加载角色聊天..."
onClose={characterChatUi.closeChat}
/>
}
>
<CharacterChatModal
modal={characterChatUi.modal}
onClose={characterChatUi.closeChat}
onDraftChange={characterChatUi.setDraft}
onUseSuggestion={characterChatUi.useSuggestion}
onRefreshSuggestions={characterChatUi.refreshSuggestions}
onSendDraft={characterChatUi.sendDraft}
/>
</Suspense>
)}
{shouldMountNpcModals && (
<Suspense
fallback={<ModalLoadingFallback label="正在加载角色交互..." />}
>
<NpcModals gameState={gameState} npcUi={npcUi} />
</Suspense>
)}
</>
);
}