@codehz/ecs 0.6.10 → 0.7.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/world.mjs CHANGED
@@ -1,4 +1,4 @@
1
- //#region src/core/entity-types.ts
1
+ //#region src/entity/types.ts
2
2
  const COMPONENT_ID_MAX = 1023;
3
3
  const ENTITY_ID_START = 1024;
4
4
  /**
@@ -32,7 +32,7 @@ function isRelationId(id) {
32
32
  }
33
33
 
34
34
  //#endregion
35
- //#region src/core/entity-relation.ts
35
+ //#region src/entity/relation.ts
36
36
  /**
37
37
  * Internal function to decode a relation ID into raw component and target IDs
38
38
  * @param id The EntityId to decode
@@ -57,7 +57,10 @@ function relation(componentId, targetId) {
57
57
  return -(componentId * RELATION_SHIFT + actualTargetId);
58
58
  }
59
59
  /**
60
- * Check if an ID is a wildcard relation id
60
+ * Check if an ID is a wildcard relation (created with `relation(componentId, "*")`).
61
+ *
62
+ * @param id - The ID to check
63
+ * @returns `true` if the ID is a wildcard relation, `false` otherwise
61
64
  */
62
65
  function isWildcardRelationId(id) {
63
66
  const decoded = decodeRelationRaw(id);
@@ -169,7 +172,7 @@ function isEntityRelation(id) {
169
172
  }
170
173
 
171
174
  //#endregion
172
- //#region src/core/entity-manager.ts
175
+ //#region src/entity/manager.ts
173
176
  /**
174
177
  * Entity ID Manager for automatic allocation and freelist recycling
175
178
  */
@@ -362,7 +365,7 @@ var BitSet = class {
362
365
  };
363
366
 
364
367
  //#endregion
365
- //#region src/core/component-registry.ts
368
+ //#region src/component/registry.ts
366
369
  const globalComponentIdAllocator = new ComponentIdAllocator();
367
370
  const ComponentIdForNames = /* @__PURE__ */ new Map();
368
371
  const componentNames = new Array(COMPONENT_ID_MAX + 1);
@@ -428,8 +431,16 @@ function getBaseComponentId(componentType) {
428
431
  return isValidComponentId(decoded.componentId) ? decoded.componentId : void 0;
429
432
  }
430
433
  /**
431
- * Get merge callback for a componentType (including relation component types).
432
- * Returns undefined if the base component has no merge callback.
434
+ * Get the merge callback for a component type (including relation component types).
435
+ *
436
+ * Looks up the base component's merge function, resolving through relation wrappers.
437
+ * For example, if `ChildOf` has a merge function and you pass `relation(ChildOf, parent)`,
438
+ * the same merge function is returned.
439
+ *
440
+ * @param componentType - A raw component ID or a relation-wrapped component type
441
+ * (e.g., `relation(MyComp, targetEntity)`).
442
+ * @returns The merge callback if one was registered via {@link ComponentOptions.merge},
443
+ * or `undefined` if no merge was configured for the base component.
433
444
  */
434
445
  function getComponentMerge(componentType) {
435
446
  const baseComponentId = getBaseComponentId(componentType);
@@ -437,27 +448,64 @@ function getComponentMerge(componentType) {
437
448
  return componentMerges[baseComponentId];
438
449
  }
439
450
  /**
440
- * Check if a component is marked as exclusive
441
- * @param id The component ID
442
- * @returns true if the component is exclusive, false otherwise
451
+ * Check if a component was created with `exclusive: true`.
452
+ *
453
+ * This is a fast O(1) bitset lookup that determines whether the component enforces
454
+ * the one-to-one relation constraint — an entity can have at most one relation of
455
+ * this component type, and setting a new relation target automatically removes the
456
+ * previous one.
457
+ *
458
+ * **Note**: This only checks the component's intrinsic property, not whether a
459
+ * specific entity/relation ID is actually an exclusive relation. For checking
460
+ * runtime relation IDs (including wildcards), use {@link isExclusiveRelation}
461
+ * or {@link isExclusiveWildcard}.
462
+ *
463
+ * @param id - The component ID to check. Must be a plain component ID (1–1023),
464
+ * not a relation-wrapped ID.
465
+ * @returns `true` if the component was created with `exclusive: true`.
466
+ *
467
+ * @see {@link ComponentOptions.exclusive} for the full explanation of exclusive
468
+ * relation behavior.
469
+ * @see {@link isExclusiveRelation} for checking specific-target exclusive relations.
470
+ * @see {@link isExclusiveWildcard} for checking wildcard exclusive relations.
443
471
  */
444
472
  function isExclusiveComponent(id) {
445
473
  return exclusiveFlags.has(id);
446
474
  }
447
475
  /**
448
- * Check if a component is marked as dontFragment
449
- * @param id The component ID
450
- * @returns true if the component is dontFragment, false otherwise
476
+ * Check if a component is marked as `dontFragment`.
477
+ *
478
+ * When a component has `dontFragment: true`, relations using it do not cause
479
+ * archetype fragmentation — entities with different relation targets can share
480
+ * the same archetype. This is a fast O(1) bitset lookup.
481
+ *
482
+ * @param id - The component ID to check.
483
+ * @returns `true` if the component was created with `dontFragment: true`.
484
+ *
485
+ * @see {@link ComponentOptions.dontFragment} for the full explanation of how
486
+ * `dontFragment` prevents archetype fragmentation.
451
487
  */
452
488
  function isDontFragmentComponent(id) {
453
489
  return dontFragmentFlags.has(id);
454
490
  }
455
491
  /**
456
- * Generic function to check relation flags with specific target conditions
457
- * @param id The entity/relation ID to check
458
- * @param flagBitSet The bitset for the flag
459
- * @param targetCondition Function to check target ID condition
460
- * @returns true if the condition is met, false otherwise
492
+ * Generic optimized function to check whether a relation ID's base component
493
+ * has a specific flag in a bitset.
494
+ *
495
+ * Avoids the overhead of `getDetailedIdType` by directly decoding the relation
496
+ * ID and checking: (1) the ID is a valid relation, (2) the component ID is in the
497
+ * valid range, (3) the target satisfies the condition, and (4) the flag bit is set.
498
+ *
499
+ * Used as the fast-path implementation for `isDontFragmentRelation`,
500
+ * `isDontFragmentWildcard`, `isExclusiveRelation`, `isExclusiveWildcard`,
501
+ * and `isCascadeDeleteRelation`.
502
+ *
503
+ * @param id - The entity/relation ID to check.
504
+ * @param flagBitSet - The bitset tracking which component IDs have the flag.
505
+ * @param targetCondition - Predicate on the target ID (e.g., check for wildcard
506
+ * vs. specific entity target).
507
+ * @returns `true` if the relation's base component has the flag and the target
508
+ * condition is met.
461
509
  */
462
510
  function checkRelationFlag(id, flagBitSet, targetCondition) {
463
511
  const decoded = decodeRelationRaw(id);
@@ -466,42 +514,104 @@ function checkRelationFlag(id, flagBitSet, targetCondition) {
466
514
  return isValidComponentId(componentId) && targetCondition(targetId) && flagBitSet.has(componentId);
467
515
  }
468
516
  /**
469
- * Check if a relation ID is a dontFragment relation (entity-relation or component-relation with dontFragment component)
470
- * This is an optimized function that avoids the overhead of getDetailedIdType
471
- * @param id The entity/relation ID to check
472
- * @returns true if this is a dontFragment relation, false otherwise
517
+ * Check if an ID is a specific (non-wildcard) relation backed by a `dontFragment`
518
+ * component.
519
+ *
520
+ * This is used in hot paths (archetype resolution, command processing) to determine
521
+ * whether a relation should be excluded from the archetype signature. Relations with
522
+ * `dontFragment` components are stored in the shared {@link DontFragmentStore} instead
523
+ * of being part of the archetype's component type list.
524
+ *
525
+ * This is an optimized function that avoids the overhead of `getDetailedIdType`
526
+ * by directly decoding and checking the relation's component ID against the
527
+ * `dontFragment` bitset.
528
+ *
529
+ * @param id - The entity/relation ID to check (must be a relation ID, not a plain
530
+ * component ID).
531
+ * @returns `true` if this is a specific-target relation (not wildcard) whose base
532
+ * component was created with `dontFragment: true`.
533
+ *
534
+ * @see {@link isDontFragmentWildcard} for the wildcard variant.
535
+ * @see {@link ComponentOptions.dontFragment} for the full explanation.
473
536
  */
474
537
  function isDontFragmentRelation(id) {
475
538
  return checkRelationFlag(id, dontFragmentFlags, (targetId) => targetId !== WILDCARD_TARGET_ID);
476
539
  }
477
540
  /**
478
- * Check if an ID is a wildcard relation with dontFragment component
479
- * This is an optimized function for filtering archetype component types
480
- * @param id The entity/relation ID to check
481
- * @returns true if this is a wildcard relation with dontFragment component, false otherwise
541
+ * Check if an ID is a wildcard relation (`relation(Comp, "*")`) backed by a
542
+ * `dontFragment` component.
543
+ *
544
+ * Wildcard markers for `dontFragment` components are placed in the archetype
545
+ * component list so that queries can discover archetypes containing entities
546
+ * with that relation type. This function is used in `filterRegularComponentTypes`
547
+ * to **keep** these wildcard markers in the archetype signature while stripping
548
+ * out specific-target `dontFragment` relations.
549
+ *
550
+ * This is an optimized function that avoids the overhead of `getDetailedIdType`
551
+ * by directly decoding and checking the relation's component ID against the
552
+ * `dontFragment` bitset.
553
+ *
554
+ * @param id - The entity/relation ID to check.
555
+ * @returns `true` if this is a wildcard relation (`"*"` target) whose base
556
+ * component was created with `dontFragment: true`.
557
+ *
558
+ * @see {@link isDontFragmentRelation} for the specific-target variant.
559
+ * @see {@link ComponentOptions.dontFragment} for the full explanation.
482
560
  */
483
561
  function isDontFragmentWildcard(id) {
484
562
  return checkRelationFlag(id, dontFragmentFlags, (targetId) => targetId === WILDCARD_TARGET_ID);
485
563
  }
486
564
  /**
487
- * Check if a relation ID is a cascade delete entity-relation
488
- * This is an optimized function that avoids the overhead of getDetailedIdType
489
- * Note: Cascade delete only applies to entity-relations (not component-relations or wildcards)
565
+ * Check if a relation ID is a cascade delete entity-relation.
566
+ *
567
+ * This is an optimized function that avoids the overhead of getDetailedIdType.
568
+ *
569
+ * Cascade delete only applies to entity-relations (not component-relations or
570
+ * wildcards). When a cascade-delete-marked relation's target entity is deleted,
571
+ * the **entire source entity** (the one holding the relation) is deleted — not
572
+ * just the relation component. Without cascade delete, the relation component
573
+ * is simply removed (which is the default cleanup for all relations when their
574
+ * target is deleted).
575
+ *
490
576
  * @param id The entity/relation ID to check
491
577
  * @returns true if this is an entity-relation with cascade delete, false otherwise
578
+ * @see {@link ComponentOptions.cascadeDelete}
492
579
  */
493
580
  function isCascadeDeleteRelation(id) {
494
581
  return checkRelationFlag(id, cascadeDeleteFlags, (targetId) => targetId !== WILDCARD_TARGET_ID && targetId >= ENTITY_ID_START);
495
582
  }
496
583
 
497
584
  //#endregion
498
- //#region src/core/builder.ts
585
+ //#region src/world/builder.ts
586
+ /**
587
+ * Fluent API for constructing entities with multiple components.
588
+ * Create instances via {@link World.spawn}.
589
+ *
590
+ * @example
591
+ * const entity = world.spawn()
592
+ * .with(Position, { x: 0, y: 0 })
593
+ * .withRelation(Parent, parentEntity)
594
+ * .build();
595
+ * world.sync();
596
+ */
499
597
  var EntityBuilder = class {
500
598
  world;
501
599
  components = [];
502
600
  constructor(world) {
503
601
  this.world = world;
504
602
  }
603
+ /**
604
+ * Add a regular component to the entity under construction.
605
+ *
606
+ * @template T - The component data type
607
+ * @param componentId - The component type to add
608
+ * @param args - Component data (omit for void components)
609
+ * @returns This builder for chaining
610
+ *
611
+ * @example
612
+ * builder.with(Position, { x: 10, y: 20 });
613
+ * builder.with(Marker); // void component
614
+ */
505
615
  with(componentId, ...args) {
506
616
  const value = args.length > 0 ? args[0] : void 0;
507
617
  this.components.push({
@@ -512,16 +622,18 @@ var EntityBuilder = class {
512
622
  return this;
513
623
  }
514
624
  /**
515
- * @deprecated Use `with(componentId)` instead for void components
625
+ * Add a relation component to the entity under construction.
626
+ *
627
+ * @template T - The relation data type
628
+ * @param componentId - The base component type for the relation
629
+ * @param targetEntity - The target entity or component for the relation
630
+ * @param args - Relation data (omit for void relations)
631
+ * @returns This builder for chaining
632
+ *
633
+ * @example
634
+ * builder.withRelation(Parent, parentEntity);
635
+ * builder.withRelation(ChildOf, childEntity, { order: 1 });
516
636
  */
517
- withTag(componentId) {
518
- this.components.push({
519
- type: "component",
520
- id: componentId,
521
- value: void 0
522
- });
523
- return this;
524
- }
525
637
  withRelation(componentId, targetEntity, ...args) {
526
638
  const value = args.length > 0 ? args[0] : void 0;
527
639
  this.components.push({
@@ -533,22 +645,16 @@ var EntityBuilder = class {
533
645
  return this;
534
646
  }
535
647
  /**
536
- * @deprecated Use `withRelation(componentId, targetEntity)` instead for void relations
537
- */
538
- withRelationTag(componentId, targetEntity) {
539
- this.components.push({
540
- type: "relation",
541
- componentId,
542
- targetId: targetEntity,
543
- value: void 0
544
- });
545
- return this;
546
- }
547
- /**
548
- * Create an entity and enqueue components to be applied. This method
549
- * does NOT call `world.sync()` automatically; callers must invoke
550
- * `world.sync()` to apply deferred commands.
551
- * (Previously auto-synced; now a breaking change — buildDeferred() removed.)
648
+ * Create the entity and enqueue all configured components.
649
+ * The entity and components are only materialised after {@link World.sync} is called.
650
+ *
651
+ * @returns The newly created entity ID
652
+ *
653
+ * @example
654
+ * const entity = world.spawn()
655
+ * .with(Position, { x: 0, y: 0 })
656
+ * .build();
657
+ * world.sync(); // Apply changes
552
658
  */
553
659
  build() {
554
660
  const entity = this.world.new();
@@ -562,88 +668,447 @@ var EntityBuilder = class {
562
668
  };
563
669
 
564
670
  //#endregion
565
- //#region src/commands/changeset.ts
671
+ //#region src/component/type-utils.ts
566
672
  /**
567
- * @internal Represents a set of component changes to be applied to an entity
673
+ * Normalize component type collections into a stable ascending order.
674
+ * This keeps cache keys and archetype signatures deterministic.
568
675
  */
569
- var ComponentChangeset = class {
570
- adds = /* @__PURE__ */ new Map();
571
- removes = /* @__PURE__ */ new Set();
676
+ function normalizeComponentTypes(componentTypes) {
677
+ return [...componentTypes].sort((a, b) => a - b);
678
+ }
679
+
680
+ //#endregion
681
+ //#region src/types/index.ts
682
+ function isOptionalEntityId(type) {
683
+ return typeof type === "object" && type !== null && "optional" in type;
684
+ }
685
+
686
+ //#endregion
687
+ //#region src/utils/utils.ts
688
+ /**
689
+ * Utility functions for ECS library
690
+ */
691
+ /**
692
+ * Get a value from cache or compute and cache it if not present
693
+ * @param cache The cache map
694
+ * @param key The cache key
695
+ * @param compute Function to compute the value if not cached (may have side effects)
696
+ * @returns The cached or computed value
697
+ */
698
+ function getOrCompute(cache, key, compute) {
699
+ let value = cache.get(key);
700
+ if (value === void 0) {
701
+ value = compute();
702
+ cache.set(key, value);
703
+ }
704
+ return value;
705
+ }
706
+
707
+ //#endregion
708
+ //#region src/archetype/helpers.ts
709
+ /**
710
+ * Check if a components map has any wildcard relations matching a component ID
711
+ * @param components - Component entity's components map
712
+ * @param wildcardComponentId - The component ID to match
713
+ * @returns True if at least one matching relation exists
714
+ */
715
+ function hasWildcardRelation(components, wildcardComponentId) {
716
+ for (const relId of components.keys()) if (isRelationId(relId)) {
717
+ if (getComponentIdFromRelationId(relId) === wildcardComponentId) return true;
718
+ }
719
+ return false;
720
+ }
721
+ /**
722
+ * Check if a detailed type represents a relation (entity or component)
723
+ */
724
+ function isRelationType(detailedType) {
725
+ return detailedType.type === "entity-relation" || detailedType.type === "component-relation";
726
+ }
727
+ /**
728
+ * Check if a component type matches a given component ID for relations
729
+ */
730
+ function matchesRelationComponentId(componentType, componentId) {
731
+ const detailedType = getDetailedIdType(componentType);
732
+ return isRelationType(detailedType) && detailedType.componentId === componentId;
733
+ }
734
+ /**
735
+ * Find all relations in dontFragment data that match a component ID
736
+ */
737
+ function findMatchingDontFragmentRelations(dontFragmentData, componentId, relations = []) {
738
+ if (!dontFragmentData) return relations;
739
+ for (const [relType, data] of dontFragmentData) {
740
+ const relDetailed = getDetailedIdType(relType);
741
+ if (isRelationType(relDetailed) && relDetailed.componentId === componentId) relations.push([relDetailed.targetId, data]);
742
+ }
743
+ return relations;
744
+ }
745
+ /**
746
+ * Build cache key for component types
747
+ */
748
+ function buildCacheKey(componentTypes) {
749
+ return componentTypes.map((id) => isOptionalEntityId(id) ? `opt(${id.optional})` : `${id}`).join(",");
750
+ }
751
+ /**
752
+ * Get data source for wildcard relations from component types
753
+ */
754
+ function getWildcardRelationDataSource(componentTypes, componentId, optional) {
755
+ const matchingRelations = componentTypes.filter((ct) => matchesRelationComponentId(ct, componentId));
756
+ return optional ? matchingRelations.length > 0 ? matchingRelations : void 0 : matchingRelations;
757
+ }
758
+ /**
759
+ * Build wildcard relation value from matching relations
760
+ */
761
+ function buildWildcardRelationValue(wildcardRelationType, matchingRelations, getDataAtIndex, dontFragmentData, entityId, optional) {
762
+ const relations = [];
763
+ const targetComponentId = getComponentIdFromRelationId(wildcardRelationType);
764
+ for (const relType of matchingRelations || []) {
765
+ const data = getDataAtIndex(relType);
766
+ const targetId = getTargetIdFromRelationId(relType);
767
+ relations.push([targetId, data === MISSING_COMPONENT ? void 0 : data]);
768
+ }
769
+ if (targetComponentId !== void 0) findMatchingDontFragmentRelations(dontFragmentData, targetComponentId, relations);
770
+ if (relations.length === 0) {
771
+ if (!optional) {
772
+ const componentId = getComponentIdFromRelationId(wildcardRelationType);
773
+ throw new Error(`No matching relations found for mandatory wildcard relation component ${componentId} on entity ${entityId}`);
774
+ }
775
+ return;
776
+ }
777
+ return optional ? { value: relations } : relations;
778
+ }
779
+ /**
780
+ * Build regular component value from data source
781
+ */
782
+ function buildRegularComponentValue(dataSource, entityIndex, optional) {
783
+ if (dataSource === void 0) {
784
+ if (optional) return void 0;
785
+ throw new Error(`Component data not found for mandatory component type`);
786
+ }
787
+ const data = dataSource[entityIndex];
788
+ const result = data === MISSING_COMPONENT ? void 0 : data;
789
+ return optional ? { value: result } : result;
790
+ }
791
+ /**
792
+ * Build a single component value based on its type
793
+ */
794
+ function buildSingleComponent(compType, dataSource, entityIndex, entityId, getComponentData, dontFragmentRelations) {
795
+ const optional = isOptionalEntityId(compType);
796
+ const actualType = optional ? compType.optional : compType;
797
+ if (getIdType(actualType) === "wildcard-relation") return buildWildcardRelationValue(actualType, dataSource, (relType) => getComponentData(relType)[entityIndex], dontFragmentRelations.get(entityId), entityId, optional);
798
+ else return buildRegularComponentValue(dataSource, entityIndex, optional);
799
+ }
800
+
801
+ //#endregion
802
+ //#region src/archetype/archetype.ts
803
+ /**
804
+ * Special value to represent missing component data
805
+ */
806
+ const MISSING_COMPONENT = Symbol("missing component");
807
+ /**
808
+ * Archetype class for ECS architecture
809
+ * Represents a group of entities that share the same set of components
810
+ * Optimized for fast iteration and component access
811
+ */
812
+ var Archetype = class {
572
813
  /**
573
- * Add a component to the changeset
814
+ * The component types that define this archetype
574
815
  */
575
- set(componentType, component$1) {
576
- this.adds.set(componentType, component$1);
577
- this.removes.delete(componentType);
578
- }
816
+ componentTypes;
579
817
  /**
580
- * Remove a component from the changeset
818
+ * Set version of componentTypes for O(1) lookups in hot paths
581
819
  */
582
- delete(componentType) {
583
- this.removes.add(componentType);
584
- this.adds.delete(componentType);
585
- }
820
+ componentTypeSet;
586
821
  /**
587
- * Check if the changeset has any changes
822
+ * List of entities in this archetype
588
823
  */
589
- hasChanges() {
590
- return this.adds.size > 0 || this.removes.size > 0;
591
- }
824
+ entities = [];
592
825
  /**
593
- * Clear all changes
826
+ * Component data storage - maps component type to array of component data
827
+ * Each array index corresponds to the entity index in the entities array
594
828
  */
595
- clear() {
596
- this.adds.clear();
597
- this.removes.clear();
598
- }
829
+ componentData = /* @__PURE__ */ new Map();
599
830
  /**
600
- * Merge another changeset into this one
831
+ * Reverse mapping from entity to its index in this archetype
601
832
  */
602
- merge(other) {
603
- for (const [componentType, component$1] of other.adds) {
604
- this.adds.set(componentType, component$1);
605
- this.removes.delete(componentType);
606
- }
607
- for (const componentType of other.removes) {
608
- this.removes.add(componentType);
609
- this.adds.delete(componentType);
610
- }
611
- }
833
+ entityToIndex = /* @__PURE__ */ new Map();
612
834
  /**
613
- * Apply the changeset to existing components and return the final state
835
+ * DontFragmentStore for relation data keyed by entity ID.
836
+ * This allows entities with different relation targets to share the same archetype
837
+ * without migration overhead when entities change archetypes.
614
838
  */
615
- applyTo(existingComponents) {
616
- for (const componentType of this.removes) existingComponents.delete(componentType);
617
- for (const [componentType, component$1] of this.adds) existingComponents.set(componentType, component$1);
618
- return existingComponents;
619
- }
839
+ dontFragmentRelations;
620
840
  /**
621
- * Get the final component types after applying the changeset
622
- * @param existingComponentTypes - The current component types on the entity
623
- * @returns The final component types or undefined if no changes
841
+ * Multi-hooks that match this archetype
624
842
  */
625
- getFinalComponentTypes(existingComponentTypes) {
626
- const finalComponentTypes = new Set(existingComponentTypes);
627
- let changed = false;
628
- for (const componentType of this.removes) {
629
- if (!finalComponentTypes.has(componentType)) {
630
- this.removes.delete(componentType);
631
- continue;
843
+ matchingMultiHooks = /* @__PURE__ */ new Set();
844
+ /**
845
+ * Cache for pre-computed component data sources to avoid repeated calculations
846
+ */
847
+ componentDataSourcesCache = /* @__PURE__ */ new Map();
848
+ constructor(componentTypes, dontFragmentRelations) {
849
+ this.componentTypes = normalizeComponentTypes(componentTypes);
850
+ this.componentTypeSet = new Set(this.componentTypes);
851
+ this.dontFragmentRelations = dontFragmentRelations;
852
+ for (const componentType of this.componentTypes) this.componentData.set(componentType, []);
853
+ }
854
+ get size() {
855
+ return this.entities.length;
856
+ }
857
+ /**
858
+ * Check if the given component types match this archetype
859
+ * @param componentTypes - Component types to check (can be in any order)
860
+ * @returns true if the types match this archetype's component set
861
+ * @note This method handles unsorted input by internally sorting for comparison
862
+ */
863
+ matches(componentTypes) {
864
+ if (this.componentTypes.length !== componentTypes.length) return false;
865
+ const sortedTypes = normalizeComponentTypes(componentTypes);
866
+ return this.componentTypes.every((type, index) => type === sortedTypes[index]);
867
+ }
868
+ addEntity(entityId, componentData) {
869
+ if (this.entityToIndex.has(entityId)) throw new Error(`Entity ${entityId} is already in this archetype`);
870
+ const index = this.entities.length;
871
+ this.entities.push(entityId);
872
+ this.entityToIndex.set(entityId, index);
873
+ for (const componentType of this.componentTypes) {
874
+ const data = componentData.get(componentType);
875
+ this.getComponentData(componentType).push(!componentData.has(componentType) ? MISSING_COMPONENT : data);
876
+ }
877
+ this.addDontFragmentRelations(entityId, componentData);
878
+ }
879
+ addDontFragmentRelations(entityId, componentData) {
880
+ const dontFragmentData = /* @__PURE__ */ new Map();
881
+ for (const [componentType, data] of componentData) {
882
+ if (this.componentTypeSet.has(componentType)) continue;
883
+ const detailedType = getDetailedIdType(componentType);
884
+ if (isRelationType(detailedType) && isDontFragmentComponent(detailedType.componentId)) dontFragmentData.set(componentType, data);
885
+ }
886
+ if (dontFragmentData.size > 0) this.dontFragmentRelations.set(entityId, dontFragmentData);
887
+ }
888
+ getEntity(entityId) {
889
+ const index = this.entityToIndex.get(entityId);
890
+ if (index === void 0) return void 0;
891
+ const entityData = /* @__PURE__ */ new Map();
892
+ for (const componentType of this.componentTypes) {
893
+ const data = this.getComponentData(componentType)[index];
894
+ entityData.set(componentType, data === MISSING_COMPONENT ? void 0 : data);
895
+ }
896
+ const dontFragmentData = this.dontFragmentRelations.get(entityId);
897
+ if (dontFragmentData) for (const [componentType, data] of dontFragmentData) entityData.set(componentType, data);
898
+ return entityData;
899
+ }
900
+ getEntityDontFragmentRelations(entityId) {
901
+ return this.dontFragmentRelations.get(entityId);
902
+ }
903
+ dump() {
904
+ return this.entities.map((entity, i) => {
905
+ const components = /* @__PURE__ */ new Map();
906
+ for (const componentType of this.componentTypes) {
907
+ const data = this.getComponentData(componentType)[i];
908
+ components.set(componentType, data === MISSING_COMPONENT ? void 0 : data);
632
909
  }
633
- changed = true;
634
- finalComponentTypes.delete(componentType);
910
+ const dontFragmentData = this.dontFragmentRelations.get(entity);
911
+ if (dontFragmentData) for (const [componentType, data] of dontFragmentData) components.set(componentType, data);
912
+ return {
913
+ entity,
914
+ components
915
+ };
916
+ });
917
+ }
918
+ removeEntity(entityId) {
919
+ const index = this.entityToIndex.get(entityId);
920
+ if (index === void 0) return void 0;
921
+ const removedData = /* @__PURE__ */ new Map();
922
+ for (const componentType of this.componentTypes) removedData.set(componentType, this.getComponentData(componentType)[index]);
923
+ const dontFragmentData = this.dontFragmentRelations.get(entityId);
924
+ if (dontFragmentData) {
925
+ for (const [componentType, data] of dontFragmentData) removedData.set(componentType, data);
926
+ this.dontFragmentRelations.delete(entityId);
635
927
  }
636
- for (const componentType of this.adds.keys()) {
637
- if (finalComponentTypes.has(componentType)) continue;
638
- changed = true;
639
- finalComponentTypes.add(componentType);
928
+ this.entityToIndex.delete(entityId);
929
+ const lastIndex = this.entities.length - 1;
930
+ if (index !== lastIndex) {
931
+ const lastEntity = this.entities[lastIndex];
932
+ this.entities[index] = lastEntity;
933
+ this.entityToIndex.set(lastEntity, index);
934
+ for (const componentType of this.componentTypes) {
935
+ const dataArray = this.getComponentData(componentType);
936
+ dataArray[index] = dataArray[lastIndex];
937
+ }
640
938
  }
641
- return changed ? Array.from(finalComponentTypes) : void 0;
939
+ this.entities.pop();
940
+ for (const componentType of this.componentTypes) this.getComponentData(componentType).pop();
941
+ return removedData;
942
+ }
943
+ exists(entityId) {
944
+ return this.entityToIndex.has(entityId);
945
+ }
946
+ get(entityId, componentType) {
947
+ const index = this.entityToIndex.get(entityId);
948
+ if (index === void 0) throw new Error(`Entity ${entityId} is not in this archetype`);
949
+ if (isWildcardRelationId(componentType)) return this.getWildcardRelations(entityId, index, componentType);
950
+ return this.getRegularComponent(entityId, index, componentType);
951
+ }
952
+ getWildcardRelations(entityId, index, componentType) {
953
+ const componentId = getComponentIdFromRelationId(componentType);
954
+ const relations = [];
955
+ for (const relType of this.componentTypes) {
956
+ const relDetailed = getDetailedIdType(relType);
957
+ if (isRelationType(relDetailed) && relDetailed.componentId === componentId) {
958
+ const dataArray = this.getComponentData(relType);
959
+ if (dataArray && dataArray[index] !== void 0) {
960
+ const data = dataArray[index];
961
+ relations.push([relDetailed.targetId, data === MISSING_COMPONENT ? void 0 : data]);
962
+ }
963
+ }
964
+ }
965
+ if (componentId !== void 0) findMatchingDontFragmentRelations(this.dontFragmentRelations.get(entityId), componentId, relations);
966
+ return relations;
967
+ }
968
+ getRegularComponent(entityId, index, componentType) {
969
+ if (this.componentTypeSet.has(componentType)) {
970
+ const data = this.getComponentData(componentType)[index];
971
+ if (data === MISSING_COMPONENT) throw new Error(`Component type ${componentType} not found for entity ${entityId}`);
972
+ return data;
973
+ }
974
+ const dontFragmentData = this.dontFragmentRelations.get(entityId);
975
+ if (dontFragmentData?.has(componentType)) return dontFragmentData.get(componentType);
976
+ throw new Error(`Component type ${componentType} not found for entity ${entityId}`);
977
+ }
978
+ getOptional(entityId, componentType) {
979
+ const index = this.entityToIndex.get(entityId);
980
+ if (index === void 0) throw new Error(`Entity ${entityId} is not in this archetype`);
981
+ if (this.componentTypeSet.has(componentType)) {
982
+ const data = this.getComponentData(componentType)[index];
983
+ if (data === MISSING_COMPONENT) return void 0;
984
+ return { value: data };
985
+ }
986
+ const dontFragmentData = this.dontFragmentRelations.get(entityId);
987
+ if (dontFragmentData?.has(componentType)) return { value: dontFragmentData.get(componentType) };
988
+ }
989
+ set(entityId, componentType, data) {
990
+ const index = this.entityToIndex.get(entityId);
991
+ if (index === void 0) throw new Error(`Entity ${entityId} is not in this archetype`);
992
+ if (this.componentData.has(componentType)) {
993
+ this.getComponentData(componentType)[index] = data;
994
+ return;
995
+ }
996
+ const detailedType = getDetailedIdType(componentType);
997
+ if (isRelationType(detailedType) && isDontFragmentComponent(detailedType.componentId)) {
998
+ let dontFragmentData = this.dontFragmentRelations.get(entityId);
999
+ if (!dontFragmentData) {
1000
+ dontFragmentData = /* @__PURE__ */ new Map();
1001
+ this.dontFragmentRelations.set(entityId, dontFragmentData);
1002
+ }
1003
+ dontFragmentData.set(componentType, data);
1004
+ return;
1005
+ }
1006
+ throw new Error(`Component type ${componentType} is not in this archetype`);
1007
+ }
1008
+ getEntities() {
1009
+ return this.entities;
1010
+ }
1011
+ getEntityToIndexMap() {
1012
+ return this.entityToIndex;
1013
+ }
1014
+ getComponentData(componentType) {
1015
+ const data = this.componentData.get(componentType);
1016
+ if (!data) throw new Error(`Component type ${componentType} is not in this archetype`);
1017
+ return data;
1018
+ }
1019
+ getOptionalComponentData(componentType) {
1020
+ return this.componentData.get(componentType);
1021
+ }
1022
+ getCachedComponentDataSources(componentTypes) {
1023
+ const cacheKey = buildCacheKey(componentTypes);
1024
+ return getOrCompute(this.componentDataSourcesCache, cacheKey, () => componentTypes.map((compType) => this.getComponentDataSource(compType)));
1025
+ }
1026
+ getComponentDataSource(compType) {
1027
+ const optional = isOptionalEntityId(compType);
1028
+ const actualType = optional ? compType.optional : compType;
1029
+ if (getIdType(actualType) === "wildcard-relation") {
1030
+ const componentId = getComponentIdFromRelationId(actualType);
1031
+ return getWildcardRelationDataSource(this.componentTypes, componentId, optional);
1032
+ }
1033
+ return optional ? this.getOptionalComponentData(actualType) : this.getComponentData(actualType);
1034
+ }
1035
+ buildComponentsForIndex(componentTypes, componentDataSources, entityIndex, entityId) {
1036
+ return componentDataSources.map((dataSource, i) => buildSingleComponent(componentTypes[i], dataSource, entityIndex, entityId, (type) => this.getComponentData(type), this.dontFragmentRelations));
1037
+ }
1038
+ getEntitiesWithComponents(componentTypes) {
1039
+ const result = [];
1040
+ this.appendEntitiesWithComponents(componentTypes, result);
1041
+ return result;
1042
+ }
1043
+ appendEntitiesWithComponents(componentTypes, result) {
1044
+ this.forEachWithComponents(componentTypes, (entity, ...components) => {
1045
+ result.push({
1046
+ entity,
1047
+ components
1048
+ });
1049
+ });
1050
+ }
1051
+ *iterateWithComponents(componentTypes) {
1052
+ const componentDataSources = this.getCachedComponentDataSources(componentTypes);
1053
+ for (let entityIndex = 0; entityIndex < this.entities.length; entityIndex++) {
1054
+ const entity = this.entities[entityIndex];
1055
+ yield [entity, ...this.buildComponentsForIndex(componentTypes, componentDataSources, entityIndex, entity)];
1056
+ }
1057
+ }
1058
+ forEachWithComponents(componentTypes, callback) {
1059
+ const componentDataSources = this.getCachedComponentDataSources(componentTypes);
1060
+ for (let entityIndex = 0; entityIndex < this.entities.length; entityIndex++) {
1061
+ const entity = this.entities[entityIndex];
1062
+ callback(entity, ...this.buildComponentsForIndex(componentTypes, componentDataSources, entityIndex, entity));
1063
+ }
1064
+ }
1065
+ forEach(callback) {
1066
+ for (let i = 0; i < this.entities.length; i++) {
1067
+ const components = /* @__PURE__ */ new Map();
1068
+ for (const componentType of this.componentTypes) {
1069
+ const data = this.getComponentData(componentType)[i];
1070
+ components.set(componentType, data === MISSING_COMPONENT ? void 0 : data);
1071
+ }
1072
+ callback(this.entities[i], components);
1073
+ }
1074
+ }
1075
+ hasRelationWithComponentId(componentId) {
1076
+ for (const componentType of this.componentTypes) {
1077
+ const detailedType = getDetailedIdType(componentType);
1078
+ if (isRelationType(detailedType) && detailedType.componentId === componentId) return true;
1079
+ }
1080
+ for (const entityId of this.entities) {
1081
+ const entityDontFragmentRelations = this.dontFragmentRelations.get(entityId);
1082
+ if (entityDontFragmentRelations) for (const relationType of entityDontFragmentRelations.keys()) {
1083
+ const detailedType = getDetailedIdType(relationType);
1084
+ if (isRelationType(detailedType) && detailedType.componentId === componentId) return true;
1085
+ }
1086
+ }
1087
+ return false;
1088
+ }
1089
+ };
1090
+
1091
+ //#endregion
1092
+ //#region src/archetype/store.ts
1093
+ /**
1094
+ * Default implementation backed by a plain `Map`.
1095
+ * Created once by `World` and shared with every `Archetype`.
1096
+ */
1097
+ var DontFragmentStoreImpl = class {
1098
+ data = /* @__PURE__ */ new Map();
1099
+ get(entityId) {
1100
+ return this.data.get(entityId);
1101
+ }
1102
+ set(entityId, data) {
1103
+ this.data.set(entityId, data);
1104
+ }
1105
+ delete(entityId) {
1106
+ this.data.delete(entityId);
642
1107
  }
643
1108
  };
644
1109
 
645
1110
  //#endregion
646
- //#region src/commands/command-buffer.ts
1111
+ //#region src/commands/buffer.ts
647
1112
  /**
648
1113
  * Maximum number of command buffer execution iterations to prevent infinite loops
649
1114
  */
@@ -727,702 +1192,591 @@ var CommandBuffer = class {
727
1192
  };
728
1193
 
729
1194
  //#endregion
730
- //#region src/query/filter.ts
731
- /**
732
- * Serialize a QueryFilter into a deterministic string suitable for cache keys.
733
- * Currently only serializes `negativeComponentTypes`.
734
- */
735
- function serializeQueryFilter(filter = {}) {
736
- const negative = (filter.negativeComponentTypes || []).slice().sort((a, b) => a - b);
737
- if (negative.length === 0) return "";
738
- return `neg:${negative.join(",")}`;
739
- }
740
- /**
741
- * Check if an archetype matches the given component types
742
- */
743
- function matchesComponentTypes(archetype, componentTypes) {
744
- return componentTypes.every((type) => {
745
- const detailedType = getDetailedIdType(type);
746
- if (detailedType.type === "wildcard-relation") return archetype.componentTypes.some((archetypeType) => {
747
- if (!isRelationId(archetypeType)) return false;
748
- return getComponentIdFromRelationId(archetypeType) === detailedType.componentId;
749
- });
750
- else if ((detailedType.type === "entity-relation" || detailedType.type === "component-relation") && detailedType.componentId !== void 0 && isDontFragmentComponent(detailedType.componentId)) {
751
- const wildcardMarker = relation(detailedType.componentId, "*");
752
- return archetype.componentTypeSet.has(wildcardMarker);
753
- } else return archetype.componentTypeSet.has(type);
754
- });
755
- }
756
- /**
757
- * Check if an archetype matches the filter conditions (only filtering logic)
758
- */
759
- function matchesFilter(archetype, filter) {
760
- return (filter.negativeComponentTypes || []).every((type) => {
761
- const detailedType = getDetailedIdType(type);
762
- if (detailedType.type === "wildcard-relation") return !archetype.componentTypes.some((archetypeType) => {
763
- if (!isRelationId(archetypeType)) return false;
764
- return getComponentIdFromRelationId(archetypeType) === detailedType.componentId;
765
- });
766
- else return !archetype.componentTypeSet.has(type);
767
- });
768
- }
769
-
770
- //#endregion
771
- //#region src/core/component-type-utils.ts
772
- /**
773
- * Normalize component type collections into a stable ascending order.
774
- * This keeps cache keys and archetype signatures deterministic.
775
- */
776
- function normalizeComponentTypes(componentTypes) {
777
- return [...componentTypes].sort((a, b) => a - b);
778
- }
779
-
780
- //#endregion
781
- //#region src/query/query.ts
1195
+ //#region src/commands/changeset.ts
782
1196
  /**
783
- * Query class for efficient entity queries with cached archetypes
1197
+ * @internal Represents a set of component changes to be applied to an entity
784
1198
  */
785
- var Query = class {
786
- world;
787
- componentTypes;
788
- filter;
789
- cachedArchetypes = [];
790
- isDisposed = false;
791
- /** Cache key assigned by World for O(1) releaseQuery lookup */
792
- _cacheKey;
793
- /** Cached wildcard component types for faster entity filtering */
794
- wildcardTypes;
795
- /** Cached specific dontFragment relation types that need entity-level filtering */
796
- specificDontFragmentTypes;
797
- constructor(world, componentTypes, filter = {}) {
798
- this.world = world;
799
- this.componentTypes = normalizeComponentTypes(componentTypes);
800
- this.filter = filter;
801
- this.wildcardTypes = this.componentTypes.filter((ct) => getDetailedIdType(ct).type === "wildcard-relation");
802
- this.specificDontFragmentTypes = this.componentTypes.filter((ct) => {
803
- const detailedType = getDetailedIdType(ct);
804
- return (detailedType.type === "entity-relation" || detailedType.type === "component-relation") && detailedType.componentId !== void 0 && isDontFragmentComponent(detailedType.componentId);
805
- });
806
- this.updateCache();
807
- world._registerQuery(this);
1199
+ var ComponentChangeset = class {
1200
+ adds = /* @__PURE__ */ new Map();
1201
+ removes = /* @__PURE__ */ new Set();
1202
+ /**
1203
+ * Add a component to the changeset
1204
+ */
1205
+ set(componentType, component$1) {
1206
+ this.adds.set(componentType, component$1);
1207
+ this.removes.delete(componentType);
808
1208
  }
809
1209
  /**
810
- * Check if query is disposed and throw error if so
1210
+ * Remove a component from the changeset
811
1211
  */
812
- ensureNotDisposed() {
813
- if (this.isDisposed) throw new Error("Query has been disposed");
1212
+ delete(componentType) {
1213
+ this.removes.add(componentType);
1214
+ this.adds.delete(componentType);
814
1215
  }
815
1216
  /**
816
- * Get all entities matching the query
1217
+ * Check if the changeset has any changes
817
1218
  */
818
- getEntities() {
819
- this.ensureNotDisposed();
820
- if (this.wildcardTypes.length === 0 && this.specificDontFragmentTypes.length === 0) {
821
- const result$1 = [];
822
- for (const archetype of this.cachedArchetypes) result$1.push(...archetype.getEntities());
823
- return result$1;
1219
+ hasChanges() {
1220
+ return this.adds.size > 0 || this.removes.size > 0;
1221
+ }
1222
+ /**
1223
+ * Clear all changes
1224
+ */
1225
+ clear() {
1226
+ this.adds.clear();
1227
+ this.removes.clear();
1228
+ }
1229
+ /**
1230
+ * Merge another changeset into this one
1231
+ */
1232
+ merge(other) {
1233
+ for (const [componentType, component$1] of other.adds) {
1234
+ this.adds.set(componentType, component$1);
1235
+ this.removes.delete(componentType);
1236
+ }
1237
+ for (const componentType of other.removes) {
1238
+ this.removes.add(componentType);
1239
+ this.adds.delete(componentType);
824
1240
  }
825
- const result = [];
826
- for (const archetype of this.cachedArchetypes) for (const entity of archetype.getEntities()) if (this.entityMatchesQuery(archetype, entity)) result.push(entity);
827
- return result;
828
1241
  }
829
1242
  /**
830
- * Check if entity matches all query requirements (wildcards and specific dontFragment relations)
1243
+ * Apply the changeset to existing components and return the final state
1244
+ */
1245
+ applyTo(existingComponents) {
1246
+ for (const componentType of this.removes) existingComponents.delete(componentType);
1247
+ for (const [componentType, component$1] of this.adds) existingComponents.set(componentType, component$1);
1248
+ return existingComponents;
1249
+ }
1250
+ /**
1251
+ * Get the final component types after applying the changeset
1252
+ * @param existingComponentTypes - The current component types on the entity
1253
+ * @returns The final component types or undefined if no changes
1254
+ */
1255
+ getFinalComponentTypes(existingComponentTypes) {
1256
+ const finalComponentTypes = new Set(existingComponentTypes);
1257
+ let changed = false;
1258
+ for (const componentType of this.removes) {
1259
+ if (!finalComponentTypes.has(componentType)) {
1260
+ this.removes.delete(componentType);
1261
+ continue;
1262
+ }
1263
+ changed = true;
1264
+ finalComponentTypes.delete(componentType);
1265
+ }
1266
+ for (const componentType of this.adds.keys()) {
1267
+ if (finalComponentTypes.has(componentType)) continue;
1268
+ changed = true;
1269
+ finalComponentTypes.add(componentType);
1270
+ }
1271
+ return changed ? Array.from(finalComponentTypes) : void 0;
1272
+ }
1273
+ };
1274
+
1275
+ //#endregion
1276
+ //#region src/component/entity-store.ts
1277
+ /**
1278
+ * Manages component entity (singleton) storage.
1279
+ *
1280
+ * Component entities use a flat Map-based storage rather than the Archetype-based
1281
+ * storage used for regular entities. Their IDs are in the component ID range
1282
+ * (or are relation IDs), distinguishing them from regular entity IDs.
1283
+ */
1284
+ var ComponentEntityStore = class {
1285
+ componentEntityComponents = /* @__PURE__ */ new Map();
1286
+ relationEntityIdsByTarget = /* @__PURE__ */ new Map();
1287
+ /**
1288
+ * Check if an entity ID is a component entity type.
1289
+ * Returns true for component IDs, component-relation IDs, and entity-relation IDs —
1290
+ * i.e. anything that is NOT a plain entity or an invalid ID.
831
1291
  */
832
- entityMatchesQuery(archetype, entity) {
833
- for (const wildcardType of this.wildcardTypes) {
834
- const relations = archetype.get(entity, wildcardType);
835
- if (!relations || relations.length === 0) return false;
836
- }
837
- for (const specificType of this.specificDontFragmentTypes) if (archetype.getOptional(entity, specificType) === void 0) return false;
838
- return true;
1292
+ exists(entityId) {
1293
+ const detailed = getDetailedIdType(entityId);
1294
+ return detailed.type !== "entity" && detailed.type !== "invalid";
839
1295
  }
840
1296
  /**
841
- * Get entities with their component data
842
- * @param componentTypes Array of component types to retrieve
843
- * @returns Array of objects with entity and component data
1297
+ * Check if a component entity has a specific component.
844
1298
  */
845
- getEntitiesWithComponents(componentTypes) {
846
- this.ensureNotDisposed();
847
- const result = [];
848
- for (const archetype of this.cachedArchetypes) {
849
- const entitiesWithData = archetype.getEntitiesWithComponents(componentTypes);
850
- result.push(...entitiesWithData);
851
- }
852
- return result;
1299
+ has(entityId, componentType) {
1300
+ return this.componentEntityComponents.get(entityId)?.has(componentType) ?? false;
853
1301
  }
854
1302
  /**
855
- * Iterate over entities with their component data
856
- * @param componentTypes Array of component types to retrieve
857
- * @param callback Function called for each entity with its components
1303
+ * Check if a singleton component has data — the has(componentId) overload.
1304
+ * In singleton usage the entity ID and the component type are the same value.
858
1305
  */
859
- forEach(componentTypes, callback) {
860
- this.ensureNotDisposed();
861
- for (const archetype of this.cachedArchetypes) archetype.forEachWithComponents(componentTypes, callback);
1306
+ hasSingleton(componentId) {
1307
+ return this.componentEntityComponents.get(componentId)?.has(componentId) ?? false;
862
1308
  }
863
1309
  /**
864
- * Iterate over entities with their component data (generator)
865
- * @param componentTypes Array of component types to retrieve
1310
+ * Check if a component entity has any wildcard relations matching a component ID.
866
1311
  */
867
- *iterate(componentTypes) {
868
- this.ensureNotDisposed();
869
- for (const archetype of this.cachedArchetypes) yield* archetype.iterateWithComponents(componentTypes);
1312
+ hasWildcard(entityId, componentId) {
1313
+ const data = this.componentEntityComponents.get(entityId);
1314
+ if (!data) return false;
1315
+ return hasWildcardRelation(data, componentId);
870
1316
  }
871
1317
  /**
872
- * Get component data arrays for all matching entities
873
- * @param componentType The component type to retrieve
874
- * @returns Array of component data for all matching entities
1318
+ * Get a component value from a component entity.
1319
+ * Throws if the component does not exist.
875
1320
  */
876
- getComponentData(componentType) {
877
- this.ensureNotDisposed();
878
- const result = [];
879
- for (const archetype of this.cachedArchetypes) result.push(...archetype.getComponentData(componentType));
880
- return result;
1321
+ get(entityId, componentType) {
1322
+ const data = this.componentEntityComponents.get(entityId);
1323
+ if (!data || !data.has(componentType)) throw new Error(`Entity ${entityId} does not have component ${componentType}. Use has() to check component existence before calling get().`);
1324
+ return data.get(componentType);
881
1325
  }
882
1326
  /**
883
- * Update the cached archetypes
884
- * Called when new archetypes are created
1327
+ * Get an optional component value from a component entity.
1328
+ * Returns undefined if the component does not exist.
885
1329
  */
886
- updateCache() {
887
- if (this.isDisposed) return;
888
- this.cachedArchetypes = this.world.getMatchingArchetypes(this.componentTypes).filter((archetype) => matchesFilter(archetype, this.filter));
1330
+ getOptional(entityId, componentType) {
1331
+ const data = this.componentEntityComponents.get(entityId);
1332
+ if (!data || !data.has(componentType)) return void 0;
1333
+ return { value: data.get(componentType) };
889
1334
  }
890
1335
  /**
891
- * Check if a new archetype matches this query and add to cache if it does
1336
+ * Get all wildcard relations of a given type from a component entity.
892
1337
  */
893
- checkNewArchetype(archetype) {
894
- if (this.isDisposed) return;
895
- if (matchesComponentTypes(archetype, this.componentTypes) && matchesFilter(archetype, this.filter) && !this.cachedArchetypes.includes(archetype)) this.cachedArchetypes.push(archetype);
1338
+ getWildcard(entityId, wildcardComponentType) {
1339
+ const componentId = getComponentIdFromRelationId(wildcardComponentType);
1340
+ const data = this.componentEntityComponents.get(entityId);
1341
+ if (componentId === void 0 || !data) return [];
1342
+ const relations = [];
1343
+ for (const [key, value] of data.entries()) {
1344
+ if (getComponentIdFromRelationId(key) !== componentId) continue;
1345
+ const detailed = getDetailedIdType(key);
1346
+ if (detailed.type === "entity-relation" || detailed.type === "component-relation") relations.push([detailed.targetId, value]);
1347
+ }
1348
+ return relations;
896
1349
  }
897
1350
  /**
898
- * Remove an archetype from the cached archetypes
1351
+ * Clear all data for a component entity.
899
1352
  */
900
- removeArchetype(archetype) {
901
- if (this.isDisposed) return;
902
- const index = this.cachedArchetypes.indexOf(archetype);
903
- if (index !== -1) this.cachedArchetypes.splice(index, 1);
1353
+ clear(entityId) {
1354
+ if (this.componentEntityComponents.delete(entityId)) this.unregisterRelationEntityId(entityId);
904
1355
  }
905
1356
  /**
906
- * Request disposal of this query.
907
- * This will decrement the world's reference count for the query.
908
- * The query will only be fully disposed when the ref count reaches zero.
1357
+ * Cleanup all component entities that reference a given target entity.
1358
+ * Called when a target entity is destroyed.
909
1359
  */
910
- dispose() {
911
- this.world.releaseQuery(this);
1360
+ cleanupReferencesTo(targetId) {
1361
+ const relationEntities = this.relationEntityIdsByTarget.get(targetId);
1362
+ if (!relationEntities) return;
1363
+ for (const relationEntityId of relationEntities) this.componentEntityComponents.delete(relationEntityId);
1364
+ this.relationEntityIdsByTarget.delete(targetId);
912
1365
  }
913
1366
  /**
914
- * Internal full dispose called by World when refCount reaches zero.
1367
+ * Execute a batch of commands for a component entity.
915
1368
  */
916
- _disposeInternal() {
917
- if (!this.isDisposed) {
918
- this.world._unregisterQuery(this);
919
- this.cachedArchetypes = [];
920
- this.isDisposed = true;
1369
+ executeCommands(entityId, commands) {
1370
+ if (commands.some((cmd) => cmd.type === "destroy")) {
1371
+ this.clear(entityId);
1372
+ return;
1373
+ }
1374
+ const pendingSetValues = /* @__PURE__ */ new Map();
1375
+ for (const command of commands) if (command.type === "set" && command.componentType) {
1376
+ const merge = getComponentMerge(command.componentType);
1377
+ let nextValue = command.component;
1378
+ if (merge !== void 0 && pendingSetValues.has(command.componentType)) nextValue = merge(pendingSetValues.get(command.componentType), command.component);
1379
+ pendingSetValues.set(command.componentType, nextValue);
1380
+ let data = this.componentEntityComponents.get(entityId);
1381
+ if (!data) {
1382
+ data = /* @__PURE__ */ new Map();
1383
+ this.componentEntityComponents.set(entityId, data);
1384
+ this.registerRelationEntityId(entityId);
1385
+ }
1386
+ data.set(command.componentType, nextValue);
1387
+ } else if (command.type === "delete" && command.componentType) {
1388
+ const data = this.componentEntityComponents.get(entityId);
1389
+ if (isWildcardRelationId(command.componentType)) {
1390
+ const componentId = getComponentIdFromRelationId(command.componentType);
1391
+ if (componentId !== void 0) {
1392
+ if (data) {
1393
+ for (const key of Array.from(data.keys())) if (getComponentIdFromRelationId(key) === componentId) data.delete(key);
1394
+ }
1395
+ for (const key of Array.from(pendingSetValues.keys())) if (getComponentIdFromRelationId(key) === componentId) pendingSetValues.delete(key);
1396
+ }
1397
+ } else {
1398
+ data?.delete(command.componentType);
1399
+ pendingSetValues.delete(command.componentType);
1400
+ }
1401
+ if (data?.size === 0) this.clear(entityId);
921
1402
  }
922
1403
  }
923
1404
  /**
924
- * Symbol.dispose implementation for automatic resource management
1405
+ * Initialize a component entity from a deserialization snapshot.
925
1406
  */
926
- [Symbol.dispose]() {
927
- this.dispose();
1407
+ initFromSnapshot(entityId, componentMap) {
1408
+ this.componentEntityComponents.set(entityId, componentMap);
1409
+ this.registerRelationEntityId(entityId);
928
1410
  }
929
1411
  /**
930
- * Check if the query has been disposed
1412
+ * Iterate over all component entity entries.
1413
+ * Used for serialization.
931
1414
  */
932
- get disposed() {
933
- return this.isDisposed;
1415
+ entries() {
1416
+ return this.componentEntityComponents.entries();
934
1417
  }
935
- };
936
-
937
- //#endregion
938
- //#region src/utils/utils.ts
939
- /**
940
- * Utility functions for ECS library
941
- */
942
- /**
943
- * Get a value from cache or compute and cache it if not present
944
- * @param cache The cache map
945
- * @param key The cache key
946
- * @param compute Function to compute the value if not cached (may have side effects)
947
- * @returns The cached or computed value
948
- */
949
- function getOrCompute(cache, key, compute) {
950
- let value = cache.get(key);
951
- if (value === void 0) {
952
- value = compute();
953
- cache.set(key, value);
1418
+ registerRelationEntityId(entityId) {
1419
+ const detailed = getDetailedIdType(entityId);
1420
+ if (detailed.type !== "entity-relation") return;
1421
+ const targetId = detailed.targetId;
1422
+ if (targetId === void 0) return;
1423
+ const existing = this.relationEntityIdsByTarget.get(targetId);
1424
+ if (existing) {
1425
+ existing.add(entityId);
1426
+ return;
1427
+ }
1428
+ this.relationEntityIdsByTarget.set(targetId, new Set([entityId]));
954
1429
  }
955
- return value;
956
- }
957
-
958
- //#endregion
959
- //#region src/core/types.ts
960
- function isOptionalEntityId(type) {
961
- return typeof type === "object" && type !== null && "optional" in type;
962
- }
1430
+ unregisterRelationEntityId(entityId) {
1431
+ const detailed = getDetailedIdType(entityId);
1432
+ if (detailed.type !== "entity-relation") return;
1433
+ const targetId = detailed.targetId;
1434
+ if (targetId === void 0) return;
1435
+ const existing = this.relationEntityIdsByTarget.get(targetId);
1436
+ if (!existing) return;
1437
+ existing.delete(entityId);
1438
+ if (existing.size === 0) this.relationEntityIdsByTarget.delete(targetId);
1439
+ }
1440
+ };
963
1441
 
964
1442
  //#endregion
965
- //#region src/core/archetype-helpers.ts
966
- /**
967
- * Check if a components map has any wildcard relations matching a component ID
968
- * @param components - Component entity's components map
969
- * @param wildcardComponentId - The component ID to match
970
- * @returns True if at least one matching relation exists
971
- */
972
- function hasWildcardRelation(components, wildcardComponentId) {
973
- for (const relId of components.keys()) if (isRelationId(relId)) {
974
- if (getComponentIdFromRelationId(relId) === wildcardComponentId) return true;
975
- }
976
- return false;
977
- }
978
- /**
979
- * Check if a detailed type represents a relation (entity or component)
980
- */
981
- function isRelationType(detailedType) {
982
- return detailedType.type === "entity-relation" || detailedType.type === "component-relation";
983
- }
984
- /**
985
- * Check if a component type matches a given component ID for relations
986
- */
987
- function matchesRelationComponentId(componentType, componentId) {
988
- const detailedType = getDetailedIdType(componentType);
989
- return isRelationType(detailedType) && detailedType.componentId === componentId;
990
- }
991
- /**
992
- * Find all relations in dontFragment data that match a component ID
993
- */
994
- function findMatchingDontFragmentRelations(dontFragmentData, componentId) {
995
- const relations = [];
996
- if (!dontFragmentData) return relations;
997
- for (const [relType, data] of dontFragmentData) {
998
- const relDetailed = getDetailedIdType(relType);
999
- if (isRelationType(relDetailed) && relDetailed.componentId === componentId) relations.push([relDetailed.targetId, data]);
1000
- }
1001
- return relations;
1002
- }
1003
- /**
1004
- * Build cache key for component types
1005
- */
1006
- function buildCacheKey(componentTypes) {
1007
- return componentTypes.map((id) => isOptionalEntityId(id) ? `opt(${id.optional})` : `${id}`).join(",");
1008
- }
1009
- /**
1010
- * Get data source for wildcard relations from component types
1011
- */
1012
- function getWildcardRelationDataSource(componentTypes, componentId, optional) {
1013
- const matchingRelations = componentTypes.filter((ct) => matchesRelationComponentId(ct, componentId));
1014
- return optional ? matchingRelations.length > 0 ? matchingRelations : void 0 : matchingRelations;
1015
- }
1443
+ //#region src/query/filter.ts
1016
1444
  /**
1017
- * Build wildcard relation value from matching relations
1445
+ * Serialize a QueryFilter into a deterministic string suitable for cache keys.
1446
+ * Currently only serializes `negativeComponentTypes`.
1018
1447
  */
1019
- function buildWildcardRelationValue(wildcardRelationType, matchingRelations, getDataAtIndex, dontFragmentData, entityId, optional) {
1020
- const relations = [];
1021
- const targetComponentId = getComponentIdFromRelationId(wildcardRelationType);
1022
- for (const relType of matchingRelations || []) {
1023
- const data = getDataAtIndex(relType);
1024
- const targetId = getTargetIdFromRelationId(relType);
1025
- relations.push([targetId, data === MISSING_COMPONENT ? void 0 : data]);
1026
- }
1027
- if (targetComponentId !== void 0) relations.push(...findMatchingDontFragmentRelations(dontFragmentData, targetComponentId));
1028
- if (relations.length === 0) {
1029
- if (!optional) {
1030
- const componentId = getComponentIdFromRelationId(wildcardRelationType);
1031
- throw new Error(`No matching relations found for mandatory wildcard relation component ${componentId} on entity ${entityId}`);
1032
- }
1033
- return;
1034
- }
1035
- return optional ? { value: relations } : relations;
1448
+ function serializeQueryFilter(filter = {}) {
1449
+ const negative = (filter.negativeComponentTypes || []).slice().sort((a, b) => a - b);
1450
+ if (negative.length === 0) return "";
1451
+ return `neg:${negative.join(",")}`;
1036
1452
  }
1037
1453
  /**
1038
- * Build regular component value from data source
1454
+ * Check if an archetype matches the given component types
1039
1455
  */
1040
- function buildRegularComponentValue(dataSource, entityIndex, optional) {
1041
- if (dataSource === void 0) {
1042
- if (optional) return void 0;
1043
- throw new Error(`Component data not found for mandatory component type`);
1044
- }
1045
- const data = dataSource[entityIndex];
1046
- const result = data === MISSING_COMPONENT ? void 0 : data;
1047
- return optional ? { value: result } : result;
1456
+ function matchesComponentTypes(archetype, componentTypes) {
1457
+ return componentTypes.every((type) => {
1458
+ const detailedType = getDetailedIdType(type);
1459
+ if (detailedType.type === "wildcard-relation") return archetype.componentTypes.some((archetypeType) => {
1460
+ if (!isRelationId(archetypeType)) return false;
1461
+ return getComponentIdFromRelationId(archetypeType) === detailedType.componentId;
1462
+ });
1463
+ else if ((detailedType.type === "entity-relation" || detailedType.type === "component-relation") && detailedType.componentId !== void 0 && isDontFragmentComponent(detailedType.componentId)) {
1464
+ const wildcardMarker = relation(detailedType.componentId, "*");
1465
+ return archetype.componentTypeSet.has(wildcardMarker);
1466
+ } else return archetype.componentTypeSet.has(type);
1467
+ });
1048
1468
  }
1049
1469
  /**
1050
- * Build a single component value based on its type
1470
+ * Check if an archetype matches the filter conditions (only filtering logic)
1051
1471
  */
1052
- function buildSingleComponent(compType, dataSource, entityIndex, entityId, getComponentData, dontFragmentRelations) {
1053
- const optional = isOptionalEntityId(compType);
1054
- const actualType = optional ? compType.optional : compType;
1055
- if (getIdType(actualType) === "wildcard-relation") return buildWildcardRelationValue(actualType, dataSource, (relType) => getComponentData(relType)[entityIndex], dontFragmentRelations.get(entityId), entityId, optional);
1056
- else return buildRegularComponentValue(dataSource, entityIndex, optional);
1472
+ function matchesFilter(archetype, filter) {
1473
+ return (filter.negativeComponentTypes || []).every((type) => {
1474
+ const detailedType = getDetailedIdType(type);
1475
+ if (detailedType.type === "wildcard-relation") return !archetype.componentTypes.some((archetypeType) => {
1476
+ if (!isRelationId(archetypeType)) return false;
1477
+ return getComponentIdFromRelationId(archetypeType) === detailedType.componentId;
1478
+ });
1479
+ else return !archetype.componentTypeSet.has(type);
1480
+ });
1057
1481
  }
1058
1482
 
1059
1483
  //#endregion
1060
- //#region src/core/archetype.ts
1061
- /**
1062
- * Special value to represent missing component data
1063
- */
1064
- const MISSING_COMPONENT = Symbol("missing component");
1484
+ //#region src/query/query.ts
1065
1485
  /**
1066
- * Archetype class for ECS architecture
1067
- * Represents a group of entities that share the same set of components
1068
- * Optimized for fast iteration and component access
1486
+ * Cached query for efficiently iterating entities with specific components.
1487
+ *
1488
+ * Queries are created via {@link World.createQuery} and should be **reused across frames**
1489
+ * for optimal performance. The world automatically keeps the query's internal archetype cache
1490
+ * up to date as entities are created and destroyed.
1491
+ *
1492
+ * @example
1493
+ * const movementQuery = world.createQuery([Position, Velocity]);
1494
+ *
1495
+ * // In the game loop
1496
+ * movementQuery.forEach([Position, Velocity], (entity, pos, vel) => {
1497
+ * pos.x += vel.x;
1498
+ * pos.y += vel.y;
1499
+ * });
1069
1500
  */
1070
- var Archetype = class {
1071
- /**
1072
- * The component types that define this archetype
1073
- */
1501
+ var Query = class {
1502
+ world;
1074
1503
  componentTypes;
1504
+ filter;
1505
+ cachedArchetypes = [];
1506
+ isDisposed = false;
1507
+ /** Cache key assigned by World for O(1) releaseQuery lookup */
1508
+ _cacheKey;
1509
+ /** Cached wildcard component types for faster entity filtering */
1510
+ wildcardTypes;
1511
+ /** Cached specific dontFragment relation types that need entity-level filtering */
1512
+ specificDontFragmentTypes;
1075
1513
  /**
1076
- * Set version of componentTypes for O(1) lookups in hot paths
1077
- */
1078
- componentTypeSet;
1079
- /**
1080
- * List of entities in this archetype
1081
- */
1082
- entities = [];
1083
- /**
1084
- * Component data storage - maps component type to array of component data
1085
- * Each array index corresponds to the entity index in the entities array
1086
- */
1087
- componentData = /* @__PURE__ */ new Map();
1088
- /**
1089
- * Reverse mapping from entity to its index in this archetype
1090
- */
1091
- entityToIndex = /* @__PURE__ */ new Map();
1092
- /**
1093
- * Reference to dontFragment relations storage from World
1094
- * This allows entities with different relation targets to share the same archetype
1095
- * Stored in World to avoid migration overhead when entities change archetypes
1096
- */
1097
- dontFragmentRelations;
1098
- /**
1099
- * Multi-hooks that match this archetype
1100
- */
1101
- matchingMultiHooks = /* @__PURE__ */ new Set();
1102
- /**
1103
- * Cache for pre-computed component data sources to avoid repeated calculations
1514
+ * @internal Queries should be created via {@link World.createQuery}, not instantiated directly.
1104
1515
  */
1105
- componentDataSourcesCache = /* @__PURE__ */ new Map();
1106
- constructor(componentTypes, dontFragmentRelations) {
1516
+ constructor(world, componentTypes, filter = {}, registry) {
1517
+ this.world = world;
1107
1518
  this.componentTypes = normalizeComponentTypes(componentTypes);
1108
- this.componentTypeSet = new Set(this.componentTypes);
1109
- this.dontFragmentRelations = dontFragmentRelations;
1110
- for (const componentType of this.componentTypes) this.componentData.set(componentType, []);
1111
- }
1112
- get size() {
1113
- return this.entities.length;
1519
+ this.filter = filter;
1520
+ this.wildcardTypes = this.componentTypes.filter((ct) => getDetailedIdType(ct).type === "wildcard-relation");
1521
+ this.specificDontFragmentTypes = this.componentTypes.filter((ct) => {
1522
+ const detailedType = getDetailedIdType(ct);
1523
+ return (detailedType.type === "entity-relation" || detailedType.type === "component-relation") && detailedType.componentId !== void 0 && isDontFragmentComponent(detailedType.componentId);
1524
+ });
1525
+ this.updateCache();
1526
+ if (registry) registry.register(this);
1114
1527
  }
1115
1528
  /**
1116
- * Check if the given component types match this archetype
1117
- * @param componentTypes - Component types to check (can be in any order)
1118
- * @returns true if the types match this archetype's component set
1119
- * @note This method handles unsorted input by internally sorting for comparison
1529
+ * Check if query is disposed and throw error if so
1120
1530
  */
1121
- matches(componentTypes) {
1122
- if (this.componentTypes.length !== componentTypes.length) return false;
1123
- const sortedTypes = normalizeComponentTypes(componentTypes);
1124
- return this.componentTypes.every((type, index) => type === sortedTypes[index]);
1125
- }
1126
- addEntity(entityId, componentData) {
1127
- if (this.entityToIndex.has(entityId)) throw new Error(`Entity ${entityId} is already in this archetype`);
1128
- const index = this.entities.length;
1129
- this.entities.push(entityId);
1130
- this.entityToIndex.set(entityId, index);
1131
- for (const componentType of this.componentTypes) {
1132
- const data = componentData.get(componentType);
1133
- this.getComponentData(componentType).push(!componentData.has(componentType) ? MISSING_COMPONENT : data);
1134
- }
1135
- this.addDontFragmentRelations(entityId, componentData);
1136
- }
1137
- addDontFragmentRelations(entityId, componentData) {
1138
- const dontFragmentData = /* @__PURE__ */ new Map();
1139
- for (const [componentType, data] of componentData) {
1140
- if (this.componentTypeSet.has(componentType)) continue;
1141
- const detailedType = getDetailedIdType(componentType);
1142
- if (isRelationType(detailedType) && isDontFragmentComponent(detailedType.componentId)) dontFragmentData.set(componentType, data);
1143
- }
1144
- if (dontFragmentData.size > 0) this.dontFragmentRelations.set(entityId, dontFragmentData);
1145
- }
1146
- getEntity(entityId) {
1147
- const index = this.entityToIndex.get(entityId);
1148
- if (index === void 0) return void 0;
1149
- const entityData = /* @__PURE__ */ new Map();
1150
- for (const componentType of this.componentTypes) {
1151
- const data = this.getComponentData(componentType)[index];
1152
- entityData.set(componentType, data === MISSING_COMPONENT ? void 0 : data);
1153
- }
1154
- const dontFragmentData = this.dontFragmentRelations.get(entityId);
1155
- if (dontFragmentData) for (const [componentType, data] of dontFragmentData) entityData.set(componentType, data);
1156
- return entityData;
1157
- }
1158
- getEntityDontFragmentRelations(entityId) {
1159
- return this.dontFragmentRelations.get(entityId);
1160
- }
1161
- dump() {
1162
- return this.entities.map((entity, i) => {
1163
- const components = /* @__PURE__ */ new Map();
1164
- for (const componentType of this.componentTypes) {
1165
- const data = this.getComponentData(componentType)[i];
1166
- components.set(componentType, data === MISSING_COMPONENT ? void 0 : data);
1167
- }
1168
- const dontFragmentData = this.dontFragmentRelations.get(entity);
1169
- if (dontFragmentData) for (const [componentType, data] of dontFragmentData) components.set(componentType, data);
1170
- return {
1171
- entity,
1172
- components
1173
- };
1174
- });
1175
- }
1176
- removeEntity(entityId) {
1177
- const index = this.entityToIndex.get(entityId);
1178
- if (index === void 0) return void 0;
1179
- const removedData = /* @__PURE__ */ new Map();
1180
- for (const componentType of this.componentTypes) removedData.set(componentType, this.getComponentData(componentType)[index]);
1181
- const dontFragmentData = this.dontFragmentRelations.get(entityId);
1182
- if (dontFragmentData) {
1183
- for (const [componentType, data] of dontFragmentData) removedData.set(componentType, data);
1184
- this.dontFragmentRelations.delete(entityId);
1185
- }
1186
- this.entityToIndex.delete(entityId);
1187
- const lastIndex = this.entities.length - 1;
1188
- if (index !== lastIndex) {
1189
- const lastEntity = this.entities[lastIndex];
1190
- this.entities[index] = lastEntity;
1191
- this.entityToIndex.set(lastEntity, index);
1192
- for (const componentType of this.componentTypes) {
1193
- const dataArray = this.getComponentData(componentType);
1194
- dataArray[index] = dataArray[lastIndex];
1195
- }
1196
- }
1197
- this.entities.pop();
1198
- for (const componentType of this.componentTypes) this.getComponentData(componentType).pop();
1199
- return removedData;
1200
- }
1201
- exists(entityId) {
1202
- return this.entityToIndex.has(entityId);
1203
- }
1204
- get(entityId, componentType) {
1205
- const index = this.entityToIndex.get(entityId);
1206
- if (index === void 0) throw new Error(`Entity ${entityId} is not in this archetype`);
1207
- if (isWildcardRelationId(componentType)) return this.getWildcardRelations(entityId, index, componentType);
1208
- return this.getRegularComponent(entityId, index, componentType);
1209
- }
1210
- getWildcardRelations(entityId, index, componentType) {
1211
- const componentId = getComponentIdFromRelationId(componentType);
1212
- const relations = [];
1213
- for (const relType of this.componentTypes) {
1214
- const relDetailed = getDetailedIdType(relType);
1215
- if (isRelationType(relDetailed) && relDetailed.componentId === componentId) {
1216
- const dataArray = this.getComponentData(relType);
1217
- if (dataArray && dataArray[index] !== void 0) {
1218
- const data = dataArray[index];
1219
- relations.push([relDetailed.targetId, data === MISSING_COMPONENT ? void 0 : data]);
1220
- }
1221
- }
1222
- }
1223
- if (componentId !== void 0) relations.push(...findMatchingDontFragmentRelations(this.dontFragmentRelations.get(entityId), componentId));
1224
- return relations;
1531
+ ensureNotDisposed() {
1532
+ if (this.isDisposed) throw new Error("Query has been disposed");
1225
1533
  }
1226
- getRegularComponent(entityId, index, componentType) {
1227
- if (this.componentTypeSet.has(componentType)) {
1228
- const data = this.getComponentData(componentType)[index];
1229
- if (data === MISSING_COMPONENT) throw new Error(`Component type ${componentType} not found for entity ${entityId}`);
1230
- return data;
1534
+ /**
1535
+ * Returns all entity IDs that match this query.
1536
+ *
1537
+ * @returns Array of matching entity IDs
1538
+ *
1539
+ * @example
1540
+ * const entities = query.getEntities();
1541
+ * for (const entity of entities) {
1542
+ * const pos = world.get(entity, Position);
1543
+ * }
1544
+ */
1545
+ getEntities() {
1546
+ this.ensureNotDisposed();
1547
+ if (this.wildcardTypes.length === 0 && this.specificDontFragmentTypes.length === 0) {
1548
+ const result$1 = [];
1549
+ for (const archetype of this.cachedArchetypes) for (const entity of archetype.getEntities()) result$1.push(entity);
1550
+ return result$1;
1231
1551
  }
1232
- const dontFragmentData = this.dontFragmentRelations.get(entityId);
1233
- if (dontFragmentData?.has(componentType)) return dontFragmentData.get(componentType);
1234
- throw new Error(`Component type ${componentType} not found for entity ${entityId}`);
1552
+ const result = [];
1553
+ for (const archetype of this.cachedArchetypes) for (const entity of archetype.getEntities()) if (this.entityMatchesQuery(archetype, entity)) result.push(entity);
1554
+ return result;
1235
1555
  }
1236
- getOptional(entityId, componentType) {
1237
- const index = this.entityToIndex.get(entityId);
1238
- if (index === void 0) throw new Error(`Entity ${entityId} is not in this archetype`);
1239
- if (this.componentTypeSet.has(componentType)) {
1240
- const data = this.getComponentData(componentType)[index];
1241
- if (data === MISSING_COMPONENT) return void 0;
1242
- return { value: data };
1556
+ /**
1557
+ * Check if entity matches all query requirements (wildcards and specific dontFragment relations)
1558
+ */
1559
+ entityMatchesQuery(archetype, entity) {
1560
+ for (const wildcardType of this.wildcardTypes) {
1561
+ const relations = archetype.get(entity, wildcardType);
1562
+ if (!relations || relations.length === 0) return false;
1243
1563
  }
1244
- const dontFragmentData = this.dontFragmentRelations.get(entityId);
1245
- if (dontFragmentData?.has(componentType)) return { value: dontFragmentData.get(componentType) };
1564
+ for (const specificType of this.specificDontFragmentTypes) if (archetype.getOptional(entity, specificType) === void 0) return false;
1565
+ return true;
1246
1566
  }
1247
- set(entityId, componentType, data) {
1248
- const index = this.entityToIndex.get(entityId);
1249
- if (index === void 0) throw new Error(`Entity ${entityId} is not in this archetype`);
1250
- if (this.componentData.has(componentType)) {
1251
- this.getComponentData(componentType)[index] = data;
1252
- return;
1253
- }
1254
- const detailedType = getDetailedIdType(componentType);
1255
- if (isRelationType(detailedType) && isDontFragmentComponent(detailedType.componentId)) {
1256
- let dontFragmentData = this.dontFragmentRelations.get(entityId);
1257
- if (!dontFragmentData) {
1258
- dontFragmentData = /* @__PURE__ */ new Map();
1259
- this.dontFragmentRelations.set(entityId, dontFragmentData);
1260
- }
1261
- dontFragmentData.set(componentType, data);
1262
- return;
1263
- }
1264
- throw new Error(`Component type ${componentType} is not in this archetype`);
1567
+ /**
1568
+ * Returns all matching entities along with their component data.
1569
+ *
1570
+ * @param componentTypes - Array of component types to retrieve
1571
+ * @returns Array of objects containing the entity ID and its component tuple
1572
+ *
1573
+ * @example
1574
+ * const results = query.getEntitiesWithComponents([Position, Velocity]);
1575
+ * results.forEach(({ entity, components: [pos, vel] }) => {
1576
+ * pos.x += vel.x;
1577
+ * });
1578
+ */
1579
+ getEntitiesWithComponents(componentTypes) {
1580
+ this.ensureNotDisposed();
1581
+ const result = [];
1582
+ for (const archetype of this.cachedArchetypes) archetype.appendEntitiesWithComponents(componentTypes, result);
1583
+ return result;
1265
1584
  }
1266
- getEntities() {
1267
- return this.entities;
1585
+ /**
1586
+ * Iterates over all matching entities and invokes the callback with their component data.
1587
+ * This is the preferred way to read and mutate components in a hot loop.
1588
+ *
1589
+ * @param componentTypes - Array of component types to retrieve
1590
+ * @param callback - Function called for each matching entity with its components
1591
+ *
1592
+ * @example
1593
+ * query.forEach([Position, Velocity], (entity, pos, vel) => {
1594
+ * pos.x += vel.x;
1595
+ * pos.y += vel.y;
1596
+ * });
1597
+ */
1598
+ forEach(componentTypes, callback) {
1599
+ this.ensureNotDisposed();
1600
+ for (const archetype of this.cachedArchetypes) archetype.forEachWithComponents(componentTypes, callback);
1268
1601
  }
1269
- getEntityToIndexMap() {
1270
- return this.entityToIndex;
1602
+ /**
1603
+ * Generator that yields each matching entity together with its component data.
1604
+ *
1605
+ * @param componentTypes - Array of component types to retrieve
1606
+ * @yields Tuples of `[entityId, ...components]`
1607
+ *
1608
+ * @example
1609
+ * for (const [entity, pos, vel] of query.iterate([Position, Velocity])) {
1610
+ * pos.x += vel.x;
1611
+ * }
1612
+ */
1613
+ *iterate(componentTypes) {
1614
+ this.ensureNotDisposed();
1615
+ for (const archetype of this.cachedArchetypes) yield* archetype.iterateWithComponents(componentTypes);
1271
1616
  }
1617
+ /**
1618
+ * Returns an array containing the data of a single component for every matching entity.
1619
+ *
1620
+ * @param componentType - The component type to retrieve
1621
+ * @returns Array of component data (one entry per matching entity)
1622
+ *
1623
+ * @example
1624
+ * const positions = query.getComponentData(Position);
1625
+ */
1272
1626
  getComponentData(componentType) {
1273
- const data = this.componentData.get(componentType);
1274
- if (!data) throw new Error(`Component type ${componentType} is not in this archetype`);
1275
- return data;
1276
- }
1277
- getOptionalComponentData(componentType) {
1278
- return this.componentData.get(componentType);
1279
- }
1280
- getCachedComponentDataSources(componentTypes) {
1281
- const cacheKey = buildCacheKey(componentTypes);
1282
- return getOrCompute(this.componentDataSourcesCache, cacheKey, () => componentTypes.map((compType) => this.getComponentDataSource(compType)));
1627
+ this.ensureNotDisposed();
1628
+ const result = [];
1629
+ for (const archetype of this.cachedArchetypes) for (const data of archetype.getComponentData(componentType)) result.push(data);
1630
+ return result;
1283
1631
  }
1284
- getComponentDataSource(compType) {
1285
- const optional = isOptionalEntityId(compType);
1286
- const actualType = optional ? compType.optional : compType;
1287
- if (getIdType(actualType) === "wildcard-relation") {
1288
- const componentId = getComponentIdFromRelationId(actualType);
1289
- return getWildcardRelationDataSource(this.componentTypes, componentId, optional);
1290
- }
1291
- return optional ? this.getOptionalComponentData(actualType) : this.getComponentData(actualType);
1632
+ /**
1633
+ * @internal Rebuilds the cached archetype list. Called automatically by the world.
1634
+ */
1635
+ updateCache() {
1636
+ if (this.isDisposed) return;
1637
+ this.cachedArchetypes = this.world.getMatchingArchetypes(this.componentTypes).filter((archetype) => matchesFilter(archetype, this.filter));
1292
1638
  }
1293
- buildComponentsForIndex(componentTypes, componentDataSources, entityIndex, entityId) {
1294
- return componentDataSources.map((dataSource, i) => buildSingleComponent(componentTypes[i], dataSource, entityIndex, entityId, (type) => this.getComponentData(type), this.dontFragmentRelations));
1639
+ /**
1640
+ * @internal Called by the world when a new archetype is created.
1641
+ */
1642
+ checkNewArchetype(archetype) {
1643
+ if (this.isDisposed) return;
1644
+ if (matchesComponentTypes(archetype, this.componentTypes) && matchesFilter(archetype, this.filter) && !this.cachedArchetypes.includes(archetype)) this.cachedArchetypes.push(archetype);
1295
1645
  }
1296
- getEntitiesWithComponents(componentTypes) {
1297
- const result = [];
1298
- this.forEachWithComponents(componentTypes, (entity, ...components) => {
1299
- result.push({
1300
- entity,
1301
- components
1302
- });
1303
- });
1304
- return result;
1646
+ /**
1647
+ * @internal Called by the world when an archetype is destroyed.
1648
+ */
1649
+ removeArchetype(archetype) {
1650
+ if (this.isDisposed) return;
1651
+ const index = this.cachedArchetypes.indexOf(archetype);
1652
+ if (index !== -1) this.cachedArchetypes.splice(index, 1);
1305
1653
  }
1306
- *iterateWithComponents(componentTypes) {
1307
- const componentDataSources = this.getCachedComponentDataSources(componentTypes);
1308
- for (let entityIndex = 0; entityIndex < this.entities.length; entityIndex++) {
1309
- const entity = this.entities[entityIndex];
1310
- yield [entity, ...this.buildComponentsForIndex(componentTypes, componentDataSources, entityIndex, entity)];
1311
- }
1654
+ /**
1655
+ * Request disposal of this query.
1656
+ * This will decrement the world's reference count for the query.
1657
+ * The query will only be fully disposed when the ref count reaches zero.
1658
+ */
1659
+ dispose() {
1660
+ this.world.releaseQuery(this);
1312
1661
  }
1313
- forEachWithComponents(componentTypes, callback) {
1314
- const componentDataSources = this.getCachedComponentDataSources(componentTypes);
1315
- for (let entityIndex = 0; entityIndex < this.entities.length; entityIndex++) {
1316
- const entity = this.entities[entityIndex];
1317
- callback(entity, ...this.buildComponentsForIndex(componentTypes, componentDataSources, entityIndex, entity));
1662
+ /**
1663
+ * @internal Fully disposes the query when the world's refCount reaches zero.
1664
+ */
1665
+ _disposeInternal(registry) {
1666
+ if (!this.isDisposed) {
1667
+ if (registry) registry.unregister(this);
1668
+ this.cachedArchetypes = [];
1669
+ this.isDisposed = true;
1318
1670
  }
1319
1671
  }
1320
- forEach(callback) {
1321
- for (let i = 0; i < this.entities.length; i++) {
1322
- const components = /* @__PURE__ */ new Map();
1323
- for (const componentType of this.componentTypes) {
1324
- const data = this.getComponentData(componentType)[i];
1325
- components.set(componentType, data === MISSING_COMPONENT ? void 0 : data);
1326
- }
1327
- callback(this.entities[i], components);
1328
- }
1672
+ /**
1673
+ * Using-with-disposals support. Calls {@link dispose} automatically.
1674
+ *
1675
+ * @example
1676
+ * using query = world.createQuery([Position]);
1677
+ * // query is released automatically when the block exits
1678
+ */
1679
+ [Symbol.dispose]() {
1680
+ this.dispose();
1329
1681
  }
1330
- hasRelationWithComponentId(componentId) {
1331
- for (const componentType of this.componentTypes) {
1332
- const detailedType = getDetailedIdType(componentType);
1333
- if (isRelationType(detailedType) && detailedType.componentId === componentId) return true;
1334
- }
1335
- for (const entityId of this.entities) {
1336
- const entityDontFragmentRelations = this.dontFragmentRelations.get(entityId);
1337
- if (entityDontFragmentRelations) for (const relationType of entityDontFragmentRelations.keys()) {
1338
- const detailedType = getDetailedIdType(relationType);
1339
- if (isRelationType(detailedType) && detailedType.componentId === componentId) return true;
1340
- }
1341
- }
1342
- return false;
1682
+ /**
1683
+ * Whether the query has been disposed and can no longer be used.
1684
+ */
1685
+ get disposed() {
1686
+ return this.isDisposed;
1343
1687
  }
1344
1688
  };
1345
1689
 
1346
1690
  //#endregion
1347
- //#region src/core/serialization.ts
1691
+ //#region src/query/registry.ts
1348
1692
  /**
1349
- * Encode an internal EntityId into a SerializedEntityId for snapshots
1693
+ * Manages the lifecycle and caching of `Query` instances.
1694
+ *
1695
+ * Responsibilities:
1696
+ * - Create / reuse cached queries keyed by component-type + filter signature.
1697
+ * - Track reference counts so queries are only disposed when truly unused.
1698
+ * - Notify registered queries when new archetypes are created or destroyed.
1699
+ *
1700
+ * The `_cacheKey` string that was previously attached directly to `Query` is now
1701
+ * kept in a private `WeakMap` so the `Query` class doesn't need to expose it.
1350
1702
  */
1351
- function encodeEntityId(id) {
1352
- const detailed = getDetailedIdType(id);
1353
- switch (detailed.type) {
1354
- case "component": {
1355
- const name = getComponentNameById(id);
1356
- if (!name) console.warn(`Component ID ${id} has no registered name, serializing as number`);
1357
- return name || id;
1358
- }
1359
- case "entity-relation": {
1360
- const componentName = getComponentNameById(detailed.componentId);
1361
- if (!componentName) console.warn(`Component ID ${detailed.componentId} in relation has no registered name`);
1362
- return {
1363
- component: componentName || detailed.componentId.toString(),
1364
- target: detailed.targetId
1365
- };
1366
- }
1367
- case "component-relation": {
1368
- const componentName = getComponentNameById(detailed.componentId);
1369
- const targetName = getComponentNameById(detailed.targetId);
1370
- if (!componentName) console.warn(`Component ID ${detailed.componentId} in relation has no registered name`);
1371
- if (!targetName) console.warn(`Target component ID ${detailed.targetId} in relation has no registered name`);
1372
- return {
1373
- component: componentName || detailed.componentId.toString(),
1374
- target: targetName || detailed.targetId
1375
- };
1376
- }
1377
- case "wildcard-relation": {
1378
- const componentName = getComponentNameById(detailed.componentId);
1379
- if (!componentName) console.warn(`Component ID ${detailed.componentId} in relation has no registered name`);
1380
- return {
1381
- component: componentName || detailed.componentId.toString(),
1382
- target: "*"
1383
- };
1703
+ var QueryRegistry = class {
1704
+ /** All live queries that should receive archetype notifications. */
1705
+ queries = /* @__PURE__ */ new Set();
1706
+ /** Cache of reusable queries keyed by a deterministic signature string. */
1707
+ cache = /* @__PURE__ */ new Map();
1708
+ /** Maps each query to its cache key without polluting the Query public API. */
1709
+ cacheKeys = /* @__PURE__ */ new WeakMap();
1710
+ /**
1711
+ * Returns (or creates) a cached query for the given component types and filter.
1712
+ * Increments the reference count on cache hits.
1713
+ *
1714
+ * @param world The world that owns this registry.
1715
+ * @param sortedTypes Normalized (sorted) component types.
1716
+ * @param key Combined cache key (`types|filter`).
1717
+ * @param filter The raw query filter (used when creating a new Query).
1718
+ */
1719
+ getOrCreate(world, sortedTypes, key, filter) {
1720
+ const cached = this.cache.get(key);
1721
+ if (cached) {
1722
+ cached.refCount++;
1723
+ return cached.query;
1384
1724
  }
1385
- default: return id;
1725
+ const query = new Query(world, sortedTypes, filter, this);
1726
+ this.cacheKeys.set(query, key);
1727
+ this.cache.set(key, {
1728
+ query,
1729
+ refCount: 1
1730
+ });
1731
+ return query;
1386
1732
  }
1387
- }
1388
- /**
1389
- * Decode a SerializedEntityId back into an internal EntityId
1390
- */
1391
- function decodeSerializedId(sid) {
1392
- if (typeof sid === "number") return sid;
1393
- if (typeof sid === "string") {
1394
- const id = getComponentIdByName(sid);
1395
- if (id === void 0) {
1396
- const num = parseInt(sid, 10);
1397
- if (!isNaN(num)) return num;
1398
- throw new Error(`Unknown component name in snapshot: ${sid}`);
1733
+ /**
1734
+ * Decrements the reference count for the given query.
1735
+ * When the count reaches zero the query is fully disposed.
1736
+ */
1737
+ release(query) {
1738
+ const key = this.cacheKeys.get(query);
1739
+ if (!key) return;
1740
+ const cached = this.cache.get(key);
1741
+ if (!cached || cached.query !== query) return;
1742
+ cached.refCount--;
1743
+ if (cached.refCount <= 0) {
1744
+ this.cache.delete(key);
1745
+ cached.query._disposeInternal(this);
1399
1746
  }
1400
- return id;
1401
1747
  }
1402
- if (typeof sid === "object" && sid !== null && typeof sid.component === "string") {
1403
- let compId = getComponentIdByName(sid.component);
1404
- if (compId === void 0) {
1405
- const num = parseInt(sid.component, 10);
1406
- if (!isNaN(num)) compId = num;
1407
- }
1408
- if (compId === void 0) throw new Error(`Unknown component name in snapshot: ${sid.component}`);
1409
- if (sid.target === "*") return relation(compId, "*");
1410
- let targetId;
1411
- if (typeof sid.target === "string") {
1412
- const tid = getComponentIdByName(sid.target);
1413
- if (tid === void 0) {
1414
- const num = parseInt(sid.target, 10);
1415
- if (!isNaN(num)) targetId = num;
1416
- else throw new Error(`Unknown target component name in snapshot: ${sid.target}`);
1417
- } else targetId = tid;
1418
- } else targetId = sid.target;
1419
- return relation(compId, targetId);
1748
+ /**
1749
+ * Registers a query so it receives future archetype notifications.
1750
+ * Called automatically by the `Query` constructor via `world._registerQuery`.
1751
+ */
1752
+ register(query) {
1753
+ this.queries.add(query);
1420
1754
  }
1421
- throw new Error(`Invalid ID in snapshot: ${JSON.stringify(sid)}`);
1422
- }
1755
+ /**
1756
+ * Removes a query from the notification list.
1757
+ * Called by `Query._disposeInternal` via `world._unregisterQuery`.
1758
+ */
1759
+ unregister(query) {
1760
+ this.queries.delete(query);
1761
+ }
1762
+ /**
1763
+ * Notifies all live queries that a new archetype has been created.
1764
+ * Queries will add the archetype to their cache if it matches.
1765
+ */
1766
+ onNewArchetype(archetype) {
1767
+ for (const query of this.queries) query.checkNewArchetype(archetype);
1768
+ }
1769
+ /**
1770
+ * Notifies all live queries that an archetype has been destroyed.
1771
+ * Queries will remove the archetype from their internal cache.
1772
+ */
1773
+ onArchetypeRemoved(archetype) {
1774
+ for (const query of this.queries) query.removeArchetype(archetype);
1775
+ }
1776
+ };
1423
1777
 
1424
1778
  //#endregion
1425
- //#region src/core/world-commands.ts
1779
+ //#region src/world/commands.ts
1426
1780
  function processCommands(entityId, currentArchetype, commands, changeset, handleExclusiveRelation) {
1427
1781
  for (const command of commands) if (command.type === "set") processSetCommand(entityId, currentArchetype, command.componentType, command.component, changeset, handleExclusiveRelation);
1428
1782
  else if (command.type === "delete") processDeleteCommand(entityId, currentArchetype, command.componentType, changeset);
@@ -1503,69 +1857,31 @@ function hasArchetypeStructuralChange(changeset, currentArchetype) {
1503
1857
  function buildFinalRegularComponentTypes(currentArchetype, changeset) {
1504
1858
  const finalRegularTypes = new Set(currentArchetype.componentTypes);
1505
1859
  for (const componentType of changeset.removes) if (!isDontFragmentRelation(componentType)) finalRegularTypes.delete(componentType);
1506
- for (const componentType of changeset.adds.keys()) if (!isDontFragmentRelation(componentType)) finalRegularTypes.add(componentType);
1860
+ for (const [componentType] of changeset.adds) if (!isDontFragmentRelation(componentType)) finalRegularTypes.add(componentType);
1507
1861
  return Array.from(finalRegularTypes);
1508
1862
  }
1509
- function applyChangeset(ctx, entityId, currentArchetype, changeset, entityToArchetype) {
1510
- const removedComponents = /* @__PURE__ */ new Map();
1511
- pruneMissingRemovals(changeset, currentArchetype, entityId);
1512
- if (hasArchetypeStructuralChange(changeset, currentArchetype)) return {
1513
- removedComponents,
1514
- newArchetype: moveEntityToNewArchetype(ctx, entityId, currentArchetype, buildFinalRegularComponentTypes(currentArchetype, changeset), changeset, removedComponents, entityToArchetype)
1515
- };
1516
- updateEntityInSameArchetype(ctx, entityId, currentArchetype, changeset, removedComponents);
1517
- return {
1518
- removedComponents,
1519
- newArchetype: currentArchetype
1520
- };
1521
- }
1522
- /**
1523
- * Optimized variant of applyChangeset for when no lifecycle hooks are registered.
1524
- * Skips creating the removedComponents map, reducing allocations in the hot path.
1525
- */
1526
- function applyChangesetNoHooks(ctx, entityId, currentArchetype, changeset, entityToArchetype) {
1863
+ function applyChangeset(ctx, entityId, currentArchetype, changeset, entityToArchetype, removedComponents) {
1527
1864
  pruneMissingRemovals(changeset, currentArchetype, entityId);
1528
- if (hasArchetypeStructuralChange(changeset, currentArchetype)) return moveEntityToNewArchetypeNoHooks(ctx, entityId, currentArchetype, buildFinalRegularComponentTypes(currentArchetype, changeset), changeset, entityToArchetype);
1529
- updateEntityInSameArchetypeNoHooks(ctx, entityId, currentArchetype, changeset);
1530
- return currentArchetype;
1531
- }
1532
- function moveEntityToNewArchetype(ctx, entityId, currentArchetype, finalComponentTypes, changeset, removedComponents, entityToArchetype) {
1533
- const newArchetype = ctx.ensureArchetype(finalComponentTypes);
1534
- const currentComponents = currentArchetype.removeEntity(entityId);
1535
- for (const componentType of changeset.removes) removedComponents.set(componentType, currentComponents.get(componentType));
1536
- newArchetype.addEntity(entityId, changeset.applyTo(currentComponents));
1537
- entityToArchetype.set(entityId, newArchetype);
1538
- return newArchetype;
1539
- }
1540
- function updateEntityInSameArchetype(ctx, entityId, currentArchetype, changeset, removedComponents) {
1541
- applyDontFragmentChanges(ctx.dontFragmentRelations, entityId, changeset, removedComponents);
1865
+ if (hasArchetypeStructuralChange(changeset, currentArchetype)) {
1866
+ const finalRegularTypes = buildFinalRegularComponentTypes(currentArchetype, changeset);
1867
+ const newArchetype = ctx.ensureArchetype(finalRegularTypes);
1868
+ const currentComponents = currentArchetype.removeEntity(entityId);
1869
+ if (removedComponents !== null) for (const componentType of changeset.removes) removedComponents.set(componentType, currentComponents.get(componentType));
1870
+ newArchetype.addEntity(entityId, changeset.applyTo(currentComponents));
1871
+ entityToArchetype.set(entityId, newArchetype);
1872
+ return newArchetype;
1873
+ }
1874
+ if (removedComponents !== null) applyDontFragmentChanges(ctx.dontFragmentStore, entityId, changeset, removedComponents);
1875
+ else applyDontFragmentChangesNoHooks(ctx.dontFragmentStore, entityId, changeset);
1542
1876
  for (const [componentType, component$1] of changeset.adds) {
1543
1877
  if (isDontFragmentRelation(componentType)) continue;
1544
1878
  currentArchetype.set(entityId, componentType, component$1);
1545
1879
  }
1880
+ return currentArchetype;
1546
1881
  }
1547
1882
  /**
1548
- * No-hooks variant: moves entity to new archetype without collecting removed component data.
1549
- * Only called from applyChangesetNoHooks when no lifecycle hooks are registered.
1550
- */
1551
- function moveEntityToNewArchetypeNoHooks(ctx, entityId, currentArchetype, finalComponentTypes, changeset, entityToArchetype) {
1552
- const newArchetype = ctx.ensureArchetype(finalComponentTypes);
1553
- const currentComponents = currentArchetype.removeEntity(entityId);
1554
- newArchetype.addEntity(entityId, changeset.applyTo(currentComponents));
1555
- entityToArchetype.set(entityId, newArchetype);
1556
- return newArchetype;
1557
- }
1558
- /**
1559
- * No-hooks variant: updates entity in same archetype without tracking removed component data.
1560
- * Only called from applyChangesetNoHooks when no lifecycle hooks are registered.
1883
+ * No-hooks variant of applyDontFragmentChanges that skips tracking removed component data.
1561
1884
  */
1562
- function updateEntityInSameArchetypeNoHooks(ctx, entityId, currentArchetype, changeset) {
1563
- applyDontFragmentChangesNoHooks(ctx.dontFragmentRelations, entityId, changeset);
1564
- for (const [componentType, component$1] of changeset.adds) {
1565
- if (isDontFragmentRelation(componentType)) continue;
1566
- currentArchetype.set(entityId, componentType, component$1);
1567
- }
1568
- }
1569
1885
  function applyDontFragmentChanges(dontFragmentRelations, entityId, changeset, removedComponents) {
1570
1886
  let entityRelations = dontFragmentRelations.get(entityId);
1571
1887
  for (const componentType of changeset.removes) if (isDontFragmentRelation(componentType)) {
@@ -1586,9 +1902,6 @@ function applyDontFragmentChanges(dontFragmentRelations, entityId, changeset, re
1586
1902
  }
1587
1903
  if (entityRelations && entityRelations.size === 0) dontFragmentRelations.delete(entityId);
1588
1904
  }
1589
- /**
1590
- * No-hooks variant of applyDontFragmentChanges that skips tracking removed component data.
1591
- */
1592
1905
  function applyDontFragmentChangesNoHooks(dontFragmentRelations, entityId, changeset) {
1593
1906
  let entityRelations = dontFragmentRelations.get(entityId);
1594
1907
  for (const componentType of changeset.removes) if (isDontFragmentRelation(componentType)) {
@@ -1617,7 +1930,20 @@ function filterRegularComponentTypes(componentTypes) {
1617
1930
  }
1618
1931
 
1619
1932
  //#endregion
1620
- //#region src/core/world-hooks.ts
1933
+ //#region src/world/hooks.ts
1934
+ /**
1935
+ * Unified hook invocation: prefers entry.callback (callback style) over hook.on_* (object style).
1936
+ */
1937
+ function invokeHook(entry, event, entityId, components) {
1938
+ if (entry.callback) {
1939
+ entry.callback(event, entityId, ...components);
1940
+ return;
1941
+ }
1942
+ const hook = entry.hook;
1943
+ if (event === "init") hook.on_init?.(entityId, ...components);
1944
+ else if (event === "set") hook.on_set?.(entityId, ...components);
1945
+ else hook.on_remove?.(entityId, ...components);
1946
+ }
1621
1947
  /**
1622
1948
  * Check if a component change matches a hook component type.
1623
1949
  * Handles wildcard-relation matching: if hookComponent is a wildcard relation (e.g., relation(A, "*")),
@@ -1647,8 +1973,6 @@ function findMatchingComponent(changes, hookComponent) {
1647
1973
  for (const [changedComponent, value] of changes.entries()) if (componentMatchesHookType(changedComponent, hookComponent)) return [changedComponent, value];
1648
1974
  }
1649
1975
  function triggerLifecycleHooks(ctx, entityId, addedComponents, removedComponents, oldArchetype, newArchetype) {
1650
- invokeHooksForComponents(ctx.hooks, entityId, addedComponents, "on_set");
1651
- invokeHooksForComponents(ctx.hooks, entityId, removedComponents, "on_remove");
1652
1976
  triggerMultiComponentHooks(ctx, entityId, addedComponents, removedComponents, oldArchetype, newArchetype);
1653
1977
  }
1654
1978
  /**
@@ -1656,42 +1980,31 @@ function triggerLifecycleHooks(ctx, entityId, addedComponents, removedComponents
1656
1980
  * This avoids unnecessary archetype lookups and on_set checks since the entity
1657
1981
  * is being completely removed.
1658
1982
  */
1659
- function triggerRemoveHooksForEntityDeletion(ctx, entityId, removedComponents, oldArchetype) {
1983
+ function triggerRemoveHooksForEntityDeletion(entityId, removedComponents, oldArchetype) {
1660
1984
  if (removedComponents.size === 0) return;
1661
- invokeHooksForComponents(ctx.hooks, entityId, removedComponents, "on_remove");
1662
1985
  for (const entry of oldArchetype.matchingMultiHooks) {
1663
- const { hook, requiredComponents, componentTypes } = entry;
1664
- if (!hook.on_remove) continue;
1986
+ const { requiredComponents, componentTypes } = entry;
1987
+ if (!entry.callback && !entry.hook.on_remove) continue;
1665
1988
  if (!requiredComponents.some((c) => anyComponentMatches(removedComponents, c))) continue;
1666
1989
  if (!requiredComponents.every((c) => anyComponentMatches(removedComponents, c))) continue;
1667
- const components = collectComponentsFromRemoved(componentTypes, removedComponents);
1668
- hook.on_remove(entityId, ...components);
1669
- }
1670
- }
1671
- function invokeHooksForComponents(hooks, entityId, components, hookType) {
1672
- for (const [componentType, component$1] of components) {
1673
- const directHooks = hooks.get(componentType);
1674
- if (directHooks) for (const hook of directHooks) hook[hookType]?.(entityId, componentType, component$1);
1675
- const componentId = getComponentIdFromRelationId(componentType);
1676
- if (componentId !== void 0) {
1677
- const wildcardHooks = hooks.get(relation(componentId, "*"));
1678
- if (wildcardHooks) for (const hook of wildcardHooks) hook[hookType]?.(entityId, componentType, component$1);
1679
- }
1990
+ invokeHook(entry, "remove", entityId, collectComponentsFromRemoved(componentTypes, removedComponents));
1680
1991
  }
1681
1992
  }
1682
1993
  function triggerMultiComponentHooks(ctx, entityId, addedComponents, removedComponents, oldArchetype, newArchetype) {
1683
1994
  for (const entry of newArchetype.matchingMultiHooks) {
1684
- const { hook, requiredComponents, optionalComponents, componentTypes } = entry;
1685
- if (!hook.on_set) continue;
1995
+ const { requiredComponents, optionalComponents, componentTypes } = entry;
1996
+ if (!entry.callback && !entry.hook.on_set) continue;
1686
1997
  const anyRequiredAdded = requiredComponents.some((c) => anyComponentMatches(addedComponents, c));
1687
1998
  const anyOptionalAdded = optionalComponents.some((c) => anyComponentMatches(addedComponents, c));
1688
1999
  const anyOptionalRemoved = optionalComponents.some((c) => anyComponentMatches(removedComponents, c));
1689
- if ((anyRequiredAdded || anyOptionalAdded || anyOptionalRemoved) && entityHasAllComponents(ctx, entityId, requiredComponents)) hook.on_set(entityId, ...collectMultiHookComponents(ctx, entityId, componentTypes));
2000
+ if (!oldArchetype.matchingMultiHooks.has(entry) || (anyRequiredAdded || anyOptionalAdded || anyOptionalRemoved) && entityHasAllComponents(ctx, entityId, requiredComponents)) invokeHook(entry, "set", entityId, collectMultiHookComponents(ctx, entityId, componentTypes));
1690
2001
  }
1691
- if (removedComponents.size > 0) for (const entry of oldArchetype.matchingMultiHooks) {
1692
- const { hook, requiredComponents, componentTypes } = entry;
1693
- if (!hook.on_remove) continue;
1694
- if (requiredComponents.some((c) => anyComponentMatches(removedComponents, c)) && entityHadAllComponentsBefore(ctx, entityId, requiredComponents, removedComponents) && !entityHasAllComponents(ctx, entityId, requiredComponents)) hook.on_remove(entityId, ...collectMultiHookComponentsWithRemoved(ctx, entityId, componentTypes, removedComponents));
2002
+ for (const entry of oldArchetype.matchingMultiHooks) {
2003
+ const { requiredComponents, componentTypes } = entry;
2004
+ if (!entry.callback && !entry.hook.on_remove) continue;
2005
+ const lostRequiredMatch = requiredComponents.some((c) => anyComponentMatches(removedComponents, c)) && entityHadAllComponentsBefore(ctx, entityId, requiredComponents, removedComponents) && !entityHasAllComponents(ctx, entityId, requiredComponents);
2006
+ const exitedMatchingSet = !newArchetype.matchingMultiHooks.has(entry);
2007
+ if (lostRequiredMatch || exitedMatchingSet) invokeHook(entry, "remove", entityId, collectMultiHookComponentsWithRemoved(ctx, entityId, componentTypes, removedComponents));
1695
2008
  }
1696
2009
  }
1697
2010
  function entityHasAllComponents(ctx, entityId, requiredComponents) {
@@ -1785,6 +2098,7 @@ function collectWildcardFromRemoved(wildcardId, removedComponents) {
1785
2098
 
1786
2099
  //#endregion
1787
2100
  //#region src/utils/multi-map.ts
2101
+ const _MISSING = Symbol("missing");
1788
2102
  var MultiMap = class {
1789
2103
  map = /* @__PURE__ */ new Map();
1790
2104
  _valueCount = 0;
@@ -1797,10 +2111,10 @@ var MultiMap = class {
1797
2111
  hasKey(key) {
1798
2112
  return this.map.has(key);
1799
2113
  }
1800
- has(key, value) {
2114
+ has(key, value = _MISSING) {
1801
2115
  const set = this.map.get(key);
1802
2116
  if (!set) return false;
1803
- if (arguments.length === 1) return true;
2117
+ if (value === _MISSING) return true;
1804
2118
  return set.has(value);
1805
2119
  }
1806
2120
  add(key, value) {
@@ -1853,7 +2167,7 @@ var MultiMap = class {
1853
2167
  };
1854
2168
 
1855
2169
  //#endregion
1856
- //#region src/core/world-references.ts
2170
+ //#region src/world/references.ts
1857
2171
  function trackEntityReference(entityReferences, sourceEntityId, componentType, targetEntityId) {
1858
2172
  if (!entityReferences.has(targetEntityId)) entityReferences.set(targetEntityId, new MultiMap());
1859
2173
  entityReferences.get(targetEntityId).add(sourceEntityId, componentType);
@@ -1870,7 +2184,155 @@ function getEntityReferences(entityReferences, targetEntityId) {
1870
2184
  }
1871
2185
 
1872
2186
  //#endregion
1873
- //#region src/core/world.ts
2187
+ //#region src/storage/serialization.ts
2188
+ /**
2189
+ * Encode an internal EntityId into a SerializedEntityId for snapshots
2190
+ */
2191
+ function encodeEntityId(id) {
2192
+ const detailed = getDetailedIdType(id);
2193
+ switch (detailed.type) {
2194
+ case "component": {
2195
+ const name = getComponentNameById(id);
2196
+ if (!name) console.warn(`Component ID ${id} has no registered name, serializing as number`);
2197
+ return name || id;
2198
+ }
2199
+ case "entity-relation": {
2200
+ const componentName = getComponentNameById(detailed.componentId);
2201
+ if (!componentName) console.warn(`Component ID ${detailed.componentId} in relation has no registered name`);
2202
+ return {
2203
+ component: componentName || detailed.componentId.toString(),
2204
+ target: detailed.targetId
2205
+ };
2206
+ }
2207
+ case "component-relation": {
2208
+ const componentName = getComponentNameById(detailed.componentId);
2209
+ const targetName = getComponentNameById(detailed.targetId);
2210
+ if (!componentName) console.warn(`Component ID ${detailed.componentId} in relation has no registered name`);
2211
+ if (!targetName) console.warn(`Target component ID ${detailed.targetId} in relation has no registered name`);
2212
+ return {
2213
+ component: componentName || detailed.componentId.toString(),
2214
+ target: targetName || detailed.targetId
2215
+ };
2216
+ }
2217
+ case "wildcard-relation": {
2218
+ const componentName = getComponentNameById(detailed.componentId);
2219
+ if (!componentName) console.warn(`Component ID ${detailed.componentId} in relation has no registered name`);
2220
+ return {
2221
+ component: componentName || detailed.componentId.toString(),
2222
+ target: "*"
2223
+ };
2224
+ }
2225
+ default: return id;
2226
+ }
2227
+ }
2228
+ /**
2229
+ * Decode a SerializedEntityId back into an internal EntityId
2230
+ */
2231
+ function decodeSerializedId(sid) {
2232
+ if (typeof sid === "number") return sid;
2233
+ if (typeof sid === "string") {
2234
+ const id = getComponentIdByName(sid);
2235
+ if (id === void 0) {
2236
+ const num = parseInt(sid, 10);
2237
+ if (!isNaN(num)) return num;
2238
+ throw new Error(`Unknown component name in snapshot: ${sid}`);
2239
+ }
2240
+ return id;
2241
+ }
2242
+ if (typeof sid === "object" && sid !== null && typeof sid.component === "string") {
2243
+ let compId = getComponentIdByName(sid.component);
2244
+ if (compId === void 0) {
2245
+ const num = parseInt(sid.component, 10);
2246
+ if (!isNaN(num)) compId = num;
2247
+ }
2248
+ if (compId === void 0) throw new Error(`Unknown component name in snapshot: ${sid.component}`);
2249
+ if (sid.target === "*") return relation(compId, "*");
2250
+ let targetId;
2251
+ if (typeof sid.target === "string") {
2252
+ const tid = getComponentIdByName(sid.target);
2253
+ if (tid === void 0) {
2254
+ const num = parseInt(sid.target, 10);
2255
+ if (!isNaN(num)) targetId = num;
2256
+ else throw new Error(`Unknown target component name in snapshot: ${sid.target}`);
2257
+ } else targetId = tid;
2258
+ } else targetId = sid.target;
2259
+ return relation(compId, targetId);
2260
+ }
2261
+ throw new Error(`Invalid ID in snapshot: ${JSON.stringify(sid)}`);
2262
+ }
2263
+
2264
+ //#endregion
2265
+ //#region src/world/serialization.ts
2266
+ /**
2267
+ * Serializes the full world state to a plain JS object suitable for JSON encoding.
2268
+ */
2269
+ function serializeWorld(archetypes, componentEntities, entityIdManager) {
2270
+ const entities = [];
2271
+ for (const archetype of archetypes) {
2272
+ const dumpedEntities = archetype.dump();
2273
+ for (const { entity, components } of dumpedEntities) entities.push({
2274
+ id: encodeEntityId(entity),
2275
+ components: Array.from(components.entries()).map(([rawType, value]) => ({
2276
+ type: encodeEntityId(rawType),
2277
+ value: value === MISSING_COMPONENT ? void 0 : value
2278
+ }))
2279
+ });
2280
+ }
2281
+ const componentEntitiesArr = [];
2282
+ for (const [entityId, components] of componentEntities.entries()) componentEntitiesArr.push({
2283
+ id: encodeEntityId(entityId),
2284
+ components: Array.from(components.entries()).map(([rawType, value]) => ({
2285
+ type: encodeEntityId(rawType),
2286
+ value: value === MISSING_COMPONENT ? void 0 : value
2287
+ }))
2288
+ });
2289
+ return {
2290
+ version: 1,
2291
+ entityManager: entityIdManager.serializeState(),
2292
+ entities,
2293
+ componentEntities: componentEntitiesArr
2294
+ };
2295
+ }
2296
+ /**
2297
+ * Restores world state from a snapshot into the provided context.
2298
+ * Intended to be called from `World`'s constructor.
2299
+ */
2300
+ function deserializeWorld(ctx, snapshot) {
2301
+ if (snapshot.entityManager) ctx.entityIdManager.deserializeState(snapshot.entityManager);
2302
+ if (Array.isArray(snapshot.componentEntities)) for (const entry of snapshot.componentEntities) {
2303
+ const entityId = decodeSerializedId(entry.id);
2304
+ if (!ctx.componentEntities.exists(entityId)) continue;
2305
+ const componentsArray = entry.components || [];
2306
+ const componentMap = /* @__PURE__ */ new Map();
2307
+ for (const componentEntry of componentsArray) {
2308
+ const componentType = decodeSerializedId(componentEntry.type);
2309
+ componentMap.set(componentType, componentEntry.value);
2310
+ }
2311
+ ctx.componentEntities.initFromSnapshot(entityId, componentMap);
2312
+ }
2313
+ if (Array.isArray(snapshot.entities)) for (const entry of snapshot.entities) {
2314
+ const entityId = decodeSerializedId(entry.id);
2315
+ const componentsArray = entry.components || [];
2316
+ const componentMap = /* @__PURE__ */ new Map();
2317
+ const componentTypes = [];
2318
+ for (const componentEntry of componentsArray) {
2319
+ const componentType = decodeSerializedId(componentEntry.type);
2320
+ componentMap.set(componentType, componentEntry.value);
2321
+ componentTypes.push(componentType);
2322
+ }
2323
+ const archetype = ctx.ensureArchetype(componentTypes);
2324
+ archetype.addEntity(entityId, componentMap);
2325
+ ctx.setEntityToArchetype(entityId, archetype);
2326
+ for (const compType of componentTypes) {
2327
+ const detailedType = getDetailedIdType(compType);
2328
+ if (detailedType.type === "entity-relation") trackEntityReference(ctx.entityReferences, entityId, compType, detailedType.targetId);
2329
+ else if (detailedType.type === "entity") trackEntityReference(ctx.entityReferences, entityId, compType, compType);
2330
+ }
2331
+ }
2332
+ }
2333
+
2334
+ //#endregion
2335
+ //#region src/world/world.ts
1874
2336
  /**
1875
2337
  * World class for ECS architecture
1876
2338
  * Manages entities and components
@@ -1882,64 +2344,37 @@ var World = class {
1882
2344
  entityToArchetype = /* @__PURE__ */ new Map();
1883
2345
  archetypesByComponent = /* @__PURE__ */ new Map();
1884
2346
  entityReferences = /* @__PURE__ */ new Map();
1885
- dontFragmentRelations = /* @__PURE__ */ new Map();
1886
- componentEntityComponents = /* @__PURE__ */ new Map();
1887
- relationEntityIdsByTarget = /* @__PURE__ */ new Map();
1888
- queries = [];
1889
- queryCache = /* @__PURE__ */ new Map();
1890
- legacyHooks = /* @__PURE__ */ new Map();
2347
+ /** Reverse index: entity ID set of archetypes whose componentTypes include that entity ID */
2348
+ entityToReferencingArchetypes = /* @__PURE__ */ new Map();
2349
+ /** DontFragment relation storage, shared with all Archetype instances */
2350
+ dontFragmentStore = new DontFragmentStoreImpl();
2351
+ /** Component entity (singleton) storage */
2352
+ componentEntities = new ComponentEntityStore();
2353
+ queryRegistry = new QueryRegistry();
1891
2354
  hooks = /* @__PURE__ */ new Set();
1892
2355
  commandBuffer = new CommandBuffer((entityId, commands) => this.executeEntityCommands(entityId, commands));
1893
2356
  _changeset = new ComponentChangeset();
2357
+ _removeChangeset = new ComponentChangeset();
1894
2358
  /** Cached command processor context to avoid per-entity object allocation */
1895
2359
  _commandCtx = {
1896
- dontFragmentRelations: this.dontFragmentRelations,
2360
+ dontFragmentStore: this.dontFragmentStore,
1897
2361
  ensureArchetype: (ct) => this.ensureArchetype(ct)
1898
2362
  };
1899
2363
  /** Cached hooks context to avoid per-entity object allocation */
1900
2364
  _hooksCtx = {
1901
- hooks: this.legacyHooks,
1902
2365
  multiHooks: this.hooks,
1903
2366
  has: (eid, ct) => this.has(eid, ct),
1904
2367
  get: (eid, ct) => this.get(eid, ct),
1905
2368
  getOptional: (eid, ct) => this.getOptional(eid, ct)
1906
2369
  };
1907
2370
  constructor(snapshot) {
1908
- if (snapshot && typeof snapshot === "object") this.deserializeSnapshot(snapshot);
1909
- }
1910
- deserializeSnapshot(snapshot) {
1911
- if (snapshot.entityManager) this.entityIdManager.deserializeState(snapshot.entityManager);
1912
- if (Array.isArray(snapshot.componentEntities)) for (const entry of snapshot.componentEntities) {
1913
- const entityId = decodeSerializedId(entry.id);
1914
- if (!this.isComponentEntityId(entityId)) continue;
1915
- const componentsArray = entry.components || [];
1916
- const componentMap = /* @__PURE__ */ new Map();
1917
- for (const componentEntry of componentsArray) {
1918
- const componentType = decodeSerializedId(componentEntry.type);
1919
- componentMap.set(componentType, componentEntry.value);
1920
- }
1921
- this.componentEntityComponents.set(entityId, componentMap);
1922
- this.registerRelationEntityId(entityId);
1923
- }
1924
- if (Array.isArray(snapshot.entities)) for (const entry of snapshot.entities) {
1925
- const entityId = decodeSerializedId(entry.id);
1926
- const componentsArray = entry.components || [];
1927
- const componentMap = /* @__PURE__ */ new Map();
1928
- const componentTypes = [];
1929
- for (const componentEntry of componentsArray) {
1930
- const componentType = decodeSerializedId(componentEntry.type);
1931
- componentMap.set(componentType, componentEntry.value);
1932
- componentTypes.push(componentType);
1933
- }
1934
- const archetype = this.ensureArchetype(componentTypes);
1935
- archetype.addEntity(entityId, componentMap);
1936
- this.entityToArchetype.set(entityId, archetype);
1937
- for (const compType of componentTypes) {
1938
- const detailedType = getDetailedIdType(compType);
1939
- if (detailedType.type === "entity-relation") trackEntityReference(this.entityReferences, entityId, compType, detailedType.targetId);
1940
- else if (detailedType.type === "entity") trackEntityReference(this.entityReferences, entityId, compType, compType);
1941
- }
1942
- }
2371
+ if (snapshot && typeof snapshot === "object") deserializeWorld({
2372
+ entityIdManager: this.entityIdManager,
2373
+ componentEntities: this.componentEntities,
2374
+ entityReferences: this.entityReferences,
2375
+ ensureArchetype: (ct) => this.ensureArchetype(ct),
2376
+ setEntityToArchetype: (eid, arch) => this.entityToArchetype.set(eid, arch)
2377
+ }, snapshot);
1943
2378
  }
1944
2379
  createArchetypeSignature(componentTypes) {
1945
2380
  return componentTypes.join(",");
@@ -1963,51 +2398,34 @@ var World = class {
1963
2398
  this.entityToArchetype.set(entityId, emptyArchetype);
1964
2399
  return entityId;
1965
2400
  }
1966
- isComponentEntityId(entityId) {
1967
- const detailed = getDetailedIdType(entityId);
1968
- return detailed.type !== "entity" && detailed.type !== "invalid";
1969
- }
1970
- registerRelationEntityId(entityId) {
1971
- const detailed = getDetailedIdType(entityId);
1972
- if (detailed.type !== "entity-relation") return;
1973
- const targetId = detailed.targetId;
1974
- if (targetId === void 0) return;
1975
- const existing = this.relationEntityIdsByTarget.get(targetId);
1976
- if (existing) {
1977
- existing.add(entityId);
1978
- return;
1979
- }
1980
- this.relationEntityIdsByTarget.set(targetId, new Set([entityId]));
1981
- }
1982
- unregisterRelationEntityId(entityId) {
1983
- const detailed = getDetailedIdType(entityId);
1984
- if (detailed.type !== "entity-relation") return;
1985
- const targetId = detailed.targetId;
1986
- if (targetId === void 0) return;
1987
- const existing = this.relationEntityIdsByTarget.get(targetId);
1988
- if (!existing) return;
1989
- existing.delete(entityId);
1990
- if (existing.size === 0) this.relationEntityIdsByTarget.delete(targetId);
1991
- }
1992
- getComponentEntityComponents(entityId, create) {
1993
- let data = this.componentEntityComponents.get(entityId);
1994
- if (!data && create) {
1995
- data = /* @__PURE__ */ new Map();
1996
- this.componentEntityComponents.set(entityId, data);
1997
- this.registerRelationEntityId(entityId);
1998
- }
1999
- return data;
2000
- }
2001
- clearComponentEntityComponents(entityId) {
2002
- if (this.componentEntityComponents.delete(entityId)) this.unregisterRelationEntityId(entityId);
2401
+ /**
2402
+ * Semantic alias for `new()` to avoid confusion with the `new` keyword.
2403
+ * Creates a new entity with an empty component set.
2404
+ *
2405
+ * @example
2406
+ * const entity = world.create<MyComponent>();
2407
+ */
2408
+ create() {
2409
+ return this.new();
2003
2410
  }
2004
- cleanupComponentEntitiesReferencingEntity(targetId) {
2005
- const relationEntities = this.relationEntityIdsByTarget.get(targetId);
2006
- if (!relationEntities) return;
2007
- for (const relationEntityId of relationEntities) this.componentEntityComponents.delete(relationEntityId);
2008
- this.relationEntityIdsByTarget.delete(targetId);
2411
+ /** Fast path: destroy an entity that is not referenced by any other entity, skipping BFS */
2412
+ destroySingleEntity(entityId) {
2413
+ const archetype = this.entityToArchetype.get(entityId);
2414
+ if (!archetype) return;
2415
+ for (const [sourceEntityId, componentType] of getEntityReferences(this.entityReferences, entityId)) if (this.entityToArchetype.has(sourceEntityId)) this.removeComponentImmediate(sourceEntityId, componentType, entityId);
2416
+ this.entityReferences.delete(entityId);
2417
+ const removedComponents = archetype.removeEntity(entityId);
2418
+ this.entityToArchetype.delete(entityId);
2419
+ triggerRemoveHooksForEntityDeletion(entityId, removedComponents, archetype);
2420
+ this.cleanupArchetypesReferencingEntity(entityId);
2421
+ this.entityIdManager.deallocate(entityId);
2422
+ this.componentEntities.cleanupReferencesTo(entityId);
2009
2423
  }
2010
2424
  destroyEntityImmediate(entityId) {
2425
+ if (!this.entityReferences.has(entityId)) {
2426
+ this.destroySingleEntity(entityId);
2427
+ return;
2428
+ }
2011
2429
  const queue = [entityId];
2012
2430
  const visited = /* @__PURE__ */ new Set();
2013
2431
  let queueIndex = 0;
@@ -2026,25 +2444,33 @@ var World = class {
2026
2444
  this.entityReferences.delete(cur);
2027
2445
  const removedComponents = archetype.removeEntity(cur);
2028
2446
  this.entityToArchetype.delete(cur);
2029
- triggerRemoveHooksForEntityDeletion(this.createHooksContext(), cur, removedComponents, archetype);
2447
+ triggerRemoveHooksForEntityDeletion(cur, removedComponents, archetype);
2030
2448
  this.cleanupArchetypesReferencingEntity(cur);
2031
2449
  this.entityIdManager.deallocate(cur);
2032
- this.cleanupComponentEntitiesReferencingEntity(cur);
2450
+ this.componentEntities.cleanupReferencesTo(cur);
2033
2451
  }
2034
2452
  }
2035
2453
  /**
2036
- * Checks if an entity exists in the world.
2454
+ * Checks if an **entity** (not a component) exists in the world.
2455
+ *
2456
+ * This is specifically for checking entity liveness — whether the given entity ID
2457
+ * is currently alive in the world. For checking if a component is present on an
2458
+ * entity, use {@link has} instead.
2037
2459
  *
2038
2460
  * @param entityId - The entity identifier to check
2039
2461
  * @returns `true` if the entity exists, `false` otherwise
2040
2462
  *
2041
2463
  * @example
2464
+ * // Check if an entity is alive
2042
2465
  * if (world.exists(entityId)) {
2043
2466
  * console.log("Entity exists");
2044
2467
  * }
2468
+ *
2469
+ * // To check for a component, use has() instead:
2470
+ * if (world.has(entity, Position)) { ... }
2045
2471
  */
2046
2472
  exists(entityId) {
2047
- if (this.isComponentEntityId(entityId)) return true;
2473
+ if (this.componentEntities.exists(entityId)) return true;
2048
2474
  return this.entityToArchetype.has(entityId);
2049
2475
  }
2050
2476
  assertEntityExists(entityId, label) {
@@ -2099,18 +2525,6 @@ var World = class {
2099
2525
  componentType
2100
2526
  };
2101
2527
  }
2102
- getComponentEntityWildcardRelations(entityId, wildcardComponentType) {
2103
- const componentId = getComponentIdFromRelationId(wildcardComponentType);
2104
- const data = this.componentEntityComponents.get(entityId);
2105
- if (componentId === void 0 || !data) return [];
2106
- const relations = [];
2107
- for (const [key, value] of data.entries()) {
2108
- if (getComponentIdFromRelationId(key) !== componentId) continue;
2109
- const detailed = getDetailedIdType(key);
2110
- if (detailed.type === "entity-relation" || detailed.type === "component-relation") relations.push([detailed.targetId, value]);
2111
- }
2112
- return relations;
2113
- }
2114
2528
  set(entityId, componentTypeOrComponent, maybeComponent) {
2115
2529
  const { entityId: targetEntityId, componentType, component: component$1 } = this.resolveSetOperation(entityId, componentTypeOrComponent, maybeComponent);
2116
2530
  this.commandBuffer.set(targetEntityId, componentType, component$1);
@@ -2136,50 +2550,44 @@ var World = class {
2136
2550
  has(entityId, componentType) {
2137
2551
  if (componentType === void 0) {
2138
2552
  const componentId = entityId;
2139
- return this.componentEntityComponents.get(componentId)?.has(componentId) ?? false;
2553
+ return this.componentEntities.hasSingleton(componentId);
2140
2554
  }
2141
- if (this.isComponentEntityId(entityId)) {
2555
+ if (this.componentEntities.exists(entityId)) {
2142
2556
  if (isWildcardRelationId(componentType)) {
2143
2557
  const componentId = getComponentIdFromRelationId(componentType);
2144
2558
  if (componentId === void 0) return false;
2145
- const data = this.componentEntityComponents.get(entityId);
2146
- if (!data) return false;
2147
- return hasWildcardRelation(data, componentId);
2559
+ return this.componentEntities.hasWildcard(entityId, componentId);
2148
2560
  }
2149
- return this.componentEntityComponents.get(entityId)?.has(componentType) ?? false;
2561
+ return this.componentEntities.has(entityId, componentType);
2150
2562
  }
2151
2563
  const archetype = this.entityToArchetype.get(entityId);
2152
2564
  if (!archetype) return false;
2153
2565
  if (archetype.componentTypeSet.has(componentType)) return true;
2154
- if (isDontFragmentRelation(componentType)) return this.dontFragmentRelations.get(entityId)?.has(componentType) ?? false;
2566
+ if (isDontFragmentRelation(componentType)) return this.dontFragmentStore.get(entityId)?.has(componentType) ?? false;
2155
2567
  return false;
2156
2568
  }
2157
2569
  get(entityId, componentType = entityId) {
2158
- if (this.isComponentEntityId(entityId)) {
2159
- if (isWildcardRelationId(componentType)) return this.getComponentEntityWildcardRelations(entityId, componentType);
2160
- const data = this.componentEntityComponents.get(entityId);
2161
- if (!data || !data.has(componentType)) throw new Error(`Entity ${entityId} does not have component ${componentType}. Use has() to check component existence before calling get().`);
2162
- return data.get(componentType);
2570
+ if (this.componentEntities.exists(entityId)) {
2571
+ if (isWildcardRelationId(componentType)) return this.componentEntities.getWildcard(entityId, componentType);
2572
+ return this.componentEntities.get(entityId, componentType);
2163
2573
  }
2164
2574
  const archetype = this.entityToArchetype.get(entityId);
2165
2575
  if (!archetype) throw new Error(`Entity ${entityId} does not exist`);
2166
2576
  if (componentType >= 0 || componentType % RELATION_SHIFT !== 0) {
2167
2577
  const inArchetype = archetype.componentTypeSet.has(componentType);
2168
2578
  const hasDontFragment = isDontFragmentRelation(componentType);
2169
- if (!(inArchetype || hasDontFragment && this.dontFragmentRelations.get(entityId)?.has(componentType))) throw new Error(`Entity ${entityId} does not have component ${componentType}. Use has() to check component existence before calling get().`);
2579
+ if (!(inArchetype || hasDontFragment && this.dontFragmentStore.get(entityId)?.has(componentType))) throw new Error(`Entity ${entityId} does not have component ${componentType}. Use has() to check component existence before calling get().`);
2170
2580
  }
2171
2581
  return archetype.get(entityId, componentType);
2172
2582
  }
2173
2583
  getOptional(entityId, componentType = entityId) {
2174
- if (this.isComponentEntityId(entityId)) {
2584
+ if (this.componentEntities.exists(entityId)) {
2175
2585
  if (isWildcardRelationId(componentType)) {
2176
- const relations = this.getComponentEntityWildcardRelations(entityId, componentType);
2586
+ const relations = this.componentEntities.getWildcard(entityId, componentType);
2177
2587
  if (relations.length === 0) return void 0;
2178
2588
  return { value: relations };
2179
2589
  }
2180
- const data = this.componentEntityComponents.get(entityId);
2181
- if (!data || !data.has(componentType)) return void 0;
2182
- return { value: data.get(componentType) };
2590
+ return this.componentEntities.getOptional(entityId, componentType);
2183
2591
  }
2184
2592
  const archetype = this.entityToArchetype.get(entityId);
2185
2593
  if (!archetype) throw new Error(`Entity ${entityId} does not exist`);
@@ -2190,88 +2598,39 @@ var World = class {
2190
2598
  }
2191
2599
  return archetype.getOptional(entityId, componentType);
2192
2600
  }
2193
- hook(componentTypesOrSingle, hook) {
2194
- if (typeof hook === "function") if (Array.isArray(componentTypesOrSingle)) {
2195
- const callback = hook;
2196
- hook = {
2197
- on_init: (entityId, ...components) => callback("init", entityId, ...components),
2198
- on_set: (entityId, ...components) => callback("set", entityId, ...components),
2199
- on_remove: (entityId, ...components) => callback("remove", entityId, ...components)
2200
- };
2201
- } else {
2202
- const callback = hook;
2203
- hook = {
2204
- on_init: (entityId, componentType, component$1) => callback("init", entityId, componentType, component$1),
2205
- on_set: (entityId, componentType, component$1) => callback("set", entityId, componentType, component$1),
2206
- on_remove: (entityId, componentType, component$1) => callback("remove", entityId, componentType, component$1)
2207
- };
2208
- }
2209
- if (Array.isArray(componentTypesOrSingle)) {
2210
- const componentTypes = componentTypesOrSingle;
2211
- const requiredComponents = [];
2212
- const optionalComponents = [];
2213
- for (const ct of componentTypes) if (!isOptionalEntityId(ct)) requiredComponents.push(ct);
2214
- else optionalComponents.push(ct.optional);
2215
- if (requiredComponents.length === 0) throw new Error("Hook must have at least one required component");
2216
- const entry = {
2217
- componentTypes,
2218
- requiredComponents,
2219
- optionalComponents,
2220
- hook
2221
- };
2222
- this.hooks.add(entry);
2223
- for (const archetype of this.archetypes) if (this.archetypeMatchesHook(archetype, entry)) archetype.matchingMultiHooks.add(entry);
2224
- const multiHook = hook;
2225
- if (multiHook.on_init !== void 0) {
2226
- const matchingArchetypes = this.getMatchingArchetypes(requiredComponents);
2227
- for (const archetype of matchingArchetypes) for (const entityId of archetype.getEntities()) {
2228
- const components = collectMultiHookComponents(this.createHooksContext(), entityId, componentTypes);
2229
- multiHook.on_init(entityId, ...components);
2230
- }
2231
- }
2232
- return () => {
2233
- this.hooks.delete(entry);
2234
- for (const archetype of this.archetypes) archetype.matchingMultiHooks.delete(entry);
2235
- };
2236
- } else {
2237
- const componentType = componentTypesOrSingle;
2238
- if (!this.legacyHooks.has(componentType)) this.legacyHooks.set(componentType, /* @__PURE__ */ new Set());
2239
- const legacyHook = hook;
2240
- this.legacyHooks.get(componentType).add(legacyHook);
2241
- if (legacyHook.on_init !== void 0) this.archetypesByComponent.get(componentType)?.forEach((archetype) => {
2242
- const entities = archetype.getEntityToIndexMap();
2243
- const componentData = archetype.getComponentData(componentType);
2244
- for (const [entity, index] of entities) {
2245
- const data = componentData[index];
2246
- const value = data === MISSING_COMPONENT ? void 0 : data;
2247
- legacyHook.on_init?.(entity, componentType, value);
2248
- }
2249
- });
2250
- return () => {
2251
- const hooks = this.legacyHooks.get(componentType);
2252
- if (hooks) {
2253
- hooks.delete(legacyHook);
2254
- if (hooks.size === 0) this.legacyHooks.delete(componentType);
2255
- }
2256
- };
2257
- }
2258
- }
2259
- /** @deprecated use the unsubscribe function returned by hook() instead */
2260
- unhook(componentTypesOrSingle, hook) {
2261
- if (Array.isArray(componentTypesOrSingle)) {
2262
- for (const entry of this.hooks) if (entry.hook === hook) {
2263
- this.hooks.delete(entry);
2264
- for (const archetype of this.archetypes) archetype.matchingMultiHooks.delete(entry);
2265
- break;
2266
- }
2267
- } else {
2268
- const componentType = componentTypesOrSingle;
2269
- const hooks = this.legacyHooks.get(componentType);
2270
- if (hooks) {
2271
- hooks.delete(hook);
2272
- if (hooks.size === 0) this.legacyHooks.delete(componentType);
2273
- }
2274
- }
2601
+ hook(componentTypes, hook, filter) {
2602
+ const isCallback = typeof hook === "function";
2603
+ const callback = isCallback ? hook : void 0;
2604
+ const requiredComponents = [];
2605
+ const optionalComponents = [];
2606
+ for (const ct of componentTypes) if (!isOptionalEntityId(ct)) requiredComponents.push(ct);
2607
+ else optionalComponents.push(ct.optional);
2608
+ if (requiredComponents.length === 0) throw new Error("Hook must have at least one required component");
2609
+ const entry = {
2610
+ componentTypes,
2611
+ requiredComponents,
2612
+ optionalComponents,
2613
+ filter: filter || {},
2614
+ hook: isCallback ? {} : hook,
2615
+ callback,
2616
+ matchedArchetypes: /* @__PURE__ */ new Set()
2617
+ };
2618
+ this.hooks.add(entry);
2619
+ const matchedArchetypes = [];
2620
+ for (const archetype of this.archetypes) if (this.archetypeMatchesHook(archetype, entry)) {
2621
+ archetype.matchingMultiHooks.add(entry);
2622
+ entry.matchedArchetypes.add(archetype);
2623
+ matchedArchetypes.push(archetype);
2624
+ }
2625
+ if (isCallback || hook.on_init !== void 0) for (const archetype of matchedArchetypes) for (const entityId of archetype.getEntities()) {
2626
+ const components = collectMultiHookComponents(this.createHooksContext(), entityId, componentTypes);
2627
+ if (isCallback) callback("init", entityId, ...components);
2628
+ else hook.on_init(entityId, ...components);
2629
+ }
2630
+ return () => {
2631
+ this.hooks.delete(entry);
2632
+ if (entry.matchedArchetypes) for (const archetype of entry.matchedArchetypes) archetype.matchingMultiHooks.delete(entry);
2633
+ };
2275
2634
  }
2276
2635
  /**
2277
2636
  * Synchronizes all buffered commands (set/remove/delete) to the world.
@@ -2318,18 +2677,7 @@ var World = class {
2318
2677
  const sortedTypes = normalizeComponentTypes(componentTypes);
2319
2678
  const filterKey = serializeQueryFilter(filter);
2320
2679
  const key = `${this.createArchetypeSignature(sortedTypes)}${filterKey ? `|${filterKey}` : ""}`;
2321
- const cached = this.queryCache.get(key);
2322
- if (cached) {
2323
- cached.refCount++;
2324
- return cached.query;
2325
- }
2326
- const query = new Query(this, sortedTypes, filter);
2327
- query._cacheKey = key;
2328
- this.queryCache.set(key, {
2329
- query,
2330
- refCount: 1
2331
- });
2332
- return query;
2680
+ return this.queryRegistry.getOrCreate(this, sortedTypes, key, filter);
2333
2681
  }
2334
2682
  /**
2335
2683
  * Creates a new entity builder for fluent entity configuration.
@@ -2371,13 +2719,6 @@ var World = class {
2371
2719
  }
2372
2720
  return entities;
2373
2721
  }
2374
- _registerQuery(query) {
2375
- this.queries.push(query);
2376
- }
2377
- _unregisterQuery(query) {
2378
- const index = this.queries.indexOf(query);
2379
- if (index !== -1) this.queries.splice(index, 1);
2380
- }
2381
2722
  /**
2382
2723
  * Releases a cached query and frees its resources if no longer needed.
2383
2724
  * Call this when you're done using a query to allow the world to clean up its cache entry.
@@ -2390,16 +2731,7 @@ var World = class {
2390
2731
  * world.releaseQuery(query); // Optional cleanup
2391
2732
  */
2392
2733
  releaseQuery(query) {
2393
- const key = query._cacheKey;
2394
- if (!key) return;
2395
- const cached = this.queryCache.get(key);
2396
- if (!cached || cached.query !== query) return;
2397
- cached.refCount--;
2398
- if (cached.refCount <= 0) {
2399
- this.queryCache.delete(key);
2400
- this._unregisterQuery(query);
2401
- cached.query._disposeInternal();
2402
- }
2734
+ this.queryRegistry.release(query);
2403
2735
  }
2404
2736
  /**
2405
2737
  * Returns all archetypes that contain entities with the specified components.
@@ -2422,26 +2754,29 @@ var World = class {
2422
2754
  } else regularComponents.push(componentType);
2423
2755
  let matchingArchetypes = this.getArchetypesWithComponents(regularComponents);
2424
2756
  for (const { componentId, relationId } of wildcardRelations) {
2425
- const archetypesWithMarker = this.archetypesByComponent.get(relationId) || [];
2426
- matchingArchetypes = matchingArchetypes.length === 0 ? archetypesWithMarker : matchingArchetypes.filter((a) => archetypesWithMarker.includes(a) || a.hasRelationWithComponentId(componentId));
2757
+ const markerSet = this.archetypesByComponent.get(relationId);
2758
+ const archetypesWithMarker = markerSet ? Array.from(markerSet) : [];
2759
+ matchingArchetypes = matchingArchetypes.length === 0 ? archetypesWithMarker : matchingArchetypes.filter((a) => markerSet?.has(a) || a.hasRelationWithComponentId(componentId));
2427
2760
  }
2428
2761
  return matchingArchetypes;
2429
2762
  }
2430
2763
  getArchetypesWithComponents(componentTypes) {
2431
2764
  if (componentTypes.length === 0) return [...this.archetypes];
2432
- if (componentTypes.length === 1) return this.archetypesByComponent.get(componentTypes[0]) || [];
2433
- const archetypeLists = componentTypes.map((type) => this.archetypesByComponent.get(type) || []).sort((a, b) => a.length - b.length);
2434
- const shortest = archetypeLists[0];
2435
- if (shortest.length === 0) return [];
2436
- if (archetypeLists.length === 2) {
2437
- const second = archetypeLists[1];
2438
- const secondSet = new Set(second);
2439
- return shortest.filter((a) => secondSet.has(a));
2440
- }
2441
- let result = new Set(shortest);
2442
- for (let i = 1; i < archetypeLists.length; i++) {
2443
- const listSet = new Set(archetypeLists[i]);
2444
- for (const item of result) if (!listSet.has(item)) result.delete(item);
2765
+ if (componentTypes.length === 1) {
2766
+ const set = this.archetypesByComponent.get(componentTypes[0]);
2767
+ return set ? Array.from(set) : [];
2768
+ }
2769
+ const sets = componentTypes.map((type) => this.archetypesByComponent.get(type)).filter((s) => s !== void 0 && s.size > 0).sort((a, b) => a.size - b.size);
2770
+ if (sets.length === 0) return [];
2771
+ if (sets.length < componentTypes.length) return [];
2772
+ const smallest = sets[0];
2773
+ if (sets.length === 2) {
2774
+ const other = sets[1];
2775
+ return Array.from(smallest).filter((a) => other.has(a));
2776
+ }
2777
+ let result = new Set(smallest);
2778
+ for (let i = 1; i < sets.length; i++) {
2779
+ for (const item of result) if (!sets[i].has(item)) result.delete(item);
2445
2780
  if (result.size === 0) return [];
2446
2781
  }
2447
2782
  return Array.from(result);
@@ -2450,70 +2785,43 @@ var World = class {
2450
2785
  const matchingArchetypes = this.getMatchingArchetypes(componentTypes);
2451
2786
  if (includeComponents) {
2452
2787
  const result = [];
2453
- for (const archetype of matchingArchetypes) result.push(...archetype.getEntitiesWithComponents(componentTypes));
2788
+ for (const archetype of matchingArchetypes) archetype.appendEntitiesWithComponents(componentTypes, result);
2454
2789
  return result;
2455
2790
  } else {
2456
2791
  const result = [];
2457
- for (const archetype of matchingArchetypes) result.push(...archetype.getEntities());
2792
+ for (const archetype of matchingArchetypes) for (const entity of archetype.getEntities()) result.push(entity);
2458
2793
  return result;
2459
2794
  }
2460
2795
  }
2461
2796
  executeEntityCommands(entityId, commands) {
2462
- const changeset = this._changeset;
2463
- changeset.clear();
2464
- if (this.isComponentEntityId(entityId)) {
2465
- this.executeComponentEntityCommands(entityId, commands);
2466
- return changeset;
2797
+ this._changeset.clear();
2798
+ if (this.componentEntities.exists(entityId)) {
2799
+ this.componentEntities.executeCommands(entityId, commands);
2800
+ return;
2467
2801
  }
2468
2802
  if (commands.some((cmd) => cmd.type === "destroy")) {
2469
2803
  this.destroyEntityImmediate(entityId);
2470
- return changeset;
2804
+ return;
2471
2805
  }
2806
+ this.applyEntityCommands(entityId, commands);
2807
+ }
2808
+ applyEntityCommands(entityId, commands) {
2472
2809
  const currentArchetype = this.entityToArchetype.get(entityId);
2473
- if (!currentArchetype) return changeset;
2810
+ if (!currentArchetype) return;
2811
+ const changeset = this._changeset;
2474
2812
  processCommands(entityId, currentArchetype, commands, changeset, (eid, arch, compId) => {
2475
2813
  if (isExclusiveComponent(compId)) removeMatchingRelations(eid, arch, compId, changeset);
2476
2814
  });
2477
- const hasHooks = this.legacyHooks.size > 0 || this.hooks.size > 0;
2478
- const hasEntityRefs = changeset.removes.size > 0 || changeset.adds.size > 0;
2479
- if (!hasHooks) {
2480
- applyChangesetNoHooks(this._commandCtx, entityId, currentArchetype, changeset, this.entityToArchetype);
2481
- if (hasEntityRefs) this.updateEntityReferences(entityId, changeset);
2482
- return changeset;
2483
- }
2484
- const { removedComponents, newArchetype } = applyChangeset(this._commandCtx, entityId, currentArchetype, changeset, this.entityToArchetype);
2485
- if (hasEntityRefs) this.updateEntityReferences(entityId, changeset);
2486
- triggerLifecycleHooks(this.createHooksContext(), entityId, changeset.adds, removedComponents, currentArchetype, newArchetype);
2487
- return changeset;
2488
- }
2489
- executeComponentEntityCommands(entityId, commands) {
2490
- if (commands.some((cmd) => cmd.type === "destroy")) {
2491
- this.clearComponentEntityComponents(entityId);
2815
+ const hasStructuralChange = changeset.removes.size > 0 || changeset.adds.size > 0;
2816
+ if (this.hooks.size === 0) {
2817
+ applyChangeset(this._commandCtx, entityId, currentArchetype, changeset, this.entityToArchetype, null);
2818
+ if (hasStructuralChange) this.updateEntityReferences(entityId, changeset);
2492
2819
  return;
2493
2820
  }
2494
- const pendingSetValues = /* @__PURE__ */ new Map();
2495
- for (const command of commands) if (command.type === "set" && command.componentType) {
2496
- const merge = getComponentMerge(command.componentType);
2497
- let nextValue = command.component;
2498
- if (merge !== void 0 && pendingSetValues.has(command.componentType)) nextValue = merge(pendingSetValues.get(command.componentType), command.component);
2499
- pendingSetValues.set(command.componentType, nextValue);
2500
- this.getComponentEntityComponents(entityId, true).set(command.componentType, nextValue);
2501
- } else if (command.type === "delete" && command.componentType) {
2502
- const data = this.componentEntityComponents.get(entityId);
2503
- if (isWildcardRelationId(command.componentType)) {
2504
- const componentId = getComponentIdFromRelationId(command.componentType);
2505
- if (componentId !== void 0) {
2506
- if (data) {
2507
- for (const key of Array.from(data.keys())) if (getComponentIdFromRelationId(key) === componentId) data.delete(key);
2508
- }
2509
- for (const key of Array.from(pendingSetValues.keys())) if (getComponentIdFromRelationId(key) === componentId) pendingSetValues.delete(key);
2510
- }
2511
- } else {
2512
- data?.delete(command.componentType);
2513
- pendingSetValues.delete(command.componentType);
2514
- }
2515
- if (data?.size === 0) this.clearComponentEntityComponents(entityId);
2516
- }
2821
+ const removedComponents = /* @__PURE__ */ new Map();
2822
+ const newArchetype = applyChangeset(this._commandCtx, entityId, currentArchetype, changeset, this.entityToArchetype, removedComponents);
2823
+ if (hasStructuralChange) this.updateEntityReferences(entityId, changeset);
2824
+ triggerLifecycleHooks(this.createHooksContext(), entityId, changeset.adds, removedComponents, currentArchetype, newArchetype);
2517
2825
  }
2518
2826
  createHooksContext() {
2519
2827
  return this._hooksCtx;
@@ -2521,11 +2829,12 @@ var World = class {
2521
2829
  removeComponentImmediate(entityId, componentType, targetEntityId) {
2522
2830
  const sourceArchetype = this.entityToArchetype.get(entityId);
2523
2831
  if (!sourceArchetype) return;
2524
- const changeset = new ComponentChangeset();
2832
+ const changeset = this._removeChangeset;
2833
+ changeset.clear();
2525
2834
  changeset.delete(componentType);
2526
2835
  maybeRemoveWildcardMarker(entityId, sourceArchetype, componentType, getComponentIdFromRelationId(componentType), changeset);
2527
2836
  const removedComponent = sourceArchetype.get(entityId, componentType);
2528
- const { newArchetype } = applyChangeset(this._commandCtx, entityId, sourceArchetype, changeset, this.entityToArchetype);
2837
+ const newArchetype = applyChangeset(this._commandCtx, entityId, sourceArchetype, changeset, this.entityToArchetype, null);
2529
2838
  untrackEntityReference(this.entityReferences, entityId, componentType, targetEntityId);
2530
2839
  triggerLifecycleHooks(this.createHooksContext(), entityId, /* @__PURE__ */ new Map(), new Map([[componentType, removedComponent]]), sourceArchetype, newArchetype);
2531
2840
  }
@@ -2544,20 +2853,56 @@ var World = class {
2544
2853
  const hashKey = this.createArchetypeSignature(sortedTypes);
2545
2854
  return getOrCompute(this.archetypeBySignature, hashKey, () => this.createNewArchetype(sortedTypes));
2546
2855
  }
2856
+ /** Add componentType to the reverse index if it contains an entity ID */
2857
+ addToReferencingIndex(componentType, archetype) {
2858
+ const detailedType = getDetailedIdType(componentType);
2859
+ let entityId;
2860
+ if (detailedType.type === "entity") entityId = componentType;
2861
+ else if (detailedType.type === "entity-relation") entityId = detailedType.targetId;
2862
+ if (entityId !== void 0) {
2863
+ let refs = this.entityToReferencingArchetypes.get(entityId);
2864
+ if (!refs) {
2865
+ refs = /* @__PURE__ */ new Set();
2866
+ this.entityToReferencingArchetypes.set(entityId, refs);
2867
+ }
2868
+ refs.add(archetype);
2869
+ }
2870
+ }
2871
+ /** Remove componentType from the reverse index */
2872
+ removeFromReferencingIndex(componentType, archetype) {
2873
+ const detailedType = getDetailedIdType(componentType);
2874
+ let entityId;
2875
+ if (detailedType.type === "entity") entityId = componentType;
2876
+ else if (detailedType.type === "entity-relation") entityId = detailedType.targetId;
2877
+ if (entityId !== void 0) {
2878
+ const refs = this.entityToReferencingArchetypes.get(entityId);
2879
+ if (refs) {
2880
+ refs.delete(archetype);
2881
+ if (refs.size === 0) this.entityToReferencingArchetypes.delete(entityId);
2882
+ }
2883
+ }
2884
+ }
2547
2885
  createNewArchetype(componentTypes) {
2548
- const newArchetype = new Archetype(componentTypes, this.dontFragmentRelations);
2886
+ const newArchetype = new Archetype(componentTypes, this.dontFragmentStore);
2549
2887
  this.archetypes.push(newArchetype);
2550
2888
  for (const componentType of componentTypes) {
2551
- const archetypes = this.archetypesByComponent.get(componentType) || [];
2552
- archetypes.push(newArchetype);
2553
- this.archetypesByComponent.set(componentType, archetypes);
2889
+ let archetypes = this.archetypesByComponent.get(componentType);
2890
+ if (!archetypes) {
2891
+ archetypes = /* @__PURE__ */ new Set();
2892
+ this.archetypesByComponent.set(componentType, archetypes);
2893
+ }
2894
+ archetypes.add(newArchetype);
2895
+ this.addToReferencingIndex(componentType, newArchetype);
2554
2896
  }
2555
- for (const query of this.queries) query.checkNewArchetype(newArchetype);
2897
+ this.queryRegistry.onNewArchetype(newArchetype);
2556
2898
  this.updateArchetypeHookMatches(newArchetype);
2557
2899
  return newArchetype;
2558
2900
  }
2559
2901
  updateArchetypeHookMatches(archetype) {
2560
- for (const entry of this.hooks) if (this.archetypeMatchesHook(archetype, entry)) archetype.matchingMultiHooks.add(entry);
2902
+ for (const entry of this.hooks) if (this.archetypeMatchesHook(archetype, entry)) {
2903
+ archetype.matchingMultiHooks.add(entry);
2904
+ if (entry.matchedArchetypes) entry.matchedArchetypes.add(archetype);
2905
+ }
2561
2906
  }
2562
2907
  archetypeMatchesHook(archetype, entry) {
2563
2908
  return entry.requiredComponents.every((c) => {
@@ -2567,32 +2912,31 @@ var World = class {
2567
2912
  return componentId !== void 0 && archetype.hasRelationWithComponentId(componentId);
2568
2913
  }
2569
2914
  return archetype.componentTypeSet.has(c) || isDontFragmentRelation(c);
2570
- });
2571
- }
2572
- archetypeReferencesEntity(archetype, entityId) {
2573
- return archetype.componentTypes.some((ct) => ct === entityId || isEntityRelation(ct) && getTargetIdFromRelationId(ct) === entityId);
2915
+ }) && matchesFilter(archetype, entry.filter);
2574
2916
  }
2575
2917
  cleanupArchetypesReferencingEntity(entityId) {
2576
- for (let i = this.archetypes.length - 1; i >= 0; i--) {
2577
- const archetype = this.archetypes[i];
2578
- if (archetype.getEntities().length === 0 && this.archetypeReferencesEntity(archetype, entityId)) this.removeArchetype(archetype);
2579
- }
2918
+ const refs = this.entityToReferencingArchetypes.get(entityId);
2919
+ if (!refs) return;
2920
+ for (const archetype of refs) if (archetype.getEntities().length === 0) this.removeArchetype(archetype);
2921
+ this.entityToReferencingArchetypes.delete(entityId);
2580
2922
  }
2581
2923
  removeArchetype(archetype) {
2582
2924
  const index = this.archetypes.indexOf(archetype);
2583
- if (index !== -1) this.archetypes.splice(index, 1);
2925
+ if (index !== -1) {
2926
+ const last = this.archetypes[this.archetypes.length - 1];
2927
+ this.archetypes[index] = last;
2928
+ this.archetypes.pop();
2929
+ }
2584
2930
  this.archetypeBySignature.delete(this.createArchetypeSignature(archetype.componentTypes));
2585
2931
  for (const componentType of archetype.componentTypes) {
2586
2932
  const archetypes = this.archetypesByComponent.get(componentType);
2587
2933
  if (archetypes) {
2588
- const compIndex = archetypes.indexOf(archetype);
2589
- if (compIndex !== -1) {
2590
- archetypes.splice(compIndex, 1);
2591
- if (archetypes.length === 0) this.archetypesByComponent.delete(componentType);
2592
- }
2934
+ archetypes.delete(archetype);
2935
+ if (archetypes.size === 0) this.archetypesByComponent.delete(componentType);
2593
2936
  }
2937
+ this.removeFromReferencingIndex(componentType, archetype);
2594
2938
  }
2595
- for (const query of this.queries) query.removeArchetype(archetype);
2939
+ this.queryRegistry.onArchetypeRemoved(archetype);
2596
2940
  }
2597
2941
  /**
2598
2942
  * Serializes the entire world state to a plain JavaScript object.
@@ -2616,31 +2960,7 @@ var World = class {
2616
2960
  * const newWorld = new World(savedData);
2617
2961
  */
2618
2962
  serialize() {
2619
- const entities = [];
2620
- for (const archetype of this.archetypes) {
2621
- const dumpedEntities = archetype.dump();
2622
- for (const { entity, components } of dumpedEntities) entities.push({
2623
- id: encodeEntityId(entity),
2624
- components: Array.from(components.entries()).map(([rawType, value]) => ({
2625
- type: encodeEntityId(rawType),
2626
- value: value === MISSING_COMPONENT ? void 0 : value
2627
- }))
2628
- });
2629
- }
2630
- const componentEntities = [];
2631
- for (const [entityId, components] of this.componentEntityComponents.entries()) componentEntities.push({
2632
- id: encodeEntityId(entityId),
2633
- components: Array.from(components.entries()).map(([rawType, value]) => ({
2634
- type: encodeEntityId(rawType),
2635
- value: value === MISSING_COMPONENT ? void 0 : value
2636
- }))
2637
- });
2638
- return {
2639
- version: 1,
2640
- entityManager: this.entityIdManager.serializeState(),
2641
- entities,
2642
- componentEntities
2643
- };
2963
+ return serializeWorld(this.archetypes, this.componentEntities, this.entityIdManager);
2644
2964
  }
2645
2965
  };
2646
2966