@vworlds/vecs 1.0.20 → 1.0.22

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 (155) hide show
  1. package/README.md +84 -99
  2. package/dist/component.d.ts +5 -2
  3. package/dist/component.js +4 -57
  4. package/dist/component.js.map +1 -1
  5. package/dist/component_meta.d.ts +15 -8
  6. package/dist/component_meta.js +6 -4
  7. package/dist/component_meta.js.map +1 -1
  8. package/dist/dsl.d.ts +56 -11
  9. package/dist/dsl.js +55 -38
  10. package/dist/dsl.js.map +1 -1
  11. package/dist/entity/entity.base.d.ts +4 -3
  12. package/dist/entity/entity.base.js +4 -4
  13. package/dist/entity/entity.base.js.map +1 -1
  14. package/dist/entity/entity.components.d.ts +16 -7
  15. package/dist/entity/entity.components.js +48 -23
  16. package/dist/entity/entity.components.js.map +1 -1
  17. package/dist/entity/entity.d.ts +5 -1
  18. package/dist/entity/entity.js +5 -0
  19. package/dist/entity/entity.js.map +1 -1
  20. package/dist/entity/entity.lifecycle.js +25 -25
  21. package/dist/entity/entity.lifecycle.js.map +1 -1
  22. package/dist/entity/entity.queries.js +0 -6
  23. package/dist/entity/entity.queries.js.map +1 -1
  24. package/dist/entity/entity.relationships.d.ts +1 -1
  25. package/dist/entity/entity.relationships.js +7 -5
  26. package/dist/entity/entity.relationships.js.map +1 -1
  27. package/dist/entity/index.d.ts +1 -1
  28. package/dist/entity/index.js.map +1 -1
  29. package/dist/filter.d.ts +6 -6
  30. package/dist/filter.js +2 -1
  31. package/dist/filter.js.map +1 -1
  32. package/dist/index.d.ts +10 -6
  33. package/dist/index.js +6 -3
  34. package/dist/index.js.map +1 -1
  35. package/dist/inject.d.ts +18 -17
  36. package/dist/inject.js +34 -34
  37. package/dist/inject.js.map +1 -1
  38. package/dist/module.d.ts +4 -4
  39. package/dist/module.js +0 -3
  40. package/dist/module.js.map +1 -1
  41. package/dist/modules/pipeline.d.ts +20 -0
  42. package/dist/modules/pipeline.js +57 -0
  43. package/dist/modules/pipeline.js.map +1 -0
  44. package/dist/modules/relationship_types.d.ts +37 -0
  45. package/dist/modules/relationship_types.js +39 -0
  46. package/dist/modules/relationship_types.js.map +1 -0
  47. package/dist/modules/relationships.d.ts +24 -0
  48. package/dist/modules/relationships.js +82 -0
  49. package/dist/modules/relationships.js.map +1 -0
  50. package/dist/modules/singleton.js +1 -2
  51. package/dist/modules/singleton.js.map +1 -1
  52. package/dist/package.json +2 -2
  53. package/dist/query/callbacks.d.ts +17 -0
  54. package/dist/query/callbacks.js +113 -0
  55. package/dist/query/callbacks.js.map +1 -0
  56. package/dist/query/constants.d.ts +1 -0
  57. package/dist/query/constants.js +2 -0
  58. package/dist/query/constants.js.map +1 -0
  59. package/dist/query/{query.03.tracking.d.ts → group.d.ts} +11 -9
  60. package/dist/query/group.js +31 -0
  61. package/dist/query/group.js.map +1 -0
  62. package/dist/query/grouped_query.d.ts +26 -0
  63. package/dist/query/grouped_query.js +121 -0
  64. package/dist/query/grouped_query.js.map +1 -0
  65. package/dist/query/grouped_query_registry.d.ts +4 -0
  66. package/dist/query/grouped_query_registry.js +11 -0
  67. package/dist/query/grouped_query_registry.js.map +1 -0
  68. package/dist/query/grouping.d.ts +24 -0
  69. package/dist/query/{query.07.groups.js → grouping.js} +29 -60
  70. package/dist/query/grouping.js.map +1 -0
  71. package/dist/query/index.d.ts +6 -6
  72. package/dist/query/index.js +6 -4
  73. package/dist/query/index.js.map +1 -1
  74. package/dist/query/query.d.ts +58 -28
  75. package/dist/query/query.js +334 -88
  76. package/dist/query/query.js.map +1 -1
  77. package/dist/system.d.ts +24 -54
  78. package/dist/system.js +47 -81
  79. package/dist/system.js.map +1 -1
  80. package/dist/terms/all_term.d.ts +2 -6
  81. package/dist/terms/all_term.js +2 -7
  82. package/dist/terms/all_term.js.map +1 -1
  83. package/dist/terms/any_term.d.ts +2 -6
  84. package/dist/terms/any_term.js +2 -7
  85. package/dist/terms/any_term.js.map +1 -1
  86. package/dist/terms/build.d.ts +9 -6
  87. package/dist/terms/build.js +17 -14
  88. package/dist/terms/build.js.map +1 -1
  89. package/dist/terms/component_term.d.ts +3 -3
  90. package/dist/terms/component_term.js.map +1 -1
  91. package/dist/terms/composite_build.d.ts +32 -0
  92. package/dist/terms/composite_build.js +28 -0
  93. package/dist/terms/composite_build.js.map +1 -0
  94. package/dist/terms/only_term.d.ts +3 -3
  95. package/dist/terms/only_term.js.map +1 -1
  96. package/dist/terms/predicate_term.d.ts +3 -3
  97. package/dist/terms/predicate_term.js +3 -3
  98. package/dist/terms/predicate_term.js.map +1 -1
  99. package/dist/terms/target_term.d.ts +3 -2
  100. package/dist/terms/target_term.js +11 -17
  101. package/dist/terms/target_term.js.map +1 -1
  102. package/dist/util/dense_set.js +1 -1
  103. package/dist/util/id_pool.d.ts +2 -0
  104. package/dist/util/id_pool.js +10 -0
  105. package/dist/util/id_pool.js.map +1 -1
  106. package/dist/util/ordered_set.js +15 -10
  107. package/dist/util/ordered_set.js.map +1 -1
  108. package/dist/world/world.components.d.ts +5 -9
  109. package/dist/world/world.components.js +16 -28
  110. package/dist/world/world.components.js.map +1 -1
  111. package/dist/world/world.d.ts +3 -6
  112. package/dist/world/world.entities.d.ts +2 -2
  113. package/dist/world/world.entities.js.map +1 -1
  114. package/dist/world/world.js +10 -10
  115. package/dist/world/world.js.map +1 -1
  116. package/dist/world/world.modules.d.ts +4 -7
  117. package/dist/world/world.modules.js +13 -10
  118. package/dist/world/world.modules.js.map +1 -1
  119. package/dist/world/world.pipeline.d.ts +5 -8
  120. package/dist/world/world.pipeline.js +24 -53
  121. package/dist/world/world.pipeline.js.map +1 -1
  122. package/dist/world/world.pools.d.ts +2 -1
  123. package/dist/world/world.pools.js.map +1 -1
  124. package/dist/world/world.queries.d.ts +3 -5
  125. package/dist/world/world.queries.js +11 -8
  126. package/dist/world/world.queries.js.map +1 -1
  127. package/package.json +2 -2
  128. package/dist/phase.d.ts +0 -29
  129. package/dist/phase.js +0 -24
  130. package/dist/phase.js.map +0 -1
  131. package/dist/query/query.00.base.d.ts +0 -23
  132. package/dist/query/query.00.base.js +0 -77
  133. package/dist/query/query.00.base.js.map +0 -1
  134. package/dist/query/query.01.reactive.d.ts +0 -7
  135. package/dist/query/query.01.reactive.js +0 -58
  136. package/dist/query/query.01.reactive.js.map +0 -1
  137. package/dist/query/query.02.lifecycle.d.ts +0 -6
  138. package/dist/query/query.02.lifecycle.js +0 -63
  139. package/dist/query/query.02.lifecycle.js.map +0 -1
  140. package/dist/query/query.03.tracking.js +0 -31
  141. package/dist/query/query.03.tracking.js.map +0 -1
  142. package/dist/query/query.04.callbacks.d.ts +0 -14
  143. package/dist/query/query.04.callbacks.js +0 -65
  144. package/dist/query/query.04.callbacks.js.map +0 -1
  145. package/dist/query/query.05.updates.d.ts +0 -14
  146. package/dist/query/query.05.updates.js +0 -81
  147. package/dist/query/query.05.updates.js.map +0 -1
  148. package/dist/query/query.06.predicate.d.ts +0 -13
  149. package/dist/query/query.06.predicate.js +0 -40
  150. package/dist/query/query.06.predicate.js.map +0 -1
  151. package/dist/query/query.07.groups.d.ts +0 -41
  152. package/dist/query/query.07.groups.js.map +0 -1
  153. package/dist/relationship.d.ts +0 -19
  154. package/dist/relationship.js +0 -18
  155. package/dist/relationship.js.map +0 -1
package/README.md CHANGED
@@ -14,30 +14,30 @@ yarn add @vworlds/vecs
14
14
 
15
15
  | Concept | What it is |
16
16
  | ------------------------ | ------------------------------------------------------------------------------- |
17
- | **World** | Central container. Owns every entity, query, system, and pipeline phase. |
17
+ | **World** | Central container. Owns every entity, query, system, and flat system pipeline. |
18
18
  | **Component** | A registered plain data class with a no-argument constructor. |
19
19
  | **Entity** | A numeric id with a set of components. Created via the world. |
20
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`). |
21
+ | **System** | A `Query` with per-tick logic (`update`, `each`, `run`). |
22
22
  | **Filter** | A non-reactive, one-shot scan: walks all world entities on each `forEach` call. |
23
23
  | **Hook** | Lightweight `onAdd` / `onRemove` / `onSet` callbacks per component class. |
24
- | **Phase** | Named ordered bucket of systems within the update pipeline. |
24
+ | **Phase** | Marker component for entities used as pipeline ordering anchors. |
25
25
  | **Exclusive components** | A group of components where at most one may exist on any entity at a time. |
26
26
 
27
27
  ### Lifecycle in brief
28
28
 
29
29
  ```
30
- world.component() × N → addPhase() / system() / query() × N → start() → progress() every frame
30
+ world.component() / system() / query() × N → progress() every frame
31
31
  ```
32
32
 
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.
33
+ Components must be registered before they are used as component classes. Components, systems, and standalone queries can be created at any time. Newly-created systems are inserted into the pipeline the next time `world.progress()` runs; standalone queries backfill existing matched entities immediately.
34
34
 
35
35
  ---
36
36
 
37
37
  ## Example
38
38
 
39
39
  ```ts
40
- import { World, type IPhase } from "@vworlds/vecs";
40
+ import { DependsOn, ON_UPDATE, Phase, World } from "@vworlds/vecs";
41
41
 
42
42
  // ─── Components ────────────────────────────────────────────────────────────
43
43
 
@@ -63,18 +63,17 @@ world.component(Position);
63
63
  world.component(Velocity);
64
64
  world.component(Health);
65
65
 
66
- // ─── Phases ────────────────────────────────────────────────────────────────
66
+ // ─── Pipeline anchors ──────────────────────────────────────────────────────
67
67
 
68
- const update: IPhase = world.addPhase("update");
69
- const cleanup: IPhase = world.addPhase("cleanup");
68
+ const update = world.entity(ON_UPDATE);
69
+ const cleanup = world.entity("cleanup").add(Phase).set(DependsOn, { target: update });
70
70
 
71
71
  // ─── Systems ───────────────────────────────────────────────────────────────
72
72
 
73
73
  // MoveSystem: integrates Velocity into Position every tick.
74
74
  world
75
75
  .system("Move")
76
- .phase(update)
77
- .requires(Position, Velocity)
76
+ .with(Position, Velocity)
78
77
  .each([Position, Velocity], (e, [pos, vel]) => {
79
78
  pos.x += vel.vx;
80
79
  pos.y += vel.vy;
@@ -85,7 +84,7 @@ world
85
84
  world
86
85
  .system("Health")
87
86
  .phase(cleanup)
88
- .requires(Health)
87
+ .with(Health)
89
88
  .update(Health, (entity, health) => {
90
89
  if (health.hp <= 0) {
91
90
  entity.destroy();
@@ -99,10 +98,6 @@ world
99
98
  .onAdd((entity, h) => console.log(`entity ${entity.eid} spawned with hp=${h.hp}`))
100
99
  .onRemove((entity) => console.log(`entity ${entity.eid} died`));
101
100
 
102
- // ─── Start ─────────────────────────────────────────────────────────────────
103
-
104
- world.start(); // freeze registration, distribute systems into phases
105
-
106
101
  // ─── Spawn entities ────────────────────────────────────────────────────────
107
102
 
108
103
  world.entity().set(Position, { x: 0, y: 0 }).set(Velocity, { vx: 5, vy: 0 }).set(Health, { hp: 3 });
@@ -145,7 +140,7 @@ const world = new World();
145
140
 
146
141
  #### Component registration
147
142
 
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`.
143
+ 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`, `with`, `filter`, hook registration, or `setExclusiveComponents`. Registration can happen at any time.
149
144
 
150
145
  ```ts
151
146
  class Position {
@@ -160,7 +155,7 @@ const positionComponent = world.component(Position);
160
155
  world.component(Position, 42);
161
156
 
162
157
  // Access component metadata (numeric id, cleanup policy, etc.):
163
- const meta = world.component(Position).ownMeta; // ComponentMeta
158
+ const componentMeta = world.component(Position).meta; // ComponentMeta
164
159
 
165
160
  // Give the component a name so it can be looked up by string later:
166
161
  world.component(Position).name = "Position";
@@ -177,7 +172,7 @@ Id ranges are configured by passing `idPools` to the `World` constructor. The bu
177
172
  | `module` | 900 – 999 |
178
173
  | `entity` | 1000 – ∞ |
179
174
 
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.
175
+ There is no automatic component registration; using an unregistered component class as a component is an error.
181
176
 
182
177
  #### Exclusive component groups
183
178
 
@@ -189,7 +184,7 @@ e.add(Walking);
189
184
  e.add(Running); // Walking is automatically removed first
190
185
  ```
191
186
 
192
- 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()`.
187
+ 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 at any time.
193
188
 
194
189
  #### Entity management
195
190
 
@@ -222,7 +217,7 @@ world.component("Position"); // resolves the same component entity
222
217
 
223
218
  #### Component entity deletion cleanup
224
219
 
225
- Every component key is backed by a component entity. Destroying that component entity is controlled by `world.component(C).ownMeta.onDelete`:
220
+ Every component key is backed by a component entity. Destroying that component entity is controlled by `world.component(C).meta.onDelete`:
226
221
 
227
222
  | Policy | Meaning |
228
223
  | ---------------------- | -------------------------------------------------------------------------------------------------- |
@@ -233,7 +228,7 @@ Every component key is backed by a component entity. Destroying that component e
233
228
  ```ts
234
229
  import { CleanupPolicy } from "@vworlds/vecs";
235
230
 
236
- world.component(Temporary).ownMeta.onDelete = CleanupPolicy.Remove;
231
+ world.component(Temporary).meta.onDelete = CleanupPolicy.Remove;
237
232
  world.component(Temporary).destroy(); // strips Temporary from all carriers
238
233
  ```
239
234
 
@@ -251,36 +246,28 @@ world
251
246
 
252
247
  `onAdd` fires when the component is first attached. `onRemove` fires when it is removed (or the entity is destroyed). `onSet` fires whenever `entity.modified(C)` is called, when `entity.set(C, props)` applies data, and when `entity.attach(instance)` stores an existing instance. Hook callbacks receive the owning entity because component instances do not carry entity references.
253
248
 
254
- #### Phases
249
+ #### Pipeline
255
250
 
256
251
  ```ts
257
- const preUpdate = world.addPhase("preupdate");
258
- const update = world.addPhase("update");
259
- const send = world.addPhase("send");
252
+ const update = world.entity("vecs::OnUpdate");
253
+ const send = world.entity("send").add(Phase).set(DependsOn, { target: update });
260
254
 
261
- // Drive every phase in registration order:
262
- world.progress(now, delta);
255
+ world.system("Send").phase(send).run(sendPackets);
263
256
 
264
- // ...or run individual phases manually:
265
- world.beginFrame(delta); // one numeric arg: ms since last frame
266
- try {
267
- world.runPhase(preUpdate, now, delta);
268
- world.runPhase(update, now, delta);
269
- world.runPhase(send, now, delta);
270
- } finally {
271
- world.endFrame();
272
- }
257
+ // Drive every system returned by the pipeline query:
258
+ world.progress(now, delta);
273
259
  ```
274
260
 
275
- Systems with no explicit phase are placed in the built-in `"update"` phase.
261
+ Systems default to `vecs::OnUpdate`; call `.phase(...)` only when a system needs another pipeline anchor. The default pipeline query matches systems whose backing entity `DependsOn` a `Phase`, ordered by the `DependsOn` cascade. Built-in phase entity names are namespaced: `vecs::OnLoad`, `vecs::PostLoad`, `vecs::PreUpdate`, `vecs::OnUpdate`, `vecs::OnValidate`, `vecs::PostUpdate`, `vecs::PreStore`, and `vecs::OnStore`.
262
+
263
+ Use `world.setPipeline(spec)` to replace the default. `World` appends `System` to the query, so the query result is always flattened to systems.
276
264
 
277
265
  #### Systems
278
266
 
279
267
  ```ts
280
268
  world
281
269
  .system("MySystem")
282
- .phase("update")
283
- .requires(A, B)
270
+ .with(A, B)
284
271
  .enter(...)
285
272
  .update(...)
286
273
  .each(...)
@@ -353,14 +340,13 @@ Tick source objects and systems can both be used as sources. Disabling a source
353
340
  ```ts
354
341
  const enemies = world
355
342
  .query("Enemies")
356
- .requires(Enemy, Health)
343
+ .with(Enemy, Health)
357
344
  .enter((e) => console.log("enemy spawned", e.eid));
358
345
 
359
- world.start();
360
346
  // enemies.count and query iteration are kept up-to-date automatically.
361
347
 
362
- // Standalone queries can also be created after start(); existing matched
363
- // entities are backfilled immediately.
348
+ // Standalone queries can also be created later; existing matched entities are
349
+ // backfilled immediately.
364
350
  ```
365
351
 
366
352
  #### Filters
@@ -380,17 +366,16 @@ world.filter({ all: [Position, Velocity] }).forEach([Position, Velocity], (e, [p
380
366
  });
381
367
 
382
368
  // Manual hint for queries the type extractor can't see through:
383
- world.filter({ any: [Position, Velocity] }, [Position]).forEach([Position], (e, [pos]) => pos.x);
369
+ world
370
+ .filter({ with: { any: [Position, Velocity] }, hint: [Position] })
371
+ .forEach([Position], (e, [pos]) => pos.x);
384
372
  ```
385
373
 
386
- A `Filter` requires no name, no `world.start()`, and no `destroy()` — create it anywhere and discard freely.
374
+ A `Filter` requires no name and no `destroy()` — create it anywhere and discard freely.
387
375
 
388
376
  #### Pipeline control
389
377
 
390
378
  ```ts
391
- world.start(); // freeze registration, distribute systems
392
- world.disableComponentRegistration(); // freeze registration without sorting
393
-
394
379
  world.flush(); // drain queued top-level mutations
395
380
  world.defer(() => { ... }); // run a block in deferred mode
396
381
  world.beginDefer(); // pair with endDefer() for finer scoping
@@ -433,7 +418,7 @@ entity.get(Position) === shared; // true
433
418
  | Shared instances possible | `entity.attach(instance)` stores the exact passed object; code should use the entity passed by vecs callbacks. |
434
419
  | Manual dirty marking | After mutating fields directly, call `entity.modified(C)` to notify hooks, queries, and systems. |
435
420
 
436
- Use `world.component(C).ownMeta` when you need metadata such as the numeric type id or component name. Metadata is world-specific.
421
+ Use `world.component(C).meta` when you need metadata such as the numeric type id or component name. Metadata is world-specific.
437
422
 
438
423
  ---
439
424
 
@@ -495,14 +480,14 @@ item.parent(EquippedBy); // player
495
480
  player.children(EquippedBy).has(item); // true
496
481
  ```
497
482
 
498
- Relationship target cleanup is controlled by `ownMeta.onDeleteTarget`:
483
+ Relationship target cleanup is controlled by `meta.onDeleteTarget`:
499
484
 
500
485
  | Policy | Meaning |
501
486
  | ---------------------- | -------------------------------------------------------------------------------------------- |
502
487
  | `CleanupPolicy.Remove` | Default. When a target is destroyed, remove the relationship component from each source. |
503
488
  | `CleanupPolicy.Delete` | When a target is destroyed, destroy each source entity that targets it through the relation. |
504
489
 
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.
490
+ `ChildOf` is registered by every `World` and uses `CleanupPolicy.Delete`. Custom relationships default to `Remove`; set `world.component(MyRelationship).meta.onDeleteTarget = CleanupPolicy.Delete` if target deletion should cascade.
506
491
 
507
492
  `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
493
 
@@ -511,8 +496,8 @@ Retarget relationships with `entity.set(RelationshipClass, { target })`. `entity
511
496
  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
497
 
513
498
  ```ts
514
- world.query("body-friends").query({ all: [Body, { target: [FriendOf, [Position, Velocity]] }] });
515
- world.query("positioned-children").query({ all: [Position, { parent: Body }] });
499
+ world.query("body-friends").with({ all: [Body, { target: [FriendOf, [Position, Velocity]] }] });
500
+ world.query("positioned-children").with({ all: [Position, { parent: Body }] });
516
501
  ```
517
502
 
518
503
  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.
@@ -530,7 +515,7 @@ Component injection supports lowercase `down` in `forEach` and system `each` inj
530
515
  ```ts
531
516
  world
532
517
  .query("parents")
533
- .requires(Body)
518
+ .with(Body)
534
519
  .forEach([Body, { down: [ChildOf, [Position]] }], (parent, [body, childPos]) => {
535
520
  // One call per ChildOf child of parent. childPos is undefined when that child lacks Position.
536
521
  });
@@ -547,17 +532,18 @@ Two details matter:
547
532
 
548
533
  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.
549
534
 
550
- #### `.requires(...components)` and `.query(q)`
535
+ #### `.with(...specs)`
551
536
 
552
537
  Declare which entities the system tracks.
553
538
 
554
539
  ```ts
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
540
+ .with(Position, Velocity) // explicit component list
541
+ .with([Position, Velocity]) // array shorthand
542
+ .with({ all: [Position, { any: [Sprite, Container] }] }) // compound
543
+ .with({ not: Invisible })
544
+ .without(Invisible) // shorthand for .with({ not: Invisible })
545
+ .with({ target: [FriendOf, Position] }) // relationship target has Position
546
+ .with({ parent: Body }) // ChildOf target has Body
561
547
  ```
562
548
 
563
549
  **Query operators:**
@@ -578,31 +564,31 @@ Declare which entities the system tracks.
578
564
 
579
565
  `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
566
 
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:
567
+ Component injection also supports lowercase relationship markers. `target` 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
568
 
583
569
  ```ts
584
570
  world
585
571
  .query("friends")
586
- .query({ target: [FriendOf, Position] })
587
- .forEach([Body, { up: [FriendOf, [Position, Velocity]] }], (e, [body, pos, vel]) => {
572
+ .with({ target: [FriendOf, Position] })
573
+ .forEach([Body, { target: [FriendOf, [Position, Velocity]] }], (e, [body, pos, vel]) => {
588
574
  // body is from e; pos and vel are from e.parent(FriendOf)
589
575
  // target-side injected components are undefined if the target is absent or lacks them.
590
576
  });
591
577
  ```
592
578
 
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.
579
+ On `exit`, direct component injection is snapshot-stable when the exiting component was just removed. `target` 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.
594
580
 
595
581
  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
582
 
597
583
  #### Iter cursor
598
584
 
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:
585
+ Pass `Iter` as the first argument to `each`, `forEach`, `enter`, `exit`, `update`, or `orderBy` to receive a reusable cursor object instead of the bare entity. The cursor exposes:
600
586
 
601
587
  - `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.
588
+ - `it.src` — a same-length array of source entities: the visited entity for a directly-injected component, the relationship target for a `target`-injected one, or the specific child for a `down`-injected one.
603
589
 
604
590
  ```ts
605
- system.each(Iter, [Body, { up: [ChildOf, [Position]] }], (it, [body, pos]) => {
591
+ system.each(Iter, [Body, { target: [ChildOf, [Position]] }], (it, [body, pos]) => {
606
592
  it.entity; // the visited entity
607
593
  it.src[0]; // entity `body` was read from (the visited entity)
608
594
  it.src[1]; // entity `pos` was read from (the ChildOf target)
@@ -611,26 +597,27 @@ system.each(Iter, [Body, { up: [ChildOf, [Position]] }], (it, [body, pos]) => {
611
597
 
612
598
  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
599
 
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.
600
+ `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; `orderBy` 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
601
 
616
602
  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.
617
603
 
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:
604
+ **Type inference.** `with()` records inferrable component classes as a type parameter `R` on the system. Callbacks in `.orderBy()`, `.each()`, and `.update()` injection treat those components as non-nullable — no `!` needed. For complex `with()` expressions the type system can't introspect, supply a `hint` field:
619
605
 
620
606
  ```ts
621
- .query({ all: [Position, Velocity] }, [Position, Velocity])
607
+ .with({ with: { all: [Position, Velocity] }, hint: [Position, Velocity] })
622
608
  .each([Position, Velocity], (e, [pos, vel]) => {
623
609
  pos.x += vel.vx; // pos and vel are non-null
624
610
  });
625
611
  ```
626
612
 
627
- #### `.phase(p)`
613
+ #### `.phase(anchor)`
628
614
 
629
- Assign the system to a phase by name or `IPhase` reference. Default phase is `"update"`.
615
+ Place the system after a pipeline anchor by name or entity. Sets `DependsOn` on the system's backing entity (replacing any previous target) and tags the anchor with `Phase`, so it is picked up and ordered by the default pipeline query. Systems default to `vecs::OnUpdate`, so call this only to run elsewhere.
630
616
 
631
617
  ```ts
632
- .phase("preupdate")
633
- .phase(myPhase)
618
+ .phase(ON_LOAD) // a built-in phase constant
619
+ .phase("custom") // a custom anchor, created and tagged if missing
620
+ .phase(world.entity("custom")) // an existing entity
634
621
  ```
635
622
 
636
623
  #### `.enter(callback)` / `.enter(inject, callback)`
@@ -646,7 +633,7 @@ Fires once when an entity first matches the system.
646
633
 
647
634
  #### `.exit(callback)` / `.exit(inject, callback)`
648
635
 
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.
636
+ Fires when an entity leaves the system (component removed or entity destroyed). Direct components removed in the same frame are still resolvable in `inject`; `target` injection resolves relationship targets live.
650
637
 
651
638
  ```ts
652
639
  .exit([Sprite], (e, [sprite]) => sprite.destroy());
@@ -664,38 +651,38 @@ Fires when `entity.modified(ComponentClass)` is called for the watched component
664
651
  });
665
652
  ```
666
653
 
667
- If `query()` has not been called, `update` automatically expands the implicit component predicate to require the watched component.
654
+ If `with()` has not been called, `update` automatically expands the implicit component predicate to require the watched component.
668
655
 
669
656
  #### `.each(components, callback)`
670
657
 
671
658
  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.
672
659
 
673
660
  ```ts
674
- .requires(Position, Velocity)
661
+ .with(Position, Velocity)
675
662
  .each([Position, Velocity], (e, [pos, vel]) => {
676
663
  pos.x += vel.vx;
677
664
  });
678
665
  ```
679
666
 
680
- #### `.sort(components, compare)`
667
+ #### `.orderBy(components, compare)`
681
668
 
682
669
  Store matched entities in a custom order determined by `compare`. Implies `.track()`. Iterating the system, `forEach`, and `each` walks entities in sorted order.
683
670
 
684
671
  ```ts
685
672
  world
686
673
  .system("Render")
687
- .requires(Position, Sprite)
688
- .sort([Position], (_entityA, [posA], _entityB, [posB]) => posA.z - posB.z)
674
+ .with(Position, Sprite)
675
+ .orderBy([Position], (_entityA, [posA], _entityB, [posB]) => posA.z - posB.z)
689
676
  .each([Position, Sprite], (e, [pos, sprite]) => sprite.draw(pos.x, pos.y));
690
677
  ```
691
678
 
692
679
  #### `.track()`
693
680
 
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.
681
+ Enable entity tracking without an `each` callback — exposes matched entities via `system.count`, `system.has(e)`, and direct iteration. `each` and `orderBy` imply `track` automatically. Backfills existing matched entities when the system is built.
695
682
 
696
683
  #### `.run(callback)`
697
684
 
698
- Fires every tick when the system's phase runs, regardless of entity state. Use for polling, network I/O, timers, etc.
685
+ Fires every tick when the system runs in the pipeline, regardless of entity state. Use for polling, network I/O, timers, etc.
699
686
 
700
687
  ```ts
701
688
  .run((now, delta) => {
@@ -708,7 +695,7 @@ Fires every tick when the system's phase runs, regardless of entity state. Use f
708
695
  Pause and resume a system at runtime. While disabled the system is effectively invisible: the inbox is cleared immediately, any new `enter`, `exit`, or `update` events are silently dropped, `run` and `each` callbacks do not fire, and the system skips its `_run` entirely. Entity membership in the underlying query is still maintained, so the tracked set remains correct and the system resumes cleanly when re-enabled. Events that occurred while the system was disabled are **not** replayed.
709
696
 
710
697
  ```ts
711
- const ai = world.system("AI").requires(Enemy).run(tickAI);
698
+ const ai = world.system("AI").with(Enemy).run(tickAI);
712
699
 
713
700
  // Pause AI processing during a cutscene:
714
701
  ai.disable();
@@ -721,25 +708,23 @@ Both methods return `this` for chaining and are idempotent (calling `disable()`
721
708
 
722
709
  #### `.destroy()`
723
710
 
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.
711
+ Permanently remove this system from the world. Calls `disable()` first (clearing the inbox), then 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.
725
712
 
726
713
  ---
727
714
 
728
715
  ### `Query`
729
716
 
730
- `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.
717
+ `world.query(name)` returns a standalone reactive entity set, configured through the same builder API as `System`. It has no per-tick callbacks.
731
718
 
732
719
  ```ts
733
720
  const projectiles = world
734
721
  .query("Projectiles")
735
- .requires(Position, Velocity)
736
- .sort([Position], (_entityA, [a], _entityB, [b]) => a.z - b.z)
722
+ .with(Position, Velocity)
723
+ .orderBy([Position], (_entityA, [a], _entityB, [b]) => a.z - b.z)
737
724
  .enter([Position], (e, [pos]) => {
738
725
  pos.x = spawnX;
739
726
  });
740
727
 
741
- world.start();
742
-
743
728
  projectiles.forEach((e) => { ... });
744
729
  for (const e of projectiles) { ... }
745
730
  console.log(projectiles.count, "active projectiles");
@@ -747,13 +732,13 @@ console.log(projectiles.count, "active projectiles");
747
732
 
748
733
  | Method | Description |
749
734
  | ------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
750
- | `.requires(...components)` | Set the membership predicate to require all listed components and start tracking. |
751
- | `.query(expr, _guaranteed?)` | Set the membership predicate using a `QueryDSL` expression. |
735
+ | `.with(...specs)` | Add membership predicates using `QuerySpec` expressions. |
736
+ | `.without(dsl)` | Add a negated membership predicate; shorthand for `.with({ not: dsl })`. |
752
737
  | `.enter(callback)` / `.enter(inject, callback)` | Fires when an entity joins the query. |
753
738
  | `.exit(callback)` / `.exit(inject, callback)` | Fires when an entity leaves the query. |
754
739
  | `.update(C, callback)` / `.update(C, inject, callback)` | Fires when `C` is modified on a tracked entity. Callback receives `(entity, component, injected?)`. |
755
- | `.sort(components, compare)` | Store matched entities in sorted order. Comparator receives `(entityA, tupleA, entityB, tupleB)`. |
756
- | `.track()` | Enable tracking. Backfills when called after `start()`. |
740
+ | `.orderBy(components, compare)` | Store matched entities in sorted order. Comparator receives `(entityA, tupleA, entityB, tupleB)`. |
741
+ | `.track()` | Enable tracking. Backfills existing matches when the query is built. |
757
742
  | `.belongs(e)` | Returns `true` if the entity satisfies the predicate. |
758
743
  | `.count` | Number of currently tracked entities. |
759
744
  | `.has(e)` | Returns `true` if the entity is currently tracked. |
@@ -767,12 +752,12 @@ console.log(projectiles.count, "active projectiles");
767
752
  `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**.
768
753
 
769
754
  ```ts
770
- const q = world.query("Temporary").requires(Position);
755
+ const q = world.query("Temporary").with(Position);
771
756
  // ... use q.count, q.has(e), or iterate q ...
772
757
  q.destroy();
773
758
  ```
774
759
 
775
- `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.
760
+ `System` shares the same DSL, callback, sorting, and tracking machinery — `System` extends `Query` and adds `run`, `each`, and an inbox replayed on every tick.
776
761
 
777
762
  ---
778
763
 
@@ -791,14 +776,14 @@ const f = world.filter([Position, Velocity]);
791
776
 
792
777
  `forEach` runs inside a deferred scope, so mutations made by the callback are batched and become visible after iteration finishes.
793
778
 
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()`:
779
+ **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 `hint` field:
795
780
 
796
781
  ```ts
797
782
  // Auto-deduced — both non-null:
798
783
  world.filter([Position, Velocity]).forEach([Position, Velocity], (e, [pos, vel]) => { ... });
799
784
 
800
785
  // Manual hint for any / not / test:
801
- world.filter({ any: [Position, Velocity] }, [Position]).forEach([Position], (e, [pos]) => pos.x);
786
+ world.filter({ with: { any: [Position, Velocity] }, hint: [Position] }).forEach([Position], (e, [pos]) => pos.x);
802
787
  ```
803
788
 
804
789
  A `Filter` holds no tracked set, makes no registration calls, and needs no `destroy()`.
@@ -1,6 +1,6 @@
1
1
  import { Entity } from "./entity/index.js";
2
- import type { ComponentType } from "./component_meta.js";
3
- export { CleanupPolicy, ComponentMeta, type ComponentClass, type ComponentInstance, type ComponentRef, type ComponentRefArray, type ComponentType, type Hook, } from "./component_meta.js";
2
+ import type { ComponentInstance, ComponentType } from "./component_meta.js";
3
+ export { CleanupPolicy, ComponentMeta, Relationship, type ComponentClass, type ComponentInstance, type ComponentRef, type ComponentRefArray, type ComponentType, type Hook, } from "./component_meta.js";
4
4
  /** A component entity created by {@link World.component}. */
5
5
  export declare class Component<T extends ComponentType = ComponentType> extends Entity {
6
6
  /**
@@ -19,6 +19,7 @@ export declare class Component<T extends ComponentType = ComponentType> extends
19
19
  * ```
20
20
  */
21
21
  onAdd(handler: (entity: Entity, c: InstanceType<T>) => void): this;
22
+ onAdd(handler: (entity: Entity, c: ComponentInstance) => void): this;
22
23
  /**
23
24
  * Register a callback fired each time this component is **removed** from an
24
25
  * entity, or when the owning entity is destroyed.
@@ -36,6 +37,7 @@ export declare class Component<T extends ComponentType = ComponentType> extends
36
37
  * ```
37
38
  */
38
39
  onRemove(handler: (entity: Entity, c: InstanceType<T>) => void): this;
40
+ onRemove(handler: (entity: Entity, c: ComponentInstance) => void): this;
39
41
  /**
40
42
  * Register a callback fired each time this component's data is **set or
41
43
  * modified**.
@@ -59,4 +61,5 @@ export declare class Component<T extends ComponentType = ComponentType> extends
59
61
  * ```
60
62
  */
61
63
  onSet(handler: (entity: Entity, c: InstanceType<T>) => void): this;
64
+ onSet(handler: (entity: Entity, c: ComponentInstance) => void): this;
62
65
  }
package/dist/component.js CHANGED
@@ -1,70 +1,17 @@
1
1
  import { Entity } from "./entity/index.js";
2
- export { CleanupPolicy, ComponentMeta, } from "./component_meta.js";
2
+ export { CleanupPolicy, ComponentMeta, Relationship, } from "./component_meta.js";
3
3
  /** A component entity created by {@link World.component}. */
4
4
  export class Component extends Entity {
5
- /**
6
- * Register a callback fired each time this component is **added** to an entity.
7
- *
8
- * The handler is called with the owning entity and the freshly created
9
- * component instance. Use it to initialise external state (e.g. scene objects)
10
- * that should exist for the lifetime of the component.
11
- *
12
- * @param handler - Callback invoked with `(entity, componentInstance)`.
13
- * @returns This `Component` entity, for chaining.
14
- *
15
- * @example
16
- * ```ts
17
- * world.component(Sprite).onAdd((entity, sprite) => sprite.initialize(scene));
18
- * ```
19
- */
20
5
  onAdd(handler) {
21
- this.ownMeta.onAdd(handler);
6
+ super.onAdd(handler);
22
7
  return this;
23
8
  }
24
- /**
25
- * Register a callback fired each time this component is **removed** from an
26
- * entity, or when the owning entity is destroyed.
27
- *
28
- * The handler is called with the owning entity and the component instance that
29
- * is about to be detached. Use it to clean up any external state associated
30
- * with the component.
31
- *
32
- * @param handler - Callback invoked with `(entity, componentInstance)`.
33
- * @returns This `Component` entity, for chaining.
34
- *
35
- * @example
36
- * ```ts
37
- * world.component(Sprite).onRemove((entity, sprite) => sprite.destroy(scene));
38
- * ```
39
- */
40
9
  onRemove(handler) {
41
- this.ownMeta.onRemove(handler);
10
+ super.onRemove(handler);
42
11
  return this;
43
12
  }
44
- /**
45
- * Register a callback fired each time this component's data is **set or
46
- * modified**.
47
- *
48
- * Fires when:
49
- * - `entity.set(C, props)` applies data to the component.
50
- * - `entity.attach(instance)` stores an existing instance.
51
- * - `entity.modified(C)` is called after in-place mutation.
52
- * - A system's `getMut(C)` marks the component dirty.
53
- *
54
- * The handler receives the owning entity and the current component instance.
55
- * Component instances do not carry entity references, which is why the entity
56
- * is passed explicitly.
57
- *
58
- * @param handler - Callback invoked with `(entity, componentInstance)`.
59
- * @returns This `Component` entity, for chaining.
60
- *
61
- * @example
62
- * ```ts
63
- * world.component(Transform).onSet((entity, t) => renderer.syncTransform(entity.eid, t));
64
- * ```
65
- */
66
13
  onSet(handler) {
67
- this.ownMeta.onSet(handler);
14
+ super.onSet(handler);
68
15
  return this;
69
16
  }
70
17
  }
@@ -1 +1 @@
1
- {"version":3,"file":"component.js","sourceRoot":"","sources":["../src/component.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAG3C,OAAO,EACL,aAAa,EACb,aAAa,GAOd,MAAM,qBAAqB,CAAC;AAE7B,6DAA6D;AAC7D,MAAM,OAAO,SAAmD,SAAQ,MAAM;IAC5E;;;;;;;;;;;;;;OAcG;IACI,KAAK,CAAC,OAAqD;QAChE,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,OAAyD,CAAC,CAAC;QAC9E,OAAO,IAAI,CAAC;IACd,CAAC;IAED;;;;;;;;;;;;;;;OAeG;IACI,QAAQ,CAAC,OAAqD;QACnE,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,OAAyD,CAAC,CAAC;QACjF,OAAO,IAAI,CAAC;IACd,CAAC;IAED;;;;;;;;;;;;;;;;;;;;;OAqBG;IACI,KAAK,CAAC,OAAqD;QAChE,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,OAAyD,CAAC,CAAC;QAC9E,OAAO,IAAI,CAAC;IACd,CAAC;CACF"}
1
+ {"version":3,"file":"component.js","sourceRoot":"","sources":["../src/component.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAG3C,OAAO,EACL,aAAa,EACb,aAAa,EACb,YAAY,GAOb,MAAM,qBAAqB,CAAC;AAE7B,6DAA6D;AAC7D,MAAM,OAAO,SAAmD,SAAQ,MAAM;IAkB5D,KAAK,CAAC,OAAyC;QAC7D,KAAK,CAAC,KAAK,CAAC,OAAyD,CAAC,CAAC;QACvE,OAAO,IAAI,CAAC;IACd,CAAC;IAoBe,QAAQ,CAAC,OAAyC;QAChE,KAAK,CAAC,QAAQ,CAAC,OAAyD,CAAC,CAAC;QAC1E,OAAO,IAAI,CAAC;IACd,CAAC;IA0Be,KAAK,CAAC,OAAyC;QAC7D,KAAK,CAAC,KAAK,CAAC,OAAyD,CAAC,CAAC;QACvE,OAAO,IAAI,CAAC;IACd,CAAC;CACF"}