Some Useful Utility Functions From a Relative Newb

1541583086

Edited 1541583592
Ryan
Pro
Thought I'd post some simple utility functions I use in a lot of my sheetworker scripts, for two reasons:  They may be helpful to some of you, and/or you may know better ways to go about these things (or offer suggestions on writing more-concise code).  I'm still very much a beginner with javascript, so I've written this as a sort of beginner's tutorial to two of the more-brainwracking aspects of sheetworker: asynchronous functions and repeating sections. HANDLING ASYNCHRONOUS FUNCTIONS (TRIGGER WARNING: I use the $ sign in function names to indicate they're part of my system for asynchronous function handling.  If this hurts you, I'm sorry.) Plenty of functions in Roll20 are asynchronous, meaning they start when you call them in the script, but the script just keeps on going without waiting for them to finish.  This causes problems if, say, you change some values with one async function (like getAttrs()), and then immediately afterwards try to fetch them with another async function.  The second async function will run before the first one has completed, giving you values that the first function hasn't had a chance to update yet. The solution is to create a queue of functions in an array, and then feed that array into a master controller that will run each function in order, waiting until each one completes before moving on to the next.   First, you need to create your function queue.  Each function must include a "callback" parameter, which you call (as a function) when it's finished its work.  Here's an example of a queue with five functions, so you can see the various ways you can include them: let  $funcs  =   [      // FUNCTION 1      function   ( callback )   {         getAttrs ( attrList ,   function   ( attributes )   {              // ... do stuff with the attributes you got ...             callback ( null ,  returnVals );              // "returnVals" contains any data that you want to pass down to the next function in the queue.              // It always comes AFTER the "null" parameter.          });      },      // FUNCTION 2      function   ( receivedVals ,  callback )   {          // the FIRST parameter will receive any data you passed down to it; the SECOND parameter must be your callback function.         setAttrs ( receivedVals ,   {},  callback ( null ));          // setAttrs takes three parameters: the values to set, an options parameter, and then a callback function that is run          // when setAttrs has finished its work.  Since that's when we want our callback function to fire, we stick it there.      },      // FUNCTION 3 (more on this one below)     $getRepAttrs ([ "meleeWeapons" ,   "rangedWeapons" ],   [ "name" ,   "type" ,   "damage" ]),      // FUNCTION 4     $updateDamage ,    // You can define your functions elsewhere, and just include them in the array by name.      // FUNCTION 5 (more on this one below)     $set ]; The next thing you need is your master controller function.  Now, I have no idea how this function does what it does (my java-fu is that rudimentary).  I don't even know where I got it, or I'd credit the original author (please, if you recognize the code, do let me know so I can link to his or her work!)  But here it is nonetheless --- just copy it into your sheetworker script, and never look at it again ;) var  run$  =   function   ( tasks ,  cb )   {      var  current  =   0 ;      function  done ( err ,  args )   {          function  end ()   {             args  =  args  ?   []. concat ( err ,  args )   :   [ err ];              if   ( cb )                 cb . apply ( undefined ,  args );          }         end ();      };      function  each ( err )   {          var  args  =  Array . prototype . slice . call ( arguments ,   1 );          if   (++ current  >=  tasks . length  ||  err )             done ( err ,  args );          else             tasks [ current ]. apply ( undefined ,   []. concat ( args ,  each ));      };      if   ( tasks . length )         tasks [ 0 ]( each );      else         done ( null ); }; Once you have constructed your array of functions, simply pass it to the controller like so: run$ ( $funcs ,   function   ()   {  console . log ( "ASYNC FUNCTION QUEUE COMPLETE!" );   return ;   }); (or if you're like me and you just learned about shorthand arrow notation, you can be even fancier:) run$ ( $funcs ,   ()   =>  console . log ( "ASYNC FUNCTION QUEUE COMPLETE!" )); The function included in the second parameter is optional ("run$($funcs)" works just fine).  If you do include it, it'll fire when the whole queue has finished its work. $getRepAttrs():  GETTING ATTRIBUTES IN REPEATING SECTIONS Attributes in repeating sections have weird names, like "repeating_meleeWeapons_asahdlawuausjdhaagllw_damage".  No one wants to deal with those, so this function constructs all of those names for you.  Supply $getRepAttrs() with an array of fieldset section names, and an array of attribute names within those sections.  It'll iterate through them all, then pass down the queue an array containing all of the full attribute names, as well as a separate array of just the ids (the "asahdlawuausjdhaagllw" part): const  $getRepAttrs  =   function   ( sections ,  attrNames )   {      return   function   ( callback )   {          let  repVals  =   {  attrs :   [],  ids :   []   };          let  $funcs  =   [];         _ . each ( sections ,   function   ( sec )   {             $funcs . push ( function   ( cb )   {                 getSectionIDs ( sec ,   function   ( idarray )   {                     _ . each ( idarray ,   function   ( id )   {                         _ . each ( attrNames ,   function   ( stat )   {                             repVals . attrs . push ( "repeating_"   +  sec  +   "_"   +  id  +   "_"   +  stat );                          });                         repVals . ids . push ( id );                      });                     cb ( null );                  });              });          });         run$ ( $funcs ,   function   ()   {   return  callback ( null ,  repVals );   });      }; }; (Note the '$' --- this should be run inside a run$ controller queue.) You can see it uses its own run$ controller function to work, which is fine (you can nest them).  It uses that controller's callback function to kick the return values up a layer, so they get passed down to the next function in the higher run$ queue (i.e. Function 4 in our example array).  I realize that's confusing... it reminds me of the ending of Inception. Remember, the next function in line (Function 4 in our array) will have to be given the right parameters to receive the stats, so something like: const  $updateDamage  =   function   ( repVals ,  callback )   {     getAttrs ( repVals . attrs ,   function   ( v )   {          // Do stuff with all your repeating attributes --- remember, it's repVals.attrs, not repVals on its own.          // You can also get the ids; they're contained in the repVals.ids array.         callback ( null ,  attrList );      }); }; EASY ATTRIBUTE SETTING  Last in my sequence of asynchronous functions, we have $set.  This is my favourite, because it's so simple and it takes four keystrokes to type.  Stick it in a run$ queue, and it'll accept an attribute list from the function before it, and set all the attributes for you. const  $set  =   function   ( attrList ,  callback )   {     setAttrs ( attrList ,   {},  callback ( null )); }; EASY ON("CHANGE:") TRIGGER CONSTRUCTION If you have a lot of stats to watch for, it's a pain to write that "change:blah change:meh change:foo ..." string (it's also kind of ugly).  Here's a really simple function that takes an array of values, an optional prefix that you can apply to each one, and returns the appropriate trigger string.  You can plug the function right into on(), too: const  getTriggers  =   function   ( tArray ,  tPrefix )   {      return  _ . map ( tArray ,  t  =>   ( "change:"   +  tPrefix  +  t )). join ( " " ); } Hey, I didn't pretend like I was going to revolutionize coding here, but this is a useful bit of code.  It turns this: on ( "change:g3willpower_1 change:g3willpower_2 change:g3willpower_3 change:g3willpower_4 change:g3willpower_5 "   +      "change:g3willpower_6 change:g3willpower_7 change:g3willpower_8 change:g3willpower_9 change:g3willpower_10 "   +      "change:g3willpower_sDmg change:g3willpower_aDmg" ,   function   ( eventInfo )   { /*stuff*/ }); Into this: on ( getTriggers ([ 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 , "sDmg" , "aDmg" ],   "g3willpower_" ),   function   ( eventInfo )   { /*stuff*/ }); Anyways, I hope at least something in here was useful or interesting or at least not a total waste of time you'll never get back!  And if you see any ways I could improve my coding, I'd be very grateful to hear from you!
1541620733
G G
Pro
Sheet Author
The last function will be very useful to me. Do you have an example of a working character sheet using the async functions? I'm having trouble understanding when i would attempt to use them, and need a practical example.
1541681735
The Aaron
Pro
API Scripter
Nice! You might like looking into Promises and Async/Await. Those are facilities in modern javascript to help reduce the complexity of Asynchronous code.  Also, I’ve got TheAaronSheet which can help you with simplifying Repeating Sections.  BTW, Java !== JavaScript, so be sure you’re digging into javascript-fu! =D
1541688929
Scott C.
Pro
Sheet Author
API Scripter
The Aaron said: You might like looking into Promises and Async/Await. Those are facilities in modern javascript to help reduce the complexity of Asynchronous code. I wish us sheet authors could use those, unfortunately they break the instance's connection to a specific sheet, so we're stuck in call back hell :(