1
This commit is contained in:
@@ -398,6 +398,88 @@ describe('createStoryChoiceActions', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps the deferred auto choice for the scene transition model to trigger after entry', async () => {
|
||||
const state = {
|
||||
...createBaseState(),
|
||||
inBattle: false,
|
||||
currentBattleNpcId: null,
|
||||
currentNpcBattleMode: null,
|
||||
};
|
||||
const autoChoice = createBattleOption('npc_preview_talk');
|
||||
const continueOption: StoryOption = {
|
||||
functionId: 'story_continue_adventure',
|
||||
actionText: '继续冒险',
|
||||
text: '继续冒险',
|
||||
visuals: {
|
||||
playerAnimation: AnimationState.RUN,
|
||||
playerMoveMeters: 0,
|
||||
playerOffsetY: 0,
|
||||
playerFacing: 'right' as const,
|
||||
scrollWorld: false,
|
||||
monsterChanges: [],
|
||||
},
|
||||
};
|
||||
const currentStory: StoryMoment = {
|
||||
text: '对话已经完成',
|
||||
options: [continueOption],
|
||||
deferredOptions: [autoChoice],
|
||||
deferredAutoChoice: autoChoice,
|
||||
};
|
||||
const setCurrentStory = vi.fn();
|
||||
|
||||
const { handleChoice } = createStoryChoiceActions({
|
||||
gameState: state,
|
||||
currentStory,
|
||||
isLoading: false,
|
||||
setGameState: vi.fn(),
|
||||
setCurrentStory,
|
||||
setAiError: vi.fn(),
|
||||
setIsLoading: vi.fn(),
|
||||
setBattleReward: vi.fn(),
|
||||
buildResolvedChoiceState: vi.fn(),
|
||||
playResolvedChoice: vi.fn(),
|
||||
buildStoryContextFromState: vi.fn(),
|
||||
buildStoryFromResponse: vi.fn((_, __, response) => response),
|
||||
buildFallbackStoryForState: vi.fn(() => createFallbackStory()),
|
||||
generateStoryForState: vi.fn(),
|
||||
getAvailableOptionsForState: vi.fn(() => null),
|
||||
getStoryGenerationHostileNpcs: vi.fn(() => []),
|
||||
getResolvedSceneHostileNpcs: vi.fn(
|
||||
(inputState: GameState) => inputState.sceneHostileNpcs,
|
||||
),
|
||||
buildNpcStory: vi.fn(() => createFallbackStory()),
|
||||
handleNpcBattleConversationContinuation: vi.fn(() => false),
|
||||
updateQuestLog: vi.fn((inputState: GameState) => inputState),
|
||||
incrementRuntimeStats: vi.fn((inputState: GameState) => inputState),
|
||||
getCampCompanionTravelScene: vi.fn(() => null),
|
||||
enterNpcInteraction: vi.fn(() => false),
|
||||
handleNpcInteraction: vi.fn(),
|
||||
handleTreasureInteraction: vi.fn(() => false),
|
||||
commitGeneratedStateWithEncounterEntry: vi.fn(),
|
||||
finalizeNpcBattleResult: vi.fn(() => null),
|
||||
isContinueAdventureOption: vi.fn(
|
||||
(option: StoryOption) =>
|
||||
option.functionId === 'story_continue_adventure',
|
||||
),
|
||||
isCampTravelHomeOption: vi.fn(() => false),
|
||||
isRegularNpcEncounter: neverNpcEncounter,
|
||||
isNpcEncounter: neverNpcEncounter,
|
||||
npcPreviewTalkFunctionId: 'npc_preview_talk',
|
||||
fallbackCompanionName: '同伴',
|
||||
turnVisualMs: 820,
|
||||
});
|
||||
|
||||
await handleChoice(continueOption);
|
||||
|
||||
expect(setCurrentStory).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
options: [autoChoice],
|
||||
deferredOptions: undefined,
|
||||
deferredAutoChoice: autoChoice,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps npc chat choices on the local UI path so chat mode can continue streaming locally', async () => {
|
||||
const state = createBaseState();
|
||||
const option = createBattleOption('npc_chat');
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Dispatch, SetStateAction } from 'react';
|
||||
|
||||
import { ensureSceneEncounterPreview } from '../../data/sceneEncounterPreviews';
|
||||
import type { StoryGenerationContext } from '../../services/aiTypes';
|
||||
import { isServerRuntimeFunctionId } from '../../services/rpg-runtime';
|
||||
import {
|
||||
@@ -185,8 +186,13 @@ export function createStoryChoiceActions({
|
||||
currentStory?.deferredOptions?.length &&
|
||||
isContinueAdventureOption(option)
|
||||
) {
|
||||
const deferredAutoChoice =
|
||||
currentStory.deferredAutoChoice &&
|
||||
currentStory.deferredOptions.includes(currentStory.deferredAutoChoice)
|
||||
? currentStory.deferredAutoChoice
|
||||
: undefined;
|
||||
if (currentStory.deferredRuntimeState) {
|
||||
setGameState({
|
||||
const restoredState = ensureSceneEncounterPreview({
|
||||
...gameState,
|
||||
currentEncounter: null,
|
||||
npcInteractionActive: false,
|
||||
@@ -202,12 +208,15 @@ export function createStoryChoiceActions({
|
||||
currentStory.deferredRuntimeState.storyEngineMemory ??
|
||||
gameState.storyEngineMemory,
|
||||
});
|
||||
|
||||
setGameState(restoredState);
|
||||
}
|
||||
setCurrentStory({
|
||||
...currentStory,
|
||||
options: currentStory.deferredOptions,
|
||||
deferredOptions: undefined,
|
||||
deferredRuntimeState: undefined,
|
||||
deferredAutoChoice,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import { useStoryChoiceCoordinator } from './useStoryChoiceCoordinator';
|
||||
type RpgRuntimeInteractionFlowParams = {
|
||||
gameState: GameState;
|
||||
isLoading: boolean;
|
||||
sceneTransitionPhase?: 'idle' | 'exiting' | 'entering';
|
||||
interactionConfig: StoryInteractionCoordinatorConfig;
|
||||
runtimeSupport: StoryRuntimeSupport;
|
||||
buildResolvedChoiceState: (
|
||||
@@ -76,6 +77,7 @@ export function createClearStoryInteractionUi(params: {
|
||||
export function useRpgRuntimeInteractionFlow({
|
||||
gameState,
|
||||
isLoading,
|
||||
sceneTransitionPhase = 'idle',
|
||||
interactionConfig,
|
||||
runtimeSupport,
|
||||
buildResolvedChoiceState,
|
||||
@@ -117,7 +119,15 @@ export function useRpgRuntimeInteractionFlow({
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoading || gameState.inBattle || gameState.npcInteractionActive) {
|
||||
const pendingAutoChoice =
|
||||
interactionConfig.npcEncounterActions.currentStory?.deferredAutoChoice;
|
||||
if (
|
||||
isLoading ||
|
||||
sceneTransitionPhase !== 'idle' ||
|
||||
pendingAutoChoice ||
|
||||
gameState.inBattle ||
|
||||
gameState.npcInteractionActive
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -134,8 +144,10 @@ export function useRpgRuntimeInteractionFlow({
|
||||
gameState.currentEncounter,
|
||||
gameState.inBattle,
|
||||
gameState.npcInteractionActive,
|
||||
interactionConfig.npcEncounterActions.currentStory?.deferredAutoChoice,
|
||||
isLoading,
|
||||
isNpcEncounter,
|
||||
sceneTransitionPhase,
|
||||
]);
|
||||
|
||||
const choiceRuntimeController: Parameters<
|
||||
|
||||
@@ -25,8 +25,8 @@ import type { StoryGenerationContext } from '../../services/aiTypes';
|
||||
import {
|
||||
advanceSceneActRuntimeState,
|
||||
getSceneConnectionDirectionText,
|
||||
resolveSceneActProgression,
|
||||
resolveLimitedPrimaryNpcChatState,
|
||||
resolveSceneActProgression,
|
||||
} from '../../services/customWorldSceneActRuntime';
|
||||
import { normalizeStoryEngineMemoryState } from '../../services/storyEngine/visibilityEngine';
|
||||
import type {
|
||||
@@ -1730,6 +1730,14 @@ export function createStoryNpcEncounterActions({
|
||||
deferredOptions: progressionResult?.options,
|
||||
deferredRuntimeState:
|
||||
progressionResult?.deferredRuntimeState ?? undefined,
|
||||
deferredAutoChoice:
|
||||
progressionResult?.options.find(
|
||||
(option) => option.functionId === 'npc_preview_talk',
|
||||
) ??
|
||||
progressionResult?.options.find(
|
||||
(option) => option.functionId === 'npc_chat',
|
||||
) ??
|
||||
undefined,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -56,11 +56,13 @@ export type {
|
||||
export function useRpgRuntimeStory({
|
||||
gameState,
|
||||
setGameState,
|
||||
sceneTransitionPhase = 'idle',
|
||||
buildResolvedChoiceState,
|
||||
playResolvedChoice,
|
||||
}: {
|
||||
gameState: GameState;
|
||||
setGameState: Dispatch<SetStateAction<GameState>>;
|
||||
sceneTransitionPhase?: 'idle' | 'exiting' | 'entering';
|
||||
buildResolvedChoiceState: (
|
||||
state: GameState,
|
||||
option: StoryOption,
|
||||
@@ -108,6 +110,7 @@ export function useRpgRuntimeStory({
|
||||
} = useRpgRuntimeStoryFlow({
|
||||
gameState,
|
||||
setGameState,
|
||||
sceneTransitionPhase,
|
||||
buildResolvedChoiceState,
|
||||
playResolvedChoice,
|
||||
getStoryGenerationHostileNpcs,
|
||||
|
||||
@@ -14,6 +14,7 @@ import { useStoryGoalOptionCoordinator } from './useStoryGoalOptionCoordinator';
|
||||
type RpgRuntimeStoryFlowParams = {
|
||||
gameState: GameState;
|
||||
setGameState: Dispatch<SetStateAction<GameState>>;
|
||||
sceneTransitionPhase?: 'idle' | 'exiting' | 'entering';
|
||||
buildResolvedChoiceState: (
|
||||
state: GameState,
|
||||
option: StoryOption,
|
||||
@@ -61,6 +62,7 @@ type RpgRuntimeStoryFlowParams = {
|
||||
export function useRpgRuntimeStoryFlow({
|
||||
gameState,
|
||||
setGameState,
|
||||
sceneTransitionPhase = 'idle',
|
||||
buildResolvedChoiceState,
|
||||
playResolvedChoice,
|
||||
getStoryGenerationHostileNpcs,
|
||||
@@ -148,6 +150,7 @@ export function useRpgRuntimeStoryFlow({
|
||||
} = useRpgRuntimeInteractionFlow({
|
||||
gameState,
|
||||
isLoading,
|
||||
sceneTransitionPhase,
|
||||
interactionConfig,
|
||||
runtimeSupport,
|
||||
buildResolvedChoiceState,
|
||||
|
||||
Reference in New Issue
Block a user