@casual-simulation/aux-common
Version:
Common library for AUX projects
177 lines • 6.43 kB
JavaScript
/* 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