@vworlds/vecs 1.0.43 → 1.0.44

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.
package/docs/README.md CHANGED
@@ -14,13 +14,13 @@ Read in this order the first time through:
14
14
  3. [Execution model](./execution-model.md) — phases, the pipeline, deferred mutation, reactive
15
15
  routing. Read this before designing anything non-trivial.
16
16
  4. Subsystem guides, in any order:
17
- - [Components](./components.md) — registration, hooks, exclusivity, cleanup policies,
17
+ - [Components](./components.md) — registration, hooks, traits, exclusivity, cleanup policies,
18
18
  interfaces, singletons.
19
- - [Entities](./entities.md) — creation, lookup, the component API, lifecycle.
19
+ - [Entities](./entities.md) — creation, lookup, the component API, relationship targets, lifecycle.
20
20
  - [Systems](./systems.md) — membership, reactive callbacks, callback options, `each`/`run`, cadence, injection.
21
21
  - [Queries and filters](./queries-and-filters.md) — the query DSL, live queries, one-shot
22
22
  filters, grouped queries, `Bitset`.
23
- - [Relationships](./relationships.md) — entity-to-entity links, hierarchy, cleanup cascades.
23
+ - [Relationships](./relationships.md) — links, target creation and ownership, hierarchy, cleanup cascades.
24
24
  - [Modules](./modules.md) — packaging features, composition, the built-in modules.
25
25
  5. [Design guide](./design-guide.md) — opinionated patterns and anti-patterns distilled from real
26
26
  designs. Read it once you know the machinery.
@@ -36,11 +36,11 @@ Keep the [Glossary](./glossary.md) at hand for terminology.
36
36
  | [getting-started.md](./getting-started.md) | A runnable tutorial from `npm install` to a ticking world. |
37
37
  | [concepts.md](./concepts.md) | The core vocabulary and how the pieces relate. |
38
38
  | [execution-model.md](./execution-model.md) | `world.progress`, the 8 built-in phases, deferred commands, flush boundaries, event routing. |
39
- | [components.md](./components.md) | `world.component`, hooks, exclusive groups, `CleanupPolicy`, `Implements`, `Singleton`. |
40
- | [entities.md](./entities.md) | `world.entity`, `add`/`set`/`get`/`remove`/`destroy`, deferred read semantics. |
39
+ | [components.md](./components.md) | Component registration, hooks, traits, exclusivity, cleanup, aliases, and singletons. |
40
+ | [entities.md](./entities.md) | Entity creation, component mutation, relationship targets, lifecycle, deferred reads. |
41
41
  | [systems.md](./systems.md) | `world.system`, `with`/`without`, `enter`/`update`/`exit`, callback options, tick sources. |
42
42
  | [queries-and-filters.md](./queries-and-filters.md) | `QueryDSL`, `Query`, `Filter`, `groupBy`, `ignoreSource`. |
43
- | [relationships.md](./relationships.md) | `Relationship`, `ChildOf`/`DependsOn`, `parent`/`children`, `Traversable`, cascades. |
43
+ | [relationships.md](./relationships.md) | Relationships, target creation and ownership, hierarchy, traversal, and cleanup cascades. |
44
44
  | [modules.md](./modules.md) | `Module<Config>`, `world.module`, composition, built-in modules. |
45
45
  | [design-guide.md](./design-guide.md) | Patterns, anti-patterns, and a Phaser render-layer case study. |
46
46
  | [utilities.md](./utilities.md) | Public helper classes: `Events`, `ArrayMap`, `IterableArrayMap`, plus `Bitset` cross-ref. |
@@ -71,7 +71,7 @@ Pass `idPools` to the `World` constructor to change ranges (each entry is an `Id
71
71
  released this frame is not immediately recycled. `world.peekNextEid(poolName)` returns the next id
72
72
  a pool would allocate without reserving it. Id 0 is reserved.
73
73
 
74
- ## Hooks: `onAdd`, `onSet`, `onRemove`
74
+ ## Hooks: `onAdd`, `onSet`, `onRemove`, `onRetarget`
75
75
 
76
76
  The `Component` entity exposes lifecycle hooks fired for every entity carrying the component:
77
77
 
@@ -83,11 +83,15 @@ world
83
83
  .onRemove((entity, sprite) => sprite.destroy(scene, entity));
84
84
  ```
85
85
 
86
- | Hook | Fires when… |
87
- | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
88
- | `onAdd` | The component is first attached (`add`, or `set` on an entity that lacked it). |
89
- | `onSet` | `entity.set(C, props)` applies data, `entity.attach(instance)` stores an instance, `entity.modified(C)` is called, or `getMut` marks it dirty. |
90
- | `onRemove` | The component is detached (`entity.remove(C)`), or the owning entity is destroyed. |
86
+ | Hook | Fires when… |
87
+ | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
88
+ | `onAdd` | The component is first attached (`add`, or `set` on an entity that lacked it). |
89
+ | `onSet` | `entity.set(C, props)` applies data, `entity.attach(instance)` stores an instance, `entity.modified(C)` is called, or `getMut` / `ensure` marks it dirty. |
90
+ | `onRemove` | The component is detached (`entity.remove(C)`), or the owning entity is destroyed. |
91
+
92
+ Relationship component entities additionally expose `onRetarget`. It fires after an existing
93
+ relationship changes to a different target, once `target` and `children` already reflect the new
94
+ reverse-index state. Initial assignment, same-target `set`, and removal do not fire it.
91
95
 
92
96
  Handlers receive `(entity, componentInstance)` — component instances do not carry entity
93
97
  references, so the entity is passed explicitly. Hooks fire synchronously when the corresponding
@@ -95,6 +99,59 @@ command is applied (see [Execution model](./execution-model.md#reactive-routing)
95
99
  resource management and cross-cutting reactions that aren't worth a system; use systems when the
96
100
  reaction belongs to a phase.
97
101
 
102
+ ## Component traits: policy on component entities
103
+
104
+ Registered components are themselves entities. Attach a configuration component to a component
105
+ entity to change policy for every use of that component type. vecs calls these configuration
106
+ components **component traits**:
107
+
108
+ ```ts
109
+ world.component(Gravity).add(Singleton);
110
+ world.component(Inventory).add(OwnedTarget);
111
+ world.component(ChildOf).add(Traversable);
112
+ world.component(Orange).set(Implements, { interfaces: [Fruit] });
113
+ ```
114
+
115
+ | Trait/declaration | Applies to | Effect |
116
+ | ----------------- | ---------------------------------- | -------------------------------------------------------------------- |
117
+ | `Singleton` | Concrete components | Allows at most one carrier entity |
118
+ | `Implements` | Non-relationship components | Exposes concrete data through abstract aliases |
119
+ | `Traversable` | Relationships | Maintains hierarchy depth for cascade ordering |
120
+ | `OwnedTarget` | Relationships other than `ChildOf` | Owns the active target through `ChildOf` and destructive replacement |
121
+
122
+ Traits declare policy; their built-in modules install the hooks and maintain the metadata that
123
+ enforce it. Configure traits in feature-module setup rather than opportunistically inside systems.
124
+ Do not assume all traits have the same lifecycle:
125
+
126
+ - Add `Singleton` before component instances exist.
127
+ - Add `OwnedTarget` before relationship instances exist; it does not backfill.
128
+ - `Traversable` backfills existing relationship instances.
129
+ - Setting or removing `Implements` recomputes interface aliases.
130
+
131
+ Treat traits as setup policy unless their documentation explicitly supports runtime changes. Before
132
+ writing custom component-type bookkeeping, check whether an existing trait already expresses the
133
+ invariant.
134
+
135
+ Custom traits use the same mechanism. Keep the trait class as data and put its behavior in a module:
136
+
137
+ ```ts
138
+ class Audited {}
139
+
140
+ class AuditModule extends Module {
141
+ public init(): void {
142
+ this.world.component(Audited).onAdd((component) => {
143
+ component.onSet((entity) => console.log("changed", component.meta.Class.name, entity.eid));
144
+ });
145
+ }
146
+ }
147
+
148
+ world.module(AuditModule);
149
+ world.component(Position).add(Audited);
150
+ ```
151
+
152
+ Create a custom trait only when several component types need the same reusable, declarative policy.
153
+ For one component type, register hooks directly in its owning module.
154
+
98
155
  ## Exclusive component groups
99
156
 
100
157
  `world.setExclusiveComponents(A, B, C)` declares a mutually exclusive group: setting one
@@ -183,7 +240,7 @@ Rules (enforced by `ImplementsModule`, `lib/vecs/src/modules/implements.ts`):
183
240
 
184
241
  - **Aliases are read/query only.** `get`, `has`, `with`, `filter`, and injection accept an
185
242
  interface type; mutation must go through the concrete component. `add`, `set`, `remove`,
186
- `modified`, `getMut`, and `attach` on an interface throw at runtime (and `add`/`set` are
243
+ `modified`, `getMut`, `ensure`, and `attach` on an interface throw at runtime (and `add`/`set` are
187
244
  rejected by TypeScript for abstract classes).
188
245
  - **Declarations are transitive.** `Orange` implements `Fruit`, `Fruit` implements `Food`, so an
189
246
  entity with `Orange` matches `with(Food)`. Cyclic declarations throw.
@@ -249,12 +306,13 @@ interface markers for polymorphic dispatch — is in the
249
306
  A `Component<T>` is an `Entity` subclass, so the full [entity API](./entities.md#reference)
250
307
  applies. On top of it:
251
308
 
252
- | Member | Description |
253
- | ------------------- | ---------------------------------------------------------------- |
254
- | `onAdd(handler)` | Register an add hook. Returns the component entity for chaining. |
255
- | `onRemove(handler)` | Register a remove hook. |
256
- | `onSet(handler)` | Register a set/modified hook. |
257
- | `meta` | The `ComponentMeta` bookkeeping record. |
309
+ | Member | Description |
310
+ | --------------------- | -------------------------------------------------------------------- |
311
+ | `onAdd(handler)` | Register an add hook. Returns the component entity for chaining. |
312
+ | `onRemove(handler)` | Register a remove hook. |
313
+ | `onRetarget(handler)` | Register a relationship retarget hook. Throws for non-relationships. |
314
+ | `onSet(handler)` | Register a set/modified hook. |
315
+ | `meta` | The `ComponentMeta` bookkeeping record. |
258
316
 
259
317
  ### `ComponentMeta` (`lib/vecs/src/component_meta.ts`)
260
318
 
package/docs/concepts.md CHANGED
@@ -20,9 +20,10 @@ every idiom in the [Design guide](./design-guide.md) is a consequence of taking
20
20
  | **System** | A `Query` plus per-tick logic (`update`, `each`, `run`), assigned to one pipeline phase. |
21
21
  | **Query** | A reactive, always-up-to-date set of entities matching a predicate, with `enter`/`exit`/`update` callbacks. |
22
22
  | **Filter** | A non-reactive, one-shot scan: walks all world entities on each `forEach` call. |
23
- | **Hook** | Lightweight `onAdd` / `onRemove` / `onSet` callbacks registered per component class. |
23
+ | **Hook** | Lightweight component callbacks (`onAdd` / `onSet` / `onRemove`, plus relationship `onRetarget`). |
24
24
  | **Phase** | Marker component for entities used as pipeline ordering anchors. |
25
25
  | **Relationship** | A component whose `target` field points at another entity, with reverse lookup and cleanup cascades. |
26
+ | **Component trait** | Policy attached to a component entity, configuring every use of that component type. |
26
27
  | **Module** | A unit of packaging: a class whose `init()` registers components, systems, phases, and policy. |
27
28
  | **Exclusive components** | A group of components where at most one may exist on any entity at a time. |
28
29
  | **Implements** | Declares abstract/interface component aliases for a concrete component, for polymorphic queries. |
@@ -43,6 +44,11 @@ own. All behavior lives in systems (and, for small cross-cutting reactions, in h
43
44
  is what makes composition work: any system can operate on any entity that happens to carry the
44
45
  right components, and adding a capability to an entity is just adding a component.
45
46
 
47
+ Because a registered component is also an entity, it can itself carry components. Built-in
48
+ **component traits** use that meta-ECS layer to declare reusable policy: `Singleton` constrains a
49
+ component, `Implements` declares aliases, and `Traversable` / `OwnedTarget` configure relationships.
50
+ The trait remains data; a module owns the hooks and metadata behavior it activates.
51
+
46
52
  Prefer many small, single-purpose components over one large one. An entity that never rotates
47
53
  simply omits `Rotation`; absence encodes "not applicable". See
48
54
  [Components](./components.md).
@@ -77,7 +83,7 @@ on component combinations. The canonical example is the companion-component patt
77
83
 
78
84
  ## Where each topic lives
79
85
 
80
- - Registering, configuring, and hooking components → [Components](./components.md)
86
+ - Registering, configuring, hooking, and applying traits to components → [Components](./components.md)
81
87
  - Creating, mutating, and destroying entities → [Entities](./entities.md)
82
88
  - System membership, callbacks, and cadence → [Systems](./systems.md)
83
89
  - The query DSL, live queries, filters, grouping → [Queries and filters](./queries-and-filters.md)
@@ -154,6 +154,31 @@ Use them for resource management and cross-cutting reactions that aren't worth a
154
154
  the difference from systems: hooks are per-component-instance and run at the mutation site;
155
155
  systems are queries that run in a phase.)
156
156
 
157
+ ### Component traits: policy on component entities
158
+
159
+ A registered component is also an entity. Components attached to that component entity can declare
160
+ policy for every use of the component type; these configuration components are **component traits**:
161
+
162
+ ```ts
163
+ world.component(Gravity).add(Singleton);
164
+ world.component(Inventory).add(OwnedTarget);
165
+ world.component(ChildOf).add(Traversable);
166
+ world.component(Orange).set(Implements, { interfaces: [Fruit] });
167
+ ```
168
+
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 |
175
+
176
+ Traits declare policy; modules implement it through hooks and metadata. Configure them in module
177
+ setup, not inside systems. Lifecycle differs by trait: `OwnedTarget` and `Singleton` are setup-time
178
+ policy, `Traversable` backfills, and `Implements` recomputes declarations. Do not assume traits are
179
+ dynamically removable. See the [Components guide](./components.md#component-traits-policy-on-component-entities)
180
+ for the full model and custom-trait guidance.
181
+
157
182
  ### Exclusive component groups
158
183
 
159
184
  `world.setExclusiveComponents(A, B, C)` declares a mutually exclusive set: setting one
@@ -178,6 +203,18 @@ entity.target(InCell); // the target entity
178
203
  world.query("ByCell").with(Networked, Position).groupBy(InCell); // index by target
179
204
  ```
180
205
 
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:
209
+
210
+ ```ts
211
+ world.component(Inventory).add(OwnedTarget);
212
+ const inventory = player.ensureTarget(Inventory); // inventory is ChildOf player
213
+ ```
214
+
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
+
181
218
  - **`Relationship.target` is an `Entity` — not directly serialisable, but
182
219
  relationships can be networked** when listed in `networkComponents`.
183
220
  `vecs-protocol` registers `Relationship.prototype.target` as an `"entity"`
@@ -543,6 +580,86 @@ or when the callback is intentionally "future changes only."
543
580
  Bucket entities by a relationship target (or a derived key) and look up buckets in O(1) instead of
544
581
  scanning. Moving between buckets is a retarget (data change), not a component swap.
545
582
 
583
+ ### Owned target entities (entity-valued properties)
584
+
585
+ Before adding a nested object to a component or a side map from owner eid to state, ask whether the
586
+ value is really one ECS sub-entity. Use an owned target when all of these are true:
587
+
588
+ - The owner has zero or one value for the concern.
589
+ - The value needs its own optional components, systems, or query identity.
590
+ - The value is exclusively owned and never shared.
591
+ - Replacement, relationship removal, and owner destruction should destroy it.
592
+
593
+ ```ts
594
+ class Inventory extends Relationship {}
595
+ class InventoryState {}
596
+ class Capacity {
597
+ slots = 0;
598
+ }
599
+ class Weight {
600
+ current = 0;
601
+ }
602
+
603
+ world.component(Inventory).add(OwnedTarget);
604
+ world.component(InventoryState);
605
+ world.component(Capacity);
606
+ world.component(Weight);
607
+
608
+ const inventory = player.ensureTarget(Inventory);
609
+ inventory.add(InventoryState).set(Capacity, { slots: 20 }).set(Weight, { current: 0 });
610
+ ```
611
+
612
+ `Inventory` is an entity-valued property of `player`, but it remains entirely inside the ECS.
613
+ Inventory systems operate on inventory entities without adding inventory-specific state to every
614
+ player archetype:
615
+
616
+ ```ts
617
+ world
618
+ .system("inventory.weight")
619
+ .with(InventoryState, Weight)
620
+ .each([Weight], (_inventory, [weight]) => {
621
+ void weight.current;
622
+ });
623
+ ```
624
+
625
+ The lifecycle follows from the relationship and trait:
626
+
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 |
634
+
635
+ Inside deferred execution, mutate the entity returned by `ensureTarget`; do not immediately re-read
636
+ the relationship:
637
+
638
+ ```ts
639
+ world.defer(() => {
640
+ const inventory = player.ensureTarget(Inventory);
641
+ inventory.set(Capacity, { slots: 20 }); // correct: pending entity is usable
642
+ player.target(Inventory); // still undefined until flush
643
+ });
644
+ ```
645
+
646
+ Use the narrowest model that fits:
647
+
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 |
656
+
657
+ Good fits include an inventory aggregate, equipment loadout, character progression sheet, AI
658
+ blackboard, quest journal, ability book, or unique vehicle subsystem. Individual inventory items,
659
+ abilities, objectives, and other collections should remain separate entities. Networking remains
660
+ explicit: register the relationship and replicate the target's required components; `OwnedTarget`
661
+ itself is local lifecycle policy.
662
+
546
663
  ### Initialize via opt-in `update`-on-entry
547
664
 
548
665
  When an update callback owns both initialization and later changes, pass `{ onEnter: true }`. The
@@ -559,6 +676,10 @@ system after whatever produces the entity/companion.
559
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. |
560
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. |
561
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. |
562
683
  | Duplicated `enter` + `update` doing the same thing | One `update({ watch: C, onEnter: true })` when update owns initialization. |
563
684
  | One system trying to act in two phases | Split into two systems, one per phase. |
564
685
  | Custom phases for app/render logic | Use the standard taxonomy; only engine modules insert phases. |
@@ -588,6 +709,14 @@ system after whatever produces the entity/companion.
588
709
  `vecs-protocol` registers it as an `"entity"` wire field (u32 eid) at module
589
710
  load, so `Relationship` subclasses listed in `networkComponents` serialise
590
711
  correctly.
712
+ - **Component traits have trait-specific setup semantics.** In particular, configure `OwnedTarget`
713
+ 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`.
716
+ 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.
591
720
  - **`world.module(M)` runs `init` once.** Re-registering with a different config throws.
592
721
 
593
722
  ---
package/docs/entities.md CHANGED
@@ -47,6 +47,8 @@ for end-of-session teardown, not selective resets.
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
49
  e.get(Position); // Readonly<Position> | undefined
50
+ 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
50
52
  e.has(Position); // boolean
51
53
  e.remove(Position); // detach; fires exit callbacks, then onRemove
52
54
  e.modified(Position); // mark changed after in-place mutation
@@ -63,10 +65,12 @@ key.
63
65
  `onSet`.
64
66
  - `set(C, props)` attaches if needed (firing `onAdd`), copies `props` onto the instance, and fires
65
67
  `onSet` plus `update` callbacks.
68
+ - `ensure(C, callback)` creates the component if missing, then lets you mutate it and fires
69
+ `onAdd` followed by one `onSet`. When the component already exists, it behaves like `getMut`.
66
70
  - `modified(C)` fires `onSet`/`update` for data you already mutated in place. Repeated calls are
67
71
  coalesced until the world routes the notification.
68
72
 
69
- ### `getMut`: mutating in place
73
+ ### `getMut` and `ensure`: mutating in place
70
74
 
71
75
  `get` returns a `Readonly` view. To mutate, use `getMut`, which marks the component modified for
72
76
  you:
@@ -83,8 +87,50 @@ e.getMut(Velocity, (vel) => {
83
87
  });
84
88
  ```
85
89
 
86
- Calling `getMut(C)` without a callback outside deferred mode throws. `getMut` also throws for
87
- relationship components (retarget with `set` instead — see
90
+ Use `ensure` when absence should mean "create with defaults, then mutate":
91
+
92
+ ```ts
93
+ e.ensure(Velocity, (vel) => {
94
+ vel.vx = 2;
95
+ });
96
+ ```
97
+
98
+ `ensure` always returns a component for a live entity. Calling it on a destroyed entity throws.
99
+ When several calls ensure the same missing component during one deferred scope, they return the
100
+ same pending instance. The instance remains hidden from `get` and queries until the queue drains,
101
+ but every ensure callback mutates that shared pending value.
102
+
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
+ 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
88
134
  [Relationships](./relationships.md#retargeting)) and for `Implements` interface aliases.
89
135
 
90
136
  ### `attach`: storing an existing instance
@@ -111,6 +157,8 @@ Inside a system body, a `forEach`, or a `world.defer` block, all of the mutators
111
157
  commands instead of applying immediately, and reads return the pre-mutation state until the queue
112
158
  drains. The exact read-after-write rules are tabulated in
113
159
  [Execution model — deferred mutation](./execution-model.md#deferred-mutation-and-flush-boundaries).
160
+ `ensure(C)` is the exception for mutable access: it returns a mutable component immediately, but a
161
+ newly created component does not appear in `get(C)` until the queue drains.
114
162
 
115
163
  Deferred entity creation is idempotent within one deferred scope: if `world.entity()` or
116
164
  `getOrCreateEntity(eid)` creates a new entity, `getEntity(eid)`, `entity(eid)`, and later
@@ -135,6 +183,7 @@ Any entity can serve as a component key: `other.add(keyEntity)` attaches a marke
135
183
  type id is the key entity's eid. The key entity's `meta` (created lazily) carries the same hook
136
184
  and policy surface as a registered class, and `keyEntity.onAdd/onSet/onRemove(...)` register hooks
137
185
  directly. This is how tag-like, runtime-created component types work without declaring a class.
186
+ Registered relationship component entities additionally expose `onRetarget(...)`.
138
187
 
139
188
  ## Relationships, in brief
140
189
 
@@ -164,6 +213,8 @@ is in [Relationships](./relationships.md).
164
213
  | `set(ref, props)` | Attach if needed and assign `props`; fires `onSet` and `update` callbacks. Returns `this`. |
165
214
  | `get(ref)` | Read-only component lookup, or `undefined`. |
166
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. |
167
218
  | `has(ref)` | `true` when the component is attached. |
168
219
  | `modified(ref)` | Queue an `onSet`/`update` notification; coalesced while pending. Returns `this`. |
169
220
  | `remove(ref)` | Detach a component; routes `exit`, then fires `onRemove`. |
@@ -174,4 +225,5 @@ is in [Relationships](./relationships.md).
174
225
  | `parent` / `parent = p` | Getter/setter for the built-in `ChildOf` target; assigning `undefined` removes `ChildOf`. |
175
226
  | `destroyed` / `alive` | `true` when the entity has been / has not been destroyed (marked for deletion). |
176
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. |
177
229
  | `toString()` | Returns `"EntityN"`. |
@@ -97,9 +97,9 @@ built-in taxonomy.
97
97
  ## Deferred mutation and flush boundaries
98
98
 
99
99
  Inside a system body, a `Query.forEach` / `Filter.forEach` iteration, or any `world.defer(...)`
100
- block, the world is in **deferred mode**: entity mutations (`add` / `attach` / `set` / `remove` /
101
- `destroy` / `modified`, and `world.entity()` creation) are queued as commands instead of applied
102
- inline. The queue drains when the scope that opened deferral ends.
100
+ block, the world is in **deferred mode**: entity mutations (`add` / `attach` / `set` / `ensure` /
101
+ `ensureTarget` / `remove` / `destroy` / `modified`, and `world.entity()` creation) are queued as
102
+ commands instead of applied inline. The queue drains when the scope that opened deferral ends.
103
103
 
104
104
  For the pipeline this means: **a structural change made by system A is applied in the flush
105
105
  immediately after A finishes, so it is visible to every later system in the same tick** — later in
@@ -116,8 +116,12 @@ While a mutation is queued but not yet applied, reads return the pre-mutation st
116
116
  | `entity.add(C)` | `undefined` — no instance has been created yet. |
117
117
  | `entity.attach(instance)` | `undefined`, if `C` was absent before. |
118
118
  | `entity.set(C, props)` | The **previous** value (or `undefined` if `C` was absent). |
119
+ | `entity.ensure(C)` | A mutable component, even when newly created. |
119
120
  | `entity.remove(C)` | Still the component instance. |
120
121
 
122
+ `entity.ensureTarget(R)` similarly returns one shared pending target while `entity.target(R)`
123
+ continues to return the previously committed target until flush.
124
+
121
125
  Outside any deferred scope (top-level user code), the same calls execute inline and effects are
122
126
  visible immediately.
123
127
 
@@ -156,7 +160,7 @@ Three routing rules worth memorizing:
156
160
  - **`exit` reads a snapshot.** Components injected into an `exit` callback are captured when the
157
161
  exit is routed, so they are still resolvable even though they are being (or have been) removed.
158
162
  - **In-place mutation needs `modified`.** Assigning to component fields directly notifies no one.
159
- Call `entity.modified(C)` (or use `set` / `getMut`) to route the change.
163
+ Call `entity.modified(C)` (or use `set` / `getMut` / `ensure`) to route the change.
160
164
 
161
165
  ## Why the patterns work
162
166
 
package/docs/glossary.md CHANGED
@@ -50,8 +50,9 @@ entity lifecycle API. [Utilities](./utilities.md#events).
50
50
  **Filter** — a non-reactive, one-shot predicate that scans world entities on each `forEach`.
51
51
  [Queries and filters](./queries-and-filters.md#filters-one-shot-scans).
52
52
 
53
- **Hook** — an `onAdd` / `onSet` / `onRemove` callback registered per component class, fired
54
- synchronously at the mutation site. [Components](./components.md#hooks-onadd-onset-onremove).
53
+ **Hook** — an `onAdd` / `onSet` / `onRemove` callback registered per component class, or an
54
+ `onRetarget` callback for relationships, fired synchronously at the mutation site.
55
+ [Components](./components.md#hooks-onadd-onset-onremove-onretarget).
55
56
 
56
57
  **Implements / interface alias** — a declaration that a concrete component is queryable through
57
58
  abstract/interface component types; aliases are read-only views of the same instance.
@@ -71,6 +72,10 @@ visited entity and the source entity of each injected slot.
71
72
  **Module** — a packaging unit (`class M extends Module<Config>`) whose `init()` registers
72
73
  components, systems, and policy once per world. [Modules](./modules.md).
73
74
 
75
+ **Owned target entity** — a zero-or-one aggregate entity targeted through a relationship carrying
76
+ the `OwnedTarget` trait. It is parented to the source and destroyed when displaced, when the
77
+ relationship is removed, or when the source dies. [Relationships](./relationships.md#owned-targets).
78
+
74
79
  **Phase** — an entity used as a pipeline ordering anchor (tagged with the `Phase` component);
75
80
  systems attach to exactly one phase via `DependsOn`.
76
81
  [Execution model](./execution-model.md#phases-the-pipeline-spine).
@@ -102,6 +107,10 @@ assigned to one phase. [Systems](./systems.md).
102
107
  **Tick source** — a clock (`IntervalTickSource`, `RateTickSource`, or another system) that gates
103
108
  how often a system fires. [Systems](./systems.md#cadence-interval-rate-and-tick-sources).
104
109
 
110
+ **Trait (component trait)** — policy attached as a component to a registered component entity,
111
+ configuring every use of that component type. Built-ins include `Singleton`, `Implements`,
112
+ `Traversable`, and `OwnedTarget`. [Components](./components.md#component-traits-policy-on-component-entities).
113
+
105
114
  **Watchlist** — the set of component types a query or system reacts to via `update`, stored as a
106
115
  bitmask. [Systems](./systems.md#reactive-callbacks-enter-update-exit).
107
116
 
package/docs/modules.md CHANGED
@@ -89,13 +89,13 @@ Guidelines:
89
89
  Every `World` constructor installs these (`lib/vecs/src/world/world.ts`); you interact with their
90
90
  _components_, not with the module objects:
91
91
 
92
- | Module | Provides |
93
- | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
94
- | `IdentityModule` | The `Identity` name component and the name → entity index behind `world.entity("name")` / `entity.name`. |
95
- | `ImplementsModule` | Maintains [`Implements` interface aliases](./components.md#interface-aliases-with-implements) and their transitive closure. |
96
- | `SingletonModule` | Enforces the [`Singleton`](./components.md#singletons-and-world-global-data) one-carrier constraint. |
97
- | `RelationshipsModule` | Registers `ChildOf` (with the `Delete` cascade) and maintains [`Traversable` depth tracking](./relationships.md#traversal-order-traversable-and-cascade). |
98
- | `PipelineModule` | Creates the eight [built-in phases](./execution-model.md#phases-the-pipeline-spine) and installs the default pipeline query. |
92
+ | Module | Provides |
93
+ | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
94
+ | `IdentityModule` | The `Identity` name component and the name → entity index behind `world.entity("name")` / `entity.name`. |
95
+ | `ImplementsModule` | Maintains [`Implements` interface aliases](./components.md#interface-aliases-with-implements) and their transitive closure. |
96
+ | `SingletonModule` | Enforces the [`Singleton`](./components.md#singletons-and-world-global-data) one-carrier constraint. |
97
+ | `RelationshipsModule` | Registers `ChildOf`, installs [`OwnedTarget`](./relationships.md#owned-targets) ownership, and maintains [`Traversable` depth tracking](./relationships.md#traversal-order-traversable-and-cascade). |
98
+ | `PipelineModule` | Creates the eight [built-in phases](./execution-model.md#phases-the-pipeline-spine) and installs the default pipeline query. |
99
99
 
100
100
  All are exported, so a custom world setup can reuse them.
101
101