import { AnimatePresence, motion } from 'motion/react'; import { type CSSProperties, useEffect, useMemo, useState } from 'react'; import { getCustomWorldSceneRelativePositionLabel } from '../data/customWorldSceneGraph'; import { getConnectedScenePresets } from '../data/scenePresets'; import { useResolvedAssetReadUrl } from '../hooks/useResolvedAssetReadUrl'; import { ScenePresetInfo, WorldType } from '../types'; import { CHROME_ICONS, getNineSliceStyle, UI_CHROME } from '../uiAssets'; import { PixelCloseButton } from './PixelCloseButton'; import { PixelIcon } from './PixelIcon'; function buildSceneBackdropStyle(imageSrc?: string | null): CSSProperties { return { backgroundImage: imageSrc ? `linear-gradient(rgba(7,10,18,0.82), rgba(7,10,18,0.76)), url("${imageSrc}")` : 'linear-gradient(rgba(7,10,18,0.82), rgba(7,10,18,0.76))', backgroundSize: 'cover', backgroundPosition: 'center', backgroundRepeat: 'no-repeat', }; } const MAP_NODE_MIN_HEIGHT_PX = 52; const MAP_NODE_GAP_PX = 12; function getMapDestinationStackHeight(count: number) { if (count <= 0) return MAP_NODE_MIN_HEIGHT_PX; return count * MAP_NODE_MIN_HEIGHT_PX + (count - 1) * MAP_NODE_GAP_PX; } function getMapDestinationCenterPercent(index: number, count: number) { const totalHeight = getMapDestinationStackHeight(count); const centerY = index * (MAP_NODE_MIN_HEIGHT_PX + MAP_NODE_GAP_PX) + MAP_NODE_MIN_HEIGHT_PX / 2; return (centerY / totalHeight) * 100; } function MudMapRoom({ scene, label, compact = false, isInteractive = false, isSelected = false, description, onClick, }: { key?: string; scene: ScenePresetInfo | null | undefined; label: string; compact?: boolean; isInteractive?: boolean; isSelected?: boolean; description?: string; onClick?: (() => void) | null; }) { if (!scene) { return (
); } const content = (
{label}
{scene.name}
{!compact && description ? (
{description}
) : null}
); if (!isInteractive || !onClick) { return content; } return ( ); } interface MapModalProps { isOpen: boolean; currentScenePreset: ScenePresetInfo | null; worldType: WorldType | null; onClose: () => void; onTravelToScene: (scene: ScenePresetInfo) => void; isTraveling?: boolean; canTravel?: boolean; } type MapConnectionEntry = { scene: ScenePresetInfo; label: string; summary: string; }; function isMapConnectionEntry( entry: MapConnectionEntry | null, ): entry is MapConnectionEntry { return entry !== null; } function buildFallbackConnectionEntries( currentScenePreset: ScenePresetInfo | null, connectedScenes: ScenePresetInfo[], ) { const forwardSceneId = currentScenePreset?.forwardSceneId; const forwardScene = connectedScenes.find((scene) => scene.id === forwardSceneId) ?? null; const branchScenes = connectedScenes.filter((scene) => scene.id !== forwardSceneId); const fallbackEntries: Array = [ forwardScene ? ({ scene: forwardScene, label: '前方', summary: '沿主路继续深入', } satisfies MapConnectionEntry) : null, ...branchScenes.map( (scene, index) => ({ scene, label: index === 0 ? '支路左侧' : index === 1 ? '支路右侧' : `支路${index + 1}`, summary: '可转向另一片区域', }) satisfies MapConnectionEntry, ), ]; return fallbackEntries.filter(isMapConnectionEntry); } export function MapModal({ isOpen, currentScenePreset, worldType, onClose, onTravelToScene, isTraveling = false, canTravel = true, }: MapModalProps) { const [pendingScene, setPendingScene] = useState(null); const { resolvedUrl: resolvedBackdropImageSrc, shouldResolve: shouldResolveBackdropImage, } = useResolvedAssetReadUrl(currentScenePreset?.imageSrc); const connectedScenes = useMemo( () => worldType && currentScenePreset ? getConnectedScenePresets(worldType, currentScenePreset.id) : [], [currentScenePreset, worldType], ); const connectionEntries = useMemo(() => { if (currentScenePreset?.connections?.length) { const entries: Array = currentScenePreset.connections .map((connection) => { const scene = connectedScenes.find( (item) => item.id === connection.sceneId, ); if (!scene) { return null; } return { scene, label: getCustomWorldSceneRelativePositionLabel( connection.relativePosition, ), summary: connection.summary, } satisfies MapConnectionEntry; }); return entries.filter(isMapConnectionEntry); } return buildFallbackConnectionEntries(currentScenePreset, connectedScenes); }, [connectedScenes, currentScenePreset]); const sceneBackdropStyle = buildSceneBackdropStyle( resolvedBackdropImageSrc || (!shouldResolveBackdropImage ? currentScenePreset?.imageSrc : ''), ); const destinationStackHeightPx = getMapDestinationStackHeight(connectionEntries.length); useEffect(() => { if (!isOpen) { setPendingScene(null); } }, [isOpen]); useEffect(() => { if (!pendingScene) return; if (!connectionEntries.some((scene) => scene.scene.id === pendingScene.scene.id)) { setPendingScene(null); } }, [connectionEntries, pendingScene]); const handleSceneSelect = (scene: MapConnectionEntry | null) => { if (!scene || scene.scene.id === currentScenePreset?.id) return; setPendingScene(scene); }; const confirmTravel = () => { if (!pendingScene) return; onTravelToScene(pendingScene.scene); setPendingScene(null); }; return ( {isOpen && currentScenePreset && ( event.stopPropagation()} >
地图
当前位置
{currentScenePreset.name}
{currentScenePreset.description}
{connectionEntries.map((entry) => (
{`- ${entry.label}:${entry.scene.name}`}
))} {connectionEntries.length === 0 &&
- 暂无
}
{connectionEntries.length > 0 && ( {connectionEntries.map((entry, index) => ( ))} )}
{connectionEntries.map(entry => ( handleSceneSelect(entry)} /> ))}
{connectionEntries.length > 0 && ( {connectionEntries.map((entry, index) => ( ))} )}
{connectionEntries.map(entry => ( handleSceneSelect(entry)} /> ))}
{pendingScene && ( { event.stopPropagation(); setPendingScene(null); }} > event.stopPropagation()} >
场景切换
{pendingScene.scene.name}
setPendingScene(null)} label="关闭场景切换" />
目标场景
{pendingScene.scene.name}
{pendingScene.label}
{pendingScene.scene.description}
{pendingScene.summary ? (
连接说明:{pendingScene.summary}
) : null}
当前
{currentScenePreset.name}
前往
{pendingScene.scene.name}
)}
)} ); }