refactor: script

This commit is contained in:
cha0s 2021-04-19 05:50:43 -05:00
parent 6fdf4e2327
commit 2f7b5d458f
51 changed files with 511 additions and 1755 deletions

View File

@ -1,3 +1,4 @@
- visibleEntities is way overloaded
- entities should probably belong to room instead of layer - entities should probably belong to room instead of layer
- don't tick entities without any ticking traits - don't tick entities without any ticking traits
- ~~production build~~ - ~~production build~~

View File

@ -24,7 +24,9 @@
"@avocado/math": "^2.0.0", "@avocado/math": "^2.0.0",
"@avocado/persea": "^1.0.0", "@avocado/persea": "^1.0.0",
"@avocado/resource": "^2.0.0", "@avocado/resource": "^2.0.0",
"@avocado/sandbox": "^1.0.0",
"@avocado/traits": "^2.0.0", "@avocado/traits": "^2.0.0",
"@babel/parser": "^7.13.13",
"@latus/core": "2.0.0", "@latus/core": "2.0.0",
"autoprefixer": "^9.8.6", "autoprefixer": "^9.8.6",
"classnames": "^2.2.6", "classnames": "^2.2.6",
@ -33,6 +35,7 @@
"lodash.get": "^4.4.2", "lodash.get": "^4.4.2",
"lodash.mapvalues": "^4.6.0", "lodash.mapvalues": "^4.6.0",
"lodash.set": "^4.3.2", "lodash.set": "^4.3.2",
"lru-cache": "^6.0.0",
"natsort": "^2.0.2", "natsort": "^2.0.2",
"react-tabs": "^3.1.2" "react-tabs": "^3.1.2"
}, },

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +0,0 @@
import compile from './compile';
export default (latus) => ({expressions}) => () => (
expressions.map((expression) => compile(expression, latus))
);

View File

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

View File

@ -1 +0,0 @@
export default () => ({value}) => () => value;

View File

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

View File

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

View File

@ -1,10 +1,12 @@
import Flow from './flow'; import {TickingPromise} from '@avocado/core';
import Timing from './timing'; import Timing from './timing';
import Utility from './utility'; import Utility from './utility';
export default (latus) => ({ export default (latus) => ({
latus, latus,
Flow, SIDE: process.env.SIDE,
TickingPromise,
Timing, Timing,
Utility, Utility,
}); });

View File

@ -76,30 +76,10 @@ export default {
get: (object, path, defaultValue) => get(object, path, defaultValue), get: (object, path, defaultValue) => get(object, path, defaultValue),
log: (...args) => { log: (...args) => {
// No context!
args.pop();
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log(...args); 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), merge: (l, r) => merge(l, r),
set: (object, path, value) => set(object, path, value), set: (object, path, value) => set(object, path, value),

View File

@ -1,20 +1,13 @@
import {gatherWithLatus} from '@latus/core'; import {gatherWithLatus} from '@latus/core';
import compilers from './compilers';
import globals from './globals'; 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 { export default {
hooks: { hooks: {
'@avocado/behavior/compilers': compilers,
'@avocado/behavior/globals': globals, '@avocado/behavior/globals': globals,
'@avocado/behavior/types': types, '@avocado/resource/resources': gatherWithLatus(
require.context('./resources', false, /\.js$/),
),
'@avocado/traits/traits': gatherWithLatus( '@avocado/traits/traits': gatherWithLatus(
require.context('./traits', false, /\.js$/), require.context('./traits', false, /\.js$/),
), ),

View File

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

View File

@ -0,0 +1,3 @@
.text-renderer {
font-family: monospace;
}

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

View File

@ -1,27 +1,16 @@
import {gatherComponents} from '@latus/react'; import {gatherComponents} from '@latus/react';
import Condition from './components/condition'; import ScriptController from './controllers/script';
import Expression from './components/expression';
import Expressions from './components/expressions';
import Literal from './components/literal';
export { export {
Condition, ScriptController,
Expression,
Expressions,
Literal,
}; };
export default { export default {
hooks: { hooks: {
'@latus/core/starting': async (latus) => { '@avocado/resource/persea.controllers': () => [
latus.set('%behavior-components', { ScriptController,
condition: Condition, ],
expression: Expression,
expressions: Expressions,
literal: Literal,
});
},
'@avocado/traits/components': gatherComponents( '@avocado/traits/components': gatherComponents(
require.context('./traits', false, /\.jsx$/), require.context('./traits', false, /\.jsx$/),
), ),

View File

@ -8,7 +8,7 @@ import {
PropTypes, PropTypes,
React, React,
} from '@latus/react'; } from '@latus/react';
import {Number} from '@avocado/persea'; import {Code} from '@avocado/persea';
import { import {
Tab, Tab,
Tabs, Tabs,
@ -16,11 +16,7 @@ import {
TabPanel, TabPanel,
} from 'react-tabs'; } from 'react-tabs';
import Expression from '../components/expression';
import Expressions from '../components/expressions';
const Behaved = ({ const Behaved = ({
entity,
json, json,
path, path,
}) => { }) => {
@ -29,113 +25,36 @@ const Behaved = ({
<div className="behaved"> <div className="behaved">
<Tabs> <Tabs>
<TabList> <TabList>
<Tab>Daemons</Tab>
<Tab>Routines</Tab> <Tab>Routines</Tab>
<Tab>Collectives</Tab> <Tab>Daemons</Tab>
</TabList> </TabList>
<div className="behaved__tab-panels"> <div className="behaved__tab-panels">
<TabPanel> <TabPanel>
<JsonTabs <JsonTabs
createPanel={(expressions, key) => ( createPanel={(code, key) => (
<Expressions <Code
context={entity.context} code={code}
value={expressions} name={join(path, 'params/routines', key)}
path={join(path, 'params/daemons', key)} onChange={patch.onChange(join(path, 'params/routines', key))}
/> />
)} )}
defaultValue={{ 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: [],
}}
path={join(path, 'params/routines')} path={join(path, 'params/routines')}
map={json.params.routines} map={json.params.routines}
/> />
</TabPanel> </TabPanel>
<TabPanel> <TabPanel>
<JsonTabs <JsonTabs
createPanel={( createPanel={(code, key) => (
{ <Code
find, code={code}
max, name={join(path, 'params/daemons', key)}
reset, onChange={patch.onChange(join(path, 'params/daemons', key))}
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>
</>
)} )}
defaultValue={{ defaultValue=""
find: { path={join(path, 'params/daemons')}
type: 'expression', map={json.params.daemons}
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}
/> />
</TabPanel> </TabPanel>
</div> </div>
@ -147,12 +66,9 @@ const Behaved = ({
Behaved.displayName = 'Behaved'; Behaved.displayName = 'Behaved';
Behaved.propTypes = { Behaved.propTypes = {
entity: PropTypes.shape({ entity: PropTypes.shape({}).isRequired,
context: PropTypes.shape({}),
}).isRequired,
json: PropTypes.shape({ json: PropTypes.shape({
params: PropTypes.shape({ params: PropTypes.shape({
collectives: PropTypes.shape({}),
daemons: PropTypes.shape({}), daemons: PropTypes.shape({}),
routines: PropTypes.shape({}), routines: PropTypes.shape({}),
}), }),

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

View File

@ -0,0 +1,3 @@
import {Sandbox} from '@avocado/sandbox';

View File

@ -1,3 +0,0 @@
export const isInvocation = (op) => 'invoke' === op.type;
export const isKey = (op) => 'key' === op.type;

View File

@ -1,10 +1,6 @@
import {mapValuesAsync} from '@avocado/core';
import {StateProperty, Trait} from '@avocado/traits'; import {StateProperty, Trait} from '@avocado/traits';
import {compose} from '@latus/core'; 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( const decorate = compose(
StateProperty('activeCollective'), StateProperty('activeCollective'),
@ -22,7 +18,7 @@ export default (latus) => class Behaved extends decorate(Trait) {
#collectives = []; #collectives = [];
#context = new Context({}, latus); #context = {};
#currentRoutine = ''; #currentRoutine = '';
@ -35,6 +31,8 @@ export default (latus) => class Behaved extends decorate(Trait) {
({ ({
currentRoutine: this.#currentRoutine, currentRoutine: this.#currentRoutine,
} = this.constructor.defaultState()); } = this.constructor.defaultState());
const {Script} = latus.get('%resources');
this.#context = Script.createContext();
} }
get context() { get context() {
@ -43,6 +41,7 @@ export default (latus) => class Behaved extends decorate(Trait) {
static defaultParams() { static defaultParams() {
return { return {
codeRoutines: {},
collectives: {}, collectives: {},
daemons: {}, daemons: {},
routines: {}, routines: {},
@ -114,7 +113,7 @@ export default (latus) => class Behaved extends decorate(Trait) {
destroy() { destroy() {
super.destroy(); super.destroy();
this.#context.destroy(); this.#context = {};
this.#currentRoutine = undefined; this.#currentRoutine = undefined;
this.#routines = 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) { async load(json) {
await super.load(json); await super.load(json);
this.#context = new Context( const {Script} = latus.get('%resources');
{ this.#context = Script.createContext({
entity: this.entity, entity: this.entity,
}, });
latus, const daemons = {
); ...this.entity.invokeHookReduced('daemons'),
this.#daemons = Object.values(this.params.daemons) ...this.params.daemons,
.map((daemon) => new Actions(compile(daemon, latus))); };
this.#routines = mapValues( this.#daemons = Object.values(await this.constructor.loadScripts(daemons, this.#context));
this.params.routines, this.#routines = await this.constructor.loadScripts(this.params.routines, this.#context);
(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,
},
]);
this.updateCurrentRoutine(this.state.currentRoutine); this.updateCurrentRoutine(this.state.currentRoutine);
super.isBehaving = 'client' !== process.env.SIDE; super.isBehaving = 'client' !== process.env.SIDE;
} }
@ -172,11 +160,6 @@ export default (latus) => class Behaved extends decorate(Trait) {
methods() { methods() {
return { return {
jumpToRoutine: (routine, index) => {
this.updateCurrentRoutine(routine);
this.#currentRoutine.index = 'number' === typeof index ? index : 0;
},
leaveCollection: () => { leaveCollection: () => {
this.entity.activeCollective = null; this.entity.activeCollective = null;
}, },
@ -189,53 +172,19 @@ export default (latus) => class Behaved extends decorate(Trait) {
this.#accumulator += elapsed; this.#accumulator += elapsed;
if (this.#accumulator >= STATIC_INTERVAL) { if (this.#accumulator >= STATIC_INTERVAL) {
for (let i = 0; i < this.#daemons.length; ++i) { for (let i = 0; i < this.#daemons.length; ++i) {
this.#daemons[i].tick(this.#context, elapsed); this.#daemons[i].tick(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.#accumulator -= STATIC_INTERVAL; this.#accumulator -= STATIC_INTERVAL;
} }
if (this.#currentRoutine) { if (this.#currentRoutine) {
this.#currentRoutine.tick(this.#context, elapsed); this.#currentRoutine.tick(elapsed);
} }
} }
} }
updateCurrentRoutine(currentRoutine) { updateCurrentRoutine(currentRoutine) {
this.#currentRoutine = this.#routines[currentRoutine]; this.#currentRoutine = this.#routines[currentRoutine];
this.#currentRoutine.reset();
super.currentRoutine = currentRoutine; super.currentRoutine = currentRoutine;
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -163,6 +163,13 @@
debug "4.3.1" debug "4.3.1"
msgpack-lite "^0.1.26" 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": "@avocado/timing@2.0.0", "@avocado/timing@^2.0.0":
version "2.0.0" version "2.0.0"
resolved "http://npm.cha0sdev/@avocado%2ftiming/-/timing-2.0.0.tgz#b84a09f8b50b31d79dc27dd65a3003da810b7f86" resolved "http://npm.cha0sdev/@avocado%2ftiming/-/timing-2.0.0.tgz#b84a09f8b50b31d79dc27dd65a3003da810b7f86"

View File

@ -1,4 +1,3 @@
import {decorateWithLatus} from '@latus/core';
import {gatherComponents} from '@latus/react'; import {gatherComponents} from '@latus/react';
import EntityController from './controllers/entity'; import EntityController from './controllers/entity';
@ -12,9 +11,6 @@ export default {
'@avocado/resource/persea.controllers': () => [ '@avocado/resource/persea.controllers': () => [
EntityController, EntityController,
], ],
'@avocado/resource/resources.decorate': decorateWithLatus(
require.context('./resources/decorators', false, /\.js$/),
),
'@avocado/traits/components': gatherComponents( '@avocado/traits/components': gatherComponents(
require.context('./traits', false, /\.jsx$/), require.context('./traits', false, /\.jsx$/),
), ),

View File

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

View File

@ -1,47 +1,38 @@
import './alive.scss'; import './alive.scss';
import {join} from 'path'; import {join} from 'path';
import {Number} from '@avocado/persea'; import {Code, Number} from '@avocado/persea';
import {Condition, Expressions} from '@avocado/behavior/persea';
import { import {
hot, hot,
PropTypes, PropTypes,
React, React,
useEffect,
useState,
} from '@latus/react'; } from '@latus/react';
import {useJsonPatcher} from '@avocado/resource/persea'; import {useJsonPatcher} from '@avocado/resource/persea';
const Alive = ({ const Alive = ({
entity,
json, json,
path, path,
}) => { }) => {
const patch = useJsonPatcher(); const patch = useJsonPatcher();
const [context, setContext] = useState(entity.createContext());
useEffect(() => {
setContext(entity.createContext());
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [entity]);
return ( return (
<div className="alive"> <div className="alive">
<div className="label"> <div className="label">
<div className="vertical">Death</div> <div className="vertical">Death</div>
<div> <div>
<div className="label"> <div className="label">
<div className="vertical">Condition</div> <div className="vertical">Check</div>
<Condition <Code
context={context} code={json.params.deathCheck}
value={json.params.deathCondition} name={join(path, 'params/deathCheck')}
path={join(path, 'params/deathCondition')} onChange={patch.onChange(join(path, 'params/deathCheck'))}
/> />
</div> </div>
<div className="label"> <div className="label">
<div className="vertical">Actions</div> <div className="vertical">Script</div>
<Expressions <Code
context={context} code={json.params.deathScript}
value={json.params.deathActions} name={join(path, 'params/deathScript')}
path={join(path, 'params/deathActions')} onChange={patch.onChange(join(path, 'params/deathScript'))}
/> />
</div> </div>
</div> </div>
@ -67,15 +58,11 @@ const Alive = ({
Alive.displayName = 'Alive'; Alive.displayName = 'Alive';
Alive.propTypes = { Alive.propTypes = {
entity: PropTypes.shape({ entity: PropTypes.shape({}).isRequired,
createContext: PropTypes.func,
}).isRequired,
json: PropTypes.shape({ json: PropTypes.shape({
params: PropTypes.shape({ params: PropTypes.shape({
deathActions: PropTypes.shape({ deathCheck: PropTypes.string,
expressions: PropTypes.arrayOf(PropTypes.shape({})), deathScript: PropTypes.string,
}),
deathCondition: PropTypes.shape({}),
}), }),
state: PropTypes.shape({ state: PropTypes.shape({
life: PropTypes.number, life: PropTypes.number,

View File

@ -139,30 +139,30 @@ export default (latus) => {
return this.#quadTree; return this.#quadTree;
} }
queryEntities(query, condition, context) { // queryEntities(query, condition, context) {
if (!context) { // if (!context) {
// eslint-disable-next-line no-param-reassign // // eslint-disable-next-line no-param-reassign
context = new Context({}, latus); // context = new Context({}, latus);
} // }
const check = compile(condition, latus); // const check = compile(condition, latus);
const candidates = this.visibleEntities(query, true); // const candidates = this.visibleEntities(query, true);
const fails = []; // const fails = [];
for (let i = 0; i < candidates.length; ++i) { // for (let i = 0; i < candidates.length; ++i) {
const entity = candidates[i]; // const entity = candidates[i];
context.add('query', entity); // context.add('query', entity);
if (!check(context)) { // if (!check(context)) {
fails.push(entity); // fails.push(entity);
} // }
} // }
for (let i = 0; i < fails.length; ++i) { // for (let i = 0; i < fails.length; ++i) {
candidates.splice(candidates.indexOf(fails[i]), 1); // candidates.splice(candidates.indexOf(fails[i]), 1);
} // }
return candidates; // return candidates;
} // }
queryPoint(query, condition, context) { // queryPoint(query, condition, context) {
return this.queryEntities(Rectangle.centerOn(query, [1, 1]), condition, context); // return this.queryEntities(Rectangle.centerOn(query, [1, 1]), condition, context);
} // }
removeEntity(entity) { removeEntity(entity) {
const uuid = entity.instanceUuid; const uuid = entity.instanceUuid;

View File

@ -188,6 +188,19 @@ export default (latus) => {
this.#markedAsDirty = false; 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() { async destroy() {
if (this.#isDestroying) { if (this.#isDestroying) {
return; return;

View File

@ -1,12 +1,3 @@
import {
Actions,
buildCondition,
buildInvoke,
buildExpression,
buildValue,
compile,
Context,
} from '@avocado/behavior';
import {TickingPromise} from '@avocado/core'; import {TickingPromise} from '@avocado/core';
import {StateProperty, Trait} from '@avocado/traits'; import {StateProperty, Trait} from '@avocado/traits';
import {compose} from '@latus/core'; import {compose} from '@latus/core';
@ -22,11 +13,7 @@ const decorate = compose(
export default (latus) => class Alive extends decorate(Trait) { export default (latus) => class Alive extends decorate(Trait) {
#context = new Context({}, latus); #deathCheck;
#deathActions;
#deathCondition;
#hasDied = false; #hasDied = false;
@ -34,13 +21,6 @@ export default (latus) => class Alive extends decorate(Trait) {
#packets = []; #packets = [];
constructor() {
super();
const {deathActions, deathCondition} = this.constructor.defaultParams();
this.#deathActions = deathActions;
this.#deathCondition = deathCondition;
}
acceptPacket(packet) { acceptPacket(packet) {
switch (packet.constructor.type) { switch (packet.constructor.type) {
case 'Died': case 'Died':
@ -60,30 +40,23 @@ export default (latus) => class Alive extends decorate(Trait) {
} }
static defaultParams() { 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 { return {
deathActions: { deathCheck: 'return entity.life <= 0',
type: 'expressions', deathScript: `
expressions: [ if ('client' === SIDE) {
playDeathSound, TickingPromise.all([
squeeze, entity.playSound("deathSound"),
], entity.transition(
}, {
deathCondition: isLifeGone, opacity: 0,
visibleScaleX: 0.3,
visibleScaleY: 3,
},
0.4,
),
]);
}
`,
}; };
} }
@ -94,27 +67,22 @@ export default (latus) => class Alive extends decorate(Trait) {
}; };
} }
static children() { async die() {
return { this.entity.emit('startedDying');
die: { const {Script} = latus.get('%resources');
type: 'void', const deathScript = await Script.load(this.params.deathScript, this.entity.contextOrDefault);
label: 'Force death', await this.entity.addTickingPromise(deathScript.tickingPromise());
args: [], 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', this.#hasDied = true;
label: 'Life points', this.entity.destroy();
},
maxLife: {
type: 'number',
label: 'Maximum life points',
},
};
}
destroy() {
super.destroy();
this.#context.destroy();
} }
hooks() { hooks() {
@ -150,14 +118,8 @@ export default (latus) => class Alive extends decorate(Trait) {
async load(json) { async load(json) {
await super.load(json); await super.load(json);
this.#context = new Context( const {Script} = latus.get('%resources');
{ this.#deathCheck = await Script.load(this.params.deathCheck, this.entity.contextOrDefault);
entity: this.entity,
},
latus,
);
this.#deathActions = new Actions(compile(this.params.deathActions, latus));
this.#deathCondition = compile(this.params.deathCondition, latus);
} }
get maxLife() { get maxLife() {
@ -172,24 +134,11 @@ export default (latus) => class Alive extends decorate(Trait) {
methods() { methods() {
return { return {
die: async () => { die: () => {
if (this.#isDying) { if (!this.#isDying) {
return; this.#isDying = this.die();
} }
this.#isDying = true; return this.#isDying;
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();
}, },
}; };
@ -212,8 +161,12 @@ export default (latus) => class Alive extends decorate(Trait) {
tick() { tick() {
if ('client' !== process.env.SIDE) { if ('client' !== process.env.SIDE) {
if (!this.#isDying && this.#deathCondition(this.#context)) { if (!this.#isDying) {
this.entity.die(); this.#deathCheck.evaluate(({value: died}) => {
if (died) {
this.entity.die();
}
});
} }
} }
} }

View File

@ -1,4 +1,3 @@
import {Context} from '@avocado/behavior';
import {compose} from '@latus/core'; import {compose} from '@latus/core';
import {StateProperty, Trait} from '@avocado/traits'; import {StateProperty, Trait} from '@avocado/traits';
import merge from 'deepmerge'; import merge from 'deepmerge';
@ -203,13 +202,7 @@ export default (latus) => class Spawner extends decorate(Trait) {
return Promise.all(promises); return Promise.all(promises);
}, },
spawn: async (key, json = {}, context) => { spawn: async (key, json = {}) => {
if (!context && json instanceof Context) {
/* eslint-disable no-param-reassign */
context = json;
json = {};
/* eslint-enable no-param-reassign */
}
if (!this.maySpawn()) { if (!this.maySpawn()) {
return undefined; return undefined;
} }
@ -228,13 +221,7 @@ export default (latus) => class Spawner extends decorate(Trait) {
return entity; return entity;
}, },
spawnAt: async (key, position, json = {}, context) => { spawnAt: async (key, position, json = {}) => {
if (!context && json instanceof Context) {
/* eslint-disable no-param-reassign */
context = json;
json = {};
/* eslint-enable no-param-reassign */
}
const entity = this.maySpawn() const entity = this.maySpawn()
? await this.entity.spawn( ? await this.entity.spawn(
key, key,
@ -245,7 +232,6 @@ export default (latus) => class Spawner extends decorate(Trait) {
arrayMerge: (l, r) => r, arrayMerge: (l, r) => r,
}, },
), ),
context,
) )
: undefined; : undefined;
if (entity) { if (entity) {

View File

@ -229,7 +229,7 @@ export default () => class Visible extends decorate(Trait) {
} }
set visibleScaleX(x) { set visibleScaleX(x) {
this.state.visibleScale = [x, this.state.visibleScale[1]]; this.entity.visibleScale = [x, this.state.visibleScale[1]];
} }
get visibleScaleY() { get visibleScaleY() {
@ -237,7 +237,7 @@ export default () => class Visible extends decorate(Trait) {
} }
set visibleScaleY(y) { set visibleScaleY(y) {
this.state.visibleScale = [this.state.visibleScale[0], y]; this.entity.visibleScale = [this.state.visibleScale[0], y];
} }
}; };

View File

@ -1,5 +1,4 @@
/* eslint-disable no-unused-vars */ /* eslint-disable no-unused-vars */
import {buildInvoke} from '@avocado/behavior';
import {Rectangle, Vector} from '@avocado/math'; import {Rectangle, Vector} from '@avocado/math';
import {LfoResult, Ticker} from '@avocado/timing'; import {LfoResult, Ticker} from '@avocado/timing';
import {Trait} from '@avocado/traits'; import {Trait} from '@avocado/traits';
@ -65,10 +64,14 @@ export default () => class Initiator extends Trait {
Rectangle.centerOn(incident, [16, 16]), Rectangle.centerOn(incident, [16, 16]),
Vector.scale(Vector.abs(directional), 2), Vector.scale(Vector.abs(directional), 2),
); );
const interactives = this.entity.list.queryEntities( const {list} = this.entity;
query, const entities = list.visibleEntities(query);
buildInvoke(['query', 'is'], ['Interactive']), const interactives = [];
); for (let i = 0; i < entities.length; ++i) {
if (entities[i].is('Interactive')) {
interactives.push(entities[i]);
}
}
const oldTarget = this.#target; const oldTarget = this.#target;
if (interactives.length > 0) { if (interactives.length > 0) {
[this.#target] = interactives [this.#target] = interactives

View File

@ -1,9 +1,3 @@
import {
Actions,
buildExpressions,
compile,
Context,
} from '@avocado/behavior';
import {StateProperty, Trait} from '@avocado/traits'; import {StateProperty, Trait} from '@avocado/traits';
import {compose} from '@latus/core'; import {compose} from '@latus/core';
@ -23,7 +17,7 @@ export default (latus) => class Interactive extends decorate(Trait) {
static defaultParams() { static defaultParams() {
return { 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() { methods() {
return { return {
interact: (initiator) => { interact: async (initiator) => {
const context = new Context( const {Script} = latus.get('%resources');
{ const script = await Script.load(this.params.interactScript, {
entity: this.entity, entity: this.entity,
initiator, initiator,
}, });
latus, this.entity.addTickingPromise(script.tickingPromise());
);
this.entity.addTickingPromise(this.#actions.serial(context));
}, },
}; };

View File

@ -0,0 +1,7 @@
module.exports = () => ({
dependencies: {
dom: [
'ace-builds',
],
},
});

View File

@ -16,9 +16,9 @@ const Code = ({
onChange, onChange,
}) => ( }) => (
<AceEditor <AceEditor
name={name} name={name.replace(/[^a-zA-Z0-9]/g, '-')}
fontSize={fontSize} fontSize={fontSize}
height={height} height={height || `${code.split('\n').length}em`}
width="100%" width="100%"
mode="javascript" mode="javascript"
tabSize={2} tabSize={2}
@ -30,7 +30,7 @@ const Code = ({
Code.defaultProps = { Code.defaultProps = {
fontSize: 12, fontSize: 12,
height: '10em', height: null,
onChange: () => {}, onChange: () => {},
name: 'ace-editor', name: 'ace-editor',
}; };

View File

@ -1,35 +1,17 @@
import {join} from 'path'; import {join} from 'path';
import {Expressions} from '@avocado/behavior/persea'; import {Code, Json} from '@avocado/persea';
import {fullEntity} from '@avocado/entity';
import {Json} from '@avocado/persea';
import {useJsonPatcher} from '@avocado/resource/persea'; import {useJsonPatcher} from '@avocado/resource/persea';
import { import {
PropTypes, PropTypes,
React, React,
useEffect,
useLatus,
useState,
} from '@latus/react'; } from '@latus/react';
const Collider = ({ const Collider = ({
entity,
json, json,
path, path,
}) => { }) => {
const latus = useLatus();
const patch = useJsonPatcher(); 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 ( return (
<div className="collider"> <div className="collider">
<div className="label"> <div className="label">
@ -44,23 +26,19 @@ const Collider = ({
<div> <div>
<div className="label"> <div className="label">
<div className="vertical">Start</div> <div className="vertical">Start</div>
{context && ( <Code
<Expressions code={json.params.collisionStartScript}
context={context} name={join(path, 'params/collisionStartScript')}
value={json.params.collisionStartActions} onChange={patch.onChange(join(path, 'params/collisionStartScript'))}
path={join(path, 'params/collisionStartActions')} />
/>
)}
</div> </div>
<div className="label"> <div className="label">
<div className="vertical">End</div> <div className="vertical">End</div>
{context && ( <Code
<Expressions code={json.params.collisionEndScript}
context={context} name={join(path, 'params/collisionEndScript')}
value={json.params.collisionEndActions} onChange={patch.onChange(join(path, 'params/collisionEndScript'))}
path={join(path, 'params/collisionEndActions')} />
/>
)}
</div> </div>
</div> </div>
</div> </div>
@ -117,17 +95,15 @@ Collider.defaultProps = {};
Collider.displayName = 'Collider'; Collider.displayName = 'Collider';
Collider.propTypes = { Collider.propTypes = {
entity: PropTypes.shape({ entity: PropTypes.shape({}).isRequired,
createContext: PropTypes.func,
}).isRequired,
json: PropTypes.shape({ json: PropTypes.shape({
params: PropTypes.shape({ params: PropTypes.shape({
activeCollision: PropTypes.bool, activeCollision: PropTypes.bool,
collidesWithGroups: PropTypes.arrayOf( collidesWithGroups: PropTypes.arrayOf(
PropTypes.string, PropTypes.string,
), ),
collisionEndActions: PropTypes.shape({}), collisionEndScript: PropTypes.string,
collisionStartActions: PropTypes.shape({}), collisionStartScript: PropTypes.string,
collisionGroup: PropTypes.string, collisionGroup: PropTypes.string,
isSensor: PropTypes.bool, isSensor: PropTypes.bool,
}), }),

View File

@ -1,4 +1,3 @@
import {Actions, compile, Context} from '@avocado/behavior';
import {Rectangle, Vector} from '@avocado/math'; import {Rectangle, Vector} from '@avocado/math';
import {StateProperty, Trait} from '@avocado/traits'; import {StateProperty, Trait} from '@avocado/traits';
import {compose} from '@latus/core'; import {compose} from '@latus/core';
@ -12,10 +11,6 @@ export default (latus) => class Collider extends decorate(Trait) {
#collidesWithGroups = []; #collidesWithGroups = [];
#collisionEndActions;
#collisionStartActions;
#collisionGroup = ''; #collisionGroup = '';
#doesNotCollideWith = []; #doesNotCollideWith = [];
@ -27,8 +22,6 @@ export default (latus) => class Collider extends decorate(Trait) {
constructor() { constructor() {
super(); super();
({ ({
collisionEndActions: this.#collisionEndActions,
collisionStartActions: this.#collisionStartActions,
collidesWithGroups: this.#collidesWithGroups, collidesWithGroups: this.#collidesWithGroups,
collisionGroup: this.#collisionGroup, collisionGroup: this.#collisionGroup,
isSensor: this.#isSensor, isSensor: this.#isSensor,
@ -41,14 +34,8 @@ export default (latus) => class Collider extends decorate(Trait) {
collidesWithGroups: [ collidesWithGroups: [
'default', 'default',
], ],
collisionEndActions: { collisionStartScript: 'void 0',
type: 'expressions', collisionEndScript: 'void 0',
expressions: [],
},
collisionStartActions: {
type: 'expressions',
expressions: [],
},
collisionGroup: 'default', collisionGroup: 'default',
isSensor: false, 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() { destroy() {
super.destroy(); super.destroy();
this.releaseAllCollisions(); this.releaseAllCollisions();
@ -206,32 +136,22 @@ export default (latus) => class Collider extends decorate(Trait) {
await super.load(json); await super.load(json);
const { const {
collidesWithGroups, collidesWithGroups,
collisionEndActions,
collisionGroup, collisionGroup,
collisionStartActions,
isSensor, isSensor,
} = this.params; } = this.params;
this.#collidesWithGroups = collidesWithGroups; 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.#collisionGroup = collisionGroup;
this.#isSensor = isSensor; this.#isSensor = isSensor;
} }
pushCollisionTickingPromise(actions, other, incident) { async pushCollisionTickingPromise(codeOrUri, other, incident) {
const context = new Context( const {Script} = latus.get('%resources');
{ const script = await Script.load(codeOrUri, {
entity: this.entity, entity: this.entity,
incident, incident,
other, other,
}, });
latus, this.entity.addTickingPromise(script.tickingPromise());
);
this.entity.addTickingPromise(actions.tickingPromise(context));
} }
releaseAllCollisions() { releaseAllCollisions() {
@ -258,8 +178,8 @@ export default (latus) => class Collider extends decorate(Trait) {
const index = this.indexOfCollidingEntity(other); const index = this.indexOfCollidingEntity(other);
if (-1 !== index) { if (-1 !== index) {
this.#isCollidingWith.splice(index, 1); this.#isCollidingWith.splice(index, 1);
if (this.#collisionEndActions) { if (this.params.collisionEndScript) {
this.pushCollisionTickingPromise(this.#collisionEndActions, other, incident); this.pushCollisionTickingPromise(this.params.collisionEndScript, other, incident);
} }
} }
}, },
@ -271,8 +191,8 @@ export default (latus) => class Collider extends decorate(Trait) {
entity: other, entity: other,
incident, incident,
}); });
if (this.#collisionStartActions) { if (this.params.collisionStartScript) {
this.pushCollisionTickingPromise(this.#collisionStartActions, other, incident); this.pushCollisionTickingPromise(this.params.collisionStartScript, other, incident);
} }
} }
}, },

View File

@ -10,7 +10,6 @@ import useJsonPatcher from '../../hooks/use-json-patcher';
import {controller as controllerPropTypes} from '../../prop-types'; import {controller as controllerPropTypes} from '../../prop-types';
const Json = ({path, resource}) => { const Json = ({path, resource}) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
const patch = useJsonPatcher(); const patch = useJsonPatcher();
return ( return (
<JsonComponent <JsonComponent

View File

@ -6,8 +6,10 @@ export default class Sandbox {
constructor(ast, context) { constructor(ast, context) {
this.ast = ast; this.ast = ast;
this.reset(); this.parents = new WeakMap();
this.scopes = new WeakMap();
this.context = context; this.context = context;
this.reset();
} }
get context() { get context() {
@ -53,13 +55,17 @@ export default class Sandbox {
const scope = this.nodeScope(node); const scope = this.nodeScope(node);
const {Vasync, value} = this.evaluate(right); const {Vasync, value} = this.evaluate(right);
if (!types.isMemberExpression(left)) { if (!types.isMemberExpression(left)) {
const assign = (value) => {
scope.set(left.name, value);
return value;
};
if (Vasync) { if (Vasync) {
return { return {
async: true, 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 { const {
computed, computed,
@ -67,7 +73,7 @@ export default class Sandbox {
optional, optional,
property, property,
} = left; } = left;
const assign = (O, P, value) => { const memberAssign = (O, P, value) => {
if (optional && !O) { if (optional && !O) {
return undefined; return undefined;
} }
@ -75,7 +81,7 @@ export default class Sandbox {
return O[P] = value; return O[P] = value;
}; };
const makeAsync = (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 O = this.evaluate(object);
const P = computed ? this.evaluate(property) : {value: property.name}; const P = computed ? this.evaluate(property) : {value: property.name};
@ -86,7 +92,7 @@ export default class Sandbox {
value: makeAsync(O.value, P.value, value), 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}) { evaluateAwaitExpression({argument}) {
@ -132,15 +138,17 @@ export default class Sandbox {
return undefined; return undefined;
} }
}; };
const {async: lasync, value: left} = this.evaluate(node.left); const left = this.evaluate(node.left);
const {async: rasync, value: right} = this.evaluate(node.right); const right = this.evaluate(node.right);
if (lasync || rasync) { if (left.async || right.async) {
return { return {
async: true, 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) { evaluateCallExpression(node) {
@ -148,6 +156,7 @@ export default class Sandbox {
const args = []; const args = [];
for (let i = 0; i < node.arguments.length; i++) { for (let i = 0; i < node.arguments.length; i++) {
const arg = node.arguments[i]; const arg = node.arguments[i];
this.setNextScope(arg, this.nodeScope(node));
const {async, value} = this.evaluate(arg); const {async, value} = this.evaluate(arg);
// eslint-disable-next-line no-bitwise // eslint-disable-next-line no-bitwise
asyncArgs |= async; asyncArgs |= async;
@ -190,6 +199,7 @@ export default class Sandbox {
optional: memberOptional, optional: memberOptional,
property, property,
} = callee; } = callee;
this.setNextScope(callee);
const O = this.evaluate(object); const O = this.evaluate(object);
const P = computed ? this.evaluate(property) : {value: property.name}; const P = computed ? this.evaluate(property) : {value: property.name};
if (asyncArgs || O.async || P.async) { if (asyncArgs || O.async || P.async) {
@ -209,7 +219,8 @@ export default class Sandbox {
} }
evaluateIdentifier(node) { 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 // 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) { evaluateUpdateExpression(node) {
const {argument, operator, prefix} = node; const {argument, operator, prefix} = node;
const {async, value} = this.evaluate(argument); const {async, value} = this.evaluate(argument);
@ -375,8 +417,10 @@ export default class Sandbox {
reset() { reset() {
const self = this; const self = this;
const {context} = this;
this.parents = new WeakMap(); this.parents = new WeakMap();
this.scopes = new WeakMap(); this.scopes = new WeakMap();
this.context = context;
this.runner = (function* traverse() { this.runner = (function* traverse() {
return yield* self.traverse(self.ast); return yield* self.traverse(self.ast);
}()); }());
@ -414,6 +458,7 @@ export default class Sandbox {
'Literal', 'Literal',
'MemberExpression', 'MemberExpression',
'ObjectExpression', 'ObjectExpression',
'UnaryExpression',
'UpdateExpression', 'UpdateExpression',
]; ];
for (let i = 0; i < flat.length; i++) { for (let i = 0; i < flat.length; i++) {
@ -615,7 +660,9 @@ export default class Sandbox {
// Evaluate... // Evaluate...
if (types.isReturnStatement(node)) { if (types.isReturnStatement(node)) {
// eslint-disable-next-line consistent-return // eslint-disable-next-line consistent-return
return this.evaluate(node.argument); return !node.argument
? {value: undefined}
: this.evaluate(node.argument);
} }
if (types.isDirective(node)) { if (types.isDirective(node)) {
yield this.evaluate(node.value); yield this.evaluate(node.value);

View File

@ -55,11 +55,17 @@ export default class Scope {
set(key, value) { set(key, value) {
let walk = this; let walk = this;
// eslint-disable-next-line no-constant-condition
while (walk) { while (walk) {
if (key in walk.context) { if (key in walk.context) {
walk.context[key] = value; walk.context[key] = value;
return value; return value;
} }
// TODO: option to disallow global set
if (!walk.parent) {
walk.context[key] = value;
return value;
}
walk = walk.parent; walk = walk.parent;
} }
return undefined; return undefined;