@vworlds/vecs 1.0.0 → 1.0.1
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/dist/package.json +6 -2
- package/package.json +6 -2
- package/.claude/settings.json +0 -12
- package/.devcontainer/devcontainer.json +0 -22
- package/.github/workflows/publish.yml +0 -32
- package/src/component.ts +0 -180
- package/src/entity.ts +0 -276
- package/src/index.ts +0 -6
- package/src/phase.ts +0 -49
- package/src/system.ts +0 -693
- package/src/util/array_map.ts +0 -93
- package/src/util/bitset.ts +0 -199
- package/src/util/events.ts +0 -95
- package/src/util/ordered_set.ts +0 -82
- package/src/world.ts +0 -534
- package/tests/_helpers.ts +0 -30
- package/tests/array_map.test.ts +0 -68
- package/tests/bitset.test.ts +0 -127
- package/tests/component.test.ts +0 -104
- package/tests/entity.test.ts +0 -179
- package/tests/events.test.ts +0 -48
- package/tests/ordered_set.test.ts +0 -153
- package/tests/setup.ts +0 -6
- package/tests/system.test.ts +0 -800
- package/tests/world.test.ts +0 -174
- package/tsconfig.json +0 -21
- package/vitest.config.ts +0 -9
package/src/system.ts
DELETED
|
@@ -1,693 +0,0 @@
|
|
|
1
|
-
import { ArrayMap } from "./util/array_map.js";
|
|
2
|
-
import { Bitset } from "./util/bitset.js";
|
|
3
|
-
import { OrderedSet } from "./util/ordered_set.js";
|
|
4
|
-
import {
|
|
5
|
-
Component,
|
|
6
|
-
ComponentClassArray,
|
|
7
|
-
ComponentClassOrType,
|
|
8
|
-
calculateComponentBitmask,
|
|
9
|
-
} from "./component.js";
|
|
10
|
-
import type { Entity } from "./entity.js";
|
|
11
|
-
import { Phase, type IPhase } from "./phase.js";
|
|
12
|
-
import { type World } from "./world.js";
|
|
13
|
-
|
|
14
|
-
type EntityCallback = (e: Entity) => void;
|
|
15
|
-
type ComponentCallback = (c: Component) => void;
|
|
16
|
-
type RunCallback = (now: number, delta: number) => void;
|
|
17
|
-
|
|
18
|
-
/** A function that tests whether a given entity belongs to a system. */
|
|
19
|
-
export type EntityTestFunc = (e: Entity) => boolean;
|
|
20
|
-
|
|
21
|
-
type ComponentOrParent = typeof Component | { parent: typeof Component };
|
|
22
|
-
type ComponentOrParentType = number | { parent: number };
|
|
23
|
-
|
|
24
|
-
type ComponentInstance<T> = T extends { parent: typeof Component }
|
|
25
|
-
? InstanceType<T["parent"]>
|
|
26
|
-
: T extends typeof Component
|
|
27
|
-
? InstanceType<T>
|
|
28
|
-
: never;
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* A composable query expression used to declare which entities a
|
|
32
|
-
* {@link System} should track.
|
|
33
|
-
*
|
|
34
|
-
* Queries can be nested arbitrarily:
|
|
35
|
-
*
|
|
36
|
-
* ```ts
|
|
37
|
-
* // Entities that have Position AND (Sprite OR Container):
|
|
38
|
-
* world.system("render").query({
|
|
39
|
-
* AND: [Position, { OR: [Sprite, Container] }]
|
|
40
|
-
* });
|
|
41
|
-
*
|
|
42
|
-
* // Entities that have a parent with Player AND Container:
|
|
43
|
-
* world.system("attach").query({
|
|
44
|
-
* PARENT: { AND: [Player, Container] }
|
|
45
|
-
* });
|
|
46
|
-
* ```
|
|
47
|
-
*
|
|
48
|
-
* Short forms:
|
|
49
|
-
* - A single class or type id is equivalent to `{ HAS: [C] }`.
|
|
50
|
-
* - An array `[A, B]` is equivalent to `{ HAS: [A, B] }`.
|
|
51
|
-
* - Pass an {@link EntityTestFunc} directly for fully custom membership logic.
|
|
52
|
-
*/
|
|
53
|
-
export type SystemQuery =
|
|
54
|
-
| ComponentClassArray
|
|
55
|
-
| ComponentClassOrType
|
|
56
|
-
| EntityTestFunc
|
|
57
|
-
| { HAS: ComponentClassArray | ComponentClassOrType }
|
|
58
|
-
| { HAS_ONLY: ComponentClassArray | ComponentClassOrType }
|
|
59
|
-
| { AND: SystemQuery[] }
|
|
60
|
-
| { OR: SystemQuery[] }
|
|
61
|
-
| { NOT: SystemQuery }
|
|
62
|
-
| { PARENT: SystemQuery };
|
|
63
|
-
|
|
64
|
-
function HAS(world: World, ...components: ComponentClassArray): EntityTestFunc {
|
|
65
|
-
const testBitmask = calculateComponentBitmask(components, world);
|
|
66
|
-
return (e: Entity) => e.componentBitmask.hasBitset(testBitmask);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function HAS_ONLY(
|
|
70
|
-
world: World,
|
|
71
|
-
...components: ComponentClassArray
|
|
72
|
-
): EntityTestFunc {
|
|
73
|
-
const testBitmask = calculateComponentBitmask(components, world);
|
|
74
|
-
return (e: Entity) => e.componentBitmask.equal(testBitmask);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function NOT(func: EntityTestFunc): EntityTestFunc {
|
|
78
|
-
return (e: Entity) => !func(e);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function AND(...funcs: EntityTestFunc[]): EntityTestFunc {
|
|
82
|
-
return (e: Entity) => funcs.every((f) => f(e));
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function OR(...funcs: EntityTestFunc[]): EntityTestFunc {
|
|
86
|
-
return (e: Entity) => funcs.some((f) => f(e));
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function PARENT(func: EntityTestFunc) {
|
|
90
|
-
return (e: Entity) => (e.parent && func(e.parent)) || false;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* A reactive processor that operates on a filtered subset of world entities.
|
|
95
|
-
*
|
|
96
|
-
* Systems are created and registered through {@link World.system}:
|
|
97
|
-
*
|
|
98
|
-
* ```ts
|
|
99
|
-
* world.system("Move")
|
|
100
|
-
* .requires(Position, Velocity) // track entities with both components
|
|
101
|
-
* .phase("update")
|
|
102
|
-
* .enter([Position], (e, [pos]) => { pos.x = 0; })
|
|
103
|
-
* .update(Position, (pos) => { pos.x += pos.vx; })
|
|
104
|
-
* .exit((e) => { console.log("entity left", e.eid); });
|
|
105
|
-
* ```
|
|
106
|
-
*
|
|
107
|
-
* All builder methods return `this` for chaining. Call {@link World.start}
|
|
108
|
-
* once all systems are registered; after that, drive the loop with
|
|
109
|
-
* {@link World.runPhase}.
|
|
110
|
-
*
|
|
111
|
-
* ### Component injection and type inference
|
|
112
|
-
*
|
|
113
|
-
* `enter`, `exit`, `update`, `each`, and `sort` all accept an array of
|
|
114
|
-
* component classes that are resolved from the entity and passed as a typed
|
|
115
|
-
* tuple to the callback. Use `{ parent: SomeComponent }` to resolve from the
|
|
116
|
-
* entity's parent instead of the entity itself.
|
|
117
|
-
*
|
|
118
|
-
* Components declared via {@link requires} (or the second argument of
|
|
119
|
-
* {@link query}) are tracked as a type parameter `R` on the system. In
|
|
120
|
-
* `sort`, `each`, and `update` inject callbacks, those components appear as
|
|
121
|
-
* non-nullable; any component not in `R` remains `Type | undefined`.
|
|
122
|
-
*/
|
|
123
|
-
|
|
124
|
-
type MaybeRequired<C, R extends (typeof Component)[]> = C extends typeof Component
|
|
125
|
-
? C extends R[number]
|
|
126
|
-
? InstanceType<C>
|
|
127
|
-
: InstanceType<C> | undefined
|
|
128
|
-
: never;
|
|
129
|
-
|
|
130
|
-
const EMPTY_ENTITIES: ReadonlySet<Entity> = new Set();
|
|
131
|
-
|
|
132
|
-
export class System<R extends (typeof Component)[] = []> {
|
|
133
|
-
protected componentUpdateCallbacks = new ArrayMap<ComponentCallback>();
|
|
134
|
-
protected eachCallback: EntityCallback | undefined;
|
|
135
|
-
protected _entities: Set<Entity> | undefined;
|
|
136
|
-
protected _enterCallback: EntityCallback[] = [];
|
|
137
|
-
protected _exitCallback: EntityCallback[] = [];
|
|
138
|
-
private _runCallback: RunCallback | undefined;
|
|
139
|
-
protected _belongs: EntityTestFunc = (e: Entity) => false;
|
|
140
|
-
private readonly updateQueue: (Component | undefined)[] = [];
|
|
141
|
-
private hasQuery = false;
|
|
142
|
-
/** @internal */
|
|
143
|
-
public _phase: string | Phase | undefined;
|
|
144
|
-
|
|
145
|
-
protected watchlistBitmask: Bitset = new Bitset();
|
|
146
|
-
|
|
147
|
-
constructor(
|
|
148
|
-
/** Unique name for this system, used in logs and pipeline output. */
|
|
149
|
-
public readonly name: string,
|
|
150
|
-
/** The world that owns this system. */
|
|
151
|
-
public readonly world: World
|
|
152
|
-
) {}
|
|
153
|
-
|
|
154
|
-
/** Returns the system name. */
|
|
155
|
-
public toString(): string {
|
|
156
|
-
return this.name;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* Read-only view of the entities currently tracked by this system.
|
|
161
|
-
*
|
|
162
|
-
* Empty unless {@link track} (or {@link each}, which implies it) was
|
|
163
|
-
* called during system configuration.
|
|
164
|
-
*/
|
|
165
|
-
public get entities(): ReadonlySet<Entity> {
|
|
166
|
-
return this._entities ?? EMPTY_ENTITIES;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* Assign this system to a pipeline phase.
|
|
171
|
-
*
|
|
172
|
-
* The phase can be specified by name (the world will resolve it at
|
|
173
|
-
* {@link World.start | start} time) or by an {@link IPhase} reference
|
|
174
|
-
* returned from {@link World.addPhase}. Systems without an explicit phase
|
|
175
|
-
* are placed in the built-in `"update"` phase.
|
|
176
|
-
*
|
|
177
|
-
* @param p - Phase name or `IPhase` reference.
|
|
178
|
-
* @returns `this` for chaining.
|
|
179
|
-
*/
|
|
180
|
-
public phase(p: string | IPhase) {
|
|
181
|
-
if (typeof p !== "string") {
|
|
182
|
-
if (!(p instanceof Phase)) throw "Invalid Phase object";
|
|
183
|
-
if (p.world !== this.world)
|
|
184
|
-
throw "Phase does not belong to this system's world";
|
|
185
|
-
}
|
|
186
|
-
this._phase = p;
|
|
187
|
-
return this;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/** @internal Delivers a component-modified notification to this system. */
|
|
191
|
-
public notifyModified(c: Component) {
|
|
192
|
-
if (!this.watchlistBitmask.hasBit(c.bitPtr)) return;
|
|
193
|
-
this.updateQueue.push(c);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
/** Returns `true` if the entity satisfies this system's query. */
|
|
197
|
-
public belongs(e: Entity): boolean {
|
|
198
|
-
return this._belongs(e);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
/** @internal Fires `enter` callbacks for a newly matched entity. */
|
|
202
|
-
public _enter(e: Entity) {
|
|
203
|
-
this._enterCallback.forEach((callback) => callback(e));
|
|
204
|
-
e.forEachComponent((c) => this.notifyModified(c));
|
|
205
|
-
this._entities?.add(e);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/** @internal Fires `exit` callbacks when an entity leaves the system. */
|
|
209
|
-
public _exit(e: Entity) {
|
|
210
|
-
this._exitCallback.forEach((callback) => callback(e));
|
|
211
|
-
this._entities?.delete(e);
|
|
212
|
-
// remove queued updates for components of the exiting entity:
|
|
213
|
-
this.updateQueue.forEach((c, i) => {
|
|
214
|
-
if (!c) return;
|
|
215
|
-
if (c.entity === e) this.updateQueue[i] = undefined;
|
|
216
|
-
});
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
/** @internal Execute one tick: run `run`, fire `each`, then drain the update queue. */
|
|
220
|
-
public _run(now: number, delta: number) {
|
|
221
|
-
if (this._runCallback) this._runCallback(now, delta);
|
|
222
|
-
|
|
223
|
-
if (this.eachCallback) {
|
|
224
|
-
const cb = this.eachCallback;
|
|
225
|
-
this._entities?.forEach((e) => cb(e));
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
this.updateQueue.forEach((c) => {
|
|
229
|
-
if (!c) return;
|
|
230
|
-
const callback = this.componentUpdateCallbacks.get(c.type);
|
|
231
|
-
if (callback) {
|
|
232
|
-
callback(c);
|
|
233
|
-
}
|
|
234
|
-
});
|
|
235
|
-
this.updateQueue.length = 0;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
private getComponent(
|
|
239
|
-
e: Entity,
|
|
240
|
-
C: ComponentOrParentType,
|
|
241
|
-
considerDeleted: boolean
|
|
242
|
-
) {
|
|
243
|
-
let c: Component | undefined;
|
|
244
|
-
if (typeof C === "number") {
|
|
245
|
-
c = e.get(C, considerDeleted); // obtain an instance of C
|
|
246
|
-
} else {
|
|
247
|
-
c = e.parent && e.parent.get(C.parent, considerDeleted);
|
|
248
|
-
}
|
|
249
|
-
return c;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
private getInjected(
|
|
253
|
-
e: Entity,
|
|
254
|
-
inject: ComponentOrParentType[],
|
|
255
|
-
considerDeleted = false
|
|
256
|
-
) {
|
|
257
|
-
const injected: Component[] = [];
|
|
258
|
-
inject.forEach((C) => {
|
|
259
|
-
const c = this.getComponent(e, C, considerDeleted);
|
|
260
|
-
if (!c) throw "system does not contain component";
|
|
261
|
-
injected.push(c);
|
|
262
|
-
});
|
|
263
|
-
return injected;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
private mapInjectedClassToTypes<J extends ComponentOrParent[]>(
|
|
267
|
-
inject: readonly [...J]
|
|
268
|
-
): ComponentOrParentType[] {
|
|
269
|
-
//map injected class constructors to type numbers which are faster to search for later
|
|
270
|
-
return inject.map((C) => {
|
|
271
|
-
if (typeof C === "function") return this.world.getComponentType(C);
|
|
272
|
-
return { parent: this.world.getComponentType(C.parent) };
|
|
273
|
-
});
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
/**
|
|
277
|
-
* Register a callback that fires when an entity **enters** this system
|
|
278
|
-
* (i.e. first satisfies the system's query) with injected components.
|
|
279
|
-
*
|
|
280
|
-
* @param inject - Ordered list of component classes (or `{ parent: C }`) to
|
|
281
|
-
* resolve from the entering entity and pass to `callback`.
|
|
282
|
-
* @param callback - Receives the entity and the resolved component tuple.
|
|
283
|
-
* @returns `this` for chaining.
|
|
284
|
-
*
|
|
285
|
-
* @example
|
|
286
|
-
* ```ts
|
|
287
|
-
* system.enter([Position, Sprite], (e, [pos, sprite]) => {
|
|
288
|
-
* sprite.initialize(scene);
|
|
289
|
-
* sprite.sprite.setPosition(pos.x, pos.y);
|
|
290
|
-
* });
|
|
291
|
-
* ```
|
|
292
|
-
*/
|
|
293
|
-
public enter<J extends ComponentOrParent[]>(
|
|
294
|
-
inject: readonly [...J],
|
|
295
|
-
callback: (
|
|
296
|
-
e: Entity,
|
|
297
|
-
injected: { [K in keyof J]: ComponentInstance<J[K]> }
|
|
298
|
-
) => void
|
|
299
|
-
): this;
|
|
300
|
-
|
|
301
|
-
/**
|
|
302
|
-
* Register a callback that fires when an entity enters this system.
|
|
303
|
-
*
|
|
304
|
-
* @param callback - Receives only the entity (no injection).
|
|
305
|
-
* @returns `this` for chaining.
|
|
306
|
-
*/
|
|
307
|
-
public enter(callback: (e: Entity) => void): this;
|
|
308
|
-
|
|
309
|
-
// Implement the overloaded function
|
|
310
|
-
public enter<J extends ComponentOrParent[]>(
|
|
311
|
-
injectOrCallback: readonly [...J] | ((e: Entity) => void),
|
|
312
|
-
callback?: (
|
|
313
|
-
e: Entity,
|
|
314
|
-
injected: { [K in keyof J]: ComponentInstance<J[K]> }
|
|
315
|
-
) => void
|
|
316
|
-
): this {
|
|
317
|
-
if (typeof injectOrCallback === "function") {
|
|
318
|
-
// It is the second signature
|
|
319
|
-
this._enterCallback.push(injectOrCallback);
|
|
320
|
-
} else {
|
|
321
|
-
// It is the first signature
|
|
322
|
-
const inject = this.mapInjectedClassToTypes(injectOrCallback);
|
|
323
|
-
this._enterCallback.push((e: Entity) => {
|
|
324
|
-
callback!(e, this.getInjected(e, inject) as any);
|
|
325
|
-
});
|
|
326
|
-
}
|
|
327
|
-
return this;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
/**
|
|
331
|
-
* Register a callback that fires when an entity **exits** this system
|
|
332
|
-
* (its components no longer satisfy the query, or it was destroyed) with
|
|
333
|
-
* injected components.
|
|
334
|
-
*
|
|
335
|
-
* Components that were just removed are still accessible via `get_deleted`
|
|
336
|
-
* semantics — the injected tuple includes them even though they are no
|
|
337
|
-
* longer in the entity's active component set.
|
|
338
|
-
*
|
|
339
|
-
* @param inject - Component classes to resolve and inject.
|
|
340
|
-
* @param callback - Receives the entity and the resolved component tuple.
|
|
341
|
-
* @returns `this` for chaining.
|
|
342
|
-
*/
|
|
343
|
-
public exit<J extends ComponentOrParent[]>(
|
|
344
|
-
inject: readonly [...J],
|
|
345
|
-
callback: (
|
|
346
|
-
e: Entity,
|
|
347
|
-
injected: { [K in keyof J]: ComponentInstance<J[K]> }
|
|
348
|
-
) => void
|
|
349
|
-
): this;
|
|
350
|
-
|
|
351
|
-
/**
|
|
352
|
-
* Register a callback that fires when an entity exits this system.
|
|
353
|
-
*
|
|
354
|
-
* @param callback - Receives only the entity.
|
|
355
|
-
* @returns `this` for chaining.
|
|
356
|
-
*/
|
|
357
|
-
public exit(callback: (e: Entity) => void): this;
|
|
358
|
-
|
|
359
|
-
// Implement the overloaded function
|
|
360
|
-
public exit<J extends ComponentOrParent[]>(
|
|
361
|
-
injectOrCallback: readonly [...J] | ((e: Entity) => void),
|
|
362
|
-
callback?: (
|
|
363
|
-
e: Entity,
|
|
364
|
-
injected: { [K in keyof J]: ComponentInstance<J[K]> }
|
|
365
|
-
) => void
|
|
366
|
-
): this {
|
|
367
|
-
if (typeof injectOrCallback === "function") {
|
|
368
|
-
// It is the second signature
|
|
369
|
-
this._exitCallback.push(injectOrCallback);
|
|
370
|
-
} else {
|
|
371
|
-
// It is the first signature
|
|
372
|
-
const inject = this.mapInjectedClassToTypes(injectOrCallback);
|
|
373
|
-
this._exitCallback.push((e: Entity) => {
|
|
374
|
-
callback!(e, this.getInjected(e, inject, true) as any);
|
|
375
|
-
});
|
|
376
|
-
}
|
|
377
|
-
return this;
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
/**
|
|
381
|
-
* Register a per-tick callback that runs every time this system's phase
|
|
382
|
-
* executes, regardless of entity membership.
|
|
383
|
-
*
|
|
384
|
-
* Use this for logic that is not driven by component updates — polling,
|
|
385
|
-
* network flushing, global timers, etc.
|
|
386
|
-
*
|
|
387
|
-
* @param callback - Receives `now` (absolute timestamp in ms) and `delta`
|
|
388
|
-
* (ms since the last tick).
|
|
389
|
-
* @returns `this` for chaining.
|
|
390
|
-
*/
|
|
391
|
-
public run(callback: RunCallback): this {
|
|
392
|
-
this._runCallback = callback;
|
|
393
|
-
return this;
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
/**
|
|
397
|
-
* Register a callback that fires when a component of type `ComponentClass`
|
|
398
|
-
* is modified on any entity in this system.
|
|
399
|
-
*
|
|
400
|
-
* The system will automatically begin tracking entities that have this
|
|
401
|
-
* component type (equivalent to adding it to a `requires` / `HAS` query)
|
|
402
|
-
* unless a custom {@link query} was already set.
|
|
403
|
-
*
|
|
404
|
-
* @param ComponentClass - The component class to watch.
|
|
405
|
-
* @param callback - Receives the modified component instance.
|
|
406
|
-
* @returns `this` for chaining.
|
|
407
|
-
*
|
|
408
|
-
* @example
|
|
409
|
-
* ```ts
|
|
410
|
-
* world.system("RenderPosition")
|
|
411
|
-
* .update(Position, (pos) => {
|
|
412
|
-
* sprite.setPosition(pos.x, pos.y);
|
|
413
|
-
* });
|
|
414
|
-
* ```
|
|
415
|
-
*/
|
|
416
|
-
public update<C extends typeof Component>(
|
|
417
|
-
ComponentClass: C,
|
|
418
|
-
callback: (c: InstanceType<C>) => void
|
|
419
|
-
): this;
|
|
420
|
-
|
|
421
|
-
/**
|
|
422
|
-
* Register a callback that fires when `ComponentClass` is modified, with
|
|
423
|
-
* additional components injected from the same entity.
|
|
424
|
-
*
|
|
425
|
-
* @param ComponentClass - The component class to watch.
|
|
426
|
-
* @param inject - Additional component classes to resolve from the entity.
|
|
427
|
-
* @param callback - Receives the modified component and the injected tuple.
|
|
428
|
-
* @returns `this` for chaining.
|
|
429
|
-
*
|
|
430
|
-
* @example
|
|
431
|
-
* ```ts
|
|
432
|
-
* world.system("SyncSprite")
|
|
433
|
-
* .update(Position, [Sprite], (pos, [sprite]) => {
|
|
434
|
-
* sprite.sprite.setPosition(pos.x, pos.y);
|
|
435
|
-
* });
|
|
436
|
-
* ```
|
|
437
|
-
*/
|
|
438
|
-
update<C extends typeof Component, J extends (typeof Component)[]>(
|
|
439
|
-
ComponentClass: C,
|
|
440
|
-
inject: readonly [...J],
|
|
441
|
-
callback: (
|
|
442
|
-
c: InstanceType<C>,
|
|
443
|
-
injected: { [K in keyof J]: MaybeRequired<J[K], R> }
|
|
444
|
-
) => void
|
|
445
|
-
): this;
|
|
446
|
-
|
|
447
|
-
update<C extends typeof Component, J extends (typeof Component)[]>(
|
|
448
|
-
ComponentClass: C,
|
|
449
|
-
injectOrCallback: readonly [...J] | ((c: InstanceType<C>) => void),
|
|
450
|
-
callback?: (
|
|
451
|
-
c: InstanceType<C>,
|
|
452
|
-
injected: { [K in keyof J]: MaybeRequired<J[K], R> }
|
|
453
|
-
) => void
|
|
454
|
-
): this {
|
|
455
|
-
const type = this.world.getComponentType(ComponentClass);
|
|
456
|
-
if (typeof injectOrCallback === "function") {
|
|
457
|
-
// Only ComponentClass and callback are passed
|
|
458
|
-
callback = injectOrCallback;
|
|
459
|
-
this.componentUpdateCallbacks.set(type, callback as any);
|
|
460
|
-
} else {
|
|
461
|
-
// ComponentClass, inject, and callback are passed
|
|
462
|
-
const inject = injectOrCallback;
|
|
463
|
-
//map injected class constructors to component type numbers which are faster to search for later
|
|
464
|
-
const injectedComponentTypes = inject.map((C) =>
|
|
465
|
-
this.world.getComponentType(C)
|
|
466
|
-
);
|
|
467
|
-
const cb = (c: Component) => {
|
|
468
|
-
const injected: any[] = [];
|
|
469
|
-
injectedComponentTypes.forEach((InjectedComponentType) => {
|
|
470
|
-
injected.push(c.entity.get(InjectedComponentType));
|
|
471
|
-
});
|
|
472
|
-
|
|
473
|
-
if (callback) {
|
|
474
|
-
callback(c as InstanceType<C>, injected as any);
|
|
475
|
-
}
|
|
476
|
-
};
|
|
477
|
-
|
|
478
|
-
this.componentUpdateCallbacks.set(type, cb);
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
this.watchlistBitmask.add(type);
|
|
482
|
-
|
|
483
|
-
if (!this.hasQuery) {
|
|
484
|
-
const watchlist: number[] = this.watchlistBitmask.indices();
|
|
485
|
-
this._belongs = HAS(this.world, ...watchlist);
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
return this;
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
/**
|
|
492
|
-
* Register a callback that fires **every tick** for every entity currently
|
|
493
|
-
* tracked by this system, with the listed components resolved from each
|
|
494
|
-
* entity.
|
|
495
|
-
*
|
|
496
|
-
* Unlike {@link update} (which only fires when `component.modified()` is
|
|
497
|
-
* called), `each` fires unconditionally on every tick the system runs,
|
|
498
|
-
* once per tracked entity. Components declared via {@link requires} are
|
|
499
|
-
* guaranteed non-null in the resolved tuple; any other component class
|
|
500
|
-
* may be `undefined` if the entity lacks it.
|
|
501
|
-
*
|
|
502
|
-
* `each` does **not** modify the system's query — define membership with
|
|
503
|
-
* {@link requires} or {@link query} as usual. It does, however, implicitly
|
|
504
|
-
* enable {@link track}, so matched entities are exposed via {@link entities}.
|
|
505
|
-
*
|
|
506
|
-
* Only a single `each` callback may be registered per system; calling
|
|
507
|
-
* `each` a second time throws.
|
|
508
|
-
*
|
|
509
|
-
* @param components - Component classes to resolve from each entity.
|
|
510
|
-
* @param callback - Receives the entity and a tuple of resolved component
|
|
511
|
-
* instances (`undefined` for components not covered by {@link requires}).
|
|
512
|
-
* @returns `this` for chaining.
|
|
513
|
-
* @throws If `each` has already been registered on this system.
|
|
514
|
-
*
|
|
515
|
-
* @example
|
|
516
|
-
* ```ts
|
|
517
|
-
* world.system("Move")
|
|
518
|
-
* .requires(Position, Velocity)
|
|
519
|
-
* .each([Position, Velocity], (e, [pos, vel]) => {
|
|
520
|
-
* pos.x += vel.vx;
|
|
521
|
-
* pos.y += vel.vy;
|
|
522
|
-
* });
|
|
523
|
-
* ```
|
|
524
|
-
*/
|
|
525
|
-
public each<J extends (typeof Component)[]>(
|
|
526
|
-
components: readonly [...J],
|
|
527
|
-
callback: (
|
|
528
|
-
e: Entity,
|
|
529
|
-
resolved: { [K in keyof J]: MaybeRequired<J[K], R> }
|
|
530
|
-
) => void
|
|
531
|
-
): this {
|
|
532
|
-
if (this.eachCallback) {
|
|
533
|
-
throw `each already registered for system '${this.name}'`;
|
|
534
|
-
}
|
|
535
|
-
this.track();
|
|
536
|
-
const types = components.map((C) => this.world.getComponentType(C));
|
|
537
|
-
this.eachCallback = (e: Entity) => {
|
|
538
|
-
const resolved = types.map((t) => e.get(t));
|
|
539
|
-
callback(e, resolved as any);
|
|
540
|
-
};
|
|
541
|
-
return this;
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
/**
|
|
545
|
-
* Enable entity tracking: matched entities are inserted into
|
|
546
|
-
* {@link entities} as they enter the system and removed as they exit.
|
|
547
|
-
*
|
|
548
|
-
* Idempotent. Intended to be called during system configuration before
|
|
549
|
-
* `world.start()`; entities already matched when `track` is called late
|
|
550
|
-
* will not be backfilled.
|
|
551
|
-
*
|
|
552
|
-
* {@link each} implies `track` — call this directly only when you want
|
|
553
|
-
* the tracked set without an `each` callback.
|
|
554
|
-
*
|
|
555
|
-
* @returns `this` for chaining.
|
|
556
|
-
*/
|
|
557
|
-
public track(): this {
|
|
558
|
-
this._entities ??= new Set<Entity>();
|
|
559
|
-
return this;
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
/**
|
|
563
|
-
* Enable sorted entity tracking: matched entities are stored in insertion
|
|
564
|
-
* order determined by `compare`, which receives a tuple of resolved
|
|
565
|
-
* component instances for each pair of entities being ordered.
|
|
566
|
-
*
|
|
567
|
-
* Implies {@link track}.
|
|
568
|
-
*
|
|
569
|
-
* @param components - Component classes to resolve and pass to `compare`.
|
|
570
|
-
* @param compare - Returns a negative number, zero, or positive number when
|
|
571
|
-
* `a` should sort before, equal to, or after `b`.
|
|
572
|
-
* @returns `this` for chaining.
|
|
573
|
-
*
|
|
574
|
-
* @example
|
|
575
|
-
* ```ts
|
|
576
|
-
* world.system("Render")
|
|
577
|
-
* .requires(Position, Sprite)
|
|
578
|
-
* .sort([Position], ([posA], [posB]) => posA.z - posB.z);
|
|
579
|
-
* ```
|
|
580
|
-
*/
|
|
581
|
-
public sort<J extends (typeof Component)[]>(
|
|
582
|
-
components: readonly [...J],
|
|
583
|
-
compare: (
|
|
584
|
-
a: { [K in keyof J]: MaybeRequired<J[K], R> },
|
|
585
|
-
b: { [K in keyof J]: MaybeRequired<J[K], R> }
|
|
586
|
-
) => number
|
|
587
|
-
): this {
|
|
588
|
-
const types = components.map((C) => this.world.getComponentType(C));
|
|
589
|
-
this._entities = new OrderedSet<Entity>((a, b) =>
|
|
590
|
-
compare(
|
|
591
|
-
types.map((t) => a.get(t, true)) as any,
|
|
592
|
-
types.map((t) => b.get(t, true)) as any
|
|
593
|
-
)
|
|
594
|
-
);
|
|
595
|
-
return this;
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
private queryBuilder(q: SystemQuery): EntityTestFunc {
|
|
599
|
-
if (
|
|
600
|
-
typeof q === "number" ||
|
|
601
|
-
(typeof q === "function" && q.prototype instanceof Component)
|
|
602
|
-
) {
|
|
603
|
-
return HAS(this.world, q as typeof Component);
|
|
604
|
-
} else if (typeof q === "function") {
|
|
605
|
-
return q as EntityTestFunc;
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
if (q instanceof Array) {
|
|
609
|
-
return HAS(this.world, ...q);
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
if ("HAS" in q) {
|
|
613
|
-
return this.queryBuilder(q.HAS);
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
if ("HAS_ONLY" in q) {
|
|
617
|
-
const v = q.HAS_ONLY;
|
|
618
|
-
if (v instanceof Array) {
|
|
619
|
-
return HAS_ONLY(this.world, ...v);
|
|
620
|
-
}
|
|
621
|
-
return HAS_ONLY(this.world, v);
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
if ("AND" in q) {
|
|
625
|
-
return AND(...q.AND.map((sq) => this.queryBuilder(sq)));
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
if ("OR" in q) {
|
|
629
|
-
return OR(...q.OR.map((sq) => this.queryBuilder(sq)));
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
if ("NOT" in q) {
|
|
633
|
-
return NOT(this.queryBuilder(q.NOT));
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
if ("PARENT" in q) {
|
|
637
|
-
return PARENT(this.queryBuilder(q.PARENT));
|
|
638
|
-
}
|
|
639
|
-
throw "Unrecognized query term";
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
/**
|
|
643
|
-
* Set the entity membership predicate using the {@link SystemQuery} DSL.
|
|
644
|
-
*
|
|
645
|
-
* Replaces any implicit query derived from `update` watchlists and any
|
|
646
|
-
* previous `requires` call. After calling `query`, auto-expanding of
|
|
647
|
-
* `update` watchlists is disabled.
|
|
648
|
-
*
|
|
649
|
-
* The optional `guaranteed` tuple is a pure type-level hint: it tells
|
|
650
|
-
* `sort`, `each`, and `update` callbacks which components are guaranteed
|
|
651
|
-
* to be present on every matched entity, eliminating `| undefined` from
|
|
652
|
-
* those positions. It has no effect at runtime.
|
|
653
|
-
*
|
|
654
|
-
* @param q - A {@link SystemQuery} expression.
|
|
655
|
-
* @param _guaranteed - Component classes guaranteed present on every matched
|
|
656
|
-
* entity (type hint only — not validated at runtime).
|
|
657
|
-
* @returns `this` for chaining.
|
|
658
|
-
*
|
|
659
|
-
* @example
|
|
660
|
-
* ```ts
|
|
661
|
-
* world.system("Move")
|
|
662
|
-
* .query({ AND: [{ HAS: Position }, { HAS: Velocity }] }, [Position, Velocity])
|
|
663
|
-
* .each([Position, Velocity], (e, [pos, vel]) => {
|
|
664
|
-
* pos.x += vel.vx; // no ! needed
|
|
665
|
-
* });
|
|
666
|
-
* ```
|
|
667
|
-
*/
|
|
668
|
-
public query<T extends (typeof Component)[] = []>(
|
|
669
|
-
q: SystemQuery,
|
|
670
|
-
_guaranteed?: readonly [...T]
|
|
671
|
-
): System<T> {
|
|
672
|
-
this._belongs = this.queryBuilder(q);
|
|
673
|
-
this.hasQuery = true;
|
|
674
|
-
return this as unknown as System<T>;
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
/**
|
|
678
|
-
* Shorthand for `query([...components])` — the system tracks entities that
|
|
679
|
-
* have **all** of the listed component types.
|
|
680
|
-
*
|
|
681
|
-
* Equivalent to `query({ HAS: components })`. Unlike `query`, passing
|
|
682
|
-
* component classes here also informs the types of {@link sort} and
|
|
683
|
-
* {@link each} callbacks: listed components will be non-nullable in those
|
|
684
|
-
* tuples.
|
|
685
|
-
*
|
|
686
|
-
* @param components - One or more component classes.
|
|
687
|
-
* @returns `this` for chaining.
|
|
688
|
-
*/
|
|
689
|
-
public requires<T extends (typeof Component)[]>(...components: [...T]): System<T> {
|
|
690
|
-
this.query(components);
|
|
691
|
-
return this as unknown as System<T>;
|
|
692
|
-
}
|
|
693
|
-
}
|