This commit is contained in:
204
src/data/stateFunctions.test.ts
Normal file
204
src/data/stateFunctions.test.ts
Normal file
@@ -0,0 +1,204 @@
|
||||
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> = {},
|
||||
): 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> = {},
|
||||
): 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([]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user