@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.
- package/CHANGELOG.md +122 -0
- package/dist/chunk-J6QWRVRL.js +2 -0
- package/dist/chunk-J6QWRVRL.js.map +1 -0
- package/dist/deprecated/index.js +2 -0
- package/dist/deprecated/index.js.map +1 -0
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +17 -6
- package/src/Component.spec.ts +275 -0
- package/src/Component.ts +20 -8
- package/src/Entity.spec.ts +45 -0
- package/src/Entity.ts +1 -1
- package/src/Indexer.spec.ts +288 -0
- package/src/Performance.spec.ts +152 -0
- package/src/Query.spec.ts +811 -0
- package/src/System.spec.ts +139 -0
- package/src/World.spec.ts +79 -0
- package/src/deprecated/constants.ts +9 -0
- package/src/deprecated/createActionSystem.spec.ts +501 -0
- package/src/deprecated/createActionSystem.ts +236 -0
- package/src/deprecated/defineActionComponent.ts +18 -0
- package/src/deprecated/index.ts +2 -0
- package/src/deprecated/types.ts +45 -0
- package/src/deprecated/waitForActionCompletion.ts +15 -0
- package/src/deprecated/waitForComponentValueIn.ts +38 -0
- package/src/types.ts +0 -1
@@ -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
|
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
|
-
|
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
|
-
|
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
|
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}).
|