Skip to content

Commit 2343748

Browse files
committed
fix(hooks): trigger on_set callback when optional component is removed
Previously, on_set was only triggered when components were added. Now it also triggers when an optional component is removed, notifying hooks that the entity state changed (optional component becomes undefined).
1 parent f829de4 commit 2343748

File tree

2 files changed

+39
-10
lines changed

2 files changed

+39
-10
lines changed

src/__tests__/world-multi-component-hooks.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,33 @@ describe("World - Multi-Component Hooks", () => {
219219
expect(removeCalls.length).toBe(0);
220220
});
221221

222+
it("should trigger on_set when optional component is removed", () => {
223+
const world = new World();
224+
const A = component<number>();
225+
const B = component<string>();
226+
const setCalls: { entityId: EntityId; components: readonly [number, { value: string } | undefined] }[] = [];
227+
228+
world.hook([A, { optional: B }], {
229+
on_set: (entityId, ...components) => {
230+
setCalls.push({ entityId, components });
231+
},
232+
});
233+
234+
const entity = world.spawn().with(A, 42).with(B, "hello").build();
235+
world.sync();
236+
237+
expect(setCalls.length).toBe(1);
238+
expect(setCalls[0]!.components).toEqual([42, { value: "hello" }]);
239+
240+
// Remove optional component B - should trigger on_set with undefined for B
241+
world.remove(entity, B);
242+
world.sync();
243+
244+
expect(setCalls.length).toBe(2);
245+
expect(setCalls[1]!.entityId).toBe(entity);
246+
expect(setCalls[1]!.components).toEqual([42, undefined]);
247+
});
248+
222249
it("should trigger on_remove with complete snapshot when required component is removed", () => {
223250
const world = new World();
224251
const A = component<number>();

src/core/world-hooks.ts

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -115,18 +115,20 @@ function triggerMultiComponentHooks(
115115
oldArchetype: Archetype,
116116
newArchetype: Archetype,
117117
): void {
118-
// Handle on_set: triggers if any required or optional component was added and entity matches now
119-
if (addedComponents.size > 0) {
120-
for (const entry of newArchetype.matchingMultiHooks) {
121-
const { hook, requiredComponents, optionalComponents, componentTypes } = entry;
122-
if (!hook.on_set) continue;
118+
// Handle on_set: triggers if any required or optional component was added/removed and entity still matches
119+
for (const entry of newArchetype.matchingMultiHooks) {
120+
const { hook, requiredComponents, optionalComponents, componentTypes } = entry;
121+
if (!hook.on_set) continue;
123122

124-
const anyRequiredAdded = requiredComponents.some((c) => anyComponentMatches(addedComponents, c));
125-
const anyOptionalAdded = optionalComponents.some((c) => anyComponentMatches(addedComponents, c));
123+
const anyRequiredAdded = requiredComponents.some((c) => anyComponentMatches(addedComponents, c));
124+
const anyOptionalAdded = optionalComponents.some((c) => anyComponentMatches(addedComponents, c));
125+
const anyOptionalRemoved = optionalComponents.some((c) => anyComponentMatches(removedComponents, c));
126126

127-
if ((anyRequiredAdded || anyOptionalAdded) && entityHasAllComponents(ctx, entityId, requiredComponents)) {
128-
hook.on_set(entityId, ...collectMultiHookComponents(ctx, entityId, componentTypes));
129-
}
127+
if (
128+
(anyRequiredAdded || anyOptionalAdded || anyOptionalRemoved) &&
129+
entityHasAllComponents(ctx, entityId, requiredComponents)
130+
) {
131+
hook.on_set(entityId, ...collectMultiHookComponents(ctx, entityId, componentTypes));
130132
}
131133
}
132134

0 commit comments

Comments
 (0)