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