@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
package/dist/world.js ADDED
@@ -0,0 +1,453 @@
1
+ import { ComponentMeta, } from "./component.js";
2
+ import { Entity } from "./entity.js";
3
+ import { System } from "./system.js";
4
+ import { ArrayMap } from "./util/array_map.js";
5
+ import { Phase } from "./phase.js";
6
+ const LOCAL_COMPONENT_MIN = 256;
7
+ /**
8
+ * The central ECS container.
9
+ *
10
+ * A `World` owns all entities, components, systems, and the update pipeline.
11
+ * Typical lifecycle:
12
+ *
13
+ * 1. **Register components** — call {@link registerComponent} (and optionally
14
+ * {@link registerComponentType}) for every component class.
15
+ * 2. **Register systems** — call {@link system} (or {@link addSystem}) to
16
+ * create and configure {@link System | systems}.
17
+ * 3. **Start** — call {@link start} to freeze registration and sort systems
18
+ * into their phases.
19
+ * 4. **Run loop** — call {@link runPhase} once per frame for each phase.
20
+ *
21
+ * ```ts
22
+ * const world = new World();
23
+ *
24
+ * world.registerComponent(Position);
25
+ * world.registerComponent(Velocity);
26
+ *
27
+ * world.system("Move")
28
+ * .requires(Position, Velocity)
29
+ * .update(Position, (pos) => { pos.x += vel.x; });
30
+ *
31
+ * world.start();
32
+ *
33
+ * // game loop:
34
+ * world.runPhase(updatePhase, Date.now(), 16);
35
+ * ```
36
+ */
37
+ export class World {
38
+ constructor() {
39
+ this.entities = new Map(); // maps entity Id to Entity
40
+ this.componentNameTypeMap = new Map();
41
+ this.archChangeQueue = [];
42
+ this.destroyedEntities = [];
43
+ this.pendingSystems = [];
44
+ this.allSystems = [];
45
+ this.Class2Meta = new Map();
46
+ this.Type2Meta = new ArrayMap();
47
+ this.updatedComponents = [];
48
+ this.localComponentCounter = LOCAL_COMPONENT_MIN;
49
+ this.componentRegistrationDisabled = false;
50
+ this.systemRegistrationDisabled = false;
51
+ this.pipeline = new Map();
52
+ this.eidCounter = 0;
53
+ }
54
+ /**
55
+ * Return the entity with id `eid`, creating it if it does not yet exist.
56
+ *
57
+ * Used by networking code to materialise server-assigned entities:
58
+ *
59
+ * ```ts
60
+ * const e = world.getOrCreateEntity(snapshot.eid, (e) => {
61
+ * networkEntities.add(e);
62
+ * });
63
+ * const c = e.add(snapshot.type, false);
64
+ * ```
65
+ *
66
+ * @param eid - The entity id to look up or create.
67
+ * @param onCreateCallback - Optional callback invoked only when a **new**
68
+ * entity is created, before it is returned. Use this to initialise
69
+ * bookkeeping (e.g. tracking it in a local set).
70
+ * @returns The existing or newly created entity.
71
+ */
72
+ getOrCreateEntity(eid, onCreateCallback) {
73
+ let e = this.entities.get(eid);
74
+ if (!e) {
75
+ e = new Entity(this, eid);
76
+ this.entities.set(eid, e);
77
+ if (onCreateCallback)
78
+ onCreateCallback(e);
79
+ }
80
+ return e;
81
+ }
82
+ /**
83
+ * Look up an entity by id.
84
+ *
85
+ * @param id - Numeric entity id.
86
+ * @returns The entity, or `undefined` if no entity with that id exists.
87
+ */
88
+ entity(id) {
89
+ return this.entities.get(id);
90
+ }
91
+ /**
92
+ * Create a new entity with an auto-assigned id and register it in the world.
93
+ *
94
+ * The id counter starts at 0 (or at the value set by
95
+ * {@link setEntityIdRange}) and increments by one for each call.
96
+ *
97
+ * @returns The new entity.
98
+ */
99
+ createEntity() {
100
+ const eid = this.eidCounter++;
101
+ const e = new Entity(this, eid);
102
+ this.entities.set(eid, e);
103
+ return e;
104
+ }
105
+ /**
106
+ * Set the starting value for the auto-incrementing entity id counter.
107
+ *
108
+ * Must be called **before** {@link start} (or
109
+ * {@link disableComponentRegistration}). Useful when the world runs alongside
110
+ * a server that owns a different id range — for example, locally-created
111
+ * client entities can start at a high offset to avoid collisions with
112
+ * server-assigned ids.
113
+ *
114
+ * @param min - The first id that will be assigned by {@link createEntity}.
115
+ * @throws If called after registration has been disabled.
116
+ */
117
+ setEntityIdRange(min) {
118
+ if (this.componentRegistrationDisabled)
119
+ throw "setEntityIdRange must be called before component registration is disabled";
120
+ this.eidCounter = min;
121
+ }
122
+ getComponentInstance(typeOrClass, entity) {
123
+ const meta = this.getComponentMeta(typeOrClass);
124
+ const c = new meta.Class(entity, meta);
125
+ const hook = meta["onAddHandler"];
126
+ if (hook)
127
+ hook(c);
128
+ return c;
129
+ }
130
+ /**
131
+ * Retrieve the {@link ComponentMeta} record for a registered component.
132
+ *
133
+ * @param typeOrClass - A component class constructor or a numeric type id.
134
+ * @returns The corresponding `ComponentMeta`.
135
+ * @throws If no component with that class or type id has been registered.
136
+ */
137
+ getComponentMeta(typeOrClass) {
138
+ let meta;
139
+ if (typeof typeOrClass === "function") {
140
+ meta = this.Class2Meta.get(typeOrClass);
141
+ }
142
+ else {
143
+ meta = this.Type2Meta.get(typeOrClass);
144
+ }
145
+ if (!meta)
146
+ throw `unregistered component meta for component type or class '${typeOrClass}'`;
147
+ return meta;
148
+ }
149
+ /**
150
+ * Resolve a component class or type id to its numeric type id.
151
+ *
152
+ * @param typeOrClass - A component class constructor or a numeric type id.
153
+ * @returns The numeric type id.
154
+ */
155
+ getComponentType(typeOrClass) {
156
+ if (typeof typeOrClass === "function") {
157
+ return this.getComponentMeta(typeOrClass).type;
158
+ }
159
+ return typeOrClass;
160
+ }
161
+ /**
162
+ * Mark an entity's archetype as changed, queuing it for re-evaluation
163
+ * against all system queries at the end of the current system run.
164
+ *
165
+ * Also recursively marks all children as changed so that `{ PARENT: ... }`
166
+ * queries are re-evaluated.
167
+ *
168
+ * @internal Called automatically by {@link Entity.add} and
169
+ * {@link Entity.remove}.
170
+ */
171
+ archetypeChanged(e) {
172
+ if (e._archetypeChanged)
173
+ return;
174
+ e._archetypeChanged = true;
175
+ this.archChangeQueue.push(e);
176
+ e.children.forEach((child) => this.archetypeChanged(child));
177
+ }
178
+ /** @internal */
179
+ _notifyComponentAdded(e, c) {
180
+ this.archetypeChanged(e);
181
+ }
182
+ /** @internal */
183
+ _notifyComponentRemoved(e, c) {
184
+ const hook = c.meta["onRemoveHandler"];
185
+ if (hook)
186
+ hook(c);
187
+ this.archetypeChanged(e);
188
+ }
189
+ /** @internal */
190
+ _notifyEntityDestroyed(e) {
191
+ if (!this.entities.delete(e.eid))
192
+ return;
193
+ e.forEachComponent((c) => {
194
+ e.remove(c.type);
195
+ });
196
+ this.destroyedEntities.push(e);
197
+ }
198
+ updateArchetypes() {
199
+ if (this.archChangeQueue.length > 0) {
200
+ this.allSystems.forEach((s) => {
201
+ this.archChangeQueue.forEach((e) => {
202
+ if (s.belongs(e)) {
203
+ e._addSystem(s);
204
+ }
205
+ else {
206
+ e._removeSystem(s);
207
+ }
208
+ });
209
+ });
210
+ this.archChangeQueue.forEach((e) => {
211
+ e.clearDeletedComponents();
212
+ });
213
+ }
214
+ this.destroyedEntities.forEach((e) => {
215
+ e["_destroy"]();
216
+ });
217
+ this.destroyedEntities.length = 0;
218
+ this.updatedComponents.forEach((c) => {
219
+ const hook = c.meta["onSetHandler"];
220
+ if (hook)
221
+ hook(c);
222
+ c.entity._notifyModified(c);
223
+ c["dirty"] = false;
224
+ });
225
+ this.archChangeQueue.forEach((e) => {
226
+ e._updateSystems();
227
+ e._archetypeChanged = false;
228
+ });
229
+ this.archChangeQueue.length = 0;
230
+ this.updatedComponents.length = 0;
231
+ }
232
+ /** @internal Queues a component for onSet / update delivery. */
233
+ _queueUpdatedComponent(c) {
234
+ if (c["dirty"])
235
+ return;
236
+ c["dirty"] = true;
237
+ this.updatedComponents.push(c);
238
+ }
239
+ registerComponent(ComponentClass, typeOrComponentName, componentName) {
240
+ if (this.componentRegistrationDisabled) {
241
+ throw "World component registartion is disabled";
242
+ }
243
+ let type = undefined;
244
+ // Determine if the second argument is type or componentName based on its type
245
+ if (typeof typeOrComponentName === "number") {
246
+ type = typeOrComponentName;
247
+ }
248
+ else if (typeof typeOrComponentName === "string") {
249
+ componentName = typeOrComponentName;
250
+ }
251
+ componentName = componentName || ComponentClass.name;
252
+ let local = false;
253
+ if (type === undefined) {
254
+ // attempt to get type id from name->type map
255
+ type = this.componentNameTypeMap.get(componentName);
256
+ if (type === undefined) {
257
+ type = this.localComponentCounter++;
258
+ local = true;
259
+ }
260
+ }
261
+ let meta = this.Class2Meta.get(ComponentClass);
262
+ if (meta) {
263
+ if (local)
264
+ this.localComponentCounter--;
265
+ throw `Trying to register ${componentName} with type=${type} which is already registered to ${meta.componentName}`;
266
+ }
267
+ this.registerComponentType(componentName, type);
268
+ meta = new ComponentMeta(ComponentClass, type, componentName);
269
+ this.Class2Meta.set(ComponentClass, meta);
270
+ this.Type2Meta.set(type, meta);
271
+ console.log("Registered component %s with type=%d as %s component", componentName, type, local ? "local" : "networked");
272
+ }
273
+ /**
274
+ * Pre-register a component name → type id mapping without associating a
275
+ * class.
276
+ *
277
+ * Useful when network messages refer to components by type id and the
278
+ * corresponding class may be registered later. Call this before
279
+ * {@link registerComponent} to ensure the class picks up the server-assigned
280
+ * id rather than a locally generated one.
281
+ *
282
+ * @param componentName - The string name used in network payloads.
283
+ * @param type - The numeric type id assigned by the server.
284
+ */
285
+ registerComponentType(componentName, type) {
286
+ this.componentNameTypeMap.set(componentName, type);
287
+ }
288
+ /**
289
+ * Register a pre-built {@link System} with the world.
290
+ *
291
+ * In most cases it is more convenient to use {@link system} which both
292
+ * creates and registers in one call. Use `addSystem` when you need to
293
+ * subclass `System` directly.
294
+ *
295
+ * @param s - The system to add.
296
+ * @throws If system registration is disabled (after {@link start}).
297
+ */
298
+ addSystem(s) {
299
+ if (this.systemRegistrationDisabled)
300
+ throw "System registration is disabled";
301
+ this.pendingSystems.push(s);
302
+ }
303
+ /**
304
+ * Create a new {@link System}, register it, and return it for configuration.
305
+ *
306
+ * This is the primary way to define systems:
307
+ *
308
+ * ```ts
309
+ * world.system("Render")
310
+ * .phase("update")
311
+ * .requires(Position, Sprite)
312
+ * .enter([Sprite], (e, [sprite]) => sprite.initialize(scene))
313
+ * .update(Position, (pos) => { ... });
314
+ * ```
315
+ *
316
+ * @param name - A unique display name for the system.
317
+ * @returns The new `System` instance.
318
+ */
319
+ system(name) {
320
+ const system = new System(name, this);
321
+ this.addSystem(system);
322
+ return system;
323
+ }
324
+ /**
325
+ * Prevent any further calls to {@link registerComponent}.
326
+ *
327
+ * Called automatically by {@link start}. Can be called early if you want to
328
+ * lock component registration before systems are fully configured.
329
+ */
330
+ disableComponentRegistration() {
331
+ this.componentRegistrationDisabled = true;
332
+ }
333
+ /**
334
+ * Freeze registration and prepare the world for running.
335
+ *
336
+ * Disables both component and system registration, then distributes all
337
+ * pending systems into their assigned pipeline phases (defaulting to
338
+ * `"update"`). Logs the resulting phase → system order to the console.
339
+ *
340
+ * Call this once, after all components and systems are registered but before
341
+ * the first {@link runPhase} call.
342
+ */
343
+ start() {
344
+ this.componentRegistrationDisabled = true;
345
+ this.systemRegistrationDisabled = true;
346
+ this.reindexSystems();
347
+ }
348
+ reindexSystems() {
349
+ let _defaultPhase = this.pipeline.get("update");
350
+ if (!_defaultPhase) {
351
+ _defaultPhase = new Phase("update", this);
352
+ this.pipeline.set(_defaultPhase.name, _defaultPhase);
353
+ }
354
+ const defaultPhase = _defaultPhase;
355
+ this.pendingSystems.forEach((s) => {
356
+ let phase = s._phase;
357
+ if (typeof phase === "string") {
358
+ phase = this.pipeline.get(phase);
359
+ }
360
+ phase = phase || defaultPhase;
361
+ phase.systems.push(s);
362
+ });
363
+ this.pendingSystems.length = 0;
364
+ this.allSystems.length = 0;
365
+ this.pipeline.forEach((phase) => {
366
+ this.allSystems.push(...phase.systems);
367
+ console.log("Phase %s : %s", phase.name, phase.systems.map((s) => s.name).join(" -> "));
368
+ });
369
+ }
370
+ /**
371
+ * Return the {@link Hook} for a component class.
372
+ *
373
+ * Hooks let you react to component lifecycle events (add / remove / set)
374
+ * without building a full {@link System}. The hook is backed by the
375
+ * component's {@link ComponentMeta} and the same object is returned on every
376
+ * call.
377
+ *
378
+ * ```ts
379
+ * world.hook(Sprite)
380
+ * .onAdd(c => c.initialize(scene))
381
+ * .onRemove(c => c.destroy());
382
+ * ```
383
+ *
384
+ * @param C - The component class.
385
+ * @returns The `Hook` for that component type.
386
+ */
387
+ hook(C) {
388
+ return this.getComponentMeta(C);
389
+ }
390
+ /**
391
+ * Add a named phase to the update pipeline and return it.
392
+ *
393
+ * Phases are executed in insertion order when you call {@link runPhase} for
394
+ * each one. Systems are assigned to a phase via {@link System.phase}.
395
+ *
396
+ * ```ts
397
+ * const preUpdate = world.addPhase("preupdate");
398
+ * const update = world.addPhase("update");
399
+ * const send = world.addPhase("send");
400
+ * ```
401
+ *
402
+ * @param name - Unique phase name. Systems can reference it by this string.
403
+ * @returns The new {@link IPhase}.
404
+ */
405
+ addPhase(name) {
406
+ const phase = new Phase(name, this);
407
+ this.pipeline.set(name, phase);
408
+ return phase;
409
+ }
410
+ /**
411
+ * Execute all systems in the given phase for one tick.
412
+ *
413
+ * After each system runs, pending archetype changes (entity add/remove
414
+ * component events) are flushed so that `enter` / `exit` callbacks are
415
+ * delivered before the next system in the same phase executes.
416
+ *
417
+ * @param phase - The {@link IPhase} to run (returned by {@link addPhase}).
418
+ * @param now - Absolute timestamp in milliseconds (e.g. `Date.now()`).
419
+ * @param delta - Milliseconds elapsed since the previous tick.
420
+ */
421
+ runPhase(phase, now, delta) {
422
+ phase.systems.forEach((s) => {
423
+ s._run(now, delta);
424
+ this.updateArchetypes();
425
+ });
426
+ }
427
+ /**
428
+ * Run every phase in the pipeline in insertion order (the order phases were
429
+ * registered via {@link addPhase}). Equivalent to calling
430
+ * {@link runPhase} for each phase manually.
431
+ *
432
+ * @param now - Absolute timestamp in milliseconds (e.g. `Date.now()`).
433
+ * @param delta - Milliseconds elapsed since the previous tick.
434
+ */
435
+ progress(now, delta) {
436
+ this.pipeline.forEach((phase) => {
437
+ this.runPhase(phase, now, delta);
438
+ });
439
+ }
440
+ /**
441
+ * Destroy every entity currently tracked by the world.
442
+ *
443
+ * Triggers all `onRemove` hooks and `exit` callbacks. Useful when
444
+ * transitioning between game sessions or resetting to a clean state.
445
+ */
446
+ clearAllEntities() {
447
+ this.entities.forEach((e) => {
448
+ e.destroy();
449
+ });
450
+ this.entities.clear();
451
+ }
452
+ }
453
+ //# sourceMappingURL=world.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"world.js","sourceRoot":"","sources":["../src/world.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "@vworlds/vecs",
3
+ "version": "1.0.0",
4
+ "main": "dist/index.js",
5
+ "types": "dist/index.d.ts",
6
+ "type": "module",
7
+ "scripts": {
8
+ "clean": "rimraf dist && rimraf tsconfig.tsbuildinfo",
9
+ "prepack": "yarn build",
10
+ "build": "yarn clean && yarn compile",
11
+ "compile": "tsc --build && cp \"./package.json\" ./dist/",
12
+ "test": "vitest run",
13
+ "test:watch": "vitest"
14
+ },
15
+ "devDependencies": {
16
+ "@types/node": "^20.0.0",
17
+ "rimraf": "^5.0.0",
18
+ "typescript": "^4.8.4",
19
+ "vitest": "^1.6.0"
20
+ },
21
+ "dependencies": {
22
+ "eventemitter3": "^4.0.7"
23
+ },
24
+ "license": "UNLICENSED"
25
+ }
@@ -0,0 +1,180 @@
1
+ import { BitPtr, Bitset } from "./util/bitset.js";
2
+ import type { Entity } from "./entity.js";
3
+ import { type World } from "./world.js";
4
+
5
+ /**
6
+ * Lifecycle hook for a component type. Obtained via {@link World.hook}.
7
+ *
8
+ * Hooks let you react to component lifecycle events without building a full
9
+ * {@link System}. Each call returns the same `Hook` so the methods can be
10
+ * chained:
11
+ *
12
+ * ```ts
13
+ * world.hook(Sprite)
14
+ * .onAdd(c => initSprite(c))
15
+ * .onRemove(c => destroySprite(c))
16
+ * .onSet(c => syncSprite(c));
17
+ * ```
18
+ *
19
+ * Callbacks are invoked synchronously during {@link World.runPhase} when
20
+ * archetype changes are flushed.
21
+ *
22
+ * @typeParam C - The `Component` subclass this hook is bound to.
23
+ */
24
+ export interface Hook<C extends Component = Component> {
25
+ /**
26
+ * Register a callback that fires when a component of this type is added to
27
+ * an entity.
28
+ *
29
+ * @param handler - Receives the newly created component instance.
30
+ * @returns `this` for chaining.
31
+ */
32
+ onAdd(handler: (c: C) => void): Hook<C>;
33
+
34
+ /**
35
+ * Register a callback that fires when a component of this type is removed
36
+ * from an entity (including when the entity is destroyed).
37
+ *
38
+ * @param handler - Receives the component instance being removed.
39
+ * @returns `this` for chaining.
40
+ */
41
+ onRemove(handler: (c: C) => void): Hook<C>;
42
+
43
+ /**
44
+ * Register a callback that fires when {@link Component.modified} is called
45
+ * on a component of this type.
46
+ *
47
+ * @param handler - Receives the component instance that changed.
48
+ * @returns `this` for chaining.
49
+ */
50
+ onSet(handler: (c: C) => void): Hook<C>;
51
+ }
52
+
53
+ /**
54
+ * Internal bookkeeping record for a registered component class.
55
+ *
56
+ * Every component class that is passed to {@link World.registerComponent} gets
57
+ * a `ComponentMeta` that maps it to a numeric type id, a string name, and a
58
+ * pre-computed {@link BitPtr} used for fast archetype checks.
59
+ *
60
+ * `ComponentMeta` also implements {@link Hook}, so you can attach lifecycle
61
+ * callbacks directly on the meta object (as `World.hook()` returns it).
62
+ */
63
+ export class ComponentMeta implements Hook<Component> {
64
+ /** The component class constructor. */
65
+ public readonly Class: typeof Component;
66
+ /** Numeric type id assigned at registration time. */
67
+ public readonly type: number;
68
+ /** Human-readable name used in logs and serialization lookups. */
69
+ public readonly componentName: string;
70
+ /** Pre-computed bit-pointer into the entity archetype {@link Bitset}. */
71
+ public readonly bitPtr: BitPtr;
72
+ private onAddHandler: ((c: Component) => void) | undefined;
73
+ private onRemoveHandler: ((c: Component) => void) | undefined;
74
+ private onSetHandler: ((c: Component) => void) | undefined;
75
+
76
+ constructor(Class: typeof Component, type: number, componentName: string) {
77
+ this.Class = Class;
78
+ this.type = type;
79
+ this.componentName = componentName;
80
+ this.bitPtr = new BitPtr(type);
81
+ }
82
+
83
+ /** @inheritdoc */
84
+ public onAdd(handler: (c: Component) => void): ComponentMeta {
85
+ this.onAddHandler = handler;
86
+ return this;
87
+ }
88
+
89
+ /** @inheritdoc */
90
+ public onRemove(handler: (c: Component) => void): ComponentMeta {
91
+ this.onRemoveHandler = handler;
92
+ return this;
93
+ }
94
+
95
+ /** @inheritdoc */
96
+ public onSet(handler: (c: Component) => void): ComponentMeta {
97
+ this.onSetHandler = handler;
98
+ return this;
99
+ }
100
+ }
101
+
102
+ /** A component class constructor or its numeric type id. */
103
+ export type ComponentClassOrType = number | typeof Component;
104
+
105
+ /** An array of component class constructors or type ids. */
106
+ export type ComponentClassArray = ComponentClassOrType[];
107
+
108
+ /**
109
+ * Base class for all ECS components.
110
+ *
111
+ * Extend this class to define data that can be attached to an {@link Entity}:
112
+ *
113
+ * ```ts
114
+ * class Position extends Component {
115
+ * x = 0;
116
+ * y = 0;
117
+ * }
118
+ *
119
+ * world.registerComponent(Position);
120
+ * const pos = entity.add(Position);
121
+ * pos.x = 100;
122
+ * pos.modified(); // notify watching systems
123
+ * ```
124
+ *
125
+ * A component instance is always bound to a single entity and is created by
126
+ * the world when {@link Entity.add} is called.
127
+ */
128
+ export class Component {
129
+ private dirty: boolean = false;
130
+
131
+ constructor(
132
+ /** The entity this component belongs to. */
133
+ public readonly entity: Entity,
134
+ /** Registration metadata (type id, name, bit-pointer). */
135
+ public readonly meta: ComponentMeta
136
+ ) {}
137
+
138
+ /** Numeric type id — shorthand for `this.meta.type`. */
139
+ public get type(): number {
140
+ return this.meta.type;
141
+ }
142
+
143
+ /** Pre-computed bit-pointer — shorthand for `this.meta.bitPtr`. */
144
+ public get bitPtr(): BitPtr {
145
+ return this.meta.bitPtr;
146
+ }
147
+
148
+ /**
149
+ * Notify the world that this component's data has changed.
150
+ *
151
+ * Queues the component for delivery to all {@link System.update} callbacks
152
+ * that watch this component type. Call this after mutating the component's
153
+ * fields to ensure systems react to the new values.
154
+ */
155
+ public modified() {
156
+ this.entity.world._queueUpdatedComponent(this);
157
+ }
158
+
159
+ /** Returns the component's registered name, e.g. `"Position"`. */
160
+ public toString(): string {
161
+ return this.meta.componentName;
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Compute a {@link Bitset} that has a bit set for every component class or
167
+ * type id in `classes`.
168
+ *
169
+ * @internal Used internally to build archetype masks for system queries.
170
+ */
171
+ export function calculateComponentBitmask(
172
+ classes: ComponentClassArray,
173
+ world: World
174
+ ) {
175
+ const bitmask = new Bitset();
176
+ classes.forEach((C) => {
177
+ bitmask.add(world.getComponentType(C));
178
+ });
179
+ return bitmask;
180
+ }