@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.
- package/.claude/settings.json +12 -0
- package/.devcontainer/devcontainer.json +22 -0
- package/.github/workflows/publish.yml +32 -0
- package/README.md +464 -0
- package/dist/component.d.ts +135 -0
- package/dist/component.js +101 -0
- package/dist/component.js.map +1 -0
- package/dist/entity.d.ts +157 -0
- package/dist/entity.js +199 -0
- package/dist/entity.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/package.json +25 -0
- package/dist/phase.d.ts +47 -0
- package/dist/phase.js +23 -0
- package/dist/phase.js.map +1 -0
- package/dist/system.d.ts +361 -0
- package/dist/system.js +396 -0
- package/dist/system.js.map +1 -0
- package/dist/util/array_map.d.ts +58 -0
- package/dist/util/array_map.js +84 -0
- package/dist/util/array_map.js.map +1 -0
- package/dist/util/bitset.d.ts +117 -0
- package/dist/util/bitset.js +177 -0
- package/dist/util/bitset.js.map +1 -0
- package/dist/util/events.d.ts +27 -0
- package/dist/util/events.js +43 -0
- package/dist/util/events.js.map +1 -0
- package/dist/util/ordered_set.d.ts +17 -0
- package/dist/util/ordered_set.js +69 -0
- package/dist/util/ordered_set.js.map +1 -0
- package/dist/world.d.ts +279 -0
- package/dist/world.js +453 -0
- package/dist/world.js.map +1 -0
- package/package.json +25 -0
- package/src/component.ts +180 -0
- package/src/entity.ts +276 -0
- package/src/index.ts +6 -0
- package/src/phase.ts +49 -0
- package/src/system.ts +693 -0
- package/src/util/array_map.ts +93 -0
- package/src/util/bitset.ts +199 -0
- package/src/util/events.ts +95 -0
- package/src/util/ordered_set.ts +82 -0
- package/src/world.ts +534 -0
- package/tests/_helpers.ts +30 -0
- package/tests/array_map.test.ts +68 -0
- package/tests/bitset.test.ts +127 -0
- package/tests/component.test.ts +104 -0
- package/tests/entity.test.ts +179 -0
- package/tests/events.test.ts +48 -0
- package/tests/ordered_set.test.ts +153 -0
- package/tests/setup.ts +6 -0
- package/tests/system.test.ts +800 -0
- package/tests/world.test.ts +174 -0
- package/tsconfig.json +21 -0
- package/vitest.config.ts +9 -0
package/dist/system.d.ts
ADDED
|
@@ -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 {};
|