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}。`; }