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.
Files changed (43) hide show
  1. package/README.md +79 -14
  2. package/bench/allocations-1m.js +1 -1
  3. package/bench/component-churn-bench.js +193 -0
  4. package/bench/iterate.wat +95 -0
  5. package/bench/multi-ecs-bench.js +1 -1
  6. package/bench/run-js-vs-go-ts.sh +147 -0
  7. package/bench/typed-vs-bitecs-1m.js +1 -1
  8. package/bench/typed-vs-untyped.js +1 -1
  9. package/bench/vs-bitecs.js +1 -1
  10. package/bench/wasm-iteration-bench.js +289 -0
  11. package/dist/{src/ComponentRegistry.d.ts → ComponentRegistry.d.ts} +8 -3
  12. package/dist/{src/EntityManager.d.ts → EntityManager.d.ts} +15 -10
  13. package/dist/{src/EntityManager.js → EntityManager.js} +196 -95
  14. package/dist/{src/System.d.ts → System.d.ts} +4 -0
  15. package/dist/{src/System.js → System.js} +25 -5
  16. package/dist/WasmArena.d.ts +13 -0
  17. package/dist/WasmArena.js +48 -0
  18. package/dist/{src/index.d.ts → index.d.ts} +7 -9
  19. package/dist/{src/index.js → index.js} +2 -0
  20. package/dist/wasm-kernels.d.ts +10 -0
  21. package/dist/wasm-kernels.js +59 -0
  22. package/package.json +12 -7
  23. package/src/ComponentRegistry.ts +7 -3
  24. package/src/EntityManager.ts +209 -119
  25. package/src/System.ts +34 -9
  26. package/src/WasmArena.ts +83 -0
  27. package/src/index.ts +16 -11
  28. package/src/iterate.wat +135 -0
  29. package/src/wasm-kernels.ts +68 -0
  30. package/tests/EntityManager.test.ts +51 -86
  31. package/tests/System.test.ts +184 -0
  32. package/tests/types.ts +1 -1
  33. package/tsconfig.json +2 -2
  34. package/tsconfig.test.json +13 -0
  35. package/dist/tests/EntityManager.test.d.ts +0 -1
  36. package/dist/tests/EntityManager.test.js +0 -651
  37. package/dist/tests/System.test.d.ts +0 -1
  38. package/dist/tests/System.test.js +0 -630
  39. package/dist/tests/types.d.ts +0 -1
  40. package/dist/tests/types.js +0 -129
  41. /package/dist/{src/ComponentRegistry.js → ComponentRegistry.js} +0 -0
  42. /package/dist/{src/Profiler.d.ts → Profiler.d.ts} +0 -0
  43. /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, any> = { Position, Velocity, Health };
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 any).secret, undefined);
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, any> = { ...nameToSymbol, Meta2: Meta };
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: any) => any>([
212
- ['Meta2', (compData: any) => ({ ...compData, restored: true })]
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, any> = { PosSer: Pos };
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
- // Remove first entity during iteration — should be deferred
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
- // Add a new component to entity a — causes migration, should be deferred
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.deepEqual(em.getAllEntities().includes(b), false);
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
- // After inner forEach, structural change is still deferred
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
- // Should be immediately visible
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]);
@@ -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: any = em.get(id, Position.x);
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
@@ -6,8 +6,8 @@
6
6
  "moduleResolution": "NodeNext",
7
7
  "declaration": true,
8
8
  "outDir": "dist",
9
- "rootDir": ".",
9
+ "rootDir": "src",
10
10
  "skipLibCheck": true
11
11
  },
12
- "include": ["src/**/*.ts", "tests/**/*.ts"]
12
+ "include": ["src/**/*.ts"]
13
13
  }
@@ -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 {};