@codehz/ecs 0.5.0 → 0.5.2

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/world.mjs CHANGED
@@ -1,101 +1,4 @@
1
- //#region src/bit-set.ts
2
- var BitSet = class {
3
- data;
4
- _length;
5
- constructor(length) {
6
- this._length = length;
7
- const numWords = Math.ceil(length / 32);
8
- this.data = new Uint32Array(numWords);
9
- }
10
- get length() {
11
- return this._length;
12
- }
13
- has(index) {
14
- if (index < 0 || index >= this._length) return false;
15
- const word = index >>> 5;
16
- const bit = index & 31;
17
- return (this.data[word] >>> bit & 1) !== 0;
18
- }
19
- set(index) {
20
- if (index < 0 || index >= this._length) return;
21
- const word = index >>> 5;
22
- const bit = index & 31;
23
- this.data[word] |= 1 << bit;
24
- }
25
- clear(index) {
26
- if (index < 0 || index >= this._length) return;
27
- const word = index >>> 5;
28
- const bit = index & 31;
29
- this.data[word] &= ~(1 << bit);
30
- }
31
- setRange(lo, hi) {
32
- if (lo > hi) return;
33
- if (lo < 0) lo = 0;
34
- if (hi >= this._length) hi = this._length - 1;
35
- const firstWord = lo >>> 5;
36
- const lastWord = hi >>> 5;
37
- const loBit = lo & 31;
38
- const hiBit = hi & 31;
39
- const maskFor = (a, b) => {
40
- const width = b - a + 1;
41
- if (width <= 0) return 0;
42
- if (width >= 32) return 4294967295;
43
- return (1 << width) - 1 << a >>> 0;
44
- };
45
- if (firstWord === lastWord) {
46
- const mask = maskFor(loBit, hiBit);
47
- this.data[firstWord] = (this.data[firstWord] | mask) >>> 0;
48
- return;
49
- }
50
- const firstMask = maskFor(loBit, 31);
51
- this.data[firstWord] = (this.data[firstWord] | firstMask) >>> 0;
52
- for (let w = firstWord + 1; w <= lastWord - 1; w++) this.data[w] = 4294967295;
53
- const lastMask = maskFor(0, hiBit);
54
- this.data[lastWord] = (this.data[lastWord] | lastMask) >>> 0;
55
- }
56
- anyClearInRange(lo, hi) {
57
- if (lo > hi) return false;
58
- if (lo < 0) lo = 0;
59
- if (hi >= this._length) hi = this._length - 1;
60
- const firstWord = lo >>> 5;
61
- const lastWord = hi >>> 5;
62
- const loBit = lo & 31;
63
- const hiBit = hi & 31;
64
- const maskFor = (a, b) => {
65
- const width = b - a + 1;
66
- if (width <= 0) return 0;
67
- if (width >= 32) return 4294967295;
68
- return (1 << width) - 1 << a >>> 0;
69
- };
70
- if (firstWord === lastWord) {
71
- const mask = maskFor(loBit, hiBit);
72
- return (this.data[firstWord] & mask) >>> 0 !== mask >>> 0;
73
- }
74
- const firstMask = maskFor(loBit, 31);
75
- if ((this.data[firstWord] & firstMask) >>> 0 !== firstMask >>> 0) return true;
76
- for (let w = firstWord + 1; w <= lastWord - 1; w++) if (this.data[w] !== 4294967295) return true;
77
- const lastMask = maskFor(0, hiBit);
78
- if ((this.data[lastWord] & lastMask) >>> 0 !== lastMask >>> 0) return true;
79
- return false;
80
- }
81
- reset() {
82
- this.data.fill(0);
83
- }
84
- *[Symbol.iterator]() {
85
- for (let wordIndex = 0; wordIndex < this.data.length; wordIndex++) {
86
- let word = this.data[wordIndex];
87
- if (word === 0) continue;
88
- const baseIndex = wordIndex * 32;
89
- for (let bit = 0; bit < 32 && baseIndex + bit < this._length; bit++) {
90
- if (word & 1) yield baseIndex + bit;
91
- word >>>= 1;
92
- }
93
- }
94
- }
95
- };
96
-
97
- //#endregion
98
- //#region src/entity.ts
1
+ //#region src/core/entity-types.ts
99
2
  const COMPONENT_ID_MAX = 1023;
100
3
  const ENTITY_ID_START = 1024;
101
4
  /**
@@ -104,34 +7,11 @@ const ENTITY_ID_START = 1024;
104
7
  const RELATION_SHIFT = 2 ** 42;
105
8
  const WILDCARD_TARGET_ID = 0;
106
9
  /**
107
- * Internal function to decode a relation ID into raw component and target IDs
108
- * @param id The EntityId to decode
109
- * @returns Object with componentId and targetId, or null if not a relation
110
- */
111
- function decodeRelationRaw(id) {
112
- if (id >= 0) return null;
113
- const absId = -id;
114
- return {
115
- componentId: Math.floor(absId / RELATION_SHIFT),
116
- targetId: absId % RELATION_SHIFT
117
- };
118
- }
119
- /**
120
10
  * Check if a component ID is valid (1-1023)
121
11
  */
122
12
  function isValidComponentId(componentId) {
123
13
  return componentId >= 1 && componentId <= COMPONENT_ID_MAX;
124
14
  }
125
- function relation(componentId, targetId) {
126
- if (!isComponentId(componentId)) throw new Error("First argument must be a valid component ID");
127
- let actualTargetId;
128
- if (targetId === "*") actualTargetId = WILDCARD_TARGET_ID;
129
- else {
130
- if (!isEntityId(targetId) && !isComponentId(targetId)) throw new Error("Second argument must be a valid entity ID, component ID, or '*'");
131
- actualTargetId = targetId;
132
- }
133
- return -(componentId * RELATION_SHIFT + actualTargetId);
134
- }
135
15
  /**
136
16
  * Check if an ID is a component ID
137
17
  */
@@ -150,6 +30,32 @@ function isEntityId(id) {
150
30
  function isRelationId(id) {
151
31
  return id < 0;
152
32
  }
33
+
34
+ //#endregion
35
+ //#region src/core/entity-relation.ts
36
+ /**
37
+ * Internal function to decode a relation ID into raw component and target IDs
38
+ * @param id The EntityId to decode
39
+ * @returns Object with componentId and targetId, or null if not a relation
40
+ */
41
+ function decodeRelationRaw(id) {
42
+ if (id >= 0) return null;
43
+ const absId = -id;
44
+ return {
45
+ componentId: Math.floor(absId / RELATION_SHIFT),
46
+ targetId: absId % RELATION_SHIFT
47
+ };
48
+ }
49
+ function relation(componentId, targetId) {
50
+ if (!isComponentId(componentId)) throw new Error("First argument must be a valid component ID");
51
+ let actualTargetId;
52
+ if (targetId === "*") actualTargetId = WILDCARD_TARGET_ID;
53
+ else {
54
+ if (!isEntityId(targetId) && !isComponentId(targetId)) throw new Error("Second argument must be a valid entity ID, component ID, or '*'");
55
+ actualTargetId = targetId;
56
+ }
57
+ return -(componentId * RELATION_SHIFT + actualTargetId);
58
+ }
153
59
  /**
154
60
  * Check if an ID is a wildcard relation id
155
61
  */
@@ -192,7 +98,7 @@ function decodeRelationId(relationId) {
192
98
  function getIdType(id) {
193
99
  if (isComponentId(id)) return "component";
194
100
  if (isEntityId(id)) return "entity";
195
- if (isRelationId(id)) try {
101
+ if (id < 0) try {
196
102
  const decoded = decodeRelationId(id);
197
103
  if (decoded.type !== "wildcard" && !isEntityId(decoded.targetId) && !isComponentId(decoded.targetId)) return "invalid";
198
104
  switch (decoded.type) {
@@ -213,7 +119,7 @@ function getIdType(id) {
213
119
  function getDetailedIdType(id) {
214
120
  if (isComponentId(id)) return { type: "component" };
215
121
  if (isEntityId(id)) return { type: "entity" };
216
- if (isRelationId(id)) try {
122
+ if (id < 0) try {
217
123
  const decoded = decodeRelationId(id);
218
124
  if (decoded.type !== "wildcard" && !isEntityId(decoded.targetId) && !isComponentId(decoded.targetId)) return { type: "invalid" };
219
125
  let type;
@@ -239,6 +145,32 @@ function getDetailedIdType(id) {
239
145
  return { type: "invalid" };
240
146
  }
241
147
  /**
148
+ * Get the componentId from a relation ID without fully decoding the relation.
149
+ * Returns undefined for non-relation IDs or invalid component IDs.
150
+ */
151
+ function getComponentIdFromRelationId(id) {
152
+ const decoded = decodeRelationRaw(id);
153
+ if (decoded === null || !isValidComponentId(decoded.componentId)) return void 0;
154
+ return decoded.componentId;
155
+ }
156
+ /**
157
+ * Get the targetId from a relation ID without fully decoding the relation.
158
+ * Returns undefined for non-relation IDs.
159
+ */
160
+ function getTargetIdFromRelationId(id) {
161
+ return decodeRelationRaw(id)?.targetId;
162
+ }
163
+ /**
164
+ * Check if an ID is an entity-relation (relation targeting an entity, not a component or wildcard)
165
+ */
166
+ function isEntityRelation(id) {
167
+ const decoded = decodeRelationRaw(id);
168
+ return decoded !== null && decoded.targetId >= ENTITY_ID_START;
169
+ }
170
+
171
+ //#endregion
172
+ //#region src/core/entity-manager.ts
173
+ /**
242
174
  * Entity ID Manager for automatic allocation and freelist recycling
243
175
  */
244
176
  var EntityIdManager = class {
@@ -330,80 +262,180 @@ var ComponentIdAllocator = class {
330
262
  return this.nextId <= COMPONENT_ID_MAX;
331
263
  }
332
264
  };
333
- const globalComponentIdAllocator = new ComponentIdAllocator();
334
- const ComponentIdForNames = /* @__PURE__ */ new Map();
335
- const componentNames = new Array(COMPONENT_ID_MAX + 1);
336
- const exclusiveFlags = new BitSet(COMPONENT_ID_MAX + 1);
337
- const cascadeDeleteFlags = new BitSet(COMPONENT_ID_MAX + 1);
338
- const dontFragmentFlags = new BitSet(COMPONENT_ID_MAX + 1);
339
- /**
340
- * Allocate a new component ID from the global allocator.
341
- * @param nameOrOptions Optional name for the component (for serialization/debugging) or options object
342
- * @returns The allocated component ID
343
- * @example
344
- * // Just a name
345
- * const Position = component<Position>("Position");
346
- *
347
- * // With options
348
- * const ChildOf = component({ exclusive: true, cascadeDelete: true });
349
- *
350
- * // With name and options
351
- * const ChildOf = component({ name: "ChildOf", exclusive: true });
352
- */
353
- function component(nameOrOptions) {
354
- const id = globalComponentIdAllocator.allocate();
355
- let name;
356
- let options;
357
- if (typeof nameOrOptions === "string") name = nameOrOptions;
358
- else if (typeof nameOrOptions === "object" && nameOrOptions !== null) {
359
- options = nameOrOptions;
360
- name = options.name;
265
+
266
+ //#endregion
267
+ //#region src/utils/bit-set.ts
268
+ var BitSet = class {
269
+ data;
270
+ _length;
271
+ constructor(length) {
272
+ this._length = length;
273
+ const numWords = Math.ceil(length / 32);
274
+ this.data = new Uint32Array(numWords);
361
275
  }
362
- if (name) {
363
- if (ComponentIdForNames.has(name)) throw new Error(`Component name "${name}" is already registered`);
364
- componentNames[id] = name;
365
- ComponentIdForNames.set(name, id);
276
+ get length() {
277
+ return this._length;
366
278
  }
367
- if (options) {
368
- if (options.exclusive) exclusiveFlags.set(id);
369
- if (options.cascadeDelete) cascadeDeleteFlags.set(id);
370
- if (options.dontFragment) dontFragmentFlags.set(id);
279
+ has(index) {
280
+ if (index < 0 || index >= this._length) return false;
281
+ const word = index >>> 5;
282
+ const bit = index & 31;
283
+ return (this.data[word] >>> bit & 1) !== 0;
371
284
  }
372
- return id;
373
- }
374
- /**
375
- * Get a component ID by its registered name
376
- * @param name The component name
377
- * @returns The component ID if found, undefined otherwise
378
- */
379
- function getComponentIdByName(name) {
380
- return ComponentIdForNames.get(name);
381
- }
382
- /** Get a component name by its ID
383
- * @param id The component ID
384
- * @returns The component name if found, undefined otherwise
385
- */
386
- function getComponentNameById(id) {
387
- return componentNames[id];
388
- }
389
- /**
390
- * Check if a component is marked as exclusive
391
- * @param id The component ID
392
- * @returns true if the component is exclusive, false otherwise
393
- */
394
- function isExclusiveComponent(id) {
395
- return exclusiveFlags.has(id);
396
- }
397
- /**
398
- * Check if a component is marked as dontFragment
399
- * @param id The component ID
400
- * @returns true if the component is dontFragment, false otherwise
401
- */
402
- function isDontFragmentComponent(id) {
403
- return dontFragmentFlags.has(id);
404
- }
405
- /**
406
- * Generic function to check relation flags with specific target conditions
285
+ set(index) {
286
+ if (index < 0 || index >= this._length) return;
287
+ const word = index >>> 5;
288
+ const bit = index & 31;
289
+ this.data[word] |= 1 << bit;
290
+ }
291
+ clear(index) {
292
+ if (index < 0 || index >= this._length) return;
293
+ const word = index >>> 5;
294
+ const bit = index & 31;
295
+ this.data[word] &= ~(1 << bit);
296
+ }
297
+ setRange(lo, hi) {
298
+ if (lo > hi) return;
299
+ if (lo < 0) lo = 0;
300
+ if (hi >= this._length) hi = this._length - 1;
301
+ const firstWord = lo >>> 5;
302
+ const lastWord = hi >>> 5;
303
+ const loBit = lo & 31;
304
+ const hiBit = hi & 31;
305
+ const maskFor = (a, b) => {
306
+ const width = b - a + 1;
307
+ if (width <= 0) return 0;
308
+ if (width >= 32) return 4294967295;
309
+ return (1 << width) - 1 << a >>> 0;
310
+ };
311
+ if (firstWord === lastWord) {
312
+ const mask = maskFor(loBit, hiBit);
313
+ this.data[firstWord] = (this.data[firstWord] | mask) >>> 0;
314
+ return;
315
+ }
316
+ const firstMask = maskFor(loBit, 31);
317
+ this.data[firstWord] = (this.data[firstWord] | firstMask) >>> 0;
318
+ for (let w = firstWord + 1; w <= lastWord - 1; w++) this.data[w] = 4294967295;
319
+ const lastMask = maskFor(0, hiBit);
320
+ this.data[lastWord] = (this.data[lastWord] | lastMask) >>> 0;
321
+ }
322
+ anyClearInRange(lo, hi) {
323
+ if (lo > hi) return false;
324
+ if (lo < 0) lo = 0;
325
+ if (hi >= this._length) hi = this._length - 1;
326
+ const firstWord = lo >>> 5;
327
+ const lastWord = hi >>> 5;
328
+ const loBit = lo & 31;
329
+ const hiBit = hi & 31;
330
+ const maskFor = (a, b) => {
331
+ const width = b - a + 1;
332
+ if (width <= 0) return 0;
333
+ if (width >= 32) return 4294967295;
334
+ return (1 << width) - 1 << a >>> 0;
335
+ };
336
+ if (firstWord === lastWord) {
337
+ const mask = maskFor(loBit, hiBit);
338
+ return (this.data[firstWord] & mask) >>> 0 !== mask >>> 0;
339
+ }
340
+ const firstMask = maskFor(loBit, 31);
341
+ if ((this.data[firstWord] & firstMask) >>> 0 !== firstMask >>> 0) return true;
342
+ for (let w = firstWord + 1; w <= lastWord - 1; w++) if (this.data[w] !== 4294967295) return true;
343
+ const lastMask = maskFor(0, hiBit);
344
+ if ((this.data[lastWord] & lastMask) >>> 0 !== lastMask >>> 0) return true;
345
+ return false;
346
+ }
347
+ reset() {
348
+ this.data.fill(0);
349
+ }
350
+ *[Symbol.iterator]() {
351
+ for (let wordIndex = 0; wordIndex < this.data.length; wordIndex++) {
352
+ let word = this.data[wordIndex];
353
+ if (word === 0) continue;
354
+ const baseIndex = wordIndex * 32;
355
+ for (let bit = 0; bit < 32 && baseIndex + bit < this._length; bit++) {
356
+ if (word & 1) yield baseIndex + bit;
357
+ word >>>= 1;
358
+ }
359
+ }
360
+ }
361
+ };
362
+
363
+ //#endregion
364
+ //#region src/core/component-registry.ts
365
+ const globalComponentIdAllocator = new ComponentIdAllocator();
366
+ const ComponentIdForNames = /* @__PURE__ */ new Map();
367
+ const componentNames = new Array(COMPONENT_ID_MAX + 1);
368
+ const exclusiveFlags = new BitSet(COMPONENT_ID_MAX + 1);
369
+ const cascadeDeleteFlags = new BitSet(COMPONENT_ID_MAX + 1);
370
+ const dontFragmentFlags = new BitSet(COMPONENT_ID_MAX + 1);
371
+ /**
372
+ * Allocate a new component ID from the global allocator.
373
+ * @param nameOrOptions Optional name for the component (for serialization/debugging) or options object
374
+ * @returns The allocated component ID
375
+ * @example
376
+ * // Just a name
377
+ * const Position = component<Position>("Position");
378
+ *
379
+ * // With options
380
+ * const ChildOf = component({ exclusive: true, cascadeDelete: true });
381
+ *
382
+ * // With name and options
383
+ * const ChildOf = component({ name: "ChildOf", exclusive: true });
384
+ */
385
+ function component(nameOrOptions) {
386
+ const id = globalComponentIdAllocator.allocate();
387
+ let name;
388
+ let options;
389
+ if (typeof nameOrOptions === "string") name = nameOrOptions;
390
+ else if (typeof nameOrOptions === "object" && nameOrOptions !== null) {
391
+ options = nameOrOptions;
392
+ name = options.name;
393
+ }
394
+ if (name) {
395
+ if (ComponentIdForNames.has(name)) throw new Error(`Component name "${name}" is already registered`);
396
+ componentNames[id] = name;
397
+ ComponentIdForNames.set(name, id);
398
+ }
399
+ if (options) {
400
+ if (options.exclusive) exclusiveFlags.set(id);
401
+ if (options.cascadeDelete) cascadeDeleteFlags.set(id);
402
+ if (options.dontFragment) dontFragmentFlags.set(id);
403
+ }
404
+ return id;
405
+ }
406
+ /**
407
+ * Get a component ID by its registered name
408
+ * @param name The component name
409
+ * @returns The component ID if found, undefined otherwise
410
+ */
411
+ function getComponentIdByName(name) {
412
+ return ComponentIdForNames.get(name);
413
+ }
414
+ /** Get a component name by its ID
415
+ * @param id The component ID
416
+ * @returns The component name if found, undefined otherwise
417
+ */
418
+ function getComponentNameById(id) {
419
+ return componentNames[id];
420
+ }
421
+ /**
422
+ * Check if a component is marked as exclusive
423
+ * @param id The component ID
424
+ * @returns true if the component is exclusive, false otherwise
425
+ */
426
+ function isExclusiveComponent(id) {
427
+ return exclusiveFlags.has(id);
428
+ }
429
+ /**
430
+ * Check if a component is marked as dontFragment
431
+ * @param id The component ID
432
+ * @returns true if the component is dontFragment, false otherwise
433
+ */
434
+ function isDontFragmentComponent(id) {
435
+ return dontFragmentFlags.has(id);
436
+ }
437
+ /**
438
+ * Generic function to check relation flags with specific target conditions
407
439
  * @param id The entity/relation ID to check
408
440
  * @param flagBitSet The bitset for the flag
409
441
  * @param targetCondition Function to check target ID condition
@@ -443,920 +475,803 @@ function isDontFragmentWildcard(id) {
443
475
  function isCascadeDeleteRelation(id) {
444
476
  return checkRelationFlag(id, cascadeDeleteFlags, (targetId) => targetId !== WILDCARD_TARGET_ID && targetId >= ENTITY_ID_START);
445
477
  }
446
- /**
447
- * Get the componentId from a relation ID without fully decoding the relation.
448
- * Returns undefined for non-relation IDs or invalid component IDs.
449
- */
450
- function getComponentIdFromRelationId(id) {
451
- const decoded = decodeRelationRaw(id);
452
- if (decoded === null || !isValidComponentId(decoded.componentId)) return void 0;
453
- return decoded.componentId;
454
- }
455
- /**
456
- * Get the targetId from a relation ID without fully decoding the relation.
457
- * Returns undefined for non-relation IDs.
458
- */
459
- function getTargetIdFromRelationId(id) {
460
- return decodeRelationRaw(id)?.targetId;
461
- }
462
- /**
463
- * Check if an ID is an entity-relation (relation targeting an entity, not a component or wildcard)
464
- */
465
- function isEntityRelation(id) {
466
- const decoded = decodeRelationRaw(id);
467
- return decoded !== null && decoded.targetId >= ENTITY_ID_START;
468
- }
469
478
 
470
479
  //#endregion
471
- //#region src/types.ts
472
- function isOptionalEntityId(type) {
473
- return typeof type === "object" && type !== null && "optional" in type;
474
- }
480
+ //#region src/core/builder.ts
481
+ var EntityBuilder = class {
482
+ world;
483
+ components = [];
484
+ constructor(world) {
485
+ this.world = world;
486
+ }
487
+ with(componentId, value) {
488
+ this.components.push({
489
+ type: "component",
490
+ id: componentId,
491
+ value
492
+ });
493
+ return this;
494
+ }
495
+ withTag(componentId) {
496
+ this.components.push({
497
+ type: "component",
498
+ id: componentId,
499
+ value: void 0
500
+ });
501
+ return this;
502
+ }
503
+ withRelation(componentId, targetEntity, value) {
504
+ this.components.push({
505
+ type: "relation",
506
+ componentId,
507
+ targetId: targetEntity,
508
+ value
509
+ });
510
+ return this;
511
+ }
512
+ withRelationTag(componentId, targetEntity) {
513
+ this.components.push({
514
+ type: "relation",
515
+ componentId,
516
+ targetId: targetEntity,
517
+ value: void 0
518
+ });
519
+ return this;
520
+ }
521
+ /**
522
+ * Create an entity and enqueue components to be applied. This method
523
+ * does NOT call `world.sync()` automatically; callers must invoke
524
+ * `world.sync()` to apply deferred commands.
525
+ * (Previously auto-synced; now a breaking change — buildDeferred() removed.)
526
+ */
527
+ build() {
528
+ const entity = this.world.new();
529
+ for (const def of this.components) if (def.type === "component") this.world.set(entity, def.id, def.value);
530
+ else {
531
+ const relationId = relation(def.componentId, def.targetId);
532
+ this.world.set(entity, relationId, def.value);
533
+ }
534
+ return entity;
535
+ }
536
+ };
475
537
 
476
538
  //#endregion
477
- //#region src/utils.ts
478
- /**
479
- * Utility functions for ECS library
480
- */
539
+ //#region src/commands/changeset.ts
481
540
  /**
482
- * Get a value from cache or compute and cache it if not present
483
- * @param cache The cache map
484
- * @param key The cache key
485
- * @param compute Function to compute the value if not cached
486
- * @returns The cached or computed value
541
+ * @internal Represents a set of component changes to be applied to an entity
487
542
  */
488
- function getOrComputeCache(cache, key, compute) {
489
- let value = cache.get(key);
490
- if (value === void 0) {
491
- value = compute();
492
- cache.set(key, value);
493
- }
494
- return value;
495
- }
496
- /**
497
- * Get a value from cache or create and cache it if not present, allowing side effects during creation
498
- * @param cache The cache map
499
- * @param key The cache key
500
- * @param create Function to create the value if not cached (can have side effects)
501
- * @returns The cached or created value
502
- */
503
- function getOrCreateWithSideEffect(cache, key, create) {
504
- let value = cache.get(key);
505
- if (value === void 0) {
506
- value = create();
507
- cache.set(key, value);
508
- }
509
- return value;
510
- }
511
-
512
- //#endregion
513
- //#region src/archetype.ts
514
- /**
515
- * Special value to represent missing component data
516
- */
517
- const MISSING_COMPONENT = Symbol("missing component");
518
- /**
519
- * Archetype class for ECS architecture
520
- * Represents a group of entities that share the same set of components
521
- * Optimized for fast iteration and component access
522
- */
523
- var Archetype = class {
524
- /**
525
- * The component types that define this archetype
526
- */
527
- componentTypes;
528
- /**
529
- * List of entities in this archetype
530
- */
531
- entities = [];
532
- /**
533
- * Component data storage - maps component type to array of component data
534
- * Each array index corresponds to the entity index in the entities array
535
- */
536
- componentData = /* @__PURE__ */ new Map();
537
- /**
538
- * Reverse mapping from entity to its index in this archetype
539
- */
540
- entityToIndex = /* @__PURE__ */ new Map();
541
- /**
542
- * Reference to dontFragment relations storage from World
543
- * This allows entities with different relation targets to share the same archetype
544
- * Stored in World to avoid migration overhead when entities change archetypes
545
- */
546
- dontFragmentRelations;
543
+ var ComponentChangeset = class {
544
+ adds = /* @__PURE__ */ new Map();
545
+ removes = /* @__PURE__ */ new Set();
547
546
  /**
548
- * Cache for pre-computed component data sources to avoid repeated calculations
549
- * For regular components: data array
550
- * For wildcards: matching relation types array
547
+ * Add a component to the changeset
551
548
  */
552
- componentDataSourcesCache = /* @__PURE__ */ new Map();
549
+ set(componentType, component$1) {
550
+ this.adds.set(componentType, component$1);
551
+ this.removes.delete(componentType);
552
+ }
553
553
  /**
554
- * Create a new archetype with the specified component types
555
- * @param componentTypes The component types that define this archetype
556
- * @param dontFragmentRelations Reference to the World's dontFragmentRelations storage
554
+ * Remove a component from the changeset
557
555
  */
558
- constructor(componentTypes, dontFragmentRelations) {
559
- this.componentTypes = [...componentTypes].sort((a, b) => a - b);
560
- this.dontFragmentRelations = dontFragmentRelations;
561
- for (const componentType of this.componentTypes) this.componentData.set(componentType, []);
556
+ delete(componentType) {
557
+ this.removes.add(componentType);
558
+ this.adds.delete(componentType);
562
559
  }
563
560
  /**
564
- * Get the number of entities in this archetype
561
+ * Check if the changeset has any changes
565
562
  */
566
- get size() {
567
- return this.entities.length;
563
+ hasChanges() {
564
+ return this.adds.size > 0 || this.removes.size > 0;
568
565
  }
569
566
  /**
570
- * Check if this archetype matches the given component types
571
- * @param componentTypes The component types to check
567
+ * Clear all changes
572
568
  */
573
- matches(componentTypes) {
574
- if (this.componentTypes.length !== componentTypes.length) return false;
575
- const sortedTypes = [...componentTypes].sort((a, b) => a - b);
576
- return this.componentTypes.every((type, index) => type === sortedTypes[index]);
569
+ clear() {
570
+ this.adds.clear();
571
+ this.removes.clear();
577
572
  }
578
573
  /**
579
- * Add an entity to this archetype with initial component data
580
- * @param entityId The entity to add
581
- * @param componentData Map of component type to component data (includes both regular and dontFragment components)
574
+ * Merge another changeset into this one
582
575
  */
583
- addEntity(entityId, componentData) {
584
- if (this.entityToIndex.has(entityId)) throw new Error(`Entity ${entityId} is already in this archetype`);
585
- const index = this.entities.length;
586
- this.entities.push(entityId);
587
- this.entityToIndex.set(entityId, index);
588
- for (const componentType of this.componentTypes) {
589
- const data = componentData.get(componentType);
590
- this.getComponentData(componentType).push(!componentData.has(componentType) ? MISSING_COMPONENT : data);
576
+ merge(other) {
577
+ for (const [componentType, component$1] of other.adds) {
578
+ this.adds.set(componentType, component$1);
579
+ this.removes.delete(componentType);
591
580
  }
592
- const dontFragmentData = /* @__PURE__ */ new Map();
593
- for (const [componentType, data] of componentData) {
594
- if (this.componentTypes.includes(componentType)) continue;
595
- const detailedType = getDetailedIdType(componentType);
596
- if ((detailedType.type === "entity-relation" || detailedType.type === "component-relation") && isDontFragmentComponent(detailedType.componentId)) dontFragmentData.set(componentType, data);
581
+ for (const componentType of other.removes) {
582
+ this.removes.add(componentType);
583
+ this.adds.delete(componentType);
597
584
  }
598
- if (dontFragmentData.size > 0) this.dontFragmentRelations.set(entityId, dontFragmentData);
599
585
  }
600
586
  /**
601
- * Get all component data for a specific entity
602
- * @param entityId The entity to get data for
603
- * @returns Map of component type to component data (includes both regular and dontFragment components)
587
+ * Apply the changeset to existing components and return the final state
604
588
  */
605
- getEntity(entityId) {
606
- const index = this.entityToIndex.get(entityId);
607
- if (index === void 0) return;
608
- const entityData = /* @__PURE__ */ new Map();
609
- for (const componentType of this.componentTypes) {
610
- const data = this.getComponentData(componentType)[index];
611
- entityData.set(componentType, data === MISSING_COMPONENT ? void 0 : data);
612
- }
613
- const dontFragmentData = this.dontFragmentRelations.get(entityId);
614
- if (dontFragmentData) for (const [componentType, data] of dontFragmentData) entityData.set(componentType, data);
615
- return entityData;
589
+ applyTo(existingComponents) {
590
+ for (const componentType of this.removes) existingComponents.delete(componentType);
591
+ for (const [componentType, component$1] of this.adds) existingComponents.set(componentType, component$1);
592
+ return existingComponents;
616
593
  }
617
594
  /**
618
- * Dump all entities and their component data in this archetype
619
- * @returns Array of objects with entity and component data (includes both regular and dontFragment components)
595
+ * Get the final component types after applying the changeset
596
+ * @param existingComponentTypes - The current component types on the entity
597
+ * @returns The final component types or undefined if no changes
620
598
  */
621
- dump() {
622
- const result = [];
623
- for (let i = 0; i < this.entities.length; i++) {
624
- const entity = this.entities[i];
625
- const components = /* @__PURE__ */ new Map();
626
- for (const componentType of this.componentTypes) {
627
- const data = this.getComponentData(componentType)[i];
628
- components.set(componentType, data === MISSING_COMPONENT ? void 0 : data);
599
+ getFinalComponentTypes(existingComponentTypes) {
600
+ const finalComponentTypes = new Set(existingComponentTypes);
601
+ let changed = false;
602
+ for (const componentType of this.removes) {
603
+ if (!finalComponentTypes.has(componentType)) {
604
+ this.removes.delete(componentType);
605
+ continue;
629
606
  }
630
- const dontFragmentData = this.dontFragmentRelations.get(entity);
631
- if (dontFragmentData) for (const [componentType, data] of dontFragmentData) components.set(componentType, data);
632
- result.push({
633
- entity,
634
- components
635
- });
607
+ changed = true;
608
+ finalComponentTypes.delete(componentType);
636
609
  }
637
- return result;
610
+ for (const componentType of this.adds.keys()) {
611
+ if (finalComponentTypes.has(componentType)) continue;
612
+ changed = true;
613
+ finalComponentTypes.add(componentType);
614
+ }
615
+ return changed ? Array.from(finalComponentTypes) : void 0;
638
616
  }
617
+ };
618
+
619
+ //#endregion
620
+ //#region src/commands/command-buffer.ts
621
+ /**
622
+ * Command buffer for deferred structural changes
623
+ */
624
+ var CommandBuffer = class {
625
+ commands = [];
626
+ executeEntityCommands;
639
627
  /**
640
- * Remove an entity from this archetype
641
- * @param entityId The entity to remove
642
- * @returns The component data of the removed entity (includes both regular and dontFragment components)
628
+ * Create a command buffer with an executor function
643
629
  */
644
- removeEntity(entityId) {
645
- const index = this.entityToIndex.get(entityId);
646
- if (index === void 0) return;
647
- const removedData = /* @__PURE__ */ new Map();
648
- for (const componentType of this.componentTypes) {
649
- const dataArray = this.getComponentData(componentType);
650
- removedData.set(componentType, dataArray[index]);
651
- }
652
- const dontFragmentData = this.dontFragmentRelations.get(entityId);
653
- if (dontFragmentData) {
654
- for (const [componentType, data] of dontFragmentData) removedData.set(componentType, data);
655
- this.dontFragmentRelations.delete(entityId);
656
- }
657
- this.entityToIndex.delete(entityId);
658
- const lastIndex = this.entities.length - 1;
659
- if (index !== lastIndex) {
660
- const lastEntity = this.entities[lastIndex];
661
- this.entities[index] = lastEntity;
662
- this.entityToIndex.set(lastEntity, index);
663
- for (const componentType of this.componentTypes) {
664
- const dataArray = this.getComponentData(componentType);
665
- dataArray[index] = dataArray[lastIndex];
666
- }
667
- }
668
- this.entities.pop();
669
- for (const componentType of this.componentTypes) this.getComponentData(componentType).pop();
670
- return removedData;
630
+ constructor(executeEntityCommands) {
631
+ this.executeEntityCommands = executeEntityCommands;
632
+ }
633
+ set(entityId, componentType, component$1) {
634
+ this.commands.push({
635
+ type: "set",
636
+ entityId,
637
+ componentType,
638
+ component: component$1
639
+ });
671
640
  }
672
641
  /**
673
- * Check if an entity is in this archetype
674
- * @param entityId The entity to check
642
+ * Remove a component from an entity (deferred)
675
643
  */
676
- exists(entityId) {
677
- return this.entityToIndex.has(entityId);
678
- }
679
- get(entityId, componentType) {
680
- const index = this.entityToIndex.get(entityId);
681
- if (index === void 0) throw new Error(`Entity ${entityId} is not in this archetype`);
682
- if (isWildcardRelationId(componentType)) {
683
- const componentId = getComponentIdFromRelationId(componentType);
684
- const relations = [];
685
- for (const relType of this.componentTypes) {
686
- const relDetailed = getDetailedIdType(relType);
687
- if ((relDetailed.type === "entity-relation" || relDetailed.type === "component-relation") && relDetailed.componentId === componentId) {
688
- const dataArray = this.getComponentData(relType);
689
- if (dataArray && dataArray[index] !== void 0) {
690
- const data = dataArray[index];
691
- relations.push([relDetailed.targetId, data === MISSING_COMPONENT ? void 0 : data]);
692
- }
693
- }
694
- }
695
- const dontFragmentData = this.dontFragmentRelations.get(entityId);
696
- if (dontFragmentData) for (const [relType, data] of dontFragmentData) {
697
- const relDetailed = getDetailedIdType(relType);
698
- if ((relDetailed.type === "entity-relation" || relDetailed.type === "component-relation") && relDetailed.componentId === componentId) relations.push([relDetailed.targetId, data]);
699
- }
700
- return relations;
701
- } else {
702
- if (this.componentTypes.includes(componentType)) {
703
- const data = this.getComponentData(componentType)[index];
704
- if (data === MISSING_COMPONENT) throw new Error(`Component type ${componentType} not found for entity ${entityId}`);
705
- return data;
706
- }
707
- const dontFragmentData = this.dontFragmentRelations.get(entityId);
708
- if (dontFragmentData && dontFragmentData.has(componentType)) return dontFragmentData.get(componentType);
709
- throw new Error(`Component type ${componentType} not found for entity ${entityId}`);
710
- }
644
+ remove(entityId, componentType) {
645
+ this.commands.push({
646
+ type: "delete",
647
+ entityId,
648
+ componentType
649
+ });
711
650
  }
712
651
  /**
713
- * Get optional component data for a specific entity and component type
714
- * @param entityId The entity
715
- * @param componentType The component type
716
- * @returns { value: T } if component exists, undefined otherwise
652
+ * Destroy an entity (deferred)
717
653
  */
718
- getOptional(entityId, componentType) {
719
- const index = this.entityToIndex.get(entityId);
720
- if (index === void 0) throw new Error(`Entity ${entityId} is not in this archetype`);
721
- if (this.componentTypes.includes(componentType)) {
722
- const data = this.getComponentData(componentType)[index];
723
- if (data === MISSING_COMPONENT) return;
724
- return { value: data };
725
- }
726
- const dontFragmentData = this.dontFragmentRelations.get(entityId);
727
- if (dontFragmentData && dontFragmentData.has(componentType)) return { value: dontFragmentData.get(componentType) };
654
+ delete(entityId) {
655
+ this.commands.push({
656
+ type: "destroy",
657
+ entityId
658
+ });
728
659
  }
729
660
  /**
730
- * Set component data for a specific entity and component type
731
- * @param entityId The entity
732
- * @param componentType The component type
733
- * @param data The component data
661
+ * Execute all commands and clear the buffer
734
662
  */
735
- set(entityId, componentType, data) {
736
- const index = this.entityToIndex.get(entityId);
737
- if (index === void 0) throw new Error(`Entity ${entityId} is not in this archetype`);
738
- if (this.componentData.has(componentType)) {
739
- const dataArray = this.getComponentData(componentType);
740
- dataArray[index] = data;
741
- return;
742
- }
743
- const detailedType = getDetailedIdType(componentType);
744
- if ((detailedType.type === "entity-relation" || detailedType.type === "component-relation") && isDontFragmentComponent(detailedType.componentId)) {
745
- let dontFragmentData = this.dontFragmentRelations.get(entityId);
746
- if (!dontFragmentData) {
747
- dontFragmentData = /* @__PURE__ */ new Map();
748
- this.dontFragmentRelations.set(entityId, dontFragmentData);
663
+ execute() {
664
+ const MAX_ITERATIONS = 100;
665
+ let iterations = 0;
666
+ while (this.commands.length > 0) {
667
+ if (iterations >= MAX_ITERATIONS) throw new Error("Command execution exceeded maximum iterations, possible infinite loop");
668
+ iterations++;
669
+ const currentCommands = [...this.commands];
670
+ this.commands = [];
671
+ const entityCommands = /* @__PURE__ */ new Map();
672
+ for (const cmd of currentCommands) {
673
+ if (!entityCommands.has(cmd.entityId)) entityCommands.set(cmd.entityId, []);
674
+ entityCommands.get(cmd.entityId).push(cmd);
749
675
  }
750
- dontFragmentData.set(componentType, data);
751
- return;
676
+ for (const [entityId, commands] of entityCommands) this.executeEntityCommands(entityId, commands);
752
677
  }
753
- throw new Error(`Component type ${componentType} is not in this archetype`);
754
678
  }
755
679
  /**
756
- * Get all entities in this archetype
680
+ * Get current commands (for testing)
757
681
  */
758
- getEntities() {
759
- return this.entities;
682
+ getCommands() {
683
+ return [...this.commands];
760
684
  }
761
685
  /**
762
- * Get the mapping of entities to their indices in this archetype
763
- */
764
- getEntityToIndexMap() {
765
- return this.entityToIndex;
766
- }
767
- /**
768
- * Get component data for all entities of a specific component type
769
- * @param componentType The component type
770
- */
771
- getComponentData(componentType) {
772
- const data = this.componentData.get(componentType);
773
- if (!data) throw new Error(`Component type ${componentType} is not in this archetype`);
774
- return data;
775
- }
776
- /**
777
- * Get optional component data for all entities of a specific component type
778
- * @param componentType The component type
779
- * @returns An array of component data or undefined if not present
780
- */
781
- getOptionalComponentData(componentType) {
782
- return this.componentData.get(componentType);
783
- }
784
- /**
785
- * Helper: compute or return cached data sources for provided componentTypes
786
- */
787
- getCachedComponentDataSources(componentTypes) {
788
- const cacheKey = this.buildCacheKey(componentTypes);
789
- return getOrComputeCache(this.componentDataSourcesCache, cacheKey, () => componentTypes.map((compType) => this.getComponentDataSource(compType)));
790
- }
791
- /**
792
- * Build cache key for component types
793
- */
794
- buildCacheKey(componentTypes) {
795
- return componentTypes.map((id) => isOptionalEntityId(id) ? `opt(${id.optional})` : `${id}`).join(",");
796
- }
797
- /**
798
- * Get data source for a single component type
686
+ * Clear all commands
799
687
  */
800
- getComponentDataSource(compType) {
801
- const optional = isOptionalEntityId(compType);
802
- const actualType = optional ? compType.optional : compType;
803
- const detailedType = getDetailedIdType(actualType);
804
- if (detailedType.type === "wildcard-relation") return this.getWildcardRelationDataSource(detailedType.componentId, optional);
805
- else return optional ? this.getOptionalComponentData(actualType) : this.getComponentData(actualType);
688
+ clear() {
689
+ this.commands = [];
806
690
  }
807
- /**
808
- * Get data source for wildcard relations
809
- */
810
- getWildcardRelationDataSource(componentId, optional) {
811
- const matchingRelations = this.componentTypes.filter((ct) => {
812
- const detailedCt = getDetailedIdType(ct);
813
- return (detailedCt.type === "entity-relation" || detailedCt.type === "component-relation") && detailedCt.componentId === componentId;
691
+ };
692
+
693
+ //#endregion
694
+ //#region src/query/filter.ts
695
+ /**
696
+ * Serialize a QueryFilter into a deterministic string suitable for cache keys.
697
+ * Currently only serializes `negativeComponentTypes`.
698
+ */
699
+ function serializeQueryFilter(filter = {}) {
700
+ const negative = (filter.negativeComponentTypes || []).slice().sort((a, b) => a - b);
701
+ if (negative.length === 0) return "";
702
+ return `neg:${negative.join(",")}`;
703
+ }
704
+ /**
705
+ * Check if an archetype matches the given component types
706
+ */
707
+ function matchesComponentTypes(archetype, componentTypes) {
708
+ return componentTypes.every((type) => {
709
+ const detailedType = getDetailedIdType(type);
710
+ if (detailedType.type === "wildcard-relation") return archetype.componentTypes.some((archetypeType) => {
711
+ if (!isRelationId(archetypeType)) return false;
712
+ return getComponentIdFromRelationId(archetypeType) === detailedType.componentId;
814
713
  });
815
- return optional ? matchingRelations.length > 0 ? matchingRelations : void 0 : matchingRelations;
816
- }
817
- /**
818
- * Helper: build component tuples for a specific entity index using precomputed data sources
819
- */
820
- buildComponentsForIndex(componentTypes, componentDataSources, entityIndex, entityId) {
821
- return componentDataSources.map((dataSource, i) => {
822
- const compType = componentTypes[i];
823
- return this.buildSingleComponent(compType, dataSource, entityIndex, entityId);
714
+ else return archetype.componentTypes.includes(type);
715
+ });
716
+ }
717
+ /**
718
+ * Check if an archetype matches the filter conditions (only filtering logic)
719
+ */
720
+ function matchesFilter(archetype, filter) {
721
+ return (filter.negativeComponentTypes || []).every((type) => {
722
+ const detailedType = getDetailedIdType(type);
723
+ if (detailedType.type === "wildcard-relation") return !archetype.componentTypes.some((archetypeType) => {
724
+ if (!isRelationId(archetypeType)) return false;
725
+ return getComponentIdFromRelationId(archetypeType) === detailedType.componentId;
824
726
  });
727
+ else return !archetype.componentTypes.includes(type);
728
+ });
729
+ }
730
+
731
+ //#endregion
732
+ //#region src/query/query.ts
733
+ /**
734
+ * Query class for efficient entity queries with cached archetypes
735
+ */
736
+ var Query = class {
737
+ world;
738
+ componentTypes;
739
+ filter;
740
+ cachedArchetypes = [];
741
+ isDisposed = false;
742
+ /** Cached wildcard component types for faster entity filtering */
743
+ wildcardTypes;
744
+ constructor(world, componentTypes, filter = {}) {
745
+ this.world = world;
746
+ this.componentTypes = [...componentTypes].sort((a, b) => a - b);
747
+ this.filter = filter;
748
+ this.wildcardTypes = this.componentTypes.filter((ct) => getDetailedIdType(ct).type === "wildcard-relation");
749
+ this.updateCache();
750
+ world._registerQuery(this);
825
751
  }
826
752
  /**
827
- * Build a single component value from its data source
753
+ * Check if query is disposed and throw error if so
828
754
  */
829
- buildSingleComponent(compType, dataSource, entityIndex, entityId) {
830
- const optional = isOptionalEntityId(compType);
831
- const actualType = optional ? compType.optional : compType;
832
- if (getIdType(actualType) === "wildcard-relation") return this.buildWildcardRelationValue(actualType, dataSource, entityIndex, entityId, optional);
833
- else return this.buildRegularComponentValue(dataSource, entityIndex, optional);
755
+ ensureNotDisposed() {
756
+ if (this.isDisposed) throw new Error("Query has been disposed");
834
757
  }
835
758
  /**
836
- * Build wildcard relation value from matching relations
759
+ * Get all entities matching the query
837
760
  */
838
- buildWildcardRelationValue(wildcardRelationType, dataSource, entityIndex, entityId, optional) {
839
- const matchingRelations = dataSource || [];
840
- const relations = [];
841
- for (const relType of matchingRelations) {
842
- const data = this.getComponentData(relType)[entityIndex];
843
- const targetId = getTargetIdFromRelationId(relType);
844
- relations.push([targetId, data === MISSING_COMPONENT ? void 0 : data]);
845
- }
846
- const targetComponentId = getComponentIdFromRelationId(wildcardRelationType);
847
- const dontFragmentData = this.dontFragmentRelations.get(entityId);
848
- if (dontFragmentData) for (const [relType, data] of dontFragmentData) {
849
- const relDetailed = getDetailedIdType(relType);
850
- if ((relDetailed.type === "entity-relation" || relDetailed.type === "component-relation") && relDetailed.componentId === targetComponentId) relations.push([relDetailed.targetId, data]);
851
- }
852
- if (relations.length === 0) {
853
- if (!optional) {
854
- const componentId = getComponentIdFromRelationId(wildcardRelationType);
855
- throw new Error(`No matching relations found for mandatory wildcard relation component ${componentId} on entity ${entityId}`);
856
- }
857
- return;
761
+ getEntities() {
762
+ this.ensureNotDisposed();
763
+ if (this.wildcardTypes.length === 0) {
764
+ const result$1 = [];
765
+ for (const archetype of this.cachedArchetypes) result$1.push(...archetype.getEntities());
766
+ return result$1;
858
767
  }
859
- return optional ? { value: relations } : relations;
768
+ const result = [];
769
+ for (const archetype of this.cachedArchetypes) for (const entity of archetype.getEntities()) if (this.entityHasAllWildcards(archetype, entity)) result.push(entity);
770
+ return result;
860
771
  }
861
772
  /**
862
- * Build regular component value from data source
773
+ * Check if entity has all required wildcard relations
863
774
  */
864
- buildRegularComponentValue(dataSource, entityIndex, optional) {
865
- if (dataSource === void 0) {
866
- if (optional) return;
867
- throw new Error(`Component data not found for mandatory component type`);
775
+ entityHasAllWildcards(archetype, entity) {
776
+ for (const wildcardType of this.wildcardTypes) {
777
+ const relations = archetype.get(entity, wildcardType);
778
+ if (!relations || relations.length === 0) return false;
868
779
  }
869
- const data = dataSource[entityIndex];
870
- const result = data === MISSING_COMPONENT ? void 0 : data;
871
- return optional ? { value: result } : result;
780
+ return true;
872
781
  }
873
782
  /**
874
- * Get entities with their component data for specified component types
875
- * Optimized for bulk component access with pre-computed indices
783
+ * Get entities with their component data
876
784
  * @param componentTypes Array of component types to retrieve
877
785
  * @returns Array of objects with entity and component data
878
786
  */
879
787
  getEntitiesWithComponents(componentTypes) {
788
+ this.ensureNotDisposed();
880
789
  const result = [];
881
- this.forEachWithComponents(componentTypes, (entity, ...components) => {
882
- result.push({
883
- entity,
884
- components
885
- });
886
- });
887
- return result;
888
- }
889
- /**
890
- * Iterate over entities with their component data for specified component types
891
- * implemented as a generator returning each entity/components pair lazily
892
- * @param componentTypes Array of component types to retrieve
893
- */
894
- *iterateWithComponents(componentTypes) {
895
- const componentDataSources = this.getCachedComponentDataSources(componentTypes);
896
- for (let entityIndex = 0; entityIndex < this.entities.length; entityIndex++) {
897
- const entity = this.entities[entityIndex];
898
- yield [entity, ...this.buildComponentsForIndex(componentTypes, componentDataSources, entityIndex, entity)];
790
+ for (const archetype of this.cachedArchetypes) {
791
+ const entitiesWithData = archetype.getEntitiesWithComponents(componentTypes);
792
+ result.push(...entitiesWithData);
899
793
  }
794
+ return result;
900
795
  }
901
796
  /**
902
- * Iterate over entities with their component data for specified component types
903
- * Optimized for bulk component access
797
+ * Iterate over entities with their component data
904
798
  * @param componentTypes Array of component types to retrieve
905
799
  * @param callback Function called for each entity with its components
906
800
  */
907
- forEachWithComponents(componentTypes, callback) {
908
- const componentDataSources = this.getCachedComponentDataSources(componentTypes);
909
- for (let entityIndex = 0; entityIndex < this.entities.length; entityIndex++) {
910
- const entity = this.entities[entityIndex];
911
- callback(entity, ...this.buildComponentsForIndex(componentTypes, componentDataSources, entityIndex, entity));
912
- }
801
+ forEach(componentTypes, callback) {
802
+ this.ensureNotDisposed();
803
+ for (const archetype of this.cachedArchetypes) archetype.forEachWithComponents(componentTypes, callback);
913
804
  }
914
805
  /**
915
- * Iterate over all entities with their component data
916
- * @param callback Function called for each entity with its component data
806
+ * Iterate over entities with their component data (generator)
807
+ * @param componentTypes Array of component types to retrieve
917
808
  */
918
- forEach(callback) {
919
- for (let i = 0; i < this.entities.length; i++) {
920
- const components = /* @__PURE__ */ new Map();
921
- for (const componentType of this.componentTypes) {
922
- const data = this.getComponentData(componentType)[i];
923
- components.set(componentType, data === MISSING_COMPONENT ? void 0 : data);
924
- }
925
- callback(this.entities[i], components);
926
- }
809
+ *iterate(componentTypes) {
810
+ this.ensureNotDisposed();
811
+ for (const archetype of this.cachedArchetypes) yield* archetype.iterateWithComponents(componentTypes);
927
812
  }
928
813
  /**
929
- * Check if any entity in this archetype has a relation matching the given component ID
930
- * This includes both regular relations in componentTypes and dontFragment relations
931
- * @param componentId The component ID to match
932
- * @returns true if any entity has a matching relation
814
+ * Get component data arrays for all matching entities
815
+ * @param componentType The component type to retrieve
816
+ * @returns Array of component data for all matching entities
933
817
  */
934
- hasRelationWithComponentId(componentId) {
935
- for (const componentType of this.componentTypes) {
936
- const detailedType = getDetailedIdType(componentType);
937
- if ((detailedType.type === "entity-relation" || detailedType.type === "component-relation") && detailedType.componentId === componentId) return true;
938
- }
939
- for (const entityId of this.entities) {
940
- const entityDontFragmentRelations = this.dontFragmentRelations.get(entityId);
941
- if (entityDontFragmentRelations) for (const relationType of entityDontFragmentRelations.keys()) {
942
- const detailedType = getDetailedIdType(relationType);
943
- if ((detailedType.type === "entity-relation" || detailedType.type === "component-relation") && detailedType.componentId === componentId) return true;
944
- }
945
- }
946
- return false;
818
+ getComponentData(componentType) {
819
+ this.ensureNotDisposed();
820
+ const result = [];
821
+ for (const archetype of this.cachedArchetypes) result.push(...archetype.getComponentData(componentType));
822
+ return result;
947
823
  }
948
- };
949
-
950
- //#endregion
951
- //#region src/changeset.ts
952
- /**
953
- * @internal Represents a set of component changes to be applied to an entity
954
- */
955
- var ComponentChangeset = class {
956
- adds = /* @__PURE__ */ new Map();
957
- removes = /* @__PURE__ */ new Set();
958
824
  /**
959
- * Add a component to the changeset
825
+ * Update the cached archetypes
826
+ * Called when new archetypes are created
960
827
  */
961
- set(componentType, component$1) {
962
- this.adds.set(componentType, component$1);
963
- this.removes.delete(componentType);
828
+ updateCache() {
829
+ if (this.isDisposed) return;
830
+ this.cachedArchetypes = this.world.getMatchingArchetypes(this.componentTypes).filter((archetype) => matchesFilter(archetype, this.filter));
964
831
  }
965
832
  /**
966
- * Remove a component from the changeset
833
+ * Check if a new archetype matches this query and add to cache if it does
967
834
  */
968
- delete(componentType) {
969
- this.removes.add(componentType);
970
- this.adds.delete(componentType);
835
+ checkNewArchetype(archetype) {
836
+ if (this.isDisposed) return;
837
+ if (matchesComponentTypes(archetype, this.componentTypes) && matchesFilter(archetype, this.filter) && !this.cachedArchetypes.includes(archetype)) this.cachedArchetypes.push(archetype);
971
838
  }
972
839
  /**
973
- * Check if the changeset has any changes
840
+ * Remove an archetype from the cached archetypes
974
841
  */
975
- hasChanges() {
976
- return this.adds.size > 0 || this.removes.size > 0;
842
+ removeArchetype(archetype) {
843
+ if (this.isDisposed) return;
844
+ const index = this.cachedArchetypes.indexOf(archetype);
845
+ if (index !== -1) this.cachedArchetypes.splice(index, 1);
977
846
  }
978
847
  /**
979
- * Clear all changes
848
+ * Request disposal of this query.
849
+ * This will decrement the world's reference count for the query.
850
+ * The query will only be fully disposed when the ref count reaches zero.
980
851
  */
981
- clear() {
982
- this.adds.clear();
983
- this.removes.clear();
852
+ dispose() {
853
+ this.world.releaseQuery(this);
984
854
  }
985
855
  /**
986
- * Merge another changeset into this one
856
+ * Internal full dispose called by World when refCount reaches zero.
987
857
  */
988
- merge(other) {
989
- for (const [componentType, component$1] of other.adds) {
990
- this.adds.set(componentType, component$1);
991
- this.removes.delete(componentType);
992
- }
993
- for (const componentType of other.removes) {
994
- this.removes.add(componentType);
995
- this.adds.delete(componentType);
858
+ _disposeInternal() {
859
+ if (!this.isDisposed) {
860
+ this.world._unregisterQuery(this);
861
+ this.cachedArchetypes = [];
862
+ this.isDisposed = true;
996
863
  }
997
864
  }
998
865
  /**
999
- * Apply the changeset to existing components and return the final state
866
+ * Symbol.dispose implementation for automatic resource management
1000
867
  */
1001
- applyTo(existingComponents) {
1002
- for (const componentType of this.removes) existingComponents.delete(componentType);
1003
- for (const [componentType, component$1] of this.adds) existingComponents.set(componentType, component$1);
1004
- return existingComponents;
868
+ [Symbol.dispose]() {
869
+ this.dispose();
1005
870
  }
1006
871
  /**
1007
- * Get the final component types after applying the changeset
1008
- * @param existingComponentTypes - The current component types on the entity
1009
- * @returns The final component types or undefined if no changes
872
+ * Check if the query has been disposed
1010
873
  */
1011
- getFinalComponentTypes(existingComponentTypes) {
1012
- const finalComponentTypes = new Set(existingComponentTypes);
1013
- let changed = false;
1014
- for (const componentType of this.removes) {
1015
- if (!finalComponentTypes.has(componentType)) {
1016
- this.removes.delete(componentType);
1017
- continue;
1018
- }
1019
- changed = true;
1020
- finalComponentTypes.delete(componentType);
1021
- }
1022
- for (const componentType of this.adds.keys()) {
1023
- if (finalComponentTypes.has(componentType)) continue;
1024
- changed = true;
1025
- finalComponentTypes.add(componentType);
1026
- }
1027
- return changed ? Array.from(finalComponentTypes) : void 0;
874
+ get disposed() {
875
+ return this.isDisposed;
1028
876
  }
1029
877
  };
1030
878
 
1031
879
  //#endregion
1032
- //#region src/command-buffer.ts
880
+ //#region src/utils/utils.ts
1033
881
  /**
1034
- * Command buffer for deferred structural changes
882
+ * Utility functions for ECS library
1035
883
  */
1036
- var CommandBuffer = class {
1037
- commands = [];
1038
- executeEntityCommands;
1039
- /**
1040
- * Create a command buffer with an executor function
1041
- */
1042
- constructor(executeEntityCommands) {
1043
- this.executeEntityCommands = executeEntityCommands;
884
+ /**
885
+ * Get a value from cache or compute and cache it if not present
886
+ * @param cache The cache map
887
+ * @param key The cache key
888
+ * @param compute Function to compute the value if not cached
889
+ * @returns The cached or computed value
890
+ */
891
+ function getOrComputeCache(cache, key, compute) {
892
+ let value = cache.get(key);
893
+ if (value === void 0) {
894
+ value = compute();
895
+ cache.set(key, value);
1044
896
  }
1045
- set(entityId, componentType, component$1) {
1046
- this.commands.push({
1047
- type: "set",
1048
- entityId,
1049
- componentType,
1050
- component: component$1
1051
- });
897
+ return value;
898
+ }
899
+ /**
900
+ * Get a value from cache or create and cache it if not present, allowing side effects during creation
901
+ * @param cache The cache map
902
+ * @param key The cache key
903
+ * @param create Function to create the value if not cached (can have side effects)
904
+ * @returns The cached or created value
905
+ */
906
+ function getOrCreateWithSideEffect(cache, key, create) {
907
+ let value = cache.get(key);
908
+ if (value === void 0) {
909
+ value = create();
910
+ cache.set(key, value);
1052
911
  }
912
+ return value;
913
+ }
914
+
915
+ //#endregion
916
+ //#region src/core/types.ts
917
+ function isOptionalEntityId(type) {
918
+ return typeof type === "object" && type !== null && "optional" in type;
919
+ }
920
+
921
+ //#endregion
922
+ //#region src/core/archetype-helpers.ts
923
+ /**
924
+ * Check if a detailed type represents a relation (entity or component)
925
+ */
926
+ function isRelationType(detailedType) {
927
+ return detailedType.type === "entity-relation" || detailedType.type === "component-relation";
928
+ }
929
+ /**
930
+ * Check if a component type matches a given component ID for relations
931
+ */
932
+ function matchesRelationComponentId(componentType, componentId) {
933
+ const detailedType = getDetailedIdType(componentType);
934
+ return isRelationType(detailedType) && detailedType.componentId === componentId;
935
+ }
936
+ /**
937
+ * Find all relations in dontFragment data that match a component ID
938
+ */
939
+ function findMatchingDontFragmentRelations(dontFragmentData, componentId) {
940
+ const relations = [];
941
+ if (!dontFragmentData) return relations;
942
+ for (const [relType, data] of dontFragmentData) {
943
+ const relDetailed = getDetailedIdType(relType);
944
+ if (isRelationType(relDetailed) && relDetailed.componentId === componentId) relations.push([relDetailed.targetId, data]);
945
+ }
946
+ return relations;
947
+ }
948
+ /**
949
+ * Build cache key for component types
950
+ */
951
+ function buildCacheKey(componentTypes) {
952
+ return componentTypes.map((id) => isOptionalEntityId(id) ? `opt(${id.optional})` : `${id}`).join(",");
953
+ }
954
+ /**
955
+ * Get data source for wildcard relations from component types
956
+ */
957
+ function getWildcardRelationDataSource(componentTypes, componentId, optional) {
958
+ const matchingRelations = componentTypes.filter((ct) => matchesRelationComponentId(ct, componentId));
959
+ return optional ? matchingRelations.length > 0 ? matchingRelations : void 0 : matchingRelations;
960
+ }
961
+ /**
962
+ * Build wildcard relation value from matching relations
963
+ */
964
+ function buildWildcardRelationValue(wildcardRelationType, matchingRelations, getDataAtIndex, dontFragmentData, entityId, optional) {
965
+ const relations = [];
966
+ const targetComponentId = getComponentIdFromRelationId(wildcardRelationType);
967
+ for (const relType of matchingRelations || []) {
968
+ const data = getDataAtIndex(relType);
969
+ const targetId = getTargetIdFromRelationId(relType);
970
+ relations.push([targetId, data === MISSING_COMPONENT ? void 0 : data]);
971
+ }
972
+ if (targetComponentId !== void 0) relations.push(...findMatchingDontFragmentRelations(dontFragmentData, targetComponentId));
973
+ if (relations.length === 0) {
974
+ if (!optional) {
975
+ const componentId = getComponentIdFromRelationId(wildcardRelationType);
976
+ throw new Error(`No matching relations found for mandatory wildcard relation component ${componentId} on entity ${entityId}`);
977
+ }
978
+ return;
979
+ }
980
+ return optional ? { value: relations } : relations;
981
+ }
982
+ /**
983
+ * Build regular component value from data source
984
+ */
985
+ function buildRegularComponentValue(dataSource, entityIndex, optional) {
986
+ if (dataSource === void 0) {
987
+ if (optional) return void 0;
988
+ throw new Error(`Component data not found for mandatory component type`);
989
+ }
990
+ const data = dataSource[entityIndex];
991
+ const result = data === MISSING_COMPONENT ? void 0 : data;
992
+ return optional ? { value: result } : result;
993
+ }
994
+ /**
995
+ * Build a single component value based on its type
996
+ */
997
+ function buildSingleComponent(compType, dataSource, entityIndex, entityId, getComponentData, dontFragmentRelations) {
998
+ const optional = isOptionalEntityId(compType);
999
+ const actualType = optional ? compType.optional : compType;
1000
+ if (getIdType(actualType) === "wildcard-relation") return buildWildcardRelationValue(actualType, dataSource, (relType) => getComponentData(relType)[entityIndex], dontFragmentRelations.get(entityId), entityId, optional);
1001
+ else return buildRegularComponentValue(dataSource, entityIndex, optional);
1002
+ }
1003
+
1004
+ //#endregion
1005
+ //#region src/core/archetype.ts
1006
+ /**
1007
+ * Special value to represent missing component data
1008
+ */
1009
+ const MISSING_COMPONENT = Symbol("missing component");
1010
+ /**
1011
+ * Archetype class for ECS architecture
1012
+ * Represents a group of entities that share the same set of components
1013
+ * Optimized for fast iteration and component access
1014
+ */
1015
+ var Archetype = class {
1053
1016
  /**
1054
- * Remove a component from an entity (deferred)
1017
+ * The component types that define this archetype
1055
1018
  */
1056
- remove(entityId, componentType) {
1057
- this.commands.push({
1058
- type: "delete",
1059
- entityId,
1060
- componentType
1061
- });
1062
- }
1019
+ componentTypes;
1063
1020
  /**
1064
- * Destroy an entity (deferred)
1021
+ * List of entities in this archetype
1065
1022
  */
1066
- delete(entityId) {
1067
- this.commands.push({
1068
- type: "destroy",
1069
- entityId
1070
- });
1071
- }
1023
+ entities = [];
1072
1024
  /**
1073
- * Execute all commands and clear the buffer
1025
+ * Component data storage - maps component type to array of component data
1026
+ * Each array index corresponds to the entity index in the entities array
1074
1027
  */
1075
- execute() {
1076
- const MAX_ITERATIONS = 100;
1077
- let iterations = 0;
1078
- while (this.commands.length > 0) {
1079
- if (iterations >= MAX_ITERATIONS) throw new Error("Command execution exceeded maximum iterations, possible infinite loop");
1080
- iterations++;
1081
- const currentCommands = [...this.commands];
1082
- this.commands = [];
1083
- const entityCommands = /* @__PURE__ */ new Map();
1084
- for (const cmd of currentCommands) {
1085
- if (!entityCommands.has(cmd.entityId)) entityCommands.set(cmd.entityId, []);
1086
- entityCommands.get(cmd.entityId).push(cmd);
1087
- }
1088
- for (const [entityId, commands] of entityCommands) this.executeEntityCommands(entityId, commands);
1089
- }
1090
- }
1028
+ componentData = /* @__PURE__ */ new Map();
1091
1029
  /**
1092
- * Get current commands (for testing)
1030
+ * Reverse mapping from entity to its index in this archetype
1093
1031
  */
1094
- getCommands() {
1095
- return [...this.commands];
1096
- }
1032
+ entityToIndex = /* @__PURE__ */ new Map();
1097
1033
  /**
1098
- * Clear all commands
1034
+ * Reference to dontFragment relations storage from World
1035
+ * This allows entities with different relation targets to share the same archetype
1036
+ * Stored in World to avoid migration overhead when entities change archetypes
1099
1037
  */
1100
- clear() {
1101
- this.commands = [];
1102
- }
1103
- };
1104
-
1105
- //#endregion
1106
- //#region src/multi-map.ts
1107
- var MultiMap = class {
1108
- map = /* @__PURE__ */ new Map();
1109
- _valueCount = 0;
1110
- get valueCount() {
1111
- return this._valueCount;
1112
- }
1113
- get keyCount() {
1114
- return this.map.size;
1038
+ dontFragmentRelations;
1039
+ /**
1040
+ * Cache for pre-computed component data sources to avoid repeated calculations
1041
+ */
1042
+ componentDataSourcesCache = /* @__PURE__ */ new Map();
1043
+ constructor(componentTypes, dontFragmentRelations) {
1044
+ this.componentTypes = [...componentTypes].sort((a, b) => a - b);
1045
+ this.dontFragmentRelations = dontFragmentRelations;
1046
+ for (const componentType of this.componentTypes) this.componentData.set(componentType, []);
1115
1047
  }
1116
- hasKey(key) {
1117
- return this.map.has(key);
1048
+ get size() {
1049
+ return this.entities.length;
1118
1050
  }
1119
- has(key, value) {
1120
- const set = this.map.get(key);
1121
- if (!set) return false;
1122
- if (arguments.length === 1) return true;
1123
- return set.has(value);
1051
+ matches(componentTypes) {
1052
+ if (this.componentTypes.length !== componentTypes.length) return false;
1053
+ const sortedTypes = [...componentTypes].sort((a, b) => a - b);
1054
+ return this.componentTypes.every((type, index) => type === sortedTypes[index]);
1124
1055
  }
1125
- add(key, value) {
1126
- let set = this.map.get(key);
1127
- if (!set) {
1128
- set = /* @__PURE__ */ new Set();
1129
- this.map.set(key, set);
1130
- }
1131
- if (!set.has(value)) {
1132
- set.add(value);
1133
- this._valueCount++;
1056
+ addEntity(entityId, componentData) {
1057
+ if (this.entityToIndex.has(entityId)) throw new Error(`Entity ${entityId} is already in this archetype`);
1058
+ const index = this.entities.length;
1059
+ this.entities.push(entityId);
1060
+ this.entityToIndex.set(entityId, index);
1061
+ for (const componentType of this.componentTypes) {
1062
+ const data = componentData.get(componentType);
1063
+ this.getComponentData(componentType).push(!componentData.has(componentType) ? MISSING_COMPONENT : data);
1134
1064
  }
1065
+ this.addDontFragmentRelations(entityId, componentData);
1135
1066
  }
1136
- remove(key, value) {
1137
- const set = this.map.get(key);
1138
- if (!set) return false;
1139
- if (!set.has(value)) return false;
1140
- set.delete(value);
1141
- this._valueCount--;
1142
- if (set.size === 0) this.map.delete(key);
1143
- return true;
1144
- }
1145
- deleteKey(key) {
1146
- const set = this.map.get(key);
1147
- if (!set) return false;
1148
- this._valueCount -= set.size;
1149
- this.map.delete(key);
1150
- return true;
1067
+ addDontFragmentRelations(entityId, componentData) {
1068
+ const dontFragmentData = /* @__PURE__ */ new Map();
1069
+ for (const [componentType, data] of componentData) {
1070
+ if (this.componentTypes.includes(componentType)) continue;
1071
+ const detailedType = getDetailedIdType(componentType);
1072
+ if (isRelationType(detailedType) && isDontFragmentComponent(detailedType.componentId)) dontFragmentData.set(componentType, data);
1073
+ }
1074
+ if (dontFragmentData.size > 0) this.dontFragmentRelations.set(entityId, dontFragmentData);
1151
1075
  }
1152
- get(key) {
1153
- const set = this.map.get(key);
1154
- return set ? new Set(set) : /* @__PURE__ */ new Set();
1076
+ getEntity(entityId) {
1077
+ const index = this.entityToIndex.get(entityId);
1078
+ if (index === void 0) return void 0;
1079
+ const entityData = /* @__PURE__ */ new Map();
1080
+ for (const componentType of this.componentTypes) {
1081
+ const data = this.getComponentData(componentType)[index];
1082
+ entityData.set(componentType, data === MISSING_COMPONENT ? void 0 : data);
1083
+ }
1084
+ const dontFragmentData = this.dontFragmentRelations.get(entityId);
1085
+ if (dontFragmentData) for (const [componentType, data] of dontFragmentData) entityData.set(componentType, data);
1086
+ return entityData;
1155
1087
  }
1156
- *keys() {
1157
- yield* this.map.keys();
1088
+ dump() {
1089
+ return this.entities.map((entity, i) => {
1090
+ const components = /* @__PURE__ */ new Map();
1091
+ for (const componentType of this.componentTypes) {
1092
+ const data = this.getComponentData(componentType)[i];
1093
+ components.set(componentType, data === MISSING_COMPONENT ? void 0 : data);
1094
+ }
1095
+ const dontFragmentData = this.dontFragmentRelations.get(entity);
1096
+ if (dontFragmentData) for (const [componentType, data] of dontFragmentData) components.set(componentType, data);
1097
+ return {
1098
+ entity,
1099
+ components
1100
+ };
1101
+ });
1158
1102
  }
1159
- *values() {
1160
- for (const set of this.map.values()) for (const v of set) yield v;
1103
+ removeEntity(entityId) {
1104
+ const index = this.entityToIndex.get(entityId);
1105
+ if (index === void 0) return void 0;
1106
+ const removedData = /* @__PURE__ */ new Map();
1107
+ for (const componentType of this.componentTypes) removedData.set(componentType, this.getComponentData(componentType)[index]);
1108
+ const dontFragmentData = this.dontFragmentRelations.get(entityId);
1109
+ if (dontFragmentData) {
1110
+ for (const [componentType, data] of dontFragmentData) removedData.set(componentType, data);
1111
+ this.dontFragmentRelations.delete(entityId);
1112
+ }
1113
+ this.entityToIndex.delete(entityId);
1114
+ const lastIndex = this.entities.length - 1;
1115
+ if (index !== lastIndex) {
1116
+ const lastEntity = this.entities[lastIndex];
1117
+ this.entities[index] = lastEntity;
1118
+ this.entityToIndex.set(lastEntity, index);
1119
+ for (const componentType of this.componentTypes) {
1120
+ const dataArray = this.getComponentData(componentType);
1121
+ dataArray[index] = dataArray[lastIndex];
1122
+ }
1123
+ }
1124
+ this.entities.pop();
1125
+ for (const componentType of this.componentTypes) this.getComponentData(componentType).pop();
1126
+ return removedData;
1161
1127
  }
1162
- [Symbol.iterator]() {
1163
- return this.entries();
1128
+ exists(entityId) {
1129
+ return this.entityToIndex.has(entityId);
1164
1130
  }
1165
- *entries() {
1166
- for (const [k, set] of this.map.entries()) for (const v of set) yield [k, v];
1131
+ get(entityId, componentType) {
1132
+ const index = this.entityToIndex.get(entityId);
1133
+ if (index === void 0) throw new Error(`Entity ${entityId} is not in this archetype`);
1134
+ if (isWildcardRelationId(componentType)) return this.getWildcardRelations(entityId, index, componentType);
1135
+ return this.getRegularComponent(entityId, index, componentType);
1167
1136
  }
1168
- clear() {
1169
- this.map.clear();
1170
- this._valueCount = 0;
1137
+ getWildcardRelations(entityId, index, componentType) {
1138
+ const componentId = getComponentIdFromRelationId(componentType);
1139
+ const relations = [];
1140
+ for (const relType of this.componentTypes) {
1141
+ const relDetailed = getDetailedIdType(relType);
1142
+ if (isRelationType(relDetailed) && relDetailed.componentId === componentId) {
1143
+ const dataArray = this.getComponentData(relType);
1144
+ if (dataArray && dataArray[index] !== void 0) {
1145
+ const data = dataArray[index];
1146
+ relations.push([relDetailed.targetId, data === MISSING_COMPONENT ? void 0 : data]);
1147
+ }
1148
+ }
1149
+ }
1150
+ if (componentId !== void 0) relations.push(...findMatchingDontFragmentRelations(this.dontFragmentRelations.get(entityId), componentId));
1151
+ return relations;
1171
1152
  }
1172
- };
1173
-
1174
- //#endregion
1175
- //#region src/query-filter.ts
1176
- /**
1177
- * Serialize a QueryFilter into a deterministic string suitable for cache keys.
1178
- * Currently only serializes `negativeComponentTypes`.
1179
- */
1180
- function serializeQueryFilter(filter = {}) {
1181
- const negative = (filter.negativeComponentTypes || []).slice().sort((a, b) => a - b);
1182
- if (negative.length === 0) return "";
1183
- return `neg:${negative.join(",")}`;
1184
- }
1185
- /**
1186
- * Check if an archetype matches the given component types
1187
- */
1188
- function matchesComponentTypes(archetype, componentTypes) {
1189
- return componentTypes.every((type) => {
1190
- const detailedType = getDetailedIdType(type);
1191
- if (detailedType.type === "wildcard-relation") return archetype.componentTypes.some((archetypeType) => {
1192
- if (!isRelationId(archetypeType)) return false;
1193
- return getComponentIdFromRelationId(archetypeType) === detailedType.componentId;
1194
- });
1195
- else return archetype.componentTypes.includes(type);
1196
- });
1197
- }
1198
- /**
1199
- * Check if an archetype matches the filter conditions (only filtering logic)
1200
- */
1201
- function matchesFilter(archetype, filter) {
1202
- return (filter.negativeComponentTypes || []).every((type) => {
1203
- const detailedType = getDetailedIdType(type);
1204
- if (detailedType.type === "wildcard-relation") return !archetype.componentTypes.some((archetypeType) => {
1205
- if (!isRelationId(archetypeType)) return false;
1206
- return getComponentIdFromRelationId(archetypeType) === detailedType.componentId;
1207
- });
1208
- else return !archetype.componentTypes.includes(type);
1209
- });
1210
- }
1211
-
1212
- //#endregion
1213
- //#region src/query.ts
1214
- /**
1215
- * Query class for efficient entity queries with cached archetypes
1216
- */
1217
- var Query = class {
1218
- world;
1219
- componentTypes;
1220
- filter;
1221
- cachedArchetypes = [];
1222
- isDisposed = false;
1223
- constructor(world, componentTypes, filter = {}) {
1224
- this.world = world;
1225
- this.componentTypes = [...componentTypes].sort((a, b) => a - b);
1226
- this.filter = filter;
1227
- this.updateCache();
1228
- world._registerQuery(this);
1153
+ getRegularComponent(entityId, index, componentType) {
1154
+ if (this.componentTypes.includes(componentType)) {
1155
+ const data = this.getComponentData(componentType)[index];
1156
+ if (data === MISSING_COMPONENT) throw new Error(`Component type ${componentType} not found for entity ${entityId}`);
1157
+ return data;
1158
+ }
1159
+ const dontFragmentData = this.dontFragmentRelations.get(entityId);
1160
+ if (dontFragmentData?.has(componentType)) return dontFragmentData.get(componentType);
1161
+ throw new Error(`Component type ${componentType} not found for entity ${entityId}`);
1229
1162
  }
1230
- /**
1231
- * Check if query is disposed and throw error if so
1232
- */
1233
- ensureNotDisposed() {
1234
- if (this.isDisposed) throw new Error("Query has been disposed");
1163
+ getOptional(entityId, componentType) {
1164
+ const index = this.entityToIndex.get(entityId);
1165
+ if (index === void 0) throw new Error(`Entity ${entityId} is not in this archetype`);
1166
+ if (this.componentTypes.includes(componentType)) {
1167
+ const data = this.getComponentData(componentType)[index];
1168
+ if (data === MISSING_COMPONENT) return void 0;
1169
+ return { value: data };
1170
+ }
1171
+ const dontFragmentData = this.dontFragmentRelations.get(entityId);
1172
+ if (dontFragmentData?.has(componentType)) return { value: dontFragmentData.get(componentType) };
1235
1173
  }
1236
- /**
1237
- * Get all entities matching the query
1238
- */
1239
- getEntities() {
1240
- this.ensureNotDisposed();
1241
- const result = [];
1242
- if (this.componentTypes.some((ct) => {
1243
- return getDetailedIdType(ct).type === "wildcard-relation";
1244
- })) for (const archetype of this.cachedArchetypes) for (const entity of archetype.getEntities()) {
1245
- let hasAllRelations = true;
1246
- for (const componentType of this.componentTypes) if (getDetailedIdType(componentType).type === "wildcard-relation") {
1247
- const relations = archetype.get(entity, componentType);
1248
- if (!relations || relations.length === 0) {
1249
- hasAllRelations = false;
1250
- break;
1251
- }
1174
+ set(entityId, componentType, data) {
1175
+ const index = this.entityToIndex.get(entityId);
1176
+ if (index === void 0) throw new Error(`Entity ${entityId} is not in this archetype`);
1177
+ if (this.componentData.has(componentType)) {
1178
+ this.getComponentData(componentType)[index] = data;
1179
+ return;
1180
+ }
1181
+ const detailedType = getDetailedIdType(componentType);
1182
+ if (isRelationType(detailedType) && isDontFragmentComponent(detailedType.componentId)) {
1183
+ let dontFragmentData = this.dontFragmentRelations.get(entityId);
1184
+ if (!dontFragmentData) {
1185
+ dontFragmentData = /* @__PURE__ */ new Map();
1186
+ this.dontFragmentRelations.set(entityId, dontFragmentData);
1252
1187
  }
1253
- if (hasAllRelations) result.push(entity);
1254
- }
1255
- else for (const archetype of this.cachedArchetypes) result.push(...archetype.getEntities());
1256
- return result;
1257
- }
1258
- /**
1259
- * Get entities with their component data
1260
- * @param componentTypes Array of component types to retrieve
1261
- * @returns Array of objects with entity and component data
1262
- */
1263
- getEntitiesWithComponents(componentTypes) {
1264
- this.ensureNotDisposed();
1265
- const result = [];
1266
- for (const archetype of this.cachedArchetypes) {
1267
- const entitiesWithData = archetype.getEntitiesWithComponents(componentTypes);
1268
- result.push(...entitiesWithData);
1188
+ dontFragmentData.set(componentType, data);
1189
+ return;
1269
1190
  }
1270
- return result;
1191
+ throw new Error(`Component type ${componentType} is not in this archetype`);
1271
1192
  }
1272
- /**
1273
- * Iterate over entities with their component data
1274
- * @param componentTypes Array of component types to retrieve
1275
- * @param callback Function called for each entity with its components
1276
- */
1277
- forEach(componentTypes, callback) {
1278
- this.ensureNotDisposed();
1279
- for (const archetype of this.cachedArchetypes) archetype.forEachWithComponents(componentTypes, callback);
1193
+ getEntities() {
1194
+ return this.entities;
1280
1195
  }
1281
- /**
1282
- * Iterate over entities with their component data (generator)
1283
- * @param componentTypes Array of component types to retrieve
1284
- */
1285
- *iterate(componentTypes) {
1286
- this.ensureNotDisposed();
1287
- for (const archetype of this.cachedArchetypes) yield* archetype.iterateWithComponents(componentTypes);
1196
+ getEntityToIndexMap() {
1197
+ return this.entityToIndex;
1288
1198
  }
1289
- /**
1290
- * Get component data arrays for all matching entities
1291
- * @param componentType The component type to retrieve
1292
- * @returns Array of component data for all matching entities
1293
- */
1294
1199
  getComponentData(componentType) {
1295
- this.ensureNotDisposed();
1296
- const result = [];
1297
- for (const archetype of this.cachedArchetypes) result.push(...archetype.getComponentData(componentType));
1298
- return result;
1200
+ const data = this.componentData.get(componentType);
1201
+ if (!data) throw new Error(`Component type ${componentType} is not in this archetype`);
1202
+ return data;
1299
1203
  }
1300
- /**
1301
- * Update the cached archetypes
1302
- * Called when new archetypes are created
1303
- */
1304
- updateCache() {
1305
- if (this.isDisposed) return;
1306
- this.cachedArchetypes = this.world.getMatchingArchetypes(this.componentTypes).filter((archetype) => matchesFilter(archetype, this.filter));
1204
+ getOptionalComponentData(componentType) {
1205
+ return this.componentData.get(componentType);
1307
1206
  }
1308
- /**
1309
- * Check if a new archetype matches this query and add to cache if it does
1310
- */
1311
- checkNewArchetype(archetype) {
1312
- if (this.isDisposed) return;
1313
- if (matchesComponentTypes(archetype, this.componentTypes) && matchesFilter(archetype, this.filter) && !this.cachedArchetypes.includes(archetype)) this.cachedArchetypes.push(archetype);
1207
+ getCachedComponentDataSources(componentTypes) {
1208
+ const cacheKey = buildCacheKey(componentTypes);
1209
+ return getOrComputeCache(this.componentDataSourcesCache, cacheKey, () => componentTypes.map((compType) => this.getComponentDataSource(compType)));
1314
1210
  }
1315
- /**
1316
- * Remove an archetype from the cached archetypes
1317
- */
1318
- removeArchetype(archetype) {
1319
- if (this.isDisposed) return;
1320
- const index = this.cachedArchetypes.indexOf(archetype);
1321
- if (index !== -1) this.cachedArchetypes.splice(index, 1);
1211
+ getComponentDataSource(compType) {
1212
+ const optional = isOptionalEntityId(compType);
1213
+ const actualType = optional ? compType.optional : compType;
1214
+ if (getIdType(actualType) === "wildcard-relation") {
1215
+ const componentId = getComponentIdFromRelationId(actualType);
1216
+ return getWildcardRelationDataSource(this.componentTypes, componentId, optional);
1217
+ }
1218
+ return optional ? this.getOptionalComponentData(actualType) : this.getComponentData(actualType);
1322
1219
  }
1323
- /**
1324
- * Dispose the query and disconnect from world
1325
- */
1326
- /**
1327
- * Request disposal of this query.
1328
- * This will decrement the world's reference count for the query.
1329
- * The query will only be fully disposed when the ref count reaches zero.
1330
- */
1331
- dispose() {
1332
- this.world.releaseQuery(this);
1220
+ buildComponentsForIndex(componentTypes, componentDataSources, entityIndex, entityId) {
1221
+ return componentDataSources.map((dataSource, i) => buildSingleComponent(componentTypes[i], dataSource, entityIndex, entityId, (type) => this.getComponentData(type), this.dontFragmentRelations));
1333
1222
  }
1334
- /**
1335
- * Internal full dispose called by World when refCount reaches zero.
1336
- */
1337
- _disposeInternal() {
1338
- if (!this.isDisposed) {
1339
- this.world._unregisterQuery(this);
1340
- this.cachedArchetypes = [];
1341
- this.isDisposed = true;
1223
+ getEntitiesWithComponents(componentTypes) {
1224
+ const result = [];
1225
+ this.forEachWithComponents(componentTypes, (entity, ...components) => {
1226
+ result.push({
1227
+ entity,
1228
+ components
1229
+ });
1230
+ });
1231
+ return result;
1232
+ }
1233
+ *iterateWithComponents(componentTypes) {
1234
+ const componentDataSources = this.getCachedComponentDataSources(componentTypes);
1235
+ for (let entityIndex = 0; entityIndex < this.entities.length; entityIndex++) {
1236
+ const entity = this.entities[entityIndex];
1237
+ yield [entity, ...this.buildComponentsForIndex(componentTypes, componentDataSources, entityIndex, entity)];
1342
1238
  }
1343
1239
  }
1344
- /**
1345
- * Symbol.dispose implementation for automatic resource management
1346
- */
1347
- [Symbol.dispose]() {
1348
- this.dispose();
1240
+ forEachWithComponents(componentTypes, callback) {
1241
+ const componentDataSources = this.getCachedComponentDataSources(componentTypes);
1242
+ for (let entityIndex = 0; entityIndex < this.entities.length; entityIndex++) {
1243
+ const entity = this.entities[entityIndex];
1244
+ callback(entity, ...this.buildComponentsForIndex(componentTypes, componentDataSources, entityIndex, entity));
1245
+ }
1349
1246
  }
1350
- /**
1351
- * Check if the query has been disposed
1352
- */
1353
- get disposed() {
1354
- return this.isDisposed;
1247
+ forEach(callback) {
1248
+ for (let i = 0; i < this.entities.length; i++) {
1249
+ const components = /* @__PURE__ */ new Map();
1250
+ for (const componentType of this.componentTypes) {
1251
+ const data = this.getComponentData(componentType)[i];
1252
+ components.set(componentType, data === MISSING_COMPONENT ? void 0 : data);
1253
+ }
1254
+ callback(this.entities[i], components);
1255
+ }
1256
+ }
1257
+ hasRelationWithComponentId(componentId) {
1258
+ for (const componentType of this.componentTypes) {
1259
+ const detailedType = getDetailedIdType(componentType);
1260
+ if (isRelationType(detailedType) && detailedType.componentId === componentId) return true;
1261
+ }
1262
+ for (const entityId of this.entities) {
1263
+ const entityDontFragmentRelations = this.dontFragmentRelations.get(entityId);
1264
+ if (entityDontFragmentRelations) for (const relationType of entityDontFragmentRelations.keys()) {
1265
+ const detailedType = getDetailedIdType(relationType);
1266
+ if (isRelationType(detailedType) && detailedType.componentId === componentId) return true;
1267
+ }
1268
+ }
1269
+ return false;
1355
1270
  }
1356
1271
  };
1357
1272
 
1358
1273
  //#endregion
1359
- //#region src/world.ts
1274
+ //#region src/core/serialization.ts
1360
1275
  /**
1361
1276
  * Encode an internal EntityId into a SerializedEntityId for snapshots
1362
1277
  */
@@ -1430,77 +1345,304 @@ function decodeSerializedId(sid) {
1430
1345
  } else targetId = sid.target;
1431
1346
  return relation(compId, targetId);
1432
1347
  }
1433
- throw new Error(`Invalid ID in snapshot: ${JSON.stringify(sid)}`);
1348
+ throw new Error(`Invalid ID in snapshot: ${JSON.stringify(sid)}`);
1349
+ }
1350
+
1351
+ //#endregion
1352
+ //#region src/core/world-commands.ts
1353
+ function processCommands(entityId, currentArchetype, commands, changeset, handleExclusiveRelation) {
1354
+ for (const command of commands) if (command.type === "set" && command.componentType) processSetCommand(entityId, currentArchetype, command.componentType, command.component, changeset, handleExclusiveRelation);
1355
+ else if (command.type === "delete" && command.componentType) processDeleteCommand(entityId, currentArchetype, command.componentType, changeset);
1356
+ }
1357
+ function processSetCommand(entityId, currentArchetype, componentType, component$1, changeset, handleExclusiveRelation) {
1358
+ const componentId = getComponentIdFromRelationId(componentType);
1359
+ if (componentId !== void 0) {
1360
+ handleExclusiveRelation(entityId, currentArchetype, componentId);
1361
+ if (isDontFragmentComponent(componentId)) {
1362
+ const wildcardMarker = relation(componentId, "*");
1363
+ if (!currentArchetype.componentTypes.includes(wildcardMarker)) changeset.set(wildcardMarker, void 0);
1364
+ }
1365
+ }
1366
+ changeset.set(componentType, component$1);
1367
+ }
1368
+ function processDeleteCommand(entityId, currentArchetype, componentType, changeset) {
1369
+ const componentId = getComponentIdFromRelationId(componentType);
1370
+ if (isWildcardRelationId(componentType) && componentId !== void 0) removeWildcardRelations(entityId, currentArchetype, componentId, changeset);
1371
+ else {
1372
+ changeset.delete(componentType);
1373
+ maybeRemoveWildcardMarker(entityId, currentArchetype, componentType, componentId, changeset);
1374
+ }
1375
+ }
1376
+ function removeMatchingRelations(entityId, archetype, baseComponentId, changeset) {
1377
+ for (const componentType of archetype.componentTypes) if (getComponentIdFromRelationId(componentType) === baseComponentId) changeset.delete(componentType);
1378
+ const entityData = archetype.getEntity(entityId);
1379
+ if (entityData) for (const [componentType] of entityData) {
1380
+ if (archetype.componentTypes.includes(componentType)) continue;
1381
+ if (getComponentIdFromRelationId(componentType) === baseComponentId) changeset.delete(componentType);
1382
+ }
1383
+ }
1384
+ function removeWildcardRelations(entityId, currentArchetype, baseComponentId, changeset) {
1385
+ removeMatchingRelations(entityId, currentArchetype, baseComponentId, changeset);
1386
+ if (isDontFragmentComponent(baseComponentId)) changeset.delete(relation(baseComponentId, "*"));
1387
+ }
1388
+ function maybeRemoveWildcardMarker(entityId, archetype, removedComponentType, componentId, changeset) {
1389
+ if (componentId === void 0 || !isDontFragmentComponent(componentId)) return;
1390
+ const wildcardMarker = relation(componentId, "*");
1391
+ const entityData = archetype.getEntity(entityId);
1392
+ if (!entityData) {
1393
+ changeset.delete(wildcardMarker);
1394
+ return;
1395
+ }
1396
+ for (const [otherComponentType] of entityData) {
1397
+ if (otherComponentType === removedComponentType) continue;
1398
+ if (otherComponentType === wildcardMarker) continue;
1399
+ if (changeset.removes.has(otherComponentType)) continue;
1400
+ if (getComponentIdFromRelationId(otherComponentType) === componentId) return;
1401
+ }
1402
+ changeset.delete(wildcardMarker);
1403
+ }
1404
+ function applyChangeset(ctx, entityId, currentArchetype, changeset, entityToArchetype) {
1405
+ const currentEntityData = currentArchetype.getEntity(entityId);
1406
+ const allCurrentComponentTypes = currentEntityData ? Array.from(currentEntityData.keys()) : currentArchetype.componentTypes;
1407
+ const finalComponentTypes = changeset.getFinalComponentTypes(allCurrentComponentTypes);
1408
+ const removedComponents = /* @__PURE__ */ new Map();
1409
+ if (finalComponentTypes) if (!areComponentTypesEqual(filterRegularComponentTypes(allCurrentComponentTypes), filterRegularComponentTypes(finalComponentTypes))) moveEntityToNewArchetype(ctx, entityId, currentArchetype, finalComponentTypes, changeset, removedComponents, entityToArchetype);
1410
+ else updateEntityInSameArchetype(ctx, entityId, currentArchetype, changeset, removedComponents);
1411
+ else updateEntityInSameArchetype(ctx, entityId, currentArchetype, changeset, removedComponents);
1412
+ return removedComponents;
1413
+ }
1414
+ function moveEntityToNewArchetype(ctx, entityId, currentArchetype, finalComponentTypes, changeset, removedComponents, entityToArchetype) {
1415
+ const newArchetype = ctx.ensureArchetype(finalComponentTypes);
1416
+ const currentComponents = currentArchetype.removeEntity(entityId);
1417
+ for (const componentType of changeset.removes) removedComponents.set(componentType, currentComponents.get(componentType));
1418
+ newArchetype.addEntity(entityId, changeset.applyTo(currentComponents));
1419
+ entityToArchetype.set(entityId, newArchetype);
1420
+ }
1421
+ function updateEntityInSameArchetype(ctx, entityId, currentArchetype, changeset, removedComponents) {
1422
+ applyDontFragmentChanges(ctx.dontFragmentRelations, entityId, changeset, removedComponents);
1423
+ for (const [componentType, component$1] of changeset.adds) {
1424
+ if (isDontFragmentRelation(componentType)) continue;
1425
+ currentArchetype.set(entityId, componentType, component$1);
1426
+ }
1427
+ }
1428
+ function applyDontFragmentChanges(dontFragmentRelations, entityId, changeset, removedComponents) {
1429
+ let entityRelations = dontFragmentRelations.get(entityId);
1430
+ for (const componentType of changeset.removes) if (isDontFragmentRelation(componentType)) {
1431
+ if (entityRelations) {
1432
+ const removedValue = entityRelations.get(componentType);
1433
+ if (removedValue !== void 0 || entityRelations.has(componentType)) {
1434
+ removedComponents.set(componentType, removedValue);
1435
+ entityRelations.delete(componentType);
1436
+ }
1437
+ }
1438
+ }
1439
+ for (const [componentType, component$1] of changeset.adds) if (isDontFragmentRelation(componentType)) {
1440
+ if (!entityRelations) {
1441
+ entityRelations = /* @__PURE__ */ new Map();
1442
+ dontFragmentRelations.set(entityId, entityRelations);
1443
+ }
1444
+ entityRelations.set(componentType, component$1);
1445
+ }
1446
+ if (entityRelations && entityRelations.size === 0) dontFragmentRelations.delete(entityId);
1447
+ }
1448
+ function filterRegularComponentTypes(componentTypes) {
1449
+ const regularTypes = [];
1450
+ for (const componentType of componentTypes) {
1451
+ if (isDontFragmentWildcard(componentType)) {
1452
+ regularTypes.push(componentType);
1453
+ continue;
1454
+ }
1455
+ if (isDontFragmentRelation(componentType)) continue;
1456
+ regularTypes.push(componentType);
1457
+ }
1458
+ return regularTypes;
1459
+ }
1460
+ function areComponentTypesEqual(types1, types2) {
1461
+ if (types1.length !== types2.length) return false;
1462
+ const sorted1 = [...types1].sort((a, b) => a - b);
1463
+ const sorted2 = [...types2].sort((a, b) => a - b);
1464
+ return sorted1.every((v, i) => v === sorted2[i]);
1465
+ }
1466
+
1467
+ //#endregion
1468
+ //#region src/core/world-hooks.ts
1469
+ function triggerLifecycleHooks(ctx, entityId, addedComponents, removedComponents) {
1470
+ invokeHooksForComponents(ctx.hooks, entityId, addedComponents, "on_set");
1471
+ invokeHooksForComponents(ctx.hooks, entityId, removedComponents, "on_remove");
1472
+ triggerMultiComponentHooks(ctx, entityId, addedComponents, removedComponents);
1473
+ }
1474
+ function invokeHooksForComponents(hooks, entityId, components, hookType) {
1475
+ for (const [componentType, component$1] of components) {
1476
+ const directHooks = hooks.get(componentType);
1477
+ if (directHooks) for (const hook of directHooks) hook[hookType]?.(entityId, componentType, component$1);
1478
+ const componentId = getComponentIdFromRelationId(componentType);
1479
+ if (componentId !== void 0) {
1480
+ const wildcardHooks = hooks.get(relation(componentId, "*"));
1481
+ if (wildcardHooks) for (const hook of wildcardHooks) hook[hookType]?.(entityId, componentType, component$1);
1482
+ }
1483
+ }
1484
+ }
1485
+ function triggerMultiComponentHooks(ctx, entityId, addedComponents, removedComponents) {
1486
+ for (const { componentTypes, requiredComponents, hook } of ctx.multiHooks) {
1487
+ const anyRequiredAdded = requiredComponents.some((c) => addedComponents.has(c));
1488
+ const anyRequiredRemoved = requiredComponents.some((c) => removedComponents.has(c));
1489
+ if (anyRequiredAdded && hook.on_set && entityHasAllComponents(ctx, entityId, requiredComponents)) hook.on_set(entityId, componentTypes, collectMultiHookComponents(ctx, entityId, componentTypes));
1490
+ if (anyRequiredRemoved && hook.on_remove && entityHadAllComponentsBefore(ctx, entityId, requiredComponents, removedComponents)) hook.on_remove(entityId, componentTypes, collectMultiHookComponentsWithRemoved(ctx, entityId, componentTypes, removedComponents));
1491
+ }
1492
+ }
1493
+ function entityHasAllComponents(ctx, entityId, requiredComponents) {
1494
+ return requiredComponents.every((c) => ctx.has(entityId, c));
1495
+ }
1496
+ function entityHadAllComponentsBefore(ctx, entityId, requiredComponents, removedComponents) {
1497
+ return requiredComponents.every((c) => removedComponents.has(c) || ctx.has(entityId, c));
1498
+ }
1499
+ function collectMultiHookComponents(ctx, entityId, componentTypes) {
1500
+ return componentTypes.map((ct) => isOptionalEntityId(ct) ? ctx.getOptional(entityId, ct.optional) : ctx.get(entityId, ct));
1501
+ }
1502
+ function collectMultiHookComponentsWithRemoved(ctx, entityId, componentTypes, removedComponents) {
1503
+ return componentTypes.map((ct) => {
1504
+ if (isOptionalEntityId(ct)) {
1505
+ const optionalId = ct.optional;
1506
+ return removedComponents.has(optionalId) ? { value: removedComponents.get(optionalId) } : ctx.getOptional(entityId, optionalId);
1507
+ }
1508
+ const compId = ct;
1509
+ return removedComponents.has(compId) ? removedComponents.get(compId) : ctx.get(entityId, compId);
1510
+ });
1511
+ }
1512
+
1513
+ //#endregion
1514
+ //#region src/utils/multi-map.ts
1515
+ var MultiMap = class {
1516
+ map = /* @__PURE__ */ new Map();
1517
+ _valueCount = 0;
1518
+ get valueCount() {
1519
+ return this._valueCount;
1520
+ }
1521
+ get keyCount() {
1522
+ return this.map.size;
1523
+ }
1524
+ hasKey(key) {
1525
+ return this.map.has(key);
1526
+ }
1527
+ has(key, value) {
1528
+ const set = this.map.get(key);
1529
+ if (!set) return false;
1530
+ if (arguments.length === 1) return true;
1531
+ return set.has(value);
1532
+ }
1533
+ add(key, value) {
1534
+ let set = this.map.get(key);
1535
+ if (!set) {
1536
+ set = /* @__PURE__ */ new Set();
1537
+ this.map.set(key, set);
1538
+ }
1539
+ if (!set.has(value)) {
1540
+ set.add(value);
1541
+ this._valueCount++;
1542
+ }
1543
+ }
1544
+ remove(key, value) {
1545
+ const set = this.map.get(key);
1546
+ if (!set) return false;
1547
+ if (!set.has(value)) return false;
1548
+ set.delete(value);
1549
+ this._valueCount--;
1550
+ if (set.size === 0) this.map.delete(key);
1551
+ return true;
1552
+ }
1553
+ deleteKey(key) {
1554
+ const set = this.map.get(key);
1555
+ if (!set) return false;
1556
+ this._valueCount -= set.size;
1557
+ this.map.delete(key);
1558
+ return true;
1559
+ }
1560
+ get(key) {
1561
+ const set = this.map.get(key);
1562
+ return set ? new Set(set) : /* @__PURE__ */ new Set();
1563
+ }
1564
+ *keys() {
1565
+ yield* this.map.keys();
1566
+ }
1567
+ *values() {
1568
+ for (const set of this.map.values()) for (const v of set) yield v;
1569
+ }
1570
+ [Symbol.iterator]() {
1571
+ return this.entries();
1572
+ }
1573
+ *entries() {
1574
+ for (const [k, set] of this.map.entries()) for (const v of set) yield [k, v];
1575
+ }
1576
+ clear() {
1577
+ this.map.clear();
1578
+ this._valueCount = 0;
1579
+ }
1580
+ };
1581
+
1582
+ //#endregion
1583
+ //#region src/core/world-references.ts
1584
+ function trackEntityReference(entityReferences, sourceEntityId, componentType, targetEntityId) {
1585
+ if (!entityReferences.has(targetEntityId)) entityReferences.set(targetEntityId, new MultiMap());
1586
+ entityReferences.get(targetEntityId).add(sourceEntityId, componentType);
1434
1587
  }
1588
+ function untrackEntityReference(entityReferences, sourceEntityId, componentType, targetEntityId) {
1589
+ const references = entityReferences.get(targetEntityId);
1590
+ if (references) {
1591
+ references.remove(sourceEntityId, componentType);
1592
+ if (references.keyCount === 0) entityReferences.delete(targetEntityId);
1593
+ }
1594
+ }
1595
+ function getEntityReferences(entityReferences, targetEntityId) {
1596
+ return entityReferences.get(targetEntityId) ?? new MultiMap();
1597
+ }
1598
+
1599
+ //#endregion
1600
+ //#region src/core/world.ts
1435
1601
  /**
1436
1602
  * World class for ECS architecture
1437
1603
  * Manages entities and components
1438
1604
  */
1439
1605
  var World = class {
1440
- /** Manages allocation and deallocation of entity IDs */
1441
1606
  entityIdManager = new EntityIdManager();
1442
- /** Array of all archetypes in the world */
1443
1607
  archetypes = [];
1444
- /** Maps archetype signatures (component type signatures) to archetype instances */
1445
1608
  archetypeBySignature = /* @__PURE__ */ new Map();
1446
- /** Maps entity IDs to their current archetype */
1447
1609
  entityToArchetype = /* @__PURE__ */ new Map();
1448
- /** Maps component types to arrays of archetypes that contain them */
1449
1610
  archetypesByComponent = /* @__PURE__ */ new Map();
1450
- /** Tracks which entities reference each entity as a component type */
1451
1611
  entityReferences = /* @__PURE__ */ new Map();
1452
- /** Storage for dontFragment relations - maps entity ID to a map of relation type to component data */
1453
1612
  dontFragmentRelations = /* @__PURE__ */ new Map();
1454
- /** Array of all active queries for archetype change notifications */
1455
1613
  queries = [];
1456
- /** Cache for queries keyed by component types and filter signatures */
1457
1614
  queryCache = /* @__PURE__ */ new Map();
1458
- /** Buffers structural changes for deferred execution */
1459
1615
  commandBuffer = new CommandBuffer((entityId, commands) => this.executeEntityCommands(entityId, commands));
1460
- /** Stores lifecycle hooks for component and relation events */
1461
1616
  hooks = /* @__PURE__ */ new Map();
1462
- /**
1463
- * Create a new World.
1464
- * If an optional snapshot object is provided (previously produced by `world.serialize()`),
1465
- * the world will be restored from that snapshot. The snapshot may contain non-JSON values.
1466
- */
1617
+ multiHooks = /* @__PURE__ */ new Set();
1467
1618
  constructor(snapshot) {
1468
- if (snapshot && typeof snapshot === "object") {
1469
- if (snapshot.entityManager) this.entityIdManager.deserializeState(snapshot.entityManager);
1470
- if (Array.isArray(snapshot.entities)) for (const entry of snapshot.entities) {
1471
- const entityId = decodeSerializedId(entry.id);
1472
- const componentsArray = entry.components || [];
1473
- const componentMap = /* @__PURE__ */ new Map();
1474
- const componentTypes = [];
1475
- for (const componentEntry of componentsArray) {
1476
- const componentType = decodeSerializedId(componentEntry.type);
1477
- componentMap.set(componentType, componentEntry.value);
1478
- componentTypes.push(componentType);
1479
- }
1480
- const archetype = this.ensureArchetype(componentTypes);
1481
- archetype.addEntity(entityId, componentMap);
1482
- this.entityToArchetype.set(entityId, archetype);
1483
- for (const compType of componentTypes) {
1484
- const detailedType = getDetailedIdType(compType);
1485
- if (detailedType.type === "entity-relation") {
1486
- const targetEntityId = detailedType.targetId;
1487
- this.trackEntityReference(entityId, compType, targetEntityId);
1488
- } else if (detailedType.type === "entity") this.trackEntityReference(entityId, compType, compType);
1489
- }
1619
+ if (snapshot && typeof snapshot === "object") this.deserializeSnapshot(snapshot);
1620
+ }
1621
+ deserializeSnapshot(snapshot) {
1622
+ if (snapshot.entityManager) this.entityIdManager.deserializeState(snapshot.entityManager);
1623
+ if (Array.isArray(snapshot.entities)) for (const entry of snapshot.entities) {
1624
+ const entityId = decodeSerializedId(entry.id);
1625
+ const componentsArray = entry.components || [];
1626
+ const componentMap = /* @__PURE__ */ new Map();
1627
+ const componentTypes = [];
1628
+ for (const componentEntry of componentsArray) {
1629
+ const componentType = decodeSerializedId(componentEntry.type);
1630
+ componentMap.set(componentType, componentEntry.value);
1631
+ componentTypes.push(componentType);
1632
+ }
1633
+ const archetype = this.ensureArchetype(componentTypes);
1634
+ archetype.addEntity(entityId, componentMap);
1635
+ this.entityToArchetype.set(entityId, archetype);
1636
+ for (const compType of componentTypes) {
1637
+ const detailedType = getDetailedIdType(compType);
1638
+ if (detailedType.type === "entity-relation") trackEntityReference(this.entityReferences, entityId, compType, detailedType.targetId);
1639
+ else if (detailedType.type === "entity") trackEntityReference(this.entityReferences, entityId, compType, compType);
1490
1640
  }
1491
1641
  }
1492
1642
  }
1493
- /**
1494
- * Generate a signature string for component types array
1495
- * @returns A string signature for the component types
1496
- */
1497
1643
  createArchetypeSignature(componentTypes) {
1498
1644
  return componentTypes.join(",");
1499
1645
  }
1500
- /**
1501
- * Create a new entity
1502
- * @returns The ID of the newly created entity
1503
- */
1504
1646
  new() {
1505
1647
  const entityId = this.entityIdManager.allocate();
1506
1648
  let emptyArchetype = this.ensureArchetype([]);
@@ -1508,9 +1650,6 @@ var World = class {
1508
1650
  this.entityToArchetype.set(entityId, emptyArchetype);
1509
1651
  return entityId;
1510
1652
  }
1511
- /**
1512
- * Destroy an entity and remove all its components (immediate execution)
1513
- */
1514
1653
  destroyEntityImmediate(entityId) {
1515
1654
  const queue = [entityId];
1516
1655
  const visited = /* @__PURE__ */ new Set();
@@ -1520,7 +1659,7 @@ var World = class {
1520
1659
  visited.add(cur);
1521
1660
  const archetype = this.entityToArchetype.get(cur);
1522
1661
  if (!archetype) continue;
1523
- const componentReferences = Array.from(this.getEntityReferences(cur));
1662
+ const componentReferences = Array.from(getEntityReferences(this.entityReferences, cur));
1524
1663
  for (const [sourceEntityId, componentType] of componentReferences) {
1525
1664
  if (!this.entityToArchetype.get(sourceEntityId)) continue;
1526
1665
  if (isCascadeDeleteRelation(componentType)) {
@@ -1536,9 +1675,6 @@ var World = class {
1536
1675
  this.entityIdManager.deallocate(cur);
1537
1676
  }
1538
1677
  }
1539
- /**
1540
- * Check if an entity exists
1541
- */
1542
1678
  exists(entityId) {
1543
1679
  return this.entityToArchetype.has(entityId);
1544
1680
  }
@@ -1549,23 +1685,14 @@ var World = class {
1549
1685
  if (detailedType.type === "wildcard-relation") throw new Error(`Cannot directly add wildcard relation components: ${componentType}`);
1550
1686
  this.commandBuffer.set(entityId, componentType, component$1);
1551
1687
  }
1552
- /**
1553
- * Remove a component from an entity (deferred)
1554
- */
1555
1688
  remove(entityId, componentType) {
1556
1689
  if (!this.exists(entityId)) throw new Error(`Entity ${entityId} does not exist`);
1557
1690
  if (getDetailedIdType(componentType).type === "invalid") throw new Error(`Invalid component type: ${componentType}`);
1558
1691
  this.commandBuffer.remove(entityId, componentType);
1559
1692
  }
1560
- /**
1561
- * Destroy an entity and remove all its components (deferred)
1562
- */
1563
1693
  delete(entityId) {
1564
1694
  this.commandBuffer.delete(entityId);
1565
1695
  }
1566
- /**
1567
- * Check if an entity has a specific component
1568
- */
1569
1696
  has(entityId, componentType) {
1570
1697
  const archetype = this.entityToArchetype.get(entityId);
1571
1698
  if (!archetype) return false;
@@ -1583,54 +1710,65 @@ var World = class {
1583
1710
  }
1584
1711
  return archetype.get(entityId, componentType);
1585
1712
  }
1586
- /**
1587
- * Get optional component data for a specific entity and component type
1588
- * @param entityId The entity
1589
- * @param componentType The component type
1590
- * @returns { value: T } if component exists, undefined otherwise
1591
- */
1592
1713
  getOptional(entityId, componentType) {
1593
1714
  const archetype = this.entityToArchetype.get(entityId);
1594
1715
  if (!archetype) throw new Error(`Entity ${entityId} does not exist`);
1595
- if (isWildcardRelationId(componentType)) return;
1716
+ if (isWildcardRelationId(componentType)) return void 0;
1596
1717
  return archetype.getOptional(entityId, componentType);
1597
1718
  }
1598
- /**
1599
- * Register a lifecycle hook for component or wildcard relation events
1600
- */
1601
- hook(componentType, hook) {
1602
- if (!this.hooks.has(componentType)) this.hooks.set(componentType, /* @__PURE__ */ new Set());
1603
- this.hooks.get(componentType).add(hook);
1604
- if (hook.on_init !== void 0) this.archetypesByComponent.get(componentType)?.forEach((archetype) => {
1605
- const entities = archetype.getEntityToIndexMap();
1606
- const componentData = archetype.getComponentData(componentType);
1607
- for (const [entity, index] of entities) {
1608
- const data = componentData[index];
1609
- const value = data === MISSING_COMPONENT ? void 0 : data;
1610
- hook.on_init?.(entity, componentType, value);
1719
+ hook(componentTypesOrSingle, hook) {
1720
+ if (Array.isArray(componentTypesOrSingle)) {
1721
+ const componentTypes = componentTypesOrSingle;
1722
+ const requiredComponents = [];
1723
+ for (const ct of componentTypes) if (!isOptionalEntityId(ct)) requiredComponents.push(ct);
1724
+ const entry = {
1725
+ componentTypes,
1726
+ requiredComponents,
1727
+ hook
1728
+ };
1729
+ this.multiHooks.add(entry);
1730
+ const multiHook = hook;
1731
+ if (multiHook.on_init !== void 0) {
1732
+ const matchingArchetypes = this.getMatchingArchetypes(requiredComponents);
1733
+ for (const archetype of matchingArchetypes) for (const entityId of archetype.getEntities()) {
1734
+ const components = collectMultiHookComponents(this.createHooksContext(), entityId, componentTypes);
1735
+ multiHook.on_init(entityId, componentTypes, components);
1736
+ }
1611
1737
  }
1612
- });
1738
+ } else {
1739
+ const componentType = componentTypesOrSingle;
1740
+ if (!this.hooks.has(componentType)) this.hooks.set(componentType, /* @__PURE__ */ new Set());
1741
+ this.hooks.get(componentType).add(hook);
1742
+ const singleHook = hook;
1743
+ if (singleHook.on_init !== void 0) this.archetypesByComponent.get(componentType)?.forEach((archetype) => {
1744
+ const entities = archetype.getEntityToIndexMap();
1745
+ const componentData = archetype.getComponentData(componentType);
1746
+ for (const [entity, index] of entities) {
1747
+ const data = componentData[index];
1748
+ const value = data === MISSING_COMPONENT ? void 0 : data;
1749
+ singleHook.on_init?.(entity, componentType, value);
1750
+ }
1751
+ });
1752
+ }
1613
1753
  }
1614
- /**
1615
- * Unregister a lifecycle hook for component or wildcard relation events
1616
- */
1617
- unhook(componentType, hook) {
1618
- const hooks = this.hooks.get(componentType);
1619
- if (hooks) {
1620
- hooks.delete(hook);
1621
- if (hooks.size === 0) this.hooks.delete(componentType);
1754
+ unhook(componentTypesOrSingle, hook) {
1755
+ if (Array.isArray(componentTypesOrSingle)) {
1756
+ for (const entry of this.multiHooks) if (entry.hook === hook) {
1757
+ this.multiHooks.delete(entry);
1758
+ break;
1759
+ }
1760
+ } else {
1761
+ const componentType = componentTypesOrSingle;
1762
+ const hooks = this.hooks.get(componentType);
1763
+ if (hooks) {
1764
+ hooks.delete(hook);
1765
+ if (hooks.size === 0) this.hooks.delete(componentType);
1766
+ }
1622
1767
  }
1623
1768
  }
1624
- /**
1625
- * Execute all deferred commands immediately
1626
- */
1627
1769
  sync() {
1628
1770
  this.commandBuffer.execute();
1629
1771
  }
1630
- /**
1631
- * Create a cached query for efficient entity lookups
1632
- * @returns A Query object for the specified component types and filter
1633
- */
1634
1772
  createQuery(componentTypes, filter = {}) {
1635
1773
  const sortedTypes = [...componentTypes].sort((a, b) => a - b);
1636
1774
  const filterKey = serializeQueryFilter(filter);
@@ -1647,19 +1785,9 @@ var World = class {
1647
1785
  });
1648
1786
  return query;
1649
1787
  }
1650
- /**
1651
- * Create an EntityBuilder for convenient entity creation.
1652
- * @returns EntityBuilder
1653
- */
1654
1788
  spawn() {
1655
1789
  return new EntityBuilder(this);
1656
1790
  }
1657
- /**
1658
- * Spawn multiple entities using an EntityBuilder configuration callback
1659
- * @param count number of entities
1660
- * @param configure builder configuration callback
1661
- * @returns Created entity IDs
1662
- */
1663
1791
  spawnMany(count, configure) {
1664
1792
  const entities = [];
1665
1793
  for (let i = 0; i < count; i++) {
@@ -1668,23 +1796,13 @@ var World = class {
1668
1796
  }
1669
1797
  return entities;
1670
1798
  }
1671
- /**
1672
- * @internal Register a query for archetype update notifications
1673
- */
1674
1799
  _registerQuery(query) {
1675
1800
  this.queries.push(query);
1676
1801
  }
1677
- /**
1678
- * @internal Unregister a query
1679
- */
1680
1802
  _unregisterQuery(query) {
1681
1803
  const index = this.queries.indexOf(query);
1682
1804
  if (index !== -1) this.queries.splice(index, 1);
1683
1805
  }
1684
- /**
1685
- * Release a query reference obtained from createQuery.
1686
- * Decrements the refCount and fully disposes the query when it reaches zero.
1687
- */
1688
1806
  releaseQuery(query) {
1689
1807
  for (const [k, v] of this.queryCache.entries()) if (v.query === query) {
1690
1808
  v.refCount--;
@@ -1696,9 +1814,6 @@ var World = class {
1696
1814
  return;
1697
1815
  }
1698
1816
  }
1699
- /**
1700
- * @internal Get archetypes that match specific component types (for internal use by queries)
1701
- */
1702
1817
  getMatchingArchetypes(componentTypes) {
1703
1818
  if (componentTypes.length === 0) return [...this.archetypes];
1704
1819
  const regularComponents = [];
@@ -1710,42 +1825,24 @@ var World = class {
1710
1825
  relationId: componentType
1711
1826
  });
1712
1827
  } else regularComponents.push(componentType);
1713
- let matchingArchetypes = [];
1714
- if (regularComponents.length > 0) {
1715
- const sortedRegularTypes = [...regularComponents].sort((a, b) => a - b);
1716
- if (sortedRegularTypes.length === 1) {
1717
- const componentType = sortedRegularTypes[0];
1718
- matchingArchetypes = this.archetypesByComponent.get(componentType) || [];
1719
- } else {
1720
- const archetypeLists = sortedRegularTypes.map((type) => this.archetypesByComponent.get(type) || []);
1721
- const firstList = archetypeLists[0] || [];
1722
- const intersection = /* @__PURE__ */ new Set();
1723
- for (const archetype of firstList) {
1724
- let hasAllComponents = true;
1725
- for (let listIndex = 1; listIndex < archetypeLists.length; listIndex++) if (!archetypeLists[listIndex].includes(archetype)) {
1726
- hasAllComponents = false;
1727
- break;
1728
- }
1729
- if (hasAllComponents) intersection.add(archetype);
1730
- }
1731
- matchingArchetypes = Array.from(intersection);
1732
- }
1733
- } else matchingArchetypes = [...this.archetypes];
1734
- for (const wildcard of wildcardRelations) if (isDontFragmentComponent(wildcard.componentId)) {
1735
- const archetypesWithMarker = this.archetypesByComponent.get(wildcard.relationId) || [];
1736
- if (matchingArchetypes.length === 0) matchingArchetypes = archetypesWithMarker;
1737
- else matchingArchetypes = matchingArchetypes.filter((archetype) => archetypesWithMarker.includes(archetype));
1738
- } else matchingArchetypes = matchingArchetypes.filter((archetype) => archetype.hasRelationWithComponentId(wildcard.componentId));
1828
+ let matchingArchetypes = this.getArchetypesWithComponents(regularComponents);
1829
+ for (const { componentId, relationId } of wildcardRelations) {
1830
+ const archetypesWithMarker = this.archetypesByComponent.get(relationId) || [];
1831
+ matchingArchetypes = matchingArchetypes.length === 0 ? archetypesWithMarker : matchingArchetypes.filter((a) => archetypesWithMarker.includes(a) || a.hasRelationWithComponentId(componentId));
1832
+ }
1739
1833
  return matchingArchetypes;
1740
1834
  }
1835
+ getArchetypesWithComponents(componentTypes) {
1836
+ if (componentTypes.length === 0) return [...this.archetypes];
1837
+ if (componentTypes.length === 1) return this.archetypesByComponent.get(componentTypes[0]) || [];
1838
+ const archetypeLists = componentTypes.map((type) => this.archetypesByComponent.get(type) || []);
1839
+ return archetypeLists[0].filter((archetype) => archetypeLists.slice(1).every((list) => list.includes(archetype)));
1840
+ }
1741
1841
  query(componentTypes, includeComponents) {
1742
1842
  const matchingArchetypes = this.getMatchingArchetypes(componentTypes);
1743
1843
  if (includeComponents) {
1744
1844
  const result = [];
1745
- for (const archetype of matchingArchetypes) {
1746
- const entitiesWithData = archetype.getEntitiesWithComponents(componentTypes);
1747
- result.push(...entitiesWithData);
1748
- }
1845
+ for (const archetype of matchingArchetypes) result.push(...archetype.getEntitiesWithComponents(componentTypes));
1749
1846
  return result;
1750
1847
  } else {
1751
1848
  const result = [];
@@ -1753,10 +1850,6 @@ var World = class {
1753
1850
  return result;
1754
1851
  }
1755
1852
  }
1756
- /**
1757
- * @internal Execute commands for a single entity (for internal use by CommandBuffer)
1758
- * @returns ComponentChangeset describing the changes made
1759
- */
1760
1853
  executeEntityCommands(entityId, commands) {
1761
1854
  const changeset = new ComponentChangeset();
1762
1855
  if (commands.some((cmd) => cmd.type === "destroy")) {
@@ -1765,327 +1858,79 @@ var World = class {
1765
1858
  }
1766
1859
  const currentArchetype = this.entityToArchetype.get(entityId);
1767
1860
  if (!currentArchetype) return changeset;
1768
- this.processCommands(entityId, currentArchetype, commands, changeset);
1769
- const removedComponents = this.applyChangeset(entityId, currentArchetype, changeset);
1861
+ processCommands(entityId, currentArchetype, commands, changeset, (eid, arch, compId) => {
1862
+ if (isExclusiveComponent(compId)) removeMatchingRelations(eid, arch, compId, changeset);
1863
+ });
1864
+ const removedComponents = applyChangeset({
1865
+ dontFragmentRelations: this.dontFragmentRelations,
1866
+ ensureArchetype: (ct) => this.ensureArchetype(ct)
1867
+ }, entityId, currentArchetype, changeset, this.entityToArchetype);
1770
1868
  this.updateEntityReferences(entityId, changeset);
1771
- this.triggerLifecycleHooks(entityId, changeset.adds, removedComponents);
1869
+ triggerLifecycleHooks(this.createHooksContext(), entityId, changeset.adds, removedComponents);
1772
1870
  return changeset;
1773
1871
  }
1774
- /**
1775
- * Process commands and populate the changeset
1776
- */
1777
- processCommands(entityId, currentArchetype, commands, changeset) {
1778
- for (const command of commands) if (command.type === "set" && command.componentType) this.processSetCommand(entityId, currentArchetype, command.componentType, command.component, changeset);
1779
- else if (command.type === "delete" && command.componentType) this.processDeleteCommand(entityId, currentArchetype, command.componentType, changeset);
1780
- }
1781
- /**
1782
- * Process a set command, handling exclusive relations
1783
- */
1784
- processSetCommand(entityId, currentArchetype, componentType, component$1, changeset) {
1785
- const componentId = getComponentIdFromRelationId(componentType);
1786
- if (componentId !== void 0) {
1787
- if (componentId !== void 0 && isExclusiveComponent(componentId)) this.removeExclusiveRelations(entityId, currentArchetype, componentId, changeset);
1788
- if (componentId !== void 0 && isDontFragmentComponent(componentId)) {
1789
- const wildcardMarker = relation(componentId, "*");
1790
- if (!currentArchetype.componentTypes.includes(wildcardMarker)) changeset.set(wildcardMarker, void 0);
1791
- }
1792
- }
1793
- changeset.set(componentType, component$1);
1794
- }
1795
- /**
1796
- * Remove all relations with the same base component (for exclusive relations)
1797
- */
1798
- removeExclusiveRelations(entityId, currentArchetype, baseComponentId, changeset) {
1799
- for (const componentType of currentArchetype.componentTypes) if (this.isRelationWithComponent(componentType, baseComponentId)) changeset.delete(componentType);
1800
- const entityData = currentArchetype.getEntity(entityId);
1801
- if (entityData) for (const [componentType] of entityData) {
1802
- if (currentArchetype.componentTypes.includes(componentType)) continue;
1803
- if (this.isRelationWithComponent(componentType, baseComponentId)) changeset.delete(componentType);
1804
- }
1805
- }
1806
- isRelationWithComponent(componentType, baseComponentId) {
1807
- return getComponentIdFromRelationId(componentType) === baseComponentId;
1808
- }
1809
- /**
1810
- * Process a delete command, handling wildcard relations
1811
- */
1812
- processDeleteCommand(entityId, currentArchetype, componentType, changeset) {
1813
- const componentId = getComponentIdFromRelationId(componentType);
1814
- if (isWildcardRelationId(componentType) && componentId !== void 0) this.removeWildcardRelations(entityId, currentArchetype, componentId, changeset);
1815
- else {
1816
- changeset.delete(componentType);
1817
- if (componentId !== void 0 && isDontFragmentComponent(componentId)) {
1818
- const wildcardMarker = relation(componentId, "*");
1819
- const entityData = currentArchetype.getEntity(entityId);
1820
- let hasOtherRelations = false;
1821
- if (entityData) for (const [otherComponentType] of entityData) {
1822
- if (otherComponentType === componentType) continue;
1823
- if (otherComponentType === wildcardMarker) continue;
1824
- if (changeset.removes.has(otherComponentType)) continue;
1825
- if (getComponentIdFromRelationId(otherComponentType) === componentId) {
1826
- hasOtherRelations = true;
1827
- break;
1828
- }
1829
- }
1830
- if (!hasOtherRelations) changeset.delete(wildcardMarker);
1831
- }
1832
- }
1833
- }
1834
- /**
1835
- * Remove all relations matching a wildcard component ID
1836
- */
1837
- removeWildcardRelations(entityId, currentArchetype, baseComponentId, changeset) {
1838
- for (const componentType of currentArchetype.componentTypes) if (this.isRelationWithComponent(componentType, baseComponentId)) changeset.delete(componentType);
1839
- const entityData = currentArchetype.getEntity(entityId);
1840
- if (entityData) for (const [componentType] of entityData) {
1841
- if (currentArchetype.componentTypes.includes(componentType)) continue;
1842
- if (this.isRelationWithComponent(componentType, baseComponentId)) changeset.delete(componentType);
1843
- }
1844
- if (isDontFragmentComponent(baseComponentId)) {
1845
- const wildcardMarker = relation(baseComponentId, "*");
1846
- changeset.delete(wildcardMarker);
1847
- }
1872
+ createHooksContext() {
1873
+ return {
1874
+ hooks: this.hooks,
1875
+ multiHooks: this.multiHooks,
1876
+ has: (eid, ct) => this.has(eid, ct),
1877
+ get: (eid, ct) => this.get(eid, ct),
1878
+ getOptional: (eid, ct) => this.getOptional(eid, ct)
1879
+ };
1848
1880
  }
1849
- /**
1850
- * Remove a single component from an entity immediately, handling dontFragment relations correctly.
1851
- * Used by destroyEntityImmediate for non-cascade relation cleanup.
1852
- */
1853
1881
  removeComponentImmediate(entityId, componentType, targetEntityId) {
1854
1882
  const sourceArchetype = this.entityToArchetype.get(entityId);
1855
1883
  if (!sourceArchetype) return;
1856
1884
  const changeset = new ComponentChangeset();
1857
- const componentId = getComponentIdFromRelationId(componentType);
1858
1885
  changeset.delete(componentType);
1859
- if (componentId !== void 0 && isDontFragmentComponent(componentId)) {
1860
- const wildcardMarker = relation(componentId, "*");
1861
- const entityData = sourceArchetype.getEntity(entityId);
1862
- let hasOtherRelations = false;
1863
- if (entityData) for (const [otherComponentType] of entityData) {
1864
- if (otherComponentType === componentType) continue;
1865
- if (otherComponentType === wildcardMarker) continue;
1866
- if (getComponentIdFromRelationId(otherComponentType) === componentId) {
1867
- hasOtherRelations = true;
1868
- break;
1869
- }
1870
- }
1871
- if (!hasOtherRelations) changeset.delete(wildcardMarker);
1872
- }
1886
+ maybeRemoveWildcardMarker(entityId, sourceArchetype, componentType, getComponentIdFromRelationId(componentType), changeset);
1873
1887
  const removedComponent = sourceArchetype.get(entityId, componentType);
1874
- this.applyChangeset(entityId, sourceArchetype, changeset);
1875
- this.untrackEntityReference(entityId, componentType, targetEntityId);
1876
- this.triggerLifecycleHooks(entityId, /* @__PURE__ */ new Map(), new Map([[componentType, removedComponent]]));
1877
- }
1878
- /**
1879
- * Apply changeset to entity, moving to new archetype if needed
1880
- * @returns Map of removed components with their data
1881
- */
1882
- applyChangeset(entityId, currentArchetype, changeset) {
1883
- const currentEntityData = currentArchetype.getEntity(entityId);
1884
- const allCurrentComponentTypes = currentEntityData ? Array.from(currentEntityData.keys()) : currentArchetype.componentTypes;
1885
- const finalComponentTypes = changeset.getFinalComponentTypes(allCurrentComponentTypes);
1886
- const removedComponents = /* @__PURE__ */ new Map();
1887
- if (finalComponentTypes) {
1888
- const currentRegularTypes = this.filterRegularComponentTypes(allCurrentComponentTypes);
1889
- const finalRegularTypes = this.filterRegularComponentTypes(finalComponentTypes);
1890
- if (!this.areComponentTypesEqual(currentRegularTypes, finalRegularTypes)) this.moveEntityToNewArchetype(entityId, currentArchetype, finalComponentTypes, changeset, removedComponents);
1891
- else this.updateEntityInSameArchetype(entityId, currentArchetype, changeset, removedComponents);
1892
- } else this.updateEntityInSameArchetype(entityId, currentArchetype, changeset, removedComponents);
1893
- return removedComponents;
1894
- }
1895
- /**
1896
- * Move entity to a new archetype with updated components
1897
- */
1898
- moveEntityToNewArchetype(entityId, currentArchetype, finalComponentTypes, changeset, removedComponents) {
1899
- const newArchetype = this.ensureArchetype(finalComponentTypes);
1900
- const currentComponents = currentArchetype.removeEntity(entityId);
1901
- for (const componentType of changeset.removes) removedComponents.set(componentType, currentComponents.get(componentType));
1902
- newArchetype.addEntity(entityId, changeset.applyTo(currentComponents));
1903
- this.entityToArchetype.set(entityId, newArchetype);
1904
- }
1905
- /**
1906
- * Update entity in same archetype (no archetype change needed)
1907
- */
1908
- updateEntityInSameArchetype(entityId, currentArchetype, changeset, removedComponents) {
1909
- this.applyDontFragmentChanges(entityId, changeset, removedComponents);
1910
- for (const [componentType, component$1] of changeset.adds) {
1911
- if (isDontFragmentRelation(componentType)) continue;
1912
- currentArchetype.set(entityId, componentType, component$1);
1913
- }
1914
- }
1915
- /**
1916
- * Apply dontFragment relation changes directly to World's storage
1917
- * This is much more efficient than the removeEntity + addEntity approach
1918
- */
1919
- applyDontFragmentChanges(entityId, changeset, removedComponents) {
1920
- let entityRelations = this.dontFragmentRelations.get(entityId);
1921
- for (const componentType of changeset.removes) if (isDontFragmentRelation(componentType)) {
1922
- if (entityRelations) {
1923
- const removedValue = entityRelations.get(componentType);
1924
- if (removedValue !== void 0 || entityRelations.has(componentType)) {
1925
- removedComponents.set(componentType, removedValue);
1926
- entityRelations.delete(componentType);
1927
- }
1928
- }
1929
- }
1930
- for (const [componentType, component$1] of changeset.adds) if (isDontFragmentRelation(componentType)) {
1931
- if (!entityRelations) {
1932
- entityRelations = /* @__PURE__ */ new Map();
1933
- this.dontFragmentRelations.set(entityId, entityRelations);
1934
- }
1935
- entityRelations.set(componentType, component$1);
1936
- }
1937
- if (entityRelations && entityRelations.size === 0) this.dontFragmentRelations.delete(entityId);
1888
+ applyChangeset({
1889
+ dontFragmentRelations: this.dontFragmentRelations,
1890
+ ensureArchetype: (ct) => this.ensureArchetype(ct)
1891
+ }, entityId, sourceArchetype, changeset, this.entityToArchetype);
1892
+ untrackEntityReference(this.entityReferences, entityId, componentType, targetEntityId);
1893
+ triggerLifecycleHooks(this.createHooksContext(), entityId, /* @__PURE__ */ new Map(), new Map([[componentType, removedComponent]]));
1938
1894
  }
1939
- /**
1940
- * Update entity reference tracking based on changeset
1941
- */
1942
1895
  updateEntityReferences(entityId, changeset) {
1943
1896
  for (const componentType of changeset.removes) if (isEntityRelation(componentType)) {
1944
1897
  const targetId = getTargetIdFromRelationId(componentType);
1945
- this.untrackEntityReference(entityId, componentType, targetId);
1946
- } else if (componentType >= 1024) this.untrackEntityReference(entityId, componentType, componentType);
1898
+ untrackEntityReference(this.entityReferences, entityId, componentType, targetId);
1899
+ } else if (componentType >= 1024) untrackEntityReference(this.entityReferences, entityId, componentType, componentType);
1947
1900
  for (const [componentType] of changeset.adds) if (isEntityRelation(componentType)) {
1948
1901
  const targetId = getTargetIdFromRelationId(componentType);
1949
- this.trackEntityReference(entityId, componentType, targetId);
1950
- } else if (componentType >= 1024) this.trackEntityReference(entityId, componentType, componentType);
1902
+ trackEntityReference(this.entityReferences, entityId, componentType, targetId);
1903
+ } else if (componentType >= 1024) trackEntityReference(this.entityReferences, entityId, componentType, componentType);
1951
1904
  }
1952
- /**
1953
- * Get or create an archetype for the given component types
1954
- * Filters out dontFragment relations from the archetype signature
1955
- * @returns The archetype for the given component types (excluding dontFragment relations)
1956
- */
1957
1905
  ensureArchetype(componentTypes) {
1958
- const sortedTypes = this.filterRegularComponentTypes(componentTypes).sort((a, b) => a - b);
1906
+ const sortedTypes = filterRegularComponentTypes(componentTypes).sort((a, b) => a - b);
1959
1907
  const hashKey = this.createArchetypeSignature(sortedTypes);
1960
1908
  return getOrCreateWithSideEffect(this.archetypeBySignature, hashKey, () => this.createNewArchetype(sortedTypes));
1961
1909
  }
1962
- /**
1963
- * Compare two arrays of component types for equality (order-independent)
1964
- */
1965
- areComponentTypesEqual(types1, types2) {
1966
- if (types1.length !== types2.length) return false;
1967
- const sorted1 = [...types1].sort((a, b) => a - b);
1968
- const sorted2 = [...types2].sort((a, b) => a - b);
1969
- return sorted1.every((v, i) => v === sorted2[i]);
1970
- }
1971
- /**
1972
- * Filter out dontFragment relations from component types, but keep wildcard markers
1973
- */
1974
- filterRegularComponentTypes(componentTypes) {
1975
- const regularTypes = [];
1976
- for (const componentType of componentTypes) {
1977
- if (isDontFragmentWildcard(componentType)) {
1978
- regularTypes.push(componentType);
1979
- continue;
1980
- }
1981
- if (isDontFragmentRelation(componentType)) continue;
1982
- regularTypes.push(componentType);
1983
- }
1984
- return regularTypes;
1985
- }
1986
- /**
1987
- * Create a new archetype and register it with all tracking structures
1988
- */
1989
1910
  createNewArchetype(componentTypes) {
1990
1911
  const newArchetype = new Archetype(componentTypes, this.dontFragmentRelations);
1991
1912
  this.archetypes.push(newArchetype);
1992
- this.registerArchetypeInComponentIndex(newArchetype, componentTypes);
1993
- this.notifyQueriesOfNewArchetype(newArchetype);
1994
- return newArchetype;
1995
- }
1996
- /**
1997
- * Register archetype in the component-to-archetype index
1998
- */
1999
- registerArchetypeInComponentIndex(archetype, componentTypes) {
2000
1913
  for (const componentType of componentTypes) {
2001
1914
  const archetypes = this.archetypesByComponent.get(componentType) || [];
2002
- archetypes.push(archetype);
1915
+ archetypes.push(newArchetype);
2003
1916
  this.archetypesByComponent.set(componentType, archetypes);
2004
1917
  }
1918
+ for (const query of this.queries) query.checkNewArchetype(newArchetype);
1919
+ return newArchetype;
2005
1920
  }
2006
- /**
2007
- * Notify all queries to check the new archetype
2008
- */
2009
- notifyQueriesOfNewArchetype(archetype) {
2010
- for (const query of this.queries) query.checkNewArchetype(archetype);
2011
- }
2012
- /**
2013
- * Add a component reference to the reverse index when an entity is used as a component type
2014
- * @param sourceEntityId The entity that has the component
2015
- * @param componentType The component type (which may be an entity ID used as component type)
2016
- * @param targetEntityId The entity being used as component type
2017
- */
2018
- trackEntityReference(sourceEntityId, componentType, targetEntityId) {
2019
- if (!this.entityReferences.has(targetEntityId)) this.entityReferences.set(targetEntityId, new MultiMap());
2020
- this.entityReferences.get(targetEntityId).add(sourceEntityId, componentType);
2021
- }
2022
- /**
2023
- * Remove a component reference from the reverse index
2024
- * @param sourceEntityId The entity that has the component
2025
- * @param componentType The component type
2026
- * @param targetEntityId The entity being used as component type
2027
- */
2028
- untrackEntityReference(sourceEntityId, componentType, targetEntityId) {
2029
- const references = this.entityReferences.get(targetEntityId);
2030
- if (references) {
2031
- references.remove(sourceEntityId, componentType);
2032
- if (references.keyCount === 0) this.entityReferences.delete(targetEntityId);
2033
- }
2034
- }
2035
- /**
2036
- * Get all component references where a target entity is used as a component type
2037
- * @param targetEntityId The target entity
2038
- * @returns A MultiMap of sourceEntityId to componentTypes that reference the target entity
2039
- */
2040
- getEntityReferences(targetEntityId) {
2041
- return this.entityReferences.get(targetEntityId) ?? new MultiMap();
2042
- }
2043
- /**
2044
- * Check if an archetype's signature references a specific entity
2045
- * (via entity-relation targeting the entity, or using entity as component type)
2046
- */
2047
1921
  archetypeReferencesEntity(archetype, entityId) {
2048
- for (const componentType of archetype.componentTypes) {
2049
- if (componentType === entityId) return true;
2050
- if (isEntityRelation(componentType)) {
2051
- if (getTargetIdFromRelationId(componentType) === entityId) return true;
2052
- }
2053
- }
2054
- return false;
1922
+ return archetype.componentTypes.some((ct) => ct === entityId || isEntityRelation(ct) && getTargetIdFromRelationId(ct) === entityId);
2055
1923
  }
2056
- /**
2057
- * Cleanup empty archetypes that reference a specific deleted entity
2058
- * Only removes archetypes whose component types reference the entity
2059
- */
2060
1924
  cleanupArchetypesReferencingEntity(entityId) {
2061
1925
  for (let i = this.archetypes.length - 1; i >= 0; i--) {
2062
1926
  const archetype = this.archetypes[i];
2063
- if (archetype.getEntities().length === 0 && this.archetypeReferencesEntity(archetype, entityId)) {
2064
- this.removeArchetypeFromList(archetype);
2065
- this.removeArchetypeFromSignatureMap(archetype);
2066
- this.removeArchetypeFromComponentIndex(archetype);
2067
- this.removeArchetypeFromQueries(archetype);
2068
- }
1927
+ if (archetype.getEntities().length === 0 && this.archetypeReferencesEntity(archetype, entityId)) this.removeArchetype(archetype);
2069
1928
  }
2070
1929
  }
2071
- /**
2072
- * Remove archetype from the main archetypes list
2073
- */
2074
- removeArchetypeFromList(archetype) {
1930
+ removeArchetype(archetype) {
2075
1931
  const index = this.archetypes.indexOf(archetype);
2076
1932
  if (index !== -1) this.archetypes.splice(index, 1);
2077
- }
2078
- /**
2079
- * Remove archetype from the signature-to-archetype map
2080
- */
2081
- removeArchetypeFromSignatureMap(archetype) {
2082
- const hashKey = this.createArchetypeSignature(archetype.componentTypes);
2083
- this.archetypeBySignature.delete(hashKey);
2084
- }
2085
- /**
2086
- * Remove archetype from the component-to-archetypes index
2087
- */
2088
- removeArchetypeFromComponentIndex(archetype) {
1933
+ this.archetypeBySignature.delete(this.createArchetypeSignature(archetype.componentTypes));
2089
1934
  for (const componentType of archetype.componentTypes) {
2090
1935
  const archetypes = this.archetypesByComponent.get(componentType);
2091
1936
  if (archetypes) {
@@ -2096,43 +1941,8 @@ var World = class {
2096
1941
  }
2097
1942
  }
2098
1943
  }
2099
- }
2100
- /**
2101
- * Remove archetype from all queries
2102
- */
2103
- removeArchetypeFromQueries(archetype) {
2104
1944
  for (const query of this.queries) query.removeArchetype(archetype);
2105
1945
  }
2106
- /**
2107
- * Execute component lifecycle hooks for added and removed components
2108
- */
2109
- triggerLifecycleHooks(entityId, addedComponents, removedComponents) {
2110
- for (const [componentType, component$1] of addedComponents) {
2111
- const directHooks = this.hooks.get(componentType);
2112
- if (directHooks) for (const lifecycleHook of directHooks) lifecycleHook.on_set?.(entityId, componentType, component$1);
2113
- const componentId = getComponentIdFromRelationId(componentType);
2114
- if (componentId !== void 0) {
2115
- const wildcardRelationId = relation(componentId, "*");
2116
- const wildcardHooks = this.hooks.get(wildcardRelationId);
2117
- if (wildcardHooks) for (const lifecycleHook of wildcardHooks) lifecycleHook.on_set?.(entityId, componentType, component$1);
2118
- }
2119
- }
2120
- for (const [componentType, component$1] of removedComponents) {
2121
- const directHooks = this.hooks.get(componentType);
2122
- if (directHooks) for (const lifecycleHook of directHooks) lifecycleHook.on_remove?.(entityId, componentType, component$1);
2123
- const componentId = getComponentIdFromRelationId(componentType);
2124
- if (componentId !== void 0) {
2125
- const wildcardRelationId = relation(componentId, "*");
2126
- const wildcardHooks = this.hooks.get(wildcardRelationId);
2127
- if (wildcardHooks) for (const hook of wildcardHooks) hook.on_remove?.(entityId, componentType, component$1);
2128
- }
2129
- }
2130
- }
2131
- /**
2132
- * Convert the world into a plain snapshot object.
2133
- * This returns an in-memory structure and does not perform JSON stringification.
2134
- * Component values are stored as-is (they may be non-JSON-serializable).
2135
- */
2136
1946
  serialize() {
2137
1947
  const entities = [];
2138
1948
  for (const archetype of this.archetypes) {
@@ -2152,63 +1962,7 @@ var World = class {
2152
1962
  };
2153
1963
  }
2154
1964
  };
2155
- var EntityBuilder = class {
2156
- world;
2157
- components = [];
2158
- constructor(world) {
2159
- this.world = world;
2160
- }
2161
- with(componentId, value) {
2162
- this.components.push({
2163
- type: "component",
2164
- id: componentId,
2165
- value
2166
- });
2167
- return this;
2168
- }
2169
- withTag(componentId) {
2170
- this.components.push({
2171
- type: "component",
2172
- id: componentId,
2173
- value: void 0
2174
- });
2175
- return this;
2176
- }
2177
- withRelation(componentId, targetEntity, value) {
2178
- this.components.push({
2179
- type: "relation",
2180
- componentId,
2181
- targetId: targetEntity,
2182
- value
2183
- });
2184
- return this;
2185
- }
2186
- withRelationTag(componentId, targetEntity) {
2187
- this.components.push({
2188
- type: "relation",
2189
- componentId,
2190
- targetId: targetEntity,
2191
- value: void 0
2192
- });
2193
- return this;
2194
- }
2195
- /**
2196
- * Create an entity and enqueue components to be applied. This method
2197
- * does NOT call `world.sync()` automatically; callers must invoke
2198
- * `world.sync()` to apply deferred commands.
2199
- * (Previously auto-synced; now a breaking change — buildDeferred() removed.)
2200
- */
2201
- build() {
2202
- const entity = this.world.new();
2203
- for (const def of this.components) if (def.type === "component") this.world.set(entity, def.id, def.value);
2204
- else {
2205
- const relationId = relation(def.componentId, def.targetId);
2206
- this.world.set(entity, relationId, def.value);
2207
- }
2208
- return entity;
2209
- }
2210
- };
2211
1965
 
2212
1966
  //#endregion
2213
- export { decodeRelationId as a, isComponentId as c, isWildcardRelationId as d, relation as f, component as i, isEntityId as l, World as n, getComponentIdByName as o, Query as r, getComponentNameById as s, EntityBuilder as t, isRelationId as u };
1967
+ export { getComponentIdByName as a, isWildcardRelationId as c, isEntityId as d, isRelationId as f, component as i, relation as l, Query as n, getComponentNameById as o, EntityBuilder as r, decodeRelationId as s, World as t, isComponentId as u };
2214
1968
  //# sourceMappingURL=world.mjs.map