refactor: behavior

This commit is contained in:
cha0s 2020-06-23 11:19:59 -05:00
parent de5b51b8ea
commit abbab55d30
54 changed files with 675 additions and 1232 deletions

View File

@ -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);

View File

@ -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);
}),
}; };
} }

View 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);

View 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),
};
}

View 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);

View File

@ -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: {},
};
}

View File

@ -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);
}
}
}
}

View File

@ -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',
}],
],
},
},
},
};
}

View File

@ -1 +0,0 @@
export {Context} from './context';

View File

@ -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'],
};
}

View File

@ -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'],
};
}

View File

@ -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';

View File

@ -1,12 +0,0 @@
import {Traversal} from './traversal';
export class Action extends Traversal {
toJSON() {
return {
...super.toJSON(),
type: 'action',
};
}
}

View File

@ -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);
});
});
});
});

View File

@ -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());
}
};
}

View File

@ -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()),
};
}
}

View File

@ -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;
});
});
});
});

View File

@ -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;
}
}

View File

@ -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,
};
}

View File

@ -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,
}
}
}

View File

@ -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);
});
});
});

View File

@ -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;
}

View File

@ -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(),
}
}
}

View File

@ -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,
};
}
}

View File

@ -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,
};
}
}

View File

@ -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);
}
}

View File

@ -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
View 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);

View 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: {},
};
}

View 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);
}
}

View 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();
}
},
);
}
}

View 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);
}
}

View File

@ -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 = {};

View File

@ -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);
}, },
}; };

View File

@ -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);

View File

@ -38,7 +38,7 @@ export class Trait extends decorate(class {}) {
this._fastDirtyCheck = false; this._fastDirtyCheck = false;
} }
static behaviorContextTypes() { static behaviorTypes() {
return {}; return {};
} }

View File

@ -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();
} }
} }

View File

@ -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',

View File

@ -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',

View File

@ -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',

View File

@ -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',

View File

@ -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',

View File

@ -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,

View File

@ -6,7 +6,7 @@ export function behaviorContextGlobals() {
}; };
} }
export function behaviorContextTypes() { export function behaviorTypes() {
return { return {
Math: (Math) => ({ Math: (Math) => ({
children: { children: {

View File

@ -1,4 +1,4 @@
export function behaviorContextTypes() { export function behaviorTypes() {
return { return {
vector: { vector: {
defaultLiteral: [0, 0], defaultLiteral: [0, 0],

View File

@ -409,7 +409,7 @@ export class Range extends MathRange {
} }
export function behaviorContextTypes() { export function behaviorTypes() {
return { return {
type: 'Vector', type: 'Vector',
children: { children: {

View File

@ -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();
} }
} }

View File

@ -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',

View File

@ -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,

View File

@ -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',

View File

@ -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',

View File

@ -1,4 +1,4 @@
export function behaviorContextTypes() { export function behaviorTypes() {
return { return {
layer: (layer) => { layer: (layer) => {
return { return {

View File

@ -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',

View File

@ -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',