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