@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.
- 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/{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,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
|
+
});
|