@vworlds/vecs 1.0.44 → 1.0.46

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 (136) hide show
  1. package/README.md +7 -1
  2. package/dist/component.d.ts +57 -51
  3. package/dist/component.js +25 -9
  4. package/dist/component.js.map +1 -1
  5. package/dist/component_meta.d.ts +71 -66
  6. package/dist/component_meta.js +331 -34
  7. package/dist/component_meta.js.map +1 -1
  8. package/dist/dsl.js +1 -1
  9. package/dist/dsl.js.map +1 -1
  10. package/dist/entity/entity.base.js +4 -0
  11. package/dist/entity/entity.base.js.map +1 -1
  12. package/dist/entity/entity.components.d.ts +39 -27
  13. package/dist/entity/entity.components.js +258 -74
  14. package/dist/entity/entity.components.js.map +1 -1
  15. package/dist/entity/entity.d.ts +1 -2
  16. package/dist/entity/entity.js +2 -3
  17. package/dist/entity/entity.js.map +1 -1
  18. package/dist/entity/entity.lifecycle.d.ts +7 -1
  19. package/dist/entity/entity.lifecycle.js +40 -53
  20. package/dist/entity/entity.lifecycle.js.map +1 -1
  21. package/dist/entity/entity.queries.js +30 -0
  22. package/dist/entity/entity.queries.js.map +1 -1
  23. package/dist/entity/entity.relationships.d.ts +4 -0
  24. package/dist/entity/entity.relationships.js +18 -10
  25. package/dist/entity/entity.relationships.js.map +1 -1
  26. package/dist/index.d.ts +7 -4
  27. package/dist/index.js +4 -2
  28. package/dist/index.js.map +1 -1
  29. package/dist/inject.d.ts +63 -14
  30. package/dist/inject.js +165 -89
  31. package/dist/inject.js.map +1 -1
  32. package/dist/modules/identity.js +2 -2
  33. package/dist/modules/identity.js.map +1 -1
  34. package/dist/modules/implements.js +2 -2
  35. package/dist/modules/implements.js.map +1 -1
  36. package/dist/modules/pipeline.js +5 -5
  37. package/dist/modules/pipeline.js.map +1 -1
  38. package/dist/modules/relationship_types.d.ts +70 -3
  39. package/dist/modules/relationship_types.js +70 -3
  40. package/dist/modules/relationship_types.js.map +1 -1
  41. package/dist/modules/relationships.d.ts +31 -8
  42. package/dist/modules/relationships.js +120 -30
  43. package/dist/modules/relationships.js.map +1 -1
  44. package/dist/modules/singleton.js +2 -2
  45. package/dist/modules/singleton.js.map +1 -1
  46. package/dist/modules/snapshot.d.ts +26 -0
  47. package/dist/modules/snapshot.js +45 -0
  48. package/dist/modules/snapshot.js.map +1 -0
  49. package/dist/modules/snapshot_types.d.ts +85 -0
  50. package/dist/modules/snapshot_types.js +130 -0
  51. package/dist/modules/snapshot_types.js.map +1 -0
  52. package/dist/package.json +2 -2
  53. package/dist/query/callbacks.d.ts +9 -7
  54. package/dist/query/callbacks.js +27 -41
  55. package/dist/query/callbacks.js.map +1 -1
  56. package/dist/query/grouped_query.js +7 -7
  57. package/dist/query/grouped_query.js.map +1 -1
  58. package/dist/query/grouping.d.ts +6 -2
  59. package/dist/query/grouping.js +84 -15
  60. package/dist/query/grouping.js.map +1 -1
  61. package/dist/query/query.d.ts +4 -4
  62. package/dist/query/query.js +37 -14
  63. package/dist/query/query.js.map +1 -1
  64. package/dist/snapshot/capture.d.ts +1 -0
  65. package/dist/snapshot/capture.js +212 -0
  66. package/dist/snapshot/capture.js.map +1 -0
  67. package/dist/snapshot/clone.d.ts +1 -0
  68. package/dist/snapshot/clone.js +107 -0
  69. package/dist/snapshot/clone.js.map +1 -0
  70. package/dist/snapshot/index.d.ts +5 -0
  71. package/dist/snapshot/index.js +21 -0
  72. package/dist/snapshot/index.js.map +1 -0
  73. package/dist/snapshot/preflight.d.ts +1 -0
  74. package/dist/snapshot/preflight.js +40 -0
  75. package/dist/snapshot/preflight.js.map +1 -0
  76. package/dist/snapshot/restore.d.ts +1 -0
  77. package/dist/snapshot/restore.js +111 -0
  78. package/dist/snapshot/restore.js.map +1 -0
  79. package/dist/snapshot/types.d.ts +135 -0
  80. package/dist/snapshot/types.js +26 -0
  81. package/dist/snapshot/types.js.map +1 -0
  82. package/dist/system.d.ts +3 -1
  83. package/dist/system.js +33 -23
  84. package/dist/system.js.map +1 -1
  85. package/dist/terms/all_term.js +19 -0
  86. package/dist/terms/all_term.js.map +1 -1
  87. package/dist/terms/any_term.js +19 -0
  88. package/dist/terms/any_term.js.map +1 -1
  89. package/dist/terms/not_term.js +4 -0
  90. package/dist/terms/not_term.js.map +1 -1
  91. package/dist/terms/target_term.d.ts +1 -2
  92. package/dist/terms/target_term.js +2 -4
  93. package/dist/terms/target_term.js.map +1 -1
  94. package/dist/terms/term.d.ts +2 -0
  95. package/dist/terms/term.js +59 -10
  96. package/dist/terms/term.js.map +1 -1
  97. package/dist/util/array_map.d.ts +43 -12
  98. package/dist/util/array_map.js +90 -41
  99. package/dist/util/array_map.js.map +1 -1
  100. package/dist/util/dense_set.js +26 -0
  101. package/dist/util/dense_set.js.map +1 -1
  102. package/dist/world/world.base.js +12 -0
  103. package/dist/world/world.base.js.map +1 -1
  104. package/dist/world/world.d.ts +7 -0
  105. package/dist/world/world.deferred.js +17 -0
  106. package/dist/world/world.deferred.js.map +1 -1
  107. package/dist/world/world.entities.d.ts +13 -2
  108. package/dist/world/world.entities.js +16 -2
  109. package/dist/world/world.entities.js.map +1 -1
  110. package/dist/world/world.js +15 -3
  111. package/dist/world/world.js.map +1 -1
  112. package/dist/world/world.pipeline.d.ts +2 -2
  113. package/dist/world/world.pipeline.js +4 -2
  114. package/dist/world/world.pipeline.js.map +1 -1
  115. package/dist/world/world.queries.d.ts +7 -0
  116. package/dist/world/world.queries.js +52 -0
  117. package/dist/world/world.queries.js.map +1 -1
  118. package/dist/world/world.transfers.d.ts +137 -0
  119. package/dist/world/world.transfers.js +188 -0
  120. package/dist/world/world.transfers.js.map +1 -0
  121. package/docs/README.md +18 -6
  122. package/docs/components.md +91 -48
  123. package/docs/concepts.md +21 -17
  124. package/docs/design-guide.md +125 -89
  125. package/docs/entities.md +62 -72
  126. package/docs/execution-model.md +26 -10
  127. package/docs/getting-started.md +6 -6
  128. package/docs/glossary.md +19 -10
  129. package/docs/modules.md +8 -8
  130. package/docs/queries-and-filters.md +15 -10
  131. package/docs/relationships.md +250 -115
  132. package/docs/snapshots.md +181 -0
  133. package/docs/systems.md +64 -21
  134. package/docs/utilities.md +10 -6
  135. package/package.json +2 -2
  136. package/skills/vecs/SKILL.md +5 -4
@@ -130,7 +130,7 @@ component set _is_ the lifecycle; queries drive create/destroy; reset is free.
130
130
  const obj = scene.add.arc(0, 0); // a Phaser GameObject
131
131
  entity.attach(obj); // the GameObject is now a component, keyed by its class
132
132
 
133
- world.component(Phaser.GameObjects.Arc).onRemove((_entity, obj) => obj.destroy());
133
+ world.component(Phaser.GameObjects.Arc).on("remove", (_entity, obj) => obj.destroy());
134
134
  ```
135
135
 
136
136
  `onRemove` (below) releases the resource whenever it leaves — whether removed explicitly or
@@ -144,15 +144,17 @@ beside the resource.
144
144
  `world.component(C)` returns a handle with hooks fired on every entity:
145
145
 
146
146
  ```ts
147
- world.component(Impulse).onSet((entity, impulse) => {
147
+ world.component(Impulse).on("set", (entity, impulse) => {
148
148
  /* accumulate */
149
149
  });
150
- world.component(Phaser.GameObjects.Arc).onRemove((_entity, obj) => obj.destroy());
150
+ world.component(Phaser.GameObjects.Arc).on("remove", (_entity, obj) => obj.destroy());
151
151
  ```
152
152
 
153
153
  Use them for resource management and cross-cutting reactions that aren't worth a system. (Note
154
154
  the difference from systems: hooks are per-component-instance and run at the mutation site;
155
- systems are queries that run in a phase.)
155
+ systems are queries that run in a phase.) Multiple handlers may be registered per event; they fire
156
+ in registration order and are removed with `off(event, handler)` by reference (see
157
+ [Components — hooks](./components.md#hooks-component-and-relationship-edges)).
156
158
 
157
159
  ### Component traits: policy on component entities
158
160
 
@@ -161,17 +163,19 @@ policy for every use of the component type; these configuration components are *
161
163
 
162
164
  ```ts
163
165
  world.component(Gravity).add(Singleton);
164
- world.component(Inventory).add(OwnedTarget);
165
- world.component(ChildOf).add(Traversable);
166
+ world.component(Inventory).add(Exclusive).add(OwnedTarget);
167
+ world.component(ChildOf).add(Exclusive).add(Traversable);
166
168
  world.component(Orange).set(Implements, { interfaces: [Fruit] });
167
169
  ```
168
170
 
169
- | Trait/declaration | Reach for it when |
170
- | ----------------- | -------------------------------------------------------------------------- |
171
- | `Singleton` | At most one entity may carry the component |
172
- | `Implements` | Concrete components should be queryable through abstract aliases |
173
- | `Traversable` | A relationship hierarchy needs depth or cascade ordering |
174
- | `OwnedTarget` | A relationship is an exclusively owned, zero-or-one entity-valued property |
171
+ | Trait/declaration | Reach for it when |
172
+ | ----------------- | ---------------------------------------------------------------------------------- |
173
+ | `Singleton` | At most one entity may carry the component |
174
+ | `Implements` | Concrete components should be queryable through abstract aliases |
175
+ | `Exclusive` | A relationship must have at most one target |
176
+ | `Traversable` | An exclusive relationship hierarchy needs depth or cascade ordering |
177
+ | `OwnedTarget` | An exclusive relationship owns its explicitly added target |
178
+ | `OneOf` | A relationship's targets must be direct children of a declared parent (enum-style) |
175
179
 
176
180
  Traits declare policy; modules implement it through hooks and metadata. Configure them in module
177
181
  setup, not inside systems. Lifecycle differs by trait: `OwnedTarget` and `Singleton` are setup-time
@@ -194,38 +198,52 @@ entity.set(Rectangle, { width: 2, height: 1 }); // Arc auto-removed
194
198
 
195
199
  ### Relationships
196
200
 
197
- A relationship is a component whose value points at another entity:
201
+ A relationship is a component that stores edges from its source entity to one or more target
202
+ entities. Add and remove those edges explicitly:
198
203
 
199
204
  ```ts
200
205
  class InCell extends Relationship {}
201
- entity.set(InCell, { target: cell }); // retarget freely
202
- entity.target(InCell); // the target entity
206
+ entity.add(InCell, cell);
207
+ entity.add(InCell, neighboringCell);
208
+ entity.remove(InCell, cell);
209
+
210
+ entity.target(InCell); // one current target, or undefined
211
+ entity.targets(InCell); // readonly snapshot of every target
212
+ entity.hasTarget(InCell, neighboringCell); // true
203
213
  world.query("ByCell").with(Networked, Position).groupBy(InCell); // index by target
204
214
  ```
205
215
 
206
- Use `entity.ensureTarget(InCell)` when an absent relationship should default to a new independent
207
- entity. For aggregate parts whose lifetime should remain bound to the source once assigned, attach
208
- `OwnedTarget` during setup:
216
+ Relationships are multi-target by default. Add the `Exclusive` relationship trait when a source may
217
+ have at most one target; adding a different edge then replaces the old edge. This cardinality trait
218
+ is separate from `world.setExclusiveComponents(...)`, which prevents different component types from
219
+ coexisting on one entity.
220
+
221
+ For a zero-or-one aggregate whose lifetime remains bound to its source, create the target explicitly
222
+ and configure the relationship with both `Exclusive` and `OwnedTarget` during setup:
209
223
 
210
224
  ```ts
211
- world.component(Inventory).add(OwnedTarget);
212
- const inventory = player.ensureTarget(Inventory); // inventory is ChildOf player
225
+ world.component(Inventory).add(Exclusive).add(OwnedTarget);
226
+ const inventory = world.entity();
227
+ player.add(Inventory, inventory); // inventory becomes ChildOf player
213
228
  ```
214
229
 
215
- `OwnedTarget` destroys displaced targets on retarget and destroys the current target when the
216
- relationship is removed, keeping aggregate cleanup aligned with the active relationship.
217
-
218
- - **`Relationship.target` is an `Entity` — not directly serialisable, but
219
- relationships can be networked** when listed in `networkComponents`.
220
- `vecs-protocol` registers `Relationship.prototype.target` as an `"entity"`
221
- wire field (u32 eid), so subclasses serialise their target as a raw entity
222
- id. The client resolves the id to a live entity (creating a ghost
223
- placeholder if the parent hasn't arrived yet). Use a dedicated relationship
224
- class per concern — e.g. `ChildOf` for server-local ownership/lifecycle,
225
- `DrawIn` (in vecs-phaser) for networked display hierarchy — so the two
226
- concerns don't overload a single relationship.
227
- - **Retargeting is a plain data change** (no archetype churn). Prefer retargeting a relationship
228
- over swapping marker components to express "which bucket this entity is in."
230
+ `OwnedTarget` requires `Exclusive`, never creates a target implicitly, destroys the displaced target
231
+ when a new edge replaces it, and destroys the current target when its edge or relationship is
232
+ removed. `Traversable` also requires `Exclusive` because hierarchy depth needs one parent. Edge
233
+ hooks, `onTargetAdd` and `onTargetRemove`, observe each explicit edge transition and compose with
234
+ these policies.
235
+
236
+ - **Target storage is private.** Read edges with `target` / `targets` / `hasTarget`; mutate them only
237
+ with `add(Rel, target)` and `remove(Rel, target)` (or `remove(Rel)` for all edges). There is no
238
+ public `Relationship.target` field.
239
+ - **Relationships can be networked** when listed in `networkComponents`. A relationship snapshot
240
+ carries all target eids plus the relationship's ordinary wire fields. The client reconciles the
241
+ complete edge set and creates ghost targets that have not arrived yet. Use a dedicated
242
+ relationship class per concern so ownership, hierarchy, and display links do not overload one
243
+ relationship.
244
+ - **Edge changes do not change the source archetype** while at least one edge remains. Prefer
245
+ replacing an exclusive edge, or adding/removing multi-target edges, over swapping marker
246
+ components to express membership.
229
247
 
230
248
  ---
231
249
 
@@ -253,7 +271,8 @@ classes or DSL objects (`{ all: [...] }`, `{ any: [...] }`, `{ parent: X }`, `{
253
271
  ### Reactive callbacks: `enter` / `update` / `exit`
254
272
 
255
273
  - **`enter`** — fires once when an entity _becomes_ a member. Use it for one-time setup
256
- (create the resource).
274
+ (create the resource). It fires only when the entity remains a member when the system drains;
275
+ vecs drops a transient entry and its paired exit, so setup code does not need a `destroyed` guard.
257
276
  - **`update(C, …)`** — adds `C` to the predicate and fires when watched component `C` is modified.
258
277
  Pass `{ onEnter: true }` when it should also fire on entry for an already-present watched
259
278
  component. Use that to initialize on entry _and_ react to changes; leave it off when a separate
@@ -406,7 +425,7 @@ Guidelines:
406
425
  export class PhaserRenderModule extends Module<{ scene; pixelsPerMeter? }> {
407
426
  public init(cfg): void {
408
427
  const coords = new CoordSpace(cfg.scene, cfg.pixelsPerMeter);
409
- this.world.component(GameObject).onRemove((_e, obj) => obj.destroy()); // GameObject: Implements marker over every Phaser object
428
+ this.world.component(GameObject).on("remove", (_e, obj) => obj.destroy()); // GameObject: Implements marker over every Phaser object
410
429
  this.world.module(ArcModule, { scene: cfg.scene, coords });
411
430
  this.world.module(RectangleModule, { scene: cfg.scene, coords });
412
431
  // … shape modules first …
@@ -516,7 +535,7 @@ live object as its own component** and let a remove hook release it — no wrapp
516
535
 
517
536
  ```ts
518
537
  // Release the resource whenever its component leaves (explicit remove or entity destroy):
519
- world.component(Phaser.GameObjects.Arc).onRemove((_e, obj) => obj.destroy());
538
+ world.component(Phaser.GameObjects.Arc).on("remove", (_e, obj) => obj.destroy());
520
539
 
521
540
  // CREATE: a renderable that has no object yet
522
541
  world
@@ -541,7 +560,7 @@ world
541
560
 
542
561
  // TEARDOWN: when the shape data leaves (type swap) or the entity dies, drop the object.
543
562
  // A component remove hook keeps this declarative — no exit system, no phase to coordinate:
544
- world.component(Arc).onRemove((e) => {
563
+ world.component(Arc).on("remove", (e) => {
545
564
  if (e.has(Phaser.GameObjects.Arc)) e.remove(Phaser.GameObjects.Arc);
546
565
  });
547
566
  ```
@@ -575,10 +594,12 @@ the current `A` as soon as the entity enters the sync system:
575
594
  Leave `onEnter: true` off (or set `onEnter: false`) only when a separate `.enter(...)` already initializes `B`,
576
595
  or when the callback is intentionally "future changes only."
577
596
 
578
- ### Relationship retargeting + grouped queries for spatial/interest indexing
597
+ ### Relationship edges + grouped queries for spatial/interest indexing
579
598
 
580
599
  Bucket entities by a relationship target (or a derived key) and look up buckets in O(1) instead of
581
- scanning. Moving between buckets is a retarget (data change), not a component swap.
600
+ scanning. `groupBy(Rel)` places a source in every bucket targeted through `Rel`; adding or removing
601
+ an edge reconciles only the affected buckets. Use `Exclusive` when the model permits one bucket and
602
+ multi-target relationships when a source legitimately belongs to several.
582
603
 
583
604
  ### Owned target entities (entity-valued properties)
584
605
 
@@ -600,12 +621,13 @@ class Weight {
600
621
  current = 0;
601
622
  }
602
623
 
603
- world.component(Inventory).add(OwnedTarget);
624
+ world.component(Inventory).add(Exclusive).add(OwnedTarget);
604
625
  world.component(InventoryState);
605
626
  world.component(Capacity);
606
627
  world.component(Weight);
607
628
 
608
- const inventory = player.ensureTarget(Inventory);
629
+ const inventory = world.entity();
630
+ player.add(Inventory, inventory);
609
631
  inventory.add(InventoryState).set(Capacity, { slots: 20 }).set(Weight, { current: 0 });
610
632
  ```
611
633
 
@@ -624,35 +646,36 @@ world
624
646
 
625
647
  The lifecycle follows from the relationship and trait:
626
648
 
627
- | Operation | Result |
628
- | -------------------------------------- | ---------------------------------------- |
629
- | First `player.ensureTarget(Inventory)` | Creates and returns the inventory entity |
630
- | Later `ensureTarget(Inventory)` | Returns the same target |
631
- | Retarget `Inventory` | Destroys the displaced inventory |
632
- | Remove `Inventory` | Destroys the current inventory |
633
- | Destroy `player` | `ChildOf` destroys the inventory |
649
+ | Operation | Result |
650
+ | ------------------------------------- | ------------------------------------------------------------------------- |
651
+ | `player.add(Inventory, inventory)` | Parents the explicit inventory entity |
652
+ | `player.add(Inventory, replacement)` | Destroys the displaced inventory |
653
+ | `player.remove(Inventory, inventory)` | Destroys that inventory and removes `Inventory` when it was the sole edge |
654
+ | `player.remove(Inventory)` | Destroys every current target |
655
+ | Destroy `player` | `ChildOf` destroys the inventory |
634
656
 
635
- Inside deferred execution, mutate the entity returned by `ensureTarget`; do not immediately re-read
636
- the relationship:
657
+ Inside deferred execution, keep and mutate the target you created; the queued edge is not visible
658
+ through `target` until flush:
637
659
 
638
660
  ```ts
639
661
  world.defer(() => {
640
- const inventory = player.ensureTarget(Inventory);
641
- inventory.set(Capacity, { slots: 20 }); // correct: pending entity is usable
662
+ const inventory = world.entity();
663
+ player.add(Inventory, inventory);
664
+ inventory.set(Capacity, { slots: 20 }); // the newly created entity is usable
642
665
  player.target(Inventory); // still undefined until flush
643
666
  });
644
667
  ```
645
668
 
646
669
  Use the narrowest model that fits:
647
670
 
648
- | Need | Model |
649
- | --------------------------------------------------- | ------------------------------------------ |
650
- | A few fields sharing the owner's identity/lifecycle | Components directly on the owner |
651
- | One composable, exclusively owned sub-entity | `ensureTarget` + `OwnedTarget` |
652
- | One shared or independently living linked entity | Relationship without `OwnedTarget` |
653
- | Zero-to-many subobjects | Child entities with identifying components |
654
- | A non-ECS runtime object | `attach(instance)` |
655
- | World-global state | Singleton/component entity |
671
+ | Need | Model |
672
+ | --------------------------------------------------- | --------------------------------------------- |
673
+ | A few fields sharing the owner's identity/lifecycle | Components directly on the owner |
674
+ | One composable, exclusively owned sub-entity | Explicit target + `Exclusive` + `OwnedTarget` |
675
+ | One shared or independently living linked entity | Relationship without `OwnedTarget` |
676
+ | Zero-to-many subobjects | Child entities with identifying components |
677
+ | A non-ECS runtime object | `attach(instance)` |
678
+ | World-global state | Singleton/component entity |
656
679
 
657
680
  Good fits include an inventory aggregate, equipment loadout, character progression sheet, AI
658
681
  blackboard, quest journal, ability book, or unique vehicle subsystem. Individual inventory items,
@@ -670,22 +693,22 @@ system after whatever produces the entity/companion.
670
693
 
671
694
  ## 8. Anti-patterns (keep ECS semantics)
672
695
 
673
- | Smell | Do instead |
674
- | ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
675
- | A side `Map<eid, resource>` | Store the resource in a **companion component**; let queries drive its lifecycle; release in `onRemove`. |
676
- | An imperative runtime object with `start()/stop()/track/enable` | A **module** + the world's own lifecycle (`clearAllEntities()`, closing the world). Systems live on the world; they don't need a wrapper to turn on. |
677
- | Builder/factory helpers that _create entities_ (`createArc(world, …)`) | Let callers `world.entity().add(Networked).set(Arc, …).set(FillStyle, …)`. Components are small and optional; the caller adds exactly what it wants. |
678
- | A god component with many optional fields | Split into small single-purpose components; absence encodes "not applicable." |
679
- | A large nested `inventory`, `brain`, or progression object | Use an owned target entity when the subobject needs its own components, systems, and lifecycle. |
680
- | A side `Map<ownerEid, SubsystemState>` | Use `owner.ensureTarget(Rel)`; add `OwnedTarget` only for exclusive destructive ownership. |
681
- | `OwnedTarget` on `CurrentEnemy`, `InCell`, `Team`, or shared state | Use a normal relationship; retargeting or removal of an owned relationship destroys its target. |
682
- | One owned target used as a collection | Use multiple child/item entities; owned targets model zero-or-one aggregates. |
683
- | Duplicated `enter` + `update` doing the same thing | One `update({ watch: C, onEnter: true })` when update owns initialization. |
684
- | One system trying to act in two phases | Split into two systems, one per phase. |
685
- | Custom phases for app/render logic | Use the standard taxonomy; only engine modules insert phases. |
686
- | `.each` scanning every entity to detect change | Reactive `.update(C)` — fires only on change. |
687
- | Networking a relationship | Relationships can be networked when listed in `networkComponents`: `Relationship.target` serialises as a u32 entity id. List `ChildOf` (or custom subclasses) explicitly to replicate parent-child links. |
688
- | A `registerFooSystem(world)` wrapper per system | Declare the system inline in the module's `init`. |
696
+ | Smell | Do instead |
697
+ | ---------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
698
+ | A side `Map<eid, resource>` | Store the resource in a **companion component**; let queries drive its lifecycle; release in `onRemove`. |
699
+ | An imperative runtime object with `start()/stop()/track/enable` | A **module** + the world's own lifecycle (`clearAllEntities()`, closing the world). Systems live on the world; they don't need a wrapper to turn on. |
700
+ | Builder/factory helpers that _create entities_ (`createArc(world, …)`) | Let callers `world.entity().add(Networked).set(Arc, …).set(FillStyle, …)`. Components are small and optional; the caller adds exactly what it wants. |
701
+ | A god component with many optional fields | Split into small single-purpose components; absence encodes "not applicable." |
702
+ | A large nested `inventory`, `brain`, or progression object | Use an owned target entity when the subobject needs its own components, systems, and lifecycle. |
703
+ | A side `Map<ownerEid, SubsystemState>` | Create a target entity and add an explicit edge; use `Exclusive` + `OwnedTarget` only for destructive ownership. |
704
+ | `OwnedTarget` on `CurrentEnemy`, `InCell`, `Team`, or shared state | Use a normal relationship; replacing or removing an owned edge destroys its target. |
705
+ | One owned target used as a collection | Use multiple child/item entities; owned targets model zero-or-one aggregates. |
706
+ | Duplicated `enter` + `update` doing the same thing | One `update({ watch: C, onEnter: true })` when update owns initialization. |
707
+ | One system trying to act in two phases | Split into two systems, one per phase. |
708
+ | Custom phases for app/render logic | Use the standard taxonomy; only engine modules insert phases. |
709
+ | `.each` scanning every entity to detect change | Reactive `.update(C)` — fires only on change. |
710
+ | Treating target storage as component data | Use `add(Rel, target)` / `remove(Rel, target)` and read with `target` / `targets` / `hasTarget`; target storage is private. |
711
+ | A `registerFooSystem(world)` wrapper per system | Declare the system inline in the module's `init`. |
689
712
 
690
713
  ---
691
714
 
@@ -699,24 +722,37 @@ system after whatever produces the entity/companion.
699
722
  when `.enter(...)` already owns initialization.
700
723
  - **Membership is fixed by `.with(...)` / `.without(...)` / `.update(C)`;** `update(C)` watches `C`
701
724
  and requires it.
702
- - **`exit` reads a snapshot** of the injected components captured at exit (they're being removed).
725
+ - **`exit` retains injected references captured at exit** (components are not deep-cloned).
703
726
  - **Within a phase, order = registration order;** across phases, the spine guarantees order. Don't
704
727
  use registration order for cross-module correctness — use phases.
705
728
  - **Networked component order = wire protocol.** Reordering breaks the protocol.
706
729
  - **`@wireType` must survive esbuild's standard decorators** when components load from
707
730
  source (tsx/Vite). Otherwise fields silently don't register.
708
- - **`Relationship.target` is an `Entity`** — not directly wire-encodable, but
709
- `vecs-protocol` registers it as an `"entity"` wire field (u32 eid) at module
710
- load, so `Relationship` subclasses listed in `networkComponents` serialise
711
- correctly.
731
+ - **Relationships are multi-target by default.** `target(R)` returns one current target for singular
732
+ code but is not a stable selection policy; `targets(R)` snapshots all targets with unspecified
733
+ ordering; `hasTarget(R, target)` tests one edge.
734
+ - **Relationship edges are explicit.** Add with `add(R, target)`, remove one with
735
+ `remove(R, target)`, and remove all with `remove(R)`. Do not use `set` for target storage.
736
+ - **Relationship cardinality and component exclusivity are different.** Add the `Exclusive` trait
737
+ to a relationship for at-most-one target; use `setExclusiveComponents` for mutually exclusive
738
+ component types.
739
+ - **Target queries are existential.** `{ target: [R, q] }` matches when any `R` target matches `q`.
740
+ Singular `{ target: ... }` injection projects one current target; plural `{ targets: ... }`
741
+ injection returns one projected row per target.
742
+ - **Relationship grouping is multi-bucket.** `groupBy(R)` places an entity in every current target
743
+ bucket and reconciles buckets as edges change.
744
+ - **Network relationship snapshots contain all targets.** Register the relationship in
745
+ `networkComponents`; the client reconciles the full edge set and relationship data.
712
746
  - **Component traits have trait-specific setup semantics.** In particular, configure `OwnedTarget`
713
747
  before creating relationship instances; it does not backfill.
714
- - **`OwnedTarget` is destructive and exclusive.** Retargeting destroys the old target, removing the
715
- relationship destroys the current target, and owner destruction cascades through `ChildOf`.
748
+ - **`OwnedTarget` requires `Exclusive` and explicit target creation.** Replacing an edge destroys
749
+ the old target, removing the relationship destroys the current target, and owner destruction
750
+ cascades through `ChildOf`.
716
751
  Never use it for shared teams, cells, enemies, definitions, or other world entities.
717
- - **Owned targets are zero-or-one.** Use child entities for collections, and during deferred
718
- execution mutate the entity returned by `ensureTarget` instead of re-reading `target(R)` before
719
- flush.
752
+ - **`Traversable` requires `Exclusive`.** Depth and cascade ordering need one parent per source.
753
+ - **Owned targets are zero-or-one.** Use child entities for collections, create the aggregate target
754
+ explicitly, and during deferred execution keep that entity instead of re-reading `target(R)`
755
+ before flush.
720
756
  - **`world.module(M)` runs `init` once.** Re-registering with a different config throws.
721
757
 
722
758
  ---
@@ -742,14 +778,14 @@ render code**.
742
778
  systems drives every renderable polymorphically. Per shape: a **creation** system
743
779
  (`.with(Shape).without(Phaser.GameObjects.Kind).enter` → `scene.add.*` + `entity.attach`) in
744
780
  `ON_STORE`; a **geometry** system (`.with(Shape, m.Geometry).update`) in `ON_STORE`; and a
745
- shape-data **remove hook** (`component(Shape).onRemove` → remove the Phaser object) instead of a
781
+ shape-data **remove hook** (`component(Shape).on("remove", ...)` → remove the Phaser object) instead of a
746
782
  teardown system. Shared transform/style **sync** systems
747
783
  (`.with(Component, m.Marker).update({ watch: Component, onEnter: true })`) apply
748
784
  position/rotation/scale/alpha/depth/size/fill/stroke through the markers — initialization is
749
785
  explicit via `onEnter: true`, and the umbrella loads shape modules _before_ the sync module so
750
786
  creation precedes sync within `ON_STORE`.
751
787
  - **Lifecycle that falls out for free:** destruction is a single hook —
752
- `component(GameObject).onRemove((_e, obj) => obj.destroy())`. A renderable **type swap**
788
+ `component(GameObject).on("remove", (_e, obj) => obj.destroy())`. A renderable **type swap**
753
789
  (exclusivity removes the old shape data → its remove hook drops the attached object → it's
754
790
  destroyed → the new shape's creation system rebinds) and a **baseline reset / disconnect**
755
791
  (`clearAllEntities()` → entities destroyed → attached objects removed → destroyed) both work with
package/docs/entities.md CHANGED
@@ -46,9 +46,10 @@ for end-of-session teardown, not selective resets.
46
46
  ```ts
47
47
  e.add(Position); // attach with default field values (idempotent)
48
48
  e.set(Position, { x: 100 }); // attach if needed, assign props, fire onSet/update
49
+ e.add(FriendOf, friend); // relationship edges require an explicit target
50
+ e.remove(FriendOf, friend); // remove one relationship edge
49
51
  e.get(Position); // Readonly<Position> | undefined
50
52
  e.ensure(Position, (pos) => (pos.x += 1)); // create if needed, then mutate and notify
51
- e.ensureTarget(FriendOf); // get or create an independent relationship target
52
53
  e.has(Position); // boolean
53
54
  e.remove(Position); // detach; fires exit callbacks, then onRemove
54
55
  e.modified(Position); // mark changed after in-place mutation
@@ -57,19 +58,26 @@ e.destroy(); // remove everything and unregister the entity
57
58
 
58
59
  All mutators return the entity for chaining (`remove` and `destroy` return `void`). Component
59
60
  references can be a registered class, a numeric component id, or an entity used as a component
60
- key.
61
+ key. Relationship `target` arguments accept the same `ComponentRef` shapes (class, numeric id, or
62
+ entity) and are resolved eagerly via `world.entity(target)`, so the target entity must already
63
+ exist when the call is made.
61
64
 
62
65
  ### `set` vs `add` vs `modified`
63
66
 
64
67
  - `add(C)` attaches with defaults and fires `onAdd` only. It is idempotent and does **not** fire
65
68
  `onSet`.
66
- - `set(C, props)` attaches if needed (firing `onAdd`), copies `props` onto the instance, and fires
67
- `onSet` plus `update` callbacks.
69
+ - For ordinary components, `set(C, props)` attaches if needed (firing `onAdd`), copies `props` onto
70
+ the instance, and fires `onSet` plus `update` callbacks.
68
71
  - `ensure(C, callback)` creates the component if missing, then lets you mutate it and fires
69
72
  `onAdd` followed by one `onSet`. When the component already exists, it behaves like `getMut`.
70
73
  - `modified(C)` fires `onSet`/`update` for data you already mutated in place. Repeated calls are
71
74
  coalesced until the world routes the notification.
72
75
 
76
+ Relationships use a stricter API: `add(R, target)` creates a unique edge, `remove(R, target)`
77
+ removes one edge, and `remove(R)` removes every edge. `set(R, props)` updates only ordinary fields
78
+ on an existing relationship and never creates or changes edges. See
79
+ [Relationships](./relationships.md#adding-removing-and-setting).
80
+
73
81
  ### `getMut` and `ensure`: mutating in place
74
82
 
75
83
  `get` returns a `Readonly` view. To mutate, use `getMut`, which marks the component modified for
@@ -100,38 +108,10 @@ When several calls ensure the same missing component during one deferred scope,
100
108
  same pending instance. The instance remains hidden from `get` and queries until the queue drains,
101
109
  but every ensure callback mutates that shared pending value.
102
110
 
103
- Use `ensureTarget` for the corresponding get-or-create operation on a registered relationship:
104
-
105
- ```ts
106
- world.component(FriendOf);
107
-
108
- const friend = entity.ensureTarget(FriendOf);
109
- entity.ensureTarget(FriendOf) === friend; // true
110
- entity.target(FriendOf) === friend; // true after deferred changes flush
111
- ```
112
-
113
- When the relationship is absent, `ensureTarget` creates an independent entity and points the
114
- relationship at it. It does not add `ChildOf`; destroying the source does not destroy the generated
115
- target. Repeated calls during one deferred scope return the same pending target, while `target`
116
- continues to expose only committed state until the queue drains.
117
-
118
- Mark the relationship with `OwnedTarget` during world setup when its targets should instead be
119
- owned by their sources:
120
-
121
- ```ts
122
- world.component(Inventory).add(OwnedTarget);
123
-
124
- const inventory = player.ensureTarget(Inventory);
125
- inventory.parent === player; // true after deferred changes flush
126
- ```
127
-
128
- The relationship module adds `ChildOf` whenever `Inventory` is set, destroys displaced targets on
129
- retarget, and destroys the current target when the relationship is removed. Destroying `player`
130
- therefore also destroys `inventory`. See [Relationships](./relationships.md#owned-targets).
131
-
132
111
  Calling `getMut(C)` or `ensure(C)` without a callback outside deferred mode throws. Both methods
133
- also throw for relationship components (retarget with `set` instead — see
134
- [Relationships](./relationships.md#retargeting)) and for `Implements` interface aliases.
112
+ also throw for relationship components (change edges with `add` / `remove` and ordinary data with
113
+ `set`) and for `Implements` interface aliases. `attach` rejects relationships for the same reason:
114
+ their private edge storage must remain under vecs's control.
135
115
 
136
116
  ### `attach`: storing an existing instance
137
117
 
@@ -160,6 +140,9 @@ drains. The exact read-after-write rules are tabulated in
160
140
  `ensure(C)` is the exception for mutable access: it returns a mutable component immediately, but a
161
141
  newly created component does not appear in `get(C)` until the queue drains.
162
142
 
143
+ Relationship edge commands share the same FIFO queue. Until it drains, `has(R)`, `target(R)`,
144
+ `targets(R)`, `hasTarget(R, target)`, and `children(R)` expose only committed edges.
145
+
163
146
  Deferred entity creation is idempotent within one deferred scope: if `world.entity()` or
164
147
  `getOrCreateEntity(eid)` creates a new entity, `getEntity(eid)`, `entity(eid)`, and later
165
148
  `getOrCreateEntity(eid)` calls before the drain return that same pending entity. Collection-style
@@ -177,53 +160,60 @@ not be used.
177
160
  If the entity is itself in use as a component key, `destroy` honors `meta.onDelete` — by default
178
161
  it throws while carriers remain ([Components — cleanup policies](./components.md#cleanup-policies-destroying-a-component-entity)).
179
162
 
163
+ A `destroy()` thrown synchronously (a `Throw`-policy component entity still in use) leaves the world
164
+ usable because nothing has been mutated. A throw during deferred drain or a relationship cleanup
165
+ cascade — an impossible policy combination, a `Throw`-policy component reached via a cascade, or a
166
+ hook that throws — propagates out of `progress()` / `flush()` and leaves the world in a partial
167
+ state. Treat this as a programming error: recreate the world or stop.
168
+
180
169
  ## Entities as component keys
181
170
 
182
171
  Any entity can serve as a component key: `other.add(keyEntity)` attaches a marker component whose
183
172
  type id is the key entity's eid. The key entity's `meta` (created lazily) carries the same hook
184
- and policy surface as a registered class, and `keyEntity.onAdd/onSet/onRemove(...)` register hooks
185
- directly. This is how tag-like, runtime-created component types work without declaring a class.
186
- Registered relationship component entities additionally expose `onRetarget(...)`.
173
+ and policy surface as a registered class, and `keyEntity.on("add" / "set" / "remove" / "targetAdd" /
174
+ "targetRemove", handler)` registers its lifecycle handlers directly. This is how tag-like,
175
+ runtime-created component types work without declaring a class.
187
176
 
188
177
  ## Relationships, in brief
189
178
 
190
- `e.target(Rel)` returns the target of a relationship component, `e.children(Rel)` the set of
191
- entities targeting `e` through it, and `e.childOf(parent)` is shorthand for
192
- `e.set(ChildOf, { target: parent })`. The full model — retargeting, cleanup cascades, traversal —
193
- is in [Relationships](./relationships.md).
179
+ `e.target(Rel)` returns one current target, `e.targets(Rel)` a readonly snapshot of every target,
180
+ `e.hasTarget(Rel, target)` tests one edge, and `e.children(Rel)` returns the sources targeting `e`.
181
+ `e.childOf(parent)` is shorthand for `e.add(ChildOf, parent)`. The full model is in
182
+ [Relationships](./relationships.md).
194
183
 
195
184
  ## Reference
196
185
 
197
186
  `lib/vecs/src/entity/` (split across `entity.base.ts`, `entity.components.ts`,
198
187
  `entity.lifecycle.ts`, `entity.relationships.ts`):
199
188
 
200
- | Property / Method | Description |
201
- | ---------------------------------------------- | --------------------------------------------------------------------------------------------------- |
202
- | `eid` | Unique numeric entity id. |
203
- | `world` | The `World` that owns this entity. |
204
- | `name` | Identity name (getter/setter backed by the `Identity` component); `undefined` when unnamed. |
205
- | `componentBitmask` | `Bitset` of component type ids attached. Used for archetype matching. |
206
- | `components` | Read-only `ArrayMap` view of attached instances keyed by type id (`forEach`, `get`, `has`, `size`). |
207
- | `empty` | `true` when no components are attached. |
208
- | `properties` | Free-form `Map<string, any>` for module-level bookkeeping. |
209
- | `meta` | `ComponentMeta` for using this entity as a component key (created lazily). |
210
- | `instanceCount()` | Number of entities currently carrying this entity's eid as a component. |
211
- | `add(ref)` | Attach a component with default values (idempotent). Returns `this`. |
212
- | `attach(instance)` / `attach(Class, instance)` | Store an existing instance, replacing any previous one; fires set-side events. Returns `this`. |
213
- | `set(ref, props)` | Attach if needed and assign `props`; fires `onSet` and `update` callbacks. Returns `this`. |
214
- | `get(ref)` | Read-only component lookup, or `undefined`. |
215
- | `getMut(ref)` / `getMut(ref, callback)` | Mutable lookup that marks the component modified. Callback form required outside deferred mode. |
216
- | `ensure(ref)` / `ensure(ref, callback)` | Mutable lookup that creates when missing. Callback form required outside deferred mode. |
217
- | `ensureTarget(Relationship)` | Return a relationship target, creating an independent target when absent. |
218
- | `has(ref)` | `true` when the component is attached. |
219
- | `modified(ref)` | Queue an `onSet`/`update` notification; coalesced while pending. Returns `this`. |
220
- | `remove(ref)` | Detach a component; routes `exit`, then fires `onRemove`. |
221
- | `destroy()` | Remove all components, unregister, and apply incoming relationship cleanup. |
222
- | `target(rel)` | Target entity of relationship `rel`, or `undefined`. |
223
- | `children(rel)` | `ReadonlySet<Entity>` of entities targeting this one through `rel`. |
224
- | `childOf(parent)` | Shorthand for `set(ChildOf, { target: parent })`. Returns `this`. |
225
- | `parent` / `parent = p` | Getter/setter for the built-in `ChildOf` target; assigning `undefined` removes `ChildOf`. |
226
- | `destroyed` / `alive` | `true` when the entity has been / has not been destroyed (marked for deletion). |
227
- | `onAdd/onSet/onRemove(handler)` | Register hooks for this entity used as a component key. Return `this`. |
228
- | `onRetarget(handler)` | Register a retarget hook on a relationship component entity. |
229
- | `toString()` | Returns `"EntityN"`. |
189
+ | Property / Method | Description |
190
+ | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
191
+ | `eid` | Unique numeric entity id. |
192
+ | `world` | The `World` that owns this entity. |
193
+ | `name` | Identity name (getter/setter backed by the `Identity` component); `undefined` when unnamed. |
194
+ | `componentBitmask` | `Bitset` of component type ids attached. Used for archetype matching. |
195
+ | `components` | Read-only `ArrayMap` view of attached instances keyed by type id (`forEach`, `get`, `has`, `size`). |
196
+ | `empty` | `true` when no components are attached. |
197
+ | `properties` | Free-form `Map<string, any>` for module-level bookkeeping. |
198
+ | `meta` | `ComponentMeta` for using this entity as a component key (created lazily). |
199
+ | `instanceCount()` | Number of entities currently carrying this entity's eid as a component. |
200
+ | `add(ref)` / `add(Relationship, target)` | Attach an ordinary component, or add one explicit relationship edge. `target` accepts any `ComponentRef` resolved eagerly via `world.entity(target)`. Returns `this`. |
201
+ | `attach(instance)` / `attach(Class, instance)` | Store an existing instance, replacing any previous one; fires set-side events. Returns `this`. |
202
+ | `set(ref, props)` | Attach/set ordinary data; relationships must exist and only ordinary fields are assigned. |
203
+ | `get(ref)` | Read-only component lookup, or `undefined`. |
204
+ | `getMut(ref)` / `getMut(ref, callback)` | Mutable lookup that marks the component modified. Callback form required outside deferred mode. |
205
+ | `ensure(ref)` / `ensure(ref, callback)` | Mutable lookup that creates when missing. Callback form required outside deferred mode. |
206
+ | `has(ref)` | `true` when the component is attached. |
207
+ | `modified(ref)` | Queue an `onSet`/`update` notification; coalesced while pending. Returns `this`. |
208
+ | `remove(ref)` / `remove(Relationship, target)` | Detach a component/all edges, or remove one relationship edge. `target` accepts any `ComponentRef` resolved eagerly via `world.entity(target)`. |
209
+ | `destroy()` | Remove all components, unregister, and apply incoming relationship cleanup. |
210
+ | `target(rel)` | One current target entity of relationship `rel`, or `undefined`; not a stable selection policy. |
211
+ | `targets(rel)` | Readonly snapshot of all targets; ordering is unspecified. |
212
+ | `hasTarget(rel, target)` | `true` when the specific relationship edge exists. |
213
+ | `children(rel)` | `ReadonlySet<Entity>` of entities targeting this one through `rel`. |
214
+ | `childOf(parent)` | Shorthand for `add(ChildOf, parent)`. Returns `this`. |
215
+ | `parent` / `parent = p` | Getter/setter for the built-in `ChildOf` target; assigning `undefined` removes `ChildOf`. |
216
+ | `destroyed` / `alive` | `true` when the entity has been / has not been destroyed (marked for deletion). |
217
+ | `on(event, handler, insert?)` | Register a handler for a hook event on this component's meta. `insert` defaults to `false` (append); `true` prepends. Returns `this`. |
218
+ | `off(event, handler)` | Remove a previously-registered handler by reference. Idempotent. Returns `this`. |
219
+ | `toString()` | Returns `"EntityN"`. |