Files
Genarrative/src/prompts/customWorldPrompts.ts
kdletters cbc27bad4a
Some checks failed
CI / verify (push) Has been cancelled
init with react+axum+spacetimedb
2026-04-26 18:06:23 +08:00

1012 lines
52 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { AFFINITY_BACKSTORY_CHAPTER_THRESHOLDS } from '../data/affinityLevels';
import { CUSTOM_WORLD_SCENE_RELATIVE_POSITION_OPTIONS } from '../data/customWorldSceneGraph';
import {
CustomWorldLandmark,
CustomWorldProfile,
ThemePack,
WorldStoryGraph,
} from '../types';
const MIN_CUSTOM_WORLD_PLAYABLE_NPC_COUNT = 5;
const MIN_CUSTOM_WORLD_TOTAL_NPC_COUNT = 30;
const MIN_CUSTOM_WORLD_LANDMARK_COUNT = 10;
const MIN_CUSTOM_WORLD_STORY_NPC_COUNT = Math.max(
0,
MIN_CUSTOM_WORLD_TOTAL_NPC_COUNT - MIN_CUSTOM_WORLD_PLAYABLE_NPC_COUNT,
);
type CustomWorldGenerationRoleBatchType = 'playable' | 'story';
type CustomWorldGenerationRoleBatchStage = 'narrative' | 'dossier';
type CustomWorldGenerationRoleOutline = {
name: string;
title: string;
role: string;
description: string;
visualDescription?: string;
actionDescription?: string;
sceneVisualDescription?: string;
initialAffinity: number;
relationshipHooks: string[];
tags: string[];
};
type CustomWorldGenerationLandmarkOutline = {
name: string;
description: string;
visualDescription?: string;
actNPCNames: string[];
sceneNpcNames?: string[];
connections: Array<{
targetLandmarkName: string;
relativePosition: string;
summary: string;
}>;
};
type CustomWorldGenerationFramework = {
settingText: string;
name: string;
subtitle: string;
summary: string;
tone: string;
playerGoal: string;
templateWorldType: string;
compatibilityTemplateWorldType: string;
majorFactions: string[];
coreConflicts: string[];
camp: {
name: string;
description: string;
};
playableNpcs: CustomWorldGenerationRoleOutline[];
storyNpcs: CustomWorldGenerationRoleOutline[];
landmarks: CustomWorldGenerationLandmarkOutline[];
};
const CUSTOM_WORLD_BACKSTORY_CHAPTER_AFFINITIES =
AFFINITY_BACKSTORY_CHAPTER_THRESHOLDS;
function toText(value: unknown) {
return typeof value === 'string' ? value.trim() : '';
}
function normalizeTags(value: unknown, fallbackTags: string[] = []) {
const tags = Array.isArray(value)
? value.map((item) => toText(item)).filter(Boolean)
: [];
return [
...new Set((tags.length > 0 ? tags : fallbackTags).filter(Boolean)),
].slice(0, 5);
}
function buildFrameworkSummaryText(
framework: CustomWorldGenerationFramework,
options: {
maxLandmarks?: number;
} = {},
) {
const maxLandmarks = options.maxLandmarks ?? MIN_CUSTOM_WORLD_LANDMARK_COUNT;
const landmarkText = framework.landmarks
.slice(0, maxLandmarks)
.map(
(landmark) =>
`${landmark.name}${landmark.description}`,
)
.join('、');
return [
`世界:${framework.name}`,
`副标题:${framework.subtitle}`,
`世界概述:${framework.summary}`,
`世界基调:${framework.tone}`,
`玩家核心目标:${framework.playerGoal}`,
framework.majorFactions.length > 0
? `主要势力:${framework.majorFactions.join('、')}`
: '',
framework.coreConflicts.length > 0
? `核心冲突:${framework.coreConflicts.join('、')}`
: '',
`开局归处:${framework.camp.name}${framework.camp.description}`,
landmarkText ? `关键场景:${landmarkText}` : '',
]
.filter(Boolean)
.join('\n');
}
function buildLandmarkAppearanceLookup(
framework: CustomWorldGenerationFramework,
) {
const lookup = new Map<string, string[]>();
framework.landmarks.forEach((landmark) => {
const npcNames = landmark.actNPCNames.length > 0
? landmark.actNPCNames
: (landmark.sceneNpcNames ?? []);
npcNames.forEach((npcName) => {
const key = npcName.trim();
if (!key) {
return;
}
const current = lookup.get(key) ?? [];
if (!current.includes(landmark.name)) {
current.push(landmark.name);
}
lookup.set(key, current);
});
});
return lookup;
}
function buildRoleOutlinePromptLines(
roleBatch: CustomWorldGenerationRoleOutline[],
options: {
framework: CustomWorldGenerationFramework;
roleType: CustomWorldGenerationRoleBatchType;
},
) {
const appearanceLookup =
options.roleType === 'story'
? buildLandmarkAppearanceLookup(options.framework)
: new Map<string, string[]>();
return roleBatch
.map((role) => {
const appearanceText =
options.roleType === 'story'
? (appearanceLookup.get(role.name)?.join('、') ?? '未指定')
: '';
return [
`- ${role.name} / ${role.title}`,
`身份:${role.role}`,
`框架描述:${role.description}`,
`预设好感:${role.initialAffinity}`,
role.relationshipHooks.length > 0
? `关系切入口:${role.relationshipHooks.join('、')}`
: '',
role.tags.length > 0 ? `标签:${role.tags.join('、')}` : '',
appearanceText ? `出现场景:${appearanceText}` : '',
]
.filter(Boolean)
.join('');
})
.join('\n');
}
export function buildCustomWorldFrameworkPrompt(settingText: string) {
return [
'请先根据下面的玩家设定创建一份“世界核心骨架”,后续我会分步骤生成角色名单、场景名单和详细档案。',
'你必须只输出一个能被 JSON.parse 直接解析的 JSON 对象,不要输出 Markdown、代码块、注释或解释。',
'这一步只保留世界顶层信息与一个开局归处场景,不要输出 playableNpcs、storyNpcs、landmarks也不要展开人物和地图细节。',
'玩家设定:',
settingText.trim(),
'',
'输出 JSON 模板:',
'{',
' "name": "世界名称",',
' "subtitle": "世界副标题",',
' "summary": "世界概述",',
' "tone": "世界基调",',
' "playerGoal": "玩家核心目标",',
' "templateWorldType": "WUXIA|XIANXIA",',
' "majorFactions": ["势力甲", "势力乙"],',
' "coreConflicts": ["冲突甲", "冲突乙"],',
' "camp": {',
' "name": "开局归处名称",',
' "description": "这是玩家进入世界后的第一处落脚点描述",',
' }',
'}',
'',
'要求:',
'- 所有生成文本都必须使用中文。',
'- 这一步只输出顶层 9 个字段name、subtitle、summary、tone、playerGoal、templateWorldType、majorFactions、coreConflicts、camp。',
'- 这是一个完全独立的自定义世界;不要在任何正文里直接写出“武侠世界”“仙侠世界”等现成世界名。',
'- templateWorldType 只是系统兼容字段,不代表正文应当引用的世界名称。',
'- camp 必须表示玩家开局时的落脚处,名字不要直接写成“某某营地”,更接近归舍、住处、栖居、前哨居所这类“家/归处”的概念。',
'- 不要输出 playableNpcs、storyNpcs、landmarks、items也不要输出任何角色和地图细节。',
'- majorFactions 保持 2 到 3 个coreConflicts 保持 2 到 3 个。',
'- 世界设定必须直接源自玩家输入,不要脱离主题乱扩写。',
'- 每个字符串尽量简洁subtitle 控制在 8 到 18 个汉字内summary 控制在 16 到 32 个汉字内tone 控制在 6 到 16 个汉字内playerGoal 控制在 16 到 32 个汉字内camp.description 控制在 18 到 40 个汉字内。',
'- 返回前自检:必须是一个能被 JSON.parse 直接解析的单个 JSON 对象。',
].join('\n');
}
export function buildCustomWorldThemePackPrompt(params: {
framework: CustomWorldGenerationFramework;
}) {
const { framework } = params;
return [
'请根据下面的世界框架,生成一份题材适配层 ThemePack。',
'你必须只输出一个能被 JSON.parse 直接解析的 JSON 对象,不要输出 Markdown、代码块、注释或解释。',
'世界框架摘要:',
buildFrameworkSummaryText(framework, { maxLandmarks: 6 }),
'',
'输出 JSON 模板:',
'{',
' "id": "theme-pack-id",',
' "displayName": "题材包名称",',
' "toneRange": ["基调1", "基调2"],',
' "institutionLexicon": ["制度词1", "制度词2", "制度词3"],',
' "tabooLexicon": ["禁忌词1", "禁忌词2", "禁忌词3"],',
' "artifactClasses": ["载体种类1", "载体种类2", "载体种类3"],',
' "actorArchetypes": ["角色原型1", "角色原型2", "角色原型3"],',
' "conflictForms": ["冲突形式1", "冲突形式2", "冲突形式3"],',
' "clueForms": ["线索形态1", "线索形态2", "线索形态3"],',
' "namingPatterns": ["命名范式1", "命名范式2"],',
' "revealStyles": ["揭示方式1", "揭示方式2"]',
'}',
'',
'要求:',
'- 所有文本必须使用中文。',
'- 输出必须贴合当前世界,不要写泛化奇幻模板。',
'- institutionLexicon / tabooLexicon / artifactClasses / conflictForms / clueForms 至少各给 4 项。',
'- 命名范式要直接服务后续 NPC、场景、物件、文书的统一词根。',
'- 返回前自检:必须是一个能被 JSON.parse 直接解析的单个 JSON 对象。',
].join('\n');
}
export function buildCustomWorldThemePackJsonRepairPrompt(params: {
responseText: string;
}) {
return [
'下面这段文本本应是自定义世界 ThemePack 的单个 JSON 对象,但当前不能被 JSON.parse 直接解析。',
'请只输出修复后的 JSON 对象。',
'顶层必须包含id、displayName、toneRange、institutionLexicon、tabooLexicon、artifactClasses、actorArchetypes、conflictForms、clueForms、namingPatterns、revealStyles。',
'如果缺少数组字段,补空数组;如果缺少字符串字段,补空字符串。',
'原始文本:',
params.responseText.trim(),
].join('\n');
}
export function buildCustomWorldStoryGraphPrompt(params: {
framework: CustomWorldGenerationFramework;
themePack: ThemePack;
}) {
const { framework, themePack } = params;
const roleText = [
...framework.playableNpcs.slice(0, 5),
...framework.storyNpcs.slice(0, 10),
]
.map((role) => `- ${role.name} / ${role.role}${role.description}`)
.join('\n');
const landmarkText = framework.landmarks
.slice(0, 10)
.map((landmark) => `- ${landmark.name}${landmark.description}`)
.join('\n');
return [
'请根据下面的世界框架和 ThemePack生成 WorldStoryGraph。',
'你必须只输出一个能被 JSON.parse 直接解析的 JSON 对象,不要输出 Markdown、代码块、注释或解释。',
'世界框架摘要:',
buildFrameworkSummaryText(framework, { maxLandmarks: 8 }),
'',
`ThemePack${themePack.displayName}`,
`制度词汇:${themePack.institutionLexicon.join('、')}`,
`禁忌词汇:${themePack.tabooLexicon.join('、')}`,
`冲突形式:${themePack.conflictForms.join('、')}`,
`线索形态:${themePack.clueForms.join('、')}`,
'',
`角色索引:\n${roleText}`,
`场景索引:\n${landmarkText}`,
'',
'输出 JSON 模板:',
'{',
' "visibleThreads": [',
' {',
' "id": "visible-thread-1",',
' "title": "明线标题",',
' "visibility": "visible",',
' "summary": "明线摘要",',
' "conflictType": "冲突形式",',
' "stakes": "代价与利害",',
' "involvedFactionIds": ["势力1"],',
' "involvedActorIds": ["角色id1"],',
' "relatedLocationIds": ["场景id1"]',
' }',
' ],',
' "hiddenThreads": [',
' {',
' "id": "hidden-thread-1",',
' "title": "暗线标题",',
' "visibility": "hidden",',
' "summary": "暗线摘要",',
' "conflictType": "冲突形式",',
' "stakes": "代价与利害",',
' "involvedFactionIds": ["势力1"],',
' "involvedActorIds": ["角色id1"],',
' "relatedLocationIds": ["场景id1"]',
' }',
' ],',
' "scars": [',
' {',
' "id": "scar-1",',
' "title": "旧伤标题",',
' "pastEvent": "过去发生的事件",',
' "publicResidue": "表面残痕",',
' "hiddenTruth": "隐藏真相",',
' "relatedActorIds": ["角色id1"],',
' "relatedLocationIds": ["场景id1"]',
' }',
' ],',
' "motifs": [',
' {',
' "id": "motif-1",',
' "label": "意象词根",',
' "semanticRole": "institution|ritual|technology|taboo|ruin|memory|resource|creature",',
' "lexicalHints": ["提示1", "提示2"]',
' }',
' ]',
'}',
'',
'要求:',
'- 至少生成 3 条 visibleThreads、4 条 hiddenThreads、4 条 scars、8 个 motifs。',
'- involvedActorIds / relatedLocationIds 优先使用已给出的真实角色与场景 id。',
'- 所有文本必须使用中文。',
'- 输出要让角色、场景、旧痕之间可互相印证,不要让每条线程彼此无关。',
].join('\n');
}
export function buildCustomWorldStoryGraphJsonRepairPrompt(params: {
responseText: string;
}) {
return [
'下面这段文本本应是自定义世界 WorldStoryGraph 的单个 JSON 对象,但当前不能被 JSON.parse 直接解析。',
'请只输出修复后的 JSON 对象。',
'顶层必须包含visibleThreads、hiddenThreads、scars、motifs。',
'每个线程对象必须包含id、title、visibility、summary、conflictType、stakes、involvedFactionIds、involvedActorIds、relatedLocationIds。',
'每个 scar 必须包含id、title、pastEvent、publicResidue、hiddenTruth、relatedActorIds、relatedLocationIds。',
'每个 motif 必须包含id、label、semanticRole、lexicalHints。',
'原始文本:',
params.responseText.trim(),
].join('\n');
}
export function buildCustomWorldActorNarrativeProfileBatchPrompt(params: {
framework: CustomWorldGenerationFramework;
roleType: CustomWorldGenerationRoleBatchType;
roleBatch: Array<Record<string, unknown>>;
themePack: ThemePack;
storyGraph: WorldStoryGraph;
}) {
const { framework, roleType, roleBatch, themePack, storyGraph } = params;
const key = roleType === 'playable' ? 'playableNpcs' : 'storyNpcs';
const label = roleType === 'playable' ? '可扮演角色' : '场景角色';
const roleText = roleBatch
.map((role) => {
const roleName = toText(role.name);
return `- ${roleName} / ${toText(role.role)}${toText(role.description)};背景:${toText(role.backstory)};动机:${toText(role.motivation)};关系切口:${normalizeTags(role.relationshipHooks).join('、')}`;
})
.join('\n');
const threadText = [...storyGraph.visibleThreads, ...storyGraph.hiddenThreads]
.slice(0, 8)
.map((thread) => `- ${thread.id} / ${thread.title}${thread.summary}`)
.join('\n');
const scarText = storyGraph.scars
.slice(0, 8)
.map((scar) => `- ${scar.id} / ${scar.title}${scar.publicResidue}`)
.join('\n');
return [
`请根据世界框架、ThemePack 和 StoryGraph为这一批${label}生成 ActorNarrativeProfile。`,
'你必须只输出一个能被 JSON.parse 直接解析的 JSON 对象,不要输出 Markdown、代码块、注释或解释。',
'世界框架摘要:',
buildFrameworkSummaryText(framework, { maxLandmarks: 6 }),
'',
`ThemePack${themePack.displayName}`,
`揭示方式:${themePack.revealStyles.join('、')}`,
`命名范式:${themePack.namingPatterns.join('、')}`,
'',
`世界线程:\n${threadText}`,
`世界旧伤:\n${scarText}`,
`本批角色:\n${roleText}`,
'',
'输出 JSON 模板:',
'{',
` "${key}": [`,
' {',
' "name": "角色名称",',
' "narrativeProfile": {',
' "publicMask": "公开面",',
' "firstContactMask": "首遇说辞",',
' "visibleLine": "表层线",',
' "hiddenLine": "隐藏线",',
' "contradiction": "说辞错位",',
' "debtOrBurden": "债务或负担",',
' "taboo": "不愿被提起的禁区",',
' "immediatePressure": "此刻压力",',
' "relatedThreadIds": ["thread-id"],',
' "relatedScarIds": ["scar-id"],',
' "reactionHooks": ["反应钩子1", "反应钩子2"]',
' }',
' }',
' ]',
'}',
'',
'要求:',
'- 名称必须与本批角色完全一致,不得改名。',
'- 每个角色都必须给出 1 个 publicMask、1 个 firstContactMask、1 个 visibleLine、1 个 hiddenLine、1 个 contradiction、1 个 debtOrBurden、1 个 taboo、1 个 immediatePressure。',
'- relatedThreadIds 至少 1 个relatedScarIds 至少 0 到 2 个reactionHooks 至少 2 个。',
'- 低好感角色必须明显表现“压力、错位、钩子”,不要只写冷淡。',
'- 所有文本必须使用中文。',
].join('\n');
}
export function buildCustomWorldActorNarrativeProfileBatchJsonRepairPrompt(params: {
responseText: string;
roleType: CustomWorldGenerationRoleBatchType;
expectedNames: string[];
}) {
const key = params.roleType === 'playable' ? 'playableNpcs' : 'storyNpcs';
return [
`下面这段文本本应是自定义世界角色叙事档案批次的单个 JSON 对象,但当前不能被 JSON.parse 直接解析。`,
'请只输出修复后的 JSON 对象。',
`顶层必须只包含一个 ${key} 数组。`,
`数组里只能保留这些名称:${params.expectedNames.join('、')}`,
'每个角色对象必须包含name、narrativeProfile。',
'narrativeProfile 必须包含publicMask、firstContactMask、visibleLine、hiddenLine、contradiction、debtOrBurden、taboo、immediatePressure、relatedThreadIds、relatedScarIds、reactionHooks。',
'原始文本:',
params.responseText.trim(),
].join('\n');
}
export function buildCustomWorldFrameworkJsonRepairPrompt(
responseText: string,
) {
return [
'下面这段文本本应是自定义世界核心骨架的单个 JSON 对象,但当前不能被 JSON.parse 直接解析。',
'请只输出修复后的 JSON 对象。',
'顶层必须只包含name、subtitle、summary、tone、playerGoal、templateWorldType、majorFactions、coreConflicts、camp。',
'不要输出 playableNpcs、storyNpcs、landmarks、items 或任何其他字段。',
'majorFactions 与 coreConflicts 必须是字符串数组。',
'camp 必须是对象且包含name、description。',
'原始文本:',
responseText.trim(),
].join('\n');
}
export function buildCustomWorldRoleOutlineBatchPrompt(params: {
framework: CustomWorldGenerationFramework;
roleType: CustomWorldGenerationRoleBatchType;
batchCount: number;
forbiddenNames?: string[];
}) {
const { framework, roleType, batchCount, forbiddenNames = [] } = params;
const key = roleType === 'playable' ? 'playableNpcs' : 'storyNpcs';
const label = roleType === 'playable' ? '可扮演角色' : '场景角色';
return [
`请根据下面的世界核心信息,生成一批${label}框架名单。`,
'后续我会继续补全人物档案,所以这一步每个角色只保留最少字段。',
'你必须只输出一个能被 JSON.parse 直接解析的 JSON 对象,不要输出 Markdown、代码块、注释或解释。',
'世界核心信息:',
buildFrameworkSummaryText(framework, { maxLandmarks: 0 }),
forbiddenNames.length > 0
? `这些名字已经生成,禁止重复:${forbiddenNames.join('、')}`
: '',
'',
'输出 JSON 模板:',
'{',
` "${key}": [`,
' {',
' "name": "角色名称",',
' "title": "称号",',
' "role": "身份",',
' "description": "极简定位描述",',
' "visualDescription": "专门用于主形象生成的外观描述",',
' "actionDescription": "专门用于动作生成的动作气质描述",',
' "sceneVisualDescription": "角色常出现的场景氛围描述",',
' "initialAffinity": 18,',
' "relationshipHooks": ["一个关系切入口"],',
' "tags": ["标签1", "标签2"]',
' }',
' ]',
'}',
'',
'要求:',
`- 必须生成恰好 ${batchCount}${label}`,
'- 这是一个完全独立的自定义世界;不要把角色写成来自“武侠世界”“仙侠世界”等现成世界。',
'- 名称必须具体且互不重复,不要使用 角色1、NPC1、场景角色1 之类的占位名。',
'- 只保留name、title、role、description、visualDescription、actionDescription、sceneVisualDescription、initialAffinity、relationshipHooks、tags。',
'- description 控制在 8 到 18 个汉字内,只写角色定位,不写外观。',
'- visualDescription 必须跟随本步骤直接生成,专门描述角色外观,包含轮廓、服饰 / 身体特征、携带物或材质气质,不能复制 description。',
'- actionDescription 专门描述动作气质sceneVisualDescription 专门描述角色常出现的场景氛围。',
'- relationshipHooks 最多 1 条tags 保持 1 到 2 个。',
'- title 和 role 尽量短。',
'- initialAffinity 必须是 -40 到 90 的整数。',
roleType === 'playable'
? '- 可扮演角色的定位必须明显不同,通常使用 18 到 40 的初始好感。'
: '- 场景角色要覆盖势力成员、居民、异类或怪物,不要全是同一种身份;敌对或怪物型角色可以使用负好感。',
'- 所有生成文本都必须使用中文。',
'- 返回前自检:必须是一个能被 JSON.parse 直接解析的单个 JSON 对象。',
]
.filter(Boolean)
.join('\n');
}
export function buildCustomWorldRoleOutlineBatchJsonRepairPrompt(params: {
responseText: string;
roleType: CustomWorldGenerationRoleBatchType;
expectedCount: number;
forbiddenNames?: string[];
}) {
const { responseText, roleType, expectedCount, forbiddenNames = [] } = params;
const key = roleType === 'playable' ? 'playableNpcs' : 'storyNpcs';
return [
`下面这段文本本应是自定义世界${roleType === 'playable' ? '可扮演角色' : '场景角色'}框架名单批次的单个 JSON 对象,但当前不能被 JSON.parse 直接解析。`,
'请只输出修复后的 JSON 对象。',
`顶层必须只包含一个 ${key} 数组。`,
`必须保留恰好 ${expectedCount} 个角色对象。`,
forbiddenNames.length > 0
? `禁止使用这些重复名:${forbiddenNames.join('、')}`
: '',
'每个角色只包含name、title、role、description、visualDescription、actionDescription、sceneVisualDescription、initialAffinity、relationshipHooks、tags。',
'如果缺少字段字符串补空字符串relationshipHooks 和 tags 补空数组initialAffinity 补默认整数。',
'visualDescription 必须是独立外观描述,不能用 description 原文替代。',
'不要输出 backstory、skills、landmarks 或任何其他字段。',
'原始文本:',
responseText.trim(),
]
.filter(Boolean)
.join('\n');
}
export function buildCustomWorldLandmarkSeedBatchPrompt(params: {
framework: CustomWorldGenerationFramework;
batchCount: number;
forbiddenNames?: string[];
}) {
const { framework, batchCount, forbiddenNames = [] } = params;
const allLandmarkNames = framework.landmarks.map((landmark) => landmark.name);
const storyNpcNames = framework.storyNpcs.map((npc) => npc.name);
const relativePositionValues =
CUSTOM_WORLD_SCENE_RELATIVE_POSITION_OPTIONS.map(
(option) => option.value,
).join('|');
return [
'请根据下面的世界核心信息,生成一批场景地标骨架。',
'这一步必须一次性生成场景骨架、逐幕主场景角色和地图连接关系。',
'你必须只输出一个能被 JSON.parse 直接解析的 JSON 对象,不要输出 Markdown、代码块、注释或解释。',
'世界核心信息:',
buildFrameworkSummaryText(framework, { maxLandmarks: 0 }),
allLandmarkNames.length > 0
? `已知场景名:${allLandmarkNames.join('、')}`
: '',
storyNpcNames.length > 0
? `可用场景角色名:${storyNpcNames.join('、')}`
: '',
forbiddenNames.length > 0
? `这些场景名已经生成,禁止重复:${forbiddenNames.join('、')}`
: '',
'',
'输出 JSON 模板:',
'{',
' "landmarks": [',
' {',
' "name": "场景名称",',
' "description": "极简场景描述",',
' "actNPCNames": ["第一幕主场景角色", "第二幕主场景角色", "第三幕主场景角色"],',
' "connections": [',
' {',
' "targetLandmarkName": "其他场景名称",',
` "relativePosition": "${CUSTOM_WORLD_SCENE_RELATIVE_POSITION_OPTIONS[0]?.value ?? 'forward'}",`,
' "summary": "极简通路说明"',
' }',
' ]',
' }',
' ]',
'}',
'',
'要求:',
`- 必须生成恰好 ${batchCount} 个 landmarks。`,
'- 这是一个完全独立的自定义世界;不要在场景描述里直接写“武侠世界”“仙侠世界”等现成世界名。',
'- 这一步只保留name、description、actNPCNames、connections。',
'- 每个场景必须提供 actNPCNames表示第 1/2/3 幕各自的主场景角色,且只能从可用场景角色名里选择;如果可用角色名为空,输出空数组。',
'- 可用场景角色名非空时actNPCNames 必须恰好 3 个;可以重复使用同一角色,但每一项都必须服务对应幕事件。',
`- 每个场景提供 1 到 2 条 connectionsrelativePosition 只能使用:${relativePositionValues}`,
'- targetLandmarkName 优先来自已知场景名或本批新场景名,且不能连接自己。',
'- 不要输出 imageSrc 或其他字段。',
'- 名称必须具体且互不重复,不要使用 场景1、地点1 之类的占位名。',
'- description 控制在 8 到 18 个汉字内。',
'- 所有生成文本都必须使用中文。',
'- 返回前自检:必须是一个能被 JSON.parse 直接解析的单个 JSON 对象。',
]
.filter(Boolean)
.join('\n');
}
export function buildCustomWorldLandmarkSeedBatchJsonRepairPrompt(params: {
responseText: string;
expectedCount: number;
forbiddenNames?: string[];
}) {
const { responseText, expectedCount, forbiddenNames = [] } = params;
return [
'下面这段文本本应是自定义世界场景地标骨架批次的单个 JSON 对象,但当前不能被 JSON.parse 直接解析。',
'请只输出修复后的 JSON 对象。',
'顶层必须只包含一个 landmarks 数组。',
`必须保留恰好 ${expectedCount} 个地标对象。`,
forbiddenNames.length > 0
? `禁止使用这些重复场景名:${forbiddenNames.join('、')}`
: '',
'每个地标只包含name、description、actNPCNames、connections。',
'如果缺少字段字符串补空字符串actNPCNames 和 connections 补空数组。',
'不要输出其他字段。',
'原始文本:',
responseText.trim(),
]
.filter(Boolean)
.join('\n');
}
export function buildCustomWorldRoleBatchPrompt(params: {
framework: CustomWorldGenerationFramework;
roleType: CustomWorldGenerationRoleBatchType;
roleBatch: CustomWorldGenerationRoleOutline[];
stage: CustomWorldGenerationRoleBatchStage;
}) {
const { framework, roleType, roleBatch, stage } = params;
const key = roleType === 'playable' ? 'playableNpcs' : 'storyNpcs';
const label = roleType === 'playable' ? '可扮演角色' : '场景角色';
const roleOutlineText = buildRoleOutlinePromptLines(roleBatch, {
framework,
roleType,
});
if (stage === 'narrative') {
return [
`请根据下面的世界框架,补全这一批${label}的叙事基础设定。`,
'你必须只输出一个能被 JSON.parse 直接解析的 JSON 对象,不要输出 Markdown、代码块、注释或解释。',
'玩家原始设定:',
framework.settingText,
'',
'世界框架摘要:',
buildFrameworkSummaryText(framework, { maxLandmarks: 8 }),
'',
`本批次需要补全的${label}(名称必须原样保留):`,
roleOutlineText,
'',
'输出 JSON 模板:',
'{',
` "${key}": [`,
' {',
' "name": "角色名称",',
' "backstory": "背景经历",',
' "personality": "性格特点",',
' "motivation": "当前动机",',
' "combatStyle": "战斗风格"',
' }',
' ]',
'}',
'',
'要求:',
`- 只输出这批${label},不要输出其他角色、场景或额外顶层字段。`,
'- 这是一个完全独立的自定义世界;不要在角色背景、性格、动机或战斗风格里直接写“武侠世界”“仙侠世界”等现成世界名。',
`- ${key} 的数量必须与本批次名单完全一致。`,
'- 名称必须与批次名单完全一致,不得增删改名。',
'- 只补全 backstory、personality、motivation、combatStyle 这 4 个字段,不要输出 backstoryReveal、skills、initialItems。',
'- 必须严格沿用框架中的 title、role、description、initialAffinity、relationshipHooks、tags 所表达的角色定位,不要改名,不要改阵营。',
'- backstory 必须写出角色和当前世界的具体关系,至少落到一个势力、一个地点、一个正在发生的局势变化,不要只写抽象气质或泛泛成长史。',
'- personality 不能只写单个形容词,要体现角色在这个世界里的处事习惯、应对压力的方式和与人相处的锋面。',
'- motivation 必须是“此刻正在推动角色行动”的现实目标,而不是空泛理想;它要和玩家目标、核心冲突或开局处境形成直接拉扯。',
'- combatStyle 要体现角色为什么会这样战斗,它最好能反映其身份、经历、所属势力或长期栖身的场景环境。',
roleType === 'story'
? '- 怪物型场景角色要在 backstory 或 combatStyle 中直接写出怪物特征、栖息环境或攻击方式。'
: '- 可扮演角色要体现成长空间、协作价值和明确的入队理由。',
'- 所有生成文本都必须使用中文。',
'- 每个字符串尽量简洁但不能空泛backstory/personality/motivation/combatStyle 控制在 18 到 56 个汉字内。',
'- 返回前自检:必须是一个能被 JSON.parse 直接解析的单个 JSON 对象。',
].join('\n');
}
return [
`请根据下面的世界框架,补全这一批${label}的背景章节、技能和初始物品。`,
'你必须只输出一个能被 JSON.parse 直接解析的 JSON 对象,不要输出 Markdown、代码块、注释或解释。',
'玩家原始设定:',
framework.settingText,
'',
'世界框架摘要:',
buildFrameworkSummaryText(framework, { maxLandmarks: 8 }),
'',
`本批次需要补全的${label}(名称必须原样保留):`,
roleOutlineText,
'',
'输出 JSON 模板:',
'{',
` "${key}": [`,
' {',
' "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": "可供后续剧情引用的摘要" },`,
` { "id": "scar", "title": "旧事裂痕", "affinityRequired": ${CUSTOM_WORLD_BACKSTORY_CHAPTER_AFFINITIES[1]}, "teaser": "${CUSTOM_WORLD_BACKSTORY_CHAPTER_AFFINITIES[1]}好感可见的提示", "content": "${CUSTOM_WORLD_BACKSTORY_CHAPTER_AFFINITIES[1]}好感时解锁的背景内容", "contextSnippet": "可供后续剧情引用的摘要" },`,
` { "id": "hidden", "title": "隐藏执念", "affinityRequired": ${CUSTOM_WORLD_BACKSTORY_CHAPTER_AFFINITIES[2]}, "teaser": "${CUSTOM_WORLD_BACKSTORY_CHAPTER_AFFINITIES[2]}好感可见的提示", "content": "${CUSTOM_WORLD_BACKSTORY_CHAPTER_AFFINITIES[2]}好感时解锁的背景内容", "contextSnippet": "可供后续剧情引用的摘要" },`,
` { "id": "final", "title": "最终底牌", "affinityRequired": ${CUSTOM_WORLD_BACKSTORY_CHAPTER_AFFINITIES[3]}, "teaser": "${CUSTOM_WORLD_BACKSTORY_CHAPTER_AFFINITIES[3]}好感可见的提示", "content": "${CUSTOM_WORLD_BACKSTORY_CHAPTER_AFFINITIES[3]}好感时解锁的背景内容", "contextSnippet": "可供后续剧情引用的摘要" }`,
' ]',
' },',
' "skills": [',
' { "name": "技能1", "summary": "技能说明", "style": "起手压制" },',
' { "name": "技能2", "summary": "技能说明", "style": "机动周旋" },',
' { "name": "技能3", "summary": "技能说明", "style": "爆发终结" }',
' ],',
' "initialItems": [',
' { "name": "初始物品1", "category": "武器", "quantity": 1, "rarity": "rare", "description": "物品说明", "tags": ["标签1", "标签2"] },',
' { "name": "初始物品2", "category": "消耗品", "quantity": 2, "rarity": "uncommon", "description": "物品说明", "tags": ["标签1", "标签2"] },',
' { "name": "初始物品3", "category": "专属物品", "quantity": 1, "rarity": "rare", "description": "物品说明", "tags": ["标签1", "标签2"] }',
' ]',
' }',
' ]',
'}',
'',
'要求:',
`- 只输出这批${label},不要输出其他角色、场景或额外顶层字段。`,
'- 这是一个完全独立的自定义世界;不要在公开背景、技能名、物品名或说明里直接带入“武侠”“仙侠”等现成世界名。',
`- ${key} 的数量必须与本批次名单完全一致。`,
'- 名称必须与批次名单完全一致,不得增删改名。',
'- 这一阶段只补全 backstoryReveal、skills、initialItems不要重复输出 title、role、description、backstory、personality、motivation、combatStyle、initialAffinity、relationshipHooks、tags。',
'- 背景章节、技能和初始物品必须严格围绕框架中的角色定位来写,不要改变阵营、身份或出现场景。',
'- backstoryReveal 的 4 章必须形成明显递进:第 1 章写表层来意与第一印象,第 2 章写旧伤或代价,第 3 章写角色真正隐瞒的线索,第 4 章写最终底牌或不可回避的真相。',
'- 每一章都必须紧贴当前世界设定,至少落到具体势力、地点、事件、制度、禁忌或关系链中的一项,不要写成可套用到任何世界的空泛心情。',
'- teaser 必须像“继续相处后能戳到的钩子”content 必须像“真正解锁后得到的新信息”contextSnippet 必须可直接被后续剧情复用,三者不要只是同一句话改写。',
'- skills 不只是职业标签,要体现角色的个人经历、所属阵营、地理环境或禁忌系统影响,尽量写出这个世界独有的招式语感。',
'- initialItems 不只是常规装备清单,至少要有一件能反映角色背景、关系或任务压力的私人物件。',
`- backstoryReveal.chapters 必须恰好 4 章affinityRequired 固定使用 ${CUSTOM_WORLD_BACKSTORY_CHAPTER_AFFINITIES.join('、')}`,
'- 每个角色必须提供恰好 3 个 skills 和恰好 3 个 initialItems。',
'- initialItems.category 只能使用:武器、护甲、饰品、消耗品、材料、稀有品、专属物品。',
roleType === 'story'
? '- 怪物型角色仍然放进 storyNpcs并在 role、description、backstory、combatStyle、tags、backstoryReveal、skills 或 initialItems 中明确写出怪物特征、栖息环境、攻击方式或异形外观。'
: '- 可扮演角色要保持明确的成长空间、协作价值和入队理由。',
'- 所有生成文本都必须使用中文。',
'- 每个字符串尽量简洁但要有信息量backstoryReveal.publicSummary 控制在 14 到 36 个汉字内backstoryReveal.teaser 控制在 12 到 28 个汉字内backstoryReveal.content 控制在 20 到 64 个汉字内contextSnippet 控制在 12 到 36 个汉字内skills.summary 和 initialItems.description 控制在 12 到 32 个汉字内。',
'- 返回前自检:必须是一个能被 JSON.parse 直接解析的单个 JSON 对象。',
].join('\n');
}
export function buildCustomWorldRoleBatchJsonRepairPrompt(params: {
responseText: string;
roleType: CustomWorldGenerationRoleBatchType;
expectedNames: string[];
stage: CustomWorldGenerationRoleBatchStage;
}) {
const { responseText, roleType, expectedNames, stage } = params;
const key = roleType === 'playable' ? 'playableNpcs' : 'storyNpcs';
if (stage === 'narrative') {
return [
`下面这段文本本应是自定义世界${roleType === 'playable' ? '可扮演角色' : '场景角色'}叙事设定批次的单个 JSON 对象,但当前不能被 JSON.parse 直接解析。`,
'请只输出修复后的 JSON 对象。',
`顶层必须只包含一个 ${key} 数组。`,
`这个数组里只能保留这些角色名:${expectedNames.join('、')}`,
'名称必须与名单完全一致,不得增删改名;如果原文遗漏,可按名单顺序补齐占位对象。',
'每个角色都必须包含name、backstory、personality、motivation、combatStyle。',
'如果缺少字段:字符串补空字符串。',
'不要输出 backstoryReveal、skills、initialItems也不要新增名单外的角色。',
'原始文本:',
responseText.trim(),
].join('\n');
}
return [
`下面这段文本本应是自定义世界${roleType === 'playable' ? '可扮演角色' : '场景角色'}档案补全批次的单个 JSON 对象,但当前不能被 JSON.parse 直接解析。`,
'请只输出修复后的 JSON 对象。',
`顶层必须只包含一个 ${key} 数组。`,
`这个数组里只能保留这些角色名:${expectedNames.join('、')}`,
'名称必须与名单完全一致,不得增删改名;如果原文遗漏,可按名单顺序补齐占位对象。',
'每个角色都必须包含name、backstoryReveal、skills、initialItems。',
`backstoryReveal 必须包含 publicSummary 和 4 个 chapterschapters.affinityRequired 固定为 ${CUSTOM_WORLD_BACKSTORY_CHAPTER_AFFINITIES.join('、')}`,
'skills 默认补成 3 个对象,每个对象包含 name、summary、styleinitialItems 默认补成 3 个对象,每个对象包含 name、category、quantity、rarity、description、tags。',
'不要输出 backstory、personality、motivation、combatStyle、landmarks也不要新增名单外的角色。',
'原始文本:',
responseText.trim(),
].join('\n');
}
export function buildCustomWorldGenerationPrompt(settingText: string) {
const relativePositionValues =
CUSTOM_WORLD_SCENE_RELATIVE_POSITION_OPTIONS.map(
(option) => option.value,
).join('|');
return [
'请根据下面的玩家设定创建一份自定义世界档案。',
'你必须只输出一个能被 JSON.parse 直接解析的 JSON 对象,不要输出 Markdown、代码块、注释或解释。',
'玩家设定:',
settingText.trim(),
'',
'输出 JSON 模板:',
'{',
' "name": "世界名称",',
' "subtitle": "世界副标题",',
' "summary": "世界概述",',
' "tone": "世界基调",',
' "playerGoal": "玩家核心目标",',
' "majorFactions": ["势力甲", "势力乙"],',
' "coreConflicts": ["冲突甲", "冲突乙"],',
' "camp": {',
' "name": "开局归处名称",',
' "description": "玩家进入世界后的第一处落脚点描述",',
' },',
' "playableNpcs": [',
' {',
' "name": "角色名称",',
' "title": "称号",',
' "role": "在世界中的身份/职责",',
' "description": "简短描述",',
' "backstory": "背景经历",',
' "personality": "性格特点",',
' "motivation": "当前动机",',
' "combatStyle": "战斗风格",',
' "initialAffinity": 18,',
' "relationshipHooks": ["关系切入口1", "关系切入口2"],',
' "tags": ["标签1", "标签2"],',
' "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": "可供后续剧情引用的摘要" },`,
` { "id": "scar", "title": "旧事裂痕", "affinityRequired": ${CUSTOM_WORLD_BACKSTORY_CHAPTER_AFFINITIES[1]}, "teaser": "${CUSTOM_WORLD_BACKSTORY_CHAPTER_AFFINITIES[1]}好感可见的提示", "content": "${CUSTOM_WORLD_BACKSTORY_CHAPTER_AFFINITIES[1]}好感时解锁的背景内容", "contextSnippet": "可供后续剧情引用的摘要" },`,
` { "id": "hidden", "title": "隐藏执念", "affinityRequired": ${CUSTOM_WORLD_BACKSTORY_CHAPTER_AFFINITIES[2]}, "teaser": "${CUSTOM_WORLD_BACKSTORY_CHAPTER_AFFINITIES[2]}好感可见的提示", "content": "${CUSTOM_WORLD_BACKSTORY_CHAPTER_AFFINITIES[2]}好感时解锁的背景内容", "contextSnippet": "可供后续剧情引用的摘要" },`,
` { "id": "final", "title": "最终底牌", "affinityRequired": ${CUSTOM_WORLD_BACKSTORY_CHAPTER_AFFINITIES[3]}, "teaser": "${CUSTOM_WORLD_BACKSTORY_CHAPTER_AFFINITIES[3]}好感可见的提示", "content": "${CUSTOM_WORLD_BACKSTORY_CHAPTER_AFFINITIES[3]}好感时解锁的背景内容", "contextSnippet": "可供后续剧情引用的摘要" }`,
' ]',
' },',
' "skills": [',
' { "name": "技能1", "summary": "技能说明", "style": "起手压制" },',
' { "name": "技能2", "summary": "技能说明", "style": "机动周旋" },',
' { "name": "技能3", "summary": "技能说明", "style": "爆发终结" }',
' ],',
' "initialItems": [',
' { "name": "初始物品1", "category": "武器", "quantity": 1, "rarity": "rare", "description": "物品说明", "tags": ["标签1", "标签2"] },',
' { "name": "初始物品2", "category": "消耗品", "quantity": 2, "rarity": "uncommon", "description": "物品说明", "tags": ["标签1", "标签2"] },',
' { "name": "初始物品3", "category": "专属物品", "quantity": 1, "rarity": "rare", "description": "物品说明", "tags": ["标签1", "标签2"] }',
' ]',
' }',
' ],',
' "storyNpcs": [',
' {',
' "name": "场景角色名称",',
' "title": "称号",',
' "role": "身份",',
' "description": "简短描述",',
' "backstory": "背景经历",',
' "personality": "性格特点",',
' "motivation": "动机",',
' "combatStyle": "战斗风格",',
' "initialAffinity": 6,',
' "relationshipHooks": ["关系切入口1", "关系切入口2"],',
' "tags": ["标签1", "标签2"],',
' "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": "可供后续剧情引用的摘要" },`,
` { "id": "scar", "title": "旧事裂痕", "affinityRequired": ${CUSTOM_WORLD_BACKSTORY_CHAPTER_AFFINITIES[1]}, "teaser": "${CUSTOM_WORLD_BACKSTORY_CHAPTER_AFFINITIES[1]}好感可见的提示", "content": "${CUSTOM_WORLD_BACKSTORY_CHAPTER_AFFINITIES[1]}好感时解锁的背景内容", "contextSnippet": "可供后续剧情引用的摘要" },`,
` { "id": "hidden", "title": "隐藏执念", "affinityRequired": ${CUSTOM_WORLD_BACKSTORY_CHAPTER_AFFINITIES[2]}, "teaser": "${CUSTOM_WORLD_BACKSTORY_CHAPTER_AFFINITIES[2]}好感可见的提示", "content": "${CUSTOM_WORLD_BACKSTORY_CHAPTER_AFFINITIES[2]}好感时解锁的背景内容", "contextSnippet": "可供后续剧情引用的摘要" },`,
` { "id": "final", "title": "最终底牌", "affinityRequired": ${CUSTOM_WORLD_BACKSTORY_CHAPTER_AFFINITIES[3]}, "teaser": "${CUSTOM_WORLD_BACKSTORY_CHAPTER_AFFINITIES[3]}好感可见的提示", "content": "${CUSTOM_WORLD_BACKSTORY_CHAPTER_AFFINITIES[3]}好感时解锁的背景内容", "contextSnippet": "可供后续剧情引用的摘要" }`,
' ]',
' },',
' "skills": [',
' { "name": "技能1", "summary": "技能说明", "style": "起手压制" },',
' { "name": "技能2", "summary": "技能说明", "style": "机动周旋" },',
' { "name": "技能3", "summary": "技能说明", "style": "爆发终结" }',
' ],',
' "initialItems": [',
' { "name": "初始物品1", "category": "武器", "quantity": 1, "rarity": "rare", "description": "物品说明", "tags": ["标签1", "标签2"] },',
' { "name": "初始物品2", "category": "消耗品", "quantity": 2, "rarity": "uncommon", "description": "物品说明", "tags": ["标签1", "标签2"] },',
' { "name": "初始物品3", "category": "专属物品", "quantity": 1, "rarity": "rare", "description": "物品说明", "tags": ["标签1", "标签2"] }',
' ]',
' }',
' ],',
' "landmarks": [',
' {',
' "name": "场景名称",',
' "description": "场景描述",',
' "actNPCNames": ["第一幕主场景角色", "第二幕主场景角色", "第三幕主场景角色"],',
' "connections": [',
' {',
' "targetLandmarkName": "相邻场景名称",',
` "relativePosition": "${CUSTOM_WORLD_SCENE_RELATIVE_POSITION_OPTIONS[0]?.value ?? 'forward'}",`,
' "summary": "两处场景之间的相对位置或通路说明"',
' }',
' ]',
' }',
' ]',
'}',
'',
'要求:',
'- 所有生成文本都必须使用中文。',
'- camp 必须存在,代表玩家开局时的落脚处;名字不要直接写成“某某营地”,更接近归舍、住处、栖居、前哨居所这类“家/归处”的概念。',
'- 必须生成恰好 5 个 playableNpcs。',
'- 至少生成 25 个 storyNpcs并保证 playableNpcs + storyNpcs 的唯一名称总数不少于 30。',
'- 至少生成 10 个真正可游玩的 landmarks。',
'- playableNpcs 与 storyNpcs 必须使用完全相同的字段结构name、title、role、description、backstory、personality、motivation、combatStyle、initialAffinity、relationshipHooks、tags、backstoryReveal、skills、initialItems。',
`- backstoryReveal.chapters 必须恰好 4 章affinityRequired 固定使用 ${CUSTOM_WORLD_BACKSTORY_CHAPTER_AFFINITIES.join('、')}`,
'- 每个 NPC 必须提供恰好 3 个 skills 和恰好 3 个 initialItems。',
'- 每个 landmark 必须提供 actNPCNames 和 connections 两个字段。',
'- 每个 landmark.actNPCNames 必须恰好 3 个来自 storyNpcs 的角色名,不要引用 playableNpcs可以重复使用同一角色但每一项表示对应幕的主场景角色。',
`- 每个 landmark.connections 至少包含 2 条连接relativePosition 只能使用:${relativePositionValues}`,
'- landmark.connections.targetLandmarkName 必须指向 landmarks 中真实存在的其他场景,不能引用自己。',
'- initialItems.category 只能使用:武器、护甲、饰品、消耗品、材料、稀有品、专属物品。',
'- initialAffinity 必须是 -40 到 90 的整数;可扮演角色通常不低于 18敌对或怪物型 NPC 应使用负数。',
'- 每个场景角色和地标都必须直接源自玩家设定。',
'- 怪物型角色仍然放进 storyNpcs并在 role、description、backstory、combatStyle、tags、backstoryReveal、skills 或 initialItems 中明确写出怪物特征、栖息环境、攻击方式或异形外观,方便后续形象解析同时引用 Medieval 和怪物素材。',
'- 场景之间的连接关系要形成可遍历的地图网络,并在 summary 中直接写出相对位置、通路或地理关系。',
'- 每个字符串尽量简洁description/backstory/personality/motivation/combatStyle 控制在 10 到 40 个汉字内backstoryReveal.content 控制在 12 到 36 个汉字内skills.summary 和 initialItems.description 控制在 8 到 24 个汉字内。',
'- 名称必须具体且有辨识度,不要使用 角色1、NPC1、场景1 之类的占位名。',
'- 返回前自检:必须是一个能被 JSON.parse 直接解析的单个 JSON 对象。',
].join('\n');
}
function clampSceneImageText(value: string, maxLength: number) {
const normalized = value.trim().replace(/\s+/g, ' ');
if (!normalized) {
return '';
}
if (normalized.length <= maxLength) {
return normalized;
}
return `${normalized.slice(0, Math.max(0, maxLength - 1)).trim()}`;
}
export const DEFAULT_CUSTOM_WORLD_SCENE_IMAGE_NEGATIVE_PROMPT = [
'文字',
'水印',
'logo',
'UI界面',
'对话框',
'边框',
'人物近景特写',
'多人合照',
'模糊',
'低清晰度',
'畸形建筑',
'现代车辆',
'监控摄像头',
].join('');
export function buildCustomWorldSceneImagePrompt(
profile: Pick<
CustomWorldProfile,
'name' | 'subtitle' | 'summary' | 'tone' | 'playerGoal' | 'settingText'
>,
landmark: Pick<CustomWorldLandmark, 'name' | 'description'>,
userPrompt = '',
options: {
hasReferenceImage?: boolean;
} = {},
) {
const worldName = clampSceneImageText(profile.name, 18) || '未命名世界';
const worldSubtitle = clampSceneImageText(profile.subtitle, 18);
const worldTone = clampSceneImageText(profile.tone, 48);
const worldGoal = clampSceneImageText(profile.playerGoal, 48);
const worldSummary = clampSceneImageText(profile.summary, 72);
const worldSetting = clampSceneImageText(profile.settingText, 72);
const landmarkName = clampSceneImageText(landmark.name, 18) || '未命名场景';
const landmarkDescription = clampSceneImageText(landmark.description, 96);
const requestedVisual = clampSceneImageText(userPrompt, 120);
return [
'为横版 16:9 2D RPG 生成高完成度像素风场景背景,适合作为剧情探索与战斗底图。',
'画面构图必须严格按上下 1:1 分区:上半部分严格控制在整张图的 1/2 高度内,只描绘场景远景与中远景轮廓,不要让背景内容向下侵占超过半屏。',
'下半部分严格占据整张图的 1/2 高度,用于玩家角色站位与展示,必须是模拟 3D 游戏视角的地面近景,有明确的透视延伸和近大远小关系,不是平铺的 2D 侧视地面。',
'下半部分的内容必须是明确可站立的地面本体,例如道路、石板、平台、广场、甲板、沙地或草地,要有连续、稳定、可落脚的站位逻辑,不能只是装饰性前景、坑洞、障碍堆、栏杆带或不可通行的景物。',
'下半部分地面近景要保持相对简洁、低细节、轮廓清楚、便于角色站立,不要堆满道具、植被、碎石、栏杆或复杂装饰。',
options.hasReferenceImage
? '已提供一张自定义参考图,请沿用其构图、镜头或氛围线索,同时继续满足本次场景需求。'
: '',
`世界:${worldName}${worldSubtitle ? `${worldSubtitle}` : ''}`,
worldSetting ? `玩家设定:${worldSetting}` : '',
worldSummary ? `世界概述:${worldSummary}` : '',
worldTone ? `整体基调:${worldTone}` : '',
worldGoal ? `玩家目标关联:${worldGoal}` : '',
`场景名称:${landmarkName}`,
landmarkDescription ? `场景描述:${landmarkDescription}` : '',
requestedVisual ? `本次想要生成的画面内容:${requestedVisual}` : '',
'不要出现 UI、字幕、文字、水印、logo 或装饰边框,人物仅可作为很小的远景剪影,画面重点放在场景本身,不要遮挡下半部分的角色展示区域。',
]
.filter(Boolean)
.join('');
}