@codehz/ecs 0.6.4 → 0.6.6
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 +296 -1
- package/package.json +1 -1
- package/world.mjs +223 -6
- package/world.mjs.map +1 -1
package/builder.d.mts
CHANGED
|
@@ -436,21 +436,182 @@ declare class World {
|
|
|
436
436
|
constructor(snapshot?: SerializedWorld);
|
|
437
437
|
private deserializeSnapshot;
|
|
438
438
|
private createArchetypeSignature;
|
|
439
|
+
/**
|
|
440
|
+
* Creates a new entity.
|
|
441
|
+
* The entity is created with an empty component set and can be configured using `set()`.
|
|
442
|
+
*
|
|
443
|
+
* @template T - The initial component type (defaults to void if not specified)
|
|
444
|
+
* @returns A unique identifier for the new entity
|
|
445
|
+
*
|
|
446
|
+
* @example
|
|
447
|
+
* const entity = world.new<MyComponent>();
|
|
448
|
+
* world.set(entity, MyComponent, { value: 42 });
|
|
449
|
+
* world.sync();
|
|
450
|
+
*/
|
|
439
451
|
new<T = void>(): EntityId<T>;
|
|
440
452
|
private destroyEntityImmediate;
|
|
453
|
+
/**
|
|
454
|
+
* Checks if an entity exists in the world.
|
|
455
|
+
*
|
|
456
|
+
* @param entityId - The entity identifier to check
|
|
457
|
+
* @returns `true` if the entity exists, `false` otherwise
|
|
458
|
+
*
|
|
459
|
+
* @example
|
|
460
|
+
* if (world.exists(entityId)) {
|
|
461
|
+
* console.log("Entity exists");
|
|
462
|
+
* }
|
|
463
|
+
*/
|
|
441
464
|
exists(entityId: EntityId): boolean;
|
|
465
|
+
/**
|
|
466
|
+
* Adds or updates a component on an entity (or marks void component as present).
|
|
467
|
+
* The change is buffered and takes effect after calling `world.sync()`.
|
|
468
|
+
* If the entity does not exist, throws an error.
|
|
469
|
+
*
|
|
470
|
+
* @overload set(entityId: EntityId, componentType: EntityId<void>): void
|
|
471
|
+
* Marks a void component as present on the entity
|
|
472
|
+
*
|
|
473
|
+
* @overload set<T>(entityId: EntityId, componentType: EntityId<T>, component: NoInfer<T>): void
|
|
474
|
+
* Adds or updates a component with data on the entity
|
|
475
|
+
*
|
|
476
|
+
* @throws {Error} If the entity does not exist
|
|
477
|
+
* @throws {Error} If the component type is invalid or is a wildcard relation
|
|
478
|
+
*
|
|
479
|
+
* @example
|
|
480
|
+
* world.set(entity, Position, { x: 10, y: 20 });
|
|
481
|
+
* world.set(entity, Marker); // void component
|
|
482
|
+
* world.sync(); // Apply changes
|
|
483
|
+
*/
|
|
442
484
|
set(entityId: EntityId, componentType: EntityId<void>): void;
|
|
443
485
|
set<T>(entityId: EntityId, componentType: EntityId<T>, component: NoInfer<T>): void;
|
|
486
|
+
/**
|
|
487
|
+
* Removes a component from an entity.
|
|
488
|
+
* The change is buffered and takes effect after calling `world.sync()`.
|
|
489
|
+
* If the entity does not exist, throws an error.
|
|
490
|
+
*
|
|
491
|
+
* @template T - The component data type
|
|
492
|
+
* @param entityId - The entity identifier
|
|
493
|
+
* @param componentType - The component type to remove
|
|
494
|
+
*
|
|
495
|
+
* @throws {Error} If the entity does not exist
|
|
496
|
+
* @throws {Error} If the component type is invalid
|
|
497
|
+
*
|
|
498
|
+
* @example
|
|
499
|
+
* world.remove(entity, Position);
|
|
500
|
+
* world.sync(); // Apply changes
|
|
501
|
+
*/
|
|
444
502
|
remove<T>(entityId: EntityId, componentType: EntityId<T>): void;
|
|
503
|
+
/**
|
|
504
|
+
* Deletes an entity and all its components from the world.
|
|
505
|
+
* The change is buffered and takes effect after calling `world.sync()`.
|
|
506
|
+
* Related entities may trigger cascade delete hooks if configured.
|
|
507
|
+
*
|
|
508
|
+
* @param entityId - The entity identifier to delete
|
|
509
|
+
*
|
|
510
|
+
* @example
|
|
511
|
+
* world.delete(entity);
|
|
512
|
+
* world.sync(); // Apply changes
|
|
513
|
+
*/
|
|
445
514
|
delete(entityId: EntityId): void;
|
|
515
|
+
/**
|
|
516
|
+
* Checks if an entity has a specific component.
|
|
517
|
+
* Immediately reflects the current state without waiting for `sync()`.
|
|
518
|
+
*
|
|
519
|
+
* @template T - The component data type
|
|
520
|
+
* @param entityId - The entity identifier
|
|
521
|
+
* @param componentType - The component type to check
|
|
522
|
+
* @returns `true` if the entity has the component, `false` otherwise
|
|
523
|
+
*
|
|
524
|
+
* @example
|
|
525
|
+
* if (world.has(entity, Position)) {
|
|
526
|
+
* const pos = world.get(entity, Position);
|
|
527
|
+
* }
|
|
528
|
+
*/
|
|
446
529
|
has<T>(entityId: EntityId, componentType: EntityId<T>): boolean;
|
|
530
|
+
/**
|
|
531
|
+
* Retrieves a component from an entity.
|
|
532
|
+
* For wildcard relations, returns all relations of that type.
|
|
533
|
+
* Throws an error if the component does not exist; use `has()` to check first or use `getOptional()`.
|
|
534
|
+
*
|
|
535
|
+
* @overload get<T>(entityId: EntityId<T>): T
|
|
536
|
+
* When called with only an entity ID, retrieves the entity's primary component.
|
|
537
|
+
*
|
|
538
|
+
* @overload get<T>(entityId: EntityId, componentType: WildcardRelationId<T>): [EntityId<unknown>, T][]
|
|
539
|
+
* For wildcard relations, returns an array of [target entity, component value] pairs.
|
|
540
|
+
*
|
|
541
|
+
* @overload get<T>(entityId: EntityId, componentType: EntityId<T>): T
|
|
542
|
+
* Retrieves a specific component from the entity.
|
|
543
|
+
*
|
|
544
|
+
* @throws {Error} If the entity does not exist
|
|
545
|
+
* @throws {Error} If the component does not exist on the entity
|
|
546
|
+
*
|
|
547
|
+
* @example
|
|
548
|
+
* const position = world.get(entity, Position); // Throws if no Position
|
|
549
|
+
* const relations = world.get(entity, relation(Parent, "*")); // Wildcard relation
|
|
550
|
+
*/
|
|
551
|
+
get<T>(entityId: EntityId<T>): T;
|
|
447
552
|
get<T>(entityId: EntityId, componentType: WildcardRelationId<T>): [EntityId<unknown>, T][];
|
|
448
553
|
get<T>(entityId: EntityId, componentType: EntityId<T>): T;
|
|
554
|
+
/**
|
|
555
|
+
* Safely retrieves a component from an entity without throwing an error.
|
|
556
|
+
* Returns `undefined` if the component does not exist.
|
|
557
|
+
* For wildcard relations, returns `undefined` if there are no relations.
|
|
558
|
+
*
|
|
559
|
+
* @template T - The component data type
|
|
560
|
+
* @overload getOptional<T>(entityId: EntityId<T>): { value: T } | undefined
|
|
561
|
+
* Retrieves the entity's primary component safely.
|
|
562
|
+
*
|
|
563
|
+
* @overload getOptional<T>(entityId: EntityId, componentType: EntityId<T>): { value: T } | undefined
|
|
564
|
+
* Retrieves a specific component safely.
|
|
565
|
+
*
|
|
566
|
+
* @throws {Error} If the entity does not exist
|
|
567
|
+
*
|
|
568
|
+
* @example
|
|
569
|
+
* const position = world.getOptional(entity, Position);
|
|
570
|
+
* if (position) {
|
|
571
|
+
* console.log(position.value.x);
|
|
572
|
+
* }
|
|
573
|
+
*/
|
|
574
|
+
getOptional<T>(entityId: EntityId<T>): {
|
|
575
|
+
value: T;
|
|
576
|
+
} | undefined;
|
|
449
577
|
getOptional<T>(entityId: EntityId, componentType: EntityId<T>): {
|
|
450
578
|
value: T;
|
|
451
579
|
} | undefined;
|
|
452
580
|
/**
|
|
453
|
-
*
|
|
581
|
+
* Registers a lifecycle hook that responds to component changes.
|
|
582
|
+
* The hook callback is invoked when components matching the specified types are added, updated, or removed.
|
|
583
|
+
*
|
|
584
|
+
* @deprecated For single components, use the array overload with LifecycleCallback for better multi-component support
|
|
585
|
+
*
|
|
586
|
+
* @overload hook<T>(componentType: EntityId<T>, hook: LegacyLifecycleHook<T> | LegacyLifecycleCallback<T>): () => void
|
|
587
|
+
* Registers a hook for a single component type (legacy API).
|
|
588
|
+
*
|
|
589
|
+
* @overload hook<const T extends readonly ComponentType<any>[]>(
|
|
590
|
+
* componentTypes: T,
|
|
591
|
+
* hook: LifecycleHook<T> | LifecycleCallback<T>,
|
|
592
|
+
* ): () => void
|
|
593
|
+
* Registers a hook for multiple component types.
|
|
594
|
+
* The hook is triggered when all required components change together.
|
|
595
|
+
*
|
|
596
|
+
* @param componentTypesOrSingle - A single component type or an array of component types
|
|
597
|
+
* @param hook - Either a hook object with on_init/on_set/on_remove handlers, or a callback function
|
|
598
|
+
* @returns A function that unsubscribes the hook when called
|
|
599
|
+
*
|
|
600
|
+
* @throws {Error} If no required components are specified in array overload
|
|
601
|
+
*
|
|
602
|
+
* @example
|
|
603
|
+
* // Array overload (recommended)
|
|
604
|
+
* const unsubscribe = world.hook([Position, Velocity], {
|
|
605
|
+
* on_init: (entityId, position, velocity) => console.log("Initialized"),
|
|
606
|
+
* on_set: (entityId, position, velocity) => console.log("Updated"),
|
|
607
|
+
* on_remove: (entityId, position, velocity) => console.log("Removed"),
|
|
608
|
+
* });
|
|
609
|
+
* unsubscribe(); // Remove hook
|
|
610
|
+
*
|
|
611
|
+
* // Callback style
|
|
612
|
+
* const unsubscribe = world.hook([Position], (event, entityId, position) => {
|
|
613
|
+
* if (event === "init") console.log("Initialized");
|
|
614
|
+
* });
|
|
454
615
|
*/
|
|
455
616
|
hook<T>(componentType: EntityId<T>, hook: LegacyLifecycleHook<T> | LegacyLifecycleCallback<T>): () => void;
|
|
456
617
|
hook<const T extends readonly ComponentType<any>[]>(componentTypes: T, hook: LifecycleHook<T> | LifecycleCallback<T>): () => void;
|
|
@@ -458,15 +619,128 @@ declare class World {
|
|
|
458
619
|
unhook<T>(componentType: EntityId<T>, hook: LegacyLifecycleHook<T>): void;
|
|
459
620
|
/** @deprecated use the unsubscribe function returned by hook() instead */
|
|
460
621
|
unhook<const T extends readonly ComponentType<any>[]>(componentTypes: T, hook: LifecycleHook<T>): void;
|
|
622
|
+
/**
|
|
623
|
+
* Synchronizes all buffered commands (set/remove/delete) to the world.
|
|
624
|
+
* This method must be called after making changes via `set()`, `remove()`, or `delete()` for them to take effect.
|
|
625
|
+
* Typically called once per frame at the end of your game loop.
|
|
626
|
+
*
|
|
627
|
+
* @example
|
|
628
|
+
* world.set(entity, Position, { x: 10, y: 20 });
|
|
629
|
+
* world.remove(entity, OldComponent);
|
|
630
|
+
* world.sync(); // Apply all buffered changes
|
|
631
|
+
*/
|
|
461
632
|
sync(): void;
|
|
633
|
+
/**
|
|
634
|
+
* Creates a cached query for efficiently iterating entities with specific components.
|
|
635
|
+
* The query is cached internally and reused across calls with the same component types and filter.
|
|
636
|
+
*
|
|
637
|
+
* **Important:** Store the query reference and reuse it across frames for optimal performance.
|
|
638
|
+
* Creating a new query each frame defeats the caching mechanism.
|
|
639
|
+
*
|
|
640
|
+
* @param componentTypes - Array of component types to match
|
|
641
|
+
* @param filter - Optional filter for additional constraints (e.g., without specific components)
|
|
642
|
+
* @returns A Query instance that can be used to iterate matching entities
|
|
643
|
+
*
|
|
644
|
+
* @example
|
|
645
|
+
* // Create once, reuse many times
|
|
646
|
+
* const movementQuery = world.createQuery([Position, Velocity]);
|
|
647
|
+
*
|
|
648
|
+
* // In game loop
|
|
649
|
+
* movementQuery.forEach((entity) => {
|
|
650
|
+
* const pos = world.get(entity, Position);
|
|
651
|
+
* const vel = world.get(entity, Velocity);
|
|
652
|
+
* pos.x += vel.x;
|
|
653
|
+
* pos.y += vel.y;
|
|
654
|
+
* });
|
|
655
|
+
*
|
|
656
|
+
* // With filter
|
|
657
|
+
* const activeQuery = world.createQuery([Position], {
|
|
658
|
+
* without: [Disabled]
|
|
659
|
+
* });
|
|
660
|
+
*/
|
|
462
661
|
createQuery(componentTypes: EntityId<any>[], filter?: QueryFilter): Query;
|
|
662
|
+
/**
|
|
663
|
+
* Creates a new entity builder for fluent entity configuration.
|
|
664
|
+
* Useful for building entities with multiple components in a single expression.
|
|
665
|
+
*
|
|
666
|
+
* @returns An EntityBuilder instance
|
|
667
|
+
*
|
|
668
|
+
* @example
|
|
669
|
+
* const entity = world.spawn()
|
|
670
|
+
* .with(Position, { x: 0, y: 0 })
|
|
671
|
+
* .with(Velocity, { x: 1, y: 1 })
|
|
672
|
+
* .build();
|
|
673
|
+
* world.sync(); // Apply changes
|
|
674
|
+
*/
|
|
463
675
|
spawn(): EntityBuilder;
|
|
676
|
+
/**
|
|
677
|
+
* Spawns multiple entities with a configuration callback.
|
|
678
|
+
* More efficient than calling `spawn()` multiple times when creating many entities.
|
|
679
|
+
*
|
|
680
|
+
* @param count - Number of entities to spawn
|
|
681
|
+
* @param configure - Callback that receives an EntityBuilder and index; must return the configured builder
|
|
682
|
+
* @returns Array of created entity IDs
|
|
683
|
+
*
|
|
684
|
+
* @example
|
|
685
|
+
* const entities = world.spawnMany(100, (builder, index) => {
|
|
686
|
+
* return builder
|
|
687
|
+
* .with(Position, { x: index * 10, y: 0 })
|
|
688
|
+
* .with(Velocity, { x: 0, y: 1 });
|
|
689
|
+
* });
|
|
690
|
+
* world.sync();
|
|
691
|
+
*/
|
|
464
692
|
spawnMany(count: number, configure: (builder: EntityBuilder, index: number) => EntityBuilder): EntityId[];
|
|
465
693
|
_registerQuery(query: Query): void;
|
|
466
694
|
_unregisterQuery(query: Query): void;
|
|
695
|
+
/**
|
|
696
|
+
* Releases a cached query and frees its resources if no longer needed.
|
|
697
|
+
* Call this when you're done using a query to allow the world to clean up its cache entry.
|
|
698
|
+
*
|
|
699
|
+
* @param query - The query to release
|
|
700
|
+
*
|
|
701
|
+
* @example
|
|
702
|
+
* const query = world.createQuery([Position]);
|
|
703
|
+
* // ... use query ...
|
|
704
|
+
* world.releaseQuery(query); // Optional cleanup
|
|
705
|
+
*/
|
|
467
706
|
releaseQuery(query: Query): void;
|
|
707
|
+
/**
|
|
708
|
+
* Returns all archetypes that contain entities with the specified components.
|
|
709
|
+
* Used internally for query optimization but can be useful for debugging.
|
|
710
|
+
*
|
|
711
|
+
* @param componentTypes - Array of component types to match
|
|
712
|
+
* @returns Array of Archetype objects containing matching components
|
|
713
|
+
* @internal
|
|
714
|
+
*/
|
|
468
715
|
getMatchingArchetypes(componentTypes: EntityId<any>[]): Archetype[];
|
|
469
716
|
private getArchetypesWithComponents;
|
|
717
|
+
/**
|
|
718
|
+
* Queries entities with specific components.
|
|
719
|
+
* For simpler use cases, prefer using `createQuery()` with `forEach()` which is cached and more efficient.
|
|
720
|
+
*
|
|
721
|
+
* @overload query(componentTypes: EntityId<any>[]): EntityId[]
|
|
722
|
+
* Returns an array of entity IDs that have all specified components.
|
|
723
|
+
*
|
|
724
|
+
* @overload query<const T extends readonly EntityId<any>[]>(
|
|
725
|
+
* componentTypes: T,
|
|
726
|
+
* includeComponents: true,
|
|
727
|
+
* ): Array<{ entity: EntityId; components: ComponentTuple<T> }>
|
|
728
|
+
* Returns entities along with their component data.
|
|
729
|
+
*
|
|
730
|
+
* @param componentTypes - Array of component types to query
|
|
731
|
+
* @param includeComponents - If true, includes component data in results
|
|
732
|
+
* @returns Array of entity IDs or objects with entities and components
|
|
733
|
+
*
|
|
734
|
+
* @example
|
|
735
|
+
* // Just entity IDs
|
|
736
|
+
* const entities = world.query([Position, Velocity]);
|
|
737
|
+
*
|
|
738
|
+
* // With components
|
|
739
|
+
* const results = world.query([Position, Velocity], true);
|
|
740
|
+
* results.forEach(({ entity, components: [pos, vel] }) => {
|
|
741
|
+
* pos.x += vel.x;
|
|
742
|
+
* });
|
|
743
|
+
*/
|
|
470
744
|
query(componentTypes: EntityId<any>[]): EntityId[];
|
|
471
745
|
query<const T extends readonly EntityId<any>[]>(componentTypes: T, includeComponents: true): Array<{
|
|
472
746
|
entity: EntityId;
|
|
@@ -483,6 +757,27 @@ declare class World {
|
|
|
483
757
|
private archetypeReferencesEntity;
|
|
484
758
|
private cleanupArchetypesReferencingEntity;
|
|
485
759
|
private removeArchetype;
|
|
760
|
+
/**
|
|
761
|
+
* Serializes the entire world state to a plain JavaScript object.
|
|
762
|
+
* This creates a "memory snapshot" that can be stored or transmitted.
|
|
763
|
+
* The snapshot can be restored using `new World(snapshot)`.
|
|
764
|
+
*
|
|
765
|
+
* **Note:** This is NOT automatically persistent storage. To persist data,
|
|
766
|
+
* you must serialize the returned object to JSON or another format yourself.
|
|
767
|
+
*
|
|
768
|
+
* @returns A serializable object representing the world state
|
|
769
|
+
*
|
|
770
|
+
* @example
|
|
771
|
+
* // Create snapshot
|
|
772
|
+
* const snapshot = world.serialize();
|
|
773
|
+
*
|
|
774
|
+
* // Save to storage (example)
|
|
775
|
+
* localStorage.setItem('save', JSON.stringify(snapshot));
|
|
776
|
+
*
|
|
777
|
+
* // Later, restore from snapshot
|
|
778
|
+
* const savedData = JSON.parse(localStorage.getItem('save'));
|
|
779
|
+
* const newWorld = new World(savedData);
|
|
780
|
+
*/
|
|
486
781
|
serialize(): SerializedWorld;
|
|
487
782
|
}
|
|
488
783
|
//#endregion
|
package/package.json
CHANGED
package/world.mjs
CHANGED
|
@@ -1534,6 +1534,23 @@ function triggerLifecycleHooks(ctx, entityId, addedComponents, removedComponents
|
|
|
1534
1534
|
invokeHooksForComponents(ctx.hooks, entityId, removedComponents, "on_remove");
|
|
1535
1535
|
triggerMultiComponentHooks(ctx, entityId, addedComponents, removedComponents, oldArchetype, newArchetype);
|
|
1536
1536
|
}
|
|
1537
|
+
/**
|
|
1538
|
+
* Fast path for triggering lifecycle hooks when an entity is being deleted.
|
|
1539
|
+
* This avoids unnecessary archetype lookups and on_set checks since the entity
|
|
1540
|
+
* is being completely removed.
|
|
1541
|
+
*/
|
|
1542
|
+
function triggerRemoveHooksForEntityDeletion(ctx, entityId, removedComponents, oldArchetype) {
|
|
1543
|
+
if (removedComponents.size === 0) return;
|
|
1544
|
+
invokeHooksForComponents(ctx.hooks, entityId, removedComponents, "on_remove");
|
|
1545
|
+
for (const entry of oldArchetype.matchingMultiHooks) {
|
|
1546
|
+
const { hook, requiredComponents, componentTypes } = entry;
|
|
1547
|
+
if (!hook.on_remove) continue;
|
|
1548
|
+
if (!requiredComponents.some((c) => anyComponentMatches(removedComponents, c))) continue;
|
|
1549
|
+
if (!requiredComponents.every((c) => anyComponentMatches(removedComponents, c))) continue;
|
|
1550
|
+
const components = collectComponentsFromRemoved(componentTypes, removedComponents);
|
|
1551
|
+
hook.on_remove(entityId, ...components);
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1537
1554
|
function invokeHooksForComponents(hooks, entityId, components, hookType) {
|
|
1538
1555
|
for (const [componentType, component$1] of components) {
|
|
1539
1556
|
const directHooks = hooks.get(componentType);
|
|
@@ -1616,6 +1633,38 @@ function collectMultiHookComponentsWithRemoved(ctx, entityId, componentTypes, re
|
|
|
1616
1633
|
return match ? match[1] : ctx.get(entityId, compId);
|
|
1617
1634
|
});
|
|
1618
1635
|
}
|
|
1636
|
+
/**
|
|
1637
|
+
* Collect component values directly from removedComponents map.
|
|
1638
|
+
* Used for entity deletion fast path where the entity no longer exists.
|
|
1639
|
+
*/
|
|
1640
|
+
function collectComponentsFromRemoved(componentTypes, removedComponents) {
|
|
1641
|
+
return componentTypes.map((ct) => {
|
|
1642
|
+
if (isOptionalEntityId(ct)) {
|
|
1643
|
+
const optionalId = ct.optional;
|
|
1644
|
+
if (isWildcardRelationId(optionalId)) {
|
|
1645
|
+
const result = collectWildcardFromRemoved(optionalId, removedComponents);
|
|
1646
|
+
return result.length > 0 ? { value: result } : void 0;
|
|
1647
|
+
}
|
|
1648
|
+
const match$1 = findMatchingComponent(removedComponents, optionalId);
|
|
1649
|
+
return match$1 ? { value: match$1[1] } : void 0;
|
|
1650
|
+
}
|
|
1651
|
+
const compId = ct;
|
|
1652
|
+
if (isWildcardRelationId(compId)) return collectWildcardFromRemoved(compId, removedComponents);
|
|
1653
|
+
const match = findMatchingComponent(removedComponents, compId);
|
|
1654
|
+
return match ? match[1] : void 0;
|
|
1655
|
+
});
|
|
1656
|
+
}
|
|
1657
|
+
/**
|
|
1658
|
+
* Collect all matching wildcard relation data from removed components.
|
|
1659
|
+
*/
|
|
1660
|
+
function collectWildcardFromRemoved(wildcardId, removedComponents) {
|
|
1661
|
+
const result = [];
|
|
1662
|
+
for (const [removedCompId, removedValue] of removedComponents.entries()) if (componentMatchesHookType(removedCompId, wildcardId)) {
|
|
1663
|
+
const targetId = getTargetIdFromRelationId(removedCompId);
|
|
1664
|
+
if (targetId !== void 0) result.push([targetId, removedValue]);
|
|
1665
|
+
}
|
|
1666
|
+
return result;
|
|
1667
|
+
}
|
|
1619
1668
|
|
|
1620
1669
|
//#endregion
|
|
1621
1670
|
//#region src/utils/multi-map.ts
|
|
@@ -1750,6 +1799,18 @@ var World = class {
|
|
|
1750
1799
|
createArchetypeSignature(componentTypes) {
|
|
1751
1800
|
return componentTypes.join(",");
|
|
1752
1801
|
}
|
|
1802
|
+
/**
|
|
1803
|
+
* Creates a new entity.
|
|
1804
|
+
* The entity is created with an empty component set and can be configured using `set()`.
|
|
1805
|
+
*
|
|
1806
|
+
* @template T - The initial component type (defaults to void if not specified)
|
|
1807
|
+
* @returns A unique identifier for the new entity
|
|
1808
|
+
*
|
|
1809
|
+
* @example
|
|
1810
|
+
* const entity = world.new<MyComponent>();
|
|
1811
|
+
* world.set(entity, MyComponent, { value: 42 });
|
|
1812
|
+
* world.sync();
|
|
1813
|
+
*/
|
|
1753
1814
|
new() {
|
|
1754
1815
|
const entityId = this.entityIdManager.allocate();
|
|
1755
1816
|
let emptyArchetype = this.ensureArchetype([]);
|
|
@@ -1775,14 +1836,22 @@ var World = class {
|
|
|
1775
1836
|
this.entityReferences.delete(cur);
|
|
1776
1837
|
const removedComponents = archetype.removeEntity(cur);
|
|
1777
1838
|
this.entityToArchetype.delete(cur);
|
|
1778
|
-
|
|
1779
|
-
const emptyArchetype = this.ensureArchetype([]);
|
|
1780
|
-
triggerLifecycleHooks(this.createHooksContext(), cur, /* @__PURE__ */ new Map(), removedComponents, archetype, emptyArchetype);
|
|
1781
|
-
}
|
|
1839
|
+
triggerRemoveHooksForEntityDeletion(this.createHooksContext(), cur, removedComponents, archetype);
|
|
1782
1840
|
this.cleanupArchetypesReferencingEntity(cur);
|
|
1783
1841
|
this.entityIdManager.deallocate(cur);
|
|
1784
1842
|
}
|
|
1785
1843
|
}
|
|
1844
|
+
/**
|
|
1845
|
+
* Checks if an entity exists in the world.
|
|
1846
|
+
*
|
|
1847
|
+
* @param entityId - The entity identifier to check
|
|
1848
|
+
* @returns `true` if the entity exists, `false` otherwise
|
|
1849
|
+
*
|
|
1850
|
+
* @example
|
|
1851
|
+
* if (world.exists(entityId)) {
|
|
1852
|
+
* console.log("Entity exists");
|
|
1853
|
+
* }
|
|
1854
|
+
*/
|
|
1786
1855
|
exists(entityId) {
|
|
1787
1856
|
return this.entityToArchetype.has(entityId);
|
|
1788
1857
|
}
|
|
@@ -1793,14 +1862,55 @@ var World = class {
|
|
|
1793
1862
|
if (detailedType.type === "wildcard-relation") throw new Error(`Cannot directly add wildcard relation components: ${componentType}`);
|
|
1794
1863
|
this.commandBuffer.set(entityId, componentType, component$1);
|
|
1795
1864
|
}
|
|
1865
|
+
/**
|
|
1866
|
+
* Removes a component from an entity.
|
|
1867
|
+
* The change is buffered and takes effect after calling `world.sync()`.
|
|
1868
|
+
* If the entity does not exist, throws an error.
|
|
1869
|
+
*
|
|
1870
|
+
* @template T - The component data type
|
|
1871
|
+
* @param entityId - The entity identifier
|
|
1872
|
+
* @param componentType - The component type to remove
|
|
1873
|
+
*
|
|
1874
|
+
* @throws {Error} If the entity does not exist
|
|
1875
|
+
* @throws {Error} If the component type is invalid
|
|
1876
|
+
*
|
|
1877
|
+
* @example
|
|
1878
|
+
* world.remove(entity, Position);
|
|
1879
|
+
* world.sync(); // Apply changes
|
|
1880
|
+
*/
|
|
1796
1881
|
remove(entityId, componentType) {
|
|
1797
1882
|
if (!this.exists(entityId)) throw new Error(`Entity ${entityId} does not exist`);
|
|
1798
1883
|
if (getDetailedIdType(componentType).type === "invalid") throw new Error(`Invalid component type: ${componentType}`);
|
|
1799
1884
|
this.commandBuffer.remove(entityId, componentType);
|
|
1800
1885
|
}
|
|
1886
|
+
/**
|
|
1887
|
+
* Deletes an entity and all its components from the world.
|
|
1888
|
+
* The change is buffered and takes effect after calling `world.sync()`.
|
|
1889
|
+
* Related entities may trigger cascade delete hooks if configured.
|
|
1890
|
+
*
|
|
1891
|
+
* @param entityId - The entity identifier to delete
|
|
1892
|
+
*
|
|
1893
|
+
* @example
|
|
1894
|
+
* world.delete(entity);
|
|
1895
|
+
* world.sync(); // Apply changes
|
|
1896
|
+
*/
|
|
1801
1897
|
delete(entityId) {
|
|
1802
1898
|
this.commandBuffer.delete(entityId);
|
|
1803
1899
|
}
|
|
1900
|
+
/**
|
|
1901
|
+
* Checks if an entity has a specific component.
|
|
1902
|
+
* Immediately reflects the current state without waiting for `sync()`.
|
|
1903
|
+
*
|
|
1904
|
+
* @template T - The component data type
|
|
1905
|
+
* @param entityId - The entity identifier
|
|
1906
|
+
* @param componentType - The component type to check
|
|
1907
|
+
* @returns `true` if the entity has the component, `false` otherwise
|
|
1908
|
+
*
|
|
1909
|
+
* @example
|
|
1910
|
+
* if (world.has(entity, Position)) {
|
|
1911
|
+
* const pos = world.get(entity, Position);
|
|
1912
|
+
* }
|
|
1913
|
+
*/
|
|
1804
1914
|
has(entityId, componentType) {
|
|
1805
1915
|
const archetype = this.entityToArchetype.get(entityId);
|
|
1806
1916
|
if (!archetype) return false;
|
|
@@ -1808,7 +1918,7 @@ var World = class {
|
|
|
1808
1918
|
if (isDontFragmentRelation(componentType)) return this.dontFragmentRelations.get(entityId)?.has(componentType) ?? false;
|
|
1809
1919
|
return false;
|
|
1810
1920
|
}
|
|
1811
|
-
get(entityId, componentType) {
|
|
1921
|
+
get(entityId, componentType = entityId) {
|
|
1812
1922
|
const archetype = this.entityToArchetype.get(entityId);
|
|
1813
1923
|
if (!archetype) throw new Error(`Entity ${entityId} does not exist`);
|
|
1814
1924
|
if (componentType >= 0 || componentType % 2 ** 42 !== 0) {
|
|
@@ -1818,7 +1928,7 @@ var World = class {
|
|
|
1818
1928
|
}
|
|
1819
1929
|
return archetype.get(entityId, componentType);
|
|
1820
1930
|
}
|
|
1821
|
-
getOptional(entityId, componentType) {
|
|
1931
|
+
getOptional(entityId, componentType = entityId) {
|
|
1822
1932
|
const archetype = this.entityToArchetype.get(entityId);
|
|
1823
1933
|
if (!archetype) throw new Error(`Entity ${entityId} does not exist`);
|
|
1824
1934
|
if (isWildcardRelationId(componentType)) {
|
|
@@ -1911,9 +2021,47 @@ var World = class {
|
|
|
1911
2021
|
}
|
|
1912
2022
|
}
|
|
1913
2023
|
}
|
|
2024
|
+
/**
|
|
2025
|
+
* Synchronizes all buffered commands (set/remove/delete) to the world.
|
|
2026
|
+
* This method must be called after making changes via `set()`, `remove()`, or `delete()` for them to take effect.
|
|
2027
|
+
* Typically called once per frame at the end of your game loop.
|
|
2028
|
+
*
|
|
2029
|
+
* @example
|
|
2030
|
+
* world.set(entity, Position, { x: 10, y: 20 });
|
|
2031
|
+
* world.remove(entity, OldComponent);
|
|
2032
|
+
* world.sync(); // Apply all buffered changes
|
|
2033
|
+
*/
|
|
1914
2034
|
sync() {
|
|
1915
2035
|
this.commandBuffer.execute();
|
|
1916
2036
|
}
|
|
2037
|
+
/**
|
|
2038
|
+
* Creates a cached query for efficiently iterating entities with specific components.
|
|
2039
|
+
* The query is cached internally and reused across calls with the same component types and filter.
|
|
2040
|
+
*
|
|
2041
|
+
* **Important:** Store the query reference and reuse it across frames for optimal performance.
|
|
2042
|
+
* Creating a new query each frame defeats the caching mechanism.
|
|
2043
|
+
*
|
|
2044
|
+
* @param componentTypes - Array of component types to match
|
|
2045
|
+
* @param filter - Optional filter for additional constraints (e.g., without specific components)
|
|
2046
|
+
* @returns A Query instance that can be used to iterate matching entities
|
|
2047
|
+
*
|
|
2048
|
+
* @example
|
|
2049
|
+
* // Create once, reuse many times
|
|
2050
|
+
* const movementQuery = world.createQuery([Position, Velocity]);
|
|
2051
|
+
*
|
|
2052
|
+
* // In game loop
|
|
2053
|
+
* movementQuery.forEach((entity) => {
|
|
2054
|
+
* const pos = world.get(entity, Position);
|
|
2055
|
+
* const vel = world.get(entity, Velocity);
|
|
2056
|
+
* pos.x += vel.x;
|
|
2057
|
+
* pos.y += vel.y;
|
|
2058
|
+
* });
|
|
2059
|
+
*
|
|
2060
|
+
* // With filter
|
|
2061
|
+
* const activeQuery = world.createQuery([Position], {
|
|
2062
|
+
* without: [Disabled]
|
|
2063
|
+
* });
|
|
2064
|
+
*/
|
|
1917
2065
|
createQuery(componentTypes, filter = {}) {
|
|
1918
2066
|
const sortedTypes = [...componentTypes].sort((a, b) => a - b);
|
|
1919
2067
|
const filterKey = serializeQueryFilter(filter);
|
|
@@ -1930,9 +2078,38 @@ var World = class {
|
|
|
1930
2078
|
});
|
|
1931
2079
|
return query;
|
|
1932
2080
|
}
|
|
2081
|
+
/**
|
|
2082
|
+
* Creates a new entity builder for fluent entity configuration.
|
|
2083
|
+
* Useful for building entities with multiple components in a single expression.
|
|
2084
|
+
*
|
|
2085
|
+
* @returns An EntityBuilder instance
|
|
2086
|
+
*
|
|
2087
|
+
* @example
|
|
2088
|
+
* const entity = world.spawn()
|
|
2089
|
+
* .with(Position, { x: 0, y: 0 })
|
|
2090
|
+
* .with(Velocity, { x: 1, y: 1 })
|
|
2091
|
+
* .build();
|
|
2092
|
+
* world.sync(); // Apply changes
|
|
2093
|
+
*/
|
|
1933
2094
|
spawn() {
|
|
1934
2095
|
return new EntityBuilder(this);
|
|
1935
2096
|
}
|
|
2097
|
+
/**
|
|
2098
|
+
* Spawns multiple entities with a configuration callback.
|
|
2099
|
+
* More efficient than calling `spawn()` multiple times when creating many entities.
|
|
2100
|
+
*
|
|
2101
|
+
* @param count - Number of entities to spawn
|
|
2102
|
+
* @param configure - Callback that receives an EntityBuilder and index; must return the configured builder
|
|
2103
|
+
* @returns Array of created entity IDs
|
|
2104
|
+
*
|
|
2105
|
+
* @example
|
|
2106
|
+
* const entities = world.spawnMany(100, (builder, index) => {
|
|
2107
|
+
* return builder
|
|
2108
|
+
* .with(Position, { x: index * 10, y: 0 })
|
|
2109
|
+
* .with(Velocity, { x: 0, y: 1 });
|
|
2110
|
+
* });
|
|
2111
|
+
* world.sync();
|
|
2112
|
+
*/
|
|
1936
2113
|
spawnMany(count, configure) {
|
|
1937
2114
|
const entities = [];
|
|
1938
2115
|
for (let i = 0; i < count; i++) {
|
|
@@ -1948,6 +2125,17 @@ var World = class {
|
|
|
1948
2125
|
const index = this.queries.indexOf(query);
|
|
1949
2126
|
if (index !== -1) this.queries.splice(index, 1);
|
|
1950
2127
|
}
|
|
2128
|
+
/**
|
|
2129
|
+
* Releases a cached query and frees its resources if no longer needed.
|
|
2130
|
+
* Call this when you're done using a query to allow the world to clean up its cache entry.
|
|
2131
|
+
*
|
|
2132
|
+
* @param query - The query to release
|
|
2133
|
+
*
|
|
2134
|
+
* @example
|
|
2135
|
+
* const query = world.createQuery([Position]);
|
|
2136
|
+
* // ... use query ...
|
|
2137
|
+
* world.releaseQuery(query); // Optional cleanup
|
|
2138
|
+
*/
|
|
1951
2139
|
releaseQuery(query) {
|
|
1952
2140
|
for (const [k, v] of this.queryCache.entries()) if (v.query === query) {
|
|
1953
2141
|
v.refCount--;
|
|
@@ -1959,6 +2147,14 @@ var World = class {
|
|
|
1959
2147
|
return;
|
|
1960
2148
|
}
|
|
1961
2149
|
}
|
|
2150
|
+
/**
|
|
2151
|
+
* Returns all archetypes that contain entities with the specified components.
|
|
2152
|
+
* Used internally for query optimization but can be useful for debugging.
|
|
2153
|
+
*
|
|
2154
|
+
* @param componentTypes - Array of component types to match
|
|
2155
|
+
* @returns Array of Archetype objects containing matching components
|
|
2156
|
+
* @internal
|
|
2157
|
+
*/
|
|
1962
2158
|
getMatchingArchetypes(componentTypes) {
|
|
1963
2159
|
if (componentTypes.length === 0) return [...this.archetypes];
|
|
1964
2160
|
const regularComponents = [];
|
|
@@ -2102,6 +2298,27 @@ var World = class {
|
|
|
2102
2298
|
}
|
|
2103
2299
|
for (const query of this.queries) query.removeArchetype(archetype);
|
|
2104
2300
|
}
|
|
2301
|
+
/**
|
|
2302
|
+
* Serializes the entire world state to a plain JavaScript object.
|
|
2303
|
+
* This creates a "memory snapshot" that can be stored or transmitted.
|
|
2304
|
+
* The snapshot can be restored using `new World(snapshot)`.
|
|
2305
|
+
*
|
|
2306
|
+
* **Note:** This is NOT automatically persistent storage. To persist data,
|
|
2307
|
+
* you must serialize the returned object to JSON or another format yourself.
|
|
2308
|
+
*
|
|
2309
|
+
* @returns A serializable object representing the world state
|
|
2310
|
+
*
|
|
2311
|
+
* @example
|
|
2312
|
+
* // Create snapshot
|
|
2313
|
+
* const snapshot = world.serialize();
|
|
2314
|
+
*
|
|
2315
|
+
* // Save to storage (example)
|
|
2316
|
+
* localStorage.setItem('save', JSON.stringify(snapshot));
|
|
2317
|
+
*
|
|
2318
|
+
* // Later, restore from snapshot
|
|
2319
|
+
* const savedData = JSON.parse(localStorage.getItem('save'));
|
|
2320
|
+
* const newWorld = new World(savedData);
|
|
2321
|
+
*/
|
|
2105
2322
|
serialize() {
|
|
2106
2323
|
const entities = [];
|
|
2107
2324
|
for (const archetype of this.archetypes) {
|