Compare commits
27 Commits
71d35bd228
...
782fdfc28c
Author | SHA1 | Date | |
---|---|---|---|
|
782fdfc28c | ||
|
a2a33536e7 | ||
|
70cc56c7b6 | ||
|
f522c61307 | ||
|
9b9b06f1dc | ||
|
739a3e4f95 | ||
|
34247e00db | ||
|
26b3f5049c | ||
|
29471d8b5d | ||
|
6e8f74b369 | ||
|
2fe09d3cb2 | ||
|
6a6be49a61 | ||
|
a8d32e2b01 | ||
|
6c815e7749 | ||
|
b2b52b5414 | ||
|
6a2dddef0e | ||
|
be1e162da5 | ||
|
603d2035f0 | ||
|
26f69b4d6b | ||
|
66e6c4a727 | ||
|
6f094d02c2 | ||
|
2041b38678 | ||
|
bea551fa19 | ||
|
68b908ec43 | ||
|
62f1584a42 | ||
|
8b720619a5 | ||
|
611b41e96a |
|
@ -28,6 +28,7 @@ module.exports = {
|
|||
extends: ['eslint:recommended'],
|
||||
rules: {
|
||||
'no-constant-condition': ['error', {checkLoops: false}],
|
||||
'require-yield': 0,
|
||||
},
|
||||
|
||||
overrides: [
|
||||
|
@ -60,10 +61,10 @@ module.exports = {
|
|||
],
|
||||
},
|
||||
rules: {
|
||||
'react/prop-types': 'off',
|
||||
'jsx-a11y/label-has-associated-control': [2, {
|
||||
controlComponents: ['SliderText'],
|
||||
}],
|
||||
'react/prop-types': 'off',
|
||||
},
|
||||
},
|
||||
|
||||
|
@ -80,14 +81,5 @@ module.exports = {
|
|||
},
|
||||
},
|
||||
|
||||
// Assets
|
||||
{
|
||||
files: [
|
||||
'resources/**/*.js',
|
||||
],
|
||||
rules: {
|
||||
'no-undef': 0,
|
||||
},
|
||||
}
|
||||
],
|
||||
};
|
||||
|
|
|
@ -58,7 +58,7 @@ const interpolate = () => {
|
|||
}
|
||||
requestAnimationFrame(interpolate);
|
||||
|
||||
onmessage = async (event) => {
|
||||
onmessage = (event) => {
|
||||
const packet = event.data;
|
||||
switch (packet.type) {
|
||||
case 'EcsChange': {
|
||||
|
|
|
@ -71,11 +71,9 @@ function applyClientActions(elapsed) {
|
|||
}
|
||||
}
|
||||
|
||||
let downPromise;
|
||||
|
||||
const pending = new Map();
|
||||
|
||||
onmessage = async (event) => {
|
||||
onmessage = (event) => {
|
||||
const [flow, packet] = event.data;
|
||||
switch (flow) {
|
||||
case Flow.UP: {
|
||||
|
@ -114,53 +112,51 @@ onmessage = async (event) => {
|
|||
break;
|
||||
}
|
||||
case Flow.DOWN: {
|
||||
downPromise = Promise.resolve(downPromise).then(async () => {
|
||||
switch (packet.type) {
|
||||
case 'ActionAck': {
|
||||
const action = actions.get(packet.payload.ack);
|
||||
action.ack = true;
|
||||
return;
|
||||
}
|
||||
case 'EcsChange': {
|
||||
ecs = new PredictionEcs({Components, Systems});
|
||||
mainEntityId = 0;
|
||||
break;
|
||||
}
|
||||
case 'Tick': {
|
||||
for (const entityId in packet.payload.ecs) {
|
||||
if (packet.payload.ecs[entityId]) {
|
||||
if (packet.payload.ecs[entityId].MainEntity) {
|
||||
mainEntityId = parseInt(entityId);
|
||||
}
|
||||
}
|
||||
}
|
||||
await ecs.apply(packet.payload.ecs);
|
||||
if (actions.size > 0) {
|
||||
const main = ecs.get(mainEntityId);
|
||||
const authoritative = structuredClone(main.toNet(main));
|
||||
applyClientActions(packet.payload.elapsed);
|
||||
if (ecs.diff[mainEntityId]) {
|
||||
packet.payload.ecs[mainEntityId] ??= {};
|
||||
ecs.mergeDiff(
|
||||
packet.payload.ecs[mainEntityId],
|
||||
ecs.diff[mainEntityId],
|
||||
);
|
||||
const reset = {};
|
||||
for (const componentName in ecs.diff[mainEntityId]) {
|
||||
reset[componentName] = {};
|
||||
for (const property in ecs.diff[mainEntityId][componentName]) {
|
||||
reset[componentName][property] = authoritative[componentName][property];
|
||||
}
|
||||
}
|
||||
await ecs.apply({[mainEntityId]: reset});
|
||||
}
|
||||
}
|
||||
ecs.setClean();
|
||||
break;
|
||||
}
|
||||
switch (packet.type) {
|
||||
case 'ActionAck': {
|
||||
const action = actions.get(packet.payload.ack);
|
||||
action.ack = true;
|
||||
return;
|
||||
}
|
||||
postMessage([1, packet]);
|
||||
});
|
||||
case 'EcsChange': {
|
||||
ecs = new PredictionEcs({Components, Systems});
|
||||
mainEntityId = 0;
|
||||
break;
|
||||
}
|
||||
case 'Tick': {
|
||||
for (const entityId in packet.payload.ecs) {
|
||||
if (packet.payload.ecs[entityId]) {
|
||||
if (packet.payload.ecs[entityId].MainEntity) {
|
||||
mainEntityId = parseInt(entityId);
|
||||
}
|
||||
}
|
||||
}
|
||||
ecs.apply(packet.payload.ecs);
|
||||
if (actions.size > 0) {
|
||||
const main = ecs.get(mainEntityId);
|
||||
const authoritative = structuredClone(main.toNet(main));
|
||||
applyClientActions(packet.payload.elapsed);
|
||||
if (ecs.diff[mainEntityId]) {
|
||||
packet.payload.ecs[mainEntityId] ??= {};
|
||||
ecs.mergeDiff(
|
||||
packet.payload.ecs[mainEntityId],
|
||||
ecs.diff[mainEntityId],
|
||||
);
|
||||
const reset = {};
|
||||
for (const componentName in ecs.diff[mainEntityId]) {
|
||||
reset[componentName] = {};
|
||||
for (const property in ecs.diff[mainEntityId][componentName]) {
|
||||
reset[componentName][property] = authoritative[componentName][property];
|
||||
}
|
||||
}
|
||||
ecs.apply({[mainEntityId]: reset});
|
||||
}
|
||||
}
|
||||
ecs.setClean();
|
||||
break;
|
||||
}
|
||||
}
|
||||
postMessage([1, packet]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,12 +27,12 @@ export default class Component {
|
|||
return results;
|
||||
}
|
||||
|
||||
async create(entityId, values) {
|
||||
const [created] = await this.createMany([[entityId, values]]);
|
||||
create(entityId, values) {
|
||||
const [created] = this.createMany([[entityId, values]]);
|
||||
return created;
|
||||
}
|
||||
|
||||
async createMany(entries) {
|
||||
createMany(entries) {
|
||||
if (0 === entries.length) {
|
||||
return [];
|
||||
}
|
||||
|
@ -61,7 +61,6 @@ export default class Component {
|
|||
}
|
||||
}
|
||||
}
|
||||
const promises = [];
|
||||
for (let i = 0; i < entries.length; ++i) {
|
||||
const [entityId, values] = entries[i];
|
||||
const instance = allocated[i];
|
||||
|
@ -78,9 +77,8 @@ export default class Component {
|
|||
: defaults[key];
|
||||
}
|
||||
instance.initialize(values, defaultValues);
|
||||
promises.push(this.load(instance));
|
||||
this.load(instance);
|
||||
}
|
||||
await Promise.all(promises);
|
||||
return allocated;
|
||||
}
|
||||
|
||||
|
@ -125,8 +123,8 @@ export default class Component {
|
|||
return this.instances[entityId];
|
||||
}
|
||||
|
||||
async insertMany(entities) {
|
||||
await this.createMany(entities);
|
||||
insertMany(entities) {
|
||||
this.createMany(entities);
|
||||
}
|
||||
|
||||
instanceFromSchema() {
|
||||
|
@ -161,7 +159,7 @@ export default class Component {
|
|||
toJSON() {
|
||||
return this.toFullJSON();
|
||||
}
|
||||
async update(values) {
|
||||
update(values) {
|
||||
for (const key in values) {
|
||||
if (concrete.properties[key]) {
|
||||
this[`$$${key}`] = values[key];
|
||||
|
@ -198,7 +196,7 @@ export default class Component {
|
|||
return Instance;
|
||||
}
|
||||
|
||||
async load(instance) {
|
||||
load(instance) {
|
||||
return instance;
|
||||
}
|
||||
|
||||
|
@ -228,13 +226,11 @@ export default class Component {
|
|||
return this.constructor.schema.sizeOf(this.get(entityId));
|
||||
}
|
||||
|
||||
async updateMany(entities) {
|
||||
const promises = [];
|
||||
updateMany(entities) {
|
||||
for (let i = 0; i < entities.length; i++) {
|
||||
const [entityId, values] = entities[i];
|
||||
promises.push(this.get(entityId).update(values));
|
||||
this.get(entityId).update(values)
|
||||
}
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -4,26 +4,26 @@ import Component from './component.js';
|
|||
|
||||
const fakeEcs = {markChange() {}};
|
||||
|
||||
test('creates instances', async () => {
|
||||
test('creates instances', () => {
|
||||
class CreatingComponent extends Component {
|
||||
static properties = {
|
||||
foo: {defaultValue: 'bar', type: 'string'},
|
||||
};
|
||||
}
|
||||
const ComponentInstance = new CreatingComponent(fakeEcs);
|
||||
await ComponentInstance.create(1);
|
||||
ComponentInstance.create(1);
|
||||
expect(ComponentInstance.get(1).entity)
|
||||
.to.equal(1);
|
||||
});
|
||||
|
||||
test('does not serialize default values', async () => {
|
||||
test.skip('does not serialize default values', () => {
|
||||
class CreatingComponent extends Component {
|
||||
static properties = {
|
||||
foo: {defaultValue: 'bar', type: 'string'}, bar: {type: 'uint8'},
|
||||
};
|
||||
}
|
||||
const ComponentInstance = new CreatingComponent(fakeEcs);
|
||||
await ComponentInstance.create(1)
|
||||
ComponentInstance.create(1)
|
||||
expect(ComponentInstance.get(1).toJSON())
|
||||
.to.deep.equal({});
|
||||
ComponentInstance.get(1).bar = 1;
|
||||
|
@ -31,14 +31,14 @@ test('does not serialize default values', async () => {
|
|||
.to.deep.equal({bar: 1});
|
||||
});
|
||||
|
||||
test('reuses instances', async () => {
|
||||
test('reuses instances', () => {
|
||||
class ReusingComponent extends Component {
|
||||
static properties = {
|
||||
foo: {type: 'string'},
|
||||
};
|
||||
}
|
||||
const ComponentInstance = new ReusingComponent(fakeEcs);
|
||||
await ComponentInstance.create(1);
|
||||
ComponentInstance.create(1);
|
||||
const instance = ComponentInstance.get(1);
|
||||
ComponentInstance.destroy(1);
|
||||
expect(ComponentInstance.get(1))
|
||||
|
@ -47,7 +47,7 @@ test('reuses instances', async () => {
|
|||
ComponentInstance.destroy(1);
|
||||
})
|
||||
.to.throw();
|
||||
await ComponentInstance.create(1);
|
||||
ComponentInstance.create(1);
|
||||
expect(ComponentInstance.get(1))
|
||||
.to.equal(instance);
|
||||
});
|
||||
|
|
|
@ -19,28 +19,27 @@ export default class Alive extends Component {
|
|||
this.$$dead = true;
|
||||
const {Ticking} = ecs.get(this.entity);
|
||||
if (Ticking) {
|
||||
this.$$death.context.entity = ecs.get(this.entity);
|
||||
this.$$death.locals.entity = ecs.get(this.entity);
|
||||
const ticker = this.$$death.ticker();
|
||||
ecs.addDestructionDependency(this.entity.id, ticker);
|
||||
Ticking.add(ticker);
|
||||
ecs.addDestructionDependency(this.entity.id, Ticking.add(ticker));
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
async load(instance) {
|
||||
load(instance) {
|
||||
if (0 === instance.maxHealth) {
|
||||
instance.maxHealth = instance.health;
|
||||
}
|
||||
// heavy handed...
|
||||
if ('undefined' !== typeof window) {
|
||||
return;
|
||||
}
|
||||
instance.$$death = await this.ecs.readScript(
|
||||
instance.$$death = this.ecs.readScript(
|
||||
instance.deathScript,
|
||||
{
|
||||
ecs: this.ecs,
|
||||
},
|
||||
);
|
||||
if (0 === instance.maxHealth) {
|
||||
instance.maxHealth = instance.health;
|
||||
}
|
||||
}
|
||||
static properties = {
|
||||
deathScript: {
|
||||
|
|
20
app/ecs/components/arbitrary.js
Normal file
20
app/ecs/components/arbitrary.js
Normal file
|
@ -0,0 +1,20 @@
|
|||
import Component from '@/ecs/component.js';
|
||||
|
||||
export default class Arbitrary extends Component {
|
||||
instanceFromSchema() {
|
||||
return class ArbitraryInstance extends super.instanceFromSchema() {
|
||||
bag = {};
|
||||
toFullJSON() {
|
||||
return {
|
||||
blob: JSON.stringify(this.bag),
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
load(instance) {
|
||||
instance.bag = JSON.parse(instance.blob);
|
||||
}
|
||||
static properties = {
|
||||
blob: {type: 'string'},
|
||||
};
|
||||
}
|
|
@ -8,27 +8,21 @@ export default class Behaving extends Component {
|
|||
tick(elapsed) {
|
||||
const routine = this.$$routineInstances[this.currentRoutine];
|
||||
if (routine) {
|
||||
routine.context.entity = ecs.get(this.entity);
|
||||
routine.locals.ecs = ecs;
|
||||
routine.locals.entity = ecs.get(this.entity);
|
||||
routine.tick(elapsed);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
async load(instance) {
|
||||
load(instance) {
|
||||
// heavy handed...
|
||||
if ('undefined' !== typeof window) {
|
||||
return;
|
||||
}
|
||||
const promises = [];
|
||||
for (const key in instance.routines) {
|
||||
promises.push(
|
||||
this.ecs.readScript(instance.routines[key])
|
||||
.then((script) => {
|
||||
instance.$$routineInstances[key] = script;
|
||||
}),
|
||||
);
|
||||
instance.$$routineInstances[key] = this.ecs.readScript(instance.routines[key]);
|
||||
}
|
||||
await Promise.all(promises);
|
||||
}
|
||||
static properties = {
|
||||
currentRoutine: {defaultValue: 'initial', type: 'string'},
|
||||
|
|
|
@ -35,6 +35,9 @@ export default class Collider extends Component {
|
|||
return aabbs;
|
||||
}
|
||||
checkCollision(other) {
|
||||
if (!this.isColliding || !other.isColliding) {
|
||||
return;
|
||||
}
|
||||
const otherEntity = ecs.get(other.entity);
|
||||
const thisEntity = ecs.get(this.entity);
|
||||
const intersections = this.intersectionsWith(other);
|
||||
|
@ -76,23 +79,23 @@ export default class Collider extends Component {
|
|||
if (!hasMatchingIntersection) {
|
||||
if (this.$$collisionStart) {
|
||||
const script = this.$$collisionStart.clone();
|
||||
script.context.entity = thisEntity;
|
||||
script.context.other = otherEntity;
|
||||
script.context.pair = [body, otherBody];
|
||||
script.locals.entity = thisEntity;
|
||||
script.locals.other = otherEntity;
|
||||
script.locals.pair = [body, otherBody];
|
||||
const ticker = script.ticker();
|
||||
ecs.addDestructionDependency(otherEntity.id, ticker);
|
||||
ecs.addDestructionDependency(thisEntity.id, ticker);
|
||||
thisEntity.Ticking.add(ticker);
|
||||
const promise = thisEntity.Ticking.add(ticker);
|
||||
ecs.addDestructionDependency(otherEntity.id, promise);
|
||||
ecs.addDestructionDependency(thisEntity.id, promise);
|
||||
}
|
||||
if (other.$$collisionStart) {
|
||||
const script = other.$$collisionStart.clone();
|
||||
script.context.entity = otherEntity;
|
||||
script.context.other = thisEntity;
|
||||
script.context.pair = [otherBody, body];
|
||||
script.locals.entity = otherEntity;
|
||||
script.locals.other = thisEntity;
|
||||
script.locals.pair = [otherBody, body];
|
||||
const ticker = script.ticker();
|
||||
ecs.addDestructionDependency(otherEntity.id, ticker);
|
||||
ecs.addDestructionDependency(thisEntity.id, ticker);
|
||||
otherEntity.Ticking.add(ticker);
|
||||
const promise = otherEntity.Ticking.add(ticker);
|
||||
ecs.addDestructionDependency(otherEntity.id, promise);
|
||||
ecs.addDestructionDependency(thisEntity.id, promise);
|
||||
}
|
||||
activeIntersections.add(intersection);
|
||||
}
|
||||
|
@ -161,21 +164,21 @@ export default class Collider extends Component {
|
|||
];
|
||||
if (this.$$collisionEnd) {
|
||||
const script = this.$$collisionEnd.clone();
|
||||
script.context.other = otherEntity;
|
||||
script.context.pair = [body, otherBody];
|
||||
script.locals.other = otherEntity;
|
||||
script.locals.pair = [body, otherBody];
|
||||
const ticker = script.ticker();
|
||||
ecs.addDestructionDependency(thisEntity.id, ticker);
|
||||
ecs.addDestructionDependency(otherEntity.id, ticker);
|
||||
thisEntity.Ticking.add(ticker);
|
||||
const promise = thisEntity.Ticking.add(ticker);
|
||||
ecs.addDestructionDependency(thisEntity.id, promise);
|
||||
ecs.addDestructionDependency(otherEntity.id, promise);
|
||||
}
|
||||
if (other.$$collisionEnd) {
|
||||
const script = other.$$collisionEnd.clone();
|
||||
script.context.other = thisEntity;
|
||||
script.context.pair = [otherBody, body];
|
||||
script.locals.other = thisEntity;
|
||||
script.locals.pair = [otherBody, body];
|
||||
const ticker = script.ticker();
|
||||
ecs.addDestructionDependency(thisEntity.id, ticker);
|
||||
ecs.addDestructionDependency(otherEntity.id, ticker);
|
||||
otherEntity.Ticking.add(ticker);
|
||||
const promise = otherEntity.Ticking.add(ticker);
|
||||
ecs.addDestructionDependency(thisEntity.id, promise);
|
||||
ecs.addDestructionDependency(otherEntity.id, promise);
|
||||
}
|
||||
}
|
||||
this.$$intersections.delete(other);
|
||||
|
@ -255,7 +258,7 @@ export default class Collider extends Component {
|
|||
}
|
||||
}
|
||||
}
|
||||
async load(instance) {
|
||||
load(instance) {
|
||||
for (const i in instance.bodies) {
|
||||
instance.bodies[i] = {
|
||||
...this.constructor.schema.constructor.defaultValue(
|
||||
|
@ -269,13 +272,13 @@ export default class Collider extends Component {
|
|||
if ('undefined' !== typeof window) {
|
||||
return;
|
||||
}
|
||||
instance.$$collisionEnd = await this.ecs.readScript(
|
||||
instance.$$collisionEnd = this.ecs.readScript(
|
||||
instance.collisionEndScript,
|
||||
{
|
||||
ecs: this.ecs,
|
||||
},
|
||||
);
|
||||
instance.$$collisionStart = await this.ecs.readScript(
|
||||
instance.$$collisionStart = this.ecs.readScript(
|
||||
instance.collisionStartScript,
|
||||
{
|
||||
ecs: this.ecs,
|
||||
|
@ -306,5 +309,6 @@ export default class Collider extends Component {
|
|||
},
|
||||
collisionEndScript: {type: 'string'},
|
||||
collisionStartScript: {type: 'string'},
|
||||
isColliding: {defaultValue: 1, type: 'uint8'},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -6,21 +6,17 @@ export default class Controlled extends Component {
|
|||
directionMove(direction) {
|
||||
const x = Math.cos(direction);
|
||||
if (x > 0) {
|
||||
this.moveLeft = 0;
|
||||
this.moveRight = x;
|
||||
this.moveRight += x;
|
||||
}
|
||||
else {
|
||||
this.moveLeft = -x;
|
||||
this.moveRight = 0;
|
||||
this.moveLeft -= x;
|
||||
}
|
||||
const y = Math.sin(direction);
|
||||
if (y > 0) {
|
||||
this.moveUp = 0;
|
||||
this.moveDown = y;
|
||||
this.moveDown += y;
|
||||
}
|
||||
else {
|
||||
this.moveUp = -y;
|
||||
this.moveDown = 0;
|
||||
this.moveUp -= y;
|
||||
}
|
||||
}
|
||||
stop() {
|
||||
|
@ -35,6 +31,7 @@ export default class Controlled extends Component {
|
|||
}
|
||||
}
|
||||
static properties = {
|
||||
locked: {type: 'uint8'},
|
||||
moveUp: {type: 'float32'},
|
||||
moveRight: {type: 'float32'},
|
||||
moveDown: {type: 'float32'},
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import Component from '@/ecs/component.js';
|
||||
|
||||
import Emitter from '@/particles/emitter.js';
|
||||
import {Ticker as TickerPromise} from '@/util/promise.js';
|
||||
import Ticker from '@/util/ticker.js';
|
||||
|
||||
export default class EmitterComponent extends Component {
|
||||
instanceFromSchema() {
|
||||
|
@ -12,21 +12,20 @@ export default class EmitterComponent extends Component {
|
|||
id = 0;
|
||||
emit(specification) {
|
||||
if (specification.server) {
|
||||
const {Ticker} = ecs.get(1);
|
||||
if (Ticker) {
|
||||
const master = ecs.get(1);
|
||||
if (master.Ticking) {
|
||||
const emitter = new Emitter(ecs);
|
||||
const promise = new Promise((resolve) => {
|
||||
emitter.emit().onEnd(resolve);
|
||||
});
|
||||
Ticker.add(
|
||||
new TickerPromise(
|
||||
(resolve) => {
|
||||
promise.then(resolve);
|
||||
},
|
||||
(elapsed) => {
|
||||
master.Ticking.add(
|
||||
new Ticker(function* () {
|
||||
let emitting = true;
|
||||
emitter.emit().onEnd(() => {
|
||||
emitting = false;
|
||||
});
|
||||
while (emitting) {
|
||||
const elapsed = yield;
|
||||
this.emitter.tick(elapsed);
|
||||
}
|
||||
)
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,16 @@ export default class Forces extends Component {
|
|||
instanceFromSchema() {
|
||||
const {ecs} = this;
|
||||
return class ForcesInstance extends super.instanceFromSchema() {
|
||||
applyForce({x, y}) {
|
||||
this.$$forceX += x;
|
||||
this.$$forceY += y;
|
||||
ecs.markChange(this.entity, {
|
||||
Forces: {
|
||||
forceX: this.$$forceX,
|
||||
forceY: this.$$forceY,
|
||||
},
|
||||
});
|
||||
}
|
||||
applyImpulse({x, y}) {
|
||||
this.$$impulseX += x;
|
||||
this.$$impulseY += y;
|
||||
|
@ -17,6 +27,8 @@ export default class Forces extends Component {
|
|||
}
|
||||
}
|
||||
static properties = {
|
||||
forceX: {type: 'float32'},
|
||||
forceY: {type: 'float32'},
|
||||
impulseX: {type: 'float32'},
|
||||
impulseY: {type: 'float32'},
|
||||
};
|
||||
|
|
3
app/ecs/components/grabber.js
Normal file
3
app/ecs/components/grabber.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
import Component from '@/ecs/component.js';
|
||||
|
||||
export default class Grabber extends Component {}
|
31
app/ecs/components/harmful.js
Normal file
31
app/ecs/components/harmful.js
Normal file
|
@ -0,0 +1,31 @@
|
|||
import Component from '@/ecs/component.js';
|
||||
|
||||
export default class Harmful extends Component {
|
||||
instanceFromSchema() {
|
||||
const {ecs} = this;
|
||||
return class HarmfulInstance extends super.instanceFromSchema() {
|
||||
harm(other) {
|
||||
const entity = ecs.get(this.entity);
|
||||
const script = this.$$harm.clone();
|
||||
script.locals.other = other;
|
||||
script.locals.entity = entity;
|
||||
entity.Ticking.add(script.ticker());
|
||||
}
|
||||
}
|
||||
}
|
||||
load(instance) {
|
||||
// heavy handed...
|
||||
if ('undefined' !== typeof window) {
|
||||
return;
|
||||
}
|
||||
instance.$$harm = this.ecs.readScript(
|
||||
instance.harmScript,
|
||||
{
|
||||
ecs: this.ecs,
|
||||
},
|
||||
);
|
||||
}
|
||||
static properties = {
|
||||
harmScript: {type: 'string'},
|
||||
};
|
||||
}
|
|
@ -4,15 +4,30 @@ const Gathered = gather(
|
|||
import.meta.glob(['./*.js', '!./*.test.js'], {eager: true, import: 'default'}),
|
||||
);
|
||||
|
||||
let wrapComponent;
|
||||
if (import.meta.env.PROD) {
|
||||
wrapComponent = (componentName, Component) => (
|
||||
class extends Component {
|
||||
static componentName = componentName;
|
||||
}
|
||||
);
|
||||
}
|
||||
else {
|
||||
const {default: ieval} = await import('@/util/eval.js');
|
||||
wrapComponent = (componentName, Component) => (
|
||||
ieval(`
|
||||
((Component) => (
|
||||
class ${componentName} extends Component {
|
||||
static componentName = '${componentName}';
|
||||
}
|
||||
))
|
||||
`)(Component)
|
||||
);
|
||||
}
|
||||
|
||||
const Components = {};
|
||||
for (const componentName in Gathered) {
|
||||
Components[componentName] = eval(`
|
||||
((Gathered) => (
|
||||
class ${componentName} extends Gathered['${componentName}'] {
|
||||
static componentName = '${componentName}';
|
||||
}
|
||||
))
|
||||
`)(Gathered);
|
||||
Components[componentName] = wrapComponent(componentName, Gathered[componentName]);
|
||||
}
|
||||
|
||||
export default Components;
|
||||
|
|
|
@ -7,9 +7,9 @@ export default class Interactive extends Component {
|
|||
$$interact;
|
||||
interact(initiator) {
|
||||
const script = this.$$interact.clone();
|
||||
script.context.initiator = initiator;
|
||||
script.context.subject = ecs.get(this.entity);
|
||||
const {Ticking} = script.context.subject;
|
||||
script.locals.initiator = initiator;
|
||||
script.locals.subject = ecs.get(this.entity);
|
||||
const {Ticking} = script.locals.subject;
|
||||
Ticking.add(script.ticker());
|
||||
}
|
||||
get interacting() {
|
||||
|
@ -20,12 +20,12 @@ export default class Interactive extends Component {
|
|||
}
|
||||
}
|
||||
}
|
||||
async load(instance) {
|
||||
load(instance) {
|
||||
// heavy handed...
|
||||
if ('undefined' !== typeof window) {
|
||||
return;
|
||||
}
|
||||
instance.$$interact = await this.ecs.readScript(
|
||||
instance.$$interact = this.ecs.readScript(
|
||||
instance.interactScript,
|
||||
{
|
||||
ecs: this.ecs,
|
||||
|
|
|
@ -3,25 +3,29 @@ import Component from '@/ecs/component.js';
|
|||
import {distribute} from '@/util/inventory.js';
|
||||
|
||||
class ItemProxy {
|
||||
scripts = {};
|
||||
constructor(Component, instance, slot) {
|
||||
this.Component = Component;
|
||||
this.instance = instance;
|
||||
this.slot = slot;
|
||||
}
|
||||
async load(source) {
|
||||
load(source) {
|
||||
const {ecs} = this.Component;
|
||||
const json = await ecs.readJson(source);
|
||||
const json = ecs.readJson(source);
|
||||
this.json = json;
|
||||
const scripts = {};
|
||||
if (json.projectionCheck) {
|
||||
scripts.projectionCheckInstance = await ecs.readScript(json.projectionCheck);
|
||||
scripts.projectionCheckInstance = ecs.readScript(json.projectionCheck);
|
||||
}
|
||||
if (json.start) {
|
||||
scripts.startInstance = await ecs.readScript(json.start);
|
||||
// heavy handed...
|
||||
if ('undefined' === typeof window) {
|
||||
if (json.start) {
|
||||
scripts.startInstance = ecs.readScript(json.start);
|
||||
}
|
||||
if (json.stop) {
|
||||
scripts.stopInstance = ecs.readScript(json.stop);
|
||||
}
|
||||
}
|
||||
if (json.stop) {
|
||||
scripts.stopInstance = await ecs.readScript(json.stop);
|
||||
}
|
||||
this.json = json;
|
||||
this.scripts = scripts;
|
||||
}
|
||||
project(position, direction) {
|
||||
|
@ -82,8 +86,8 @@ class ItemProxy {
|
|||
}
|
||||
}
|
||||
if (this.scripts.projectionCheckInstance) {
|
||||
this.scripts.projectionCheckInstance.context.ecs = this.Component.ecs;
|
||||
this.scripts.projectionCheckInstance.context.projected = projected;
|
||||
this.scripts.projectionCheckInstance.locals.ecs = this.Component.ecs;
|
||||
this.scripts.projectionCheckInstance.locals.projected = projected;
|
||||
return this.scripts.projectionCheckInstance.evaluate();
|
||||
}
|
||||
else {
|
||||
|
@ -128,7 +132,7 @@ class ItemProxy {
|
|||
}
|
||||
|
||||
export default class Inventory extends Component {
|
||||
async updateMany(entities) {
|
||||
updateMany(entities) {
|
||||
for (const [id, {cleared, given, qtyUpdated, swapped}] of entities) {
|
||||
const instance = this.get(id);
|
||||
const {$$items, slots} = instance;
|
||||
|
@ -141,7 +145,7 @@ export default class Inventory extends Component {
|
|||
if (given) {
|
||||
for (const slot in given) {
|
||||
$$items[slot] = new ItemProxy(this, instance, slot);
|
||||
await $$items[slot].load(given[slot].source);
|
||||
$$items[slot].load(given[slot].source);
|
||||
slots[slot] = given[slot];
|
||||
}
|
||||
}
|
||||
|
@ -166,14 +170,14 @@ export default class Inventory extends Component {
|
|||
}
|
||||
}
|
||||
}
|
||||
await super.updateMany(entities);
|
||||
super.updateMany(entities);
|
||||
for (const [id, {slots}] of entities) {
|
||||
if (slots) {
|
||||
const instance = this.get(id);
|
||||
instance.$$items = {};
|
||||
for (const slot in slots) {
|
||||
instance.$$items[slot] = new ItemProxy(this, instance, slot);
|
||||
await instance.$$items[slot].load(instance.slots[slot].source);
|
||||
instance.$$items[slot].load(instance.slots[slot].source);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -189,7 +193,7 @@ export default class Inventory extends Component {
|
|||
delete this.slots[slot];
|
||||
delete this.$$items[slot];
|
||||
}
|
||||
async distribute(slot, potentialDestinations) {
|
||||
distribute(slot, potentialDestinations) {
|
||||
const {slots} = this;
|
||||
if (!slots[slot]) {
|
||||
return;
|
||||
|
@ -230,7 +234,7 @@ export default class Inventory extends Component {
|
|||
Inventory.slots[destination] = {...stack};
|
||||
Inventory.$$items[destination] = new ItemProxy(Component, Inventory, destination);
|
||||
Component.markChange(entityId, 'given', {[destination]: {...stack}});
|
||||
await Inventory.$$items[destination].load(source);
|
||||
Inventory.$$items[destination].load(source);
|
||||
}
|
||||
}
|
||||
// update qty of existing
|
||||
|
@ -255,12 +259,17 @@ export default class Inventory extends Component {
|
|||
item(slot) {
|
||||
return this.$$items[slot];
|
||||
}
|
||||
async give(stack) {
|
||||
give(stack) {
|
||||
const {slots} = this;
|
||||
for (let slot = 1; slot < 11; ++slot) {
|
||||
if (slots[slot]?.source === stack.source) {
|
||||
slots[slot].qty += stack.qty;
|
||||
Component.markChange(this.entity, 'qtyUpdated', {[slot]: stack.qty});
|
||||
if (ecs.diff[this.entity]?.Inventory?.given?.[slot]) {
|
||||
ecs.diff[this.entity].Inventory.given[slot].qty += stack.qty;
|
||||
}
|
||||
else {
|
||||
Component.markChange(this.entity, 'qtyUpdated', {[slot]: stack.qty});
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -269,7 +278,7 @@ export default class Inventory extends Component {
|
|||
slots[slot] = {...stack};
|
||||
this.$$items[slot] = new ItemProxy(Component, this, slot);
|
||||
Component.markChange(this.entity, 'given', {[slot]: {...stack}});
|
||||
await this.$$items[slot].load(stack.source);
|
||||
this.$$items[slot].load(stack.source);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -305,10 +314,10 @@ export default class Inventory extends Component {
|
|||
}
|
||||
}
|
||||
}
|
||||
async load(instance) {
|
||||
load(instance) {
|
||||
for (const slot in instance.slots) {
|
||||
instance.$$items[slot] = new ItemProxy(this, instance, slot);
|
||||
await instance.$$items[slot].load(instance.slots[slot].source);
|
||||
instance.$$items[slot].load(instance.slots[slot].source);
|
||||
}
|
||||
}
|
||||
mergeDiff(original, update) {
|
||||
|
|
21
app/ecs/components/item-stack.js
Normal file
21
app/ecs/components/item-stack.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
import Component from '@/ecs/component.js';
|
||||
|
||||
export default class ItemStack extends Component {
|
||||
instanceFromSchema() {
|
||||
const {ecs} = this;
|
||||
return class ItemStackInstance extends super.instanceFromSchema() {
|
||||
pickup(other) {
|
||||
const {Collider} = ecs.get(this.entity);
|
||||
if (Collider) {
|
||||
Collider.isColliding = 0;
|
||||
}
|
||||
other.Inventory.give(this.toJSON());
|
||||
ecs.destroy(this.entity);
|
||||
}
|
||||
};
|
||||
}
|
||||
static properties = {
|
||||
qty: {type: 'uint32'},
|
||||
source: {type: 'string'},
|
||||
};
|
||||
}
|
|
@ -16,19 +16,19 @@ export default class Plant extends Component {
|
|||
}
|
||||
};
|
||||
}
|
||||
async load(instance) {
|
||||
load(instance) {
|
||||
// heavy handed...
|
||||
if ('undefined' !== typeof window) {
|
||||
return;
|
||||
}
|
||||
instance.$$grow = await this.ecs.readScript(
|
||||
instance.$$grow = this.ecs.readScript(
|
||||
instance.growScript,
|
||||
{
|
||||
ecs: this.ecs,
|
||||
plant: instance,
|
||||
},
|
||||
);
|
||||
instance.$$mayGrow = await this.ecs.readScript(
|
||||
instance.$$mayGrow = this.ecs.readScript(
|
||||
instance.mayGrowScript,
|
||||
{
|
||||
ecs: this.ecs,
|
||||
|
|
|
@ -191,9 +191,9 @@ export default class Sprite extends Component {
|
|||
}
|
||||
};
|
||||
}
|
||||
async load(instance) {
|
||||
load(instance) {
|
||||
if (instance.source) {
|
||||
instance.$$sourceJson = await this.ecs.readJson(instance.source);
|
||||
instance.$$sourceJson = this.ecs.readJson(instance.source);
|
||||
}
|
||||
}
|
||||
markChange(entityId, key, value) {
|
||||
|
@ -215,5 +215,6 @@ export default class Sprite extends Component {
|
|||
source: {type: 'string'},
|
||||
speed: {type: 'float32'},
|
||||
tint: {defaultValue: 0xffffff, type: 'uint32'},
|
||||
zIndex: {defaultValue: 65535, type: 'uint16'},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import Component from '@/ecs/component.js';
|
||||
import {withResolvers} from '@/util/promise.js';
|
||||
|
||||
export default class Ticking extends Component {
|
||||
instanceFromSchema() {
|
||||
|
@ -7,13 +8,9 @@ export default class Ticking extends Component {
|
|||
$$tickers = [];
|
||||
|
||||
add(ticker) {
|
||||
this.$$tickers.push(ticker);
|
||||
ticker.then(() => {
|
||||
this.$$tickers.splice(
|
||||
this.$$tickers.indexOf(ticker),
|
||||
1,
|
||||
);
|
||||
});
|
||||
const resolvers = withResolvers();
|
||||
this.$$tickers.push({resolvers, ticker});
|
||||
return resolvers.promise;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
|
@ -21,8 +18,14 @@ export default class Ticking extends Component {
|
|||
}
|
||||
|
||||
tick(elapsed) {
|
||||
for (const ticker of this.$$tickers) {
|
||||
ticker.tick(elapsed);
|
||||
for (let i = 0; i < this.$$tickers.length; ++i) {
|
||||
const {resolvers, ticker} = this.$$tickers[i];
|
||||
const result = ticker.tick(elapsed);
|
||||
if (result.done) {
|
||||
resolvers.resolve();
|
||||
this.$$tickers.splice(i, 1);
|
||||
i -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -98,9 +98,9 @@ class LayerProxy {
|
|||
get layer() {
|
||||
return this.instance.layers[this.index];
|
||||
}
|
||||
async load() {
|
||||
load() {
|
||||
this.$$sourceJson = this.layer.source
|
||||
? await this.Component.ecs.readJson(this.layer.source)
|
||||
? this.Component.ecs.readJson(this.layer.source)
|
||||
: {};
|
||||
}
|
||||
get source() {
|
||||
|
@ -139,7 +139,7 @@ class LayerProxy {
|
|||
}
|
||||
|
||||
export default class TileLayers extends Component {
|
||||
async createMany(entities) {
|
||||
createMany(entities) {
|
||||
for (const [, {layerChange, layers}] of entities) {
|
||||
if (layers) {
|
||||
for (const layer of layers) {
|
||||
|
@ -164,7 +164,7 @@ export default class TileLayers extends Component {
|
|||
}
|
||||
return super.createMany(entities);
|
||||
}
|
||||
async updateMany(entities) {
|
||||
updateMany(entities) {
|
||||
for (const [, {layers}] of entities) {
|
||||
if (layers) {
|
||||
for (const layer of layers) {
|
||||
|
@ -179,7 +179,7 @@ export default class TileLayers extends Component {
|
|||
}
|
||||
}
|
||||
}
|
||||
await super.updateMany(entities);
|
||||
super.updateMany(entities);
|
||||
for (const [id, {layerChange}] of entities) {
|
||||
if (layerChange) {
|
||||
const component = this.get(id);
|
||||
|
@ -208,10 +208,10 @@ export default class TileLayers extends Component {
|
|||
}
|
||||
}
|
||||
}
|
||||
async load(instance) {
|
||||
load(instance) {
|
||||
for (const index in instance.layers) {
|
||||
instance.$$layersProxies[index] = new LayerProxy(instance, this, index);
|
||||
await instance.$$layersProxies[index].load();
|
||||
instance.$$layersProxies[index].load();
|
||||
}
|
||||
}
|
||||
mergeDiff(original, update) {
|
||||
|
|
|
@ -4,14 +4,14 @@ import Ecs from '@/ecs/ecs.js';
|
|||
|
||||
import TileLayers from './tile-layers.js';
|
||||
|
||||
test('creates hulls', async () => {
|
||||
test('creates hulls', () => {
|
||||
const Component = new TileLayers(new Ecs());
|
||||
const data = Array(64).fill(0);
|
||||
data[9] = 1;
|
||||
data[10] = 1;
|
||||
data[17] = 1;
|
||||
data[18] = 1;
|
||||
const layers = await Component.create(1, {
|
||||
const layers = Component.create(1, {
|
||||
layers: [
|
||||
{
|
||||
area: {x: 8, y: 8},
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import Component from '@/ecs/component.js';
|
||||
import * as Math from '@/util/math.js';
|
||||
import Ticker from '@/util/ticker.js';
|
||||
|
||||
export const DamageTypes = {
|
||||
PAIN: 0,
|
||||
|
@ -21,12 +23,20 @@ export default class Vulnerable extends Component {
|
|||
const Component = this;
|
||||
return class VulnerableInstance extends super.instanceFromSchema() {
|
||||
id = 0;
|
||||
locked = new Set();
|
||||
Types = DamageTypes;
|
||||
damage(specification) {
|
||||
damage(fullSpecification) {
|
||||
if (this.isInvulnerable) {
|
||||
return;
|
||||
}
|
||||
const {Alive} = Component.ecs.get(this.entity);
|
||||
const {
|
||||
lockout,
|
||||
...specification
|
||||
} = fullSpecification;
|
||||
if (lockout && this.locked.has(lockout.subject)) {
|
||||
return;
|
||||
}
|
||||
const {Alive, Forces, Position, Ticking} = Component.ecs.get(this.entity);
|
||||
if (Alive) {
|
||||
switch (specification.type) {
|
||||
case DamageTypes.HEALING:
|
||||
|
@ -35,6 +45,28 @@ export default class Vulnerable extends Component {
|
|||
}
|
||||
}
|
||||
}
|
||||
if (specification.knockback) {
|
||||
const diff = Math.normalizeVector({
|
||||
x: Position.x - specification.knockback.origin.x,
|
||||
y: Position.y - specification.knockback.origin.y,
|
||||
});
|
||||
Forces.applyImpulse({
|
||||
x: diff.x * specification.knockback.magnitude,
|
||||
y: diff.y * specification.knockback.magnitude,
|
||||
});
|
||||
}
|
||||
if (lockout) {
|
||||
let {duration, subject} = lockout;
|
||||
const self = this;
|
||||
self.locked.add(subject);
|
||||
Ticking.add(new Ticker(function*() {
|
||||
while (duration > 0) {
|
||||
const elapsed = yield;
|
||||
duration -= elapsed;
|
||||
}
|
||||
self.locked.delete(subject);
|
||||
}));
|
||||
}
|
||||
Component.markChange(
|
||||
this.entity,
|
||||
'damage',
|
||||
|
|
|
@ -17,10 +17,10 @@ export default class Wielder extends Component {
|
|||
let script = state ? startInstance : stopInstance;
|
||||
if (script) {
|
||||
script = script.clone();
|
||||
script.context.ecs = ecs;
|
||||
script.context.item = activeItem;
|
||||
script.context.where = where;
|
||||
script.context.wielder = entity;
|
||||
script.locals.ecs = ecs;
|
||||
script.locals.item = activeItem;
|
||||
script.locals.where = where;
|
||||
script.locals.wielder = entity;
|
||||
Ticking.add(script.ticker());
|
||||
}
|
||||
}
|
||||
|
|
121
app/ecs/ecs.js
121
app/ecs/ecs.js
|
@ -1,15 +1,10 @@
|
|||
import {Encoder, Decoder} from '@msgpack/msgpack';
|
||||
import {LRUCache} from 'lru-cache';
|
||||
|
||||
import {withResolvers} from '@/util/promise.js';
|
||||
import Script from '@/util/script.js';
|
||||
|
||||
import EntityFactory from './entity-factory.js';
|
||||
|
||||
const cache = new LRUCache({
|
||||
max: 128,
|
||||
});
|
||||
|
||||
const decoder = new Decoder();
|
||||
const encoder = new Encoder();
|
||||
|
||||
|
@ -58,7 +53,7 @@ export default class Ecs {
|
|||
});
|
||||
}
|
||||
|
||||
async apply(patch) {
|
||||
apply(patch) {
|
||||
const creating = [];
|
||||
const destroying = new Set();
|
||||
const inserting = [];
|
||||
|
@ -111,17 +106,15 @@ export default class Ecs {
|
|||
creating.push([entityId, componentsToUpdate]);
|
||||
}
|
||||
}
|
||||
const promises = [];
|
||||
if (inserting.length > 0) {
|
||||
promises.push(this.insertMany(inserting));
|
||||
this.insertMany(inserting);
|
||||
}
|
||||
if (updating.length > 0) {
|
||||
promises.push(this.updateMany(updating));
|
||||
this.updateMany(updating);
|
||||
}
|
||||
if (creating.length > 0) {
|
||||
promises.push(this.createManySpecific(creating));
|
||||
this.createManySpecific(creating);
|
||||
}
|
||||
await Promise.all(promises);
|
||||
if (destroying.size > 0) {
|
||||
this.destroyMany(destroying);
|
||||
}
|
||||
|
@ -175,17 +168,17 @@ export default class Ecs {
|
|||
};
|
||||
}
|
||||
|
||||
async create(components = {}) {
|
||||
const [entityId] = await this.createMany([components]);
|
||||
create(components = {}) {
|
||||
const [entityId] = this.createMany([components]);
|
||||
return entityId;
|
||||
}
|
||||
|
||||
async createDetached(components = {}) {
|
||||
const [entityId] = await this.createManyDetached([components]);
|
||||
createDetached(components = {}) {
|
||||
const [entityId] = this.createManyDetached([components]);
|
||||
return entityId;
|
||||
}
|
||||
|
||||
async createMany(componentsList) {
|
||||
createMany(componentsList) {
|
||||
const specificsList = [];
|
||||
for (const components of componentsList) {
|
||||
specificsList.push([this.$$caret, components]);
|
||||
|
@ -194,7 +187,7 @@ export default class Ecs {
|
|||
return this.createManySpecific(specificsList);
|
||||
}
|
||||
|
||||
async createManyDetached(componentsList) {
|
||||
createManyDetached(componentsList) {
|
||||
const specificsList = [];
|
||||
for (const components of componentsList) {
|
||||
specificsList.push([this.$$caret, components]);
|
||||
|
@ -205,14 +198,33 @@ export default class Ecs {
|
|||
return this.createManySpecific(specificsList);
|
||||
}
|
||||
|
||||
async createManySpecific(specificsList) {
|
||||
if (0 === specificsList.length) {
|
||||
return;
|
||||
}
|
||||
createManySpecific(specificsList) {
|
||||
const entityIds = new Set();
|
||||
if (0 === specificsList.length) {
|
||||
return entityIds;
|
||||
}
|
||||
const creating = {};
|
||||
const extendedSpecificsList = [];
|
||||
for (let i = 0; i < specificsList.length; i++) {
|
||||
const [entityId, components] = specificsList[i];
|
||||
if (!specificsList[i][1].$$extends) {
|
||||
extendedSpecificsList.push(specificsList[i]);
|
||||
continue;
|
||||
}
|
||||
const [entityId, rawComponents] = specificsList[i];
|
||||
let {
|
||||
$$extends,
|
||||
...components
|
||||
} = rawComponents;
|
||||
if ($$extends) {
|
||||
components = {
|
||||
...this.readJson($$extends),
|
||||
...components,
|
||||
};
|
||||
}
|
||||
extendedSpecificsList.push([entityId, components]);
|
||||
}
|
||||
for (let i = 0; i < extendedSpecificsList.length; i++) {
|
||||
const [entityId, components] = extendedSpecificsList[i];
|
||||
if (!this.$$detached.has(entityId)) {
|
||||
this.deferredChanges[entityId] = [];
|
||||
}
|
||||
|
@ -231,13 +243,11 @@ export default class Ecs {
|
|||
}
|
||||
this.markChange(entityId, components);
|
||||
}
|
||||
const promises = [];
|
||||
for (const i in creating) {
|
||||
promises.push(this.Components[i].createMany(creating[i]));
|
||||
this.Components[i].createMany(creating[i])
|
||||
}
|
||||
await Promise.all(promises);
|
||||
for (let i = 0; i < specificsList.length; i++) {
|
||||
const [entityId, components] = specificsList[i];
|
||||
for (let i = 0; i < extendedSpecificsList.length; i++) {
|
||||
const [entityId, components] = extendedSpecificsList[i];
|
||||
this.rebuild(entityId, () => Object.keys(components));
|
||||
if (this.$$detached.has(entityId)) {
|
||||
continue;
|
||||
|
@ -248,8 +258,8 @@ export default class Ecs {
|
|||
return entityIds;
|
||||
}
|
||||
|
||||
async createSpecific(entityId, components) {
|
||||
const [created] = await this.createManySpecific([[entityId, components]]);
|
||||
createSpecific(entityId, components) {
|
||||
const [created] = this.createManySpecific([[entityId, components]]);
|
||||
return created;
|
||||
}
|
||||
|
||||
|
@ -272,7 +282,7 @@ export default class Ecs {
|
|||
}
|
||||
}
|
||||
|
||||
static async deserialize(ecs, view) {
|
||||
static deserialize(ecs, view) {
|
||||
const componentNames = Object.keys(ecs.Components);
|
||||
const {entities, systems} = decoder.decode(view.buffer);
|
||||
for (const system of systems) {
|
||||
|
@ -294,7 +304,7 @@ export default class Ecs {
|
|||
]);
|
||||
}
|
||||
ecs.$$caret = max + 1;
|
||||
await ecs.createManySpecific(specifics);
|
||||
ecs.createManySpecific(specifics);
|
||||
return ecs;
|
||||
}
|
||||
|
||||
|
@ -352,11 +362,11 @@ export default class Ecs {
|
|||
return this.$$entities[entityId];
|
||||
}
|
||||
|
||||
async insert(entityId, components) {
|
||||
insert(entityId, components) {
|
||||
return this.insertMany([[entityId, components]]);
|
||||
}
|
||||
|
||||
async insertMany(entities) {
|
||||
insertMany(entities) {
|
||||
const inserting = {};
|
||||
const unique = new Set();
|
||||
for (const [entityId, components] of entities) {
|
||||
|
@ -371,11 +381,9 @@ export default class Ecs {
|
|||
unique.add(entityId);
|
||||
this.markChange(entityId, diff);
|
||||
}
|
||||
const promises = [];
|
||||
for (const componentName in inserting) {
|
||||
promises.push(this.Components[componentName].insertMany(inserting[componentName]));
|
||||
this.Components[componentName].insertMany(inserting[componentName]);
|
||||
}
|
||||
await Promise.all(promises);
|
||||
for (const [entityId, components] of entities) {
|
||||
this.rebuild(entityId, (componentNames) => [...new Set(componentNames.concat(Object.keys(components)))]);
|
||||
}
|
||||
|
@ -423,35 +431,16 @@ export default class Ecs {
|
|||
}
|
||||
|
||||
readJson(uri) {
|
||||
const key = ['$$json', uri].join(':');
|
||||
if (!cache.has(key)) {
|
||||
const buffer = this.readAsset(uri);
|
||||
const json = buffer.byteLength > 0
|
||||
? JSON.parse(textDecoder.decode(buffer))
|
||||
: {};
|
||||
cache.set(key, json);
|
||||
}
|
||||
return cache.get(key);
|
||||
const buffer = this.readAsset(uri);
|
||||
const {$$extends, ...json} = buffer.byteLength > 0 ? JSON.parse(textDecoder.decode(buffer)) : {};
|
||||
return {
|
||||
...json,
|
||||
...$$extends && this.readJson($$extends),
|
||||
};
|
||||
}
|
||||
|
||||
readScript(uriOrCode, context = {}) {
|
||||
if (!uriOrCode) {
|
||||
return undefined;
|
||||
}
|
||||
let code = '';
|
||||
if (!uriOrCode.startsWith('/')) {
|
||||
code = uriOrCode;
|
||||
}
|
||||
else {
|
||||
const buffer = this.readAsset(uriOrCode);
|
||||
if (buffer.byteLength > 0) {
|
||||
code = textDecoder.decode(buffer);
|
||||
}
|
||||
}
|
||||
if (!code) {
|
||||
return undefined;
|
||||
}
|
||||
return Script.fromCode(code, context);
|
||||
readScript(path, locals = {}) {
|
||||
return Script.load(path, locals);
|
||||
}
|
||||
|
||||
rebuild(entityId, componentNames) {
|
||||
|
@ -588,7 +577,7 @@ export default class Ecs {
|
|||
};
|
||||
}
|
||||
|
||||
async updateMany(entities) {
|
||||
updateMany(entities) {
|
||||
const updating = {};
|
||||
const unique = new Set();
|
||||
for (const [entityId, components] of entities) {
|
||||
|
@ -600,11 +589,9 @@ export default class Ecs {
|
|||
}
|
||||
unique.add(entityId);
|
||||
}
|
||||
const promises = [];
|
||||
for (const componentName in updating) {
|
||||
promises.push(this.Components[componentName].updateMany(updating[componentName]));
|
||||
this.Components[componentName].updateMany(updating[componentName]);
|
||||
}
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import {expect, test} from 'vitest';
|
||||
|
||||
import Component from './component.js';
|
||||
import Ecs from './ecs.js';
|
||||
import System from './system.js';
|
||||
import {wrapComponents} from './test-helper.js';
|
||||
|
@ -19,24 +18,6 @@ const {
|
|||
Name,
|
||||
} = Components;
|
||||
|
||||
function asyncTimesTwo(x) {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve(x * 2)
|
||||
}, 5);
|
||||
});
|
||||
}
|
||||
|
||||
class Async extends Component {
|
||||
static componentName = 'Async';
|
||||
static properties = {
|
||||
foo: {type: 'uint8'},
|
||||
};
|
||||
async load(instance) {
|
||||
instance.foo = await asyncTimesTwo(instance.foo);
|
||||
}
|
||||
}
|
||||
|
||||
test('activates and deactivates systems at runtime', () => {
|
||||
let oneCount = 0;
|
||||
let twoCount = 0;
|
||||
|
@ -77,33 +58,33 @@ test('activates and deactivates systems at runtime', () => {
|
|||
.to.equal(2);
|
||||
});
|
||||
|
||||
test('creates entities with components', async () => {
|
||||
test('creates entities with components', () => {
|
||||
const ecs = new Ecs({Components: {Empty, Position}});
|
||||
const entity = await ecs.create({Empty: {}, Position: {y: 128}});
|
||||
const entity = ecs.create({Empty: {}, Position: {y: 128}});
|
||||
expect(JSON.stringify(ecs.get(entity)))
|
||||
.to.deep.equal(JSON.stringify({Empty: {}, Position: {y: 128}}));
|
||||
.to.deep.equal(JSON.stringify({Empty: {}, Position: {x: 32, y: 128, z: 0}}));
|
||||
});
|
||||
|
||||
test("removes entities' components", async () => {
|
||||
test("removes entities' components", () => {
|
||||
const ecs = new Ecs({Components: {Empty, Position}});
|
||||
const entity = await ecs.create({Empty: {}, Position: {y: 128}});
|
||||
const entity = ecs.create({Empty: {}, Position: {y: 128}});
|
||||
ecs.remove(entity, ['Position']);
|
||||
expect(JSON.stringify(ecs.get(entity)))
|
||||
.to.deep.equal(JSON.stringify({Empty: {}}));
|
||||
});
|
||||
|
||||
test('gets entities', async () => {
|
||||
test('gets entities', () => {
|
||||
const ecs = new Ecs({Components: {Empty, Position}});
|
||||
const entity = await ecs.create({Empty: {}, Position: {y: 128}});
|
||||
const entity = ecs.create({Empty: {}, Position: {y: 128}});
|
||||
expect(JSON.stringify(ecs.get(entity)))
|
||||
.to.deep.equal(JSON.stringify({Empty: {}, Position: {y: 128}}));
|
||||
.to.deep.equal(JSON.stringify({Empty: {}, Position: {x: 32, y: 128, z: 0}}));
|
||||
});
|
||||
|
||||
test('destroys entities', async () => {
|
||||
test('destroys entities', () => {
|
||||
const ecs = new Ecs({Components: {Empty, Position}});
|
||||
const entity = await ecs.create({Empty: {}, Position: {y: 128}});
|
||||
const entity = ecs.create({Empty: {}, Position: {y: 128}});
|
||||
expect(JSON.stringify(ecs.get(entity)))
|
||||
.to.deep.equal(JSON.stringify({Empty: {}, Position: {y: 128}}));
|
||||
.to.deep.equal(JSON.stringify({Empty: {}, Position: {x: 32, y: 128, z: 0}}));
|
||||
expect(ecs.get(entity))
|
||||
.to.not.be.undefined;
|
||||
ecs.destroyMany(new Set([entity]));
|
||||
|
@ -115,18 +96,18 @@ test('destroys entities', async () => {
|
|||
.to.throw();
|
||||
});
|
||||
|
||||
test('inserts components into entities', async () => {
|
||||
test('inserts components into entities', () => {
|
||||
const ecs = new Ecs({Components: {Empty, Position}});
|
||||
const entity = await ecs.create({Empty: {}});
|
||||
await ecs.insert(entity, {Position: {y: 128}});
|
||||
const entity = ecs.create({Empty: {}});
|
||||
ecs.insert(entity, {Position: {y: 128}});
|
||||
expect(JSON.stringify(ecs.get(entity)))
|
||||
.to.deep.equal(JSON.stringify({Empty: {}, Position: {y: 128}}));
|
||||
await ecs.insert(entity, {Position: {y: 64}});
|
||||
.to.deep.equal(JSON.stringify({Empty: {}, Position: {x: 32, y: 128, z: 0}}));
|
||||
ecs.insert(entity, {Position: {y: 64}});
|
||||
expect(JSON.stringify(ecs.get(entity)))
|
||||
.to.deep.equal(JSON.stringify({Empty: {}, Position: {y: 64}}));
|
||||
.to.deep.equal(JSON.stringify({Empty: {}, Position: {x: 32, y: 64, z: 0}}));
|
||||
});
|
||||
|
||||
test('ticks systems', async () => {
|
||||
test('ticks systems', () => {
|
||||
const ecs = new Ecs({
|
||||
Components: {Momentum, Position},
|
||||
Systems: {
|
||||
|
@ -150,7 +131,7 @@ test('ticks systems', async () => {
|
|||
},
|
||||
});
|
||||
ecs.system('Physics').active = true;
|
||||
const entity = await ecs.create({Momentum: {}, Position: {y: 128}});
|
||||
const entity = ecs.create({Momentum: {}, Position: {y: 128}});
|
||||
const position = JSON.stringify(ecs.get(entity).Position);
|
||||
ecs.tick(1);
|
||||
expect(JSON.stringify(ecs.get(entity).Position))
|
||||
|
@ -158,10 +139,10 @@ test('ticks systems', async () => {
|
|||
ecs.get(1).Momentum.y = 30;
|
||||
ecs.tick(1);
|
||||
expect(JSON.stringify(ecs.get(entity).Position))
|
||||
.to.deep.equal(JSON.stringify({y: 128 + 30}));
|
||||
.to.deep.equal(JSON.stringify({x: 32, y: 128 + 30, z: 0}));
|
||||
});
|
||||
|
||||
test('schedules entities to be deleted when ticking systems', async () => {
|
||||
test('schedules entities to be deleted when ticking systems', () => {
|
||||
const ecs = new Ecs({
|
||||
Components: {Empty},
|
||||
Systems: {
|
||||
|
@ -180,7 +161,7 @@ test('schedules entities to be deleted when ticking systems', async () => {
|
|||
},
|
||||
});
|
||||
ecs.system('Despawn').active = true;
|
||||
await ecs.create({Empty: {}});
|
||||
ecs.create({Empty: {}});
|
||||
ecs.tick(1);
|
||||
expect(Array.from(ecs.system('Despawn').select('default')))
|
||||
.to.have.lengthOf(0);
|
||||
|
@ -188,7 +169,7 @@ test('schedules entities to be deleted when ticking systems', async () => {
|
|||
.to.be.undefined;
|
||||
});
|
||||
|
||||
test('skips indexing detached entities', async () => {
|
||||
test('skips indexing detached entities', () => {
|
||||
const ecs = new Ecs({
|
||||
Components: {Empty},
|
||||
Systems: {
|
||||
|
@ -203,7 +184,7 @@ test('skips indexing detached entities', async () => {
|
|||
});
|
||||
const {$$map: map} = ecs.system('Indexer').queries.default;
|
||||
ecs.system('Indexer').active = true;
|
||||
const attached = await ecs.create({Empty: {}});
|
||||
const attached = ecs.create({Empty: {}});
|
||||
ecs.tick(0);
|
||||
expect(Array.from(map.keys()))
|
||||
.to.deep.equal([attached]);
|
||||
|
@ -211,7 +192,7 @@ test('skips indexing detached entities', async () => {
|
|||
ecs.tick(0);
|
||||
expect(Array.from(map.keys()))
|
||||
.to.deep.equal([]);
|
||||
const detached = await ecs.createDetached({Empty: {}});
|
||||
const detached = ecs.createDetached({Empty: {}});
|
||||
ecs.tick(0);
|
||||
expect(Array.from(map.keys()))
|
||||
.to.deep.equal([]);
|
||||
|
@ -221,20 +202,20 @@ test('skips indexing detached entities', async () => {
|
|||
.to.deep.equal([]);
|
||||
});
|
||||
|
||||
test('generates diffs for entity creation', async () => {
|
||||
test('generates diffs for entity creation', () => {
|
||||
const ecs = new Ecs();
|
||||
let entity;
|
||||
entity = await ecs.create();
|
||||
entity = ecs.create();
|
||||
expect(ecs.diff)
|
||||
.to.deep.equal({[entity]: {}});
|
||||
});
|
||||
|
||||
test('generates diffs for adding and removing components', async () => {
|
||||
test('generates diffs for adding and removing components', () => {
|
||||
const ecs = new Ecs({Components: {Position}});
|
||||
let entity;
|
||||
entity = await ecs.create();
|
||||
entity = ecs.create();
|
||||
ecs.setClean();
|
||||
await ecs.insert(entity, {Position: {x: 64}});
|
||||
ecs.insert(entity, {Position: {x: 64}});
|
||||
expect(ecs.diff)
|
||||
.to.deep.equal({[entity]: {Position: {x: 64}}});
|
||||
ecs.setClean();
|
||||
|
@ -245,10 +226,10 @@ test('generates diffs for adding and removing components', async () => {
|
|||
.to.deep.equal({[entity]: {Position: false}});
|
||||
});
|
||||
|
||||
test('generates diffs for empty components', async () => {
|
||||
test('generates diffs for empty components', () => {
|
||||
const ecs = new Ecs({Components: {Empty}});
|
||||
let entity;
|
||||
entity = await ecs.create({Empty: {}});
|
||||
entity = ecs.create({Empty: {}});
|
||||
expect(ecs.diff)
|
||||
.to.deep.equal({[entity]: {Empty: {}}});
|
||||
ecs.setClean();
|
||||
|
@ -257,10 +238,10 @@ test('generates diffs for empty components', async () => {
|
|||
.to.deep.equal({[entity]: {Empty: false}});
|
||||
});
|
||||
|
||||
test('generates diffs for entity mutations', async () => {
|
||||
test('generates diffs for entity mutations', () => {
|
||||
const ecs = new Ecs({Components: {Position}});
|
||||
let entity;
|
||||
entity = await ecs.create({Position: {}});
|
||||
entity = ecs.create({Position: {}});
|
||||
ecs.setClean();
|
||||
ecs.get(entity).Position.x = 128;
|
||||
expect(ecs.diff)
|
||||
|
@ -270,13 +251,13 @@ test('generates diffs for entity mutations', async () => {
|
|||
.to.deep.equal({});
|
||||
});
|
||||
|
||||
test('generates no diffs for detached entities', async () => {
|
||||
test('generates no diffs for detached entities', () => {
|
||||
const ecs = new Ecs({Components: {Position}});
|
||||
let entity;
|
||||
entity = await ecs.createDetached();
|
||||
entity = ecs.createDetached();
|
||||
expect(ecs.diff)
|
||||
.to.deep.equal({});
|
||||
await ecs.insert(entity, {Position: {x: 64}});
|
||||
ecs.insert(entity, {Position: {x: 64}});
|
||||
expect(ecs.diff)
|
||||
.to.deep.equal({});
|
||||
ecs.get(entity).Position.x = 128;
|
||||
|
@ -287,22 +268,22 @@ test('generates no diffs for detached entities', async () => {
|
|||
.to.deep.equal({});
|
||||
});
|
||||
|
||||
test('generates coalesced diffs for components', async () => {
|
||||
test('generates coalesced diffs for components', () => {
|
||||
const ecs = new Ecs({Components: {Position}});
|
||||
let entity;
|
||||
entity = await ecs.create({Position});
|
||||
entity = ecs.create({Position});
|
||||
ecs.remove(entity, ['Position']);
|
||||
expect(ecs.diff)
|
||||
.to.deep.equal({[entity]: {Position: false}});
|
||||
await ecs.insert(entity, {Position: {}});
|
||||
ecs.insert(entity, {Position: {}});
|
||||
expect(ecs.diff)
|
||||
.to.deep.equal({[entity]: {Position: {}}});
|
||||
});
|
||||
|
||||
test('generates coalesced diffs for mutations', async () => {
|
||||
test('generates coalesced diffs for mutations', () => {
|
||||
const ecs = new Ecs({Components: {Position}});
|
||||
let entity;
|
||||
entity = await ecs.create({Position});
|
||||
entity = ecs.create({Position});
|
||||
ecs.setClean();
|
||||
ecs.get(entity).Position.x = 128;
|
||||
ecs.get(entity).Position.x = 256;
|
||||
|
@ -311,10 +292,10 @@ test('generates coalesced diffs for mutations', async () => {
|
|||
.to.deep.equal({[entity]: {Position: {x: 512}}});
|
||||
});
|
||||
|
||||
test('generates diffs for deletions', async () => {
|
||||
test('generates diffs for deletions', () => {
|
||||
const ecs = new Ecs();
|
||||
let entity;
|
||||
entity = await ecs.create();
|
||||
entity = ecs.create();
|
||||
ecs.setClean();
|
||||
ecs.destroy(entity);
|
||||
ecs.tick(0);
|
||||
|
@ -322,66 +303,66 @@ test('generates diffs for deletions', async () => {
|
|||
.to.deep.equal({[entity]: false});
|
||||
});
|
||||
|
||||
test('applies creation patches', async () => {
|
||||
test('applies creation patches', () => {
|
||||
const ecs = new Ecs({Components: {Position}});
|
||||
await ecs.apply({16: {Position: {x: 64}}});
|
||||
ecs.apply({16: {Position: {x: 64}}});
|
||||
expect(Array.from(ecs.entities).length)
|
||||
.to.equal(1);
|
||||
expect(ecs.get(16).Position.x)
|
||||
.to.equal(64);
|
||||
});
|
||||
|
||||
test('applies update patches', async () => {
|
||||
test('applies update patches', () => {
|
||||
const ecs = new Ecs({Components: {Position}});
|
||||
await ecs.createSpecific(16, {Position: {x: 64}});
|
||||
await ecs.apply({16: {Position: {x: 128}}});
|
||||
ecs.createSpecific(16, {Position: {x: 64}});
|
||||
ecs.apply({16: {Position: {x: 128}}});
|
||||
expect(Object.keys(ecs.$$entities).length)
|
||||
.to.equal(1);
|
||||
expect(ecs.get(16).Position.x)
|
||||
.to.equal(128);
|
||||
});
|
||||
|
||||
test('applies entity deletion patches', async () => {
|
||||
test('applies entity deletion patches', () => {
|
||||
const ecs = new Ecs({Components: {Position}});
|
||||
await ecs.createSpecific(16, {Position: {x: 64}});
|
||||
await ecs.apply({16: false});
|
||||
ecs.createSpecific(16, {Position: {x: 64}});
|
||||
ecs.apply({16: false});
|
||||
expect(Array.from(ecs.entities).length)
|
||||
.to.equal(0);
|
||||
});
|
||||
|
||||
test('applies component deletion patches', async () => {
|
||||
test('applies component deletion patches', () => {
|
||||
const ecs = new Ecs({Components: {Empty, Position}});
|
||||
await ecs.createSpecific(16, {Empty: {}, Position: {x: 64}});
|
||||
ecs.createSpecific(16, {Empty: {}, Position: {x: 64}});
|
||||
expect(ecs.get(16).constructor.componentNames)
|
||||
.to.deep.equal(['Empty', 'Position']);
|
||||
await ecs.apply({16: {Empty: false}});
|
||||
ecs.apply({16: {Empty: false}});
|
||||
expect(ecs.get(16).constructor.componentNames)
|
||||
.to.deep.equal(['Position']);
|
||||
});
|
||||
|
||||
test('calculates entity size', async () => {
|
||||
test('calculates entity size', () => {
|
||||
const ecs = new Ecs({Components: {Empty, Position}});
|
||||
await ecs.createSpecific(1, {Empty: {}, Position: {}});
|
||||
ecs.createSpecific(1, {Empty: {}, Position: {}});
|
||||
// ID + # of components + Empty + Position + x + y + z
|
||||
// 4 + 2 + 2 + 4 + 2 + 4 + 4 + 4 + 4 = 30
|
||||
expect(ecs.get(1).size())
|
||||
.to.equal(30);
|
||||
});
|
||||
|
||||
test('serializes and deserializes', async () => {
|
||||
test('serializes and deserializes', () => {
|
||||
const ecs = new Ecs({Components: {Empty, Name, Position}});
|
||||
await ecs.createSpecific(1, {Empty: {}, Position: {x: 64}});
|
||||
await ecs.createSpecific(16, {Name: {name: 'foobar'}, Position: {x: 128}});
|
||||
ecs.createSpecific(1, {Empty: {}, Position: {x: 64}});
|
||||
ecs.createSpecific(16, {Name: {name: 'foobar'}, Position: {x: 128}});
|
||||
expect(ecs.toJSON())
|
||||
.to.deep.equal({
|
||||
entities: {
|
||||
1: {Empty: {}, Position: {x: 64}},
|
||||
16: {Name: {name: 'foobar'}, Position: {x: 128}},
|
||||
1: {Empty: {}, Position: {x: 64, y: 0, z: 0}},
|
||||
16: {Name: {name: 'foobar'}, Position: {x: 128, y: 0, z: 0}},
|
||||
},
|
||||
systems: [],
|
||||
});
|
||||
const view = Ecs.serialize(ecs);
|
||||
const deserialized = await Ecs.deserialize(
|
||||
const deserialized = Ecs.deserialize(
|
||||
new Ecs({Components: {Empty, Name, Position}}),
|
||||
view,
|
||||
);
|
||||
|
@ -392,21 +373,21 @@ test('serializes and deserializes', async () => {
|
|||
expect(deserialized.get(16).constructor.componentNames)
|
||||
.to.deep.equal(['Name', 'Position']);
|
||||
expect(JSON.stringify(deserialized.get(1)))
|
||||
.to.equal(JSON.stringify({Empty: {}, Position: {x: 64}}))
|
||||
.to.equal(JSON.stringify({Empty: {}, Position: {x: 64, y: 0, z: 0}}))
|
||||
expect(JSON.stringify(deserialized.get(1).Position))
|
||||
.to.equal(JSON.stringify({x: 64}));
|
||||
.to.equal(JSON.stringify({x: 64, y: 0, z: 0}));
|
||||
expect(JSON.stringify(deserialized.get(16).Position))
|
||||
.to.equal(JSON.stringify({x: 128}));
|
||||
.to.equal(JSON.stringify({x: 128, y: 0, z: 0}));
|
||||
expect(deserialized.get(16).Name.name)
|
||||
.to.equal('foobar');
|
||||
});
|
||||
|
||||
test('deserializes from compatible ECS', async () => {
|
||||
test('deserializes from compatible ECS', () => {
|
||||
const ecs = new Ecs({Components: {Empty, Name, Position}});
|
||||
await ecs.createSpecific(1, {Empty: {}, Position: {x: 64}});
|
||||
await ecs.createSpecific(16, {Name: {name: 'foobar'}, Position: {x: 128}});
|
||||
ecs.createSpecific(1, {Empty: {}, Position: {x: 64}});
|
||||
ecs.createSpecific(16, {Name: {name: 'foobar'}, Position: {x: 128}});
|
||||
const view = Ecs.serialize(ecs);
|
||||
const deserialized = await Ecs.deserialize(
|
||||
const deserialized = Ecs.deserialize(
|
||||
new Ecs({Components: {Empty, Name}}),
|
||||
view,
|
||||
);
|
||||
|
@ -415,30 +396,3 @@ test('deserializes from compatible ECS', async () => {
|
|||
expect(deserialized.get(16).toJSON())
|
||||
.to.deep.equal({Name: {name: 'foobar'}});
|
||||
});
|
||||
|
||||
test('creates entities asynchronously', async () => {
|
||||
const ecs = new Ecs({Components: {Async}});
|
||||
const entity = await ecs.create({Async: {foo: 64}});
|
||||
expect(ecs.get(entity).toJSON())
|
||||
.to.deep.equal({Async: {foo: 128}});
|
||||
});
|
||||
|
||||
test('inserts components asynchronously', async () => {
|
||||
const ecs = new Ecs({Components: {Async}});
|
||||
const entity = await ecs.create();
|
||||
await ecs.insert(entity, {Async: {foo: 64}});
|
||||
expect(ecs.get(entity).toJSON())
|
||||
.to.deep.equal({Async: {foo: 128}});
|
||||
});
|
||||
|
||||
test('deserializes asynchronously', async () => {
|
||||
const ecs = new Ecs({Components: {Async}});
|
||||
await ecs.createSpecific(16, {Async: {foo: 16}});
|
||||
const view = Ecs.serialize(ecs);
|
||||
const deserialized = await Ecs.deserialize(
|
||||
new Ecs({Components: {Async}}),
|
||||
view,
|
||||
);
|
||||
expect(deserialized.get(16).toJSON())
|
||||
.to.deep.equal({Async: {foo: 64}});
|
||||
});
|
||||
|
|
|
@ -13,7 +13,7 @@ const Components = wrapComponents([
|
|||
const ecsTest = test.extend({
|
||||
ecs: async ({}, use) => {
|
||||
const ecs = new Ecs({Components});
|
||||
await ecs.createManySpecific([
|
||||
ecs.createManySpecific([
|
||||
[1, {B: {}}],
|
||||
[2, {A: {}, B: {}, C: {}}],
|
||||
[3, {A: {}}],
|
||||
|
@ -23,7 +23,7 @@ const ecsTest = test.extend({
|
|||
},
|
||||
});
|
||||
|
||||
async function testQuery(ecs, parameters, expected) {
|
||||
function testQuery(ecs, parameters, expected) {
|
||||
const query = new Query(parameters, ecs);
|
||||
query.reindex([1, 2, 3]);
|
||||
expect(query.count)
|
||||
|
@ -34,21 +34,21 @@ async function testQuery(ecs, parameters, expected) {
|
|||
}
|
||||
}
|
||||
|
||||
ecsTest('can query all', async ({ecs}) => {
|
||||
await testQuery(ecs, [], [1, 2, 3]);
|
||||
ecsTest('can query all', ({ecs}) => {
|
||||
testQuery(ecs, [], [1, 2, 3]);
|
||||
});
|
||||
|
||||
ecsTest('can query some', async ({ecs}) => {
|
||||
await testQuery(ecs, ['A'], [2, 3]);
|
||||
await testQuery(ecs, ['A', 'B'], [2]);
|
||||
ecsTest('can query some', ({ecs}) => {
|
||||
testQuery(ecs, ['A'], [2, 3]);
|
||||
testQuery(ecs, ['A', 'B'], [2]);
|
||||
});
|
||||
|
||||
ecsTest('can query excluding', async ({ecs}) => {
|
||||
await testQuery(ecs, ['!A'], [1]);
|
||||
await testQuery(ecs, ['A', '!B'], [3]);
|
||||
ecsTest('can query excluding', ({ecs}) => {
|
||||
testQuery(ecs, ['!A'], [1]);
|
||||
testQuery(ecs, ['A', '!B'], [3]);
|
||||
});
|
||||
|
||||
ecsTest('can deindex', async ({ecs}) => {
|
||||
ecsTest('can deindex', ({ecs}) => {
|
||||
const query = new Query(['A'], ecs);
|
||||
query.reindex([1, 2, 3]);
|
||||
expect(query.count)
|
||||
|
@ -58,7 +58,7 @@ ecsTest('can deindex', async ({ecs}) => {
|
|||
.to.equal(1);
|
||||
});
|
||||
|
||||
ecsTest('can reindex', async ({ecs}) => {
|
||||
ecsTest('can reindex', ({ecs}) => {
|
||||
const query = new Query(['B'], ecs);
|
||||
query.reindex([1, 2]);
|
||||
expect(query.count)
|
||||
|
@ -69,7 +69,7 @@ ecsTest('can reindex', async ({ecs}) => {
|
|||
.to.equal(1);
|
||||
});
|
||||
|
||||
ecsTest('can select', async ({ecs}) => {
|
||||
ecsTest('can select', ({ecs}) => {
|
||||
const query = new Query(['A'], ecs);
|
||||
query.reindex([1, 2, 3]);
|
||||
const it = query.select();
|
||||
|
|
24
app/ecs/systems/harm.js
Normal file
24
app/ecs/systems/harm.js
Normal file
|
@ -0,0 +1,24 @@
|
|||
import {System} from '@/ecs/index.js';
|
||||
|
||||
export default class Harm extends System {
|
||||
|
||||
static queries() {
|
||||
return {
|
||||
default: ['Collider', 'Harmful'],
|
||||
};
|
||||
}
|
||||
|
||||
tick() {
|
||||
for (const entity of this.select('default')) {
|
||||
const intersecting = new Set();
|
||||
for (const [other] of entity.Collider.$$intersections) {
|
||||
intersecting.add(this.ecs.get(other.entity));
|
||||
}
|
||||
for (const other of intersecting) {
|
||||
entity.Harmful.harm(other);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -23,18 +23,16 @@ export default class IntegratePhysics extends System {
|
|||
if (!Forces || !Position) {
|
||||
return;
|
||||
}
|
||||
Position.lastX = Position.$$x;
|
||||
Position.$$x += elapsed * (Forces.$$impulseX);
|
||||
Position.lastY = Position.$$y;
|
||||
Position.$$y += elapsed * (Forces.$$impulseY);
|
||||
this.ecs.markChange(
|
||||
entity.id, {
|
||||
Position: {
|
||||
x: Position.$$x,
|
||||
y: Position.$$y,
|
||||
},
|
||||
},
|
||||
);
|
||||
const xd = elapsed * (Forces.$$impulseX + Forces.$$forceX);
|
||||
if (xd) {
|
||||
Position.lastX = Position.$$x;
|
||||
Position.x = Position.$$x + xd;
|
||||
}
|
||||
const yd = elapsed * (Forces.$$impulseY + Forces.$$forceY);;
|
||||
if (yd) {
|
||||
Position.lastY = Position.$$y;
|
||||
Position.y = Position.$$y + yd;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
22
app/ecs/systems/pickup-items.js
Normal file
22
app/ecs/systems/pickup-items.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
import {System} from '@/ecs/index.js';
|
||||
|
||||
export default class InventoryCloser extends System {
|
||||
|
||||
static queries() {
|
||||
return {
|
||||
default: ['Collider', 'ItemStack'],
|
||||
};
|
||||
}
|
||||
|
||||
tick() {
|
||||
for (const {Collider, ItemStack} of this.select('default')) {
|
||||
for (const [other] of Collider.$$intersections) {
|
||||
const otherEntity = this.ecs.get(other.entity);
|
||||
if (otherEntity.Grabber && otherEntity.Inventory) {
|
||||
ItemStack.pickup(otherEntity);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -27,16 +27,12 @@ export default class ResetForces extends System {
|
|||
if (!Forces) {
|
||||
return;
|
||||
}
|
||||
Forces.$$impulseX = 0;
|
||||
Forces.$$impulseY = 0;
|
||||
this.ecs.markChange(
|
||||
entity.id, {
|
||||
Forces: {
|
||||
impulseX: Forces.$$impulseX,
|
||||
impulseY: Forces.$$impulseY,
|
||||
},
|
||||
},
|
||||
);
|
||||
if (Forces.$$impulseX) {
|
||||
Forces.impulseX = 0;
|
||||
}
|
||||
if (Forces.$$impulseY) {
|
||||
Forces.impulseY = 0;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ export default class Emitter {
|
|||
this.ecs = ecs;
|
||||
this.scheduled = [];
|
||||
}
|
||||
async configure(entityId, {fields, shape}) {
|
||||
configure(entityId, {fields, shape}) {
|
||||
const entity = this.ecs.get(entityId);
|
||||
if (shape) {
|
||||
switch (shape.type) {
|
||||
|
@ -64,8 +64,8 @@ export default class Emitter {
|
|||
for (let i = 0; i < count * spurt; ++i) {
|
||||
specifications[i] = entity;
|
||||
}
|
||||
const stream = K.stream(async (emitter) => {
|
||||
const entityIds = await this.ecs.createManyDetached(specifications);
|
||||
const stream = K.stream((emitter) => {
|
||||
const entityIds = this.ecs.createManyDetached(specifications);
|
||||
if (0 === frequency) {
|
||||
this.ecs.attach(entityIds);
|
||||
for (const entityId of entityIds) {
|
||||
|
|
|
@ -28,6 +28,8 @@ addEventListener('message', (particle) => {
|
|||
.onEnd(() => {});
|
||||
});
|
||||
|
||||
postMessage(null);
|
||||
|
||||
let last = performance.now();
|
||||
function tick(now) {
|
||||
const elapsed = (now - last) / 1000;
|
||||
|
|
|
@ -15,7 +15,6 @@ export default function Entities({monopolizers, particleWorker}) {
|
|||
const [debug] = useDebug();
|
||||
const ecsRef = useEcs();
|
||||
const containerRef = useRef();
|
||||
const latestTick = useRef();
|
||||
const entities = useRef({});
|
||||
const pool = useRef([]);
|
||||
const mainEntityRef = useMainEntity();
|
||||
|
@ -64,7 +63,7 @@ export default function Entities({monopolizers, particleWorker}) {
|
|||
entities.current[key].setDebug(debug);
|
||||
}
|
||||
}, [debug]);
|
||||
usePacket('EcsChange', async () => {
|
||||
usePacket('EcsChange', () => {
|
||||
for (const id in entities.current) {
|
||||
entities.current[id].removeFromContainer();
|
||||
}
|
||||
|
@ -75,14 +74,12 @@ export default function Entities({monopolizers, particleWorker}) {
|
|||
if (!particleWorker) {
|
||||
return;
|
||||
}
|
||||
async function onMessage(diff) {
|
||||
function onMessage(diff) {
|
||||
if (!ecsRef.current) {
|
||||
return;
|
||||
}
|
||||
latestTick.current = Promise.resolve(latestTick.current).then(async () => {
|
||||
await ecsRef.current.apply(diff.data);
|
||||
updateEntities(diff.data, ecsRef.current);
|
||||
});
|
||||
ecsRef.current.apply(diff.data);
|
||||
updateEntities(diff.data, ecsRef.current);
|
||||
}
|
||||
particleWorker.addEventListener('message', onMessage);
|
||||
return () => {
|
||||
|
|
|
@ -106,10 +106,17 @@ export default class Entity {
|
|||
const {x, y} = this.entity.Position;
|
||||
this.container.x = x;
|
||||
this.container.y = y;
|
||||
this.container.zIndex = y;
|
||||
if (this.entity.Sprite?.zIndex === 65535) {
|
||||
this.container.zIndex = y;
|
||||
}
|
||||
}
|
||||
if (Sprite) {
|
||||
const {diffuse, normals} = this;
|
||||
if (!this.attached || 'zIndex' in Sprite) {
|
||||
this.container.zIndex = this.entity.Sprite?.zIndex === 65535
|
||||
? this.entity.Position.y
|
||||
: this.entity.Sprite.zIndex;
|
||||
}
|
||||
if (!this.attached || 'alpha' in Sprite) {
|
||||
diffuse.alpha = normals.alpha = this.entity.Sprite.alpha;
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ const TileLayerInternal = PixiComponent('TileLayer', {
|
|||
},
|
||||
applyProps: (container, {tileLayer: oldTileLayer}, props) => {
|
||||
const {asset, group, renderer, tileLayer} = props;
|
||||
const extless = tileLayer.source.slice('/resources/'.length, -'.json'.length);
|
||||
const extless = tileLayer.source.slice('/resources/'.length, -'.sprite.json'.length);
|
||||
const {textures} = asset;
|
||||
if (tileLayer === oldTileLayer) {
|
||||
return;
|
||||
|
|
|
@ -38,7 +38,6 @@ function Ui({disconnected}) {
|
|||
// Key input.
|
||||
const client = useClient();
|
||||
const chatInputRef = useRef();
|
||||
const latestTick = useRef();
|
||||
const gameRef = useRef();
|
||||
const pixiRef = useRef();
|
||||
const mainEntityRef = useMainEntity();
|
||||
|
@ -234,20 +233,18 @@ function Ui({disconnected}) {
|
|||
mainEntityRef,
|
||||
]);
|
||||
usePacket('EcsChange', onEcsChangePacket);
|
||||
const onTickPacket = useCallback(async (payload, client) => {
|
||||
const onTickPacket = useCallback((payload, client) => {
|
||||
if (!ecsRef.current || 0 === Object.keys(payload.ecs).length) {
|
||||
return;
|
||||
}
|
||||
latestTick.current = Promise.resolve(latestTick.current).then(async () => {
|
||||
try {
|
||||
await ecsRef.current.apply(payload.ecs);
|
||||
client.emitter.invoke(':Ecs', payload.ecs);
|
||||
}
|
||||
catch (error) {
|
||||
ecsRef.current = undefined;
|
||||
console.error('tick crash', error);
|
||||
}
|
||||
});
|
||||
try {
|
||||
ecsRef.current.apply(payload.ecs);
|
||||
client.emitter.invoke(':Ecs', payload.ecs);
|
||||
}
|
||||
catch (error) {
|
||||
ecsRef.current = undefined;
|
||||
console.error('tick crash', error);
|
||||
}
|
||||
}, [ecsRef]);
|
||||
usePacket('Tick', onTickPacket);
|
||||
const onEcsTick = useCallback((payload, ecs) => {
|
||||
|
@ -320,19 +317,19 @@ function Ui({disconnected}) {
|
|||
}, [mainEntityRef]);
|
||||
useEcsTick(onEcsTick);
|
||||
const onEcsTickParticles = useCallback((payload, ecs) => {
|
||||
if (!('1' in payload) || particleWorker) {
|
||||
return
|
||||
if (!payload[1]?.AreaSize) {
|
||||
return;
|
||||
}
|
||||
if (particleWorker) {
|
||||
particleWorker.terminate();
|
||||
}
|
||||
const localParticleWorker = new Worker(
|
||||
new URL('./particle-worker.js', import.meta.url),
|
||||
{type: 'module'},
|
||||
);
|
||||
localParticleWorker.postMessage(ecs.get(1).toJSON());
|
||||
setParticleWorker((particleWorker) => {
|
||||
if (particleWorker) {
|
||||
particleWorker.terminate();
|
||||
}
|
||||
return localParticleWorker;
|
||||
localParticleWorker.addEventListener('message', () => {
|
||||
localParticleWorker.postMessage(ecs.get(1).toJSON());
|
||||
setParticleWorker(localParticleWorker);
|
||||
});
|
||||
}, [particleWorker]);
|
||||
useEcsTick(onEcsTickParticles);
|
||||
|
|
|
@ -118,7 +118,7 @@ function Gen() {
|
|||
Math.ceil(area.x / CHUNK_SIZE) * Math.ceil(area.y / CHUNK_SIZE)
|
||||
).fill(0).map(() => ({})),
|
||||
data: Array(area.x * area.y).fill(1),
|
||||
source: '/resources/tileset.json',
|
||||
source: '/resources/tileset.sprite.json',
|
||||
tileSize: {x: 16, y: 16},
|
||||
};
|
||||
const tileGenerator = new Generator({
|
||||
|
|
|
@ -9,6 +9,7 @@ export default function createEcs(Ecs) {
|
|||
'Attract',
|
||||
'ResetForces',
|
||||
'ApplyControlMovement',
|
||||
'Harm',
|
||||
'IntegratePhysics',
|
||||
'ClampPositions',
|
||||
'PlantGrowth',
|
||||
|
@ -17,6 +18,7 @@ export default function createEcs(Ecs) {
|
|||
'MaintainColliderHash',
|
||||
'Colliders',
|
||||
'ControlDirection',
|
||||
'PickupItems',
|
||||
'SpriteDirection',
|
||||
'RunAnimations',
|
||||
'RunTickingPromises',
|
||||
|
|
|
@ -52,7 +52,7 @@ const Forest = new Generator({
|
|||
],
|
||||
});
|
||||
|
||||
export default async function createForest() {
|
||||
export default function createForest() {
|
||||
const area = {x: w, y: h};
|
||||
const entities = [];
|
||||
entities.push({
|
||||
|
@ -63,13 +63,13 @@ export default async function createForest() {
|
|||
{
|
||||
area,
|
||||
data: Array(w * h).fill(0),
|
||||
source: '/resources/tileset.json',
|
||||
source: '/resources/tileset.sprite.json',
|
||||
tileSize: {x: 16, y: 16},
|
||||
},
|
||||
{
|
||||
area,
|
||||
data: Array(w * h).fill(0),
|
||||
source: '/resources/tileset.json',
|
||||
source: '/resources/tileset.sprite.json',
|
||||
tileSize: {x: 16, y: 16},
|
||||
},
|
||||
],
|
||||
|
@ -113,7 +113,7 @@ export default async function createForest() {
|
|||
Position: entityPosition(x, y),
|
||||
Sprite: {
|
||||
anchorY: 0.7,
|
||||
source: '/resources/ambient/shrub.json',
|
||||
source: '/resources/ambient/shrub.sprite.json',
|
||||
},
|
||||
VisibleAabb: {},
|
||||
});
|
||||
|
@ -123,7 +123,7 @@ export default async function createForest() {
|
|||
Position: entityPosition(x, y),
|
||||
Sprite: {
|
||||
anchorY: 0.875,
|
||||
source: '/resources/ambient/tree.json',
|
||||
source: '/resources/ambient/tree.sprite.json',
|
||||
},
|
||||
VisibleAabb: {},
|
||||
});
|
||||
|
@ -134,7 +134,7 @@ export default async function createForest() {
|
|||
Position: entityPosition(x, y),
|
||||
Sprite: {
|
||||
anchorY: 0.7,
|
||||
source: '/resources/ambient/flower.json',
|
||||
source: '/resources/ambient/flower.sprite.json',
|
||||
},
|
||||
VisibleAabb: {},
|
||||
});
|
||||
|
|
|
@ -32,13 +32,13 @@ function createMaster() {
|
|||
{
|
||||
area,
|
||||
data,
|
||||
source: '/resources/tileset.json',
|
||||
source: '/resources/tileset.sprite.json',
|
||||
tileSize: {x: 16, y: 16},
|
||||
},
|
||||
{
|
||||
area,
|
||||
data: Array(area.x * area.y).fill(0),
|
||||
source: '/resources/tileset.json',
|
||||
source: '/resources/tileset.sprite.json',
|
||||
tileSize: {x: 16, y: 16},
|
||||
},
|
||||
],
|
||||
|
@ -48,7 +48,7 @@ function createMaster() {
|
|||
};
|
||||
}
|
||||
|
||||
function createShitShack(id) {
|
||||
function createShitShack() {
|
||||
return {
|
||||
Collider: {
|
||||
bodies: [
|
||||
|
@ -63,21 +63,18 @@ function createShitShack(id) {
|
|||
},
|
||||
],
|
||||
},
|
||||
Ecs: {
|
||||
path: ['houses', `${id}`].join('/'),
|
||||
},
|
||||
Position: {x: 100, y: 100},
|
||||
Sprite: {
|
||||
anchorX: 0.5,
|
||||
anchorY: 0.8,
|
||||
source: '/resources/shit-shack/shit-shack.json',
|
||||
source: '/resources/shit-shack/shit-shack.sprite.json',
|
||||
},
|
||||
Ticking: {},
|
||||
VisibleAabb: {},
|
||||
};
|
||||
}
|
||||
|
||||
function createHouseTeleport(id) {
|
||||
function createHouseTeleport() {
|
||||
return {
|
||||
Collider: {
|
||||
bodies: [
|
||||
|
@ -90,20 +87,7 @@ function createHouseTeleport(id) {
|
|||
],
|
||||
},
|
||||
],
|
||||
collisionStartScript: `
|
||||
if (other.Player) {
|
||||
ecs.switchEcs(
|
||||
other,
|
||||
'houses/${id}',
|
||||
{
|
||||
Position: {
|
||||
x: 72,
|
||||
y: 304,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
`,
|
||||
collisionStartScript: '/resources/homestead/house-teleport/collision-start.js',
|
||||
},
|
||||
Position: {x: 71, y: 113},
|
||||
Ticking: {},
|
||||
|
@ -127,35 +111,26 @@ function createChest() {
|
|||
},
|
||||
Interactive: {
|
||||
interacting: 1,
|
||||
interactScript: `
|
||||
initiator.Player.openInventory = subject.Inventory;
|
||||
// subject.Interlocutor.dialogue({
|
||||
// body: "Sure, I'm a treasure chest. Probably. Do you really think that means you're about to get some treasure? Hah!",
|
||||
// monopolizer: true,
|
||||
// offset: {x: 0, y: -48},
|
||||
// origin: 'track',
|
||||
// position: 'track',
|
||||
// })
|
||||
`,
|
||||
interactScript: '/resources/chest/interact.js',
|
||||
},
|
||||
Interlocutor: {},
|
||||
Inventory: {
|
||||
slots: {
|
||||
2: {
|
||||
qty: 1,
|
||||
source: '/resources/watering-can/watering-can.json',
|
||||
source: '/resources/watering-can/watering-can.item.json',
|
||||
},
|
||||
3: {
|
||||
qty: 1,
|
||||
source: '/resources/tomato-seeds/tomato-seeds.json',
|
||||
source: '/resources/tomato-seeds/tomato-seeds.item.json',
|
||||
},
|
||||
4: {
|
||||
qty: 1,
|
||||
source: '/resources/hoe/hoe.json',
|
||||
source: '/resources/hoe/hoe.item.json',
|
||||
},
|
||||
5: {
|
||||
qty: 1,
|
||||
source: '/resources/brush/brush.json',
|
||||
source: '/resources/brush/brush.item.json',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -164,7 +139,7 @@ function createChest() {
|
|||
Sprite: {
|
||||
anchorX: 0.5,
|
||||
anchorY: 0.7,
|
||||
source: '/resources/chest/chest.json',
|
||||
source: '/resources/chest/chest.sprite.json',
|
||||
},
|
||||
Ticking: {},
|
||||
VisibleAabb: {},
|
||||
|
@ -173,43 +148,11 @@ function createChest() {
|
|||
|
||||
function createTomato() {
|
||||
return {
|
||||
Collider: {
|
||||
bodies: [
|
||||
{
|
||||
points: [
|
||||
{x: -4, y: -4},
|
||||
{x: 3, y: -4},
|
||||
{x: 3, y: 3},
|
||||
{x: -4, y: 3},
|
||||
],
|
||||
},
|
||||
],
|
||||
collisionStartScript: [
|
||||
'if (other.Inventory) {',
|
||||
' other.Inventory.give({',
|
||||
' qty: 1,',
|
||||
" source: '/resources/tomato/tomato.json',",
|
||||
' })',
|
||||
' ecs.destroy(entity.id)',
|
||||
' undefined;',
|
||||
'}',
|
||||
].join('\n'),
|
||||
},
|
||||
Forces: {},
|
||||
Magnetic: {},
|
||||
$$extends: '/resources/tomato/tomato.entity.json',
|
||||
Position: {
|
||||
x: 168 + Math.random() * 30,
|
||||
y: 448 + Math.random() * 30,
|
||||
},
|
||||
Sprite: {
|
||||
anchorX: 0.5,
|
||||
anchorY: 0.5,
|
||||
scaleX: 0.333,
|
||||
scaleY: 0.333,
|
||||
source: '/resources/tomato/tomato-sprite.json',
|
||||
},
|
||||
Ticking: {},
|
||||
VisibleAabb: {},
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -236,23 +179,7 @@ function createTestKitten() {
|
|||
},
|
||||
Interactive: {
|
||||
interacting: 1,
|
||||
interactScript: `
|
||||
const lines = [
|
||||
'mrowwr',
|
||||
'p<shake>rrr</shake>o<wave>wwwww</wave>',
|
||||
'mew<rate frequency={0.5}> </rate>mew!',
|
||||
'me<wave>wwwww</wave>',
|
||||
'\\\\*pu<shake>rrrrr</shake>\\\\*',
|
||||
];
|
||||
const line = lines[Math.floor(Math.random() * lines.length)];
|
||||
subject.Interlocutor.dialogue({
|
||||
body: line,
|
||||
linger: 2,
|
||||
offset: {x: 0, y: -16},
|
||||
origin: 'track',
|
||||
position: 'track',
|
||||
})
|
||||
`,
|
||||
interactScript: '/resources/kitty/interact.js',
|
||||
},
|
||||
Position: {
|
||||
x: 250 + (Math.random() - 0.5) * 300,
|
||||
|
@ -262,7 +189,7 @@ function createTestKitten() {
|
|||
...animalJson.Sprite,
|
||||
anchorX: 0.5,
|
||||
anchorY: 0.7,
|
||||
source: '/resources/kitty/kitty.json',
|
||||
source: '/resources/kitty/kitty.sprite.json',
|
||||
speed: 0.115,
|
||||
},
|
||||
Tags: {tags: ['kittan']},
|
||||
|
@ -292,20 +219,7 @@ function createTestCow() {
|
|||
},
|
||||
Interactive: {
|
||||
interacting: 1,
|
||||
interactScript: `
|
||||
const lines = [
|
||||
'sno<shake>rr</shake>t',
|
||||
'm<wave>ooooooooooo</wave>',
|
||||
];
|
||||
const line = lines[Math.floor(Math.random() * lines.length)];
|
||||
subject.Interlocutor.dialogue({
|
||||
body: line,
|
||||
linger: 2,
|
||||
offset: {x: 0, y: -16},
|
||||
origin: 'track',
|
||||
position: 'track',
|
||||
})
|
||||
`,
|
||||
interactScript: '/resources/farm/animals/cow-adult/interact.js',
|
||||
},
|
||||
Position: {
|
||||
x: 350 + (Math.random() - 0.5) * 300,
|
||||
|
@ -315,7 +229,7 @@ function createTestCow() {
|
|||
...animalJson.Sprite,
|
||||
anchorX: 0.5,
|
||||
anchorY: 0.8,
|
||||
source: '/resources/farm/animals/cow-adult/cow-adult.json',
|
||||
source: '/resources/farm/animals/cow-adult/cow-adult.sprite.json',
|
||||
speed: 0.25,
|
||||
},
|
||||
};
|
||||
|
@ -344,19 +258,7 @@ function createTestGoat() {
|
|||
},
|
||||
Interactive: {
|
||||
interacting: 1,
|
||||
interactScript: `
|
||||
const lines = [
|
||||
'Mind your own business, buddy.\\n\\ner, I mean, <shake>MEEHHHHHH</shake>',
|
||||
];
|
||||
const line = lines[Math.floor(Math.random() * lines.length)];
|
||||
subject.Interlocutor.dialogue({
|
||||
body: line,
|
||||
linger: 2,
|
||||
offset: {x: 0, y: -16},
|
||||
origin: 'track',
|
||||
position: 'track',
|
||||
})
|
||||
`,
|
||||
interactScript: '/resources/farm/animals/goat-white/interact.js',
|
||||
},
|
||||
Position: {
|
||||
x: 350 + (Math.random() - 0.5) * 300,
|
||||
|
@ -366,7 +268,7 @@ function createTestGoat() {
|
|||
...animalJson.Sprite,
|
||||
anchorX: 0.5,
|
||||
anchorY: 0.8,
|
||||
source: '/resources/farm/animals/goat-white/goat-white.json',
|
||||
source: '/resources/farm/animals/goat-white/goat-white.sprite.json',
|
||||
speed: 0.25,
|
||||
},
|
||||
};
|
||||
|
@ -385,20 +287,7 @@ function createTownTeleport() {
|
|||
],
|
||||
},
|
||||
],
|
||||
collisionStartScript: `
|
||||
if (other.Player) {
|
||||
ecs.switchEcs(
|
||||
other,
|
||||
'town',
|
||||
{
|
||||
Position: {
|
||||
x: 940,
|
||||
y: 480,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
`,
|
||||
collisionStartScript: '/resources/homestead/town-teleport/collision-start.js',
|
||||
},
|
||||
|
||||
Position: {x: 8, y: 432},
|
||||
|
@ -436,23 +325,25 @@ function createTomatoPlant() {
|
|||
Sprite: {
|
||||
anchorY: 0.75,
|
||||
animation: 'stage/0',
|
||||
source: '/resources/tomato-plant/tomato-plant.json',
|
||||
source: '/resources/tomato-plant/tomato-plant.sprite.json',
|
||||
zIndex: 0,
|
||||
},
|
||||
Ticking: {},
|
||||
VisibleAabb: {},
|
||||
};
|
||||
}
|
||||
|
||||
export default async function createHomestead(id) {
|
||||
export default function createHomestead() {
|
||||
const entities = [];
|
||||
entities.push(createMaster());
|
||||
entities.push(createShitShack(id));
|
||||
entities.push(createHouseTeleport(id));
|
||||
entities.push(createChest());
|
||||
// entities.push(createShitShack());
|
||||
// entities.push(createHouseTeleport());
|
||||
// entities.push(createTownTeleport());
|
||||
// entities.push(createChest());
|
||||
// for (let i = 0; i < 200; ++i) {
|
||||
// entities.push(createTomato());
|
||||
// }
|
||||
entities.push(createTomatoPlant());
|
||||
// entities.push(createTomatoPlant());
|
||||
// for (let i = 0; i < 10; ++i) {
|
||||
// entities.push(createTestKitten());
|
||||
// }
|
||||
|
@ -462,6 +353,5 @@ export default async function createHomestead(id) {
|
|||
// for (let i = 0; i < 10; ++i) {
|
||||
// entities.push(createTestGoat());
|
||||
// }
|
||||
entities.push(createTownTeleport());
|
||||
return entities;
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import createEcs from './ecs.js';
|
||||
|
||||
export default async function createHouse(Ecs, id) {
|
||||
export default function createHouse(Ecs, id) {
|
||||
const ecs = createEcs(Ecs);
|
||||
const area = {x: 20, y: 20};
|
||||
await ecs.create({
|
||||
ecs.create({
|
||||
AreaSize: {x: area.x * 16, y: area.y * 16},
|
||||
Ticking: {},
|
||||
TileLayers: {
|
||||
|
@ -11,13 +11,13 @@ export default async function createHouse(Ecs, id) {
|
|||
{
|
||||
area,
|
||||
data: Array(area.x * area.y).fill(0).map(() => 5 + Math.floor(Math.random() * 2)),
|
||||
source: '/resources/tileset.json',
|
||||
source: '/resources/tileset.sprite.json',
|
||||
tileSize: {x: 16, y: 16},
|
||||
}
|
||||
],
|
||||
},
|
||||
});
|
||||
await ecs.create({
|
||||
ecs.create({
|
||||
Collider: {
|
||||
bodies: [
|
||||
{
|
||||
|
@ -29,20 +29,7 @@ export default async function createHouse(Ecs, id) {
|
|||
],
|
||||
},
|
||||
],
|
||||
collisionStartScript: `
|
||||
if (other.Player) {
|
||||
ecs.switchEcs(
|
||||
other,
|
||||
'homesteads/${id}',
|
||||
{
|
||||
Position: {
|
||||
x: 74,
|
||||
y: 128,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
`,
|
||||
collisionStartScript: '/resources/house/homestead-teleport/collision-start.js',
|
||||
},
|
||||
Position: {
|
||||
x: 72,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
export default async function createPlayer(id) {
|
||||
export default function createPlayer(id) {
|
||||
const player = {
|
||||
Alive: {health: 100},
|
||||
Camera: {},
|
||||
|
@ -25,37 +25,38 @@ export default async function createPlayer(id) {
|
|||
slots: {
|
||||
1: {
|
||||
qty: 100,
|
||||
source: '/resources/potion/potion.json',
|
||||
source: '/resources/potion/potion.item.json',
|
||||
},
|
||||
2: {
|
||||
qty: 1,
|
||||
source: '/resources/magic-swords/magic-swords.json',
|
||||
source: '/resources/magic-swords/magic-swords.item.json',
|
||||
},
|
||||
3: {
|
||||
qty: 1,
|
||||
source: '/resources/watering-can/watering-can.json',
|
||||
source: '/resources/watering-can/watering-can.item.json',
|
||||
},
|
||||
4: {
|
||||
qty: 1,
|
||||
source: '/resources/tomato-seeds/tomato-seeds.json',
|
||||
source: '/resources/tomato-seeds/tomato-seeds.item.json',
|
||||
},
|
||||
5: {
|
||||
qty: 1,
|
||||
source: '/resources/hoe/hoe.json',
|
||||
source: '/resources/hoe/hoe.item.json',
|
||||
},
|
||||
6: {
|
||||
qty: 95,
|
||||
source: '/resources/potion/potion.json',
|
||||
source: '/resources/potion/potion.item.json',
|
||||
},
|
||||
7: {
|
||||
qty: 95,
|
||||
source: '/resources/potion/potion.json',
|
||||
source: '/resources/potion/potion.item.json',
|
||||
},
|
||||
},
|
||||
},
|
||||
Grabber: {},
|
||||
Health: {health: 100},
|
||||
Light: {brightness: 0},
|
||||
Magnet: {strength: 24},
|
||||
// Magnet: {strength: 24},
|
||||
Player: {},
|
||||
Position: {x: 128, y: 448},
|
||||
Speed: {speed: 100},
|
||||
|
@ -65,7 +66,7 @@ export default async function createPlayer(id) {
|
|||
anchorY: 0.9,
|
||||
animation: 'moving:down',
|
||||
frame: 0,
|
||||
source: '/resources/dude/dude.json',
|
||||
source: '/resources/dude/dude.sprite.json',
|
||||
speed: 0.115,
|
||||
},
|
||||
Ticking: {},
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import data from './town.json';
|
||||
|
||||
export default async function createTown() {
|
||||
export default function createTown() {
|
||||
const area = {x: 60, y: 60};
|
||||
const entities = [];
|
||||
entities.push({
|
||||
|
@ -11,13 +11,13 @@ export default async function createTown() {
|
|||
{
|
||||
area,
|
||||
data,
|
||||
source: '/resources/tileset.json',
|
||||
source: '/resources/tileset.sprite.json',
|
||||
tileSize: {x: 16, y: 16},
|
||||
},
|
||||
{
|
||||
area,
|
||||
data: Array(area.x * area.y).fill(0),
|
||||
source: '/resources/tileset.json',
|
||||
source: '/resources/tileset.sprite.json',
|
||||
tileSize: {x: 16, y: 16},
|
||||
},
|
||||
],
|
||||
|
@ -37,20 +37,7 @@ export default async function createTown() {
|
|||
],
|
||||
},
|
||||
],
|
||||
collisionStartScript: `
|
||||
if (other.Player) {
|
||||
ecs.switchEcs(
|
||||
other,
|
||||
['homesteads', other.Player.id].join('/'),
|
||||
{
|
||||
Position: {
|
||||
x: 20,
|
||||
y: 438,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
`,
|
||||
collisionStartScript: '/resources/town/homestead-teleport/collision-start.js',
|
||||
},
|
||||
Position: {x: 952, y: 480},
|
||||
Ticking: {},
|
||||
|
|
|
@ -397,7 +397,7 @@ export default class Engine {
|
|||
throw error;
|
||||
}
|
||||
const homestead = this.createEcs();
|
||||
for (const entity of await createHomestead(id)) {
|
||||
for (const entity of await createHomestead()) {
|
||||
await homestead.create(entity);
|
||||
}
|
||||
await this.saveEcs(
|
||||
|
|
|
@ -122,7 +122,7 @@ if (import.meta.hot) {
|
|||
delete engine.ecses['homesteads/0'];
|
||||
await engine.server.removeData('homesteads/0');
|
||||
const homestead = createEcs(engine.Ecs);
|
||||
for (const entity of await createHomestead('0')) {
|
||||
for (const entity of await createHomestead()) {
|
||||
await homestead.create(entity);
|
||||
}
|
||||
await engine.saveEcs('homesteads/0', homestead);
|
||||
|
|
|
@ -1,32 +1,36 @@
|
|||
import {Ticker} from '@/util/promise.js';
|
||||
import Ticker from '@/util/ticker.js';
|
||||
|
||||
export default function delta(object, properties) {
|
||||
const deltas = {};
|
||||
for (const key in properties) {
|
||||
const keys = new Set(Object.keys(properties));
|
||||
function stop() {
|
||||
keys.clear();
|
||||
}
|
||||
for (const key of keys) {
|
||||
const property = properties[key];
|
||||
const delta = {
|
||||
duration: Infinity,
|
||||
elapsed: 0,
|
||||
...property,
|
||||
stop: () => {
|
||||
keys.delete(key);
|
||||
},
|
||||
};
|
||||
deltas[key] = delta;
|
||||
}
|
||||
let stop;
|
||||
const promise = new Ticker(
|
||||
(resolve) => {
|
||||
stop = resolve;
|
||||
},
|
||||
(elapsed, resolve) => {
|
||||
for (const key in deltas) {
|
||||
const ticker = new Ticker(function* () {
|
||||
while (keys.size > 0) {
|
||||
const elapsed = yield;
|
||||
for (const key of keys) {
|
||||
deltas[key].elapsed += elapsed;
|
||||
object[key] += deltas[key].delta * elapsed;
|
||||
if (deltas[key].elapsed >= deltas[key].duration) {
|
||||
object[key] += deltas[key].delta * (deltas[key].duration - deltas[key].elapsed);
|
||||
resolve();
|
||||
return;
|
||||
deltas[key].stop();
|
||||
break;
|
||||
}
|
||||
object[key] += deltas[key].delta * elapsed;
|
||||
}
|
||||
},
|
||||
);
|
||||
return {stop, deltas, promise};
|
||||
}
|
||||
});
|
||||
return {deltas, stop, ticker};
|
||||
}
|
||||
|
|
51
app/util/delta.test.js
Normal file
51
app/util/delta.test.js
Normal file
|
@ -0,0 +1,51 @@
|
|||
import {expect, test} from 'vitest';
|
||||
|
||||
import delta from './delta.js';
|
||||
|
||||
test('mutates', () => {
|
||||
const O = {x: 10};
|
||||
const {ticker} = delta(O, {x: {delta: 20}});
|
||||
expect(O.x).to.equal(10);
|
||||
ticker.tick(0.5);
|
||||
expect(O.x).to.equal(20);
|
||||
ticker.tick(0.5);
|
||||
expect(O.x).to.equal(30);
|
||||
ticker.tick(0.5);
|
||||
expect(O.x).to.equal(40);
|
||||
ticker.tick(0.5);
|
||||
expect(O.x).to.equal(50);
|
||||
});
|
||||
|
||||
test('does not overshoot', () => {
|
||||
const O = {x: 10};
|
||||
const {ticker} = delta(O, {x: {delta: 20, duration: 0.75}});
|
||||
ticker.tick(0.5);
|
||||
expect(O.x).to.equal(20);
|
||||
ticker.tick(0.5);
|
||||
expect(O.x).to.equal(25);
|
||||
ticker.tick(0.5);
|
||||
expect(O.x).to.equal(25);
|
||||
});
|
||||
|
||||
test('stops mutating', () => {
|
||||
const O = {x: 10, y: 20};
|
||||
const {deltas, stop, ticker} = delta(O, {x: {delta: 20}, y: {delta: 10}});
|
||||
ticker.tick(0.5);
|
||||
expect(O.x).to.equal(20);
|
||||
expect(O.y).to.equal(25);
|
||||
deltas.x.stop();
|
||||
ticker.tick(0.5);
|
||||
expect(O.x).to.equal(20);
|
||||
expect(O.y).to.equal(30);
|
||||
stop();
|
||||
expect(O.x).to.equal(20);
|
||||
expect(O.y).to.equal(30);
|
||||
});
|
||||
|
||||
test('exposes deltas', () => {
|
||||
const O = {x: 10};
|
||||
const {deltas, ticker} = delta(O, {x: {delta: 20}});
|
||||
deltas.x.delta = 50;
|
||||
ticker.tick(0.5);
|
||||
expect(O.x).to.equal(35);
|
||||
});
|
4
app/util/eval.js
Normal file
4
app/util/eval.js
Normal file
|
@ -0,0 +1,4 @@
|
|||
// this is only here to avoid build warnings that should have been tree-shaken away
|
||||
export default function importedEval(code) {
|
||||
return eval(code);
|
||||
}
|
|
@ -2,7 +2,7 @@ import {expect, test} from 'vitest';
|
|||
|
||||
import {distribute} from './inventory.js';
|
||||
|
||||
test('distributes', async () => {
|
||||
test('distributes', () => {
|
||||
let item;
|
||||
item = {maximumStack: 20, qty: 10};
|
||||
expect(distribute(
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import {Ticker, withResolvers} from '@/util/promise.js';
|
||||
import Ticker from '@/util/ticker.js';
|
||||
|
||||
const Modulators = {
|
||||
flat: () => 0.5,
|
||||
|
@ -11,8 +11,11 @@ const Modulators = {
|
|||
|
||||
export default function lfo(object, properties) {
|
||||
const oscillators = {};
|
||||
const promises = [];
|
||||
for (const key in properties) {
|
||||
const keys = new Set(Object.keys(properties));
|
||||
function stop() {
|
||||
keys.clear();
|
||||
}
|
||||
for (const key of keys) {
|
||||
const property = properties[key];
|
||||
const oscillator = {
|
||||
count: Infinity,
|
||||
|
@ -35,38 +38,38 @@ export default function lfo(object, properties) {
|
|||
}
|
||||
}
|
||||
oscillator.low = oscillator.median - oscillator.magnitude / 2;
|
||||
({promise: oscillator.promise, resolve: oscillator.stop} = withResolvers());
|
||||
oscillator.promise.then(() => {
|
||||
delete oscillators[key];
|
||||
});
|
||||
promises.push(oscillator.promise);
|
||||
oscillator.stop = () => {
|
||||
keys.delete(key);
|
||||
};
|
||||
oscillator.compute = (elapsed) => {
|
||||
const x = (oscillator.offset + (elapsed / oscillator.frequency)) % 1;
|
||||
let y = 0;
|
||||
for (const modulator of oscillator.modulators) {
|
||||
y += modulator(x);
|
||||
}
|
||||
return oscillator.low + oscillator.magnitude * (y / oscillator.modulators.length);
|
||||
}
|
||||
oscillators[key] = oscillator;
|
||||
}
|
||||
let stop;
|
||||
const promise = new Ticker(
|
||||
(resolve) => {
|
||||
stop = resolve;
|
||||
Promise.all(promises).then(resolve);
|
||||
},
|
||||
(elapsed) => {
|
||||
for (const key in oscillators) {
|
||||
const ticker = new Ticker(function* () {
|
||||
while (keys.size > 0) {
|
||||
const elapsed = yield;
|
||||
for (const key of keys) {
|
||||
const oscillator = oscillators[key];
|
||||
oscillator.elapsed += elapsed;
|
||||
if (oscillator.elapsed >= oscillator.frequency) {
|
||||
if (0 === --oscillator.count) {
|
||||
oscillator.stop();
|
||||
return;
|
||||
}
|
||||
oscillator.elapsed = oscillator.elapsed % oscillator.frequency;
|
||||
const rollover = oscillator.elapsed >= oscillator.frequency;
|
||||
if (rollover) {
|
||||
oscillator.count -= 1;
|
||||
oscillator.elapsed = 0 === oscillator.count
|
||||
? oscillator.frequency
|
||||
: oscillator.elapsed % oscillator.frequency;
|
||||
}
|
||||
const x = (oscillator.offset + (oscillator.elapsed / oscillator.frequency)) % 1;
|
||||
let y = 0;
|
||||
for (const modulator of oscillator.modulators) {
|
||||
y += modulator(x);
|
||||
object[key] = oscillator.compute(oscillator.elapsed);
|
||||
if (rollover && 0 === oscillator.count) {
|
||||
oscillator.stop();
|
||||
}
|
||||
object[key] = oscillator.low + oscillator.magnitude * (y / oscillator.modulators.length);
|
||||
}
|
||||
},
|
||||
);
|
||||
return {stop, oscillators, promise};
|
||||
}
|
||||
});
|
||||
return {oscillators, stop, ticker};
|
||||
}
|
||||
|
|
103
app/util/lfo.test.js
Normal file
103
app/util/lfo.test.js
Normal file
|
@ -0,0 +1,103 @@
|
|||
import {expect, test} from 'vitest';
|
||||
|
||||
import lfo from './lfo.js';
|
||||
|
||||
test('mutates', () => {
|
||||
const O = {x: 0.25};
|
||||
const {ticker} = lfo(
|
||||
O,
|
||||
{
|
||||
x: {
|
||||
count: 1,
|
||||
frequency: 0.5,
|
||||
magnitude: 0.5,
|
||||
modulators: ['sine'],
|
||||
},
|
||||
},
|
||||
);
|
||||
expect(O.x).to.equal(0.25);
|
||||
ticker.tick(0.125);
|
||||
expect(O.x).to.equal(0.5);
|
||||
ticker.tick(0.125);
|
||||
expect(O.x).to.be.closeTo(0.25, 0.0001);
|
||||
ticker.tick(0.125);
|
||||
expect(O.x).to.equal(0);
|
||||
ticker.tick(0.125);
|
||||
expect(O.x).to.equal(0.25);
|
||||
ticker.tick(0.125);
|
||||
expect(O.x).to.equal(0.25);
|
||||
});
|
||||
|
||||
test('does not overshoot', () => {
|
||||
const O = {x: 0.25};
|
||||
const {oscillators, ticker} = lfo(
|
||||
O,
|
||||
{
|
||||
x: {
|
||||
count: 1,
|
||||
frequency: 0.5,
|
||||
magnitude: 0.5,
|
||||
modulators: ['sine'],
|
||||
},
|
||||
},
|
||||
);
|
||||
ticker.tick(0.3);
|
||||
expect(O.x).to.equal(oscillators.x.compute(0.3));
|
||||
ticker.tick(0.3);
|
||||
expect(O.x).to.equal(oscillators.x.compute(0.5));
|
||||
});
|
||||
|
||||
test('stops mutating', () => {
|
||||
const O = {x: 0.25, y: 0.5};
|
||||
const {oscillators, stop, ticker} = lfo(
|
||||
O,
|
||||
{
|
||||
x: {
|
||||
count: 1,
|
||||
frequency: 0.5,
|
||||
magnitude: 0.5,
|
||||
modulators: ['sine'],
|
||||
},
|
||||
y: {
|
||||
count: 1,
|
||||
frequency: 0.5,
|
||||
magnitude: 0.5,
|
||||
modulators: ['sine'],
|
||||
offset: 0.25,
|
||||
},
|
||||
},
|
||||
);
|
||||
ticker.tick(0.2);
|
||||
expect(O.x).to.equal(oscillators.x.compute(0.2));
|
||||
expect(O.y).to.equal(oscillators.y.compute(0.2));
|
||||
oscillators.x.stop();
|
||||
ticker.tick(0.2);
|
||||
expect(O.x).to.equal(oscillators.x.compute(0.2));
|
||||
expect(O.y).to.equal(oscillators.y.compute(0.4));
|
||||
stop();
|
||||
ticker.tick(0.2);
|
||||
expect(O.x).to.equal(oscillators.x.compute(0.2));
|
||||
expect(O.y).to.equal(oscillators.y.compute(0.4));
|
||||
});
|
||||
|
||||
test('exposes oscillators', () => {
|
||||
const O = {x: 0};
|
||||
const {oscillators, ticker} = lfo(
|
||||
O,
|
||||
{
|
||||
x: {
|
||||
count: 1,
|
||||
frequency: 0.5,
|
||||
magnitude: 0.5,
|
||||
modulators: ['sine'],
|
||||
},
|
||||
},
|
||||
);
|
||||
ticker.tick(0.2);
|
||||
expect(O.x).to.equal(oscillators.x.compute(0.2));
|
||||
oscillators.x.count = 2;
|
||||
ticker.tick(0.3);
|
||||
expect(O.x).to.equal(oscillators.x.compute(0));
|
||||
ticker.tick(0.5);
|
||||
expect(O.x).to.equal(oscillators.x.compute(0.5));
|
||||
});
|
|
@ -2,7 +2,7 @@ import {expect, test} from 'vitest';
|
|||
|
||||
import * as MathUtil from './math.js';
|
||||
|
||||
test('transforms vertices', async () => {
|
||||
test('transforms vertices', () => {
|
||||
const expectCloseTo = (l, r) => {
|
||||
expect(l.length)
|
||||
.to.equal(r.length);
|
||||
|
|
|
@ -12,63 +12,3 @@ export function withResolvers() {
|
|||
});
|
||||
return {promise, reject, resolve};
|
||||
}
|
||||
|
||||
export class Ticker 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 tickers = [];
|
||||
for (let i = 0; i < promises.length; i++) {
|
||||
const promise = promises[i];
|
||||
if (promise instanceof Ticker) {
|
||||
tickers.push(promise);
|
||||
// After resolution, stop ticking the promise.
|
||||
promise.then(() => {
|
||||
tickers.splice(tickers.indexOf(promise), 1);
|
||||
});
|
||||
}
|
||||
}
|
||||
/* v8 ignore next 3 */
|
||||
if (0 === tickers.length) {
|
||||
return super.all(promises);
|
||||
}
|
||||
return new Ticker(
|
||||
(resolve, reject) => {
|
||||
super.all(promises)
|
||||
.then(resolve)
|
||||
/* v8 ignore next */
|
||||
.catch(reject);
|
||||
},
|
||||
(elapsed) => {
|
||||
for (let i = 0; i < tickers.length; i++) {
|
||||
tickers[i].tick(elapsed);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
tick(elapsed) {
|
||||
this.ticker(elapsed, this.resolve, this.reject);
|
||||
}
|
||||
|
||||
then(...args) {
|
||||
const promise = super.then(...args);
|
||||
promise.ticker = this.ticker;
|
||||
return promise;
|
||||
}
|
||||
|
||||
}
|
|
@ -1,100 +0,0 @@
|
|||
import {expect, test} from 'vitest';
|
||||
|
||||
import {Ticker} from './promise.js';
|
||||
|
||||
test('runs executor', async () => {
|
||||
expect(
|
||||
await new Ticker((resolve) => {
|
||||
resolve(32);
|
||||
}),
|
||||
)
|
||||
.to.equal(32);
|
||||
expect(
|
||||
async () => {
|
||||
await new Ticker((resolve, reject) => {
|
||||
reject(new Error(''));
|
||||
})
|
||||
}
|
||||
)
|
||||
.rejects.toThrowError('');
|
||||
});
|
||||
|
||||
test('ticks and resolves', async () => {
|
||||
let done = false;
|
||||
let e = 0;
|
||||
const tp = new Ticker(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 Ticker(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 Ticker(undefined, (elapsed, resolve) => {
|
||||
e1 += elapsed;
|
||||
if (1 === e1) {
|
||||
done += 1;
|
||||
resolve(16);
|
||||
}
|
||||
});
|
||||
const tp2 = new Ticker(undefined, (elapsed, resolve) => {
|
||||
e2 += elapsed;
|
||||
if (2 === e2) {
|
||||
done += 1;
|
||||
resolve(32);
|
||||
}
|
||||
});
|
||||
const tpa = Ticker.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]);
|
||||
});
|
|
@ -1,188 +1,122 @@
|
|||
import {parse as acornParse} from 'acorn';
|
||||
import {Runner} from 'astride';
|
||||
import {LRUCache} from 'lru-cache';
|
||||
|
||||
import * as color from '@/util/color.js';
|
||||
import delta from '@/util/delta.js';
|
||||
import lfo from '@/util/lfo.js';
|
||||
import * as MathUtil from '@/util/math.js';
|
||||
import * as PromiseUtil from '@/util/promise.js';
|
||||
import transition from '@/util/transition.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,
|
||||
});
|
||||
import Ticker from '@/util/ticker.js';
|
||||
|
||||
export default class Script {
|
||||
|
||||
constructor(sandbox, code) {
|
||||
this.code = code;
|
||||
this.sandbox = sandbox;
|
||||
this.promise = null;
|
||||
static registered = {};
|
||||
|
||||
constructor(fn, locals) {
|
||||
if (!fn) {
|
||||
throw new TypeError('Script needs a function');
|
||||
}
|
||||
this.fn = fn;
|
||||
this.iterator = null;
|
||||
this.locals = locals;
|
||||
this.$$ticker = null;
|
||||
}
|
||||
|
||||
clone() {
|
||||
return new this.constructor(this.sandbox.clone(), this.code);
|
||||
}
|
||||
|
||||
get context() {
|
||||
return this.sandbox.locals;
|
||||
}
|
||||
|
||||
static contextDefaults() {
|
||||
return {
|
||||
color,
|
||||
console,
|
||||
delta,
|
||||
lfo,
|
||||
Math: MathUtil,
|
||||
Promise: PromiseUtil,
|
||||
transition,
|
||||
wait: (seconds = 0) => (
|
||||
new PromiseUtil.Ticker(
|
||||
(resolve) => {
|
||||
if (0 === seconds) {
|
||||
resolve();
|
||||
}
|
||||
},
|
||||
(elapsed, resolve) => {
|
||||
seconds -= elapsed;
|
||||
if (seconds <= 0) {
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
)
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
static createContext(locals = {}) {
|
||||
if (locals[Populated]) {
|
||||
return locals;
|
||||
}
|
||||
return {
|
||||
[Populated]: true,
|
||||
...this.contextDefaults(),
|
||||
...locals,
|
||||
};
|
||||
return new this.constructor(this.fn, this.locals);
|
||||
}
|
||||
|
||||
evaluate() {
|
||||
this.sandbox.reset();
|
||||
try {
|
||||
const {value} = this.sandbox.step();
|
||||
return value;
|
||||
return this.fn(this.locals).next().value;
|
||||
}
|
||||
|
||||
static load(pathOrFunction, locals) {
|
||||
let fn;
|
||||
if (this.registered[pathOrFunction]) {
|
||||
fn = this.registered[pathOrFunction];
|
||||
}
|
||||
catch (error) {
|
||||
console.warn(this.sandbox.$$stack);
|
||||
console.warn(error);
|
||||
else if (pathOrFunction) {
|
||||
try {
|
||||
fn = eval(pathOrFunction);
|
||||
}
|
||||
catch (error) {
|
||||
console.error("Couldn't eval script", pathOrFunction);
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
if (!fn) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
static fromCode(code, context = {}) {
|
||||
if (!cache.has(code)) {
|
||||
cache.set(code, this.parse(code));
|
||||
const script = new this(fn, locals);
|
||||
if (import.meta.hot) {
|
||||
import.meta.hot.accept('./scripts.js', ({default: scripts}) => {
|
||||
script.fn = scripts[`../..${pathOrFunction}`];
|
||||
});
|
||||
}
|
||||
return new this(
|
||||
new Runner(cache.get(code), this.createContext(context)),
|
||||
code,
|
||||
);
|
||||
return script;
|
||||
}
|
||||
|
||||
static parse(code) {
|
||||
return parse(
|
||||
code,
|
||||
{
|
||||
allowReturnOutsideFunction: true,
|
||||
},
|
||||
);
|
||||
static register(path, fn) {
|
||||
this.registered[path] = fn;
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.promise = null;
|
||||
this.sandbox.reset();
|
||||
this.iterator = null;
|
||||
this.$$ticker = null;
|
||||
}
|
||||
|
||||
tick(elapsed, resolve, reject) {
|
||||
if (this.promise) {
|
||||
if (this.promise instanceof PromiseUtil.Ticker) {
|
||||
this.promise.tick(elapsed);
|
||||
tick(elapsed) {
|
||||
this.locals.elapsed = elapsed;
|
||||
if (this.$$ticker) {
|
||||
const result = this.$$ticker.tick(elapsed);
|
||||
this.locals.elapsed -= result.value;
|
||||
if (result.done) {
|
||||
this.$$ticker = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
while (true) {
|
||||
this.sandbox.locals.elapsed = elapsed;
|
||||
let async, done, value;
|
||||
try {
|
||||
({async, done, value} = this.sandbox.step());
|
||||
}
|
||||
catch (error) {
|
||||
console.warn(this.sandbox.$$stack);
|
||||
console.warn(error);
|
||||
if (resolve) {
|
||||
resolve();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (async || value instanceof Promise) {
|
||||
this.promise = value;
|
||||
value
|
||||
.catch(reject ? reject : () => {})
|
||||
.then(() => {
|
||||
if (done) {
|
||||
if (resolve) {
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
this.promise = null;
|
||||
});
|
||||
break;
|
||||
}
|
||||
if (done) {
|
||||
if (resolve) {
|
||||
resolve();
|
||||
}
|
||||
break;
|
||||
else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (!this.iterator) {
|
||||
this.iterator = this.fn(this.locals);
|
||||
}
|
||||
let result;
|
||||
do {
|
||||
result = this.iterator.next(this.locals.elapsed);
|
||||
if (result.value instanceof Ticker) {
|
||||
this.$$ticker = result.value;
|
||||
const tickerResult = this.$$ticker.tick(elapsed);
|
||||
this.locals.elapsed -= tickerResult.value;
|
||||
if (tickerResult.done) {
|
||||
this.$$ticker = null;
|
||||
}
|
||||
else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} while (this.locals.elapsed > 0 && !result.done);
|
||||
if (result.done) {
|
||||
this.reset();
|
||||
}
|
||||
return result.done;
|
||||
}
|
||||
|
||||
ticker() {
|
||||
return new PromiseUtil.Ticker(
|
||||
() => {},
|
||||
(elapsed, resolve, reject) => {
|
||||
this.tick(elapsed, resolve, reject);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
static ticker(code, context = {}) {
|
||||
let ticker;
|
||||
return new PromiseUtil.Ticker(
|
||||
(resolve) => {
|
||||
this.fromCode(code, context)
|
||||
.then((script) => {
|
||||
ticker = script.ticker();
|
||||
resolve(ticker);
|
||||
})
|
||||
},
|
||||
(elapsed) => {
|
||||
ticker?.tick?.(elapsed);
|
||||
},
|
||||
);
|
||||
const self = this;
|
||||
return new Ticker(function* () {
|
||||
while (true) {
|
||||
const elapsed = yield;
|
||||
if (self.tick(elapsed)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function register({default: scripts}) {
|
||||
for (const path in scripts) {
|
||||
Script.register(path.slice('../..'.length), scripts[path]);
|
||||
}
|
||||
}
|
||||
|
||||
register(await import('./scripts.js'));
|
||||
|
||||
if (import.meta.hot) {
|
||||
import.meta.hot.accept('./scripts.js', (M) => {
|
||||
register(M);
|
||||
});
|
||||
}
|
||||
|
|
5
app/util/scripts.js
Normal file
5
app/util/scripts.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
export default import.meta.glob(
|
||||
'../../resources/**/*.js',
|
||||
{eager: true, import: 'default'},
|
||||
);
|
||||
|
52
app/util/ticker.js
Normal file
52
app/util/ticker.js
Normal file
|
@ -0,0 +1,52 @@
|
|||
export default class Ticker {
|
||||
constructor(fn) {
|
||||
this.fn = fn;
|
||||
this.reset();
|
||||
}
|
||||
static all(tickers) {
|
||||
tickers = [...tickers];
|
||||
return new this(function* all() {
|
||||
let consumed = 0;
|
||||
while (tickers.length > 0) {
|
||||
const elapsed = yield consumed;
|
||||
consumed = 0;
|
||||
for (let i = 0; i < tickers.length; ++i) {
|
||||
const result = tickers[i].tick(elapsed);
|
||||
if (result.value > consumed) {
|
||||
consumed = result.value;
|
||||
}
|
||||
if (result.done) {
|
||||
tickers.splice(i, 1);
|
||||
i -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return consumed;
|
||||
});
|
||||
}
|
||||
reset() {
|
||||
this.iterator = null;
|
||||
}
|
||||
tick(elapsed) {
|
||||
let result;
|
||||
if (!this.iterator) {
|
||||
this.iterator = this.fn();
|
||||
result = this.iterator.next();
|
||||
}
|
||||
if (!result || !result.done) {
|
||||
result = this.iterator.next(elapsed);
|
||||
}
|
||||
if (result.done) {
|
||||
this.reset();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
static wait(seconds = 0) {
|
||||
return new this(function* wait() {
|
||||
while (seconds > 0) {
|
||||
const elapsed = yield;
|
||||
seconds -= elapsed;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
35
app/util/ticker.test.js
Normal file
35
app/util/ticker.test.js
Normal file
|
@ -0,0 +1,35 @@
|
|||
import {expect, test} from 'vitest';
|
||||
|
||||
import Ticker from './ticker.js';
|
||||
|
||||
function wait(seconds = 0) {
|
||||
return new Ticker(function* wait() {
|
||||
let elapsed = 0;
|
||||
while (seconds > 0) {
|
||||
elapsed = yield elapsed;
|
||||
seconds -= elapsed;
|
||||
}
|
||||
return seconds + elapsed;
|
||||
});
|
||||
}
|
||||
|
||||
test('runs ticker', async () => {
|
||||
const ticker = wait(1);
|
||||
expect(ticker.tick(0.4)).to.deep.equal({done: false, value: 0.4});
|
||||
expect(ticker.tick(0.4)).to.deep.equal({done: false, value: 0.4});
|
||||
const result = ticker.tick(0.4);
|
||||
expect(result.done).to.be.true;
|
||||
expect(result.value).to.be.closeTo(0.2, 0.0001);
|
||||
});
|
||||
|
||||
test('runs all tickers', async () => {
|
||||
const ticker = Ticker.all([
|
||||
wait(0.5),
|
||||
wait(0.3),
|
||||
]);
|
||||
expect(ticker.tick(0.2)).to.deep.equal({done: false, value: 0.2});
|
||||
expect(ticker.tick(0.2)).to.deep.equal({done: false, value: 0.2});
|
||||
const result = ticker.tick(0.2);
|
||||
expect(result.done).to.be.true;
|
||||
expect(result.value).to.be.closeTo(0.1, 0.0001);
|
||||
});
|
|
@ -1,10 +1,14 @@
|
|||
import {Ticker} from '@/util/promise.js';
|
||||
import Ticker from '@/util/ticker.js';
|
||||
|
||||
import * as Easing from './easing';
|
||||
|
||||
export default function transition(object, properties) {
|
||||
const transitions = {};
|
||||
for (const key in properties) {
|
||||
const keys = new Set(Object.keys(properties));
|
||||
function stop() {
|
||||
keys.clear();
|
||||
}
|
||||
for (const key of keys) {
|
||||
const property = properties[key];
|
||||
const transition = {
|
||||
elapsed: 0,
|
||||
|
@ -19,30 +23,31 @@ export default function transition(object, properties) {
|
|||
transition.easing = Easing[transition.easing];
|
||||
}
|
||||
}
|
||||
transition.stop = () => {
|
||||
keys.delete(key);
|
||||
},
|
||||
transitions[key] = transition;
|
||||
}
|
||||
let stop;
|
||||
const promise = new Ticker(
|
||||
(resolve) => {
|
||||
stop = resolve;
|
||||
},
|
||||
(elapsed, resolve) => {
|
||||
for (const key in transitions) {
|
||||
const ticker = new Ticker(function* () {
|
||||
while (keys.size > 0) {
|
||||
const elapsed = yield;
|
||||
for (const key of keys) {
|
||||
const transition = transitions[key];
|
||||
transition.elapsed += elapsed;
|
||||
if (transition.elapsed >= transition.duration) {
|
||||
object[key] = transition.start + transition.magnitude;
|
||||
resolve();
|
||||
return;
|
||||
transition.stop();
|
||||
}
|
||||
else {
|
||||
object[key] = transition.easing(
|
||||
transition.elapsed,
|
||||
transition.start,
|
||||
transition.magnitude,
|
||||
transition.duration,
|
||||
);
|
||||
}
|
||||
object[key] = transition.easing(
|
||||
transition.elapsed,
|
||||
transition.start,
|
||||
transition.magnitude,
|
||||
transition.duration,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
return {stop, transitions, promise};
|
||||
}
|
||||
});
|
||||
return {stop, ticker, transitions};
|
||||
}
|
||||
|
|
91
app/util/transition.test.js
Normal file
91
app/util/transition.test.js
Normal file
|
@ -0,0 +1,91 @@
|
|||
import {expect, test} from 'vitest';
|
||||
|
||||
import transition from './transition.js';
|
||||
|
||||
test('mutates', () => {
|
||||
const O = {x: 0};
|
||||
const {ticker} = transition(
|
||||
O,
|
||||
{
|
||||
x: {
|
||||
duration: 1,
|
||||
easing: 'linear',
|
||||
magnitude: 0.5,
|
||||
},
|
||||
},
|
||||
);
|
||||
expect(O.x).to.equal(0);
|
||||
ticker.tick(0.5);
|
||||
expect(O.x).to.equal(0.25);
|
||||
ticker.tick(0.5);
|
||||
expect(O.x).to.equal(0.5);
|
||||
ticker.tick(0.5);
|
||||
expect(O.x).to.equal(0.5);
|
||||
});
|
||||
|
||||
test('does not overshoot', () => {
|
||||
const O = {x: 0};
|
||||
const {ticker} = transition(
|
||||
O,
|
||||
{
|
||||
x: {
|
||||
duration: 1,
|
||||
easing: 'linear',
|
||||
magnitude: 0.5,
|
||||
},
|
||||
},
|
||||
);
|
||||
ticker.tick(0.6);
|
||||
expect(O.x).to.equal(0.3);
|
||||
ticker.tick(0.6);
|
||||
expect(O.x).to.equal(0.5);
|
||||
});
|
||||
|
||||
test('stops mutating', () => {
|
||||
const O = {x: 0, y: 0.25};
|
||||
const {stop, ticker, transitions} = transition(
|
||||
O,
|
||||
{
|
||||
x: {
|
||||
duration: 1,
|
||||
easing: 'linear',
|
||||
magnitude: 0.5,
|
||||
},
|
||||
y: {
|
||||
duration: 1,
|
||||
easing: 'linear',
|
||||
magnitude: 0.5,
|
||||
},
|
||||
},
|
||||
);
|
||||
ticker.tick(0.3);
|
||||
expect(O.x).to.equal(0.15);
|
||||
expect(O.y).to.equal(0.4);
|
||||
transitions.x.stop();
|
||||
ticker.tick(0.3);
|
||||
expect(O.x).to.equal(0.15);
|
||||
expect(O.y).to.equal(0.55);
|
||||
stop();
|
||||
ticker.tick(0.3);
|
||||
expect(O.x).to.equal(0.15);
|
||||
expect(O.y).to.equal(0.55);
|
||||
});
|
||||
|
||||
test('exposes transitions', () => {
|
||||
const O = {x: 0};
|
||||
const {ticker, transitions} = transition(
|
||||
O,
|
||||
{
|
||||
x: {
|
||||
duration: 1,
|
||||
easing: 'linear',
|
||||
magnitude: 0.5,
|
||||
},
|
||||
},
|
||||
);
|
||||
ticker.tick(0.6);
|
||||
expect(O.x).to.equal(0.3);
|
||||
transitions.x.duration = 1.8;
|
||||
ticker.tick(0.6);
|
||||
expect(O.x).to.be.closeTo(0.3333, 0.0001);
|
||||
});
|
7217
package-lock.json
generated
7217
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
|
@ -27,16 +27,13 @@
|
|||
"@remix-run/express": "^2.9.2",
|
||||
"@remix-run/node": "^2.9.2",
|
||||
"@remix-run/react": "^2.9.2",
|
||||
"acorn": "^8.12.0",
|
||||
"alea": "^1.0.1",
|
||||
"astride": "file:../astride",
|
||||
"compression": "^1.7.4",
|
||||
"cross-env": "^7.0.3",
|
||||
"express": "^4.18.2",
|
||||
"idb-keyval": "^6.2.1",
|
||||
"isbot": "^4.1.0",
|
||||
"kefir": "^3.8.8",
|
||||
"lru-cache": "^10.2.2",
|
||||
"morgan": "^1.10.0",
|
||||
"pixi.js": "^7.4.2",
|
||||
"react": "^18.2.0",
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
{"frames":{"":{"frame":{"x":0,"y":0,"w":128,"h":160},"spriteSourceSize":{"x":0,"y":0,"w":128,"h":160},"sourceSize":{"w":128,"h":160}}},"meta":{"format":"RGBA8888","image":"./blossom.png","scale":1,"size":{"w":128,"h":160}}}
|
31
resources/ambient/blossom.sprite.json
Normal file
31
resources/ambient/blossom.sprite.json
Normal file
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"frames": {
|
||||
"": {
|
||||
"frame": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"w": 128,
|
||||
"h": 160
|
||||
},
|
||||
"spriteSourceSize": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"w": 128,
|
||||
"h": 160
|
||||
},
|
||||
"sourceSize": {
|
||||
"w": 128,
|
||||
"h": 160
|
||||
}
|
||||
}
|
||||
},
|
||||
"meta": {
|
||||
"format": "RGBA8888",
|
||||
"image": "./blossom.png",
|
||||
"scale": 1,
|
||||
"size": {
|
||||
"w": 128,
|
||||
"h": 160
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
{"frames":{"":{"frame":{"x":0,"y":0,"w":32,"h":32},"spriteSourceSize":{"x":0,"y":0,"w":32,"h":32},"sourceSize":{"w":32,"h":32}}},"meta":{"format":"RGBA8888","image":"./flower.png","scale":1,"size":{"w":32,"h":32}}}
|
31
resources/ambient/flower.sprite.json
Normal file
31
resources/ambient/flower.sprite.json
Normal file
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"frames": {
|
||||
"": {
|
||||
"frame": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"w": 32,
|
||||
"h": 32
|
||||
},
|
||||
"spriteSourceSize": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"w": 32,
|
||||
"h": 32
|
||||
},
|
||||
"sourceSize": {
|
||||
"w": 32,
|
||||
"h": 32
|
||||
}
|
||||
}
|
||||
},
|
||||
"meta": {
|
||||
"format": "RGBA8888",
|
||||
"image": "./flower.png",
|
||||
"scale": 1,
|
||||
"size": {
|
||||
"w": 32,
|
||||
"h": 32
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
{"frames":{"":{"frame":{"x":0,"y":0,"w":32,"h":32},"spriteSourceSize":{"x":0,"y":0,"w":32,"h":32},"sourceSize":{"w":32,"h":32}}},"meta":{"format":"RGBA8888","image":"./shrub.png","scale":1,"size":{"w":32,"h":32}}}
|
31
resources/ambient/shrub.sprite.json
Normal file
31
resources/ambient/shrub.sprite.json
Normal file
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"frames": {
|
||||
"": {
|
||||
"frame": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"w": 32,
|
||||
"h": 32
|
||||
},
|
||||
"spriteSourceSize": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"w": 32,
|
||||
"h": 32
|
||||
},
|
||||
"sourceSize": {
|
||||
"w": 32,
|
||||
"h": 32
|
||||
}
|
||||
}
|
||||
},
|
||||
"meta": {
|
||||
"format": "RGBA8888",
|
||||
"image": "./shrub.png",
|
||||
"scale": 1,
|
||||
"size": {
|
||||
"w": 32,
|
||||
"h": 32
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
{"frames":{"":{"frame":{"x":0,"y":0,"w":96,"h":160},"spriteSourceSize":{"x":0,"y":0,"w":96,"h":160},"sourceSize":{"w":96,"h":160}}},"meta":{"format":"RGBA8888","image":"./tree.png","scale":1,"size":{"w":96,"h":160}}}
|
31
resources/ambient/tree.sprite.json
Normal file
31
resources/ambient/tree.sprite.json
Normal file
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"frames": {
|
||||
"": {
|
||||
"frame": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"w": 96,
|
||||
"h": 160
|
||||
},
|
||||
"spriteSourceSize": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"w": 96,
|
||||
"h": 160
|
||||
},
|
||||
"sourceSize": {
|
||||
"w": 96,
|
||||
"h": 160
|
||||
}
|
||||
}
|
||||
},
|
||||
"meta": {
|
||||
"format": "RGBA8888",
|
||||
"image": "./tree.png",
|
||||
"scale": 1,
|
||||
"size": {
|
||||
"w": 96,
|
||||
"h": 160
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,110 +1,114 @@
|
|||
const {Collider, Controlled, Interacts, Inventory, Sound, Sprite} = wielder
|
||||
const entities = Collider.closest(Interacts.aabb());
|
||||
for (const entity of entities) {
|
||||
const {Emitter, Position, Tags} = entity;
|
||||
if (Tags && Tags.has('kittan')) {
|
||||
Controlled.locked = 1
|
||||
const [, direction] = Sprite.animation.split(':')
|
||||
for (let i = 0; i < 2; ++i) {
|
||||
Sound.play('/resources/brush/brush.wav');
|
||||
Sprite.animation = ['moving', direction].join(':');
|
||||
await wait(0.3)
|
||||
Sprite.animation = ['idle', direction].join(':');
|
||||
await wait(0.1)
|
||||
import Ticker from '@/util/ticker.js';
|
||||
|
||||
export default function*({wielder}) {
|
||||
const {Collider, Controlled, Interacts, Inventory, Sound, Sprite} = wielder
|
||||
const entities = Collider.closest(Interacts.aabb());
|
||||
for (const entity of entities) {
|
||||
const {Emitter, Position, Tags} = entity;
|
||||
if (Tags && Tags.has('kittan')) {
|
||||
Controlled.locked = 1
|
||||
const [, direction] = Sprite.animation.split(':')
|
||||
for (let i = 0; i < 2; ++i) {
|
||||
Sound.play('/resources/brush/brush.wav');
|
||||
Sprite.animation = ['moving', direction].join(':');
|
||||
yield Ticker.wait(0.3)
|
||||
Sprite.animation = ['idle', direction].join(':');
|
||||
yield Ticker.wait(0.1)
|
||||
}
|
||||
Inventory.give({
|
||||
qty: 1,
|
||||
source: '/resources/furball/furball.item.json',
|
||||
});
|
||||
Controlled.locked = 0;
|
||||
|
||||
const heartParticles = {
|
||||
behaviors: [
|
||||
{
|
||||
type: 'moveAcceleration',
|
||||
config: {
|
||||
accel: {
|
||||
x: 0,
|
||||
y: -100,
|
||||
},
|
||||
minStart: 0,
|
||||
maxStart: 0,
|
||||
rotate: false,
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'moveSpeed',
|
||||
config: {
|
||||
speed: {
|
||||
list: [
|
||||
{
|
||||
time: 0,
|
||||
value: 30
|
||||
},
|
||||
{
|
||||
time: 1,
|
||||
value: 0
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'scale',
|
||||
config: {
|
||||
scale: {
|
||||
list: [
|
||||
{
|
||||
value: 0.5,
|
||||
time: 0,
|
||||
},
|
||||
{
|
||||
value: 0.125,
|
||||
time: 1,
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'textureSingle',
|
||||
config: {
|
||||
texture: '/resources/heart/heart.png',
|
||||
}
|
||||
},
|
||||
],
|
||||
lifetime: {
|
||||
min: 0.5,
|
||||
max: 0.5,
|
||||
},
|
||||
frequency: 0.1,
|
||||
emitterLifetime: 0.25,
|
||||
pos: {
|
||||
x: 0,
|
||||
y: 0
|
||||
},
|
||||
rotation: 180,
|
||||
};
|
||||
|
||||
|
||||
Emitter.emit({
|
||||
...heartParticles,
|
||||
behaviors: [
|
||||
...heartParticles.behaviors,
|
||||
{
|
||||
type: 'spawnShape',
|
||||
config: {
|
||||
type: 'rect',
|
||||
data: {
|
||||
x: Position.x - 8,
|
||||
y: Position.y,
|
||||
w: 16,
|
||||
h: 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
break;
|
||||
}
|
||||
Inventory.give({
|
||||
qty: 1,
|
||||
source: '/resources/furball/furball.json',
|
||||
});
|
||||
Controlled.locked = 0;
|
||||
|
||||
const heartParticles = {
|
||||
behaviors: [
|
||||
{
|
||||
type: 'moveAcceleration',
|
||||
config: {
|
||||
accel: {
|
||||
x: 0,
|
||||
y: -100,
|
||||
},
|
||||
minStart: 0,
|
||||
maxStart: 0,
|
||||
rotate: false,
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'moveSpeed',
|
||||
config: {
|
||||
speed: {
|
||||
list: [
|
||||
{
|
||||
time: 0,
|
||||
value: 30
|
||||
},
|
||||
{
|
||||
time: 1,
|
||||
value: 0
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'scale',
|
||||
config: {
|
||||
scale: {
|
||||
list: [
|
||||
{
|
||||
value: 0.5,
|
||||
time: 0,
|
||||
},
|
||||
{
|
||||
value: 0.125,
|
||||
time: 1,
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'textureSingle',
|
||||
config: {
|
||||
texture: '/resources/heart/heart.png',
|
||||
}
|
||||
},
|
||||
],
|
||||
lifetime: {
|
||||
min: 0.5,
|
||||
max: 0.5,
|
||||
},
|
||||
frequency: 0.1,
|
||||
emitterLifetime: 0.25,
|
||||
pos: {
|
||||
x: 0,
|
||||
y: 0
|
||||
},
|
||||
rotation: 180,
|
||||
};
|
||||
|
||||
|
||||
Emitter.emit({
|
||||
...heartParticles,
|
||||
behaviors: [
|
||||
...heartParticles.behaviors,
|
||||
{
|
||||
type: 'spawnShape',
|
||||
config: {
|
||||
type: 'rect',
|
||||
data: {
|
||||
x: Position.x - 8,
|
||||
y: Position.y,
|
||||
w: 16,
|
||||
h: 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
{"frames":{"":{"frame":{"x":0,"y":0,"w":22,"h":22},"spriteSourceSize":{"x":0,"y":0,"w":22,"h":22},"sourceSize":{"w":22,"h":22}}},"meta":{"format":"RGBA8888","image":"./chest.png","scale":1,"size":{"w":22,"h":22}}}
|
31
resources/chest/chest.sprite.json
Normal file
31
resources/chest/chest.sprite.json
Normal file
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"frames": {
|
||||
"": {
|
||||
"frame": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"w": 22,
|
||||
"h": 22
|
||||
},
|
||||
"spriteSourceSize": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"w": 22,
|
||||
"h": 22
|
||||
},
|
||||
"sourceSize": {
|
||||
"w": 22,
|
||||
"h": 22
|
||||
}
|
||||
}
|
||||
},
|
||||
"meta": {
|
||||
"format": "RGBA8888",
|
||||
"image": "./chest.png",
|
||||
"scale": 1,
|
||||
"size": {
|
||||
"w": 22,
|
||||
"h": 22
|
||||
}
|
||||
}
|
||||
}
|
10
resources/chest/interact.js
Normal file
10
resources/chest/interact.js
Normal file
|
@ -0,0 +1,10 @@
|
|||
export default function* ({initiator, subject}) {
|
||||
initiator.Player.openInventory = subject.Inventory;
|
||||
// subject.Interlocutor.dialogue({
|
||||
// body: "Sure, I'm a treasure chest. Probably. Do you really think that means you're about to get some treasure? Hah!",
|
||||
// monopolizer: true,
|
||||
// offset: {x: 0, y: -48},
|
||||
// origin: 'track',
|
||||
// position: 'track',
|
||||
// })
|
||||
}
|
7
resources/combat/projectile/base.entity.json
Normal file
7
resources/combat/projectile/base.entity.json
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"Controlled": {},
|
||||
"Forces": {},
|
||||
"Speed": {},
|
||||
"Ticking": {},
|
||||
"VisibleAabb": {}
|
||||
}
|
11
resources/combat/projectile/diffuse.js
Normal file
11
resources/combat/projectile/diffuse.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
import Ticker from '@/util/ticker.js';
|
||||
|
||||
export default function *({Controlled, Direction, Speed}, duration, speed) {
|
||||
let accumulated = 0;
|
||||
while (accumulated <= duration) {
|
||||
Controlled.stop();
|
||||
Controlled.directionMove(Direction.direction);
|
||||
accumulated += yield Ticker.wait();
|
||||
Speed.speed = speed * (1 - (accumulated / duration));
|
||||
}
|
||||
}
|
9
resources/combat/projectile/toward.js
Normal file
9
resources/combat/projectile/toward.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
import * as Math from '@/util/math.js';
|
||||
import Ticker from '@/util/ticker.js';
|
||||
|
||||
export default function*({Controlled, Direction, Position}, {x, y}, duration) {
|
||||
const toward = Math.atan2(y - Position.y, x - Position.x);
|
||||
Direction.direction = (Math.TAU + toward) % Math.TAU;
|
||||
Controlled.directionMove(Direction.direction);
|
||||
yield Ticker.wait(duration);
|
||||
}
|
File diff suppressed because one or more lines are too long
1247
resources/dude/dude.sprite.json
Normal file
1247
resources/dude/dude.sprite.json
Normal file
File diff suppressed because it is too large
Load Diff
|
@ -1,17 +1,15 @@
|
|||
entity.Direction.direction = Math.random() * Math.TAU;
|
||||
import * as Math from '@/util/math.js';
|
||||
import Ticker from '@/util/ticker.js';
|
||||
|
||||
entity.Controlled.directionMove(entity.Direction.direction);
|
||||
|
||||
await wait(0.25 + Math.random() * 2.25);
|
||||
|
||||
entity.Controlled.stop();
|
||||
|
||||
entity.Sprite.isAnimating = 0;
|
||||
|
||||
await wait(1 + Math.random() * 3);
|
||||
|
||||
entity.Direction.direction = Math.random() * Math.TAU;
|
||||
|
||||
await wait(0.5 + Math.random() * 2.5);
|
||||
|
||||
entity.Sprite.isAnimating = 1;
|
||||
export default function*({entity}) {
|
||||
const {Controlled, Direction, Sprite} = entity;
|
||||
Direction.direction = Math.random() * Math.TAU;
|
||||
Controlled.directionMove(Direction.direction);
|
||||
yield Ticker.wait(0.25 + Math.random() * 2.25);
|
||||
Controlled.stop();
|
||||
Sprite.isAnimating = 0;
|
||||
yield Ticker.wait(1 + Math.random() * 3);
|
||||
Direction.direction = Math.random() * Math.TAU;
|
||||
yield Ticker.wait(0.5 + Math.random() * 2.5);
|
||||
Sprite.isAnimating = 1;
|
||||
}
|
16
resources/farm/animals/cow-adult/interact.js
Normal file
16
resources/farm/animals/cow-adult/interact.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
import * as Math from '@/util/math.js';
|
||||
|
||||
export default function* ({subject}) {
|
||||
const lines = [
|
||||
'sno<shake>rr</shake>t',
|
||||
'm<wave>ooooooooooo</wave>',
|
||||
];
|
||||
const line = lines[Math.floor(Math.random() * lines.length)];
|
||||
subject.Interlocutor.dialogue({
|
||||
body: line,
|
||||
linger: 2,
|
||||
offset: {x: 0, y: -16},
|
||||
origin: 'track',
|
||||
position: 'track',
|
||||
})
|
||||
}
|
|
@ -1,17 +1,15 @@
|
|||
entity.Direction.direction = Math.random() * Math.TAU;
|
||||
import * as Math from '@/util/math.js';
|
||||
import Ticker from '@/util/ticker.js';
|
||||
|
||||
entity.Controlled.directionMove(entity.Direction.direction);
|
||||
|
||||
await wait(0.25 + Math.random() * 2.25);
|
||||
|
||||
entity.Controlled.stop();
|
||||
|
||||
entity.Sprite.isAnimating = 0;
|
||||
|
||||
await wait(1 + Math.random() * 3);
|
||||
|
||||
entity.Direction.direction = Math.random() * Math.TAU;
|
||||
|
||||
await wait(0.5 + Math.random() * 2.5);
|
||||
|
||||
entity.Sprite.isAnimating = 1;
|
||||
export default function*({entity}) {
|
||||
const {Controlled, Direction, Sprite} = entity;
|
||||
Direction.direction = Math.random() * Math.TAU;
|
||||
Controlled.directionMove(Direction.direction);
|
||||
yield Ticker.wait(0.25 + Math.random() * 2.25);
|
||||
Controlled.stop();
|
||||
Sprite.isAnimating = 0;
|
||||
yield Ticker.wait(1 + Math.random() * 3);
|
||||
Direction.direction = Math.random() * Math.TAU;
|
||||
yield Ticker.wait(0.5 + Math.random() * 2.5);
|
||||
Sprite.isAnimating = 1;
|
||||
}
|
15
resources/farm/animals/goat-white/interact.js
Normal file
15
resources/farm/animals/goat-white/interact.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
import * as Math from '@/util/math.js';
|
||||
|
||||
export default function* ({subject}) {
|
||||
const lines = [
|
||||
'Mind your own business, buddy.\n\ner, I mean, <shake>MEEHHHHHH</shake>',
|
||||
];
|
||||
const line = lines[Math.floor(Math.random() * lines.length)];
|
||||
subject.Interlocutor.dialogue({
|
||||
body: line,
|
||||
linger: 2,
|
||||
offset: {x: 0, y: -16},
|
||||
origin: 'track',
|
||||
position: 'track',
|
||||
})
|
||||
}
|
5
resources/hoe/dirt-particle.js
Normal file
5
resources/hoe/dirt-particle.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
import delta from '@/util/delta.js';
|
||||
|
||||
export default function*({entity}) {
|
||||
yield delta(entity.Forces, {forceY: {delta: 480, duration: 0.125}}).ticker;
|
||||
}
|
|
@ -4,11 +4,9 @@
|
|||
"price": 100,
|
||||
"projectionCheck": "/resources/hoe/projection-check.js",
|
||||
"projection": {
|
||||
"distance": [3, -1],
|
||||
"distance": [1, 0],
|
||||
"grid": [
|
||||
[1, 1, 1],
|
||||
[1, 1, 1],
|
||||
[1, 1, 1]
|
||||
[1]
|
||||
]
|
||||
},
|
||||
"start": "/resources/hoe/start.js"
|
|
@ -1,15 +1,14 @@
|
|||
const layer0 = ecs.get(1).TileLayers.layer(0)
|
||||
const layer1 = ecs.get(1).TileLayers.layer(1)
|
||||
|
||||
const filtered = []
|
||||
|
||||
for (const position of projected) {
|
||||
if (
|
||||
[1, 2, 3, 4, 6].includes(layer0.tile(position))
|
||||
&& ![7].includes(layer1.tile(position))
|
||||
) {
|
||||
filtered.push(position)
|
||||
export default function*({ecs, projected}) {
|
||||
const layer0 = ecs.get(1).TileLayers.layer(0);
|
||||
const layer1 = ecs.get(1).TileLayers.layer(1);
|
||||
const filtered = [];
|
||||
for (const position of projected) {
|
||||
if (
|
||||
[1, 2, 3, 4, 6].includes(layer0.tile(position))
|
||||
&& ![7].includes(layer1.tile(position))
|
||||
) {
|
||||
filtered.push(position);
|
||||
}
|
||||
}
|
||||
return filtered;
|
||||
}
|
||||
|
||||
return filtered
|
||||
|
|
|
@ -1,67 +1,71 @@
|
|||
const {Direction, Position, Wielder} = wielder
|
||||
const projected = Wielder.activeItem()?.project(Position.tile, Direction.quantize(4))
|
||||
if (projected?.length > 0) {
|
||||
import Ticker from '@/util/ticker.js';
|
||||
|
||||
const {Controlled, Emitter, Sound, Sprite} = wielder
|
||||
const {TileLayers} = ecs.get(1)
|
||||
const layer = TileLayers.layer(0)
|
||||
export default function*({ecs, wielder}) {
|
||||
const {Direction, Position, Wielder} = wielder;
|
||||
const projected = Wielder.activeItem()?.project(Position.tile, Direction.quantize(4))
|
||||
if (projected?.length > 0) {
|
||||
|
||||
Controlled.locked = 1
|
||||
const [, direction] = Sprite.animation.split(':')
|
||||
const {Controlled, Emitter, Sound, Sprite} = wielder
|
||||
const {TileLayers} = ecs.get(1)
|
||||
const layer = TileLayers.layer(0)
|
||||
|
||||
for (let i = 0; i < 2; ++i) {
|
||||
Sound.play('/resources/hoe/dig.wav');
|
||||
for (const {x, y} of projected) {
|
||||
Emitter.emit({
|
||||
entity: {
|
||||
Behaving: {
|
||||
routines: {
|
||||
initial: 'await delta(entity.Forces, {forceY: {delta: 640, duration: 0.125}}).promise',
|
||||
Controlled.locked = 1
|
||||
const [, direction] = Sprite.animation.split(':')
|
||||
|
||||
for (let i = 0; i < 2; ++i) {
|
||||
Sound.play('/resources/hoe/dig.wav');
|
||||
for (const {x, y} of projected) {
|
||||
Emitter.emit({
|
||||
entity: {
|
||||
Behaving: {
|
||||
routines: {
|
||||
initial: '/resources/hoe/dirt-particle.js',
|
||||
},
|
||||
},
|
||||
Forces: {forceY: -80},
|
||||
Position: {
|
||||
x: x * layer.tileSize.x + (layer.tileSize.x / 2),
|
||||
y: y * layer.tileSize.y + (layer.tileSize.y / 2),
|
||||
},
|
||||
Sprite: {
|
||||
tint: 0x552200,
|
||||
},
|
||||
Ttl: {ttl: 0.35},
|
||||
},
|
||||
Forces: {forceY: -80},
|
||||
Position: {
|
||||
x: x * layer.tileSize.x + (layer.tileSize.x / 2),
|
||||
y: y * layer.tileSize.y + (layer.tileSize.y / 2),
|
||||
fields: [
|
||||
{
|
||||
path: ['Sprite', 'lightness'],
|
||||
value: [0.05, 0.25],
|
||||
},
|
||||
{
|
||||
path: ['Sprite', 'alpha'],
|
||||
value: [0.5, 1],
|
||||
},
|
||||
{
|
||||
path: ['Sprite', 'scale'],
|
||||
value: [0.05, 0.1],
|
||||
},
|
||||
],
|
||||
frequency: 0.05,
|
||||
shape: {
|
||||
type: 'filledRect',
|
||||
payload: {width: 12, height: 12},
|
||||
},
|
||||
Sprite: {
|
||||
tint: 0x552200,
|
||||
},
|
||||
Ttl: {ttl: 0.35},
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
path: ['Sprite', 'lightness'],
|
||||
value: [0.05, 0.25],
|
||||
},
|
||||
{
|
||||
path: ['Sprite', 'alpha'],
|
||||
value: [0.5, 1],
|
||||
},
|
||||
{
|
||||
path: ['Sprite', 'scale'],
|
||||
value: [0.05, 0.1],
|
||||
},
|
||||
],
|
||||
frequency: 0.05,
|
||||
shape: {
|
||||
type: 'filledRect',
|
||||
payload: {width: 12, height: 12},
|
||||
},
|
||||
spurt: 5,
|
||||
ttl: 0.4,
|
||||
});
|
||||
spurt: 5,
|
||||
ttl: 0.4,
|
||||
});
|
||||
}
|
||||
Sprite.animation = ['moving', direction].join(':');
|
||||
yield Ticker.wait(0.3)
|
||||
Sprite.animation = ['idle', direction].join(':');
|
||||
yield Ticker.wait(0.1)
|
||||
}
|
||||
Sprite.animation = ['moving', direction].join(':');
|
||||
await wait(0.3)
|
||||
Sprite.animation = ['idle', direction].join(':');
|
||||
await wait(0.1)
|
||||
|
||||
for (const position of projected) {
|
||||
TileLayers.layer(1).stamp(position, [[7]])
|
||||
}
|
||||
|
||||
Controlled.locked = 0;
|
||||
|
||||
}
|
||||
|
||||
for (const position of projected) {
|
||||
TileLayers.layer(1).stamp(position, [[7]])
|
||||
}
|
||||
|
||||
Controlled.locked = 0;
|
||||
|
||||
}
|
||||
|
|
14
resources/homestead/house-teleport/collision-start.js
Normal file
14
resources/homestead/house-teleport/collision-start.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
export default function* ({ecs, other}) {
|
||||
if (other.Player) {
|
||||
ecs.switchEcs(
|
||||
other,
|
||||
['houses', other.Player.id].join('/'),
|
||||
{
|
||||
Position: {
|
||||
x: 72,
|
||||
y: 304,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
14
resources/homestead/town-teleport/collision-start.js
Normal file
14
resources/homestead/town-teleport/collision-start.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
export default function* ({ecs, other}) {
|
||||
if (other.Player) {
|
||||
ecs.switchEcs(
|
||||
other,
|
||||
'town',
|
||||
{
|
||||
Position: {
|
||||
x: 940,
|
||||
y: 480,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
14
resources/house/homestead-teleport/collision-start.js
Normal file
14
resources/house/homestead-teleport/collision-start.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
export default function* ({ecs, other}) {
|
||||
if (other.Player) {
|
||||
ecs.switchEcs(
|
||||
other,
|
||||
['homesteads', other.Player.id].join('/'),
|
||||
{
|
||||
Position: {
|
||||
x: 74,
|
||||
y: 128,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,17 +1,15 @@
|
|||
entity.Direction.direction = Math.random() * Math.TAU;
|
||||
import * as Math from '@/util/math.js';
|
||||
import Ticker from '@/util/ticker.js';
|
||||
|
||||
entity.Controlled.directionMove(entity.Direction.direction);
|
||||
|
||||
await wait(0.25 + Math.random() * 2.25);
|
||||
|
||||
entity.Controlled.stop();
|
||||
|
||||
entity.Sprite.isAnimating = 0;
|
||||
|
||||
await wait(1 + Math.random() * 3);
|
||||
|
||||
entity.Direction.direction = Math.random() * Math.TAU;
|
||||
|
||||
await wait(0.5 + Math.random() * 2.5);
|
||||
|
||||
entity.Sprite.isAnimating = 1;
|
||||
export default function*({entity}) {
|
||||
const {Controlled, Direction, Sprite} = entity;
|
||||
Direction.direction = Math.random() * Math.TAU;
|
||||
Controlled.directionMove(Direction.direction);
|
||||
yield Ticker.wait(0.25 + Math.random() * 2.25);
|
||||
Controlled.stop();
|
||||
Sprite.isAnimating = 0;
|
||||
yield Ticker.wait(1 + Math.random() * 3);
|
||||
Direction.direction = Math.random() * Math.TAU;
|
||||
yield Ticker.wait(0.5 + Math.random() * 2.5);
|
||||
Sprite.isAnimating = 1;
|
||||
}
|
19
resources/kitty/interact.js
Normal file
19
resources/kitty/interact.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
import * as Math from '@/util/math.js';
|
||||
|
||||
export default function* ({subject}) {
|
||||
const lines = [
|
||||
'mrowwr',
|
||||
'p<shake>rrr</shake>o<wave>wwwww</wave>',
|
||||
'mew<rate frequency={0.5}> </rate>mew!',
|
||||
'me<wave>wwwww</wave>',
|
||||
'\\*pu<shake>rrrrr</shake>\\*',
|
||||
];
|
||||
const line = lines[Math.floor(Math.random() * lines.length)];
|
||||
subject.Interlocutor.dialogue({
|
||||
body: line,
|
||||
linger: 2,
|
||||
offset: {x: 0, y: -16},
|
||||
origin: 'track',
|
||||
position: 'track',
|
||||
});
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
const playerEntity = ecs.lookupPlayerEntity(entity.Owned.owner);
|
||||
if (playerEntity !== other && other.Vulnerable) {
|
||||
const magnitude = Math.floor(Math.random() * 2)
|
||||
other.Vulnerable.damage({
|
||||
amount: -Math.floor(
|
||||
Math.pow(10, magnitude)
|
||||
+ Math.random() * (Math.pow(10, magnitude + 1) - Math.pow(10, magnitude)),
|
||||
),
|
||||
position: other.Position.toJSON(),
|
||||
type: other.Vulnerable.Types.PAIN,
|
||||
})
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user