Files
Genarrative/src/services/storyEngine/threadSignalRouter.ts
高物 ddcb5d5c8c
Some checks failed
CI / verify (push) Has been cancelled
Rework story engine flow and reorganize project docs
2026-04-06 23:19:00 +08:00

170 lines
5.0 KiB
TypeScript

import type {
GameState,
InventoryItem,
QuestLogEntry,
StorySignal,
ThreadContract,
} from '../../types';
function dedupeStrings(values: Array<string | null | undefined>, limit = 12) {
return [...new Set(values.map((value) => value?.trim() ?? '').filter(Boolean))]
.slice(0, limit);
}
function createSignalId(prefix: string, key: string) {
return `${prefix}:${key}`;
}
export function collectStorySignals(params: {
prevState: GameState;
nextState: GameState;
actionText: string;
lastFunctionId?: string | null;
rewardItems?: InventoryItem[];
}) {
const signals: StorySignal[] = [];
const activeThreadIds = params.nextState.storyEngineMemory?.activeThreadIds ?? [];
if (params.prevState.currentScenePreset?.id !== params.nextState.currentScenePreset?.id) {
if (params.prevState.currentScenePreset?.id) {
signals.push({
id: createSignalId('leave_scene', params.prevState.currentScenePreset.id),
signalType: 'leave_scene',
sceneId: params.prevState.currentScenePreset.id,
threadIds: activeThreadIds,
});
}
if (params.nextState.currentScenePreset?.id) {
signals.push({
id: createSignalId('enter_scene', params.nextState.currentScenePreset.id),
signalType: 'enter_scene',
sceneId: params.nextState.currentScenePreset.id,
threadIds: activeThreadIds,
});
}
}
if (params.lastFunctionId === 'idle_observe_signs') {
signals.push({
id: createSignalId('inspect_scene', params.nextState.currentScenePreset?.id ?? 'scene'),
signalType: 'inspect_scene',
sceneId: params.nextState.currentScenePreset?.id ?? null,
threadIds: activeThreadIds,
});
}
if (params.nextState.currentEncounter?.kind === 'npc' || /||/u.test(params.actionText)) {
signals.push({
id: createSignalId(
'talk_to_actor',
params.nextState.currentEncounter?.id
?? params.prevState.currentEncounter?.id
?? params.actionText,
),
signalType: 'talk_to_actor',
actorId:
params.nextState.currentEncounter?.id
?? params.prevState.currentEncounter?.id
?? null,
threadIds: activeThreadIds,
});
}
if (params.lastFunctionId === 'npc_gift') {
signals.push({
id: createSignalId('give_item', params.actionText),
signalType: 'give_item',
actorId:
params.prevState.currentEncounter?.id
?? params.nextState.currentEncounter?.id
?? null,
threadIds: activeThreadIds,
});
}
if (params.lastFunctionId === 'npc_quest_accept') {
signals.push({
id: createSignalId('accept_contract', params.actionText),
signalType: 'accept_contract',
actorId:
params.prevState.currentEncounter?.id
?? params.nextState.currentEncounter?.id
?? null,
threadIds: activeThreadIds,
});
}
if ((params.rewardItems ?? []).length > 0) {
params.rewardItems!.forEach((item) => {
const threadIds = item.runtimeMetadata?.storyFingerprint?.relatedThreadIds ?? activeThreadIds;
signals.push({
id: createSignalId('obtain_carrier', item.id),
signalType: 'obtain_carrier',
carrierId: item.id,
threadIds,
});
});
}
return signals;
}
function updateQuestFromSignals(
quest: QuestLogEntry,
signals: StorySignal[],
contracts: ThreadContract[],
) {
const relevantSignals = signals.filter((signal) =>
(quest.threadId && signal.threadIds?.includes(quest.threadId))
|| (quest.contractId && contracts.some((contract) => contract.id === quest.contractId)),
);
if (relevantSignals.length <= 0) {
return quest;
}
const relatedCarrierIds = dedupeStrings([
...(quest.relatedCarrierIds ?? []),
...relevantSignals.map((signal) => signal.carrierId ?? ''),
], 8);
const discoveredFactIds = dedupeStrings([
...(quest.discoveredFactIds ?? []),
...relevantSignals.flatMap((signal) => signal.threadIds ?? []),
], 12);
return {
...quest,
visibleStage: Math.min(
(quest.visibleStage ?? 0) + relevantSignals.length,
Math.max(1, quest.steps?.length ?? 1),
),
relatedCarrierIds,
discoveredFactIds,
};
}
export function resolveSignalsToThreadUpdates(params: {
state: GameState;
signals: StorySignal[];
contracts?: ThreadContract[] | null;
}) {
const storyEngineMemory = params.state.storyEngineMemory;
if (!storyEngineMemory || params.signals.length <= 0) {
return params.state;
}
const contracts = params.contracts ?? [];
return {
...params.state,
storyEngineMemory: {
...storyEngineMemory,
activeThreadIds: dedupeStrings([
...storyEngineMemory.activeThreadIds,
...params.signals.flatMap((signal) => signal.threadIds ?? []),
], 8),
recentSignalIds: dedupeStrings([
...(storyEngineMemory.recentSignalIds ?? []),
...params.signals.map((signal) => signal.id),
], 12),
},
quests: params.state.quests.map((quest) =>
updateQuestFromSignals(quest, params.signals, contracts),
),
};
}