@codehz/ecs 0.8.2 → 0.10.0
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/README.en.md +26 -3
- package/README.md +41 -4
- package/dist/builder.d.mts +348 -83
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -2
- package/dist/testing.d.mts +1 -1
- package/dist/testing.mjs +1 -1
- package/dist/world.mjs +1922 -1400
- package/dist/world.mjs.map +1 -1
- package/examples/debug-observability.ts +92 -0
- package/examples/inventory-system-relations.ts +1 -1
- package/examples/parent-child-hierarchy.ts +18 -38
- package/examples/spatial-grid.ts +1 -1
- package/package.json +1 -1
- package/skills/ecs/SKILL.md +4 -4
- package/src/__tests__/component/singleton.test.ts +116 -35
- package/src/__tests__/core/archetype.test.ts +155 -13
- package/src/__tests__/core/bitset.test.ts +12 -0
- package/src/__tests__/entity/entity.test.ts +33 -0
- package/src/__tests__/entity/id-system.test.ts +40 -0
- package/src/__tests__/perf/comprehensive.perf.test.ts +6 -9
- package/src/__tests__/perf/serialization.perf.test.ts +242 -0
- package/src/__tests__/perf/{dontfragment-wildcard.perf.test.ts → sparse-wildcard.perf.test.ts} +13 -16
- package/src/__tests__/query/caching.test.ts +62 -0
- package/src/__tests__/query/filter.test.ts +16 -22
- package/src/__tests__/query/perf.test.ts +3 -5
- package/src/__tests__/relations/hierarchy.test.ts +208 -0
- package/src/__tests__/relations/{dont-fragment → sparse}/basic.test.ts +64 -69
- package/src/__tests__/relations/{dont-fragment → sparse}/query-notification.test.ts +17 -9
- package/src/__tests__/serialization/bounds.test.ts +133 -1
- package/src/__tests__/world/commands.test.ts +337 -0
- package/src/__tests__/world/component-management.test.ts +6 -5
- package/src/__tests__/world/debug-stats.test.ts +206 -0
- package/src/__tests__/world/multi-component-hooks.test.ts +44 -0
- package/src/__tests__/world/serialize.test.ts +17 -0
- package/src/__tests__/world/wildcard-relation-hooks.test.ts +127 -0
- package/src/archetype/archetype.ts +96 -46
- package/src/archetype/helpers.ts +7 -29
- package/src/archetype/store.ts +35 -20
- package/src/commands/buffer.ts +5 -2
- package/src/commands/changeset.ts +0 -31
- package/src/component/registry.ts +64 -63
- package/src/entity/index.ts +6 -3
- package/src/index.ts +15 -0
- package/src/query/filter.ts +4 -10
- package/src/query/query.ts +12 -12
- package/src/storage/serialization.ts +29 -2
- package/src/types/index.ts +71 -0
- package/src/world/archetype-manager.ts +283 -0
- package/src/world/command-executor.ts +258 -0
- package/src/world/commands.ts +44 -56
- package/src/world/debug-stats.ts +147 -0
- package/src/world/hooks.ts +8 -0
- package/src/world/operations.ts +88 -0
- package/src/world/serialization.ts +32 -18
- package/src/world/singleton.ts +51 -0
- package/src/world/world.ts +429 -457
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Example: Using the Debug Stats Collector for development & leak detection.
|
|
3
|
+
*
|
|
4
|
+
* Run with:
|
|
5
|
+
* bun run examples/debug-observability.ts
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { component, relation, World } from "../src";
|
|
9
|
+
|
|
10
|
+
type Position = { x: number; y: number };
|
|
11
|
+
type Velocity = { x: number; y: number };
|
|
12
|
+
|
|
13
|
+
// Components
|
|
14
|
+
const Position = component<Position>();
|
|
15
|
+
const Velocity = component<Velocity>();
|
|
16
|
+
const ChildOf = component<void>({ exclusive: true, sparse: true });
|
|
17
|
+
|
|
18
|
+
const world = new World();
|
|
19
|
+
|
|
20
|
+
// Create a debug stats collector
|
|
21
|
+
const collector = world.createDebugStatsCollector((stats) => {
|
|
22
|
+
console.log("=== Debug Stats ===");
|
|
23
|
+
console.log(`Sync time: ${(stats.timestamps.syncEnd - stats.timestamps.syncStart).toFixed(3)}ms`);
|
|
24
|
+
console.log(`Command buffer iterations: ${stats.commandIterations}`);
|
|
25
|
+
console.log(`Entities: ${stats.entities.total} (freelist: ${stats.entities.freelistSize})`);
|
|
26
|
+
console.log(`Archetypes: ${stats.archetypes.total} (empty: ${stats.archetypes.empty})`);
|
|
27
|
+
console.log(`Queries (cached/registered): ${stats.queries.cached}/${stats.queries.registered}`);
|
|
28
|
+
console.log(`Hooks: ${stats.hooks.total}`);
|
|
29
|
+
console.log(`Indices:`, stats.indices);
|
|
30
|
+
|
|
31
|
+
const act = stats.activity;
|
|
32
|
+
console.log("Activity this sync:");
|
|
33
|
+
console.log(` Migrations: ${act.migrations}`);
|
|
34
|
+
console.log(` Hooks executed: ${act.hooksExecuted}`);
|
|
35
|
+
console.log(` Archetypes created: ${act.archetypesCreated}`);
|
|
36
|
+
console.log(` Archetypes removed: ${act.archetypesRemoved}`);
|
|
37
|
+
console.log("===================\n");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Setup some entities
|
|
41
|
+
const parent = world.new();
|
|
42
|
+
world.set(parent, Position, { x: 0, y: 0 });
|
|
43
|
+
|
|
44
|
+
const children: any[] = [];
|
|
45
|
+
for (let i = 0; i < 5; i++) {
|
|
46
|
+
const child = world.new();
|
|
47
|
+
world.set(child, Position, { x: i * 10, y: 0 });
|
|
48
|
+
world.set(child, Velocity, { x: 1, y: 0.5 });
|
|
49
|
+
world.set(child, relation(ChildOf, parent));
|
|
50
|
+
children.push(child);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
console.log("Initial sync (should create archetypes + relations)");
|
|
54
|
+
world.sync();
|
|
55
|
+
|
|
56
|
+
// Cause some migrations by adding/removing components
|
|
57
|
+
console.log("Adding/removing components to trigger migrations...");
|
|
58
|
+
for (const c of children) {
|
|
59
|
+
world.remove(c, Velocity);
|
|
60
|
+
}
|
|
61
|
+
world.sync();
|
|
62
|
+
|
|
63
|
+
world.set(children[0], Velocity, { x: 2, y: 0 });
|
|
64
|
+
world.sync();
|
|
65
|
+
|
|
66
|
+
// Add a hook to observe hook execution counting
|
|
67
|
+
world.hook([Position, Velocity], {
|
|
68
|
+
on_set: () => {},
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
console.log("Triggering hook + more structural changes");
|
|
72
|
+
world.set(children[1], Velocity, { x: 3, y: 1 });
|
|
73
|
+
world.sync();
|
|
74
|
+
|
|
75
|
+
// Clean up some entities to potentially remove archetypes
|
|
76
|
+
console.log("Deleting some children...");
|
|
77
|
+
world.delete(children[2]);
|
|
78
|
+
world.delete(children[3]);
|
|
79
|
+
world.sync();
|
|
80
|
+
|
|
81
|
+
// Dispose the collector when done
|
|
82
|
+
collector[Symbol.dispose]();
|
|
83
|
+
|
|
84
|
+
console.log("Collector disposed. Further syncs will not trigger callbacks.");
|
|
85
|
+
world.set(parent, Position, { x: 100, y: 100 });
|
|
86
|
+
world.sync();
|
|
87
|
+
|
|
88
|
+
console.log("Done. This demonstrates typical usage for spotting:");
|
|
89
|
+
console.log("- Unexpected archetype growth (fragmentation)");
|
|
90
|
+
console.log("- High migration or hook execution counts");
|
|
91
|
+
console.log("- Command buffer iteration spikes");
|
|
92
|
+
console.log("- Leaking entities or relations (watch entity/freelist numbers over time)");
|
|
@@ -9,7 +9,7 @@ const ItemName = component<ItemName>({ name: "ItemName" });
|
|
|
9
9
|
const Stackable = component<Stackable>({ name: "Stackable" });
|
|
10
10
|
const Gold = component<Gold>({ name: "Gold" });
|
|
11
11
|
const EquipmentSlot = component<EquipmentSlot>({ name: "EquipmentSlot" });
|
|
12
|
-
const InInventory = component<void>({ name: "InInventory",
|
|
12
|
+
const InInventory = component<void>({ name: "InInventory", sparse: true });
|
|
13
13
|
|
|
14
14
|
const world = new World();
|
|
15
15
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { pipeline } from "@codehz/pipeline";
|
|
2
|
-
import { World, component, relation, type
|
|
2
|
+
import { World, component, relation, type Query } from "../src";
|
|
3
3
|
|
|
4
4
|
// Define component types
|
|
5
5
|
type Transform = { x: number; y: number; rotation: number; scale: number };
|
|
@@ -13,7 +13,7 @@ const LocalTransform = component<Transform>({ name: "LocalTransform" });
|
|
|
13
13
|
const WorldTransform = component<Transform>({ name: "WorldTransform" });
|
|
14
14
|
const LinearVelocity = component<LinearVelocity>({ name: "LinearVelocity" });
|
|
15
15
|
const AngularVelocity = component<AngularVelocity>({ name: "AngularVelocity" });
|
|
16
|
-
const ChildOf = component<void>({ exclusive: true,
|
|
16
|
+
const ChildOf = component<void>({ exclusive: true, sparse: true, name: "ChildOf" });
|
|
17
17
|
|
|
18
18
|
// Create the world
|
|
19
19
|
const world = new World();
|
|
@@ -22,7 +22,6 @@ const world = new World();
|
|
|
22
22
|
const movementQuery: Query = world.createQuery([LocalTransform, LinearVelocity]);
|
|
23
23
|
const rotationQuery: Query = world.createQuery([LocalTransform, AngularVelocity]);
|
|
24
24
|
const transformQuery: Query = world.createQuery([Name, LocalTransform, WorldTransform]);
|
|
25
|
-
const childQuery: Query = world.createQuery([relation(ChildOf, "*")]);
|
|
26
25
|
const renderQuery: Query = world.createQuery([Name, WorldTransform]);
|
|
27
26
|
|
|
28
27
|
function toRadians(degrees: number): number {
|
|
@@ -59,38 +58,9 @@ function formatTransform(transform: Transform): string {
|
|
|
59
58
|
return `pos=(${transform.x.toFixed(2)}, ${transform.y.toFixed(2)}) rot=${transform.rotation.toFixed(1)}deg scale=${transform.scale.toFixed(2)}`;
|
|
60
59
|
}
|
|
61
60
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
childQuery.forEach([relation(ChildOf, "*")], (child, parents) => {
|
|
66
|
-
const parent = parents[0]?.[0];
|
|
67
|
-
if (parent === undefined) return;
|
|
68
|
-
|
|
69
|
-
const children = childrenByParent.get(parent) ?? [];
|
|
70
|
-
children.push(child);
|
|
71
|
-
childrenByParent.set(parent, children);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
return childrenByParent;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function propagateChildren(
|
|
78
|
-
parent: EntityId,
|
|
79
|
-
parentWorld: Transform,
|
|
80
|
-
childrenByParent: Map<EntityId, EntityId[]>,
|
|
81
|
-
): void {
|
|
82
|
-
const children = childrenByParent.get(parent);
|
|
83
|
-
if (!children) return;
|
|
84
|
-
|
|
85
|
-
for (const child of children) {
|
|
86
|
-
const name = world.get(child, Name);
|
|
87
|
-
const local = world.get(child, LocalTransform);
|
|
88
|
-
const worldTransform = world.get(child, WorldTransform);
|
|
89
|
-
copyTransform(worldTransform, composeTransform(local, parentWorld));
|
|
90
|
-
console.log(` Child ${name.value}: ${formatTransform(worldTransform)}`);
|
|
91
|
-
propagateChildren(child, worldTransform, childrenByParent);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
61
|
+
// NOTE: Before the relation/hierarchy companion tools, this required manually
|
|
62
|
+
// building a children map every frame + writing recursive propagation.
|
|
63
|
+
// Now we can use the built-in efficient helpers (getChildren + traverseDescendants).
|
|
94
64
|
|
|
95
65
|
// Build game loop using pipeline
|
|
96
66
|
// Pass execution order is determined by addition order; no need to manually manage dependencies
|
|
@@ -115,16 +85,26 @@ const gameLoop = pipeline<{ deltaTime: number }>()
|
|
|
115
85
|
});
|
|
116
86
|
})
|
|
117
87
|
// Hierarchy pass - propagate parent transforms into world transforms
|
|
88
|
+
// (modernized with the new relation/hierarchy companion tools)
|
|
118
89
|
.addPass(() => {
|
|
119
90
|
console.log(`[HierarchyPass] Propagating world transforms`);
|
|
120
|
-
const childrenByParent = buildChildrenByParent();
|
|
121
91
|
|
|
122
92
|
transformQuery.forEach([Name, LocalTransform, WorldTransform], (entity, name, localTransform, worldTransform) => {
|
|
123
|
-
if (world.has(entity, relation(ChildOf, "*"))) return;
|
|
93
|
+
if (world.has(entity, relation(ChildOf, "*"))) return; // skip non-roots
|
|
124
94
|
|
|
125
95
|
copyTransform(worldTransform, composeTransform(localTransform));
|
|
126
96
|
console.log(` Root ${name.value}: ${formatTransform(worldTransform)}`);
|
|
127
|
-
|
|
97
|
+
|
|
98
|
+
// Use the efficient built-in traverser (replaces manual Map + recursion)
|
|
99
|
+
world.traverseDescendants(entity, ChildOf, (child, _depth, parent) => {
|
|
100
|
+
if (!parent) return;
|
|
101
|
+
const childName = world.get(child, Name);
|
|
102
|
+
const local = world.get(child, LocalTransform);
|
|
103
|
+
const childWorld = world.get(child, WorldTransform);
|
|
104
|
+
const parentWorldT = world.get(parent, WorldTransform);
|
|
105
|
+
copyTransform(childWorld, composeTransform(local, parentWorldT));
|
|
106
|
+
console.log(` Child ${childName.value}: ${formatTransform(childWorld)}`);
|
|
107
|
+
});
|
|
128
108
|
});
|
|
129
109
|
})
|
|
130
110
|
// Render pass - render propagated world transforms
|
package/examples/spatial-grid.ts
CHANGED
|
@@ -216,7 +216,7 @@ function main() {
|
|
|
216
216
|
console.log("=========================================================\n");
|
|
217
217
|
|
|
218
218
|
// Create singleton SpatialGrid
|
|
219
|
-
world.set(
|
|
219
|
+
world.singleton(SpatialGrid).set({ cells: new Map(), cellSize: 64 });
|
|
220
220
|
console.log("SpatialGrid singleton created (cellSize=64)");
|
|
221
221
|
|
|
222
222
|
// Create 1 player near the center
|
package/package.json
CHANGED
package/skills/ecs/SKILL.md
CHANGED
|
@@ -34,7 +34,7 @@ The library uses **archetype storage + deferred command buffering**. All structu
|
|
|
34
34
|
7. **NEVER** call `sync()` inside `forEach`, hooks, or while iterating query results.
|
|
35
35
|
8. **NEVER** confuse `remove(entity, Component)` with `delete(entity)`.
|
|
36
36
|
9. **MUST** use relation components (`relation(Comp, target)`) instead of storing `EntityId` in data when you need to reference other entities.
|
|
37
|
-
10. **MUST** understand the three relation flags (`exclusive`, `cascadeDelete`, `dontFragment`) before using relations.
|
|
37
|
+
10. **MUST** understand the three relation flags (`exclusive`, `cascadeDelete`, `sparse` / legacy `dontFragment`) before using relations.
|
|
38
38
|
|
|
39
39
|
---
|
|
40
40
|
|
|
@@ -205,7 +205,7 @@ Use `relation(Component, target)` to create entity-to-entity references.
|
|
|
205
205
|
- When the target entity is deleted, **the entire referencing entity is deleted**.
|
|
206
206
|
- This is transitive and powerful. Use deliberately.
|
|
207
207
|
|
|
208
|
-
**`
|
|
208
|
+
**`sparse: true`** (preferred; legacy key `dontFragment` is fully equivalent and supported)
|
|
209
209
|
|
|
210
210
|
- Prevents archetype fragmentation when many different targets exist.
|
|
211
211
|
- **Required** for relations with high cardinality or frequent target changes (e.g. `ChildOf` with thousands of children, AI targeting, inventory).
|
|
@@ -214,8 +214,8 @@ Use `relation(Component, target)` to create entity-to-entity references.
|
|
|
214
214
|
|
|
215
215
|
```ts
|
|
216
216
|
const ChildOf = component({ exclusive: true, cascadeDelete: true });
|
|
217
|
-
const Targeting = component({ exclusive: true,
|
|
218
|
-
const InventorySlot = component({
|
|
217
|
+
const Targeting = component({ exclusive: true, sparse: true });
|
|
218
|
+
const InventorySlot = component({ sparse: true });
|
|
219
219
|
```
|
|
220
220
|
|
|
221
221
|
### 8. Referencing Other Entities — Never Store Raw EntityId
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from "bun:test";
|
|
2
|
-
import {
|
|
2
|
+
import { ComponentEntityStore } from "../../component/entity-store";
|
|
3
|
+
import { component, createEntityId, relation, type EntityId } from "../../entity";
|
|
3
4
|
import { World } from "../../world/world";
|
|
4
5
|
|
|
5
6
|
describe("World - Singleton Component", () => {
|
|
@@ -9,29 +10,82 @@ describe("World - Singleton Component", () => {
|
|
|
9
10
|
const GlobalConfigId = component<GlobalConfig>();
|
|
10
11
|
const GameStateId = component<GameState>();
|
|
11
12
|
|
|
12
|
-
it("should set singleton component
|
|
13
|
+
it("should set singleton component through the explicit handle", () => {
|
|
13
14
|
const world = new World();
|
|
14
15
|
const config: GlobalConfig = { debug: true, version: "1.0.0" };
|
|
16
|
+
const singleton = world.singleton(GlobalConfigId);
|
|
15
17
|
|
|
16
|
-
|
|
17
|
-
world.set(GlobalConfigId, config);
|
|
18
|
+
singleton.set(config);
|
|
18
19
|
world.sync();
|
|
19
20
|
|
|
20
|
-
// Verify it was set on the component entity itself
|
|
21
21
|
expect(world.has(GlobalConfigId)).toBe(true);
|
|
22
22
|
expect(world.get(GlobalConfigId)).toEqual(config);
|
|
23
23
|
});
|
|
24
24
|
|
|
25
|
-
it("should
|
|
25
|
+
it("should interpret 2-argument set on a component entity as a void component set", () => {
|
|
26
|
+
const world = new World();
|
|
27
|
+
const singleton = world.singleton(GlobalConfigId);
|
|
28
|
+
const Marker = component<void>();
|
|
29
|
+
|
|
30
|
+
world.set(GlobalConfigId, Marker);
|
|
31
|
+
world.sync();
|
|
32
|
+
|
|
33
|
+
expect(world.has(GlobalConfigId, Marker)).toBe(true);
|
|
34
|
+
expect(singleton.has()).toBe(false);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("should reject the removed singleton data shorthand at runtime", () => {
|
|
38
|
+
const world = new World();
|
|
39
|
+
const config: GlobalConfig = { debug: true, version: "1.0.0" };
|
|
40
|
+
|
|
41
|
+
expect(() => {
|
|
42
|
+
world.set(GlobalConfigId as any, config as any);
|
|
43
|
+
}).toThrow("Invalid component type");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("should manage singleton data through an explicit handle", () => {
|
|
47
|
+
const world = new World();
|
|
48
|
+
const config = world.singleton(GlobalConfigId);
|
|
49
|
+
|
|
50
|
+
expect(config.getOptional()).toBeUndefined();
|
|
51
|
+
expect(config.has()).toBe(false);
|
|
52
|
+
|
|
53
|
+
config.set({ debug: true, version: "1.0.0" });
|
|
54
|
+
world.sync();
|
|
55
|
+
|
|
56
|
+
expect(config.has()).toBe(true);
|
|
57
|
+
expect(config.get()).toEqual({ debug: true, version: "1.0.0" });
|
|
58
|
+
|
|
59
|
+
config.remove();
|
|
60
|
+
world.sync();
|
|
61
|
+
|
|
62
|
+
expect(config.has()).toBe(false);
|
|
63
|
+
expect(config.getOptional()).toBeUndefined();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("should support void singleton components through an explicit handle", () => {
|
|
67
|
+
const world = new World();
|
|
68
|
+
const Tag = component<void>();
|
|
69
|
+
const tag = world.singleton(Tag);
|
|
70
|
+
|
|
71
|
+
tag.set();
|
|
72
|
+
world.sync();
|
|
73
|
+
|
|
74
|
+
expect(tag.has()).toBe(true);
|
|
75
|
+
expect(world.has(Tag)).toBe(true);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("should update singleton component through the explicit handle", () => {
|
|
26
79
|
const world = new World();
|
|
27
80
|
const config1: GlobalConfig = { debug: true, version: "1.0.0" };
|
|
28
81
|
const config2: GlobalConfig = { debug: false, version: "2.0.0" };
|
|
82
|
+
const singleton = world.singleton(GlobalConfigId);
|
|
29
83
|
|
|
30
|
-
|
|
84
|
+
singleton.set(config1);
|
|
31
85
|
world.sync();
|
|
32
86
|
expect(world.get(GlobalConfigId)).toEqual(config1);
|
|
33
87
|
|
|
34
|
-
|
|
88
|
+
singleton.set(config2);
|
|
35
89
|
world.sync();
|
|
36
90
|
expect(world.get(GlobalConfigId)).toEqual(config2);
|
|
37
91
|
});
|
|
@@ -41,15 +95,12 @@ describe("World - Singleton Component", () => {
|
|
|
41
95
|
const world2 = new World();
|
|
42
96
|
const config: GlobalConfig = { debug: true, version: "1.0.0" };
|
|
43
97
|
|
|
44
|
-
|
|
45
|
-
world1.set(GlobalConfigId, config);
|
|
98
|
+
world1.singleton(GlobalConfigId).set(config);
|
|
46
99
|
world1.sync();
|
|
47
100
|
|
|
48
|
-
// Traditional syntax
|
|
49
101
|
world2.set(GlobalConfigId, GlobalConfigId, config);
|
|
50
102
|
world2.sync();
|
|
51
103
|
|
|
52
|
-
// Both should have the same result
|
|
53
104
|
expect(world1.get(GlobalConfigId)).toEqual(world2.get(GlobalConfigId));
|
|
54
105
|
});
|
|
55
106
|
|
|
@@ -58,8 +109,8 @@ describe("World - Singleton Component", () => {
|
|
|
58
109
|
const config: GlobalConfig = { debug: true, version: "1.0.0" };
|
|
59
110
|
const state: GameState = { score: 100, level: 5 };
|
|
60
111
|
|
|
61
|
-
world.set(
|
|
62
|
-
world.set(
|
|
112
|
+
world.singleton(GlobalConfigId).set(config);
|
|
113
|
+
world.singleton(GameStateId).set(state);
|
|
63
114
|
world.sync();
|
|
64
115
|
|
|
65
116
|
expect(world.get(GlobalConfigId)).toEqual(config);
|
|
@@ -77,19 +128,17 @@ describe("World - Singleton Component", () => {
|
|
|
77
128
|
}).toThrow("does not exist");
|
|
78
129
|
});
|
|
79
130
|
|
|
80
|
-
it("should check singleton component existence
|
|
131
|
+
it("should check singleton component existence through the explicit handle", () => {
|
|
81
132
|
const world = new World();
|
|
82
133
|
const config: GlobalConfig = { debug: true, version: "1.0.0" };
|
|
134
|
+
const singleton = world.singleton(GlobalConfigId);
|
|
83
135
|
|
|
84
|
-
|
|
85
|
-
expect(world.has(GlobalConfigId)).toBe(false);
|
|
136
|
+
expect(singleton.has()).toBe(false);
|
|
86
137
|
|
|
87
|
-
|
|
88
|
-
world.set(GlobalConfigId, config);
|
|
138
|
+
singleton.set(config);
|
|
89
139
|
world.sync();
|
|
90
140
|
|
|
91
|
-
|
|
92
|
-
expect(world.has(GlobalConfigId)).toBe(true);
|
|
141
|
+
expect(singleton.has()).toBe(true);
|
|
93
142
|
});
|
|
94
143
|
|
|
95
144
|
it("should be equivalent to has(comp, comp)", () => {
|
|
@@ -97,29 +146,26 @@ describe("World - Singleton Component", () => {
|
|
|
97
146
|
const world2 = new World();
|
|
98
147
|
const config: GlobalConfig = { debug: true, version: "1.0.0" };
|
|
99
148
|
|
|
100
|
-
|
|
101
|
-
world1.set(GlobalConfigId, config);
|
|
149
|
+
world1.singleton(GlobalConfigId).set(config);
|
|
102
150
|
world1.sync();
|
|
103
151
|
|
|
104
|
-
// Traditional syntax
|
|
105
152
|
world2.set(GlobalConfigId, GlobalConfigId, config);
|
|
106
153
|
world2.sync();
|
|
107
154
|
|
|
108
|
-
// Both should have the same result
|
|
109
155
|
expect(world1.has(GlobalConfigId)).toBe(world2.has(GlobalConfigId, GlobalConfigId));
|
|
110
156
|
expect(world1.has(GlobalConfigId)).toBe(true);
|
|
111
157
|
});
|
|
112
158
|
|
|
113
|
-
it("should remove singleton component
|
|
159
|
+
it("should remove singleton component through the explicit handle", () => {
|
|
114
160
|
const world = new World();
|
|
115
161
|
const config: GlobalConfig = { debug: true, version: "1.0.0" };
|
|
162
|
+
const singleton = world.singleton(GlobalConfigId);
|
|
116
163
|
|
|
117
|
-
|
|
164
|
+
singleton.set(config);
|
|
118
165
|
world.sync();
|
|
119
166
|
expect(world.has(GlobalConfigId)).toBe(true);
|
|
120
167
|
|
|
121
|
-
|
|
122
|
-
world.remove(GlobalConfigId);
|
|
168
|
+
singleton.remove();
|
|
123
169
|
world.sync();
|
|
124
170
|
expect(world.has(GlobalConfigId)).toBe(false);
|
|
125
171
|
});
|
|
@@ -129,20 +175,55 @@ describe("World - Singleton Component", () => {
|
|
|
129
175
|
const world2 = new World();
|
|
130
176
|
const config: GlobalConfig = { debug: true, version: "1.0.0" };
|
|
131
177
|
|
|
132
|
-
|
|
133
|
-
world1.set(GlobalConfigId, config);
|
|
178
|
+
world1.singleton(GlobalConfigId).set(config);
|
|
134
179
|
world1.sync();
|
|
135
180
|
world2.set(GlobalConfigId, GlobalConfigId, config);
|
|
136
181
|
world2.sync();
|
|
137
182
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
world2.remove(GlobalConfigId, GlobalConfigId); // Traditional syntax
|
|
183
|
+
world1.singleton(GlobalConfigId).remove();
|
|
184
|
+
world2.remove(GlobalConfigId, GlobalConfigId);
|
|
141
185
|
world1.sync();
|
|
142
186
|
world2.sync();
|
|
143
187
|
|
|
144
|
-
// Both should have the same result
|
|
145
188
|
expect(world1.has(GlobalConfigId)).toBe(world2.has(GlobalConfigId, GlobalConfigId));
|
|
146
189
|
expect(world1.has(GlobalConfigId)).toBe(false);
|
|
147
190
|
});
|
|
191
|
+
|
|
192
|
+
it("should cover ComponentEntityStore hasWildcard, getWildcard, wildcard delete paths", () => {
|
|
193
|
+
const store = new ComponentEntityStore();
|
|
194
|
+
const compE = GlobalConfigId as EntityId; // reuse as component entity id (valid in range)
|
|
195
|
+
const target1 = createEntityId(1024);
|
|
196
|
+
const target2 = createEntityId(1025);
|
|
197
|
+
const relComp = relation(GlobalConfigId, target1); // entity-relation on the comp entity
|
|
198
|
+
const relComp2 = relation(GlobalConfigId, target2);
|
|
199
|
+
const wildcard = relation(GlobalConfigId, "*");
|
|
200
|
+
|
|
201
|
+
// Setup via internal? Use executeCommands to populate (simulates)
|
|
202
|
+
store.executeCommands(compE, [
|
|
203
|
+
{ type: "set", componentType: relComp, component: { dist: 1 } } as any,
|
|
204
|
+
{ type: "set", componentType: relComp2, component: { dist: 2 } } as any,
|
|
205
|
+
]);
|
|
206
|
+
|
|
207
|
+
// hasWildcard
|
|
208
|
+
expect(store.hasWildcard(compE, GlobalConfigId as any)).toBe(true);
|
|
209
|
+
expect(store.hasWildcard(compE, GameStateId as any)).toBe(false);
|
|
210
|
+
expect(store.hasWildcard(createEntityId(9999), GlobalConfigId as any)).toBe(false); // no data
|
|
211
|
+
|
|
212
|
+
// getWildcard
|
|
213
|
+
const w1 = store.getWildcard(compE, wildcard as any);
|
|
214
|
+
expect(w1.length).toBe(2);
|
|
215
|
+
|
|
216
|
+
// wildcard delete via executeCommands
|
|
217
|
+
store.executeCommands(compE, [{ type: "delete", componentType: wildcard } as any]);
|
|
218
|
+
const afterDel = store.getWildcard(compE, wildcard as any);
|
|
219
|
+
expect(afterDel.length).toBe(0);
|
|
220
|
+
|
|
221
|
+
// also test get on non exist throws
|
|
222
|
+
expect(() => store.get(compE, createEntityId(5000) as any)).toThrow();
|
|
223
|
+
expect(store.getOptional(compE, createEntityId(5000) as any)).toBeUndefined();
|
|
224
|
+
|
|
225
|
+
// clear
|
|
226
|
+
store.clear(compE);
|
|
227
|
+
expect(store.has(compE, relComp)).toBe(false);
|
|
228
|
+
});
|
|
148
229
|
});
|