@codehz/ecs 0.7.1 → 0.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/{builder.d.mts → dist/builder.d.mts} +4 -2
  2. package/{world.mjs → dist/world.mjs} +9 -30
  3. package/dist/world.mjs.map +1 -0
  4. package/examples/advanced-scheduling.ts +96 -0
  5. package/examples/collision-detection.ts +229 -0
  6. package/examples/inventory-system-relations.ts +108 -0
  7. package/examples/parent-child-hierarchy.ts +206 -0
  8. package/examples/serialization.ts +337 -0
  9. package/examples/simple.ts +96 -0
  10. package/examples/spatial-grid.ts +276 -0
  11. package/examples/state-machine.ts +273 -0
  12. package/examples/tag-filtering.ts +266 -0
  13. package/package.json +58 -12
  14. package/src/__tests__/commands/buffer-limits.test.ts +72 -0
  15. package/src/__tests__/commands/buffer.test.ts +195 -0
  16. package/src/__tests__/component/singleton.test.ts +148 -0
  17. package/src/__tests__/core/archetype.test.ts +247 -0
  18. package/src/__tests__/core/bitset.test.ts +171 -0
  19. package/src/__tests__/core/changeset.test.ts +254 -0
  20. package/src/__tests__/core/multi-map.test.ts +74 -0
  21. package/src/__tests__/entity/component-registry.test.ts +66 -0
  22. package/src/__tests__/entity/entity.test.ts +520 -0
  23. package/src/__tests__/entity/id-manager.test.ts +157 -0
  24. package/src/__tests__/entity/id-system.test.ts +260 -0
  25. package/src/__tests__/perf/comprehensive.perf.test.ts +300 -0
  26. package/src/__tests__/perf/sync-hotpath.perf.test.ts +79 -0
  27. package/src/__tests__/query/basic.test.ts +341 -0
  28. package/src/__tests__/query/caching.test.ts +112 -0
  29. package/src/__tests__/query/filter.test.ts +111 -0
  30. package/src/__tests__/query/optional.test.ts +231 -0
  31. package/src/__tests__/query/perf.test.ts +99 -0
  32. package/src/__tests__/relations/dont-fragment/basic.test.ts +496 -0
  33. package/src/__tests__/relations/dont-fragment/query-notification.test.ts +125 -0
  34. package/src/__tests__/relations/wildcard.test.ts +179 -0
  35. package/src/__tests__/serialization/bounds.test.ts +237 -0
  36. package/src/__tests__/testing/assertions.test.ts +224 -0
  37. package/src/__tests__/testing/entity-builder.test.ts +84 -0
  38. package/src/__tests__/testing/snapshot.test.ts +150 -0
  39. package/src/__tests__/testing/world-fixture.test.ts +73 -0
  40. package/src/__tests__/world/component-hooks.test.ts +185 -0
  41. package/src/__tests__/world/component-management.test.ts +447 -0
  42. package/src/__tests__/world/entity-management.test.ts +86 -0
  43. package/src/__tests__/world/get-optional.test.ts +96 -0
  44. package/src/__tests__/world/multi-component-hooks.test.ts +502 -0
  45. package/src/__tests__/world/perf.test.ts +93 -0
  46. package/src/__tests__/world/query.test.ts +223 -0
  47. package/src/__tests__/world/serialize.test.ts +83 -0
  48. package/src/__tests__/world/wildcard-relation-hooks.test.ts +332 -0
  49. package/src/archetype/archetype.ts +472 -0
  50. package/src/archetype/helpers.ts +186 -0
  51. package/src/archetype/store.ts +33 -0
  52. package/src/commands/buffer.ts +110 -0
  53. package/src/commands/changeset.ts +104 -0
  54. package/src/component/entity-store.ts +223 -0
  55. package/src/component/registry.ts +657 -0
  56. package/src/component/type-utils.ts +9 -0
  57. package/src/entity/index.ts +63 -0
  58. package/src/entity/manager.ts +115 -0
  59. package/src/entity/relation.ts +319 -0
  60. package/src/entity/types.ts +135 -0
  61. package/src/index.ts +41 -0
  62. package/src/query/filter.ts +75 -0
  63. package/src/query/query.ts +313 -0
  64. package/src/query/registry.ts +101 -0
  65. package/src/storage/serialization.ts +130 -0
  66. package/src/testing/index.ts +634 -0
  67. package/src/types/index.ts +99 -0
  68. package/src/utils/bit-set.ts +133 -0
  69. package/src/utils/multi-map.ts +96 -0
  70. package/src/utils/utils.ts +19 -0
  71. package/src/world/builder.ts +100 -0
  72. package/src/world/commands.ts +378 -0
  73. package/src/world/hooks.ts +358 -0
  74. package/src/world/references.ts +38 -0
  75. package/src/world/serialization.ts +122 -0
  76. package/src/world/world.ts +1201 -0
  77. package/world.mjs.map +0 -1
  78. /package/{index.d.mts → dist/index.d.mts} +0 -0
  79. /package/{index.mjs → dist/index.mjs} +0 -0
  80. /package/{testing.d.mts → dist/testing.d.mts} +0 -0
  81. /package/{testing.mjs → dist/testing.mjs} +0 -0
  82. /package/{testing.mjs.map → dist/testing.mjs.map} +0 -0
@@ -0,0 +1,1201 @@
1
+ import { Archetype } from "../archetype/archetype";
2
+ import { DontFragmentStoreImpl } from "../archetype/store";
3
+ import { CommandBuffer, type Command } from "../commands/buffer";
4
+ import { ComponentChangeset } from "../commands/changeset";
5
+ import { ComponentEntityStore } from "../component/entity-store";
6
+ import { normalizeComponentTypes } from "../component/type-utils";
7
+ import type { ComponentId, EntityId, WildcardRelationId } from "../entity";
8
+ import {
9
+ ENTITY_ID_START,
10
+ EntityIdManager,
11
+ RELATION_SHIFT,
12
+ getComponentIdFromRelationId,
13
+ getDetailedIdType,
14
+ getTargetIdFromRelationId,
15
+ isCascadeDeleteRelation,
16
+ isDontFragmentRelation,
17
+ isDontFragmentWildcard,
18
+ isEntityRelation,
19
+ isExclusiveComponent,
20
+ isWildcardRelationId,
21
+ } from "../entity";
22
+ import { matchesFilter, serializeQueryFilter, type QueryFilter } from "../query/filter";
23
+ import type { Query } from "../query/query";
24
+ import { QueryRegistry } from "../query/registry";
25
+ import type { SerializedWorld } from "../storage/serialization";
26
+ import type { ComponentTuple, ComponentType, LifecycleCallback, LifecycleHook, LifecycleHookEntry } from "../types";
27
+ import { isOptionalEntityId } from "../types";
28
+ import { getOrCompute } from "../utils/utils";
29
+ import { EntityBuilder } from "./builder";
30
+ import {
31
+ applyChangeset,
32
+ filterRegularComponentTypes,
33
+ maybeRemoveWildcardMarker,
34
+ processCommands,
35
+ removeMatchingRelations,
36
+ type CommandProcessorContext,
37
+ } from "./commands";
38
+ import {
39
+ collectMultiHookComponents,
40
+ triggerLifecycleHooks,
41
+ triggerRemoveHooksForEntityDeletion,
42
+ type HooksContext,
43
+ } from "./hooks";
44
+ import {
45
+ getEntityReferences,
46
+ trackEntityReference,
47
+ untrackEntityReference,
48
+ type EntityReferencesMap,
49
+ } from "./references";
50
+ import { deserializeWorld, serializeWorld } from "./serialization";
51
+
52
+ /**
53
+ * World class for ECS architecture
54
+ * Manages entities and components
55
+ */
56
+ export class World {
57
+ // Core data structures for entity and archetype management
58
+ private entityIdManager = new EntityIdManager();
59
+ private archetypes: Archetype[] = [];
60
+ private archetypeBySignature = new Map<string, Archetype>();
61
+ private entityToArchetype = new Map<EntityId, Archetype>();
62
+ private archetypesByComponent = new Map<EntityId<any>, Set<Archetype>>();
63
+ private entityReferences: EntityReferencesMap = new Map();
64
+ /** Reverse index: entity ID → set of archetypes whose componentTypes include that entity ID */
65
+ private entityToReferencingArchetypes = new Map<EntityId, Set<Archetype>>();
66
+ /** DontFragment relation storage, shared with all Archetype instances */
67
+ private readonly dontFragmentStore = new DontFragmentStoreImpl();
68
+ /** Component entity (singleton) storage */
69
+ private readonly componentEntities = new ComponentEntityStore();
70
+
71
+ // Query registry – manages caching, ref counts, and archetype notifications
72
+ private readonly queryRegistry = new QueryRegistry();
73
+
74
+ // Lifecycle hooks (declared before cached contexts that reference them)
75
+ private hooks: Set<LifecycleHookEntry> = new Set();
76
+
77
+ // Command execution
78
+ private commandBuffer = new CommandBuffer((entityId, commands) => this.executeEntityCommands(entityId, commands));
79
+
80
+ // Reusable instances to reduce per-frame allocations
81
+ private readonly _changeset = new ComponentChangeset();
82
+ private readonly _removeChangeset = new ComponentChangeset();
83
+ /** Cached command processor context to avoid per-entity object allocation */
84
+ private readonly _commandCtx: CommandProcessorContext = {
85
+ dontFragmentStore: this.dontFragmentStore,
86
+ ensureArchetype: (ct) => this.ensureArchetype(ct),
87
+ };
88
+ /** Cached hooks context to avoid per-entity object allocation */
89
+ private readonly _hooksCtx: HooksContext = {
90
+ multiHooks: this.hooks,
91
+ has: (eid, ct) => this.has(eid, ct),
92
+ get: (eid, ct) => this.get(eid, ct),
93
+ getOptional: (eid, ct) => this.getOptional(eid, ct),
94
+ };
95
+
96
+ constructor(snapshot?: SerializedWorld) {
97
+ if (snapshot && typeof snapshot === "object") {
98
+ deserializeWorld(
99
+ {
100
+ entityIdManager: this.entityIdManager,
101
+ componentEntities: this.componentEntities,
102
+ entityReferences: this.entityReferences,
103
+ ensureArchetype: (ct) => this.ensureArchetype(ct),
104
+ setEntityToArchetype: (eid, arch) => this.entityToArchetype.set(eid, arch),
105
+ },
106
+ snapshot,
107
+ );
108
+ }
109
+ }
110
+
111
+ private createArchetypeSignature(componentTypes: EntityId<any>[]): string {
112
+ return componentTypes.join(",");
113
+ }
114
+
115
+ /**
116
+ * Creates a new entity.
117
+ * The entity is created with an empty component set and can be configured using `set()`.
118
+ *
119
+ * @template T - The initial component type (defaults to void if not specified)
120
+ * @returns A unique identifier for the new entity
121
+ *
122
+ * @example
123
+ * const entity = world.new<MyComponent>();
124
+ * world.set(entity, MyComponent, { value: 42 });
125
+ * world.sync();
126
+ */
127
+ new<T = void>(): EntityId<T> {
128
+ const entityId = this.entityIdManager.allocate();
129
+ let emptyArchetype = this.ensureArchetype([]);
130
+ emptyArchetype.addEntity(entityId, new Map());
131
+ this.entityToArchetype.set(entityId, emptyArchetype);
132
+ return entityId as EntityId<T>;
133
+ }
134
+
135
+ /**
136
+ * Semantic alias for `new()` to avoid confusion with the `new` keyword.
137
+ * Creates a new entity with an empty component set.
138
+ *
139
+ * @example
140
+ * const entity = world.create<MyComponent>();
141
+ */
142
+ create<T = void>(): EntityId<T> {
143
+ return this.new<T>();
144
+ }
145
+
146
+ /** Fast path: destroy an entity that is not referenced by any other entity, skipping BFS */
147
+ private destroySingleEntity(entityId: EntityId): void {
148
+ const archetype = this.entityToArchetype.get(entityId);
149
+ if (!archetype) return;
150
+
151
+ // Handle entity references (this entity references other entities)
152
+ for (const [sourceEntityId, componentType] of getEntityReferences(this.entityReferences, entityId)) {
153
+ if (this.entityToArchetype.has(sourceEntityId)) {
154
+ this.removeComponentImmediate(sourceEntityId, componentType, entityId);
155
+ }
156
+ }
157
+
158
+ this.entityReferences.delete(entityId);
159
+ const removedComponents = archetype.removeEntity(entityId)!;
160
+ this.entityToArchetype.delete(entityId);
161
+
162
+ triggerRemoveHooksForEntityDeletion(entityId, removedComponents, archetype);
163
+
164
+ this.cleanupArchetypesReferencingEntity(entityId);
165
+ this.entityIdManager.deallocate(entityId);
166
+ this.componentEntities.cleanupReferencesTo(entityId);
167
+ }
168
+
169
+ private destroyEntityImmediate(entityId: EntityId): void {
170
+ // Fast path: no other entity references this one, delete directly
171
+ if (!this.entityReferences.has(entityId)) {
172
+ this.destroySingleEntity(entityId);
173
+ return;
174
+ }
175
+
176
+ const queue: EntityId[] = [entityId];
177
+ const visited = new Set<EntityId>();
178
+ let queueIndex = 0;
179
+
180
+ while (queueIndex < queue.length) {
181
+ const cur = queue[queueIndex++]!;
182
+ if (visited.has(cur)) continue;
183
+ visited.add(cur);
184
+
185
+ const archetype = this.entityToArchetype.get(cur);
186
+ if (!archetype) continue;
187
+
188
+ // Process entity references before removal
189
+ for (const [sourceEntityId, componentType] of getEntityReferences(this.entityReferences, cur)) {
190
+ if (!this.entityToArchetype.has(sourceEntityId)) continue;
191
+
192
+ if (isCascadeDeleteRelation(componentType)) {
193
+ if (!visited.has(sourceEntityId)) {
194
+ queue.push(sourceEntityId);
195
+ }
196
+ } else {
197
+ this.removeComponentImmediate(sourceEntityId, componentType, cur);
198
+ }
199
+ }
200
+
201
+ // Remove entity from archetype - this also cleans up dontFragment relations
202
+ // and returns all removed component data
203
+ this.entityReferences.delete(cur);
204
+ const removedComponents = archetype.removeEntity(cur)!;
205
+ this.entityToArchetype.delete(cur);
206
+
207
+ // Trigger lifecycle hooks for removed components (fast path for entity deletion)
208
+ triggerRemoveHooksForEntityDeletion(cur, removedComponents, archetype);
209
+
210
+ this.cleanupArchetypesReferencingEntity(cur);
211
+ this.entityIdManager.deallocate(cur);
212
+ this.componentEntities.cleanupReferencesTo(cur);
213
+ }
214
+ }
215
+
216
+ /**
217
+ * Checks if an **entity** (not a component) exists in the world.
218
+ *
219
+ * This is specifically for checking entity liveness — whether the given entity ID
220
+ * is currently alive in the world. For checking if a component is present on an
221
+ * entity, use {@link has} instead.
222
+ *
223
+ * @param entityId - The entity identifier to check
224
+ * @returns `true` if the entity exists, `false` otherwise
225
+ *
226
+ * @example
227
+ * // Check if an entity is alive
228
+ * if (world.exists(entityId)) {
229
+ * console.log("Entity exists");
230
+ * }
231
+ *
232
+ * // To check for a component, use has() instead:
233
+ * if (world.has(entity, Position)) { ... }
234
+ */
235
+ exists(entityId: EntityId): boolean {
236
+ if (this.componentEntities.exists(entityId)) return true;
237
+ return this.entityToArchetype.has(entityId);
238
+ }
239
+
240
+ private assertEntityExists(entityId: EntityId, label: "Entity" | "Component entity"): void {
241
+ if (!this.exists(entityId)) {
242
+ throw new Error(`${label} ${entityId} does not exist`);
243
+ }
244
+ }
245
+
246
+ private assertComponentTypeValid(componentType: EntityId): void {
247
+ const detailedType = getDetailedIdType(componentType);
248
+ if (detailedType.type === "invalid") {
249
+ throw new Error(`Invalid component type: ${componentType}`);
250
+ }
251
+ }
252
+
253
+ private assertSetComponentTypeValid(componentType: EntityId): void {
254
+ const detailedType = getDetailedIdType(componentType);
255
+ if (detailedType.type === "invalid") {
256
+ throw new Error(`Invalid component type: ${componentType}`);
257
+ }
258
+ if (detailedType.type === "wildcard-relation") {
259
+ throw new Error(`Cannot directly add wildcard relation components: ${componentType}`);
260
+ }
261
+ }
262
+
263
+ private resolveSetOperation(
264
+ entityId: EntityId | ComponentId,
265
+ componentTypeOrComponent?: EntityId | any,
266
+ maybeComponent?: any,
267
+ ): { entityId: EntityId; componentType: EntityId; component: any } {
268
+ // Handle singleton component overload: set(componentId, data)
269
+ if (maybeComponent === undefined && componentTypeOrComponent !== undefined) {
270
+ const detailedType = getDetailedIdType(entityId);
271
+ if (detailedType.type === "component" || detailedType.type === "component-relation") {
272
+ const componentId = entityId as ComponentId;
273
+ this.assertEntityExists(componentId, "Component entity");
274
+ this.assertSetComponentTypeValid(componentId);
275
+ return { entityId: componentId, componentType: componentId, component: componentTypeOrComponent };
276
+ }
277
+ }
278
+
279
+ const targetEntityId = entityId as EntityId;
280
+ const componentType = componentTypeOrComponent as EntityId;
281
+ this.assertEntityExists(targetEntityId, "Entity");
282
+ this.assertSetComponentTypeValid(componentType);
283
+
284
+ return { entityId: targetEntityId, componentType, component: maybeComponent };
285
+ }
286
+
287
+ private resolveRemoveOperation<T>(
288
+ entityId: EntityId | ComponentId,
289
+ componentType?: EntityId<T>,
290
+ ): { entityId: EntityId; componentType: EntityId } {
291
+ // Handle singleton component overload: remove(componentId)
292
+ if (componentType === undefined) {
293
+ const componentId = entityId as ComponentId<T>;
294
+ this.assertEntityExists(componentId, "Component entity");
295
+ return { entityId: componentId, componentType: componentId };
296
+ }
297
+
298
+ const targetEntityId = entityId as EntityId;
299
+ this.assertEntityExists(targetEntityId, "Entity");
300
+ this.assertComponentTypeValid(componentType);
301
+
302
+ return { entityId: targetEntityId, componentType };
303
+ }
304
+
305
+ /**
306
+ * Adds or updates a component on an entity (or marks void component as present).
307
+ * The change is buffered and takes effect after calling `world.sync()`.
308
+ * If the entity does not exist, throws an error.
309
+ *
310
+ * @overload set(entityId: EntityId, componentType: EntityId<void>): void
311
+ * Marks a void component as present on the entity
312
+ *
313
+ * @overload set<T>(entityId: EntityId, componentType: EntityId<T>, component: NoInfer<T>): void
314
+ * Adds or updates a component with data on the entity
315
+ *
316
+ * @overload set<T>(componentId: ComponentId<T>, component: NoInfer<T>): void
317
+ * Adds or updates a singleton component (shorthand for set(componentId, componentId, component))
318
+ *
319
+ * @throws {Error} If the entity does not exist
320
+ * @throws {Error} If the component type is invalid or is a wildcard relation
321
+ *
322
+ * @example
323
+ * world.set(entity, Position, { x: 10, y: 20 });
324
+ * world.set(entity, Marker); // void component
325
+ * world.set(GlobalConfig, { debug: true }); // singleton component
326
+ * world.sync(); // Apply changes
327
+ */
328
+ set(entityId: EntityId, componentType: EntityId<void>): void;
329
+ set<T>(entityId: EntityId, componentType: EntityId<T>, component: NoInfer<T>): void;
330
+ set<T>(componentId: ComponentId<T>, component: NoInfer<T>): void;
331
+ set(entityId: EntityId | ComponentId, componentTypeOrComponent?: EntityId | any, maybeComponent?: any): void {
332
+ const {
333
+ entityId: targetEntityId,
334
+ componentType,
335
+ component,
336
+ } = this.resolveSetOperation(entityId, componentTypeOrComponent, maybeComponent);
337
+ this.commandBuffer.set(targetEntityId, componentType, component);
338
+ }
339
+
340
+ /**
341
+ * Removes a component from an entity.
342
+ * The change is buffered and takes effect after calling `world.sync()`.
343
+ * If the entity does not exist, throws an error.
344
+ *
345
+ * @overload remove<T>(entityId: EntityId, componentType: EntityId<T>): void
346
+ * Removes a component from an entity.
347
+ *
348
+ * @overload remove<T>(componentId: ComponentId<T>): void
349
+ * Removes a singleton component (shorthand for remove(componentId, componentId)).
350
+ *
351
+ * @template T - The component data type
352
+ * @param entityId - The entity identifier
353
+ * @param componentType - The component type to remove
354
+ *
355
+ * @throws {Error} If the entity does not exist
356
+ * @throws {Error} If the component type is invalid
357
+ *
358
+ * @example
359
+ * world.remove(entity, Position);
360
+ * world.remove(GlobalConfig); // Remove singleton component
361
+ * world.sync(); // Apply changes
362
+ */
363
+ remove<T>(componentId: ComponentId<T>): void;
364
+ remove<T>(entityId: EntityId, componentType: EntityId<T>): void;
365
+ remove<T>(entityId: EntityId | ComponentId, componentType?: EntityId<T>): void {
366
+ const { entityId: targetEntityId, componentType: targetComponentType } = this.resolveRemoveOperation(
367
+ entityId,
368
+ componentType,
369
+ );
370
+ this.commandBuffer.remove(targetEntityId, targetComponentType);
371
+ }
372
+
373
+ /**
374
+ * Deletes an entity and all its components from the world.
375
+ * The change is buffered and takes effect after calling `world.sync()`.
376
+ * Related entities may trigger cascade delete hooks if configured.
377
+ *
378
+ * @param entityId - The entity identifier to delete
379
+ *
380
+ * @example
381
+ * world.delete(entity);
382
+ * world.sync(); // Apply changes
383
+ */
384
+ delete(entityId: EntityId): void {
385
+ this.commandBuffer.delete(entityId);
386
+ }
387
+
388
+ /**
389
+ * Checks if a specific **component** is present on an entity.
390
+ *
391
+ * This is for component membership checks — does the given entity have this
392
+ * component type? For checking whether an entity itself is alive, use
393
+ * {@link exists} instead.
394
+ *
395
+ * Immediately reflects the current state without waiting for `sync()`.
396
+ *
397
+ * @overload has<T>(entityId: EntityId, componentType: EntityId<T>): boolean
398
+ * Checks if a specific component type is present on the entity.
399
+ *
400
+ * @overload has<T>(componentId: ComponentId<T>): boolean
401
+ * Shorthand for checking a **singleton component** — a component that is its own
402
+ * entity (component-as-entity pattern). Equivalent to `has(componentId, componentId)`.
403
+ *
404
+ * @template T - The component data type
405
+ * @param entityId - The entity identifier, or a singleton component ID
406
+ * @param componentType - The component type to check
407
+ * @returns `true` if the entity has the component, `false` otherwise
408
+ *
409
+ * @example
410
+ * // Check if an entity has a component
411
+ * if (world.has(entity, Position)) {
412
+ * const pos = world.get(entity, Position);
413
+ * }
414
+ *
415
+ * // Check a singleton component (component-as-entity)
416
+ * if (world.has(GlobalConfig)) {
417
+ * const config = world.get(GlobalConfig);
418
+ * }
419
+ *
420
+ * // Use exists() for entity liveness checks
421
+ * if (world.exists(entity)) { ... }
422
+ */
423
+ has<T>(componentId: ComponentId<T>): boolean;
424
+ has<T>(entityId: EntityId, componentType: EntityId<T>): boolean;
425
+ has<T>(entityId: EntityId | ComponentId, componentType?: EntityId<T>): boolean {
426
+ // Handle singleton component overload: has(componentId)
427
+ if (componentType === undefined) {
428
+ const componentId = entityId as ComponentId<T>;
429
+ return this.componentEntities.hasSingleton(componentId);
430
+ }
431
+
432
+ if (this.componentEntities.exists(entityId)) {
433
+ if (isWildcardRelationId(componentType)) {
434
+ const componentId = getComponentIdFromRelationId(componentType);
435
+ if (componentId === undefined) return false;
436
+ return this.componentEntities.hasWildcard(entityId, componentId);
437
+ }
438
+ return this.componentEntities.has(entityId, componentType);
439
+ }
440
+
441
+ const archetype = this.entityToArchetype.get(entityId);
442
+ if (!archetype) return false;
443
+
444
+ if (archetype.componentTypeSet.has(componentType)) return true;
445
+
446
+ if (isDontFragmentRelation(componentType)) {
447
+ return this.dontFragmentStore.get(entityId)?.has(componentType) ?? false;
448
+ }
449
+
450
+ return false;
451
+ }
452
+
453
+ /**
454
+ * Retrieves a component from an entity.
455
+ * For wildcard relations, returns all relations of that type.
456
+ * Throws an error if the component does not exist; use `has()` to check first or use `getOptional()`.
457
+ *
458
+ * @overload get<T>(entityId: EntityId<T>): T
459
+ * When called with only an entity ID, retrieves the entity's primary component.
460
+ *
461
+ * @overload get<T>(entityId: EntityId, componentType: WildcardRelationId<T>): [EntityId<unknown>, T][]
462
+ * For wildcard relations, returns an array of [target entity, component value] pairs.
463
+ *
464
+ * @overload get<T>(entityId: EntityId, componentType: EntityId<T>): T
465
+ * Retrieves a specific component from the entity.
466
+ *
467
+ * @throws {Error} If the entity does not exist
468
+ * @throws {Error} If the component does not exist on the entity
469
+ *
470
+ * @example
471
+ * const position = world.get(entity, Position); // Throws if no Position
472
+ * const relations = world.get(entity, relation(Parent, "*")); // Wildcard relation
473
+ */
474
+ get<T>(entityId: EntityId<T>): T;
475
+ get<T>(entityId: EntityId, componentType: WildcardRelationId<T>): [EntityId<unknown>, T][];
476
+ get<T>(entityId: EntityId, componentType: EntityId<T>): T;
477
+ get<T>(
478
+ entityId: EntityId,
479
+ componentType: EntityId<T> | WildcardRelationId<T> = entityId as EntityId<T>,
480
+ ): T | [EntityId<unknown>, any][] {
481
+ if (this.componentEntities.exists(entityId)) {
482
+ if (isWildcardRelationId(componentType as EntityId<any>)) {
483
+ return this.componentEntities.getWildcard(entityId, componentType as WildcardRelationId<T>);
484
+ }
485
+ return this.componentEntities.get(entityId, componentType as EntityId<T>);
486
+ }
487
+
488
+ const archetype = this.entityToArchetype.get(entityId);
489
+ if (!archetype) {
490
+ throw new Error(`Entity ${entityId} does not exist`);
491
+ }
492
+
493
+ if (componentType >= 0 || componentType % RELATION_SHIFT !== 0) {
494
+ const inArchetype = archetype.componentTypeSet.has(componentType);
495
+ const hasDontFragment = isDontFragmentRelation(componentType);
496
+ const hasComponent = inArchetype || (hasDontFragment && this.dontFragmentStore.get(entityId)?.has(componentType));
497
+
498
+ if (!hasComponent) {
499
+ throw new Error(
500
+ `Entity ${entityId} does not have component ${componentType}. Use has() to check component existence before calling get().`,
501
+ );
502
+ }
503
+ }
504
+
505
+ return archetype.get(entityId, componentType) as T | [EntityId<unknown>, any][];
506
+ }
507
+
508
+ /**
509
+ * Safely retrieves a component from an entity without throwing an error.
510
+ * Returns `undefined` if the component does not exist.
511
+ * For wildcard relations, returns `undefined` if there are no relations.
512
+ *
513
+ * @template T - The component data type
514
+ * @overload getOptional<T>(entityId: EntityId<T>): { value: T } | undefined
515
+ * Retrieves the entity's primary component safely.
516
+ *
517
+ * @overload getOptional<T>(entityId: EntityId, componentType: WildcardRelationId<T>): { value: [EntityId<unknown>, T][] } | undefined
518
+ * Retrieves all matching relation values safely.
519
+ *
520
+ * @overload getOptional<T>(entityId: EntityId, componentType: EntityId<T>): { value: T } | undefined
521
+ * Retrieves a specific component safely.
522
+ *
523
+ * @throws {Error} If the entity does not exist
524
+ *
525
+ * @example
526
+ * const position = world.getOptional(entity, Position);
527
+ * if (position) {
528
+ * console.log(position.value.x);
529
+ * }
530
+ */
531
+ getOptional<T>(entityId: EntityId<T>): { value: T } | undefined;
532
+ getOptional<T>(
533
+ entityId: EntityId,
534
+ componentType: WildcardRelationId<T>,
535
+ ): { value: [EntityId<unknown>, T][] } | undefined;
536
+ getOptional<T>(entityId: EntityId, componentType: EntityId<T>): { value: T } | undefined;
537
+ getOptional<T>(
538
+ entityId: EntityId,
539
+ componentType: EntityId<T> | WildcardRelationId<T> = entityId as EntityId<T>,
540
+ ): { value: T } | { value: [EntityId<unknown>, T][] } | undefined {
541
+ if (this.componentEntities.exists(entityId)) {
542
+ if (isWildcardRelationId(componentType)) {
543
+ const relations = this.componentEntities.getWildcard(entityId, componentType);
544
+ if (relations.length === 0) return undefined;
545
+ return { value: relations };
546
+ }
547
+ return this.componentEntities.getOptional(entityId, componentType);
548
+ }
549
+
550
+ const archetype = this.entityToArchetype.get(entityId);
551
+ if (!archetype) {
552
+ throw new Error(`Entity ${entityId} does not exist`);
553
+ }
554
+
555
+ if (isWildcardRelationId(componentType)) {
556
+ // For wildcard relations, get the data and wrap in optional if non-empty
557
+ const wildcardData = archetype.get(entityId, componentType) as [EntityId<unknown>, T][];
558
+ if (Array.isArray(wildcardData) && wildcardData.length > 0) {
559
+ return { value: wildcardData };
560
+ }
561
+ return undefined;
562
+ }
563
+
564
+ return archetype.getOptional(entityId, componentType);
565
+ }
566
+
567
+ /**
568
+ * Registers a lifecycle hook that responds to component changes.
569
+ * The hook callback is invoked when components matching the specified types are added, updated, or removed.
570
+ * @overload hook<const T extends readonly ComponentType<any>[]>(
571
+ * componentTypes: T,
572
+ * hook: LifecycleHook<T> | LifecycleCallback<T>,
573
+ * filter?: QueryFilter,
574
+ * ): () => void
575
+ * Registers a hook for multiple component types.
576
+ * The hook is triggered when entities enter/exit the matching set.
577
+ *
578
+ * @param componentTypes - Component types that define the matching entity set
579
+ * @param hook - Either a hook object with on_init/on_set/on_remove handlers, or a callback function
580
+ * @param filter - Optional query-style filter applied to the hook match set
581
+ * @returns A function that unsubscribes the hook when called
582
+ *
583
+ * @throws {Error} If no required components are specified in array overload
584
+ *
585
+ * @example
586
+ * const unsubscribe = world.hook([Position, Velocity], {
587
+ * on_init: (entityId, position, velocity) => console.log("Initialized"),
588
+ * on_set: (entityId, position, velocity) => console.log("Updated"),
589
+ * on_remove: (entityId, position, velocity) => console.log("Removed"),
590
+ * });
591
+ * unsubscribe(); // Remove hook
592
+ *
593
+ * // Callback style
594
+ * const unsubscribe = world.hook([Position], (event, entityId, position) => {
595
+ * if (event === "init") console.log("Initialized");
596
+ * });
597
+ *
598
+ * // With filter
599
+ * const unsubscribe2 = world.hook(
600
+ * [Position, Velocity],
601
+ * {
602
+ * on_set: (entityId, position, velocity) => console.log(entityId, position, velocity),
603
+ * },
604
+ * { negativeComponentTypes: [Disabled] },
605
+ * );
606
+ */
607
+ hook<const T extends readonly ComponentType<any>[]>(
608
+ componentTypes: T,
609
+ hook: LifecycleHook<T> | LifecycleCallback<T>,
610
+ filter?: QueryFilter,
611
+ ): () => void;
612
+ hook(
613
+ componentTypes: readonly ComponentType<any>[],
614
+ hook: LifecycleHook<any> | LifecycleCallback<any>,
615
+ filter?: QueryFilter,
616
+ ): () => void {
617
+ const isCallback = typeof hook === "function";
618
+ const callback = isCallback ? (hook as LifecycleCallback<any>) : undefined;
619
+
620
+ const requiredComponents: EntityId<any>[] = [];
621
+ const optionalComponents: EntityId<any>[] = [];
622
+ for (const ct of componentTypes) {
623
+ if (!isOptionalEntityId(ct)) {
624
+ requiredComponents.push(ct as EntityId<any>);
625
+ } else {
626
+ optionalComponents.push(ct.optional);
627
+ }
628
+ }
629
+
630
+ if (requiredComponents.length === 0) {
631
+ throw new Error("Hook must have at least one required component");
632
+ }
633
+
634
+ const entry: LifecycleHookEntry = {
635
+ componentTypes,
636
+ requiredComponents,
637
+ optionalComponents,
638
+ filter: filter || {},
639
+ hook: isCallback ? ({} as LifecycleHook<any>) : (hook as LifecycleHook<any>),
640
+ callback,
641
+ matchedArchetypes: new Set(),
642
+ };
643
+ this.hooks.add(entry);
644
+
645
+ // Single pass: collect matching archetypes
646
+ const matchedArchetypes: Archetype[] = [];
647
+ for (const archetype of this.archetypes) {
648
+ if (this.archetypeMatchesHook(archetype, entry)) {
649
+ archetype.matchingMultiHooks.add(entry);
650
+ entry.matchedArchetypes!.add(archetype);
651
+ matchedArchetypes.push(archetype);
652
+ }
653
+ }
654
+
655
+ // Callback style: invoke callback("init", ...); hook style: invoke hook.on_init(...)
656
+ const shouldFireInit = isCallback || (hook as LifecycleHook<any>).on_init !== undefined;
657
+ if (shouldFireInit) {
658
+ for (const archetype of matchedArchetypes) {
659
+ for (const entityId of archetype.getEntities()) {
660
+ const components = collectMultiHookComponents(this.createHooksContext(), entityId, componentTypes);
661
+ if (isCallback) {
662
+ (callback as LifecycleCallback<any>)("init", entityId, ...components);
663
+ } else {
664
+ (hook as LifecycleHook<any>).on_init!(entityId, ...components);
665
+ }
666
+ }
667
+ }
668
+ }
669
+
670
+ return () => {
671
+ this.hooks.delete(entry);
672
+ if (entry.matchedArchetypes) {
673
+ for (const archetype of entry.matchedArchetypes) {
674
+ archetype.matchingMultiHooks.delete(entry);
675
+ }
676
+ }
677
+ };
678
+ }
679
+
680
+ /**
681
+ * Synchronizes all buffered commands (set/remove/delete) to the world.
682
+ * This method must be called after making changes via `set()`, `remove()`, or `delete()` for them to take effect.
683
+ * Typically called once per frame at the end of your game loop.
684
+ *
685
+ * @example
686
+ * world.set(entity, Position, { x: 10, y: 20 });
687
+ * world.remove(entity, OldComponent);
688
+ * world.sync(); // Apply all buffered changes
689
+ */
690
+ sync(): void {
691
+ this.commandBuffer.execute();
692
+ }
693
+
694
+ /**
695
+ * Creates a cached query for efficiently iterating entities with specific components.
696
+ * The query is cached internally and reused across calls with the same component types and filter.
697
+ *
698
+ * **Important:** Store the query reference and reuse it across frames for optimal performance.
699
+ * Creating a new query each frame defeats the caching mechanism.
700
+ *
701
+ * @param componentTypes - Array of component types to match
702
+ * @param filter - Optional filter for additional constraints (e.g., without specific components)
703
+ * @returns A Query instance that can be used to iterate matching entities
704
+ *
705
+ * @example
706
+ * // Create once, reuse many times
707
+ * const movementQuery = world.createQuery([Position, Velocity]);
708
+ *
709
+ * // In game loop
710
+ * movementQuery.forEach((entity) => {
711
+ * const pos = world.get(entity, Position);
712
+ * const vel = world.get(entity, Velocity);
713
+ * pos.x += vel.x;
714
+ * pos.y += vel.y;
715
+ * });
716
+ *
717
+ * // With filter
718
+ * const activeQuery = world.createQuery([Position], {
719
+ * without: [Disabled]
720
+ * });
721
+ */
722
+ createQuery(componentTypes: EntityId<any>[], filter: QueryFilter = {}): Query {
723
+ const sortedTypes = normalizeComponentTypes(componentTypes);
724
+ const filterKey = serializeQueryFilter(filter);
725
+ const key = `${this.createArchetypeSignature(sortedTypes)}${filterKey ? `|${filterKey}` : ""}`;
726
+ return this.queryRegistry.getOrCreate(this, sortedTypes, key, filter);
727
+ }
728
+
729
+ /**
730
+ * Creates a new entity builder for fluent entity configuration.
731
+ * Useful for building entities with multiple components in a single expression.
732
+ *
733
+ * @returns An EntityBuilder instance
734
+ *
735
+ * @example
736
+ * const entity = world.spawn()
737
+ * .with(Position, { x: 0, y: 0 })
738
+ * .with(Velocity, { x: 1, y: 1 })
739
+ * .build();
740
+ * world.sync(); // Apply changes
741
+ */
742
+ spawn(): EntityBuilder {
743
+ return new EntityBuilder(this);
744
+ }
745
+
746
+ /**
747
+ * Spawns multiple entities with a configuration callback.
748
+ * More efficient than calling `spawn()` multiple times when creating many entities.
749
+ *
750
+ * @param count - Number of entities to spawn
751
+ * @param configure - Callback that receives an EntityBuilder and index; must return the configured builder
752
+ * @returns Array of created entity IDs
753
+ *
754
+ * @example
755
+ * const entities = world.spawnMany(100, (builder, index) => {
756
+ * return builder
757
+ * .with(Position, { x: index * 10, y: 0 })
758
+ * .with(Velocity, { x: 0, y: 1 });
759
+ * });
760
+ * world.sync();
761
+ */
762
+ spawnMany(count: number, configure: (builder: EntityBuilder, index: number) => EntityBuilder): EntityId[] {
763
+ const entities: EntityId[] = [];
764
+ for (let i = 0; i < count; i++) {
765
+ const builder = new EntityBuilder(this);
766
+ entities.push(configure(builder, i).build());
767
+ }
768
+ return entities;
769
+ }
770
+
771
+ /**
772
+ * Releases a cached query and frees its resources if no longer needed.
773
+ * Call this when you're done using a query to allow the world to clean up its cache entry.
774
+ *
775
+ * @param query - The query to release
776
+ *
777
+ * @example
778
+ * const query = world.createQuery([Position]);
779
+ * // ... use query ...
780
+ * world.releaseQuery(query); // Optional cleanup
781
+ */
782
+ releaseQuery(query: Query): void {
783
+ this.queryRegistry.release(query);
784
+ }
785
+
786
+ /**
787
+ * Returns all archetypes that contain entities with the specified components.
788
+ * Used internally for query optimization but can be useful for debugging.
789
+ *
790
+ * @param componentTypes - Array of component types to match
791
+ * @returns Array of Archetype objects containing matching components
792
+ * @internal
793
+ */
794
+ getMatchingArchetypes(componentTypes: EntityId<any>[]): Archetype[] {
795
+ if (componentTypes.length === 0) {
796
+ return [...this.archetypes];
797
+ }
798
+
799
+ const regularComponents: EntityId<any>[] = [];
800
+ const wildcardRelations: { componentId: ComponentId<any>; relationId: EntityId<any> }[] = [];
801
+
802
+ for (const componentType of componentTypes) {
803
+ if (isWildcardRelationId(componentType)) {
804
+ const componentId = getComponentIdFromRelationId(componentType);
805
+ if (componentId !== undefined) {
806
+ wildcardRelations.push({ componentId, relationId: componentType });
807
+ }
808
+ } else {
809
+ regularComponents.push(componentType);
810
+ }
811
+ }
812
+
813
+ let matchingArchetypes = this.getArchetypesWithComponents(regularComponents);
814
+
815
+ for (const { componentId, relationId } of wildcardRelations) {
816
+ const markerSet = this.archetypesByComponent.get(relationId);
817
+ const archetypesWithMarker = markerSet ? Array.from(markerSet) : [];
818
+ matchingArchetypes =
819
+ matchingArchetypes.length === 0
820
+ ? archetypesWithMarker
821
+ : matchingArchetypes.filter((a) => markerSet?.has(a) || a.hasRelationWithComponentId(componentId));
822
+ }
823
+
824
+ return matchingArchetypes;
825
+ }
826
+
827
+ private getArchetypesWithComponents(componentTypes: EntityId<any>[]): Archetype[] {
828
+ if (componentTypes.length === 0) return [...this.archetypes];
829
+ if (componentTypes.length === 1) {
830
+ const set = this.archetypesByComponent.get(componentTypes[0]!);
831
+ return set ? Array.from(set) : [];
832
+ }
833
+
834
+ // Sort by Set size, intersect starting from the smallest
835
+ const sets = componentTypes
836
+ .map((type) => this.archetypesByComponent.get(type))
837
+ .filter((s): s is Set<Archetype> => s !== undefined && s.size > 0)
838
+ .sort((a, b) => a.size - b.size);
839
+
840
+ if (sets.length === 0) return [];
841
+ if (sets.length < componentTypes.length) return []; // One component has no matching archetypes
842
+
843
+ const smallest = sets[0]!;
844
+
845
+ // 2-component fast path
846
+ if (sets.length === 2) {
847
+ const other = sets[1]!;
848
+ return Array.from(smallest).filter((a) => other.has(a));
849
+ }
850
+
851
+ // Multi-component intersection
852
+ let result = new Set(smallest);
853
+ for (let i = 1; i < sets.length; i++) {
854
+ for (const item of result) {
855
+ if (!sets[i]!.has(item)) result.delete(item);
856
+ }
857
+ if (result.size === 0) return [];
858
+ }
859
+ return Array.from(result);
860
+ }
861
+
862
+ /**
863
+ * Queries entities with specific components.
864
+ * For simpler use cases, prefer using `createQuery()` with `forEach()` which is cached and more efficient.
865
+ *
866
+ * @overload query(componentTypes: EntityId<any>[]): EntityId[]
867
+ * Returns an array of entity IDs that have all specified components.
868
+ *
869
+ * @overload query<const T extends readonly EntityId<any>[]>(
870
+ * componentTypes: T,
871
+ * includeComponents: true,
872
+ * ): Array<{ entity: EntityId; components: ComponentTuple<T> }>
873
+ * Returns entities along with their component data.
874
+ *
875
+ * @param componentTypes - Array of component types to query
876
+ * @param includeComponents - If true, includes component data in results
877
+ * @returns Array of entity IDs or objects with entities and components
878
+ *
879
+ * @example
880
+ * // Just entity IDs
881
+ * const entities = world.query([Position, Velocity]);
882
+ *
883
+ * // With components
884
+ * const results = world.query([Position, Velocity], true);
885
+ * results.forEach(({ entity, components: [pos, vel] }) => {
886
+ * pos.x += vel.x;
887
+ * });
888
+ */
889
+ query(componentTypes: EntityId<any>[]): EntityId[];
890
+ query<const T extends readonly EntityId<any>[]>(
891
+ componentTypes: T,
892
+ includeComponents: true,
893
+ ): Array<{ entity: EntityId; components: ComponentTuple<T> }>;
894
+ query(
895
+ componentTypes: EntityId<any>[],
896
+ includeComponents?: boolean,
897
+ ): EntityId[] | Array<{ entity: EntityId; components: any }> {
898
+ const matchingArchetypes = this.getMatchingArchetypes(componentTypes);
899
+
900
+ if (includeComponents) {
901
+ const result: Array<{ entity: EntityId; components: any }> = [];
902
+ for (const archetype of matchingArchetypes) {
903
+ archetype.appendEntitiesWithComponents(componentTypes as EntityId<any>[], result);
904
+ }
905
+ return result;
906
+ } else {
907
+ const result: EntityId[] = [];
908
+ for (const archetype of matchingArchetypes) {
909
+ for (const entity of archetype.getEntities()) {
910
+ result.push(entity);
911
+ }
912
+ }
913
+ return result;
914
+ }
915
+ }
916
+
917
+ private executeEntityCommands(entityId: EntityId, commands: Command[]): void {
918
+ this._changeset.clear();
919
+
920
+ // 1. Route: component entities use flat-map storage
921
+ if (this.componentEntities.exists(entityId)) {
922
+ this.componentEntities.executeCommands(entityId, commands);
923
+ return;
924
+ }
925
+
926
+ // 2. Route: destroy uses fast path
927
+ if (commands.some((cmd) => cmd.type === "destroy")) {
928
+ this.destroyEntityImmediate(entityId);
929
+ return;
930
+ }
931
+
932
+ // 3. Apply structural changes
933
+ this.applyEntityCommands(entityId, commands);
934
+ }
935
+
936
+ private applyEntityCommands(entityId: EntityId, commands: Command[]): void {
937
+ const currentArchetype = this.entityToArchetype.get(entityId);
938
+ if (!currentArchetype) return;
939
+
940
+ const changeset = this._changeset;
941
+ processCommands(entityId, currentArchetype, commands, changeset, (eid, arch, compId) => {
942
+ if (isExclusiveComponent(compId)) {
943
+ removeMatchingRelations(eid, arch, compId, changeset);
944
+ }
945
+ });
946
+
947
+ const hasStructuralChange = changeset.removes.size > 0 || changeset.adds.size > 0;
948
+
949
+ if (this.hooks.size === 0) {
950
+ // Fast path: no hooks, skip removedComponents map allocation and hook triggering
951
+ applyChangeset(this._commandCtx, entityId, currentArchetype, changeset, this.entityToArchetype, null);
952
+ if (hasStructuralChange) {
953
+ this.updateEntityReferences(entityId, changeset);
954
+ }
955
+ return;
956
+ }
957
+
958
+ const removedComponents = new Map<EntityId<any>, any>();
959
+ const newArchetype = applyChangeset(
960
+ this._commandCtx,
961
+ entityId,
962
+ currentArchetype,
963
+ changeset,
964
+ this.entityToArchetype,
965
+ removedComponents,
966
+ );
967
+
968
+ if (hasStructuralChange) {
969
+ this.updateEntityReferences(entityId, changeset);
970
+ }
971
+ triggerLifecycleHooks(
972
+ this.createHooksContext(),
973
+ entityId,
974
+ changeset.adds,
975
+ removedComponents,
976
+ currentArchetype,
977
+ newArchetype,
978
+ );
979
+ }
980
+
981
+ private createHooksContext(): HooksContext {
982
+ return this._hooksCtx;
983
+ }
984
+
985
+ private removeComponentImmediate(entityId: EntityId, componentType: EntityId<any>, targetEntityId: EntityId): void {
986
+ const sourceArchetype = this.entityToArchetype.get(entityId);
987
+ if (!sourceArchetype) return;
988
+
989
+ const changeset = this._removeChangeset;
990
+ changeset.clear();
991
+ changeset.delete(componentType);
992
+ maybeRemoveWildcardMarker(
993
+ entityId,
994
+ sourceArchetype,
995
+ componentType,
996
+ getComponentIdFromRelationId(componentType),
997
+ changeset,
998
+ );
999
+
1000
+ const removedComponent = sourceArchetype.get(entityId, componentType);
1001
+ const newArchetype = applyChangeset(
1002
+ this._commandCtx,
1003
+ entityId,
1004
+ sourceArchetype,
1005
+ changeset,
1006
+ this.entityToArchetype,
1007
+ null,
1008
+ );
1009
+ untrackEntityReference(this.entityReferences, entityId, componentType, targetEntityId);
1010
+ triggerLifecycleHooks(
1011
+ this.createHooksContext(),
1012
+ entityId,
1013
+ new Map(),
1014
+ new Map([[componentType, removedComponent]]),
1015
+ sourceArchetype,
1016
+ newArchetype,
1017
+ );
1018
+ }
1019
+
1020
+ private updateEntityReferences(entityId: EntityId, changeset: ComponentChangeset): void {
1021
+ for (const componentType of changeset.removes) {
1022
+ if (isEntityRelation(componentType)) {
1023
+ const targetId = getTargetIdFromRelationId(componentType)!;
1024
+ untrackEntityReference(this.entityReferences, entityId, componentType, targetId);
1025
+ } else if (componentType >= ENTITY_ID_START) {
1026
+ untrackEntityReference(this.entityReferences, entityId, componentType, componentType);
1027
+ }
1028
+ }
1029
+
1030
+ for (const [componentType] of changeset.adds) {
1031
+ if (isEntityRelation(componentType)) {
1032
+ const targetId = getTargetIdFromRelationId(componentType)!;
1033
+ trackEntityReference(this.entityReferences, entityId, componentType, targetId);
1034
+ } else if (componentType >= ENTITY_ID_START) {
1035
+ trackEntityReference(this.entityReferences, entityId, componentType, componentType);
1036
+ }
1037
+ }
1038
+ }
1039
+
1040
+ private ensureArchetype(componentTypes: Iterable<EntityId<any>>): Archetype {
1041
+ const regularTypes = filterRegularComponentTypes(componentTypes);
1042
+ const sortedTypes = normalizeComponentTypes(regularTypes);
1043
+ const hashKey = this.createArchetypeSignature(sortedTypes);
1044
+
1045
+ return getOrCompute(this.archetypeBySignature, hashKey, () => this.createNewArchetype(sortedTypes));
1046
+ }
1047
+
1048
+ /** Add componentType to the reverse index if it contains an entity ID */
1049
+ private addToReferencingIndex(componentType: EntityId<any>, archetype: Archetype): void {
1050
+ const detailedType = getDetailedIdType(componentType);
1051
+ let entityId: EntityId | undefined;
1052
+
1053
+ if (detailedType.type === "entity") {
1054
+ entityId = componentType as EntityId;
1055
+ } else if (detailedType.type === "entity-relation") {
1056
+ entityId = detailedType.targetId;
1057
+ }
1058
+
1059
+ if (entityId !== undefined) {
1060
+ let refs = this.entityToReferencingArchetypes.get(entityId);
1061
+ if (!refs) {
1062
+ refs = new Set();
1063
+ this.entityToReferencingArchetypes.set(entityId, refs);
1064
+ }
1065
+ refs.add(archetype);
1066
+ }
1067
+ }
1068
+
1069
+ /** Remove componentType from the reverse index */
1070
+ private removeFromReferencingIndex(componentType: EntityId<any>, archetype: Archetype): void {
1071
+ const detailedType = getDetailedIdType(componentType);
1072
+ let entityId: EntityId | undefined;
1073
+
1074
+ if (detailedType.type === "entity") {
1075
+ entityId = componentType as EntityId;
1076
+ } else if (detailedType.type === "entity-relation") {
1077
+ entityId = detailedType.targetId;
1078
+ }
1079
+
1080
+ if (entityId !== undefined) {
1081
+ const refs = this.entityToReferencingArchetypes.get(entityId);
1082
+ if (refs) {
1083
+ refs.delete(archetype);
1084
+ if (refs.size === 0) {
1085
+ this.entityToReferencingArchetypes.delete(entityId);
1086
+ }
1087
+ }
1088
+ }
1089
+ }
1090
+
1091
+ private createNewArchetype(componentTypes: EntityId<any>[]): Archetype {
1092
+ const newArchetype = new Archetype(componentTypes, this.dontFragmentStore);
1093
+ this.archetypes.push(newArchetype);
1094
+
1095
+ for (const componentType of componentTypes) {
1096
+ let archetypes = this.archetypesByComponent.get(componentType);
1097
+ if (!archetypes) {
1098
+ archetypes = new Set();
1099
+ this.archetypesByComponent.set(componentType, archetypes);
1100
+ }
1101
+ archetypes.add(newArchetype);
1102
+
1103
+ // Update reverse index
1104
+ this.addToReferencingIndex(componentType, newArchetype);
1105
+ }
1106
+
1107
+ this.queryRegistry.onNewArchetype(newArchetype);
1108
+ this.updateArchetypeHookMatches(newArchetype);
1109
+
1110
+ return newArchetype;
1111
+ }
1112
+
1113
+ private updateArchetypeHookMatches(archetype: Archetype): void {
1114
+ for (const entry of this.hooks) {
1115
+ if (this.archetypeMatchesHook(archetype, entry)) {
1116
+ archetype.matchingMultiHooks.add(entry);
1117
+ if (entry.matchedArchetypes) {
1118
+ entry.matchedArchetypes.add(archetype);
1119
+ }
1120
+ }
1121
+ }
1122
+ }
1123
+
1124
+ private archetypeMatchesHook(archetype: Archetype, entry: LifecycleHookEntry): boolean {
1125
+ return (
1126
+ entry.requiredComponents.every((c: EntityId<any>) => {
1127
+ if (isWildcardRelationId(c)) {
1128
+ if (isDontFragmentWildcard(c)) return true;
1129
+ const componentId = getComponentIdFromRelationId(c);
1130
+ return componentId !== undefined && archetype.hasRelationWithComponentId(componentId);
1131
+ }
1132
+ return archetype.componentTypeSet.has(c) || isDontFragmentRelation(c);
1133
+ }) && matchesFilter(archetype, entry.filter)
1134
+ );
1135
+ }
1136
+
1137
+ private cleanupArchetypesReferencingEntity(entityId: EntityId): void {
1138
+ const refs = this.entityToReferencingArchetypes.get(entityId);
1139
+ if (!refs) return;
1140
+
1141
+ for (const archetype of refs) {
1142
+ if (archetype.getEntities().length === 0) {
1143
+ this.removeArchetype(archetype);
1144
+ }
1145
+ }
1146
+ // removeArchetype already cleans up the reverse index entries
1147
+ this.entityToReferencingArchetypes.delete(entityId);
1148
+ }
1149
+
1150
+ private removeArchetype(archetype: Archetype): void {
1151
+ const index = this.archetypes.indexOf(archetype);
1152
+ if (index !== -1) {
1153
+ // swap-and-pop: O(1) removal
1154
+ const last = this.archetypes[this.archetypes.length - 1]!;
1155
+ this.archetypes[index] = last;
1156
+ this.archetypes.pop();
1157
+ }
1158
+
1159
+ this.archetypeBySignature.delete(this.createArchetypeSignature(archetype.componentTypes));
1160
+
1161
+ for (const componentType of archetype.componentTypes) {
1162
+ const archetypes = this.archetypesByComponent.get(componentType);
1163
+ if (archetypes) {
1164
+ archetypes.delete(archetype);
1165
+ if (archetypes.size === 0) {
1166
+ this.archetypesByComponent.delete(componentType);
1167
+ }
1168
+ }
1169
+
1170
+ // Clean up reverse index
1171
+ this.removeFromReferencingIndex(componentType, archetype);
1172
+ }
1173
+
1174
+ this.queryRegistry.onArchetypeRemoved(archetype);
1175
+ }
1176
+
1177
+ /**
1178
+ * Serializes the entire world state to a plain JavaScript object.
1179
+ * This creates a "memory snapshot" that can be stored or transmitted.
1180
+ * The snapshot can be restored using `new World(snapshot)`.
1181
+ *
1182
+ * **Note:** This is NOT automatically persistent storage. To persist data,
1183
+ * you must serialize the returned object to JSON or another format yourself.
1184
+ *
1185
+ * @returns A serializable object representing the world state
1186
+ *
1187
+ * @example
1188
+ * // Create snapshot
1189
+ * const snapshot = world.serialize();
1190
+ *
1191
+ * // Save to storage (example)
1192
+ * localStorage.setItem('save', JSON.stringify(snapshot));
1193
+ *
1194
+ * // Later, restore from snapshot
1195
+ * const savedData = JSON.parse(localStorage.getItem('save'));
1196
+ * const newWorld = new World(savedData);
1197
+ */
1198
+ serialize(): SerializedWorld {
1199
+ return serializeWorld(this.archetypes, this.componentEntities, this.entityIdManager);
1200
+ }
1201
+ }