@codehz/ecs 0.6.11 → 0.7.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/builder.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- //#region src/core/entity-types.d.ts
1
+ //#region src/entity/types.d.ts
2
2
  /**
3
3
  * Unique symbol brand for associating component type information with EntityId
4
4
  */
@@ -14,14 +14,53 @@ declare const __entityIdTypeTag: unique symbol;
14
14
  * - Entity IDs: 1024+
15
15
  * - Relation IDs: negative numbers encoding component and entity associations
16
16
  */
17
+ /**
18
+ * Branded numeric type representing an ECS identifier.
19
+ *
20
+ * - {@link ComponentId}: positive values in range `1–1023`
21
+ * - Entity IDs: values `1024+`
22
+ * - {@link RelationId}: negative values encoding `(componentId, targetId)`
23
+ *
24
+ * @template T - The data type associated with this ID
25
+ * @template U - Discriminant for the ID kind (e.g. `"component"`, `"entity-relation"`)
26
+ */
17
27
  type EntityId<T = unknown, U$1 = unknown> = number & {
18
28
  readonly [__componentTypeMarker]: T;
19
29
  readonly [__entityIdTypeTag]: U$1;
20
30
  };
31
+ /**
32
+ * Component identifier. Valid values are `1` through `1023`.
33
+ * Created with {@link component}.
34
+ *
35
+ * @template T - The data type stored by this component (`void` for tag components)
36
+ */
21
37
  type ComponentId<T = void> = EntityId<T, "component">;
38
+ /**
39
+ * Relation identifier targeting an entity.
40
+ * Created with {@link relation}.
41
+ *
42
+ * @template T - The data type stored by this relation
43
+ */
22
44
  type EntityRelationId<T = void> = EntityId<T, "entity-relation">;
45
+ /**
46
+ * Relation identifier targeting another component (singleton relation).
47
+ * Created with {@link relation}.
48
+ *
49
+ * @template T - The data type stored by this relation
50
+ */
23
51
  type ComponentRelationId<T = void> = EntityId<T, "component-relation">;
52
+ /**
53
+ * Wildcard relation identifier used to query all targets of a given relation component.
54
+ * Created with `relation(componentId, "*")`.
55
+ *
56
+ * @template T - The data type stored by the relation
57
+ */
24
58
  type WildcardRelationId<T = void> = EntityId<T, "wildcard-relation">;
59
+ /**
60
+ * Union of all relation identifier kinds.
61
+ *
62
+ * @template T - The data type stored by the relation
63
+ */
25
64
  type RelationId<T = void> = EntityRelationId<T> | ComponentRelationId<T> | WildcardRelationId<T>;
26
65
  /**
27
66
  * Check if an ID is a component ID
@@ -36,20 +75,42 @@ declare function isEntityId<T>(id: EntityId<T>): id is EntityId<T>;
36
75
  */
37
76
  declare function isRelationId<T>(id: EntityId<T>): id is RelationId<T>;
38
77
  //#endregion
39
- //#region src/core/entity-relation.d.ts
78
+ //#region src/entity/relation.d.ts
40
79
  /**
41
80
  * Type for relation ID based on component and target types
42
81
  */
43
82
  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;
44
83
  /**
45
- * Create a relation ID by associating a component with another ID (entity or component)
46
- * @param componentId The component ID (0-1023)
47
- * @param targetId The target ID (entity, component, or '*' for wildcard)
84
+ * Create a relation ID by associating a component with a target entity, component, or wildcard.
85
+ *
86
+ * Relations are encoded as negative numbers and can be used anywhere a regular component ID is accepted.
87
+ * Use `"*"` as the target to create a wildcard relation for querying all targets of a given relation type.
88
+ *
89
+ * @param componentId - The base component ID (must be a valid component)
90
+ * @param targetId - The target entity ID, component ID, or `"*"` for wildcard
91
+ * @returns A relation ID that encodes both the component and target
92
+ *
93
+ * @throws {Error} If `componentId` is not a valid component ID
94
+ * @throws {Error} If `targetId` is not a valid entity, component, or `"*"`
95
+ *
96
+ * @example
97
+ * const ChildOf = component();
98
+ * const parent = world.new();
99
+ *
100
+ * // Entity relation
101
+ * const childRelation = relation(ChildOf, parent);
102
+ * world.set(child, childRelation);
103
+ *
104
+ * // Wildcard relation (queries all targets)
105
+ * const allChildren = world.createQuery([relation(ChildOf, "*")]);
48
106
  */
49
107
  declare function relation<T>(componentId: ComponentId<T>, targetId: "*"): WildcardRelationId<T>;
50
108
  declare function relation<T, R extends EntityId<any>>(componentId: ComponentId<T>, targetId: R): RelationIdType<T, R>;
51
109
  /**
52
- * Check if an ID is a wildcard relation id
110
+ * Check if an ID is a wildcard relation (created with `relation(componentId, "*")`).
111
+ *
112
+ * @param id - The ID to check
113
+ * @returns `true` if the ID is a wildcard relation, `false` otherwise
53
114
  */
54
115
  declare function isWildcardRelationId<T>(id: EntityId<T>): id is WildcardRelationId<T>;
55
116
  /**
@@ -63,39 +124,306 @@ declare function decodeRelationId(relationId: RelationId<any>): {
63
124
  type: "entity" | "component" | "wildcard";
64
125
  };
65
126
  //#endregion
66
- //#region src/core/component-registry.d.ts
127
+ //#region src/component/registry.d.ts
128
+ /**
129
+ * Merge function type for combining repeated `set()` values within a single sync batch.
130
+ *
131
+ * When `world.set(entity, componentType, value)` is called **multiple times** for the
132
+ * same entity and same component type **before** the next `world.sync()`, the merge
133
+ * callback is invoked to combine the values instead of simply overwriting. This allows
134
+ * additive or custom composition of component data in a single frame.
135
+ *
136
+ * @typeParam T - The component's value type.
137
+ *
138
+ * @param prev - The value from the **previous** `set()` call (or the merged result of
139
+ * earlier calls) for this entity/componentType pair within the current sync batch.
140
+ * @param next - The value from the **current** `set()` call being processed.
141
+ *
142
+ * @returns The merged value to be stored. This becomes `prev` if another `set()` for
143
+ * the same entity and componentType is encountered later in the same batch.
144
+ *
145
+ * @remarks
146
+ * **Idempotency**: Merge functions **must be idempotent**. The ECS does not guarantee
147
+ * that `world.sync()` won't be called multiple times in edge cases (e.g., intermediate
148
+ * syncs during pipeline execution), so the merge result should not depend on call
149
+ * count or non-deterministic state.
150
+ *
151
+ * **Single-batch scope**: Merging only applies to `set()` calls within the **same sync
152
+ * batch** (i.e., between two `world.sync()` calls). After `world.sync()`, the component
153
+ * value is committed to storage, and the next `set()` starts with a fresh `prev` value.
154
+ *
155
+ * @example
156
+ * ```ts
157
+ * // Accumulate damage events in a single frame
158
+ * const DamageEvents = component<DamageEvent[]>({
159
+ * merge: (prev, next) => [...prev, ...next],
160
+ * });
161
+ *
162
+ * world.set(player, DamageEvents, [{ source: "fire", amount: 10 }]);
163
+ * world.set(player, DamageEvents, [{ source: "ice", amount: 5 }]);
164
+ * // After sync: player has [{ source: "fire", amount: 10 }, { source: "ice", amount: 5 }]
165
+ * ```
166
+ */
67
167
  type ComponentMerge<T = any> = (prev: T, next: T) => T;
68
168
  /**
69
169
  * Component options that define intrinsic properties
70
170
  */
71
171
  interface ComponentOptions<T = any> {
72
172
  /**
73
- * Optional name for the component (for serialization/debugging)
173
+ * An optional human-readable name for the component, used for debugging and
174
+ * serialization.
175
+ *
176
+ * While `name` is **optional** at registration time, omitting it can cause
177
+ * problems when serializing and later deserializing the world:
178
+ *
179
+ * 1. **Cross-session portability**: Without a name, the component is
180
+ * serialized as a raw numeric ID. Component IDs are allocated sequentially
181
+ * at registration time, so if the order of `component()` calls changes
182
+ * between sessions (e.g. due to code refactoring, lazy-loading, or
183
+ * tree-shaking), those numeric IDs will no longer point to the same
184
+ * component type, leading to **silent data corruption** on restore.
185
+ *
186
+ * 2. **Runtime warnings**: `encodeEntityId` logs a `console.warn` for every
187
+ * unnamed component it encounters during `world.serialize()`, which can be
188
+ * noisy in production when serialization is used for save-games or
189
+ * snapshots.
190
+ *
191
+ * 3. **Debugging ergonomics**: Named components make serialized snapshots
192
+ * human-readable (e.g. `"Position"` instead of `42`), which is invaluable
193
+ * when inspecting save files or network dumps.
194
+ *
195
+ * **Recommendation**: Always provide a `name` for any component that may
196
+ * appear in a serialized world — even if it's just the same string as the
197
+ * variable name.
198
+ *
199
+ * @example
200
+ * ```ts
201
+ * // ✅ Good: explicit name ensures stable serialization
202
+ * const Position = component<{ x: number; y: number }>({ name: "Position" });
203
+ *
204
+ * // ⚠️ Risky: no name — serialization falls back to numeric ID
205
+ * const Velocity = component<{ dx: number; dy: number }>();
206
+ * ```
74
207
  */
75
208
  name?: string;
76
209
  /**
77
- * If true, an entity can have at most one relation per base component.
78
- * When adding a new relation with the same base component, any existing relations
79
- * with that base component are automatically removed.
80
- * Only applicable to relation components.
210
+ * If `true`, an entity can have **at most one** relation per base component type.
211
+ * When a new relation with the same base component is added, any existing relations
212
+ * with that base component are **automatically removed** before the new one is applied.
213
+ *
214
+ * **Only applicable to relation components** — components used via
215
+ * `relation(componentId, target)`. Regular (non-relation) components ignore this flag.
216
+ *
217
+ * ## Behavior
218
+ *
219
+ * Exclusive relations enforce a **one-to-one** constraint at the entity level:
220
+ * each entity can hold at most one relation of a given exclusive component type.
221
+ *
222
+ * - **Same base component, different targets**: `set(entity, relation(Comp, A))`
223
+ * followed by `set(entity, relation(Comp, B))` results in only `(Comp, B)` —
224
+ * the `(Comp, A)` relation is automatically removed.
225
+ * - **Same base component, same target**: Re-setting the same relation target
226
+ * simply updates the component value (no extra removal overhead).
227
+ * - **Different exclusive components**: Independent — `exclusive` on `CompA` does
228
+ * not affect relations using `CompB`.
229
+ *
230
+ * The removal happens **during `world.sync()`**, as part of the command buffer
231
+ * processing, so it respects the same deferred execution model as other structural
232
+ * changes.
233
+ *
234
+ * ## Use cases
235
+ *
236
+ * - **Ownership**: An entity can only be owned by one parent at a time
237
+ * (`ChildOf` with `exclusive: true`).
238
+ * - **Equipment slots**: An item can only be in one slot at a time
239
+ * (`EquippedBy` with `exclusive: true`).
240
+ * - **Targeting**: An AI agent can only track one target at a time
241
+ * (`Targeting` with `exclusive: true`).
242
+ * - **State machines**: An entity can only have one active state from a set
243
+ * (`ActiveState` with `exclusive: true`).
244
+ *
245
+ * ## Interaction with other options
246
+ *
247
+ * - **`cascadeDelete`**: Compatible. When an exclusive relation uses
248
+ * `cascadeDelete`, deleting the target entity will both (a) delete the
249
+ * referencing entity, and (b) the exclusivity constraint prevents the
250
+ * entity from having multiple cascade-delete relations of the same type.
251
+ * - **`dontFragment`**: Compatible. Exclusivity is enforced at the data level
252
+ * regardless of whether the archetype is fragmented.
253
+ *
254
+ * @example
255
+ * ```ts
256
+ * // Without exclusive: entity can have multiple ChildOf relations
257
+ * const ChildOf = component();
258
+ * world.set(child, relation(ChildOf, parentA));
259
+ * world.set(child, relation(ChildOf, parentB));
260
+ * world.sync();
261
+ * // child now has TWO ChildOf relations (parentA and parentB)
262
+ * ```
263
+ *
264
+ * @example
265
+ * ```ts
266
+ * // With exclusive: only the last relation survives
267
+ * const ChildOf = component({ exclusive: true });
268
+ * world.set(child, relation(ChildOf, parentA));
269
+ * world.set(child, relation(ChildOf, parentB));
270
+ * world.sync();
271
+ * // child has only (ChildOf, parentB); (ChildOf, parentA) was auto-removed
272
+ * ```
81
273
  */
82
274
  exclusive?: boolean;
83
275
  /**
84
276
  * If true, when a relation target entity is deleted, all entities that reference
85
277
  * it through this component will also be deleted (cascade delete).
278
+ *
86
279
  * Only applicable to entity-relation components.
280
+ *
281
+ * **Important distinction from default cleanup**:
282
+ * By default, the ECS library **always** cleans up relation components that point
283
+ * to a deleted entity — the relation component is removed from the referencing
284
+ * entity, but the referencing entity itself **survives**. When `cascadeDelete` is
285
+ * enabled, the **entire referencing entity** is deleted, not just the relation
286
+ * component. This deletion is transitive: if entity C references entity B (which
287
+ * is cascade-deleted), entity C will also be deleted, and so on.
288
+ *
289
+ * @example
290
+ * // Without cascadeDelete (default behavior):
291
+ * const ChildOf = component(); // no cascadeDelete
292
+ * world.set(child, relation(ChildOf, parent));
293
+ * world.sync();
294
+ * world.delete(parent);
295
+ * world.sync();
296
+ * // child still exists, but the ChildOf relation is cleaned up
297
+ *
298
+ * @example
299
+ * // With cascadeDelete:
300
+ * const ChildOf = component({ cascadeDelete: true });
301
+ * world.set(child, relation(ChildOf, parent));
302
+ * world.sync();
303
+ * world.delete(parent);
304
+ * world.sync();
305
+ * // child is also deleted (entity deleted, not just relation cleaned up)
87
306
  */
88
307
  cascadeDelete?: boolean;
89
308
  /**
90
309
  * If true, relations with this component will not cause archetype fragmentation.
91
- * Entities with different target entities for this relation component will be stored
92
- * in the same archetype, preventing fragmentation when there are many different targets.
93
- * Only applicable to relation components.
94
- * Inspired by Flecs' DontFragment trait.
310
+ *
311
+ * **Problem it solves**: By default, each unique relation pair `(component, target)`
312
+ * creates a **separate archetype**. If 100 entities each have a `ChildOf` relation
313
+ * to a different parent, you get 100 archetypes — this is **archetype fragmentation**.
314
+ * Queries that iterate over all entities with a `ChildOf` relation must check all
315
+ * 100 archetypes, which degrades iteration performance and increases memory overhead.
316
+ *
317
+ * **How it works**: When `dontFragment` is enabled, the relation's target does **not**
318
+ * contribute to the archetype signature. Entities with different targets for the same
319
+ * relation component share a **single archetype**, and the per-entity target data is
320
+ * stored in a separate `DontFragmentStore` (a `Map<EntityId, Map<EntityId, any>>`).
321
+ * A wildcard relation marker (`relation(Comp, "*")`) is placed in the archetype
322
+ * component list so queries can still discover matching archetypes.
323
+ *
324
+ * **Use cases**:
325
+ * - **Hierarchy/ownership**: `ChildOf` relations where thousands of entities each
326
+ * point to different parent entities.
327
+ * - **Dynamic targeting**: Relations where targets change frequently (e.g., AI
328
+ * targeting, inventory slots) — without `dontFragment`, each target change would
329
+ * cause an archetype migration, which is expensive.
330
+ * - **High-cardinality relations**: Any relation where the number of unique targets
331
+ * is large compared to the number of entities.
332
+ *
333
+ * **Performance implications**:
334
+ * - **Without `dontFragment`**: Archetype count grows linearly with unique targets.
335
+ * Each archetype migration (changing a relation target) requires moving the entity's
336
+ * data between component arrays.
337
+ * - **With `dontFragment`**: Archetype count stays constant regardless of target
338
+ * diversity. Changing a relation target is an O(1) update in the `DontFragmentStore`.
339
+ * The trade-off is an extra map lookup when accessing the relation data.
340
+ *
341
+ * **Constraints**:
342
+ * - Only applicable to **relation components** (components used with `relation()`).
343
+ * - Wildcard queries (e.g., `relation(Comp, "*")`) still work correctly — the
344
+ * archetype carries a wildcard marker so queries can discover it.
345
+ * - Works with `exclusive` and `cascadeDelete` simultaneously.
346
+ *
347
+ * @example
348
+ * ```ts
349
+ * // Without dontFragment: 100 entities with different parents = 100 archetypes
350
+ * const ChildOf = component(); // default: fragmentation happens
351
+ *
352
+ * // With dontFragment: 100 entities with different parents = 1 archetype
353
+ * const ChildOf = component({ dontFragment: true });
354
+ *
355
+ * for (let i = 0; i < 100; i++) {
356
+ * const parent = world.new();
357
+ * const child = world.new();
358
+ * world.set(child, Position);
359
+ * world.set(child, relation(ChildOf, parent));
360
+ * }
361
+ * world.sync();
362
+ * // dontFragment: 1 archetype for all 100 entities
363
+ * // without: 100 archetypes, one per unique parent
364
+ * ```
365
+ *
366
+ * Inspired by Flecs' `DontFragment` trait.
95
367
  */
96
368
  dontFragment?: boolean;
97
369
  /**
98
- * Custom merge behavior for repeated set() of the same componentType in a single sync batch.
370
+ * Custom merge behavior for repeated `set()` of the same component type on the
371
+ * same entity within a single sync batch.
372
+ *
373
+ * By default, calling `world.set(entity, comp, value)` multiple times for the same
374
+ * entity and component before `world.sync()` simply overwrites the previous value —
375
+ * the last `set()` wins. When `merge` is provided, the values are combined using
376
+ * your function instead.
377
+ *
378
+ * @remarks
379
+ * **Use cases**:
380
+ * - **Accumulation**: Collecting events, tags, or modifiers that multiple systems
381
+ * contribute to within the same frame.
382
+ * - **Composition**: Merging partial updates into a single component value (e.g.,
383
+ * applying multiple `Vec3` deltas to a position).
384
+ * - **Conflict resolution**: Choosing the max/min/latest value when multiple
385
+ * systems want to set the same component.
386
+ *
387
+ * **Scope**: This only affects `set()` calls on the **same entity** with the **same
388
+ * component type** within **one sync batch** (i.e., between `world.sync()` calls).
389
+ * It does NOT merge values across different entities or across sync boundaries.
390
+ *
391
+ * **Relation support**: If the component is used as a relation (via
392
+ * `relation(componentId, target)`), the merge function also applies per-target.
393
+ * `set(entity, relation(Comp, A), v1)` and `set(entity, relation(Comp, A), v2)`
394
+ * will be merged, but `set(entity, relation(Comp, B), v)` is independent.
395
+ *
396
+ * **Idempotency required**: Your merge function should be idempotent — calling it
397
+ * multiple times with the same inputs must produce the same result. The ECS
398
+ * runtime does not guarantee exactly-once `sync()` execution in all scenarios.
399
+ *
400
+ * **Return value**: The function **must return** the merged value. It should not
401
+ * mutate `prev` or `next` in place unless you intentionally want shared mutable
402
+ * state (which is discouraged).
403
+ *
404
+ * @example
405
+ * ```ts
406
+ * // Collect tags from multiple systems in one frame
407
+ * const Tags = component<string[]>({
408
+ * merge: (prev, next) => [...prev, ...next],
409
+ * });
410
+ * ```
411
+ *
412
+ * @example
413
+ * ```ts
414
+ * // Only keep the highest priority value
415
+ * const Alert = component<{ level: number; msg: string }>({
416
+ * merge: (prev, next) => prev.level >= next.level ? prev : next,
417
+ * });
418
+ * ```
419
+ *
420
+ * @example
421
+ * ```ts
422
+ * // Accumulate numeric deltas (e.g., for movement)
423
+ * const Velocity = component<{ x: number; y: number }>({
424
+ * merge: (prev, next) => ({ x: prev.x + next.x, y: prev.y + next.y }),
425
+ * });
426
+ * ```
99
427
  */
100
428
  merge?: ComponentMerge<T>;
101
429
  }
@@ -126,98 +454,86 @@ declare function getComponentIdByName(name: string): ComponentId<any> | undefine
126
454
  */
127
455
  declare function getComponentNameById(id: ComponentId<any>): string | undefined;
128
456
  //#endregion
129
- //#region src/commands/changeset.d.ts
457
+ //#region src/storage/serialization.d.ts
458
+ type SerializedEntityId = number | string | {
459
+ component: string;
460
+ target: number | string | "*";
461
+ };
130
462
  /**
131
- * @internal Represents a set of component changes to be applied to an entity
463
+ * Serialized state of EntityIdManager
132
464
  */
133
- declare class ComponentChangeset {
134
- readonly adds: Map<EntityId<any>, any>;
135
- readonly removes: Set<EntityId<any>>;
136
- /**
137
- * Add a component to the changeset
138
- */
139
- set<T>(componentType: EntityId<T>, component: T): void;
140
- /**
141
- * Remove a component from the changeset
142
- */
143
- delete<T>(componentType: EntityId<T>): void;
144
- /**
145
- * Check if the changeset has any changes
146
- */
147
- hasChanges(): boolean;
148
- /**
149
- * Clear all changes
150
- */
151
- clear(): void;
152
- /**
153
- * Merge another changeset into this one
154
- */
155
- merge(other: ComponentChangeset): void;
156
- /**
157
- * Apply the changeset to existing components and return the final state
158
- */
159
- applyTo(existingComponents: Map<EntityId<any>, any>): Map<EntityId<any>, any>;
160
- /**
161
- * Get the final component types after applying the changeset
162
- * @param existingComponentTypes - The current component types on the entity
163
- * @returns The final component types or undefined if no changes
164
- */
165
- getFinalComponentTypes(existingComponentTypes: EntityId<any>[]): EntityId<any>[] | undefined;
465
+ interface SerializedEntityIdManager {
466
+ nextId: number;
467
+ freelist?: number[];
166
468
  }
469
+ type SerializedWorld = {
470
+ version: number;
471
+ entityManager: SerializedEntityIdManager;
472
+ entities: SerializedEntity[];
473
+ componentEntities?: SerializedEntity[];
474
+ };
475
+ type SerializedEntity = {
476
+ id: SerializedEntityId;
477
+ components: SerializedComponent[];
478
+ };
479
+ type SerializedComponent = {
480
+ type: SerializedEntityId;
481
+ value: any;
482
+ };
167
483
  //#endregion
168
- //#region src/commands/command-buffer.d.ts
484
+ //#region src/query/filter.d.ts
169
485
  /**
170
- * Command for deferred execution
171
- * Uses discriminated union for type safety
486
+ * Filter options for queries
172
487
  */
173
- type Command = {
174
- type: "set";
175
- entityId: EntityId;
176
- componentType: EntityId<any>;
177
- component: any;
178
- } | {
179
- type: "delete";
180
- entityId: EntityId;
181
- componentType: EntityId<any>;
182
- } | {
183
- type: "destroy";
184
- entityId: EntityId;
185
- };
488
+ interface QueryFilter {
489
+ negativeComponentTypes?: EntityId<any>[];
490
+ }
186
491
  //#endregion
187
- //#region src/core/types.d.ts
492
+ //#region src/types/index.d.ts
188
493
  /**
189
- * Hook types for component lifecycle events
494
+ * Lifecycle hook definition for reacting to component additions, updates, and removals.
495
+ * Register hooks with {@link World.hook}.
190
496
  */
191
- interface LegacyLifecycleHook<T = unknown> {
497
+ interface LifecycleHook<T extends readonly ComponentType<any>[]> {
192
498
  /**
193
- * Called when a component is added to an entity
499
+ * Called once for each entity that already matches the hook's component types
500
+ * when the hook is first registered, and then for every new matching entity.
194
501
  */
195
- on_init?: (entityId: EntityId, componentType: EntityId<T>, component: T) => void;
502
+ on_init?: (entityId: EntityId, ...components: ComponentTuple<T>) => void;
196
503
  /**
197
- * Called when a component is updated on an entity
504
+ * Called whenever a matching entity's component data is updated via `set()`.
198
505
  */
199
- on_set?: (entityId: EntityId, componentType: EntityId<T>, component: T) => void;
506
+ on_set?: (entityId: EntityId, ...components: ComponentTuple<T>) => void;
200
507
  /**
201
- * Called when a component is deleted from an entity
508
+ * Called whenever a matching entity loses one of the required components
509
+ * or is deleted.
202
510
  */
203
- on_remove?: (entityId: EntityId, componentType: EntityId<T>, component: T) => void;
204
- }
205
- interface LifecycleHook<T extends readonly ComponentType<any>[]> {
206
- on_init?: (entityId: EntityId, ...components: ComponentTuple<T>) => void;
207
- on_set?: (entityId: EntityId, ...components: ComponentTuple<T>) => void;
208
511
  on_remove?: (entityId: EntityId, ...components: ComponentTuple<T>) => void;
209
512
  }
210
513
  /**
211
- * Convenience function type for single component lifecycle events
212
- * Combines on_init, on_set, and on_remove into a single callback
514
+ * Shorthand callback style for multi-component lifecycle hooks.
515
+ * The same function receives all three events distinguished by the `type` parameter.
516
+ *
517
+ * @example
518
+ * world.hook([Position, Velocity], (type, entityId, position, velocity) => {
519
+ * if (type === "init") console.log("spawned");
520
+ * if (type === "set") console.log("updated");
521
+ * if (type === "remove") console.log("despawned");
522
+ * });
213
523
  */
214
- type LegacyLifecycleCallback<T = unknown> = (type: "init" | "set" | "remove", entityId: EntityId, componentType: EntityId<T>, component: T) => void;
524
+ type LifecycleCallback<T extends readonly ComponentType<any>[]> = (type: "init" | "set" | "remove", entityId: EntityId, ...components: ComponentTuple<T>) => void;
215
525
  /**
216
- * Convenience function type for multi-component lifecycle events
217
- * Combines on_init, on_set, and on_remove into a single callback
526
+ * A component type used in queries and hooks.
527
+ * Can be a plain {@link EntityId} or an {@link OptionalEntityId} wrapped with `.optional`.
218
528
  */
219
- type LifecycleCallback<T extends readonly ComponentType<any>[]> = (type: "init" | "set" | "remove", entityId: EntityId, ...components: ComponentTuple<T>) => void;
220
529
  type ComponentType<T> = EntityId<T> | OptionalEntityId<T>;
530
+ /**
531
+ * Wrapper that marks a component as optional in queries and hooks.
532
+ * When a component is optional, entities missing it are still included in results.
533
+ *
534
+ * @example
535
+ * world.createQuery([Position, { optional: Velocity }]);
536
+ */
221
537
  type OptionalEntityId<T> = {
222
538
  optional: EntityId<T>;
223
539
  };
@@ -227,7 +543,8 @@ type ComponentTypeToData<T> = T extends {
227
543
  value: ComponentTypeToData<U>;
228
544
  } | undefined : T extends WildcardRelationId<infer U> ? [EntityId<unknown>, U][] : T extends EntityId<infer U> ? U : never;
229
545
  /**
230
- * Type helper for component tuples extracted from EntityId array
546
+ * Maps an array of {@link ComponentType} to their corresponding data tuples.
547
+ * Used by {@link World.query} and {@link Query.forEach} to type component results.
231
548
  */
232
549
  type ComponentTuple<T extends readonly ComponentType<any>[]> = { readonly [K in keyof T]: ComponentTypeToData<T[K]> };
233
550
  interface LifecycleHookEntry {
@@ -236,9 +553,26 @@ interface LifecycleHookEntry {
236
553
  optionalComponents: EntityId<any>[];
237
554
  filter: QueryFilter;
238
555
  hook: LifecycleHook<any>;
556
+ /** Raw callback function; takes precedence over hook.on_* when present */
557
+ callback?: LifecycleCallback<any>;
558
+ /** Archetypes that match this hook, used for precise cleanup on unsubscription */
559
+ matchedArchetypes?: Set<any>;
560
+ }
561
+ //#endregion
562
+ //#region src/archetype/store.d.ts
563
+ /**
564
+ * Minimal interface for storing dontFragment relation data keyed by entity ID.
565
+ *
566
+ * Using an interface here decouples `Archetype` (and `world-commands.ts`) from
567
+ * the concrete `Map` used by `World`, making archetypes independently testable.
568
+ */
569
+ interface DontFragmentStore {
570
+ get(entityId: EntityId): Map<EntityId<any>, any> | undefined;
571
+ set(entityId: EntityId, data: Map<EntityId<any>, any>): void;
572
+ delete(entityId: EntityId): void;
239
573
  }
240
574
  //#endregion
241
- //#region src/core/archetype.d.ts
575
+ //#region src/archetype/archetype.d.ts
242
576
  /**
243
577
  * Archetype class for ECS architecture
244
578
  * Represents a group of entities that share the same set of components
@@ -267,9 +601,9 @@ declare class Archetype {
267
601
  */
268
602
  private entityToIndex;
269
603
  /**
270
- * Reference to dontFragment relations storage from World
604
+ * DontFragmentStore for relation data keyed by entity ID.
271
605
  * This allows entities with different relation targets to share the same archetype
272
- * Stored in World to avoid migration overhead when entities change archetypes
606
+ * without migration overhead when entities change archetypes.
273
607
  */
274
608
  private dontFragmentRelations;
275
609
  /**
@@ -280,7 +614,7 @@ declare class Archetype {
280
614
  * Cache for pre-computed component data sources to avoid repeated calculations
281
615
  */
282
616
  private componentDataSourcesCache;
283
- constructor(componentTypes: EntityId<any>[], dontFragmentRelations: Map<EntityId, Map<EntityId<any>, any>>);
617
+ constructor(componentTypes: EntityId<any>[], dontFragmentRelations: DontFragmentStore);
284
618
  get size(): number;
285
619
  /**
286
620
  * Check if the given component types match this archetype
@@ -318,23 +652,88 @@ declare class Archetype {
318
652
  entity: EntityId;
319
653
  components: ComponentTuple<T>;
320
654
  }>;
655
+ appendEntitiesWithComponents<const T extends readonly ComponentType<any>[]>(componentTypes: T, result: Array<{
656
+ entity: EntityId;
657
+ components: ComponentTuple<T>;
658
+ }>): void;
321
659
  iterateWithComponents<const T extends readonly ComponentType<any>[]>(componentTypes: T): IterableIterator<[EntityId, ...ComponentTuple<T>]>;
322
660
  forEachWithComponents<const T extends readonly ComponentType<any>[]>(componentTypes: T, callback: (entity: EntityId, ...components: ComponentTuple<T>) => void): void;
323
661
  forEach(callback: (entityId: EntityId, components: Map<EntityId<any>, any>) => void): void;
324
662
  hasRelationWithComponentId(componentId: EntityId<any>): boolean;
325
663
  }
326
664
  //#endregion
327
- //#region src/query/filter.d.ts
665
+ //#region src/query/registry.d.ts
328
666
  /**
329
- * Filter options for queries
667
+ * Manages the lifecycle and caching of `Query` instances.
668
+ *
669
+ * Responsibilities:
670
+ * - Create / reuse cached queries keyed by component-type + filter signature.
671
+ * - Track reference counts so queries are only disposed when truly unused.
672
+ * - Notify registered queries when new archetypes are created or destroyed.
673
+ *
674
+ * The `_cacheKey` string that was previously attached directly to `Query` is now
675
+ * kept in a private `WeakMap` so the `Query` class doesn't need to expose it.
330
676
  */
331
- interface QueryFilter {
332
- negativeComponentTypes?: EntityId<any>[];
677
+ declare class QueryRegistry {
678
+ /** All live queries that should receive archetype notifications. */
679
+ private readonly queries;
680
+ /** Cache of reusable queries keyed by a deterministic signature string. */
681
+ private readonly cache;
682
+ /** Maps each query to its cache key without polluting the Query public API. */
683
+ private readonly cacheKeys;
684
+ /**
685
+ * Returns (or creates) a cached query for the given component types and filter.
686
+ * Increments the reference count on cache hits.
687
+ *
688
+ * @param world The world that owns this registry.
689
+ * @param sortedTypes Normalized (sorted) component types.
690
+ * @param key Combined cache key (`types|filter`).
691
+ * @param filter The raw query filter (used when creating a new Query).
692
+ */
693
+ getOrCreate(world: World, sortedTypes: EntityId<any>[], key: string, filter: QueryFilter): Query;
694
+ /**
695
+ * Decrements the reference count for the given query.
696
+ * When the count reaches zero the query is fully disposed.
697
+ */
698
+ release(query: Query): void;
699
+ /**
700
+ * Registers a query so it receives future archetype notifications.
701
+ * Called automatically by the `Query` constructor via `world._registerQuery`.
702
+ */
703
+ register(query: Query): void;
704
+ /**
705
+ * Removes a query from the notification list.
706
+ * Called by `Query._disposeInternal` via `world._unregisterQuery`.
707
+ */
708
+ unregister(query: Query): void;
709
+ /**
710
+ * Notifies all live queries that a new archetype has been created.
711
+ * Queries will add the archetype to their cache if it matches.
712
+ */
713
+ onNewArchetype(archetype: Archetype): void;
714
+ /**
715
+ * Notifies all live queries that an archetype has been destroyed.
716
+ * Queries will remove the archetype from their internal cache.
717
+ */
718
+ onArchetypeRemoved(archetype: Archetype): void;
333
719
  }
334
720
  //#endregion
335
721
  //#region src/query/query.d.ts
336
722
  /**
337
- * Query class for efficient entity queries with cached archetypes
723
+ * Cached query for efficiently iterating entities with specific components.
724
+ *
725
+ * Queries are created via {@link World.createQuery} and should be **reused across frames**
726
+ * for optimal performance. The world automatically keeps the query's internal archetype cache
727
+ * up to date as entities are created and destroyed.
728
+ *
729
+ * @example
730
+ * const movementQuery = world.createQuery([Position, Velocity]);
731
+ *
732
+ * // In the game loop
733
+ * movementQuery.forEach([Position, Velocity], (entity, pos, vel) => {
734
+ * pos.x += vel.x;
735
+ * pos.y += vel.y;
736
+ * });
338
737
  */
339
738
  declare class Query {
340
739
  private world;
@@ -348,13 +747,24 @@ declare class Query {
348
747
  private wildcardTypes;
349
748
  /** Cached specific dontFragment relation types that need entity-level filtering */
350
749
  private specificDontFragmentTypes;
351
- constructor(world: World, componentTypes: EntityId<any>[], filter?: QueryFilter);
750
+ /**
751
+ * @internal Queries should be created via {@link World.createQuery}, not instantiated directly.
752
+ */
753
+ constructor(world: World, componentTypes: EntityId<any>[], filter?: QueryFilter, registry?: QueryRegistry);
352
754
  /**
353
755
  * Check if query is disposed and throw error if so
354
756
  */
355
757
  private ensureNotDisposed;
356
758
  /**
357
- * Get all entities matching the query
759
+ * Returns all entity IDs that match this query.
760
+ *
761
+ * @returns Array of matching entity IDs
762
+ *
763
+ * @example
764
+ * const entities = query.getEntities();
765
+ * for (const entity of entities) {
766
+ * const pos = world.get(entity, Position);
767
+ * }
358
768
  */
359
769
  getEntities(): EntityId[];
360
770
  /**
@@ -362,42 +772,67 @@ declare class Query {
362
772
  */
363
773
  private entityMatchesQuery;
364
774
  /**
365
- * Get entities with their component data
366
- * @param componentTypes Array of component types to retrieve
367
- * @returns Array of objects with entity and component data
775
+ * Returns all matching entities along with their component data.
776
+ *
777
+ * @param componentTypes - Array of component types to retrieve
778
+ * @returns Array of objects containing the entity ID and its component tuple
779
+ *
780
+ * @example
781
+ * const results = query.getEntitiesWithComponents([Position, Velocity]);
782
+ * results.forEach(({ entity, components: [pos, vel] }) => {
783
+ * pos.x += vel.x;
784
+ * });
368
785
  */
369
786
  getEntitiesWithComponents<const T extends readonly ComponentType<any>[]>(componentTypes: T): Array<{
370
787
  entity: EntityId;
371
788
  components: ComponentTuple<T>;
372
789
  }>;
373
790
  /**
374
- * Iterate over entities with their component data
375
- * @param componentTypes Array of component types to retrieve
376
- * @param callback Function called for each entity with its components
791
+ * Iterates over all matching entities and invokes the callback with their component data.
792
+ * This is the preferred way to read and mutate components in a hot loop.
793
+ *
794
+ * @param componentTypes - Array of component types to retrieve
795
+ * @param callback - Function called for each matching entity with its components
796
+ *
797
+ * @example
798
+ * query.forEach([Position, Velocity], (entity, pos, vel) => {
799
+ * pos.x += vel.x;
800
+ * pos.y += vel.y;
801
+ * });
377
802
  */
378
803
  forEach<const T extends readonly ComponentType<any>[]>(componentTypes: T, callback: (entity: EntityId, ...components: ComponentTuple<T>) => void): void;
379
804
  /**
380
- * Iterate over entities with their component data (generator)
381
- * @param componentTypes Array of component types to retrieve
805
+ * Generator that yields each matching entity together with its component data.
806
+ *
807
+ * @param componentTypes - Array of component types to retrieve
808
+ * @yields Tuples of `[entityId, ...components]`
809
+ *
810
+ * @example
811
+ * for (const [entity, pos, vel] of query.iterate([Position, Velocity])) {
812
+ * pos.x += vel.x;
813
+ * }
382
814
  */
383
815
  iterate<const T extends readonly ComponentType<any>[]>(componentTypes: T): IterableIterator<[EntityId, ...ComponentTuple<T>]>;
384
816
  /**
385
- * Get component data arrays for all matching entities
386
- * @param componentType The component type to retrieve
387
- * @returns Array of component data for all matching entities
817
+ * Returns an array containing the data of a single component for every matching entity.
818
+ *
819
+ * @param componentType - The component type to retrieve
820
+ * @returns Array of component data (one entry per matching entity)
821
+ *
822
+ * @example
823
+ * const positions = query.getComponentData(Position);
388
824
  */
389
825
  getComponentData<T>(componentType: EntityId<T>): T[];
390
826
  /**
391
- * Update the cached archetypes
392
- * Called when new archetypes are created
827
+ * @internal Rebuilds the cached archetype list. Called automatically by the world.
393
828
  */
394
829
  updateCache(): void;
395
830
  /**
396
- * Check if a new archetype matches this query and add to cache if it does
831
+ * @internal Called by the world when a new archetype is created.
397
832
  */
398
833
  checkNewArchetype(archetype: Archetype): void;
399
834
  /**
400
- * Remove an archetype from the cached archetypes
835
+ * @internal Called by the world when an archetype is destroyed.
401
836
  */
402
837
  removeArchetype(archetype: Archetype): void;
403
838
  /**
@@ -407,47 +842,24 @@ declare class Query {
407
842
  */
408
843
  dispose(): void;
409
844
  /**
410
- * Internal full dispose called by World when refCount reaches zero.
845
+ * @internal Fully disposes the query when the world's refCount reaches zero.
411
846
  */
412
- _disposeInternal(): void;
847
+ _disposeInternal(registry?: QueryRegistry): void;
413
848
  /**
414
- * Symbol.dispose implementation for automatic resource management
849
+ * Using-with-disposals support. Calls {@link dispose} automatically.
850
+ *
851
+ * @example
852
+ * using query = world.createQuery([Position]);
853
+ * // query is released automatically when the block exits
415
854
  */
416
855
  [Symbol.dispose](): void;
417
856
  /**
418
- * Check if the query has been disposed
857
+ * Whether the query has been disposed and can no longer be used.
419
858
  */
420
859
  get disposed(): boolean;
421
860
  }
422
861
  //#endregion
423
- //#region src/core/serialization.d.ts
424
- type SerializedEntityId = number | string | {
425
- component: string;
426
- target: number | string | "*";
427
- };
428
- /**
429
- * Serialized state of EntityIdManager
430
- */
431
- interface SerializedEntityIdManager {
432
- nextId: number;
433
- freelist?: number[];
434
- }
435
- type SerializedWorld = {
436
- version: number;
437
- entityManager: SerializedEntityIdManager;
438
- entities: SerializedEntity[];
439
- componentEntities?: SerializedEntity[];
440
- };
441
- type SerializedEntity = {
442
- id: SerializedEntityId;
443
- components: SerializedComponent[];
444
- };
445
- type SerializedComponent = {
446
- type: SerializedEntityId;
447
- value: any;
448
- };
449
- //#endregion
450
- //#region src/core/world.d.ts
862
+ //#region src/world/world.d.ts
451
863
  /**
452
864
  * World class for ECS architecture
453
865
  * Manages entities and components
@@ -459,21 +871,22 @@ declare class World {
459
871
  private entityToArchetype;
460
872
  private archetypesByComponent;
461
873
  private entityReferences;
462
- private dontFragmentRelations;
463
- private componentEntityComponents;
464
- private relationEntityIdsByTarget;
465
- private queries;
466
- private queryCache;
467
- private legacyHooks;
874
+ /** Reverse index: entity ID → set of archetypes whose componentTypes include that entity ID */
875
+ private entityToReferencingArchetypes;
876
+ /** DontFragment relation storage, shared with all Archetype instances */
877
+ private readonly dontFragmentStore;
878
+ /** Component entity (singleton) storage */
879
+ private readonly componentEntities;
880
+ private readonly queryRegistry;
468
881
  private hooks;
469
882
  private commandBuffer;
470
883
  private readonly _changeset;
884
+ private readonly _removeChangeset;
471
885
  /** Cached command processor context to avoid per-entity object allocation */
472
886
  private readonly _commandCtx;
473
887
  /** Cached hooks context to avoid per-entity object allocation */
474
888
  private readonly _hooksCtx;
475
889
  constructor(snapshot?: SerializedWorld);
476
- private deserializeSnapshot;
477
890
  private createArchetypeSignature;
478
891
  /**
479
892
  * Creates a new entity.
@@ -488,23 +901,35 @@ declare class World {
488
901
  * world.sync();
489
902
  */
490
903
  new<T = void>(): EntityId<T>;
491
- private isComponentEntityId;
492
- private registerRelationEntityId;
493
- private unregisterRelationEntityId;
494
- private getComponentEntityComponents;
495
- private clearComponentEntityComponents;
496
- private cleanupComponentEntitiesReferencingEntity;
904
+ /**
905
+ * Semantic alias for `new()` to avoid confusion with the `new` keyword.
906
+ * Creates a new entity with an empty component set.
907
+ *
908
+ * @example
909
+ * const entity = world.create<MyComponent>();
910
+ */
911
+ create<T = void>(): EntityId<T>;
912
+ /** Fast path: destroy an entity that is not referenced by any other entity, skipping BFS */
913
+ private destroySingleEntity;
497
914
  private destroyEntityImmediate;
498
915
  /**
499
- * Checks if an entity exists in the world.
916
+ * Checks if an **entity** (not a component) exists in the world.
917
+ *
918
+ * This is specifically for checking entity liveness — whether the given entity ID
919
+ * is currently alive in the world. For checking if a component is present on an
920
+ * entity, use {@link has} instead.
500
921
  *
501
922
  * @param entityId - The entity identifier to check
502
923
  * @returns `true` if the entity exists, `false` otherwise
503
924
  *
504
925
  * @example
926
+ * // Check if an entity is alive
505
927
  * if (world.exists(entityId)) {
506
928
  * console.log("Entity exists");
507
929
  * }
930
+ *
931
+ * // To check for a component, use has() instead:
932
+ * if (world.has(entity, Position)) { ... }
508
933
  */
509
934
  exists(entityId: EntityId): boolean;
510
935
  private assertEntityExists;
@@ -512,7 +937,6 @@ declare class World {
512
937
  private assertSetComponentTypeValid;
513
938
  private resolveSetOperation;
514
939
  private resolveRemoveOperation;
515
- private getComponentEntityWildcardRelations;
516
940
  /**
517
941
  * Adds or updates a component on an entity (or marks void component as present).
518
942
  * The change is buffered and takes effect after calling `world.sync()`.
@@ -577,27 +1001,39 @@ declare class World {
577
1001
  */
578
1002
  delete(entityId: EntityId): void;
579
1003
  /**
580
- * Checks if an entity has a specific component.
1004
+ * Checks if a specific **component** is present on an entity.
1005
+ *
1006
+ * This is for component membership checks — does the given entity have this
1007
+ * component type? For checking whether an entity itself is alive, use
1008
+ * {@link exists} instead.
1009
+ *
581
1010
  * Immediately reflects the current state without waiting for `sync()`.
582
1011
  *
583
1012
  * @overload has<T>(entityId: EntityId, componentType: EntityId<T>): boolean
584
1013
  * Checks if a specific component type is present on the entity.
585
1014
  *
586
1015
  * @overload has<T>(componentId: ComponentId<T>): boolean
587
- * Checks if a singleton component has data (shorthand for has(componentId, componentId)).
1016
+ * Shorthand for checking a **singleton component** a component that is its own
1017
+ * entity (component-as-entity pattern). Equivalent to `has(componentId, componentId)`.
588
1018
  *
589
1019
  * @template T - The component data type
590
- * @param entityId - The entity identifier
1020
+ * @param entityId - The entity identifier, or a singleton component ID
591
1021
  * @param componentType - The component type to check
592
1022
  * @returns `true` if the entity has the component, `false` otherwise
593
1023
  *
594
1024
  * @example
1025
+ * // Check if an entity has a component
595
1026
  * if (world.has(entity, Position)) {
596
1027
  * const pos = world.get(entity, Position);
597
1028
  * }
1029
+ *
1030
+ * // Check a singleton component (component-as-entity)
598
1031
  * if (world.has(GlobalConfig)) {
599
1032
  * const config = world.get(GlobalConfig);
600
1033
  * }
1034
+ *
1035
+ * // Use exists() for entity liveness checks
1036
+ * if (world.exists(entity)) { ... }
601
1037
  */
602
1038
  has<T>(componentId: ComponentId<T>): boolean;
603
1039
  has<T>(entityId: EntityId, componentType: EntityId<T>): boolean;
@@ -634,6 +1070,9 @@ declare class World {
634
1070
  * @overload getOptional<T>(entityId: EntityId<T>): { value: T } | undefined
635
1071
  * Retrieves the entity's primary component safely.
636
1072
  *
1073
+ * @overload getOptional<T>(entityId: EntityId, componentType: WildcardRelationId<T>): { value: [EntityId<unknown>, T][] } | undefined
1074
+ * Retrieves all matching relation values safely.
1075
+ *
637
1076
  * @overload getOptional<T>(entityId: EntityId, componentType: EntityId<T>): { value: T } | undefined
638
1077
  * Retrieves a specific component safely.
639
1078
  *
@@ -648,18 +1087,15 @@ declare class World {
648
1087
  getOptional<T>(entityId: EntityId<T>): {
649
1088
  value: T;
650
1089
  } | undefined;
1090
+ getOptional<T>(entityId: EntityId, componentType: WildcardRelationId<T>): {
1091
+ value: [EntityId<unknown>, T][];
1092
+ } | undefined;
651
1093
  getOptional<T>(entityId: EntityId, componentType: EntityId<T>): {
652
1094
  value: T;
653
1095
  } | undefined;
654
1096
  /**
655
1097
  * Registers a lifecycle hook that responds to component changes.
656
1098
  * The hook callback is invoked when components matching the specified types are added, updated, or removed.
657
- *
658
- * @deprecated For single components, use the array overload with LifecycleCallback for better multi-component support
659
- *
660
- * @overload hook<T>(componentType: EntityId<T>, hook: LegacyLifecycleHook<T> | LegacyLifecycleCallback<T>): () => void
661
- * Registers a hook for a single component type (legacy API).
662
- *
663
1099
  * @overload hook<const T extends readonly ComponentType<any>[]>(
664
1100
  * componentTypes: T,
665
1101
  * hook: LifecycleHook<T> | LifecycleCallback<T>,
@@ -668,15 +1104,14 @@ declare class World {
668
1104
  * Registers a hook for multiple component types.
669
1105
  * The hook is triggered when entities enter/exit the matching set.
670
1106
  *
671
- * @param componentTypesOrSingle - A single component type or an array of component types
1107
+ * @param componentTypes - Component types that define the matching entity set
672
1108
  * @param hook - Either a hook object with on_init/on_set/on_remove handlers, or a callback function
673
- * @param filter - Optional filter, only applied to array overload
1109
+ * @param filter - Optional query-style filter applied to the hook match set
674
1110
  * @returns A function that unsubscribes the hook when called
675
1111
  *
676
1112
  * @throws {Error} If no required components are specified in array overload
677
1113
  *
678
1114
  * @example
679
- * // Array overload (recommended)
680
1115
  * const unsubscribe = world.hook([Position, Velocity], {
681
1116
  * on_init: (entityId, position, velocity) => console.log("Initialized"),
682
1117
  * on_set: (entityId, position, velocity) => console.log("Updated"),
@@ -698,12 +1133,7 @@ declare class World {
698
1133
  * { negativeComponentTypes: [Disabled] },
699
1134
  * );
700
1135
  */
701
- hook<T>(componentType: EntityId<T>, hook: LegacyLifecycleHook<T> | LegacyLifecycleCallback<T>): () => void;
702
1136
  hook<const T extends readonly ComponentType<any>[]>(componentTypes: T, hook: LifecycleHook<T> | LifecycleCallback<T>, filter?: QueryFilter): () => void;
703
- /** @deprecated use the unsubscribe function returned by hook() instead */
704
- unhook<T>(componentType: EntityId<T>, hook: LegacyLifecycleHook<T>): void;
705
- /** @deprecated use the unsubscribe function returned by hook() instead */
706
- unhook<const T extends readonly ComponentType<any>[]>(componentTypes: T, hook: LifecycleHook<T>): void;
707
1137
  /**
708
1138
  * Synchronizes all buffered commands (set/remove/delete) to the world.
709
1139
  * This method must be called after making changes via `set()`, `remove()`, or `delete()` for them to take effect.
@@ -775,8 +1205,6 @@ declare class World {
775
1205
  * world.sync();
776
1206
  */
777
1207
  spawnMany(count: number, configure: (builder: EntityBuilder, index: number) => EntityBuilder): EntityId[];
778
- _registerQuery(query: Query): void;
779
- _unregisterQuery(query: Query): void;
780
1208
  /**
781
1209
  * Releases a cached query and frees its resources if no longer needed.
782
1210
  * Call this when you're done using a query to allow the world to clean up its cache entry.
@@ -831,16 +1259,19 @@ declare class World {
831
1259
  entity: EntityId;
832
1260
  components: ComponentTuple<T>;
833
1261
  }>;
834
- executeEntityCommands(entityId: EntityId, commands: Command[]): ComponentChangeset;
835
- private executeComponentEntityCommands;
1262
+ private executeEntityCommands;
1263
+ private applyEntityCommands;
836
1264
  private createHooksContext;
837
1265
  private removeComponentImmediate;
838
1266
  private updateEntityReferences;
839
1267
  private ensureArchetype;
1268
+ /** Add componentType to the reverse index if it contains an entity ID */
1269
+ private addToReferencingIndex;
1270
+ /** Remove componentType from the reverse index */
1271
+ private removeFromReferencingIndex;
840
1272
  private createNewArchetype;
841
1273
  private updateArchetypeHookMatches;
842
1274
  private archetypeMatchesHook;
843
- private archetypeReferencesEntity;
844
1275
  private cleanupArchetypesReferencingEntity;
845
1276
  private removeArchetype;
846
1277
  /**
@@ -867,7 +1298,7 @@ declare class World {
867
1298
  serialize(): SerializedWorld;
868
1299
  }
869
1300
  //#endregion
870
- //#region src/core/builder.d.ts
1301
+ //#region src/world/builder.d.ts
871
1302
  /**
872
1303
  * A component definition for entity building, supporting both regular components and relations
873
1304
  */
@@ -881,28 +1312,62 @@ type ComponentDef<T = unknown> = {
881
1312
  targetId: EntityId<any>;
882
1313
  value: T;
883
1314
  };
1315
+ /**
1316
+ * Fluent API for constructing entities with multiple components.
1317
+ * Create instances via {@link World.spawn}.
1318
+ *
1319
+ * @example
1320
+ * const entity = world.spawn()
1321
+ * .with(Position, { x: 0, y: 0 })
1322
+ * .withRelation(Parent, parentEntity)
1323
+ * .build();
1324
+ * world.sync();
1325
+ */
884
1326
  declare class EntityBuilder {
885
1327
  private world;
886
1328
  private components;
887
1329
  constructor(world: World);
888
- with<T>(componentId: EntityId<T>, ...args: T extends void ? [] | [void] : [T]): this;
889
1330
  /**
890
- * @deprecated Use `with(componentId)` instead for void components
1331
+ * Add a regular component to the entity under construction.
1332
+ *
1333
+ * @template T - The component data type
1334
+ * @param componentId - The component type to add
1335
+ * @param args - Component data (omit for void components)
1336
+ * @returns This builder for chaining
1337
+ *
1338
+ * @example
1339
+ * builder.with(Position, { x: 10, y: 20 });
1340
+ * builder.with(Marker); // void component
891
1341
  */
892
- withTag(componentId: EntityId<void>): this;
893
- withRelation<T>(componentId: ComponentId<T>, targetEntity: EntityId<any>, ...args: T extends void ? [] | [void] : [T]): this;
1342
+ with<T>(componentId: EntityId<T>, ...args: T extends void ? [] | [void] : [T]): this;
894
1343
  /**
895
- * @deprecated Use `withRelation(componentId, targetEntity)` instead for void relations
1344
+ * Add a relation component to the entity under construction.
1345
+ *
1346
+ * @template T - The relation data type
1347
+ * @param componentId - The base component type for the relation
1348
+ * @param targetEntity - The target entity or component for the relation
1349
+ * @param args - Relation data (omit for void relations)
1350
+ * @returns This builder for chaining
1351
+ *
1352
+ * @example
1353
+ * builder.withRelation(Parent, parentEntity);
1354
+ * builder.withRelation(ChildOf, childEntity, { order: 1 });
896
1355
  */
897
- withRelationTag(componentId: ComponentId<void>, targetEntity: EntityId<any>): this;
1356
+ withRelation<T>(componentId: ComponentId<T>, targetEntity: EntityId<any>, ...args: T extends void ? [] | [void] : [T]): this;
898
1357
  /**
899
- * Create an entity and enqueue components to be applied. This method
900
- * does NOT call `world.sync()` automatically; callers must invoke
901
- * `world.sync()` to apply deferred commands.
902
- * (Previously auto-synced; now a breaking change — buildDeferred() removed.)
1358
+ * Create the entity and enqueue all configured components.
1359
+ * The entity and components are only materialised after {@link World.sync} is called.
1360
+ *
1361
+ * @returns The newly created entity ID
1362
+ *
1363
+ * @example
1364
+ * const entity = world.spawn()
1365
+ * .with(Position, { x: 0, y: 0 })
1366
+ * .build();
1367
+ * world.sync(); // Apply changes
903
1368
  */
904
1369
  build(): EntityId;
905
1370
  }
906
1371
  //#endregion
907
- export { isRelationId as A, ComponentRelationId as C, WildcardRelationId as D, RelationId as E, isComponentId as O, ComponentId as S, EntityRelationId as T, getComponentIdByName as _, SerializedEntity as a, isWildcardRelationId as b, Query as c, LegacyLifecycleCallback as d, LegacyLifecycleHook as f, component as g, ComponentOptions as h, SerializedComponent as i, isEntityId as k, ComponentTuple as l, LifecycleHook as m, EntityBuilder as n, SerializedEntityId as o, LifecycleCallback as p, World as r, SerializedWorld as s, ComponentDef as t, ComponentType as u, getComponentNameById as v, EntityId as w, relation as x, decodeRelationId as y };
1372
+ export { EntityRelationId as C, isEntityId as D, isComponentId as E, isRelationId as O, EntityId as S, WildcardRelationId as T, decodeRelationId as _, ComponentTuple as a, ComponentId as b, LifecycleHook as c, SerializedEntityId as d, SerializedWorld as f, getComponentNameById as g, getComponentIdByName as h, Query as i, SerializedComponent as l, component as m, EntityBuilder as n, ComponentType as o, ComponentOptions as p, World as r, LifecycleCallback as s, ComponentDef as t, SerializedEntity as u, isWildcardRelationId as v, RelationId as w, ComponentRelationId as x, relation as y };
908
1373
  //# sourceMappingURL=builder.d.mts.map