@codehz/ecs 0.3.8 → 0.3.9

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.
Files changed (4) hide show
  1. package/index.d.mts +161 -151
  2. package/index.mjs +391 -174
  3. package/index.mjs.map +1 -1
  4. package/package.json +1 -1
package/index.mjs CHANGED
@@ -1,8 +1,4 @@
1
1
  //#region src/entity.ts
2
- /**
3
- * Constants for ID ranges
4
- */
5
- const INVALID_COMPONENT_ID = 0;
6
2
  const COMPONENT_ID_MAX = 1023;
7
3
  const ENTITY_ID_START = 1024;
8
4
  /**
@@ -10,23 +6,6 @@ const ENTITY_ID_START = 1024;
10
6
  */
11
7
  const RELATION_SHIFT = 2 ** 42;
12
8
  const WILDCARD_TARGET_ID = 0;
13
- /**
14
- * Create a component ID
15
- * @param id Component identifier (1-1023)
16
- * @see component
17
- */
18
- function createComponentId(id) {
19
- if (id < 1 || id > COMPONENT_ID_MAX) throw new Error(`Component ID must be between 1 and ${COMPONENT_ID_MAX}`);
20
- return id;
21
- }
22
- /**
23
- * Create an entity ID
24
- * @param id Entity identifier (starting from 1024)
25
- */
26
- function createEntityId(id) {
27
- if (id < ENTITY_ID_START) throw new Error(`Entity ID must be ${ENTITY_ID_START} or greater`);
28
- return id;
29
- }
30
9
  function relation(componentId, targetId) {
31
10
  if (!isComponentId(componentId)) throw new Error("First argument must be a valid component ID");
32
11
  let actualTargetId;
@@ -142,24 +121,6 @@ function getDetailedIdType(id) {
142
121
  return { type: "invalid" };
143
122
  }
144
123
  /**
145
- * Inspect an EntityId and return a human-readable string representation
146
- * @param id The EntityId to inspect
147
- * @returns A friendly string representation of the ID
148
- */
149
- function inspectEntityId(id) {
150
- if (id === INVALID_COMPONENT_ID) return "Invalid Component ID (0)";
151
- if (isComponentId(id)) return `Component ID (${id})`;
152
- if (isEntityId(id)) return `Entity ID (${id})`;
153
- if (isRelationId(id)) try {
154
- const decoded = decodeRelationId(id);
155
- if (!isComponentId(decoded.componentId) || decoded.type !== "wildcard" && !isEntityId(decoded.targetId) && !isComponentId(decoded.targetId)) return `Invalid Relation ID (${id})`;
156
- return `Relation ID: ${`Component ID (${decoded.componentId})`} -> ${decoded.type === "entity" ? `Entity ID (${decoded.targetId})` : decoded.type === "component" ? `Component ID (${decoded.targetId})` : "Wildcard (*)"}`;
157
- } catch (error) {
158
- return `Invalid Relation ID (${id})`;
159
- }
160
- return `Unknown ID (${id})`;
161
- }
162
- /**
163
124
  * Entity ID Manager for automatic allocation and freelist recycling
164
125
  */
165
126
  var EntityIdManager = class {
@@ -254,20 +215,36 @@ var ComponentIdAllocator = class {
254
215
  const globalComponentIdAllocator = new ComponentIdAllocator();
255
216
  const ComponentNames = /* @__PURE__ */ new Map();
256
217
  const ComponentIdForNames = /* @__PURE__ */ new Map();
218
+ const ComponentOptions = /* @__PURE__ */ new Map();
257
219
  /**
258
220
  * Allocate a new component ID from the global allocator.
259
- * Optionally register a name for the component.
260
- * The name is only for serialization/debugging and does not affect base functionality.
261
- * @param name Optional name for the component
221
+ * @param nameOrOptions Optional name for the component (for serialization/debugging) or options object
262
222
  * @returns The allocated component ID
223
+ * @example
224
+ * // Just a name
225
+ * const Position = component<Position>("Position");
226
+ *
227
+ * // With options
228
+ * const ChildOf = component({ exclusive: true, cascadeDelete: true });
229
+ *
230
+ * // With name and options
231
+ * const ChildOf = component({ name: "ChildOf", exclusive: true });
263
232
  */
264
- function component(name) {
233
+ function component(nameOrOptions) {
265
234
  const id = globalComponentIdAllocator.allocate();
235
+ let name;
236
+ let options;
237
+ if (typeof nameOrOptions === "string") name = nameOrOptions;
238
+ else if (typeof nameOrOptions === "object" && nameOrOptions !== null) {
239
+ options = nameOrOptions;
240
+ name = options.name;
241
+ }
266
242
  if (name) {
267
243
  if (ComponentIdForNames.has(name)) throw new Error(`Component name "${name}" is already registered`);
268
244
  ComponentNames.set(id, name);
269
245
  ComponentIdForNames.set(name, id);
270
246
  }
247
+ if (options) ComponentOptions.set(id, options);
271
248
  return id;
272
249
  }
273
250
  /**
@@ -285,6 +262,30 @@ function getComponentIdByName(name) {
285
262
  function getComponentNameById(id) {
286
263
  return ComponentNames.get(id);
287
264
  }
265
+ /**
266
+ * Check if a component is marked as exclusive
267
+ * @param id The component ID
268
+ * @returns true if the component is exclusive, false otherwise
269
+ */
270
+ function isExclusiveComponent(id) {
271
+ return ComponentOptions.get(id)?.exclusive ?? false;
272
+ }
273
+ /**
274
+ * Check if a component is marked as cascade delete
275
+ * @param id The component ID
276
+ * @returns true if the component is cascade delete, false otherwise
277
+ */
278
+ function isCascadeDeleteComponent(id) {
279
+ return ComponentOptions.get(id)?.cascadeDelete ?? false;
280
+ }
281
+ /**
282
+ * Check if a component is marked as dontFragment
283
+ * @param id The component ID
284
+ * @returns true if the component is dontFragment, false otherwise
285
+ */
286
+ function isDontFragmentComponent(id) {
287
+ return ComponentOptions.get(id)?.dontFragment ?? false;
288
+ }
288
289
 
289
290
  //#endregion
290
291
  //#region src/types.ts
@@ -358,6 +359,12 @@ var Archetype = class {
358
359
  */
359
360
  entityToIndex = /* @__PURE__ */ new Map();
360
361
  /**
362
+ * Reference to dontFragment relations storage from World
363
+ * This allows entities with different relation targets to share the same archetype
364
+ * Stored in World to avoid migration overhead when entities change archetypes
365
+ */
366
+ dontFragmentRelations;
367
+ /**
361
368
  * Cache for pre-computed component data sources to avoid repeated calculations
362
369
  * For regular components: data array
363
370
  * For wildcards: matching relation types array
@@ -366,9 +373,11 @@ var Archetype = class {
366
373
  /**
367
374
  * Create a new archetype with the specified component types
368
375
  * @param componentTypes The component types that define this archetype
376
+ * @param dontFragmentRelations Reference to the World's dontFragmentRelations storage
369
377
  */
370
- constructor(componentTypes) {
378
+ constructor(componentTypes, dontFragmentRelations) {
371
379
  this.componentTypes = [...componentTypes].sort((a, b) => a - b);
380
+ this.dontFragmentRelations = dontFragmentRelations;
372
381
  for (const componentType of this.componentTypes) this.componentData.set(componentType, []);
373
382
  }
374
383
  /**
@@ -389,7 +398,7 @@ var Archetype = class {
389
398
  /**
390
399
  * Add an entity to this archetype with initial component data
391
400
  * @param entityId The entity to add
392
- * @param componentData Map of component type to component data
401
+ * @param componentData Map of component type to component data (includes both regular and dontFragment components)
393
402
  */
394
403
  addEntity(entityId, componentData) {
395
404
  if (this.entityToIndex.has(entityId)) throw new Error(`Entity ${entityId} is already in this archetype`);
@@ -400,11 +409,18 @@ var Archetype = class {
400
409
  const data = componentData.get(componentType);
401
410
  this.getComponentData(componentType).push(data === void 0 ? MISSING_COMPONENT : data);
402
411
  }
412
+ const dontFragmentData = /* @__PURE__ */ new Map();
413
+ for (const [componentType, data] of componentData) {
414
+ if (this.componentTypes.includes(componentType)) continue;
415
+ const detailedType = getDetailedIdType(componentType);
416
+ if ((detailedType.type === "entity-relation" || detailedType.type === "component-relation") && isDontFragmentComponent(detailedType.componentId)) dontFragmentData.set(componentType, data);
417
+ }
418
+ if (dontFragmentData.size > 0) this.dontFragmentRelations.set(entityId, dontFragmentData);
403
419
  }
404
420
  /**
405
421
  * Get all component data for a specific entity
406
422
  * @param entityId The entity to get data for
407
- * @returns Map of component type to component data
423
+ * @returns Map of component type to component data (includes both regular and dontFragment components)
408
424
  */
409
425
  getEntity(entityId) {
410
426
  const index = this.entityToIndex.get(entityId);
@@ -414,11 +430,13 @@ var Archetype = class {
414
430
  const data = this.getComponentData(componentType)[index];
415
431
  entityData.set(componentType, data === MISSING_COMPONENT ? void 0 : data);
416
432
  }
433
+ const dontFragmentData = this.dontFragmentRelations.get(entityId);
434
+ if (dontFragmentData) for (const [componentType, data] of dontFragmentData) entityData.set(componentType, data);
417
435
  return entityData;
418
436
  }
419
437
  /**
420
438
  * Dump all entities and their component data in this archetype
421
- * @returns Array of objects with entity and component data
439
+ * @returns Array of objects with entity and component data (includes both regular and dontFragment components)
422
440
  */
423
441
  dump() {
424
442
  const result = [];
@@ -429,6 +447,8 @@ var Archetype = class {
429
447
  const data = this.getComponentData(componentType)[i];
430
448
  components.set(componentType, data === MISSING_COMPONENT ? void 0 : data);
431
449
  }
450
+ const dontFragmentData = this.dontFragmentRelations.get(entity);
451
+ if (dontFragmentData) for (const [componentType, data] of dontFragmentData) components.set(componentType, data);
432
452
  result.push({
433
453
  entity,
434
454
  components
@@ -439,7 +459,7 @@ var Archetype = class {
439
459
  /**
440
460
  * Remove an entity from this archetype
441
461
  * @param entityId The entity to remove
442
- * @returns The component data of the removed entity
462
+ * @returns The component data of the removed entity (includes both regular and dontFragment components)
443
463
  */
444
464
  removeEntity(entityId) {
445
465
  const index = this.entityToIndex.get(entityId);
@@ -449,6 +469,11 @@ var Archetype = class {
449
469
  const dataArray = this.getComponentData(componentType);
450
470
  removedData.set(componentType, dataArray[index]);
451
471
  }
472
+ const dontFragmentData = this.dontFragmentRelations.get(entityId);
473
+ if (dontFragmentData) {
474
+ for (const [componentType, data] of dontFragmentData) removedData.set(componentType, data);
475
+ this.dontFragmentRelations.delete(entityId);
476
+ }
452
477
  this.entityToIndex.delete(entityId);
453
478
  const lastIndex = this.entities.length - 1;
454
479
  if (index !== lastIndex) {
@@ -487,10 +512,20 @@ var Archetype = class {
487
512
  }
488
513
  }
489
514
  }
515
+ const dontFragmentData = this.dontFragmentRelations.get(entityId);
516
+ if (dontFragmentData) for (const [relType, data] of dontFragmentData) {
517
+ const relDetailed = getDetailedIdType(relType);
518
+ if ((relDetailed.type === "entity-relation" || relDetailed.type === "component-relation") && relDetailed.componentId === componentId) relations.push([relDetailed.targetId, data]);
519
+ }
490
520
  return relations;
491
521
  } else {
492
- const data = this.getComponentData(componentType)[index];
493
- return data === MISSING_COMPONENT ? void 0 : data;
522
+ if (this.componentTypes.includes(componentType)) {
523
+ const data = this.getComponentData(componentType)[index];
524
+ return data === MISSING_COMPONENT ? void 0 : data;
525
+ }
526
+ const dontFragmentData = this.dontFragmentRelations.get(entityId);
527
+ if (dontFragmentData && dontFragmentData.has(componentType)) return dontFragmentData.get(componentType);
528
+ throw new Error(`Component type ${componentType} not found for entity ${entityId}`);
494
529
  }
495
530
  }
496
531
  /**
@@ -500,11 +535,24 @@ var Archetype = class {
500
535
  * @param data The component data
501
536
  */
502
537
  set(entityId, componentType, data) {
503
- if (!this.componentData.has(componentType)) throw new Error(`Component type ${componentType} is not in this archetype`);
504
538
  const index = this.entityToIndex.get(entityId);
505
539
  if (index === void 0) throw new Error(`Entity ${entityId} is not in this archetype`);
506
- const dataArray = this.getComponentData(componentType);
507
- dataArray[index] = data;
540
+ if (this.componentData.has(componentType)) {
541
+ const dataArray = this.getComponentData(componentType);
542
+ dataArray[index] = data;
543
+ return;
544
+ }
545
+ const detailedType = getDetailedIdType(componentType);
546
+ if ((detailedType.type === "entity-relation" || detailedType.type === "component-relation") && isDontFragmentComponent(detailedType.componentId)) {
547
+ let dontFragmentData = this.dontFragmentRelations.get(entityId);
548
+ if (!dontFragmentData) {
549
+ dontFragmentData = /* @__PURE__ */ new Map();
550
+ this.dontFragmentRelations.set(entityId, dontFragmentData);
551
+ }
552
+ dontFragmentData.set(componentType, data);
553
+ return;
554
+ }
555
+ throw new Error(`Component type ${componentType} is not in this archetype`);
508
556
  }
509
557
  /**
510
558
  * Get all entities in this archetype
@@ -539,59 +587,82 @@ var Archetype = class {
539
587
  * Helper: compute or return cached data sources for provided componentTypes
540
588
  */
541
589
  getCachedComponentDataSources(componentTypes) {
542
- const cacheKey = componentTypes.map((id) => isOptionalEntityId(id) ? `opt(${id.optional})` : `${id}`).join(",");
543
- return getOrComputeCache(this.componentDataSourcesCache, cacheKey, () => {
544
- return componentTypes.map((compType) => {
545
- let optional = false;
546
- if (isOptionalEntityId(compType)) {
547
- compType = compType.optional;
548
- optional = true;
549
- }
550
- const detailedType = getDetailedIdType(compType);
551
- if (detailedType.type === "wildcard-relation") {
552
- const componentId = detailedType.componentId;
553
- const matchingRelations = this.componentTypes.filter((ct) => {
554
- const detailedCt = getDetailedIdType(ct);
555
- if (detailedCt.type !== "entity-relation" && detailedCt.type !== "component-relation") return false;
556
- return detailedCt.componentId === componentId;
557
- });
558
- return optional ? matchingRelations.length > 0 ? matchingRelations : void 0 : matchingRelations;
559
- } else return optional ? this.getOptionalComponentData(compType) : this.getComponentData(compType);
560
- });
590
+ const cacheKey = this.buildCacheKey(componentTypes);
591
+ return getOrComputeCache(this.componentDataSourcesCache, cacheKey, () => componentTypes.map((compType) => this.getComponentDataSource(compType)));
592
+ }
593
+ /**
594
+ * Build cache key for component types
595
+ */
596
+ buildCacheKey(componentTypes) {
597
+ return componentTypes.map((id) => isOptionalEntityId(id) ? `opt(${id.optional})` : `${id}`).join(",");
598
+ }
599
+ /**
600
+ * Get data source for a single component type
601
+ */
602
+ getComponentDataSource(compType) {
603
+ const optional = isOptionalEntityId(compType);
604
+ const actualType = optional ? compType.optional : compType;
605
+ const detailedType = getDetailedIdType(actualType);
606
+ if (detailedType.type === "wildcard-relation") return this.getWildcardRelationDataSource(detailedType.componentId, optional);
607
+ else return optional ? this.getOptionalComponentData(actualType) : this.getComponentData(actualType);
608
+ }
609
+ /**
610
+ * Get data source for wildcard relations
611
+ */
612
+ getWildcardRelationDataSource(componentId, optional) {
613
+ const matchingRelations = this.componentTypes.filter((ct) => {
614
+ const detailedCt = getDetailedIdType(ct);
615
+ return (detailedCt.type === "entity-relation" || detailedCt.type === "component-relation") && detailedCt.componentId === componentId;
561
616
  });
617
+ return optional ? matchingRelations.length > 0 ? matchingRelations : void 0 : matchingRelations;
562
618
  }
563
619
  /**
564
620
  * Helper: build component tuples for a specific entity index using precomputed data sources
565
621
  */
566
622
  buildComponentsForIndex(componentTypes, componentDataSources, entityIndex) {
567
623
  return componentDataSources.map((dataSource, i) => {
568
- let compType = componentTypes[i];
569
- let optional = false;
570
- if (isOptionalEntityId(compType)) {
571
- compType = compType.optional;
572
- optional = true;
573
- }
574
- if (getIdType(compType) === "wildcard-relation") {
575
- if (dataSource === void 0) if (optional) return;
576
- else throw new Error(`No matching relations found for mandatory wildcard relation component type`);
577
- const matchingRelations = dataSource;
578
- const relations = [];
579
- for (const relType of matchingRelations) {
580
- const data = this.getComponentData(relType)[entityIndex];
581
- const decodedRel = decodeRelationId(relType);
582
- relations.push([decodedRel.targetId, data === MISSING_COMPONENT ? void 0 : data]);
583
- }
584
- return optional ? { value: relations } : relations;
585
- } else {
586
- if (dataSource === void 0) if (optional) return;
587
- else throw new Error(`No matching relations found for mandatory wildcard relation component type`);
588
- const data = dataSource[entityIndex];
589
- const result = data === MISSING_COMPONENT ? void 0 : data;
590
- return optional ? { value: result } : result;
591
- }
624
+ const compType = componentTypes[i];
625
+ return this.buildSingleComponent(compType, dataSource, entityIndex);
592
626
  });
593
627
  }
594
628
  /**
629
+ * Build a single component value from its data source
630
+ */
631
+ buildSingleComponent(compType, dataSource, entityIndex) {
632
+ const optional = isOptionalEntityId(compType);
633
+ if (getIdType(optional ? compType.optional : compType) === "wildcard-relation") return this.buildWildcardRelationValue(dataSource, entityIndex, optional);
634
+ else return this.buildRegularComponentValue(dataSource, entityIndex, optional);
635
+ }
636
+ /**
637
+ * Build wildcard relation value from matching relations
638
+ */
639
+ buildWildcardRelationValue(dataSource, entityIndex, optional) {
640
+ if (dataSource === void 0) {
641
+ if (optional) return;
642
+ throw new Error(`No matching relations found for mandatory wildcard relation component type`);
643
+ }
644
+ const matchingRelations = dataSource;
645
+ const relations = [];
646
+ for (const relType of matchingRelations) {
647
+ const data = this.getComponentData(relType)[entityIndex];
648
+ const decodedRel = decodeRelationId(relType);
649
+ relations.push([decodedRel.targetId, data === MISSING_COMPONENT ? void 0 : data]);
650
+ }
651
+ return optional ? { value: relations } : relations;
652
+ }
653
+ /**
654
+ * Build regular component value from data source
655
+ */
656
+ buildRegularComponentValue(dataSource, entityIndex, optional) {
657
+ if (dataSource === void 0) {
658
+ if (optional) return;
659
+ throw new Error(`Component data not found for mandatory component type`);
660
+ }
661
+ const data = dataSource[entityIndex];
662
+ const result = data === MISSING_COMPONENT ? void 0 : data;
663
+ return optional ? { value: result } : result;
664
+ }
665
+ /**
595
666
  * Get entities with their component data for specified component types
596
667
  * Optimized for bulk component access with pre-computed indices
597
668
  * @param componentTypes Array of component types to retrieve
@@ -926,10 +997,16 @@ var Query = class {
926
997
  world._registerQuery(this);
927
998
  }
928
999
  /**
1000
+ * Check if query is disposed and throw error if so
1001
+ */
1002
+ ensureNotDisposed() {
1003
+ if (this.isDisposed) throw new Error("Query has been disposed");
1004
+ }
1005
+ /**
929
1006
  * Get all entities matching the query
930
1007
  */
931
1008
  getEntities() {
932
- if (this.isDisposed) throw new Error("Query has been disposed");
1009
+ this.ensureNotDisposed();
933
1010
  const result = [];
934
1011
  for (const archetype of this.cachedArchetypes) result.push(...archetype.getEntities());
935
1012
  return result;
@@ -940,7 +1017,7 @@ var Query = class {
940
1017
  * @returns Array of objects with entity and component data
941
1018
  */
942
1019
  getEntitiesWithComponents(componentTypes) {
943
- if (this.isDisposed) throw new Error("Query has been disposed");
1020
+ this.ensureNotDisposed();
944
1021
  const result = [];
945
1022
  for (const archetype of this.cachedArchetypes) {
946
1023
  const entitiesWithData = archetype.getEntitiesWithComponents(componentTypes);
@@ -954,7 +1031,7 @@ var Query = class {
954
1031
  * @param callback Function called for each entity with its components
955
1032
  */
956
1033
  forEach(componentTypes, callback) {
957
- if (this.isDisposed) throw new Error("Query has been disposed");
1034
+ this.ensureNotDisposed();
958
1035
  for (const archetype of this.cachedArchetypes) archetype.forEachWithComponents(componentTypes, callback);
959
1036
  }
960
1037
  /**
@@ -962,7 +1039,7 @@ var Query = class {
962
1039
  * @param componentTypes Array of component types to retrieve
963
1040
  */
964
1041
  *iterate(componentTypes) {
965
- if (this.isDisposed) throw new Error("Query has been disposed");
1042
+ this.ensureNotDisposed();
966
1043
  for (const archetype of this.cachedArchetypes) yield* archetype.iterateWithComponents(componentTypes);
967
1044
  }
968
1045
  /**
@@ -971,7 +1048,7 @@ var Query = class {
971
1048
  * @returns Array of component data for all matching entities
972
1049
  */
973
1050
  getComponentData(componentType) {
974
- if (this.isDisposed) throw new Error("Query has been disposed");
1051
+ this.ensureNotDisposed();
975
1052
  const result = [];
976
1053
  for (const archetype of this.cachedArchetypes) result.push(...archetype.getComponentData(componentType));
977
1054
  return result;
@@ -1119,6 +1196,8 @@ var World = class {
1119
1196
  archetypesByComponent = /* @__PURE__ */ new Map();
1120
1197
  /** Tracks which entities reference each entity as a component type */
1121
1198
  entityReferences = /* @__PURE__ */ new Map();
1199
+ /** Storage for dontFragment relations - maps entity ID to a map of relation type to component data */
1200
+ dontFragmentRelations = /* @__PURE__ */ new Map();
1122
1201
  /** Array of all active queries for archetype change notifications */
1123
1202
  queries = [];
1124
1203
  /** Cache for queries keyed by component types and filter signatures */
@@ -1129,10 +1208,6 @@ var World = class {
1129
1208
  commandBuffer = new CommandBuffer((entityId, commands) => this.executeEntityCommands(entityId, commands));
1130
1209
  /** Stores lifecycle hooks for component and relation events */
1131
1210
  hooks = /* @__PURE__ */ new Map();
1132
- /** Set of component IDs marked as exclusive relations */
1133
- exclusiveComponents = /* @__PURE__ */ new Set();
1134
- /** Set of component IDs that will cascade delete when the relation target is deleted */
1135
- cascadeDeleteComponents = /* @__PURE__ */ new Set();
1136
1211
  /**
1137
1212
  * Create a new World.
1138
1213
  * If an optional snapshot object is provided (previously produced by `world.serialize()`),
@@ -1214,7 +1289,7 @@ var World = class {
1214
1289
  const sourceArchetype = this.entityToArchetype.get(sourceEntityId);
1215
1290
  if (!sourceArchetype) continue;
1216
1291
  const detailedType = getDetailedIdType(componentType);
1217
- if (detailedType.type === "entity-relation" && this.cascadeDeleteComponents.has(detailedType.componentId)) {
1292
+ if (detailedType.type === "entity-relation" && isCascadeDeleteComponent(detailedType.componentId)) {
1218
1293
  if (!visited.has(sourceEntityId)) queue.push(sourceEntityId);
1219
1294
  continue;
1220
1295
  }
@@ -1271,13 +1346,20 @@ var World = class {
1271
1346
  */
1272
1347
  has(entityId, componentType) {
1273
1348
  const archetype = this.entityToArchetype.get(entityId);
1274
- return archetype ? archetype.componentTypes.includes(componentType) : false;
1349
+ if (!archetype) return false;
1350
+ if (archetype.componentTypes.includes(componentType)) return true;
1351
+ const detailedType = getDetailedIdType(componentType);
1352
+ if ((detailedType.type === "entity-relation" || detailedType.type === "component-relation") && isDontFragmentComponent(detailedType.componentId)) return this.dontFragmentRelations.get(entityId)?.has(componentType) ?? false;
1353
+ return false;
1275
1354
  }
1276
1355
  get(entityId, componentType) {
1277
1356
  const archetype = this.entityToArchetype.get(entityId);
1278
1357
  if (!archetype) throw new Error(`Entity ${entityId} does not exist`);
1279
- if (getDetailedIdType(componentType).type !== "wildcard-relation") {
1280
- if (!archetype.componentTypes.includes(componentType)) throw new Error(`Entity ${entityId} does not have component ${componentType}. Use has() to check component existence before calling get().`);
1358
+ const detailedType = getDetailedIdType(componentType);
1359
+ if (detailedType.type !== "wildcard-relation") {
1360
+ const inArchetype = archetype.componentTypes.includes(componentType);
1361
+ const isDontFragment = (detailedType.type === "entity-relation" || detailedType.type === "component-relation") && isDontFragmentComponent(detailedType.componentId);
1362
+ if (!(inArchetype || isDontFragment && this.dontFragmentRelations.get(entityId)?.has(componentType))) throw new Error(`Entity ${entityId} does not have component ${componentType}. Use has() to check component existence before calling get().`);
1281
1363
  }
1282
1364
  return archetype.get(entityId, componentType);
1283
1365
  }
@@ -1315,19 +1397,19 @@ var World = class {
1315
1397
  }
1316
1398
  /**
1317
1399
  * Mark a component as exclusive relation
1318
- * For exclusive relations, an entity can have at most one relation per base component
1400
+ * @deprecated This method has been removed. Use component options instead: component({ exclusive: true })
1401
+ * @throws Always throws an error directing to the new API
1319
1402
  */
1320
1403
  setExclusive(componentId) {
1321
- this.exclusiveComponents.add(componentId);
1404
+ throw new Error("setExclusive has been removed. Use component options instead: component({ exclusive: true })");
1322
1405
  }
1323
1406
  /**
1324
1407
  * Mark a component as cascade-delete relation
1325
- * For cascade relations, when the relation target entity is deleted,
1326
- * the referencing entity will also be deleted (cascade).
1327
- * Only applicable to entity-relation components
1408
+ * @deprecated This method has been removed. Use component options instead: component({ cascadeDelete: true })
1409
+ * @throws Always throws an error directing to the new API
1328
1410
  */
1329
1411
  setCascadeDelete(componentId) {
1330
- this.cascadeDeleteComponents.add(componentId);
1412
+ throw new Error("setCascadeDelete has been removed. Use component options instead: component({ cascadeDelete: true })");
1331
1413
  }
1332
1414
  /**
1333
1415
  * Update the world (run all systems in dependency order)
@@ -1462,76 +1544,187 @@ var World = class {
1462
1544
  }
1463
1545
  const currentArchetype = this.entityToArchetype.get(entityId);
1464
1546
  if (!currentArchetype) return changeset;
1465
- for (const command of commands) switch (command.type) {
1466
- case "set":
1467
- if (command.componentType) {
1468
- const detailedType = getDetailedIdType(command.componentType);
1469
- if ((detailedType.type === "entity-relation" || detailedType.type === "component-relation") && this.exclusiveComponents.has(detailedType.componentId)) for (const componentType of currentArchetype.componentTypes) {
1470
- const componentDetailedType = getDetailedIdType(componentType);
1471
- if ((componentDetailedType.type === "entity-relation" || componentDetailedType.type === "component-relation") && componentDetailedType.componentId === detailedType.componentId) changeset.delete(componentType);
1472
- }
1473
- changeset.set(command.componentType, command.component);
1474
- }
1475
- break;
1476
- case "delete":
1477
- if (command.componentType) {
1478
- const detailedType = getDetailedIdType(command.componentType);
1479
- if (detailedType.type === "wildcard-relation") {
1480
- const baseComponentId = detailedType.componentId;
1481
- for (const componentType of currentArchetype.componentTypes) {
1482
- const componentDetailedType = getDetailedIdType(componentType);
1483
- if (componentDetailedType.type === "entity-relation" || componentDetailedType.type === "component-relation") {
1484
- if (componentDetailedType.componentId === baseComponentId) changeset.delete(componentType);
1485
- }
1486
- }
1487
- } else changeset.delete(command.componentType);
1488
- }
1489
- break;
1547
+ this.processCommands(entityId, currentArchetype, commands, changeset);
1548
+ const removedComponents = this.applyChangeset(entityId, currentArchetype, changeset);
1549
+ this.updateEntityReferences(entityId, changeset);
1550
+ this.triggerLifecycleHooks(entityId, changeset.adds, removedComponents);
1551
+ return changeset;
1552
+ }
1553
+ /**
1554
+ * Process commands and populate the changeset
1555
+ */
1556
+ processCommands(entityId, currentArchetype, commands, changeset) {
1557
+ for (const command of commands) if (command.type === "set" && command.componentType) this.processSetCommand(entityId, currentArchetype, command.componentType, command.component, changeset);
1558
+ else if (command.type === "delete" && command.componentType) this.processDeleteCommand(entityId, currentArchetype, command.componentType, changeset);
1559
+ }
1560
+ /**
1561
+ * Process a set command, handling exclusive relations
1562
+ */
1563
+ processSetCommand(entityId, currentArchetype, componentType, component$1, changeset) {
1564
+ const detailedType = getDetailedIdType(componentType);
1565
+ if ((detailedType.type === "entity-relation" || detailedType.type === "component-relation") && isExclusiveComponent(detailedType.componentId)) this.removeExclusiveRelations(entityId, currentArchetype, detailedType.componentId, changeset);
1566
+ changeset.set(componentType, component$1);
1567
+ }
1568
+ /**
1569
+ * Remove all relations with the same base component (for exclusive relations)
1570
+ */
1571
+ removeExclusiveRelations(entityId, currentArchetype, baseComponentId, changeset) {
1572
+ for (const componentType of currentArchetype.componentTypes) if (this.isRelationWithComponent(componentType, baseComponentId)) changeset.delete(componentType);
1573
+ const entityData = currentArchetype.getEntity(entityId);
1574
+ if (entityData) for (const [componentType] of entityData) {
1575
+ if (currentArchetype.componentTypes.includes(componentType)) continue;
1576
+ if (this.isRelationWithComponent(componentType, baseComponentId)) changeset.delete(componentType);
1490
1577
  }
1491
- const finalComponentTypes = changeset.getFinalComponentTypes(currentArchetype.componentTypes);
1492
- const removedCompoents = /* @__PURE__ */ new Map();
1493
- if (finalComponentTypes) {
1494
- const newArchetype = this.ensureArchetype(finalComponentTypes);
1495
- const currentComponents = currentArchetype.removeEntity(entityId);
1496
- for (const componentType of changeset.removes) removedCompoents.set(componentType, currentComponents.get(componentType));
1497
- newArchetype.addEntity(entityId, changeset.applyTo(currentComponents));
1498
- this.entityToArchetype.set(entityId, newArchetype);
1499
- } else for (const [componentType, component$1] of changeset.adds) currentArchetype.set(entityId, componentType, component$1);
1578
+ }
1579
+ /**
1580
+ * Check if a component type is a relation with the given base component
1581
+ */
1582
+ isRelationWithComponent(componentType, baseComponentId) {
1583
+ const detailedType = getDetailedIdType(componentType);
1584
+ return (detailedType.type === "entity-relation" || detailedType.type === "component-relation") && detailedType.componentId === baseComponentId;
1585
+ }
1586
+ /**
1587
+ * Process a delete command, handling wildcard relations
1588
+ */
1589
+ processDeleteCommand(entityId, currentArchetype, componentType, changeset) {
1590
+ const detailedType = getDetailedIdType(componentType);
1591
+ if (detailedType.type === "wildcard-relation") this.removeWildcardRelations(entityId, currentArchetype, detailedType.componentId, changeset);
1592
+ else changeset.delete(componentType);
1593
+ }
1594
+ /**
1595
+ * Remove all relations matching a wildcard component ID
1596
+ */
1597
+ removeWildcardRelations(entityId, currentArchetype, baseComponentId, changeset) {
1598
+ for (const componentType of currentArchetype.componentTypes) if (this.isRelationWithComponent(componentType, baseComponentId)) changeset.delete(componentType);
1599
+ const entityData = currentArchetype.getEntity(entityId);
1600
+ if (entityData) for (const [componentType] of entityData) {
1601
+ if (currentArchetype.componentTypes.includes(componentType)) continue;
1602
+ if (this.isRelationWithComponent(componentType, baseComponentId)) changeset.delete(componentType);
1603
+ }
1604
+ }
1605
+ /**
1606
+ * Apply changeset to entity, moving to new archetype if needed
1607
+ * @returns Map of removed components with their data
1608
+ */
1609
+ applyChangeset(entityId, currentArchetype, changeset) {
1610
+ const currentEntityData = currentArchetype.getEntity(entityId);
1611
+ const allCurrentComponentTypes = currentEntityData ? Array.from(currentEntityData.keys()) : currentArchetype.componentTypes;
1612
+ const finalComponentTypes = changeset.getFinalComponentTypes(allCurrentComponentTypes);
1613
+ const removedComponents = /* @__PURE__ */ new Map();
1614
+ if (finalComponentTypes) this.moveEntityToNewArchetype(entityId, currentArchetype, finalComponentTypes, changeset, removedComponents);
1615
+ else this.updateEntityInSameArchetype(entityId, currentArchetype, changeset, removedComponents);
1616
+ return removedComponents;
1617
+ }
1618
+ /**
1619
+ * Move entity to a new archetype with updated components
1620
+ */
1621
+ moveEntityToNewArchetype(entityId, currentArchetype, finalComponentTypes, changeset, removedComponents) {
1622
+ const newArchetype = this.ensureArchetype(finalComponentTypes);
1623
+ const currentComponents = currentArchetype.removeEntity(entityId);
1624
+ for (const componentType of changeset.removes) removedComponents.set(componentType, currentComponents.get(componentType));
1625
+ newArchetype.addEntity(entityId, changeset.applyTo(currentComponents));
1626
+ this.entityToArchetype.set(entityId, newArchetype);
1627
+ if (currentArchetype.getEntities().length === 0) this.cleanupEmptyArchetype(currentArchetype);
1628
+ }
1629
+ /**
1630
+ * Update entity in same archetype (no archetype change needed)
1631
+ */
1632
+ updateEntityInSameArchetype(entityId, currentArchetype, changeset, removedComponents) {
1633
+ const currentComponents = currentArchetype.getEntity(entityId);
1634
+ const hasDontFragmentChanges = this.hasDontFragmentChanges(changeset);
1635
+ if (hasDontFragmentChanges) for (const componentType of changeset.removes) {
1636
+ const detailedType = getDetailedIdType(componentType);
1637
+ if ((detailedType.type === "entity-relation" || detailedType.type === "component-relation") && isDontFragmentComponent(detailedType.componentId)) removedComponents.set(componentType, currentComponents.get(componentType));
1638
+ }
1639
+ if (hasDontFragmentChanges) this.readdEntityWithUpdatedComponents(entityId, currentArchetype, currentComponents, changeset);
1640
+ else for (const [componentType, component$1] of changeset.adds) currentArchetype.set(entityId, componentType, component$1);
1641
+ }
1642
+ /**
1643
+ * Check if changeset contains dontFragment relation changes
1644
+ */
1645
+ hasDontFragmentChanges(changeset) {
1500
1646
  for (const componentType of changeset.removes) {
1501
1647
  const detailedType = getDetailedIdType(componentType);
1502
- if (detailedType.type === "entity-relation") {
1503
- const targetEntityId = detailedType.targetId;
1504
- this.untrackEntityReference(entityId, componentType, targetEntityId);
1505
- } else if (detailedType.type === "entity") this.untrackEntityReference(entityId, componentType, componentType);
1648
+ if ((detailedType.type === "entity-relation" || detailedType.type === "component-relation") && isDontFragmentComponent(detailedType.componentId)) return true;
1506
1649
  }
1507
- for (const [componentType, component$1] of changeset.adds) {
1650
+ for (const [componentType] of changeset.adds) {
1508
1651
  const detailedType = getDetailedIdType(componentType);
1509
- if (detailedType.type === "entity-relation") {
1510
- const targetEntityId = detailedType.targetId;
1511
- this.trackEntityReference(entityId, componentType, targetEntityId);
1512
- } else if (detailedType.type === "entity") this.trackEntityReference(entityId, componentType, componentType);
1652
+ if ((detailedType.type === "entity-relation" || detailedType.type === "component-relation") && isDontFragmentComponent(detailedType.componentId)) return true;
1653
+ }
1654
+ return false;
1655
+ }
1656
+ /**
1657
+ * Remove and re-add entity with updated components (for dontFragment changes)
1658
+ */
1659
+ readdEntityWithUpdatedComponents(entityId, archetype, currentComponents, changeset) {
1660
+ const newComponents = /* @__PURE__ */ new Map();
1661
+ for (const [ct, value] of currentComponents) if (!changeset.removes.has(ct)) newComponents.set(ct, value);
1662
+ for (const [ct, value] of changeset.adds) newComponents.set(ct, value);
1663
+ archetype.removeEntity(entityId);
1664
+ archetype.addEntity(entityId, newComponents);
1665
+ }
1666
+ /**
1667
+ * Update entity reference tracking based on changeset
1668
+ */
1669
+ updateEntityReferences(entityId, changeset) {
1670
+ for (const componentType of changeset.removes) {
1671
+ const detailedType = getDetailedIdType(componentType);
1672
+ if (detailedType.type === "entity-relation") this.untrackEntityReference(entityId, componentType, detailedType.targetId);
1673
+ else if (detailedType.type === "entity") this.untrackEntityReference(entityId, componentType, componentType);
1674
+ }
1675
+ for (const [componentType] of changeset.adds) {
1676
+ const detailedType = getDetailedIdType(componentType);
1677
+ if (detailedType.type === "entity-relation") this.trackEntityReference(entityId, componentType, detailedType.targetId);
1678
+ else if (detailedType.type === "entity") this.trackEntityReference(entityId, componentType, componentType);
1513
1679
  }
1514
- this.triggerLifecycleHooks(entityId, changeset.adds, removedCompoents);
1515
- return changeset;
1516
1680
  }
1517
1681
  /**
1518
1682
  * Get or create an archetype for the given component types
1519
- * @returns The archetype for the given component types
1683
+ * Filters out dontFragment relations from the archetype signature
1684
+ * @returns The archetype for the given component types (excluding dontFragment relations)
1520
1685
  */
1521
1686
  ensureArchetype(componentTypes) {
1522
- const sortedTypes = Array.from(componentTypes).sort((a, b) => a - b);
1687
+ const sortedTypes = this.filterRegularComponentTypes(componentTypes).sort((a, b) => a - b);
1523
1688
  const hashKey = this.createArchetypeSignature(sortedTypes);
1524
- return getOrCreateWithSideEffect(this.archetypeBySignature, hashKey, () => {
1525
- const newArchetype = new Archetype(sortedTypes);
1526
- this.archetypes.push(newArchetype);
1527
- for (const componentType of sortedTypes) {
1528
- const archetypes = this.archetypesByComponent.get(componentType) || [];
1529
- archetypes.push(newArchetype);
1530
- this.archetypesByComponent.set(componentType, archetypes);
1531
- }
1532
- for (const query of this.queries) query.checkNewArchetype(newArchetype);
1533
- return newArchetype;
1534
- });
1689
+ return getOrCreateWithSideEffect(this.archetypeBySignature, hashKey, () => this.createNewArchetype(sortedTypes));
1690
+ }
1691
+ /**
1692
+ * Filter out dontFragment relations from component types
1693
+ */
1694
+ filterRegularComponentTypes(componentTypes) {
1695
+ const regularTypes = [];
1696
+ for (const componentType of componentTypes) {
1697
+ const detailedType = getDetailedIdType(componentType);
1698
+ if ((detailedType.type === "entity-relation" || detailedType.type === "component-relation") && isDontFragmentComponent(detailedType.componentId)) continue;
1699
+ regularTypes.push(componentType);
1700
+ }
1701
+ return regularTypes;
1702
+ }
1703
+ /**
1704
+ * Create a new archetype and register it with all tracking structures
1705
+ */
1706
+ createNewArchetype(componentTypes) {
1707
+ const newArchetype = new Archetype(componentTypes, this.dontFragmentRelations);
1708
+ this.archetypes.push(newArchetype);
1709
+ this.registerArchetypeInComponentIndex(newArchetype, componentTypes);
1710
+ this.notifyQueriesOfNewArchetype(newArchetype);
1711
+ return newArchetype;
1712
+ }
1713
+ /**
1714
+ * Register archetype in the component-to-archetype index
1715
+ */
1716
+ registerArchetypeInComponentIndex(archetype, componentTypes) {
1717
+ for (const componentType of componentTypes) {
1718
+ const archetypes = this.archetypesByComponent.get(componentType) || [];
1719
+ archetypes.push(archetype);
1720
+ this.archetypesByComponent.set(componentType, archetypes);
1721
+ }
1722
+ }
1723
+ /**
1724
+ * Notify all queries to check the new archetype
1725
+ */
1726
+ notifyQueriesOfNewArchetype(archetype) {
1727
+ for (const query of this.queries) query.checkNewArchetype(archetype);
1535
1728
  }
1536
1729
  /**
1537
1730
  * Add a component reference to the reverse index when an entity is used as a component type
@@ -1569,10 +1762,29 @@ var World = class {
1569
1762
  */
1570
1763
  cleanupEmptyArchetype(archetype) {
1571
1764
  if (archetype.getEntities().length > 0) return;
1765
+ this.removeArchetypeFromList(archetype);
1766
+ this.removeArchetypeFromSignatureMap(archetype);
1767
+ this.removeArchetypeFromComponentIndex(archetype);
1768
+ this.removeArchetypeFromQueries(archetype);
1769
+ }
1770
+ /**
1771
+ * Remove archetype from the main archetypes list
1772
+ */
1773
+ removeArchetypeFromList(archetype) {
1572
1774
  const index = this.archetypes.indexOf(archetype);
1573
1775
  if (index !== -1) this.archetypes.splice(index, 1);
1776
+ }
1777
+ /**
1778
+ * Remove archetype from the signature-to-archetype map
1779
+ */
1780
+ removeArchetypeFromSignatureMap(archetype) {
1574
1781
  const hashKey = this.createArchetypeSignature(archetype.componentTypes);
1575
1782
  this.archetypeBySignature.delete(hashKey);
1783
+ }
1784
+ /**
1785
+ * Remove archetype from the component-to-archetypes index
1786
+ */
1787
+ removeArchetypeFromComponentIndex(archetype) {
1576
1788
  for (const componentType of archetype.componentTypes) {
1577
1789
  const archetypes = this.archetypesByComponent.get(componentType);
1578
1790
  if (archetypes) {
@@ -1583,6 +1795,11 @@ var World = class {
1583
1795
  }
1584
1796
  }
1585
1797
  }
1798
+ }
1799
+ /**
1800
+ * Remove archetype from all queries
1801
+ */
1802
+ removeArchetypeFromQueries(archetype) {
1586
1803
  for (const query of this.queries) query.removeArchetype(archetype);
1587
1804
  }
1588
1805
  /**
@@ -1660,5 +1877,5 @@ var World = class {
1660
1877
  };
1661
1878
 
1662
1879
  //#endregion
1663
- export { Archetype, COMPONENT_ID_MAX, ComponentIdAllocator, ENTITY_ID_START, EntityIdManager, INVALID_COMPONENT_ID, MISSING_COMPONENT, Query, RELATION_SHIFT, SystemScheduler, WILDCARD_TARGET_ID, World, component, createComponentId, createEntityId, decodeRelationId, getComponentIdByName, getComponentNameById, getDetailedIdType, getIdType, inspectEntityId, isComponentId, isEntityId, isOptionalEntityId, isRelationId, isWildcardRelationId, relation };
1880
+ export { Query, World, component, decodeRelationId, getComponentIdByName, getComponentNameById, isComponentId, isEntityId, isRelationId, isWildcardRelationId, relation };
1664
1881
  //# sourceMappingURL=index.mjs.map