This commit is contained in:
2026-04-27 22:50:18 +08:00
parent ded6f6ee2a
commit b6c6640548
77 changed files with 5240 additions and 833 deletions

View File

@@ -738,4 +738,204 @@ describe('createStoryChoiceActions', () => {
expect.objectContaining({ hostileNpcsDefeated: 0 }),
);
});
it('keeps battle attack and skill choices on the local combat path even if runtime server supports them', async () => {
const state = {
...createBaseState(),
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: StoryOption = {
...createBattleOption('battle_use_skill'),
runtimePayload: {
skillId: 'skill-basic',
},
};
const setGameState = vi.fn();
const setCurrentStory = vi.fn();
const buildResolvedChoiceState = vi.fn(() => ({
optionKind: 'battle' as const,
battlePlan: null,
afterSequence: state,
}));
const playResolvedChoice = vi.fn().mockResolvedValue(state);
isRpgRuntimeServerFunctionIdMock.mockReturnValue(true);
const { handleChoice } = createStoryChoiceActions({
gameState: state,
currentStory: createFallbackStory(),
isLoading: false,
setGameState,
setCurrentStory,
setAiError: vi.fn(),
setIsLoading: vi.fn(),
setBattleReward: vi.fn(),
buildResolvedChoiceState,
playResolvedChoice,
buildStoryContextFromState: vi.fn(() => ({
playerHp: 100,
playerMaxHp: 100,
playerMana: 20,
playerMaxMana: 20,
inBattle: true,
playerX: 0,
playerFacing: 'right' as const,
playerAnimation: AnimationState.IDLE,
skillCooldowns: {},
})),
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(buildResolvedChoiceState).toHaveBeenCalledWith(
state,
option,
state.playerCharacter!,
);
expect(playResolvedChoice).toHaveBeenCalled();
expect(setGameState).toHaveBeenCalled();
expect(setCurrentStory).toHaveBeenCalled();
});
it('keeps stale battle panel choices on the local combat path when combat presentation is still visible', async () => {
const battleOption = createBattleOption('battle_attack_basic');
const state = {
...createBaseState(),
inBattle: false,
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 currentStory: StoryMoment = {
text: '山狼还在你面前压低身位,战斗并未真正结束。',
options: [battleOption],
};
const buildResolvedChoiceState = vi.fn(() => ({
optionKind: 'battle' as const,
battlePlan: null,
afterSequence: {
...state,
inBattle: true,
},
}));
const playResolvedChoice = vi.fn().mockResolvedValue({
...state,
inBattle: true,
});
isRpgRuntimeServerFunctionIdMock.mockReturnValue(true);
const { handleChoice } = createStoryChoiceActions({
gameState: state,
currentStory,
isLoading: false,
setGameState: vi.fn(),
setCurrentStory: vi.fn(),
setAiError: vi.fn(),
setIsLoading: vi.fn(),
setBattleReward: vi.fn(),
buildResolvedChoiceState,
playResolvedChoice,
buildStoryContextFromState: vi.fn(() => ({
playerHp: 100,
playerMaxHp: 100,
playerMana: 20,
playerMaxMana: 20,
inBattle: true,
playerX: 0,
playerFacing: 'right' as const,
playerAnimation: AnimationState.IDLE,
skillCooldowns: {},
})),
buildStoryFromResponse: vi.fn((_, __, response) => response),
buildFallbackStoryForState: vi.fn(() => createFallbackStory()),
generateStoryForState: vi.fn(),
getAvailableOptionsForState: vi.fn(() => [battleOption]),
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(battleOption);
expect(buildResolvedChoiceState).toHaveBeenCalledWith(
state,
battleOption,
state.playerCharacter!,
);
expect(playResolvedChoice).toHaveBeenCalled();
});
});