@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.
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 +58 -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,195 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { CommandBuffer, type Command } from "../../commands/buffer";
3
+ import type { EntityId } from "../../entity";
4
+
5
+ describe("CommandBuffer", () => {
6
+ it("should buffer commands and execute them", () => {
7
+ const executedCommands: { entityId: EntityId; commands: Command[] }[] = [];
8
+
9
+ const mockExecutor = (entityId: EntityId, commands: Command[]) => {
10
+ executedCommands.push({ entityId, commands });
11
+ };
12
+
13
+ const buffer = new CommandBuffer(mockExecutor);
14
+
15
+ // Create mock entity IDs
16
+ const entity1 = 1 as EntityId;
17
+ const entity2 = 2 as EntityId;
18
+ const componentType1 = 100 as EntityId<any>;
19
+ const componentType2 = 200 as EntityId<any>;
20
+
21
+ // Add commands
22
+ buffer.set(entity1, componentType1, { x: 1 });
23
+ buffer.set(entity1, componentType2, { y: 2 });
24
+ buffer.remove(entity2, componentType1);
25
+
26
+ // Execute
27
+ buffer.execute();
28
+
29
+ // Verify execution
30
+ expect(executedCommands).toHaveLength(2);
31
+
32
+ // Check entity1 commands
33
+ const entity1Execution = executedCommands.find((e) => e.entityId === entity1);
34
+ expect(entity1Execution).toBeDefined();
35
+ if (entity1Execution) {
36
+ expect(entity1Execution.commands).toHaveLength(2);
37
+ expect(entity1Execution.commands[0]!.type).toBe("set");
38
+ expect(entity1Execution.commands[1]!.type).toBe("set");
39
+ }
40
+
41
+ // Check entity2 commands
42
+ const entity2Execution = executedCommands.find((e) => e.entityId === entity2);
43
+ expect(entity2Execution).toBeDefined();
44
+ if (entity2Execution) {
45
+ expect(entity2Execution.commands).toHaveLength(1);
46
+ expect(entity2Execution.commands[0]!.type).toBe("delete");
47
+ }
48
+
49
+ // Verify buffer is cleared
50
+ expect(buffer.getCommands()).toHaveLength(0);
51
+ });
52
+
53
+ it("should handle destroy commands", () => {
54
+ const executedCommands: { entityId: EntityId; commands: Command[] }[] = [];
55
+
56
+ const mockExecutor = (entityId: EntityId, commands: Command[]) => {
57
+ executedCommands.push({ entityId, commands });
58
+ };
59
+
60
+ const buffer = new CommandBuffer(mockExecutor);
61
+
62
+ const entity = 1 as EntityId;
63
+ const componentType = 100 as EntityId<any>;
64
+
65
+ // Add commands including destroy
66
+ buffer.set(entity, componentType, { x: 1 });
67
+ buffer.delete(entity);
68
+
69
+ buffer.execute();
70
+
71
+ // Should still execute (destroy logic is handled in the executor)
72
+ expect(executedCommands).toHaveLength(1);
73
+ const execution = executedCommands[0]!;
74
+ expect(execution.entityId).toBe(entity);
75
+ expect(execution.commands).toHaveLength(2);
76
+ });
77
+
78
+ it("should clear commands after execution", () => {
79
+ const mockExecutor = () => {};
80
+ const buffer = new CommandBuffer(mockExecutor);
81
+
82
+ const entity = 1 as EntityId;
83
+ const componentType = 100 as EntityId<any>;
84
+
85
+ buffer.set(entity, componentType, { x: 1 });
86
+ expect(buffer.getCommands()).toHaveLength(1);
87
+
88
+ buffer.execute();
89
+ expect(buffer.getCommands()).toHaveLength(0);
90
+ });
91
+
92
+ it("should allow manual clearing", () => {
93
+ const mockExecutor = () => {};
94
+ const buffer = new CommandBuffer(mockExecutor);
95
+
96
+ const entity = 1 as EntityId;
97
+ const componentType = 100 as EntityId<any>;
98
+
99
+ buffer.set(entity, componentType, { x: 1 });
100
+ expect(buffer.getCommands()).toHaveLength(1);
101
+
102
+ buffer.clear();
103
+ expect(buffer.getCommands()).toHaveLength(0);
104
+ });
105
+
106
+ it("should execute commands added during execution until queue is empty", () => {
107
+ const executedCommands: { entityId: EntityId; commands: Command[] }[] = [];
108
+
109
+ let bufferRef: CommandBuffer;
110
+ const mockExecutor = (entityId: EntityId, commands: Command[]) => {
111
+ executedCommands.push({ entityId, commands });
112
+
113
+ // If this is the first execution, add more commands
114
+ if (executedCommands.length === 1) {
115
+ const newEntity = 3 as EntityId;
116
+ const newComponentType = 300 as EntityId<any>;
117
+ bufferRef.set(newEntity, newComponentType, { z: 3 });
118
+ }
119
+ };
120
+
121
+ const buffer = new CommandBuffer(mockExecutor);
122
+ bufferRef = buffer;
123
+
124
+ const entity1 = 1 as EntityId;
125
+ const entity2 = 2 as EntityId;
126
+ const componentType1 = 100 as EntityId<any>;
127
+ const componentType2 = 200 as EntityId<any>;
128
+
129
+ // Add initial commands
130
+ buffer.set(entity1, componentType1, { x: 1 });
131
+ buffer.set(entity2, componentType2, { y: 2 });
132
+
133
+ // Execute
134
+ buffer.execute();
135
+
136
+ // Should have executed three times: entity1, entity2, and the new entity3
137
+ expect(executedCommands).toHaveLength(3);
138
+
139
+ // First execution: entity1
140
+ const entity1Execution = executedCommands.find((e) => e.entityId === entity1);
141
+ expect(entity1Execution).toBeDefined();
142
+ if (entity1Execution) {
143
+ expect(entity1Execution.commands).toHaveLength(1);
144
+ expect(entity1Execution.commands[0]!.type).toBe("set");
145
+ }
146
+
147
+ // Second execution: entity2
148
+ const entity2Execution = executedCommands.find((e) => e.entityId === entity2);
149
+ expect(entity2Execution).toBeDefined();
150
+ if (entity2Execution) {
151
+ expect(entity2Execution.commands).toHaveLength(1);
152
+ expect(entity2Execution.commands[0]!.type).toBe("set");
153
+ }
154
+
155
+ // Third execution: new entity
156
+ const entity3Execution = executedCommands.find((e) => e.entityId === (3 as EntityId));
157
+ expect(entity3Execution).toBeDefined();
158
+ if (entity3Execution) {
159
+ expect(entity3Execution.commands).toHaveLength(1);
160
+ expect(entity3Execution.commands[0]!.type).toBe("set");
161
+ }
162
+
163
+ // Buffer should be empty
164
+ expect(buffer.getCommands()).toHaveLength(0);
165
+ });
166
+
167
+ it("should throw error on infinite loop detection", () => {
168
+ const executedCommands: { entityId: EntityId; commands: Command[] }[] = [];
169
+
170
+ let bufferRef: CommandBuffer;
171
+ const mockExecutor = (entityId: EntityId, commands: Command[]) => {
172
+ executedCommands.push({ entityId, commands });
173
+
174
+ // Always add more commands to create infinite loop
175
+ const newEntity = (entityId + 1) as EntityId;
176
+ const newComponentType = 100 as EntityId<any>;
177
+ bufferRef.set(newEntity, newComponentType, { value: entityId });
178
+ };
179
+
180
+ const buffer = new CommandBuffer(mockExecutor);
181
+ bufferRef = buffer;
182
+
183
+ const entity = 1 as EntityId;
184
+ const componentType = 100 as EntityId<any>;
185
+
186
+ // Add initial command
187
+ buffer.set(entity, componentType, { x: 1 });
188
+
189
+ // Execute should throw error due to infinite loop
190
+ expect(() => buffer.execute()).toThrow("Command execution exceeded maximum iterations, possible infinite loop");
191
+
192
+ // Should have executed many times (up to MAX_ITERATIONS)
193
+ expect(executedCommands.length).toBeGreaterThan(0);
194
+ });
195
+ });
@@ -0,0 +1,148 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { component } from "../../entity";
3
+ import { World } from "../../world/world";
4
+
5
+ describe("World - Singleton Component", () => {
6
+ type GlobalConfig = { debug: boolean; version: string };
7
+ type GameState = { score: number; level: number };
8
+
9
+ const GlobalConfigId = component<GlobalConfig>();
10
+ const GameStateId = component<GameState>();
11
+
12
+ it("should set singleton component using shorthand syntax", () => {
13
+ const world = new World();
14
+ const config: GlobalConfig = { debug: true, version: "1.0.0" };
15
+
16
+ // Use singleton syntax: set(componentId, data)
17
+ world.set(GlobalConfigId, config);
18
+ world.sync();
19
+
20
+ // Verify it was set on the component entity itself
21
+ expect(world.has(GlobalConfigId)).toBe(true);
22
+ expect(world.get(GlobalConfigId)).toEqual(config);
23
+ });
24
+
25
+ it("should update singleton component using shorthand syntax", () => {
26
+ const world = new World();
27
+ const config1: GlobalConfig = { debug: true, version: "1.0.0" };
28
+ const config2: GlobalConfig = { debug: false, version: "2.0.0" };
29
+
30
+ world.set(GlobalConfigId, config1);
31
+ world.sync();
32
+ expect(world.get(GlobalConfigId)).toEqual(config1);
33
+
34
+ world.set(GlobalConfigId, config2);
35
+ world.sync();
36
+ expect(world.get(GlobalConfigId)).toEqual(config2);
37
+ });
38
+
39
+ it("should be equivalent to set(comp, comp, data)", () => {
40
+ const world1 = new World();
41
+ const world2 = new World();
42
+ const config: GlobalConfig = { debug: true, version: "1.0.0" };
43
+
44
+ // Singleton syntax
45
+ world1.set(GlobalConfigId, config);
46
+ world1.sync();
47
+
48
+ // Traditional syntax
49
+ world2.set(GlobalConfigId, GlobalConfigId, config);
50
+ world2.sync();
51
+
52
+ // Both should have the same result
53
+ expect(world1.get(GlobalConfigId)).toEqual(world2.get(GlobalConfigId));
54
+ });
55
+
56
+ it("should work with multiple singleton components", () => {
57
+ const world = new World();
58
+ const config: GlobalConfig = { debug: true, version: "1.0.0" };
59
+ const state: GameState = { score: 100, level: 5 };
60
+
61
+ world.set(GlobalConfigId, config);
62
+ world.set(GameStateId, state);
63
+ world.sync();
64
+
65
+ expect(world.get(GlobalConfigId)).toEqual(config);
66
+ expect(world.get(GameStateId)).toEqual(state);
67
+ });
68
+
69
+ it("should throw error if component entity does not exist", () => {
70
+ const world = new World();
71
+ const config: GlobalConfig = { debug: true, version: "1.0.0" };
72
+
73
+ // Try to set a component on an entity that doesn't exist
74
+ const nonExistentEntity = 99999 as any; // Use a fake entity ID
75
+ expect(() => {
76
+ world.set(nonExistentEntity, GlobalConfigId, config);
77
+ }).toThrow("does not exist");
78
+ });
79
+
80
+ it("should check singleton component existence using shorthand syntax", () => {
81
+ const world = new World();
82
+ const config: GlobalConfig = { debug: true, version: "1.0.0" };
83
+
84
+ // Before setting, should return false
85
+ expect(world.has(GlobalConfigId)).toBe(false);
86
+
87
+ // Set singleton component
88
+ world.set(GlobalConfigId, config);
89
+ world.sync();
90
+
91
+ // After setting, should return true
92
+ expect(world.has(GlobalConfigId)).toBe(true);
93
+ });
94
+
95
+ it("should be equivalent to has(comp, comp)", () => {
96
+ const world1 = new World();
97
+ const world2 = new World();
98
+ const config: GlobalConfig = { debug: true, version: "1.0.0" };
99
+
100
+ // Singleton syntax
101
+ world1.set(GlobalConfigId, config);
102
+ world1.sync();
103
+
104
+ // Traditional syntax
105
+ world2.set(GlobalConfigId, GlobalConfigId, config);
106
+ world2.sync();
107
+
108
+ // Both should have the same result
109
+ expect(world1.has(GlobalConfigId)).toBe(world2.has(GlobalConfigId, GlobalConfigId));
110
+ expect(world1.has(GlobalConfigId)).toBe(true);
111
+ });
112
+
113
+ it("should remove singleton component using shorthand syntax", () => {
114
+ const world = new World();
115
+ const config: GlobalConfig = { debug: true, version: "1.0.0" };
116
+
117
+ world.set(GlobalConfigId, config);
118
+ world.sync();
119
+ expect(world.has(GlobalConfigId)).toBe(true);
120
+
121
+ // Remove using singleton syntax
122
+ world.remove(GlobalConfigId);
123
+ world.sync();
124
+ expect(world.has(GlobalConfigId)).toBe(false);
125
+ });
126
+
127
+ it("should be equivalent to remove(comp, comp)", () => {
128
+ const world1 = new World();
129
+ const world2 = new World();
130
+ const config: GlobalConfig = { debug: true, version: "1.0.0" };
131
+
132
+ // Set on both worlds
133
+ world1.set(GlobalConfigId, config);
134
+ world1.sync();
135
+ world2.set(GlobalConfigId, GlobalConfigId, config);
136
+ world2.sync();
137
+
138
+ // Remove using different syntax
139
+ world1.remove(GlobalConfigId); // Singleton syntax
140
+ world2.remove(GlobalConfigId, GlobalConfigId); // Traditional syntax
141
+ world1.sync();
142
+ world2.sync();
143
+
144
+ // Both should have the same result
145
+ expect(world1.has(GlobalConfigId)).toBe(world2.has(GlobalConfigId, GlobalConfigId));
146
+ expect(world1.has(GlobalConfigId)).toBe(false);
147
+ });
148
+ });
@@ -0,0 +1,247 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { Archetype } from "../../archetype/archetype";
3
+ import { component, createEntityId, relation, type EntityId } from "../../entity";
4
+
5
+ describe("Archetype", () => {
6
+ type Position = { x: number; y: number };
7
+ type Velocity = { x: number; y: number };
8
+
9
+ const positionComponent = component<Position>();
10
+ const velocityComponent = component<Velocity>();
11
+
12
+ // Helper function to create a dontFragmentRelations map for testing
13
+ const createDontFragmentRelations = () => new Map<EntityId, Map<EntityId<any>, any>>();
14
+
15
+ it("should create an archetype with component types", () => {
16
+ const archetype = new Archetype([positionComponent, velocityComponent], createDontFragmentRelations());
17
+ expect(archetype.componentTypes).toEqual([positionComponent, velocityComponent]);
18
+ expect(archetype.size).toBe(0);
19
+ });
20
+
21
+ it("should match component types", () => {
22
+ const archetype = new Archetype([positionComponent, velocityComponent], createDontFragmentRelations());
23
+ expect(archetype.matches([positionComponent, velocityComponent])).toBe(true);
24
+ expect(archetype.matches([velocityComponent, positionComponent])).toBe(true); // Order doesn't matter
25
+ expect(archetype.matches([positionComponent])).toBe(false);
26
+ });
27
+
28
+ it("should add and remove entities", () => {
29
+ const archetype = new Archetype([positionComponent, velocityComponent], createDontFragmentRelations());
30
+ const entity1 = createEntityId(1024);
31
+ const entity2 = createEntityId(1025);
32
+
33
+ const componentData1 = new Map([
34
+ [positionComponent, { x: 0, y: 0 }],
35
+ [velocityComponent, { x: 1, y: 1 }],
36
+ ]);
37
+
38
+ const componentData2 = new Map([
39
+ [positionComponent, { x: 10, y: 10 }],
40
+ [velocityComponent, { x: 2, y: 2 }],
41
+ ]);
42
+
43
+ archetype.addEntity(entity1, componentData1);
44
+ expect(archetype.size).toBe(1);
45
+ expect(archetype.exists(entity1)).toBe(true);
46
+
47
+ archetype.addEntity(entity2, componentData2);
48
+ expect(archetype.size).toBe(2);
49
+ expect(archetype.exists(entity2)).toBe(true);
50
+
51
+ const removedData = archetype.removeEntity(entity1);
52
+ expect(archetype.size).toBe(1);
53
+ expect(archetype.exists(entity1)).toBe(false);
54
+ expect(removedData).toEqual(componentData1);
55
+ });
56
+
57
+ it("should get and set component data", () => {
58
+ const archetype = new Archetype([positionComponent], createDontFragmentRelations());
59
+ const entity = createEntityId(1024);
60
+ const initialPosition: Position = { x: 5, y: 5 };
61
+
62
+ archetype.addEntity(entity, new Map([[positionComponent, initialPosition]]));
63
+
64
+ const retrieved = archetype.get(entity, positionComponent);
65
+ expect(retrieved).toEqual(initialPosition);
66
+
67
+ const newPosition: Position = { x: 10, y: 10 };
68
+ archetype.set(entity, positionComponent, newPosition);
69
+ const retrieved2 = archetype.get(entity, positionComponent);
70
+ expect(retrieved2).toEqual(newPosition);
71
+ });
72
+
73
+ it("should get wildcard relation components", () => {
74
+ // Create relation component types
75
+ const target1 = createEntityId(1027);
76
+ const target2 = createEntityId(1028);
77
+ const relation1 = relation(positionComponent, target1);
78
+ const relation2 = relation(positionComponent, target2);
79
+ const wildcardPositionRelation = relation(positionComponent, "*");
80
+
81
+ const entity = createEntityId(1024);
82
+
83
+ // Archetype with multiple relations
84
+ const archetype = new Archetype([relation1, relation2], createDontFragmentRelations());
85
+
86
+ // Add entity with relations to target1 and target2
87
+ archetype.addEntity(
88
+ entity,
89
+ new Map([
90
+ [relation1, { distance: 10 }],
91
+ [relation2, { distance: 20 }],
92
+ ]),
93
+ );
94
+
95
+ // Get wildcard relations
96
+ const relations = archetype.get(entity, wildcardPositionRelation);
97
+ expect(relations).toEqual([
98
+ [target2, { distance: 20 }],
99
+ [target1, { distance: 10 }],
100
+ ]);
101
+
102
+ // Test with entity not in archetype
103
+ const nonExistentEntity = createEntityId(9999);
104
+ expect(() => archetype.get(nonExistentEntity, wildcardPositionRelation)).toThrow(
105
+ "Entity 9999 is not in this archetype",
106
+ );
107
+ });
108
+
109
+ it("should iterate over entities", () => {
110
+ const archetype = new Archetype([positionComponent], createDontFragmentRelations());
111
+ const entity1 = createEntityId(1024);
112
+ const entity2 = createEntityId(1025);
113
+
114
+ archetype.addEntity(entity1, new Map([[positionComponent, { x: 1, y: 1 }]]));
115
+ archetype.addEntity(entity2, new Map([[positionComponent, { x: 2, y: 2 }]]));
116
+
117
+ const iteratedEntities: EntityId[] = [];
118
+ archetype.forEach((entityId, _components) => {
119
+ iteratedEntities.push(entityId);
120
+ });
121
+
122
+ expect(iteratedEntities).toEqual([entity1, entity2]);
123
+ });
124
+
125
+ it("should get component data arrays", () => {
126
+ const archetype = new Archetype([positionComponent], createDontFragmentRelations());
127
+ const entity1 = createEntityId(1024);
128
+ const entity2 = createEntityId(1025);
129
+ const pos1: Position = { x: 1, y: 1 };
130
+ const pos2: Position = { x: 2, y: 2 };
131
+
132
+ archetype.addEntity(entity1, new Map([[positionComponent, pos1]]));
133
+ archetype.addEntity(entity2, new Map([[positionComponent, pos2]]));
134
+
135
+ const data = archetype.getComponentData(positionComponent);
136
+ expect(data).toEqual([pos1, pos2]);
137
+ });
138
+
139
+ it("should handle wildcard relations in forEachWithComponents", () => {
140
+ // Create a relation component type: position relation from entity to entity
141
+ const wildcardPositionRelation = relation(positionComponent, "*");
142
+
143
+ const entity1 = createEntityId(1024);
144
+ const entity2 = createEntityId(1025);
145
+ const target1 = createEntityId(1027);
146
+ const target2 = createEntityId(1028);
147
+
148
+ // Create specific relations for entity1 and entity2
149
+ const relation1 = relation(positionComponent, target1);
150
+ const relation2 = relation(positionComponent, target2);
151
+ const relation3 = relation(positionComponent, createEntityId(1029)); // For entity2
152
+
153
+ // Archetype with multiple relations
154
+ const archetype1 = new Archetype([relation1, relation2], createDontFragmentRelations());
155
+ const archetype2 = new Archetype([relation3], createDontFragmentRelations());
156
+
157
+ // Add entity1 with relations to target1 and target2
158
+ archetype1.addEntity(
159
+ entity1,
160
+ new Map([
161
+ [relation1, { distance: 10 }],
162
+ [relation2, { distance: 20 }],
163
+ ]),
164
+ );
165
+
166
+ // Add entity2 with relation to another target
167
+ archetype2.addEntity(entity2, new Map([[relation3, { distance: 30 }]]));
168
+
169
+ // Test forEachWithComponents with wildcard relation on archetype1
170
+ const results: Array<{ entity: EntityId; relations: [EntityId<any>, any][] }> = [];
171
+ archetype1.forEachWithComponents([wildcardPositionRelation], (entity, relations) => {
172
+ results.push({ entity, relations });
173
+ });
174
+
175
+ expect(results).toHaveLength(1);
176
+ expect(results[0]!.entity).toBe(entity1);
177
+ expect(results[0]!.relations).toEqual([
178
+ [target2, { distance: 20 }],
179
+ [target1, { distance: 10 }],
180
+ ]);
181
+
182
+ // Test on archetype2
183
+ const results2: Array<{ entity: EntityId; relations: [EntityId<any>, any][] }> = [];
184
+ archetype2.forEachWithComponents([wildcardPositionRelation], (entity, relations) => {
185
+ results2.push({ entity, relations });
186
+ });
187
+
188
+ expect(results2).toHaveLength(1);
189
+ expect(results2[0]!.entity).toBe(entity2);
190
+ expect(results2[0]!.relations).toEqual([[createEntityId(1029), { distance: 30 }]]);
191
+ });
192
+
193
+ it("should cache componentDataArrays correctly", () => {
194
+ // Test with wildcard relations to check cache invalidation
195
+ const target1 = createEntityId(1027);
196
+ const target2 = createEntityId(1028);
197
+ const relation1 = relation(positionComponent, target1);
198
+ const relation2 = relation(positionComponent, target2);
199
+ const wildcardPositionRelation = relation(positionComponent, "*");
200
+
201
+ const archetype = new Archetype([relation1, relation2], createDontFragmentRelations());
202
+
203
+ const entity1 = createEntityId(1024);
204
+
205
+ archetype.addEntity(
206
+ entity1,
207
+ new Map([
208
+ [relation1, { distance: 10 }],
209
+ [relation2, { distance: 20 }],
210
+ ]),
211
+ );
212
+
213
+ // First call - should compute and cache
214
+ let results1: [EntityId<any>, any][][] = [];
215
+ archetype.forEachWithComponents([wildcardPositionRelation], (_entity, relations) => {
216
+ results1.push(relations);
217
+ });
218
+ expect(results1[0]).toEqual([
219
+ [target2, { distance: 20 }],
220
+ [target1, { distance: 10 }],
221
+ ]);
222
+
223
+ // Second call - should use cache
224
+ let results2: [EntityId<any>, any][][] = [];
225
+ archetype.forEachWithComponents([wildcardPositionRelation], (_entity, relations) => {
226
+ results2.push(relations);
227
+ });
228
+ expect(results2[0]).toEqual([
229
+ [target2, { distance: 20 }],
230
+ [target1, { distance: 10 }],
231
+ ]);
232
+
233
+ // Modify data
234
+ (archetype as any).set(entity1, relation1, { distance: 100 });
235
+
236
+ // Third call - should still use cache (data is computed dynamically)
237
+ let results3: [EntityId<any>, any][][] = [];
238
+ archetype.forEachWithComponents([wildcardPositionRelation], (_entity, relations) => {
239
+ results3.push(relations);
240
+ });
241
+ // Since cache stores structure and data is computed dynamically, this should show updated data
242
+ expect(results3[0]).toEqual([
243
+ [target2, { distance: 20 }],
244
+ [target1, { distance: 100 }], // Updated
245
+ ]);
246
+ });
247
+ });