2024-06-10 22:42:30 -05:00
|
|
|
import {expect, test} from 'vitest';
|
|
|
|
|
|
|
|
import Ecs from './ecs.js';
|
|
|
|
import System from './system.js';
|
|
|
|
|
|
|
|
const Empty = {};
|
|
|
|
|
|
|
|
const Name = {
|
2024-06-12 01:38:05 -05:00
|
|
|
name: {type: 'string'},
|
2024-06-10 22:42:30 -05:00
|
|
|
};
|
|
|
|
|
|
|
|
const Position = {
|
|
|
|
x: {type: 'int32', defaultValue: 32},
|
2024-06-12 01:38:05 -05:00
|
|
|
y: {type: 'int32'},
|
|
|
|
z: {type: 'int32'},
|
2024-06-10 22:42:30 -05:00
|
|
|
};
|
|
|
|
|
|
|
|
test('adds and remove systems at runtime', () => {
|
|
|
|
const ecs = new Ecs();
|
|
|
|
let oneCount = 0;
|
|
|
|
let twoCount = 0;
|
|
|
|
const oneSystem = () => {
|
|
|
|
oneCount++;
|
|
|
|
};
|
|
|
|
ecs.addSystem(oneSystem);
|
|
|
|
ecs.tick();
|
|
|
|
expect(oneCount)
|
|
|
|
.to.equal(1);
|
|
|
|
const twoSystem = () => {
|
|
|
|
twoCount++;
|
|
|
|
};
|
|
|
|
ecs.addSystem(twoSystem);
|
|
|
|
ecs.tick();
|
|
|
|
expect(oneCount)
|
|
|
|
.to.equal(2);
|
|
|
|
expect(twoCount)
|
|
|
|
.to.equal(1);
|
|
|
|
ecs.removeSystem(oneSystem);
|
|
|
|
ecs.tick();
|
|
|
|
expect(oneCount)
|
|
|
|
.to.equal(2);
|
|
|
|
expect(twoCount)
|
|
|
|
.to.equal(2);
|
|
|
|
});
|
|
|
|
|
|
|
|
test('creates entities with components', () => {
|
|
|
|
class CreateEcs extends Ecs {
|
|
|
|
static Types = {Empty, Position};
|
|
|
|
}
|
|
|
|
const ecs = new CreateEcs();
|
|
|
|
const entity = 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", () => {
|
|
|
|
class RemoveEcs extends Ecs {
|
|
|
|
static Types = {Empty, Position};
|
|
|
|
}
|
|
|
|
const ecs = new RemoveEcs();
|
|
|
|
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', () => {
|
|
|
|
class GetEcs extends Ecs {
|
|
|
|
static Types = {Empty, Position};
|
|
|
|
}
|
|
|
|
const ecs = new GetEcs();
|
|
|
|
const entity = ecs.create({Empty: {}, Position: {y: 128}});
|
|
|
|
expect(JSON.stringify(ecs.get(entity)))
|
|
|
|
.to.deep.equal(JSON.stringify({Empty: {}, Position: {y: 128}}));
|
|
|
|
});
|
|
|
|
|
|
|
|
test('destroys entities', () => {
|
|
|
|
class DestroyEcs extends Ecs {
|
|
|
|
static Types = {Empty, Position};
|
|
|
|
}
|
|
|
|
const ecs = new DestroyEcs();
|
|
|
|
const entity = 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.destroyAll();
|
|
|
|
expect(ecs.get(entity))
|
|
|
|
.to.be.undefined;
|
|
|
|
expect(() => {
|
|
|
|
ecs.destroy(entity);
|
|
|
|
})
|
|
|
|
.to.throw();
|
|
|
|
});
|
|
|
|
|
|
|
|
test('inserts components into entities', () => {
|
|
|
|
class InsertEcs extends Ecs {
|
|
|
|
static Types = {Empty, Position};
|
|
|
|
}
|
|
|
|
const ecs = new InsertEcs();
|
|
|
|
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}}));
|
|
|
|
ecs.insert(entity, {Position: {y: 64}});
|
|
|
|
expect(JSON.stringify(ecs.get(entity)))
|
|
|
|
.to.deep.equal(JSON.stringify({Empty: {}, Position: {y: 64}}));
|
|
|
|
});
|
|
|
|
|
|
|
|
test('ticks systems', () => {
|
|
|
|
const Momentum = {
|
2024-06-12 01:38:05 -05:00
|
|
|
x: {type: 'int32'},
|
|
|
|
y: {type: 'int32'},
|
|
|
|
z: {type: 'int32'},
|
2024-06-10 22:42:30 -05:00
|
|
|
};
|
|
|
|
class TickEcs extends Ecs {
|
|
|
|
static Types = {Momentum, Position};
|
|
|
|
}
|
|
|
|
const ecs = new TickEcs();
|
|
|
|
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.addSystem(Physics);
|
|
|
|
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))
|
|
|
|
.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', () => {
|
|
|
|
const ecs = new Ecs();
|
|
|
|
class Spawn extends System {
|
|
|
|
tick() {
|
|
|
|
this.createManyEntities(Array.from({length: 5}).map(() => []));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
ecs.addSystem(Spawn);
|
|
|
|
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', () => {
|
|
|
|
const ecs = new Ecs();
|
|
|
|
class Spawn extends System {
|
|
|
|
tick() {
|
|
|
|
this.createEntity();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
ecs.addSystem(Spawn);
|
|
|
|
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', () => {
|
|
|
|
const ecs = new Ecs();
|
|
|
|
let entity;
|
|
|
|
class Despawn extends System {
|
|
|
|
|
|
|
|
finalize() {
|
|
|
|
entity = ecs.get(1);
|
|
|
|
}
|
|
|
|
|
|
|
|
tick() {
|
|
|
|
this.destroyEntity(1);
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
ecs.addSystem(Despawn);
|
|
|
|
ecs.create();
|
|
|
|
ecs.tick(1);
|
|
|
|
expect(entity)
|
|
|
|
.to.not.be.undefined;
|
|
|
|
expect(ecs.get(1))
|
|
|
|
.to.be.undefined;
|
|
|
|
});
|
|
|
|
|
|
|
|
test('adds components to and remove components from entities when ticking systems', () => {
|
|
|
|
class TickingEcs extends Ecs {
|
2024-06-12 01:38:05 -05:00
|
|
|
static Types = {Foo: {bar: {type: 'uint8'}}};
|
2024-06-10 22:42:30 -05:00
|
|
|
}
|
|
|
|
const ecs = new TickingEcs();
|
|
|
|
let addLength, removeLength;
|
|
|
|
class AddComponent extends System {
|
|
|
|
static queries() {
|
|
|
|
return {
|
|
|
|
default: ['Foo'],
|
|
|
|
};
|
|
|
|
}
|
|
|
|
tick() {
|
|
|
|
this.insertComponents(1, {Foo: {}});
|
|
|
|
}
|
|
|
|
finalize() {
|
|
|
|
addLength = Array.from(this.select('default')).length;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
class RemoveComponent extends System {
|
|
|
|
static queries() {
|
|
|
|
return {
|
|
|
|
default: ['Foo'],
|
|
|
|
};
|
|
|
|
}
|
|
|
|
tick() {
|
|
|
|
this.removeComponents(1, ['Foo']);
|
|
|
|
}
|
|
|
|
finalize() {
|
|
|
|
removeLength = Array.from(this.select('default')).length;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
ecs.addSystem(AddComponent);
|
|
|
|
ecs.create();
|
|
|
|
ecs.tick(1);
|
|
|
|
expect(addLength)
|
|
|
|
.to.equal(1);
|
|
|
|
expect(ecs.get(1).Foo)
|
|
|
|
.to.not.be.undefined;
|
|
|
|
ecs.removeSystem(AddComponent);
|
|
|
|
ecs.addSystem(RemoveComponent);
|
|
|
|
ecs.tick(1);
|
|
|
|
expect(removeLength)
|
|
|
|
.to.equal(0);
|
|
|
|
expect(ecs.get(1).Foo)
|
|
|
|
.to.be.undefined;
|
|
|
|
});
|
|
|
|
|
|
|
|
test('generates coalesced diffs for entity creation', () => {
|
|
|
|
const ecs = new Ecs();
|
|
|
|
let entity;
|
|
|
|
entity = ecs.create();
|
|
|
|
expect(ecs.diff)
|
|
|
|
.to.deep.equal({[entity]: {}});
|
|
|
|
});
|
|
|
|
|
|
|
|
test('generates diffs for adding and removing components', () => {
|
|
|
|
class DiffedEcs extends Ecs {
|
|
|
|
static Types = {Position};
|
|
|
|
}
|
|
|
|
const ecs = new DiffedEcs();
|
|
|
|
let entity;
|
|
|
|
entity = ecs.create();
|
|
|
|
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}});
|
|
|
|
});
|
|
|
|
|
|
|
|
test('generates diffs for empty components', () => {
|
|
|
|
class DiffedEcs extends Ecs {
|
|
|
|
static Types = {Empty};
|
|
|
|
}
|
|
|
|
const ecs = new DiffedEcs();
|
|
|
|
let entity;
|
|
|
|
entity = 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', () => {
|
|
|
|
class DiffedEcs extends Ecs {
|
|
|
|
static Types = {Position};
|
|
|
|
}
|
|
|
|
const ecs = new DiffedEcs();
|
|
|
|
let entity;
|
|
|
|
entity = 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 coalesced diffs for components', () => {
|
|
|
|
class DiffedEcs extends Ecs {
|
|
|
|
static Types = {Position};
|
|
|
|
}
|
|
|
|
const ecs = new DiffedEcs();
|
|
|
|
let entity;
|
|
|
|
entity = ecs.create({Position});
|
|
|
|
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: {}}});
|
|
|
|
});
|
|
|
|
|
|
|
|
test('generates coalesced diffs for mutations', () => {
|
|
|
|
class DiffedEcs extends Ecs {
|
|
|
|
static Types = {Position};
|
|
|
|
}
|
|
|
|
const ecs = new DiffedEcs();
|
|
|
|
let entity;
|
|
|
|
entity = 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', () => {
|
|
|
|
const ecs = new Ecs();
|
|
|
|
let entity;
|
|
|
|
entity = ecs.create();
|
|
|
|
ecs.setClean();
|
|
|
|
ecs.destroy(entity);
|
|
|
|
expect(ecs.diff)
|
|
|
|
.to.deep.equal({[entity]: false});
|
|
|
|
});
|
|
|
|
|
|
|
|
test('applies creation patches', () => {
|
|
|
|
class PatchedEcs extends Ecs {
|
|
|
|
static Types = {Position};
|
|
|
|
}
|
|
|
|
const ecs = new PatchedEcs();
|
|
|
|
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', () => {
|
|
|
|
class PatchedEcs extends Ecs {
|
|
|
|
static Types = {Position};
|
|
|
|
}
|
|
|
|
const ecs = new PatchedEcs();
|
|
|
|
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', () => {
|
|
|
|
class PatchedEcs extends Ecs {
|
|
|
|
static Types = {Position};
|
|
|
|
}
|
|
|
|
const ecs = new PatchedEcs();
|
|
|
|
ecs.createSpecific(16, {Position: {x: 64}});
|
|
|
|
ecs.apply({16: false});
|
|
|
|
expect(Array.from(ecs.entities).length)
|
|
|
|
.to.equal(0);
|
|
|
|
});
|
|
|
|
|
|
|
|
test('applies component deletion patches', () => {
|
|
|
|
class PatchedEcs extends Ecs {
|
|
|
|
static Types = {Empty, Position};
|
|
|
|
}
|
|
|
|
const ecs = new PatchedEcs();
|
|
|
|
ecs.createSpecific(16, {Empty: {}, Position: {x: 64}});
|
|
|
|
expect(ecs.get(16).constructor.types)
|
|
|
|
.to.deep.equal(['Empty', 'Position']);
|
|
|
|
ecs.apply({16: {Empty: false}});
|
|
|
|
expect(ecs.get(16).constructor.types)
|
|
|
|
.to.deep.equal(['Position']);
|
|
|
|
});
|
|
|
|
|
|
|
|
test('calculates entity size', () => {
|
|
|
|
class SizingEcs extends Ecs {
|
|
|
|
static Types = {Empty, Position};
|
|
|
|
}
|
|
|
|
const ecs = new SizingEcs();
|
|
|
|
ecs.createSpecific(1, {Empty: {}, Position: {}});
|
|
|
|
// ID + # of components + Empty + Position + x + y + z
|
|
|
|
// 4 + 2 + 2 + 2 + 4 + 4 + 4 = 22
|
|
|
|
expect(ecs.get(1).size())
|
|
|
|
.to.equal(22);
|
|
|
|
});
|
|
|
|
|
|
|
|
test('serializes and deserializes', () => {
|
|
|
|
class SerializingEcs extends Ecs {
|
|
|
|
static Types = {Empty, Name, Position};
|
|
|
|
}
|
|
|
|
const ecs = new SerializingEcs();
|
|
|
|
// ID + # of components + Empty + Position + x + y + z
|
|
|
|
// 4 + 2 + 2 + 2 + 4 + 4 + 4 = 22
|
|
|
|
ecs.createSpecific(1, {Empty: {}, Position: {x: 64}});
|
|
|
|
// ID + # of components + Name + 'foobar' + Position + x + y + z
|
|
|
|
// 4 + 2 + 2 + 4 + 6 + 2 + 4 + 4 + 4 = 32
|
|
|
|
ecs.createSpecific(16, {Name: {name: 'foobar'}, Position: {x: 128}});
|
|
|
|
const view = SerializingEcs.serialize(ecs);
|
|
|
|
// # of entities + Entity(1) + Entity(16)
|
|
|
|
// 4 + 22 + 32 = 58
|
|
|
|
expect(view.byteLength)
|
|
|
|
.to.equal(58);
|
|
|
|
// Entity values.
|
|
|
|
expect(view.getUint32(4 + 22 - 12, true))
|
|
|
|
.to.equal(64);
|
|
|
|
expect(view.getUint32(4 + 22 + 32 - 12, true))
|
|
|
|
.to.equal(128);
|
|
|
|
const deserialized = SerializingEcs.deserialize(view);
|
|
|
|
// # of entities.
|
|
|
|
expect(Array.from(deserialized.entities).length)
|
|
|
|
.to.equal(2);
|
|
|
|
// Composition of entities.
|
|
|
|
expect(deserialized.get(1).constructor.types)
|
|
|
|
.to.deep.equal(['Empty', 'Position']);
|
|
|
|
expect(deserialized.get(16).constructor.types)
|
|
|
|
.to.deep.equal(['Name', 'Position']);
|
|
|
|
// Entity values.
|
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');
|
|
|
|
});
|
|
|
|
|
|
|
|
test('deserializes empty', () => {
|
|
|
|
class SerializingEcs extends Ecs {
|
|
|
|
static Types = {Empty, Name, Position};
|
|
|
|
}
|
|
|
|
const ecs = SerializingEcs.deserialize(new DataView(new Uint32Array([0]).buffer));
|
|
|
|
expect(ecs)
|
|
|
|
.to.not.be.undefined;
|
|
|
|
});
|