1
This commit is contained in:
230
src/hooks/rpg-runtime-story/openingAdventure.ts
Normal file
230
src/hooks/rpg-runtime-story/openingAdventure.ts
Normal file
@@ -0,0 +1,230 @@
|
||||
import type { Dispatch, SetStateAction } from 'react';
|
||||
|
||||
import {
|
||||
hasEncounterEntity,
|
||||
interpolateEncounterTransitionState,
|
||||
} from '../../data/encounterTransition';
|
||||
import { STORY_OPENING_CAMP_DIALOGUE_FUNCTION } from '../../data/functionCatalog';
|
||||
import {
|
||||
CALL_OUT_ENTRY_X_METERS,
|
||||
RESOLVED_ENTITY_X_METERS,
|
||||
} from '../../data/sceneEncounterPreviews';
|
||||
import { getWorldCampScenePreset } from '../../data/scenePresets';
|
||||
import { sortStoryOptionsByPriority } from '../../data/stateFunctions';
|
||||
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';
|
||||
|
||||
const ENCOUNTER_ENTRY_DURATION_MS = 1800;
|
||||
const ENCOUNTER_ENTRY_TICK_MS = 180;
|
||||
const OPENING_CAMP_DIALOGUE_FUNCTION_ID =
|
||||
STORY_OPENING_CAMP_DIALOGUE_FUNCTION.id;
|
||||
|
||||
export type PreparedOpeningAdventure = {
|
||||
encounterKey: string;
|
||||
actionText: string;
|
||||
resultText: string;
|
||||
fallbackText: string;
|
||||
openingOptions: StoryOption[];
|
||||
};
|
||||
|
||||
export function buildPreparedOpeningAdventure({
|
||||
state,
|
||||
character,
|
||||
getNpcEncounterKey,
|
||||
appendHistory,
|
||||
buildCampCompanionOpeningOptions,
|
||||
buildCampCompanionOpeningResultText,
|
||||
buildInitialCompanionDialogueText,
|
||||
}: {
|
||||
state: GameState;
|
||||
character: Character;
|
||||
getNpcEncounterKey: (encounter: Encounter) => string;
|
||||
appendHistory: (
|
||||
state: GameState,
|
||||
actionText: string,
|
||||
resultText: string,
|
||||
) => GameState['storyHistory'];
|
||||
buildCampCompanionOpeningOptions: (
|
||||
state: GameState,
|
||||
character: Character,
|
||||
encounter: Encounter,
|
||||
) => StoryOption[];
|
||||
buildCampCompanionOpeningResultText: (
|
||||
character: Character,
|
||||
encounter: Encounter,
|
||||
worldType: GameState['worldType'],
|
||||
) => string;
|
||||
buildInitialCompanionDialogueText: (
|
||||
character: Character,
|
||||
encounter: Encounter,
|
||||
worldType: GameState['worldType'],
|
||||
) => string;
|
||||
}): PreparedOpeningAdventure | null {
|
||||
const encounter = state.currentEncounter;
|
||||
if (
|
||||
!encounter ||
|
||||
encounter.kind !== 'npc' ||
|
||||
encounter.specialBehavior !== 'initial_companion'
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const campScene = state.worldType
|
||||
? getWorldCampScenePreset(state.worldType)
|
||||
: null;
|
||||
const actionText = '开始冒险';
|
||||
const resultText = buildCampCompanionOpeningResultText(
|
||||
character,
|
||||
encounter,
|
||||
state.worldType,
|
||||
);
|
||||
const dialogueText = buildInitialCompanionDialogueText(
|
||||
character,
|
||||
encounter,
|
||||
state.worldType,
|
||||
);
|
||||
const resolvedEncounter: Encounter = {
|
||||
...encounter,
|
||||
specialBehavior: 'camp_companion',
|
||||
xMeters: RESOLVED_ENTITY_X_METERS,
|
||||
};
|
||||
const resolvedState: GameState = {
|
||||
...state,
|
||||
currentScenePreset: campScene ?? state.currentScenePreset,
|
||||
currentEncounter: resolvedEncounter,
|
||||
npcInteractionActive: false,
|
||||
};
|
||||
const nextHistory = appendHistory(state, actionText, resultText);
|
||||
const stateWithHistory: GameState = {
|
||||
...resolvedState,
|
||||
storyHistory: nextHistory,
|
||||
};
|
||||
|
||||
return {
|
||||
encounterKey: getNpcEncounterKey(encounter),
|
||||
actionText,
|
||||
resultText,
|
||||
fallbackText: dialogueText,
|
||||
openingOptions: buildCampCompanionOpeningOptions(
|
||||
stateWithHistory,
|
||||
character,
|
||||
resolvedEncounter,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
export async function playOpeningAdventureSequence({
|
||||
gameState,
|
||||
encounter,
|
||||
preparedStory,
|
||||
setGameState,
|
||||
setCurrentStory,
|
||||
setAiError,
|
||||
setIsLoading,
|
||||
}: {
|
||||
gameState: GameState;
|
||||
character: Character;
|
||||
encounter: Encounter;
|
||||
preparedStory: PreparedOpeningAdventure;
|
||||
setGameState: Dispatch<SetStateAction<GameState>>;
|
||||
setCurrentStory: Dispatch<SetStateAction<StoryMoment | null>>;
|
||||
setAiError: Dispatch<SetStateAction<string | null>>;
|
||||
setIsLoading: Dispatch<SetStateAction<boolean>>;
|
||||
buildDialogueStoryMoment: (
|
||||
npcName: string,
|
||||
text: string,
|
||||
options: StoryOption[],
|
||||
streaming?: boolean,
|
||||
) => StoryMoment;
|
||||
buildStoryContextFromState: (
|
||||
state: GameState,
|
||||
extras?: { lastFunctionId?: string | null },
|
||||
) => StoryGenerationContext;
|
||||
getStoryGenerationHostileNpcs: (
|
||||
state: GameState,
|
||||
) => GameState['sceneHostileNpcs'];
|
||||
hasRenderableDialogueTurns: (text: string, npcName: string) => boolean;
|
||||
inferOpeningCampFollowupOptions: (
|
||||
state: GameState,
|
||||
character: Character,
|
||||
baseOptions: StoryOption[],
|
||||
openingBackground: string,
|
||||
openingDialogue: string,
|
||||
) => Promise<StoryOption[]>;
|
||||
getTypewriterDelay: (char: string) => number;
|
||||
}) {
|
||||
const { fallbackText, openingOptions } = preparedStory;
|
||||
const campScene = gameState.worldType
|
||||
? getWorldCampScenePreset(gameState.worldType)
|
||||
: null;
|
||||
const storyEncounter: Encounter = {
|
||||
...encounter,
|
||||
xMeters: RESOLVED_ENTITY_X_METERS,
|
||||
specialBehavior: 'camp_companion',
|
||||
};
|
||||
const resolvedState: GameState = {
|
||||
...gameState,
|
||||
currentScenePreset: campScene ?? gameState.currentScenePreset,
|
||||
currentEncounter: storyEncounter,
|
||||
npcInteractionActive: true,
|
||||
};
|
||||
|
||||
setAiError(null);
|
||||
setIsLoading(false);
|
||||
|
||||
try {
|
||||
setGameState(resolvedState);
|
||||
setCurrentStory({
|
||||
text: fallbackText,
|
||||
options: sortStoryOptionsByPriority(openingOptions),
|
||||
displayMode: 'dialogue',
|
||||
dialogue: [
|
||||
{
|
||||
speaker: 'npc',
|
||||
speakerName: encounter.npcName,
|
||||
text: fallbackText,
|
||||
},
|
||||
],
|
||||
streaming: false,
|
||||
npcChatState: {
|
||||
npcId: storyEncounter.id ?? storyEncounter.npcName,
|
||||
npcName: storyEncounter.npcName,
|
||||
turnCount: 0,
|
||||
customInputPlaceholder: '输入你想对 TA 说的话',
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to play opening adventure sequence:', error);
|
||||
setAiError(error instanceof Error ? error.message : '未知智能生成错误');
|
||||
setGameState(resolvedState);
|
||||
setCurrentStory({
|
||||
text: fallbackText,
|
||||
options: sortStoryOptionsByPriority(openingOptions),
|
||||
displayMode: 'dialogue',
|
||||
dialogue: [
|
||||
{
|
||||
speaker: 'npc',
|
||||
speakerName: encounter.npcName,
|
||||
text: fallbackText,
|
||||
},
|
||||
],
|
||||
streaming: false,
|
||||
npcChatState: {
|
||||
npcId: storyEncounter.id ?? storyEncounter.npcName,
|
||||
npcName: storyEncounter.npcName,
|
||||
turnCount: 0,
|
||||
customInputPlaceholder: '输入你想对 TA 说的话',
|
||||
},
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user