@vworlds/vecs 1.0.0

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 (58) hide show
  1. package/.claude/settings.json +12 -0
  2. package/.devcontainer/devcontainer.json +22 -0
  3. package/.github/workflows/publish.yml +32 -0
  4. package/README.md +464 -0
  5. package/dist/component.d.ts +135 -0
  6. package/dist/component.js +101 -0
  7. package/dist/component.js.map +1 -0
  8. package/dist/entity.d.ts +157 -0
  9. package/dist/entity.js +199 -0
  10. package/dist/entity.js.map +1 -0
  11. package/dist/index.d.ts +6 -0
  12. package/dist/index.js +4 -0
  13. package/dist/index.js.map +1 -0
  14. package/dist/package.json +25 -0
  15. package/dist/phase.d.ts +47 -0
  16. package/dist/phase.js +23 -0
  17. package/dist/phase.js.map +1 -0
  18. package/dist/system.d.ts +361 -0
  19. package/dist/system.js +396 -0
  20. package/dist/system.js.map +1 -0
  21. package/dist/util/array_map.d.ts +58 -0
  22. package/dist/util/array_map.js +84 -0
  23. package/dist/util/array_map.js.map +1 -0
  24. package/dist/util/bitset.d.ts +117 -0
  25. package/dist/util/bitset.js +177 -0
  26. package/dist/util/bitset.js.map +1 -0
  27. package/dist/util/events.d.ts +27 -0
  28. package/dist/util/events.js +43 -0
  29. package/dist/util/events.js.map +1 -0
  30. package/dist/util/ordered_set.d.ts +17 -0
  31. package/dist/util/ordered_set.js +69 -0
  32. package/dist/util/ordered_set.js.map +1 -0
  33. package/dist/world.d.ts +279 -0
  34. package/dist/world.js +453 -0
  35. package/dist/world.js.map +1 -0
  36. package/package.json +25 -0
  37. package/src/component.ts +180 -0
  38. package/src/entity.ts +276 -0
  39. package/src/index.ts +6 -0
  40. package/src/phase.ts +49 -0
  41. package/src/system.ts +693 -0
  42. package/src/util/array_map.ts +93 -0
  43. package/src/util/bitset.ts +199 -0
  44. package/src/util/events.ts +95 -0
  45. package/src/util/ordered_set.ts +82 -0
  46. package/src/world.ts +534 -0
  47. package/tests/_helpers.ts +30 -0
  48. package/tests/array_map.test.ts +68 -0
  49. package/tests/bitset.test.ts +127 -0
  50. package/tests/component.test.ts +104 -0
  51. package/tests/entity.test.ts +179 -0
  52. package/tests/events.test.ts +48 -0
  53. package/tests/ordered_set.test.ts +153 -0
  54. package/tests/setup.ts +6 -0
  55. package/tests/system.test.ts +800 -0
  56. package/tests/world.test.ts +174 -0
  57. package/tsconfig.json +21 -0
  58. package/vitest.config.ts +9 -0
@@ -0,0 +1,12 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Read(//home/jm/src/procura/.devcontainer/**)",
5
+ "Bash(git add *)",
6
+ "Bash(git commit -m ' *)",
7
+ "Bash(npx tsc *)",
8
+ "Bash(npx vitest *)",
9
+ "Bash(git push *)"
10
+ ]
11
+ }
12
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "vecs",
3
+ "image": "mcr.microsoft.com/devcontainers/typescript-node:1-20",
4
+ "remoteUser": "node",
5
+ "postCreateCommand": "yarn install",
6
+ "customizations": {
7
+ "vscode": {
8
+ "extensions": [
9
+ "dbaeumer.vscode-eslint",
10
+ "esbenp.prettier-vscode",
11
+ "anthropic.claude-code"
12
+ ],
13
+ "settings": {
14
+ "claudeCode.allowDangerouslySkipPermissions": true
15
+ }
16
+ }
17
+ },
18
+ "mounts": [
19
+ // Claude Code auth — mounts ~/.claude from the host so login carries over seamlessly
20
+ "source=${localEnv:HOME}/.claude,target=/home/node/.claude,type=bind,consistency=cached"
21
+ ]
22
+ }
@@ -0,0 +1,32 @@
1
+ name: Publish to npm
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - master
7
+
8
+ jobs:
9
+ publish:
10
+ runs-on: ubuntu-latest
11
+ permissions:
12
+ id-token: write
13
+ contents: read
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+
17
+ - uses: actions/setup-node@v4
18
+ with:
19
+ node-version: '20'
20
+ registry-url: 'https://registry.npmjs.org'
21
+
22
+ - name: Install dependencies
23
+ run: yarn install --frozen-lockfile
24
+
25
+ - name: Run tests
26
+ run: yarn test
27
+
28
+ - name: Build
29
+ run: yarn build
30
+
31
+ - name: Publish
32
+ run: yarn publish --access public
package/README.md ADDED
@@ -0,0 +1,464 @@
1
+ # vecs
2
+
3
+ A TypeScript Entity Component System (ECS) for real-time games and simulations.
4
+
5
+ `vecs` lets you model game state as **entities** (integer IDs) with **components** (typed data bags) attached to them. **Systems** declare which component combinations they care about and receive automatic callbacks when entities enter or leave their query, when component data changes, and on every tick. A **World** ties it all together and drives the update loop.
6
+
7
+ ## Install
8
+
9
+ ```
10
+ yarn add @vworlds/vecs
11
+ ```
12
+
13
+ ## Concepts
14
+
15
+ | Concept | What it is |
16
+ |---|---|
17
+ | **World** | Central container. Owns all entities, runs all systems. |
18
+ | **Component** | A plain data class. Extend `Component` and attach instances to entities. |
19
+ | **Entity** | An integer id with a set of components. Create via the world. |
20
+ | **System** | Reactive logic. Declare which components you need; get called when things change. |
21
+
22
+ ### Lifecycle in brief
23
+
24
+ ```
25
+ registerComponent() × N → system() × N → start() → progress() every frame
26
+ ```
27
+
28
+ After `start()`, no new components or systems can be registered.
29
+
30
+ ---
31
+
32
+ ## Example
33
+
34
+ The example below defines three components, two systems, a phase, and a hook, then runs a simple "move and despawn" loop.
35
+
36
+ ```ts
37
+ import { World, Component, IPhase } from "@vworlds/vecs";
38
+
39
+ // ─── Components ────────────────────────────────────────────────────────────
40
+
41
+ class Position extends Component {
42
+ x = 0;
43
+ y = 0;
44
+ }
45
+
46
+ class Velocity extends Component {
47
+ vx = 0;
48
+ vy = 0;
49
+ }
50
+
51
+ class Health extends Component {
52
+ hp = 100;
53
+ }
54
+
55
+ // ─── World setup ───────────────────────────────────────────────────────────
56
+
57
+ const world = new World();
58
+
59
+ world.registerComponent(Position);
60
+ world.registerComponent(Velocity);
61
+ world.registerComponent(Health);
62
+
63
+ // ─── Phases ────────────────────────────────────────────────────────────────
64
+
65
+ const update: IPhase = world.addPhase("update");
66
+ const cleanup: IPhase = world.addPhase("cleanup");
67
+
68
+ // ─── Systems ───────────────────────────────────────────────────────────────
69
+
70
+ // MoveSystem: runs every tick for entities that have both Position and Velocity.
71
+ world
72
+ .system("Move")
73
+ .phase(update)
74
+ .requires(Position, Velocity)
75
+ .enter([Position, Velocity], (e, [pos, vel]) => {
76
+ console.log(`entity ${e.eid} entered Move with pos=(${pos.x},${pos.y})`);
77
+ })
78
+ .update(Velocity, [Position], (vel, [pos]) => {
79
+ // Called whenever vel.modified() is queued.
80
+ pos.x += vel.vx;
81
+ pos.y += vel.vy;
82
+ pos.modified(); // propagate position change to other systems
83
+ })
84
+ .exit((e) => {
85
+ console.log(`entity ${e.eid} left Move`);
86
+ });
87
+
88
+ // HealthSystem: despawns entities whose HP drops to zero.
89
+ world
90
+ .system("Health")
91
+ .phase(cleanup)
92
+ .requires(Health)
93
+ .update(Health, (health) => {
94
+ if (health.hp <= 0) {
95
+ health.entity.destroy();
96
+ }
97
+ });
98
+
99
+ // ─── Hooks ─────────────────────────────────────────────────────────────────
100
+
101
+ // Hooks are a lightweight alternative to systems for side effects on a single
102
+ // component type — no per-entity query, just callbacks on add/remove/set.
103
+ world
104
+ .hook(Health)
105
+ .onAdd((h) => console.log(`entity ${h.entity.eid} spawned with hp=${h.hp}`))
106
+ .onRemove((h) => console.log(`entity ${h.entity.eid} died`));
107
+
108
+ // ─── Start ─────────────────────────────────────────────────────────────────
109
+
110
+ world.start(); // freeze registration, sort systems into phases
111
+
112
+ // ─── Create entities ───────────────────────────────────────────────────────
113
+
114
+ const bullet = world.createEntity();
115
+ const pos = bullet.add(Position);
116
+ pos.x = 0;
117
+ pos.y = 0;
118
+
119
+ const vel = bullet.add(Velocity);
120
+ vel.vx = 5;
121
+ vel.vy = 0;
122
+ vel.modified(); // first update: notify Move system
123
+
124
+ const hp = bullet.add(Health);
125
+ hp.hp = 3;
126
+ hp.modified();
127
+
128
+ // ─── Game loop ─────────────────────────────────────────────────────────────
129
+
130
+ let now = 0;
131
+ for (let tick = 0; tick < 5; tick++) {
132
+ now += 16;
133
+ world.progress(now, 16);
134
+ }
135
+ ```
136
+
137
+ ---
138
+
139
+ ## API Reference
140
+
141
+ ### World
142
+
143
+ The world owns everything. Create one per game session.
144
+
145
+ ```ts
146
+ const world = new World();
147
+ ```
148
+
149
+ #### Component registration
150
+
151
+ ```ts
152
+ // Auto-assigned type id (starts at 256 for "local" components):
153
+ world.registerComponent(Position);
154
+
155
+ // Explicit numeric type id (required when the id comes from a server):
156
+ world.registerComponent(Position, 1);
157
+
158
+ // With a display name different from the class name:
159
+ world.registerComponent(Position, "pos");
160
+
161
+ // Pre-register a name → id mapping before the class is available:
162
+ world.registerComponentType("Position", 1);
163
+ ```
164
+
165
+ After `world.start()` any further call to `registerComponent` throws.
166
+
167
+ #### Entity management
168
+
169
+ ```ts
170
+ // Locally-owned entity with an auto-incrementing id:
171
+ const e = world.createEntity();
172
+
173
+ // Server-assigned id; creates the entity if it doesn't exist yet:
174
+ const e = world.getOrCreateEntity(serverId, (newEntity) => {
175
+ tracked.add(newEntity);
176
+ });
177
+
178
+ // Look up by id (returns undefined if not found):
179
+ const e = world.entity(42);
180
+
181
+ // Destroy everything (e.g. on level reset):
182
+ world.clearAllEntities();
183
+
184
+ // Reserve a high id range for locally-created entities so they don't
185
+ // collide with server-assigned ids (call before world.start()):
186
+ world.setEntityIdRange(0x10000);
187
+ ```
188
+
189
+ #### Systems
190
+
191
+ ```ts
192
+ // Create, configure, and register a system in one chain:
193
+ world.system("MySystem")
194
+ .phase("update")
195
+ .requires(A, B)
196
+ .enter(...)
197
+ .update(...)
198
+ .exit(...);
199
+
200
+ world.start(); // must be called once, after all systems are set up
201
+ ```
202
+
203
+ #### Phases
204
+
205
+ ```ts
206
+ // Declare phases in the order they should run each frame:
207
+ const preUpdate = world.addPhase("preupdate");
208
+ const update = world.addPhase("update");
209
+ const send = world.addPhase("send");
210
+
211
+ // Each frame, run all phases in registration order:
212
+ world.progress(Date.now(), deltaMs);
213
+
214
+ // Or drive individual phases manually:
215
+ world.runPhase(preUpdate, Date.now(), deltaMs);
216
+ world.runPhase(update, Date.now(), deltaMs);
217
+ world.runPhase(send, Date.now(), deltaMs);
218
+ ```
219
+
220
+ Systems with no explicit phase go into a built-in `"update"` phase.
221
+
222
+ #### Hooks
223
+
224
+ A hook is a shorthand for reacting to a single component's lifecycle without writing a full system:
225
+
226
+ ```ts
227
+ world.hook(Sprite)
228
+ .onAdd((sprite) => sprite.initialize(scene))
229
+ .onRemove((sprite) => sprite.destroy())
230
+ .onSet((sprite) => sprite.syncToScene());
231
+ ```
232
+
233
+ `onSet` fires whenever `component.modified()` is called.
234
+ `onAdd` fires when the component is first attached to an entity.
235
+ `onRemove` fires when it is removed or the entity is destroyed.
236
+
237
+ ---
238
+
239
+ ### Component
240
+
241
+ Extend `Component` to define your data:
242
+
243
+ ```ts
244
+ class Position extends Component {
245
+ x = 0;
246
+ y = 0;
247
+ }
248
+
249
+ world.registerComponent(Position);
250
+
251
+ const pos = entity.add(Position);
252
+ pos.x = 100;
253
+ pos.modified(); // tell the world this component changed
254
+ ```
255
+
256
+ Every component instance exposes:
257
+
258
+ | Property / Method | Description |
259
+ |---|---|
260
+ | `entity` | The `Entity` this component belongs to. |
261
+ | `meta` | `ComponentMeta` — holds the type id, name, and bitset pointer. |
262
+ | `type` | Numeric type id (shorthand for `meta.type`). |
263
+ | `modified()` | Queue an `onSet` / `update` notification. Call after mutating fields. |
264
+
265
+ ---
266
+
267
+ ### Entity
268
+
269
+ ```ts
270
+ const e = world.createEntity();
271
+ ```
272
+
273
+ | Property / Method | Description |
274
+ |---|---|
275
+ | `eid` | Unique numeric entity id. |
276
+ | `world` | The `World` that owns this entity. |
277
+ | `add(Class)` | Attach a component; returns the typed instance. Idempotent. |
278
+ | `get(Class)` | Return the component instance, or `undefined` if not present. |
279
+ | `remove(Class)` | Detach a component (triggers `onRemove` hooks and `exit` callbacks). |
280
+ | `destroy()` | Remove all components and unregister the entity. Recurses to children. |
281
+ | `empty` | `true` when no components are attached. |
282
+ | `forEachComponent(cb)` | Iterate over all attached components. |
283
+ | `parent` | Parent entity in the scene hierarchy, or `undefined`. |
284
+ | `children` | `Set<Entity>` of direct children (lazy, created on first access). |
285
+ | `events` | Typed event emitter. Currently emits `"destroy"` before teardown. |
286
+ | `properties` | `Map<string, any>` free-form bag for module-level bookkeeping. |
287
+
288
+ #### Parent–child hierarchy
289
+
290
+ ```ts
291
+ child.parent = parent;
292
+ parent.children.add(child);
293
+
294
+ // Destroying a parent recursively destroys all children:
295
+ parent.destroy();
296
+ ```
297
+
298
+ Archetype queries that use `{ PARENT: ... }` are automatically re-evaluated when a parent's component set changes.
299
+
300
+ ---
301
+
302
+ ### System
303
+
304
+ Systems are created via `world.system(name)` and configured through a fluent builder API. All methods return `this` for chaining.
305
+
306
+ #### `.requires(...components)` and `.query(q)`
307
+
308
+ Declare which entities the system should track:
309
+
310
+ ```ts
311
+ // Entities that have both Position and Velocity:
312
+ .requires(Position, Velocity)
313
+
314
+ // Equivalent explicit query:
315
+ .query({ HAS: [Position, Velocity] })
316
+
317
+ // Entities that have a parent with Player AND Container:
318
+ .query({ PARENT: { AND: [Player, Container] } })
319
+
320
+ // Compound queries:
321
+ .query({ AND: [Position, { OR: [Sprite, Container] }] })
322
+ .query({ NOT: Invisible })
323
+ ```
324
+
325
+ **Query operators:**
326
+
327
+ | Operator | Meaning |
328
+ |---|---|
329
+ | `{ HAS: [A, B] }` | Entity has all of A and B |
330
+ | `{ HAS_ONLY: [A, B] }` | Entity has exactly A and B, nothing else |
331
+ | `{ AND: [q1, q2] }` | Both sub-queries must match |
332
+ | `{ OR: [q1, q2] }` | Either sub-query matches |
333
+ | `{ NOT: q }` | Sub-query must not match |
334
+ | `{ PARENT: q }` | Entity's parent matches q |
335
+ | An array `[A, B]` | Shorthand for `HAS: [A, B]` |
336
+
337
+ **Type inference:** `requires()` records the listed classes as a type parameter on the system. Callbacks in `.sort()`, `.each()`, and `.update()` inject then treat those components as non-nullable — no `!` needed. For complex `query()` expressions the type system cannot introspect, pass a second argument as an explicit hint:
338
+
339
+ ```ts
340
+ .query({ AND: [{ HAS: Position }, { HAS: Velocity }] }, [Position, Velocity])
341
+ .each([Position, Velocity], (e, [pos, vel]) => {
342
+ pos.x += vel.vx; // pos and vel are non-null
343
+ })
344
+ ```
345
+
346
+ #### `.phase(p)`
347
+
348
+ Assign the system to a named phase or an `IPhase` reference. Systems without a phase run in `"update"`.
349
+
350
+ ```ts
351
+ .phase("preupdate") // by name
352
+ .phase(myPhase) // by IPhase reference
353
+ ```
354
+
355
+ #### `.enter(callback)` / `.enter(inject, callback)`
356
+
357
+ Called once when an entity first matches the system's query.
358
+
359
+ ```ts
360
+ // No injection:
361
+ .enter((e) => { console.log("entity joined", e.eid); })
362
+
363
+ // With injection — component instances resolved from the entity:
364
+ .enter([Position, Sprite], (e, [pos, sprite]) => {
365
+ sprite.setPosition(pos.x, pos.y);
366
+ })
367
+
368
+ // Resolve from parent:
369
+ .enter([{ parent: Container }], (e, [container]) => {
370
+ container.add(e.get(Sprite)!.gameObject);
371
+ })
372
+ ```
373
+
374
+ #### `.exit(callback)` / `.exit(inject, callback)`
375
+
376
+ Called when an entity leaves the system (component removed or entity destroyed). Components removed in the same frame are still accessible in exit callbacks.
377
+
378
+ ```ts
379
+ .exit([Sprite], (e, [sprite]) => {
380
+ sprite.destroy();
381
+ })
382
+ ```
383
+
384
+ #### `.update(ComponentClass, callback)` / `.update(ComponentClass, inject, callback)`
385
+
386
+ Called when `component.modified()` is queued on a watched component of a tracked entity.
387
+
388
+ ```ts
389
+ // Simple — receives the modified component:
390
+ .update(Position, (pos) => {
391
+ renderer.setPosition(pos.x, pos.y);
392
+ })
393
+
394
+ // With injection — receives the modified component and extra components:
395
+ .update(Position, [Sprite], (pos, [sprite]) => {
396
+ sprite.sprite.setPosition(pos.x, pos.y);
397
+ })
398
+ ```
399
+
400
+ Injected components listed in `requires()` are non-nullable in the callback; any others are `Type | undefined`.
401
+
402
+ Calling `update` also adds that component type to the system's implicit `HAS` query (unless you called `query()` first).
403
+
404
+ #### `.each(components, callback)`
405
+
406
+ Called every tick for **every tracked entity**, unconditionally. Unlike `update` (which only fires when `component.modified()` is called), `each` fires regardless of whether the component was modified — use it for per-entity logic that must run on every frame.
407
+
408
+ The callback receives the entity and a tuple of resolved component instances. Components declared via `requires()` are guaranteed non-null; any others are `undefined` if the entity lacks them.
409
+
410
+ ```ts
411
+ .requires(Position, Velocity)
412
+ .each([Position, Velocity], (e, [pos, vel]) => {
413
+ pos.x += vel.vx; // non-null — both are in requires()
414
+ pos.y += vel.vy;
415
+ })
416
+ ```
417
+
418
+ `each` does not modify the system's query — define membership with `requires(...)` or `query(...)` as usual. Only one `each` may be registered per system; a second call throws.
419
+
420
+ #### `.sort(components, compare)`
421
+
422
+ Enable sorted entity tracking. Matched entities are stored in an ordered set whose insertion position is determined by `compare`, which receives a tuple of resolved component instances for each pair being ordered. Implies `.track()`.
423
+
424
+ Components declared via `requires()` are non-null in the compare callback.
425
+
426
+ ```ts
427
+ world.system("Render")
428
+ .requires(Position, Sprite)
429
+ .sort([Position], ([posA], [posB]) => posA.z - posB.z)
430
+ .each([Position, Sprite], (e, [pos, sprite]) => {
431
+ sprite.draw(pos.x, pos.y);
432
+ });
433
+ ```
434
+
435
+ Iterating `system.entities` after a phase run yields entities in the sorted order.
436
+
437
+ #### `.track()`
438
+
439
+ Enable entity tracking without an `each` callback — matched entities are exposed via `system.entities` as they enter and leave. `each` and `sort` imply `track` automatically; call this directly only when you need the set without a per-tick callback.
440
+
441
+ #### `.run(callback)`
442
+
443
+ Called every tick when the system's phase runs, regardless of entity state. Use this for polling, network I/O, timers, etc.
444
+
445
+ ```ts
446
+ .run((now, delta) => {
447
+ sendNetworkPacket(now);
448
+ })
449
+ ```
450
+
451
+ ---
452
+
453
+ ## Build & Test
454
+
455
+ ```
456
+ yarn build
457
+ yarn test
458
+ ```
459
+
460
+ ---
461
+
462
+ ## License
463
+
464
+ UNLICENSED
@@ -0,0 +1,135 @@
1
+ import { BitPtr, Bitset } from "./util/bitset.js";
2
+ import type { Entity } from "./entity.js";
3
+ import { type World } from "./world.js";
4
+ /**
5
+ * Lifecycle hook for a component type. Obtained via {@link World.hook}.
6
+ *
7
+ * Hooks let you react to component lifecycle events without building a full
8
+ * {@link System}. Each call returns the same `Hook` so the methods can be
9
+ * chained:
10
+ *
11
+ * ```ts
12
+ * world.hook(Sprite)
13
+ * .onAdd(c => initSprite(c))
14
+ * .onRemove(c => destroySprite(c))
15
+ * .onSet(c => syncSprite(c));
16
+ * ```
17
+ *
18
+ * Callbacks are invoked synchronously during {@link World.runPhase} when
19
+ * archetype changes are flushed.
20
+ *
21
+ * @typeParam C - The `Component` subclass this hook is bound to.
22
+ */
23
+ export interface Hook<C extends Component = Component> {
24
+ /**
25
+ * Register a callback that fires when a component of this type is added to
26
+ * an entity.
27
+ *
28
+ * @param handler - Receives the newly created component instance.
29
+ * @returns `this` for chaining.
30
+ */
31
+ onAdd(handler: (c: C) => void): Hook<C>;
32
+ /**
33
+ * Register a callback that fires when a component of this type is removed
34
+ * from an entity (including when the entity is destroyed).
35
+ *
36
+ * @param handler - Receives the component instance being removed.
37
+ * @returns `this` for chaining.
38
+ */
39
+ onRemove(handler: (c: C) => void): Hook<C>;
40
+ /**
41
+ * Register a callback that fires when {@link Component.modified} is called
42
+ * on a component of this type.
43
+ *
44
+ * @param handler - Receives the component instance that changed.
45
+ * @returns `this` for chaining.
46
+ */
47
+ onSet(handler: (c: C) => void): Hook<C>;
48
+ }
49
+ /**
50
+ * Internal bookkeeping record for a registered component class.
51
+ *
52
+ * Every component class that is passed to {@link World.registerComponent} gets
53
+ * a `ComponentMeta` that maps it to a numeric type id, a string name, and a
54
+ * pre-computed {@link BitPtr} used for fast archetype checks.
55
+ *
56
+ * `ComponentMeta` also implements {@link Hook}, so you can attach lifecycle
57
+ * callbacks directly on the meta object (as `World.hook()` returns it).
58
+ */
59
+ export declare class ComponentMeta implements Hook<Component> {
60
+ /** The component class constructor. */
61
+ readonly Class: typeof Component;
62
+ /** Numeric type id assigned at registration time. */
63
+ readonly type: number;
64
+ /** Human-readable name used in logs and serialization lookups. */
65
+ readonly componentName: string;
66
+ /** Pre-computed bit-pointer into the entity archetype {@link Bitset}. */
67
+ readonly bitPtr: BitPtr;
68
+ private onAddHandler;
69
+ private onRemoveHandler;
70
+ private onSetHandler;
71
+ constructor(Class: typeof Component, type: number, componentName: string);
72
+ /** @inheritdoc */
73
+ onAdd(handler: (c: Component) => void): ComponentMeta;
74
+ /** @inheritdoc */
75
+ onRemove(handler: (c: Component) => void): ComponentMeta;
76
+ /** @inheritdoc */
77
+ onSet(handler: (c: Component) => void): ComponentMeta;
78
+ }
79
+ /** A component class constructor or its numeric type id. */
80
+ export type ComponentClassOrType = number | typeof Component;
81
+ /** An array of component class constructors or type ids. */
82
+ export type ComponentClassArray = ComponentClassOrType[];
83
+ /**
84
+ * Base class for all ECS components.
85
+ *
86
+ * Extend this class to define data that can be attached to an {@link Entity}:
87
+ *
88
+ * ```ts
89
+ * class Position extends Component {
90
+ * x = 0;
91
+ * y = 0;
92
+ * }
93
+ *
94
+ * world.registerComponent(Position);
95
+ * const pos = entity.add(Position);
96
+ * pos.x = 100;
97
+ * pos.modified(); // notify watching systems
98
+ * ```
99
+ *
100
+ * A component instance is always bound to a single entity and is created by
101
+ * the world when {@link Entity.add} is called.
102
+ */
103
+ export declare class Component {
104
+ /** The entity this component belongs to. */
105
+ readonly entity: Entity;
106
+ /** Registration metadata (type id, name, bit-pointer). */
107
+ readonly meta: ComponentMeta;
108
+ private dirty;
109
+ constructor(
110
+ /** The entity this component belongs to. */
111
+ entity: Entity,
112
+ /** Registration metadata (type id, name, bit-pointer). */
113
+ meta: ComponentMeta);
114
+ /** Numeric type id — shorthand for `this.meta.type`. */
115
+ get type(): number;
116
+ /** Pre-computed bit-pointer — shorthand for `this.meta.bitPtr`. */
117
+ get bitPtr(): BitPtr;
118
+ /**
119
+ * Notify the world that this component's data has changed.
120
+ *
121
+ * Queues the component for delivery to all {@link System.update} callbacks
122
+ * that watch this component type. Call this after mutating the component's
123
+ * fields to ensure systems react to the new values.
124
+ */
125
+ modified(): void;
126
+ /** Returns the component's registered name, e.g. `"Position"`. */
127
+ toString(): string;
128
+ }
129
+ /**
130
+ * Compute a {@link Bitset} that has a bit set for every component class or
131
+ * type id in `classes`.
132
+ *
133
+ * @internal Used internally to build archetype masks for system queries.
134
+ */
135
+ export declare function calculateComponentBitmask(classes: ComponentClassArray, world: World): Bitset;