@vworlds/vecs 1.0.15 → 1.0.17

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 (181) hide show
  1. package/README.md +249 -119
  2. package/dist/component.d.ts +52 -76
  3. package/dist/component.js +60 -45
  4. package/dist/component.js.map +1 -1
  5. package/dist/component_meta.d.ts +98 -0
  6. package/dist/component_meta.js +65 -0
  7. package/dist/component_meta.js.map +1 -0
  8. package/dist/dsl.d.ts +46 -34
  9. package/dist/dsl.js +459 -61
  10. package/dist/dsl.js.map +1 -1
  11. package/dist/entity/entity.base.d.ts +57 -0
  12. package/dist/entity/entity.base.js +81 -0
  13. package/dist/entity/entity.base.js.map +1 -0
  14. package/dist/entity/entity.components.d.ts +117 -0
  15. package/dist/entity/entity.components.js +244 -0
  16. package/dist/entity/entity.components.js.map +1 -0
  17. package/dist/entity/entity.d.ts +35 -0
  18. package/dist/entity/entity.identity.d.ts +8 -0
  19. package/dist/entity/entity.identity.js +15 -0
  20. package/dist/entity/entity.identity.js.map +1 -0
  21. package/dist/entity/entity.js +33 -0
  22. package/dist/entity/entity.js.map +1 -0
  23. package/dist/entity/entity.lifecycle.d.ts +12 -0
  24. package/dist/entity/entity.lifecycle.js +111 -0
  25. package/dist/entity/entity.lifecycle.js.map +1 -0
  26. package/dist/entity/entity.queries.d.ts +3 -0
  27. package/dist/entity/entity.queries.js +33 -0
  28. package/dist/entity/entity.queries.js.map +1 -0
  29. package/dist/entity/entity.relationships.d.ts +9 -0
  30. package/dist/entity/entity.relationships.js +74 -0
  31. package/dist/entity/entity.relationships.js.map +1 -0
  32. package/dist/entity/index.d.ts +2 -0
  33. package/dist/entity/index.js +3 -0
  34. package/dist/entity/index.js.map +1 -0
  35. package/dist/filter.d.ts +27 -8
  36. package/dist/filter.js +33 -18
  37. package/dist/filter.js.map +1 -1
  38. package/dist/index.d.ts +13 -5
  39. package/dist/index.js +13 -2
  40. package/dist/index.js.map +1 -1
  41. package/dist/inject.d.ts +80 -0
  42. package/dist/inject.js +270 -0
  43. package/dist/inject.js.map +1 -0
  44. package/dist/module.d.ts +23 -0
  45. package/dist/module.js +17 -0
  46. package/dist/module.js.map +1 -0
  47. package/dist/modules/identity.d.ts +15 -0
  48. package/dist/modules/identity.js +41 -0
  49. package/dist/modules/identity.js.map +1 -0
  50. package/dist/modules/singleton.d.ts +26 -0
  51. package/dist/modules/singleton.js +41 -0
  52. package/dist/modules/singleton.js.map +1 -0
  53. package/dist/package.json +12 -1
  54. package/dist/phase.d.ts +2 -2
  55. package/dist/query/index.d.ts +6 -0
  56. package/dist/query/index.js +5 -0
  57. package/dist/query/index.js.map +1 -0
  58. package/dist/query/query.00.base.d.ts +23 -0
  59. package/dist/query/query.00.base.js +77 -0
  60. package/dist/query/query.00.base.js.map +1 -0
  61. package/dist/query/query.01.reactive.d.ts +7 -0
  62. package/dist/query/query.01.reactive.js +58 -0
  63. package/dist/query/query.01.reactive.js.map +1 -0
  64. package/dist/query/query.02.lifecycle.d.ts +6 -0
  65. package/dist/query/query.02.lifecycle.js +63 -0
  66. package/dist/query/query.02.lifecycle.js.map +1 -0
  67. package/dist/query/query.03.tracking.d.ts +15 -0
  68. package/dist/query/query.03.tracking.js +31 -0
  69. package/dist/query/query.03.tracking.js.map +1 -0
  70. package/dist/query/query.04.callbacks.d.ts +14 -0
  71. package/dist/query/query.04.callbacks.js +65 -0
  72. package/dist/query/query.04.callbacks.js.map +1 -0
  73. package/dist/query/query.05.updates.d.ts +14 -0
  74. package/dist/query/query.05.updates.js +81 -0
  75. package/dist/query/query.05.updates.js.map +1 -0
  76. package/dist/query/query.06.predicate.d.ts +13 -0
  77. package/dist/query/query.06.predicate.js +40 -0
  78. package/dist/query/query.06.predicate.js.map +1 -0
  79. package/dist/query/query.07.groups.d.ts +41 -0
  80. package/dist/query/query.07.groups.js +110 -0
  81. package/dist/query/query.07.groups.js.map +1 -0
  82. package/dist/query/query.d.ts +53 -0
  83. package/dist/query/query.js +138 -0
  84. package/dist/query/query.js.map +1 -0
  85. package/dist/relationship.d.ts +19 -0
  86. package/dist/relationship.js +18 -0
  87. package/dist/relationship.js.map +1 -0
  88. package/dist/system.d.ts +37 -23
  89. package/dist/system.js +80 -64
  90. package/dist/system.js.map +1 -1
  91. package/dist/terms/all_term.d.ts +32 -0
  92. package/dist/terms/all_term.js +41 -0
  93. package/dist/terms/all_term.js.map +1 -0
  94. package/dist/terms/any_term.d.ts +33 -0
  95. package/dist/terms/any_term.js +42 -0
  96. package/dist/terms/any_term.js.map +1 -0
  97. package/dist/terms/build.d.ts +62 -0
  98. package/dist/terms/build.js +382 -0
  99. package/dist/terms/build.js.map +1 -0
  100. package/dist/terms/component_term.d.ts +37 -0
  101. package/dist/terms/component_term.js +49 -0
  102. package/dist/terms/component_term.js.map +1 -0
  103. package/dist/terms/empty_term.d.ts +6 -0
  104. package/dist/terms/empty_term.js +12 -0
  105. package/dist/terms/empty_term.js.map +1 -0
  106. package/dist/terms/index.d.ts +11 -0
  107. package/dist/terms/index.js +12 -0
  108. package/dist/terms/index.js.map +1 -0
  109. package/dist/terms/not_term.d.ts +35 -0
  110. package/dist/terms/not_term.js +47 -0
  111. package/dist/terms/not_term.js.map +1 -0
  112. package/dist/terms/only_term.d.ts +47 -0
  113. package/dist/terms/only_term.js +79 -0
  114. package/dist/terms/only_term.js.map +1 -0
  115. package/dist/terms/predicate_term.d.ts +80 -0
  116. package/dist/terms/predicate_term.js +109 -0
  117. package/dist/terms/predicate_term.js.map +1 -0
  118. package/dist/terms/target_term.d.ts +43 -0
  119. package/dist/terms/target_term.js +87 -0
  120. package/dist/terms/target_term.js.map +1 -0
  121. package/dist/terms/term.d.ts +94 -0
  122. package/dist/terms/term.js +202 -0
  123. package/dist/terms/term.js.map +1 -0
  124. package/dist/terms/world_term.d.ts +68 -0
  125. package/dist/terms/world_term.js +99 -0
  126. package/dist/terms/world_term.js.map +1 -0
  127. package/dist/timer.js +2 -2
  128. package/dist/timer.js.map +1 -1
  129. package/dist/util/array_map.js +12 -0
  130. package/dist/util/array_map.js.map +1 -1
  131. package/dist/util/bitset.js +107 -22
  132. package/dist/util/bitset.js.map +1 -1
  133. package/dist/util/dense_set.d.ts +1 -0
  134. package/dist/util/dense_set.js +90 -0
  135. package/dist/util/dense_set.js.map +1 -0
  136. package/dist/util/id_pool.d.ts +30 -0
  137. package/dist/util/id_pool.js +222 -0
  138. package/dist/util/id_pool.js.map +1 -0
  139. package/dist/world/index.d.ts +3 -0
  140. package/dist/world/index.js +3 -0
  141. package/dist/world/index.js.map +1 -0
  142. package/dist/world/world.base.d.ts +6 -0
  143. package/dist/world/world.base.js +21 -0
  144. package/dist/world/world.base.js.map +1 -0
  145. package/dist/world/world.components.d.ts +67 -0
  146. package/dist/world/world.components.js +93 -0
  147. package/dist/world/world.components.js.map +1 -0
  148. package/dist/world/world.d.ts +29 -0
  149. package/dist/world/world.deferred.d.ts +13 -0
  150. package/dist/world/world.deferred.js +93 -0
  151. package/dist/world/world.deferred.js.map +1 -0
  152. package/dist/world/world.entities.d.ts +18 -0
  153. package/dist/world/world.entities.js +97 -0
  154. package/dist/world/world.entities.js.map +1 -0
  155. package/dist/world/world.js +39 -0
  156. package/dist/world/world.js.map +1 -0
  157. package/dist/world/world.modules.d.ts +12 -0
  158. package/dist/world/world.modules.js +21 -0
  159. package/dist/world/world.modules.js.map +1 -0
  160. package/dist/world/world.pipeline.d.ts +21 -0
  161. package/dist/world/world.pipeline.js +106 -0
  162. package/dist/world/world.pipeline.js.map +1 -0
  163. package/dist/world/world.pools.d.ts +9 -0
  164. package/dist/world/world.pools.js +63 -0
  165. package/dist/world/world.pools.js.map +1 -0
  166. package/dist/world/world.queries.d.ts +18 -0
  167. package/dist/world/world.queries.js +101 -0
  168. package/dist/world/world.queries.js.map +1 -0
  169. package/dist/world/world.storage.d.ts +7 -0
  170. package/dist/world/world.storage.js +26 -0
  171. package/dist/world/world.storage.js.map +1 -0
  172. package/package.json +12 -1
  173. package/dist/entity.d.ts +0 -215
  174. package/dist/entity.js +0 -457
  175. package/dist/entity.js.map +0 -1
  176. package/dist/query.d.ts +0 -251
  177. package/dist/query.js +0 -353
  178. package/dist/query.js.map +0 -1
  179. package/dist/world.d.ts +0 -389
  180. package/dist/world.js +0 -631
  181. package/dist/world.js.map +0 -1
package/README.md CHANGED
@@ -27,7 +27,7 @@ yarn add @vworlds/vecs
27
27
  ### Lifecycle in brief
28
28
 
29
29
  ```
30
- registerComponent() × N → addPhase() / system() / query() × N → start() → progress() every frame
30
+ world.component() × N → addPhase() / system() / query() × N → start() → progress() every frame
31
31
  ```
32
32
 
33
33
  Components must be registered before they are used as component classes. After `start()`, component registration is disabled. Systems and queries can still be created — standalone queries backfill existing matched entities immediately.
@@ -59,9 +59,9 @@ class Health {
59
59
 
60
60
  const world = new World();
61
61
 
62
- world.registerComponent(Position);
63
- world.registerComponent(Velocity);
64
- world.registerComponent(Health);
62
+ world.component(Position);
63
+ world.component(Velocity);
64
+ world.component(Health);
65
65
 
66
66
  // ─── Phases ────────────────────────────────────────────────────────────────
67
67
 
@@ -95,7 +95,7 @@ world
95
95
  // ─── Hooks ─────────────────────────────────────────────────────────────────
96
96
 
97
97
  world
98
- .hook(Health)
98
+ .component(Health)
99
99
  .onAdd((entity, h) => console.log(`entity ${entity.eid} spawned with hp=${h.hp}`))
100
100
  .onRemove((entity) => console.log(`entity ${entity.eid} died`));
101
101
 
@@ -120,14 +120,14 @@ for (let tick = 0; tick < 5; tick++) {
120
120
 
121
121
  ## Deferred mode
122
122
 
123
- Inside a system body, a `Query.forEach`, or any `world.defer(...)` block, the world is in **deferred mode**: entity mutations (`add` / `attach` / `set` / `remove` / `destroy` / `setParent` / `modified`) are queued instead of applied inline. The queue drains on the boundary that opened the deferred scope.
123
+ Inside a system body, a `Query.forEach`, or any `world.defer(...)` block, the world is in **deferred mode**: entity mutations (`add` / `attach` / `set` / `remove` / `destroy` / `modified`) are queued instead of applied inline. The queue drains on the boundary that opened the deferred scope.
124
124
 
125
125
  Concretely, while deferred:
126
126
 
127
- - `entity.get(C)` returns `undefined` after `entity.add(C)` (no instance has been created yet).
128
- - `entity.get(C)` returns `undefined` after `entity.attach(instance)` if C was absent.
129
- - `entity.get(C)` returns the **previous** value after `entity.set(C, props)`.
130
- - `entity.get(C)` still returns the component after `entity.remove(C)`.
127
+ - `entity.get(C)` / `entity.getMut(C)` returns `undefined` after `entity.add(C)` (no instance has been created yet).
128
+ - `entity.get(C)` / `entity.getMut(C)` returns `undefined` after `entity.attach(instance)` if C was absent.
129
+ - `entity.get(C)` / `entity.getMut(C)` returns the **previous** value after `entity.set(C, props)`.
130
+ - `entity.get(C)` / `entity.getMut(C)` still returns the component after `entity.remove(C)`.
131
131
 
132
132
  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`.
133
133
 
@@ -145,7 +145,7 @@ const world = new World();
145
145
 
146
146
  #### Component registration
147
147
 
148
- Components are ordinary classes. They do not inherit from a vecs base class, and vecs constructs them with `new ComponentClass()`, so constructors should take no parameters. Register every component class before using it in `add`, `set`, `get`, `requires`, `query`, `filter`, `hook`, or `setExclusiveComponents`.
148
+ Components are ordinary classes. They do not inherit from a vecs base class, and vecs constructs them with `new ComponentClass()`, so constructors should take no parameters. Register every component class before using it in `add`, `set`, `get`, `requires`, `query`, `filter`, hook registration, or `setExclusiveComponents`.
149
149
 
150
150
  ```ts
151
151
  class Position {
@@ -153,22 +153,31 @@ class Position {
153
153
  y = 0;
154
154
  }
155
155
 
156
- // Auto-assigned type id ( 256 for "local" components):
157
- const positionMeta = world.registerComponent(Position);
156
+ // Auto-assigned component id (drawn from the "component" id pool, 1–899 by default):
157
+ const positionComponent = world.component(Position);
158
158
 
159
- // Explicit numeric type id (e.g. server-assigned):
160
- world.registerComponent(Position, 1);
159
+ // Explicit numeric id (e.g. a server-assigned stable id):
160
+ world.component(Position, 42);
161
161
 
162
- // Explicit display name (e.g. when the class name differs from the network name):
163
- world.registerComponent(Position, "pos");
162
+ // Access component metadata (numeric id, cleanup policy, etc.):
163
+ const meta = world.component(Position).ownMeta; // ComponentMeta
164
164
 
165
- // Pre-register a name id mapping before the class is available:
166
- world.registerComponentType("Position", 1);
165
+ // Give the component a name so it can be looked up by string later:
166
+ world.component(Position).name = "Position";
167
+ world.component("Position"); // resolves the same Component entity
167
168
  ```
168
169
 
169
- `registerComponent` returns the `ComponentMeta` for the class. Internally, vecs stores that metadata on the component class under a hidden world-specific key, so the same class can be registered independently in multiple worlds. Numeric type lookup still goes through the world's type table.
170
+ `world.component(Class)` returns the `Component` entity for that class, creating and registering it on the first call. Subsequent calls with the same class return the same entity. The same class can be registered independently in multiple worlds.
170
171
 
171
- After `world.start()` (or `world.disableComponentRegistration()`) any further call to `registerComponent` throws. There is no automatic component registration; using an unregistered component class as a component is an error.
172
+ Id ranges are configured by passing `idPools` to the `World` constructor. The built-in defaults are:
173
+
174
+ | Pool | Range |
175
+ | ----------- | --------- |
176
+ | `component` | 1 – 899 |
177
+ | `module` | 900 – 999 |
178
+ | `entity` | 1000 – ∞ |
179
+
180
+ After `world.start()` (or `world.disableComponentRegistration()`) any further call to `world.component(Class)` that would register a new class throws. There is no automatic component registration; using an unregistered component class as a component is an error.
172
181
 
173
182
  #### Exclusive component groups
174
183
 
@@ -188,27 +197,53 @@ Each call defines one independent group. A component may belong to at most one g
188
197
  // New entity with an auto-incrementing id:
189
198
  const e = world.entity();
190
199
 
191
- // Look up by id (returns undefined if not found):
200
+ // Look up by id (throws if not found):
192
201
  const found = world.entity(42);
193
202
 
203
+ // Optional lookup by id or component ref:
204
+ const maybeFound = world.getEntity(42);
205
+
194
206
  // Server-assigned id; creates the entity if it doesn't exist yet:
195
207
  const net = world.getOrCreateEntity(serverId, (newEntity) => {
196
208
  tracked.add(newEntity);
197
209
  });
198
210
 
199
- // Reserve a high id range for locally created entities so they don't collide
200
- // with server-assigned ids (call before world.start()):
201
- world.setEntityIdRange(0x10000);
202
-
203
211
  // Destroy everything (e.g. on level reset):
204
212
  world.clearAllEntities();
213
+
214
+ // Named entity lookup via Identity.name — creates entity with that name if absent:
215
+ world.entity("player").name === "player"; // true
216
+ world.getEntity("player") === entity; // true (returns undefined when absent)
217
+
218
+ // Named component lookup — set the name first, then resolve by name:
219
+ world.component(Position).name = "Position";
220
+ world.component("Position"); // resolves the same component entity
221
+ ```
222
+
223
+ #### Component entity deletion cleanup
224
+
225
+ Every component key is backed by a component entity. Destroying that component entity is controlled by `world.component(C).ownMeta.onDelete`:
226
+
227
+ | Policy | Meaning |
228
+ | ---------------------- | -------------------------------------------------------------------------------------------------- |
229
+ | `CleanupPolicy.Throw` | Default. Destroying the component entity while other entities carry it throws and changes nothing. |
230
+ | `CleanupPolicy.Remove` | Remove the component from every carrier, then destroy the component entity. |
231
+ | `CleanupPolicy.Delete` | Destroy every carrier, then destroy the component entity. |
232
+
233
+ ```ts
234
+ import { CleanupPolicy } from "@vworlds/vecs";
235
+
236
+ world.component(Temporary).ownMeta.onDelete = CleanupPolicy.Remove;
237
+ world.component(Temporary).destroy(); // strips Temporary from all carriers
205
238
  ```
206
239
 
240
+ The `Throw` default also applies to plain entities used as component keys (`entity.add(key)`). It preserves consistency: a failed destroy leaves the component entity, its carriers, and the usage count untouched.
241
+
207
242
  #### Hooks
208
243
 
209
244
  ```ts
210
245
  world
211
- .hook(Sprite)
246
+ .component(Sprite)
212
247
  .onAdd((entity, sprite) => sprite.initialize(scene, entity))
213
248
  .onRemove((entity, sprite) => sprite.destroy(scene, entity))
214
249
  .onSet((entity, sprite) => sprite.syncToScene(entity));
@@ -227,7 +262,7 @@ const send = world.addPhase("send");
227
262
  world.progress(now, delta);
228
263
 
229
264
  // ...or run individual phases manually:
230
- world.beginFrame(delta);
265
+ world.beginFrame(delta); // one numeric arg: ms since last frame
231
266
  try {
232
267
  world.runPhase(preUpdate, now, delta);
233
268
  world.runPhase(update, now, delta);
@@ -322,7 +357,7 @@ const enemies = world
322
357
  .enter((e) => console.log("enemy spawned", e.eid));
323
358
 
324
359
  world.start();
325
- // enemies.entities is kept up-to-date automatically.
360
+ // enemies.count and query iteration are kept up-to-date automatically.
326
361
 
327
362
  // Standalone queries can also be created after start(); existing matched
328
363
  // entities are backfilled immediately.
@@ -340,14 +375,12 @@ world.filter([Position, Velocity]).forEach([Position, Velocity], (e, [pos, vel])
340
375
  });
341
376
 
342
377
  // Full DSL, with auto-deduced required components:
343
- world
344
- .filter({ AND: [{ HAS: Position }, { HAS: Velocity }] })
345
- .forEach([Position, Velocity], (e, [pos, vel]) => {
346
- pos.x += vel.vx;
347
- });
378
+ world.filter({ all: [Position, Velocity] }).forEach([Position, Velocity], (e, [pos, vel]) => {
379
+ pos.x += vel.vx;
380
+ });
348
381
 
349
382
  // Manual hint for queries the type extractor can't see through:
350
- world.filter({ OR: [Position, Velocity] }, [Position]).forEach([Position], (e, [pos]) => pos.x);
383
+ world.filter({ any: [Position, Velocity] }, [Position]).forEach([Position], (e, [pos]) => pos.x);
351
384
  ```
352
385
 
353
386
  A `Filter` requires no name, no `world.start()`, and no `destroy()` — create it anywhere and discard freely.
@@ -366,7 +399,7 @@ world.endDefer();
366
399
 
367
400
  ---
368
401
 
369
- ### `Component`
402
+ ### `ComponentInstance`
370
403
 
371
404
  Components are plain classes. vecs does not provide a runtime base class and does not attach `entity`, `meta`, `type`, `bitPtr`, or `modified()` to component instances.
372
405
 
@@ -376,12 +409,12 @@ class Position {
376
409
  y = 0;
377
410
  }
378
411
 
379
- world.registerComponent(Position);
412
+ world.component(Position);
380
413
 
381
414
  entity.add(Position);
382
- const pos = entity.get(Position)!;
415
+ const pos = entity.getMut(Position)!;
383
416
  pos.x = 100;
384
- entity.modified(Position); // tell the world this component changed
417
+ // getMut marks Position changed before returning the mutable instance.
385
418
 
386
419
  // Equivalent — set assigns props and fires onSet automatically:
387
420
  entity.set(Position, { x: 100 });
@@ -396,11 +429,11 @@ entity.get(Position) === shared; // true
396
429
  | ------------------------- | -------------------------------------------------------------------------------------------------------------- |
397
430
  | Plain class | Components should be ordinary classes with field initializers and methods as needed. |
398
431
  | No-arg construction | vecs calls `new ComponentClass()`, so constructors should be omitted or take no parameters. |
399
- | Explicit registration | Call `world.registerComponent(C)` before using the class as a component. |
432
+ | Explicit registration | Call `world.component(C)` before using the class as a component. |
400
433
  | Shared instances possible | `entity.attach(instance)` stores the exact passed object; code should use the entity passed by vecs callbacks. |
401
434
  | Manual dirty marking | After mutating fields directly, call `entity.modified(C)` to notify hooks, queries, and systems. |
402
435
 
403
- Use `world.getComponentMeta(C)` or `world.getComponentType(C)` when you need metadata such as the numeric type id or component name. Metadata is world-specific.
436
+ Use `world.component(C).ownMeta` when you need metadata such as the numeric type id or component name. Metadata is world-specific.
404
437
 
405
438
  ---
406
439
 
@@ -408,48 +441,105 @@ Use `world.getComponentMeta(C)` or `world.getComponentType(C)` when you need met
408
441
 
409
442
  Created via `world.entity()` (auto-assigned id) or `world.getOrCreateEntity(id, ...)` (caller-supplied id).
410
443
 
411
- | Property / Method | Description |
412
- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
413
- | `eid` | Unique numeric entity id. |
414
- | `world` | The `World` that owns this entity. |
415
- | `componentBitmask` | `Bitset` of component type ids attached to this entity. Used by archetype matching. |
416
- | `properties` | `Map<string, any>` free-form bag for module-level bookkeeping. |
417
- | `add(Class)` | Attach a component (idempotent). Returns the entity for chaining. |
418
- | `attach(instance)` | Attach an existing registered component instance directly; replaces any previous instance for that component class and fires `onSet`. |
419
- | `set(Class, props)` | Attach a component and assign `props`; fires `onSet`. Returns the entity for chaining. |
420
- | `modified(Class)` | Queue an `onSet` / `update` notification for a component class or numeric type id. Returns the entity for chaining. |
421
- | `get(Class)` | Return the component instance, or `undefined`. |
422
- | `remove(Class)` | Detach a component (fires `onRemove` and `exit`). |
423
- | `destroy()` | Remove all components, unregister from the world, recurse into children. |
424
- | `components` | `ReadonlyArrayMap<Component>` read-only view of attached components keyed by type id. Supports `forEach`, `get`, `has`, and `size`. |
425
- | `empty` | `true` when no components are attached. |
426
- | `parent` | Parent entity, or `undefined` for a root entity. |
427
- | `children` | `ReadonlySet<Entity>` of direct children (lazy). |
428
- | `setParent(p)` | Reparent the entity. `undefined` makes it a root entity. Throws on cycles. |
429
- | `events` | Typed event emitter. Currently emits `"destroy"` just before teardown. |
430
- | `toString()` | Returns `"EntityN"`. |
431
-
432
- Call `entity.modified(C)` after mutating a component directly. Repeated calls for the same component type are coalesced while the world is deferred:
433
-
434
- ```ts
435
- const vel = entity.get(Velocity)!;
444
+ | Property / Method | Description |
445
+ | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
446
+ | `eid` | Unique numeric entity id. |
447
+ | `world` | The `World` that owns this entity. |
448
+ | `componentBitmask` | `Bitset` of component type ids attached to this entity. Used by archetype matching. |
449
+ | `properties` | `Map<string, any>` free-form bag for module-level bookkeeping. |
450
+ | `add(Class)` | Attach a component (idempotent). Returns the entity for chaining. |
451
+ | `attach(instance)` | Attach an existing registered component instance directly; replaces any previous instance for that component class and fires `onSet`. |
452
+ | `set(Class, props)` | Attach a component and assign `props`; fires `onSet`. Returns the entity for chaining. |
453
+ | `modified(Class)` | Queue an `onSet` / `update` notification for a component class or numeric type id. Returns the entity for chaining. |
454
+ | `get(Class)` | Return the component as a readonly-typed value, or `undefined`. |
455
+ | `getMut(Class)` | Mark the component modified and return the mutable instance, or `undefined`. |
456
+ | `remove(Class)` | Detach a component (fires `onRemove` and `exit`). |
457
+ | `destroy()` | Remove all components, unregister from the world, then applies incoming relationship cleanup policies. |
458
+ | `components` | `ReadonlyArrayMap<ComponentInstance>` read-only view of attached components keyed by type id. Supports `forEach`, `get`, `has`, and `size`. |
459
+ | `empty` | `true` when no components are attached. |
460
+ | `parent(ref)` | Target entity for relationship component `ref`, or `undefined`. |
461
+ | `children(ref)` | `ReadonlySet<Entity>` of entities targeting this one through relationship component `ref`. |
462
+ | `events` | Typed event emitter. Currently emits `"destroy"` just before teardown. |
463
+ | `toString()` | Returns `"EntityN"`. |
464
+
465
+ Use `entity.getMut(C)` when you need to mutate a component directly. It calls `entity.modified(C)` before returning the mutable instance. Repeated modified notifications for the same component type are coalesced while the world is deferred:
466
+
467
+ ```ts
468
+ const vel = entity.getMut(Velocity)!;
436
469
  vel.vx += accel;
437
- entity.modified(Velocity); // chainable
438
470
  ```
439
471
 
440
472
  Use `entity.attach(instance)` when component ownership is intentionally shared with caller code or another object graph. The instance constructor must be registered in the entity's world; unregistered instances throw. If the component belongs to an exclusive component group, conflicting components are removed before the instance is stored.
441
473
 
442
- #### Parent / child hierarchy
474
+ #### Relationships and parent / child hierarchy
443
475
 
444
476
  ```ts
445
- child.setParent(parent);
446
- parent.children.has(child); // true
477
+ import { ChildOf, CleanupPolicy, Relationship } from "@vworlds/vecs";
447
478
 
448
- // Destroying a parent recursively destroys all children:
479
+ child.set(ChildOf, { target: parent });
480
+ child.parent(ChildOf) === parent; // true
481
+ parent.children(ChildOf).has(child); // true
482
+
483
+ // ChildOf is configured with CleanupPolicy.Delete, so destroying a parent
484
+ // recursively destroys children that target it through ChildOf:
449
485
  parent.destroy();
450
486
  ```
451
487
 
452
- `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.
488
+ Any component that extends `Relationship` can point at another entity through its `target` field. The source entity stores the relationship component; the target entity can enumerate sources with `children(ref)`.
489
+
490
+ ```ts
491
+ class EquippedBy extends Relationship {}
492
+
493
+ item.set(EquippedBy, { target: player });
494
+ item.parent(EquippedBy); // player
495
+ player.children(EquippedBy).has(item); // true
496
+ ```
497
+
498
+ Relationship target cleanup is controlled by `ownMeta.onDeleteTarget`:
499
+
500
+ | Policy | Meaning |
501
+ | ---------------------- | -------------------------------------------------------------------------------------------- |
502
+ | `CleanupPolicy.Remove` | Default. When a target is destroyed, remove the relationship component from each source. |
503
+ | `CleanupPolicy.Delete` | When a target is destroyed, destroy each source entity that targets it through the relation. |
504
+
505
+ `ChildOf` is registered by every `World` and uses `CleanupPolicy.Delete`. Custom relationships default to `Remove`; set `world.component(MyRelationship).ownMeta.onDeleteTarget = CleanupPolicy.Delete` if target deletion should cascade.
506
+
507
+ `onDeleteTarget` is independent from `onDelete`: `onDeleteTarget` runs when a relationship target is destroyed, while `onDelete` runs when the relationship component entity itself is destroyed.
508
+
509
+ Retarget relationships with `entity.set(RelationshipClass, { target })`. `entity.getMut(RelationshipClass)` throws for relationship components because in-place target mutation would bypass the reverse index used by `children(ref)` and cleanup cascades.
510
+
511
+ Relationship queries use `target` to follow a relationship from the candidate entity to its target and test that target with another DSL expression. Use `parent` for the built-in `ChildOf` hierarchy:
512
+
513
+ ```ts
514
+ world.query("body-friends").query({ all: [Body, { target: [FriendOf, [Position, Velocity]] }] });
515
+ world.query("positioned-children").query({ all: [Position, { parent: Body }] });
516
+ ```
517
+
518
+ Tracked queries stay live when either side changes. Adding or removing `FriendOf` on the candidate re-routes the candidate normally; adding or removing `Position` / `Velocity` on the target also refreshes every candidate that points at that target. Nested `target` works the same way across multiple hops.
519
+
520
+ Use `source` with `world.filter(...)` when you need the opposite direction: test whether at least one child points at the candidate through a relationship and matches an inner DSL expression. Use `children` for the built-in `ChildOf` hierarchy. `source` and `children` are exact for each filter scan, but they are non-reactive and rejected by tracked `Query` / `System` membership.
521
+
522
+ ```ts
523
+ world.filter({ all: [Body, { children: Position }] }).forEach((parent) => {
524
+ // parent has at least one ChildOf child with Position right now.
525
+ });
526
+ ```
527
+
528
+ Component injection supports lowercase `down` in `forEach` and system `each` injection lists. It fans out: the callback fires once for each child pointing at the visited entity through the relationship, and child component slots are nullable.
529
+
530
+ ```ts
531
+ world
532
+ .query("parents")
533
+ .requires(Body)
534
+ .forEach([Body, { down: [ChildOf, [Position]] }], (parent, [body, childPos]) => {
535
+ // One call per ChildOf child of parent. childPos is undefined when that child lacks Position.
536
+ });
537
+ ```
538
+
539
+ Two details matter:
540
+
541
+ - A `down` injection does not call the callback for a matched entity with zero children through that relationship.
542
+ - `down` injection iterates all children through the relationship, independent of any `children` / `source` filter used for membership. For example, `world.filter({ children: Position }).forEach([{ down: [ChildOf, [Position]] }], ...)` also visits children without `Position`, with `childPos === undefined`.
453
543
 
454
544
  ---
455
545
 
@@ -462,33 +552,73 @@ Systems are created via `world.system(name)` and configured through a fluent bui
462
552
  Declare which entities the system tracks.
463
553
 
464
554
  ```ts
465
- .requires(Position, Velocity) // shorthand for HAS
466
- .query({ HAS: [Position, Velocity] }) // explicit
467
- .query({ PARENT: { AND: [Player, Container] } }) // parent-aware
468
- .query({ AND: [Position, { OR: [Sprite, Container] }] }) // compound
469
- .query({ NOT: Invisible })
555
+ .requires(Position, Velocity) // shorthand for [Position, Velocity]
556
+ .query([Position, Velocity]) // explicit component list
557
+ .query({ all: [Position, { any: [Sprite, Container] }] }) // compound
558
+ .query({ not: Invisible })
559
+ .query({ target: [FriendOf, Position] }) // relationship target has Position
560
+ .query({ parent: Body }) // ChildOf target has Body
470
561
  ```
471
562
 
472
563
  **Query operators:**
473
564
 
474
- | Operator | Meaning |
475
- | ---------------------- | ---------------------------------------- |
476
- | `{ HAS: [A, B] }` | Entity has all of A and B |
477
- | `{ HAS_ONLY: [A, B] }` | Entity has exactly A and B, nothing else |
478
- | `{ AND: [q1, q2] }` | Both sub-queries must match |
479
- | `{ OR: [q1, q2] }` | Either sub-query matches |
480
- | `{ NOT: q }` | Sub-query must not match |
481
- | `{ PARENT: q }` | Entity's parent matches q |
482
- | An array `[A, B]` | Shorthand for `{ HAS: [A, B] }` |
483
- | A single class / id | Shorthand for `{ HAS: [C] }` |
484
- | A predicate function | Custom membership logic |
565
+ | Operator | Meaning |
566
+ | -------------------- | ------------------------------------------------------ |
567
+ | A single class / id | Entity has that component |
568
+ | An array `[A, B]` | Entity has all of A and B |
569
+ | `{ only: [A, B] }` | Entity has exactly A and B, nothing else |
570
+ | `{ all: [q1, q2] }` | Both sub-queries must match |
571
+ | `{ any: [q1, q2] }` | Either sub-query matches |
572
+ | `{ not: q }` | Sub-query must not match |
573
+ | `{ target: [R, q] }` | Relationship `R` target matches `q` |
574
+ | `{ source: [R, q] }` | Filter-only: some `R` source/child matches `q` |
575
+ | `{ parent: q }` | Built-in `ChildOf` target matches `q` |
576
+ | `{ children: q }` | Filter-only: some built-in `ChildOf` child matches `q` |
577
+ | `{ test: fn }` | Custom membership logic |
578
+
579
+ `target` and `source` use tuple form only: `{ target: [RelationshipClass, innerDSL] }` / `{ source: [RelationshipClass, innerDSL] }`. The first element must be a component that extends `Relationship`; passing a normal component throws. `target` evaluates the inner DSL against the relationship target; `source` evaluates it against each child/source and matches when at least one child/source matches. Because `source` and `children` are non-reactive, use them with `world.filter(...)`, not tracked queries or systems.
580
+
581
+ Component injection also supports lowercase relationship markers. `up` works in `forEach`, `each`, `enter`, and `exit` injection lists. It follows the relationship target and flattens the requested target components into the callback tuple:
582
+
583
+ ```ts
584
+ world
585
+ .query("friends")
586
+ .query({ target: [FriendOf, Position] })
587
+ .forEach([Body, { up: [FriendOf, [Position, Velocity]] }], (e, [body, pos, vel]) => {
588
+ // body is from e; pos and vel are from e.parent(FriendOf)
589
+ // target-side injected components are undefined if the target is absent or lacks them.
590
+ });
591
+ ```
592
+
593
+ On `exit`, direct component injection is snapshot-stable when the exiting component was just removed. `up` injection resolves the target live at callback time, so target-side slots can be `undefined` if the exit was caused by the target losing that component.
485
594
 
486
- Class-valued query terms are recognized as components by looking up registered metadata for the current world. Register component classes before using them in a `QueryDSL`; an unregistered class is treated like a predicate function and will fail if it cannot be called that way.
595
+ Lowercase `down` is allowed only in `forEach` and system `each`, where fan-out has a clear meaning. Only one `down` marker is allowed per injection tuple.
596
+
597
+ #### Iter cursor
598
+
599
+ Pass `Iter` as the first argument to `each`, `forEach`, `enter`, `exit`, `update`, or `sort` to receive a reusable cursor object instead of the bare entity. The cursor exposes:
600
+
601
+ - `it.entity` — the visited entity.
602
+ - `it.src` — a same-length array of source entities: the visited entity for a directly-injected component, the relationship target for an `up`-injected one, or the specific child for a `down`-injected one.
603
+
604
+ ```ts
605
+ system.each(Iter, [Body, { up: [ChildOf, [Position]] }], (it, [body, pos]) => {
606
+ it.entity; // the visited entity
607
+ it.src[0]; // entity `body` was read from (the visited entity)
608
+ it.src[1]; // entity `pos` was read from (the ChildOf target)
609
+ });
610
+ ```
611
+
612
+ When `Iter` is **not** requested, the original code path runs with zero per-entity overhead. Dispatch to the cursor or non-cursor path happens once at setup time — not per entity. A single `Iter` instance is mutated before each callback; read what you need inside the callback but do not retain the cursor or its `src` array across callbacks.
613
+
614
+ `enter` and `exit` allocate a fresh `Iter` per event (they fire reentrantly during command routing). `each` / `forEach` / `update` reuse a single `Iter` instance for the whole pass; `sort` reuses two (one per side of the comparison). Because a plain `Query.update` callback fires synchronously during command routing, a nested mutation triggered from inside it can re-enter `update` and overwrite that reused cursor and tuple mid-callback — snapshot any `it`, `it.src`, or tuple values you need before triggering further mutations.
615
+
616
+ Class-valued query terms are components. Register component classes before using them in a `QueryDSL`; an unregistered class throws. Use `{ test: fn }` for arbitrary predicate functions.
487
617
 
488
618
  **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:
489
619
 
490
620
  ```ts
491
- .query({ AND: [{ HAS: Position }, { HAS: Velocity }] }, [Position, Velocity])
621
+ .query({ all: [Position, Velocity] }, [Position, Velocity])
492
622
  .each([Position, Velocity], (e, [pos, vel]) => {
493
623
  pos.x += vel.vx; // pos and vel are non-null
494
624
  });
@@ -512,16 +642,11 @@ Fires once when an entity first matches the system.
512
642
  .enter([Position, Sprite], (e, [pos, sprite]) => {
513
643
  sprite.setPosition(pos.x, pos.y);
514
644
  })
515
-
516
- // Resolve from the entity's parent:
517
- .enter([{ parent: Container }], (e, [container]) => {
518
- container.add(e.get(Sprite)!.gameObject);
519
- });
520
645
  ```
521
646
 
522
647
  #### `.exit(callback)` / `.exit(inject, callback)`
523
648
 
524
- Fires when an entity leaves the system (component removed or entity destroyed). Components removed in the same frame are still resolvable in `inject`.
649
+ Fires when an entity leaves the system (component removed or entity destroyed). Direct components removed in the same frame are still resolvable in `inject`; `up` injection resolves relationship targets live.
525
650
 
526
651
  ```ts
527
652
  .exit([Sprite], (e, [sprite]) => sprite.destroy());
@@ -539,7 +664,7 @@ Fires when `entity.modified(ComponentClass)` is called for the watched component
539
664
  });
540
665
  ```
541
666
 
542
- If `query()` has not been called, `update` automatically expands the implicit `HAS` predicate to require the watched component.
667
+ If `query()` has not been called, `update` automatically expands the implicit component predicate to require the watched component.
543
668
 
544
669
  #### `.each(components, callback)`
545
670
 
@@ -554,7 +679,7 @@ Fires every tick for **every tracked entity**, regardless of whether anything ch
554
679
 
555
680
  #### `.sort(components, compare)`
556
681
 
557
- Store matched entities in a custom order determined by `compare`. Implies `.track()`. Iterating `system.entities`, `forEach`, and `each` walks entities in sorted order.
682
+ Store matched entities in a custom order determined by `compare`. Implies `.track()`. Iterating the system, `forEach`, and `each` walks entities in sorted order.
558
683
 
559
684
  ```ts
560
685
  world
@@ -566,7 +691,7 @@ world
566
691
 
567
692
  #### `.track()`
568
693
 
569
- 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.
694
+ Enable entity tracking without an `each` callback — exposes matched entities via `system.count`, `system.has(e)`, and direct iteration. `each` and `sort` imply `track` automatically. When called after `world.start()`, immediately backfills existing matched entities.
570
695
 
571
696
  #### `.run(callback)`
572
697
 
@@ -596,7 +721,7 @@ Both methods return `this` for chaining and are idempotent (calling `disable()`
596
721
 
597
722
  #### `.destroy()`
598
723
 
599
- **Not supported on `System`** calling it throws. Systems live for the duration of the world. Use a standalone `Query` for temporary reactive sets.
724
+ Permanently remove this system from the world. Calls `disable()` first (clearing the inbox), then removes the system from its phase and unregisters its backing entity. No `exit` callbacks fire. Use a standalone `Query` if you need a temporary reactive set that can be destroyed mid-session.
600
725
 
601
726
  ---
602
727
 
@@ -616,12 +741,13 @@ const projectiles = world
616
741
  world.start();
617
742
 
618
743
  projectiles.forEach((e) => { ... });
619
- console.log(projectiles.entities.size, "active projectiles");
744
+ for (const e of projectiles) { ... }
745
+ console.log(projectiles.count, "active projectiles");
620
746
  ```
621
747
 
622
748
  | Method | Description |
623
749
  | ------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
624
- | `.requires(...components)` | Set the membership predicate to `HAS(...components)` and start tracking. |
750
+ | `.requires(...components)` | Set the membership predicate to require all listed components and start tracking. |
625
751
  | `.query(expr, _guaranteed?)` | Set the membership predicate using a `QueryDSL` expression. |
626
752
  | `.enter(callback)` / `.enter(inject, callback)` | Fires when an entity joins the query. |
627
753
  | `.exit(callback)` / `.exit(inject, callback)` | Fires when an entity leaves the query. |
@@ -629,9 +755,11 @@ console.log(projectiles.entities.size, "active projectiles");
629
755
  | `.sort(components, compare)` | Store matched entities in sorted order. Comparator receives `(entityA, tupleA, entityB, tupleB)`. |
630
756
  | `.track()` | Enable tracking. Backfills when called after `start()`. |
631
757
  | `.belongs(e)` | Returns `true` if the entity satisfies the predicate. |
758
+ | `.count` | Number of currently tracked entities. |
759
+ | `.has(e)` | Returns `true` if the entity is currently tracked. |
760
+ | `[Symbol.iterator]()` | Iterate currently tracked entities with `for (const e of query)`. |
632
761
  | `.forEach(callback)` | Iterate currently tracked entities. |
633
762
  | `.forEach(components, callback)` | Iterate with component injection. |
634
- | `.entities` | `ReadonlySet<Entity>` of currently tracked entities. |
635
763
  | `.destroy()` | Remove the query from the world and from every entity (no exit fires). |
636
764
 
637
765
  #### `.destroy()` semantics
@@ -640,7 +768,7 @@ console.log(projectiles.entities.size, "active projectiles");
640
768
 
641
769
  ```ts
642
770
  const q = world.query("Temporary").requires(Position);
643
- // ... use q.entities ...
771
+ // ... use q.count, q.has(e), or iterate q ...
644
772
  q.destroy();
645
773
  ```
646
774
 
@@ -663,14 +791,14 @@ const f = world.filter([Position, Velocity]);
663
791
 
664
792
  `forEach` runs inside a deferred scope, so mutations made by the callback are batched and become visible after iteration finishes.
665
793
 
666
- **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()`:
794
+ **Type inference.** Component classes the type system can extract from the DSL (plain component classes, plain arrays, `only`, and `all` of those) are non-nullable in the callback tuple. For the rest, supply a `_guaranteed` second argument to `world.filter()`:
667
795
 
668
796
  ```ts
669
797
  // Auto-deduced — both non-null:
670
798
  world.filter([Position, Velocity]).forEach([Position, Velocity], (e, [pos, vel]) => { ... });
671
799
 
672
- // Manual hint for OR / NOT / PARENT / custom function:
673
- world.filter({ OR: [Position, Velocity] }, [Position]).forEach([Position], (e, [pos]) => pos.x);
800
+ // Manual hint for any / not / test:
801
+ world.filter({ any: [Position, Velocity] }, [Position]).forEach([Position], (e, [pos]) => pos.x);
674
802
  ```
675
803
 
676
804
  A `Filter` holds no tracked set, makes no registration calls, and needs no `destroy()`.
@@ -681,18 +809,20 @@ A `Filter` holds no tracked set, makes no registration calls, and needs no `dest
681
809
 
682
810
  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.
683
811
 
684
- | Method | Description |
685
- | ------------------ | ------------------------------------------------------------------------ |
686
- | `add(n)` | Set bit `n`. |
687
- | `addBit(bptr)` | Set the bit at a pre-computed `BitPtr`. |
688
- | `delete(n)` | Clear bit `n`. Trims trailing zero words. |
689
- | `clear()` | Remove every set bit. |
690
- | `has(n)` | Returns `true` if bit `n` is set. |
691
- | `hasBit(bptr)` | Fast check via a pre-computed `BitPtr`. |
692
- | `equal(other)` | Returns `true` when both bitsets have the same bits set. |
693
- | `hasBitset(other)` | Returns `true` when every bit set in `other` is also set in this bitset. |
694
- | `forEach(cb)` | Visit each set bit index in ascending order. |
695
- | `indices()` | Return all set bit indices as a `number[]`. |
812
+ | Method | Description |
813
+ | ------------------ | ------------------------------------------------------------------------------------- |
814
+ | `add(n)` | Set bit `n`. |
815
+ | `addBit(bptr)` | Set the bit at a pre-computed `BitPtr` (fast path). |
816
+ | `delete(n)` | Clear bit `n`. Storage is not compacted automatically; call `compact()` when needed. |
817
+ | `deleteBit(bptr)` | Clear the bit at a pre-computed `BitPtr` (fast path). Does not compact automatically. |
818
+ | `compact()` | Trim trailing zero words from backing storage. |
819
+ | `clear()` | Remove every set bit. |
820
+ | `has(n)` | Returns `true` if bit `n` is set. |
821
+ | `hasBit(bptr)` | Fast check via a pre-computed `BitPtr`. |
822
+ | `equal(other)` | Returns `true` when both bitsets have the same bits set. |
823
+ | `hasBitset(other)` | Returns `true` when every bit set in `other` is also set in this bitset. |
824
+ | `forEach(cb)` | Visit each set bit index in ascending order. |
825
+ | `indices()` | Return all set bit indices as a `number[]`. |
696
826
 
697
827
  ```ts
698
828
  class Tags {