@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.
@@ -1,800 +0,0 @@
1
- import { describe, it, expect, vi } from "vitest";
2
- import { World, Component } from "../src/index.js";
3
-
4
- class Position extends Component {
5
- x = 0;
6
- y = 0;
7
- }
8
- class Velocity extends Component {
9
- vx = 0;
10
- vy = 0;
11
- }
12
- class Sprite extends Component {}
13
- class Container extends Component {}
14
- class Player extends Component {}
15
-
16
- function setup() {
17
- const w = new World();
18
- w.registerComponent(Position);
19
- w.registerComponent(Velocity);
20
- w.registerComponent(Sprite);
21
- w.registerComponent(Container);
22
- w.registerComponent(Player);
23
- const phase = w.addPhase("p");
24
- return { w, phase };
25
- }
26
-
27
- describe("System — enter / exit / update", () => {
28
- it("enter fires when an entity first matches the query", () => {
29
- const { w, phase } = setup();
30
- const enter = vi.fn();
31
- w.system("test").phase(phase).requires(Position).enter(enter);
32
- w.start();
33
- const e = w.createEntity();
34
- e.add(Position);
35
- w.runPhase(phase, 0, 0);
36
- expect(enter).toHaveBeenCalledWith(e);
37
- });
38
-
39
- it("enter with injection receives a typed tuple", () => {
40
- const { w, phase } = setup();
41
- const cb = vi.fn();
42
- w.system("test")
43
- .phase(phase)
44
- .requires(Position, Velocity)
45
- .enter([Position, Velocity], cb);
46
- w.start();
47
- const e = w.createEntity();
48
- const pos = e.add(Position);
49
- const vel = e.add(Velocity);
50
- w.runPhase(phase, 0, 0);
51
- expect(cb).toHaveBeenCalledWith(e, [pos, vel]);
52
- });
53
-
54
- it("exit fires when an entity stops matching", () => {
55
- const { w, phase } = setup();
56
- const exit = vi.fn();
57
- w.system("test").phase(phase).requires(Position).exit(exit);
58
- w.start();
59
- const e = w.createEntity();
60
- e.add(Position);
61
- w.runPhase(phase, 0, 0);
62
- e.remove(Position);
63
- w.runPhase(phase, 0, 0);
64
- expect(exit).toHaveBeenCalledWith(e);
65
- });
66
-
67
- it("exit injection includes recently-removed components", () => {
68
- const { w, phase } = setup();
69
- const cb = vi.fn();
70
- w.system("test").phase(phase).requires(Position).exit([Position], cb);
71
- w.start();
72
- const e = w.createEntity();
73
- const pos = e.add(Position);
74
- w.runPhase(phase, 0, 0);
75
- e.remove(Position);
76
- w.runPhase(phase, 0, 0);
77
- expect(cb).toHaveBeenCalledWith(e, [pos]);
78
- });
79
-
80
- it("update fires on component.modified()", () => {
81
- const { w, phase } = setup();
82
- const cb = vi.fn();
83
- w.system("test").phase(phase).requires(Position).update(Position, cb);
84
- w.start();
85
- const e = w.createEntity();
86
- const pos = e.add(Position, false);
87
- w.runPhase(phase, 0, 0); // entity entered
88
- cb.mockClear();
89
- pos.modified();
90
- w.runPhase(phase, 0, 0);
91
- expect(cb).toHaveBeenCalledWith(pos);
92
- });
93
-
94
- it("enter delivers an initial update for components already on the entity", () => {
95
- const { w, phase } = setup();
96
- const cb = vi.fn();
97
- w.system("test").phase(phase).requires(Position).update(Position, cb);
98
- w.start();
99
- const e = w.createEntity();
100
- const pos = e.add(Position);
101
- w.runPhase(phase, 0, 0); // tick 1 enters the entity & queues pos
102
- w.runPhase(phase, 0, 0); // tick 2 drains the queue
103
- expect(cb).toHaveBeenCalledWith(pos);
104
- });
105
-
106
- it("update with injection delivers extra components", () => {
107
- const { w, phase } = setup();
108
- const cb = vi.fn();
109
- w.system("test")
110
- .phase(phase)
111
- .requires(Position, Velocity)
112
- .update(Velocity, [Position], cb);
113
- w.start();
114
- const e = w.createEntity();
115
- const pos = e.add(Position);
116
- const vel = e.add(Velocity, false);
117
- w.runPhase(phase, 0, 0);
118
- cb.mockClear();
119
- vel.modified();
120
- w.runPhase(phase, 0, 0);
121
- expect(cb).toHaveBeenCalledWith(vel, [pos]);
122
- });
123
-
124
- it("update without an explicit query implicitly requires its component", () => {
125
- const { w, phase } = setup();
126
- const cb = vi.fn();
127
- w.system("test").phase(phase).update(Position, cb);
128
- w.start();
129
- const e = w.createEntity();
130
- const pos = e.add(Position);
131
- w.runPhase(phase, 0, 0); // enter
132
- w.runPhase(phase, 0, 0); // drain
133
- expect(cb).toHaveBeenCalledWith(pos);
134
-
135
- // Entity without Position must not match.
136
- const f = w.createEntity();
137
- f.add(Velocity);
138
- w.runPhase(phase, 0, 0);
139
- w.runPhase(phase, 0, 0);
140
- expect(cb).toHaveBeenCalledTimes(1);
141
- });
142
-
143
- it("run fires every tick regardless of entity state", () => {
144
- const { w, phase } = setup();
145
- const cb = vi.fn();
146
- w.system("test").phase(phase).run(cb);
147
- w.start();
148
- w.runPhase(phase, 100, 16);
149
- w.runPhase(phase, 116, 16);
150
- expect(cb).toHaveBeenCalledTimes(2);
151
- expect(cb).toHaveBeenLastCalledWith(116, 16);
152
- });
153
-
154
- it("run returns the system for chaining", () => {
155
- const { w } = setup();
156
- const sys = w.system("test");
157
- expect(sys.run(() => {})).toBe(sys);
158
- });
159
-
160
- it("toString returns the system name", () => {
161
- const { w } = setup();
162
- const sys = w.system("Move");
163
- expect(sys.toString()).toBe("Move");
164
- });
165
-
166
- it("destroyed entity also fires exit", () => {
167
- const { w, phase } = setup();
168
- const exit = vi.fn();
169
- w.system("test").phase(phase).requires(Position).exit(exit);
170
- w.start();
171
- const e = w.createEntity();
172
- e.add(Position);
173
- w.runPhase(phase, 0, 0);
174
- e.destroy();
175
- w.runPhase(phase, 0, 0);
176
- expect(exit).toHaveBeenCalledWith(e);
177
- });
178
- });
179
-
180
- describe("System — query DSL", () => {
181
- it("HAS / requires matches entities that have all components", () => {
182
- const { w, phase } = setup();
183
- const enter = vi.fn();
184
- w.system("test").phase(phase).requires(Position, Velocity).enter(enter);
185
- w.start();
186
-
187
- const a = w.createEntity();
188
- a.add(Position);
189
- const b = w.createEntity();
190
- b.add(Position);
191
- b.add(Velocity);
192
- w.runPhase(phase, 0, 0);
193
- expect(enter).toHaveBeenCalledTimes(1);
194
- expect(enter).toHaveBeenCalledWith(b);
195
- });
196
-
197
- it("HAS_ONLY matches entities with exactly that component set", () => {
198
- const { w, phase } = setup();
199
- const enter = vi.fn();
200
- w.system("test").phase(phase).query({ HAS_ONLY: [Position] }).enter(enter);
201
- w.start();
202
-
203
- const a = w.createEntity();
204
- a.add(Position);
205
- const b = w.createEntity();
206
- b.add(Position);
207
- b.add(Velocity);
208
- w.runPhase(phase, 0, 0);
209
- expect(enter).toHaveBeenCalledTimes(1);
210
- expect(enter).toHaveBeenCalledWith(a);
211
- });
212
-
213
- it("AND combines sub-queries", () => {
214
- const { w, phase } = setup();
215
- const enter = vi.fn();
216
- w.system("test").phase(phase).query({ AND: [Position, Velocity] }).enter(enter);
217
- w.start();
218
-
219
- const a = w.createEntity();
220
- a.add(Position);
221
- const b = w.createEntity();
222
- b.add(Position);
223
- b.add(Velocity);
224
- w.runPhase(phase, 0, 0);
225
- expect(enter).toHaveBeenCalledTimes(1);
226
- expect(enter).toHaveBeenCalledWith(b);
227
- });
228
-
229
- it("OR matches either branch", () => {
230
- const { w, phase } = setup();
231
- const enter = vi.fn();
232
- w.system("test").phase(phase).query({ OR: [Sprite, Container] }).enter(enter);
233
- w.start();
234
-
235
- const a = w.createEntity();
236
- a.add(Sprite);
237
- const b = w.createEntity();
238
- b.add(Container);
239
- const c = w.createEntity();
240
- c.add(Position);
241
- w.runPhase(phase, 0, 0);
242
- expect(enter).toHaveBeenCalledTimes(2);
243
- });
244
-
245
- it("NOT inverts a sub-query", () => {
246
- const { w, phase } = setup();
247
- const matches: number[] = [];
248
- w.system("test")
249
- .phase(phase)
250
- .query({ AND: [Position, { NOT: Velocity }] })
251
- .enter((e) => matches.push(e.eid));
252
- w.start();
253
-
254
- const a = w.createEntity();
255
- a.add(Position);
256
- const b = w.createEntity();
257
- b.add(Position);
258
- b.add(Velocity);
259
- w.runPhase(phase, 0, 0);
260
- expect(matches).toEqual([a.eid]);
261
- });
262
-
263
- it("PARENT looks up the entity's parent", () => {
264
- const { w, phase } = setup();
265
- const cb = vi.fn();
266
- w.system("test")
267
- .phase(phase)
268
- .query({ PARENT: { AND: [Player, Container] } })
269
- .enter(cb);
270
- w.start();
271
-
272
- const parent = w.createEntity();
273
- parent.add(Player);
274
- parent.add(Container);
275
-
276
- const child = w.createEntity();
277
- child.parent = parent;
278
- parent.children.add(child);
279
- child.add(Position); // any component, just so its archetype fires once
280
-
281
- w.runPhase(phase, 0, 0);
282
- expect(cb).toHaveBeenCalledWith(child);
283
- });
284
-
285
- it("an EntityTestFunc can be passed directly", () => {
286
- const { w, phase } = setup();
287
- const cb = vi.fn();
288
- w.system("test").phase(phase).query((e) => e.eid === 7).enter(cb);
289
- w.start();
290
-
291
- w.createEntity(); // 0
292
- const seven = w.getOrCreateEntity(7);
293
- seven.add(Position); // trigger archetype change
294
- w.runPhase(phase, 0, 0);
295
- expect(cb).toHaveBeenCalledWith(seven);
296
- });
297
-
298
- it("a single class is shorthand for HAS", () => {
299
- const { w, phase } = setup();
300
- const cb = vi.fn();
301
- w.system("test").phase(phase).query(Position).enter(cb);
302
- w.start();
303
-
304
- const e = w.createEntity();
305
- e.add(Position);
306
- w.runPhase(phase, 0, 0);
307
- expect(cb).toHaveBeenCalledWith(e);
308
- });
309
-
310
- it("an array is shorthand for HAS", () => {
311
- const { w, phase } = setup();
312
- const cb = vi.fn();
313
- w.system("test").phase(phase).query([Position, Velocity]).enter(cb);
314
- w.start();
315
-
316
- const a = w.createEntity();
317
- a.add(Position);
318
- const b = w.createEntity();
319
- b.add(Position);
320
- b.add(Velocity);
321
- w.runPhase(phase, 0, 0);
322
- expect(cb).toHaveBeenCalledWith(b);
323
- expect(cb).not.toHaveBeenCalledWith(a);
324
- });
325
-
326
- it("query overrides any prior implicit watchlist query", () => {
327
- const { w, phase } = setup();
328
- const cb = vi.fn();
329
- // update would normally add Position to the implicit query;
330
- // a subsequent query() should fully replace it.
331
- w.system("test")
332
- .phase(phase)
333
- .update(Position, () => {})
334
- .query(Velocity)
335
- .enter(cb);
336
- w.start();
337
-
338
- const e = w.createEntity();
339
- e.add(Position); // would have matched the implicit query
340
- w.runPhase(phase, 0, 0);
341
- expect(cb).not.toHaveBeenCalled();
342
-
343
- e.add(Velocity);
344
- w.runPhase(phase, 0, 0);
345
- expect(cb).toHaveBeenCalledWith(e);
346
- });
347
-
348
- it("explicit { HAS: ... } is equivalent to requires", () => {
349
- const { w, phase } = setup();
350
- const cb = vi.fn();
351
- w.system("test").phase(phase).query({ HAS: [Position, Velocity] }).enter(cb);
352
- w.start();
353
- const e = w.createEntity();
354
- e.add(Position);
355
- e.add(Velocity);
356
- w.runPhase(phase, 0, 0);
357
- expect(cb).toHaveBeenCalledWith(e);
358
- });
359
- });
360
-
361
- describe("System — phases", () => {
362
- it("phase by IPhase reference assigns the system to that phase", () => {
363
- const w = new World();
364
- const seen: string[] = [];
365
- const a = w.addPhase("a");
366
- const b = w.addPhase("b");
367
- w.system("sysA").phase(a).run(() => seen.push("A"));
368
- w.system("sysB").phase(b).run(() => seen.push("B"));
369
- w.start();
370
- w.runPhase(b, 0, 0);
371
- w.runPhase(a, 0, 0);
372
- expect(seen).toEqual(["B", "A"]);
373
- });
374
-
375
- it("phase by name resolves at start() time", () => {
376
- const w = new World();
377
- const cb = vi.fn();
378
- w.addPhase("custom");
379
- w.system("test").phase("custom").run(cb);
380
- w.start();
381
- const custom = [...w["pipeline"].values()].find((p) => p.name === "custom")!;
382
- w.runPhase(custom, 0, 0);
383
- expect(cb).toHaveBeenCalled();
384
- });
385
-
386
- it("systems without a phase land in the built-in 'update' phase", () => {
387
- const w = new World();
388
- const cb = vi.fn();
389
- w.system("test").run(cb);
390
- w.start();
391
- const update = [...w["pipeline"].values()].find((p) => p.name === "update")!;
392
- w.runPhase(update, 0, 0);
393
- expect(cb).toHaveBeenCalled();
394
- });
395
-
396
- it("phase rejects a Phase from another world", () => {
397
- const w = new World();
398
- const other = new World();
399
- const otherPhase = other.addPhase("x");
400
- expect(() => w.system("test").phase(otherPhase)).toThrow();
401
- });
402
-
403
- it("phase rejects a non-Phase object", () => {
404
- const w = new World();
405
- expect(() =>
406
- w.system("test").phase({ name: "fake", world: w } as any)
407
- ).toThrow();
408
- });
409
- });
410
-
411
- describe("System — registration timing", () => {
412
- it("system registration is disabled after start()", () => {
413
- const w = new World();
414
- w.start();
415
- expect(() => w.system("late")).toThrow();
416
- });
417
- });
418
-
419
- describe("System — each", () => {
420
- it("each fires every tick for a tracked entity", () => {
421
- const { w, phase } = setup();
422
- const cb = vi.fn();
423
- w.system("test").phase(phase).requires(Position).each([Position], cb);
424
- w.start();
425
- const e = w.createEntity();
426
- const pos = e.add(Position);
427
- w.runPhase(phase, 0, 0); // entry happens after run() in updateArchetypes
428
- w.runPhase(phase, 0, 0); // first each
429
- expect(cb).toHaveBeenCalledWith(e, [pos]);
430
- });
431
-
432
- it("each fires across multiple ticks", () => {
433
- const { w, phase } = setup();
434
- const cb = vi.fn();
435
- w.system("test").phase(phase).requires(Position).each([Position], cb);
436
- w.start();
437
- const e = w.createEntity();
438
- e.add(Position);
439
- w.runPhase(phase, 0, 0);
440
- w.runPhase(phase, 0, 0);
441
- w.runPhase(phase, 0, 0);
442
- w.runPhase(phase, 0, 0);
443
- expect(cb).toHaveBeenCalledTimes(3);
444
- });
445
-
446
- it("each fires for every matching entity in one tick", () => {
447
- const { w, phase } = setup();
448
- const cb = vi.fn();
449
- w.system("test").phase(phase).requires(Position).each([Position], cb);
450
- w.start();
451
- const a = w.createEntity();
452
- const posA = a.add(Position);
453
- const b = w.createEntity();
454
- const posB = b.add(Position);
455
- w.runPhase(phase, 0, 0);
456
- w.runPhase(phase, 0, 0);
457
- expect(cb).toHaveBeenCalledWith(a, [posA]);
458
- expect(cb).toHaveBeenCalledWith(b, [posB]);
459
- expect(cb).toHaveBeenCalledTimes(2);
460
- });
461
-
462
- it("each is not called for entities that don't match the system query", () => {
463
- const { w, phase } = setup();
464
- const cb = vi.fn();
465
- w.system("test").phase(phase).requires(Position).each([Position], cb);
466
- w.start();
467
- const a = w.createEntity();
468
- a.add(Position);
469
- const b = w.createEntity();
470
- b.add(Velocity);
471
- w.runPhase(phase, 0, 0);
472
- w.runPhase(phase, 0, 0);
473
- expect(cb).toHaveBeenCalledTimes(1);
474
- expect(cb).toHaveBeenCalledWith(a, [a.get(Position)]);
475
- });
476
-
477
- it("missing components on a tracked entity are passed as undefined", () => {
478
- const { w, phase } = setup();
479
- const cb = vi.fn();
480
- // Entity matches via Position only, but each also asks for Velocity:
481
- w.system("test")
482
- .phase(phase)
483
- .requires(Position)
484
- .each([Position, Velocity], cb);
485
- w.start();
486
- const e = w.createEntity();
487
- const pos = e.add(Position);
488
- w.runPhase(phase, 0, 0);
489
- w.runPhase(phase, 0, 0);
490
- expect(cb).toHaveBeenCalledWith(e, [pos, undefined]);
491
- });
492
-
493
- it("each stops firing after entity component is removed", () => {
494
- const { w, phase } = setup();
495
- const cb = vi.fn();
496
- w.system("test").phase(phase).requires(Position).each([Position], cb);
497
- w.start();
498
- const e = w.createEntity();
499
- e.add(Position);
500
- w.runPhase(phase, 0, 0);
501
- w.runPhase(phase, 0, 0);
502
- e.remove(Position);
503
- // Entity exits during updateArchetypes after run() in the next tick.
504
- w.runPhase(phase, 0, 0);
505
- w.runPhase(phase, 0, 0);
506
- const callsBefore = cb.mock.calls.length;
507
- w.runPhase(phase, 0, 0);
508
- expect(cb).toHaveBeenCalledTimes(callsBefore);
509
- });
510
-
511
- it("each stops firing after entity is destroyed", () => {
512
- const { w, phase } = setup();
513
- const cb = vi.fn();
514
- w.system("test").phase(phase).requires(Position).each([Position], cb);
515
- w.start();
516
- const e = w.createEntity();
517
- e.add(Position);
518
- w.runPhase(phase, 0, 0);
519
- w.runPhase(phase, 0, 0);
520
- e.destroy();
521
- w.runPhase(phase, 0, 0);
522
- const countAfterDestroy = cb.mock.calls.length;
523
- w.runPhase(phase, 0, 0);
524
- expect(cb).toHaveBeenCalledTimes(countAfterDestroy);
525
- });
526
-
527
- it("each with multiple components delivers the resolved tuple", () => {
528
- const { w, phase } = setup();
529
- const cb = vi.fn();
530
- w.system("test")
531
- .phase(phase)
532
- .requires(Position, Velocity)
533
- .each([Position, Velocity], cb);
534
- w.start();
535
- const e = w.createEntity();
536
- const pos = e.add(Position);
537
- const vel = e.add(Velocity);
538
- w.runPhase(phase, 0, 0);
539
- w.runPhase(phase, 0, 0);
540
- expect(cb).toHaveBeenCalledWith(e, [pos, vel]);
541
- });
542
-
543
- it("each does NOT modify the system query — without requires/query no entity matches", () => {
544
- const { w, phase } = setup();
545
- const cb = vi.fn();
546
- w.system("test").phase(phase).each([Position], cb);
547
- w.start();
548
- const e = w.createEntity();
549
- e.add(Position);
550
- w.runPhase(phase, 0, 0);
551
- w.runPhase(phase, 0, 0);
552
- expect(cb).not.toHaveBeenCalled();
553
- });
554
-
555
- it("each with empty component list still fires per entity", () => {
556
- const { w, phase } = setup();
557
- const cb = vi.fn();
558
- w.system("test").phase(phase).requires(Position).each([], cb);
559
- w.start();
560
- const e = w.createEntity();
561
- e.add(Position);
562
- w.runPhase(phase, 0, 0);
563
- w.runPhase(phase, 0, 0);
564
- expect(cb).toHaveBeenCalledWith(e, []);
565
- });
566
-
567
- it("each returns the system for chaining", () => {
568
- const { w } = setup();
569
- const sys = w.system("test");
570
- expect(sys.each([Position], () => {})).toBe(sys);
571
- });
572
-
573
- it("each throws if registered twice on the same system", () => {
574
- const { w } = setup();
575
- const sys = w.system("test");
576
- sys.each([Position], () => {});
577
- expect(() => sys.each([Velocity], () => {})).toThrow();
578
- });
579
-
580
- it("entities are only tracked when track or each is registered", () => {
581
- const { w, phase } = setup();
582
- const sys = w.system("test").phase(phase).requires(Position);
583
- w.start();
584
- const e = w.createEntity();
585
- e.add(Position);
586
- w.runPhase(phase, 0, 0);
587
- expect(sys.entities.size).toBe(0);
588
- });
589
-
590
- it("track() populates entities on enter and clears them on exit", () => {
591
- const { w, phase } = setup();
592
- const sys = w.system("test").phase(phase).requires(Position).track();
593
- w.start();
594
- const e = w.createEntity();
595
- e.add(Position);
596
- w.runPhase(phase, 0, 0);
597
- expect(sys.entities.size).toBe(1);
598
- expect(sys.entities.has(e)).toBe(true);
599
- e.remove(Position);
600
- w.runPhase(phase, 0, 0);
601
- expect(sys.entities.size).toBe(0);
602
- });
603
-
604
- it("track() returns the system and is idempotent", () => {
605
- const { w, phase } = setup();
606
- const sys = w.system("test").phase(phase).requires(Position);
607
- expect(sys.track()).toBe(sys);
608
- expect(sys.track().track()).toBe(sys);
609
- w.start();
610
- const e = w.createEntity();
611
- e.add(Position);
612
- w.runPhase(phase, 0, 0);
613
- expect(sys.entities.size).toBe(1);
614
- });
615
-
616
- it("each() implies track() — entities is populated without an explicit track call", () => {
617
- const { w, phase } = setup();
618
- const sys = w.system("test")
619
- .phase(phase)
620
- .requires(Position)
621
- .each([Position], () => {});
622
- w.start();
623
- const e = w.createEntity();
624
- e.add(Position);
625
- w.runPhase(phase, 0, 0);
626
- expect(sys.entities.has(e)).toBe(true);
627
- });
628
-
629
- it("system.entities is typed as a ReadonlySet", () => {
630
- const { w } = setup();
631
- const sys = w.system("test").requires(Position).track();
632
- const fakeEntity = {} as any;
633
- // @ts-expect-error entities is a ReadonlySet — add() is not exposed
634
- sys.entities.add(fakeEntity);
635
- // @ts-expect-error entities is a ReadonlySet — delete() is not exposed
636
- sys.entities.delete(fakeEntity);
637
- });
638
-
639
- it("each and update coexist on the same system", () => {
640
- const { w, phase } = setup();
641
- const eachCb = vi.fn();
642
- const updateCb = vi.fn();
643
- w.system("test")
644
- .phase(phase)
645
- .requires(Position)
646
- .each([Position], eachCb)
647
- .update(Position, updateCb);
648
- w.start();
649
- const e = w.createEntity();
650
- const pos = e.add(Position, false);
651
- w.runPhase(phase, 0, 0); // entry queues pos for update
652
- w.runPhase(phase, 0, 0); // both fire
653
- expect(eachCb).toHaveBeenCalledWith(e, [pos]);
654
- expect(updateCb).toHaveBeenCalledWith(pos);
655
- eachCb.mockClear();
656
- updateCb.mockClear();
657
-
658
- // Without modified(): each fires, update does not.
659
- w.runPhase(phase, 0, 0);
660
- expect(eachCb).toHaveBeenCalled();
661
- expect(updateCb).not.toHaveBeenCalled();
662
- });
663
- });
664
-
665
- describe("System — sort", () => {
666
- it("sort() returns this for chaining", () => {
667
- const { w } = setup();
668
- const sys = w.system("test").requires(Position);
669
- expect(sys.sort([Position], ([a], [b]) => a.x - b.x)).toBe(sys);
670
- });
671
-
672
- it("sort() implies track()", () => {
673
- const { w, phase } = setup();
674
- const sys = w
675
- .system("test")
676
- .phase(phase)
677
- .requires(Position)
678
- .sort([Position], ([a], [b]) => a.x - b.x);
679
- w.start();
680
- const e = w.createEntity();
681
- const pos = e.add(Position);
682
- pos.x = 5;
683
- w.runPhase(phase, 0, 0);
684
- expect(sys.entities.has(e)).toBe(true);
685
- });
686
-
687
- it("entities are iterated in sorted order", () => {
688
- const { w, phase } = setup();
689
- const sys = w
690
- .system("test")
691
- .phase(phase)
692
- .requires(Position)
693
- .sort([Position], ([a], [b]) => a.x - b.x);
694
- w.start();
695
-
696
- const e1 = w.createEntity();
697
- const p1 = e1.add(Position, false);
698
- p1.x = 30;
699
-
700
- const e2 = w.createEntity();
701
- const p2 = e2.add(Position, false);
702
- p2.x = 10;
703
-
704
- const e3 = w.createEntity();
705
- const p3 = e3.add(Position, false);
706
- p3.x = 20;
707
-
708
- w.runPhase(phase, 0, 0);
709
-
710
- expect([...sys.entities]).toEqual([e2, e3, e1]);
711
- });
712
-
713
- it("each() visits entities in sorted order", () => {
714
- const { w, phase } = setup();
715
- const visited: number[] = [];
716
- w.system("test")
717
- .phase(phase)
718
- .requires(Position)
719
- .sort([Position], ([a], [b]) => a.x - b.x)
720
- .each([Position], (_e, [pos]) => visited.push(pos.x));
721
- w.start();
722
-
723
- const e1 = w.createEntity();
724
- const p1 = e1.add(Position, false);
725
- p1.x = 30;
726
-
727
- const e2 = w.createEntity();
728
- const p2 = e2.add(Position, false);
729
- p2.x = 10;
730
-
731
- const e3 = w.createEntity();
732
- const p3 = e3.add(Position, false);
733
- p3.x = 20;
734
-
735
- w.runPhase(phase, 0, 0); // enter
736
- w.runPhase(phase, 0, 0); // each fires
737
-
738
- expect(visited).toEqual([10, 20, 30]);
739
- });
740
-
741
- it("exiting entity is removed from the sorted set", () => {
742
- const { w, phase } = setup();
743
- const sys = w
744
- .system("test")
745
- .phase(phase)
746
- .requires(Position)
747
- .sort([Position], ([a], [b]) => a.x - b.x);
748
- w.start();
749
-
750
- const e1 = w.createEntity();
751
- const p1 = e1.add(Position, false);
752
- p1.x = 10;
753
-
754
- const e2 = w.createEntity();
755
- const p2 = e2.add(Position, false);
756
- p2.x = 20;
757
-
758
- w.runPhase(phase, 0, 0);
759
- expect([...sys.entities]).toEqual([e1, e2]);
760
-
761
- e1.remove(Position);
762
- w.runPhase(phase, 0, 0);
763
- expect([...sys.entities]).toEqual([e2]);
764
- });
765
-
766
- it("sort with multiple components passes tuples to compare", () => {
767
- const { w, phase } = setup();
768
- const sys = w
769
- .system("test")
770
- .phase(phase)
771
- .requires(Position, Velocity)
772
- .sort(
773
- [Position, Velocity],
774
- ([posA, velA], [posB, velB]) =>
775
- posA.x + velA.vx - (posB.x + velB.vx)
776
- );
777
- w.start();
778
-
779
- const e1 = w.createEntity();
780
- const p1 = e1.add(Position, false);
781
- const v1 = e1.add(Velocity, false);
782
- p1.x = 10;
783
- v1.vx = 5; // sum = 15
784
-
785
- const e2 = w.createEntity();
786
- const p2 = e2.add(Position, false);
787
- const v2 = e2.add(Velocity, false);
788
- p2.x = 1;
789
- v2.vx = 1; // sum = 2
790
-
791
- const e3 = w.createEntity();
792
- const p3 = e3.add(Position, false);
793
- const v3 = e3.add(Velocity, false);
794
- p3.x = 5;
795
- v3.vx = 5; // sum = 10
796
-
797
- w.runPhase(phase, 0, 0);
798
- expect([...sys.entities]).toEqual([e2, e3, e1]);
799
- });
800
- });