@latticexyz/recs 2.0.0-next.0 → 2.0.0-next.10

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.
@@ -0,0 +1,275 @@
1
+ import {
2
+ defineComponent,
3
+ setComponent,
4
+ removeComponent,
5
+ getComponentValue,
6
+ hasComponent,
7
+ withValue,
8
+ componentValueEquals,
9
+ getEntitiesWithValue,
10
+ overridableComponent,
11
+ } from "./Component";
12
+ import { Type } from "./constants";
13
+ import { createEntity, getEntitySymbol } from "./Entity";
14
+ import { AnyComponent, Entity, World } from "./types";
15
+ import { createWorld } from "./World";
16
+
17
+ describe("Component", () => {
18
+ let world: World;
19
+
20
+ beforeEach(() => {
21
+ world = createWorld();
22
+ });
23
+
24
+ it("emit changes to its stream", () => {
25
+ const entity = createEntity(world);
26
+ const component = defineComponent(world, { x: Type.Number, y: Type.Number });
27
+
28
+ const mock = jest.fn();
29
+ component.update$.subscribe((update) => {
30
+ mock(update);
31
+ });
32
+
33
+ setComponent(component, entity, { x: 1, y: 2 });
34
+ setComponent(component, entity, { x: 7, y: 2 });
35
+ setComponent(component, entity, { x: 7, y: 2 });
36
+ removeComponent(component, entity);
37
+
38
+ expect(mock).toHaveBeenNthCalledWith(1, { entity, value: [{ x: 1, y: 2 }, undefined], component });
39
+ expect(mock).toHaveBeenNthCalledWith(2, {
40
+ entity,
41
+ component,
42
+ value: [
43
+ { x: 7, y: 2 },
44
+ { x: 1, y: 2 },
45
+ ],
46
+ });
47
+ expect(mock).toHaveBeenNthCalledWith(3, {
48
+ entity,
49
+ component,
50
+ value: [
51
+ { x: 7, y: 2 },
52
+ { x: 7, y: 2 },
53
+ ],
54
+ });
55
+ expect(mock).toHaveBeenNthCalledWith(4, { entity, component, value: [undefined, { x: 7, y: 2 }] });
56
+ });
57
+
58
+ describe("defineComponent", () => {
59
+ it("should register the component in the world", () => {
60
+ expect(world.components.length).toBe(0);
61
+ defineComponent(world, { value: Type.Boolean });
62
+ expect(world.components.length).toBe(1);
63
+ });
64
+ });
65
+
66
+ describe("setComponent", () => {
67
+ let component: AnyComponent;
68
+ let entity: Entity;
69
+ let value: number;
70
+
71
+ beforeEach(() => {
72
+ component = defineComponent(world, { value: Type.Number });
73
+ entity = createEntity(world);
74
+ value = 1;
75
+ setComponent(component, entity, { value });
76
+ });
77
+
78
+ it("should store the component value", () => {
79
+ expect(component.values.value.get(getEntitySymbol(entity))).toBe(value);
80
+ });
81
+
82
+ it("should store the entity", () => {
83
+ expect(hasComponent(component, entity)).toBe(true);
84
+ });
85
+
86
+ it.todo("should store the value array");
87
+ });
88
+
89
+ describe("removeComponent", () => {
90
+ let component: AnyComponent;
91
+ let entity: Entity;
92
+ let value: number;
93
+
94
+ beforeEach(() => {
95
+ component = defineComponent(world, { value: Type.Number });
96
+ entity = createEntity(world);
97
+ value = 1;
98
+ setComponent(component, entity, { value });
99
+ removeComponent(component, entity);
100
+ });
101
+
102
+ it("should remove the component value", () => {
103
+ expect(component.values.value.get(getEntitySymbol(entity))).toBe(undefined);
104
+ });
105
+
106
+ it("should remove the entity", () => {
107
+ expect(hasComponent(component, entity)).toBe(false);
108
+ });
109
+
110
+ // it("shouldremove the component from the entity's component set", () => {
111
+ // expect(world.entities.get(entity)?.has(component)).toBe(false);
112
+ // });
113
+ });
114
+
115
+ describe("hasComponent", () => {
116
+ it("should return true if the entity has the component", () => {
117
+ const component = defineComponent(world, { x: Type.Number, y: Type.Number });
118
+ const entity = createEntity(world);
119
+ const value = { x: 1, y: 2 };
120
+ setComponent(component, entity, value);
121
+
122
+ expect(hasComponent(component, entity)).toEqual(true);
123
+ });
124
+ });
125
+
126
+ describe("getComponentValue", () => {
127
+ it("should return the correct component value", () => {
128
+ const component = defineComponent(world, { x: Type.Number, y: Type.Number });
129
+ const entity = createEntity(world);
130
+ const value = { x: 1, y: 2 };
131
+ setComponent(component, entity, value);
132
+
133
+ const receivedValue = getComponentValue(component, entity);
134
+ expect(receivedValue).toEqual(value);
135
+ });
136
+ });
137
+
138
+ describe("getComponentValueStrict", () => {
139
+ it.todo("should return the correct component value");
140
+ it.todo("should error if the component value does not exist");
141
+ });
142
+
143
+ describe("componentValueEquals", () => {
144
+ const value1 = { x: 1, y: 2, z: "x" };
145
+ const value2 = { x: 1, y: 2, z: "x" };
146
+ const value3 = { x: "1", y: 2, z: "x" };
147
+
148
+ expect(componentValueEquals(value1, value2)).toBe(true);
149
+ expect(componentValueEquals(value2, value3)).toBe(false);
150
+ });
151
+
152
+ describe("withValue", () => {
153
+ it("should return a ComponentWithValue", () => {
154
+ const component = defineComponent(world, { x: Type.Number, y: Type.Number });
155
+ const value = { x: 1, y: 2 };
156
+ const componentWithValue = withValue(component, value);
157
+ expect(componentWithValue).toEqual([component, value]);
158
+ });
159
+ });
160
+
161
+ describe("getEntitiesWithValue", () => {
162
+ it("Should return all and only entities with this value", () => {
163
+ const Position = defineComponent(world, { x: Type.Number, y: Type.Number });
164
+ const entity1 = createEntity(world, [withValue(Position, { x: 1, y: 2 })]);
165
+ createEntity(world, [withValue(Position, { x: 2, y: 1 })]);
166
+ createEntity(world);
167
+ const entity4 = createEntity(world, [withValue(Position, { x: 1, y: 2 })]);
168
+
169
+ expect(getEntitiesWithValue(Position, { x: 1, y: 2 })).toEqual(new Set([entity1, entity4]));
170
+ });
171
+ });
172
+
173
+ describe("overridableComponent", () => {
174
+ it("should return a overridable component", () => {
175
+ const Position = defineComponent(world, { x: Type.Number, y: Type.Number });
176
+ const OverridablePosition = overridableComponent(Position);
177
+ expect("addOverride" in OverridablePosition).toBe(true);
178
+ expect("addOverride" in OverridablePosition).toBe(true);
179
+ });
180
+
181
+ it("should mirror all values of the source component", () => {
182
+ const Position = defineComponent(world, { x: Type.Number, y: Type.Number });
183
+ const entity1 = createEntity(world);
184
+ setComponent(Position, entity1, { x: 1, y: 2 });
185
+
186
+ const OverridablePosition = overridableComponent(Position);
187
+ expect(getComponentValue(OverridablePosition, entity1)).toEqual({ x: 1, y: 2 });
188
+ });
189
+
190
+ it("the overridable component should be updated if the original component is updated", () => {
191
+ const Position = defineComponent(world, { x: Type.Number, y: Type.Number });
192
+ const entity1 = createEntity(world);
193
+ setComponent(Position, entity1, { x: 1, y: 2 });
194
+
195
+ const OverridableComponent = overridableComponent(Position);
196
+
197
+ setComponent(Position, entity1, { x: 2, y: 2 });
198
+ expect(getComponentValue(OverridableComponent, entity1)).toEqual({ x: 2, y: 2 });
199
+
200
+ const entity2 = createEntity(world, [withValue(Position, { x: 3, y: 3 })]);
201
+ expect(getComponentValue(OverridableComponent, entity2)).toEqual({ x: 3, y: 3 });
202
+ });
203
+
204
+ it("should return the updated component value if there is a relevant update for the given entity", () => {
205
+ const Position = defineComponent(world, { x: Type.Number, y: Type.Number });
206
+ const entity1 = createEntity(world);
207
+ const entity2 = createEntity(world);
208
+ setComponent(Position, entity1, { x: 1, y: 2 });
209
+ setComponent(Position, entity2, { x: 5, y: 6 });
210
+
211
+ const OverridableComponent = overridableComponent(Position);
212
+ OverridableComponent.addOverride("firstOverride", { entity: entity1, value: { x: 2, y: 3 } });
213
+ expect(getComponentValue(OverridableComponent, entity1)).toEqual({ x: 2, y: 3 });
214
+ expect(getComponentValue(OverridableComponent, entity2)).toEqual({ x: 5, y: 6 });
215
+
216
+ OverridableComponent.addOverride("secondOverride", { entity: entity1, value: { x: 3, y: 3 } });
217
+ expect(getComponentValue(OverridableComponent, entity1)).toEqual({ x: 3, y: 3 });
218
+
219
+ OverridableComponent.removeOverride("secondOverride");
220
+ expect(getComponentValue(OverridableComponent, entity1)).toEqual({ x: 2, y: 3 });
221
+
222
+ setComponent(Position, entity1, { x: 10, y: 20 });
223
+ expect(getComponentValue(OverridableComponent, entity1)).toEqual({ x: 2, y: 3 });
224
+
225
+ OverridableComponent.removeOverride("firstOverride");
226
+ expect(getComponentValue(OverridableComponent, entity1)).toEqual({ x: 10, y: 20 });
227
+ });
228
+
229
+ it("adding an override should trigger reactions depending on the getComponentValue of the overriden component", () => {
230
+ const Position = defineComponent(world, { x: Type.Number, y: Type.Number });
231
+ const entity1 = createEntity(world);
232
+ setComponent(Position, entity1, { x: 1, y: 2 });
233
+
234
+ const OverridablePosition = overridableComponent(Position);
235
+
236
+ const spy = jest.fn();
237
+ OverridablePosition.update$.subscribe(spy);
238
+
239
+ expect(spy).toHaveBeenCalledTimes(0);
240
+
241
+ OverridablePosition.addOverride("firstOverride", { entity: entity1, value: { x: 3, y: 3 } });
242
+ expect(spy).toHaveBeenCalledTimes(1);
243
+ expect(spy).toHaveBeenLastCalledWith({
244
+ entity: entity1,
245
+ component: OverridablePosition,
246
+ value: [
247
+ { x: 3, y: 3 },
248
+ { x: 1, y: 2 },
249
+ ],
250
+ });
251
+
252
+ OverridablePosition.removeOverride("firstOverride");
253
+ expect(spy).toHaveBeenCalledTimes(2);
254
+ expect(spy).toHaveBeenLastCalledWith({
255
+ entity: entity1,
256
+ component: OverridablePosition,
257
+ value: [
258
+ { x: 1, y: 2 },
259
+ { x: 3, y: 3 },
260
+ ],
261
+ });
262
+
263
+ OverridablePosition.addOverride("secondOverride", {
264
+ entity: "42" as Entity,
265
+ value: { x: 2, y: 3 },
266
+ });
267
+ expect(spy).toHaveBeenLastCalledWith({
268
+ entity: "42",
269
+ component: OverridablePosition,
270
+ value: [{ x: 2, y: 3 }, undefined],
271
+ });
272
+ expect(spy).toHaveBeenCalledTimes(3);
273
+ });
274
+ });
275
+ });
package/src/Component.ts CHANGED
@@ -19,7 +19,13 @@ import { isFullComponentValue, isIndexer } from "./utils";
19
19
  import { getEntityString, getEntitySymbol } from "./Entity";
20
20
 
21
21
  function getComponentName(component: Component<any, any, any>) {
22
- return component.metadata?.tableId ?? component.metadata?.contractId ?? component.id;
22
+ return (
23
+ component.metadata?.componentName ??
24
+ component.metadata?.tableName ??
25
+ component.metadata?.tableId ??
26
+ component.metadata?.contractId ??
27
+ component.id
28
+ );
23
29
  }
24
30
 
25
31
  /**
@@ -31,7 +37,7 @@ function getComponentName(component: Component<any, any, any>) {
31
37
  * @param schema {@link Schema} of component values. Uses Type enum as bridge between typescript types and javascript accessible values.
32
38
  * @param options Optional: {
33
39
  * id: descriptive id for this component (otherwise an autogenerated id is used),
34
- * metadata: arbitrary metadata (eg. contractId for solecs mapped components),
40
+ * metadata: arbitrary metadata,
35
41
  * indexed: if this flag is set, an indexer is applied to this component (see {@link createIndexer})
36
42
  * }
37
43
  * @returns Component object linked to the provided World
@@ -41,7 +47,7 @@ function getComponentName(component: Component<any, any, any>) {
41
47
  * const Position = defineComponent(world, { x: Type.Number, y: Type.Number }, { id: "Position" });
42
48
  * ```
43
49
  */
44
- export function defineComponent<S extends Schema, M extends Metadata, T = undefined>(
50
+ export function defineComponent<S extends Schema, M extends Metadata, T = unknown>(
45
51
  world: World,
46
52
  schema: S,
47
53
  options?: { id?: string; metadata?: M; indexed?: boolean }
@@ -71,7 +77,7 @@ export function defineComponent<S extends Schema, M extends Metadata, T = undefi
71
77
  * setComponent(Position, entity, { x: 1, y: 2 });
72
78
  * ```
73
79
  */
74
- export function setComponent<S extends Schema, T = undefined>(
80
+ export function setComponent<S extends Schema, T = unknown>(
75
81
  component: Component<S, Metadata, T>,
76
82
  entity: Entity,
77
83
  value: ComponentValue<S, T>
@@ -122,7 +128,7 @@ export function setComponent<S extends Schema, T = undefined>(
122
128
  * updateComponent(Position, entity, { x: 1 });
123
129
  * ```
124
130
  */
125
- export function updateComponent<S extends Schema, T = undefined>(
131
+ export function updateComponent<S extends Schema, T = unknown>(
126
132
  component: Component<S, Metadata, T>,
127
133
  entity: Entity,
128
134
  value: Partial<ComponentValue<S, T>>,
@@ -145,7 +151,7 @@ export function updateComponent<S extends Schema, T = undefined>(
145
151
  * @param component {@link defineComponent Component} to be updated.
146
152
  * @param entity {@link Entity} whose value should be removed from this component.
147
153
  */
148
- export function removeComponent<S extends Schema, M extends Metadata, T>(
154
+ export function removeComponent<S extends Schema, M extends Metadata, T = unknown>(
149
155
  component: Component<S, M, T>,
150
156
  entity: Entity
151
157
  ) {
@@ -164,7 +170,7 @@ export function removeComponent<S extends Schema, M extends Metadata, T>(
164
170
  * @param entity {@link Entity} to check whether it has a value in the given component.
165
171
  * @returns true if the component contains a value for the given entity, else false.
166
172
  */
167
- export function hasComponent<S extends Schema, T = undefined>(
173
+ export function hasComponent<S extends Schema, T = unknown>(
168
174
  component: Component<S, Metadata, T>,
169
175
  entity: Entity
170
176
  ): boolean {
@@ -181,7 +187,7 @@ export function hasComponent<S extends Schema, T = undefined>(
181
187
  * @param entity {@link Entity} to get the value for from the given component.
182
188
  * @returns Value of the given entity in the given component or undefined if no value exists.
183
189
  */
184
- export function getComponentValue<S extends Schema, T = undefined>(
190
+ export function getComponentValue<S extends Schema, T = unknown>(
185
191
  component: Component<S, Metadata, T>,
186
192
  entity: Entity
187
193
  ): ComponentValue<S, T> | undefined {
@@ -210,7 +216,7 @@ export function getComponentValue<S extends Schema, T = undefined>(
210
216
  * @remarks
211
217
  * Throws an error if no value exists in the component for the given entity.
212
218
  */
213
- export function getComponentValueStrict<S extends Schema, T = undefined>(
219
+ export function getComponentValueStrict<S extends Schema, T = unknown>(
214
220
  component: Component<S, Metadata, T>,
215
221
  entity: Entity
216
222
  ): ComponentValue<S, T> {
@@ -233,7 +239,7 @@ export function getComponentValueStrict<S extends Schema, T = undefined>(
233
239
  * componentValueEquals({ x: 1 }, { x: 1, y: 3 }) // returns true because x is equal and y is not present in a
234
240
  * ```
235
241
  */
236
- export function componentValueEquals<S extends Schema, T = undefined>(
242
+ export function componentValueEquals<S extends Schema, T = unknown>(
237
243
  a?: Partial<ComponentValue<S, T>>,
238
244
  b?: ComponentValue<S, T>
239
245
  ): boolean {
@@ -256,7 +262,7 @@ export function componentValueEquals<S extends Schema, T = undefined>(
256
262
  * @param value {@link ComponentValue} with {@link ComponentSchema} `S`
257
263
  * @returns Tuple `[component, value]`
258
264
  */
259
- export function withValue<S extends Schema, T = undefined>(
265
+ export function withValue<S extends Schema, T = unknown>(
260
266
  component: Component<S, Metadata, T>,
261
267
  value: ComponentValue<S, T>
262
268
  ): [Component<S, Metadata, T>, ComponentValue<S, T>] {
@@ -296,7 +302,7 @@ export function getEntitiesWithValue<S extends Schema>(
296
302
  * @param component {@link defineComponent Component} to get all entities from
297
303
  * @returns Set of all entities in the given component.
298
304
  */
299
- export function getComponentEntities<S extends Schema, T = undefined>(
305
+ export function getComponentEntities<S extends Schema, T = unknown>(
300
306
  component: Component<S, Metadata, T>
301
307
  ): IterableIterator<Entity> {
302
308
  return component.entities();
@@ -316,7 +322,7 @@ export function getComponentEntities<S extends Schema, T = undefined>(
316
322
  * @param component {@link defineComponent Component} to use as underlying source for the overridable component
317
323
  * @returns overridable component
318
324
  */
319
- export function overridableComponent<S extends Schema, M extends Metadata, T = undefined>(
325
+ export function overridableComponent<S extends Schema, M extends Metadata, T = unknown>(
320
326
  component: Component<S, M, T>
321
327
  ): OverridableComponent<S, M, T> {
322
328
  let nonce = 0;
@@ -455,7 +461,7 @@ export function clearLocalCache(component: Component, uniqueWorldIdentifier?: st
455
461
  }
456
462
 
457
463
  // Note: Only proof of concept for now - use this only for component that do not update frequently
458
- export function createLocalCache<S extends Schema, M extends Metadata, T = undefined>(
464
+ export function createLocalCache<S extends Schema, M extends Metadata, T = unknown>(
459
465
  component: Component<S, M, T>,
460
466
  uniqueWorldIdentifier?: string
461
467
  ): Component<S, M, T> {
@@ -0,0 +1,45 @@
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 CHANGED
@@ -8,7 +8,7 @@ import { Component, ComponentValue, Entity, EntitySymbol, World } from "./types"
8
8
  * @param components Array of [{@link defineComponent Component}, {@link ComponentValue}] tuples to be added to this entity.
9
9
  * (Use {@link withValue} to generate these tuples with type safety.)
10
10
  * @param options Optional: {
11
- * id: {@link Entity} for this entity. Use this for entities that were created outside of recs, eg. in the corresponding solecs contracts.
11
+ * id: {@link Entity} for this entity. Use this for entities that were created outside of recs.
12
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
13
  * }
14
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}).