import {beforeEach, describe, expect, it, vi} from 'vitest'; import {getCharacterById} from '../data/characterPresets'; import {AnimationState, type Character, type GameState,WorldType} from '../types'; import {buildCompanionRenderStatesForGameState} from './useNpcInteractionFlow'; vi.mock('../data/characterPresets', () => ({ getCharacterById: vi.fn(), })); function createTestCharacter(id: string, name: string): Character { return { id, name, title: '测试同伴', description: '用于测试的角色', backstory: '测试背景', avatar: '/test-avatar.png', portrait: '/test-portrait.png', assetFolder: 'test-character', assetVariant: 'default', attributes: { strength: 10, agility: 10, intelligence: 10, spirit: 10, }, personality: 'steady', skills: [ { id: 'basic-strike', name: '试探一击', animation: AnimationState.ATTACK, damage: 10, manaCost: 0, cooldownTurns: 0, range: 1, style: 'steady', }, ], adventureOpenings: {}, }; } function createBaseState(): GameState { return { worldType: WorldType.WUXIA, customWorldProfile: null, playerCharacter: createTestCharacter('player', '主角'), 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, }; } describe('buildCompanionRenderStatesForGameState', () => { beforeEach(() => { vi.mocked(getCharacterById).mockReset(); }); it('builds render states from the provided transition snapshot', () => { const companionCharacter = createTestCharacter('companion-a', '阿青'); vi.mocked(getCharacterById).mockImplementation((characterId: string) => { if (characterId === companionCharacter.id || characterId === 'player') { return companionCharacter; } return null; }); const transitionSnapshot: GameState = { ...createBaseState(), playerFacing: 'left', animationState: AnimationState.ATTACK, companions: [ { npcId: 'npc-aqing', characterId: companionCharacter.id, joinedAtAffinity: 10, hp: 36, maxHp: 48, mana: 12, maxMana: 18, skillCooldowns: {basicStrike: 1}, offsetX: 14, offsetY: -6, transitionMs: 90, }, ], }; const renderStates = buildCompanionRenderStatesForGameState({ gameState: transitionSnapshot, presentationByNpcId: { 'npc-aqing': { animationState: AnimationState.ACQUIRE, entryOffsetX: 28, entryOffsetY: 12, transitionMs: 240, recruitToken: 42, }, }, observeFacingByNpcId: { 'npc-aqing': 'right', }, }); expect(renderStates).toHaveLength(1); expect(renderStates[0]).toMatchObject({ npcId: 'npc-aqing', character: companionCharacter, hp: 36, maxHp: 48, mana: 12, maxMana: 18, animationState: AnimationState.ACQUIRE, slot: 'upper', facing: 'right', entryOffsetX: 42, entryOffsetY: 6, transitionMs: 240, recruitToken: 42, }); }); it('lets callers render a visible snapshot even if the live state already changed', () => { const companionCharacter = createTestCharacter('companion-b', '小舟'); vi.mocked(getCharacterById).mockImplementation((characterId: string) => { if (characterId === companionCharacter.id || characterId === 'player') { return companionCharacter; } return null; }); const visibleSnapshot: GameState = { ...createBaseState(), scrollWorld: true, companions: [ { npcId: 'npc-xiaozhou', characterId: companionCharacter.id, joinedAtAffinity: 18, hp: 30, maxHp: 30, mana: 9, maxMana: 12, skillCooldowns: {}, }, ], }; const liveState: GameState = { ...createBaseState(), companions: [], }; const visibleRenderStates = buildCompanionRenderStatesForGameState({ gameState: visibleSnapshot, }); const liveRenderStates = buildCompanionRenderStatesForGameState({ gameState: liveState, }); expect(visibleRenderStates).toHaveLength(1); expect(visibleRenderStates[0]?.animationState).toBe(AnimationState.RUN); expect(liveRenderStates).toHaveLength(0); }); });