@codehz/ecs 0.6.11 → 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,867 +668,1115 @@ 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;
632
- }
633
- changed = true;
634
- finalComponentTypes.delete(componentType);
635
- }
636
- for (const componentType of this.adds.keys()) {
637
- if (finalComponentTypes.has(componentType)) continue;
638
- changed = true;
639
- finalComponentTypes.add(componentType);
640
- }
641
- return changed ? Array.from(finalComponentTypes) : void 0;
642
- }
643
- };
644
-
645
- //#endregion
646
- //#region src/commands/command-buffer.ts
647
- /**
648
- * Maximum number of command buffer execution iterations to prevent infinite loops
649
- */
650
- const MAX_COMMAND_ITERATIONS = 100;
651
- /**
652
- * Command buffer for deferred structural changes
653
- */
654
- var CommandBuffer = class {
655
- commands = [];
656
- swapBuffer = [];
657
- /** Reusable map to group commands by entity, avoids per-sync allocations */
658
- entityCommands = /* @__PURE__ */ new Map();
659
- executeEntityCommands;
843
+ matchingMultiHooks = /* @__PURE__ */ new Set();
660
844
  /**
661
- * Create a command buffer with an executor function
845
+ * Cache for pre-computed component data sources to avoid repeated calculations
662
846
  */
663
- constructor(executeEntityCommands) {
664
- this.executeEntityCommands = executeEntityCommands;
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, []);
665
853
  }
666
- set(entityId, componentType, component$1) {
667
- this.commands.push({
668
- type: "set",
669
- entityId,
670
- componentType,
671
- component: component$1
672
- });
854
+ get size() {
855
+ return this.entities.length;
673
856
  }
674
857
  /**
675
- * Remove a component from an entity (deferred)
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
676
862
  */
677
- remove(entityId, componentType) {
678
- this.commands.push({
679
- type: "delete",
680
- entityId,
681
- componentType
682
- });
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]);
683
867
  }
684
- /**
685
- * Destroy an entity (deferred)
686
- */
687
- delete(entityId) {
688
- this.commands.push({
689
- type: "destroy",
690
- entityId
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);
909
+ }
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
+ };
691
916
  });
692
917
  }
693
- /**
694
- * Execute all commands and clear the buffer
695
- */
696
- execute() {
697
- let iterations = 0;
698
- while (this.commands.length > 0) {
699
- if (iterations >= MAX_COMMAND_ITERATIONS) throw new Error("Command execution exceeded maximum iterations, possible infinite loop");
700
- iterations++;
701
- const currentCommands = this.commands;
702
- this.commands = this.swapBuffer;
703
- const entityCommands = this.entityCommands;
704
- for (const cmd of currentCommands) {
705
- const existing = entityCommands.get(cmd.entityId);
706
- if (existing !== void 0) existing.push(cmd);
707
- else entityCommands.set(cmd.entityId, [cmd]);
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);
927
+ }
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];
708
937
  }
709
- currentCommands.length = 0;
710
- this.swapBuffer = currentCommands;
711
- for (const [entityId, commands] of entityCommands) this.executeEntityCommands(entityId, commands);
712
- entityCommands.clear();
713
938
  }
939
+ this.entities.pop();
940
+ for (const componentType of this.componentTypes) this.getComponentData(componentType).pop();
941
+ return removedData;
714
942
  }
715
- /**
716
- * Get current commands (for testing)
717
- */
718
- getCommands() {
719
- return [...this.commands];
943
+ exists(entityId) {
944
+ return this.entityToIndex.has(entityId);
720
945
  }
721
- /**
722
- * Clear all commands
723
- */
724
- clear() {
725
- this.commands = [];
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);
726
951
  }
727
- };
728
-
729
- //#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
782
- /**
783
- * Query class for efficient entity queries with cached archetypes
784
- */
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);
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;
808
967
  }
809
- /**
810
- * Check if query is disposed and throw error if so
811
- */
812
- ensureNotDisposed() {
813
- if (this.isDisposed) throw new Error("Query has been disposed");
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`);
814
1007
  }
815
- /**
816
- * Get all entities matching the query
817
- */
818
1008
  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;
824
- }
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;
1009
+ return this.entities;
828
1010
  }
829
- /**
830
- * Check if entity matches all query requirements (wildcards and specific dontFragment relations)
831
- */
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;
1011
+ getEntityToIndexMap() {
1012
+ return this.entityToIndex;
839
1013
  }
840
- /**
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
844
- */
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;
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;
853
1018
  }
854
- /**
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
858
- */
859
- forEach(componentTypes, callback) {
860
- this.ensureNotDisposed();
861
- for (const archetype of this.cachedArchetypes) archetype.forEachWithComponents(componentTypes, callback);
1019
+ getOptionalComponentData(componentType) {
1020
+ return this.componentData.get(componentType);
862
1021
  }
863
- /**
864
- * Iterate over entities with their component data (generator)
865
- * @param componentTypes Array of component types to retrieve
866
- */
867
- *iterate(componentTypes) {
868
- this.ensureNotDisposed();
869
- for (const archetype of this.cachedArchetypes) yield* archetype.iterateWithComponents(componentTypes);
1022
+ getCachedComponentDataSources(componentTypes) {
1023
+ const cacheKey = buildCacheKey(componentTypes);
1024
+ return getOrCompute(this.componentDataSourcesCache, cacheKey, () => componentTypes.map((compType) => this.getComponentDataSource(compType)));
870
1025
  }
871
- /**
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
875
- */
876
- getComponentData(componentType) {
877
- this.ensureNotDisposed();
878
- const result = [];
879
- for (const archetype of this.cachedArchetypes) result.push(...archetype.getComponentData(componentType));
880
- return result;
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);
881
1034
  }
882
- /**
883
- * Update the cached archetypes
884
- * Called when new archetypes are created
885
- */
886
- updateCache() {
887
- if (this.isDisposed) return;
888
- this.cachedArchetypes = this.world.getMatchingArchetypes(this.componentTypes).filter((archetype) => matchesFilter(archetype, this.filter));
1035
+ buildComponentsForIndex(componentTypes, componentDataSources, entityIndex, entityId) {
1036
+ return componentDataSources.map((dataSource, i) => buildSingleComponent(componentTypes[i], dataSource, entityIndex, entityId, (type) => this.getComponentData(type), this.dontFragmentRelations));
889
1037
  }
890
- /**
891
- * Check if a new archetype matches this query and add to cache if it does
892
- */
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);
1038
+ getEntitiesWithComponents(componentTypes) {
1039
+ const result = [];
1040
+ this.appendEntitiesWithComponents(componentTypes, result);
1041
+ return result;
896
1042
  }
897
- /**
898
- * Remove an archetype from the cached archetypes
899
- */
900
- removeArchetype(archetype) {
901
- if (this.isDisposed) return;
902
- const index = this.cachedArchetypes.indexOf(archetype);
903
- if (index !== -1) this.cachedArchetypes.splice(index, 1);
1043
+ appendEntitiesWithComponents(componentTypes, result) {
1044
+ this.forEachWithComponents(componentTypes, (entity, ...components) => {
1045
+ result.push({
1046
+ entity,
1047
+ components
1048
+ });
1049
+ });
904
1050
  }
905
- /**
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.
909
- */
910
- dispose() {
911
- this.world.releaseQuery(this);
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
+ }
912
1057
  }
913
- /**
914
- * Internal full dispose called by World when refCount reaches zero.
915
- */
916
- _disposeInternal() {
917
- if (!this.isDisposed) {
918
- this.world._unregisterQuery(this);
919
- this.cachedArchetypes = [];
920
- this.isDisposed = true;
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));
921
1063
  }
922
1064
  }
923
- /**
924
- * Symbol.dispose implementation for automatic resource management
925
- */
926
- [Symbol.dispose]() {
927
- this.dispose();
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
+ }
928
1074
  }
929
- /**
930
- * Check if the query has been disposed
931
- */
932
- get disposed() {
933
- return this.isDisposed;
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;
934
1088
  }
935
1089
  };
936
1090
 
937
1091
  //#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);
954
- }
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
- }
963
-
964
- //#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
- }
1092
+ //#region src/archetype/store.ts
1016
1093
  /**
1017
- * Build wildcard relation value from matching relations
1094
+ * Default implementation backed by a plain `Map`.
1095
+ * Created once by `World` and shared with every `Archetype`.
1018
1096
  */
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;
1097
+ var DontFragmentStoreImpl = class {
1098
+ data = /* @__PURE__ */ new Map();
1099
+ get(entityId) {
1100
+ return this.data.get(entityId);
1034
1101
  }
1035
- return optional ? { value: relations } : relations;
1036
- }
1037
- /**
1038
- * Build regular component value from data source
1039
- */
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`);
1102
+ set(entityId, data) {
1103
+ this.data.set(entityId, data);
1044
1104
  }
1045
- const data = dataSource[entityIndex];
1046
- const result = data === MISSING_COMPONENT ? void 0 : data;
1047
- return optional ? { value: result } : result;
1048
- }
1049
- /**
1050
- * Build a single component value based on its type
1051
- */
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);
1057
- }
1105
+ delete(entityId) {
1106
+ this.data.delete(entityId);
1107
+ }
1108
+ };
1058
1109
 
1059
1110
  //#endregion
1060
- //#region src/core/archetype.ts
1111
+ //#region src/commands/buffer.ts
1061
1112
  /**
1062
- * Special value to represent missing component data
1113
+ * Maximum number of command buffer execution iterations to prevent infinite loops
1063
1114
  */
1064
- const MISSING_COMPONENT = Symbol("missing component");
1115
+ const MAX_COMMAND_ITERATIONS = 100;
1065
1116
  /**
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
1117
+ * Command buffer for deferred structural changes
1069
1118
  */
1070
- var Archetype = class {
1119
+ var CommandBuffer = class {
1120
+ commands = [];
1121
+ swapBuffer = [];
1122
+ /** Reusable map to group commands by entity, avoids per-sync allocations */
1123
+ entityCommands = /* @__PURE__ */ new Map();
1124
+ executeEntityCommands;
1071
1125
  /**
1072
- * The component types that define this archetype
1126
+ * Create a command buffer with an executor function
1073
1127
  */
1074
- componentTypes;
1128
+ constructor(executeEntityCommands) {
1129
+ this.executeEntityCommands = executeEntityCommands;
1130
+ }
1131
+ set(entityId, componentType, component$1) {
1132
+ this.commands.push({
1133
+ type: "set",
1134
+ entityId,
1135
+ componentType,
1136
+ component: component$1
1137
+ });
1138
+ }
1075
1139
  /**
1076
- * Set version of componentTypes for O(1) lookups in hot paths
1140
+ * Remove a component from an entity (deferred)
1077
1141
  */
1078
- componentTypeSet;
1142
+ remove(entityId, componentType) {
1143
+ this.commands.push({
1144
+ type: "delete",
1145
+ entityId,
1146
+ componentType
1147
+ });
1148
+ }
1079
1149
  /**
1080
- * List of entities in this archetype
1150
+ * Destroy an entity (deferred)
1081
1151
  */
1082
- entities = [];
1152
+ delete(entityId) {
1153
+ this.commands.push({
1154
+ type: "destroy",
1155
+ entityId
1156
+ });
1157
+ }
1083
1158
  /**
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
1159
+ * Execute all commands and clear the buffer
1086
1160
  */
1087
- componentData = /* @__PURE__ */ new Map();
1161
+ execute() {
1162
+ let iterations = 0;
1163
+ while (this.commands.length > 0) {
1164
+ if (iterations >= MAX_COMMAND_ITERATIONS) throw new Error("Command execution exceeded maximum iterations, possible infinite loop");
1165
+ iterations++;
1166
+ const currentCommands = this.commands;
1167
+ this.commands = this.swapBuffer;
1168
+ const entityCommands = this.entityCommands;
1169
+ for (const cmd of currentCommands) {
1170
+ const existing = entityCommands.get(cmd.entityId);
1171
+ if (existing !== void 0) existing.push(cmd);
1172
+ else entityCommands.set(cmd.entityId, [cmd]);
1173
+ }
1174
+ currentCommands.length = 0;
1175
+ this.swapBuffer = currentCommands;
1176
+ for (const [entityId, commands] of entityCommands) this.executeEntityCommands(entityId, commands);
1177
+ entityCommands.clear();
1178
+ }
1179
+ }
1088
1180
  /**
1089
- * Reverse mapping from entity to its index in this archetype
1181
+ * Get current commands (for testing)
1090
1182
  */
1091
- entityToIndex = /* @__PURE__ */ new Map();
1183
+ getCommands() {
1184
+ return [...this.commands];
1185
+ }
1092
1186
  /**
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
1187
+ * Clear all commands
1096
1188
  */
1097
- dontFragmentRelations;
1189
+ clear() {
1190
+ this.commands = [];
1191
+ }
1192
+ };
1193
+
1194
+ //#endregion
1195
+ //#region src/commands/changeset.ts
1196
+ /**
1197
+ * @internal Represents a set of component changes to be applied to an entity
1198
+ */
1199
+ var ComponentChangeset = class {
1200
+ adds = /* @__PURE__ */ new Map();
1201
+ removes = /* @__PURE__ */ new Set();
1098
1202
  /**
1099
- * Multi-hooks that match this archetype
1203
+ * Add a component to the changeset
1100
1204
  */
1101
- matchingMultiHooks = /* @__PURE__ */ new Set();
1205
+ set(componentType, component$1) {
1206
+ this.adds.set(componentType, component$1);
1207
+ this.removes.delete(componentType);
1208
+ }
1102
1209
  /**
1103
- * Cache for pre-computed component data sources to avoid repeated calculations
1210
+ * Remove a component from the changeset
1104
1211
  */
1105
- componentDataSourcesCache = /* @__PURE__ */ new Map();
1106
- constructor(componentTypes, dontFragmentRelations) {
1107
- 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;
1212
+ delete(componentType) {
1213
+ this.removes.add(componentType);
1214
+ this.adds.delete(componentType);
1114
1215
  }
1115
1216
  /**
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
1217
+ * Check if the changeset has any changes
1120
1218
  */
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]);
1219
+ hasChanges() {
1220
+ return this.adds.size > 0 || this.removes.size > 0;
1125
1221
  }
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);
1222
+ /**
1223
+ * Clear all changes
1224
+ */
1225
+ clear() {
1226
+ this.adds.clear();
1227
+ this.removes.clear();
1136
1228
  }
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);
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);
1143
1236
  }
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);
1237
+ for (const componentType of other.removes) {
1238
+ this.removes.add(componentType);
1239
+ this.adds.delete(componentType);
1153
1240
  }
1154
- const dontFragmentData = this.dontFragmentRelations.get(entityId);
1155
- if (dontFragmentData) for (const [componentType, data] of dontFragmentData) entityData.set(componentType, data);
1156
- return entityData;
1157
1241
  }
1158
- getEntityDontFragmentRelations(entityId) {
1159
- return this.dontFragmentRelations.get(entityId);
1242
+ /**
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;
1160
1249
  }
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);
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;
1167
1262
  }
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);
1263
+ changed = true;
1264
+ finalComponentTypes.delete(componentType);
1185
1265
  }
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
- }
1266
+ for (const componentType of this.adds.keys()) {
1267
+ if (finalComponentTypes.has(componentType)) continue;
1268
+ changed = true;
1269
+ finalComponentTypes.add(componentType);
1196
1270
  }
1197
- this.entities.pop();
1198
- for (const componentType of this.componentTypes) this.getComponentData(componentType).pop();
1199
- return removedData;
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.
1291
+ */
1292
+ exists(entityId) {
1293
+ const detailed = getDetailedIdType(entityId);
1294
+ return detailed.type !== "entity" && detailed.type !== "invalid";
1295
+ }
1296
+ /**
1297
+ * Check if a component entity has a specific component.
1298
+ */
1299
+ has(entityId, componentType) {
1300
+ return this.componentEntityComponents.get(entityId)?.has(componentType) ?? false;
1301
+ }
1302
+ /**
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.
1305
+ */
1306
+ hasSingleton(componentId) {
1307
+ return this.componentEntityComponents.get(componentId)?.has(componentId) ?? false;
1200
1308
  }
1201
- exists(entityId) {
1202
- return this.entityToIndex.has(entityId);
1309
+ /**
1310
+ * Check if a component entity has any wildcard relations matching a component ID.
1311
+ */
1312
+ hasWildcard(entityId, componentId) {
1313
+ const data = this.componentEntityComponents.get(entityId);
1314
+ if (!data) return false;
1315
+ return hasWildcardRelation(data, componentId);
1203
1316
  }
1317
+ /**
1318
+ * Get a component value from a component entity.
1319
+ * Throws if the component does not exist.
1320
+ */
1204
1321
  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);
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);
1209
1325
  }
1210
- getWildcardRelations(entityId, index, componentType) {
1211
- const componentId = getComponentIdFromRelationId(componentType);
1326
+ /**
1327
+ * Get an optional component value from a component entity.
1328
+ * Returns undefined if the component does not exist.
1329
+ */
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) };
1334
+ }
1335
+ /**
1336
+ * Get all wildcard relations of a given type from a component entity.
1337
+ */
1338
+ getWildcard(entityId, wildcardComponentType) {
1339
+ const componentId = getComponentIdFromRelationId(wildcardComponentType);
1340
+ const data = this.componentEntityComponents.get(entityId);
1341
+ if (componentId === void 0 || !data) return [];
1212
1342
  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
- }
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]);
1222
1347
  }
1223
- if (componentId !== void 0) relations.push(...findMatchingDontFragmentRelations(this.dontFragmentRelations.get(entityId), componentId));
1224
1348
  return relations;
1225
1349
  }
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;
1231
- }
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}`);
1350
+ /**
1351
+ * Clear all data for a component entity.
1352
+ */
1353
+ clear(entityId) {
1354
+ if (this.componentEntityComponents.delete(entityId)) this.unregisterRelationEntityId(entityId);
1235
1355
  }
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 };
1243
- }
1244
- const dontFragmentData = this.dontFragmentRelations.get(entityId);
1245
- if (dontFragmentData?.has(componentType)) return { value: dontFragmentData.get(componentType) };
1356
+ /**
1357
+ * Cleanup all component entities that reference a given target entity.
1358
+ * Called when a target entity is destroyed.
1359
+ */
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);
1246
1365
  }
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;
1366
+ /**
1367
+ * Execute a batch of commands for a component entity.
1368
+ */
1369
+ executeCommands(entityId, commands) {
1370
+ if (commands.some((cmd) => cmd.type === "destroy")) {
1371
+ this.clear(entityId);
1252
1372
  return;
1253
1373
  }
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);
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);
1260
1385
  }
1261
- dontFragmentData.set(componentType, data);
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);
1402
+ }
1403
+ }
1404
+ /**
1405
+ * Initialize a component entity from a deserialization snapshot.
1406
+ */
1407
+ initFromSnapshot(entityId, componentMap) {
1408
+ this.componentEntityComponents.set(entityId, componentMap);
1409
+ this.registerRelationEntityId(entityId);
1410
+ }
1411
+ /**
1412
+ * Iterate over all component entity entries.
1413
+ * Used for serialization.
1414
+ */
1415
+ entries() {
1416
+ return this.componentEntityComponents.entries();
1417
+ }
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);
1262
1426
  return;
1263
1427
  }
1264
- throw new Error(`Component type ${componentType} is not in this archetype`);
1428
+ this.relationEntityIdsByTarget.set(targetId, new Set([entityId]));
1429
+ }
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
+ };
1441
+
1442
+ //#endregion
1443
+ //#region src/query/filter.ts
1444
+ /**
1445
+ * Serialize a QueryFilter into a deterministic string suitable for cache keys.
1446
+ * Currently only serializes `negativeComponentTypes`.
1447
+ */
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(",")}`;
1452
+ }
1453
+ /**
1454
+ * Check if an archetype matches the given component types
1455
+ */
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
+ });
1468
+ }
1469
+ /**
1470
+ * Check if an archetype matches the filter conditions (only filtering logic)
1471
+ */
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
+ });
1481
+ }
1482
+
1483
+ //#endregion
1484
+ //#region src/query/query.ts
1485
+ /**
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
+ * });
1500
+ */
1501
+ var Query = class {
1502
+ world;
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;
1513
+ /**
1514
+ * @internal Queries should be created via {@link World.createQuery}, not instantiated directly.
1515
+ */
1516
+ constructor(world, componentTypes, filter = {}, registry) {
1517
+ this.world = world;
1518
+ this.componentTypes = normalizeComponentTypes(componentTypes);
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);
1527
+ }
1528
+ /**
1529
+ * Check if query is disposed and throw error if so
1530
+ */
1531
+ ensureNotDisposed() {
1532
+ if (this.isDisposed) throw new Error("Query has been disposed");
1265
1533
  }
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
+ */
1266
1545
  getEntities() {
1267
- return this.entities;
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;
1551
+ }
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;
1555
+ }
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;
1563
+ }
1564
+ for (const specificType of this.specificDontFragmentTypes) if (archetype.getOptional(entity, specificType) === void 0) return false;
1565
+ return true;
1566
+ }
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;
1584
+ }
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();
1863
+ function applyChangeset(ctx, entityId, currentArchetype, changeset, entityToArchetype, removedComponents) {
1511
1864
  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) {
1527
- 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)) {
@@ -1615,9 +1928,22 @@ function filterRegularComponentTypes(componentTypes) {
1615
1928
  }
1616
1929
  return regularTypes;
1617
1930
  }
1618
-
1619
- //#endregion
1620
- //#region src/core/world-hooks.ts
1931
+
1932
+ //#endregion
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,44 +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 (!oldArchetype.matchingMultiHooks.has(entry) || (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
2002
  for (const entry of oldArchetype.matchingMultiHooks) {
1692
- const { hook, requiredComponents, componentTypes } = entry;
1693
- if (!hook.on_remove) continue;
2003
+ const { requiredComponents, componentTypes } = entry;
2004
+ if (!entry.callback && !entry.hook.on_remove) continue;
1694
2005
  const lostRequiredMatch = requiredComponents.some((c) => anyComponentMatches(removedComponents, c)) && entityHadAllComponentsBefore(ctx, entityId, requiredComponents, removedComponents) && !entityHasAllComponents(ctx, entityId, requiredComponents);
1695
2006
  const exitedMatchingSet = !newArchetype.matchingMultiHooks.has(entry);
1696
- if (lostRequiredMatch || exitedMatchingSet) hook.on_remove(entityId, ...collectMultiHookComponentsWithRemoved(ctx, entityId, componentTypes, removedComponents));
2007
+ if (lostRequiredMatch || exitedMatchingSet) invokeHook(entry, "remove", entityId, collectMultiHookComponentsWithRemoved(ctx, entityId, componentTypes, removedComponents));
1697
2008
  }
1698
2009
  }
1699
2010
  function entityHasAllComponents(ctx, entityId, requiredComponents) {
@@ -1787,6 +2098,7 @@ function collectWildcardFromRemoved(wildcardId, removedComponents) {
1787
2098
 
1788
2099
  //#endregion
1789
2100
  //#region src/utils/multi-map.ts
2101
+ const _MISSING = Symbol("missing");
1790
2102
  var MultiMap = class {
1791
2103
  map = /* @__PURE__ */ new Map();
1792
2104
  _valueCount = 0;
@@ -1799,10 +2111,10 @@ var MultiMap = class {
1799
2111
  hasKey(key) {
1800
2112
  return this.map.has(key);
1801
2113
  }
1802
- has(key, value) {
2114
+ has(key, value = _MISSING) {
1803
2115
  const set = this.map.get(key);
1804
2116
  if (!set) return false;
1805
- if (arguments.length === 1) return true;
2117
+ if (value === _MISSING) return true;
1806
2118
  return set.has(value);
1807
2119
  }
1808
2120
  add(key, value) {
@@ -1855,7 +2167,7 @@ var MultiMap = class {
1855
2167
  };
1856
2168
 
1857
2169
  //#endregion
1858
- //#region src/core/world-references.ts
2170
+ //#region src/world/references.ts
1859
2171
  function trackEntityReference(entityReferences, sourceEntityId, componentType, targetEntityId) {
1860
2172
  if (!entityReferences.has(targetEntityId)) entityReferences.set(targetEntityId, new MultiMap());
1861
2173
  entityReferences.get(targetEntityId).add(sourceEntityId, componentType);
@@ -1872,7 +2184,155 @@ function getEntityReferences(entityReferences, targetEntityId) {
1872
2184
  }
1873
2185
 
1874
2186
  //#endregion
1875
- //#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
1876
2336
  /**
1877
2337
  * World class for ECS architecture
1878
2338
  * Manages entities and components
@@ -1884,64 +2344,37 @@ var World = class {
1884
2344
  entityToArchetype = /* @__PURE__ */ new Map();
1885
2345
  archetypesByComponent = /* @__PURE__ */ new Map();
1886
2346
  entityReferences = /* @__PURE__ */ new Map();
1887
- dontFragmentRelations = /* @__PURE__ */ new Map();
1888
- componentEntityComponents = /* @__PURE__ */ new Map();
1889
- relationEntityIdsByTarget = /* @__PURE__ */ new Map();
1890
- queries = [];
1891
- queryCache = /* @__PURE__ */ new Map();
1892
- 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();
1893
2354
  hooks = /* @__PURE__ */ new Set();
1894
2355
  commandBuffer = new CommandBuffer((entityId, commands) => this.executeEntityCommands(entityId, commands));
1895
2356
  _changeset = new ComponentChangeset();
2357
+ _removeChangeset = new ComponentChangeset();
1896
2358
  /** Cached command processor context to avoid per-entity object allocation */
1897
2359
  _commandCtx = {
1898
- dontFragmentRelations: this.dontFragmentRelations,
2360
+ dontFragmentStore: this.dontFragmentStore,
1899
2361
  ensureArchetype: (ct) => this.ensureArchetype(ct)
1900
2362
  };
1901
2363
  /** Cached hooks context to avoid per-entity object allocation */
1902
2364
  _hooksCtx = {
1903
- hooks: this.legacyHooks,
1904
2365
  multiHooks: this.hooks,
1905
2366
  has: (eid, ct) => this.has(eid, ct),
1906
2367
  get: (eid, ct) => this.get(eid, ct),
1907
2368
  getOptional: (eid, ct) => this.getOptional(eid, ct)
1908
2369
  };
1909
2370
  constructor(snapshot) {
1910
- if (snapshot && typeof snapshot === "object") this.deserializeSnapshot(snapshot);
1911
- }
1912
- deserializeSnapshot(snapshot) {
1913
- if (snapshot.entityManager) this.entityIdManager.deserializeState(snapshot.entityManager);
1914
- if (Array.isArray(snapshot.componentEntities)) for (const entry of snapshot.componentEntities) {
1915
- const entityId = decodeSerializedId(entry.id);
1916
- if (!this.isComponentEntityId(entityId)) continue;
1917
- const componentsArray = entry.components || [];
1918
- const componentMap = /* @__PURE__ */ new Map();
1919
- for (const componentEntry of componentsArray) {
1920
- const componentType = decodeSerializedId(componentEntry.type);
1921
- componentMap.set(componentType, componentEntry.value);
1922
- }
1923
- this.componentEntityComponents.set(entityId, componentMap);
1924
- this.registerRelationEntityId(entityId);
1925
- }
1926
- if (Array.isArray(snapshot.entities)) for (const entry of snapshot.entities) {
1927
- const entityId = decodeSerializedId(entry.id);
1928
- const componentsArray = entry.components || [];
1929
- const componentMap = /* @__PURE__ */ new Map();
1930
- const componentTypes = [];
1931
- for (const componentEntry of componentsArray) {
1932
- const componentType = decodeSerializedId(componentEntry.type);
1933
- componentMap.set(componentType, componentEntry.value);
1934
- componentTypes.push(componentType);
1935
- }
1936
- const archetype = this.ensureArchetype(componentTypes);
1937
- archetype.addEntity(entityId, componentMap);
1938
- this.entityToArchetype.set(entityId, archetype);
1939
- for (const compType of componentTypes) {
1940
- const detailedType = getDetailedIdType(compType);
1941
- if (detailedType.type === "entity-relation") trackEntityReference(this.entityReferences, entityId, compType, detailedType.targetId);
1942
- else if (detailedType.type === "entity") trackEntityReference(this.entityReferences, entityId, compType, compType);
1943
- }
1944
- }
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);
1945
2378
  }
1946
2379
  createArchetypeSignature(componentTypes) {
1947
2380
  return componentTypes.join(",");
@@ -1965,51 +2398,34 @@ var World = class {
1965
2398
  this.entityToArchetype.set(entityId, emptyArchetype);
1966
2399
  return entityId;
1967
2400
  }
1968
- isComponentEntityId(entityId) {
1969
- const detailed = getDetailedIdType(entityId);
1970
- return detailed.type !== "entity" && detailed.type !== "invalid";
1971
- }
1972
- registerRelationEntityId(entityId) {
1973
- const detailed = getDetailedIdType(entityId);
1974
- if (detailed.type !== "entity-relation") return;
1975
- const targetId = detailed.targetId;
1976
- if (targetId === void 0) return;
1977
- const existing = this.relationEntityIdsByTarget.get(targetId);
1978
- if (existing) {
1979
- existing.add(entityId);
1980
- return;
1981
- }
1982
- this.relationEntityIdsByTarget.set(targetId, new Set([entityId]));
1983
- }
1984
- unregisterRelationEntityId(entityId) {
1985
- const detailed = getDetailedIdType(entityId);
1986
- if (detailed.type !== "entity-relation") return;
1987
- const targetId = detailed.targetId;
1988
- if (targetId === void 0) return;
1989
- const existing = this.relationEntityIdsByTarget.get(targetId);
1990
- if (!existing) return;
1991
- existing.delete(entityId);
1992
- if (existing.size === 0) this.relationEntityIdsByTarget.delete(targetId);
1993
- }
1994
- getComponentEntityComponents(entityId, create) {
1995
- let data = this.componentEntityComponents.get(entityId);
1996
- if (!data && create) {
1997
- data = /* @__PURE__ */ new Map();
1998
- this.componentEntityComponents.set(entityId, data);
1999
- this.registerRelationEntityId(entityId);
2000
- }
2001
- return data;
2002
- }
2003
- clearComponentEntityComponents(entityId) {
2004
- 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();
2005
2410
  }
2006
- cleanupComponentEntitiesReferencingEntity(targetId) {
2007
- const relationEntities = this.relationEntityIdsByTarget.get(targetId);
2008
- if (!relationEntities) return;
2009
- for (const relationEntityId of relationEntities) this.componentEntityComponents.delete(relationEntityId);
2010
- 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);
2011
2423
  }
2012
2424
  destroyEntityImmediate(entityId) {
2425
+ if (!this.entityReferences.has(entityId)) {
2426
+ this.destroySingleEntity(entityId);
2427
+ return;
2428
+ }
2013
2429
  const queue = [entityId];
2014
2430
  const visited = /* @__PURE__ */ new Set();
2015
2431
  let queueIndex = 0;
@@ -2028,25 +2444,33 @@ var World = class {
2028
2444
  this.entityReferences.delete(cur);
2029
2445
  const removedComponents = archetype.removeEntity(cur);
2030
2446
  this.entityToArchetype.delete(cur);
2031
- triggerRemoveHooksForEntityDeletion(this.createHooksContext(), cur, removedComponents, archetype);
2447
+ triggerRemoveHooksForEntityDeletion(cur, removedComponents, archetype);
2032
2448
  this.cleanupArchetypesReferencingEntity(cur);
2033
2449
  this.entityIdManager.deallocate(cur);
2034
- this.cleanupComponentEntitiesReferencingEntity(cur);
2450
+ this.componentEntities.cleanupReferencesTo(cur);
2035
2451
  }
2036
2452
  }
2037
2453
  /**
2038
- * 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.
2039
2459
  *
2040
2460
  * @param entityId - The entity identifier to check
2041
2461
  * @returns `true` if the entity exists, `false` otherwise
2042
2462
  *
2043
2463
  * @example
2464
+ * // Check if an entity is alive
2044
2465
  * if (world.exists(entityId)) {
2045
2466
  * console.log("Entity exists");
2046
2467
  * }
2468
+ *
2469
+ * // To check for a component, use has() instead:
2470
+ * if (world.has(entity, Position)) { ... }
2047
2471
  */
2048
2472
  exists(entityId) {
2049
- if (this.isComponentEntityId(entityId)) return true;
2473
+ if (this.componentEntities.exists(entityId)) return true;
2050
2474
  return this.entityToArchetype.has(entityId);
2051
2475
  }
2052
2476
  assertEntityExists(entityId, label) {
@@ -2101,18 +2525,6 @@ var World = class {
2101
2525
  componentType
2102
2526
  };
2103
2527
  }
2104
- getComponentEntityWildcardRelations(entityId, wildcardComponentType) {
2105
- const componentId = getComponentIdFromRelationId(wildcardComponentType);
2106
- const data = this.componentEntityComponents.get(entityId);
2107
- if (componentId === void 0 || !data) return [];
2108
- const relations = [];
2109
- for (const [key, value] of data.entries()) {
2110
- if (getComponentIdFromRelationId(key) !== componentId) continue;
2111
- const detailed = getDetailedIdType(key);
2112
- if (detailed.type === "entity-relation" || detailed.type === "component-relation") relations.push([detailed.targetId, value]);
2113
- }
2114
- return relations;
2115
- }
2116
2528
  set(entityId, componentTypeOrComponent, maybeComponent) {
2117
2529
  const { entityId: targetEntityId, componentType, component: component$1 } = this.resolveSetOperation(entityId, componentTypeOrComponent, maybeComponent);
2118
2530
  this.commandBuffer.set(targetEntityId, componentType, component$1);
@@ -2138,50 +2550,44 @@ var World = class {
2138
2550
  has(entityId, componentType) {
2139
2551
  if (componentType === void 0) {
2140
2552
  const componentId = entityId;
2141
- return this.componentEntityComponents.get(componentId)?.has(componentId) ?? false;
2553
+ return this.componentEntities.hasSingleton(componentId);
2142
2554
  }
2143
- if (this.isComponentEntityId(entityId)) {
2555
+ if (this.componentEntities.exists(entityId)) {
2144
2556
  if (isWildcardRelationId(componentType)) {
2145
2557
  const componentId = getComponentIdFromRelationId(componentType);
2146
2558
  if (componentId === void 0) return false;
2147
- const data = this.componentEntityComponents.get(entityId);
2148
- if (!data) return false;
2149
- return hasWildcardRelation(data, componentId);
2559
+ return this.componentEntities.hasWildcard(entityId, componentId);
2150
2560
  }
2151
- return this.componentEntityComponents.get(entityId)?.has(componentType) ?? false;
2561
+ return this.componentEntities.has(entityId, componentType);
2152
2562
  }
2153
2563
  const archetype = this.entityToArchetype.get(entityId);
2154
2564
  if (!archetype) return false;
2155
2565
  if (archetype.componentTypeSet.has(componentType)) return true;
2156
- if (isDontFragmentRelation(componentType)) return this.dontFragmentRelations.get(entityId)?.has(componentType) ?? false;
2566
+ if (isDontFragmentRelation(componentType)) return this.dontFragmentStore.get(entityId)?.has(componentType) ?? false;
2157
2567
  return false;
2158
2568
  }
2159
2569
  get(entityId, componentType = entityId) {
2160
- if (this.isComponentEntityId(entityId)) {
2161
- if (isWildcardRelationId(componentType)) return this.getComponentEntityWildcardRelations(entityId, componentType);
2162
- const data = this.componentEntityComponents.get(entityId);
2163
- if (!data || !data.has(componentType)) throw new Error(`Entity ${entityId} does not have component ${componentType}. Use has() to check component existence before calling get().`);
2164
- 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);
2165
2573
  }
2166
2574
  const archetype = this.entityToArchetype.get(entityId);
2167
2575
  if (!archetype) throw new Error(`Entity ${entityId} does not exist`);
2168
2576
  if (componentType >= 0 || componentType % RELATION_SHIFT !== 0) {
2169
2577
  const inArchetype = archetype.componentTypeSet.has(componentType);
2170
2578
  const hasDontFragment = isDontFragmentRelation(componentType);
2171
- 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().`);
2172
2580
  }
2173
2581
  return archetype.get(entityId, componentType);
2174
2582
  }
2175
2583
  getOptional(entityId, componentType = entityId) {
2176
- if (this.isComponentEntityId(entityId)) {
2584
+ if (this.componentEntities.exists(entityId)) {
2177
2585
  if (isWildcardRelationId(componentType)) {
2178
- const relations = this.getComponentEntityWildcardRelations(entityId, componentType);
2586
+ const relations = this.componentEntities.getWildcard(entityId, componentType);
2179
2587
  if (relations.length === 0) return void 0;
2180
2588
  return { value: relations };
2181
2589
  }
2182
- const data = this.componentEntityComponents.get(entityId);
2183
- if (!data || !data.has(componentType)) return void 0;
2184
- return { value: data.get(componentType) };
2590
+ return this.componentEntities.getOptional(entityId, componentType);
2185
2591
  }
2186
2592
  const archetype = this.entityToArchetype.get(entityId);
2187
2593
  if (!archetype) throw new Error(`Entity ${entityId} does not exist`);
@@ -2192,89 +2598,39 @@ var World = class {
2192
2598
  }
2193
2599
  return archetype.getOptional(entityId, componentType);
2194
2600
  }
2195
- hook(componentTypesOrSingle, hook, filter) {
2196
- if (typeof hook === "function") if (Array.isArray(componentTypesOrSingle)) {
2197
- const callback = hook;
2198
- hook = {
2199
- on_init: (entityId, ...components) => callback("init", entityId, ...components),
2200
- on_set: (entityId, ...components) => callback("set", entityId, ...components),
2201
- on_remove: (entityId, ...components) => callback("remove", entityId, ...components)
2202
- };
2203
- } else {
2204
- const callback = hook;
2205
- hook = {
2206
- on_init: (entityId, componentType, component$1) => callback("init", entityId, componentType, component$1),
2207
- on_set: (entityId, componentType, component$1) => callback("set", entityId, componentType, component$1),
2208
- on_remove: (entityId, componentType, component$1) => callback("remove", entityId, componentType, component$1)
2209
- };
2210
- }
2211
- if (Array.isArray(componentTypesOrSingle)) {
2212
- const componentTypes = componentTypesOrSingle;
2213
- const requiredComponents = [];
2214
- const optionalComponents = [];
2215
- for (const ct of componentTypes) if (!isOptionalEntityId(ct)) requiredComponents.push(ct);
2216
- else optionalComponents.push(ct.optional);
2217
- if (requiredComponents.length === 0) throw new Error("Hook must have at least one required component");
2218
- const entry = {
2219
- componentTypes,
2220
- requiredComponents,
2221
- optionalComponents,
2222
- filter: filter || {},
2223
- hook
2224
- };
2225
- this.hooks.add(entry);
2226
- for (const archetype of this.archetypes) if (this.archetypeMatchesHook(archetype, entry)) archetype.matchingMultiHooks.add(entry);
2227
- const multiHook = hook;
2228
- if (multiHook.on_init !== void 0) for (const archetype of this.archetypes) {
2229
- if (!this.archetypeMatchesHook(archetype, entry)) continue;
2230
- for (const entityId of archetype.getEntities()) {
2231
- const components = collectMultiHookComponents(this.createHooksContext(), entityId, componentTypes);
2232
- multiHook.on_init(entityId, ...components);
2233
- }
2234
- }
2235
- return () => {
2236
- this.hooks.delete(entry);
2237
- for (const archetype of this.archetypes) archetype.matchingMultiHooks.delete(entry);
2238
- };
2239
- } else {
2240
- const componentType = componentTypesOrSingle;
2241
- if (!this.legacyHooks.has(componentType)) this.legacyHooks.set(componentType, /* @__PURE__ */ new Set());
2242
- const legacyHook = hook;
2243
- this.legacyHooks.get(componentType).add(legacyHook);
2244
- if (legacyHook.on_init !== void 0) this.archetypesByComponent.get(componentType)?.forEach((archetype) => {
2245
- const entities = archetype.getEntityToIndexMap();
2246
- const componentData = archetype.getComponentData(componentType);
2247
- for (const [entity, index] of entities) {
2248
- const data = componentData[index];
2249
- const value = data === MISSING_COMPONENT ? void 0 : data;
2250
- legacyHook.on_init?.(entity, componentType, value);
2251
- }
2252
- });
2253
- return () => {
2254
- const hooks = this.legacyHooks.get(componentType);
2255
- if (hooks) {
2256
- hooks.delete(legacyHook);
2257
- if (hooks.size === 0) this.legacyHooks.delete(componentType);
2258
- }
2259
- };
2260
- }
2261
- }
2262
- /** @deprecated use the unsubscribe function returned by hook() instead */
2263
- unhook(componentTypesOrSingle, hook) {
2264
- if (Array.isArray(componentTypesOrSingle)) {
2265
- for (const entry of this.hooks) if (entry.hook === hook) {
2266
- this.hooks.delete(entry);
2267
- for (const archetype of this.archetypes) archetype.matchingMultiHooks.delete(entry);
2268
- break;
2269
- }
2270
- } else {
2271
- const componentType = componentTypesOrSingle;
2272
- const hooks = this.legacyHooks.get(componentType);
2273
- if (hooks) {
2274
- hooks.delete(hook);
2275
- if (hooks.size === 0) this.legacyHooks.delete(componentType);
2276
- }
2277
- }
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
+ };
2278
2634
  }
2279
2635
  /**
2280
2636
  * Synchronizes all buffered commands (set/remove/delete) to the world.
@@ -2321,18 +2677,7 @@ var World = class {
2321
2677
  const sortedTypes = normalizeComponentTypes(componentTypes);
2322
2678
  const filterKey = serializeQueryFilter(filter);
2323
2679
  const key = `${this.createArchetypeSignature(sortedTypes)}${filterKey ? `|${filterKey}` : ""}`;
2324
- const cached = this.queryCache.get(key);
2325
- if (cached) {
2326
- cached.refCount++;
2327
- return cached.query;
2328
- }
2329
- const query = new Query(this, sortedTypes, filter);
2330
- query._cacheKey = key;
2331
- this.queryCache.set(key, {
2332
- query,
2333
- refCount: 1
2334
- });
2335
- return query;
2680
+ return this.queryRegistry.getOrCreate(this, sortedTypes, key, filter);
2336
2681
  }
2337
2682
  /**
2338
2683
  * Creates a new entity builder for fluent entity configuration.
@@ -2374,13 +2719,6 @@ var World = class {
2374
2719
  }
2375
2720
  return entities;
2376
2721
  }
2377
- _registerQuery(query) {
2378
- this.queries.push(query);
2379
- }
2380
- _unregisterQuery(query) {
2381
- const index = this.queries.indexOf(query);
2382
- if (index !== -1) this.queries.splice(index, 1);
2383
- }
2384
2722
  /**
2385
2723
  * Releases a cached query and frees its resources if no longer needed.
2386
2724
  * Call this when you're done using a query to allow the world to clean up its cache entry.
@@ -2393,16 +2731,7 @@ var World = class {
2393
2731
  * world.releaseQuery(query); // Optional cleanup
2394
2732
  */
2395
2733
  releaseQuery(query) {
2396
- const key = query._cacheKey;
2397
- if (!key) return;
2398
- const cached = this.queryCache.get(key);
2399
- if (!cached || cached.query !== query) return;
2400
- cached.refCount--;
2401
- if (cached.refCount <= 0) {
2402
- this.queryCache.delete(key);
2403
- this._unregisterQuery(query);
2404
- cached.query._disposeInternal();
2405
- }
2734
+ this.queryRegistry.release(query);
2406
2735
  }
2407
2736
  /**
2408
2737
  * Returns all archetypes that contain entities with the specified components.
@@ -2425,26 +2754,29 @@ var World = class {
2425
2754
  } else regularComponents.push(componentType);
2426
2755
  let matchingArchetypes = this.getArchetypesWithComponents(regularComponents);
2427
2756
  for (const { componentId, relationId } of wildcardRelations) {
2428
- const archetypesWithMarker = this.archetypesByComponent.get(relationId) || [];
2429
- 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));
2430
2760
  }
2431
2761
  return matchingArchetypes;
2432
2762
  }
2433
2763
  getArchetypesWithComponents(componentTypes) {
2434
2764
  if (componentTypes.length === 0) return [...this.archetypes];
2435
- if (componentTypes.length === 1) return this.archetypesByComponent.get(componentTypes[0]) || [];
2436
- const archetypeLists = componentTypes.map((type) => this.archetypesByComponent.get(type) || []).sort((a, b) => a.length - b.length);
2437
- const shortest = archetypeLists[0];
2438
- if (shortest.length === 0) return [];
2439
- if (archetypeLists.length === 2) {
2440
- const second = archetypeLists[1];
2441
- const secondSet = new Set(second);
2442
- return shortest.filter((a) => secondSet.has(a));
2443
- }
2444
- let result = new Set(shortest);
2445
- for (let i = 1; i < archetypeLists.length; i++) {
2446
- const listSet = new Set(archetypeLists[i]);
2447
- 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);
2448
2780
  if (result.size === 0) return [];
2449
2781
  }
2450
2782
  return Array.from(result);
@@ -2453,70 +2785,43 @@ var World = class {
2453
2785
  const matchingArchetypes = this.getMatchingArchetypes(componentTypes);
2454
2786
  if (includeComponents) {
2455
2787
  const result = [];
2456
- for (const archetype of matchingArchetypes) result.push(...archetype.getEntitiesWithComponents(componentTypes));
2788
+ for (const archetype of matchingArchetypes) archetype.appendEntitiesWithComponents(componentTypes, result);
2457
2789
  return result;
2458
2790
  } else {
2459
2791
  const result = [];
2460
- for (const archetype of matchingArchetypes) result.push(...archetype.getEntities());
2792
+ for (const archetype of matchingArchetypes) for (const entity of archetype.getEntities()) result.push(entity);
2461
2793
  return result;
2462
2794
  }
2463
2795
  }
2464
2796
  executeEntityCommands(entityId, commands) {
2465
- const changeset = this._changeset;
2466
- changeset.clear();
2467
- if (this.isComponentEntityId(entityId)) {
2468
- this.executeComponentEntityCommands(entityId, commands);
2469
- return changeset;
2797
+ this._changeset.clear();
2798
+ if (this.componentEntities.exists(entityId)) {
2799
+ this.componentEntities.executeCommands(entityId, commands);
2800
+ return;
2470
2801
  }
2471
2802
  if (commands.some((cmd) => cmd.type === "destroy")) {
2472
2803
  this.destroyEntityImmediate(entityId);
2473
- return changeset;
2804
+ return;
2474
2805
  }
2806
+ this.applyEntityCommands(entityId, commands);
2807
+ }
2808
+ applyEntityCommands(entityId, commands) {
2475
2809
  const currentArchetype = this.entityToArchetype.get(entityId);
2476
- if (!currentArchetype) return changeset;
2810
+ if (!currentArchetype) return;
2811
+ const changeset = this._changeset;
2477
2812
  processCommands(entityId, currentArchetype, commands, changeset, (eid, arch, compId) => {
2478
2813
  if (isExclusiveComponent(compId)) removeMatchingRelations(eid, arch, compId, changeset);
2479
2814
  });
2480
- const hasHooks = this.legacyHooks.size > 0 || this.hooks.size > 0;
2481
- const hasEntityRefs = changeset.removes.size > 0 || changeset.adds.size > 0;
2482
- if (!hasHooks) {
2483
- applyChangesetNoHooks(this._commandCtx, entityId, currentArchetype, changeset, this.entityToArchetype);
2484
- if (hasEntityRefs) this.updateEntityReferences(entityId, changeset);
2485
- return changeset;
2486
- }
2487
- const { removedComponents, newArchetype } = applyChangeset(this._commandCtx, entityId, currentArchetype, changeset, this.entityToArchetype);
2488
- if (hasEntityRefs) this.updateEntityReferences(entityId, changeset);
2489
- triggerLifecycleHooks(this.createHooksContext(), entityId, changeset.adds, removedComponents, currentArchetype, newArchetype);
2490
- return changeset;
2491
- }
2492
- executeComponentEntityCommands(entityId, commands) {
2493
- if (commands.some((cmd) => cmd.type === "destroy")) {
2494
- 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);
2495
2819
  return;
2496
2820
  }
2497
- const pendingSetValues = /* @__PURE__ */ new Map();
2498
- for (const command of commands) if (command.type === "set" && command.componentType) {
2499
- const merge = getComponentMerge(command.componentType);
2500
- let nextValue = command.component;
2501
- if (merge !== void 0 && pendingSetValues.has(command.componentType)) nextValue = merge(pendingSetValues.get(command.componentType), command.component);
2502
- pendingSetValues.set(command.componentType, nextValue);
2503
- this.getComponentEntityComponents(entityId, true).set(command.componentType, nextValue);
2504
- } else if (command.type === "delete" && command.componentType) {
2505
- const data = this.componentEntityComponents.get(entityId);
2506
- if (isWildcardRelationId(command.componentType)) {
2507
- const componentId = getComponentIdFromRelationId(command.componentType);
2508
- if (componentId !== void 0) {
2509
- if (data) {
2510
- for (const key of Array.from(data.keys())) if (getComponentIdFromRelationId(key) === componentId) data.delete(key);
2511
- }
2512
- for (const key of Array.from(pendingSetValues.keys())) if (getComponentIdFromRelationId(key) === componentId) pendingSetValues.delete(key);
2513
- }
2514
- } else {
2515
- data?.delete(command.componentType);
2516
- pendingSetValues.delete(command.componentType);
2517
- }
2518
- if (data?.size === 0) this.clearComponentEntityComponents(entityId);
2519
- }
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);
2520
2825
  }
2521
2826
  createHooksContext() {
2522
2827
  return this._hooksCtx;
@@ -2524,11 +2829,12 @@ var World = class {
2524
2829
  removeComponentImmediate(entityId, componentType, targetEntityId) {
2525
2830
  const sourceArchetype = this.entityToArchetype.get(entityId);
2526
2831
  if (!sourceArchetype) return;
2527
- const changeset = new ComponentChangeset();
2832
+ const changeset = this._removeChangeset;
2833
+ changeset.clear();
2528
2834
  changeset.delete(componentType);
2529
2835
  maybeRemoveWildcardMarker(entityId, sourceArchetype, componentType, getComponentIdFromRelationId(componentType), changeset);
2530
2836
  const removedComponent = sourceArchetype.get(entityId, componentType);
2531
- const { newArchetype } = applyChangeset(this._commandCtx, entityId, sourceArchetype, changeset, this.entityToArchetype);
2837
+ const newArchetype = applyChangeset(this._commandCtx, entityId, sourceArchetype, changeset, this.entityToArchetype, null);
2532
2838
  untrackEntityReference(this.entityReferences, entityId, componentType, targetEntityId);
2533
2839
  triggerLifecycleHooks(this.createHooksContext(), entityId, /* @__PURE__ */ new Map(), new Map([[componentType, removedComponent]]), sourceArchetype, newArchetype);
2534
2840
  }
@@ -2547,20 +2853,56 @@ var World = class {
2547
2853
  const hashKey = this.createArchetypeSignature(sortedTypes);
2548
2854
  return getOrCompute(this.archetypeBySignature, hashKey, () => this.createNewArchetype(sortedTypes));
2549
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
+ }
2550
2885
  createNewArchetype(componentTypes) {
2551
- const newArchetype = new Archetype(componentTypes, this.dontFragmentRelations);
2886
+ const newArchetype = new Archetype(componentTypes, this.dontFragmentStore);
2552
2887
  this.archetypes.push(newArchetype);
2553
2888
  for (const componentType of componentTypes) {
2554
- const archetypes = this.archetypesByComponent.get(componentType) || [];
2555
- archetypes.push(newArchetype);
2556
- 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);
2557
2896
  }
2558
- for (const query of this.queries) query.checkNewArchetype(newArchetype);
2897
+ this.queryRegistry.onNewArchetype(newArchetype);
2559
2898
  this.updateArchetypeHookMatches(newArchetype);
2560
2899
  return newArchetype;
2561
2900
  }
2562
2901
  updateArchetypeHookMatches(archetype) {
2563
- 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
+ }
2564
2906
  }
2565
2907
  archetypeMatchesHook(archetype, entry) {
2566
2908
  return entry.requiredComponents.every((c) => {
@@ -2572,30 +2914,29 @@ var World = class {
2572
2914
  return archetype.componentTypeSet.has(c) || isDontFragmentRelation(c);
2573
2915
  }) && matchesFilter(archetype, entry.filter);
2574
2916
  }
2575
- archetypeReferencesEntity(archetype, entityId) {
2576
- return archetype.componentTypes.some((ct) => ct === entityId || isEntityRelation(ct) && getTargetIdFromRelationId(ct) === entityId);
2577
- }
2578
2917
  cleanupArchetypesReferencingEntity(entityId) {
2579
- for (let i = this.archetypes.length - 1; i >= 0; i--) {
2580
- const archetype = this.archetypes[i];
2581
- if (archetype.getEntities().length === 0 && this.archetypeReferencesEntity(archetype, entityId)) this.removeArchetype(archetype);
2582
- }
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);
2583
2922
  }
2584
2923
  removeArchetype(archetype) {
2585
2924
  const index = this.archetypes.indexOf(archetype);
2586
- 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
+ }
2587
2930
  this.archetypeBySignature.delete(this.createArchetypeSignature(archetype.componentTypes));
2588
2931
  for (const componentType of archetype.componentTypes) {
2589
2932
  const archetypes = this.archetypesByComponent.get(componentType);
2590
2933
  if (archetypes) {
2591
- const compIndex = archetypes.indexOf(archetype);
2592
- if (compIndex !== -1) {
2593
- archetypes.splice(compIndex, 1);
2594
- if (archetypes.length === 0) this.archetypesByComponent.delete(componentType);
2595
- }
2934
+ archetypes.delete(archetype);
2935
+ if (archetypes.size === 0) this.archetypesByComponent.delete(componentType);
2596
2936
  }
2937
+ this.removeFromReferencingIndex(componentType, archetype);
2597
2938
  }
2598
- for (const query of this.queries) query.removeArchetype(archetype);
2939
+ this.queryRegistry.onArchetypeRemoved(archetype);
2599
2940
  }
2600
2941
  /**
2601
2942
  * Serializes the entire world state to a plain JavaScript object.
@@ -2619,31 +2960,7 @@ var World = class {
2619
2960
  * const newWorld = new World(savedData);
2620
2961
  */
2621
2962
  serialize() {
2622
- const entities = [];
2623
- for (const archetype of this.archetypes) {
2624
- const dumpedEntities = archetype.dump();
2625
- for (const { entity, components } of dumpedEntities) entities.push({
2626
- id: encodeEntityId(entity),
2627
- components: Array.from(components.entries()).map(([rawType, value]) => ({
2628
- type: encodeEntityId(rawType),
2629
- value: value === MISSING_COMPONENT ? void 0 : value
2630
- }))
2631
- });
2632
- }
2633
- const componentEntities = [];
2634
- for (const [entityId, components] of this.componentEntityComponents.entries()) componentEntities.push({
2635
- id: encodeEntityId(entityId),
2636
- components: Array.from(components.entries()).map(([rawType, value]) => ({
2637
- type: encodeEntityId(rawType),
2638
- value: value === MISSING_COMPONENT ? void 0 : value
2639
- }))
2640
- });
2641
- return {
2642
- version: 1,
2643
- entityManager: this.entityIdManager.serializeState(),
2644
- entities,
2645
- componentEntities
2646
- };
2963
+ return serializeWorld(this.archetypes, this.componentEntities, this.entityIdManager);
2647
2964
  }
2648
2965
  };
2649
2966