312 lines
10 KiB
TypeScript
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>
|
|
)}
|
|
</>
|
|
);
|
|
}
|