refactor: behavior
This commit is contained in:
parent
de5b51b8ea
commit
abbab55d30
|
@ -1,18 +1,15 @@
|
||||||
import {compose, EventEmitter, TickingPromise} from '@avocado/core';
|
import {compose, EventEmitter, TickingPromise} from '@avocado/core';
|
||||||
|
|
||||||
import {Traversal} from './traversal';
|
|
||||||
import {Traversals} from './traversals';
|
|
||||||
|
|
||||||
const decorate = compose(
|
const decorate = compose(
|
||||||
EventEmitter,
|
EventEmitter,
|
||||||
);
|
);
|
||||||
|
|
||||||
export class Actions extends decorate(Traversals) {
|
class Actions {
|
||||||
|
|
||||||
constructor() {
|
constructor(expressions) {
|
||||||
super();
|
this.expressions = 'function' === typeof expressions ? expressions() : expressions;
|
||||||
this._index = 0;
|
this._index = 0;
|
||||||
this._actionPromise = null;
|
this.promise = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
emitFinished() {
|
emitFinished() {
|
||||||
|
@ -33,29 +30,24 @@ export class Actions extends decorate(Traversals) {
|
||||||
|
|
||||||
tick(context, elapsed) {
|
tick(context, elapsed) {
|
||||||
// Empty resolves immediately.
|
// Empty resolves immediately.
|
||||||
if (this.traversals.length === 0) {
|
if (this.expressions.length === 0) {
|
||||||
this.emitFinished();
|
this.emitFinished();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// If the action promise ticks, tick it.
|
// If the action promise ticks, tick it.
|
||||||
if (this._actionPromise && this._actionPromise instanceof TickingPromise) {
|
if (this.promise && this.promise instanceof TickingPromise) {
|
||||||
this._actionPromise.tick(elapsed);
|
this.promise.tick(elapsed);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Actions execute immediately until a promise is made, or they're all
|
// Actions execute immediately until a promise is made, or they're all
|
||||||
// executed.
|
// executed.
|
||||||
while (true) {
|
while (true) {
|
||||||
// Run the action.
|
// Run the action.
|
||||||
const result = this.traversals[this.index].traverse(context);
|
const result = this.expressions[this.index](context);
|
||||||
// Deferred result.
|
// Deferred result.
|
||||||
if (result instanceof Promise) {
|
if (result instanceof Promise) {
|
||||||
this._actionPromise = result;
|
this.promise = result;
|
||||||
// Handle any errors.
|
this.promise.catch(console.error).finally(() => this.prologue());
|
||||||
result.catch(console.error);
|
|
||||||
result.finally(() => {
|
|
||||||
// Finally, run the prologue.
|
|
||||||
this.prologue();
|
|
||||||
});
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
// Immediate result.
|
// Immediate result.
|
||||||
|
@ -68,19 +60,24 @@ export class Actions extends decorate(Traversals) {
|
||||||
}
|
}
|
||||||
|
|
||||||
parallel(context) {
|
parallel(context) {
|
||||||
// Map all traversals to results.
|
const results = this.expressions.map((expression) => expression(context));
|
||||||
const results = this.traversals.map((traversal) => {
|
for (let i = 0; i < results.length; i++) {
|
||||||
return traversal.traverse(context);
|
const result = results[i];
|
||||||
});
|
if (result instanceof TickingPromise) {
|
||||||
// Wrap all results in a TickingPromise.
|
|
||||||
return TickingPromise.all(results);
|
return TickingPromise.all(results);
|
||||||
}
|
}
|
||||||
|
if (result instanceof Promise) {
|
||||||
|
return Promise.all(results);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
prologue() {
|
prologue() {
|
||||||
// Clear out the action promise.
|
// Clear out the action promise.
|
||||||
this._actionPromise = null;
|
this.promise = null;
|
||||||
// Increment and wrap the index.
|
// Increment and wrap the index.
|
||||||
this.index = (this.index + 1) % this.traversals.length;
|
this.index = (this.index + 1) % this.expressions.length;
|
||||||
// If rolled over, the actions are finished.
|
// If rolled over, the actions are finished.
|
||||||
if (0 === this.index) {
|
if (0 === this.index) {
|
||||||
this.emitFinished();
|
this.emitFinished();
|
||||||
|
@ -103,3 +100,5 @@ export class Actions extends decorate(Traversals) {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default decorate(Actions);
|
|
@ -1,36 +1,29 @@
|
||||||
export function buildTraversal(path, value) {
|
export function buildExpression(path, value) {
|
||||||
const traversal = {
|
const expression = {
|
||||||
type: 'traversal',
|
type: 'expression',
|
||||||
steps: path.map((key) => {
|
ops: path.map((key) => ({type: 'key', key: key})),
|
||||||
return {
|
|
||||||
type: 'key',
|
|
||||||
key: key,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
};
|
};
|
||||||
if ('undefined' !== typeof value) {
|
if ('undefined' !== typeof value) {
|
||||||
traversal.value = buildValue(value);
|
expression.value = buildValue(value);
|
||||||
}
|
}
|
||||||
return traversal;
|
return expression;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildInvoke (path, args = []) {
|
export function buildInvoke (path, args = []) {
|
||||||
const traversal = buildTraversal(path);
|
const expression = buildExpression(path);
|
||||||
traversal.steps.push({
|
expression.ops.push({
|
||||||
type: 'invoke',
|
type: 'invoke',
|
||||||
args: args.map((arg) => {
|
args: args.map((arg) => buildValue(arg)),
|
||||||
return buildValue(arg);
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
return traversal;
|
return expression;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildValue(value) {
|
export function buildValue(value) {
|
||||||
if (
|
if (
|
||||||
'object' === typeof value
|
'object' === typeof value
|
||||||
&& (
|
&& (
|
||||||
'traversal' === value.type
|
'expression' === value.type
|
||||||
|| 'actions' === value.type
|
|| 'expressions' === value.type
|
||||||
|| 'condition' === value.type
|
|| 'condition' === value.type
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
|
@ -46,8 +39,6 @@ export function buildCondition(operator, operands) {
|
||||||
return {
|
return {
|
||||||
type: 'condition',
|
type: 'condition',
|
||||||
operator,
|
operator,
|
||||||
operands: operands.map((operand) => {
|
operands: operands.map((operand) => buildValue(operand)),
|
||||||
return buildValue(operand);
|
|
||||||
}),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
26
packages/behavior/compile.js
Normal file
26
packages/behavior/compile.js
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import memoize from 'lodash.memoize';
|
||||||
|
import {invokeHookFlat, registerHooks} from 'scwp';
|
||||||
|
|
||||||
|
export const compilers = memoize(() => (
|
||||||
|
invokeHookFlat('behaviorCompilers')
|
||||||
|
.reduce((r, results) => ({
|
||||||
|
...r,
|
||||||
|
...results,
|
||||||
|
}), {})
|
||||||
|
));
|
||||||
|
|
||||||
|
export default function compile(variant) {
|
||||||
|
const {[variant.type]: compiler} = compilers();
|
||||||
|
if (!compiler) {
|
||||||
|
return () => Promise.reject(new Error(`No compiler for '${variant.type}'`));
|
||||||
|
}
|
||||||
|
return compiler(variant);
|
||||||
|
}
|
||||||
|
|
||||||
|
registerHooks({
|
||||||
|
autoreg$accept: (type, M) => {
|
||||||
|
if ('hooks' === type && 'behaviorCompilers' in M) {
|
||||||
|
compilers.cache.clear();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}, module.id);
|
142
packages/behavior/compilers.hooks.js
Normal file
142
packages/behavior/compilers.hooks.js
Normal file
|
@ -0,0 +1,142 @@
|
||||||
|
import {fastApply} from '@avocado/core';
|
||||||
|
|
||||||
|
import compile from './compile';
|
||||||
|
|
||||||
|
const render = (ops) => ops.reduce((rendered, op) => {
|
||||||
|
switch (op.type) {
|
||||||
|
case 'key':
|
||||||
|
return `${rendered ? `${rendered}.` : ''}${op.key}`;
|
||||||
|
case 'invoke':
|
||||||
|
return `${rendered}(${op.args.length > 0 ? '...' : ''})`;
|
||||||
|
default:
|
||||||
|
return rendered;
|
||||||
|
}
|
||||||
|
}, '');
|
||||||
|
|
||||||
|
function compileOp(op) {
|
||||||
|
let args;
|
||||||
|
if ('invoke' === op.type) {
|
||||||
|
args = op.args.map(compile);
|
||||||
|
}
|
||||||
|
return (context, previous, current) => {
|
||||||
|
switch (op.type) {
|
||||||
|
case 'key':
|
||||||
|
return current[op.key];
|
||||||
|
case 'invoke':
|
||||||
|
// Pass the context itself as the last arg.
|
||||||
|
const evaluated = args.map((fn) => fn(context)).concat(context);
|
||||||
|
// Promises are resolved transparently.
|
||||||
|
const apply = (args) => fastApply(previous, current, args);
|
||||||
|
return evaluated.some((arg) => arg instanceof Promise)
|
||||||
|
? Promise.all(evaluated).then(apply)
|
||||||
|
: apply(evaluated);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function disambiguateResult(value, fn) {
|
||||||
|
const apply = (value) => fn(value);
|
||||||
|
return value instanceof Promise ? value.then(apply) : apply(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function compileExpression(expression) {
|
||||||
|
const assign = 'undefined' !== typeof expression.assign
|
||||||
|
? compile(expression.assign)
|
||||||
|
: undefined;
|
||||||
|
const ops = expression.ops.map(compileOp);
|
||||||
|
const {ops: rawOps} = expression;
|
||||||
|
return (context) => {
|
||||||
|
let previous = null;
|
||||||
|
let shorted = false;
|
||||||
|
const [first, ...rest] = ops;
|
||||||
|
return rest.reduce((current, op, index) => disambiguateResult(current, (current) => {
|
||||||
|
if (shorted) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
let next = undefined;
|
||||||
|
const isLastOp = index === ops.length - 2;
|
||||||
|
if (!isLastOp || !assign) {
|
||||||
|
if ('undefined' === typeof current) {
|
||||||
|
const rendered = render(rawOps.slice(0, index + 2));
|
||||||
|
next = Promise.reject(new Error(`'${rendered}' is undefined`));
|
||||||
|
shorted = true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
next = op(context, previous, current);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const rawOp = rawOps[index + 1];
|
||||||
|
switch (rawOp.type) {
|
||||||
|
case 'key':
|
||||||
|
if ('object' === typeof current) {
|
||||||
|
current[rawOp.key] = assign(context);
|
||||||
|
next = undefined;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'invoke':
|
||||||
|
const rendered = render(rawOps.slice(0, index + 2));
|
||||||
|
next = Promise.reject(new Error(`invalid assignment to function '${rendered}'`));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
previous = current;
|
||||||
|
current = next;
|
||||||
|
return current;
|
||||||
|
}), context.getValue(rawOps[0].key));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function compileCondition(condition) {
|
||||||
|
const {operator} = condition;
|
||||||
|
const operands = condition.operands.map(compile);
|
||||||
|
return (context) => {
|
||||||
|
switch (operator) {
|
||||||
|
case 'is':
|
||||||
|
return operands[0](context) === operands[1](context);
|
||||||
|
case 'isnt':
|
||||||
|
return operands[0](context) !== operands[1](context);
|
||||||
|
case '>':
|
||||||
|
return operands[0](context) > operands[1](context);
|
||||||
|
case '>=':
|
||||||
|
return operands[0](context) >= operands[1](context);
|
||||||
|
case '<':
|
||||||
|
return operands[0](context) < operands[1](context);
|
||||||
|
case '<=':
|
||||||
|
return operands[0](context) <= operands[1](context);
|
||||||
|
case 'or':
|
||||||
|
if (0 === operands.length) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
for (let i = 0; i < operands.length; i++) {
|
||||||
|
if (!!operands[i](context)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
case 'and':
|
||||||
|
if (0 === operands.length) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
for (let i = 0; i < operands.length; i++) {
|
||||||
|
if (!operands[i](context)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
case 'contains':
|
||||||
|
const haystack = operands[0](context);
|
||||||
|
const needle = operands[1](context);
|
||||||
|
return -1 !== haystack.indexOf(needle);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function behaviorCompilers() {
|
||||||
|
return {
|
||||||
|
condition: (condition) => compileCondition(condition),
|
||||||
|
literal: ({value}) => (context) => value,
|
||||||
|
expression: (expression) => compileExpression(expression),
|
||||||
|
expressions: ({expressions}) => () => expressions.map(compileExpression),
|
||||||
|
};
|
||||||
|
}
|
74
packages/behavior/context.js
Normal file
74
packages/behavior/context.js
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
import {arrayUnique, flatten, mapObject} from '@avocado/core';
|
||||||
|
import memoize from 'lodash.memoize';
|
||||||
|
import {invokeHookFlat, registerHooks} from 'scwp';
|
||||||
|
|
||||||
|
export const globals = memoize(() => (
|
||||||
|
invokeHookFlat('behaviorContextGlobals')
|
||||||
|
.reduce((r, results) => ({
|
||||||
|
...r,
|
||||||
|
...results,
|
||||||
|
}), {})
|
||||||
|
));
|
||||||
|
|
||||||
|
export default class Context {
|
||||||
|
|
||||||
|
constructor(defaults = {}) {
|
||||||
|
this.map = new Map();
|
||||||
|
this.clear();
|
||||||
|
this.addObjectMap(defaults);
|
||||||
|
}
|
||||||
|
|
||||||
|
add(key, value, type = 'undefined') {
|
||||||
|
this.map.set(key, [value, type]);
|
||||||
|
}
|
||||||
|
|
||||||
|
addObjectMap(map) {
|
||||||
|
Object.entries(map)
|
||||||
|
.forEach(([key, [variable, type]]) => (
|
||||||
|
this.add(key, variable, type)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
all() {
|
||||||
|
return Array.from(this.map.keys())
|
||||||
|
.reduce((r, key) => ({
|
||||||
|
...r,
|
||||||
|
[key]: this.get(key),
|
||||||
|
}), {});
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this.destroy();
|
||||||
|
this.add('context', this, 'context');
|
||||||
|
this.addObjectMap(globals());
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
this.map.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
get(key) {
|
||||||
|
return this.has(key) ? this.map.get(key) : [undefined, 'undefined'];
|
||||||
|
}
|
||||||
|
|
||||||
|
getValue(key) {
|
||||||
|
return this.get(key)[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
getType(key) {
|
||||||
|
return this.get(key)[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
has(key) {
|
||||||
|
return this.map.has(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
registerHooks({
|
||||||
|
autoreg$accept: (type, M) => {
|
||||||
|
if ('hooks' === type && 'behaviorContextGlobals' in M) {
|
||||||
|
globals.cache.clear();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}, module.id);
|
|
@ -1,34 +0,0 @@
|
||||||
import {Context} from './context';
|
|
||||||
|
|
||||||
export function behaviorContextTypes() {
|
|
||||||
return {
|
|
||||||
bool: {
|
|
||||||
defaultLiteral: true,
|
|
||||||
},
|
|
||||||
context: {
|
|
||||||
children: {
|
|
||||||
add: {
|
|
||||||
type: 'void',
|
|
||||||
label: 'Add $2 as $1.',
|
|
||||||
args: [
|
|
||||||
['key', {
|
|
||||||
type: 'string',
|
|
||||||
}],
|
|
||||||
['value', {
|
|
||||||
type: 'any',
|
|
||||||
}],
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
number: {
|
|
||||||
defaultLiteral: 0,
|
|
||||||
},
|
|
||||||
stream: {},
|
|
||||||
string: {
|
|
||||||
defaultLiteral: '',
|
|
||||||
},
|
|
||||||
void: {},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,237 +0,0 @@
|
||||||
import {arrayUnique, flatten, mapObject} from '@avocado/core';
|
|
||||||
import D from 'debug';
|
|
||||||
import {invokeHookFlat} from 'scwp';
|
|
||||||
|
|
||||||
import {fastApply, objectFromEntries} from '@avocado/core';
|
|
||||||
|
|
||||||
const debug = D('@avocado:behavior:context');
|
|
||||||
|
|
||||||
export class Context {
|
|
||||||
|
|
||||||
constructor(defaults = {}) {
|
|
||||||
this.typeMap = new Map();
|
|
||||||
this.variableMap = new Map();
|
|
||||||
this.initialize();
|
|
||||||
this.addObjectMap(defaults);
|
|
||||||
}
|
|
||||||
|
|
||||||
static allTypesDigraph() {
|
|
||||||
return Object.keys(this.types())
|
|
||||||
.reduce((r, type) => ({...r, [type]: this.typeDigraph(type)}), {});
|
|
||||||
}
|
|
||||||
|
|
||||||
static allTypesInvertedDigraph() {
|
|
||||||
const digraph = this.allTypesDigraph();
|
|
||||||
let inverted = {};
|
|
||||||
const fromTypes = Object.keys(digraph);
|
|
||||||
for (let i = 0; i < fromTypes.length; i++) {
|
|
||||||
const fromType = fromTypes[i];
|
|
||||||
const toTypes = digraph[fromType];
|
|
||||||
for (let j = 0; j < toTypes.length; j++) {
|
|
||||||
const toType = toTypes[j];
|
|
||||||
inverted[toType] = (inverted[toType] || {});
|
|
||||||
inverted[toType][fromType] = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
inverted = mapObject(inverted, (types) => Object.keys(types));
|
|
||||||
inverted = mapObject(
|
|
||||||
inverted,
|
|
||||||
(types) => arrayUnique(flatten(types.map((type) => (inverted[type] || []).concat(type)))),
|
|
||||||
);
|
|
||||||
return inverted;
|
|
||||||
}
|
|
||||||
|
|
||||||
static describe(description, fn) {
|
|
||||||
const {type} = description;
|
|
||||||
fn.type = type;
|
|
||||||
return fn;
|
|
||||||
}
|
|
||||||
|
|
||||||
static globals() {
|
|
||||||
return invokeHookFlat('behaviorContextGlobals')
|
|
||||||
.reduce((r, results) => ({...r, ...results}), {});
|
|
||||||
}
|
|
||||||
|
|
||||||
static typeDigraph(type, marked = {}) {
|
|
||||||
const types = this.types();
|
|
||||||
if (marked[type] || !types[type]) {
|
|
||||||
marked[type] = true;
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
const description = this.typeDescription(type, undefined);
|
|
||||||
const subtypes = Object.keys(
|
|
||||||
Object.values(description.children)
|
|
||||||
.reduce((r, spec) => ({...r, [spec.type]: true}), {})
|
|
||||||
);
|
|
||||||
subtypes.forEach((type) => marked[type] = true);
|
|
||||||
return arrayUnique(
|
|
||||||
flatten(
|
|
||||||
subtypes.map(
|
|
||||||
(type) => this.typeDigraph(type, marked),
|
|
||||||
),
|
|
||||||
).concat(subtypes),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
static typeDescription(type, variable) {
|
|
||||||
const types = this.types();
|
|
||||||
if (!types[type]) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
children: {},
|
|
||||||
type,
|
|
||||||
...('function' === typeof types[type] ? types[type](variable) : types[type])
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
static types() {
|
|
||||||
return invokeHookFlat('behaviorContextTypes')
|
|
||||||
.reduce((r, results) => ({
|
|
||||||
...r,
|
|
||||||
...results,
|
|
||||||
}), {});
|
|
||||||
}
|
|
||||||
|
|
||||||
add(key, value, type) {
|
|
||||||
this.typeMap.set(key, type);
|
|
||||||
this.variableMap.set(key, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
addObjectMap(map) {
|
|
||||||
Object.entries(map).forEach(([key, [variable, type]]) => this.add(key, variable, type));
|
|
||||||
}
|
|
||||||
|
|
||||||
all() {
|
|
||||||
return Array.from(this.variableMap.keys())
|
|
||||||
.reduce((r, key) => ({...r, [key]: this.get(key)}), {});
|
|
||||||
}
|
|
||||||
|
|
||||||
initialize() {
|
|
||||||
this.typeMap.clear()
|
|
||||||
this.variableMap.clear()
|
|
||||||
this.add('context', this, 'context');
|
|
||||||
this.addObjectMap(this.constructor.globals());
|
|
||||||
}
|
|
||||||
|
|
||||||
clear() {
|
|
||||||
this.initialize();
|
|
||||||
}
|
|
||||||
|
|
||||||
destroy() {
|
|
||||||
this.typeMap.clear()
|
|
||||||
this.variableMap.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
get(key) {
|
|
||||||
const type = this.typeMap.has(key) ? this.typeMap.get(key) : 'undefined';
|
|
||||||
return [
|
|
||||||
this.variableMap.get(key),
|
|
||||||
type,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
static renderSteps(steps) {
|
|
||||||
return steps.reduce((rendered, step) => {
|
|
||||||
switch (step.type) {
|
|
||||||
case 'key':
|
|
||||||
if (rendered) {
|
|
||||||
rendered += '.';
|
|
||||||
}
|
|
||||||
rendered += step.key;
|
|
||||||
break;
|
|
||||||
case 'invoke':
|
|
||||||
rendered += `(${step.args.length > 0 ? '...' : ''})`;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return rendered;
|
|
||||||
}, '');
|
|
||||||
}
|
|
||||||
|
|
||||||
static renderStepsUntilNow(steps, step) {
|
|
||||||
const stepsUntilNow = steps.slice(0, steps.indexOf(step));
|
|
||||||
return this.renderSteps(stepsUntilNow);
|
|
||||||
}
|
|
||||||
|
|
||||||
takeStep(value, fn) {
|
|
||||||
if (value instanceof Promise) {
|
|
||||||
return value.then((value) => {
|
|
||||||
return fn(value);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return fn(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
traverse(traversal) {
|
|
||||||
const {steps, value} = traversal;
|
|
||||||
return this.traverseAndDo(steps, (node, previousNode, step, index) => {
|
|
||||||
const isLastStep = index === steps.length - 2;
|
|
||||||
// Traverse if we're not at the final step with a value.
|
|
||||||
if (!isLastStep || !value) {
|
|
||||||
return this.traverseOneStep(steps, previousNode, node, step);
|
|
||||||
}
|
|
||||||
// Try to set the value.
|
|
||||||
switch (step.type) {
|
|
||||||
case 'key':
|
|
||||||
if ('object' === typeof node) {
|
|
||||||
return node[step.key] = value.get(this);
|
|
||||||
}
|
|
||||||
case 'invoke':
|
|
||||||
const rendered = this.constructor.renderStepsUntilNow(steps, step);
|
|
||||||
debug(`invalid assignment to function "${rendered}"`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
traverseAndDo(steps, fn) {
|
|
||||||
const [first, ...rest] = steps;
|
|
||||||
if ('key' !== first.type) {
|
|
||||||
throw new TypeError(`First step in a traversal must be type "key"`);
|
|
||||||
}
|
|
||||||
let previousNode = null;
|
|
||||||
return rest.reduce((node, step, index) => {
|
|
||||||
return this.takeStep(node, (node) => {
|
|
||||||
const result = fn(node, previousNode, step, index);
|
|
||||||
previousNode = node;
|
|
||||||
return result;
|
|
||||||
});
|
|
||||||
}, this.variableMap.get(first.key));
|
|
||||||
}
|
|
||||||
|
|
||||||
traverseOneStep(steps, previousNode, node, step) {
|
|
||||||
if ('undefined' === typeof node) {
|
|
||||||
const rendered = this.constructor.renderStepsUntilNow(steps, step);
|
|
||||||
debug(`"${rendered}" is traversed through but undefined`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
switch (step.type) {
|
|
||||||
case 'key':
|
|
||||||
return node[step.key];
|
|
||||||
case 'invoke':
|
|
||||||
const args = step.args.map((arg) => arg.get(this));
|
|
||||||
// Pass the context itself as the last arg.
|
|
||||||
args.push(this);
|
|
||||||
// Any arg promises will be resolved; the arg values will be passed
|
|
||||||
// transparently to the invocation.
|
|
||||||
let hasPromise = false;
|
|
||||||
for (let i = 0; i < args.length; i++) {
|
|
||||||
if (args[i] instanceof Promise) {
|
|
||||||
hasPromise = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (hasPromise) {
|
|
||||||
return Promise.all(args).then((args) => {
|
|
||||||
return fastApply(previousNode, node, args);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return fastApply(previousNode, node, args);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,65 +0,0 @@
|
||||||
import {Context} from './context';
|
|
||||||
|
|
||||||
class Flow {
|
|
||||||
|
|
||||||
static conditional(condition, actions, context) {
|
|
||||||
if (condition.get(context)) {
|
|
||||||
return actions.serial(context);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
static parallel(actions, context) {
|
|
||||||
return actions.parallel(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
static serial(actions, context) {
|
|
||||||
return actions.serial(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export function behaviorContextGlobals() {
|
|
||||||
return {
|
|
||||||
Flow: [Flow, 'Flow'],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function behaviorContextTypes() {
|
|
||||||
return {
|
|
||||||
Flow: {
|
|
||||||
children: {
|
|
||||||
conditional: {
|
|
||||||
type: 'bool',
|
|
||||||
label: 'If $1 then run $2.',
|
|
||||||
args: [
|
|
||||||
['condition', {
|
|
||||||
type: 'condition',
|
|
||||||
}],
|
|
||||||
['actions', {
|
|
||||||
type: 'actions',
|
|
||||||
}],
|
|
||||||
],
|
|
||||||
},
|
|
||||||
parallel: {
|
|
||||||
type: 'void',
|
|
||||||
label: 'Run $1 in parallel.',
|
|
||||||
args: [
|
|
||||||
['actions', {
|
|
||||||
type: 'actions',
|
|
||||||
}],
|
|
||||||
],
|
|
||||||
},
|
|
||||||
serial: {
|
|
||||||
type: 'void',
|
|
||||||
label: 'Run $1 serially.',
|
|
||||||
args: [
|
|
||||||
['actions', {
|
|
||||||
type: 'actions',
|
|
||||||
}],
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
export {Context} from './context';
|
|
|
@ -1,41 +0,0 @@
|
||||||
import {TickingPromise} from '@avocado/core';
|
|
||||||
|
|
||||||
class Timing {
|
|
||||||
|
|
||||||
static wait (duration) {
|
|
||||||
return new TickingPromise(
|
|
||||||
() => {},
|
|
||||||
(elapsed, resolve) => {
|
|
||||||
duration -= elapsed;
|
|
||||||
if (duration <= 0) {
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export function behaviorContextTypes() {
|
|
||||||
return {
|
|
||||||
Timing: {
|
|
||||||
children: {
|
|
||||||
wait: {
|
|
||||||
type: 'void',
|
|
||||||
label: 'Wait for $1 seconds.',
|
|
||||||
args: [
|
|
||||||
['duration', {
|
|
||||||
type: 'number',
|
|
||||||
}],
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function behaviorContextGlobals() {
|
|
||||||
return {
|
|
||||||
Timing: [Timing, 'Timing'],
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,68 +0,0 @@
|
||||||
import {merge as mergeObject} from '@avocado/core';
|
|
||||||
|
|
||||||
class Utility {
|
|
||||||
|
|
||||||
static makeArray(...args) {
|
|
||||||
// No context!
|
|
||||||
args.pop();
|
|
||||||
return args;
|
|
||||||
};
|
|
||||||
|
|
||||||
static makeObject(...args) {
|
|
||||||
// No context!
|
|
||||||
args.pop();
|
|
||||||
const object = {};
|
|
||||||
while (args.length > 0) {
|
|
||||||
const key = args.shift();
|
|
||||||
const value = args.shift();
|
|
||||||
object[key] = value;
|
|
||||||
}
|
|
||||||
return object;
|
|
||||||
};
|
|
||||||
|
|
||||||
static log(...args) {
|
|
||||||
return console.log(...args);
|
|
||||||
}
|
|
||||||
|
|
||||||
static merge(...args) {
|
|
||||||
// No context!
|
|
||||||
args.pop();
|
|
||||||
return mergeObject(...args);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export function behaviorContextTypes() {
|
|
||||||
return {
|
|
||||||
Utility: {
|
|
||||||
children: {
|
|
||||||
makeArray: {
|
|
||||||
type: 'array',
|
|
||||||
label: 'Make array.',
|
|
||||||
args: [],
|
|
||||||
},
|
|
||||||
makeObject: {
|
|
||||||
type: 'object',
|
|
||||||
label: 'Make object.',
|
|
||||||
args: [],
|
|
||||||
},
|
|
||||||
log: {
|
|
||||||
type: 'void',
|
|
||||||
label: 'Log.',
|
|
||||||
args: [],
|
|
||||||
},
|
|
||||||
merge: {
|
|
||||||
type: 'object',
|
|
||||||
label: 'Merge objects.',
|
|
||||||
args: [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function behaviorContextGlobals() {
|
|
||||||
return {
|
|
||||||
Utility: [Utility, 'Utility'],
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,10 +1,29 @@
|
||||||
export {Context} from './context';
|
|
||||||
export {
|
export {
|
||||||
fromJSON as behaviorItemFromJSON,
|
default as Actions,
|
||||||
} from './item/registry';
|
} from './actions';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
buildCondition,
|
buildCondition,
|
||||||
buildInvoke,
|
buildInvoke,
|
||||||
buildTraversal,
|
buildExpression,
|
||||||
buildValue,
|
buildValue,
|
||||||
} from './builders';
|
} from './builders';
|
||||||
|
|
||||||
|
export {
|
||||||
|
default as compile,
|
||||||
|
compilers,
|
||||||
|
} from './compile';
|
||||||
|
|
||||||
|
export {
|
||||||
|
default as Context,
|
||||||
|
globals as contextGlobals,
|
||||||
|
} from './context';
|
||||||
|
|
||||||
|
export {
|
||||||
|
candidates,
|
||||||
|
description,
|
||||||
|
digraph,
|
||||||
|
fitsInto,
|
||||||
|
invertedDigraph,
|
||||||
|
types,
|
||||||
|
} from './type';
|
||||||
|
|
|
@ -1,12 +0,0 @@
|
||||||
import {Traversal} from './traversal';
|
|
||||||
|
|
||||||
export class Action extends Traversal {
|
|
||||||
|
|
||||||
toJSON() {
|
|
||||||
return {
|
|
||||||
...super.toJSON(),
|
|
||||||
type: 'action',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,93 +0,0 @@
|
||||||
import {expect} from 'chai';
|
|
||||||
|
|
||||||
import {TickingPromise} from '@avocado/core';
|
|
||||||
|
|
||||||
import {buildInvoke, buildTraversal, buildValue} from '../builders';
|
|
||||||
import {Context} from '../context';
|
|
||||||
import {Actions} from './actions';
|
|
||||||
import './initialize'
|
|
||||||
|
|
||||||
describe('behavior', () => {
|
|
||||||
describe('Actions', () => {
|
|
||||||
const context = new Context();
|
|
||||||
beforeEach(() => {
|
|
||||||
context.clear();
|
|
||||||
});
|
|
||||||
it('may resolve immediately', (done) => {
|
|
||||||
const actions = new Actions();
|
|
||||||
let total = 0;
|
|
||||||
const increment = () => total++;
|
|
||||||
context.add('increment', increment);
|
|
||||||
actions.fromJSON({
|
|
||||||
type: 'actions',
|
|
||||||
traversals: [
|
|
||||||
buildTraversal(['increment']),
|
|
||||||
buildTraversal(['increment']),
|
|
||||||
buildTraversal(['increment']),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
actions.on('actionsFinished', done);
|
|
||||||
actions.tick(context, 0);
|
|
||||||
expect(actions.index).to.equal(0);
|
|
||||||
expect(total).to.equal(3);
|
|
||||||
});
|
|
||||||
it('may defer', async () => {
|
|
||||||
const actions = new Actions();
|
|
||||||
let resolve;
|
|
||||||
const promise = new Promise((_) => resolve = _);
|
|
||||||
context.add('test', promise);
|
|
||||||
context.add('test2', new Promise(() => {}));
|
|
||||||
actions.fromJSON({
|
|
||||||
type: 'actions',
|
|
||||||
traversals: [
|
|
||||||
buildTraversal(['test']),
|
|
||||||
buildTraversal(['test2']),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
actions.tick(context, 0);
|
|
||||||
// Waiting?
|
|
||||||
expect(actions.index).to.equal(0);
|
|
||||||
resolve();
|
|
||||||
return promise.finally(() => {
|
|
||||||
// Waiting on next...
|
|
||||||
expect(actions.index).to.equal(1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
it('may defer with ticking', async () => {
|
|
||||||
const actions = new Actions();
|
|
||||||
let resolve;
|
|
||||||
let duration = 1;
|
|
||||||
const tickingPromise = new TickingPromise(
|
|
||||||
() => {},
|
|
||||||
(elapsed, resolve) => {
|
|
||||||
duration -= elapsed;
|
|
||||||
if (duration <= 0) {
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
context.add('test', tickingPromise);
|
|
||||||
context.add('test2', new Promise(() => {}));
|
|
||||||
actions.fromJSON({
|
|
||||||
type: 'actions',
|
|
||||||
traversals: [
|
|
||||||
buildTraversal(['test']),
|
|
||||||
buildTraversal(['test2']),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
actions.tick(context, 0);
|
|
||||||
// Waiting?
|
|
||||||
expect(actions.index).to.equal(0);
|
|
||||||
actions.tick(context, 0.5);
|
|
||||||
expect(actions.index).to.equal(0);
|
|
||||||
actions.tick(context, 0.5);
|
|
||||||
// Still gotta be waiting...
|
|
||||||
expect(actions.index).to.equal(0);
|
|
||||||
return tickingPromise.finally(() => {
|
|
||||||
// Waiting on next...
|
|
||||||
expect(duration).to.equal(0);
|
|
||||||
expect(actions.index).to.equal(1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,46 +0,0 @@
|
||||||
import {fromJSON as behaviorItemFromJSON} from './registry';
|
|
||||||
|
|
||||||
export function Collection(type) {
|
|
||||||
const plural = `${type}s`;
|
|
||||||
return class Collection {
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this[plural] = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
clone(other) {
|
|
||||||
if (0 === other[plural].length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const Item = other[plural][0].constructor;
|
|
||||||
for (let i = 0; i < other[plural].length; ++i) {
|
|
||||||
this[plural][i] = new Item();
|
|
||||||
this[plural][i].clone(other[plural][i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
createClone() {
|
|
||||||
const Items = this.constructor;
|
|
||||||
const items = new Items();
|
|
||||||
items.clone(this);
|
|
||||||
return items;
|
|
||||||
}
|
|
||||||
|
|
||||||
count() {
|
|
||||||
return this[plural].length;
|
|
||||||
}
|
|
||||||
|
|
||||||
fromJSON(json) {
|
|
||||||
this[plural] = [];
|
|
||||||
for (const item of json[plural]) {
|
|
||||||
this[plural].push(behaviorItemFromJSON(item));
|
|
||||||
}
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
toJSON() {
|
|
||||||
return this[plural].map((item) => item.toJSON());
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,106 +0,0 @@
|
||||||
import {fromJSON as behaviorItemFromJSON} from './registry';
|
|
||||||
|
|
||||||
export class Condition {
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.operator = '';
|
|
||||||
this.operands = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
check(context) {
|
|
||||||
return this.get(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
clone(other) {
|
|
||||||
this.operator = other.operator;
|
|
||||||
this.operands = other.operands.map((operand) => operand.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
fromJSON(json) {
|
|
||||||
this.operator = json.operator;
|
|
||||||
this.operands = json.operands.map((operand) => {
|
|
||||||
return behaviorItemFromJSON(operand);
|
|
||||||
});
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
get(context) {
|
|
||||||
switch (this.operator) {
|
|
||||||
case 'is':
|
|
||||||
return this.operands[0].get(context) === this.operands[1].get(context);
|
|
||||||
case 'isnt':
|
|
||||||
return this.operands[0].get(context) !== this.operands[1].get(context);
|
|
||||||
case '>':
|
|
||||||
return this.operands[0].get(context) > this.operands[1].get(context);
|
|
||||||
case '>=':
|
|
||||||
return this.operands[0].get(context) >= this.operands[1].get(context);
|
|
||||||
case '<':
|
|
||||||
return this.operands[0].get(context) < this.operands[1].get(context);
|
|
||||||
case '<=':
|
|
||||||
return this.operands[0].get(context) <= this.operands[1].get(context);
|
|
||||||
|
|
||||||
case 'or':
|
|
||||||
if (0 === this.operands.length) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
for (const operand of this.operands) {
|
|
||||||
if (!!operand.get(context)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
|
|
||||||
case 'and':
|
|
||||||
if (0 === this.operands.length) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
for (const operand of this.operands) {
|
|
||||||
if (!operand.get(context)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
|
|
||||||
case 'contains':
|
|
||||||
if (this.operands.length < 2) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const haystack = this.operands[0].get(context);
|
|
||||||
const needle = this.operands[1].get(context);
|
|
||||||
return -1 !== haystack.indexOf(needle);
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
operandCount() {
|
|
||||||
return this.operands.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
operand(index) {
|
|
||||||
return this.operands[index];
|
|
||||||
}
|
|
||||||
|
|
||||||
operands() {
|
|
||||||
return this.operands;
|
|
||||||
}
|
|
||||||
|
|
||||||
operator() {
|
|
||||||
return this.operator;
|
|
||||||
}
|
|
||||||
|
|
||||||
setOperand(index, operand) {
|
|
||||||
this.operands[index] = operand;
|
|
||||||
}
|
|
||||||
|
|
||||||
setOperator(operator) {
|
|
||||||
this.operator = operator;
|
|
||||||
}
|
|
||||||
|
|
||||||
toJSON() {
|
|
||||||
return {
|
|
||||||
type: 'condition',
|
|
||||||
operator: this.operator,
|
|
||||||
operands: this.operands.map((operand) => operand.toJSON()),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,109 +0,0 @@
|
||||||
import {expect} from 'chai';
|
|
||||||
|
|
||||||
import {Condition} from './condition';
|
|
||||||
import {Literal} from './literal';
|
|
||||||
import {deregister, register} from './registry';
|
|
||||||
|
|
||||||
describe('behavior', () => {
|
|
||||||
describe('condition', () => {
|
|
||||||
it('can do binary compares', () => {
|
|
||||||
const condition = new Condition();
|
|
||||||
condition.setOperand(0, new Literal().fromJSON({
|
|
||||||
type: 'literal',
|
|
||||||
value: 500,
|
|
||||||
}));
|
|
||||||
condition.setOperand(1, new Literal().fromJSON({
|
|
||||||
type: 'literal',
|
|
||||||
value: 420,
|
|
||||||
}));
|
|
||||||
// Greater than.
|
|
||||||
condition.setOperator('>');
|
|
||||||
expect(condition.check()).to.be.true;
|
|
||||||
// Less than.
|
|
||||||
condition.setOperator('<');
|
|
||||||
expect(condition.check()).to.be.false;
|
|
||||||
// Greater than or equal.
|
|
||||||
condition.setOperator('>=');
|
|
||||||
expect(condition.check()).to.be.true;
|
|
||||||
// Less than or equal.
|
|
||||||
condition.setOperator('<=');
|
|
||||||
expect(condition.check()).to.be.false;
|
|
||||||
// Is.
|
|
||||||
condition.setOperator('is');
|
|
||||||
expect(condition.check()).to.be.false;
|
|
||||||
// Is not.
|
|
||||||
condition.setOperator('isnt');
|
|
||||||
expect(condition.check()).to.be.true;
|
|
||||||
});
|
|
||||||
it('can do varnary compares', () => {
|
|
||||||
const condition = new Condition();
|
|
||||||
condition.setOperand(0, new Literal().fromJSON({
|
|
||||||
type: 'literal',
|
|
||||||
value: true,
|
|
||||||
}));
|
|
||||||
condition.setOperand(1, new Literal().fromJSON({
|
|
||||||
type: 'literal',
|
|
||||||
value: true,
|
|
||||||
}));
|
|
||||||
condition.setOperand(2, new Literal().fromJSON({
|
|
||||||
type: 'literal',
|
|
||||||
value: false,
|
|
||||||
}));
|
|
||||||
// AND, mixed.
|
|
||||||
condition.setOperator('and');
|
|
||||||
expect(condition.check()).to.be.false;
|
|
||||||
// OR, mixed.
|
|
||||||
condition.setOperator('or');
|
|
||||||
expect(condition.check()).to.be.true;
|
|
||||||
// OR, all true.
|
|
||||||
condition.setOperand(2, new Literal().fromJSON({
|
|
||||||
type: 'literal',
|
|
||||||
value: true,
|
|
||||||
}));
|
|
||||||
expect(condition.check()).to.be.true;
|
|
||||||
// AND, all true.
|
|
||||||
condition.setOperator('and');
|
|
||||||
expect(condition.check()).to.be.true;
|
|
||||||
// AND, all false.
|
|
||||||
for (const i of [0, 1, 2]) {
|
|
||||||
condition.setOperand(i, new Literal().fromJSON({
|
|
||||||
type: 'literal',
|
|
||||||
value: false,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
expect(condition.check()).to.be.false;
|
|
||||||
// OR, all false.
|
|
||||||
condition.setOperator('or');
|
|
||||||
expect(condition.check()).to.be.false;
|
|
||||||
});
|
|
||||||
describe('JSON', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
register(Literal);
|
|
||||||
});
|
|
||||||
afterEach(() => {
|
|
||||||
deregister(Literal);
|
|
||||||
});
|
|
||||||
it('can instantiate operands', () => {
|
|
||||||
const condition = new Condition();
|
|
||||||
// Greater than.
|
|
||||||
condition.fromJSON({
|
|
||||||
operator: '>',
|
|
||||||
operands: [
|
|
||||||
{
|
|
||||||
type: 'literal',
|
|
||||||
value: 500,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'literal',
|
|
||||||
value: 420,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
expect(condition.check()).to.be.true;
|
|
||||||
// Less than.
|
|
||||||
condition.setOperator('<');
|
|
||||||
expect(condition.check()).to.be.false;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,14 +0,0 @@
|
||||||
import {Collection} from './collection';
|
|
||||||
|
|
||||||
export class Conditions extends Collection('condition') {
|
|
||||||
|
|
||||||
check(context) {
|
|
||||||
for (condition of this.conditions) {
|
|
||||||
if (!this.conditions[index].check(context)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,23 +0,0 @@
|
||||||
import {Action} from './action';
|
|
||||||
import {Actions} from './actions';
|
|
||||||
import {Condition} from './condition';
|
|
||||||
import {Conditions} from './conditions';
|
|
||||||
import {Literal} from './literal';
|
|
||||||
import {Routine} from './routine';
|
|
||||||
import {Routines} from './routines';
|
|
||||||
import {Traversal} from './traversal';
|
|
||||||
import {Traversals} from './traversals';
|
|
||||||
|
|
||||||
export function avocadoBehaviorItems() {
|
|
||||||
return {
|
|
||||||
action: Action,
|
|
||||||
actions: Actions,
|
|
||||||
condition: Condition,
|
|
||||||
conditions: Conditions,
|
|
||||||
literal: Literal,
|
|
||||||
routine: Routine,
|
|
||||||
routines: Routines,
|
|
||||||
traversal: Traversal,
|
|
||||||
traversals: Traversals,
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,30 +0,0 @@
|
||||||
export class Literal {
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.value = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
clone(other) {
|
|
||||||
this.value = other.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
fromJSON(json) {
|
|
||||||
this.value = json.value;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
get(context) {
|
|
||||||
return this.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
set(value) {
|
|
||||||
this.value = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
toJSON() {
|
|
||||||
return {
|
|
||||||
type: 'literal',
|
|
||||||
value: this.value,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,22 +0,0 @@
|
||||||
import {expect} from 'chai';
|
|
||||||
|
|
||||||
import {Literal} from './literal';
|
|
||||||
|
|
||||||
describe('behavior', () => {
|
|
||||||
describe('Literal', () => {
|
|
||||||
let literal = undefined;
|
|
||||||
beforeEach(() => {
|
|
||||||
literal = new Literal();
|
|
||||||
literal.fromJSON({
|
|
||||||
value: 69,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
it('can access literals', () => {
|
|
||||||
expect(literal.get()).to.equal(69);
|
|
||||||
});
|
|
||||||
it('can modify literals', () => {
|
|
||||||
literal.set(420);
|
|
||||||
expect(literal.get()).to.equal(420);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,26 +0,0 @@
|
||||||
import {invokeHookFlat} from 'scwp';
|
|
||||||
|
|
||||||
let _behaviorItems;
|
|
||||||
|
|
||||||
export function fromJSON({type, ...json}) {
|
|
||||||
const items = behaviorItems();
|
|
||||||
const Class = items[type];
|
|
||||||
if (!Class) {
|
|
||||||
throw new TypeError(`There is no class for the behavior item "${type}"`);
|
|
||||||
}
|
|
||||||
return (new Class()).fromJSON(json);
|
|
||||||
}
|
|
||||||
|
|
||||||
function behaviorItems() {
|
|
||||||
if (!_behaviorItems) {
|
|
||||||
_behaviorItems = {};
|
|
||||||
const itemsLists = invokeHookFlat('avocadoBehaviorItems');
|
|
||||||
for (let i = 0; i < itemsLists.length; i++) {
|
|
||||||
_behaviorItems = {
|
|
||||||
..._behaviorItems,
|
|
||||||
...itemsLists[i],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return _behaviorItems;
|
|
||||||
}
|
|
|
@ -1,29 +0,0 @@
|
||||||
import {Actions} from './actions';
|
|
||||||
|
|
||||||
export class Routine {
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.actions = new Actions();
|
|
||||||
}
|
|
||||||
|
|
||||||
clone(other) {
|
|
||||||
this.actions = other.actions.clone();
|
|
||||||
}
|
|
||||||
|
|
||||||
fromJSON(json) {
|
|
||||||
this.actions.fromJSON(json.routine);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
tick(context, elapsed) {
|
|
||||||
this.actions.tick(context, elapsed);
|
|
||||||
}
|
|
||||||
|
|
||||||
toJSON() {
|
|
||||||
return {
|
|
||||||
type: 'routine',
|
|
||||||
actions: this.actions.toJSON(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,45 +0,0 @@
|
||||||
import {Routine} from './routine';
|
|
||||||
|
|
||||||
export class Routines {
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.routines = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
*[Symbol.iterator]() {
|
|
||||||
for (const index in this.routines) {
|
|
||||||
const routine = this.routines[index];
|
|
||||||
yield routine;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
clone(other) {
|
|
||||||
for (const i in other.routines) {
|
|
||||||
const routine = other.routines[i];
|
|
||||||
this.routines[i] = routine.clone();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fromJSON(json) {
|
|
||||||
for (const i in json.routines) {
|
|
||||||
const routineJSON = json.routines[i];
|
|
||||||
this.routines[i] = (new Routine()).fromJSON(routineJSON);
|
|
||||||
}
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
routine(index) {
|
|
||||||
return this.routines[index];
|
|
||||||
}
|
|
||||||
|
|
||||||
toJSON() {
|
|
||||||
const routines = {};
|
|
||||||
for (const i in this.routines) {
|
|
||||||
routines[i] = this.routines[i].toJSON();
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
type: 'routines',
|
|
||||||
routines,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,80 +0,0 @@
|
||||||
import {fromJSON as behaviorItemFromJSON} from './registry';
|
|
||||||
|
|
||||||
export class Traversal {
|
|
||||||
|
|
||||||
constructor(json) {
|
|
||||||
this.steps = [];
|
|
||||||
this.value = undefined;
|
|
||||||
if (json) {
|
|
||||||
this.fromJSON(json);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
clone(other) {
|
|
||||||
this.steps = other.steps.map((step) => {
|
|
||||||
switch (step.type) {
|
|
||||||
case 'key':
|
|
||||||
return step;
|
|
||||||
case 'invoke':
|
|
||||||
return {
|
|
||||||
type: 'invoke',
|
|
||||||
args: step.args.map((arg, index) => {
|
|
||||||
const Item = arg.constructor;
|
|
||||||
const item = new Item();
|
|
||||||
item.clone(arg);
|
|
||||||
return item;
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (other.value) {
|
|
||||||
this.value = other.value.clone();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fromJSON(json) {
|
|
||||||
this.steps = json.steps.map((step) => {
|
|
||||||
switch (step.type) {
|
|
||||||
case 'key':
|
|
||||||
return step;
|
|
||||||
case 'invoke':
|
|
||||||
return {
|
|
||||||
type: 'invoke',
|
|
||||||
args: step.args.map((arg) => behaviorItemFromJSON(arg)),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (json.value) {
|
|
||||||
this.value = behaviorItemFromJSON(json.value);
|
|
||||||
}
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
get(context) {
|
|
||||||
return this.traverse(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
traverse(context) {
|
|
||||||
if (context) {
|
|
||||||
return context.traverse(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
toJSON() {
|
|
||||||
return {
|
|
||||||
type: 'traversal',
|
|
||||||
steps: this.steps.map((step) => {
|
|
||||||
switch (step.type) {
|
|
||||||
case 'key':
|
|
||||||
return step;
|
|
||||||
case 'invoke':
|
|
||||||
return {
|
|
||||||
type: 'invoke',
|
|
||||||
args: step.args.map((arg) => arg.toJSON()),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
value: this.value ? this.value.toJSON() : undefined,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,21 +0,0 @@
|
||||||
import {TickingPromise} from '@avocado/core';
|
|
||||||
|
|
||||||
import {Collection} from './collection';
|
|
||||||
import {Traversal} from './traversal';
|
|
||||||
|
|
||||||
export class Traversals extends Collection('traversal') {
|
|
||||||
|
|
||||||
parallel(context) {
|
|
||||||
const results = this.traversals.map((traversal) => {
|
|
||||||
return traversal.traverse(context);
|
|
||||||
});
|
|
||||||
// Early out if no promises.
|
|
||||||
if (!results.reduce((has, result) => {
|
|
||||||
return has || result instanceof Promise;
|
|
||||||
}, false)) {
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
return TickingPromise.all(results);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,8 +1,9 @@
|
||||||
import {compose, flatten} from '@avocado/core';
|
import {compose, flatten, mapObject} from '@avocado/core';
|
||||||
import {StateProperty, Trait} from '@avocado/entity';
|
import {StateProperty, Trait} from '@avocado/entity';
|
||||||
|
|
||||||
import {Context} from '../context';
|
import Actions from '../actions';
|
||||||
import {Routines} from '../item/routines';
|
import compile from '../compile';
|
||||||
|
import Context from '../context';
|
||||||
|
|
||||||
const decorate = compose(
|
const decorate = compose(
|
||||||
StateProperty('currentRoutine', {
|
StateProperty('currentRoutine', {
|
||||||
|
@ -13,7 +14,7 @@ const decorate = compose(
|
||||||
|
|
||||||
export default class Behaved extends decorate(Trait) {
|
export default class Behaved extends decorate(Trait) {
|
||||||
|
|
||||||
static behaviorContextTypes() {
|
static behaviorTypes() {
|
||||||
return {
|
return {
|
||||||
context: {
|
context: {
|
||||||
type: 'context',
|
type: 'context',
|
||||||
|
@ -67,7 +68,10 @@ export default class Behaved extends decorate(Trait) {
|
||||||
entity: [this.entity, 'entity'],
|
entity: [this.entity, 'entity'],
|
||||||
});
|
});
|
||||||
this._currentRoutine = undefined;
|
this._currentRoutine = undefined;
|
||||||
this._routines = (new Routines()).fromJSON(this.params.routines);
|
this._routines = mapObject(
|
||||||
|
this.params.routines,
|
||||||
|
(routine) => new Actions(compile(routine))
|
||||||
|
);
|
||||||
this.updateCurrentRoutine(this.state.currentRoutine);
|
this.updateCurrentRoutine(this.state.currentRoutine);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -82,7 +86,7 @@ export default class Behaved extends decorate(Trait) {
|
||||||
}
|
}
|
||||||
|
|
||||||
updateCurrentRoutine(currentRoutine) {
|
updateCurrentRoutine(currentRoutine) {
|
||||||
this._currentRoutine = this._routines.routine(currentRoutine);
|
this._currentRoutine = this._routines[currentRoutine];
|
||||||
}
|
}
|
||||||
|
|
||||||
listeners() {
|
listeners() {
|
||||||
|
|
108
packages/behavior/type.js
Normal file
108
packages/behavior/type.js
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
import {arrayUnique, flatten, mapObject} from '@avocado/core';
|
||||||
|
import memoize from 'lodash.memoize';
|
||||||
|
import {invokeHookFlat, registerHooks} from 'scwp';
|
||||||
|
|
||||||
|
export const types = memoize(() => {
|
||||||
|
return invokeHookFlat('behaviorTypes')
|
||||||
|
.reduce((r, results) => ({
|
||||||
|
...r,
|
||||||
|
...results,
|
||||||
|
}), {});
|
||||||
|
});
|
||||||
|
|
||||||
|
function typeDigraph(type, marked = {}) {
|
||||||
|
const allTypes = types();
|
||||||
|
if (marked[type] || !allTypes[type]) {
|
||||||
|
marked[type] = true;
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const description = description(type, undefined);
|
||||||
|
const subtypes = Object.keys(
|
||||||
|
Object.values(description.children)
|
||||||
|
.reduce((r, {type: childType}) => ({
|
||||||
|
...r,
|
||||||
|
[childType]: true,
|
||||||
|
}), {})
|
||||||
|
);
|
||||||
|
subtypes.forEach((type) => marked[type] = true);
|
||||||
|
return arrayUnique(flatten(subtypes.map((type) => typeDigraph(type, marked))).concat(subtypes));
|
||||||
|
}
|
||||||
|
|
||||||
|
export const digraph = memoize(() => {
|
||||||
|
return Object.keys(allTypes())
|
||||||
|
.reduce((r, type) => ({
|
||||||
|
...r,
|
||||||
|
[type]: typeDigraph(type),
|
||||||
|
}), {});
|
||||||
|
});
|
||||||
|
|
||||||
|
export const invertedDigraph = memoize(() => {
|
||||||
|
const digraph = digraph();
|
||||||
|
let inverted = {};
|
||||||
|
const fromTypes = Object.keys(digraph);
|
||||||
|
for (let i = 0; i < fromTypes.length; i++) {
|
||||||
|
const fromType = fromTypes[i];
|
||||||
|
const toTypes = digraph[fromType];
|
||||||
|
for (let j = 0; j < toTypes.length; j++) {
|
||||||
|
const toType = toTypes[j];
|
||||||
|
inverted[toType] = (inverted[toType] || {});
|
||||||
|
inverted[toType][fromType] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
inverted = mapObject(inverted, (types) => Object.keys(types));
|
||||||
|
inverted = mapObject(inverted, (types) => (
|
||||||
|
arrayUnique(flatten(types.map((type) => (inverted[type] || []).concat(type))))
|
||||||
|
));
|
||||||
|
return inverted;
|
||||||
|
});
|
||||||
|
|
||||||
|
export const candidates = (description, type) => {
|
||||||
|
const inverted = invertedDigraph();
|
||||||
|
const types = (inverted[type] || []).concat(type);
|
||||||
|
const {children} = description;
|
||||||
|
return 'any' === type
|
||||||
|
? Object.keys(children)
|
||||||
|
: Object.entries(children)
|
||||||
|
.reduce((r, [key, {type}]) => (
|
||||||
|
r.concat(
|
||||||
|
types.some((candidate) => typeFits(candidate, type))
|
||||||
|
? [key]
|
||||||
|
: []
|
||||||
|
)
|
||||||
|
), []);
|
||||||
|
};
|
||||||
|
|
||||||
|
export function description(type, instance) {
|
||||||
|
const allTypes = types();
|
||||||
|
const defaults = {
|
||||||
|
children: {},
|
||||||
|
type,
|
||||||
|
};
|
||||||
|
if (!allTypes[type]) {
|
||||||
|
return {
|
||||||
|
...defaults,
|
||||||
|
type: 'undefined',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...defaults,
|
||||||
|
...('function' === typeof allTypes[type] ? allTypes[type](variable) : allTypes[type])
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fitsInto(candidate, reference) {
|
||||||
|
if ('any' === reference) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return -1 !== reference.split('|').map((type) => type.trim()).indexOf(candidate);
|
||||||
|
}
|
||||||
|
|
||||||
|
registerHooks({
|
||||||
|
autoreg$accept: (type, M) => {
|
||||||
|
if ('hooks' === type && 'behaviorTypes' in M) {
|
||||||
|
allTypes.cache.clear();
|
||||||
|
digraph.cache.clear();
|
||||||
|
invertedDigraph.cache.clear();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}, module.id);
|
114
packages/behavior/types.hooks.js
Normal file
114
packages/behavior/types.hooks.js
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
import Flow from './types/flow';
|
||||||
|
import Timing from './types/timing';
|
||||||
|
import Utility from './types/utility';
|
||||||
|
|
||||||
|
export function behaviorContextGlobals() {
|
||||||
|
return {
|
||||||
|
Flow: [Flow, 'Flow'],
|
||||||
|
Timing: [Timing, 'Timing'],
|
||||||
|
Utility: [Utility, 'Utility'],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function behaviorTypes() {
|
||||||
|
return {
|
||||||
|
bool: {
|
||||||
|
defaultLiteral: true,
|
||||||
|
},
|
||||||
|
context: {
|
||||||
|
children: {
|
||||||
|
add: {
|
||||||
|
type: 'void',
|
||||||
|
label: 'Add $2 as $1.',
|
||||||
|
args: [
|
||||||
|
['key', {
|
||||||
|
type: 'string',
|
||||||
|
}],
|
||||||
|
['value', {
|
||||||
|
type: 'any',
|
||||||
|
}],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Flow: {
|
||||||
|
children: {
|
||||||
|
conditional: {
|
||||||
|
type: 'bool',
|
||||||
|
label: 'If $1 then run $2.',
|
||||||
|
args: [
|
||||||
|
['condition', {
|
||||||
|
type: 'condition',
|
||||||
|
}],
|
||||||
|
['actions', {
|
||||||
|
type: 'actions',
|
||||||
|
}],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
parallel: {
|
||||||
|
type: 'void',
|
||||||
|
label: 'Run $1 in parallel.',
|
||||||
|
args: [
|
||||||
|
['actions', {
|
||||||
|
type: 'actions',
|
||||||
|
}],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
serial: {
|
||||||
|
type: 'void',
|
||||||
|
label: 'Run $1 serially.',
|
||||||
|
args: [
|
||||||
|
['actions', {
|
||||||
|
type: 'actions',
|
||||||
|
}],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
number: {
|
||||||
|
defaultLiteral: 0,
|
||||||
|
},
|
||||||
|
stream: {},
|
||||||
|
string: {
|
||||||
|
defaultLiteral: '',
|
||||||
|
},
|
||||||
|
Timing: {
|
||||||
|
children: {
|
||||||
|
wait: {
|
||||||
|
type: 'void',
|
||||||
|
label: 'Wait for $1 seconds.',
|
||||||
|
args: [
|
||||||
|
['duration', {
|
||||||
|
type: 'number',
|
||||||
|
}],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Utility: {
|
||||||
|
children: {
|
||||||
|
makeArray: {
|
||||||
|
type: 'array',
|
||||||
|
label: 'Make array.',
|
||||||
|
args: [],
|
||||||
|
},
|
||||||
|
makeObject: {
|
||||||
|
type: 'object',
|
||||||
|
label: 'Make object.',
|
||||||
|
args: [],
|
||||||
|
},
|
||||||
|
log: {
|
||||||
|
type: 'void',
|
||||||
|
label: 'Log.',
|
||||||
|
args: [],
|
||||||
|
},
|
||||||
|
merge: {
|
||||||
|
type: 'object',
|
||||||
|
label: 'Merge objects.',
|
||||||
|
args: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
void: {},
|
||||||
|
};
|
||||||
|
}
|
19
packages/behavior/types/flow.js
Normal file
19
packages/behavior/types/flow.js
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import Actions from '../actions';
|
||||||
|
|
||||||
|
export default class Flow {
|
||||||
|
|
||||||
|
static conditional(condition, expressions, context) {
|
||||||
|
if (condition.get(context)) {
|
||||||
|
return (new Actions(expressions)).serial(context);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
static parallel(expressions, context) {
|
||||||
|
return (new Actions(expressions)).parallel(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
static serial(expressions, context) {
|
||||||
|
return (new Actions(expressions)).serial(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
17
packages/behavior/types/timing.js
Normal file
17
packages/behavior/types/timing.js
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import {TickingPromise} from '@avocado/core';
|
||||||
|
|
||||||
|
export default class Timing {
|
||||||
|
|
||||||
|
static wait (duration) {
|
||||||
|
return new TickingPromise(
|
||||||
|
() => {},
|
||||||
|
(elapsed, resolve) => {
|
||||||
|
duration -= elapsed;
|
||||||
|
if (duration <= 0) {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
33
packages/behavior/types/utility.js
Normal file
33
packages/behavior/types/utility.js
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import {merge as mergeObject} from '@avocado/core';
|
||||||
|
|
||||||
|
export default class Utility {
|
||||||
|
|
||||||
|
static makeArray(...args) {
|
||||||
|
// No context!
|
||||||
|
args.pop();
|
||||||
|
return args;
|
||||||
|
};
|
||||||
|
|
||||||
|
static makeObject(...args) {
|
||||||
|
// No context!
|
||||||
|
args.pop();
|
||||||
|
const object = {};
|
||||||
|
while (args.length > 0) {
|
||||||
|
const key = args.shift();
|
||||||
|
const value = args.shift();
|
||||||
|
object[key] = value;
|
||||||
|
}
|
||||||
|
return object;
|
||||||
|
};
|
||||||
|
|
||||||
|
static log(...args) {
|
||||||
|
return console.log(...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
static merge(...args) {
|
||||||
|
// No context!
|
||||||
|
args.pop();
|
||||||
|
return mergeObject(...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -243,11 +243,12 @@ export default class Entity extends decorate(Resource) {
|
||||||
promises.push(this._traitsFlat[i].hydrate());
|
promises.push(this._traitsFlat[i].hydrate());
|
||||||
}
|
}
|
||||||
this._hydrationPromise = Promise.all(promises);
|
this._hydrationPromise = Promise.all(promises);
|
||||||
}
|
this._hydrationPromise.then(() => {
|
||||||
return this._hydrationPromise.then(() => {
|
|
||||||
this.tick(0);
|
this.tick(0);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
return this._hydrationPromise;
|
||||||
|
}
|
||||||
|
|
||||||
invokeHook(hook, ...args) {
|
invokeHook(hook, ...args) {
|
||||||
const results = {};
|
const results = {};
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
export function behaviorContextTypes() {
|
export function behaviorTypes() {
|
||||||
return {
|
return {
|
||||||
entity: (entity) => {
|
entity: (entity) => {
|
||||||
const {allTraits} = require('./trait/registrar');
|
const {allTraits} = require('./trait/registrar');
|
||||||
|
@ -20,7 +20,7 @@ export function behaviorContextTypes() {
|
||||||
};
|
};
|
||||||
return Traits
|
return Traits
|
||||||
.reduce((r, T) => ({
|
.reduce((r, T) => ({
|
||||||
...r, children: {...r.children, ...T.behaviorContextTypes(), ...T.describeState()},
|
...r, children: {...r.children, ...T.behaviorTypes(), ...T.describeState()},
|
||||||
}), core);
|
}), core);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -71,6 +71,7 @@ export class EntityList extends decorate(class {}) {
|
||||||
if (AVOCADO_SERVER) {
|
if (AVOCADO_SERVER) {
|
||||||
this._informedEntities.set(entity, []);
|
this._informedEntities.set(entity, []);
|
||||||
}
|
}
|
||||||
|
entity.hydrate();
|
||||||
entity.attachToList(this);
|
entity.attachToList(this);
|
||||||
entity.once('destroy', () => {
|
entity.once('destroy', () => {
|
||||||
this.removeEntity(entity);
|
this.removeEntity(entity);
|
||||||
|
|
|
@ -38,7 +38,7 @@ export class Trait extends decorate(class {}) {
|
||||||
this._fastDirtyCheck = false;
|
this._fastDirtyCheck = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
static behaviorContextTypes() {
|
static behaviorTypes() {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import {
|
import {
|
||||||
behaviorItemFromJSON,
|
Actions,
|
||||||
buildCondition,
|
buildCondition,
|
||||||
buildInvoke,
|
buildInvoke,
|
||||||
buildTraversal,
|
buildExpression,
|
||||||
|
compile,
|
||||||
Context,
|
Context,
|
||||||
} from '@avocado/behavior';
|
} from '@avocado/behavior';
|
||||||
import {compose} from '@avocado/core';
|
import {compose} from '@avocado/core';
|
||||||
|
@ -25,7 +26,7 @@ const decorate = compose(
|
||||||
|
|
||||||
export default class Alive extends decorate(Trait) {
|
export default class Alive extends decorate(Trait) {
|
||||||
|
|
||||||
static behaviorContextTypes() {
|
static behaviorTypes() {
|
||||||
return {
|
return {
|
||||||
deathSound: {
|
deathSound: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
|
@ -40,7 +41,7 @@ export default class Alive extends decorate(Trait) {
|
||||||
|
|
||||||
static defaultParams() {
|
static defaultParams() {
|
||||||
const playDeathSound = buildInvoke(['entity', 'playSound'], [
|
const playDeathSound = buildInvoke(['entity', 'playSound'], [
|
||||||
buildTraversal(['entity', 'deathSound']),
|
buildExpression(['entity', 'deathSound']),
|
||||||
]);
|
]);
|
||||||
const squeeze = buildInvoke(['entity', 'transition'], [
|
const squeeze = buildInvoke(['entity', 'transition'], [
|
||||||
{
|
{
|
||||||
|
@ -51,13 +52,13 @@ export default class Alive extends decorate(Trait) {
|
||||||
0.2,
|
0.2,
|
||||||
]);
|
]);
|
||||||
const isLifeGone = buildCondition('<=', [
|
const isLifeGone = buildCondition('<=', [
|
||||||
buildTraversal(['entity', 'life']),
|
buildExpression(['entity', 'life']),
|
||||||
0,
|
0,
|
||||||
]);
|
]);
|
||||||
return {
|
return {
|
||||||
deathActions: {
|
deathActions: {
|
||||||
type: 'actions',
|
type: 'expressions',
|
||||||
traversals: [
|
expressions: [
|
||||||
playDeathSound,
|
playDeathSound,
|
||||||
squeeze,
|
squeeze,
|
||||||
],
|
],
|
||||||
|
@ -118,8 +119,8 @@ export default class Alive extends decorate(Trait) {
|
||||||
this._context = new Context({
|
this._context = new Context({
|
||||||
entity: [this.entity, 'entity'],
|
entity: [this.entity, 'entity'],
|
||||||
});
|
});
|
||||||
this._deathActions = behaviorItemFromJSON(this.params.deathActions);
|
this._deathActions = new Actions(compile(this.params.deathActions));
|
||||||
this._deathCondition = behaviorItemFromJSON(this.params.deathCondition);
|
this._deathCondition = compile(this.params.deathCondition);
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
|
@ -183,13 +184,13 @@ export default class Alive extends decorate(Trait) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.entity.isDying = true;
|
this.entity.isDying = true;
|
||||||
const dyingTickingPromise = this._deathActions.tickingPromise(this._context);
|
return this.entity.addTickingPromise(this._deathActions.tickingPromise(this._context))
|
||||||
this.entity.addTickingPromise(dyingTickingPromise).then(() => {
|
.then(() => (
|
||||||
const diedPromises = this.entity.invokeHookFlat('died');
|
Promise.all(this.entity.invokeHookFlat('died'))
|
||||||
Promise.all(diedPromises).then(() => {
|
.then(() => {
|
||||||
this.entity.destroy();
|
this.entity.destroy();
|
||||||
});
|
})
|
||||||
});
|
));
|
||||||
},
|
},
|
||||||
|
|
||||||
};
|
};
|
||||||
|
@ -197,7 +198,7 @@ export default class Alive extends decorate(Trait) {
|
||||||
|
|
||||||
tick(elapsed) {
|
tick(elapsed) {
|
||||||
if (AVOCADO_SERVER) {
|
if (AVOCADO_SERVER) {
|
||||||
if (!this.entity.isDying && this._deathCondition.check(this._context)) {
|
if (!this.entity.isDying && this._deathCondition(this._context)) {
|
||||||
this.entity.forceDeath();
|
this.entity.forceDeath();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ const decorate = compose(
|
||||||
|
|
||||||
export default class Existent extends decorate(Trait) {
|
export default class Existent extends decorate(Trait) {
|
||||||
|
|
||||||
static behaviorContextTypes() {
|
static behaviorTypes() {
|
||||||
return {
|
return {
|
||||||
destroy: {
|
destroy: {
|
||||||
type: 'void',
|
type: 'void',
|
||||||
|
|
|
@ -4,7 +4,7 @@ import {Trait} from '../trait';
|
||||||
|
|
||||||
export default class Listed extends Trait {
|
export default class Listed extends Trait {
|
||||||
|
|
||||||
static behaviorContextTypes() {
|
static behaviorTypes() {
|
||||||
return {
|
return {
|
||||||
detachFromList: {
|
detachFromList: {
|
||||||
type: 'void',
|
type: 'void',
|
||||||
|
|
|
@ -10,7 +10,7 @@ const decorate = compose(
|
||||||
|
|
||||||
export default class Mobile extends decorate(Trait) {
|
export default class Mobile extends decorate(Trait) {
|
||||||
|
|
||||||
static behaviorContextTypes() {
|
static behaviorTypes() {
|
||||||
return {
|
return {
|
||||||
moveFor: {
|
moveFor: {
|
||||||
type: 'void',
|
type: 'void',
|
||||||
|
|
|
@ -17,7 +17,7 @@ const decorate = compose(
|
||||||
// < 16768 will pack into 1 short per axe and give +/- 0.25 precision.
|
// < 16768 will pack into 1 short per axe and give +/- 0.25 precision.
|
||||||
export default class Positioned extends decorate(Trait) {
|
export default class Positioned extends decorate(Trait) {
|
||||||
|
|
||||||
static behaviorContextTypes() {
|
static behaviorTypes() {
|
||||||
return {
|
return {
|
||||||
position: {
|
position: {
|
||||||
type: 'vector',
|
type: 'vector',
|
||||||
|
|
|
@ -13,7 +13,7 @@ const decorate = compose(
|
||||||
|
|
||||||
export default class Spawner extends decorate(Trait) {
|
export default class Spawner extends decorate(Trait) {
|
||||||
|
|
||||||
static behaviorContextTypes() {
|
static behaviorTypes() {
|
||||||
return {
|
return {
|
||||||
killAllChildren: {
|
killAllChildren: {
|
||||||
type: 'void',
|
type: 'void',
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
import * as I from 'immutable';
|
|
||||||
|
|
||||||
import {compose, Property} from '@avocado/core';
|
import {compose, Property} from '@avocado/core';
|
||||||
import {StateProperty, Trait} from '@avocado/entity';
|
import {StateProperty, Trait} from '@avocado/entity';
|
||||||
import {Rectangle, Vector} from '@avocado/math';
|
import {Rectangle, Vector} from '@avocado/math';
|
||||||
|
@ -30,7 +28,7 @@ const decorate = compose(
|
||||||
|
|
||||||
export default class Visible extends decorate(Trait) {
|
export default class Visible extends decorate(Trait) {
|
||||||
|
|
||||||
static behaviorContextTypes() {
|
static behaviorTypes() {
|
||||||
return {
|
return {
|
||||||
updateVisibleBoundingBox: {
|
updateVisibleBoundingBox: {
|
||||||
advanced: true,
|
advanced: true,
|
||||||
|
|
|
@ -6,7 +6,7 @@ export function behaviorContextGlobals() {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function behaviorContextTypes() {
|
export function behaviorTypes() {
|
||||||
return {
|
return {
|
||||||
Math: (Math) => ({
|
Math: (Math) => ({
|
||||||
children: {
|
children: {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
export function behaviorContextTypes() {
|
export function behaviorTypes() {
|
||||||
return {
|
return {
|
||||||
vector: {
|
vector: {
|
||||||
defaultLiteral: [0, 0],
|
defaultLiteral: [0, 0],
|
||||||
|
|
|
@ -409,7 +409,7 @@ export class Range extends MathRange {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function behaviorContextTypes() {
|
export function behaviorTypes() {
|
||||||
return {
|
return {
|
||||||
type: 'Vector',
|
type: 'Vector',
|
||||||
children: {
|
children: {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import {behaviorItemFromJSON, Context} from '@avocado/behavior';
|
import {Actions, compile, Context} from '@avocado/behavior';
|
||||||
import {compose, TickingPromise} from '@avocado/core';
|
import {compose, TickingPromise} from '@avocado/core';
|
||||||
import {StateProperty, Trait} from '@avocado/entity';
|
import {StateProperty, Trait} from '@avocado/entity';
|
||||||
import {Rectangle, Vector} from '@avocado/math';
|
import {Rectangle, Vector} from '@avocado/math';
|
||||||
|
@ -10,7 +10,7 @@ const decorate = compose(
|
||||||
|
|
||||||
export default class Collider extends decorate(Trait) {
|
export default class Collider extends decorate(Trait) {
|
||||||
|
|
||||||
static behaviorContextTypes() {
|
static behaviorTypes() {
|
||||||
return {
|
return {
|
||||||
collidesWith: {
|
collidesWith: {
|
||||||
advanced: true,
|
advanced: true,
|
||||||
|
@ -61,8 +61,14 @@ export default class Collider extends decorate(Trait) {
|
||||||
collidesWithGroups: [
|
collidesWithGroups: [
|
||||||
'default',
|
'default',
|
||||||
],
|
],
|
||||||
collisionEndActions: undefined,
|
collisionEndActions: {
|
||||||
collisionStartActions: undefined,
|
type: 'expressions',
|
||||||
|
expressions: [],
|
||||||
|
},
|
||||||
|
collisionStartActions: {
|
||||||
|
type: 'expressions',
|
||||||
|
expressions: [],
|
||||||
|
},
|
||||||
collisionGroup: 'default',
|
collisionGroup: 'default',
|
||||||
isSensor: false,
|
isSensor: false,
|
||||||
}
|
}
|
||||||
|
@ -123,22 +129,24 @@ export default class Collider extends decorate(Trait) {
|
||||||
|
|
||||||
constructor(entity, params, state) {
|
constructor(entity, params, state) {
|
||||||
super(entity, params, state);
|
super(entity, params, state);
|
||||||
this._collidesWithGroups = this.params.collidesWithGroups;
|
const {
|
||||||
if (this.params.collisionEndActions) {
|
collidesWithGroups,
|
||||||
this._collisionEndActions = behaviorItemFromJSON(
|
collisionEndActions,
|
||||||
this.params.collisionEndActions,
|
collisionGroup,
|
||||||
);
|
collisionStartActions,
|
||||||
}
|
isSensor,
|
||||||
if (this.params.collisionStartActions) {
|
} = this.params;
|
||||||
this._collisionStartActions = behaviorItemFromJSON(
|
this._collidesWithGroups = collidesWithGroups;
|
||||||
this.params.collisionStartActions,
|
this._collisionEndActions = collisionEndActions.length > 0
|
||||||
);
|
? new Actions(compile(collisionEndActions))
|
||||||
}
|
: undefined;
|
||||||
this._collisionGroup = this.params.collisionGroup;
|
this._collisionStartActions = collisionStartActions.length > 0
|
||||||
this._collisionTickingPromises = [];
|
? new Actions(compile(collisionStartActions))
|
||||||
|
: undefined;
|
||||||
|
this._collisionGroup = collisionGroup;
|
||||||
this._doesNotCollideWith = [];
|
this._doesNotCollideWith = [];
|
||||||
this._isCollidingWith = [];
|
this._isCollidingWith = [];
|
||||||
this._isSensor = this.params.isSensor;
|
this._isSensor = isSensor;
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
|
@ -214,8 +222,7 @@ export default class Collider extends decorate(Trait) {
|
||||||
entity: [this.entity, 'entity'],
|
entity: [this.entity, 'entity'],
|
||||||
other: [this.entity, 'entity'],
|
other: [this.entity, 'entity'],
|
||||||
});
|
});
|
||||||
const tickingPromise = actions.tickingPromise(context);
|
this.entity.addTickingPromise(actions.tickingPromise(context));
|
||||||
this._collisionTickingPromises.push(tickingPromise);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
releaseAllCollisions() {
|
releaseAllCollisions() {
|
||||||
|
@ -243,10 +250,7 @@ export default class Collider extends decorate(Trait) {
|
||||||
if (-1 !== index) {
|
if (-1 !== index) {
|
||||||
this._isCollidingWith.splice(index, 1);
|
this._isCollidingWith.splice(index, 1);
|
||||||
if (this._collisionEndActions) {
|
if (this._collisionEndActions) {
|
||||||
this.pushCollisionTickingPromise(
|
this.pushCollisionTickingPromise(this._collisionEndActions, other);
|
||||||
this._collisionEndActions,
|
|
||||||
other
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -256,10 +260,7 @@ export default class Collider extends decorate(Trait) {
|
||||||
if (-1 === index) {
|
if (-1 === index) {
|
||||||
this._isCollidingWith.push(other);
|
this._isCollidingWith.push(other);
|
||||||
if (this._collisionStartActions) {
|
if (this._collisionStartActions) {
|
||||||
this.pushCollisionTickingPromise(
|
this.pushCollisionTickingPromise(this._collisionStartActions, other);
|
||||||
this._collisionStartActions,
|
|
||||||
other
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -306,9 +307,6 @@ export default class Collider extends decorate(Trait) {
|
||||||
|
|
||||||
tick(elapsed) {
|
tick(elapsed) {
|
||||||
if (AVOCADO_SERVER) {
|
if (AVOCADO_SERVER) {
|
||||||
for (let i = 0; i < this._collisionTickingPromises.length; ++i) {
|
|
||||||
this._collisionTickingPromises[i].tick(elapsed);
|
|
||||||
}
|
|
||||||
this.checkActiveCollision();
|
this.checkActiveCollision();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ const decorate = compose(
|
||||||
|
|
||||||
export default class Emitted extends decorate(Trait) {
|
export default class Emitted extends decorate(Trait) {
|
||||||
|
|
||||||
static behaviorContextTypes() {
|
static behaviorTypes() {
|
||||||
return {
|
return {
|
||||||
particle: {
|
particle: {
|
||||||
type: 'particle',
|
type: 'particle',
|
||||||
|
|
|
@ -14,7 +14,7 @@ const decorate = compose(
|
||||||
|
|
||||||
export default class Emitter extends decorate(Trait) {
|
export default class Emitter extends decorate(Trait) {
|
||||||
|
|
||||||
static behaviorContextTypes() {
|
static behaviorTypes() {
|
||||||
return {
|
return {
|
||||||
emitParticleEntity: {
|
emitParticleEntity: {
|
||||||
cycle: true,
|
cycle: true,
|
||||||
|
|
|
@ -12,7 +12,7 @@ const decorate = compose(
|
||||||
|
|
||||||
export default class Physical extends decorate(Trait) {
|
export default class Physical extends decorate(Trait) {
|
||||||
|
|
||||||
static behaviorContextTypes() {
|
static behaviorTypes() {
|
||||||
return {
|
return {
|
||||||
applyForce: {
|
applyForce: {
|
||||||
type: 'void',
|
type: 'void',
|
||||||
|
|
|
@ -4,7 +4,7 @@ import {Sound} from '..';
|
||||||
|
|
||||||
export default class Audible extends Trait {
|
export default class Audible extends Trait {
|
||||||
|
|
||||||
static behaviorContextTypes() {
|
static behaviorTypes() {
|
||||||
return {
|
return {
|
||||||
hasSound: {
|
hasSound: {
|
||||||
type: 'bool',
|
type: 'bool',
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
export function behaviorContextTypes() {
|
export function behaviorTypes() {
|
||||||
return {
|
return {
|
||||||
layer: (layer) => {
|
layer: (layer) => {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -3,7 +3,7 @@ import {Vector} from '@avocado/math';
|
||||||
|
|
||||||
export default class Layered extends Trait {
|
export default class Layered extends Trait {
|
||||||
|
|
||||||
static behaviorContextTypes() {
|
static behaviorTypes() {
|
||||||
return {
|
return {
|
||||||
layer: {
|
layer: {
|
||||||
type: 'layer',
|
type: 'layer',
|
||||||
|
|
|
@ -2,7 +2,7 @@ import {Trait} from '@avocado/entity';
|
||||||
|
|
||||||
export default class Roomed extends Trait {
|
export default class Roomed extends Trait {
|
||||||
|
|
||||||
static behaviorContextTypes() {
|
static behaviorTypes() {
|
||||||
return {
|
return {
|
||||||
detachFromRoom: {
|
detachFromRoom: {
|
||||||
type: 'void',
|
type: 'void',
|
||||||
|
|
Loading…
Reference in New Issue
Block a user