From abbab55d3034d3271c0d599a8ae2e57a28d230a3 Mon Sep 17 00:00:00 2001 From: cha0s Date: Tue, 23 Jun 2020 11:19:59 -0500 Subject: [PATCH] refactor: behavior --- packages/behavior/{item => }/actions.js | 51 +++-- packages/behavior/builders.js | 35 ++- packages/behavior/compile.js | 26 +++ packages/behavior/compilers.hooks.js | 142 ++++++++++++ packages/behavior/context.js | 74 +++++++ packages/behavior/context/context.hooks.js | 34 --- packages/behavior/context/context.js | 237 --------------------- packages/behavior/context/flow.hooks.js | 65 ------ packages/behavior/context/index.js | 1 - packages/behavior/context/timing.hooks.js | 41 ---- packages/behavior/context/utility.hooks.js | 68 ------ packages/behavior/index.js | 27 ++- packages/behavior/item/action.js | 12 -- packages/behavior/item/actions.spec.js | 93 -------- packages/behavior/item/collection.js | 46 ---- packages/behavior/item/condition.js | 106 --------- packages/behavior/item/condition.spec.js | 109 ---------- packages/behavior/item/conditions.js | 14 -- packages/behavior/item/items.hooks.js | 23 -- packages/behavior/item/literal.js | 30 --- packages/behavior/item/literal.spec.js | 22 -- packages/behavior/item/registry.js | 26 --- packages/behavior/item/routine.js | 29 --- packages/behavior/item/routines.js | 45 ---- packages/behavior/item/traversal.js | 80 ------- packages/behavior/item/traversals.js | 21 -- packages/behavior/traits/behaved.trait.js | 16 +- packages/behavior/type.js | 108 ++++++++++ packages/behavior/types.hooks.js | 114 ++++++++++ packages/behavior/types/flow.js | 19 ++ packages/behavior/types/timing.js | 17 ++ packages/behavior/types/utility.js | 33 +++ packages/entity/entity.synchronized.js | 7 +- packages/entity/index.hooks.js | 4 +- packages/entity/list/index.js | 1 + packages/entity/trait/index.js | 2 +- packages/entity/traits/alive.trait.js | 35 +-- packages/entity/traits/existent.trait.js | 2 +- packages/entity/traits/listed.trait.js | 2 +- packages/entity/traits/mobile.trait.js | 2 +- packages/entity/traits/positioned.trait.js | 2 +- packages/entity/traits/spawner.trait.js | 2 +- packages/graphics/traits/visible.trait.js | 4 +- packages/math/index.hooks.js | 2 +- packages/math/vector/index.hooks.js | 2 +- packages/math/vector/index.js | 2 +- packages/physics/traits/collider.trait.js | 60 +++--- packages/physics/traits/emitted.trait.js | 2 +- packages/physics/traits/emitter.trait.js | 2 +- packages/physics/traits/physical.trait.js | 2 +- packages/sound/traits/audible.trait.js | 2 +- packages/topdown/index.hooks.js | 2 +- packages/topdown/traits/layered.trait.js | 2 +- packages/topdown/traits/roomed.trait.js | 2 +- 54 files changed, 675 insertions(+), 1232 deletions(-) rename packages/behavior/{item => }/actions.js (59%) create mode 100644 packages/behavior/compile.js create mode 100644 packages/behavior/compilers.hooks.js create mode 100644 packages/behavior/context.js delete mode 100644 packages/behavior/context/context.hooks.js delete mode 100644 packages/behavior/context/context.js delete mode 100644 packages/behavior/context/flow.hooks.js delete mode 100644 packages/behavior/context/index.js delete mode 100644 packages/behavior/context/timing.hooks.js delete mode 100644 packages/behavior/context/utility.hooks.js delete mode 100644 packages/behavior/item/action.js delete mode 100644 packages/behavior/item/actions.spec.js delete mode 100644 packages/behavior/item/collection.js delete mode 100644 packages/behavior/item/condition.js delete mode 100644 packages/behavior/item/condition.spec.js delete mode 100644 packages/behavior/item/conditions.js delete mode 100644 packages/behavior/item/items.hooks.js delete mode 100644 packages/behavior/item/literal.js delete mode 100644 packages/behavior/item/literal.spec.js delete mode 100644 packages/behavior/item/registry.js delete mode 100644 packages/behavior/item/routine.js delete mode 100644 packages/behavior/item/routines.js delete mode 100644 packages/behavior/item/traversal.js delete mode 100644 packages/behavior/item/traversals.js create mode 100644 packages/behavior/type.js create mode 100644 packages/behavior/types.hooks.js create mode 100644 packages/behavior/types/flow.js create mode 100644 packages/behavior/types/timing.js create mode 100644 packages/behavior/types/utility.js diff --git a/packages/behavior/item/actions.js b/packages/behavior/actions.js similarity index 59% rename from packages/behavior/item/actions.js rename to packages/behavior/actions.js index 425e1c4..9c8db8c 100644 --- a/packages/behavior/item/actions.js +++ b/packages/behavior/actions.js @@ -1,18 +1,15 @@ import {compose, EventEmitter, TickingPromise} from '@avocado/core'; -import {Traversal} from './traversal'; -import {Traversals} from './traversals'; - const decorate = compose( EventEmitter, ); -export class Actions extends decorate(Traversals) { +class Actions { - constructor() { - super(); + constructor(expressions) { + this.expressions = 'function' === typeof expressions ? expressions() : expressions; this._index = 0; - this._actionPromise = null; + this.promise = null; } emitFinished() { @@ -33,29 +30,24 @@ export class Actions extends decorate(Traversals) { tick(context, elapsed) { // Empty resolves immediately. - if (this.traversals.length === 0) { + if (this.expressions.length === 0) { this.emitFinished(); return; } // If the action promise ticks, tick it. - if (this._actionPromise && this._actionPromise instanceof TickingPromise) { - this._actionPromise.tick(elapsed); + if (this.promise && this.promise instanceof TickingPromise) { + this.promise.tick(elapsed); return; } // Actions execute immediately until a promise is made, or they're all // executed. while (true) { // Run the action. - const result = this.traversals[this.index].traverse(context); + const result = this.expressions[this.index](context); // Deferred result. if (result instanceof Promise) { - this._actionPromise = result; - // Handle any errors. - result.catch(console.error); - result.finally(() => { - // Finally, run the prologue. - this.prologue(); - }); + this.promise = result; + this.promise.catch(console.error).finally(() => this.prologue()); break; } // Immediate result. @@ -68,19 +60,24 @@ export class Actions extends decorate(Traversals) { } parallel(context) { - // Map all traversals to results. - const results = this.traversals.map((traversal) => { - return traversal.traverse(context); - }); - // Wrap all results in a TickingPromise. - return TickingPromise.all(results); + const results = this.expressions.map((expression) => expression(context)); + for (let i = 0; i < results.length; i++) { + const result = results[i]; + if (result instanceof TickingPromise) { + return TickingPromise.all(results); + } + if (result instanceof Promise) { + return Promise.all(results); + } + } + return results; } prologue() { // Clear out the action promise. - this._actionPromise = null; + this.promise = null; // 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 (0 === this.index) { this.emitFinished(); @@ -103,3 +100,5 @@ export class Actions extends decorate(Traversals) { } } + +export default decorate(Actions); diff --git a/packages/behavior/builders.js b/packages/behavior/builders.js index ebaab55..24d898d 100644 --- a/packages/behavior/builders.js +++ b/packages/behavior/builders.js @@ -1,36 +1,29 @@ -export function buildTraversal(path, value) { - const traversal = { - type: 'traversal', - steps: path.map((key) => { - return { - type: 'key', - key: key, - }; - }), +export function buildExpression(path, value) { + const expression = { + type: 'expression', + ops: path.map((key) => ({type: 'key', key: key})), }; if ('undefined' !== typeof value) { - traversal.value = buildValue(value); + expression.value = buildValue(value); } - return traversal; + return expression; } export function buildInvoke (path, args = []) { - const traversal = buildTraversal(path); - traversal.steps.push({ + const expression = buildExpression(path); + expression.ops.push({ type: 'invoke', - args: args.map((arg) => { - return buildValue(arg); - }), + args: args.map((arg) => buildValue(arg)), }); - return traversal; + return expression; } export function buildValue(value) { if ( 'object' === typeof value && ( - 'traversal' === value.type - || 'actions' === value.type + 'expression' === value.type + || 'expressions' === value.type || 'condition' === value.type ) ) { @@ -46,8 +39,6 @@ export function buildCondition(operator, operands) { return { type: 'condition', operator, - operands: operands.map((operand) => { - return buildValue(operand); - }), + operands: operands.map((operand) => buildValue(operand)), }; } diff --git a/packages/behavior/compile.js b/packages/behavior/compile.js new file mode 100644 index 0000000..71bd78f --- /dev/null +++ b/packages/behavior/compile.js @@ -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); diff --git a/packages/behavior/compilers.hooks.js b/packages/behavior/compilers.hooks.js new file mode 100644 index 0000000..3df78ad --- /dev/null +++ b/packages/behavior/compilers.hooks.js @@ -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), + }; +} diff --git a/packages/behavior/context.js b/packages/behavior/context.js new file mode 100644 index 0000000..ed3c8fe --- /dev/null +++ b/packages/behavior/context.js @@ -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); diff --git a/packages/behavior/context/context.hooks.js b/packages/behavior/context/context.hooks.js deleted file mode 100644 index 499d976..0000000 --- a/packages/behavior/context/context.hooks.js +++ /dev/null @@ -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: {}, - }; -} - diff --git a/packages/behavior/context/context.js b/packages/behavior/context/context.js deleted file mode 100644 index 11e9786..0000000 --- a/packages/behavior/context/context.js +++ /dev/null @@ -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); - } - } - } - -} diff --git a/packages/behavior/context/flow.hooks.js b/packages/behavior/context/flow.hooks.js deleted file mode 100644 index 0a407b9..0000000 --- a/packages/behavior/context/flow.hooks.js +++ /dev/null @@ -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', - }], - ], - }, - }, - }, - }; -} - diff --git a/packages/behavior/context/index.js b/packages/behavior/context/index.js deleted file mode 100644 index 1a7fb45..0000000 --- a/packages/behavior/context/index.js +++ /dev/null @@ -1 +0,0 @@ -export {Context} from './context'; diff --git a/packages/behavior/context/timing.hooks.js b/packages/behavior/context/timing.hooks.js deleted file mode 100644 index 41db66b..0000000 --- a/packages/behavior/context/timing.hooks.js +++ /dev/null @@ -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'], - }; -} diff --git a/packages/behavior/context/utility.hooks.js b/packages/behavior/context/utility.hooks.js deleted file mode 100644 index 637687c..0000000 --- a/packages/behavior/context/utility.hooks.js +++ /dev/null @@ -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'], - }; -} diff --git a/packages/behavior/index.js b/packages/behavior/index.js index 14745dc..071d3b8 100644 --- a/packages/behavior/index.js +++ b/packages/behavior/index.js @@ -1,10 +1,29 @@ -export {Context} from './context'; export { - fromJSON as behaviorItemFromJSON, -} from './item/registry'; + default as Actions, +} from './actions'; + export { buildCondition, buildInvoke, - buildTraversal, + buildExpression, buildValue, } 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'; diff --git a/packages/behavior/item/action.js b/packages/behavior/item/action.js deleted file mode 100644 index 641e05c..0000000 --- a/packages/behavior/item/action.js +++ /dev/null @@ -1,12 +0,0 @@ -import {Traversal} from './traversal'; - -export class Action extends Traversal { - - toJSON() { - return { - ...super.toJSON(), - type: 'action', - }; - } - -} diff --git a/packages/behavior/item/actions.spec.js b/packages/behavior/item/actions.spec.js deleted file mode 100644 index b742073..0000000 --- a/packages/behavior/item/actions.spec.js +++ /dev/null @@ -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); - }); - }); - }); -}); diff --git a/packages/behavior/item/collection.js b/packages/behavior/item/collection.js deleted file mode 100644 index f06d7cd..0000000 --- a/packages/behavior/item/collection.js +++ /dev/null @@ -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()); - } - - }; -} diff --git a/packages/behavior/item/condition.js b/packages/behavior/item/condition.js deleted file mode 100644 index 48a6c64..0000000 --- a/packages/behavior/item/condition.js +++ /dev/null @@ -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()), - }; - } -} diff --git a/packages/behavior/item/condition.spec.js b/packages/behavior/item/condition.spec.js deleted file mode 100644 index 2f901aa..0000000 --- a/packages/behavior/item/condition.spec.js +++ /dev/null @@ -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; - }); - }); - }); -}); diff --git a/packages/behavior/item/conditions.js b/packages/behavior/item/conditions.js deleted file mode 100644 index 16437cb..0000000 --- a/packages/behavior/item/conditions.js +++ /dev/null @@ -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; - } - -} \ No newline at end of file diff --git a/packages/behavior/item/items.hooks.js b/packages/behavior/item/items.hooks.js deleted file mode 100644 index 9fd3a92..0000000 --- a/packages/behavior/item/items.hooks.js +++ /dev/null @@ -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, - }; -} diff --git a/packages/behavior/item/literal.js b/packages/behavior/item/literal.js deleted file mode 100644 index 8fe97b2..0000000 --- a/packages/behavior/item/literal.js +++ /dev/null @@ -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, - } - } -} diff --git a/packages/behavior/item/literal.spec.js b/packages/behavior/item/literal.spec.js deleted file mode 100644 index 27cbb9a..0000000 --- a/packages/behavior/item/literal.spec.js +++ /dev/null @@ -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); - }); - }); -}); diff --git a/packages/behavior/item/registry.js b/packages/behavior/item/registry.js deleted file mode 100644 index a2f06d5..0000000 --- a/packages/behavior/item/registry.js +++ /dev/null @@ -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; -} diff --git a/packages/behavior/item/routine.js b/packages/behavior/item/routine.js deleted file mode 100644 index 899e4bb..0000000 --- a/packages/behavior/item/routine.js +++ /dev/null @@ -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(), - } - } - -} diff --git a/packages/behavior/item/routines.js b/packages/behavior/item/routines.js deleted file mode 100644 index 9baff7f..0000000 --- a/packages/behavior/item/routines.js +++ /dev/null @@ -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, - }; - } -} diff --git a/packages/behavior/item/traversal.js b/packages/behavior/item/traversal.js deleted file mode 100644 index 1ecf714..0000000 --- a/packages/behavior/item/traversal.js +++ /dev/null @@ -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, - }; - } -} diff --git a/packages/behavior/item/traversals.js b/packages/behavior/item/traversals.js deleted file mode 100644 index f8a0b3c..0000000 --- a/packages/behavior/item/traversals.js +++ /dev/null @@ -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); - } - -} diff --git a/packages/behavior/traits/behaved.trait.js b/packages/behavior/traits/behaved.trait.js index cc07f9d..3747439 100644 --- a/packages/behavior/traits/behaved.trait.js +++ b/packages/behavior/traits/behaved.trait.js @@ -1,8 +1,9 @@ -import {compose, flatten} from '@avocado/core'; +import {compose, flatten, mapObject} from '@avocado/core'; import {StateProperty, Trait} from '@avocado/entity'; -import {Context} from '../context'; -import {Routines} from '../item/routines'; +import Actions from '../actions'; +import compile from '../compile'; +import Context from '../context'; const decorate = compose( StateProperty('currentRoutine', { @@ -13,7 +14,7 @@ const decorate = compose( export default class Behaved extends decorate(Trait) { - static behaviorContextTypes() { + static behaviorTypes() { return { context: { type: 'context', @@ -67,7 +68,10 @@ export default class Behaved extends decorate(Trait) { entity: [this.entity, 'entity'], }); 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); } @@ -82,7 +86,7 @@ export default class Behaved extends decorate(Trait) { } updateCurrentRoutine(currentRoutine) { - this._currentRoutine = this._routines.routine(currentRoutine); + this._currentRoutine = this._routines[currentRoutine]; } listeners() { diff --git a/packages/behavior/type.js b/packages/behavior/type.js new file mode 100644 index 0000000..30f83d7 --- /dev/null +++ b/packages/behavior/type.js @@ -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); diff --git a/packages/behavior/types.hooks.js b/packages/behavior/types.hooks.js new file mode 100644 index 0000000..696d3f2 --- /dev/null +++ b/packages/behavior/types.hooks.js @@ -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: {}, + }; +} diff --git a/packages/behavior/types/flow.js b/packages/behavior/types/flow.js new file mode 100644 index 0000000..9e4d1b6 --- /dev/null +++ b/packages/behavior/types/flow.js @@ -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); + } + +} diff --git a/packages/behavior/types/timing.js b/packages/behavior/types/timing.js new file mode 100644 index 0000000..1fdc368 --- /dev/null +++ b/packages/behavior/types/timing.js @@ -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(); + } + }, + ); + } + +} diff --git a/packages/behavior/types/utility.js b/packages/behavior/types/utility.js new file mode 100644 index 0000000..de8c4d9 --- /dev/null +++ b/packages/behavior/types/utility.js @@ -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); + } + +} diff --git a/packages/entity/entity.synchronized.js b/packages/entity/entity.synchronized.js index 3737a20..ea4878c 100644 --- a/packages/entity/entity.synchronized.js +++ b/packages/entity/entity.synchronized.js @@ -243,10 +243,11 @@ export default class Entity extends decorate(Resource) { promises.push(this._traitsFlat[i].hydrate()); } this._hydrationPromise = Promise.all(promises); + this._hydrationPromise.then(() => { + this.tick(0); + }); } - return this._hydrationPromise.then(() => { - this.tick(0); - }); + return this._hydrationPromise; } invokeHook(hook, ...args) { diff --git a/packages/entity/index.hooks.js b/packages/entity/index.hooks.js index 9be6490..924464b 100644 --- a/packages/entity/index.hooks.js +++ b/packages/entity/index.hooks.js @@ -1,4 +1,4 @@ -export function behaviorContextTypes() { +export function behaviorTypes() { return { entity: (entity) => { const {allTraits} = require('./trait/registrar'); @@ -20,7 +20,7 @@ export function behaviorContextTypes() { }; return Traits .reduce((r, T) => ({ - ...r, children: {...r.children, ...T.behaviorContextTypes(), ...T.describeState()}, + ...r, children: {...r.children, ...T.behaviorTypes(), ...T.describeState()}, }), core); }, }; diff --git a/packages/entity/list/index.js b/packages/entity/list/index.js index 068cd68..1eb6757 100644 --- a/packages/entity/list/index.js +++ b/packages/entity/list/index.js @@ -71,6 +71,7 @@ export class EntityList extends decorate(class {}) { if (AVOCADO_SERVER) { this._informedEntities.set(entity, []); } + entity.hydrate(); entity.attachToList(this); entity.once('destroy', () => { this.removeEntity(entity); diff --git a/packages/entity/trait/index.js b/packages/entity/trait/index.js index 4d81657..64bc5d3 100644 --- a/packages/entity/trait/index.js +++ b/packages/entity/trait/index.js @@ -38,7 +38,7 @@ export class Trait extends decorate(class {}) { this._fastDirtyCheck = false; } - static behaviorContextTypes() { + static behaviorTypes() { return {}; } diff --git a/packages/entity/traits/alive.trait.js b/packages/entity/traits/alive.trait.js index f3fda4d..e1f2c72 100644 --- a/packages/entity/traits/alive.trait.js +++ b/packages/entity/traits/alive.trait.js @@ -1,8 +1,9 @@ import { - behaviorItemFromJSON, + Actions, buildCondition, buildInvoke, - buildTraversal, + buildExpression, + compile, Context, } from '@avocado/behavior'; import {compose} from '@avocado/core'; @@ -25,7 +26,7 @@ const decorate = compose( export default class Alive extends decorate(Trait) { - static behaviorContextTypes() { + static behaviorTypes() { return { deathSound: { type: 'string', @@ -40,7 +41,7 @@ export default class Alive extends decorate(Trait) { static defaultParams() { const playDeathSound = buildInvoke(['entity', 'playSound'], [ - buildTraversal(['entity', 'deathSound']), + buildExpression(['entity', 'deathSound']), ]); const squeeze = buildInvoke(['entity', 'transition'], [ { @@ -51,13 +52,13 @@ export default class Alive extends decorate(Trait) { 0.2, ]); const isLifeGone = buildCondition('<=', [ - buildTraversal(['entity', 'life']), + buildExpression(['entity', 'life']), 0, ]); return { deathActions: { - type: 'actions', - traversals: [ + type: 'expressions', + expressions: [ playDeathSound, squeeze, ], @@ -118,8 +119,8 @@ export default class Alive extends decorate(Trait) { this._context = new Context({ entity: [this.entity, 'entity'], }); - this._deathActions = behaviorItemFromJSON(this.params.deathActions); - this._deathCondition = behaviorItemFromJSON(this.params.deathCondition); + this._deathActions = new Actions(compile(this.params.deathActions)); + this._deathCondition = compile(this.params.deathCondition); } destroy() { @@ -183,13 +184,13 @@ export default class Alive extends decorate(Trait) { return; } this.entity.isDying = true; - const dyingTickingPromise = this._deathActions.tickingPromise(this._context); - this.entity.addTickingPromise(dyingTickingPromise).then(() => { - const diedPromises = this.entity.invokeHookFlat('died'); - Promise.all(diedPromises).then(() => { - this.entity.destroy(); - }); - }); + return this.entity.addTickingPromise(this._deathActions.tickingPromise(this._context)) + .then(() => ( + Promise.all(this.entity.invokeHookFlat('died')) + .then(() => { + this.entity.destroy(); + }) + )); }, }; @@ -197,7 +198,7 @@ export default class Alive extends decorate(Trait) { tick(elapsed) { if (AVOCADO_SERVER) { - if (!this.entity.isDying && this._deathCondition.check(this._context)) { + if (!this.entity.isDying && this._deathCondition(this._context)) { this.entity.forceDeath(); } } diff --git a/packages/entity/traits/existent.trait.js b/packages/entity/traits/existent.trait.js index 6dd129e..72ba7d6 100644 --- a/packages/entity/traits/existent.trait.js +++ b/packages/entity/traits/existent.trait.js @@ -10,7 +10,7 @@ const decorate = compose( export default class Existent extends decorate(Trait) { - static behaviorContextTypes() { + static behaviorTypes() { return { destroy: { type: 'void', diff --git a/packages/entity/traits/listed.trait.js b/packages/entity/traits/listed.trait.js index e8142cf..6fd93b9 100644 --- a/packages/entity/traits/listed.trait.js +++ b/packages/entity/traits/listed.trait.js @@ -4,7 +4,7 @@ import {Trait} from '../trait'; export default class Listed extends Trait { - static behaviorContextTypes() { + static behaviorTypes() { return { detachFromList: { type: 'void', diff --git a/packages/entity/traits/mobile.trait.js b/packages/entity/traits/mobile.trait.js index f8dca6d..2bfaf5f 100644 --- a/packages/entity/traits/mobile.trait.js +++ b/packages/entity/traits/mobile.trait.js @@ -10,7 +10,7 @@ const decorate = compose( export default class Mobile extends decorate(Trait) { - static behaviorContextTypes() { + static behaviorTypes() { return { moveFor: { type: 'void', diff --git a/packages/entity/traits/positioned.trait.js b/packages/entity/traits/positioned.trait.js index abb4037..dd3ebad 100644 --- a/packages/entity/traits/positioned.trait.js +++ b/packages/entity/traits/positioned.trait.js @@ -17,7 +17,7 @@ const decorate = compose( // < 16768 will pack into 1 short per axe and give +/- 0.25 precision. export default class Positioned extends decorate(Trait) { - static behaviorContextTypes() { + static behaviorTypes() { return { position: { type: 'vector', diff --git a/packages/entity/traits/spawner.trait.js b/packages/entity/traits/spawner.trait.js index 5d3dd9d..c3e65bd 100644 --- a/packages/entity/traits/spawner.trait.js +++ b/packages/entity/traits/spawner.trait.js @@ -13,7 +13,7 @@ const decorate = compose( export default class Spawner extends decorate(Trait) { - static behaviorContextTypes() { + static behaviorTypes() { return { killAllChildren: { type: 'void', diff --git a/packages/graphics/traits/visible.trait.js b/packages/graphics/traits/visible.trait.js index e962aca..b1b56c2 100644 --- a/packages/graphics/traits/visible.trait.js +++ b/packages/graphics/traits/visible.trait.js @@ -1,5 +1,3 @@ -import * as I from 'immutable'; - import {compose, Property} from '@avocado/core'; import {StateProperty, Trait} from '@avocado/entity'; import {Rectangle, Vector} from '@avocado/math'; @@ -30,7 +28,7 @@ const decorate = compose( export default class Visible extends decorate(Trait) { - static behaviorContextTypes() { + static behaviorTypes() { return { updateVisibleBoundingBox: { advanced: true, diff --git a/packages/math/index.hooks.js b/packages/math/index.hooks.js index 594dd99..d1f7cc9 100644 --- a/packages/math/index.hooks.js +++ b/packages/math/index.hooks.js @@ -6,7 +6,7 @@ export function behaviorContextGlobals() { }; } -export function behaviorContextTypes() { +export function behaviorTypes() { return { Math: (Math) => ({ children: { diff --git a/packages/math/vector/index.hooks.js b/packages/math/vector/index.hooks.js index 1b7155e..17787bc 100644 --- a/packages/math/vector/index.hooks.js +++ b/packages/math/vector/index.hooks.js @@ -1,4 +1,4 @@ -export function behaviorContextTypes() { +export function behaviorTypes() { return { vector: { defaultLiteral: [0, 0], diff --git a/packages/math/vector/index.js b/packages/math/vector/index.js index c660eb5..ed5baa9 100644 --- a/packages/math/vector/index.js +++ b/packages/math/vector/index.js @@ -409,7 +409,7 @@ export class Range extends MathRange { } -export function behaviorContextTypes() { +export function behaviorTypes() { return { type: 'Vector', children: { diff --git a/packages/physics/traits/collider.trait.js b/packages/physics/traits/collider.trait.js index dac11c8..7f1a55e 100644 --- a/packages/physics/traits/collider.trait.js +++ b/packages/physics/traits/collider.trait.js @@ -1,4 +1,4 @@ -import {behaviorItemFromJSON, Context} from '@avocado/behavior'; +import {Actions, compile, Context} from '@avocado/behavior'; import {compose, TickingPromise} from '@avocado/core'; import {StateProperty, Trait} from '@avocado/entity'; import {Rectangle, Vector} from '@avocado/math'; @@ -10,7 +10,7 @@ const decorate = compose( export default class Collider extends decorate(Trait) { - static behaviorContextTypes() { + static behaviorTypes() { return { collidesWith: { advanced: true, @@ -61,8 +61,14 @@ export default class Collider extends decorate(Trait) { collidesWithGroups: [ 'default', ], - collisionEndActions: undefined, - collisionStartActions: undefined, + collisionEndActions: { + type: 'expressions', + expressions: [], + }, + collisionStartActions: { + type: 'expressions', + expressions: [], + }, collisionGroup: 'default', isSensor: false, } @@ -123,22 +129,24 @@ export default class Collider extends decorate(Trait) { constructor(entity, params, state) { super(entity, params, state); - this._collidesWithGroups = this.params.collidesWithGroups; - if (this.params.collisionEndActions) { - this._collisionEndActions = behaviorItemFromJSON( - this.params.collisionEndActions, - ); - } - if (this.params.collisionStartActions) { - this._collisionStartActions = behaviorItemFromJSON( - this.params.collisionStartActions, - ); - } - this._collisionGroup = this.params.collisionGroup; - this._collisionTickingPromises = []; + const { + collidesWithGroups, + collisionEndActions, + collisionGroup, + collisionStartActions, + isSensor, + } = this.params; + this._collidesWithGroups = collidesWithGroups; + this._collisionEndActions = collisionEndActions.length > 0 + ? new Actions(compile(collisionEndActions)) + : undefined; + this._collisionStartActions = collisionStartActions.length > 0 + ? new Actions(compile(collisionStartActions)) + : undefined; + this._collisionGroup = collisionGroup; this._doesNotCollideWith = []; this._isCollidingWith = []; - this._isSensor = this.params.isSensor; + this._isSensor = isSensor; } destroy() { @@ -214,8 +222,7 @@ export default class Collider extends decorate(Trait) { entity: [this.entity, 'entity'], other: [this.entity, 'entity'], }); - const tickingPromise = actions.tickingPromise(context); - this._collisionTickingPromises.push(tickingPromise); + this.entity.addTickingPromise(actions.tickingPromise(context)); } releaseAllCollisions() { @@ -243,10 +250,7 @@ export default class Collider extends decorate(Trait) { if (-1 !== index) { this._isCollidingWith.splice(index, 1); if (this._collisionEndActions) { - this.pushCollisionTickingPromise( - this._collisionEndActions, - other - ); + this.pushCollisionTickingPromise(this._collisionEndActions, other); } } }, @@ -256,10 +260,7 @@ export default class Collider extends decorate(Trait) { if (-1 === index) { this._isCollidingWith.push(other); if (this._collisionStartActions) { - this.pushCollisionTickingPromise( - this._collisionStartActions, - other - ); + this.pushCollisionTickingPromise(this._collisionStartActions, other); } } }, @@ -306,9 +307,6 @@ export default class Collider extends decorate(Trait) { tick(elapsed) { if (AVOCADO_SERVER) { - for (let i = 0; i < this._collisionTickingPromises.length; ++i) { - this._collisionTickingPromises[i].tick(elapsed); - } this.checkActiveCollision(); } } diff --git a/packages/physics/traits/emitted.trait.js b/packages/physics/traits/emitted.trait.js index 08628dc..6642bd2 100644 --- a/packages/physics/traits/emitted.trait.js +++ b/packages/physics/traits/emitted.trait.js @@ -8,7 +8,7 @@ const decorate = compose( export default class Emitted extends decorate(Trait) { - static behaviorContextTypes() { + static behaviorTypes() { return { particle: { type: 'particle', diff --git a/packages/physics/traits/emitter.trait.js b/packages/physics/traits/emitter.trait.js index 1bcd729..f3f950b 100644 --- a/packages/physics/traits/emitter.trait.js +++ b/packages/physics/traits/emitter.trait.js @@ -14,7 +14,7 @@ const decorate = compose( export default class Emitter extends decorate(Trait) { - static behaviorContextTypes() { + static behaviorTypes() { return { emitParticleEntity: { cycle: true, diff --git a/packages/physics/traits/physical.trait.js b/packages/physics/traits/physical.trait.js index 764bae7..641e4d9 100644 --- a/packages/physics/traits/physical.trait.js +++ b/packages/physics/traits/physical.trait.js @@ -12,7 +12,7 @@ const decorate = compose( export default class Physical extends decorate(Trait) { - static behaviorContextTypes() { + static behaviorTypes() { return { applyForce: { type: 'void', diff --git a/packages/sound/traits/audible.trait.js b/packages/sound/traits/audible.trait.js index 1f2d2ff..87772a9 100644 --- a/packages/sound/traits/audible.trait.js +++ b/packages/sound/traits/audible.trait.js @@ -4,7 +4,7 @@ import {Sound} from '..'; export default class Audible extends Trait { - static behaviorContextTypes() { + static behaviorTypes() { return { hasSound: { type: 'bool', diff --git a/packages/topdown/index.hooks.js b/packages/topdown/index.hooks.js index 8b927f3..da28d23 100644 --- a/packages/topdown/index.hooks.js +++ b/packages/topdown/index.hooks.js @@ -1,4 +1,4 @@ -export function behaviorContextTypes() { +export function behaviorTypes() { return { layer: (layer) => { return { diff --git a/packages/topdown/traits/layered.trait.js b/packages/topdown/traits/layered.trait.js index 0dfdee1..3bfb92e 100644 --- a/packages/topdown/traits/layered.trait.js +++ b/packages/topdown/traits/layered.trait.js @@ -3,7 +3,7 @@ import {Vector} from '@avocado/math'; export default class Layered extends Trait { - static behaviorContextTypes() { + static behaviorTypes() { return { layer: { type: 'layer', diff --git a/packages/topdown/traits/roomed.trait.js b/packages/topdown/traits/roomed.trait.js index 32d67c5..599c38e 100644 --- a/packages/topdown/traits/roomed.trait.js +++ b/packages/topdown/traits/roomed.trait.js @@ -2,7 +2,7 @@ import {Trait} from '@avocado/entity'; export default class Roomed extends Trait { - static behaviorContextTypes() { + static behaviorTypes() { return { detachFromRoom: { type: 'void',