@codehz/ecs 0.7.2 → 0.7.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/examples/advanced-scheduling.ts +96 -0
  2. package/examples/collision-detection.ts +229 -0
  3. package/examples/inventory-system-relations.ts +108 -0
  4. package/examples/parent-child-hierarchy.ts +206 -0
  5. package/examples/serialization.ts +337 -0
  6. package/examples/simple.ts +96 -0
  7. package/examples/spatial-grid.ts +276 -0
  8. package/examples/state-machine.ts +273 -0
  9. package/examples/tag-filtering.ts +266 -0
  10. package/package.json +60 -12
  11. package/src/__tests__/commands/buffer-limits.test.ts +72 -0
  12. package/src/__tests__/commands/buffer.test.ts +195 -0
  13. package/src/__tests__/component/singleton.test.ts +148 -0
  14. package/src/__tests__/core/archetype.test.ts +247 -0
  15. package/src/__tests__/core/bitset.test.ts +171 -0
  16. package/src/__tests__/core/changeset.test.ts +254 -0
  17. package/src/__tests__/core/multi-map.test.ts +74 -0
  18. package/src/__tests__/entity/component-registry.test.ts +66 -0
  19. package/src/__tests__/entity/entity.test.ts +520 -0
  20. package/src/__tests__/entity/id-manager.test.ts +157 -0
  21. package/src/__tests__/entity/id-system.test.ts +260 -0
  22. package/src/__tests__/perf/comprehensive.perf.test.ts +300 -0
  23. package/src/__tests__/perf/sync-hotpath.perf.test.ts +79 -0
  24. package/src/__tests__/query/basic.test.ts +341 -0
  25. package/src/__tests__/query/caching.test.ts +112 -0
  26. package/src/__tests__/query/filter.test.ts +111 -0
  27. package/src/__tests__/query/optional.test.ts +231 -0
  28. package/src/__tests__/query/perf.test.ts +99 -0
  29. package/src/__tests__/relations/dont-fragment/basic.test.ts +496 -0
  30. package/src/__tests__/relations/dont-fragment/query-notification.test.ts +125 -0
  31. package/src/__tests__/relations/wildcard.test.ts +179 -0
  32. package/src/__tests__/serialization/bounds.test.ts +237 -0
  33. package/src/__tests__/testing/assertions.test.ts +224 -0
  34. package/src/__tests__/testing/entity-builder.test.ts +84 -0
  35. package/src/__tests__/testing/snapshot.test.ts +150 -0
  36. package/src/__tests__/testing/world-fixture.test.ts +73 -0
  37. package/src/__tests__/world/component-hooks.test.ts +185 -0
  38. package/src/__tests__/world/component-management.test.ts +447 -0
  39. package/src/__tests__/world/entity-management.test.ts +86 -0
  40. package/src/__tests__/world/get-optional.test.ts +96 -0
  41. package/src/__tests__/world/multi-component-hooks.test.ts +502 -0
  42. package/src/__tests__/world/perf.test.ts +93 -0
  43. package/src/__tests__/world/query.test.ts +223 -0
  44. package/src/__tests__/world/serialize.test.ts +83 -0
  45. package/src/__tests__/world/wildcard-relation-hooks.test.ts +332 -0
  46. package/src/archetype/archetype.ts +472 -0
  47. package/src/archetype/helpers.ts +186 -0
  48. package/src/archetype/store.ts +33 -0
  49. package/src/commands/buffer.ts +110 -0
  50. package/src/commands/changeset.ts +104 -0
  51. package/src/component/entity-store.ts +223 -0
  52. package/src/component/registry.ts +657 -0
  53. package/src/component/type-utils.ts +9 -0
  54. package/src/entity/index.ts +63 -0
  55. package/src/entity/manager.ts +115 -0
  56. package/src/entity/relation.ts +319 -0
  57. package/src/entity/types.ts +135 -0
  58. package/src/index.ts +41 -0
  59. package/src/query/filter.ts +75 -0
  60. package/src/query/query.ts +313 -0
  61. package/src/query/registry.ts +101 -0
  62. package/src/storage/serialization.ts +130 -0
  63. package/src/testing/index.ts +634 -0
  64. package/src/types/index.ts +99 -0
  65. package/src/utils/bit-set.ts +133 -0
  66. package/src/utils/multi-map.ts +96 -0
  67. package/src/utils/utils.ts +19 -0
  68. package/src/world/builder.ts +100 -0
  69. package/src/world/commands.ts +378 -0
  70. package/src/world/hooks.ts +358 -0
  71. package/src/world/references.ts +38 -0
  72. package/src/world/serialization.ts +122 -0
  73. package/src/world/world.ts +1201 -0
  74. /package/{builder.d.mts → dist/builder.d.mts} +0 -0
  75. /package/{index.d.mts → dist/index.d.mts} +0 -0
  76. /package/{index.mjs → dist/index.mjs} +0 -0
  77. /package/{testing.d.mts → dist/testing.d.mts} +0 -0
  78. /package/{testing.mjs → dist/testing.mjs} +0 -0
  79. /package/{testing.mjs.map → dist/testing.mjs.map} +0 -0
  80. /package/{world.mjs → dist/world.mjs} +0 -0
  81. /package/{world.mjs.map → dist/world.mjs.map} +0 -0
@@ -0,0 +1,472 @@
1
+ import { normalizeComponentTypes } from "../component/type-utils";
2
+ import type { EntityId, WildcardRelationId } from "../entity";
3
+ import {
4
+ getComponentIdFromRelationId,
5
+ getDetailedIdType,
6
+ getIdType,
7
+ isDontFragmentComponent,
8
+ isWildcardRelationId,
9
+ } from "../entity";
10
+ import { isOptionalEntityId, type ComponentTuple, type ComponentType, type LifecycleHookEntry } from "../types";
11
+ import { getOrCompute } from "../utils/utils";
12
+ import {
13
+ buildCacheKey,
14
+ buildSingleComponent,
15
+ findMatchingDontFragmentRelations,
16
+ getWildcardRelationDataSource,
17
+ isRelationType,
18
+ } from "./helpers";
19
+ import type { DontFragmentStore } from "./store";
20
+
21
+ /**
22
+ * Special value to represent missing component data
23
+ */
24
+ export const MISSING_COMPONENT = Symbol("missing component");
25
+
26
+ /**
27
+ * Archetype class for ECS architecture
28
+ * Represents a group of entities that share the same set of components
29
+ * Optimized for fast iteration and component access
30
+ */
31
+ export class Archetype {
32
+ /**
33
+ * The component types that define this archetype
34
+ */
35
+ public readonly componentTypes: EntityId<any>[];
36
+
37
+ /**
38
+ * Set version of componentTypes for O(1) lookups in hot paths
39
+ */
40
+ public readonly componentTypeSet: ReadonlySet<EntityId<any>>;
41
+
42
+ /**
43
+ * List of entities in this archetype
44
+ */
45
+ private entities: EntityId[] = [];
46
+
47
+ /**
48
+ * Component data storage - maps component type to array of component data
49
+ * Each array index corresponds to the entity index in the entities array
50
+ */
51
+ private componentData: Map<EntityId<any>, any[]> = new Map();
52
+
53
+ /**
54
+ * Reverse mapping from entity to its index in this archetype
55
+ */
56
+ private entityToIndex: Map<EntityId, number> = new Map();
57
+
58
+ /**
59
+ * DontFragmentStore for relation data keyed by entity ID.
60
+ * This allows entities with different relation targets to share the same archetype
61
+ * without migration overhead when entities change archetypes.
62
+ */
63
+ private dontFragmentRelations: DontFragmentStore;
64
+
65
+ /**
66
+ * Multi-hooks that match this archetype
67
+ */
68
+ public readonly matchingMultiHooks: Set<LifecycleHookEntry> = new Set();
69
+
70
+ /**
71
+ * Cache for pre-computed component data sources to avoid repeated calculations
72
+ */
73
+ private componentDataSourcesCache: Map<string, (any[] | EntityId<any>[] | undefined)[]> = new Map();
74
+
75
+ constructor(componentTypes: EntityId<any>[], dontFragmentRelations: DontFragmentStore) {
76
+ this.componentTypes = normalizeComponentTypes(componentTypes);
77
+ this.componentTypeSet = new Set(this.componentTypes);
78
+ this.dontFragmentRelations = dontFragmentRelations;
79
+
80
+ for (const componentType of this.componentTypes) {
81
+ this.componentData.set(componentType, []);
82
+ }
83
+ }
84
+
85
+ get size(): number {
86
+ return this.entities.length;
87
+ }
88
+
89
+ /**
90
+ * Check if the given component types match this archetype
91
+ * @param componentTypes - Component types to check (can be in any order)
92
+ * @returns true if the types match this archetype's component set
93
+ * @note This method handles unsorted input by internally sorting for comparison
94
+ */
95
+ matches(componentTypes: EntityId<any>[]): boolean {
96
+ if (this.componentTypes.length !== componentTypes.length) return false;
97
+ const sortedTypes = normalizeComponentTypes(componentTypes);
98
+ return this.componentTypes.every((type, index) => type === sortedTypes[index]);
99
+ }
100
+
101
+ addEntity(entityId: EntityId, componentData: Map<EntityId<any>, any>): void {
102
+ if (this.entityToIndex.has(entityId)) {
103
+ throw new Error(`Entity ${entityId} is already in this archetype`);
104
+ }
105
+
106
+ const index = this.entities.length;
107
+ this.entities.push(entityId);
108
+ this.entityToIndex.set(entityId, index);
109
+
110
+ // Add component data for regular components
111
+ for (const componentType of this.componentTypes) {
112
+ const data = componentData.get(componentType);
113
+ this.getComponentData(componentType).push(!componentData.has(componentType) ? MISSING_COMPONENT : data);
114
+ }
115
+
116
+ // Add dontFragment relations separately
117
+ this.addDontFragmentRelations(entityId, componentData);
118
+ }
119
+
120
+ private addDontFragmentRelations(entityId: EntityId, componentData: Map<EntityId<any>, any>): void {
121
+ const dontFragmentData = new Map<EntityId<any>, any>();
122
+
123
+ for (const [componentType, data] of componentData) {
124
+ if (this.componentTypeSet.has(componentType)) continue;
125
+
126
+ const detailedType = getDetailedIdType(componentType);
127
+ if (isRelationType(detailedType) && isDontFragmentComponent(detailedType.componentId!)) {
128
+ dontFragmentData.set(componentType, data);
129
+ }
130
+ }
131
+
132
+ if (dontFragmentData.size > 0) {
133
+ this.dontFragmentRelations.set(entityId, dontFragmentData);
134
+ }
135
+ }
136
+
137
+ getEntity(entityId: EntityId): Map<EntityId<any>, any> | undefined {
138
+ const index = this.entityToIndex.get(entityId);
139
+ if (index === undefined) return undefined;
140
+
141
+ const entityData = new Map<EntityId<any>, any>();
142
+
143
+ // Add regular components
144
+ for (const componentType of this.componentTypes) {
145
+ const data = this.getComponentData(componentType)[index];
146
+ entityData.set(componentType, data === MISSING_COMPONENT ? undefined : data);
147
+ }
148
+
149
+ // Add dontFragment relations
150
+ const dontFragmentData = this.dontFragmentRelations.get(entityId);
151
+ if (dontFragmentData) {
152
+ for (const [componentType, data] of dontFragmentData) {
153
+ entityData.set(componentType, data);
154
+ }
155
+ }
156
+
157
+ return entityData;
158
+ }
159
+
160
+ getEntityDontFragmentRelations(entityId: EntityId): Map<EntityId<any>, any> | undefined {
161
+ return this.dontFragmentRelations.get(entityId);
162
+ }
163
+
164
+ dump(): Array<{ entity: EntityId; components: Map<EntityId<any>, any> }> {
165
+ return this.entities.map((entity, i) => {
166
+ const components = new Map<EntityId<any>, any>();
167
+
168
+ for (const componentType of this.componentTypes) {
169
+ const data = this.getComponentData(componentType)[i];
170
+ components.set(componentType, data === MISSING_COMPONENT ? undefined : data);
171
+ }
172
+
173
+ const dontFragmentData = this.dontFragmentRelations.get(entity);
174
+ if (dontFragmentData) {
175
+ for (const [componentType, data] of dontFragmentData) {
176
+ components.set(componentType, data);
177
+ }
178
+ }
179
+
180
+ return { entity, components };
181
+ });
182
+ }
183
+
184
+ removeEntity(entityId: EntityId): Map<EntityId<any>, any> | undefined {
185
+ const index = this.entityToIndex.get(entityId);
186
+ if (index === undefined) return undefined;
187
+
188
+ // Extract component data before removal
189
+ const removedData = new Map<EntityId<any>, any>();
190
+ for (const componentType of this.componentTypes) {
191
+ removedData.set(componentType, this.getComponentData(componentType)[index]);
192
+ }
193
+
194
+ // Include dontFragment relations
195
+ const dontFragmentData = this.dontFragmentRelations.get(entityId);
196
+ if (dontFragmentData) {
197
+ for (const [componentType, data] of dontFragmentData) {
198
+ removedData.set(componentType, data);
199
+ }
200
+ this.dontFragmentRelations.delete(entityId);
201
+ }
202
+
203
+ this.entityToIndex.delete(entityId);
204
+
205
+ // Swap-and-pop for O(1) removal
206
+ const lastIndex = this.entities.length - 1;
207
+ if (index !== lastIndex) {
208
+ const lastEntity = this.entities[lastIndex]!;
209
+ this.entities[index] = lastEntity;
210
+ this.entityToIndex.set(lastEntity, index);
211
+
212
+ for (const componentType of this.componentTypes) {
213
+ const dataArray = this.getComponentData(componentType);
214
+ dataArray[index] = dataArray[lastIndex];
215
+ }
216
+ }
217
+
218
+ this.entities.pop();
219
+ for (const componentType of this.componentTypes) {
220
+ this.getComponentData(componentType).pop();
221
+ }
222
+
223
+ return removedData;
224
+ }
225
+
226
+ exists(entityId: EntityId): boolean {
227
+ return this.entityToIndex.has(entityId);
228
+ }
229
+
230
+ get<T>(entityId: EntityId, componentType: WildcardRelationId<T>): [EntityId<unknown>, any][];
231
+ get<T>(entityId: EntityId, componentType: EntityId<T>): T;
232
+ get<T>(entityId: EntityId, componentType: EntityId<T> | WildcardRelationId<T>): T | [EntityId<unknown>, any][] {
233
+ const index = this.entityToIndex.get(entityId);
234
+ if (index === undefined) {
235
+ throw new Error(`Entity ${entityId} is not in this archetype`);
236
+ }
237
+
238
+ if (isWildcardRelationId(componentType)) {
239
+ return this.getWildcardRelations(entityId, index, componentType);
240
+ }
241
+
242
+ return this.getRegularComponent(entityId, index, componentType);
243
+ }
244
+
245
+ private getWildcardRelations<T>(
246
+ entityId: EntityId,
247
+ index: number,
248
+ componentType: WildcardRelationId<T>,
249
+ ): [EntityId<unknown>, any][] {
250
+ const componentId = getComponentIdFromRelationId(componentType);
251
+ const relations: [EntityId<unknown>, any][] = [];
252
+
253
+ // Check regular archetype components
254
+ for (const relType of this.componentTypes) {
255
+ const relDetailed = getDetailedIdType(relType);
256
+ if (isRelationType(relDetailed) && relDetailed.componentId === componentId) {
257
+ const dataArray = this.getComponentData(relType);
258
+ if (dataArray && dataArray[index] !== undefined) {
259
+ const data = dataArray[index];
260
+ relations.push([relDetailed.targetId, data === MISSING_COMPONENT ? undefined : data]);
261
+ }
262
+ }
263
+ }
264
+
265
+ // Check dontFragment relations
266
+ if (componentId !== undefined) {
267
+ findMatchingDontFragmentRelations(this.dontFragmentRelations.get(entityId), componentId, relations);
268
+ }
269
+
270
+ return relations;
271
+ }
272
+
273
+ private getRegularComponent<T>(entityId: EntityId, index: number, componentType: EntityId<T>): T {
274
+ if (this.componentTypeSet.has(componentType)) {
275
+ const data = this.getComponentData(componentType)[index]!;
276
+ if (data === MISSING_COMPONENT) {
277
+ throw new Error(`Component type ${componentType} not found for entity ${entityId}`);
278
+ }
279
+ return data as T;
280
+ }
281
+
282
+ const dontFragmentData = this.dontFragmentRelations.get(entityId);
283
+ if (dontFragmentData?.has(componentType)) {
284
+ return dontFragmentData.get(componentType);
285
+ }
286
+
287
+ throw new Error(`Component type ${componentType} not found for entity ${entityId}`);
288
+ }
289
+
290
+ getOptional<T>(entityId: EntityId, componentType: EntityId<T>): { value: T } | undefined {
291
+ const index = this.entityToIndex.get(entityId);
292
+ if (index === undefined) {
293
+ throw new Error(`Entity ${entityId} is not in this archetype`);
294
+ }
295
+
296
+ if (this.componentTypeSet.has(componentType)) {
297
+ const data = this.getComponentData(componentType)[index]!;
298
+ if (data === MISSING_COMPONENT) return undefined;
299
+ return { value: data as T };
300
+ }
301
+
302
+ const dontFragmentData = this.dontFragmentRelations.get(entityId);
303
+ if (dontFragmentData?.has(componentType)) {
304
+ return { value: dontFragmentData.get(componentType) };
305
+ }
306
+
307
+ return undefined;
308
+ }
309
+
310
+ set<T>(entityId: EntityId, componentType: EntityId<T>, data: T): void {
311
+ const index = this.entityToIndex.get(entityId);
312
+ if (index === undefined) {
313
+ throw new Error(`Entity ${entityId} is not in this archetype`);
314
+ }
315
+
316
+ if (this.componentData.has(componentType)) {
317
+ this.getComponentData(componentType)[index] = data;
318
+ return;
319
+ }
320
+
321
+ const detailedType = getDetailedIdType(componentType);
322
+ if (isRelationType(detailedType) && isDontFragmentComponent(detailedType.componentId!)) {
323
+ let dontFragmentData = this.dontFragmentRelations.get(entityId);
324
+ if (!dontFragmentData) {
325
+ dontFragmentData = new Map();
326
+ this.dontFragmentRelations.set(entityId, dontFragmentData);
327
+ }
328
+ dontFragmentData.set(componentType, data);
329
+ return;
330
+ }
331
+
332
+ throw new Error(`Component type ${componentType} is not in this archetype`);
333
+ }
334
+
335
+ getEntities(): EntityId[] {
336
+ return this.entities;
337
+ }
338
+
339
+ getEntityToIndexMap(): Map<EntityId, number> {
340
+ return this.entityToIndex;
341
+ }
342
+
343
+ getComponentData<T>(componentType: EntityId<T>): T[] {
344
+ const data = this.componentData.get(componentType);
345
+ if (!data) {
346
+ throw new Error(`Component type ${componentType} is not in this archetype`);
347
+ }
348
+ return data;
349
+ }
350
+
351
+ getOptionalComponentData<T>(componentType: EntityId<T>): T[] | undefined {
352
+ return this.componentData.get(componentType);
353
+ }
354
+
355
+ private getCachedComponentDataSources<const T extends readonly ComponentType<any>[]>(
356
+ componentTypes: T,
357
+ ): (any[] | EntityId<any>[] | undefined)[] {
358
+ const cacheKey = buildCacheKey(componentTypes);
359
+ return getOrCompute(this.componentDataSourcesCache, cacheKey, () =>
360
+ componentTypes.map((compType) => this.getComponentDataSource(compType)),
361
+ );
362
+ }
363
+
364
+ private getComponentDataSource(compType: ComponentType<any>): any[] | EntityId<any>[] | undefined {
365
+ const optional = isOptionalEntityId(compType);
366
+ const actualType = optional ? compType.optional : compType;
367
+ const idType = getIdType(actualType);
368
+
369
+ if (idType === "wildcard-relation") {
370
+ const componentId = getComponentIdFromRelationId(actualType)!;
371
+ return getWildcardRelationDataSource(this.componentTypes, componentId, optional);
372
+ }
373
+
374
+ return optional ? this.getOptionalComponentData(actualType) : this.getComponentData(actualType);
375
+ }
376
+
377
+ private buildComponentsForIndex<const T extends readonly ComponentType<any>[]>(
378
+ componentTypes: T,
379
+ componentDataSources: (any[] | EntityId<any>[] | undefined)[],
380
+ entityIndex: number,
381
+ entityId: EntityId,
382
+ ): ComponentTuple<T> {
383
+ return componentDataSources.map((dataSource, i) =>
384
+ buildSingleComponent(
385
+ componentTypes[i]!,
386
+ dataSource,
387
+ entityIndex,
388
+ entityId,
389
+ (type) => this.getComponentData(type),
390
+ this.dontFragmentRelations,
391
+ ),
392
+ ) as ComponentTuple<T>;
393
+ }
394
+
395
+ getEntitiesWithComponents<const T extends readonly ComponentType<any>[]>(
396
+ componentTypes: T,
397
+ ): Array<{ entity: EntityId; components: ComponentTuple<T> }> {
398
+ const result: Array<{ entity: EntityId; components: ComponentTuple<T> }> = [];
399
+ this.appendEntitiesWithComponents(componentTypes, result);
400
+ return result;
401
+ }
402
+
403
+ appendEntitiesWithComponents<const T extends readonly ComponentType<any>[]>(
404
+ componentTypes: T,
405
+ result: Array<{ entity: EntityId; components: ComponentTuple<T> }>,
406
+ ): void {
407
+ this.forEachWithComponents(componentTypes, (entity, ...components) => {
408
+ result.push({ entity, components });
409
+ });
410
+ }
411
+
412
+ *iterateWithComponents<const T extends readonly ComponentType<any>[]>(
413
+ componentTypes: T,
414
+ ): IterableIterator<[EntityId, ...ComponentTuple<T>]> {
415
+ const componentDataSources = this.getCachedComponentDataSources(componentTypes);
416
+
417
+ for (let entityIndex = 0; entityIndex < this.entities.length; entityIndex++) {
418
+ const entity = this.entities[entityIndex]!;
419
+ const components = this.buildComponentsForIndex(componentTypes, componentDataSources, entityIndex, entity);
420
+ yield [entity, ...components];
421
+ }
422
+ }
423
+
424
+ forEachWithComponents<const T extends readonly ComponentType<any>[]>(
425
+ componentTypes: T,
426
+ callback: (entity: EntityId, ...components: ComponentTuple<T>) => void,
427
+ ): void {
428
+ const componentDataSources = this.getCachedComponentDataSources(componentTypes);
429
+
430
+ for (let entityIndex = 0; entityIndex < this.entities.length; entityIndex++) {
431
+ const entity = this.entities[entityIndex]!;
432
+ const components = this.buildComponentsForIndex(componentTypes, componentDataSources, entityIndex, entity);
433
+ callback(entity, ...components);
434
+ }
435
+ }
436
+
437
+ forEach(callback: (entityId: EntityId, components: Map<EntityId<any>, any>) => void): void {
438
+ for (let i = 0; i < this.entities.length; i++) {
439
+ const components = new Map<EntityId<any>, any>();
440
+ for (const componentType of this.componentTypes) {
441
+ const data = this.getComponentData(componentType)[i];
442
+ components.set(componentType, data === MISSING_COMPONENT ? undefined : data);
443
+ }
444
+ callback(this.entities[i]!, components);
445
+ }
446
+ }
447
+
448
+ hasRelationWithComponentId(componentId: EntityId<any>): boolean {
449
+ // Check regular archetype components
450
+ for (const componentType of this.componentTypes) {
451
+ const detailedType = getDetailedIdType(componentType);
452
+ if (isRelationType(detailedType) && detailedType.componentId === componentId) {
453
+ return true;
454
+ }
455
+ }
456
+
457
+ // Check dontFragment relations
458
+ for (const entityId of this.entities) {
459
+ const entityDontFragmentRelations = this.dontFragmentRelations.get(entityId);
460
+ if (entityDontFragmentRelations) {
461
+ for (const relationType of entityDontFragmentRelations.keys()) {
462
+ const detailedType = getDetailedIdType(relationType);
463
+ if (isRelationType(detailedType) && detailedType.componentId === componentId) {
464
+ return true;
465
+ }
466
+ }
467
+ }
468
+ }
469
+
470
+ return false;
471
+ }
472
+ }
@@ -0,0 +1,186 @@
1
+ import type { ComponentId, EntityId, WildcardRelationId } from "../entity";
2
+ import {
3
+ getComponentIdFromRelationId,
4
+ getDetailedIdType,
5
+ getIdType,
6
+ getTargetIdFromRelationId,
7
+ isRelationId,
8
+ } from "../entity";
9
+ import { isOptionalEntityId, type ComponentType } from "../types";
10
+ import { MISSING_COMPONENT } from "./archetype";
11
+ import type { DontFragmentStore } from "./store";
12
+
13
+ type DetailedIdType = ReturnType<typeof getDetailedIdType>;
14
+
15
+ type RelationDetailedType =
16
+ | { type: "entity-relation"; componentId: ComponentId<any>; targetId: EntityId<any> }
17
+ | { type: "component-relation"; componentId: ComponentId<any>; targetId: ComponentId<any> };
18
+
19
+ /**
20
+ * Find all wildcard relations matching a specific component ID from a components map
21
+ * @param components - Component entity's components map
22
+ * @param wildcardComponentId - The component ID to match (relation part)
23
+ * @returns Array of matching relation IDs
24
+ */
25
+ export function findWildcardRelations(components: Map<EntityId, any>, wildcardComponentId: EntityId): EntityId<any>[] {
26
+ const result: EntityId<any>[] = [];
27
+ for (const [relId] of components) {
28
+ if (isRelationId(relId)) {
29
+ const decoded = getComponentIdFromRelationId(relId);
30
+ if (decoded === wildcardComponentId) {
31
+ result.push(relId);
32
+ }
33
+ }
34
+ }
35
+ return result;
36
+ }
37
+
38
+ /**
39
+ * Check if a components map has any wildcard relations matching a component ID
40
+ * @param components - Component entity's components map
41
+ * @param wildcardComponentId - The component ID to match
42
+ * @returns True if at least one matching relation exists
43
+ */
44
+ export function hasWildcardRelation(components: Map<EntityId, any>, wildcardComponentId: EntityId): boolean {
45
+ for (const relId of components.keys()) {
46
+ if (isRelationId(relId)) {
47
+ const decoded = getComponentIdFromRelationId(relId);
48
+ if (decoded === wildcardComponentId) return true;
49
+ }
50
+ }
51
+ return false;
52
+ }
53
+
54
+ /**
55
+ * Check if a detailed type represents a relation (entity or component)
56
+ */
57
+ export function isRelationType(detailedType: DetailedIdType): detailedType is RelationDetailedType {
58
+ return detailedType.type === "entity-relation" || detailedType.type === "component-relation";
59
+ }
60
+
61
+ /**
62
+ * Check if a component type matches a given component ID for relations
63
+ */
64
+ export function matchesRelationComponentId(componentType: EntityId<any>, componentId: EntityId<any>): boolean {
65
+ const detailedType = getDetailedIdType(componentType);
66
+ return isRelationType(detailedType) && detailedType.componentId === componentId;
67
+ }
68
+
69
+ /**
70
+ * Find all relations in dontFragment data that match a component ID
71
+ */
72
+ export function findMatchingDontFragmentRelations(
73
+ dontFragmentData: Map<EntityId<any>, any> | undefined,
74
+ componentId: EntityId<any>,
75
+ relations: [EntityId<unknown>, any][] = [],
76
+ ): [EntityId<unknown>, any][] {
77
+ if (!dontFragmentData) return relations;
78
+
79
+ for (const [relType, data] of dontFragmentData) {
80
+ const relDetailed = getDetailedIdType(relType);
81
+ if (isRelationType(relDetailed) && relDetailed.componentId === componentId) {
82
+ relations.push([relDetailed.targetId, data]);
83
+ }
84
+ }
85
+ return relations;
86
+ }
87
+
88
+ /**
89
+ * Build cache key for component types
90
+ */
91
+ export function buildCacheKey(componentTypes: readonly ComponentType<any>[]): string {
92
+ return componentTypes.map((id) => (isOptionalEntityId(id) ? `opt(${id.optional})` : `${id}`)).join(",");
93
+ }
94
+
95
+ /**
96
+ * Get data source for wildcard relations from component types
97
+ */
98
+ export function getWildcardRelationDataSource(
99
+ componentTypes: EntityId<any>[],
100
+ componentId: EntityId<any>,
101
+ optional: boolean,
102
+ ): EntityId<any>[] | undefined {
103
+ const matchingRelations = componentTypes.filter((ct) => matchesRelationComponentId(ct, componentId));
104
+ return optional ? (matchingRelations.length > 0 ? matchingRelations : undefined) : matchingRelations;
105
+ }
106
+
107
+ /**
108
+ * Build wildcard relation value from matching relations
109
+ */
110
+ export function buildWildcardRelationValue(
111
+ wildcardRelationType: WildcardRelationId<any>,
112
+ matchingRelations: EntityId<any>[] | undefined,
113
+ getDataAtIndex: (relType: EntityId<any>) => any,
114
+ dontFragmentData: Map<EntityId<any>, any> | undefined,
115
+ entityId: EntityId,
116
+ optional: boolean,
117
+ ): any {
118
+ const relations: [EntityId<unknown>, any][] = [];
119
+ const targetComponentId = getComponentIdFromRelationId(wildcardRelationType);
120
+
121
+ // Add regular archetype relations
122
+ for (const relType of matchingRelations || []) {
123
+ const data = getDataAtIndex(relType);
124
+ const targetId = getTargetIdFromRelationId(relType)!;
125
+ relations.push([targetId, data === MISSING_COMPONENT ? undefined : data]);
126
+ }
127
+
128
+ // Add dontFragment relations
129
+ if (targetComponentId !== undefined) {
130
+ findMatchingDontFragmentRelations(dontFragmentData, targetComponentId, relations);
131
+ }
132
+
133
+ if (relations.length === 0) {
134
+ if (!optional) {
135
+ const componentId = getComponentIdFromRelationId(wildcardRelationType);
136
+ throw new Error(
137
+ `No matching relations found for mandatory wildcard relation component ${componentId} on entity ${entityId}`,
138
+ );
139
+ }
140
+ return undefined;
141
+ }
142
+
143
+ return optional ? { value: relations } : relations;
144
+ }
145
+
146
+ /**
147
+ * Build regular component value from data source
148
+ */
149
+ export function buildRegularComponentValue(dataSource: any[] | undefined, entityIndex: number, optional: boolean): any {
150
+ if (dataSource === undefined) {
151
+ if (optional) return undefined;
152
+ throw new Error(`Component data not found for mandatory component type`);
153
+ }
154
+
155
+ const data = dataSource[entityIndex];
156
+ const result = data === MISSING_COMPONENT ? undefined : data;
157
+ return optional ? { value: result } : result;
158
+ }
159
+
160
+ /**
161
+ * Build a single component value based on its type
162
+ */
163
+ export function buildSingleComponent(
164
+ compType: ComponentType<any>,
165
+ dataSource: any[] | EntityId<any>[] | undefined,
166
+ entityIndex: number,
167
+ entityId: EntityId,
168
+ getComponentData: (type: EntityId<any>) => any[],
169
+ dontFragmentRelations: DontFragmentStore,
170
+ ): any {
171
+ const optional = isOptionalEntityId(compType);
172
+ const actualType = optional ? compType.optional : compType;
173
+
174
+ if (getIdType(actualType) === "wildcard-relation") {
175
+ return buildWildcardRelationValue(
176
+ actualType as WildcardRelationId<any>,
177
+ dataSource as EntityId<any>[] | undefined,
178
+ (relType) => getComponentData(relType)[entityIndex],
179
+ dontFragmentRelations.get(entityId),
180
+ entityId,
181
+ optional,
182
+ );
183
+ } else {
184
+ return buildRegularComponentValue(dataSource as any[] | undefined, entityIndex, optional);
185
+ }
186
+ }
@@ -0,0 +1,33 @@
1
+ import type { EntityId } from "../entity";
2
+
3
+ /**
4
+ * Minimal interface for storing dontFragment relation data keyed by entity ID.
5
+ *
6
+ * Using an interface here decouples `Archetype` (and `world-commands.ts`) from
7
+ * the concrete `Map` used by `World`, making archetypes independently testable.
8
+ */
9
+ export interface DontFragmentStore {
10
+ get(entityId: EntityId): Map<EntityId<any>, any> | undefined;
11
+ set(entityId: EntityId, data: Map<EntityId<any>, any>): void;
12
+ delete(entityId: EntityId): void;
13
+ }
14
+
15
+ /**
16
+ * Default implementation backed by a plain `Map`.
17
+ * Created once by `World` and shared with every `Archetype`.
18
+ */
19
+ export class DontFragmentStoreImpl implements DontFragmentStore {
20
+ private readonly data: Map<EntityId, Map<EntityId<any>, any>> = new Map();
21
+
22
+ get(entityId: EntityId): Map<EntityId<any>, any> | undefined {
23
+ return this.data.get(entityId);
24
+ }
25
+
26
+ set(entityId: EntityId, data: Map<EntityId<any>, any>): void {
27
+ this.data.set(entityId, data);
28
+ }
29
+
30
+ delete(entityId: EntityId): void {
31
+ this.data.delete(entityId);
32
+ }
33
+ }