@codehz/ecs 0.7.6 → 0.8.1

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.
@@ -1,8 +1,41 @@
1
1
  import type { EntityId } from "../entity";
2
2
  import { MultiMap } from "../utils/multi-map";
3
3
 
4
+ /**
5
+ * Reverse reference index: maps each target entity to the set of (source entity, component) pairs
6
+ * that currently hold a reference to it.
7
+ *
8
+ * Used internally to support efficient entity deletion, including:
9
+ * - Fast-path deletion for unreferenced entities
10
+ * - Cascading deletes for relations marked with `cascadeDelete`
11
+ * - Automatic cleanup of entity-valued components and entity-relations when their target is destroyed
12
+ *
13
+ * Structure:
14
+ * targetEntityId -> MultiMap<sourceEntityId, componentOrRelationId>
15
+ *
16
+ * - For plain entity-valued components (component value is an EntityId):
17
+ * componentOrRelationId === the component type (which is also the entity id being pointed to)
18
+ * - For entity-relations (`relation(Comp, target)`):
19
+ * componentOrRelationId is the encoded (negative) relation ID
20
+ *
21
+ * This index is maintained in sync with structural changes via `updateEntityReferences` in World.
22
+ *
23
+ * @internal
24
+ */
4
25
  export type EntityReferencesMap = Map<EntityId, MultiMap<EntityId, EntityId>>;
5
26
 
27
+ /**
28
+ * Record that `sourceEntityId` holds a reference to `targetEntityId` via the given component/relation.
29
+ *
30
+ * Called when an entity-valued component or an entity-relation is added to an entity.
31
+ *
32
+ * @param entityReferences - The shared reverse index map
33
+ * @param sourceEntityId - The entity that contains the reference
34
+ * @param componentType - The component type or encoded relation ID used for the reference
35
+ * @param targetEntityId - The entity being referenced
36
+ *
37
+ * @internal
38
+ */
6
39
  export function trackEntityReference(
7
40
  entityReferences: EntityReferencesMap,
8
41
  sourceEntityId: EntityId,
@@ -15,6 +48,19 @@ export function trackEntityReference(
15
48
  entityReferences.get(targetEntityId)!.add(sourceEntityId, componentType);
16
49
  }
17
50
 
51
+ /**
52
+ * Remove the record that `sourceEntityId` references `targetEntityId` via the given component/relation.
53
+ *
54
+ * Called when an entity-valued component or entity-relation is removed (or during deletion).
55
+ * Automatically prunes empty target entries from the map.
56
+ *
57
+ * @param entityReferences - The shared reverse index map
58
+ * @param sourceEntityId - The entity that no longer holds the reference
59
+ * @param componentType - The component type or encoded relation ID that was used
60
+ * @param targetEntityId - The previously referenced entity
61
+ *
62
+ * @internal
63
+ */
18
64
  export function untrackEntityReference(
19
65
  entityReferences: EntityReferencesMap,
20
66
  sourceEntityId: EntityId,
@@ -30,6 +76,19 @@ export function untrackEntityReference(
30
76
  }
31
77
  }
32
78
 
79
+ /**
80
+ * Iterate over all (sourceEntityId, componentOrRelationId) pairs that currently reference the given target.
81
+ *
82
+ * Returns an empty iterable when the target has no incoming references.
83
+ * The returned iterable yields `[source, componentType]` pairs suitable for cleanup decisions
84
+ * (e.g. whether to cascade-delete the source or just remove the specific component/relation).
85
+ *
86
+ * @param entityReferences - The shared reverse index map
87
+ * @param targetEntityId - The entity whose referrers we want to inspect
88
+ * @returns Iterable of [sourceEntityId, componentOrRelationId]
89
+ *
90
+ * @internal
91
+ */
33
92
  export function getEntityReferences(
34
93
  entityReferences: EntityReferencesMap,
35
94
  targetEntityId: EntityId,
@@ -444,7 +444,10 @@ export class World {
444
444
  if (archetype.componentTypeSet.has(componentType)) return true;
445
445
 
446
446
  if (isDontFragmentRelation(componentType)) {
447
- return this.dontFragmentStore.get(entityId)?.has(componentType) ?? false;
447
+ // Use getValue; presence check via getAllForEntity only if value can legitimately be undefined
448
+ const val = this.dontFragmentStore.getValue(entityId, componentType);
449
+ if (val !== undefined) return true;
450
+ return this.dontFragmentStore.getAllForEntity(entityId).some(([t]) => t === componentType);
448
451
  }
449
452
 
450
453
  return false;
@@ -493,7 +496,11 @@ export class World {
493
496
  if (componentType >= 0 || componentType % RELATION_SHIFT !== 0) {
494
497
  const inArchetype = archetype.componentTypeSet.has(componentType);
495
498
  const hasDontFragment = isDontFragmentRelation(componentType);
496
- const hasComponent = inArchetype || (hasDontFragment && this.dontFragmentStore.get(entityId)?.has(componentType));
499
+ const hasComponent =
500
+ inArchetype ||
501
+ (hasDontFragment &&
502
+ (this.dontFragmentStore.getValue(entityId, componentType) !== undefined ||
503
+ this.dontFragmentStore.getAllForEntity(entityId).some(([t]) => t === componentType)));
497
504
 
498
505
  if (!hasComponent) {
499
506
  throw new Error(
@@ -698,25 +705,28 @@ export class World {
698
705
  * **Important:** Store the query reference and reuse it across frames for optimal performance.
699
706
  * Creating a new query each frame defeats the caching mechanism.
700
707
  *
701
- * @param componentTypes - Array of component types to match
702
- * @param filter - Optional filter for additional constraints (e.g., without specific components)
708
+ * **Note on optional components:** Only **required** (non-optional) component types should be
709
+ * passed to `createQuery`. Optional components (wrapped with `{ optional: ... }`) must be
710
+ * specified at **iteration time** via {@link Query.forEach}, {@link Query.getEntitiesWithComponents},
711
+ * or {@link Query.iterate} — NOT here. Including optional wrappers in `createQuery` will cause
712
+ * undefined behavior because the internal normalization relies on numeric sorting of component IDs.
713
+ *
714
+ * @param componentTypes - Array of **required** component types to match (do not include optional wrappers)
715
+ * @param filter - Optional filter for additional constraints (e.g., exclude entities with certain components)
703
716
  * @returns A Query instance that can be used to iterate matching entities
704
717
  *
705
718
  * @example
706
- * // Create once, reuse many times
719
+ * // Create once, reuse many times (required components only)
707
720
  * const movementQuery = world.createQuery([Position, Velocity]);
708
721
  *
709
- * // In game loop
710
- * movementQuery.forEach((entity) => {
711
- * const pos = world.get(entity, Position);
712
- * const vel = world.get(entity, Velocity);
713
- * pos.x += vel.x;
714
- * pos.y += vel.y;
722
+ * // Optional components are passed at iteration time, not creation time:
723
+ * movementQuery.forEach([Position, { optional: Velocity }], (entity, pos, vel) => {
724
+ * pos.x += vel?.value?.x ?? 0;
715
725
  * });
716
726
  *
717
727
  * // With filter
718
728
  * const activeQuery = world.createQuery([Position], {
719
- * without: [Disabled]
729
+ * negativeComponentTypes: [Disabled]
720
730
  * });
721
731
  */
722
732
  createQuery(componentTypes: EntityId<any>[], filter: QueryFilter = {}): Query {