@latticexyz/recs 2.0.12-main-9be2bb86 → 2.0.12-main-e43c0938

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,501 +0,0 @@
1
- import { deferred, sleep } from "@latticexyz/utils";
2
- import { ReplaySubject } from "rxjs";
3
- import { defineComponent, getComponentValueStrict, withValue, setComponent } from "../Component";
4
- import { createEntity } from "../Entity";
5
- import { runQuery, HasValue } from "../Query";
6
- import { createWorld } from "../World";
7
- import { Type } from "../constants";
8
- import { World, Component } from "../types";
9
- import { waitForComponentValueIn } from "./waitForComponentValueIn";
10
- import { ActionState } from "./constants";
11
- import { createActionSystem } from "./createActionSystem";
12
- import { waitForActionCompletion } from "./waitForActionCompletion";
13
-
14
- describe("ActionSystem", () => {
15
- let world: World;
16
- let Resource: Component<{ amount: Type.Number }>;
17
- let Action: Component<{
18
- state: Type.String;
19
- on: Type.OptionalEntity;
20
- metadata: Type.OptionalT;
21
- overrides: Type.OptionalStringArray;
22
- txHash: Type.OptionalString;
23
- }>;
24
- let actions: ReturnType<typeof createActionSystem>;
25
- let txReduced$: ReplaySubject<string>;
26
-
27
- beforeEach(async () => {
28
- world = createWorld();
29
- txReduced$ = new ReplaySubject<string>();
30
- actions = createActionSystem(world, txReduced$, async () => {
31
- // mimic wait for tx
32
- await sleep(100);
33
- });
34
- Action = actions.Action;
35
- Resource = defineComponent(world, { amount: Type.Number });
36
- });
37
-
38
- afterEach(() => {
39
- world.dispose();
40
- });
41
-
42
- it("should immediately execute actions if their requirement is met and set the Action component", async () => {
43
- const mockFn = jest.fn();
44
- const entity = actions.add({
45
- id: "action",
46
- components: {},
47
- requirement: () => true,
48
- updates: () => [],
49
- execute: () => {
50
- mockFn();
51
- },
52
- });
53
-
54
- expect(mockFn).toHaveBeenCalledTimes(1);
55
- expect(getComponentValueStrict(Action, entity).state).toBe(ActionState.Executing);
56
- await waitForActionCompletion(Action, entity);
57
- expect(getComponentValueStrict(Action, entity).state).toBe(ActionState.Complete);
58
- });
59
-
60
- it("should not execute actions if their requirement is not met and set the Action component", () => {
61
- const mockFn = jest.fn();
62
- const entity = actions.add({
63
- id: "action",
64
- components: {},
65
- requirement: () => false,
66
- updates: () => [],
67
- execute: () => {
68
- mockFn();
69
- },
70
- });
71
-
72
- expect(mockFn).toHaveBeenCalledTimes(0);
73
- expect(getComponentValueStrict(Action, entity).state).toBe(ActionState.Requested);
74
- });
75
-
76
- it("should set the Action component of failed actions", async () => {
77
- const [, reject, promise] = deferred<void>();
78
- const entity = actions.add({
79
- id: "action",
80
- components: {},
81
- requirement: () => true,
82
- updates: () => [],
83
- execute: () => promise,
84
- });
85
-
86
- reject(new Error("action failed"));
87
-
88
- await waitForActionCompletion(Action, entity);
89
-
90
- expect(getComponentValueStrict(Action, entity).state).toBe(ActionState.Failed);
91
- });
92
-
93
- it("should set the Action component of cancelled actions", async () => {
94
- const entity = actions.add({
95
- id: "action",
96
- components: {},
97
- requirement: () => false,
98
- updates: () => [],
99
- execute: () => void 0,
100
- });
101
-
102
- const cancelled = actions.cancel("action");
103
- await waitForActionCompletion(Action, entity);
104
-
105
- expect(getComponentValueStrict(Action, entity).state).toBe(ActionState.Cancelled);
106
- expect(cancelled).toBe(true);
107
- });
108
-
109
- it("should not be possible to cancel actions that are already executing", async () => {
110
- const [resolve, , promise] = deferred<void>();
111
- const entity = actions.add({
112
- id: "action",
113
- components: {},
114
- requirement: () => true,
115
- updates: () => [],
116
- execute: () => promise,
117
- });
118
-
119
- const cancelled = actions.cancel("action");
120
- resolve();
121
- await waitForActionCompletion(Action, entity);
122
- expect(getComponentValueStrict(Action, entity).state).toBe(ActionState.Complete);
123
- expect(cancelled).toBe(false);
124
- });
125
-
126
- it("should execute actions if components it depends on changed and the requirement is met now", () => {
127
- const mockFn = jest.fn();
128
- const player = createEntity(world, [withValue(Resource, { amount: 0 })]);
129
-
130
- actions.add({
131
- id: "action",
132
- components: { Resource },
133
- requirement: ({ Resource }) => getComponentValueStrict(Resource, player).amount > 100,
134
- updates: () => [],
135
- execute: () => {
136
- mockFn();
137
- },
138
- });
139
-
140
- expect(mockFn).toHaveBeenCalledTimes(0);
141
- setComponent(Resource, player, { amount: 99 });
142
- expect(mockFn).toHaveBeenCalledTimes(0);
143
- setComponent(Resource, player, { amount: 101 });
144
- expect(mockFn).toHaveBeenCalledTimes(1);
145
- });
146
-
147
- it("should return all actions related to a given entity", () => {
148
- const settlement1 = createEntity(world);
149
- const settlement2 = createEntity(world);
150
-
151
- const entity1 = actions.add({
152
- id: "action1",
153
- on: settlement1,
154
- components: { Resource },
155
- requirement: () => false,
156
- updates: () => [],
157
- execute: () => void 0,
158
- });
159
-
160
- const entity2 = actions.add({
161
- id: "action2",
162
- on: settlement2,
163
- components: { Resource },
164
- requirement: () => false,
165
- updates: () => [],
166
- execute: () => void 0,
167
- });
168
-
169
- const entity3 = actions.add({
170
- id: "action3",
171
- components: { Resource },
172
- requirement: () => false,
173
- updates: () => [],
174
- execute: () => void 0,
175
- });
176
-
177
- expect(runQuery([HasValue(Action, { on: settlement1 })])).toEqual(new Set([entity1]));
178
- expect(runQuery([HasValue(Action, { on: settlement2 })])).toEqual(new Set([entity2]));
179
- expect(runQuery([HasValue(Action, { state: ActionState.Requested })])).toEqual(
180
- new Set([entity1, entity2, entity3]),
181
- );
182
- });
183
-
184
- it("should not remove pending update until all corresponding tx have been reduced", async () => {
185
- const player = createEntity(world, [withValue(Resource, { amount: 100 })]);
186
-
187
- const entity1 = actions.add({
188
- id: "action1",
189
- components: { Resource },
190
- requirement: () => true,
191
- updates: ({ Resource }) => [
192
- {
193
- component: Resource,
194
- entity: player,
195
- value: { amount: getComponentValueStrict(Resource, player).amount - 1 },
196
- },
197
- ],
198
- execute: async () => Promise.resolve("tx1"),
199
- });
200
-
201
- const entity2 = actions.add({
202
- id: "action2",
203
- components: { Resource },
204
- // Resource needs to be 100 in order for this action to be executed
205
- requirement: ({ Resource }) => getComponentValueStrict(Resource, player).amount === 100,
206
- updates: () => [],
207
- execute: () => void 0,
208
- });
209
-
210
- await waitForComponentValueIn(Action, entity1, [{ state: ActionState.WaitingForTxEvents }]);
211
- // While action1 is waiting for tx, action 2 is not executed yet
212
- expect(getComponentValueStrict(Action, entity1).state).toBe(ActionState.WaitingForTxEvents);
213
- expect(getComponentValueStrict(Action, entity2).state).toBe(ActionState.Requested);
214
-
215
- txReduced$.next("tx1");
216
- // Now it's done
217
- await waitForComponentValueIn(Action, entity1, [{ state: ActionState.Complete }]);
218
- expect(getComponentValueStrict(Action, entity1).state).toBe(ActionState.Complete);
219
- await sleep(0);
220
- expect(getComponentValueStrict(Action, entity2).state).toBe(ActionState.Complete);
221
- });
222
-
223
- // TODO: get tests to pass
224
- it.skip("should execute actions if the requirement is met while taking into account pending updates", async () => {
225
- const requirementSpy1 = jest.fn();
226
- const requirementSpy2 = jest.fn();
227
- const requirementSpy3 = jest.fn();
228
-
229
- const executeSpy1 = jest.fn();
230
- const executeSpy2 = jest.fn();
231
- const executeSpy3 = jest.fn();
232
-
233
- const player = createEntity(world, [withValue(Resource, { amount: 0 })]);
234
-
235
- let nonce = 0;
236
-
237
- // First schedule action1
238
- const [resolveAction1, , action1Promise] = deferred<void>();
239
- const entity1 = actions.add({
240
- id: "action1",
241
- components: { Resource },
242
- // This action requires a resource amount of 100 to be executed
243
- requirement: ({ Resource }) => {
244
- requirementSpy1();
245
- return getComponentValueStrict(Resource, player).amount >= 100;
246
- },
247
- // When this action is executed it will subtract 100 from the resource amount
248
- updates: ({ Resource }) => [
249
- {
250
- component: Resource,
251
- entity: player,
252
- value: { amount: getComponentValueStrict(Resource, player).amount - 100 },
253
- },
254
- ],
255
- execute: async () => {
256
- executeSpy1(nonce++);
257
- await action1Promise;
258
- const { amount } = getComponentValueStrict(Resource, player);
259
- setComponent(Resource, player, { amount: amount - 100 });
260
- },
261
- });
262
-
263
- // Action1 is not executed yet because requirement is not met
264
- expect(executeSpy1).toHaveBeenCalledTimes(0);
265
-
266
- // The requirement was checked once when adding the action
267
- expect(requirementSpy1).toHaveBeenCalledTimes(1);
268
-
269
- // Then shedule action3
270
- actions.add({
271
- id: "action3",
272
- components: { Resource },
273
- // This action also requires a resource amount of 100 to be executed
274
- requirement: ({ Resource }) => {
275
- requirementSpy3();
276
- const amount = getComponentValueStrict(Resource, player).amount;
277
- return amount >= 100;
278
- },
279
- updates: ({ Resource }) => [
280
- {
281
- component: Resource,
282
- entity: player,
283
- value: { amount: getComponentValueStrict(Resource, player).amount - 100 },
284
- },
285
- ],
286
- execute: () => {
287
- executeSpy3(nonce++);
288
- },
289
- });
290
-
291
- // Action3 is also not executed yet because the requirement is not met
292
- expect(executeSpy3).toHaveBeenCalledTimes(0);
293
-
294
- // The requirement was cheecked once when adding the action
295
- expect(requirementSpy3).toHaveBeenCalledTimes(1);
296
-
297
- // Action 1's requirement was not checked again, because neither pending updates nor components changed.
298
- expect(requirementSpy1).toHaveBeenCalledTimes(1);
299
-
300
- // Now schedule action2.
301
- // This action declares it will update the Resource component to be 100
302
- const [resolveAction2, , action2Promise] = deferred<void>();
303
- const entity2 = actions.add({
304
- id: "action2",
305
- components: { Resource },
306
- requirement: () => {
307
- requirementSpy2();
308
- return true;
309
- },
310
- updates: () => [{ component: Resource, entity: player, value: { amount: 100 } }],
311
- execute: async () => {
312
- executeSpy2(nonce++);
313
- await action2Promise;
314
- const { amount } = getComponentValueStrict(Resource, player);
315
- setComponent(Resource, player, { amount: amount + 100 });
316
- },
317
- });
318
-
319
- // action2 is executed immediately
320
- expect(executeSpy2).toHaveBeenCalledTimes(1);
321
- expect(executeSpy2).toHaveBeenCalledWith(0);
322
-
323
- // But it is not done yet, because the promise is not resolved
324
- await waitForComponentValueIn(Action, entity2, [{ state: ActionState.Executing }]);
325
- expect(getComponentValueStrict(Action, entity2).state).toBe(ActionState.Executing);
326
-
327
- // action 2's requirement was checked only once
328
- expect(requirementSpy2).toHaveBeenCalledTimes(1);
329
-
330
- // Executing action 2 added pending updates and thereby triggered rechecking the requirements of action 1 and action 3
331
- expect(requirementSpy1).toHaveBeenCalledTimes(2);
332
- expect(requirementSpy3).toHaveBeenCalledTimes(2);
333
-
334
- // Action 1 is already executed before action 2 resolves because it trusts action 2's update declaration
335
- expect(executeSpy1).toHaveBeenCalledTimes(1);
336
-
337
- // Action 1 should be executed after action 2
338
- expect(executeSpy1).toHaveBeenCalledWith(1);
339
-
340
- // action 3 should not have been executed, because action 1 declared it will reduce the resource amount, such that action3's requirement is not met
341
- expect(executeSpy3).toHaveBeenCalledTimes(0);
342
-
343
- // Now resolve action2
344
- resolveAction2();
345
- await waitForActionCompletion(Action, entity2);
346
-
347
- // The real component amount should be at 100 now
348
- expect(getComponentValueStrict(Resource, player).amount).toBe(100);
349
-
350
- // Removing action 2's pending updates and modifying the component state should have triggered two requirement checks on action 3
351
- expect(requirementSpy3).toHaveBeenCalledTimes(4);
352
-
353
- // action3 should still not have been executed because action2 is not resolved yet and declared an update
354
- expect(executeSpy3).toHaveBeenCalledTimes(0);
355
-
356
- // Now resolve action1
357
- resolveAction1();
358
- await waitForActionCompletion(Action, entity1);
359
-
360
- // The real component amount should be at 0 now
361
- expect(getComponentValueStrict(Resource, player).amount).toBe(0);
362
-
363
- // Removing action 1's pending updates and modifying the component state should have triggered two requirement checks on action 3
364
- expect(requirementSpy3).toHaveBeenCalledTimes(6);
365
-
366
- // action3 should still not have been executed
367
- expect(executeSpy3).toHaveBeenCalledTimes(0);
368
-
369
- // Setting the resource amount to 100 should trigger a requirement check on action 3
370
- setComponent(Resource, player, { amount: 100 });
371
- expect(requirementSpy3).toHaveBeenCalledTimes(7);
372
-
373
- // Now action3 should finally have been executed
374
- expect(executeSpy3).toHaveBeenCalledTimes(1);
375
- expect(executeSpy3).toHaveBeenCalledWith(2);
376
-
377
- // In total action 1's requirements should have been checked 2 times
378
- expect(requirementSpy1).toHaveBeenCalledTimes(2);
379
-
380
- // In total action 2's requirements should have been checked 1 time
381
- expect(requirementSpy2).toHaveBeenCalledTimes(1);
382
-
383
- // In total action 3's requirements should have been checked 7 times
384
- expect(requirementSpy3).toHaveBeenCalledTimes(7);
385
- });
386
-
387
- it("declaring component updates should not modify real components", async () => {
388
- const player = createEntity(world, [withValue(Resource, { amount: 0 })]);
389
-
390
- expect(getComponentValueStrict(Resource, player)).toEqual({ amount: 0 });
391
-
392
- const [resolve, , promise] = deferred<void>();
393
- const entity = actions.add({
394
- id: "action",
395
- components: { Resource },
396
- requirement: () => true,
397
- updates: () => [{ component: Resource, entity: player, value: { amount: 1000 } }],
398
- execute: async () => {
399
- await promise;
400
- },
401
- });
402
-
403
- expect(getComponentValueStrict(Resource, player)).toEqual({ amount: 0 });
404
-
405
- resolve();
406
- await waitForActionCompletion(Action, entity);
407
-
408
- expect(getComponentValueStrict(Resource, player)).toEqual({ amount: 0 });
409
- });
410
-
411
- // TODO: get tests to pass
412
- it.skip("should rerun the requirement function only if a component value accessed in the requirement changed", () => {
413
- const player = createEntity(world, [withValue(Resource, { amount: 0 })]);
414
- const requirementSpy = jest.fn();
415
-
416
- actions.add({
417
- id: "action",
418
- components: { Resource },
419
- requirement: ({ Resource }) => {
420
- requirementSpy();
421
- return getComponentValueStrict(Resource, player).amount >= 100;
422
- },
423
- updates: () => [],
424
- execute: async () => void 0,
425
- });
426
-
427
- // The requirement should be checked once when adding the action
428
- expect(requirementSpy).toHaveBeenCalledTimes(1);
429
-
430
- // Setting unrelated values in the component should not retrigger a requirement check
431
- const player2 = createEntity(world, [withValue(Resource, { amount: 0 })]);
432
- setComponent(Resource, player2, { amount: 10 });
433
- expect(requirementSpy).toHaveBeenCalledTimes(1);
434
-
435
- // Setting a relevant value in the component should trigger a requirement check
436
- setComponent(Resource, player, { amount: 10 });
437
- expect(requirementSpy).toHaveBeenCalledTimes(2);
438
- });
439
-
440
- // TODO: get tests to pass
441
- it.skip("should rerun the requirement function only if a pending update relevant to a value accessed in the requirement changed", () => {
442
- const player1 = createEntity(world, [withValue(Resource, { amount: 0 })]);
443
- const player2 = createEntity(world);
444
-
445
- const requirementSpy = jest.fn();
446
-
447
- actions.add({
448
- id: "action1",
449
- components: { Resource },
450
- requirement: ({ Resource }) => {
451
- requirementSpy();
452
- return getComponentValueStrict(Resource, player1).amount >= 100;
453
- },
454
- updates: () => [],
455
- execute: () => void 0,
456
- });
457
-
458
- // The requirement should be checked once when adding the action
459
- expect(requirementSpy).toHaveBeenCalledTimes(1);
460
-
461
- // Another action is executed, which does not declare any updates
462
- actions.add({
463
- id: "action2",
464
- components: { Resource },
465
- requirement: () => true,
466
- updates: () => [],
467
- execute: () => void 0,
468
- });
469
-
470
- // Executing actions with no pending updates that don't modify component states should not trigger a requirement check
471
- expect(requirementSpy).toHaveBeenCalledTimes(1);
472
-
473
- // Another action declares an update to the resource amount of player2, which is unrelated to action1's requirement
474
- actions.add({
475
- id: "action3",
476
- components: { Resource },
477
- requirement: () => true,
478
- updates: () => [{ component: Resource, entity: player2, value: { amount: 1000 } }],
479
- execute: () => void 0,
480
- });
481
-
482
- // Unrelated pending updates should not retrigger a requirement check
483
- expect(requirementSpy).toHaveBeenCalledTimes(1);
484
-
485
- const [resolve, , promise] = deferred<void>();
486
- // Another action declares an update to the resource amount of player1, which is relevant to action1's requirement
487
- actions.add({
488
- id: "action4",
489
- components: { Resource },
490
- requirement: () => true,
491
- updates: () => [{ component: Resource, entity: player1, value: { amount: 10000 } }],
492
- execute: async () => {
493
- await promise;
494
- },
495
- });
496
-
497
- // Relevant pending updates should trigger a requirement check
498
- expect(requirementSpy).toHaveBeenCalledTimes(2);
499
- resolve();
500
- });
501
- });