120 lines
4.8 KiB
TypeScript
120 lines
4.8 KiB
TypeScript
import {
|
||
buildRuntimeItemAiIntent,
|
||
buildRuntimeItemAiPromptInput,
|
||
} from '../data/runtimeItemNarrative';
|
||
import type {
|
||
RuntimeItemGenerationContext,
|
||
RuntimeItemPlan,
|
||
RuntimeRelationAnchor,
|
||
} from '../types';
|
||
import { buildRuntimeItemStoryFingerprint } from '../services/storyEngine/carrierNarrativeCompiler';
|
||
import { buildCarrierVisibilitySlice } from '../services/storyEngine/visibilityEngine';
|
||
|
||
function describeRelationAnchor(anchor: RuntimeRelationAnchor) {
|
||
switch (anchor.type) {
|
||
case 'npc':
|
||
return `NPC:${anchor.npcName}`;
|
||
case 'scene':
|
||
return `场景:${anchor.sceneName}`;
|
||
case 'monster':
|
||
return `怪物:${anchor.monsterName}`;
|
||
case 'quest':
|
||
return `任务:${anchor.questName}`;
|
||
case 'faction':
|
||
return `势力:${anchor.factionName}`;
|
||
default:
|
||
return `地标:${anchor.landmarkName}`;
|
||
}
|
||
}
|
||
|
||
function describeCarrierFactId(factId: string) {
|
||
if (factId === 'visibleClue') return '可见线索';
|
||
if (factId === 'currentAppearanceReason') return '当前出现理由';
|
||
if (factId === 'witnessMark') return '见证痕';
|
||
if (factId === 'unresolvedQuestion') return '未完成问题';
|
||
if (factId.startsWith('thread:')) return `线程索引(${factId.slice('thread:'.length)})`;
|
||
return factId;
|
||
}
|
||
|
||
function describePlan(
|
||
context: RuntimeItemGenerationContext,
|
||
plan: RuntimeItemPlan,
|
||
index: number,
|
||
) {
|
||
const promptInput = buildRuntimeItemAiPromptInput(context, plan);
|
||
const fallbackIntent = buildRuntimeItemAiIntent(context, plan);
|
||
const fallbackFingerprint = buildRuntimeItemStoryFingerprint({
|
||
context,
|
||
plan,
|
||
intent: fallbackIntent,
|
||
});
|
||
const visibilitySlice = buildCarrierVisibilitySlice({
|
||
activeThreadIds: context.activeThreadIds,
|
||
storyFingerprint: fallbackFingerprint,
|
||
});
|
||
|
||
return [
|
||
`物品 ${index + 1}`,
|
||
`- slot: ${plan.slot}`,
|
||
`- 物品类型: ${promptInput.desiredItemKind}`,
|
||
`- 持续性: ${promptInput.permanence}`,
|
||
`- 关系锚点: ${describeRelationAnchor(plan.relationAnchor)}`,
|
||
`- 世界摘要: ${promptInput.worldSummary}`,
|
||
`- 场景摘要: ${promptInput.sceneSummary || '无'}`,
|
||
`- 遭遇摘要: ${promptInput.encounterSummary || '无'}`,
|
||
`- 相关人物: ${promptInput.relatedNpcSummary}`,
|
||
`- 当前激活线程: ${promptInput.activeThreadSummary || '暂无'}`,
|
||
`- 近期剧情: ${promptInput.recentStorySummary}`,
|
||
`- 玩家当前 build: ${promptInput.playerBuildDirection.join('、') || '均衡'}`,
|
||
`- 玩家待补缺口: ${promptInput.playerBuildGaps.join('、') || '无明显缺口'}`,
|
||
`- 本次目标方向: ${plan.targetBuildDirection.join('、') || '均衡'}`,
|
||
`- 物件可直写线索: ${visibilitySlice.sayableFactIds.map(describeCarrierFactId).join('、') || '无'}`,
|
||
`- 物件只宜暗写: ${visibilitySlice.inferredFactIds.map(describeCarrierFactId).join('、') || '无'}`,
|
||
].join('\n');
|
||
}
|
||
|
||
export const RUNTIME_ITEM_INTENT_SYSTEM_PROMPT = `你是 AI 原生叙事 RPG 的运行时物品导演。
|
||
你只返回 JSON,不要输出 Markdown、解释或代码块。
|
||
|
||
输出结构:
|
||
{
|
||
"intents": [
|
||
{
|
||
"shortNameSeed": "中文短种子",
|
||
"sourcePhrase": "中文来源短语",
|
||
"reasonToAppear": "中文出现理由",
|
||
"relationHooks": ["中文关系钩子"],
|
||
"desiredBuildTags": ["中文 build 标签"],
|
||
"desiredFunctionalBias": ["heal|mana|cooldown|guard|damage"],
|
||
"tone": "grim|mysterious|martial|ritual|survival",
|
||
"visibleClue": "玩家第一眼能抓到的痕迹",
|
||
"witnessMark": "它见证过什么的使用痕",
|
||
"unfinishedBusiness": "背后仍未结清的问题",
|
||
"hiddenHook": "更深一层但别直接讲穿的钩子",
|
||
"reactionHooks": ["以后谁会对它起反应"],
|
||
"namingPattern": "命名范式建议"
|
||
}
|
||
]
|
||
}
|
||
|
||
规则:
|
||
- intents 数量必须与输入物品数量完全一致,顺序也必须一致。
|
||
- 所有自然语言字段都必须使用中文。
|
||
- 物品意图必须贴合当前场景、关系锚点、近期剧情和玩家当前 build。
|
||
- visibleClue / witnessMark / unfinishedBusiness / hiddenHook 要优先围绕当前激活线程、相关角色压力和旧痕来写。
|
||
- desiredBuildTags 要优先围绕玩家当前 build 与待补缺口,不要写空数组。
|
||
- desiredFunctionalBias 只能从给定枚举里选 1 到 2 个。
|
||
- reasonToAppear 必须解释为什么这件东西会在当前局势里出现,而不是泛泛描述。`;
|
||
|
||
export function buildRuntimeItemIntentPrompt(params: {
|
||
context: RuntimeItemGenerationContext;
|
||
plans: RuntimeItemPlan[];
|
||
}) {
|
||
return [
|
||
`生成渠道:${params.context.generationChannel}`,
|
||
`以下每个物品都需要给出一条可编译的运行时物品意图。`,
|
||
...params.plans.map((plan, index) => describePlan(params.context, plan, index)),
|
||
'请严格返回 JSON。',
|
||
].join('\n\n');
|
||
}
|