1
This commit is contained in:
@@ -6,19 +6,36 @@ vi.mock('../../services/aiService', () => ({
|
||||
|
||||
const {
|
||||
isRpgRuntimeServerFunctionIdMock,
|
||||
runServerRuntimeChoiceActionMock,
|
||||
} = vi.hoisted(() => ({
|
||||
isRpgRuntimeServerFunctionIdMock: vi.fn(() => false),
|
||||
runServerRuntimeChoiceActionMock: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../services/rpg-runtime', () => ({
|
||||
isRpgRuntimeServerFunctionId: isRpgRuntimeServerFunctionIdMock,
|
||||
}));
|
||||
|
||||
import { generateNextStep } from '../../services/aiService';
|
||||
import { getScenePresetsByWorld } from '../../data/scenePresets';
|
||||
import { AnimationState, type Character, type Encounter, type GameState, type StoryMoment, type StoryOption, WorldType } from '../../types';
|
||||
import { createStoryChoiceActions } from './choiceActions';
|
||||
|
||||
vi.mock('./storyChoiceRuntime', async () => {
|
||||
return {
|
||||
runCampTravelHomeChoice: vi.fn(),
|
||||
runServerRuntimeChoiceAction: runServerRuntimeChoiceActionMock,
|
||||
shouldOpenLocalRuntimeNpcModal: (option: StoryOption) =>
|
||||
(
|
||||
option.interaction?.kind === 'npc' ||
|
||||
!option.interaction
|
||||
) &&
|
||||
(
|
||||
option.functionId === 'npc_chat' ||
|
||||
option.functionId === 'npc_trade' ||
|
||||
option.functionId === 'npc_gift'
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
function createTestCharacter(): Character {
|
||||
return {
|
||||
id: 'test-hero',
|
||||
@@ -150,6 +167,7 @@ describe('createStoryChoiceActions', () => {
|
||||
beforeEach(() => {
|
||||
isRpgRuntimeServerFunctionIdMock.mockReset();
|
||||
isRpgRuntimeServerFunctionIdMock.mockReturnValue(false);
|
||||
runServerRuntimeChoiceActionMock.mockReset();
|
||||
});
|
||||
|
||||
it('reveals deferred adventure options when story_continue_adventure is selected', async () => {
|
||||
@@ -290,19 +308,13 @@ describe('createStoryChoiceActions', () => {
|
||||
options: [continueOption],
|
||||
deferredOptions,
|
||||
deferredRuntimeState: {
|
||||
storyEngineMemory: {
|
||||
discoveredFactIds: [],
|
||||
activeThreadIds: [],
|
||||
resolvedScarIds: [],
|
||||
recentCarrierIds: [],
|
||||
currentSceneActState: {
|
||||
sceneId: 'scene-bridge',
|
||||
chapterId: 'scene-bridge-chapter',
|
||||
currentActId: 'scene-bridge-act-2',
|
||||
currentActIndex: 1,
|
||||
completedActIds: ['scene-bridge-act-1'],
|
||||
visitedActIds: ['scene-bridge-act-1', 'scene-bridge-act-2'],
|
||||
},
|
||||
currentScenePreset: {
|
||||
id: 'scene-bridge',
|
||||
name: '断桥',
|
||||
description: '桥上雾气很重。',
|
||||
imageSrc: '/scene-bridge.png',
|
||||
treasureHints: [],
|
||||
npcs: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -355,13 +367,14 @@ describe('createStoryChoiceActions', () => {
|
||||
|
||||
expect(setGameState).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
storyEngineMemory: expect.objectContaining({
|
||||
currentSceneActState: expect.objectContaining({
|
||||
currentActId: 'scene-bridge-act-2',
|
||||
}),
|
||||
currentScenePreset: expect.objectContaining({
|
||||
id: 'scene-bridge',
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(setGameState.mock.calls[0]?.[0]).not.toHaveProperty(
|
||||
'storyEngineMemory',
|
||||
);
|
||||
expect(setCurrentStory).toHaveBeenCalledWith({
|
||||
...currentStory,
|
||||
options: deferredOptions,
|
||||
@@ -527,360 +540,7 @@ describe('createStoryChoiceActions', () => {
|
||||
expect(handleNpcInteraction).toHaveBeenCalledWith(option);
|
||||
});
|
||||
|
||||
it('uses deterministic continue option after local npc victory', async () => {
|
||||
const encounter: Encounter = {
|
||||
id: 'npc-opponent',
|
||||
kind: 'npc',
|
||||
npcName: '山道客',
|
||||
npcDescription: '拦路旧敌',
|
||||
npcAvatar: '/npc.png',
|
||||
context: '山道旧案',
|
||||
};
|
||||
const state = {
|
||||
...createBaseState(),
|
||||
currentEncounter: encounter,
|
||||
npcInteractionActive: true,
|
||||
};
|
||||
const option = createBattleOption();
|
||||
const afterSequence = {
|
||||
...state,
|
||||
inBattle: false,
|
||||
sceneHostileNpcs: [],
|
||||
currentNpcBattleOutcome: 'fight_victory' as const,
|
||||
};
|
||||
const generateStoryForState = vi.fn().mockResolvedValue(createFallbackStory('战后续写'));
|
||||
const setCurrentStory = vi.fn();
|
||||
const setGameState = vi.fn();
|
||||
const handleNpcBattleConversationContinuation = vi.fn(() => true);
|
||||
|
||||
const { handleChoice } = createStoryChoiceActions({
|
||||
gameState: state,
|
||||
currentStory: createFallbackStory(),
|
||||
isLoading: false,
|
||||
setGameState,
|
||||
setCurrentStory,
|
||||
setAiError: vi.fn(),
|
||||
setIsLoading: vi.fn(),
|
||||
setBattleReward: vi.fn(),
|
||||
buildResolvedChoiceState: vi.fn(() => ({
|
||||
optionKind: 'battle' as const,
|
||||
battlePlan: null,
|
||||
afterSequence,
|
||||
})),
|
||||
playResolvedChoice: vi.fn().mockResolvedValue(afterSequence),
|
||||
buildStoryContextFromState: vi.fn(() => ({
|
||||
playerHp: 100,
|
||||
playerMaxHp: 100,
|
||||
playerMana: 20,
|
||||
playerMaxMana: 20,
|
||||
inBattle: false,
|
||||
playerX: 0,
|
||||
playerFacing: 'right',
|
||||
playerAnimation: AnimationState.IDLE,
|
||||
skillCooldowns: {},
|
||||
})),
|
||||
buildStoryFromResponse: vi.fn((_, __, response) => response),
|
||||
buildFallbackStoryForState: vi.fn(() => createFallbackStory()),
|
||||
generateStoryForState,
|
||||
getAvailableOptionsForState: vi.fn(() => null),
|
||||
getStoryGenerationHostileNpcs: vi.fn(() => []),
|
||||
getResolvedSceneHostileNpcs: vi.fn((inputState: GameState) => inputState.sceneHostileNpcs),
|
||||
buildNpcStory: vi.fn(() => createFallbackStory()),
|
||||
handleNpcBattleConversationContinuation,
|
||||
updateQuestLog: vi.fn((inputState: GameState) => inputState),
|
||||
incrementRuntimeStats: vi.fn((inputState: GameState) => inputState),
|
||||
getCampCompanionTravelScene: vi.fn(() => null),
|
||||
enterNpcInteraction: vi.fn(() => false),
|
||||
handleNpcInteraction: vi.fn(() => false),
|
||||
handleTreasureInteraction: vi.fn(() => false),
|
||||
commitGeneratedStateWithEncounterEntry: vi.fn(),
|
||||
finalizeNpcBattleResult: vi.fn(() => ({
|
||||
nextState: {
|
||||
...afterSequence,
|
||||
currentBattleNpcId: null,
|
||||
currentNpcBattleMode: null,
|
||||
currentNpcBattleOutcome: null,
|
||||
inBattle: false,
|
||||
},
|
||||
resultText: '山道客已经败下阵来。胜利奖励:无战利品。',
|
||||
})),
|
||||
isContinueAdventureOption: vi.fn(() => false),
|
||||
isCampTravelHomeOption: vi.fn(() => false),
|
||||
isRegularNpcEncounter: neverNpcEncounter,
|
||||
isNpcEncounter: neverNpcEncounter,
|
||||
npcPreviewTalkFunctionId: 'npc_preview_talk',
|
||||
fallbackCompanionName: '同伴',
|
||||
turnVisualMs: 820,
|
||||
});
|
||||
|
||||
await handleChoice(option);
|
||||
|
||||
expect(handleNpcBattleConversationContinuation).not.toHaveBeenCalled();
|
||||
expect(setGameState).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
currentBattleNpcId: null,
|
||||
currentNpcBattleMode: null,
|
||||
currentNpcBattleOutcome: null,
|
||||
inBattle: false,
|
||||
}),
|
||||
);
|
||||
expect(generateStoryForState).not.toHaveBeenCalled();
|
||||
expect(setCurrentStory).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
text: '山道客已经败下阵来。胜利奖励:无战利品。',
|
||||
options: [
|
||||
expect.objectContaining({
|
||||
functionId: 'story_continue_adventure',
|
||||
actionText: '继续前进',
|
||||
}),
|
||||
],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps local npc defeat on the death revive chain and resets to the first scene act', async () => {
|
||||
vi.useFakeTimers();
|
||||
const firstScene = getScenePresetsByWorld(WorldType.WUXIA)[0]!;
|
||||
const state = {
|
||||
...createBaseState(),
|
||||
currentScenePreset: firstScene,
|
||||
storyEngineMemory: {
|
||||
discoveredFactIds: [],
|
||||
activeThreadIds: [],
|
||||
resolvedScarIds: [],
|
||||
recentCarrierIds: [],
|
||||
currentSceneActState: {
|
||||
sceneId: firstScene.id,
|
||||
chapterId: `${firstScene.id}-chapter`,
|
||||
currentActId: `${firstScene.id}-act-2`,
|
||||
currentActIndex: 1,
|
||||
completedActIds: [`${firstScene.id}-act-1`],
|
||||
visitedActIds: [`${firstScene.id}-act-1`, `${firstScene.id}-act-2`],
|
||||
},
|
||||
},
|
||||
currentEncounter: {
|
||||
id: 'npc-opponent',
|
||||
kind: 'npc' as const,
|
||||
npcName: '山道客',
|
||||
npcDescription: '拦路旧敌',
|
||||
npcAvatar: '/npc.png',
|
||||
context: '山道旧案',
|
||||
},
|
||||
npcInteractionActive: true,
|
||||
};
|
||||
const option = createBattleOption();
|
||||
const afterSequence = {
|
||||
...state,
|
||||
playerHp: 0,
|
||||
inBattle: false,
|
||||
sceneHostileNpcs: [],
|
||||
currentNpcBattleOutcome: 'fight_defeat' as const,
|
||||
};
|
||||
const finalizeNpcBattleResult = vi.fn(() => ({
|
||||
nextState: afterSequence,
|
||||
resultText: '不应该进入胜利结算',
|
||||
}));
|
||||
const setCurrentStory = vi.fn();
|
||||
const setGameState = vi.fn();
|
||||
const { handleChoice } = createStoryChoiceActions({
|
||||
gameState: state,
|
||||
currentStory: createFallbackStory(),
|
||||
isLoading: false,
|
||||
setGameState,
|
||||
setCurrentStory,
|
||||
setAiError: vi.fn(),
|
||||
setIsLoading: vi.fn(),
|
||||
setBattleReward: vi.fn(),
|
||||
buildResolvedChoiceState: vi.fn(() => ({
|
||||
optionKind: 'battle' as const,
|
||||
battlePlan: null,
|
||||
afterSequence,
|
||||
})),
|
||||
playResolvedChoice: vi.fn().mockResolvedValue(afterSequence),
|
||||
buildStoryContextFromState: vi.fn(() => ({
|
||||
playerHp: 0,
|
||||
playerMaxHp: 100,
|
||||
playerMana: 20,
|
||||
playerMaxMana: 20,
|
||||
inBattle: false,
|
||||
playerX: 0,
|
||||
playerFacing: 'right',
|
||||
playerAnimation: AnimationState.IDLE,
|
||||
skillCooldowns: {},
|
||||
})),
|
||||
buildStoryFromResponse: vi.fn((_, __, response) => response),
|
||||
buildFallbackStoryForState: vi.fn(() => createFallbackStory('fallback')),
|
||||
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(() => false),
|
||||
handleTreasureInteraction: vi.fn(() => false),
|
||||
commitGeneratedStateWithEncounterEntry: vi.fn(),
|
||||
finalizeNpcBattleResult,
|
||||
isContinueAdventureOption: vi.fn(() => false),
|
||||
isCampTravelHomeOption: vi.fn(() => false),
|
||||
isRegularNpcEncounter: neverNpcEncounter,
|
||||
isNpcEncounter: neverNpcEncounter,
|
||||
npcPreviewTalkFunctionId: 'npc_preview_talk',
|
||||
fallbackCompanionName: '同伴',
|
||||
turnVisualMs: 820,
|
||||
});
|
||||
|
||||
const choicePromise = handleChoice(option);
|
||||
await vi.advanceTimersByTimeAsync(3000);
|
||||
await choicePromise;
|
||||
vi.useRealTimers();
|
||||
|
||||
expect(finalizeNpcBattleResult).not.toHaveBeenCalled();
|
||||
expect(setGameState).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.objectContaining({
|
||||
playerHp: 0,
|
||||
inBattle: false,
|
||||
currentNpcBattleOutcome: 'fight_defeat',
|
||||
animationState: AnimationState.DIE,
|
||||
}),
|
||||
);
|
||||
expect(setGameState).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
expect.objectContaining({
|
||||
currentScenePreset: expect.objectContaining({
|
||||
id: firstScene.id,
|
||||
}),
|
||||
playerHp: 100,
|
||||
playerMana: 20,
|
||||
inBattle: false,
|
||||
currentNpcBattleOutcome: null,
|
||||
}),
|
||||
);
|
||||
const revivedState = setGameState.mock.calls[1]?.[0] as GameState;
|
||||
expect(revivedState.currentBattleNpcId).toBeNull();
|
||||
expect(revivedState.currentNpcBattleMode).toBeNull();
|
||||
expect(revivedState.currentNpcBattleOutcome).toBeNull();
|
||||
expect(
|
||||
revivedState.currentEncounter !== null || revivedState.sceneHostileNpcs.length > 0,
|
||||
).toBe(true);
|
||||
expect(setCurrentStory).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
text: expect.stringContaining('重新醒来'),
|
||||
}),
|
||||
);
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('settles escape locally without ai continuation', async () => {
|
||||
const mockedGenerateNextStep = vi.mocked(generateNextStep);
|
||||
|
||||
const state = {
|
||||
...createBaseState(),
|
||||
currentBattleNpcId: null,
|
||||
currentNpcBattleMode: null,
|
||||
sceneHostileNpcs: [
|
||||
{
|
||||
id: 'wolf-1',
|
||||
name: '山狼',
|
||||
action: '低伏逼近',
|
||||
description: '一头山狼',
|
||||
animation: 'idle' as const,
|
||||
xMeters: 3.2,
|
||||
yOffset: 0,
|
||||
facing: 'left' as const,
|
||||
attackRange: 1.4,
|
||||
speed: 7,
|
||||
hp: 10,
|
||||
maxHp: 10,
|
||||
renderKind: 'npc' as const,
|
||||
},
|
||||
],
|
||||
};
|
||||
const option = createBattleOption('battle_escape_breakout');
|
||||
const afterSequence = {
|
||||
...state,
|
||||
inBattle: false,
|
||||
sceneHostileNpcs: [],
|
||||
playerX: -1.2,
|
||||
};
|
||||
const setBattleReward = vi.fn();
|
||||
const setCurrentStory = vi.fn();
|
||||
const incrementRuntimeStats = vi.fn((inputState: GameState) => inputState);
|
||||
const buildStoryContextFromState = vi.fn(() => ({
|
||||
playerHp: 100,
|
||||
playerMaxHp: 100,
|
||||
playerMana: 20,
|
||||
playerMaxMana: 20,
|
||||
inBattle: false,
|
||||
playerX: -1.2,
|
||||
playerFacing: 'right' as const,
|
||||
playerAnimation: AnimationState.IDLE,
|
||||
skillCooldowns: {},
|
||||
}));
|
||||
|
||||
const { handleChoice } = createStoryChoiceActions({
|
||||
gameState: state,
|
||||
currentStory: createFallbackStory(),
|
||||
isLoading: false,
|
||||
setGameState: vi.fn(),
|
||||
setCurrentStory,
|
||||
setAiError: vi.fn(),
|
||||
setIsLoading: vi.fn(),
|
||||
setBattleReward,
|
||||
buildResolvedChoiceState: vi.fn(() => ({
|
||||
optionKind: 'escape' as const,
|
||||
battlePlan: null,
|
||||
afterSequence,
|
||||
})),
|
||||
playResolvedChoice: vi.fn().mockResolvedValue(afterSequence),
|
||||
buildStoryContextFromState,
|
||||
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,
|
||||
getCampCompanionTravelScene: vi.fn(() => null),
|
||||
enterNpcInteraction: vi.fn(() => false),
|
||||
handleNpcInteraction: vi.fn(() => false),
|
||||
handleTreasureInteraction: vi.fn(() => false),
|
||||
commitGeneratedStateWithEncounterEntry: vi.fn(),
|
||||
finalizeNpcBattleResult: vi.fn(() => null),
|
||||
isContinueAdventureOption: vi.fn(() => false),
|
||||
isCampTravelHomeOption: vi.fn(() => false),
|
||||
isRegularNpcEncounter: neverNpcEncounter,
|
||||
isNpcEncounter: neverNpcEncounter,
|
||||
npcPreviewTalkFunctionId: 'npc_preview_talk',
|
||||
fallbackCompanionName: '同伴',
|
||||
turnVisualMs: 820,
|
||||
});
|
||||
|
||||
await handleChoice(option);
|
||||
|
||||
expect(mockedGenerateNextStep).not.toHaveBeenCalled();
|
||||
expect(buildStoryContextFromState).not.toHaveBeenCalled();
|
||||
expect(setCurrentStory).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
text: '你已成功逃脱,与山狼的交战已经被甩开,对方暂时落在身后,当前不再处于战斗状态。',
|
||||
}),
|
||||
);
|
||||
expect(setBattleReward).toHaveBeenCalledTimes(1);
|
||||
expect(setBattleReward).toHaveBeenCalledWith(null);
|
||||
expect(incrementRuntimeStats).toHaveBeenCalledWith(
|
||||
expect.any(Object),
|
||||
expect.objectContaining({ hostileNpcsDefeated: 0 }),
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps battle attack and skill choices on the local combat path even if runtime server supports them', async () => {
|
||||
it('routes battle attack and skill choices to the backend resolver even while in battle', async () => {
|
||||
const state = {
|
||||
...createBaseState(),
|
||||
sceneHostileNpcs: [
|
||||
@@ -969,17 +629,20 @@ describe('createStoryChoiceActions', () => {
|
||||
|
||||
await handleChoice(option);
|
||||
|
||||
expect(buildResolvedChoiceState).toHaveBeenCalledWith(
|
||||
state,
|
||||
option,
|
||||
state.playerCharacter!,
|
||||
expect(runServerRuntimeChoiceActionMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
gameState: state,
|
||||
option,
|
||||
character: state.playerCharacter,
|
||||
}),
|
||||
);
|
||||
expect(playResolvedChoice).toHaveBeenCalled();
|
||||
expect(setGameState).toHaveBeenCalled();
|
||||
expect(setCurrentStory).toHaveBeenCalled();
|
||||
expect(buildResolvedChoiceState).not.toHaveBeenCalled();
|
||||
expect(playResolvedChoice).not.toHaveBeenCalled();
|
||||
expect(setGameState).not.toHaveBeenCalled();
|
||||
expect(setCurrentStory).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('keeps stale battle panel choices on the local combat path when combat presentation is still visible', async () => {
|
||||
it('routes stale battle panel choices to the backend resolver when combat presentation is still visible', async () => {
|
||||
const battleOption = createBattleOption('battle_attack_basic');
|
||||
const state = {
|
||||
...createBaseState(),
|
||||
@@ -1072,11 +735,80 @@ describe('createStoryChoiceActions', () => {
|
||||
|
||||
await handleChoice(battleOption);
|
||||
|
||||
expect(buildResolvedChoiceState).toHaveBeenCalledWith(
|
||||
state,
|
||||
battleOption,
|
||||
state.playerCharacter!,
|
||||
expect(runServerRuntimeChoiceActionMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
gameState: state,
|
||||
currentStory,
|
||||
option: battleOption,
|
||||
character: state.playerCharacter,
|
||||
}),
|
||||
);
|
||||
expect(playResolvedChoice).toHaveBeenCalled();
|
||||
expect(buildResolvedChoiceState).not.toHaveBeenCalled();
|
||||
expect(playResolvedChoice).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('routes inventory_use combat choices to the backend resolver', async () => {
|
||||
const state = createBaseState();
|
||||
const option: StoryOption = {
|
||||
...createBattleOption('inventory_use'),
|
||||
runtimePayload: {
|
||||
itemId: 'focus-tonic',
|
||||
},
|
||||
};
|
||||
const buildResolvedChoiceState = vi.fn();
|
||||
const playResolvedChoice = 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(() => state.sceneHostileNpcs),
|
||||
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(() => false),
|
||||
handleTreasureInteraction: vi.fn(() => false),
|
||||
commitGeneratedStateWithEncounterEntry: vi.fn(),
|
||||
finalizeNpcBattleResult: vi.fn(() => null),
|
||||
isContinueAdventureOption: vi.fn(() => false),
|
||||
isCampTravelHomeOption: vi.fn(() => false),
|
||||
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(buildResolvedChoiceState).not.toHaveBeenCalled();
|
||||
expect(playResolvedChoice).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user