blueshell
Version:
A Behavior Tree implementation in modern Javascript
247 lines (205 loc) • 6.01 kB
text/typescript
/**
* Created by josh on 1/10/16.
*/
import { v4 } from 'uuid';
import { BlueshellState, NodeStorage, rc, ResultCode, BaseNode, isParentNode } from '../models';
import { TreePublisher, TreeNonPublisher, NodeManager } from '../utils';
/**
* Interface that defines what is stored in private node storage at the root
*/
export interface PrivateNodeStorage {
debug: boolean;
eventCounter: number | undefined;
}
/**
* Base class of all Nodes.
* @author Joshua Chaitin-Pollak
*/
export class Base<S extends BlueshellState, E> implements BaseNode<S, E> {
private _id = '';
private _parent: string;
// Hard to properly type this since the static can't
// inherit the types from this generic class. This static is
// here because it's difficult to inject this functionality
// into blueshell in the current form, but this is maybe
// marginally better than a global
static treePublisher: TreePublisher<any, any> = new TreeNonPublisher();
public static registerTreePublisher<S extends BlueshellState, E>(
publisher: TreePublisher<S, E>,
): void {
Base.treePublisher = publisher;
}
public static registerNodeForDebug<S extends BlueshellState, E>(node: BaseNode<S, E>): void {
NodeManager.getInstance<S, E>().addNode(node);
}
public static unregisterNodeForDebug<S extends BlueshellState, E>(node: BaseNode<S, E>): void {
NodeManager.getInstance<S, E>().removeNode(node);
}
/**
* @constructor
* @param name The name of the Node. If no name is given, the name of the Class will be used.
*/
constructor(public readonly name: string = '') {
if (!this.name) {
this.name = this.constructor.name;
}
this._parent = '';
}
/**
* Handles the Event, and invokes `onEvent(state, event)`
* @param state The state when the event occured.
* @param event The event to handle.
* @protected
*/
public handleEvent(state: S, event: E): ResultCode {
this._beforeEvent(state, event);
const passed = this.precondition(state, event);
if (!passed) {
return rc.FAILURE;
}
try {
Base.treePublisher.publishResult(state, event, false);
const result = this.onEvent(state, event);
return this._afterEvent(result, state, event);
} catch (err: any) {
state.errorReason = err;
if (this.getDebug(state)) {
console.error('Error: ', err.stack); // eslint-disable-line no-console
}
return rc.ERROR;
}
}
/**
* Return an empty object
* @ignore
* @param state
* @param event
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
protected _beforeEvent(state: S, event: E) {
const pStorage = this._privateStorage(state);
const nodeStorage = this.getNodeStorage(state);
// If this is the root node, increment the event counter
if (!this._parent) {
pStorage.eventCounter = ++pStorage.eventCounter || 1;
}
// Reset the lastResult unless it was previously RUNNING and we're not a parent
if (nodeStorage.lastResult !== rc.RUNNING || isParentNode(this)) {
nodeStorage.lastResult = '';
}
// Record the last event we've seen
// console.log('%s: incrementing event counter %s, %s',
// this.path, nodeStorage.lastEventSeen, pStorage.eventCounter);
nodeStorage.lastEventSeen = pStorage.eventCounter;
return {};
}
/**
* Logging
* @ignore
* @param res
* @param state
* @param event
*/
protected _afterEvent(res: ResultCode, state: S, event: E): ResultCode {
if (this.getDebug(state)) {
console.log(this.path, ' => ', event, ' => ', res); // eslint-disable-line no-console
}
const storage = this.getNodeStorage(state);
// Cache our results for the next iteration
storage.lastResult = res;
return res;
}
/**
* Return true if this Node should proceed handling the event. false otherwise.
* @param state
* @param event
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
protected precondition(state: S, event: E): boolean {
return true;
}
/**
* Invoked when there is a new event.
* @param state
* @param event
* @return Result. Must be rc.SUCCESS, rc.FAILURE, or rc.RUNNING
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
protected onEvent(state: S, event: E): ResultCode {
return rc.SUCCESS;
}
public set parent(path: string) {
this._parent = path;
}
public get parent() {
return this._parent;
}
public get id(): string {
if (!this._id) {
this._id = `n${v4().replace(/\-/g, '')}`;
}
return this._id;
}
public get path() {
return (!!this._parent ? `${this._parent}_` : '') + this.name;
}
/**
* Returns storage unique to this Node, keyed on the Node's path.
* @param state
*/
public getNodeStorage(state: S): NodeStorage {
const path = this.path;
const blueshell = this._privateStorage(state);
blueshell[path] = blueshell[path] || {};
return blueshell[path];
}
/**
* Resets the storage unique to this Node, via the Node's path.
* If this node is a parent, then also reset all children.
* @param state
*/
public resetNodeStorage(state: S) {
if (isParentNode(this)) {
for (const child of this.getChildren()) {
child.resetNodeStorage(state);
}
}
const path = this.path;
const blueshell = this._privateStorage(state);
blueshell[path] = {};
return blueshell[path];
}
/**
* @ignore
* @param state
*/
protected _privateStorage(state: S) {
state.__blueshell = state.__blueshell || {};
return state.__blueshell;
}
public getDebug(state: S) {
return this._privateStorage(state).debug;
}
public getTreeEventCounter(state: S) {
return this._privateStorage(state).eventCounter;
}
/**
* Getter for the previous event seen.
* @param state
*/
public getLastEventSeen(state: S) {
return this.getNodeStorage(state).lastEventSeen;
}
/**
* Getter for the result of the last handled Event.
* @param state
*/
public getLastResult(state: S) {
return this.getNodeStorage(state).lastResult;
}
public get symbol(): string {
return '';
}
}
export { Base as Action };
export { Base as Condition };