This commit is contained in:
2026-04-24 17:59:48 +08:00
parent 929febb4fe
commit 6cb3efae61
55 changed files with 2373 additions and 435 deletions

View File

@@ -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">
pngjpgwebp 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">
pngjpgwebp 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) =>