@vworlds/vecs 1.0.25 → 1.0.27

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.
Files changed (87) hide show
  1. package/README.md +29 -778
  2. package/dist/component.d.ts +1 -1
  3. package/dist/component.js +1 -1
  4. package/dist/component.js.map +1 -1
  5. package/dist/component_meta.d.ts +5 -1
  6. package/dist/component_meta.js +10 -0
  7. package/dist/component_meta.js.map +1 -1
  8. package/dist/dsl.d.ts +23 -2
  9. package/dist/dsl.js +21 -18
  10. package/dist/dsl.js.map +1 -1
  11. package/dist/entity/entity.base.d.ts +2 -18
  12. package/dist/entity/entity.base.js +2 -20
  13. package/dist/entity/entity.base.js.map +1 -1
  14. package/dist/entity/entity.components.d.ts +1 -0
  15. package/dist/entity/entity.components.js +50 -0
  16. package/dist/entity/entity.components.js.map +1 -1
  17. package/dist/entity/entity.d.ts +8 -0
  18. package/dist/entity/entity.js +21 -0
  19. package/dist/entity/entity.js.map +1 -1
  20. package/dist/entity/entity.lifecycle.d.ts +3 -3
  21. package/dist/entity/entity.lifecycle.js +6 -9
  22. package/dist/entity/entity.lifecycle.js.map +1 -1
  23. package/dist/entity/entity.relationships.d.ts +1 -1
  24. package/dist/entity/entity.relationships.js +3 -3
  25. package/dist/entity/entity.relationships.js.map +1 -1
  26. package/dist/filter.d.ts +1 -0
  27. package/dist/filter.js +3 -2
  28. package/dist/filter.js.map +1 -1
  29. package/dist/index.d.ts +4 -2
  30. package/dist/index.js +4 -2
  31. package/dist/index.js.map +1 -1
  32. package/dist/inject.d.ts +3 -2
  33. package/dist/inject.js.map +1 -1
  34. package/dist/modules/implements.d.ts +14 -0
  35. package/dist/modules/implements.js +98 -0
  36. package/dist/modules/implements.js.map +1 -0
  37. package/dist/modules/relationships.d.ts +1 -1
  38. package/dist/modules/relationships.js +2 -2
  39. package/dist/package.json +1 -4
  40. package/dist/query/callbacks.d.ts +6 -2
  41. package/dist/query/callbacks.js +5 -2
  42. package/dist/query/callbacks.js.map +1 -1
  43. package/dist/query/grouped_query.js +1 -1
  44. package/dist/query/query.d.ts +14 -1
  45. package/dist/query/query.js +26 -15
  46. package/dist/query/query.js.map +1 -1
  47. package/dist/system.d.ts +5 -4
  48. package/dist/system.js +17 -6
  49. package/dist/system.js.map +1 -1
  50. package/dist/terms/build.js +7 -5
  51. package/dist/terms/build.js.map +1 -1
  52. package/dist/util/array_map.d.ts +70 -12
  53. package/dist/util/array_map.js +113 -26
  54. package/dist/util/array_map.js.map +1 -1
  55. package/dist/util/bitset.js +0 -17
  56. package/dist/util/bitset.js.map +1 -1
  57. package/dist/util/events.d.ts +42 -12
  58. package/dist/util/events.js +94 -43
  59. package/dist/util/events.js.map +1 -1
  60. package/dist/util/ordered_set.js +43 -19
  61. package/dist/util/ordered_set.js.map +1 -1
  62. package/dist/world/world.deferred.js +2 -0
  63. package/dist/world/world.deferred.js.map +1 -1
  64. package/dist/world/world.entities.d.ts +8 -1
  65. package/dist/world/world.entities.js +25 -6
  66. package/dist/world/world.entities.js.map +1 -1
  67. package/dist/world/world.js +8 -1
  68. package/dist/world/world.js.map +1 -1
  69. package/dist/world/world.queries.js +6 -1
  70. package/dist/world/world.queries.js.map +1 -1
  71. package/dist/world/world.storage.d.ts +2 -2
  72. package/dist/world/world.storage.js +6 -3
  73. package/dist/world/world.storage.js.map +1 -1
  74. package/docs/README.md +50 -0
  75. package/docs/components.md +267 -0
  76. package/docs/concepts.md +86 -0
  77. package/docs/design-guide.md +506 -0
  78. package/docs/entities.md +177 -0
  79. package/docs/execution-model.md +173 -0
  80. package/docs/getting-started.md +215 -0
  81. package/docs/glossary.md +113 -0
  82. package/docs/modules.md +108 -0
  83. package/docs/queries-and-filters.md +187 -0
  84. package/docs/relationships.md +149 -0
  85. package/docs/systems.md +311 -0
  86. package/docs/utilities.md +139 -0
  87. package/package.json +1 -4
@@ -0,0 +1,506 @@
1
+ # vecs Design Guide
2
+
3
+ How to design with **vecs** — for humans and agents. This is a practical, opinionated
4
+ guide distilled from real designs (the physics engine, the Phaser render layer, the demo).
5
+ It explains the model, the execution semantics that make the tricks work, the idioms to
6
+ reach for, and the anti-patterns to avoid.
7
+
8
+ This guide assumes you know the machinery; the [docs home](./README.md) holds the
9
+ subsystem guides — start with [Concepts](./concepts.md) and the
10
+ [Execution model](./execution-model.md) if you are new.
11
+
12
+ > **Golden rule:** components are _data_, systems are _behavior_, and **lifecycle is driven
13
+ > by the presence and change of components — not by imperative bookkeeping.** Almost every
14
+ > good pattern below is a consequence of taking that seriously.
15
+
16
+ ---
17
+
18
+ ## 1. The model in one minute
19
+
20
+ | Concept | What it is |
21
+ | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
22
+ | **World** | The container. Holds entities, registered components, systems, phases, modules. You drive it with `world.progress(now, delta)`. |
23
+ | **Entity** | An id with a set of components. Create with `world.entity()`. |
24
+ | **Component** | A plain data class. Registered with `world.component(C)` (or by a module). No behavior. |
25
+ | **System** | A query + reactive callbacks (`enter`/`update`/`exit`) or a `run`/`each` body. Created with `world.system(name)`. |
26
+ | **Phase** | An ordered slot in the per-tick pipeline. Systems are assigned to a phase with `.phase(P)`. |
27
+ | **Module** | A unit of packaging: `class M extends Module<Config>` whose `init()` registers components, systems, phases, and policy. Installed with `world.module(M, config)`. |
28
+
29
+ ---
30
+
31
+ ## 2. The execution model (read this first — every trick follows from it)
32
+
33
+ `world.progress(now, delta)` runs the **pipeline**: systems grouped by **phase**, phases in a
34
+ fixed order. (The full mechanics are in [Execution model](./execution-model.md); this section
35
+ is the design-relevant core.) Internalize these four facts:
36
+
37
+ 1. **Phases run in a fixed order.** The eight built-in phases (`lib/vecs/src/modules/pipeline.ts`):
38
+
39
+ | Phase | Use it for |
40
+ | ------------- | ------------------------------------------------------------------------------------------------------ |
41
+ | `ON_LOAD` | Load data _into_ the ECS: inputs, network snapshots (`ClientWorld.apply()` runs here), external reads. |
42
+ | `POST_LOAD` | Process raw loaded data: turn key presses into high-level actions, map inputs to commands. |
43
+ | `PRE_UPDATE` | Final prep before game logic: clean up last frame, prepare state gameplay will use. |
44
+ | `ON_UPDATE` | **The default phase.** Main gameplay/simulation logic goes here. |
45
+ | `ON_VALIDATE` | Validate state after updates: collision detection, constraint checks. |
46
+ | `POST_UPDATE` | Apply corrections from validation: resolve collisions, fix up state. |
47
+ | `PRE_STORE` | Prepare data for output: compute transforms/render state once all logic is done, before storage. |
48
+ | `ON_STORE` | Store/emit the final frame: rendering, writing output, sending snapshots. |
49
+
50
+ 2. **Within a phase, systems run in registration order** (creation/eid order). So the order you
51
+ _create_ systems (e.g. the order an umbrella module loads sub-modules) is the order they run
52
+ _within the same phase_. Across **different** phases, ordering is guaranteed by the phase
53
+ spine — independent of registration order.
54
+
55
+ 3. **Structural changes are deferred and flushed after every system.** When a system calls
56
+ `entity.set/add/remove`/`entity.destroy()` during its run, the change is queued and applied
57
+ in a `flush()` **immediately after that system finishes**
58
+ (`lib/vecs/src/world/world.pipeline.ts`: `system._run()` then `this.flush()`). Consequence:
59
+ **a structural change made by system A is visible to every
60
+ later system in the same tick** (later in the same phase, or in a later phase). It is _not_
61
+ visible to A mid-iteration. This single fact enables the companion-component lifecycle, the
62
+ teardown-before-create ordering, and same-tick reconciliation.
63
+
64
+ 4. **Reactive routing.** `set`/`add`/`remove`/`destroy` route `enter`/`update`/`exit` events into
65
+ the matching systems' inboxes; each system drains its inbox at the top of its run. So systems
66
+ react to change without scanning.
67
+
68
+ ---
69
+
70
+ ## 3. Components
71
+
72
+ ### Small and single-purpose, always optional
73
+
74
+ Prefer many small components over one big one. `Position {x,y}`, `Rotation {angle}`,
75
+ `LinearVelocity {x,y}` — **not** a `Transform {x,y,angle,sx,sy}` god-component. Why:
76
+
77
+ - **Queries compose.** `.with(Position, Rotation)` selects exactly what a system needs; a system
78
+ that only reads position isn't coupled to rotation.
79
+ - **Optionality is the point.** An entity has a component or it doesn't. A render entity that
80
+ never rotates simply omits `Rotation`; a sync system gated on `.with(Rotation, ...)` ignores it.
81
+ Don't bake optional fields into a required mega-component with sentinel defaults.
82
+ - **Exclusivity and reuse.** Small components can be grouped, made exclusive, and reused across
83
+ unrelated features.
84
+
85
+ A component is a **plain data class**. No methods, no behavior:
86
+
87
+ ```ts
88
+ export class Position {
89
+ x = 0;
90
+ y = 0;
91
+ }
92
+ export class Rotation {
93
+ angle = 0;
94
+ }
95
+ ```
96
+
97
+ ### Networked components
98
+
99
+ Mark wire fields with `@type` from `@vworlds/vecs-wire` (conventionally imported as
100
+ `import { type as wireType }`). The
101
+ **order** you pass components in `networkComponents` _is the wire protocol_ (type id = index + 1).
102
+ Reordering is a breaking protocol change — snapshot-test the order.
103
+
104
+ ```ts
105
+ class Arc {
106
+ @wireType("f32") radius = 0.5;
107
+ }
108
+ new ServerWorld({ networkComponents: [Position, Rotation, /* … */ Arc] });
109
+ ```
110
+
111
+ > **Decorator gotcha.** `@type` must work under both _legacy_ (`experimentalDecorators`, used by
112
+ > `tsc`) and _standard_ TC39 decorators (what esbuild — `tsx`/Vite — emits for source files).
113
+ > If you load decorated component classes from **source** through an esbuild-based runtime and
114
+ > their fields silently fail to register, that's the standard-vs-legacy mismatch. vecs-wire
115
+ > supports both; if you author new decorators, preserve that.
116
+
117
+ ### Companion / wrapper components (hold non-ECS resources)
118
+
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.
122
+
123
+ ```ts
124
+ export class VGameObject {
125
+ obj!: Phaser.GameObjects.GameObject;
126
+ }
127
+
128
+ world.component(VGameObject).onRemove((_entity, vgo) => vgo.obj.destroy());
129
+ ```
130
+
131
+ `onRemove` (below) releases the resource whenever the component leaves — whether removed
132
+ explicitly or because the entity was destroyed.
133
+
134
+ ### Component lifecycle hooks: `onAdd` / `onSet` / `onRemove`
135
+
136
+ `world.component(C)` returns a handle with hooks fired on every entity:
137
+
138
+ ```ts
139
+ world.component(Impulse).onSet((entity, impulse) => {
140
+ /* accumulate */
141
+ });
142
+ world.component(VGameObject).onRemove((_entity, vgo) => vgo.obj.destroy());
143
+ ```
144
+
145
+ Use them for resource management and cross-cutting reactions that aren't worth a system. (Note
146
+ the difference from systems: hooks are per-component-instance and run at the mutation site;
147
+ systems are queries that run in a phase.)
148
+
149
+ ### Exclusive component groups
150
+
151
+ `world.setExclusiveComponents(A, B, C)` declares a mutually exclusive set: setting one
152
+ **auto-removes** the others. This is how you model "exactly one of N" (one renderable per
153
+ entity; one physics shape per body) and it makes **type changes** clean — see the swap pattern
154
+ in §7.
155
+
156
+ ```ts
157
+ world.setExclusiveComponents(...phaserRenderableComponents); // Arc | Rectangle | Text | …
158
+ entity.set(Arc, { radius: 1 });
159
+ entity.set(Rectangle, { width: 2, height: 1 }); // Arc auto-removed
160
+ ```
161
+
162
+ ### Relationships
163
+
164
+ A relationship is a component whose value points at another entity:
165
+
166
+ ```ts
167
+ class InCell extends Relationship {}
168
+ entity.set(InCell, { target: cell }); // retarget freely
169
+ entity.target(InCell); // the target entity
170
+ world.query("ByCell").with(Networked, Position).groupBy(InCell); // index by target
171
+ ```
172
+
173
+ - **`Relationship.target` is an `Entity` — not serializable, so relationships are not
174
+ networkable.** Don't try to replicate them.
175
+ - **Retargeting is a plain data change** (no archetype churn). Prefer retargeting a relationship
176
+ over swapping marker components to express "which bucket this entity is in."
177
+
178
+ ---
179
+
180
+ ## 4. Systems & queries
181
+
182
+ ```ts
183
+ world
184
+ .system("name")
185
+ .with(/* membership spec */)
186
+ .without(/* exclusion spec */)
187
+ .phase(ON_UPDATE)
188
+ .enter(/* … */)
189
+ .update(C /* … */)
190
+ .exit(/* … */);
191
+ ```
192
+
193
+ ### Membership: `.with` / `.without`
194
+
195
+ `.with(...)` defines who's in the query; `.without(...)` excludes. Specs can be component
196
+ classes or DSL objects (`{ all: [...] }`, `{ any: [...] }`, `{ parent: X }`, `{ target: [Rel, Tag] }`).
197
+
198
+ > `.with(...)`, `.without(...)`, and `.update(C)` own membership. `.update(C)` watches change events
199
+ > and also requires `C`, so `.with(A).update(C)` matches entities carrying both `A` and `C`.
200
+
201
+ ### Reactive callbacks: `enter` / `update` / `exit`
202
+
203
+ - **`enter`** — fires once when an entity _becomes_ a member. Use it for one-time setup
204
+ (create the resource).
205
+ - **`update(C, …)`** — adds `C` to the predicate and fires when watched component `C` is modified.
206
+ **It also fires on entry for every already-present watched component** (`System._enter` re-pushes
207
+ update events for watched components). So `.update(C)` initializes on entry _and_ reacts to
208
+ changes — **don't write a separate `.enter` that duplicates the same logic.**
209
+ - **`exit`** — fires when an entity stops matching (a component removed, or the entity destroyed).
210
+ Its injected components are read from a **snapshot captured at exit time**, so they're still
211
+ resolvable even though they're being removed.
212
+
213
+ ```ts
214
+ // ❌ redundant
215
+ .enter([Rotation, VGameObject], (_e, [r, vgo]) => vgo.obj.setRotation(coords.rot(r.angle)))
216
+ .update(Rotation, [VGameObject], (_e, r, [vgo]) => vgo.obj.setRotation(coords.rot(r.angle)));
217
+
218
+ // ✅ update fires on entry too
219
+ .update(Rotation, [VGameObject], (_e, r, [vgo]) => vgo.obj.setRotation(coords.rot(r.angle)));
220
+ ```
221
+
222
+ ### Injection
223
+
224
+ Pull components into the callback instead of `entity.get()`-ing them by hand:
225
+
226
+ ```ts
227
+ .enter([Arc], (entity, [arc]) => { /* … */ })
228
+ .update(Arc, [VGameObject], (entity, arc, [vgo]) => { /* … */ })
229
+ .exit([Arc, VGameObject], (entity, [arc, vgo]) => { /* … */ })
230
+ .each([Position, Velocity], (entity, [pos, vel]) => { /* … */ })
231
+ ```
232
+
233
+ Components listed in the inject tuple but **not** guaranteed by `.with(...)` resolve as
234
+ `T | undefined` (the types reflect this) — guard them; the ones in `.with` are always present.
235
+
236
+ ### `each` vs reactive — don't scan when you can react
237
+
238
+ - **`.each([...], cb)`** iterates **every matching entity every tick**. Use it only when you
239
+ genuinely need a per-frame sweep over all members.
240
+ - **Reactive `enter`/`update`/`exit`** fire only on change. For "copy A→B when A changes,"
241
+ reactive `.update(A)` touches _nothing_ on idle entities, where `.each` would scan them all.
242
+
243
+ ```ts
244
+ // ❌ scans every body every tick, even if it didn't move
245
+ .with(PhysicsPosition, RenderPosition).each([...], (e, [...]) => { if (changed) e.set(...) });
246
+
247
+ // ✅ fires only when physics actually moved the body
248
+ .with(PhysicsPosition, RenderPosition).update(PhysicsPosition, (e, p) => e.set(RenderPosition, { x: p.x, y: p.y }));
249
+ ```
250
+
251
+ ### `run` and `interval`
252
+
253
+ - **`.run(cb)`** — a system with no per-entity query; the body runs once per tick. For
254
+ frame-level work, external I/O, stepping an embedded simulation.
255
+ - **`.interval(seconds)`** — throttle a system to run at most every `seconds`.
256
+
257
+ ### Grouped queries (indexing without scans)
258
+
259
+ `groupBy` maintains a live bucketing you can look up by key — for spatial grids, interest
260
+ management, any "find members in bucket K" need — without re-scanning each tick:
261
+
262
+ ```ts
263
+ const ballsByCell = world
264
+ .query("ByCell")
265
+ .with(Networked, Ball, Position)
266
+ .groupBy([Position], (_e, [p]) => cellIndex(p)); // re-buckets reactively as Position changes
267
+
268
+ ballsByCell.group(k)?.forEach([Position, Ball], (e, [p, b]) => {
269
+ /* candidates in cell k */
270
+ });
271
+ ballsByCell.group(k)?.count;
272
+ ```
273
+
274
+ ### One system = one phase
275
+
276
+ A system lives in exactly **one** phase. If a concern needs part of its work earlier and part
277
+ later, **split it into multiple systems** in different phases (e.g. teardown in `PRE_STORE`,
278
+ build in `ON_STORE`). You cannot put a system's `.exit` in one phase and its `.update` in another.
279
+
280
+ ---
281
+
282
+ ## 5. Phases & ordering
283
+
284
+ - **Map your work onto the standard taxonomy** (§2). Input → `ON_LOAD`/`POST_LOAD`; gameplay →
285
+ `ON_UPDATE`; validation → `ON_VALIDATE`; corrections → `POST_UPDATE`; render prep → `PRE_STORE`;
286
+ output/render/send → `ON_STORE`.
287
+ - **Avoid custom phases for application/render code.** `insertPhaseAfter` exists, but it's for
288
+ engine-level modules that genuinely need their own spine (e.g. physics inserts
289
+ `physics-pre`/`physics-step`/`physics-post` after `ON_UPDATE`). App and render systems should
290
+ use the built-in phases; a render system just needs to run _after_ whoever produced its inputs,
291
+ and `PRE_STORE`/`ON_STORE` are after gameplay and physics.
292
+ - **Order within a phase = registration order**, which an umbrella module controls by the order
293
+ it loads sub-modules. **Cross-module ordering should be expressed with phases, not registration
294
+ order** — never rely on "module A happened to load before module B" for correctness when the two
295
+ are independent. (Example: teardown must run before creation across all shapes → put teardown
296
+ in `PRE_STORE` and creation in `ON_STORE`; then it holds no matter which shape module loaded
297
+ first.)
298
+
299
+ ---
300
+
301
+ ## 6. Modules
302
+
303
+ A module packages a feature: it registers components, systems, exclusivity, and (rarely) phases.
304
+
305
+ ```ts
306
+ export class PhaserServerModule extends Module<undefined> {
307
+ public init(): void {
308
+ const world = this.world;
309
+ world.setExclusiveComponents(...phaserRenderableComponents);
310
+ world
311
+ .system("phaser.sync.position")
312
+ .with(PhysicsPosition, RenderPosition)
313
+ .phase(PRE_STORE)
314
+ .update(PhysicsPosition, (e, p) => e.set(RenderPosition, { x: p.x, y: p.y }));
315
+ world
316
+ .system("phaser.sync.rotation")
317
+ .with(PhysicsRotation, RenderRotation)
318
+ .phase(PRE_STORE)
319
+ .update(PhysicsRotation, (e, r) => e.set(RenderRotation, { angle: r.angle }));
320
+ }
321
+ }
322
+
323
+ world.module(PhaserServerModule); // registered once; init runs once
324
+ ```
325
+
326
+ Guidelines:
327
+
328
+ - **Declare systems inline in `init`.** Don't write an `installFooSystem(world)` helper that just
329
+ registers one system — it adds a layer for nothing.
330
+ - **Config flows through `world.module(M, config)`** and into `init(config)`. Keep it minimal;
331
+ prefer zero-config when a sensible default (e.g. a standard phase) exists.
332
+ - **One module per cohesive thing.** A big feature can be **one module per item** (e.g. an
333
+ `ArcModule`, `RectangleModule`, …) plus a thin **umbrella module** whose `init` composes them:
334
+
335
+ ```ts
336
+ export class PhaserRenderModule extends Module<{ scene; pixelsPerMeter? }> {
337
+ public init(cfg): void {
338
+ const coords = new CoordSpace(cfg.scene, cfg.pixelsPerMeter);
339
+ this.world.component(VGameObject).onRemove((_e, vgo) => vgo.obj.destroy());
340
+ this.world.module(ArcModule, { scene: cfg.scene, coords });
341
+ this.world.module(RectangleModule, { scene: cfg.scene, coords });
342
+ // … shape modules first …
343
+ this.world.module(TransformStyleSyncModule, { coords }); // … then sync, so create precedes sync within ON_STORE
344
+ }
345
+ }
346
+ ```
347
+
348
+ A module's `init` can `this.world.module(Sub, subConfig)` to compose, and the **load order sets
349
+ same-phase ordering**.
350
+
351
+ - **Modules own cross-cutting policy:** exclusivity (`setExclusiveComponents`), phase placement,
352
+ component registration. Keep that out of application code.
353
+
354
+ ---
355
+
356
+ ## 7. Patterns (reach for these)
357
+
358
+ ### Resource-as-component lifecycle (kills the `eid → object` map)
359
+
360
+ The canonical pattern for binding an external object to entities, fully query-driven:
361
+
362
+ ```ts
363
+ class VGameObject {
364
+ obj!: RenderObject;
365
+ }
366
+ world.component(VGameObject).onRemove((_e, vgo) => vgo.obj.destroy()); // release on remove/destroy
367
+
368
+ // CREATE: a renderable that has no object yet
369
+ world
370
+ .system("new.arc")
371
+ .with(Arc)
372
+ .without(VGameObject)
373
+ .phase(ON_STORE)
374
+ .enter([Arc], (e) => {
375
+ const obj = scene.add.arc(0, 0);
376
+ e.set(VGameObject, { obj });
377
+ });
378
+ // ↑ set(VGameObject) is flushed after this system → the entity now matches the systems below, this same tick
379
+
380
+ // UPDATE geometry on the bound object (fires on entry + on change)
381
+ world
382
+ .system("render.arc")
383
+ .with(Arc, VGameObject)
384
+ .phase(ON_STORE)
385
+ .update(Arc, [VGameObject], (e, arc, [vgo]) =>
386
+ (vgo.obj as ArcObject).setRadius(coords.len(arc.radius))
387
+ );
388
+
389
+ // TEARDOWN: when the shape leaves (type swap) or the entity dies, drop the companion → onRemove destroys
390
+ world
391
+ .system("teardown.arc")
392
+ .with(Arc, VGameObject)
393
+ .phase(PRE_STORE)
394
+ .exit([Arc, VGameObject], (e) => {
395
+ if (e.has(VGameObject)) e.remove(VGameObject);
396
+ });
397
+ ```
398
+
399
+ Why it's good: no manual map; **destroy/reset is automatic** (destroying the entity removes
400
+ `VGameObject` → `onRemove` releases the resource); **type changes work for free** via exclusivity
401
+ (see below). Note the ordering: teardown in `PRE_STORE` runs before creation in `ON_STORE`, so an
402
+ `Arc → Rectangle` swap removes the old companion _before_ the new shape's `.without(VGameObject)`
403
+ creation query runs — same frame.
404
+
405
+ ### Exclusivity-driven type swap
406
+
407
+ With `setExclusiveComponents(...renderables)`, replacing the renderable removes the old one. The
408
+ old shape's teardown system (`.with(OldShape, VGameObject).exit`) fires and drops the companion;
409
+ the new shape's creation system (`.with(NewShape).without(VGameObject).enter`) fires and rebinds.
410
+ No special-casing anywhere.
411
+
412
+ ### Reactive sync between two component spaces
413
+
414
+ Copy `A → B` only when `A` changes, gated on both existing (so you never _create_ `B`, you only
415
+ keep an existing `B` in sync — pure-ECS: the owner decides whether `B` exists):
416
+
417
+ ```ts
418
+ .with(PhysicsPosition, RenderPosition).update(PhysicsPosition, (e, p) => e.set(RenderPosition, { x: p.x, y: p.y }));
419
+ ```
420
+
421
+ ### Relationship retargeting + grouped queries for spatial/interest indexing
422
+
423
+ Bucket entities by a relationship target (or a derived key) and look up buckets in O(1) instead of
424
+ scanning. Moving between buckets is a retarget (data change), not a component swap.
425
+
426
+ ### Initialize via `update`-on-entry
427
+
428
+ Because `update(C)` fires on entry, the "apply current value when the entity appears" step needs
429
+ no separate `enter` — just order the system after whatever produces the entity/companion.
430
+
431
+ ---
432
+
433
+ ## 8. Anti-patterns (keep ECS semantics)
434
+
435
+ | Smell | Do instead |
436
+ | ---------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
437
+ | A side `Map<eid, resource>` | Store the resource in a **companion component**; let queries drive its lifecycle; release in `onRemove`. |
438
+ | 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. |
439
+ | 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. |
440
+ | A god component with many optional fields | Split into small single-purpose components; absence encodes "not applicable." |
441
+ | Duplicated `enter` + `update` doing the same thing | One `update(C)` — it fires on entry too. |
442
+ | One system trying to act in two phases | Split into two systems, one per phase. |
443
+ | Custom phases for app/render logic | Use the standard taxonomy; only engine modules insert phases. |
444
+ | `.each` scanning every entity to detect change | Reactive `.update(C)` — fires only on change. |
445
+ | Networking a relationship | You can't (target is an `Entity`); replicate a plain id/tag instead, or keep the relationship server-local. |
446
+ | A `registerFooSystem(world)` wrapper per system | Declare the system inline in the module's `init`. |
447
+
448
+ ---
449
+
450
+ ## 9. Pitfalls / gotchas checklist
451
+
452
+ - **A system is in exactly one phase.** Plan splits accordingly.
453
+ - **Structural changes are deferred and flushed _after_ each system** — visible to later systems
454
+ this tick, not to the current system mid-iteration.
455
+ - **`update(C)` fires on entry** for already-present watched components. Lean on it; don't
456
+ duplicate in `enter`.
457
+ - **Membership is fixed by `.with(...)` / `.without(...)` / `.update(C)`;** `update(C)` watches `C`
458
+ and requires it.
459
+ - **`exit` reads a snapshot** of the injected components captured at exit (they're being removed).
460
+ - **Within a phase, order = registration order;** across phases, the spine guarantees order. Don't
461
+ use registration order for cross-module correctness — use phases.
462
+ - **Networked component order = wire protocol.** Reordering breaks the protocol.
463
+ - **`@type`/`@wireType` must survive esbuild's standard decorators** when components load from
464
+ source (tsx/Vite). Otherwise fields silently don't register.
465
+ - **`Relationship.target` is an `Entity`** — not wire-encodable.
466
+ - **`world.module(M)` runs `init` once.** Re-registering with a different config throws.
467
+
468
+ ---
469
+
470
+ ## 10. Case study: the Phaser render layer
471
+
472
+ A compact tour that uses everything above. Goal: the server owns authoritative entities with
473
+ small **render components**; the client turns them into Phaser `GameObject`s with **no per-entity
474
+ render code**.
475
+
476
+ - **Data (`@vworlds/vecs-phaser`):** small, single-purpose, networked components — `Position`,
477
+ `Rotation`, `Scale`, `Alpha`, `Depth`, `FillStyle`, `StrokeStyle`, and one-of-N renderables
478
+ (`Arc`, `Rectangle`, `Text`, …). `networkComponents` order is the protocol.
479
+ - **Server (`PhaserServerModule`, zero-config):** owns exclusivity
480
+ (`setExclusiveComponents(...renderables)`); two **reactive** pose-sync systems in `PRE_STORE`
481
+ copy physics `Position`/`Rotation` → render pose only when physics moved (`.update`, not
482
+ `.each`), and only for entities that already have a render pose (no creation — pure ECS). No
483
+ builders: game code creates entities and adds the components it wants.
484
+ - **Client (`PhaserRenderModule` umbrella → per-shape modules):** binds each entity's renderable
485
+ to a Phaser object stored in the **`VGameObject` companion component** (`onRemove → destroy`).
486
+ Per shape: a **creation** system (`.with(Shape).without(VGameObject).enter` → `scene.add.*` +
487
+ `set(VGameObject)`) in `ON_STORE`; a **geometry** system (`.with(Shape, VGameObject).update`)
488
+ in `ON_STORE`; a **teardown** system (`.exit → remove VGameObject`) in `PRE_STORE`. Shared
489
+ transform/style **sync** systems (`.with(Component, VGameObject).update(Component)`) apply
490
+ position/rotation/scale/alpha/depth/fill/stroke — initialization comes for free because
491
+ `update` fires on entry, and the umbrella loads shape modules _before_ the sync module so
492
+ creation precedes sync within `ON_STORE`.
493
+ - **Lifecycle that falls out for free:** a renderable **type swap** is handled by exclusivity +
494
+ teardown(`PRE_STORE`)-before-create(`ON_STORE`); a **baseline reset / disconnect** is just
495
+ `clearAllEntities()` → entities destroyed → `VGameObject` removed → `onRemove` destroys the
496
+ Phaser objects. No maps, no `start/stop`, no reset handler.
497
+
498
+ The whole thing is "server says _what exists, where, and what it looks like_; the client owns the
499
+ Phaser lifecycle" — expressed entirely as small components + phased reactive systems + modules.
500
+
501
+ ---
502
+
503
+ _Keep components small and dumb, let systems and phases express behavior and order, store
504
+ resources in components, react to change instead of scanning, and package it as modules. When a
505
+ design starts growing maps, factories, and start/stop wrappers, that's the signal you've stepped
506
+ outside ECS — step back in._