@latticexyz/recs 2.0.0-next.1 → 2.0.0-next.11

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
@@ -18,6 +18,11 @@ import {
18
18
  import { isFullComponentValue, isIndexer } from "./utils";
19
19
  import { getEntityString, getEntitySymbol } from "./Entity";
20
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
+
21
26
  function getComponentName(component: Component<any, any, any>) {
22
27
  return (
23
28
  component.metadata?.componentName ??
@@ -37,7 +42,7 @@ function getComponentName(component: Component<any, any, any>) {
37
42
  * @param schema {@link Schema} of component values. Uses Type enum as bridge between typescript types and javascript accessible values.
38
43
  * @param options Optional: {
39
44
  * id: descriptive id for this component (otherwise an autogenerated id is used),
40
- * metadata: arbitrary metadata (eg. contractId for solecs mapped components),
45
+ * metadata: arbitrary metadata,
41
46
  * indexed: if this flag is set, an indexer is applied to this component (see {@link createIndexer})
42
47
  * }
43
48
  * @returns Component object linked to the provided World
@@ -80,7 +85,8 @@ export function defineComponent<S extends Schema, M extends Metadata, T = unknow
80
85
  export function setComponent<S extends Schema, T = unknown>(
81
86
  component: Component<S, Metadata, T>,
82
87
  entity: Entity,
83
- value: ComponentValue<S, T>
88
+ value: ComponentValue<S, T>,
89
+ options: ComponentMutationOptions = {}
84
90
  ) {
85
91
  const entitySymbol = getEntitySymbol(entity);
86
92
  const prevValue = getComponentValue(component, entity);
@@ -109,7 +115,9 @@ export function setComponent<S extends Schema, T = unknown>(
109
115
  }
110
116
  }
111
117
  }
112
- component.update$.next({ entity, value: [value, prevValue], component });
118
+ if (!options.skipUpdateStream) {
119
+ component.update$.next({ entity, value: [value, prevValue], component });
120
+ }
113
121
  }
114
122
 
115
123
  /**
@@ -132,16 +140,17 @@ export function updateComponent<S extends Schema, T = unknown>(
132
140
  component: Component<S, Metadata, T>,
133
141
  entity: Entity,
134
142
  value: Partial<ComponentValue<S, T>>,
135
- initialValue?: ComponentValue<S, T>
143
+ initialValue?: ComponentValue<S, T>,
144
+ options: ComponentMutationOptions = {}
136
145
  ) {
137
146
  const currentValue = getComponentValue(component, entity);
138
147
  if (currentValue === undefined) {
139
148
  if (initialValue === undefined) {
140
149
  throw new Error(`Can't update component ${getComponentName(component)} without a current value or initial value`);
141
150
  }
142
- setComponent(component, entity, { ...initialValue, ...value });
151
+ setComponent(component, entity, { ...initialValue, ...value }, options);
143
152
  } else {
144
- setComponent(component, entity, { ...currentValue, ...value });
153
+ setComponent(component, entity, { ...currentValue, ...value }, options);
145
154
  }
146
155
  }
147
156
 
@@ -153,14 +162,17 @@ export function updateComponent<S extends Schema, T = unknown>(
153
162
  */
154
163
  export function removeComponent<S extends Schema, M extends Metadata, T = unknown>(
155
164
  component: Component<S, M, T>,
156
- entity: Entity
165
+ entity: Entity,
166
+ options: ComponentMutationOptions = {}
157
167
  ) {
158
168
  const entitySymbol = getEntitySymbol(entity);
159
169
  const prevValue = getComponentValue(component, entity);
160
170
  for (const key of Object.keys(component.values)) {
161
171
  component.values[key].delete(entitySymbol);
162
172
  }
163
- component.update$.next({ entity, value: [undefined, prevValue], component });
173
+ if (!options.skipUpdateStream) {
174
+ component.update$.next({ entity, value: [undefined, prevValue], component });
175
+ }
164
176
  }
165
177
 
166
178
  /**
@@ -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}).