290 lines
8.7 KiB
TypeScript
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,
|
|
};
|
|
}
|