@codehz/ecs 0.7.2 → 0.7.4

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.
Files changed (81) hide show
  1. package/examples/advanced-scheduling.ts +96 -0
  2. package/examples/collision-detection.ts +229 -0
  3. package/examples/inventory-system-relations.ts +108 -0
  4. package/examples/parent-child-hierarchy.ts +206 -0
  5. package/examples/serialization.ts +337 -0
  6. package/examples/simple.ts +96 -0
  7. package/examples/spatial-grid.ts +276 -0
  8. package/examples/state-machine.ts +273 -0
  9. package/examples/tag-filtering.ts +266 -0
  10. package/package.json +60 -12
  11. package/src/__tests__/commands/buffer-limits.test.ts +72 -0
  12. package/src/__tests__/commands/buffer.test.ts +195 -0
  13. package/src/__tests__/component/singleton.test.ts +148 -0
  14. package/src/__tests__/core/archetype.test.ts +247 -0
  15. package/src/__tests__/core/bitset.test.ts +171 -0
  16. package/src/__tests__/core/changeset.test.ts +254 -0
  17. package/src/__tests__/core/multi-map.test.ts +74 -0
  18. package/src/__tests__/entity/component-registry.test.ts +66 -0
  19. package/src/__tests__/entity/entity.test.ts +520 -0
  20. package/src/__tests__/entity/id-manager.test.ts +157 -0
  21. package/src/__tests__/entity/id-system.test.ts +260 -0
  22. package/src/__tests__/perf/comprehensive.perf.test.ts +300 -0
  23. package/src/__tests__/perf/sync-hotpath.perf.test.ts +79 -0
  24. package/src/__tests__/query/basic.test.ts +341 -0
  25. package/src/__tests__/query/caching.test.ts +112 -0
  26. package/src/__tests__/query/filter.test.ts +111 -0
  27. package/src/__tests__/query/optional.test.ts +231 -0
  28. package/src/__tests__/query/perf.test.ts +99 -0
  29. package/src/__tests__/relations/dont-fragment/basic.test.ts +496 -0
  30. package/src/__tests__/relations/dont-fragment/query-notification.test.ts +125 -0
  31. package/src/__tests__/relations/wildcard.test.ts +179 -0
  32. package/src/__tests__/serialization/bounds.test.ts +237 -0
  33. package/src/__tests__/testing/assertions.test.ts +224 -0
  34. package/src/__tests__/testing/entity-builder.test.ts +84 -0
  35. package/src/__tests__/testing/snapshot.test.ts +150 -0
  36. package/src/__tests__/testing/world-fixture.test.ts +73 -0
  37. package/src/__tests__/world/component-hooks.test.ts +185 -0
  38. package/src/__tests__/world/component-management.test.ts +447 -0
  39. package/src/__tests__/world/entity-management.test.ts +86 -0
  40. package/src/__tests__/world/get-optional.test.ts +96 -0
  41. package/src/__tests__/world/multi-component-hooks.test.ts +502 -0
  42. package/src/__tests__/world/perf.test.ts +93 -0
  43. package/src/__tests__/world/query.test.ts +223 -0
  44. package/src/__tests__/world/serialize.test.ts +83 -0
  45. package/src/__tests__/world/wildcard-relation-hooks.test.ts +332 -0
  46. package/src/archetype/archetype.ts +472 -0
  47. package/src/archetype/helpers.ts +186 -0
  48. package/src/archetype/store.ts +33 -0
  49. package/src/commands/buffer.ts +110 -0
  50. package/src/commands/changeset.ts +104 -0
  51. package/src/component/entity-store.ts +223 -0
  52. package/src/component/registry.ts +657 -0
  53. package/src/component/type-utils.ts +9 -0
  54. package/src/entity/index.ts +63 -0
  55. package/src/entity/manager.ts +115 -0
  56. package/src/entity/relation.ts +319 -0
  57. package/src/entity/types.ts +135 -0
  58. package/src/index.ts +41 -0
  59. package/src/query/filter.ts +75 -0
  60. package/src/query/query.ts +313 -0
  61. package/src/query/registry.ts +101 -0
  62. package/src/storage/serialization.ts +130 -0
  63. package/src/testing/index.ts +634 -0
  64. package/src/types/index.ts +99 -0
  65. package/src/utils/bit-set.ts +133 -0
  66. package/src/utils/multi-map.ts +96 -0
  67. package/src/utils/utils.ts +19 -0
  68. package/src/world/builder.ts +100 -0
  69. package/src/world/commands.ts +378 -0
  70. package/src/world/hooks.ts +358 -0
  71. package/src/world/references.ts +38 -0
  72. package/src/world/serialization.ts +122 -0
  73. package/src/world/world.ts +1201 -0
  74. /package/{builder.d.mts → dist/builder.d.mts} +0 -0
  75. /package/{index.d.mts → dist/index.d.mts} +0 -0
  76. /package/{index.mjs → dist/index.mjs} +0 -0
  77. /package/{testing.d.mts → dist/testing.d.mts} +0 -0
  78. /package/{testing.mjs → dist/testing.mjs} +0 -0
  79. /package/{testing.mjs.map → dist/testing.mjs.map} +0 -0
  80. /package/{world.mjs → dist/world.mjs} +0 -0
  81. /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
+ });