@vworlds/vecs 1.0.10 → 1.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/.husky/pre-commit +1 -0
  2. package/README.md +299 -228
  3. package/dist/command.d.ts +1 -46
  4. package/dist/component.d.ts +51 -59
  5. package/dist/component.js +31 -25
  6. package/dist/component.js.map +1 -1
  7. package/dist/dsl.d.ts +34 -26
  8. package/dist/dsl.js +46 -20
  9. package/dist/dsl.js.map +1 -1
  10. package/dist/entity.d.ts +96 -106
  11. package/dist/entity.js +261 -190
  12. package/dist/entity.js.map +1 -1
  13. package/dist/filter.d.ts +31 -23
  14. package/dist/filter.js +24 -17
  15. package/dist/filter.js.map +1 -1
  16. package/dist/index.d.ts +2 -1
  17. package/dist/index.js +1 -0
  18. package/dist/index.js.map +1 -1
  19. package/dist/package.json +3 -1
  20. package/dist/phase.d.ts +12 -30
  21. package/dist/phase.js +11 -10
  22. package/dist/phase.js.map +1 -1
  23. package/dist/query.d.ts +107 -144
  24. package/dist/query.js +200 -169
  25. package/dist/query.js.map +1 -1
  26. package/dist/system.d.ts +170 -86
  27. package/dist/system.js +253 -114
  28. package/dist/system.js.map +1 -1
  29. package/dist/timer.d.ts +50 -0
  30. package/dist/timer.js +154 -0
  31. package/dist/timer.js.map +1 -0
  32. package/dist/util/array_map.d.ts +4 -55
  33. package/dist/util/array_map.js +35 -37
  34. package/dist/util/array_map.js.map +1 -1
  35. package/dist/util/bitset.d.ts +40 -50
  36. package/dist/util/bitset.js +76 -62
  37. package/dist/util/bitset.js.map +1 -1
  38. package/dist/util/events.d.ts +14 -18
  39. package/dist/util/events.js +24 -3
  40. package/dist/util/events.js.map +1 -1
  41. package/dist/util/ordered_set.d.ts +1 -17
  42. package/dist/util/ordered_set.js +74 -25
  43. package/dist/util/ordered_set.js.map +1 -1
  44. package/dist/world.d.ts +230 -218
  45. package/dist/world.js +422 -327
  46. package/dist/world.js.map +1 -1
  47. package/eslint-rules/internal-underscore.js +60 -0
  48. package/eslint.config.js +5 -0
  49. package/package.json +3 -1
package/dist/world.d.ts CHANGED
@@ -4,21 +4,21 @@ import { Query } from "./query.js";
4
4
  import { System } from "./system.js";
5
5
  import { Filter } from "./filter.js";
6
6
  import { type QueryDSL, type ExtractRequired } from "./dsl.js";
7
- import { IPhase, Phase } from "./phase.js";
8
- import { type Command } from "./command.js";
7
+ import { IPhase } from "./phase.js";
9
8
  /**
10
- * The central ECS container.
9
+ * The central ECS container. One world per game session.
11
10
  *
12
- * A `World` owns all entities, components, systems, queries, and the update
13
- * pipeline. Typical lifecycle:
11
+ * A `World` owns every entity, every registered component class, every
12
+ * registered query / system, and the update pipeline. The typical lifecycle:
14
13
  *
15
- * 1. **Register components** — call {@link registerComponent} (and optionally
16
- * {@link registerComponentType}) for every component class.
17
- * 2. **Register systems and queries** — call {@link system} and {@link query}
18
- * to create and configure them.
14
+ * 1. **Register components** — {@link registerComponent} (and optionally
15
+ * {@link registerComponentType}) for every component class you plan to use.
16
+ * 2. **Build the pipeline** — {@link addPhase} for every named phase, then
17
+ * {@link system} / {@link query} for each processor.
19
18
  * 3. **Start** — call {@link start} to freeze component registration and
20
19
  * distribute systems into their phases.
21
- * 4. **Run loop** — call {@link runPhase} once per frame for each phase.
20
+ * 4. **Run loop** — call {@link runPhase} per phase or {@link progress} for
21
+ * every phase, once per frame.
22
22
  *
23
23
  * ```ts
24
24
  * const world = new World();
@@ -28,204 +28,234 @@ import { type Command } from "./command.js";
28
28
  *
29
29
  * world.system("Move")
30
30
  * .requires(Position, Velocity)
31
- * .update(Position, (pos) => { pos.x += vel.x; });
31
+ * .each([Position, Velocity], (e, [pos, vel]) => {
32
+ * pos.x += vel.vx;
33
+ * });
32
34
  *
33
35
  * world.start();
34
36
  *
35
37
  * // game loop:
36
- * world.runPhase(updatePhase, Date.now(), 16);
38
+ * world.progress(now, delta);
37
39
  * ```
40
+ *
41
+ * ## Deferred mode
42
+ *
43
+ * The world can be in **deferred mode**, in which case entity mutations
44
+ * (`add` / `set` / `remove` / `destroy` / `setParent` / `modified`) are
45
+ * queued instead of applied inline. Systems run inside an automatically
46
+ * deferred scope; user code can wrap arbitrary blocks with
47
+ * {@link beginDefer} / {@link endDefer} or {@link defer}. {@link flush}
48
+ * drains the queue at top level.
38
49
  */
39
50
  export declare class World {
40
- private _entities;
41
- private componentNameTypeMap;
42
- private _queries;
43
- private Class2Meta;
44
- private Type2Meta;
45
- private localComponentCounter;
46
- private componentRegistrationDisabled;
47
- /** @internal Single ordered command queue used in deferred mode. */
48
- private commandQueue;
49
- /** @internal Nested `beginDefer` / `endDefer` count. */
50
- private deferredDepth;
51
- /** @internal True while `processCommandQueue` is iterating, to avoid re-entrant drains. */
52
- private draining;
53
- /** `true` when the world is in deferred mode — mutations are queued rather than applied immediately. */
54
- get deferred(): boolean;
55
- /** @internal */
56
- _pipeline: Map<string, Phase>;
57
- private eidCounter;
58
51
  constructor();
59
- /** @readonly */
52
+ /** Read-only view of the live entities, keyed by entity id. */
60
53
  get entities(): Omit<Map<number, Entity>, "set" | "delete" | "clear">;
61
- /** @readonly */
54
+ /** Read-only view of every registered query (includes systems). */
62
55
  get queries(): ReadonlyArray<Query>;
63
56
  /**
64
- * Return the entity with id `eid`, creating it if it does not yet exist.
65
- *
66
- * Used by networking code to materialise server-assigned entities:
57
+ * `true` while the world is in deferred mode entity mutations are queued
58
+ * rather than applied inline. Equivalent to "the queue depth is non-zero or
59
+ * the world is currently draining".
60
+ */
61
+ get deferred(): boolean;
62
+ /**
63
+ * Enter deferred mode. Mutations made until the matching {@link endDefer}
64
+ * are queued instead of executing inline.
67
65
  *
68
- * ```ts
69
- * const e = world.getOrCreateEntity(snapshot.eid, (e) => {
70
- * networkEntities.add(e);
71
- * });
72
- * e.add(snapshot.type, false);
73
- * ```
66
+ * Nested `beginDefer` / `endDefer` pairs are allowed; only the outermost
67
+ * `endDefer` triggers a queue drain.
68
+ */
69
+ beginDefer(): void;
70
+ /**
71
+ * Leave deferred mode. When the depth returns to zero the world drains the
72
+ * command queue (firing hooks and routing enter / exit / update events).
73
+ */
74
+ endDefer(): void;
75
+ /**
76
+ * Run `fn` inside a deferred scope. Equivalent to
77
+ * `beginDefer(); try { fn(); } finally { endDefer(); }`.
74
78
  *
75
- * @param eid - The entity id to look up or create.
76
- * @param onCreateCallback - Optional callback invoked only when a **new**
77
- * entity is created, before it is returned. Use this to initialise
78
- * bookkeeping (e.g. tracking it in a local set).
79
- * @returns The existing or newly created entity.
79
+ * @param fn - Callback executed in deferred mode.
80
80
  */
81
- getOrCreateEntity(eid: number, onCreateCallback?: (e: Entity) => void): Entity;
81
+ defer(fn: () => void): void;
82
82
  /**
83
- * Create a new entity with an auto-assigned id and register it in the world.
83
+ * Drain any commands queued at the top level (depth 0).
84
84
  *
85
- * The id counter starts at 0 (or at the value set by
86
- * {@link setEntityIdRange}) and increments by one for each call.
85
+ * Call between phases or after batch-loading network snapshots to surface
86
+ * accumulated mutations (firing hooks and routing enter / exit / update)
87
+ * before the next read or system run.
87
88
  */
88
- entity(): Entity;
89
+ flush(): void;
89
90
  /**
90
- * Look up an entity by id.
91
+ * Pre-register a `componentName typeId` mapping without binding a class.
91
92
  *
92
- * @param id - Numeric entity id.
93
- * @returns The entity, or `undefined` if no entity with that id exists.
93
+ * Useful when network messages refer to components by type id and the
94
+ * corresponding class may be registered later. Call this **before**
95
+ * {@link registerComponent} so the class picks up the server-assigned id
96
+ * rather than a locally generated one.
97
+ *
98
+ * @param componentName - String name used in network payloads.
99
+ * @param type - Numeric type id assigned by the server.
94
100
  */
95
- entity(id: number): Entity | undefined;
101
+ registerComponentType(componentName: string, type: number): void;
96
102
  /**
97
- * Set the starting value for the auto-incrementing entity id counter.
103
+ * Register a component class with the world.
98
104
  *
99
- * Must be called **before** {@link start} (or
100
- * {@link disableComponentRegistration}). Useful when the world runs alongside
101
- * a server that owns a different id range — for example, locally-created
102
- * client entities can start at a high offset to avoid collisions with
103
- * server-assigned ids.
105
+ * Must be called before any entity uses the component. Registration is
106
+ * disabled once {@link start} (or {@link disableComponentRegistration}) is
107
+ * called.
104
108
  *
105
- * @param min - The first id that will be assigned by {@link entity}.
106
- * @throws If called after registration has been disabled.
109
+ * **Overloads:**
110
+ * - `registerComponent(Class)` type id auto-assigned from the
111
+ * {@link registerComponentType} map, falling back to a local counter
112
+ * (≥ 256) if the name is not yet mapped.
113
+ * - `registerComponent(Class, type)` — explicit numeric type id.
114
+ * - `registerComponent(Class, componentName)` — auto-assigned id, custom
115
+ * display name (useful when the class name differs from the network name).
116
+ * - `registerComponent(Class, type, componentName)` — explicit id + name.
117
+ *
118
+ * @param ComponentClass - Component class to register.
119
+ * @throws When the class has already been registered or registration is
120
+ * disabled.
107
121
  */
108
- setEntityIdRange(min: number): void;
122
+ registerComponent(ComponentClass: typeof Component): void;
123
+ registerComponent(ComponentClass: typeof Component, type: number): void;
124
+ registerComponent(ComponentClass: typeof Component, componentName?: string): void;
125
+ registerComponent(ComponentClass: typeof Component, type: number, componentName: string): void;
109
126
  /**
110
- * Retrieve the {@link ComponentMeta} record for a registered component.
127
+ * Look up the {@link ComponentMeta} for a registered component.
111
128
  *
112
- * @param typeOrClass - A component class constructor or a numeric type id.
113
- * @returns The corresponding `ComponentMeta`.
114
- * @throws If no component with that class or type id has been registered.
129
+ * @param typeOrClass - Component class or numeric type id.
130
+ * @returns The corresponding meta record.
131
+ * @throws When no component with that class or type id has been registered.
115
132
  */
116
133
  getComponentMeta(typeOrClass: ComponentClassOrType): ComponentMeta;
117
134
  /**
118
135
  * Resolve a component class or type id to its numeric type id.
119
136
  *
120
- * @param typeOrClass - A component class constructor or a numeric type id.
137
+ * @param typeOrClass - Component class or numeric type id.
121
138
  * @returns The numeric type id.
122
139
  */
123
140
  getComponentType(typeOrClass: ComponentClassOrType): number;
124
141
  /**
125
- * Enter deferred mode. Mutations made until the matching {@link endDefer}
126
- * are queued instead of executing inline.
142
+ * Return the {@link Hook} for a component class.
127
143
  *
128
- * Nested begin/end pairs are allowed; only the outermost `endDefer`
129
- * triggers a drain.
144
+ * Hooks let you react to component lifecycle events (add / remove / set)
145
+ * without building a full {@link System}. The same hook is returned on every
146
+ * call — handlers stack on the underlying meta record.
130
147
  *
131
- */
132
- beginDefer(): void;
133
- /**
134
- * Leave deferred mode. When the depth returns to zero, the world processes
135
- * the command queue (firing hooks and routing enter / exit / update events).
148
+ * ```ts
149
+ * world.hook(Sprite)
150
+ * .onAdd(c => c.initialize(scene))
151
+ * .onRemove(c => c.destroy());
152
+ * ```
136
153
  *
154
+ * @param C - Component class.
155
+ * @returns The hook bound to that component type.
137
156
  */
138
- endDefer(): void;
157
+ hook<T extends typeof Component>(C: T): Hook<InstanceType<T>>;
139
158
  /**
159
+ * Declare a group of mutually exclusive components.
140
160
  *
141
- * @param fn callback to invoke in deferred mode.
161
+ * Adding any component in the group to an entity that already has another
162
+ * member of the group automatically removes the previous member. Members
163
+ * not in the group are unaffected.
164
+ *
165
+ * ```ts
166
+ * world.setExclusiveComponents(Walking, Running, Idle);
167
+ * entity.add(Walking);
168
+ * entity.add(Running); // Walking is removed automatically
169
+ * ```
170
+ *
171
+ * Each call defines one independent group. A component may belong to at
172
+ * most one group at a time; calling {@link setExclusiveComponents} with the
173
+ * same class again overwrites its group. Safe to call before or after
174
+ * {@link start}.
175
+ *
176
+ * @param components - Two or more component classes that cannot coexist.
177
+ * @throws When any class has not been registered.
142
178
  */
143
- defer(fn: () => void): void;
179
+ setExclusiveComponents(...components: (typeof Component)[]): void;
144
180
  /**
145
- * Drain any pending commands queued at the top level (depth 0).
181
+ * Set the starting value of the auto-incrementing entity id counter.
182
+ *
183
+ * Must be called **before** {@link start} (or
184
+ * {@link disableComponentRegistration}). Useful when the world runs
185
+ * alongside a server that owns a different id range — locally created
186
+ * client entities can start at a high offset to avoid collisions with
187
+ * server-assigned ids.
146
188
  *
147
- * Useful between phases or after batch-loading network snapshots, to make
148
- * accumulated mutations visible (fire hooks, route enter/exit/update) before
149
- * the next read or system run.
189
+ * @param min - First id assigned by {@link entity}.
190
+ * @throws When called after registration has been disabled.
150
191
  */
151
- flush(): void;
152
- /** @internal Append a command to the queue. */
153
- _enqueue(cmd: Command): void;
192
+ setEntityIdRange(min: number): void;
154
193
  /**
155
- * @internal Walk the command queue in insertion order, executing each
156
- * command. Callbacks may push more commands, which are processed in the
157
- * same pass via index iteration.
194
+ * Return the entity with id `eid`, creating it if it does not yet exist.
195
+ *
196
+ * Used by networking code to materialise server-assigned entities:
197
+ *
198
+ * ```ts
199
+ * const e = world.getOrCreateEntity(snapshot.eid, (e) => {
200
+ * networkEntities.add(e);
201
+ * });
202
+ * e.add(snapshot.type);
203
+ * ```
204
+ *
205
+ * @param eid - Entity id to look up or create.
206
+ * @param onCreateCallback - Optional callback invoked only when a new
207
+ * entity is created, before it is returned. Use it to initialise
208
+ * bookkeeping (e.g. tracking it in a local set).
209
+ * @returns The existing or newly created entity.
158
210
  */
159
- private processCommandQueue;
211
+ getOrCreateEntity(eid: number, onCreateCallback?: (e: Entity) => void): Entity;
160
212
  /**
161
- * @internal Run a single command's side effects: data-layer mutation, hook
162
- * firing, and routing to all registered queries / systems.
213
+ * Create a new entity with an auto-assigned id.
214
+ *
215
+ * The id counter starts at `0` (or at the value set by
216
+ * {@link setEntityIdRange}) and increments by one for each call. In
217
+ * deferred mode the new entity is queued onto the command queue and is not
218
+ * visible in {@link entities} until the queue drains.
163
219
  */
164
- private executeCommand;
165
- /** @internal Remove an entity from the world's entity map. Called by Entity._destroy. */
166
- _unregisterEntity(entity: Entity): void;
220
+ entity(): Entity;
167
221
  /**
168
- * Register a component class with the world.
222
+ * Look up an existing entity by id.
169
223
  *
170
- * Must be called before any entity can use the component. Registration is
171
- * disabled once {@link start} is called.
172
- *
173
- * **Overloads:**
174
- * - `registerComponent(Class)` — type id auto-assigned from the name map, or
175
- * from a local counter (≥ 256) if the name is not yet mapped.
176
- * - `registerComponent(Class, type)` — explicit numeric type id.
177
- * - `registerComponent(Class, componentName)` — auto-assigned id, custom
178
- * display name (useful when the class name differs from the network name).
179
- * - `registerComponent(Class, type, componentName)` — explicit id + name.
180
- *
181
- * @param ComponentClass - The component class to register.
182
- * @throws If the class has already been registered, or if registration is
183
- * disabled.
224
+ * @param id - Numeric entity id.
225
+ * @returns The entity, or `undefined` when no entity with that id exists.
184
226
  */
185
- registerComponent(ComponentClass: typeof Component): void;
186
- registerComponent(ComponentClass: typeof Component, type: number): void;
187
- registerComponent(ComponentClass: typeof Component, componentName?: string): void;
188
- registerComponent(ComponentClass: typeof Component, type: number, componentName: string): void;
227
+ entity(id: number): Entity | undefined;
189
228
  /**
190
- * Pre-register a component name type id mapping without associating a
191
- * class.
192
- *
193
- * Useful when network messages refer to components by type id and the
194
- * corresponding class may be registered later. Call this before
195
- * {@link registerComponent} to ensure the class picks up the server-assigned
196
- * id rather than a locally generated one.
229
+ * Destroy every entity currently tracked by the world.
197
230
  *
198
- * @param componentName - The string name used in network payloads.
199
- * @param type - The numeric type id assigned by the server.
231
+ * Triggers all `onRemove` hooks and `exit` callbacks. Useful when
232
+ * transitioning between game sessions or resetting to a clean state.
200
233
  */
201
- registerComponentType(componentName: string, type: number): void;
202
- /** @internal Called by the {@link Query} constructor to register itself. */
203
- _addQuery(q: Query): void;
204
- /** @internal Called by {@link Query.destroy} to unregister a query and remove it from all entities. */
205
- _removeQuery(q: Query): void;
234
+ clearAllEntities(): void;
206
235
  /**
207
- * Create a new {@link System}, register it, and return it for configuration.
236
+ * Create, register, and return a new {@link System}, ready for fluent
237
+ * configuration.
208
238
  *
209
239
  * ```ts
210
240
  * world.system("Render")
211
241
  * .phase("update")
212
242
  * .requires(Position, Sprite)
213
243
  * .enter([Sprite], (e, [sprite]) => sprite.initialize(scene))
214
- * .update(Position, (pos) => { ... });
244
+ * .each([Position, Sprite], (e, [pos, sprite]) => sprite.draw(pos.x, pos.y));
215
245
  * ```
216
246
  *
217
- * @param name - A unique display name for the system.
218
- * @returns The new `System` instance.
247
+ * @param name - Unique display name for the system.
248
+ * @returns The new system.
219
249
  */
220
- system(name: string): System<[]>;
250
+ system(name: string): System;
221
251
  /**
222
- * Create a standalone {@link Query}, register it, and return it for
252
+ * Create, register, and return a standalone {@link Query}, ready for fluent
223
253
  * configuration.
224
254
  *
225
255
  * Unlike a {@link System}, a standalone query has no phase and no per-tick
226
- * callbacks — it is a reactive, always-updated entity set that can be read
227
- * at any time after {@link start}. Standalone queries can also be created
228
- * after {@link start}; existing matched entities are backfilled immediately.
256
+ * callbacks — it is a reactive entity set that can be read at any time. It
257
+ * can also be created **after** {@link start}; existing matched entities
258
+ * are backfilled immediately.
229
259
  *
230
260
  * ```ts
231
261
  * const enemies = world.query("Enemies")
@@ -236,135 +266,117 @@ export declare class World {
236
266
  * // enemies.entities is kept up-to-date automatically
237
267
  * ```
238
268
  *
239
- * @param name - A unique display name for the query.
240
- * @returns The new `Query` instance.
269
+ * @param name - Unique display name for the query.
270
+ * @returns The new query.
241
271
  */
242
- query(name: string): Query<[]>;
272
+ query(name: string): Query;
243
273
  /**
244
274
  * Create a non-reactive {@link Filter} that matches entities satisfying `q`.
245
275
  *
246
276
  * Unlike {@link query}, the returned filter holds no tracked entity set and
247
- * registers nothing with the world. Each call to {@link Filter.forEach} walks
248
- * all current world entities and invokes the callback for matching ones.
277
+ * registers nothing on the world. Each call to {@link Filter.forEach} walks
278
+ * all current world entities and invokes the callback on the matches.
249
279
  *
250
- * Component classes guaranteed present on every matched entity are inferred
251
- * automatically from the DSL where possible (plain arrays, `HAS`, `HAS_ONLY`,
252
- * and `AND` of those forms). For cases the type extractor cannot see through
253
- * (`OR`, `NOT`, `PARENT`, custom `EntityTestFunc`), pass a `_guaranteed`
280
+ * The component classes guaranteed present on every matched entity are
281
+ * inferred from the DSL where possible (plain arrays, `HAS`, `HAS_ONLY`,
282
+ * and `AND` of those forms). For shapes the inferer cannot see through
283
+ * (`OR`, `NOT`, `PARENT`, custom `EntityTestFunc`) supply a `_guaranteed`
254
284
  * tuple as a type-level override:
255
285
  *
256
286
  * ```ts
257
- * // Auto-deduced pos and vel are non-nullable
287
+ * // Auto-deduced: pos and vel are non-nullable.
258
288
  * world.filter([Position, Velocity])
259
289
  * .forEach([Position, Velocity], (e, [pos, vel]) => { ... });
260
290
  *
261
- * // Manual override for an opaque query
291
+ * // Manual override for an opaque query.
262
292
  * world.filter(myTestFunc, [Position])
263
293
  * .forEach([Position], (e, [pos]) => pos.x);
264
294
  * ```
265
295
  *
266
- * @param q - A {@link QueryDSL} expression.
296
+ * @param q - Query expression.
267
297
  * @param _guaranteed - Optional type hint declaring which components are
268
298
  * guaranteed present (not validated at runtime).
269
299
  */
270
300
  filter<Q extends QueryDSL>(q: Q): Filter<ExtractRequired<Q>>;
271
301
  filter<T extends (typeof Component)[]>(q: QueryDSL, _guaranteed: readonly [...T]): Filter<T>;
302
+ /**
303
+ * Add a named phase to the update pipeline.
304
+ *
305
+ * Phases are executed in insertion order when {@link runPhase} or
306
+ * {@link progress} is called. Systems join a phase via {@link System.phase}.
307
+ *
308
+ * ```ts
309
+ * const preUpdate = world.addPhase("preupdate");
310
+ * const update = world.addPhase("update");
311
+ * const send = world.addPhase("send");
312
+ * ```
313
+ *
314
+ * @param name - Unique phase name. Systems can reference it by this string.
315
+ * @returns The new phase.
316
+ */
317
+ addPhase(name: string): IPhase;
272
318
  /**
273
319
  * Prevent any further calls to {@link registerComponent}.
274
320
  *
275
- * Called automatically by {@link start}. Can be called early if you want to
276
- * lock component registration before systems are fully configured.
321
+ * Called automatically by {@link start}. Call directly if you want to lock
322
+ * registration before the rest of the systems are wired up.
277
323
  */
278
324
  disableComponentRegistration(): void;
279
325
  /**
280
326
  * Freeze component registration and prepare the world for running.
281
327
  *
282
- * Distributes all systems registered so far into their pipeline phases
283
- * (defaulting to `"update"`) and logs the phase → system order to the
284
- * console. Systems and queries can still be created after this call —
285
- * standalone queries will immediately backfill existing matched entities.
328
+ * Distributes every system registered so far into its phase (defaulting to
329
+ * `"update"`) and logs the phase → system order to the console. Systems
330
+ * and queries can still be created after this call — standalone queries
331
+ * backfill existing matched entities immediately.
286
332
  *
287
- * Call this once before the first {@link runPhase} call.
333
+ * Call once before the first {@link runPhase} / {@link progress}.
288
334
  */
289
335
  start(): void;
290
- private reindexSystems;
291
336
  /**
292
- * Return the {@link Hook} for a component class.
337
+ * Open a new frame and evaluate every registered tick source once.
293
338
  *
294
- * Hooks let you react to component lifecycle events (add / remove / set)
295
- * without building a full {@link System}. The hook is backed by the
296
- * component's {@link ComponentMeta} and the same object is returned on every
297
- * call.
298
- *
299
- * ```ts
300
- * world.hook(Sprite)
301
- * .onAdd(c => c.initialize(scene))
302
- * .onRemove(c => c.destroy());
303
- * ```
339
+ * Call this before one or more {@link runPhase} calls when manually driving
340
+ * phases. {@link progress} wraps this automatically for the full pipeline.
304
341
  *
305
- * @param C - The component class.
306
- * @returns The `Hook` for that component type.
342
+ * @param delta - Milliseconds elapsed since the previous frame.
343
+ * @throws When a frame is already open.
307
344
  */
308
- hook<T extends typeof Component>(C: T): Hook<InstanceType<T>>;
345
+ beginFrame(delta: number): void;
309
346
  /**
310
- * Add a named phase to the update pipeline and return it.
311
- *
312
- * Phases are executed in insertion order when you call {@link runPhase} for
313
- * each one. Systems are assigned to a phase via {@link System.phase}.
314
- *
315
- * ```ts
316
- * const preUpdate = world.addPhase("preupdate");
317
- * const update = world.addPhase("update");
318
- * const send = world.addPhase("send");
319
- * ```
347
+ * Close the current frame.
320
348
  *
321
- * @param name - Unique phase name. Systems can reference it by this string.
322
- * @returns The new {@link IPhase}.
349
+ * @throws When no frame is currently open.
323
350
  */
324
- addPhase(name: string): IPhase;
351
+ endFrame(): void;
325
352
  /**
326
- * Execute all systems in the given phase for one tick.
353
+ * Execute every system in `phase` within the current frame.
327
354
  *
328
- * Pending top-level mutations are drained at the start of the phase so the
329
- * first system observes a consistent world. Each system's body runs in a
330
- * deferred scope; mutations made by callbacks are appended to the world
331
- * queue and processed by the world after the system returns, before the
332
- * next system runs.
355
+ * Pending top-level mutations are drained before the first system runs so
356
+ * each system observes a consistent world. Each system body executes in a
357
+ * deferred scope; mutations made by callbacks land in the world queue and
358
+ * are processed before the next system runs.
333
359
  *
334
- * @param phase - The {@link IPhase} to run (returned by {@link addPhase}).
360
+ * `runPhase` is safe to call re-entrantly from a system body: it reuses the
361
+ * frame opened by {@link beginFrame} and does not advance `_frameCounter` or
362
+ * re-evaluate tick sources.
363
+ *
364
+ * @param phase - Phase reference returned from {@link addPhase}.
335
365
  * @param now - Absolute timestamp in milliseconds (e.g. `Date.now()`).
336
366
  * @param delta - Milliseconds elapsed since the previous tick.
367
+ * @throws When called outside an open frame.
337
368
  */
338
369
  runPhase(phase: IPhase, now: number, delta: number): void;
339
370
  /**
340
- * Run every phase in the pipeline in insertion order (the order phases were
341
- * registered via {@link addPhase}). Equivalent to calling
342
- * {@link runPhase} for each phase manually.
371
+ * Run every phase in the pipeline in registration order.
372
+ *
373
+ * Equivalent to `beginFrame(delta)`, calling {@link runPhase} for each
374
+ * phase, then {@link endFrame}. All registered tick sources are evaluated
375
+ * once up front for the whole frame, and the frame is closed in a `finally`
376
+ * block if a system throws.
343
377
  *
344
378
  * @param now - Absolute timestamp in milliseconds (e.g. `Date.now()`).
345
379
  * @param delta - Milliseconds elapsed since the previous tick.
346
380
  */
347
381
  progress(now: number, delta: number): void;
348
- /**
349
- * Declare a group of mutually exclusive components.
350
- *
351
- * After this call, adding any component in the group to an entity that
352
- * already has another component from the same group will remove the other component
353
- *
354
- * ```ts
355
- * world.setExclusiveComponents(Walking, Running, Idle);
356
- * // entity.add(Running) throws if entity already has Walking or Idle
357
- * ```
358
- *
359
- * @param components - Two or more component classes that cannot coexist.
360
- * @throws If any class has not been registered.
361
- */
362
- setExclusiveComponents(...components: (typeof Component)[]): void;
363
- /**
364
- * Destroy every entity currently tracked by the world.
365
- *
366
- * Triggers all `onRemove` hooks and `exit` callbacks. Useful when
367
- * transitioning between game sessions or resetting to a clean state.
368
- */
369
- clearAllEntities(): void;
370
382
  }