import type { CustomWorldProfile, CustomWorldRoleProfile, InventoryItem, KnowledgeFact, WorldStoryGraph, } from '../../types'; function dedupeStrings(values: Array, limit = 12) { return [...new Set(values.map((value) => value?.trim() ?? '').filter(Boolean))] .slice(0, limit); } function slugify(value: string) { const ascii = value .trim() .toLowerCase() .replace(/[^a-z0-9\u4e00-\u9fa5]+/giu, '-') .replace(/^-+|-+$/g, ''); return ascii || 'fact'; } function createFactId(prefix: string, label: string, index: number) { return `${prefix}:${slugify(label)}:${index + 1}`; } function resolveRelatedFacts( role: Pick, graph: WorldStoryGraph, ) { const relatedThreadIds = role.narrativeProfile?.relatedThreadIds.length ? role.narrativeProfile.relatedThreadIds : graph.visibleThreads.slice(0, 1).map((thread) => thread.id); const relatedScarIds = role.narrativeProfile?.relatedScarIds ?? []; return { relatedThreadIds, relatedScarIds, }; } function buildFact( role: Pick, graph: WorldStoryGraph, options: { key: string; title: string; content: string; sourceType: KnowledgeFact['sourceType']; visibility: KnowledgeFact['visibility']; sayability: KnowledgeFact['sayability']; aliases?: string[]; index: number; }, ) { const related = resolveRelatedFacts(role, graph); return { id: createFactId(`${role.id}:${options.key}`, options.title, options.index), title: options.title, content: options.content, ownerActorIds: [role.id], relatedThreadIds: related.relatedThreadIds, relatedScarIds: related.relatedScarIds, sourceType: options.sourceType, visibility: options.visibility, sayability: options.sayability, aliases: options.aliases, } satisfies KnowledgeFact; } export function buildActorKnowledgeFacts( role: CustomWorldRoleProfile, graph: WorldStoryGraph, ) { const profile = role.narrativeProfile; if (!profile) { return [] as KnowledgeFact[]; } const chapterFacts = role.backstoryReveal.chapters.map((chapter, index) => buildFact(role, graph, { key: `chapter:${chapter.id}`, title: chapter.title, content: chapter.contextSnippet || chapter.content || chapter.teaser, sourceType: 'actor', visibility: index === 0 ? 'public' : index === 1 ? 'discoverable' : index === 2 ? 'private' : 'forbidden', sayability: index <= 1 ? 'direct' : index === 2 ? 'indirect' : 'reactive_only', aliases: [chapter.id, chapter.teaser], index, }), ); return [ buildFact(role, graph, { key: 'publicMask', title: `${role.name}的公开面`, content: profile.publicMask, sourceType: 'actor', visibility: 'public', sayability: 'direct', aliases: [role.name, role.title], index: 0, }), buildFact(role, graph, { key: 'firstContactMask', title: `${role.name}的首遇说辞`, content: profile.firstContactMask, sourceType: 'actor', visibility: 'discoverable', sayability: 'direct', aliases: [role.name, '首遇'], index: 1, }), buildFact(role, graph, { key: 'visibleLine', title: `${role.name}的表层线`, content: profile.visibleLine, sourceType: 'actor', visibility: 'discoverable', sayability: 'direct', aliases: profile.reactionHooks, index: 2, }), buildFact(role, graph, { key: 'contradiction', title: `${role.name}的错位`, content: profile.contradiction, sourceType: 'actor', visibility: 'discoverable', sayability: 'indirect', aliases: profile.reactionHooks, index: 3, }), buildFact(role, graph, { key: 'immediatePressure', title: `${role.name}的当前压力`, content: profile.immediatePressure, sourceType: 'actor', visibility: 'discoverable', sayability: 'direct', aliases: profile.relatedThreadIds, index: 4, }), buildFact(role, graph, { key: 'hiddenLine', title: `${role.name}的隐藏线`, content: profile.hiddenLine, sourceType: 'actor', visibility: 'private', sayability: 'reactive_only', aliases: profile.relatedThreadIds, index: 5, }), buildFact(role, graph, { key: 'debtOrBurden', title: `${role.name}的代价线`, content: profile.debtOrBurden, sourceType: 'actor', visibility: 'private', sayability: 'indirect', aliases: profile.relatedScarIds, index: 6, }), buildFact(role, graph, { key: 'taboo', title: `${role.name}的禁区`, content: profile.taboo, sourceType: 'actor', visibility: 'forbidden', sayability: 'reactive_only', aliases: profile.reactionHooks, index: 7, }), ...chapterFacts, ]; } export function buildCarrierKnowledgeFacts( carrier: InventoryItem, _graph: WorldStoryGraph, ) { const fingerprint = carrier.runtimeMetadata?.storyFingerprint; if (!fingerprint) { return [] as KnowledgeFact[]; } return [ { id: createFactId(`carrier:${carrier.id}`, carrier.name, 0), title: `${carrier.name}的可见线索`, content: fingerprint.visibleClue, ownerActorIds: [], relatedThreadIds: fingerprint.relatedThreadIds, relatedScarIds: fingerprint.relatedScarIds, sourceType: 'item', visibility: 'discoverable', sayability: 'direct', aliases: dedupeStrings([carrier.name]), }, { id: createFactId(`carrier:${carrier.id}`, `${carrier.name}-witness`, 1), title: `${carrier.name}的见证痕`, content: fingerprint.witnessMark, ownerActorIds: [], relatedThreadIds: fingerprint.relatedThreadIds, relatedScarIds: fingerprint.relatedScarIds, sourceType: 'item', visibility: 'discoverable', sayability: 'indirect', aliases: fingerprint.reactionHooks, }, { id: createFactId(`carrier:${carrier.id}`, `${carrier.name}-question`, 2), title: `${carrier.name}的未完成问题`, content: fingerprint.unresolvedQuestion, ownerActorIds: [], relatedThreadIds: fingerprint.relatedThreadIds, relatedScarIds: fingerprint.relatedScarIds, sourceType: 'item', visibility: 'private', sayability: 'indirect', aliases: fingerprint.reactionHooks, }, ] satisfies KnowledgeFact[]; } function buildSceneKnowledgeFacts(profile: CustomWorldProfile, graph: WorldStoryGraph) { return profile.landmarks.flatMap((landmark, landmarkIndex) => (landmark.narrativeResidues ?? []).map((residue, residueIndex) => ({ id: createFactId(`scene:${landmark.id}`, residue.title, residueIndex + landmarkIndex * 10), title: residue.title, content: residue.visibleClue, ownerActorIds: landmark.sceneNpcIds, relatedThreadIds: residue.linkedThreadIds, relatedScarIds: graph.scars .filter((scar) => scar.relatedLocationIds.includes(landmark.id)) .map((scar) => scar.id), sourceType: 'scene', visibility: 'discoverable', sayability: 'direct', aliases: [landmark.name, residue.title], }) satisfies KnowledgeFact), ); } export function buildKnowledgeGraph(profile: CustomWorldProfile) { const graph = profile.storyGraph; if (!graph) { return [] as KnowledgeFact[]; } return dedupeStrings([ ...profile.playableNpcs.flatMap((role) => buildActorKnowledgeFacts(role, graph).map((fact) => fact.id)), ]).length >= 0 ? [ ...profile.playableNpcs.flatMap((role) => buildActorKnowledgeFacts(role, graph)), ...profile.storyNpcs.flatMap((role) => buildActorKnowledgeFacts(role, graph)), ...profile.items.flatMap((item) => buildCarrierKnowledgeFacts( { id: item.id, category: item.category, name: item.name, quantity: 1, rarity: item.rarity, tags: item.tags, description: item.description, } as InventoryItem, graph, ), ), ...buildSceneKnowledgeFacts(profile, graph), ] : []; }