Implement scene-based chapter quest progression
Some checks failed
CI / verify (push) Has been cancelled
Some checks failed
CI / verify (push) Has been cancelled
This commit is contained in:
@@ -14,7 +14,8 @@ import {
|
||||
} from '../data/customWorldSceneGraph';
|
||||
import {
|
||||
getAllCustomWorldSceneImages,
|
||||
getDefaultCustomWorldSceneImage,
|
||||
resolveCustomWorldCampSceneImage,
|
||||
resolveCustomWorldLandmarkImage,
|
||||
} from '../data/customWorldVisuals';
|
||||
import {
|
||||
type CustomWorldSceneImageResult,
|
||||
@@ -24,6 +25,7 @@ import {
|
||||
buildCustomWorldSceneImagePrompt,
|
||||
DEFAULT_CUSTOM_WORLD_SCENE_IMAGE_NEGATIVE_PROMPT,
|
||||
} from '../services/customWorld';
|
||||
import { resolveCustomWorldCampScene } from '../services/customWorldCamp';
|
||||
import {
|
||||
AnimationState,
|
||||
CustomWorldLandmark,
|
||||
@@ -612,7 +614,25 @@ function SceneImageGenerationModal({
|
||||
const [latestResult, setLatestResult] =
|
||||
useState<CustomWorldSceneImageResult | null>(null);
|
||||
|
||||
const previewImageSrc = latestResult?.imageSrc ?? landmark.imageSrc;
|
||||
const previewImageSrc = useMemo(() => {
|
||||
if (latestResult?.imageSrc) {
|
||||
return latestResult.imageSrc;
|
||||
}
|
||||
|
||||
const landmarkIndex = profile.landmarks.findIndex(
|
||||
(entry) => entry.id === landmark.id,
|
||||
);
|
||||
|
||||
return resolveCustomWorldLandmarkImage(
|
||||
profile,
|
||||
landmark,
|
||||
landmarkIndex >= 0 ? landmarkIndex : profile.landmarks.length,
|
||||
profile.landmarks
|
||||
.filter((entry) => entry.id !== landmark.id)
|
||||
.map((entry) => entry.imageSrc)
|
||||
.filter((imageSrc): imageSrc is string => Boolean(imageSrc)),
|
||||
);
|
||||
}, [landmark, latestResult, profile]);
|
||||
|
||||
const handleGenerate = async () => {
|
||||
if (!prompt.trim()) {
|
||||
@@ -1238,6 +1258,29 @@ function WorldEditor({
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const [draft, setDraft] = useDraft(profile);
|
||||
const [isCampPresetPickerOpen, setIsCampPresetPickerOpen] = useState(false);
|
||||
const [isCampAiGenerateOpen, setIsCampAiGenerateOpen] = useState(false);
|
||||
const presetImages = useMemo(() => getAllCustomWorldSceneImages(), []);
|
||||
const resolvedCampScene = useMemo(
|
||||
() => resolveCustomWorldCampScene(draft),
|
||||
[draft],
|
||||
);
|
||||
const resolvedCampImageSrc = useMemo(
|
||||
() => resolveCustomWorldCampSceneImage(draft),
|
||||
[draft],
|
||||
);
|
||||
const campSceneDraft = useMemo<CustomWorldLandmark>(
|
||||
() => ({
|
||||
id: 'custom-scene-camp',
|
||||
name: resolvedCampScene.name,
|
||||
description: resolvedCampScene.description,
|
||||
dangerLevel: resolvedCampScene.dangerLevel,
|
||||
imageSrc: resolvedCampScene.imageSrc,
|
||||
sceneNpcIds: [],
|
||||
connections: [],
|
||||
}),
|
||||
[resolvedCampScene],
|
||||
);
|
||||
|
||||
return (
|
||||
<ModalShell
|
||||
@@ -1289,6 +1332,84 @@ function WorldEditor({
|
||||
rows={3}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="开局归处名称">
|
||||
<TextInput
|
||||
value={resolvedCampScene.name}
|
||||
onChange={(value) =>
|
||||
setDraft((current) => ({
|
||||
...current,
|
||||
camp: {
|
||||
...resolveCustomWorldCampScene(current),
|
||||
name: value,
|
||||
},
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="开局归处描述">
|
||||
<TextArea
|
||||
value={resolvedCampScene.description}
|
||||
onChange={(value) =>
|
||||
setDraft((current) => ({
|
||||
...current,
|
||||
camp: {
|
||||
...resolveCustomWorldCampScene(current),
|
||||
description: value,
|
||||
},
|
||||
}))
|
||||
}
|
||||
rows={4}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="开局归处危险度">
|
||||
<TextInput
|
||||
value={resolvedCampScene.dangerLevel}
|
||||
onChange={(value) =>
|
||||
setDraft((current) => ({
|
||||
...current,
|
||||
camp: {
|
||||
...resolveCustomWorldCampScene(current),
|
||||
dangerLevel: value,
|
||||
},
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
<ImageField
|
||||
label="开局归处背景"
|
||||
value={resolvedCampImageSrc}
|
||||
onChange={(value) =>
|
||||
setDraft((current) => ({
|
||||
...current,
|
||||
camp: {
|
||||
...resolveCustomWorldCampScene(current),
|
||||
imageSrc: value || undefined,
|
||||
},
|
||||
}))
|
||||
}
|
||||
fallbackLabel={resolvedCampScene.name.slice(0, 4) || '归处'}
|
||||
tone="landscape"
|
||||
showInput={false}
|
||||
previewOverlay={<SceneSparringPreview profile={draft} />}
|
||||
footer={(
|
||||
<div className="space-y-3">
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<ActionButton
|
||||
label="预设选择"
|
||||
onClick={() => setIsCampPresetPickerOpen(true)}
|
||||
tone="sky"
|
||||
/>
|
||||
<ActionButton
|
||||
label="智能生成"
|
||||
onClick={() => setIsCampAiGenerateOpen(true)}
|
||||
/>
|
||||
</div>
|
||||
<div className="rounded-2xl border border-white/8 bg-black/20 px-4 py-3 text-sm leading-6 text-zinc-300">
|
||||
开局归处会直接作为进入自定义世界时的第一张背景。这里可以单独指定或生成这张背景图。
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<Field label="玩家原始设定">
|
||||
<TextArea
|
||||
value={draft.settingText}
|
||||
@@ -1298,6 +1419,38 @@ function WorldEditor({
|
||||
rows={4}
|
||||
/>
|
||||
</Field>
|
||||
{isCampPresetPickerOpen ? (
|
||||
<ScenePresetPickerModal
|
||||
selectedSrc={resolvedCampScene.imageSrc}
|
||||
presetImages={presetImages}
|
||||
onSelect={(value) =>
|
||||
setDraft((current) => ({
|
||||
...current,
|
||||
camp: {
|
||||
...resolveCustomWorldCampScene(current),
|
||||
imageSrc: value,
|
||||
},
|
||||
}))
|
||||
}
|
||||
onClose={() => setIsCampPresetPickerOpen(false)}
|
||||
/>
|
||||
) : null}
|
||||
{isCampAiGenerateOpen ? (
|
||||
<SceneImageGenerationModal
|
||||
profile={draft}
|
||||
landmark={campSceneDraft}
|
||||
onApply={(result) => {
|
||||
setDraft((current) => ({
|
||||
...current,
|
||||
camp: {
|
||||
...resolveCustomWorldCampScene(current),
|
||||
imageSrc: result.imageSrc,
|
||||
},
|
||||
}));
|
||||
}}
|
||||
onClose={() => setIsCampAiGenerateOpen(false)}
|
||||
/>
|
||||
) : null}
|
||||
<SaveBar
|
||||
onClose={onClose}
|
||||
onSave={() => {
|
||||
@@ -1769,6 +1922,21 @@ function LandmarkEditor({
|
||||
npc: CustomWorldNpc;
|
||||
} | null>(null);
|
||||
const presetImages = useMemo(() => getAllCustomWorldSceneImages(), []);
|
||||
const resolvedDraftImageSrc = useMemo(() => {
|
||||
const landmarkIndex = profile.landmarks.findIndex(
|
||||
(entry) => entry.id === draft.id,
|
||||
);
|
||||
|
||||
return resolveCustomWorldLandmarkImage(
|
||||
profile,
|
||||
draft,
|
||||
landmarkIndex >= 0 ? landmarkIndex : profile.landmarks.length,
|
||||
profile.landmarks
|
||||
.filter((entry) => entry.id !== draft.id)
|
||||
.map((entry) => entry.imageSrc)
|
||||
.filter((imageSrc): imageSrc is string => Boolean(imageSrc)),
|
||||
);
|
||||
}, [draft, profile]);
|
||||
const storyNpcById = useMemo(
|
||||
() => new Map(draftStoryNpcs.map((npc) => [npc.id, npc])),
|
||||
[draftStoryNpcs],
|
||||
@@ -1847,7 +2015,7 @@ function LandmarkEditor({
|
||||
<div className="space-y-4">
|
||||
<ImageField
|
||||
label="场景图片"
|
||||
value={draft.imageSrc}
|
||||
value={resolvedDraftImageSrc}
|
||||
onChange={(value) =>
|
||||
setDraft((current) => ({
|
||||
...current,
|
||||
@@ -2367,11 +2535,7 @@ function createLandmark(profile: CustomWorldProfile): CustomWorldLandmark {
|
||||
name: `自定义场景${profile.landmarks.length + 1}`,
|
||||
description: '',
|
||||
dangerLevel: '中',
|
||||
imageSrc: getDefaultCustomWorldSceneImage(
|
||||
profile.id || profile.name,
|
||||
profile.landmarks.length,
|
||||
profile.templateWorldType,
|
||||
),
|
||||
imageSrc: undefined,
|
||||
sceneNpcIds: profile.storyNpcs.slice(0, 3).map((npc) => npc.id),
|
||||
connections: previousLandmark
|
||||
? [
|
||||
|
||||
Reference in New Issue
Block a user