Rework story engine flow and reorganize project docs
Some checks failed
CI / verify (push) Has been cancelled
Some checks failed
CI / verify (push) Has been cancelled
This commit is contained in:
169
src/services/storyEngine/threadSignalRouter.ts
Normal file
169
src/services/storyEngine/threadSignalRouter.ts
Normal file
@@ -0,0 +1,169 @@
|
||||
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),
|
||||
),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user