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