Files
Genarrative/src/hooks/rpg-runtime-story/choiceActions.ts
2026-04-28 19:36:39 +08:00

290 lines
8.7 KiB
TypeScript

import type {
Dispatch,
SetStateAction,
} from 'react';
import type { StoryGenerationContext } from '../../services/aiTypes';
import { isRpgRuntimeServerFunctionId } from '../../services/rpg-runtime';
import {
type Character,
type Encounter,
type GameState,
type StoryMoment,
type StoryOption,
} from '../../types';
import type { EscapePlaybackSync } from '../combat/escapeFlow';
import type { ResolvedChoiceState } from '../combat/resolvedChoice';
import type {
CommitGeneratedStateWithEncounterEntry,
GenerateStoryForState,
} from './progressionActions';
import { runLocalStoryChoiceContinuation } from './storyChoiceContinuation';
import {
runCampTravelHomeChoice,
runServerRuntimeChoiceAction,
shouldOpenLocalRuntimeNpcModal,
} from './storyChoiceRuntime';
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<GameState['currentNpcBattleMode']>;
}) => boolean;
type BuildStoryContextFromState = (
state: GameState,
extras?: {
lastFunctionId?: string | null;
observeSignsRequested?: boolean;
recentActionResult?: string | null;
},
) => StoryGenerationContext;
type UpdateQuestLog = (
state: GameState,
updater: (quests: GameState['quests']) => GameState['quests'],
) => GameState;
type IncrementRuntimeStats = (
state: GameState,
increments: RuntimeStatsIncrements,
) => GameState;
export function createStoryChoiceActions({
gameState,
currentStory,
isLoading,
setGameState,
setCurrentStory,
setAiError,
setIsLoading,
setBattleReward,
buildResolvedChoiceState,
playResolvedChoice,
buildStoryContextFromState,
buildStoryFromResponse,
buildFallbackStoryForState,
generateStoryForState,
getAvailableOptionsForState,
getStoryGenerationHostileNpcs,
getResolvedSceneHostileNpcs,
buildNpcStory,
handleNpcBattleConversationContinuation,
updateQuestLog,
incrementRuntimeStats,
getCampCompanionTravelScene,
enterNpcInteraction,
handleNpcInteraction,
handleTreasureInteraction,
commitGeneratedStateWithEncounterEntry,
finalizeNpcBattleResult,
isContinueAdventureOption,
isCampTravelHomeOption,
isRegularNpcEncounter,
isNpcEncounter,
npcPreviewTalkFunctionId,
fallbackCompanionName,
turnVisualMs,
}: {
gameState: GameState;
currentStory: StoryMoment | null;
isLoading: boolean;
setGameState: Dispatch<SetStateAction<GameState>>;
setCurrentStory: Dispatch<SetStateAction<StoryMoment | null>>;
setAiError: Dispatch<SetStateAction<string | null>>;
setIsLoading: Dispatch<SetStateAction<boolean>>;
setBattleReward: Dispatch<SetStateAction<BattleRewardSummary | null>>;
buildResolvedChoiceState: (state: GameState, option: StoryOption, character: Character) => ResolvedChoiceState;
playResolvedChoice: (
state: GameState,
option: StoryOption,
character: Character,
resolvedChoice: ResolvedChoiceState,
sync?: EscapePlaybackSync,
) => Promise<GameState>;
buildStoryContextFromState: BuildStoryContextFromState;
buildStoryFromResponse: BuildStoryFromResponse;
buildFallbackStoryForState: BuildFallbackStoryForState;
generateStoryForState: GenerateStoryForState;
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;
getCampCompanionTravelScene: (state: GameState, character: Character) => GameState['currentScenePreset'] | null;
enterNpcInteraction: (encounter: Encounter, actionText: string) => boolean;
handleNpcInteraction: (option: StoryOption) => boolean | Promise<boolean>;
handleTreasureInteraction: (
option: StoryOption,
) => void | Promise<void> | boolean | Promise<boolean>;
commitGeneratedStateWithEncounterEntry: CommitGeneratedStateWithEncounterEntry;
finalizeNpcBattleResult: (
state: GameState,
character: Character,
battleMode: NonNullable<GameState['currentNpcBattleMode']>,
battleOutcome: GameState['currentNpcBattleOutcome'],
) => { nextState: GameState; resultText: string } | null;
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;
}) {
const handleChoice = async (option: StoryOption) => {
const character = gameState.playerCharacter;
if (!gameState.worldType || !character || isLoading) return;
if (option.disabled) return;
if (currentStory?.deferredOptions?.length && isContinueAdventureOption(option)) {
if (currentStory.deferredRuntimeState) {
setGameState({
...gameState,
currentEncounter: null,
npcInteractionActive: false,
sceneHostileNpcs: [],
inBattle: false,
currentBattleNpcId: null,
currentNpcBattleMode: null,
currentNpcBattleOutcome: null,
currentScenePreset:
currentStory.deferredRuntimeState.currentScenePreset ??
gameState.currentScenePreset,
});
}
setCurrentStory({
...currentStory,
options: currentStory.deferredOptions,
deferredOptions: undefined,
deferredRuntimeState: undefined,
});
return;
}
if (isCampTravelHomeOption(option)) {
await runCampTravelHomeChoice({
gameState,
option,
character,
setBattleReward,
setAiError,
setIsLoading,
setGameState,
incrementRuntimeStats,
getCampCompanionTravelScene,
commitGeneratedStateWithEncounterEntry,
isNpcEncounter,
fallbackCompanionName,
turnVisualMs,
});
return;
}
if (shouldOpenLocalRuntimeNpcModal(option)) {
setAiError(null);
await handleNpcInteraction(option);
return;
}
if (isRpgRuntimeServerFunctionId(option.functionId)) {
await runServerRuntimeChoiceAction({
gameState,
currentStory,
option,
character,
setBattleReward,
setAiError,
setIsLoading,
setGameState,
setCurrentStory: (story) => setCurrentStory(story),
buildFallbackStoryForState,
turnVisualMs,
});
return;
}
if (
option.functionId === npcPreviewTalkFunctionId
&& isRegularNpcEncounter(gameState.currentEncounter)
&& !gameState.npcInteractionActive
) {
setAiError(null);
enterNpcInteraction(gameState.currentEncounter, option.actionText);
return;
}
if (option.interaction?.kind === 'npc') {
setAiError(null);
await handleNpcInteraction(option);
return;
}
if (option.interaction?.kind === 'treasure') {
setAiError(null);
await handleTreasureInteraction(option);
return;
}
await runLocalStoryChoiceContinuation({
gameState,
currentStory,
option,
character,
setGameState,
setCurrentStory: (story) => setCurrentStory(story),
setAiError,
setIsLoading,
setBattleReward,
buildResolvedChoiceState,
playResolvedChoice,
buildStoryContextFromState,
buildStoryFromResponse,
buildFallbackStoryForState,
generateStoryForState,
getAvailableOptionsForState,
getStoryGenerationHostileNpcs,
getResolvedSceneHostileNpcs,
buildNpcStory,
handleNpcBattleConversationContinuation,
updateQuestLog,
incrementRuntimeStats,
finalizeNpcBattleResult,
isRegularNpcEncounter,
});
};
return {
handleChoice,
};
}