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