@vworlds/vecs 1.0.12 → 1.0.14

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 CHANGED
@@ -15,7 +15,7 @@ yarn add @vworlds/vecs
15
15
  | Concept | What it is |
16
16
  | ------------------------ | ------------------------------------------------------------------------------- |
17
17
  | **World** | Central container. Owns every entity, query, system, and pipeline phase. |
18
- | **Component** | A plain data class. Extend `Component` and attach instances to entities. |
18
+ | **Component** | A registered plain data class with a no-argument constructor. |
19
19
  | **Entity** | A numeric id with a set of components. Created via the world. |
20
20
  | **Query** | A reactive, always-up-to-date set of entities matching a predicate. |
21
21
  | **System** | A `Query` with phase placement and per-tick logic (`update`, `each`, `run`). |
@@ -30,28 +30,28 @@ yarn add @vworlds/vecs
30
30
  registerComponent() × N → addPhase() / system() / query() × N → start() → progress() every frame
31
31
  ```
32
32
 
33
- After `start()`, component registration is disabled. Systems and queries can still be created — standalone queries backfill existing matched entities immediately.
33
+ Components must be registered before they are used as component classes. After `start()`, component registration is disabled. Systems and queries can still be created — standalone queries backfill existing matched entities immediately.
34
34
 
35
35
  ---
36
36
 
37
37
  ## Example
38
38
 
39
39
  ```ts
40
- import { World, Component, IPhase } from "@vworlds/vecs";
40
+ import { World, type IPhase } from "@vworlds/vecs";
41
41
 
42
42
  // ─── Components ────────────────────────────────────────────────────────────
43
43
 
44
- class Position extends Component {
44
+ class Position {
45
45
  x = 0;
46
46
  y = 0;
47
47
  }
48
48
 
49
- class Velocity extends Component {
49
+ class Velocity {
50
50
  vx = 0;
51
51
  vy = 0;
52
52
  }
53
53
 
54
- class Health extends Component {
54
+ class Health {
55
55
  hp = 100;
56
56
  }
57
57
 
@@ -78,7 +78,7 @@ world
78
78
  .each([Position, Velocity], (e, [pos, vel]) => {
79
79
  pos.x += vel.vx;
80
80
  pos.y += vel.vy;
81
- pos.modified(); // signal that Position changed so other systems react
81
+ e.modified(Position); // signal that Position changed so other systems react
82
82
  });
83
83
 
84
84
  // HealthSystem: despawns entities whose HP drops to zero.
@@ -86,9 +86,9 @@ world
86
86
  .system("Health")
87
87
  .phase(cleanup)
88
88
  .requires(Health)
89
- .update(Health, (health) => {
89
+ .update(Health, (entity, health) => {
90
90
  if (health.hp <= 0) {
91
- health.entity.destroy();
91
+ entity.destroy();
92
92
  }
93
93
  });
94
94
 
@@ -96,8 +96,8 @@ world
96
96
 
97
97
  world
98
98
  .hook(Health)
99
- .onAdd((h) => console.log(`entity ${h.entity.eid} spawned with hp=${h.hp}`))
100
- .onRemove((h) => console.log(`entity ${h.entity.eid} died`));
99
+ .onAdd((entity, h) => console.log(`entity ${entity.eid} spawned with hp=${h.hp}`))
100
+ .onRemove((entity) => console.log(`entity ${entity.eid} died`));
101
101
 
102
102
  // ─── Start ─────────────────────────────────────────────────────────────────
103
103
 
@@ -120,11 +120,12 @@ for (let tick = 0; tick < 5; tick++) {
120
120
 
121
121
  ## Deferred mode
122
122
 
123
- Inside a system body, a `Query.forEach`, or any `world.defer(...)` block, the world is in **deferred mode**: entity mutations (`add` / `set` / `remove` / `destroy` / `setParent` / `modified`) are queued instead of applied inline. The queue drains on the boundary that opened the deferred scope.
123
+ Inside a system body, a `Query.forEach`, or any `world.defer(...)` block, the world is in **deferred mode**: entity mutations (`add` / `attach` / `set` / `remove` / `destroy` / `setParent` / `modified`) are queued instead of applied inline. The queue drains on the boundary that opened the deferred scope.
124
124
 
125
125
  Concretely, while deferred:
126
126
 
127
127
  - `entity.get(C)` returns `undefined` after `entity.add(C)` (no instance has been created yet).
128
+ - `entity.get(C)` returns `undefined` after `entity.attach(instance)` if C was absent.
128
129
  - `entity.get(C)` returns the **previous** value after `entity.set(C, props)`.
129
130
  - `entity.get(C)` still returns the component after `entity.remove(C)`.
130
131
 
@@ -144,9 +145,16 @@ const world = new World();
144
145
 
145
146
  #### Component registration
146
147
 
148
+ Components are ordinary classes. They do not inherit from a vecs base class, and vecs constructs them with `new ComponentClass()`, so constructors should take no parameters. Register every component class before using it in `add`, `set`, `get`, `requires`, `query`, `filter`, `hook`, or `setExclusiveComponents`.
149
+
147
150
  ```ts
151
+ class Position {
152
+ x = 0;
153
+ y = 0;
154
+ }
155
+
148
156
  // Auto-assigned type id (≥ 256 for "local" components):
149
- world.registerComponent(Position);
157
+ const positionMeta = world.registerComponent(Position);
150
158
 
151
159
  // Explicit numeric type id (e.g. server-assigned):
152
160
  world.registerComponent(Position, 1);
@@ -158,7 +166,9 @@ world.registerComponent(Position, "pos");
158
166
  world.registerComponentType("Position", 1);
159
167
  ```
160
168
 
161
- After `world.start()` (or `world.disableComponentRegistration()`) any further call to `registerComponent` throws.
169
+ `registerComponent` returns the `ComponentMeta` for the class. Internally, vecs stores that metadata on the component class under a hidden world-specific key, so the same class can be registered independently in multiple worlds. Numeric type lookup still goes through the world's type table.
170
+
171
+ After `world.start()` (or `world.disableComponentRegistration()`) any further call to `registerComponent` throws. There is no automatic component registration; using an unregistered component class as a component is an error.
162
172
 
163
173
  #### Exclusive component groups
164
174
 
@@ -199,12 +209,12 @@ world.clearAllEntities();
199
209
  ```ts
200
210
  world
201
211
  .hook(Sprite)
202
- .onAdd((sprite) => sprite.initialize(scene))
203
- .onRemove((sprite) => sprite.destroy())
204
- .onSet((sprite) => sprite.syncToScene());
212
+ .onAdd((entity, sprite) => sprite.initialize(scene, entity))
213
+ .onRemove((entity, sprite) => sprite.destroy(scene, entity))
214
+ .onSet((entity, sprite) => sprite.syncToScene(entity));
205
215
  ```
206
216
 
207
- `onAdd` fires when the component is first attached. `onRemove` fires when it is removed (or the entity is destroyed). `onSet` fires whenever `component.modified()` (or `entity.modified(c)`) is called, and when `entity.set(C, props)` is applied to an entity that already has the component.
217
+ `onAdd` fires when the component is first attached. `onRemove` fires when it is removed (or the entity is destroyed). `onSet` fires whenever `entity.modified(C)` is called, when `entity.set(C, props)` applies data, and when `entity.attach(instance)` stores an existing instance. Hook callbacks receive the owning entity because component instances do not carry entity references.
208
218
 
209
219
  #### Phases
210
220
 
@@ -217,7 +227,7 @@ const send = world.addPhase("send");
217
227
  world.progress(now, delta);
218
228
 
219
229
  // ...or run individual phases manually:
220
- world.beginFrame(now, delta);
230
+ world.beginFrame(delta);
221
231
  try {
222
232
  world.runPhase(preUpdate, now, delta);
223
233
  world.runPhase(update, now, delta);
@@ -358,8 +368,10 @@ world.endDefer();
358
368
 
359
369
  ### `Component`
360
370
 
371
+ Components are plain classes. vecs does not provide a runtime base class and does not attach `entity`, `meta`, `type`, `bitPtr`, or `modified()` to component instances.
372
+
361
373
  ```ts
362
- class Position extends Component {
374
+ class Position {
363
375
  x = 0;
364
376
  y = 0;
365
377
  }
@@ -369,20 +381,26 @@ world.registerComponent(Position);
369
381
  entity.add(Position);
370
382
  const pos = entity.get(Position)!;
371
383
  pos.x = 100;
372
- pos.modified(); // tell the world this component changed
384
+ entity.modified(Position); // tell the world this component changed
373
385
 
374
386
  // Equivalent — set assigns props and fires onSet automatically:
375
387
  entity.set(Position, { x: 100 });
388
+
389
+ // Store an existing instance directly:
390
+ const shared = new Position();
391
+ entity.attach(shared);
392
+ entity.get(Position) === shared; // true
376
393
  ```
377
394
 
378
- | Property / Method | Description |
379
- | ----------------- | --------------------------------------------------------------------- |
380
- | `entity` | The `Entity` this component belongs to. |
381
- | `meta` | `ComponentMeta` type id, display name, and bit-pointer. |
382
- | `type` | Numeric type id (shorthand for `meta.type`). |
383
- | `bitPtr` | `BitPtr` (shorthand for `meta.bitPtr`). |
384
- | `modified()` | Queue an `onSet` / `update` notification. Call after mutating fields. |
385
- | `toString()` | Returns the registered component name. |
395
+ | Rule | Description |
396
+ | ------------------------- | -------------------------------------------------------------------------------------------------------------- |
397
+ | Plain class | Components should be ordinary classes with field initializers and methods as needed. |
398
+ | No-arg construction | vecs calls `new ComponentClass()`, so constructors should be omitted or take no parameters. |
399
+ | Explicit registration | Call `world.registerComponent(C)` before using the class as a component. |
400
+ | Shared instances possible | `entity.attach(instance)` stores the exact passed object; code should use the entity passed by vecs callbacks. |
401
+ | Manual dirty marking | After mutating fields directly, call `entity.modified(C)` to notify hooks, queries, and systems. |
402
+
403
+ Use `world.getComponentMeta(C)` or `world.getComponentType(C)` when you need metadata such as the numeric type id or component name. Metadata is world-specific.
386
404
 
387
405
  ---
388
406
 
@@ -390,34 +408,37 @@ entity.set(Position, { x: 100 });
390
408
 
391
409
  Created via `world.entity()` (auto-assigned id) or `world.getOrCreateEntity(id, ...)` (caller-supplied id).
392
410
 
393
- | Property / Method | Description |
394
- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
395
- | `eid` | Unique numeric entity id. |
396
- | `world` | The `World` that owns this entity. |
397
- | `componentBitmask` | `Bitset` of component type ids attached to this entity. Used by archetype matching. |
398
- | `properties` | `Map<string, any>` free-form bag for module-level bookkeeping. |
399
- | `add(Class)` | Attach a component (idempotent). Returns the entity for chaining. |
400
- | `set(Class, props)` | Attach a component and assign `props`; fires `onSet`. Returns the entity for chaining. |
401
- | `modified(component)` | Queue an `onSet` / `update` notification. Returns the entity for chaining. |
402
- | `get(Class)` | Return the component instance, or `undefined`. |
403
- | `remove(Class)` | Detach a component (fires `onRemove` and `exit`). |
404
- | `destroy()` | Remove all components, unregister from the world, recurse into children. |
405
- | `components` | `ReadonlyArrayMap<Component>` read-only view of attached components keyed by type id. Supports `forEach`, `get`, `has`, and `size`. |
406
- | `empty` | `true` when no components are attached. |
407
- | `parent` | Parent entity, or `undefined` for a root entity. |
408
- | `children` | `ReadonlySet<Entity>` of direct children (lazy). |
409
- | `setParent(p)` | Reparent the entity. `undefined` makes it a root entity. Throws on cycles. |
410
- | `events` | Typed event emitter. Currently emits `"destroy"` just before teardown. |
411
- | `toString()` | Returns `"EntityN"`. |
412
-
413
- `entity.modified(c)` is equivalent to `c.modified()` but returns the entity so it can chain:
411
+ | Property / Method | Description |
412
+ | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
413
+ | `eid` | Unique numeric entity id. |
414
+ | `world` | The `World` that owns this entity. |
415
+ | `componentBitmask` | `Bitset` of component type ids attached to this entity. Used by archetype matching. |
416
+ | `properties` | `Map<string, any>` free-form bag for module-level bookkeeping. |
417
+ | `add(Class)` | Attach a component (idempotent). Returns the entity for chaining. |
418
+ | `attach(instance)` | Attach an existing registered component instance directly; replaces any previous instance for that component class and fires `onSet`. |
419
+ | `set(Class, props)` | Attach a component and assign `props`; fires `onSet`. Returns the entity for chaining. |
420
+ | `modified(Class)` | Queue an `onSet` / `update` notification for a component class or numeric type id. Returns the entity for chaining. |
421
+ | `get(Class)` | Return the component instance, or `undefined`. |
422
+ | `remove(Class)` | Detach a component (fires `onRemove` and `exit`). |
423
+ | `destroy()` | Remove all components, unregister from the world, recurse into children. |
424
+ | `components` | `ReadonlyArrayMap<Component>` read-only view of attached components keyed by type id. Supports `forEach`, `get`, `has`, and `size`. |
425
+ | `empty` | `true` when no components are attached. |
426
+ | `parent` | Parent entity, or `undefined` for a root entity. |
427
+ | `children` | `ReadonlySet<Entity>` of direct children (lazy). |
428
+ | `setParent(p)` | Reparent the entity. `undefined` makes it a root entity. Throws on cycles. |
429
+ | `events` | Typed event emitter. Currently emits `"destroy"` just before teardown. |
430
+ | `toString()` | Returns `"EntityN"`. |
431
+
432
+ Call `entity.modified(C)` after mutating a component directly. Repeated calls for the same component type are coalesced while the world is deferred:
414
433
 
415
434
  ```ts
416
435
  const vel = entity.get(Velocity)!;
417
436
  vel.vx += accel;
418
- entity.modified(vel); // chainable
437
+ entity.modified(Velocity); // chainable
419
438
  ```
420
439
 
440
+ Use `entity.attach(instance)` when component ownership is intentionally shared with caller code or another object graph. The instance constructor must be registered in the entity's world; unregistered instances throw. If the component belongs to an exclusive component group, conflicting components are removed before the instance is stored.
441
+
421
442
  #### Parent / child hierarchy
422
443
 
423
444
  ```ts
@@ -462,6 +483,8 @@ Declare which entities the system tracks.
462
483
  | A single class / id | Shorthand for `{ HAS: [C] }` |
463
484
  | A predicate function | Custom membership logic |
464
485
 
486
+ Class-valued query terms are recognized as components by looking up registered metadata for the current world. Register component classes before using them in a `QueryDSL`; an unregistered class is treated like a predicate function and will fail if it cannot be called that way.
487
+
465
488
  **Type inference.** `requires()` records the listed classes as a type parameter `R` on the system. Callbacks in `.sort()`, `.each()`, and `.update()` injection treat those components as non-nullable — no `!` needed. For complex `query()` expressions the type system can't introspect, supply a `_guaranteed` second argument:
466
489
 
467
490
  ```ts
@@ -506,12 +529,12 @@ Fires when an entity leaves the system (component removed or entity destroyed).
506
529
 
507
530
  #### `.update(ComponentClass, callback)` / `.update(ComponentClass, inject, callback)`
508
531
 
509
- Fires when `component.modified()` is called for the watched component on a tracked entity.
532
+ Fires when `entity.modified(ComponentClass)` is called for the watched component on a tracked entity. It also fires for `entity.set(C, props)` on an already-attached component and for initial watched components when an entity enters the query. The callback receives the entity first because component instances do not carry owner references.
510
533
 
511
534
  ```ts
512
- .update(Position, (pos) => renderer.setPosition(pos.x, pos.y));
535
+ .update(Position, (entity, pos) => renderer.setPosition(entity.eid, pos.x, pos.y));
513
536
 
514
- .update(Position, [Sprite], (pos, [sprite]) => {
537
+ .update(Position, [Sprite], (entity, pos, [sprite]) => {
515
538
  sprite.sprite.setPosition(pos.x, pos.y);
516
539
  });
517
540
  ```
@@ -537,7 +560,7 @@ Store matched entities in a custom order determined by `compare`. Implies `.trac
537
560
  world
538
561
  .system("Render")
539
562
  .requires(Position, Sprite)
540
- .sort([Position], ([posA], [posB]) => posA.z - posB.z)
563
+ .sort([Position], (_entityA, [posA], _entityB, [posB]) => posA.z - posB.z)
541
564
  .each([Position, Sprite], (e, [pos, sprite]) => sprite.draw(pos.x, pos.y));
542
565
  ```
543
566
 
@@ -585,7 +608,7 @@ Both methods return `this` for chaining and are idempotent (calling `disable()`
585
608
  const projectiles = world
586
609
  .query("Projectiles")
587
610
  .requires(Position, Velocity)
588
- .sort([Position], ([a], [b]) => a.z - b.z)
611
+ .sort([Position], (_entityA, [a], _entityB, [b]) => a.z - b.z)
589
612
  .enter([Position], (e, [pos]) => {
590
613
  pos.x = spawnX;
591
614
  });
@@ -596,20 +619,20 @@ projectiles.forEach((e) => { ... });
596
619
  console.log(projectiles.entities.size, "active projectiles");
597
620
  ```
598
621
 
599
- | Method | Description |
600
- | ------------------------------------------------------- | ------------------------------------------------------------------------ |
601
- | `.requires(...components)` | Set the membership predicate to `HAS(...components)` and start tracking. |
602
- | `.query(expr, _guaranteed?)` | Set the membership predicate using a `QueryDSL` expression. |
603
- | `.enter(callback)` / `.enter(inject, callback)` | Fires when an entity joins the query. |
604
- | `.exit(callback)` / `.exit(inject, callback)` | Fires when an entity leaves the query. |
605
- | `.update(C, callback)` / `.update(C, inject, callback)` | Fires when `C` is modified on a tracked entity. |
606
- | `.sort(components, compare)` | Store matched entities in sorted order. |
607
- | `.track()` | Enable tracking. Backfills when called after `start()`. |
608
- | `.belongs(e)` | Returns `true` if the entity satisfies the predicate. |
609
- | `.forEach(callback)` | Iterate currently tracked entities. |
610
- | `.forEach(components, callback)` | Iterate with component injection. |
611
- | `.entities` | `ReadonlySet<Entity>` of currently tracked entities. |
612
- | `.destroy()` | Remove the query from the world and from every entity (no exit fires). |
622
+ | Method | Description |
623
+ | ------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
624
+ | `.requires(...components)` | Set the membership predicate to `HAS(...components)` and start tracking. |
625
+ | `.query(expr, _guaranteed?)` | Set the membership predicate using a `QueryDSL` expression. |
626
+ | `.enter(callback)` / `.enter(inject, callback)` | Fires when an entity joins the query. |
627
+ | `.exit(callback)` / `.exit(inject, callback)` | Fires when an entity leaves the query. |
628
+ | `.update(C, callback)` / `.update(C, inject, callback)` | Fires when `C` is modified on a tracked entity. Callback receives `(entity, component, injected?)`. |
629
+ | `.sort(components, compare)` | Store matched entities in sorted order. Comparator receives `(entityA, tupleA, entityB, tupleB)`. |
630
+ | `.track()` | Enable tracking. Backfills when called after `start()`. |
631
+ | `.belongs(e)` | Returns `true` if the entity satisfies the predicate. |
632
+ | `.forEach(callback)` | Iterate currently tracked entities. |
633
+ | `.forEach(components, callback)` | Iterate with component injection. |
634
+ | `.entities` | `ReadonlySet<Entity>` of currently tracked entities. |
635
+ | `.destroy()` | Remove the query from the world and from every entity (no exit fires). |
613
636
 
614
637
  #### `.destroy()` semantics
615
638
 
@@ -663,6 +686,7 @@ A compact, growable set of non-negative integers backed by 32-bit words. Used in
663
686
  | `add(n)` | Set bit `n`. |
664
687
  | `addBit(bptr)` | Set the bit at a pre-computed `BitPtr`. |
665
688
  | `delete(n)` | Clear bit `n`. Trims trailing zero words. |
689
+ | `clear()` | Remove every set bit. |
666
690
  | `has(n)` | Returns `true` if bit `n` is set. |
667
691
  | `hasBit(bptr)` | Fast check via a pre-computed `BitPtr`. |
668
692
  | `equal(other)` | Returns `true` when both bitsets have the same bits set. |
@@ -671,7 +695,7 @@ A compact, growable set of non-negative integers backed by 32-bit words. Used in
671
695
  | `indices()` | Return all set bit indices as a `number[]`. |
672
696
 
673
697
  ```ts
674
- class Tags extends Component {
698
+ class Tags {
675
699
  tags = new Bitset();
676
700
  }
677
701
 
@@ -1,5 +1,11 @@
1
1
  import { BitPtr } from "./util/bitset.js";
2
2
  import type { Entity } from "./entity.js";
3
+ /** A component instance. Components are plain objects created with a no-arg constructor. */
4
+ export type Component = object;
5
+ /** A component class constructor. */
6
+ export type ComponentClass<T extends Component = Component> = new () => T;
7
+ /** A component class constructor or its numeric type id. */
8
+ export type ComponentClassOrType = number | ComponentClass;
3
9
  /**
4
10
  * Lifecycle hook for a registered component class. Obtained via
5
11
  * {@link World.hook}.
@@ -11,16 +17,16 @@ import type { Entity } from "./entity.js";
11
17
  *
12
18
  * ```ts
13
19
  * world.hook(Sprite)
14
- * .onAdd(c => initSprite(c))
15
- * .onRemove(c => destroySprite(c))
16
- * .onSet(c => syncSprite(c));
20
+ * .onAdd((e, c) => initSprite(e, c))
21
+ * .onRemove((e, c) => destroySprite(e, c))
22
+ * .onSet((e, c) => syncSprite(e, c));
17
23
  * ```
18
24
  *
19
25
  * Callbacks fire synchronously when the corresponding entity command is
20
26
  * applied: inline outside deferred mode, or while the world drains its command
21
27
  * queue inside a system / `forEach` / `defer` block.
22
28
  *
23
- * @typeParam C - Component subclass this hook is bound to.
29
+ * @typeParam C - Component class this hook is bound to.
24
30
  */
25
31
  export interface Hook<C extends Component = Component> {
26
32
  /**
@@ -28,29 +34,29 @@ export interface Hook<C extends Component = Component> {
28
34
  * to an entity (`entity.add(C)` or `entity.set(C, ...)` on an entity that
29
35
  * does not yet have the component).
30
36
  *
31
- * @param handler - Receives the freshly created component instance.
37
+ * @param handler - Receives the entity and freshly created component instance.
32
38
  * @returns This hook, for chaining.
33
39
  */
34
- onAdd(handler: (c: C) => void): Hook<C>;
40
+ onAdd(handler: (entity: Entity, c: C) => void): Hook<C>;
35
41
  /**
36
42
  * Register a handler invoked when a component of this type is removed from
37
43
  * an entity (explicit `entity.remove(C)` or implicit removal during
38
44
  * `entity.destroy()`).
39
45
  *
40
- * @param handler - Receives the component instance that was removed.
46
+ * @param handler - Receives the entity and component instance that was removed.
41
47
  * @returns This hook, for chaining.
42
48
  */
43
- onRemove(handler: (c: C) => void): Hook<C>;
49
+ onRemove(handler: (entity: Entity, c: C) => void): Hook<C>;
44
50
  /**
45
51
  * Register a handler invoked when a component's data has been marked as
46
- * changed (`component.modified()` or `entity.modified(c)`), and when
52
+ * changed (`entity.modified(C)`), and when
47
53
  * `entity.set(C, props)` is called on an entity that already has the
48
54
  * component.
49
55
  *
50
- * @param handler - Receives the component instance whose data changed.
56
+ * @param handler - Receives the entity and component instance whose data changed.
51
57
  * @returns This hook, for chaining.
52
58
  */
53
- onSet(handler: (c: C) => void): Hook<C>;
59
+ onSet(handler: (entity: Entity, c: C) => void): Hook<C>;
54
60
  }
55
61
  /**
56
62
  * Bookkeeping record produced for each component class registered via
@@ -63,72 +69,18 @@ export interface Hook<C extends Component = Component> {
63
69
  */
64
70
  export declare class ComponentMeta implements Hook<Component> {
65
71
  /** The component class constructor this meta represents. */
66
- readonly Class: typeof Component;
72
+ readonly Class: ComponentClass;
67
73
  /** Numeric type id assigned at registration time. */
68
74
  readonly type: number;
69
75
  /** Human-readable name used in logs and serialization lookups. */
70
76
  readonly componentName: string;
71
77
  /** Pre-computed bit-pointer into the entity archetype {@link Bitset}. */
72
78
  readonly bitPtr: BitPtr;
73
- /**
74
- * Type ids of components that cannot coexist with this one on the same
75
- * entity. Set via {@link World.setExclusiveComponents}; `undefined` means
76
- * no restriction.
77
- */
78
- exclusive: number[] | undefined;
79
- constructor(Class: typeof Component, type: number, componentName: string);
79
+ constructor(Class: ComponentClass, type: number, componentName: string);
80
80
  /** @inheritdoc */
81
- onAdd(handler: (c: Component) => void): ComponentMeta;
81
+ onAdd(handler: (entity: Entity, c: Component) => void): ComponentMeta;
82
82
  /** @inheritdoc */
83
- onRemove(handler: (c: Component) => void): ComponentMeta;
83
+ onRemove(handler: (entity: Entity, c: Component) => void): ComponentMeta;
84
84
  /** @inheritdoc */
85
- onSet(handler: (c: Component) => void): ComponentMeta;
86
- }
87
- /** A component class constructor or its numeric type id. */
88
- export type ComponentClassOrType = number | typeof Component;
89
- /**
90
- * Base class for all ECS components.
91
- *
92
- * Subclass `Component` to declare data that can be attached to an
93
- * {@link Entity}. Instances are constructed by the world when
94
- * {@link Entity.add} or {@link Entity.set} runs — never instantiate manually.
95
- *
96
- * ```ts
97
- * class Position extends Component {
98
- * x = 0;
99
- * y = 0;
100
- * }
101
- *
102
- * world.registerComponent(Position);
103
- * entity.set(Position, { x: 100 });
104
- * ```
105
- *
106
- * Each instance is bound to a single entity via {@link entity}; that link is
107
- * permanent for the component's lifetime.
108
- */
109
- export declare class Component {
110
- /** The entity this component belongs to. */
111
- readonly entity: Entity;
112
- /** Registration metadata (type id, display name, bit-pointer). */
113
- readonly meta: ComponentMeta;
114
- constructor(
115
- /** The entity this component belongs to. */
116
- entity: Entity,
117
- /** Registration metadata (type id, display name, bit-pointer). */
118
- meta: ComponentMeta);
119
- /** Numeric type id — shorthand for `this.meta.type`. */
120
- get type(): number;
121
- /** Pre-computed bit-pointer — shorthand for `this.meta.bitPtr`. */
122
- get bitPtr(): BitPtr;
123
- /**
124
- * Notify the world that this component's data has changed.
125
- *
126
- * Queues a modified event that fires `update` callbacks on every system /
127
- * query that watches this component type, plus the component's `onSet`
128
- * hook. Repeated calls before the world drains its queue are coalesced
129
- * into one delivery.
130
- */
131
- modified(): void;
132
- /** Returns the component's registered display name (e.g. `"Position"`). */
133
- toString(): string;
85
+ onSet(handler: (entity: Entity, c: Component) => void): ComponentMeta;
134
86
  }
package/dist/component.js CHANGED
@@ -11,11 +11,11 @@ import { BitPtr, Bitset } from "./util/bitset.js";
11
11
  export class ComponentMeta {
12
12
  constructor(Class, type, componentName) {
13
13
  /**
14
- * Type ids of components that cannot coexist with this one on the same
15
- * entity. Set via {@link World.setExclusiveComponents}; `undefined` means
16
- * no restriction.
14
+ * @internal Peer metas of components that cannot coexist with this one on
15
+ * the same entity. Set via {@link World.setExclusiveComponents}; `undefined`
16
+ * means no restriction.
17
17
  */
18
- this.exclusive = undefined;
18
+ this._exclusive = undefined;
19
19
  this.Class = Class;
20
20
  this.type = type;
21
21
  this.componentName = componentName;
@@ -37,61 +37,6 @@ export class ComponentMeta {
37
37
  return this;
38
38
  }
39
39
  }
40
- /**
41
- * Base class for all ECS components.
42
- *
43
- * Subclass `Component` to declare data that can be attached to an
44
- * {@link Entity}. Instances are constructed by the world when
45
- * {@link Entity.add} or {@link Entity.set} runs — never instantiate manually.
46
- *
47
- * ```ts
48
- * class Position extends Component {
49
- * x = 0;
50
- * y = 0;
51
- * }
52
- *
53
- * world.registerComponent(Position);
54
- * entity.set(Position, { x: 100 });
55
- * ```
56
- *
57
- * Each instance is bound to a single entity via {@link entity}; that link is
58
- * permanent for the component's lifetime.
59
- */
60
- export class Component {
61
- constructor(
62
- /** The entity this component belongs to. */
63
- entity,
64
- /** Registration metadata (type id, display name, bit-pointer). */
65
- meta) {
66
- this.entity = entity;
67
- this.meta = meta;
68
- /** @internal Set by {@link Entity.modified} to coalesce repeated calls until the world routes the modified command. */
69
- this._dirty = false;
70
- }
71
- /** Numeric type id — shorthand for `this.meta.type`. */
72
- get type() {
73
- return this.meta.type;
74
- }
75
- /** Pre-computed bit-pointer — shorthand for `this.meta.bitPtr`. */
76
- get bitPtr() {
77
- return this.meta.bitPtr;
78
- }
79
- /**
80
- * Notify the world that this component's data has changed.
81
- *
82
- * Queues a modified event that fires `update` callbacks on every system /
83
- * query that watches this component type, plus the component's `onSet`
84
- * hook. Repeated calls before the world drains its queue are coalesced
85
- * into one delivery.
86
- */
87
- modified() {
88
- this.entity.modified(this);
89
- }
90
- /** Returns the component's registered display name (e.g. `"Position"`). */
91
- toString() {
92
- return this.meta.componentName;
93
- }
94
- }
95
40
  /**
96
41
  * Compute a {@link Bitset} with one bit set for every component class or
97
42
  * numeric type id in `classes`.
@@ -1 +1 @@
1
- {"version":3,"file":"component.js","sourceRoot":"","sources":["../src/component.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AA2DlD;;;;;;;;GAQG;AACH,MAAM,OAAO,aAAa;IAwBxB,YAAY,KAAuB,EAAE,IAAY,EAAE,aAAqB;QAdxE;;;;WAIG;QACI,cAAS,GAAyB,SAAS,CAAC;QAUjD,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,aAAa,GAAG,aAAa,CAAC;QACnC,IAAI,CAAC,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,CAAC;IACjC,CAAC;IAED,kBAAkB;IACX,KAAK,CAAC,OAA+B;QAC1C,CAAC,IAAI,CAAC,cAAc,KAAnB,IAAI,CAAC,cAAc,GAAK,EAAE,EAAC,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QAC9C,OAAO,IAAI,CAAC;IACd,CAAC;IAED,kBAAkB;IACX,QAAQ,CAAC,OAA+B;QAC7C,CAAC,IAAI,CAAC,iBAAiB,KAAtB,IAAI,CAAC,iBAAiB,GAAK,EAAE,EAAC,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QACjD,OAAO,IAAI,CAAC;IACd,CAAC;IAED,kBAAkB;IACX,KAAK,CAAC,OAA+B;QAC1C,CAAC,IAAI,CAAC,cAAc,KAAnB,IAAI,CAAC,cAAc,GAAK,EAAE,EAAC,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QAC9C,OAAO,IAAI,CAAC;IACd,CAAC;CACF;AAYD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,OAAO,SAAS;IAIpB;IACE,4CAA4C;IAC5B,MAAc;IAC9B,kEAAkE;IAClD,IAAmB;QAFnB,WAAM,GAAN,MAAM,CAAQ;QAEd,SAAI,GAAJ,IAAI,CAAe;QAPrC,uHAAuH;QAChH,WAAM,GAAY,KAAK,CAAC;IAO5B,CAAC;IAEJ,wDAAwD;IACxD,IAAW,IAAI;QACb,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC;IACxB,CAAC;IAED,mEAAmE;IACnE,IAAW,MAAM;QACf,OAAO,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC;IAC1B,CAAC;IAED;;;;;;;OAOG;IACI,QAAQ;QACb,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;IAC7B,CAAC;IAED,2EAA2E;IACpE,QAAQ;QACb,OAAO,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC;IACjC,CAAC;CACF;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,0BAA0B,CAAC,OAA4B,EAAE,KAAY;IACnF,MAAM,OAAO,GAAG,IAAI,MAAM,EAAE,CAAC;IAC7B,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE;QACpB,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC,CAAC;IACzC,CAAC,CAAC,CAAC;IACH,OAAO,OAAO,CAAC;AACjB,CAAC"}
1
+ {"version":3,"file":"component.js","sourceRoot":"","sources":["../src/component.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAoElD;;;;;;;;GAQG;AACH,MAAM,OAAO,aAAa;IAwBxB,YAAY,KAAqB,EAAE,IAAY,EAAE,aAAqB;QAdtE;;;;WAIG;QACI,eAAU,GAAgC,SAAS,CAAC;QAUzD,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,aAAa,GAAG,aAAa,CAAC;QACnC,IAAI,CAAC,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,CAAC;IACjC,CAAC;IAED,kBAAkB;IACX,KAAK,CAAC,OAA+C;QAC1D,CAAC,IAAI,CAAC,cAAc,KAAnB,IAAI,CAAC,cAAc,GAAK,EAAE,EAAC,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QAC9C,OAAO,IAAI,CAAC;IACd,CAAC;IAED,kBAAkB;IACX,QAAQ,CAAC,OAA+C;QAC7D,CAAC,IAAI,CAAC,iBAAiB,KAAtB,IAAI,CAAC,iBAAiB,GAAK,EAAE,EAAC,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QACjD,OAAO,IAAI,CAAC;IACd,CAAC;IAED,kBAAkB;IACX,KAAK,CAAC,OAA+C;QAC1D,CAAC,IAAI,CAAC,cAAc,KAAnB,IAAI,CAAC,cAAc,GAAK,EAAE,EAAC,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QAC9C,OAAO,IAAI,CAAC;IACd,CAAC;CACF;AASD;;;;;;;;GAQG;AACH,MAAM,UAAU,0BAA0B,CAAC,OAA4B,EAAE,KAAY;IACnF,MAAM,OAAO,GAAG,IAAI,MAAM,EAAE,CAAC;IAC7B,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE;QACpB,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC,CAAC;IACzC,CAAC,CAAC,CAAC;IACH,OAAO,OAAO,CAAC;AACjB,CAAC"}
package/dist/dsl.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Component, ComponentClassArray, ComponentClassOrType } from "./component.js";
1
+ import { type ComponentClass, ComponentClassArray, ComponentClassOrType } from "./component.js";
2
2
  import type { Entity } from "./entity.js";
3
3
  /**
4
4
  * A predicate that decides whether a given entity belongs to a query.
@@ -26,9 +26,13 @@ export type EntityTestFunc = (e: Entity) => boolean;
26
26
  * ```
27
27
  *
28
28
  * Short forms recognized by `query` / `filter`:
29
- * - A single class or numeric type id is shorthand for `{ HAS: [C] }`.
29
+ * - A registered component class or numeric type id is shorthand for `{ HAS: [C] }`.
30
30
  * - An array `[A, B]` is shorthand for `{ HAS: [A, B] }`.
31
31
  * - An {@link EntityTestFunc} is invoked directly for fully custom logic.
32
+ *
33
+ * Function values are treated as component classes only when the world already
34
+ * has registered metadata for that class. Register component classes before
35
+ * using them in query DSL expressions.
32
36
  */
33
37
  export type QueryDSL = ComponentClassArray | ComponentClassOrType | EntityTestFunc | {
34
38
  HAS: ComponentClassArray | ComponentClassOrType;
@@ -53,13 +57,13 @@ export type QueryDSL = ComponentClassArray | ComponentClassOrType | EntityTestFu
53
57
  * @typeParam C - Component class being injected.
54
58
  * @typeParam R - Tuple of component classes guaranteed present.
55
59
  */
56
- export type MaybeRequired<C, R extends (typeof Component)[]> = C extends typeof Component ? C extends R[number] ? InstanceType<C> : InstanceType<C> | undefined : never;
60
+ export type MaybeRequired<C, R extends ComponentClass[]> = C extends ComponentClass ? C extends R[number] ? InstanceType<C> : InstanceType<C> | undefined : never;
57
61
  /**
58
62
  * Statically extract the component classes that are **guaranteed present** on
59
63
  * every entity matched by a {@link QueryDSL} expression.
60
64
  *
61
65
  * Rules:
62
- * - Plain class `C` → `[C]`
66
+ * - Plain component class `C` → `[C]`
63
67
  * - Plain array `[A, B]` → `[A, B]`
64
68
  * - `{ HAS: ... }` / `{ HAS_ONLY: ... }` → recurse into the payload
65
69
  * - `{ AND: [q1, q2, ...] }` → concatenate each branch's extraction
@@ -68,7 +72,7 @@ export type MaybeRequired<C, R extends (typeof Component)[]> = C extends typeof
68
72
  *
69
73
  * @typeParam Q - Query expression to analyse.
70
74
  */
71
- export type ExtractRequired<Q> = Q extends typeof Component ? [Q] : Q extends readonly (typeof Component)[] ? Q : Q extends {
75
+ export type ExtractRequired<Q> = Q extends ComponentClass ? [Q] : Q extends readonly ComponentClass[] ? Q : Q extends {
72
76
  HAS: infer H;
73
77
  } ? ExtractRequired<H> : Q extends {
74
78
  HAS_ONLY: infer H;