@codehz/ecs 0.3.4 → 0.3.6

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/archetype.d.ts CHANGED
@@ -117,6 +117,14 @@ export declare class Archetype {
117
117
  * @returns An array of component data or undefined if not present
118
118
  */
119
119
  getOptionalComponentData<T>(componentType: EntityId<T>): T[] | undefined;
120
+ /**
121
+ * Helper: compute or return cached data sources for provided componentTypes
122
+ */
123
+ private getCachedComponentDataSources;
124
+ /**
125
+ * Helper: build component tuples for a specific entity index using precomputed data sources
126
+ */
127
+ private buildComponentsForIndex;
120
128
  /**
121
129
  * Get entities with their component data for specified component types
122
130
  * Optimized for bulk component access with pre-computed indices
@@ -127,6 +135,15 @@ export declare class Archetype {
127
135
  entity: EntityId;
128
136
  components: ComponentTuple<T>;
129
137
  }>;
138
+ /**
139
+ * Iterate over entities with their component data for specified component types
140
+ * implemented as a generator returning each entity/components pair lazily
141
+ * @param componentTypes Array of component types to retrieve
142
+ */
143
+ iterateWithComponents<const T extends readonly ComponentType<any>[]>(componentTypes: T): IterableIterator<{
144
+ entity: EntityId;
145
+ components: ComponentTuple<T>;
146
+ }>;
130
147
  /**
131
148
  * Iterate over entities with their component data for specified component types
132
149
  * Optimized for bulk component access
package/index.js CHANGED
@@ -402,16 +402,9 @@ class Archetype {
402
402
  getOptionalComponentData(componentType) {
403
403
  return this.componentData.get(componentType);
404
404
  }
405
- getEntitiesWithComponents(componentTypes) {
406
- const result = [];
407
- this.forEachWithComponents(componentTypes, (entity, ...components) => {
408
- result.push({ entity, components });
409
- });
410
- return result;
411
- }
412
- forEachWithComponents(componentTypes, callback) {
405
+ getCachedComponentDataSources(componentTypes) {
413
406
  const cacheKey = componentTypes.map((id) => isOptionalEntityId(id) ? `opt(${id.optional})` : `${id}`).join(",");
414
- const componentDataSources = getOrComputeCache(this.componentDataSourcesCache, cacheKey, () => {
407
+ return getOrComputeCache(this.componentDataSourcesCache, cacheKey, () => {
415
408
  return componentTypes.map((compType) => {
416
409
  let optional = false;
417
410
  if (isOptionalEntityId(compType)) {
@@ -433,46 +426,67 @@ class Archetype {
433
426
  }
434
427
  });
435
428
  });
436
- for (let entityIndex = 0;entityIndex < this.entities.length; entityIndex++) {
437
- const entity = this.entities[entityIndex];
438
- const components = componentDataSources.map((dataSource, i) => {
439
- let compType = componentTypes[i];
440
- let optional = false;
441
- if (isOptionalEntityId(compType)) {
442
- compType = compType.optional;
443
- optional = true;
444
- }
445
- if (getIdType(compType) === "wildcard-relation") {
446
- if (dataSource === undefined) {
447
- if (optional) {
448
- return;
449
- } else {
450
- throw new Error(`No matching relations found for mandatory wildcard relation component type`);
451
- }
452
- }
453
- const matchingRelations = dataSource;
454
- const relations = [];
455
- for (const relType of matchingRelations) {
456
- const dataArray = this.getComponentData(relType);
457
- const data = dataArray[entityIndex];
458
- const decodedRel = decodeRelationId(relType);
459
- relations.push([decodedRel.targetId, data === MISSING_COMPONENT ? undefined : data]);
460
- }
461
- return optional ? { value: relations } : relations;
462
- } else {
463
- if (dataSource === undefined) {
464
- if (optional) {
465
- return;
466
- } else {
467
- throw new Error(`No matching relations found for mandatory wildcard relation component type`);
468
- }
429
+ }
430
+ buildComponentsForIndex(componentTypes, componentDataSources, entityIndex) {
431
+ return componentDataSources.map((dataSource, i) => {
432
+ let compType = componentTypes[i];
433
+ let optional = false;
434
+ if (isOptionalEntityId(compType)) {
435
+ compType = compType.optional;
436
+ optional = true;
437
+ }
438
+ if (getIdType(compType) === "wildcard-relation") {
439
+ if (dataSource === undefined) {
440
+ if (optional) {
441
+ return;
442
+ } else {
443
+ throw new Error(`No matching relations found for mandatory wildcard relation component type`);
469
444
  }
470
- const dataArray = dataSource;
445
+ }
446
+ const matchingRelations = dataSource;
447
+ const relations = [];
448
+ for (const relType of matchingRelations) {
449
+ const dataArray = this.getComponentData(relType);
471
450
  const data = dataArray[entityIndex];
472
- const result = data === MISSING_COMPONENT ? undefined : data;
473
- return optional ? { value: result } : result;
451
+ const decodedRel = decodeRelationId(relType);
452
+ relations.push([decodedRel.targetId, data === MISSING_COMPONENT ? undefined : data]);
474
453
  }
475
- });
454
+ return optional ? { value: relations } : relations;
455
+ } else {
456
+ if (dataSource === undefined) {
457
+ if (optional) {
458
+ return;
459
+ } else {
460
+ throw new Error(`No matching relations found for mandatory wildcard relation component type`);
461
+ }
462
+ }
463
+ const dataArray = dataSource;
464
+ const data = dataArray[entityIndex];
465
+ const result = data === MISSING_COMPONENT ? undefined : data;
466
+ return optional ? { value: result } : result;
467
+ }
468
+ });
469
+ }
470
+ getEntitiesWithComponents(componentTypes) {
471
+ const result = [];
472
+ this.forEachWithComponents(componentTypes, (entity, ...components) => {
473
+ result.push({ entity, components });
474
+ });
475
+ return result;
476
+ }
477
+ *iterateWithComponents(componentTypes) {
478
+ const componentDataSources = this.getCachedComponentDataSources(componentTypes);
479
+ for (let entityIndex = 0;entityIndex < this.entities.length; entityIndex++) {
480
+ const entity = this.entities[entityIndex];
481
+ const components = this.buildComponentsForIndex(componentTypes, componentDataSources, entityIndex);
482
+ yield { entity, components };
483
+ }
484
+ }
485
+ forEachWithComponents(componentTypes, callback) {
486
+ const componentDataSources = this.getCachedComponentDataSources(componentTypes);
487
+ for (let entityIndex = 0;entityIndex < this.entities.length; entityIndex++) {
488
+ const entity = this.entities[entityIndex];
489
+ const components = this.buildComponentsForIndex(componentTypes, componentDataSources, entityIndex);
476
490
  callback(entity, ...components);
477
491
  }
478
492
  }
@@ -756,6 +770,14 @@ class Query {
756
770
  archetype.forEachWithComponents(componentTypes, callback);
757
771
  }
758
772
  }
773
+ *iterate(componentTypes) {
774
+ if (this.isDisposed) {
775
+ throw new Error("Query has been disposed");
776
+ }
777
+ for (const archetype of this.cachedArchetypes) {
778
+ yield* archetype.iterateWithComponents(componentTypes);
779
+ }
780
+ }
759
781
  getComponentData(componentType) {
760
782
  if (this.isDisposed) {
761
783
  throw new Error("Query has been disposed");
@@ -884,6 +906,7 @@ class World {
884
906
  commandBuffer = new CommandBuffer((entityId, commands) => this.executeEntityCommands(entityId, commands));
885
907
  hooks = new Map;
886
908
  exclusiveComponents = new Set;
909
+ cascadeDeleteComponents = new Set;
887
910
  constructor(snapshot) {
888
911
  if (snapshot && typeof snapshot === "object") {
889
912
  if (snapshot.entityManager) {
@@ -953,14 +976,29 @@ class World {
953
976
  return entityId;
954
977
  }
955
978
  destroyEntityImmediate(entityId) {
956
- const archetype = this.entityToArchetype.get(entityId);
957
- if (!archetype) {
958
- return;
959
- }
960
- const componentReferences = this.getEntityReferences(entityId);
961
- for (const [sourceEntityId, componentType] of componentReferences) {
962
- const sourceArchetype = this.entityToArchetype.get(sourceEntityId);
963
- if (sourceArchetype) {
979
+ const queue = [entityId];
980
+ const visited = new Set;
981
+ while (queue.length > 0) {
982
+ const cur = queue.shift();
983
+ if (visited.has(cur))
984
+ continue;
985
+ visited.add(cur);
986
+ const archetype = this.entityToArchetype.get(cur);
987
+ if (!archetype) {
988
+ continue;
989
+ }
990
+ const componentReferences = Array.from(this.getEntityReferences(cur));
991
+ for (const [sourceEntityId, componentType] of componentReferences) {
992
+ const sourceArchetype = this.entityToArchetype.get(sourceEntityId);
993
+ if (!sourceArchetype)
994
+ continue;
995
+ const detailedType = getDetailedIdType(componentType);
996
+ if (detailedType.type === "entity-relation" && this.cascadeDeleteComponents.has(detailedType.componentId)) {
997
+ if (!visited.has(sourceEntityId)) {
998
+ queue.push(sourceEntityId);
999
+ }
1000
+ continue;
1001
+ }
964
1002
  const currentComponents = new Map;
965
1003
  let removedComponent = sourceArchetype.get(sourceEntityId, componentType);
966
1004
  for (const archetypeComponentType of sourceArchetype.componentTypes) {
@@ -976,17 +1014,17 @@ class World {
976
1014
  }
977
1015
  newArchetype.addEntity(sourceEntityId, currentComponents);
978
1016
  this.entityToArchetype.set(sourceEntityId, newArchetype);
979
- this.untrackEntityReference(sourceEntityId, componentType, entityId);
1017
+ this.untrackEntityReference(sourceEntityId, componentType, cur);
980
1018
  this.triggerLifecycleHooks(sourceEntityId, new Map, new Map([[componentType, removedComponent]]));
981
1019
  }
1020
+ this.entityReferences.delete(cur);
1021
+ archetype.removeEntity(cur);
1022
+ if (archetype.getEntities().length === 0) {
1023
+ this.cleanupEmptyArchetype(archetype);
1024
+ }
1025
+ this.entityToArchetype.delete(cur);
1026
+ this.entityIdManager.deallocate(cur);
982
1027
  }
983
- this.entityReferences.delete(entityId);
984
- archetype.removeEntity(entityId);
985
- if (archetype.getEntities().length === 0) {
986
- this.cleanupEmptyArchetype(archetype);
987
- }
988
- this.entityToArchetype.delete(entityId);
989
- this.entityIdManager.deallocate(entityId);
990
1028
  }
991
1029
  exists(entityId) {
992
1030
  return this.entityToArchetype.has(entityId);
@@ -1066,6 +1104,9 @@ class World {
1066
1104
  setExclusive(componentId) {
1067
1105
  this.exclusiveComponents.add(componentId);
1068
1106
  }
1107
+ setCascadeDelete(componentId) {
1108
+ this.cascadeDeleteComponents.add(componentId);
1109
+ }
1069
1110
  update(...params) {
1070
1111
  const result = this.systemScheduler.update(...params);
1071
1112
  if (result instanceof Promise) {
@@ -1293,14 +1334,14 @@ class World {
1293
1334
  untrackEntityReference(sourceEntityId, componentType, targetEntityId) {
1294
1335
  const references = this.entityReferences.get(targetEntityId);
1295
1336
  if (references) {
1296
- references.get(sourceEntityId).delete(componentType);
1337
+ references.remove(sourceEntityId, componentType);
1297
1338
  if (references.keyCount === 0) {
1298
1339
  this.entityReferences.delete(targetEntityId);
1299
1340
  }
1300
1341
  }
1301
1342
  }
1302
1343
  getEntityReferences(targetEntityId) {
1303
- return this.entityReferences.get(targetEntityId) ?? [];
1344
+ return this.entityReferences.get(targetEntityId) ?? new MultiMap;
1304
1345
  }
1305
1346
  cleanupEmptyArchetype(archetype) {
1306
1347
  if (archetype.getEntities().length > 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codehz/ecs",
3
- "version": "0.3.4",
3
+ "version": "0.3.6",
4
4
  "type": "module",
5
5
  "main": "./index.js",
6
6
  "types": "./index.d.ts",
package/query.d.ts CHANGED
@@ -32,6 +32,14 @@ export declare class Query {
32
32
  * @param callback Function called for each entity with its components
33
33
  */
34
34
  forEach<const T extends readonly ComponentType<any>[]>(componentTypes: T, callback: (entity: EntityId, ...components: ComponentTuple<T>) => void): void;
35
+ /**
36
+ * Iterate over entities with their component data (generator)
37
+ * @param componentTypes Array of component types to retrieve
38
+ */
39
+ iterate<const T extends readonly ComponentType<any>[]>(componentTypes: T): IterableIterator<{
40
+ entity: EntityId;
41
+ components: ComponentTuple<T>;
42
+ }>;
35
43
  /**
36
44
  * Get component data arrays for all matching entities
37
45
  * @param componentType The component type to retrieve
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,