@ste_tisci/ecs 0.1.6 → 0.1.8

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.md CHANGED
@@ -75,6 +75,39 @@ function gameLoop() {
75
75
  gameLoop();
76
76
  ```
77
77
 
78
+ ## Hot systems: `queryIds`
79
+
80
+ `query` yields one result object plus one `{ id }` object per component per entity, which is
81
+ convenient but allocates on every iteration. For systems that run every frame over many
82
+ entities, `queryIds` returns the matching entity IDs instead, and `indices` translates an
83
+ entity ID into its slot in the SoA arrays:
84
+
85
+ ```typescript
86
+ function movementSystem(World) {
87
+ const { Position, Velocity } = World.components;
88
+ const pos = World.indices.Position;
89
+ const vel = World.indices.Velocity;
90
+
91
+ for (const eid of World.queryIds('Position', 'Velocity')) {
92
+ Position.x[pos[eid]] += Velocity.x[vel[eid]];
93
+ Position.y[pos[eid]] += Velocity.y[vel[eid]];
94
+ }
95
+ }
96
+ ```
97
+
98
+ Both APIs return the same entities; `queryIds` is roughly 3x faster on a packed query and 5x
99
+ on a fragmented one, at the cost of resolving indices yourself.
100
+
101
+ ## Iteration and mutation
102
+
103
+ `query` iterates a snapshot taken when the generator starts, so calling `removeComponent` or
104
+ `destroyEntity` from inside the loop cannot skip an entity. Entities created mid-iteration are
105
+ not visited by the query in flight; the next call picks them up.
106
+
107
+ `queryIds` builds its whole result before returning, so the same guarantee holds. Note that
108
+ destroying an entity invalidates its entry in `indices`, so if a loop destroys entities it
109
+ must not read their storage index afterwards.
110
+
78
111
  ## Error Handling
79
112
 
80
113
  Every operation validates its inputs and throws a descriptive `Error` instead of failing silently or corrupting state:
package/dist/index.cjs ADDED
@@ -0,0 +1,309 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ ECS: () => ECS
24
+ });
25
+ module.exports = __toCommonJS(index_exports);
26
+
27
+ // src/utils/SparseSet.ts
28
+ function SparseSet() {
29
+ const sparse = [];
30
+ const dense = [];
31
+ let size = 0;
32
+ function has(eid) {
33
+ const ID = getIndex(eid);
34
+ const exist = ID !== void 0 && dense[ID] === eid;
35
+ const isWhitinBounds = ID >= 0 && ID < size;
36
+ return exist && isWhitinBounds;
37
+ }
38
+ function add(eid) {
39
+ if (has(eid)) throw new Error(`ID ${eid} already present in the set`);
40
+ sparse[eid] = size;
41
+ dense[size] = eid;
42
+ size++;
43
+ }
44
+ function remove(eid) {
45
+ if (!has(eid)) throw new Error(`Cannot remove ID ${eid} because does not exists`);
46
+ const ID = getIndex(eid);
47
+ const last = dense[size - 1];
48
+ dense[ID] = last;
49
+ sparse[last] = ID;
50
+ dense.pop();
51
+ size--;
52
+ delete sparse[eid];
53
+ }
54
+ function getIndex(eid) {
55
+ return sparse[eid];
56
+ }
57
+ function getDense() {
58
+ return dense;
59
+ }
60
+ function getSize() {
61
+ return size;
62
+ }
63
+ function getSparse() {
64
+ return sparse;
65
+ }
66
+ return { has, add, remove, getIndex, getDense, getSize, getSparse };
67
+ }
68
+
69
+ // src/ComponentStore.ts
70
+ var INITIAL_CAPACITY = 8;
71
+ function grow(buffer, requiredIndex) {
72
+ let capacity = buffer.length || INITIAL_CAPACITY;
73
+ while (capacity <= requiredIndex) capacity *= 2;
74
+ const grown = new Float32Array(capacity);
75
+ grown.set(buffer);
76
+ return grown;
77
+ }
78
+ function ComponentStore() {
79
+ const componentSet = SparseSet();
80
+ const componentData = /* @__PURE__ */ Object.create(null);
81
+ const isNumeric = /* @__PURE__ */ Object.create(null);
82
+ function add(eid, data) {
83
+ if (componentSet.has(eid)) throw new Error(`Entity ${eid} already has this component`);
84
+ const ID = componentSet.getSize();
85
+ componentSet.add(eid);
86
+ for (const key in data) {
87
+ const value = data[key];
88
+ if (!(key in componentData)) {
89
+ isNumeric[key] = typeof value === "number";
90
+ componentData[key] = isNumeric[key] ? new Float32Array(INITIAL_CAPACITY) : [];
91
+ }
92
+ if (isNumeric[key] && ID >= componentData[key].length) {
93
+ componentData[key] = grow(componentData[key], ID);
94
+ }
95
+ componentData[key][ID] = value;
96
+ }
97
+ }
98
+ function remove(eid) {
99
+ if (!componentSet.has(eid)) throw new Error(`Entity ${eid} does not have this component`);
100
+ const ID = componentSet.getIndex(eid);
101
+ const lastID = componentSet.getSize() - 1;
102
+ if (ID !== lastID) {
103
+ for (const key of Object.keys(componentData)) {
104
+ componentData[key][ID] = componentData[key][lastID];
105
+ }
106
+ }
107
+ componentSet.remove(eid);
108
+ for (const key in componentData) {
109
+ if (!isNumeric[key]) {
110
+ componentData[key].pop();
111
+ }
112
+ }
113
+ }
114
+ function getDense() {
115
+ return componentSet.getDense();
116
+ }
117
+ function getData() {
118
+ return componentData;
119
+ }
120
+ function getIndex(eid) {
121
+ return componentSet.getIndex(eid);
122
+ }
123
+ function getSize() {
124
+ return componentSet.getSize();
125
+ }
126
+ function getSparse() {
127
+ return componentSet.getSparse();
128
+ }
129
+ return { add, remove, getIndex, getDense, getData, getSize, getSparse };
130
+ }
131
+
132
+ // src/EntityManager.ts
133
+ function EntityManager(registry) {
134
+ let nextID = 0;
135
+ const recycledIDs = [];
136
+ const bitMasks = [];
137
+ function exists(eid) {
138
+ const isWithinBounds = eid >= 0 && eid < nextID;
139
+ const hasMask = bitMasks[eid] !== void 0;
140
+ return isWithinBounds && hasMask;
141
+ }
142
+ function hasComponent(eid, name) {
143
+ if (!exists(eid)) throw new Error(`Entity ${eid} does not exist`);
144
+ const cid = registry.getID(name);
145
+ return (bitMasks[eid] & 1n << BigInt(cid)) !== 0n;
146
+ }
147
+ function create() {
148
+ const ID = recycledIDs.length > 0 ? recycledIDs.pop() : nextID++;
149
+ bitMasks[ID] = 0n;
150
+ return ID;
151
+ }
152
+ function remove(eid) {
153
+ if (!exists(eid)) throw new Error(`Entity ${eid} does not exist`);
154
+ bitMasks[eid] = void 0;
155
+ recycledIDs.push(eid);
156
+ }
157
+ function addComponent(eid, name) {
158
+ if (!exists(eid)) throw new Error(`Entity ${eid} does not exists`);
159
+ if (hasComponent(eid, name)) throw new Error(`Entity ${eid} already has component ${name}`);
160
+ const cid = registry.getID(name);
161
+ bitMasks[eid] |= 1n << BigInt(cid);
162
+ }
163
+ function removeComponent(eid, name) {
164
+ if (!exists(eid)) throw new Error(`Entity ${eid} does not exists`);
165
+ if (!hasComponent(eid, name)) throw new Error(`Entity ${eid} does not have component ${name}`);
166
+ const cid = registry.getID(name);
167
+ bitMasks[eid] &= ~(1n << BigInt(cid));
168
+ }
169
+ function getMask(eid) {
170
+ if (!exists(eid)) throw new Error(`Entity ${eid} does not exist`);
171
+ return bitMasks[eid];
172
+ }
173
+ function tryGetMask(eid) {
174
+ return eid >= 0 && eid < nextID ? bitMasks[eid] : void 0;
175
+ }
176
+ return { exists, hasComponent, create, remove, addComponent, removeComponent, getMask, tryGetMask };
177
+ }
178
+
179
+ // src/utils/ComponentRegistry.ts
180
+ function createComponentRegistry() {
181
+ const nameToID = /* @__PURE__ */ new Map();
182
+ const IDtoName = /* @__PURE__ */ new Map();
183
+ let nextID = 0;
184
+ function register(name) {
185
+ if (!nameToID.has(name)) {
186
+ const eid = nextID++;
187
+ nameToID.set(name, eid);
188
+ IDtoName.set(eid, name);
189
+ }
190
+ }
191
+ function getID(name) {
192
+ const eid = nameToID.get(name);
193
+ if (eid === void 0) throw new Error(`Component ${name} not registered`);
194
+ return eid;
195
+ }
196
+ function getName(cid) {
197
+ const name = IDtoName.get(cid);
198
+ if (!name) throw new Error(`Component ID ${cid} not registered`);
199
+ return name;
200
+ }
201
+ return { register, getID, getName };
202
+ }
203
+
204
+ // src/Ecs.ts
205
+ function ECS() {
206
+ const ComponentRegistry = createComponentRegistry();
207
+ const Entities = EntityManager(ComponentRegistry);
208
+ const internalStores = /* @__PURE__ */ Object.create(null);
209
+ const components = /* @__PURE__ */ Object.create(null);
210
+ const indices = /* @__PURE__ */ Object.create(null);
211
+ function defineComponents(...names) {
212
+ for (const name of names) {
213
+ if (Object.hasOwn(internalStores, name))
214
+ throw new Error(`Component ${String(name)} is already defined`);
215
+ ComponentRegistry.register(name);
216
+ internalStores[name] = ComponentStore();
217
+ components[name] = internalStores[name].getData();
218
+ indices[name] = internalStores[name].getSparse();
219
+ }
220
+ }
221
+ function createEntity() {
222
+ return Entities.create();
223
+ }
224
+ function destroyEntity(eid) {
225
+ const entityMask = Entities.getMask(eid);
226
+ let cid = 0;
227
+ let tempMask = entityMask;
228
+ while (tempMask !== 0n) {
229
+ if ((tempMask & 1n) !== 0n) {
230
+ const name = ComponentRegistry.getName(cid);
231
+ internalStores[name].remove(eid);
232
+ }
233
+ tempMask >>= 1n;
234
+ cid++;
235
+ }
236
+ Entities.remove(eid);
237
+ }
238
+ function addComponent(eid, name, data) {
239
+ Entities.addComponent(eid, name);
240
+ internalStores[name].add(eid, data);
241
+ }
242
+ function removeComponent(eid, name) {
243
+ Entities.removeComponent(eid, name);
244
+ internalStores[name].remove(eid);
245
+ }
246
+ function planQuery(componentName) {
247
+ let targetMask = 0n;
248
+ for (const name of componentName) {
249
+ targetMask |= 1n << BigInt(ComponentRegistry.getID(name));
250
+ }
251
+ let pivot = internalStores[componentName[0]];
252
+ let pivotSize = pivot.getSize();
253
+ for (let i = 1; i < componentName.length; i++) {
254
+ const store = internalStores[componentName[i]];
255
+ const size = store.getSize();
256
+ if (size < pivotSize) {
257
+ pivot = store;
258
+ pivotSize = size;
259
+ }
260
+ }
261
+ return { targetMask, pivot };
262
+ }
263
+ function* query(...componentName) {
264
+ if (componentName.length === 0) return;
265
+ const { targetMask, pivot } = planQuery(componentName);
266
+ const count = componentName.length;
267
+ const sparses = new Array(count);
268
+ for (let i = 0; i < count; i++) sparses[i] = internalStores[componentName[i]].getSparse();
269
+ const entities = [...pivot.getDense()];
270
+ for (let i = 0; i < entities.length; i++) {
271
+ const eid = entities[i];
272
+ const mask = Entities.tryGetMask(eid);
273
+ if (mask === void 0 || (mask & targetMask) !== targetMask) continue;
274
+ const result = {};
275
+ for (let c = 0; c < count; c++) {
276
+ result[componentName[c]] = { id: sparses[c][eid] };
277
+ }
278
+ yield result;
279
+ }
280
+ }
281
+ function queryIds(...componentName) {
282
+ const matches = [];
283
+ if (componentName.length === 0) return matches;
284
+ const { targetMask, pivot } = planQuery(componentName);
285
+ const dense = pivot.getDense();
286
+ for (let i = 0; i < dense.length; i++) {
287
+ const eid = dense[i];
288
+ const mask = Entities.tryGetMask(eid);
289
+ if (mask === void 0 || (mask & targetMask) !== targetMask) continue;
290
+ matches.push(eid);
291
+ }
292
+ return matches;
293
+ }
294
+ return {
295
+ defineComponents,
296
+ createEntity,
297
+ destroyEntity,
298
+ addComponent,
299
+ removeComponent,
300
+ query,
301
+ queryIds,
302
+ components,
303
+ indices
304
+ };
305
+ }
306
+ // Annotate the CommonJS export names for ESM import in node:
307
+ 0 && (module.exports = {
308
+ ECS
309
+ });
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Represents component data in Structure of Arrays (SoA) format.
3
+ * Each property of the component is stored in a separate array,
4
+ * enabling cache-efficient iteration and SIMD operations.
5
+ * Numeric properties are backed by a Float32Array for a compact, homogeneous
6
+ * memory layout; non-numeric properties fall back to a plain JS array.
7
+ * @template T - The component data type
8
+ * @example
9
+ * // For Position component: { x: number, y: number }
10
+ * // ComponentDataArrays would be: { x: Float32Array, y: Float32Array }
11
+ */
12
+ type ComponentDataArrays<T> = {
13
+ [K in keyof T]: T[K] extends number ? Float32Array : T[K][];
14
+ };
15
+
16
+ /**
17
+ * Maps component names to their data arrays for direct access.
18
+ * Used to expose component data to the user without store methods.
19
+ * @template T - Record type defining all component types
20
+ */
21
+ type StoreDataMap<T> = {
22
+ [K in keyof T]: ComponentDataArrays<T[K]>;
23
+ };
24
+
25
+ /**
26
+ * Maps component names to their raw sparse lookup table.
27
+ * Each table translates an entity ID into that entity's index in the component's
28
+ * data arrays, which is what queryIds callers need to read the SoA storage.
29
+ * @template T - Record type defining all component types
30
+ */
31
+ type IndexMap<T> = {
32
+ [K in keyof T]: number[];
33
+ };
34
+
35
+ /**
36
+ * Type helper for the query result.
37
+ * Creates an object with an entry for each requested component, holding its store index.
38
+ * @template C - Array of the component names requested in the query.
39
+ */
40
+ type QueryResult<C extends readonly PropertyKey[]> = {
41
+ [K in C[number]]: { id: number };
42
+ };
43
+
44
+ /**
45
+ * Interface for the main ECS registry.
46
+ * Provides high-level API for entity-component management,
47
+ * coordinating between EntityManager and ComponentStores.
48
+ * @template T - Record type defining all component types in the ECS
49
+ */
50
+ interface IECS<T> {
51
+ /**
52
+ * Initializes the ECS with component definitions.
53
+ * Registers all components and creates their corresponding stores.
54
+ * Must be called before any other operations.
55
+ * @param names -component names
56
+ * @throws Error if a component name is already defined
57
+ */
58
+ defineComponents<K extends readonly (keyof T)[]>(...names: K): void;
59
+
60
+ /**
61
+ * Creates a new entity in the ECS.
62
+ * @returns The ID of the newly created entity
63
+ */
64
+ createEntity: () => number;
65
+
66
+ /**
67
+ * Completely removes an entity and all its components from the ECS.
68
+ * Cleans up all component data and entity records.
69
+ * @param eid - The entity ID to destroy
70
+ */
71
+ destroyEntity: (eid: number) => void;
72
+
73
+ /**
74
+ * Adds a component to an entity with the provided data.
75
+ * Updates both the entity's bitmask and the component store.
76
+ * @template K - The component name type (key of T)
77
+ * @param eid - The entity ID
78
+ * @param name - The component name
79
+ * @param data - The component data to store
80
+ */
81
+ addComponent: <K extends keyof T>(eid: number, name: K, data: T[K]) => void;
82
+
83
+ /**
84
+ * Removes a component from an entity.
85
+ * Updates both the entity's bitmask and the component store.
86
+ * @template K - The component name type (key of T)
87
+ * @param eid - The entity ID
88
+ * @param name - The component name
89
+ */
90
+ removeComponent: <K extends keyof T>(eid: number, name: K) => void;
91
+
92
+ /**
93
+ * Retrieves all entities that contain the specified set of components.
94
+ * Performs a filtered search across component stores and yields each matching entity.
95
+ * The returned generator allows efficient iteration without allocating large arrays.
96
+ *
97
+ * @template C - Array of component names to query for
98
+ * @param components - List of component names that the entity must include
99
+ * @returns A generator that yields an object for each matching entity,
100
+ * containing the entity ID and the related component data
101
+ */
102
+ query: <C extends (keyof T)[]>(...componentName: C) => Generator<QueryResult<C>>;
103
+
104
+ /**
105
+ * Allocation-free counterpart to query, for hot systems.
106
+ * Returns the IDs of every entity holding all the specified components, instead of
107
+ * yielding one result object plus one { id } object per component per entity.
108
+ * Translate an entity ID into a storage index through `indices`:
109
+ *
110
+ * ```ts
111
+ * const { Position, Velocity } = World.components;
112
+ * const pos = World.indices.Position;
113
+ * const vel = World.indices.Velocity;
114
+ *
115
+ * for (const eid of World.queryIds('Position', 'Velocity')) {
116
+ * Position.x[pos[eid]] += Velocity.x[vel[eid]];
117
+ * }
118
+ * ```
119
+ *
120
+ * The array is complete before it is returned, so adding or removing components while
121
+ * looping over it cannot skip an entity. Destroying an entity mid-loop does invalidate
122
+ * its storage index, so guard that case yourself.
123
+ *
124
+ * @template C - Array of component names to query for
125
+ * @param componentName - List of component names that the entity must include
126
+ * @returns A fresh array of matching entity IDs
127
+ */
128
+ queryIds: <C extends (keyof T)[]>(...componentName: C) => number[];
129
+
130
+ components: StoreDataMap<T>;
131
+
132
+ /**
133
+ * Maps each component name to its sparse lookup table, translating an entity ID into
134
+ * that entity's index in the component's data arrays. Needed to read `components`
135
+ * when iterating the output of queryIds.
136
+ */
137
+ indices: IndexMap<T>;
138
+ }
139
+
140
+ declare function ECS<T extends Record<string, Record<string, any>>>(): IECS<T>;
141
+
142
+ export { ECS };
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Represents component data in Structure of Arrays (SoA) format.
3
+ * Each property of the component is stored in a separate array,
4
+ * enabling cache-efficient iteration and SIMD operations.
5
+ * Numeric properties are backed by a Float32Array for a compact, homogeneous
6
+ * memory layout; non-numeric properties fall back to a plain JS array.
7
+ * @template T - The component data type
8
+ * @example
9
+ * // For Position component: { x: number, y: number }
10
+ * // ComponentDataArrays would be: { x: Float32Array, y: Float32Array }
11
+ */
12
+ type ComponentDataArrays<T> = {
13
+ [K in keyof T]: T[K] extends number ? Float32Array : T[K][];
14
+ };
15
+
16
+ /**
17
+ * Maps component names to their data arrays for direct access.
18
+ * Used to expose component data to the user without store methods.
19
+ * @template T - Record type defining all component types
20
+ */
21
+ type StoreDataMap<T> = {
22
+ [K in keyof T]: ComponentDataArrays<T[K]>;
23
+ };
24
+
25
+ /**
26
+ * Maps component names to their raw sparse lookup table.
27
+ * Each table translates an entity ID into that entity's index in the component's
28
+ * data arrays, which is what queryIds callers need to read the SoA storage.
29
+ * @template T - Record type defining all component types
30
+ */
31
+ type IndexMap<T> = {
32
+ [K in keyof T]: number[];
33
+ };
34
+
35
+ /**
36
+ * Type helper for the query result.
37
+ * Creates an object with an entry for each requested component, holding its store index.
38
+ * @template C - Array of the component names requested in the query.
39
+ */
40
+ type QueryResult<C extends readonly PropertyKey[]> = {
41
+ [K in C[number]]: { id: number };
42
+ };
43
+
44
+ /**
45
+ * Interface for the main ECS registry.
46
+ * Provides high-level API for entity-component management,
47
+ * coordinating between EntityManager and ComponentStores.
48
+ * @template T - Record type defining all component types in the ECS
49
+ */
50
+ interface IECS<T> {
51
+ /**
52
+ * Initializes the ECS with component definitions.
53
+ * Registers all components and creates their corresponding stores.
54
+ * Must be called before any other operations.
55
+ * @param names -component names
56
+ * @throws Error if a component name is already defined
57
+ */
58
+ defineComponents<K extends readonly (keyof T)[]>(...names: K): void;
59
+
60
+ /**
61
+ * Creates a new entity in the ECS.
62
+ * @returns The ID of the newly created entity
63
+ */
64
+ createEntity: () => number;
65
+
66
+ /**
67
+ * Completely removes an entity and all its components from the ECS.
68
+ * Cleans up all component data and entity records.
69
+ * @param eid - The entity ID to destroy
70
+ */
71
+ destroyEntity: (eid: number) => void;
72
+
73
+ /**
74
+ * Adds a component to an entity with the provided data.
75
+ * Updates both the entity's bitmask and the component store.
76
+ * @template K - The component name type (key of T)
77
+ * @param eid - The entity ID
78
+ * @param name - The component name
79
+ * @param data - The component data to store
80
+ */
81
+ addComponent: <K extends keyof T>(eid: number, name: K, data: T[K]) => void;
82
+
83
+ /**
84
+ * Removes a component from an entity.
85
+ * Updates both the entity's bitmask and the component store.
86
+ * @template K - The component name type (key of T)
87
+ * @param eid - The entity ID
88
+ * @param name - The component name
89
+ */
90
+ removeComponent: <K extends keyof T>(eid: number, name: K) => void;
91
+
92
+ /**
93
+ * Retrieves all entities that contain the specified set of components.
94
+ * Performs a filtered search across component stores and yields each matching entity.
95
+ * The returned generator allows efficient iteration without allocating large arrays.
96
+ *
97
+ * @template C - Array of component names to query for
98
+ * @param components - List of component names that the entity must include
99
+ * @returns A generator that yields an object for each matching entity,
100
+ * containing the entity ID and the related component data
101
+ */
102
+ query: <C extends (keyof T)[]>(...componentName: C) => Generator<QueryResult<C>>;
103
+
104
+ /**
105
+ * Allocation-free counterpart to query, for hot systems.
106
+ * Returns the IDs of every entity holding all the specified components, instead of
107
+ * yielding one result object plus one { id } object per component per entity.
108
+ * Translate an entity ID into a storage index through `indices`:
109
+ *
110
+ * ```ts
111
+ * const { Position, Velocity } = World.components;
112
+ * const pos = World.indices.Position;
113
+ * const vel = World.indices.Velocity;
114
+ *
115
+ * for (const eid of World.queryIds('Position', 'Velocity')) {
116
+ * Position.x[pos[eid]] += Velocity.x[vel[eid]];
117
+ * }
118
+ * ```
119
+ *
120
+ * The array is complete before it is returned, so adding or removing components while
121
+ * looping over it cannot skip an entity. Destroying an entity mid-loop does invalidate
122
+ * its storage index, so guard that case yourself.
123
+ *
124
+ * @template C - Array of component names to query for
125
+ * @param componentName - List of component names that the entity must include
126
+ * @returns A fresh array of matching entity IDs
127
+ */
128
+ queryIds: <C extends (keyof T)[]>(...componentName: C) => number[];
129
+
130
+ components: StoreDataMap<T>;
131
+
132
+ /**
133
+ * Maps each component name to its sparse lookup table, translating an entity ID into
134
+ * that entity's index in the component's data arrays. Needed to read `components`
135
+ * when iterating the output of queryIds.
136
+ */
137
+ indices: IndexMap<T>;
138
+ }
139
+
140
+ declare function ECS<T extends Record<string, Record<string, any>>>(): IECS<T>;
141
+
142
+ export { ECS };