@codehz/ecs 0.6.5 → 0.6.7

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
@@ -1766,6 +1766,8 @@ var World = class {
1766
1766
  archetypesByComponent = /* @__PURE__ */ new Map();
1767
1767
  entityReferences = /* @__PURE__ */ new Map();
1768
1768
  dontFragmentRelations = /* @__PURE__ */ new Map();
1769
+ componentEntityComponents = /* @__PURE__ */ new Map();
1770
+ relationEntityIdsByTarget = /* @__PURE__ */ new Map();
1769
1771
  queries = [];
1770
1772
  queryCache = /* @__PURE__ */ new Map();
1771
1773
  commandBuffer = new CommandBuffer((entityId, commands) => this.executeEntityCommands(entityId, commands));
@@ -1776,6 +1778,18 @@ var World = class {
1776
1778
  }
1777
1779
  deserializeSnapshot(snapshot) {
1778
1780
  if (snapshot.entityManager) this.entityIdManager.deserializeState(snapshot.entityManager);
1781
+ if (Array.isArray(snapshot.componentEntities)) for (const entry of snapshot.componentEntities) {
1782
+ const entityId = decodeSerializedId(entry.id);
1783
+ if (!this.isComponentEntityId(entityId)) continue;
1784
+ const componentsArray = entry.components || [];
1785
+ const componentMap = /* @__PURE__ */ new Map();
1786
+ for (const componentEntry of componentsArray) {
1787
+ const componentType = decodeSerializedId(componentEntry.type);
1788
+ componentMap.set(componentType, componentEntry.value);
1789
+ }
1790
+ this.componentEntityComponents.set(entityId, componentMap);
1791
+ this.registerRelationEntityId(entityId);
1792
+ }
1779
1793
  if (Array.isArray(snapshot.entities)) for (const entry of snapshot.entities) {
1780
1794
  const entityId = decodeSerializedId(entry.id);
1781
1795
  const componentsArray = entry.components || [];
@@ -1799,6 +1813,18 @@ var World = class {
1799
1813
  createArchetypeSignature(componentTypes) {
1800
1814
  return componentTypes.join(",");
1801
1815
  }
1816
+ /**
1817
+ * Creates a new entity.
1818
+ * The entity is created with an empty component set and can be configured using `set()`.
1819
+ *
1820
+ * @template T - The initial component type (defaults to void if not specified)
1821
+ * @returns A unique identifier for the new entity
1822
+ *
1823
+ * @example
1824
+ * const entity = world.new<MyComponent>();
1825
+ * world.set(entity, MyComponent, { value: 42 });
1826
+ * world.sync();
1827
+ */
1802
1828
  new() {
1803
1829
  const entityId = this.entityIdManager.allocate();
1804
1830
  let emptyArchetype = this.ensureArchetype([]);
@@ -1806,6 +1832,50 @@ var World = class {
1806
1832
  this.entityToArchetype.set(entityId, emptyArchetype);
1807
1833
  return entityId;
1808
1834
  }
1835
+ isComponentEntityId(entityId) {
1836
+ const detailed = getDetailedIdType(entityId);
1837
+ return detailed.type !== "entity" && detailed.type !== "invalid";
1838
+ }
1839
+ registerRelationEntityId(entityId) {
1840
+ const detailed = getDetailedIdType(entityId);
1841
+ if (detailed.type !== "entity-relation") return;
1842
+ const targetId = detailed.targetId;
1843
+ if (targetId === void 0) return;
1844
+ const existing = this.relationEntityIdsByTarget.get(targetId);
1845
+ if (existing) {
1846
+ existing.add(entityId);
1847
+ return;
1848
+ }
1849
+ this.relationEntityIdsByTarget.set(targetId, new Set([entityId]));
1850
+ }
1851
+ unregisterRelationEntityId(entityId) {
1852
+ const detailed = getDetailedIdType(entityId);
1853
+ if (detailed.type !== "entity-relation") return;
1854
+ const targetId = detailed.targetId;
1855
+ if (targetId === void 0) return;
1856
+ const existing = this.relationEntityIdsByTarget.get(targetId);
1857
+ if (!existing) return;
1858
+ existing.delete(entityId);
1859
+ if (existing.size === 0) this.relationEntityIdsByTarget.delete(targetId);
1860
+ }
1861
+ getComponentEntityComponents(entityId, create) {
1862
+ let data = this.componentEntityComponents.get(entityId);
1863
+ if (!data && create) {
1864
+ data = /* @__PURE__ */ new Map();
1865
+ this.componentEntityComponents.set(entityId, data);
1866
+ this.registerRelationEntityId(entityId);
1867
+ }
1868
+ return data;
1869
+ }
1870
+ clearComponentEntityComponents(entityId) {
1871
+ if (this.componentEntityComponents.delete(entityId)) this.unregisterRelationEntityId(entityId);
1872
+ }
1873
+ cleanupComponentEntitiesReferencingEntity(targetId) {
1874
+ const relationEntities = this.relationEntityIdsByTarget.get(targetId);
1875
+ if (!relationEntities) return;
1876
+ for (const relationEntityId of relationEntities) this.componentEntityComponents.delete(relationEntityId);
1877
+ this.relationEntityIdsByTarget.delete(targetId);
1878
+ }
1809
1879
  destroyEntityImmediate(entityId) {
1810
1880
  const queue = [entityId];
1811
1881
  const visited = /* @__PURE__ */ new Set();
@@ -1827,9 +1897,22 @@ var World = class {
1827
1897
  triggerRemoveHooksForEntityDeletion(this.createHooksContext(), cur, removedComponents, archetype);
1828
1898
  this.cleanupArchetypesReferencingEntity(cur);
1829
1899
  this.entityIdManager.deallocate(cur);
1900
+ this.cleanupComponentEntitiesReferencingEntity(cur);
1830
1901
  }
1831
1902
  }
1903
+ /**
1904
+ * Checks if an entity exists in the world.
1905
+ *
1906
+ * @param entityId - The entity identifier to check
1907
+ * @returns `true` if the entity exists, `false` otherwise
1908
+ *
1909
+ * @example
1910
+ * if (world.exists(entityId)) {
1911
+ * console.log("Entity exists");
1912
+ * }
1913
+ */
1832
1914
  exists(entityId) {
1915
+ if (this.isComponentEntityId(entityId)) return true;
1833
1916
  return this.entityToArchetype.has(entityId);
1834
1917
  }
1835
1918
  set(entityId, componentType, component$1) {
@@ -1839,22 +1922,91 @@ var World = class {
1839
1922
  if (detailedType.type === "wildcard-relation") throw new Error(`Cannot directly add wildcard relation components: ${componentType}`);
1840
1923
  this.commandBuffer.set(entityId, componentType, component$1);
1841
1924
  }
1925
+ /**
1926
+ * Removes a component from an entity.
1927
+ * The change is buffered and takes effect after calling `world.sync()`.
1928
+ * If the entity does not exist, throws an error.
1929
+ *
1930
+ * @template T - The component data type
1931
+ * @param entityId - The entity identifier
1932
+ * @param componentType - The component type to remove
1933
+ *
1934
+ * @throws {Error} If the entity does not exist
1935
+ * @throws {Error} If the component type is invalid
1936
+ *
1937
+ * @example
1938
+ * world.remove(entity, Position);
1939
+ * world.sync(); // Apply changes
1940
+ */
1842
1941
  remove(entityId, componentType) {
1843
1942
  if (!this.exists(entityId)) throw new Error(`Entity ${entityId} does not exist`);
1844
1943
  if (getDetailedIdType(componentType).type === "invalid") throw new Error(`Invalid component type: ${componentType}`);
1845
1944
  this.commandBuffer.remove(entityId, componentType);
1846
1945
  }
1946
+ /**
1947
+ * Deletes an entity and all its components from the world.
1948
+ * The change is buffered and takes effect after calling `world.sync()`.
1949
+ * Related entities may trigger cascade delete hooks if configured.
1950
+ *
1951
+ * @param entityId - The entity identifier to delete
1952
+ *
1953
+ * @example
1954
+ * world.delete(entity);
1955
+ * world.sync(); // Apply changes
1956
+ */
1847
1957
  delete(entityId) {
1848
1958
  this.commandBuffer.delete(entityId);
1849
1959
  }
1960
+ /**
1961
+ * Checks if an entity has a specific component.
1962
+ * Immediately reflects the current state without waiting for `sync()`.
1963
+ *
1964
+ * @template T - The component data type
1965
+ * @param entityId - The entity identifier
1966
+ * @param componentType - The component type to check
1967
+ * @returns `true` if the entity has the component, `false` otherwise
1968
+ *
1969
+ * @example
1970
+ * if (world.has(entity, Position)) {
1971
+ * const pos = world.get(entity, Position);
1972
+ * }
1973
+ */
1850
1974
  has(entityId, componentType) {
1975
+ if (this.isComponentEntityId(entityId)) {
1976
+ if (isWildcardRelationId(componentType)) {
1977
+ const componentId = getComponentIdFromRelationId(componentType);
1978
+ if (componentId === void 0) return false;
1979
+ const data = this.componentEntityComponents.get(entityId);
1980
+ if (!data) return false;
1981
+ for (const key of data.keys()) if (getComponentIdFromRelationId(key) === componentId) return true;
1982
+ return false;
1983
+ }
1984
+ return this.componentEntityComponents.get(entityId)?.has(componentType) ?? false;
1985
+ }
1851
1986
  const archetype = this.entityToArchetype.get(entityId);
1852
1987
  if (!archetype) return false;
1853
1988
  if (archetype.componentTypes.includes(componentType)) return true;
1854
1989
  if (isDontFragmentRelation(componentType)) return this.dontFragmentRelations.get(entityId)?.has(componentType) ?? false;
1855
1990
  return false;
1856
1991
  }
1857
- get(entityId, componentType) {
1992
+ get(entityId, componentType = entityId) {
1993
+ if (this.isComponentEntityId(entityId)) {
1994
+ if (isWildcardRelationId(componentType)) {
1995
+ const componentId = getComponentIdFromRelationId(componentType);
1996
+ const data$1 = this.componentEntityComponents.get(entityId);
1997
+ const relations = [];
1998
+ if (componentId !== void 0 && data$1) {
1999
+ for (const [key, value] of data$1.entries()) if (getComponentIdFromRelationId(key) === componentId) {
2000
+ const detailed = getDetailedIdType(key);
2001
+ if (detailed.type === "entity-relation" || detailed.type === "component-relation") relations.push([detailed.targetId, value]);
2002
+ }
2003
+ }
2004
+ return relations;
2005
+ }
2006
+ const data = this.componentEntityComponents.get(entityId);
2007
+ if (!data || !data.has(componentType)) throw new Error(`Entity ${entityId} does not have component ${componentType}. Use has() to check component existence before calling get().`);
2008
+ return data.get(componentType);
2009
+ }
1858
2010
  const archetype = this.entityToArchetype.get(entityId);
1859
2011
  if (!archetype) throw new Error(`Entity ${entityId} does not exist`);
1860
2012
  if (componentType >= 0 || componentType % 2 ** 42 !== 0) {
@@ -1864,7 +2016,25 @@ var World = class {
1864
2016
  }
1865
2017
  return archetype.get(entityId, componentType);
1866
2018
  }
1867
- getOptional(entityId, componentType) {
2019
+ getOptional(entityId, componentType = entityId) {
2020
+ if (this.isComponentEntityId(entityId)) {
2021
+ if (isWildcardRelationId(componentType)) {
2022
+ const componentId = getComponentIdFromRelationId(componentType);
2023
+ if (componentId === void 0) return void 0;
2024
+ const data$1 = this.componentEntityComponents.get(entityId);
2025
+ if (!data$1) return void 0;
2026
+ const relations = [];
2027
+ for (const [key, value] of data$1.entries()) if (getComponentIdFromRelationId(key) === componentId) {
2028
+ const detailed = getDetailedIdType(key);
2029
+ if (detailed.type === "entity-relation" || detailed.type === "component-relation") relations.push([detailed.targetId, value]);
2030
+ }
2031
+ if (relations.length === 0) return void 0;
2032
+ return { value: relations };
2033
+ }
2034
+ const data = this.componentEntityComponents.get(entityId);
2035
+ if (!data || !data.has(componentType)) return void 0;
2036
+ return { value: data.get(componentType) };
2037
+ }
1868
2038
  const archetype = this.entityToArchetype.get(entityId);
1869
2039
  if (!archetype) throw new Error(`Entity ${entityId} does not exist`);
1870
2040
  if (isWildcardRelationId(componentType)) {
@@ -1957,9 +2127,47 @@ var World = class {
1957
2127
  }
1958
2128
  }
1959
2129
  }
2130
+ /**
2131
+ * Synchronizes all buffered commands (set/remove/delete) to the world.
2132
+ * This method must be called after making changes via `set()`, `remove()`, or `delete()` for them to take effect.
2133
+ * Typically called once per frame at the end of your game loop.
2134
+ *
2135
+ * @example
2136
+ * world.set(entity, Position, { x: 10, y: 20 });
2137
+ * world.remove(entity, OldComponent);
2138
+ * world.sync(); // Apply all buffered changes
2139
+ */
1960
2140
  sync() {
1961
2141
  this.commandBuffer.execute();
1962
2142
  }
2143
+ /**
2144
+ * Creates a cached query for efficiently iterating entities with specific components.
2145
+ * The query is cached internally and reused across calls with the same component types and filter.
2146
+ *
2147
+ * **Important:** Store the query reference and reuse it across frames for optimal performance.
2148
+ * Creating a new query each frame defeats the caching mechanism.
2149
+ *
2150
+ * @param componentTypes - Array of component types to match
2151
+ * @param filter - Optional filter for additional constraints (e.g., without specific components)
2152
+ * @returns A Query instance that can be used to iterate matching entities
2153
+ *
2154
+ * @example
2155
+ * // Create once, reuse many times
2156
+ * const movementQuery = world.createQuery([Position, Velocity]);
2157
+ *
2158
+ * // In game loop
2159
+ * movementQuery.forEach((entity) => {
2160
+ * const pos = world.get(entity, Position);
2161
+ * const vel = world.get(entity, Velocity);
2162
+ * pos.x += vel.x;
2163
+ * pos.y += vel.y;
2164
+ * });
2165
+ *
2166
+ * // With filter
2167
+ * const activeQuery = world.createQuery([Position], {
2168
+ * without: [Disabled]
2169
+ * });
2170
+ */
1963
2171
  createQuery(componentTypes, filter = {}) {
1964
2172
  const sortedTypes = [...componentTypes].sort((a, b) => a - b);
1965
2173
  const filterKey = serializeQueryFilter(filter);
@@ -1976,9 +2184,38 @@ var World = class {
1976
2184
  });
1977
2185
  return query;
1978
2186
  }
2187
+ /**
2188
+ * Creates a new entity builder for fluent entity configuration.
2189
+ * Useful for building entities with multiple components in a single expression.
2190
+ *
2191
+ * @returns An EntityBuilder instance
2192
+ *
2193
+ * @example
2194
+ * const entity = world.spawn()
2195
+ * .with(Position, { x: 0, y: 0 })
2196
+ * .with(Velocity, { x: 1, y: 1 })
2197
+ * .build();
2198
+ * world.sync(); // Apply changes
2199
+ */
1979
2200
  spawn() {
1980
2201
  return new EntityBuilder(this);
1981
2202
  }
2203
+ /**
2204
+ * Spawns multiple entities with a configuration callback.
2205
+ * More efficient than calling `spawn()` multiple times when creating many entities.
2206
+ *
2207
+ * @param count - Number of entities to spawn
2208
+ * @param configure - Callback that receives an EntityBuilder and index; must return the configured builder
2209
+ * @returns Array of created entity IDs
2210
+ *
2211
+ * @example
2212
+ * const entities = world.spawnMany(100, (builder, index) => {
2213
+ * return builder
2214
+ * .with(Position, { x: index * 10, y: 0 })
2215
+ * .with(Velocity, { x: 0, y: 1 });
2216
+ * });
2217
+ * world.sync();
2218
+ */
1982
2219
  spawnMany(count, configure) {
1983
2220
  const entities = [];
1984
2221
  for (let i = 0; i < count; i++) {
@@ -1994,6 +2231,17 @@ var World = class {
1994
2231
  const index = this.queries.indexOf(query);
1995
2232
  if (index !== -1) this.queries.splice(index, 1);
1996
2233
  }
2234
+ /**
2235
+ * Releases a cached query and frees its resources if no longer needed.
2236
+ * Call this when you're done using a query to allow the world to clean up its cache entry.
2237
+ *
2238
+ * @param query - The query to release
2239
+ *
2240
+ * @example
2241
+ * const query = world.createQuery([Position]);
2242
+ * // ... use query ...
2243
+ * world.releaseQuery(query); // Optional cleanup
2244
+ */
1997
2245
  releaseQuery(query) {
1998
2246
  for (const [k, v] of this.queryCache.entries()) if (v.query === query) {
1999
2247
  v.refCount--;
@@ -2005,6 +2253,14 @@ var World = class {
2005
2253
  return;
2006
2254
  }
2007
2255
  }
2256
+ /**
2257
+ * Returns all archetypes that contain entities with the specified components.
2258
+ * Used internally for query optimization but can be useful for debugging.
2259
+ *
2260
+ * @param componentTypes - Array of component types to match
2261
+ * @returns Array of Archetype objects containing matching components
2262
+ * @internal
2263
+ */
2008
2264
  getMatchingArchetypes(componentTypes) {
2009
2265
  if (componentTypes.length === 0) return [...this.archetypes];
2010
2266
  const regularComponents = [];
@@ -2043,6 +2299,10 @@ var World = class {
2043
2299
  }
2044
2300
  executeEntityCommands(entityId, commands) {
2045
2301
  const changeset = new ComponentChangeset();
2302
+ if (this.isComponentEntityId(entityId)) {
2303
+ this.executeComponentEntityCommands(entityId, commands);
2304
+ return changeset;
2305
+ }
2046
2306
  if (commands.some((cmd) => cmd.type === "destroy")) {
2047
2307
  this.destroyEntityImmediate(entityId);
2048
2308
  return changeset;
@@ -2060,6 +2320,24 @@ var World = class {
2060
2320
  triggerLifecycleHooks(this.createHooksContext(), entityId, changeset.adds, removedComponents, currentArchetype, newArchetype);
2061
2321
  return changeset;
2062
2322
  }
2323
+ executeComponentEntityCommands(entityId, commands) {
2324
+ if (commands.some((cmd) => cmd.type === "destroy")) {
2325
+ this.clearComponentEntityComponents(entityId);
2326
+ return;
2327
+ }
2328
+ for (const command of commands) if (command.type === "set" && command.componentType) this.getComponentEntityComponents(entityId, true).set(command.componentType, command.component);
2329
+ else if (command.type === "delete" && command.componentType) {
2330
+ const data = this.componentEntityComponents.get(entityId);
2331
+ if (!data) continue;
2332
+ if (isWildcardRelationId(command.componentType)) {
2333
+ const componentId = getComponentIdFromRelationId(command.componentType);
2334
+ if (componentId !== void 0) {
2335
+ for (const key of Array.from(data.keys())) if (getComponentIdFromRelationId(key) === componentId) data.delete(key);
2336
+ }
2337
+ } else data.delete(command.componentType);
2338
+ if (data.size === 0) this.clearComponentEntityComponents(entityId);
2339
+ }
2340
+ }
2063
2341
  createHooksContext() {
2064
2342
  return {
2065
2343
  hooks: this.legacyHooks,
@@ -2148,6 +2426,27 @@ var World = class {
2148
2426
  }
2149
2427
  for (const query of this.queries) query.removeArchetype(archetype);
2150
2428
  }
2429
+ /**
2430
+ * Serializes the entire world state to a plain JavaScript object.
2431
+ * This creates a "memory snapshot" that can be stored or transmitted.
2432
+ * The snapshot can be restored using `new World(snapshot)`.
2433
+ *
2434
+ * **Note:** This is NOT automatically persistent storage. To persist data,
2435
+ * you must serialize the returned object to JSON or another format yourself.
2436
+ *
2437
+ * @returns A serializable object representing the world state
2438
+ *
2439
+ * @example
2440
+ * // Create snapshot
2441
+ * const snapshot = world.serialize();
2442
+ *
2443
+ * // Save to storage (example)
2444
+ * localStorage.setItem('save', JSON.stringify(snapshot));
2445
+ *
2446
+ * // Later, restore from snapshot
2447
+ * const savedData = JSON.parse(localStorage.getItem('save'));
2448
+ * const newWorld = new World(savedData);
2449
+ */
2151
2450
  serialize() {
2152
2451
  const entities = [];
2153
2452
  for (const archetype of this.archetypes) {
@@ -2160,10 +2459,19 @@ var World = class {
2160
2459
  }))
2161
2460
  });
2162
2461
  }
2462
+ const componentEntities = [];
2463
+ for (const [entityId, components] of this.componentEntityComponents.entries()) componentEntities.push({
2464
+ id: encodeEntityId(entityId),
2465
+ components: Array.from(components.entries()).map(([rawType, value]) => ({
2466
+ type: encodeEntityId(rawType),
2467
+ value: value === MISSING_COMPONENT ? void 0 : value
2468
+ }))
2469
+ });
2163
2470
  return {
2164
2471
  version: 1,
2165
2472
  entityManager: this.entityIdManager.serializeState(),
2166
- entities
2473
+ entities,
2474
+ componentEntities
2167
2475
  };
2168
2476
  }
2169
2477
  };