@codehz/ecs 0.7.1 → 0.7.3
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/{builder.d.mts → dist/builder.d.mts} +4 -2
- package/{world.mjs → dist/world.mjs} +9 -30
- package/dist/world.mjs.map +1 -0
- package/examples/advanced-scheduling.ts +96 -0
- package/examples/collision-detection.ts +229 -0
- package/examples/inventory-system-relations.ts +108 -0
- package/examples/parent-child-hierarchy.ts +206 -0
- package/examples/serialization.ts +337 -0
- package/examples/simple.ts +96 -0
- package/examples/spatial-grid.ts +276 -0
- package/examples/state-machine.ts +273 -0
- package/examples/tag-filtering.ts +266 -0
- package/package.json +58 -12
- package/src/__tests__/commands/buffer-limits.test.ts +72 -0
- package/src/__tests__/commands/buffer.test.ts +195 -0
- package/src/__tests__/component/singleton.test.ts +148 -0
- package/src/__tests__/core/archetype.test.ts +247 -0
- package/src/__tests__/core/bitset.test.ts +171 -0
- package/src/__tests__/core/changeset.test.ts +254 -0
- package/src/__tests__/core/multi-map.test.ts +74 -0
- package/src/__tests__/entity/component-registry.test.ts +66 -0
- package/src/__tests__/entity/entity.test.ts +520 -0
- package/src/__tests__/entity/id-manager.test.ts +157 -0
- package/src/__tests__/entity/id-system.test.ts +260 -0
- package/src/__tests__/perf/comprehensive.perf.test.ts +300 -0
- package/src/__tests__/perf/sync-hotpath.perf.test.ts +79 -0
- package/src/__tests__/query/basic.test.ts +341 -0
- package/src/__tests__/query/caching.test.ts +112 -0
- package/src/__tests__/query/filter.test.ts +111 -0
- package/src/__tests__/query/optional.test.ts +231 -0
- package/src/__tests__/query/perf.test.ts +99 -0
- package/src/__tests__/relations/dont-fragment/basic.test.ts +496 -0
- package/src/__tests__/relations/dont-fragment/query-notification.test.ts +125 -0
- package/src/__tests__/relations/wildcard.test.ts +179 -0
- package/src/__tests__/serialization/bounds.test.ts +237 -0
- package/src/__tests__/testing/assertions.test.ts +224 -0
- package/src/__tests__/testing/entity-builder.test.ts +84 -0
- package/src/__tests__/testing/snapshot.test.ts +150 -0
- package/src/__tests__/testing/world-fixture.test.ts +73 -0
- package/src/__tests__/world/component-hooks.test.ts +185 -0
- package/src/__tests__/world/component-management.test.ts +447 -0
- package/src/__tests__/world/entity-management.test.ts +86 -0
- package/src/__tests__/world/get-optional.test.ts +96 -0
- package/src/__tests__/world/multi-component-hooks.test.ts +502 -0
- package/src/__tests__/world/perf.test.ts +93 -0
- package/src/__tests__/world/query.test.ts +223 -0
- package/src/__tests__/world/serialize.test.ts +83 -0
- package/src/__tests__/world/wildcard-relation-hooks.test.ts +332 -0
- package/src/archetype/archetype.ts +472 -0
- package/src/archetype/helpers.ts +186 -0
- package/src/archetype/store.ts +33 -0
- package/src/commands/buffer.ts +110 -0
- package/src/commands/changeset.ts +104 -0
- package/src/component/entity-store.ts +223 -0
- package/src/component/registry.ts +657 -0
- package/src/component/type-utils.ts +9 -0
- package/src/entity/index.ts +63 -0
- package/src/entity/manager.ts +115 -0
- package/src/entity/relation.ts +319 -0
- package/src/entity/types.ts +135 -0
- package/src/index.ts +41 -0
- package/src/query/filter.ts +75 -0
- package/src/query/query.ts +313 -0
- package/src/query/registry.ts +101 -0
- package/src/storage/serialization.ts +130 -0
- package/src/testing/index.ts +634 -0
- package/src/types/index.ts +99 -0
- package/src/utils/bit-set.ts +133 -0
- package/src/utils/multi-map.ts +96 -0
- package/src/utils/utils.ts +19 -0
- package/src/world/builder.ts +100 -0
- package/src/world/commands.ts +378 -0
- package/src/world/hooks.ts +358 -0
- package/src/world/references.ts +38 -0
- package/src/world/serialization.ts +122 -0
- package/src/world/world.ts +1201 -0
- package/world.mjs.map +0 -1
- /package/{index.d.mts → dist/index.d.mts} +0 -0
- /package/{index.mjs → dist/index.mjs} +0 -0
- /package/{testing.d.mts → dist/testing.d.mts} +0 -0
- /package/{testing.mjs → dist/testing.mjs} +0 -0
- /package/{testing.mjs.map → dist/testing.mjs.map} +0 -0
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { component, relation, type EntityId } from "../../entity";
|
|
3
|
+
import { World } from "../../world/world";
|
|
4
|
+
|
|
5
|
+
describe("Query", () => {
|
|
6
|
+
describe("Query Creation and Basic Functionality", () => {
|
|
7
|
+
type Position = { x: number; y: number };
|
|
8
|
+
type Velocity = { x: number; y: number };
|
|
9
|
+
type Health = { value: number };
|
|
10
|
+
|
|
11
|
+
const positionComponent = component<Position>();
|
|
12
|
+
const velocityComponent = component<Velocity>();
|
|
13
|
+
const healthComponent = component<Health>();
|
|
14
|
+
|
|
15
|
+
it("should create a query and return matching entities", () => {
|
|
16
|
+
const world = new World();
|
|
17
|
+
const query = world.createQuery([positionComponent]);
|
|
18
|
+
|
|
19
|
+
const entity1 = world.new();
|
|
20
|
+
const entity2 = world.new();
|
|
21
|
+
const entity3 = world.new();
|
|
22
|
+
|
|
23
|
+
world.set(entity1, positionComponent, { x: 1, y: 2 });
|
|
24
|
+
world.set(entity2, positionComponent, { x: 3, y: 4 });
|
|
25
|
+
// entity3 has no components
|
|
26
|
+
|
|
27
|
+
world.sync(); // Execute deferred commands
|
|
28
|
+
|
|
29
|
+
const entities = query.getEntities();
|
|
30
|
+
expect(entities).toContain(entity1);
|
|
31
|
+
expect(entities).toContain(entity2);
|
|
32
|
+
expect(entities).not.toContain(entity3);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("should update cache when new archetypes are created", () => {
|
|
36
|
+
const world = new World();
|
|
37
|
+
const query = world.createQuery([positionComponent, velocityComponent]);
|
|
38
|
+
|
|
39
|
+
const entity1 = world.new();
|
|
40
|
+
world.set(entity1, positionComponent, { x: 1, y: 2 });
|
|
41
|
+
world.set(entity1, velocityComponent, { x: 0.1, y: 0.2 });
|
|
42
|
+
|
|
43
|
+
world.sync();
|
|
44
|
+
|
|
45
|
+
// Initially should have entity1
|
|
46
|
+
expect(query.getEntities()).toEqual([entity1]);
|
|
47
|
+
|
|
48
|
+
// Create entity2 with same components (should reuse archetype)
|
|
49
|
+
const entity2 = world.new();
|
|
50
|
+
world.set(entity2, positionComponent, { x: 3, y: 4 });
|
|
51
|
+
world.set(entity2, velocityComponent, { x: 0.2, y: 0.3 });
|
|
52
|
+
|
|
53
|
+
world.sync();
|
|
54
|
+
|
|
55
|
+
// Should still work (archetype reused, no new archetype created)
|
|
56
|
+
expect(query.getEntities()).toContain(entity1);
|
|
57
|
+
expect(query.getEntities()).toContain(entity2);
|
|
58
|
+
|
|
59
|
+
// Create entity3 with only position (creates new archetype)
|
|
60
|
+
const entity3 = world.new();
|
|
61
|
+
world.set(entity3, positionComponent, { x: 5, y: 6 });
|
|
62
|
+
|
|
63
|
+
// Query should still only return entities with both components
|
|
64
|
+
const entities = query.getEntities();
|
|
65
|
+
expect(entities).toContain(entity1);
|
|
66
|
+
expect(entities).toContain(entity2);
|
|
67
|
+
expect(entities).not.toContain(entity3);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("should handle empty results", () => {
|
|
71
|
+
const world = new World();
|
|
72
|
+
const query = world.createQuery([velocityComponent]);
|
|
73
|
+
|
|
74
|
+
const entity = world.new();
|
|
75
|
+
world.set(entity, positionComponent, { x: 1, y: 2 });
|
|
76
|
+
|
|
77
|
+
const entities = query.getEntities();
|
|
78
|
+
expect(entities).toEqual([]);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("should dispose properly", () => {
|
|
82
|
+
const world = new World();
|
|
83
|
+
const query = world.createQuery([positionComponent]);
|
|
84
|
+
|
|
85
|
+
const entity = world.new();
|
|
86
|
+
world.set(entity, positionComponent, { x: 1, y: 2 });
|
|
87
|
+
|
|
88
|
+
world.sync();
|
|
89
|
+
|
|
90
|
+
expect(query.disposed).toBe(false);
|
|
91
|
+
expect(query.getEntities()).toEqual([entity]);
|
|
92
|
+
|
|
93
|
+
query.dispose();
|
|
94
|
+
expect(query.disposed).toBe(true);
|
|
95
|
+
|
|
96
|
+
// Should throw after dispose
|
|
97
|
+
expect(() => query.getEntities()).toThrow("Query has been disposed");
|
|
98
|
+
// iterate should also throw
|
|
99
|
+
expect(() => {
|
|
100
|
+
// use spread to attempt to consume iterator
|
|
101
|
+
[...query.iterate([positionComponent])];
|
|
102
|
+
}).toThrow("Query has been disposed");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("should handle multiple queries", () => {
|
|
106
|
+
const world = new World();
|
|
107
|
+
const positionQuery = world.createQuery([positionComponent]);
|
|
108
|
+
const velocityQuery = world.createQuery([velocityComponent]);
|
|
109
|
+
const bothQuery = world.createQuery([positionComponent, velocityComponent]);
|
|
110
|
+
|
|
111
|
+
const entity1 = world.new();
|
|
112
|
+
const entity2 = world.new();
|
|
113
|
+
|
|
114
|
+
world.set(entity1, positionComponent, { x: 1, y: 2 });
|
|
115
|
+
world.set(entity1, velocityComponent, { x: 0.1, y: 0.2 });
|
|
116
|
+
|
|
117
|
+
world.set(entity2, positionComponent, { x: 3, y: 4 });
|
|
118
|
+
|
|
119
|
+
world.sync();
|
|
120
|
+
|
|
121
|
+
const positionEntities = positionQuery.getEntities();
|
|
122
|
+
expect(positionEntities).toContain(entity1);
|
|
123
|
+
expect(positionEntities).toContain(entity2);
|
|
124
|
+
expect(positionEntities.length).toBe(2);
|
|
125
|
+
expect(velocityQuery.getEntities()).toEqual([entity1]);
|
|
126
|
+
expect(bothQuery.getEntities()).toEqual([entity1]);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("should handle query disposal without affecting other queries", () => {
|
|
130
|
+
const world = new World();
|
|
131
|
+
const query1 = world.createQuery([positionComponent]);
|
|
132
|
+
const query2 = world.createQuery([velocityComponent]);
|
|
133
|
+
|
|
134
|
+
const entity = world.new();
|
|
135
|
+
world.set(entity, positionComponent, { x: 1, y: 2 });
|
|
136
|
+
world.set(entity, velocityComponent, { x: 0.1, y: 0.2 });
|
|
137
|
+
|
|
138
|
+
world.sync();
|
|
139
|
+
|
|
140
|
+
expect(query1.getEntities()).toEqual([entity]);
|
|
141
|
+
expect(query2.getEntities()).toEqual([entity]);
|
|
142
|
+
|
|
143
|
+
query1.dispose();
|
|
144
|
+
|
|
145
|
+
// query1 should be disposed
|
|
146
|
+
expect(query1.disposed).toBe(true);
|
|
147
|
+
expect(() => query1.getEntities()).toThrow("Query has been disposed");
|
|
148
|
+
|
|
149
|
+
// query2 should still work
|
|
150
|
+
expect(query2.disposed).toBe(false);
|
|
151
|
+
expect(query2.getEntities()).toEqual([entity]);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("should get entities with component data", () => {
|
|
155
|
+
const world = new World();
|
|
156
|
+
const query = world.createQuery([positionComponent, velocityComponent]);
|
|
157
|
+
|
|
158
|
+
const entity1 = world.new();
|
|
159
|
+
const entity2 = world.new();
|
|
160
|
+
|
|
161
|
+
const pos1: Position = { x: 1, y: 2 };
|
|
162
|
+
const vel1: Velocity = { x: 0.1, y: 0.2 };
|
|
163
|
+
const pos2: Position = { x: 3, y: 4 };
|
|
164
|
+
const vel2: Velocity = { x: 0.3, y: 0.4 };
|
|
165
|
+
|
|
166
|
+
world.set(entity1, positionComponent, pos1);
|
|
167
|
+
world.set(entity1, velocityComponent, vel1);
|
|
168
|
+
world.set(entity2, positionComponent, pos2);
|
|
169
|
+
world.set(entity2, velocityComponent, vel2);
|
|
170
|
+
|
|
171
|
+
world.sync();
|
|
172
|
+
|
|
173
|
+
const results = query.getEntitiesWithComponents([positionComponent, velocityComponent]);
|
|
174
|
+
|
|
175
|
+
expect(results.length).toBe(2);
|
|
176
|
+
|
|
177
|
+
// Find results for each entity
|
|
178
|
+
const result1 = results.find((r) => r.entity === entity1);
|
|
179
|
+
const result2 = results.find((r) => r.entity === entity2);
|
|
180
|
+
|
|
181
|
+
expect(result1).toBeDefined();
|
|
182
|
+
expect(result2).toBeDefined();
|
|
183
|
+
|
|
184
|
+
expect(result1!.components[0]).toEqual(pos1);
|
|
185
|
+
expect(result1!.components[1]).toEqual(vel1);
|
|
186
|
+
expect(result2!.components[0]).toEqual(pos2);
|
|
187
|
+
expect(result2!.components[1]).toEqual(vel2);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("should iterate over entities with forEach", () => {
|
|
191
|
+
const world = new World();
|
|
192
|
+
const query = world.createQuery([positionComponent]);
|
|
193
|
+
|
|
194
|
+
const entity1 = world.new();
|
|
195
|
+
const entity2 = world.new();
|
|
196
|
+
|
|
197
|
+
const pos1: Position = { x: 1, y: 2 };
|
|
198
|
+
const pos2: Position = { x: 3, y: 4 };
|
|
199
|
+
|
|
200
|
+
world.set(entity1, positionComponent, pos1);
|
|
201
|
+
world.set(entity2, positionComponent, pos2);
|
|
202
|
+
|
|
203
|
+
world.sync();
|
|
204
|
+
|
|
205
|
+
const visitedEntities: EntityId[] = [];
|
|
206
|
+
const visitedPositions: Position[] = [];
|
|
207
|
+
|
|
208
|
+
query.forEach([positionComponent], (entity, position) => {
|
|
209
|
+
visitedEntities.push(entity);
|
|
210
|
+
visitedPositions.push(position);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
expect(visitedEntities.length).toBe(2);
|
|
214
|
+
expect(visitedPositions.length).toBe(2);
|
|
215
|
+
expect(visitedEntities).toContain(entity1);
|
|
216
|
+
expect(visitedEntities).toContain(entity2);
|
|
217
|
+
expect(visitedPositions).toContainEqual(pos1);
|
|
218
|
+
expect(visitedPositions).toContainEqual(pos2);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("should iterate over entities with iterate", () => {
|
|
222
|
+
const world = new World();
|
|
223
|
+
const query = world.createQuery([positionComponent]);
|
|
224
|
+
|
|
225
|
+
const entity1 = world.new();
|
|
226
|
+
const entity2 = world.new();
|
|
227
|
+
|
|
228
|
+
const pos1: Position = { x: 1, y: 2 };
|
|
229
|
+
const pos2: Position = { x: 3, y: 4 };
|
|
230
|
+
|
|
231
|
+
world.set(entity1, positionComponent, pos1);
|
|
232
|
+
world.set(entity2, positionComponent, pos2);
|
|
233
|
+
|
|
234
|
+
world.sync();
|
|
235
|
+
|
|
236
|
+
const visitedEntities: EntityId[] = [];
|
|
237
|
+
const visitedPositions: Position[] = [];
|
|
238
|
+
|
|
239
|
+
for (const [entity, position] of query.iterate([positionComponent])) {
|
|
240
|
+
visitedEntities.push(entity);
|
|
241
|
+
visitedPositions.push(position);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
expect(visitedEntities.length).toBe(2);
|
|
245
|
+
expect(visitedPositions.length).toBe(2);
|
|
246
|
+
expect(visitedEntities).toContain(entity1);
|
|
247
|
+
expect(visitedEntities).toContain(entity2);
|
|
248
|
+
expect(visitedPositions).toContainEqual(pos1);
|
|
249
|
+
expect(visitedPositions).toContainEqual(pos2);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it("should get component data arrays", () => {
|
|
253
|
+
const world = new World();
|
|
254
|
+
const query = world.createQuery([positionComponent]);
|
|
255
|
+
|
|
256
|
+
const entity1 = world.new();
|
|
257
|
+
const entity2 = world.new();
|
|
258
|
+
|
|
259
|
+
const pos1: Position = { x: 1, y: 2 };
|
|
260
|
+
const pos2: Position = { x: 3, y: 4 };
|
|
261
|
+
|
|
262
|
+
world.set(entity1, positionComponent, pos1);
|
|
263
|
+
world.set(entity2, positionComponent, pos2);
|
|
264
|
+
|
|
265
|
+
world.sync();
|
|
266
|
+
|
|
267
|
+
const positions = query.getComponentData(positionComponent);
|
|
268
|
+
|
|
269
|
+
expect(positions.length).toBe(2);
|
|
270
|
+
expect(positions).toContainEqual(pos1);
|
|
271
|
+
expect(positions).toContainEqual(pos2);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it("should support negative components to exclude entities", () => {
|
|
275
|
+
const world = new World();
|
|
276
|
+
const query = world.createQuery([positionComponent], { negativeComponentTypes: [healthComponent] });
|
|
277
|
+
|
|
278
|
+
const entity1 = world.new();
|
|
279
|
+
const entity2 = world.new();
|
|
280
|
+
const entity3 = world.new();
|
|
281
|
+
|
|
282
|
+
world.set(entity1, positionComponent, { x: 1, y: 2 });
|
|
283
|
+
world.set(entity2, positionComponent, { x: 3, y: 4 });
|
|
284
|
+
world.set(entity2, healthComponent, { value: 100 }); // entity2 has health, should be excluded
|
|
285
|
+
world.set(entity3, healthComponent, { value: 50 }); // entity3 has no position, already excluded
|
|
286
|
+
|
|
287
|
+
world.sync();
|
|
288
|
+
|
|
289
|
+
const entities = query.getEntities();
|
|
290
|
+
expect(entities).toContain(entity1);
|
|
291
|
+
expect(entities).not.toContain(entity2);
|
|
292
|
+
expect(entities).not.toContain(entity3);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it("should support wildcard relations in queries", () => {
|
|
296
|
+
const world = new World();
|
|
297
|
+
|
|
298
|
+
const tag = component();
|
|
299
|
+
// Create a wildcard relation for tag component
|
|
300
|
+
const wildcardTagRelation = relation(tag, "*");
|
|
301
|
+
const query = world.createQuery([wildcardTagRelation]);
|
|
302
|
+
|
|
303
|
+
const entity1 = world.new();
|
|
304
|
+
const entity2 = world.new();
|
|
305
|
+
const entity3 = world.new();
|
|
306
|
+
|
|
307
|
+
world.set(entity1, relation(tag, positionComponent), { x: 1, y: 2 });
|
|
308
|
+
world.set(entity1, relation(tag, velocityComponent), { x: 0.1, y: 0.2 });
|
|
309
|
+
|
|
310
|
+
world.set(entity2, relation(tag, positionComponent), { x: 3, y: 4 });
|
|
311
|
+
|
|
312
|
+
// entity3 has no position component
|
|
313
|
+
|
|
314
|
+
world.sync();
|
|
315
|
+
|
|
316
|
+
const entities = query.getEntities();
|
|
317
|
+
expect(entities).toContain(entity1);
|
|
318
|
+
expect(entities).toContain(entity2);
|
|
319
|
+
expect(entities).not.toContain(entity3);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it("should support mixed queries with components and wildcard relations", () => {
|
|
323
|
+
const world = new World();
|
|
324
|
+
|
|
325
|
+
const entity1 = world.new();
|
|
326
|
+
const entity2 = world.new();
|
|
327
|
+
const entity3 = world.new();
|
|
328
|
+
|
|
329
|
+
world.set(entity1, positionComponent, { x: 1, y: 2 });
|
|
330
|
+
world.set(entity1, velocityComponent, { x: 0.1, y: 0.2 });
|
|
331
|
+
|
|
332
|
+
world.set(entity2, positionComponent, { x: 3, y: 4 });
|
|
333
|
+
// entity2 doesn't have velocity
|
|
334
|
+
|
|
335
|
+
world.set(entity3, velocityComponent, { x: 0.5, y: 0.6 });
|
|
336
|
+
// entity3 doesn't have position
|
|
337
|
+
|
|
338
|
+
world.sync();
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
});
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { component } from "../../entity";
|
|
3
|
+
import { World } from "../../world/world";
|
|
4
|
+
|
|
5
|
+
describe("Query", () => {
|
|
6
|
+
describe("Query Caching and Reference Counting", () => {
|
|
7
|
+
type Position = { x: number; y: number };
|
|
8
|
+
type Velocity = { x: number; y: number };
|
|
9
|
+
|
|
10
|
+
const positionComponent = component<Position>();
|
|
11
|
+
const velocityComponent = component<Velocity>();
|
|
12
|
+
|
|
13
|
+
it("should cache queries and return the same instance for identical queries", () => {
|
|
14
|
+
const world = new World();
|
|
15
|
+
|
|
16
|
+
// Create two queries with the same component types
|
|
17
|
+
const query1 = world.createQuery([positionComponent]);
|
|
18
|
+
const query2 = world.createQuery([positionComponent]);
|
|
19
|
+
|
|
20
|
+
// Should return the same cached instance
|
|
21
|
+
expect(query1).toBe(query2);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("should cache queries with different component orders as the same query", () => {
|
|
25
|
+
const world = new World();
|
|
26
|
+
|
|
27
|
+
// Create queries with same components but different order
|
|
28
|
+
const query1 = world.createQuery([positionComponent, velocityComponent]);
|
|
29
|
+
const query2 = world.createQuery([velocityComponent, positionComponent]);
|
|
30
|
+
|
|
31
|
+
// Should return the same cached instance (sorted internally)
|
|
32
|
+
expect(query1).toBe(query2);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("should create different queries for different component combinations", () => {
|
|
36
|
+
const world = new World();
|
|
37
|
+
|
|
38
|
+
const query1 = world.createQuery([positionComponent]);
|
|
39
|
+
const query2 = world.createQuery([velocityComponent]);
|
|
40
|
+
const query3 = world.createQuery([positionComponent, velocityComponent]);
|
|
41
|
+
|
|
42
|
+
// All should be different instances
|
|
43
|
+
expect(query1).not.toBe(query2);
|
|
44
|
+
expect(query1).not.toBe(query3);
|
|
45
|
+
expect(query2).not.toBe(query3);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("should properly handle reference counting", () => {
|
|
49
|
+
const world = new World();
|
|
50
|
+
|
|
51
|
+
// Create multiple references to the same query
|
|
52
|
+
const query1 = world.createQuery([positionComponent]);
|
|
53
|
+
const query2 = world.createQuery([positionComponent]);
|
|
54
|
+
const query3 = world.createQuery([positionComponent]);
|
|
55
|
+
|
|
56
|
+
// All should be the same instance
|
|
57
|
+
expect(query1).toBe(query2);
|
|
58
|
+
expect(query2).toBe(query3);
|
|
59
|
+
|
|
60
|
+
// Release all three references
|
|
61
|
+
world.releaseQuery(query1);
|
|
62
|
+
world.releaseQuery(query2);
|
|
63
|
+
world.releaseQuery(query3);
|
|
64
|
+
|
|
65
|
+
// Now create a new query - should be a new instance since cache was cleared
|
|
66
|
+
const query4 = world.createQuery([positionComponent]);
|
|
67
|
+
expect(query4).not.toBe(query1); // Should be a new instance
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("should handle releaseQuery on non-cached queries gracefully", () => {
|
|
71
|
+
const world = new World();
|
|
72
|
+
|
|
73
|
+
// Create a query and immediately release it
|
|
74
|
+
const query = world.createQuery([positionComponent]);
|
|
75
|
+
world.releaseQuery(query);
|
|
76
|
+
|
|
77
|
+
// Should not throw and should create a new instance next time
|
|
78
|
+
const query2 = world.createQuery([positionComponent]);
|
|
79
|
+
expect(query2).not.toBe(query);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("should cache queries with filters separately", () => {
|
|
83
|
+
const world = new World();
|
|
84
|
+
type Health = { value: number };
|
|
85
|
+
const healthComponent = component<Health>();
|
|
86
|
+
|
|
87
|
+
// Create queries with and without filters
|
|
88
|
+
const query1 = world.createQuery([positionComponent]);
|
|
89
|
+
const query2 = world.createQuery([positionComponent], { negativeComponentTypes: [healthComponent] });
|
|
90
|
+
|
|
91
|
+
// Should be different instances due to different filters
|
|
92
|
+
expect(query1).not.toBe(query2);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("should maintain separate caches for queries with different filters", () => {
|
|
96
|
+
const world = new World();
|
|
97
|
+
type Health = { value: number };
|
|
98
|
+
const healthComponent = component<Health>();
|
|
99
|
+
|
|
100
|
+
// Create multiple queries with the same filter
|
|
101
|
+
const query1 = world.createQuery([positionComponent], { negativeComponentTypes: [healthComponent] });
|
|
102
|
+
const query2 = world.createQuery([positionComponent], { negativeComponentTypes: [healthComponent] });
|
|
103
|
+
|
|
104
|
+
// Should return the same cached instance
|
|
105
|
+
expect(query1).toBe(query2);
|
|
106
|
+
|
|
107
|
+
// Create queries with different filters
|
|
108
|
+
const query3 = world.createQuery([positionComponent], { negativeComponentTypes: [velocityComponent] });
|
|
109
|
+
expect(query1).not.toBe(query3);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
});
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { Archetype } from "../../archetype/archetype";
|
|
3
|
+
import type { ComponentId, EntityId } from "../../entity";
|
|
4
|
+
import { relation } from "../../entity";
|
|
5
|
+
import { matchesComponentTypes, matchesFilter, type QueryFilter } from "../../query/filter";
|
|
6
|
+
|
|
7
|
+
// Mock component IDs for testing
|
|
8
|
+
const positionComponent = 1 as ComponentId<{ x: number; y: number }>;
|
|
9
|
+
const velocityComponent = 2 as ComponentId<{ dx: number; dy: number }>;
|
|
10
|
+
const healthComponent = 3 as ComponentId<{ value: number }>;
|
|
11
|
+
const relationComponent = 4 as ComponentId<{ strength: number }>;
|
|
12
|
+
|
|
13
|
+
// Helper function to create a dontFragmentRelations map for testing
|
|
14
|
+
const createDontFragmentRelations = () => new Map<EntityId, Map<EntityId<any>, any>>();
|
|
15
|
+
|
|
16
|
+
describe("Query Filter Functions", () => {
|
|
17
|
+
describe("matchesComponentTypes", () => {
|
|
18
|
+
it("should return true when archetype contains all required component types", () => {
|
|
19
|
+
const archetype = new Archetype([positionComponent, velocityComponent], createDontFragmentRelations());
|
|
20
|
+
const componentTypes = [positionComponent, velocityComponent];
|
|
21
|
+
expect(matchesComponentTypes(archetype, componentTypes)).toBe(true);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("should return true when archetype contains required component types and more", () => {
|
|
25
|
+
const archetype = new Archetype(
|
|
26
|
+
[positionComponent, velocityComponent, healthComponent],
|
|
27
|
+
createDontFragmentRelations(),
|
|
28
|
+
);
|
|
29
|
+
const componentTypes = [positionComponent, velocityComponent];
|
|
30
|
+
expect(matchesComponentTypes(archetype, componentTypes)).toBe(true);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("should return false when archetype is missing a required component type", () => {
|
|
34
|
+
const archetype = new Archetype([positionComponent], createDontFragmentRelations());
|
|
35
|
+
const componentTypes = [positionComponent, velocityComponent];
|
|
36
|
+
expect(matchesComponentTypes(archetype, componentTypes)).toBe(false);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("should return true for empty component types array", () => {
|
|
40
|
+
const archetype = new Archetype([positionComponent], createDontFragmentRelations());
|
|
41
|
+
const componentTypes: EntityId<any>[] = [];
|
|
42
|
+
expect(matchesComponentTypes(archetype, componentTypes)).toBe(true);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe("matchesFilter", () => {
|
|
47
|
+
it("should return true when no negative component types are specified", () => {
|
|
48
|
+
const archetype = new Archetype([positionComponent, velocityComponent], createDontFragmentRelations());
|
|
49
|
+
const filter: QueryFilter = {};
|
|
50
|
+
expect(matchesFilter(archetype, filter)).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("should return true when archetype does not contain any negative component types", () => {
|
|
54
|
+
const archetype = new Archetype([positionComponent, velocityComponent], createDontFragmentRelations());
|
|
55
|
+
const filter: QueryFilter = { negativeComponentTypes: [healthComponent] };
|
|
56
|
+
expect(matchesFilter(archetype, filter)).toBe(true);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("should return false when archetype contains a negative component type", () => {
|
|
60
|
+
const archetype = new Archetype(
|
|
61
|
+
[positionComponent, velocityComponent, healthComponent],
|
|
62
|
+
createDontFragmentRelations(),
|
|
63
|
+
);
|
|
64
|
+
const filter: QueryFilter = { negativeComponentTypes: [healthComponent] };
|
|
65
|
+
expect(matchesFilter(archetype, filter)).toBe(false);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("should return false when archetype contains any of multiple negative component types", () => {
|
|
69
|
+
const archetype = new Archetype([positionComponent, healthComponent], createDontFragmentRelations());
|
|
70
|
+
const filter: QueryFilter = { negativeComponentTypes: [velocityComponent, healthComponent] };
|
|
71
|
+
expect(matchesFilter(archetype, filter)).toBe(false);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("should return true when archetype contains none of multiple negative component types", () => {
|
|
75
|
+
const archetype = new Archetype([positionComponent], createDontFragmentRelations());
|
|
76
|
+
const filter: QueryFilter = { negativeComponentTypes: [velocityComponent, healthComponent] };
|
|
77
|
+
expect(matchesFilter(archetype, filter)).toBe(true);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("should return false when archetype contains a negative wildcard relation component", () => {
|
|
81
|
+
const wildcardRelation = relation(relationComponent, "*");
|
|
82
|
+
const archetype = new Archetype([positionComponent, wildcardRelation], createDontFragmentRelations());
|
|
83
|
+
const filter: QueryFilter = { negativeComponentTypes: [wildcardRelation] };
|
|
84
|
+
expect(matchesFilter(archetype, filter)).toBe(false);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("should return false when archetype contains a specific relation matching negative wildcard filter", () => {
|
|
88
|
+
const wildcardRelation = relation(relationComponent, "*");
|
|
89
|
+
const otherRelation = relation(relationComponent, 1025 as EntityId);
|
|
90
|
+
const archetype = new Archetype([positionComponent, otherRelation], createDontFragmentRelations());
|
|
91
|
+
const filter: QueryFilter = { negativeComponentTypes: [wildcardRelation] };
|
|
92
|
+
expect(matchesFilter(archetype, filter)).toBe(false);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("should return true when archetype does not contain any relations with the wildcard component", () => {
|
|
96
|
+
const wildcardRelation = relation(relationComponent, "*");
|
|
97
|
+
const otherComponent = 5 as EntityId<{ other: number }>;
|
|
98
|
+
const archetype = new Archetype([positionComponent, otherComponent], createDontFragmentRelations());
|
|
99
|
+
const filter: QueryFilter = { negativeComponentTypes: [wildcardRelation] };
|
|
100
|
+
expect(matchesFilter(archetype, filter)).toBe(true);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("should return false when archetype contains wildcard relation matching negative filter", () => {
|
|
104
|
+
const wildcardRelation = relation(relationComponent, "*");
|
|
105
|
+
const matchingRelation = relation(relationComponent, 1026 as EntityId);
|
|
106
|
+
const archetype = new Archetype([positionComponent, matchingRelation], createDontFragmentRelations());
|
|
107
|
+
const filter: QueryFilter = { negativeComponentTypes: [wildcardRelation] };
|
|
108
|
+
expect(matchesFilter(archetype, filter)).toBe(false);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
});
|