Compare commits
8 Commits
3eb94f2ef8
...
5fe346372b
Author | SHA1 | Date | |
---|---|---|---|
![]() |
5fe346372b | ||
![]() |
0439c52076 | ||
![]() |
557c8285ba | ||
![]() |
b9985a573d | ||
![]() |
ce802a8499 | ||
![]() |
443797017f | ||
![]() |
62eaa16b28 | ||
![]() |
14effd2455 |
|
@ -35,6 +35,8 @@ export default function evaluate(node, {scope} = {}) {
|
|||
return evaluators.binary(node, {evaluate, scope});
|
||||
case 'MemberExpression':
|
||||
return evaluators.member(node, {evaluate, scope});
|
||||
case 'NewExpression':
|
||||
return evaluators.new(node, {evaluate, scope});
|
||||
case 'ObjectExpression':
|
||||
return evaluators.object(node, {evaluate, scope});
|
||||
case 'UnaryExpression':
|
||||
|
|
21
app/astride/evaluators/new.js
Normal file
21
app/astride/evaluators/new.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
export default function(node, {evaluate, scope}) {
|
||||
let asyncArgs = false;
|
||||
const args = [];
|
||||
for (let i = 0; i < node.arguments.length; i++) {
|
||||
const arg = node.arguments[i];
|
||||
const {async, value} = evaluate(arg, {scope});
|
||||
asyncArgs ||= async;
|
||||
args.push(value);
|
||||
}
|
||||
const {callee} = node;
|
||||
const {async, value} = evaluate(callee, {scope});
|
||||
if (asyncArgs || async) {
|
||||
return {
|
||||
async: true,
|
||||
value: Promise
|
||||
.all([value, Promise.all(args)])
|
||||
.then(([callee, args]) => new callee(...args)),
|
||||
};
|
||||
}
|
||||
return {value: new value(...args)};
|
||||
}
|
48
app/astride/evaluators/new.test.js
Normal file
48
app/astride/evaluators/new.test.js
Normal file
|
@ -0,0 +1,48 @@
|
|||
import {expect, test} from 'vitest';
|
||||
|
||||
import evaluate from '@/astride/evaluate.js';
|
||||
import expression from '@/astride/test/expression.js';
|
||||
|
||||
const scopeTest = test.extend({
|
||||
scope: async ({}, use) => {
|
||||
await use({
|
||||
S: {O: {}},
|
||||
get(k) { return this.S[k]; },
|
||||
set(k, v) { return this.S[k] = v; }
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
class C {
|
||||
foo = 'bar';
|
||||
constructor(a, b) {
|
||||
this.a = a;
|
||||
this.b = b;
|
||||
}
|
||||
}
|
||||
|
||||
scopeTest('creates instances', async ({scope}) => {
|
||||
scope.set('C', C);
|
||||
const evaluated = evaluate(await expression('new C(1, 2)'), {scope});
|
||||
expect(evaluated.value)
|
||||
.to.deep.include({
|
||||
a: 1,
|
||||
b: 2,
|
||||
foo: 'bar',
|
||||
});
|
||||
});
|
||||
|
||||
scopeTest('creates instances with async dependencies', async ({scope}) => {
|
||||
scope.set('C', C);
|
||||
scope.set('a', Promise.resolve(1));
|
||||
scope.set('b', Promise.resolve(2));
|
||||
const evaluated = evaluate(await expression('new C(await a, await b)'), {scope});
|
||||
expect(evaluated.async)
|
||||
.to.equal(true);
|
||||
expect(await evaluated.value)
|
||||
.to.deep.include({
|
||||
a: 1,
|
||||
b: 2,
|
||||
foo: 'bar',
|
||||
});
|
||||
});
|
|
@ -150,7 +150,7 @@ export default class Sandbox {
|
|||
case 'ObjectExpression':
|
||||
case 'Identifier':
|
||||
case 'MemberExpression':
|
||||
case 'UnaryExpression':
|
||||
case 'NewExpression':
|
||||
case 'UpdateExpression': {
|
||||
result = this.evaluateToResult(node);
|
||||
if (result.yield) {
|
||||
|
@ -523,6 +523,31 @@ export default class Sandbox {
|
|||
};
|
||||
break;
|
||||
}
|
||||
case 'UnaryExpression': {
|
||||
if ('delete' === node.operator) {
|
||||
let property;
|
||||
if (node.argument.computed) {
|
||||
property = this.executeSync(node.argument.property, depth + 1);
|
||||
if (property.yield) {
|
||||
return property;
|
||||
}
|
||||
}
|
||||
else {
|
||||
property = {value: node.argument.property.name, yield: YIELD_NONE};
|
||||
}
|
||||
const scope = this.scopes.get(node);
|
||||
const object = scope.get(node.argument.object.name, undefined);
|
||||
delete object[property.value];
|
||||
result = {value: true, yield: YIELD_NONE};
|
||||
}
|
||||
else {
|
||||
result = this.evaluateToResult(node);
|
||||
if (result.yield) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'VariableDeclaration': {
|
||||
let skipping = isReplaying;
|
||||
for (const child of node.declarations) {
|
||||
|
|
|
@ -121,6 +121,69 @@ test('runs arbitrary number of ops', async () => {
|
|||
.to.equal(150);
|
||||
});
|
||||
|
||||
test('instantiates', async () => {
|
||||
const sandbox = new Sandbox(
|
||||
await parse(`
|
||||
const x = new C(1, 2);
|
||||
const y = new C(await a, await b);
|
||||
`),
|
||||
{
|
||||
a: Promise.resolve(1),
|
||||
b: Promise.resolve(2),
|
||||
C: class {
|
||||
foo = 'bar';
|
||||
constructor(a, b) {
|
||||
this.a = a;
|
||||
this.b = b;
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
await finish(sandbox);
|
||||
expect(sandbox.context.x)
|
||||
.to.deep.include({
|
||||
a: 1,
|
||||
b: 2,
|
||||
foo: 'bar',
|
||||
});
|
||||
expect(sandbox.context.y)
|
||||
.to.deep.include({
|
||||
a: 1,
|
||||
b: 2,
|
||||
foo: 'bar',
|
||||
});
|
||||
});
|
||||
|
||||
test('deletes', async () => {
|
||||
const sandbox = new Sandbox(
|
||||
await parse(`
|
||||
delete foo.one;
|
||||
delete foo['two'];
|
||||
const x = 'three';
|
||||
delete foo[x];
|
||||
const y = 'four';
|
||||
delete foo[await y];
|
||||
`),
|
||||
{
|
||||
foo: {
|
||||
one: 1,
|
||||
two: 2,
|
||||
three: 3,
|
||||
four: 4,
|
||||
},
|
||||
},
|
||||
);
|
||||
await finish(sandbox);
|
||||
expect(sandbox.context.foo.one)
|
||||
.to.be.undefined;
|
||||
expect(sandbox.context.foo.two)
|
||||
.to.be.undefined;
|
||||
expect(sandbox.context.foo.three)
|
||||
.to.be.undefined;
|
||||
expect(sandbox.context.foo.four)
|
||||
.to.be.undefined;
|
||||
});
|
||||
|
||||
test('evaluates conditional branches', async () => {
|
||||
const sandbox = new Sandbox(
|
||||
await parse(`
|
||||
|
|
|
@ -16,6 +16,7 @@ export const TRAVERSAL_PATH = {
|
|||
Identifier: [],
|
||||
IfStatement: ['alternate', 'consequent', 'test'],
|
||||
MemberExpression: ['object', 'property'],
|
||||
NewExpression: ['arguments', 'callee'],
|
||||
Literal: [],
|
||||
LogicalExpression: ['left', 'right'],
|
||||
ObjectExpression: ['properties'],
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import Component from '@/ecs/component.js';
|
||||
import {distance, intersects} from '@/util/math.js';
|
||||
import {distance, intersects, transform} from '@/util/math.js';
|
||||
|
||||
import vector2d from './helpers/vector-2d';
|
||||
|
||||
|
@ -7,6 +7,8 @@ export default class Collider extends Component {
|
|||
instanceFromSchema() {
|
||||
const {ecs} = this;
|
||||
return class ColliderInstance extends super.instanceFromSchema() {
|
||||
$$aabb = {x0: Infinity, x1: -Infinity, y0: Infinity, y1: -Infinity};
|
||||
$$aabbs = [];
|
||||
collidingWith = {};
|
||||
get aabb() {
|
||||
const {Position: {x: px, y: py}} = ecs.get(this.entity);
|
||||
|
@ -76,34 +78,36 @@ export default class Collider extends Component {
|
|||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
async load(instance) {
|
||||
instance.$$aabb = {x0: Infinity, x1: -Infinity, y0: Infinity, y1: -Infinity};
|
||||
instance.$$aabbs = [];
|
||||
const {bodies} = instance;
|
||||
updateAabbs() {
|
||||
this.$$aabb = {x0: Infinity, x1: -Infinity, y0: Infinity, y1: -Infinity};
|
||||
this.$$aabbs = [];
|
||||
const {bodies} = this;
|
||||
const {Direction: {direction = 0} = {}} = ecs.get(this.entity);
|
||||
for (const body of bodies) {
|
||||
let x0 = Infinity, x1 = -Infinity, y0 = Infinity, y1 = -Infinity;
|
||||
for (const point of body.points) {
|
||||
const x = point.x;
|
||||
const y = point.y;
|
||||
for (const point of transform(body.points, {rotation: direction})) {
|
||||
const {x, y} = point;
|
||||
if (x < x0) x0 = x;
|
||||
if (x < instance.$$aabb.x0) instance.$$aabb.x0 = x;
|
||||
if (x < this.$$aabb.x0) this.$$aabb.x0 = x;
|
||||
if (x > x1) x1 = x;
|
||||
if (x > instance.$$aabb.x1) instance.$$aabb.x1 = x;
|
||||
if (x > this.$$aabb.x1) this.$$aabb.x1 = x;
|
||||
if (y < y0) y0 = y;
|
||||
if (y < instance.$$aabb.y0) instance.$$aabb.y0 = y;
|
||||
if (y < this.$$aabb.y0) this.$$aabb.y0 = y;
|
||||
if (y > y1) y1 = y;
|
||||
if (y > instance.$$aabb.y1) instance.$$aabb.y1 = y;
|
||||
if (y > this.$$aabb.y1) this.$$aabb.y1 = y;
|
||||
}
|
||||
instance.$$aabbs.push({
|
||||
this.$$aabbs.push({
|
||||
x0: x0 > x1 ? x1 : x0,
|
||||
x1: x0 > x1 ? x0 : x1,
|
||||
y0: y0 > y1 ? y1 : y0,
|
||||
y1: y0 > y1 ? y0 : y1,
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
async load(instance) {
|
||||
instance.updateAabbs();
|
||||
// heavy handed...
|
||||
if ('undefined' !== typeof window) {
|
||||
return;
|
||||
|
|
|
@ -11,6 +11,9 @@ export default class Sprite extends Component {
|
|||
return super.animation;
|
||||
}
|
||||
set animation(animation) {
|
||||
if (this.$$animation === animation) {
|
||||
return;
|
||||
}
|
||||
super.animation = animation;
|
||||
// eslint-disable-next-line no-self-assign
|
||||
this.frame = this.frame;
|
||||
|
@ -31,6 +34,27 @@ export default class Sprite extends Component {
|
|||
}
|
||||
return this.$$sourceJson.animations[this.animation].length;
|
||||
}
|
||||
hasAnimation(animation) {
|
||||
if (
|
||||
!this.$$sourceJson.animations
|
||||
|| !(animation in this.$$sourceJson.animations)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
get rotates() {
|
||||
if (!this.$$sourceJson.meta) {
|
||||
return false;
|
||||
}
|
||||
return 'rotation' in this.$$sourceJson.meta;
|
||||
}
|
||||
get rotation() {
|
||||
if (!this.$$sourceJson.meta) {
|
||||
return 0;
|
||||
}
|
||||
return this.$$sourceJson.meta.rotation;
|
||||
}
|
||||
get scale() {
|
||||
return {x: this.scaleX, y: this.scaleY};
|
||||
}
|
||||
|
@ -51,6 +75,7 @@ export default class Sprite extends Component {
|
|||
super.markChange(entityId, key, value);
|
||||
}
|
||||
static properties = {
|
||||
alpha: {defaultValue: 1, type: 'float32'},
|
||||
anchorX: {defaultValue: 0.5, type: 'float32'},
|
||||
anchorY: {defaultValue: 0.5, type: 'float32'},
|
||||
animation: {type: 'string'},
|
||||
|
|
|
@ -8,7 +8,7 @@ export default class Wielder extends Component {
|
|||
const {Inventory, Wielder} = ecs.get(this.entity);
|
||||
return Inventory.item(Wielder.activeSlot + 1);
|
||||
}
|
||||
useActiveItem(state) {
|
||||
useActiveItem([state, where]) {
|
||||
const entity = ecs.get(this.entity);
|
||||
const {Ticking} = entity;
|
||||
const activeItem = this.activeItem();
|
||||
|
@ -19,6 +19,7 @@ export default class Wielder extends Component {
|
|||
script = script.clone();
|
||||
script.context.ecs = ecs;
|
||||
script.context.item = activeItem;
|
||||
script.context.where = where;
|
||||
script.context.wielder = entity;
|
||||
Ticking.add(script.ticker());
|
||||
}
|
||||
|
|
|
@ -242,28 +242,17 @@ export default class Ecs {
|
|||
this.Components[i].destroyMany(destroying[i]);
|
||||
}
|
||||
for (const entityId of entityIds) {
|
||||
this.$$entities[entityId] = undefined;
|
||||
delete this.$$entities[entityId];
|
||||
this.diff[entityId] = false;
|
||||
}
|
||||
}
|
||||
|
||||
get entities() {
|
||||
const it = Object.values(this.$$entities).values();
|
||||
return {
|
||||
[Symbol.iterator]() {
|
||||
return this;
|
||||
},
|
||||
next: () => {
|
||||
let result = it.next();
|
||||
while (!result.done && !result.value) {
|
||||
result = it.next();
|
||||
const ids = [];
|
||||
for (const entity of Object.values(this.$$entities)) {
|
||||
ids.push(entity.id);
|
||||
}
|
||||
if (result.done) {
|
||||
return {done: true, value: undefined};
|
||||
}
|
||||
return {done: false, value: result.value.id};
|
||||
},
|
||||
};
|
||||
return ids;
|
||||
}
|
||||
|
||||
get(entityId) {
|
||||
|
|
|
@ -40,7 +40,16 @@ export default class Colliders extends System {
|
|||
|
||||
tick() {
|
||||
const collisions = new Map();
|
||||
for (const entity of this.ecs.changed(['Direction'])) {
|
||||
if (!entity.Collider) {
|
||||
continue;
|
||||
}
|
||||
entity.Collider.updateAabbs();
|
||||
}
|
||||
for (const entity of this.ecs.changed(['Position'])) {
|
||||
if (!entity.Collider) {
|
||||
continue;
|
||||
}
|
||||
this.updateHash(entity);
|
||||
}
|
||||
for (const entity of this.ecs.changed(['Position'])) {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import {System} from '@/ecs/index.js';
|
||||
import {HALF_PI} from '@/util/math.js';
|
||||
import {TAU} from '@/util/math.js';
|
||||
|
||||
export default class ControlDirection extends System {
|
||||
|
||||
|
@ -9,18 +9,20 @@ export default class ControlDirection extends System {
|
|||
if (locked) {
|
||||
continue;
|
||||
}
|
||||
if (moveUp > 0) {
|
||||
Direction.direction = HALF_PI * 3;
|
||||
}
|
||||
if (moveDown > 0) {
|
||||
Direction.direction = HALF_PI * 1;
|
||||
}
|
||||
if (moveLeft > 0) {
|
||||
Direction.direction = HALF_PI * 2;
|
||||
}
|
||||
if (moveRight > 0) {
|
||||
Direction.direction = HALF_PI * 0;
|
||||
if (
|
||||
0 === moveRight
|
||||
&& 0 === moveDown
|
||||
&& 0 === moveLeft
|
||||
&& 0 === moveUp
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
Direction.direction = (
|
||||
TAU + Math.atan2(
|
||||
moveDown - moveUp,
|
||||
moveRight - moveLeft,
|
||||
)
|
||||
) % TAU;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ export default class SpriteDirection extends System {
|
|||
}
|
||||
}
|
||||
if (Direction) {
|
||||
if (!Sprite.rotates) {
|
||||
const name = {
|
||||
0: 'right',
|
||||
1: 'down',
|
||||
|
@ -32,10 +33,13 @@ export default class SpriteDirection extends System {
|
|||
};
|
||||
parts.push(name[Direction.quantize(4)]);
|
||||
}
|
||||
}
|
||||
if (parts.length > 0) {
|
||||
if (Sprite.hasAnimation(parts.join(':'))) {
|
||||
Sprite.animation = parts.join(':');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -11,6 +11,9 @@ function textureFromAsset(asset, animation, frame) {
|
|||
}
|
||||
let texture;
|
||||
if (asset.data.animations) {
|
||||
if (!animation) {
|
||||
return undefined;
|
||||
}
|
||||
texture = asset.animations[animation][frame];
|
||||
}
|
||||
else {
|
||||
|
@ -23,7 +26,7 @@ export default function Sprite({entity, ...rest}) {
|
|||
const [mounted, setMounted] = useState();
|
||||
const [normals, setNormals] = useState();
|
||||
const [normalsMounted, setNormalsMounted] = useState();
|
||||
const {anchor, animation, frame, scale, source} = entity.Sprite;
|
||||
const {alpha, anchor, animation, frame, scale, rotates, rotation, source} = entity.Sprite;
|
||||
const asset = useAsset(source);
|
||||
const normalsAsset = useAsset(normals);
|
||||
useEffect(() => {
|
||||
|
@ -59,8 +62,10 @@ export default function Sprite({entity, ...rest}) {
|
|||
<>
|
||||
{texture && (
|
||||
<PixiSprite
|
||||
alpha={alpha}
|
||||
anchor={anchor}
|
||||
ref={setMounted}
|
||||
{...(rotates ? {rotation: entity.Direction.direction + rotation} : {})}
|
||||
scale={scale}
|
||||
texture={texture}
|
||||
x={Math.round(entity.Position.x)}
|
||||
|
@ -70,8 +75,10 @@ export default function Sprite({entity, ...rest}) {
|
|||
)}
|
||||
{normalsTexture && (
|
||||
<PixiSprite
|
||||
alpha={alpha}
|
||||
anchor={anchor}
|
||||
ref={setNormalsMounted}
|
||||
{...(rotates ? {rotation: entity.Direction.direction + rotation} : {})}
|
||||
scale={scale}
|
||||
texture={normalsTexture}
|
||||
x={Math.round(entity.Position.x)}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import {memo, useEffect, useRef, useState} from 'react';
|
||||
import {memo, useCallback, useEffect, useRef, useState} from 'react';
|
||||
|
||||
import {useClient, usePacket} from '@/react/context/client.js';
|
||||
import {useDebug} from '@/react/context/debug.js';
|
||||
|
@ -166,10 +166,6 @@ function Ui({disconnected}) {
|
|||
}
|
||||
break;
|
||||
}
|
||||
case ' ': {
|
||||
actionPayload = {type: 'use', value: KEY_MAP[type]};
|
||||
break;
|
||||
}
|
||||
case 'Tab': {
|
||||
if ('keyDown' === type) {
|
||||
if (isInventoryOpen) {
|
||||
|
@ -390,6 +386,9 @@ function Ui({disconnected}) {
|
|||
if (!update) {
|
||||
continue;
|
||||
}
|
||||
if (update.Direction && entity.Collider) {
|
||||
entity.Collider.updateAabbs();
|
||||
}
|
||||
if (update.Sound?.play) {
|
||||
for (const sound of update.Sound.play) {
|
||||
(new Audio(sound)).play();
|
||||
|
@ -430,6 +429,30 @@ function Ui({disconnected}) {
|
|||
document.body.removeEventListener('contextmenu', onContextMenu);
|
||||
};
|
||||
}, []);
|
||||
const computePosition = useCallback(({clientX, clientY}) => {
|
||||
if (!gameRef.current || !mainEntity) {
|
||||
return;
|
||||
}
|
||||
const {top, left, width} = gameRef.current.getBoundingClientRect();
|
||||
const master = ecs.get(1);
|
||||
if (!master) {
|
||||
return;
|
||||
}
|
||||
const {Camera} = ecs.get(mainEntity);
|
||||
const size = width / RESOLUTION.x;
|
||||
const camera = {
|
||||
x: ((Camera.x * scale) - (RESOLUTION.x / 2)),
|
||||
y: ((Camera.y * scale) - (RESOLUTION.y / 2)),
|
||||
}
|
||||
return {
|
||||
x: (((clientX - left) / size) + camera.x) / scale,
|
||||
y: (((clientY - top) / size) + camera.y) / scale,
|
||||
};
|
||||
}, [
|
||||
ecs,
|
||||
mainEntity,
|
||||
scale,
|
||||
]);
|
||||
return (
|
||||
<div
|
||||
className={styles.ui}
|
||||
|
@ -455,39 +478,19 @@ function Ui({disconnected}) {
|
|||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
const where = computePosition(event);
|
||||
switch (event.button) {
|
||||
case 0:
|
||||
if (devtoolsIsOpen) {
|
||||
if (!gameRef.current || !mainEntity) {
|
||||
return;
|
||||
}
|
||||
const {top, left, width} = gameRef.current.getBoundingClientRect();
|
||||
const master = ecs.get(1);
|
||||
if (!master) {
|
||||
return;
|
||||
}
|
||||
const {Camera} = ecs.get(mainEntity);
|
||||
const size = width / RESOLUTION.x;
|
||||
const client = {
|
||||
x: (event.clientX - left) / size,
|
||||
y: (event.clientY - top) / size,
|
||||
};
|
||||
const camera = {
|
||||
x: ((Camera.x * scale) - (RESOLUTION.x / 2)),
|
||||
y: ((Camera.y * scale) - (RESOLUTION.y / 2)),
|
||||
}
|
||||
devEventsChannel.invoke(
|
||||
'click',
|
||||
{
|
||||
x: (client.x + camera.x) / scale,
|
||||
y: (client.y + camera.y) / scale,
|
||||
},
|
||||
where,
|
||||
);
|
||||
}
|
||||
else {
|
||||
client.send({
|
||||
type: 'Action',
|
||||
payload: {type: 'use', value: 1},
|
||||
payload: {type: 'use', value: [1, where]},
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
@ -508,11 +511,12 @@ function Ui({disconnected}) {
|
|||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
const where = computePosition(event);
|
||||
switch (event.button) {
|
||||
case 0:
|
||||
client.send({
|
||||
type: 'Action',
|
||||
payload: {type: 'use', value: 0},
|
||||
payload: {type: 'use', value: [0, where]},
|
||||
});
|
||||
break;
|
||||
case 2:
|
||||
|
|
|
@ -88,6 +88,26 @@ export default async function createHomestead(id) {
|
|||
`,
|
||||
},
|
||||
Interlocutor: {},
|
||||
Inventory: {
|
||||
slots: {
|
||||
2: {
|
||||
qty: 1,
|
||||
source: '/assets/watering-can/watering-can.json',
|
||||
},
|
||||
3: {
|
||||
qty: 1,
|
||||
source: '/assets/tomato-seeds/tomato-seeds.json',
|
||||
},
|
||||
4: {
|
||||
qty: 1,
|
||||
source: '/assets/hoe/hoe.json',
|
||||
},
|
||||
5: {
|
||||
qty: 1,
|
||||
source: '/assets/brush/brush.json',
|
||||
},
|
||||
},
|
||||
},
|
||||
Position: {x: 200, y: 200},
|
||||
Sprite: {
|
||||
anchorX: 0.5,
|
||||
|
|
|
@ -26,22 +26,6 @@ export default async function createPlayer(id) {
|
|||
qty: 100,
|
||||
source: '/assets/potion/potion.json',
|
||||
},
|
||||
2: {
|
||||
qty: 1,
|
||||
source: '/assets/watering-can/watering-can.json',
|
||||
},
|
||||
3: {
|
||||
qty: 1,
|
||||
source: '/assets/tomato-seeds/tomato-seeds.json',
|
||||
},
|
||||
4: {
|
||||
qty: 1,
|
||||
source: '/assets/hoe/hoe.json',
|
||||
},
|
||||
5: {
|
||||
qty: 1,
|
||||
source: '/assets/brush/brush.json',
|
||||
},
|
||||
},
|
||||
},
|
||||
Health: {health: 100},
|
||||
|
|
|
@ -316,3 +316,52 @@ export function removeCollinear([...vertices]) {
|
|||
}
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
export function transform(
|
||||
vertices,
|
||||
{
|
||||
rotation = 0,
|
||||
scale = 1,
|
||||
translation = {x: 0, y: 0},
|
||||
origin = {x: 0, y: 0},
|
||||
},
|
||||
) {
|
||||
// nop
|
||||
if (0 === rotation && 1 === scale && 0 === translation.x && 0 === translation.y) {
|
||||
return vertices;
|
||||
}
|
||||
const transformed = [];
|
||||
// scale
|
||||
for (const vertice of vertices) {
|
||||
if (1 === scale) {
|
||||
transformed.push({x: vertice.x, y: vertice.y});
|
||||
continue;
|
||||
}
|
||||
transformed.push({
|
||||
x: origin.x + (vertice.x - origin.x) * scale,
|
||||
y: origin.y + (vertice.y - origin.y) * scale,
|
||||
});
|
||||
}
|
||||
// rotation
|
||||
rotation = rotation % TAU;
|
||||
if (0 !== rotation) {
|
||||
for (const vertice of transformed) {
|
||||
let a = rotation + Math.atan2(
|
||||
vertice.y - origin.y,
|
||||
vertice.x - origin.x,
|
||||
);
|
||||
a = (a >= 0 || a < TAU) ? a : (a % TAU + TAU) % TAU;
|
||||
const d = distance(vertice, origin);
|
||||
vertice.x = origin.x + d * Math.cos(a);
|
||||
vertice.y = origin.y + d * Math.sin(a);
|
||||
}
|
||||
}
|
||||
// translation
|
||||
if (0 !== translation.x || 0 !== translation.y) {
|
||||
for (const vertice of transformed) {
|
||||
vertice.x += translation.x;
|
||||
vertice.y += translation.y;
|
||||
}
|
||||
}
|
||||
return transformed;
|
||||
}
|
||||
|
|
87
app/util/math.test.js
Normal file
87
app/util/math.test.js
Normal file
|
@ -0,0 +1,87 @@
|
|||
import {expect, test} from 'vitest';
|
||||
|
||||
import * as MathUtil from './math.js';
|
||||
|
||||
test('transforms vertices', async () => {
|
||||
const expectCloseTo = (l, r) => {
|
||||
expect(l.length)
|
||||
.to.equal(r.length);
|
||||
for (let i = 0; i < l.length; ++i) {
|
||||
expect(l[i].x)
|
||||
.to.be.closeTo(r[i].x, 0.0001);
|
||||
expect(l[i].y)
|
||||
.to.be.closeTo(r[i].y, 0.0001);
|
||||
}
|
||||
}
|
||||
const vertices = [
|
||||
{x: -1, y: -1},
|
||||
{x: 1, y: -1},
|
||||
{x: 1, y: 1},
|
||||
{x: -1, y: 1},
|
||||
];
|
||||
expect(MathUtil.transform(vertices, {}))
|
||||
.to.deep.equal(vertices);
|
||||
expectCloseTo(
|
||||
MathUtil.transform(vertices, {scale: 2}),
|
||||
[
|
||||
{x: -2, y: -2},
|
||||
{x: 2, y: -2},
|
||||
{x: 2, y: 2},
|
||||
{x: -2, y: 2},
|
||||
],
|
||||
);
|
||||
expectCloseTo(
|
||||
MathUtil.transform(vertices, {rotation: MathUtil.QUARTER_PI}),
|
||||
[
|
||||
{x: 0, y: -Math.sqrt(2)},
|
||||
{x: Math.sqrt(2), y: 0},
|
||||
{x: 0, y: Math.sqrt(2)},
|
||||
{x: -Math.sqrt(2), y: 0},
|
||||
],
|
||||
);
|
||||
expectCloseTo(
|
||||
MathUtil.transform(vertices, {rotation: MathUtil.QUARTER_PI, scale: 2}),
|
||||
[
|
||||
{x: 0, y: -Math.sqrt(2) * 2},
|
||||
{x: Math.sqrt(2) * 2, y: 0},
|
||||
{x: 0, y: Math.sqrt(2) * 2},
|
||||
{x: -Math.sqrt(2) * 2, y: 0},
|
||||
],
|
||||
);
|
||||
expectCloseTo(
|
||||
MathUtil.transform(vertices, {translation: {x: 10, y: 10}}),
|
||||
[
|
||||
{x: 9, y: 9},
|
||||
{x: 11, y: 9},
|
||||
{x: 11, y: 11},
|
||||
{x: 9, y: 11},
|
||||
],
|
||||
);
|
||||
expectCloseTo(
|
||||
MathUtil.transform(vertices, {rotation: MathUtil.QUARTER_PI, translation: {x: 10, y: 10}}),
|
||||
[
|
||||
{x: 10, y: 10 - Math.sqrt(2)},
|
||||
{x: 10 + Math.sqrt(2), y: 10},
|
||||
{x: 10, y: 10 + Math.sqrt(2)},
|
||||
{x: 10 - Math.sqrt(2), y: 10},
|
||||
],
|
||||
);
|
||||
expectCloseTo(
|
||||
MathUtil.transform(vertices, {scale: 2, translation: {x: 10, y: 10}}),
|
||||
[
|
||||
{x: 8, y: 8},
|
||||
{x: 12, y: 8},
|
||||
{x: 12, y: 12},
|
||||
{x: 8, y: 12},
|
||||
],
|
||||
);
|
||||
expectCloseTo(
|
||||
MathUtil.transform(vertices, {rotation: MathUtil.QUARTER_PI, scale: 2, translation: {x: 10, y: 10}}),
|
||||
[
|
||||
{x: 10, y: 10 - Math.sqrt(2) * 2},
|
||||
{x: 10 + Math.sqrt(2) * 2, y: 10},
|
||||
{x: 10, y: 10 + Math.sqrt(2) * 2},
|
||||
{x: 10 - Math.sqrt(2) * 2, y: 10},
|
||||
],
|
||||
);
|
||||
});
|
Loading…
Reference in New Issue
Block a user