@vworlds/vecs 1.0.9 → 1.0.11

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 (46) hide show
  1. package/.husky/pre-commit +1 -0
  2. package/README.md +218 -229
  3. package/dist/command.d.ts +1 -0
  4. package/dist/command.js +2 -0
  5. package/dist/command.js.map +1 -0
  6. package/dist/component.d.ts +51 -59
  7. package/dist/component.js +31 -25
  8. package/dist/component.js.map +1 -1
  9. package/dist/dsl.d.ts +34 -26
  10. package/dist/dsl.js +46 -20
  11. package/dist/dsl.js.map +1 -1
  12. package/dist/entity.d.ts +110 -127
  13. package/dist/entity.js +323 -164
  14. package/dist/entity.js.map +1 -1
  15. package/dist/filter.d.ts +31 -23
  16. package/dist/filter.js +41 -32
  17. package/dist/filter.js.map +1 -1
  18. package/dist/index.d.ts +1 -1
  19. package/dist/package.json +3 -1
  20. package/dist/phase.d.ts +5 -28
  21. package/dist/phase.js +11 -10
  22. package/dist/phase.js.map +1 -1
  23. package/dist/query.d.ts +128 -94
  24. package/dist/query.js +254 -145
  25. package/dist/query.js.map +1 -1
  26. package/dist/system.d.ts +64 -128
  27. package/dist/system.js +156 -149
  28. package/dist/system.js.map +1 -1
  29. package/dist/util/array_map.d.ts +4 -55
  30. package/dist/util/array_map.js +35 -37
  31. package/dist/util/array_map.js.map +1 -1
  32. package/dist/util/bitset.d.ts +40 -50
  33. package/dist/util/bitset.js +76 -62
  34. package/dist/util/bitset.js.map +1 -1
  35. package/dist/util/events.d.ts +14 -18
  36. package/dist/util/events.js +24 -3
  37. package/dist/util/events.js.map +1 -1
  38. package/dist/util/ordered_set.d.ts +1 -17
  39. package/dist/util/ordered_set.js +74 -25
  40. package/dist/util/ordered_set.js.map +1 -1
  41. package/dist/world.d.ts +222 -201
  42. package/dist/world.js +394 -323
  43. package/dist/world.js.map +1 -1
  44. package/eslint-rules/internal-underscore.js +60 -0
  45. package/eslint.config.js +5 -0
  46. package/package.json +3 -1
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  A TypeScript Entity Component System (ECS) for real-time games and simulations.
4
4
 
5
- `vecs` lets you model game state as **entities** (integer IDs) with **components** (typed data bags) attached to them. **Systems** declare which component combinations they care about and receive automatic callbacks when entities enter or leave their query, when component data changes, and on every tick. A **World** ties it all together and drives the update loop.
5
+ `vecs` lets you model game state as **entities** (numeric ids) with **components** (typed data bags) attached to them. **Systems** declare which component combinations they care about and receive automatic callbacks when entities enter or leave their query, when component data changes, and on every tick. A **World** ties it all together and drives the update loop.
6
6
 
7
7
  ## Install
8
8
 
@@ -14,18 +14,20 @@ yarn add @vworlds/vecs
14
14
 
15
15
  | Concept | What it is |
16
16
  | ------------------------ | ------------------------------------------------------------------------------- |
17
- | **World** | Central container. Owns all entities, runs all systems and queries. |
17
+ | **World** | Central container. Owns every entity, query, system, and pipeline phase. |
18
18
  | **Component** | A plain data class. Extend `Component` and attach instances to entities. |
19
- | **Entity** | An integer id with a set of components. Create via the world. |
20
- | **Query** | A reactive, always-updated set of entities that match a predicate. |
21
- | **System** | A `Query` with per-tick runtime logic (phases, `update`, `each`, `run`). |
19
+ | **Entity** | A numeric id with a set of components. Created via the world. |
20
+ | **Query** | A reactive, always-up-to-date set of entities matching a predicate. |
21
+ | **System** | A `Query` with phase placement and per-tick logic (`update`, `each`, `run`). |
22
22
  | **Filter** | A non-reactive, one-shot scan: walks all world entities on each `forEach` call. |
23
- | **Exclusive components** | A group of components where at most one may be present on any entity at a time. |
23
+ | **Hook** | Lightweight `onAdd` / `onRemove` / `onSet` callbacks per component class. |
24
+ | **Phase** | Named ordered bucket of systems within the update pipeline. |
25
+ | **Exclusive components** | A group of components where at most one may exist on any entity at a time. |
24
26
 
25
27
  ### Lifecycle in brief
26
28
 
27
29
  ```
28
- registerComponent() × N → system() / query() × N → start() → progress() every frame
30
+ registerComponent() × N → addPhase() / system() / query() × N → start() → progress() every frame
29
31
  ```
30
32
 
31
33
  After `start()`, component registration is disabled. Systems and queries can still be created — standalone queries backfill existing matched entities immediately.
@@ -34,8 +36,6 @@ After `start()`, component registration is disabled. Systems and queries can sti
34
36
 
35
37
  ## Example
36
38
 
37
- The example below defines three components, two systems, a phase, and a hook, then runs a simple "move and despawn" loop.
38
-
39
39
  ```ts
40
40
  import { World, Component, IPhase } from "@vworlds/vecs";
41
41
 
@@ -70,22 +70,15 @@ const cleanup: IPhase = world.addPhase("cleanup");
70
70
 
71
71
  // ─── Systems ───────────────────────────────────────────────────────────────
72
72
 
73
- // MoveSystem: runs every tick for entities that have both Position and Velocity.
73
+ // MoveSystem: integrates Velocity into Position every tick.
74
74
  world
75
75
  .system("Move")
76
76
  .phase(update)
77
77
  .requires(Position, Velocity)
78
- .enter([Position, Velocity], (e, [pos, vel]) => {
79
- console.log(`entity ${e.eid} entered Move with pos=(${pos.x},${pos.y})`);
80
- })
81
- .update(Velocity, [Position], (vel, [pos]) => {
82
- // Called whenever vel.modified() is queued.
78
+ .each([Position, Velocity], (e, [pos, vel]) => {
83
79
  pos.x += vel.vx;
84
80
  pos.y += vel.vy;
85
- pos.modified(); // propagate position change to other systems
86
- })
87
- .exit((e) => {
88
- console.log(`entity ${e.eid} left Move`);
81
+ pos.modified(); // signal that Position changed so other systems react
89
82
  });
90
83
 
91
84
  // HealthSystem: despawns entities whose HP drops to zero.
@@ -101,8 +94,6 @@ world
101
94
 
102
95
  // ─── Hooks ─────────────────────────────────────────────────────────────────
103
96
 
104
- // Hooks are a lightweight alternative to systems for side effects on a single
105
- // component type — no per-entity query, just callbacks on add/remove/set.
106
97
  world
107
98
  .hook(Health)
108
99
  .onAdd((h) => console.log(`entity ${h.entity.eid} spawned with hp=${h.hp}`))
@@ -110,16 +101,11 @@ world
110
101
 
111
102
  // ─── Start ─────────────────────────────────────────────────────────────────
112
103
 
113
- world.start(); // freeze registration, sort systems into phases
114
-
115
- // ─── Create entities ───────────────────────────────────────────────────────
104
+ world.start(); // freeze registration, distribute systems into phases
116
105
 
117
- const bullet = world.entity();
118
- bullet.set(Position, { x: 0, y: 0 });
106
+ // ─── Spawn entities ────────────────────────────────────────────────────────
119
107
 
120
- const vel = bullet.set(Velocity, { vx: 5, vy: 0 }).get(Velocity)!;
121
-
122
- const hp = bullet.set(Health, { hp: 3 }).get(Health)!;
108
+ world.entity().set(Position, { x: 0, y: 0 }).set(Velocity, { vx: 5, vy: 0 }).set(Health, { hp: 3 });
123
109
 
124
110
  // ─── Game loop ─────────────────────────────────────────────────────────────
125
111
 
@@ -132,11 +118,25 @@ for (let tick = 0; tick < 5; tick++) {
132
118
 
133
119
  ---
134
120
 
121
+ ## Deferred mode
122
+
123
+ Inside a system body, a `Query.forEach`, or any `world.defer(...)` block, the world is in **deferred mode**: entity mutations (`add` / `set` / `remove` / `destroy` / `setParent` / `modified`) are queued instead of applied inline. The queue drains on the boundary that opened the deferred scope.
124
+
125
+ Concretely, while deferred:
126
+
127
+ - `entity.get(C)` returns `undefined` after `entity.add(C)` (no instance has been created yet).
128
+ - `entity.get(C)` returns the **previous** value after `entity.set(C, props)`.
129
+ - `entity.get(C)` still returns the component after `entity.remove(C)`.
130
+
131
+ Outside any deferred scope (top-level user code) the same calls execute inline and effects are visible immediately. `world.flush()` drains any pending top-level commands; `world.defer(fn)` is sugar for `beginDefer / fn / endDefer`.
132
+
133
+ ---
134
+
135
135
  ## API Reference
136
136
 
137
- ### World
137
+ ### `World`
138
138
 
139
- The world owns everything. Create one per game session.
139
+ Create one per game session.
140
140
 
141
141
  ```ts
142
142
  const world = new World();
@@ -145,24 +145,22 @@ const world = new World();
145
145
  #### Component registration
146
146
 
147
147
  ```ts
148
- // Auto-assigned type id (starts at 256 for "local" components):
148
+ // Auto-assigned type id ( 256 for "local" components):
149
149
  world.registerComponent(Position);
150
150
 
151
- // Explicit numeric type id (required when the id comes from a server):
151
+ // Explicit numeric type id (e.g. server-assigned):
152
152
  world.registerComponent(Position, 1);
153
153
 
154
- // With a display name different from the class name:
154
+ // Explicit display name (e.g. when the class name differs from the network name):
155
155
  world.registerComponent(Position, "pos");
156
156
 
157
157
  // Pre-register a name → id mapping before the class is available:
158
158
  world.registerComponentType("Position", 1);
159
159
  ```
160
160
 
161
- After `world.start()` any further call to `registerComponent` throws.
162
-
163
- #### Exclusive components
161
+ After `world.start()` (or `world.disableComponentRegistration()`) any further call to `registerComponent` throws.
164
162
 
165
- Declare a group of components that cannot coexist on the same entity. Adding a member of the group automatically removes any other member that was already present.
163
+ #### Exclusive component groups
166
164
 
167
165
  ```ts
168
166
  world.setExclusiveComponents(Walking, Running, Idle);
@@ -170,12 +168,9 @@ world.setExclusiveComponents(Walking, Running, Idle);
170
168
  const e = world.entity();
171
169
  e.add(Walking);
172
170
  e.add(Running); // Walking is automatically removed first
173
- // e.get(Walking) === undefined, e.get(Running) is defined
174
171
  ```
175
172
 
176
- Each call to `setExclusiveComponents` defines one independent group. Components not in the group are unaffected. A component may belong to at most one exclusivity group (calling `setExclusiveComponents` a second time with the same class overwrites its group).
177
-
178
- `setExclusiveComponents` may be called before or after `world.start()`.
173
+ Each call defines one independent group. A component may belong to at most one group; calling `setExclusiveComponents` again with the same class overwrites its group. Safe to call before or after `world.start()`.
179
174
 
180
175
  #### Entity management
181
176
 
@@ -184,58 +179,81 @@ Each call to `setExclusiveComponents` defines one independent group. Components
184
179
  const e = world.entity();
185
180
 
186
181
  // Look up by id (returns undefined if not found):
187
- const e = world.entity(42);
182
+ const found = world.entity(42);
188
183
 
189
184
  // Server-assigned id; creates the entity if it doesn't exist yet:
190
- const e = world.getOrCreateEntity(serverId, (newEntity) => {
185
+ const net = world.getOrCreateEntity(serverId, (newEntity) => {
191
186
  tracked.add(newEntity);
192
187
  });
193
188
 
189
+ // Reserve a high id range for locally created entities so they don't collide
190
+ // with server-assigned ids (call before world.start()):
191
+ world.setEntityIdRange(0x10000);
192
+
194
193
  // Destroy everything (e.g. on level reset):
195
194
  world.clearAllEntities();
195
+ ```
196
196
 
197
- // Reserve a high id range for locally-created entities so they don't
198
- // collide with server-assigned ids (call before world.start()):
199
- world.setEntityIdRange(0x10000);
197
+ #### Hooks
198
+
199
+ ```ts
200
+ world
201
+ .hook(Sprite)
202
+ .onAdd((sprite) => sprite.initialize(scene))
203
+ .onRemove((sprite) => sprite.destroy())
204
+ .onSet((sprite) => sprite.syncToScene());
200
205
  ```
201
206
 
207
+ `onAdd` fires when the component is first attached. `onRemove` fires when it is removed (or the entity is destroyed). `onSet` fires whenever `component.modified()` (or `entity.modified(c)`) is called, and when `entity.set(C, props)` is applied to an entity that already has the component.
208
+
209
+ #### Phases
210
+
211
+ ```ts
212
+ const preUpdate = world.addPhase("preupdate");
213
+ const update = world.addPhase("update");
214
+ const send = world.addPhase("send");
215
+
216
+ // Drive every phase in registration order:
217
+ world.progress(now, delta);
218
+
219
+ // ...or run individual phases manually:
220
+ world.runPhase(preUpdate, now, delta);
221
+ world.runPhase(update, now, delta);
222
+ world.runPhase(send, now, delta);
223
+ ```
224
+
225
+ Systems with no explicit phase are placed in the built-in `"update"` phase.
226
+
202
227
  #### Systems
203
228
 
204
229
  ```ts
205
- // Create, configure, and register a system in one chain:
206
- world.system("MySystem")
230
+ world
231
+ .system("MySystem")
207
232
  .phase("update")
208
233
  .requires(A, B)
209
234
  .enter(...)
210
235
  .update(...)
236
+ .each(...)
211
237
  .exit(...);
212
-
213
- world.start(); // distributes systems to phases, freezes component registration
214
238
  ```
215
239
 
216
240
  #### Queries
217
241
 
218
- A standalone `Query` is a reactive entity set without a phase or per-tick callbacks. Use it when you need the matched set kept up-to-date automatically — for example to enumerate scene nodes or find the nearest enemy.
219
-
220
242
  ```ts
221
243
  const enemies = world
222
244
  .query("Enemies")
223
245
  .requires(Enemy, Health)
224
- .enter((e) => console.log("enemy spawned", e.eid))
225
- .exit((e) => console.log("enemy died", e.eid));
246
+ .enter((e) => console.log("enemy spawned", e.eid));
226
247
 
227
248
  world.start();
228
- // enemies.entities is kept up-to-date automatically
249
+ // enemies.entities is kept up-to-date automatically.
229
250
 
230
- // Can also be created after start(); existing matched entities are backfilled:
231
- const lateQuery = world.query("Walls").requires(Wall);
232
- // lateQuery.entities immediately contains all current Wall entities
251
+ // Standalone queries can also be created after start(); existing matched
252
+ // entities are backfilled immediately.
233
253
  ```
234
254
 
235
255
  #### Filters
236
256
 
237
- A `Filter` is a non-reactive, one-shot scan. It holds no tracked entity set — each `forEach` call walks all world entities at that moment. Use it for ad-hoc lookups that don't need to stay live.
238
-
239
257
  ```ts
240
258
  // Entity only:
241
259
  world.filter([Position]).forEach((e) => console.log(e.eid));
@@ -249,55 +267,30 @@ world.filter([Position, Velocity]).forEach([Position, Velocity], (e, [pos, vel])
249
267
  world
250
268
  .filter({ AND: [{ HAS: Position }, { HAS: Velocity }] })
251
269
  .forEach([Position, Velocity], (e, [pos, vel]) => {
252
- pos.x += vel.vx; // pos and vel are non-null — deduced from AND of HAS
270
+ pos.x += vel.vx;
253
271
  });
254
272
 
255
- // Manual type hint for queries the extractor can't see through:
273
+ // Manual hint for queries the type extractor can't see through:
256
274
  world.filter({ OR: [Position, Velocity] }, [Position]).forEach([Position], (e, [pos]) => pos.x);
257
275
  ```
258
276
 
259
- Unlike `Query`, a `Filter` requires no name, no `world.start()`, and no `destroy()` — create it anywhere and discard it freely.
277
+ A `Filter` requires no name, no `world.start()`, and no `destroy()` — create it anywhere and discard freely.
260
278
 
261
- #### Phases
279
+ #### Pipeline control
262
280
 
263
281
  ```ts
264
- // Declare phases in the order they should run each frame:
265
- const preUpdate = world.addPhase("preupdate");
266
- const update = world.addPhase("update");
267
- const send = world.addPhase("send");
268
-
269
- // Each frame, run all phases in registration order:
270
- world.progress(Date.now(), deltaMs);
271
-
272
- // Or drive individual phases manually:
273
- world.runPhase(preUpdate, Date.now(), deltaMs);
274
- world.runPhase(update, Date.now(), deltaMs);
275
- world.runPhase(send, Date.now(), deltaMs);
276
- ```
277
-
278
- Systems with no explicit phase go into a built-in `"update"` phase.
282
+ world.start(); // freeze registration, distribute systems
283
+ world.disableComponentRegistration(); // freeze registration without sorting
279
284
 
280
- #### Hooks
281
-
282
- A hook is a shorthand for reacting to a single component's lifecycle without writing a full system:
283
-
284
- ```ts
285
- world
286
- .hook(Sprite)
287
- .onAdd((sprite) => sprite.initialize(scene))
288
- .onRemove((sprite) => sprite.destroy())
289
- .onSet((sprite) => sprite.syncToScene());
285
+ world.flush(); // drain queued top-level mutations
286
+ world.defer(() => { ... }); // run a block in deferred mode
287
+ world.beginDefer(); // pair with endDefer() for finer scoping
288
+ world.endDefer();
290
289
  ```
291
290
 
292
- `onSet` fires whenever `component.modified()` is called.
293
- `onAdd` fires when the component is first attached to an entity.
294
- `onRemove` fires when it is removed or the entity is destroyed.
295
-
296
291
  ---
297
292
 
298
- ### Component
299
-
300
- Extend `Component` to define your data:
293
+ ### `Component`
301
294
 
302
295
  ```ts
303
296
  class Position extends Component {
@@ -312,90 +305,80 @@ const pos = entity.get(Position)!;
312
305
  pos.x = 100;
313
306
  pos.modified(); // tell the world this component changed
314
307
 
315
- // Alternatively:
308
+ // Equivalent — set assigns props and fires onSet automatically:
316
309
  entity.set(Position, { x: 100 });
317
310
  ```
318
311
 
319
- Every component instance exposes:
320
-
321
312
  | Property / Method | Description |
322
313
  | ----------------- | --------------------------------------------------------------------- |
323
314
  | `entity` | The `Entity` this component belongs to. |
324
- | `meta` | `ComponentMeta` — holds the type id, name, and bitset pointer. |
315
+ | `meta` | `ComponentMeta` — type id, display name, and bit-pointer. |
325
316
  | `type` | Numeric type id (shorthand for `meta.type`). |
317
+ | `bitPtr` | `BitPtr` (shorthand for `meta.bitPtr`). |
326
318
  | `modified()` | Queue an `onSet` / `update` notification. Call after mutating fields. |
319
+ | `toString()` | Returns the registered component name. |
327
320
 
328
321
  ---
329
322
 
330
- ### Entity
331
-
332
- ```ts
333
- const e = world.entity();
334
- ```
335
-
336
- | Property / Method | Description |
337
- | ---------------------- | ------------------------------------------------------------------------------------------------------------- |
338
- | `eid` | Unique numeric entity id. |
339
- | `world` | The `World` that owns this entity. |
340
- | `add(Class)` | Attach a component and return the entity for chaining. Idempotent. |
341
- | `set(Class, props)` | Like `add`, but also assigns the given partial properties onto the instance. Returns the entity for chaining. |
342
- | `modified(component)` | Queue an `onSet` / `update` notification for the component. Returns the entity for chaining. |
343
- | `get(Class)` | Return the component instance, or `undefined` if not present. |
344
- | `remove(Class)` | Detach a component (triggers `onRemove` hooks and `exit` callbacks). |
345
- | `destroy()` | Remove all components and unregister the entity. Recurses to children. |
346
- | `empty` | `true` when no components are attached. |
347
- | `forEachComponent(cb)` | Iterate over all attached components. |
348
- | `parent` | Parent entity in the scene hierarchy, or `undefined`. |
349
- | `children` | `Set<Entity>` of direct children (lazy, created on first access). |
350
- | `events` | Typed event emitter. Currently emits `"destroy"` before teardown. |
351
- | `properties` | `Map<string, any>` free-form bag for module-level bookkeeping. |
352
-
353
- `entity.modified(c)` is equivalent to `c.modified()` but returns the entity, making it usable in a method chain:
323
+ ### `Entity`
324
+
325
+ Created via `world.entity()` (auto-assigned id) or `world.getOrCreateEntity(id, ...)` (caller-supplied id).
326
+
327
+ | Property / Method | Description |
328
+ | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
329
+ | `eid` | Unique numeric entity id. |
330
+ | `world` | The `World` that owns this entity. |
331
+ | `componentBitmask` | `Bitset` of component type ids attached to this entity. Used by archetype matching. |
332
+ | `properties` | `Map<string, any>` free-form bag for module-level bookkeeping. |
333
+ | `add(Class)` | Attach a component (idempotent). Returns the entity for chaining. |
334
+ | `set(Class, props)` | Attach a component and assign `props`; fires `onSet`. Returns the entity for chaining. |
335
+ | `modified(component)` | Queue an `onSet` / `update` notification. Returns the entity for chaining. |
336
+ | `get(Class)` | Return the component instance, or `undefined`. |
337
+ | `remove(Class)` | Detach a component (fires `onRemove` and `exit`). |
338
+ | `destroy()` | Remove all components, unregister from the world, recurse into children. |
339
+ | `components` | `ReadonlyArrayMap<Component>` read-only view of attached components keyed by type id. Supports `forEach`, `get`, `has`, and `size`. |
340
+ | `empty` | `true` when no components are attached. |
341
+ | `parent` | Parent entity, or `undefined` for a root entity. |
342
+ | `children` | `ReadonlySet<Entity>` of direct children (lazy). |
343
+ | `setParent(p)` | Reparent the entity. `undefined` makes it a root entity. Throws on cycles. |
344
+ | `events` | Typed event emitter. Currently emits `"destroy"` just before teardown. |
345
+ | `toString()` | Returns `"EntityN"`. |
346
+
347
+ `entity.modified(c)` is equivalent to `c.modified()` but returns the entity so it can chain:
354
348
 
355
349
  ```ts
356
- // Mutate fields then signal the change inline:
357
350
  const vel = entity.get(Velocity)!;
358
351
  vel.vx += accel;
359
- entity.modified(vel); // same effect as vel.modified(), returns entity
360
-
361
- // Or in a chain — add without initial notification, then notify later:
362
- entity.add(Position, false).modified(entity.get(Position)!);
352
+ entity.modified(vel); // chainable
363
353
  ```
364
354
 
365
- #### Parentchild hierarchy
355
+ #### Parent / child hierarchy
366
356
 
367
357
  ```ts
368
- child.parent = parent;
369
- parent.children.add(child);
358
+ child.setParent(parent);
359
+ parent.children.has(child); // true
370
360
 
371
361
  // Destroying a parent recursively destroys all children:
372
362
  parent.destroy();
373
363
  ```
374
364
 
375
- Archetype queries that use `{ PARENT: ... }` are automatically re-evaluated when a parent's component set changes.
365
+ `setParent` throws if the new parent is a descendant of the entity. Archetype queries that use `{ PARENT: ... }` are re-evaluated automatically when a parent's component set changes.
376
366
 
377
367
  ---
378
368
 
379
- ### System
369
+ ### `System`
380
370
 
381
- Systems are created via `world.system(name)` and configured through a fluent builder API. All methods return `this` for chaining.
371
+ Systems are created via `world.system(name)` and configured through a fluent builder. Every method returns `this` for chaining. `System` extends `Query`, so the membership / enter / exit / update / sort APIs are shared.
382
372
 
383
373
  #### `.requires(...components)` and `.query(q)`
384
374
 
385
- Declare which entities the system should track:
375
+ Declare which entities the system tracks.
386
376
 
387
377
  ```ts
388
- // Entities that have both Position and Velocity:
389
- .requires(Position, Velocity)
390
-
391
- // Equivalent explicit query:
392
- .query({ HAS: [Position, Velocity] })
393
-
394
- // Entities that have a parent with Player AND Container:
395
- .query({ PARENT: { AND: [Player, Container] } })
396
-
397
- // Compound queries:
398
- .query({ AND: [Position, { OR: [Sprite, Container] }] })
378
+ .requires(Position, Velocity) // shorthand for HAS
379
+ .query({ HAS: [Position, Velocity] }) // explicit
380
+ .query({ PARENT: { AND: [Player, Container] } }) // parent-aware
381
+ .query({ AND: [Position, { OR: [Sprite, Container] }] }) // compound
399
382
  .query({ NOT: Invisible })
400
383
  ```
401
384
 
@@ -409,130 +392,112 @@ Declare which entities the system should track:
409
392
  | `{ OR: [q1, q2] }` | Either sub-query matches |
410
393
  | `{ NOT: q }` | Sub-query must not match |
411
394
  | `{ PARENT: q }` | Entity's parent matches q |
412
- | An array `[A, B]` | Shorthand for `HAS: [A, B]` |
395
+ | An array `[A, B]` | Shorthand for `{ HAS: [A, B] }` |
396
+ | A single class / id | Shorthand for `{ HAS: [C] }` |
397
+ | A predicate function | Custom membership logic |
413
398
 
414
- **Type inference:** `requires()` records the listed classes as a type parameter on the system. Callbacks in `.sort()`, `.each()`, and `.update()` inject then treat those components as non-nullable — no `!` needed. For complex `query()` expressions the type system cannot introspect, pass a second argument as an explicit hint:
399
+ **Type inference.** `requires()` records the listed classes as a type parameter `R` on the system. Callbacks in `.sort()`, `.each()`, and `.update()` injection treat those components as non-nullable — no `!` needed. For complex `query()` expressions the type system can't introspect, supply a `_guaranteed` second argument:
415
400
 
416
401
  ```ts
417
402
  .query({ AND: [{ HAS: Position }, { HAS: Velocity }] }, [Position, Velocity])
418
403
  .each([Position, Velocity], (e, [pos, vel]) => {
419
404
  pos.x += vel.vx; // pos and vel are non-null
420
- })
405
+ });
421
406
  ```
422
407
 
423
408
  #### `.phase(p)`
424
409
 
425
- Assign the system to a named phase or an `IPhase` reference. Systems without a phase run in `"update"`.
410
+ Assign the system to a phase by name or `IPhase` reference. Default phase is `"update"`.
426
411
 
427
412
  ```ts
428
- .phase("preupdate") // by name
429
- .phase(myPhase) // by IPhase reference
413
+ .phase("preupdate")
414
+ .phase(myPhase)
430
415
  ```
431
416
 
432
417
  #### `.enter(callback)` / `.enter(inject, callback)`
433
418
 
434
- Called once when an entity first matches the system's query.
419
+ Fires once when an entity first matches the system.
435
420
 
436
421
  ```ts
437
- // No injection:
438
- .enter((e) => { console.log("entity joined", e.eid); })
439
-
440
- // With injection — component instances resolved from the entity:
422
+ .enter((e) => { ... })
441
423
  .enter([Position, Sprite], (e, [pos, sprite]) => {
442
424
  sprite.setPosition(pos.x, pos.y);
443
425
  })
444
426
 
445
- // Resolve from parent:
427
+ // Resolve from the entity's parent:
446
428
  .enter([{ parent: Container }], (e, [container]) => {
447
429
  container.add(e.get(Sprite)!.gameObject);
448
- })
430
+ });
449
431
  ```
450
432
 
451
433
  #### `.exit(callback)` / `.exit(inject, callback)`
452
434
 
453
- Called when an entity leaves the system (component removed or entity destroyed). Components removed in the same frame are still accessible in exit callbacks.
435
+ Fires when an entity leaves the system (component removed or entity destroyed). Components removed in the same frame are still resolvable in `inject`.
454
436
 
455
437
  ```ts
456
- .exit([Sprite], (e, [sprite]) => {
457
- sprite.destroy();
458
- })
438
+ .exit([Sprite], (e, [sprite]) => sprite.destroy());
459
439
  ```
460
440
 
461
441
  #### `.update(ComponentClass, callback)` / `.update(ComponentClass, inject, callback)`
462
442
 
463
- Called when `component.modified()` is queued on a watched component of a tracked entity.
443
+ Fires when `component.modified()` is called for the watched component on a tracked entity.
464
444
 
465
445
  ```ts
466
- // Simple receives the modified component:
467
- .update(Position, (pos) => {
468
- renderer.setPosition(pos.x, pos.y);
469
- })
446
+ .update(Position, (pos) => renderer.setPosition(pos.x, pos.y));
470
447
 
471
- // With injection — receives the modified component and extra components:
472
448
  .update(Position, [Sprite], (pos, [sprite]) => {
473
449
  sprite.sprite.setPosition(pos.x, pos.y);
474
- })
450
+ });
475
451
  ```
476
452
 
477
- Injected components listed in `requires()` are non-nullable in the callback; any others are `Type | undefined`.
478
-
479
- Calling `update` also adds that component type to the system's implicit `HAS` query (unless you called `query()` first).
453
+ If `query()` has not been called, `update` automatically expands the implicit `HAS` predicate to require the watched component.
480
454
 
481
455
  #### `.each(components, callback)`
482
456
 
483
- Called every tick for **every tracked entity**, unconditionally. Unlike `update` (which only fires when `component.modified()` is called), `each` fires regardless of whether the component was modified — use it for per-entity logic that must run on every frame.
484
-
485
- The callback receives the entity and a tuple of resolved component instances. Components declared via `requires()` are guaranteed non-null; any others are `undefined` if the entity lacks them.
457
+ Fires every tick for **every tracked entity**, regardless of whether anything changed. Use it for per-entity logic that must run every frame. Implies `.track()`. Only one `each` per system.
486
458
 
487
459
  ```ts
488
460
  .requires(Position, Velocity)
489
461
  .each([Position, Velocity], (e, [pos, vel]) => {
490
- pos.x += vel.vx; // non-null — both are in requires()
491
- pos.y += vel.vy;
492
- })
462
+ pos.x += vel.vx;
463
+ });
493
464
  ```
494
465
 
495
- `each` does not modify the system's query — define membership with `requires(...)` or `query(...)` as usual. Only one `each` may be registered per system; a second call throws.
496
-
497
466
  #### `.sort(components, compare)`
498
467
 
499
- Enable sorted entity tracking. Matched entities are stored in an ordered set whose insertion position is determined by `compare`, which receives a tuple of resolved component instances for each pair being ordered. Implies `.track()`.
500
-
501
- Components declared via `requires()` are non-null in the compare callback.
468
+ Store matched entities in a custom order determined by `compare`. Implies `.track()`. Iterating `system.entities`, `forEach`, and `each` walks entities in sorted order.
502
469
 
503
470
  ```ts
504
471
  world
505
472
  .system("Render")
506
473
  .requires(Position, Sprite)
507
474
  .sort([Position], ([posA], [posB]) => posA.z - posB.z)
508
- .each([Position, Sprite], (e, [pos, sprite]) => {
509
- sprite.draw(pos.x, pos.y);
510
- });
475
+ .each([Position, Sprite], (e, [pos, sprite]) => sprite.draw(pos.x, pos.y));
511
476
  ```
512
477
 
513
- Iterating `system.entities` after a phase run yields entities in the sorted order.
514
-
515
478
  #### `.track()`
516
479
 
517
- Enable entity tracking without an `each` callback — matched entities are exposed via `system.entities` (or `query.entities`) as they enter and leave. `each` and `sort` imply `track` automatically; call this directly only when you need the tracked set without a per-tick callback.
518
-
519
- When called after `world.start()`, `track()` immediately backfills existing entities that satisfy the query predicate.
480
+ Enable entity tracking without an `each` callback — exposes matched entities via `system.entities`. `each` and `sort` imply `track` automatically. When called after `world.start()`, immediately backfills existing matched entities.
520
481
 
521
482
  #### `.run(callback)`
522
483
 
523
- Called every tick when the system's phase runs, regardless of entity state. Use this for polling, network I/O, timers, etc.
484
+ Fires every tick when the system's phase runs, regardless of entity state. Use for polling, network I/O, timers, etc.
524
485
 
525
486
  ```ts
526
487
  .run((now, delta) => {
527
488
  sendNetworkPacket(now);
528
- })
489
+ });
529
490
  ```
530
491
 
492
+ #### `.destroy()`
493
+
494
+ **Not supported on `System`** — calling it throws. Systems live for the duration of the world. Use a standalone `Query` for temporary reactive sets.
495
+
531
496
  ---
532
497
 
533
- ### Query
498
+ ### `Query`
534
499
 
535
- A standalone query is created via `world.query(name)` and configured through the same fluent builder API as `System` (`requires`, `query`, `enter`, `exit`, `sort`, `track`, `forEach`, `entities`). It has no phase and no per-tick callbacks.
500
+ `world.query(name)` returns a standalone reactive entity set, configured through the same builder API as `System`. It has no phase and no per-tick callbacks.
536
501
 
537
502
  ```ts
538
503
  const projectiles = world
@@ -545,46 +510,42 @@ const projectiles = world
545
510
 
546
511
  world.start();
547
512
 
548
- // Anywhere in game code:
549
- projectiles.forEach((e) => {
550
- /* ... */
551
- });
513
+ projectiles.forEach((e) => { ... });
552
514
  console.log(projectiles.entities.size, "active projectiles");
553
515
  ```
554
516
 
555
- | Method | Description |
556
- | ----------------------------------------------- | ------------------------------------------------------------------------- |
557
- | `.requires(...components)` | Set the membership predicate and start tracking. |
558
- | `.query(expr)` | Set the membership predicate using the {@link SystemQuery} DSL. |
559
- | `.enter(callback)` / `.enter(inject, callback)` | Fires when an entity joins the query. |
560
- | `.exit(callback)` / `.exit(inject, callback)` | Fires when an entity leaves the query. |
561
- | `.sort(components, compare)` | Store matched entities in sorted order. |
562
- | `.track()` | Enable tracking (implied by `sort`; backfills when called after `start`). |
563
- | `.belongs(e)` | Returns `true` if the entity satisfies the predicate. |
564
- | `.forEach(callback)` | Iterate all currently tracked entities (entity only). |
565
- | `.forEach(components, callback)` | Iterate with component injection — same signature as `Filter.forEach`. |
566
- | `.entities` | `ReadonlySet<Entity>` of all currently tracked entities. |
567
- | `.destroy()` | Remove the query from the world and all entities. See below. |
517
+ | Method | Description |
518
+ | ------------------------------------------------------- | ------------------------------------------------------------------------ |
519
+ | `.requires(...components)` | Set the membership predicate to `HAS(...components)` and start tracking. |
520
+ | `.query(expr, _guaranteed?)` | Set the membership predicate using a `QueryDSL` expression. |
521
+ | `.enter(callback)` / `.enter(inject, callback)` | Fires when an entity joins the query. |
522
+ | `.exit(callback)` / `.exit(inject, callback)` | Fires when an entity leaves the query. |
523
+ | `.update(C, callback)` / `.update(C, inject, callback)` | Fires when `C` is modified on a tracked entity. |
524
+ | `.sort(components, compare)` | Store matched entities in sorted order. |
525
+ | `.track()` | Enable tracking. Backfills when called after `start()`. |
526
+ | `.belongs(e)` | Returns `true` if the entity satisfies the predicate. |
527
+ | `.forEach(callback)` | Iterate currently tracked entities. |
528
+ | `.forEach(components, callback)` | Iterate with component injection. |
529
+ | `.entities` | `ReadonlySet<Entity>` of currently tracked entities. |
530
+ | `.destroy()` | Remove the query from the world and from every entity (no exit fires). |
568
531
 
569
- #### `.destroy()`
532
+ #### `.destroy()` semantics
570
533
 
571
- Permanently removes a standalone query from the world. All entity references are silently purged (no exit callbacks fire), the tracked entity set is cleared, and the query's `world` reference is set to `undefined`. After this call, any use of the query object is **undefined behavior**.
534
+ `destroy()` permanently removes a standalone query from the world. Entity references are silently purged (no `exit` callbacks fire), the tracked set is cleared, and the `world` reference is set to `undefined`. Any further use of the object is **undefined behavior**.
572
535
 
573
536
  ```ts
574
537
  const q = world.query("Temporary").requires(Position);
575
538
  // ... use q.entities ...
576
- q.destroy(); // unregisters from world and all entities
539
+ q.destroy();
577
540
  ```
578
541
 
579
- `System` does **not** support `destroy()` calling it throws. Systems are owned by the world for the lifetime of the session. Use a standalone `Query` when you need a temporary reactive set.
580
-
581
- Both `System` and `Query` share the same query DSL, enter/exit callbacks, sort, and `entities` set — `System` extends `Query` and layers phase execution on top.
542
+ `System` shares the same DSL, callback, sorting, and tracking machinery `System` extends `Query` and adds phase placement, `run`, `each`, and an inbox replayed on every tick.
582
543
 
583
544
  ---
584
545
 
585
- ### Filter
546
+ ### `Filter`
586
547
 
587
- A `Filter` is created via `world.filter(dsl)` and provides a non-reactive `forEach`. It accepts the same [`QueryDSL`](#-requirescomponents-and-queryq) expressions as systems and queries.
548
+ `world.filter(dsl)` returns a `Filter` that performs a non-reactive scan. It accepts the same `QueryDSL` expressions as systems and queries.
588
549
 
589
550
  ```ts
590
551
  const f = world.filter([Position, Velocity]);
@@ -592,30 +553,58 @@ const f = world.filter([Position, Velocity]);
592
553
 
593
554
  | Method | Description |
594
555
  | -------------------------------- | -------------------------------------------------------------------------- |
595
- | `.forEach(callback)` | Walk all world entities; invoke callback for each matching one. |
556
+ | `.forEach(callback)` | Walk all world entities; invoke callback on each match. |
596
557
  | `.forEach(components, callback)` | Same, with component injection and non-null types for required components. |
597
558
 
598
- **Type inference** works the same way as for `requires()` on systems/queries: component classes extractable from the DSL (`HAS`, `HAS_ONLY`, plain arrays, and `AND` of those) are non-nullable in the callback tuple. Pass a `_guaranteed` second argument to `world.filter()` as a manual override when inference can't reach:
559
+ `forEach` runs inside a deferred scope, so mutations made by the callback are batched and become visible after iteration finishes.
560
+
561
+ **Type inference.** Component classes the type system can extract from the DSL (`HAS`, `HAS_ONLY`, plain arrays, `AND` of those) are non-nullable in the callback tuple. For the rest, supply a `_guaranteed` second argument to `world.filter()`:
599
562
 
600
563
  ```ts
601
564
  // Auto-deduced — both non-null:
602
- world.filter([Position, Velocity])
603
- .forEach([Position, Velocity], (e, [pos, vel]) => { ... });
565
+ world.filter([Position, Velocity]).forEach([Position, Velocity], (e, [pos, vel]) => { ... });
604
566
 
605
567
  // Manual hint for OR / NOT / PARENT / custom function:
606
- world.filter({ OR: [Position, Velocity] }, [Position])
607
- .forEach([Position], (e, [pos]) => pos.x);
568
+ world.filter({ OR: [Position, Velocity] }, [Position]).forEach([Position], (e, [pos]) => pos.x);
608
569
  ```
609
570
 
610
571
  A `Filter` holds no tracked set, makes no registration calls, and needs no `destroy()`.
611
572
 
612
573
  ---
613
574
 
575
+ ### `Bitset`
576
+
577
+ A compact, growable set of non-negative integers backed by 32-bit words. Used internally for entity archetypes and watchlists, and exposed in the public API so component data can use it for bit-flag fields.
578
+
579
+ | Method | Description |
580
+ | ------------------ | ------------------------------------------------------------------------ |
581
+ | `add(n)` | Set bit `n`. |
582
+ | `addBit(bptr)` | Set the bit at a pre-computed `BitPtr`. |
583
+ | `delete(n)` | Clear bit `n`. Trims trailing zero words. |
584
+ | `has(n)` | Returns `true` if bit `n` is set. |
585
+ | `hasBit(bptr)` | Fast check via a pre-computed `BitPtr`. |
586
+ | `equal(other)` | Returns `true` when both bitsets have the same bits set. |
587
+ | `hasBitset(other)` | Returns `true` when every bit set in `other` is also set in this bitset. |
588
+ | `forEach(cb)` | Visit each set bit index in ascending order. |
589
+ | `indices()` | Return all set bit indices as a `number[]`. |
590
+
591
+ ```ts
592
+ class Tags extends Component {
593
+ tags = new Bitset();
594
+ }
595
+
596
+ tags.tags.add(TAG_VISIBLE);
597
+ if (tags.tags.has(TAG_VISIBLE)) { ... }
598
+ ```
599
+
600
+ ---
601
+
614
602
  ## Build & Test
615
603
 
616
604
  ```
617
605
  yarn build
618
606
  yarn test
607
+ yarn lint
619
608
  ```
620
609
 
621
610
  ---