UNPKG

@casual-simulation/aux-common

Version:
177 lines 6.43 kB
/* CasualOS is a set of web-based tools designed to facilitate the creation of real-time, multi-user, context-aware interactive experiences. * * Copyright (c) 2019-2025 Casual Simulation, Inc. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. */ export class StateMachine { get currentState() { return this._states[this._curStateId]; } /** * Create a new state machine. Setup the state machine using `addState` and `addStateTransition`. * Start the state machine using the `start` method. Manually call `update` every frame in order to update the state machine. */ constructor(name) { /** * Debug level of this state machine. * 0 = off; 1 = enter, exit; 2 = transitions, 3 = update */ this.debugLevel = 0; /** * Map of available states. state id -> State. */ this._states = {}; /** * Map of transition hash code -> state id */ this._transitions = {}; this.name = name; } start(startingStateId, curFrame) { if (this._active) return; this._active = true; this._curFrame = curFrame; this._changeState(startingStateId); this.update(curFrame); } pause() { if (!this._active) return; this._active = false; } resume(curFrame) { if (this._active) return; this._active = true; this._curFrame = curFrame; this.update(curFrame); } addState(state) { if (this._active) throw new Error('Cannot add states to state machine after it has started.'); this._states[state.id] = state; } addStateTransition(transition, nextStateId) { if (this._active) throw new Error('Cannot add state transitions after the state machine has been started.'); if (!this._states[nextStateId]) throw new Error(`Can't add transition for state '${nextStateId}'. This state does not exist on the state machine.`); let transitionHash = computeTransitionHash(transition); this._transitions[transitionHash] = nextStateId; } getState(stateId) { return this._states[stateId]; } update(curFrame) { if (!this._active) return; if (this._lastUpdateFrame === curFrame) return; this._curFrame = curFrame; this._lastUpdateFrame = curFrame; if (this._changeStateId) { let curState = this._states[this._curStateId]; if (curState) { if (this.debugLevel >= 1) console.log(`${this.toString()} ${curState.id} onStateExit. frame: ${curFrame}`); curState.onStateExit(); } curState = this._states[this._changeStateId]; if (curState) { if (this.debugLevel >= 1) console.log(`${this.toString()} ${curState.id} onStateEnter. frame: ${curFrame}`); curState.onStateEnter(); } } else { throw new Error(`${this.toString()} No state with id '${this._changeStateId}' found.`); } let curState = this._states[this._curStateId]; if (curState) { if (this.debugLevel >= 3) console.log(`${this.toString()} ${curState.id} onStateUpdate. frame: ${curFrame}`); let command = curState.onStateUpdate(); if (command) { if (this.debugLevel >= 2) console.log(`${this.toString()} ${curState.id} command: ${command}, nextState: ${this.getNextStateId(command)}, frame: ${curFrame}`); this._changeState(this.getNextStateId(command)); } } this._changeStateId = undefined; } toString() { return `[StateMachine::${this.name}]`; } /** * Call this when getting rid of the state machine. */ dispose() { throw new Error("StateMachine's dispose is not implemented yet."); } /** * Usually this state machine should be controlled with state transitions, but in some cases we may want to force a state change. */ forceChangeState(stateId) { this._changeState(stateId); } _changeState(stateId) { this._changeStateId = stateId; // Set the last update frame to an invalid number so that the update state function is forced to run. this._lastUpdateFrame = -1; // Run the update state function immediately to respond to change the state in the same frame. this.update(this._curFrame); } getNextStateId(command) { if (this._transitions) return; let transition = { stateId: this._curStateId, command: command, }; let transitionHash = computeTransitionHash(transition); let nextStateId = this._transitions[transitionHash]; if (!nextStateId) throw new Error(`No transition found for State '${this._curStateId}' with command '${command}'`); return nextStateId; } } export class State { get id() { return this._id; } constructor(id) { this._id = id; } } /** * Compute a hash code for the specified Transition. * @param transition The transition to compute hash code for. */ function computeTransitionHash(transition) { let hashCode = (s) => { let h = 0, l = s.length, i = 0; if (l > 0) { while (i < l) { h = ((h << 5) - h + s.charCodeAt(i++)) | 0; } } return h; }; return (17 + 31 * hashCode(transition.stateId) + 31 * hashCode(transition.command)); } //# sourceMappingURL=StateMachine.js.map