refactor: sync

This commit is contained in:
cha0s 2024-10-16 05:51:04 -05:00
parent 71d35bd228
commit 611b41e96a
29 changed files with 245 additions and 323 deletions

View File

@ -58,7 +58,7 @@ const interpolate = () => {
}
requestAnimationFrame(interpolate);
onmessage = async (event) => {
onmessage = (event) => {
const packet = event.data;
switch (packet.type) {
case 'EcsChange': {

View File

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

View File

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

View File

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

View File

@ -27,20 +27,20 @@ export default class Alive extends Component {
}
};
}
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: {

View File

@ -14,21 +14,14 @@ export default class Behaving extends Component {
}
};
}
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'},

View File

@ -255,7 +255,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 +269,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,

View File

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

View File

@ -3,25 +3,30 @@ 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 = {};
// heavy handed...
if ('undefined' !== typeof window) {
return;
}
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);
scripts.startInstance = ecs.readScript(json.start);
}
if (json.stop) {
scripts.stopInstance = await ecs.readScript(json.stop);
scripts.stopInstance = ecs.readScript(json.stop);
}
this.json = json;
this.scripts = scripts;
}
project(position, direction) {
@ -128,7 +133,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 +146,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 +171,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 +194,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 +235,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,7 +260,7 @@ 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) {
@ -269,7 +274,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 +310,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) {

View File

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

View File

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

View File

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

View File

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

View File

@ -58,7 +58,7 @@ export default class Ecs {
});
}
async apply(patch) {
apply(patch) {
const creating = [];
const destroying = new Set();
const inserting = [];
@ -111,17 +111,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 +173,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 +192,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,7 +203,7 @@ export default class Ecs {
return this.createManySpecific(specificsList);
}
async createManySpecific(specificsList) {
createManySpecific(specificsList) {
if (0 === specificsList.length) {
return;
}
@ -231,11 +229,9 @@ 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];
this.rebuild(entityId, () => Object.keys(components));
@ -248,8 +244,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 +268,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 +290,7 @@ export default class Ecs {
]);
}
ecs.$$caret = max + 1;
await ecs.createManySpecific(specifics);
ecs.createManySpecific(specifics);
return ecs;
}
@ -352,11 +348,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 +367,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)))]);
}
@ -588,7 +582,7 @@ export default class Ecs {
};
}
async updateMany(entities) {
updateMany(entities) {
const updating = {};
const unique = new Set();
for (const [entityId, components] of entities) {
@ -600,11 +594,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);
}
}

View File

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

View File

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

View File

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

View File

@ -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 () => {

View File

@ -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) => {

View File

@ -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({

View File

@ -443,7 +443,7 @@ function createTomatoPlant() {
};
}
export default async function createHomestead(id) {
export default function createHomestead(id) {
const entities = [];
entities.push(createMaster());
entities.push(createShitShack(id));

View File

@ -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: {
@ -17,7 +17,7 @@ export default async function createHouse(Ecs, id) {
],
},
});
await ecs.create({
ecs.create({
Collider: {
bodies: [
{

View File

@ -1,4 +1,4 @@
export default async function createPlayer(id) {
export default function createPlayer(id) {
const player = {
Alive: {health: 100},
Camera: {},

View File

@ -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({

View File

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

View File

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

View File

@ -5,12 +5,11 @@ const N = 14;
const SPREAD = 1;
const creating = [];
const promises = []
const offset = Math.random() * Math.TAU;
for (let i = 0; i < N; ++i) {
promises.push(ecs.create({
creating.push(ecs.get(ecs.create({
Collider: {
bodies: [
{
@ -39,11 +38,7 @@ for (let i = 0; i < N; ++i) {
},
Ticking: {},
VisibleAabb: {},
}));
}
for (const id of await Promise.all(promises)) {
creating.push(ecs.get(id));
})));
}
const shot = creating.shift();

View File

@ -1,11 +1,11 @@
const {Interactive, Position, Plant, Sprite} = subject;
Interactive.interacting = false;
const promises = [];
const ids = [];
for (let i = 0; i < 10; ++i) {
promises.push(ecs.create({
ids.push(ecs.create({
Collider: {
bodies: [
{
@ -44,7 +44,6 @@ for (let i = 0; i < 10; ++i) {
}
const ids = await Promise.all(promises);
for (const id of ids) {
const tomato = ecs.get(id);

View File

@ -72,9 +72,8 @@ if (projected?.length > 0) {
Sprite.animation = ['idle', direction].join(':');
const promises = [];
for (const {x, y} of projected) {
promises.push(ecs.create({
ecs.create({
...plant,
Plant: {
...plant.Plant,
@ -84,9 +83,8 @@ if (projected?.length > 0) {
x: x * layer.tileSize.x + (0.5 * layer.tileSize.x),
y: y * layer.tileSize.y + (0.5 * layer.tileSize.y),
},
}))
});
}
await Promise.all(promises)
Controlled.locked = 0;