import { applyStoryReasoningRecovery } from '../../data/storyRecovery'; import { generateNextStep } from '../../services/aiService'; import type { StoryGenerationContext } from '../../services/aiTypes'; import { createHistoryMoment } from '../../services/storyHistory'; import type { Character, Encounter, GameState, StoryMoment, StoryOption, } from '../../types'; import type { ResolvedChoiceState } from '../combat/resolvedChoice'; import type { BattleRewardSummary } from './uiTypes'; type RuntimeStatsIncrements = Partial< Pick< GameState['runtimeStats'], 'hostileNpcsDefeated' | 'questsAccepted' | 'itemsUsed' | 'scenesTraveled' > >; type BuildFallbackStoryForState = ( state: GameState, character: Character, fallbackText?: string, ) => StoryMoment; type BuildStoryFromResponse = ( state: GameState, character: Character, response: StoryMoment, availableOptions: StoryOption[] | null, optionCatalog?: StoryOption[] | null, ) => StoryMoment; type BuildNpcStory = ( state: GameState, character: Character, encounter: Encounter, overrideText?: string, ) => StoryMoment; type HandleNpcBattleConversationContinuation = (params: { nextState: GameState; encounter: Encounter; character: Character; actionText: string; resultText: string; battleMode: NonNullable; }) => boolean; type BuildStoryContextFromState = ( state: GameState, extras?: { lastFunctionId?: string | null; observeSignsRequested?: boolean; recentActionResult?: string | null; currentStory?: StoryMoment | null; }, ) => StoryGenerationContext; type UpdateQuestLog = ( state: GameState, updater: (quests: GameState['quests']) => GameState['quests'], ) => GameState; type IncrementRuntimeStats = ( state: GameState, increments: RuntimeStatsIncrements, ) => GameState; function isBackendOwnedCombatChoice(option: StoryOption) { return ( option.functionId.startsWith('battle_') || option.functionId === 'inventory_use' ); } export async function runLocalStoryChoiceContinuation(params: { gameState: GameState; currentStory: StoryMoment | null; option: StoryOption; character: Character; setGameState: (state: GameState) => void; setCurrentStory: (story: StoryMoment) => void; setAiError: (message: string | null) => void; setIsLoading: (loading: boolean) => void; setBattleReward: (reward: BattleRewardSummary | null) => void; buildResolvedChoiceState: ( state: GameState, option: StoryOption, character: Character, ) => ResolvedChoiceState; playResolvedChoice: ( state: GameState, option: StoryOption, character: Character, resolvedChoice: ResolvedChoiceState, ) => Promise; buildStoryContextFromState: BuildStoryContextFromState; buildStoryFromResponse: BuildStoryFromResponse; buildFallbackStoryForState: BuildFallbackStoryForState; generateStoryForState: (params: { state: GameState; character: Character; history: StoryMoment[]; choice?: string; lastFunctionId?: string | null; optionCatalog?: StoryOption[] | null; }) => Promise; getAvailableOptionsForState: ( state: GameState, character: Character, ) => StoryOption[] | null; getStoryGenerationHostileNpcs: ( state: GameState, ) => GameState['sceneHostileNpcs']; getResolvedSceneHostileNpcs: ( state: GameState, ) => GameState['sceneHostileNpcs']; buildNpcStory: BuildNpcStory; handleNpcBattleConversationContinuation: HandleNpcBattleConversationContinuation; updateQuestLog: UpdateQuestLog; incrementRuntimeStats: IncrementRuntimeStats; finalizeNpcBattleResult: ( state: GameState, character: Character, battleMode: NonNullable, battleOutcome: GameState['currentNpcBattleOutcome'], ) => { nextState: GameState; resultText: string } | null; isRegularNpcEncounter: ( encounter: GameState['currentEncounter'], ) => encounter is Encounter; }) { params.setBattleReward(null); params.setAiError(null); params.setIsLoading(true); const baseChoiceState = params.isRegularNpcEncounter(params.gameState.currentEncounter) && !params.gameState.npcInteractionActive && !params.option.interaction ? { ...params.gameState, currentEncounter: null, npcInteractionActive: false, } : params.gameState; let fallbackState = baseChoiceState; try { if (isBackendOwnedCombatChoice(params.option)) { throw new Error( `战斗与物品动作必须由后端结算,禁止进入本地 continuation:${params.option.functionId}`, ); } const history = baseChoiceState.storyHistory; const resolvedChoice = params.buildResolvedChoiceState( baseChoiceState, params.option, params.character, ); if ( resolvedChoice.optionKind === 'battle' || resolvedChoice.optionKind === 'escape' ) { throw new Error( `战斗与逃脱动作必须由后端结算,禁止进入本地 continuation:${params.option.functionId}`, ); } const projectedState = resolvedChoice.afterSequence; const projectedStateWithBattleReward = projectedState; fallbackState = projectedStateWithBattleReward; const projectedAvailableOptions = params.getAvailableOptionsForState( projectedStateWithBattleReward, params.character, ); const combatResolutionContextText = null; const historyForStoryGeneration = combatResolutionContextText ? [ ...history, createHistoryMoment(params.option.actionText, 'action'), createHistoryMoment(combatResolutionContextText, 'result'), ] : history; const responsePromise = generateNextStep( params.gameState.worldType!, params.character, params.getStoryGenerationHostileNpcs(projectedStateWithBattleReward), historyForStoryGeneration, params.option.actionText, params.buildStoryContextFromState(projectedStateWithBattleReward, { lastFunctionId: params.option.functionId, observeSignsRequested: params.option.functionId === 'idle_observe_signs', recentActionResult: combatResolutionContextText, currentStory: params.currentStory, }), projectedAvailableOptions ? { availableOptions: projectedAvailableOptions } : undefined, ); const actionPromise = params.playResolvedChoice( baseChoiceState, params.option, params.character, resolvedChoice, ); const [actionResult, responseResult] = await Promise.allSettled([ actionPromise, responsePromise, ]); if (actionResult.status === 'rejected') { throw actionResult.reason; } const afterSequence = actionResult.value; fallbackState = afterSequence; if (responseResult.status === 'rejected') { throw responseResult.reason; } const response = responseResult.value!; const nextHistory = combatResolutionContextText ? [ ...historyForStoryGeneration, createHistoryMoment(response.storyText, 'result', response.options), ] : [ ...baseChoiceState.storyHistory, createHistoryMoment(params.option.actionText, 'action'), createHistoryMoment(response.storyText, 'result', response.options), ]; const nextState = params.incrementRuntimeStats( { ...afterSequence, lastObserveSignsSceneId: params.option.functionId === 'idle_observe_signs' ? (afterSequence.currentScenePreset?.id ?? null) : (afterSequence.lastObserveSignsSceneId ?? null), lastObserveSignsReport: params.option.functionId === 'idle_observe_signs' ? response.storyText : (afterSequence.lastObserveSignsReport ?? null), storyHistory: nextHistory, }, {}, ); const recoveredState = applyStoryReasoningRecovery(nextState); params.setGameState(recoveredState); params.setCurrentStory( params.buildStoryFromResponse( recoveredState, params.character, { text: response.storyText, options: response.options, }, projectedAvailableOptions, ), ); } catch (error) { console.error('Failed to get next step:', error); params.setAiError( error instanceof Error ? error.message : '未知智能生成错误', ); params.setCurrentStory( params.buildFallbackStoryForState(fallbackState, params.character), ); } finally { params.setIsLoading(false); } }