UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

247 lines (187 loc) • 6.81 kB
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; } }