@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/README.en.md +446 -0
- package/README.md +250 -288
- package/builder.d.mts +660 -195
- 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 +1532 -1215
- 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,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
|
-
*
|
|
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/
|
|
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
|
-
*
|
|
604
|
+
* DontFragmentStore for relation data keyed by entity ID.
|
|
271
605
|
* This allows entities with different relation targets to share the same archetype
|
|
272
|
-
*
|
|
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:
|
|
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/
|
|
665
|
+
//#region src/query/registry.d.ts
|
|
328
666
|
/**
|
|
329
|
-
*
|
|
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
|
-
|
|
332
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
366
|
-
*
|
|
367
|
-
* @
|
|
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
|
-
*
|
|
375
|
-
*
|
|
376
|
-
*
|
|
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
|
-
*
|
|
381
|
-
*
|
|
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
|
-
*
|
|
386
|
-
*
|
|
387
|
-
* @
|
|
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
|
-
*
|
|
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
|
-
*
|
|
831
|
+
* @internal Called by the world when a new archetype is created.
|
|
397
832
|
*/
|
|
398
833
|
checkNewArchetype(archetype: Archetype): void;
|
|
399
834
|
/**
|
|
400
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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/
|
|
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
|
-
|
|
463
|
-
private
|
|
464
|
-
|
|
465
|
-
private
|
|
466
|
-
|
|
467
|
-
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;
|
|
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
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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
|
|
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
|
|
835
|
-
private
|
|
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/
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
1356
|
+
withRelation<T>(componentId: ComponentId<T>, targetEntity: EntityId<any>, ...args: T extends void ? [] | [void] : [T]): this;
|
|
898
1357
|
/**
|
|
899
|
-
* Create
|
|
900
|
-
*
|
|
901
|
-
*
|
|
902
|
-
*
|
|
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 {
|
|
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
|