This commit is contained in:
98
src/services/storyEngine/knowledgeContract.ts
Normal file
98
src/services/storyEngine/knowledgeContract.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user