import type { CarrierStoryFingerprint, InventoryItem, RuntimeItemAiIntent, RuntimeItemGenerationContext, RuntimeItemPlan, RuntimeRelationAnchor, ThemePack, WorldStoryGraph, } from '../../types'; import { buildThemePackFromWorldProfile } from './themePack'; import { buildFallbackWorldStoryGraph } from './worldStoryGraph'; type CarrierNarrativeParams = { item: InventoryItem; context: RuntimeItemGenerationContext; plan: RuntimeItemPlan; intent: RuntimeItemAiIntent; }; function dedupeStrings(values: Array, limit = 8) { return [...new Set(values.map((value) => value?.trim() ?? '').filter(Boolean))] .slice(0, limit); } 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 '此地'; } } function resolveThemePack(context: RuntimeItemGenerationContext): ThemePack | null { if (!context.customWorldProfile) { return null; } return context.customWorldProfile.themePack ?? buildThemePackFromWorldProfile(context.customWorldProfile); } function resolveStoryGraph( context: RuntimeItemGenerationContext, themePack: ThemePack | null, ): WorldStoryGraph | null { if (!context.customWorldProfile || !themePack) { return null; } return context.customWorldProfile.storyGraph ?? buildFallbackWorldStoryGraph(context.customWorldProfile, themePack); } function resolveThreadLabel( graph: WorldStoryGraph | null, threadIds: string[], ) { const thread = [...(graph?.visibleThreads ?? []), ...(graph?.hiddenThreads ?? [])] .find((candidate) => threadIds.includes(candidate.id)); return thread?.title ?? '未尽旧事'; } function resolveScarLabel( graph: WorldStoryGraph | null, scarIds: string[], ) { const scar = graph?.scars.find((candidate) => scarIds.includes(candidate.id)); return scar?.title ?? '旧痕'; } function resolveFunctionWord(item: InventoryItem, plan: RuntimeItemPlan, intent: RuntimeItemAiIntent) { const topTag = intent.desiredBuildTags[0] ?? plan.targetBuildDirection[0] ?? ''; if (plan.itemKind === 'consumable') { if (intent.desiredFunctionalBias.includes('heal')) return '灵露'; if (intent.desiredFunctionalBias.includes('mana')) return '回气散'; if (intent.desiredFunctionalBias.includes('cooldown')) return '压纹符'; return '药包'; } if (plan.itemKind === 'material') { return topTag ? `${topTag}残材` : '残材'; } if (plan.itemKind === 'quest') { return '信物'; } if (item.equipmentSlotId === 'weapon') { if (topTag === '快剑' || topTag === '追击') return '短刃'; if (topTag === '远射') return '短弓'; if (topTag === '重击') return '战锤'; return '兵刃'; } if (item.equipmentSlotId === 'armor') { return topTag === '守御' ? '护甲' : '护符'; } if (item.equipmentSlotId === 'relic' || plan.itemKind === 'relic') { return topTag === '法力' ? '灵坠' : '护心佩'; } return sanitizeFragment(intent.shortNameSeed) || '秘物'; } export function buildRuntimeItemStoryFingerprint( params: Pick, ) { const { context, plan, intent } = params; const themePack = resolveThemePack(context); const graph = resolveStoryGraph(context, themePack); const anchorLabel = resolveAnchorLabel(plan.relationAnchor); const relatedThreadIds = dedupeStrings([ ...(context.activeThreadIds ?? []), ...(context.relatedNpcNarrativeProfile?.relatedThreadIds ?? []), graph?.visibleThreads[0]?.id, ], 3); const relatedScarIds = dedupeStrings([ ...(context.relatedNpcNarrativeProfile?.relatedScarIds ?? []), graph?.scars[0]?.id, ], 2); const primaryThreadLabel = resolveThreadLabel(graph, relatedThreadIds); const primaryScarLabel = resolveScarLabel(graph, relatedScarIds); const sceneLabel = context.sceneName ?? context.customWorldProfile?.name ?? '此地'; return { visibleClue: intent.visibleClue ?? `${anchorLabel}留下的${themePack?.clueForms[0] ?? '旧痕'}仍粘在这件物上。`, witnessMark: intent.witnessMark ?? `${primaryScarLabel}的余震被磨进了它的纹路与边角。`, unresolvedQuestion: intent.unfinishedBusiness ?? context.relatedNpcNarrativeProfile?.contradiction ?? `${primaryThreadLabel}为什么会在${sceneLabel}重新露头?`, currentAppearanceReason: intent.reasonToAppear || `${sceneLabel}与最近的局势把它推到了你眼前。`, relatedThreadIds, relatedScarIds, reactionHooks: dedupeStrings([ ...(intent.reactionHooks ?? []), ...(context.relatedNpcNarrativeProfile?.reactionHooks ?? []), primaryThreadLabel, anchorLabel, ], 5), } satisfies CarrierStoryFingerprint; } export function buildCarrierNarrativeName(params: CarrierNarrativeParams) { const { item, plan, intent } = params; const fingerprint = buildRuntimeItemStoryFingerprint(params); const anchorWord = sanitizeFragment(resolveAnchorLabel(plan.relationAnchor), 4) || '旧誓'; const threadWord = sanitizeFragment(fingerprint.visibleClue, 4) || sanitizeFragment(fingerprint.witnessMark, 4) || sanitizeFragment(intent.sourcePhrase, 4) || '余痕'; const functionWord = resolveFunctionWord(item, plan, intent); const tabooWord = sanitizeFragment(params.context.relatedNpcNarrativeProfile?.taboo, 4) || '禁名'; switch (intent.namingPattern) { case 'scene_relic': return `${anchorWord}${sanitizeFragment(fingerprint.witnessMark, 4) || '灰纹'}${functionWord}`; case 'faction_issue': return `${anchorWord}制式${functionWord}`; case 'monster_trophy': return `${anchorWord}${sanitizeFragment(fingerprint.visibleClue, 4) || '猎印'}${functionWord}`; case 'quest_evidence': return `${sanitizeFragment(fingerprint.unresolvedQuestion, 4) || anchorWord}${threadWord}信物`; case 'forbidden_object': return `${tabooWord}封痕${functionWord}`; case 'npc_relic': default: return `${anchorWord}${threadWord}${functionWord}`; } } export function buildCarrierNarrativeDescription(params: CarrierNarrativeParams) { const fingerprint = buildRuntimeItemStoryFingerprint(params); const threadLabel = resolveThreadLabel( resolveStoryGraph(params.context, resolveThemePack(params.context)), fingerprint.relatedThreadIds, ); const buildDirectionText = params.intent.desiredBuildTags.join('、') || params.context.playerBuildTags.join('、') || '均衡'; const sceneLabel = params.context.sceneName ?? params.context.customWorldProfile?.name ?? '此地'; return [ fingerprint.visibleClue, `${fingerprint.witnessMark} 它和${threadLabel}之间的牵连还没有真正结清,${fingerprint.unresolvedQuestion}`, `如今它会在${sceneLabel}出现,是因为${fingerprint.currentAppearanceReason}。它也自然偏向${buildDirectionText}方向,适合当前局势里的临场构筑调整。`, ].join(' '); }