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

Updating a repeating section with an event trigger outside of it

First off, I want to say I’m in no way a coder. That being said, I’m having a problem with getting a value in a repeating section to update using an event trigger outside the repeating section. My code is below. on("change:bpTot sheet:opened", function() { getAttrs(["bpTot"], function(value) { let bpTotal = parseInt(value.bpTot,10)||0; setAttrs ({ repeating_body_bodySegBpTot: bpTotal }); }); }); I have a box that has a stat called bp and I want it to copy to all the rows of a repeating section called body. I also want all the rows to update if the bp changes. I’d really appreciate the help. Thanks in advance.
1568310944

Edited 1568313491
GiGs
Pro
Sheet Author
API Scripter
You need to use getSectionIDs for this. A repeating section could have a variable number of rows (or, indeed, none) and you need to get a list of all the row ids, build complete names for row's attribute (in the form repeating_section _ id _ attribute_name) and then you can update all those stats. Here's one way to do that: on("change:bptot sheet:opened", function () { getAttrs(["bpTot"], function (value) { let bpTotal = parseInt(value.bpTot, 10) || 0; getSectionIDs("repeating_body", function (idarray) { let rows = {}; for (let i = 0; i < idarray.length; i++) { rows['repeating_body_' + idarray[i] + '_bodySegBpTot'] = bpTotal; }             if(rows) setAttrs(rows); }); }); });
1568311099
GiGs
Pro
Sheet Author
API Scripter
That said, if the attribute in question always equals bpTotal, you could just include a @{bpTotal} reference within the repeating section, and not need a sheet worker.
1568314895
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
GiGs said: That said, if the attribute in question always equals bpTotal, you could just include a @{bpTotal} reference within the repeating section, and not need a sheet worker. Although, autocalc fields do not play well with sheetworkers.
1568317571

Edited 1568317587
GiGs
Pro
Sheet Author
API Scripter
That's true. I was assuming the attribute was being shown just for display purposes. If its going to be used in sheet workers, setting it via sheet worker is the better way to go. I just noticed an error in the original script, that i had also carried over into mine. In  change  statements, attribute names must always be lower case. I fixed my code above. The above worker will be fine if all you want to do is overwrite the bodySegBpTot attribute on every row every time the bpTot value changes.  However, if the attributes in your repeating section can  individually  change, and you need to preserve those changes, the above worker wont do. You'd need something like the one below.  Through the eventInfo object, you can get the value of bpTot when it changes, and also its old value. You can thus calculate how much the value has changed, and apply that to any attributes. One way of doing it looks like this: on("change:bptot sheet:opened", function (eventInfo) { getSectionIDs("repeating_body", function (idArray) { const fieldNames = idArray.map(id => `repeating_body_${id}_bodySegBpTot`); getAttrs(["bpTot",...fieldNames], function (values) { const newValue = parseInt(eventInfo.newValue, 10) || 0; const oldValue = parseInt(eventInfo.previousValue, 10) || 0; const change = newValue - oldValue; const rows = fieldNames.reduce((obj, rowName) => { const rowValue = parseInt(values[rowName]) ||0; obj[rowName] = rowValue + change; return obj; }, {}); if(rows) setAttrs(rows); }); }); });
The first example worked out great. Thanks for the help. I have some follow up questions though. What exactly are you doing with the rows variable here? It looks like you made it blank then set it in the for loop. Is that what’s going on? I’m also confused how the if statement  and setAttrs works at the end.
1568424688
GiGs
Pro
Sheet Author
API Scripter
Okay, this might be a bit mindbending. Let me introduce you to the concept of object variables. Bear with me and I'll get to your question below. An Object can look like this: let values = {         hp: 17,      strength: 12,      ac: 10 } This way of storing data is very handy - it allows you to group a bunch of different attributes and their values in a single object. Each item has a key (like an attribute name) and a value (like that attribute's value). You can get a specific item's value out by using its key like so: let hp_working = values.hp or let hp_working = values['hp']; In fact when you use getAttrs, the sheet worker scans your character sheet, looking for all attributes named in the worker's on(change) line, gets their values, and stores them in this kind of object. And you then get those values out of that object, to use them in the worker. You can also update values in an object, or create new objects. Let's say my sheet worker just updated HP by adding three to it. I  could do this: hp_working = hp_working +3; let values['hp'] = hp_working; or  hp_working = hp_working +3; let values.hp = hp_working; After this, the previous object would now have these values {        hp: 20,     strength: 12,     ac: 10 } because hp has been increased by 3. Now you may recognise that kind of object from setAttrs. When you end a worker, and run the setAttrs function, what you are doing is creating an object of this sort, and roll20 recieves it and updates the character sheet. So I could run setAttrs({        hp: 20,     strength: 12,     ac: 10 }); to update the stats on the sheet. But since that object is also the values object, I could do this too: setAttrs(values); They are identical. So getting back to your question.  With this line let rows = {}; I'm creating an empty Object variable, and with the loop I'm f illing it with data. Notice its the same structure as let values = {        hp: 17,     strength: 12,     ac: 10 } just with nothing inside the brackets. Then with this         for (let i = 0; i < idarray.length; i++) { rows['repeating_body_' + idarray[i] + '_bodySegBpTot'] = bpTotal; } I am looping through each row id, and creating an attribute name of this format repeating_body_ROWID_bodySegBpTot and setting its value to be equal to the bpTotal. Lets say bpTotal was 12, and the row ids were -xyz-001, -xyz-011, and -xyz-111. Then after running that loop, I'll have an object that looks like  {     repeating_body_-xyz-001_bodySegBpTot: 12,     repeating_body_-xyz-011_bodySegBpTot: 12,     repeating_body_-xyz-111_bodySegBpTot: 12 } And now I can save them using setAttrs. Remember thar, since this object is called rows , this setAttrs({     repeating_body_-xyz-001_bodySegBpTot: 12,     repeating_body_-xyz-011_bodySegBpTot: 12,     repeating_body_-xyz-111_bodySegBpTot: 12 }); is the same as setAttrs(rows); Finally, for the statement: Sometimes this sheet worker will run when the repeating section is empty. In this case there are no row ids. In this case, the rows variable will be empty (just a simple { }), so there's no need to run the setAttrs line. if(rows) says "if the object exists and is not empty", so setAttrs only runs when there is something in the repeating section. Does this help?
Yeah, that makes sense. Is there a way to have each repeating section in an object be equal to a different value? I’m trying to adapt this into the following code. on("change:endtot change:repeating_body:bodypercent sheet:opened", function () { getAttrs(["endTot", "repeating_body_bodyPercent"], function (value) { let endTotal = parseInt(value.endTot, 10) || 0; getSectionIDs("repeating_body", function (idarray) { let rowsPercent = {}; let rowsTotalPercent = {}; for (let i = 0; i < idarray.length; i++) { rowsPercent['repeating_body_' + idarray[i] + '_bodyPercent'] = parseInt(value.'repeating_body_' + idarray[i] + '_bodyPercent', 10) || 0; rowsTotalPercent['repeating_body_' + idarray[i] + '_bodySegEndTot'] = Math.round((rowsPercent/100)*endTotal); } if(rowsTotalPercent) setAttrs(rowsTotalPercent); }); }); }); The idea here is each row has a percentage that is multiplied by the total Endurance (End for short), which is a box outside the repeating section, and the result is to be shown in that row. When the endTot and the bodyPercent changes I want it to update. Is this possible using this method or do I have to do it a different way?
1568656712

Edited 1568662693
GiGs
Pro
Sheet Author
API Scripter
You cant do it quite like that. One way is to use two sheet workers. First, for the internal changes. When bodyPercent changes, you dont need sectionIds, you can just update that row alone: on("change:repeating_body:bodypercent sheet:opened", function () { getAttrs(["endTot", "repeating_body_bodyPercent"], function (value) { let endTotal = parseInt(value.endTot, 10) || 0; let rowsPercent = parseInt(value.repeating_body_bodyPercent, 10) ||0; let rowsTotalPercent = Math.round((rowsPercent/100)*endTotal); setAttrs({repeating_body_bodySegEndTot: rowsTotalPercent}); }); }); When endTot changes, you need to change every row, and in this case you need to do getSectionIds before getAttrs. I''l write this script twice, first the more easily understandable way, and the second the more compact way. In case you're not familiar, any text starting with // is a comment and is ignored by roll20. You can delete them when you understand whats happening. on("change:endtot sheet:opened", function () {     getSectionIDs("repeating_body", function (idarray) {         // first we need to get an array of all the relevant row names, so you can getAttrs them         const fieldNames = [];         for (let i = 0; i < idarray.length; i++) {             fieldNames.push('repeating_body_' + idarray[i] + '_bodyPercent');         }         // now need to get the attribute values, along with endTotal. concat allows us to join two arrays.          // putting [ ] around endtotal treats it as an array so concat can be used with it.         getAttrs(fieldNames.concat(["endTot"]), function (values) {             const endTotal = parseInt(value.endTot, 10) || 0;             // lets name the variable for setAttrs something unambigious so it can be confused with anything else:             const output = {};             for (let i = 0; i < idarray.length; i++) {                 // now need to calculate the value. Use variable names that describe what the value actually is.                 const thisRowsPercentValue = parseInt(values['repeating_body_' + idarray[i] + '_bodyPercent'],10) ||0;                 const EndMultipliedByPercent = Math.round((thisRowsPercentValue/100) * endTotal);                 output['repeating_body_' + idarray[i] + '_bodySegEndTot'] = EndMultipliedByPercent;             }             if(output) setAttrs(output);         });     }); }); Note, i used const in place of let, just because we can. const is for variables that dont change, once created. Let is for variables you intend to modify. Both const and let create variables that only exist within the scope. For instance, in that last for loop, each time the loop was entered, a new thisRowsPercentvalue variable was created, and when the end of that loop was reached, it was discarded. So, the above is one way to do it. Here's a bit more compact version that uses some more advanced techniques. on("change:endtot sheet:opened", function () {     getSectionIDs("repeating_body", function (idArray) { // map is a way to essentially loop through an array, as a single operation. Here it replaces the first for loop above. const fieldNames = idArray.map(id => `repeating_body_${id}_bodyPercent`);         // it takes every item in the array (id being the current row id),         //    and performs a conversion on it, and saves the converted items into a new array.         // by the way: `repeating_body_${id}_bodyPercent` is the same as 'repeating_body_' + id + '_bodyPercent' // This is called a string literal and gets very handy when strings get more complicated. // the way of writign strings that you're more familiar with would work fine too.         getAttrs([...fieldNames, "endTot"], function (values) {         // when putting three dots ... before an arrays name, it expands that array into individual elements - so here we // avoid the need to use concat. This ... is called the spread operator.             const endTotal = parseInt(value.endTot, 10) || 0;             const output = {};         // the forEach function is a very useful alternative to using for loops.         //    instead of having to use i and get array[i] for the value //    it loops through the values directly. In the below forEach, each turn through the loop,         //    field is set to the next instance of repeating_body_${id}_bodyPercent (with id filled in already of course)             fieldNames.forEach( field => {                 const thisRowsPercentValue = parseInt(values[field],10) ||0;                 const EndMultipliedByPercent = Math.round((thisRowsPercentValue/100) * endTotal);                 output[`repeating_body_${id}_bodySegEndTot`] = EndMultipliedByPercent;             });             if(output) setAttrs(output);         });     }); });
1568662738
GiGs
Pro
Sheet Author
API Scripter
Note: just noticed i hadnt updated the change: line on the last 2 workers, I've corrected them now.
Sweet! This is making a lot more sense now. Thank you so much! Just to let you know, in your second and third code example the change statement should be endtot instead of repeating_body:bodypercent and you're missing an s on values on the const endTotal line. Might have more questions on the subject as I go on but that's it for now. Again thank you.
1568663527

Edited 1568663589
GiGs
Pro
Sheet Author
API Scripter
Looks like we posted at the same time-  but well-spotted :) I did change value to values, should have checked all references to it. Glad you spotted my deliberate errors put there to test you, honest.