@codehz/ecs 0.7.2 → 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/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/{builder.d.mts → dist/builder.d.mts} +0 -0
- /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
- /package/{world.mjs → dist/world.mjs} +0 -0
- /package/{world.mjs.map → dist/world.mjs.map} +0 -0
|
@@ -0,0 +1,496 @@
|
|
|
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("DontFragment Relations", () => {
|
|
6
|
+
it("should prevent archetype fragmentation for dontFragment relations", () => {
|
|
7
|
+
const world = new World();
|
|
8
|
+
|
|
9
|
+
// Create component types
|
|
10
|
+
type Position = { x: number; y: number };
|
|
11
|
+
const PositionId = component<Position>();
|
|
12
|
+
const VelocityId = component();
|
|
13
|
+
|
|
14
|
+
// Create ChildOf with dontFragment option
|
|
15
|
+
const ChildOf = component({ dontFragment: true });
|
|
16
|
+
|
|
17
|
+
// Create parent entities
|
|
18
|
+
const parent1 = world.new();
|
|
19
|
+
const parent2 = world.new();
|
|
20
|
+
const parent3 = world.new();
|
|
21
|
+
|
|
22
|
+
// Create child entities with different parents
|
|
23
|
+
const child1 = world.new();
|
|
24
|
+
world.set(child1, PositionId, { x: 1, y: 1 });
|
|
25
|
+
world.set(child1, VelocityId);
|
|
26
|
+
world.set(child1, relation(ChildOf, parent1));
|
|
27
|
+
|
|
28
|
+
const child2 = world.new();
|
|
29
|
+
world.set(child2, PositionId, { x: 2, y: 2 });
|
|
30
|
+
world.set(child2, VelocityId);
|
|
31
|
+
world.set(child2, relation(ChildOf, parent2));
|
|
32
|
+
|
|
33
|
+
const child3 = world.new();
|
|
34
|
+
world.set(child3, PositionId, { x: 3, y: 3 });
|
|
35
|
+
world.set(child3, VelocityId);
|
|
36
|
+
world.set(child3, relation(ChildOf, parent3));
|
|
37
|
+
|
|
38
|
+
world.sync();
|
|
39
|
+
|
|
40
|
+
// Verify all children are in the same archetype
|
|
41
|
+
// This is the key benefit: despite having different parent relations,
|
|
42
|
+
// they share the same archetype because ChildOf is marked as dontFragment
|
|
43
|
+
const archetypes = (world as any).archetypes;
|
|
44
|
+
|
|
45
|
+
// Count archetypes with Position and Velocity
|
|
46
|
+
const matchingArchetypes = archetypes.filter((arch: any) => {
|
|
47
|
+
const types = arch.componentTypes;
|
|
48
|
+
return types.includes(PositionId) && types.includes(VelocityId);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// All three children should be in the SAME archetype
|
|
52
|
+
expect(matchingArchetypes.length).toBe(1);
|
|
53
|
+
expect(matchingArchetypes[0].size).toBe(3);
|
|
54
|
+
|
|
55
|
+
// Verify we can still access the relations
|
|
56
|
+
expect(world.has(child1, relation(ChildOf, parent1))).toBe(true);
|
|
57
|
+
expect(world.has(child2, relation(ChildOf, parent2))).toBe(true);
|
|
58
|
+
expect(world.has(child3, relation(ChildOf, parent3))).toBe(true);
|
|
59
|
+
|
|
60
|
+
// Verify queries still work
|
|
61
|
+
const entities = world.query([PositionId, VelocityId]);
|
|
62
|
+
expect(entities.length).toBe(3);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("should handle dontFragment relations with wildcard queries", () => {
|
|
66
|
+
const world = new World();
|
|
67
|
+
|
|
68
|
+
const PositionId = component();
|
|
69
|
+
const ChildOf = component({ dontFragment: true });
|
|
70
|
+
|
|
71
|
+
const parent1 = world.new();
|
|
72
|
+
const parent2 = world.new();
|
|
73
|
+
|
|
74
|
+
const child1 = world.new();
|
|
75
|
+
world.set(child1, PositionId);
|
|
76
|
+
world.set(child1, relation(ChildOf, parent1));
|
|
77
|
+
|
|
78
|
+
const child2 = world.new();
|
|
79
|
+
world.set(child2, PositionId);
|
|
80
|
+
world.set(child2, relation(ChildOf, parent2));
|
|
81
|
+
|
|
82
|
+
world.sync();
|
|
83
|
+
|
|
84
|
+
// Wildcard query should work with dontFragment relations
|
|
85
|
+
const wildcardChildOf = relation(ChildOf, "*");
|
|
86
|
+
const child1Relations = world.get(child1, wildcardChildOf);
|
|
87
|
+
const child2Relations = world.get(child2, wildcardChildOf);
|
|
88
|
+
|
|
89
|
+
expect(child1Relations.length).toBe(1);
|
|
90
|
+
expect(child1Relations[0]![0]).toBe(parent1);
|
|
91
|
+
|
|
92
|
+
expect(child2Relations.length).toBe(1);
|
|
93
|
+
expect(child2Relations[0]![0]).toBe(parent2);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("should allow updating dontFragment relations", () => {
|
|
97
|
+
const world = new World();
|
|
98
|
+
|
|
99
|
+
const ChildOf = component({ dontFragment: true, exclusive: true });
|
|
100
|
+
const PositionId = component();
|
|
101
|
+
|
|
102
|
+
const parent1 = world.new();
|
|
103
|
+
const parent2 = world.new();
|
|
104
|
+
const child = world.new();
|
|
105
|
+
|
|
106
|
+
world.set(child, PositionId);
|
|
107
|
+
world.set(child, relation(ChildOf, parent1));
|
|
108
|
+
world.sync();
|
|
109
|
+
|
|
110
|
+
expect(world.has(child, relation(ChildOf, parent1))).toBe(true);
|
|
111
|
+
|
|
112
|
+
// Change parent (exclusive should replace)
|
|
113
|
+
world.set(child, relation(ChildOf, parent2));
|
|
114
|
+
world.sync();
|
|
115
|
+
|
|
116
|
+
expect(world.has(child, relation(ChildOf, parent1))).toBe(false);
|
|
117
|
+
expect(world.has(child, relation(ChildOf, parent2))).toBe(true);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("should handle removing dontFragment relations", () => {
|
|
121
|
+
const world = new World();
|
|
122
|
+
|
|
123
|
+
const ChildOf = component({ dontFragment: true });
|
|
124
|
+
const PositionId = component();
|
|
125
|
+
|
|
126
|
+
const parent = world.new();
|
|
127
|
+
const child = world.new();
|
|
128
|
+
|
|
129
|
+
world.set(child, PositionId);
|
|
130
|
+
world.set(child, relation(ChildOf, parent));
|
|
131
|
+
world.sync();
|
|
132
|
+
|
|
133
|
+
expect(world.has(child, relation(ChildOf, parent))).toBe(true);
|
|
134
|
+
|
|
135
|
+
// Remove the relation
|
|
136
|
+
world.remove(child, relation(ChildOf, parent));
|
|
137
|
+
world.sync();
|
|
138
|
+
|
|
139
|
+
expect(world.has(child, relation(ChildOf, parent))).toBe(false);
|
|
140
|
+
expect(world.has(child, PositionId)).toBe(true);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("should handle queries with dontFragment relations", () => {
|
|
144
|
+
const world = new World();
|
|
145
|
+
|
|
146
|
+
const PositionId = component();
|
|
147
|
+
const VelocityId = component();
|
|
148
|
+
const ChildOf = component({ dontFragment: true });
|
|
149
|
+
|
|
150
|
+
const parent1 = world.new();
|
|
151
|
+
const parent2 = world.new();
|
|
152
|
+
|
|
153
|
+
// Create entities with dontFragment relations
|
|
154
|
+
for (let i = 0; i < 10; i++) {
|
|
155
|
+
const entity = world.new();
|
|
156
|
+
world.set(entity, PositionId);
|
|
157
|
+
world.set(entity, VelocityId);
|
|
158
|
+
world.set(entity, relation(ChildOf, i % 2 === 0 ? parent1 : parent2));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
world.sync();
|
|
162
|
+
|
|
163
|
+
// Query should find all entities despite different parent relations
|
|
164
|
+
const query = world.createQuery([PositionId, VelocityId]);
|
|
165
|
+
const entities = query.getEntities();
|
|
166
|
+
expect(entities.length).toBe(10);
|
|
167
|
+
|
|
168
|
+
// All should be in the same archetype
|
|
169
|
+
const archetypes = (world as any).archetypes;
|
|
170
|
+
const matchingArchetypes = archetypes.filter((arch: any) => {
|
|
171
|
+
return arch.componentTypes.includes(PositionId) && arch.componentTypes.includes(VelocityId);
|
|
172
|
+
});
|
|
173
|
+
expect(matchingArchetypes.length).toBe(1);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("should compare fragmentation: with and without dontFragment", () => {
|
|
177
|
+
// Test WITHOUT dontFragment (causes fragmentation)
|
|
178
|
+
const world1 = new World();
|
|
179
|
+
const PositionId1 = component();
|
|
180
|
+
const ChildOf1 = component(); // No dontFragment
|
|
181
|
+
|
|
182
|
+
for (let i = 0; i < 5; i++) {
|
|
183
|
+
const parent = world1.new();
|
|
184
|
+
const child = world1.new();
|
|
185
|
+
world1.set(child, PositionId1);
|
|
186
|
+
world1.set(child, relation(ChildOf1, parent));
|
|
187
|
+
}
|
|
188
|
+
world1.sync();
|
|
189
|
+
|
|
190
|
+
const archetypes1 = (world1 as any).archetypes.filter((arch: any) => {
|
|
191
|
+
return arch.componentTypes.includes(PositionId1);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// Test WITH dontFragment (prevents fragmentation)
|
|
195
|
+
const world2 = new World();
|
|
196
|
+
const PositionId2 = component();
|
|
197
|
+
const ChildOf2 = component({ dontFragment: true }); // With dontFragment
|
|
198
|
+
|
|
199
|
+
for (let i = 0; i < 5; i++) {
|
|
200
|
+
const parent = world2.new();
|
|
201
|
+
const child = world2.new();
|
|
202
|
+
world2.set(child, PositionId2);
|
|
203
|
+
world2.set(child, relation(ChildOf2, parent));
|
|
204
|
+
}
|
|
205
|
+
world2.sync();
|
|
206
|
+
|
|
207
|
+
const archetypes2 = (world2 as any).archetypes.filter((arch: any) => {
|
|
208
|
+
return arch.componentTypes.includes(PositionId2);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// Without dontFragment: 5 archetypes (one per parent)
|
|
212
|
+
expect(archetypes1.length).toBe(5);
|
|
213
|
+
|
|
214
|
+
// With dontFragment: 1 archetype (all children share it)
|
|
215
|
+
expect(archetypes2.length).toBe(1);
|
|
216
|
+
expect(archetypes2[0].size).toBe(5);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("should query entities with wildcard relation on dontFragment component using createQuery", () => {
|
|
220
|
+
const world = new World();
|
|
221
|
+
|
|
222
|
+
const PositionId = component();
|
|
223
|
+
const ChildOf = component({ dontFragment: true });
|
|
224
|
+
|
|
225
|
+
const parent1 = world.new();
|
|
226
|
+
const parent2 = world.new();
|
|
227
|
+
|
|
228
|
+
const child1 = world.new();
|
|
229
|
+
world.set(child1, PositionId);
|
|
230
|
+
world.set(child1, relation(ChildOf, parent1));
|
|
231
|
+
|
|
232
|
+
const child2 = world.new();
|
|
233
|
+
world.set(child2, PositionId);
|
|
234
|
+
world.set(child2, relation(ChildOf, parent2));
|
|
235
|
+
|
|
236
|
+
world.sync();
|
|
237
|
+
|
|
238
|
+
// Try to query entities with wildcard ChildOf relation
|
|
239
|
+
const wildcardChildOf = relation(ChildOf, "*");
|
|
240
|
+
const query = world.createQuery([wildcardChildOf]);
|
|
241
|
+
const entities = query.getEntities();
|
|
242
|
+
|
|
243
|
+
// This should find both child1 and child2
|
|
244
|
+
expect(entities.length).toBe(2);
|
|
245
|
+
expect(entities).toContain(child1);
|
|
246
|
+
expect(entities).toContain(child2);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it("should query entities with wildcard relation + other components on dontFragment", () => {
|
|
250
|
+
const world = new World();
|
|
251
|
+
|
|
252
|
+
const PositionId = component();
|
|
253
|
+
const VelocityId = component();
|
|
254
|
+
const ChildOf = component({ dontFragment: true });
|
|
255
|
+
|
|
256
|
+
const parent1 = world.new();
|
|
257
|
+
const parent2 = world.new();
|
|
258
|
+
|
|
259
|
+
const child1 = world.new();
|
|
260
|
+
world.set(child1, PositionId);
|
|
261
|
+
world.set(child1, VelocityId);
|
|
262
|
+
world.set(child1, relation(ChildOf, parent1));
|
|
263
|
+
|
|
264
|
+
const child2 = world.new();
|
|
265
|
+
world.set(child2, PositionId);
|
|
266
|
+
world.set(child2, VelocityId);
|
|
267
|
+
world.set(child2, relation(ChildOf, parent2));
|
|
268
|
+
|
|
269
|
+
// Entity without ChildOf relation
|
|
270
|
+
const child3 = world.new();
|
|
271
|
+
world.set(child3, PositionId);
|
|
272
|
+
world.set(child3, VelocityId);
|
|
273
|
+
|
|
274
|
+
world.sync();
|
|
275
|
+
|
|
276
|
+
// Query for entities with wildcard ChildOf relation AND Position
|
|
277
|
+
const wildcardChildOf = relation(ChildOf, "*");
|
|
278
|
+
const query = world.createQuery([wildcardChildOf, PositionId]);
|
|
279
|
+
const entities = query.getEntities();
|
|
280
|
+
|
|
281
|
+
// Should find child1 and child2, but not child3 (no ChildOf relation)
|
|
282
|
+
expect(entities.length).toBe(2);
|
|
283
|
+
expect(entities).toContain(child1);
|
|
284
|
+
expect(entities).toContain(child2);
|
|
285
|
+
expect(entities).not.toContain(child3);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it("should correctly cleanup dontFragment relations when target entity is destroyed", () => {
|
|
289
|
+
const world = new World();
|
|
290
|
+
|
|
291
|
+
const PositionId = component();
|
|
292
|
+
const VelocityId = component();
|
|
293
|
+
const ChildOf = component({ dontFragment: true });
|
|
294
|
+
|
|
295
|
+
const parent1 = world.new();
|
|
296
|
+
const parent2 = world.new();
|
|
297
|
+
|
|
298
|
+
// Create children with dontFragment relations
|
|
299
|
+
const child1 = world.new();
|
|
300
|
+
world.set(child1, PositionId);
|
|
301
|
+
world.set(child1, VelocityId);
|
|
302
|
+
world.set(child1, relation(ChildOf, parent1));
|
|
303
|
+
|
|
304
|
+
const child2 = world.new();
|
|
305
|
+
world.set(child2, PositionId);
|
|
306
|
+
world.set(child2, VelocityId);
|
|
307
|
+
world.set(child2, relation(ChildOf, parent2));
|
|
308
|
+
|
|
309
|
+
const child3 = world.new();
|
|
310
|
+
world.set(child3, PositionId);
|
|
311
|
+
world.set(child3, VelocityId);
|
|
312
|
+
world.set(child3, relation(ChildOf, parent1)); // Same parent as child1
|
|
313
|
+
|
|
314
|
+
world.sync();
|
|
315
|
+
|
|
316
|
+
// All children should be in the same archetype (due to dontFragment)
|
|
317
|
+
const archetypes = (world as any).archetypes;
|
|
318
|
+
const matchingArchetypesBefore = archetypes.filter((arch: any) => {
|
|
319
|
+
return arch.componentTypes.includes(PositionId) && arch.componentTypes.includes(VelocityId);
|
|
320
|
+
});
|
|
321
|
+
expect(matchingArchetypesBefore.length).toBe(1);
|
|
322
|
+
expect(matchingArchetypesBefore[0].size).toBe(3);
|
|
323
|
+
|
|
324
|
+
// Verify relations exist
|
|
325
|
+
expect(world.has(child1, relation(ChildOf, parent1))).toBe(true);
|
|
326
|
+
expect(world.has(child2, relation(ChildOf, parent2))).toBe(true);
|
|
327
|
+
expect(world.has(child3, relation(ChildOf, parent1))).toBe(true);
|
|
328
|
+
|
|
329
|
+
// Delete parent1 - should remove relations from child1 and child3
|
|
330
|
+
world.delete(parent1);
|
|
331
|
+
world.sync();
|
|
332
|
+
|
|
333
|
+
// Relations to parent1 should be removed
|
|
334
|
+
expect(world.has(child1, relation(ChildOf, parent1))).toBe(false);
|
|
335
|
+
expect(world.has(child3, relation(ChildOf, parent1))).toBe(false);
|
|
336
|
+
|
|
337
|
+
// Relation to parent2 should still exist
|
|
338
|
+
expect(world.has(child2, relation(ChildOf, parent2))).toBe(true);
|
|
339
|
+
|
|
340
|
+
// Entities should still exist with their other components
|
|
341
|
+
expect(world.exists(child1)).toBe(true);
|
|
342
|
+
expect(world.exists(child2)).toBe(true);
|
|
343
|
+
expect(world.exists(child3)).toBe(true);
|
|
344
|
+
expect(world.has(child1, PositionId)).toBe(true);
|
|
345
|
+
expect(world.has(child2, PositionId)).toBe(true);
|
|
346
|
+
expect(world.has(child3, PositionId)).toBe(true);
|
|
347
|
+
|
|
348
|
+
// Archetype should not fragment - entities without relations should move to a different archetype
|
|
349
|
+
// (one without the wildcard marker)
|
|
350
|
+
const matchingArchetypesAfter = archetypes.filter((arch: any) => {
|
|
351
|
+
return arch.componentTypes.includes(PositionId) && arch.componentTypes.includes(VelocityId);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
// child1 and child3 no longer have ChildOf relations, so they should be in an archetype
|
|
355
|
+
// without the wildcard marker, while child2 should be in the one with the marker
|
|
356
|
+
expect(matchingArchetypesAfter.length).toBe(2);
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it("should not create new archetypes when removing dontFragment relation from entity", () => {
|
|
360
|
+
const world = new World();
|
|
361
|
+
|
|
362
|
+
const PositionId = component();
|
|
363
|
+
const ChildOf = component({ dontFragment: true });
|
|
364
|
+
|
|
365
|
+
const parent1 = world.new();
|
|
366
|
+
const parent2 = world.new();
|
|
367
|
+
|
|
368
|
+
// Create multiple children with different parents
|
|
369
|
+
const children: EntityId[] = [];
|
|
370
|
+
for (let i = 0; i < 5; i++) {
|
|
371
|
+
const child = world.new();
|
|
372
|
+
world.set(child, PositionId);
|
|
373
|
+
world.set(child, relation(ChildOf, i % 2 === 0 ? parent1 : parent2));
|
|
374
|
+
children.push(child);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
world.sync();
|
|
378
|
+
|
|
379
|
+
// Count archetypes before deletion
|
|
380
|
+
const archetypesBefore = (world as any).archetypes.length;
|
|
381
|
+
|
|
382
|
+
// Delete parent1 - this should remove relations but not fragment
|
|
383
|
+
world.delete(parent1);
|
|
384
|
+
world.sync();
|
|
385
|
+
|
|
386
|
+
// Some children (those with parent1) should have lost their ChildOf relation
|
|
387
|
+
// but the archetype structure should be minimal (not fragmented)
|
|
388
|
+
const archetypesAfter = (world as any).archetypes.length;
|
|
389
|
+
|
|
390
|
+
// We expect at most one new archetype (for entities without ChildOf)
|
|
391
|
+
// The key point is we don't create separate archetypes per entity
|
|
392
|
+
expect(archetypesAfter).toBeLessThanOrEqual(archetypesBefore + 1);
|
|
393
|
+
|
|
394
|
+
// Verify entities still exist and have Position
|
|
395
|
+
for (const child of children) {
|
|
396
|
+
expect(world.exists(child)).toBe(true);
|
|
397
|
+
expect(world.has(child, PositionId)).toBe(true);
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
it("should trigger lifecycle hooks when dontFragment relations are removed due to entity destruction", () => {
|
|
402
|
+
const world = new World();
|
|
403
|
+
|
|
404
|
+
const ChildOf = component({ dontFragment: true });
|
|
405
|
+
const PositionId = component();
|
|
406
|
+
|
|
407
|
+
const parent = world.new();
|
|
408
|
+
const child = world.new();
|
|
409
|
+
world.set(child, PositionId);
|
|
410
|
+
world.set(child, relation(ChildOf, parent));
|
|
411
|
+
world.sync();
|
|
412
|
+
|
|
413
|
+
// Set up hook to track removals
|
|
414
|
+
const removedRelations: Array<{ entity: number; relations: [number, void][] }> = [];
|
|
415
|
+
const wildcardChildOf = relation(ChildOf, "*");
|
|
416
|
+
world.hook([wildcardChildOf], {
|
|
417
|
+
on_remove: (entity, relations) => {
|
|
418
|
+
removedRelations.push({ entity, relations });
|
|
419
|
+
},
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
// Delete parent - should trigger hook for removed relation
|
|
423
|
+
world.delete(parent);
|
|
424
|
+
world.sync();
|
|
425
|
+
|
|
426
|
+
// Hook should have been called
|
|
427
|
+
expect(removedRelations.length).toBe(1);
|
|
428
|
+
expect(removedRelations[0]!.entity).toBe(child);
|
|
429
|
+
expect(removedRelations[0]!.relations).toEqual([[parent, undefined]]);
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
it("should handle cascade delete with dontFragment relations correctly", () => {
|
|
433
|
+
const world = new World();
|
|
434
|
+
|
|
435
|
+
const PositionId = component();
|
|
436
|
+
// Cascade delete AND dontFragment - when parent dies, children die too
|
|
437
|
+
const ChildOf = component({ dontFragment: true, cascadeDelete: true });
|
|
438
|
+
|
|
439
|
+
const grandparent = world.new();
|
|
440
|
+
const parent = world.new();
|
|
441
|
+
world.set(parent, PositionId);
|
|
442
|
+
world.set(parent, relation(ChildOf, grandparent));
|
|
443
|
+
|
|
444
|
+
const child = world.new();
|
|
445
|
+
world.set(child, PositionId);
|
|
446
|
+
world.set(child, relation(ChildOf, parent));
|
|
447
|
+
|
|
448
|
+
world.sync();
|
|
449
|
+
|
|
450
|
+
// Verify hierarchy
|
|
451
|
+
expect(world.exists(grandparent)).toBe(true);
|
|
452
|
+
expect(world.exists(parent)).toBe(true);
|
|
453
|
+
expect(world.exists(child)).toBe(true);
|
|
454
|
+
|
|
455
|
+
// Delete grandparent - should cascade to parent, then to child
|
|
456
|
+
world.delete(grandparent);
|
|
457
|
+
world.sync();
|
|
458
|
+
|
|
459
|
+
// All should be deleted due to cascade
|
|
460
|
+
expect(world.exists(grandparent)).toBe(false);
|
|
461
|
+
expect(world.exists(parent)).toBe(false);
|
|
462
|
+
expect(world.exists(child)).toBe(false);
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
it("should maintain entity archetype integrity when removing dontFragment relations", () => {
|
|
466
|
+
const world = new World();
|
|
467
|
+
|
|
468
|
+
const PositionId = component<{ x: number; y: number }>();
|
|
469
|
+
const VelocityId = component<{ vx: number; vy: number }>();
|
|
470
|
+
const ChildOf = component({ dontFragment: true });
|
|
471
|
+
|
|
472
|
+
const parent = world.new();
|
|
473
|
+
|
|
474
|
+
// Create entity with components and dontFragment relation
|
|
475
|
+
const entity = world.new();
|
|
476
|
+
world.set(entity, PositionId, { x: 10, y: 20 });
|
|
477
|
+
world.set(entity, VelocityId, { vx: 1, vy: 2 });
|
|
478
|
+
world.set(entity, relation(ChildOf, parent));
|
|
479
|
+
world.sync();
|
|
480
|
+
|
|
481
|
+
// Verify initial state
|
|
482
|
+
expect(world.get(entity, PositionId)).toEqual({ x: 10, y: 20 });
|
|
483
|
+
expect(world.get(entity, VelocityId)).toEqual({ vx: 1, vy: 2 });
|
|
484
|
+
expect(world.has(entity, relation(ChildOf, parent))).toBe(true);
|
|
485
|
+
|
|
486
|
+
// Delete parent - relation should be removed but other components preserved
|
|
487
|
+
world.delete(parent);
|
|
488
|
+
world.sync();
|
|
489
|
+
|
|
490
|
+
// Entity should still exist with all other components intact
|
|
491
|
+
expect(world.exists(entity)).toBe(true);
|
|
492
|
+
expect(world.has(entity, relation(ChildOf, parent))).toBe(false);
|
|
493
|
+
expect(world.get(entity, PositionId)).toEqual({ x: 10, y: 20 });
|
|
494
|
+
expect(world.get(entity, VelocityId)).toEqual({ vx: 1, vy: 2 });
|
|
495
|
+
});
|
|
496
|
+
});
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { component, relation } from "../../../entity";
|
|
3
|
+
import { World } from "../../../world/world";
|
|
4
|
+
|
|
5
|
+
describe("DontFragment Query Notification Issue", () => {
|
|
6
|
+
it("should handle dontFragment wildcard queries and archetype lifecycle", () => {
|
|
7
|
+
const world = new World();
|
|
8
|
+
const Position = component();
|
|
9
|
+
const ChildOf = component({ dontFragment: true });
|
|
10
|
+
const WildcardChildOf = relation(ChildOf, "*");
|
|
11
|
+
|
|
12
|
+
const query = world.createQuery([WildcardChildOf, Position]);
|
|
13
|
+
expect(query.getEntities().length).toBe(0);
|
|
14
|
+
|
|
15
|
+
const parent1 = world.new();
|
|
16
|
+
const child1 = world.new();
|
|
17
|
+
world.set(child1, Position);
|
|
18
|
+
world.set(child1, relation(ChildOf, parent1));
|
|
19
|
+
world.sync();
|
|
20
|
+
|
|
21
|
+
// Verify entity is found and archetype has wildcard marker
|
|
22
|
+
expect(query.getEntities()).toContain(child1);
|
|
23
|
+
const arch1 = (world as any).entityToArchetype.get(child1);
|
|
24
|
+
expect(arch1.componentTypes).toContain(WildcardChildOf);
|
|
25
|
+
|
|
26
|
+
// Verify archetype separation: entity without relation shouldn't match
|
|
27
|
+
const entityWithout = world.new();
|
|
28
|
+
world.set(entityWithout, Position);
|
|
29
|
+
world.sync();
|
|
30
|
+
expect(query.getEntities()).not.toContain(entityWithout);
|
|
31
|
+
expect((world as any).entityToArchetype.get(entityWithout)).not.toBe(arch1);
|
|
32
|
+
|
|
33
|
+
// Add relation to existing entity
|
|
34
|
+
const parent2 = world.new();
|
|
35
|
+
world.set(entityWithout, relation(ChildOf, parent2));
|
|
36
|
+
world.sync();
|
|
37
|
+
expect(query.getEntities()).toContain(entityWithout);
|
|
38
|
+
|
|
39
|
+
// Remove relation: marker should disappear when last one is gone
|
|
40
|
+
world.remove(child1, relation(ChildOf, parent1));
|
|
41
|
+
world.sync();
|
|
42
|
+
expect(query.getEntities()).not.toContain(child1);
|
|
43
|
+
expect((world as any).entityToArchetype.get(child1).componentTypes).not.toContain(WildcardChildOf);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("should handle exclusive dontFragment relations and specific target queries", () => {
|
|
47
|
+
const world = new World();
|
|
48
|
+
const ChildOf = component({ dontFragment: true, exclusive: true });
|
|
49
|
+
const p1 = world.new();
|
|
50
|
+
const p2 = world.new();
|
|
51
|
+
const entity = world.new();
|
|
52
|
+
|
|
53
|
+
const queryP1 = world.createQuery([relation(ChildOf, p1)]);
|
|
54
|
+
const queryP2 = world.createQuery([relation(ChildOf, p2)]);
|
|
55
|
+
|
|
56
|
+
// Set p1
|
|
57
|
+
world.set(entity, relation(ChildOf, p1));
|
|
58
|
+
world.sync();
|
|
59
|
+
expect(queryP1.getEntities()).toContain(entity);
|
|
60
|
+
expect(queryP2.getEntities()).not.toContain(entity);
|
|
61
|
+
|
|
62
|
+
// Re-set p1 (no-op/stable)
|
|
63
|
+
world.set(entity, relation(ChildOf, p1));
|
|
64
|
+
world.sync();
|
|
65
|
+
expect(queryP1.getEntities().length).toBe(1);
|
|
66
|
+
|
|
67
|
+
// Switch to p2
|
|
68
|
+
world.set(entity, relation(ChildOf, p2));
|
|
69
|
+
world.sync();
|
|
70
|
+
expect(queryP1.getEntities()).not.toContain(entity);
|
|
71
|
+
expect(queryP2.getEntities()).toContain(entity);
|
|
72
|
+
|
|
73
|
+
// Wildcard query should still work
|
|
74
|
+
const wildcardQuery = world.createQuery([relation(ChildOf, "*")]);
|
|
75
|
+
expect(wildcardQuery.getEntities()).toContain(entity);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("should handle multiple non-exclusive dontFragment relations", () => {
|
|
79
|
+
const world = new World();
|
|
80
|
+
const Tag = component({ dontFragment: true });
|
|
81
|
+
const t1 = world.new();
|
|
82
|
+
const t2 = world.new();
|
|
83
|
+
const entity = world.new();
|
|
84
|
+
const wildcardQuery = world.createQuery([relation(Tag, "*")]);
|
|
85
|
+
|
|
86
|
+
world.set(entity, relation(Tag, t1));
|
|
87
|
+
world.set(entity, relation(Tag, t2));
|
|
88
|
+
world.sync();
|
|
89
|
+
|
|
90
|
+
expect(wildcardQuery.getEntities().length).toBe(1);
|
|
91
|
+
expect(world.has(entity, relation(Tag, t1))).toBe(true);
|
|
92
|
+
expect(world.has(entity, relation(Tag, t2))).toBe(true);
|
|
93
|
+
|
|
94
|
+
world.remove(entity, relation(Tag, t1));
|
|
95
|
+
world.sync();
|
|
96
|
+
expect(wildcardQuery.getEntities().length).toBe(1);
|
|
97
|
+
expect(world.has(entity, relation(Tag, t1))).toBe(false);
|
|
98
|
+
|
|
99
|
+
world.remove(entity, relation(Tag, t2));
|
|
100
|
+
world.sync();
|
|
101
|
+
expect(wildcardQuery.getEntities().length).toBe(0);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("should correctly filter false positives in wildcard queries", () => {
|
|
105
|
+
const world = new World();
|
|
106
|
+
const TagA = component({ dontFragment: true });
|
|
107
|
+
const TagB = component({ dontFragment: true });
|
|
108
|
+
const p = world.new();
|
|
109
|
+
|
|
110
|
+
const e1 = world.new();
|
|
111
|
+
world.set(e1, relation(TagA, p));
|
|
112
|
+
const e2 = world.new();
|
|
113
|
+
world.set(e2, relation(TagB, p));
|
|
114
|
+
world.sync();
|
|
115
|
+
|
|
116
|
+
// QueryA should only find e1, QueryB should only find e2
|
|
117
|
+
const queryA = world.createQuery([relation(TagA, "*")]);
|
|
118
|
+
const queryB = world.createQuery([relation(TagB, "*")]);
|
|
119
|
+
|
|
120
|
+
expect(queryA.getEntities()).toContain(e1);
|
|
121
|
+
expect(queryA.getEntities()).not.toContain(e2);
|
|
122
|
+
expect(queryB.getEntities()).toContain(e2);
|
|
123
|
+
expect(queryB.getEntities()).not.toContain(e1);
|
|
124
|
+
});
|
|
125
|
+
});
|