@latticexyz/recs 2.0.12-main-9be2bb86 → 2.0.12-main-96e7bf43
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/Component.ts
DELETED
@@ -1,531 +0,0 @@
|
|
1
|
-
import { transformIterator, uuid } from "@latticexyz/utils";
|
2
|
-
import { mapObject } from "@latticexyz/utils";
|
3
|
-
import { filter, map, Subject } from "rxjs";
|
4
|
-
import { OptionalTypes } from "./constants";
|
5
|
-
import { createIndexer } from "./Indexer";
|
6
|
-
import {
|
7
|
-
Component,
|
8
|
-
ComponentValue,
|
9
|
-
Entity,
|
10
|
-
EntitySymbol,
|
11
|
-
Indexer,
|
12
|
-
Metadata,
|
13
|
-
OverridableComponent,
|
14
|
-
Override,
|
15
|
-
Schema,
|
16
|
-
World,
|
17
|
-
} from "./types";
|
18
|
-
import { isFullComponentValue, isIndexer } from "./utils";
|
19
|
-
import { getEntityString, getEntitySymbol } from "./Entity";
|
20
|
-
|
21
|
-
export type ComponentMutationOptions = {
|
22
|
-
/** Skip publishing this mutation to the component's update stream. Mostly used internally during initial hydration. */
|
23
|
-
skipUpdateStream?: boolean;
|
24
|
-
};
|
25
|
-
|
26
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
27
|
-
function getComponentName(component: Component<any, any, any>) {
|
28
|
-
return (
|
29
|
-
component.metadata?.componentName ??
|
30
|
-
component.metadata?.tableName ??
|
31
|
-
component.metadata?.tableId ??
|
32
|
-
component.metadata?.contractId ??
|
33
|
-
component.id
|
34
|
-
);
|
35
|
-
}
|
36
|
-
|
37
|
-
/**
|
38
|
-
* Components contain state indexed by entities and are one of the fundamental building blocks in ECS.
|
39
|
-
* Besides containing the state, components expose an rxjs update$ stream, that emits an event any time the value
|
40
|
-
* of an entity in this component is updated.
|
41
|
-
*
|
42
|
-
* @param world {@link World} object this component should be registered onto.
|
43
|
-
* @param schema {@link Schema} of component values. Uses Type enum as bridge between typescript types and javascript accessible values.
|
44
|
-
* @param options Optional: {
|
45
|
-
* id: descriptive id for this component (otherwise an autogenerated id is used),
|
46
|
-
* metadata: arbitrary metadata,
|
47
|
-
* indexed: if this flag is set, an indexer is applied to this component (see {@link createIndexer})
|
48
|
-
* }
|
49
|
-
* @returns Component object linked to the provided World
|
50
|
-
*
|
51
|
-
* @example
|
52
|
-
* ```
|
53
|
-
* const Position = defineComponent(world, { x: Type.Number, y: Type.Number }, { id: "Position" });
|
54
|
-
* ```
|
55
|
-
*/
|
56
|
-
export function defineComponent<S extends Schema, M extends Metadata, T = unknown>(
|
57
|
-
world: World,
|
58
|
-
schema: S,
|
59
|
-
options?: { id?: string; metadata?: M; indexed?: boolean },
|
60
|
-
) {
|
61
|
-
if (Object.keys(schema).length === 0) throw new Error("Component schema must have at least one key");
|
62
|
-
const id = options?.id ?? uuid();
|
63
|
-
const values = mapObject(schema, () => new Map());
|
64
|
-
const update$ = new Subject();
|
65
|
-
const metadata = options?.metadata;
|
66
|
-
const entities = () =>
|
67
|
-
transformIterator((Object.values(values)[0] as Map<EntitySymbol, unknown>).keys(), getEntityString);
|
68
|
-
let component = { values, schema, id, update$, metadata, entities, world } as Component<S, M, T>;
|
69
|
-
if (options?.indexed) component = createIndexer(component);
|
70
|
-
world.registerComponent(component as Component);
|
71
|
-
return component;
|
72
|
-
}
|
73
|
-
|
74
|
-
/**
|
75
|
-
* Set the value for a given entity in a given component.
|
76
|
-
*
|
77
|
-
* @param component {@link defineComponent Component} to be updated.
|
78
|
-
* @param entity {@link Entity} whose value in the given component should be set.
|
79
|
-
* @param value Value to set, schema must match the component schema.
|
80
|
-
*
|
81
|
-
* @example
|
82
|
-
* ```
|
83
|
-
* setComponent(Position, entity, { x: 1, y: 2 });
|
84
|
-
* ```
|
85
|
-
*/
|
86
|
-
export function setComponent<S extends Schema, T = unknown>(
|
87
|
-
component: Component<S, Metadata, T>,
|
88
|
-
entity: Entity,
|
89
|
-
value: ComponentValue<S, T>,
|
90
|
-
options: ComponentMutationOptions = {},
|
91
|
-
) {
|
92
|
-
const entitySymbol = getEntitySymbol(entity);
|
93
|
-
const prevValue = getComponentValue(component, entity);
|
94
|
-
for (const [key, val] of Object.entries(value)) {
|
95
|
-
if (component.values[key]) {
|
96
|
-
component.values[key].set(entitySymbol, val);
|
97
|
-
} else {
|
98
|
-
const isTableFieldIndex = component.metadata?.tableId && /^\d+$/.test(key);
|
99
|
-
if (!isTableFieldIndex) {
|
100
|
-
// If this key looks like a field index from `defineStoreComponents`,
|
101
|
-
// we can ignore this value without logging anything.
|
102
|
-
//
|
103
|
-
// Otherwise, we should let the user know we found undefined data.
|
104
|
-
console.warn(
|
105
|
-
"Component definition for",
|
106
|
-
getComponentName(component),
|
107
|
-
"is missing key",
|
108
|
-
key,
|
109
|
-
", ignoring value",
|
110
|
-
val,
|
111
|
-
"for entity",
|
112
|
-
entity,
|
113
|
-
". Existing keys: ",
|
114
|
-
Object.keys(component.values),
|
115
|
-
);
|
116
|
-
}
|
117
|
-
}
|
118
|
-
}
|
119
|
-
if (!options.skipUpdateStream) {
|
120
|
-
component.update$.next({ entity, value: [value, prevValue], component });
|
121
|
-
}
|
122
|
-
}
|
123
|
-
|
124
|
-
/**
|
125
|
-
* Update the value for a given entity in a given component while keeping the old value of keys not included in the update.
|
126
|
-
*
|
127
|
-
* @param component {@link defineComponent Component} to be updated.
|
128
|
-
* @param entity {@link Entity} whose value in the given component should be updated.
|
129
|
-
* @param value Partial value to be set, remaining keys will be taken from the existing component value.
|
130
|
-
*
|
131
|
-
* @remarks
|
132
|
-
* This function fails silently during runtime if a partial value is set for an entity that
|
133
|
-
* does not have a component value yet, since then a partial value will be set in the component for this entity.
|
134
|
-
*
|
135
|
-
* @example
|
136
|
-
* ```
|
137
|
-
* updateComponent(Position, entity, { x: 1 });
|
138
|
-
* ```
|
139
|
-
*/
|
140
|
-
export function updateComponent<S extends Schema, T = unknown>(
|
141
|
-
component: Component<S, Metadata, T>,
|
142
|
-
entity: Entity,
|
143
|
-
value: Partial<ComponentValue<S, T>>,
|
144
|
-
initialValue?: ComponentValue<S, T>,
|
145
|
-
options: ComponentMutationOptions = {},
|
146
|
-
) {
|
147
|
-
const currentValue = getComponentValue(component, entity);
|
148
|
-
if (currentValue === undefined) {
|
149
|
-
if (initialValue === undefined) {
|
150
|
-
throw new Error(`Can't update component ${getComponentName(component)} without a current value or initial value`);
|
151
|
-
}
|
152
|
-
setComponent(component, entity, { ...initialValue, ...value }, options);
|
153
|
-
} else {
|
154
|
-
setComponent(component, entity, { ...currentValue, ...value }, options);
|
155
|
-
}
|
156
|
-
}
|
157
|
-
|
158
|
-
/**
|
159
|
-
* Remove a given entity from a given component.
|
160
|
-
*
|
161
|
-
* @param component {@link defineComponent Component} to be updated.
|
162
|
-
* @param entity {@link Entity} whose value should be removed from this component.
|
163
|
-
*/
|
164
|
-
export function removeComponent<S extends Schema, M extends Metadata, T = unknown>(
|
165
|
-
component: Component<S, M, T>,
|
166
|
-
entity: Entity,
|
167
|
-
options: ComponentMutationOptions = {},
|
168
|
-
) {
|
169
|
-
const entitySymbol = getEntitySymbol(entity);
|
170
|
-
const prevValue = getComponentValue(component, entity);
|
171
|
-
for (const key of Object.keys(component.values)) {
|
172
|
-
component.values[key].delete(entitySymbol);
|
173
|
-
}
|
174
|
-
if (!options.skipUpdateStream) {
|
175
|
-
component.update$.next({ entity, value: [undefined, prevValue], component });
|
176
|
-
}
|
177
|
-
}
|
178
|
-
|
179
|
-
/**
|
180
|
-
* Check whether a component contains a value for a given entity.
|
181
|
-
*
|
182
|
-
* @param component {@link defineComponent Component} to check whether it has a value for the given entity.
|
183
|
-
* @param entity {@link Entity} to check whether it has a value in the given component.
|
184
|
-
* @returns true if the component contains a value for the given entity, else false.
|
185
|
-
*/
|
186
|
-
export function hasComponent<S extends Schema, T = unknown>(
|
187
|
-
component: Component<S, Metadata, T>,
|
188
|
-
entity: Entity,
|
189
|
-
): boolean {
|
190
|
-
const entitySymbol = getEntitySymbol(entity);
|
191
|
-
const map = Object.values(component.values)[0];
|
192
|
-
return map.has(entitySymbol);
|
193
|
-
}
|
194
|
-
|
195
|
-
/**
|
196
|
-
* Get the value of a given entity in the given component.
|
197
|
-
* Returns undefined if no value or only a partial value is found.
|
198
|
-
*
|
199
|
-
* @param component {@link defineComponent Component} to get the value from for the given entity.
|
200
|
-
* @param entity {@link Entity} to get the value for from the given component.
|
201
|
-
* @returns Value of the given entity in the given component or undefined if no value exists.
|
202
|
-
*/
|
203
|
-
export function getComponentValue<S extends Schema, T = unknown>(
|
204
|
-
component: Component<S, Metadata, T>,
|
205
|
-
entity: Entity,
|
206
|
-
): ComponentValue<S, T> | undefined {
|
207
|
-
const value: Record<string, unknown> = {};
|
208
|
-
const entitySymbol = getEntitySymbol(entity);
|
209
|
-
|
210
|
-
// Get the value of each schema key
|
211
|
-
const schemaKeys = Object.keys(component.schema);
|
212
|
-
for (const key of schemaKeys) {
|
213
|
-
const val = component.values[key].get(entitySymbol);
|
214
|
-
if (val === undefined && !OptionalTypes.includes(component.schema[key])) return undefined;
|
215
|
-
value[key] = val;
|
216
|
-
}
|
217
|
-
|
218
|
-
return value as ComponentValue<S, T>;
|
219
|
-
}
|
220
|
-
|
221
|
-
/**
|
222
|
-
* Get the value of a given entity in the given component.
|
223
|
-
* Throws an error if no value exists for the given entity in the given component.
|
224
|
-
*
|
225
|
-
* @param component {@link defineComponent Component} to get the value from for the given entity.
|
226
|
-
* @param entity {@link Entity} of the entity to get the value for from the given component.
|
227
|
-
* @returns Value of the given entity in the given component.
|
228
|
-
*
|
229
|
-
* @remarks
|
230
|
-
* Throws an error if no value exists in the component for the given entity.
|
231
|
-
*/
|
232
|
-
export function getComponentValueStrict<S extends Schema, T = unknown>(
|
233
|
-
component: Component<S, Metadata, T>,
|
234
|
-
entity: Entity,
|
235
|
-
): ComponentValue<S, T> {
|
236
|
-
const value = getComponentValue(component, entity);
|
237
|
-
if (!value) throw new Error(`No value for component ${getComponentName(component)} on entity ${entity}`);
|
238
|
-
return value;
|
239
|
-
}
|
240
|
-
|
241
|
-
/**
|
242
|
-
* Compare two {@link ComponentValue}s.
|
243
|
-
* `a` can be a partial component value, in which case only the keys present in `a` are compared to the corresponding keys in `b`.
|
244
|
-
*
|
245
|
-
* @param a Partial {@link ComponentValue} to compare to `b`
|
246
|
-
* @param b Component value to compare `a` to.
|
247
|
-
* @returns True if `a` equals `b` in the keys present in a or neither `a` nor `b` are defined, else false.
|
248
|
-
*
|
249
|
-
* @example
|
250
|
-
* ```
|
251
|
-
* componentValueEquals({ x: 1, y: 2 }, { x: 1, y: 3 }) // returns false because value of y doesn't match
|
252
|
-
* componentValueEquals({ x: 1 }, { x: 1, y: 3 }) // returns true because x is equal and y is not present in a
|
253
|
-
* ```
|
254
|
-
*/
|
255
|
-
export function componentValueEquals<S extends Schema, T = unknown>(
|
256
|
-
a?: Partial<ComponentValue<S, T>>,
|
257
|
-
b?: ComponentValue<S, T>,
|
258
|
-
): boolean {
|
259
|
-
if (!a && !b) return true;
|
260
|
-
if (!a || !b) return false;
|
261
|
-
|
262
|
-
let equals = true;
|
263
|
-
for (const key of Object.keys(a)) {
|
264
|
-
equals = a[key] === b[key];
|
265
|
-
if (!equals) return false;
|
266
|
-
}
|
267
|
-
return equals;
|
268
|
-
}
|
269
|
-
|
270
|
-
/**
|
271
|
-
* Util to create a tuple of a component and value with matching schema.
|
272
|
-
* (Used to enforce Typescript type safety.)
|
273
|
-
*
|
274
|
-
* @param component {@link defineComponent Component} with {@link ComponentSchema} `S`
|
275
|
-
* @param value {@link ComponentValue} with {@link ComponentSchema} `S`
|
276
|
-
* @returns Tuple `[component, value]`
|
277
|
-
*/
|
278
|
-
export function withValue<S extends Schema, T = unknown>(
|
279
|
-
component: Component<S, Metadata, T>,
|
280
|
-
value: ComponentValue<S, T>,
|
281
|
-
): [Component<S, Metadata, T>, ComponentValue<S, T>] {
|
282
|
-
return [component, value];
|
283
|
-
}
|
284
|
-
|
285
|
-
/**
|
286
|
-
* Get a set of entities that have the given component value in the given component.
|
287
|
-
*
|
288
|
-
* @param component {@link defineComponent Component} to get entities with the given value from.
|
289
|
-
* @param value look for entities with this {@link ComponentValue}.
|
290
|
-
* @returns Set with {@link Entity Entities} with the given component value.
|
291
|
-
*/
|
292
|
-
export function getEntitiesWithValue<S extends Schema>(
|
293
|
-
component: Component<S> | Indexer<S>,
|
294
|
-
value: Partial<ComponentValue<S>>,
|
295
|
-
): Set<Entity> {
|
296
|
-
// Shortcut for indexers
|
297
|
-
if (isIndexer(component) && isFullComponentValue(component, value)) {
|
298
|
-
return component.getEntitiesWithValue(value);
|
299
|
-
}
|
300
|
-
|
301
|
-
// Trivial implementation for regular components
|
302
|
-
const entities = new Set<Entity>();
|
303
|
-
for (const entity of getComponentEntities(component)) {
|
304
|
-
const val = getComponentValue(component, entity);
|
305
|
-
if (componentValueEquals(value, val)) {
|
306
|
-
entities.add(entity);
|
307
|
-
}
|
308
|
-
}
|
309
|
-
return entities;
|
310
|
-
}
|
311
|
-
|
312
|
-
/**
|
313
|
-
* Get a set of all entities of the given component.
|
314
|
-
*
|
315
|
-
* @param component {@link defineComponent Component} to get all entities from
|
316
|
-
* @returns Set of all entities in the given component.
|
317
|
-
*/
|
318
|
-
export function getComponentEntities<S extends Schema, T = unknown>(
|
319
|
-
component: Component<S, Metadata, T>,
|
320
|
-
): IterableIterator<Entity> {
|
321
|
-
return component.entities();
|
322
|
-
}
|
323
|
-
|
324
|
-
/**
|
325
|
-
* An overridable component is a mirror of the source component, with functions to lazily override specific entity values.
|
326
|
-
* Lazily override means the values are not actually set to the source component, but the override is only returned if the value is read.
|
327
|
-
*
|
328
|
-
* - When an override for an entity is added to the component, the override is propagated via the component's `update$` stream.
|
329
|
-
* - While an override is set for a specific entity, no updates to the source component for this entity will be propagated to the `update$` stream.
|
330
|
-
* - When an override is removed for a specific entity and there are more overrides targeting this entity,
|
331
|
-
* the override with the highest nonce will be propagated to the `update$` stream.
|
332
|
-
* - When an override is removed for a specific entity and there are no more overrides targeting this entity,
|
333
|
-
* the non-overridden underlying component value of this entity will be propagated to the `update$` stream.
|
334
|
-
*
|
335
|
-
* @param component {@link defineComponent Component} to use as underlying source for the overridable component
|
336
|
-
* @returns overridable component
|
337
|
-
*/
|
338
|
-
export function overridableComponent<S extends Schema, M extends Metadata, T = unknown>(
|
339
|
-
component: Component<S, M, T>,
|
340
|
-
): OverridableComponent<S, M, T> {
|
341
|
-
let nonce = 0;
|
342
|
-
|
343
|
-
// Map from OverrideId to Override (to be able to add multiple overrides to the same Entity)
|
344
|
-
const overrides = new Map<string, { update: Override<S, T>; nonce: number }>();
|
345
|
-
|
346
|
-
// Map from EntitySymbol to current overridden component value
|
347
|
-
const overriddenEntityValues = new Map<EntitySymbol, Partial<ComponentValue<S, T>> | null>();
|
348
|
-
|
349
|
-
// Update event stream that takes into account overridden entity values
|
350
|
-
const update$ = new Subject<{
|
351
|
-
entity: Entity;
|
352
|
-
value: [ComponentValue<S, T> | undefined, ComponentValue<S, T> | undefined];
|
353
|
-
component: Component<S, Metadata, T>;
|
354
|
-
}>();
|
355
|
-
|
356
|
-
// Add a new override to some entity
|
357
|
-
function addOverride(id: string, update: Override<S, T>) {
|
358
|
-
overrides.set(id, { update, nonce: nonce++ });
|
359
|
-
setOverriddenComponentValue(update.entity, update.value);
|
360
|
-
}
|
361
|
-
|
362
|
-
// Remove an override from an entity
|
363
|
-
function removeOverride(id: string) {
|
364
|
-
const affectedEntity = overrides.get(id)?.update.entity;
|
365
|
-
overrides.delete(id);
|
366
|
-
|
367
|
-
if (affectedEntity == null) return;
|
368
|
-
|
369
|
-
// If there are more overries affecting this entity,
|
370
|
-
// set the overriddenEntityValue to the last override
|
371
|
-
const relevantOverrides = [...overrides.values()]
|
372
|
-
.filter((o) => o.update.entity === affectedEntity)
|
373
|
-
.sort((a, b) => (a.nonce < b.nonce ? -1 : 1));
|
374
|
-
|
375
|
-
if (relevantOverrides.length > 0) {
|
376
|
-
const lastOverride = relevantOverrides[relevantOverrides.length - 1];
|
377
|
-
setOverriddenComponentValue(affectedEntity, lastOverride.update.value);
|
378
|
-
} else {
|
379
|
-
setOverriddenComponentValue(affectedEntity, undefined);
|
380
|
-
}
|
381
|
-
}
|
382
|
-
|
383
|
-
// Internal function to get the current overridden value or value of the source component
|
384
|
-
function getOverriddenComponentValue(entity: Entity): ComponentValue<S, T> | undefined {
|
385
|
-
const originalValue = getComponentValue(component, entity);
|
386
|
-
const entitySymbol = getEntitySymbol(entity);
|
387
|
-
const overriddenValue = overriddenEntityValues.get(entitySymbol);
|
388
|
-
return (originalValue || overriddenValue) && overriddenValue !== null // null is a valid override, in this case return undefined
|
389
|
-
? ({ ...originalValue, ...overriddenValue } as ComponentValue<S, T>)
|
390
|
-
: undefined;
|
391
|
-
}
|
392
|
-
|
393
|
-
const valueProxyHandler: (key: keyof S) => ProxyHandler<(typeof component.values)[typeof key]> = (key: keyof S) => ({
|
394
|
-
get(target, prop) {
|
395
|
-
// Intercept calls to component.value[key].get(entity)
|
396
|
-
if (prop === "get") {
|
397
|
-
return (entity: EntitySymbol) => {
|
398
|
-
const originalValue = target.get(entity);
|
399
|
-
const overriddenValue = overriddenEntityValues.get(entity);
|
400
|
-
return overriddenValue && overriddenValue[key] != null ? overriddenValue[key] : originalValue;
|
401
|
-
};
|
402
|
-
}
|
403
|
-
|
404
|
-
// Intercept calls to component.value[key].has(entity)
|
405
|
-
if (prop === "has") {
|
406
|
-
return (entity: EntitySymbol) => {
|
407
|
-
return target.has(entity) || overriddenEntityValues.has(entity);
|
408
|
-
};
|
409
|
-
}
|
410
|
-
|
411
|
-
// Intercept calls to component.value[key].keys()
|
412
|
-
if (prop === "keys") {
|
413
|
-
return () => new Set([...target.keys(), ...overriddenEntityValues.keys()]).values();
|
414
|
-
}
|
415
|
-
|
416
|
-
return Reflect.get(target, prop, target);
|
417
|
-
},
|
418
|
-
});
|
419
|
-
|
420
|
-
const partialValues: Partial<Component<S, M, T>["values"]> = {};
|
421
|
-
for (const key of Object.keys(component.values) as (keyof S)[]) {
|
422
|
-
partialValues[key] = new Proxy(component.values[key], valueProxyHandler(key));
|
423
|
-
}
|
424
|
-
const valuesProxy = partialValues as Component<S, M, T>["values"];
|
425
|
-
|
426
|
-
const overriddenComponent = new Proxy(component, {
|
427
|
-
get(target, prop) {
|
428
|
-
if (prop === "addOverride") return addOverride;
|
429
|
-
if (prop === "removeOverride") return removeOverride;
|
430
|
-
if (prop === "values") return valuesProxy;
|
431
|
-
if (prop === "update$") return update$;
|
432
|
-
if (prop === "entities")
|
433
|
-
return () =>
|
434
|
-
new Set([
|
435
|
-
...transformIterator(overriddenEntityValues.keys(), getEntityString),
|
436
|
-
...target.entities(),
|
437
|
-
]).values();
|
438
|
-
|
439
|
-
return Reflect.get(target, prop);
|
440
|
-
},
|
441
|
-
has(target, prop) {
|
442
|
-
if (prop === "addOverride" || prop === "removeOverride") return true;
|
443
|
-
return prop in target;
|
444
|
-
},
|
445
|
-
}) as OverridableComponent<S, M, T>;
|
446
|
-
|
447
|
-
// Internal function to set the current overridden component value and emit the update event
|
448
|
-
function setOverriddenComponentValue(entity: Entity, value?: Partial<ComponentValue<S, T>> | null) {
|
449
|
-
const entitySymbol = getEntitySymbol(entity);
|
450
|
-
// Check specifically for undefined - null is a valid override
|
451
|
-
const prevValue = getOverriddenComponentValue(entity);
|
452
|
-
if (value !== undefined) overriddenEntityValues.set(entitySymbol, value);
|
453
|
-
else overriddenEntityValues.delete(entitySymbol);
|
454
|
-
update$.next({ entity, value: [getOverriddenComponentValue(entity), prevValue], component: overriddenComponent });
|
455
|
-
}
|
456
|
-
|
457
|
-
// Channel through update events from the original component if there are no overrides
|
458
|
-
component.update$
|
459
|
-
.pipe(
|
460
|
-
filter((e) => !overriddenEntityValues.get(getEntitySymbol(e.entity))),
|
461
|
-
map((update) => ({ ...update, component: overriddenComponent })),
|
462
|
-
)
|
463
|
-
.subscribe(update$);
|
464
|
-
|
465
|
-
return overriddenComponent;
|
466
|
-
}
|
467
|
-
|
468
|
-
function getLocalCacheId(component: Component, uniqueWorldIdentifier?: string): string {
|
469
|
-
return `localcache-${uniqueWorldIdentifier}-${component.id}`;
|
470
|
-
}
|
471
|
-
|
472
|
-
export function clearLocalCache(component: Component, uniqueWorldIdentifier?: string): void {
|
473
|
-
localStorage.removeItem(getLocalCacheId(component, uniqueWorldIdentifier));
|
474
|
-
}
|
475
|
-
|
476
|
-
// Note: Only proof of concept for now - use this only for component that do not update frequently
|
477
|
-
export function createLocalCache<S extends Schema, M extends Metadata, T = unknown>(
|
478
|
-
component: Component<S, M, T>,
|
479
|
-
uniqueWorldIdentifier?: string,
|
480
|
-
): Component<S, M, T> {
|
481
|
-
const { world, update$, values } = component;
|
482
|
-
const cacheId = getLocalCacheId(component as Component, uniqueWorldIdentifier);
|
483
|
-
let numUpdates = 0;
|
484
|
-
const creation = Date.now();
|
485
|
-
|
486
|
-
// On creation, check if this component has locally cached values
|
487
|
-
const encodedCache = localStorage.getItem(cacheId);
|
488
|
-
if (encodedCache) {
|
489
|
-
const cache = JSON.parse(encodedCache) as [string, [Entity, unknown][]][];
|
490
|
-
const state: { [entity: Entity]: { [key: string]: unknown } } = {};
|
491
|
-
|
492
|
-
for (const [key, values] of cache) {
|
493
|
-
for (const [entity, value] of values) {
|
494
|
-
state[entity] = state[entity] || {};
|
495
|
-
state[entity][key] = value;
|
496
|
-
}
|
497
|
-
}
|
498
|
-
|
499
|
-
for (const [entityId, value] of Object.entries(state)) {
|
500
|
-
const entity = world.registerEntity({ id: entityId });
|
501
|
-
setComponent(component, entity, value as ComponentValue<S, T>);
|
502
|
-
}
|
503
|
-
|
504
|
-
console.info("Loading component", getComponentName(component), "from local cache.");
|
505
|
-
}
|
506
|
-
|
507
|
-
// Flush the entire component to the local cache every time it updates.
|
508
|
-
// Note: this is highly unperformant and should only be used for components that
|
509
|
-
// don't update often and don't have many values
|
510
|
-
const updateSub = update$.subscribe(() => {
|
511
|
-
numUpdates++;
|
512
|
-
const encoded = JSON.stringify(
|
513
|
-
Object.entries(mapObject(values, (m) => [...m.entries()].map((e) => [getEntityString(e[0]), e[1]]))),
|
514
|
-
);
|
515
|
-
localStorage.setItem(cacheId, encoded);
|
516
|
-
if (numUpdates > 200) {
|
517
|
-
console.warn(
|
518
|
-
"Component",
|
519
|
-
getComponentName(component),
|
520
|
-
"was locally cached",
|
521
|
-
numUpdates,
|
522
|
-
"times since",
|
523
|
-
new Date(creation).toLocaleTimeString(),
|
524
|
-
"- the local cache is in an alpha state and should not be used with components that update frequently yet",
|
525
|
-
);
|
526
|
-
}
|
527
|
-
});
|
528
|
-
component.world.registerDisposer(() => updateSub?.unsubscribe());
|
529
|
-
|
530
|
-
return component;
|
531
|
-
}
|
package/src/Entity.spec.ts
DELETED
@@ -1,45 +0,0 @@
|
|
1
|
-
import { defineComponent, getComponentValue, hasComponent, withValue } from "./Component";
|
2
|
-
import { Type } from "./constants";
|
3
|
-
import { createEntity } from "./Entity";
|
4
|
-
import { World } from "./types";
|
5
|
-
import { createWorld } from "./World";
|
6
|
-
|
7
|
-
describe("Entity", () => {
|
8
|
-
let world: World;
|
9
|
-
|
10
|
-
beforeEach(() => {
|
11
|
-
world = createWorld();
|
12
|
-
});
|
13
|
-
|
14
|
-
describe("createEntity", () => {
|
15
|
-
it("should return a unique id", () => {
|
16
|
-
const firstEntity = createEntity(world);
|
17
|
-
const secondEntity = createEntity(world);
|
18
|
-
expect(firstEntity).not.toEqual(secondEntity);
|
19
|
-
});
|
20
|
-
|
21
|
-
it("should register the entity in the world", () => {
|
22
|
-
expect([...world.getEntities()].length).toEqual(0);
|
23
|
-
createEntity(world);
|
24
|
-
expect([...world.getEntities()].length).toEqual(1);
|
25
|
-
});
|
26
|
-
|
27
|
-
it("should create an entity with given components and values", () => {
|
28
|
-
const Position = defineComponent(world, { x: Type.Number, y: Type.Number });
|
29
|
-
const CanMove = defineComponent(world, { value: Type.Boolean });
|
30
|
-
|
31
|
-
const value1 = { x: 1, y: 1 };
|
32
|
-
const value2 = { x: 2, y: 1 };
|
33
|
-
|
34
|
-
const movableEntity = createEntity(world, [withValue(Position, value1), withValue(CanMove, { value: true })]);
|
35
|
-
|
36
|
-
const staticEntity = createEntity(world, [withValue(Position, value2)]);
|
37
|
-
|
38
|
-
expect(getComponentValue(Position, movableEntity)).toEqual(value1);
|
39
|
-
expect(hasComponent(CanMove, movableEntity)).toBe(true);
|
40
|
-
|
41
|
-
expect(getComponentValue(Position, staticEntity)).toEqual(value2);
|
42
|
-
expect(hasComponent(CanMove, staticEntity)).toBe(false);
|
43
|
-
});
|
44
|
-
});
|
45
|
-
});
|
package/src/Entity.ts
DELETED
@@ -1,46 +0,0 @@
|
|
1
|
-
import { setComponent } from "./Component";
|
2
|
-
import { Component, ComponentValue, Entity, EntitySymbol, World } from "./types";
|
3
|
-
|
4
|
-
/**
|
5
|
-
* Register a new entity in the given {@link World} and initialize it with the given {@link ComponentValue}s.
|
6
|
-
*
|
7
|
-
* @param world World object this entity should be registered in.
|
8
|
-
* @param components Array of [{@link defineComponent Component}, {@link ComponentValue}] tuples to be added to this entity.
|
9
|
-
* (Use {@link withValue} to generate these tuples with type safety.)
|
10
|
-
* @param options Optional: {
|
11
|
-
* id: {@link Entity} for this entity. Use this for entities that were created outside of recs.
|
12
|
-
* idSuffix: string to be appended to the auto-generated id. Use this for improved readability. Do not use this if the `id` option is provided.
|
13
|
-
* }
|
14
|
-
* @returns index of this entity in the {@link World}. This {@link Entity} is used to refer to this entity in other recs methods (eg {@link setComponent}).
|
15
|
-
* (This is to avoid having to store strings in every component.)
|
16
|
-
*/
|
17
|
-
export function createEntity(
|
18
|
-
world: World,
|
19
|
-
components?: [Component, ComponentValue][],
|
20
|
-
options?: { id?: string } | { idSuffix?: string },
|
21
|
-
): Entity {
|
22
|
-
const entity = world.registerEntity(options ?? {});
|
23
|
-
|
24
|
-
if (components) {
|
25
|
-
for (const [component, value] of components) {
|
26
|
-
setComponent(component, entity, value);
|
27
|
-
}
|
28
|
-
}
|
29
|
-
|
30
|
-
return entity;
|
31
|
-
}
|
32
|
-
|
33
|
-
/*
|
34
|
-
* Get the symbol corresponding to an entity's string ID.
|
35
|
-
* Entities are represented as symbols internally for memory efficiency.
|
36
|
-
*/
|
37
|
-
export function getEntitySymbol(entityString: string): EntitySymbol {
|
38
|
-
return Symbol.for(entityString) as EntitySymbol;
|
39
|
-
}
|
40
|
-
|
41
|
-
/**
|
42
|
-
* Get the underlying entity string of an entity symbol.
|
43
|
-
*/
|
44
|
-
export function getEntityString(entity: EntitySymbol): Entity {
|
45
|
-
return Symbol.keyFor(entity) as Entity;
|
46
|
-
}
|