This commit is contained in:
289
src/hooks/rpg-runtime-story/useRpgRuntimeInteractionFlow.ts
Normal file
289
src/hooks/rpg-runtime-story/useRpgRuntimeInteractionFlow.ts
Normal file
@@ -0,0 +1,289 @@
|
||||
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<GameState>;
|
||||
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 () => {
|
||||
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)) {
|
||||
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,
|
||||
);
|
||||
},
|
||||
};
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
Reference in New Issue
Block a user