@vworlds/vecs 1.0.45 → 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 -60
  3. package/dist/component.js +24 -17
  4. package/dist/component.js.map +1 -1
  5. package/dist/component_meta.d.ts +67 -79
  6. package/dist/component_meta.js +331 -37
  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 -41
  13. package/dist/entity/entity.components.js +249 -82
  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 -50
  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 -6
  39. package/dist/modules/relationship_types.js +70 -6
  40. package/dist/modules/relationship_types.js.map +1 -1
  41. package/dist/modules/relationships.d.ts +31 -9
  42. package/dist/modules/relationships.js +120 -44
  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 +90 -53
  123. package/docs/concepts.md +21 -17
  124. package/docs/design-guide.md +125 -94
  125. package/docs/entities.md +62 -76
  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 +245 -124
  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,16 +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.) Each event has one handler slot: setting another handler
156
- replaces it, calling the hook method with no argument reads it, and passing `undefined` removes it.
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)).
157
158
 
158
159
  ### Component traits: policy on component entities
159
160
 
@@ -162,17 +163,19 @@ policy for every use of the component type; these configuration components are *
162
163
 
163
164
  ```ts
164
165
  world.component(Gravity).add(Singleton);
165
- world.component(Inventory).add(OwnedTarget);
166
- world.component(ChildOf).add(Traversable);
166
+ world.component(Inventory).add(Exclusive).add(OwnedTarget);
167
+ world.component(ChildOf).add(Exclusive).add(Traversable);
167
168
  world.component(Orange).set(Implements, { interfaces: [Fruit] });
168
169
  ```
169
170
 
170
- | Trait/declaration | Reach for it when |
171
- | ----------------- | -------------------------------------------------------------------------- |
172
- | `Singleton` | At most one entity may carry the component |
173
- | `Implements` | Concrete components should be queryable through abstract aliases |
174
- | `Traversable` | A relationship hierarchy needs depth or cascade ordering |
175
- | `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) |
176
179
 
177
180
  Traits declare policy; modules implement it through hooks and metadata. Configure them in module
178
181
  setup, not inside systems. Lifecycle differs by trait: `OwnedTarget` and `Singleton` are setup-time
@@ -195,41 +198,52 @@ entity.set(Rectangle, { width: 2, height: 1 }); // Arc auto-removed
195
198
 
196
199
  ### Relationships
197
200
 
198
- 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:
199
203
 
200
204
  ```ts
201
205
  class InCell extends Relationship {}
202
- entity.set(InCell, { target: cell }); // retarget freely
203
- 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
204
213
  world.query("ByCell").with(Networked, Position).groupBy(InCell); // index by target
205
214
  ```
206
215
 
207
- Use `entity.ensureTarget(InCell)` when an absent relationship should default to a new independent
208
- entity. For aggregate parts whose lifetime should remain bound to the source once assigned, attach
209
- `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:
210
223
 
211
224
  ```ts
212
- world.component(Inventory).add(OwnedTarget);
213
- const inventory = player.ensureTarget(Inventory); // inventory is ChildOf player
214
- player.add(Inventory); // also creates an owned target
225
+ world.component(Inventory).add(Exclusive).add(OwnedTarget);
226
+ const inventory = world.entity();
227
+ player.add(Inventory, inventory); // inventory becomes ChildOf player
215
228
  ```
216
229
 
217
- `OwnedTarget` creates a target when the relationship is added, destroys displaced targets on
218
- retarget, and destroys the current target when the relationship is removed, keeping aggregate
219
- cleanup aligned with the active relationship. Configure the relationship owner's lifecycle hooks
220
- before attaching the trait; it wraps those hooks and runs ownership first.
221
-
222
- - **`Relationship.target` is an `Entity` — not directly serialisable, but
223
- relationships can be networked** when listed in `networkComponents`.
224
- `vecs-protocol` registers `Relationship.prototype.target` as an `"entity"`
225
- wire field (u32 eid), so subclasses serialise their target as a raw entity
226
- id. The client resolves the id to a live entity (creating a ghost
227
- placeholder if the parent hasn't arrived yet). Use a dedicated relationship
228
- class per concern — e.g. `ChildOf` for server-local ownership/lifecycle,
229
- `DrawIn` (in vecs-phaser) for networked display hierarchy — so the two
230
- concerns don't overload a single relationship.
231
- - **Retargeting is a plain data change** (no archetype churn). Prefer retargeting a relationship
232
- 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.
233
247
 
234
248
  ---
235
249
 
@@ -257,7 +271,8 @@ classes or DSL objects (`{ all: [...] }`, `{ any: [...] }`, `{ parent: X }`, `{
257
271
  ### Reactive callbacks: `enter` / `update` / `exit`
258
272
 
259
273
  - **`enter`** — fires once when an entity _becomes_ a member. Use it for one-time setup
260
- (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.
261
276
  - **`update(C, …)`** — adds `C` to the predicate and fires when watched component `C` is modified.
262
277
  Pass `{ onEnter: true }` when it should also fire on entry for an already-present watched
263
278
  component. Use that to initialize on entry _and_ react to changes; leave it off when a separate
@@ -410,7 +425,7 @@ Guidelines:
410
425
  export class PhaserRenderModule extends Module<{ scene; pixelsPerMeter? }> {
411
426
  public init(cfg): void {
412
427
  const coords = new CoordSpace(cfg.scene, cfg.pixelsPerMeter);
413
- 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
414
429
  this.world.module(ArcModule, { scene: cfg.scene, coords });
415
430
  this.world.module(RectangleModule, { scene: cfg.scene, coords });
416
431
  // … shape modules first …
@@ -520,7 +535,7 @@ live object as its own component** and let a remove hook release it — no wrapp
520
535
 
521
536
  ```ts
522
537
  // Release the resource whenever its component leaves (explicit remove or entity destroy):
523
- world.component(Phaser.GameObjects.Arc).onRemove((_e, obj) => obj.destroy());
538
+ world.component(Phaser.GameObjects.Arc).on("remove", (_e, obj) => obj.destroy());
524
539
 
525
540
  // CREATE: a renderable that has no object yet
526
541
  world
@@ -545,7 +560,7 @@ world
545
560
 
546
561
  // TEARDOWN: when the shape data leaves (type swap) or the entity dies, drop the object.
547
562
  // A component remove hook keeps this declarative — no exit system, no phase to coordinate:
548
- world.component(Arc).onRemove((e) => {
563
+ world.component(Arc).on("remove", (e) => {
549
564
  if (e.has(Phaser.GameObjects.Arc)) e.remove(Phaser.GameObjects.Arc);
550
565
  });
551
566
  ```
@@ -579,10 +594,12 @@ the current `A` as soon as the entity enters the sync system:
579
594
  Leave `onEnter: true` off (or set `onEnter: false`) only when a separate `.enter(...)` already initializes `B`,
580
595
  or when the callback is intentionally "future changes only."
581
596
 
582
- ### Relationship retargeting + grouped queries for spatial/interest indexing
597
+ ### Relationship edges + grouped queries for spatial/interest indexing
583
598
 
584
599
  Bucket entities by a relationship target (or a derived key) and look up buckets in O(1) instead of
585
- 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.
586
603
 
587
604
  ### Owned target entities (entity-valued properties)
588
605
 
@@ -604,12 +621,13 @@ class Weight {
604
621
  current = 0;
605
622
  }
606
623
 
607
- world.component(Inventory).add(OwnedTarget);
624
+ world.component(Inventory).add(Exclusive).add(OwnedTarget);
608
625
  world.component(InventoryState);
609
626
  world.component(Capacity);
610
627
  world.component(Weight);
611
628
 
612
- const inventory = player.ensureTarget(Inventory);
629
+ const inventory = world.entity();
630
+ player.add(Inventory, inventory);
613
631
  inventory.add(InventoryState).set(Capacity, { slots: 20 }).set(Weight, { current: 0 });
614
632
  ```
615
633
 
@@ -628,36 +646,36 @@ world
628
646
 
629
647
  The lifecycle follows from the relationship and trait:
630
648
 
631
- | Operation | Result |
632
- | -------------------------------------- | ---------------------------------------- |
633
- | `player.add(Inventory)` | Creates the inventory entity |
634
- | First `player.ensureTarget(Inventory)` | Creates and returns the inventory entity |
635
- | Later `ensureTarget(Inventory)` | Returns the same target |
636
- | Retarget `Inventory` | Destroys the displaced inventory |
637
- | Remove `Inventory` | Destroys the current inventory |
638
- | 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 |
639
656
 
640
- Inside deferred execution, mutate the entity returned by `ensureTarget`; do not immediately re-read
641
- the relationship:
657
+ Inside deferred execution, keep and mutate the target you created; the queued edge is not visible
658
+ through `target` until flush:
642
659
 
643
660
  ```ts
644
661
  world.defer(() => {
645
- const inventory = player.ensureTarget(Inventory);
646
- 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
647
665
  player.target(Inventory); // still undefined until flush
648
666
  });
649
667
  ```
650
668
 
651
669
  Use the narrowest model that fits:
652
670
 
653
- | Need | Model |
654
- | --------------------------------------------------- | ------------------------------------------ |
655
- | A few fields sharing the owner's identity/lifecycle | Components directly on the owner |
656
- | One composable, exclusively owned sub-entity | `ensureTarget` + `OwnedTarget` |
657
- | One shared or independently living linked entity | Relationship without `OwnedTarget` |
658
- | Zero-to-many subobjects | Child entities with identifying components |
659
- | A non-ECS runtime object | `attach(instance)` |
660
- | 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 |
661
679
 
662
680
  Good fits include an inventory aggregate, equipment loadout, character progression sheet, AI
663
681
  blackboard, quest journal, ability book, or unique vehicle subsystem. Individual inventory items,
@@ -675,22 +693,22 @@ system after whatever produces the entity/companion.
675
693
 
676
694
  ## 8. Anti-patterns (keep ECS semantics)
677
695
 
678
- | Smell | Do instead |
679
- | ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
680
- | A side `Map<eid, resource>` | Store the resource in a **companion component**; let queries drive its lifecycle; release in `onRemove`. |
681
- | 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. |
682
- | 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. |
683
- | A god component with many optional fields | Split into small single-purpose components; absence encodes "not applicable." |
684
- | A large nested `inventory`, `brain`, or progression object | Use an owned target entity when the subobject needs its own components, systems, and lifecycle. |
685
- | A side `Map<ownerEid, SubsystemState>` | Use `owner.ensureTarget(Rel)`; add `OwnedTarget` only for exclusive destructive ownership. |
686
- | `OwnedTarget` on `CurrentEnemy`, `InCell`, `Team`, or shared state | Use a normal relationship; retargeting or removal of an owned relationship destroys its target. |
687
- | One owned target used as a collection | Use multiple child/item entities; owned targets model zero-or-one aggregates. |
688
- | Duplicated `enter` + `update` doing the same thing | One `update({ watch: C, onEnter: true })` when update owns initialization. |
689
- | One system trying to act in two phases | Split into two systems, one per phase. |
690
- | Custom phases for app/render logic | Use the standard taxonomy; only engine modules insert phases. |
691
- | `.each` scanning every entity to detect change | Reactive `.update(C)` — fires only on change. |
692
- | 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. |
693
- | 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`. |
694
712
 
695
713
  ---
696
714
 
@@ -704,24 +722,37 @@ system after whatever produces the entity/companion.
704
722
  when `.enter(...)` already owns initialization.
705
723
  - **Membership is fixed by `.with(...)` / `.without(...)` / `.update(C)`;** `update(C)` watches `C`
706
724
  and requires it.
707
- - **`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).
708
726
  - **Within a phase, order = registration order;** across phases, the spine guarantees order. Don't
709
727
  use registration order for cross-module correctness — use phases.
710
728
  - **Networked component order = wire protocol.** Reordering breaks the protocol.
711
729
  - **`@wireType` must survive esbuild's standard decorators** when components load from
712
730
  source (tsx/Vite). Otherwise fields silently don't register.
713
- - **`Relationship.target` is an `Entity`** — not directly wire-encodable, but
714
- `vecs-protocol` registers it as an `"entity"` wire field (u32 eid) at module
715
- load, so `Relationship` subclasses listed in `networkComponents` serialise
716
- 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.
717
746
  - **Component traits have trait-specific setup semantics.** In particular, configure `OwnedTarget`
718
747
  before creating relationship instances; it does not backfill.
719
- - **`OwnedTarget` is destructive and exclusive.** Retargeting destroys the old target, removing the
720
- 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`.
721
751
  Never use it for shared teams, cells, enemies, definitions, or other world entities.
722
- - **Owned targets are zero-or-one.** Use child entities for collections, and during deferred
723
- execution mutate the entity returned by `ensureTarget` instead of re-reading `target(R)` before
724
- 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.
725
756
  - **`world.module(M)` runs `init` once.** Re-registering with a different config throws.
726
757
 
727
758
  ---
@@ -747,14 +778,14 @@ render code**.
747
778
  systems drives every renderable polymorphically. Per shape: a **creation** system
748
779
  (`.with(Shape).without(Phaser.GameObjects.Kind).enter` → `scene.add.*` + `entity.attach`) in
749
780
  `ON_STORE`; a **geometry** system (`.with(Shape, m.Geometry).update`) in `ON_STORE`; and a
750
- 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
751
782
  teardown system. Shared transform/style **sync** systems
752
783
  (`.with(Component, m.Marker).update({ watch: Component, onEnter: true })`) apply
753
784
  position/rotation/scale/alpha/depth/size/fill/stroke through the markers — initialization is
754
785
  explicit via `onEnter: true`, and the umbrella loads shape modules _before_ the sync module so
755
786
  creation precedes sync within `ON_STORE`.
756
787
  - **Lifecycle that falls out for free:** destruction is a single hook —
757
- `component(GameObject).onRemove((_e, obj) => obj.destroy())`. A renderable **type swap**
788
+ `component(GameObject).on("remove", (_e, obj) => obj.destroy())`. A renderable **type swap**
758
789
  (exclusivity removes the old shape data → its remove hook drops the attached object → it's
759
790
  destroyed → the new shape's creation system rebinds) and a **baseline reset / disconnect**
760
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,41 +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
- player.add(Inventory); // also creates an owned target
128
- ```
129
-
130
- The relationship module creates a target when an owned relationship is added, adds `ChildOf`
131
- whenever `Inventory` is assigned, destroys displaced targets on retarget, and destroys the current
132
- target when the relationship is removed. Destroying `player` therefore also destroys `inventory`.
133
- See [Relationships](./relationships.md#owned-targets).
134
-
135
111
  Calling `getMut(C)` or `ensure(C)` without a callback outside deferred mode throws. Both methods
136
- also throw for relationship components (retarget with `set` instead — see
137
- [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.
138
115
 
139
116
  ### `attach`: storing an existing instance
140
117
 
@@ -163,6 +140,9 @@ drains. The exact read-after-write rules are tabulated in
163
140
  `ensure(C)` is the exception for mutable access: it returns a mutable component immediately, but a
164
141
  newly created component does not appear in `get(C)` until the queue drains.
165
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
+
166
146
  Deferred entity creation is idempotent within one deferred scope: if `world.entity()` or
167
147
  `getOrCreateEntity(eid)` creates a new entity, `getEntity(eid)`, `entity(eid)`, and later
168
148
  `getOrCreateEntity(eid)` calls before the drain return that same pending entity. Collection-style
@@ -180,54 +160,60 @@ not be used.
180
160
  If the entity is itself in use as a component key, `destroy` honors `meta.onDelete` — by default
181
161
  it throws while carriers remain ([Components — cleanup policies](./components.md#cleanup-policies-destroying-a-component-entity)).
182
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
+
183
169
  ## Entities as component keys
184
170
 
185
171
  Any entity can serve as a component key: `other.add(keyEntity)` attaches a marker component whose
186
172
  type id is the key entity's eid. The key entity's `meta` (created lazily) carries the same hook
187
- and policy surface as a registered class, and `keyEntity.onAdd/onSet/onRemove(...)` get, set, or
188
- remove its single lifecycle handlers directly. This is how tag-like, runtime-created component
189
- types work without declaring a class. Registered relationship component entities additionally
190
- 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.
191
176
 
192
177
  ## Relationships, in brief
193
178
 
194
- `e.target(Rel)` returns the target of a relationship component, `e.children(Rel)` the set of
195
- entities targeting `e` through it, and `e.childOf(parent)` is shorthand for
196
- `e.set(ChildOf, { target: parent })`. The full model — retargeting, cleanup cascades, traversal —
197
- 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).
198
183
 
199
184
  ## Reference
200
185
 
201
186
  `lib/vecs/src/entity/` (split across `entity.base.ts`, `entity.components.ts`,
202
187
  `entity.lifecycle.ts`, `entity.relationships.ts`):
203
188
 
204
- | Property / Method | Description |
205
- | ---------------------------------------------- | --------------------------------------------------------------------------------------------------- |
206
- | `eid` | Unique numeric entity id. |
207
- | `world` | The `World` that owns this entity. |
208
- | `name` | Identity name (getter/setter backed by the `Identity` component); `undefined` when unnamed. |
209
- | `componentBitmask` | `Bitset` of component type ids attached. Used for archetype matching. |
210
- | `components` | Read-only `ArrayMap` view of attached instances keyed by type id (`forEach`, `get`, `has`, `size`). |
211
- | `empty` | `true` when no components are attached. |
212
- | `properties` | Free-form `Map<string, any>` for module-level bookkeeping. |
213
- | `meta` | `ComponentMeta` for using this entity as a component key (created lazily). |
214
- | `instanceCount()` | Number of entities currently carrying this entity's eid as a component. |
215
- | `add(ref)` | Attach a component with default values (idempotent). Returns `this`. |
216
- | `attach(instance)` / `attach(Class, instance)` | Store an existing instance, replacing any previous one; fires set-side events. Returns `this`. |
217
- | `set(ref, props)` | Attach if needed and assign `props`; fires `onSet` and `update` callbacks. Returns `this`. |
218
- | `get(ref)` | Read-only component lookup, or `undefined`. |
219
- | `getMut(ref)` / `getMut(ref, callback)` | Mutable lookup that marks the component modified. Callback form required outside deferred mode. |
220
- | `ensure(ref)` / `ensure(ref, callback)` | Mutable lookup that creates when missing. Callback form required outside deferred mode. |
221
- | `ensureTarget(Relationship)` | Return a relationship target, creating an independent target when absent. |
222
- | `has(ref)` | `true` when the component is attached. |
223
- | `modified(ref)` | Queue an `onSet`/`update` notification; coalesced while pending. Returns `this`. |
224
- | `remove(ref)` | Detach a component; routes `exit`, then fires `onRemove`. |
225
- | `destroy()` | Remove all components, unregister, and apply incoming relationship cleanup. |
226
- | `target(rel)` | Target entity of relationship `rel`, or `undefined`. |
227
- | `children(rel)` | `ReadonlySet<Entity>` of entities targeting this one through `rel`. |
228
- | `childOf(parent)` | Shorthand for `set(ChildOf, { target: parent })`. Returns `this`. |
229
- | `parent` / `parent = p` | Getter/setter for the built-in `ChildOf` target; assigning `undefined` removes `ChildOf`. |
230
- | `destroyed` / `alive` | `true` when the entity has been / has not been destroyed (marked for deletion). |
231
- | `onAdd/onSet/onRemove(handler?)` | Get, set, or remove this component key's single hook. Setters return `this`. |
232
- | `onRetarget(handler?)` | Get, set, or remove a relationship component entity's single retarget hook. |
233
- | `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"`. |