255 lines
8.4 KiB
TypeScript
255 lines
8.4 KiB
TypeScript
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<string | null | undefined>, 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;
|
|
}
|