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 { ScenePresetInfo, WorldType } from '../types';
import { CHROME_ICONS, getNineSliceStyle, UI_CHROME } from '../uiAssets';
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 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(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 => (
handleSceneSelect(entry)}
/>
))}
{connectionEntries.length > 0 && (
)}
{connectionEntries.map(entry => (
handleSceneSelect(entry)}
/>
))}
{pendingScene && (
{
event.stopPropagation();
setPendingScene(null);
}}
>
event.stopPropagation()}
>
场景切换
{pendingScene.scene.name}
目标场景
{pendingScene.scene.name}
{pendingScene.label}
{pendingScene.scene.description}
{pendingScene.summary ? (
连接说明:{pendingScene.summary}
) : null}
当前
{currentScenePreset.name}
前往
{pendingScene.scene.name}
)}
)}
);
}