Split custom world generation into staged lightweight batches
Some checks failed
CI / verify (push) Has been cancelled
Some checks failed
CI / verify (push) Has been cancelled
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
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';
|
||||
@@ -33,9 +34,11 @@ function getMapDestinationCenterPercent(index: number, count: number) {
|
||||
|
||||
function MudMapRoom({
|
||||
scene,
|
||||
label: _label,
|
||||
label,
|
||||
compact = false,
|
||||
isInteractive = false,
|
||||
isSelected = false,
|
||||
description,
|
||||
onClick,
|
||||
}: {
|
||||
key?: string;
|
||||
@@ -43,6 +46,8 @@ function MudMapRoom({
|
||||
label: string;
|
||||
compact?: boolean;
|
||||
isInteractive?: boolean;
|
||||
isSelected?: boolean;
|
||||
description?: string;
|
||||
onClick?: (() => void) | null;
|
||||
}) {
|
||||
if (!scene) {
|
||||
@@ -56,11 +61,21 @@ function MudMapRoom({
|
||||
|
||||
const content = (
|
||||
<div
|
||||
className={`pixel-nine-slice map-room-cell h-full font-mono ${compact ? 'text-[10px]' : 'text-[11px]'} leading-relaxed ${isInteractive ? 'transition-transform duration-150 hover:-translate-y-0.5 hover:brightness-110' : ''}`}
|
||||
className={`pixel-nine-slice map-room-cell h-full font-mono ${compact ? 'text-[10px]' : 'text-[11px]'} leading-relaxed ${isInteractive ? 'transition-transform duration-150 hover:-translate-y-0.5 hover:brightness-110' : ''} ${isSelected ? 'brightness-125' : ''}`}
|
||||
style={getNineSliceStyle(UI_CHROME.mapRoomCell)}
|
||||
>
|
||||
<div className={`flex min-h-[3.25rem] items-center justify-center px-3 py-2 text-center ${compact ? 'text-[13px]' : 'text-sm'} font-semibold leading-tight text-white`}>
|
||||
{scene.name}
|
||||
<div className="flex min-h-[3.25rem] flex-col items-center justify-center px-3 py-2 text-center">
|
||||
<div className="rounded-full border border-emerald-300/25 bg-emerald-500/10 px-2 py-0.5 text-[9px] tracking-[0.16em] text-emerald-100/85">
|
||||
{label}
|
||||
</div>
|
||||
<div className={`mt-1 ${compact ? 'text-[13px]' : 'text-sm'} font-semibold leading-tight text-white`}>
|
||||
{scene.name}
|
||||
</div>
|
||||
{!compact && description ? (
|
||||
<div className="mt-2 text-[10px] leading-5 text-zinc-300/85">
|
||||
{description}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -86,6 +101,48 @@ interface MapModalProps {
|
||||
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<MapConnectionEntry | null> = [
|
||||
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,
|
||||
@@ -95,7 +152,7 @@ export function MapModal({
|
||||
isTraveling = false,
|
||||
canTravel = true,
|
||||
}: MapModalProps) {
|
||||
const [pendingScene, setPendingScene] = useState<ScenePresetInfo | null>(null);
|
||||
const [pendingScene, setPendingScene] = useState<MapConnectionEntry | null>(null);
|
||||
|
||||
const connectedScenes = useMemo(
|
||||
() =>
|
||||
@@ -104,14 +161,33 @@ export function MapModal({
|
||||
: [],
|
||||
[currentScenePreset, worldType],
|
||||
);
|
||||
const forwardSceneId = currentScenePreset?.forwardSceneId;
|
||||
const forwardScene = connectedScenes.find(scene => scene.id === forwardSceneId) ?? null;
|
||||
const branchScenes = connectedScenes.filter(scene => scene.id !== forwardSceneId);
|
||||
const leftBranchScene = branchScenes[0] ?? null;
|
||||
const rightBranchScene = branchScenes[1] ?? null;
|
||||
const destinationScenes = [forwardScene, leftBranchScene, rightBranchScene].filter(Boolean) as ScenePresetInfo[];
|
||||
const connectionEntries = useMemo(() => {
|
||||
if (currentScenePreset?.connections?.length) {
|
||||
const entries: Array<MapConnectionEntry | null> = 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(destinationScenes.length);
|
||||
const destinationStackHeightPx = getMapDestinationStackHeight(connectionEntries.length);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
@@ -121,19 +197,19 @@ export function MapModal({
|
||||
|
||||
useEffect(() => {
|
||||
if (!pendingScene) return;
|
||||
if (!connectedScenes.some(scene => scene.id === pendingScene.id)) {
|
||||
if (!connectionEntries.some((scene) => scene.scene.id === pendingScene.scene.id)) {
|
||||
setPendingScene(null);
|
||||
}
|
||||
}, [connectedScenes, pendingScene]);
|
||||
}, [connectionEntries, pendingScene]);
|
||||
|
||||
const handleSceneSelect = (scene: ScenePresetInfo | null) => {
|
||||
if (!scene || scene.id === currentScenePreset?.id) return;
|
||||
const handleSceneSelect = (scene: MapConnectionEntry | null) => {
|
||||
if (!scene || scene.scene.id === currentScenePreset?.id) return;
|
||||
setPendingScene(scene);
|
||||
};
|
||||
|
||||
const confirmTravel = () => {
|
||||
if (!pendingScene) return;
|
||||
onTravelToScene(pendingScene);
|
||||
onTravelToScene(pendingScene.scene);
|
||||
setPendingScene(null);
|
||||
};
|
||||
|
||||
@@ -187,11 +263,10 @@ export function MapModal({
|
||||
<div className="mt-2 text-zinc-300">{currentScenePreset.description}</div>
|
||||
|
||||
<div className="mt-2 space-y-1.5 text-zinc-300">
|
||||
{forwardScene && <div>{`- 前路:${forwardScene.name}`}</div>}
|
||||
{branchScenes.map((scene, index) => (
|
||||
<div key={scene.id}>{`- 支路 ${index + 1}:${scene.name}`}</div>
|
||||
{connectionEntries.map((entry) => (
|
||||
<div key={entry.scene.id}>{`- ${entry.label}:${entry.scene.name}`}</div>
|
||||
))}
|
||||
{connectedScenes.length === 0 && <div>- 暂无</div>}
|
||||
{connectionEntries.length === 0 && <div>- 暂无</div>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -202,15 +277,15 @@ export function MapModal({
|
||||
<MudMapRoom scene={currentScenePreset} label="当前位置" compact />
|
||||
</div>
|
||||
<div className="relative" style={{ minHeight: `${destinationStackHeightPx}px` }}>
|
||||
{destinationScenes.length > 0 && (
|
||||
{connectionEntries.length > 0 && (
|
||||
<svg className="absolute inset-0 h-full w-full" preserveAspectRatio="none">
|
||||
{destinationScenes.map((scene, index) => (
|
||||
{connectionEntries.map((entry, index) => (
|
||||
<line
|
||||
key={`connector-${scene.id}`}
|
||||
key={`connector-${entry.scene.id}`}
|
||||
x1="0"
|
||||
y1={`${getMapDestinationCenterPercent(0, destinationScenes.length)}%`}
|
||||
y1={`${getMapDestinationCenterPercent(0, connectionEntries.length)}%`}
|
||||
x2="100%"
|
||||
y2={`${getMapDestinationCenterPercent(index, destinationScenes.length)}%`}
|
||||
y2={`${getMapDestinationCenterPercent(index, connectionEntries.length)}%`}
|
||||
stroke="rgba(74, 222, 128, 0.35)"
|
||||
strokeWidth="1.5"
|
||||
/>
|
||||
@@ -219,14 +294,16 @@ export function MapModal({
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-3" style={{ minHeight: `${destinationStackHeightPx}px` }}>
|
||||
{destinationScenes.map(scene => (
|
||||
{connectionEntries.map(entry => (
|
||||
<MudMapRoom
|
||||
key={scene.id}
|
||||
scene={scene}
|
||||
label={scene.id === forwardScene?.id ? '前路' : '支路'}
|
||||
key={entry.scene.id}
|
||||
scene={entry.scene}
|
||||
label={entry.label}
|
||||
description={entry.summary}
|
||||
compact
|
||||
isInteractive={canTravel}
|
||||
onClick={() => handleSceneSelect(scene)}
|
||||
isSelected={pendingScene?.scene.id === entry.scene.id}
|
||||
onClick={() => handleSceneSelect(entry)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -239,15 +316,15 @@ export function MapModal({
|
||||
<MudMapRoom scene={currentScenePreset} label="当前位置" compact />
|
||||
</div>
|
||||
<div className="relative" style={{ minHeight: `${destinationStackHeightPx}px` }}>
|
||||
{destinationScenes.length > 0 && (
|
||||
{connectionEntries.length > 0 && (
|
||||
<svg className="absolute inset-0 h-full w-full" preserveAspectRatio="none">
|
||||
{destinationScenes.map((scene, index) => (
|
||||
{connectionEntries.map((entry, index) => (
|
||||
<line
|
||||
key={`connector-desktop-${scene.id}`}
|
||||
key={`connector-desktop-${entry.scene.id}`}
|
||||
x1="0"
|
||||
y1={`${getMapDestinationCenterPercent(0, destinationScenes.length)}%`}
|
||||
y1={`${getMapDestinationCenterPercent(0, connectionEntries.length)}%`}
|
||||
x2="100%"
|
||||
y2={`${getMapDestinationCenterPercent(index, destinationScenes.length)}%`}
|
||||
y2={`${getMapDestinationCenterPercent(index, connectionEntries.length)}%`}
|
||||
stroke="rgba(74, 222, 128, 0.35)"
|
||||
strokeWidth="1.5"
|
||||
/>
|
||||
@@ -256,13 +333,15 @@ export function MapModal({
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-3" style={{ minHeight: `${destinationStackHeightPx}px` }}>
|
||||
{destinationScenes.map(scene => (
|
||||
{connectionEntries.map(entry => (
|
||||
<MudMapRoom
|
||||
key={scene.id}
|
||||
scene={scene}
|
||||
label={scene.id === forwardScene?.id ? '前路' : '支路'}
|
||||
key={entry.scene.id}
|
||||
scene={entry.scene}
|
||||
label={entry.label}
|
||||
description={entry.summary}
|
||||
isInteractive={canTravel}
|
||||
onClick={() => handleSceneSelect(scene)}
|
||||
isSelected={pendingScene?.scene.id === entry.scene.id}
|
||||
onClick={() => handleSceneSelect(entry)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -296,7 +375,7 @@ export function MapModal({
|
||||
<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">
|
||||
<div className="text-[10px] tracking-[0.22em] text-amber-200/80">场景切换</div>
|
||||
<div className="mt-1 truncate text-sm font-semibold text-white">{pendingScene.name}</div>
|
||||
<div className="mt-1 truncate text-sm font-semibold text-white">{pendingScene.scene.name}</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
@@ -310,8 +389,16 @@ export function MapModal({
|
||||
<div className="min-h-0 flex-1 space-y-4 overflow-y-auto p-4 sm:p-5">
|
||||
<div className="rounded-xl border border-amber-400/20 bg-amber-500/10 px-4 py-4">
|
||||
<div className="text-[10px] tracking-[0.18em] text-amber-200/75">目标场景</div>
|
||||
<div className="mt-2 text-base font-semibold text-white">{pendingScene.name}</div>
|
||||
<div className="mt-2 text-sm leading-relaxed text-zinc-300">{pendingScene.description}</div>
|
||||
<div className="mt-2 text-base font-semibold text-white">{pendingScene.scene.name}</div>
|
||||
<div className="mt-2 rounded-full border border-amber-300/20 bg-black/20 px-3 py-1 text-[10px] tracking-[0.18em] text-amber-50">
|
||||
{pendingScene.label}
|
||||
</div>
|
||||
<div className="mt-2 text-sm leading-relaxed text-zinc-300">{pendingScene.scene.description}</div>
|
||||
{pendingScene.summary ? (
|
||||
<div className="mt-2 text-xs leading-6 text-zinc-400">
|
||||
连接说明:{pendingScene.summary}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
@@ -321,7 +408,7 @@ export function MapModal({
|
||||
</div>
|
||||
<div className="rounded-xl border border-white/8 bg-black/20 px-4 py-3">
|
||||
<div className="text-[10px] tracking-[0.18em] text-zinc-500">前往</div>
|
||||
<div className="mt-2 text-sm font-semibold text-white">{pendingScene.name}</div>
|
||||
<div className="mt-2 text-sm font-semibold text-white">{pendingScene.scene.name}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user