Files
Genarrative/src/services/storyEngine/knowledgeContract.ts
T
kdletters 071faa482c 统一 Rust 与 TypeScript 格式化门禁
纳入 AGC Cargo workspace 的统一 rustfmt 检查与格式化入口

完成项目 TypeScript/Prettier 与 Rust 全量格式化

修复 Pingora expected executable 门禁的空白敏感误报

同步开发运维文档与 AGC skill pack 格式化忽略规则
2026-09-01 16:28:34 +08:00

115 lines
3.4 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;
}