071faa482c
纳入 AGC Cargo workspace 的统一 rustfmt 检查与格式化入口 完成项目 TypeScript/Prettier 与 Rust 全量格式化 修复 Pingora expected executable 门禁的空白敏感误报 同步开发运维文档与 AGC skill pack 格式化忽略规则
252 lines
8.1 KiB
TypeScript
252 lines
8.1 KiB
TypeScript
import {
|
|
buildCarrierNarrativeDescription,
|
|
buildCarrierNarrativeName,
|
|
buildRuntimeItemStoryFingerprint,
|
|
} from '../services/storyEngine/carrierNarrativeCompiler';
|
|
import type {
|
|
DirectedRuntimeReward,
|
|
InventoryItem,
|
|
RuntimeItemAiIntent,
|
|
RuntimeItemAiPromptInput,
|
|
RuntimeItemGenerationContext,
|
|
RuntimeItemPlan,
|
|
RuntimeRelationAnchor,
|
|
} from '../types';
|
|
|
|
function sanitizeFragment(value: string | null | undefined, maxLength = 4) {
|
|
return (value ?? '')
|
|
.replace(/[^\u4e00-\u9fa5a-z0-9]/giu, '')
|
|
.slice(0, maxLength);
|
|
}
|
|
|
|
function resolveAnchorLabel(anchor: RuntimeRelationAnchor) {
|
|
switch (anchor.type) {
|
|
case 'npc':
|
|
return anchor.npcName;
|
|
case 'scene':
|
|
return anchor.sceneName;
|
|
case 'landmark':
|
|
return anchor.landmarkName;
|
|
case 'monster':
|
|
return anchor.monsterName;
|
|
case 'faction':
|
|
return anchor.factionName;
|
|
case 'quest':
|
|
return anchor.questName;
|
|
default:
|
|
return '无名之地';
|
|
}
|
|
}
|
|
|
|
export function buildRuntimeItemAiPromptInput(
|
|
context: RuntimeItemGenerationContext,
|
|
plan: RuntimeItemPlan,
|
|
): RuntimeItemAiPromptInput {
|
|
const storyGraph = context.customWorldProfile?.storyGraph;
|
|
const activeThreadSummary = (context.activeThreadIds ?? [])
|
|
.map(
|
|
(threadId) =>
|
|
[
|
|
...(storyGraph?.visibleThreads ?? []),
|
|
...(storyGraph?.hiddenThreads ?? []),
|
|
].find((thread) => thread.id === threadId)?.title ?? threadId,
|
|
)
|
|
.join('、');
|
|
|
|
return {
|
|
worldSummary:
|
|
context.customWorldProfile?.summary ?? context.worldType ?? '未知世界',
|
|
sceneSummary: [context.sceneName, context.sceneDescription]
|
|
.filter(Boolean)
|
|
.join(' / '),
|
|
encounterSummary: [context.encounterNpcName, context.encounterContextText]
|
|
.filter(Boolean)
|
|
.join(' / '),
|
|
relatedNpcSummary: context.relatedNpcNarrativeProfile
|
|
? `${context.encounterNpcName ?? '相关人物'}:公开面 ${context.relatedNpcNarrativeProfile.publicMask};当前压力 ${context.relatedNpcNarrativeProfile.immediatePressure}`
|
|
: context.relatedNpcState
|
|
? `${context.encounterNpcName ?? '相关人物'} 当前好感 ${context.relatedNpcState.affinity}`
|
|
: '暂无明确人物关系',
|
|
recentStorySummary: context.recentStorySummary,
|
|
activeThreadSummary,
|
|
generationChannel: context.generationChannel,
|
|
playerBuildDirection: context.playerBuildTags,
|
|
playerBuildGaps: context.playerBuildGaps,
|
|
desiredItemKind: plan.itemKind,
|
|
permanence: plan.permanence,
|
|
};
|
|
}
|
|
|
|
export function buildRuntimeItemAiIntent(
|
|
context: RuntimeItemGenerationContext,
|
|
plan: RuntimeItemPlan,
|
|
): RuntimeItemAiIntent {
|
|
const anchorLabel = resolveAnchorLabel(plan.relationAnchor);
|
|
const sourceSeed =
|
|
sanitizeFragment(context.sceneName, 4) ||
|
|
sanitizeFragment(context.customWorldProfile?.name, 4) ||
|
|
sanitizeFragment(anchorLabel, 4) ||
|
|
'旧誓';
|
|
const functionalBias: RuntimeItemAiIntent['desiredFunctionalBias'] = [];
|
|
|
|
if (plan.permanence === 'timed') {
|
|
functionalBias.push(
|
|
context.playerBuildGaps.includes('survival_gap') ? 'heal' : 'cooldown',
|
|
);
|
|
}
|
|
if (context.playerBuildGaps.includes('mana_gap')) functionalBias.push('mana');
|
|
if (context.playerBuildGaps.includes('survival_gap'))
|
|
functionalBias.push('guard');
|
|
if (
|
|
functionalBias.length <= 0 ||
|
|
context.playerBuildGaps.includes('finisher_gap') ||
|
|
plan.itemKind === 'equipment'
|
|
) {
|
|
functionalBias.push('damage');
|
|
}
|
|
|
|
return {
|
|
shortNameSeed: sourceSeed,
|
|
sourcePhrase: anchorLabel,
|
|
reasonToAppear:
|
|
context.generationChannel === 'monster_drop'
|
|
? `${anchorLabel}倒下后,${context.sceneName ?? '这片战场'}里最值得带走的残留被翻了出来。`
|
|
: `${anchorLabel}与最近局势把它推到了你面前。`,
|
|
relationHooks: [
|
|
context.encounterContextText ?? context.sceneName ?? anchorLabel,
|
|
...context.recentActions,
|
|
]
|
|
.filter(Boolean)
|
|
.slice(0, 2) as string[],
|
|
desiredBuildTags: [
|
|
...new Set([
|
|
...plan.targetBuildDirection,
|
|
...context.playerBuildTags.slice(0, 2),
|
|
]),
|
|
].slice(0, 3),
|
|
desiredFunctionalBias: [...new Set(functionalBias)].slice(0, 2),
|
|
tone:
|
|
context.generationChannel === 'monster_drop'
|
|
? 'grim'
|
|
: context.generationChannel === 'quest_reward'
|
|
? 'ritual'
|
|
: context.playerBuildGaps.includes('survival_gap')
|
|
? 'survival'
|
|
: 'martial',
|
|
visibleClue:
|
|
context.relatedNpcNarrativeProfile?.visibleLine ??
|
|
`${anchorLabel}身上留下的旧痕`,
|
|
witnessMark:
|
|
context.relatedNpcNarrativeProfile?.debtOrBurden ??
|
|
`${anchorLabel}尚未散尽的使用痕`,
|
|
unfinishedBusiness:
|
|
context.relatedNpcNarrativeProfile?.contradiction ??
|
|
`${anchorLabel}背后还有没说完的问题`,
|
|
hiddenHook:
|
|
context.relatedNpcNarrativeProfile?.taboo ??
|
|
`${anchorLabel}为什么会在此刻重新出现`,
|
|
reactionHooks: [
|
|
...(context.relatedNpcNarrativeProfile?.reactionHooks ?? []),
|
|
...(context.activeThreadIds ?? []),
|
|
].slice(0, 4),
|
|
namingPattern:
|
|
plan.itemKind === 'quest'
|
|
? 'quest_evidence'
|
|
: plan.itemKind === 'material'
|
|
? 'scene_relic'
|
|
: plan.relationAnchor.type === 'monster'
|
|
? 'monster_trophy'
|
|
: plan.relationAnchor.type === 'npc'
|
|
? 'npc_relic'
|
|
: 'faction_issue',
|
|
};
|
|
}
|
|
|
|
export function applyRuntimeItemNarrative(params: {
|
|
item: InventoryItem;
|
|
context: RuntimeItemGenerationContext;
|
|
plan: RuntimeItemPlan;
|
|
intent: RuntimeItemAiIntent;
|
|
}) {
|
|
const fingerprint = buildRuntimeItemStoryFingerprint(params);
|
|
const runtimeMetadata = params.item.runtimeMetadata ?? {
|
|
origin: 'ai_compiled' as const,
|
|
generationChannel: params.context.generationChannel,
|
|
seedKey: `${params.context.generationChannel}:${params.item.id}`,
|
|
sourceReason: params.intent.reasonToAppear,
|
|
};
|
|
|
|
return {
|
|
...params.item,
|
|
name: buildCarrierNarrativeName(params),
|
|
description: buildCarrierNarrativeDescription(params),
|
|
runtimeMetadata: {
|
|
...runtimeMetadata,
|
|
storyFingerprint: fingerprint,
|
|
},
|
|
} satisfies InventoryItem;
|
|
}
|
|
|
|
export function applyRuntimeItemNarrativeToExistingItem(params: {
|
|
item: InventoryItem;
|
|
context: RuntimeItemGenerationContext;
|
|
plan: RuntimeItemPlan;
|
|
intent: RuntimeItemAiIntent;
|
|
preserveName?: boolean;
|
|
}) {
|
|
const fingerprint = buildRuntimeItemStoryFingerprint(params);
|
|
const runtimeMetadata = params.item.runtimeMetadata ?? {
|
|
origin: 'procedural' as const,
|
|
generationChannel: params.context.generationChannel,
|
|
relationAnchor: params.plan.relationAnchor,
|
|
seedKey: `${params.context.generationChannel}:${params.item.id}`,
|
|
sourceReason: params.intent.reasonToAppear,
|
|
};
|
|
|
|
return {
|
|
...params.item,
|
|
name: params.preserveName
|
|
? params.item.name
|
|
: buildCarrierNarrativeName(params),
|
|
description: buildCarrierNarrativeDescription(params),
|
|
runtimeMetadata: {
|
|
...runtimeMetadata,
|
|
relationAnchor:
|
|
runtimeMetadata.relationAnchor ?? params.plan.relationAnchor,
|
|
sourceReason: params.intent.reasonToAppear,
|
|
storyFingerprint: fingerprint,
|
|
},
|
|
} satisfies InventoryItem;
|
|
}
|
|
|
|
export function describeRuntimeRelationAnchor(
|
|
anchor: RuntimeRelationAnchor | undefined,
|
|
) {
|
|
if (!anchor) return '无明确锚点';
|
|
return `${anchor.type}:${resolveAnchorLabel(anchor)}`;
|
|
}
|
|
|
|
export function flattenDirectedRuntimeRewardItems(
|
|
reward: DirectedRuntimeReward,
|
|
) {
|
|
return [
|
|
...(reward.primaryItem ? [reward.primaryItem] : []),
|
|
...reward.supportItems,
|
|
];
|
|
}
|
|
|
|
export function buildRuntimeRewardStoryHint(reward: DirectedRuntimeReward) {
|
|
const primaryItem = reward.primaryItem;
|
|
const fingerprint = primaryItem?.runtimeMetadata?.storyFingerprint;
|
|
if (!primaryItem) {
|
|
return reward.storyHint ?? '你得到了一件与当前局势相关的物品。';
|
|
}
|
|
if (reward.storyHint) {
|
|
return reward.storyHint;
|
|
}
|
|
if (fingerprint) {
|
|
return `${primaryItem.name}先露出的是“${fingerprint.visibleClue}”,但它背后还压着“${fingerprint.unresolvedQuestion}”。`;
|
|
}
|
|
return `这次得到的核心物件是 ${primaryItem.name}。`;
|
|
}
|