Files
Genarrative/src/services/storyEngine/knowledgeContract.ts
kdletters cbc27bad4a
Some checks failed
CI / verify (push) Has been cancelled
init with react+axum+spacetimedb
2026-04-26 18:06:23 +08:00

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;
}