archetype-ecs 1.4.2 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +79 -14
- package/bench/allocations-1m.js +1 -1
- package/bench/component-churn-bench.js +193 -0
- package/bench/iterate.wat +95 -0
- package/bench/multi-ecs-bench.js +1 -1
- package/bench/run-js-vs-go-ts.sh +147 -0
- package/bench/typed-vs-bitecs-1m.js +1 -1
- package/bench/typed-vs-untyped.js +1 -1
- package/bench/vs-bitecs.js +1 -1
- package/bench/wasm-iteration-bench.js +289 -0
- package/dist/{src/ComponentRegistry.d.ts → ComponentRegistry.d.ts} +8 -3
- package/dist/{src/EntityManager.d.ts → EntityManager.d.ts} +15 -10
- package/dist/{src/EntityManager.js → EntityManager.js} +196 -95
- package/dist/{src/System.d.ts → System.d.ts} +4 -0
- package/dist/{src/System.js → System.js} +25 -5
- package/dist/WasmArena.d.ts +13 -0
- package/dist/WasmArena.js +48 -0
- package/dist/{src/index.d.ts → index.d.ts} +7 -9
- package/dist/{src/index.js → index.js} +2 -0
- package/dist/wasm-kernels.d.ts +10 -0
- package/dist/wasm-kernels.js +59 -0
- package/package.json +12 -7
- package/src/ComponentRegistry.ts +7 -3
- package/src/EntityManager.ts +209 -119
- package/src/System.ts +34 -9
- package/src/WasmArena.ts +83 -0
- package/src/index.ts +16 -11
- package/src/iterate.wat +135 -0
- package/src/wasm-kernels.ts +68 -0
- package/tests/EntityManager.test.ts +51 -86
- package/tests/System.test.ts +184 -0
- package/tests/types.ts +1 -1
- package/tsconfig.json +2 -2
- package/tsconfig.test.json +13 -0
- package/dist/tests/EntityManager.test.d.ts +0 -1
- package/dist/tests/EntityManager.test.js +0 -651
- package/dist/tests/System.test.d.ts +0 -1
- package/dist/tests/System.test.js +0 -630
- package/dist/tests/types.d.ts +0 -1
- package/dist/tests/types.js +0 -129
- /package/dist/{src/ComponentRegistry.js → ComponentRegistry.js} +0 -0
- /package/dist/{src/Profiler.d.ts → Profiler.d.ts} +0 -0
- /package/dist/{src/Profiler.js → Profiler.js} +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, it, beforeEach } from 'node:test';
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
3
|
import { createEntityManager, type EntityManager } from '../src/EntityManager.js';
|
|
4
|
-
import { component } from '../src/index.js';
|
|
4
|
+
import { component, type ComponentDef } from '../src/index.js';
|
|
5
5
|
|
|
6
6
|
describe('EntityManager', () => {
|
|
7
7
|
let em: EntityManager;
|
|
@@ -140,7 +140,7 @@ describe('EntityManager', () => {
|
|
|
140
140
|
[Velocity._sym, 'Velocity'],
|
|
141
141
|
[Health._sym, 'Health']
|
|
142
142
|
]);
|
|
143
|
-
const nameToSymbol: Record<string,
|
|
143
|
+
const nameToSymbol: Record<string, ComponentDef> = { Position, Velocity, Health };
|
|
144
144
|
|
|
145
145
|
it('round-trips entities and components', () => {
|
|
146
146
|
const a = em.createEntity();
|
|
@@ -195,27 +195,27 @@ describe('EntityManager', () => {
|
|
|
195
195
|
|
|
196
196
|
const result = em.serialize(metaSymbolToName, [], [], { serializers });
|
|
197
197
|
assert.deepEqual(result.components['Meta'][a], { x: 1, y: 2 });
|
|
198
|
-
assert.equal((result.components['Meta'][a] as
|
|
198
|
+
assert.equal((result.components['Meta'][a] as Record<string, unknown>).secret, undefined);
|
|
199
199
|
});
|
|
200
200
|
|
|
201
201
|
it('custom deserializers are used when provided', () => {
|
|
202
202
|
const Meta = component('Meta2', { x: 'f32', y: 'f32' });
|
|
203
203
|
const metaSymbolToName = new Map([...symbolToName, [Meta._sym, 'Meta2']]);
|
|
204
|
-
const metaNameToSymbol: Record<string,
|
|
204
|
+
const metaNameToSymbol: Record<string, ComponentDef> = { ...nameToSymbol, Meta2: Meta };
|
|
205
205
|
|
|
206
206
|
const a = em.createEntity();
|
|
207
207
|
em.addComponent(a, Meta, { x: 1, y: 2 });
|
|
208
208
|
|
|
209
209
|
const data = em.serialize(metaSymbolToName);
|
|
210
210
|
|
|
211
|
-
const deserializers = new Map<string, (data:
|
|
212
|
-
['Meta2', (compData
|
|
211
|
+
const deserializers = new Map<string, (data: unknown) => unknown>([
|
|
212
|
+
['Meta2', (compData) => ({ ...(compData as Record<string, unknown>), restored: true })]
|
|
213
213
|
]);
|
|
214
214
|
|
|
215
215
|
em.deserialize(data, metaNameToSymbol, { deserializers });
|
|
216
|
-
const result = em.getComponent(a, Meta)
|
|
217
|
-
assert.ok(Math.abs(result.x - 1) < 0.01);
|
|
218
|
-
assert.ok(Math.abs(result.y - 2) < 0.01);
|
|
216
|
+
const result = em.getComponent(a, Meta)!;
|
|
217
|
+
assert.ok(Math.abs(result.x as number - 1) < 0.01);
|
|
218
|
+
assert.ok(Math.abs(result.y as number - 2) < 0.01);
|
|
219
219
|
});
|
|
220
220
|
|
|
221
221
|
it('deserialize clears previous state', () => {
|
|
@@ -239,9 +239,9 @@ describe('Typed Components (SoA)', () => {
|
|
|
239
239
|
const Pos = component('Pos', 'f32', ['x', 'y']);
|
|
240
240
|
const id = em.createEntity();
|
|
241
241
|
em.addComponent(id, Pos, { x: 1.5, y: 2.5 });
|
|
242
|
-
const result = em.getComponent(id, Pos)
|
|
243
|
-
assert.ok(Math.abs(result.x - 1.5) < 0.001);
|
|
244
|
-
assert.ok(Math.abs(result.y - 2.5) < 0.001);
|
|
242
|
+
const result = em.getComponent(id, Pos)!;
|
|
243
|
+
assert.ok(Math.abs(result.x as number - 1.5) < 0.001);
|
|
244
|
+
assert.ok(Math.abs(result.y as number - 2.5) < 0.001);
|
|
245
245
|
});
|
|
246
246
|
|
|
247
247
|
it('growth past initial capacity (>64 entities)', () => {
|
|
@@ -253,9 +253,9 @@ describe('Typed Components (SoA)', () => {
|
|
|
253
253
|
ids.push(id);
|
|
254
254
|
}
|
|
255
255
|
for (let i = 0; i < 100; i++) {
|
|
256
|
-
const result = em.getComponent(ids[i], Pos)
|
|
257
|
-
assert.ok(Math.abs(result.x - i) < 0.001);
|
|
258
|
-
assert.ok(Math.abs(result.y - i * 2) < 0.001);
|
|
256
|
+
const result = em.getComponent(ids[i], Pos)!;
|
|
257
|
+
assert.ok(Math.abs(result.x as number - i) < 0.001);
|
|
258
|
+
assert.ok(Math.abs(result.y as number - i * 2) < 0.001);
|
|
259
259
|
}
|
|
260
260
|
});
|
|
261
261
|
|
|
@@ -270,10 +270,10 @@ describe('Typed Components (SoA)', () => {
|
|
|
270
270
|
|
|
271
271
|
em.destroyEntity(a);
|
|
272
272
|
|
|
273
|
-
const resultB = em.getComponent(b, Pos)
|
|
274
|
-
assert.ok(Math.abs(resultB.x - 3) < 0.001);
|
|
275
|
-
const resultC = em.getComponent(c, Pos)
|
|
276
|
-
assert.ok(Math.abs(resultC.x - 5) < 0.001);
|
|
273
|
+
const resultB = em.getComponent(b, Pos)!;
|
|
274
|
+
assert.ok(Math.abs(resultB.x as number - 3) < 0.001);
|
|
275
|
+
const resultC = em.getComponent(c, Pos)!;
|
|
276
|
+
assert.ok(Math.abs(resultC.x as number - 5) < 0.001);
|
|
277
277
|
});
|
|
278
278
|
|
|
279
279
|
it('typed + tag on same entity', () => {
|
|
@@ -283,8 +283,8 @@ describe('Typed Components (SoA)', () => {
|
|
|
283
283
|
em.addComponent(id, Pos, { x: 10, y: 20 });
|
|
284
284
|
em.addComponent(id, Tag, {});
|
|
285
285
|
|
|
286
|
-
const pos = em.getComponent(id, Pos)
|
|
287
|
-
assert.ok(Math.abs(pos.x - 10) < 0.001);
|
|
286
|
+
const pos = em.getComponent(id, Pos)!;
|
|
287
|
+
assert.ok(Math.abs(pos.x as number - 10) < 0.001);
|
|
288
288
|
assert.equal(em.getComponent(id, Tag), undefined);
|
|
289
289
|
assert.equal(em.hasComponent(id, Tag), true);
|
|
290
290
|
});
|
|
@@ -300,10 +300,10 @@ describe('Typed Components (SoA)', () => {
|
|
|
300
300
|
}
|
|
301
301
|
|
|
302
302
|
em.forEach([Pos, Vel], (arch) => {
|
|
303
|
-
const px = arch.field(Pos.x);
|
|
304
|
-
const py = arch.field(Pos.y);
|
|
305
|
-
const vx = arch.field(Vel.vx);
|
|
306
|
-
const vy = arch.field(Vel.vy);
|
|
303
|
+
const px = arch.field(Pos.x) as Float32Array;
|
|
304
|
+
const py = arch.field(Pos.y) as Float32Array;
|
|
305
|
+
const vx = arch.field(Vel.vx) as Float32Array;
|
|
306
|
+
const vy = arch.field(Vel.vy) as Float32Array;
|
|
307
307
|
for (let i = 0; i < arch.count; i++) {
|
|
308
308
|
px[i] += vx[i];
|
|
309
309
|
py[i] += vy[i];
|
|
@@ -312,16 +312,16 @@ describe('Typed Components (SoA)', () => {
|
|
|
312
312
|
|
|
313
313
|
const ids = em.query([Pos, Vel]);
|
|
314
314
|
for (const id of ids) {
|
|
315
|
-
const pos = em.getComponent(id, Pos)
|
|
316
|
-
assert.ok(pos.x >= 1);
|
|
317
|
-
assert.ok(Math.abs(pos.y - 2) < 0.001);
|
|
315
|
+
const pos = em.getComponent(id, Pos)!;
|
|
316
|
+
assert.ok((pos.x as number) >= 1);
|
|
317
|
+
assert.ok(Math.abs(pos.y as number - 2) < 0.001);
|
|
318
318
|
}
|
|
319
319
|
});
|
|
320
320
|
|
|
321
321
|
it('serialize/deserialize round-trip with typed components', () => {
|
|
322
322
|
const Pos = component('PosSer', 'f32', ['x', 'y']);
|
|
323
323
|
const symbolToName = new Map([[Pos._sym, 'PosSer']]);
|
|
324
|
-
const nameToSymbol: Record<string,
|
|
324
|
+
const nameToSymbol: Record<string, ComponentDef> = { PosSer: Pos };
|
|
325
325
|
|
|
326
326
|
const a = em.createEntity();
|
|
327
327
|
em.addComponent(a, Pos, { x: 1.5, y: 2.5 });
|
|
@@ -331,10 +331,10 @@ describe('Typed Components (SoA)', () => {
|
|
|
331
331
|
const data = em.serialize(symbolToName);
|
|
332
332
|
em.deserialize(data, nameToSymbol);
|
|
333
333
|
|
|
334
|
-
const posA = em.getComponent(a, Pos)
|
|
335
|
-
assert.ok(Math.abs(posA.x - 1.5) < 0.01);
|
|
336
|
-
const posB = em.getComponent(b, Pos)
|
|
337
|
-
assert.ok(Math.abs(posB.x - 3.5) < 0.01);
|
|
334
|
+
const posA = em.getComponent(a, Pos)!;
|
|
335
|
+
assert.ok(Math.abs(posA.x as number - 1.5) < 0.01);
|
|
336
|
+
const posB = em.getComponent(b, Pos)!;
|
|
337
|
+
assert.ok(Math.abs(posB.x as number - 3.5) < 0.01);
|
|
338
338
|
});
|
|
339
339
|
|
|
340
340
|
it('archetype migration with typed components', () => {
|
|
@@ -345,12 +345,12 @@ describe('Typed Components (SoA)', () => {
|
|
|
345
345
|
em.addComponent(id, Pos, { x: 5, y: 10 });
|
|
346
346
|
em.addComponent(id, Vel, { vx: 1, vy: 2 });
|
|
347
347
|
|
|
348
|
-
const pos = em.getComponent(id, Pos)
|
|
349
|
-
assert.ok(Math.abs(pos.x - 5) < 0.001);
|
|
348
|
+
const pos = em.getComponent(id, Pos)!;
|
|
349
|
+
assert.ok(Math.abs(pos.x as number - 5) < 0.001);
|
|
350
350
|
|
|
351
351
|
em.removeComponent(id, Vel);
|
|
352
|
-
const pos2 = em.getComponent(id, Pos)
|
|
353
|
-
assert.ok(Math.abs(pos2.x - 5) < 0.001);
|
|
352
|
+
const pos2 = em.getComponent(id, Pos)!;
|
|
353
|
+
assert.ok(Math.abs(pos2.x as number - 5) < 0.001);
|
|
354
354
|
assert.equal(em.hasComponent(id, Vel), false);
|
|
355
355
|
});
|
|
356
356
|
|
|
@@ -359,17 +359,17 @@ describe('Typed Components (SoA)', () => {
|
|
|
359
359
|
const id = em.createEntity();
|
|
360
360
|
em.addComponent(id, Pos, { x: 1, y: 2 });
|
|
361
361
|
em.addComponent(id, Pos, { x: 99, y: 88 });
|
|
362
|
-
const result = em.getComponent(id, Pos)
|
|
363
|
-
assert.ok(Math.abs(result.x - 99) < 0.001);
|
|
362
|
+
const result = em.getComponent(id, Pos)!;
|
|
363
|
+
assert.ok(Math.abs(result.x as number - 99) < 0.001);
|
|
364
364
|
});
|
|
365
365
|
|
|
366
366
|
it('get/set for zero-allocation field access', () => {
|
|
367
367
|
const Pos = component('PosGS', 'f32', ['x', 'y']);
|
|
368
368
|
const id = em.createEntity();
|
|
369
369
|
em.addComponent(id, Pos, { x: 3.5, y: 7.5 });
|
|
370
|
-
assert.ok(Math.abs(em.get(id, Pos.x) - 3.5) < 0.001);
|
|
370
|
+
assert.ok(Math.abs(em.get(id, Pos.x)! as number - 3.5) < 0.001);
|
|
371
371
|
em.set(id, Pos.x, 42);
|
|
372
|
-
assert.ok(Math.abs(em.get(id, Pos.x) - 42) < 0.001);
|
|
372
|
+
assert.ok(Math.abs(em.get(id, Pos.x)! as number - 42) < 0.001);
|
|
373
373
|
});
|
|
374
374
|
|
|
375
375
|
it('get returns undefined for missing entity/component', () => {
|
|
@@ -447,7 +447,7 @@ describe('Typed Components (SoA)', () => {
|
|
|
447
447
|
em.addComponent(id, Item, { name: 'Sword', weight: 3.5 });
|
|
448
448
|
|
|
449
449
|
assert.equal(em.get(id, Item.name), 'Sword');
|
|
450
|
-
assert.ok(Math.abs(em.get(id, Item.weight) - 3.5) < 0.01);
|
|
450
|
+
assert.ok(Math.abs(em.get(id, Item.weight)! as number - 3.5) < 0.01);
|
|
451
451
|
});
|
|
452
452
|
|
|
453
453
|
it('string component forEach field access', () => {
|
|
@@ -470,7 +470,6 @@ describe('Deferred Structural Changes during forEach', () => {
|
|
|
470
470
|
let em: EntityManager;
|
|
471
471
|
const Pos = component('DPos', 'f32', ['x', 'y']);
|
|
472
472
|
const Vel = component('DVel', 'f32', ['vx', 'vy']);
|
|
473
|
-
const Tag = component('DTag');
|
|
474
473
|
|
|
475
474
|
beforeEach(() => {
|
|
476
475
|
em = createEntityManager();
|
|
@@ -489,23 +488,13 @@ describe('Deferred Structural Changes during forEach', () => {
|
|
|
489
488
|
const ids = arch.entityIds;
|
|
490
489
|
for (let i = 0; i < arch.count; i++) {
|
|
491
490
|
visited.push(ids[i]);
|
|
492
|
-
|
|
493
|
-
if (ids[i] === a) {
|
|
494
|
-
em.removeComponent(a, Pos);
|
|
495
|
-
}
|
|
491
|
+
if (ids[i] === a) em.removeComponent(a, Pos);
|
|
496
492
|
}
|
|
497
493
|
});
|
|
498
494
|
|
|
499
|
-
// All 3 should have been visited (removal was deferred)
|
|
500
495
|
assert.equal(visited.length, 3);
|
|
501
|
-
assert.ok(visited.includes(a));
|
|
502
|
-
assert.ok(visited.includes(b));
|
|
503
|
-
assert.ok(visited.includes(c));
|
|
504
|
-
|
|
505
|
-
// After forEach, the removal should have been applied
|
|
506
496
|
assert.equal(em.hasComponent(a, Pos), false);
|
|
507
497
|
assert.equal(em.hasComponent(b, Pos), true);
|
|
508
|
-
assert.equal(em.hasComponent(c, Pos), true);
|
|
509
498
|
});
|
|
510
499
|
|
|
511
500
|
it('addComponent (migration) during forEach is deferred', () => {
|
|
@@ -517,21 +506,13 @@ describe('Deferred Structural Changes during forEach', () => {
|
|
|
517
506
|
em.forEach([Pos], (arch) => {
|
|
518
507
|
const ids = arch.entityIds;
|
|
519
508
|
for (let i = 0; i < arch.count; i++) {
|
|
520
|
-
|
|
521
|
-
if (ids[i] === a) {
|
|
522
|
-
em.addComponent(a, Vel, { vx: 10, vy: 20 });
|
|
523
|
-
}
|
|
509
|
+
if (ids[i] === a) em.addComponent(a, Vel, { vx: 10, vy: 20 });
|
|
524
510
|
}
|
|
525
511
|
});
|
|
526
512
|
|
|
527
|
-
// After forEach, migration should have been applied
|
|
528
513
|
assert.equal(em.hasComponent(a, Vel), true);
|
|
529
|
-
const vel = em.getComponent(a, Vel)
|
|
530
|
-
assert.ok(Math.abs(vel.vx - 10) < 0.001);
|
|
531
|
-
assert.ok(Math.abs(vel.vy - 20) < 0.001);
|
|
532
|
-
// Original data preserved after migration
|
|
533
|
-
const pos = em.getComponent(a, Pos);
|
|
534
|
-
assert.ok(Math.abs(pos.x - 1) < 0.001);
|
|
514
|
+
const vel = em.getComponent(a, Vel)!;
|
|
515
|
+
assert.ok(Math.abs((vel.vx as number) - 10) < 0.001);
|
|
535
516
|
});
|
|
536
517
|
|
|
537
518
|
it('addComponent overwrite during forEach is immediate (no migration)', () => {
|
|
@@ -539,10 +520,8 @@ describe('Deferred Structural Changes during forEach', () => {
|
|
|
539
520
|
em.addComponent(a, Pos, { x: 1, y: 2 });
|
|
540
521
|
|
|
541
522
|
em.forEach([Pos], (arch) => {
|
|
542
|
-
const px = arch.field(Pos.x);
|
|
543
|
-
// Overwrite via addComponent — same archetype, should be immediate
|
|
523
|
+
const px = arch.field(Pos.x) as Float32Array;
|
|
544
524
|
em.addComponent(a, Pos, { x: 99, y: 88 });
|
|
545
|
-
// The array should reflect the change immediately
|
|
546
525
|
assert.ok(Math.abs(px[0] - 99) < 0.001);
|
|
547
526
|
});
|
|
548
527
|
});
|
|
@@ -560,16 +539,13 @@ describe('Deferred Structural Changes during forEach', () => {
|
|
|
560
539
|
const ids = arch.entityIds;
|
|
561
540
|
for (let i = 0; i < arch.count; i++) {
|
|
562
541
|
visited.push(ids[i]);
|
|
563
|
-
if (ids[i] === b)
|
|
564
|
-
em.destroyEntity(b);
|
|
565
|
-
}
|
|
542
|
+
if (ids[i] === b) em.destroyEntity(b);
|
|
566
543
|
}
|
|
567
544
|
});
|
|
568
545
|
|
|
569
546
|
assert.equal(visited.length, 3);
|
|
570
|
-
// After forEach, entity b should be destroyed
|
|
571
547
|
assert.equal(em.hasComponent(b, Pos), false);
|
|
572
|
-
assert.
|
|
548
|
+
assert.equal(em.getAllEntities().includes(b), false);
|
|
573
549
|
});
|
|
574
550
|
|
|
575
551
|
it('multiple deferred operations are applied in order', () => {
|
|
@@ -577,15 +553,12 @@ describe('Deferred Structural Changes during forEach', () => {
|
|
|
577
553
|
em.addComponent(a, Pos, { x: 1, y: 0 });
|
|
578
554
|
|
|
579
555
|
em.forEach([Pos], () => {
|
|
580
|
-
// Remove Pos then add Vel — both deferred, applied in order
|
|
581
556
|
em.removeComponent(a, Pos);
|
|
582
557
|
em.addComponent(a, Vel, { vx: 5, vy: 6 });
|
|
583
558
|
});
|
|
584
559
|
|
|
585
560
|
assert.equal(em.hasComponent(a, Pos), false);
|
|
586
561
|
assert.equal(em.hasComponent(a, Vel), true);
|
|
587
|
-
const vel = em.getComponent(a, Vel);
|
|
588
|
-
assert.ok(Math.abs(vel.vx - 5) < 0.001);
|
|
589
562
|
});
|
|
590
563
|
|
|
591
564
|
it('nested forEach properly defers until outermost forEach completes', () => {
|
|
@@ -594,19 +567,13 @@ describe('Deferred Structural Changes during forEach', () => {
|
|
|
594
567
|
em.addComponent(a, Pos, { x: 1, y: 0 });
|
|
595
568
|
em.addComponent(b, Vel, { vx: 2, vy: 0 });
|
|
596
569
|
|
|
597
|
-
let innerComplete = false;
|
|
598
570
|
em.forEach([Pos], () => {
|
|
599
571
|
em.forEach([Vel], () => {
|
|
600
572
|
em.removeComponent(b, Vel);
|
|
601
|
-
innerComplete = true;
|
|
602
573
|
});
|
|
603
|
-
|
|
604
|
-
// (outermost forEach is still running)
|
|
605
|
-
assert.equal(innerComplete, true);
|
|
606
|
-
assert.equal(em.hasComponent(b, Vel), true); // live state — still deferred
|
|
574
|
+
assert.equal(em.hasComponent(b, Vel), true); // still deferred
|
|
607
575
|
});
|
|
608
576
|
|
|
609
|
-
// Now outermost forEach is done — deferred ops should be flushed
|
|
610
577
|
assert.equal(em.hasComponent(b, Vel), false);
|
|
611
578
|
});
|
|
612
579
|
|
|
@@ -616,8 +583,7 @@ describe('Deferred Structural Changes during forEach', () => {
|
|
|
616
583
|
|
|
617
584
|
em.forEach([Pos], (arch) => {
|
|
618
585
|
em.set(a, Pos.x, 42);
|
|
619
|
-
|
|
620
|
-
const px = arch.field(Pos.x);
|
|
586
|
+
const px = arch.field(Pos.x) as Float32Array;
|
|
621
587
|
assert.ok(Math.abs(px[0] - 42) < 0.001);
|
|
622
588
|
});
|
|
623
589
|
});
|
|
@@ -637,7 +603,6 @@ describe('Deferred Structural Changes during forEach', () => {
|
|
|
637
603
|
em.addComponent(a, Vel, { vx: 1, vy: 2 });
|
|
638
604
|
});
|
|
639
605
|
|
|
640
|
-
// Deferred ops were applied, hooks should be pending
|
|
641
606
|
em.flushHooks();
|
|
642
607
|
assert.deepEqual(removed, [a]);
|
|
643
608
|
assert.deepEqual(added, [a]);
|
package/tests/System.test.ts
CHANGED
|
@@ -177,6 +177,46 @@ describe('createSystem (functional)', () => {
|
|
|
177
177
|
sys.dispose();
|
|
178
178
|
});
|
|
179
179
|
|
|
180
|
+
it('onRemoved handler can read removed component data via get()', () => {
|
|
181
|
+
const hpValues: number[] = [];
|
|
182
|
+
const sys = createSystem(em, (s) => {
|
|
183
|
+
s.onRemoved(Health, (id: EntityId) => {
|
|
184
|
+
hpValues.push(em.get(id, Health.hp) as number);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const e1 = em.createEntityWith(Health, { hp: 77 });
|
|
189
|
+
em.flushHooks();
|
|
190
|
+
sys();
|
|
191
|
+
|
|
192
|
+
em.removeComponent(e1, Health);
|
|
193
|
+
em.flushHooks();
|
|
194
|
+
sys();
|
|
195
|
+
|
|
196
|
+
assert.ok(Math.abs(hpValues[0] - 77) < 0.001);
|
|
197
|
+
sys.dispose();
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('onRemoved handler can read data of destroyed entity', () => {
|
|
201
|
+
const hpValues: number[] = [];
|
|
202
|
+
const sys = createSystem(em, (s) => {
|
|
203
|
+
s.onRemoved(Health, (id: EntityId) => {
|
|
204
|
+
hpValues.push(em.get(id, Health.hp) as number);
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
const e1 = em.createEntityWith(Health, { hp: 55 });
|
|
209
|
+
em.flushHooks();
|
|
210
|
+
sys();
|
|
211
|
+
|
|
212
|
+
em.destroyEntity(e1);
|
|
213
|
+
em.flushHooks();
|
|
214
|
+
sys();
|
|
215
|
+
|
|
216
|
+
assert.ok(Math.abs(hpValues[0] - 55) < 0.001);
|
|
217
|
+
sys.dispose();
|
|
218
|
+
});
|
|
219
|
+
|
|
180
220
|
it('query-only system (no hooks) works', () => {
|
|
181
221
|
const e1 = em.createEntity();
|
|
182
222
|
em.addComponent(e1, Position, { x: 10, y: 20 });
|
|
@@ -420,6 +460,97 @@ describe('System (class + decorators)', () => {
|
|
|
420
460
|
sys.dispose();
|
|
421
461
|
});
|
|
422
462
|
|
|
463
|
+
it('@OnRemoved handler can read removed component data via get()', () => {
|
|
464
|
+
const hpValues: number[] = [];
|
|
465
|
+
|
|
466
|
+
class TestSys extends System {
|
|
467
|
+
@OnRemoved(Health)
|
|
468
|
+
handleRemove(id: EntityId) {
|
|
469
|
+
hpValues.push(this.em.get(id, Health.hp) as number);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const sys = new TestSys(em);
|
|
474
|
+
const e1 = em.createEntityWith(Health, { hp: 42 });
|
|
475
|
+
em.flushHooks();
|
|
476
|
+
sys.run();
|
|
477
|
+
|
|
478
|
+
em.removeComponent(e1, Health);
|
|
479
|
+
em.flushHooks();
|
|
480
|
+
sys.run();
|
|
481
|
+
|
|
482
|
+
assert.ok(Math.abs(hpValues[0] - 42) < 0.001);
|
|
483
|
+
sys.dispose();
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
it('@OnRemoved handler can read data of destroyed entity via get()', () => {
|
|
487
|
+
const hpValues: number[] = [];
|
|
488
|
+
|
|
489
|
+
class TestSys extends System {
|
|
490
|
+
@OnRemoved(Health)
|
|
491
|
+
handleRemove(id: EntityId) {
|
|
492
|
+
hpValues.push(this.em.get(id, Health.hp) as number);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const sys = new TestSys(em);
|
|
497
|
+
const e1 = em.createEntityWith(Health, { hp: 99 });
|
|
498
|
+
em.flushHooks();
|
|
499
|
+
sys.run();
|
|
500
|
+
|
|
501
|
+
em.destroyEntity(e1);
|
|
502
|
+
em.flushHooks();
|
|
503
|
+
sys.run();
|
|
504
|
+
|
|
505
|
+
assert.ok(Math.abs(hpValues[0] - 99) < 0.001);
|
|
506
|
+
sys.dispose();
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
it('@OnRemoved handler can read data via getComponent()', () => {
|
|
510
|
+
const results: Record<string, number | string | number[]>[] = [];
|
|
511
|
+
|
|
512
|
+
class TestSys extends System {
|
|
513
|
+
@OnRemoved(Position)
|
|
514
|
+
handleRemove(id: EntityId) {
|
|
515
|
+
const comp = this.em.getComponent(id, Position);
|
|
516
|
+
if (comp) results.push(comp);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const sys = new TestSys(em);
|
|
521
|
+
const e1 = em.createEntityWith(Position, { x: 10, y: 20 });
|
|
522
|
+
em.flushHooks();
|
|
523
|
+
sys.run();
|
|
524
|
+
|
|
525
|
+
em.removeComponent(e1, Position);
|
|
526
|
+
em.flushHooks();
|
|
527
|
+
sys.run();
|
|
528
|
+
|
|
529
|
+
assert.ok(Math.abs(results[0].x as number - 10) < 0.001);
|
|
530
|
+
assert.ok(Math.abs(results[0].y as number - 20) < 0.001);
|
|
531
|
+
sys.dispose();
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
it('removed data is cleared after commitRemovals (via run())', () => {
|
|
535
|
+
class TestSys extends System {
|
|
536
|
+
@OnRemoved(Health)
|
|
537
|
+
handleRemove(_id: EntityId) {}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const sys = new TestSys(em);
|
|
541
|
+
const e1 = em.createEntityWith(Health, { hp: 42 });
|
|
542
|
+
em.flushHooks();
|
|
543
|
+
sys.run();
|
|
544
|
+
|
|
545
|
+
em.removeComponent(e1, Health);
|
|
546
|
+
em.flushHooks();
|
|
547
|
+
sys.run(); // calls commitRemovals internally
|
|
548
|
+
|
|
549
|
+
// After run(), removed data should be cleared
|
|
550
|
+
assert.equal(em.get(e1, Health.hp), undefined);
|
|
551
|
+
sys.dispose();
|
|
552
|
+
});
|
|
553
|
+
|
|
423
554
|
it('multiple instances have independent buffers', () => {
|
|
424
555
|
const collectedA: EntityId[] = [];
|
|
425
556
|
const collectedB: EntityId[] = [];
|
|
@@ -514,6 +645,59 @@ describe('createSystems', () => {
|
|
|
514
645
|
assert.deepEqual(collected, []);
|
|
515
646
|
});
|
|
516
647
|
|
|
648
|
+
it('@OnRemoved in pipeline can access removed data', () => {
|
|
649
|
+
const hpValues: number[] = [];
|
|
650
|
+
|
|
651
|
+
class DeathSys extends System {
|
|
652
|
+
@OnRemoved(Health)
|
|
653
|
+
handleRemove(id: EntityId) {
|
|
654
|
+
hpValues.push(this.em.get(id, Health.hp) as number);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
const pipeline = createSystems(em, [DeathSys]);
|
|
659
|
+
const e1 = em.createEntityWith(Health, { hp: 77 });
|
|
660
|
+
em.flushHooks();
|
|
661
|
+
pipeline();
|
|
662
|
+
|
|
663
|
+
em.removeComponent(e1, Health);
|
|
664
|
+
em.flushHooks();
|
|
665
|
+
pipeline();
|
|
666
|
+
|
|
667
|
+
assert.ok(Math.abs(hpValues[0] - 77) < 0.001);
|
|
668
|
+
// Data should be cleared after pipeline (commitRemovals)
|
|
669
|
+
assert.equal(em.get(e1, Health.hp), undefined);
|
|
670
|
+
pipeline.dispose();
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
it('multiple systems in pipeline all see removed data', () => {
|
|
674
|
+
const hpA: number[] = [];
|
|
675
|
+
const hpB: number[] = [];
|
|
676
|
+
|
|
677
|
+
class SysA extends System {
|
|
678
|
+
@OnRemoved(Health)
|
|
679
|
+
handle(id: EntityId) { hpA.push(this.em.get(id, Health.hp) as number); }
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
class SysB extends System {
|
|
683
|
+
@OnRemoved(Health)
|
|
684
|
+
handle(id: EntityId) { hpB.push(this.em.get(id, Health.hp) as number); }
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
const pipeline = createSystems(em, [SysA, SysB]);
|
|
688
|
+
const e1 = em.createEntityWith(Health, { hp: 33 });
|
|
689
|
+
em.flushHooks();
|
|
690
|
+
pipeline();
|
|
691
|
+
|
|
692
|
+
em.destroyEntity(e1);
|
|
693
|
+
em.flushHooks();
|
|
694
|
+
pipeline();
|
|
695
|
+
|
|
696
|
+
assert.ok(Math.abs(hpA[0] - 33) < 0.001);
|
|
697
|
+
assert.ok(Math.abs(hpB[0] - 33) < 0.001);
|
|
698
|
+
pipeline.dispose();
|
|
699
|
+
});
|
|
700
|
+
|
|
517
701
|
it('class hooks fire through pipeline', () => {
|
|
518
702
|
const added: EntityId[] = [];
|
|
519
703
|
let moved = 0;
|
package/tests/types.ts
CHANGED
|
@@ -33,7 +33,7 @@ em.addComponent(id, Name, { name: 'Hero', title: 'Sir' });
|
|
|
33
33
|
const pos = em.getComponent(id, Position);
|
|
34
34
|
|
|
35
35
|
// get/set: field descriptor access
|
|
36
|
-
const px
|
|
36
|
+
const px = em.get(id, Position.x);
|
|
37
37
|
em.set(id, Position.x, 10);
|
|
38
38
|
|
|
39
39
|
// forEach + field: accepts FieldRef
|
package/tsconfig.json
CHANGED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"strict": true,
|
|
4
|
+
"target": "ES2022",
|
|
5
|
+
"module": "NodeNext",
|
|
6
|
+
"moduleResolution": "NodeNext",
|
|
7
|
+
"declaration": false,
|
|
8
|
+
"outDir": "dist-test",
|
|
9
|
+
"rootDir": ".",
|
|
10
|
+
"skipLibCheck": true
|
|
11
|
+
},
|
|
12
|
+
"include": ["src/**/*.ts", "tests/**/*.ts"]
|
|
13
|
+
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|