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:
@@ -42,17 +42,15 @@ import {
|
||||
} from '../types';
|
||||
import type { StoryGenerationContext } from './aiTypes';
|
||||
import { buildCustomWorldReferenceText } from './customWorld';
|
||||
import { sanitizePromptNarrativeText } from './narrativeLanguage';
|
||||
import { describeGoalStackForPrompt } from './storyEngine/goalDirector';
|
||||
import { buildStoryPromptHistory } from './storyHistory';
|
||||
|
||||
export const SYSTEM_PROMPT = `你是角色扮演 RPG 的剧情推进者,只能返回 JSON 对象,不能输出解释、markdown 或代码块。
|
||||
输出格式必须严格符合:
|
||||
{
|
||||
"storyText": "剧情文本",
|
||||
"encounter": {
|
||||
"kind": "npc|treasure|none",
|
||||
"npcId": "仅当 kind=npc 时填写",
|
||||
"treasureText": "仅当 kind=treasure 时填写"
|
||||
},
|
||||
"encounter": null,
|
||||
"options": [
|
||||
{
|
||||
"functionId": "预定义功能ID",
|
||||
@@ -61,10 +59,18 @@ export const SYSTEM_PROMPT = `你是角色扮演 RPG 的剧情推进者,只能
|
||||
]
|
||||
}
|
||||
|
||||
只有当提示语明确要求你判断“主角继续推进后下一刻会遇到什么”时,才允许把 "encounter" 改成:
|
||||
{
|
||||
"kind": "npc|treasure|none",
|
||||
"npcId": "仅当 kind=npc 时填写",
|
||||
"treasureText": "仅当 kind=treasure 时填写"
|
||||
}
|
||||
|
||||
严格规则:
|
||||
- 所有文本必须是中文。
|
||||
- 如果提示语给出了特定可选列表,你必须严格保留原有数量和 functionId;你可以调整这些特定项的顺序,但排序必须参考最近剧情、刚发生的结果、当前局面轻重缓急,再重点优化 actionText;下文会直接说明每个 function 的行为边界,不是让你发挥的剧本。
|
||||
- 如果提示语中没有给特定可选列表,则必须输出至少 6 个选项。
|
||||
- 除非提示语明确要求你判断下一刻遭遇,否则 encounter 必须保持为 null;战斗结束后的续写、聊天续写、固定选项续写都不能生成新的 encounter。
|
||||
- 每个选项只能包含 functionId 和 actionText。
|
||||
- 没有特定列表时,所有 functionId 必须互不重复。
|
||||
- 每个选项只能包含一个 function,不要把多个动作塞进同一行。
|
||||
@@ -139,6 +145,329 @@ export const CHARACTER_PANEL_CHAT_SUMMARY_SYSTEM_PROMPT = `你要把玩家与这
|
||||
- 聊天里出现的重要信息、承诺、顾虑或暗示
|
||||
长度控制在 45 到 120 个字。`;
|
||||
|
||||
function describeConversationSituationLabel(
|
||||
situation: StoryGenerationContext['conversationSituation'],
|
||||
) {
|
||||
switch (situation) {
|
||||
case 'camp_first_contact':
|
||||
return '营地初次试探';
|
||||
case 'camp_followup':
|
||||
return '营地顺势续谈';
|
||||
case 'post_battle_breath':
|
||||
return '战后缓气';
|
||||
case 'shared_danger_coordination':
|
||||
return '危险中协同';
|
||||
case 'private_followup':
|
||||
return '私下续谈';
|
||||
case 'first_contact_cautious':
|
||||
return '谨慎初见';
|
||||
default:
|
||||
return '当前对话';
|
||||
}
|
||||
}
|
||||
|
||||
function describeConversationPressureLabel(
|
||||
pressure: StoryGenerationContext['conversationPressure'],
|
||||
) {
|
||||
switch (pressure) {
|
||||
case 'high':
|
||||
return '高压';
|
||||
case 'medium':
|
||||
return '中压';
|
||||
case 'low':
|
||||
return '低压';
|
||||
default:
|
||||
return '未知';
|
||||
}
|
||||
}
|
||||
|
||||
function describeRevealBudgetLabel(revealBudget: string | null | undefined) {
|
||||
switch (revealBudget) {
|
||||
case 'low':
|
||||
return '低';
|
||||
case 'medium':
|
||||
return '中';
|
||||
case 'high':
|
||||
return '高';
|
||||
default:
|
||||
return '未设定';
|
||||
}
|
||||
}
|
||||
|
||||
function describeEmotionalCadenceLabel(cadence: string | null | undefined) {
|
||||
switch (cadence) {
|
||||
case 'tense':
|
||||
return '紧绷';
|
||||
case 'curious':
|
||||
return '试探';
|
||||
case 'hostile':
|
||||
return '敌意';
|
||||
case 'intimate':
|
||||
return '亲近';
|
||||
case 'tragic':
|
||||
return '沉重';
|
||||
case 'mysterious':
|
||||
return '迷雾';
|
||||
default:
|
||||
return '未设定';
|
||||
}
|
||||
}
|
||||
|
||||
function describeCompanionReactionTypeLabel(reactionType: string) {
|
||||
switch (reactionType) {
|
||||
case 'approve':
|
||||
return '认可';
|
||||
case 'disapprove':
|
||||
return '保留';
|
||||
case 'concern':
|
||||
return '担心';
|
||||
case 'silence':
|
||||
return '沉默';
|
||||
case 'curious':
|
||||
return '被勾起兴趣';
|
||||
default:
|
||||
return '反应';
|
||||
}
|
||||
}
|
||||
|
||||
function describeActStatusLabel(status: string | null | undefined) {
|
||||
switch (status) {
|
||||
case 'opening':
|
||||
return '开场';
|
||||
case 'midgame':
|
||||
return '中段';
|
||||
case 'late_game':
|
||||
return '后段';
|
||||
case 'finale':
|
||||
return '收束';
|
||||
case 'resolved':
|
||||
return '已落定';
|
||||
default:
|
||||
return '进行中';
|
||||
}
|
||||
}
|
||||
|
||||
function describeBranchBudgetPressureLabel(
|
||||
pressure: StoryGenerationContext['branchBudgetPressure'],
|
||||
) {
|
||||
switch (pressure) {
|
||||
case 'low':
|
||||
return '低';
|
||||
case 'medium':
|
||||
return '中';
|
||||
case 'high':
|
||||
return '高';
|
||||
default:
|
||||
return '未知';
|
||||
}
|
||||
}
|
||||
|
||||
function describePlayerStyleLabel(style: string | null | undefined) {
|
||||
switch (style) {
|
||||
case 'story_first':
|
||||
return '剧情优先';
|
||||
case 'explorer':
|
||||
return '探索驱动';
|
||||
case 'combat_driver':
|
||||
return '战斗推进';
|
||||
case 'companion_bond':
|
||||
return '同伴关系';
|
||||
case 'collector':
|
||||
return '收集倾向';
|
||||
default:
|
||||
return '综合型';
|
||||
}
|
||||
}
|
||||
|
||||
function describeQaSeverityLabel(severity: string) {
|
||||
switch (severity) {
|
||||
case 'low':
|
||||
return '低';
|
||||
case 'medium':
|
||||
return '中';
|
||||
case 'high':
|
||||
return '高';
|
||||
default:
|
||||
return '未知';
|
||||
}
|
||||
}
|
||||
|
||||
function describeQaCategoryLabel(category: string) {
|
||||
switch (category) {
|
||||
case 'consistency':
|
||||
return '一致性';
|
||||
case 'pacing':
|
||||
return '节奏';
|
||||
case 'payoff':
|
||||
return '回收';
|
||||
case 'branch_budget':
|
||||
return '分支预算';
|
||||
case 'reveal_leak':
|
||||
return '信息泄露';
|
||||
default:
|
||||
return '叙事问题';
|
||||
}
|
||||
}
|
||||
|
||||
function describeReleaseGateStatusLabel(status: string | null | undefined) {
|
||||
switch (status) {
|
||||
case 'pass':
|
||||
return '通过';
|
||||
case 'warn':
|
||||
return '警告';
|
||||
case 'block':
|
||||
return '阻塞';
|
||||
default:
|
||||
return '未知';
|
||||
}
|
||||
}
|
||||
|
||||
function describeChapterStageLabel(stage: string | null | undefined) {
|
||||
switch (stage) {
|
||||
case 'opening':
|
||||
return '开篇';
|
||||
case 'expansion':
|
||||
return '展开';
|
||||
case 'turning_point':
|
||||
return '转折';
|
||||
case 'climax':
|
||||
return '高潮';
|
||||
case 'aftermath':
|
||||
return '余波';
|
||||
default:
|
||||
return '进行中';
|
||||
}
|
||||
}
|
||||
|
||||
function describeJourneyBeatLabel(beatType: string | null | undefined) {
|
||||
switch (beatType) {
|
||||
case 'approach':
|
||||
return '接近';
|
||||
case 'investigation':
|
||||
return '调查';
|
||||
case 'camp':
|
||||
return '休整';
|
||||
case 'conflict':
|
||||
return '冲突';
|
||||
case 'boss_prelude':
|
||||
return '决战前奏';
|
||||
case 'climax':
|
||||
return '高潮';
|
||||
case 'recovery':
|
||||
return '恢复';
|
||||
default:
|
||||
return '旅程';
|
||||
}
|
||||
}
|
||||
|
||||
function describeCampEventTypeLabel(eventType: string | null | undefined) {
|
||||
switch (eventType) {
|
||||
case 'private_talk':
|
||||
return '私下谈话';
|
||||
case 'party_banter':
|
||||
return '队伍闲谈';
|
||||
case 'conflict':
|
||||
return '冲突';
|
||||
case 'comfort':
|
||||
return '安抚';
|
||||
case 'reveal':
|
||||
return '揭露';
|
||||
case 'decision':
|
||||
return '抉择';
|
||||
default:
|
||||
return '营地事件';
|
||||
}
|
||||
}
|
||||
|
||||
function describeSetpieceTypeLabel(setpieceType: string | null | undefined) {
|
||||
switch (setpieceType) {
|
||||
case 'boss_prelude':
|
||||
return '决战前奏';
|
||||
case 'showdown':
|
||||
return '对峙';
|
||||
case 'climax':
|
||||
return '高潮';
|
||||
case 'aftermath':
|
||||
return '余波';
|
||||
default:
|
||||
return '高光节点';
|
||||
}
|
||||
}
|
||||
|
||||
function describeWorldMutationTypeLabel(mutationType: string) {
|
||||
switch (mutationType) {
|
||||
case 'scene_text':
|
||||
return '场景变化';
|
||||
case 'npc_attitude':
|
||||
return '人物态度变化';
|
||||
case 'shop_style':
|
||||
return '商铺风格变化';
|
||||
case 'enemy_pressure':
|
||||
return '敌方压力变化';
|
||||
case 'route_lock':
|
||||
return '路径封锁';
|
||||
case 'route_unlock':
|
||||
return '路径开启';
|
||||
default:
|
||||
return '世界变化';
|
||||
}
|
||||
}
|
||||
|
||||
function describeAnimationLabel(animation: string | null | undefined) {
|
||||
switch (animation) {
|
||||
case 'idle':
|
||||
return '待机';
|
||||
case 'acquire':
|
||||
return '收取';
|
||||
case 'attack':
|
||||
return '攻击';
|
||||
case 'run':
|
||||
return '奔跑';
|
||||
case 'jump':
|
||||
return '跳跃';
|
||||
case 'double jump':
|
||||
return '二段跳';
|
||||
case 'jump attack':
|
||||
return '跳击';
|
||||
case 'dash':
|
||||
return '冲刺';
|
||||
case 'hurt':
|
||||
return '受击';
|
||||
case 'die':
|
||||
return '倒下';
|
||||
case 'climb':
|
||||
return '攀爬';
|
||||
case 'skill1':
|
||||
return '技能一';
|
||||
case 'skill1 jump':
|
||||
return '技能一起跳';
|
||||
case 'skill1 bullet':
|
||||
return '技能一弹道';
|
||||
case 'skill1 bullet FX':
|
||||
return '技能一特效';
|
||||
case 'skill2':
|
||||
return '技能二';
|
||||
case 'skill2 jump':
|
||||
return '技能二起跳';
|
||||
case 'skill3':
|
||||
return '技能三';
|
||||
case 'skill3 jump':
|
||||
return '技能三起跳';
|
||||
case 'skill3 bullet':
|
||||
return '技能三弹道';
|
||||
case 'skill3 bullet FX':
|
||||
return '技能三特效';
|
||||
case 'skill4':
|
||||
return '技能四';
|
||||
case 'Wall Slide':
|
||||
return '贴墙滑行';
|
||||
case 'move':
|
||||
return '逼近';
|
||||
default:
|
||||
return animation ?? '当前动作';
|
||||
}
|
||||
}
|
||||
|
||||
export function describeWorld(world: WorldType) {
|
||||
if (world === WorldType.WUXIA) return '武侠';
|
||||
if (world === WorldType.XIANXIA) return '仙侠';
|
||||
@@ -259,12 +588,25 @@ function describeConversationSituationDirective(context: StoryGenerationContext)
|
||||
return null;
|
||||
}
|
||||
|
||||
const recentSharedEvent = sanitizePromptNarrativeText(
|
||||
context.recentSharedEvent,
|
||||
'你们刚共同经历了一段需要承接的局势变化。',
|
||||
);
|
||||
const talkPriority = sanitizePromptNarrativeText(
|
||||
context.talkPriority,
|
||||
'优先承接眼前局势与刚刚发生的变化。',
|
||||
);
|
||||
|
||||
return [
|
||||
'当前对话情景控制:',
|
||||
context.conversationSituation ? `- 情景标签:${context.conversationSituation}` : null,
|
||||
context.conversationPressure ? `- 当前压力:${context.conversationPressure}` : null,
|
||||
context.recentSharedEvent ? `- 刚刚共同经历:${context.recentSharedEvent}` : null,
|
||||
context.talkPriority ? `- 本轮优先说法:${context.talkPriority}` : null,
|
||||
context.conversationSituation
|
||||
? `- 情景标签:${describeConversationSituationLabel(context.conversationSituation)}`
|
||||
: null,
|
||||
context.conversationPressure
|
||||
? `- 当前压力:${describeConversationPressureLabel(context.conversationPressure)}`
|
||||
: null,
|
||||
recentSharedEvent ? `- 刚刚共同经历:${recentSharedEvent}` : null,
|
||||
talkPriority ? `- 本轮优先说法:${talkPriority}` : null,
|
||||
].filter(Boolean).join('\n');
|
||||
}
|
||||
|
||||
@@ -356,12 +698,18 @@ function describeSceneNarrativeDirectiveSection(context: StoryGenerationContext)
|
||||
}
|
||||
|
||||
const directive = context.sceneNarrativeDirective;
|
||||
const primaryPressure = sanitizePromptNarrativeText(
|
||||
directive.primaryPressure,
|
||||
'当前场景仍有未被说透的压力。',
|
||||
);
|
||||
return [
|
||||
'当前场景导演指令:',
|
||||
`- 主压力:${directive.primaryPressure}`,
|
||||
`- 激活线程:${directive.activeThreadIds.join('、') || '暂无'}`,
|
||||
`- 揭示预算:${directive.revealBudget}`,
|
||||
`- 情绪节奏:${directive.emotionalCadence}`,
|
||||
primaryPressure ? `- 主压力:${primaryPressure}` : null,
|
||||
directive.activeThreadIds.length > 0
|
||||
? `- 当前激活故事线程数量:${directive.activeThreadIds.length}`
|
||||
: null,
|
||||
`- 揭示预算:${describeRevealBudgetLabel(directive.revealBudget)}`,
|
||||
`- 情绪节奏:${describeEmotionalCadenceLabel(directive.emotionalCadence)}`,
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
@@ -373,8 +721,15 @@ function describeRecentCompanionReactionsSection(context: StoryGenerationContext
|
||||
return [
|
||||
'最近一次同行反应:',
|
||||
...context.recentCompanionReactions.slice(-3).map(
|
||||
(reaction) =>
|
||||
`- ${reaction.characterId} / ${reaction.reactionType}:${reaction.reason}`,
|
||||
(reaction) => {
|
||||
const safeReason = sanitizePromptNarrativeText(
|
||||
reaction.reason,
|
||||
'同行角色对你刚才那一步有了新的态度变化。',
|
||||
);
|
||||
const speaker =
|
||||
getCharacterById(reaction.characterId)?.name ?? '同行角色';
|
||||
return `- ${speaker} / ${describeCompanionReactionTypeLabel(reaction.reactionType)}:${safeReason}`;
|
||||
},
|
||||
),
|
||||
].join('\n');
|
||||
}
|
||||
@@ -398,10 +753,10 @@ function describeCampaignSection(context: StoryGenerationContext) {
|
||||
return [
|
||||
'当前战役状态:',
|
||||
context.campaignState
|
||||
? `- Campaign:${context.campaignState.title}(Act ${context.campaignState.currentActIndex + 1})`
|
||||
? `- 当前战役:${context.campaignState.title}(第 ${context.campaignState.currentActIndex + 1} 幕)`
|
||||
: null,
|
||||
context.actState
|
||||
? `- 当前 Act:${context.actState.title} / ${context.actState.status} / ${context.actState.theme}`
|
||||
? `- 当前幕:${context.actState.title} / ${describeActStatusLabel(context.actState.status)} / ${context.actState.theme}`
|
||||
: null,
|
||||
].filter(Boolean).join('\n');
|
||||
}
|
||||
@@ -431,7 +786,7 @@ function describeConstraintSection(context: StoryGenerationContext) {
|
||||
`- 禁止模式:${pack.noGoPatterns.join('、') || '暂无'}`,
|
||||
`- 必须回收:${pack.requiredPayoffs.join('、') || '暂无'}`,
|
||||
context.branchBudgetPressure
|
||||
? `- 当前分支预算压力:${context.branchBudgetPressure}`
|
||||
? `- 当前分支预算压力:${describeBranchBudgetPressureLabel(context.branchBudgetPressure)}`
|
||||
: null,
|
||||
].filter(Boolean).join('\n');
|
||||
}
|
||||
@@ -444,10 +799,10 @@ function describePackSection(context: StoryGenerationContext) {
|
||||
return [
|
||||
'当前内容包:',
|
||||
context.activeScenarioPack
|
||||
? `- Scenario Pack:${context.activeScenarioPack.title} v${context.activeScenarioPack.version}`
|
||||
? `- 当前场景包:${context.activeScenarioPack.title} v${context.activeScenarioPack.version}`
|
||||
: null,
|
||||
context.activeCampaignPack
|
||||
? `- Campaign Pack:${context.activeCampaignPack.title} / ${context.activeCampaignPack.authoringStyle}`
|
||||
? `- 当前战役包:${context.activeCampaignPack.title} / ${context.activeCampaignPack.authoringStyle}`
|
||||
: null,
|
||||
].filter(Boolean).join('\n');
|
||||
}
|
||||
@@ -459,7 +814,7 @@ function describePlayerStyleSection(context: StoryGenerationContext) {
|
||||
|
||||
return [
|
||||
'当前玩家画像:',
|
||||
`- 风格:${context.playerStyleProfile.dominantStyle}`,
|
||||
`- 风格:${describePlayerStyleLabel(context.playerStyleProfile.dominantStyle)}`,
|
||||
`- 倾向:剧情 ${context.playerStyleProfile.preferenceWeights.story} / 探索 ${context.playerStyleProfile.preferenceWeights.exploration} / 战斗 ${context.playerStyleProfile.preferenceWeights.combat} / 同伴 ${context.playerStyleProfile.preferenceWeights.companion} / 收集 ${context.playerStyleProfile.preferenceWeights.collection}`,
|
||||
].join('\n');
|
||||
}
|
||||
@@ -473,13 +828,14 @@ function describeNarrativeQaSection(context: StoryGenerationContext) {
|
||||
'当前叙事 QA:',
|
||||
`- 摘要:${context.narrativeQaReport.summary}`,
|
||||
...context.narrativeQaReport.issues.slice(0, 4).map(
|
||||
(issue) => `- ${issue.severity}/${issue.category}:${issue.summary}`,
|
||||
(issue) =>
|
||||
`- ${describeQaSeverityLabel(issue.severity)} / ${describeQaCategoryLabel(issue.category)}:${issue.summary}`,
|
||||
),
|
||||
context.releaseGateReport
|
||||
? `- Release Gate:${context.releaseGateReport.status} / ${context.releaseGateReport.summary}`
|
||||
? `- 发布门禁:${describeReleaseGateStatusLabel(context.releaseGateReport.status)} / ${context.releaseGateReport.summary}`
|
||||
: null,
|
||||
context.simulationRunResults?.length
|
||||
? `- Simulation 覆盖:${context.simulationRunResults.length} 条`
|
||||
? `- 模拟覆盖:${context.simulationRunResults.length} 条`
|
||||
: null,
|
||||
].join('\n');
|
||||
}
|
||||
@@ -492,7 +848,7 @@ function describeChapterSection(context: StoryGenerationContext) {
|
||||
return [
|
||||
'当前章节状态:',
|
||||
`- 标题:${context.chapterState.title}`,
|
||||
`- 阶段:${context.chapterState.stage}`,
|
||||
`- 阶段:${describeChapterStageLabel(context.chapterState.stage)}`,
|
||||
`- 主题:${context.chapterState.theme}`,
|
||||
`- 摘要:${context.chapterState.chapterSummary}`,
|
||||
].join('\n');
|
||||
@@ -505,12 +861,16 @@ function describeJourneyBeatSection(context: StoryGenerationContext) {
|
||||
|
||||
return [
|
||||
'当前旅程段落:',
|
||||
`- 类型:${context.journeyBeat.beatType}`,
|
||||
`- 类型:${describeJourneyBeatLabel(context.journeyBeat.beatType)}`,
|
||||
`- 标题:${context.journeyBeat.title}`,
|
||||
`- 情绪目标:${context.journeyBeat.emotionalGoal}`,
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
function describeGoalStackSection(context: StoryGenerationContext) {
|
||||
return describeGoalStackForPrompt(context.goalStack);
|
||||
}
|
||||
|
||||
function describeCampEventSection(context: StoryGenerationContext) {
|
||||
if (!context.currentCampEvent) {
|
||||
return null;
|
||||
@@ -519,7 +879,7 @@ function describeCampEventSection(context: StoryGenerationContext) {
|
||||
return [
|
||||
'当前可触发营地/旅途事件:',
|
||||
`- 标题:${context.currentCampEvent.title}`,
|
||||
`- 类型:${context.currentCampEvent.eventType}`,
|
||||
`- 类型:${describeCampEventTypeLabel(context.currentCampEvent.eventType)}`,
|
||||
`- 原因:${context.currentCampEvent.triggerReason}`,
|
||||
].join('\n');
|
||||
}
|
||||
@@ -531,7 +891,7 @@ function describeSetpieceSection(context: StoryGenerationContext) {
|
||||
|
||||
return [
|
||||
'当前高光导演指令:',
|
||||
`- 类型:${context.setpieceDirective.setpieceType}`,
|
||||
`- 类型:${describeSetpieceTypeLabel(context.setpieceDirective.setpieceType)}`,
|
||||
`- 标题:${context.setpieceDirective.title}`,
|
||||
`- 核心问题:${context.setpieceDirective.dramaticQuestion}`,
|
||||
].join('\n');
|
||||
@@ -546,7 +906,7 @@ function describeWorldMutationSection(context: StoryGenerationContext) {
|
||||
'最近世界变化:',
|
||||
...context.recentWorldMutations.slice(-4).map(
|
||||
(mutation) =>
|
||||
`- ${mutation.mutationType} / ${mutation.targetId}:${mutation.reason}`,
|
||||
`- ${describeWorldMutationTypeLabel(mutation.mutationType)}:${mutation.reason}`,
|
||||
),
|
||||
].join('\n');
|
||||
}
|
||||
@@ -560,17 +920,20 @@ function describeFactionTensionSection(context: StoryGenerationContext) {
|
||||
'当前阵营温度:',
|
||||
...context.recentFactionTensionStates.slice(0, 4).map(
|
||||
(tension) =>
|
||||
`- ${tension.factionId} / 温度 ${tension.temperature}:${tension.pressureSummary}`,
|
||||
`- 温度 ${tension.temperature}:${tension.pressureSummary}`,
|
||||
),
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
function describeChronicleSection(context: StoryGenerationContext) {
|
||||
if (!context.recentChronicleSummary?.trim()) {
|
||||
const chronicleSummary = sanitizePromptNarrativeText(
|
||||
context.recentChronicleSummary,
|
||||
);
|
||||
if (!chronicleSummary) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return `近期旅程回顾:\n${context.recentChronicleSummary}`;
|
||||
return `近期旅程回顾:\n${chronicleSummary}`;
|
||||
}
|
||||
|
||||
function buildCustomEncounterBackstoryLines(context: StoryGenerationContext) {
|
||||
@@ -611,7 +974,12 @@ function buildCustomEncounterBackstoryLines(context: StoryGenerationContext) {
|
||||
|
||||
function describeBackstoryContext(label: string, snippets: string[]) {
|
||||
const normalized = snippets
|
||||
.map(snippet => snippet.trim())
|
||||
.map((snippet) =>
|
||||
sanitizePromptNarrativeText(
|
||||
snippet,
|
||||
`${label === '主角背景' ? '主角' : '对方'}仍有自己的来路,但此刻不直接沿用非中文原句。`,
|
||||
),
|
||||
)
|
||||
.filter(Boolean);
|
||||
|
||||
if (normalized.length === 0) {
|
||||
@@ -847,8 +1215,8 @@ function describeFrontEntity(
|
||||
context.encounterKind === 'npc' && context.encounterAffinityText
|
||||
? `- 对你的态度:${context.encounterAffinityText}`
|
||||
: null,
|
||||
context.encounterRelationshipSummary
|
||||
? `- 你与对方私下相处补充:${context.encounterRelationshipSummary}`
|
||||
sanitizePromptNarrativeText(context.encounterRelationshipSummary)
|
||||
? `- 你与对方私下相处补充:${sanitizePromptNarrativeText(context.encounterRelationshipSummary)}`
|
||||
: null,
|
||||
].filter(Boolean).join('\n');
|
||||
}
|
||||
@@ -872,7 +1240,7 @@ function describeFrontEntity(
|
||||
'- 身份:当前最靠前的敌对目标',
|
||||
`- 描述:${monsterPreset?.description ?? primaryMonster.description}`,
|
||||
'- 性格:更接近本能性的压迫与试探,会按当前动作持续逼近你',
|
||||
`- 状态:生命状态 ${describeHpBand(hpRatio)},当前动作 ${primaryMonster.animation},朝向 ${describeFacing(primaryMonster.facing)}`,
|
||||
`- 状态:生命状态 ${describeHpBand(hpRatio)},当前动作 ${describeAnimationLabel(primaryMonster.animation)},朝向 ${describeFacing(primaryMonster.facing)}`,
|
||||
...describeAttributeProfileForPrompt('敌对实体', world, context, monsterProfile).map(line => `- ${line}`),
|
||||
].join('\n');
|
||||
}
|
||||
@@ -893,14 +1261,19 @@ function describePlayerState(world: WorldType, character: Character, context: St
|
||||
`玩家状态:${context.inBattle ? '战斗状态' : '空闲状态'}`,
|
||||
`当前场景:${sceneName}`,
|
||||
`场景描述:${sceneDescription}`,
|
||||
context.lastObserveSignsReport ? `最近一次观察结果:${context.lastObserveSignsReport}` : null,
|
||||
sanitizePromptNarrativeText(context.lastObserveSignsReport)
|
||||
? `最近一次观察结果:${sanitizePromptNarrativeText(context.lastObserveSignsReport)}`
|
||||
: null,
|
||||
sanitizePromptNarrativeText(context.recentActionResult)
|
||||
? `刚刚结算结果:${sanitizePromptNarrativeText(context.recentActionResult)}`
|
||||
: null,
|
||||
`主角:${character.name},${character.title}`,
|
||||
`主角描述:${character.description}`,
|
||||
...playerBackstoryLines,
|
||||
`主角性格:${character.personality}`,
|
||||
...describePlayerOpeningByContext(character, world, context),
|
||||
`世界属性框架:${buildSchemaSummary(schema).map(slot => `${slot.name}:${slot.definition}`).join('、')}`,
|
||||
`主角状态:生命状态 ${describeHpBand(hpRatio)},灵力状态 ${describeManaBand(manaRatio)},整体判断 ${describeOverallBand(hpRatio, manaRatio)},朝向 ${describeFacing(context.playerFacing)},当前动作 ${context.playerAnimation}`,
|
||||
`主角状态:生命状态 ${describeHpBand(hpRatio)},灵力状态 ${describeManaBand(manaRatio)},整体判断 ${describeOverallBand(hpRatio, manaRatio)},朝向 ${describeFacing(context.playerFacing)},当前动作 ${describeAnimationLabel(context.playerAnimation)}`,
|
||||
...describeAttributeProfileForPrompt('主角', world, context, attributeProfile),
|
||||
].filter(Boolean).join('\n');
|
||||
}
|
||||
@@ -913,7 +1286,7 @@ function describeMonsters(monsters: SceneHostileNpc[]) {
|
||||
return monsters
|
||||
.map(monster => {
|
||||
const hpRatio = monster.hp / Math.max(monster.maxHp, 1);
|
||||
return `敌对目标 ${monster.name}:生命状态 ${describeHpBand(hpRatio)},当前动作 ${monster.animation},行为“${monster.action}”,朝向 ${describeFacing(monster.facing)}`;
|
||||
return `敌对目标 ${monster.name}:生命状态 ${describeHpBand(hpRatio)},当前动作 ${describeAnimationLabel(monster.animation)},行为“${monster.action}”,朝向 ${describeFacing(monster.facing)}`;
|
||||
})
|
||||
.join('\n');
|
||||
}
|
||||
@@ -928,17 +1301,29 @@ function _describeHistory(history: string[]) {
|
||||
|
||||
function describeStoryHistory(history: StoryMoment[]) {
|
||||
const promptHistory = buildStoryPromptHistory(history);
|
||||
const previousSummary = sanitizePromptNarrativeText(
|
||||
promptHistory.previousSummary,
|
||||
'更早的剧情已经推进过数轮,请只承接既有结果,不直接沿用其中的非中文原句。',
|
||||
);
|
||||
const recentOriginalRounds = promptHistory.recentOriginalRounds
|
||||
.map((item) =>
|
||||
sanitizePromptNarrativeText(
|
||||
item,
|
||||
'这一轮的原始文本里夹杂了非中文描述,续写时只承接已发生的结果与局势变化。',
|
||||
),
|
||||
)
|
||||
.filter(Boolean);
|
||||
|
||||
if (!promptHistory.previousSummary && promptHistory.recentOriginalRounds.length === 0) {
|
||||
if (!previousSummary && recentOriginalRounds.length === 0) {
|
||||
return '最近剧情:暂无。';
|
||||
}
|
||||
|
||||
return [
|
||||
promptHistory.previousSummary
|
||||
? `3轮以前的历史剧情总结:\n${promptHistory.previousSummary}`
|
||||
previousSummary
|
||||
? `3轮以前的历史剧情总结:\n${previousSummary}`
|
||||
: '3轮以前的历史剧情总结:暂无。',
|
||||
promptHistory.recentOriginalRounds.length > 0
|
||||
? `最近3轮剧情原文(续写时优先承接):\n${promptHistory.recentOriginalRounds
|
||||
recentOriginalRounds.length > 0
|
||||
? `最近3轮剧情原文(续写时优先承接):\n${recentOriginalRounds
|
||||
.map((item, index) => `- 第${index + 1}轮\n${item}`)
|
||||
.join('\n')}`
|
||||
: '最近3轮剧情原文:暂无。',
|
||||
@@ -975,8 +1360,23 @@ function _buildResolvedUserPrompt(
|
||||
const hasProvidedNpcChatOptions = Boolean(availableOptions?.some(option => option.functionId === 'npc_chat'));
|
||||
const isOpeningCampDialogue = context.lastFunctionId === 'story_opening_camp_dialogue' && Boolean(context.encounterName);
|
||||
const hasOpeningCampFollowupContext = hasProvidedNpcChatOptions
|
||||
&& Boolean(context.openingCampBackground?.trim())
|
||||
&& Boolean(context.openingCampDialogue?.trim());
|
||||
&& Boolean(sanitizePromptNarrativeText(context.openingCampBackground))
|
||||
&& Boolean(sanitizePromptNarrativeText(context.openingCampDialogue));
|
||||
const partyRelationshipNotes = sanitizePromptNarrativeText(
|
||||
context.partyRelationshipNotes,
|
||||
);
|
||||
const openingCampBackground = sanitizePromptNarrativeText(
|
||||
context.openingCampBackground,
|
||||
);
|
||||
const openingCampDialogue = sanitizePromptNarrativeText(
|
||||
context.openingCampDialogue,
|
||||
);
|
||||
const safeChoice = choice
|
||||
? sanitizePromptNarrativeText(
|
||||
choice,
|
||||
'玩家刚刚做出了一个新的决定。',
|
||||
)
|
||||
: null;
|
||||
const sceneMonsterIds = getSceneHostileNpcPresetIds(scene);
|
||||
const battleCatalog = scene
|
||||
? buildFunctionCatalogText({
|
||||
@@ -1005,6 +1405,7 @@ function _buildResolvedUserPrompt(
|
||||
describeCampaignSection(context),
|
||||
describeChapterSection(context),
|
||||
describeJourneyBeatSection(context),
|
||||
describeGoalStackSection(context),
|
||||
describeSceneNarrativeDirectiveSection(context),
|
||||
describeVisibilitySliceSection(context),
|
||||
describeConsequenceLedgerSection(context),
|
||||
@@ -1022,11 +1423,11 @@ function _buildResolvedUserPrompt(
|
||||
context.encounterName ? `当前面前实体性别:${describeGender(getEncounterGender(context))}` : null,
|
||||
describeSkills(character, context),
|
||||
`当前敌对目标状态:\n${describeMonsters(monsters)}`,
|
||||
context.partyRelationshipNotes ? `同行角色补充关系信息:\n${context.partyRelationshipNotes}` : null,
|
||||
partyRelationshipNotes ? `同行角色补充关系信息:\n${partyRelationshipNotes}` : null,
|
||||
describeStoryHistory(history),
|
||||
hasOpeningCampFollowupContext ? `营地开场背景:\n${context.openingCampBackground}` : null,
|
||||
hasOpeningCampFollowupContext ? `刚刚发生的第一段营地对话:\n${context.openingCampDialogue}` : null,
|
||||
choice ? `玩家刚刚选择:${choice}` : '玩家刚进入当前局面。',
|
||||
hasOpeningCampFollowupContext ? `营地开场背景:\n${openingCampBackground}` : null,
|
||||
hasOpeningCampFollowupContext ? `刚刚发生的第一段营地对话:\n${openingCampDialogue}` : null,
|
||||
safeChoice ? `玩家刚刚选择:${safeChoice}` : '玩家刚进入当前局面。',
|
||||
hasProvidedOptions
|
||||
? `固定可选项列表(必须保持数量与 functionId 一致,可按最近剧情重排顺序):\n${describeProvidedOptions(availableOptions ?? [])}`
|
||||
: pendingEncounter
|
||||
@@ -1048,6 +1449,7 @@ function _buildResolvedUserPrompt(
|
||||
hasProvidedOptions || !pendingEncounter
|
||||
? null
|
||||
: '你必须先判断主角继续推进后下一刻会遇到什么,并在 encounter 中填写结果。若遇到敌对对象,请使用 kind=npc 并填写 npcId;若不是场景角色,则 options 必须使用空闲 function。',
|
||||
describeEncounterOutputRequirement(Boolean(pendingEncounter)),
|
||||
'当敌人状态偏差时,攻击类 actionText 要更像收割、补刀或终结;当主角低血时,恢复类 actionText 要更像稳住伤势、打坐或调息;当主角低蓝时,至少一个 actionText 要体现节省消耗或稳住节奏。',
|
||||
'storyText 和 options 必须与当前状态严格一致,不能把已经死亡、已经离场或当前不在眼前的实体重新写回画面。',
|
||||
];
|
||||
@@ -1141,6 +1543,12 @@ function describeProvidedOptions(options: StoryOption[]) {
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
function describeEncounterOutputRequirement(pendingEncounter: boolean) {
|
||||
return pendingEncounter
|
||||
? '只有当前文明确要求你判断“主角继续推进后下一刻会遇到什么”时,encounter 才能填写对象;如果这一刻什么都没遇到,请填写 kind=none。'
|
||||
: '当前这一步不是遭遇生成流程。encounter 必须为 null(保持为空),不要生成新的 encounter;尤其是战斗结束后的续写、聊天续写、固定选项续写时,禁止新增场景实体。';
|
||||
}
|
||||
|
||||
function buildCatalogAwareUserPrompt(
|
||||
world: WorldType,
|
||||
character: Character,
|
||||
@@ -1170,8 +1578,23 @@ function buildCatalogAwareUserPrompt(
|
||||
const hasProvidedNpcChatOptions = Boolean(availableOptions?.some(option => option.functionId === 'npc_chat'));
|
||||
const isOpeningCampDialogue = context.lastFunctionId === 'story_opening_camp_dialogue' && Boolean(context.encounterName);
|
||||
const hasOpeningCampFollowupContext = hasProvidedNpcChatOptions
|
||||
&& Boolean(context.openingCampBackground?.trim())
|
||||
&& Boolean(context.openingCampDialogue?.trim());
|
||||
&& Boolean(sanitizePromptNarrativeText(context.openingCampBackground))
|
||||
&& Boolean(sanitizePromptNarrativeText(context.openingCampDialogue));
|
||||
const partyRelationshipNotes = sanitizePromptNarrativeText(
|
||||
context.partyRelationshipNotes,
|
||||
);
|
||||
const openingCampBackground = sanitizePromptNarrativeText(
|
||||
context.openingCampBackground,
|
||||
);
|
||||
const openingCampDialogue = sanitizePromptNarrativeText(
|
||||
context.openingCampDialogue,
|
||||
);
|
||||
const safeChoice = choice
|
||||
? sanitizePromptNarrativeText(
|
||||
choice,
|
||||
'玩家刚刚做出了一个新的决定。',
|
||||
)
|
||||
: null;
|
||||
const battleCatalog = scene
|
||||
? buildFunctionCatalogText({
|
||||
...functionContext,
|
||||
@@ -1199,6 +1622,7 @@ function buildCatalogAwareUserPrompt(
|
||||
describeCampaignSection(context),
|
||||
describeChapterSection(context),
|
||||
describeJourneyBeatSection(context),
|
||||
describeGoalStackSection(context),
|
||||
describeSceneNarrativeDirectiveSection(context),
|
||||
describeVisibilitySliceSection(context),
|
||||
describeConsequenceLedgerSection(context),
|
||||
@@ -1217,11 +1641,11 @@ function buildCatalogAwareUserPrompt(
|
||||
describeConversationSituationDirective(context),
|
||||
describeEncounterConversationDirective(context),
|
||||
describeFirstMeaningfulContactDirective(context),
|
||||
context.partyRelationshipNotes ? `同行角色补充关系信息:\n${context.partyRelationshipNotes}` : null,
|
||||
partyRelationshipNotes ? `同行角色补充关系信息:\n${partyRelationshipNotes}` : null,
|
||||
describeStoryHistory(history),
|
||||
hasOpeningCampFollowupContext ? `营地开场背景:\n${context.openingCampBackground}` : null,
|
||||
hasOpeningCampFollowupContext ? `刚刚发生的第一段营地对话:\n${context.openingCampDialogue}` : null,
|
||||
choice ? `玩家刚刚选择:${choice}` : '玩家刚进入当前局面。',
|
||||
hasOpeningCampFollowupContext ? `营地开场背景:\n${openingCampBackground}` : null,
|
||||
hasOpeningCampFollowupContext ? `刚刚发生的第一段营地对话:\n${openingCampDialogue}` : null,
|
||||
safeChoice ? `玩家刚刚选择:${safeChoice}` : '玩家刚进入当前局面。',
|
||||
hasProvidedOptions
|
||||
? `固定可选项列表(必须保留数量与 functionId,可按最近剧情重排顺序):\n${describeProvidedOptions(availableOptions ?? [])}`
|
||||
: hasOptionCatalog
|
||||
@@ -1251,6 +1675,7 @@ function buildCatalogAwareUserPrompt(
|
||||
hasProvidedOptions || hasOptionCatalog || !pendingEncounter
|
||||
? null
|
||||
: '你必须先判断主角继续推进后下一刻会遇到什么,并在 encounter 中填写结果。若遇到敌对对象,请使用 kind=npc 并填写 npcId;若不是场景角色,则 options 必须使用空闲 function。',
|
||||
describeEncounterOutputRequirement(Boolean(pendingEncounter)),
|
||||
'当敌人状态偏差时,攻击类 actionText 要更像收割、补刀或终结;当主角低血时,恢复类 actionText 要更像稳住伤势、打坐或调息;当主角低蓝时,至少一个 actionText 要体现节省消耗或稳住节奏。',
|
||||
'storyText 和 options 必须与当前状态严格一致,不能把已经死亡、已经离场或当前不在眼前的实体重新写回画面。',
|
||||
];
|
||||
@@ -1297,6 +1722,20 @@ function buildResolvedNpcChatDialoguePrompt(
|
||||
topic: string,
|
||||
resultSummary: string,
|
||||
) {
|
||||
const openingCampBackground = sanitizePromptNarrativeText(
|
||||
context.openingCampBackground,
|
||||
);
|
||||
const openingCampDialogue = sanitizePromptNarrativeText(
|
||||
context.openingCampDialogue,
|
||||
);
|
||||
const safeTopic =
|
||||
sanitizePromptNarrativeText(topic, '眼前刚刚谈到的话头') ?? topic;
|
||||
const safeResultSummary =
|
||||
sanitizePromptNarrativeText(
|
||||
resultSummary,
|
||||
'这段聊天刚让你们之间的气氛发生了新的变化。',
|
||||
) ?? resultSummary;
|
||||
|
||||
return [
|
||||
`世界:${describeWorldForPrompt(world, context.customWorldProfile)}`,
|
||||
describePlayerState(world, character, context),
|
||||
@@ -1308,6 +1747,7 @@ function buildResolvedNpcChatDialoguePrompt(
|
||||
describeCampaignSection(context),
|
||||
describeChapterSection(context),
|
||||
describeJourneyBeatSection(context),
|
||||
describeGoalStackSection(context),
|
||||
describeSceneNarrativeDirectiveSection(context),
|
||||
describeVisibilitySliceSection(context),
|
||||
describeConsequenceLedgerSection(context),
|
||||
@@ -1324,18 +1764,18 @@ function buildResolvedNpcChatDialoguePrompt(
|
||||
describeSkills(character, context),
|
||||
`当前敌对目标状态:\n${describeMonsters(monsters)}`,
|
||||
describeStoryHistory(history),
|
||||
context.openingCampBackground ? `营地开场背景:\n${context.openingCampBackground}` : null,
|
||||
context.openingCampDialogue ? `刚刚发生的第一段营地对话:\n${context.openingCampDialogue}` : null,
|
||||
openingCampBackground ? `营地开场背景:\n${openingCampBackground}` : null,
|
||||
openingCampDialogue ? `刚刚发生的第一段营地对话:\n${openingCampDialogue}` : null,
|
||||
`当前交谈对象:${encounterName}`,
|
||||
`聊天主题:${topic}`,
|
||||
`关系变化结果:${resultSummary}`,
|
||||
`聊天主题:${safeTopic}`,
|
||||
`关系变化结果:${safeResultSummary}`,
|
||||
describeConversationSituationDirective(context),
|
||||
describeEncounterConversationDirective(context),
|
||||
describeFirstMeaningfulContactDirective(context),
|
||||
context.openingCampBackground && context.openingCampDialogue
|
||||
openingCampBackground && openingCampDialogue
|
||||
? '这段 npc_chat 必须承接上面的营地开场背景和第一段对话,像同一段谈话自然往下推进,不要把语气和话题重置成初见模板。'
|
||||
: null,
|
||||
`请围绕“${topic}”写一段刚刚发生的对话。必须只输出对白正文,每一行都必须以“你:”或“${encounterName}:”开头。`,
|
||||
`请围绕“${safeTopic}”写一段刚刚发生的对话。必须只输出对白正文,每一行都必须以“你:”或“${encounterName}:”开头。`,
|
||||
].filter(Boolean).join('\n\n');
|
||||
}
|
||||
|
||||
@@ -1391,6 +1831,15 @@ export function buildNpcRecruitDialoguePrompt(
|
||||
invitationText: string,
|
||||
recruitSummary: string,
|
||||
) {
|
||||
const safeInvitationText =
|
||||
sanitizePromptNarrativeText(invitationText, '我希望你能加入队伍,与我并肩同行。') ??
|
||||
invitationText;
|
||||
const safeRecruitSummary =
|
||||
sanitizePromptNarrativeText(
|
||||
recruitSummary,
|
||||
'双方已经具备继续同行的条件。',
|
||||
) ?? recruitSummary;
|
||||
|
||||
return [
|
||||
`世界:${describeWorldForPrompt(world, context.customWorldProfile)}`,
|
||||
describePlayerState(world, character, context),
|
||||
@@ -1402,6 +1851,7 @@ export function buildNpcRecruitDialoguePrompt(
|
||||
describeCampaignSection(context),
|
||||
describeChapterSection(context),
|
||||
describeJourneyBeatSection(context),
|
||||
describeGoalStackSection(context),
|
||||
describeSceneNarrativeDirectiveSection(context),
|
||||
describeVisibilitySliceSection(context),
|
||||
describeConsequenceLedgerSection(context),
|
||||
@@ -1419,8 +1869,8 @@ export function buildNpcRecruitDialoguePrompt(
|
||||
`当前敌对目标状态:\n${describeMonsters(monsters)}`,
|
||||
describeStoryHistory(history),
|
||||
`当前招募对象:${encounter.npcName}`,
|
||||
`玩家邀请:${invitationText}`,
|
||||
`招募补充条件:${recruitSummary}`,
|
||||
`玩家邀请:${safeInvitationText}`,
|
||||
`招募补充条件:${safeRecruitSummary}`,
|
||||
describeConversationSituationDirective(context),
|
||||
describeEncounterConversationDirective(context),
|
||||
describeFirstMeaningfulContactDirective(context),
|
||||
|
||||
Reference in New Issue
Block a user