Simplify custom world result editing controls
This commit is contained in:
@@ -11,6 +11,8 @@ import {
|
||||
} from '../data/customWorldSceneGraph';
|
||||
import {
|
||||
ActorNarrativeProfile,
|
||||
AnimationState,
|
||||
CharacterAnimationConfig,
|
||||
CharacterBackstoryChapter,
|
||||
CharacterBackstoryRevealConfig,
|
||||
CustomWorldAnchorPack,
|
||||
@@ -35,6 +37,7 @@ import {
|
||||
normalizeCustomWorldCreatorIntent,
|
||||
normalizeCustomWorldLockState,
|
||||
} from './customWorldCreatorIntent';
|
||||
import { normalizeCustomWorldOwnedSettingLayers } from './customWorldOwnedSettingLayers';
|
||||
import {
|
||||
buildFallbackActorNarrativeProfile,
|
||||
normalizeActorNarrativeProfile,
|
||||
@@ -73,6 +76,9 @@ const DEFAULT_CUSTOM_WORLD_ROLE_SKILL_COUNT = 3;
|
||||
const DEFAULT_CUSTOM_WORLD_ROLE_INITIAL_ITEM_COUNT = 3;
|
||||
const CUSTOM_WORLD_BACKSTORY_CHAPTER_AFFINITIES =
|
||||
AFFINITY_BACKSTORY_CHAPTER_THRESHOLDS;
|
||||
const CUSTOM_WORLD_ANIMATION_STATES = new Set<AnimationState>(
|
||||
Object.values(AnimationState),
|
||||
);
|
||||
|
||||
export const MIN_CUSTOM_WORLD_PLAYABLE_NPC_COUNT = 5;
|
||||
export const MIN_CUSTOM_WORLD_TOTAL_NPC_COUNT = 30;
|
||||
@@ -135,6 +141,12 @@ function toText(value: unknown) {
|
||||
return typeof value === 'string' ? value.trim() : '';
|
||||
}
|
||||
|
||||
function toFiniteInteger(value: unknown) {
|
||||
return typeof value === 'number' && Number.isFinite(value)
|
||||
? Math.round(value)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
function toRecordArray(value: unknown) {
|
||||
return Array.isArray(value)
|
||||
? (value.filter((item) => item && typeof item === 'object') as Array<
|
||||
@@ -165,6 +177,59 @@ function normalizeInitialAffinity(value: unknown, fallback: number) {
|
||||
: fallback;
|
||||
}
|
||||
|
||||
function normalizeGeneratedAnimationConfig(
|
||||
value: unknown,
|
||||
): CharacterAnimationConfig | null {
|
||||
if (!value || typeof value !== 'object') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const item = value as Record<string, unknown>;
|
||||
const folder = toText(item.folder);
|
||||
const prefix = toText(item.prefix);
|
||||
const frames = Math.max(1, toFiniteInteger(item.frames) ?? 0);
|
||||
|
||||
if (!folder || !prefix || frames <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const startFrame = toFiniteInteger(item.startFrame);
|
||||
const extension = toText(item.extension);
|
||||
const file = toText(item.file);
|
||||
const basePath = toText(item.basePath);
|
||||
|
||||
return {
|
||||
folder,
|
||||
prefix,
|
||||
frames,
|
||||
...(startFrame ? { startFrame: Math.max(1, startFrame) } : {}),
|
||||
...(extension ? { extension } : {}),
|
||||
...(file ? { file } : {}),
|
||||
...(basePath ? { basePath } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeGeneratedAnimationMap(value: unknown) {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const entries = Object.entries(value).flatMap(([key, rawConfig]) => {
|
||||
if (!CUSTOM_WORLD_ANIMATION_STATES.has(key as AnimationState)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const config = normalizeGeneratedAnimationConfig(rawConfig);
|
||||
return config ? [[key as AnimationState, config] as const] : [];
|
||||
});
|
||||
|
||||
return entries.length > 0
|
||||
? (Object.fromEntries(entries) as Partial<
|
||||
Record<AnimationState, CharacterAnimationConfig>
|
||||
>)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
function normalizeWorldType(value: unknown, sourceText: string) {
|
||||
const worldType = toText(value).toUpperCase();
|
||||
if (worldType === WorldType.WUXIA || worldType === WorldType.XIANXIA) {
|
||||
@@ -184,9 +249,7 @@ function normalizeRarity(
|
||||
function normalizeRoleItemCategory(value: unknown, fallback = '材料') {
|
||||
const category = toText(value);
|
||||
if (
|
||||
(
|
||||
CUSTOM_WORLD_ROLE_ITEM_CATEGORIES as readonly string[]
|
||||
).includes(category)
|
||||
(CUSTOM_WORLD_ROLE_ITEM_CATEGORIES as readonly string[]).includes(category)
|
||||
) {
|
||||
return category === '专属物' ? '专属物品' : category;
|
||||
}
|
||||
@@ -289,7 +352,10 @@ function buildFallbackBackstoryReveal(
|
||||
CUSTOM_WORLD_BACKSTORY_CHAPTER_TITLES[index] ??
|
||||
`背景片段${index + 1}`,
|
||||
affinityRequired,
|
||||
teaser: truncateText(fallbackContents[index] ?? normalizedBackstory, 22),
|
||||
teaser: truncateText(
|
||||
fallbackContents[index] ?? normalizedBackstory,
|
||||
22,
|
||||
),
|
||||
content: truncateText(
|
||||
fallbackContents[index] ?? normalizedBackstory,
|
||||
72,
|
||||
@@ -335,7 +401,8 @@ function normalizeBackstoryReveal(
|
||||
(rawChapter && toText(rawChapter.title)) ||
|
||||
fallbackChapter?.title ||
|
||||
`背景片段${index + 1}`,
|
||||
affinityRequired: fallbackChapter?.affinityRequired ?? defaultAffinity,
|
||||
affinityRequired:
|
||||
fallbackChapter?.affinityRequired ?? defaultAffinity,
|
||||
teaser:
|
||||
(rawChapter && toText(rawChapter.teaser)) ||
|
||||
fallbackChapter?.teaser ||
|
||||
@@ -358,7 +425,8 @@ function buildFallbackRoleSkills(source: CustomWorldRoleFallbackSource) {
|
||||
const skillNameSeed = source.title || source.role || source.name || '角色';
|
||||
const skillSummarySeed =
|
||||
source.combatStyle || source.description || `${source.name}善于把握局势。`;
|
||||
const motivationSeed = source.motivation || source.personality || source.backstory;
|
||||
const motivationSeed =
|
||||
source.motivation || source.personality || source.backstory;
|
||||
|
||||
return [
|
||||
{
|
||||
@@ -447,7 +515,9 @@ function buildFallbackRoleInitialItems(source: CustomWorldRoleFallbackSource) {
|
||||
quantity: 1,
|
||||
rarity: 'rare',
|
||||
description: truncateText(
|
||||
source.backstory || source.motivation || `${source.name}不愿随意交出的信物。`,
|
||||
source.backstory ||
|
||||
source.motivation ||
|
||||
`${source.name}不愿随意交出的信物。`,
|
||||
36,
|
||||
),
|
||||
tags: normalizeTags(
|
||||
@@ -540,7 +610,7 @@ function buildBaseCustomWorldProfile(settingText: string): CustomWorldProfile {
|
||||
templateWorldType,
|
||||
});
|
||||
|
||||
return {
|
||||
const baseProfile = {
|
||||
id: `custom-world-${Date.now().toString(36)}-${slugify(name)}`,
|
||||
settingText: settingText.trim(),
|
||||
name,
|
||||
@@ -573,6 +643,14 @@ function buildBaseCustomWorldProfile(settingText: string): CustomWorldProfile {
|
||||
lockState: normalizeCustomWorldLockState(null),
|
||||
generationMode: 'full',
|
||||
generationStatus: 'complete',
|
||||
} satisfies CustomWorldProfile;
|
||||
|
||||
return {
|
||||
...baseProfile,
|
||||
ownedSettingLayers: normalizeCustomWorldOwnedSettingLayers(
|
||||
null,
|
||||
baseProfile,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -715,7 +793,8 @@ function normalizeRoleProfile(
|
||||
},
|
||||
) {
|
||||
const name = toText(item.name);
|
||||
const title = toText(item.title) || toText(item.role) || options.titleFallback;
|
||||
const title =
|
||||
toText(item.title) || toText(item.role) || options.titleFallback;
|
||||
const role = toText(item.role) || title;
|
||||
const relationshipHooks = normalizeTags(
|
||||
item.relationshipHooks,
|
||||
@@ -741,9 +820,19 @@ function normalizeRoleProfile(
|
||||
|
||||
return {
|
||||
...normalizedRole,
|
||||
backstoryReveal: normalizeBackstoryReveal(item.backstoryReveal, normalizedRole),
|
||||
backstoryReveal: normalizeBackstoryReveal(
|
||||
item.backstoryReveal,
|
||||
normalizedRole,
|
||||
),
|
||||
skills: normalizeRoleSkillList(item.skills, normalizedRole),
|
||||
initialItems: normalizeRoleInitialItemList(item.initialItems, normalizedRole),
|
||||
initialItems: normalizeRoleInitialItemList(
|
||||
item.initialItems,
|
||||
normalizedRole,
|
||||
),
|
||||
imageSrc: toText(item.imageSrc) || undefined,
|
||||
generatedVisualAssetId: toText(item.generatedVisualAssetId) || undefined,
|
||||
generatedAnimationSetId: toText(item.generatedAnimationSetId) || undefined,
|
||||
animationMap: normalizeGeneratedAnimationMap(item.animationMap),
|
||||
narrativeProfile:
|
||||
item.narrativeProfile && typeof item.narrativeProfile === 'object'
|
||||
? (item.narrativeProfile as ActorNarrativeProfile)
|
||||
@@ -767,19 +856,20 @@ function normalizePlayableNpcList(value: unknown) {
|
||||
|
||||
function normalizeStoryNpcList(value: unknown) {
|
||||
return toRecordArray(value)
|
||||
.map((item, index) =>
|
||||
({
|
||||
...normalizeRoleProfile(item, index, {
|
||||
idPrefix: 'story-npc',
|
||||
titleFallback: '未定称号',
|
||||
defaultAffinity: DEFAULT_STORY_NPC_INITIAL_AFFINITY,
|
||||
}),
|
||||
imageSrc: toText(item.imageSrc) || undefined,
|
||||
visual:
|
||||
item.visual && typeof item.visual === 'object'
|
||||
? (item.visual as CustomWorldNpc['visual'])
|
||||
: undefined,
|
||||
}) satisfies CustomWorldNpc,
|
||||
.map(
|
||||
(item, index) =>
|
||||
({
|
||||
...normalizeRoleProfile(item, index, {
|
||||
idPrefix: 'story-npc',
|
||||
titleFallback: '未定称号',
|
||||
defaultAffinity: DEFAULT_STORY_NPC_INITIAL_AFFINITY,
|
||||
}),
|
||||
imageSrc: toText(item.imageSrc) || undefined,
|
||||
visual:
|
||||
item.visual && typeof item.visual === 'object'
|
||||
? (item.visual as CustomWorldNpc['visual'])
|
||||
: undefined,
|
||||
}) satisfies CustomWorldNpc,
|
||||
)
|
||||
.filter((entry) => entry.name);
|
||||
}
|
||||
@@ -812,7 +902,8 @@ function normalizeRoleOutlineList(
|
||||
const normalized = toRecordArray(value)
|
||||
.map((item) => {
|
||||
const name = toText(item.name);
|
||||
const title = toText(item.title) || toText(item.role) || options.titleFallback;
|
||||
const title =
|
||||
toText(item.title) || toText(item.role) || options.titleFallback;
|
||||
const role = toText(item.role) || title;
|
||||
const relationshipHooks = normalizeTags(
|
||||
item.relationshipHooks,
|
||||
@@ -846,12 +937,19 @@ function normalizeCampOutline(
|
||||
value: unknown,
|
||||
fallbackProfile: Pick<
|
||||
CustomWorldProfile,
|
||||
'name' | 'summary' | 'tone' | 'playerGoal' | 'settingText' | 'templateWorldType'
|
||||
| 'name'
|
||||
| 'summary'
|
||||
| 'tone'
|
||||
| 'playerGoal'
|
||||
| 'settingText'
|
||||
| 'templateWorldType'
|
||||
>,
|
||||
): CustomWorldGenerationCampOutline {
|
||||
const fallback = buildFallbackCustomWorldCampScene(fallbackProfile);
|
||||
const item =
|
||||
value && typeof value === 'object' ? (value as Record<string, unknown>) : {};
|
||||
value && typeof value === 'object'
|
||||
? (value as Record<string, unknown>)
|
||||
: {};
|
||||
|
||||
return {
|
||||
name: toText(item.name) || fallback.name,
|
||||
@@ -867,7 +965,8 @@ function normalizeLandmarkOutlineList(value: unknown) {
|
||||
return {
|
||||
name,
|
||||
description:
|
||||
toText(item.description) || truncateText(`${name}暗藏新的局势变化。`, 40),
|
||||
toText(item.description) ||
|
||||
truncateText(`${name}暗藏新的局势变化。`, 40),
|
||||
dangerLevel: toText(item.dangerLevel) || 'medium',
|
||||
sceneNpcNames: [
|
||||
...toStringArray(item.sceneNpcNames),
|
||||
@@ -956,12 +1055,19 @@ function normalizeCampScene(
|
||||
value: unknown,
|
||||
fallbackProfile: Pick<
|
||||
CustomWorldProfile,
|
||||
'name' | 'summary' | 'tone' | 'playerGoal' | 'settingText' | 'templateWorldType'
|
||||
| 'name'
|
||||
| 'summary'
|
||||
| 'tone'
|
||||
| 'playerGoal'
|
||||
| 'settingText'
|
||||
| 'templateWorldType'
|
||||
>,
|
||||
): CustomWorldCampScene {
|
||||
const fallback = buildFallbackCustomWorldCampScene(fallbackProfile);
|
||||
const item =
|
||||
value && typeof value === 'object' ? (value as Record<string, unknown>) : {};
|
||||
value && typeof value === 'object'
|
||||
? (value as Record<string, unknown>)
|
||||
: {};
|
||||
|
||||
return {
|
||||
name: toText(item.name) || fallback.name,
|
||||
@@ -1019,7 +1125,7 @@ export function normalizeCustomWorldProfile(
|
||||
templateWorldType,
|
||||
});
|
||||
|
||||
return {
|
||||
const normalizedProfile = {
|
||||
id:
|
||||
toText(item.id) ||
|
||||
`custom-world-${Date.now().toString(36)}-${slugify(name)}`,
|
||||
@@ -1070,9 +1176,18 @@ export function normalizeCustomWorldProfile(
|
||||
? item.generationMode
|
||||
: fallback.generationMode,
|
||||
generationStatus:
|
||||
item.generationStatus === 'key_only' || item.generationStatus === 'complete'
|
||||
item.generationStatus === 'key_only' ||
|
||||
item.generationStatus === 'complete'
|
||||
? item.generationStatus
|
||||
: fallback.generationStatus,
|
||||
} satisfies CustomWorldProfile;
|
||||
|
||||
return {
|
||||
...normalizedProfile,
|
||||
ownedSettingLayers: normalizeCustomWorldOwnedSettingLayers(
|
||||
item.ownedSettingLayers,
|
||||
normalizedProfile,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1148,7 +1263,7 @@ function buildRoleOutlinePromptLines(
|
||||
.map((role) => {
|
||||
const appearanceText =
|
||||
options.roleType === 'story'
|
||||
? appearanceLookup.get(role.name)?.join('、') ?? '未指定'
|
||||
? (appearanceLookup.get(role.name)?.join('、') ?? '未指定')
|
||||
: '';
|
||||
return [
|
||||
`- ${role.name} / ${role.title}`,
|
||||
@@ -1636,9 +1751,10 @@ export function buildCustomWorldLandmarkNetworkBatchPrompt(params: {
|
||||
storyNpcs: CustomWorldGenerationRoleOutline[];
|
||||
}) {
|
||||
const { framework, landmarkBatch, storyNpcs } = params;
|
||||
const relativePositionValues = CUSTOM_WORLD_SCENE_RELATIVE_POSITION_OPTIONS.map(
|
||||
(option) => option.value,
|
||||
).join('|');
|
||||
const relativePositionValues =
|
||||
CUSTOM_WORLD_SCENE_RELATIVE_POSITION_OPTIONS.map(
|
||||
(option) => option.value,
|
||||
).join('|');
|
||||
const allLandmarkNames = framework.landmarks.map((landmark) => landmark.name);
|
||||
const storyNpcNames = storyNpcs.map((npc) => npc.name);
|
||||
|
||||
@@ -1779,8 +1895,8 @@ export function buildCustomWorldRoleBatchPrompt(params: {
|
||||
'{',
|
||||
` "${key}": [`,
|
||||
' {',
|
||||
' "name": "角色名称",',
|
||||
' "backstoryReveal": {',
|
||||
' "name": "角色名称",',
|
||||
' "backstoryReveal": {',
|
||||
' "publicSummary": "公开可见的背景摘要",',
|
||||
' "chapters": [',
|
||||
` { "id": "surface", "title": "表层来意", "affinityRequired": ${CUSTOM_WORLD_BACKSTORY_CHAPTER_AFFINITIES[0]}, "teaser": "${CUSTOM_WORLD_BACKSTORY_CHAPTER_AFFINITIES[0]}好感可见的提示", "content": "${CUSTOM_WORLD_BACKSTORY_CHAPTER_AFFINITIES[0]}好感时解锁的背景内容", "contextSnippet": "可供后续剧情引用的摘要" },`,
|
||||
@@ -1862,9 +1978,10 @@ export function buildCustomWorldRoleBatchJsonRepairPrompt(params: {
|
||||
}
|
||||
|
||||
export function buildCustomWorldGenerationPrompt(settingText: string) {
|
||||
const relativePositionValues = CUSTOM_WORLD_SCENE_RELATIVE_POSITION_OPTIONS.map(
|
||||
(option) => option.value,
|
||||
).join('|');
|
||||
const relativePositionValues =
|
||||
CUSTOM_WORLD_SCENE_RELATIVE_POSITION_OPTIONS.map(
|
||||
(option) => option.value,
|
||||
).join('|');
|
||||
return [
|
||||
'请根据下面的玩家设定创建一份自定义世界档案。',
|
||||
'你必须只输出一个能被 JSON.parse 直接解析的 JSON 对象,不要输出 Markdown、代码块、注释或解释。',
|
||||
@@ -2005,21 +2122,28 @@ export function buildCustomWorldReferenceText(
|
||||
const landmarkById = new Map(
|
||||
profile.landmarks.map((landmark) => [landmark.id, landmark]),
|
||||
);
|
||||
const themePack = profile.themePack ?? buildThemePackFromWorldProfile(profile);
|
||||
const themePack =
|
||||
profile.themePack ?? buildThemePackFromWorldProfile(profile);
|
||||
const storyGraph =
|
||||
profile.storyGraph ?? buildFallbackWorldStoryGraph(profile, themePack);
|
||||
const activeThreadIds =
|
||||
options.activeThreadIds?.filter(Boolean)?.length
|
||||
? options.activeThreadIds.filter(Boolean)
|
||||
: storyGraph.visibleThreads.slice(0, 3).map((thread) => thread.id);
|
||||
const activeThreads = [...storyGraph.visibleThreads, ...storyGraph.hiddenThreads]
|
||||
const activeThreadIds = options.activeThreadIds?.filter(Boolean)?.length
|
||||
? options.activeThreadIds.filter(Boolean)
|
||||
: storyGraph.visibleThreads.slice(0, 3).map((thread) => thread.id);
|
||||
const activeThreads = [
|
||||
...storyGraph.visibleThreads,
|
||||
...storyGraph.hiddenThreads,
|
||||
]
|
||||
.filter((thread) => activeThreadIds.includes(thread.id))
|
||||
.slice(0, 3);
|
||||
const highlightNpcNames = new Set(
|
||||
(options.highlightNpcNames ?? []).map((name) => name.trim()).filter(Boolean),
|
||||
(options.highlightNpcNames ?? [])
|
||||
.map((name) => name.trim())
|
||||
.filter(Boolean),
|
||||
);
|
||||
const describeNpcReference = (
|
||||
npc: CustomWorldProfile['storyNpcs'][number] | CustomWorldProfile['playableNpcs'][number],
|
||||
npc:
|
||||
| CustomWorldProfile['storyNpcs'][number]
|
||||
| CustomWorldProfile['playableNpcs'][number],
|
||||
) => {
|
||||
const narrativeProfile = normalizeActorNarrativeProfile(
|
||||
npc.narrativeProfile,
|
||||
@@ -2028,9 +2152,11 @@ export function buildCustomWorldReferenceText(
|
||||
|
||||
return `- ${npc.name} / ${npc.title}:身份 ${npc.role};公开面:${narrativeProfile.publicMask};表层线:${narrativeProfile.visibleLine};当前压力:${narrativeProfile.immediatePressure};相关线程:${
|
||||
narrativeProfile.relatedThreadIds
|
||||
.map((threadId) =>
|
||||
[...storyGraph.visibleThreads, ...storyGraph.hiddenThreads]
|
||||
.find((thread) => thread.id === threadId)?.title ?? threadId,
|
||||
.map(
|
||||
(threadId) =>
|
||||
[...storyGraph.visibleThreads, ...storyGraph.hiddenThreads].find(
|
||||
(thread) => thread.id === threadId,
|
||||
)?.title ?? threadId,
|
||||
)
|
||||
.join('、') || '暂无'
|
||||
};反应钩子:${narrativeProfile.reactionHooks.join('、') || '暂无'}`;
|
||||
@@ -2058,7 +2184,9 @@ export function buildCustomWorldReferenceText(
|
||||
};连接:${
|
||||
landmark.connections
|
||||
.map((connection) => {
|
||||
const targetLandmark = landmarkById.get(connection.targetLandmarkId);
|
||||
const targetLandmark = landmarkById.get(
|
||||
connection.targetLandmarkId,
|
||||
);
|
||||
if (!targetLandmark) {
|
||||
return '';
|
||||
}
|
||||
@@ -2110,7 +2238,9 @@ export function validateGeneratedCustomWorldProfile(
|
||||
}
|
||||
|
||||
const validStoryNpcIds = new Set(profile.storyNpcs.map((npc) => npc.id));
|
||||
const validLandmarkIds = new Set(profile.landmarks.map((landmark) => landmark.id));
|
||||
const validLandmarkIds = new Set(
|
||||
profile.landmarks.map((landmark) => landmark.id),
|
||||
);
|
||||
|
||||
profile.landmarks.forEach((landmark) => {
|
||||
const uniqueSceneNpcIds = [...new Set(landmark.sceneNpcIds)];
|
||||
@@ -2185,6 +2315,10 @@ export function buildCustomWorldSceneImagePrompt(
|
||||
'name' | 'subtitle' | 'summary' | 'tone' | 'playerGoal' | 'settingText'
|
||||
>,
|
||||
landmark: Pick<CustomWorldLandmark, 'name' | 'description' | 'dangerLevel'>,
|
||||
userPrompt = '',
|
||||
options: {
|
||||
hasReferenceImage?: boolean;
|
||||
} = {},
|
||||
) {
|
||||
const worldName = clampSceneImageText(profile.name, 18) || '未命名世界';
|
||||
const worldSubtitle = clampSceneImageText(profile.subtitle, 18);
|
||||
@@ -2194,10 +2328,18 @@ export function buildCustomWorldSceneImagePrompt(
|
||||
const worldSetting = clampSceneImageText(profile.settingText, 72);
|
||||
const landmarkName = clampSceneImageText(landmark.name, 18) || '未命名场景';
|
||||
const landmarkDescription = clampSceneImageText(landmark.description, 96);
|
||||
const requestedVisual = clampSceneImageText(userPrompt, 120);
|
||||
const dangerMood = describeDangerLevel(landmark.dangerLevel);
|
||||
|
||||
return [
|
||||
'横版幻想 RPG 场景背景概念图,适合作为 2D 游戏战斗与探索背景,环境主体清晰,空间层次明确,电影感光影,细节丰富。',
|
||||
'为横版 16:9 2D RPG 生成高完成度像素风场景背景,适合作为剧情探索与战斗底图。',
|
||||
'画面构图必须严格按上下 1:1 分区:上半部分严格控制在整张图的 1/2 高度内,只描绘场景远景与中远景轮廓,不要让背景内容向下侵占超过半屏。',
|
||||
'下半部分严格占据整张图的 1/2 高度,用于玩家角色站位与展示,必须是模拟 3D 游戏视角的地面近景,有明确的透视延伸和近大远小关系,不是平铺的 2D 侧视地面。',
|
||||
'下半部分的内容必须是明确可站立的地面本体,例如道路、石板、平台、广场、甲板、沙地或草地,要有连续、稳定、可落脚的站位逻辑,不能只是装饰性前景、坑洞、障碍堆、栏杆带或不可通行的景物。',
|
||||
'下半部分地面近景要保持相对简洁、低细节、轮廓清楚、便于角色站立,不要堆满道具、植被、碎石、栏杆或复杂装饰。',
|
||||
options.hasReferenceImage
|
||||
? '已提供一张自定义参考图,可适度参考其构图、镜头或氛围,但仍以本次场景需求为准,不要生硬照搬。'
|
||||
: '',
|
||||
`世界:${worldName}${worldSubtitle ? `,${worldSubtitle}` : ''}。`,
|
||||
worldSetting ? `玩家设定:${worldSetting}。` : '',
|
||||
worldSummary ? `世界概述:${worldSummary}。` : '',
|
||||
@@ -2205,8 +2347,9 @@ export function buildCustomWorldSceneImagePrompt(
|
||||
worldGoal ? `玩家目标关联:${worldGoal}。` : '',
|
||||
`场景名称:${landmarkName}。`,
|
||||
landmarkDescription ? `场景描述:${landmarkDescription}。` : '',
|
||||
requestedVisual ? `本次想要生成的画面内容:${requestedVisual}。` : '',
|
||||
`${dangerMood}。`,
|
||||
'不要出现 UI、字幕、文字、水印或 logo,人物仅可作为很小的远景剪影,画面重点放在建筑、地貌、光线与氛围。',
|
||||
'不要出现 UI、字幕、文字、水印、logo 或装饰边框,人物仅可作为很小的远景剪影,画面重点放在场景本身,不要遮挡下半部分的角色展示区域。',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join('');
|
||||
|
||||
Reference in New Issue
Block a user