1
This commit is contained in:
@@ -202,6 +202,10 @@ function dedupeTextValues(values: Array<string | null | undefined>) {
|
||||
];
|
||||
}
|
||||
|
||||
function compactTextList(values: Array<string | null | undefined>) {
|
||||
return values.map((value) => value?.trim() ?? '').filter(Boolean);
|
||||
}
|
||||
|
||||
function moveArrayItem<T>(values: T[], fromIndex: number, toIndex: number) {
|
||||
if (
|
||||
fromIndex < 0 ||
|
||||
@@ -326,19 +330,26 @@ function buildDefaultSceneActBlueprint(params: {
|
||||
const encounterNpcIds = dedupeTextValues(params.encounterNpcIds).slice(0, 1);
|
||||
const actTitle = buildDefaultSceneActTitle(params.index);
|
||||
const sceneLabel = params.sceneName.trim() || '当前场景';
|
||||
const sceneSummary = params.sceneSummary.trim();
|
||||
const stageCoverage = buildSceneActStageCoverage(params.index, params.actCount);
|
||||
const actSummary =
|
||||
params.index === 0
|
||||
? `玩家会在${sceneLabel}接住这一章的开场入口。`
|
||||
: params.index >= params.actCount - 1
|
||||
? `${sceneLabel}这一章会在这里把下一步方向抛给玩家。`
|
||||
: `${sceneLabel}的主要压力会在这一幕继续加深。`;
|
||||
|
||||
return {
|
||||
id: `${params.sceneId}-act-${params.index + 1}`,
|
||||
sceneId: params.sceneId,
|
||||
title: actTitle,
|
||||
summary:
|
||||
params.index === 0
|
||||
? `玩家会在${sceneLabel}接住这一章的开场入口。`
|
||||
: params.index >= params.actCount - 1
|
||||
? `${sceneLabel}这一章会在这里把下一步方向抛给玩家。`
|
||||
: `${sceneLabel}的主要压力会在这一幕继续加深。`,
|
||||
summary: actSummary,
|
||||
stageCoverage,
|
||||
backgroundPromptText: compactTextList([
|
||||
`${sceneLabel}${actTitle}背景`,
|
||||
sceneSummary,
|
||||
actSummary,
|
||||
]).join(';'),
|
||||
backgroundImageSrc: params.backgroundImageSrc || undefined,
|
||||
encounterNpcIds,
|
||||
primaryNpcId: encounterNpcIds[0] ?? '',
|
||||
@@ -461,6 +472,9 @@ function sanitizeSceneChapterBlueprint(params: {
|
||||
title: currentAct?.title?.trim() || fallbackAct.title,
|
||||
summary: currentAct?.summary?.trim() || fallbackAct.summary,
|
||||
stageCoverage: buildSceneActStageCoverage(index, targetActCount),
|
||||
backgroundPromptText:
|
||||
currentAct?.backgroundPromptText?.trim() ||
|
||||
fallbackAct.backgroundPromptText,
|
||||
backgroundImageSrc:
|
||||
currentAct?.backgroundImageSrc?.trim() ||
|
||||
params.fallbackImageSrc ||
|
||||
@@ -2391,15 +2405,18 @@ const FIXED_SCENE_IMAGE_SIZE = '1280*720';
|
||||
function SceneImageGenerationModal({
|
||||
profile,
|
||||
landmark,
|
||||
initialPromptText,
|
||||
onApply,
|
||||
onClose,
|
||||
}: {
|
||||
profile: CustomWorldProfile;
|
||||
landmark: CustomWorldLandmark;
|
||||
initialPromptText?: string;
|
||||
onApply: (result: CustomWorldSceneImageResult) => void;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const [userPrompt, setUserPrompt] = useDraft(
|
||||
initialPromptText?.trim() ||
|
||||
landmark.visualDescription?.trim() ||
|
||||
landmark.description.trim() ||
|
||||
landmark.name.trim(),
|
||||
@@ -2504,12 +2521,12 @@ function SceneImageGenerationModal({
|
||||
<ModalShell
|
||||
title={`智能生成:${landmark.name || '当前场景'}`}
|
||||
onClose={handleRequestClose}
|
||||
panelClassName="sm:max-w-5xl"
|
||||
panelClassName="sm:max-w-4xl"
|
||||
overlayClassName="z-[99]"
|
||||
disableClose={isGenerating}
|
||||
usePixelFont
|
||||
>
|
||||
<div className="grid gap-5 lg:grid-cols-[minmax(0,1.1fr)_minmax(20rem,0.9fr)]">
|
||||
<div className="grid gap-5 lg:grid-cols-[minmax(0,1.15fr)_minmax(17rem,0.85fr)]">
|
||||
<div className="space-y-4">
|
||||
<Field label="画面内容描述">
|
||||
<TextArea
|
||||
@@ -2640,6 +2657,7 @@ function SceneImageGenerationModal({
|
||||
function SceneActBackgroundModal({
|
||||
profile,
|
||||
landmark,
|
||||
act,
|
||||
actLabel,
|
||||
currentImageSrc,
|
||||
fallbackImageSrc,
|
||||
@@ -2648,6 +2666,7 @@ function SceneActBackgroundModal({
|
||||
}: {
|
||||
profile: CustomWorldProfile;
|
||||
landmark: CustomWorldLandmark;
|
||||
act: SceneActBlueprint;
|
||||
actLabel: string;
|
||||
currentImageSrc?: string | null;
|
||||
fallbackImageSrc?: string | null;
|
||||
@@ -2738,6 +2757,10 @@ function SceneActBackgroundModal({
|
||||
<SceneImageGenerationModal
|
||||
profile={profile}
|
||||
landmark={landmark}
|
||||
initialPromptText={
|
||||
act.backgroundPromptText?.trim() ||
|
||||
compactTextList([act.title, act.summary, act.actGoal]).join(';')
|
||||
}
|
||||
onApply={(result) => {
|
||||
setDraftImageSrc(result.imageSrc);
|
||||
}}
|
||||
@@ -3027,7 +3050,7 @@ function CoverImageGenerationModal({
|
||||
<TextArea
|
||||
value={userPrompt}
|
||||
onChange={(value) => setUserPrompt(value)}
|
||||
rows={7}
|
||||
rows={5}
|
||||
placeholder="例如:海雾压进旧码头,主角站在残灯与潮水之间,整体像一张正式 RPG 作品封面。"
|
||||
/>
|
||||
</Field>
|
||||
@@ -3077,7 +3100,7 @@ function CoverImageGenerationModal({
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="rounded-2xl border border-white/8 bg-black/18 p-3">
|
||||
<div className="rounded-2xl border border-white/8 bg-black/18 p-2.5">
|
||||
<CustomWorldCoverArtwork
|
||||
imageSrc={previewImageSrc}
|
||||
title={profile.name}
|
||||
@@ -3088,7 +3111,7 @@ function CoverImageGenerationModal({
|
||||
characterImageSrcs={
|
||||
latestResult ? [] : initialPresentation.characterImageSrcs
|
||||
}
|
||||
className="aspect-[16/9] rounded-2xl"
|
||||
className="aspect-[16/9] max-h-[14rem] rounded-2xl"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -3249,77 +3272,83 @@ export function WorldCoverEditor({
|
||||
|
||||
return (
|
||||
<>
|
||||
<ModalShell title="编辑作品封面" onClose={onClose}>
|
||||
<div className="space-y-4">
|
||||
<div className="rounded-2xl border border-white/8 bg-black/18 p-3">
|
||||
<ModalShell
|
||||
title="编辑作品封面"
|
||||
onClose={onClose}
|
||||
panelClassName="sm:max-w-3xl"
|
||||
>
|
||||
<div className="grid gap-4 md:grid-cols-[minmax(0,0.95fr)_minmax(17rem,1.05fr)]">
|
||||
<div className="rounded-2xl border border-white/8 bg-black/18 p-2.5">
|
||||
<CustomWorldCoverArtwork
|
||||
imageSrc={previewPresentation.imageSrc}
|
||||
title={profile.name}
|
||||
fallbackLabel={profile.name.slice(0, 4) || '封面'}
|
||||
renderMode={previewPresentation.renderMode}
|
||||
characterImageSrcs={previewPresentation.characterImageSrcs}
|
||||
className="aspect-[16/9] rounded-2xl"
|
||||
className="aspect-[16/9] max-h-[13rem] rounded-2xl"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<span className="rounded-full border border-white/10 bg-black/24 px-3 py-1 text-[11px] text-zinc-200">
|
||||
{draftCover.sourceType === 'uploaded'
|
||||
? '当前为上传封面'
|
||||
: draftCover.sourceType === 'generated'
|
||||
? '当前为 AI 封面'
|
||||
: '当前为默认封面'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<span className="rounded-full border border-white/10 bg-black/24 px-3 py-1 text-[11px] text-zinc-200">
|
||||
{draftCover.sourceType === 'uploaded'
|
||||
? '当前为上传封面'
|
||||
: draftCover.sourceType === 'generated'
|
||||
? '当前为 AI 封面'
|
||||
: '当前为默认封面'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<label className="block rounded-2xl border border-dashed border-white/12 bg-black/20 px-4 py-4">
|
||||
<div className="mb-3 text-[11px] font-bold tracking-[0.14em] text-zinc-300">
|
||||
上传封面
|
||||
<label className="block rounded-2xl border border-dashed border-white/12 bg-black/20 px-4 py-4">
|
||||
<div className="mb-3 text-[11px] font-bold tracking-[0.14em] text-zinc-300">
|
||||
上传封面
|
||||
</div>
|
||||
<div className="mb-3 text-xs leading-5 text-zinc-400">
|
||||
支持 png、jpg、webp。上传后会先裁剪成 16:9,再保存成封面。
|
||||
</div>
|
||||
<input
|
||||
type="file"
|
||||
accept="image/png,image/jpeg,image/webp"
|
||||
onChange={(event) => {
|
||||
void handleUploadCover(event);
|
||||
}}
|
||||
className="w-full text-xs text-zinc-300 file:mr-3 file:rounded-lg file:border-0 file:bg-sky-500 file:px-3 file:py-1.5 file:text-xs file:font-medium file:text-black"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<ActionButton
|
||||
label="AI 生成"
|
||||
onClick={() => setIsGenerating(true)}
|
||||
tone="sky"
|
||||
/>
|
||||
<ActionButton
|
||||
label="重置为默认"
|
||||
onClick={() =>
|
||||
setDraftCover(buildDefaultCustomWorldCoverProfile(profile))
|
||||
}
|
||||
disabled={draftCover.sourceType === 'default'}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3 text-xs leading-5 text-zinc-400">
|
||||
支持 png、jpg、webp。上传后会先裁剪成 16:9,再保存成封面。
|
||||
</div>
|
||||
<input
|
||||
type="file"
|
||||
accept="image/png,image/jpeg,image/webp"
|
||||
onChange={(event) => {
|
||||
void handleUploadCover(event);
|
||||
|
||||
{uploadError ? (
|
||||
<div className="rounded-2xl border border-rose-400/18 bg-rose-500/10 px-4 py-3 text-sm leading-6 text-rose-100">
|
||||
{uploadError}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<SaveBar
|
||||
onClose={onClose}
|
||||
onSave={() => {
|
||||
onSaveProfile({
|
||||
...profile,
|
||||
cover: draftCover,
|
||||
});
|
||||
onClose();
|
||||
}}
|
||||
className="w-full text-xs text-zinc-300 file:mr-3 file:rounded-lg file:border-0 file:bg-sky-500 file:px-3 file:py-1.5 file:text-xs file:font-medium file:text-black"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<ActionButton
|
||||
label="AI 生成"
|
||||
onClick={() => setIsGenerating(true)}
|
||||
tone="sky"
|
||||
/>
|
||||
<ActionButton
|
||||
label="重置为默认"
|
||||
onClick={() =>
|
||||
setDraftCover(buildDefaultCustomWorldCoverProfile(profile))
|
||||
}
|
||||
disabled={draftCover.sourceType === 'default'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{uploadError ? (
|
||||
<div className="rounded-2xl border border-rose-400/18 bg-rose-500/10 px-4 py-3 text-sm leading-6 text-rose-100">
|
||||
{uploadError}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<SaveBar
|
||||
onClose={onClose}
|
||||
onSave={() => {
|
||||
onSaveProfile({
|
||||
...profile,
|
||||
cover: draftCover,
|
||||
});
|
||||
onClose();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</ModalShell>
|
||||
|
||||
@@ -4787,6 +4816,7 @@ export function PlayableNpcEditor({
|
||||
<RpgCreationRoleAssetStudioModal
|
||||
role={draft}
|
||||
roleKind="playable"
|
||||
cacheScopeId={profile.id}
|
||||
onApply={(nextRole) =>
|
||||
setDraft((current) => ({
|
||||
...current,
|
||||
@@ -5083,6 +5113,7 @@ export function StoryNpcEditor({
|
||||
<RpgCreationRoleAssetStudioModal
|
||||
role={draft}
|
||||
roleKind="story"
|
||||
cacheScopeId={profile.id}
|
||||
onApply={(nextRole) =>
|
||||
setDraft((current) => ({
|
||||
...current,
|
||||
@@ -5781,6 +5812,7 @@ export function LandmarkEditor({
|
||||
activeSceneActBackgroundDraft.title.trim() ||
|
||||
buildDefaultSceneActTitle(activeSceneActBackgroundIndex)
|
||||
}
|
||||
act={activeSceneActBackgroundDraft}
|
||||
currentImageSrc={activeSceneActBackgroundDraft.backgroundImageSrc}
|
||||
fallbackImageSrc={resolvedDraftImageSrc}
|
||||
onApply={(imageSrc) =>
|
||||
|
||||
Reference in New Issue
Block a user