Files
Genarrative/src/hooks/story/openingAdventure.ts
2026-04-19 20:33:18 +08:00

231 lines
6.3 KiB
TypeScript

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);
}
}