@vworlds/vecs 1.0.45 → 1.0.47
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/README.md +7 -1
- package/dist/component.d.ts +57 -60
- package/dist/component.js +24 -17
- package/dist/component.js.map +1 -1
- package/dist/component_meta.d.ts +67 -79
- package/dist/component_meta.js +331 -37
- package/dist/component_meta.js.map +1 -1
- package/dist/dsl.js +1 -1
- package/dist/dsl.js.map +1 -1
- package/dist/entity/entity.base.js +4 -0
- package/dist/entity/entity.base.js.map +1 -1
- package/dist/entity/entity.components.d.ts +39 -41
- package/dist/entity/entity.components.js +249 -82
- package/dist/entity/entity.components.js.map +1 -1
- package/dist/entity/entity.d.ts +1 -2
- package/dist/entity/entity.js +2 -3
- package/dist/entity/entity.js.map +1 -1
- package/dist/entity/entity.lifecycle.d.ts +7 -1
- package/dist/entity/entity.lifecycle.js +40 -50
- package/dist/entity/entity.lifecycle.js.map +1 -1
- package/dist/entity/entity.queries.js +30 -0
- package/dist/entity/entity.queries.js.map +1 -1
- package/dist/entity/entity.relationships.d.ts +4 -0
- package/dist/entity/entity.relationships.js +18 -10
- package/dist/entity/entity.relationships.js.map +1 -1
- package/dist/index.d.ts +7 -4
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/inject.d.ts +63 -14
- package/dist/inject.js +165 -89
- package/dist/inject.js.map +1 -1
- package/dist/modules/identity.js +2 -2
- package/dist/modules/identity.js.map +1 -1
- package/dist/modules/implements.js +2 -2
- package/dist/modules/implements.js.map +1 -1
- package/dist/modules/pipeline.js +5 -5
- package/dist/modules/pipeline.js.map +1 -1
- package/dist/modules/relationship_types.d.ts +70 -6
- package/dist/modules/relationship_types.js +70 -6
- package/dist/modules/relationship_types.js.map +1 -1
- package/dist/modules/relationships.d.ts +31 -9
- package/dist/modules/relationships.js +120 -44
- package/dist/modules/relationships.js.map +1 -1
- package/dist/modules/singleton.js +2 -2
- package/dist/modules/singleton.js.map +1 -1
- package/dist/modules/snapshot.d.ts +26 -0
- package/dist/modules/snapshot.js +45 -0
- package/dist/modules/snapshot.js.map +1 -0
- package/dist/modules/snapshot_types.d.ts +85 -0
- package/dist/modules/snapshot_types.js +130 -0
- package/dist/modules/snapshot_types.js.map +1 -0
- package/dist/package.json +2 -2
- package/dist/query/callbacks.d.ts +9 -7
- package/dist/query/callbacks.js +27 -41
- package/dist/query/callbacks.js.map +1 -1
- package/dist/query/grouped_query.js +7 -7
- package/dist/query/grouped_query.js.map +1 -1
- package/dist/query/grouping.d.ts +6 -2
- package/dist/query/grouping.js +84 -15
- package/dist/query/grouping.js.map +1 -1
- package/dist/query/query.d.ts +4 -4
- package/dist/query/query.js +37 -14
- package/dist/query/query.js.map +1 -1
- package/dist/snapshot/capture.d.ts +1 -0
- package/dist/snapshot/capture.js +212 -0
- package/dist/snapshot/capture.js.map +1 -0
- package/dist/snapshot/clone.d.ts +1 -0
- package/dist/snapshot/clone.js +107 -0
- package/dist/snapshot/clone.js.map +1 -0
- package/dist/snapshot/index.d.ts +5 -0
- package/dist/snapshot/index.js +21 -0
- package/dist/snapshot/index.js.map +1 -0
- package/dist/snapshot/preflight.d.ts +1 -0
- package/dist/snapshot/preflight.js +40 -0
- package/dist/snapshot/preflight.js.map +1 -0
- package/dist/snapshot/restore.d.ts +1 -0
- package/dist/snapshot/restore.js +111 -0
- package/dist/snapshot/restore.js.map +1 -0
- package/dist/snapshot/types.d.ts +135 -0
- package/dist/snapshot/types.js +26 -0
- package/dist/snapshot/types.js.map +1 -0
- package/dist/system.d.ts +3 -1
- package/dist/system.js +33 -23
- package/dist/system.js.map +1 -1
- package/dist/terms/all_term.js +19 -0
- package/dist/terms/all_term.js.map +1 -1
- package/dist/terms/any_term.js +19 -0
- package/dist/terms/any_term.js.map +1 -1
- package/dist/terms/not_term.js +4 -0
- package/dist/terms/not_term.js.map +1 -1
- package/dist/terms/target_term.d.ts +1 -2
- package/dist/terms/target_term.js +2 -4
- package/dist/terms/target_term.js.map +1 -1
- package/dist/terms/term.d.ts +2 -0
- package/dist/terms/term.js +59 -10
- package/dist/terms/term.js.map +1 -1
- package/dist/util/array_map.d.ts +43 -12
- package/dist/util/array_map.js +90 -41
- package/dist/util/array_map.js.map +1 -1
- package/dist/util/dense_set.js +26 -0
- package/dist/util/dense_set.js.map +1 -1
- package/dist/world/world.base.js +12 -0
- package/dist/world/world.base.js.map +1 -1
- package/dist/world/world.d.ts +7 -0
- package/dist/world/world.deferred.js +17 -0
- package/dist/world/world.deferred.js.map +1 -1
- package/dist/world/world.entities.d.ts +13 -2
- package/dist/world/world.entities.js +16 -2
- package/dist/world/world.entities.js.map +1 -1
- package/dist/world/world.js +15 -3
- package/dist/world/world.js.map +1 -1
- package/dist/world/world.pipeline.d.ts +2 -2
- package/dist/world/world.pipeline.js +4 -2
- package/dist/world/world.pipeline.js.map +1 -1
- package/dist/world/world.queries.d.ts +7 -0
- package/dist/world/world.queries.js +52 -0
- package/dist/world/world.queries.js.map +1 -1
- package/dist/world/world.transfers.d.ts +137 -0
- package/dist/world/world.transfers.js +188 -0
- package/dist/world/world.transfers.js.map +1 -0
- package/docs/README.md +18 -6
- package/docs/components.md +90 -53
- package/docs/concepts.md +21 -17
- package/docs/design-guide.md +125 -94
- package/docs/entities.md +62 -76
- package/docs/execution-model.md +26 -10
- package/docs/getting-started.md +6 -6
- package/docs/glossary.md +19 -10
- package/docs/modules.md +8 -8
- package/docs/queries-and-filters.md +15 -10
- package/docs/relationships.md +245 -124
- package/docs/snapshots.md +181 -0
- package/docs/systems.md +64 -21
- package/docs/utilities.md +10 -6
- package/package.json +2 -2
- package/skills/vecs/SKILL.md +5 -4
package/docs/relationships.md
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
# Relationships
|
|
2
2
|
|
|
3
|
-
Link entities to entities with relationship components:
|
|
4
|
-
cleanup cascades, and depth-ordered traversal.
|
|
3
|
+
Link entities to entities with relationship components: many-target edges, reverse lookup,
|
|
4
|
+
one-target constraints, ownership, cleanup cascades, and depth-ordered traversal.
|
|
5
5
|
|
|
6
6
|
## What a relationship is
|
|
7
7
|
|
|
8
|
-
A relationship is a component
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
A relationship is a component with private target storage managed by vecs. Define one by extending
|
|
9
|
+
the abstract `Relationship` base (`lib/vecs/src/component_meta.ts`) and register it like any
|
|
10
|
+
component:
|
|
11
11
|
|
|
12
12
|
```ts
|
|
13
13
|
import { Relationship } from "@vworlds/vecs";
|
|
@@ -16,163 +16,262 @@ class EquippedBy extends Relationship {}
|
|
|
16
16
|
|
|
17
17
|
world.component(EquippedBy);
|
|
18
18
|
|
|
19
|
-
item.
|
|
19
|
+
item.add(EquippedBy, player);
|
|
20
20
|
item.target(EquippedBy); // player
|
|
21
|
+
item.targets(EquippedBy); // readonly snapshot: [player]
|
|
22
|
+
item.hasTarget(EquippedBy, player); // true
|
|
21
23
|
player.children(EquippedBy).has(item); // true
|
|
22
24
|
```
|
|
23
25
|
|
|
24
|
-
The **source** entity stores the relationship component;
|
|
25
|
-
with `children(ref)` through a reverse index the world maintains automatically.
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
The **source** entity stores the relationship component; each **target** can enumerate its sources
|
|
27
|
+
with `children(ref)` through a reverse index the world maintains automatically. Relationships allow
|
|
28
|
+
many targets by default. Each source/relationship/target edge is unique, so adding the same target
|
|
29
|
+
again is idempotent. Target ordering is not a public contract.
|
|
28
30
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
entity (creating a ghost placeholder if the parent hasn't arrived yet). List `ChildOf` (or custom
|
|
34
|
-
subclasses) explicitly in your `networkComponents` array to replicate parent-child links.
|
|
31
|
+
`target(ref)` is a singular convenience that returns one current target, or `undefined`. Use
|
|
32
|
+
`targets(ref)` for all targets; it returns a safe readonly array snapshot, not vecs's internal
|
|
33
|
+
storage. For a non-exclusive relationship, do not use `target(ref)` as a stable selection policy.
|
|
34
|
+
Use `hasTarget(ref, target)` to test one edge without allocating a snapshot.
|
|
35
35
|
|
|
36
36
|
## Built-in relationships
|
|
37
37
|
|
|
38
38
|
| Class | Purpose |
|
|
39
39
|
| --------------- | -------------------------------------------------------------------------------------------------------------------------- |
|
|
40
|
-
| `ChildOf` |
|
|
41
|
-
| `DependsOn` |
|
|
42
|
-
| `SilenceSource` |
|
|
40
|
+
| `ChildOf` | Exclusive parent/child hierarchy. `onDeleteTarget = Delete`: destroying a parent destroys its `ChildOf` children. |
|
|
41
|
+
| `DependsOn` | Exclusive pipeline ordering: phases chain to each other and systems attach to phases through it. |
|
|
42
|
+
| `SilenceSource` | Exclusive query silence domain; configured via [`ignoreSource`](./queries-and-filters.md#silencing-feedback-ignoresource). |
|
|
43
43
|
|
|
44
|
-
`
|
|
44
|
+
`ChildOf` and `DependsOn` are also `Traversable`; all three built-ins above are `Exclusive`.
|
|
45
45
|
|
|
46
|
-
##
|
|
46
|
+
## Adding, removing, and setting
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
independent entity is the correct default:
|
|
48
|
+
Create edges explicitly with `add(Relationship, target)`:
|
|
50
49
|
|
|
51
50
|
```ts
|
|
52
|
-
|
|
51
|
+
unit.add(VisibleTo, playerA).add(VisibleTo, playerB);
|
|
53
52
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
entity.target(FriendOf) === friend; // true
|
|
53
|
+
unit.remove(VisibleTo, playerA); // remove one edge
|
|
54
|
+
unit.remove(VisibleTo); // remove the component and every remaining edge
|
|
57
55
|
```
|
|
58
56
|
|
|
59
|
-
|
|
60
|
-
the
|
|
61
|
-
|
|
62
|
-
|
|
57
|
+
`add(Relationship)` without a target is invalid. Targets must be live entities in the same world as
|
|
58
|
+
the source. The `target` argument accepts any `ComponentRef` (registered class, numeric component
|
|
59
|
+
id, or entity) and is resolved eagerly via `world.entity(target)`, so the target entity must
|
|
60
|
+
already exist when the call is made. Removing a missing edge is a no-op; removing the final edge
|
|
61
|
+
also removes the relationship component.
|
|
63
62
|
|
|
64
|
-
|
|
65
|
-
source does not destroy the target. Add ownership explicitly when your model requires it. The
|
|
66
|
-
relationship class must be registered before calling `ensureTarget`.
|
|
63
|
+
A relationship subclass can still declare ordinary data fields:
|
|
67
64
|
|
|
68
|
-
|
|
65
|
+
```ts
|
|
66
|
+
class WeightedLink extends Relationship {
|
|
67
|
+
weight = 0;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
source.add(WeightedLink, target);
|
|
71
|
+
source.set(WeightedLink, { weight: 10 });
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
For relationships, `set` is **existing-only and data-only**. It updates ordinary fields on an
|
|
75
|
+
attached relationship, but does nothing when the relationship is absent and ignores target-storage
|
|
76
|
+
spellings. Edge changes must use `add` and `remove`; they never fire `onSet`.
|
|
69
77
|
|
|
70
|
-
|
|
71
|
-
|
|
78
|
+
`attach`, `ensure`, and `getMut` reject relationship components because replacing or mutating an
|
|
79
|
+
instance could bypass edge uniqueness, reverse indexes, and cleanup policies. Target storage is
|
|
80
|
+
private: there is no public `target` or `targets` field to assign. The former `ensureTarget` and
|
|
81
|
+
`onRetarget` APIs have been removed.
|
|
82
|
+
|
|
83
|
+
`entity.childOf(parent)` is shorthand for `entity.add(ChildOf, parent)`. The `parent` property reads
|
|
84
|
+
the first `ChildOf` target; assigning a parent adds or replaces that target because `ChildOf` is
|
|
85
|
+
exclusive, while assigning `undefined` removes `ChildOf`.
|
|
86
|
+
|
|
87
|
+
## Exclusive one-target relationships
|
|
88
|
+
|
|
89
|
+
Add the `Exclusive` trait to restrict a relationship to one target:
|
|
72
90
|
|
|
73
91
|
```ts
|
|
74
|
-
import {
|
|
92
|
+
import { Exclusive, Relationship } from "@vworlds/vecs";
|
|
75
93
|
|
|
76
|
-
class
|
|
94
|
+
class InCell extends Relationship {}
|
|
77
95
|
|
|
78
|
-
world.component(
|
|
96
|
+
world.component(InCell).add(Exclusive);
|
|
79
97
|
|
|
80
|
-
|
|
81
|
-
|
|
98
|
+
unit.add(InCell, oldCell);
|
|
99
|
+
unit.add(InCell, newCell); // atomically replaces oldCell with newCell
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Adding a different target atomically replaces the existing edge. Observers never see two targets:
|
|
103
|
+
the reverse indexes already reflect the replacement when the old edge's remove hook runs, followed
|
|
104
|
+
by the new edge's add hook. Re-adding the current target is a no-op.
|
|
105
|
+
|
|
106
|
+
`ChildOf`, `DependsOn`, and `SilenceSource` are `Exclusive` out of the box. Configure `Exclusive`
|
|
107
|
+
before relationship instances exist; vecs rejects first-time configuration or removal while the
|
|
108
|
+
relationship is in use (re-adding an already attached trait is an idempotent no-op), and rejects
|
|
109
|
+
removing it while `OwnedTarget` or `Traversable` depends on it. The checks run when the trait
|
|
110
|
+
component is actually attached or detached, so in deferred mode the rejection surfaces from
|
|
111
|
+
`flush()` / `progress()` rather than from the `add` / `remove` call site. A thrown trait operation
|
|
112
|
+
leaves the world partially mutated; treat it as a programming error and discard the world.
|
|
113
|
+
|
|
114
|
+
Do not confuse this trait with `world.setExclusiveComponents(A, B)`. `Exclusive` limits **one
|
|
115
|
+
relationship class** to one target. `setExclusiveComponents` declares that **different component
|
|
116
|
+
classes** cannot coexist on one entity (see
|
|
117
|
+
[Components](./components.md#exclusive-component-groups)).
|
|
118
|
+
|
|
119
|
+
## OneOf target constraint
|
|
120
|
+
|
|
121
|
+
Add the `OneOf` pair-form trait to constrain every target of a relationship to be a direct
|
|
122
|
+
`ChildOf` child of a declared parent entity. Use the pair form `R.add(OneOf, parent)`:
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
import { OneOf, Relationship } from "@vworlds/vecs";
|
|
126
|
+
|
|
127
|
+
class ResidesIn extends Relationship {}
|
|
128
|
+
class Region extends Relationship {}
|
|
82
129
|
|
|
83
|
-
|
|
84
|
-
|
|
130
|
+
world.component(Region);
|
|
131
|
+
world.component(ResidesIn).add(OneOf, world.component(Region));
|
|
132
|
+
|
|
133
|
+
const forest = world.entity().childOf(world.component(Region));
|
|
134
|
+
const weather = world.entity();
|
|
135
|
+
|
|
136
|
+
world.entity().add(ResidesIn, forest); // ok: forest is a child of Region
|
|
137
|
+
world.entity().add(ResidesIn, weather); // throws: weather is not a child of Region
|
|
85
138
|
```
|
|
86
139
|
|
|
87
|
-
|
|
88
|
-
|
|
140
|
+
For the enum-style form where the relationship's own entity is the parent, point `OneOf` at the
|
|
141
|
+
relationship itself. A faction diplomacy alignment is a natural fit — every alignment target must
|
|
142
|
+
be a declared value:
|
|
89
143
|
|
|
90
144
|
```ts
|
|
91
|
-
|
|
145
|
+
class Alignment extends Relationship {}
|
|
146
|
+
|
|
147
|
+
const alignment = world.component(Alignment).add(OneOf, world.component(Alignment));
|
|
148
|
+
const good = world.entity().childOf(alignment);
|
|
149
|
+
const evil = world.entity().childOf(alignment);
|
|
92
150
|
|
|
93
|
-
|
|
94
|
-
|
|
151
|
+
world.entity().add(Alignment, good); // ok: good is a child of Alignment
|
|
152
|
+
world.entity().add(Alignment, weather); // throws: weather is not a child of Alignment
|
|
95
153
|
```
|
|
96
154
|
|
|
97
|
-
`
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
(
|
|
101
|
-
|
|
102
|
-
|
|
155
|
+
`OneOf` checks `target.parent === parentEntity`, so only a **direct** `ChildOf` edge satisfies
|
|
156
|
+
the constraint — transitive descendants are rejected. The check runs in the relationship's
|
|
157
|
+
`targetPolicyAdd` hook, so in deferred mode an invalid target surfaces from `flush()` /
|
|
158
|
+
`progress()` rather than from the `add` call site. A thrown edge leaves the world partially
|
|
159
|
+
mutated; treat it as a programming error and discard the world (same contract as other
|
|
160
|
+
trait-invariant violations).
|
|
103
161
|
|
|
104
|
-
Configure
|
|
105
|
-
|
|
106
|
-
the
|
|
107
|
-
|
|
108
|
-
relationship can destroy it. `OwnedTarget` rejects ordinary components and `ChildOf` itself.
|
|
162
|
+
Configure `OneOf` before relationship instances exist. Attaching it while the relationship is
|
|
163
|
+
already in use throws, and removing it throws once attached — `OneOf` is irrevocable. Re-adding
|
|
164
|
+
`OneOf` to the same parent is an idempotent no-op. World teardown bypasses these guards, so
|
|
165
|
+
`clearAllEntities` can still tear down worlds with `OneOf` configured.
|
|
109
166
|
|
|
110
|
-
|
|
167
|
+
`OneOf` is independent of `Exclusive`: a non-exclusive relationship may carry it, in which case
|
|
168
|
+
every one of its many targets must be a child of the declared parent. It composes freely with
|
|
169
|
+
`OwnedTarget` and `Traversable`.
|
|
111
170
|
|
|
112
|
-
|
|
171
|
+
## Edge hooks
|
|
172
|
+
|
|
173
|
+
Relationship component entities expose one public hook slot for edge addition and one for edge
|
|
174
|
+
removal:
|
|
113
175
|
|
|
114
176
|
```ts
|
|
115
|
-
|
|
177
|
+
world
|
|
178
|
+
.component(InCell)
|
|
179
|
+
.on("targetAdd", (source, relationship, target) => {
|
|
180
|
+
// The edge and reverse index already exist.
|
|
181
|
+
})
|
|
182
|
+
.on("targetRemove", (source, relationship, target) => {
|
|
183
|
+
// The edge and reverse index are already gone.
|
|
184
|
+
});
|
|
116
185
|
```
|
|
117
186
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
187
|
+
Both handlers receive `(source, relationshipInstance, target)`. For a new relationship,
|
|
188
|
+
`onTargetAdd` runs before `onAdd`; query entry follows `onAdd`. Removing the whole relationship
|
|
189
|
+
routes query exit and detaches all edges, calls `onTargetRemove` once per edge, then
|
|
190
|
+
calls `onRemove`. An exclusive replacement calls `onTargetRemove` for the old target before
|
|
191
|
+
`onTargetAdd` for the new target without removing and re-adding the component. Edge changes route
|
|
192
|
+
relationship-dependent query updates, but do not call `onSet`; `onSet` is reserved for ordinary
|
|
193
|
+
relationship data changed through `set`.
|
|
123
194
|
|
|
124
|
-
|
|
125
|
-
|
|
195
|
+
Register handlers with `on("targetAdd", handler)` and `on("targetRemove", handler)`; remove them
|
|
196
|
+
with `off(event, handler)`. Multiple handlers may be registered for the same event and fire in
|
|
197
|
+
registration order. Framework traits install separate internal `targetPolicyAdd` /
|
|
198
|
+
`targetPolicyRemove` policies, so adding or removing a trait does not replace these public hooks.
|
|
126
199
|
|
|
127
|
-
|
|
200
|
+
## Owned targets
|
|
201
|
+
|
|
202
|
+
Use `OwnedTarget` when an exclusive relationship explicitly owns its target:
|
|
128
203
|
|
|
129
204
|
```ts
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
205
|
+
import { Exclusive, OwnedTarget, Relationship } from "@vworlds/vecs";
|
|
206
|
+
|
|
207
|
+
class Inventory extends Relationship {}
|
|
208
|
+
|
|
209
|
+
world.component(Inventory).add(Exclusive).add(OwnedTarget);
|
|
210
|
+
|
|
211
|
+
const inventory = world.entity();
|
|
212
|
+
player.add(Inventory, inventory);
|
|
213
|
+
inventory.parent === player; // true
|
|
214
|
+
|
|
215
|
+
player.remove(Inventory); // destroys inventory
|
|
135
216
|
```
|
|
136
217
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
218
|
+
`OwnedTarget` requires `Exclusive`, rejects ordinary components and `ChildOf`, and never creates a
|
|
219
|
+
target implicitly. You must create the target and pass it to `add`. When an edge is added, the target
|
|
220
|
+
becomes a `ChildOf` child of its source before the public `onTargetAdd` hook runs. When that edge is
|
|
221
|
+
removed, its target is destroyed before the public `onTargetRemove` hook runs. Exclusive replacement
|
|
222
|
+
therefore destroys the displaced target and adopts the replacement atomically; removing the
|
|
223
|
+
relationship or destroying its source destroys the current target.
|
|
224
|
+
|
|
225
|
+
Configure the traits before creating relationship instances. `OwnedTarget` is irrevocable once
|
|
226
|
+
attached: removing it throws, and only world teardown (`clearAllEntities`) bypasses the guard —
|
|
227
|
+
the same contract as `Traversable` and `OneOf`. Do not share an owned target between
|
|
228
|
+
sources because either owner can destroy it when its edge is removed.
|
|
140
229
|
|
|
141
230
|
## Cleanup when a target dies
|
|
142
231
|
|
|
143
|
-
What happens to
|
|
144
|
-
`meta.onDeleteTarget`:
|
|
232
|
+
What happens to each incoming edge when its target entity is destroyed is governed by the
|
|
233
|
+
relationship's `meta.onDeleteTarget`:
|
|
145
234
|
|
|
146
|
-
| Policy | Meaning
|
|
147
|
-
| ---------------------- |
|
|
148
|
-
| `CleanupPolicy.Remove` | Default.
|
|
149
|
-
| `CleanupPolicy.Delete` |
|
|
235
|
+
| Policy | Meaning |
|
|
236
|
+
| ---------------------- | ------------------------------------------------------------------------------------------ |
|
|
237
|
+
| `CleanupPolicy.Remove` | Default. Remove only the edge to the destroyed target; other targets remain attached. |
|
|
238
|
+
| `CleanupPolicy.Delete` | Destroy each source entity that has an edge to the destroyed target through this relation. |
|
|
150
239
|
|
|
151
240
|
```ts
|
|
152
241
|
import { CleanupPolicy } from "@vworlds/vecs";
|
|
153
242
|
|
|
154
|
-
world.component(MyRel).meta.onDeleteTarget = CleanupPolicy.Delete;
|
|
243
|
+
world.component(MyRel).meta.onDeleteTarget = CleanupPolicy.Delete;
|
|
155
244
|
```
|
|
156
245
|
|
|
157
246
|
`ChildOf` ships with `Delete`, so destroying a parent recursively destroys its subtree:
|
|
158
247
|
|
|
159
248
|
```ts
|
|
160
|
-
child.
|
|
161
|
-
parent.destroy(); // child
|
|
249
|
+
child.add(ChildOf, parent);
|
|
250
|
+
parent.destroy(); // child and its own ChildOf children are destroyed too
|
|
162
251
|
```
|
|
163
252
|
|
|
164
|
-
`
|
|
165
|
-
|
|
253
|
+
With the default `Remove`, cleanup follows normal per-edge semantics: `onTargetRemove` receives the
|
|
254
|
+
destroyed target, and the relationship component remains if another edge survives. Removing the
|
|
255
|
+
last edge also removes the component.
|
|
256
|
+
|
|
257
|
+
`onDeleteTarget` is independent from `onDelete`: `onDeleteTarget` applies when a relationship
|
|
258
|
+
**target** is destroyed; `onDelete` applies when the relationship **component entity itself** is
|
|
166
259
|
destroyed (see
|
|
167
260
|
[Components — cleanup policies](./components.md#cleanup-policies-destroying-a-component-entity)).
|
|
168
261
|
|
|
262
|
+
If a cleanup cascade hits an impossible combination — for example a `Delete` source that is itself an
|
|
263
|
+
in-use `Throw`-policy component entity — vecs throws and the world is unusable from that point. The
|
|
264
|
+
exception propagates out of the `destroy()` / `progress()` / `flush()` call that triggered the
|
|
265
|
+
cascade. Configure policies so cascades are consistent; this is a programming error, not a
|
|
266
|
+
recoverable runtime condition.
|
|
267
|
+
|
|
169
268
|
## Querying through relationships
|
|
170
269
|
|
|
171
270
|
The DSL follows relationships in both directions
|
|
172
271
|
([operator table](./queries-and-filters.md#the-query-dsl)):
|
|
173
272
|
|
|
174
273
|
```ts
|
|
175
|
-
// Reactive: candidates
|
|
274
|
+
// Reactive: candidates with any FriendOf target that has Position and Velocity.
|
|
176
275
|
world.query("body-friends").with({ all: [Body, { target: [FriendOf, [Position, Velocity]] }] });
|
|
177
276
|
|
|
178
277
|
// Reactive: candidates whose ChildOf parent has Body.
|
|
@@ -182,53 +281,75 @@ world.query("positioned-children").with({ all: [Position, { parent: Body }] });
|
|
|
182
281
|
world.filter({ all: [Body, { children: Position }] }).forEach((parent) => { ... });
|
|
183
282
|
```
|
|
184
283
|
|
|
185
|
-
`target`
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
index and are
|
|
189
|
-
systems.
|
|
284
|
+
`target` is existential: a source matches when **any** target through the relationship matches the
|
|
285
|
+
inner DSL. `target` / `parent` queries are live on both sides: edge changes re-evaluate the source,
|
|
286
|
+
and changes to any target's components refresh every source pointing at it, across nested `target`
|
|
287
|
+
hops. `source` / `children` look down the reverse index and are non-reactive: exact for each
|
|
288
|
+
`Filter` scan but rejected by tracked queries and systems.
|
|
190
289
|
|
|
191
|
-
To pull
|
|
192
|
-
[Systems — relationship injection](./systems.md#relationship-injection
|
|
290
|
+
To pull one current target or all targets into callbacks, use singular `target` or plural `targets`
|
|
291
|
+
injection; see [Systems — relationship injection](./systems.md#relationship-injection).
|
|
193
292
|
|
|
194
293
|
## Traversal order: `Traversable` and `cascade`
|
|
195
294
|
|
|
196
|
-
Tag
|
|
197
|
-
entity carrying the relationship gets a depth (hops to its root), maintained
|
|
198
|
-
`RelationshipsModule` (`lib/vecs/src/modules/relationships.ts`):
|
|
295
|
+
Tag an exclusive relationship component entity with `Traversable` to enable automatic depth
|
|
296
|
+
tracking: every entity carrying the relationship gets a depth (hops to its root), maintained
|
|
297
|
+
reactively by `RelationshipsModule` (`lib/vecs/src/modules/relationships.ts`):
|
|
199
298
|
|
|
200
299
|
```ts
|
|
201
|
-
import { Traversable } from "@vworlds/vecs";
|
|
300
|
+
import { Exclusive, Traversable } from "@vworlds/vecs";
|
|
202
301
|
|
|
203
|
-
world.component(InCell).add(Traversable);
|
|
302
|
+
world.component(InCell).add(Exclusive).add(Traversable);
|
|
204
303
|
```
|
|
205
304
|
|
|
206
|
-
|
|
305
|
+
`Traversable` requires `Exclusive` because a node needs one unambiguous parent. It backfills existing
|
|
306
|
+
exclusive edges when attached. A query can then order members depth-first with `cascade`:
|
|
207
307
|
|
|
208
308
|
```ts
|
|
209
309
|
world.query("tree").with({ with: Node, cascade: ChildOf });
|
|
210
310
|
```
|
|
211
311
|
|
|
212
|
-
Parents sort before children.
|
|
213
|
-
|
|
214
|
-
the box
|
|
215
|
-
[Execution model](./execution-model.md#the-pipeline-is-a-query)).
|
|
312
|
+
Parents sort before children. Replacing an exclusive edge updates relationship depths, and
|
|
313
|
+
cascade-ordered queries refresh before the next iteration. `ChildOf` and `DependsOn` are
|
|
314
|
+
`Exclusive` and `Traversable` out of the box; the world's default pipeline is itself a
|
|
315
|
+
`cascade: DependsOn` query (see [Execution model](./execution-model.md#the-pipeline-is-a-query)).
|
|
316
|
+
|
|
317
|
+
## Network snapshots
|
|
318
|
+
|
|
319
|
+
Relationship subclasses listed in a server/client world's `networkComponents` are networkable.
|
|
320
|
+
Their payload writes a u32 target count, every target eid, then the relationship's ordinary wire
|
|
321
|
+
fields. Target ids are sorted by the server for deterministic snapshots; duplicate, zero, or empty
|
|
322
|
+
target lists are rejected, and an `Exclusive` relationship rejects a payload with more than one
|
|
323
|
+
target.
|
|
324
|
+
|
|
325
|
+
The client reconciles every received edge and applies ordinary fields. If a target entity has not
|
|
326
|
+
arrived, it creates a networked ghost for that eid immediately; each missing target in a many-target
|
|
327
|
+
payload gets its own ghost. List `ChildOf` or a custom relationship explicitly in
|
|
328
|
+
`networkComponents` when those edges must replicate.
|
|
216
329
|
|
|
217
330
|
## Reference
|
|
218
331
|
|
|
219
|
-
| API
|
|
220
|
-
|
|
|
221
|
-
| `class R extends Relationship {}`
|
|
222
|
-
| `e.
|
|
223
|
-
| `e.target(R)`
|
|
224
|
-
| `e.
|
|
225
|
-
| `e.
|
|
226
|
-
| `e.
|
|
227
|
-
| `
|
|
228
|
-
| `
|
|
229
|
-
| `e.parent`
|
|
230
|
-
| `
|
|
231
|
-
| `world.component(R).add(
|
|
232
|
-
| `
|
|
233
|
-
| `
|
|
234
|
-
| `
|
|
332
|
+
| API | Description |
|
|
333
|
+
| ------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
334
|
+
| `class R extends Relationship {}` | Declare a many-target relationship with private target storage. |
|
|
335
|
+
| `e.add(R, target)` | Add one unique edge; atomically replace the edge when `R` is `Exclusive`. `target` accepts any `ComponentRef` resolved eagerly via `world.entity(target)`. |
|
|
336
|
+
| `e.remove(R, target)` / `e.remove(R)` | Remove one edge, or remove the relationship and all edges. `target` accepts any `ComponentRef` resolved eagerly via `world.entity(target)`. |
|
|
337
|
+
| `e.set(R, ordinaryProps)` | Update ordinary data on an existing relationship; never creates or changes edges. |
|
|
338
|
+
| `e.target(R)` | First target through `R`, or `undefined`. |
|
|
339
|
+
| `e.targets(R)` | Readonly snapshot of every target through `R`; ordering is unspecified. |
|
|
340
|
+
| `e.hasTarget(R, target)` | Test whether one edge exists. |
|
|
341
|
+
| `e.children(R)` | `ReadonlySet<Entity>` of sources targeting `e` through `R`. |
|
|
342
|
+
| `e.childOf(parent)` | Shorthand for `e.add(ChildOf, parent)`. |
|
|
343
|
+
| `e.parent` / `e.parent = p` | First built-in `ChildOf` target; assignment replaces it or removes `ChildOf` for `undefined`. |
|
|
344
|
+
| `world.component(R).add(Exclusive)` | Restrict `R` to one target. |
|
|
345
|
+
| `world.component(R).add(OneOf, parent)` | Reject targets of `R` that are not direct `ChildOf` children of `parent`. |
|
|
346
|
+
| `world.component(R).on("targetAdd", handler)` | Register a `(source, relationship, target)` edge-add handler. |
|
|
347
|
+
| `world.component(R).on("targetRemove", handler)` | Register a `(source, relationship, target)` edge-remove handler. |
|
|
348
|
+
| `world.component(R).add(OwnedTarget)` | Adopt and destroy explicit targets; requires `Exclusive`. |
|
|
349
|
+
| `meta.onDeleteTarget` | `Remove` (default) per edge or `Delete` the source when a target dies. |
|
|
350
|
+
| `world.component(R).add(Traversable)` | Enable depth tracking for cascade ordering; requires `Exclusive`. |
|
|
351
|
+
| `{ target: [R, q] }` / `{ parent: q }` | Reactive existential DSL operators through relationship targets. |
|
|
352
|
+
| `{ source: [R, q] }` / `{ children: q }` | Filter-only DSL operators down the reverse index. |
|
|
353
|
+
| `{ target: [R, projection] }` | Inject projections from one current target; selection is unspecified for non-exclusive relations. |
|
|
354
|
+
| `{ targets: [R, projection] }` | Inject one row-array slot covering all targets; see [Systems](./systems.md#relationship-injection). |
|
|
355
|
+
| `{ down: [R, [C...]] }` | Fan out over sources; see [Systems](./systems.md#relationship-injection). |
|