Implement scene-based chapter quest progression
Some checks failed
CI / verify (push) Has been cancelled
Some checks failed
CI / verify (push) Has been cancelled
This commit is contained in:
@@ -14,6 +14,7 @@ import {
|
||||
CharacterBackstoryChapter,
|
||||
CharacterBackstoryRevealConfig,
|
||||
CustomWorldAnchorPack,
|
||||
CustomWorldCampScene,
|
||||
CustomWorldItem,
|
||||
CustomWorldLandmark,
|
||||
CustomWorldNpc,
|
||||
@@ -27,6 +28,7 @@ import {
|
||||
WorldType,
|
||||
} from '../types';
|
||||
import { generateWorldAttributeSchema } from './attributeSchemaGenerator';
|
||||
import { buildFallbackCustomWorldCampScene } from './customWorldCamp';
|
||||
import {
|
||||
buildCustomWorldAnchorPackFromIntent,
|
||||
deriveCustomWorldLockStateFromIntent,
|
||||
@@ -104,6 +106,12 @@ export interface CustomWorldGenerationLandmarkOutline {
|
||||
connections: CustomWorldGenerationLandmarkConnectionOutline[];
|
||||
}
|
||||
|
||||
export interface CustomWorldGenerationCampOutline {
|
||||
name: string;
|
||||
description: string;
|
||||
dangerLevel: string;
|
||||
}
|
||||
|
||||
export interface CustomWorldGenerationFramework {
|
||||
settingText: string;
|
||||
name: string;
|
||||
@@ -114,6 +122,7 @@ export interface CustomWorldGenerationFramework {
|
||||
templateWorldType: WorldType;
|
||||
majorFactions: string[];
|
||||
coreConflicts: string[];
|
||||
camp: CustomWorldGenerationCampOutline;
|
||||
playableNpcs: CustomWorldGenerationRoleOutline[];
|
||||
storyNpcs: CustomWorldGenerationRoleOutline[];
|
||||
landmarks: CustomWorldGenerationLandmarkOutline[];
|
||||
@@ -508,32 +517,28 @@ function buildSeedPhrase(settingText: string, fallback: string) {
|
||||
}
|
||||
|
||||
function buildWorldName(settingText: string, worldType: WorldType) {
|
||||
const seed = buildSeedPhrase(
|
||||
settingText,
|
||||
worldType === WorldType.XIANXIA ? '灵潮' : '江湖',
|
||||
);
|
||||
const suffix = worldType === WorldType.XIANXIA ? '界' : '录';
|
||||
const seed = buildSeedPhrase(settingText, '新旅');
|
||||
const suffix = worldType === WorldType.XIANXIA ? '境' : '域';
|
||||
return `${seed}${suffix}`;
|
||||
}
|
||||
|
||||
function buildBaseCustomWorldProfile(settingText: string): CustomWorldProfile {
|
||||
const templateWorldType = inferWorldTypeFromSetting(settingText);
|
||||
const name = buildWorldName(settingText, templateWorldType);
|
||||
const subtitle =
|
||||
templateWorldType === WorldType.XIANXIA ? '灵潮未定' : '风云将起';
|
||||
const subtitle = '前路未明';
|
||||
const summary = settingText.trim()
|
||||
? `这个世界围绕“${settingText.trim().slice(0, 28)}”展开。`
|
||||
: templateWorldType === WorldType.XIANXIA
|
||||
? '灵潮未定,旧秩序正在崩裂。'
|
||||
: '旧案复起,江湖格局正在改变。';
|
||||
const tone =
|
||||
templateWorldType === WorldType.XIANXIA
|
||||
? '空灵、危险、层层递进'
|
||||
: '紧张、克制、暗流涌动';
|
||||
const playerGoal =
|
||||
templateWorldType === WorldType.XIANXIA
|
||||
? '查清异变源头,在诸方势力之前抢到关键线索'
|
||||
: '沿着旧案痕迹追查幕后之人,并守住仍值得相信的人与路';
|
||||
: '一个仍待展开的独立世界正在成形。';
|
||||
const tone = '未知、紧绷、仍在展开';
|
||||
const playerGoal = '查清眼前局势的关键矛盾,并守住仍值得相信的人与事';
|
||||
const camp = buildFallbackCustomWorldCampScene({
|
||||
name,
|
||||
summary,
|
||||
tone,
|
||||
playerGoal,
|
||||
settingText: settingText.trim(),
|
||||
templateWorldType,
|
||||
});
|
||||
|
||||
return {
|
||||
id: `custom-world-${Date.now().toString(36)}-${slugify(name)}`,
|
||||
@@ -559,6 +564,7 @@ function buildBaseCustomWorldProfile(settingText: string): CustomWorldProfile {
|
||||
playableNpcs: [],
|
||||
storyNpcs: [],
|
||||
items: [],
|
||||
camp,
|
||||
landmarks: [],
|
||||
themePack: null,
|
||||
storyGraph: null,
|
||||
@@ -592,6 +598,11 @@ export function normalizeCustomWorldGenerationFramework(
|
||||
templateWorldType: fallback.templateWorldType,
|
||||
majorFactions: [],
|
||||
coreConflicts: [fallback.summary],
|
||||
camp: {
|
||||
name: fallback.camp?.name ?? '归舍',
|
||||
description: fallback.camp?.description ?? '',
|
||||
dangerLevel: fallback.camp?.dangerLevel ?? 'low',
|
||||
},
|
||||
playableNpcs: [],
|
||||
storyNpcs: [],
|
||||
landmarks: [],
|
||||
@@ -623,6 +634,14 @@ export function normalizeCustomWorldGenerationFramework(
|
||||
templateWorldType,
|
||||
majorFactions: normalizeTags(item.majorFactions, []),
|
||||
coreConflicts: normalizeTags(item.coreConflicts, [fallback.summary]),
|
||||
camp: normalizeCampOutline(item.camp, {
|
||||
name,
|
||||
summary: toText(item.summary) || fallback.summary,
|
||||
tone: toText(item.tone) || fallback.tone,
|
||||
playerGoal: toText(item.playerGoal) || fallback.playerGoal,
|
||||
settingText: settingText.trim(),
|
||||
templateWorldType,
|
||||
}),
|
||||
playableNpcs: normalizeRoleOutlineList(item.playableNpcs, {
|
||||
titleFallback: '未定称号',
|
||||
defaultAffinity: DEFAULT_PLAYABLE_INITIAL_AFFINITY,
|
||||
@@ -649,6 +668,11 @@ export function buildCustomWorldRawProfileFromFramework(
|
||||
templateWorldType: framework.templateWorldType,
|
||||
majorFactions: framework.majorFactions,
|
||||
coreConflicts: framework.coreConflicts,
|
||||
camp: {
|
||||
name: framework.camp.name,
|
||||
description: framework.camp.description,
|
||||
dangerLevel: framework.camp.dangerLevel,
|
||||
},
|
||||
playableNpcs: framework.playableNpcs.map((npc) => ({
|
||||
name: npc.name,
|
||||
title: npc.title,
|
||||
@@ -818,6 +842,24 @@ function normalizeRoleOutlineList(
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function normalizeCampOutline(
|
||||
value: unknown,
|
||||
fallbackProfile: Pick<
|
||||
CustomWorldProfile,
|
||||
'name' | 'summary' | 'tone' | 'playerGoal' | 'settingText' | 'templateWorldType'
|
||||
>,
|
||||
): CustomWorldGenerationCampOutline {
|
||||
const fallback = buildFallbackCustomWorldCampScene(fallbackProfile);
|
||||
const item =
|
||||
value && typeof value === 'object' ? (value as Record<string, unknown>) : {};
|
||||
|
||||
return {
|
||||
name: toText(item.name) || fallback.name,
|
||||
description: toText(item.description) || fallback.description,
|
||||
dangerLevel: toText(item.dangerLevel) || fallback.dangerLevel,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeLandmarkOutlineList(value: unknown) {
|
||||
return toRecordArray(value)
|
||||
.map((item) => {
|
||||
@@ -910,6 +952,25 @@ function normalizeLandmarkDraftList(value: unknown) {
|
||||
.filter((entry) => entry.name);
|
||||
}
|
||||
|
||||
function normalizeCampScene(
|
||||
value: unknown,
|
||||
fallbackProfile: Pick<
|
||||
CustomWorldProfile,
|
||||
'name' | 'summary' | 'tone' | 'playerGoal' | 'settingText' | 'templateWorldType'
|
||||
>,
|
||||
): CustomWorldCampScene {
|
||||
const fallback = buildFallbackCustomWorldCampScene(fallbackProfile);
|
||||
const item =
|
||||
value && typeof value === 'object' ? (value as Record<string, unknown>) : {};
|
||||
|
||||
return {
|
||||
name: toText(item.name) || fallback.name,
|
||||
description: toText(item.description) || fallback.description,
|
||||
dangerLevel: toText(item.dangerLevel) || fallback.dangerLevel,
|
||||
imageSrc: toText(item.imageSrc) || undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export function normalizeCustomWorldProfile(
|
||||
raw: unknown,
|
||||
settingText: string,
|
||||
@@ -949,6 +1010,14 @@ export function normalizeCustomWorldProfile(
|
||||
const playableNpcs = normalizePlayableNpcList(item.playableNpcs);
|
||||
const storyNpcs = normalizeStoryNpcList(item.storyNpcs);
|
||||
const landmarkDrafts = normalizeLandmarkDraftList(item.landmarks);
|
||||
const camp = normalizeCampScene(item.camp, {
|
||||
name,
|
||||
summary,
|
||||
tone,
|
||||
playerGoal,
|
||||
settingText: settingText.trim(),
|
||||
templateWorldType,
|
||||
});
|
||||
|
||||
return {
|
||||
id:
|
||||
@@ -970,6 +1039,7 @@ export function normalizeCustomWorldProfile(
|
||||
playableNpcs,
|
||||
storyNpcs,
|
||||
items: normalizeItemList(item.items),
|
||||
camp,
|
||||
landmarks: normalizeCustomWorldLandmarks({
|
||||
landmarks: landmarkDrafts,
|
||||
storyNpcs,
|
||||
@@ -1033,6 +1103,7 @@ function buildFrameworkSummaryText(
|
||||
framework.coreConflicts.length > 0
|
||||
? `核心冲突:${framework.coreConflicts.join('、')}`
|
||||
: '',
|
||||
`开局归处:${framework.camp.name}(${framework.camp.description})`,
|
||||
landmarkText ? `关键场景:${landmarkText}` : '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
@@ -1113,13 +1184,17 @@ export function validateCustomWorldGenerationFramework(
|
||||
`自定义世界框架至少需要 ${MIN_CUSTOM_WORLD_LANDMARK_COUNT} 个场景,当前仅有 ${landmarkCount} 个。`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!framework.camp.name.trim() || !framework.camp.description.trim()) {
|
||||
throw new Error('自定义世界框架必须包含一个有效的开局归处场景。');
|
||||
}
|
||||
}
|
||||
|
||||
export function buildCustomWorldFrameworkPrompt(settingText: string) {
|
||||
return [
|
||||
'请先根据下面的玩家设定创建一份“世界核心骨架”,后续我会分步骤生成角色名单、场景名单和详细档案。',
|
||||
'你必须只输出一个能被 JSON.parse 直接解析的 JSON 对象,不要输出 Markdown、代码块、注释或解释。',
|
||||
'这一步只保留世界顶层信息,不要输出 playableNpcs、storyNpcs、landmarks,也不要展开人物和地图细节。',
|
||||
'这一步只保留世界顶层信息与一个开局归处场景,不要输出 playableNpcs、storyNpcs、landmarks,也不要展开人物和地图细节。',
|
||||
'玩家设定:',
|
||||
settingText.trim(),
|
||||
'',
|
||||
@@ -1132,16 +1207,24 @@ export function buildCustomWorldFrameworkPrompt(settingText: string) {
|
||||
' "playerGoal": "玩家核心目标",',
|
||||
' "templateWorldType": "WUXIA|XIANXIA",',
|
||||
' "majorFactions": ["势力甲", "势力乙"],',
|
||||
' "coreConflicts": ["冲突甲", "冲突乙"]',
|
||||
' "coreConflicts": ["冲突甲", "冲突乙"],',
|
||||
' "camp": {',
|
||||
' "name": "开局归处名称",',
|
||||
' "description": "这是玩家进入世界后的第一处落脚点描述",',
|
||||
' "dangerLevel": "low|medium|high|extreme"',
|
||||
' }',
|
||||
'}',
|
||||
'',
|
||||
'要求:',
|
||||
'- 所有生成文本都必须使用中文。',
|
||||
'- 这一步只输出顶层 8 个字段:name、subtitle、summary、tone、playerGoal、templateWorldType、majorFactions、coreConflicts。',
|
||||
'- 不要输出 playableNpcs、storyNpcs、landmarks、items,也不要输出任何角色和场景细节。',
|
||||
'- 这一步只输出顶层 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 个汉字内。',
|
||||
'- 每个字符串尽量简洁:subtitle 控制在 8 到 18 个汉字内,summary 控制在 16 到 32 个汉字内,tone 控制在 6 到 16 个汉字内,playerGoal 控制在 16 到 32 个汉字内,camp.description 控制在 18 到 40 个汉字内。',
|
||||
'- 返回前自检:必须是一个能被 JSON.parse 直接解析的单个 JSON 对象。',
|
||||
].join('\n');
|
||||
}
|
||||
@@ -1392,9 +1475,10 @@ export function buildCustomWorldFrameworkJsonRepairPrompt(
|
||||
return [
|
||||
'下面这段文本本应是自定义世界核心骨架的单个 JSON 对象,但当前不能被 JSON.parse 直接解析。',
|
||||
'请只输出修复后的 JSON 对象。',
|
||||
'顶层必须只包含:name、subtitle、summary、tone、playerGoal、templateWorldType、majorFactions、coreConflicts。',
|
||||
'顶层必须只包含:name、subtitle、summary、tone、playerGoal、templateWorldType、majorFactions、coreConflicts、camp。',
|
||||
'不要输出 playableNpcs、storyNpcs、landmarks、items 或任何其他字段。',
|
||||
'majorFactions 与 coreConflicts 必须是字符串数组。',
|
||||
'camp 必须是对象,且包含:name、description、dangerLevel。',
|
||||
'原始文本:',
|
||||
responseText.trim(),
|
||||
].join('\n');
|
||||
@@ -1437,6 +1521,7 @@ export function buildCustomWorldRoleOutlineBatchPrompt(params: {
|
||||
'',
|
||||
'要求:',
|
||||
`- 必须生成恰好 ${batchCount} 个${label}。`,
|
||||
'- 这是一个完全独立的自定义世界;不要把角色写成来自“武侠世界”“仙侠世界”等现成世界。',
|
||||
'- 名称必须具体且互不重复,不要使用 角色1、NPC1、场景角色1 之类的占位名。',
|
||||
'- 只保留:name、title、role、description、initialAffinity、relationshipHooks、tags。',
|
||||
'- relationshipHooks 最多 1 条;tags 保持 1 到 2 个。',
|
||||
@@ -1509,6 +1594,7 @@ export function buildCustomWorldLandmarkSeedBatchPrompt(params: {
|
||||
'',
|
||||
'要求:',
|
||||
`- 必须生成恰好 ${batchCount} 个 landmarks。`,
|
||||
'- 这是一个完全独立的自定义世界;不要在场景描述里直接写“武侠世界”“仙侠世界”等现成世界名。',
|
||||
'- 这一步只保留:name、description、dangerLevel。',
|
||||
'- 不要输出 sceneNpcNames、connections、imageSrc 或其他字段。',
|
||||
'- 名称必须具体且互不重复,不要使用 场景1、地点1 之类的占位名。',
|
||||
@@ -1590,6 +1676,7 @@ export function buildCustomWorldLandmarkNetworkBatchPrompt(params: {
|
||||
'',
|
||||
'要求:',
|
||||
`- 只输出这批 ${landmarkBatch.length} 个场景,不要输出其他场景。`,
|
||||
'- 这是一个完全独立的自定义世界;summary 不要带入“武侠”“仙侠”等现成世界名称。',
|
||||
'- 名称必须与本批次场景骨架完全一致,不得改名。',
|
||||
'- 每个场景必须提供恰好 3 个唯一 sceneNpcNames,且只能从可用场景角色名里选择。',
|
||||
`- 每个场景必须提供恰好 2 条 connections;relativePosition 只能使用:${relativePositionValues}。`,
|
||||
@@ -1662,6 +1749,7 @@ export function buildCustomWorldRoleBatchPrompt(params: {
|
||||
'',
|
||||
'要求:',
|
||||
`- 只输出这批${label},不要输出其他角色、场景或额外顶层字段。`,
|
||||
'- 这是一个完全独立的自定义世界;不要在角色背景、性格、动机或战斗风格里直接写“武侠世界”“仙侠世界”等现成世界名。',
|
||||
`- ${key} 的数量必须与本批次名单完全一致。`,
|
||||
'- 名称必须与批次名单完全一致,不得增删改名。',
|
||||
'- 只补全 backstory、personality、motivation、combatStyle 这 4 个字段,不要输出 backstoryReveal、skills、initialItems。',
|
||||
@@ -1717,6 +1805,7 @@ export function buildCustomWorldRoleBatchPrompt(params: {
|
||||
'',
|
||||
'要求:',
|
||||
`- 只输出这批${label},不要输出其他角色、场景或额外顶层字段。`,
|
||||
'- 这是一个完全独立的自定义世界;不要在公开背景、技能名、物品名或说明里直接带入“武侠”“仙侠”等现成世界名。',
|
||||
`- ${key} 的数量必须与本批次名单完全一致。`,
|
||||
'- 名称必须与批次名单完全一致,不得增删改名。',
|
||||
'- 这一阶段只补全 backstoryReveal、skills、initialItems,不要重复输出 title、role、description、backstory、personality、motivation、combatStyle、initialAffinity、relationshipHooks、tags。',
|
||||
@@ -1791,6 +1880,11 @@ export function buildCustomWorldGenerationPrompt(settingText: string) {
|
||||
' "playerGoal": "玩家核心目标",',
|
||||
' "majorFactions": ["势力甲", "势力乙"],',
|
||||
' "coreConflicts": ["冲突甲", "冲突乙"],',
|
||||
' "camp": {',
|
||||
' "name": "开局归处名称",',
|
||||
' "description": "玩家进入世界后的第一处落脚点描述",',
|
||||
' "dangerLevel": "low|medium|high|extreme"',
|
||||
' },',
|
||||
' "playableNpcs": [',
|
||||
' {',
|
||||
' "name": "角色名称",',
|
||||
@@ -1878,6 +1972,7 @@ export function buildCustomWorldGenerationPrompt(settingText: string) {
|
||||
'',
|
||||
'要求:',
|
||||
'- 所有生成文本都必须使用中文。',
|
||||
'- camp 必须存在,代表玩家开局时的落脚处;名字不要直接写成“某某营地”,更接近归舍、住处、栖居、前哨居所这类“家/归处”的概念。',
|
||||
'- 必须生成恰好 5 个 playableNpcs。',
|
||||
'- 至少生成 25 个 storyNpcs,并保证 playableNpcs + storyNpcs 的唯一名称总数不少于 30。',
|
||||
'- 至少生成 10 个真正可游玩的 landmarks。',
|
||||
@@ -1982,6 +2077,7 @@ export function buildCustomWorldReferenceText(
|
||||
`世界概述:${profile.summary}`,
|
||||
`世界基调:${profile.tone}`,
|
||||
`玩家核心目标:${profile.playerGoal}`,
|
||||
`开局归处:${profile.camp?.name ?? '未设定'}${profile.camp?.description ? `;${profile.camp.description}` : ''}`,
|
||||
`题材适配层:${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(';')}`,
|
||||
|
||||
Reference in New Issue
Block a user