@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.
- package/{builder.d.mts → dist/builder.d.mts} +4 -2
- package/{world.mjs → dist/world.mjs} +9 -30
- package/dist/world.mjs.map +1 -0
- package/examples/advanced-scheduling.ts +96 -0
- package/examples/collision-detection.ts +229 -0
- package/examples/inventory-system-relations.ts +108 -0
- package/examples/parent-child-hierarchy.ts +206 -0
- package/examples/serialization.ts +337 -0
- package/examples/simple.ts +96 -0
- package/examples/spatial-grid.ts +276 -0
- package/examples/state-machine.ts +273 -0
- package/examples/tag-filtering.ts +266 -0
- package/package.json +58 -12
- package/src/__tests__/commands/buffer-limits.test.ts +72 -0
- package/src/__tests__/commands/buffer.test.ts +195 -0
- package/src/__tests__/component/singleton.test.ts +148 -0
- package/src/__tests__/core/archetype.test.ts +247 -0
- package/src/__tests__/core/bitset.test.ts +171 -0
- package/src/__tests__/core/changeset.test.ts +254 -0
- package/src/__tests__/core/multi-map.test.ts +74 -0
- package/src/__tests__/entity/component-registry.test.ts +66 -0
- package/src/__tests__/entity/entity.test.ts +520 -0
- package/src/__tests__/entity/id-manager.test.ts +157 -0
- package/src/__tests__/entity/id-system.test.ts +260 -0
- package/src/__tests__/perf/comprehensive.perf.test.ts +300 -0
- package/src/__tests__/perf/sync-hotpath.perf.test.ts +79 -0
- package/src/__tests__/query/basic.test.ts +341 -0
- package/src/__tests__/query/caching.test.ts +112 -0
- package/src/__tests__/query/filter.test.ts +111 -0
- package/src/__tests__/query/optional.test.ts +231 -0
- package/src/__tests__/query/perf.test.ts +99 -0
- package/src/__tests__/relations/dont-fragment/basic.test.ts +496 -0
- package/src/__tests__/relations/dont-fragment/query-notification.test.ts +125 -0
- package/src/__tests__/relations/wildcard.test.ts +179 -0
- package/src/__tests__/serialization/bounds.test.ts +237 -0
- package/src/__tests__/testing/assertions.test.ts +224 -0
- package/src/__tests__/testing/entity-builder.test.ts +84 -0
- package/src/__tests__/testing/snapshot.test.ts +150 -0
- package/src/__tests__/testing/world-fixture.test.ts +73 -0
- package/src/__tests__/world/component-hooks.test.ts +185 -0
- package/src/__tests__/world/component-management.test.ts +447 -0
- package/src/__tests__/world/entity-management.test.ts +86 -0
- package/src/__tests__/world/get-optional.test.ts +96 -0
- package/src/__tests__/world/multi-component-hooks.test.ts +502 -0
- package/src/__tests__/world/perf.test.ts +93 -0
- package/src/__tests__/world/query.test.ts +223 -0
- package/src/__tests__/world/serialize.test.ts +83 -0
- package/src/__tests__/world/wildcard-relation-hooks.test.ts +332 -0
- package/src/archetype/archetype.ts +472 -0
- package/src/archetype/helpers.ts +186 -0
- package/src/archetype/store.ts +33 -0
- package/src/commands/buffer.ts +110 -0
- package/src/commands/changeset.ts +104 -0
- package/src/component/entity-store.ts +223 -0
- package/src/component/registry.ts +657 -0
- package/src/component/type-utils.ts +9 -0
- package/src/entity/index.ts +63 -0
- package/src/entity/manager.ts +115 -0
- package/src/entity/relation.ts +319 -0
- package/src/entity/types.ts +135 -0
- package/src/index.ts +41 -0
- package/src/query/filter.ts +75 -0
- package/src/query/query.ts +313 -0
- package/src/query/registry.ts +101 -0
- package/src/storage/serialization.ts +130 -0
- package/src/testing/index.ts +634 -0
- package/src/types/index.ts +99 -0
- package/src/utils/bit-set.ts +133 -0
- package/src/utils/multi-map.ts +96 -0
- package/src/utils/utils.ts +19 -0
- package/src/world/builder.ts +100 -0
- package/src/world/commands.ts +378 -0
- package/src/world/hooks.ts +358 -0
- package/src/world/references.ts +38 -0
- package/src/world/serialization.ts +122 -0
- package/src/world/world.ts +1201 -0
- package/world.mjs.map +0 -1
- /package/{index.d.mts → dist/index.d.mts} +0 -0
- /package/{index.mjs → dist/index.mjs} +0 -0
- /package/{testing.d.mts → dist/testing.d.mts} +0 -0
- /package/{testing.mjs → dist/testing.mjs} +0 -0
- /package/{testing.mjs.map → dist/testing.mjs.map} +0 -0
|
@@ -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
|
+
});
|