import type { KnowledgeFact, NpcDisclosureStage, VisibilitySlice, } from '../../types'; type BuildVisibilitySliceFromFactsParams = { facts: KnowledgeFact[]; discoveredFactIds?: string[] | null; activeThreadIds?: string[] | null; disclosureStage?: NpcDisclosureStage | null; isFirstMeaningfulContact?: boolean; }; function dedupeStrings(values: Array, limit = 20) { return [...new Set(values.map((value) => value?.trim() ?? '').filter(Boolean))] .slice(0, limit); } function canDiscloseFact( fact: KnowledgeFact, disclosureStage: NpcDisclosureStage | null | undefined, isFirstMeaningfulContact: boolean | undefined, ) { const visibility = fact.visibility; if (visibility === 'forbidden') return false; if (isFirstMeaningfulContact) { return visibility === 'public' || fact.title.includes('首遇') || fact.title.includes('当前压力'); } if (disclosureStage === 'guarded') { return visibility === 'public' || fact.sayability === 'direct'; } if (disclosureStage === 'partial') { return fact.sayability !== 'reactive_only'; } if (disclosureStage === 'honest') { return true; } return true; } export function buildMisdirectionFacts(params: { facts: KnowledgeFact[]; activeThreadIds?: string[] | null; }) { return params.facts.filter((fact) => fact.sayability === 'indirect' && ( !params.activeThreadIds?.length || fact.relatedThreadIds.some((threadId) => params.activeThreadIds?.includes(threadId)) ), ); } export function buildVisibilitySliceFromFacts( params: BuildVisibilitySliceFromFactsParams, ) { const discoveredFactIds = new Set(params.discoveredFactIds ?? []); const activeThreadIds = params.activeThreadIds ?? []; const relevantFacts = params.facts.filter((fact) => activeThreadIds.length <= 0 || fact.relatedThreadIds.length <= 0 || fact.relatedThreadIds.some((threadId) => activeThreadIds.includes(threadId)), ); const discoveredFacts = relevantFacts.filter((fact) => discoveredFactIds.has(fact.id) || fact.visibility === 'public', ); const sayableFacts = relevantFacts.filter((fact) => canDiscloseFact( fact, params.disclosureStage ?? null, params.isFirstMeaningfulContact, ) && (fact.visibility === 'public' || discoveredFactIds.has(fact.id) || fact.sayability === 'direct'), ); const inferredFacts = buildMisdirectionFacts({ facts: relevantFacts, activeThreadIds, }); const forbiddenFacts = relevantFacts.filter((fact) => ['forbidden'].includes(fact.visibility) || fact.sayability === 'reactive_only' || (fact.visibility === 'private' && !discoveredFactIds.has(fact.id)), ); return { factIds: dedupeStrings(relevantFacts.map((fact) => fact.id)), sayableFactIds: dedupeStrings(sayableFacts.map((fact) => fact.id)), inferredFactIds: dedupeStrings(inferredFacts.map((fact) => fact.id)), forbiddenFactIds: dedupeStrings(forbiddenFacts.map((fact) => fact.id)), misdirectionHints: dedupeStrings([ ...inferredFacts.map((fact) => fact.content), ...discoveredFacts .filter((fact) => fact.sayability === 'indirect') .map((fact) => `可让玩家先误以为:${fact.content}`), ], 8), } satisfies VisibilitySlice; }