@codehz/ecs 0.6.10 → 0.7.0
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/README.en.md +440 -0
- package/README.md +248 -269
- package/builder.d.mts +667 -196
- package/index.d.mts +2 -2
- package/package.json +1 -1
- package/testing.d.mts +2 -2
- package/testing.mjs.map +1 -1
- package/world.mjs +1457 -1137
- package/world.mjs.map +1 -1
package/builder.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//#region src/
|
|
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/
|
|
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
|
|
46
|
-
*
|
|
47
|
-
*
|
|
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
|
|
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/
|
|
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
|
-
*
|
|
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
|
|
78
|
-
* When
|
|
79
|
-
* with that base component are automatically removed.
|
|
80
|
-
*
|
|
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
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
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
|
|
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/
|
|
457
|
+
//#region src/storage/serialization.d.ts
|
|
458
|
+
type SerializedEntityId = number | string | {
|
|
459
|
+
component: string;
|
|
460
|
+
target: number | string | "*";
|
|
461
|
+
};
|
|
130
462
|
/**
|
|
131
|
-
*
|
|
463
|
+
* Serialized state of EntityIdManager
|
|
132
464
|
*/
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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/
|
|
484
|
+
//#region src/query/filter.d.ts
|
|
169
485
|
/**
|
|
170
|
-
*
|
|
171
|
-
* Uses discriminated union for type safety
|
|
486
|
+
* Filter options for queries
|
|
172
487
|
*/
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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/
|
|
492
|
+
//#region src/types/index.d.ts
|
|
188
493
|
/**
|
|
189
|
-
*
|
|
494
|
+
* Lifecycle hook definition for reacting to component additions, updates, and removals.
|
|
495
|
+
* Register hooks with {@link World.hook}.
|
|
190
496
|
*/
|
|
191
|
-
interface
|
|
497
|
+
interface LifecycleHook<T extends readonly ComponentType<any>[]> {
|
|
192
498
|
/**
|
|
193
|
-
* Called
|
|
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,
|
|
502
|
+
on_init?: (entityId: EntityId, ...components: ComponentTuple<T>) => void;
|
|
196
503
|
/**
|
|
197
|
-
* Called
|
|
504
|
+
* Called whenever a matching entity's component data is updated via `set()`.
|
|
198
505
|
*/
|
|
199
|
-
on_set?: (entityId: EntityId,
|
|
506
|
+
on_set?: (entityId: EntityId, ...components: ComponentTuple<T>) => void;
|
|
200
507
|
/**
|
|
201
|
-
* Called
|
|
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
|
-
*
|
|
212
|
-
*
|
|
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
|
|
524
|
+
type LifecycleCallback<T extends readonly ComponentType<any>[]> = (type: "init" | "set" | "remove", entityId: EntityId, ...components: ComponentTuple<T>) => void;
|
|
215
525
|
/**
|
|
216
|
-
*
|
|
217
|
-
*
|
|
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,17 +543,36 @@ 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
|
-
*
|
|
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 {
|
|
234
551
|
componentTypes: readonly ComponentType<any>[];
|
|
235
552
|
requiredComponents: EntityId<any>[];
|
|
236
553
|
optionalComponents: EntityId<any>[];
|
|
554
|
+
filter: QueryFilter;
|
|
237
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;
|
|
238
573
|
}
|
|
239
574
|
//#endregion
|
|
240
|
-
//#region src/
|
|
575
|
+
//#region src/archetype/archetype.d.ts
|
|
241
576
|
/**
|
|
242
577
|
* Archetype class for ECS architecture
|
|
243
578
|
* Represents a group of entities that share the same set of components
|
|
@@ -266,9 +601,9 @@ declare class Archetype {
|
|
|
266
601
|
*/
|
|
267
602
|
private entityToIndex;
|
|
268
603
|
/**
|
|
269
|
-
*
|
|
604
|
+
* DontFragmentStore for relation data keyed by entity ID.
|
|
270
605
|
* This allows entities with different relation targets to share the same archetype
|
|
271
|
-
*
|
|
606
|
+
* without migration overhead when entities change archetypes.
|
|
272
607
|
*/
|
|
273
608
|
private dontFragmentRelations;
|
|
274
609
|
/**
|
|
@@ -279,7 +614,7 @@ declare class Archetype {
|
|
|
279
614
|
* Cache for pre-computed component data sources to avoid repeated calculations
|
|
280
615
|
*/
|
|
281
616
|
private componentDataSourcesCache;
|
|
282
|
-
constructor(componentTypes: EntityId<any>[], dontFragmentRelations:
|
|
617
|
+
constructor(componentTypes: EntityId<any>[], dontFragmentRelations: DontFragmentStore);
|
|
283
618
|
get size(): number;
|
|
284
619
|
/**
|
|
285
620
|
* Check if the given component types match this archetype
|
|
@@ -317,23 +652,88 @@ declare class Archetype {
|
|
|
317
652
|
entity: EntityId;
|
|
318
653
|
components: ComponentTuple<T>;
|
|
319
654
|
}>;
|
|
655
|
+
appendEntitiesWithComponents<const T extends readonly ComponentType<any>[]>(componentTypes: T, result: Array<{
|
|
656
|
+
entity: EntityId;
|
|
657
|
+
components: ComponentTuple<T>;
|
|
658
|
+
}>): void;
|
|
320
659
|
iterateWithComponents<const T extends readonly ComponentType<any>[]>(componentTypes: T): IterableIterator<[EntityId, ...ComponentTuple<T>]>;
|
|
321
660
|
forEachWithComponents<const T extends readonly ComponentType<any>[]>(componentTypes: T, callback: (entity: EntityId, ...components: ComponentTuple<T>) => void): void;
|
|
322
661
|
forEach(callback: (entityId: EntityId, components: Map<EntityId<any>, any>) => void): void;
|
|
323
662
|
hasRelationWithComponentId(componentId: EntityId<any>): boolean;
|
|
324
663
|
}
|
|
325
664
|
//#endregion
|
|
326
|
-
//#region src/query/
|
|
665
|
+
//#region src/query/registry.d.ts
|
|
327
666
|
/**
|
|
328
|
-
*
|
|
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.
|
|
329
676
|
*/
|
|
330
|
-
|
|
331
|
-
|
|
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;
|
|
332
719
|
}
|
|
333
720
|
//#endregion
|
|
334
721
|
//#region src/query/query.d.ts
|
|
335
722
|
/**
|
|
336
|
-
*
|
|
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
|
+
* });
|
|
337
737
|
*/
|
|
338
738
|
declare class Query {
|
|
339
739
|
private world;
|
|
@@ -347,13 +747,24 @@ declare class Query {
|
|
|
347
747
|
private wildcardTypes;
|
|
348
748
|
/** Cached specific dontFragment relation types that need entity-level filtering */
|
|
349
749
|
private specificDontFragmentTypes;
|
|
350
|
-
|
|
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);
|
|
351
754
|
/**
|
|
352
755
|
* Check if query is disposed and throw error if so
|
|
353
756
|
*/
|
|
354
757
|
private ensureNotDisposed;
|
|
355
758
|
/**
|
|
356
|
-
*
|
|
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
|
+
* }
|
|
357
768
|
*/
|
|
358
769
|
getEntities(): EntityId[];
|
|
359
770
|
/**
|
|
@@ -361,42 +772,67 @@ declare class Query {
|
|
|
361
772
|
*/
|
|
362
773
|
private entityMatchesQuery;
|
|
363
774
|
/**
|
|
364
|
-
*
|
|
365
|
-
*
|
|
366
|
-
* @
|
|
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
|
+
* });
|
|
367
785
|
*/
|
|
368
786
|
getEntitiesWithComponents<const T extends readonly ComponentType<any>[]>(componentTypes: T): Array<{
|
|
369
787
|
entity: EntityId;
|
|
370
788
|
components: ComponentTuple<T>;
|
|
371
789
|
}>;
|
|
372
790
|
/**
|
|
373
|
-
*
|
|
374
|
-
*
|
|
375
|
-
*
|
|
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
|
+
* });
|
|
376
802
|
*/
|
|
377
803
|
forEach<const T extends readonly ComponentType<any>[]>(componentTypes: T, callback: (entity: EntityId, ...components: ComponentTuple<T>) => void): void;
|
|
378
804
|
/**
|
|
379
|
-
*
|
|
380
|
-
*
|
|
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
|
+
* }
|
|
381
814
|
*/
|
|
382
815
|
iterate<const T extends readonly ComponentType<any>[]>(componentTypes: T): IterableIterator<[EntityId, ...ComponentTuple<T>]>;
|
|
383
816
|
/**
|
|
384
|
-
*
|
|
385
|
-
*
|
|
386
|
-
* @
|
|
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);
|
|
387
824
|
*/
|
|
388
825
|
getComponentData<T>(componentType: EntityId<T>): T[];
|
|
389
826
|
/**
|
|
390
|
-
*
|
|
391
|
-
* Called when new archetypes are created
|
|
827
|
+
* @internal Rebuilds the cached archetype list. Called automatically by the world.
|
|
392
828
|
*/
|
|
393
829
|
updateCache(): void;
|
|
394
830
|
/**
|
|
395
|
-
*
|
|
831
|
+
* @internal Called by the world when a new archetype is created.
|
|
396
832
|
*/
|
|
397
833
|
checkNewArchetype(archetype: Archetype): void;
|
|
398
834
|
/**
|
|
399
|
-
*
|
|
835
|
+
* @internal Called by the world when an archetype is destroyed.
|
|
400
836
|
*/
|
|
401
837
|
removeArchetype(archetype: Archetype): void;
|
|
402
838
|
/**
|
|
@@ -406,47 +842,24 @@ declare class Query {
|
|
|
406
842
|
*/
|
|
407
843
|
dispose(): void;
|
|
408
844
|
/**
|
|
409
|
-
*
|
|
845
|
+
* @internal Fully disposes the query when the world's refCount reaches zero.
|
|
410
846
|
*/
|
|
411
|
-
_disposeInternal(): void;
|
|
847
|
+
_disposeInternal(registry?: QueryRegistry): void;
|
|
412
848
|
/**
|
|
413
|
-
*
|
|
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
|
|
414
854
|
*/
|
|
415
855
|
[Symbol.dispose](): void;
|
|
416
856
|
/**
|
|
417
|
-
*
|
|
857
|
+
* Whether the query has been disposed and can no longer be used.
|
|
418
858
|
*/
|
|
419
859
|
get disposed(): boolean;
|
|
420
860
|
}
|
|
421
861
|
//#endregion
|
|
422
|
-
//#region src/
|
|
423
|
-
type SerializedEntityId = number | string | {
|
|
424
|
-
component: string;
|
|
425
|
-
target: number | string | "*";
|
|
426
|
-
};
|
|
427
|
-
/**
|
|
428
|
-
* Serialized state of EntityIdManager
|
|
429
|
-
*/
|
|
430
|
-
interface SerializedEntityIdManager {
|
|
431
|
-
nextId: number;
|
|
432
|
-
freelist?: number[];
|
|
433
|
-
}
|
|
434
|
-
type SerializedWorld = {
|
|
435
|
-
version: number;
|
|
436
|
-
entityManager: SerializedEntityIdManager;
|
|
437
|
-
entities: SerializedEntity[];
|
|
438
|
-
componentEntities?: SerializedEntity[];
|
|
439
|
-
};
|
|
440
|
-
type SerializedEntity = {
|
|
441
|
-
id: SerializedEntityId;
|
|
442
|
-
components: SerializedComponent[];
|
|
443
|
-
};
|
|
444
|
-
type SerializedComponent = {
|
|
445
|
-
type: SerializedEntityId;
|
|
446
|
-
value: any;
|
|
447
|
-
};
|
|
448
|
-
//#endregion
|
|
449
|
-
//#region src/core/world.d.ts
|
|
862
|
+
//#region src/world/world.d.ts
|
|
450
863
|
/**
|
|
451
864
|
* World class for ECS architecture
|
|
452
865
|
* Manages entities and components
|
|
@@ -458,21 +871,22 @@ declare class World {
|
|
|
458
871
|
private entityToArchetype;
|
|
459
872
|
private archetypesByComponent;
|
|
460
873
|
private entityReferences;
|
|
461
|
-
|
|
462
|
-
private
|
|
463
|
-
|
|
464
|
-
private
|
|
465
|
-
|
|
466
|
-
private
|
|
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;
|
|
467
881
|
private hooks;
|
|
468
882
|
private commandBuffer;
|
|
469
883
|
private readonly _changeset;
|
|
884
|
+
private readonly _removeChangeset;
|
|
470
885
|
/** Cached command processor context to avoid per-entity object allocation */
|
|
471
886
|
private readonly _commandCtx;
|
|
472
887
|
/** Cached hooks context to avoid per-entity object allocation */
|
|
473
888
|
private readonly _hooksCtx;
|
|
474
889
|
constructor(snapshot?: SerializedWorld);
|
|
475
|
-
private deserializeSnapshot;
|
|
476
890
|
private createArchetypeSignature;
|
|
477
891
|
/**
|
|
478
892
|
* Creates a new entity.
|
|
@@ -487,23 +901,35 @@ declare class World {
|
|
|
487
901
|
* world.sync();
|
|
488
902
|
*/
|
|
489
903
|
new<T = void>(): EntityId<T>;
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
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;
|
|
496
914
|
private destroyEntityImmediate;
|
|
497
915
|
/**
|
|
498
|
-
* 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.
|
|
499
921
|
*
|
|
500
922
|
* @param entityId - The entity identifier to check
|
|
501
923
|
* @returns `true` if the entity exists, `false` otherwise
|
|
502
924
|
*
|
|
503
925
|
* @example
|
|
926
|
+
* // Check if an entity is alive
|
|
504
927
|
* if (world.exists(entityId)) {
|
|
505
928
|
* console.log("Entity exists");
|
|
506
929
|
* }
|
|
930
|
+
*
|
|
931
|
+
* // To check for a component, use has() instead:
|
|
932
|
+
* if (world.has(entity, Position)) { ... }
|
|
507
933
|
*/
|
|
508
934
|
exists(entityId: EntityId): boolean;
|
|
509
935
|
private assertEntityExists;
|
|
@@ -511,7 +937,6 @@ declare class World {
|
|
|
511
937
|
private assertSetComponentTypeValid;
|
|
512
938
|
private resolveSetOperation;
|
|
513
939
|
private resolveRemoveOperation;
|
|
514
|
-
private getComponentEntityWildcardRelations;
|
|
515
940
|
/**
|
|
516
941
|
* Adds or updates a component on an entity (or marks void component as present).
|
|
517
942
|
* The change is buffered and takes effect after calling `world.sync()`.
|
|
@@ -576,27 +1001,39 @@ declare class World {
|
|
|
576
1001
|
*/
|
|
577
1002
|
delete(entityId: EntityId): void;
|
|
578
1003
|
/**
|
|
579
|
-
* Checks if
|
|
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
|
+
*
|
|
580
1010
|
* Immediately reflects the current state without waiting for `sync()`.
|
|
581
1011
|
*
|
|
582
1012
|
* @overload has<T>(entityId: EntityId, componentType: EntityId<T>): boolean
|
|
583
1013
|
* Checks if a specific component type is present on the entity.
|
|
584
1014
|
*
|
|
585
1015
|
* @overload has<T>(componentId: ComponentId<T>): boolean
|
|
586
|
-
*
|
|
1016
|
+
* Shorthand for checking a **singleton component** — a component that is its own
|
|
1017
|
+
* entity (component-as-entity pattern). Equivalent to `has(componentId, componentId)`.
|
|
587
1018
|
*
|
|
588
1019
|
* @template T - The component data type
|
|
589
|
-
* @param entityId - The entity identifier
|
|
1020
|
+
* @param entityId - The entity identifier, or a singleton component ID
|
|
590
1021
|
* @param componentType - The component type to check
|
|
591
1022
|
* @returns `true` if the entity has the component, `false` otherwise
|
|
592
1023
|
*
|
|
593
1024
|
* @example
|
|
1025
|
+
* // Check if an entity has a component
|
|
594
1026
|
* if (world.has(entity, Position)) {
|
|
595
1027
|
* const pos = world.get(entity, Position);
|
|
596
1028
|
* }
|
|
1029
|
+
*
|
|
1030
|
+
* // Check a singleton component (component-as-entity)
|
|
597
1031
|
* if (world.has(GlobalConfig)) {
|
|
598
1032
|
* const config = world.get(GlobalConfig);
|
|
599
1033
|
* }
|
|
1034
|
+
*
|
|
1035
|
+
* // Use exists() for entity liveness checks
|
|
1036
|
+
* if (world.exists(entity)) { ... }
|
|
600
1037
|
*/
|
|
601
1038
|
has<T>(componentId: ComponentId<T>): boolean;
|
|
602
1039
|
has<T>(entityId: EntityId, componentType: EntityId<T>): boolean;
|
|
@@ -653,27 +1090,22 @@ declare class World {
|
|
|
653
1090
|
/**
|
|
654
1091
|
* Registers a lifecycle hook that responds to component changes.
|
|
655
1092
|
* The hook callback is invoked when components matching the specified types are added, updated, or removed.
|
|
656
|
-
*
|
|
657
|
-
* @deprecated For single components, use the array overload with LifecycleCallback for better multi-component support
|
|
658
|
-
*
|
|
659
|
-
* @overload hook<T>(componentType: EntityId<T>, hook: LegacyLifecycleHook<T> | LegacyLifecycleCallback<T>): () => void
|
|
660
|
-
* Registers a hook for a single component type (legacy API).
|
|
661
|
-
*
|
|
662
1093
|
* @overload hook<const T extends readonly ComponentType<any>[]>(
|
|
663
1094
|
* componentTypes: T,
|
|
664
1095
|
* hook: LifecycleHook<T> | LifecycleCallback<T>,
|
|
1096
|
+
* filter?: QueryFilter,
|
|
665
1097
|
* ): () => void
|
|
666
1098
|
* Registers a hook for multiple component types.
|
|
667
|
-
* The hook is triggered when
|
|
1099
|
+
* The hook is triggered when entities enter/exit the matching set.
|
|
668
1100
|
*
|
|
669
|
-
* @param
|
|
1101
|
+
* @param componentTypes - Component types that define the matching entity set
|
|
670
1102
|
* @param hook - Either a hook object with on_init/on_set/on_remove handlers, or a callback function
|
|
1103
|
+
* @param filter - Optional query-style filter applied to the hook match set
|
|
671
1104
|
* @returns A function that unsubscribes the hook when called
|
|
672
1105
|
*
|
|
673
1106
|
* @throws {Error} If no required components are specified in array overload
|
|
674
1107
|
*
|
|
675
1108
|
* @example
|
|
676
|
-
* // Array overload (recommended)
|
|
677
1109
|
* const unsubscribe = world.hook([Position, Velocity], {
|
|
678
1110
|
* on_init: (entityId, position, velocity) => console.log("Initialized"),
|
|
679
1111
|
* on_set: (entityId, position, velocity) => console.log("Updated"),
|
|
@@ -685,13 +1117,17 @@ declare class World {
|
|
|
685
1117
|
* const unsubscribe = world.hook([Position], (event, entityId, position) => {
|
|
686
1118
|
* if (event === "init") console.log("Initialized");
|
|
687
1119
|
* });
|
|
1120
|
+
*
|
|
1121
|
+
* // With filter
|
|
1122
|
+
* const unsubscribe2 = world.hook(
|
|
1123
|
+
* [Position, Velocity],
|
|
1124
|
+
* {
|
|
1125
|
+
* on_set: (entityId, position, velocity) => console.log(entityId, position, velocity),
|
|
1126
|
+
* },
|
|
1127
|
+
* { negativeComponentTypes: [Disabled] },
|
|
1128
|
+
* );
|
|
688
1129
|
*/
|
|
689
|
-
hook<T>(
|
|
690
|
-
hook<const T extends readonly ComponentType<any>[]>(componentTypes: T, hook: LifecycleHook<T> | LifecycleCallback<T>): () => void;
|
|
691
|
-
/** @deprecated use the unsubscribe function returned by hook() instead */
|
|
692
|
-
unhook<T>(componentType: EntityId<T>, hook: LegacyLifecycleHook<T>): void;
|
|
693
|
-
/** @deprecated use the unsubscribe function returned by hook() instead */
|
|
694
|
-
unhook<const T extends readonly ComponentType<any>[]>(componentTypes: T, hook: LifecycleHook<T>): void;
|
|
1130
|
+
hook<const T extends readonly ComponentType<any>[]>(componentTypes: T, hook: LifecycleHook<T> | LifecycleCallback<T>, filter?: QueryFilter): () => void;
|
|
695
1131
|
/**
|
|
696
1132
|
* Synchronizes all buffered commands (set/remove/delete) to the world.
|
|
697
1133
|
* This method must be called after making changes via `set()`, `remove()`, or `delete()` for them to take effect.
|
|
@@ -763,8 +1199,6 @@ declare class World {
|
|
|
763
1199
|
* world.sync();
|
|
764
1200
|
*/
|
|
765
1201
|
spawnMany(count: number, configure: (builder: EntityBuilder, index: number) => EntityBuilder): EntityId[];
|
|
766
|
-
_registerQuery(query: Query): void;
|
|
767
|
-
_unregisterQuery(query: Query): void;
|
|
768
1202
|
/**
|
|
769
1203
|
* Releases a cached query and frees its resources if no longer needed.
|
|
770
1204
|
* Call this when you're done using a query to allow the world to clean up its cache entry.
|
|
@@ -819,16 +1253,19 @@ declare class World {
|
|
|
819
1253
|
entity: EntityId;
|
|
820
1254
|
components: ComponentTuple<T>;
|
|
821
1255
|
}>;
|
|
822
|
-
executeEntityCommands
|
|
823
|
-
private
|
|
1256
|
+
private executeEntityCommands;
|
|
1257
|
+
private applyEntityCommands;
|
|
824
1258
|
private createHooksContext;
|
|
825
1259
|
private removeComponentImmediate;
|
|
826
1260
|
private updateEntityReferences;
|
|
827
1261
|
private ensureArchetype;
|
|
1262
|
+
/** Add componentType to the reverse index if it contains an entity ID */
|
|
1263
|
+
private addToReferencingIndex;
|
|
1264
|
+
/** Remove componentType from the reverse index */
|
|
1265
|
+
private removeFromReferencingIndex;
|
|
828
1266
|
private createNewArchetype;
|
|
829
1267
|
private updateArchetypeHookMatches;
|
|
830
1268
|
private archetypeMatchesHook;
|
|
831
|
-
private archetypeReferencesEntity;
|
|
832
1269
|
private cleanupArchetypesReferencingEntity;
|
|
833
1270
|
private removeArchetype;
|
|
834
1271
|
/**
|
|
@@ -855,7 +1292,7 @@ declare class World {
|
|
|
855
1292
|
serialize(): SerializedWorld;
|
|
856
1293
|
}
|
|
857
1294
|
//#endregion
|
|
858
|
-
//#region src/
|
|
1295
|
+
//#region src/world/builder.d.ts
|
|
859
1296
|
/**
|
|
860
1297
|
* A component definition for entity building, supporting both regular components and relations
|
|
861
1298
|
*/
|
|
@@ -869,28 +1306,62 @@ type ComponentDef<T = unknown> = {
|
|
|
869
1306
|
targetId: EntityId<any>;
|
|
870
1307
|
value: T;
|
|
871
1308
|
};
|
|
1309
|
+
/**
|
|
1310
|
+
* Fluent API for constructing entities with multiple components.
|
|
1311
|
+
* Create instances via {@link World.spawn}.
|
|
1312
|
+
*
|
|
1313
|
+
* @example
|
|
1314
|
+
* const entity = world.spawn()
|
|
1315
|
+
* .with(Position, { x: 0, y: 0 })
|
|
1316
|
+
* .withRelation(Parent, parentEntity)
|
|
1317
|
+
* .build();
|
|
1318
|
+
* world.sync();
|
|
1319
|
+
*/
|
|
872
1320
|
declare class EntityBuilder {
|
|
873
1321
|
private world;
|
|
874
1322
|
private components;
|
|
875
1323
|
constructor(world: World);
|
|
876
|
-
with<T>(componentId: EntityId<T>, ...args: T extends void ? [] | [void] : [T]): this;
|
|
877
1324
|
/**
|
|
878
|
-
*
|
|
1325
|
+
* Add a regular component to the entity under construction.
|
|
1326
|
+
*
|
|
1327
|
+
* @template T - The component data type
|
|
1328
|
+
* @param componentId - The component type to add
|
|
1329
|
+
* @param args - Component data (omit for void components)
|
|
1330
|
+
* @returns This builder for chaining
|
|
1331
|
+
*
|
|
1332
|
+
* @example
|
|
1333
|
+
* builder.with(Position, { x: 10, y: 20 });
|
|
1334
|
+
* builder.with(Marker); // void component
|
|
879
1335
|
*/
|
|
880
|
-
|
|
881
|
-
withRelation<T>(componentId: ComponentId<T>, targetEntity: EntityId<any>, ...args: T extends void ? [] | [void] : [T]): this;
|
|
1336
|
+
with<T>(componentId: EntityId<T>, ...args: T extends void ? [] | [void] : [T]): this;
|
|
882
1337
|
/**
|
|
883
|
-
*
|
|
1338
|
+
* Add a relation component to the entity under construction.
|
|
1339
|
+
*
|
|
1340
|
+
* @template T - The relation data type
|
|
1341
|
+
* @param componentId - The base component type for the relation
|
|
1342
|
+
* @param targetEntity - The target entity or component for the relation
|
|
1343
|
+
* @param args - Relation data (omit for void relations)
|
|
1344
|
+
* @returns This builder for chaining
|
|
1345
|
+
*
|
|
1346
|
+
* @example
|
|
1347
|
+
* builder.withRelation(Parent, parentEntity);
|
|
1348
|
+
* builder.withRelation(ChildOf, childEntity, { order: 1 });
|
|
884
1349
|
*/
|
|
885
|
-
|
|
1350
|
+
withRelation<T>(componentId: ComponentId<T>, targetEntity: EntityId<any>, ...args: T extends void ? [] | [void] : [T]): this;
|
|
886
1351
|
/**
|
|
887
|
-
* Create
|
|
888
|
-
*
|
|
889
|
-
*
|
|
890
|
-
*
|
|
1352
|
+
* Create the entity and enqueue all configured components.
|
|
1353
|
+
* The entity and components are only materialised after {@link World.sync} is called.
|
|
1354
|
+
*
|
|
1355
|
+
* @returns The newly created entity ID
|
|
1356
|
+
*
|
|
1357
|
+
* @example
|
|
1358
|
+
* const entity = world.spawn()
|
|
1359
|
+
* .with(Position, { x: 0, y: 0 })
|
|
1360
|
+
* .build();
|
|
1361
|
+
* world.sync(); // Apply changes
|
|
891
1362
|
*/
|
|
892
1363
|
build(): EntityId;
|
|
893
1364
|
}
|
|
894
1365
|
//#endregion
|
|
895
|
-
export {
|
|
1366
|
+
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 };
|
|
896
1367
|
//# sourceMappingURL=builder.d.mts.map
|