import { describe, expect, it } from 'vitest'; import { buildStateFunctionDefinitions, getExecutableFunctions, getFunctionById, resolveFunctionOption, sortStoryOptionsByPriority, } from './stateFunctions'; import { getScenePresetsByWorld, getTravelScenePreset, getWorldCampScenePreset, } from './scenePresets'; import type { FunctionAvailabilityContext } from './stateFunctions'; import type { SceneHostileNpc, StoryOption } from '../types'; import { AnimationState, WorldType } from '../types'; function createMonster( overrides: Partial = {}, ): SceneHostileNpc { return { id: 'monster-wolf', name: '山狼', action: '朝你低吼逼近', description: '一只逼近的山狼。', animation: 'idle', xMeters: 3.2, yOffset: 0, facing: 'left', attackRange: 1.4, speed: 1, hp: 18, maxHp: 18, ...overrides, }; } function createContext( overrides: Partial = {}, ): FunctionAvailabilityContext { const defaultScene = getScenePresetsByWorld(WorldType.WUXIA)[0]; if (!defaultScene) { throw new Error('Expected wuxia scene presets to exist'); } return { worldType: WorldType.WUXIA, playerCharacter: null, inBattle: false, currentSceneId: defaultScene.id, currentSceneName: defaultScene.name, monsters: [], playerHp: 80, playerMaxHp: 100, playerMana: 50, playerMaxMana: 100, ...overrides, }; } describe('stateFunctions', () => { it('builds runtime state function definitions without inactive idle_follow_clue', () => { const definitions = buildStateFunctionDefinitions(); expect(getFunctionById('idle_follow_clue', definitions)).toBeNull(); expect(getFunctionById('idle_explore_forward', definitions)?.text).toBe( '继续向前探索', ); }); it('prioritizes battle_recover_breath in high-pressure combat contexts', () => { const executableFunctions = getExecutableFunctions( createContext({ inBattle: true, monsters: [createMonster()], playerHp: 20, playerMana: 10, }), ); expect(executableFunctions[0]?.id).toBe('battle_recover_breath'); }); it('hides idle_explore_forward in the world camp scene', () => { const campScene = getWorldCampScenePreset(WorldType.WUXIA); if (!campScene) { throw new Error('Expected wuxia camp scene to exist'); } const executableIds = getExecutableFunctions( createContext({ currentSceneId: campScene.id, currentSceneName: campScene.name, }), ).map((definition) => definition.id); expect(executableIds).not.toContain('idle_explore_forward'); }); it('forces suggested action text for travel options and keeps custom battle copy', () => { const idleContext = createContext(); const travelScene = getTravelScenePreset( idleContext.worldType, idleContext.currentSceneId, ); const travelOption = resolveFunctionOption( 'idle_travel_next_scene', idleContext, '这段自定义文案会被覆盖', ); const battleOption = resolveFunctionOption( 'battle_all_in_crush', createContext({ inBattle: true, monsters: [createMonster()], }), '顶上去狠狠干一轮', ); expect(travelOption).not.toBeNull(); expect(travelOption?.actionText).toBe( travelScene ? `前往${travelScene.name}` : '前往其他场景', ); expect(battleOption?.actionText).toBe('顶上去狠狠干一轮'); }); it('sorts story options after preserving the first two model-locked entries', () => { const options: StoryOption[] = [ { functionId: 'npc_chat', actionText: '继续交谈', text: '继续交谈', visuals: { playerAnimation: AnimationState.IDLE, playerMoveMeters: 0, playerOffsetY: 0, playerFacing: 'right', scrollWorld: false, monsterChanges: [], }, }, { functionId: 'battle_probe_pressure', actionText: '稳住节奏', text: '稳住节奏', visuals: { playerAnimation: AnimationState.IDLE, playerMoveMeters: 0, playerOffsetY: 0, playerFacing: 'right', scrollWorld: false, monsterChanges: [], }, }, { functionId: 'npc_preview_talk', actionText: '和对方说话', text: '和对方说话', visuals: { playerAnimation: AnimationState.IDLE, playerMoveMeters: 0, playerOffsetY: 0, playerFacing: 'right', scrollWorld: false, monsterChanges: [], }, }, { functionId: 'camp_travel_home_scene', actionText: '离开营地', text: '离开营地', visuals: { playerAnimation: AnimationState.IDLE, playerMoveMeters: 0, playerOffsetY: 0, playerFacing: 'right', scrollWorld: false, monsterChanges: [], }, }, ]; const sortedOptions = sortStoryOptionsByPriority(options); expect(sortedOptions.map((option) => option.functionId)).toEqual([ 'npc_chat', 'battle_probe_pressure', 'camp_travel_home_scene', 'npc_preview_talk', ]); }); it('removes battle_recover_breath when combat has no living monsters', () => { const executableIds = getExecutableFunctions( createContext({ inBattle: true, monsters: [createMonster({ hp: 0 })], }), ).map((definition) => definition.id); expect(executableIds).toEqual([]); }); });