@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.
- 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 +60 -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/{builder.d.mts → dist/builder.d.mts} +0 -0
- /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
- /package/{world.mjs → dist/world.mjs} +0 -0
- /package/{world.mjs.map → dist/world.mjs.map} +0 -0
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { component, relation, type EntityId } from "../../entity";
|
|
3
|
+
import { World } from "../../world/world";
|
|
4
|
+
|
|
5
|
+
describe("Query", () => {
|
|
6
|
+
describe("Optional Components in Queries", () => {
|
|
7
|
+
type Position = { x: number; y: number };
|
|
8
|
+
type Velocity = { x: number; y: number };
|
|
9
|
+
type Health = { value: number };
|
|
10
|
+
|
|
11
|
+
const positionComponent = component<Position>();
|
|
12
|
+
const velocityComponent = component<Velocity>();
|
|
13
|
+
const healthComponent = component<Health>();
|
|
14
|
+
|
|
15
|
+
it("should handle optional components in forEach", () => {
|
|
16
|
+
const world = new World();
|
|
17
|
+
const query = world.createQuery([positionComponent]);
|
|
18
|
+
|
|
19
|
+
const entity1 = world.new();
|
|
20
|
+
const entity2 = world.new();
|
|
21
|
+
const entity3 = world.new();
|
|
22
|
+
|
|
23
|
+
world.set(entity1, positionComponent, { x: 1, y: 2 });
|
|
24
|
+
world.set(entity1, velocityComponent, { x: 0.1, y: 0.2 });
|
|
25
|
+
world.set(entity2, positionComponent, { x: 3, y: 4 });
|
|
26
|
+
// entity2 has no velocity
|
|
27
|
+
world.set(entity3, positionComponent, { x: 5, y: 6 });
|
|
28
|
+
world.set(entity3, healthComponent, { value: 100 });
|
|
29
|
+
|
|
30
|
+
world.sync();
|
|
31
|
+
|
|
32
|
+
const results: Array<{ entity: EntityId; position: Position; velocity?: { value: Velocity } }> = [];
|
|
33
|
+
|
|
34
|
+
query.forEach([positionComponent, { optional: velocityComponent }], (entity, position, velocity) => {
|
|
35
|
+
results.push({ entity, position, velocity });
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
expect(results.length).toBe(3);
|
|
39
|
+
|
|
40
|
+
const result1 = results.find((r) => r.entity === entity1);
|
|
41
|
+
const result2 = results.find((r) => r.entity === entity2);
|
|
42
|
+
const result3 = results.find((r) => r.entity === entity3);
|
|
43
|
+
|
|
44
|
+
expect(result1).toBeDefined();
|
|
45
|
+
expect(result2).toBeDefined();
|
|
46
|
+
expect(result3).toBeDefined();
|
|
47
|
+
|
|
48
|
+
expect(result1!.position).toEqual({ x: 1, y: 2 });
|
|
49
|
+
expect(result1!.velocity).toEqual({ value: { x: 0.1, y: 0.2 } });
|
|
50
|
+
|
|
51
|
+
expect(result2!.position).toEqual({ x: 3, y: 4 });
|
|
52
|
+
expect(result2!.velocity).toBeUndefined();
|
|
53
|
+
|
|
54
|
+
expect(result3!.position).toEqual({ x: 5, y: 6 });
|
|
55
|
+
expect(result3!.velocity).toBeUndefined();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("should handle optional components in getEntitiesWithComponents", () => {
|
|
59
|
+
const world = new World();
|
|
60
|
+
const query = world.createQuery([positionComponent]);
|
|
61
|
+
|
|
62
|
+
const entity1 = world.new();
|
|
63
|
+
const entity2 = world.new();
|
|
64
|
+
|
|
65
|
+
world.set(entity1, positionComponent, { x: 1, y: 2 });
|
|
66
|
+
world.set(entity1, velocityComponent, { x: 0.1, y: 0.2 });
|
|
67
|
+
world.set(entity2, positionComponent, { x: 3, y: 4 });
|
|
68
|
+
// entity2 has no velocity
|
|
69
|
+
|
|
70
|
+
world.sync();
|
|
71
|
+
|
|
72
|
+
const results = query.getEntitiesWithComponents([positionComponent, { optional: velocityComponent }]);
|
|
73
|
+
|
|
74
|
+
expect(results.length).toBe(2);
|
|
75
|
+
|
|
76
|
+
const result1 = results.find((r) => r.entity === entity1);
|
|
77
|
+
const result2 = results.find((r) => r.entity === entity2);
|
|
78
|
+
|
|
79
|
+
expect(result1).toBeDefined();
|
|
80
|
+
expect(result2).toBeDefined();
|
|
81
|
+
|
|
82
|
+
expect(result1!.components[0]).toEqual({ x: 1, y: 2 });
|
|
83
|
+
expect(result1!.components[1]).toEqual({ value: { x: 0.1, y: 0.2 } });
|
|
84
|
+
|
|
85
|
+
expect(result2!.components[0]).toEqual({ x: 3, y: 4 });
|
|
86
|
+
expect(result2!.components[1]).toBeUndefined();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("should handle optional components in iterate", () => {
|
|
90
|
+
const world = new World();
|
|
91
|
+
const query = world.createQuery([positionComponent]);
|
|
92
|
+
|
|
93
|
+
const entity1 = world.new();
|
|
94
|
+
const entity2 = world.new();
|
|
95
|
+
|
|
96
|
+
world.set(entity1, positionComponent, { x: 1, y: 2 });
|
|
97
|
+
world.set(entity1, velocityComponent, { x: 0.1, y: 0.2 });
|
|
98
|
+
world.set(entity2, positionComponent, { x: 3, y: 4 });
|
|
99
|
+
// entity2 has no velocity
|
|
100
|
+
|
|
101
|
+
world.sync();
|
|
102
|
+
|
|
103
|
+
const results: Array<{ entity: EntityId; position: Position; velocity?: { value: Velocity } }> = [];
|
|
104
|
+
|
|
105
|
+
for (const [entity, position, velocity] of query.iterate([positionComponent, { optional: velocityComponent }])) {
|
|
106
|
+
results.push({ entity, position, velocity });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
expect(results.length).toBe(2);
|
|
110
|
+
|
|
111
|
+
const result1 = results.find((r) => r.entity === entity1);
|
|
112
|
+
const result2 = results.find((r) => r.entity === entity2);
|
|
113
|
+
|
|
114
|
+
expect(result1).toBeDefined();
|
|
115
|
+
expect(result2).toBeDefined();
|
|
116
|
+
|
|
117
|
+
expect(result1!.position).toEqual({ x: 1, y: 2 });
|
|
118
|
+
expect(result1!.velocity).toEqual({ value: { x: 0.1, y: 0.2 } });
|
|
119
|
+
|
|
120
|
+
expect(result2!.position).toEqual({ x: 3, y: 4 });
|
|
121
|
+
expect(result2!.velocity).toBeUndefined();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("should handle mixed mandatory and optional components", () => {
|
|
125
|
+
const world = new World();
|
|
126
|
+
const query = world.createQuery([positionComponent, velocityComponent]);
|
|
127
|
+
|
|
128
|
+
const entity1 = world.new();
|
|
129
|
+
const entity2 = world.new();
|
|
130
|
+
const entity3 = world.new();
|
|
131
|
+
|
|
132
|
+
world.set(entity1, positionComponent, { x: 1, y: 2 });
|
|
133
|
+
world.set(entity1, velocityComponent, { x: 0.1, y: 0.2 });
|
|
134
|
+
world.set(entity1, healthComponent, { value: 100 });
|
|
135
|
+
|
|
136
|
+
world.set(entity2, positionComponent, { x: 3, y: 4 });
|
|
137
|
+
world.set(entity2, velocityComponent, { x: 0.2, y: 0.3 });
|
|
138
|
+
// entity2 has no health
|
|
139
|
+
|
|
140
|
+
world.set(entity3, positionComponent, { x: 5, y: 6 });
|
|
141
|
+
world.set(entity3, velocityComponent, { x: 0.3, y: 0.4 });
|
|
142
|
+
world.set(entity3, healthComponent, { value: 50 });
|
|
143
|
+
|
|
144
|
+
world.sync();
|
|
145
|
+
|
|
146
|
+
const results: Array<{
|
|
147
|
+
entity: EntityId;
|
|
148
|
+
position: Position;
|
|
149
|
+
velocity: Velocity;
|
|
150
|
+
health?: { value: Health };
|
|
151
|
+
}> = [];
|
|
152
|
+
|
|
153
|
+
query.forEach(
|
|
154
|
+
[positionComponent, velocityComponent, { optional: healthComponent }],
|
|
155
|
+
(entity, position, velocity, health) => {
|
|
156
|
+
results.push({ entity, position, velocity, health });
|
|
157
|
+
},
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
expect(results.length).toBe(3);
|
|
161
|
+
|
|
162
|
+
const result1 = results.find((r) => r.entity === entity1);
|
|
163
|
+
const result2 = results.find((r) => r.entity === entity2);
|
|
164
|
+
const result3 = results.find((r) => r.entity === entity3);
|
|
165
|
+
|
|
166
|
+
expect(result1).toBeDefined();
|
|
167
|
+
expect(result2).toBeDefined();
|
|
168
|
+
expect(result3).toBeDefined();
|
|
169
|
+
|
|
170
|
+
expect(result1!.position).toEqual({ x: 1, y: 2 });
|
|
171
|
+
expect(result1!.velocity).toEqual({ x: 0.1, y: 0.2 });
|
|
172
|
+
expect(result1!.health).toEqual({ value: { value: 100 } });
|
|
173
|
+
|
|
174
|
+
expect(result2!.position).toEqual({ x: 3, y: 4 });
|
|
175
|
+
expect(result2!.velocity).toEqual({ x: 0.2, y: 0.3 });
|
|
176
|
+
expect(result2!.health).toBeUndefined();
|
|
177
|
+
|
|
178
|
+
expect(result3!.position).toEqual({ x: 5, y: 6 });
|
|
179
|
+
expect(result3!.velocity).toEqual({ x: 0.3, y: 0.4 });
|
|
180
|
+
expect(result3!.health).toEqual({ value: { value: 50 } });
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("should handle optional wildcard relations", () => {
|
|
184
|
+
const world = new World();
|
|
185
|
+
|
|
186
|
+
const wildcardPositionRelation = relation(positionComponent, "*");
|
|
187
|
+
const query = world.createQuery([velocityComponent]);
|
|
188
|
+
|
|
189
|
+
const entity1 = world.new();
|
|
190
|
+
const entity2 = world.new();
|
|
191
|
+
const targetEntity = world.new();
|
|
192
|
+
|
|
193
|
+
world.set(entity1, velocityComponent, { x: 0.1, y: 0.2 });
|
|
194
|
+
world.set(entity1, relation(positionComponent, targetEntity), { x: 1, y: 2 });
|
|
195
|
+
|
|
196
|
+
world.set(entity2, velocityComponent, { x: 0.2, y: 0.3 });
|
|
197
|
+
// entity2 has no position relation
|
|
198
|
+
|
|
199
|
+
world.sync();
|
|
200
|
+
|
|
201
|
+
const results: Array<{
|
|
202
|
+
entity: EntityId;
|
|
203
|
+
velocity: Velocity;
|
|
204
|
+
positionRelation?: { value: [EntityId<unknown>, Position][] };
|
|
205
|
+
}> = [];
|
|
206
|
+
|
|
207
|
+
query.forEach(
|
|
208
|
+
[velocityComponent, { optional: wildcardPositionRelation }],
|
|
209
|
+
(entity, velocity, positionRelation) => {
|
|
210
|
+
results.push({ entity, velocity, positionRelation });
|
|
211
|
+
},
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
expect(results.length).toBe(2);
|
|
215
|
+
|
|
216
|
+
const result1 = results.find((r) => r.entity === entity1);
|
|
217
|
+
const result2 = results.find((r) => r.entity === entity2);
|
|
218
|
+
|
|
219
|
+
expect(result1).toBeDefined();
|
|
220
|
+
expect(result2).toBeDefined();
|
|
221
|
+
|
|
222
|
+
expect(result1!.velocity).toEqual({ x: 0.1, y: 0.2 });
|
|
223
|
+
expect(result1!.positionRelation).toEqual({
|
|
224
|
+
value: [[targetEntity, { x: 1, y: 2 }]],
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
expect(result2!.velocity).toEqual({ x: 0.2, y: 0.3 });
|
|
228
|
+
expect(result2!.positionRelation).toBeUndefined();
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { World, component } from "../../index";
|
|
2
|
+
|
|
3
|
+
// Define component types
|
|
4
|
+
type Position = { x: number; y: number };
|
|
5
|
+
type Velocity = { x: number; y: number };
|
|
6
|
+
type Health = { value: number };
|
|
7
|
+
|
|
8
|
+
// Create component IDs
|
|
9
|
+
const positionComponent = component<Position>();
|
|
10
|
+
const velocityComponent = component<Velocity>();
|
|
11
|
+
const healthComponent = component<Health>();
|
|
12
|
+
|
|
13
|
+
// Performance test function
|
|
14
|
+
function performanceTest() {
|
|
15
|
+
console.log("=== Query Performance Test ===");
|
|
16
|
+
|
|
17
|
+
const world = new World();
|
|
18
|
+
|
|
19
|
+
// Create many entities
|
|
20
|
+
console.log("Creating 1000 entities...");
|
|
21
|
+
const startCreate = performance.now();
|
|
22
|
+
|
|
23
|
+
for (let i = 0; i < 1000; i++) {
|
|
24
|
+
const entity = world.new();
|
|
25
|
+
|
|
26
|
+
// Add position component
|
|
27
|
+
world.set(entity, positionComponent, {
|
|
28
|
+
x: Math.random() * 100,
|
|
29
|
+
y: Math.random() * 100,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// 50% of entities have velocity component
|
|
33
|
+
if (i % 2 === 0) {
|
|
34
|
+
world.set(entity, velocityComponent, {
|
|
35
|
+
x: Math.random() - 0.5,
|
|
36
|
+
y: Math.random() - 0.5,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// 25% of entities have health component
|
|
41
|
+
if (i % 4 === 0) {
|
|
42
|
+
world.set(entity, healthComponent, {
|
|
43
|
+
value: Math.floor(Math.random() * 100) + 1,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
world.sync();
|
|
49
|
+
|
|
50
|
+
const endCreate = performance.now();
|
|
51
|
+
console.log(`Entity creation time: ${(endCreate - startCreate).toFixed(2)}ms`);
|
|
52
|
+
|
|
53
|
+
// Create queries
|
|
54
|
+
const positionVelocityQuery = world.createQuery([positionComponent, velocityComponent]);
|
|
55
|
+
const healthQuery = world.createQuery([healthComponent]);
|
|
56
|
+
|
|
57
|
+
// Test getEntitiesWithComponents performance
|
|
58
|
+
console.log("\nTesting getEntitiesWithComponents performance...");
|
|
59
|
+
const iterations = 100;
|
|
60
|
+
|
|
61
|
+
let totalTime = 0;
|
|
62
|
+
for (let i = 0; i < iterations; i++) {
|
|
63
|
+
const start = performance.now();
|
|
64
|
+
positionVelocityQuery.getEntitiesWithComponents([positionComponent, velocityComponent]);
|
|
65
|
+
const end = performance.now();
|
|
66
|
+
totalTime += end - start;
|
|
67
|
+
}
|
|
68
|
+
console.log(`Average getEntitiesWithComponents time: ${(totalTime / iterations).toFixed(4)}ms`);
|
|
69
|
+
|
|
70
|
+
// Test forEach performance
|
|
71
|
+
totalTime = 0;
|
|
72
|
+
for (let i = 0; i < iterations; i++) {
|
|
73
|
+
const start = performance.now();
|
|
74
|
+
positionVelocityQuery.forEach([positionComponent, velocityComponent], (_entity, _position, _velocity) => {
|
|
75
|
+
// No-op, just for measuring iteration performance
|
|
76
|
+
});
|
|
77
|
+
const end = performance.now();
|
|
78
|
+
totalTime += end - start;
|
|
79
|
+
}
|
|
80
|
+
console.log(`Average forEach time: ${(totalTime / iterations).toFixed(4)}ms`);
|
|
81
|
+
|
|
82
|
+
// Verify result correctness
|
|
83
|
+
const entitiesWithData = positionVelocityQuery.getEntitiesWithComponents([positionComponent, velocityComponent]);
|
|
84
|
+
console.log(`\nFound ${entitiesWithData.length} entities with position and velocity`);
|
|
85
|
+
|
|
86
|
+
let forEachCount = 0;
|
|
87
|
+
positionVelocityQuery.forEach([positionComponent, velocityComponent], () => {
|
|
88
|
+
forEachCount++;
|
|
89
|
+
});
|
|
90
|
+
console.log(`forEach iterated over ${forEachCount} entities`);
|
|
91
|
+
|
|
92
|
+
// Cleanup
|
|
93
|
+
positionVelocityQuery.dispose();
|
|
94
|
+
healthQuery.dispose();
|
|
95
|
+
|
|
96
|
+
console.log("\nPerformance test completed!");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
performanceTest();
|