import { useCallback, useEffect } from 'react'; import type { Character, Encounter, GameState, StoryMoment, StoryOption, } from '../../types'; import type { EscapePlaybackSync as ResolvedChoicePlaybackSync } from '../combat/escapeFlow'; import type { ResolvedChoiceState } from '../combat/resolvedChoice'; import { useTreasureFlow } from '../useTreasureFlow'; import { useStoryInventoryActions } from './inventoryActions'; import { useStoryNpcInteractionFlow } from './npcInteraction'; import type { ChoiceRuntimeController, ChoiceRuntimeSupport, StoryChoiceCoordinatorParams, } from './storyChoiceCoordinator'; import type { StoryInteractionCoordinatorConfig } from './storyInteractionCoordinator'; import type { StoryRuntimeSupport } from './storyRuntimeSupport'; import { createRpgRuntimeNpcEncounterActions } from './useRpgRuntimeNpcInteraction'; import { useStoryChoiceCoordinator } from './useStoryChoiceCoordinator'; type RpgRuntimeInteractionFlowParams = { gameState: GameState; isLoading: boolean; interactionConfig: StoryInteractionCoordinatorConfig; runtimeSupport: StoryRuntimeSupport; buildResolvedChoiceState: ( state: GameState, option: StoryOption, character: Character, ) => ResolvedChoiceState; playResolvedChoice: ( state: GameState, option: StoryOption, character: Character, resolvedChoice: ResolvedChoiceState, sync?: ResolvedChoicePlaybackSync, ) => Promise; buildStoryFromResponse: ChoiceRuntimeController['buildStoryFromResponse']; getResolvedSceneHostileNpcs: ( state: GameState, ) => GameState['sceneHostileNpcs']; getCampCompanionTravelScene: StoryChoiceCoordinatorParams['runtimeController']['getCampCompanionTravelScene']; isContinueAdventureOption: (option: StoryOption) => boolean; isCampTravelHomeOption: (option: StoryOption) => boolean; isRegularNpcEncounter: ( encounter: GameState['currentEncounter'], ) => encounter is Encounter; isNpcEncounter: ( encounter: GameState['currentEncounter'], ) => encounter is Encounter; npcPreviewTalkFunctionId: string; fallbackCompanionName: string; turnVisualMs: number; }; export function createClearStoryInteractionUi(params: { clearStoryChoiceUi: () => void; clearNpcInteractionUi: () => void; }) { return () => { // 中文注释:story 选择面板和 NPC 交互面板是两套独立 UI; // 清理运行时交互态时必须同时重置,避免战斗/对话切换后残留旧弹层。 params.clearStoryChoiceUi(); params.clearNpcInteractionUi(); }; } /** * RPG runtime 交互分发层。 * 统一串起宝箱、背包、NPC 交互与 story choice 的正式分发。 */ export function useRpgRuntimeInteractionFlow({ gameState, isLoading, interactionConfig, runtimeSupport, buildResolvedChoiceState, playResolvedChoice, buildStoryFromResponse, getResolvedSceneHostileNpcs, getCampCompanionTravelScene, isContinueAdventureOption, isCampTravelHomeOption, isRegularNpcEncounter, isNpcEncounter, npcPreviewTalkFunctionId, fallbackCompanionName, turnVisualMs, }: RpgRuntimeInteractionFlowParams) { const { handleTreasureInteraction } = useTreasureFlow( interactionConfig.treasureFlow, ); const { inventoryUi } = useStoryInventoryActions( interactionConfig.inventoryFlow, ); const npcInteractionFlow = useStoryNpcInteractionFlow( interactionConfig.npcInteractionFlow, ); const { enterNpcInteraction, handleNpcInteraction, finalizeNpcBattleResult, reopenNpcChatAfterBattle, handleNpcChatTurn, exitNpcChat, replacePendingNpcQuestOffer, abandonPendingNpcQuestOffer, acceptPendingNpcQuestOffer, } = createRpgRuntimeNpcEncounterActions({ ...interactionConfig.npcEncounterActions, buildNpcStory: runtimeSupport.buildNpcStory, npcInteractionFlow, }); useEffect(() => { if (isLoading || gameState.inBattle || gameState.npcInteractionActive) { return; } if (isNpcEncounter(gameState.currentEncounter)) { // 中文注释:当场景里已经解析出 NPC 遭遇,且当前不在战斗/加载中时, // 自动进入 NPC 交互态,让开场相遇和旅行后遭遇都能无缝落到对话/互动面板。 enterNpcInteraction( gameState.currentEncounter, `与${gameState.currentEncounter.npcName}搭话`, ); } }, [ enterNpcInteraction, gameState.currentEncounter, gameState.inBattle, gameState.npcInteractionActive, isLoading, isNpcEncounter, ]); const choiceRuntimeController: Parameters< typeof useStoryChoiceCoordinator >[0]['runtimeController'] = { currentStory: interactionConfig.npcEncounterActions.currentStory, buildStoryContextFromState: interactionConfig.npcEncounterActions.buildStoryContextFromState, buildStoryFromResponse: ( state: GameState, character: Character, response: StoryMoment, availableOptions: StoryOption[] | null, optionCatalog?: StoryOption[] | null, ) => buildStoryFromResponse( state, character, response, availableOptions, optionCatalog, ), buildFallbackStoryForState: interactionConfig.npcEncounterActions.buildFallbackStoryForState, generateStoryForState: async (params) => interactionConfig.npcEncounterActions.generateStoryForState(params), getAvailableOptionsForState: interactionConfig.npcEncounterActions.getAvailableOptionsForState, getCampCompanionTravelScene: (state, character) => getCampCompanionTravelScene(state, character), commitGeneratedStateWithEncounterEntry: async ( entryState, resolvedState, character, actionText, resultText, lastFunctionId, ) => { await interactionConfig.npcEncounterActions.commitGeneratedStateWithEncounterEntry( entryState, resolvedState, character, actionText, resultText, lastFunctionId, ); }, }; // 中文注释:choice coordinator 只关心“点下某个 story option 后怎么结算”, // NPC 战斗结束后要不要回到对话态,则通过 runtimeSupport 在这里桥接进去。 const choiceRuntimeSupport: ChoiceRuntimeSupport = { ...runtimeSupport, handleNpcBattleConversationContinuation: ({ nextState, encounter, actionText, resultText, battleMode, }) => reopenNpcChatAfterBattle({ nextState, encounter, actionText, resultText, battleMode, }), }; const { handleChoice, battleRewardUi, clearStoryChoiceUi } = useStoryChoiceCoordinator({ gameState, isLoading, setGameState: interactionConfig.npcEncounterActions.setGameState, setCurrentStory: interactionConfig.npcEncounterActions.setCurrentStory, setAiError: interactionConfig.npcEncounterActions.setAiError, setIsLoading: interactionConfig.npcEncounterActions.setIsLoading, buildResolvedChoiceState, playResolvedChoice, getStoryGenerationHostileNpcs: interactionConfig.npcEncounterActions.getStoryGenerationHostileNpcs, getResolvedSceneHostileNpcs, runtimeController: choiceRuntimeController, runtimeSupport: choiceRuntimeSupport, enterNpcInteraction, handleNpcInteraction, handleTreasureInteraction, finalizeNpcBattleResult, sortOptions: interactionConfig.npcEncounterActions.sortOptions, buildContinueAdventureOption: interactionConfig.npcEncounterActions.buildContinueAdventureOption, isContinueAdventureOption, isCampTravelHomeOption, isRegularNpcEncounter, isNpcEncounter, npcPreviewTalkFunctionId, fallbackCompanionName, turnVisualMs, }); const clearStoryInteractionUi = useCallback( createClearStoryInteractionUi({ clearStoryChoiceUi, clearNpcInteractionUi: npcInteractionFlow.clearNpcInteractionUi, }), [clearStoryChoiceUi, npcInteractionFlow.clearNpcInteractionUi], ); return { handleChoice, battleRewardUi, npcUi: npcInteractionFlow.npcUi, inventoryUi, clearStoryInteractionUi, handleNpcChatInput: (input: string) => { const encounter = interactionConfig.npcEncounterActions.gameState.currentEncounter; if (!encounter || encounter.kind !== 'npc') { return false; } // 中文注释:聊天提交是 fire-and-forget, // 调用方只需要知道“当前能不能发给 NPC”,不需要阻塞等待整轮对话结束。 void handleNpcChatTurn(encounter, input); return true; }, refreshNpcChatOptions: () => { const story = interactionConfig.npcEncounterActions.currentStory; const encounter = interactionConfig.npcEncounterActions.gameState.currentEncounter; if (!story?.npcChatState || !story.options.length || !encounter || encounter.kind !== 'npc') { return false; } const [firstOption, ...restOptions] = story.options; if (!firstOption || restOptions.length === 0) { return false; } // 中文注释:NPC 聊天的“换一组回应建议”当前通过轮转 options 实现, // 不额外发请求,优先复用本轮已经拿到的候选动作。 interactionConfig.npcEncounterActions.setCurrentStory({ ...story, options: [...restOptions, firstOption], }); return true; }, exitNpcChat, npcChatQuestOfferUi: { replacePendingOffer: replacePendingNpcQuestOffer, abandonPendingOffer: abandonPendingNpcQuestOffer, acceptPendingOffer: acceptPendingNpcQuestOffer, }, }; } export type UseRpgRuntimeInteractionFlowParams = Parameters< typeof useRpgRuntimeInteractionFlow >[0]; export type RpgRuntimeInteractionFlowResult = ReturnType< typeof useRpgRuntimeInteractionFlow >; export const createRpgRuntimeInteractionUiResetter = createClearStoryInteractionUi;