@vworlds/vecs 1.0.48 → 1.0.49

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.48",
3
+ "version": "1.0.49",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -305,7 +305,8 @@ Choose `onEnter: true` by asking who owns the initial value:
305
305
 
306
306
  ### Injection
307
307
 
308
- Pull components into the callback instead of `entity.get()`-ing them by hand:
308
+ **Inject every component a system callback always uses.** Pull it into the callback instead of
309
+ `entity.get()`-ing it by hand:
309
310
 
310
311
  ```ts
311
312
  .enter([Arc], (entity, [arc]) => { /* … */ })
@@ -314,6 +315,23 @@ Pull components into the callback instead of `entity.get()`-ing them by hand:
314
315
  .each([Position, Velocity], (entity, [pos, vel]) => { /* … */ })
315
316
  ```
316
317
 
318
+ Use `entity.get(C)` only for a component that is conditionally needed and rarely required: a read on
319
+ a branch most calls never take, where resolving the component for every call would be waste.
320
+
321
+ Why:
322
+
323
+ - **The signature states the inputs.** What a callback reads sits next to the `.with(...)` that
324
+ guarantees it, not in the body.
325
+ - **No dead guards.** A component guaranteed by `.with(...)` is injected as non-nullable, so there
326
+ is no `undefined` to check. `get` returns `Readonly<C> | undefined`, which invites an
327
+ `if (c !== undefined)` that tells the next reader the component might be missing when the query
328
+ says it cannot be. An injected instance is also the live, mutable one: a callback that writes in
329
+ place and calls `entity.modified(C)` (see §7) needs no cast.
330
+ - **Resolved once.** Injection resolves a component's id when the callback is registered;
331
+ `entity.get(C)` resolves the class on every call. In an `exit` callback injection matters more
332
+ still: it reads the snapshot captured when the entity left, while a `get` at replay time can find
333
+ the component already removed.
334
+
317
335
  Components listed in the inject tuple but **not** guaranteed by `.with(...)` resolve as
318
336
  `T | undefined` (the types reflect this) — guard them; the ones in `.with` are always present.
319
337
 
@@ -328,7 +346,7 @@ Components listed in the inject tuple but **not** guaranteed by `.with(...)` res
328
346
  // ❌ scans every body every tick, even if it didn't move
329
347
  .with(PhysicsPosition, RenderPosition).each([...], (e, [...]) => { if (changed) e.set(...) });
330
348
 
331
- // ✅ fires only when physics actually moved the body
349
+ // ✅ fires only when physics actually moved the body (compare before writing a networked B: see §7)
332
350
  .with(PhysicsPosition, RenderPosition).update(PhysicsPosition, (e, p) => e.set(RenderPosition, { x: p.x, y: p.y }));
333
351
  ```
334
352
 
@@ -396,15 +414,28 @@ export class PhaserServerModule extends Module<undefined> {
396
414
  .system("phaser.sync.position")
397
415
  .with(PhysicsPosition, RenderPosition)
398
416
  .phase(POST_UPDATE)
399
- .update({ watch: PhysicsPosition, onEnter: true }, (e, p) =>
400
- e.set(RenderPosition, { x: p.x, y: p.y })
417
+ .update(
418
+ { watch: PhysicsPosition, onEnter: true, inject: [RenderPosition] },
419
+ (e, p, [render]) => {
420
+ if (render.x !== p.x || render.y !== p.y) {
421
+ render.x = p.x;
422
+ render.y = p.y;
423
+ e.modified(RenderPosition);
424
+ }
425
+ }
401
426
  );
402
427
  world
403
428
  .system("phaser.sync.rotation")
404
429
  .with(PhysicsRotation, RenderRotation)
405
430
  .phase(POST_UPDATE)
406
- .update({ watch: PhysicsRotation, onEnter: true }, (e, r) =>
407
- e.set(RenderRotation, { angle: r.angle })
431
+ .update(
432
+ { watch: PhysicsRotation, onEnter: true, inject: [RenderRotation] },
433
+ (e, r, [render]) => {
434
+ if (render.angle !== r.angle) {
435
+ render.angle = r.angle;
436
+ e.modified(RenderRotation);
437
+ }
438
+ }
408
439
  );
409
440
  }
410
441
  }
@@ -583,14 +614,31 @@ rebinds. No teardown system, no special-casing anywhere.
583
614
 
584
615
  Copy `A → B`, gated on both existing (so you never _create_ `B`, you only keep an existing `B` in
585
616
  sync — pure-ECS: the owner decides whether `B` exists). Use `{ onEnter: true }` when `B` must reflect
586
- the current `A` as soon as the entity enters the sync system:
617
+ the current `A` as soon as the entity enters the sync system, inject `B` (the `.with(...)` guarantees
618
+ it, so it is non-nullable, see §4), and write only when `B` differs from `A`:
587
619
 
588
620
  ```ts
589
- .with(PhysicsPosition, RenderPosition).update({ watch: PhysicsPosition, onEnter: true }, (e, p) =>
590
- e.set(RenderPosition, { x: p.x, y: p.y })
621
+ .with(PhysicsPosition, RenderPosition).update(
622
+ { watch: PhysicsPosition, onEnter: true, inject: [RenderPosition] },
623
+ (e, p, [render]) => {
624
+ if (render.x !== p.x || render.y !== p.y) {
625
+ render.x = p.x;
626
+ render.y = p.y;
627
+ e.modified(RenderPosition);
628
+ }
629
+ }
591
630
  );
592
631
  ```
593
632
 
633
+ Every write is a modification, even with an equal value: both `set` and `modified` fire `onSet`,
634
+ route `update` events to every watching system and replicate a networked component. So a sync into a
635
+ networked or watched `B` compares before it writes. The injected `B` is the live instance, so assign
636
+ into it and call `entity.modified(B)`: that has the same effects as `set` without the props literal
637
+ and the deferred copy, and a second event for the same entity in one drain finds the first write
638
+ applied and writes nothing. Always pair an in-place write with `modified`: assignment alone notifies
639
+ nobody, so hooks, watchers and replication never see it. Use `set` when you do not hold the instance,
640
+ or when the write should create the component.
641
+
594
642
  Leave `onEnter: true` off (or set `onEnter: false`) only when a separate `.enter(...)` already initializes `B`,
595
643
  or when the callback is intentionally "future changes only."
596
644
 
@@ -707,6 +755,7 @@ system after whatever produces the entity/companion.
707
755
  | One system trying to act in two phases | Split into two systems, one per phase. |
708
756
  | Custom phases for app/render logic | Use the standard taxonomy; only engine modules insert phases. |
709
757
  | `.each` scanning every entity to detect change | Reactive `.update(C)` — fires only on change. |
758
+ | `entity.get(C)` for a component the callback always uses | Inject it (`inject: [C]`); keep `get` for a component that is conditionally needed and rarely required. |
710
759
  | Treating target storage as component data | Use `add(Rel, target)` / `remove(Rel, target)` and read with `target` / `targets` / `hasTarget`; target storage is private. |
711
760
  | A `registerFooSystem(world)` wrapper per system | Declare the system inline in the module's `init`. |
712
761
 
@@ -720,6 +769,13 @@ system after whatever produces the entity/companion.
720
769
  - **`update(C)` is change-only by default.** Add `{ onEnter: true }` for projection/sync callbacks
721
770
  that must apply the current watched value on entry; leave it off for mutation-only callbacks or
722
771
  when `.enter(...)` already owns initialization.
772
+ - **A write marks the component modified, even with an equal value.** `set`, and an in-place
773
+ assignment followed by `modified`, fire `onSet`, route `update` events to every watching system and
774
+ replicate a networked component. A writer that runs every tick and copies a value into a networked
775
+ or watched component (simulation output, a derived pose) should compare first and write only when
776
+ the value changed (see §7). An in-place assignment without `modified` notifies nobody.
777
+ - **A callback that always uses a component should inject it, not `get` it.** `entity.get(C)` is for
778
+ a component that is conditionally needed and rarely required (see §4).
723
779
  - **Membership is fixed by `.with(...)` / `.without(...)` / `.update(C)`;** `update(C)` watches `C`
724
780
  and requires it.
725
781
  - **`exit` retains injected references captured at exit** (components are not deep-cloned).
package/docs/systems.md CHANGED
@@ -189,6 +189,9 @@ an options object with `inject`, resolved per entity and passed as a typed tuple
189
189
  .each({ inject: [Position, Sprite] }, (e, [pos, sprite]) => { ... })
190
190
  ```
191
191
 
192
+ Inject every component a callback always uses, and keep `entity.get` for one that is only
193
+ conditionally needed (see [Design guide](./design-guide.md#injection)).
194
+
192
195
  Nullability follows membership: components guaranteed by `with` (or `hint`) are non-nullable;
193
196
  anything else resolves as `T | undefined` — guard it. (`enter`/`exit` injection requires the
194
197
  direct components to be present and throws otherwise; list only components your predicate
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vworlds/vecs",
3
- "version": "1.0.48",
3
+ "version": "1.0.49",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",