Compare commits

...

17 Commits

Author SHA1 Message Date
cha0s
0c06dd7b83 refactor: ecs diff 2024-06-22 12:30:25 -05:00
cha0s
3b6aee099f feat: sprite anchor 2024-06-22 12:12:13 -05:00
cha0s
d94784e58e fix: qty spacing 2024-06-22 12:12:03 -05:00
cha0s
9275b75701 fix: line-height 2024-06-22 12:11:51 -05:00
cha0s
ea2337ee00 feat: quick and dirty inventory 2024-06-22 11:44:49 -05:00
cha0s
3c562ca69a fix: traverse spread 2024-06-22 11:43:52 -05:00
cha0s
45bb06002e refactor: acorn 2024-06-22 10:47:17 -05:00
cha0s
f69ee95732 refactor: swc ain't ready for primetime 2024-06-22 08:02:23 -05:00
cha0s
630740e71e feat: quick and dirty item use and scripting 2024-06-22 07:47:19 -05:00
cha0s
9e5efdf413 feat: ticking promise 2024-06-22 05:11:54 -05:00
cha0s
1a908bc0af chore: test scopes 2024-06-22 04:05:18 -05:00
cha0s
c7cb1b6876 refactor: simplify 2024-06-22 04:05:09 -05:00
cha0s
a62a578664 fix: empty test 2024-06-22 04:03:42 -05:00
cha0s
103f458158 refactor: default blank schema 2024-06-22 03:59:43 -05:00
cha0s
247ca15002 chore: dev tools 2024-06-21 22:24:58 -05:00
cha0s
945e759942 feat: save ECS 2024-06-21 18:16:41 -05:00
cha0s
b56d95f4fa fix: skip empty 2024-06-21 18:15:15 -05:00
62 changed files with 808 additions and 527 deletions

1
.vscode/launch.json vendored
View File

@ -10,6 +10,7 @@
"name": "Silphius Chrome", "name": "Silphius Chrome",
"url": "https://localhost:3000", "url": "https://localhost:3000",
"webRoot": "${workspaceFolder}", "webRoot": "${workspaceFolder}",
"runtimeArgs": ["--auto-open-devtools-for-tabs"]
}, },
{ {
"type": "node", "type": "node",

3
app/astride/README.md Normal file
View File

@ -0,0 +1,3 @@
# ASTRide
Ride your AST :)

47
app/astride/evaluate.js Normal file
View File

@ -0,0 +1,47 @@
const evaluators = Object.fromEntries(
Object.entries(
import.meta.glob(
['./evaluators/*.js', '!./evaluators/*.test.js'],
{eager: true, import: 'default'},
)
)
.map(([path, evaluator]) => ([
path.replace(/\.\/evaluators\/(.*)\.js/, '$1'),
evaluator,
])),
);
export default function evaluate(node, {scope} = {}) {
switch (node.type) {
case 'ArrayExpression':
return evaluators.array(node, {evaluate, scope});
case 'AssignmentExpression':
return evaluators.assignment(node, {evaluate, scope});
case 'AwaitExpression':
return evaluators.await(node, {evaluate, scope});
case 'BinaryExpression':
return evaluators.binary(node, {evaluate, scope});
case 'Literal':
return evaluators.literal(node, {evaluate, scope});
case 'CallExpression':
return evaluators.call(node, {evaluate, scope});
case 'ChainExpression':
return evaluate(node.expression, {evaluate, scope});
case 'ConditionalExpression':
return evaluators.conditional(node, {evaluate, scope});
case 'Identifier':
return evaluators.identifier(node, {evaluate, scope});
case 'LogicalExpression':
return evaluators.binary(node, {evaluate, scope});
case 'MemberExpression':
return evaluators.member(node, {evaluate, scope});
case 'ObjectExpression':
return evaluators.object(node, {evaluate, scope});
case 'UnaryExpression':
return evaluators.unary(node, {evaluate, scope});
case 'UpdateExpression':
return evaluators.update(node, {evaluate, scope});
default:
throw new EvalError(`astride: Can't evaluate node of type ${node.type}`)
}
}

View File

@ -1,8 +1,8 @@
export default function(node, {evaluate, scope}) { export default function(node, {evaluate, scope}) {
const elements = []; const elements = [];
let isAsync = false; let isAsync = false;
for (const {expression} of node.elements) { for (const element of node.elements) {
const {async, value} = evaluate(expression, {scope}); const {async, value} = evaluate(element, {scope});
isAsync = isAsync || async; isAsync = isAsync || async;
elements.push(value); elements.push(value);
} }

View File

@ -1,7 +1,7 @@
import {expect, test} from 'vitest'; import {expect, test} from 'vitest';
import evaluate from '@/swcx/evaluate.js'; import evaluate from '@/astride/evaluate.js';
import expression from '@/swcx/test/expression.js'; import expression from '@/astride/test/expression.js';
test('evaluates array of literals', async () => { test('evaluates array of literals', async () => {
expect(evaluate(await expression('[1.5, 2, "three"]'))) expect(evaluate(await expression('[1.5, 2, "three"]')))

View File

@ -1,7 +1,6 @@
import { import {
isComputed,
isMemberExpression, isMemberExpression,
} from '@/swcx/types.js'; } from '@/astride/types.js';
export default function(node, {evaluate, scope}) { export default function(node, {evaluate, scope}) {
const {operator, left} = node; const {operator, left} = node;
@ -9,24 +8,24 @@ export default function(node, {evaluate, scope}) {
if (!isMemberExpression(left)) { if (!isMemberExpression(left)) {
const assign = (value) => { const assign = (value) => {
switch (operator) { switch (operator) {
case '=' : return scope.set(left.value, value); case '=' : return scope.set(left.name, value);
case '+=' : return scope.set(left.value, scope.get(left.value) + value); case '+=' : return scope.set(left.name, scope.get(left.name) + value);
case '-=' : return scope.set(left.value, scope.get(left.value) - value); case '-=' : return scope.set(left.name, scope.get(left.name) - value);
case '*=' : return scope.set(left.value, scope.get(left.value) * value); case '*=' : return scope.set(left.name, scope.get(left.name) * value);
case '/=' : return scope.set(left.value, scope.get(left.value) / value); case '/=' : return scope.set(left.name, scope.get(left.name) / value);
case '%=' : return scope.set(left.value, scope.get(left.value) % value); case '%=' : return scope.set(left.name, scope.get(left.name) % value);
case '**=' : return scope.set(left.value, scope.get(left.value) ** value); case '**=' : return scope.set(left.name, scope.get(left.name) ** value);
case '<<=' : return scope.set(left.value, scope.get(left.value) << value); case '<<=' : return scope.set(left.name, scope.get(left.name) << value);
case '>>=' : return scope.set(left.value, scope.get(left.value) >> value); case '>>=' : return scope.set(left.name, scope.get(left.name) >> value);
case '>>>=': return scope.set(left.value, scope.get(left.value) >>> value); case '>>>=': return scope.set(left.name, scope.get(left.name) >>> value);
case '|=' : return scope.set(left.value, scope.get(left.value) | value); case '|=' : return scope.set(left.name, scope.get(left.name) | value);
case '^=' : return scope.set(left.value, scope.get(left.value) ^ value); case '^=' : return scope.set(left.name, scope.get(left.name) ^ value);
case '&=' : return scope.set(left.value, scope.get(left.value) & value); case '&=' : return scope.set(left.name, scope.get(left.name) & value);
case '||=' : return scope.set(left.value, scope.get(left.value) || value); case '||=' : return scope.set(left.name, scope.get(left.name) || value);
case '&&=' : return scope.set(left.value, scope.get(left.value) && value); case '&&=' : return scope.set(left.name, scope.get(left.name) && value);
case '??=' : { case '??=' : {
const l = scope.get(left.value); const l = scope.get(left.name);
return scope.set(left.value, (l === null || l === undefined) ? value : l); return scope.set(left.name, (l === null || l === undefined) ? value : l);
} }
/* v8 ignore next 2 */ /* v8 ignore next 2 */
default: default:
@ -42,6 +41,7 @@ export default function(node, {evaluate, scope}) {
return {value: assign(right.value)}; return {value: assign(right.value)};
} }
const { const {
computed,
object, object,
property, property,
} = left; } = left;
@ -72,7 +72,10 @@ export default function(node, {evaluate, scope}) {
Promise.all([O, P, value]).then(([O, P, value]) => memberAssign(O, P, value)) Promise.all([O, P, value]).then(([O, P, value]) => memberAssign(O, P, value))
); );
const O = evaluate(object, {scope}); const O = evaluate(object, {scope});
const P = isComputed(property) ? evaluate(property, {scope}) : {value: property.value}; const P = computed
? evaluate(property, {scope})
// Otherwise, identifier
: {value: property.name};
if (right.async || O.async || P.async) { if (right.async || O.async || P.async) {
return { return {
async: true, async: true,

View File

@ -1,7 +1,7 @@
import {expect, test} from 'vitest'; import {expect, test} from 'vitest';
import evaluate from '@/swcx/evaluate.js'; import evaluate from '@/astride/evaluate.js';
import expression from '@/swcx/test/expression.js'; import expression from '@/astride/test/expression.js';
const scopeTest = test.extend({ const scopeTest = test.extend({
scope: async ({}, use) => { scope: async ({}, use) => {
@ -18,10 +18,14 @@ scopeTest('evaluates =', async ({scope}) => {
.to.deep.include({value: 4}); .to.deep.include({value: 4});
expect(scope.get('x')) expect(scope.get('x'))
.to.equal(4); .to.equal(4);
expect(evaluate(await expression('O.x = 4'), {scope})) expect(evaluate(await expression('O.x = 8'), {scope}))
.to.deep.include({value: 4}); .to.deep.include({value: 8});
expect(scope.get('O').x) expect(scope.get('O').x)
.to.equal(4); .to.equal(8);
expect(evaluate(await expression('O["y"] = 16'), {scope}))
.to.deep.include({value: 16});
expect(scope.get('O').y)
.to.equal(16);
}); });
scopeTest('evaluates +=', async ({scope}) => { scopeTest('evaluates +=', async ({scope}) => {

View File

@ -1,7 +1,7 @@
import {expect, test} from 'vitest'; import {expect, test} from 'vitest';
import evaluate from '@/swcx/evaluate.js'; import evaluate from '@/astride/evaluate.js';
import expression from '@/swcx/test/expression.js'; import expression from '@/astride/test/expression.js';
test('evaluates await expressions', async () => { test('evaluates await expressions', async () => {
const evaluated = evaluate(await expression('await 1')); const evaluated = evaluate(await expression('await 1'));

View File

@ -1,7 +1,7 @@
import {expect, test} from 'vitest'; import {expect, test} from 'vitest';
import evaluate from '@/swcx/evaluate.js'; import evaluate from '@/astride/evaluate.js';
import expression from '@/swcx/test/expression.js'; import expression from '@/astride/test/expression.js';
test('evaluates +', async () => { test('evaluates +', async () => {
expect(evaluate(await expression('10 + 2'))) expect(evaluate(await expression('10 + 2')))

View File

@ -1,24 +1,25 @@
import fastCall from '@/util/fast-call.js'; import fastCall from '@/util/fast-call.js';
import { import {
isComputed,
isMemberExpression, isMemberExpression,
unwrap, } from '@/astride/types.js';
} from '@/swcx/types.js';
export default function(node, {evaluate, scope}) { export default function(node, {evaluate, scope}) {
let asyncArgs = false; let asyncArgs = false;
const args = []; const args = [];
for (let i = 0; i < node.arguments.length; i++) { for (let i = 0; i < node.arguments.length; i++) {
const {expression: arg} = node.arguments[i]; const arg = node.arguments[i];
const {async, value} = evaluate(arg, {scope}); const {async, value} = evaluate(arg, {scope});
asyncArgs ||= async; asyncArgs ||= async;
args.push(value); args.push(value);
} }
const {callee: wrappedCallee} = node; const {callee} = node;
const callee = unwrap(wrappedCallee); const {
const callOptional = callee.wrapper?.optional || node.wrapper?.optional; computed,
object,
property,
} = callee;
const invoke = (fn, holder, args) => { const invoke = (fn, holder, args) => {
if (callOptional && !fn) { if (node.optional && !fn) {
return undefined; return undefined;
} }
return fastCall(fn, holder, args); return fastCall(fn, holder, args);
@ -35,19 +36,18 @@ export default function(node, {evaluate, scope}) {
} }
return {value: invoke(value, undefined, args)}; return {value: invoke(value, undefined, args)};
} }
const {
object,
property,
} = callee;
const O = evaluate(object, {scope}); const O = evaluate(object, {scope});
const P = isComputed(property) ? evaluate(property, {scope}) : {value: property.value}; const P = computed
? evaluate(property, {scope})
// Otherwise, identifier.
: {value: property.name};
if (asyncArgs || O.async || P.async) { if (asyncArgs || O.async || P.async) {
return { return {
async: true, async: true,
value: Promise value: Promise
.all([O.value, P.value, Promise.all(args)]) .all([O.value, P.value, Promise.all(args)])
.then(([O, P, args]) => invoke(callOptional ? O?.[P] : O[P], O, args)), .then(([O, P, args]) => invoke(callee.optional ? O?.[P] : O[P], O, args)),
}; };
} }
return {value: invoke(callOptional ? O.value?.[P.value] : O.value[P.value], O.value, args)}; return {value: invoke(callee.optional ? O.value?.[P.value] : O.value[P.value], O.value, args)};
} }

View File

@ -1,7 +1,7 @@
import {expect, test} from 'vitest'; import {expect, test} from 'vitest';
import evaluate from '@/swcx/evaluate.js'; import evaluate from '@/astride/evaluate.js';
import expression from '@/swcx/test/expression.js'; import expression from '@/astride/test/expression.js';
const scopeTest = test.extend({ const scopeTest = test.extend({
scope: async ({}, use) => { scope: async ({}, use) => {
@ -48,10 +48,10 @@ scopeTest('evaluates optional calls', async ({scope}) => {
scope.set('O', {}); scope.set('O', {});
expect(evaluate(await expression('g?.(1, 2, 3)'), {scope}).value) expect(evaluate(await expression('g?.(1, 2, 3)'), {scope}).value)
.to.equal(undefined); .to.equal(undefined);
expect(evaluate(await expression('O?.g(1, 2, 3)'), {scope}).value) // expect(evaluate(await expression('O?.g(1, 2, 3)'), {scope}).value)
.to.equal(undefined); // .to.equal(undefined);
expect(evaluate(await expression('O?.g?.(1, 2, 3)'), {scope}).value) // expect(evaluate(await expression('O?.g?.(1, 2, 3)'), {scope}).value)
.to.equal(undefined); // .to.equal(undefined);
}); });
scopeTest('evaluates async calls', async ({scope}) => { scopeTest('evaluates async calls', async ({scope}) => {

View File

@ -1,7 +1,7 @@
import {expect, test} from 'vitest'; import {expect, test} from 'vitest';
import evaluate from '@/swcx/evaluate.js'; import evaluate from '@/astride/evaluate.js';
import expression from '@/swcx/test/expression.js'; import expression from '@/astride/test/expression.js';
const scopeTest = test.extend({ const scopeTest = test.extend({
scope: async ({}, use) => { scope: async ({}, use) => {

View File

@ -1,3 +1,3 @@
export default function(node, {scope}) { export default function(node, {scope}) {
return {value: scope.get(node.value)}; return {value: scope.get(node.name)};
} }

View File

@ -1,7 +1,7 @@
import {expect, test} from 'vitest'; import {expect, test} from 'vitest';
import evaluate from '@/swcx/evaluate.js'; import evaluate from '@/astride/evaluate.js';
import expression from '@/swcx/test/expression.js'; import expression from '@/astride/test/expression.js';
test('evaluates numeric literals', async () => { test('evaluates numeric literals', async () => {
expect(evaluate(await expression('1'))) expect(evaluate(await expression('1')))

View File

@ -1,11 +1,11 @@
import { export default function(node, {evaluate, scope}) {
isComputed, const {computed, object, property, wrapper} = node;
} from '@/swcx/types.js';
export default function({object, property, wrapper}, {evaluate, scope}) {
const member = (O, P) => (wrapper?.optional ? O?.[P] : O[P]); const member = (O, P) => (wrapper?.optional ? O?.[P] : O[P]);
const O = evaluate(object, {scope}); const O = evaluate(object, {scope});
const P = isComputed(property) ? evaluate(property, {scope}) : {value: property.value}; const P = computed
? evaluate(property, {scope})
// Otherwise, identifier
: {value: property.name};
if (O.async || P.async) { if (O.async || P.async) {
return { return {
async: true, async: true,

View File

@ -1,7 +1,7 @@
import {expect, test} from 'vitest'; import {expect, test} from 'vitest';
import evaluate from '@/swcx/evaluate.js'; import evaluate from '@/astride/evaluate.js';
import expression from '@/swcx/test/expression.js'; import expression from '@/astride/test/expression.js';
const scopeTest = test.extend({ const scopeTest = test.extend({
scope: async ({}, use) => { scope: async ({}, use) => {

View File

@ -1,30 +1,25 @@
import { import {
isComputed,
isIdentifier, isIdentifier,
isKeyValueProperty, isLiteral,
isNumericLiteral, isProperty,
isSpreadElement, isSpreadElement,
isStringLiteral, } from '@/astride/types.js';
} from '@/swcx/types.js';
export default function(node, {evaluate, scope}) { export default function(node, {evaluate, scope}) {
const {properties} = node; const {properties} = node;
let isAsync = false; let isAsync = false;
const entries = []; const entries = [];
for (let i = 0; i < properties.length; i++) { for (let i = 0; i < properties.length; i++) {
if (isKeyValueProperty(properties[i])) { if (isProperty(properties[i])) {
const {key, value} = properties[i]; const {computed, key, value} = properties[i];
let k; let k;
if (isComputed(key)) { if (computed) {
k = evaluate(key, {scope}); k = evaluate(key, {scope});
} }
else if (isIdentifier(key)) { else if (isIdentifier(key)) {
k = {value: key.value}; k = {value: key.name};
} }
else if (isNumericLiteral(key)) { else if (isLiteral(key)) {
k = {value: key.value};
}
else if (isStringLiteral(key)) {
k = {value: key.value}; k = {value: key.value};
} }
/* v8 ignore next 3 */ /* v8 ignore next 3 */
@ -41,7 +36,7 @@ export default function(node, {evaluate, scope}) {
} }
} }
if (isSpreadElement(properties[i])) { if (isSpreadElement(properties[i])) {
const {arguments: argument} = properties[i]; const {argument} = properties[i];
const spreading = evaluate(argument, {scope}); const spreading = evaluate(argument, {scope});
isAsync ||= spreading.async; isAsync ||= spreading.async;
if (spreading.async) { if (spreading.async) {

View File

@ -1,7 +1,7 @@
import {expect, test} from 'vitest'; import {expect, test} from 'vitest';
import evaluate from '@/swcx/evaluate.js'; import evaluate from '@/astride/evaluate.js';
import expression from '@/swcx/test/expression.js'; import expression from '@/astride/test/expression.js';
test('evaluates object expression', async () => { test('evaluates object expression', async () => {
let evaluated; let evaluated;

View File

@ -1,7 +1,7 @@
import {expect, test} from 'vitest'; import {expect, test} from 'vitest';
import evaluate from '@/swcx/evaluate.js'; import evaluate from '@/astride/evaluate.js';
import expression from '@/swcx/test/expression.js'; import expression from '@/astride/test/expression.js';
test('evaluates +', async () => { test('evaluates +', async () => {
expect(evaluate(await expression('+1'))) expect(evaluate(await expression('+1')))

View File

@ -4,16 +4,16 @@ export default function(node, {evaluate, scope}) {
const update = (value) => { const update = (value) => {
if (prefix) { if (prefix) {
switch (operator) { switch (operator) {
case '++': return scope.set(argument.value, value + 1); case '++': return scope.set(argument.name, value + 1);
case '--': return scope.set(argument.value, value - 1); case '--': return scope.set(argument.name, value - 1);
} }
} }
switch (operator) { switch (operator) {
case '++': case '++':
scope.set(argument.value, value + 1); scope.set(argument.name, value + 1);
return value; return value;
case '--': case '--':
scope.set(argument.value, value - 1); scope.set(argument.name, value - 1);
return value; return value;
} }
/* v8 ignore next */ /* v8 ignore next */

View File

@ -1,7 +1,7 @@
import {expect, test} from 'vitest'; import {expect, test} from 'vitest';
import evaluate from '@/swcx/evaluate.js'; import evaluate from '@/astride/evaluate.js';
import expression from '@/swcx/test/expression.js'; import expression from '@/astride/test/expression.js';
const scopeTest = test.extend({ const scopeTest = test.extend({
scope: async ({}, use) => { scope: async ({}, use) => {

View File

@ -1,6 +1,6 @@
import evaluate from '@/swcx/evaluate.js'; import evaluate from '@/astride/evaluate.js';
import Scope from '@/swcx/scope.js'; import Scope from '@/astride/scope.js';
import traverse, {TRAVERSAL_PATH} from '@/swcx/traverse.js'; import traverse, {TRAVERSAL_PATH} from '@/astride/traverse.js';
import { import {
isArrayPattern, isArrayPattern,
isBlockStatement, isBlockStatement,
@ -13,7 +13,7 @@ import {
isReturnStatement, isReturnStatement,
isVariableDeclarator, isVariableDeclarator,
isWhileStatement, isWhileStatement,
} from '@/swcx/types.js'; } from '@/astride/types.js';
export default class Sandbox { export default class Sandbox {
@ -57,7 +57,7 @@ export default class Sandbox {
} }
get context() { get context() {
return this.scopes.get(this.ast).context; return this.$$context;
} }
destructureArray(id, init) { destructureArray(id, init) {
@ -69,7 +69,7 @@ export default class Sandbox {
continue; continue;
} }
if (isIdentifier(element)) { if (isIdentifier(element)) {
scope.allocate(element.value, init[i]); scope.allocate(element.name, init[i]);
} }
/* v8 ignore next 3 */ /* v8 ignore next 3 */
else { else {
@ -85,12 +85,12 @@ export default class Sandbox {
for (let i = 0; i < properties.length; ++i) { for (let i = 0; i < properties.length; ++i) {
const property = properties[i]; const property = properties[i];
if (isObjectPattern(property.value)) { if (isObjectPattern(property.value)) {
this.destructureObject(property.value, init[property.key.value], scope); this.destructureObject(property.value, init[property.key.name], scope);
} }
else { else {
scope.allocate( scope.allocate(
property.value ? property.value.value : property.key.value, property.value.name,
init[property.key.value], init[property.key.name],
); );
} }
} }
@ -109,7 +109,7 @@ export default class Sandbox {
const {id} = node; const {id} = node;
const scope = this.scopes.get(node); const scope = this.scopes.get(node);
if (null === node.init) { if (null === node.init) {
scope.allocate(id.value, undefined); scope.allocate(id.name, undefined);
} }
else { else {
const init = this.evaluate(node.init); const init = this.evaluate(node.init);
@ -118,12 +118,12 @@ export default class Sandbox {
yield { yield {
async: true, async: true,
value: Promise.resolve(init.value).then((value) => { value: Promise.resolve(init.value).then((value) => {
scope.allocate(id.value, value); scope.allocate(id.name, value);
}), }),
}; };
} }
else { else {
scope.allocate(id.value, init.value); scope.allocate(id.name, init.value);
} }
} }
else if (isArrayPattern(id)) { else if (isArrayPattern(id)) {
@ -242,7 +242,7 @@ export default class Sandbox {
yield this.evaluate(node.expression); yield this.evaluate(node.expression);
} }
// yield ForStatement afterthought. // yield ForStatement afterthought.
if (isForStatement(parent) && !isBlockStatement(node)) { if (isForStatement(parent) && node === parent.update) {
yield this.evaluate(node); yield this.evaluate(node);
/* v8 ignore next */ /* v8 ignore next */
} }

View File

@ -1,12 +1,20 @@
import {parse as acornParse} from 'acorn';
import {expect, test} from 'vitest'; import {expect, test} from 'vitest';
import {parse} from '@swc/core'; import Sandbox from '@/astride/sandbox.js';
import Sandbox from '@/swcx/sandbox.js';
function parse(code, options = {}) {
return acornParse(code, {
ecmaVersion: 'latest',
sourceType: 'module',
...options,
})
}
test('declares variables', async () => { test('declares variables', async () => {
const sandbox = new Sandbox( const sandbox = new Sandbox(
await parse(` await parse(`
const scalar = 1; const scalar = true ? +1 : 32;
const asyncScalar = await 2; const asyncScalar = await 2;
const array = [3, 4, 5]; const array = [3, 4, 5];
const asyncArray = await [6, 7, 8]; const asyncArray = await [6, 7, 8];
@ -32,6 +40,30 @@ test('declares variables', async () => {
}); });
}); });
test('scopes variables', async () => {
const sandbox = new Sandbox(
await parse(`
const result = [];
const scalar = 1;
result.push(scalar);
{
const scalar = 2;
result.push(scalar);
}
result.push(scalar);
`),
);
let result;
do {
result = sandbox.step();
if (result.value?.async) {
await result.value.async;
}
} while (!result.done);
expect(sandbox.context.result)
.to.deep.equal([1, 2, 1]);
});
test('destructures variables', async () => { test('destructures variables', async () => {
const sandbox = new Sandbox( const sandbox = new Sandbox(
await parse(` await parse(`

View File

@ -0,0 +1,8 @@
export default async function(code) {
const {parse} = await import('acorn');
const ast = parse(code, {
ecmaVersion: 'latest',
sourceType: 'module',
});
return ast.body[0].expression;
}

View File

@ -1,36 +1,25 @@
export const TRAVERSAL_PATH = { export const TRAVERSAL_PATH = {
ArrayExpression: (node) => node.elements.map(({expression}) => expression), ArrayExpression: ['elements'],
ArrayPattern: ['elements'], ArrayPattern: ['elements'],
AssignmentExpression: ['left', 'right'], AssignmentExpression: ['left', 'right'],
AssignmentPatternProperty: ['key'],
AwaitExpression: ['argument'], AwaitExpression: ['argument'],
BinaryExpression: ['left', 'right'], BinaryExpression: ['left', 'right'],
BlockStatement: ['stmts'], BlockStatement: ['body'],
BooleanLiteral: [], CallExpression: ['arguments', 'callee'],
CallExpression: (node) => ([
node.callee,
...node.arguments.map(({expression}) => expression),
]),
Computed: ['expression'],
ConditionalExpression: ['alternate', 'consequent', 'test'], ConditionalExpression: ['alternate', 'consequent', 'test'],
DoWhileStatement: ['body', 'test'], DoWhileStatement: ['body', 'test'],
ExpressionStatement: ['expression'], ExpressionStatement: ['expression'],
ForStatement: ['body', 'init', 'test', 'update'], ForStatement: ['body', 'init', 'test', 'update'],
Identifier: [], Identifier: [],
IfStatement: ['alternate', 'consequent', 'test'], IfStatement: ['alternate', 'consequent', 'test'],
KeyValuePatternProperty: ['key', 'value'],
KeyValueProperty: ['key', 'value'],
MemberExpression: ['object', 'property'], MemberExpression: ['object', 'property'],
Module: ['body'], Literal: [],
NullLiteral: [],
NumericLiteral: [],
ObjectExpression: ['properties'], ObjectExpression: ['properties'],
ObjectPattern: ['properties'], ObjectPattern: ['properties'],
OptionalChainingExpression: ['base'], Program: ['body'],
ParenthesisExpression: ['expression'], Property: ['key', 'value'],
RegExpLiteral: [],
ReturnStatement: ['argument'], ReturnStatement: ['argument'],
StringLiteral: [], SpreadElement: ['argument'],
UnaryExpression: ['argument'], UnaryExpression: ['argument'],
UpdateExpression: ['argument'], UpdateExpression: ['argument'],
VariableDeclaration: ['declarations'], VariableDeclaration: ['declarations'],
@ -41,7 +30,7 @@ export const TRAVERSAL_PATH = {
export default function traverse(node, visitor) { export default function traverse(node, visitor) {
/* v8 ignore next 3 */ /* v8 ignore next 3 */
if (!(node.type in TRAVERSAL_PATH)) { if (!(node.type in TRAVERSAL_PATH)) {
throw new Error(`node type ${node.type} not traversable`); throw new Error(`node type ${node.type} not traversable. (${Object.keys(node).join(', ')})`);
} }
visitor(node, 'enter'); visitor(node, 'enter');
const path = TRAVERSAL_PATH[node.type]; const path = TRAVERSAL_PATH[node.type];

View File

@ -14,14 +14,6 @@ export function isBlockStatement(node) {
return true; return true;
} }
export function isComputed(node) {
/* v8 ignore next 3 */
if (!node || node.type !== 'Computed') {
return false;
}
return true;
}
export function isDoWhileStatement(node) { export function isDoWhileStatement(node) {
/* v8 ignore next 3 */ /* v8 ignore next 3 */
if (!node || node.type !== 'DoWhileStatement') { if (!node || node.type !== 'DoWhileStatement') {
@ -60,9 +52,9 @@ export function isIfStatement(node) {
return true; return true;
} }
export function isKeyValueProperty(node) { export function isLiteral(node) {
/* v8 ignore next 3 */ /* v8 ignore next 3 */
if (!node || node.type !== 'KeyValueProperty') { if (!node || node.type !== 'Literal') {
return false; return false;
} }
return true; return true;
@ -76,17 +68,17 @@ export function isMemberExpression(node) {
return true; return true;
} }
export function isNumericLiteral(node) { export function isObjectPattern(node) {
/* v8 ignore next 3 */ /* v8 ignore next 3 */
if (!node || node.type !== 'NumericLiteral') { if (!node || node.type !== 'ObjectPattern') {
return false; return false;
} }
return true; return true;
} }
export function isObjectPattern(node) { export function isProperty(node) {
/* v8 ignore next 3 */ /* v8 ignore next 3 */
if (!node || node.type !== 'ObjectPattern') { if (!node || node.type !== 'Property') {
return false; return false;
} }
return true; return true;
@ -108,14 +100,6 @@ export function isSpreadElement(node) {
return true; return true;
} }
export function isStringLiteral(node) {
/* v8 ignore next 3 */
if (!node || node.type !== 'StringLiteral') {
return false;
}
return true;
}
export function isVariableDeclarator(node) { export function isVariableDeclarator(node) {
/* v8 ignore next 3 */ /* v8 ignore next 3 */
if (!node || node.type !== 'VariableDeclarator') { if (!node || node.type !== 'VariableDeclarator') {
@ -130,22 +114,3 @@ export function isWhileStatement(node) {
} }
return true; return true;
} }
export function unwrap(node) {
let wrapped = node;
switch (node.type) {
case 'Computed':
wrapped = unwrap(node.expression);
break;
case 'OptionalChainingExpression':
wrapped = unwrap(node.base);
break;
case 'ParenthesisExpression':
wrapped = unwrap(node.expression);
break;
}
if (node !== wrapped) {
wrapped.wrapper = node;
}
return wrapped;
}

View File

@ -0,0 +1,3 @@
export default {
health: {type: 'uint32'},
};

View File

@ -1,7 +1,7 @@
export default (type) => ({ export default (type, {x = 0, y = 0} = {}) => ({
type: 'object', type: 'object',
properties: { properties: {
x: {type}, x: {defaultValue: x, type},
y: {type}, y: {defaultValue: y, type},
}, },
}); });

View File

@ -0,0 +1,34 @@
import Schema from '@/ecs/schema.js';
export default function(Component) {
return class Inventory extends Component {
instanceFromSchema() {
const Instance = super.instanceFromSchema();
Instance.prototype.item = function (slot) {
const {slots} = this;
for (const {slotIndex, source} of this.slots) {
if (slot === slotIndex) {
return source;
}
}
};
return Instance;
}
static schema = new Schema({
type: 'object',
properties: {
slots: {
type: 'array',
subtype: {
type: 'object',
properties: {
quantity: {type: 'uint16'},
slotIndex: {type: 'uint16'},
source: {type: 'string'},
},
},
},
},
});
}
}

View File

@ -1,4 +1,6 @@
import vector2d from "./helpers/vector-2d";
export default { export default {
anchor: vector2d('float32', {x: 0.5, y: 0.5}),
animation: {type: 'string'}, animation: {type: 'string'},
elapsed: {type: 'float32'}, elapsed: {type: 'float32'},
frame: {type: 'uint16'}, frame: {type: 'uint16'},

View File

@ -0,0 +1,44 @@
import Schema from '@/ecs/schema.js';
export default function(Component) {
return class Ticking extends Component {
instanceFromSchema() {
const Instance = super.instanceFromSchema();
Instance.prototype.$$finished = [];
Instance.prototype.$$tickingPromises = [];
Instance.prototype.addTickingPromise = function(tickingPromise) {
this.$$tickingPromises.push(tickingPromise);
tickingPromise.then(() => {
this.$$finished.push(tickingPromise);
});
}
Instance.prototype.tick = function(elapsed) {
for (const tickingPromise of this.$$finished) {
this.$$tickingPromises.splice(
this.$$tickingPromises.indexOf(tickingPromise),
1,
);
}
this.$$finished = [];
for (const tickingPromise of this.$$tickingPromises) {
tickingPromise.tick(elapsed);
}
for (const tickingPromise of this.$$finished) {
this.$$tickingPromises.splice(
this.$$tickingPromises.indexOf(tickingPromise),
1,
);
}
this.$$finished = [];
}
return Instance;
}
static schema = new Schema({
type: 'object',
properties: {
isTicking: {defaultValue: 1, type: 'uint8'},
},
});
};
}

View File

@ -3,13 +3,9 @@ import {System} from '@/ecs/index.js';
export default class ApplyControlMovement extends System { export default class ApplyControlMovement extends System {
tick() { tick() {
const {diff} = this.ecs; for (const {Controlled, Momentum, Speed} of this.ecs.changed(['Controlled'])) {
for (const id in diff) { Momentum.x = Speed.speed * (Controlled.moveRight - Controlled.moveLeft);
if (diff[id].Controlled) { Momentum.y = Speed.speed * (Controlled.moveDown - Controlled.moveUp);
const {Controlled, Momentum, Speed} = this.ecs.get(id);
Momentum.x = Speed.speed * (Controlled.moveRight - Controlled.moveLeft);
Momentum.y = Speed.speed * (Controlled.moveDown - Controlled.moveUp);
}
} }
} }

View File

@ -3,16 +3,12 @@ import {System} from '@/ecs/index.js';
export default class CalculateAabbs extends System { export default class CalculateAabbs extends System {
tick() { tick() {
const {diff} = this.ecs; for (const {Position: {x, y}, VisibleAabb} of this.ecs.changed(['Position'])) {
for (const id in diff) { if (VisibleAabb) {
if (diff[id].Position) { VisibleAabb.x0 = x - 32;
const {Position: {x, y}, VisibleAabb} = this.ecs.get(id); VisibleAabb.x1 = x + 32;
if (VisibleAabb) { VisibleAabb.y0 = y - 32;
VisibleAabb.x0 = x - 32; VisibleAabb.y1 = y + 32;
VisibleAabb.x1 = x + 32;
VisibleAabb.y0 = y - 32;
VisibleAabb.y1 = y + 32;
}
} }
} }
} }

View File

@ -3,23 +3,19 @@ import {System} from '@/ecs/index.js';
export default class ClampPositions extends System { export default class ClampPositions extends System {
tick() { tick() {
const {diff} = this.ecs;
const {AreaSize} = this.ecs.get(1); const {AreaSize} = this.ecs.get(1);
for (const id in diff) { for (const {Position} of this.ecs.changed(['Position'])) {
if (diff[id].Position) { if (Position.x < 0) {
const {Position} = this.ecs.get(id); Position.x = 0;
if (Position.x < 0) { }
Position.x = 0; if (Position.y < 0) {
} Position.y = 0;
if (Position.y < 0) { }
Position.y = 0; if (Position.x >= AreaSize.x) {
} Position.x = AreaSize.x - 0.0001;
if (Position.x >= AreaSize.x) { }
Position.x = AreaSize.x - 0.0001; if (Position.y >= AreaSize.y) {
} Position.y = AreaSize.y - 0.0001;
if (Position.y >= AreaSize.y) {
Position.y = AreaSize.y - 0.0001;
}
} }
} }
} }

View File

@ -3,23 +3,19 @@ import {System} from '@/ecs/index.js';
export default class ControlDirection extends System { export default class ControlDirection extends System {
tick() { tick() {
const {diff} = this.ecs; for (const {Controlled, Direction} of this.ecs.changed(['Controlled'])) {
for (const id in diff) { const {moveUp, moveRight, moveDown, moveLeft} = Controlled;
const {Controlled} = diff[id]; if (moveUp > 0) {
if (Controlled) { Direction.direction = 0;
const {Controlled: {moveUp, moveRight, moveDown, moveLeft}, Direction} = this.ecs.get(id); }
if (moveUp > 0) { if (moveDown > 0) {
Direction.direction = 0; Direction.direction = 2;
} }
if (moveDown > 0) { if (moveLeft > 0) {
Direction.direction = 2; Direction.direction = 3;
} }
if (moveLeft > 0) { if (moveRight > 0) {
Direction.direction = 3; Direction.direction = 1;
}
if (moveRight > 0) {
Direction.direction = 1;
}
} }
} }
} }

View File

@ -11,11 +11,8 @@ export default class FollowCamera extends System {
} }
tick() { tick() {
const {diff} = this.ecs; for (const entity of this.ecs.changed(['Position'])) {
for (const id in diff) { this.updateCamera(entity);
if (diff[id].Position) {
this.updateCamera(this.ecs.get(id));
}
} }
} }

View File

@ -0,0 +1,19 @@
import {System} from '@/ecs/index.js';
export default class RunTickingPromises extends System {
static queries() {
return {
default: ['Ticking'],
};
}
tick(elapsed) {
for (const [Ticking] of this.select('default')) {
if (Ticking.isTicking) {
Ticking.tick(elapsed);
}
}
}
}

View File

@ -81,11 +81,8 @@ export default class UpdateSpatialHash extends System {
} }
tick() { tick() {
const {diff} = this.ecs; for (const entity of this.ecs.changed(['VisibleAabb'])) {
for (const id in diff) { this.updateHash(entity);
if (diff[id].VisibleAabb) {
this.updateHash(this.ecs.get(id));
}
} }
} }

View File

@ -1,3 +1,5 @@
import Schema from './schema.js';
export default class Base { export default class Base {
ecs; ecs;
@ -6,7 +8,10 @@ export default class Base {
pool = []; pool = [];
static schema; static schema = new Schema({
type: 'object',
properties: {},
});
constructor(ecs) { constructor(ecs) {
this.ecs = ecs; this.ecs = ecs;

View File

@ -67,6 +67,32 @@ export default class Ecs {
this.createManySpecific(creating); this.createManySpecific(creating);
} }
changed(criteria) {
const it = Object.entries(this.diff).values();
return {
[Symbol.iterator]() {
return this;
},
next: () => {
let result = it.next();
let satisfied = false;
while (!result.done) {
for (const componentName of criteria) {
if (!(componentName in result.value[1])) {
result = it.next();
continue;
}
}
break;
}
if (result.done) {
return {done: true, value: undefined};
}
return {done: false, value: this.get(result.value[0])};
},
};
}
create(components = {}) { create(components = {}) {
const [entityId] = this.createMany([components]); const [entityId] = this.createMany([components]);
return entityId; return entityId;
@ -356,7 +382,9 @@ export default class Ecs {
toJSON() { toJSON() {
const entities = {}; const entities = {};
for (const id in this.$$entities) { for (const id in this.$$entities) {
entities[id] = this.$$entities[id].toJSON(); if (this.$$entities[id]) {
entities[id] = this.$$entities[id].toJSON();
}
} }
const systems = []; const systems = [];
for (const systemName in this.Systems) { for (const systemName in this.Systems) {

View File

@ -293,7 +293,7 @@ test('generates diffs for adding and removing components', () => {
test('generates diffs for empty components', () => { test('generates diffs for empty components', () => {
const ecs = new Ecs({Components: {Empty}}); const ecs = new Ecs({Components: {Empty}});
let entity; let entity;
entity = ecs.create({Empty}); entity = ecs.create({Empty: {}});
expect(ecs.diff) expect(ecs.diff)
.to.deep.equal({[entity]: {Empty: {}}}); .to.deep.equal({[entity]: {Empty: {}}});
ecs.setClean(); ecs.setClean();

View File

@ -5,6 +5,7 @@ import Ecs from '@/ecs/ecs.js';
import Components from '@/ecs-components/index.js'; import Components from '@/ecs-components/index.js';
import Systems from '@/ecs-systems/index.js'; import Systems from '@/ecs-systems/index.js';
import {decode, encode} from '@/packets/index.js'; import {decode, encode} from '@/packets/index.js';
import Script from '@/util/script.js';
function join(...parts) { function join(...parts) {
return parts.join('/'); return parts.join('/');
@ -22,6 +23,8 @@ export default class Engine {
frame = 0; frame = 0;
handle;
last = Date.now(); last = Date.now();
server; server;
@ -90,6 +93,7 @@ export default class Engine {
'ControlDirection', 'ControlDirection',
'SpriteDirection', 'SpriteDirection',
'RunAnimations', 'RunAnimations',
'RunTickingPromises',
]; ];
defaultSystems.forEach((defaultSystem) => { defaultSystems.forEach((defaultSystem) => {
const System = ecs.system(defaultSystem); const System = ecs.system(defaultSystem);
@ -97,11 +101,7 @@ export default class Engine {
System.active = true; System.active = true;
} }
}); });
const view = Ecs.serialize(ecs); this.saveEcs(join('homesteads', `${id}`), ecs);
await this.server.writeData(
join('homesteads', `${id}`),
view,
);
} }
async createPlayer(id) { async createPlayer(id) {
@ -111,16 +111,28 @@ export default class Engine {
Direction: {direction: 2}, Direction: {direction: 2},
Ecs: {path: join('homesteads', `${id}`)}, Ecs: {path: join('homesteads', `${id}`)},
Momentum: {}, Momentum: {},
Inventory: {
slots: [
{
qty: 10,
slotIndex: 0,
source: '/assets/potion',
}
]
},
Health: {health: 100},
Position: {x: 368, y: 368}, Position: {x: 368, y: 368},
VisibleAabb: {}, VisibleAabb: {},
Speed: {speed: 100}, Speed: {speed: 100},
Sprite: { Sprite: {
anchor: {x: 0.5, y: 0.8},
animation: 'moving:down', animation: 'moving:down',
frame: 0, frame: 0,
frames: 8, frames: 8,
source: '/assets/dude.json', source: '/assets/dude.json',
speed: 0.115, speed: 0.115,
}, },
Ticking: {},
Wielder: { Wielder: {
activeSlot: 0, activeSlot: 0,
}, },
@ -167,6 +179,19 @@ export default class Engine {
return JSON.parse((new TextDecoder()).decode(buffer)); return JSON.parse((new TextDecoder()).decode(buffer));
} }
async saveEcs(path, ecs) {
const view = Ecs.serialize(ecs);
await this.server.writeData(path, view);
}
async saveEcses() {
const promises = []
for (const i in this.ecses) {
promises.push(this.saveEcs(i, this.ecses[i]));
}
await Promise.all(promises);
}
async savePlayer(id, entity) { async savePlayer(id, entity) {
const encoder = new TextEncoder(); const encoder = new TextEncoder();
const buffer = encoder.encode(JSON.stringify(entity.toJSON())); const buffer = encoder.encode(JSON.stringify(entity.toJSON()));
@ -174,7 +199,7 @@ export default class Engine {
} }
start() { start() {
return setInterval(() => { this.handle = setInterval(() => {
const elapsed = (Date.now() - this.last) / 1000; const elapsed = (Date.now() - this.last) / 1000;
this.last = Date.now(); this.last = Date.now();
this.tick(elapsed); this.tick(elapsed);
@ -183,12 +208,24 @@ export default class Engine {
}, 1000 / TPS); }, 1000 / TPS);
} }
stop() {
clearInterval(this.handle);
this.handle = undefined;
}
tick(elapsed) { tick(elapsed) {
for (const i in this.ecses) { for (const i in this.ecses) {
this.ecses[i].setClean(); this.ecses[i].setClean();
} }
for (const [{Controlled, Wielder}, payload] of this.incomingActions) { for (const [
entity,
payload,
] of this.incomingActions) {
const {Ecs, Controlled, id, Inventory, Position, Ticking, Wielder} = entity;
switch (payload.type) { switch (payload.type) {
case 'changeSlot': {
Wielder.activeSlot = payload.value - 1;
}
case 'moveUp': case 'moveUp':
case 'moveRight': case 'moveRight':
case 'moveDown': case 'moveDown':
@ -196,8 +233,17 @@ export default class Engine {
Controlled[payload.type] = payload.value; Controlled[payload.type] = payload.value;
break; break;
} }
case 'changeSlot': { case 'use': {
Wielder.activeSlot = payload.value - 1; if (payload.value) {
const item = Inventory.item(Wielder.activeSlot);
this.server.readAsset(item + '/start.js')
.then((response) => response.text())
.then((code) => {
Ticking.addTickingPromise(
Script.tickingPromise(code, {console, wielder: entity}),
);
});
}
} }
} }
} }

View File

@ -13,6 +13,9 @@ class WorkerServer extends Server {
static qualify(path) { static qualify(path) {
return ['UNIVERSE', path].join('/'); return ['UNIVERSE', path].join('/');
} }
async readAsset(path) {
return fetch(path);
}
async readData(path) { async readData(path) {
const data = await get(this.constructor.qualify(path)); const data = await get(this.constructor.qualify(path));
if ('undefined' !== typeof data) { if ('undefined' !== typeof data) {
@ -32,7 +35,9 @@ const engine = new Engine(WorkerServer);
onmessage = async (event) => { onmessage = async (event) => {
if (0 === event.data) { if (0 === event.data) {
engine.stop();
await engine.disconnectPlayer(0, 0); await engine.disconnectPlayer(0, 0);
await engine.saveEcses();
postMessage(0); postMessage(0);
return; return;
} }

View File

@ -14,7 +14,7 @@
.slotInner { .slotInner {
background-position: center; background-position: center;
background-repeat: no-repeat; background-repeat: no-repeat;
background-size: contain; background-size: 75%;
height: calc(100% - var(--space) * 2); height: calc(100% - var(--space) * 2);
padding: var(--space); padding: var(--space);
position: relative; position: relative;
@ -22,12 +22,12 @@
} }
.qty { .qty {
bottom: calc(var(--space) / -1.25); bottom: calc(var(--space) * 0.125);
font-family: monospace; font-family: monospace;
font-size: calc(var(--space) * 2); font-size: calc(var(--space) * 2);
line-height: 1; line-height: 1;
position: absolute; position: absolute;
right: calc(var(--space) / -1.25); right: calc(var(--space) * 0.25);
text-shadow: text-shadow:
0px -1px 0px white, 0px -1px 0px white,
1px 0px 0px white, 1px 0px 0px white,

View File

@ -17,7 +17,7 @@ export default function Sprite({entity}) {
} }
return ( return (
<PixiSprite <PixiSprite
anchor={0.5} anchor={entity.Sprite.anchor}
texture={texture} texture={texture}
x={Math.round(entity.Position.x)} x={Math.round(entity.Position.x)}
y={Math.round(entity.Position.y)} y={Math.round(entity.Position.y)}

View File

@ -19,6 +19,7 @@ export default function Ui({disconnected}) {
const client = useContext(ClientContext); const client = useContext(ClientContext);
const [mainEntity, setMainEntity] = useMainEntity(); const [mainEntity, setMainEntity] = useMainEntity();
const [showDisconnected, setShowDisconnected] = useState(false); const [showDisconnected, setShowDisconnected] = useState(false);
const [hotbarSlots, setHotbarSlots] = useState(Array(10).fill(0).map(() => {}));
const [activeSlot, setActiveSlot] = useState(0); const [activeSlot, setActiveSlot] = useState(0);
useEffect(() => { useEffect(() => {
let handle; let handle;
@ -141,12 +142,23 @@ export default function Ui({disconnected}) {
setMainEntity(localMainEntity = id); setMainEntity(localMainEntity = id);
} }
if (localMainEntity === id) { if (localMainEntity === id) {
if (payload.ecs[id].Inventory) {
const newHotbarSlots = [...hotbarSlots];
payload.ecs[id].Inventory.slots
.forEach(({qty, slotIndex, source}) => {
newHotbarSlots[slotIndex] = {
image: source + '/icon.png',
qty,
};
});
setHotbarSlots(newHotbarSlots);
}
if (payload.ecs[id].Wielder && 'activeSlot' in payload.ecs[id].Wielder) { if (payload.ecs[id].Wielder && 'activeSlot' in payload.ecs[id].Wielder) {
setActiveSlot(payload.ecs[id].Wielder.activeSlot); setActiveSlot(payload.ecs[id].Wielder.activeSlot);
} }
} }
} }
}, [mainEntity, setMainEntity]); }, [hotbarSlots, mainEntity, setMainEntity]);
return ( return (
<div className={styles.ui}> <div className={styles.ui}>
<style> <style>
@ -166,7 +178,7 @@ export default function Ui({disconnected}) {
payload: {type: 'changeSlot', value: i + 1}, payload: {type: 'changeSlot', value: i + 1},
}); });
}} }}
slots={Array(10).fill(0).map(() => {})} slots={hotbarSlots}
/> />
{showDisconnected && ( {showDisconnected && (
<Disconnected /> <Disconnected />

View File

@ -1,4 +1,5 @@
.ui { .ui {
align-self: center; align-self: center;
line-height: 0;
position: relative; position: relative;
} }

View File

@ -1,47 +0,0 @@
import {unwrap} from '@/swcx/types.js';
const evaluators = Object.fromEntries(
Object.entries(
import.meta.glob(
['./evaluators/*.js', '!./evaluators/*.test.js'],
{eager: true, import: 'default'},
)
)
.map(([path, evaluator]) => ([
path.replace(/\.\/evaluators\/(.*)\.js/, '$1'),
evaluator,
])),
);
export default function evaluate(node, {scope} = {}) {
const unwrapped = unwrap(node);
switch (unwrapped.type) {
case 'ArrayExpression':
return evaluators.array(unwrapped, {evaluate, scope});
case 'AssignmentExpression':
return evaluators.assignment(unwrapped, {evaluate, scope});
case 'AwaitExpression':
return evaluators.await(unwrapped, {evaluate, scope});
case 'BinaryExpression':
return evaluators.binary(unwrapped, {evaluate, scope});
case 'BooleanLiteral':
case 'NullLiteral':
case 'NumericLiteral':
case 'StringLiteral':
return evaluators.literal(unwrapped, {evaluate, scope});
case 'CallExpression':
return evaluators.call(unwrapped, {evaluate, scope});
case 'ConditionalExpression':
return evaluators.conditional(unwrapped, {evaluate, scope});
case 'Identifier':
return evaluators.identifier(unwrapped, {evaluate, scope});
case 'MemberExpression':
return evaluators.member(unwrapped, {evaluate, scope});
case 'ObjectExpression':
return evaluators.object(unwrapped, {evaluate, scope});
case 'UnaryExpression':
return evaluators.unary(unwrapped, {evaluate, scope});
case 'UpdateExpression':
return evaluators.update(unwrapped, {evaluate, scope});
}
}

View File

@ -1,5 +0,0 @@
export default async function(code) {
const {parse} = await import('@swc/core');
const ast = await parse(code);
return ast.body[0].expression;
}

149
app/util/script.js Normal file
View File

@ -0,0 +1,149 @@
import {parse as acornParse} from 'acorn';
import {LRUCache} from 'lru-cache';
import Sandbox from '@/astride/sandbox.js';
import TickingPromise from '@/util/ticking-promise.js';
function parse(code, options = {}) {
return acornParse(code, {
ecmaVersion: 'latest',
sourceType: 'module',
...options,
})
}
const Populated = Symbol.for('sandbox.populated');
export const cache = new LRUCache({
max: 128,
});
export default class Script {
constructor(sandbox) {
this.sandbox = sandbox;
this.promise = null;
}
get context() {
return this.sandbox.context;
}
static createContext(locals = {}) {
if (locals[Populated]) {
return locals;
}
return {
[Populated]: true,
...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 async fromCode(code, context = {}) {
let ast;
if (cache.has(code)) {
ast = cache.get(code);
}
else {
cache.set(code, ast = await this.parse(code));
}
return new this(
new Sandbox(ast, this.createContext(context)),
);
}
static async parse(code) {
return parse(
code,
{
allowReturnOutsideFunction: true,
},
);
}
reset() {
this.promise = null;
this.sandbox.compile();
}
tick(elapsed, resolve, reject) {
if (this.promise) {
if (this.promise instanceof TickingPromise) {
this.promise.tick(elapsed);
}
return;
}
while (true) {
this.sandbox.context.elapsed = elapsed;
const {done, value} = this.sandbox.step();
if (value) {
const {value: result} = value;
if (result instanceof Promise) {
this.promise = result;
result
.catch(reject)
.then(() => {
if (done) {
resolve();
}
})
.finally(() => {
this.promise = null;
});
break;
}
}
if (done) {
resolve();
break;
}
}
}
tickingPromise() {
return new TickingPromise(
() => {},
(elapsed, resolve, reject) => {
this.tick(elapsed, resolve, reject);
},
);
}
static tickingPromise(code, context = {}) {
let tickingPromise;
return new TickingPromise(
(resolve) => {
this.fromCode(code, context)
.then((script) => {
tickingPromise = script.tickingPromise();
resolve(tickingPromise);
})
},
(elapsed) => {
tickingPromise?.tick?.(elapsed);
},
);
}
};

View File

@ -0,0 +1,53 @@
export default class TickingPromise extends Promise {
constructor(executor, ticker) {
let _reject;
let _resolve;
super((resolve, reject) => {
_reject = reject;
_resolve = resolve;
if (executor) {
executor(resolve, reject);
}
});
this.reject = _reject;
this.resolve = _resolve;
this.ticker = ticker;
}
static all(promises) {
const tickingPromises = [];
for (let i = 0; i < promises.length; i++) {
const promise = promises[i];
if (promise instanceof TickingPromise) {
tickingPromises.push(promise);
// After resolution, stop ticking the promise.
promise.then(() => {
tickingPromises.splice(tickingPromises.indexOf(promise), 1);
});
}
}
/* v8 ignore next 3 */
if (0 === tickingPromises.length) {
return super.all(promises);
}
return new TickingPromise(
(resolve, reject) => {
super.all(promises)
.then(resolve)
/* v8 ignore next */
.catch(reject);
},
(elapsed) => {
for (let i = 0; i < tickingPromises.length; i++) {
tickingPromises[i].tick(elapsed);
}
},
);
}
tick(elapsed) {
this.ticker(elapsed, this.resolve, this.reject);
}
}

View File

@ -0,0 +1,100 @@
import {expect, test} from 'vitest';
import TickingPromise from './ticking-promise.js';
test('runs executor', async () => {
expect(
await new TickingPromise((resolve, reject) => {
resolve(32);
}),
)
.to.equal(32);
expect(
async () => {
await new TickingPromise((resolve, reject) => {
reject(new Error(''));
})
}
)
.rejects.toThrowError('');
});
test('ticks and resolves', async () => {
let done = false;
let e = 0;
const tp = new TickingPromise(undefined, (elapsed, resolve) => {
e += elapsed;
if (1 === e) {
done = true;
resolve(16);
}
});
expect(done)
.to.be.false;
tp.tick(0.25);
expect(done)
.to.be.false;
tp.tick(0.25);
expect(done)
.to.be.false;
tp.tick(0.25);
expect(done)
.to.be.false;
tp.tick(0.25);
expect(done)
.to.be.true;
expect(await tp)
.to.equal(16);
});
test('ticks and rejects', async () => {
let caught = false;
const tp = new TickingPromise(undefined, (elapsed, resolve, reject) => {
reject(new Error());
});
tp.catch(() => {
caught = true;
});
expect(caught)
.to.be.false;
tp.tick(0.25);
await Promise.resolve();
expect(caught)
.to.be.true;
});
test('handles all', async () => {
let done = 0;
let e1 = 0, e2 = 0;
const tp1 = new TickingPromise(undefined, (elapsed, resolve) => {
e1 += elapsed;
if (1 === e1) {
done += 1;
resolve(16);
}
});
const tp2 = new TickingPromise(undefined, (elapsed, resolve) => {
e2 += elapsed;
if (2 === e2) {
done += 1;
resolve(32);
}
});
const tpa = TickingPromise.all([
Promise.resolve(8),
tp1,
tp2,
]);
expect(done)
.to.equal(0);
while (2 !== done) {
tpa.tick(0.25);
await Promise.resolve();
}
expect(e1)
.to.equal(1);
expect(e2)
.to.equal(2);
expect(await tpa)
.to.deep.equal([8, 16, 32]);
});

251
package-lock.json generated
View File

@ -13,11 +13,12 @@
"@remix-run/express": "^2.9.2", "@remix-run/express": "^2.9.2",
"@remix-run/node": "^2.9.2", "@remix-run/node": "^2.9.2",
"@remix-run/react": "^2.9.2", "@remix-run/react": "^2.9.2",
"@swc/core": "^1.6.0", "acorn": "^8.12.0",
"compression": "^1.7.4", "compression": "^1.7.4",
"express": "^4.18.2", "express": "^4.18.2",
"idb-keyval": "^6.2.1", "idb-keyval": "^6.2.1",
"isbot": "^4.1.0", "isbot": "^4.1.0",
"lru-cache": "^10.2.2",
"morgan": "^1.10.0", "morgan": "^1.10.0",
"pixi.js": "^7.4.2", "pixi.js": "^7.4.2",
"react": "^18.2.0", "react": "^18.2.0",
@ -212,6 +213,15 @@
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
"integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
"dev": true,
"dependencies": {
"yallist": "^3.0.2"
}
},
"node_modules/@babel/helper-compilation-targets/node_modules/semver": { "node_modules/@babel/helper-compilation-targets/node_modules/semver": {
"version": "6.3.1", "version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
@ -4004,9 +4014,9 @@
} }
}, },
"node_modules/@remix-run/dev/node_modules/ws": { "node_modules/@remix-run/dev/node_modules/ws": {
"version": "7.5.9", "version": "7.5.10",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz",
"integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==",
"dev": true, "dev": true,
"engines": { "engines": {
"node": ">=8.3.0" "node": ">=8.3.0"
@ -6429,206 +6439,6 @@
"url": "https://opencollective.com/storybook" "url": "https://opencollective.com/storybook"
} }
}, },
"node_modules/@swc/core": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.6.0.tgz",
"integrity": "sha512-Wynbo79uIVBgmq3TPcTMdtXUkqk69IPSVuzo7/Jl1OhR4msC7cUaoRB1216ZanWttrAZ4/g6u17w9XZG4fzp1A==",
"hasInstallScript": true,
"dependencies": {
"@swc/counter": "^0.1.3",
"@swc/types": "^0.1.8"
},
"engines": {
"node": ">=10"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/swc"
},
"optionalDependencies": {
"@swc/core-darwin-arm64": "1.6.0",
"@swc/core-darwin-x64": "1.6.0",
"@swc/core-linux-arm-gnueabihf": "1.6.0",
"@swc/core-linux-arm64-gnu": "1.6.0",
"@swc/core-linux-arm64-musl": "1.6.0",
"@swc/core-linux-x64-gnu": "1.6.0",
"@swc/core-linux-x64-musl": "1.6.0",
"@swc/core-win32-arm64-msvc": "1.6.0",
"@swc/core-win32-ia32-msvc": "1.6.0",
"@swc/core-win32-x64-msvc": "1.6.0"
},
"peerDependencies": {
"@swc/helpers": "*"
},
"peerDependenciesMeta": {
"@swc/helpers": {
"optional": true
}
}
},
"node_modules/@swc/core-darwin-arm64": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.6.0.tgz",
"integrity": "sha512-W1Mwk0WRrJ5lAVkYRPxpxOmwu8p9ASXeOmiORhXvE7DYREyI30005xlqSOITU1pfSNKj7G9u3+9DjsOzPPPbBw==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-darwin-x64": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.6.0.tgz",
"integrity": "sha512-EzxLnpPC1zgLb2Y0iVUG6b+/GUv43k6uJUIs52UzxOnBElYP/WeItI3RJ+LUMFzCpZMk/IxB10wofEoeQ1H/Xg==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-linux-arm-gnueabihf": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.6.0.tgz",
"integrity": "sha512-uP/STDjWZ5N6lc8mxJFsex4NXDaqhfzd8UOrI3LfdV97+4faE4/BC6bVqDNHFFzZi0PHuVBxD6md7IfPjugk6A==",
"cpu": [
"arm"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-linux-arm64-gnu": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.6.0.tgz",
"integrity": "sha512-UgNz6anowcnYzJtZohzpii31FOgouBHJqluiq+p2geX/agbC+KfOKwVXdljn95+Qc4ygBuw/hjKjgF2msOLeVg==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-linux-arm64-musl": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.6.0.tgz",
"integrity": "sha512-xPV6qrnj4nFwXQbIv70C1Kn5z5Th53sirIY76aEonr78qeC6+ywaBZR4uLFNHsljVjyuvVQfTTcl2qraGhu6oQ==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-linux-x64-gnu": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.6.0.tgz",
"integrity": "sha512-xTeWn4OT5uQ+DxT2cy94ngK8tF1U/5fMC49/V6FhCS2Wh+Xa/O+OWcOyKvYtk3b0eGYS4iNIRKgzog7fLSFtvQ==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-linux-x64-musl": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.6.0.tgz",
"integrity": "sha512-3P01mYD5XbyaVLT0MGZmZE+ZdgmGSvuvIhSejRDBlEXqkFnH79nWds+KsE+91hzVU8XsgzX57Yzv4eO5dlIuPw==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-win32-arm64-msvc": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.6.0.tgz",
"integrity": "sha512-xFuook1efU0ctzMAEeol4eI7J6+k/c/pMJpp/NP/4JJDnhlHwAi2iyiZcID8YZS+ePHgXMLndGdIMHVv/wIPkQ==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-win32-ia32-msvc": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.6.0.tgz",
"integrity": "sha512-VCJa5vTywxzASqvf9OEUM5SZBcNrWbuIkSGM5T9guuBzyrh/tSqVHjzOWL9qpP69uPVj5G/I5bJObLiUKErhvQ==",
"cpu": [
"ia32"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-win32-x64-msvc": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.6.0.tgz",
"integrity": "sha512-L7i8WBSIJTQiMONJGHnznDydZmlJIqHjZ3VhBHeTTms8cEAuwkAVgzPwgr5cD9GhmcwdeBI9iYdOuKr1pUx19Q==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/counter": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
"integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ=="
},
"node_modules/@swc/types": {
"version": "0.1.8",
"resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.8.tgz",
"integrity": "sha512-RNFA3+7OJFNYY78x0FYwi1Ow+iF1eF5WvmfY1nXPOEH4R2p/D4Cr1vzje7dNAI2aLFqpv8Wyz4oKSWqIZArpQA==",
"dependencies": {
"@swc/counter": "^0.1.3"
}
},
"node_modules/@testing-library/dom": { "node_modules/@testing-library/dom": {
"version": "9.3.4", "version": "9.3.4",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.4.tgz", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.4.tgz",
@ -7495,10 +7305,9 @@
} }
}, },
"node_modules/acorn": { "node_modules/acorn": {
"version": "8.11.3", "version": "8.12.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.0.tgz",
"integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", "integrity": "sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw==",
"dev": true,
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
}, },
@ -12674,12 +12483,11 @@
} }
}, },
"node_modules/lru-cache": { "node_modules/lru-cache": {
"version": "5.1.1", "version": "10.2.2",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz",
"integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==",
"dev": true, "engines": {
"dependencies": { "node": "14 || >=16.14"
"yallist": "^3.0.2"
} }
}, },
"node_modules/lz-string": { "node_modules/lz-string": {
@ -14714,15 +14522,6 @@
"url": "https://github.com/sponsors/isaacs" "url": "https://github.com/sponsors/isaacs"
} }
}, },
"node_modules/path-scurry/node_modules/lru-cache": {
"version": "10.2.2",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz",
"integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==",
"dev": true,
"engines": {
"node": "14 || >=16.14"
}
},
"node_modules/path-to-regexp": { "node_modules/path-to-regexp": {
"version": "0.1.7", "version": "0.1.7",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
@ -19243,9 +19042,9 @@
} }
}, },
"node_modules/ws": { "node_modules/ws": {
"version": "8.17.0", "version": "8.17.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz", "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
"integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==", "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
"engines": { "engines": {
"node": ">=10.0.0" "node": ">=10.0.0"
}, },

View File

@ -20,11 +20,12 @@
"@remix-run/express": "^2.9.2", "@remix-run/express": "^2.9.2",
"@remix-run/node": "^2.9.2", "@remix-run/node": "^2.9.2",
"@remix-run/react": "^2.9.2", "@remix-run/react": "^2.9.2",
"@swc/core": "^1.6.0", "acorn": "^8.12.0",
"compression": "^1.7.4", "compression": "^1.7.4",
"express": "^4.18.2", "express": "^4.18.2",
"idb-keyval": "^6.2.1", "idb-keyval": "^6.2.1",
"isbot": "^4.1.0", "isbot": "^4.1.0",
"lru-cache": "^10.2.2",
"morgan": "^1.10.0", "morgan": "^1.10.0",
"pixi.js": "^7.4.2", "pixi.js": "^7.4.2",
"react": "^18.2.0", "react": "^18.2.0",

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@ -0,0 +1,7 @@
wielder.Health.health += 10
wielder.Inventory.slots = [
{
...wielder.Inventory.slots[0],
qty: wielder.Inventory.slots[0].qty - 1,
}
]