silphius/app/ecs/ecs.test.js

475 lines
13 KiB
JavaScript
Raw Normal View History

2024-06-14 15:18:55 -05:00
import {expect, test} from 'vitest';
2024-06-10 22:42:30 -05:00
2024-06-26 10:33:31 -05:00
import Component from './component.js';
2024-06-10 22:42:30 -05:00
import Ecs from './ecs.js';
import System from './system.js';
2024-06-26 21:08:09 -05:00
function wrapProperties(name, properties) {
2024-06-26 10:33:31 -05:00
return class WrappedComponent extends Component {
2024-06-24 03:21:04 -05:00
static componentName = name;
2024-06-26 21:08:09 -05:00
static properties = properties;
2024-06-14 15:18:55 -05:00
};
}
2024-06-26 21:08:09 -05:00
const Empty = wrapProperties('Empty', {});
2024-06-10 22:42:30 -05:00
2024-06-26 21:08:09 -05:00
const Name = wrapProperties('Name', {
2024-06-12 01:38:05 -05:00
name: {type: 'string'},
2024-06-14 15:18:55 -05:00
});
2024-06-10 22:42:30 -05:00
2024-06-26 21:08:09 -05:00
const Position = wrapProperties('Position', {
2024-06-10 22:42:30 -05:00
x: {type: 'int32', defaultValue: 32},
2024-06-12 01:38:05 -05:00
y: {type: 'int32'},
z: {type: 'int32'},
2024-06-14 15:18:55 -05:00
});
2024-06-10 22:42:30 -05:00
2024-06-27 06:28:00 -05:00
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);
}
}
2024-06-14 15:18:55 -05:00
test('activates and deactivates systems at runtime', () => {
2024-06-10 22:42:30 -05:00
let oneCount = 0;
let twoCount = 0;
2024-06-15 18:23:10 -05:00
const ecs = new Ecs({
Systems: {
2024-06-14 15:18:55 -05:00
OneSystem: class extends System {
tick() {
oneCount += 1;
}
},
TwoSystem: class extends System {
tick() {
twoCount += 1;
}
},
2024-06-15 18:23:10 -05:00
},
});
2024-06-14 15:18:55 -05:00
ecs.tick();
expect(oneCount)
.to.equal(0);
expect(twoCount)
.to.equal(0);
ecs.system('OneSystem').active = true;
2024-06-10 22:42:30 -05:00
ecs.tick();
expect(oneCount)
.to.equal(1);
2024-06-14 15:18:55 -05:00
ecs.system('TwoSystem').active = true;
2024-06-10 22:42:30 -05:00
ecs.tick();
expect(oneCount)
.to.equal(2);
expect(twoCount)
.to.equal(1);
2024-06-14 15:18:55 -05:00
ecs.system('OneSystem').active = false;
2024-06-10 22:42:30 -05:00
ecs.tick();
expect(oneCount)
.to.equal(2);
expect(twoCount)
.to.equal(2);
});
2024-06-27 06:28:00 -05:00
test('creates entities with components', async () => {
2024-06-15 19:38:49 -05:00
const ecs = new Ecs({Components: {Empty, Position}});
2024-06-27 06:28:00 -05:00
const entity = await ecs.create({Empty: {}, Position: {y: 128}});
2024-06-10 22:42:30 -05:00
expect(JSON.stringify(ecs.get(entity)))
.to.deep.equal(JSON.stringify({Empty: {}, Position: {y: 128}}));
});
2024-06-27 06:28:00 -05:00
test("removes entities' components", async () => {
2024-06-15 19:38:49 -05:00
const ecs = new Ecs({Components: {Empty, Position}});
2024-06-27 06:28:00 -05:00
const entity = await ecs.create({Empty: {}, Position: {y: 128}});
2024-06-10 22:42:30 -05:00
ecs.remove(entity, ['Position']);
expect(JSON.stringify(ecs.get(entity)))
.to.deep.equal(JSON.stringify({Empty: {}}));
});
2024-06-27 06:28:00 -05:00
test('gets entities', async () => {
2024-06-15 19:38:49 -05:00
const ecs = new Ecs({Components: {Empty, Position}});
2024-06-27 06:28:00 -05:00
const entity = await ecs.create({Empty: {}, Position: {y: 128}});
2024-06-10 22:42:30 -05:00
expect(JSON.stringify(ecs.get(entity)))
.to.deep.equal(JSON.stringify({Empty: {}, Position: {y: 128}}));
});
2024-06-27 06:28:00 -05:00
test('destroys entities', async () => {
2024-06-15 19:38:49 -05:00
const ecs = new Ecs({Components: {Empty, Position}});
2024-06-27 06:28:00 -05:00
const entity = await ecs.create({Empty: {}, Position: {y: 128}});
2024-06-10 22:42:30 -05:00
expect(JSON.stringify(ecs.get(entity)))
.to.deep.equal(JSON.stringify({Empty: {}, Position: {y: 128}}));
expect(ecs.get(entity))
.to.not.be.undefined;
ecs.destroyAll();
expect(ecs.get(entity))
.to.be.undefined;
expect(() => {
ecs.destroy(entity);
})
.to.throw();
});
2024-06-27 06:28:00 -05:00
test('inserts components into entities', async () => {
2024-06-15 19:38:49 -05:00
const ecs = new Ecs({Components: {Empty, Position}});
2024-06-27 06:28:00 -05:00
const entity = await ecs.create({Empty: {}});
2024-06-10 22:42:30 -05:00
ecs.insert(entity, {Position: {y: 128}});
expect(JSON.stringify(ecs.get(entity)))
.to.deep.equal(JSON.stringify({Empty: {}, Position: {y: 128}}));
ecs.insert(entity, {Position: {y: 64}});
expect(JSON.stringify(ecs.get(entity)))
.to.deep.equal(JSON.stringify({Empty: {}, Position: {y: 64}}));
});
2024-06-27 06:28:00 -05:00
test('ticks systems', async () => {
2024-06-26 21:08:09 -05:00
const Momentum = wrapProperties('Momentum', {
2024-06-12 01:38:05 -05:00
x: {type: 'int32'},
y: {type: 'int32'},
z: {type: 'int32'},
2024-06-14 15:18:55 -05:00
});
2024-06-15 18:23:10 -05:00
const ecs = new Ecs({
2024-06-15 19:38:49 -05:00
Components: {Momentum, Position},
2024-06-15 18:23:10 -05:00
Systems: {
2024-06-14 15:18:55 -05:00
Physics: class Physics extends System {
static queries() {
return {
default: ['Position', 'Momentum'],
};
}
tick(elapsed) {
2024-06-26 07:41:07 -05:00
for (const {Position, Momentum} of this.select('default')) {
Position.x += Momentum.x * elapsed;
Position.y += Momentum.y * elapsed;
Position.z += Momentum.z * elapsed;
2024-06-14 15:18:55 -05:00
}
}
},
2024-06-15 18:23:10 -05:00
},
});
2024-06-14 15:18:55 -05:00
ecs.system('Physics').active = true;
2024-06-27 06:28:00 -05:00
const entity = await ecs.create({Momentum: {}, Position: {y: 128}});
2024-06-10 22:42:30 -05:00
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('creates many entities when ticking systems', () => {
2024-06-15 18:23:10 -05:00
const ecs = new Ecs({
Systems: {
2024-06-14 15:18:55 -05:00
Spawn: class extends System {
tick() {
this.createManyEntities(Array.from({length: 5}).map(() => []));
}
},
2024-06-15 18:23:10 -05:00
},
});
2024-06-14 15:18:55 -05:00
ecs.system('Spawn').active = true;
2024-06-10 22:42:30 -05:00
ecs.create();
expect(ecs.get(5))
.to.be.undefined;
ecs.tick(1);
expect(ecs.get(5))
.to.not.be.undefined;
});
test('creates entities when ticking systems', () => {
2024-06-15 18:23:10 -05:00
const ecs = new Ecs({
Systems: {
2024-06-14 15:18:55 -05:00
Spawn: class extends System {
tick() {
this.createEntity();
}
},
2024-06-15 18:23:10 -05:00
},
});
2024-06-14 15:18:55 -05:00
ecs.system('Spawn').active = true;
2024-06-10 22:42:30 -05:00
ecs.create();
expect(ecs.get(2))
.to.be.undefined;
ecs.tick(1);
expect(ecs.get(2))
.to.not.be.undefined;
});
test('schedules entities to be deleted when ticking systems', () => {
2024-06-15 18:23:10 -05:00
const ecs = new Ecs({
Systems: {
2024-06-14 15:18:55 -05:00
Despawn: class extends System {
tick() {
this.destroyEntity(1);
}
},
2024-06-15 18:23:10 -05:00
},
});
2024-06-14 15:18:55 -05:00
ecs.system('Despawn').active = true;
2024-06-10 22:42:30 -05:00
ecs.create();
ecs.tick(1);
expect(ecs.get(1))
.to.be.undefined;
});
2024-06-27 10:17:47 -05:00
test('adds components to and remove components from entities when ticking systems', async () => {
let promise;
2024-06-15 18:23:10 -05:00
const ecs = new Ecs({
2024-06-26 21:08:09 -05:00
Components: {Foo: wrapProperties('Foo', {bar: {type: 'uint8'}})},
2024-06-15 18:23:10 -05:00
Systems: {
2024-06-14 15:18:55 -05:00
AddComponent: class extends System {
static queries() {
return {
default: ['Foo'],
};
}
tick() {
2024-06-27 10:17:47 -05:00
promise = this.insertComponents(1, {Foo: {}});
2024-06-14 15:18:55 -05:00
}
},
RemoveComponent: class extends System {
static queries() {
return {
default: ['Foo'],
};
}
tick() {
this.removeComponents(1, ['Foo']);
}
},
2024-06-15 18:23:10 -05:00
},
});
2024-06-14 15:18:55 -05:00
ecs.system('AddComponent').active = true;
2024-06-10 22:42:30 -05:00
ecs.create();
ecs.tick(1);
2024-06-27 10:17:47 -05:00
await promise;
expect(Array.from(ecs.system('AddComponent').select('default')).length)
2024-06-10 22:42:30 -05:00
.to.equal(1);
expect(ecs.get(1).Foo)
.to.not.be.undefined;
2024-06-14 15:18:55 -05:00
ecs.system('AddComponent').active = false;
ecs.system('RemoveComponent').active = true;
2024-06-10 22:42:30 -05:00
ecs.tick(1);
2024-06-27 10:17:47 -05:00
expect(Array.from(ecs.system('RemoveComponent').select('default')).length)
2024-06-10 22:42:30 -05:00
.to.equal(0);
expect(ecs.get(1).Foo)
.to.be.undefined;
});
2024-06-27 10:17:47 -05:00
test('generates diffs for entity creation', async () => {
2024-06-10 22:42:30 -05:00
const ecs = new Ecs();
let entity;
2024-06-27 06:28:00 -05:00
entity = await ecs.create();
2024-06-10 22:42:30 -05:00
expect(ecs.diff)
.to.deep.equal({[entity]: {}});
});
2024-06-27 10:17:47 -05:00
test('generates diffs for adding and removing components', async () => {
2024-06-15 19:38:49 -05:00
const ecs = new Ecs({Components: {Position}});
2024-06-10 22:42:30 -05:00
let entity;
2024-06-27 10:17:47 -05:00
entity = await ecs.create();
2024-06-10 22:42:30 -05:00
ecs.setClean();
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}});
});
2024-06-27 06:28:00 -05:00
test('generates diffs for empty components', async () => {
2024-06-15 19:38:49 -05:00
const ecs = new Ecs({Components: {Empty}});
2024-06-10 22:42:30 -05:00
let entity;
2024-06-27 06:28:00 -05:00
entity = await ecs.create({Empty: {}});
2024-06-10 22:42:30 -05:00
expect(ecs.diff)
.to.deep.equal({[entity]: {Empty: {}}});
ecs.setClean();
ecs.remove(entity, ['Empty']);
expect(ecs.diff)
.to.deep.equal({[entity]: {Empty: false}});
});
2024-06-27 06:28:00 -05:00
test('generates diffs for entity mutations', async () => {
2024-06-15 19:38:49 -05:00
const ecs = new Ecs({Components: {Position}});
2024-06-10 22:42:30 -05:00
let entity;
2024-06-27 06:28:00 -05:00
entity = await ecs.create({Position: {}});
2024-06-10 22:42:30 -05:00
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({});
});
2024-06-27 06:28:00 -05:00
test('generates coalesced diffs for components', async () => {
2024-06-15 19:38:49 -05:00
const ecs = new Ecs({Components: {Position}});
2024-06-10 22:42:30 -05:00
let entity;
2024-06-27 06:28:00 -05:00
entity = await ecs.create({Position});
2024-06-10 22:42:30 -05:00
ecs.remove(entity, ['Position']);
expect(ecs.diff)
.to.deep.equal({[entity]: {Position: false}});
ecs.insert(entity, {Position: {}});
expect(ecs.diff)
.to.deep.equal({[entity]: {Position: {}}});
});
2024-06-27 06:28:00 -05:00
test('generates coalesced diffs for mutations', async () => {
2024-06-15 19:38:49 -05:00
const ecs = new Ecs({Components: {Position}});
2024-06-10 22:42:30 -05:00
let entity;
2024-06-27 06:28:00 -05:00
entity = await ecs.create({Position});
2024-06-10 22:42:30 -05:00
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}}});
});
2024-06-27 06:28:00 -05:00
test('generates diffs for deletions', async () => {
2024-06-10 22:42:30 -05:00
const ecs = new Ecs();
let entity;
2024-06-27 06:28:00 -05:00
entity = await ecs.create();
2024-06-10 22:42:30 -05:00
ecs.setClean();
ecs.destroy(entity);
expect(ecs.diff)
.to.deep.equal({[entity]: false});
});
2024-07-02 17:46:31 -05:00
test('applies creation patches', async () => {
2024-06-15 19:38:49 -05:00
const ecs = new Ecs({Components: {Position}});
2024-07-02 17:46:31 -05:00
await ecs.apply({16: {Position: {x: 64}}});
2024-06-10 22:42:30 -05:00
expect(Array.from(ecs.entities).length)
.to.equal(1);
expect(ecs.get(16).Position.x)
.to.equal(64);
});
test('applies update patches', () => {
2024-06-15 19:38:49 -05:00
const ecs = new Ecs({Components: {Position}});
2024-06-10 22:42:30 -05:00
ecs.createSpecific(16, {Position: {x: 64}});
ecs.apply({16: {Position: {x: 128}}});
expect(Array.from(ecs.entities).length)
.to.equal(1);
expect(ecs.get(16).Position.x)
.to.equal(128);
});
test('applies entity deletion patches', () => {
2024-06-15 19:38:49 -05:00
const ecs = new Ecs({Components: {Position}});
2024-06-10 22:42:30 -05:00
ecs.createSpecific(16, {Position: {x: 64}});
ecs.apply({16: false});
expect(Array.from(ecs.entities).length)
.to.equal(0);
});
2024-07-02 17:46:31 -05:00
test('applies component deletion patches', async () => {
2024-06-15 19:38:49 -05:00
const ecs = new Ecs({Components: {Empty, Position}});
2024-07-02 17:46:31 -05:00
await ecs.createSpecific(16, {Empty: {}, Position: {x: 64}});
2024-06-15 19:38:49 -05:00
expect(ecs.get(16).constructor.componentNames)
2024-06-10 22:42:30 -05:00
.to.deep.equal(['Empty', 'Position']);
2024-07-02 17:46:31 -05:00
await ecs.apply({16: {Empty: false}});
2024-06-15 19:38:49 -05:00
expect(ecs.get(16).constructor.componentNames)
2024-06-10 22:42:30 -05:00
.to.deep.equal(['Position']);
});
test('calculates entity size', () => {
2024-06-15 19:38:49 -05:00
const ecs = new Ecs({Components: {Empty, Position}});
2024-06-10 22:42:30 -05:00
ecs.createSpecific(1, {Empty: {}, Position: {}});
// ID + # of components + Empty + Position + x + y + z
2024-06-14 01:11:14 -05:00
// 4 + 2 + 2 + 4 + 2 + 4 + 4 + 4 + 4 = 30
2024-06-10 22:42:30 -05:00
expect(ecs.get(1).size())
2024-06-14 01:11:14 -05:00
.to.equal(30);
2024-06-10 22:42:30 -05:00
});
2024-06-27 10:17:47 -05:00
test('serializes and deserializes', async () => {
2024-06-15 19:38:49 -05:00
const ecs = new Ecs({Components: {Empty, Name, Position}});
2024-06-10 22:42:30 -05:00
ecs.createSpecific(1, {Empty: {}, Position: {x: 64}});
ecs.createSpecific(16, {Name: {name: 'foobar'}, Position: {x: 128}});
2024-06-14 15:18:55 -05:00
expect(ecs.toJSON())
.to.deep.equal({
entities: {
1: {Empty: {}, Position: {x: 64}},
16: {Name: {name: 'foobar'}, Position: {x: 128}},
},
systems: [],
});
2024-06-15 18:23:10 -05:00
const view = Ecs.serialize(ecs);
2024-06-27 10:17:47 -05:00
const deserialized = await Ecs.deserialize(
2024-06-15 19:38:49 -05:00
new Ecs({Components: {Empty, Name, Position}}),
2024-06-15 18:23:10 -05:00
view,
);
2024-06-10 22:42:30 -05:00
expect(Array.from(deserialized.entities).length)
.to.equal(2);
2024-06-15 19:38:49 -05:00
expect(deserialized.get(1).constructor.componentNames)
2024-06-10 22:42:30 -05:00
.to.deep.equal(['Empty', 'Position']);
2024-06-15 19:38:49 -05:00
expect(deserialized.get(16).constructor.componentNames)
2024-06-10 22:42:30 -05:00
.to.deep.equal(['Name', 'Position']);
2024-06-11 02:10:08 -05:00
expect(JSON.stringify(deserialized.get(1)))
.to.equal(JSON.stringify({Empty: {}, Position: {x: 64}}))
2024-06-10 22:42:30 -05:00
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');
});
2024-06-27 10:17:47 -05:00
test('deserializes from compatible ECS', async () => {
2024-06-15 19:38:49 -05:00
const ecs = new Ecs({Components: {Empty, Name, Position}});
2024-06-14 01:11:14 -05:00
ecs.createSpecific(1, {Empty: {}, Position: {x: 64}});
ecs.createSpecific(16, {Name: {name: 'foobar'}, Position: {x: 128}});
2024-06-15 18:23:10 -05:00
const view = Ecs.serialize(ecs);
2024-06-27 10:17:47 -05:00
const deserialized = await Ecs.deserialize(
2024-06-15 19:38:49 -05:00
new Ecs({Components: {Empty, Name}}),
2024-06-15 18:23:10 -05:00
view,
);
2024-06-14 01:11:14 -05:00
expect(deserialized.get(1).toJSON())
.to.deep.equal({Empty: {}});
expect(deserialized.get(16).toJSON())
.to.deep.equal({Name: {name: 'foobar'}});
});
2024-06-27 06:28:00 -05:00
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}});
});