445 lines
13 KiB
JavaScript
445 lines
13 KiB
JavaScript
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';
|
|
|
|
const Components = wrapComponents([
|
|
['Empty', {}],
|
|
['Momentum', {x: {type: 'int32'}, y: {type: 'int32'}, z: {type: 'int32'}}],
|
|
['Name', {name: {type: 'string'}}],
|
|
['Position', {x: {type: 'int32', defaultValue: 32}, y: {type: 'int32'}, z: {type: 'int32'}}],
|
|
]);
|
|
|
|
const {
|
|
Empty,
|
|
Momentum,
|
|
Position,
|
|
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;
|
|
const ecs = new Ecs({
|
|
Systems: {
|
|
OneSystem: class extends System {
|
|
tick() {
|
|
oneCount += 1;
|
|
}
|
|
},
|
|
TwoSystem: class extends System {
|
|
tick() {
|
|
twoCount += 1;
|
|
}
|
|
},
|
|
},
|
|
});
|
|
ecs.tick();
|
|
expect(oneCount)
|
|
.to.equal(0);
|
|
expect(twoCount)
|
|
.to.equal(0);
|
|
ecs.system('OneSystem').active = true;
|
|
ecs.tick();
|
|
expect(oneCount)
|
|
.to.equal(1);
|
|
ecs.system('TwoSystem').active = true;
|
|
ecs.tick();
|
|
expect(oneCount)
|
|
.to.equal(2);
|
|
expect(twoCount)
|
|
.to.equal(1);
|
|
ecs.system('OneSystem').active = false;
|
|
ecs.tick();
|
|
expect(oneCount)
|
|
.to.equal(2);
|
|
expect(twoCount)
|
|
.to.equal(2);
|
|
});
|
|
|
|
test('creates entities with components', async () => {
|
|
const ecs = new Ecs({Components: {Empty, Position}});
|
|
const entity = await ecs.create({Empty: {}, Position: {y: 128}});
|
|
expect(JSON.stringify(ecs.get(entity)))
|
|
.to.deep.equal(JSON.stringify({Empty: {}, Position: {y: 128}}));
|
|
});
|
|
|
|
test("removes entities' components", async () => {
|
|
const ecs = new Ecs({Components: {Empty, Position}});
|
|
const entity = await 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 () => {
|
|
const ecs = new Ecs({Components: {Empty, Position}});
|
|
const entity = await ecs.create({Empty: {}, Position: {y: 128}});
|
|
expect(JSON.stringify(ecs.get(entity)))
|
|
.to.deep.equal(JSON.stringify({Empty: {}, Position: {y: 128}}));
|
|
});
|
|
|
|
test('destroys entities', async () => {
|
|
const ecs = new Ecs({Components: {Empty, Position}});
|
|
const entity = await ecs.create({Empty: {}, Position: {y: 128}});
|
|
expect(JSON.stringify(ecs.get(entity)))
|
|
.to.deep.equal(JSON.stringify({Empty: {}, Position: {y: 128}}));
|
|
expect(ecs.get(entity))
|
|
.to.not.be.undefined;
|
|
ecs.destroyMany(new Set([entity]));
|
|
expect(ecs.get(entity))
|
|
.to.be.undefined;
|
|
expect(() => {
|
|
ecs.destroyMany(new Set([entity]));
|
|
})
|
|
.to.throw();
|
|
});
|
|
|
|
test('inserts components into entities', async () => {
|
|
const ecs = new Ecs({Components: {Empty, Position}});
|
|
const entity = await ecs.create({Empty: {}});
|
|
await 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}});
|
|
expect(JSON.stringify(ecs.get(entity)))
|
|
.to.deep.equal(JSON.stringify({Empty: {}, Position: {y: 64}}));
|
|
});
|
|
|
|
test('ticks systems', async () => {
|
|
const ecs = new Ecs({
|
|
Components: {Momentum, Position},
|
|
Systems: {
|
|
Physics: class Physics extends System {
|
|
|
|
static queries() {
|
|
return {
|
|
default: ['Position', 'Momentum'],
|
|
};
|
|
}
|
|
|
|
tick(elapsed) {
|
|
for (const {Position, Momentum} of this.select('default')) {
|
|
Position.x += Momentum.x * elapsed;
|
|
Position.y += Momentum.y * elapsed;
|
|
Position.z += Momentum.z * elapsed;
|
|
}
|
|
}
|
|
|
|
},
|
|
},
|
|
});
|
|
ecs.system('Physics').active = true;
|
|
const entity = await ecs.create({Momentum: {}, Position: {y: 128}});
|
|
const position = JSON.stringify(ecs.get(entity).Position);
|
|
ecs.tick(1);
|
|
expect(JSON.stringify(ecs.get(entity).Position))
|
|
.to.deep.equal(position);
|
|
ecs.get(1).Momentum.y = 30;
|
|
ecs.tick(1);
|
|
expect(JSON.stringify(ecs.get(entity).Position))
|
|
.to.deep.equal(JSON.stringify({y: 128 + 30}));
|
|
});
|
|
|
|
test('schedules entities to be deleted when ticking systems', async () => {
|
|
const ecs = new Ecs({
|
|
Components: {Empty},
|
|
Systems: {
|
|
Despawn: class extends System {
|
|
static queries() {
|
|
return {
|
|
default: ['Empty'],
|
|
};
|
|
}
|
|
tick() {
|
|
this.ecs.destroy(1);
|
|
expect(ecs.get(1))
|
|
.to.not.be.undefined;
|
|
}
|
|
},
|
|
},
|
|
});
|
|
ecs.system('Despawn').active = true;
|
|
await ecs.create({Empty: {}});
|
|
ecs.tick(1);
|
|
expect(Array.from(ecs.system('Despawn').select('default')))
|
|
.to.have.lengthOf(0);
|
|
expect(ecs.get(1))
|
|
.to.be.undefined;
|
|
});
|
|
|
|
test('skips indexing detached entities', async () => {
|
|
const ecs = new Ecs({
|
|
Components: {Empty},
|
|
Systems: {
|
|
Indexer: class extends System {
|
|
static queries() {
|
|
return {
|
|
default: ['Empty'],
|
|
};
|
|
}
|
|
},
|
|
},
|
|
});
|
|
const {$$map: map} = ecs.system('Indexer').queries.default;
|
|
ecs.system('Indexer').active = true;
|
|
const attached = await ecs.create({Empty: {}});
|
|
ecs.tick(0);
|
|
expect(Array.from(map.keys()))
|
|
.to.deep.equal([attached]);
|
|
ecs.destroyMany(new Set([attached]));
|
|
ecs.tick(0);
|
|
expect(Array.from(map.keys()))
|
|
.to.deep.equal([]);
|
|
const detached = await ecs.createDetached({Empty: {}});
|
|
ecs.tick(0);
|
|
expect(Array.from(map.keys()))
|
|
.to.deep.equal([]);
|
|
ecs.destroyMany(new Set([detached]));
|
|
ecs.tick(0);
|
|
expect(Array.from(map.keys()))
|
|
.to.deep.equal([]);
|
|
});
|
|
|
|
test('generates diffs for entity creation', async () => {
|
|
const ecs = new Ecs();
|
|
let entity;
|
|
entity = await ecs.create();
|
|
expect(ecs.diff)
|
|
.to.deep.equal({[entity]: {}});
|
|
});
|
|
|
|
test('generates diffs for adding and removing components', async () => {
|
|
const ecs = new Ecs({Components: {Position}});
|
|
let entity;
|
|
entity = await ecs.create();
|
|
ecs.setClean();
|
|
await ecs.insert(entity, {Position: {x: 64}});
|
|
expect(ecs.diff)
|
|
.to.deep.equal({[entity]: {Position: {x: 64}}});
|
|
ecs.setClean();
|
|
expect(ecs.diff)
|
|
.to.deep.equal({});
|
|
ecs.remove(entity, ['Position']);
|
|
expect(ecs.diff)
|
|
.to.deep.equal({[entity]: {Position: false}});
|
|
});
|
|
|
|
test('generates diffs for empty components', async () => {
|
|
const ecs = new Ecs({Components: {Empty}});
|
|
let entity;
|
|
entity = await ecs.create({Empty: {}});
|
|
expect(ecs.diff)
|
|
.to.deep.equal({[entity]: {Empty: {}}});
|
|
ecs.setClean();
|
|
ecs.remove(entity, ['Empty']);
|
|
expect(ecs.diff)
|
|
.to.deep.equal({[entity]: {Empty: false}});
|
|
});
|
|
|
|
test('generates diffs for entity mutations', async () => {
|
|
const ecs = new Ecs({Components: {Position}});
|
|
let entity;
|
|
entity = await ecs.create({Position: {}});
|
|
ecs.setClean();
|
|
ecs.get(entity).Position.x = 128;
|
|
expect(ecs.diff)
|
|
.to.deep.equal({[entity]: {Position: {x: 128}}});
|
|
ecs.setClean();
|
|
expect(ecs.diff)
|
|
.to.deep.equal({});
|
|
});
|
|
|
|
test('generates no diffs for detached entities', async () => {
|
|
const ecs = new Ecs({Components: {Position}});
|
|
let entity;
|
|
entity = await ecs.createDetached();
|
|
expect(ecs.diff)
|
|
.to.deep.equal({});
|
|
await ecs.insert(entity, {Position: {x: 64}});
|
|
expect(ecs.diff)
|
|
.to.deep.equal({});
|
|
ecs.get(entity).Position.x = 128;
|
|
expect(ecs.diff)
|
|
.to.deep.equal({});
|
|
ecs.remove(entity, ['Position']);
|
|
expect(ecs.diff)
|
|
.to.deep.equal({});
|
|
});
|
|
|
|
test('generates coalesced diffs for components', async () => {
|
|
const ecs = new Ecs({Components: {Position}});
|
|
let entity;
|
|
entity = await ecs.create({Position});
|
|
ecs.remove(entity, ['Position']);
|
|
expect(ecs.diff)
|
|
.to.deep.equal({[entity]: {Position: false}});
|
|
await ecs.insert(entity, {Position: {}});
|
|
expect(ecs.diff)
|
|
.to.deep.equal({[entity]: {Position: {}}});
|
|
});
|
|
|
|
test('generates coalesced diffs for mutations', async () => {
|
|
const ecs = new Ecs({Components: {Position}});
|
|
let entity;
|
|
entity = await ecs.create({Position});
|
|
ecs.setClean();
|
|
ecs.get(entity).Position.x = 128;
|
|
ecs.get(entity).Position.x = 256;
|
|
ecs.get(entity).Position.x = 512;
|
|
expect(ecs.diff)
|
|
.to.deep.equal({[entity]: {Position: {x: 512}}});
|
|
});
|
|
|
|
test('generates diffs for deletions', async () => {
|
|
const ecs = new Ecs();
|
|
let entity;
|
|
entity = await ecs.create();
|
|
ecs.setClean();
|
|
ecs.destroy(entity);
|
|
ecs.tick(0);
|
|
expect(ecs.diff)
|
|
.to.deep.equal({[entity]: false});
|
|
});
|
|
|
|
test('applies creation patches', async () => {
|
|
const ecs = new Ecs({Components: {Position}});
|
|
await 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 () => {
|
|
const ecs = new Ecs({Components: {Position}});
|
|
await ecs.createSpecific(16, {Position: {x: 64}});
|
|
await 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', () => {
|
|
const ecs = new Ecs({Components: {Position}});
|
|
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 () => {
|
|
const ecs = new Ecs({Components: {Empty, Position}});
|
|
await ecs.createSpecific(16, {Empty: {}, Position: {x: 64}});
|
|
expect(ecs.get(16).constructor.componentNames)
|
|
.to.deep.equal(['Empty', 'Position']);
|
|
await ecs.apply({16: {Empty: false}});
|
|
expect(ecs.get(16).constructor.componentNames)
|
|
.to.deep.equal(['Position']);
|
|
});
|
|
|
|
test('calculates entity size', async () => {
|
|
const ecs = new Ecs({Components: {Empty, Position}});
|
|
await 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 () => {
|
|
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}});
|
|
expect(ecs.toJSON())
|
|
.to.deep.equal({
|
|
entities: {
|
|
1: {Empty: {}, Position: {x: 64}},
|
|
16: {Name: {name: 'foobar'}, Position: {x: 128}},
|
|
},
|
|
systems: [],
|
|
});
|
|
const view = Ecs.serialize(ecs);
|
|
const deserialized = await Ecs.deserialize(
|
|
new Ecs({Components: {Empty, Name, Position}}),
|
|
view,
|
|
);
|
|
expect(Array.from(deserialized.entities).length)
|
|
.to.equal(2);
|
|
expect(deserialized.get(1).constructor.componentNames)
|
|
.to.deep.equal(['Empty', 'Position']);
|
|
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}}))
|
|
expect(JSON.stringify(deserialized.get(1).Position))
|
|
.to.equal(JSON.stringify({x: 64}));
|
|
expect(JSON.stringify(deserialized.get(16).Position))
|
|
.to.equal(JSON.stringify({x: 128}));
|
|
expect(deserialized.get(16).Name.name)
|
|
.to.equal('foobar');
|
|
});
|
|
|
|
test('deserializes from compatible ECS', async () => {
|
|
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}});
|
|
const view = Ecs.serialize(ecs);
|
|
const deserialized = await Ecs.deserialize(
|
|
new Ecs({Components: {Empty, Name}}),
|
|
view,
|
|
);
|
|
expect(deserialized.get(1).toJSON())
|
|
.to.deep.equal({Empty: {}});
|
|
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}});
|
|
});
|