@codehz/ecs 0.6.9 → 0.6.11

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
@@ -1593,12 +1686,14 @@ function triggerMultiComponentHooks(ctx, entityId, addedComponents, removedCompo
1593
1686
  const anyRequiredAdded = requiredComponents.some((c) => anyComponentMatches(addedComponents, c));
1594
1687
  const anyOptionalAdded = optionalComponents.some((c) => anyComponentMatches(addedComponents, c));
1595
1688
  const anyOptionalRemoved = optionalComponents.some((c) => anyComponentMatches(removedComponents, c));
1596
- if ((anyRequiredAdded || anyOptionalAdded || anyOptionalRemoved) && entityHasAllComponents(ctx, entityId, requiredComponents)) hook.on_set(entityId, ...collectMultiHookComponents(ctx, entityId, componentTypes));
1689
+ if (!oldArchetype.matchingMultiHooks.has(entry) || (anyRequiredAdded || anyOptionalAdded || anyOptionalRemoved) && entityHasAllComponents(ctx, entityId, requiredComponents)) hook.on_set(entityId, ...collectMultiHookComponents(ctx, entityId, componentTypes));
1597
1690
  }
1598
- if (removedComponents.size > 0) for (const entry of oldArchetype.matchingMultiHooks) {
1691
+ for (const entry of oldArchetype.matchingMultiHooks) {
1599
1692
  const { hook, requiredComponents, componentTypes } = entry;
1600
1693
  if (!hook.on_remove) continue;
1601
- if (requiredComponents.some((c) => anyComponentMatches(removedComponents, c)) && entityHadAllComponentsBefore(ctx, entityId, requiredComponents, removedComponents) && !entityHasAllComponents(ctx, entityId, requiredComponents)) hook.on_remove(entityId, ...collectMultiHookComponentsWithRemoved(ctx, entityId, componentTypes, removedComponents));
1694
+ const lostRequiredMatch = requiredComponents.some((c) => anyComponentMatches(removedComponents, c)) && entityHadAllComponentsBefore(ctx, entityId, requiredComponents, removedComponents) && !entityHasAllComponents(ctx, entityId, requiredComponents);
1695
+ const exitedMatchingSet = !newArchetype.matchingMultiHooks.has(entry);
1696
+ if (lostRequiredMatch || exitedMatchingSet) hook.on_remove(entityId, ...collectMultiHookComponentsWithRemoved(ctx, entityId, componentTypes, removedComponents));
1602
1697
  }
1603
1698
  }
1604
1699
  function entityHasAllComponents(ctx, entityId, requiredComponents) {
@@ -1794,9 +1889,23 @@ var World = class {
1794
1889
  relationEntityIdsByTarget = /* @__PURE__ */ new Map();
1795
1890
  queries = [];
1796
1891
  queryCache = /* @__PURE__ */ new Map();
1797
- commandBuffer = new CommandBuffer((entityId, commands) => this.executeEntityCommands(entityId, commands));
1798
1892
  legacyHooks = /* @__PURE__ */ new Map();
1799
1893
  hooks = /* @__PURE__ */ new Set();
1894
+ commandBuffer = new CommandBuffer((entityId, commands) => this.executeEntityCommands(entityId, commands));
1895
+ _changeset = new ComponentChangeset();
1896
+ /** Cached command processor context to avoid per-entity object allocation */
1897
+ _commandCtx = {
1898
+ dontFragmentRelations: this.dontFragmentRelations,
1899
+ ensureArchetype: (ct) => this.ensureArchetype(ct)
1900
+ };
1901
+ /** Cached hooks context to avoid per-entity object allocation */
1902
+ _hooksCtx = {
1903
+ hooks: this.legacyHooks,
1904
+ multiHooks: this.hooks,
1905
+ has: (eid, ct) => this.has(eid, ct),
1906
+ get: (eid, ct) => this.get(eid, ct),
1907
+ getOptional: (eid, ct) => this.getOptional(eid, ct)
1908
+ };
1800
1909
  constructor(snapshot) {
1801
1910
  if (snapshot && typeof snapshot === "object") this.deserializeSnapshot(snapshot);
1802
1911
  }
@@ -1940,40 +2049,77 @@ var World = class {
1940
2049
  if (this.isComponentEntityId(entityId)) return true;
1941
2050
  return this.entityToArchetype.has(entityId);
1942
2051
  }
1943
- set(entityId, componentTypeOrComponent, maybeComponent) {
2052
+ assertEntityExists(entityId, label) {
2053
+ if (!this.exists(entityId)) throw new Error(`${label} ${entityId} does not exist`);
2054
+ }
2055
+ assertComponentTypeValid(componentType) {
2056
+ if (getDetailedIdType(componentType).type === "invalid") throw new Error(`Invalid component type: ${componentType}`);
2057
+ }
2058
+ assertSetComponentTypeValid(componentType) {
2059
+ const detailedType = getDetailedIdType(componentType);
2060
+ if (detailedType.type === "invalid") throw new Error(`Invalid component type: ${componentType}`);
2061
+ if (detailedType.type === "wildcard-relation") throw new Error(`Cannot directly add wildcard relation components: ${componentType}`);
2062
+ }
2063
+ resolveSetOperation(entityId, componentTypeOrComponent, maybeComponent) {
1944
2064
  if (maybeComponent === void 0 && componentTypeOrComponent !== void 0) {
1945
- const detailedType$1 = getDetailedIdType(entityId);
1946
- if (detailedType$1.type === "component" || detailedType$1.type === "component-relation") {
2065
+ const detailedType = getDetailedIdType(entityId);
2066
+ if (detailedType.type === "component" || detailedType.type === "component-relation") {
1947
2067
  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;
2068
+ this.assertEntityExists(componentId, "Component entity");
2069
+ this.assertSetComponentTypeValid(componentId);
2070
+ return {
2071
+ entityId: componentId,
2072
+ componentType: componentId,
2073
+ component: componentTypeOrComponent
2074
+ };
1955
2075
  }
1956
2076
  }
1957
- const entityIdArg = entityId;
2077
+ const targetEntityId = entityId;
1958
2078
  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);
2079
+ this.assertEntityExists(targetEntityId, "Entity");
2080
+ this.assertSetComponentTypeValid(componentType);
2081
+ return {
2082
+ entityId: targetEntityId,
2083
+ componentType,
2084
+ component: maybeComponent
2085
+ };
1965
2086
  }
1966
- remove(entityId, componentType) {
2087
+ resolveRemoveOperation(entityId, componentType) {
1967
2088
  if (componentType === void 0) {
1968
2089
  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;
2090
+ this.assertEntityExists(componentId, "Component entity");
2091
+ return {
2092
+ entityId: componentId,
2093
+ componentType: componentId
2094
+ };
1972
2095
  }
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);
2096
+ const targetEntityId = entityId;
2097
+ this.assertEntityExists(targetEntityId, "Entity");
2098
+ this.assertComponentTypeValid(componentType);
2099
+ return {
2100
+ entityId: targetEntityId,
2101
+ componentType
2102
+ };
2103
+ }
2104
+ getComponentEntityWildcardRelations(entityId, wildcardComponentType) {
2105
+ const componentId = getComponentIdFromRelationId(wildcardComponentType);
2106
+ const data = this.componentEntityComponents.get(entityId);
2107
+ if (componentId === void 0 || !data) return [];
2108
+ const relations = [];
2109
+ for (const [key, value] of data.entries()) {
2110
+ if (getComponentIdFromRelationId(key) !== componentId) continue;
2111
+ const detailed = getDetailedIdType(key);
2112
+ if (detailed.type === "entity-relation" || detailed.type === "component-relation") relations.push([detailed.targetId, value]);
2113
+ }
2114
+ return relations;
2115
+ }
2116
+ set(entityId, componentTypeOrComponent, maybeComponent) {
2117
+ const { entityId: targetEntityId, componentType, component: component$1 } = this.resolveSetOperation(entityId, componentTypeOrComponent, maybeComponent);
2118
+ this.commandBuffer.set(targetEntityId, componentType, component$1);
2119
+ }
2120
+ remove(entityId, componentType) {
2121
+ const { entityId: targetEntityId, componentType: targetComponentType } = this.resolveRemoveOperation(entityId, componentType);
2122
+ this.commandBuffer.remove(targetEntityId, targetComponentType);
1977
2123
  }
1978
2124
  /**
1979
2125
  * Deletes an entity and all its components from the world.
@@ -2012,18 +2158,7 @@ var World = class {
2012
2158
  }
2013
2159
  get(entityId, componentType = entityId) {
2014
2160
  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
- }
2161
+ if (isWildcardRelationId(componentType)) return this.getComponentEntityWildcardRelations(entityId, componentType);
2027
2162
  const data = this.componentEntityComponents.get(entityId);
2028
2163
  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
2164
  return data.get(componentType);
@@ -2040,15 +2175,7 @@ var World = class {
2040
2175
  getOptional(entityId, componentType = entityId) {
2041
2176
  if (this.isComponentEntityId(entityId)) {
2042
2177
  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
- }
2178
+ const relations = this.getComponentEntityWildcardRelations(entityId, componentType);
2052
2179
  if (relations.length === 0) return void 0;
2053
2180
  return { value: relations };
2054
2181
  }
@@ -2065,7 +2192,7 @@ var World = class {
2065
2192
  }
2066
2193
  return archetype.getOptional(entityId, componentType);
2067
2194
  }
2068
- hook(componentTypesOrSingle, hook) {
2195
+ hook(componentTypesOrSingle, hook, filter) {
2069
2196
  if (typeof hook === "function") if (Array.isArray(componentTypesOrSingle)) {
2070
2197
  const callback = hook;
2071
2198
  hook = {
@@ -2092,14 +2219,15 @@ var World = class {
2092
2219
  componentTypes,
2093
2220
  requiredComponents,
2094
2221
  optionalComponents,
2222
+ filter: filter || {},
2095
2223
  hook
2096
2224
  };
2097
2225
  this.hooks.add(entry);
2098
2226
  for (const archetype of this.archetypes) if (this.archetypeMatchesHook(archetype, entry)) archetype.matchingMultiHooks.add(entry);
2099
2227
  const multiHook = hook;
2100
- if (multiHook.on_init !== void 0) {
2101
- const matchingArchetypes = this.getMatchingArchetypes(requiredComponents);
2102
- for (const archetype of matchingArchetypes) for (const entityId of archetype.getEntities()) {
2228
+ if (multiHook.on_init !== void 0) for (const archetype of this.archetypes) {
2229
+ if (!this.archetypeMatchesHook(archetype, entry)) continue;
2230
+ for (const entityId of archetype.getEntities()) {
2103
2231
  const components = collectMultiHookComponents(this.createHooksContext(), entityId, componentTypes);
2104
2232
  multiHook.on_init(entityId, ...components);
2105
2233
  }
@@ -2190,7 +2318,7 @@ var World = class {
2190
2318
  * });
2191
2319
  */
2192
2320
  createQuery(componentTypes, filter = {}) {
2193
- const sortedTypes = [...componentTypes].sort((a, b) => a - b);
2321
+ const sortedTypes = normalizeComponentTypes(componentTypes);
2194
2322
  const filterKey = serializeQueryFilter(filter);
2195
2323
  const key = `${this.createArchetypeSignature(sortedTypes)}${filterKey ? `|${filterKey}` : ""}`;
2196
2324
  const cached = this.queryCache.get(key);
@@ -2308,6 +2436,11 @@ var World = class {
2308
2436
  const archetypeLists = componentTypes.map((type) => this.archetypesByComponent.get(type) || []).sort((a, b) => a.length - b.length);
2309
2437
  const shortest = archetypeLists[0];
2310
2438
  if (shortest.length === 0) return [];
2439
+ if (archetypeLists.length === 2) {
2440
+ const second = archetypeLists[1];
2441
+ const secondSet = new Set(second);
2442
+ return shortest.filter((a) => secondSet.has(a));
2443
+ }
2311
2444
  let result = new Set(shortest);
2312
2445
  for (let i = 1; i < archetypeLists.length; i++) {
2313
2446
  const listSet = new Set(archetypeLists[i]);
@@ -2329,7 +2462,8 @@ var World = class {
2329
2462
  }
2330
2463
  }
2331
2464
  executeEntityCommands(entityId, commands) {
2332
- const changeset = new ComponentChangeset();
2465
+ const changeset = this._changeset;
2466
+ changeset.clear();
2333
2467
  if (this.isComponentEntityId(entityId)) {
2334
2468
  this.executeComponentEntityCommands(entityId, commands);
2335
2469
  return changeset;
@@ -2343,11 +2477,15 @@ var World = class {
2343
2477
  processCommands(entityId, currentArchetype, commands, changeset, (eid, arch, compId) => {
2344
2478
  if (isExclusiveComponent(compId)) removeMatchingRelations(eid, arch, compId, changeset);
2345
2479
  });
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);
2480
+ const hasHooks = this.legacyHooks.size > 0 || this.hooks.size > 0;
2481
+ const hasEntityRefs = changeset.removes.size > 0 || changeset.adds.size > 0;
2482
+ if (!hasHooks) {
2483
+ applyChangesetNoHooks(this._commandCtx, entityId, currentArchetype, changeset, this.entityToArchetype);
2484
+ if (hasEntityRefs) this.updateEntityReferences(entityId, changeset);
2485
+ return changeset;
2486
+ }
2487
+ const { removedComponents, newArchetype } = applyChangeset(this._commandCtx, entityId, currentArchetype, changeset, this.entityToArchetype);
2488
+ if (hasEntityRefs) this.updateEntityReferences(entityId, changeset);
2351
2489
  triggerLifecycleHooks(this.createHooksContext(), entityId, changeset.adds, removedComponents, currentArchetype, newArchetype);
2352
2490
  return changeset;
2353
2491
  }
@@ -2356,27 +2494,32 @@ var World = class {
2356
2494
  this.clearComponentEntityComponents(entityId);
2357
2495
  return;
2358
2496
  }
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) {
2497
+ const pendingSetValues = /* @__PURE__ */ new Map();
2498
+ for (const command of commands) if (command.type === "set" && command.componentType) {
2499
+ const merge = getComponentMerge(command.componentType);
2500
+ let nextValue = command.component;
2501
+ if (merge !== void 0 && pendingSetValues.has(command.componentType)) nextValue = merge(pendingSetValues.get(command.componentType), command.component);
2502
+ pendingSetValues.set(command.componentType, nextValue);
2503
+ this.getComponentEntityComponents(entityId, true).set(command.componentType, nextValue);
2504
+ } else if (command.type === "delete" && command.componentType) {
2361
2505
  const data = this.componentEntityComponents.get(entityId);
2362
- if (!data) continue;
2363
2506
  if (isWildcardRelationId(command.componentType)) {
2364
2507
  const componentId = getComponentIdFromRelationId(command.componentType);
2365
2508
  if (componentId !== void 0) {
2366
- for (const key of Array.from(data.keys())) if (getComponentIdFromRelationId(key) === componentId) data.delete(key);
2509
+ if (data) {
2510
+ for (const key of Array.from(data.keys())) if (getComponentIdFromRelationId(key) === componentId) data.delete(key);
2511
+ }
2512
+ for (const key of Array.from(pendingSetValues.keys())) if (getComponentIdFromRelationId(key) === componentId) pendingSetValues.delete(key);
2367
2513
  }
2368
- } else data.delete(command.componentType);
2369
- if (data.size === 0) this.clearComponentEntityComponents(entityId);
2514
+ } else {
2515
+ data?.delete(command.componentType);
2516
+ pendingSetValues.delete(command.componentType);
2517
+ }
2518
+ if (data?.size === 0) this.clearComponentEntityComponents(entityId);
2370
2519
  }
2371
2520
  }
2372
2521
  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
- };
2522
+ return this._hooksCtx;
2380
2523
  }
2381
2524
  removeComponentImmediate(entityId, componentType, targetEntityId) {
2382
2525
  const sourceArchetype = this.entityToArchetype.get(entityId);
@@ -2385,10 +2528,7 @@ var World = class {
2385
2528
  changeset.delete(componentType);
2386
2529
  maybeRemoveWildcardMarker(entityId, sourceArchetype, componentType, getComponentIdFromRelationId(componentType), changeset);
2387
2530
  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);
2531
+ const { newArchetype } = applyChangeset(this._commandCtx, entityId, sourceArchetype, changeset, this.entityToArchetype);
2392
2532
  untrackEntityReference(this.entityReferences, entityId, componentType, targetEntityId);
2393
2533
  triggerLifecycleHooks(this.createHooksContext(), entityId, /* @__PURE__ */ new Map(), new Map([[componentType, removedComponent]]), sourceArchetype, newArchetype);
2394
2534
  }
@@ -2403,9 +2543,9 @@ var World = class {
2403
2543
  } else if (componentType >= ENTITY_ID_START) trackEntityReference(this.entityReferences, entityId, componentType, componentType);
2404
2544
  }
2405
2545
  ensureArchetype(componentTypes) {
2406
- const sortedTypes = filterRegularComponentTypes(componentTypes).sort((a, b) => a - b);
2546
+ const sortedTypes = normalizeComponentTypes(filterRegularComponentTypes(componentTypes));
2407
2547
  const hashKey = this.createArchetypeSignature(sortedTypes);
2408
- return getOrCreateWithSideEffect(this.archetypeBySignature, hashKey, () => this.createNewArchetype(sortedTypes));
2548
+ return getOrCompute(this.archetypeBySignature, hashKey, () => this.createNewArchetype(sortedTypes));
2409
2549
  }
2410
2550
  createNewArchetype(componentTypes) {
2411
2551
  const newArchetype = new Archetype(componentTypes, this.dontFragmentRelations);
@@ -2430,7 +2570,7 @@ var World = class {
2430
2570
  return componentId !== void 0 && archetype.hasRelationWithComponentId(componentId);
2431
2571
  }
2432
2572
  return archetype.componentTypeSet.has(c) || isDontFragmentRelation(c);
2433
- });
2573
+ }) && matchesFilter(archetype, entry.filter);
2434
2574
  }
2435
2575
  archetypeReferencesEntity(archetype, entityId) {
2436
2576
  return archetype.componentTypes.some((ct) => ct === entityId || isEntityRelation(ct) && getTargetIdFromRelationId(ct) === entityId);