@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,171 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { BitSet } from "../../utils/bit-set";
|
|
3
|
+
|
|
4
|
+
describe("BitSet word boundary tests", () => {
|
|
5
|
+
it("should setRange across word boundaries", () => {
|
|
6
|
+
const bitset = new BitSet(100);
|
|
7
|
+
|
|
8
|
+
// Set range crossing 32-bit boundary (30-35)
|
|
9
|
+
bitset.setRange(30, 35);
|
|
10
|
+
|
|
11
|
+
// Check that all bits in range are set
|
|
12
|
+
for (let i = 30; i <= 35; i++) {
|
|
13
|
+
expect(bitset.has(i)).toBe(true);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Check boundary bits
|
|
17
|
+
expect(bitset.has(29)).toBe(false);
|
|
18
|
+
expect(bitset.has(36)).toBe(false);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("should handle setRange at word boundary exactly", () => {
|
|
22
|
+
const bitset = new BitSet(100);
|
|
23
|
+
|
|
24
|
+
// Set range [0..31] = first word
|
|
25
|
+
bitset.setRange(0, 31);
|
|
26
|
+
|
|
27
|
+
for (let i = 0; i <= 31; i++) {
|
|
28
|
+
expect(bitset.has(i)).toBe(true);
|
|
29
|
+
}
|
|
30
|
+
expect(bitset.has(32)).toBe(false);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("should handle setRange spanning multiple words", () => {
|
|
34
|
+
const bitset = new BitSet(200);
|
|
35
|
+
|
|
36
|
+
// Set range [20..80] spanning parts of 3 words
|
|
37
|
+
bitset.setRange(20, 80);
|
|
38
|
+
|
|
39
|
+
for (let i = 20; i <= 80; i++) {
|
|
40
|
+
expect(bitset.has(i)).toBe(true);
|
|
41
|
+
}
|
|
42
|
+
expect(bitset.has(19)).toBe(false);
|
|
43
|
+
expect(bitset.has(81)).toBe(false);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("should anyClearInRange work across word boundaries", () => {
|
|
47
|
+
const bitset = new BitSet(100);
|
|
48
|
+
|
|
49
|
+
// Set all bits
|
|
50
|
+
bitset.setRange(0, 99);
|
|
51
|
+
|
|
52
|
+
// All bits are set, so anyClearInRange should return false
|
|
53
|
+
expect(bitset.anyClearInRange(30, 35)).toBe(false);
|
|
54
|
+
expect(bitset.anyClearInRange(0, 99)).toBe(false);
|
|
55
|
+
|
|
56
|
+
// Clear some bits crossing boundary
|
|
57
|
+
bitset.clear(31);
|
|
58
|
+
bitset.clear(32);
|
|
59
|
+
|
|
60
|
+
expect(bitset.anyClearInRange(30, 35)).toBe(true);
|
|
61
|
+
expect(bitset.anyClearInRange(0, 30)).toBe(false);
|
|
62
|
+
expect(bitset.anyClearInRange(33, 99)).toBe(false);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("should handle anyClearInRange with single word", () => {
|
|
66
|
+
const bitset = new BitSet(100);
|
|
67
|
+
|
|
68
|
+
bitset.setRange(10, 20);
|
|
69
|
+
|
|
70
|
+
// Range entirely in one word and all bits set
|
|
71
|
+
expect(bitset.anyClearInRange(10, 20)).toBe(false);
|
|
72
|
+
|
|
73
|
+
// Clear one bit in range
|
|
74
|
+
bitset.clear(15);
|
|
75
|
+
expect(bitset.anyClearInRange(10, 20)).toBe(true);
|
|
76
|
+
|
|
77
|
+
// Check range before and after
|
|
78
|
+
expect(bitset.anyClearInRange(0, 9)).toBe(true);
|
|
79
|
+
expect(bitset.anyClearInRange(21, 99)).toBe(true);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("should handle large ranges", () => {
|
|
83
|
+
const bitset = new BitSet(1000);
|
|
84
|
+
|
|
85
|
+
// Set all bits
|
|
86
|
+
bitset.setRange(0, 999);
|
|
87
|
+
|
|
88
|
+
expect(bitset.anyClearInRange(0, 999)).toBe(false);
|
|
89
|
+
|
|
90
|
+
// Clear one bit in the middle
|
|
91
|
+
bitset.clear(500);
|
|
92
|
+
expect(bitset.anyClearInRange(0, 999)).toBe(true);
|
|
93
|
+
expect(bitset.anyClearInRange(0, 499)).toBe(false);
|
|
94
|
+
expect(bitset.anyClearInRange(501, 999)).toBe(false);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("should handle range with no overlap", () => {
|
|
98
|
+
const bitset = new BitSet(100);
|
|
99
|
+
|
|
100
|
+
bitset.setRange(10, 20);
|
|
101
|
+
|
|
102
|
+
expect(bitset.anyClearInRange(0, 9)).toBe(true);
|
|
103
|
+
expect(bitset.anyClearInRange(21, 99)).toBe(true);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("should iterate over set bits correctly", () => {
|
|
107
|
+
const bitset = new BitSet(100);
|
|
108
|
+
|
|
109
|
+
bitset.set(5);
|
|
110
|
+
bitset.set(35);
|
|
111
|
+
bitset.set(65);
|
|
112
|
+
bitset.set(99);
|
|
113
|
+
|
|
114
|
+
const setBits: number[] = [];
|
|
115
|
+
for (const bit of bitset) {
|
|
116
|
+
setBits.push(bit);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
expect(setBits).toEqual([5, 35, 65, 99]);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("should iterate over setRange result", () => {
|
|
123
|
+
const bitset = new BitSet(100);
|
|
124
|
+
|
|
125
|
+
bitset.setRange(10, 15);
|
|
126
|
+
|
|
127
|
+
const setBits: number[] = [];
|
|
128
|
+
for (const bit of bitset) {
|
|
129
|
+
setBits.push(bit);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
expect(setBits).toEqual([10, 11, 12, 13, 14, 15]);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("should handle reset correctly", () => {
|
|
136
|
+
const bitset = new BitSet(100);
|
|
137
|
+
|
|
138
|
+
bitset.setRange(0, 99);
|
|
139
|
+
bitset.reset();
|
|
140
|
+
|
|
141
|
+
expect(bitset.anyClearInRange(0, 99)).toBe(true);
|
|
142
|
+
|
|
143
|
+
const setBits: number[] = [];
|
|
144
|
+
for (const bit of bitset) {
|
|
145
|
+
setBits.push(bit);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
expect(setBits).toHaveLength(0);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("should handle edge case: setRange with reversed bounds", () => {
|
|
152
|
+
const bitset = new BitSet(100);
|
|
153
|
+
|
|
154
|
+
// setRange(hi, lo) where hi > lo should do nothing
|
|
155
|
+
bitset.setRange(50, 30);
|
|
156
|
+
|
|
157
|
+
for (let i = 0; i < 100; i++) {
|
|
158
|
+
expect(bitset.has(i)).toBe(false);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("should handle single bit in range", () => {
|
|
163
|
+
const bitset = new BitSet(100);
|
|
164
|
+
|
|
165
|
+
bitset.setRange(25, 25);
|
|
166
|
+
|
|
167
|
+
expect(bitset.has(25)).toBe(true);
|
|
168
|
+
expect(bitset.has(24)).toBe(false);
|
|
169
|
+
expect(bitset.has(26)).toBe(false);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { ComponentChangeset } from "../../commands/changeset";
|
|
3
|
+
import { component } from "../../entity";
|
|
4
|
+
|
|
5
|
+
describe("ComponentChangeset", () => {
|
|
6
|
+
const PositionId = component<{ x: number; y: number }>();
|
|
7
|
+
const VelocityId = component<{ x: number; y: number }>();
|
|
8
|
+
const HealthId = component<number>();
|
|
9
|
+
|
|
10
|
+
describe("Basic Operations", () => {
|
|
11
|
+
it("should start with no changes", () => {
|
|
12
|
+
const changeset = new ComponentChangeset();
|
|
13
|
+
expect(changeset.hasChanges()).toBe(false);
|
|
14
|
+
expect(changeset.adds.size).toBe(0);
|
|
15
|
+
expect(changeset.removes.size).toBe(0);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("should add components", () => {
|
|
19
|
+
const changeset = new ComponentChangeset();
|
|
20
|
+
changeset.set(PositionId, { x: 10, y: 20 });
|
|
21
|
+
|
|
22
|
+
expect(changeset.hasChanges()).toBe(true);
|
|
23
|
+
expect(changeset.adds.get(PositionId)).toEqual({ x: 10, y: 20 });
|
|
24
|
+
expect(changeset.removes.size).toBe(0);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("should remove components", () => {
|
|
28
|
+
const changeset = new ComponentChangeset();
|
|
29
|
+
changeset.delete(PositionId);
|
|
30
|
+
|
|
31
|
+
expect(changeset.hasChanges()).toBe(true);
|
|
32
|
+
expect(changeset.adds.size).toBe(0);
|
|
33
|
+
expect(changeset.removes.has(PositionId)).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("should clear all changes", () => {
|
|
37
|
+
const changeset = new ComponentChangeset();
|
|
38
|
+
changeset.set(PositionId, { x: 10, y: 20 });
|
|
39
|
+
changeset.delete(VelocityId);
|
|
40
|
+
|
|
41
|
+
expect(changeset.hasChanges()).toBe(true);
|
|
42
|
+
|
|
43
|
+
changeset.clear();
|
|
44
|
+
|
|
45
|
+
expect(changeset.hasChanges()).toBe(false);
|
|
46
|
+
expect(changeset.adds.size).toBe(0);
|
|
47
|
+
expect(changeset.removes.size).toBe(0);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe("Conflict Resolution", () => {
|
|
52
|
+
it("should remove from removes when adding a component that was going to be removed", () => {
|
|
53
|
+
const changeset = new ComponentChangeset();
|
|
54
|
+
changeset.delete(PositionId);
|
|
55
|
+
changeset.set(PositionId, { x: 10, y: 20 });
|
|
56
|
+
|
|
57
|
+
expect(changeset.adds.get(PositionId)).toEqual({ x: 10, y: 20 });
|
|
58
|
+
expect(changeset.removes.has(PositionId)).toBe(false);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("should remove from adds when removing a component that was going to be added", () => {
|
|
62
|
+
const changeset = new ComponentChangeset();
|
|
63
|
+
changeset.set(PositionId, { x: 10, y: 20 });
|
|
64
|
+
changeset.delete(PositionId);
|
|
65
|
+
|
|
66
|
+
expect(changeset.adds.has(PositionId)).toBe(false);
|
|
67
|
+
expect(changeset.removes.has(PositionId)).toBe(true);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe("Apply Changes", () => {
|
|
72
|
+
it("should apply additions to existing components", () => {
|
|
73
|
+
const changeset = new ComponentChangeset();
|
|
74
|
+
changeset.set(PositionId, { x: 10, y: 20 });
|
|
75
|
+
changeset.set(VelocityId, { x: 1, y: 2 });
|
|
76
|
+
|
|
77
|
+
const existing = new Map();
|
|
78
|
+
existing.set(HealthId, 100);
|
|
79
|
+
|
|
80
|
+
const result = changeset.applyTo(existing);
|
|
81
|
+
|
|
82
|
+
expect(result.get(PositionId)).toEqual({ x: 10, y: 20 });
|
|
83
|
+
expect(result.get(VelocityId)).toEqual({ x: 1, y: 2 });
|
|
84
|
+
expect(result.get(HealthId)).toBe(100);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("should apply removals to existing components", () => {
|
|
88
|
+
const changeset = new ComponentChangeset();
|
|
89
|
+
changeset.delete(PositionId);
|
|
90
|
+
|
|
91
|
+
const existing = new Map();
|
|
92
|
+
existing.set(PositionId, { x: 10, y: 20 });
|
|
93
|
+
existing.set(VelocityId, { x: 1, y: 2 });
|
|
94
|
+
|
|
95
|
+
const result = changeset.applyTo(existing);
|
|
96
|
+
|
|
97
|
+
expect(result.has(PositionId)).toBe(false);
|
|
98
|
+
expect(result.get(VelocityId)).toEqual({ x: 1, y: 2 });
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("should apply both additions and removals", () => {
|
|
102
|
+
const changeset = new ComponentChangeset();
|
|
103
|
+
changeset.set(PositionId, { x: 50, y: 60 });
|
|
104
|
+
changeset.delete(VelocityId);
|
|
105
|
+
|
|
106
|
+
const existing = new Map();
|
|
107
|
+
existing.set(PositionId, { x: 10, y: 20 });
|
|
108
|
+
existing.set(VelocityId, { x: 1, y: 2 });
|
|
109
|
+
existing.set(HealthId, 100);
|
|
110
|
+
|
|
111
|
+
const result = changeset.applyTo(existing);
|
|
112
|
+
|
|
113
|
+
expect(result.get(PositionId)).toEqual({ x: 50, y: 60 }); // Updated
|
|
114
|
+
expect(result.has(VelocityId)).toBe(false); // Removed
|
|
115
|
+
expect(result.get(HealthId)).toBe(100); // Unchanged
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("should get final component types correctly", () => {
|
|
119
|
+
const changeset = new ComponentChangeset();
|
|
120
|
+
changeset.set(PositionId, { x: 10, y: 20 });
|
|
121
|
+
changeset.delete(VelocityId);
|
|
122
|
+
|
|
123
|
+
const existing = new Map();
|
|
124
|
+
existing.set(VelocityId, { x: 1, y: 2 });
|
|
125
|
+
existing.set(HealthId, 100);
|
|
126
|
+
|
|
127
|
+
const finalTypes = changeset.applyTo(existing);
|
|
128
|
+
|
|
129
|
+
expect([...finalTypes.keys()]).toEqual([HealthId, PositionId]);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe("Direct Access", () => {
|
|
134
|
+
it("should return direct references to internal maps for performance", () => {
|
|
135
|
+
const changeset = new ComponentChangeset();
|
|
136
|
+
changeset.set(PositionId, { x: 10, y: 20 });
|
|
137
|
+
|
|
138
|
+
const adds = changeset.adds;
|
|
139
|
+
const removes = changeset.removes;
|
|
140
|
+
|
|
141
|
+
// Since this is internal API, direct modification is allowed
|
|
142
|
+
adds.clear();
|
|
143
|
+
removes.add(VelocityId);
|
|
144
|
+
|
|
145
|
+
expect(changeset.adds.size).toBe(0);
|
|
146
|
+
expect(changeset.removes.size).toBe(1);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe("Merge Changesets", () => {
|
|
151
|
+
it("should merge additions into an empty changeset", () => {
|
|
152
|
+
const changeset1 = new ComponentChangeset();
|
|
153
|
+
const changeset2 = new ComponentChangeset();
|
|
154
|
+
changeset2.set(PositionId, { x: 10, y: 20 });
|
|
155
|
+
changeset2.set(VelocityId, { x: 1, y: 2 });
|
|
156
|
+
|
|
157
|
+
changeset1.merge(changeset2);
|
|
158
|
+
|
|
159
|
+
expect(changeset1.adds.get(PositionId)).toEqual({ x: 10, y: 20 });
|
|
160
|
+
expect(changeset1.adds.get(VelocityId)).toEqual({ x: 1, y: 2 });
|
|
161
|
+
expect(changeset1.removes.size).toBe(0);
|
|
162
|
+
expect(changeset1.hasChanges()).toBe(true);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("should merge removals into an empty changeset", () => {
|
|
166
|
+
const changeset1 = new ComponentChangeset();
|
|
167
|
+
const changeset2 = new ComponentChangeset();
|
|
168
|
+
changeset2.delete(PositionId);
|
|
169
|
+
changeset2.delete(VelocityId);
|
|
170
|
+
|
|
171
|
+
changeset1.merge(changeset2);
|
|
172
|
+
|
|
173
|
+
expect(changeset1.removes.has(PositionId)).toBe(true);
|
|
174
|
+
expect(changeset1.removes.has(VelocityId)).toBe(true);
|
|
175
|
+
expect(changeset1.adds.size).toBe(0);
|
|
176
|
+
expect(changeset1.hasChanges()).toBe(true);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("should merge additions and removals together", () => {
|
|
180
|
+
const changeset1 = new ComponentChangeset();
|
|
181
|
+
const changeset2 = new ComponentChangeset();
|
|
182
|
+
changeset2.set(PositionId, { x: 10, y: 20 });
|
|
183
|
+
changeset2.delete(VelocityId);
|
|
184
|
+
|
|
185
|
+
changeset1.merge(changeset2);
|
|
186
|
+
|
|
187
|
+
expect(changeset1.adds.get(PositionId)).toEqual({ x: 10, y: 20 });
|
|
188
|
+
expect(changeset1.removes.has(VelocityId)).toBe(true);
|
|
189
|
+
expect(changeset1.hasChanges()).toBe(true);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("should override removal with addition when merging", () => {
|
|
193
|
+
const changeset1 = new ComponentChangeset();
|
|
194
|
+
changeset1.delete(PositionId); // Initially removing
|
|
195
|
+
|
|
196
|
+
const changeset2 = new ComponentChangeset();
|
|
197
|
+
changeset2.set(PositionId, { x: 10, y: 20 }); // Adding the same component
|
|
198
|
+
|
|
199
|
+
changeset1.merge(changeset2);
|
|
200
|
+
|
|
201
|
+
expect(changeset1.adds.get(PositionId)).toEqual({ x: 10, y: 20 });
|
|
202
|
+
expect(changeset1.removes.has(PositionId)).toBe(false);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("should override addition with removal when merging", () => {
|
|
206
|
+
const changeset1 = new ComponentChangeset();
|
|
207
|
+
changeset1.set(PositionId, { x: 5, y: 5 }); // Initially adding
|
|
208
|
+
|
|
209
|
+
const changeset2 = new ComponentChangeset();
|
|
210
|
+
changeset2.delete(PositionId); // Removing the same component
|
|
211
|
+
|
|
212
|
+
changeset1.merge(changeset2);
|
|
213
|
+
|
|
214
|
+
expect(changeset1.adds.has(PositionId)).toBe(false);
|
|
215
|
+
expect(changeset1.removes.has(PositionId)).toBe(true);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("should merge multiple changesets sequentially", () => {
|
|
219
|
+
const changeset1 = new ComponentChangeset();
|
|
220
|
+
changeset1.set(PositionId, { x: 10, y: 20 });
|
|
221
|
+
|
|
222
|
+
const changeset2 = new ComponentChangeset();
|
|
223
|
+
changeset2.delete(PositionId);
|
|
224
|
+
changeset2.set(VelocityId, { x: 1, y: 2 });
|
|
225
|
+
|
|
226
|
+
const changeset3 = new ComponentChangeset();
|
|
227
|
+
changeset3.delete(VelocityId);
|
|
228
|
+
changeset3.set(HealthId, 100);
|
|
229
|
+
|
|
230
|
+
changeset1.merge(changeset2);
|
|
231
|
+
changeset1.merge(changeset3);
|
|
232
|
+
|
|
233
|
+
expect(changeset1.adds.has(PositionId)).toBe(false); // Removed by changeset2
|
|
234
|
+
expect(changeset1.removes.has(PositionId)).toBe(true);
|
|
235
|
+
expect(changeset1.adds.has(VelocityId)).toBe(false); // Removed by changeset3
|
|
236
|
+
expect(changeset1.removes.has(VelocityId)).toBe(true);
|
|
237
|
+
expect(changeset1.adds.get(HealthId)).toBe(100);
|
|
238
|
+
expect(changeset1.hasChanges()).toBe(true);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it("should handle merging empty changeset", () => {
|
|
242
|
+
const changeset1 = new ComponentChangeset();
|
|
243
|
+
changeset1.set(PositionId, { x: 10, y: 20 });
|
|
244
|
+
|
|
245
|
+
const changeset2 = new ComponentChangeset(); // Empty
|
|
246
|
+
|
|
247
|
+
changeset1.merge(changeset2);
|
|
248
|
+
|
|
249
|
+
expect(changeset1.adds.get(PositionId)).toEqual({ x: 10, y: 20 });
|
|
250
|
+
expect(changeset1.removes.size).toBe(0);
|
|
251
|
+
expect(changeset1.hasChanges()).toBe(true);
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { MultiMap } from "../../utils/multi-map";
|
|
3
|
+
|
|
4
|
+
describe("MultiMap", () => {
|
|
5
|
+
test("add and get values", () => {
|
|
6
|
+
const mm = new MultiMap<string, number>();
|
|
7
|
+
mm.add("a", 1);
|
|
8
|
+
mm.add("a", 2);
|
|
9
|
+
mm.add("b", 3);
|
|
10
|
+
|
|
11
|
+
expect(mm.keyCount).toBe(2);
|
|
12
|
+
expect(mm.valueCount).toBe(3);
|
|
13
|
+
|
|
14
|
+
const aValues = mm.get("a");
|
|
15
|
+
expect(Array.from(aValues).sort()).toEqual([1, 2]);
|
|
16
|
+
expect(Array.from(mm.get("c"))).toEqual([]);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("remove value and deleteKey behavior", () => {
|
|
20
|
+
const mm = new MultiMap<string, string>();
|
|
21
|
+
mm.add("x", "one");
|
|
22
|
+
mm.add("x", "two");
|
|
23
|
+
mm.add("y", "three");
|
|
24
|
+
|
|
25
|
+
expect(mm.valueCount).toBe(3);
|
|
26
|
+
|
|
27
|
+
const removed = mm.remove("x", "one");
|
|
28
|
+
expect(removed).toBe(true);
|
|
29
|
+
expect(mm.valueCount).toBe(2);
|
|
30
|
+
expect(mm.has("x", "one")).toBe(false);
|
|
31
|
+
|
|
32
|
+
// remove remaining value for x -> key removed
|
|
33
|
+
expect(mm.remove("x", "two")).toBe(true);
|
|
34
|
+
expect(mm.hasKey("x")).toBe(false);
|
|
35
|
+
expect(mm.keyCount).toBe(1);
|
|
36
|
+
expect(mm.valueCount).toBe(1);
|
|
37
|
+
|
|
38
|
+
// delete whole key y
|
|
39
|
+
expect(mm.deleteKey("y")).toBe(true);
|
|
40
|
+
expect(mm.keyCount).toBe(0);
|
|
41
|
+
expect(mm.valueCount).toBe(0);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("keys, values and entries iteration", () => {
|
|
45
|
+
const mm = new MultiMap<number, string>();
|
|
46
|
+
mm.add(1, "a");
|
|
47
|
+
mm.add(1, "b");
|
|
48
|
+
mm.add(2, "c");
|
|
49
|
+
|
|
50
|
+
const keys = Array.from(mm.keys()).sort((a, b) => a - b);
|
|
51
|
+
expect(keys).toEqual([1, 2]);
|
|
52
|
+
|
|
53
|
+
const values = Array.from(mm.values()).sort();
|
|
54
|
+
expect(values).toEqual(["a", "b", "c"]);
|
|
55
|
+
|
|
56
|
+
const entries = Array.from(mm.entries());
|
|
57
|
+
// entries should be pairs of key and a Set copy
|
|
58
|
+
expect(entries.length).toBe(3);
|
|
59
|
+
const entryMap = new Map(entries);
|
|
60
|
+
expect(entryMap.get(1)!).toEqual("b");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("clear resets state", () => {
|
|
64
|
+
const mm = new MultiMap<string, number>();
|
|
65
|
+
mm.add("k", 1);
|
|
66
|
+
mm.add("k", 2);
|
|
67
|
+
expect(mm.keyCount).toBe(1);
|
|
68
|
+
expect(mm.valueCount).toBe(2);
|
|
69
|
+
mm.clear();
|
|
70
|
+
expect(mm.keyCount).toBe(0);
|
|
71
|
+
expect(mm.valueCount).toBe(0);
|
|
72
|
+
expect(Array.from(mm.keys()).length).toBe(0);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { ComponentIdAllocator } from "../../entity/manager";
|
|
3
|
+
import { COMPONENT_ID_MAX, createComponentId } from "../../entity/types";
|
|
4
|
+
|
|
5
|
+
describe("ComponentIdAllocator", () => {
|
|
6
|
+
it("should allocate component IDs sequentially", () => {
|
|
7
|
+
const allocator = new ComponentIdAllocator();
|
|
8
|
+
|
|
9
|
+
const id1 = allocator.allocate();
|
|
10
|
+
const id2 = allocator.allocate();
|
|
11
|
+
const id3 = allocator.allocate();
|
|
12
|
+
|
|
13
|
+
expect(id1).toBe(createComponentId(1));
|
|
14
|
+
expect(id2).toBe(createComponentId(2));
|
|
15
|
+
expect(id3).toBe(createComponentId(3));
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("should throw when exceeding COMPONENT_ID_MAX", () => {
|
|
19
|
+
const allocator = new ComponentIdAllocator();
|
|
20
|
+
|
|
21
|
+
// Allocate up to the limit
|
|
22
|
+
for (let i = 1; i <= COMPONENT_ID_MAX; i++) {
|
|
23
|
+
allocator.allocate();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// The next allocation should throw
|
|
27
|
+
expect(() => allocator.allocate()).toThrow(/out of component IDs|overflow/i);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("should check availability correctly", () => {
|
|
31
|
+
const allocator = new ComponentIdAllocator();
|
|
32
|
+
|
|
33
|
+
expect(allocator.hasAvailableIds()).toBe(true);
|
|
34
|
+
|
|
35
|
+
// Allocate up to the limit
|
|
36
|
+
for (let i = 1; i <= COMPONENT_ID_MAX; i++) {
|
|
37
|
+
allocator.allocate();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
expect(allocator.hasAvailableIds()).toBe(false);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("should track next ID correctly", () => {
|
|
44
|
+
const allocator = new ComponentIdAllocator();
|
|
45
|
+
|
|
46
|
+
expect(allocator.getNextId()).toBe(1);
|
|
47
|
+
|
|
48
|
+
allocator.allocate();
|
|
49
|
+
expect(allocator.getNextId()).toBe(2);
|
|
50
|
+
|
|
51
|
+
allocator.allocate();
|
|
52
|
+
expect(allocator.getNextId()).toBe(3);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("should allocate exactly COMPONENT_ID_MAX IDs", () => {
|
|
56
|
+
const allocator = new ComponentIdAllocator();
|
|
57
|
+
|
|
58
|
+
for (let i = 1; i <= COMPONENT_ID_MAX; i++) {
|
|
59
|
+
const id = allocator.allocate();
|
|
60
|
+
expect(id).toBe(createComponentId(i));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Should be exhausted now
|
|
64
|
+
expect(allocator.hasAvailableIds()).toBe(false);
|
|
65
|
+
});
|
|
66
|
+
});
|