@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,361 @@
1
+ import { ArrayMap } from "./util/array_map.js";
2
+ import { Bitset } from "./util/bitset.js";
3
+ import { Component, ComponentClassArray, ComponentClassOrType } from "./component.js";
4
+ import type { Entity } from "./entity.js";
5
+ import { Phase, type IPhase } from "./phase.js";
6
+ import { type World } from "./world.js";
7
+ type EntityCallback = (e: Entity) => void;
8
+ type ComponentCallback = (c: Component) => void;
9
+ type RunCallback = (now: number, delta: number) => void;
10
+ /** A function that tests whether a given entity belongs to a system. */
11
+ export type EntityTestFunc = (e: Entity) => boolean;
12
+ type ComponentOrParent = typeof Component | {
13
+ parent: typeof Component;
14
+ };
15
+ type ComponentInstance<T> = T extends {
16
+ parent: typeof Component;
17
+ } ? InstanceType<T["parent"]> : T extends typeof Component ? InstanceType<T> : never;
18
+ /**
19
+ * A composable query expression used to declare which entities a
20
+ * {@link System} should track.
21
+ *
22
+ * Queries can be nested arbitrarily:
23
+ *
24
+ * ```ts
25
+ * // Entities that have Position AND (Sprite OR Container):
26
+ * world.system("render").query({
27
+ * AND: [Position, { OR: [Sprite, Container] }]
28
+ * });
29
+ *
30
+ * // Entities that have a parent with Player AND Container:
31
+ * world.system("attach").query({
32
+ * PARENT: { AND: [Player, Container] }
33
+ * });
34
+ * ```
35
+ *
36
+ * Short forms:
37
+ * - A single class or type id is equivalent to `{ HAS: [C] }`.
38
+ * - An array `[A, B]` is equivalent to `{ HAS: [A, B] }`.
39
+ * - Pass an {@link EntityTestFunc} directly for fully custom membership logic.
40
+ */
41
+ export type SystemQuery = ComponentClassArray | ComponentClassOrType | EntityTestFunc | {
42
+ HAS: ComponentClassArray | ComponentClassOrType;
43
+ } | {
44
+ HAS_ONLY: ComponentClassArray | ComponentClassOrType;
45
+ } | {
46
+ AND: SystemQuery[];
47
+ } | {
48
+ OR: SystemQuery[];
49
+ } | {
50
+ NOT: SystemQuery;
51
+ } | {
52
+ PARENT: SystemQuery;
53
+ };
54
+ /**
55
+ * A reactive processor that operates on a filtered subset of world entities.
56
+ *
57
+ * Systems are created and registered through {@link World.system}:
58
+ *
59
+ * ```ts
60
+ * world.system("Move")
61
+ * .requires(Position, Velocity) // track entities with both components
62
+ * .phase("update")
63
+ * .enter([Position], (e, [pos]) => { pos.x = 0; })
64
+ * .update(Position, (pos) => { pos.x += pos.vx; })
65
+ * .exit((e) => { console.log("entity left", e.eid); });
66
+ * ```
67
+ *
68
+ * All builder methods return `this` for chaining. Call {@link World.start}
69
+ * once all systems are registered; after that, drive the loop with
70
+ * {@link World.runPhase}.
71
+ *
72
+ * ### Component injection and type inference
73
+ *
74
+ * `enter`, `exit`, `update`, `each`, and `sort` all accept an array of
75
+ * component classes that are resolved from the entity and passed as a typed
76
+ * tuple to the callback. Use `{ parent: SomeComponent }` to resolve from the
77
+ * entity's parent instead of the entity itself.
78
+ *
79
+ * Components declared via {@link requires} (or the second argument of
80
+ * {@link query}) are tracked as a type parameter `R` on the system. In
81
+ * `sort`, `each`, and `update` inject callbacks, those components appear as
82
+ * non-nullable; any component not in `R` remains `Type | undefined`.
83
+ */
84
+ type MaybeRequired<C, R extends (typeof Component)[]> = C extends typeof Component ? C extends R[number] ? InstanceType<C> : InstanceType<C> | undefined : never;
85
+ export declare class System<R extends (typeof Component)[] = []> {
86
+ /** Unique name for this system, used in logs and pipeline output. */
87
+ readonly name: string;
88
+ /** The world that owns this system. */
89
+ readonly world: World;
90
+ protected componentUpdateCallbacks: ArrayMap<ComponentCallback>;
91
+ protected eachCallback: EntityCallback | undefined;
92
+ protected _entities: Set<Entity> | undefined;
93
+ protected _enterCallback: EntityCallback[];
94
+ protected _exitCallback: EntityCallback[];
95
+ private _runCallback;
96
+ protected _belongs: EntityTestFunc;
97
+ private readonly updateQueue;
98
+ private hasQuery;
99
+ /** @internal */
100
+ _phase: string | Phase | undefined;
101
+ protected watchlistBitmask: Bitset;
102
+ constructor(
103
+ /** Unique name for this system, used in logs and pipeline output. */
104
+ name: string,
105
+ /** The world that owns this system. */
106
+ world: World);
107
+ /** Returns the system name. */
108
+ toString(): string;
109
+ /**
110
+ * Read-only view of the entities currently tracked by this system.
111
+ *
112
+ * Empty unless {@link track} (or {@link each}, which implies it) was
113
+ * called during system configuration.
114
+ */
115
+ get entities(): ReadonlySet<Entity>;
116
+ /**
117
+ * Assign this system to a pipeline phase.
118
+ *
119
+ * The phase can be specified by name (the world will resolve it at
120
+ * {@link World.start | start} time) or by an {@link IPhase} reference
121
+ * returned from {@link World.addPhase}. Systems without an explicit phase
122
+ * are placed in the built-in `"update"` phase.
123
+ *
124
+ * @param p - Phase name or `IPhase` reference.
125
+ * @returns `this` for chaining.
126
+ */
127
+ phase(p: string | IPhase): this;
128
+ /** @internal Delivers a component-modified notification to this system. */
129
+ notifyModified(c: Component): void;
130
+ /** Returns `true` if the entity satisfies this system's query. */
131
+ belongs(e: Entity): boolean;
132
+ /** @internal Fires `enter` callbacks for a newly matched entity. */
133
+ _enter(e: Entity): void;
134
+ /** @internal Fires `exit` callbacks when an entity leaves the system. */
135
+ _exit(e: Entity): void;
136
+ /** @internal Execute one tick: run `run`, fire `each`, then drain the update queue. */
137
+ _run(now: number, delta: number): void;
138
+ private getComponent;
139
+ private getInjected;
140
+ private mapInjectedClassToTypes;
141
+ /**
142
+ * Register a callback that fires when an entity **enters** this system
143
+ * (i.e. first satisfies the system's query) with injected components.
144
+ *
145
+ * @param inject - Ordered list of component classes (or `{ parent: C }`) to
146
+ * resolve from the entering entity and pass to `callback`.
147
+ * @param callback - Receives the entity and the resolved component tuple.
148
+ * @returns `this` for chaining.
149
+ *
150
+ * @example
151
+ * ```ts
152
+ * system.enter([Position, Sprite], (e, [pos, sprite]) => {
153
+ * sprite.initialize(scene);
154
+ * sprite.sprite.setPosition(pos.x, pos.y);
155
+ * });
156
+ * ```
157
+ */
158
+ enter<J extends ComponentOrParent[]>(inject: readonly [...J], callback: (e: Entity, injected: {
159
+ [K in keyof J]: ComponentInstance<J[K]>;
160
+ }) => void): this;
161
+ /**
162
+ * Register a callback that fires when an entity enters this system.
163
+ *
164
+ * @param callback - Receives only the entity (no injection).
165
+ * @returns `this` for chaining.
166
+ */
167
+ enter(callback: (e: Entity) => void): this;
168
+ /**
169
+ * Register a callback that fires when an entity **exits** this system
170
+ * (its components no longer satisfy the query, or it was destroyed) with
171
+ * injected components.
172
+ *
173
+ * Components that were just removed are still accessible via `get_deleted`
174
+ * semantics — the injected tuple includes them even though they are no
175
+ * longer in the entity's active component set.
176
+ *
177
+ * @param inject - Component classes to resolve and inject.
178
+ * @param callback - Receives the entity and the resolved component tuple.
179
+ * @returns `this` for chaining.
180
+ */
181
+ exit<J extends ComponentOrParent[]>(inject: readonly [...J], callback: (e: Entity, injected: {
182
+ [K in keyof J]: ComponentInstance<J[K]>;
183
+ }) => void): this;
184
+ /**
185
+ * Register a callback that fires when an entity exits this system.
186
+ *
187
+ * @param callback - Receives only the entity.
188
+ * @returns `this` for chaining.
189
+ */
190
+ exit(callback: (e: Entity) => void): this;
191
+ /**
192
+ * Register a per-tick callback that runs every time this system's phase
193
+ * executes, regardless of entity membership.
194
+ *
195
+ * Use this for logic that is not driven by component updates — polling,
196
+ * network flushing, global timers, etc.
197
+ *
198
+ * @param callback - Receives `now` (absolute timestamp in ms) and `delta`
199
+ * (ms since the last tick).
200
+ * @returns `this` for chaining.
201
+ */
202
+ run(callback: RunCallback): this;
203
+ /**
204
+ * Register a callback that fires when a component of type `ComponentClass`
205
+ * is modified on any entity in this system.
206
+ *
207
+ * The system will automatically begin tracking entities that have this
208
+ * component type (equivalent to adding it to a `requires` / `HAS` query)
209
+ * unless a custom {@link query} was already set.
210
+ *
211
+ * @param ComponentClass - The component class to watch.
212
+ * @param callback - Receives the modified component instance.
213
+ * @returns `this` for chaining.
214
+ *
215
+ * @example
216
+ * ```ts
217
+ * world.system("RenderPosition")
218
+ * .update(Position, (pos) => {
219
+ * sprite.setPosition(pos.x, pos.y);
220
+ * });
221
+ * ```
222
+ */
223
+ update<C extends typeof Component>(ComponentClass: C, callback: (c: InstanceType<C>) => void): this;
224
+ /**
225
+ * Register a callback that fires when `ComponentClass` is modified, with
226
+ * additional components injected from the same entity.
227
+ *
228
+ * @param ComponentClass - The component class to watch.
229
+ * @param inject - Additional component classes to resolve from the entity.
230
+ * @param callback - Receives the modified component and the injected tuple.
231
+ * @returns `this` for chaining.
232
+ *
233
+ * @example
234
+ * ```ts
235
+ * world.system("SyncSprite")
236
+ * .update(Position, [Sprite], (pos, [sprite]) => {
237
+ * sprite.sprite.setPosition(pos.x, pos.y);
238
+ * });
239
+ * ```
240
+ */
241
+ update<C extends typeof Component, J extends (typeof Component)[]>(ComponentClass: C, inject: readonly [...J], callback: (c: InstanceType<C>, injected: {
242
+ [K in keyof J]: MaybeRequired<J[K], R>;
243
+ }) => void): this;
244
+ /**
245
+ * Register a callback that fires **every tick** for every entity currently
246
+ * tracked by this system, with the listed components resolved from each
247
+ * entity.
248
+ *
249
+ * Unlike {@link update} (which only fires when `component.modified()` is
250
+ * called), `each` fires unconditionally on every tick the system runs,
251
+ * once per tracked entity. Components declared via {@link requires} are
252
+ * guaranteed non-null in the resolved tuple; any other component class
253
+ * may be `undefined` if the entity lacks it.
254
+ *
255
+ * `each` does **not** modify the system's query — define membership with
256
+ * {@link requires} or {@link query} as usual. It does, however, implicitly
257
+ * enable {@link track}, so matched entities are exposed via {@link entities}.
258
+ *
259
+ * Only a single `each` callback may be registered per system; calling
260
+ * `each` a second time throws.
261
+ *
262
+ * @param components - Component classes to resolve from each entity.
263
+ * @param callback - Receives the entity and a tuple of resolved component
264
+ * instances (`undefined` for components not covered by {@link requires}).
265
+ * @returns `this` for chaining.
266
+ * @throws If `each` has already been registered on this system.
267
+ *
268
+ * @example
269
+ * ```ts
270
+ * world.system("Move")
271
+ * .requires(Position, Velocity)
272
+ * .each([Position, Velocity], (e, [pos, vel]) => {
273
+ * pos.x += vel.vx;
274
+ * pos.y += vel.vy;
275
+ * });
276
+ * ```
277
+ */
278
+ each<J extends (typeof Component)[]>(components: readonly [...J], callback: (e: Entity, resolved: {
279
+ [K in keyof J]: MaybeRequired<J[K], R>;
280
+ }) => void): this;
281
+ /**
282
+ * Enable entity tracking: matched entities are inserted into
283
+ * {@link entities} as they enter the system and removed as they exit.
284
+ *
285
+ * Idempotent. Intended to be called during system configuration before
286
+ * `world.start()`; entities already matched when `track` is called late
287
+ * will not be backfilled.
288
+ *
289
+ * {@link each} implies `track` — call this directly only when you want
290
+ * the tracked set without an `each` callback.
291
+ *
292
+ * @returns `this` for chaining.
293
+ */
294
+ track(): this;
295
+ /**
296
+ * Enable sorted entity tracking: matched entities are stored in insertion
297
+ * order determined by `compare`, which receives a tuple of resolved
298
+ * component instances for each pair of entities being ordered.
299
+ *
300
+ * Implies {@link track}.
301
+ *
302
+ * @param components - Component classes to resolve and pass to `compare`.
303
+ * @param compare - Returns a negative number, zero, or positive number when
304
+ * `a` should sort before, equal to, or after `b`.
305
+ * @returns `this` for chaining.
306
+ *
307
+ * @example
308
+ * ```ts
309
+ * world.system("Render")
310
+ * .requires(Position, Sprite)
311
+ * .sort([Position], ([posA], [posB]) => posA.z - posB.z);
312
+ * ```
313
+ */
314
+ sort<J extends (typeof Component)[]>(components: readonly [...J], compare: (a: {
315
+ [K in keyof J]: MaybeRequired<J[K], R>;
316
+ }, b: {
317
+ [K in keyof J]: MaybeRequired<J[K], R>;
318
+ }) => number): this;
319
+ private queryBuilder;
320
+ /**
321
+ * Set the entity membership predicate using the {@link SystemQuery} DSL.
322
+ *
323
+ * Replaces any implicit query derived from `update` watchlists and any
324
+ * previous `requires` call. After calling `query`, auto-expanding of
325
+ * `update` watchlists is disabled.
326
+ *
327
+ * The optional `guaranteed` tuple is a pure type-level hint: it tells
328
+ * `sort`, `each`, and `update` callbacks which components are guaranteed
329
+ * to be present on every matched entity, eliminating `| undefined` from
330
+ * those positions. It has no effect at runtime.
331
+ *
332
+ * @param q - A {@link SystemQuery} expression.
333
+ * @param _guaranteed - Component classes guaranteed present on every matched
334
+ * entity (type hint only — not validated at runtime).
335
+ * @returns `this` for chaining.
336
+ *
337
+ * @example
338
+ * ```ts
339
+ * world.system("Move")
340
+ * .query({ AND: [{ HAS: Position }, { HAS: Velocity }] }, [Position, Velocity])
341
+ * .each([Position, Velocity], (e, [pos, vel]) => {
342
+ * pos.x += vel.vx; // no ! needed
343
+ * });
344
+ * ```
345
+ */
346
+ query<T extends (typeof Component)[] = []>(q: SystemQuery, _guaranteed?: readonly [...T]): System<T>;
347
+ /**
348
+ * Shorthand for `query([...components])` — the system tracks entities that
349
+ * have **all** of the listed component types.
350
+ *
351
+ * Equivalent to `query({ HAS: components })`. Unlike `query`, passing
352
+ * component classes here also informs the types of {@link sort} and
353
+ * {@link each} callbacks: listed components will be non-nullable in those
354
+ * tuples.
355
+ *
356
+ * @param components - One or more component classes.
357
+ * @returns `this` for chaining.
358
+ */
359
+ requires<T extends (typeof Component)[]>(...components: [...T]): System<T>;
360
+ }
361
+ export {};