@codehz/ecs 0.8.2 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +26 -3
- package/README.md +41 -4
- package/dist/builder.d.mts +348 -83
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -2
- package/dist/testing.d.mts +1 -1
- package/dist/testing.mjs +1 -1
- package/dist/world.mjs +1922 -1400
- package/dist/world.mjs.map +1 -1
- package/examples/debug-observability.ts +92 -0
- package/examples/inventory-system-relations.ts +1 -1
- package/examples/parent-child-hierarchy.ts +18 -38
- package/examples/spatial-grid.ts +1 -1
- package/package.json +1 -1
- package/skills/ecs/SKILL.md +4 -4
- package/src/__tests__/component/singleton.test.ts +116 -35
- package/src/__tests__/core/archetype.test.ts +155 -13
- package/src/__tests__/core/bitset.test.ts +12 -0
- package/src/__tests__/entity/entity.test.ts +33 -0
- package/src/__tests__/entity/id-system.test.ts +40 -0
- package/src/__tests__/perf/comprehensive.perf.test.ts +6 -9
- package/src/__tests__/perf/serialization.perf.test.ts +242 -0
- package/src/__tests__/perf/{dontfragment-wildcard.perf.test.ts → sparse-wildcard.perf.test.ts} +13 -16
- package/src/__tests__/query/caching.test.ts +62 -0
- package/src/__tests__/query/filter.test.ts +16 -22
- package/src/__tests__/query/perf.test.ts +3 -5
- package/src/__tests__/relations/hierarchy.test.ts +208 -0
- package/src/__tests__/relations/{dont-fragment → sparse}/basic.test.ts +64 -69
- package/src/__tests__/relations/{dont-fragment → sparse}/query-notification.test.ts +17 -9
- package/src/__tests__/serialization/bounds.test.ts +133 -1
- package/src/__tests__/world/commands.test.ts +337 -0
- package/src/__tests__/world/component-management.test.ts +6 -5
- package/src/__tests__/world/debug-stats.test.ts +206 -0
- package/src/__tests__/world/multi-component-hooks.test.ts +44 -0
- package/src/__tests__/world/serialize.test.ts +17 -0
- package/src/__tests__/world/wildcard-relation-hooks.test.ts +127 -0
- package/src/archetype/archetype.ts +96 -46
- package/src/archetype/helpers.ts +7 -29
- package/src/archetype/store.ts +35 -20
- package/src/commands/buffer.ts +5 -2
- package/src/commands/changeset.ts +0 -31
- package/src/component/registry.ts +64 -63
- package/src/entity/index.ts +6 -3
- package/src/index.ts +15 -0
- package/src/query/filter.ts +4 -10
- package/src/query/query.ts +12 -12
- package/src/storage/serialization.ts +29 -2
- package/src/types/index.ts +71 -0
- package/src/world/archetype-manager.ts +283 -0
- package/src/world/command-executor.ts +258 -0
- package/src/world/commands.ts +44 -56
- package/src/world/debug-stats.ts +147 -0
- package/src/world/hooks.ts +8 -0
- package/src/world/operations.ts +88 -0
- package/src/world/serialization.ts +32 -18
- package/src/world/singleton.ts +51 -0
- package/src/world/world.ts +429 -457
package/dist/builder.d.mts
CHANGED
|
@@ -248,7 +248,7 @@ interface ComponentOptions<T = any> {
|
|
|
248
248
|
* `cascadeDelete`, deleting the target entity will both (a) delete the
|
|
249
249
|
* referencing entity, and (b) the exclusivity constraint prevents the
|
|
250
250
|
* entity from having multiple cascade-delete relations of the same type.
|
|
251
|
-
* - **`
|
|
251
|
+
* - **`sparse`**: Compatible. Exclusivity is enforced at the data level
|
|
252
252
|
* regardless of whether the archetype is fragmented.
|
|
253
253
|
*
|
|
254
254
|
* @example
|
|
@@ -306,7 +306,8 @@ interface ComponentOptions<T = any> {
|
|
|
306
306
|
*/
|
|
307
307
|
cascadeDelete?: boolean;
|
|
308
308
|
/**
|
|
309
|
-
* If true, relations with this component will not cause
|
|
309
|
+
* If true, relations with this component use sparse storage and will not cause
|
|
310
|
+
* archetype fragmentation.
|
|
310
311
|
*
|
|
311
312
|
* **Problem it solves**: By default, each unique relation pair `(component, target)`
|
|
312
313
|
* creates a **separate archetype**. If 100 entities each have a `ChildOf` relation
|
|
@@ -314,28 +315,28 @@ interface ComponentOptions<T = any> {
|
|
|
314
315
|
* Queries that iterate over all entities with a `ChildOf` relation must check all
|
|
315
316
|
* 100 archetypes, which degrades iteration performance and increases memory overhead.
|
|
316
317
|
*
|
|
317
|
-
* **How it works**: When `
|
|
318
|
-
* contribute to the archetype signature. Entities with different targets
|
|
319
|
-
* relation component share a **single archetype**, and the per-entity
|
|
320
|
-
* stored in a separate
|
|
321
|
-
* A wildcard relation marker (`relation(Comp, "*")`) is placed
|
|
322
|
-
* component list so queries can still discover matching archetypes.
|
|
318
|
+
* **How it works (sparse storage)**: When `sparse` is enabled, the relation's target
|
|
319
|
+
* does **not** contribute to the archetype signature. Entities with different targets
|
|
320
|
+
* for the same relation component share a **single archetype**, and the per-entity
|
|
321
|
+
* target data is stored in a separate side store (historically called
|
|
322
|
+
* `DontFragmentStore`, now `SparseStore`). A wildcard relation marker (`relation(Comp, "*")`) is placed
|
|
323
|
+
* in the archetype component list so queries can still discover matching archetypes.
|
|
323
324
|
*
|
|
324
325
|
* **Use cases**:
|
|
325
326
|
* - **Hierarchy/ownership**: `ChildOf` relations where thousands of entities each
|
|
326
327
|
* point to different parent entities.
|
|
327
328
|
* - **Dynamic targeting**: Relations where targets change frequently (e.g., AI
|
|
328
|
-
* targeting, inventory slots) — without `
|
|
329
|
-
*
|
|
329
|
+
* targeting, inventory slots) — without `sparse`, each target change would cause
|
|
330
|
+
* an archetype migration, which is expensive.
|
|
330
331
|
* - **High-cardinality relations**: Any relation where the number of unique targets
|
|
331
332
|
* is large compared to the number of entities.
|
|
332
333
|
*
|
|
333
334
|
* **Performance implications**:
|
|
334
|
-
* - **Without `
|
|
335
|
+
* - **Without `sparse`**: Archetype count grows linearly with unique targets.
|
|
335
336
|
* Each archetype migration (changing a relation target) requires moving the entity's
|
|
336
337
|
* data between component arrays.
|
|
337
|
-
* - **With `
|
|
338
|
-
*
|
|
338
|
+
* - **With `sparse`**: Archetype count stays constant regardless of target diversity.
|
|
339
|
+
* Changing a relation target is an O(1) update in the sparse side store.
|
|
339
340
|
* The trade-off is an extra map lookup when accessing the relation data.
|
|
340
341
|
*
|
|
341
342
|
* **Constraints**:
|
|
@@ -344,13 +345,16 @@ interface ComponentOptions<T = any> {
|
|
|
344
345
|
* archetype carries a wildcard marker so queries can discover it.
|
|
345
346
|
* - Works with `exclusive` and `cascadeDelete` simultaneously.
|
|
346
347
|
*
|
|
348
|
+
* **Backward compatibility**: The legacy key `dontFragment` is still accepted and
|
|
349
|
+
* behaves identically. Prefer `sparse` in new code.
|
|
350
|
+
*
|
|
347
351
|
* @example
|
|
348
352
|
* ```ts
|
|
349
|
-
* // Without
|
|
353
|
+
* // Without sparse: 100 entities with different parents = 100 archetypes
|
|
350
354
|
* const ChildOf = component(); // default: fragmentation happens
|
|
351
355
|
*
|
|
352
|
-
* // With
|
|
353
|
-
* const ChildOf = component({
|
|
356
|
+
* // With sparse: 100 entities with different parents = 1 archetype
|
|
357
|
+
* const ChildOf = component({ sparse: true });
|
|
354
358
|
*
|
|
355
359
|
* for (let i = 0; i < 100; i++) {
|
|
356
360
|
* const parent = world.new();
|
|
@@ -359,11 +363,17 @@ interface ComponentOptions<T = any> {
|
|
|
359
363
|
* world.set(child, relation(ChildOf, parent));
|
|
360
364
|
* }
|
|
361
365
|
* world.sync();
|
|
362
|
-
* //
|
|
366
|
+
* // sparse: 1 archetype for all 100 entities
|
|
363
367
|
* // without: 100 archetypes, one per unique parent
|
|
364
368
|
* ```
|
|
365
369
|
*
|
|
366
|
-
* Inspired by Flecs' `DontFragment` trait.
|
|
370
|
+
* Inspired by Flecs' `DontFragment` trait (now exposed as the clearer `sparse` option).
|
|
371
|
+
*/
|
|
372
|
+
sparse?: boolean;
|
|
373
|
+
/**
|
|
374
|
+
* @deprecated Use `sparse: true` instead. This key is kept solely for backward
|
|
375
|
+
* compatibility; `component({ dontFragment: true })` continues to work exactly
|
|
376
|
+
* as before and is equivalent to `sparse: true`.
|
|
367
377
|
*/
|
|
368
378
|
dontFragment?: boolean;
|
|
369
379
|
/**
|
|
@@ -453,6 +463,53 @@ declare function getComponentIdByName(name: string): ComponentId<any> | undefine
|
|
|
453
463
|
* @returns The component name if found, undefined otherwise
|
|
454
464
|
*/
|
|
455
465
|
declare function getComponentNameById(id: ComponentId<any>): string | undefined;
|
|
466
|
+
/**
|
|
467
|
+
* Check if a component is marked as `sparse` (sparse storage for relations).
|
|
468
|
+
*
|
|
469
|
+
* When a component has `sparse: true`, relations using it do not cause archetype
|
|
470
|
+
* fragmentation — entities with different relation targets can share the same
|
|
471
|
+
* archetype. This is a fast O(1) bitset lookup. The legacy `dontFragment` key
|
|
472
|
+
* is still accepted and sets the same internal flag.
|
|
473
|
+
*
|
|
474
|
+
* @param id - The component ID to check.
|
|
475
|
+
* @returns `true` if the component was created with `sparse: true` (or the
|
|
476
|
+
* legacy `dontFragment: true`).
|
|
477
|
+
*
|
|
478
|
+
* @see {@link ComponentOptions.sparse} for the full explanation of sparse storage.
|
|
479
|
+
*/
|
|
480
|
+
declare function isSparseComponent(id: ComponentId<any>): boolean;
|
|
481
|
+
/**
|
|
482
|
+
* Check if an ID is a specific (non-wildcard) relation backed by a `sparse`
|
|
483
|
+
* component (i.e. stored in the side sparse store rather than the archetype).
|
|
484
|
+
*
|
|
485
|
+
* This is used in hot paths (archetype resolution, command processing) to determine
|
|
486
|
+
* whether a relation should be excluded from the archetype signature.
|
|
487
|
+
*
|
|
488
|
+
* @param id - The entity/relation ID to check (must be a relation ID, not a plain
|
|
489
|
+
* component ID).
|
|
490
|
+
* @returns `true` if this is a specific-target relation (not wildcard) whose base
|
|
491
|
+
* component was created with `sparse: true` (or legacy `dontFragment: true`).
|
|
492
|
+
*
|
|
493
|
+
* @see {@link isSparseWildcard} for the wildcard variant.
|
|
494
|
+
* @see {@link ComponentOptions.sparse} for the full explanation.
|
|
495
|
+
*/
|
|
496
|
+
declare function isSparseRelation(id: EntityId<any>): boolean;
|
|
497
|
+
/**
|
|
498
|
+
* Check if an ID is a wildcard relation (`relation(Comp, "*")`) backed by a
|
|
499
|
+
* `sparse` component.
|
|
500
|
+
*
|
|
501
|
+
* Wildcard markers for sparse components are placed in the archetype component
|
|
502
|
+
* list so that queries can discover archetypes containing entities with that
|
|
503
|
+
* relation type.
|
|
504
|
+
*
|
|
505
|
+
* @param id - The entity/relation ID to check.
|
|
506
|
+
* @returns `true` if this is a wildcard relation (`"*"` target) whose base
|
|
507
|
+
* component was created with `sparse: true` (or legacy `dontFragment: true`).
|
|
508
|
+
*
|
|
509
|
+
* @see {@link isSparseRelation} for the specific-target variant.
|
|
510
|
+
* @see {@link ComponentOptions.sparse} for the full explanation.
|
|
511
|
+
*/
|
|
512
|
+
declare function isSparseWildcard(id: EntityId<any>): boolean;
|
|
456
513
|
//#endregion
|
|
457
514
|
//#region src/storage/serialization.d.ts
|
|
458
515
|
type SerializedEntityId = number | string | {
|
|
@@ -558,24 +615,78 @@ interface LifecycleHookEntry {
|
|
|
558
615
|
/** Archetypes that match this hook, used for precise cleanup on unsubscription */
|
|
559
616
|
matchedArchetypes?: Set<any>;
|
|
560
617
|
}
|
|
618
|
+
/**
|
|
619
|
+
* Statistics payload delivered to callbacks registered via `World.createDebugStatsCollector`.
|
|
620
|
+
*
|
|
621
|
+
* All structural counts are snapshots taken after the sync that triggered delivery.
|
|
622
|
+
* `activity` always reflects work performed during that specific sync.
|
|
623
|
+
*
|
|
624
|
+
* Timestamps are raw `performance.now()` values suitable for `performance.measure`.
|
|
625
|
+
*/
|
|
626
|
+
interface SyncDebugStats {
|
|
627
|
+
readonly timestamps: {
|
|
628
|
+
readonly syncStart: number;
|
|
629
|
+
readonly syncEnd: number;
|
|
630
|
+
readonly commandBufferStart: number;
|
|
631
|
+
readonly commandBufferEnd: number;
|
|
632
|
+
};
|
|
633
|
+
/** Number of iterations the internal command buffer loop performed during this sync. */
|
|
634
|
+
readonly commandIterations: number;
|
|
635
|
+
readonly entities: {
|
|
636
|
+
readonly total: number;
|
|
637
|
+
readonly freelistSize: number;
|
|
638
|
+
readonly nextId: number;
|
|
639
|
+
};
|
|
640
|
+
readonly archetypes: {
|
|
641
|
+
readonly total: number;
|
|
642
|
+
readonly empty: number;
|
|
643
|
+
};
|
|
644
|
+
readonly queries: {
|
|
645
|
+
readonly cached: number;
|
|
646
|
+
readonly registered: number;
|
|
647
|
+
};
|
|
648
|
+
readonly hooks: {
|
|
649
|
+
readonly total: number;
|
|
650
|
+
};
|
|
651
|
+
/** Sizes of stable internal reverse indices (conservative set). */
|
|
652
|
+
readonly indices: {
|
|
653
|
+
readonly entityReferences: number;
|
|
654
|
+
readonly entityToReferencingArchetypes: number;
|
|
655
|
+
readonly archetypesByComponent: number;
|
|
656
|
+
};
|
|
657
|
+
/**
|
|
658
|
+
* Activity that occurred as a direct result of this sync.
|
|
659
|
+
* All fields are always present (never optional).
|
|
660
|
+
*/
|
|
661
|
+
readonly activity: {
|
|
662
|
+
/** Number of entities that performed an archetype migration (hasArchetypeStructuralChange was true). */readonly migrations: number; /** Total number of individual hook callback invocations (invokeHook calls). */
|
|
663
|
+
readonly hooksExecuted: number; /** Number of new archetypes created during this sync. */
|
|
664
|
+
readonly archetypesCreated: number; /** Number of archetypes removed during this sync. */
|
|
665
|
+
readonly archetypesRemoved: number;
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* Handle returned by `World.createDebugStatsCollector`.
|
|
670
|
+
* The object itself carries no data — its only responsibility is lifetime management.
|
|
671
|
+
* Use with `using` or call `[Symbol.dispose]()` when you no longer need collection.
|
|
672
|
+
*/
|
|
673
|
+
interface DebugStatsCollector {
|
|
674
|
+
[Symbol.dispose](): void;
|
|
675
|
+
}
|
|
561
676
|
//#endregion
|
|
562
677
|
//#region src/archetype/store.d.ts
|
|
563
678
|
/**
|
|
564
|
-
* Interface for
|
|
565
|
-
*
|
|
566
|
-
* Storage is now primarily keyed by relation ComponentId (the "kind" of relation)
|
|
567
|
-
* rather than by entity. This provides O(1) or near-O(1) answers for the hot
|
|
568
|
-
* wildcard-related paths (hasRelationWithComponentId, wildcard materialization
|
|
569
|
-
* during iteration, hook matching, etc.).
|
|
679
|
+
* Interface for the sparse side store used by components declared with `sparse: true`
|
|
680
|
+
* (or the legacy `dontFragment: true` alias).
|
|
570
681
|
*
|
|
571
|
-
*
|
|
572
|
-
*
|
|
573
|
-
* operations (removeEntity, dump, getEntity, serialization).
|
|
682
|
+
* Relation data for these components lives here instead of in archetype columns,
|
|
683
|
+
* preventing fragmentation for high-cardinality or frequently-changing relations.
|
|
574
684
|
*
|
|
575
|
-
*
|
|
576
|
-
*
|
|
685
|
+
* Storage is primarily keyed by base relation ComponentId. This enables efficient
|
|
686
|
+
* per-component lookups required by wildcard queries (relation(Comp, "*")) and
|
|
687
|
+
* archetype filtering, while still supporting full-entity enumeration when needed.
|
|
577
688
|
*/
|
|
578
|
-
interface
|
|
689
|
+
interface SparseStore {
|
|
579
690
|
getValue(entityId: EntityId, relationType: EntityId<any>): any | undefined;
|
|
580
691
|
setValue(entityId: EntityId, relationType: EntityId<any>, data: any): void;
|
|
581
692
|
deleteValue(entityId: EntityId, relationType: EntityId<any>): boolean;
|
|
@@ -583,6 +694,12 @@ interface DontFragmentStore {
|
|
|
583
694
|
getRelationsForComponent(entityId: EntityId, componentId: EntityId<any>): [target: EntityId, data: any][];
|
|
584
695
|
getAllForEntity(entityId: EntityId): Array<[relationType: EntityId<any>, data: any]>;
|
|
585
696
|
deleteEntity(entityId: EntityId): void;
|
|
697
|
+
/**
|
|
698
|
+
* @internal Bulk helper for serialization of many entities.
|
|
699
|
+
* Default implementation simply loops getAllForEntity; subclasses / future
|
|
700
|
+
* implementations can provide a more efficient fused walk.
|
|
701
|
+
*/
|
|
702
|
+
getAllForEntities(entityIds: readonly EntityId[]): Map<EntityId, Array<[EntityId<any>, any]>>;
|
|
586
703
|
}
|
|
587
704
|
//#endregion
|
|
588
705
|
//#region src/archetype/archetype.d.ts
|
|
@@ -614,11 +731,10 @@ declare class Archetype {
|
|
|
614
731
|
*/
|
|
615
732
|
private entityToIndex;
|
|
616
733
|
/**
|
|
617
|
-
*
|
|
618
|
-
* Uses optimized RelationEntry (single/multi) for the common exclusive case.
|
|
734
|
+
* SparseStore used for relations declared with `sparse: true`.
|
|
619
735
|
* See store.ts for implementation details.
|
|
620
736
|
*/
|
|
621
|
-
private
|
|
737
|
+
private sparseRelations;
|
|
622
738
|
/**
|
|
623
739
|
* Multi-hooks that match this archetype
|
|
624
740
|
*/
|
|
@@ -627,7 +743,7 @@ declare class Archetype {
|
|
|
627
743
|
* Cache for pre-computed component data sources to avoid repeated calculations
|
|
628
744
|
*/
|
|
629
745
|
private componentDataSourcesCache;
|
|
630
|
-
constructor(componentTypes: EntityId<any>[],
|
|
746
|
+
constructor(componentTypes: EntityId<any>[], sparseStore: SparseStore);
|
|
631
747
|
get size(): number;
|
|
632
748
|
/**
|
|
633
749
|
* Check if the given component types match this archetype
|
|
@@ -637,19 +753,34 @@ declare class Archetype {
|
|
|
637
753
|
*/
|
|
638
754
|
matches(componentTypes: EntityId<any>[]): boolean;
|
|
639
755
|
addEntity(entityId: EntityId, componentData: Map<EntityId<any>, any>): void;
|
|
640
|
-
private
|
|
756
|
+
private addSparseRelations;
|
|
641
757
|
getEntity(entityId: EntityId): Map<EntityId<any>, any> | undefined;
|
|
642
758
|
/**
|
|
643
|
-
* Returns all
|
|
644
|
-
*
|
|
645
|
-
*
|
|
646
|
-
* Prefer the new DontFragmentStore methods when possible.
|
|
759
|
+
* Returns all sparse-stored relations for the given entity.
|
|
760
|
+
* Internal helper used by command processing and tests.
|
|
647
761
|
*/
|
|
648
|
-
|
|
762
|
+
getEntitySparseRelations(entityId: EntityId): Map<EntityId<any>, any> | undefined;
|
|
649
763
|
dump(): Array<{
|
|
650
764
|
entity: EntityId;
|
|
651
765
|
components: Map<EntityId<any>, any>;
|
|
652
766
|
}>;
|
|
767
|
+
/**
|
|
768
|
+
* @internal Serialization fast-path.
|
|
769
|
+
*
|
|
770
|
+
* Appends SerializedEntity records directly from the archetype's column storage
|
|
771
|
+
* (componentData arrays) plus sparse relations, avoiding per-entity Map
|
|
772
|
+
* allocation and repeated Array.from(entries()).
|
|
773
|
+
*
|
|
774
|
+
* Component type IDs should be pre-encoded by the caller (once per archetype)
|
|
775
|
+
* and passed in `encodedComponentTypes` (same order and length as this.componentTypes).
|
|
776
|
+
*
|
|
777
|
+
* The provided `encode` function should be the cached variant for best performance
|
|
778
|
+
* on entity IDs and any sparse relation type IDs.
|
|
779
|
+
*
|
|
780
|
+
* `sparseByEntity` is an optional pre-fetched map from a bulk
|
|
781
|
+
* `SparseStore.getAllForEntities` call (further reduces per-entity calls).
|
|
782
|
+
*/
|
|
783
|
+
appendSerializedEntities(out: SerializedEntity[], encode: (id: EntityId<any>) => SerializedEntityId, encodedComponentTypes: SerializedEntityId[], sparseByEntity?: Map<EntityId, Array<[EntityId<any>, any]>>): void;
|
|
653
784
|
removeEntity(entityId: EntityId): Map<EntityId<any>, any> | undefined;
|
|
654
785
|
exists(entityId: EntityId): boolean;
|
|
655
786
|
get<T>(entityId: EntityId, componentType: WildcardRelationId<T>): [EntityId<unknown>, any][];
|
|
@@ -764,8 +895,8 @@ declare class Query {
|
|
|
764
895
|
_cacheKey: string | undefined;
|
|
765
896
|
/** Cached wildcard component types for faster entity filtering */
|
|
766
897
|
private wildcardTypes;
|
|
767
|
-
/** Cached specific
|
|
768
|
-
private
|
|
898
|
+
/** Cached specific sparse relation types that need entity-level filtering */
|
|
899
|
+
private specificSparseRelationTypes;
|
|
769
900
|
/**
|
|
770
901
|
* @internal Queries should be created via {@link World.createQuery}, not instantiated directly.
|
|
771
902
|
*/
|
|
@@ -787,7 +918,7 @@ declare class Query {
|
|
|
787
918
|
*/
|
|
788
919
|
getEntities(): EntityId[];
|
|
789
920
|
/**
|
|
790
|
-
* Check if entity matches all query requirements (wildcards and specific
|
|
921
|
+
* Check if entity matches all query requirements (wildcards and specific sparse relations)
|
|
791
922
|
*/
|
|
792
923
|
private entityMatchesQuery;
|
|
793
924
|
/**
|
|
@@ -878,6 +1009,41 @@ declare class Query {
|
|
|
878
1009
|
get disposed(): boolean;
|
|
879
1010
|
}
|
|
880
1011
|
//#endregion
|
|
1012
|
+
//#region src/world/singleton.d.ts
|
|
1013
|
+
interface SingletonHandleOps<T> {
|
|
1014
|
+
has(): boolean;
|
|
1015
|
+
get(): T;
|
|
1016
|
+
getOptional(): {
|
|
1017
|
+
value: T;
|
|
1018
|
+
} | undefined;
|
|
1019
|
+
remove(): void;
|
|
1020
|
+
set(value: T | undefined): void;
|
|
1021
|
+
}
|
|
1022
|
+
/**
|
|
1023
|
+
* Explicit handle for a singleton component (component-as-entity).
|
|
1024
|
+
*
|
|
1025
|
+
* This provides an explicit and concise API for singleton components without
|
|
1026
|
+
* overloading `world.set()` semantics.
|
|
1027
|
+
*
|
|
1028
|
+
* @example
|
|
1029
|
+
* const config = world.singleton(Config);
|
|
1030
|
+
* config.set({ debug: true });
|
|
1031
|
+
* world.sync();
|
|
1032
|
+
* console.log(config.get());
|
|
1033
|
+
*/
|
|
1034
|
+
declare class SingletonHandle<T = void> {
|
|
1035
|
+
readonly componentId: ComponentId<T>;
|
|
1036
|
+
private readonly ops;
|
|
1037
|
+
constructor(componentId: ComponentId<T>, ops: SingletonHandleOps<T>);
|
|
1038
|
+
has(): boolean;
|
|
1039
|
+
get(): T;
|
|
1040
|
+
getOptional(): {
|
|
1041
|
+
value: T;
|
|
1042
|
+
} | undefined;
|
|
1043
|
+
remove(): void;
|
|
1044
|
+
set(...args: T extends void ? [] : [value: NoInfer<T>]): void;
|
|
1045
|
+
}
|
|
1046
|
+
//#endregion
|
|
881
1047
|
//#region src/world/world.d.ts
|
|
882
1048
|
/**
|
|
883
1049
|
* World class for ECS architecture
|
|
@@ -885,28 +1051,22 @@ declare class Query {
|
|
|
885
1051
|
*/
|
|
886
1052
|
declare class World {
|
|
887
1053
|
private entityIdManager;
|
|
888
|
-
private archetypes;
|
|
889
|
-
private archetypeBySignature;
|
|
890
|
-
private entityToArchetype;
|
|
891
|
-
private archetypesByComponent;
|
|
892
1054
|
private entityReferences;
|
|
893
|
-
/**
|
|
894
|
-
private
|
|
895
|
-
/** DontFragment relation storage, shared with all Archetype instances */
|
|
896
|
-
private readonly dontFragmentStore;
|
|
1055
|
+
/** Sparse relation storage (for components created with `sparse: true`), shared with all Archetype instances */
|
|
1056
|
+
private readonly sparseStore;
|
|
897
1057
|
/** Component entity (singleton) storage */
|
|
898
1058
|
private readonly componentEntities;
|
|
1059
|
+
private archetypeManager;
|
|
1060
|
+
private get archetypes();
|
|
1061
|
+
private get entityToArchetype();
|
|
1062
|
+
private get archetypesByComponent();
|
|
1063
|
+
private get entityToReferencingArchetypes();
|
|
899
1064
|
private readonly queryRegistry;
|
|
900
1065
|
private hooks;
|
|
1066
|
+
private readonly debugStats;
|
|
901
1067
|
private commandBuffer;
|
|
902
|
-
private
|
|
903
|
-
private readonly _removeChangeset;
|
|
904
|
-
/** Cached command processor context to avoid per-entity object allocation */
|
|
905
|
-
private readonly _commandCtx;
|
|
906
|
-
/** Cached hooks context to avoid per-entity object allocation */
|
|
907
|
-
private readonly _hooksCtx;
|
|
1068
|
+
private commandExecutor;
|
|
908
1069
|
constructor(snapshot?: SerializedWorld);
|
|
909
|
-
private createArchetypeSignature;
|
|
910
1070
|
/**
|
|
911
1071
|
* Creates a new entity.
|
|
912
1072
|
* The entity is created with an empty component set and can be configured using `set()`.
|
|
@@ -951,11 +1111,6 @@ declare class World {
|
|
|
951
1111
|
* if (world.has(entity, Position)) { ... }
|
|
952
1112
|
*/
|
|
953
1113
|
exists(entityId: EntityId): boolean;
|
|
954
|
-
private assertEntityExists;
|
|
955
|
-
private assertComponentTypeValid;
|
|
956
|
-
private assertSetComponentTypeValid;
|
|
957
|
-
private resolveSetOperation;
|
|
958
|
-
private resolveRemoveOperation;
|
|
959
1114
|
/**
|
|
960
1115
|
* Adds or updates a component on an entity (or marks void component as present).
|
|
961
1116
|
* The change is buffered and takes effect after calling `world.sync()`.
|
|
@@ -967,21 +1122,17 @@ declare class World {
|
|
|
967
1122
|
* @overload set<T>(entityId: EntityId, componentType: EntityId<T>, component: NoInfer<T>): void
|
|
968
1123
|
* Adds or updates a component with data on the entity
|
|
969
1124
|
*
|
|
970
|
-
* @overload set<T>(componentId: ComponentId<T>, component: NoInfer<T>): void
|
|
971
|
-
* Adds or updates a singleton component (shorthand for set(componentId, componentId, component))
|
|
972
|
-
*
|
|
973
1125
|
* @throws {Error} If the entity does not exist
|
|
974
1126
|
* @throws {Error} If the component type is invalid or is a wildcard relation
|
|
975
1127
|
*
|
|
976
1128
|
* @example
|
|
977
1129
|
* world.set(entity, Position, { x: 10, y: 20 });
|
|
978
1130
|
* world.set(entity, Marker); // void component
|
|
979
|
-
* world.set(
|
|
1131
|
+
* world.singleton(GlobalConfig).set({ debug: true }); // singleton component
|
|
980
1132
|
* world.sync(); // Apply changes
|
|
981
1133
|
*/
|
|
982
1134
|
set(entityId: EntityId, componentType: EntityId<void>): void;
|
|
983
1135
|
set<T>(entityId: EntityId, componentType: EntityId<T>, component: NoInfer<T>): void;
|
|
984
|
-
set<T>(componentId: ComponentId<T>, component: NoInfer<T>): void;
|
|
985
1136
|
/**
|
|
986
1137
|
* Removes a component from an entity.
|
|
987
1138
|
* The change is buffered and takes effect after calling `world.sync()`.
|
|
@@ -1019,6 +1170,18 @@ declare class World {
|
|
|
1019
1170
|
* world.sync(); // Apply changes
|
|
1020
1171
|
*/
|
|
1021
1172
|
delete(entityId: EntityId): void;
|
|
1173
|
+
/**
|
|
1174
|
+
* Returns an explicit handle for a singleton component (component-as-entity).
|
|
1175
|
+
*
|
|
1176
|
+
* This is the preferred API for singleton components.
|
|
1177
|
+
*
|
|
1178
|
+
* @example
|
|
1179
|
+
* const config = world.singleton(GlobalConfig);
|
|
1180
|
+
* config.set({ debug: true });
|
|
1181
|
+
* world.sync();
|
|
1182
|
+
* console.log(config.get());
|
|
1183
|
+
*/
|
|
1184
|
+
singleton<T>(componentId: ComponentId<T>): SingletonHandle<T>;
|
|
1022
1185
|
/**
|
|
1023
1186
|
* Checks if a specific **component** is present on an entity.
|
|
1024
1187
|
*
|
|
@@ -1112,6 +1275,106 @@ declare class World {
|
|
|
1112
1275
|
getOptional<T>(entityId: EntityId, componentType: EntityId<T>): {
|
|
1113
1276
|
value: T;
|
|
1114
1277
|
} | undefined;
|
|
1278
|
+
/**
|
|
1279
|
+
* Retrieves all targets (and their associated data) for relations of a given
|
|
1280
|
+
* base component on an entity.
|
|
1281
|
+
*
|
|
1282
|
+
* This is the ergonomic replacement for the common pattern:
|
|
1283
|
+
* world.get(entity, relation(Comp, "*"))
|
|
1284
|
+
*
|
|
1285
|
+
* @example
|
|
1286
|
+
* const ChildOf = component({ exclusive: true, sparse: true });
|
|
1287
|
+
* const children = world.getRelationTargets(parent, ChildOf); // usually []
|
|
1288
|
+
* const items = world.getRelationTargets(player, InInventory);
|
|
1289
|
+
*
|
|
1290
|
+
* // For common hierarchy use cases, prefer the higher-level helpers:
|
|
1291
|
+
* // world.getChildren(parent, ChildOf), world.getParent(child, ChildOf)
|
|
1292
|
+
*/
|
|
1293
|
+
getRelationTargets<T = void>(entityId: EntityId, relationComp: ComponentId<T>): [target: EntityId<unknown>, data: T | undefined][];
|
|
1294
|
+
/**
|
|
1295
|
+
* Returns every entity that currently holds a relation of the given base
|
|
1296
|
+
* component pointing at `targetId`.
|
|
1297
|
+
*
|
|
1298
|
+
* This is the efficient **reverse** lookup. For common hierarchy cases,
|
|
1299
|
+
* prefer the higher-level `world.getChildren(parent, ChildOf)` instead.
|
|
1300
|
+
*
|
|
1301
|
+
* @example
|
|
1302
|
+
* const ChildOf = component({ exclusive: true, sparse: true });
|
|
1303
|
+
* const directChildren = world.getRelationSources(ship, ChildOf);
|
|
1304
|
+
*/
|
|
1305
|
+
getRelationSources(targetId: EntityId, relationComp: ComponentId<any>): EntityId[];
|
|
1306
|
+
/**
|
|
1307
|
+
* Returns true if the entity has any (or a specific-target) relation of the
|
|
1308
|
+
* given base component.
|
|
1309
|
+
*/
|
|
1310
|
+
hasRelation(entityId: EntityId, relationComp: ComponentId<any>, targetId?: EntityId): boolean;
|
|
1311
|
+
/**
|
|
1312
|
+
* Returns the number of relations of the given base component held by the entity.
|
|
1313
|
+
*/
|
|
1314
|
+
countRelations(entityId: EntityId, relationComp: ComponentId<any>): number;
|
|
1315
|
+
/**
|
|
1316
|
+
* For an *exclusive* relation (e.g. ChildOf, Owner), returns the single
|
|
1317
|
+
* target entity (or undefined if none).
|
|
1318
|
+
*
|
|
1319
|
+
* When the component was declared `exclusive: true`, this is the preferred
|
|
1320
|
+
* accessor (clearer intent than array destructuring).
|
|
1321
|
+
*/
|
|
1322
|
+
getSingleRelationTarget<T = void>(entityId: EntityId, relationComp: ComponentId<T>): EntityId | undefined;
|
|
1323
|
+
/**
|
|
1324
|
+
* Returns the direct children of `parent` for the given relationship component
|
|
1325
|
+
* (typically a `ChildOf` or similar exclusive `sparse` relation).
|
|
1326
|
+
*
|
|
1327
|
+
* This is the recommended high-level API for hierarchy traversal.
|
|
1328
|
+
* It uses the internal reverse reference index for efficiency.
|
|
1329
|
+
*
|
|
1330
|
+
* @example
|
|
1331
|
+
* const ChildOf = component({ exclusive: true, sparse: true });
|
|
1332
|
+
* const kids = world.getChildren(ship, ChildOf);
|
|
1333
|
+
*/
|
|
1334
|
+
getChildren(parent: EntityId, childOf: ComponentId<any>): EntityId[];
|
|
1335
|
+
/**
|
|
1336
|
+
* Returns the parent of `child` for the given relationship component
|
|
1337
|
+
* (typically an exclusive `ChildOf` relation).
|
|
1338
|
+
*
|
|
1339
|
+
* @example
|
|
1340
|
+
* const ChildOf = component({ exclusive: true, sparse: true });
|
|
1341
|
+
* const parent = world.getParent(turret, ChildOf);
|
|
1342
|
+
*/
|
|
1343
|
+
getParent(child: EntityId, childOf: ComponentId<any>): EntityId | undefined;
|
|
1344
|
+
/**
|
|
1345
|
+
* Returns the ancestor chain from the immediate parent up to (but not
|
|
1346
|
+
* including) the root for the given relationship component.
|
|
1347
|
+
*
|
|
1348
|
+
* @example
|
|
1349
|
+
* const ChildOf = component({ exclusive: true, sparse: true });
|
|
1350
|
+
* const ancestors = world.getAncestors(muzzle, ChildOf); // [turret, ship]
|
|
1351
|
+
*/
|
|
1352
|
+
getAncestors(entity: EntityId, childOf: ComponentId<any>): EntityId[];
|
|
1353
|
+
/**
|
|
1354
|
+
* Iteratively traverses all descendants of `root` in DFS pre-order.
|
|
1355
|
+
* This is a generator and is safe for very deep hierarchies.
|
|
1356
|
+
*
|
|
1357
|
+
* @example
|
|
1358
|
+
* for (const { entity, depth, parent } of world.iterateDescendants(root, ChildOf)) {
|
|
1359
|
+
* console.log(depth, entity);
|
|
1360
|
+
* }
|
|
1361
|
+
*/
|
|
1362
|
+
iterateDescendants(root: EntityId, childOf: ComponentId<any>, opts?: {
|
|
1363
|
+
includeSelf?: boolean;
|
|
1364
|
+
maxDepth?: number;
|
|
1365
|
+
}): IterableIterator<{
|
|
1366
|
+
entity: EntityId;
|
|
1367
|
+
depth: number;
|
|
1368
|
+
parent: EntityId | null;
|
|
1369
|
+
}>;
|
|
1370
|
+
/**
|
|
1371
|
+
* Callback-based descendant traversal (hot path friendly).
|
|
1372
|
+
* Return `false` from the visitor to stop early.
|
|
1373
|
+
*/
|
|
1374
|
+
traverseDescendants(root: EntityId, childOf: ComponentId<any>, visitor: (entity: EntityId, depth: number, parent: EntityId | null) => void | boolean, opts?: {
|
|
1375
|
+
includeSelf?: boolean;
|
|
1376
|
+
maxDepth?: number;
|
|
1377
|
+
}): void;
|
|
1115
1378
|
/**
|
|
1116
1379
|
* Registers a lifecycle hook that responds to component changes.
|
|
1117
1380
|
* The hook callback is invoked when components matching the specified types are added, updated, or removed.
|
|
@@ -1153,6 +1416,19 @@ declare class World {
|
|
|
1153
1416
|
* );
|
|
1154
1417
|
*/
|
|
1155
1418
|
hook<const T extends readonly ComponentType<any>[]>(componentTypes: T, hook: LifecycleHook<T> | LifecycleCallback<T>, filter?: QueryFilter): () => void;
|
|
1419
|
+
/**
|
|
1420
|
+
* Creates a debug stats collector that will receive a `SyncDebugStats` payload
|
|
1421
|
+
* after every subsequent `sync()`.
|
|
1422
|
+
*
|
|
1423
|
+
* The returned object is a pure lifecycle handle. It does not store data.
|
|
1424
|
+
* Collection stops when you call `[Symbol.dispose]()` (or use a `using` declaration).
|
|
1425
|
+
*
|
|
1426
|
+
* All active collectors receive the exact same stats object for a given sync.
|
|
1427
|
+
* Exceptions thrown by callbacks are ignored.
|
|
1428
|
+
*
|
|
1429
|
+
* This is intended for development/debugging and leak detection.
|
|
1430
|
+
*/
|
|
1431
|
+
createDebugStatsCollector(callback: (stats: SyncDebugStats) => void): DebugStatsCollector;
|
|
1156
1432
|
/**
|
|
1157
1433
|
* Synchronizes all buffered commands (set/remove/delete) to the world.
|
|
1158
1434
|
* This method must be called after making changes via `set()`, `remove()`, or `delete()` for them to take effect.
|
|
@@ -1248,7 +1524,6 @@ declare class World {
|
|
|
1248
1524
|
* @internal
|
|
1249
1525
|
*/
|
|
1250
1526
|
getMatchingArchetypes(componentTypes: EntityId<any>[]): Archetype[];
|
|
1251
|
-
private getArchetypesWithComponents;
|
|
1252
1527
|
/**
|
|
1253
1528
|
* Queries entities with specific components.
|
|
1254
1529
|
* For simpler use cases, prefer using `createQuery()` with `forEach()` which is cached and more efficient.
|
|
@@ -1281,21 +1556,9 @@ declare class World {
|
|
|
1281
1556
|
entity: EntityId;
|
|
1282
1557
|
components: ComponentTuple<T>;
|
|
1283
1558
|
}>;
|
|
1284
|
-
private executeEntityCommands;
|
|
1285
|
-
private applyEntityCommands;
|
|
1286
|
-
private createHooksContext;
|
|
1287
|
-
private removeComponentImmediate;
|
|
1288
|
-
private updateEntityReferences;
|
|
1289
1559
|
private ensureArchetype;
|
|
1290
|
-
/** Add componentType to the reverse index if it contains an entity ID */
|
|
1291
|
-
private addToReferencingIndex;
|
|
1292
|
-
/** Remove componentType from the reverse index */
|
|
1293
|
-
private removeFromReferencingIndex;
|
|
1294
|
-
private createNewArchetype;
|
|
1295
|
-
private updateArchetypeHookMatches;
|
|
1296
|
-
private archetypeMatchesHook;
|
|
1297
1560
|
private cleanupArchetypesReferencingEntity;
|
|
1298
|
-
private
|
|
1561
|
+
private archetypeMatchesHook;
|
|
1299
1562
|
/**
|
|
1300
1563
|
* Serializes the entire world state to a plain JavaScript object.
|
|
1301
1564
|
* This creates a "memory snapshot" that can be stored or transmitted.
|
|
@@ -1317,6 +1580,8 @@ declare class World {
|
|
|
1317
1580
|
* const savedData = JSON.parse(localStorage.getItem('save'));
|
|
1318
1581
|
* const newWorld = new World(savedData);
|
|
1319
1582
|
*/
|
|
1583
|
+
private removeComponentImmediate;
|
|
1584
|
+
private createHooksContext;
|
|
1320
1585
|
serialize(): SerializedWorld;
|
|
1321
1586
|
}
|
|
1322
1587
|
//#endregion
|
|
@@ -1393,5 +1658,5 @@ declare class EntityBuilder {
|
|
|
1393
1658
|
build(): EntityId;
|
|
1394
1659
|
}
|
|
1395
1660
|
//#endregion
|
|
1396
|
-
export { EntityRelationId as C,
|
|
1661
|
+
export { EntityRelationId as A, isSparseWildcard as C, ComponentId as D, relation as E, isRelationId as F, WildcardRelationId as M, isComponentId as N, ComponentRelationId as O, isEntityId as P, isSparseRelation as S, isWildcardRelationId as T, ComponentOptions as _, SingletonHandleOps as a, getComponentNameById as b, ComponentType as c, LifecycleHook as d, SyncDebugStats as f, SerializedWorld as g, SerializedEntityId as h, SingletonHandle as i, RelationId as j, EntityId as k, DebugStatsCollector as l, SerializedEntity as m, EntityBuilder as n, Query as o, SerializedComponent as p, World as r, ComponentTuple as s, ComponentDef as t, LifecycleCallback as u, component as v, decodeRelationId as w, isSparseComponent as x, getComponentIdByName as y };
|
|
1397
1662
|
//# sourceMappingURL=builder.d.mts.map
|
package/dist/index.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export { type ComponentDef, type ComponentId, type ComponentOptions, type ComponentRelationId, type ComponentTuple, type ComponentType, EntityBuilder, type EntityId, type EntityRelationId, type LifecycleCallback, type LifecycleHook, Query, type RelationId, type SerializedComponent, type SerializedEntity, type SerializedEntityId, type SerializedWorld, type WildcardRelationId, World, component, decodeRelationId, getComponentIdByName, getComponentNameById, isComponentId, isEntityId, isRelationId, isWildcardRelationId, relation };
|
|
1
|
+
import { A as EntityRelationId, C as isSparseWildcard, D as ComponentId, E as relation, F as isRelationId, M as WildcardRelationId, N as isComponentId, O as ComponentRelationId, P as isEntityId, S as isSparseRelation, T as isWildcardRelationId, _ as ComponentOptions, a as SingletonHandleOps, b as getComponentNameById, c as ComponentType, d as LifecycleHook, f as SyncDebugStats, g as SerializedWorld, h as SerializedEntityId, i as SingletonHandle, j as RelationId, k as EntityId, l as DebugStatsCollector, m as SerializedEntity, n as EntityBuilder, o as Query, p as SerializedComponent, r as World, s as ComponentTuple, t as ComponentDef, u as LifecycleCallback, v as component, w as decodeRelationId, x as isSparseComponent, y as getComponentIdByName } from "./builder.mjs";
|
|
2
|
+
export { type ComponentDef, type ComponentId, type ComponentOptions, type ComponentRelationId, type ComponentTuple, type ComponentType, type DebugStatsCollector, EntityBuilder, type EntityId, type EntityRelationId, type LifecycleCallback, type LifecycleHook, Query, type RelationId, type SerializedComponent, type SerializedEntity, type SerializedEntityId, type SerializedWorld, SingletonHandle, type SingletonHandleOps, type SyncDebugStats, type WildcardRelationId, World, component, decodeRelationId, getComponentIdByName, getComponentNameById, isComponentId, isSparseComponent as isDontFragmentComponent, isSparseComponent, isSparseRelation as isDontFragmentRelation, isSparseRelation, isSparseWildcard as isDontFragmentWildcard, isSparseWildcard, isEntityId, isRelationId, isWildcardRelationId, relation };
|
package/dist/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as
|
|
2
|
-
export { EntityBuilder, Query, World, component, decodeRelationId, getComponentIdByName, getComponentNameById, isComponentId, isEntityId, isRelationId, isWildcardRelationId, relation };
|
|
1
|
+
import { a as component, c as isSparseComponent, d as decodeRelationId, f as isWildcardRelationId, g as isRelationId, h as isEntityId, i as EntityBuilder, l as isSparseRelation, m as isComponentId, n as Query, o as getComponentIdByName, p as relation, r as SingletonHandle, s as getComponentNameById, t as World, u as isSparseWildcard } from "./world.mjs";
|
|
2
|
+
export { EntityBuilder, Query, SingletonHandle, World, component, decodeRelationId, getComponentIdByName, getComponentNameById, isComponentId, isSparseComponent as isDontFragmentComponent, isSparseComponent, isSparseRelation as isDontFragmentRelation, isSparseRelation, isSparseWildcard as isDontFragmentWildcard, isSparseWildcard, isEntityId, isRelationId, isWildcardRelationId, relation };
|
package/dist/testing.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { D as ComponentId, E as relation, M as WildcardRelationId, d as LifecycleHook, j as RelationId, k as EntityId, n as EntityBuilder, o as Query, r as World, t as ComponentDef, u as LifecycleCallback, v as component } from "./builder.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/testing/index.d.ts
|
|
4
4
|
/**
|