@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.
Files changed (82) hide show
  1. package/{builder.d.mts → dist/builder.d.mts} +4 -2
  2. package/{world.mjs → dist/world.mjs} +9 -30
  3. package/dist/world.mjs.map +1 -0
  4. package/examples/advanced-scheduling.ts +96 -0
  5. package/examples/collision-detection.ts +229 -0
  6. package/examples/inventory-system-relations.ts +108 -0
  7. package/examples/parent-child-hierarchy.ts +206 -0
  8. package/examples/serialization.ts +337 -0
  9. package/examples/simple.ts +96 -0
  10. package/examples/spatial-grid.ts +276 -0
  11. package/examples/state-machine.ts +273 -0
  12. package/examples/tag-filtering.ts +266 -0
  13. package/package.json +58 -12
  14. package/src/__tests__/commands/buffer-limits.test.ts +72 -0
  15. package/src/__tests__/commands/buffer.test.ts +195 -0
  16. package/src/__tests__/component/singleton.test.ts +148 -0
  17. package/src/__tests__/core/archetype.test.ts +247 -0
  18. package/src/__tests__/core/bitset.test.ts +171 -0
  19. package/src/__tests__/core/changeset.test.ts +254 -0
  20. package/src/__tests__/core/multi-map.test.ts +74 -0
  21. package/src/__tests__/entity/component-registry.test.ts +66 -0
  22. package/src/__tests__/entity/entity.test.ts +520 -0
  23. package/src/__tests__/entity/id-manager.test.ts +157 -0
  24. package/src/__tests__/entity/id-system.test.ts +260 -0
  25. package/src/__tests__/perf/comprehensive.perf.test.ts +300 -0
  26. package/src/__tests__/perf/sync-hotpath.perf.test.ts +79 -0
  27. package/src/__tests__/query/basic.test.ts +341 -0
  28. package/src/__tests__/query/caching.test.ts +112 -0
  29. package/src/__tests__/query/filter.test.ts +111 -0
  30. package/src/__tests__/query/optional.test.ts +231 -0
  31. package/src/__tests__/query/perf.test.ts +99 -0
  32. package/src/__tests__/relations/dont-fragment/basic.test.ts +496 -0
  33. package/src/__tests__/relations/dont-fragment/query-notification.test.ts +125 -0
  34. package/src/__tests__/relations/wildcard.test.ts +179 -0
  35. package/src/__tests__/serialization/bounds.test.ts +237 -0
  36. package/src/__tests__/testing/assertions.test.ts +224 -0
  37. package/src/__tests__/testing/entity-builder.test.ts +84 -0
  38. package/src/__tests__/testing/snapshot.test.ts +150 -0
  39. package/src/__tests__/testing/world-fixture.test.ts +73 -0
  40. package/src/__tests__/world/component-hooks.test.ts +185 -0
  41. package/src/__tests__/world/component-management.test.ts +447 -0
  42. package/src/__tests__/world/entity-management.test.ts +86 -0
  43. package/src/__tests__/world/get-optional.test.ts +96 -0
  44. package/src/__tests__/world/multi-component-hooks.test.ts +502 -0
  45. package/src/__tests__/world/perf.test.ts +93 -0
  46. package/src/__tests__/world/query.test.ts +223 -0
  47. package/src/__tests__/world/serialize.test.ts +83 -0
  48. package/src/__tests__/world/wildcard-relation-hooks.test.ts +332 -0
  49. package/src/archetype/archetype.ts +472 -0
  50. package/src/archetype/helpers.ts +186 -0
  51. package/src/archetype/store.ts +33 -0
  52. package/src/commands/buffer.ts +110 -0
  53. package/src/commands/changeset.ts +104 -0
  54. package/src/component/entity-store.ts +223 -0
  55. package/src/component/registry.ts +657 -0
  56. package/src/component/type-utils.ts +9 -0
  57. package/src/entity/index.ts +63 -0
  58. package/src/entity/manager.ts +115 -0
  59. package/src/entity/relation.ts +319 -0
  60. package/src/entity/types.ts +135 -0
  61. package/src/index.ts +41 -0
  62. package/src/query/filter.ts +75 -0
  63. package/src/query/query.ts +313 -0
  64. package/src/query/registry.ts +101 -0
  65. package/src/storage/serialization.ts +130 -0
  66. package/src/testing/index.ts +634 -0
  67. package/src/types/index.ts +99 -0
  68. package/src/utils/bit-set.ts +133 -0
  69. package/src/utils/multi-map.ts +96 -0
  70. package/src/utils/utils.ts +19 -0
  71. package/src/world/builder.ts +100 -0
  72. package/src/world/commands.ts +378 -0
  73. package/src/world/hooks.ts +358 -0
  74. package/src/world/references.ts +38 -0
  75. package/src/world/serialization.ts +122 -0
  76. package/src/world/world.ts +1201 -0
  77. package/world.mjs.map +0 -1
  78. /package/{index.d.mts → dist/index.d.mts} +0 -0
  79. /package/{index.mjs → dist/index.mjs} +0 -0
  80. /package/{testing.d.mts → dist/testing.d.mts} +0 -0
  81. /package/{testing.mjs → dist/testing.mjs} +0 -0
  82. /package/{testing.mjs.map → dist/testing.mjs.map} +0 -0
@@ -0,0 +1,447 @@
1
+ import { beforeEach, describe, expect, it } from "bun:test";
2
+ import { component, createEntityId, relation, type ComponentId, type EntityId } from "../../entity";
3
+ import { World } from "../../world/world";
4
+
5
+ describe("World - Component Management", () => {
6
+ type Position = { x: number; y: number };
7
+ type Velocity = { x: number; y: number };
8
+
9
+ let positionComponent: ComponentId<Position>;
10
+ let velocityComponent: ComponentId<Velocity>;
11
+
12
+ beforeEach(() => {
13
+ positionComponent = component<Position>();
14
+ velocityComponent = component<Velocity>();
15
+ });
16
+
17
+ it("should add components to entities", () => {
18
+ const world = new World();
19
+ const entity = world.new();
20
+ const position: Position = { x: 10, y: 20 };
21
+
22
+ world.set(entity, positionComponent, position);
23
+ world.sync(); // Execute deferred commands
24
+
25
+ expect(world.has(entity, positionComponent)).toBe(true);
26
+ expect(world.get(entity, positionComponent)).toEqual(position);
27
+ });
28
+
29
+ it("should update existing components", () => {
30
+ const world = new World();
31
+ const entity = world.new();
32
+ const position1: Position = { x: 10, y: 20 };
33
+ const position2: Position = { x: 30, y: 40 };
34
+
35
+ world.set(entity, positionComponent, position1);
36
+ world.sync();
37
+ expect(world.get(entity, positionComponent)).toEqual(position1);
38
+
39
+ world.set(entity, positionComponent, position2);
40
+ world.sync();
41
+ expect(world.get(entity, positionComponent)).toEqual(position2);
42
+ });
43
+
44
+ it("should keep last value for repeated set in one sync when merge is not configured", () => {
45
+ const world = new World();
46
+ const entity = world.new();
47
+ const position1: Position = { x: 10, y: 20 };
48
+ const position2: Position = { x: 30, y: 40 };
49
+
50
+ world.set(entity, positionComponent, position1);
51
+ world.set(entity, positionComponent, position2);
52
+ world.sync();
53
+
54
+ expect(world.get(entity, positionComponent)).toEqual(position2);
55
+ });
56
+
57
+ it("should merge repeated sets in one sync for merge-enabled components", () => {
58
+ const world = new World();
59
+ const entity = world.new();
60
+ const Mailbox = component<string[]>({
61
+ merge: (prev, next) => [...prev, ...next],
62
+ });
63
+
64
+ world.set(entity, Mailbox, ["A"]);
65
+ world.set(entity, Mailbox, ["B", "C"]);
66
+ world.sync();
67
+
68
+ expect(world.get(entity, Mailbox)).toEqual(["A", "B", "C"]);
69
+ });
70
+
71
+ it("should reset merge accumulation after remove in one sync", () => {
72
+ const world = new World();
73
+ const entity = world.new();
74
+ const Mailbox = component<string[]>({
75
+ merge: (prev, next) => [...prev, ...next],
76
+ });
77
+
78
+ world.set(entity, Mailbox, ["A1"]);
79
+ world.set(entity, Mailbox, ["A2"]);
80
+ world.remove(entity, Mailbox);
81
+ world.set(entity, Mailbox, ["B1"]);
82
+ world.set(entity, Mailbox, ["B2"]);
83
+ world.sync();
84
+
85
+ expect(world.get(entity, Mailbox)).toEqual(["B1", "B2"]);
86
+ });
87
+
88
+ it("should merge relation sets by exact component type only", () => {
89
+ const world = new World();
90
+ const entity = world.new();
91
+ const target1 = world.new();
92
+ const target2 = world.new();
93
+ const MailRel = component<string[]>({
94
+ merge: (prev, next) => [...prev, ...next],
95
+ });
96
+ const rel1 = relation(MailRel, target1);
97
+ const rel2 = relation(MailRel, target2);
98
+
99
+ world.set(entity, rel1, ["T1-A"]);
100
+ world.set(entity, rel2, ["T2-A"]);
101
+ world.set(entity, rel1, ["T1-B"]);
102
+ world.set(entity, rel2, ["T2-B"]);
103
+ world.sync();
104
+
105
+ expect(world.get(entity, rel1)).toEqual(["T1-A", "T1-B"]);
106
+ expect(world.get(entity, rel2)).toEqual(["T2-A", "T2-B"]);
107
+ });
108
+
109
+ it("should apply merge for singleton(component entity) sets", () => {
110
+ const world = new World();
111
+ const Inbox = component<string[]>({
112
+ merge: (prev, next) => [...prev, ...next],
113
+ });
114
+
115
+ world.set(Inbox, ["A"]);
116
+ world.set(Inbox, ["B"]);
117
+ world.sync();
118
+ expect(world.get(Inbox)).toEqual(["A", "B"]);
119
+
120
+ world.remove(Inbox);
121
+ world.set(Inbox, ["C"]);
122
+ world.set(Inbox, ["D"]);
123
+ world.sync();
124
+ expect(world.get(Inbox)).toEqual(["C", "D"]);
125
+ });
126
+
127
+ it("should remove components from entities", () => {
128
+ const world = new World();
129
+ const entity = world.new();
130
+ const position: Position = { x: 10, y: 20 };
131
+
132
+ world.set(entity, positionComponent, position);
133
+ world.sync();
134
+ expect(world.has(entity, positionComponent)).toBe(true);
135
+
136
+ world.remove(entity, positionComponent);
137
+ world.sync();
138
+ expect(world.has(entity, positionComponent)).toBe(false);
139
+ expect(() => world.get(entity, positionComponent)).toThrow(
140
+ /^Entity \d+ does not have component \d+\. Use has\(\) to check component existence before calling get\(\)\.$/,
141
+ );
142
+ });
143
+
144
+ it("should throw error when removing invalid component type", () => {
145
+ const world = new World();
146
+ const entity = world.new();
147
+ const invalidComponentType = 0 as EntityId<any>; // Invalid component ID
148
+
149
+ expect(() => world.remove(entity, invalidComponentType)).toThrow("Invalid component type: 0");
150
+ });
151
+
152
+ it("should allow removing wildcard relation components", () => {
153
+ const world = new World();
154
+ const entity = world.new();
155
+ const position: Position = { x: 10, y: 20 };
156
+ const targetEntity1 = world.new();
157
+ const targetEntity2 = world.new();
158
+ const relationId1 = relation(positionComponent, targetEntity1);
159
+ const relationId2 = relation(positionComponent, targetEntity2);
160
+
161
+ // Add multiple relation components with the same base component
162
+ world.set(entity, relationId1, position);
163
+ world.set(entity, relationId2, { x: 20, y: 30 });
164
+ world.sync();
165
+ expect(world.has(entity, relationId1)).toBe(true);
166
+ expect(world.has(entity, relationId2)).toBe(true);
167
+
168
+ // Remove using wildcard relation should remove all matching components
169
+ const wildcardRelation = relation(positionComponent, "*");
170
+ world.remove(entity, wildcardRelation);
171
+ world.sync();
172
+ expect(world.has(entity, relationId1)).toBe(false);
173
+ expect(world.has(entity, relationId2)).toBe(false);
174
+ });
175
+
176
+ it("should get wildcard relation components", () => {
177
+ const world = new World();
178
+ const entity = world.new();
179
+ const position: Position = { x: 10, y: 20 };
180
+ const targetEntity1 = world.new();
181
+ const targetEntity2 = world.new();
182
+ const relationId1 = relation(positionComponent, targetEntity1);
183
+ const relationId2 = relation(positionComponent, targetEntity2);
184
+
185
+ // Add multiple relation components with the same base component
186
+ world.set(entity, relationId1, position);
187
+ world.set(entity, relationId2, { x: 20, y: 30 });
188
+ world.sync();
189
+
190
+ // Get wildcard relations
191
+ const wildcardRelation = relation(positionComponent, "*");
192
+ const relations = world.get(entity, wildcardRelation);
193
+ expect(relations).toEqual([
194
+ [targetEntity2, { x: 20, y: 30 }],
195
+ [targetEntity1, { x: 10, y: 20 }],
196
+ ]);
197
+
198
+ // Test with entity not having components
199
+ const otherEntity = world.new();
200
+ const result = world.get(otherEntity, wildcardRelation);
201
+ expect(result).toEqual([]);
202
+ });
203
+
204
+ it("should handle exclusive relations", () => {
205
+ const world = new World();
206
+ const entity = world.new();
207
+ const parent1 = world.new();
208
+ const parent2 = world.new();
209
+
210
+ // Create ChildOf component with exclusive option
211
+ const ChildOf = component({ exclusive: true });
212
+
213
+ const childOfParent1 = relation(ChildOf, parent1);
214
+ const childOfParent2 = relation(ChildOf, parent2);
215
+
216
+ // Add first relation
217
+ world.set(entity, childOfParent1);
218
+ world.sync();
219
+ expect(world.has(entity, childOfParent1)).toBe(true);
220
+ expect(world.has(entity, childOfParent2)).toBe(false);
221
+
222
+ // Add second relation - should replace the first
223
+ world.set(entity, childOfParent2);
224
+ world.sync();
225
+ expect(world.has(entity, childOfParent1)).toBe(false);
226
+ expect(world.has(entity, childOfParent2)).toBe(true);
227
+ });
228
+
229
+ it("should cascade delete referencing entities when cascade enabled", () => {
230
+ const world = new World();
231
+ const parent = world.new();
232
+ const child = world.new();
233
+ // Create ChildOf component with cascadeDelete option
234
+ const ChildOf = component({ cascadeDelete: true });
235
+
236
+ const childOfParent = relation(ChildOf, parent);
237
+ world.set(child, childOfParent);
238
+ world.sync();
239
+
240
+ world.delete(parent);
241
+ world.sync();
242
+
243
+ expect(world.exists(parent)).toBe(false);
244
+ expect(world.exists(child)).toBe(false);
245
+ });
246
+
247
+ it("should not cascade delete referencing entities when cascade disabled", () => {
248
+ const world = new World();
249
+ const parent = world.new();
250
+ const child = world.new();
251
+ const ChildOf = component();
252
+
253
+ const childOfParent = relation(ChildOf, parent);
254
+ world.set(child, childOfParent);
255
+ world.sync();
256
+
257
+ world.delete(parent);
258
+ world.sync();
259
+
260
+ expect(world.exists(parent)).toBe(false);
261
+ // child should still exist but without the relation
262
+ expect(world.exists(child)).toBe(true);
263
+ expect(world.has(child, childOfParent)).toBe(false);
264
+ });
265
+
266
+ it("should cascade delete transitively", () => {
267
+ const world = new World();
268
+ const a = world.new();
269
+ const b = world.new();
270
+ const c = world.new();
271
+ // Create ChildOf component with cascadeDelete option
272
+ const ChildOf = component({ cascadeDelete: true });
273
+
274
+ world.set(b, relation(ChildOf, a));
275
+ world.set(c, relation(ChildOf, b));
276
+ world.sync();
277
+
278
+ world.delete(a);
279
+ world.sync();
280
+
281
+ expect(world.exists(a)).toBe(false);
282
+ expect(world.exists(b)).toBe(false);
283
+ expect(world.exists(c)).toBe(false);
284
+ });
285
+
286
+ it("should handle cyclic cascade without infinite loop", () => {
287
+ const world = new World();
288
+ const a = world.new();
289
+ const b = world.new();
290
+ // Create ChildOf component with cascadeDelete option
291
+ const ChildOf = component({ cascadeDelete: true });
292
+
293
+ world.set(a, relation(ChildOf, b));
294
+ world.set(b, relation(ChildOf, a));
295
+ world.sync();
296
+
297
+ world.delete(a);
298
+ world.sync();
299
+
300
+ expect(world.exists(a)).toBe(false);
301
+ expect(world.exists(b)).toBe(false);
302
+ });
303
+
304
+ it("should prevent archetype fragmentation with dontFragment relations", () => {
305
+ const world = new World();
306
+ const entity1 = world.new();
307
+ const entity2 = world.new();
308
+ const target1 = world.new();
309
+ const target2 = world.new();
310
+
311
+ // Create Follows component with dontFragment option
312
+ const Follows = component<{ strength: number }>({ dontFragment: true });
313
+
314
+ const followsTarget1 = relation(Follows, target1);
315
+ const followsTarget2 = relation(Follows, target2);
316
+
317
+ // Add different relations to different entities
318
+ world.set(entity1, followsTarget1, { strength: 1 });
319
+ world.set(entity2, followsTarget2, { strength: 2 });
320
+ world.sync();
321
+
322
+ // Both entities should exist and have their relations
323
+ expect(world.has(entity1, followsTarget1)).toBe(true);
324
+ expect(world.has(entity2, followsTarget2)).toBe(true);
325
+
326
+ // They should be in the same archetype despite having different relation targets
327
+ // (this is the key behavior of dontFragment)
328
+ const archetype1 = (world as any).entityToArchetype.get(entity1);
329
+ const archetype2 = (world as any).entityToArchetype.get(entity2);
330
+ expect(archetype1).toBe(archetype2);
331
+
332
+ // Verify the wildcard marker is present
333
+ const wildcardMarker = relation(Follows, "*");
334
+ expect(archetype1.componentTypes).toContain(wildcardMarker);
335
+ });
336
+
337
+ it("should support cascadeDelete and dontFragment simultaneously", () => {
338
+ const world = new World();
339
+ const parent = world.new();
340
+ const child1 = world.new();
341
+ const child2 = world.new();
342
+
343
+ // Create ChildOf component with both cascadeDelete and dontFragment options
344
+ const ChildOf = component<{ priority: number }>({ cascadeDelete: true, dontFragment: true });
345
+
346
+ const childOfParent1 = relation(ChildOf, parent);
347
+ const childOfParent2 = relation(ChildOf, parent);
348
+
349
+ // Add relations to children
350
+ world.set(child1, childOfParent1, { priority: 1 });
351
+ world.set(child2, childOfParent2, { priority: 2 });
352
+ world.sync();
353
+
354
+ // Verify relations exist
355
+ expect(world.has(child1, childOfParent1)).toBe(true);
356
+ expect(world.has(child2, childOfParent2)).toBe(true);
357
+
358
+ // Both children should be in the same archetype (dontFragment behavior)
359
+ const archetype1 = (world as any).entityToArchetype.get(child1);
360
+ const archetype2 = (world as any).entityToArchetype.get(child2);
361
+ expect(archetype1).toBe(archetype2);
362
+
363
+ // Delete parent - should cascade delete both children (cascadeDelete behavior)
364
+ world.delete(parent);
365
+ world.sync();
366
+
367
+ expect(world.exists(parent)).toBe(false);
368
+ expect(world.exists(child1)).toBe(false);
369
+ expect(world.exists(child2)).toBe(false);
370
+ });
371
+
372
+ it("should handle multiple components", () => {
373
+ const world = new World();
374
+ const entity = world.new();
375
+ const position: Position = { x: 10, y: 20 };
376
+ const velocity: Velocity = { x: 1, y: 2 };
377
+
378
+ world.set(entity, positionComponent, position);
379
+ world.set(entity, velocityComponent, velocity);
380
+ world.sync();
381
+
382
+ expect(world.has(entity, positionComponent)).toBe(true);
383
+ expect(world.has(entity, velocityComponent)).toBe(true);
384
+ expect(world.get(entity, positionComponent)).toEqual(position);
385
+ expect(world.get(entity, velocityComponent)).toEqual(velocity);
386
+ });
387
+
388
+ it("should throw error when adding component to non-existent entity", () => {
389
+ const world = new World();
390
+ const fakeEntity = createEntityId(9999);
391
+ const position: Position = { x: 10, y: 20 };
392
+
393
+ expect(() => world.set(fakeEntity, positionComponent, position)).toThrow("Entity 9999 does not exist");
394
+ });
395
+
396
+ it("should throw error when adding invalid component type", () => {
397
+ const world = new World();
398
+ const entity = world.new();
399
+ const position: Position = { x: 10, y: 20 };
400
+ const invalidComponentType = 0 as EntityId<any>; // Invalid component ID
401
+
402
+ expect(() => world.set(entity, invalidComponentType, position)).toThrow("Invalid component type: 0");
403
+ });
404
+
405
+ it("should throw error when adding wildcard relation component", () => {
406
+ const world = new World();
407
+ const entity = world.new();
408
+ const position: Position = { x: 10, y: 20 };
409
+ const wildcardRelation = relation(positionComponent, "*");
410
+
411
+ expect(() => world.set(entity, wildcardRelation, position)).toThrow(
412
+ "Cannot directly add wildcard relation components",
413
+ );
414
+ });
415
+
416
+ it("should throw error when getting component from non-existent entity", () => {
417
+ const world = new World();
418
+ const fakeEntity = createEntityId(9999);
419
+
420
+ expect(() => world.get(fakeEntity, positionComponent)).toThrow("Entity 9999 does not exist");
421
+ });
422
+
423
+ it("should allow setting undefined as component data", () => {
424
+ const world = new World();
425
+ const entity = world.new();
426
+
427
+ const optionalPositionComponent = component<Position | undefined>();
428
+
429
+ // Add component with undefined data
430
+ world.set(entity, optionalPositionComponent, undefined);
431
+ world.sync();
432
+
433
+ expect(world.has(entity, optionalPositionComponent)).toBe(true);
434
+ expect(world.get(entity, optionalPositionComponent)).toBeUndefined();
435
+
436
+ // Update to a defined value
437
+ const position: Position = { x: 10, y: 20 };
438
+ world.set(entity, optionalPositionComponent, position);
439
+ world.sync();
440
+ expect(world.get(entity, optionalPositionComponent)).toEqual(position);
441
+
442
+ // Update back to undefined
443
+ world.set(entity, optionalPositionComponent, undefined);
444
+ world.sync();
445
+ expect(world.get(entity, optionalPositionComponent)).toBeUndefined();
446
+ });
447
+ });
@@ -0,0 +1,86 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { component, createEntityId, relation } from "../../entity";
3
+ import { World } from "../../world/world";
4
+
5
+ describe("World - Entity Management", () => {
6
+ it("should create entities", () => {
7
+ const world = new World();
8
+ const entity1 = world.new();
9
+ const entity2 = world.new();
10
+
11
+ expect(world.exists(entity1)).toBe(true);
12
+ expect(world.exists(entity2)).toBe(true);
13
+ expect(entity1).not.toBe(entity2);
14
+ });
15
+
16
+ it("should destroy entities", () => {
17
+ const world = new World();
18
+ const entity = world.new();
19
+ expect(world.exists(entity)).toBe(true);
20
+
21
+ world.delete(entity);
22
+ world.sync();
23
+ expect(world.exists(entity)).toBe(false);
24
+ });
25
+
26
+ it("should handle destroying non-existent entities gracefully", () => {
27
+ const world = new World();
28
+ const fakeEntity = createEntityId(9999);
29
+ expect(world.exists(fakeEntity)).toBe(false);
30
+ // Should not throw
31
+ world.delete(fakeEntity);
32
+ });
33
+
34
+ it("should support component id as entity id with fast path storage", () => {
35
+ const world = new World();
36
+ const Meta = component<{ tag: string }>("Meta");
37
+ const Payload = component<{ value: number }>("Payload");
38
+
39
+ expect(world.exists(Meta)).toBe(true);
40
+
41
+ world.set(Meta, Payload, { value: 42 });
42
+ world.sync();
43
+
44
+ expect(world.has(Meta, Payload)).toBe(true);
45
+ expect(world.get(Meta, Payload)).toEqual({ value: 42 });
46
+
47
+ const query = world.createQuery([Payload]);
48
+ expect(query.getEntities()).toEqual([]);
49
+
50
+ let hookCalls = 0;
51
+ world.hook([Payload], {
52
+ on_init: () => {
53
+ hookCalls++;
54
+ },
55
+ });
56
+
57
+ world.set(Meta, Payload, { value: 43 });
58
+ world.sync();
59
+ expect(hookCalls).toBe(0);
60
+
61
+ world.delete(Meta);
62
+ world.sync();
63
+ expect(world.has(Meta, Payload)).toBe(false);
64
+ expect(world.getOptional(Meta, Payload)).toBeUndefined();
65
+ expect(world.exists(Meta)).toBe(true);
66
+ });
67
+
68
+ it("should clear relation-entity data when target entity is deleted", () => {
69
+ const world = new World();
70
+ const Link = component("Link");
71
+ const Payload = component<{ value: number }>("Payload2");
72
+
73
+ const target = world.new();
74
+ const relationEntity = relation(Link, target);
75
+
76
+ world.set(relationEntity, Payload, { value: 9 });
77
+ world.sync();
78
+ expect(world.has(relationEntity, Payload)).toBe(true);
79
+
80
+ world.delete(target);
81
+ world.sync();
82
+ expect(world.has(relationEntity, Payload)).toBe(false);
83
+ expect(world.getOptional(relationEntity, Payload)).toBeUndefined();
84
+ expect(world.exists(relationEntity)).toBe(true);
85
+ });
86
+ });
@@ -0,0 +1,96 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import type { EntityId } from "../../entity";
3
+ import { component, relation } from "../../entity";
4
+ import { World } from "../../world/world";
5
+
6
+ function expectType<T>(_value: T): void {}
7
+
8
+ describe("World.getOptional", () => {
9
+ it("should return { value: T } when component exists", () => {
10
+ const world = new World();
11
+ const PositionId = component<{ x: number; y: number }>();
12
+ const entity = world.new();
13
+ world.set(entity, PositionId, { x: 10, y: 20 });
14
+ world.sync();
15
+
16
+ const result = world.getOptional(entity, PositionId);
17
+ expect(result).toEqual({ value: { x: 10, y: 20 } });
18
+ });
19
+
20
+ it("should return undefined when component does not exist", () => {
21
+ const world = new World();
22
+ const PositionId = component<{ x: number; y: number }>();
23
+ const VelocityId = component<{ x: number; y: number }>();
24
+ const entity = world.new();
25
+ world.set(entity, PositionId, { x: 10, y: 20 });
26
+ world.sync();
27
+
28
+ const result = world.getOptional(entity, VelocityId);
29
+ expect(result).toBeUndefined();
30
+ });
31
+
32
+ it("should distinguish between component value being undefined and component not existing", () => {
33
+ const world = new World();
34
+ const UndefinedComponent = component<undefined>();
35
+ const entity = world.new();
36
+ world.set(entity, UndefinedComponent, undefined);
37
+ world.sync();
38
+
39
+ // Exists with undefined value
40
+ expect(world.getOptional(entity, UndefinedComponent)).toEqual({ value: undefined });
41
+
42
+ // Not existing
43
+ const Other = component<number>();
44
+ expect(world.getOptional(entity, Other)).toBeUndefined();
45
+ });
46
+
47
+ it("should throw error when entity does not exist", () => {
48
+ const world = new World();
49
+ const PositionId = component<{ x: number; y: number }>();
50
+ const entity = 1234 as any; // non-existent entity
51
+
52
+ expect(() => world.getOptional(entity, PositionId)).toThrow("Entity 1234 does not exist");
53
+ });
54
+
55
+ it("should return matching relations for wildcard relations", () => {
56
+ const world = new World();
57
+ const Rel = component<number>();
58
+ const target = world.new();
59
+ const entity = world.new();
60
+ world.set(entity, relation(Rel, target), 100);
61
+ world.sync();
62
+
63
+ const wildcard = relation(Rel, "*");
64
+ const result = world.getOptional(entity, wildcard);
65
+ expectType<{ value: [EntityId<unknown>, number][] } | undefined>(result);
66
+ expect(result).toEqual({ value: [[target, 100]] });
67
+ });
68
+
69
+ it("should return undefined for wildcard relations with no matching relations", () => {
70
+ const world = new World();
71
+ const Rel = component<number>();
72
+ const entity = world.new();
73
+ world.sync();
74
+
75
+ const wildcard = relation(Rel, "*");
76
+ const result = world.getOptional(entity, wildcard);
77
+ expectType<{ value: [number, number][] } | undefined>(result);
78
+ expect(result).toBeUndefined();
79
+ });
80
+
81
+ it("should work with dontFragment relations", () => {
82
+ const world = new World();
83
+ const DFRel = component<number>({ dontFragment: true });
84
+ const target = world.new();
85
+ const entity = world.new();
86
+ world.set(entity, relation(DFRel, target), 42);
87
+ world.sync();
88
+
89
+ const relId = relation(DFRel, target);
90
+ expect(world.getOptional(entity, relId)).toEqual({ value: 42 });
91
+
92
+ const otherTarget = world.new();
93
+ const otherRelId = relation(DFRel, otherTarget);
94
+ expect(world.getOptional(entity, otherRelId)).toBeUndefined();
95
+ });
96
+ });