@codehz/ecs 0.6.5 → 0.6.7

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/builder.d.mts CHANGED
@@ -405,6 +405,7 @@ type SerializedWorld = {
405
405
  version: number;
406
406
  entityManager: any;
407
407
  entities: SerializedEntity[];
408
+ componentEntities?: SerializedEntity[];
408
409
  };
409
410
  type SerializedEntity = {
410
411
  id: SerializedEntityId;
@@ -428,6 +429,8 @@ declare class World {
428
429
  private archetypesByComponent;
429
430
  private entityReferences;
430
431
  private dontFragmentRelations;
432
+ private componentEntityComponents;
433
+ private relationEntityIdsByTarget;
431
434
  private queries;
432
435
  private queryCache;
433
436
  private commandBuffer;
@@ -436,21 +439,188 @@ declare class World {
436
439
  constructor(snapshot?: SerializedWorld);
437
440
  private deserializeSnapshot;
438
441
  private createArchetypeSignature;
442
+ /**
443
+ * Creates a new entity.
444
+ * The entity is created with an empty component set and can be configured using `set()`.
445
+ *
446
+ * @template T - The initial component type (defaults to void if not specified)
447
+ * @returns A unique identifier for the new entity
448
+ *
449
+ * @example
450
+ * const entity = world.new<MyComponent>();
451
+ * world.set(entity, MyComponent, { value: 42 });
452
+ * world.sync();
453
+ */
439
454
  new<T = void>(): EntityId<T>;
455
+ private isComponentEntityId;
456
+ private registerRelationEntityId;
457
+ private unregisterRelationEntityId;
458
+ private getComponentEntityComponents;
459
+ private clearComponentEntityComponents;
460
+ private cleanupComponentEntitiesReferencingEntity;
440
461
  private destroyEntityImmediate;
462
+ /**
463
+ * Checks if an entity exists in the world.
464
+ *
465
+ * @param entityId - The entity identifier to check
466
+ * @returns `true` if the entity exists, `false` otherwise
467
+ *
468
+ * @example
469
+ * if (world.exists(entityId)) {
470
+ * console.log("Entity exists");
471
+ * }
472
+ */
441
473
  exists(entityId: EntityId): boolean;
474
+ /**
475
+ * Adds or updates a component on an entity (or marks void component as present).
476
+ * The change is buffered and takes effect after calling `world.sync()`.
477
+ * If the entity does not exist, throws an error.
478
+ *
479
+ * @overload set(entityId: EntityId, componentType: EntityId<void>): void
480
+ * Marks a void component as present on the entity
481
+ *
482
+ * @overload set<T>(entityId: EntityId, componentType: EntityId<T>, component: NoInfer<T>): void
483
+ * Adds or updates a component with data on the entity
484
+ *
485
+ * @throws {Error} If the entity does not exist
486
+ * @throws {Error} If the component type is invalid or is a wildcard relation
487
+ *
488
+ * @example
489
+ * world.set(entity, Position, { x: 10, y: 20 });
490
+ * world.set(entity, Marker); // void component
491
+ * world.sync(); // Apply changes
492
+ */
442
493
  set(entityId: EntityId, componentType: EntityId<void>): void;
443
494
  set<T>(entityId: EntityId, componentType: EntityId<T>, component: NoInfer<T>): void;
495
+ /**
496
+ * Removes a component from an entity.
497
+ * The change is buffered and takes effect after calling `world.sync()`.
498
+ * If the entity does not exist, throws an error.
499
+ *
500
+ * @template T - The component data type
501
+ * @param entityId - The entity identifier
502
+ * @param componentType - The component type to remove
503
+ *
504
+ * @throws {Error} If the entity does not exist
505
+ * @throws {Error} If the component type is invalid
506
+ *
507
+ * @example
508
+ * world.remove(entity, Position);
509
+ * world.sync(); // Apply changes
510
+ */
444
511
  remove<T>(entityId: EntityId, componentType: EntityId<T>): void;
512
+ /**
513
+ * Deletes an entity and all its components from the world.
514
+ * The change is buffered and takes effect after calling `world.sync()`.
515
+ * Related entities may trigger cascade delete hooks if configured.
516
+ *
517
+ * @param entityId - The entity identifier to delete
518
+ *
519
+ * @example
520
+ * world.delete(entity);
521
+ * world.sync(); // Apply changes
522
+ */
445
523
  delete(entityId: EntityId): void;
524
+ /**
525
+ * Checks if an entity has a specific component.
526
+ * Immediately reflects the current state without waiting for `sync()`.
527
+ *
528
+ * @template T - The component data type
529
+ * @param entityId - The entity identifier
530
+ * @param componentType - The component type to check
531
+ * @returns `true` if the entity has the component, `false` otherwise
532
+ *
533
+ * @example
534
+ * if (world.has(entity, Position)) {
535
+ * const pos = world.get(entity, Position);
536
+ * }
537
+ */
446
538
  has<T>(entityId: EntityId, componentType: EntityId<T>): boolean;
539
+ /**
540
+ * Retrieves a component from an entity.
541
+ * For wildcard relations, returns all relations of that type.
542
+ * Throws an error if the component does not exist; use `has()` to check first or use `getOptional()`.
543
+ *
544
+ * @overload get<T>(entityId: EntityId<T>): T
545
+ * When called with only an entity ID, retrieves the entity's primary component.
546
+ *
547
+ * @overload get<T>(entityId: EntityId, componentType: WildcardRelationId<T>): [EntityId<unknown>, T][]
548
+ * For wildcard relations, returns an array of [target entity, component value] pairs.
549
+ *
550
+ * @overload get<T>(entityId: EntityId, componentType: EntityId<T>): T
551
+ * Retrieves a specific component from the entity.
552
+ *
553
+ * @throws {Error} If the entity does not exist
554
+ * @throws {Error} If the component does not exist on the entity
555
+ *
556
+ * @example
557
+ * const position = world.get(entity, Position); // Throws if no Position
558
+ * const relations = world.get(entity, relation(Parent, "*")); // Wildcard relation
559
+ */
560
+ get<T>(entityId: EntityId<T>): T;
447
561
  get<T>(entityId: EntityId, componentType: WildcardRelationId<T>): [EntityId<unknown>, T][];
448
562
  get<T>(entityId: EntityId, componentType: EntityId<T>): T;
563
+ /**
564
+ * Safely retrieves a component from an entity without throwing an error.
565
+ * Returns `undefined` if the component does not exist.
566
+ * For wildcard relations, returns `undefined` if there are no relations.
567
+ *
568
+ * @template T - The component data type
569
+ * @overload getOptional<T>(entityId: EntityId<T>): { value: T } | undefined
570
+ * Retrieves the entity's primary component safely.
571
+ *
572
+ * @overload getOptional<T>(entityId: EntityId, componentType: EntityId<T>): { value: T } | undefined
573
+ * Retrieves a specific component safely.
574
+ *
575
+ * @throws {Error} If the entity does not exist
576
+ *
577
+ * @example
578
+ * const position = world.getOptional(entity, Position);
579
+ * if (position) {
580
+ * console.log(position.value.x);
581
+ * }
582
+ */
583
+ getOptional<T>(entityId: EntityId<T>): {
584
+ value: T;
585
+ } | undefined;
449
586
  getOptional<T>(entityId: EntityId, componentType: EntityId<T>): {
450
587
  value: T;
451
588
  } | undefined;
452
589
  /**
453
- * @deprecated use array overload with LifecycleCallback
590
+ * Registers a lifecycle hook that responds to component changes.
591
+ * The hook callback is invoked when components matching the specified types are added, updated, or removed.
592
+ *
593
+ * @deprecated For single components, use the array overload with LifecycleCallback for better multi-component support
594
+ *
595
+ * @overload hook<T>(componentType: EntityId<T>, hook: LegacyLifecycleHook<T> | LegacyLifecycleCallback<T>): () => void
596
+ * Registers a hook for a single component type (legacy API).
597
+ *
598
+ * @overload hook<const T extends readonly ComponentType<any>[]>(
599
+ * componentTypes: T,
600
+ * hook: LifecycleHook<T> | LifecycleCallback<T>,
601
+ * ): () => void
602
+ * Registers a hook for multiple component types.
603
+ * The hook is triggered when all required components change together.
604
+ *
605
+ * @param componentTypesOrSingle - A single component type or an array of component types
606
+ * @param hook - Either a hook object with on_init/on_set/on_remove handlers, or a callback function
607
+ * @returns A function that unsubscribes the hook when called
608
+ *
609
+ * @throws {Error} If no required components are specified in array overload
610
+ *
611
+ * @example
612
+ * // Array overload (recommended)
613
+ * const unsubscribe = world.hook([Position, Velocity], {
614
+ * on_init: (entityId, position, velocity) => console.log("Initialized"),
615
+ * on_set: (entityId, position, velocity) => console.log("Updated"),
616
+ * on_remove: (entityId, position, velocity) => console.log("Removed"),
617
+ * });
618
+ * unsubscribe(); // Remove hook
619
+ *
620
+ * // Callback style
621
+ * const unsubscribe = world.hook([Position], (event, entityId, position) => {
622
+ * if (event === "init") console.log("Initialized");
623
+ * });
454
624
  */
455
625
  hook<T>(componentType: EntityId<T>, hook: LegacyLifecycleHook<T> | LegacyLifecycleCallback<T>): () => void;
456
626
  hook<const T extends readonly ComponentType<any>[]>(componentTypes: T, hook: LifecycleHook<T> | LifecycleCallback<T>): () => void;
@@ -458,21 +628,135 @@ declare class World {
458
628
  unhook<T>(componentType: EntityId<T>, hook: LegacyLifecycleHook<T>): void;
459
629
  /** @deprecated use the unsubscribe function returned by hook() instead */
460
630
  unhook<const T extends readonly ComponentType<any>[]>(componentTypes: T, hook: LifecycleHook<T>): void;
631
+ /**
632
+ * Synchronizes all buffered commands (set/remove/delete) to the world.
633
+ * This method must be called after making changes via `set()`, `remove()`, or `delete()` for them to take effect.
634
+ * Typically called once per frame at the end of your game loop.
635
+ *
636
+ * @example
637
+ * world.set(entity, Position, { x: 10, y: 20 });
638
+ * world.remove(entity, OldComponent);
639
+ * world.sync(); // Apply all buffered changes
640
+ */
461
641
  sync(): void;
642
+ /**
643
+ * Creates a cached query for efficiently iterating entities with specific components.
644
+ * The query is cached internally and reused across calls with the same component types and filter.
645
+ *
646
+ * **Important:** Store the query reference and reuse it across frames for optimal performance.
647
+ * Creating a new query each frame defeats the caching mechanism.
648
+ *
649
+ * @param componentTypes - Array of component types to match
650
+ * @param filter - Optional filter for additional constraints (e.g., without specific components)
651
+ * @returns A Query instance that can be used to iterate matching entities
652
+ *
653
+ * @example
654
+ * // Create once, reuse many times
655
+ * const movementQuery = world.createQuery([Position, Velocity]);
656
+ *
657
+ * // In game loop
658
+ * movementQuery.forEach((entity) => {
659
+ * const pos = world.get(entity, Position);
660
+ * const vel = world.get(entity, Velocity);
661
+ * pos.x += vel.x;
662
+ * pos.y += vel.y;
663
+ * });
664
+ *
665
+ * // With filter
666
+ * const activeQuery = world.createQuery([Position], {
667
+ * without: [Disabled]
668
+ * });
669
+ */
462
670
  createQuery(componentTypes: EntityId<any>[], filter?: QueryFilter): Query;
671
+ /**
672
+ * Creates a new entity builder for fluent entity configuration.
673
+ * Useful for building entities with multiple components in a single expression.
674
+ *
675
+ * @returns An EntityBuilder instance
676
+ *
677
+ * @example
678
+ * const entity = world.spawn()
679
+ * .with(Position, { x: 0, y: 0 })
680
+ * .with(Velocity, { x: 1, y: 1 })
681
+ * .build();
682
+ * world.sync(); // Apply changes
683
+ */
463
684
  spawn(): EntityBuilder;
685
+ /**
686
+ * Spawns multiple entities with a configuration callback.
687
+ * More efficient than calling `spawn()` multiple times when creating many entities.
688
+ *
689
+ * @param count - Number of entities to spawn
690
+ * @param configure - Callback that receives an EntityBuilder and index; must return the configured builder
691
+ * @returns Array of created entity IDs
692
+ *
693
+ * @example
694
+ * const entities = world.spawnMany(100, (builder, index) => {
695
+ * return builder
696
+ * .with(Position, { x: index * 10, y: 0 })
697
+ * .with(Velocity, { x: 0, y: 1 });
698
+ * });
699
+ * world.sync();
700
+ */
464
701
  spawnMany(count: number, configure: (builder: EntityBuilder, index: number) => EntityBuilder): EntityId[];
465
702
  _registerQuery(query: Query): void;
466
703
  _unregisterQuery(query: Query): void;
704
+ /**
705
+ * Releases a cached query and frees its resources if no longer needed.
706
+ * Call this when you're done using a query to allow the world to clean up its cache entry.
707
+ *
708
+ * @param query - The query to release
709
+ *
710
+ * @example
711
+ * const query = world.createQuery([Position]);
712
+ * // ... use query ...
713
+ * world.releaseQuery(query); // Optional cleanup
714
+ */
467
715
  releaseQuery(query: Query): void;
716
+ /**
717
+ * Returns all archetypes that contain entities with the specified components.
718
+ * Used internally for query optimization but can be useful for debugging.
719
+ *
720
+ * @param componentTypes - Array of component types to match
721
+ * @returns Array of Archetype objects containing matching components
722
+ * @internal
723
+ */
468
724
  getMatchingArchetypes(componentTypes: EntityId<any>[]): Archetype[];
469
725
  private getArchetypesWithComponents;
726
+ /**
727
+ * Queries entities with specific components.
728
+ * For simpler use cases, prefer using `createQuery()` with `forEach()` which is cached and more efficient.
729
+ *
730
+ * @overload query(componentTypes: EntityId<any>[]): EntityId[]
731
+ * Returns an array of entity IDs that have all specified components.
732
+ *
733
+ * @overload query<const T extends readonly EntityId<any>[]>(
734
+ * componentTypes: T,
735
+ * includeComponents: true,
736
+ * ): Array<{ entity: EntityId; components: ComponentTuple<T> }>
737
+ * Returns entities along with their component data.
738
+ *
739
+ * @param componentTypes - Array of component types to query
740
+ * @param includeComponents - If true, includes component data in results
741
+ * @returns Array of entity IDs or objects with entities and components
742
+ *
743
+ * @example
744
+ * // Just entity IDs
745
+ * const entities = world.query([Position, Velocity]);
746
+ *
747
+ * // With components
748
+ * const results = world.query([Position, Velocity], true);
749
+ * results.forEach(({ entity, components: [pos, vel] }) => {
750
+ * pos.x += vel.x;
751
+ * });
752
+ */
470
753
  query(componentTypes: EntityId<any>[]): EntityId[];
471
754
  query<const T extends readonly EntityId<any>[]>(componentTypes: T, includeComponents: true): Array<{
472
755
  entity: EntityId;
473
756
  components: ComponentTuple<T>;
474
757
  }>;
475
758
  executeEntityCommands(entityId: EntityId, commands: Command[]): ComponentChangeset;
759
+ private executeComponentEntityCommands;
476
760
  private createHooksContext;
477
761
  private removeComponentImmediate;
478
762
  private updateEntityReferences;
@@ -483,6 +767,27 @@ declare class World {
483
767
  private archetypeReferencesEntity;
484
768
  private cleanupArchetypesReferencingEntity;
485
769
  private removeArchetype;
770
+ /**
771
+ * Serializes the entire world state to a plain JavaScript object.
772
+ * This creates a "memory snapshot" that can be stored or transmitted.
773
+ * The snapshot can be restored using `new World(snapshot)`.
774
+ *
775
+ * **Note:** This is NOT automatically persistent storage. To persist data,
776
+ * you must serialize the returned object to JSON or another format yourself.
777
+ *
778
+ * @returns A serializable object representing the world state
779
+ *
780
+ * @example
781
+ * // Create snapshot
782
+ * const snapshot = world.serialize();
783
+ *
784
+ * // Save to storage (example)
785
+ * localStorage.setItem('save', JSON.stringify(snapshot));
786
+ *
787
+ * // Later, restore from snapshot
788
+ * const savedData = JSON.parse(localStorage.getItem('save'));
789
+ * const newWorld = new World(savedData);
790
+ */
486
791
  serialize(): SerializedWorld;
487
792
  }
488
793
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codehz/ecs",
3
- "version": "0.6.5",
3
+ "version": "0.6.7",
4
4
  "repository": {
5
5
  "url": "https://github.com/codehz/ecs"
6
6
  },