import { describe, expect, it, vi } from 'vitest'; import { AnimationState, type Character, type Encounter, type GameState, type StoryMoment, } from '../../types'; import { createStoryStateResolvers } from './storyEncounterState'; function createCharacter(): Character { return { id: 'hero', name: '沈行', title: '试剑客', description: '测试主角', personality: 'calm', skills: [], } as unknown as Character; } function createGameState(overrides: Partial = {}): GameState { return { worldType: null, customWorldProfile: null, playerCharacter: createCharacter(), runtimeStats: { playTimeMs: 0, lastPlayTickAt: null, hostileNpcsDefeated: 0, questsAccepted: 0, itemsUsed: 0, scenesTraveled: 0, }, currentScene: 'Story', storyHistory: [], characterChats: {}, animationState: AnimationState.IDLE, currentEncounter: null, npcInteractionActive: false, currentScenePreset: null, sceneHostileNpcs: [], playerX: 0, playerOffsetY: 0, playerFacing: 'right', playerActionMode: 'idle', scrollWorld: false, inBattle: false, playerHp: 100, playerMaxHp: 100, playerMana: 20, playerMaxMana: 20, playerSkillCooldowns: {}, activeCombatEffects: [], playerCurrency: 0, playerInventory: [], playerEquipment: { weapon: null, armor: null, relic: null, }, npcStates: {}, quests: [], roster: [], companions: [], currentBattleNpcId: null, currentNpcBattleMode: null, currentNpcBattleOutcome: null, sparReturnEncounter: null, sparPlayerHpBefore: null, sparPlayerMaxHpBefore: null, sparStoryHistoryBefore: null, ...overrides, } as GameState; } function createNpcEncounter( overrides: Partial = {}, ): Encounter { return { id: 'npc-guard', kind: 'npc', npcName: '山道客', npcDescription: '守在路口的陌生人', npcAvatar: '/npc.png', context: '山道相遇', ...overrides, } as Encounter; } describe('storyEncounterState', () => { it('uses preview talk options for regular npc encounters before formal interaction starts', () => { const character = createCharacter(); const state = createGameState({ currentEncounter: createNpcEncounter(), }); const buildNpcStory = vi.fn(); const { getAvailableOptionsForState } = createStoryStateResolvers({ buildNpcStory, }); expect(getAvailableOptionsForState(state, character)).toEqual([ expect.objectContaining({ functionId: 'npc_preview_talk', }), ]); expect(buildNpcStory).not.toHaveBeenCalled(); }); it('uses normal npc story options after the npc interaction has started', () => { const character = createCharacter(); const npcStory: StoryMoment = { text: '普通 NPC 正常对话', options: [ { functionId: 'npc_chat', actionText: '继续交谈', text: '继续交谈', visuals: { playerAnimation: AnimationState.IDLE, playerMoveMeters: 0, playerOffsetY: 0, playerFacing: 'right', scrollWorld: false, monsterChanges: [], }, }, ], }; const state = createGameState({ currentEncounter: createNpcEncounter(), npcInteractionActive: true, }); const buildNpcStory = vi.fn(() => npcStory); const { getAvailableOptionsForState } = createStoryStateResolvers({ buildNpcStory, }); expect(getAvailableOptionsForState(state, character)).toEqual( npcStory.options, ); expect(buildNpcStory).toHaveBeenCalledWith( state, character, state.currentEncounter, undefined, ); }); it('preserves explicit fallback text when the state falls back to the generic story moment', () => { const state = createGameState(); const character = createCharacter(); const { buildFallbackStoryForState } = createStoryStateResolvers({ buildNpcStory: vi.fn(), }); const story = buildFallbackStoryForState(state, character, '手动兜底文本'); expect(story.text).toBe('手动兜底文本'); expect(story.options.length).toBeGreaterThan(0); }); });