diff --git a/lib/tree/activateable.ts b/lib/tree/activateable.ts new file mode 100644 index 0000000..d88b771 --- /dev/null +++ b/lib/tree/activateable.ts @@ -0,0 +1,63 @@ +import {BlueshellState} from '../nodes/BlueshellState'; +import {Base as Action} from '../nodes/Base'; +import {ResultCode, resultCodes} from '../utils/resultCodes'; + +function activationCheck(storage: any) { + return storage.lastResult !== resultCodes.RUNNING; +} + +function reactivationCheck(storage: any, e: E) { + return (e).type === 'reactivate' || storage.lastResult !== resultCodes.RUNNING; +} + +class Activatable extends Action { + constructor( + name: string, + private readonly onActivateFn: RunningFn, + private readonly onRunningFn: RunningFn, + private readonly activationCheck: (storage: any, e: E) => boolean, + ) { + super(name); + } + + onEvent(state: S, event: E): ResultCode { + const storage = this.getNodeStorage(state); + + if (storage.lastResult !== resultCodes.ERROR) { + if (this.activationCheck) { + return this.onActivateFn(state, event); + } else { + return this.onRunningFn(state, event); + } + } else { + return resultCodes.ERROR; + } + } +} + +export type ActivateFn = (state: S, event: E) => ResultCode +export type RunningFn = (state: S, event: E) => ResultCode + +export function activatable( + name: string, + onActivateFn: ActivateFn, + onRunningFn: RunningFn, +): Action { + return new Activatable( + name, + onActivateFn, + onRunningFn, + activationCheck); +} + +export function reactivatable( + name: string, + onActivateFn: ActivateFn, + onRunningFn: RunningFn, +): Action { + return new Activatable( + name, + onActivateFn, + onRunningFn, + reactivationCheck); +} diff --git a/lib/tree/after.ts b/lib/tree/after.ts new file mode 100644 index 0000000..5f2d637 --- /dev/null +++ b/lib/tree/after.ts @@ -0,0 +1,28 @@ +import {BlueshellState} from '../nodes/BlueshellState'; +import {Decorator} from '../nodes/Decorator'; +import {DecoratorFn} from './decoratorFunction'; +import {Base as Action} from '../nodes/Base'; +import {ResultCode} from '../utils/resultCodes'; + + +class After extends Decorator { + constructor( + name: string, + action: Action, + private readonly after: DecoratorFn, + ) { + super(name, action); + } + _afterEvent(res: ResultCode, s: S, e: E) { + this.after(s, e); + + return super._afterEvent(res, s, e); + } +} + +export function after( + action: Action, + afterFn: DecoratorFn, +): Action { + return new After(`before-$action.name`, action, afterFn); +} diff --git a/lib/tree/before.ts b/lib/tree/before.ts new file mode 100644 index 0000000..0af6b8d --- /dev/null +++ b/lib/tree/before.ts @@ -0,0 +1,27 @@ +import {BlueshellState} from '../nodes/BlueshellState'; +import {Decorator} from '../nodes/Decorator'; +import {DecoratorFn} from './decoratorFunction'; +import {Base as Action} from '../nodes/Base'; + + +class Before extends Decorator { + constructor( + name: string, + action: Action, + private readonly before: DecoratorFn, + ) { + super(name, action); + } + _beforeEvent(s: S, e: E) { + this.before(s, e); + + return super._beforeEvent(s, e); + } +} + +export function before( + action: Action, + beforeFn: DecoratorFn, +): Action { + return new Before(`before-$action.name`, action, beforeFn); +} diff --git a/lib/tree/decoratorFunction.ts b/lib/tree/decoratorFunction.ts new file mode 100644 index 0000000..f7e96ff --- /dev/null +++ b/lib/tree/decoratorFunction.ts @@ -0,0 +1,3 @@ +export interface DecoratorFn { + (state: S, event: E): void; +} diff --git a/test/tree/complexTree.alt.spec.ts b/test/tree/complexTree.alt.spec.ts new file mode 100644 index 0000000..deffb59 --- /dev/null +++ b/test/tree/complexTree.alt.spec.ts @@ -0,0 +1,130 @@ +import {activatable, RunningFn} from '../../lib/tree/activateable'; +import {resultCodes, ResultCode} from '../../lib/utils/resultCodes'; +import {BlueshellState} from '../../lib/nodes/BlueshellState'; + +export type isCompletionEventFn = (state: S, event: E) => boolean; +export type onCompleteFn = (state: S, event: E) => void; +export type onIncompleteFn = (state: S, event: E) => void; + +function mkRunningFn( + isCompletionEvent: isCompletionEventFn, + onComplete: onCompleteFn, + onIncomplete: onIncompleteFn, +): RunningFn { + // log.debug(`${this.name}: runningEvent`); + + return (state: S, event: E) => { + if (isCompletionEvent(state, event)) { + // console.log(`${this.name}: Event type '${event.type}' matches done condition`); + onComplete(state, event); + return resultCodes.SUCCESS; + } else { + onIncomplete(state, event); + return resultCodes.RUNNING; + } + } + +} + +interface TypedEvent { + type: string +}; + +interface ActionData { + +} + +interface CommandActionData { + name: string, + running?: RunningFn, +} + + +// commandAction +function commandAction( + actionData: CommandActionData, +) { + const setCommands = function(mfp: S, event: E) { + // const makeResult = this.makeCommand(mfp, event); + // let cmds = Array.isArray(makeResult) ? makeResult : [makeResult]; + + // if (mfp.debug && app.serviceConfig.enableVisualDebug) { + // cmds = cmds.concat(DebugCommand.debug(mfp.id, mfp.debug)); + // } + // mfp.outgoingCommands = cmds; + }; + + return activatable( + actionData.name, + (s: S, event: E) => { + console.log(`${this.name}: Activate`); + setCommands(s, event); + + return resultCodes.RUNNING; + }, + // not works + actionData.running || () => resultCodes.SUCCESS, + // works + // actionData.running ? actionData.running : () => resultCodes.SUCCESS, + ) +} + + +interface SimpleActionData implements ActionData { + name: string, + doneEventType: string, + onComplete?: onCompleteFn, + onIncomplete?: onIncompleteFn, +} + +// simpleAction +function simpleAction( + actionData: SimpleActionData, +) { + return commandAction({ + name, + running: mkRunningFn( + (s: S, event: E) => event.type === actionData.doneEventType, + actionData.onComplete || actionData.onComplete, + actionData.onIncomplete || actionData.onIncomplete, + ) + }); +} + + +class ButtonEvent implements TypedEvent { + type: 'button' + isAction: boolean +}; + +// buttonAction +function buttonAction( +) { + return simpleAction({ + name, + 'button', + (state: S, event: E) => { + return super.isCompletionEvent(event) && + (event instanceof ButtonEvent) && + event.isAction; + } + }); +} + +describe('complexTree', function() { + it('should build a complex tree', function() { + const bootup = activatable( + 'bootup', + () => resultCodes.RUNNING, + () => resultCodes.SUCCESS, + ); + + const waitForClick = + + const fullTree = latchedSequence([ + bootup, + ]); + + fullTree.handleEvent(); + }); +}); diff --git a/test/tree/complexTree.spec.ts b/test/tree/complexTree.spec.ts new file mode 100644 index 0000000..bd47564 --- /dev/null +++ b/test/tree/complexTree.spec.ts @@ -0,0 +1,116 @@ +import {activatable, RunningFn} from '../../lib/tree/activateable'; +import {resultCodes, ResultCode} from '../../lib/utils/resultCodes'; +import {BlueshellState} from '../../lib/nodes/BlueshellState'; + +// commandAction +function commandAction( + name: string, + running: RunningFn = () => resultCodes.SUCCESS, +) { + const setCommands = function(mfp: S, event: E) { + // const makeResult = this.makeCommand(mfp, event); + // let cmds = Array.isArray(makeResult) ? makeResult : [makeResult]; + + // if (mfp.debug && app.serviceConfig.enableVisualDebug) { + // cmds = cmds.concat(DebugCommand.debug(mfp.id, mfp.debug)); + // } + // mfp.outgoingCommands = cmds; + }; + + return activatable( + name, + (s: S, event: E) => { + console.log(`${this.name}: Activate`); + setCommands(s, event); + + return resultCodes.RUNNING; + }, + running + ) +} + +export type isCompletionEventFn = (state: S, event: E) => boolean; +export type onCompleteFn = (state: S, event: E) => void; +export type onIncompleteFn = (state: S, event: E) => void; + +function running( + isCompletionEvent: isCompletionEventFn, + onComplete: onCompleteFn, + onIncomplete: onIncompleteFn, +): RunningFn { + // log.debug(`${this.name}: runningEvent`); + + return (state: S, event: E) => { + if (isCompletionEvent(state, event)) { + // console.log(`${this.name}: Event type '${event.type}' matches done condition`); + onComplete(state, event); + return resultCodes.SUCCESS; + } else { + onIncomplete(state, event); + return resultCodes.RUNNING; + } + } + +} + +interface TypedEvent { + type: string +}; + +// simpleAction +function simpleAction( + name: string, + doneEventType: string, + onComplete: onCompleteFn = () => {}, + onIncomplete: onIncompleteFn = () => {}, +) { + return commandAction( + name, + running( + (s: S, event: E) => event.type === doneEventType, + onComplete, + onIncomplete, + ) + ); +} + +class ButtonEvent implements TypedEvent { + type: 'button' + isAction: boolean +}; + +// buttonAction +function buttonAction( + name: string, + doneEventType: string, + onComplete: onCompleteFn = () => {}, + onIncomplete: onIncompleteFn = () => {}, +) { + return simpleAction( + name, + 'button', + (state: S, event: E) => { + return super.isCompletionEvent(event) && + (event instanceof ButtonEvent) && + event.isAction; + } + ); +} + +describe('complexTree', function() { + it('should build a complex tree', function() { + const bootup = activatable( + 'bootup', + () => resultCodes.RUNNING, + () => resultCodes.SUCCESS, + ); + + const waitForClick = + + const fullTree = latchedSequence([ + bootup, + ]); + + fullTree.handleEvent(); + }); +});