@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,179 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { component, relation } from "../../entity";
3
+ import { World } from "../../world/world";
4
+
5
+ describe("Wildcard relation edge cases", () => {
6
+ it("should handle empty wildcard matches", () => {
7
+ const world = new World();
8
+ const Likes = component<any>();
9
+ const entity = world.new();
10
+
11
+ // Query wildcard relation but entity has no relations
12
+ const results = world.get(entity, relation(Likes, "*"));
13
+ expect(results).toEqual([]);
14
+ });
15
+
16
+ it("should get wildcard relations with data", () => {
17
+ const world = new World();
18
+ const Follows = component<{ level: number }>();
19
+ const entity = world.new();
20
+ const target1 = world.new();
21
+ const target2 = world.new();
22
+
23
+ // Add relations with data
24
+ world.set(entity, relation(Follows, target1), { level: 1 });
25
+ world.set(entity, relation(Follows, target2), { level: 2 });
26
+ world.sync();
27
+
28
+ const results = world.get(entity, relation(Follows, "*"));
29
+ expect(results).toHaveLength(2);
30
+ expect(results.map((r: any) => r[0])).toContain(target1);
31
+ expect(results.map((r: any) => r[0])).toContain(target2);
32
+ });
33
+
34
+ it("should handle large number of wildcard relations", () => {
35
+ const world = new World();
36
+ const Likes = component<{ strength: number }>();
37
+ const entity = world.new();
38
+
39
+ // Create 100 relations
40
+ const targets = [];
41
+ for (let i = 0; i < 100; i++) {
42
+ const target = world.new();
43
+ targets.push(target);
44
+ world.set(entity, relation(Likes, target), { strength: i });
45
+ }
46
+
47
+ world.sync();
48
+
49
+ // Query all wildcard relations
50
+ const results = world.get(entity, relation(Likes, "*"));
51
+ expect(results).toHaveLength(100);
52
+
53
+ // Verify structure
54
+ for (const [target, data] of results) {
55
+ expect(targets).toContain(target as any);
56
+ expect(data.strength).toBeGreaterThanOrEqual(0);
57
+ expect(data.strength).toBeLessThan(100);
58
+ }
59
+ });
60
+
61
+ it("should remove wildcard relations correctly", () => {
62
+ const world = new World();
63
+ const Owns = component<{ quantity: number }>();
64
+ const entity = world.new();
65
+ const target1 = world.new();
66
+ const target2 = world.new();
67
+
68
+ world.set(entity, relation(Owns, target1), { quantity: 5 });
69
+ world.set(entity, relation(Owns, target2), { quantity: 3 });
70
+ world.sync();
71
+
72
+ expect(world.get(entity, relation(Owns, "*"))).toHaveLength(2);
73
+
74
+ // Remove all wildcard relations
75
+ world.remove(entity, relation(Owns, "*"));
76
+ world.sync();
77
+
78
+ const results = world.get(entity, relation(Owns, "*"));
79
+ expect(results).toEqual([]);
80
+
81
+ // Verify specific relations are also gone
82
+ expect(world.has(entity, relation(Owns, target1))).toBe(false);
83
+ expect(world.has(entity, relation(Owns, target2))).toBe(false);
84
+ });
85
+
86
+ it("should update wildcard relations", () => {
87
+ const world = new World();
88
+ const Knows = component<{ years: number }>();
89
+ const entity = world.new();
90
+ const target = world.new();
91
+
92
+ world.set(entity, relation(Knows, target), { years: 1 });
93
+ world.sync();
94
+
95
+ const initialResult = world.get(entity, relation(Knows, "*"));
96
+ expect(initialResult).toHaveLength(1);
97
+ expect(initialResult[0]![1]).toEqual({ years: 1 });
98
+
99
+ // Update the relation
100
+ world.set(entity, relation(Knows, target), { years: 5 });
101
+ world.sync();
102
+
103
+ const updatedResult = world.get(entity, relation(Knows, "*"));
104
+ expect(updatedResult).toHaveLength(1);
105
+ expect(updatedResult[0]![1]).toEqual({ years: 5 });
106
+ });
107
+
108
+ it("should handle dontFragment wildcard relations", () => {
109
+ const world = new World();
110
+ const Follows = component<{ data: number }>({ dontFragment: true });
111
+ const entity1 = world.new();
112
+ const entity2 = world.new();
113
+ const target1 = world.new();
114
+ const target2 = world.new();
115
+
116
+ // Add different wildcard relations to different entities
117
+ world.set(entity1, relation(Follows, target1), { data: 1 });
118
+ world.set(entity2, relation(Follows, target2), { data: 2 });
119
+ world.sync();
120
+
121
+ // Query both wildcards
122
+ const results1 = world.get(entity1, relation(Follows, "*"));
123
+ const results2 = world.get(entity2, relation(Follows, "*"));
124
+
125
+ expect(results1).toHaveLength(1);
126
+ expect(results2).toHaveLength(1);
127
+
128
+ // Both entities should be in the same archetype (dontFragment behavior)
129
+ const archetype1 = (world as any).entityToArchetype.get(entity1);
130
+ const archetype2 = (world as any).entityToArchetype.get(entity2);
131
+ expect(archetype1).toBe(archetype2);
132
+ });
133
+
134
+ it("should handle exclusive wildcard relations", () => {
135
+ const world = new World();
136
+ const ChildOf = component<{ priority: number }>({ exclusive: true });
137
+ const entity = world.new();
138
+ const parent1 = world.new();
139
+ const parent2 = world.new();
140
+
141
+ world.set(entity, relation(ChildOf, parent1), { priority: 1 });
142
+ world.sync();
143
+
144
+ const results1 = world.get(entity, relation(ChildOf, "*"));
145
+ expect(results1).toHaveLength(1);
146
+
147
+ // Set another parent - should replace the first due to exclusive
148
+ world.set(entity, relation(ChildOf, parent2), { priority: 2 });
149
+ world.sync();
150
+
151
+ const results2 = world.get(entity, relation(ChildOf, "*"));
152
+ expect(results2).toHaveLength(1);
153
+ expect(results2[0]![0]).toBe(parent2);
154
+ });
155
+
156
+ it("should mix specific and wildcard queries", () => {
157
+ const world = new World();
158
+ const Relates = component<{ value: number }>();
159
+ const entity = world.new();
160
+ const target1 = world.new();
161
+ const target2 = world.new();
162
+
163
+ world.set(entity, relation(Relates, target1), { value: 10 });
164
+ world.set(entity, relation(Relates, target2), { value: 20 });
165
+ world.sync();
166
+
167
+ // Get specific relation
168
+ expect(world.has(entity, relation(Relates, target1))).toBe(true);
169
+ expect(world.get(entity, relation(Relates, target1))).toEqual({ value: 10 });
170
+
171
+ // Get all via wildcard
172
+ const all = world.get(entity, relation(Relates, "*"));
173
+ expect(all).toHaveLength(2);
174
+
175
+ // Get specific relation again
176
+ expect(world.has(entity, relation(Relates, target2))).toBe(true);
177
+ expect(world.get(entity, relation(Relates, target2))).toEqual({ value: 20 });
178
+ });
179
+ });
@@ -0,0 +1,237 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { component, type EntityId } from "../../entity";
3
+ import { World } from "../../world/world";
4
+
5
+ describe("Serialization edge cases", () => {
6
+ it("should serialize empty world", () => {
7
+ const world = new World();
8
+ const snapshot = world.serialize();
9
+
10
+ expect(snapshot.entities).toHaveLength(0);
11
+ expect(snapshot.version).toBeDefined();
12
+
13
+ const newWorld = new World(snapshot);
14
+ expect(newWorld.exists(-1 as unknown as ReturnType<typeof world.new>)).toBe(false);
15
+ });
16
+
17
+ it("should serialize and deserialize single entity", () => {
18
+ const world = new World();
19
+ const Position = component<{ x: number; y: number }>();
20
+ const entity = world.new();
21
+
22
+ world.set(entity, Position, { x: 10, y: 20 });
23
+ world.sync();
24
+
25
+ const snapshot = world.serialize();
26
+ const newWorld = new World(snapshot);
27
+
28
+ expect(newWorld.exists(entity)).toBe(true);
29
+ expect(newWorld.get(entity, Position)).toEqual({ x: 10, y: 20 });
30
+ });
31
+
32
+ it("should serialize world with many entities", () => {
33
+ const world = new World();
34
+ const Value = component<{ id: number }>();
35
+
36
+ const entities: EntityId[] = [];
37
+ // Create 100 entities
38
+ for (let i = 0; i < 100; i++) {
39
+ const entity = world.new();
40
+ entities.push(entity);
41
+ world.set(entity, Value, { id: i });
42
+ }
43
+ world.sync();
44
+
45
+ const snapshot = world.serialize();
46
+ const newWorld = new World(snapshot);
47
+
48
+ // Verify all entities exist
49
+ for (let i = 0; i < entities.length; i++) {
50
+ expect(newWorld.exists(entities[i]!)).toBe(true);
51
+ const data = newWorld.get(entities[i]!, Value);
52
+ expect(data.id).toBe(i);
53
+ }
54
+ });
55
+
56
+ it("should serialize multiple components per entity", () => {
57
+ const world = new World();
58
+ const Position = component<{ x: number; y: number }>();
59
+ const Velocity = component<{ dx: number; dy: number }>();
60
+ const Health = component<{ hp: number }>();
61
+
62
+ const entity = world.new();
63
+ world.set(entity, Position, { x: 1, y: 2 });
64
+ world.set(entity, Velocity, { dx: 3, dy: 4 });
65
+ world.set(entity, Health, { hp: 100 });
66
+ world.sync();
67
+
68
+ const snapshot = world.serialize();
69
+ const newWorld = new World(snapshot);
70
+
71
+ expect(newWorld.get(entity, Position)).toEqual({ x: 1, y: 2 });
72
+ expect(newWorld.get(entity, Velocity)).toEqual({ dx: 3, dy: 4 });
73
+ expect(newWorld.get(entity, Health)).toEqual({ hp: 100 });
74
+ });
75
+
76
+ it("should handle circular entity references in serialization", () => {
77
+ const world = new World();
78
+ const Ref = component<{ ref: EntityId }>();
79
+
80
+ const e1 = world.new();
81
+ const e2 = world.new();
82
+
83
+ world.set(e1, Ref, { ref: e2 });
84
+ world.set(e2, Ref, { ref: e1 });
85
+ world.sync();
86
+
87
+ const snapshot = world.serialize();
88
+ const newWorld = new World(snapshot);
89
+
90
+ expect(newWorld.exists(e1)).toBe(true);
91
+ expect(newWorld.exists(e2)).toBe(true);
92
+
93
+ // Verify circular references are preserved
94
+ const ref1 = newWorld.get(e1, Ref).ref;
95
+ const ref2 = newWorld.get(e2, Ref).ref;
96
+
97
+ // References should be maintained
98
+ expect(ref1).toBe(e2);
99
+ expect(ref2).toBe(e1);
100
+ });
101
+
102
+ it("should serialize with void components", () => {
103
+ const world = new World();
104
+ const Tag = component<void>();
105
+ const entity = world.new();
106
+
107
+ world.set(entity, Tag);
108
+ world.sync();
109
+
110
+ const snapshot = world.serialize();
111
+ const newWorld = new World(snapshot);
112
+
113
+ expect(newWorld.has(entity, Tag)).toBe(true);
114
+ });
115
+
116
+ it("should serialize with undefined component values", () => {
117
+ const world = new World();
118
+ const Optional = component<{ value: number } | undefined>();
119
+ const entity = world.new();
120
+
121
+ world.set(entity, Optional, undefined);
122
+ world.sync();
123
+
124
+ const snapshot = world.serialize();
125
+ const newWorld = new World(snapshot);
126
+
127
+ expect(newWorld.has(entity, Optional)).toBe(true);
128
+ expect(newWorld.get(entity, Optional)).toBeUndefined();
129
+ });
130
+
131
+ it("should round-trip serialization", () => {
132
+ const world1 = new World();
133
+ const Data = component<{ x: number; y: number }>();
134
+
135
+ const entities: EntityId[] = [];
136
+ for (let i = 0; i < 10; i++) {
137
+ const entity = world1.new();
138
+ entities.push(entity);
139
+ world1.set(entity, Data, { x: i, y: i * 2 });
140
+ }
141
+ world1.sync();
142
+
143
+ // First serialization
144
+ const snapshot1 = world1.serialize();
145
+ const world2 = new World(snapshot1);
146
+
147
+ // Second serialization
148
+ const snapshot2 = world2.serialize();
149
+
150
+ // Snapshots should be equivalent
151
+ expect(snapshot1.entities).toHaveLength(snapshot2.entities.length);
152
+ expect(snapshot1.version).toBe(snapshot2.version);
153
+
154
+ // Verify data is preserved
155
+ for (const entity of entities) {
156
+ expect(world2.get(entity, Data)).toBeDefined();
157
+ }
158
+ });
159
+
160
+ it("should preserve entity existence across serialization", () => {
161
+ const world = new World();
162
+ const Dummy = component<any>();
163
+
164
+ const entities: EntityId[] = [];
165
+ for (let i = 0; i < 50; i++) {
166
+ const entity = world.new();
167
+ entities.push(entity);
168
+ world.set(entity, Dummy, { num: i });
169
+ }
170
+ world.sync();
171
+
172
+ const snapshot = world.serialize();
173
+ const newWorld = new World(snapshot);
174
+
175
+ // All entities should exist in new world
176
+ for (const entity of entities) {
177
+ expect(newWorld.exists(entity)).toBe(true);
178
+ }
179
+ });
180
+
181
+ it("should handle mixed component types in serialization", () => {
182
+ const world = new World();
183
+ const String = component<string>();
184
+ const Number = component<number>();
185
+ const Boolean = component<boolean>();
186
+ const Object = component<{ nested: { value: number } }>();
187
+
188
+ const entity = world.new();
189
+ world.set(entity, String, "test");
190
+ world.set(entity, Number, 42);
191
+ world.set(entity, Boolean, true);
192
+ world.set(entity, Object, { nested: { value: 123 } });
193
+ world.sync();
194
+
195
+ const snapshot = world.serialize();
196
+ const newWorld = new World(snapshot);
197
+
198
+ expect(newWorld.get(entity, String)).toBe("test");
199
+ expect(newWorld.get(entity, Number)).toBe(42);
200
+ expect(newWorld.get(entity, Boolean)).toBe(true);
201
+ expect(newWorld.get(entity, Object)).toEqual({ nested: { value: 123 } });
202
+ });
203
+
204
+ it("should serialize entities with different archetypes", () => {
205
+ const world = new World();
206
+ const Position = component<{ x: number }>();
207
+ const Velocity = component<{ vx: number }>();
208
+
209
+ // Entity with both components
210
+ const e1 = world.new();
211
+ world.set(e1, Position, { x: 10 });
212
+ world.set(e1, Velocity, { vx: 5 });
213
+
214
+ // Entity with only Position
215
+ const e2 = world.new();
216
+ world.set(e2, Position, { x: 20 });
217
+
218
+ // Entity with only Velocity
219
+ const e3 = world.new();
220
+ world.set(e3, Velocity, { vx: 15 });
221
+
222
+ world.sync();
223
+
224
+ const snapshot = world.serialize();
225
+ const newWorld = new World(snapshot);
226
+
227
+ // Verify all entities and their components
228
+ expect(newWorld.get(e1, Position)).toEqual({ x: 10 });
229
+ expect(newWorld.get(e1, Velocity)).toEqual({ vx: 5 });
230
+
231
+ expect(newWorld.get(e2, Position)).toEqual({ x: 20 });
232
+ expect(newWorld.has(e2, Velocity)).toBe(false);
233
+
234
+ expect(newWorld.has(e3, Position)).toBe(false);
235
+ expect(newWorld.get(e3, Velocity)).toEqual({ vx: 15 });
236
+ });
237
+ });
@@ -0,0 +1,224 @@
1
+ import { beforeEach, describe, expect, it } from "bun:test";
2
+ import { component, type ComponentId } from "../../entity";
3
+ import { AssertionError, Assertions, WorldFixture } from "../../testing/index";
4
+
5
+ let PositionId: ComponentId<{ x: number; y: number }>;
6
+ let VelocityId: ComponentId<{ x: number; y: number }>;
7
+ let ParentId: ComponentId<{ offset: { x: number; y: number } }>;
8
+
9
+ describe("Assertions", () => {
10
+ beforeEach(() => {
11
+ PositionId = component<{ x: number; y: number }>();
12
+ VelocityId = component<{ x: number; y: number }>();
13
+ ParentId = component<{ offset: { x: number; y: number } }>();
14
+ });
15
+
16
+ describe("hasComponent / lacksComponent", () => {
17
+ it("should return true when entity has component", () => {
18
+ const fixture = new WorldFixture();
19
+ const entity = fixture.spawn().with(PositionId, { x: 0, y: 0 }).build();
20
+ fixture.sync();
21
+ expect(Assertions.hasComponent(fixture.world, entity, PositionId)).toBe(true);
22
+ expect(Assertions.lacksComponent(fixture.world, entity, PositionId)).toBe(false);
23
+ });
24
+
25
+ it("should return false when entity lacks component", () => {
26
+ const fixture = new WorldFixture();
27
+ const entity = fixture.spawn().with(PositionId, { x: 0, y: 0 }).build();
28
+ fixture.sync();
29
+ expect(Assertions.hasComponent(fixture.world, entity, VelocityId)).toBe(false);
30
+ expect(Assertions.lacksComponent(fixture.world, entity, VelocityId)).toBe(true);
31
+ });
32
+
33
+ it("should handle non-existent entities", () => {
34
+ const fixture = new WorldFixture();
35
+ const entity = fixture.spawn().build();
36
+ fixture.world.delete(entity);
37
+ fixture.sync();
38
+
39
+ expect(Assertions.hasComponent(fixture.world, entity, PositionId)).toBe(false);
40
+ expect(Assertions.lacksComponent(fixture.world, entity, PositionId)).toBe(true);
41
+ });
42
+ });
43
+
44
+ describe("getComponent", () => {
45
+ it("should return component value", () => {
46
+ const fixture = new WorldFixture();
47
+ const entity = fixture.spawn().with(PositionId, { x: 42, y: 84 }).build();
48
+ fixture.sync();
49
+ expect(Assertions.getComponent(fixture.world, entity, PositionId)).toEqual({ x: 42, y: 84 });
50
+ });
51
+
52
+ it("should return undefined for missing component", () => {
53
+ const fixture = new WorldFixture();
54
+ const entity = fixture.spawn().build();
55
+ fixture.sync();
56
+ expect(Assertions.getComponent(fixture.world, entity, PositionId)).toBeUndefined();
57
+ });
58
+ });
59
+
60
+ describe("entityExists", () => {
61
+ it("should return true for existing entity", () => {
62
+ const fixture = new WorldFixture();
63
+ const entity = fixture.spawn().build();
64
+ fixture.sync();
65
+ expect(Assertions.entityExists(fixture.world, entity)).toBe(true);
66
+ });
67
+
68
+ it("should return false for deleted entity", () => {
69
+ const fixture = new WorldFixture();
70
+ const entity = fixture.spawn().build();
71
+ fixture.world.delete(entity);
72
+ fixture.sync();
73
+
74
+ expect(Assertions.entityExists(fixture.world, entity)).toBe(false);
75
+ });
76
+ });
77
+
78
+ describe("query assertions", () => {
79
+ it("should check query contains entities", () => {
80
+ const fixture = new WorldFixture();
81
+ const e1 = fixture.spawn().with(PositionId, { x: 0, y: 0 }).build();
82
+ const e2 = fixture.spawn().with(PositionId, { x: 1, y: 1 }).build();
83
+ const e3 = fixture.spawn().with(VelocityId, { x: 0, y: 0 }).build();
84
+ fixture.sync();
85
+
86
+ const query = fixture.createQuery([PositionId]);
87
+
88
+ expect(Assertions.queryContains(query, e1)).toBe(true);
89
+ expect(Assertions.queryContains(query, e1, e2)).toBe(true);
90
+ expect(Assertions.queryContains(query, e3)).toBe(false);
91
+ });
92
+
93
+ it("should check query contains exactly", () => {
94
+ const fixture = new WorldFixture();
95
+ const e1 = fixture.spawn().with(PositionId, { x: 0, y: 0 }).build();
96
+ const e2 = fixture.spawn().with(PositionId, { x: 1, y: 1 }).build();
97
+ fixture.sync();
98
+
99
+ const query = fixture.createQuery([PositionId]);
100
+
101
+ expect(Assertions.queryContainsExactly(query, e1, e2)).toBe(true);
102
+ expect(Assertions.queryContainsExactly(query, e1)).toBe(false);
103
+ });
104
+
105
+ it("should count query entities", () => {
106
+ const fixture = new WorldFixture();
107
+ fixture.spawn().with(PositionId, { x: 0, y: 0 }).build();
108
+ fixture.sync();
109
+ fixture.spawn().with(PositionId, { x: 1, y: 1 }).build();
110
+ fixture.sync();
111
+
112
+ const query = fixture.createQuery([PositionId]);
113
+
114
+ expect(Assertions.queryCount(query)).toBe(2);
115
+ });
116
+ });
117
+
118
+ describe("throwing assertions", () => {
119
+ it("assertHasComponent should throw for missing component", () => {
120
+ const fixture = new WorldFixture();
121
+ const entity = fixture.spawn().build();
122
+
123
+ expect(() => Assertions.assertHasComponent(fixture.world, entity, PositionId)).toThrow(AssertionError);
124
+ });
125
+
126
+ it("assertHasComponent should not throw for existing component", () => {
127
+ const fixture = new WorldFixture();
128
+ const entity = fixture.spawn().with(PositionId, { x: 0, y: 0 }).build();
129
+ fixture.sync();
130
+ expect(() => Assertions.assertHasComponent(fixture.world, entity, PositionId)).not.toThrow();
131
+ });
132
+
133
+ it("assertLacksComponent should throw for existing component", () => {
134
+ const fixture = new WorldFixture();
135
+ const entity = fixture.spawn().with(PositionId, { x: 0, y: 0 }).build();
136
+ fixture.sync();
137
+ expect(() => Assertions.assertLacksComponent(fixture.world, entity, PositionId)).toThrow(AssertionError);
138
+ });
139
+
140
+ it("assertComponentEquals should throw for mismatched value", () => {
141
+ const fixture = new WorldFixture();
142
+ const entity = fixture.spawn().with(PositionId, { x: 1, y: 2 }).build();
143
+ fixture.sync();
144
+ expect(() => Assertions.assertComponentEquals(fixture.world, entity, PositionId, { x: 99, y: 99 })).toThrow(
145
+ AssertionError,
146
+ );
147
+ });
148
+
149
+ it("assertComponentEquals should not throw for matching value", () => {
150
+ const fixture = new WorldFixture();
151
+ const entity = fixture.spawn().with(PositionId, { x: 1, y: 2 }).build();
152
+ fixture.sync();
153
+ expect(() => Assertions.assertComponentEquals(fixture.world, entity, PositionId, { x: 1, y: 2 })).not.toThrow();
154
+ });
155
+
156
+ it("assertEntityExists should throw for non-existent entity", () => {
157
+ const fixture = new WorldFixture();
158
+ const entity = fixture.spawn().build();
159
+ fixture.world.delete(entity);
160
+ fixture.sync();
161
+
162
+ expect(() => Assertions.assertEntityExists(fixture.world, entity)).toThrow(AssertionError);
163
+ });
164
+
165
+ it("assertEntityNotExists should throw for existing entity", () => {
166
+ const fixture = new WorldFixture();
167
+ const entity = fixture.spawn().build();
168
+
169
+ expect(() => Assertions.assertEntityNotExists(fixture.world, entity)).toThrow(AssertionError);
170
+ });
171
+
172
+ it("assertQueryContains should throw when entity missing", () => {
173
+ const fixture = new WorldFixture();
174
+ const e1 = fixture.spawn().with(PositionId, { x: 0, y: 0 }).build();
175
+ const e2 = fixture.spawn().with(VelocityId, { x: 0, y: 0 }).build();
176
+ fixture.sync();
177
+
178
+ const query = fixture.createQuery([PositionId]);
179
+
180
+ expect(() => Assertions.assertQueryContains(query, e2)).toThrow(AssertionError);
181
+ expect(() => Assertions.assertQueryContains(query, e1)).not.toThrow();
182
+ });
183
+
184
+ it("assertQueryNotContains should throw when entity present", () => {
185
+ const fixture = new WorldFixture();
186
+ const e1 = fixture.spawn().with(PositionId, { x: 0, y: 0 }).build();
187
+ fixture.sync();
188
+
189
+ const query = fixture.createQuery([PositionId]);
190
+
191
+ expect(() => Assertions.assertQueryNotContains(query, e1)).toThrow(AssertionError);
192
+ });
193
+ });
194
+
195
+ describe("relation assertions", () => {
196
+ it("should check hasRelation", () => {
197
+ const fixture = new WorldFixture();
198
+ const parent = fixture.spawn().build();
199
+ const child = fixture
200
+ .spawn()
201
+ .withRelation(ParentId, parent, { offset: { x: 0, y: 0 } })
202
+ .build();
203
+ fixture.sync();
204
+
205
+ expect(Assertions.hasRelation(fixture.world, child, ParentId, parent)).toBe(true);
206
+ expect(Assertions.hasRelation(fixture.world, parent, ParentId, child)).toBe(false);
207
+ });
208
+
209
+ it("should get relations via wildcard", () => {
210
+ const fixture = new WorldFixture();
211
+ const target1 = fixture.spawn().build();
212
+ const target2 = fixture.spawn().build();
213
+ const entity = fixture
214
+ .spawn()
215
+ .withRelation(ParentId, target1, { offset: { x: 1, y: 1 } })
216
+ .withRelation(ParentId, target2, { offset: { x: 2, y: 2 } })
217
+ .build();
218
+ fixture.sync();
219
+
220
+ const relations = Assertions.getRelations(fixture.world, entity, ParentId);
221
+ expect(relations).toHaveLength(2);
222
+ });
223
+ });
224
+ });