@codehz/ecs 0.3.7 → 0.3.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/index.mjs ADDED
@@ -0,0 +1,1664 @@
1
+ //#region src/entity.ts
2
+ /**
3
+ * Constants for ID ranges
4
+ */
5
+ const INVALID_COMPONENT_ID = 0;
6
+ const COMPONENT_ID_MAX = 1023;
7
+ const ENTITY_ID_START = 1024;
8
+ /**
9
+ * Constants for relation ID encoding
10
+ */
11
+ const RELATION_SHIFT = 2 ** 42;
12
+ const WILDCARD_TARGET_ID = 0;
13
+ /**
14
+ * Create a component ID
15
+ * @param id Component identifier (1-1023)
16
+ * @see component
17
+ */
18
+ function createComponentId(id) {
19
+ if (id < 1 || id > COMPONENT_ID_MAX) throw new Error(`Component ID must be between 1 and ${COMPONENT_ID_MAX}`);
20
+ return id;
21
+ }
22
+ /**
23
+ * Create an entity ID
24
+ * @param id Entity identifier (starting from 1024)
25
+ */
26
+ function createEntityId(id) {
27
+ if (id < ENTITY_ID_START) throw new Error(`Entity ID must be ${ENTITY_ID_START} or greater`);
28
+ return id;
29
+ }
30
+ function relation(componentId, targetId) {
31
+ if (!isComponentId(componentId)) throw new Error("First argument must be a valid component ID");
32
+ let actualTargetId;
33
+ if (targetId === "*") actualTargetId = WILDCARD_TARGET_ID;
34
+ else {
35
+ if (!isEntityId(targetId) && !isComponentId(targetId)) throw new Error("Second argument must be a valid entity ID, component ID, or '*'");
36
+ actualTargetId = targetId;
37
+ }
38
+ return -(componentId * RELATION_SHIFT + actualTargetId);
39
+ }
40
+ /**
41
+ * Check if an ID is a component ID
42
+ */
43
+ function isComponentId(id) {
44
+ return id >= 1 && id <= COMPONENT_ID_MAX;
45
+ }
46
+ /**
47
+ * Check if an ID is an entity ID
48
+ */
49
+ function isEntityId(id) {
50
+ return id >= ENTITY_ID_START;
51
+ }
52
+ /**
53
+ * Check if an ID is a relation ID
54
+ */
55
+ function isRelationId(id) {
56
+ return id < 0;
57
+ }
58
+ /**
59
+ * Check if an ID is a wildcard relation id
60
+ */
61
+ function isWildcardRelationId(id) {
62
+ if (!isRelationId(id)) return false;
63
+ return -id % RELATION_SHIFT === WILDCARD_TARGET_ID;
64
+ }
65
+ /**
66
+ * Decode a relation ID into component and target IDs
67
+ * @param relationId The relation ID (must be negative)
68
+ * @returns Object with componentId, targetId, and relation type
69
+ */
70
+ function decodeRelationId(relationId) {
71
+ if (!isRelationId(relationId)) throw new Error("ID is not a relation ID");
72
+ const absId = -relationId;
73
+ const componentId = Math.floor(absId / RELATION_SHIFT);
74
+ const targetId = absId % RELATION_SHIFT;
75
+ if (targetId === WILDCARD_TARGET_ID) return {
76
+ componentId,
77
+ targetId,
78
+ type: "wildcard"
79
+ };
80
+ else if (isEntityId(targetId)) return {
81
+ componentId,
82
+ targetId,
83
+ type: "entity"
84
+ };
85
+ else if (isComponentId(targetId)) return {
86
+ componentId,
87
+ targetId,
88
+ type: "component"
89
+ };
90
+ else throw new Error("Invalid target ID in relation");
91
+ }
92
+ /**
93
+ * Get the string representation of an ID type
94
+ */
95
+ function getIdType(id) {
96
+ if (isComponentId(id)) return "component";
97
+ if (isEntityId(id)) return "entity";
98
+ if (isRelationId(id)) try {
99
+ const decoded = decodeRelationId(id);
100
+ if (!isComponentId(decoded.componentId) || decoded.type !== "wildcard" && !isEntityId(decoded.targetId) && !isComponentId(decoded.targetId)) return "invalid";
101
+ switch (decoded.type) {
102
+ case "entity": return "entity-relation";
103
+ case "component": return "component-relation";
104
+ case "wildcard": return "wildcard-relation";
105
+ }
106
+ } catch (error) {
107
+ return "invalid";
108
+ }
109
+ return "invalid";
110
+ }
111
+ /**
112
+ * Get detailed type information for an EntityId
113
+ * @param id The EntityId to analyze
114
+ * @returns Detailed type information including relation subtypes
115
+ */
116
+ function getDetailedIdType(id) {
117
+ if (isComponentId(id)) return { type: "component" };
118
+ if (isEntityId(id)) return { type: "entity" };
119
+ if (isRelationId(id)) try {
120
+ const decoded = decodeRelationId(id);
121
+ if (!isComponentId(decoded.componentId) || decoded.type !== "wildcard" && !isEntityId(decoded.targetId) && !isComponentId(decoded.targetId)) return { type: "invalid" };
122
+ let type;
123
+ switch (decoded.type) {
124
+ case "entity":
125
+ type = "entity-relation";
126
+ break;
127
+ case "component":
128
+ type = "component-relation";
129
+ break;
130
+ case "wildcard":
131
+ type = "wildcard-relation";
132
+ break;
133
+ }
134
+ return {
135
+ type,
136
+ componentId: decoded.componentId,
137
+ targetId: decoded.targetId
138
+ };
139
+ } catch (error) {
140
+ return { type: "invalid" };
141
+ }
142
+ return { type: "invalid" };
143
+ }
144
+ /**
145
+ * Inspect an EntityId and return a human-readable string representation
146
+ * @param id The EntityId to inspect
147
+ * @returns A friendly string representation of the ID
148
+ */
149
+ function inspectEntityId(id) {
150
+ if (id === INVALID_COMPONENT_ID) return "Invalid Component ID (0)";
151
+ if (isComponentId(id)) return `Component ID (${id})`;
152
+ if (isEntityId(id)) return `Entity ID (${id})`;
153
+ if (isRelationId(id)) try {
154
+ const decoded = decodeRelationId(id);
155
+ if (!isComponentId(decoded.componentId) || decoded.type !== "wildcard" && !isEntityId(decoded.targetId) && !isComponentId(decoded.targetId)) return `Invalid Relation ID (${id})`;
156
+ return `Relation ID: ${`Component ID (${decoded.componentId})`} -> ${decoded.type === "entity" ? `Entity ID (${decoded.targetId})` : decoded.type === "component" ? `Component ID (${decoded.targetId})` : "Wildcard (*)"}`;
157
+ } catch (error) {
158
+ return `Invalid Relation ID (${id})`;
159
+ }
160
+ return `Unknown ID (${id})`;
161
+ }
162
+ /**
163
+ * Entity ID Manager for automatic allocation and freelist recycling
164
+ */
165
+ var EntityIdManager = class {
166
+ nextId = ENTITY_ID_START;
167
+ freelist = /* @__PURE__ */ new Set();
168
+ /**
169
+ * Allocate a new entity ID
170
+ * Uses freelist if available, otherwise increments counter
171
+ */
172
+ allocate() {
173
+ if (this.freelist.size > 0) {
174
+ const id = this.freelist.values().next().value;
175
+ this.freelist.delete(id);
176
+ return id;
177
+ } else {
178
+ const id = this.nextId;
179
+ this.nextId++;
180
+ if (this.nextId >= Number.MAX_SAFE_INTEGER) throw new Error("Entity ID overflow: reached maximum safe integer");
181
+ return id;
182
+ }
183
+ }
184
+ /**
185
+ * Deallocate an entity ID, adding it to the freelist for reuse
186
+ * @param id The entity ID to deallocate
187
+ */
188
+ deallocate(id) {
189
+ if (!isEntityId(id)) throw new Error("Can only deallocate valid entity IDs");
190
+ if (id >= this.nextId) throw new Error("Cannot deallocate an ID that was never allocated");
191
+ this.freelist.add(id);
192
+ }
193
+ /**
194
+ * Get the current freelist size (for debugging/monitoring)
195
+ */
196
+ getFreelistSize() {
197
+ return this.freelist.size;
198
+ }
199
+ /**
200
+ * Get the next ID that would be allocated (for debugging)
201
+ */
202
+ getNextId() {
203
+ return this.nextId;
204
+ }
205
+ /**
206
+ * Serialize internal state for persistence.
207
+ * Returns a plain object representing allocator state. Values may be non-JSON-serializable.
208
+ */
209
+ serializeState() {
210
+ return {
211
+ nextId: this.nextId,
212
+ freelist: Array.from(this.freelist)
213
+ };
214
+ }
215
+ /**
216
+ * Restore internal state from a previously-serialized object.
217
+ * Overwrites the current nextId and freelist.
218
+ */
219
+ deserializeState(state) {
220
+ if (typeof state.nextId !== "number") throw new Error("Invalid state for EntityIdManager.deserializeState");
221
+ this.nextId = state.nextId;
222
+ this.freelist = new Set(state.freelist || []);
223
+ }
224
+ };
225
+ /**
226
+ * Component ID Manager for automatic allocation
227
+ * Components are typically registered once and not recycled
228
+ */
229
+ var ComponentIdAllocator = class {
230
+ nextId = 1;
231
+ /**
232
+ * Allocate a new component ID
233
+ * Increments counter sequentially from 1
234
+ */
235
+ allocate() {
236
+ if (this.nextId > COMPONENT_ID_MAX) throw new Error(`Component ID overflow: maximum ${COMPONENT_ID_MAX} components allowed`);
237
+ const id = this.nextId;
238
+ this.nextId++;
239
+ return id;
240
+ }
241
+ /**
242
+ * Get the next ID that would be allocated (for debugging)
243
+ */
244
+ getNextId() {
245
+ return this.nextId;
246
+ }
247
+ /**
248
+ * Check if more component IDs are available
249
+ */
250
+ hasAvailableIds() {
251
+ return this.nextId <= COMPONENT_ID_MAX;
252
+ }
253
+ };
254
+ const globalComponentIdAllocator = new ComponentIdAllocator();
255
+ const ComponentNames = /* @__PURE__ */ new Map();
256
+ const ComponentIdForNames = /* @__PURE__ */ new Map();
257
+ /**
258
+ * Allocate a new component ID from the global allocator.
259
+ * Optionally register a name for the component.
260
+ * The name is only for serialization/debugging and does not affect base functionality.
261
+ * @param name Optional name for the component
262
+ * @returns The allocated component ID
263
+ */
264
+ function component(name) {
265
+ const id = globalComponentIdAllocator.allocate();
266
+ if (name) {
267
+ if (ComponentIdForNames.has(name)) throw new Error(`Component name "${name}" is already registered`);
268
+ ComponentNames.set(id, name);
269
+ ComponentIdForNames.set(name, id);
270
+ }
271
+ return id;
272
+ }
273
+ /**
274
+ * Get a component ID by its registered name
275
+ * @param name The component name
276
+ * @returns The component ID if found, undefined otherwise
277
+ */
278
+ function getComponentIdByName(name) {
279
+ return ComponentIdForNames.get(name);
280
+ }
281
+ /** Get a component name by its ID
282
+ * @param id The component ID
283
+ * @returns The component name if found, undefined otherwise
284
+ */
285
+ function getComponentNameById(id) {
286
+ return ComponentNames.get(id);
287
+ }
288
+
289
+ //#endregion
290
+ //#region src/types.ts
291
+ function isOptionalEntityId(type) {
292
+ return typeof type === "object" && type !== null && "optional" in type;
293
+ }
294
+
295
+ //#endregion
296
+ //#region src/utils.ts
297
+ /**
298
+ * Utility functions for ECS library
299
+ */
300
+ /**
301
+ * Get a value from cache or compute and cache it if not present
302
+ * @param cache The cache map
303
+ * @param key The cache key
304
+ * @param compute Function to compute the value if not cached
305
+ * @returns The cached or computed value
306
+ */
307
+ function getOrComputeCache(cache, key, compute) {
308
+ let value = cache.get(key);
309
+ if (value === void 0) {
310
+ value = compute();
311
+ cache.set(key, value);
312
+ }
313
+ return value;
314
+ }
315
+ /**
316
+ * Get a value from cache or create and cache it if not present, allowing side effects during creation
317
+ * @param cache The cache map
318
+ * @param key The cache key
319
+ * @param create Function to create the value if not cached (can have side effects)
320
+ * @returns The cached or created value
321
+ */
322
+ function getOrCreateWithSideEffect(cache, key, create) {
323
+ let value = cache.get(key);
324
+ if (value === void 0) {
325
+ value = create();
326
+ cache.set(key, value);
327
+ }
328
+ return value;
329
+ }
330
+
331
+ //#endregion
332
+ //#region src/archetype.ts
333
+ /**
334
+ * Special value to represent missing component data
335
+ */
336
+ const MISSING_COMPONENT = Symbol("missing component");
337
+ /**
338
+ * Archetype class for ECS architecture
339
+ * Represents a group of entities that share the same set of components
340
+ * Optimized for fast iteration and component access
341
+ */
342
+ var Archetype = class {
343
+ /**
344
+ * The component types that define this archetype
345
+ */
346
+ componentTypes;
347
+ /**
348
+ * List of entities in this archetype
349
+ */
350
+ entities = [];
351
+ /**
352
+ * Component data storage - maps component type to array of component data
353
+ * Each array index corresponds to the entity index in the entities array
354
+ */
355
+ componentData = /* @__PURE__ */ new Map();
356
+ /**
357
+ * Reverse mapping from entity to its index in this archetype
358
+ */
359
+ entityToIndex = /* @__PURE__ */ new Map();
360
+ /**
361
+ * Cache for pre-computed component data sources to avoid repeated calculations
362
+ * For regular components: data array
363
+ * For wildcards: matching relation types array
364
+ */
365
+ componentDataSourcesCache = /* @__PURE__ */ new Map();
366
+ /**
367
+ * Create a new archetype with the specified component types
368
+ * @param componentTypes The component types that define this archetype
369
+ */
370
+ constructor(componentTypes) {
371
+ this.componentTypes = [...componentTypes].sort((a, b) => a - b);
372
+ for (const componentType of this.componentTypes) this.componentData.set(componentType, []);
373
+ }
374
+ /**
375
+ * Get the number of entities in this archetype
376
+ */
377
+ get size() {
378
+ return this.entities.length;
379
+ }
380
+ /**
381
+ * Check if this archetype matches the given component types
382
+ * @param componentTypes The component types to check
383
+ */
384
+ matches(componentTypes) {
385
+ if (this.componentTypes.length !== componentTypes.length) return false;
386
+ const sortedTypes = [...componentTypes].sort((a, b) => a - b);
387
+ return this.componentTypes.every((type, index) => type === sortedTypes[index]);
388
+ }
389
+ /**
390
+ * Add an entity to this archetype with initial component data
391
+ * @param entityId The entity to add
392
+ * @param componentData Map of component type to component data
393
+ */
394
+ addEntity(entityId, componentData) {
395
+ if (this.entityToIndex.has(entityId)) throw new Error(`Entity ${entityId} is already in this archetype`);
396
+ const index = this.entities.length;
397
+ this.entities.push(entityId);
398
+ this.entityToIndex.set(entityId, index);
399
+ for (const componentType of this.componentTypes) {
400
+ const data = componentData.get(componentType);
401
+ this.getComponentData(componentType).push(data === void 0 ? MISSING_COMPONENT : data);
402
+ }
403
+ }
404
+ /**
405
+ * Get all component data for a specific entity
406
+ * @param entityId The entity to get data for
407
+ * @returns Map of component type to component data
408
+ */
409
+ getEntity(entityId) {
410
+ const index = this.entityToIndex.get(entityId);
411
+ if (index === void 0) return;
412
+ const entityData = /* @__PURE__ */ new Map();
413
+ for (const componentType of this.componentTypes) {
414
+ const data = this.getComponentData(componentType)[index];
415
+ entityData.set(componentType, data === MISSING_COMPONENT ? void 0 : data);
416
+ }
417
+ return entityData;
418
+ }
419
+ /**
420
+ * Dump all entities and their component data in this archetype
421
+ * @returns Array of objects with entity and component data
422
+ */
423
+ dump() {
424
+ const result = [];
425
+ for (let i = 0; i < this.entities.length; i++) {
426
+ const entity = this.entities[i];
427
+ const components = /* @__PURE__ */ new Map();
428
+ for (const componentType of this.componentTypes) {
429
+ const data = this.getComponentData(componentType)[i];
430
+ components.set(componentType, data === MISSING_COMPONENT ? void 0 : data);
431
+ }
432
+ result.push({
433
+ entity,
434
+ components
435
+ });
436
+ }
437
+ return result;
438
+ }
439
+ /**
440
+ * Remove an entity from this archetype
441
+ * @param entityId The entity to remove
442
+ * @returns The component data of the removed entity
443
+ */
444
+ removeEntity(entityId) {
445
+ const index = this.entityToIndex.get(entityId);
446
+ if (index === void 0) return;
447
+ const removedData = /* @__PURE__ */ new Map();
448
+ for (const componentType of this.componentTypes) {
449
+ const dataArray = this.getComponentData(componentType);
450
+ removedData.set(componentType, dataArray[index]);
451
+ }
452
+ this.entityToIndex.delete(entityId);
453
+ const lastIndex = this.entities.length - 1;
454
+ if (index !== lastIndex) {
455
+ const lastEntity = this.entities[lastIndex];
456
+ this.entities[index] = lastEntity;
457
+ this.entityToIndex.set(lastEntity, index);
458
+ for (const componentType of this.componentTypes) {
459
+ const dataArray = this.getComponentData(componentType);
460
+ dataArray[index] = dataArray[lastIndex];
461
+ }
462
+ }
463
+ this.entities.pop();
464
+ for (const componentType of this.componentTypes) this.getComponentData(componentType).pop();
465
+ return removedData;
466
+ }
467
+ /**
468
+ * Check if an entity is in this archetype
469
+ * @param entityId The entity to check
470
+ */
471
+ exists(entityId) {
472
+ return this.entityToIndex.has(entityId);
473
+ }
474
+ get(entityId, componentType) {
475
+ const index = this.entityToIndex.get(entityId);
476
+ if (index === void 0) throw new Error(`Entity ${entityId} is not in this archetype`);
477
+ if (isWildcardRelationId(componentType)) {
478
+ const componentId = decodeRelationId(componentType).componentId;
479
+ const relations = [];
480
+ for (const relType of this.componentTypes) {
481
+ const relDetailed = getDetailedIdType(relType);
482
+ if ((relDetailed.type === "entity-relation" || relDetailed.type === "component-relation") && relDetailed.componentId === componentId) {
483
+ const dataArray = this.getComponentData(relType);
484
+ if (dataArray && dataArray[index] !== void 0) {
485
+ const data = dataArray[index];
486
+ relations.push([relDetailed.targetId, data === MISSING_COMPONENT ? void 0 : data]);
487
+ }
488
+ }
489
+ }
490
+ return relations;
491
+ } else {
492
+ const data = this.getComponentData(componentType)[index];
493
+ return data === MISSING_COMPONENT ? void 0 : data;
494
+ }
495
+ }
496
+ /**
497
+ * Set component data for a specific entity and component type
498
+ * @param entityId The entity
499
+ * @param componentType The component type
500
+ * @param data The component data
501
+ */
502
+ set(entityId, componentType, data) {
503
+ if (!this.componentData.has(componentType)) throw new Error(`Component type ${componentType} is not in this archetype`);
504
+ const index = this.entityToIndex.get(entityId);
505
+ if (index === void 0) throw new Error(`Entity ${entityId} is not in this archetype`);
506
+ const dataArray = this.getComponentData(componentType);
507
+ dataArray[index] = data;
508
+ }
509
+ /**
510
+ * Get all entities in this archetype
511
+ */
512
+ getEntities() {
513
+ return this.entities;
514
+ }
515
+ /**
516
+ * Get the mapping of entities to their indices in this archetype
517
+ */
518
+ getEntityToIndexMap() {
519
+ return this.entityToIndex;
520
+ }
521
+ /**
522
+ * Get component data for all entities of a specific component type
523
+ * @param componentType The component type
524
+ */
525
+ getComponentData(componentType) {
526
+ const data = this.componentData.get(componentType);
527
+ if (!data) throw new Error(`Component type ${componentType} is not in this archetype`);
528
+ return data;
529
+ }
530
+ /**
531
+ * Get optional component data for all entities of a specific component type
532
+ * @param componentType The component type
533
+ * @returns An array of component data or undefined if not present
534
+ */
535
+ getOptionalComponentData(componentType) {
536
+ return this.componentData.get(componentType);
537
+ }
538
+ /**
539
+ * Helper: compute or return cached data sources for provided componentTypes
540
+ */
541
+ getCachedComponentDataSources(componentTypes) {
542
+ const cacheKey = componentTypes.map((id) => isOptionalEntityId(id) ? `opt(${id.optional})` : `${id}`).join(",");
543
+ return getOrComputeCache(this.componentDataSourcesCache, cacheKey, () => {
544
+ return componentTypes.map((compType) => {
545
+ let optional = false;
546
+ if (isOptionalEntityId(compType)) {
547
+ compType = compType.optional;
548
+ optional = true;
549
+ }
550
+ const detailedType = getDetailedIdType(compType);
551
+ if (detailedType.type === "wildcard-relation") {
552
+ const componentId = detailedType.componentId;
553
+ const matchingRelations = this.componentTypes.filter((ct) => {
554
+ const detailedCt = getDetailedIdType(ct);
555
+ if (detailedCt.type !== "entity-relation" && detailedCt.type !== "component-relation") return false;
556
+ return detailedCt.componentId === componentId;
557
+ });
558
+ return optional ? matchingRelations.length > 0 ? matchingRelations : void 0 : matchingRelations;
559
+ } else return optional ? this.getOptionalComponentData(compType) : this.getComponentData(compType);
560
+ });
561
+ });
562
+ }
563
+ /**
564
+ * Helper: build component tuples for a specific entity index using precomputed data sources
565
+ */
566
+ buildComponentsForIndex(componentTypes, componentDataSources, entityIndex) {
567
+ return componentDataSources.map((dataSource, i) => {
568
+ let compType = componentTypes[i];
569
+ let optional = false;
570
+ if (isOptionalEntityId(compType)) {
571
+ compType = compType.optional;
572
+ optional = true;
573
+ }
574
+ if (getIdType(compType) === "wildcard-relation") {
575
+ if (dataSource === void 0) if (optional) return;
576
+ else throw new Error(`No matching relations found for mandatory wildcard relation component type`);
577
+ const matchingRelations = dataSource;
578
+ const relations = [];
579
+ for (const relType of matchingRelations) {
580
+ const data = this.getComponentData(relType)[entityIndex];
581
+ const decodedRel = decodeRelationId(relType);
582
+ relations.push([decodedRel.targetId, data === MISSING_COMPONENT ? void 0 : data]);
583
+ }
584
+ return optional ? { value: relations } : relations;
585
+ } else {
586
+ if (dataSource === void 0) if (optional) return;
587
+ else throw new Error(`No matching relations found for mandatory wildcard relation component type`);
588
+ const data = dataSource[entityIndex];
589
+ const result = data === MISSING_COMPONENT ? void 0 : data;
590
+ return optional ? { value: result } : result;
591
+ }
592
+ });
593
+ }
594
+ /**
595
+ * Get entities with their component data for specified component types
596
+ * Optimized for bulk component access with pre-computed indices
597
+ * @param componentTypes Array of component types to retrieve
598
+ * @returns Array of objects with entity and component data
599
+ */
600
+ getEntitiesWithComponents(componentTypes) {
601
+ const result = [];
602
+ this.forEachWithComponents(componentTypes, (entity, ...components) => {
603
+ result.push({
604
+ entity,
605
+ components
606
+ });
607
+ });
608
+ return result;
609
+ }
610
+ /**
611
+ * Iterate over entities with their component data for specified component types
612
+ * implemented as a generator returning each entity/components pair lazily
613
+ * @param componentTypes Array of component types to retrieve
614
+ */
615
+ *iterateWithComponents(componentTypes) {
616
+ const componentDataSources = this.getCachedComponentDataSources(componentTypes);
617
+ for (let entityIndex = 0; entityIndex < this.entities.length; entityIndex++) yield [this.entities[entityIndex], ...this.buildComponentsForIndex(componentTypes, componentDataSources, entityIndex)];
618
+ }
619
+ /**
620
+ * Iterate over entities with their component data for specified component types
621
+ * Optimized for bulk component access
622
+ * @param componentTypes Array of component types to retrieve
623
+ * @param callback Function called for each entity with its components
624
+ */
625
+ forEachWithComponents(componentTypes, callback) {
626
+ const componentDataSources = this.getCachedComponentDataSources(componentTypes);
627
+ for (let entityIndex = 0; entityIndex < this.entities.length; entityIndex++) {
628
+ const entity = this.entities[entityIndex];
629
+ callback(entity, ...this.buildComponentsForIndex(componentTypes, componentDataSources, entityIndex));
630
+ }
631
+ }
632
+ /**
633
+ * Iterate over all entities with their component data
634
+ * @param callback Function called for each entity with its component data
635
+ */
636
+ forEach(callback) {
637
+ for (let i = 0; i < this.entities.length; i++) {
638
+ const components = /* @__PURE__ */ new Map();
639
+ for (const componentType of this.componentTypes) {
640
+ const data = this.getComponentData(componentType)[i];
641
+ components.set(componentType, data === MISSING_COMPONENT ? void 0 : data);
642
+ }
643
+ callback(this.entities[i], components);
644
+ }
645
+ }
646
+ };
647
+
648
+ //#endregion
649
+ //#region src/changeset.ts
650
+ /**
651
+ * @internal Represents a set of component changes to be applied to an entity
652
+ */
653
+ var ComponentChangeset = class {
654
+ adds = /* @__PURE__ */ new Map();
655
+ removes = /* @__PURE__ */ new Set();
656
+ /**
657
+ * Add a component to the changeset
658
+ */
659
+ set(componentType, component$1) {
660
+ this.adds.set(componentType, component$1);
661
+ this.removes.delete(componentType);
662
+ }
663
+ /**
664
+ * Remove a component from the changeset
665
+ */
666
+ delete(componentType) {
667
+ this.removes.add(componentType);
668
+ this.adds.delete(componentType);
669
+ }
670
+ /**
671
+ * Check if the changeset has any changes
672
+ */
673
+ hasChanges() {
674
+ return this.adds.size > 0 || this.removes.size > 0;
675
+ }
676
+ /**
677
+ * Clear all changes
678
+ */
679
+ clear() {
680
+ this.adds.clear();
681
+ this.removes.clear();
682
+ }
683
+ /**
684
+ * Merge another changeset into this one
685
+ */
686
+ merge(other) {
687
+ for (const [componentType, component$1] of other.adds) {
688
+ this.adds.set(componentType, component$1);
689
+ this.removes.delete(componentType);
690
+ }
691
+ for (const componentType of other.removes) {
692
+ this.removes.add(componentType);
693
+ this.adds.delete(componentType);
694
+ }
695
+ }
696
+ /**
697
+ * Apply the changeset to existing components and return the final state
698
+ */
699
+ applyTo(existingComponents) {
700
+ for (const componentType of this.removes) existingComponents.delete(componentType);
701
+ for (const [componentType, component$1] of this.adds) existingComponents.set(componentType, component$1);
702
+ return existingComponents;
703
+ }
704
+ /**
705
+ * Get the final component types after applying the changeset
706
+ * @param existingComponentTypes - The current component types on the entity
707
+ * @returns The final component types or undefined if no changes
708
+ */
709
+ getFinalComponentTypes(existingComponentTypes) {
710
+ const finalComponentTypes = new Set(existingComponentTypes);
711
+ let changed = false;
712
+ for (const componentType of this.removes) {
713
+ if (!finalComponentTypes.has(componentType)) {
714
+ this.removes.delete(componentType);
715
+ continue;
716
+ }
717
+ changed = true;
718
+ finalComponentTypes.delete(componentType);
719
+ }
720
+ for (const componentType of this.adds.keys()) {
721
+ if (finalComponentTypes.has(componentType)) continue;
722
+ changed = true;
723
+ finalComponentTypes.add(componentType);
724
+ }
725
+ return changed ? Array.from(finalComponentTypes) : void 0;
726
+ }
727
+ };
728
+
729
+ //#endregion
730
+ //#region src/command-buffer.ts
731
+ /**
732
+ * Command buffer for deferred structural changes
733
+ */
734
+ var CommandBuffer = class {
735
+ commands = [];
736
+ executeEntityCommands;
737
+ /**
738
+ * Create a command buffer with an executor function
739
+ */
740
+ constructor(executeEntityCommands) {
741
+ this.executeEntityCommands = executeEntityCommands;
742
+ }
743
+ set(entityId, componentType, component$1) {
744
+ this.commands.push({
745
+ type: "set",
746
+ entityId,
747
+ componentType,
748
+ component: component$1
749
+ });
750
+ }
751
+ /**
752
+ * Remove a component from an entity (deferred)
753
+ */
754
+ remove(entityId, componentType) {
755
+ this.commands.push({
756
+ type: "delete",
757
+ entityId,
758
+ componentType
759
+ });
760
+ }
761
+ /**
762
+ * Destroy an entity (deferred)
763
+ */
764
+ delete(entityId) {
765
+ this.commands.push({
766
+ type: "destroy",
767
+ entityId
768
+ });
769
+ }
770
+ /**
771
+ * Execute all commands and clear the buffer
772
+ */
773
+ execute() {
774
+ const MAX_ITERATIONS = 100;
775
+ let iterations = 0;
776
+ while (this.commands.length > 0) {
777
+ if (iterations >= MAX_ITERATIONS) throw new Error("Command execution exceeded maximum iterations, possible infinite loop");
778
+ iterations++;
779
+ const currentCommands = [...this.commands];
780
+ this.commands = [];
781
+ const entityCommands = /* @__PURE__ */ new Map();
782
+ for (const cmd of currentCommands) {
783
+ if (!entityCommands.has(cmd.entityId)) entityCommands.set(cmd.entityId, []);
784
+ entityCommands.get(cmd.entityId).push(cmd);
785
+ }
786
+ for (const [entityId, commands] of entityCommands) this.executeEntityCommands(entityId, commands);
787
+ }
788
+ }
789
+ /**
790
+ * Get current commands (for testing)
791
+ */
792
+ getCommands() {
793
+ return [...this.commands];
794
+ }
795
+ /**
796
+ * Clear all commands
797
+ */
798
+ clear() {
799
+ this.commands = [];
800
+ }
801
+ };
802
+
803
+ //#endregion
804
+ //#region src/multi-map.ts
805
+ var MultiMap = class {
806
+ map = /* @__PURE__ */ new Map();
807
+ _valueCount = 0;
808
+ get valueCount() {
809
+ return this._valueCount;
810
+ }
811
+ get keyCount() {
812
+ return this.map.size;
813
+ }
814
+ hasKey(key) {
815
+ return this.map.has(key);
816
+ }
817
+ has(key, value) {
818
+ const set = this.map.get(key);
819
+ if (!set) return false;
820
+ if (arguments.length === 1) return true;
821
+ return set.has(value);
822
+ }
823
+ add(key, value) {
824
+ let set = this.map.get(key);
825
+ if (!set) {
826
+ set = /* @__PURE__ */ new Set();
827
+ this.map.set(key, set);
828
+ }
829
+ if (!set.has(value)) {
830
+ set.add(value);
831
+ this._valueCount++;
832
+ }
833
+ }
834
+ remove(key, value) {
835
+ const set = this.map.get(key);
836
+ if (!set) return false;
837
+ if (!set.has(value)) return false;
838
+ set.delete(value);
839
+ this._valueCount--;
840
+ if (set.size === 0) this.map.delete(key);
841
+ return true;
842
+ }
843
+ deleteKey(key) {
844
+ const set = this.map.get(key);
845
+ if (!set) return false;
846
+ this._valueCount -= set.size;
847
+ this.map.delete(key);
848
+ return true;
849
+ }
850
+ get(key) {
851
+ const set = this.map.get(key);
852
+ return set ? new Set(set) : /* @__PURE__ */ new Set();
853
+ }
854
+ *keys() {
855
+ yield* this.map.keys();
856
+ }
857
+ *values() {
858
+ for (const set of this.map.values()) for (const v of set) yield v;
859
+ }
860
+ [Symbol.iterator]() {
861
+ return this.entries();
862
+ }
863
+ *entries() {
864
+ for (const [k, set] of this.map.entries()) for (const v of set) yield [k, v];
865
+ }
866
+ clear() {
867
+ this.map.clear();
868
+ this._valueCount = 0;
869
+ }
870
+ };
871
+
872
+ //#endregion
873
+ //#region src/query-filter.ts
874
+ /**
875
+ * Serialize a QueryFilter into a deterministic string suitable for cache keys.
876
+ * Currently only serializes `negativeComponentTypes`.
877
+ */
878
+ function serializeQueryFilter(filter = {}) {
879
+ const negative = (filter.negativeComponentTypes || []).slice().sort((a, b) => a - b);
880
+ if (negative.length === 0) return "";
881
+ return `neg:${negative.join(",")}`;
882
+ }
883
+ /**
884
+ * Check if an archetype matches the given component types
885
+ */
886
+ function matchesComponentTypes(archetype, componentTypes) {
887
+ return componentTypes.every((type) => {
888
+ const detailedType = getDetailedIdType(type);
889
+ if (detailedType.type === "wildcard-relation") return archetype.componentTypes.some((archetypeType) => {
890
+ if (!isRelationId(archetypeType)) return false;
891
+ return decodeRelationId(archetypeType).componentId === detailedType.componentId;
892
+ });
893
+ else return archetype.componentTypes.includes(type);
894
+ });
895
+ }
896
+ /**
897
+ * Check if an archetype matches the filter conditions (only filtering logic)
898
+ */
899
+ function matchesFilter(archetype, filter) {
900
+ return (filter.negativeComponentTypes || []).every((type) => {
901
+ const detailedType = getDetailedIdType(type);
902
+ if (detailedType.type === "wildcard-relation") return !archetype.componentTypes.some((archetypeType) => {
903
+ if (!isRelationId(archetypeType)) return false;
904
+ return decodeRelationId(archetypeType).componentId === detailedType.componentId;
905
+ });
906
+ else return !archetype.componentTypes.includes(type);
907
+ });
908
+ }
909
+
910
+ //#endregion
911
+ //#region src/query.ts
912
+ /**
913
+ * Query class for efficient entity queries with cached archetypes
914
+ */
915
+ var Query = class {
916
+ world;
917
+ componentTypes;
918
+ filter;
919
+ cachedArchetypes = [];
920
+ isDisposed = false;
921
+ constructor(world, componentTypes, filter = {}) {
922
+ this.world = world;
923
+ this.componentTypes = [...componentTypes].sort((a, b) => a - b);
924
+ this.filter = filter;
925
+ this.updateCache();
926
+ world._registerQuery(this);
927
+ }
928
+ /**
929
+ * Get all entities matching the query
930
+ */
931
+ getEntities() {
932
+ if (this.isDisposed) throw new Error("Query has been disposed");
933
+ const result = [];
934
+ for (const archetype of this.cachedArchetypes) result.push(...archetype.getEntities());
935
+ return result;
936
+ }
937
+ /**
938
+ * Get entities with their component data
939
+ * @param componentTypes Array of component types to retrieve
940
+ * @returns Array of objects with entity and component data
941
+ */
942
+ getEntitiesWithComponents(componentTypes) {
943
+ if (this.isDisposed) throw new Error("Query has been disposed");
944
+ const result = [];
945
+ for (const archetype of this.cachedArchetypes) {
946
+ const entitiesWithData = archetype.getEntitiesWithComponents(componentTypes);
947
+ result.push(...entitiesWithData);
948
+ }
949
+ return result;
950
+ }
951
+ /**
952
+ * Iterate over entities with their component data
953
+ * @param componentTypes Array of component types to retrieve
954
+ * @param callback Function called for each entity with its components
955
+ */
956
+ forEach(componentTypes, callback) {
957
+ if (this.isDisposed) throw new Error("Query has been disposed");
958
+ for (const archetype of this.cachedArchetypes) archetype.forEachWithComponents(componentTypes, callback);
959
+ }
960
+ /**
961
+ * Iterate over entities with their component data (generator)
962
+ * @param componentTypes Array of component types to retrieve
963
+ */
964
+ *iterate(componentTypes) {
965
+ if (this.isDisposed) throw new Error("Query has been disposed");
966
+ for (const archetype of this.cachedArchetypes) yield* archetype.iterateWithComponents(componentTypes);
967
+ }
968
+ /**
969
+ * Get component data arrays for all matching entities
970
+ * @param componentType The component type to retrieve
971
+ * @returns Array of component data for all matching entities
972
+ */
973
+ getComponentData(componentType) {
974
+ if (this.isDisposed) throw new Error("Query has been disposed");
975
+ const result = [];
976
+ for (const archetype of this.cachedArchetypes) result.push(...archetype.getComponentData(componentType));
977
+ return result;
978
+ }
979
+ /**
980
+ * Update the cached archetypes
981
+ * Called when new archetypes are created
982
+ */
983
+ updateCache() {
984
+ if (this.isDisposed) return;
985
+ this.cachedArchetypes = this.world.getMatchingArchetypes(this.componentTypes).filter((archetype) => matchesFilter(archetype, this.filter));
986
+ }
987
+ /**
988
+ * Check if a new archetype matches this query and add to cache if it does
989
+ */
990
+ checkNewArchetype(archetype) {
991
+ if (this.isDisposed) return;
992
+ if (matchesComponentTypes(archetype, this.componentTypes) && matchesFilter(archetype, this.filter) && !this.cachedArchetypes.includes(archetype)) this.cachedArchetypes.push(archetype);
993
+ }
994
+ /**
995
+ * Remove an archetype from the cached archetypes
996
+ */
997
+ removeArchetype(archetype) {
998
+ if (this.isDisposed) return;
999
+ const index = this.cachedArchetypes.indexOf(archetype);
1000
+ if (index !== -1) this.cachedArchetypes.splice(index, 1);
1001
+ }
1002
+ /**
1003
+ * Dispose the query and disconnect from world
1004
+ */
1005
+ /**
1006
+ * Request disposal of this query.
1007
+ * This will decrement the world's reference count for the query.
1008
+ * The query will only be fully disposed when the ref count reaches zero.
1009
+ */
1010
+ dispose() {
1011
+ this.world.releaseQuery(this);
1012
+ }
1013
+ /**
1014
+ * Internal full dispose called by World when refCount reaches zero.
1015
+ */
1016
+ _disposeInternal() {
1017
+ if (!this.isDisposed) {
1018
+ this.world._unregisterQuery(this);
1019
+ this.cachedArchetypes = [];
1020
+ this.isDisposed = true;
1021
+ }
1022
+ }
1023
+ /**
1024
+ * Symbol.dispose implementation for automatic resource management
1025
+ */
1026
+ [Symbol.dispose]() {
1027
+ this.dispose();
1028
+ }
1029
+ /**
1030
+ * Check if the query has been disposed
1031
+ */
1032
+ get disposed() {
1033
+ return this.isDisposed;
1034
+ }
1035
+ };
1036
+
1037
+ //#endregion
1038
+ //#region src/system-scheduler.ts
1039
+ /**
1040
+ * System Scheduler for managing system dependencies and execution order
1041
+ */
1042
+ var SystemScheduler = class {
1043
+ systems = /* @__PURE__ */ new Set();
1044
+ systemDependencies = /* @__PURE__ */ new Map();
1045
+ cachedExecutionOrder = null;
1046
+ /**
1047
+ * Add a system with optional dependencies
1048
+ * @param system The system to add
1049
+ * @param additionalDeps Additional dependencies for the system
1050
+ */
1051
+ addSystem(system, additionalDeps = []) {
1052
+ this.systems.add(system);
1053
+ for (const dep of system.dependencies || []) this.systems.add(dep);
1054
+ this.systemDependencies.set(system, new Set([...additionalDeps, ...system.dependencies || []]));
1055
+ this.cachedExecutionOrder = null;
1056
+ }
1057
+ /**
1058
+ * Get the execution order of systems based on dependencies
1059
+ * Uses topological sort
1060
+ */
1061
+ getExecutionOrder() {
1062
+ if (this.cachedExecutionOrder !== null) return this.cachedExecutionOrder;
1063
+ const result = [];
1064
+ const visited = /* @__PURE__ */ new Set();
1065
+ const visiting = /* @__PURE__ */ new Set();
1066
+ const visit = (system) => {
1067
+ if (visited.has(system)) return;
1068
+ if (visiting.has(system)) throw new Error("Circular dependency detected in system scheduling");
1069
+ visiting.add(system);
1070
+ for (const dep of this.systemDependencies.get(system) || []) visit(dep);
1071
+ visiting.delete(system);
1072
+ visited.add(system);
1073
+ result.push(system);
1074
+ };
1075
+ for (const system of this.systems) if (!visited.has(system)) visit(system);
1076
+ this.cachedExecutionOrder = result;
1077
+ return result;
1078
+ }
1079
+ update(...params) {
1080
+ const executionOrder = this.getExecutionOrder();
1081
+ const systemPromises = /* @__PURE__ */ new Map();
1082
+ for (const system of executionOrder) {
1083
+ const depPromises = Array.from(this.systemDependencies.get(system) || []).map((dep) => systemPromises.get(dep)).filter(Boolean);
1084
+ if (depPromises.length > 0) {
1085
+ const promise = Promise.all(depPromises).then(() => system.update(...params));
1086
+ systemPromises.set(system, promise);
1087
+ } else {
1088
+ const result = system.update(...params);
1089
+ if (result instanceof Promise) systemPromises.set(system, result);
1090
+ }
1091
+ }
1092
+ return Promise.all(systemPromises.values());
1093
+ }
1094
+ /**
1095
+ * Clear all systems and dependencies
1096
+ */
1097
+ clear() {
1098
+ this.systems.clear();
1099
+ this.cachedExecutionOrder = null;
1100
+ }
1101
+ };
1102
+
1103
+ //#endregion
1104
+ //#region src/world.ts
1105
+ /**
1106
+ * World class for ECS architecture
1107
+ * Manages entities, components, and systems
1108
+ */
1109
+ var World = class {
1110
+ /** Manages allocation and deallocation of entity IDs */
1111
+ entityIdManager = new EntityIdManager();
1112
+ /** Array of all archetypes in the world */
1113
+ archetypes = [];
1114
+ /** Maps archetype signatures (component type signatures) to archetype instances */
1115
+ archetypeBySignature = /* @__PURE__ */ new Map();
1116
+ /** Maps entity IDs to their current archetype */
1117
+ entityToArchetype = /* @__PURE__ */ new Map();
1118
+ /** Maps component types to arrays of archetypes that contain them */
1119
+ archetypesByComponent = /* @__PURE__ */ new Map();
1120
+ /** Tracks which entities reference each entity as a component type */
1121
+ entityReferences = /* @__PURE__ */ new Map();
1122
+ /** Array of all active queries for archetype change notifications */
1123
+ queries = [];
1124
+ /** Cache for queries keyed by component types and filter signatures */
1125
+ queryCache = /* @__PURE__ */ new Map();
1126
+ /** Schedules and executes systems in dependency order */
1127
+ systemScheduler = new SystemScheduler();
1128
+ /** Buffers structural changes for deferred execution */
1129
+ commandBuffer = new CommandBuffer((entityId, commands) => this.executeEntityCommands(entityId, commands));
1130
+ /** Stores lifecycle hooks for component and relation events */
1131
+ hooks = /* @__PURE__ */ new Map();
1132
+ /** Set of component IDs marked as exclusive relations */
1133
+ exclusiveComponents = /* @__PURE__ */ new Set();
1134
+ /** Set of component IDs that will cascade delete when the relation target is deleted */
1135
+ cascadeDeleteComponents = /* @__PURE__ */ new Set();
1136
+ /**
1137
+ * Create a new World.
1138
+ * If an optional snapshot object is provided (previously produced by `world.serialize()`),
1139
+ * the world will be restored from that snapshot. The snapshot may contain non-JSON values.
1140
+ */
1141
+ constructor(snapshot) {
1142
+ if (snapshot && typeof snapshot === "object") {
1143
+ if (snapshot.entityManager) this.entityIdManager.deserializeState(snapshot.entityManager);
1144
+ if (Array.isArray(snapshot.entities)) for (const entry of snapshot.entities) {
1145
+ const entityId = entry.id;
1146
+ const componentsArray = entry.components || [];
1147
+ const componentMap = /* @__PURE__ */ new Map();
1148
+ const componentTypes = [];
1149
+ for (const componentEntry of componentsArray) {
1150
+ const componentTypeRaw = componentEntry.type;
1151
+ let componentType;
1152
+ if (typeof componentTypeRaw === "number") componentType = componentTypeRaw;
1153
+ else if (typeof componentTypeRaw === "string") {
1154
+ const compId = getComponentIdByName(componentTypeRaw);
1155
+ if (compId === void 0) throw new Error(`Unknown component name in snapshot: ${componentTypeRaw}`);
1156
+ componentType = compId;
1157
+ } else if (typeof componentTypeRaw === "object" && componentTypeRaw !== null && typeof componentTypeRaw.component === "string") {
1158
+ const compId = getComponentIdByName(componentTypeRaw.component);
1159
+ if (compId === void 0) throw new Error(`Unknown component name in snapshot: ${componentTypeRaw.component}`);
1160
+ if (typeof componentTypeRaw.target === "string") {
1161
+ const targetCompId = getComponentIdByName(componentTypeRaw.target);
1162
+ if (targetCompId === void 0) throw new Error(`Unknown target component name in snapshot: ${componentTypeRaw.target}`);
1163
+ componentType = relation(compId, targetCompId);
1164
+ } else componentType = relation(compId, componentTypeRaw.target);
1165
+ } else throw new Error(`Invalid component type in snapshot: ${JSON.stringify(componentTypeRaw)}`);
1166
+ componentMap.set(componentType, componentEntry.value);
1167
+ componentTypes.push(componentType);
1168
+ }
1169
+ const archetype = this.ensureArchetype(componentTypes);
1170
+ archetype.addEntity(entityId, componentMap);
1171
+ this.entityToArchetype.set(entityId, archetype);
1172
+ for (const compType of componentTypes) {
1173
+ const detailedType = getDetailedIdType(compType);
1174
+ if (detailedType.type === "entity-relation") {
1175
+ const targetEntityId = detailedType.targetId;
1176
+ this.trackEntityReference(entityId, compType, targetEntityId);
1177
+ } else if (detailedType.type === "entity") this.trackEntityReference(entityId, compType, compType);
1178
+ }
1179
+ }
1180
+ }
1181
+ }
1182
+ /**
1183
+ * Generate a signature string for component types array
1184
+ * @returns A string signature for the component types
1185
+ */
1186
+ createArchetypeSignature(componentTypes) {
1187
+ return componentTypes.join(",");
1188
+ }
1189
+ /**
1190
+ * Create a new entity
1191
+ * @returns The ID of the newly created entity
1192
+ */
1193
+ new() {
1194
+ const entityId = this.entityIdManager.allocate();
1195
+ let emptyArchetype = this.ensureArchetype([]);
1196
+ emptyArchetype.addEntity(entityId, /* @__PURE__ */ new Map());
1197
+ this.entityToArchetype.set(entityId, emptyArchetype);
1198
+ return entityId;
1199
+ }
1200
+ /**
1201
+ * Destroy an entity and remove all its components (immediate execution)
1202
+ */
1203
+ destroyEntityImmediate(entityId) {
1204
+ const queue = [entityId];
1205
+ const visited = /* @__PURE__ */ new Set();
1206
+ while (queue.length > 0) {
1207
+ const cur = queue.shift();
1208
+ if (visited.has(cur)) continue;
1209
+ visited.add(cur);
1210
+ const archetype = this.entityToArchetype.get(cur);
1211
+ if (!archetype) continue;
1212
+ const componentReferences = Array.from(this.getEntityReferences(cur));
1213
+ for (const [sourceEntityId, componentType] of componentReferences) {
1214
+ const sourceArchetype = this.entityToArchetype.get(sourceEntityId);
1215
+ if (!sourceArchetype) continue;
1216
+ const detailedType = getDetailedIdType(componentType);
1217
+ if (detailedType.type === "entity-relation" && this.cascadeDeleteComponents.has(detailedType.componentId)) {
1218
+ if (!visited.has(sourceEntityId)) queue.push(sourceEntityId);
1219
+ continue;
1220
+ }
1221
+ const currentComponents = /* @__PURE__ */ new Map();
1222
+ let removedComponent = sourceArchetype.get(sourceEntityId, componentType);
1223
+ for (const archetypeComponentType of sourceArchetype.componentTypes) if (archetypeComponentType !== componentType) {
1224
+ const componentData = sourceArchetype.get(sourceEntityId, archetypeComponentType);
1225
+ currentComponents.set(archetypeComponentType, componentData);
1226
+ }
1227
+ const newArchetype = this.ensureArchetype(currentComponents.keys());
1228
+ sourceArchetype.removeEntity(sourceEntityId);
1229
+ if (sourceArchetype.getEntities().length === 0) this.cleanupEmptyArchetype(sourceArchetype);
1230
+ newArchetype.addEntity(sourceEntityId, currentComponents);
1231
+ this.entityToArchetype.set(sourceEntityId, newArchetype);
1232
+ this.untrackEntityReference(sourceEntityId, componentType, cur);
1233
+ this.triggerLifecycleHooks(sourceEntityId, /* @__PURE__ */ new Map(), new Map([[componentType, removedComponent]]));
1234
+ }
1235
+ this.entityReferences.delete(cur);
1236
+ archetype.removeEntity(cur);
1237
+ if (archetype.getEntities().length === 0) this.cleanupEmptyArchetype(archetype);
1238
+ this.entityToArchetype.delete(cur);
1239
+ this.entityIdManager.deallocate(cur);
1240
+ }
1241
+ }
1242
+ /**
1243
+ * Check if an entity exists
1244
+ */
1245
+ exists(entityId) {
1246
+ return this.entityToArchetype.has(entityId);
1247
+ }
1248
+ set(entityId, componentType, component$1) {
1249
+ if (!this.exists(entityId)) throw new Error(`Entity ${entityId} does not exist`);
1250
+ const detailedType = getDetailedIdType(componentType);
1251
+ if (detailedType.type === "invalid") throw new Error(`Invalid component type: ${componentType}`);
1252
+ if (detailedType.type === "wildcard-relation") throw new Error(`Cannot directly add wildcard relation components: ${componentType}`);
1253
+ this.commandBuffer.set(entityId, componentType, component$1);
1254
+ }
1255
+ /**
1256
+ * Remove a component from an entity (deferred)
1257
+ */
1258
+ remove(entityId, componentType) {
1259
+ if (!this.exists(entityId)) throw new Error(`Entity ${entityId} does not exist`);
1260
+ if (getDetailedIdType(componentType).type === "invalid") throw new Error(`Invalid component type: ${componentType}`);
1261
+ this.commandBuffer.remove(entityId, componentType);
1262
+ }
1263
+ /**
1264
+ * Destroy an entity and remove all its components (deferred)
1265
+ */
1266
+ delete(entityId) {
1267
+ this.commandBuffer.delete(entityId);
1268
+ }
1269
+ /**
1270
+ * Check if an entity has a specific component
1271
+ */
1272
+ has(entityId, componentType) {
1273
+ const archetype = this.entityToArchetype.get(entityId);
1274
+ return archetype ? archetype.componentTypes.includes(componentType) : false;
1275
+ }
1276
+ get(entityId, componentType) {
1277
+ const archetype = this.entityToArchetype.get(entityId);
1278
+ if (!archetype) throw new Error(`Entity ${entityId} does not exist`);
1279
+ if (getDetailedIdType(componentType).type !== "wildcard-relation") {
1280
+ if (!archetype.componentTypes.includes(componentType)) throw new Error(`Entity ${entityId} does not have component ${componentType}. Use has() to check component existence before calling get().`);
1281
+ }
1282
+ return archetype.get(entityId, componentType);
1283
+ }
1284
+ /**
1285
+ * Register a system with optional dependencies
1286
+ */
1287
+ registerSystem(system, additionalDeps = []) {
1288
+ this.systemScheduler.addSystem(system, additionalDeps);
1289
+ }
1290
+ /**
1291
+ * Register a lifecycle hook for component or wildcard relation events
1292
+ */
1293
+ hook(componentType, hook) {
1294
+ if (!this.hooks.has(componentType)) this.hooks.set(componentType, /* @__PURE__ */ new Set());
1295
+ this.hooks.get(componentType).add(hook);
1296
+ if (hook.on_init !== void 0) this.archetypesByComponent.get(componentType)?.forEach((archetype) => {
1297
+ const entities = archetype.getEntityToIndexMap();
1298
+ const componentData = archetype.getComponentData(componentType);
1299
+ for (const [entity, index] of entities) {
1300
+ const data = componentData[index];
1301
+ const value = data === MISSING_COMPONENT ? void 0 : data;
1302
+ hook.on_init?.(entity, componentType, value);
1303
+ }
1304
+ });
1305
+ }
1306
+ /**
1307
+ * Unregister a lifecycle hook for component or wildcard relation events
1308
+ */
1309
+ unhook(componentType, hook) {
1310
+ const hooks = this.hooks.get(componentType);
1311
+ if (hooks) {
1312
+ hooks.delete(hook);
1313
+ if (hooks.size === 0) this.hooks.delete(componentType);
1314
+ }
1315
+ }
1316
+ /**
1317
+ * Mark a component as exclusive relation
1318
+ * For exclusive relations, an entity can have at most one relation per base component
1319
+ */
1320
+ setExclusive(componentId) {
1321
+ this.exclusiveComponents.add(componentId);
1322
+ }
1323
+ /**
1324
+ * Mark a component as cascade-delete relation
1325
+ * For cascade relations, when the relation target entity is deleted,
1326
+ * the referencing entity will also be deleted (cascade).
1327
+ * Only applicable to entity-relation components
1328
+ */
1329
+ setCascadeDelete(componentId) {
1330
+ this.cascadeDeleteComponents.add(componentId);
1331
+ }
1332
+ /**
1333
+ * Update the world (run all systems in dependency order)
1334
+ * This function is synchronous when all systems are synchronous,
1335
+ * and asynchronous (returns a Promise) when any system is asynchronous.
1336
+ */
1337
+ update(...params) {
1338
+ const result = this.systemScheduler.update(...params);
1339
+ if (result instanceof Promise) return result.then(() => this.commandBuffer.execute());
1340
+ else this.commandBuffer.execute();
1341
+ }
1342
+ /**
1343
+ * Execute all deferred commands immediately without running systems
1344
+ */
1345
+ sync() {
1346
+ this.commandBuffer.execute();
1347
+ }
1348
+ /**
1349
+ * Create a cached query for efficient entity lookups
1350
+ * @returns A Query object for the specified component types and filter
1351
+ */
1352
+ createQuery(componentTypes, filter = {}) {
1353
+ const sortedTypes = [...componentTypes].sort((a, b) => a - b);
1354
+ const filterKey = serializeQueryFilter(filter);
1355
+ const key = `${this.createArchetypeSignature(sortedTypes)}${filterKey ? `|${filterKey}` : ""}`;
1356
+ const cached = this.queryCache.get(key);
1357
+ if (cached) {
1358
+ cached.refCount++;
1359
+ return cached.query;
1360
+ }
1361
+ const query = new Query(this, sortedTypes, filter);
1362
+ this.queryCache.set(key, {
1363
+ query,
1364
+ refCount: 1
1365
+ });
1366
+ return query;
1367
+ }
1368
+ /**
1369
+ * @internal Register a query for archetype update notifications
1370
+ */
1371
+ _registerQuery(query) {
1372
+ this.queries.push(query);
1373
+ }
1374
+ /**
1375
+ * @internal Unregister a query
1376
+ */
1377
+ _unregisterQuery(query) {
1378
+ const index = this.queries.indexOf(query);
1379
+ if (index !== -1) this.queries.splice(index, 1);
1380
+ }
1381
+ /**
1382
+ * Release a query reference obtained from createQuery.
1383
+ * Decrements the refCount and fully disposes the query when it reaches zero.
1384
+ */
1385
+ releaseQuery(query) {
1386
+ for (const [k, v] of this.queryCache.entries()) if (v.query === query) {
1387
+ v.refCount--;
1388
+ if (v.refCount <= 0) {
1389
+ this.queryCache.delete(k);
1390
+ this._unregisterQuery(query);
1391
+ v.query._disposeInternal();
1392
+ }
1393
+ return;
1394
+ }
1395
+ }
1396
+ /**
1397
+ * @internal Get archetypes that match specific component types (for internal use by queries)
1398
+ */
1399
+ getMatchingArchetypes(componentTypes) {
1400
+ if (componentTypes.length === 0) return [...this.archetypes];
1401
+ const regularComponents = [];
1402
+ const wildcardRelations = [];
1403
+ for (const componentType of componentTypes) {
1404
+ const detailedType = getDetailedIdType(componentType);
1405
+ if (detailedType.type === "wildcard-relation") wildcardRelations.push({
1406
+ componentId: detailedType.componentId,
1407
+ relationId: componentType
1408
+ });
1409
+ else regularComponents.push(componentType);
1410
+ }
1411
+ let matchingArchetypes = [];
1412
+ if (regularComponents.length > 0) {
1413
+ const sortedRegularTypes = [...regularComponents].sort((a, b) => a - b);
1414
+ if (sortedRegularTypes.length === 1) {
1415
+ const componentType = sortedRegularTypes[0];
1416
+ matchingArchetypes = this.archetypesByComponent.get(componentType) || [];
1417
+ } else {
1418
+ const archetypeLists = sortedRegularTypes.map((type) => this.archetypesByComponent.get(type) || []);
1419
+ const firstList = archetypeLists[0] || [];
1420
+ const intersection = /* @__PURE__ */ new Set();
1421
+ for (const archetype of firstList) {
1422
+ let hasAllComponents = true;
1423
+ for (let listIndex = 1; listIndex < archetypeLists.length; listIndex++) if (!archetypeLists[listIndex].includes(archetype)) {
1424
+ hasAllComponents = false;
1425
+ break;
1426
+ }
1427
+ if (hasAllComponents) intersection.add(archetype);
1428
+ }
1429
+ matchingArchetypes = Array.from(intersection);
1430
+ }
1431
+ } else matchingArchetypes = [...this.archetypes];
1432
+ for (const wildcard of wildcardRelations) matchingArchetypes = matchingArchetypes.filter((archetype) => archetype.componentTypes.some((archetypeType) => {
1433
+ if (!isRelationId(archetypeType)) return false;
1434
+ return decodeRelationId(archetypeType).componentId === wildcard.componentId;
1435
+ }));
1436
+ return matchingArchetypes;
1437
+ }
1438
+ query(componentTypes, includeComponents) {
1439
+ const matchingArchetypes = this.getMatchingArchetypes(componentTypes);
1440
+ if (includeComponents) {
1441
+ const result = [];
1442
+ for (const archetype of matchingArchetypes) {
1443
+ const entitiesWithData = archetype.getEntitiesWithComponents(componentTypes);
1444
+ result.push(...entitiesWithData);
1445
+ }
1446
+ return result;
1447
+ } else {
1448
+ const result = [];
1449
+ for (const archetype of matchingArchetypes) result.push(...archetype.getEntities());
1450
+ return result;
1451
+ }
1452
+ }
1453
+ /**
1454
+ * @internal Execute commands for a single entity (for internal use by CommandBuffer)
1455
+ * @returns ComponentChangeset describing the changes made
1456
+ */
1457
+ executeEntityCommands(entityId, commands) {
1458
+ const changeset = new ComponentChangeset();
1459
+ if (commands.some((cmd) => cmd.type === "destroy")) {
1460
+ this.destroyEntityImmediate(entityId);
1461
+ return changeset;
1462
+ }
1463
+ const currentArchetype = this.entityToArchetype.get(entityId);
1464
+ if (!currentArchetype) return changeset;
1465
+ for (const command of commands) switch (command.type) {
1466
+ case "set":
1467
+ if (command.componentType) {
1468
+ const detailedType = getDetailedIdType(command.componentType);
1469
+ if ((detailedType.type === "entity-relation" || detailedType.type === "component-relation") && this.exclusiveComponents.has(detailedType.componentId)) for (const componentType of currentArchetype.componentTypes) {
1470
+ const componentDetailedType = getDetailedIdType(componentType);
1471
+ if ((componentDetailedType.type === "entity-relation" || componentDetailedType.type === "component-relation") && componentDetailedType.componentId === detailedType.componentId) changeset.delete(componentType);
1472
+ }
1473
+ changeset.set(command.componentType, command.component);
1474
+ }
1475
+ break;
1476
+ case "delete":
1477
+ if (command.componentType) {
1478
+ const detailedType = getDetailedIdType(command.componentType);
1479
+ if (detailedType.type === "wildcard-relation") {
1480
+ const baseComponentId = detailedType.componentId;
1481
+ for (const componentType of currentArchetype.componentTypes) {
1482
+ const componentDetailedType = getDetailedIdType(componentType);
1483
+ if (componentDetailedType.type === "entity-relation" || componentDetailedType.type === "component-relation") {
1484
+ if (componentDetailedType.componentId === baseComponentId) changeset.delete(componentType);
1485
+ }
1486
+ }
1487
+ } else changeset.delete(command.componentType);
1488
+ }
1489
+ break;
1490
+ }
1491
+ const finalComponentTypes = changeset.getFinalComponentTypes(currentArchetype.componentTypes);
1492
+ const removedCompoents = /* @__PURE__ */ new Map();
1493
+ if (finalComponentTypes) {
1494
+ const newArchetype = this.ensureArchetype(finalComponentTypes);
1495
+ const currentComponents = currentArchetype.removeEntity(entityId);
1496
+ for (const componentType of changeset.removes) removedCompoents.set(componentType, currentComponents.get(componentType));
1497
+ newArchetype.addEntity(entityId, changeset.applyTo(currentComponents));
1498
+ this.entityToArchetype.set(entityId, newArchetype);
1499
+ } else for (const [componentType, component$1] of changeset.adds) currentArchetype.set(entityId, componentType, component$1);
1500
+ for (const componentType of changeset.removes) {
1501
+ const detailedType = getDetailedIdType(componentType);
1502
+ if (detailedType.type === "entity-relation") {
1503
+ const targetEntityId = detailedType.targetId;
1504
+ this.untrackEntityReference(entityId, componentType, targetEntityId);
1505
+ } else if (detailedType.type === "entity") this.untrackEntityReference(entityId, componentType, componentType);
1506
+ }
1507
+ for (const [componentType, component$1] of changeset.adds) {
1508
+ const detailedType = getDetailedIdType(componentType);
1509
+ if (detailedType.type === "entity-relation") {
1510
+ const targetEntityId = detailedType.targetId;
1511
+ this.trackEntityReference(entityId, componentType, targetEntityId);
1512
+ } else if (detailedType.type === "entity") this.trackEntityReference(entityId, componentType, componentType);
1513
+ }
1514
+ this.triggerLifecycleHooks(entityId, changeset.adds, removedCompoents);
1515
+ return changeset;
1516
+ }
1517
+ /**
1518
+ * Get or create an archetype for the given component types
1519
+ * @returns The archetype for the given component types
1520
+ */
1521
+ ensureArchetype(componentTypes) {
1522
+ const sortedTypes = Array.from(componentTypes).sort((a, b) => a - b);
1523
+ const hashKey = this.createArchetypeSignature(sortedTypes);
1524
+ return getOrCreateWithSideEffect(this.archetypeBySignature, hashKey, () => {
1525
+ const newArchetype = new Archetype(sortedTypes);
1526
+ this.archetypes.push(newArchetype);
1527
+ for (const componentType of sortedTypes) {
1528
+ const archetypes = this.archetypesByComponent.get(componentType) || [];
1529
+ archetypes.push(newArchetype);
1530
+ this.archetypesByComponent.set(componentType, archetypes);
1531
+ }
1532
+ for (const query of this.queries) query.checkNewArchetype(newArchetype);
1533
+ return newArchetype;
1534
+ });
1535
+ }
1536
+ /**
1537
+ * Add a component reference to the reverse index when an entity is used as a component type
1538
+ * @param sourceEntityId The entity that has the component
1539
+ * @param componentType The component type (which may be an entity ID used as component type)
1540
+ * @param targetEntityId The entity being used as component type
1541
+ */
1542
+ trackEntityReference(sourceEntityId, componentType, targetEntityId) {
1543
+ if (!this.entityReferences.has(targetEntityId)) this.entityReferences.set(targetEntityId, new MultiMap());
1544
+ this.entityReferences.get(targetEntityId).add(sourceEntityId, componentType);
1545
+ }
1546
+ /**
1547
+ * Remove a component reference from the reverse index
1548
+ * @param sourceEntityId The entity that has the component
1549
+ * @param componentType The component type
1550
+ * @param targetEntityId The entity being used as component type
1551
+ */
1552
+ untrackEntityReference(sourceEntityId, componentType, targetEntityId) {
1553
+ const references = this.entityReferences.get(targetEntityId);
1554
+ if (references) {
1555
+ references.remove(sourceEntityId, componentType);
1556
+ if (references.keyCount === 0) this.entityReferences.delete(targetEntityId);
1557
+ }
1558
+ }
1559
+ /**
1560
+ * Get all component references where a target entity is used as a component type
1561
+ * @param targetEntityId The target entity
1562
+ * @returns A MultiMap of sourceEntityId to componentTypes that reference the target entity
1563
+ */
1564
+ getEntityReferences(targetEntityId) {
1565
+ return this.entityReferences.get(targetEntityId) ?? new MultiMap();
1566
+ }
1567
+ /**
1568
+ * Remove an empty archetype from all internal data structures
1569
+ */
1570
+ cleanupEmptyArchetype(archetype) {
1571
+ if (archetype.getEntities().length > 0) return;
1572
+ const index = this.archetypes.indexOf(archetype);
1573
+ if (index !== -1) this.archetypes.splice(index, 1);
1574
+ const hashKey = this.createArchetypeSignature(archetype.componentTypes);
1575
+ this.archetypeBySignature.delete(hashKey);
1576
+ for (const componentType of archetype.componentTypes) {
1577
+ const archetypes = this.archetypesByComponent.get(componentType);
1578
+ if (archetypes) {
1579
+ const compIndex = archetypes.indexOf(archetype);
1580
+ if (compIndex !== -1) {
1581
+ archetypes.splice(compIndex, 1);
1582
+ if (archetypes.length === 0) this.archetypesByComponent.delete(componentType);
1583
+ }
1584
+ }
1585
+ }
1586
+ for (const query of this.queries) query.removeArchetype(archetype);
1587
+ }
1588
+ /**
1589
+ * Execute component lifecycle hooks for added and removed components
1590
+ */
1591
+ triggerLifecycleHooks(entityId, addedComponents, removedComponents) {
1592
+ for (const [componentType, component$1] of addedComponents) {
1593
+ const directHooks = this.hooks.get(componentType);
1594
+ if (directHooks) for (const lifecycleHook of directHooks) lifecycleHook.on_set?.(entityId, componentType, component$1);
1595
+ const detailedType = getDetailedIdType(componentType);
1596
+ if (detailedType.type === "entity-relation" || detailedType.type === "component-relation" || detailedType.type === "wildcard-relation") {
1597
+ const wildcardRelationId = relation(detailedType.componentId, "*");
1598
+ const wildcardHooks = this.hooks.get(wildcardRelationId);
1599
+ if (wildcardHooks) for (const lifecycleHook of wildcardHooks) lifecycleHook.on_set?.(entityId, componentType, component$1);
1600
+ }
1601
+ }
1602
+ for (const [componentType, component$1] of removedComponents) {
1603
+ const directHooks = this.hooks.get(componentType);
1604
+ if (directHooks) for (const lifecycleHook of directHooks) lifecycleHook.on_remove?.(entityId, componentType, component$1);
1605
+ const detailedType = getDetailedIdType(componentType);
1606
+ if (detailedType.type === "entity-relation" || detailedType.type === "component-relation" || detailedType.type === "wildcard-relation") {
1607
+ const wildcardRelationId = relation(detailedType.componentId, "*");
1608
+ const wildcardHooks = this.hooks.get(wildcardRelationId);
1609
+ if (wildcardHooks) for (const hook of wildcardHooks) hook.on_remove?.(entityId, componentType, component$1);
1610
+ }
1611
+ }
1612
+ }
1613
+ /**
1614
+ * Convert the world into a plain snapshot object.
1615
+ * This returns an in-memory structure and does not perform JSON stringification.
1616
+ * Component values are stored as-is (they may be non-JSON-serializable).
1617
+ */
1618
+ serialize() {
1619
+ const entities = [];
1620
+ for (const archetype of this.archetypes) {
1621
+ const dumpedEntities = archetype.dump();
1622
+ for (const { entity, components } of dumpedEntities) entities.push({
1623
+ id: entity,
1624
+ components: Array.from(components.entries()).map(([rawType, value]) => {
1625
+ const detailedType = getDetailedIdType(rawType);
1626
+ let type = rawType;
1627
+ let componentName;
1628
+ switch (detailedType.type) {
1629
+ case "component":
1630
+ type = getComponentNameById(rawType) || rawType;
1631
+ break;
1632
+ case "entity-relation":
1633
+ componentName = getComponentNameById(detailedType.componentId);
1634
+ if (componentName) type = {
1635
+ component: componentName,
1636
+ target: detailedType.targetId
1637
+ };
1638
+ break;
1639
+ case "component-relation":
1640
+ componentName = getComponentNameById(detailedType.componentId);
1641
+ if (componentName) type = {
1642
+ component: componentName,
1643
+ target: getComponentNameById(detailedType.targetId) || detailedType.targetId
1644
+ };
1645
+ break;
1646
+ }
1647
+ return {
1648
+ type,
1649
+ value: value === MISSING_COMPONENT ? void 0 : value
1650
+ };
1651
+ })
1652
+ });
1653
+ }
1654
+ return {
1655
+ version: 1,
1656
+ entityManager: this.entityIdManager.serializeState(),
1657
+ entities
1658
+ };
1659
+ }
1660
+ };
1661
+
1662
+ //#endregion
1663
+ export { Archetype, COMPONENT_ID_MAX, ComponentIdAllocator, ENTITY_ID_START, EntityIdManager, INVALID_COMPONENT_ID, MISSING_COMPONENT, Query, RELATION_SHIFT, SystemScheduler, WILDCARD_TARGET_ID, World, component, createComponentId, createEntityId, decodeRelationId, getComponentIdByName, getComponentNameById, getDetailedIdType, getIdType, inspectEntityId, isComponentId, isEntityId, isOptionalEntityId, isRelationId, isWildcardRelationId, relation };
1664
+ //# sourceMappingURL=index.mjs.map