refactor: script
This commit is contained in:
parent
6fdf4e2327
commit
2f7b5d458f
1
TODO.md
1
TODO.md
|
@ -1,3 +1,4 @@
|
|||
- visibleEntities is way overloaded
|
||||
- entities should probably belong to room instead of layer
|
||||
- don't tick entities without any ticking traits
|
||||
- ~~production build~~
|
||||
|
|
|
@ -24,7 +24,9 @@
|
|||
"@avocado/math": "^2.0.0",
|
||||
"@avocado/persea": "^1.0.0",
|
||||
"@avocado/resource": "^2.0.0",
|
||||
"@avocado/sandbox": "^1.0.0",
|
||||
"@avocado/traits": "^2.0.0",
|
||||
"@babel/parser": "^7.13.13",
|
||||
"@latus/core": "2.0.0",
|
||||
"autoprefixer": "^9.8.6",
|
||||
"classnames": "^2.2.6",
|
||||
|
@ -33,6 +35,7 @@
|
|||
"lodash.get": "^4.4.2",
|
||||
"lodash.mapvalues": "^4.6.0",
|
||||
"lodash.set": "^4.3.2",
|
||||
"lru-cache": "^6.0.0",
|
||||
"natsort": "^2.0.2",
|
||||
"react-tabs": "^3.1.2"
|
||||
},
|
||||
|
|
|
@ -1,108 +0,0 @@
|
|||
import {TickingPromise} from '@avocado/core';
|
||||
import {compose, EventEmitter} from '@latus/core';
|
||||
|
||||
const decorate = compose(
|
||||
EventEmitter,
|
||||
);
|
||||
|
||||
class Actions {
|
||||
|
||||
constructor(expressions) {
|
||||
this.expressions = 'function' === typeof expressions ? expressions() : expressions;
|
||||
this._index = 0;
|
||||
this.promise = null;
|
||||
}
|
||||
|
||||
emitFinished() {
|
||||
this.emit('finished');
|
||||
}
|
||||
|
||||
get index() {
|
||||
return this._index;
|
||||
}
|
||||
|
||||
set index(index) {
|
||||
// Clear out the action promise.
|
||||
this.promise = null;
|
||||
this._index = index;
|
||||
}
|
||||
|
||||
get() {
|
||||
return this;
|
||||
}
|
||||
|
||||
tick(context, elapsed) {
|
||||
// Empty resolves immediately.
|
||||
if (this.expressions.length === 0) {
|
||||
this.emitFinished();
|
||||
return;
|
||||
}
|
||||
// If the action promise ticks, tick it.
|
||||
if (this.promise) {
|
||||
if (this.promise instanceof TickingPromise) {
|
||||
this.promise.tick(elapsed);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Actions execute immediately until a promise is made, or they're all executed.
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
// Run the action.
|
||||
const result = this.expressions[this.index](context);
|
||||
// Deferred result.
|
||||
if (result instanceof Promise) {
|
||||
this.promise = result;
|
||||
// eslint-disable-next-line no-console
|
||||
this.promise.catch(console.error).finally(() => this.prologue());
|
||||
break;
|
||||
}
|
||||
// Immediate result.
|
||||
this.prologue();
|
||||
// Need to break out immediately if required.
|
||||
if (0 === this.index) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
parallel(context) {
|
||||
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() {
|
||||
// Increment and wrap the index.
|
||||
this.index = (this.index + 1) % this.expressions.length;
|
||||
// If rolled over, the actions are finished.
|
||||
if (0 === this.index) {
|
||||
this.emitFinished();
|
||||
}
|
||||
}
|
||||
|
||||
serial(context) {
|
||||
return this.tickingPromise(context);
|
||||
}
|
||||
|
||||
tickingPromise(context) {
|
||||
return new TickingPromise(
|
||||
(resolve) => {
|
||||
this.once('finished', resolve);
|
||||
},
|
||||
(elapsed) => {
|
||||
this.tick(context, elapsed);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default decorate(Actions);
|
|
@ -1,52 +0,0 @@
|
|||
export function buildValue(value) {
|
||||
if (
|
||||
'object' === typeof value
|
||||
&& -1 !== [
|
||||
'condition',
|
||||
'expression',
|
||||
'expressions',
|
||||
'literal',
|
||||
].indexOf(value.type)
|
||||
) {
|
||||
return value;
|
||||
}
|
||||
return {
|
||||
type: 'literal',
|
||||
value,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildCondition(operator, operands) {
|
||||
return {
|
||||
type: 'condition',
|
||||
operator,
|
||||
operands: operands.map((operand) => buildValue(operand)),
|
||||
};
|
||||
}
|
||||
|
||||
export function buildExpression(path, value) {
|
||||
const expression = {
|
||||
type: 'expression',
|
||||
ops: path.map((key) => ({type: 'key', key})),
|
||||
};
|
||||
if ('undefined' !== typeof value) {
|
||||
expression.value = buildValue(value);
|
||||
}
|
||||
return expression;
|
||||
}
|
||||
|
||||
export function buildExpressions(expressions) {
|
||||
return {
|
||||
type: 'expressions',
|
||||
expressions: expressions.map((expression) => buildValue(expression)),
|
||||
};
|
||||
}
|
||||
|
||||
export function buildInvoke(path, args = []) {
|
||||
const expression = buildExpression(path);
|
||||
expression.ops.push({
|
||||
type: 'invoke',
|
||||
args: args.map((arg) => buildValue(arg)),
|
||||
});
|
||||
return expression;
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
let compilers;
|
||||
|
||||
function compilerFor(type, latus) {
|
||||
if (!compilers) {
|
||||
compilers = latus.get('%behaviorCompilers');
|
||||
}
|
||||
const {[type]: compiler} = compilers;
|
||||
return compiler;
|
||||
}
|
||||
|
||||
export default function compile(variant, latus) {
|
||||
const compiler = compilerFor(variant.type, latus);
|
||||
return compiler
|
||||
? compiler(variant)
|
||||
: () => Promise.reject(new TypeError(`No compiler for '${variant.type}', variant: ${variant}`));
|
||||
}
|
|
@ -1,53 +0,0 @@
|
|||
import compile from './compile';
|
||||
|
||||
export default (latus) => (condition) => {
|
||||
const {operator} = condition;
|
||||
const operands = condition.operands.map((condition) => compile(condition, latus));
|
||||
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);
|
||||
if (!Array.isArray(haystack)) {
|
||||
return false;
|
||||
}
|
||||
const needle = operands[1](context);
|
||||
return -1 !== haystack.indexOf(needle);
|
||||
}
|
||||
default: {
|
||||
throw new TypeError(`Undefined operator '${operator}'`);
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
|
@ -1,94 +0,0 @@
|
|||
import {fastApply} from '@avocado/core';
|
||||
|
||||
import compile from './compile';
|
||||
|
||||
function compileOp(op, latus) {
|
||||
let args;
|
||||
if ('invoke' === op.type) {
|
||||
args = op.args.map((arg) => (false === arg.compile ? arg : compile(arg, latus)));
|
||||
}
|
||||
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((arg) => (
|
||||
'function' === typeof arg ? arg(context) : arg
|
||||
)).concat(context);
|
||||
// Promises are resolved transparently.
|
||||
const apply = (args) => fastApply(previous, current, args);
|
||||
try {
|
||||
return evaluated.some((arg) => arg instanceof Promise)
|
||||
? Promise.all(evaluated).then(apply)
|
||||
: apply(evaluated);
|
||||
}
|
||||
catch (error) {
|
||||
throw new Error(`Behaving ${JSON.stringify(op, null, 2)}: ${error.stack}`);
|
||||
}
|
||||
}
|
||||
default:
|
||||
throw new TypeError(`Invalid expression op: '${op.type}'`);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function disambiguateResult(value, fn) {
|
||||
const apply = (value) => fn(value);
|
||||
return value instanceof Promise ? value.then(apply) : apply(value);
|
||||
}
|
||||
|
||||
export default (latus) => (expression) => {
|
||||
if (0 === expression.ops.length) {
|
||||
return () => undefined;
|
||||
}
|
||||
const assign = expression.value && compile(expression.value, latus);
|
||||
const ops = expression.ops.map((op) => compileOp(op, latus));
|
||||
const {ops: rawOps} = expression;
|
||||
const [, ...rest] = ops;
|
||||
return (context) => {
|
||||
let previous = null;
|
||||
let current = context.get(rawOps[0].key);
|
||||
if (0 === rest.length) {
|
||||
if (assign) {
|
||||
context.add(expression.ops[0].key, assign(context));
|
||||
return undefined;
|
||||
}
|
||||
return current;
|
||||
}
|
||||
for (let index = 0; index < rest.length; ++index) {
|
||||
const op = rest[index];
|
||||
// eslint-disable-next-line no-loop-func
|
||||
current = disambiguateResult(current, (current) => {
|
||||
let next;
|
||||
const isLastOp = index === ops.length - 2;
|
||||
if (!isLastOp || !assign) {
|
||||
if ('undefined' === typeof current) {
|
||||
return undefined;
|
||||
}
|
||||
next = op(context, previous, current);
|
||||
}
|
||||
else {
|
||||
const rawOp = rawOps[index + 1];
|
||||
switch (rawOp.type) {
|
||||
case 'key':
|
||||
if ('object' === typeof current) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
current[rawOp.key] = assign(context);
|
||||
next = undefined;
|
||||
}
|
||||
break;
|
||||
case 'invoke':
|
||||
next = undefined;
|
||||
break;
|
||||
default:
|
||||
throw new TypeError(`Invalid expression op: '${op.type}'`);
|
||||
}
|
||||
}
|
||||
previous = current;
|
||||
return next;
|
||||
});
|
||||
}
|
||||
return current;
|
||||
};
|
||||
};
|
|
@ -1,5 +0,0 @@
|
|||
import compile from './compile';
|
||||
|
||||
export default (latus) => ({expressions}) => () => (
|
||||
expressions.map((expression) => compile(expression, latus))
|
||||
);
|
|
@ -1,11 +0,0 @@
|
|||
import condition from './condition';
|
||||
import expression from './expression';
|
||||
import expressions from './expressions';
|
||||
import literal from './literal';
|
||||
|
||||
export default (latus) => ({
|
||||
condition: condition(latus),
|
||||
expression: expression(latus),
|
||||
expressions: expressions(latus),
|
||||
literal: literal(latus),
|
||||
});
|
|
@ -1 +0,0 @@
|
|||
export default () => ({value}) => () => value;
|
|
@ -1,118 +0,0 @@
|
|||
export default class Context {
|
||||
|
||||
constructor(defaults = {}, latus) {
|
||||
this.latus = latus;
|
||||
this.map = new Map();
|
||||
this.clear();
|
||||
this.addObjectMap(defaults);
|
||||
}
|
||||
|
||||
add(key, value) {
|
||||
if (key) {
|
||||
this.map.set(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
addObjectMap(map) {
|
||||
Object.entries(map)
|
||||
.forEach(([key, variable]) => (
|
||||
this.add(key, variable)
|
||||
));
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.destroy();
|
||||
this.add('context', this);
|
||||
this.addObjectMap(this.latus.get('%behaviorGlobals'));
|
||||
}
|
||||
|
||||
clone() {
|
||||
const clone = new Context({}, this.latus);
|
||||
clone.map = new Map(this.map);
|
||||
return clone;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
children() {
|
||||
return {
|
||||
add: {
|
||||
label: "Add $2 to context as '$1'",
|
||||
args: [
|
||||
{
|
||||
label: 'Key',
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
label: 'Value',
|
||||
type: 'any',
|
||||
},
|
||||
],
|
||||
type: 'void',
|
||||
},
|
||||
remove: {
|
||||
args: [
|
||||
{
|
||||
label: 'Key',
|
||||
type: 'string',
|
||||
},
|
||||
],
|
||||
type: 'void',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describeChildren() {
|
||||
return Array.from(this.map.entries())
|
||||
.reduce(
|
||||
(r, [key, value]) => ({
|
||||
...r,
|
||||
children: {
|
||||
...r.children,
|
||||
[key]: {
|
||||
label: key,
|
||||
type: this.constructor.descriptionFor(undefined, value, this.latus).type,
|
||||
},
|
||||
},
|
||||
}),
|
||||
{},
|
||||
);
|
||||
}
|
||||
|
||||
static descriptionFor(type, value, latus) {
|
||||
const types = latus.get('%behavior-types');
|
||||
if ('undefined' !== typeof type && types[type]) {
|
||||
return {
|
||||
children: types[type].children?.(value) || {},
|
||||
type,
|
||||
};
|
||||
}
|
||||
const entries = Object.entries(types);
|
||||
for (let i = 0; i < entries.length; ++i) {
|
||||
const [type, {children, infer}] = entries[i];
|
||||
if (infer(value)) {
|
||||
return {
|
||||
children: children?.(value) || {},
|
||||
type,
|
||||
};
|
||||
}
|
||||
}
|
||||
return {type: 'undefined'};
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.map.clear();
|
||||
}
|
||||
|
||||
get(key) {
|
||||
return this.map.get(key);
|
||||
}
|
||||
|
||||
has(key) {
|
||||
return this.map.has(key);
|
||||
}
|
||||
|
||||
remove(key) {
|
||||
return this.map.delete(key);
|
||||
}
|
||||
|
||||
}
|
|
@ -1,86 +0,0 @@
|
|||
import {TickingPromise} from '@avocado/core';
|
||||
|
||||
import Actions from '../actions';
|
||||
import compile from '../compilers/compile';
|
||||
|
||||
const serial = (expressions, context) => (new Actions(expressions)).serial(context);
|
||||
|
||||
export default {
|
||||
|
||||
conditional: (condition, expressions, context) => (
|
||||
condition ? serial(expressions, context) : undefined
|
||||
),
|
||||
|
||||
children: () => ({
|
||||
conditional: {
|
||||
type: 'void',
|
||||
args: [
|
||||
{
|
||||
type: 'condition',
|
||||
label: 'Condition',
|
||||
},
|
||||
{
|
||||
type: 'expressions',
|
||||
label: 'Expressions',
|
||||
},
|
||||
],
|
||||
},
|
||||
nop: {
|
||||
type: 'void',
|
||||
label: 'Idle',
|
||||
args: [],
|
||||
},
|
||||
parallel: {
|
||||
type: 'void',
|
||||
label: 'Run expressions in parallel',
|
||||
args: [
|
||||
{
|
||||
type: 'expressions',
|
||||
label: 'Expressions',
|
||||
},
|
||||
],
|
||||
},
|
||||
while: {
|
||||
type: 'void',
|
||||
label: 'Loop while condition',
|
||||
args: [
|
||||
{
|
||||
compile: false,
|
||||
type: 'condition',
|
||||
label: 'Condition',
|
||||
},
|
||||
{
|
||||
type: 'expressions',
|
||||
label: 'Expressions',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
|
||||
nop: () => {},
|
||||
|
||||
parallel: (expressions, context) => (new Actions(expressions)).parallel(context),
|
||||
|
||||
serial,
|
||||
|
||||
while: (condition, expressions, context) => {
|
||||
if (!compile(condition, context.get('latus'))(context)) {
|
||||
return undefined;
|
||||
}
|
||||
const actions = new Actions(expressions);
|
||||
const ticker = actions.serial(context);
|
||||
return new TickingPromise(
|
||||
(resolve) => {
|
||||
actions.on('finished', () => {
|
||||
if (!compile(condition, context.get('latus'))(context)) {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
},
|
||||
(elapsed) => {
|
||||
ticker.tick(elapsed);
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
};
|
|
@ -1,10 +1,12 @@
|
|||
import Flow from './flow';
|
||||
import {TickingPromise} from '@avocado/core';
|
||||
|
||||
import Timing from './timing';
|
||||
import Utility from './utility';
|
||||
|
||||
export default (latus) => ({
|
||||
latus,
|
||||
Flow,
|
||||
SIDE: process.env.SIDE,
|
||||
TickingPromise,
|
||||
Timing,
|
||||
Utility,
|
||||
});
|
||||
|
|
|
@ -76,30 +76,10 @@ export default {
|
|||
get: (object, path, defaultValue) => get(object, path, defaultValue),
|
||||
|
||||
log: (...args) => {
|
||||
// No context!
|
||||
args.pop();
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(...args);
|
||||
},
|
||||
|
||||
makeArray: (...args) => {
|
||||
// No context!
|
||||
args.pop();
|
||||
return args;
|
||||
},
|
||||
|
||||
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;
|
||||
},
|
||||
|
||||
merge: (l, r) => merge(l, r),
|
||||
|
||||
set: (object, path, value) => set(object, path, value),
|
||||
|
|
|
@ -1,20 +1,13 @@
|
|||
import {gatherWithLatus} from '@latus/core';
|
||||
|
||||
import compilers from './compilers';
|
||||
import globals from './globals';
|
||||
import types from './types';
|
||||
|
||||
export {default as Actions} from './actions';
|
||||
export * from './builders';
|
||||
export * from './testers';
|
||||
export {default as compile} from './compilers/compile';
|
||||
export {default as Context} from './context';
|
||||
|
||||
export default {
|
||||
hooks: {
|
||||
'@avocado/behavior/compilers': compilers,
|
||||
'@avocado/behavior/globals': globals,
|
||||
'@avocado/behavior/types': types,
|
||||
'@avocado/resource/resources': gatherWithLatus(
|
||||
require.context('./resources', false, /\.js$/),
|
||||
),
|
||||
'@avocado/traits/traits': gatherWithLatus(
|
||||
require.context('./traits', false, /\.js$/),
|
||||
),
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
import './component.scss';
|
||||
|
||||
import {Code} from '@avocado/persea';
|
||||
import {useJsonPatcher} from '@avocado/resource/persea';
|
||||
import {
|
||||
PropTypes,
|
||||
React,
|
||||
} from '@latus/react';
|
||||
|
||||
const ScriptComponent = ({path, resource}) => {
|
||||
const patch = useJsonPatcher();
|
||||
return (
|
||||
<Code
|
||||
code={resource}
|
||||
name={path}
|
||||
onChange={patch.onChange(path)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
ScriptComponent.displayName = 'ScriptComponent';
|
||||
|
||||
ScriptComponent.propTypes = {
|
||||
path: PropTypes.string.isRequired,
|
||||
resource: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default ScriptComponent;
|
|
@ -0,0 +1,3 @@
|
|||
.text-renderer {
|
||||
font-family: monospace;
|
||||
}
|
10
packages/behavior/src/persea/controllers/script/index.js
Normal file
10
packages/behavior/src/persea/controllers/script/index.js
Normal file
|
@ -0,0 +1,10 @@
|
|||
import {TextController} from '@avocado/resource/persea';
|
||||
|
||||
import Component from './component';
|
||||
|
||||
export default {
|
||||
Component,
|
||||
matcher: /\.js$/,
|
||||
fromBuffer: TextController.fromBuffer,
|
||||
toBuffer: TextController.toBuffer,
|
||||
};
|
|
@ -1,27 +1,16 @@
|
|||
import {gatherComponents} from '@latus/react';
|
||||
|
||||
import Condition from './components/condition';
|
||||
import Expression from './components/expression';
|
||||
import Expressions from './components/expressions';
|
||||
import Literal from './components/literal';
|
||||
import ScriptController from './controllers/script';
|
||||
|
||||
export {
|
||||
Condition,
|
||||
Expression,
|
||||
Expressions,
|
||||
Literal,
|
||||
ScriptController,
|
||||
};
|
||||
|
||||
export default {
|
||||
hooks: {
|
||||
'@latus/core/starting': async (latus) => {
|
||||
latus.set('%behavior-components', {
|
||||
condition: Condition,
|
||||
expression: Expression,
|
||||
expressions: Expressions,
|
||||
literal: Literal,
|
||||
});
|
||||
},
|
||||
'@avocado/resource/persea.controllers': () => [
|
||||
ScriptController,
|
||||
],
|
||||
'@avocado/traits/components': gatherComponents(
|
||||
require.context('./traits', false, /\.jsx$/),
|
||||
),
|
||||
|
|
|
@ -8,7 +8,7 @@ import {
|
|||
PropTypes,
|
||||
React,
|
||||
} from '@latus/react';
|
||||
import {Number} from '@avocado/persea';
|
||||
import {Code} from '@avocado/persea';
|
||||
import {
|
||||
Tab,
|
||||
Tabs,
|
||||
|
@ -16,11 +16,7 @@ import {
|
|||
TabPanel,
|
||||
} from 'react-tabs';
|
||||
|
||||
import Expression from '../components/expression';
|
||||
import Expressions from '../components/expressions';
|
||||
|
||||
const Behaved = ({
|
||||
entity,
|
||||
json,
|
||||
path,
|
||||
}) => {
|
||||
|
@ -29,113 +25,36 @@ const Behaved = ({
|
|||
<div className="behaved">
|
||||
<Tabs>
|
||||
<TabList>
|
||||
<Tab>Daemons</Tab>
|
||||
<Tab>Routines</Tab>
|
||||
<Tab>Collectives</Tab>
|
||||
<Tab>Daemons</Tab>
|
||||
</TabList>
|
||||
<div className="behaved__tab-panels">
|
||||
<TabPanel>
|
||||
<JsonTabs
|
||||
createPanel={(expressions, key) => (
|
||||
<Expressions
|
||||
context={entity.context}
|
||||
value={expressions}
|
||||
path={join(path, 'params/daemons', key)}
|
||||
createPanel={(code, key) => (
|
||||
<Code
|
||||
code={code}
|
||||
name={join(path, 'params/routines', key)}
|
||||
onChange={patch.onChange(join(path, 'params/routines', key))}
|
||||
/>
|
||||
)}
|
||||
defaultValue={{
|
||||
type: 'expressions',
|
||||
expressions: [],
|
||||
}}
|
||||
path={join(path, 'params/daemons')}
|
||||
map={json.params.daemons}
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<JsonTabs
|
||||
createPanel={(expressions, key) => (
|
||||
<Expressions
|
||||
context={entity.context}
|
||||
value={expressions}
|
||||
path={join(path, 'params/routines', key)}
|
||||
/>
|
||||
)}
|
||||
defaultValue={{
|
||||
type: 'expressions',
|
||||
expressions: [],
|
||||
}}
|
||||
defaultValue=""
|
||||
path={join(path, 'params/routines')}
|
||||
map={json.params.routines}
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<JsonTabs
|
||||
createPanel={(
|
||||
{
|
||||
find,
|
||||
max,
|
||||
reset,
|
||||
threshold,
|
||||
},
|
||||
key,
|
||||
) => (
|
||||
<>
|
||||
<div className="label">
|
||||
<div className="vertical">Find</div>
|
||||
<Expression
|
||||
context={entity.context}
|
||||
value={find}
|
||||
path={join(path, 'params/collectives', key, 'find')}
|
||||
type="array"
|
||||
/>
|
||||
</div>
|
||||
<div className="label">
|
||||
<div className="vertical">Reset</div>
|
||||
<Expressions
|
||||
context={entity.context}
|
||||
value={reset}
|
||||
path={join(path, 'params/collectives', key, 'reset')}
|
||||
/>
|
||||
</div>
|
||||
<div className="label">
|
||||
<div>Threshold</div>
|
||||
<Number
|
||||
onChange={patch.onChange(join(path, 'params/collectives', key, 'threshold'))}
|
||||
value={threshold}
|
||||
/>
|
||||
</div>
|
||||
<div className="label">
|
||||
<div>Max</div>
|
||||
<Number
|
||||
onChange={patch.onChange(join(path, 'params/collectives', key, 'max'))}
|
||||
value={max}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
createPanel={(code, key) => (
|
||||
<Code
|
||||
code={code}
|
||||
name={join(path, 'params/daemons', key)}
|
||||
onChange={patch.onChange(join(path, 'params/daemons', key))}
|
||||
/>
|
||||
)}
|
||||
defaultValue={{
|
||||
find: {
|
||||
type: 'expression',
|
||||
ops: [
|
||||
{
|
||||
type: 'key',
|
||||
key: 'entity',
|
||||
},
|
||||
{
|
||||
type: 'key',
|
||||
key: 'list',
|
||||
},
|
||||
],
|
||||
},
|
||||
max: 128,
|
||||
reset: {
|
||||
type: 'expressions',
|
||||
expressions: [],
|
||||
},
|
||||
threshold: 1,
|
||||
}}
|
||||
path={join(path, 'params/collectives')}
|
||||
map={json.params.collectives}
|
||||
defaultValue=""
|
||||
path={join(path, 'params/daemons')}
|
||||
map={json.params.daemons}
|
||||
/>
|
||||
</TabPanel>
|
||||
</div>
|
||||
|
@ -147,12 +66,9 @@ const Behaved = ({
|
|||
Behaved.displayName = 'Behaved';
|
||||
|
||||
Behaved.propTypes = {
|
||||
entity: PropTypes.shape({
|
||||
context: PropTypes.shape({}),
|
||||
}).isRequired,
|
||||
entity: PropTypes.shape({}).isRequired,
|
||||
json: PropTypes.shape({
|
||||
params: PropTypes.shape({
|
||||
collectives: PropTypes.shape({}),
|
||||
daemons: PropTypes.shape({}),
|
||||
routines: PropTypes.shape({}),
|
||||
}),
|
||||
|
|
180
packages/behavior/src/resources/script.js
Normal file
180
packages/behavior/src/resources/script.js
Normal file
|
@ -0,0 +1,180 @@
|
|||
import {TickingPromise} from '@avocado/core';
|
||||
import {Resource} from '@avocado/resource';
|
||||
import {Sandbox} from '@avocado/sandbox';
|
||||
import {parse} from '@babel/parser';
|
||||
import {compose, EventEmitter} from '@latus/core';
|
||||
import LRU from 'lru-cache';
|
||||
|
||||
const cache = new LRU({
|
||||
max: 128,
|
||||
maxAge: Infinity,
|
||||
});
|
||||
|
||||
const empty = {
|
||||
type: 'File',
|
||||
program: {
|
||||
type: 'Program',
|
||||
sourceType: 'module',
|
||||
body: [],
|
||||
directives: [],
|
||||
},
|
||||
};
|
||||
|
||||
export default (latus) => {
|
||||
const decorate = compose(
|
||||
EventEmitter,
|
||||
);
|
||||
return class Script extends decorate(Resource) {
|
||||
|
||||
constructor(sandbox) {
|
||||
super();
|
||||
this.sandbox = sandbox || new Sandbox(empty);
|
||||
this.promise = null;
|
||||
this.on('finished', this.reset, this);
|
||||
}
|
||||
|
||||
get context() {
|
||||
return this.sandbox.context;
|
||||
}
|
||||
|
||||
set context(context) {
|
||||
this.sandbox.context = context;
|
||||
}
|
||||
|
||||
static createContext(locals = {}) {
|
||||
return {
|
||||
...latus.get('%behaviorGlobals'),
|
||||
...locals,
|
||||
};
|
||||
}
|
||||
|
||||
async evaluate(callback) {
|
||||
this.sandbox.reset();
|
||||
let {done, value} = this.sandbox.next();
|
||||
if (value instanceof Promise) {
|
||||
await value;
|
||||
}
|
||||
while (!done) {
|
||||
({done, value} = this.sandbox.next());
|
||||
if (value instanceof Promise) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await value;
|
||||
}
|
||||
}
|
||||
if (value instanceof Promise) {
|
||||
value.then(callback);
|
||||
}
|
||||
else {
|
||||
callback(value);
|
||||
}
|
||||
}
|
||||
|
||||
static fromCode(code, context) {
|
||||
let ast;
|
||||
if (cache.has(code)) {
|
||||
ast = cache.get(code);
|
||||
}
|
||||
else {
|
||||
cache.set(code, ast = this.parse(code));
|
||||
}
|
||||
return new this(new Sandbox(ast, this.createContext(context || {})));
|
||||
}
|
||||
|
||||
static async load(uriOrCode, context) {
|
||||
if ('/'.charCodeAt(0) === uriOrCode.charCodeAt(0)) {
|
||||
const script = await super.load(uriOrCode);
|
||||
script.context = this.createContext(context || {});
|
||||
return script;
|
||||
}
|
||||
return this.fromCode(uriOrCode, context);
|
||||
}
|
||||
|
||||
static loadTickingPromise(uriOrCode, context) {
|
||||
let tickingPromise;
|
||||
return new TickingPromise(
|
||||
(resolve) => {
|
||||
this.load(uriOrCode, context).then((script) => {
|
||||
tickingPromise = script.tickingPromise();
|
||||
resolve(tickingPromise);
|
||||
});
|
||||
},
|
||||
(elapsed) => {
|
||||
tickingPromise?.tick?.(elapsed);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async load(buffer, uri) {
|
||||
if (!buffer) {
|
||||
this.ast = new Sandbox(empty);
|
||||
}
|
||||
if (cache.has(uri)) {
|
||||
this.sandbox = new Sandbox(cache.get(uri));
|
||||
return;
|
||||
}
|
||||
const ast = this.constructor.parse(Buffer.from(buffer).toString('utf8'));
|
||||
cache.set(uri, ast);
|
||||
this.sandbox = new Sandbox(ast);
|
||||
}
|
||||
|
||||
static parse(script) {
|
||||
return parse(
|
||||
script,
|
||||
{
|
||||
allowAwaitOutsideFunction: true,
|
||||
allowReturnOutsideFunction: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.promise = null;
|
||||
this.sandbox.reset();
|
||||
}
|
||||
|
||||
tick(elapsed) {
|
||||
if (this.promise) {
|
||||
if (this.promise instanceof TickingPromise) {
|
||||
this.promise.tick(elapsed);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
const {done, value} = this.sandbox.next();
|
||||
if (value) {
|
||||
const {value: result} = value;
|
||||
if (result instanceof Promise) {
|
||||
result
|
||||
// eslint-disable-next-line no-console
|
||||
.catch(console.error)
|
||||
.finally(() => {
|
||||
if (done) {
|
||||
this.emit('finished');
|
||||
}
|
||||
this.promise = null;
|
||||
});
|
||||
this.promise = result;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (done) {
|
||||
this.emit('finished');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tickingPromise() {
|
||||
return new TickingPromise(
|
||||
(resolve) => {
|
||||
this.once('finished', resolve);
|
||||
},
|
||||
(elapsed) => {
|
||||
this.tick(elapsed);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
};
|
||||
};
|
3
packages/behavior/src/sandbox.js
Normal file
3
packages/behavior/src/sandbox.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
import {Sandbox} from '@avocado/sandbox';
|
||||
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
export const isInvocation = (op) => 'invoke' === op.type;
|
||||
|
||||
export const isKey = (op) => 'key' === op.type;
|
|
@ -1,10 +1,6 @@
|
|||
import {mapValuesAsync} from '@avocado/core';
|
||||
import {StateProperty, Trait} from '@avocado/traits';
|
||||
import {compose} from '@latus/core';
|
||||
import mapValues from 'lodash.mapvalues';
|
||||
|
||||
import Actions from '../actions';
|
||||
import compile from '../compilers/compile';
|
||||
import Context from '../context';
|
||||
|
||||
const decorate = compose(
|
||||
StateProperty('activeCollective'),
|
||||
|
@ -22,7 +18,7 @@ export default (latus) => class Behaved extends decorate(Trait) {
|
|||
|
||||
#collectives = [];
|
||||
|
||||
#context = new Context({}, latus);
|
||||
#context = {};
|
||||
|
||||
#currentRoutine = '';
|
||||
|
||||
|
@ -35,6 +31,8 @@ export default (latus) => class Behaved extends decorate(Trait) {
|
|||
({
|
||||
currentRoutine: this.#currentRoutine,
|
||||
} = this.constructor.defaultState());
|
||||
const {Script} = latus.get('%resources');
|
||||
this.#context = Script.createContext();
|
||||
}
|
||||
|
||||
get context() {
|
||||
|
@ -43,6 +41,7 @@ export default (latus) => class Behaved extends decorate(Trait) {
|
|||
|
||||
static defaultParams() {
|
||||
return {
|
||||
codeRoutines: {},
|
||||
collectives: {},
|
||||
daemons: {},
|
||||
routines: {},
|
||||
|
@ -114,7 +113,7 @@ export default (latus) => class Behaved extends decorate(Trait) {
|
|||
|
||||
destroy() {
|
||||
super.destroy();
|
||||
this.#context.destroy();
|
||||
this.#context = {};
|
||||
this.#currentRoutine = undefined;
|
||||
this.#routines = undefined;
|
||||
}
|
||||
|
@ -133,38 +132,27 @@ export default (latus) => class Behaved extends decorate(Trait) {
|
|||
};
|
||||
}
|
||||
|
||||
static async loadScripts(scripts, context) {
|
||||
return mapValuesAsync(scripts, async (codeOrUri) => {
|
||||
const {Script} = latus.get('%resources');
|
||||
const script = await Script.load(codeOrUri);
|
||||
script.context = context;
|
||||
return script;
|
||||
});
|
||||
}
|
||||
|
||||
async load(json) {
|
||||
await super.load(json);
|
||||
this.#context = new Context(
|
||||
{
|
||||
entity: this.entity,
|
||||
},
|
||||
latus,
|
||||
);
|
||||
this.#daemons = Object.values(this.params.daemons)
|
||||
.map((daemon) => new Actions(compile(daemon, latus)));
|
||||
this.#routines = mapValues(
|
||||
this.params.routines,
|
||||
(routine) => new Actions(compile(routine, latus)),
|
||||
);
|
||||
this.#collectives = Object.entries(this.params.collectives)
|
||||
.map(([
|
||||
key,
|
||||
{
|
||||
find,
|
||||
max,
|
||||
reset,
|
||||
threshold,
|
||||
},
|
||||
]) => [
|
||||
key,
|
||||
{
|
||||
find: compile(find, latus),
|
||||
max,
|
||||
reset: compile(reset, latus),
|
||||
threshold,
|
||||
},
|
||||
]);
|
||||
const {Script} = latus.get('%resources');
|
||||
this.#context = Script.createContext({
|
||||
entity: this.entity,
|
||||
});
|
||||
const daemons = {
|
||||
...this.entity.invokeHookReduced('daemons'),
|
||||
...this.params.daemons,
|
||||
};
|
||||
this.#daemons = Object.values(await this.constructor.loadScripts(daemons, this.#context));
|
||||
this.#routines = await this.constructor.loadScripts(this.params.routines, this.#context);
|
||||
this.updateCurrentRoutine(this.state.currentRoutine);
|
||||
super.isBehaving = 'client' !== process.env.SIDE;
|
||||
}
|
||||
|
@ -172,11 +160,6 @@ export default (latus) => class Behaved extends decorate(Trait) {
|
|||
methods() {
|
||||
return {
|
||||
|
||||
jumpToRoutine: (routine, index) => {
|
||||
this.updateCurrentRoutine(routine);
|
||||
this.#currentRoutine.index = 'number' === typeof index ? index : 0;
|
||||
},
|
||||
|
||||
leaveCollection: () => {
|
||||
this.entity.activeCollective = null;
|
||||
},
|
||||
|
@ -189,53 +172,19 @@ export default (latus) => class Behaved extends decorate(Trait) {
|
|||
this.#accumulator += elapsed;
|
||||
if (this.#accumulator >= STATIC_INTERVAL) {
|
||||
for (let i = 0; i < this.#daemons.length; ++i) {
|
||||
this.#daemons[i].tick(this.#context, elapsed);
|
||||
}
|
||||
for (let i = 0; i < this.#collectives.length; ++i) {
|
||||
const [
|
||||
key,
|
||||
{
|
||||
find,
|
||||
max,
|
||||
reset,
|
||||
threshold,
|
||||
},
|
||||
] = this.#collectives[i];
|
||||
if (this.entity.activeCollective && this.entity.activeCollective !== key) {
|
||||
// eslint-disable-next-line no-continue
|
||||
continue;
|
||||
}
|
||||
const crew = find(this.#context);
|
||||
const isInCrew = -1 !== crew.indexOf(this.entity);
|
||||
const hasLength = (
|
||||
!this.entity.activeCollective
|
||||
|| 'threshold' === this.entity.context.get('crew').length
|
||||
)
|
||||
? crew.length >= threshold
|
||||
: crew.length > this.entity.context.get('crew').length;
|
||||
if (isInCrew && hasLength && crew.length < max) {
|
||||
crew.forEach((entity, i) => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
entity.activeCollective = key;
|
||||
const {context} = entity;
|
||||
context.add('crew', crew);
|
||||
context.add('index', i);
|
||||
reset().forEach((expr) => {
|
||||
expr(context);
|
||||
});
|
||||
});
|
||||
}
|
||||
this.#daemons[i].tick(elapsed);
|
||||
}
|
||||
this.#accumulator -= STATIC_INTERVAL;
|
||||
}
|
||||
if (this.#currentRoutine) {
|
||||
this.#currentRoutine.tick(this.#context, elapsed);
|
||||
this.#currentRoutine.tick(elapsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateCurrentRoutine(currentRoutine) {
|
||||
this.#currentRoutine = this.#routines[currentRoutine];
|
||||
this.#currentRoutine.reset();
|
||||
super.currentRoutine = currentRoutine;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,91 +0,0 @@
|
|||
import Context from './context';
|
||||
import Flow from './globals/flow';
|
||||
import Timing from './globals/timing';
|
||||
import Utility from './globals/utility';
|
||||
|
||||
export default () => ({
|
||||
array: {
|
||||
children: () => ({
|
||||
length: {
|
||||
type: 'number',
|
||||
},
|
||||
}),
|
||||
create: (v) => ({type: 'literal', value: 'undefined' === typeof v ? [] : v}),
|
||||
infer: (v) => Array.isArray(v),
|
||||
},
|
||||
bool: {
|
||||
create: (v) => ({type: 'literal', value: 'undefined' === typeof v ? false : v}),
|
||||
infer: (v) => 'boolean' === typeof v,
|
||||
},
|
||||
condition: {
|
||||
create: () => ({
|
||||
type: 'condition',
|
||||
operator: 'is',
|
||||
operands: [
|
||||
{
|
||||
type: 'literal',
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
type: 'literal',
|
||||
value: true,
|
||||
},
|
||||
],
|
||||
}),
|
||||
infer: (v) => 'condition' === v?.type,
|
||||
},
|
||||
Context: {
|
||||
children: Context.prototype.children,
|
||||
infer: (v) => v?.children === Context.prototype.children,
|
||||
},
|
||||
expression: {
|
||||
create: () => ({type: 'expression', ops: []}),
|
||||
infer: (v) => 'expression' === v?.type,
|
||||
},
|
||||
expressions: {
|
||||
create: () => ({type: 'expressions', expressions: []}),
|
||||
infer: (v) => 'expressions' === v?.type,
|
||||
},
|
||||
flow: {
|
||||
children: Flow.children,
|
||||
infer: (v) => v?.children === Flow.children,
|
||||
},
|
||||
function: {
|
||||
children: () => ({
|
||||
name: {
|
||||
type: 'string',
|
||||
},
|
||||
}),
|
||||
create: () => null,
|
||||
infer: (v) => 'function' === typeof v,
|
||||
},
|
||||
number: {
|
||||
create: (v) => ({type: 'literal', value: 'undefined' === typeof v ? 0 : v}),
|
||||
infer: (v) => 'number' === typeof v,
|
||||
},
|
||||
timing: {
|
||||
children: Timing.children,
|
||||
infer: (v) => v?.children === Timing.children,
|
||||
},
|
||||
utility: {
|
||||
children: Utility.children,
|
||||
infer: (v) => v?.children === Utility.children,
|
||||
},
|
||||
object: {
|
||||
create: () => ({type: 'literal', value: '{}'}),
|
||||
infer: (v) => 'object' === typeof v,
|
||||
},
|
||||
string: {
|
||||
children: () => ({
|
||||
length: {
|
||||
type: 'number',
|
||||
},
|
||||
}),
|
||||
create: (v) => ({type: 'literal', value: 'undefined' === typeof v ? '' : v}),
|
||||
infer: (v) => 'string' === typeof v,
|
||||
},
|
||||
undefined: {
|
||||
create: () => ({type: 'literal'}),
|
||||
infer: () => true,
|
||||
},
|
||||
});
|
|
@ -1,146 +0,0 @@
|
|||
import {Latus} from '@latus/core';
|
||||
import {expect} from 'chai';
|
||||
|
||||
import {
|
||||
buildExpression,
|
||||
buildExpressions,
|
||||
buildInvoke,
|
||||
buildValue,
|
||||
} from '../src/builders';
|
||||
|
||||
const asyncWait = () => new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
let latus;
|
||||
let Entity;
|
||||
beforeEach(async () => {
|
||||
latus = Latus.mock({
|
||||
'@avocado/behavior': require('../src'),
|
||||
'@avocado/entity': require('@avocado/entity'),
|
||||
'@avocado/resource': require('@avocado/resource'),
|
||||
'@avocado/traits': require('@avocado/traits'),
|
||||
});
|
||||
await Promise.all(latus.invokeFlat('@latus/core/starting'));
|
||||
({Entity} = latus.get('%resources'));
|
||||
});
|
||||
describe('Behaved', () => {
|
||||
let entity;
|
||||
beforeEach(async () => {
|
||||
entity = await Entity.load({
|
||||
traits: {
|
||||
Behaved: {
|
||||
params: {
|
||||
routines: {
|
||||
initial: buildExpressions([
|
||||
buildInvoke(
|
||||
['entity', 'forceMovement'],
|
||||
[
|
||||
buildValue([10, 0]),
|
||||
],
|
||||
),
|
||||
buildInvoke(
|
||||
['Timing', 'wait'],
|
||||
[
|
||||
buildValue(1),
|
||||
],
|
||||
),
|
||||
]),
|
||||
another: buildExpressions([
|
||||
buildInvoke(
|
||||
['entity', 'forceMovement'],
|
||||
[
|
||||
buildValue([20, 0]),
|
||||
],
|
||||
),
|
||||
]),
|
||||
one: buildExpressions([
|
||||
buildInvoke(
|
||||
['entity', 'forceMovement'],
|
||||
[
|
||||
buildValue([30, 0]),
|
||||
],
|
||||
),
|
||||
buildInvoke(
|
||||
['Timing', 'wait'],
|
||||
[
|
||||
buildValue(0),
|
||||
],
|
||||
),
|
||||
buildInvoke(
|
||||
['entity', 'jumpToRoutine'],
|
||||
[
|
||||
buildValue('two'),
|
||||
],
|
||||
),
|
||||
]),
|
||||
two: buildExpressions([
|
||||
buildInvoke(
|
||||
['entity', 'forceMovement'],
|
||||
[
|
||||
buildValue([40, 0]),
|
||||
],
|
||||
),
|
||||
buildInvoke(
|
||||
['entity', 'jumpToRoutine'],
|
||||
[
|
||||
buildValue('one'),
|
||||
],
|
||||
),
|
||||
]),
|
||||
},
|
||||
},
|
||||
},
|
||||
Mobile: {},
|
||||
Positioned: {},
|
||||
},
|
||||
});
|
||||
});
|
||||
it('exists', async () => {
|
||||
expect(entity.is('Behaved')).to.be.true;
|
||||
});
|
||||
if ('client' !== process.env.SIDE) {
|
||||
it('runs routines', async () => {
|
||||
expect(entity.position).to.deep.equal([0, 0]);
|
||||
entity.tick(0);
|
||||
await asyncWait();
|
||||
expect(entity.position).to.deep.equal([10, 0]);
|
||||
entity.tick(1);
|
||||
await asyncWait();
|
||||
expect(entity.position).to.deep.equal([10, 0]);
|
||||
entity.isBehaving = false;
|
||||
entity.tick(1);
|
||||
await asyncWait();
|
||||
expect(entity.position).to.deep.equal([10, 0]);
|
||||
entity.isBehaving = true;
|
||||
entity.tick(1);
|
||||
await asyncWait();
|
||||
expect(entity.position).to.deep.equal([20, 0]);
|
||||
});
|
||||
it('can change routines', async () => {
|
||||
entity.currentRoutine = 'another';
|
||||
expect(entity.position).to.deep.equal([0, 0]);
|
||||
entity.tick(0);
|
||||
await asyncWait();
|
||||
expect(entity.position).to.deep.equal([20, 0]);
|
||||
});
|
||||
it('allows routines to set routine', async () => {
|
||||
entity.currentRoutine = 'one';
|
||||
expect(entity.position).to.deep.equal([0, 0]);
|
||||
entity.tick(0);
|
||||
await asyncWait();
|
||||
expect(entity.currentRoutine).to.equal('one');
|
||||
expect(entity.position).to.deep.equal([30, 0]);
|
||||
entity.tick(0);
|
||||
await asyncWait();
|
||||
expect(entity.currentRoutine).to.equal('one');
|
||||
expect(entity.position).to.deep.equal([30, 0]);
|
||||
entity.tick(0);
|
||||
await asyncWait();
|
||||
expect(entity.currentRoutine).to.equal('two');
|
||||
expect(entity.position).to.deep.equal([30, 0]);
|
||||
entity.tick(0);
|
||||
await asyncWait();
|
||||
expect(entity.currentRoutine).to.equal('one');
|
||||
expect(entity.position).to.deep.equal([70, 0]);
|
||||
});
|
||||
}
|
||||
});
|
|
@ -1,32 +0,0 @@
|
|||
import {Latus} from '@latus/core';
|
||||
import {expect} from 'chai';
|
||||
|
||||
import {
|
||||
buildCondition,
|
||||
buildExpression,
|
||||
buildValue,
|
||||
} from '../src/builders';
|
||||
import Context from '../src/context';
|
||||
import compile from '../src/compilers/compile';
|
||||
|
||||
let latus;
|
||||
let context;
|
||||
beforeEach(async () => {
|
||||
latus = Latus.mock({
|
||||
'@avocado/behavior': require('../src'),
|
||||
});
|
||||
await Promise.all(latus.invokeFlat('@latus/core/starting'));
|
||||
context = new Context({}, latus);
|
||||
});
|
||||
it('builds values', async () => {
|
||||
const passthroughs = [
|
||||
buildCondition('is', []),
|
||||
buildExpression([]),
|
||||
buildValue(69),
|
||||
];
|
||||
for (let i = 0; i < passthroughs.length; i++) {
|
||||
const passthrough = passthroughs[i];
|
||||
expect(passthrough).to.equal(buildValue(passthrough));
|
||||
}
|
||||
expect(buildValue(420)).to.deep.equal({type: 'literal', value: 420});
|
||||
});
|
|
@ -1,111 +0,0 @@
|
|||
import {Latus} from '@latus/core';
|
||||
import {expect} from 'chai';
|
||||
|
||||
import {
|
||||
buildCondition,
|
||||
buildValue,
|
||||
} from '../src/builders';
|
||||
import Context from '../src/context';
|
||||
import compile from '../src/compilers/compile';
|
||||
|
||||
let latus;
|
||||
let context;
|
||||
beforeEach(async () => {
|
||||
latus = Latus.mock({
|
||||
'@avocado/behavior': require('../src'),
|
||||
});
|
||||
await Promise.all(latus.invokeFlat('@latus/core/starting'));
|
||||
context = new Context({}, latus);
|
||||
});
|
||||
describe('Condition', () => {
|
||||
it('has equality operator', async () => {
|
||||
const condition = compile(buildCondition('is', [
|
||||
buildValue(69),
|
||||
buildValue(69),
|
||||
]), latus);
|
||||
expect(condition(context)).to.be.true;
|
||||
});
|
||||
it('has inequality operator', async () => {
|
||||
const condition = compile(buildCondition('isnt', [
|
||||
buildValue(69),
|
||||
buildValue(68),
|
||||
]), latus);
|
||||
expect(condition(context)).to.be.true;
|
||||
});
|
||||
it('has greater than operator', async () => {
|
||||
const condition = compile(buildCondition('>', [
|
||||
buildValue(69),
|
||||
buildValue(68),
|
||||
]), latus);
|
||||
expect(condition(context)).to.be.true;
|
||||
});
|
||||
it('has greater than or equal operator', async () => {
|
||||
expect(compile(buildCondition('>=', [
|
||||
buildValue(69),
|
||||
buildValue(69),
|
||||
]), latus)(context)).to.be.true;
|
||||
expect(compile(buildCondition('>=', [
|
||||
buildValue(69),
|
||||
buildValue(68),
|
||||
]), latus)(context)).to.be.true;
|
||||
});
|
||||
it('has less than operator', async () => {
|
||||
const condition = compile(buildCondition('<', [
|
||||
buildValue(69),
|
||||
buildValue(70),
|
||||
]), latus);
|
||||
expect(condition(context)).to.be.true;
|
||||
});
|
||||
it('has less than or equal operator', async () => {
|
||||
expect(compile(buildCondition('<=', [
|
||||
buildValue(69),
|
||||
buildValue(69),
|
||||
]), latus)(context)).to.be.true;
|
||||
expect(compile(buildCondition('<=', [
|
||||
buildValue(69),
|
||||
buildValue(70),
|
||||
]), latus)(context)).to.be.true;
|
||||
});
|
||||
it('has or operator', async () => {
|
||||
expect(compile(buildCondition('or', [
|
||||
buildValue(true),
|
||||
buildValue(false),
|
||||
]), latus)(context)).to.be.true;
|
||||
expect(compile(buildCondition('or', [
|
||||
buildValue(true),
|
||||
buildValue(true),
|
||||
]), latus)(context)).to.be.true;
|
||||
expect(compile(buildCondition('or', [
|
||||
buildValue(false),
|
||||
buildValue(false),
|
||||
]), latus)(context)).to.be.false;
|
||||
});
|
||||
it('has and operator', async () => {
|
||||
expect(compile(buildCondition('and', [
|
||||
buildValue(true),
|
||||
buildValue(false),
|
||||
]), latus)(context)).to.be.false;
|
||||
expect(compile(buildCondition('and', [
|
||||
buildValue(true),
|
||||
buildValue(true),
|
||||
]), latus)(context)).to.be.true;
|
||||
expect(compile(buildCondition('and', [
|
||||
buildValue(false),
|
||||
buildValue(false),
|
||||
]), latus)(context)).to.be.false;
|
||||
});
|
||||
it('has contains operator', async () => {
|
||||
expect(compile(buildCondition('contains', [
|
||||
buildValue([1, 2, 3, 69]),
|
||||
buildValue(69),
|
||||
]), latus)(context)).to.be.true;
|
||||
expect(compile(buildCondition('contains', [
|
||||
buildValue([1, 2, 3]),
|
||||
buildValue(69),
|
||||
]), latus)(context)).to.be.false;
|
||||
expect(compile(buildCondition('contains', [
|
||||
buildValue(69),
|
||||
buildValue(69),
|
||||
]), latus)(context)).to.be.false;
|
||||
});
|
||||
});
|
|
@ -1,40 +0,0 @@
|
|||
import {Latus} from '@latus/core';
|
||||
import {expect} from 'chai';
|
||||
|
||||
import {
|
||||
buildInvoke,
|
||||
buildExpression,
|
||||
} from '../src/builders';
|
||||
import Context from '../src/context';
|
||||
import compile from '../src/compilers/compile';
|
||||
|
||||
let latus;
|
||||
let context;
|
||||
beforeEach(async () => {
|
||||
latus = Latus.mock({
|
||||
'@avocado/behavior': require('../src'),
|
||||
});
|
||||
await Promise.all(latus.invokeFlat('@latus/core/starting'));
|
||||
context = new Context({}, latus);
|
||||
});
|
||||
describe('Expression', () => {
|
||||
it('evaluates value expressions', async () => {
|
||||
const test = {foo: 69};
|
||||
context.add('test', test);
|
||||
const expression = compile(buildExpression(['test', 'foo']), latus);
|
||||
expect(expression(context)).to.equal(test.foo);
|
||||
});
|
||||
it('evaluates assignment expressions', async () => {
|
||||
const test = {foo: 69};
|
||||
context.add('test', test);
|
||||
const expression = compile(buildExpression(['test', 'foo'], 420), latus);
|
||||
expression(context);
|
||||
expect(test.foo).to.equal(420);
|
||||
});
|
||||
it('invokes', async () => {
|
||||
const test = {foo: () => 69};
|
||||
context.add('test', test);
|
||||
const expression = compile(buildInvoke(['test', 'foo']), latus);
|
||||
expect(expression(context)).to.equal(69);
|
||||
});
|
||||
});
|
|
@ -1,86 +0,0 @@
|
|||
import {Latus} from '@latus/core';
|
||||
import {expect} from 'chai';
|
||||
|
||||
import {
|
||||
buildCondition,
|
||||
buildExpression,
|
||||
buildExpressions,
|
||||
buildInvoke,
|
||||
buildValue,
|
||||
} from '../src/builders';
|
||||
import Context from '../src/context';
|
||||
import compile from '../src/compilers/compile';
|
||||
|
||||
let latus;
|
||||
let context;
|
||||
beforeEach(async () => {
|
||||
latus = Latus.mock({
|
||||
'@avocado/behavior': require('../src'),
|
||||
});
|
||||
await Promise.all(latus.invokeFlat('@latus/core/starting'));
|
||||
context = new Context({}, latus);
|
||||
});
|
||||
describe('Flow', () => {
|
||||
it('does conditional', async () => {
|
||||
const test = {foo: 69};
|
||||
context.add('test', test);
|
||||
expect(compile(buildExpression(['test', 'foo']), latus)(context)).to.equal(69);
|
||||
const tickingPromise = compile(buildInvoke(
|
||||
['Flow', 'conditional'],
|
||||
[
|
||||
false,
|
||||
compile(buildExpressions([
|
||||
buildExpression(['test', 'foo'], 420),
|
||||
]), latus),
|
||||
],
|
||||
), latus)(context);
|
||||
tickingPromise && tickingPromise.tick();
|
||||
expect(compile(buildExpression(['test', 'foo']), latus)(context)).to.equal(69);
|
||||
const tickingPromise2 = compile(buildInvoke(
|
||||
['Flow', 'conditional'],
|
||||
[
|
||||
true,
|
||||
compile(buildExpressions([
|
||||
buildExpression(['test', 'foo'], 420),
|
||||
]), latus),
|
||||
],
|
||||
), latus)(context);
|
||||
tickingPromise2 && tickingPromise2.tick();
|
||||
expect(compile(buildExpression(['test', 'foo']), latus)(context)).to.equal(420);
|
||||
});
|
||||
it('does nop', async () => {
|
||||
compile(buildInvoke(['Flow', 'nop']), latus)(context);
|
||||
});
|
||||
it('does parallel actions', async () => {
|
||||
const tickingPromise = compile(buildInvoke(
|
||||
['Flow', 'parallel'],
|
||||
[
|
||||
buildExpressions([
|
||||
buildInvoke(['Timing', 'wait'], [100]),
|
||||
buildInvoke(['Timing', 'wait'], [200]),
|
||||
]),
|
||||
],
|
||||
), latus)(context);
|
||||
tickingPromise.tick(200);
|
||||
return tickingPromise;
|
||||
});
|
||||
it('does serial actions', async () => {
|
||||
const DELAY = 30;
|
||||
const tickingPromise = compile(buildInvoke(
|
||||
['Flow', 'serial'],
|
||||
[
|
||||
buildExpressions([
|
||||
buildInvoke(['Timing', 'wait'], [100]),
|
||||
buildInvoke(['Timing', 'wait'], [200]),
|
||||
]),
|
||||
],
|
||||
), latus)(context);
|
||||
tickingPromise.tick(200);
|
||||
let start = Date.now();
|
||||
await Promise.race([
|
||||
new Promise((resolve) => setTimeout(resolve, DELAY)),
|
||||
tickingPromise,
|
||||
])
|
||||
expect(Date.now() - start).to.be.at.least(DELAY * 0.9);
|
||||
});
|
||||
});
|
|
@ -1,23 +0,0 @@
|
|||
import {Latus} from '@latus/core';
|
||||
import {expect} from 'chai';
|
||||
|
||||
import {
|
||||
buildValue,
|
||||
} from '../src/builders';
|
||||
import Context from '../src/context';
|
||||
import compile from '../src/compilers/compile';
|
||||
|
||||
let latus;
|
||||
let context;
|
||||
beforeEach(async () => {
|
||||
latus = Latus.mock({
|
||||
'@avocado/behavior': require('../src'),
|
||||
});
|
||||
await Promise.all(latus.invokeFlat('@latus/core/starting'));
|
||||
context = new Context({}, latus);
|
||||
});
|
||||
describe('Literal', () => {
|
||||
it('evaluates literals', async () => {
|
||||
expect(compile(buildValue(420), latus)(context)).to.equal(420);
|
||||
});
|
||||
});
|
|
@ -1,39 +0,0 @@
|
|||
import {Latus} from '@latus/core';
|
||||
import {expect} from 'chai';
|
||||
|
||||
import {
|
||||
buildInvoke,
|
||||
buildValue,
|
||||
} from '../src/builders';
|
||||
import Context from '../src/context';
|
||||
import compile from '../src/compilers/compile';
|
||||
|
||||
let latus;
|
||||
let context;
|
||||
beforeEach(async () => {
|
||||
latus = Latus.mock({
|
||||
'@avocado/behavior': require('../src'),
|
||||
});
|
||||
await Promise.all(latus.invokeFlat('@latus/core/starting'));
|
||||
context = new Context({}, latus);
|
||||
});
|
||||
describe('Timing', () => {
|
||||
it('waits', async () => {
|
||||
const DELAY = 30;
|
||||
const tickingPromise = compile(buildInvoke(
|
||||
['Timing', 'wait'],
|
||||
[
|
||||
buildValue(300),
|
||||
],
|
||||
), latus)(context);
|
||||
tickingPromise.tick(200);
|
||||
let start = Date.now();
|
||||
await Promise.race([
|
||||
new Promise((resolve) => setTimeout(resolve, DELAY)),
|
||||
tickingPromise,
|
||||
])
|
||||
expect(Date.now() - start).to.be.at.least(DELAY * 0.9);
|
||||
tickingPromise.tick(100);
|
||||
return tickingPromise;
|
||||
});
|
||||
});
|
|
@ -1,51 +0,0 @@
|
|||
import {Latus} from '@latus/core';
|
||||
import {expect} from 'chai';
|
||||
|
||||
import {
|
||||
buildInvoke,
|
||||
buildValue,
|
||||
} from '../src/builders';
|
||||
import Context from '../src/context';
|
||||
import compile from '../src/compilers/compile';
|
||||
|
||||
let latus;
|
||||
let context;
|
||||
beforeEach(async () => {
|
||||
latus = Latus.mock({
|
||||
'@avocado/behavior': require('../src'),
|
||||
});
|
||||
await Promise.all(latus.invokeFlat('@latus/core/starting'));
|
||||
context = new Context({}, latus);
|
||||
});
|
||||
describe('Utility', () => {
|
||||
it('makes arrays', async () => {
|
||||
expect(compile(buildInvoke(
|
||||
['Utility', 'makeArray'],
|
||||
[
|
||||
buildValue(1),
|
||||
buildValue(2),
|
||||
buildValue(3),
|
||||
],
|
||||
), latus)(context)).to.deep.equal([1, 2, 3]);
|
||||
});
|
||||
it('makes objects', async () => {
|
||||
expect(compile(buildInvoke(
|
||||
['Utility', 'makeObject'],
|
||||
[
|
||||
buildValue('foo'),
|
||||
buildValue(2),
|
||||
buildValue('bar'),
|
||||
buildValue(3),
|
||||
],
|
||||
), latus)(context)).to.deep.equal({foo: 2, bar: 3});
|
||||
});
|
||||
it('merges', async () => {
|
||||
expect(compile(buildInvoke(
|
||||
['Utility', 'merge'],
|
||||
[
|
||||
buildValue({foo: 69, bar: 420}),
|
||||
buildValue({foo: 311, baz: 100}),
|
||||
],
|
||||
), latus)(context)).to.deep.equal({foo: 311, bar: 420, baz: 100});
|
||||
});
|
||||
});
|
|
@ -163,6 +163,13 @@
|
|||
debug "4.3.1"
|
||||
msgpack-lite "^0.1.26"
|
||||
|
||||
"@avocado/sandbox@^1.0.0":
|
||||
version "1.0.0"
|
||||
resolved "http://npm.cha0sdev/@avocado%2fsandbox/-/sandbox-1.0.0.tgz#787158bbaddbbf787b79811c508783acae9dfece"
|
||||
integrity sha512-ZdNK9OBqomL05t10hGHEqJwrOwAj23KWV3npEBgPmfC/j1gePBqqizzz3T2Nzsn67j7ZKe8HG+nhS1y11ZScAA==
|
||||
dependencies:
|
||||
"@babel/types" "^7.13.14"
|
||||
|
||||
"@avocado/timing@2.0.0", "@avocado/timing@^2.0.0":
|
||||
version "2.0.0"
|
||||
resolved "http://npm.cha0sdev/@avocado%2ftiming/-/timing-2.0.0.tgz#b84a09f8b50b31d79dc27dd65a3003da810b7f86"
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import {decorateWithLatus} from '@latus/core';
|
||||
import {gatherComponents} from '@latus/react';
|
||||
|
||||
import EntityController from './controllers/entity';
|
||||
|
@ -12,9 +11,6 @@ export default {
|
|||
'@avocado/resource/persea.controllers': () => [
|
||||
EntityController,
|
||||
],
|
||||
'@avocado/resource/resources.decorate': decorateWithLatus(
|
||||
require.context('./resources/decorators', false, /\.js$/),
|
||||
),
|
||||
'@avocado/traits/components': gatherComponents(
|
||||
require.context('./traits', false, /\.jsx$/),
|
||||
),
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
import {Context} from '@avocado/behavior';
|
||||
|
||||
export default (Entity, latus) => class ContextedEntity extends Entity {
|
||||
|
||||
createContext(variables = {}) {
|
||||
const context = this.context
|
||||
? this.context.clone()
|
||||
: new Context(
|
||||
{
|
||||
entity: this,
|
||||
},
|
||||
latus,
|
||||
);
|
||||
const entries = Object.entries(variables);
|
||||
for (let i = 0; i < entries.length; i++) {
|
||||
const [key, value] = entries[i];
|
||||
context.add(key, value);
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
};
|
|
@ -1,47 +1,38 @@
|
|||
import './alive.scss';
|
||||
import {join} from 'path';
|
||||
|
||||
import {Number} from '@avocado/persea';
|
||||
import {Condition, Expressions} from '@avocado/behavior/persea';
|
||||
import {Code, Number} from '@avocado/persea';
|
||||
import {
|
||||
hot,
|
||||
PropTypes,
|
||||
React,
|
||||
useEffect,
|
||||
useState,
|
||||
} from '@latus/react';
|
||||
import {useJsonPatcher} from '@avocado/resource/persea';
|
||||
|
||||
const Alive = ({
|
||||
entity,
|
||||
json,
|
||||
path,
|
||||
}) => {
|
||||
const patch = useJsonPatcher();
|
||||
const [context, setContext] = useState(entity.createContext());
|
||||
useEffect(() => {
|
||||
setContext(entity.createContext());
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [entity]);
|
||||
return (
|
||||
<div className="alive">
|
||||
<div className="label">
|
||||
<div className="vertical">Death</div>
|
||||
<div>
|
||||
<div className="label">
|
||||
<div className="vertical">Condition</div>
|
||||
<Condition
|
||||
context={context}
|
||||
value={json.params.deathCondition}
|
||||
path={join(path, 'params/deathCondition')}
|
||||
<div className="vertical">Check</div>
|
||||
<Code
|
||||
code={json.params.deathCheck}
|
||||
name={join(path, 'params/deathCheck')}
|
||||
onChange={patch.onChange(join(path, 'params/deathCheck'))}
|
||||
/>
|
||||
</div>
|
||||
<div className="label">
|
||||
<div className="vertical">Actions</div>
|
||||
<Expressions
|
||||
context={context}
|
||||
value={json.params.deathActions}
|
||||
path={join(path, 'params/deathActions')}
|
||||
<div className="vertical">Script</div>
|
||||
<Code
|
||||
code={json.params.deathScript}
|
||||
name={join(path, 'params/deathScript')}
|
||||
onChange={patch.onChange(join(path, 'params/deathScript'))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -67,15 +58,11 @@ const Alive = ({
|
|||
Alive.displayName = 'Alive';
|
||||
|
||||
Alive.propTypes = {
|
||||
entity: PropTypes.shape({
|
||||
createContext: PropTypes.func,
|
||||
}).isRequired,
|
||||
entity: PropTypes.shape({}).isRequired,
|
||||
json: PropTypes.shape({
|
||||
params: PropTypes.shape({
|
||||
deathActions: PropTypes.shape({
|
||||
expressions: PropTypes.arrayOf(PropTypes.shape({})),
|
||||
}),
|
||||
deathCondition: PropTypes.shape({}),
|
||||
deathCheck: PropTypes.string,
|
||||
deathScript: PropTypes.string,
|
||||
}),
|
||||
state: PropTypes.shape({
|
||||
life: PropTypes.number,
|
||||
|
|
|
@ -139,30 +139,30 @@ export default (latus) => {
|
|||
return this.#quadTree;
|
||||
}
|
||||
|
||||
queryEntities(query, condition, context) {
|
||||
if (!context) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
context = new Context({}, latus);
|
||||
}
|
||||
const check = compile(condition, latus);
|
||||
const candidates = this.visibleEntities(query, true);
|
||||
const fails = [];
|
||||
for (let i = 0; i < candidates.length; ++i) {
|
||||
const entity = candidates[i];
|
||||
context.add('query', entity);
|
||||
if (!check(context)) {
|
||||
fails.push(entity);
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < fails.length; ++i) {
|
||||
candidates.splice(candidates.indexOf(fails[i]), 1);
|
||||
}
|
||||
return candidates;
|
||||
}
|
||||
// queryEntities(query, condition, context) {
|
||||
// if (!context) {
|
||||
// // eslint-disable-next-line no-param-reassign
|
||||
// context = new Context({}, latus);
|
||||
// }
|
||||
// const check = compile(condition, latus);
|
||||
// const candidates = this.visibleEntities(query, true);
|
||||
// const fails = [];
|
||||
// for (let i = 0; i < candidates.length; ++i) {
|
||||
// const entity = candidates[i];
|
||||
// context.add('query', entity);
|
||||
// if (!check(context)) {
|
||||
// fails.push(entity);
|
||||
// }
|
||||
// }
|
||||
// for (let i = 0; i < fails.length; ++i) {
|
||||
// candidates.splice(candidates.indexOf(fails[i]), 1);
|
||||
// }
|
||||
// return candidates;
|
||||
// }
|
||||
|
||||
queryPoint(query, condition, context) {
|
||||
return this.queryEntities(Rectangle.centerOn(query, [1, 1]), condition, context);
|
||||
}
|
||||
// queryPoint(query, condition, context) {
|
||||
// return this.queryEntities(Rectangle.centerOn(query, [1, 1]), condition, context);
|
||||
// }
|
||||
|
||||
removeEntity(entity) {
|
||||
const uuid = entity.instanceUuid;
|
||||
|
|
|
@ -188,6 +188,19 @@ export default (latus) => {
|
|||
this.#markedAsDirty = false;
|
||||
}
|
||||
|
||||
// TODO: behavior decorator
|
||||
get contextOrDefault() {
|
||||
return this.context || {entity: this};
|
||||
}
|
||||
|
||||
createContext(locals = {}) {
|
||||
const {Script} = latus.get('%resources');
|
||||
return Script.createContext({
|
||||
...this.contextOrDefault,
|
||||
...locals,
|
||||
});
|
||||
}
|
||||
|
||||
async destroy() {
|
||||
if (this.#isDestroying) {
|
||||
return;
|
||||
|
|
|
@ -1,12 +1,3 @@
|
|||
import {
|
||||
Actions,
|
||||
buildCondition,
|
||||
buildInvoke,
|
||||
buildExpression,
|
||||
buildValue,
|
||||
compile,
|
||||
Context,
|
||||
} from '@avocado/behavior';
|
||||
import {TickingPromise} from '@avocado/core';
|
||||
import {StateProperty, Trait} from '@avocado/traits';
|
||||
import {compose} from '@latus/core';
|
||||
|
@ -22,11 +13,7 @@ const decorate = compose(
|
|||
|
||||
export default (latus) => class Alive extends decorate(Trait) {
|
||||
|
||||
#context = new Context({}, latus);
|
||||
|
||||
#deathActions;
|
||||
|
||||
#deathCondition;
|
||||
#deathCheck;
|
||||
|
||||
#hasDied = false;
|
||||
|
||||
|
@ -34,13 +21,6 @@ export default (latus) => class Alive extends decorate(Trait) {
|
|||
|
||||
#packets = [];
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const {deathActions, deathCondition} = this.constructor.defaultParams();
|
||||
this.#deathActions = deathActions;
|
||||
this.#deathCondition = deathCondition;
|
||||
}
|
||||
|
||||
acceptPacket(packet) {
|
||||
switch (packet.constructor.type) {
|
||||
case 'Died':
|
||||
|
@ -60,30 +40,23 @@ export default (latus) => class Alive extends decorate(Trait) {
|
|||
}
|
||||
|
||||
static defaultParams() {
|
||||
const playDeathSound = buildInvoke(['entity', 'playSound'], [
|
||||
buildValue('deathSound'),
|
||||
]);
|
||||
const squeeze = buildInvoke(['entity', 'transition'], [
|
||||
{
|
||||
opacity: 0,
|
||||
visibleScaleX: 0.3,
|
||||
visibleScaleY: 3,
|
||||
},
|
||||
0.4,
|
||||
]);
|
||||
const isLifeGone = buildCondition('<=', [
|
||||
buildExpression(['entity', 'life']),
|
||||
0,
|
||||
]);
|
||||
return {
|
||||
deathActions: {
|
||||
type: 'expressions',
|
||||
expressions: [
|
||||
playDeathSound,
|
||||
squeeze,
|
||||
],
|
||||
},
|
||||
deathCondition: isLifeGone,
|
||||
deathCheck: 'return entity.life <= 0',
|
||||
deathScript: `
|
||||
if ('client' === SIDE) {
|
||||
TickingPromise.all([
|
||||
entity.playSound("deathSound"),
|
||||
entity.transition(
|
||||
{
|
||||
opacity: 0,
|
||||
visibleScaleX: 0.3,
|
||||
visibleScaleY: 3,
|
||||
},
|
||||
0.4,
|
||||
),
|
||||
]);
|
||||
}
|
||||
`,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -94,27 +67,22 @@ export default (latus) => class Alive extends decorate(Trait) {
|
|||
};
|
||||
}
|
||||
|
||||
static children() {
|
||||
return {
|
||||
die: {
|
||||
type: 'void',
|
||||
label: 'Force death',
|
||||
args: [],
|
||||
async die() {
|
||||
this.entity.emit('startedDying');
|
||||
const {Script} = latus.get('%resources');
|
||||
const deathScript = await Script.load(this.params.deathScript, this.entity.contextOrDefault);
|
||||
await this.entity.addTickingPromise(deathScript.tickingPromise());
|
||||
const died = this.entity.invokeHookFlat('died');
|
||||
await this.entity.addTickingPromise(new TickingPromise(
|
||||
() => {},
|
||||
(elapsed, resolve) => {
|
||||
if (died.every((die) => die(elapsed), (r) => !!r)) {
|
||||
resolve();
|
||||
}
|
||||
},
|
||||
life: {
|
||||
type: 'number',
|
||||
label: 'Life points',
|
||||
},
|
||||
maxLife: {
|
||||
type: 'number',
|
||||
label: 'Maximum life points',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
destroy() {
|
||||
super.destroy();
|
||||
this.#context.destroy();
|
||||
));
|
||||
this.#hasDied = true;
|
||||
this.entity.destroy();
|
||||
}
|
||||
|
||||
hooks() {
|
||||
|
@ -150,14 +118,8 @@ export default (latus) => class Alive extends decorate(Trait) {
|
|||
|
||||
async load(json) {
|
||||
await super.load(json);
|
||||
this.#context = new Context(
|
||||
{
|
||||
entity: this.entity,
|
||||
},
|
||||
latus,
|
||||
);
|
||||
this.#deathActions = new Actions(compile(this.params.deathActions, latus));
|
||||
this.#deathCondition = compile(this.params.deathCondition, latus);
|
||||
const {Script} = latus.get('%resources');
|
||||
this.#deathCheck = await Script.load(this.params.deathCheck, this.entity.contextOrDefault);
|
||||
}
|
||||
|
||||
get maxLife() {
|
||||
|
@ -172,24 +134,11 @@ export default (latus) => class Alive extends decorate(Trait) {
|
|||
methods() {
|
||||
return {
|
||||
|
||||
die: async () => {
|
||||
if (this.#isDying) {
|
||||
return;
|
||||
die: () => {
|
||||
if (!this.#isDying) {
|
||||
this.#isDying = this.die();
|
||||
}
|
||||
this.#isDying = true;
|
||||
this.entity.emit('startedDying');
|
||||
await this.entity.addTickingPromise(this.#deathActions.tickingPromise(this.#context));
|
||||
const died = this.entity.invokeHookFlat('died');
|
||||
await this.entity.addTickingPromise(new TickingPromise(
|
||||
() => {},
|
||||
(elapsed, resolve) => {
|
||||
if (died.every((die) => die(elapsed), (r) => !!r)) {
|
||||
resolve();
|
||||
}
|
||||
},
|
||||
));
|
||||
this.#hasDied = true;
|
||||
this.entity.destroy();
|
||||
return this.#isDying;
|
||||
},
|
||||
|
||||
};
|
||||
|
@ -212,8 +161,12 @@ export default (latus) => class Alive extends decorate(Trait) {
|
|||
|
||||
tick() {
|
||||
if ('client' !== process.env.SIDE) {
|
||||
if (!this.#isDying && this.#deathCondition(this.#context)) {
|
||||
this.entity.die();
|
||||
if (!this.#isDying) {
|
||||
this.#deathCheck.evaluate(({value: died}) => {
|
||||
if (died) {
|
||||
this.entity.die();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import {Context} from '@avocado/behavior';
|
||||
import {compose} from '@latus/core';
|
||||
import {StateProperty, Trait} from '@avocado/traits';
|
||||
import merge from 'deepmerge';
|
||||
|
@ -203,13 +202,7 @@ export default (latus) => class Spawner extends decorate(Trait) {
|
|||
return Promise.all(promises);
|
||||
},
|
||||
|
||||
spawn: async (key, json = {}, context) => {
|
||||
if (!context && json instanceof Context) {
|
||||
/* eslint-disable no-param-reassign */
|
||||
context = json;
|
||||
json = {};
|
||||
/* eslint-enable no-param-reassign */
|
||||
}
|
||||
spawn: async (key, json = {}) => {
|
||||
if (!this.maySpawn()) {
|
||||
return undefined;
|
||||
}
|
||||
|
@ -228,13 +221,7 @@ export default (latus) => class Spawner extends decorate(Trait) {
|
|||
return entity;
|
||||
},
|
||||
|
||||
spawnAt: async (key, position, json = {}, context) => {
|
||||
if (!context && json instanceof Context) {
|
||||
/* eslint-disable no-param-reassign */
|
||||
context = json;
|
||||
json = {};
|
||||
/* eslint-enable no-param-reassign */
|
||||
}
|
||||
spawnAt: async (key, position, json = {}) => {
|
||||
const entity = this.maySpawn()
|
||||
? await this.entity.spawn(
|
||||
key,
|
||||
|
@ -245,7 +232,6 @@ export default (latus) => class Spawner extends decorate(Trait) {
|
|||
arrayMerge: (l, r) => r,
|
||||
},
|
||||
),
|
||||
context,
|
||||
)
|
||||
: undefined;
|
||||
if (entity) {
|
||||
|
|
|
@ -229,7 +229,7 @@ export default () => class Visible extends decorate(Trait) {
|
|||
}
|
||||
|
||||
set visibleScaleX(x) {
|
||||
this.state.visibleScale = [x, this.state.visibleScale[1]];
|
||||
this.entity.visibleScale = [x, this.state.visibleScale[1]];
|
||||
}
|
||||
|
||||
get visibleScaleY() {
|
||||
|
@ -237,7 +237,7 @@ export default () => class Visible extends decorate(Trait) {
|
|||
}
|
||||
|
||||
set visibleScaleY(y) {
|
||||
this.state.visibleScale = [this.state.visibleScale[0], y];
|
||||
this.entity.visibleScale = [this.state.visibleScale[0], y];
|
||||
}
|
||||
|
||||
};
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
/* eslint-disable no-unused-vars */
|
||||
import {buildInvoke} from '@avocado/behavior';
|
||||
import {Rectangle, Vector} from '@avocado/math';
|
||||
import {LfoResult, Ticker} from '@avocado/timing';
|
||||
import {Trait} from '@avocado/traits';
|
||||
|
@ -65,10 +64,14 @@ export default () => class Initiator extends Trait {
|
|||
Rectangle.centerOn(incident, [16, 16]),
|
||||
Vector.scale(Vector.abs(directional), 2),
|
||||
);
|
||||
const interactives = this.entity.list.queryEntities(
|
||||
query,
|
||||
buildInvoke(['query', 'is'], ['Interactive']),
|
||||
);
|
||||
const {list} = this.entity;
|
||||
const entities = list.visibleEntities(query);
|
||||
const interactives = [];
|
||||
for (let i = 0; i < entities.length; ++i) {
|
||||
if (entities[i].is('Interactive')) {
|
||||
interactives.push(entities[i]);
|
||||
}
|
||||
}
|
||||
const oldTarget = this.#target;
|
||||
if (interactives.length > 0) {
|
||||
[this.#target] = interactives
|
||||
|
|
|
@ -1,9 +1,3 @@
|
|||
import {
|
||||
Actions,
|
||||
buildExpressions,
|
||||
compile,
|
||||
Context,
|
||||
} from '@avocado/behavior';
|
||||
import {StateProperty, Trait} from '@avocado/traits';
|
||||
import {compose} from '@latus/core';
|
||||
|
||||
|
@ -23,7 +17,7 @@ export default (latus) => class Interactive extends decorate(Trait) {
|
|||
|
||||
static defaultParams() {
|
||||
return {
|
||||
actions: buildExpressions([]),
|
||||
interactScript: 'void 0',
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -33,23 +27,16 @@ export default (latus) => class Interactive extends decorate(Trait) {
|
|||
};
|
||||
}
|
||||
|
||||
async load(json) {
|
||||
await super.load(json);
|
||||
this.#actions = new Actions(compile(this.params.actions, latus));
|
||||
}
|
||||
|
||||
methods() {
|
||||
return {
|
||||
|
||||
interact: (initiator) => {
|
||||
const context = new Context(
|
||||
{
|
||||
entity: this.entity,
|
||||
initiator,
|
||||
},
|
||||
latus,
|
||||
);
|
||||
this.entity.addTickingPromise(this.#actions.serial(context));
|
||||
interact: async (initiator) => {
|
||||
const {Script} = latus.get('%resources');
|
||||
const script = await Script.load(this.params.interactScript, {
|
||||
entity: this.entity,
|
||||
initiator,
|
||||
});
|
||||
this.entity.addTickingPromise(script.tickingPromise());
|
||||
},
|
||||
|
||||
};
|
||||
|
|
7
packages/persea/.latusrc.js
Normal file
7
packages/persea/.latusrc.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
module.exports = () => ({
|
||||
dependencies: {
|
||||
dom: [
|
||||
'ace-builds',
|
||||
],
|
||||
},
|
||||
});
|
|
@ -16,9 +16,9 @@ const Code = ({
|
|||
onChange,
|
||||
}) => (
|
||||
<AceEditor
|
||||
name={name}
|
||||
name={name.replace(/[^a-zA-Z0-9]/g, '-')}
|
||||
fontSize={fontSize}
|
||||
height={height}
|
||||
height={height || `${code.split('\n').length}em`}
|
||||
width="100%"
|
||||
mode="javascript"
|
||||
tabSize={2}
|
||||
|
@ -30,7 +30,7 @@ const Code = ({
|
|||
|
||||
Code.defaultProps = {
|
||||
fontSize: 12,
|
||||
height: '10em',
|
||||
height: null,
|
||||
onChange: () => {},
|
||||
name: 'ace-editor',
|
||||
};
|
||||
|
|
|
@ -1,35 +1,17 @@
|
|||
import {join} from 'path';
|
||||
|
||||
import {Expressions} from '@avocado/behavior/persea';
|
||||
import {fullEntity} from '@avocado/entity';
|
||||
import {Json} from '@avocado/persea';
|
||||
import {Code, Json} from '@avocado/persea';
|
||||
import {useJsonPatcher} from '@avocado/resource/persea';
|
||||
import {
|
||||
PropTypes,
|
||||
React,
|
||||
useEffect,
|
||||
useLatus,
|
||||
useState,
|
||||
} from '@latus/react';
|
||||
|
||||
const Collider = ({
|
||||
entity,
|
||||
json,
|
||||
path,
|
||||
}) => {
|
||||
const latus = useLatus();
|
||||
const patch = useJsonPatcher();
|
||||
const [context, setContext] = useState(() => entity.createContext());
|
||||
useEffect(() => {
|
||||
const createContext = async () => {
|
||||
setContext(entity.createContext({
|
||||
incident: [0, 0],
|
||||
other: await fullEntity(latus),
|
||||
}));
|
||||
};
|
||||
createContext();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [entity]);
|
||||
return (
|
||||
<div className="collider">
|
||||
<div className="label">
|
||||
|
@ -44,23 +26,19 @@ const Collider = ({
|
|||
<div>
|
||||
<div className="label">
|
||||
<div className="vertical">Start</div>
|
||||
{context && (
|
||||
<Expressions
|
||||
context={context}
|
||||
value={json.params.collisionStartActions}
|
||||
path={join(path, 'params/collisionStartActions')}
|
||||
/>
|
||||
)}
|
||||
<Code
|
||||
code={json.params.collisionStartScript}
|
||||
name={join(path, 'params/collisionStartScript')}
|
||||
onChange={patch.onChange(join(path, 'params/collisionStartScript'))}
|
||||
/>
|
||||
</div>
|
||||
<div className="label">
|
||||
<div className="vertical">End</div>
|
||||
{context && (
|
||||
<Expressions
|
||||
context={context}
|
||||
value={json.params.collisionEndActions}
|
||||
path={join(path, 'params/collisionEndActions')}
|
||||
/>
|
||||
)}
|
||||
<Code
|
||||
code={json.params.collisionEndScript}
|
||||
name={join(path, 'params/collisionEndScript')}
|
||||
onChange={patch.onChange(join(path, 'params/collisionEndScript'))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -117,17 +95,15 @@ Collider.defaultProps = {};
|
|||
Collider.displayName = 'Collider';
|
||||
|
||||
Collider.propTypes = {
|
||||
entity: PropTypes.shape({
|
||||
createContext: PropTypes.func,
|
||||
}).isRequired,
|
||||
entity: PropTypes.shape({}).isRequired,
|
||||
json: PropTypes.shape({
|
||||
params: PropTypes.shape({
|
||||
activeCollision: PropTypes.bool,
|
||||
collidesWithGroups: PropTypes.arrayOf(
|
||||
PropTypes.string,
|
||||
),
|
||||
collisionEndActions: PropTypes.shape({}),
|
||||
collisionStartActions: PropTypes.shape({}),
|
||||
collisionEndScript: PropTypes.string,
|
||||
collisionStartScript: PropTypes.string,
|
||||
collisionGroup: PropTypes.string,
|
||||
isSensor: PropTypes.bool,
|
||||
}),
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import {Actions, compile, Context} from '@avocado/behavior';
|
||||
import {Rectangle, Vector} from '@avocado/math';
|
||||
import {StateProperty, Trait} from '@avocado/traits';
|
||||
import {compose} from '@latus/core';
|
||||
|
@ -12,10 +11,6 @@ export default (latus) => class Collider extends decorate(Trait) {
|
|||
|
||||
#collidesWithGroups = [];
|
||||
|
||||
#collisionEndActions;
|
||||
|
||||
#collisionStartActions;
|
||||
|
||||
#collisionGroup = '';
|
||||
|
||||
#doesNotCollideWith = [];
|
||||
|
@ -27,8 +22,6 @@ export default (latus) => class Collider extends decorate(Trait) {
|
|||
constructor() {
|
||||
super();
|
||||
({
|
||||
collisionEndActions: this.#collisionEndActions,
|
||||
collisionStartActions: this.#collisionStartActions,
|
||||
collidesWithGroups: this.#collidesWithGroups,
|
||||
collisionGroup: this.#collisionGroup,
|
||||
isSensor: this.#isSensor,
|
||||
|
@ -41,14 +34,8 @@ export default (latus) => class Collider extends decorate(Trait) {
|
|||
collidesWithGroups: [
|
||||
'default',
|
||||
],
|
||||
collisionEndActions: {
|
||||
type: 'expressions',
|
||||
expressions: [],
|
||||
},
|
||||
collisionStartActions: {
|
||||
type: 'expressions',
|
||||
expressions: [],
|
||||
},
|
||||
collisionStartScript: 'void 0',
|
||||
collisionEndScript: 'void 0',
|
||||
collisionGroup: 'default',
|
||||
isSensor: false,
|
||||
};
|
||||
|
@ -68,63 +55,6 @@ export default (latus) => class Collider extends decorate(Trait) {
|
|||
];
|
||||
}
|
||||
|
||||
static children() {
|
||||
return {
|
||||
collidesWith: {
|
||||
advanced: true,
|
||||
type: 'bool',
|
||||
label: 'I collide with $1.',
|
||||
args: [
|
||||
{
|
||||
type: 'entity',
|
||||
label: 'other',
|
||||
},
|
||||
],
|
||||
},
|
||||
doesNotCollideWith: {
|
||||
advanced: true,
|
||||
type: 'bool',
|
||||
label: 'I would not collide with $1.',
|
||||
args: [
|
||||
{
|
||||
type: 'entity',
|
||||
label: 'other',
|
||||
},
|
||||
],
|
||||
},
|
||||
isCheckingCollisions: {
|
||||
type: 'bool',
|
||||
label: 'Is checking collisions',
|
||||
},
|
||||
isColliding: {
|
||||
type: 'bool',
|
||||
label: 'Is able to collide',
|
||||
},
|
||||
setDoesCollideWith: {
|
||||
advanced: true,
|
||||
type: 'void',
|
||||
label: 'Set $1 as colliding with myself.',
|
||||
args: [
|
||||
{
|
||||
type: 'entity',
|
||||
label: 'other',
|
||||
},
|
||||
],
|
||||
},
|
||||
setDoesNotCollideWith: {
|
||||
advanced: true,
|
||||
type: 'void',
|
||||
label: 'Set $1 as not colliding with myself.',
|
||||
args: [
|
||||
{
|
||||
type: 'entity',
|
||||
label: 'other',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
destroy() {
|
||||
super.destroy();
|
||||
this.releaseAllCollisions();
|
||||
|
@ -206,32 +136,22 @@ export default (latus) => class Collider extends decorate(Trait) {
|
|||
await super.load(json);
|
||||
const {
|
||||
collidesWithGroups,
|
||||
collisionEndActions,
|
||||
collisionGroup,
|
||||
collisionStartActions,
|
||||
isSensor,
|
||||
} = this.params;
|
||||
this.#collidesWithGroups = collidesWithGroups;
|
||||
this.#collisionEndActions = collisionEndActions.expressions.length > 0
|
||||
? new Actions(compile(collisionEndActions, latus))
|
||||
: undefined;
|
||||
this.#collisionStartActions = collisionStartActions.expressions.length > 0
|
||||
? new Actions(compile(collisionStartActions, latus))
|
||||
: undefined;
|
||||
this.#collisionGroup = collisionGroup;
|
||||
this.#isSensor = isSensor;
|
||||
}
|
||||
|
||||
pushCollisionTickingPromise(actions, other, incident) {
|
||||
const context = new Context(
|
||||
{
|
||||
entity: this.entity,
|
||||
incident,
|
||||
other,
|
||||
},
|
||||
latus,
|
||||
);
|
||||
this.entity.addTickingPromise(actions.tickingPromise(context));
|
||||
async pushCollisionTickingPromise(codeOrUri, other, incident) {
|
||||
const {Script} = latus.get('%resources');
|
||||
const script = await Script.load(codeOrUri, {
|
||||
entity: this.entity,
|
||||
incident,
|
||||
other,
|
||||
});
|
||||
this.entity.addTickingPromise(script.tickingPromise());
|
||||
}
|
||||
|
||||
releaseAllCollisions() {
|
||||
|
@ -258,8 +178,8 @@ export default (latus) => class Collider extends decorate(Trait) {
|
|||
const index = this.indexOfCollidingEntity(other);
|
||||
if (-1 !== index) {
|
||||
this.#isCollidingWith.splice(index, 1);
|
||||
if (this.#collisionEndActions) {
|
||||
this.pushCollisionTickingPromise(this.#collisionEndActions, other, incident);
|
||||
if (this.params.collisionEndScript) {
|
||||
this.pushCollisionTickingPromise(this.params.collisionEndScript, other, incident);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -271,8 +191,8 @@ export default (latus) => class Collider extends decorate(Trait) {
|
|||
entity: other,
|
||||
incident,
|
||||
});
|
||||
if (this.#collisionStartActions) {
|
||||
this.pushCollisionTickingPromise(this.#collisionStartActions, other, incident);
|
||||
if (this.params.collisionStartScript) {
|
||||
this.pushCollisionTickingPromise(this.params.collisionStartScript, other, incident);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -10,7 +10,6 @@ import useJsonPatcher from '../../hooks/use-json-patcher';
|
|||
import {controller as controllerPropTypes} from '../../prop-types';
|
||||
|
||||
const Json = ({path, resource}) => {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const patch = useJsonPatcher();
|
||||
return (
|
||||
<JsonComponent
|
||||
|
|
|
@ -6,8 +6,10 @@ export default class Sandbox {
|
|||
|
||||
constructor(ast, context) {
|
||||
this.ast = ast;
|
||||
this.reset();
|
||||
this.parents = new WeakMap();
|
||||
this.scopes = new WeakMap();
|
||||
this.context = context;
|
||||
this.reset();
|
||||
}
|
||||
|
||||
get context() {
|
||||
|
@ -53,13 +55,17 @@ export default class Sandbox {
|
|||
const scope = this.nodeScope(node);
|
||||
const {Vasync, value} = this.evaluate(right);
|
||||
if (!types.isMemberExpression(left)) {
|
||||
const assign = (value) => {
|
||||
scope.set(left.name, value);
|
||||
return value;
|
||||
};
|
||||
if (Vasync) {
|
||||
return {
|
||||
async: true,
|
||||
value: value.then((value) => scope.set(left.name, value)),
|
||||
value: value.then(assign),
|
||||
};
|
||||
}
|
||||
return {value: scope.set(left.name, value)};
|
||||
return {value: assign(value)};
|
||||
}
|
||||
const {
|
||||
computed,
|
||||
|
@ -67,7 +73,7 @@ export default class Sandbox {
|
|||
optional,
|
||||
property,
|
||||
} = left;
|
||||
const assign = (O, P, value) => {
|
||||
const memberAssign = (O, P, value) => {
|
||||
if (optional && !O) {
|
||||
return undefined;
|
||||
}
|
||||
|
@ -75,7 +81,7 @@ export default class Sandbox {
|
|||
return O[P] = value;
|
||||
};
|
||||
const makeAsync = (O, P, value) => (
|
||||
Promise.all([O, P, value]).then(([O, P, value]) => assign(O, P, value))
|
||||
Promise.all([O, P, value]).then(([O, P, value]) => memberAssign(O, P, value))
|
||||
);
|
||||
const O = this.evaluate(object);
|
||||
const P = computed ? this.evaluate(property) : {value: property.name};
|
||||
|
@ -86,7 +92,7 @@ export default class Sandbox {
|
|||
value: makeAsync(O.value, P.value, value),
|
||||
};
|
||||
}
|
||||
return {value: assign(O.value, P.value, value)};
|
||||
return {value: memberAssign(O.value, P.value, value)};
|
||||
}
|
||||
|
||||
evaluateAwaitExpression({argument}) {
|
||||
|
@ -132,15 +138,17 @@ export default class Sandbox {
|
|||
return undefined;
|
||||
}
|
||||
};
|
||||
const {async: lasync, value: left} = this.evaluate(node.left);
|
||||
const {async: rasync, value: right} = this.evaluate(node.right);
|
||||
if (lasync || rasync) {
|
||||
const left = this.evaluate(node.left);
|
||||
const right = this.evaluate(node.right);
|
||||
if (left.async || right.async) {
|
||||
return {
|
||||
async: true,
|
||||
value: Promise.all([left, right]).then(([left, right]) => binary(left, right)),
|
||||
value: Promise
|
||||
.all([left.value, right.value])
|
||||
.then(([left, right]) => binary(left, right)),
|
||||
};
|
||||
}
|
||||
return {value: binary(left, right)};
|
||||
return {value: binary(left.value, right.value)};
|
||||
}
|
||||
|
||||
evaluateCallExpression(node) {
|
||||
|
@ -148,6 +156,7 @@ export default class Sandbox {
|
|||
const args = [];
|
||||
for (let i = 0; i < node.arguments.length; i++) {
|
||||
const arg = node.arguments[i];
|
||||
this.setNextScope(arg, this.nodeScope(node));
|
||||
const {async, value} = this.evaluate(arg);
|
||||
// eslint-disable-next-line no-bitwise
|
||||
asyncArgs |= async;
|
||||
|
@ -190,6 +199,7 @@ export default class Sandbox {
|
|||
optional: memberOptional,
|
||||
property,
|
||||
} = callee;
|
||||
this.setNextScope(callee);
|
||||
const O = this.evaluate(object);
|
||||
const P = computed ? this.evaluate(property) : {value: property.name};
|
||||
if (asyncArgs || O.async || P.async) {
|
||||
|
@ -209,7 +219,8 @@ export default class Sandbox {
|
|||
}
|
||||
|
||||
evaluateIdentifier(node) {
|
||||
return {value: this.nodeScope(node).get(node.name)};
|
||||
const scope = this.nodeScope(node);
|
||||
return {value: scope.get(node.name)};
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
|
@ -300,6 +311,37 @@ export default class Sandbox {
|
|||
};
|
||||
}
|
||||
|
||||
evaluateUnaryExpression(node) {
|
||||
const unary = (arg) => {
|
||||
switch (node.operator) {
|
||||
/* eslint-disable no-multi-spaces, switch-colon-spacing */
|
||||
case '+' : return +arg;
|
||||
case '-' : return -arg;
|
||||
case '!' : return !arg;
|
||||
// eslint-disable-next-line no-bitwise
|
||||
case '~' : return ~arg;
|
||||
case 'typeof': return typeof arg;
|
||||
// eslint-disable-next-line no-void
|
||||
case 'void' : return undefined;
|
||||
// case 'delete': ...
|
||||
case 'throw' : throw arg;
|
||||
/* no-multi-spaces, switch-colon-spacing */
|
||||
default:
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("evaluateUnaryExpression(): Can't handle operator", node.operator);
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
const arg = this.evaluate(node.argument);
|
||||
if (arg.async) {
|
||||
return {
|
||||
async: true,
|
||||
value: Promise.resolve(arg.value).then(unary),
|
||||
};
|
||||
}
|
||||
return {value: unary(arg.value)};
|
||||
}
|
||||
|
||||
evaluateUpdateExpression(node) {
|
||||
const {argument, operator, prefix} = node;
|
||||
const {async, value} = this.evaluate(argument);
|
||||
|
@ -375,8 +417,10 @@ export default class Sandbox {
|
|||
|
||||
reset() {
|
||||
const self = this;
|
||||
const {context} = this;
|
||||
this.parents = new WeakMap();
|
||||
this.scopes = new WeakMap();
|
||||
this.context = context;
|
||||
this.runner = (function* traverse() {
|
||||
return yield* self.traverse(self.ast);
|
||||
}());
|
||||
|
@ -414,6 +458,7 @@ export default class Sandbox {
|
|||
'Literal',
|
||||
'MemberExpression',
|
||||
'ObjectExpression',
|
||||
'UnaryExpression',
|
||||
'UpdateExpression',
|
||||
];
|
||||
for (let i = 0; i < flat.length; i++) {
|
||||
|
@ -615,7 +660,9 @@ export default class Sandbox {
|
|||
// Evaluate...
|
||||
if (types.isReturnStatement(node)) {
|
||||
// eslint-disable-next-line consistent-return
|
||||
return this.evaluate(node.argument);
|
||||
return !node.argument
|
||||
? {value: undefined}
|
||||
: this.evaluate(node.argument);
|
||||
}
|
||||
if (types.isDirective(node)) {
|
||||
yield this.evaluate(node.value);
|
||||
|
|
|
@ -55,11 +55,17 @@ export default class Scope {
|
|||
|
||||
set(key, value) {
|
||||
let walk = this;
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (walk) {
|
||||
if (key in walk.context) {
|
||||
walk.context[key] = value;
|
||||
return value;
|
||||
}
|
||||
// TODO: option to disallow global set
|
||||
if (!walk.parent) {
|
||||
walk.context[key] = value;
|
||||
return value;
|
||||
}
|
||||
walk = walk.parent;
|
||||
}
|
||||
return undefined;
|
||||
|
|
Loading…
Reference in New Issue
Block a user