@codehz/ecs 0.1.9 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/archetype.d.ts CHANGED
@@ -1,5 +1,9 @@
1
1
  import type { EntityId, WildcardRelationId } from "./entity";
2
2
  import { type ComponentTuple, type ComponentType } from "./types";
3
+ /**
4
+ * Special value to represent missing component data
5
+ */
6
+ export declare const MISSING_COMPONENT: unique symbol;
3
7
  /**
4
8
  * Archetype class for ECS architecture
5
9
  * Represents a group of entities that share the same set of components
@@ -49,6 +53,20 @@ export declare class Archetype {
49
53
  * @param componentData Map of component type to component data
50
54
  */
51
55
  addEntity(entityId: EntityId, componentData: Map<EntityId<any>, any>): void;
56
+ /**
57
+ * Get all component data for a specific entity
58
+ * @param entityId The entity to get data for
59
+ * @returns Map of component type to component data
60
+ */
61
+ getEntity(entityId: EntityId): Map<EntityId<any>, any> | undefined;
62
+ /**
63
+ * Dump all entities and their component data in this archetype
64
+ * @returns Array of objects with entity and component data
65
+ */
66
+ dump(): Array<{
67
+ entity: EntityId;
68
+ components: Map<EntityId<any>, any>;
69
+ }>;
52
70
  /**
53
71
  * Remove an entity from this archetype
54
72
  * @param entityId The entity to remove
package/changeset.d.ts CHANGED
@@ -29,4 +29,10 @@ export declare class ComponentChangeset {
29
29
  * Apply the changeset to existing components and return the final state
30
30
  */
31
31
  applyTo(existingComponents: Map<EntityId<any>, any>): Map<EntityId<any>, any>;
32
+ /**
33
+ * Get the final component types after applying the changeset
34
+ * @param existingComponentTypes - The current component types on the entity
35
+ * @returns The final component types or undefined if no changes
36
+ */
37
+ getFinalComponentTypes(existingComponentTypes: EntityId<any>[]): EntityId<any>[] | undefined;
32
38
  }
package/entity.d.ts CHANGED
@@ -47,7 +47,7 @@ export declare function createEntityId(id: number): EntityId;
47
47
  /**
48
48
  * Type for relation ID based on component and target types
49
49
  */
50
- type RelationIdType<T, R> = R extends ComponentId<infer U> ? U extends void ? ComponentRelationId<T> : ComponentRelationId<T extends void ? U : T & U> : R extends EntityId<any> ? EntityRelationId<T> : never;
50
+ type RelationIdType<T, R> = R extends ComponentId<infer U> ? U extends void ? ComponentRelationId<T> : ComponentRelationId<T extends void ? U : T> : R extends EntityId<any> ? EntityRelationId<T> : never;
51
51
  /**
52
52
  * Create a relation ID by associating a component with another ID (entity or component)
53
53
  * @param componentId The component ID (0-1023)
@@ -167,7 +167,22 @@ export declare class ComponentIdAllocator {
167
167
  hasAvailableIds(): boolean;
168
168
  }
169
169
  /**
170
- * Allocate a new component ID from the global allocator
170
+ * Allocate a new component ID from the global allocator.
171
+ * Optionally register a name for the component.
172
+ * The name is only for serialization/debugging and does not affect base functionality.
173
+ * @param name Optional name for the component
174
+ * @returns The allocated component ID
171
175
  */
172
- export declare function component<T = void>(): ComponentId<T>;
176
+ export declare function component<T = void>(name?: string): ComponentId<T>;
177
+ /**
178
+ * Get a component ID by its registered name
179
+ * @param name The component name
180
+ * @returns The component ID if found, undefined otherwise
181
+ */
182
+ export declare function getComponentIdByName(name: string): ComponentId<any> | undefined;
183
+ /** Get a component name by its ID
184
+ * @param id The component ID
185
+ * @returns The component name if found, undefined otherwise
186
+ */
187
+ export declare function getComponentNameById(id: ComponentId<any>): string | undefined;
173
188
  export {};
package/index.js CHANGED
@@ -214,8 +214,24 @@ class ComponentIdAllocator {
214
214
  }
215
215
  }
216
216
  var globalComponentIdAllocator = new ComponentIdAllocator;
217
- function component() {
218
- return globalComponentIdAllocator.allocate();
217
+ var ComponentNames = new Map;
218
+ var ComponentIdForNames = new Map;
219
+ function component(name) {
220
+ const id = globalComponentIdAllocator.allocate();
221
+ if (name) {
222
+ if (ComponentIdForNames.has(name)) {
223
+ throw new Error(`Component name "${name}" is already registered`);
224
+ }
225
+ ComponentNames.set(id, name);
226
+ ComponentIdForNames.set(name, id);
227
+ }
228
+ return id;
229
+ }
230
+ function getComponentIdByName(name) {
231
+ return ComponentIdForNames.get(name);
232
+ }
233
+ function getComponentNameById(id) {
234
+ return ComponentNames.get(id);
219
235
  }
220
236
  // src/types.ts
221
237
  function isOptionalEntityId(type) {
@@ -277,6 +293,33 @@ class Archetype {
277
293
  this.getComponentData(componentType).push(data === undefined ? MISSING_COMPONENT : data);
278
294
  }
279
295
  }
296
+ getEntity(entityId) {
297
+ const index = this.entityToIndex.get(entityId);
298
+ if (index === undefined) {
299
+ return;
300
+ }
301
+ const entityData = new Map;
302
+ for (const componentType of this.componentTypes) {
303
+ const dataArray = this.getComponentData(componentType);
304
+ const data = dataArray[index];
305
+ entityData.set(componentType, data === MISSING_COMPONENT ? undefined : data);
306
+ }
307
+ return entityData;
308
+ }
309
+ dump() {
310
+ const result = [];
311
+ for (let i = 0;i < this.entities.length; i++) {
312
+ const entity = this.entities[i];
313
+ const components = new Map;
314
+ for (const componentType of this.componentTypes) {
315
+ const dataArray = this.getComponentData(componentType);
316
+ const data = dataArray[i];
317
+ components.set(componentType, data === MISSING_COMPONENT ? undefined : data);
318
+ }
319
+ result.push({ entity, components });
320
+ }
321
+ return result;
322
+ }
280
323
  removeEntity(entityId) {
281
324
  const index = this.entityToIndex.get(entityId);
282
325
  if (index === undefined) {
@@ -364,7 +407,7 @@ class Archetype {
364
407
  return result;
365
408
  }
366
409
  forEachWithComponents(componentTypes, callback) {
367
- const cacheKey = componentTypes.map((id) => id.toString()).join(",");
410
+ const cacheKey = componentTypes.map((id) => isOptionalEntityId(id) ? `opt(${id.optional})` : `${id}`).join(",");
368
411
  const componentDataSources = getOrComputeCache(this.componentDataSourcesCache, cacheKey, () => {
369
412
  return componentTypes.map((compType) => {
370
413
  let optional = false;
@@ -480,6 +523,25 @@ class ComponentChangeset {
480
523
  }
481
524
  return existingComponents;
482
525
  }
526
+ getFinalComponentTypes(existingComponentTypes) {
527
+ const finalComponentTypes = new Set(existingComponentTypes);
528
+ let changed = false;
529
+ for (const componentType of this.removes) {
530
+ if (!finalComponentTypes.has(componentType)) {
531
+ continue;
532
+ }
533
+ changed = true;
534
+ finalComponentTypes.delete(componentType);
535
+ }
536
+ for (const componentType of this.adds.keys()) {
537
+ if (finalComponentTypes.has(componentType)) {
538
+ continue;
539
+ }
540
+ changed = true;
541
+ finalComponentTypes.add(componentType);
542
+ }
543
+ return changed ? Array.from(finalComponentTypes) : undefined;
544
+ }
483
545
  }
484
546
 
485
547
  // src/command-buffer.ts
@@ -564,17 +626,15 @@ function matchesFilter(archetype, filter) {
564
626
 
565
627
  // src/query.ts
566
628
  class Query {
567
- key;
568
629
  world;
569
630
  componentTypes;
570
631
  filter;
571
632
  cachedArchetypes = [];
572
633
  isDisposed = false;
573
- constructor(world, componentTypes, filter = {}, key) {
634
+ constructor(world, componentTypes, filter = {}) {
574
635
  this.world = world;
575
636
  this.componentTypes = [...componentTypes].sort((a, b) => a - b);
576
637
  this.filter = filter;
577
- this.key = key ?? `${this.componentTypes.join(",")}|`;
578
638
  this.updateCache();
579
639
  world._registerQuery(this);
580
640
  }
@@ -744,8 +804,27 @@ class World {
744
804
  const componentMap = new Map;
745
805
  const componentTypes = [];
746
806
  for (const componentEntry of componentsArray) {
747
- componentMap.set(componentEntry.type, componentEntry.value);
748
- componentTypes.push(componentEntry.type);
807
+ const componentTypeRaw = componentEntry.type;
808
+ let componentType;
809
+ if (typeof componentTypeRaw === "number") {
810
+ componentType = componentTypeRaw;
811
+ } else if (typeof componentTypeRaw === "string") {
812
+ const compId = getComponentIdByName(componentTypeRaw);
813
+ if (compId === undefined) {
814
+ throw new Error(`Unknown component name in snapshot: ${componentTypeRaw}`);
815
+ }
816
+ componentType = compId;
817
+ } else if (typeof componentTypeRaw === "object" && componentTypeRaw !== null && typeof componentTypeRaw.component === "string" && typeof componentTypeRaw.target === "number") {
818
+ const compId = getComponentIdByName(componentTypeRaw.component);
819
+ if (compId === undefined) {
820
+ throw new Error(`Unknown component name in snapshot: ${componentTypeRaw.component}`);
821
+ }
822
+ componentType = relation(compId, componentTypeRaw.target);
823
+ } else {
824
+ throw new Error(`Invalid component type in snapshot: ${JSON.stringify(componentTypeRaw)}`);
825
+ }
826
+ componentMap.set(componentType, componentEntry.value);
827
+ componentTypes.push(componentType);
749
828
  }
750
829
  const archetype = this.ensureArchetype(componentTypes);
751
830
  archetype.addEntity(entityId, componentMap);
@@ -895,7 +974,7 @@ class World {
895
974
  cached.refCount++;
896
975
  return cached.query;
897
976
  }
898
- const query = new Query(this, sortedTypes, filter, key);
977
+ const query = new Query(this, sortedTypes, filter);
899
978
  this.queryCache.set(key, { query, refCount: 1 });
900
979
  return query;
901
980
  }
@@ -909,27 +988,17 @@ class World {
909
988
  }
910
989
  }
911
990
  releaseQuery(query) {
912
- const key = query.key;
913
991
  for (const [k, v] of this.queryCache.entries()) {
914
992
  if (v.query === query) {
915
993
  v.refCount--;
916
994
  if (v.refCount <= 0) {
917
995
  this.queryCache.delete(k);
996
+ this._unregisterQuery(query);
918
997
  v.query._disposeInternal();
919
998
  }
920
999
  return;
921
1000
  }
922
1001
  }
923
- const entry = this.queryCache.get(key);
924
- if (!entry) {
925
- this._unregisterQuery(query);
926
- return;
927
- }
928
- entry.refCount--;
929
- if (entry.refCount <= 0) {
930
- this.queryCache.delete(key);
931
- entry.query._disposeInternal();
932
- }
933
1002
  }
934
1003
  getMatchingArchetypes(componentTypes) {
935
1004
  if (componentTypes.length === 0) {
@@ -1010,11 +1079,6 @@ class World {
1010
1079
  if (!currentArchetype) {
1011
1080
  return changeset;
1012
1081
  }
1013
- const currentComponents = new Map;
1014
- for (const componentType of currentArchetype.componentTypes) {
1015
- const componentData = currentArchetype.get(entityId, componentType);
1016
- currentComponents.set(componentType, componentData);
1017
- }
1018
1082
  for (const command of commands) {
1019
1083
  switch (command.type) {
1020
1084
  case "set":
@@ -1051,13 +1115,11 @@ class World {
1051
1115
  break;
1052
1116
  }
1053
1117
  }
1054
- const finalComponents = changeset.applyTo(currentComponents);
1055
- const currentComponentTypes = currentArchetype.componentTypes;
1056
- const needsArchetypeChange = finalComponents.size !== currentComponentTypes.length || !currentComponentTypes.every((type) => finalComponents.has(type));
1057
- if (needsArchetypeChange) {
1058
- const newArchetype = this.ensureArchetype(finalComponents.keys());
1059
- currentArchetype.removeEntity(entityId);
1060
- newArchetype.addEntity(entityId, finalComponents);
1118
+ const finalComponentTypes = changeset.getFinalComponentTypes(currentArchetype.componentTypes);
1119
+ if (finalComponentTypes) {
1120
+ const newArchetype = this.ensureArchetype(finalComponentTypes);
1121
+ const currentComponents = currentArchetype.removeEntity(entityId);
1122
+ newArchetype.addEntity(entityId, changeset.applyTo(currentComponents));
1061
1123
  this.entityToArchetype.set(entityId, newArchetype);
1062
1124
  } else {
1063
1125
  for (const [componentType, component2] of changeset.adds) {
@@ -1196,13 +1258,31 @@ class World {
1196
1258
  }
1197
1259
  serialize() {
1198
1260
  const entities = [];
1199
- for (const [entityId, archetype] of this.entityToArchetype.entries()) {
1200
- const compEntries = [];
1201
- for (const compType of archetype.componentTypes) {
1202
- const value = archetype.get(entityId, compType);
1203
- compEntries.push({ type: compType, value });
1261
+ for (const archetype of this.archetypes) {
1262
+ const dumpedEntities = archetype.dump();
1263
+ for (const { entity, components } of dumpedEntities) {
1264
+ entities.push({
1265
+ id: entity,
1266
+ components: Array.from(components.entries()).map(([rawType, value]) => {
1267
+ const detailedType = getDetailedIdType(rawType);
1268
+ let type = rawType;
1269
+ let componentName;
1270
+ switch (detailedType.type) {
1271
+ case "component":
1272
+ type = getComponentNameById(rawType) || rawType;
1273
+ break;
1274
+ case "entity-relation":
1275
+ case "component-relation":
1276
+ componentName = getComponentNameById(detailedType.componentId);
1277
+ if (componentName) {
1278
+ type = { component: componentName, target: detailedType.targetId };
1279
+ }
1280
+ break;
1281
+ }
1282
+ return { type, value: value === MISSING_COMPONENT ? undefined : value };
1283
+ })
1284
+ });
1204
1285
  }
1205
- entities.push({ id: entityId, components: compEntries });
1206
1286
  }
1207
1287
  return {
1208
1288
  version: 1,
@@ -1222,6 +1302,8 @@ export {
1222
1302
  inspectEntityId,
1223
1303
  getIdType,
1224
1304
  getDetailedIdType,
1305
+ getComponentNameById,
1306
+ getComponentIdByName,
1225
1307
  decodeRelationId,
1226
1308
  createEntityId,
1227
1309
  createComponentId,
@@ -1231,6 +1313,7 @@ export {
1231
1313
  SystemScheduler,
1232
1314
  RELATION_SHIFT,
1233
1315
  Query,
1316
+ MISSING_COMPONENT,
1234
1317
  INVALID_COMPONENT_ID,
1235
1318
  EntityIdManager,
1236
1319
  ENTITY_ID_START,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codehz/ecs",
3
- "version": "0.1.9",
3
+ "version": "0.2.1",
4
4
  "type": "module",
5
5
  "main": "./index.js",
6
6
  "types": "./index.d.ts",
package/query.d.ts CHANGED
@@ -7,13 +7,12 @@ import type { World } from "./world";
7
7
  * Query class for efficient entity queries with cached archetypes
8
8
  */
9
9
  export declare class Query {
10
- readonly key: string;
11
10
  private world;
12
11
  private componentTypes;
13
12
  private filter;
14
13
  private cachedArchetypes;
15
14
  private isDisposed;
16
- constructor(world: World<any[]>, componentTypes: EntityId<any>[], filter?: QueryFilter, key?: string);
15
+ constructor(world: World<any[]>, componentTypes: EntityId<any>[], filter?: QueryFilter);
17
16
  /**
18
17
  * Get all entities matching the query
19
18
  */
package/world.d.ts CHANGED
@@ -40,7 +40,7 @@ export declare class World<UpdateParams extends any[] = []> {
40
40
  * If an optional snapshot object is provided (previously produced by `world.serialize()`),
41
41
  * the world will be restored from that snapshot. The snapshot may contain non-JSON values.
42
42
  */
43
- constructor(snapshot?: any);
43
+ constructor(snapshot?: SerializedWorld);
44
44
  /**
45
45
  * Generate a signature string for component types array
46
46
  * @returns A string signature for the component types
@@ -192,5 +192,22 @@ export declare class World<UpdateParams extends any[] = []> {
192
192
  * This returns an in-memory structure and does not perform JSON stringification.
193
193
  * Component values are stored as-is (they may be non-JSON-serializable).
194
194
  */
195
- serialize(): any;
195
+ serialize(): SerializedWorld;
196
196
  }
197
+ export type SerializedWorld = {
198
+ version: number;
199
+ entityManager: any;
200
+ exclusiveComponents: EntityId[];
201
+ entities: SerializedEntity[];
202
+ };
203
+ export type SerializedEntity = {
204
+ id: number;
205
+ components: SerializedComponent[];
206
+ };
207
+ export type SerializedComponent = {
208
+ type: number | string | {
209
+ component: string;
210
+ target: number;
211
+ };
212
+ value: any;
213
+ };