import type { GameState, InventoryItem, QuestLogEntry, StorySignal, ThreadContract, } from '../../types'; function dedupeStrings(values: Array, 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), ), }; }