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