Roll20 uses cookies to improve your experience on our site. Cookies enable you to enjoy certain features, social sharing functionality, and tailor message and display ads to your interests on our site and others. They also help us understand how our site is being used. By continuing to use our site, you consent to our use of cookies. Update your cookie preferences .
×
Create a free account

Sheet Worker Script: Confusion, Explanation Needed

Hello!  First allow me to say that I can't for the life of me understand how sheet worker scripts actually work with JS. I have read  this page  and still don't get how it can work to solve my problem. I need this to work so that spent and unspent XP are automatically adjusted, based on both XP total written in by players manually and what is in the repeating fieldset's XP cost. Here is the HTML code that makes up the above image: ```   <h2>Experience Summary</h2>   <div style='text-align: center'>     Spent XP: <input type='number' name='attr_XP_spent' style='width: 10%; margin-right: 20px' value='@{XP_total}-(@{advancement_costL}+@{advancement_costR})' disabled='true'>     Unspent XP: <input type='number' name='attr_XP_current' style='width: 10%; margin-right: 20px' value='@{XP_total}-@{XP_spent}' disabled='true'>     XP Total: <input type='number' name='attr_XP_total' style='width: 10%'>   </div>   <br>   <fieldset name='advancements' class='repeating_advancements'>     <input type='text' name='attr_advancementL' placeholder='Write XP-costing item name here' style='width: 25%'>     <input type='number' name='attr_advancement_costL' placeholder='XP Cost' style='width: 10%'>     <input type='number' name='attr_advancement_pg#L' placeholder='Page #' style='width: 10%; margin-right: 10px'>     <input type='text' name='attr_advancementR' placeholder='Write XP-costing item name here' style='width: 25%'>     <input type='number' name='attr_advancement_costR' placeholder='XP Cost' style='width: 10%'>     <input type='number' name='attr_advancement_pg#R' placeholder='Page #' style='width: 10%'>   </fieldset> ``` Here is the code I am working with: ```   <script type='text/worker'>     on('change:repeating_advancements remove:repeating_advancements', () => {       getAttrs(['advancement_costL', 'advancement_costR'], function(values) {     let cost1 = parseInt(values['advancement_costL'], 10) || 0;     let cost2 = parseInt(values['advancement_costR'], 10) || 0;   }        }   </script> ``` What are the required steps to make this happen using sheet worker scripts? I will be watching this thread intently.
1582582972

Edited 1582670583
GiGs
Pro
Sheet Author
API Scripter
The first thing to do is remove the disable="true" section - sheet workers cant update disabled attributes, You can use readonly   instead to stop users modifying them. It looks like you are totalling the values in a repeating section. That's actually a pretty challenging problem, and definitely not the easiest way to start using sheet workers. Luckily i have a pre-written solution for you since its a very common problem: the  repeatingSum function However, your repeating section confuses me. You have two XP cost attributes, advancement_costL and advancement_costR ? Do advancements need two costs? For the example below, I'm assuming you have a single cost attribute, advancement_cost . Step 1: Copy this function into your script block and do not change it: (Don't worry, you dont need to understand it): /* ===== PARAMETERS ========== destination = the name of the attribute that stores the total quantity section = name of repeating fieldset, without the repeating_ fields = the name of the attribute field to be summed can be a single attribute: 'weight' or an array of attributes: ['weight','number','equipped'] multiplier (optional) = a multiplier to the entire fieldset total. For instance, if summing coins of weight 0.02, might want to multiply the final total by 0.02. */ const repeatingSum = (destination, section, fields, multiplier = 1 ) = > { if ( ! Array . isArray (fields)) fields = [fields]; getSectionIDs (`repeating_${section}`, idArray = > { const attrArray = idArray. reduce ( (m,id) = > [...m, ...(fields. map (field = > `repeating_${section}_${id}_${field}`))],[]); getAttrs (attrArray, v = > { console. log ( "===== values of v: " + JSON. stringify (v) + " = = = = = "); // getValue: if not a number, returns 1 if it is 'on' (checkbox), otherwise returns 0.. const getValue = (section, id,field) = > parseFloat (v[`repeating_${section}_${id}_${field}`]) | | (v[`repeating_${section}_${id}_${field}`] = = = 'on'  ? 1  : 0 ); const sumTotal = idArray. reduce ((total, id) = > total + fields. reduce ((subtotal,field) = > subtotal * getValue (section, id,field), 1 ), 0 ); setAttrs ({[destination]: sumTotal * multiplier}); }); }); }; Step 2: add the following two small sheet workers to go along with it. One uses that function to calculate the spent_XP attribute attribute, and the second monitors spent_XP and total_XP, and when either changes, calculates remaining XP. on ( 'change:repeating_advancements remove:repeating_advancements' , function () { repeatingSum ( "XP_spent" ,"advancements","advancement_cost"); }); on('change:xp_spent change:xp_total', function() {     getAttrs(['XP_spent', 'XP_total'], function(values) {         const spent = parseInt(values.XP_spent) || 0;         const total = parseInt(values.XP_total) || 0;         const left = total = spent;         setAttrs({             XP_current: left         });     }); }); The first of the two sheet workers up totals the values in the advencement_cost  column of the repeating section, and puts the total in the XP_spent attribute. The section worker reads the XP-spent and XP_total attributes, uses parseInt  to make sure they are numbers, then calculates the XP left and updates that XP_current attribute. Does this do what you need?
No, this does not work.  Please look again at the reference image of the operating character sheet and then at my code.  There are two cost values per repeating fieldset: advancement_costL and advancement_costR. I was hoping for a sort of line-by-line walkthrough.  I've only been writing JS for about 3-4 months and I feel like I'm even more lost than when I started. Do you need a link to the  build  for context?  The screencap content starts at  <div class='sheet-advancements'> for context.  Plus how did you compact your code in that sort of clean window?  I feel that I might need to read the Roll20 API documents again, or simply improve my JS programming skill.
1582585261

Edited 1582585898
GiGs
Pro
Sheet Author
API Scripter
to produce the code-like output, there's a button to the top left of the editing window that looks a bit like a flashlight. Click that and you have some style options. The one you want is code . I looked at your pic more closely and realised what you were doing. Personally I think for that kind of layout you are better using one item per row of the fieldset and using CSS to split it into two columns. For that, the code I gave early would work. But for your actual situation, here's a solution. I dont have time for a line-by-line explanation (I normally like to do that, sorry). on('change:xp_total change:repeating_advancements:advancement_costL change:repeating_advancements:advancement_costR remove:repeating_advancements', () => {         getSectionIDs(`repeating_advancements`, idArray => {             const fieldnames = [];             idArray.forEach(id => fieldnames.push(                 `repeating_advancements_${id}_advancement_costL`,                 `repeating_advancements_${id}_advancement_costR`             ));             getAttrs(['XP_total',...fieldnames], v => {                 const getValue = (id,field) => parseFloat(v[`repeating_advancements_${id}_${field}`]) || 0;                  const spent = idArray.reduce((total, id) => total + getValue(id,'advancement_costL') + getValue(id,'advancement_costR'),0);                 const total = parseFloat(v.XP_total) || 0;                 setAttrs({                     XP_spent: spent,                     XP_current: total - spent                 });                 });         });     }); The big problem with your original attempt is that you need to count up the full repeating section, so you need to use the getSectionIDs function (its described on the wiki). That lets you find all the rows that exist, and create an array of attribute names. Then you can use getAttrs to grab the values from those rows, and add them up.
So for getSectionIDs, that's necessary for Roll20's unique repeating fieldsets?  Plus I didn't think about using a spread operator in the getAttrs parameter field.  That seemed way simpler than I was making it out to be.  Maybe I need more HackerRank JS coding challenges, or maybe this indeed was too complex for me to do in an hour in the context of my HTML code.  Either way, thank you! Doesn't work quite yet.  So I have to slap this between <script type='text/worker'>  and  </script> then?  What else needs doing?
1582587391
GiGs
Pro
Sheet Author
API Scripter
Jebediah S. said: So for getSectionIDs, that's necessary for Roll20's unique repeating fieldsets?  Plus I didn't think about using a spread operator in the getAttrs parameter field.  That seemed way simpler than I was making it out to be.  Maybe I need more HackerRank JS coding challenges, or maybe this indeed was too complex for me to do in an hour in the context of my HTML code.  Either way, thank you! Doesn't work quite yet.  So I have to slap this between <script type='text/worker'>  and  </script> then?  What else needs doing? Yes, all sheet workers have to go inside your script block. You should have a single   <script type='text/worker'>  and  </script>  block, and put all sheet workers inside that. Regarding getSectionIDs: you dont need to use it if you are doing an operation on a single row of the fieldset, from that row. But if you want to do something with multiple rows, or access the fieldset from outside it, you need to use getSectionIDs.
Thanks again for telling me how getSectionIDs works in the context of accessing particular fieldset data!  I think I have a bit of a better grasp on this now, although it still seems difficult trying to figure out what you need to do in JS and what needs doing in the special Roll20 API methods. If you see this thread again, maybe you'll have a chance to explain your code to myself and other readers line-by-line.  I know what the reduce() method does, just had trouble using it on my own before.
1582668146

Edited 1594956251
GiGs
Pro
Sheet Author
API Scripter
Using reduce there wasnt essential - any loop construction would have done, and are generally easier to figure out :) I use forEach a lot (as you can see earlier in the script), but could have easily used an old for loop, and that might better illustrate what's going on. I'll do that below, but first I'll give a crash course in sheetworkers, including why getSectionIDs is different. Sheet workers have no direct access to the character sheet - you can't modify it in any way. What you can do through the getAttrs function is read attribute values from the sheet, and with setAttrs, write values to the sheet. That's essentially all sheet workers can do. With getAttrs, you supply an array of attribute names, and it builds a variable (often called values by custom) that contains the attribute names and their values.  It might look like either of these: getAttrs(['XP_total'], values => { getAttrs(['XP_total'], function(values) { You then access them within your sheet worker using things like var XP_total = values.XP_total; or var XP_total = values['XP_total']; These are equivalent for most puposes  -the first version is simpler to write, the second is needed in some cases (like when an attribute name contains characetrs that are illegal in javascript, like "-"). Since all roll20 attributes are stored as text, you often have to convert them into numbers (for instance, when you want to add a bunch of them up). There are several ways to do this. All of these work, and each has pros and cons, but mainly its a user preference. var str = parseInt(values.str) ||0; var str = +values.str || 0; var str = Number(values.str) ||0; The ||0 at the end of each is a logic function , and says "OR 0", which means if the attempt to convert the attribute to a number fails, you end with a value of 0. This helps avoid errors which break the sheet worker (like when you try to convert a word to a number). This is very important in roll20, when players might be entering the wrong values in textboxes. So we now know how to get attribute values, but attributes in repeating sections are special. You might give an attribute name advancement_costL , but then when you add new rows to the section, you end up with multiple attributes by that name. How can you access the correct one? To solve this problem, roll20 creates a composite name  for every attribute in a repeating section. The names are made of three parts: the repeating fieldset name, an id for the row, and the name you created. They look like this: repeating_section _ id _ attribute name so your costL attribute name would actually look something like repeating_advancement_-hfgstr75hjyi_advancement_costL That is its true name, which can be accessed in macros and by getAttrs - the -hfgstr75hjyi part is the row id. They are generated randomly when the row is created, and are very complex to make sure that different rows in the same campaign wont have the same row id. Using getSectionIDs The getSectionIDs function gives you an array of all the row ids that exist , and you can then use it to construct the attributes names you need.  So the procedure is: 1) use getSectionIDs to get a list of all the row ids 2) do a loop through the array, and build the names of all the attributes within the section you need 3) use getAttrs to retrieve their values (and any other attribute values you need) 4) now perform whatever operations you want to on those values (like adding together all the weights in an encumbrance section). You might have to loop through the ids array again, to do your processing. 5) and finally, use setAttrs to save the updated values to the sheet. The individual steps in the above are each fairly basic in terms of the levels of javascript skill you need to do them, once you know what you are doing, but they are really hard to figure out the first time you do each and there are so many different parts to learn it can be a real stumbling block.  So here's an annotated version of the previous function, rewritten to be more beginner-friendly on('change:xp_total change:repeating_advancements:advancement_costL change:repeating_advancements:advancement_costR remove:repeating_advancements', () => {     getSectionIDs(`repeating_advancements`, function(idArray) {                  // create a variable to hold all the needed attribute names         var fieldnames = [];                  // loop throw the rowIDs and build their actual attributes names. adding them to the fieldnames array         for(var i = 0; i < idArray.length; i++) {             fieldnames.push(                 "repeating_advancements_" + idArray[i] + "_advancement_costL",                 "repeating_advancements_" + idArray[i] + "_advancement_costR"                );         }         // if you need any other attributes in your worker, dont forget to include those         fieldnames.push('XP_total');         // now  use getAttrs to extract those attribute values from the sheet and store them in an object called values.         // since fieldnames is already an array, we dont need to use the usual [ ] brackets.         getAttrs(fieldnames, function(values) {             // get the total XP vvalue             var total = parseInt(values.XP_total) || 0;                          // create an attribute to hold the xp spent             var spent = 0;                          // now loop through the row ids again, and grab their values, adding them to the spent total             // the idArray variable created earlier by getSectionIDs still exists so we can use that.             for(var i = 0; i < idArray.length; i++) {                 // we likely need to build the attribute names again (there's a cleverer way, I'll mention later)                 var left = parseInt(values["repeating_advancements_" + idArray[i] + "_advancement_costL"]) ||0;                 // notice to get this we have to use the values['name'] version, not the values.name version                 // because we are building the names dynamically.                  var right = parseInt(values["repeating_advancements_" + idArray[i] + "_advancement_costR"]) ||0;                 // now add those values to the ongoing spentXP total.                 spent = spent + left + right;             }             // now the loop is finished we have our total spent, and can output the attributes to the sheet.                          setAttrs({                 XP_spent: spent,                 XP_current: total - spent             });             });     }); }); I mentioned there's a cleverer way to do that for loop (but it's fairly advanced). First you need to understand the values object contains all the attributes we have defined, and only one of (XP_total) them in this function isnt part of the repeating section. So if we ignore that one, we can just sum up every value in the values object, and we dont need to know their names.             // first, use destructuring to get all the values that are NOT XP_total             const { XP_total, ...rest } = values;             // now simply sum those values, without an explicit loop or needing to know their names. (You still need them for the getAttrs step though)             const spent = Object.values(rest).reduce((sum, curr) => sum + curr, 0); But I'm just showing off here! I hope the previous example and explanation helped to explain what's going on.
1582669903
vÍnce
Pro
Sheet Author
GiGs rocks!
1582674090
GiGs
Pro
Sheet Author
API Scripter
Thanks, Vince :)