@vworlds/vecs 1.0.43 → 1.0.45
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 +27 -17
- package/dist/component.js +9 -0
- package/dist/component.js.map +1 -1
- package/dist/component_meta.d.ts +40 -13
- package/dist/component_meta.js +22 -6
- package/dist/component_meta.js.map +1 -1
- package/dist/entity/entity.components.d.ts +57 -4
- package/dist/entity/entity.components.js +121 -22
- 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.lifecycle.js +1 -4
- package/dist/entity/entity.lifecycle.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 +18 -0
- package/dist/modules/relationship_types.js +18 -0
- package/dist/modules/relationship_types.js.map +1 -1
- package/dist/modules/relationships.d.ts +8 -2
- package/dist/modules/relationships.js +41 -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 +77 -13
- package/docs/concepts.md +8 -2
- package/docs/design-guide.md +135 -1
- package/docs/entities.md +62 -6
- package/docs/execution-model.md +8 -4
- package/docs/glossary.md +11 -2
- package/docs/modules.md +7 -7
- package/docs/relationships.md +81 -0
- package/package.json +1 -1
- package/skills/vecs/SKILL.md +81 -0
package/docs/design-guide.md
CHANGED
|
@@ -152,7 +152,33 @@ world.component(Phaser.GameObjects.Arc).onRemove((_entity, obj) => obj.destroy()
|
|
|
152
152
|
|
|
153
153
|
Use them for resource management and cross-cutting reactions that aren't worth a system. (Note
|
|
154
154
|
the difference from systems: hooks are per-component-instance and run at the mutation site;
|
|
155
|
-
systems are queries that run in a phase.)
|
|
155
|
+
systems are queries that run in a phase.) Each event has one handler slot: setting another handler
|
|
156
|
+
replaces it, calling the hook method with no argument reads it, and passing `undefined` removes it.
|
|
157
|
+
|
|
158
|
+
### Component traits: policy on component entities
|
|
159
|
+
|
|
160
|
+
A registered component is also an entity. Components attached to that component entity can declare
|
|
161
|
+
policy for every use of the component type; these configuration components are **component traits**:
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
world.component(Gravity).add(Singleton);
|
|
165
|
+
world.component(Inventory).add(OwnedTarget);
|
|
166
|
+
world.component(ChildOf).add(Traversable);
|
|
167
|
+
world.component(Orange).set(Implements, { interfaces: [Fruit] });
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
| Trait/declaration | Reach for it when |
|
|
171
|
+
| ----------------- | -------------------------------------------------------------------------- |
|
|
172
|
+
| `Singleton` | At most one entity may carry the component |
|
|
173
|
+
| `Implements` | Concrete components should be queryable through abstract aliases |
|
|
174
|
+
| `Traversable` | A relationship hierarchy needs depth or cascade ordering |
|
|
175
|
+
| `OwnedTarget` | A relationship is an exclusively owned, zero-or-one entity-valued property |
|
|
176
|
+
|
|
177
|
+
Traits declare policy; modules implement it through hooks and metadata. Configure them in module
|
|
178
|
+
setup, not inside systems. Lifecycle differs by trait: `OwnedTarget` and `Singleton` are setup-time
|
|
179
|
+
policy, `Traversable` backfills, and `Implements` recomputes declarations. Do not assume traits are
|
|
180
|
+
dynamically removable. See the [Components guide](./components.md#component-traits-policy-on-component-entities)
|
|
181
|
+
for the full model and custom-trait guidance.
|
|
156
182
|
|
|
157
183
|
### Exclusive component groups
|
|
158
184
|
|
|
@@ -178,6 +204,21 @@ entity.target(InCell); // the target entity
|
|
|
178
204
|
world.query("ByCell").with(Networked, Position).groupBy(InCell); // index by target
|
|
179
205
|
```
|
|
180
206
|
|
|
207
|
+
Use `entity.ensureTarget(InCell)` when an absent relationship should default to a new independent
|
|
208
|
+
entity. For aggregate parts whose lifetime should remain bound to the source once assigned, attach
|
|
209
|
+
`OwnedTarget` during setup:
|
|
210
|
+
|
|
211
|
+
```ts
|
|
212
|
+
world.component(Inventory).add(OwnedTarget);
|
|
213
|
+
const inventory = player.ensureTarget(Inventory); // inventory is ChildOf player
|
|
214
|
+
player.add(Inventory); // also creates an owned target
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
`OwnedTarget` creates a target when the relationship is added, destroys displaced targets on
|
|
218
|
+
retarget, and destroys the current target when the relationship is removed, keeping aggregate
|
|
219
|
+
cleanup aligned with the active relationship. Configure the relationship owner's lifecycle hooks
|
|
220
|
+
before attaching the trait; it wraps those hooks and runs ownership first.
|
|
221
|
+
|
|
181
222
|
- **`Relationship.target` is an `Entity` — not directly serialisable, but
|
|
182
223
|
relationships can be networked** when listed in `networkComponents`.
|
|
183
224
|
`vecs-protocol` registers `Relationship.prototype.target` as an `"entity"`
|
|
@@ -543,6 +584,87 @@ or when the callback is intentionally "future changes only."
|
|
|
543
584
|
Bucket entities by a relationship target (or a derived key) and look up buckets in O(1) instead of
|
|
544
585
|
scanning. Moving between buckets is a retarget (data change), not a component swap.
|
|
545
586
|
|
|
587
|
+
### Owned target entities (entity-valued properties)
|
|
588
|
+
|
|
589
|
+
Before adding a nested object to a component or a side map from owner eid to state, ask whether the
|
|
590
|
+
value is really one ECS sub-entity. Use an owned target when all of these are true:
|
|
591
|
+
|
|
592
|
+
- The owner has zero or one value for the concern.
|
|
593
|
+
- The value needs its own optional components, systems, or query identity.
|
|
594
|
+
- The value is exclusively owned and never shared.
|
|
595
|
+
- Replacement, relationship removal, and owner destruction should destroy it.
|
|
596
|
+
|
|
597
|
+
```ts
|
|
598
|
+
class Inventory extends Relationship {}
|
|
599
|
+
class InventoryState {}
|
|
600
|
+
class Capacity {
|
|
601
|
+
slots = 0;
|
|
602
|
+
}
|
|
603
|
+
class Weight {
|
|
604
|
+
current = 0;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
world.component(Inventory).add(OwnedTarget);
|
|
608
|
+
world.component(InventoryState);
|
|
609
|
+
world.component(Capacity);
|
|
610
|
+
world.component(Weight);
|
|
611
|
+
|
|
612
|
+
const inventory = player.ensureTarget(Inventory);
|
|
613
|
+
inventory.add(InventoryState).set(Capacity, { slots: 20 }).set(Weight, { current: 0 });
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
`Inventory` is an entity-valued property of `player`, but it remains entirely inside the ECS.
|
|
617
|
+
Inventory systems operate on inventory entities without adding inventory-specific state to every
|
|
618
|
+
player archetype:
|
|
619
|
+
|
|
620
|
+
```ts
|
|
621
|
+
world
|
|
622
|
+
.system("inventory.weight")
|
|
623
|
+
.with(InventoryState, Weight)
|
|
624
|
+
.each([Weight], (_inventory, [weight]) => {
|
|
625
|
+
void weight.current;
|
|
626
|
+
});
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
The lifecycle follows from the relationship and trait:
|
|
630
|
+
|
|
631
|
+
| Operation | Result |
|
|
632
|
+
| -------------------------------------- | ---------------------------------------- |
|
|
633
|
+
| `player.add(Inventory)` | Creates the inventory entity |
|
|
634
|
+
| First `player.ensureTarget(Inventory)` | Creates and returns the inventory entity |
|
|
635
|
+
| Later `ensureTarget(Inventory)` | Returns the same target |
|
|
636
|
+
| Retarget `Inventory` | Destroys the displaced inventory |
|
|
637
|
+
| Remove `Inventory` | Destroys the current inventory |
|
|
638
|
+
| Destroy `player` | `ChildOf` destroys the inventory |
|
|
639
|
+
|
|
640
|
+
Inside deferred execution, mutate the entity returned by `ensureTarget`; do not immediately re-read
|
|
641
|
+
the relationship:
|
|
642
|
+
|
|
643
|
+
```ts
|
|
644
|
+
world.defer(() => {
|
|
645
|
+
const inventory = player.ensureTarget(Inventory);
|
|
646
|
+
inventory.set(Capacity, { slots: 20 }); // correct: pending entity is usable
|
|
647
|
+
player.target(Inventory); // still undefined until flush
|
|
648
|
+
});
|
|
649
|
+
```
|
|
650
|
+
|
|
651
|
+
Use the narrowest model that fits:
|
|
652
|
+
|
|
653
|
+
| Need | Model |
|
|
654
|
+
| --------------------------------------------------- | ------------------------------------------ |
|
|
655
|
+
| A few fields sharing the owner's identity/lifecycle | Components directly on the owner |
|
|
656
|
+
| One composable, exclusively owned sub-entity | `ensureTarget` + `OwnedTarget` |
|
|
657
|
+
| One shared or independently living linked entity | Relationship without `OwnedTarget` |
|
|
658
|
+
| Zero-to-many subobjects | Child entities with identifying components |
|
|
659
|
+
| A non-ECS runtime object | `attach(instance)` |
|
|
660
|
+
| World-global state | Singleton/component entity |
|
|
661
|
+
|
|
662
|
+
Good fits include an inventory aggregate, equipment loadout, character progression sheet, AI
|
|
663
|
+
blackboard, quest journal, ability book, or unique vehicle subsystem. Individual inventory items,
|
|
664
|
+
abilities, objectives, and other collections should remain separate entities. Networking remains
|
|
665
|
+
explicit: register the relationship and replicate the target's required components; `OwnedTarget`
|
|
666
|
+
itself is local lifecycle policy.
|
|
667
|
+
|
|
546
668
|
### Initialize via opt-in `update`-on-entry
|
|
547
669
|
|
|
548
670
|
When an update callback owns both initialization and later changes, pass `{ onEnter: true }`. The
|
|
@@ -559,6 +681,10 @@ system after whatever produces the entity/companion.
|
|
|
559
681
|
| An imperative runtime object with `start()/stop()/track/enable` | A **module** + the world's own lifecycle (`clearAllEntities()`, closing the world). Systems live on the world; they don't need a wrapper to turn on. |
|
|
560
682
|
| Builder/factory helpers that _create entities_ (`createArc(world, …)`) | Let callers `world.entity().add(Networked).set(Arc, …).set(FillStyle, …)`. Components are small and optional; the caller adds exactly what it wants. |
|
|
561
683
|
| A god component with many optional fields | Split into small single-purpose components; absence encodes "not applicable." |
|
|
684
|
+
| A large nested `inventory`, `brain`, or progression object | Use an owned target entity when the subobject needs its own components, systems, and lifecycle. |
|
|
685
|
+
| A side `Map<ownerEid, SubsystemState>` | Use `owner.ensureTarget(Rel)`; add `OwnedTarget` only for exclusive destructive ownership. |
|
|
686
|
+
| `OwnedTarget` on `CurrentEnemy`, `InCell`, `Team`, or shared state | Use a normal relationship; retargeting or removal of an owned relationship destroys its target. |
|
|
687
|
+
| One owned target used as a collection | Use multiple child/item entities; owned targets model zero-or-one aggregates. |
|
|
562
688
|
| Duplicated `enter` + `update` doing the same thing | One `update({ watch: C, onEnter: true })` when update owns initialization. |
|
|
563
689
|
| One system trying to act in two phases | Split into two systems, one per phase. |
|
|
564
690
|
| Custom phases for app/render logic | Use the standard taxonomy; only engine modules insert phases. |
|
|
@@ -588,6 +714,14 @@ system after whatever produces the entity/companion.
|
|
|
588
714
|
`vecs-protocol` registers it as an `"entity"` wire field (u32 eid) at module
|
|
589
715
|
load, so `Relationship` subclasses listed in `networkComponents` serialise
|
|
590
716
|
correctly.
|
|
717
|
+
- **Component traits have trait-specific setup semantics.** In particular, configure `OwnedTarget`
|
|
718
|
+
before creating relationship instances; it does not backfill.
|
|
719
|
+
- **`OwnedTarget` is destructive and exclusive.** Retargeting destroys the old target, removing the
|
|
720
|
+
relationship destroys the current target, and owner destruction cascades through `ChildOf`.
|
|
721
|
+
Never use it for shared teams, cells, enemies, definitions, or other world entities.
|
|
722
|
+
- **Owned targets are zero-or-one.** Use child entities for collections, and during deferred
|
|
723
|
+
execution mutate the entity returned by `ensureTarget` instead of re-reading `target(R)` before
|
|
724
|
+
flush.
|
|
591
725
|
- **`world.module(M)` runs `init` once.** Re-registering with a different config throws.
|
|
592
726
|
|
|
593
727
|
---
|
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,53 @@ 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
|
+
player.add(Inventory); // also creates an owned target
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
The relationship module creates a target when an owned relationship is added, adds `ChildOf`
|
|
131
|
+
whenever `Inventory` is assigned, destroys displaced targets on retarget, and destroys the current
|
|
132
|
+
target when the relationship is removed. Destroying `player` therefore also destroys `inventory`.
|
|
133
|
+
See [Relationships](./relationships.md#owned-targets).
|
|
134
|
+
|
|
135
|
+
Calling `getMut(C)` or `ensure(C)` without a callback outside deferred mode throws. Both methods
|
|
136
|
+
also throw for relationship components (retarget with `set` instead — see
|
|
88
137
|
[Relationships](./relationships.md#retargeting)) and for `Implements` interface aliases.
|
|
89
138
|
|
|
90
139
|
### `attach`: storing an existing instance
|
|
@@ -111,6 +160,8 @@ Inside a system body, a `forEach`, or a `world.defer` block, all of the mutators
|
|
|
111
160
|
commands instead of applying immediately, and reads return the pre-mutation state until the queue
|
|
112
161
|
drains. The exact read-after-write rules are tabulated in
|
|
113
162
|
[Execution model — deferred mutation](./execution-model.md#deferred-mutation-and-flush-boundaries).
|
|
163
|
+
`ensure(C)` is the exception for mutable access: it returns a mutable component immediately, but a
|
|
164
|
+
newly created component does not appear in `get(C)` until the queue drains.
|
|
114
165
|
|
|
115
166
|
Deferred entity creation is idempotent within one deferred scope: if `world.entity()` or
|
|
116
167
|
`getOrCreateEntity(eid)` creates a new entity, `getEntity(eid)`, `entity(eid)`, and later
|
|
@@ -133,8 +184,10 @@ it throws while carriers remain ([Components — cleanup policies](./components.
|
|
|
133
184
|
|
|
134
185
|
Any entity can serve as a component key: `other.add(keyEntity)` attaches a marker component whose
|
|
135
186
|
type id is the key entity's eid. The key entity's `meta` (created lazily) carries the same hook
|
|
136
|
-
and policy surface as a registered class, and `keyEntity.onAdd/onSet/onRemove(...)`
|
|
137
|
-
directly. This is how tag-like, runtime-created component
|
|
187
|
+
and policy surface as a registered class, and `keyEntity.onAdd/onSet/onRemove(...)` get, set, or
|
|
188
|
+
remove its single lifecycle handlers directly. This is how tag-like, runtime-created component
|
|
189
|
+
types work without declaring a class. Registered relationship component entities additionally
|
|
190
|
+
expose `onRetarget(...)`.
|
|
138
191
|
|
|
139
192
|
## Relationships, in brief
|
|
140
193
|
|
|
@@ -164,6 +217,8 @@ is in [Relationships](./relationships.md).
|
|
|
164
217
|
| `set(ref, props)` | Attach if needed and assign `props`; fires `onSet` and `update` callbacks. Returns `this`. |
|
|
165
218
|
| `get(ref)` | Read-only component lookup, or `undefined`. |
|
|
166
219
|
| `getMut(ref)` / `getMut(ref, callback)` | Mutable lookup that marks the component modified. Callback form required outside deferred mode. |
|
|
220
|
+
| `ensure(ref)` / `ensure(ref, callback)` | Mutable lookup that creates when missing. Callback form required outside deferred mode. |
|
|
221
|
+
| `ensureTarget(Relationship)` | Return a relationship target, creating an independent target when absent. |
|
|
167
222
|
| `has(ref)` | `true` when the component is attached. |
|
|
168
223
|
| `modified(ref)` | Queue an `onSet`/`update` notification; coalesced while pending. Returns `this`. |
|
|
169
224
|
| `remove(ref)` | Detach a component; routes `exit`, then fires `onRemove`. |
|
|
@@ -173,5 +228,6 @@ is in [Relationships](./relationships.md).
|
|
|
173
228
|
| `childOf(parent)` | Shorthand for `set(ChildOf, { target: parent })`. Returns `this`. |
|
|
174
229
|
| `parent` / `parent = p` | Getter/setter for the built-in `ChildOf` target; assigning `undefined` removes `ChildOf`. |
|
|
175
230
|
| `destroyed` / `alive` | `true` when the entity has been / has not been destroyed (marked for deletion). |
|
|
176
|
-
| `onAdd/onSet/onRemove(handler)`
|
|
231
|
+
| `onAdd/onSet/onRemove(handler?)` | Get, set, or remove this component key's single hook. Setters return `this`. |
|
|
232
|
+
| `onRetarget(handler?)` | Get, set, or remove a relationship component entity's single retarget hook. |
|
|
177
233
|
| `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
|
|
package/docs/relationships.md
CHANGED
|
@@ -43,6 +43,70 @@ subclasses) explicitly in your `networkComponents` array to replicate parent-chi
|
|
|
43
43
|
|
|
44
44
|
`entity.childOf(parent)` is shorthand for `entity.set(ChildOf, { target: parent })`.
|
|
45
45
|
|
|
46
|
+
## Ensuring a target
|
|
47
|
+
|
|
48
|
+
Use `ensureTarget` when a registered relationship should always have some target and a new,
|
|
49
|
+
independent entity is the correct default:
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
world.component(FriendOf);
|
|
53
|
+
|
|
54
|
+
const friend = entity.ensureTarget(FriendOf);
|
|
55
|
+
entity.ensureTarget(FriendOf) === friend; // true
|
|
56
|
+
entity.target(FriendOf) === friend; // true
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
The method returns the existing target when present. Otherwise it creates an entity, initializes
|
|
60
|
+
the relationship with that target, and attaches the relationship. During deferred execution,
|
|
61
|
+
repeated calls share one pending relationship and return the same target; `target(FriendOf)` still
|
|
62
|
+
returns `undefined` until flush.
|
|
63
|
+
|
|
64
|
+
The generated target is independent: `ensureTarget` does not add `ChildOf`, and destroying the
|
|
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`.
|
|
67
|
+
|
|
68
|
+
## Owned targets
|
|
69
|
+
|
|
70
|
+
Mark a relationship component with `OwnedTarget` during world setup when every target assigned
|
|
71
|
+
through that relationship should become a `ChildOf` child of its source:
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
import { OwnedTarget, Relationship } from "@vworlds/vecs";
|
|
75
|
+
|
|
76
|
+
class Inventory extends Relationship {}
|
|
77
|
+
|
|
78
|
+
world.component(Inventory).add(OwnedTarget);
|
|
79
|
+
|
|
80
|
+
const inventory = player.ensureTarget(Inventory);
|
|
81
|
+
inventory.parent === player; // true
|
|
82
|
+
|
|
83
|
+
player.destroy();
|
|
84
|
+
inventory.destroyed === true; // ChildOf cascades deletion
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Use `add` when the relationship itself represents a feature and initialization belongs in an
|
|
88
|
+
`onAdd` hook or system `enter` callback:
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
world.component(Inventory).add(OwnedTarget);
|
|
92
|
+
|
|
93
|
+
player.add(Inventory);
|
|
94
|
+
const inventory = player.target(Inventory)!; // target was created by OwnedTarget
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
`OwnedTarget` wraps `onAdd`, `onSet`, `onRetarget`, and `onRemove` on `Inventory`. Adding the
|
|
98
|
+
relationship creates a target when needed. New targets become `ChildOf` children, displaced targets
|
|
99
|
+
are destroyed on retarget, and the current target is destroyed when the relationship is removed
|
|
100
|
+
(including source destruction). Ownership behavior runs before the relationship owner's existing
|
|
101
|
+
hook, so owner hooks and query entry observe an initialized target. During deferred execution, these
|
|
102
|
+
changes apply while the command queue drains.
|
|
103
|
+
|
|
104
|
+
Configure the trait before creating relationship instances; it does not backfill existing targets.
|
|
105
|
+
Configure owner lifecycle hooks before attaching the trait, because later hook assignment replaces
|
|
106
|
+
the wrapper and its ownership behavior.
|
|
107
|
+
Do not share one owned target between several sources: retargeting or removing any owning
|
|
108
|
+
relationship can destroy it. `OwnedTarget` rejects ordinary components and `ChildOf` itself.
|
|
109
|
+
|
|
46
110
|
## Retargeting
|
|
47
111
|
|
|
48
112
|
Point a relationship somewhere else with a plain `set`:
|
|
@@ -60,6 +124,20 @@ lookups.
|
|
|
60
124
|
`getMut` throws for relationship components: in-place target mutation would bypass the reverse
|
|
61
125
|
index used by `children()` and the cleanup cascades. Always retarget with `set`.
|
|
62
126
|
|
|
127
|
+
Register `onRetarget` when behavior needs both targets:
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
world.component(InCell).onRetarget((entity, oldCell, newCell) => {
|
|
131
|
+
// entity.target(InCell) is newCell here.
|
|
132
|
+
// oldCell.children(InCell) no longer contains entity.
|
|
133
|
+
// newCell.children(InCell) already contains entity.
|
|
134
|
+
});
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
It fires only when an existing relationship moves to a different existing target, after reverse
|
|
138
|
+
indexes update and before `onSet`. Initial assignment, same-target writes, and removal do not fire
|
|
139
|
+
`onRetarget`; use `onSet` and `onRemove` for those lifecycle points.
|
|
140
|
+
|
|
63
141
|
## Cleanup when a target dies
|
|
64
142
|
|
|
65
143
|
What happens to sources when their target entity is destroyed is governed by the relationship's
|
|
@@ -145,6 +223,9 @@ the box — the world's default pipeline is itself a `cascade: DependsOn` query
|
|
|
145
223
|
| `e.target(R)` | The target entity through relationship `R`, or `undefined`. |
|
|
146
224
|
| `e.children(R)` | `ReadonlySet<Entity>` of sources targeting `e` through `R`. |
|
|
147
225
|
| `e.childOf(parent)` | Shorthand for `e.set(ChildOf, { target: parent })`. |
|
|
226
|
+
| `e.ensureTarget(R)` | Return `R`'s target, creating an independent entity when absent. |
|
|
227
|
+
| `world.component(R).add(OwnedTarget)` | Create and own `R` targets as `ChildOf` children of their sources. |
|
|
228
|
+
| `world.component(R).onRetarget(handler?)` | Get, set, or remove the single retarget handler. |
|
|
148
229
|
| `e.parent` / `e.parent = p` | Getter/setter for the built-in `ChildOf` target; setting `undefined` removes `ChildOf`. |
|
|
149
230
|
| `meta.onDeleteTarget` | `Remove` (default) or `Delete` cascade when a target is destroyed. |
|
|
150
231
|
| `world.component(R).add(Traversable)` | Enable depth tracking for `cascade` ordering. |
|
package/package.json
CHANGED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: vecs
|
|
3
|
+
description: "Designs, implements, reviews, and debugs games or applications using @vworlds/vecs. Use whenever a task creates or changes vecs components, entities, systems, queries, relationships, modules, phases, lifecycle, networked components, or ECS architecture."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# vecs Application Development
|
|
7
|
+
|
|
8
|
+
Use the documentation shipped with the consuming project's installed `@vworlds/vecs` package.
|
|
9
|
+
Those docs match the API version the project actually uses and are the source of truth for vecs
|
|
10
|
+
design and behavior.
|
|
11
|
+
|
|
12
|
+
## Before Working
|
|
13
|
+
|
|
14
|
+
1. Locate `node_modules/@vworlds/vecs/docs/`. In a workspace or non-npm install, resolve the
|
|
15
|
+
installed `@vworlds/vecs` package and use its adjacent `docs/` directory.
|
|
16
|
+
2. Read `docs/design-guide.md` sections 1, 2, 8, and 9 before designing or changing ECS behavior.
|
|
17
|
+
3. Read the task-specific sections of that design guide:
|
|
18
|
+
- Components or replicated state: section 3.
|
|
19
|
+
- Systems or queries: section 4.
|
|
20
|
+
- Ordering or timing: section 5.
|
|
21
|
+
- Feature architecture: section 6.
|
|
22
|
+
- Lifecycle and synchronization patterns: section 7.
|
|
23
|
+
4. Use `docs/README.md` to select any detailed subsystem guides needed by the task. Read
|
|
24
|
+
`docs/concepts.md` and `docs/execution-model.md` when the design depends on unfamiliar vecs
|
|
25
|
+
behavior.
|
|
26
|
+
5. Inspect the consuming project's existing vecs modules and conventions before proposing a new
|
|
27
|
+
pattern.
|
|
28
|
+
|
|
29
|
+
Do not rely on memory or a different vecs checkout when installed documentation is available.
|
|
30
|
+
Confirm exact names and signatures against the installed declarations under
|
|
31
|
+
`node_modules/@vworlds/vecs/dist/`.
|
|
32
|
+
|
|
33
|
+
## Design From ECS Semantics
|
|
34
|
+
|
|
35
|
+
Start from the vecs golden rule: components are data, systems are behavior, and component presence
|
|
36
|
+
and change drive lifecycle.
|
|
37
|
+
|
|
38
|
+
Before implementing, identify:
|
|
39
|
+
|
|
40
|
+
- The state represented by each component and whether absence has meaning.
|
|
41
|
+
- Which component transition creates, updates, or removes related state or resources.
|
|
42
|
+
- Whether the work should react through `enter`, `update`, or `exit` rather than scan every frame.
|
|
43
|
+
- Which phase owns the work and what earlier or later phase it must order against.
|
|
44
|
+
- Whether an owned target, relationship, trait, grouped query, or attached resource expresses the
|
|
45
|
+
lifecycle better than imperative bookkeeping.
|
|
46
|
+
- Whether the feature belongs in a `Module` that owns registration, systems, phases, and policy.
|
|
47
|
+
|
|
48
|
+
Prefer the idioms in the installed design guide over generic ECS patterns. vecs execution details,
|
|
49
|
+
including per-system flushes and reactive inbox routing, are part of the design model.
|
|
50
|
+
|
|
51
|
+
## Avoid Generic Workarounds
|
|
52
|
+
|
|
53
|
+
Check the design guide before introducing:
|
|
54
|
+
|
|
55
|
+
- Side maps keyed by entity id.
|
|
56
|
+
- Components containing behavior or broad god-components with optional sentinel fields.
|
|
57
|
+
- Per-frame scans for changes that vecs can route reactively.
|
|
58
|
+
- Manual lifecycle bookkeeping that duplicates component presence.
|
|
59
|
+
- System registration order as a substitute for explicit cross-feature phase ordering.
|
|
60
|
+
- Parallel object graphs that duplicate relationships already represented in the ECS.
|
|
61
|
+
- Wrapper abstractions that hide normal vecs entities, components, systems, or modules without a
|
|
62
|
+
concrete need.
|
|
63
|
+
|
|
64
|
+
If the requested design conflicts with a documented vecs invariant, explain the conflict and use
|
|
65
|
+
the documented pattern unless the user explicitly chooses otherwise.
|
|
66
|
+
|
|
67
|
+
## Implement And Verify
|
|
68
|
+
|
|
69
|
+
1. Reuse existing components and modules where their semantics match; do not reuse them merely
|
|
70
|
+
because their fields look similar.
|
|
71
|
+
2. Keep components small data classes and put behavior in systems or narrowly scoped lifecycle
|
|
72
|
+
hooks as directed by the guide.
|
|
73
|
+
3. Express cross-feature ordering with phases. Treat same-phase registration order as a local
|
|
74
|
+
implementation detail unless the installed docs explicitly make it part of the design.
|
|
75
|
+
4. Test lifecycle transitions, not only steady-state values: entry, mutation, removal, retargeting,
|
|
76
|
+
destruction, and same-tick visibility where relevant.
|
|
77
|
+
5. Run the consuming project's typecheck and focused tests. Resolve API uncertainty from the
|
|
78
|
+
installed declarations and docs rather than guessing.
|
|
79
|
+
|
|
80
|
+
For networking, physics, Phaser, client, or server integration, also inspect the README and
|
|
81
|
+
declarations shipped with the corresponding `@vworlds/vecs-*` package.
|