Files
esengine/packages/tools/demos/src/fsm.demo.ts
yhh d66c18041e fix(spatial): 修复 GridAOI 可见性更新问题
- 修复 addObserver 时现有观察者无法检测到新实体的问题
- 修复实体远距离移动时观察者可见性未正确更新的问题
- 重构 demos 抽取公共测试工具
2025-12-26 22:23:03 +08:00

164 lines
5.2 KiB
TypeScript

/**
* FSM Module Demo - Tests APIs from docs/modules/fsm/index.md
*/
import { createStateMachine } from '@esengine/fsm';
import { assert, section, demoHeader, demoFooter } from './utils.js';
type PlayerState = 'idle' | 'walk' | 'run' | 'jump';
export async function runFSMDemo(): Promise<void> {
demoHeader('FSM Module Demo');
// 1. Basic Creation
section('1. createStateMachine()');
const fsm = createStateMachine<PlayerState>('idle');
assert(fsm !== null, 'State machine created');
assert(fsm.current === 'idle', 'Initial state is idle');
// 2. Define States
section('2. defineState()');
let enterCalled = false;
let exitCalled = false;
let updateCalled = false;
fsm.defineState('idle', {
onEnter: () => { enterCalled = true; },
onExit: () => { exitCalled = true; },
onUpdate: () => { updateCalled = true; }
});
fsm.defineState('walk', {});
fsm.defineState('run', {});
fsm.defineState('jump', {});
assert(fsm.hasState('idle'), 'hasState() returns true');
assert(fsm.hasState('walk'), 'walk state exists');
// 3. Manual Transition
section('3. transition()');
fsm.transition('walk');
assert(fsm.current === 'walk', 'Transitioned to walk');
assert(exitCalled, 'onExit called on idle');
assert(fsm.previous === 'idle', 'previous is idle');
// 4. State with Context
section('4. Context Support');
interface Context {
speed: number;
isMoving: boolean;
}
const fsmCtx = createStateMachine<PlayerState, Context>('idle', {
context: { speed: 0, isMoving: false }
});
fsmCtx.defineState('idle', {
onEnter: (ctx) => { ctx.speed = 0; }
});
fsmCtx.defineState('walk', {
onEnter: (ctx) => { ctx.speed = 100; }
});
fsmCtx.transition('walk');
assert(fsmCtx.context.speed === 100, 'Context updated on enter');
// 5. Transition Conditions
section('5. defineTransition() with conditions');
const fsmTrans = createStateMachine<PlayerState, Context>('idle', {
context: { speed: 0, isMoving: false }
});
fsmTrans.defineState('idle', {});
fsmTrans.defineState('walk', {});
fsmTrans.defineTransition('idle', 'walk', (ctx) => ctx.isMoving);
fsmTrans.evaluateTransitions();
assert(fsmTrans.current === 'idle', 'No transition when condition false');
fsmTrans.context.isMoving = true;
fsmTrans.evaluateTransitions();
assert(fsmTrans.current === 'walk', 'Transitions when condition true');
// 6. Transition Priority
section('6. Transition Priority');
const fsmPri = createStateMachine<'a' | 'b' | 'c'>('a');
fsmPri.defineState('a', {});
fsmPri.defineState('b', {});
fsmPri.defineState('c', {});
fsmPri.defineTransition('a', 'b', () => true, 1);
fsmPri.defineTransition('a', 'c', () => true, 10);
fsmPri.evaluateTransitions();
assert(fsmPri.current === 'c', 'Higher priority (10) wins');
// 7. Update
section('7. update()');
const fsmUpdate = createStateMachine<PlayerState>('idle');
let updateCount = 0;
fsmUpdate.defineState('idle', {
onUpdate: () => { updateCount++; }
});
fsmUpdate.update(16);
fsmUpdate.update(16);
assert(updateCount === 2, 'onUpdate called on each update');
// 8. Event Listeners
section('8. Event Listeners');
const fsmEvents = createStateMachine<PlayerState>('idle');
fsmEvents.defineState('idle', {});
fsmEvents.defineState('walk', {});
let enterEvent = false;
let exitEvent = false;
let changeEvent = false;
fsmEvents.onEnter('walk', () => { enterEvent = true; });
fsmEvents.onExit('idle', () => { exitEvent = true; });
fsmEvents.onChange(() => { changeEvent = true; });
fsmEvents.transition('walk');
assert(enterEvent, 'onEnter listener called');
assert(exitEvent, 'onExit listener called');
assert(changeEvent, 'onChange listener called');
// 9. getStates / getTransitionsFrom
section('9. Query Methods');
const states = fsmEvents.getStates();
assert(states.length >= 2, 'getStates() returns states');
// 10. canTransition
section('10. canTransition()');
const fsmCan = createStateMachine<PlayerState, Context>('idle', {
context: { speed: 0, isMoving: false }
});
fsmCan.defineState('idle', {});
fsmCan.defineState('walk', {});
fsmCan.defineTransition('idle', 'walk', (ctx) => ctx.isMoving);
assert(!fsmCan.canTransition('walk'), 'Cannot transition when condition false');
fsmCan.context.isMoving = true;
assert(fsmCan.canTransition('walk'), 'Can transition when condition true');
// 11. Reset
section('11. reset()');
fsmCan.transition('walk');
fsmCan.reset('idle');
assert(fsmCan.current === 'idle', 'Reset to idle');
// 12. History
section('12. getHistory()');
const fsmHist = createStateMachine<PlayerState>('idle', { enableHistory: true });
fsmHist.defineState('idle', {});
fsmHist.defineState('walk', {});
fsmHist.defineState('run', {});
fsmHist.transition('walk');
fsmHist.transition('run');
const history = fsmHist.getHistory();
assert(history.length >= 2, 'History recorded');
demoFooter('FSM Demo');
}
runFSMDemo().catch(console.error);