@vworlds/vecs 1.0.35 → 1.0.36

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/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vworlds/vecs",
3
- "version": "1.0.35",
3
+ "version": "1.0.36",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -212,22 +212,24 @@ world.get(Gravity)?.y; // world.component(Gravity).get(Gravity)
212
212
  `SingletonModule` enforces the constraint (`lib/vecs/src/modules/singleton.ts`); see
213
213
  [Modules](./modules.md#built-in-modules).
214
214
 
215
- ## Companion components for non-ECS resources
215
+ ## Resources as components (non-ECS objects)
216
216
 
217
- To attach a non-ECS resource (a renderer object, a WASM handle, a DOM node) to an entity, wrap it
218
- in a local component instead of keeping a side `Map<eid, resource>`:
217
+ To bind a non-ECS resource (a renderer object, a WASM handle, a DOM node) to an entity, **store
218
+ the live object itself as a component** with `entity.attach(obj)` instead of keeping a side
219
+ `Map<eid, resource>`:
219
220
 
220
221
  ```ts
221
- class VGameObject {
222
- obj!: Phaser.GameObjects.GameObject;
223
- }
222
+ const obj = scene.add.arc(0, 0); // a Phaser GameObject
223
+ entity.attach(obj); // the GameObject is now a component, keyed by its class
224
224
 
225
- world.component(VGameObject).onRemove((_entity, vgo) => vgo.obj.destroy());
225
+ world.component(Phaser.GameObjects.Arc).onRemove((_entity, obj) => obj.destroy());
226
226
  ```
227
227
 
228
228
  The entity's component set then _is_ the resource lifecycle: queries drive create/destroy, and
229
229
  `onRemove` releases the resource whether the component is removed explicitly or the entity is
230
- destroyed. The full pattern including create/update/teardown phase placement is in the
230
+ destroyed. A plain wrapper component (`class Handle { obj }`) still works when you need extra fields
231
+ beside the resource. The full pattern — create/update, the remove-hook teardown, and `Implements`
232
+ interface markers for polymorphic dispatch — is in the
231
233
  [Design guide](./design-guide.md#7-patterns-reach-for-these).
232
234
 
233
235
  ## Reference
@@ -13,6 +13,12 @@ subsystem guides — start with [Concepts](./concepts.md) and the
13
13
  > by the presence and change of components — not by imperative bookkeeping.** Almost every
14
14
  > good pattern below is a consequence of taking that seriously.
15
15
 
16
+ > **How to use this guide.** You don't need to read all of it every time. Skim §1–§2 (the model
17
+ > and the execution facts everything else follows from) and the §8 anti-patterns + §9 pitfalls
18
+ > tables — together a fast TL;DR — then deep-read only the section your task touches: §3
19
+ > Components, §4 Systems & queries, §5 Phases, §6 Modules, §7 Patterns. The §10 case study shows
20
+ > all of it working together.
21
+
16
22
  ---
17
23
 
18
24
  ## 1. The model in one minute
@@ -114,22 +120,25 @@ new ServerWorld({ networkComponents: [Position, Rotation, /* … */ Arc] });
114
120
  > their fields silently fail to register, that's the standard-vs-legacy mismatch. vecs-wire
115
121
  > supports both; if you author new decorators, preserve that.
116
122
 
117
- ### Companion / wrapper components (hold non-ECS resources)
123
+ ### Resources as components (hold non-ECS objects)
118
124
 
119
- To attach a non-ECS resource (a Phaser `GameObject`, a WASM handle, a DOM node) to an entity,
120
- **wrap it in a local, non-networked component** instead of keeping a side `Map<eid, resource>`.
121
- The entity's component set _is_ the lifecycle; queries drive create/destroy; reset is free.
125
+ To bind a non-ECS resource (a Phaser `GameObject`, a WASM handle, a DOM node) to an entity,
126
+ **store the live object itself as a component** with `entity.attach(obj)` instead of keeping a
127
+ side `Map<eid, resource>`. `attach` registers the instance under its own class, so the entity's
128
+ component set _is_ the lifecycle; queries drive create/destroy; reset is free.
122
129
 
123
130
  ```ts
124
- export class VGameObject {
125
- obj!: Phaser.GameObjects.GameObject;
126
- }
131
+ const obj = scene.add.arc(0, 0); // a Phaser GameObject
132
+ entity.attach(obj); // the GameObject is now a component, keyed by its class
127
133
 
128
- world.component(VGameObject).onRemove((_entity, vgo) => vgo.obj.destroy());
134
+ world.component(Phaser.GameObjects.Arc).onRemove((_entity, obj) => obj.destroy());
129
135
  ```
130
136
 
131
- `onRemove` (below) releases the resource whenever the component leaves — whether removed
132
- explicitly or because the entity was destroyed.
137
+ `onRemove` (below) releases the resource whenever it leaves — whether removed explicitly or
138
+ because the entity was destroyed. When several resource types should share one cleanup hook or be
139
+ queried together, give them a common **interface** with `Implements` (see §7 and the §10 case
140
+ study). A plain wrapper component (`class Handle { obj }`) is still fine when you need extra fields
141
+ beside the resource.
133
142
 
134
143
  ### Component lifecycle hooks: `onAdd` / `onSet` / `onRemove`
135
144
 
@@ -139,7 +148,7 @@ explicitly or because the entity was destroyed.
139
148
  world.component(Impulse).onSet((entity, impulse) => {
140
149
  /* accumulate */
141
150
  });
142
- world.component(VGameObject).onRemove((_entity, vgo) => vgo.obj.destroy());
151
+ world.component(Phaser.GameObjects.Arc).onRemove((_entity, obj) => obj.destroy());
143
152
  ```
144
153
 
145
154
  Use them for resource management and cross-cutting reactions that aren't worth a system. (Note
@@ -224,12 +233,12 @@ Choose `onEnter: true` by asking who owns the initial value:
224
233
  `{ onEnter: true }` update callback for "initialize from current value, then keep synced."
225
234
 
226
235
  ```ts
227
- // ❌ redundant when update already owns initialization
228
- .enter([Rotation, VGameObject], (_e, [r, vgo]) => vgo.obj.setRotation(coords.rot(r.angle)))
229
- .update({ watch: Rotation, onEnter: true, inject: [VGameObject] }, (_e, r, [vgo]) => vgo.obj.setRotation(coords.rot(r.angle)));
236
+ // ❌ redundant when update already owns initialization (Transform = Implements marker → the attached GameObject)
237
+ .enter([Rotation, Transform], (_e, [r, obj]) => obj.setRotation(coords.rot(r.angle)))
238
+ .update({ watch: Rotation, onEnter: true, inject: [Transform] }, (_e, r, [obj]) => obj.setRotation(coords.rot(r.angle)));
230
239
 
231
240
  // ✅ update opts into entry-time initialization
232
- .update({ watch: Rotation, onEnter: true, inject: [VGameObject] }, (_e, r, [vgo]) => vgo.obj.setRotation(coords.rot(r.angle)));
241
+ .update({ watch: Rotation, onEnter: true, inject: [Transform] }, (_e, r, [obj]) => obj.setRotation(coords.rot(r.angle)));
233
242
  ```
234
243
 
235
244
  ### Injection
@@ -238,8 +247,8 @@ Pull components into the callback instead of `entity.get()`-ing them by hand:
238
247
 
239
248
  ```ts
240
249
  .enter([Arc], (entity, [arc]) => { /* … */ })
241
- .update({ watch: Arc, onEnter: true, inject: [VGameObject] }, (entity, arc, [vgo]) => { /* … */ })
242
- .exit([Arc, VGameObject], (entity, [arc, vgo]) => { /* … */ })
250
+ .update({ watch: Arc, onEnter: true, inject: [Transform] }, (entity, arc, [obj]) => { /* … */ })
251
+ .exit([Arc, Transform], (entity, [arc, obj]) => { /* … */ })
243
252
  .each([Position, Velocity], (entity, [pos, vel]) => { /* … */ })
244
253
  ```
245
254
 
@@ -287,8 +296,9 @@ ballsByCell.group(k)?.count;
287
296
  ### One system = one phase
288
297
 
289
298
  A system lives in exactly **one** phase. If a concern needs part of its work earlier and part
290
- later, **split it into multiple systems** in different phases (e.g. teardown in `PRE_STORE`,
291
- build in `ON_STORE`). You cannot put a system's `.exit` in one phase and its `.update` in another.
299
+ later, **split it into multiple systems** in different phases (e.g. validation in `ON_VALIDATE`,
300
+ corrections in `POST_UPDATE`). You cannot put a system's `.exit` in one phase and its `.update` in
301
+ another.
292
302
 
293
303
  ---
294
304
 
@@ -305,8 +315,8 @@ build in `ON_STORE`). You cannot put a system's `.exit` in one phase and its `.u
305
315
  - **Order within a phase = registration order**, which an umbrella module controls by the order
306
316
  it loads sub-modules. **Cross-module ordering should be expressed with phases, not registration
307
317
  order** — never rely on "module A happened to load before module B" for correctness when the two
308
- are independent. (Example: teardown must run before creation across all shapes → put teardown
309
- in `PRE_STORE` and creation in `ON_STORE`; then it holds no matter which shape module loaded
318
+ are independent. (Example: the server's physics-to-render pose-sync must run after the physics step → put it
319
+ in `POST_UPDATE`, which is after the physics phases, rather than hoping the physics module loaded
310
320
  first.)
311
321
 
312
322
  ---
@@ -323,14 +333,14 @@ export class PhaserServerModule extends Module<undefined> {
323
333
  world
324
334
  .system("phaser.sync.position")
325
335
  .with(PhysicsPosition, RenderPosition)
326
- .phase(PRE_STORE)
336
+ .phase(POST_UPDATE)
327
337
  .update({ watch: PhysicsPosition, onEnter: true }, (e, p) =>
328
338
  e.set(RenderPosition, { x: p.x, y: p.y })
329
339
  );
330
340
  world
331
341
  .system("phaser.sync.rotation")
332
342
  .with(PhysicsRotation, RenderRotation)
333
- .phase(PRE_STORE)
343
+ .phase(POST_UPDATE)
334
344
  .update({ watch: PhysicsRotation, onEnter: true }, (e, r) =>
335
345
  e.set(RenderRotation, { angle: r.angle })
336
346
  );
@@ -353,7 +363,7 @@ Guidelines:
353
363
  export class PhaserRenderModule extends Module<{ scene; pixelsPerMeter? }> {
354
364
  public init(cfg): void {
355
365
  const coords = new CoordSpace(cfg.scene, cfg.pixelsPerMeter);
356
- this.world.component(VGameObject).onRemove((_e, vgo) => vgo.obj.destroy());
366
+ this.world.component(GameObject).onRemove((_e, obj) => obj.destroy()); // GameObject: Implements marker over every Phaser object
357
367
  this.world.module(ArcModule, { scene: cfg.scene, coords });
358
368
  this.world.module(RectangleModule, { scene: cfg.scene, coords });
359
369
  // … shape modules first …
@@ -374,57 +384,54 @@ Guidelines:
374
384
 
375
385
  ### Resource-as-component lifecycle (kills the `eid → object` map)
376
386
 
377
- The canonical pattern for binding an external object to entities, fully query-driven:
387
+ The canonical pattern for binding an external object to entities, fully query-driven. **Attach the
388
+ live object as its own component** and let a remove hook release it — no wrapper, no side map:
378
389
 
379
390
  ```ts
380
- class VGameObject {
381
- obj!: RenderObject;
382
- }
383
- world.component(VGameObject).onRemove((_e, vgo) => vgo.obj.destroy()); // release on remove/destroy
391
+ // Release the resource whenever its component leaves (explicit remove or entity destroy):
392
+ world.component(Phaser.GameObjects.Arc).onRemove((_e, obj) => obj.destroy());
384
393
 
385
394
  // CREATE: a renderable that has no object yet
386
395
  world
387
396
  .system("new.arc")
388
397
  .with(Arc)
389
- .without(VGameObject)
398
+ .without(Phaser.GameObjects.Arc)
390
399
  .phase(ON_STORE)
391
- .enter([Arc], (e) => {
392
- const obj = scene.add.arc(0, 0);
393
- e.set(VGameObject, { obj });
400
+ .enter([Arc], (e, [arc]) => {
401
+ const obj = scene.add.arc(0, 0, coords.len(arc.radius));
402
+ e.attach(obj); // the GameObject is now a component, keyed by its class
394
403
  });
395
- // ↑ set(VGameObject) is flushed after this system → the entity now matches the systems below, this same tick
404
+ // ↑ attach is flushed after this system → the entity now matches the systems below, this same tick
396
405
 
397
406
  // UPDATE geometry on the bound object (opts into entry + change)
398
407
  world
399
408
  .system("render.arc")
400
- .with(Arc, VGameObject)
409
+ .with(Arc, Phaser.GameObjects.Arc)
401
410
  .phase(ON_STORE)
402
- .update({ watch: Arc, onEnter: true, inject: [VGameObject] }, (e, arc, [vgo]) =>
403
- (vgo.obj as ArcObject).setRadius(coords.len(arc.radius))
411
+ .update({ watch: Arc, inject: [Phaser.GameObjects.Arc] }, (e, arc, [obj]) =>
412
+ obj.setRadius(coords.len(arc.radius))
404
413
  );
405
414
 
406
- // TEARDOWN: when the shape leaves (type swap) or the entity dies, drop the companion → onRemove destroys
407
- world
408
- .system("teardown.arc")
409
- .with(Arc, VGameObject)
410
- .phase(PRE_STORE)
411
- .exit([Arc, VGameObject], (e) => {
412
- if (e.has(VGameObject)) e.remove(VGameObject);
413
- });
415
+ // TEARDOWN: when the shape data leaves (type swap) or the entity dies, drop the object.
416
+ // A component remove hook keeps this declarative — no exit system, no phase to coordinate:
417
+ world.component(Arc).onRemove((e) => {
418
+ if (e.has(Phaser.GameObjects.Arc)) e.remove(Phaser.GameObjects.Arc);
419
+ });
414
420
  ```
415
421
 
416
- Why it's good: no manual map; **destroy/reset is automatic** (destroying the entity removes
417
- `VGameObject` → `onRemove` releases the resource); **type changes work for free** via exclusivity
418
- (see below). Note the ordering: teardown in `PRE_STORE` runs before creation in `ON_STORE`, so an
419
- `Arc Rectangle` swap removes the old companion _before_ the new shape's `.without(VGameObject)`
420
- creation query runs same frame.
422
+ Why it's good: no manual map; **destroy/reset is automatic** (destroying the entity removes the
423
+ attached object its `onRemove` releases the resource); **type changes work for free** via
424
+ exclusivity (see below). Teardown is a remove **hook**, not a system, so it fires synchronously at
425
+ the mutation site regardless of phase no teardown-before-create ordering to get right. When many
426
+ resource types share update/teardown logic, give them a common interface with `Implements` (§10)
427
+ and query the interface instead of each concrete class.
421
428
 
422
429
  ### Exclusivity-driven type swap
423
430
 
424
431
  With `setExclusiveComponents(...renderables)`, replacing the renderable removes the old one. The
425
- old shape's teardown system (`.with(OldShape, VGameObject).exit`) fires and drops the companion;
426
- the new shape's creation system (`.with(NewShape).without(VGameObject).enter`) fires and rebinds.
427
- No special-casing anywhere.
432
+ old shape data component's remove hook drops its attached object ( `onRemove` destroys it); the
433
+ new shape's creation system (`.with(NewShape).without(Phaser.GameObjects.NewKind).enter`) fires and
434
+ rebinds. No teardown system, no special-casing anywhere.
428
435
 
429
436
  ### Reactive sync between two component spaces
430
437
 
@@ -499,26 +506,32 @@ small **render components**; the client turns them into Phaser `GameObject`s wit
499
506
  render code**.
500
507
 
501
508
  - **Data (`@vworlds/vecs-phaser`):** small, single-purpose, networked components — `Position`,
502
- `Rotation`, `Scale`, `Alpha`, `Depth`, `FillStyle`, `StrokeStyle`, and one-of-N renderables
503
- (`Arc`, `Rectangle`, `Text`, …). `networkComponents` order is the protocol.
509
+ `Rotation`, `Scale`, `Alpha`, `Depth`, `FillStyle`, `StrokeStyle`, `Size`, and one-of-N
510
+ renderables (`Arc`, `Rectangle`, `Text`, …). `networkComponents` order is the protocol.
504
511
  - **Server (`PhaserServerModule`, zero-config):** owns exclusivity
505
- (`setExclusiveComponents(...renderables)`); two **reactive** pose-sync systems in `PRE_STORE`
512
+ (`setExclusiveComponents(...renderables)`); two **reactive** pose-sync systems in `POST_UPDATE`
506
513
  copy physics `Position`/`Rotation` → render pose only when physics moved (`.update`, not
507
514
  `.each`), and only for entities that already have a render pose (no creation — pure ECS). No
508
515
  builders: game code creates entities and adds the components it wants.
509
- - **Client (`PhaserRenderModule` umbrella → per-shape modules):** binds each entity's renderable
510
- to a Phaser object stored in the **`VGameObject` companion component** (`onRemove destroy`).
511
- Per shape: a **creation** system (`.with(Shape).without(VGameObject).enter` `scene.add.*` +
512
- `set(VGameObject)`) in `ON_STORE`; a **geometry** system (`.with(Shape, VGameObject).update`)
513
- in `ON_STORE`; a **teardown** system (`.exit remove VGameObject`) in `PRE_STORE`. Shared
514
- transform/style **sync** systems (`.with(Component, VGameObject).update({ watch: Component, onEnter: true })`)
515
- apply position/rotation/scale/alpha/depth/fill/stroke initialization is explicit via
516
- `onEnter: true`, and the umbrella loads shape modules _before_ the sync module so creation
517
- precedes sync within `ON_STORE`.
518
- - **Lifecycle that falls out for free:** a renderable **type swap** is handled by exclusivity +
519
- teardown(`PRE_STORE`)-before-create(`ON_STORE`); a **baseline reset / disconnect** is just
520
- `clearAllEntities()` entities destroyed `VGameObject` removed `onRemove` destroys the
521
- Phaser objects. No maps, no `start/stop`, no reset handler.
516
+ - **Client (`PhaserRenderModule` umbrella → per-shape modules):** the live Phaser object is
517
+ attached to its entity **as the component** (`entity.attach(obj)`) no wrapper. Each concrete
518
+ Phaser class declares the **interface markers** it satisfies with `Implements` (`GameObject`,
519
+ `Transform`, `Fill`, plus a geometry marker like `Arc`/`Vertices`/`Text`), so one set of
520
+ systems drives every renderable polymorphically. Per shape: a **creation** system
521
+ (`.with(Shape).without(Phaser.GameObjects.Kind).enter` `scene.add.*` + `entity.attach`) in
522
+ `ON_STORE`; a **geometry** system (`.with(Shape, m.Geometry).update`) in `ON_STORE`; and a
523
+ shape-data **remove hook** (`component(Shape).onRemove` remove the Phaser object) instead of a
524
+ teardown system. Shared transform/style **sync** systems
525
+ (`.with(Component, m.Marker).update({ watch: Component, onEnter: true })`) apply
526
+ position/rotation/scale/alpha/depth/size/fill/stroke through the markers initialization is
527
+ explicit via `onEnter: true`, and the umbrella loads shape modules _before_ the sync module so
528
+ creation precedes sync within `ON_STORE`.
529
+ - **Lifecycle that falls out for free:** destruction is a single hook —
530
+ `component(GameObject).onRemove((_e, obj) => obj.destroy())`. A renderable **type swap**
531
+ (exclusivity removes the old shape data → its remove hook drops the attached object → it's
532
+ destroyed → the new shape's creation system rebinds) and a **baseline reset / disconnect**
533
+ (`clearAllEntities()` → entities destroyed → attached objects removed → destroyed) both work with
534
+ no maps, no `start/stop`, no reset handler.
522
535
 
523
536
  The whole thing is "server says _what exists, where, and what it looks like_; the client owns the
524
537
  Phaser lifecycle" — expressed entirely as small components + phased reactive systems + modules.
package/docs/modules.md CHANGED
@@ -10,7 +10,7 @@ the module is registered. It is the unit of packaging for a feature: registering
10
10
  declaring systems, setting exclusivity and cleanup policy, and (rarely) inserting phases.
11
11
 
12
12
  ```ts
13
- import { Module, PRE_STORE } from "@vworlds/vecs";
13
+ import { Module, POST_UPDATE } from "@vworlds/vecs";
14
14
 
15
15
  export class PoseSyncModule extends Module {
16
16
  public init(): void {
@@ -20,7 +20,7 @@ export class PoseSyncModule extends Module {
20
20
  world
21
21
  .system("sync.position")
22
22
  .with(PhysicsPosition, RenderPosition)
23
- .phase(PRE_STORE)
23
+ .phase(POST_UPDATE)
24
24
  .update({ watch: PhysicsPosition, onEnter: true }, (e, p) =>
25
25
  e.set(RenderPosition, { x: p.x, y: p.y })
26
26
  );
@@ -62,7 +62,7 @@ is often one module per item plus a thin umbrella that composes them:
62
62
  ```ts
63
63
  export class PhaserRenderModule extends Module<{ scene: Scene }> {
64
64
  public init(cfg: { scene: Scene }): void {
65
- this.world.component(VGameObject).onRemove((_e, vgo) => vgo.obj.destroy());
65
+ this.world.component(GameObject).onRemove((_e, obj) => obj.destroy()); // GameObject: Implements marker over every Phaser object
66
66
  this.world.module(ArcModule, { scene: cfg.scene });
67
67
  this.world.module(RectangleModule, { scene: cfg.scene });
68
68
  // … shape modules first …
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vworlds/vecs",
3
- "version": "1.0.35",
3
+ "version": "1.0.36",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",