Files
Genarrative/src/services/storyEngine/chapterDirector.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

217 lines
6.1 KiB
TypeScript

import {
buildSceneChapterId,
getQuestActiveStep,
isQuestReadyToClaim,
} from '../../data/questFlow';
import type {
ChapterState,
CustomWorldProfile,
GameState,
QuestLogEntry,
} from '../../types';
function dedupeStrings(values: Array<string | null | undefined>, limit = 4) {
return [
...new Set(values.map((value) => value?.trim() ?? '').filter(Boolean)),
].slice(0, limit);
}
function resolveChapterStage(params: {
signalCount: number;
chronicleCount: number;
activeThreadCount: number;
currentStage?: ChapterState['stage'] | null;
}) {
const score =
params.signalCount + params.chronicleCount + params.activeThreadCount;
if (score >= 12) return 'aftermath' as const;
if (score >= 9) return 'climax' as const;
if (score >= 6) return 'turning_point' as const;
if (score >= 3) return 'expansion' as const;
return params.currentStage === 'aftermath' ? 'aftermath' : 'opening';
}
function resolveChapterTheme(
profile: CustomWorldProfile | null | undefined,
primaryThreadTitles: string[],
) {
if (primaryThreadTitles.length > 0) {
return primaryThreadTitles.join('、');
}
return profile?.themePack?.displayName ?? profile?.summary ?? '旅程推进';
}
function getStageLabel(stage: ChapterState['stage']) {
switch (stage) {
case 'opening':
return '序章';
case 'expansion':
return '展开';
case 'turning_point':
return '转折';
case 'climax':
return '高潮';
case 'aftermath':
return '余波';
default:
return '推进';
}
}
function resolveSceneChapterQuest(state: GameState) {
const sceneId = state.currentScenePreset?.id;
if (!sceneId) {
return null;
}
const chapterId = buildSceneChapterId(sceneId);
return (
state.quests.find(
(quest) =>
quest.chapterId === chapterId &&
quest.status !== 'failed' &&
quest.status !== 'expired',
) ?? null
);
}
function deriveChapterStageFromQuest(
quest: QuestLogEntry,
): ChapterState['stage'] {
if (quest.status === 'turned_in') {
return 'aftermath';
}
if (isQuestReadyToClaim(quest)) {
return 'climax';
}
const activeStep = getQuestActiveStep(quest);
const activeStepIndex = activeStep
? Math.max(
0,
quest.steps?.findIndex((step) => step.id === activeStep.id) ?? 0,
)
: -1;
if (activeStepIndex <= 0) {
return 'opening';
}
if (activeStepIndex === 1) {
return 'expansion';
}
return 'turning_point';
}
function buildSceneChapterSummary(params: {
sceneName: string;
quest: QuestLogEntry;
stage: ChapterState['stage'];
}) {
const { sceneName, quest, stage } = params;
const activeStep = getQuestActiveStep(quest);
switch (stage) {
case 'opening':
return `${sceneName} 的这一章刚刚开启。${activeStep?.revealText ?? quest.description}`;
case 'expansion':
return `${sceneName} 的压力正在展开。${activeStep?.revealText ?? quest.summary}`;
case 'turning_point':
return `${sceneName} 的线索正在改写当前判断。${activeStep?.revealText ?? quest.summary}`;
case 'climax':
return `${sceneName} 的核心矛盾已经被推到最后一步,只差把这一章正式收束。`;
case 'aftermath':
return `${sceneName} 这一章已经完成收束,余波和下一段去向正在显形。`;
default:
return `${sceneName} 的这一章仍在推进中。`;
}
}
export function resolveCurrentChapterState(params: { state: GameState }) {
const { state } = params;
const storyEngineMemory = state.storyEngineMemory;
const profile = state.customWorldProfile;
const activeThreadIds = storyEngineMemory?.activeThreadIds ?? [];
const threadTitles = activeThreadIds.map(
(threadId) =>
[
...(profile?.storyGraph?.visibleThreads ?? []),
...(profile?.storyGraph?.hiddenThreads ?? []),
].find((thread) => thread.id === threadId)?.title ?? threadId,
);
const signalCount = storyEngineMemory?.recentSignalIds?.length ?? 0;
const chronicleCount = storyEngineMemory?.chronicle?.length ?? 0;
const sceneChapterQuest = resolveSceneChapterQuest(state);
const currentSceneId = state.currentScenePreset?.id ?? null;
const currentSceneName = state.currentScenePreset?.name ?? '当前区域';
if (sceneChapterQuest && currentSceneId) {
const stage = deriveChapterStageFromQuest(sceneChapterQuest);
const theme =
sceneChapterQuest.title || resolveChapterTheme(profile, threadTitles);
return {
id: buildSceneChapterId(currentSceneId),
title: `${currentSceneName}·${getStageLabel(stage)}`,
theme,
primaryThreadIds: dedupeStrings(
[sceneChapterQuest.threadId, ...activeThreadIds],
3,
),
stage,
chapterSummary: buildSceneChapterSummary({
sceneName: currentSceneName,
quest: sceneChapterQuest,
stage,
}),
sceneId: currentSceneId,
chapterQuestId: sceneChapterQuest.id,
} satisfies ChapterState;
}
const stage = resolveChapterStage({
signalCount,
chronicleCount,
activeThreadCount: activeThreadIds.length,
currentStage:
state.chapterState?.stage ??
storyEngineMemory?.currentChapter?.stage ??
null,
});
const theme = resolveChapterTheme(profile, threadTitles);
const title = `${theme || '旅程'}·${getStageLabel(stage)}`;
return {
id: `chapter:${dedupeStrings(activeThreadIds, 2).join('+') || 'default'}:${stage}`,
title,
theme,
primaryThreadIds: dedupeStrings(activeThreadIds, 3),
stage,
chapterSummary: `${title} 当前围绕 ${theme || '旅程主线'} 推进。`,
sceneId: null,
chapterQuestId: null,
} satisfies ChapterState;
}
export function advanceChapterState(params: {
previousChapter: ChapterState | null | undefined;
nextChapter: ChapterState;
}) {
if (!params.previousChapter) {
return params.nextChapter;
}
if (
params.previousChapter.stage === params.nextChapter.stage &&
params.previousChapter.theme === params.nextChapter.theme
) {
return {
...params.nextChapter,
id: params.previousChapter.id,
};
}
return params.nextChapter;
}