@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/README.md +2 -2
- package/dist/component.d.ts +2 -1
- package/dist/component.js.map +1 -1
- package/dist/component_meta.d.ts +10 -0
- package/dist/component_meta.js +13 -0
- package/dist/component_meta.js.map +1 -1
- package/dist/entity/entity.components.d.ts +40 -1
- package/dist/entity/entity.components.js +93 -11
- package/dist/entity/entity.components.js.map +1 -1
- package/dist/entity/entity.d.ts +4 -2
- package/dist/entity/entity.js +4 -2
- package/dist/entity/entity.js.map +1 -1
- package/dist/entity/entity.relationships.js +3 -0
- package/dist/entity/entity.relationships.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/modules/relationship_types.d.ts +15 -0
- package/dist/modules/relationship_types.js +15 -0
- package/dist/modules/relationship_types.js.map +1 -1
- package/dist/modules/relationships.d.ts +7 -2
- package/dist/modules/relationships.js +27 -3
- package/dist/modules/relationships.js.map +1 -1
- package/dist/package.json +1 -1
- package/docs/README.md +6 -6
- package/docs/components.md +71 -13
- package/docs/concepts.md +8 -2
- package/docs/design-guide.md +129 -0
- package/docs/entities.md +55 -3
- package/docs/execution-model.md +8 -4
- package/docs/glossary.md +11 -2
- package/docs/modules.md +7 -7
- package/docs/relationships.md +80 -13
- package/package.json +1 -1
- package/skills/vecs/SKILL.md +81 -0
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) —
|
|
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) |
|
|
40
|
-
| [entities.md](./entities.md) |
|
|
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) |
|
|
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. |
|
package/docs/components.md
CHANGED
|
@@ -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
|
|
253
|
-
|
|
|
254
|
-
| `onAdd(handler)`
|
|
255
|
-
| `onRemove(handler)`
|
|
256
|
-
| `
|
|
257
|
-
| `
|
|
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` / `
|
|
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
|
|
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)
|
package/docs/design-guide.md
CHANGED
|
@@ -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
|
-
|
|
87
|
-
|
|
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"`. |
|
package/docs/execution-model.md
CHANGED
|
@@ -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` / `
|
|
101
|
-
`destroy` / `modified`, and `world.entity()` creation) are queued as
|
|
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,
|
|
54
|
-
synchronously at the mutation site.
|
|
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
|
|
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
|
|