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

128 lines
4.2 KiB
TypeScript

import type {
ChapterState,
GameState,
StorySignal,
WorldMutation,
} from '../../types';
function dedupeStrings(values: Array<string | null | undefined>, limit = 6) {
return [...new Set(values.map((value) => value?.trim() ?? '').filter(Boolean))]
.slice(0, limit);
}
export function resolveWorldMutations(params: {
state: GameState;
signals: StorySignal[];
chapterState: ChapterState | null | undefined;
}) {
const currentSceneId = params.state.currentScenePreset?.id;
const activeThreadIds = params.state.storyEngineMemory?.activeThreadIds ?? [];
const mutations: WorldMutation[] = [];
if (currentSceneId && params.chapterState) {
mutations.push({
id: `mutation:scene:${currentSceneId}:${params.chapterState.stage}`,
mutationType: 'scene_text',
targetId: currentSceneId,
reason: `${params.chapterState.title}正在改写这片地界的表面气氛。`,
relatedThreadIds: params.chapterState.primaryThreadIds,
});
}
if (currentSceneId && params.signals.some((signal) => signal.signalType === 'win_battle')) {
mutations.push({
id: `mutation:pressure:${currentSceneId}:battle`,
mutationType: 'enemy_pressure',
targetId: currentSceneId,
reason: '这一带的敌意正在因交锋结果重新聚拢。',
relatedThreadIds: dedupeStrings(activeThreadIds, 4),
});
}
if (params.signals.some((signal) => signal.signalType === 'obtain_carrier')) {
mutations.push({
id: `mutation:attitude:${currentSceneId ?? 'scene'}:carrier`,
mutationType: 'npc_attitude',
targetId: currentSceneId ?? 'scene',
reason: '关键载体已经落到你手里,相关角色的口风会开始变化。',
relatedThreadIds: dedupeStrings(activeThreadIds, 4),
});
}
if (params.chapterState?.stage === 'climax' && currentSceneId) {
mutations.push({
id: `mutation:route:${currentSceneId}:climax`,
mutationType: 'route_unlock',
targetId: currentSceneId,
reason: '章节高潮逼近,新的通路或对峙点开始显影。',
relatedThreadIds: params.chapterState.primaryThreadIds,
});
}
return mutations;
}
export function applyWorldMutationsToGameState(params: {
state: GameState;
mutations: WorldMutation[];
}) {
const knownMutations = [
...(params.state.storyEngineMemory?.worldMutations ?? []),
...params.mutations,
];
if (knownMutations.length <= 0) {
return params.state;
}
const currentSceneId = params.state.currentScenePreset?.id ?? null;
const relevantMutations = currentSceneId
? knownMutations.filter((mutation) => mutation.targetId === currentSceneId)
: knownMutations;
const latestSceneMutation = relevantMutations
.filter((mutation) => mutation.mutationType === 'scene_text')
.at(-1);
const pressureMutationCount = relevantMutations.filter(
(mutation) => mutation.mutationType === 'enemy_pressure',
).length;
const attitudeMutation = relevantMutations
.filter((mutation) => mutation.mutationType === 'npc_attitude')
.at(-1);
const currentPressureLevel =
pressureMutationCount >= 3
? 'extreme'
: pressureMutationCount === 2
? 'high'
: pressureMutationCount === 1
? 'medium'
: params.state.currentScenePreset?.currentPressureLevel ?? 'low';
return {
...params.state,
currentScenePreset: params.state.currentScenePreset
? {
...params.state.currentScenePreset,
mutationStateText:
[latestSceneMutation?.reason, attitudeMutation?.reason]
.filter(Boolean)
.join(' ')
?? params.state.currentScenePreset.mutationStateText
?? null,
currentPressureLevel,
description: [
params.state.currentScenePreset.description,
latestSceneMutation?.reason,
]
.filter(Boolean)
.join(' '),
npcs: params.state.currentScenePreset.npcs?.map((npc) => ({
...npc,
description:
attitudeMutation && !npc.hostile
? `${npc.description} ${attitudeMutation.reason}`
: npc.description,
})),
}
: params.state.currentScenePreset,
};
}