@vworlds/vecs 1.0.45 → 1.0.46
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/design-guide.md
CHANGED
|
@@ -130,7 +130,7 @@ component set _is_ the lifecycle; queries drive create/destroy; reset is free.
|
|
|
130
130
|
const obj = scene.add.arc(0, 0); // a Phaser GameObject
|
|
131
131
|
entity.attach(obj); // the GameObject is now a component, keyed by its class
|
|
132
132
|
|
|
133
|
-
world.component(Phaser.GameObjects.Arc).
|
|
133
|
+
world.component(Phaser.GameObjects.Arc).on("remove", (_entity, obj) => obj.destroy());
|
|
134
134
|
```
|
|
135
135
|
|
|
136
136
|
`onRemove` (below) releases the resource whenever it leaves — whether removed explicitly or
|
|
@@ -144,16 +144,17 @@ beside the resource.
|
|
|
144
144
|
`world.component(C)` returns a handle with hooks fired on every entity:
|
|
145
145
|
|
|
146
146
|
```ts
|
|
147
|
-
world.component(Impulse).
|
|
147
|
+
world.component(Impulse).on("set", (entity, impulse) => {
|
|
148
148
|
/* accumulate */
|
|
149
149
|
});
|
|
150
|
-
world.component(Phaser.GameObjects.Arc).
|
|
150
|
+
world.component(Phaser.GameObjects.Arc).on("remove", (_entity, obj) => obj.destroy());
|
|
151
151
|
```
|
|
152
152
|
|
|
153
153
|
Use them for resource management and cross-cutting reactions that aren't worth a system. (Note
|
|
154
154
|
the difference from systems: hooks are per-component-instance and run at the mutation site;
|
|
155
|
-
systems are queries that run in a phase.)
|
|
156
|
-
|
|
155
|
+
systems are queries that run in a phase.) Multiple handlers may be registered per event; they fire
|
|
156
|
+
in registration order and are removed with `off(event, handler)` by reference (see
|
|
157
|
+
[Components — hooks](./components.md#hooks-component-and-relationship-edges)).
|
|
157
158
|
|
|
158
159
|
### Component traits: policy on component entities
|
|
159
160
|
|
|
@@ -162,17 +163,19 @@ policy for every use of the component type; these configuration components are *
|
|
|
162
163
|
|
|
163
164
|
```ts
|
|
164
165
|
world.component(Gravity).add(Singleton);
|
|
165
|
-
world.component(Inventory).add(OwnedTarget);
|
|
166
|
-
world.component(ChildOf).add(Traversable);
|
|
166
|
+
world.component(Inventory).add(Exclusive).add(OwnedTarget);
|
|
167
|
+
world.component(ChildOf).add(Exclusive).add(Traversable);
|
|
167
168
|
world.component(Orange).set(Implements, { interfaces: [Fruit] });
|
|
168
169
|
```
|
|
169
170
|
|
|
170
|
-
| Trait/declaration | Reach for it when
|
|
171
|
-
| ----------------- |
|
|
172
|
-
| `Singleton` | At most one entity may carry the component
|
|
173
|
-
| `Implements` | Concrete components should be queryable through abstract aliases
|
|
174
|
-
| `
|
|
175
|
-
| `
|
|
171
|
+
| Trait/declaration | Reach for it when |
|
|
172
|
+
| ----------------- | ---------------------------------------------------------------------------------- |
|
|
173
|
+
| `Singleton` | At most one entity may carry the component |
|
|
174
|
+
| `Implements` | Concrete components should be queryable through abstract aliases |
|
|
175
|
+
| `Exclusive` | A relationship must have at most one target |
|
|
176
|
+
| `Traversable` | An exclusive relationship hierarchy needs depth or cascade ordering |
|
|
177
|
+
| `OwnedTarget` | An exclusive relationship owns its explicitly added target |
|
|
178
|
+
| `OneOf` | A relationship's targets must be direct children of a declared parent (enum-style) |
|
|
176
179
|
|
|
177
180
|
Traits declare policy; modules implement it through hooks and metadata. Configure them in module
|
|
178
181
|
setup, not inside systems. Lifecycle differs by trait: `OwnedTarget` and `Singleton` are setup-time
|
|
@@ -195,41 +198,52 @@ entity.set(Rectangle, { width: 2, height: 1 }); // Arc auto-removed
|
|
|
195
198
|
|
|
196
199
|
### Relationships
|
|
197
200
|
|
|
198
|
-
A relationship is a component
|
|
201
|
+
A relationship is a component that stores edges from its source entity to one or more target
|
|
202
|
+
entities. Add and remove those edges explicitly:
|
|
199
203
|
|
|
200
204
|
```ts
|
|
201
205
|
class InCell extends Relationship {}
|
|
202
|
-
entity.
|
|
203
|
-
entity.
|
|
206
|
+
entity.add(InCell, cell);
|
|
207
|
+
entity.add(InCell, neighboringCell);
|
|
208
|
+
entity.remove(InCell, cell);
|
|
209
|
+
|
|
210
|
+
entity.target(InCell); // one current target, or undefined
|
|
211
|
+
entity.targets(InCell); // readonly snapshot of every target
|
|
212
|
+
entity.hasTarget(InCell, neighboringCell); // true
|
|
204
213
|
world.query("ByCell").with(Networked, Position).groupBy(InCell); // index by target
|
|
205
214
|
```
|
|
206
215
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
`
|
|
216
|
+
Relationships are multi-target by default. Add the `Exclusive` relationship trait when a source may
|
|
217
|
+
have at most one target; adding a different edge then replaces the old edge. This cardinality trait
|
|
218
|
+
is separate from `world.setExclusiveComponents(...)`, which prevents different component types from
|
|
219
|
+
coexisting on one entity.
|
|
220
|
+
|
|
221
|
+
For a zero-or-one aggregate whose lifetime remains bound to its source, create the target explicitly
|
|
222
|
+
and configure the relationship with both `Exclusive` and `OwnedTarget` during setup:
|
|
210
223
|
|
|
211
224
|
```ts
|
|
212
|
-
world.component(Inventory).add(OwnedTarget);
|
|
213
|
-
const inventory =
|
|
214
|
-
player.add(Inventory); //
|
|
225
|
+
world.component(Inventory).add(Exclusive).add(OwnedTarget);
|
|
226
|
+
const inventory = world.entity();
|
|
227
|
+
player.add(Inventory, inventory); // inventory becomes ChildOf player
|
|
215
228
|
```
|
|
216
229
|
|
|
217
|
-
`OwnedTarget` creates a target
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
`
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
- **
|
|
232
|
-
|
|
230
|
+
`OwnedTarget` requires `Exclusive`, never creates a target implicitly, destroys the displaced target
|
|
231
|
+
when a new edge replaces it, and destroys the current target when its edge or relationship is
|
|
232
|
+
removed. `Traversable` also requires `Exclusive` because hierarchy depth needs one parent. Edge
|
|
233
|
+
hooks, `onTargetAdd` and `onTargetRemove`, observe each explicit edge transition and compose with
|
|
234
|
+
these policies.
|
|
235
|
+
|
|
236
|
+
- **Target storage is private.** Read edges with `target` / `targets` / `hasTarget`; mutate them only
|
|
237
|
+
with `add(Rel, target)` and `remove(Rel, target)` (or `remove(Rel)` for all edges). There is no
|
|
238
|
+
public `Relationship.target` field.
|
|
239
|
+
- **Relationships can be networked** when listed in `networkComponents`. A relationship snapshot
|
|
240
|
+
carries all target eids plus the relationship's ordinary wire fields. The client reconciles the
|
|
241
|
+
complete edge set and creates ghost targets that have not arrived yet. Use a dedicated
|
|
242
|
+
relationship class per concern so ownership, hierarchy, and display links do not overload one
|
|
243
|
+
relationship.
|
|
244
|
+
- **Edge changes do not change the source archetype** while at least one edge remains. Prefer
|
|
245
|
+
replacing an exclusive edge, or adding/removing multi-target edges, over swapping marker
|
|
246
|
+
components to express membership.
|
|
233
247
|
|
|
234
248
|
---
|
|
235
249
|
|
|
@@ -257,7 +271,8 @@ classes or DSL objects (`{ all: [...] }`, `{ any: [...] }`, `{ parent: X }`, `{
|
|
|
257
271
|
### Reactive callbacks: `enter` / `update` / `exit`
|
|
258
272
|
|
|
259
273
|
- **`enter`** — fires once when an entity _becomes_ a member. Use it for one-time setup
|
|
260
|
-
(create the resource).
|
|
274
|
+
(create the resource). It fires only when the entity remains a member when the system drains;
|
|
275
|
+
vecs drops a transient entry and its paired exit, so setup code does not need a `destroyed` guard.
|
|
261
276
|
- **`update(C, …)`** — adds `C` to the predicate and fires when watched component `C` is modified.
|
|
262
277
|
Pass `{ onEnter: true }` when it should also fire on entry for an already-present watched
|
|
263
278
|
component. Use that to initialize on entry _and_ react to changes; leave it off when a separate
|
|
@@ -410,7 +425,7 @@ Guidelines:
|
|
|
410
425
|
export class PhaserRenderModule extends Module<{ scene; pixelsPerMeter? }> {
|
|
411
426
|
public init(cfg): void {
|
|
412
427
|
const coords = new CoordSpace(cfg.scene, cfg.pixelsPerMeter);
|
|
413
|
-
this.world.component(GameObject).
|
|
428
|
+
this.world.component(GameObject).on("remove", (_e, obj) => obj.destroy()); // GameObject: Implements marker over every Phaser object
|
|
414
429
|
this.world.module(ArcModule, { scene: cfg.scene, coords });
|
|
415
430
|
this.world.module(RectangleModule, { scene: cfg.scene, coords });
|
|
416
431
|
// … shape modules first …
|
|
@@ -520,7 +535,7 @@ live object as its own component** and let a remove hook release it — no wrapp
|
|
|
520
535
|
|
|
521
536
|
```ts
|
|
522
537
|
// Release the resource whenever its component leaves (explicit remove or entity destroy):
|
|
523
|
-
world.component(Phaser.GameObjects.Arc).
|
|
538
|
+
world.component(Phaser.GameObjects.Arc).on("remove", (_e, obj) => obj.destroy());
|
|
524
539
|
|
|
525
540
|
// CREATE: a renderable that has no object yet
|
|
526
541
|
world
|
|
@@ -545,7 +560,7 @@ world
|
|
|
545
560
|
|
|
546
561
|
// TEARDOWN: when the shape data leaves (type swap) or the entity dies, drop the object.
|
|
547
562
|
// A component remove hook keeps this declarative — no exit system, no phase to coordinate:
|
|
548
|
-
world.component(Arc).
|
|
563
|
+
world.component(Arc).on("remove", (e) => {
|
|
549
564
|
if (e.has(Phaser.GameObjects.Arc)) e.remove(Phaser.GameObjects.Arc);
|
|
550
565
|
});
|
|
551
566
|
```
|
|
@@ -579,10 +594,12 @@ the current `A` as soon as the entity enters the sync system:
|
|
|
579
594
|
Leave `onEnter: true` off (or set `onEnter: false`) only when a separate `.enter(...)` already initializes `B`,
|
|
580
595
|
or when the callback is intentionally "future changes only."
|
|
581
596
|
|
|
582
|
-
### Relationship
|
|
597
|
+
### Relationship edges + grouped queries for spatial/interest indexing
|
|
583
598
|
|
|
584
599
|
Bucket entities by a relationship target (or a derived key) and look up buckets in O(1) instead of
|
|
585
|
-
scanning.
|
|
600
|
+
scanning. `groupBy(Rel)` places a source in every bucket targeted through `Rel`; adding or removing
|
|
601
|
+
an edge reconciles only the affected buckets. Use `Exclusive` when the model permits one bucket and
|
|
602
|
+
multi-target relationships when a source legitimately belongs to several.
|
|
586
603
|
|
|
587
604
|
### Owned target entities (entity-valued properties)
|
|
588
605
|
|
|
@@ -604,12 +621,13 @@ class Weight {
|
|
|
604
621
|
current = 0;
|
|
605
622
|
}
|
|
606
623
|
|
|
607
|
-
world.component(Inventory).add(OwnedTarget);
|
|
624
|
+
world.component(Inventory).add(Exclusive).add(OwnedTarget);
|
|
608
625
|
world.component(InventoryState);
|
|
609
626
|
world.component(Capacity);
|
|
610
627
|
world.component(Weight);
|
|
611
628
|
|
|
612
|
-
const inventory =
|
|
629
|
+
const inventory = world.entity();
|
|
630
|
+
player.add(Inventory, inventory);
|
|
613
631
|
inventory.add(InventoryState).set(Capacity, { slots: 20 }).set(Weight, { current: 0 });
|
|
614
632
|
```
|
|
615
633
|
|
|
@@ -628,36 +646,36 @@ world
|
|
|
628
646
|
|
|
629
647
|
The lifecycle follows from the relationship and trait:
|
|
630
648
|
|
|
631
|
-
| Operation
|
|
632
|
-
|
|
|
633
|
-
| `player.add(Inventory)`
|
|
634
|
-
|
|
|
635
|
-
|
|
|
636
|
-
|
|
|
637
|
-
|
|
|
638
|
-
| Destroy `player` | `ChildOf` destroys the inventory |
|
|
649
|
+
| Operation | Result |
|
|
650
|
+
| ------------------------------------- | ------------------------------------------------------------------------- |
|
|
651
|
+
| `player.add(Inventory, inventory)` | Parents the explicit inventory entity |
|
|
652
|
+
| `player.add(Inventory, replacement)` | Destroys the displaced inventory |
|
|
653
|
+
| `player.remove(Inventory, inventory)` | Destroys that inventory and removes `Inventory` when it was the sole edge |
|
|
654
|
+
| `player.remove(Inventory)` | Destroys every current target |
|
|
655
|
+
| Destroy `player` | `ChildOf` destroys the inventory |
|
|
639
656
|
|
|
640
|
-
Inside deferred execution, mutate the
|
|
641
|
-
|
|
657
|
+
Inside deferred execution, keep and mutate the target you created; the queued edge is not visible
|
|
658
|
+
through `target` until flush:
|
|
642
659
|
|
|
643
660
|
```ts
|
|
644
661
|
world.defer(() => {
|
|
645
|
-
const inventory =
|
|
646
|
-
|
|
662
|
+
const inventory = world.entity();
|
|
663
|
+
player.add(Inventory, inventory);
|
|
664
|
+
inventory.set(Capacity, { slots: 20 }); // the newly created entity is usable
|
|
647
665
|
player.target(Inventory); // still undefined until flush
|
|
648
666
|
});
|
|
649
667
|
```
|
|
650
668
|
|
|
651
669
|
Use the narrowest model that fits:
|
|
652
670
|
|
|
653
|
-
| Need | Model
|
|
654
|
-
| --------------------------------------------------- |
|
|
655
|
-
| A few fields sharing the owner's identity/lifecycle | Components directly on the owner
|
|
656
|
-
| One composable, exclusively owned sub-entity | `
|
|
657
|
-
| One shared or independently living linked entity | Relationship without `OwnedTarget`
|
|
658
|
-
| Zero-to-many subobjects | Child entities with identifying components
|
|
659
|
-
| A non-ECS runtime object | `attach(instance)`
|
|
660
|
-
| World-global state | Singleton/component entity
|
|
671
|
+
| Need | Model |
|
|
672
|
+
| --------------------------------------------------- | --------------------------------------------- |
|
|
673
|
+
| A few fields sharing the owner's identity/lifecycle | Components directly on the owner |
|
|
674
|
+
| One composable, exclusively owned sub-entity | Explicit target + `Exclusive` + `OwnedTarget` |
|
|
675
|
+
| One shared or independently living linked entity | Relationship without `OwnedTarget` |
|
|
676
|
+
| Zero-to-many subobjects | Child entities with identifying components |
|
|
677
|
+
| A non-ECS runtime object | `attach(instance)` |
|
|
678
|
+
| World-global state | Singleton/component entity |
|
|
661
679
|
|
|
662
680
|
Good fits include an inventory aggregate, equipment loadout, character progression sheet, AI
|
|
663
681
|
blackboard, quest journal, ability book, or unique vehicle subsystem. Individual inventory items,
|
|
@@ -675,22 +693,22 @@ system after whatever produces the entity/companion.
|
|
|
675
693
|
|
|
676
694
|
## 8. Anti-patterns (keep ECS semantics)
|
|
677
695
|
|
|
678
|
-
| Smell | Do instead
|
|
679
|
-
| ---------------------------------------------------------------------- |
|
|
680
|
-
| A side `Map<eid, resource>` | Store the resource in a **companion component**; let queries drive its lifecycle; release in `onRemove`.
|
|
681
|
-
| An imperative runtime object with `start()/stop()/track/enable` | A **module** + the world's own lifecycle (`clearAllEntities()`, closing the world). Systems live on the world; they don't need a wrapper to turn on.
|
|
682
|
-
| Builder/factory helpers that _create entities_ (`createArc(world, …)`) | Let callers `world.entity().add(Networked).set(Arc, …).set(FillStyle, …)`. Components are small and optional; the caller adds exactly what it wants.
|
|
683
|
-
| A god component with many optional fields | Split into small single-purpose components; absence encodes "not applicable."
|
|
684
|
-
| A large nested `inventory`, `brain`, or progression object | Use an owned target entity when the subobject needs its own components, systems, and lifecycle.
|
|
685
|
-
| A side `Map<ownerEid, SubsystemState>` |
|
|
686
|
-
| `OwnedTarget` on `CurrentEnemy`, `InCell`, `Team`, or shared state | Use a normal relationship;
|
|
687
|
-
| One owned target used as a collection | Use multiple child/item entities; owned targets model zero-or-one aggregates.
|
|
688
|
-
| Duplicated `enter` + `update` doing the same thing | One `update({ watch: C, onEnter: true })` when update owns initialization.
|
|
689
|
-
| One system trying to act in two phases | Split into two systems, one per phase.
|
|
690
|
-
| Custom phases for app/render logic | Use the standard taxonomy; only engine modules insert phases.
|
|
691
|
-
| `.each` scanning every entity to detect change | Reactive `.update(C)` — fires only on change.
|
|
692
|
-
|
|
|
693
|
-
| A `registerFooSystem(world)` wrapper per system | Declare the system inline in the module's `init`.
|
|
696
|
+
| Smell | Do instead |
|
|
697
|
+
| ---------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
698
|
+
| A side `Map<eid, resource>` | Store the resource in a **companion component**; let queries drive its lifecycle; release in `onRemove`. |
|
|
699
|
+
| An imperative runtime object with `start()/stop()/track/enable` | A **module** + the world's own lifecycle (`clearAllEntities()`, closing the world). Systems live on the world; they don't need a wrapper to turn on. |
|
|
700
|
+
| Builder/factory helpers that _create entities_ (`createArc(world, …)`) | Let callers `world.entity().add(Networked).set(Arc, …).set(FillStyle, …)`. Components are small and optional; the caller adds exactly what it wants. |
|
|
701
|
+
| A god component with many optional fields | Split into small single-purpose components; absence encodes "not applicable." |
|
|
702
|
+
| A large nested `inventory`, `brain`, or progression object | Use an owned target entity when the subobject needs its own components, systems, and lifecycle. |
|
|
703
|
+
| A side `Map<ownerEid, SubsystemState>` | Create a target entity and add an explicit edge; use `Exclusive` + `OwnedTarget` only for destructive ownership. |
|
|
704
|
+
| `OwnedTarget` on `CurrentEnemy`, `InCell`, `Team`, or shared state | Use a normal relationship; replacing or removing an owned edge destroys its target. |
|
|
705
|
+
| One owned target used as a collection | Use multiple child/item entities; owned targets model zero-or-one aggregates. |
|
|
706
|
+
| Duplicated `enter` + `update` doing the same thing | One `update({ watch: C, onEnter: true })` when update owns initialization. |
|
|
707
|
+
| One system trying to act in two phases | Split into two systems, one per phase. |
|
|
708
|
+
| Custom phases for app/render logic | Use the standard taxonomy; only engine modules insert phases. |
|
|
709
|
+
| `.each` scanning every entity to detect change | Reactive `.update(C)` — fires only on change. |
|
|
710
|
+
| Treating target storage as component data | Use `add(Rel, target)` / `remove(Rel, target)` and read with `target` / `targets` / `hasTarget`; target storage is private. |
|
|
711
|
+
| A `registerFooSystem(world)` wrapper per system | Declare the system inline in the module's `init`. |
|
|
694
712
|
|
|
695
713
|
---
|
|
696
714
|
|
|
@@ -704,24 +722,37 @@ system after whatever produces the entity/companion.
|
|
|
704
722
|
when `.enter(...)` already owns initialization.
|
|
705
723
|
- **Membership is fixed by `.with(...)` / `.without(...)` / `.update(C)`;** `update(C)` watches `C`
|
|
706
724
|
and requires it.
|
|
707
|
-
- **`exit`
|
|
725
|
+
- **`exit` retains injected references captured at exit** (components are not deep-cloned).
|
|
708
726
|
- **Within a phase, order = registration order;** across phases, the spine guarantees order. Don't
|
|
709
727
|
use registration order for cross-module correctness — use phases.
|
|
710
728
|
- **Networked component order = wire protocol.** Reordering breaks the protocol.
|
|
711
729
|
- **`@wireType` must survive esbuild's standard decorators** when components load from
|
|
712
730
|
source (tsx/Vite). Otherwise fields silently don't register.
|
|
713
|
-
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
731
|
+
- **Relationships are multi-target by default.** `target(R)` returns one current target for singular
|
|
732
|
+
code but is not a stable selection policy; `targets(R)` snapshots all targets with unspecified
|
|
733
|
+
ordering; `hasTarget(R, target)` tests one edge.
|
|
734
|
+
- **Relationship edges are explicit.** Add with `add(R, target)`, remove one with
|
|
735
|
+
`remove(R, target)`, and remove all with `remove(R)`. Do not use `set` for target storage.
|
|
736
|
+
- **Relationship cardinality and component exclusivity are different.** Add the `Exclusive` trait
|
|
737
|
+
to a relationship for at-most-one target; use `setExclusiveComponents` for mutually exclusive
|
|
738
|
+
component types.
|
|
739
|
+
- **Target queries are existential.** `{ target: [R, q] }` matches when any `R` target matches `q`.
|
|
740
|
+
Singular `{ target: ... }` injection projects one current target; plural `{ targets: ... }`
|
|
741
|
+
injection returns one projected row per target.
|
|
742
|
+
- **Relationship grouping is multi-bucket.** `groupBy(R)` places an entity in every current target
|
|
743
|
+
bucket and reconciles buckets as edges change.
|
|
744
|
+
- **Network relationship snapshots contain all targets.** Register the relationship in
|
|
745
|
+
`networkComponents`; the client reconciles the full edge set and relationship data.
|
|
717
746
|
- **Component traits have trait-specific setup semantics.** In particular, configure `OwnedTarget`
|
|
718
747
|
before creating relationship instances; it does not backfill.
|
|
719
|
-
- **`OwnedTarget`
|
|
720
|
-
relationship destroys the current target, and owner destruction
|
|
748
|
+
- **`OwnedTarget` requires `Exclusive` and explicit target creation.** Replacing an edge destroys
|
|
749
|
+
the old target, removing the relationship destroys the current target, and owner destruction
|
|
750
|
+
cascades through `ChildOf`.
|
|
721
751
|
Never use it for shared teams, cells, enemies, definitions, or other world entities.
|
|
722
|
-
-
|
|
723
|
-
|
|
724
|
-
|
|
752
|
+
- **`Traversable` requires `Exclusive`.** Depth and cascade ordering need one parent per source.
|
|
753
|
+
- **Owned targets are zero-or-one.** Use child entities for collections, create the aggregate target
|
|
754
|
+
explicitly, and during deferred execution keep that entity instead of re-reading `target(R)`
|
|
755
|
+
before flush.
|
|
725
756
|
- **`world.module(M)` runs `init` once.** Re-registering with a different config throws.
|
|
726
757
|
|
|
727
758
|
---
|
|
@@ -747,14 +778,14 @@ render code**.
|
|
|
747
778
|
systems drives every renderable polymorphically. Per shape: a **creation** system
|
|
748
779
|
(`.with(Shape).without(Phaser.GameObjects.Kind).enter` → `scene.add.*` + `entity.attach`) in
|
|
749
780
|
`ON_STORE`; a **geometry** system (`.with(Shape, m.Geometry).update`) in `ON_STORE`; and a
|
|
750
|
-
shape-data **remove hook** (`component(Shape).
|
|
781
|
+
shape-data **remove hook** (`component(Shape).on("remove", ...)` → remove the Phaser object) instead of a
|
|
751
782
|
teardown system. Shared transform/style **sync** systems
|
|
752
783
|
(`.with(Component, m.Marker).update({ watch: Component, onEnter: true })`) apply
|
|
753
784
|
position/rotation/scale/alpha/depth/size/fill/stroke through the markers — initialization is
|
|
754
785
|
explicit via `onEnter: true`, and the umbrella loads shape modules _before_ the sync module so
|
|
755
786
|
creation precedes sync within `ON_STORE`.
|
|
756
787
|
- **Lifecycle that falls out for free:** destruction is a single hook —
|
|
757
|
-
`component(GameObject).
|
|
788
|
+
`component(GameObject).on("remove", (_e, obj) => obj.destroy())`. A renderable **type swap**
|
|
758
789
|
(exclusivity removes the old shape data → its remove hook drops the attached object → it's
|
|
759
790
|
destroyed → the new shape's creation system rebinds) and a **baseline reset / disconnect**
|
|
760
791
|
(`clearAllEntities()` → entities destroyed → attached objects removed → destroyed) both work with
|
package/docs/entities.md
CHANGED
|
@@ -46,9 +46,10 @@ for end-of-session teardown, not selective resets.
|
|
|
46
46
|
```ts
|
|
47
47
|
e.add(Position); // attach with default field values (idempotent)
|
|
48
48
|
e.set(Position, { x: 100 }); // attach if needed, assign props, fire onSet/update
|
|
49
|
+
e.add(FriendOf, friend); // relationship edges require an explicit target
|
|
50
|
+
e.remove(FriendOf, friend); // remove one relationship edge
|
|
49
51
|
e.get(Position); // Readonly<Position> | undefined
|
|
50
52
|
e.ensure(Position, (pos) => (pos.x += 1)); // create if needed, then mutate and notify
|
|
51
|
-
e.ensureTarget(FriendOf); // get or create an independent relationship target
|
|
52
53
|
e.has(Position); // boolean
|
|
53
54
|
e.remove(Position); // detach; fires exit callbacks, then onRemove
|
|
54
55
|
e.modified(Position); // mark changed after in-place mutation
|
|
@@ -57,19 +58,26 @@ e.destroy(); // remove everything and unregister the entity
|
|
|
57
58
|
|
|
58
59
|
All mutators return the entity for chaining (`remove` and `destroy` return `void`). Component
|
|
59
60
|
references can be a registered class, a numeric component id, or an entity used as a component
|
|
60
|
-
key.
|
|
61
|
+
key. Relationship `target` arguments accept the same `ComponentRef` shapes (class, numeric id, or
|
|
62
|
+
entity) and are resolved eagerly via `world.entity(target)`, so the target entity must already
|
|
63
|
+
exist when the call is made.
|
|
61
64
|
|
|
62
65
|
### `set` vs `add` vs `modified`
|
|
63
66
|
|
|
64
67
|
- `add(C)` attaches with defaults and fires `onAdd` only. It is idempotent and does **not** fire
|
|
65
68
|
`onSet`.
|
|
66
|
-
- `set(C, props)` attaches if needed (firing `onAdd`), copies `props` onto
|
|
67
|
-
`onSet` plus `update` callbacks.
|
|
69
|
+
- For ordinary components, `set(C, props)` attaches if needed (firing `onAdd`), copies `props` onto
|
|
70
|
+
the instance, and fires `onSet` plus `update` callbacks.
|
|
68
71
|
- `ensure(C, callback)` creates the component if missing, then lets you mutate it and fires
|
|
69
72
|
`onAdd` followed by one `onSet`. When the component already exists, it behaves like `getMut`.
|
|
70
73
|
- `modified(C)` fires `onSet`/`update` for data you already mutated in place. Repeated calls are
|
|
71
74
|
coalesced until the world routes the notification.
|
|
72
75
|
|
|
76
|
+
Relationships use a stricter API: `add(R, target)` creates a unique edge, `remove(R, target)`
|
|
77
|
+
removes one edge, and `remove(R)` removes every edge. `set(R, props)` updates only ordinary fields
|
|
78
|
+
on an existing relationship and never creates or changes edges. See
|
|
79
|
+
[Relationships](./relationships.md#adding-removing-and-setting).
|
|
80
|
+
|
|
73
81
|
### `getMut` and `ensure`: mutating in place
|
|
74
82
|
|
|
75
83
|
`get` returns a `Readonly` view. To mutate, use `getMut`, which marks the component modified for
|
|
@@ -100,41 +108,10 @@ When several calls ensure the same missing component during one deferred scope,
|
|
|
100
108
|
same pending instance. The instance remains hidden from `get` and queries until the queue drains,
|
|
101
109
|
but every ensure callback mutates that shared pending value.
|
|
102
110
|
|
|
103
|
-
Use `ensureTarget` for the corresponding get-or-create operation on a registered relationship:
|
|
104
|
-
|
|
105
|
-
```ts
|
|
106
|
-
world.component(FriendOf);
|
|
107
|
-
|
|
108
|
-
const friend = entity.ensureTarget(FriendOf);
|
|
109
|
-
entity.ensureTarget(FriendOf) === friend; // true
|
|
110
|
-
entity.target(FriendOf) === friend; // true after deferred changes flush
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
When the relationship is absent, `ensureTarget` creates an independent entity and points the
|
|
114
|
-
relationship at it. It does not add `ChildOf`; destroying the source does not destroy the generated
|
|
115
|
-
target. Repeated calls during one deferred scope return the same pending target, while `target`
|
|
116
|
-
continues to expose only committed state until the queue drains.
|
|
117
|
-
|
|
118
|
-
Mark the relationship with `OwnedTarget` during world setup when its targets should instead be
|
|
119
|
-
owned by their sources:
|
|
120
|
-
|
|
121
|
-
```ts
|
|
122
|
-
world.component(Inventory).add(OwnedTarget);
|
|
123
|
-
|
|
124
|
-
const inventory = player.ensureTarget(Inventory);
|
|
125
|
-
inventory.parent === player; // true after deferred changes flush
|
|
126
|
-
|
|
127
|
-
player.add(Inventory); // also creates an owned target
|
|
128
|
-
```
|
|
129
|
-
|
|
130
|
-
The relationship module creates a target when an owned relationship is added, adds `ChildOf`
|
|
131
|
-
whenever `Inventory` is assigned, destroys displaced targets on retarget, and destroys the current
|
|
132
|
-
target when the relationship is removed. Destroying `player` therefore also destroys `inventory`.
|
|
133
|
-
See [Relationships](./relationships.md#owned-targets).
|
|
134
|
-
|
|
135
111
|
Calling `getMut(C)` or `ensure(C)` without a callback outside deferred mode throws. Both methods
|
|
136
|
-
also throw for relationship components (
|
|
137
|
-
|
|
112
|
+
also throw for relationship components (change edges with `add` / `remove` and ordinary data with
|
|
113
|
+
`set`) and for `Implements` interface aliases. `attach` rejects relationships for the same reason:
|
|
114
|
+
their private edge storage must remain under vecs's control.
|
|
138
115
|
|
|
139
116
|
### `attach`: storing an existing instance
|
|
140
117
|
|
|
@@ -163,6 +140,9 @@ drains. The exact read-after-write rules are tabulated in
|
|
|
163
140
|
`ensure(C)` is the exception for mutable access: it returns a mutable component immediately, but a
|
|
164
141
|
newly created component does not appear in `get(C)` until the queue drains.
|
|
165
142
|
|
|
143
|
+
Relationship edge commands share the same FIFO queue. Until it drains, `has(R)`, `target(R)`,
|
|
144
|
+
`targets(R)`, `hasTarget(R, target)`, and `children(R)` expose only committed edges.
|
|
145
|
+
|
|
166
146
|
Deferred entity creation is idempotent within one deferred scope: if `world.entity()` or
|
|
167
147
|
`getOrCreateEntity(eid)` creates a new entity, `getEntity(eid)`, `entity(eid)`, and later
|
|
168
148
|
`getOrCreateEntity(eid)` calls before the drain return that same pending entity. Collection-style
|
|
@@ -180,54 +160,60 @@ not be used.
|
|
|
180
160
|
If the entity is itself in use as a component key, `destroy` honors `meta.onDelete` — by default
|
|
181
161
|
it throws while carriers remain ([Components — cleanup policies](./components.md#cleanup-policies-destroying-a-component-entity)).
|
|
182
162
|
|
|
163
|
+
A `destroy()` thrown synchronously (a `Throw`-policy component entity still in use) leaves the world
|
|
164
|
+
usable because nothing has been mutated. A throw during deferred drain or a relationship cleanup
|
|
165
|
+
cascade — an impossible policy combination, a `Throw`-policy component reached via a cascade, or a
|
|
166
|
+
hook that throws — propagates out of `progress()` / `flush()` and leaves the world in a partial
|
|
167
|
+
state. Treat this as a programming error: recreate the world or stop.
|
|
168
|
+
|
|
183
169
|
## Entities as component keys
|
|
184
170
|
|
|
185
171
|
Any entity can serve as a component key: `other.add(keyEntity)` attaches a marker component whose
|
|
186
172
|
type id is the key entity's eid. The key entity's `meta` (created lazily) carries the same hook
|
|
187
|
-
and policy surface as a registered class, and `keyEntity.
|
|
188
|
-
|
|
189
|
-
types work without declaring a class.
|
|
190
|
-
expose `onRetarget(...)`.
|
|
173
|
+
and policy surface as a registered class, and `keyEntity.on("add" / "set" / "remove" / "targetAdd" /
|
|
174
|
+
"targetRemove", handler)` registers its lifecycle handlers directly. This is how tag-like,
|
|
175
|
+
runtime-created component types work without declaring a class.
|
|
191
176
|
|
|
192
177
|
## Relationships, in brief
|
|
193
178
|
|
|
194
|
-
`e.target(Rel)` returns
|
|
195
|
-
|
|
196
|
-
`e.
|
|
197
|
-
|
|
179
|
+
`e.target(Rel)` returns one current target, `e.targets(Rel)` a readonly snapshot of every target,
|
|
180
|
+
`e.hasTarget(Rel, target)` tests one edge, and `e.children(Rel)` returns the sources targeting `e`.
|
|
181
|
+
`e.childOf(parent)` is shorthand for `e.add(ChildOf, parent)`. The full model is in
|
|
182
|
+
[Relationships](./relationships.md).
|
|
198
183
|
|
|
199
184
|
## Reference
|
|
200
185
|
|
|
201
186
|
`lib/vecs/src/entity/` (split across `entity.base.ts`, `entity.components.ts`,
|
|
202
187
|
`entity.lifecycle.ts`, `entity.relationships.ts`):
|
|
203
188
|
|
|
204
|
-
| Property / Method | Description
|
|
205
|
-
| ---------------------------------------------- |
|
|
206
|
-
| `eid` | Unique numeric entity id.
|
|
207
|
-
| `world` | The `World` that owns this entity.
|
|
208
|
-
| `name` | Identity name (getter/setter backed by the `Identity` component); `undefined` when unnamed.
|
|
209
|
-
| `componentBitmask` | `Bitset` of component type ids attached. Used for archetype matching.
|
|
210
|
-
| `components` | Read-only `ArrayMap` view of attached instances keyed by type id (`forEach`, `get`, `has`, `size`).
|
|
211
|
-
| `empty` | `true` when no components are attached.
|
|
212
|
-
| `properties` | Free-form `Map<string, any>` for module-level bookkeeping.
|
|
213
|
-
| `meta` | `ComponentMeta` for using this entity as a component key (created lazily).
|
|
214
|
-
| `instanceCount()` | Number of entities currently carrying this entity's eid as a component.
|
|
215
|
-
| `add(ref)`
|
|
216
|
-
| `attach(instance)` / `attach(Class, instance)` | Store an existing instance, replacing any previous one; fires set-side events. Returns `this`.
|
|
217
|
-
| `set(ref, props)` | Attach
|
|
218
|
-
| `get(ref)` | Read-only component lookup, or `undefined`.
|
|
219
|
-
| `getMut(ref)` / `getMut(ref, callback)` | Mutable lookup that marks the component modified. Callback form required outside deferred mode.
|
|
220
|
-
| `ensure(ref)` / `ensure(ref, callback)` | Mutable lookup that creates when missing. Callback form required outside deferred mode.
|
|
221
|
-
| `
|
|
222
|
-
| `
|
|
223
|
-
| `
|
|
224
|
-
| `
|
|
225
|
-
| `
|
|
226
|
-
| `
|
|
227
|
-
| `
|
|
228
|
-
| `
|
|
229
|
-
| `parent`
|
|
230
|
-
| `
|
|
231
|
-
| `
|
|
232
|
-
| `
|
|
233
|
-
| `
|
|
189
|
+
| Property / Method | Description |
|
|
190
|
+
| ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
191
|
+
| `eid` | Unique numeric entity id. |
|
|
192
|
+
| `world` | The `World` that owns this entity. |
|
|
193
|
+
| `name` | Identity name (getter/setter backed by the `Identity` component); `undefined` when unnamed. |
|
|
194
|
+
| `componentBitmask` | `Bitset` of component type ids attached. Used for archetype matching. |
|
|
195
|
+
| `components` | Read-only `ArrayMap` view of attached instances keyed by type id (`forEach`, `get`, `has`, `size`). |
|
|
196
|
+
| `empty` | `true` when no components are attached. |
|
|
197
|
+
| `properties` | Free-form `Map<string, any>` for module-level bookkeeping. |
|
|
198
|
+
| `meta` | `ComponentMeta` for using this entity as a component key (created lazily). |
|
|
199
|
+
| `instanceCount()` | Number of entities currently carrying this entity's eid as a component. |
|
|
200
|
+
| `add(ref)` / `add(Relationship, target)` | Attach an ordinary component, or add one explicit relationship edge. `target` accepts any `ComponentRef` resolved eagerly via `world.entity(target)`. Returns `this`. |
|
|
201
|
+
| `attach(instance)` / `attach(Class, instance)` | Store an existing instance, replacing any previous one; fires set-side events. Returns `this`. |
|
|
202
|
+
| `set(ref, props)` | Attach/set ordinary data; relationships must exist and only ordinary fields are assigned. |
|
|
203
|
+
| `get(ref)` | Read-only component lookup, or `undefined`. |
|
|
204
|
+
| `getMut(ref)` / `getMut(ref, callback)` | Mutable lookup that marks the component modified. Callback form required outside deferred mode. |
|
|
205
|
+
| `ensure(ref)` / `ensure(ref, callback)` | Mutable lookup that creates when missing. Callback form required outside deferred mode. |
|
|
206
|
+
| `has(ref)` | `true` when the component is attached. |
|
|
207
|
+
| `modified(ref)` | Queue an `onSet`/`update` notification; coalesced while pending. Returns `this`. |
|
|
208
|
+
| `remove(ref)` / `remove(Relationship, target)` | Detach a component/all edges, or remove one relationship edge. `target` accepts any `ComponentRef` resolved eagerly via `world.entity(target)`. |
|
|
209
|
+
| `destroy()` | Remove all components, unregister, and apply incoming relationship cleanup. |
|
|
210
|
+
| `target(rel)` | One current target entity of relationship `rel`, or `undefined`; not a stable selection policy. |
|
|
211
|
+
| `targets(rel)` | Readonly snapshot of all targets; ordering is unspecified. |
|
|
212
|
+
| `hasTarget(rel, target)` | `true` when the specific relationship edge exists. |
|
|
213
|
+
| `children(rel)` | `ReadonlySet<Entity>` of entities targeting this one through `rel`. |
|
|
214
|
+
| `childOf(parent)` | Shorthand for `add(ChildOf, parent)`. Returns `this`. |
|
|
215
|
+
| `parent` / `parent = p` | Getter/setter for the built-in `ChildOf` target; assigning `undefined` removes `ChildOf`. |
|
|
216
|
+
| `destroyed` / `alive` | `true` when the entity has been / has not been destroyed (marked for deletion). |
|
|
217
|
+
| `on(event, handler, insert?)` | Register a handler for a hook event on this component's meta. `insert` defaults to `false` (append); `true` prepends. Returns `this`. |
|
|
218
|
+
| `off(event, handler)` | Remove a previously-registered handler by reference. Idempotent. Returns `this`. |
|
|
219
|
+
| `toString()` | Returns `"EntityN"`. |
|