Implement scene-based chapter quest progression
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
2026-04-08 11:58:47 +08:00
parent 9d2fc9e4b8
commit bd9fdcbe31
170 changed files with 18259 additions and 1049 deletions

View File

@@ -101,6 +101,7 @@ import {
parseJsonResponseText as parseJsonResponseTextFromParser,
parseLineListContent as parseLineListContentFromParser,
} from './llmParsers';
import { hasMixedNarrativeLanguage } from './narrativeLanguage';
import {
buildNpcRecruitDialoguePrompt,
buildStrictNpcChatDialoguePrompt,
@@ -146,6 +147,11 @@ const CUSTOM_WORLD_JSON_REPAIR_SYSTEM_PROMPT = `你是 JSON 修复器。
你的唯一任务是把它修复成能被 JSON.parse 直接解析的单个 JSON 对象。
不要输出 Markdown、代码块、解释、注释或额外文字。
尽量保留原始语义,只修复格式问题;必要时可以补齐缺失的引号、逗号、括号、数组闭合或缺失字段。`;
const STORY_LANGUAGE_REPAIR_SYSTEM_PROMPT = `你是 RPG 中文叙事文本修复器。
你会收到一个已经解析过的剧情 JSON 对象。
你的唯一任务是把 storyText 和 options[].actionText 中的英文句子、中英混杂句式、英文解释改写成自然中文。
必须保持 JSON 结构、encounter、options 数量、options 顺序以及每个 functionId 完全不变。
只输出一个 JSON 对象,不要输出 Markdown、代码块、解释、注释或额外文字。`;
const CUSTOM_WORLD_GENERATION_JSON_ONLY_SYSTEM_PROMPT = `你是严格的自定义世界 JSON 生成器。
只输出一个 JSON 对象,不要输出 Markdown、代码块、解释或额外文字。`;
const CUSTOM_WORLD_FRAMEWORK_PLAYABLE_OUTLINE_BATCH_SIZE = 5;
@@ -1447,6 +1453,15 @@ function buildEncounterDrivenResolution(
};
}
function resolveSafeGeneratedActionText(actionText: string | undefined) {
const trimmed = actionText?.trim();
if (!trimmed || hasMixedNarrativeLanguage(trimmed)) {
return undefined;
}
return trimmed;
}
function resolveOptionsFromFunctionIds(
items: RawOptionItem[],
worldType: WorldType,
@@ -1463,7 +1478,11 @@ function resolveOptionsFromFunctionIds(
return items
.map((item) =>
resolveFunctionOption(item.functionId, functionContext, item.actionText),
resolveFunctionOption(
item.functionId,
functionContext,
resolveSafeGeneratedActionText(item.actionText),
),
)
.filter(Boolean) as StoryOption[];
}
@@ -1525,7 +1544,7 @@ function resolveOptionsFromProvidedOptions(
if (!matchedOption) return;
consumedOptions.add(matchedOption);
const rewrittenText = item.actionText?.trim();
const rewrittenText = resolveSafeGeneratedActionText(item.actionText);
resolved.push({
...cloneStoryOption(matchedOption),
actionText: rewrittenText || matchedOption.actionText,
@@ -1566,7 +1585,7 @@ function resolveOptionsFromOptionCatalog(
const matchedOption = bucket?.shift();
if (!matchedOption) return;
const rewrittenText = item.actionText?.trim();
const rewrittenText = resolveSafeGeneratedActionText(item.actionText);
resolved.push({
...cloneStoryOption(matchedOption),
actionText: rewrittenText || matchedOption.actionText,
@@ -1662,6 +1681,112 @@ function buildOfflineResponse(
};
}
function buildStoryLanguageRepairPrompt(response: AIResponse) {
return [
'请把下面 JSON 中的 storyText 与 options[].actionText 修复为自然中文。',
'只改写叙事和选项文案,不要改变 encounter、options 数量、顺序或 functionId。',
'如果英文只是角色名或地点名,可以保留名字本体;其余英文句子、英文解释和中英混杂表达都必须改成中文。',
JSON.stringify(
{
storyText: response.storyText,
encounter: response.encounter ?? null,
options: response.options.map((option) => ({
functionId: option.functionId,
actionText: option.actionText,
})),
},
null,
2,
),
].join('\n\n');
}
function needsStoryLanguageRepair(response: AIResponse) {
return hasMixedNarrativeLanguage(response.storyText);
}
function buildStoryLanguageFallbackText(
context: StoryGenerationContext,
inBattle: boolean,
) {
if (inBattle) {
return '敌意仍压在眼前,战斗局势还没有真正松开。';
}
if (context.encounterName) {
return `${context.encounterName}的态度与周围气氛都出现了新的变化,你需要立刻判断接下来如何应对。`;
}
return `${context.sceneName || '眼前区域'}里的气氛又有了新的变化,你需要继续判断下一步。`;
}
function finalizeStoryNarrativeLanguage(
response: AIResponse,
context: StoryGenerationContext,
inBattle: boolean,
): AIResponse {
if (!needsStoryLanguageRepair(response)) {
return response;
}
return {
...response,
storyText: buildStoryLanguageFallbackText(context, inBattle),
};
}
async function repairStoryNarrativeLanguage(
response: AIResponse,
worldType: WorldType,
character: Character,
monsters: SceneHostileNpc[],
context: StoryGenerationContext,
requestOptions: StoryRequestOptions,
) {
const responseBattleState = buildEncounterDrivenResolution(
worldType,
monsters,
context,
response.encounter,
).inBattle;
if (!needsStoryLanguageRepair(response)) {
return finalizeStoryNarrativeLanguage(response, context, responseBattleState);
}
try {
const repairedContent = await requestChatMessageContent(
STORY_LANGUAGE_REPAIR_SYSTEM_PROMPT,
buildStoryLanguageRepairPrompt(response),
{
debugLabel: 'story-language-repair',
},
);
const repairedResponse = normalizeResponse(
parseJsonResponseTextFromParser(repairedContent),
worldType,
character,
monsters,
context,
requestOptions,
);
const repairedBattleState = buildEncounterDrivenResolution(
worldType,
monsters,
context,
repairedResponse.encounter,
).inBattle;
return finalizeStoryNarrativeLanguage(
repairedResponse,
context,
repairedBattleState,
);
} catch (error) {
console.warn('Failed to repair mixed-language story response:', error);
return finalizeStoryNarrativeLanguage(response, context, responseBattleState);
}
}
function normalizeResponse(
raw: unknown,
worldType: WorldType,
@@ -1766,7 +1891,7 @@ async function requestCompletion(
debugLabel: 'story-completion',
});
return normalizeResponse(
const response = normalizeResponse(
parseJsonResponseTextFromParser(content),
worldType,
character,
@@ -1774,6 +1899,15 @@ async function requestCompletion(
context,
requestOptions,
);
return repairStoryNarrativeLanguage(
response,
worldType,
character,
monsters,
context,
requestOptions,
);
}
export async function generateCustomWorldSceneImage({
@@ -1895,7 +2029,7 @@ export async function generateCustomWorldProfile(
landmarks: [],
} satisfies CustomWorldGenerationFramework;
reporter.complete('framework', {
phaseDetail: `世界框架已确定,基础模板锚定为${frameworkBase.templateWorldType === WorldType.WUXIA ? '武侠' : '仙侠'}`,
phaseDetail: '世界框架已确定,开始围绕你的设定继续编译题材层与关键对象。',
});
reporter.begin('theme-pack', {
phaseDetail: '正在提炼题材适配层词汇与命名范式。',