@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.
Files changed (57) hide show
  1. package/README.en.md +26 -3
  2. package/README.md +41 -4
  3. package/dist/builder.d.mts +348 -83
  4. package/dist/index.d.mts +2 -2
  5. package/dist/index.mjs +2 -2
  6. package/dist/testing.d.mts +1 -1
  7. package/dist/testing.mjs +1 -1
  8. package/dist/world.mjs +1922 -1400
  9. package/dist/world.mjs.map +1 -1
  10. package/examples/debug-observability.ts +92 -0
  11. package/examples/inventory-system-relations.ts +1 -1
  12. package/examples/parent-child-hierarchy.ts +18 -38
  13. package/examples/spatial-grid.ts +1 -1
  14. package/package.json +1 -1
  15. package/skills/ecs/SKILL.md +4 -4
  16. package/src/__tests__/component/singleton.test.ts +116 -35
  17. package/src/__tests__/core/archetype.test.ts +155 -13
  18. package/src/__tests__/core/bitset.test.ts +12 -0
  19. package/src/__tests__/entity/entity.test.ts +33 -0
  20. package/src/__tests__/entity/id-system.test.ts +40 -0
  21. package/src/__tests__/perf/comprehensive.perf.test.ts +6 -9
  22. package/src/__tests__/perf/serialization.perf.test.ts +242 -0
  23. package/src/__tests__/perf/{dontfragment-wildcard.perf.test.ts → sparse-wildcard.perf.test.ts} +13 -16
  24. package/src/__tests__/query/caching.test.ts +62 -0
  25. package/src/__tests__/query/filter.test.ts +16 -22
  26. package/src/__tests__/query/perf.test.ts +3 -5
  27. package/src/__tests__/relations/hierarchy.test.ts +208 -0
  28. package/src/__tests__/relations/{dont-fragment → sparse}/basic.test.ts +64 -69
  29. package/src/__tests__/relations/{dont-fragment → sparse}/query-notification.test.ts +17 -9
  30. package/src/__tests__/serialization/bounds.test.ts +133 -1
  31. package/src/__tests__/world/commands.test.ts +337 -0
  32. package/src/__tests__/world/component-management.test.ts +6 -5
  33. package/src/__tests__/world/debug-stats.test.ts +206 -0
  34. package/src/__tests__/world/multi-component-hooks.test.ts +44 -0
  35. package/src/__tests__/world/serialize.test.ts +17 -0
  36. package/src/__tests__/world/wildcard-relation-hooks.test.ts +127 -0
  37. package/src/archetype/archetype.ts +96 -46
  38. package/src/archetype/helpers.ts +7 -29
  39. package/src/archetype/store.ts +35 -20
  40. package/src/commands/buffer.ts +5 -2
  41. package/src/commands/changeset.ts +0 -31
  42. package/src/component/registry.ts +64 -63
  43. package/src/entity/index.ts +6 -3
  44. package/src/index.ts +15 -0
  45. package/src/query/filter.ts +4 -10
  46. package/src/query/query.ts +12 -12
  47. package/src/storage/serialization.ts +29 -2
  48. package/src/types/index.ts +71 -0
  49. package/src/world/archetype-manager.ts +283 -0
  50. package/src/world/command-executor.ts +258 -0
  51. package/src/world/commands.ts +44 -56
  52. package/src/world/debug-stats.ts +147 -0
  53. package/src/world/hooks.ts +8 -0
  54. package/src/world/operations.ts +88 -0
  55. package/src/world/serialization.ts +32 -18
  56. package/src/world/singleton.ts +51 -0
  57. 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", dontFragment: true });
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 EntityId, type Query } from "../src";
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, dontFragment: true, name: "ChildOf" });
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
- function buildChildrenByParent(): Map<EntityId, EntityId[]> {
63
- const childrenByParent = new Map<EntityId, EntityId[]>();
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
- propagateChildren(entity, worldTransform, childrenByParent);
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
@@ -216,7 +216,7 @@ function main() {
216
216
  console.log("=========================================================\n");
217
217
 
218
218
  // Create singleton SpatialGrid
219
- world.set(SpatialGrid, { cells: new Map(), cellSize: 64 });
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codehz/ecs",
3
- "version": "0.8.2",
3
+ "version": "0.10.0",
4
4
  "license": "MIT",
5
5
  "module": "src/index.ts",
6
6
  "type": "module",
@@ -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
- **`dontFragment: true`**
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, dontFragment: true });
218
- const InventorySlot = component({ dontFragment: true });
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 { component } from "../../entity";
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 using shorthand syntax", () => {
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
- // Use singleton syntax: set(componentId, data)
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 update singleton component using shorthand syntax", () => {
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
- world.set(GlobalConfigId, config1);
84
+ singleton.set(config1);
31
85
  world.sync();
32
86
  expect(world.get(GlobalConfigId)).toEqual(config1);
33
87
 
34
- world.set(GlobalConfigId, config2);
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
- // Singleton syntax
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(GlobalConfigId, config);
62
- world.set(GameStateId, state);
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 using shorthand syntax", () => {
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
- // Before setting, should return false
85
- expect(world.has(GlobalConfigId)).toBe(false);
136
+ expect(singleton.has()).toBe(false);
86
137
 
87
- // Set singleton component
88
- world.set(GlobalConfigId, config);
138
+ singleton.set(config);
89
139
  world.sync();
90
140
 
91
- // After setting, should return true
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
- // Singleton syntax
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 using shorthand syntax", () => {
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
- world.set(GlobalConfigId, config);
164
+ singleton.set(config);
118
165
  world.sync();
119
166
  expect(world.has(GlobalConfigId)).toBe(true);
120
167
 
121
- // Remove using singleton syntax
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
- // Set on both worlds
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
- // Remove using different syntax
139
- world1.remove(GlobalConfigId); // Singleton syntax
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
  });