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/ai'; 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, character, encounter, preparedStory, setGameState, setCurrentStory, setAiError, setIsLoading, buildDialogueStoryMoment, buildStoryContextFromState, getStoryGenerationHostileNpcs, hasRenderableDialogueTurns, inferOpeningCampFollowupOptions, getTypewriterDelay, }: { gameState: GameState; character: Character; encounter: Encounter; preparedStory: PreparedOpeningAdventure; setGameState: Dispatch>; setCurrentStory: Dispatch>; setAiError: Dispatch>; setIsLoading: Dispatch>; buildDialogueStoryMoment: ( npcName: string, text: string, options: StoryOption[], streaming?: boolean, ) => StoryMoment; buildStoryContextFromState: ( state: GameState, extras?: { lastFunctionId?: string | null }, ) => StoryGenerationContext; getStoryGenerationHostileNpcs: ( state: GameState, ) => GameState['sceneMonsters']; hasRenderableDialogueTurns: (text: string, npcName: string) => boolean; inferOpeningCampFollowupOptions: ( state: GameState, character: Character, baseOptions: StoryOption[], openingBackground: string, openingDialogue: string, ) => Promise; getTypewriterDelay: (char: string) => number; }) { const { fallbackText, openingOptions, resultText: openingBackground, } = preparedStory; const actionText = `在营地与 ${encounter.npcName} 交换开场判断`; const campScene = gameState.worldType ? getWorldCampScenePreset(gameState.worldType) : null; const entryState: GameState = { ...gameState, currentScenePreset: campScene ?? gameState.currentScenePreset, currentEncounter: { ...encounter, xMeters: encounter.xMeters ?? CALL_OUT_ENTRY_X_METERS, }, }; const resolvedEncounter: Encounter = { ...encounter, xMeters: RESOLVED_ENTITY_X_METERS, }; const storyEncounter: Encounter = { ...resolvedEncounter, specialBehavior: 'camp_companion', }; const resolvedState: GameState = { ...gameState, currentScenePreset: campScene ?? gameState.currentScenePreset, currentEncounter: resolvedEncounter, npcInteractionActive: false, }; setGameState(entryState); setAiError(null); setIsLoading(true); try { if (hasEncounterEntity(resolvedState)) { const runTicks = Math.max( 1, Math.ceil(ENCOUNTER_ENTRY_DURATION_MS / ENCOUNTER_ENTRY_TICK_MS), ); const tickDurationMs = Math.max( 1, Math.round(ENCOUNTER_ENTRY_DURATION_MS / runTicks), ); for (let tick = 1; tick <= runTicks; tick += 1) { const progress = tick / runTicks; setGameState( interpolateEncounterTransitionState( entryState, resolvedState, progress, ), ); await new Promise((resolve) => window.setTimeout(resolve, tickDurationMs), ); } } const storyState: GameState = { ...resolvedState, currentEncounter: storyEncounter, npcInteractionActive: false, }; setGameState(storyState); setCurrentStory(buildDialogueStoryMoment(encounter.npcName, '', [], true)); let openingText = fallbackText; let resolvedOpeningOptions = sortStoryOptionsByPriority(openingOptions); try { const response = await generateNextStep( gameState.worldType!, character, getStoryGenerationHostileNpcs(storyState), gameState.storyHistory, actionText, buildStoryContextFromState(storyState, { lastFunctionId: OPENING_CAMP_DIALOGUE_FUNCTION_ID, }), { availableOptions: openingOptions, }, ); const generatedText = response.storyText.trim(); if ( generatedText && hasRenderableDialogueTurns(generatedText, encounter.npcName) ) { openingText = generatedText; } if (response.options.length > 0) { resolvedOpeningOptions = sortStoryOptionsByPriority(response.options); } } catch (error) { console.error('Failed to infer opening camp dialogue:', error); setAiError(error instanceof Error ? error.message : '未知 AI 错误'); } const finalHistory = [ ...gameState.storyHistory, createHistoryMoment(actionText, 'action'), createHistoryMoment(openingText, 'result', openingOptions), ]; const finalState: GameState = { ...storyState, storyHistory: finalHistory, }; setGameState(finalState); const openingOptionsPromise = inferOpeningCampFollowupOptions( finalState, character, resolvedOpeningOptions, openingBackground, openingText, ); let displayedText = ''; for (const nextChar of openingText) { displayedText += nextChar; setCurrentStory( buildDialogueStoryMoment(encounter.npcName, displayedText, [], true), ); await new Promise((resolve) => window.setTimeout(resolve, getTypewriterDelay(nextChar)), ); } const finalOpeningOptions = await openingOptionsPromise; setCurrentStory( buildDialogueStoryMoment( encounter.npcName, openingText, finalOpeningOptions, false, ), ); } catch (error) { console.error('Failed to play opening adventure sequence:', error); setAiError(error instanceof Error ? error.message : '未知 AI 错误'); setCurrentStory( buildDialogueStoryMoment( encounter.npcName, fallbackText, openingOptions, false, ), ); } finally { setIsLoading(false); } }