@codehz/ecs 0.6.9 → 0.6.10

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/world.mjs CHANGED
@@ -369,6 +369,7 @@ const componentNames = new Array(COMPONENT_ID_MAX + 1);
369
369
  const exclusiveFlags = new BitSet(COMPONENT_ID_MAX + 1);
370
370
  const cascadeDeleteFlags = new BitSet(COMPONENT_ID_MAX + 1);
371
371
  const dontFragmentFlags = new BitSet(COMPONENT_ID_MAX + 1);
372
+ const componentMerges = new Array(COMPONENT_ID_MAX + 1);
372
373
  /**
373
374
  * Allocate a new component ID from the global allocator.
374
375
  * @param nameOrOptions Optional name for the component (for serialization/debugging) or options object
@@ -401,6 +402,7 @@ function component(nameOrOptions) {
401
402
  if (options.exclusive) exclusiveFlags.set(id);
402
403
  if (options.cascadeDelete) cascadeDeleteFlags.set(id);
403
404
  if (options.dontFragment) dontFragmentFlags.set(id);
405
+ if (options.merge) componentMerges[id] = options.merge;
404
406
  }
405
407
  return id;
406
408
  }
@@ -419,6 +421,21 @@ function getComponentIdByName(name) {
419
421
  function getComponentNameById(id) {
420
422
  return componentNames[id];
421
423
  }
424
+ function getBaseComponentId(componentType) {
425
+ if (isComponentId(componentType)) return componentType;
426
+ const decoded = decodeRelationRaw(componentType);
427
+ if (decoded === null) return void 0;
428
+ return isValidComponentId(decoded.componentId) ? decoded.componentId : void 0;
429
+ }
430
+ /**
431
+ * Get merge callback for a componentType (including relation component types).
432
+ * Returns undefined if the base component has no merge callback.
433
+ */
434
+ function getComponentMerge(componentType) {
435
+ const baseComponentId = getBaseComponentId(componentType);
436
+ if (baseComponentId === void 0) return void 0;
437
+ return componentMerges[baseComponentId];
438
+ }
422
439
  /**
423
440
  * Check if a component is marked as exclusive
424
441
  * @param id The component ID
@@ -637,6 +654,8 @@ const MAX_COMMAND_ITERATIONS = 100;
637
654
  var CommandBuffer = class {
638
655
  commands = [];
639
656
  swapBuffer = [];
657
+ /** Reusable map to group commands by entity, avoids per-sync allocations */
658
+ entityCommands = /* @__PURE__ */ new Map();
640
659
  executeEntityCommands;
641
660
  /**
642
661
  * Create a command buffer with an executor function
@@ -681,14 +700,16 @@ var CommandBuffer = class {
681
700
  iterations++;
682
701
  const currentCommands = this.commands;
683
702
  this.commands = this.swapBuffer;
684
- const entityCommands = /* @__PURE__ */ new Map();
703
+ const entityCommands = this.entityCommands;
685
704
  for (const cmd of currentCommands) {
686
- if (!entityCommands.has(cmd.entityId)) entityCommands.set(cmd.entityId, []);
687
- entityCommands.get(cmd.entityId).push(cmd);
705
+ const existing = entityCommands.get(cmd.entityId);
706
+ if (existing !== void 0) existing.push(cmd);
707
+ else entityCommands.set(cmd.entityId, [cmd]);
688
708
  }
689
709
  currentCommands.length = 0;
690
710
  this.swapBuffer = currentCommands;
691
711
  for (const [entityId, commands] of entityCommands) this.executeEntityCommands(entityId, commands);
712
+ entityCommands.clear();
692
713
  }
693
714
  }
694
715
  /**
@@ -746,6 +767,16 @@ function matchesFilter(archetype, filter) {
746
767
  });
747
768
  }
748
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
+
749
780
  //#endregion
750
781
  //#region src/query/query.ts
751
782
  /**
@@ -765,7 +796,7 @@ var Query = class {
765
796
  specificDontFragmentTypes;
766
797
  constructor(world, componentTypes, filter = {}) {
767
798
  this.world = world;
768
- this.componentTypes = [...componentTypes].sort((a, b) => a - b);
799
+ this.componentTypes = normalizeComponentTypes(componentTypes);
769
800
  this.filter = filter;
770
801
  this.wildcardTypes = this.componentTypes.filter((ct) => getDetailedIdType(ct).type === "wildcard-relation");
771
802
  this.specificDontFragmentTypes = this.componentTypes.filter((ct) => {
@@ -923,16 +954,6 @@ function getOrCompute(cache, key, compute) {
923
954
  }
924
955
  return value;
925
956
  }
926
- /**
927
- * Alias for getOrCompute - maintained for backwards compatibility
928
- * @deprecated Use getOrCompute instead
929
- */
930
- const getOrComputeCache = getOrCompute;
931
- /**
932
- * Alias for getOrCompute - maintained for backwards compatibility
933
- * @deprecated Use getOrCompute instead
934
- */
935
- const getOrCreateWithSideEffect = getOrCompute;
936
957
 
937
958
  //#endregion
938
959
  //#region src/core/types.ts
@@ -1083,7 +1104,7 @@ var Archetype = class {
1083
1104
  */
1084
1105
  componentDataSourcesCache = /* @__PURE__ */ new Map();
1085
1106
  constructor(componentTypes, dontFragmentRelations) {
1086
- this.componentTypes = [...componentTypes].sort((a, b) => a - b);
1107
+ this.componentTypes = normalizeComponentTypes(componentTypes);
1087
1108
  this.componentTypeSet = new Set(this.componentTypes);
1088
1109
  this.dontFragmentRelations = dontFragmentRelations;
1089
1110
  for (const componentType of this.componentTypes) this.componentData.set(componentType, []);
@@ -1099,7 +1120,7 @@ var Archetype = class {
1099
1120
  */
1100
1121
  matches(componentTypes) {
1101
1122
  if (this.componentTypes.length !== componentTypes.length) return false;
1102
- const sortedTypes = [...componentTypes].sort((a, b) => a - b);
1123
+ const sortedTypes = normalizeComponentTypes(componentTypes);
1103
1124
  return this.componentTypes.every((type, index) => type === sortedTypes[index]);
1104
1125
  }
1105
1126
  addEntity(entityId, componentData) {
@@ -1134,6 +1155,9 @@ var Archetype = class {
1134
1155
  if (dontFragmentData) for (const [componentType, data] of dontFragmentData) entityData.set(componentType, data);
1135
1156
  return entityData;
1136
1157
  }
1158
+ getEntityDontFragmentRelations(entityId) {
1159
+ return this.dontFragmentRelations.get(entityId);
1160
+ }
1137
1161
  dump() {
1138
1162
  return this.entities.map((entity, i) => {
1139
1163
  const components = /* @__PURE__ */ new Map();
@@ -1255,7 +1279,7 @@ var Archetype = class {
1255
1279
  }
1256
1280
  getCachedComponentDataSources(componentTypes) {
1257
1281
  const cacheKey = buildCacheKey(componentTypes);
1258
- return getOrComputeCache(this.componentDataSourcesCache, cacheKey, () => componentTypes.map((compType) => this.getComponentDataSource(compType)));
1282
+ return getOrCompute(this.componentDataSourcesCache, cacheKey, () => componentTypes.map((compType) => this.getComponentDataSource(compType)));
1259
1283
  }
1260
1284
  getComponentDataSource(compType) {
1261
1285
  const optional = isOptionalEntityId(compType);
@@ -1412,6 +1436,12 @@ function processSetCommand(entityId, currentArchetype, componentType, component$
1412
1436
  if (!currentArchetype.componentTypeSet.has(wildcardMarker)) changeset.set(wildcardMarker, void 0);
1413
1437
  }
1414
1438
  }
1439
+ const merge = getComponentMerge(componentType);
1440
+ if (merge !== void 0 && changeset.adds.has(componentType)) {
1441
+ const prev = changeset.adds.get(componentType);
1442
+ changeset.set(componentType, merge(prev, component$1));
1443
+ return;
1444
+ }
1415
1445
  changeset.set(componentType, component$1);
1416
1446
  }
1417
1447
  function processDeleteCommand(entityId, currentArchetype, componentType, changeset) {
@@ -1427,10 +1457,9 @@ function removeMatchingRelations(entityId, archetype, baseComponentId, changeset
1427
1457
  if (isWildcardRelationId(componentType)) continue;
1428
1458
  if (getComponentIdFromRelationId(componentType) === baseComponentId) changeset.delete(componentType);
1429
1459
  }
1430
- const entityData = archetype.getEntity(entityId);
1431
- if (entityData) for (const [componentType] of entityData) {
1432
- if (archetype.componentTypeSet.has(componentType)) continue;
1433
- if (getComponentIdFromRelationId(componentType) === baseComponentId) changeset.delete(componentType);
1460
+ const dontFragmentData = archetype.getEntityDontFragmentRelations(entityId);
1461
+ if (dontFragmentData) {
1462
+ for (const componentType of dontFragmentData.keys()) if (getComponentIdFromRelationId(componentType) === baseComponentId) changeset.delete(componentType);
1434
1463
  }
1435
1464
  }
1436
1465
  function removeWildcardRelations(entityId, currentArchetype, baseComponentId, changeset) {
@@ -1440,35 +1469,66 @@ function removeWildcardRelations(entityId, currentArchetype, baseComponentId, ch
1440
1469
  function maybeRemoveWildcardMarker(entityId, archetype, removedComponentType, componentId, changeset) {
1441
1470
  if (componentId === void 0 || !isDontFragmentComponent(componentId)) return;
1442
1471
  const wildcardMarker = relation(componentId, "*");
1443
- const entityData = archetype.getEntity(entityId);
1444
- if (!entityData) {
1445
- changeset.delete(wildcardMarker);
1446
- return;
1447
- }
1448
- for (const [otherComponentType] of entityData) {
1472
+ for (const otherComponentType of archetype.componentTypes) {
1449
1473
  if (otherComponentType === removedComponentType) continue;
1450
1474
  if (otherComponentType === wildcardMarker) continue;
1451
1475
  if (changeset.removes.has(otherComponentType)) continue;
1452
1476
  if (getComponentIdFromRelationId(otherComponentType) === componentId) return;
1453
1477
  }
1478
+ const dontFragmentData = archetype.getEntityDontFragmentRelations(entityId);
1479
+ if (dontFragmentData) for (const otherComponentType of dontFragmentData.keys()) {
1480
+ if (otherComponentType === removedComponentType) continue;
1481
+ if (changeset.removes.has(otherComponentType)) continue;
1482
+ if (getComponentIdFromRelationId(otherComponentType) === componentId) return;
1483
+ }
1454
1484
  changeset.delete(wildcardMarker);
1455
1485
  }
1486
+ function hasEntityComponent(archetype, entityId, componentType) {
1487
+ if (archetype.componentTypeSet.has(componentType)) return true;
1488
+ return archetype.getEntityDontFragmentRelations(entityId)?.has(componentType) ?? false;
1489
+ }
1490
+ function pruneMissingRemovals(changeset, archetype, entityId) {
1491
+ let toPrune;
1492
+ for (const componentType of changeset.removes) if (!hasEntityComponent(archetype, entityId, componentType)) {
1493
+ if (toPrune === void 0) toPrune = [];
1494
+ toPrune.push(componentType);
1495
+ }
1496
+ if (toPrune !== void 0) for (const componentType of toPrune) changeset.removes.delete(componentType);
1497
+ }
1498
+ function hasArchetypeStructuralChange(changeset, currentArchetype) {
1499
+ for (const componentType of changeset.removes) if (!isDontFragmentRelation(componentType) && currentArchetype.componentTypeSet.has(componentType)) return true;
1500
+ for (const componentType of changeset.adds.keys()) if (!isDontFragmentRelation(componentType) && !currentArchetype.componentTypeSet.has(componentType)) return true;
1501
+ return false;
1502
+ }
1503
+ function buildFinalRegularComponentTypes(currentArchetype, changeset) {
1504
+ const finalRegularTypes = new Set(currentArchetype.componentTypes);
1505
+ for (const componentType of changeset.removes) if (!isDontFragmentRelation(componentType)) finalRegularTypes.delete(componentType);
1506
+ for (const componentType of changeset.adds.keys()) if (!isDontFragmentRelation(componentType)) finalRegularTypes.add(componentType);
1507
+ return Array.from(finalRegularTypes);
1508
+ }
1456
1509
  function applyChangeset(ctx, entityId, currentArchetype, changeset, entityToArchetype) {
1457
- const currentEntityData = currentArchetype.getEntity(entityId);
1458
- const allCurrentComponentTypes = currentEntityData ? Array.from(currentEntityData.keys()) : currentArchetype.componentTypes;
1459
- const finalComponentTypes = changeset.getFinalComponentTypes(allCurrentComponentTypes);
1460
1510
  const removedComponents = /* @__PURE__ */ new Map();
1461
- if (finalComponentTypes) if (!areComponentTypesEqual(filterRegularComponentTypes(allCurrentComponentTypes), filterRegularComponentTypes(finalComponentTypes))) return {
1511
+ pruneMissingRemovals(changeset, currentArchetype, entityId);
1512
+ if (hasArchetypeStructuralChange(changeset, currentArchetype)) return {
1462
1513
  removedComponents,
1463
- newArchetype: moveEntityToNewArchetype(ctx, entityId, currentArchetype, finalComponentTypes, changeset, removedComponents, entityToArchetype)
1514
+ newArchetype: moveEntityToNewArchetype(ctx, entityId, currentArchetype, buildFinalRegularComponentTypes(currentArchetype, changeset), changeset, removedComponents, entityToArchetype)
1464
1515
  };
1465
- else updateEntityInSameArchetype(ctx, entityId, currentArchetype, changeset, removedComponents);
1466
- else updateEntityInSameArchetype(ctx, entityId, currentArchetype, changeset, removedComponents);
1516
+ updateEntityInSameArchetype(ctx, entityId, currentArchetype, changeset, removedComponents);
1467
1517
  return {
1468
1518
  removedComponents,
1469
1519
  newArchetype: currentArchetype
1470
1520
  };
1471
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
+ }
1472
1532
  function moveEntityToNewArchetype(ctx, entityId, currentArchetype, finalComponentTypes, changeset, removedComponents, entityToArchetype) {
1473
1533
  const newArchetype = ctx.ensureArchetype(finalComponentTypes);
1474
1534
  const currentComponents = currentArchetype.removeEntity(entityId);
@@ -1484,6 +1544,28 @@ function updateEntityInSameArchetype(ctx, entityId, currentArchetype, changeset,
1484
1544
  currentArchetype.set(entityId, componentType, component$1);
1485
1545
  }
1486
1546
  }
1547
+ /**
1548
+ * No-hooks variant: moves entity to new archetype without collecting removed component data.
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.
1561
+ */
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
+ }
1487
1569
  function applyDontFragmentChanges(dontFragmentRelations, entityId, changeset, removedComponents) {
1488
1570
  let entityRelations = dontFragmentRelations.get(entityId);
1489
1571
  for (const componentType of changeset.removes) if (isDontFragmentRelation(componentType)) {
@@ -1504,6 +1586,23 @@ function applyDontFragmentChanges(dontFragmentRelations, entityId, changeset, re
1504
1586
  }
1505
1587
  if (entityRelations && entityRelations.size === 0) dontFragmentRelations.delete(entityId);
1506
1588
  }
1589
+ /**
1590
+ * No-hooks variant of applyDontFragmentChanges that skips tracking removed component data.
1591
+ */
1592
+ function applyDontFragmentChangesNoHooks(dontFragmentRelations, entityId, changeset) {
1593
+ let entityRelations = dontFragmentRelations.get(entityId);
1594
+ for (const componentType of changeset.removes) if (isDontFragmentRelation(componentType)) {
1595
+ if (entityRelations) entityRelations.delete(componentType);
1596
+ }
1597
+ for (const [componentType, component$1] of changeset.adds) if (isDontFragmentRelation(componentType)) {
1598
+ if (!entityRelations) {
1599
+ entityRelations = /* @__PURE__ */ new Map();
1600
+ dontFragmentRelations.set(entityId, entityRelations);
1601
+ }
1602
+ entityRelations.set(componentType, component$1);
1603
+ }
1604
+ if (entityRelations && entityRelations.size === 0) dontFragmentRelations.delete(entityId);
1605
+ }
1507
1606
  function filterRegularComponentTypes(componentTypes) {
1508
1607
  const regularTypes = [];
1509
1608
  for (const componentType of componentTypes) {
@@ -1516,12 +1615,6 @@ function filterRegularComponentTypes(componentTypes) {
1516
1615
  }
1517
1616
  return regularTypes;
1518
1617
  }
1519
- function areComponentTypesEqual(types1, types2) {
1520
- if (types1.length !== types2.length) return false;
1521
- const sorted1 = [...types1].sort((a, b) => a - b);
1522
- const sorted2 = [...types2].sort((a, b) => a - b);
1523
- return sorted1.every((v, i) => v === sorted2[i]);
1524
- }
1525
1618
 
1526
1619
  //#endregion
1527
1620
  //#region src/core/world-hooks.ts
@@ -1794,9 +1887,23 @@ var World = class {
1794
1887
  relationEntityIdsByTarget = /* @__PURE__ */ new Map();
1795
1888
  queries = [];
1796
1889
  queryCache = /* @__PURE__ */ new Map();
1797
- commandBuffer = new CommandBuffer((entityId, commands) => this.executeEntityCommands(entityId, commands));
1798
1890
  legacyHooks = /* @__PURE__ */ new Map();
1799
1891
  hooks = /* @__PURE__ */ new Set();
1892
+ commandBuffer = new CommandBuffer((entityId, commands) => this.executeEntityCommands(entityId, commands));
1893
+ _changeset = new ComponentChangeset();
1894
+ /** Cached command processor context to avoid per-entity object allocation */
1895
+ _commandCtx = {
1896
+ dontFragmentRelations: this.dontFragmentRelations,
1897
+ ensureArchetype: (ct) => this.ensureArchetype(ct)
1898
+ };
1899
+ /** Cached hooks context to avoid per-entity object allocation */
1900
+ _hooksCtx = {
1901
+ hooks: this.legacyHooks,
1902
+ multiHooks: this.hooks,
1903
+ has: (eid, ct) => this.has(eid, ct),
1904
+ get: (eid, ct) => this.get(eid, ct),
1905
+ getOptional: (eid, ct) => this.getOptional(eid, ct)
1906
+ };
1800
1907
  constructor(snapshot) {
1801
1908
  if (snapshot && typeof snapshot === "object") this.deserializeSnapshot(snapshot);
1802
1909
  }
@@ -1940,40 +2047,77 @@ var World = class {
1940
2047
  if (this.isComponentEntityId(entityId)) return true;
1941
2048
  return this.entityToArchetype.has(entityId);
1942
2049
  }
1943
- set(entityId, componentTypeOrComponent, maybeComponent) {
2050
+ assertEntityExists(entityId, label) {
2051
+ if (!this.exists(entityId)) throw new Error(`${label} ${entityId} does not exist`);
2052
+ }
2053
+ assertComponentTypeValid(componentType) {
2054
+ if (getDetailedIdType(componentType).type === "invalid") throw new Error(`Invalid component type: ${componentType}`);
2055
+ }
2056
+ assertSetComponentTypeValid(componentType) {
2057
+ const detailedType = getDetailedIdType(componentType);
2058
+ if (detailedType.type === "invalid") throw new Error(`Invalid component type: ${componentType}`);
2059
+ if (detailedType.type === "wildcard-relation") throw new Error(`Cannot directly add wildcard relation components: ${componentType}`);
2060
+ }
2061
+ resolveSetOperation(entityId, componentTypeOrComponent, maybeComponent) {
1944
2062
  if (maybeComponent === void 0 && componentTypeOrComponent !== void 0) {
1945
- const detailedType$1 = getDetailedIdType(entityId);
1946
- if (detailedType$1.type === "component" || detailedType$1.type === "component-relation") {
2063
+ const detailedType = getDetailedIdType(entityId);
2064
+ if (detailedType.type === "component" || detailedType.type === "component-relation") {
1947
2065
  const componentId = entityId;
1948
- const component$2 = componentTypeOrComponent;
1949
- if (!this.exists(componentId)) throw new Error(`Component entity ${componentId} does not exist`);
1950
- const detailedComponentType = getDetailedIdType(componentId);
1951
- if (detailedComponentType.type === "invalid") throw new Error(`Invalid component type: ${componentId}`);
1952
- if (detailedComponentType.type === "wildcard-relation") throw new Error(`Cannot directly add wildcard relation components: ${componentId}`);
1953
- this.commandBuffer.set(componentId, componentId, component$2);
1954
- return;
2066
+ this.assertEntityExists(componentId, "Component entity");
2067
+ this.assertSetComponentTypeValid(componentId);
2068
+ return {
2069
+ entityId: componentId,
2070
+ componentType: componentId,
2071
+ component: componentTypeOrComponent
2072
+ };
1955
2073
  }
1956
2074
  }
1957
- const entityIdArg = entityId;
2075
+ const targetEntityId = entityId;
1958
2076
  const componentType = componentTypeOrComponent;
1959
- const component$1 = maybeComponent;
1960
- if (!this.exists(entityIdArg)) throw new Error(`Entity ${entityIdArg} does not exist`);
1961
- const detailedType = getDetailedIdType(componentType);
1962
- if (detailedType.type === "invalid") throw new Error(`Invalid component type: ${componentType}`);
1963
- if (detailedType.type === "wildcard-relation") throw new Error(`Cannot directly add wildcard relation components: ${componentType}`);
1964
- this.commandBuffer.set(entityIdArg, componentType, component$1);
2077
+ this.assertEntityExists(targetEntityId, "Entity");
2078
+ this.assertSetComponentTypeValid(componentType);
2079
+ return {
2080
+ entityId: targetEntityId,
2081
+ componentType,
2082
+ component: maybeComponent
2083
+ };
1965
2084
  }
1966
- remove(entityId, componentType) {
2085
+ resolveRemoveOperation(entityId, componentType) {
1967
2086
  if (componentType === void 0) {
1968
2087
  const componentId = entityId;
1969
- if (!this.exists(componentId)) throw new Error(`Component entity ${componentId} does not exist`);
1970
- this.commandBuffer.remove(componentId, componentId);
1971
- return;
2088
+ this.assertEntityExists(componentId, "Component entity");
2089
+ return {
2090
+ entityId: componentId,
2091
+ componentType: componentId
2092
+ };
1972
2093
  }
1973
- const entityIdArg = entityId;
1974
- if (!this.exists(entityIdArg)) throw new Error(`Entity ${entityIdArg} does not exist`);
1975
- if (getDetailedIdType(componentType).type === "invalid") throw new Error(`Invalid component type: ${componentType}`);
1976
- this.commandBuffer.remove(entityIdArg, componentType);
2094
+ const targetEntityId = entityId;
2095
+ this.assertEntityExists(targetEntityId, "Entity");
2096
+ this.assertComponentTypeValid(componentType);
2097
+ return {
2098
+ entityId: targetEntityId,
2099
+ componentType
2100
+ };
2101
+ }
2102
+ getComponentEntityWildcardRelations(entityId, wildcardComponentType) {
2103
+ const componentId = getComponentIdFromRelationId(wildcardComponentType);
2104
+ const data = this.componentEntityComponents.get(entityId);
2105
+ if (componentId === void 0 || !data) return [];
2106
+ const relations = [];
2107
+ for (const [key, value] of data.entries()) {
2108
+ if (getComponentIdFromRelationId(key) !== componentId) continue;
2109
+ const detailed = getDetailedIdType(key);
2110
+ if (detailed.type === "entity-relation" || detailed.type === "component-relation") relations.push([detailed.targetId, value]);
2111
+ }
2112
+ return relations;
2113
+ }
2114
+ set(entityId, componentTypeOrComponent, maybeComponent) {
2115
+ const { entityId: targetEntityId, componentType, component: component$1 } = this.resolveSetOperation(entityId, componentTypeOrComponent, maybeComponent);
2116
+ this.commandBuffer.set(targetEntityId, componentType, component$1);
2117
+ }
2118
+ remove(entityId, componentType) {
2119
+ const { entityId: targetEntityId, componentType: targetComponentType } = this.resolveRemoveOperation(entityId, componentType);
2120
+ this.commandBuffer.remove(targetEntityId, targetComponentType);
1977
2121
  }
1978
2122
  /**
1979
2123
  * Deletes an entity and all its components from the world.
@@ -2012,18 +2156,7 @@ var World = class {
2012
2156
  }
2013
2157
  get(entityId, componentType = entityId) {
2014
2158
  if (this.isComponentEntityId(entityId)) {
2015
- if (isWildcardRelationId(componentType)) {
2016
- const componentId = getComponentIdFromRelationId(componentType);
2017
- const data$1 = this.componentEntityComponents.get(entityId);
2018
- const relations = [];
2019
- if (componentId !== void 0 && data$1) {
2020
- for (const [key, value] of data$1.entries()) if (getComponentIdFromRelationId(key) === componentId) {
2021
- const detailed = getDetailedIdType(key);
2022
- if (detailed.type === "entity-relation" || detailed.type === "component-relation") relations.push([detailed.targetId, value]);
2023
- }
2024
- }
2025
- return relations;
2026
- }
2159
+ if (isWildcardRelationId(componentType)) return this.getComponentEntityWildcardRelations(entityId, componentType);
2027
2160
  const data = this.componentEntityComponents.get(entityId);
2028
2161
  if (!data || !data.has(componentType)) throw new Error(`Entity ${entityId} does not have component ${componentType}. Use has() to check component existence before calling get().`);
2029
2162
  return data.get(componentType);
@@ -2040,15 +2173,7 @@ var World = class {
2040
2173
  getOptional(entityId, componentType = entityId) {
2041
2174
  if (this.isComponentEntityId(entityId)) {
2042
2175
  if (isWildcardRelationId(componentType)) {
2043
- const componentId = getComponentIdFromRelationId(componentType);
2044
- if (componentId === void 0) return void 0;
2045
- const data$1 = this.componentEntityComponents.get(entityId);
2046
- if (!data$1) return void 0;
2047
- const relations = [];
2048
- for (const [key, value] of data$1.entries()) if (getComponentIdFromRelationId(key) === componentId) {
2049
- const detailed = getDetailedIdType(key);
2050
- if (detailed.type === "entity-relation" || detailed.type === "component-relation") relations.push([detailed.targetId, value]);
2051
- }
2176
+ const relations = this.getComponentEntityWildcardRelations(entityId, componentType);
2052
2177
  if (relations.length === 0) return void 0;
2053
2178
  return { value: relations };
2054
2179
  }
@@ -2190,7 +2315,7 @@ var World = class {
2190
2315
  * });
2191
2316
  */
2192
2317
  createQuery(componentTypes, filter = {}) {
2193
- const sortedTypes = [...componentTypes].sort((a, b) => a - b);
2318
+ const sortedTypes = normalizeComponentTypes(componentTypes);
2194
2319
  const filterKey = serializeQueryFilter(filter);
2195
2320
  const key = `${this.createArchetypeSignature(sortedTypes)}${filterKey ? `|${filterKey}` : ""}`;
2196
2321
  const cached = this.queryCache.get(key);
@@ -2308,6 +2433,11 @@ var World = class {
2308
2433
  const archetypeLists = componentTypes.map((type) => this.archetypesByComponent.get(type) || []).sort((a, b) => a.length - b.length);
2309
2434
  const shortest = archetypeLists[0];
2310
2435
  if (shortest.length === 0) return [];
2436
+ if (archetypeLists.length === 2) {
2437
+ const second = archetypeLists[1];
2438
+ const secondSet = new Set(second);
2439
+ return shortest.filter((a) => secondSet.has(a));
2440
+ }
2311
2441
  let result = new Set(shortest);
2312
2442
  for (let i = 1; i < archetypeLists.length; i++) {
2313
2443
  const listSet = new Set(archetypeLists[i]);
@@ -2329,7 +2459,8 @@ var World = class {
2329
2459
  }
2330
2460
  }
2331
2461
  executeEntityCommands(entityId, commands) {
2332
- const changeset = new ComponentChangeset();
2462
+ const changeset = this._changeset;
2463
+ changeset.clear();
2333
2464
  if (this.isComponentEntityId(entityId)) {
2334
2465
  this.executeComponentEntityCommands(entityId, commands);
2335
2466
  return changeset;
@@ -2343,11 +2474,15 @@ var World = class {
2343
2474
  processCommands(entityId, currentArchetype, commands, changeset, (eid, arch, compId) => {
2344
2475
  if (isExclusiveComponent(compId)) removeMatchingRelations(eid, arch, compId, changeset);
2345
2476
  });
2346
- const { removedComponents, newArchetype } = applyChangeset({
2347
- dontFragmentRelations: this.dontFragmentRelations,
2348
- ensureArchetype: (ct) => this.ensureArchetype(ct)
2349
- }, entityId, currentArchetype, changeset, this.entityToArchetype);
2350
- this.updateEntityReferences(entityId, changeset);
2477
+ const hasHooks = this.legacyHooks.size > 0 || this.hooks.size > 0;
2478
+ const hasEntityRefs = changeset.removes.size > 0 || changeset.adds.size > 0;
2479
+ if (!hasHooks) {
2480
+ applyChangesetNoHooks(this._commandCtx, entityId, currentArchetype, changeset, this.entityToArchetype);
2481
+ if (hasEntityRefs) this.updateEntityReferences(entityId, changeset);
2482
+ return changeset;
2483
+ }
2484
+ const { removedComponents, newArchetype } = applyChangeset(this._commandCtx, entityId, currentArchetype, changeset, this.entityToArchetype);
2485
+ if (hasEntityRefs) this.updateEntityReferences(entityId, changeset);
2351
2486
  triggerLifecycleHooks(this.createHooksContext(), entityId, changeset.adds, removedComponents, currentArchetype, newArchetype);
2352
2487
  return changeset;
2353
2488
  }
@@ -2356,27 +2491,32 @@ var World = class {
2356
2491
  this.clearComponentEntityComponents(entityId);
2357
2492
  return;
2358
2493
  }
2359
- for (const command of commands) if (command.type === "set" && command.componentType) this.getComponentEntityComponents(entityId, true).set(command.componentType, command.component);
2360
- else if (command.type === "delete" && command.componentType) {
2494
+ const pendingSetValues = /* @__PURE__ */ new Map();
2495
+ for (const command of commands) if (command.type === "set" && command.componentType) {
2496
+ const merge = getComponentMerge(command.componentType);
2497
+ let nextValue = command.component;
2498
+ if (merge !== void 0 && pendingSetValues.has(command.componentType)) nextValue = merge(pendingSetValues.get(command.componentType), command.component);
2499
+ pendingSetValues.set(command.componentType, nextValue);
2500
+ this.getComponentEntityComponents(entityId, true).set(command.componentType, nextValue);
2501
+ } else if (command.type === "delete" && command.componentType) {
2361
2502
  const data = this.componentEntityComponents.get(entityId);
2362
- if (!data) continue;
2363
2503
  if (isWildcardRelationId(command.componentType)) {
2364
2504
  const componentId = getComponentIdFromRelationId(command.componentType);
2365
2505
  if (componentId !== void 0) {
2366
- for (const key of Array.from(data.keys())) if (getComponentIdFromRelationId(key) === componentId) data.delete(key);
2506
+ if (data) {
2507
+ for (const key of Array.from(data.keys())) if (getComponentIdFromRelationId(key) === componentId) data.delete(key);
2508
+ }
2509
+ for (const key of Array.from(pendingSetValues.keys())) if (getComponentIdFromRelationId(key) === componentId) pendingSetValues.delete(key);
2367
2510
  }
2368
- } else data.delete(command.componentType);
2369
- if (data.size === 0) this.clearComponentEntityComponents(entityId);
2511
+ } else {
2512
+ data?.delete(command.componentType);
2513
+ pendingSetValues.delete(command.componentType);
2514
+ }
2515
+ if (data?.size === 0) this.clearComponentEntityComponents(entityId);
2370
2516
  }
2371
2517
  }
2372
2518
  createHooksContext() {
2373
- return {
2374
- hooks: this.legacyHooks,
2375
- multiHooks: this.hooks,
2376
- has: (eid, ct) => this.has(eid, ct),
2377
- get: (eid, ct) => this.get(eid, ct),
2378
- getOptional: (eid, ct) => this.getOptional(eid, ct)
2379
- };
2519
+ return this._hooksCtx;
2380
2520
  }
2381
2521
  removeComponentImmediate(entityId, componentType, targetEntityId) {
2382
2522
  const sourceArchetype = this.entityToArchetype.get(entityId);
@@ -2385,10 +2525,7 @@ var World = class {
2385
2525
  changeset.delete(componentType);
2386
2526
  maybeRemoveWildcardMarker(entityId, sourceArchetype, componentType, getComponentIdFromRelationId(componentType), changeset);
2387
2527
  const removedComponent = sourceArchetype.get(entityId, componentType);
2388
- const { newArchetype } = applyChangeset({
2389
- dontFragmentRelations: this.dontFragmentRelations,
2390
- ensureArchetype: (ct) => this.ensureArchetype(ct)
2391
- }, entityId, sourceArchetype, changeset, this.entityToArchetype);
2528
+ const { newArchetype } = applyChangeset(this._commandCtx, entityId, sourceArchetype, changeset, this.entityToArchetype);
2392
2529
  untrackEntityReference(this.entityReferences, entityId, componentType, targetEntityId);
2393
2530
  triggerLifecycleHooks(this.createHooksContext(), entityId, /* @__PURE__ */ new Map(), new Map([[componentType, removedComponent]]), sourceArchetype, newArchetype);
2394
2531
  }
@@ -2403,9 +2540,9 @@ var World = class {
2403
2540
  } else if (componentType >= ENTITY_ID_START) trackEntityReference(this.entityReferences, entityId, componentType, componentType);
2404
2541
  }
2405
2542
  ensureArchetype(componentTypes) {
2406
- const sortedTypes = filterRegularComponentTypes(componentTypes).sort((a, b) => a - b);
2543
+ const sortedTypes = normalizeComponentTypes(filterRegularComponentTypes(componentTypes));
2407
2544
  const hashKey = this.createArchetypeSignature(sortedTypes);
2408
- return getOrCreateWithSideEffect(this.archetypeBySignature, hashKey, () => this.createNewArchetype(sortedTypes));
2545
+ return getOrCompute(this.archetypeBySignature, hashKey, () => this.createNewArchetype(sortedTypes));
2409
2546
  }
2410
2547
  createNewArchetype(componentTypes) {
2411
2548
  const newArchetype = new Archetype(componentTypes, this.dontFragmentRelations);