Files
Genarrative/src/data/stateFunctions.test.ts
kdletters cbc27bad4a
Some checks failed
CI / verify (push) Has been cancelled
init with react+axum+spacetimedb
2026-04-26 18:06:23 +08:00

205 lines
5.6 KiB
TypeScript

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([]);
});
});