231 lines
6.3 KiB
TypeScript
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);
|
|
}
|
|
}
|