@vworlds/vecs 1.0.53 → 1.0.55

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 (54) hide show
  1. package/README.md +9 -0
  2. package/dist/entity/entity.components.js +4 -2
  3. package/dist/entity/entity.components.js.map +1 -1
  4. package/dist/entity/entity.lifecycle.d.ts +2 -0
  5. package/dist/entity/entity.lifecycle.js +9 -4
  6. package/dist/entity/entity.lifecycle.js.map +1 -1
  7. package/dist/geometry.d.ts +5 -0
  8. package/dist/geometry.js +2 -0
  9. package/dist/geometry.js.map +1 -0
  10. package/dist/index.d.ts +3 -1
  11. package/dist/index.js +2 -1
  12. package/dist/index.js.map +1 -1
  13. package/dist/package.json +1 -1
  14. package/dist/system.d.ts +10 -0
  15. package/dist/system.js +19 -2
  16. package/dist/system.js.map +1 -1
  17. package/dist/timer.d.ts +15 -1
  18. package/dist/timer.js +24 -4
  19. package/dist/timer.js.map +1 -1
  20. package/dist/world/destroy-order.d.ts +8 -0
  21. package/dist/world/destroy-order.js +60 -0
  22. package/dist/world/destroy-order.js.map +1 -0
  23. package/dist/world/stats.d.ts +51 -0
  24. package/dist/world/stats.js +88 -0
  25. package/dist/world/stats.js.map +1 -0
  26. package/dist/world/world.base.js +2 -0
  27. package/dist/world/world.base.js.map +1 -1
  28. package/dist/world/world.components.d.ts +2 -0
  29. package/dist/world/world.components.js +4 -0
  30. package/dist/world/world.components.js.map +1 -1
  31. package/dist/world/world.disposal.d.ts +34 -36
  32. package/dist/world/world.disposal.js +89 -94
  33. package/dist/world/world.disposal.js.map +1 -1
  34. package/dist/world/world.entities.d.ts +21 -1
  35. package/dist/world/world.entities.js +54 -7
  36. package/dist/world/world.entities.js.map +1 -1
  37. package/dist/world/world.js +17 -11
  38. package/dist/world/world.js.map +1 -1
  39. package/dist/world/world.modules.d.ts +8 -0
  40. package/dist/world/world.modules.js +22 -1
  41. package/dist/world/world.modules.js.map +1 -1
  42. package/dist/world/world.pipeline.d.ts +15 -1
  43. package/dist/world/world.pipeline.js +40 -4
  44. package/dist/world/world.pipeline.js.map +1 -1
  45. package/docs/README.md +21 -19
  46. package/docs/components.md +9 -0
  47. package/docs/concepts.md +4 -0
  48. package/docs/design-guide.md +7 -3
  49. package/docs/entities.md +36 -25
  50. package/docs/glossary.md +3 -3
  51. package/docs/modules.md +62 -0
  52. package/docs/systems.md +108 -2
  53. package/package.json +1 -1
  54. package/tsconfig.types.json +6 -1
package/docs/modules.md CHANGED
@@ -34,6 +34,10 @@ world.module(PoseSyncModule);
34
34
  module on first call — drawing its entity id from the `module` pool (900–999 by default, or pass
35
35
  an explicit `eid`) — and invokes `init(config)` exactly once. Subsequent calls return the existing
36
36
  module instance; passing a config again after the first registration throws.
37
+ Install new modules outside `progress()` and `defer`; installation also throws during a command
38
+ drain, before registering anything. Looking up an already installed module works in any of those
39
+ contexts. Lazy loading from a system or hook is the application's job: schedule it after
40
+ `progress()` returns.
37
41
 
38
42
  ## Configuration
39
43
 
@@ -84,6 +88,63 @@ Guidelines:
84
88
  placement, component registration. Keep that out of application code.
85
89
  - **One module per cohesive thing.** Don't pile unrelated features into one `init`.
86
90
 
91
+ ## Scopes and module provenance
92
+
93
+ `world.scope(parent, fn)` assigns `ChildOf parent` to entities created **synchronously in its body**:
94
+ plain entities, components, modules, query/system backing entities, and phases.
95
+ Nested scopes restore the caller's parent in `finally`, even when the callback throws. Enter scopes
96
+ outside deferred mode: `world.scope()` throws inside `progress()`, a `defer` block, or a command
97
+ drain, without running the callback. Scoped creations use `.childOf(parent)` before their creation
98
+ notification. Existing name/id/class lookups do not reparent anything. An explicit
99
+ `.childOf(other)` afterwards replaces the scope parent, just as any other exclusive `ChildOf` edge.
100
+ The parent must be live and belong to the same world. Async continuations are outside the scope.
101
+
102
+ Entities created by callbacks that vecs runs deferred inside the body are **not parented**. This
103
+ includes `query.forEach(cb)`, a query's `enter` backfill when built, and an explicit `world.defer`
104
+ inside a module's `init`. Callback-created entities are run-time data: the same callback outside the
105
+ scope would not parent them either. For example, a module's `enter` or `forEach` callback may create
106
+ one health-bar entity per existing ship. Those health bars remain roots; an entity created directly
107
+ in `init` is a child of the module.
108
+
109
+ ```ts
110
+ import { Entity, Module, World } from "@vworlds/vecs";
111
+
112
+ class Position {}
113
+ class Feature extends Module {
114
+ public init(): void {
115
+ this.world.component(Position);
116
+ this.world.entity("camera");
117
+ }
118
+ }
119
+ const world = new World();
120
+ const feature = world.module(Feature);
121
+ world.component(Position).parent === feature; // true
122
+ world.entity("camera").parent === feature; // true
123
+
124
+ function owningModule(entity: Entity): Module | undefined {
125
+ const visited = new Set<Entity>();
126
+ for (let parent = entity.parent; parent && !visited.has(parent); parent = parent.parent) {
127
+ if (parent instanceof Module) return parent;
128
+ visited.add(parent);
129
+ }
130
+ return undefined;
131
+ }
132
+ owningModule(world.component(Position)); // feature
133
+ world.scope(feature, () => world.entity("another child"));
134
+ ```
135
+
136
+ `world.module()` runs a new module's `init` inside `world.scope(moduleEntity, ...)`. Nested modules
137
+ are children of the enclosing scope; their own creations belong to the nested module. Provenance is
138
+ the **nearest module ancestor**, not necessarily the direct parent: a `OneOf` value may be parented
139
+ to its relationship entity, which is parented to the module. This is a real ownership hierarchy:
140
+ ordinary parent destruction cascades through its children, and subtree traversal follows it.
141
+
142
+ Vecs' own built-in modules installed by the `World` constructor are **not scoped**; their entities
143
+ remain roots. Provenance covers modules installed after the world is constructed, including physics,
144
+ network, Phaser and application modules. Registration outside a scope leaves a root. Idempotent
145
+ re-registration keeps the existing parent: there is no list of duplicate registrars and no record of
146
+ which module added a trait edge. Explicit reparenting changes ownership, not a historical audit.
147
+
87
148
  ## Built-in modules
88
149
 
89
150
  Every `World` constructor installs these (`lib/vecs/src/world/world.ts`); you interact with their
@@ -107,4 +168,5 @@ All are exported, so a custom world setup can reuse them.
107
168
  | `init(config)` | One-time setup hook; called by the world on first registration. |
108
169
  | `world.module(M)` / `world.module(M, config)` / `world.module(M, config, eid)` | Register (first call) or retrieve (later calls) the module. Config after first registration throws. |
109
170
  | `this.world` | Inside `init`: the owning world (a module is an entity). |
171
+ | `world.scope(parent, fn)` | Parent all synchronous creations in `fn`; existing lookups are unchanged. |
110
172
  | `ModuleConfig<T>` | Utility type extracting a module's config type. |
package/docs/systems.md CHANGED
@@ -26,7 +26,48 @@ world
26
26
 
27
27
  Newly created systems enter the pipeline the next time `world.progress` runs. Configuration must
28
28
  happen before the system is built (the first `progress`/`flush` after creation); builder calls
29
- after that throw.
29
+ after that throw. Reading `world.pipeline` between frames also builds pending queries.
30
+
31
+ ## Measuring systems
32
+
33
+ Enable opt-in synchronous timing for benchmarks, cadence guards, or live diagnostics:
34
+
35
+ ```ts
36
+ world.stats.enableSystems();
37
+ world.progress(1000, 16);
38
+ for (const { system, runs, totalMs, maxMs } of world.stats.systems()) {
39
+ console.log(system.name, runs, totalMs, maxMs);
40
+ }
41
+ world.stats.resetSystems(); // clear system and extension-operation totals
42
+ world.stats.disableSystems(); // stop measuring; retain results until reset
43
+ ```
44
+
45
+ Timing is off by default. `progress()` chooses the plain or instrumented loop once per tick;
46
+ the plain loop has no per-system timing branch, counter, or clock read. The instrumented loop
47
+ uses one `performance.now()` pair per pipeline entry and reuses first-seen slots.
48
+
49
+ `systems()` returns independent snapshots in current pipeline order, including systems with zero
50
+ runs. Each row holds the original `System`, not copied identity or cadence metadata. Newly
51
+ registered systems appear after the next pipeline refresh in `progress()`. Removed systems are
52
+ absent. Reading does not reset the totals.
53
+
54
+ A run means execution admitted by the system's enabled and tick-source gates, not a callback or
55
+ entity count: an idle reactive system or an empty `each` query still counts when due. Disabled
56
+ and cadence-skipped entries contribute neither runs nor time. For an admitted execution, timing
57
+ includes inbox processing, callbacks, and **the flush after that system**, including command
58
+ routing and hooks. Initial frame preparation and final frame closure are not attributed. An
59
+ execution that throws before completing its flush does not contribute a completed run.
60
+
61
+ Extensions expose breakdown rows through `world.stats.operations()`, under the same switch and
62
+ reset. See [ServerWorld replication timing](../../vecs-server/docs/replication-timing.md).
63
+ Reading either table allocates snapshots; normal measured ticks reuse slots. Reset keeps slots
64
+ and clears totals, including systems temporarily absent from the pipeline.
65
+
66
+ When implementing a world extension, choose the measured path once per frame with
67
+ `stats.systemsEnabled`, then call `stats.recordOperation(name, elapsedMs)` once per completed
68
+ operation. It aggregates a finite, non-negative duration into a reusable named slot. This method
69
+ does not read a clock or recheck the switch; it lets an already-measured frame complete even if
70
+ a callback disables future timing. Invalid durations throw `RangeError`.
30
71
 
31
72
  ## Membership: `with` / `without`
32
73
 
@@ -389,7 +430,10 @@ world.system("Log").tickSource(clock).run(log); // driven by a shared ITickSourc
389
430
  ```
390
431
 
391
432
  - `.interval(seconds)` replaces the cadence with an `IntervalTickSource`. Note the unit: seconds,
392
- unlike the millisecond deltas of `progress`.
433
+ unlike the millisecond deltas of `progress`. Interval boundaries tolerate floating-point
434
+ residue so whole-frame intervals do not fire one frame late (see `IntervalTickSource`'s
435
+ TSDoc for the tolerance). Each source fires at most once per frame, even with a large delta;
436
+ positive residual time is preserved, and a tolerated negative remainder is clamped to zero.
393
437
  - `.rate(n)` divides the current cadence source by `n` (for an unconfigured system: every `n`
394
438
  frames). `.rate(n, source)` divides an explicit upstream source.
395
439
  - `.tickSource(source)` mirrors another source directly — an `IntervalTickSource`,
@@ -420,6 +464,66 @@ ai.destroy(); // permanent: disables, then unregisters the backing entity
420
464
  While disabled, query membership is still maintained, so the tracked set is correct on resume.
421
465
  `destroy` fires no `exit` callbacks. Both `disable` and `enable` are idempotent and chainable.
422
466
 
467
+ ## Introspection
468
+
469
+ Read `world.pipeline` after finishing system configuration to inspect the **existing flat execution
470
+ pipeline**, not a separately maintained inventory. It returns `readonly System[]`, including disabled
471
+ systems, in the order selected by the pipeline query (including a custom `setPipeline`). Between
472
+ frames it builds pending queries and refreshes only when the pipeline query changed. It does not
473
+ execute systems or drain deferred commands. During a frame it returns that frame's pipeline; queued
474
+ changes become visible after their normal flush. Re-read the getter after changes: an old array is
475
+ not a live subscription. Read-only means the TypeScript array API; do not cast away the type to mutate it.
476
+
477
+ ```ts
478
+ import {
479
+ ALWAYS_TICK_SOURCE,
480
+ DependsOn,
481
+ IntervalTickSource,
482
+ RateTickSource,
483
+ System,
484
+ World,
485
+ type ITickSource,
486
+ } from "@vworlds/vecs";
487
+
488
+ function describeCadence(source: ITickSource): string {
489
+ if (source === ALWAYS_TICK_SOURCE) return "every tick";
490
+ if (source instanceof IntervalTickSource) return `every ${source.seconds}s`;
491
+ if (source instanceof RateTickSource) {
492
+ return `every ${source.rate} ticks of ${
493
+ source.source ? describeCadence(source.source) : "world frames"
494
+ }`;
495
+ }
496
+ if (source instanceof System) return `system ${source.name}`;
497
+ return "custom clock";
498
+ }
499
+
500
+ const world = new World();
501
+ world
502
+ .system("AI")
503
+ .interval(0.5)
504
+ .run(() => {});
505
+ for (const [order, system] of world.pipeline.entries()) {
506
+ console.log({
507
+ order,
508
+ name: system.name,
509
+ phase: system.entity.target(DependsOn)?.name,
510
+ cadence: describeCadence(system.cadence),
511
+ enabled: system.enabled,
512
+ });
513
+ }
514
+ ```
515
+
516
+ `system.cadence` exposes the current source; the existing `.tickSource(source)` method configures
517
+ it. Compare cadence by identity with the exported `ALWAYS_TICK_SOURCE` for the default every-tick
518
+ case. `IntervalTickSource.seconds`, `RateTickSource.rate`, and `RateTickSource.source` are getters;
519
+ an unconfigured standalone rate source has no upstream source and counts world frames. Replacing
520
+ cadence changes the getter immediately. A source's existing `start`/`stop` controls still work.
521
+
522
+ Name and phase need no new API: `system.name` and `system.entity.target(DependsOn)` already expose
523
+ them. Systems are entities carrying `System`, so a query with `System` can inventory them independently
524
+ of pipeline membership; retrieve the instance with `entity.get(System)`. The existing `toString()`
525
+ display is unchanged; introspection does not add structured query terms or `world.systems()`.
526
+
423
527
  ## Reference
424
528
 
425
529
  | Method | Description |
@@ -438,6 +542,8 @@ While disabled, query membership is still maintained, so the tracked set is corr
438
542
  | `disable()` / `enable()` | Pause / resume processing. Idempotent. |
439
543
  | `destroy()` | Permanently remove the system (no `exit` callbacks). |
440
544
  | `didTick` / `lastFireDelta` | This frame's cadence result; ms accumulated into the last fire. |
545
+ | `cadence` / `enabled` | Read current cadence source and enabled state; see [Introspection](#introspection). |
546
+ | `world.pipeline` | Read systems in pipeline execution order; see [Introspection](#introspection). |
441
547
  | `count` / `has(e)` / `belongs(e)` / iteration / `forEach` | Shared query surface; see [Queries](./queries-and-filters.md#reading-a-query). |
442
548
 
443
549
  `groupBy` is not available on systems — use `world.query(...).groupBy(...)`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vworlds/vecs",
3
- "version": "1.0.53",
3
+ "version": "1.0.55",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -1,4 +1,9 @@
1
1
  {
2
2
  "extends": "./tsconfig.json",
3
- "include": ["src", "tests/clock-inject.test.ts"]
3
+ "include": [
4
+ "src",
5
+ "tests/clock-inject.test.ts",
6
+ "tests/world-scope.test.ts",
7
+ "tests/system-introspection.test.ts"
8
+ ]
4
9
  }