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,9 +1,17 @@
|
||||
import { Children, type ReactNode, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import {
|
||||
AFFINITY_BACKSTORY_CHAPTER_THRESHOLDS,
|
||||
} from '../data/affinityLevels';
|
||||
import {
|
||||
buildCustomWorldPlayableCharacters,
|
||||
PRESET_CHARACTERS,
|
||||
} from '../data/characterPresets';
|
||||
import {
|
||||
CUSTOM_WORLD_SCENE_RELATIVE_POSITION_OPTIONS,
|
||||
getCustomWorldSceneRelativePositionLabel,
|
||||
normalizeCustomWorldLandmarks,
|
||||
} from '../data/customWorldSceneGraph';
|
||||
import {
|
||||
getAllCustomWorldSceneImages,
|
||||
getDefaultCustomWorldSceneImage,
|
||||
@@ -22,6 +30,7 @@ import {
|
||||
CustomWorldNpc,
|
||||
CustomWorldPlayableNpc,
|
||||
CustomWorldProfile,
|
||||
CustomWorldSceneConnection,
|
||||
} from '../types';
|
||||
import { CHROME_ICONS, getNineSliceStyle, UI_CHROME } from '../uiAssets';
|
||||
import { CharacterAnimator } from './CharacterAnimator';
|
||||
@@ -48,6 +57,13 @@ interface CustomWorldEntityEditorModalProps {
|
||||
onProfileChange: (profile: CustomWorldProfile) => void;
|
||||
}
|
||||
|
||||
const [
|
||||
BACKSTORY_UNLOCK_AFFINITY_EASED,
|
||||
BACKSTORY_UNLOCK_AFFINITY_FRIENDLY,
|
||||
BACKSTORY_UNLOCK_AFFINITY_TRUSTED,
|
||||
BACKSTORY_UNLOCK_AFFINITY_CLOSE,
|
||||
] = AFFINITY_BACKSTORY_CHAPTER_THRESHOLDS;
|
||||
|
||||
function slugify(value: string) {
|
||||
const normalized = value
|
||||
.trim()
|
||||
@@ -85,6 +101,16 @@ function clampInitialAffinity(value: string, fallback: number) {
|
||||
return Math.max(-40, Math.min(90, Math.round(parsed)));
|
||||
}
|
||||
|
||||
function syncLandmarksWithStoryNpcs(
|
||||
landmarks: CustomWorldLandmark[],
|
||||
storyNpcs: CustomWorldProfile['storyNpcs'],
|
||||
) {
|
||||
return normalizeCustomWorldLandmarks({
|
||||
landmarks,
|
||||
storyNpcs,
|
||||
});
|
||||
}
|
||||
|
||||
function useDraft<T>(value: T) {
|
||||
const [draft, setDraft] = useState(value);
|
||||
useEffect(() => setDraft(value), [value]);
|
||||
@@ -1208,24 +1234,97 @@ function LandmarkEditor({
|
||||
profile,
|
||||
landmark,
|
||||
mode,
|
||||
onSave,
|
||||
onSaveProfile,
|
||||
onClose,
|
||||
}: {
|
||||
profile: CustomWorldProfile;
|
||||
landmark: CustomWorldLandmark;
|
||||
mode: 'create' | 'edit';
|
||||
onSave: (landmark: CustomWorldLandmark) => void;
|
||||
onSaveProfile: (profile: CustomWorldProfile) => void;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const [draft, setDraft] = useDraft(landmark);
|
||||
const [draftStoryNpcs, setDraftStoryNpcs] = useDraft(profile.storyNpcs);
|
||||
const [isPresetPickerOpen, setIsPresetPickerOpen] = useState(false);
|
||||
const [isAiGenerateOpen, setIsAiGenerateOpen] = useState(false);
|
||||
const [npcEditorState, setNpcEditorState] = useState<{
|
||||
mode: 'create' | 'edit';
|
||||
npc: CustomWorldNpc;
|
||||
} | null>(null);
|
||||
const presetImages = useMemo(() => getAllCustomWorldSceneImages(), []);
|
||||
const storyNpcById = useMemo(
|
||||
() => new Map(draftStoryNpcs.map((npc) => [npc.id, npc])),
|
||||
[draftStoryNpcs],
|
||||
);
|
||||
const availableTargetLandmarks = useMemo(
|
||||
() => profile.landmarks.filter((entry) => entry.id !== draft.id),
|
||||
[draft.id, profile.landmarks],
|
||||
);
|
||||
|
||||
const toggleSceneNpc = (npcId: string) => {
|
||||
setDraft((current) => ({
|
||||
...current,
|
||||
sceneNpcIds: current.sceneNpcIds.includes(npcId)
|
||||
? current.sceneNpcIds.filter((entry) => entry !== npcId)
|
||||
: [...current.sceneNpcIds, npcId],
|
||||
}));
|
||||
};
|
||||
|
||||
const updateConnection = (
|
||||
index: number,
|
||||
updater: (connection: CustomWorldSceneConnection) => CustomWorldSceneConnection,
|
||||
) => {
|
||||
setDraft((current) => ({
|
||||
...current,
|
||||
connections: current.connections.map((connection, connectionIndex) =>
|
||||
connectionIndex === index ? updater(connection) : connection,
|
||||
),
|
||||
}));
|
||||
};
|
||||
|
||||
const addConnection = () => {
|
||||
const fallbackTarget = availableTargetLandmarks[0];
|
||||
if (!fallbackTarget) {
|
||||
window.alert('请先保留至少一个其他场景,才能配置连接关系。');
|
||||
return;
|
||||
}
|
||||
|
||||
setDraft((current) => ({
|
||||
...current,
|
||||
connections: [
|
||||
...current.connections,
|
||||
{
|
||||
targetLandmarkId: fallbackTarget.id,
|
||||
relativePosition: 'forward',
|
||||
summary: `可通往${fallbackTarget.name}`,
|
||||
},
|
||||
],
|
||||
}));
|
||||
};
|
||||
|
||||
const saveLandmarkProfile = () => {
|
||||
if (draft.sceneNpcIds.length < 3) {
|
||||
window.alert('每个场景至少需要分配 3 个 NPC。');
|
||||
return;
|
||||
}
|
||||
|
||||
const nextLandmarks =
|
||||
mode === 'create'
|
||||
? [...profile.landmarks, draft]
|
||||
: profile.landmarks.map((entry) => (entry.id === draft.id ? draft : entry));
|
||||
|
||||
onSaveProfile({
|
||||
...profile,
|
||||
storyNpcs: draftStoryNpcs,
|
||||
landmarks: syncLandmarksWithStoryNpcs(nextLandmarks, draftStoryNpcs),
|
||||
});
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<ModalShell
|
||||
title={mode === 'create' ? '新增场景' : `编辑场景:${landmark.name}`}
|
||||
subtitle="这里的场景图片会同步用于结果页展示和正式进入世界后的场景背景。"
|
||||
subtitle="这里可以同时配置场景图片、场景内 NPC,以及场景之间的相对位置连接关系。"
|
||||
onClose={onClose}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
@@ -1286,12 +1385,213 @@ function LandmarkEditor({
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
<div className="rounded-2xl border border-white/8 bg-black/20 px-4 py-4">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
<div className="text-[11px] font-bold tracking-[0.16em] text-zinc-300">
|
||||
场景内 NPC
|
||||
</div>
|
||||
<div className="mt-2 text-sm leading-6 text-zinc-400">
|
||||
每个场景至少保留 3 个 NPC。可以在这里直接继续新增 NPC,并立即加入当前场景。
|
||||
</div>
|
||||
</div>
|
||||
<ActionButton
|
||||
label="新增 NPC 并加入此场景"
|
||||
onClick={() =>
|
||||
setNpcEditorState({
|
||||
mode: 'create',
|
||||
npc: createStoryNpc({ storyNpcs: draftStoryNpcs }),
|
||||
})
|
||||
}
|
||||
tone="sky"
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-3 space-y-2">
|
||||
{draft.sceneNpcIds.length > 0 ? (
|
||||
draft.sceneNpcIds.map((npcId) => {
|
||||
const npc = storyNpcById.get(npcId);
|
||||
return (
|
||||
<div
|
||||
key={`${draft.id}-selected-npc-${npcId}`}
|
||||
className="rounded-2xl border border-white/8 bg-black/20 px-3 py-3"
|
||||
>
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<div className="text-sm font-semibold text-white">
|
||||
{npc?.name ?? '未匹配场景角色'}
|
||||
</div>
|
||||
<div className="mt-1 text-xs leading-6 text-zinc-400">
|
||||
{npc?.role || npc?.title || '未填写身份'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{npc ? (
|
||||
<ActionButton
|
||||
label="编辑"
|
||||
onClick={() =>
|
||||
setNpcEditorState({
|
||||
mode: 'edit',
|
||||
npc,
|
||||
})
|
||||
}
|
||||
tone="sky"
|
||||
/>
|
||||
) : null}
|
||||
<ActionButton
|
||||
label="移出场景"
|
||||
onClick={() => toggleSceneNpc(npcId)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div className="rounded-2xl border border-dashed border-white/12 bg-black/20 px-4 py-4 text-sm text-zinc-500">
|
||||
还没有为这个场景分配 NPC。
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-3 max-h-64 space-y-2 overflow-y-auto pr-1">
|
||||
{draftStoryNpcs.map((npc) => {
|
||||
const selected = draft.sceneNpcIds.includes(npc.id);
|
||||
return (
|
||||
<button
|
||||
key={`${draft.id}-npc-picker-${npc.id}`}
|
||||
type="button"
|
||||
onClick={() => toggleSceneNpc(npc.id)}
|
||||
className={`w-full rounded-2xl border px-3 py-3 text-left transition-colors ${
|
||||
selected
|
||||
? 'border-sky-300/28 bg-sky-500/10'
|
||||
: 'border-white/8 bg-black/20 hover:border-white/18'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<div className="text-sm font-semibold text-white">
|
||||
{npc.name}
|
||||
</div>
|
||||
<div className="mt-1 text-xs leading-6 text-zinc-400">
|
||||
{npc.role}
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-full border border-white/10 bg-black/20 px-2.5 py-1 text-[10px] text-zinc-300">
|
||||
{selected ? '已加入' : '点击加入'}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-2xl border border-white/8 bg-black/20 px-4 py-4">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
<div className="text-[11px] font-bold tracking-[0.16em] text-zinc-300">
|
||||
场景连接关系
|
||||
</div>
|
||||
<div className="mt-2 text-sm leading-6 text-zinc-400">
|
||||
编辑当前场景与其他场景之间的相对位置关系。保存时会自动同步反向连线,避免地图断开。
|
||||
</div>
|
||||
</div>
|
||||
<ActionButton
|
||||
label="新增连接"
|
||||
onClick={addConnection}
|
||||
tone="sky"
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-3 space-y-3">
|
||||
{draft.connections.length > 0 ? (
|
||||
draft.connections.map((connection, index) => (
|
||||
<div
|
||||
key={`${draft.id}-connection-${index}`}
|
||||
className="rounded-2xl border border-white/8 bg-black/20 px-3 py-3"
|
||||
>
|
||||
<div className="grid gap-3 md:grid-cols-[minmax(0,1.1fr)_minmax(0,0.9fr)]">
|
||||
<Field label="目标场景">
|
||||
<SelectField
|
||||
value={connection.targetLandmarkId}
|
||||
onChange={(value) =>
|
||||
updateConnection(index, (current) => ({
|
||||
...current,
|
||||
targetLandmarkId: value,
|
||||
}))
|
||||
}
|
||||
options={availableTargetLandmarks.map((entry) => ({
|
||||
value: entry.id,
|
||||
label: entry.name,
|
||||
}))}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="相对位置">
|
||||
<SelectField
|
||||
value={connection.relativePosition}
|
||||
onChange={(value) =>
|
||||
updateConnection(index, (current) => ({
|
||||
...current,
|
||||
relativePosition: value as CustomWorldSceneConnection['relativePosition'],
|
||||
}))
|
||||
}
|
||||
options={CUSTOM_WORLD_SCENE_RELATIVE_POSITION_OPTIONS.map(
|
||||
(option) => ({
|
||||
value: option.value,
|
||||
label: option.label,
|
||||
}),
|
||||
)}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
<Field label="连接说明">
|
||||
<TextArea
|
||||
value={connection.summary}
|
||||
onChange={(value) =>
|
||||
updateConnection(index, (current) => ({
|
||||
...current,
|
||||
summary: value,
|
||||
}))
|
||||
}
|
||||
rows={2}
|
||||
placeholder="例如:沿山脊向北翻过去,可到达断桥。"
|
||||
/>
|
||||
</Field>
|
||||
<div className="flex justify-end">
|
||||
<ActionButton
|
||||
label="删除连接"
|
||||
onClick={() =>
|
||||
setDraft((current) => ({
|
||||
...current,
|
||||
connections: current.connections.filter(
|
||||
(_item, connectionIndex) => connectionIndex !== index,
|
||||
),
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="rounded-2xl border border-dashed border-white/12 bg-black/20 px-4 py-4 text-sm text-zinc-500">
|
||||
当前还没有手动配置连接。若直接保存,系统会自动补一条可遍历的主路连接。
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{draft.connections.length > 0 ? (
|
||||
<div className="mt-3 rounded-2xl border border-white/8 bg-black/20 px-4 py-3 text-xs leading-6 text-zinc-400">
|
||||
当前连接预览:
|
||||
{draft.connections
|
||||
.map((connection) => {
|
||||
const targetLandmark = availableTargetLandmarks.find(
|
||||
(entry) => entry.id === connection.targetLandmarkId,
|
||||
);
|
||||
return `${getCustomWorldSceneRelativePositionLabel(connection.relativePosition)} -> ${targetLandmark?.name ?? '未匹配场景'}`;
|
||||
})
|
||||
.join(';')}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<SaveBar
|
||||
onClose={onClose}
|
||||
onSave={() => {
|
||||
onSave(draft);
|
||||
onClose();
|
||||
}}
|
||||
onSave={saveLandmarkProfile}
|
||||
/>
|
||||
{isPresetPickerOpen ? (
|
||||
<ScenePresetPickerModal
|
||||
@@ -1316,6 +1616,27 @@ function LandmarkEditor({
|
||||
onClose={() => setIsAiGenerateOpen(false)}
|
||||
/>
|
||||
) : null}
|
||||
{npcEditorState ? (
|
||||
<StoryNpcEditor
|
||||
npc={npcEditorState.npc}
|
||||
mode={npcEditorState.mode}
|
||||
onSave={(nextNpc) => {
|
||||
setDraftStoryNpcs((current) =>
|
||||
npcEditorState.mode === 'create'
|
||||
? [...current, nextNpc]
|
||||
: current.map((item) => (item.id === nextNpc.id ? nextNpc : item)),
|
||||
);
|
||||
setDraft((current) => ({
|
||||
...current,
|
||||
sceneNpcIds: current.sceneNpcIds.includes(nextNpc.id)
|
||||
? current.sceneNpcIds
|
||||
: [...current.sceneNpcIds, nextNpc.id],
|
||||
}));
|
||||
setNpcEditorState(null);
|
||||
}}
|
||||
onClose={() => setNpcEditorState(null)}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</ModalShell>
|
||||
);
|
||||
@@ -1347,11 +1668,82 @@ function createPlayableNpc(
|
||||
initialAffinity: 18,
|
||||
relationshipHooks: ['首次接触', '合作空间'],
|
||||
tags: ['自定义'],
|
||||
backstoryReveal: {
|
||||
publicSummary: '',
|
||||
chapters: [
|
||||
{
|
||||
id: 'surface',
|
||||
title: '表层来意',
|
||||
affinityRequired: BACKSTORY_UNLOCK_AFFINITY_EASED,
|
||||
teaser: '',
|
||||
content: '',
|
||||
contextSnippet: '',
|
||||
},
|
||||
{
|
||||
id: 'scar',
|
||||
title: '旧事裂痕',
|
||||
affinityRequired: BACKSTORY_UNLOCK_AFFINITY_FRIENDLY,
|
||||
teaser: '',
|
||||
content: '',
|
||||
contextSnippet: '',
|
||||
},
|
||||
{
|
||||
id: 'hidden',
|
||||
title: '隐藏执念',
|
||||
affinityRequired: BACKSTORY_UNLOCK_AFFINITY_TRUSTED,
|
||||
teaser: '',
|
||||
content: '',
|
||||
contextSnippet: '',
|
||||
},
|
||||
{
|
||||
id: 'final',
|
||||
title: '最终底牌',
|
||||
affinityRequired: BACKSTORY_UNLOCK_AFFINITY_CLOSE,
|
||||
teaser: '',
|
||||
content: '',
|
||||
contextSnippet: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
skills: [
|
||||
{ id: 'skill-1', name: '基础起手', summary: '', style: '起手压制' },
|
||||
{ id: 'skill-2', name: '常用变招', summary: '', style: '机动周旋' },
|
||||
{ id: 'skill-3', name: '压箱底牌', summary: '', style: '爆发终结' },
|
||||
],
|
||||
initialItems: [
|
||||
{
|
||||
id: 'item-1',
|
||||
name: '随身武具',
|
||||
category: '武器',
|
||||
quantity: 1,
|
||||
rarity: 'rare',
|
||||
description: '',
|
||||
tags: ['自定义'],
|
||||
},
|
||||
{
|
||||
id: 'item-2',
|
||||
name: '补给包',
|
||||
category: '消耗品',
|
||||
quantity: 2,
|
||||
rarity: 'uncommon',
|
||||
description: '',
|
||||
tags: ['自定义'],
|
||||
},
|
||||
{
|
||||
id: 'item-3',
|
||||
name: '私人物件',
|
||||
category: '专属物品',
|
||||
quantity: 1,
|
||||
rarity: 'rare',
|
||||
description: '',
|
||||
tags: ['自定义'],
|
||||
},
|
||||
],
|
||||
templateCharacterId: template?.id,
|
||||
};
|
||||
}
|
||||
|
||||
function createStoryNpc(profile: CustomWorldProfile): CustomWorldNpc {
|
||||
function createStoryNpc(profile: Pick<CustomWorldProfile, 'storyNpcs'>): CustomWorldNpc {
|
||||
const seed = Date.now() + profile.storyNpcs.length;
|
||||
const npc = {
|
||||
id: createEntryId(
|
||||
@@ -1370,6 +1762,77 @@ function createStoryNpc(profile: CustomWorldProfile): CustomWorldNpc {
|
||||
initialAffinity: 6,
|
||||
relationshipHooks: ['合作', '互动'],
|
||||
tags: ['自定义'],
|
||||
backstoryReveal: {
|
||||
publicSummary: '',
|
||||
chapters: [
|
||||
{
|
||||
id: 'surface',
|
||||
title: '表层来意',
|
||||
affinityRequired: BACKSTORY_UNLOCK_AFFINITY_EASED,
|
||||
teaser: '',
|
||||
content: '',
|
||||
contextSnippet: '',
|
||||
},
|
||||
{
|
||||
id: 'scar',
|
||||
title: '旧事裂痕',
|
||||
affinityRequired: BACKSTORY_UNLOCK_AFFINITY_FRIENDLY,
|
||||
teaser: '',
|
||||
content: '',
|
||||
contextSnippet: '',
|
||||
},
|
||||
{
|
||||
id: 'hidden',
|
||||
title: '隐藏执念',
|
||||
affinityRequired: BACKSTORY_UNLOCK_AFFINITY_TRUSTED,
|
||||
teaser: '',
|
||||
content: '',
|
||||
contextSnippet: '',
|
||||
},
|
||||
{
|
||||
id: 'final',
|
||||
title: '最终底牌',
|
||||
affinityRequired: BACKSTORY_UNLOCK_AFFINITY_CLOSE,
|
||||
teaser: '',
|
||||
content: '',
|
||||
contextSnippet: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
skills: [
|
||||
{ id: 'skill-1', name: '基础起手', summary: '', style: '起手压制' },
|
||||
{ id: 'skill-2', name: '常用变招', summary: '', style: '机动周旋' },
|
||||
{ id: 'skill-3', name: '压箱底牌', summary: '', style: '爆发终结' },
|
||||
],
|
||||
initialItems: [
|
||||
{
|
||||
id: 'item-1',
|
||||
name: '随身武具',
|
||||
category: '武器',
|
||||
quantity: 1,
|
||||
rarity: 'rare',
|
||||
description: '',
|
||||
tags: ['自定义'],
|
||||
},
|
||||
{
|
||||
id: 'item-2',
|
||||
name: '补给包',
|
||||
category: '消耗品',
|
||||
quantity: 2,
|
||||
rarity: 'uncommon',
|
||||
description: '',
|
||||
tags: ['自定义'],
|
||||
},
|
||||
{
|
||||
id: 'item-3',
|
||||
name: '私人物件',
|
||||
category: '专属物品',
|
||||
quantity: 1,
|
||||
rarity: 'rare',
|
||||
description: '',
|
||||
tags: ['自定义'],
|
||||
},
|
||||
],
|
||||
} satisfies CustomWorldNpc;
|
||||
|
||||
return npc;
|
||||
@@ -1377,6 +1840,7 @@ function createStoryNpc(profile: CustomWorldProfile): CustomWorldNpc {
|
||||
|
||||
function createLandmark(profile: CustomWorldProfile): CustomWorldLandmark {
|
||||
const seed = Date.now() + profile.landmarks.length;
|
||||
const previousLandmark = profile.landmarks[profile.landmarks.length - 1];
|
||||
return {
|
||||
id: createEntryId(
|
||||
'landmark',
|
||||
@@ -1391,6 +1855,16 @@ function createLandmark(profile: CustomWorldProfile): CustomWorldLandmark {
|
||||
profile.landmarks.length,
|
||||
profile.templateWorldType,
|
||||
),
|
||||
sceneNpcIds: profile.storyNpcs.slice(0, 3).map((npc) => npc.id),
|
||||
connections: previousLandmark
|
||||
? [
|
||||
{
|
||||
targetLandmarkId: previousLandmark.id,
|
||||
relativePosition: 'back',
|
||||
summary: `暂时接回${previousLandmark.name}这条旧路`,
|
||||
},
|
||||
]
|
||||
: [],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1483,20 +1957,15 @@ export function CustomWorldEntityEditorModal({
|
||||
}
|
||||
|
||||
if (target.mode === 'create') {
|
||||
return (
|
||||
<LandmarkEditor
|
||||
profile={profile}
|
||||
landmark={createLandmark(profile)}
|
||||
mode="create"
|
||||
onSave={(nextLandmark) =>
|
||||
onProfileChange({
|
||||
...profile,
|
||||
landmarks: [...profile.landmarks, nextLandmark],
|
||||
})
|
||||
}
|
||||
onClose={onClose}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<LandmarkEditor
|
||||
profile={profile}
|
||||
landmark={createLandmark(profile)}
|
||||
mode="create"
|
||||
onSaveProfile={onProfileChange}
|
||||
onClose={onClose}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const landmark = profile.landmarks.find((entry) => entry.id === target.id);
|
||||
@@ -1505,14 +1974,7 @@ export function CustomWorldEntityEditorModal({
|
||||
profile={profile}
|
||||
landmark={landmark}
|
||||
mode="edit"
|
||||
onSave={(nextLandmark) =>
|
||||
onProfileChange({
|
||||
...profile,
|
||||
landmarks: profile.landmarks.map((entry) =>
|
||||
entry.id === nextLandmark.id ? nextLandmark : entry,
|
||||
),
|
||||
})
|
||||
}
|
||||
onSaveProfile={onProfileChange}
|
||||
onClose={onClose}
|
||||
/>
|
||||
) : null;
|
||||
|
||||
Reference in New Issue
Block a user