Skip to content

Commit 7787032

Browse files
committed
feat(world): add wildcard relation lifecycle hooks support
- Introduced WildcardRelationLifecycleHook interface for handling lifecycle events of components matching wildcard relations. - Implemented methods to register and unregister these hooks in the World class. - Updated component addition and removal logic to trigger the appropriate hooks when components are added or removed. - Added tests to verify the correct triggering of hooks for matching and non-matching components.
1 parent acdaf8e commit 7787032

File tree

2 files changed

+137
-0
lines changed

2 files changed

+137
-0
lines changed

src/world.test.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -494,4 +494,70 @@ describe("World", () => {
494494
expect(removedCalled).toBe(true);
495495
});
496496
});
497+
498+
describe("Wildcard Relation Hooks", () => {
499+
it("should trigger wildcard relation hooks for matching relation components", () => {
500+
const world = new World();
501+
const positionComponent = createComponentId<{ x: number; y: number }>(1);
502+
const entity1 = world.createEntity();
503+
const entity2 = world.createEntity();
504+
505+
// Create a relation component (positionComponent -> entity2)
506+
const relationId = createRelationId(positionComponent, entity2);
507+
508+
let addedCalled = false;
509+
let removedCalled = false;
510+
let addedComponentType: EntityId<{ x: number; y: number }> | undefined;
511+
let removedComponentType: EntityId<{ x: number; y: number }> | undefined;
512+
513+
// Register a wildcard relation hook for positionComponent
514+
world.registerWildcardRelationLifecycleHook(positionComponent, {
515+
onAdded: (entityId, componentType, component) => {
516+
addedCalled = true;
517+
addedComponentType = componentType;
518+
},
519+
onRemoved: (entityId, componentType) => {
520+
removedCalled = true;
521+
removedComponentType = componentType;
522+
},
523+
});
524+
525+
// Add the relation component
526+
world.addComponent(entity1, relationId, { x: 10, y: 20 });
527+
world.flushCommands();
528+
529+
expect(addedCalled).toBe(true);
530+
expect(addedComponentType).toBe(relationId);
531+
532+
// Remove the relation component
533+
world.removeComponent(entity1, relationId);
534+
world.flushCommands();
535+
536+
expect(removedCalled).toBe(true);
537+
expect(removedComponentType).toBe(relationId);
538+
});
539+
540+
it("should not trigger wildcard relation hooks for non-matching components", () => {
541+
const world = new World();
542+
const positionComponent = createComponentId<{ x: number; y: number }>(1);
543+
const velocityComponent = createComponentId<{ vx: number; vy: number }>(2);
544+
const entity1 = world.createEntity();
545+
const entity2 = world.createEntity();
546+
547+
let hookCalled = false;
548+
549+
// Register a wildcard relation hook for positionComponent
550+
world.registerWildcardRelationLifecycleHook(positionComponent, {
551+
onAdded: () => {
552+
hookCalled = true;
553+
},
554+
});
555+
556+
// Add a velocity component (not a position relation)
557+
world.addComponent(entity1, velocityComponent, { vx: 1, vy: 2 });
558+
world.flushCommands();
559+
560+
expect(hookCalled).toBe(false);
561+
});
562+
});
497563
});

src/world.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,21 @@ export interface ComponentLifecycleHook<T> {
2222
onRemoved?: (entityId: EntityId, componentType: EntityId<T>) => void;
2323
}
2424

25+
/**
26+
* Hook types for wildcard relation lifecycle events
27+
* These hooks are triggered for any component that matches a wildcard relation pattern
28+
*/
29+
export interface WildcardRelationLifecycleHook<T = unknown> {
30+
/**
31+
* Called when any component matching the wildcard relation pattern is added to an entity
32+
*/
33+
onAdded?: (entityId: EntityId, componentType: EntityId<T>, component: T) => void;
34+
/**
35+
* Called when any component matching the wildcard relation pattern is removed from an entity
36+
*/
37+
onRemoved?: (entityId: EntityId, componentType: EntityId<T>) => void;
38+
}
39+
2540
/**
2641
* World class for ECS architecture
2742
* Manages entities, components, and systems
@@ -41,6 +56,12 @@ export class World<ExtraParams extends any[] = [deltaTime: number]> {
4156
*/
4257
private componentLifecycleHooks = new Map<EntityId<any>, Set<ComponentLifecycleHook<any>>>();
4358

59+
/**
60+
* Hook storage for wildcard relation lifecycle events
61+
* Maps base component type to set of wildcard relation hooks
62+
*/
63+
private wildcardRelationLifecycleHooks = new Map<EntityId<any>, Set<WildcardRelationLifecycleHook>>();
64+
4465
/**
4566
* Reverse index tracking which entities use each entity as a component type
4667
* Maps entity ID to set of {sourceEntityId, componentType} pairs where componentType uses this entity
@@ -219,6 +240,30 @@ export class World<ExtraParams extends any[] = [deltaTime: number]> {
219240
}
220241
}
221242

243+
/**
244+
* Register a lifecycle hook for wildcard relation events
245+
* The hook will be triggered for any component that matches the wildcard relation pattern
246+
*/
247+
registerWildcardRelationLifecycleHook<T>(baseComponentType: EntityId<T>, hook: WildcardRelationLifecycleHook<T>): void {
248+
if (!this.wildcardRelationLifecycleHooks.has(baseComponentType)) {
249+
this.wildcardRelationLifecycleHooks.set(baseComponentType, new Set());
250+
}
251+
this.wildcardRelationLifecycleHooks.get(baseComponentType)!.add(hook as WildcardRelationLifecycleHook<any>);
252+
}
253+
254+
/**
255+
* Unregister a lifecycle hook for wildcard relation events
256+
*/
257+
unregisterWildcardRelationLifecycleHook<T>(baseComponentType: EntityId<T>, hook: WildcardRelationLifecycleHook<T>): void {
258+
const hooks = this.wildcardRelationLifecycleHooks.get(baseComponentType);
259+
if (hooks) {
260+
hooks.delete(hook as WildcardRelationLifecycleHook<any>);
261+
if (hooks.size === 0) {
262+
this.wildcardRelationLifecycleHooks.delete(baseComponentType);
263+
}
264+
}
265+
}
266+
222267
/**
223268
* Update the world (run all systems)
224269
*/
@@ -616,6 +661,19 @@ export class World<ExtraParams extends any[] = [deltaTime: number]> {
616661
}
617662
}
618663
}
664+
665+
// Trigger wildcard relation hooks for added components
666+
const detailedType = getDetailedIdType(componentType);
667+
if (detailedType.type === "entity-relation" || detailedType.type === "component-relation" || detailedType.type === "wildcard-relation") {
668+
const wildcardHooks = this.wildcardRelationLifecycleHooks.get(detailedType.componentId!);
669+
if (wildcardHooks) {
670+
for (const hook of wildcardHooks) {
671+
if (hook.onAdded) {
672+
(hook as any).onAdded(entityId, componentType, component);
673+
}
674+
}
675+
}
676+
}
619677
}
620678

621679
// Trigger component removed hooks
@@ -628,6 +686,19 @@ export class World<ExtraParams extends any[] = [deltaTime: number]> {
628686
}
629687
}
630688
}
689+
690+
// Trigger wildcard relation hooks for removed components
691+
const detailedType = getDetailedIdType(componentType);
692+
if (detailedType.type === "entity-relation" || detailedType.type === "component-relation" || detailedType.type === "wildcard-relation") {
693+
const wildcardHooks = this.wildcardRelationLifecycleHooks.get(detailedType.componentId!);
694+
if (wildcardHooks) {
695+
for (const hook of wildcardHooks) {
696+
if (hook.onRemoved) {
697+
(hook as any).onRemoved(entityId, componentType);
698+
}
699+
}
700+
}
701+
}
631702
}
632703
}
633704
}

0 commit comments

Comments
 (0)