@codehz/ecs 0.8.1 → 0.9.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 (50) hide show
  1. package/README.en.md +26 -3
  2. package/README.md +28 -3
  3. package/dist/builder.d.mts +296 -46
  4. package/dist/index.d.mts +2 -2
  5. package/dist/index.mjs +2 -2
  6. package/dist/testing.d.mts +1 -1
  7. package/dist/testing.mjs +1 -1
  8. package/dist/world.mjs +452 -179
  9. package/dist/world.mjs.map +1 -1
  10. package/examples/debug-observability.ts +92 -0
  11. package/examples/inventory-system-relations.ts +1 -1
  12. package/examples/parent-child-hierarchy.ts +18 -38
  13. package/package.json +1 -1
  14. package/skills/ecs/SKILL.md +9 -4
  15. package/src/__tests__/component/singleton.test.ts +40 -1
  16. package/src/__tests__/core/archetype.test.ts +155 -13
  17. package/src/__tests__/core/bitset.test.ts +12 -0
  18. package/src/__tests__/entity/entity.test.ts +33 -0
  19. package/src/__tests__/entity/id-system.test.ts +40 -0
  20. package/src/__tests__/perf/comprehensive.perf.test.ts +6 -9
  21. package/src/__tests__/perf/serialization.perf.test.ts +242 -0
  22. package/src/__tests__/perf/{dontfragment-wildcard.perf.test.ts → sparse-wildcard.perf.test.ts} +13 -16
  23. package/src/__tests__/query/caching.test.ts +62 -0
  24. package/src/__tests__/query/filter.test.ts +16 -22
  25. package/src/__tests__/query/perf.test.ts +3 -5
  26. package/src/__tests__/relations/hierarchy.test.ts +208 -0
  27. package/src/__tests__/relations/{dont-fragment → sparse}/basic.test.ts +64 -69
  28. package/src/__tests__/relations/{dont-fragment → sparse}/query-notification.test.ts +17 -9
  29. package/src/__tests__/serialization/bounds.test.ts +134 -1
  30. package/src/__tests__/world/commands.test.ts +337 -0
  31. package/src/__tests__/world/debug-stats.test.ts +206 -0
  32. package/src/__tests__/world/multi-component-hooks.test.ts +44 -0
  33. package/src/__tests__/world/serialize.test.ts +17 -0
  34. package/src/__tests__/world/wildcard-relation-hooks.test.ts +127 -0
  35. package/src/archetype/archetype.ts +96 -46
  36. package/src/archetype/helpers.ts +7 -29
  37. package/src/archetype/store.ts +35 -20
  38. package/src/commands/buffer.ts +5 -2
  39. package/src/commands/changeset.ts +0 -31
  40. package/src/component/registry.ts +64 -63
  41. package/src/entity/index.ts +6 -3
  42. package/src/index.ts +13 -0
  43. package/src/query/filter.ts +4 -10
  44. package/src/query/query.ts +12 -12
  45. package/src/storage/serialization.ts +29 -2
  46. package/src/types/index.ts +71 -0
  47. package/src/world/commands.ts +44 -56
  48. package/src/world/hooks.ts +8 -0
  49. package/src/world/serialization.ts +32 -18
  50. package/src/world/world.ts +387 -20
@@ -1,5 +1,5 @@
1
1
  import { Archetype } from "../archetype/archetype";
2
- import { DontFragmentStoreImpl } from "../archetype/store";
2
+ import { SparseStoreImpl } from "../archetype/store";
3
3
  import { CommandBuffer, type Command } from "../commands/buffer";
4
4
  import { ComponentChangeset } from "../commands/changeset";
5
5
  import { ComponentEntityStore } from "../component/entity-store";
@@ -13,17 +13,26 @@ import {
13
13
  getDetailedIdType,
14
14
  getTargetIdFromRelationId,
15
15
  isCascadeDeleteRelation,
16
- isDontFragmentRelation,
17
- isDontFragmentWildcard,
18
16
  isEntityRelation,
19
17
  isExclusiveComponent,
18
+ isSparseRelation,
19
+ isSparseWildcard,
20
20
  isWildcardRelationId,
21
+ relation,
21
22
  } from "../entity";
22
23
  import { matchesFilter, serializeQueryFilter, type QueryFilter } from "../query/filter";
23
24
  import type { Query } from "../query/query";
24
25
  import { QueryRegistry } from "../query/registry";
25
26
  import type { SerializedWorld } from "../storage/serialization";
26
- import type { ComponentTuple, ComponentType, LifecycleCallback, LifecycleHook, LifecycleHookEntry } from "../types";
27
+ import type {
28
+ ComponentTuple,
29
+ ComponentType,
30
+ DebugStatsCollector,
31
+ LifecycleCallback,
32
+ LifecycleHook,
33
+ LifecycleHookEntry,
34
+ SyncDebugStats,
35
+ } from "../types";
27
36
  import { isOptionalEntityId } from "../types";
28
37
  import { getOrCompute } from "../utils/utils";
29
38
  import { EntityBuilder } from "./builder";
@@ -37,6 +46,7 @@ import {
37
46
  } from "./commands";
38
47
  import {
39
48
  collectMultiHookComponents,
49
+ debugHookExecutionCounter,
40
50
  triggerLifecycleHooks,
41
51
  triggerRemoveHooksForEntityDeletion,
42
52
  type HooksContext,
@@ -63,8 +73,8 @@ export class World {
63
73
  private entityReferences: EntityReferencesMap = new Map();
64
74
  /** Reverse index: entity ID → set of archetypes whose componentTypes include that entity ID */
65
75
  private entityToReferencingArchetypes = new Map<EntityId, Set<Archetype>>();
66
- /** DontFragment relation storage, shared with all Archetype instances */
67
- private readonly dontFragmentStore = new DontFragmentStoreImpl();
76
+ /** Sparse relation storage (for components created with `sparse: true`), shared with all Archetype instances */
77
+ private readonly sparseStore = new SparseStoreImpl();
68
78
  /** Component entity (singleton) storage */
69
79
  private readonly componentEntities = new ComponentEntityStore();
70
80
 
@@ -74,6 +84,14 @@ export class World {
74
84
  // Lifecycle hooks (declared before cached contexts that reference them)
75
85
  private hooks: Set<LifecycleHookEntry> = new Set();
76
86
 
87
+ // Debug observability collectors (armed only when non-empty)
88
+ private readonly _debugCollectors = new Set<(stats: SyncDebugStats) => void>();
89
+
90
+ // Transient counters for the current armed sync (reset each time)
91
+ private _debugMigrations = 0;
92
+ private _debugArchetypesCreated = 0;
93
+ private _debugArchetypesRemoved = 0;
94
+
77
95
  // Command execution
78
96
  private commandBuffer = new CommandBuffer((entityId, commands) => this.executeEntityCommands(entityId, commands));
79
97
 
@@ -82,7 +100,7 @@ export class World {
82
100
  private readonly _removeChangeset = new ComponentChangeset();
83
101
  /** Cached command processor context to avoid per-entity object allocation */
84
102
  private readonly _commandCtx: CommandProcessorContext = {
85
- dontFragmentStore: this.dontFragmentStore,
103
+ sparseStore: this.sparseStore,
86
104
  ensureArchetype: (ct) => this.ensureArchetype(ct),
87
105
  };
88
106
  /** Cached hooks context to avoid per-entity object allocation */
@@ -198,7 +216,7 @@ export class World {
198
216
  }
199
217
  }
200
218
 
201
- // Remove entity from archetype - this also cleans up dontFragment relations
219
+ // Remove entity from archetype - this also cleans up sparse relations
202
220
  // and returns all removed component data
203
221
  this.entityReferences.delete(cur);
204
222
  const removedComponents = archetype.removeEntity(cur)!;
@@ -443,11 +461,11 @@ export class World {
443
461
 
444
462
  if (archetype.componentTypeSet.has(componentType)) return true;
445
463
 
446
- if (isDontFragmentRelation(componentType)) {
464
+ if (isSparseRelation(componentType)) {
447
465
  // Use getValue; presence check via getAllForEntity only if value can legitimately be undefined
448
- const val = this.dontFragmentStore.getValue(entityId, componentType);
466
+ const val = this.sparseStore.getValue(entityId, componentType);
449
467
  if (val !== undefined) return true;
450
- return this.dontFragmentStore.getAllForEntity(entityId).some(([t]) => t === componentType);
468
+ return this.sparseStore.getAllForEntity(entityId).some(([t]) => t === componentType);
451
469
  }
452
470
 
453
471
  return false;
@@ -495,12 +513,12 @@ export class World {
495
513
 
496
514
  if (componentType >= 0 || componentType % RELATION_SHIFT !== 0) {
497
515
  const inArchetype = archetype.componentTypeSet.has(componentType);
498
- const hasDontFragment = isDontFragmentRelation(componentType);
516
+ const hasSparse = isSparseRelation(componentType);
499
517
  const hasComponent =
500
518
  inArchetype ||
501
- (hasDontFragment &&
502
- (this.dontFragmentStore.getValue(entityId, componentType) !== undefined ||
503
- this.dontFragmentStore.getAllForEntity(entityId).some(([t]) => t === componentType)));
519
+ (hasSparse &&
520
+ (this.sparseStore.getValue(entityId, componentType) !== undefined ||
521
+ this.sparseStore.getAllForEntity(entityId).some(([t]) => t === componentType)));
504
522
 
505
523
  if (!hasComponent) {
506
524
  throw new Error(
@@ -571,6 +589,213 @@ export class World {
571
589
  return archetype.getOptional(entityId, componentType);
572
590
  }
573
591
 
592
+ // ==========================================================================
593
+ // Relation & Hierarchy Companion Tools (public API)
594
+ // ==========================================================================
595
+
596
+ /**
597
+ * Retrieves all targets (and their associated data) for relations of a given
598
+ * base component on an entity.
599
+ *
600
+ * This is the ergonomic replacement for the common pattern:
601
+ * world.get(entity, relation(Comp, "*"))
602
+ *
603
+ * @example
604
+ * const ChildOf = component({ exclusive: true, sparse: true });
605
+ * const children = world.getRelationTargets(parent, ChildOf); // usually []
606
+ * const items = world.getRelationTargets(player, InInventory);
607
+ *
608
+ * // For common hierarchy use cases, prefer the higher-level helpers:
609
+ * // world.getChildren(parent, ChildOf), world.getParent(child, ChildOf)
610
+ */
611
+ getRelationTargets<T = void>(
612
+ entityId: EntityId,
613
+ relationComp: ComponentId<T>,
614
+ ): [target: EntityId<unknown>, data: T | undefined][] {
615
+ this.assertEntityExists(entityId, "Entity");
616
+
617
+ const wildcard = relation(relationComp, "*") as WildcardRelationId<T>;
618
+
619
+ // For component entities (singletons) the path is different; they rarely host relations
620
+ if (this.componentEntities.exists(entityId)) {
621
+ return this.componentEntities.getWildcard(entityId, wildcard);
622
+ }
623
+
624
+ // Regular entity path — archetype.get for wildcard always materializes the array
625
+ // (even if empty for a sparse relation that only has the marker)
626
+ const data = this.get(entityId, wildcard);
627
+ return data as [EntityId<unknown>, T | undefined][];
628
+ }
629
+
630
+ /**
631
+ * Returns every entity that currently holds a relation of the given base
632
+ * component pointing at `targetId`.
633
+ *
634
+ * This is the efficient **reverse** lookup. For common hierarchy cases,
635
+ * prefer the higher-level `world.getChildren(parent, ChildOf)` instead.
636
+ *
637
+ * @example
638
+ * const ChildOf = component({ exclusive: true, sparse: true });
639
+ * const directChildren = world.getRelationSources(ship, ChildOf);
640
+ */
641
+ getRelationSources(targetId: EntityId, relationComp: ComponentId<any>): EntityId[] {
642
+ const refs = getEntityReferences(this.entityReferences, targetId);
643
+ const result: EntityId[] = [];
644
+
645
+ for (const [source, relType] of refs) {
646
+ // Only consider still-living sources
647
+ if (!this.entityToArchetype.has(source) && !this.componentEntities.exists(source)) continue;
648
+
649
+ const decodedComp = getComponentIdFromRelationId(relType);
650
+ if (decodedComp === relationComp) {
651
+ result.push(source);
652
+ }
653
+ }
654
+ return result;
655
+ }
656
+
657
+ /**
658
+ * Returns true if the entity has any (or a specific-target) relation of the
659
+ * given base component.
660
+ */
661
+ hasRelation(entityId: EntityId, relationComp: ComponentId<any>, targetId?: EntityId): boolean {
662
+ this.assertEntityExists(entityId, "Entity");
663
+
664
+ if (targetId !== undefined) {
665
+ const specific = relation(relationComp, targetId);
666
+ return this.has(entityId, specific);
667
+ }
668
+
669
+ // Any target of this relation kind?
670
+ const targets = this.getRelationTargets(entityId, relationComp);
671
+ return targets.length > 0;
672
+ }
673
+
674
+ /**
675
+ * Returns the number of relations of the given base component held by the entity.
676
+ */
677
+ countRelations(entityId: EntityId, relationComp: ComponentId<any>): number {
678
+ this.assertEntityExists(entityId, "Entity");
679
+ const targets = this.getRelationTargets(entityId, relationComp);
680
+ return targets.length;
681
+ }
682
+
683
+ /**
684
+ * For an *exclusive* relation (e.g. ChildOf, Owner), returns the single
685
+ * target entity (or undefined if none).
686
+ *
687
+ * When the component was declared `exclusive: true`, this is the preferred
688
+ * accessor (clearer intent than array destructuring).
689
+ */
690
+ getSingleRelationTarget<T = void>(entityId: EntityId, relationComp: ComponentId<T>): EntityId | undefined {
691
+ const targets = this.getRelationTargets(entityId, relationComp);
692
+ return targets.length > 0 ? (targets[0]![0] as EntityId) : undefined;
693
+ }
694
+
695
+ // --------------------------------------------------------------------------
696
+ // High-level hierarchy helpers (convenience methods on World)
697
+ // --------------------------------------------------------------------------
698
+
699
+ /**
700
+ * Returns the direct children of `parent` for the given relationship component
701
+ * (typically a `ChildOf` or similar exclusive `sparse` relation).
702
+ *
703
+ * This is the recommended high-level API for hierarchy traversal.
704
+ * It uses the internal reverse reference index for efficiency.
705
+ *
706
+ * @example
707
+ * const ChildOf = component({ exclusive: true, sparse: true });
708
+ * const kids = world.getChildren(ship, ChildOf);
709
+ */
710
+ getChildren(parent: EntityId, childOf: ComponentId<any>): EntityId[] {
711
+ return this.getRelationSources(parent, childOf);
712
+ }
713
+
714
+ /**
715
+ * Returns the parent of `child` for the given relationship component
716
+ * (typically an exclusive `ChildOf` relation).
717
+ *
718
+ * @example
719
+ * const ChildOf = component({ exclusive: true, sparse: true });
720
+ * const parent = world.getParent(turret, ChildOf);
721
+ */
722
+ getParent(child: EntityId, childOf: ComponentId<any>): EntityId | undefined {
723
+ return this.getSingleRelationTarget(child, childOf);
724
+ }
725
+
726
+ /**
727
+ * Returns the ancestor chain from the immediate parent up to (but not
728
+ * including) the root for the given relationship component.
729
+ *
730
+ * @example
731
+ * const ChildOf = component({ exclusive: true, sparse: true });
732
+ * const ancestors = world.getAncestors(muzzle, ChildOf); // [turret, ship]
733
+ */
734
+ getAncestors(entity: EntityId, childOf: ComponentId<any>): EntityId[] {
735
+ const ancestors: EntityId[] = [];
736
+ let cur = this.getParent(entity, childOf);
737
+ while (cur !== undefined) {
738
+ ancestors.push(cur);
739
+ cur = this.getParent(cur, childOf);
740
+ }
741
+ return ancestors;
742
+ }
743
+
744
+ /**
745
+ * Iteratively traverses all descendants of `root` in DFS pre-order.
746
+ * This is a generator and is safe for very deep hierarchies.
747
+ *
748
+ * @example
749
+ * for (const { entity, depth, parent } of world.iterateDescendants(root, ChildOf)) {
750
+ * console.log(depth, entity);
751
+ * }
752
+ */
753
+ *iterateDescendants(
754
+ root: EntityId,
755
+ childOf: ComponentId<any>,
756
+ opts: { includeSelf?: boolean; maxDepth?: number } = {},
757
+ ): IterableIterator<{ entity: EntityId; depth: number; parent: EntityId | null }> {
758
+ const { includeSelf = false, maxDepth } = opts;
759
+ const stack: Array<{ entity: EntityId; depth: number; parent: EntityId | null }> = [];
760
+
761
+ if (includeSelf) {
762
+ stack.push({ entity: root, depth: 0, parent: null });
763
+ } else {
764
+ for (const child of this.getChildren(root, childOf)) {
765
+ stack.push({ entity: child, depth: 1, parent: root });
766
+ }
767
+ }
768
+
769
+ while (stack.length > 0) {
770
+ const current = stack.pop()!;
771
+ if (maxDepth !== undefined && current.depth > maxDepth) continue;
772
+
773
+ yield current;
774
+
775
+ const kids = this.getChildren(current.entity, childOf);
776
+ for (let i = kids.length - 1; i >= 0; i--) {
777
+ const k = kids[i]!;
778
+ stack.push({ entity: k, depth: current.depth + 1, parent: current.entity });
779
+ }
780
+ }
781
+ }
782
+
783
+ /**
784
+ * Callback-based descendant traversal (hot path friendly).
785
+ * Return `false` from the visitor to stop early.
786
+ */
787
+ traverseDescendants(
788
+ root: EntityId,
789
+ childOf: ComponentId<any>,
790
+ visitor: (entity: EntityId, depth: number, parent: EntityId | null) => void | boolean,
791
+ opts: { includeSelf?: boolean; maxDepth?: number } = {},
792
+ ): void {
793
+ for (const { entity, depth, parent } of this.iterateDescendants(root, childOf, opts)) {
794
+ const res = visitor(entity, depth, parent);
795
+ if (res === false) return;
796
+ }
797
+ }
798
+
574
799
  /**
575
800
  * Registers a lifecycle hook that responds to component changes.
576
801
  * The hook callback is invoked when components matching the specified types are added, updated, or removed.
@@ -684,6 +909,103 @@ export class World {
684
909
  };
685
910
  }
686
911
 
912
+ /**
913
+ * Creates a debug stats collector that will receive a `SyncDebugStats` payload
914
+ * after every subsequent `sync()`.
915
+ *
916
+ * The returned object is a pure lifecycle handle. It does not store data.
917
+ * Collection stops when you call `[Symbol.dispose]()` (or use a `using` declaration).
918
+ *
919
+ * All active collectors receive the exact same stats object for a given sync.
920
+ * Exceptions thrown by callbacks are ignored.
921
+ *
922
+ * This is intended for development/debugging and leak detection.
923
+ */
924
+ createDebugStatsCollector(callback: (stats: SyncDebugStats) => void): DebugStatsCollector {
925
+ this._debugCollectors.add(callback);
926
+
927
+ return {
928
+ [Symbol.dispose]: () => {
929
+ this._debugCollectors.delete(callback);
930
+ },
931
+ };
932
+ }
933
+
934
+ private _resetDebugActivityCounters(): void {
935
+ this._debugMigrations = 0;
936
+ this._debugArchetypesCreated = 0;
937
+ this._debugArchetypesRemoved = 0;
938
+ debugHookExecutionCounter.value = 0;
939
+ }
940
+
941
+ private _deliverDebugStats(timings: {
942
+ syncStart: number;
943
+ syncEnd: number;
944
+ commandBufferStart: number;
945
+ commandBufferEnd: number;
946
+ commandIterations: number;
947
+ }): void {
948
+ // Build structural counts (post-sync)
949
+ // Note: singletons (component-as-entity) are not included in the main archetype map.
950
+ // For debug purposes the dominant number is regular entities; we keep it simple here.
951
+ const entityCount = this.entityToArchetype.size;
952
+ let emptyArchetypes = 0;
953
+ for (const arch of this.archetypes) {
954
+ if (arch.size === 0) emptyArchetypes++;
955
+ }
956
+
957
+ let archetypesByComponentSize = 0;
958
+ for (const set of this.archetypesByComponent.values()) {
959
+ archetypesByComponentSize += set.size;
960
+ }
961
+
962
+ const stats: SyncDebugStats = {
963
+ timestamps: {
964
+ syncStart: timings.syncStart,
965
+ syncEnd: timings.syncEnd,
966
+ commandBufferStart: timings.commandBufferStart,
967
+ commandBufferEnd: timings.commandBufferEnd,
968
+ },
969
+ commandIterations: timings.commandIterations,
970
+
971
+ entities: {
972
+ total: entityCount,
973
+ freelistSize: this.entityIdManager.getFreelistSize(),
974
+ nextId: this.entityIdManager.getNextId(),
975
+ },
976
+ archetypes: {
977
+ total: this.archetypes.length,
978
+ empty: emptyArchetypes,
979
+ },
980
+ queries: {
981
+ cached: (this.queryRegistry as any).cache?.size ?? 0,
982
+ registered: (this.queryRegistry as any).queries?.size ?? 0,
983
+ },
984
+ hooks: {
985
+ total: this.hooks.size,
986
+ },
987
+ indices: {
988
+ entityReferences: this.entityReferences.size,
989
+ entityToReferencingArchetypes: this.entityToReferencingArchetypes.size,
990
+ archetypesByComponent: archetypesByComponentSize,
991
+ },
992
+ activity: {
993
+ migrations: this._debugMigrations,
994
+ hooksExecuted: debugHookExecutionCounter.value,
995
+ archetypesCreated: this._debugArchetypesCreated,
996
+ archetypesRemoved: this._debugArchetypesRemoved,
997
+ },
998
+ };
999
+
1000
+ for (const cb of this._debugCollectors) {
1001
+ try {
1002
+ cb(stats);
1003
+ } catch {
1004
+ // Intentionally ignore user callback errors
1005
+ }
1006
+ }
1007
+ }
1008
+
687
1009
  /**
688
1010
  * Synchronizes all buffered commands (set/remove/delete) to the world.
689
1011
  * This method must be called after making changes via `set()`, `remove()`, or `delete()` for them to take effect.
@@ -695,7 +1017,29 @@ export class World {
695
1017
  * world.sync(); // Apply all buffered changes
696
1018
  */
697
1019
  sync(): void {
698
- this.commandBuffer.execute();
1020
+ const hasCollectors = this._debugCollectors.size > 0;
1021
+
1022
+ const syncStart = hasCollectors ? performance.now() : 0;
1023
+
1024
+ if (hasCollectors) {
1025
+ this._resetDebugActivityCounters();
1026
+ }
1027
+
1028
+ const commandBufferStart = hasCollectors ? performance.now() : 0;
1029
+ const commandIterations = this.commandBuffer.execute();
1030
+ const commandBufferEnd = hasCollectors ? performance.now() : 0;
1031
+
1032
+ const syncEnd = hasCollectors ? performance.now() : 0;
1033
+
1034
+ if (hasCollectors) {
1035
+ this._deliverDebugStats({
1036
+ syncStart,
1037
+ syncEnd,
1038
+ commandBufferStart,
1039
+ commandBufferEnd,
1040
+ commandIterations,
1041
+ });
1042
+ }
699
1043
  }
700
1044
 
701
1045
  /**
@@ -958,10 +1302,20 @@ export class World {
958
1302
 
959
1303
  if (this.hooks.size === 0) {
960
1304
  // Fast path: no hooks, skip removedComponents map allocation and hook triggering
961
- applyChangeset(this._commandCtx, entityId, currentArchetype, changeset, this.entityToArchetype, null);
1305
+ const newArchetype = applyChangeset(
1306
+ this._commandCtx,
1307
+ entityId,
1308
+ currentArchetype,
1309
+ changeset,
1310
+ this.entityToArchetype,
1311
+ null,
1312
+ );
962
1313
  if (hasStructuralChange) {
963
1314
  this.updateEntityReferences(entityId, changeset);
964
1315
  }
1316
+ if (this._debugCollectors.size > 0 && newArchetype !== currentArchetype) {
1317
+ this._debugMigrations++;
1318
+ }
965
1319
  return;
966
1320
  }
967
1321
 
@@ -978,6 +1332,11 @@ export class World {
978
1332
  if (hasStructuralChange) {
979
1333
  this.updateEntityReferences(entityId, changeset);
980
1334
  }
1335
+
1336
+ if (this._debugCollectors.size > 0 && newArchetype !== currentArchetype) {
1337
+ this._debugMigrations++;
1338
+ }
1339
+
981
1340
  triggerLifecycleHooks(
982
1341
  this.createHooksContext(),
983
1342
  entityId,
@@ -1099,9 +1458,13 @@ export class World {
1099
1458
  }
1100
1459
 
1101
1460
  private createNewArchetype(componentTypes: EntityId<any>[]): Archetype {
1102
- const newArchetype = new Archetype(componentTypes, this.dontFragmentStore);
1461
+ const newArchetype = new Archetype(componentTypes, this.sparseStore);
1103
1462
  this.archetypes.push(newArchetype);
1104
1463
 
1464
+ if (this._debugCollectors.size > 0) {
1465
+ this._debugArchetypesCreated++;
1466
+ }
1467
+
1105
1468
  for (const componentType of componentTypes) {
1106
1469
  let archetypes = this.archetypesByComponent.get(componentType);
1107
1470
  if (!archetypes) {
@@ -1135,11 +1498,11 @@ export class World {
1135
1498
  return (
1136
1499
  entry.requiredComponents.every((c: EntityId<any>) => {
1137
1500
  if (isWildcardRelationId(c)) {
1138
- if (isDontFragmentWildcard(c)) return true;
1501
+ if (isSparseWildcard(c)) return true;
1139
1502
  const componentId = getComponentIdFromRelationId(c);
1140
1503
  return componentId !== undefined && archetype.hasRelationWithComponentId(componentId);
1141
1504
  }
1142
- return archetype.componentTypeSet.has(c) || isDontFragmentRelation(c);
1505
+ return archetype.componentTypeSet.has(c) || isSparseRelation(c);
1143
1506
  }) && matchesFilter(archetype, entry.filter)
1144
1507
  );
1145
1508
  }
@@ -1166,6 +1529,10 @@ export class World {
1166
1529
  this.archetypes.pop();
1167
1530
  }
1168
1531
 
1532
+ if (this._debugCollectors.size > 0) {
1533
+ this._debugArchetypesRemoved++;
1534
+ }
1535
+
1169
1536
  this.archetypeBySignature.delete(this.createArchetypeSignature(archetype.componentTypes));
1170
1537
 
1171
1538
  for (const componentType of archetype.componentTypes) {