071faa482c
纳入 AGC Cargo workspace 的统一 rustfmt 检查与格式化入口 完成项目 TypeScript/Prettier 与 Rust 全量格式化 修复 Pingora expected executable 门禁的空白敏感误报 同步开发运维文档与 AGC skill pack 格式化忽略规则
115 lines
3.4 KiB
TypeScript
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;
|
|
}
|