@@ -845,4 +845,159 @@ describe("World", () => {
845845 expect ( hookCalled ) . toBe ( false ) ;
846846 } ) ;
847847 } ) ;
848+
849+ describe ( "Multi-Component Hooks" , ( ) => {
850+ it ( "should trigger on_set when all required components are present" , ( ) => {
851+ const world = new World ( ) ;
852+ const A = component < number > ( ) ;
853+ const B = component < string > ( ) ;
854+ const calls : { entityId : EntityId ; components : readonly [ number , string ] } [ ] = [ ] ;
855+
856+ world . hook ( [ A , B ] , {
857+ on_set : ( entityId , _componentTypes , components ) => {
858+ calls . push ( { entityId, components } ) ;
859+ } ,
860+ } ) ;
861+
862+ const entity = world . spawn ( ) . with ( A , 42 ) . with ( B , "hello" ) . build ( ) ;
863+ world . sync ( ) ;
864+
865+ expect ( calls . length ) . toBe ( 1 ) ;
866+ expect ( calls [ 0 ] ! . entityId ) . toBe ( entity ) ;
867+ expect ( calls [ 0 ] ! . components ) . toEqual ( [ 42 , "hello" ] ) ;
868+ } ) ;
869+
870+ it ( "should not trigger on_set when some required components are missing" , ( ) => {
871+ const world = new World ( ) ;
872+ const A = component < number > ( ) ;
873+ const B = component < string > ( ) ;
874+ const calls : any [ ] = [ ] ;
875+
876+ world . hook ( [ A , B ] , {
877+ on_set : ( entityId , _componentTypes , components ) => {
878+ calls . push ( { entityId, components } ) ;
879+ } ,
880+ } ) ;
881+
882+ const entity = world . spawn ( ) . with ( A , 42 ) . build ( ) ;
883+ world . sync ( ) ;
884+
885+ expect ( calls . length ) . toBe ( 0 ) ;
886+ expect ( world . has ( entity , A ) ) . toBe ( true ) ;
887+ expect ( world . has ( entity , B ) ) . toBe ( false ) ;
888+ } ) ;
889+
890+ it ( "should trigger on_set with optional component present" , ( ) => {
891+ const world = new World ( ) ;
892+ const A = component < number > ( ) ;
893+ const B = component < string > ( ) ;
894+ const calls : { entityId : EntityId ; components : readonly [ number , { value : string } | undefined ] } [ ] = [ ] ;
895+
896+ world . hook ( [ A , { optional : B } ] , {
897+ on_set : ( entityId , _componentTypes , components ) => {
898+ calls . push ( { entityId, components } ) ;
899+ } ,
900+ } ) ;
901+
902+ const entity = world . spawn ( ) . with ( A , 42 ) . with ( B , "hello" ) . build ( ) ;
903+ world . sync ( ) ;
904+
905+ expect ( calls . length ) . toBe ( 1 ) ;
906+ expect ( calls [ 0 ] ! . entityId ) . toBe ( entity ) ;
907+ expect ( calls [ 0 ] ! . components ) . toEqual ( [ 42 , { value : "hello" } ] ) ;
908+ } ) ;
909+
910+ it ( "should trigger on_set with optional component absent" , ( ) => {
911+ const world = new World ( ) ;
912+ const A = component < number > ( ) ;
913+ const B = component < string > ( ) ;
914+ const calls : { entityId : EntityId ; components : readonly [ number , { value : string } | undefined ] } [ ] = [ ] ;
915+
916+ world . hook ( [ A , { optional : B } ] , {
917+ on_set : ( entityId , _componentTypes , components ) => {
918+ calls . push ( { entityId, components } ) ;
919+ } ,
920+ } ) ;
921+
922+ const entity = world . spawn ( ) . with ( A , 42 ) . build ( ) ;
923+ world . sync ( ) ;
924+
925+ expect ( calls . length ) . toBe ( 1 ) ;
926+ expect ( calls [ 0 ] ! . entityId ) . toBe ( entity ) ;
927+ expect ( calls [ 0 ] ! . components ) . toEqual ( [ 42 , undefined ] ) ;
928+ } ) ;
929+
930+ it ( "should trigger on_remove with complete snapshot when required component is removed" , ( ) => {
931+ const world = new World ( ) ;
932+ const A = component < number > ( ) ;
933+ const B = component < string > ( ) ;
934+ const removeCalls : { entityId : EntityId ; components : readonly [ number , string ] } [ ] = [ ] ;
935+
936+ world . hook ( [ A , B ] , {
937+ on_remove : ( entityId , _componentTypes , components ) => {
938+ removeCalls . push ( { entityId, components } ) ;
939+ } ,
940+ } ) ;
941+
942+ const entity = world . spawn ( ) . with ( A , 42 ) . with ( B , "hello" ) . build ( ) ;
943+ world . sync ( ) ;
944+
945+ world . remove ( entity , A ) ;
946+ world . sync ( ) ;
947+
948+ expect ( removeCalls . length ) . toBe ( 1 ) ;
949+ expect ( removeCalls [ 0 ] ! . entityId ) . toBe ( entity ) ;
950+ expect ( removeCalls [ 0 ] ! . components ) . toEqual ( [ 42 , "hello" ] ) ;
951+ } ) ;
952+
953+ it ( "should trigger on_init for existing entities matching all required components" , ( ) => {
954+ const world = new World ( ) ;
955+ const A = component < number > ( ) ;
956+ const B = component < string > ( ) ;
957+
958+ const entity = world . spawn ( ) . with ( A , 42 ) . with ( B , "hello" ) . build ( ) ;
959+ world . sync ( ) ;
960+
961+ const initCalls : { entityId : EntityId ; components : readonly [ number , string ] } [ ] = [ ] ;
962+
963+ world . hook ( [ A , B ] , {
964+ on_init : ( entityId , _componentTypes , components ) => {
965+ initCalls . push ( { entityId, components } ) ;
966+ } ,
967+ } ) ;
968+
969+ expect ( initCalls . length ) . toBe ( 1 ) ;
970+ expect ( initCalls [ 0 ] ! . entityId ) . toBe ( entity ) ;
971+ expect ( initCalls [ 0 ] ! . components ) . toEqual ( [ 42 , "hello" ] ) ;
972+ } ) ;
973+
974+ it ( "should stop triggering after unhook for multi-component hooks" , ( ) => {
975+ const world = new World ( ) ;
976+ const A = component < number > ( ) ;
977+ const B = component < string > ( ) ;
978+ const calls : any [ ] = [ ] ;
979+
980+ const hook = {
981+ on_set : ( entityId : EntityId , _componentTypes : any , components : any ) => {
982+ calls . push ( { entityId, components } ) ;
983+ } ,
984+ } ;
985+
986+ world . hook ( [ A , B ] , hook ) ;
987+
988+ const entity1 = world . spawn ( ) . with ( A , 1 ) . with ( B , "first" ) . build ( ) ;
989+ world . sync ( ) ;
990+
991+ expect ( calls . length ) . toBe ( 1 ) ;
992+
993+ world . unhook ( [ A , B ] , hook ) ;
994+
995+ const entity2 = world . spawn ( ) . with ( A , 2 ) . with ( B , "second" ) . build ( ) ;
996+ world . sync ( ) ;
997+
998+ expect ( calls . length ) . toBe ( 1 ) ;
999+ expect ( world . has ( entity1 , A ) ) . toBe ( true ) ;
1000+ expect ( world . has ( entity2 , A ) ) . toBe ( true ) ;
1001+ } ) ;
1002+ } ) ;
8481003} ) ;
0 commit comments