1
This commit is contained in:
@@ -21,7 +21,6 @@ import { createStoryChoiceActions } from './choiceActions';
|
||||
|
||||
vi.mock('./storyChoiceRuntime', async () => {
|
||||
return {
|
||||
runCampTravelHomeChoice: vi.fn(),
|
||||
runServerRuntimeChoiceAction: runServerRuntimeChoiceActionMock,
|
||||
shouldOpenLocalRuntimeNpcModal: (option: StoryOption) =>
|
||||
(
|
||||
@@ -811,4 +810,97 @@ describe('createStoryChoiceActions', () => {
|
||||
expect(buildResolvedChoiceState).not.toHaveBeenCalled();
|
||||
expect(playResolvedChoice).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('routes camp_travel_home_scene to the backend resolver instead of the legacy local travel branch', async () => {
|
||||
const state = {
|
||||
...createBaseState(),
|
||||
inBattle: false,
|
||||
currentBattleNpcId: null,
|
||||
currentNpcBattleMode: null,
|
||||
currentEncounter: {
|
||||
kind: 'npc' as const,
|
||||
id: 'npc-camp',
|
||||
npcName: '营地同伴',
|
||||
npcDescription: '准备一起出发的同伴',
|
||||
npcAvatar: '伴',
|
||||
context: '营地',
|
||||
hostile: false,
|
||||
},
|
||||
npcInteractionActive: false,
|
||||
sceneHostileNpcs: [],
|
||||
currentScenePreset: {
|
||||
id: 'wuxia-border-camp',
|
||||
name: '边关营地',
|
||||
description: '营火未熄。',
|
||||
imageSrc: '',
|
||||
connectedSceneIds: ['wuxia-palace-court'],
|
||||
connections: [],
|
||||
forwardSceneId: 'wuxia-palace-court',
|
||||
treasureHints: [],
|
||||
npcs: [],
|
||||
},
|
||||
} satisfies GameState;
|
||||
const option: StoryOption = {
|
||||
...createBattleOption('camp_travel_home_scene'),
|
||||
actionText: '前往宫苑内庭',
|
||||
text: '前往宫苑内庭',
|
||||
};
|
||||
const buildResolvedChoiceState = vi.fn();
|
||||
const playResolvedChoice = vi.fn();
|
||||
const commitGeneratedStateWithEncounterEntry = vi.fn();
|
||||
|
||||
isRpgRuntimeServerFunctionIdMock.mockReturnValue(true);
|
||||
|
||||
const { handleChoice } = createStoryChoiceActions({
|
||||
gameState: state,
|
||||
currentStory: createFallbackStory('营地对话已经收束。'),
|
||||
isLoading: false,
|
||||
setGameState: vi.fn(),
|
||||
setCurrentStory: vi.fn(),
|
||||
setAiError: vi.fn(),
|
||||
setIsLoading: vi.fn(),
|
||||
setBattleReward: vi.fn(),
|
||||
buildResolvedChoiceState,
|
||||
playResolvedChoice,
|
||||
buildStoryContextFromState: vi.fn(),
|
||||
buildStoryFromResponse: vi.fn((_, __, response) => response),
|
||||
buildFallbackStoryForState: vi.fn(() => createFallbackStory()),
|
||||
generateStoryForState: vi.fn(),
|
||||
getAvailableOptionsForState: vi.fn(() => [option]),
|
||||
getStoryGenerationHostileNpcs: vi.fn(() => []),
|
||||
getResolvedSceneHostileNpcs: vi.fn(() => []),
|
||||
buildNpcStory: vi.fn(() => createFallbackStory()),
|
||||
handleNpcBattleConversationContinuation: vi.fn(() => false),
|
||||
updateQuestLog: vi.fn((inputState: GameState) => inputState),
|
||||
incrementRuntimeStats: vi.fn((inputState: GameState) => inputState),
|
||||
getCampCompanionTravelScene: vi.fn(() => {
|
||||
throw new Error('legacy camp travel resolver should not run');
|
||||
}),
|
||||
enterNpcInteraction: vi.fn(() => false),
|
||||
handleNpcInteraction: vi.fn(() => false),
|
||||
handleTreasureInteraction: vi.fn(() => false),
|
||||
commitGeneratedStateWithEncounterEntry,
|
||||
finalizeNpcBattleResult: vi.fn(() => null),
|
||||
isContinueAdventureOption: vi.fn(() => false),
|
||||
isCampTravelHomeOption: vi.fn(() => true),
|
||||
isRegularNpcEncounter: neverNpcEncounter,
|
||||
isNpcEncounter: neverNpcEncounter,
|
||||
npcPreviewTalkFunctionId: 'npc_preview_talk',
|
||||
fallbackCompanionName: '同伴',
|
||||
turnVisualMs: 820,
|
||||
});
|
||||
|
||||
await handleChoice(option);
|
||||
|
||||
expect(runServerRuntimeChoiceActionMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
gameState: state,
|
||||
option,
|
||||
character: state.playerCharacter,
|
||||
}),
|
||||
);
|
||||
expect(commitGeneratedStateWithEncounterEntry).not.toHaveBeenCalled();
|
||||
expect(buildResolvedChoiceState).not.toHaveBeenCalled();
|
||||
expect(playResolvedChoice).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -20,7 +20,6 @@ import type {
|
||||
} from './progressionActions';
|
||||
import { runLocalStoryChoiceContinuation } from './storyChoiceContinuation';
|
||||
import {
|
||||
runCampTravelHomeChoice,
|
||||
runServerRuntimeChoiceAction,
|
||||
shouldOpenLocalRuntimeNpcModal,
|
||||
} from './storyChoiceRuntime';
|
||||
@@ -99,18 +98,13 @@ export function createStoryChoiceActions({
|
||||
handleNpcBattleConversationContinuation,
|
||||
updateQuestLog,
|
||||
incrementRuntimeStats,
|
||||
getCampCompanionTravelScene,
|
||||
enterNpcInteraction,
|
||||
handleNpcInteraction,
|
||||
handleTreasureInteraction,
|
||||
commitGeneratedStateWithEncounterEntry,
|
||||
finalizeNpcBattleResult,
|
||||
isContinueAdventureOption,
|
||||
isCampTravelHomeOption,
|
||||
isRegularNpcEncounter,
|
||||
isNpcEncounter,
|
||||
npcPreviewTalkFunctionId,
|
||||
fallbackCompanionName,
|
||||
turnVisualMs,
|
||||
}: {
|
||||
gameState: GameState;
|
||||
@@ -140,13 +134,13 @@ export function createStoryChoiceActions({
|
||||
handleNpcBattleConversationContinuation: HandleNpcBattleConversationContinuation;
|
||||
updateQuestLog: UpdateQuestLog;
|
||||
incrementRuntimeStats: IncrementRuntimeStats;
|
||||
getCampCompanionTravelScene: (state: GameState, character: Character) => GameState['currentScenePreset'] | null;
|
||||
getCampCompanionTravelScene?: (state: GameState, character: Character) => GameState['currentScenePreset'] | null;
|
||||
enterNpcInteraction: (encounter: Encounter, actionText: string) => boolean;
|
||||
handleNpcInteraction: (option: StoryOption) => boolean | Promise<boolean>;
|
||||
handleTreasureInteraction: (
|
||||
option: StoryOption,
|
||||
) => void | Promise<void> | boolean | Promise<boolean>;
|
||||
commitGeneratedStateWithEncounterEntry: CommitGeneratedStateWithEncounterEntry;
|
||||
commitGeneratedStateWithEncounterEntry?: CommitGeneratedStateWithEncounterEntry;
|
||||
finalizeNpcBattleResult: (
|
||||
state: GameState,
|
||||
character: Character,
|
||||
@@ -154,11 +148,11 @@ export function createStoryChoiceActions({
|
||||
battleOutcome: GameState['currentNpcBattleOutcome'],
|
||||
) => { nextState: GameState; resultText: string } | null;
|
||||
isContinueAdventureOption: (option: StoryOption) => boolean;
|
||||
isCampTravelHomeOption: (option: StoryOption) => boolean;
|
||||
isCampTravelHomeOption?: (option: StoryOption) => boolean;
|
||||
isRegularNpcEncounter: (encounter: GameState['currentEncounter']) => encounter is Encounter;
|
||||
isNpcEncounter: (encounter: GameState['currentEncounter']) => encounter is Encounter;
|
||||
isNpcEncounter?: (encounter: GameState['currentEncounter']) => encounter is Encounter;
|
||||
npcPreviewTalkFunctionId: string;
|
||||
fallbackCompanionName: string;
|
||||
fallbackCompanionName?: string;
|
||||
turnVisualMs: number;
|
||||
}) {
|
||||
const handleChoice = async (option: StoryOption) => {
|
||||
@@ -191,25 +185,6 @@ export function createStoryChoiceActions({
|
||||
return;
|
||||
}
|
||||
|
||||
if (isCampTravelHomeOption(option)) {
|
||||
await runCampTravelHomeChoice({
|
||||
gameState,
|
||||
option,
|
||||
character,
|
||||
setBattleReward,
|
||||
setAiError,
|
||||
setIsLoading,
|
||||
setGameState,
|
||||
incrementRuntimeStats,
|
||||
getCampCompanionTravelScene,
|
||||
commitGeneratedStateWithEncounterEntry,
|
||||
isNpcEncounter,
|
||||
fallbackCompanionName,
|
||||
turnVisualMs,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (shouldOpenLocalRuntimeNpcModal(option)) {
|
||||
setAiError(null);
|
||||
await handleNpcInteraction(option);
|
||||
|
||||
@@ -1,16 +1,6 @@
|
||||
import {
|
||||
buildEncounterEntryState,
|
||||
hasEncounterEntity,
|
||||
} from '../../data/encounterTransition';
|
||||
import {
|
||||
CALL_OUT_ENTRY_X_METERS,
|
||||
createSceneEncounterPreview,
|
||||
resolveSceneEncounterPreview,
|
||||
} from '../../data/sceneEncounterPreviews';
|
||||
import {
|
||||
AnimationState,
|
||||
Character,
|
||||
Encounter,
|
||||
GameState,
|
||||
StoryMoment,
|
||||
StoryOption,
|
||||
@@ -18,24 +8,12 @@ import {
|
||||
import { resolveRpgRuntimeChoice } from '.';
|
||||
import type { BattleRewardSummary } from './uiTypes';
|
||||
|
||||
type RuntimeStatsIncrements = Partial<
|
||||
Pick<
|
||||
GameState['runtimeStats'],
|
||||
'hostileNpcsDefeated' | 'questsAccepted' | 'itemsUsed' | 'scenesTraveled'
|
||||
>
|
||||
>;
|
||||
|
||||
type BuildFallbackStoryForState = (
|
||||
state: GameState,
|
||||
character: Character,
|
||||
fallbackText?: string,
|
||||
) => StoryMoment;
|
||||
|
||||
type IncrementRuntimeStats = (
|
||||
state: GameState,
|
||||
increments: RuntimeStatsIncrements,
|
||||
) => GameState;
|
||||
|
||||
function sleep(ms: number) {
|
||||
return new Promise((resolve) => globalThis.setTimeout(resolve, ms));
|
||||
}
|
||||
@@ -54,126 +32,6 @@ export function shouldOpenLocalRuntimeNpcModal(option: StoryOption) {
|
||||
);
|
||||
}
|
||||
|
||||
export async function runCampTravelHomeChoice(params: {
|
||||
gameState: GameState;
|
||||
option: StoryOption;
|
||||
character: Character;
|
||||
setBattleReward: (reward: BattleRewardSummary | null) => void;
|
||||
setAiError: (message: string | null) => void;
|
||||
setIsLoading: (loading: boolean) => void;
|
||||
setGameState: (state: GameState) => void;
|
||||
incrementRuntimeStats: IncrementRuntimeStats;
|
||||
getCampCompanionTravelScene: (
|
||||
state: GameState,
|
||||
character: Character,
|
||||
) => GameState['currentScenePreset'] | null;
|
||||
commitGeneratedStateWithEncounterEntry: (
|
||||
entryState: GameState,
|
||||
resolvedState: GameState,
|
||||
character: Character,
|
||||
actionText: string,
|
||||
resultText: string,
|
||||
lastFunctionId?: string,
|
||||
) => Promise<void> | void;
|
||||
isNpcEncounter: (
|
||||
encounter: GameState['currentEncounter'],
|
||||
) => encounter is Encounter;
|
||||
fallbackCompanionName: string;
|
||||
turnVisualMs: number;
|
||||
}) {
|
||||
const targetScene = params.getCampCompanionTravelScene(
|
||||
params.gameState,
|
||||
params.character,
|
||||
);
|
||||
if (!targetScene) {
|
||||
return false;
|
||||
}
|
||||
|
||||
params.setBattleReward(null);
|
||||
params.setAiError(null);
|
||||
|
||||
const companionName = params.isNpcEncounter(params.gameState.currentEncounter)
|
||||
? params.gameState.currentEncounter.npcName
|
||||
: params.fallbackCompanionName;
|
||||
const travelRunState: GameState = {
|
||||
...params.gameState,
|
||||
ambientIdleMode: undefined,
|
||||
currentEncounter: null,
|
||||
npcInteractionActive: false,
|
||||
sceneHostileNpcs: [],
|
||||
playerX: 0,
|
||||
playerFacing: 'right' as const,
|
||||
animationState: AnimationState.RUN,
|
||||
playerActionMode: 'idle' as const,
|
||||
activeCombatEffects: [],
|
||||
scrollWorld: true,
|
||||
inBattle: false,
|
||||
lastObserveSignsSceneId: null,
|
||||
lastObserveSignsReport: null,
|
||||
currentBattleNpcId: null,
|
||||
currentNpcBattleMode: null,
|
||||
currentNpcBattleOutcome: null,
|
||||
sparReturnEncounter: null,
|
||||
sparPlayerHpBefore: null,
|
||||
sparPlayerMaxHpBefore: null,
|
||||
sparStoryHistoryBefore: null,
|
||||
};
|
||||
const travelBaseState: GameState = params.incrementRuntimeStats(
|
||||
{
|
||||
...params.gameState,
|
||||
ambientIdleMode: undefined,
|
||||
currentScenePreset: targetScene,
|
||||
currentEncounter: null,
|
||||
npcInteractionActive: false,
|
||||
sceneHostileNpcs: [],
|
||||
playerX: 0,
|
||||
playerFacing: 'right' as const,
|
||||
animationState: AnimationState.IDLE,
|
||||
playerActionMode: 'idle' as const,
|
||||
activeCombatEffects: [],
|
||||
scrollWorld: false,
|
||||
inBattle: false,
|
||||
lastObserveSignsSceneId: null,
|
||||
lastObserveSignsReport: null,
|
||||
currentBattleNpcId: null,
|
||||
currentNpcBattleMode: null,
|
||||
currentNpcBattleOutcome: null,
|
||||
sparReturnEncounter: null,
|
||||
sparPlayerHpBefore: null,
|
||||
sparPlayerMaxHpBefore: null,
|
||||
sparStoryHistoryBefore: null,
|
||||
},
|
||||
{
|
||||
scenesTraveled: 1,
|
||||
},
|
||||
);
|
||||
const travelPreviewState: GameState = {
|
||||
...travelBaseState,
|
||||
...createSceneEncounterPreview(travelBaseState),
|
||||
};
|
||||
const resolvedState = hasEncounterEntity(travelPreviewState)
|
||||
? resolveSceneEncounterPreview(travelPreviewState)
|
||||
: travelBaseState;
|
||||
const entryState = buildEncounterEntryState(
|
||||
resolvedState,
|
||||
CALL_OUT_ENTRY_X_METERS,
|
||||
);
|
||||
|
||||
params.setIsLoading(true);
|
||||
params.setGameState(travelRunState);
|
||||
await sleep(params.turnVisualMs);
|
||||
|
||||
await params.commitGeneratedStateWithEncounterEntry(
|
||||
entryState,
|
||||
resolvedState,
|
||||
params.character,
|
||||
params.option.actionText,
|
||||
`You and ${companionName} leave camp and formally step into ${targetScene.name} to begin the adventure.`,
|
||||
params.option.functionId,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function runServerRuntimeChoiceAction(params: {
|
||||
gameState: GameState;
|
||||
currentStory: StoryMoment | null;
|
||||
|
||||
Reference in New Issue
Block a user