Rework story engine flow and reorganize project docs
Some checks failed
CI / verify (push) Has been cancelled
Some checks failed
CI / verify (push) Has been cancelled
This commit is contained in:
@@ -10,8 +10,10 @@ import {
|
||||
normalizeCustomWorldLandmarks,
|
||||
} from '../data/customWorldSceneGraph';
|
||||
import {
|
||||
ActorNarrativeProfile,
|
||||
CharacterBackstoryChapter,
|
||||
CharacterBackstoryRevealConfig,
|
||||
CustomWorldAnchorPack,
|
||||
CustomWorldItem,
|
||||
CustomWorldLandmark,
|
||||
CustomWorldNpc,
|
||||
@@ -20,9 +22,23 @@ import {
|
||||
CustomWorldRoleInitialItem,
|
||||
CustomWorldRoleSkill,
|
||||
ItemRarity,
|
||||
ThemePack,
|
||||
WorldStoryGraph,
|
||||
WorldType,
|
||||
} from '../types';
|
||||
import { generateWorldAttributeSchema } from './attributeSchemaGenerator';
|
||||
import {
|
||||
buildCustomWorldAnchorPackFromIntent,
|
||||
deriveCustomWorldLockStateFromIntent,
|
||||
normalizeCustomWorldCreatorIntent,
|
||||
normalizeCustomWorldLockState,
|
||||
} from './customWorldCreatorIntent';
|
||||
import {
|
||||
buildFallbackActorNarrativeProfile,
|
||||
normalizeActorNarrativeProfile,
|
||||
} from './storyEngine/actorNarrativeProfile';
|
||||
import { buildThemePackFromWorldProfile } from './storyEngine/themePack';
|
||||
import { buildFallbackWorldStoryGraph } from './storyEngine/worldStoryGraph';
|
||||
|
||||
const CUSTOM_WORLD_RARITIES: ItemRarity[] = [
|
||||
'common',
|
||||
@@ -528,6 +544,8 @@ function buildBaseCustomWorldProfile(settingText: string): CustomWorldProfile {
|
||||
tone,
|
||||
playerGoal,
|
||||
templateWorldType,
|
||||
majorFactions: [],
|
||||
coreConflicts: [summary],
|
||||
attributeSchema: generateWorldAttributeSchema({
|
||||
worldType: WorldType.CUSTOM,
|
||||
worldName: name,
|
||||
@@ -542,6 +560,13 @@ function buildBaseCustomWorldProfile(settingText: string): CustomWorldProfile {
|
||||
storyNpcs: [],
|
||||
items: [],
|
||||
landmarks: [],
|
||||
themePack: null,
|
||||
storyGraph: null,
|
||||
creatorIntent: null,
|
||||
anchorPack: null,
|
||||
lockState: normalizeCustomWorldLockState(null),
|
||||
generationMode: 'full',
|
||||
generationStatus: 'complete',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -673,7 +698,7 @@ function normalizeRoleProfile(
|
||||
normalizeTags(item.tags),
|
||||
);
|
||||
const normalizedRole = {
|
||||
id: createEntryId(options.idPrefix, name, index),
|
||||
id: toText(item.id) || createEntryId(options.idPrefix, name, index),
|
||||
name,
|
||||
title,
|
||||
role,
|
||||
@@ -695,6 +720,10 @@ function normalizeRoleProfile(
|
||||
backstoryReveal: normalizeBackstoryReveal(item.backstoryReveal, normalizedRole),
|
||||
skills: normalizeRoleSkillList(item.skills, normalizedRole),
|
||||
initialItems: normalizeRoleInitialItemList(item.initialItems, normalizedRole),
|
||||
narrativeProfile:
|
||||
item.narrativeProfile && typeof item.narrativeProfile === 'object'
|
||||
? (item.narrativeProfile as ActorNarrativeProfile)
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -737,7 +766,7 @@ function normalizeItemList(value: unknown) {
|
||||
const name = toText(item.name);
|
||||
const category = toText(item.category);
|
||||
return {
|
||||
id: createEntryId('item', name, index),
|
||||
id: toText(item.id) || createEntryId('item', name, index),
|
||||
name,
|
||||
category,
|
||||
rarity: normalizeRarity(item.rarity, 'rare'),
|
||||
@@ -854,7 +883,7 @@ function normalizeLandmarkDraftList(value: unknown) {
|
||||
.map((item, index) => {
|
||||
const name = toText(item.name);
|
||||
return {
|
||||
id: createEntryId('landmark', name, index),
|
||||
id: toText(item.id) || createEntryId('landmark', name, index),
|
||||
name,
|
||||
description: toText(item.description),
|
||||
dangerLevel: toText(item.dangerLevel),
|
||||
@@ -922,7 +951,9 @@ export function normalizeCustomWorldProfile(
|
||||
const landmarkDrafts = normalizeLandmarkDraftList(item.landmarks);
|
||||
|
||||
return {
|
||||
id: `custom-world-${Date.now().toString(36)}-${slugify(name)}`,
|
||||
id:
|
||||
toText(item.id) ||
|
||||
`custom-world-${Date.now().toString(36)}-${slugify(name)}`,
|
||||
settingText: settingText.trim(),
|
||||
name,
|
||||
subtitle: toText(item.subtitle) || fallback.subtitle,
|
||||
@@ -930,6 +961,8 @@ export function normalizeCustomWorldProfile(
|
||||
tone,
|
||||
playerGoal,
|
||||
templateWorldType,
|
||||
majorFactions: normalizeTags(item.majorFactions, []),
|
||||
coreConflicts: normalizeTags(item.coreConflicts, [summary]),
|
||||
attributeSchema: coerceWorldAttributeSchema(
|
||||
item.attributeSchema,
|
||||
generatedAttributeSchema,
|
||||
@@ -941,6 +974,35 @@ export function normalizeCustomWorldProfile(
|
||||
landmarks: landmarkDrafts,
|
||||
storyNpcs,
|
||||
}),
|
||||
themePack:
|
||||
item.themePack && typeof item.themePack === 'object'
|
||||
? (item.themePack as ThemePack)
|
||||
: null,
|
||||
storyGraph:
|
||||
item.storyGraph && typeof item.storyGraph === 'object'
|
||||
? (item.storyGraph as WorldStoryGraph)
|
||||
: null,
|
||||
creatorIntent: normalizeCustomWorldCreatorIntent(item.creatorIntent),
|
||||
anchorPack:
|
||||
item.anchorPack && typeof item.anchorPack === 'object'
|
||||
? (item.anchorPack as CustomWorldAnchorPack)
|
||||
: buildCustomWorldAnchorPackFromIntent(
|
||||
normalizeCustomWorldCreatorIntent(item.creatorIntent),
|
||||
),
|
||||
lockState:
|
||||
item.lockState && typeof item.lockState === 'object'
|
||||
? normalizeCustomWorldLockState(item.lockState)
|
||||
: deriveCustomWorldLockStateFromIntent(
|
||||
normalizeCustomWorldCreatorIntent(item.creatorIntent),
|
||||
),
|
||||
generationMode:
|
||||
item.generationMode === 'fast' || item.generationMode === 'full'
|
||||
? item.generationMode
|
||||
: fallback.generationMode,
|
||||
generationStatus:
|
||||
item.generationStatus === 'key_only' || item.generationStatus === 'complete'
|
||||
? item.generationStatus
|
||||
: fallback.generationStatus,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1038,12 +1100,7 @@ export function validateCustomWorldGenerationFramework(
|
||||
framework: CustomWorldGenerationFramework,
|
||||
) {
|
||||
const playableCount = countUniqueNames(framework.playableNpcs);
|
||||
const storyCount = countUniqueNames(framework.storyNpcs);
|
||||
const landmarkCount = countUniqueNames(framework.landmarks);
|
||||
const totalNpcCount = countUniqueNames([
|
||||
...framework.playableNpcs,
|
||||
...framework.storyNpcs,
|
||||
]);
|
||||
|
||||
if (playableCount < MIN_CUSTOM_WORLD_PLAYABLE_NPC_COUNT) {
|
||||
throw new Error(
|
||||
@@ -1051,18 +1108,6 @@ export function validateCustomWorldGenerationFramework(
|
||||
);
|
||||
}
|
||||
|
||||
if (storyCount < MIN_CUSTOM_WORLD_STORY_NPC_COUNT) {
|
||||
throw new Error(
|
||||
`自定义世界框架至少需要 ${MIN_CUSTOM_WORLD_STORY_NPC_COUNT} 名场景角色。`,
|
||||
);
|
||||
}
|
||||
|
||||
if (totalNpcCount < MIN_CUSTOM_WORLD_TOTAL_NPC_COUNT) {
|
||||
throw new Error(
|
||||
`自定义世界框架至少需要 ${MIN_CUSTOM_WORLD_TOTAL_NPC_COUNT} 名唯一角色,当前仅有 ${totalNpcCount} 名。`,
|
||||
);
|
||||
}
|
||||
|
||||
if (landmarkCount < MIN_CUSTOM_WORLD_LANDMARK_COUNT) {
|
||||
throw new Error(
|
||||
`自定义世界框架至少需要 ${MIN_CUSTOM_WORLD_LANDMARK_COUNT} 个场景,当前仅有 ${landmarkCount} 个。`,
|
||||
@@ -1101,6 +1146,246 @@ export function buildCustomWorldFrameworkPrompt(settingText: string) {
|
||||
].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,
|
||||
) {
|
||||
@@ -1614,24 +1899,57 @@ export function buildCustomWorldGenerationPrompt(settingText: string) {
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
export function buildCustomWorldReferenceText(profile: CustomWorldProfile) {
|
||||
export function buildCustomWorldReferenceText(
|
||||
profile: CustomWorldProfile,
|
||||
options: {
|
||||
activeThreadIds?: string[] | null;
|
||||
highlightNpcNames?: string[] | null;
|
||||
} = {},
|
||||
) {
|
||||
const storyNpcById = new Map(profile.storyNpcs.map((npc) => [npc.id, npc]));
|
||||
const landmarkById = new Map(
|
||||
profile.landmarks.map((landmark) => [landmark.id, landmark]),
|
||||
);
|
||||
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]
|
||||
.filter((thread) => activeThreadIds.includes(thread.id))
|
||||
.slice(0, 3);
|
||||
const highlightNpcNames = new Set(
|
||||
(options.highlightNpcNames ?? []).map((name) => name.trim()).filter(Boolean),
|
||||
);
|
||||
const describeNpcReference = (
|
||||
npc: CustomWorldProfile['storyNpcs'][number] | CustomWorldProfile['playableNpcs'][number],
|
||||
) => {
|
||||
const narrativeProfile = normalizeActorNarrativeProfile(
|
||||
npc.narrativeProfile,
|
||||
buildFallbackActorNarrativeProfile(npc, storyGraph, themePack),
|
||||
);
|
||||
|
||||
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,
|
||||
)
|
||||
.join('、') || '暂无'
|
||||
};反应钩子:${narrativeProfile.reactionHooks.join('、') || '暂无'}`;
|
||||
};
|
||||
const playableNpcText = profile.playableNpcs
|
||||
.slice(0, 3)
|
||||
.map(
|
||||
(npc) =>
|
||||
`- ${npc.name} / ${npc.title}:${npc.description};身份:${npc.role};公开背景:${npc.backstoryReveal.publicSummary};背景:${npc.backstory};动机:${npc.motivation};风格:${npc.combatStyle};技能:${npc.skills.map((skill) => `${skill.name}(${skill.style})`).join('、') || '暂无'};初始物品:${npc.initialItems.map((item) => `${item.name}x${item.quantity}`).join('、') || '暂无'};好感章节:${npc.backstoryReveal.chapters.map((chapter) => `${chapter.affinityRequired}:${chapter.teaser}`).join(' / ')};初始好感:${npc.initialAffinity}`,
|
||||
)
|
||||
.map((npc) => describeNpcReference(npc))
|
||||
.join('\n');
|
||||
const storyNpcText = profile.storyNpcs
|
||||
.slice(0, 8)
|
||||
.map(
|
||||
(npc) =>
|
||||
`- ${npc.name} / ${npc.role}:${npc.description};称号:${npc.title};公开背景:${npc.backstoryReveal.publicSummary};背景:${npc.backstory};动机:${npc.motivation};风格:${npc.combatStyle};技能:${npc.skills.map((skill) => `${skill.name}(${skill.style})`).join('、') || '暂无'};初始物品:${npc.initialItems.map((item) => `${item.name}x${item.quantity}`).join('、') || '暂无'};好感章节:${npc.backstoryReveal.chapters.map((chapter) => `${chapter.affinityRequired}:${chapter.teaser}`).join(' / ')};初始好感:${npc.initialAffinity}`,
|
||||
.filter((npc) =>
|
||||
highlightNpcNames.size > 0 ? highlightNpcNames.has(npc.name) : true,
|
||||
)
|
||||
.slice(0, highlightNpcNames.size > 0 ? 3 : 6)
|
||||
.map((npc) => describeNpcReference(npc))
|
||||
.join('\n');
|
||||
const landmarkText = profile.landmarks
|
||||
.slice(0, 10)
|
||||
@@ -1664,6 +1982,8 @@ export function buildCustomWorldReferenceText(profile: CustomWorldProfile) {
|
||||
`世界概述:${profile.summary}`,
|
||||
`世界基调:${profile.tone}`,
|
||||
`玩家核心目标:${profile.playerGoal}`,
|
||||
`题材适配层:${themePack.displayName};制度词汇 ${themePack.institutionLexicon.slice(0, 4).join('、')};禁忌词 ${themePack.tabooLexicon.slice(0, 4).join('、')};载体类型 ${themePack.artifactClasses.slice(0, 4).join('、')}`,
|
||||
`当前激活线程:\n${activeThreads.map((thread) => `- ${thread.title}:${thread.summary}`).join('\n') || '- 暂无'}`,
|
||||
`世界属性轴:${profile.attributeSchema.slots.map((slot) => `${slot.name}:${slot.definition}`).join(';')}`,
|
||||
`可扮演角色档案:\n${playableNpcText || '- 暂无'}`,
|
||||
`世界场景角色档案:\n${storyNpcText || '- 暂无'}`,
|
||||
@@ -1679,12 +1999,7 @@ export function validateGeneratedCustomWorldProfile(
|
||||
profile: CustomWorldProfile,
|
||||
) {
|
||||
const playableCount = countUniqueNames(profile.playableNpcs);
|
||||
const storyCount = countUniqueNames(profile.storyNpcs);
|
||||
const landmarkCount = countUniqueNames(profile.landmarks);
|
||||
const totalNpcCount = countUniqueNames([
|
||||
...profile.playableNpcs,
|
||||
...profile.storyNpcs,
|
||||
]);
|
||||
|
||||
if (playableCount < MIN_CUSTOM_WORLD_PLAYABLE_NPC_COUNT) {
|
||||
throw new Error(
|
||||
@@ -1692,22 +2007,6 @@ export function validateGeneratedCustomWorldProfile(
|
||||
);
|
||||
}
|
||||
|
||||
if (totalNpcCount < MIN_CUSTOM_WORLD_TOTAL_NPC_COUNT) {
|
||||
throw new Error(
|
||||
`自定义世界生成要求模型至少产出 ${MIN_CUSTOM_WORLD_TOTAL_NPC_COUNT} 名唯一角色,当前仅返回 ${totalNpcCount} 名。`,
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
storyCount <
|
||||
Math.max(
|
||||
0,
|
||||
MIN_CUSTOM_WORLD_TOTAL_NPC_COUNT - MIN_CUSTOM_WORLD_PLAYABLE_NPC_COUNT,
|
||||
)
|
||||
) {
|
||||
throw new Error('自定义世界生成返回的非可扮演场景角色数量不足。');
|
||||
}
|
||||
|
||||
if (landmarkCount < MIN_CUSTOM_WORLD_LANDMARK_COUNT) {
|
||||
throw new Error(
|
||||
`自定义世界生成要求至少产出 ${MIN_CUSTOM_WORLD_LANDMARK_COUNT} 个场景,当前仅返回 ${landmarkCount} 个。`,
|
||||
|
||||
Reference in New Issue
Block a user