import {expect, test} from 'vitest'; import Ecs from './ecs.js'; import System from './system.js'; const Empty = {}; const Name = { name: 'string', }; const Position = { x: {type: 'int32', defaultValue: 32}, y: 'int32', z: 'int32', }; 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 = { x: 'int32', y: 'int32', z: 'int32', }; 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 { static Types = {Foo: {bar: 'uint8'}}; } 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. 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 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; });