@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
247 lines (187 loc) • 6.81 kB
JavaScript
import { assert } from "../../assert.js";
import { dispatchViaProxy } from "../../events/signal/dispatchViaProxy.js";
import { findSignalHandlerIndexByHandle } from "../../events/signal/findSignalHandlerIndexByHandle.js";
import { SignalHandler } from "../../events/signal/SignalHandler.js";
import { noop } from "../../function/noop.js";
import { SimpleStateMachineDescription } from "./SimpleStateMachineDescription.js";
export class SimpleStateMachine {
/**
* Current state, -1 indicated "unset"
* @type {number}
* @private
*/
__state = -1;
/**
*
* @type {SignalHandler[]}
* @private
*/
__eventHandlersStateEntry = [];
/**
*
* @type {SignalHandler[]}
* @private
*/
__eventHandlersStateExit = [];
/**
*
* @param {SimpleStateMachineDescription} description
*/
constructor(description) {
assert.defined(description, 'description');
assert.notNull(description, 'description');
assert.ok(description.isSimpleStateMachineDescription, 'SimpleStateMachineDescription.isSimpleStateMachineDescription !== true');
//assert.ok(description instanceof SimpleStateMachineDescription, 'description is not an instance of SimpleStateMachineDescription');
/**
*
* @type {SimpleStateMachineDescription}
*/
this.description = description;
}
/**
*
* @param {number} state
* @param {function} handler
* @param {*} [thisArg]
*/
addEventHandlerStateEntry(state, handler, thisArg) {
//assert.ok(this.description.stateExists(state), `state ${state} doesn't exist`);
const handlers = this.__eventHandlersStateEntry[state];
const signalHandler = new SignalHandler(handler, thisArg);
if (handlers === undefined) {
this.__eventHandlersStateEntry[state] = [signalHandler];
} else {
handlers.push(signalHandler);
}
}
/**
*
* @param {number} state
* @param {function} handler
* @param {*} [thisArg]
*/
removeEventHandlerStateEntry(state, handler, thisArg) {
//assert.ok(this.description.stateExists(state), `state ${state} doesn't exist`);
const handlers = this.__eventHandlersStateEntry[state];
if (handlers !== undefined) {
const i = findSignalHandlerIndexByHandle(handlers, handler, thisArg);
if (i !== -1) {
handlers.splice(i, 1);
return true;
}
}
return false;
}
/**
*
* @param {number} state
* @param {function} handler
* @param {*} [thisArg]
*/
addEventHandlerStateExit(state, handler, thisArg) {
//assert.ok(this.description.stateExists(state), `state ${state} doesn't exist`);
const handlers = this.__eventHandlersStateExit[state];
const signalHandler = new SignalHandler(handler, thisArg);
if (handlers === undefined) {
this.__eventHandlersStateExit[state] = [signalHandler];
} else {
handlers.push(signalHandler);
}
}
/**
*
* @param {number} state
* @param {function} handler
* @param {*} [thisArg]
*/
removeEventHandlerStateExit(state, handler, thisArg) {
//assert.ok(this.description.stateExists(state), `state ${state} doesn't exist`);
const handlers = this.__eventHandlersStateExit[state];
if (handlers !== undefined) {
const i = findSignalHandlerIndexByHandle(handlers, handler, thisArg);
if (i !== -1) {
handlers.splice(i, 1);
return true;
}
}
return false;
}
/**
*
* @param {number} s
*/
setState(s) {
//assert.ok(this.description.stateExists(s), `state ${s} doesn't exist`);
const oldState = this.__state;
const exitHandlers = this.__eventHandlersStateExit[oldState];
if (exitHandlers !== undefined) {
dispatchViaProxy(exitHandlers, [oldState, s]);
}
this.__state = s;
//process event handlers
const handlers = this.__eventHandlersStateEntry[s];
if (handlers !== undefined) {
dispatchViaProxy(handlers, [s, oldState]);
}
}
/**
*
* @returns {number}
*/
getState() {
return this.__state;
}
/**
* Performs navigation to target state via the shortest path
* @throws {Error} if no path could be found to target state
* @param {number} target
*/
navigateTo(target) {
assert.isNonNegativeInteger(target, 'target');
const currentState = this.getState();
if (currentState === target) {
// already in the target state
return;
}
const description = this.description;
assert.ok(description.stateExists(target), `target state ${target} doesn't exist`);
const path = description.findPath(currentState, target);
if (path === null) {
throw new Error(`No path exists from current state '${currentState}' to target state '${target}'`);
}
const n = path.length;
for (let i = 1; i < n; i++) {
const next = path[i];
this.setState(next);
}
}
/**
* @template X
* @param {X} [input] value will be fed into selector
* @param {function} [preStateChangeHook]
* @return {boolean}
*/
advance(input, preStateChangeHook = noop) {
const description = this.description;
//get active selector
const selector = description.__actions[this.__state];
let targetState;
if (selector === undefined) {
//no selector
const targetNodes = description.getOutgoingStates(this.__state);
if (targetNodes.length !== 1) {
//no selector and number of targets is ambiguous
return false;
}
//
targetState = targetNodes[0];
} else {
targetState = selector(input);
//assert.ok(description.stateExists(targetState), `targetState ${targetState} does not exist`);
}
//assert.ok(description.edgeExists(this.__state, targetState), `no edge exists from ${this.__state} to ${targetState}`);
preStateChangeHook(targetState, input);
this.setState(targetState);
return true;
}
}