@codehz/ecs 0.3.3 → 0.3.5

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/index.js CHANGED
@@ -594,6 +594,86 @@ class CommandBuffer {
594
594
  }
595
595
  }
596
596
 
597
+ // src/multi-map.ts
598
+ class MultiMap {
599
+ map = new Map;
600
+ _valueCount = 0;
601
+ get valueCount() {
602
+ return this._valueCount;
603
+ }
604
+ get keyCount() {
605
+ return this.map.size;
606
+ }
607
+ hasKey(key) {
608
+ return this.map.has(key);
609
+ }
610
+ has(key, value) {
611
+ const set = this.map.get(key);
612
+ if (!set)
613
+ return false;
614
+ if (arguments.length === 1)
615
+ return true;
616
+ return set.has(value);
617
+ }
618
+ add(key, value) {
619
+ let set = this.map.get(key);
620
+ if (!set) {
621
+ set = new Set;
622
+ this.map.set(key, set);
623
+ }
624
+ if (!set.has(value)) {
625
+ set.add(value);
626
+ this._valueCount++;
627
+ }
628
+ }
629
+ remove(key, value) {
630
+ const set = this.map.get(key);
631
+ if (!set)
632
+ return false;
633
+ if (!set.has(value))
634
+ return false;
635
+ set.delete(value);
636
+ this._valueCount--;
637
+ if (set.size === 0)
638
+ this.map.delete(key);
639
+ return true;
640
+ }
641
+ deleteKey(key) {
642
+ const set = this.map.get(key);
643
+ if (!set)
644
+ return false;
645
+ this._valueCount -= set.size;
646
+ this.map.delete(key);
647
+ return true;
648
+ }
649
+ get(key) {
650
+ const set = this.map.get(key);
651
+ return set ? new Set(set) : new Set;
652
+ }
653
+ *keys() {
654
+ yield* this.map.keys();
655
+ }
656
+ *values() {
657
+ for (const set of this.map.values()) {
658
+ for (const v of set)
659
+ yield v;
660
+ }
661
+ }
662
+ [Symbol.iterator]() {
663
+ return this.entries();
664
+ }
665
+ *entries() {
666
+ for (const [k, set] of this.map.entries()) {
667
+ for (const v of set)
668
+ yield [k, v];
669
+ }
670
+ }
671
+ clear() {
672
+ this.map.clear();
673
+ this._valueCount = 0;
674
+ }
675
+ }
676
+
597
677
  // src/query-filter.ts
598
678
  function serializeQueryFilter(filter = {}) {
599
679
  const negative = (filter.negativeComponentTypes || []).slice().sort((a, b) => a - b);
@@ -804,6 +884,7 @@ class World {
804
884
  commandBuffer = new CommandBuffer((entityId, commands) => this.executeEntityCommands(entityId, commands));
805
885
  hooks = new Map;
806
886
  exclusiveComponents = new Set;
887
+ cascadeDeleteComponents = new Set;
807
888
  constructor(snapshot) {
808
889
  if (snapshot && typeof snapshot === "object") {
809
890
  if (snapshot.entityManager) {
@@ -873,14 +954,29 @@ class World {
873
954
  return entityId;
874
955
  }
875
956
  destroyEntityImmediate(entityId) {
876
- const archetype = this.entityToArchetype.get(entityId);
877
- if (!archetype) {
878
- return;
879
- }
880
- const componentReferences = this.getEntityReferences(entityId);
881
- for (const { sourceEntityId, componentType } of componentReferences) {
882
- const sourceArchetype = this.entityToArchetype.get(sourceEntityId);
883
- if (sourceArchetype) {
957
+ const queue = [entityId];
958
+ const visited = new Set;
959
+ while (queue.length > 0) {
960
+ const cur = queue.shift();
961
+ if (visited.has(cur))
962
+ continue;
963
+ visited.add(cur);
964
+ const archetype = this.entityToArchetype.get(cur);
965
+ if (!archetype) {
966
+ continue;
967
+ }
968
+ const componentReferences = Array.from(this.getEntityReferences(cur));
969
+ for (const [sourceEntityId, componentType] of componentReferences) {
970
+ const sourceArchetype = this.entityToArchetype.get(sourceEntityId);
971
+ if (!sourceArchetype)
972
+ continue;
973
+ const detailedType = getDetailedIdType(componentType);
974
+ if (detailedType.type === "entity-relation" && this.cascadeDeleteComponents.has(detailedType.componentId)) {
975
+ if (!visited.has(sourceEntityId)) {
976
+ queue.push(sourceEntityId);
977
+ }
978
+ continue;
979
+ }
884
980
  const currentComponents = new Map;
885
981
  let removedComponent = sourceArchetype.get(sourceEntityId, componentType);
886
982
  for (const archetypeComponentType of sourceArchetype.componentTypes) {
@@ -896,17 +992,17 @@ class World {
896
992
  }
897
993
  newArchetype.addEntity(sourceEntityId, currentComponents);
898
994
  this.entityToArchetype.set(sourceEntityId, newArchetype);
899
- this.untrackEntityReference(sourceEntityId, componentType, entityId);
995
+ this.untrackEntityReference(sourceEntityId, componentType, cur);
900
996
  this.triggerLifecycleHooks(sourceEntityId, new Map, new Map([[componentType, removedComponent]]));
901
997
  }
998
+ this.entityReferences.delete(cur);
999
+ archetype.removeEntity(cur);
1000
+ if (archetype.getEntities().length === 0) {
1001
+ this.cleanupEmptyArchetype(archetype);
1002
+ }
1003
+ this.entityToArchetype.delete(cur);
1004
+ this.entityIdManager.deallocate(cur);
902
1005
  }
903
- this.entityReferences.delete(entityId);
904
- archetype.removeEntity(entityId);
905
- if (archetype.getEntities().length === 0) {
906
- this.cleanupEmptyArchetype(archetype);
907
- }
908
- this.entityToArchetype.delete(entityId);
909
- this.entityIdManager.deallocate(entityId);
910
1006
  }
911
1007
  exists(entityId) {
912
1008
  return this.entityToArchetype.has(entityId);
@@ -986,6 +1082,9 @@ class World {
986
1082
  setExclusive(componentId) {
987
1083
  this.exclusiveComponents.add(componentId);
988
1084
  }
1085
+ setCascadeDelete(componentId) {
1086
+ this.cascadeDeleteComponents.add(componentId);
1087
+ }
989
1088
  update(...params) {
990
1089
  const result = this.systemScheduler.update(...params);
991
1090
  if (result instanceof Promise) {
@@ -1206,26 +1305,21 @@ class World {
1206
1305
  }
1207
1306
  trackEntityReference(sourceEntityId, componentType, targetEntityId) {
1208
1307
  if (!this.entityReferences.has(targetEntityId)) {
1209
- this.entityReferences.set(targetEntityId, new Set);
1308
+ this.entityReferences.set(targetEntityId, new MultiMap);
1210
1309
  }
1211
- this.entityReferences.get(targetEntityId).add({ sourceEntityId, componentType });
1310
+ this.entityReferences.get(targetEntityId).add(sourceEntityId, componentType);
1212
1311
  }
1213
1312
  untrackEntityReference(sourceEntityId, componentType, targetEntityId) {
1214
1313
  const references = this.entityReferences.get(targetEntityId);
1215
1314
  if (references) {
1216
- references.forEach((reference) => {
1217
- if (reference.sourceEntityId === sourceEntityId && reference.componentType === componentType) {
1218
- references.delete(reference);
1219
- }
1220
- });
1221
- if (references.size === 0) {
1315
+ references.remove(sourceEntityId, componentType);
1316
+ if (references.keyCount === 0) {
1222
1317
  this.entityReferences.delete(targetEntityId);
1223
1318
  }
1224
1319
  }
1225
1320
  }
1226
1321
  getEntityReferences(targetEntityId) {
1227
- const references = this.entityReferences.get(targetEntityId);
1228
- return references ? Array.from(references) : [];
1322
+ return this.entityReferences.get(targetEntityId) ?? new MultiMap;
1229
1323
  }
1230
1324
  cleanupEmptyArchetype(archetype) {
1231
1325
  if (archetype.getEntities().length > 0) {
package/multi-map.d.ts ADDED
@@ -0,0 +1,19 @@
1
+ declare class MultiMap<K, V> {
2
+ private map;
3
+ private _valueCount;
4
+ get valueCount(): number;
5
+ get keyCount(): number;
6
+ hasKey(key: K): boolean;
7
+ has(key: K, value?: V): boolean;
8
+ add(key: K, value: V): void;
9
+ remove(key: K, value: V): boolean;
10
+ deleteKey(key: K): boolean;
11
+ get(key: K): Set<V>;
12
+ keys(): IterableIterator<K>;
13
+ values(): IterableIterator<V>;
14
+ [Symbol.iterator](): IterableIterator<[K, V]>;
15
+ entries(): IterableIterator<[K, V]>;
16
+ clear(): void;
17
+ }
18
+ export { MultiMap };
19
+ export type { MultiMap as MultiMapType };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codehz/ecs",
3
- "version": "0.3.3",
3
+ "version": "0.3.5",
4
4
  "type": "module",
5
5
  "main": "./index.js",
6
6
  "types": "./index.d.ts",
package/world.d.ts CHANGED
@@ -35,6 +35,8 @@ export declare class World<UpdateParams extends any[] = []> {
35
35
  private hooks;
36
36
  /** Set of component IDs marked as exclusive relations */
37
37
  private exclusiveComponents;
38
+ /** Set of component IDs that will cascade delete when the relation target is deleted */
39
+ private cascadeDeleteComponents;
38
40
  /**
39
41
  * Create a new World.
40
42
  * If an optional snapshot object is provided (previously produced by `world.serialize()`),
@@ -108,6 +110,13 @@ export declare class World<UpdateParams extends any[] = []> {
108
110
  * For exclusive relations, an entity can have at most one relation per base component
109
111
  */
110
112
  setExclusive(componentId: EntityId): void;
113
+ /**
114
+ * Mark a component as cascade-delete relation
115
+ * For cascade relations, when the relation target entity is deleted,
116
+ * the referencing entity will also be deleted (cascade).
117
+ * Only applicable to entity-relation components
118
+ */
119
+ setCascadeDelete(componentId: EntityId): void;
111
120
  /**
112
121
  * Update the world (run all systems in dependency order)
113
122
  * This function is synchronous when all systems are synchronous,
@@ -176,7 +185,7 @@ export declare class World<UpdateParams extends any[] = []> {
176
185
  /**
177
186
  * Get all component references where a target entity is used as a component type
178
187
  * @param targetEntityId The target entity
179
- * @returns Array of {sourceEntityId, componentType} pairs
188
+ * @returns A MultiMap of sourceEntityId to componentTypes that reference the target entity
180
189
  */
181
190
  private getEntityReferences;
182
191
  /**