@vworlds/vecs 1.0.44 → 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 -51
- package/dist/component.js +25 -9
- package/dist/component.js.map +1 -1
- package/dist/component_meta.d.ts +71 -66
- package/dist/component_meta.js +331 -34
- 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 -27
- package/dist/entity/entity.components.js +258 -74
- 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 -53
- 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 -3
- package/dist/modules/relationship_types.js +70 -3
- package/dist/modules/relationship_types.js.map +1 -1
- package/dist/modules/relationships.d.ts +31 -8
- package/dist/modules/relationships.js +120 -30
- 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 +91 -48
- package/docs/concepts.md +21 -17
- package/docs/design-guide.md +125 -89
- package/docs/entities.md +62 -72
- 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 +250 -115
- 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,15 +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.)
|
|
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)).
|
|
156
158
|
|
|
157
159
|
### Component traits: policy on component entities
|
|
158
160
|
|
|
@@ -161,17 +163,19 @@ policy for every use of the component type; these configuration components are *
|
|
|
161
163
|
|
|
162
164
|
```ts
|
|
163
165
|
world.component(Gravity).add(Singleton);
|
|
164
|
-
world.component(Inventory).add(OwnedTarget);
|
|
165
|
-
world.component(ChildOf).add(Traversable);
|
|
166
|
+
world.component(Inventory).add(Exclusive).add(OwnedTarget);
|
|
167
|
+
world.component(ChildOf).add(Exclusive).add(Traversable);
|
|
166
168
|
world.component(Orange).set(Implements, { interfaces: [Fruit] });
|
|
167
169
|
```
|
|
168
170
|
|
|
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
|
-
| `
|
|
174
|
-
| `
|
|
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) |
|
|
175
179
|
|
|
176
180
|
Traits declare policy; modules implement it through hooks and metadata. Configure them in module
|
|
177
181
|
setup, not inside systems. Lifecycle differs by trait: `OwnedTarget` and `Singleton` are setup-time
|
|
@@ -194,38 +198,52 @@ entity.set(Rectangle, { width: 2, height: 1 }); // Arc auto-removed
|
|
|
194
198
|
|
|
195
199
|
### Relationships
|
|
196
200
|
|
|
197
|
-
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:
|
|
198
203
|
|
|
199
204
|
```ts
|
|
200
205
|
class InCell extends Relationship {}
|
|
201
|
-
entity.
|
|
202
|
-
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
|
|
203
213
|
world.query("ByCell").with(Networked, Position).groupBy(InCell); // index by target
|
|
204
214
|
```
|
|
205
215
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
`
|
|
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:
|
|
209
223
|
|
|
210
224
|
```ts
|
|
211
|
-
world.component(Inventory).add(OwnedTarget);
|
|
212
|
-
const inventory =
|
|
225
|
+
world.component(Inventory).add(Exclusive).add(OwnedTarget);
|
|
226
|
+
const inventory = world.entity();
|
|
227
|
+
player.add(Inventory, inventory); // inventory becomes ChildOf player
|
|
213
228
|
```
|
|
214
229
|
|
|
215
|
-
`OwnedTarget`
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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.
|
|
229
247
|
|
|
230
248
|
---
|
|
231
249
|
|
|
@@ -253,7 +271,8 @@ classes or DSL objects (`{ all: [...] }`, `{ any: [...] }`, `{ parent: X }`, `{
|
|
|
253
271
|
### Reactive callbacks: `enter` / `update` / `exit`
|
|
254
272
|
|
|
255
273
|
- **`enter`** — fires once when an entity _becomes_ a member. Use it for one-time setup
|
|
256
|
-
(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.
|
|
257
276
|
- **`update(C, …)`** — adds `C` to the predicate and fires when watched component `C` is modified.
|
|
258
277
|
Pass `{ onEnter: true }` when it should also fire on entry for an already-present watched
|
|
259
278
|
component. Use that to initialize on entry _and_ react to changes; leave it off when a separate
|
|
@@ -406,7 +425,7 @@ Guidelines:
|
|
|
406
425
|
export class PhaserRenderModule extends Module<{ scene; pixelsPerMeter? }> {
|
|
407
426
|
public init(cfg): void {
|
|
408
427
|
const coords = new CoordSpace(cfg.scene, cfg.pixelsPerMeter);
|
|
409
|
-
this.world.component(GameObject).
|
|
428
|
+
this.world.component(GameObject).on("remove", (_e, obj) => obj.destroy()); // GameObject: Implements marker over every Phaser object
|
|
410
429
|
this.world.module(ArcModule, { scene: cfg.scene, coords });
|
|
411
430
|
this.world.module(RectangleModule, { scene: cfg.scene, coords });
|
|
412
431
|
// … shape modules first …
|
|
@@ -516,7 +535,7 @@ live object as its own component** and let a remove hook release it — no wrapp
|
|
|
516
535
|
|
|
517
536
|
```ts
|
|
518
537
|
// Release the resource whenever its component leaves (explicit remove or entity destroy):
|
|
519
|
-
world.component(Phaser.GameObjects.Arc).
|
|
538
|
+
world.component(Phaser.GameObjects.Arc).on("remove", (_e, obj) => obj.destroy());
|
|
520
539
|
|
|
521
540
|
// CREATE: a renderable that has no object yet
|
|
522
541
|
world
|
|
@@ -541,7 +560,7 @@ world
|
|
|
541
560
|
|
|
542
561
|
// TEARDOWN: when the shape data leaves (type swap) or the entity dies, drop the object.
|
|
543
562
|
// A component remove hook keeps this declarative — no exit system, no phase to coordinate:
|
|
544
|
-
world.component(Arc).
|
|
563
|
+
world.component(Arc).on("remove", (e) => {
|
|
545
564
|
if (e.has(Phaser.GameObjects.Arc)) e.remove(Phaser.GameObjects.Arc);
|
|
546
565
|
});
|
|
547
566
|
```
|
|
@@ -575,10 +594,12 @@ the current `A` as soon as the entity enters the sync system:
|
|
|
575
594
|
Leave `onEnter: true` off (or set `onEnter: false`) only when a separate `.enter(...)` already initializes `B`,
|
|
576
595
|
or when the callback is intentionally "future changes only."
|
|
577
596
|
|
|
578
|
-
### Relationship
|
|
597
|
+
### Relationship edges + grouped queries for spatial/interest indexing
|
|
579
598
|
|
|
580
599
|
Bucket entities by a relationship target (or a derived key) and look up buckets in O(1) instead of
|
|
581
|
-
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.
|
|
582
603
|
|
|
583
604
|
### Owned target entities (entity-valued properties)
|
|
584
605
|
|
|
@@ -600,12 +621,13 @@ class Weight {
|
|
|
600
621
|
current = 0;
|
|
601
622
|
}
|
|
602
623
|
|
|
603
|
-
world.component(Inventory).add(OwnedTarget);
|
|
624
|
+
world.component(Inventory).add(Exclusive).add(OwnedTarget);
|
|
604
625
|
world.component(InventoryState);
|
|
605
626
|
world.component(Capacity);
|
|
606
627
|
world.component(Weight);
|
|
607
628
|
|
|
608
|
-
const inventory =
|
|
629
|
+
const inventory = world.entity();
|
|
630
|
+
player.add(Inventory, inventory);
|
|
609
631
|
inventory.add(InventoryState).set(Capacity, { slots: 20 }).set(Weight, { current: 0 });
|
|
610
632
|
```
|
|
611
633
|
|
|
@@ -624,35 +646,36 @@ world
|
|
|
624
646
|
|
|
625
647
|
The lifecycle follows from the relationship and trait:
|
|
626
648
|
|
|
627
|
-
| Operation
|
|
628
|
-
|
|
|
629
|
-
|
|
|
630
|
-
|
|
|
631
|
-
|
|
|
632
|
-
|
|
|
633
|
-
| Destroy `player`
|
|
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 |
|
|
634
656
|
|
|
635
|
-
Inside deferred execution, mutate the
|
|
636
|
-
|
|
657
|
+
Inside deferred execution, keep and mutate the target you created; the queued edge is not visible
|
|
658
|
+
through `target` until flush:
|
|
637
659
|
|
|
638
660
|
```ts
|
|
639
661
|
world.defer(() => {
|
|
640
|
-
const inventory =
|
|
641
|
-
|
|
662
|
+
const inventory = world.entity();
|
|
663
|
+
player.add(Inventory, inventory);
|
|
664
|
+
inventory.set(Capacity, { slots: 20 }); // the newly created entity is usable
|
|
642
665
|
player.target(Inventory); // still undefined until flush
|
|
643
666
|
});
|
|
644
667
|
```
|
|
645
668
|
|
|
646
669
|
Use the narrowest model that fits:
|
|
647
670
|
|
|
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 | `
|
|
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
|
|
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 |
|
|
656
679
|
|
|
657
680
|
Good fits include an inventory aggregate, equipment loadout, character progression sheet, AI
|
|
658
681
|
blackboard, quest journal, ability book, or unique vehicle subsystem. Individual inventory items,
|
|
@@ -670,22 +693,22 @@ system after whatever produces the entity/companion.
|
|
|
670
693
|
|
|
671
694
|
## 8. Anti-patterns (keep ECS semantics)
|
|
672
695
|
|
|
673
|
-
| Smell | Do instead
|
|
674
|
-
| ---------------------------------------------------------------------- |
|
|
675
|
-
| A side `Map<eid, resource>` | Store the resource in a **companion component**; let queries drive its lifecycle; release in `onRemove`.
|
|
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.
|
|
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.
|
|
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>` |
|
|
681
|
-
| `OwnedTarget` on `CurrentEnemy`, `InCell`, `Team`, or shared state | Use a normal relationship;
|
|
682
|
-
| One owned target used as a collection | Use multiple child/item entities; owned targets model zero-or-one aggregates.
|
|
683
|
-
| Duplicated `enter` + `update` doing the same thing | One `update({ watch: C, onEnter: true })` when update owns initialization.
|
|
684
|
-
| One system trying to act in two phases | Split into two systems, one per phase.
|
|
685
|
-
| Custom phases for app/render logic | Use the standard taxonomy; only engine modules insert phases.
|
|
686
|
-
| `.each` scanning every entity to detect change | Reactive `.update(C)` — fires only on change.
|
|
687
|
-
|
|
|
688
|
-
| 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`. |
|
|
689
712
|
|
|
690
713
|
---
|
|
691
714
|
|
|
@@ -699,24 +722,37 @@ system after whatever produces the entity/companion.
|
|
|
699
722
|
when `.enter(...)` already owns initialization.
|
|
700
723
|
- **Membership is fixed by `.with(...)` / `.without(...)` / `.update(C)`;** `update(C)` watches `C`
|
|
701
724
|
and requires it.
|
|
702
|
-
- **`exit`
|
|
725
|
+
- **`exit` retains injected references captured at exit** (components are not deep-cloned).
|
|
703
726
|
- **Within a phase, order = registration order;** across phases, the spine guarantees order. Don't
|
|
704
727
|
use registration order for cross-module correctness — use phases.
|
|
705
728
|
- **Networked component order = wire protocol.** Reordering breaks the protocol.
|
|
706
729
|
- **`@wireType` must survive esbuild's standard decorators** when components load from
|
|
707
730
|
source (tsx/Vite). Otherwise fields silently don't register.
|
|
708
|
-
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
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.
|
|
712
746
|
- **Component traits have trait-specific setup semantics.** In particular, configure `OwnedTarget`
|
|
713
747
|
before creating relationship instances; it does not backfill.
|
|
714
|
-
- **`OwnedTarget`
|
|
715
|
-
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`.
|
|
716
751
|
Never use it for shared teams, cells, enemies, definitions, or other world entities.
|
|
717
|
-
-
|
|
718
|
-
|
|
719
|
-
|
|
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.
|
|
720
756
|
- **`world.module(M)` runs `init` once.** Re-registering with a different config throws.
|
|
721
757
|
|
|
722
758
|
---
|
|
@@ -742,14 +778,14 @@ render code**.
|
|
|
742
778
|
systems drives every renderable polymorphically. Per shape: a **creation** system
|
|
743
779
|
(`.with(Shape).without(Phaser.GameObjects.Kind).enter` → `scene.add.*` + `entity.attach`) in
|
|
744
780
|
`ON_STORE`; a **geometry** system (`.with(Shape, m.Geometry).update`) in `ON_STORE`; and a
|
|
745
|
-
shape-data **remove hook** (`component(Shape).
|
|
781
|
+
shape-data **remove hook** (`component(Shape).on("remove", ...)` → remove the Phaser object) instead of a
|
|
746
782
|
teardown system. Shared transform/style **sync** systems
|
|
747
783
|
(`.with(Component, m.Marker).update({ watch: Component, onEnter: true })`) apply
|
|
748
784
|
position/rotation/scale/alpha/depth/size/fill/stroke through the markers — initialization is
|
|
749
785
|
explicit via `onEnter: true`, and the umbrella loads shape modules _before_ the sync module so
|
|
750
786
|
creation precedes sync within `ON_STORE`.
|
|
751
787
|
- **Lifecycle that falls out for free:** destruction is a single hook —
|
|
752
|
-
`component(GameObject).
|
|
788
|
+
`component(GameObject).on("remove", (_e, obj) => obj.destroy())`. A renderable **type swap**
|
|
753
789
|
(exclusivity removes the old shape data → its remove hook drops the attached object → it's
|
|
754
790
|
destroyed → the new shape's creation system rebinds) and a **baseline reset / disconnect**
|
|
755
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,38 +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
|
-
|
|
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
111
|
Calling `getMut(C)` or `ensure(C)` without a callback outside deferred mode throws. Both methods
|
|
133
|
-
also throw for relationship components (
|
|
134
|
-
|
|
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.
|
|
135
115
|
|
|
136
116
|
### `attach`: storing an existing instance
|
|
137
117
|
|
|
@@ -160,6 +140,9 @@ drains. The exact read-after-write rules are tabulated in
|
|
|
160
140
|
`ensure(C)` is the exception for mutable access: it returns a mutable component immediately, but a
|
|
161
141
|
newly created component does not appear in `get(C)` until the queue drains.
|
|
162
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
|
+
|
|
163
146
|
Deferred entity creation is idempotent within one deferred scope: if `world.entity()` or
|
|
164
147
|
`getOrCreateEntity(eid)` creates a new entity, `getEntity(eid)`, `entity(eid)`, and later
|
|
165
148
|
`getOrCreateEntity(eid)` calls before the drain return that same pending entity. Collection-style
|
|
@@ -177,53 +160,60 @@ not be used.
|
|
|
177
160
|
If the entity is itself in use as a component key, `destroy` honors `meta.onDelete` — by default
|
|
178
161
|
it throws while carriers remain ([Components — cleanup policies](./components.md#cleanup-policies-destroying-a-component-entity)).
|
|
179
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
|
+
|
|
180
169
|
## Entities as component keys
|
|
181
170
|
|
|
182
171
|
Any entity can serve as a component key: `other.add(keyEntity)` attaches a marker component whose
|
|
183
172
|
type id is the key entity's eid. The key entity's `meta` (created lazily) carries the same hook
|
|
184
|
-
and policy surface as a registered class, and `keyEntity.
|
|
185
|
-
directly. This is how tag-like,
|
|
186
|
-
|
|
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.
|
|
187
176
|
|
|
188
177
|
## Relationships, in brief
|
|
189
178
|
|
|
190
|
-
`e.target(Rel)` returns
|
|
191
|
-
|
|
192
|
-
`e.
|
|
193
|
-
|
|
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).
|
|
194
183
|
|
|
195
184
|
## Reference
|
|
196
185
|
|
|
197
186
|
`lib/vecs/src/entity/` (split across `entity.base.ts`, `entity.components.ts`,
|
|
198
187
|
`entity.lifecycle.ts`, `entity.relationships.ts`):
|
|
199
188
|
|
|
200
|
-
| Property / Method | Description
|
|
201
|
-
| ---------------------------------------------- |
|
|
202
|
-
| `eid` | Unique numeric entity id.
|
|
203
|
-
| `world` | The `World` that owns this entity.
|
|
204
|
-
| `name` | Identity name (getter/setter backed by the `Identity` component); `undefined` when unnamed.
|
|
205
|
-
| `componentBitmask` | `Bitset` of component type ids attached. Used for archetype matching.
|
|
206
|
-
| `components` | Read-only `ArrayMap` view of attached instances keyed by type id (`forEach`, `get`, `has`, `size`).
|
|
207
|
-
| `empty` | `true` when no components are attached.
|
|
208
|
-
| `properties` | Free-form `Map<string, any>` for module-level bookkeeping.
|
|
209
|
-
| `meta` | `ComponentMeta` for using this entity as a component key (created lazily).
|
|
210
|
-
| `instanceCount()` | Number of entities currently carrying this entity's eid as a component.
|
|
211
|
-
| `add(ref)`
|
|
212
|
-
| `attach(instance)` / `attach(Class, instance)` | Store an existing instance, replacing any previous one; fires set-side events. Returns `this`.
|
|
213
|
-
| `set(ref, props)` | Attach
|
|
214
|
-
| `get(ref)` | Read-only component lookup, or `undefined`.
|
|
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
|
-
| `
|
|
218
|
-
| `
|
|
219
|
-
| `
|
|
220
|
-
| `
|
|
221
|
-
| `
|
|
222
|
-
| `
|
|
223
|
-
| `
|
|
224
|
-
| `
|
|
225
|
-
| `parent`
|
|
226
|
-
| `
|
|
227
|
-
| `
|
|
228
|
-
| `
|
|
229
|
-
| `
|
|
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"`. |
|