@vworlds/vecs 1.0.61 → 1.0.62

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.
@@ -0,0 +1,251 @@
1
+ # Component reflection
2
+
3
+ Declare the types and metadata of a component's members, read them back through `VecsStruct`, and
4
+ check or fix component data with `validate` and `normalize` hooks.
5
+
6
+ ## Concept
7
+
8
+ A component class is plain data, so the ECS cannot tell an `i32` from an `f64` or a meter from a
9
+ degree by looking at it. Reflection lets you declare it once and then use it anywhere: an editor or
10
+ inspector, a generated UI, a schema, or a check on data that arrives from outside.
11
+
12
+ - **Member types** name what each field holds: a scalar, a nested component class, or an array.
13
+ - **`VecsStruct`** is a core component holding the declared members. It sits on the class's
14
+ **component entity**, so you query it like any other component.
15
+ - **`validate` and `normalize` hooks** check a component instance or fix it in place. They run only
16
+ when you call `world.validate`, `world.isValid` or `world.normalize`. Nothing runs them on `set`,
17
+ so declaring metadata adds no cost per mutation.
18
+
19
+ A class without declarations registers and behaves exactly as before. Its component entity does not
20
+ carry a `VecsStruct`.
21
+
22
+ ## Declaring members
23
+
24
+ You can declare members in three equivalent forms. They all produce the same definition.
25
+
26
+ ```ts
27
+ import { description, range, type, unit, World } from "@vworlds/vecs";
28
+
29
+ // Decorators, read by world.component(Position).
30
+ class Position {
31
+ @type("i32") @range(-100, 100) @unit("m") @description("The X coordinate") x = 0;
32
+ @type("i32") @range(-100, 100) y = 0;
33
+ }
34
+
35
+ // Fluent, on the component entity.
36
+ class Velocity {
37
+ dx = 0;
38
+ dy = 0;
39
+ }
40
+
41
+ const world = new World();
42
+ world.component(Position);
43
+ world
44
+ .component(Velocity)
45
+ .member("dx", "f32")
46
+ .range(-10, 10)
47
+ .unit("m/s")
48
+ .member("dy", "f32")
49
+ .range(-10, 10)
50
+ .unit("m/s");
51
+ ```
52
+
53
+ The fluent form follows Flecs: `.member(name, type)` returns the `Component` itself, and each
54
+ modifier (`.range`, `.description`, `.unit`, `.values`) applies to the member most recently
55
+ declared with `.member` on that component entity, in that world. Because the chain stays on the
56
+ `Component`, it can carry on with `.on(…)` or any other component method:
57
+
58
+ ```ts
59
+ class Score {
60
+ value = 0;
61
+ }
62
+
63
+ const score = world
64
+ .component(Score)
65
+ .member("value", "u32")
66
+ .description("Points scored")
67
+ .on("set", (entity, s) => console.log(entity.eid, s.value));
68
+ score === world.component(Score); // true
69
+ ```
70
+
71
+ A modifier called before any `.member` on that component throws, telling you to call
72
+ `.member(name, type)` first.
73
+
74
+ You can also mix the forms. A fluent call that repeats a decorated member with the same type adds
75
+ the fields it is missing:
76
+
77
+ ```ts
78
+ class Heading {
79
+ @type("f32") @unit("deg") angle = 0;
80
+ }
81
+
82
+ world.component(Heading).member("angle", "f32").range(0, 360);
83
+ ```
84
+
85
+ Decorators work in both legacy (`experimentalDecorators`) and standard (TC39) mode. In standard mode
86
+ they store their declarations in the class's decorator metadata. Importing `@vworlds/vecs` defines
87
+ `Symbol.metadata` when the runtime lacks it, so `world.component(C)` reads the declarations without
88
+ constructing an instance.
89
+
90
+ Every member that carries metadata needs a type. A `@range` without a `@type` throws when you
91
+ register the class.
92
+
93
+ ## Reading the definition
94
+
95
+ ```ts
96
+ import { VecsStruct } from "@vworlds/vecs";
97
+
98
+ const struct = world.component(Position).get(VecsStruct);
99
+ struct?.members.x; // { type: "i32", range: [-100, 100], unit: "m", description: "The X coordinate" }
100
+ ```
101
+
102
+ Members are inherited along the class chain, however the base class declared them: a subclass of a
103
+ class with fluent declarations gets the same members as a subclass of a decorated one. They are
104
+ listed base class first, then in declaration order. A subclass may repeat an inherited member to add
105
+ fields, such as a `unit`, but not to change one.
106
+
107
+ The definition is **per class, for the whole process**:
108
+
109
+ - The first registration builds it. Later declarations, in any world, merge into it.
110
+ - A declaration that repeats the definition is a no-op. That makes it safe to run the same
111
+ registration code for every world.
112
+ - A declaration that contradicts it (a different type, range, unit, description or set of values)
113
+ throws, naming the class and the member. That includes a base class declaration contradicting a
114
+ subclass that repeated the member.
115
+ - Every world attaches the **same frozen instance**. `getMut(VecsStruct, …)` throws on any write.
116
+ A later declaration, on the class or a base class, is visible through that instance in every
117
+ world. The world that makes the declaration attaches or marks modified the `VecsStruct` of the
118
+ class and of its registered subclasses.
119
+
120
+ `VecsStruct` is registered in a world only when the world first attaches one, so worlds without
121
+ declarations keep their component ids. Look it up with `world.getEntity(VecsStruct)`, because
122
+ `entity.get(VecsStruct)` throws in a world that has never registered it. Like `ComponentDef`, it has
123
+ the `DontSnapshot` policy.
124
+
125
+ ## Validating and normalizing
126
+
127
+ ```ts
128
+ const position = Object.assign(new Position(), { x: 150.5, y: 3 });
129
+
130
+ world.validate(position); // ["x: 150.5 is not an integer"]
131
+ world.isValid(position); // false
132
+ world.normalize(position); // x becomes 100: truncated, then clamped
133
+ world.isValid(position); // true
134
+ ```
135
+
136
+ All three methods resolve the class from `component.constructor`. They throw when that class is not
137
+ registered in the world.
138
+
139
+ ### Default hooks
140
+
141
+ When a class declares ranges or integer types and has no explicit hook, the first call to
142
+ `world.validate` or `world.normalize` creates a default hook from the definition:
143
+
144
+ - `validate` checks each such member against its range and its type's bounds, and checks that
145
+ integer values are whole. For example, `u8` is 0–255. An `enum` member with `values` must hold
146
+ one of them. It returns every violation.
147
+ - `normalize` truncates integers, then clamps each such member to its range and its type's bounds.
148
+ `NaN` becomes `0` first. It leaves a value of the wrong JavaScript type, or an `enum` value outside
149
+ its `values`, unchanged, since neither has a sensible fix; `validate` reports both.
150
+ - `u64` and `i64` hold `bigint` values. `normalize` converts a finite `number` to a `bigint`.
151
+ - For an array member, the checks apply to each element.
152
+ - Floats are checked against their range only. A `string`, `bool` or nested member without a range
153
+ is not checked.
154
+
155
+ The default derives its checks from the current definition on every call, so it covers members
156
+ declared later, in any world or on a base class. It stays until you register a hook explicitly, and
157
+ the first explicit hook replaces it.
158
+
159
+ ### Explicit hooks
160
+
161
+ Register explicit hooks with `on`, like the lifecycle hooks:
162
+
163
+ ```ts
164
+ class Wallet {
165
+ coins = 0;
166
+ }
167
+
168
+ world
169
+ .component(Wallet)
170
+ .on("validate", (wallet) => (wallet.coins >= 0 ? true : "coins must not be negative"))
171
+ .on("normalize", (wallet) => {
172
+ wallet.coins = Math.max(0, Math.floor(wallet.coins));
173
+ });
174
+ ```
175
+
176
+ You can also declare them as static methods on the class (see [`vecs_` statics](#vecs_-statics)).
177
+
178
+ Several hooks for the same event run in registration order. `validate` returns the first failure.
179
+ `normalize` hooks chain, each one working on the result of the previous one.
180
+
181
+ ## `vecs_` statics
182
+
183
+ `world.component(C)` registers any static `vecs_<event>` method on `C` as a hook for `<event>`. These
184
+ hooks run ahead of the hooks you add with `.on(…)`, and they cover every public hook event:
185
+
186
+ | Static | Event | Signature |
187
+ | ------------------- | -------------- | ------------------------------------------- |
188
+ | `vecs_add` | `add` | `(entity, component) => void` |
189
+ | `vecs_set` | `set` | `(entity, component) => void` |
190
+ | `vecs_remove` | `remove` | `(entity, component) => void` |
191
+ | `vecs_targetAdd` | `targetAdd` | `(source, relationship, target) => void` |
192
+ | `vecs_targetRemove` | `targetRemove` | `(source, relationship, target) => void` |
193
+ | `vecs_validate` | `validate` | `(component) => true \| string \| string[]` |
194
+ | `vecs_normalize` | `normalize` | `(component) => void` |
195
+
196
+ ```ts
197
+ class Health {
198
+ @type("u16") hp = 100;
199
+
200
+ static vecs_normalize(health: Health): void {
201
+ health.hp = Math.min(Math.max(Math.round(health.hp), 0), 1000);
202
+ }
203
+ }
204
+ ```
205
+
206
+ Statics are called with the class as `this`, and component instances never store closures.
207
+ `vecs_targetAdd` and `vecs_targetRemove` require a [relationship](./relationships.md) class.
208
+ Registering one on any other class throws. Like any explicit hook, `vecs_validate` and
209
+ `vecs_normalize` replace the default.
210
+
211
+ ## Reference
212
+
213
+ ### Member types (`lib/vecs/src/reflection/member_types.ts`)
214
+
215
+ | Type | Values |
216
+ | -------------------------------------- | ---------------------------------------------------- |
217
+ | `bool` | `boolean` |
218
+ | `u8`, `i8`, `u16`, `i16`, `u32`, `i32` | Integer `number` within the type's bounds |
219
+ | `u64`, `i64` | `bigint` within the type's bounds |
220
+ | `enum` | `i32`, optionally restricted with `values([...])` |
221
+ | `f32`, `f64` | `number` |
222
+ | `string`, `bytes`, `any`, `entity` | String, `Uint8Array`, any value, an entity reference |
223
+ | A component class | A nested instance of that class |
224
+ | `[type]` | An array of the element type |
225
+
226
+ The names match the `@wireType` vocabulary of `@vworlds/vecs-wire`.
227
+
228
+ ### Declarations
229
+
230
+ | Decorator | Fluent | Member field |
231
+ | -------------------- | ------------------------------------ | ------------- |
232
+ | `@type(memberType)` | `component.member(name, memberType)` | `type` |
233
+ | `@range(min, max)` | `.range(min, max)` | `range` |
234
+ | `@description(text)` | `.description(text)` | `description` |
235
+ | `@unit(text)` | `.unit(text)` | `unit` |
236
+ | `@values([...])` | `.values([...])` | `values` |
237
+
238
+ Each fluent call returns the `Component`, and each modifier applies to the member last declared
239
+ with `.member` on it. A `range` needs a numeric type and is inclusive. For `u64` and `i64` it is stored as `bigint`.
240
+ `values` needs an `enum` type.
241
+
242
+ ### `World` reflection methods (`lib/vecs/src/world/world.components.ts`)
243
+
244
+ | Method | Returns |
245
+ | ---------------------- | -------------------------------------------------------- |
246
+ | `validate(component)` | `true`, or the violations: a message or a list of them. |
247
+ | `isValid(component)` | `validate(component) === true`. |
248
+ | `normalize(component)` | `component`, modified in place by the `normalize` hooks. |
249
+
250
+ See [Components — hooks](./components.md#hooks-component-and-relationship-edges) for the lifecycle
251
+ events and [ECS design diagrams](./diagrams.md#member-types) for drawing member types.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vworlds/vecs",
3
- "version": "1.0.61",
3
+ "version": "1.0.62",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -6,6 +6,7 @@
6
6
  "tests/diagnostic-json.test.ts",
7
7
  "tests/clock-inject.test.ts",
8
8
  "tests/world-scope.test.ts",
9
- "tests/system-introspection.test.ts"
9
+ "tests/system-introspection.test.ts",
10
+ "tests/reflection"
10
11
  ]
11
12
  }