@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
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# Subtree snapshots
|
|
2
|
+
|
|
3
|
+
A subtree snapshot moves an entity forest between two vecs worlds: capture it in one world,
|
|
4
|
+
recreate it in another, or do both. It is the primitive behind a multi-sector host, where a ship
|
|
5
|
+
crosses a world boundary and its ECS state has to appear in the next sector's world.
|
|
6
|
+
|
|
7
|
+
Three operations, one matched set:
|
|
8
|
+
|
|
9
|
+
| Operation | Source world | Destination world |
|
|
10
|
+
| ----------------- | --------------- | ------------------- |
|
|
11
|
+
| `snapshotSubtree` | read-only | unchanged |
|
|
12
|
+
| `restoreSubtree` | unchanged | gets fresh entities |
|
|
13
|
+
| `moveSubtree` | roots destroyed | gets fresh entities |
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
const result = world.moveSubtree(ship, nextSectorWorld, {
|
|
17
|
+
components: [Position, LinearVelocity, Hull],
|
|
18
|
+
relationships: [ChildOf, Carries],
|
|
19
|
+
externalTargets: "error",
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
result.roots; // destination root entities
|
|
23
|
+
result.entities.get(sourceEid); // destination entity for one snapshot key
|
|
24
|
+
result.sourceToDestination.get(sourceEntity); // the same, keyed by source entity
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## What a snapshot holds
|
|
28
|
+
|
|
29
|
+
Nothing live. Entity ids, component ids, and relationship edges are all world-local, so a snapshot
|
|
30
|
+
records:
|
|
31
|
+
|
|
32
|
+
- the component **classes** the forest carried, plus each instance's captured **data**;
|
|
33
|
+
- each relationship's class, its non-edge **data**, and its **internal targets** as snapshot keys;
|
|
34
|
+
- how many targets were **left behind** (see external targets below).
|
|
35
|
+
|
|
36
|
+
The source's instances are never attached to the destination, and never mutated. Restore builds
|
|
37
|
+
every destination component through the same materializer `Entity.set` uses, so aliases,
|
|
38
|
+
exclusivity, `add` hooks, and query entry fire exactly once per component, the same as for an
|
|
39
|
+
ordinary `set`.
|
|
40
|
+
|
|
41
|
+
## Selecting what crosses
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
world.snapshotSubtree(root, {
|
|
45
|
+
traversal: ChildOf, // default; any registered relationship
|
|
46
|
+
components: [Position, Hull],
|
|
47
|
+
relationships: [ChildOf, Carries],
|
|
48
|
+
externalTargets: "omit", // default
|
|
49
|
+
});
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
- **`traversal`** — the relationship that defines "subtree". `ChildOf` by default; a convoy might
|
|
53
|
+
use its own ordering relationship instead. The forest is every entity reachable through that
|
|
54
|
+
relationship, transitively.
|
|
55
|
+
- **`components` / `relationships`** — allowlists. Omit one to select every type in that category;
|
|
56
|
+
supply one to select exactly those. `ChildOf` is captured as the traversal relationship whatever
|
|
57
|
+
the `relationships` list says, since without it the result is not a subtree.
|
|
58
|
+
- **`externalTargets`** — what to do with a selected edge that points outside the forest.
|
|
59
|
+
|
|
60
|
+
A cycle in the traversal relationship, or an entity reachable from two roots in one call, is an
|
|
61
|
+
error: an ambiguous forest has no single correct capture.
|
|
62
|
+
|
|
63
|
+
## External targets
|
|
64
|
+
|
|
65
|
+
An entity outside the forest is not captured, so an edge to it has nowhere to land.
|
|
66
|
+
|
|
67
|
+
- `"omit"` (default) — drop the edge, record how many targets were dropped.
|
|
68
|
+
- `"error"` — refuse to capture at all.
|
|
69
|
+
|
|
70
|
+
Use `"error"` when leaving data behind is a bug you want to hear about; use `"omit"` when the edge
|
|
71
|
+
is genuinely world-bound, like a tether to something the next sector does not own.
|
|
72
|
+
|
|
73
|
+
## Snapshot policies
|
|
74
|
+
|
|
75
|
+
Per component type, capture follows the type's `OnSnapshot` declaration. Ordinary plain-data
|
|
76
|
+
components need no declaration at all:
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
class Position {
|
|
80
|
+
x = 0;
|
|
81
|
+
y = 0;
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
`Position` has no policy, so it uses **`ClonePublic`**: a fresh instance with the source instance's
|
|
86
|
+
own enumerable fields copied over. That is the same shallow copy `set` performs, and it is exactly
|
|
87
|
+
as aliasing-sensitive — a component holding a nested object shares it until something copies it.
|
|
88
|
+
|
|
89
|
+
### `DontSnapshot`
|
|
90
|
+
|
|
91
|
+
For state that cannot exist in another world: engine handles, sockets, live queries and systems.
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
world.component(PhysicsEngine).snapshotPolicy(DontSnapshot);
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
A `DontSnapshot` type is skipped during capture. **Selecting** one explicitly is an error, so a
|
|
98
|
+
caller cannot silently lose state it asked for by name.
|
|
99
|
+
|
|
100
|
+
### `CustomSnapshot`
|
|
101
|
+
|
|
102
|
+
For a component that mixes declarative state with runtime state. The owner supplies both halves.
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
world.component(Body).snapshotPolicy(CustomSnapshot, {
|
|
106
|
+
// Keep the declarative fields, drop the engine handle.
|
|
107
|
+
capture: (body) => ({ type: body.type }),
|
|
108
|
+
// Build a destination-owned instance.
|
|
109
|
+
materialize: (data) => Object.assign(new Body(), data),
|
|
110
|
+
});
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
`capture` receives the live source instance and returns detached data. `materialize` receives that
|
|
114
|
+
data and returns a freshly constructed instance belonging to no other entity. The captured value
|
|
115
|
+
never has to be JSON — whatever `capture` returns is handed to `materialize` verbatim — but it must
|
|
116
|
+
not reference the source world, its entities, or anything the source still holds.
|
|
117
|
+
|
|
118
|
+
A policy is declared by the module that owns the component type. It may be declared, changed, or
|
|
119
|
+
revoked at any time — including after entities already carry the component — because **capture binds
|
|
120
|
+
the materializer it resolved**. Each captured record stores the materialize function that was in
|
|
121
|
+
effect at capture time, so a policy declared afterwards cannot retroactively change how an existing
|
|
122
|
+
snapshot restores. The same binding makes a snapshot safe to restore much later, after the owning
|
|
123
|
+
module has moved on.
|
|
124
|
+
|
|
125
|
+
`@vworlds/vecs-physics` installs policies for its own components, which is why a physics subtree
|
|
126
|
+
moves without any physics-specific code in the caller: `PhysicsEngine` is `DontSnapshot`, while
|
|
127
|
+
`Body`, shapes, and joint types capture their declarative fields and let the destination lifecycle
|
|
128
|
+
systems allocate fresh Box2D handles. Their custom policies deep-copy only primitives, arrays, and
|
|
129
|
+
plain `Object`/null-prototype records; a Box2D `b2Vec2` wrapper or other class instance fails at
|
|
130
|
+
capture with its component and field path. `JointFrame`, `JointSettings`, and joint feature
|
|
131
|
+
components use the default `ClonePublic` policy.
|
|
132
|
+
|
|
133
|
+
## Restore order
|
|
134
|
+
|
|
135
|
+
Restore is ordered, and the order is observable:
|
|
136
|
+
|
|
137
|
+
1. **Preflight** every captured class against the destination. A class that is not registered there,
|
|
138
|
+
or that the destination declares `DontSnapshot`, fails here — before any entity exists.
|
|
139
|
+
2. **Allocate** one destination entity per record, so every edge target exists before any edge is
|
|
140
|
+
added.
|
|
141
|
+
3. **Materialize** plain-data components.
|
|
142
|
+
4. **Rebuild edges**, traversal relationship first, then everything else. Hierarchy-sensitive
|
|
143
|
+
policies such as `OneOf` and `Traversable` therefore see a complete tree.
|
|
144
|
+
5. **Assign** each relationship's non-edge data, in capture order.
|
|
145
|
+
|
|
146
|
+
If any step throws, everything the attempt created is destroyed and the error propagates, so a
|
|
147
|
+
failed restore leaves the destination as it was found.
|
|
148
|
+
|
|
149
|
+
## Move and rollback
|
|
150
|
+
|
|
151
|
+
`moveSubtree` captures and preflights **before** it destroys anything, so a snapshot that cannot be
|
|
152
|
+
restored leaves the source world untouched.
|
|
153
|
+
|
|
154
|
+
Once the source is destroyed and the destination restore fails, everything the restore created is
|
|
155
|
+
destroyed, the source snapshot is restored back into the source world, and the original error is
|
|
156
|
+
rethrown. The rollback rebuilds equivalent state with freshly allocated ids, so `Entity` references
|
|
157
|
+
held before a failed move are stale afterwards.
|
|
158
|
+
|
|
159
|
+
Destroying a root applies its component `remove` hooks and its relationship cleanup policies, which
|
|
160
|
+
can reach outside the captured forest. That is what "the source is gone" means; select transferable
|
|
161
|
+
data accordingly.
|
|
162
|
+
|
|
163
|
+
## Deferred worlds
|
|
164
|
+
|
|
165
|
+
All three operations throw while either world is deferred. Cross-world scheduling is the
|
|
166
|
+
application's job — a transfer that interleaved with a half-applied frame of mutations would have no
|
|
167
|
+
well-defined outcome. Call them between frames.
|
|
168
|
+
|
|
169
|
+
## Inheritance
|
|
170
|
+
|
|
171
|
+
`IsA` edges are never captured. An inheritance edge and the values it materialized would both be
|
|
172
|
+
captured, and the destination would end up holding two disagreeing answers. Move an inheriting
|
|
173
|
+
entity by selecting its effective values instead. (The inheritance work that introduces `IsA` is
|
|
174
|
+
not in vecs yet; when it lands, it must keep declaring `IsA` as `DontSnapshot`.)
|
|
175
|
+
|
|
176
|
+
## What this is not
|
|
177
|
+
|
|
178
|
+
- **Not serialization.** A snapshot is an in-memory value, not a wire format, and not stable across
|
|
179
|
+
vecs versions. `@vworlds/vecs-wire` and `@vworlds/vecs-protocol` cover the network case.
|
|
180
|
+
- **Not a cross-world transaction.** Two live worlds are not locked together. Capture-then-restore
|
|
181
|
+
cannot make two worlds atomically consistent; it makes the _transfer_ recoverable.
|
package/docs/systems.md
CHANGED
|
@@ -40,7 +40,7 @@ the full query DSL (operator semantics in
|
|
|
40
40
|
.with({ all: [Position, { any: [Sprite, Container] }] }) // compound DSL
|
|
41
41
|
.with({ not: Invisible })
|
|
42
42
|
.without(Invisible) // shorthand for .with({ not: Invisible })
|
|
43
|
-
.with({ target: [FriendOf, Position] }) // relationship target has Position
|
|
43
|
+
.with({ target: [FriendOf, Position] }) // any relationship target has Position
|
|
44
44
|
.with({ parent: Body }) // ChildOf target has Body
|
|
45
45
|
```
|
|
46
46
|
|
|
@@ -89,6 +89,14 @@ Fires once when an entity starts matching the system:
|
|
|
89
89
|
.enter({ inject: [Position, Sprite] }, (e, [pos, sprite]) => sprite.setPosition(pos.x, pos.y))
|
|
90
90
|
```
|
|
91
91
|
|
|
92
|
+
The callback runs only if the entity is still a member when this system drains its inbox. If it
|
|
93
|
+
left or was destroyed first, vecs drops the queued `enter` and its paired `exit`; do not add a
|
|
94
|
+
`destroyed` guard to normal `enter` setup code. If it leaves and re-enters before draining, the
|
|
95
|
+
callback receives the ordered lifecycle transitions.
|
|
96
|
+
|
|
97
|
+
For systems, injected values and `Iter.src` are captured when entry is routed and replayed from that
|
|
98
|
+
snapshot when the system next drains its inbox.
|
|
99
|
+
|
|
92
100
|
### `.update(ComponentClass, callback)` / `.update(options, callback)`
|
|
93
101
|
|
|
94
102
|
Fires when the watched component is set or marked modified on a tracked entity.
|
|
@@ -121,9 +129,10 @@ registration throws instead of replacing the existing callback.
|
|
|
121
129
|
### `.exit(callback)` / `.exit(inject, callback)` / `.exit(options, callback)`
|
|
122
130
|
|
|
123
131
|
Fires when an entity stops matching (component removed, or entity destroyed). Directly injected
|
|
124
|
-
components are read from a **snapshot captured at exit time**, so
|
|
125
|
-
|
|
126
|
-
|
|
132
|
+
components and relationship projections are read from a **snapshot captured at exit time**, so
|
|
133
|
+
the same component and entity references remain available even if storage or edges change before
|
|
134
|
+
the system drains its inbox. Component objects are not cloned; later in-place changes to the same
|
|
135
|
+
object remain visible:
|
|
127
136
|
|
|
128
137
|
```ts
|
|
129
138
|
.exit([Sprite], (e, [sprite]) => sprite.destroy());
|
|
@@ -185,23 +194,55 @@ anything else resolves as `T | undefined` — guard it. (`enter`/`exit` injectio
|
|
|
185
194
|
direct components to be present and throws otherwise; list only components your predicate
|
|
186
195
|
guarantees.)
|
|
187
196
|
|
|
188
|
-
### Relationship injection
|
|
197
|
+
### Relationship injection
|
|
189
198
|
|
|
190
|
-
`{ target: [Rel,
|
|
191
|
-
|
|
192
|
-
|
|
199
|
+
`{ target: [Rel, projection] }` follows the entity's **first** relationship target and flattens the
|
|
200
|
+
projection into the callback tuple. Use a component class to inject one target-side component, use
|
|
201
|
+
`Entity` to inject the target itself, or use a tuple to inject several values in order:
|
|
193
202
|
|
|
194
203
|
```ts
|
|
195
|
-
.
|
|
196
|
-
//
|
|
204
|
+
.enter([{ target: [FriendOf, Entity] }], (source, [friend]) => {
|
|
205
|
+
// friend is source.target(FriendOf), one current target, or undefined.
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
.forEach([Body, { target: [FriendOf, Position] }], (source, [body, pos]) => {
|
|
209
|
+
// body is from source; pos is from source.target(FriendOf).
|
|
197
210
|
});
|
|
211
|
+
|
|
212
|
+
.forEach([{ target: [FriendOf, [Entity, Position, Velocity]] }], (source, [friend, pos, vel]) => {
|
|
213
|
+
// friend, pos, and vel all project from the same FriendOf target.
|
|
214
|
+
});
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Every target projection is optional. If the source lacks `Rel`, every projected slot is
|
|
218
|
+
`undefined`. If the relationship exists, an `Entity` slot is the target while each component slot
|
|
219
|
+
is `undefined` when that target lacks the component.
|
|
220
|
+
|
|
221
|
+
Use `{ targets: [Rel, projection] }` to project every target. Unlike singular injection, the whole
|
|
222
|
+
term occupies **one callback tuple slot** containing a readonly array of rows. Each target produces
|
|
223
|
+
one row; row ordering follows the current target snapshot but is not a public ordering contract. The
|
|
224
|
+
row contains one value for a scalar projection or several values for a tuple projection:
|
|
225
|
+
|
|
226
|
+
```ts
|
|
227
|
+
.forEach(
|
|
228
|
+
[{ targets: [FriendOf, [Entity, Position, Velocity]] }],
|
|
229
|
+
(source, [friends]) => {
|
|
230
|
+
// friends: ReadonlyArray<readonly [Entity, Position | undefined, Velocity | undefined]>
|
|
231
|
+
for (const [friend, position, velocity] of friends) { ... }
|
|
232
|
+
}
|
|
233
|
+
);
|
|
198
234
|
```
|
|
199
235
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
236
|
+
An absent relationship injects an empty array. The `Entity` value in each plural row is always
|
|
237
|
+
present; projected components remain optional. `target` and `targets` resolve live for `each`,
|
|
238
|
+
`forEach`, `update`, and `orderBy`. For deferred system `enter` and `exit` callbacks, vecs captures
|
|
239
|
+
their projected component and entity references plus sources when the event is routed. Inbox replay
|
|
240
|
+
retains those references even if storage or edges change; component objects are not cloned.
|
|
241
|
+
|
|
242
|
+
Inline projection tuples infer precise slot types. When reusing a projection tuple stored in a
|
|
243
|
+
variable, declare it `as const` to preserve each position's type.
|
|
203
244
|
|
|
204
|
-
`{ down: [Rel, [C1]] }` fans out: the callback fires once per
|
|
245
|
+
`{ down: [Rel, [C1]] }` fans out: the callback fires once per source targeting the visited entity
|
|
205
246
|
through `Rel`, with that child's components filling the slots (nullable). `down` is allowed only
|
|
206
247
|
in `each` and `forEach`, at most once per tuple, and entities with zero children produce no calls.
|
|
207
248
|
`down` iterates **all** children through the relationship, independent of any membership filter:
|
|
@@ -231,12 +272,14 @@ system.each(
|
|
|
231
272
|
);
|
|
232
273
|
```
|
|
233
274
|
|
|
234
|
-
`it.src` holds the source
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
275
|
+
`it.src` holds the source per tuple slot: the visited entity for direct components, the first
|
|
276
|
+
target for every flattened `target` slot, the readonly target-array snapshot for the single
|
|
277
|
+
`targets` slot, and the specific child for `down` slots. Event-time `enter` / `exit` snapshots also
|
|
278
|
+
capture `Iter.src`. When `Iter` is not requested, the non-cursor path has no cursor allocation;
|
|
279
|
+
dispatch happens once at setup time. `each`/`forEach`/`update`/`orderBy` reuse cursor and
|
|
280
|
+
tuple instances across the pass, so snapshot what you need before triggering further mutations
|
|
281
|
+
from inside a callback. Passing `{ cursor: Entity, ... }` is also accepted and means the default
|
|
282
|
+
entity-first signature.
|
|
240
283
|
|
|
241
284
|
## Ordering and tracking
|
|
242
285
|
|
|
@@ -321,7 +364,7 @@ While disabled, query membership is still maintained, so the tracked set is corr
|
|
|
321
364
|
| `phase(anchor)` | Place after a phase anchor (name or entity). Default `ON_UPDATE`. |
|
|
322
365
|
| `enter(cb)` / `enter(inject, cb)` | Fires once when an entity starts matching. |
|
|
323
366
|
| `update(C, cb)` / `update(options, cb)` | Fires on `set`/`modified` of `C` on a member; `options.onEnter: true` also fires on entry for present watched components. |
|
|
324
|
-
| `exit(cb)` / `exit(inject, cb)` | Fires when an entity stops matching;
|
|
367
|
+
| `exit(cb)` / `exit(inject, cb)` | Fires when an entity stops matching; injection retains references captured at exit time. |
|
|
325
368
|
| `each([C...], cb)` | Fires per tracked entity every tick. Implies `track`. One per system. |
|
|
326
369
|
| `run(cb)` | Fires once per tick, `(now, delta)`. |
|
|
327
370
|
| `interval(seconds)` / `rate(n[, source])` / `tickSource(source)` | Cadence control. |
|
package/docs/utilities.md
CHANGED
|
@@ -70,12 +70,16 @@ map.size; // 0
|
|
|
70
70
|
|
|
71
71
|
`IterableArrayMap` (`lib/vecs/src/util/array_map.ts`) is an iterable `Map<number, T>` substitute
|
|
72
72
|
backed by dense entries plus sparse indexes. Use it when callers need fast `forEach` or `values`
|
|
73
|
-
over present entries. Random access
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
73
|
+
over present entries. Random access reads the entry's dense position from a slot array indexed by
|
|
74
|
+
key, which holds only small integers so V8 keeps it on its fast path, then reads the value from a
|
|
75
|
+
dense value array. Iteration walks the dense key and value arrays directly.
|
|
76
|
+
|
|
77
|
+
Deletes keep the dense arrays dense by moving the last entry into the removed position, so
|
|
78
|
+
iteration is always a tight loop without sparse holes. Compared with `ArrayMap`, this costs extra
|
|
79
|
+
memory for the slot array and the dense key and value arrays. Because the dense arrays grow by
|
|
80
|
+
`push`, V8 cannot pretenure the entries, so a fresh process that creates tens of thousands of
|
|
81
|
+
long-lived entities in bulk and then destroys them can run slower than with per-entry
|
|
82
|
+
`[key, value]` tuples. The repository's `bench/README.md` has the measurements.
|
|
79
83
|
|
|
80
84
|
```ts
|
|
81
85
|
import { IterableArrayMap } from "@vworlds/vecs";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vworlds/vecs",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.46",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"prepack": "npm run build",
|
|
16
16
|
"build": "npm run clean && npm run compile",
|
|
17
17
|
"compile": "tsc -p tsconfig.build.json && cp \"./package.json\" ./dist/",
|
|
18
|
-
"bench:ecs": "NODE_OPTIONS=--max-old-space-size=32768 RUN_VECS_ECS_BENCH=1 vitest run tests/ecs.bench.test.ts --pool=forks --
|
|
18
|
+
"bench:ecs": "NODE_OPTIONS=--max-old-space-size=32768 RUN_VECS_ECS_BENCH=1 vitest run tests/ecs.bench.test.ts --pool=forks --maxWorkers=1 --reporter=default",
|
|
19
19
|
"test": "vitest run",
|
|
20
20
|
"test:watch": "vitest",
|
|
21
21
|
"lint": "eslint \"src/**/*.ts\" \"tests/**/*.ts\" --no-error-on-unmatched-pattern",
|
package/skills/vecs/SKILL.md
CHANGED
|
@@ -41,8 +41,9 @@ Before implementing, identify:
|
|
|
41
41
|
- Which component transition creates, updates, or removes related state or resources.
|
|
42
42
|
- Whether the work should react through `enter`, `update`, or `exit` rather than scan every frame.
|
|
43
43
|
- Which phase owns the work and what earlier or later phase it must order against.
|
|
44
|
-
- Whether
|
|
45
|
-
lifecycle better than imperative
|
|
44
|
+
- Whether a multi-target relationship, an `Exclusive` relationship, an explicitly created owned
|
|
45
|
+
target, a grouped query, or an attached resource expresses the lifecycle better than imperative
|
|
46
|
+
bookkeeping.
|
|
46
47
|
- Whether the feature belongs in a `Module` that owns registration, systems, phases, and policy.
|
|
47
48
|
|
|
48
49
|
Prefer the idioms in the installed design guide over generic ECS patterns. vecs execution details,
|
|
@@ -72,8 +73,8 @@ the documented pattern unless the user explicitly chooses otherwise.
|
|
|
72
73
|
hooks as directed by the guide.
|
|
73
74
|
3. Express cross-feature ordering with phases. Treat same-phase registration order as a local
|
|
74
75
|
implementation detail unless the installed docs explicitly make it part of the design.
|
|
75
|
-
4. Test lifecycle transitions, not only steady-state values: entry, mutation,
|
|
76
|
-
destruction, and same-tick visibility where relevant.
|
|
76
|
+
4. Test lifecycle transitions, not only steady-state values: entry, mutation, edge addition and
|
|
77
|
+
removal, exclusive edge replacement, destruction, and same-tick visibility where relevant.
|
|
77
78
|
5. Run the consuming project's typecheck and focused tests. Resolve API uncertainty from the
|
|
78
79
|
installed declarations and docs rather than guessing.
|
|
79
80
|
|