@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.
- package/package.json +6 -13
- package/CHANGELOG.md +0 -1248
- package/src/Component.spec.ts +0 -275
- package/src/Component.ts +0 -531
- package/src/Entity.spec.ts +0 -45
- package/src/Entity.ts +0 -46
- package/src/Indexer.spec.ts +0 -288
- package/src/Indexer.ts +0 -71
- package/src/Performance.spec.ts +0 -153
- package/src/Query.spec.ts +0 -811
- package/src/Query.ts +0 -554
- package/src/System.spec.ts +0 -139
- package/src/System.ts +0 -150
- package/src/World.spec.ts +0 -79
- package/src/World.ts +0 -102
- package/src/constants.ts +0 -54
- package/src/deprecated/constants.ts +0 -9
- package/src/deprecated/createActionSystem.spec.ts +0 -501
- package/src/deprecated/createActionSystem.ts +0 -236
- package/src/deprecated/defineActionComponent.ts +0 -18
- package/src/deprecated/index.ts +0 -2
- package/src/deprecated/types.ts +0 -45
- package/src/deprecated/waitForActionCompletion.ts +0 -15
- package/src/deprecated/waitForComponentValueIn.ts +0 -38
- package/src/index.ts +0 -9
- package/src/types.ts +0 -260
- package/src/utils.ts +0 -68
package/src/Query.ts
DELETED
@@ -1,554 +0,0 @@
|
|
1
|
-
import { filterNullish } from "@latticexyz/utils";
|
2
|
-
import { observable, ObservableSet } from "mobx";
|
3
|
-
import { concat, concatMap, filter, from, map, merge, Observable, of, share } from "rxjs";
|
4
|
-
import {
|
5
|
-
componentValueEquals,
|
6
|
-
getComponentEntities,
|
7
|
-
getComponentValue,
|
8
|
-
getEntitiesWithValue,
|
9
|
-
hasComponent,
|
10
|
-
} from "./Component";
|
11
|
-
import { UpdateType, Type } from "./constants";
|
12
|
-
import {
|
13
|
-
Component,
|
14
|
-
ComponentUpdate,
|
15
|
-
ComponentValue,
|
16
|
-
Entity,
|
17
|
-
EntityQueryFragment,
|
18
|
-
HasQueryFragment,
|
19
|
-
HasValueQueryFragment,
|
20
|
-
NotQueryFragment,
|
21
|
-
NotValueQueryFragment,
|
22
|
-
ProxyExpandQueryFragment,
|
23
|
-
ProxyReadQueryFragment,
|
24
|
-
QueryFragment,
|
25
|
-
QueryFragmentType,
|
26
|
-
Schema,
|
27
|
-
SettingQueryFragment,
|
28
|
-
} from "./types";
|
29
|
-
import { toUpdateStream } from "./utils";
|
30
|
-
|
31
|
-
/**
|
32
|
-
* Create a {@link HasQueryFragment}.
|
33
|
-
*
|
34
|
-
* @remarks
|
35
|
-
* The {@link HasQueryFragment} filters for entities that have the given component,
|
36
|
-
* independent from the component value.
|
37
|
-
*
|
38
|
-
* @example
|
39
|
-
* Query for all entities with a `Position`.
|
40
|
-
* ```
|
41
|
-
* runQuery([Has(Position)]);
|
42
|
-
* ```
|
43
|
-
*
|
44
|
-
* @param component Component this query fragment refers to.
|
45
|
-
* @returns query fragment to be used in {@link runQuery} or {@link defineQuery}.
|
46
|
-
*/
|
47
|
-
export function Has<T extends Schema>(component: Component<T>): HasQueryFragment<T> {
|
48
|
-
return { type: QueryFragmentType.Has, component };
|
49
|
-
}
|
50
|
-
|
51
|
-
/**
|
52
|
-
* Create a {@link NotQueryFragment}.
|
53
|
-
*
|
54
|
-
* @remarks
|
55
|
-
* The {@link NotQueryFragment} filters for entities that don't have the given component,
|
56
|
-
* independent from the component value.
|
57
|
-
*
|
58
|
-
* @example
|
59
|
-
* Query for all entities with a `Position` that are not `Movable`.
|
60
|
-
* ```
|
61
|
-
* runQuery([Has(Position), Not(Movable)]);
|
62
|
-
* ```
|
63
|
-
*
|
64
|
-
* @param component Component this query fragment refers to.
|
65
|
-
* @returns query fragment to be used in {@link runQuery} or {@link defineQuery}.
|
66
|
-
*/
|
67
|
-
export function Not<T extends Schema>(component: Component<T>): NotQueryFragment<T> {
|
68
|
-
return { type: QueryFragmentType.Not, component };
|
69
|
-
}
|
70
|
-
|
71
|
-
/**
|
72
|
-
* Create a {@link HasValueQueryFragment}.
|
73
|
-
*
|
74
|
-
* @remarks
|
75
|
-
* The {@link HasValueQueryFragment} filters for entities that have the given component
|
76
|
-
* with the given component value.
|
77
|
-
*
|
78
|
-
* @example
|
79
|
-
* Query for all entities at Position (0,0).
|
80
|
-
* ```
|
81
|
-
* runQuery([HasValue(Position, { x: 0, y: 0 })]);
|
82
|
-
* ```
|
83
|
-
*
|
84
|
-
* @param component Component this query fragment refers to.
|
85
|
-
* @param value Only include entities with this (partial) component value from the result.
|
86
|
-
* @returns query fragment to be used in {@link runQuery} or {@link defineQuery}.
|
87
|
-
*/
|
88
|
-
export function HasValue<T extends Schema>(
|
89
|
-
component: Component<T>,
|
90
|
-
value: Partial<ComponentValue<T>>,
|
91
|
-
): HasValueQueryFragment<T> {
|
92
|
-
return { type: QueryFragmentType.HasValue, component, value };
|
93
|
-
}
|
94
|
-
|
95
|
-
/**
|
96
|
-
* Create a {@link NotValueQueryFragment}.
|
97
|
-
*
|
98
|
-
* @remarks
|
99
|
-
* The {@link NotValueQueryFragment} filters for entities that don't have the given component
|
100
|
-
* with the given component value.
|
101
|
-
*
|
102
|
-
* @example
|
103
|
-
* Query for all entities that have a `Position`, except for those at `Position` (0,0).
|
104
|
-
* ```
|
105
|
-
* runQuery([Has(Position), NotValue(Position, { x: 0, y: 0 })]);
|
106
|
-
* ```
|
107
|
-
*
|
108
|
-
* @param component Component this query fragment refers to.
|
109
|
-
* @param value Exclude entities with this (partial) component value from the result.
|
110
|
-
* @returns query fragment to be used in {@link runQuery} or {@link defineQuery}.
|
111
|
-
*/
|
112
|
-
export function NotValue<T extends Schema>(
|
113
|
-
component: Component<T>,
|
114
|
-
value: Partial<ComponentValue<T>>,
|
115
|
-
): NotValueQueryFragment<T> {
|
116
|
-
return { type: QueryFragmentType.NotValue, component, value };
|
117
|
-
}
|
118
|
-
|
119
|
-
/**
|
120
|
-
* Create a {@link ProxyReadQueryFragment}.
|
121
|
-
*
|
122
|
-
* @remarks
|
123
|
-
* The {@link ProxyReadQueryFragment} activates the "proxy read mode" for the rest of the query.
|
124
|
-
* This means that for all remaining fragments in the query not only the entities themselves are checked, but also
|
125
|
-
* their "ancestors" up to the given `depth` on the relationship chain defined by the given `component`.
|
126
|
-
*
|
127
|
-
* @example
|
128
|
-
* Query for all entities that have a `Position` and are (directly or indirectly) owned by an entity with `Name` "Alice".
|
129
|
-
* ```
|
130
|
-
* runQuery([Has(Position), ProxyRead(OwnedByEntity, Number.MAX_SAFE_INTEGER), HasValue(Name, { name: "Alice" })]);
|
131
|
-
* ```
|
132
|
-
*
|
133
|
-
* @param component Component this query fragment refers to.
|
134
|
-
* @param depth Max depth in the relationship chain to traverse.
|
135
|
-
* @returns query fragment to be used in {@link runQuery} or {@link defineQuery}.
|
136
|
-
*/
|
137
|
-
export function ProxyRead(component: Component<{ value: Type.Entity }>, depth: number): ProxyReadQueryFragment {
|
138
|
-
return { type: QueryFragmentType.ProxyRead, component, depth };
|
139
|
-
}
|
140
|
-
|
141
|
-
/**
|
142
|
-
* Create a {@link ProxyExpandQueryFragment}.
|
143
|
-
*
|
144
|
-
* @remarks
|
145
|
-
* The {@link ProxyExpandQueryFragment} activates the "proxy expand mode" for the rest of the query.
|
146
|
-
* This means that for all remaining fragments in the query not only the matching entities themselves are included in the intermediate set,
|
147
|
-
* but also all their "children" down to the given `depth` on the relationship chain defined by the given `component`.
|
148
|
-
*
|
149
|
-
* @example
|
150
|
-
* Query for all entities (directly or indirectly) owned by an entity with `Name` "Alice".
|
151
|
-
* ```
|
152
|
-
* runQuery([ProxyExpand(OwnedByEntity, Number.MAX_SAFE_INTEGER), HasValue(Name, { name: "Alice" })]);
|
153
|
-
* ```
|
154
|
-
*
|
155
|
-
* @param component Component to apply this query fragment to.
|
156
|
-
* @param depth Max depth in the relationship chain to traverse.
|
157
|
-
* @returns query fragment to be used in {@link runQuery} or {@link defineQuery}.
|
158
|
-
*/
|
159
|
-
export function ProxyExpand(component: Component<{ value: Type.Entity }>, depth: number): ProxyExpandQueryFragment {
|
160
|
-
return { type: QueryFragmentType.ProxyExpand, component, depth };
|
161
|
-
}
|
162
|
-
|
163
|
-
/**
|
164
|
-
* Helper function to check whether a given entity passes a given query fragment.
|
165
|
-
*
|
166
|
-
* @param entity Entity to check.
|
167
|
-
* @param fragment Query fragment to check.
|
168
|
-
* @returns True if the entity passes the query fragment, else false.
|
169
|
-
*/
|
170
|
-
function passesQueryFragment<T extends Schema>(entity: Entity, fragment: EntityQueryFragment<T>): boolean {
|
171
|
-
if (fragment.type === QueryFragmentType.Has) {
|
172
|
-
// Entity must have the given component
|
173
|
-
return hasComponent(fragment.component, entity);
|
174
|
-
}
|
175
|
-
|
176
|
-
if (fragment.type === QueryFragmentType.HasValue) {
|
177
|
-
// Entity must have the given component value
|
178
|
-
return componentValueEquals(fragment.value, getComponentValue(fragment.component, entity));
|
179
|
-
}
|
180
|
-
|
181
|
-
if (fragment.type === QueryFragmentType.Not) {
|
182
|
-
// Entity must not have the given component
|
183
|
-
return !hasComponent(fragment.component, entity);
|
184
|
-
}
|
185
|
-
|
186
|
-
if (fragment.type === QueryFragmentType.NotValue) {
|
187
|
-
// Entity must not have the given component value
|
188
|
-
return !componentValueEquals(fragment.value, getComponentValue(fragment.component, entity));
|
189
|
-
}
|
190
|
-
|
191
|
-
throw new Error("Unknown query fragment");
|
192
|
-
}
|
193
|
-
|
194
|
-
/**
|
195
|
-
* Helper function to check whether a query fragment is "positive" (ie `Has` or `HasValue`)
|
196
|
-
*
|
197
|
-
* @param fragment Query fragment to check.
|
198
|
-
* @returns True if the query fragment is positive, else false.
|
199
|
-
*/
|
200
|
-
function isPositiveFragment<T extends Schema>(
|
201
|
-
fragment: QueryFragment<T>,
|
202
|
-
): fragment is HasQueryFragment<T> | HasValueQueryFragment<T> {
|
203
|
-
return fragment.type === QueryFragmentType.Has || fragment.type == QueryFragmentType.HasValue;
|
204
|
-
}
|
205
|
-
|
206
|
-
/**
|
207
|
-
* Helper function to check whether a query fragment is "negative" (ie `Not` or `NotValue`)
|
208
|
-
*
|
209
|
-
* @param fragment Query fragment to check.
|
210
|
-
* @returns True if the query fragment is negative, else false.
|
211
|
-
*/
|
212
|
-
function isNegativeFragment<T extends Schema>(
|
213
|
-
fragment: QueryFragment<T>,
|
214
|
-
): fragment is NotQueryFragment<T> | NotValueQueryFragment<T> {
|
215
|
-
return fragment.type === QueryFragmentType.Not || fragment.type == QueryFragmentType.NotValue;
|
216
|
-
}
|
217
|
-
|
218
|
-
/**
|
219
|
-
* Helper function to check whether a query fragment is a setting fragment (ie `ProxyExpand` or `ProxyRead`)
|
220
|
-
*
|
221
|
-
* @param fragment Query fragment to check.
|
222
|
-
* @returns True if the query fragment is a setting fragment, else false.
|
223
|
-
*/
|
224
|
-
function isSettingFragment<T extends Schema>(fragment: QueryFragment<T>): fragment is SettingQueryFragment {
|
225
|
-
return fragment.type === QueryFragmentType.ProxyExpand || fragment.type == QueryFragmentType.ProxyRead;
|
226
|
-
}
|
227
|
-
|
228
|
-
/**
|
229
|
-
* Helper function to check whether the result of a query pass check is a breaking state.
|
230
|
-
*
|
231
|
-
* @remarks
|
232
|
-
* For positive fragments (Has/HasValue) we need to find any passing entity up the proxy chain
|
233
|
-
* so as soon as passes is true, we can early return. For negative fragments (Not/NotValue) every entity
|
234
|
-
* up the proxy chain must pass, so we can early return if we find one that doesn't pass.
|
235
|
-
*
|
236
|
-
* @param passes Boolean result of previous query pass check.
|
237
|
-
* @param fragment Fragment that was used in the query pass check.
|
238
|
-
* @returns True if the result is breaking pass state, else false.
|
239
|
-
*/
|
240
|
-
function isBreakingPassState(passes: boolean, fragment: EntityQueryFragment<Schema>) {
|
241
|
-
return (passes && isPositiveFragment(fragment)) || (!passes && isNegativeFragment(fragment));
|
242
|
-
}
|
243
|
-
|
244
|
-
/**
|
245
|
-
* Helper function to check whether an entity passes a query fragment when taking into account a {@link ProxyReadQueryFragment}.
|
246
|
-
*
|
247
|
-
* @param entity {@link Entity} of the entity to check.
|
248
|
-
* @param fragment Query fragment to check.
|
249
|
-
* @param proxyRead {@link ProxyReadQueryFragment} to take into account.
|
250
|
-
* @returns True if the entity passes the query fragment, else false.
|
251
|
-
*/
|
252
|
-
function passesQueryFragmentProxy<T extends Schema>(
|
253
|
-
entity: Entity,
|
254
|
-
fragment: EntityQueryFragment<T>,
|
255
|
-
proxyRead: ProxyReadQueryFragment,
|
256
|
-
): boolean | null {
|
257
|
-
let proxyEntity = entity;
|
258
|
-
let passes = false;
|
259
|
-
for (let i = 0; i < proxyRead.depth; i++) {
|
260
|
-
const value = getComponentValue(proxyRead.component, proxyEntity);
|
261
|
-
// If the current entity does not have the proxy component, abort
|
262
|
-
if (!value) return null;
|
263
|
-
|
264
|
-
const entity = value.value;
|
265
|
-
if (!entity) return null;
|
266
|
-
|
267
|
-
// Move up the proxy chain
|
268
|
-
proxyEntity = entity;
|
269
|
-
passes = passesQueryFragment(proxyEntity, fragment);
|
270
|
-
|
271
|
-
if (isBreakingPassState(passes, fragment)) {
|
272
|
-
return passes;
|
273
|
-
}
|
274
|
-
}
|
275
|
-
return passes;
|
276
|
-
}
|
277
|
-
|
278
|
-
/**
|
279
|
-
* Recursively compute all direct and indirect child entities up to the specified depth
|
280
|
-
* down the relationship chain defined by the given component.
|
281
|
-
*
|
282
|
-
* @param entity Entity to get all child entities for up to the specified depth
|
283
|
-
* @param component Component to use for the relationship chain.
|
284
|
-
* @param depth Depth up to which the recursion should be applied.
|
285
|
-
* @returns Set of entities that are child entities of the given entity via the given component.
|
286
|
-
*/
|
287
|
-
export function getChildEntities(
|
288
|
-
entity: Entity,
|
289
|
-
component: Component<{ value: Type.Entity }>,
|
290
|
-
depth: number,
|
291
|
-
): Set<Entity> {
|
292
|
-
if (depth === 0) return new Set();
|
293
|
-
|
294
|
-
const directChildEntities = getEntitiesWithValue(component, { value: entity });
|
295
|
-
if (depth === 1) return directChildEntities;
|
296
|
-
|
297
|
-
const indirectChildEntities = [...directChildEntities]
|
298
|
-
.map((childEntity) => [...getChildEntities(childEntity, component, depth - 1)])
|
299
|
-
.flat();
|
300
|
-
|
301
|
-
return new Set([...directChildEntities, ...indirectChildEntities]);
|
302
|
-
}
|
303
|
-
|
304
|
-
/**
|
305
|
-
* Execute a list of query fragments to receive a Set of matching entities.
|
306
|
-
*
|
307
|
-
* @remarks
|
308
|
-
* The query fragments are executed from left to right and are concatenated with a logical `AND`.
|
309
|
-
* For performance reasons, the most restrictive query fragment should be first in the list of query fragments,
|
310
|
-
* in order to reduce the number of entities the next query fragment needs to be checked for.
|
311
|
-
* If no proxy fragments are used, every entity in the resulting set passes every query fragment.
|
312
|
-
* If setting fragments are used, the order of the query fragments influences the result, since settings only apply to
|
313
|
-
* fragments after the setting fragment.
|
314
|
-
*
|
315
|
-
* @param fragments Query fragments to execute.
|
316
|
-
* @param initialSet Optional: provide a Set of entities to execute the query on. If none is given, all existing entities are used for the query.
|
317
|
-
* @returns Set of entities matching the query fragments.
|
318
|
-
*/
|
319
|
-
export function runQuery(fragments: QueryFragment[], initialSet?: Set<Entity>): Set<Entity> {
|
320
|
-
let entities: Set<Entity> | undefined = initialSet ? new Set([...initialSet]) : undefined; // Copy to a fresh set because it will be modified in place
|
321
|
-
let proxyRead: ProxyReadQueryFragment | undefined = undefined;
|
322
|
-
let proxyExpand: ProxyExpandQueryFragment | undefined = undefined;
|
323
|
-
|
324
|
-
// Process fragments
|
325
|
-
for (let i = 0; i < fragments.length; i++) {
|
326
|
-
const fragment = fragments[i];
|
327
|
-
if (isSettingFragment(fragment)) {
|
328
|
-
// Store setting fragments for subsequent query fragments
|
329
|
-
if (fragment.type === QueryFragmentType.ProxyRead) proxyRead = fragment;
|
330
|
-
if (fragment.type === QueryFragmentType.ProxyExpand) proxyExpand = fragment;
|
331
|
-
} else if (!entities) {
|
332
|
-
// Handle entity query fragments
|
333
|
-
// First regular fragment must be Has or HasValue
|
334
|
-
if (isNegativeFragment(fragment)) {
|
335
|
-
throw new Error("First EntityQueryFragment must be Has or HasValue");
|
336
|
-
}
|
337
|
-
|
338
|
-
// Create the first interim result
|
339
|
-
entities =
|
340
|
-
fragment.type === QueryFragmentType.Has
|
341
|
-
? new Set([...getComponentEntities(fragment.component)])
|
342
|
-
: getEntitiesWithValue(fragment.component, fragment.value);
|
343
|
-
|
344
|
-
// Add entity's children up to the specified depth if proxy expand is active
|
345
|
-
if (proxyExpand && proxyExpand.depth > 0) {
|
346
|
-
for (const entity of [...entities]) {
|
347
|
-
for (const childEntity of getChildEntities(entity, proxyExpand.component, proxyExpand.depth)) {
|
348
|
-
entities.add(childEntity);
|
349
|
-
}
|
350
|
-
}
|
351
|
-
}
|
352
|
-
} else {
|
353
|
-
// There already is an interim result, apply the current fragment
|
354
|
-
for (const entity of [...entities]) {
|
355
|
-
// Branch 1: Simple / check if the current entity passes the query fragment
|
356
|
-
let passes = passesQueryFragment(entity, fragment);
|
357
|
-
|
358
|
-
// Branch 2: Proxy upwards / check if proxy entity passes the query
|
359
|
-
if (proxyRead && proxyRead.depth > 0 && !isBreakingPassState(passes, fragment)) {
|
360
|
-
passes = passesQueryFragmentProxy(entity, fragment, proxyRead) ?? passes;
|
361
|
-
}
|
362
|
-
|
363
|
-
// If the entity didn't pass the query fragment, remove it from the interim set
|
364
|
-
if (!passes) entities.delete(entity);
|
365
|
-
|
366
|
-
// Branch 3: Proxy downwards / run the query fragments on child entities if proxy expand is active
|
367
|
-
if (proxyExpand && proxyExpand.depth > 0) {
|
368
|
-
const childEntities = getChildEntities(entity, proxyExpand.component, proxyExpand.depth);
|
369
|
-
for (const childEntity of childEntities) {
|
370
|
-
// Add the child entity if it passes the direct check
|
371
|
-
// or if a proxy read is active and it passes the proxy read check
|
372
|
-
if (
|
373
|
-
passesQueryFragment(childEntity, fragment) ||
|
374
|
-
(proxyRead && proxyRead.depth > 0 && passesQueryFragmentProxy(childEntity, fragment, proxyRead))
|
375
|
-
)
|
376
|
-
entities.add(childEntity);
|
377
|
-
}
|
378
|
-
}
|
379
|
-
}
|
380
|
-
}
|
381
|
-
}
|
382
|
-
|
383
|
-
return entities ?? new Set<Entity>();
|
384
|
-
}
|
385
|
-
|
386
|
-
/**
|
387
|
-
* Create a query object including an update$ stream and a Set of entities currently matching the query.
|
388
|
-
*
|
389
|
-
* @remarks
|
390
|
-
* `update$` stream needs to be subscribed to in order for the logic inside the stream to be executed and therefore
|
391
|
-
* in order for the `matching` set to be updated.
|
392
|
-
*
|
393
|
-
* `defineQuery` should be strongly preferred over `runQuery` if the query is used for systems or other
|
394
|
-
* use cases that repeatedly require the query result or updates to the query result. `defineQuery` does not
|
395
|
-
* reevaluate the entire query if an accessed component changes, but only performs the minimal set of checks
|
396
|
-
* on the updated entity to evaluate wether the entity still matches the query, resulting in significant performance
|
397
|
-
* advantages over `runQuery`.
|
398
|
-
*
|
399
|
-
* The query fragments are executed from left to right and are concatenated with a logical `AND`.
|
400
|
-
* For performance reasons, the most restrictive query fragment should be first in the list of query fragments,
|
401
|
-
* in order to reduce the number of entities the next query fragment needs to be checked for.
|
402
|
-
* If no proxy fragments are used, every entity in the resulting set passes every query fragment.
|
403
|
-
* If setting fragments are used, the order of the query fragments influences the result, since settings only apply to
|
404
|
-
* fragments after the setting fragment.
|
405
|
-
*
|
406
|
-
* @param fragments Query fragments to execute.
|
407
|
-
* @param options Optional: {
|
408
|
-
* runOnInit: if true, the query is executed once with `runQuery` to build an iniital Set of matching entities. If false only updates after the query was created are considered.
|
409
|
-
* initialSet: if given, this set is passed to `runOnInit` when building the initial Set of matching entities.
|
410
|
-
* }
|
411
|
-
* @returns Query object: {
|
412
|
-
* update$: RxJS stream of updates to the query result. The update contains the component update that caused the query update, as well as the {@link UpdateType update type}.
|
413
|
-
* matching: Mobx observable set of entities currently matching the query.
|
414
|
-
* }.
|
415
|
-
*/
|
416
|
-
export function defineQuery(
|
417
|
-
fragments: QueryFragment[],
|
418
|
-
options?: { runOnInit?: boolean; initialSet?: Set<Entity> },
|
419
|
-
): {
|
420
|
-
update$: Observable<ComponentUpdate & { type: UpdateType }>;
|
421
|
-
matching: ObservableSet<Entity>;
|
422
|
-
} {
|
423
|
-
const initialSet =
|
424
|
-
options?.runOnInit || options?.initialSet ? runQuery(fragments, options.initialSet) : new Set<Entity>();
|
425
|
-
|
426
|
-
const matching = observable(initialSet);
|
427
|
-
const initial$ = from(matching).pipe(toUpdateStream(fragments[0].component));
|
428
|
-
|
429
|
-
const containsProxy =
|
430
|
-
fragments.findIndex((v) => [QueryFragmentType.ProxyExpand, QueryFragmentType.ProxyRead].includes(v.type)) !== -1;
|
431
|
-
|
432
|
-
const internal$ = merge(...fragments.map((f) => f.component.update$)) // Combine all component update streams accessed accessed in this query
|
433
|
-
.pipe(
|
434
|
-
containsProxy // Query contains proxies
|
435
|
-
? concatMap((update) => {
|
436
|
-
// If the query contains proxy read or expand fragments, entities up or down the proxy chain might match due to this update.
|
437
|
-
// We have to run the entire query again and compare the result.
|
438
|
-
// TODO: We might be able to make this more efficient by first computing the set of entities that are potentially touched by this update
|
439
|
-
// and then only rerun the query on this set.
|
440
|
-
const newMatchingSet = runQuery(fragments, options?.initialSet);
|
441
|
-
const updates: (ComponentUpdate & { type: UpdateType })[] = [];
|
442
|
-
|
443
|
-
for (const previouslyMatchingEntity of matching) {
|
444
|
-
// Entity matched before but doesn't match now
|
445
|
-
if (!newMatchingSet.has(previouslyMatchingEntity)) {
|
446
|
-
matching.delete(previouslyMatchingEntity);
|
447
|
-
updates.push({
|
448
|
-
entity: previouslyMatchingEntity,
|
449
|
-
type: UpdateType.Exit,
|
450
|
-
component: update.component,
|
451
|
-
value: [undefined, undefined],
|
452
|
-
});
|
453
|
-
}
|
454
|
-
}
|
455
|
-
|
456
|
-
for (const matchingEntity of newMatchingSet) {
|
457
|
-
if (matching.has(matchingEntity)) {
|
458
|
-
// Entity matched before and still matches
|
459
|
-
updates.push({
|
460
|
-
entity: matchingEntity,
|
461
|
-
type: UpdateType.Update,
|
462
|
-
component: update.component,
|
463
|
-
value: [getComponentValue(update.component, matchingEntity), undefined],
|
464
|
-
});
|
465
|
-
} else {
|
466
|
-
// Entity didn't match before but matches now
|
467
|
-
matching.add(matchingEntity);
|
468
|
-
updates.push({
|
469
|
-
entity: matchingEntity,
|
470
|
-
type: UpdateType.Enter,
|
471
|
-
component: update.component,
|
472
|
-
value: [getComponentValue(update.component, matchingEntity), undefined],
|
473
|
-
});
|
474
|
-
}
|
475
|
-
}
|
476
|
-
|
477
|
-
return of(...updates);
|
478
|
-
})
|
479
|
-
: // Query does not contain proxies
|
480
|
-
map((update) => {
|
481
|
-
if (matching.has(update.entity)) {
|
482
|
-
// If this entity matched the query before, check if it still matches it
|
483
|
-
// Find fragments accessign this component (linear search is fine since the number fragments is likely small)
|
484
|
-
const relevantFragments = fragments.filter((f) => f.component.id === update.component.id);
|
485
|
-
const pass = relevantFragments.every((f) => passesQueryFragment(update.entity, f as EntityQueryFragment)); // We early return if the query contains proxies
|
486
|
-
|
487
|
-
if (pass) {
|
488
|
-
// Entity passed before and still passes, forward update
|
489
|
-
return { ...update, type: UpdateType.Update };
|
490
|
-
} else {
|
491
|
-
// Entity passed before but not anymore, forward update and exit
|
492
|
-
matching.delete(update.entity);
|
493
|
-
return { ...update, type: UpdateType.Exit };
|
494
|
-
}
|
495
|
-
}
|
496
|
-
|
497
|
-
// This entity didn't match before, check all fragments
|
498
|
-
const pass = fragments.every((f) => passesQueryFragment(update.entity, f as EntityQueryFragment)); // We early return if the query contains proxies
|
499
|
-
if (pass) {
|
500
|
-
// Entity didn't pass before but passes now, forward update end enter
|
501
|
-
matching.add(update.entity);
|
502
|
-
return { ...update, type: UpdateType.Enter };
|
503
|
-
}
|
504
|
-
}),
|
505
|
-
filterNullish(),
|
506
|
-
);
|
507
|
-
|
508
|
-
return {
|
509
|
-
matching,
|
510
|
-
update$: concat(initial$, internal$).pipe(share()),
|
511
|
-
};
|
512
|
-
}
|
513
|
-
|
514
|
-
/**
|
515
|
-
* Define a query object that only passes update events of type {@link UpdateType}.Update to the `update$` stream.
|
516
|
-
* See {@link defineQuery} for details.
|
517
|
-
*
|
518
|
-
* @param fragments Query fragments
|
519
|
-
* @returns Stream of component updates of entities that had already matched the query
|
520
|
-
*/
|
521
|
-
export function defineUpdateQuery(
|
522
|
-
fragments: QueryFragment[],
|
523
|
-
options?: { runOnInit?: boolean },
|
524
|
-
): Observable<ComponentUpdate & { type: UpdateType }> {
|
525
|
-
return defineQuery(fragments, options).update$.pipe(filter((e) => e.type === UpdateType.Update));
|
526
|
-
}
|
527
|
-
|
528
|
-
/**
|
529
|
-
* Define a query object that only passes update events of type {@link UpdateType}.Enter to the `update$` stream.
|
530
|
-
* See {@link defineQuery} for details.
|
531
|
-
*
|
532
|
-
* @param fragments Query fragments
|
533
|
-
* @returns Stream of component updates of entities matching the query for the first time
|
534
|
-
*/
|
535
|
-
export function defineEnterQuery(
|
536
|
-
fragments: QueryFragment[],
|
537
|
-
options?: { runOnInit?: boolean },
|
538
|
-
): Observable<ComponentUpdate> {
|
539
|
-
return defineQuery(fragments, options).update$.pipe(filter((e) => e.type === UpdateType.Enter));
|
540
|
-
}
|
541
|
-
|
542
|
-
/**
|
543
|
-
* Define a query object that only passes update events of type {@link UpdateType}.Exit to the `update$` stream.
|
544
|
-
* See {@link defineQuery} for details.
|
545
|
-
*
|
546
|
-
* @param fragments Query fragments
|
547
|
-
* @returns Stream of component updates of entities not matching the query anymore for the first time
|
548
|
-
*/
|
549
|
-
export function defineExitQuery(
|
550
|
-
fragments: QueryFragment[],
|
551
|
-
options?: { runOnInit?: boolean },
|
552
|
-
): Observable<ComponentUpdate> {
|
553
|
-
return defineQuery(fragments, options).update$.pipe(filter((e) => e.type === UpdateType.Exit));
|
554
|
-
}
|
package/src/System.spec.ts
DELETED
@@ -1,139 +0,0 @@
|
|
1
|
-
import { defineComponent, removeComponent, setComponent, withValue } from "./Component";
|
2
|
-
import { Type, UpdateType } from "./constants";
|
3
|
-
import { createEntity } from "./Entity";
|
4
|
-
import { Has } from "./Query";
|
5
|
-
import { defineEnterSystem, defineExitSystem, defineSystem, defineUpdateSystem } from "./System";
|
6
|
-
import { Component, Entity, World } from "./types";
|
7
|
-
import { createWorld } from "./World";
|
8
|
-
|
9
|
-
describe("System", () => {
|
10
|
-
let world: World;
|
11
|
-
|
12
|
-
beforeEach(() => {
|
13
|
-
world = createWorld();
|
14
|
-
});
|
15
|
-
|
16
|
-
describe("Systems", () => {
|
17
|
-
let Position: Component<{ x: number; y: number }>;
|
18
|
-
let entity: Entity;
|
19
|
-
|
20
|
-
beforeEach(() => {
|
21
|
-
Position = defineComponent(world, { x: Type.Number, y: Type.Number });
|
22
|
-
entity = createEntity(world, [withValue(Position, { x: 1, y: 2 })]);
|
23
|
-
});
|
24
|
-
|
25
|
-
it("defineSystem should rerun the system if the query result changes (enter, update, exit)", () => {
|
26
|
-
const mock = jest.fn();
|
27
|
-
defineSystem(world, [Has(Position)], mock);
|
28
|
-
|
29
|
-
setComponent(Position, entity, { x: 2, y: 3 });
|
30
|
-
|
31
|
-
expect(mock).toHaveBeenCalledTimes(2);
|
32
|
-
expect(mock).toHaveBeenCalledWith({
|
33
|
-
entity,
|
34
|
-
component: Position,
|
35
|
-
value: [
|
36
|
-
{ x: 2, y: 3 },
|
37
|
-
{ x: 1, y: 2 },
|
38
|
-
],
|
39
|
-
type: UpdateType.Update,
|
40
|
-
});
|
41
|
-
|
42
|
-
setComponent(Position, entity, { x: 3, y: 3 });
|
43
|
-
expect(mock).toHaveBeenCalledTimes(3);
|
44
|
-
expect(mock).toHaveBeenCalledWith({
|
45
|
-
entity,
|
46
|
-
component: Position,
|
47
|
-
value: [
|
48
|
-
{ x: 3, y: 3 },
|
49
|
-
{ x: 2, y: 3 },
|
50
|
-
],
|
51
|
-
type: UpdateType.Update,
|
52
|
-
});
|
53
|
-
|
54
|
-
removeComponent(Position, entity);
|
55
|
-
expect(mock).toHaveBeenCalledTimes(4);
|
56
|
-
expect(mock).toHaveBeenCalledWith({
|
57
|
-
entity,
|
58
|
-
component: Position,
|
59
|
-
value: [undefined, { x: 3, y: 3 }],
|
60
|
-
type: UpdateType.Exit,
|
61
|
-
});
|
62
|
-
});
|
63
|
-
|
64
|
-
it("defineUpdateSystem should rerun the system if the component value of an entity matchign the query changes", () => {
|
65
|
-
const mock = jest.fn();
|
66
|
-
defineUpdateSystem(world, [Has(Position)], mock);
|
67
|
-
|
68
|
-
// The entity already had a position when the system was created and the system runs on init,
|
69
|
-
// so this position update is an update
|
70
|
-
setComponent(Position, entity, { x: 2, y: 3 });
|
71
|
-
expect(mock).toHaveBeenCalledTimes(1);
|
72
|
-
|
73
|
-
setComponent(Position, entity, { x: 2, y: 3 });
|
74
|
-
expect(mock).toHaveBeenCalledTimes(2);
|
75
|
-
expect(mock).toHaveBeenCalledWith({
|
76
|
-
entity,
|
77
|
-
component: Position,
|
78
|
-
value: [
|
79
|
-
{ x: 2, y: 3 },
|
80
|
-
{ x: 2, y: 3 },
|
81
|
-
],
|
82
|
-
type: UpdateType.Update,
|
83
|
-
});
|
84
|
-
|
85
|
-
// Setting the same value again should rerun the system
|
86
|
-
setComponent(Position, entity, { x: 2, y: 3 });
|
87
|
-
expect(mock).toHaveBeenCalledTimes(3);
|
88
|
-
|
89
|
-
setComponent(Position, entity, { x: 3, y: 3 });
|
90
|
-
expect(mock).toHaveBeenCalledTimes(4);
|
91
|
-
});
|
92
|
-
|
93
|
-
it("defineEnterSystem should rerun once with entities matching the query for the first time", () => {
|
94
|
-
const CanMove = defineComponent(world, { value: Type.Boolean });
|
95
|
-
const mock = jest.fn();
|
96
|
-
|
97
|
-
defineEnterSystem(world, [Has(CanMove)], mock);
|
98
|
-
|
99
|
-
const entity1 = createEntity(world, [withValue(CanMove, { value: true })]);
|
100
|
-
|
101
|
-
expect(mock).toHaveBeenCalledTimes(1);
|
102
|
-
expect(mock).toHaveBeenCalledWith(
|
103
|
-
expect.objectContaining({ entity: entity1, component: CanMove, value: [{ value: true }, undefined] }),
|
104
|
-
);
|
105
|
-
|
106
|
-
const entity2 = createEntity(world, [withValue(CanMove, { value: true })]);
|
107
|
-
expect(mock).toHaveBeenCalledTimes(2);
|
108
|
-
expect(mock).toHaveBeenCalledWith(
|
109
|
-
expect.objectContaining({ entity: entity2, component: CanMove, value: [{ value: true }, undefined] }),
|
110
|
-
);
|
111
|
-
});
|
112
|
-
|
113
|
-
it("defineExitSystem should rerun once with entities not matching the query anymore", () => {
|
114
|
-
const CanMove = defineComponent(world, { value: Type.Boolean });
|
115
|
-
|
116
|
-
const mock = jest.fn();
|
117
|
-
defineExitSystem(world, [Has(CanMove)], mock);
|
118
|
-
|
119
|
-
const entity1 = createEntity(world, [withValue(CanMove, { value: true })]);
|
120
|
-
const entity2 = createEntity(world);
|
121
|
-
setComponent(CanMove, entity2, { value: true });
|
122
|
-
|
123
|
-
expect(mock).toHaveBeenCalledTimes(0);
|
124
|
-
|
125
|
-
removeComponent(CanMove, entity1);
|
126
|
-
|
127
|
-
expect(mock).toHaveBeenCalledTimes(1);
|
128
|
-
expect(mock).toHaveBeenCalledWith(
|
129
|
-
expect.objectContaining({ entity: entity1, component: CanMove, value: [undefined, { value: true }] }),
|
130
|
-
);
|
131
|
-
|
132
|
-
removeComponent(CanMove, entity2);
|
133
|
-
expect(mock).toHaveBeenCalledTimes(2);
|
134
|
-
expect(mock).toHaveBeenCalledWith(
|
135
|
-
expect.objectContaining({ entity: entity2, component: CanMove, value: [undefined, { value: true }] }),
|
136
|
-
);
|
137
|
-
});
|
138
|
-
});
|
139
|
-
});
|