import type { ActorNarrativeProfile, CharacterBackstoryRevealConfig, NpcDisclosureStage, StoryEngineMemoryState, VisibilitySlice, } from '../../types'; type EncounterVisibilityParams = { narrativeProfile?: ActorNarrativeProfile | null; backstoryReveal?: CharacterBackstoryRevealConfig | null; disclosureStage?: NpcDisclosureStage | null; isFirstMeaningfulContact?: boolean; seenBackstoryChapterIds?: string[] | null; storyEngineMemory?: StoryEngineMemoryState | null; activeThreadIds?: string[] | null; }; type QuestVisibilityParams = { issuerNarrativeProfile?: ActorNarrativeProfile | null; storyEngineMemory?: StoryEngineMemoryState | null; activeThreadIds?: string[] | null; }; type CarrierVisibilityParams = { activeThreadIds?: string[] | null; storyEngineMemory?: StoryEngineMemoryState | null; storyFingerprint?: { visibleClue?: string; witnessMark?: string; unresolvedQuestion?: string; currentAppearanceReason?: string; } | null; }; function dedupeStrings(values: Array, limit = 12) { return [...new Set(values.map((value) => value?.trim() ?? '').filter(Boolean))] .slice(0, limit); } export function createEmptyStoryEngineMemoryState(): StoryEngineMemoryState { return { discoveredFactIds: [], inferredFactIds: [], activeThreadIds: [], resolvedScarIds: [], recentCarrierIds: [], openedSceneChapterIds: [], currentSceneActState: null, recentSignalIds: [], recentCompanionReactions: [], currentChapter: null, currentJourneyBeatId: null, currentJourneyBeat: null, companionArcStates: [], worldMutations: [], chronicle: [], factionTensionStates: [], currentCampEvent: null, currentSetpieceDirective: null, continueGameDigest: null, campaignState: null, actState: null, consequenceLedger: [], companionResolutions: [], endingState: null, authorialConstraintPack: null, branchBudgetStatus: null, narrativeQaReport: null, narrativeCodex: [], }; } function buildBaseFactIds( narrativeProfile?: ActorNarrativeProfile | null, backstoryReveal?: CharacterBackstoryRevealConfig | null, ) { return dedupeStrings([ narrativeProfile ? 'publicMask' : null, narrativeProfile ? 'firstContactMask' : null, narrativeProfile ? 'visibleLine' : null, narrativeProfile ? 'hiddenLine' : null, narrativeProfile ? 'contradiction' : null, narrativeProfile ? 'debtOrBurden' : null, narrativeProfile ? 'taboo' : null, narrativeProfile ? 'immediatePressure' : null, ...(narrativeProfile?.relatedThreadIds ?? []).map((id) => `thread:${id}`), ...(narrativeProfile?.relatedScarIds ?? []).map((id) => `scar:${id}`), ...(backstoryReveal?.chapters ?? []).map((chapter) => `chapter:${chapter.id}`), ]); } function resolveUnlockedChapterIds( backstoryReveal?: CharacterBackstoryRevealConfig | null, disclosureStage?: NpcDisclosureStage | null, seenBackstoryChapterIds?: string[] | null, ) { const explicitlySeen = new Set( (seenBackstoryChapterIds ?? []).filter((chapterId) => typeof chapterId === 'string'), ); return (backstoryReveal?.chapters ?? []) .filter((chapter, index) => { if (explicitlySeen.has(chapter.id)) return true; if (disclosureStage === 'partial') return index <= 0; if (disclosureStage === 'honest') return index <= 1; if (disclosureStage === 'deep') return true; return false; }) .map((chapter) => `chapter:${chapter.id}`); } export function buildEncounterVisibilitySlice( params: EncounterVisibilityParams, ) { const memory = params.storyEngineMemory ?? createEmptyStoryEngineMemoryState(); const factIds = buildBaseFactIds(params.narrativeProfile, params.backstoryReveal); const unlockedChapterIds = resolveUnlockedChapterIds( params.backstoryReveal, params.disclosureStage, params.seenBackstoryChapterIds, ); const activeThreadFactIds = dedupeStrings([ ...(params.activeThreadIds ?? []).map((id) => `thread:${id}`), ...(memory.activeThreadIds ?? []).map((id) => `thread:${id}`), ], 6); const sayableFactIds = dedupeStrings([ 'publicMask', 'firstContactMask', 'visibleLine', 'immediatePressure', ...unlockedChapterIds, ...(params.disclosureStage === 'honest' || params.disclosureStage === 'deep' ? activeThreadFactIds : activeThreadFactIds.slice(0, 1)), ], 10); const inferredFactIds = dedupeStrings([ 'contradiction', ...activeThreadFactIds, ...(params.narrativeProfile?.reactionHooks.length ? params.narrativeProfile.reactionHooks.map((_, index) => `reaction:${index + 1}`) : []), ], 8); const forbiddenFactIds = dedupeStrings([ 'hiddenLine', 'debtOrBurden', 'taboo', ...(params.narrativeProfile?.relatedScarIds ?? []).map((id) => `scar:${id}`), ...(params.backstoryReveal?.chapters ?? []) .map((chapter) => `chapter:${chapter.id}`) .filter((id) => !unlockedChapterIds.includes(id)), ], 12); if (params.isFirstMeaningfulContact) { return { factIds, sayableFactIds: sayableFactIds.filter((factId) => ['publicMask', 'firstContactMask', 'visibleLine', 'immediatePressure'].includes( factId, ) || factId.startsWith('chapter:'), ), inferredFactIds: inferredFactIds.filter((factId) => factId === 'contradiction' || factId.startsWith('thread:'), ), forbiddenFactIds, misdirectionHints: dedupeStrings([ params.narrativeProfile?.contradiction ? '对方会先拿表层说辞遮住真正的牵连。' : null, params.narrativeProfile?.taboo ? `提到${params.narrativeProfile.taboo}时,对方会本能地把话题拨开。` : null, ], 3), } satisfies VisibilitySlice; } return { factIds, sayableFactIds, inferredFactIds, forbiddenFactIds, misdirectionHints: dedupeStrings([ params.narrativeProfile?.contradiction ? '可让模型写出“这句话不全对”的缝隙感,但不要直接盖章真相。' : null, params.disclosureStage === 'guarded' ? '优先谈眼前压力与表层理由,不要主动摊开完整来历。' : null, ], 3), } satisfies VisibilitySlice; } export function buildQuestVisibilitySlice( params: QuestVisibilityParams, ) { const memory = params.storyEngineMemory ?? createEmptyStoryEngineMemoryState(); const narrativeProfile = params.issuerNarrativeProfile; const factIds = dedupeStrings([ narrativeProfile ? 'publicMask' : null, narrativeProfile ? 'visibleLine' : null, narrativeProfile ? 'immediatePressure' : null, narrativeProfile ? 'contradiction' : null, ...(params.activeThreadIds ?? []).map((id) => `thread:${id}`), ...(memory.activeThreadIds ?? []).map((id) => `thread:${id}`), ]); return { factIds, sayableFactIds: dedupeStrings([ 'publicMask', 'visibleLine', 'immediatePressure', ...(params.activeThreadIds ?? []).map((id) => `thread:${id}`), ], 8), inferredFactIds: dedupeStrings(['contradiction'], 2), forbiddenFactIds: dedupeStrings([ narrativeProfile?.hiddenLine ? 'hiddenLine' : null, narrativeProfile?.taboo ? 'taboo' : null, ], 4), misdirectionHints: dedupeStrings([ narrativeProfile?.contradiction ? '任务发布者会给出能说得过去的表层理由,但不必一次把全部牵连说透。' : null, ], 2), } satisfies VisibilitySlice; } export function buildCarrierVisibilitySlice( params: CarrierVisibilityParams, ) { const factIds = dedupeStrings([ params.storyFingerprint?.visibleClue ? 'visibleClue' : null, params.storyFingerprint?.witnessMark ? 'witnessMark' : null, params.storyFingerprint?.unresolvedQuestion ? 'unresolvedQuestion' : null, params.storyFingerprint?.currentAppearanceReason ? 'currentAppearanceReason' : null, ...(params.activeThreadIds ?? []).map((id) => `thread:${id}`), ], 8); return { factIds, sayableFactIds: dedupeStrings([ 'visibleClue', 'currentAppearanceReason', ...(params.activeThreadIds ?? []).map((id) => `thread:${id}`), ], 6), inferredFactIds: dedupeStrings(['witnessMark', 'unresolvedQuestion'], 4), forbiddenFactIds: [], misdirectionHints: dedupeStrings([ params.storyFingerprint?.unresolvedQuestion ? '保留一点未完成的问题感,不要把物件背后的旧事一次解释到底。' : null, ], 2), } satisfies VisibilitySlice; }