gibbon.js
Version:
Actor/Component system for use with pixi.js.
242 lines (179 loc) • 5.9 kB
text/typescript
import { Component } from './component';
import { State, Transition } from '../data/state';
import type { StateEffect, StateEffectDef } from '../data/state';
import { ComponentKey } from './actor';
export enum StateEvent {
enter = 'enterState',
exit = 'exitState'
}
/**
* Basis State Machine for adding/removing components on state changes.
*/
export class FSM<TKey = string | Symbol | number, TTrigger = string | Symbol> extends Component {
private _startState: State<TKey, TTrigger>;
private readonly _states: Map<TKey, State<TKey, TTrigger>> = new Map();
public get current() { return this._current; }
private _current: State<TKey, TTrigger>;
/**
* State being transitioned to.
*/
private _changeState: State<TKey, TTrigger> | null = null;
/**
* Current transition for timed transitions.
*/
private curTransition: Transition<TKey> | null = null;
/**
* Timer on current transition.
*/
private transTimer: number = 0;
constructor(start: State<TKey, TTrigger> | TKey) {
super();
this._startState = start instanceof State ? start : new State<TKey, TTrigger>(start);
this.addState(this._startState);
this._current = this._startState;
}
init() {
this.enterState(this._current);
}
/**
* Trigger transition on current state.
* @param trigger
* @returns new State or false on error.
*/
trigger(trigger: TTrigger) {
const next = this._current.getNextState(trigger);
if (next) {
return this.switchState(next);
}
return false;
}
/**
* Switch to new state, triggering exit and enter transitions
* from current and next states respectively.
* @param newState
* @throws Error if state change already in progress, or attempting to change state
* on an FSM not initialized with an Actor.
* @returns new State<TKey> or false on failure.
*/
switchState(stateName: TKey) {
if (stateName === this._current.name) {
// No state change.
return false;
} else if (!this.actor) {
throw new Error(`Attempting to change state with no actor: ${stateName}`)
}
const newState = this.getState(stateName);
if (!newState) {
return false;
}
if (this._changeState) {
if (newState.priority <= this._changeState.priority) {
return;
}
}
this.curTransition = null;
if (newState) {
this._current.onExit?.apply(this.actor!);
this.actor?.emit(StateEvent.exit, this._current);
this.enterState(newState);
}
this._changeState = null;
return newState ?? false;
}
private enterState(newState: State<TKey, TTrigger>) {
this._current = newState;
newState.onEnter?.apply(this.actor!);
this.actor!.emit(StateEvent.enter, newState);
if (newState.autoNext) {
this.curTransition = newState.autoNext;
this.transTimer = newState.autoNext.enterTime;
}
}
update(delta: number) {
if (this.curTransition) {
this.transTimer -= delta;
if (this.transTimer <= 0) {
this.switchState(this.curTransition.dest);
}
}
}
getState(name: TKey) {
return this._states.get(name);
}
/**
* Returns true if the current state responds
* to trigger.
* @param trigger
*/
canTrigger(trigger: TTrigger) {
return this.current.canTrigger(trigger);
}
/**
* Set current state without triggering transitions.
* @param name
*/
setState(name: TKey) {
const state = this._states.get(name);
if (state) {
this.enterState(state);
}
}
/**
* Create and return new state of FSM.
* @param name
* @returns
*/
createState(name: TKey, opts?: { onEnter?: StateEffect | StateEffectDef, onExit?: StateEffect | StateEffectDef }) {
if (this._states.has(name)) {
return false;
} else {
const st = new State<TKey, TTrigger>(name, opts);
this._states.set(name, st);
return st;
}
}
/**
* Set FSM Start State.
* @param state
*/
setStart(state: State<TKey, TTrigger> | TKey) {
if (state instanceof State) {
this.addState(state);
this._startState = state;
} else {
const st = this._states.get(state);
if (st) {
this._startState = st;
}
}
}
addState(state: State<TKey, TTrigger>) {
this._states.set(state.name, state);
}
/**
* Add a component to enable when entering a state.
* @param state
* @param enable
*/
addStateEnable(state: TKey, enable: ComponentKey) {
const st = this._states.get(state);
if (st) {
st.addEnterEnable(enable);
} else {
console.warn(`FSm.addStateEnable() unexpected missing state: ${state}`);
}
}
/**
* Add component to disable when entering a state.
* @param state
* @param disable
*/
addStateDisable(state: TKey, disable: ComponentKey) {
const st = this._states.get(state);
if (st) {
st.addEnterDisable(disable);
} else {
console.warn(`FSm.addStateDisable() unexpected missing state: ${state}`);
}
}
}