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

Sheetworker question on( change and sheet

1578704679

Edited 1578704760
vÍnce
Pro
Sheet Author
I've got a bit of code that handles some simple attribute calcs triggered using  change:repeating_weapons,  and this seems to work just fine as long I make a change to an attribute within repeating_weapons.  As it should.  But, I would also like to run this function on  sheet:opened  as well as  change:strength_total  (and a few other non-repeating attributes, but I'll keep it simple for now.)  If I add sheet:opened  and change:strength_total , I get a log error  SHEET WORKER ERROR: You attempted to set an attribute beginning with 'repeating_' but did not include a Row ID or Attribute Name in repeating_weaponseating_weapons_weapon_skill_tot SHEET WORKER ERROR: You attempted to set an attribute beginning with 'repeating_' but did not include a Row ID or Attribute Name in repeating_weaponseating_weapons_weapon_bonus_tot SHEET WORKER ERROR: You attempted to set an attribute beginning with 'repeating_' but did not include a Row ID or Attribute Name in repeating_weaponseating_weapons_weapon_damage_tot I'm guessing this means I need to add some additional js to grab and insert the rowID for the setAttrs?  Is that necessary, if so, can you post an example?  If it's not necessary, how can run these calcs on repeating_weapons if any of my desired events are met?  TIA for any help you can offer. code that works for changes within repeating_weapons /* set weapon bonus and damage totals */ on("change:repeating_weapons", function () { console.log(">>>> Change Detected: Weapon Attacks - recalculating totals <<<<"); getAttrs(['repeating_weapons_weapon_skill', 'repeating_weapons_weapon_skill_tot', 'repeating_weapons_weapon_bonus', 'repeating_weapons_weapon_bonus_tot', 'repeating_weapons_weapon_misc_attack','repeating_weapons_weapon_bonus_max', 'repeating_weapons_weapon_damage', 'repeating_weapons_weapon_misc_damage', 'repeating_weapons_weapon_damage_tot', 'strength_total', 'agility_total', 'marksmanship', 'melee'], function (values) { const weapon_skill = parseInt(values.repeating_weapons_weapon_skill, 10) || 0, weapon_bonus = parseInt(values.repeating_weapons_weapon_bonus, 10) || 0, weapon_misc_attack = parseInt(values.repeating_weapons_weapon_misc_attack, 10) || 0, weapon_bonus_max = parseInt(values.repeating_weapons_weapon_bonus_max, 10) || 0, weapon_damage = parseInt(values.repeating_weapons_weapon_damage, 10) || 0, weapon_misc_damage = parseInt(values.repeating_weapons_weapon_misc_damage, 10) || 0, attribute = (weapon_skill === 0 ? parseInt(values.strength_total, 10) || 0 : parseInt(values.agility_total, 10) || 0), skill = (weapon_skill === 0 ? parseInt(values.melee, 10) || 0 : parseInt(values.marksmanship, 10) || 0), weapon_bonus_limit = Math.min(weapon_bonus, weapon_bonus_max), weapon_skill_tot = attribute + skill, weapon_bonus_tot = weapon_skill_tot + weapon_bonus_limit + weapon_misc_attack, weapon_damage_tot = weapon_damage + weapon_misc_damage; setAttrs({ repeating_weapons_weapon_skill_tot : weapon_skill_tot, repeating_weapons_weapon_bonus_tot : weapon_bonus_tot, repeating_weapons_weapon_damage_tot : weapon_damage_tot }); console.log('>>>> weapon_skill_tot: '+weapon_skill_tot+' weapon_bonus_tot: '+weapon_bonus_tot+' weapon_damage_tot: '+weapon_damage_tot +' <<<<'); }); }); code that throws the log errors (there are a couple of other non-repeating attributes, but I've left them out for now to keep it simple.) /* set weapon bonus and damage totals */ on("sheet:opened change:strength_total change:repeating_weapons", function () { console.log(">>>> Change Detected: Weapon Attacks - recalculating totals <<<<"); getAttrs(['repeating_weapons_weapon_skill', 'repeating_weapons_weapon_skill_tot', 'repeating_weapons_weapon_bonus', 'repeating_weapons_weapon_bonus_tot', 'repeating_weapons_weapon_misc_attack','repeating_weapons_weapon_bonus_max', 'repeating_weapons_weapon_damage', 'repeating_weapons_weapon_misc_damage', 'repeating_weapons_weapon_damage_tot', 'strength_total', 'agility_total', 'marksmanship', 'melee'], function (values) { const weapon_skill = parseInt(values.repeating_weapons_weapon_skill, 10) || 0, weapon_bonus = parseInt(values.repeating_weapons_weapon_bonus, 10) || 0, weapon_misc_attack = parseInt(values.repeating_weapons_weapon_misc_attack, 10) || 0, weapon_bonus_max = parseInt(values.repeating_weapons_weapon_bonus_max, 10) || 0, weapon_damage = parseInt(values.repeating_weapons_weapon_damage, 10) || 0, weapon_misc_damage = parseInt(values.repeating_weapons_weapon_misc_damage, 10) || 0, attribute = (weapon_skill === 0 ? parseInt(values.strength_total, 10) || 0 : parseInt(values.agility_total, 10) || 0), skill = (weapon_skill === 0 ? parseInt(values.melee, 10) || 0 : parseInt(values.marksmanship, 10) || 0), weapon_bonus_limit = Math.min(weapon_bonus, weapon_bonus_max), weapon_skill_tot = attribute + skill, weapon_bonus_tot = weapon_skill_tot + weapon_bonus_limit + weapon_misc_attack, weapon_damage_tot = weapon_damage + weapon_misc_damage; setAttrs({ repeating_weapons_weapon_skill_tot : weapon_skill_tot, repeating_weapons_weapon_bonus_tot : weapon_bonus_tot, repeating_weapons_weapon_damage_tot : weapon_damage_tot }); console.log('>>>> weapon_skill_tot: '+weapon_skill_tot+' weapon_bonus_tot: '+weapon_bonus_tot+' weapon_damage_tot: '+weapon_damage_tot +' <<<<'); }); });
1578719529

Edited 1578727583
GiGs
Pro
Sheet Author
API Scripter
You cant use the shortened repeating section syntax with sheet opened. Rememeber that repeating section attributes each have a full name that looks something like this: repeating_weapons_-THEW64858956_weapon_skill The shorter syntax like this: repeating_weapons_weapon_skill is only for on change events, and only affects a single row of the section. The roll20 engine recognises an attribute in the section has changed, and supplies the row id automatically, so that code becomes the full version behind the scenes. When using sheet:opened,  no row id is supplied, so the code fails. To solve this problem when using sheet:opened,  you need to use getSectionids to get all the row ids, and go through the section updating all rows that need updating. Likewise when using change for a stat outside the repeating section, (e.g. change:strength_total ), you again need to use getSectionids - because the shortened syntax only works for one row at a time. When changing an external attribute, you need to update all  the rows that are affected, so you need to use getsectionids and loop through them. Also youre using the shortened syntax in the setAttrs, which can only change one row at a time, and you probably need to update multiple rows. Again, this needs getSectionids. Its pretty common to have one sheetworker for single row changes (triggered when a repeating_section attribute changes), and a second sheet worker for changes that affect the whole repeating section (sheet:opened, changes to external attributes). You might move the calculation part of the worker into a function which can be called from both sheet workers, to avoid duplicated code.
1578724417

Edited 1578727972
GiGs
Pro
Sheet Author
API Scripter
Here's how I would do it. I would create 4 sheet workers: 1 sheet worker to calculate repeating_weapons_weapon_damage_tot - this attribute is not affected by external factors (strength, agility, etc) and isnt affected by most of the repeating section attributes. It's an independent attribute of all other factors, so a dedicated sheet worker makes sense. 2 sheet workers to calculate  repeating_weapons_weapon_skill_tot repeating_weapons_weapon_bonus_tot when due to changes within the repeating section. There's an argument that you can combine these into one sheet worker. But skill_tot is made up of several attributes that only affect it, and bonus_tot is made from skill_tot and some other attributes that dont affect anything else. So it makes sense to break these out into separate workers to me - especially since these workers only fire on changes within the repeating section and only on one row at a time.  And finally, a sheet worker that fires when the attributes outside  the sheet worker fire - strength, agility, marksmanship, and melee. This goes through the repeating section, and updates skill_tot and bonus_tot for every row. The setAttrs line uses silent: true to stop changes propagating - otherwise changing skill_tot would trigger the above repeating_section worker (#3) and force a slower recalc of all the bonus_tot lines. This sheet worker doesnt touch the damage line, because thats not affected by strength, agility, marksmanship, and melee. const int = (score, on_error = 0) => parseInt(score) || on_error;    // make sure repeating_weapons_weapon_damage_tot isa readonly attribute and no need to use sheet opened. on('change:repeating_weapons:weapon_damage change:repeating_weapons:weapon_misc_damage', function() {     console.log(">>>> Change Detected: Weapon Damage - recalculating totals <<<<");     getAttrs(['repeating_weapons_weapon_damage', 'repeating_weapons_weapon_misc_damage'], function (values) {         const weapon_damage = int(values.repeating_weapons_weapon_damage);         const weapon_misc_damage = int(values.repeating_weapons_weapon_misc_damage);                  const damage = weapon_damage + weapon_misc_damage;         setAttrs({             repeating_weapons_weapon_damage_tot: damage         });         console.log('>>>> weapon_skill_damage: ' + damage);     }); }); // worker to calculate skill_total on("change:repeating_weapons:weapon_skill change:repeating_weapons:weapon_bonus change:repeating_weapons:weapon_misc_attack change:repeating_weapons:weapon_bonus_max change:repeating_weapons: change:repeating_weapons: ", function () {     console.log(">>>> Change Detected: Weapon Attacks - recalculating totals <<<<");     // made the change event specific, so its only fired when needed, and likewise reduced the getAttrs to those only needed     getAttrs(['repeating_weapons_weapon_skill', 'strength_total', 'agility_total', 'marksmanship', 'melee'], function (values) {         const strength = int(values.strength_total),             agility = int(values.agility_total),             melee = int(values.melee),             marksmanship = int(values.marksmanship),             weapon_skill = int(values.repeating_weapons_weapon_skill);         // separate out the basic values read from the sheet from those calculated within the worker. for clarity         const attribute = (weapon_skill === 0 ? strength : agility),             skill = (weapon_skill === 0 ? melee : marksmanship),             weapon_skill_tot = attribute + skill;                      setAttrs({             repeating_weapons_weapon_skill_tot:weapon_skill_tot         });         console.log('>>>> weapon_skill_tot: ' + weapon_skill_tot);     }); }); // worker to calculate bonus_total on("change:repeating_weapons:weapon_skill_tot change:repeating_weapons:weapon_bonus change:repeating_weapons:weapon_misc_attack change:repeating_weapons:weapon_bonus_max change:repeating_weapons: change:repeating_weapons: ", function () {     console.log(">>>> Change Detected: Weapon Attacks - recalculating totals <<<<");     // made the change event specific, so its only fired when needed, and likewise reduced the getAttrs to those only needed     getAttrs(['repeating_weapons_weapon_skill_tot', 'repeating_weapons_weapon_bonus', 'repeating_weapons_weapon_misc_attack',             'repeating_weapons_weapon_bonus_max'], function (values) {         const weapon_skill_tot = int(values.repeating_weapons_weapon_skill_tot),             weapon_misc_attack = int(values.repeating_weapons_weapon_misc_attack),             weapon_bonus = int(values.repeating_weapons_weapon_bonus),             weapon_bonus_max = int(values.repeating_weapons_weapon_bonus_max);         // separate out the basic values read from the sheet from those calculated within the worker         const weapon_bonus_tot = weapon_skill_tot + Math.min(weapon_bonus, weapon_bonus_max) + weapon_misc_attack;                      setAttrs({             repeating_weapons_weapon_bonus_tot: weapon_bonus_tot         });         console.log('weapon_bonus_tot: '+ weapon_bonus_tot +' <<<<');     }); }); // calculate repeating_weapons when outside attributes change on('change:strength_total change:agility_total change:melee change:marksmanship', function() {     getSectionIDs('repeating_weapons', function(ids) {         const fieldnames = [];         ids.forEach(id => {             fieldnames.push(                 `repeating_weapons_${id}_weapon_skill`,                 `repeating_weapons_${id}_weapon_bonus`,                 `repeating_weapons_${id}_weapon_bonus_max`,                 `repeating_weapons_${id}_weapon_misc_attack`,             );         });         getAttrs(['strength_total', 'agility_total', 'melee', 'marksmanship', ...fieldnames], function(values) {             const settings = {};             const strength = int(values.strength_total),                 agility = int(values.agility_total),                 melee = int(values.melee),                 marksmanship = int(values.marksmanship);                              ids.forEach(id => {                 // calculate skill total                 const weapon_skill = int(values[`repeating_weapons_${id}_weapon_skill`]),                     attribute = (weapon_skill === 0 ? strength : agility),                     skill = (weapon_skill === 0 ? melee : marksmanship),                     weapon_skill_tot = attribute + skill;                                  //calculate bonus total                 const weapon_misc_attack = int([`values.repeating_weapons_${id}_weapon_misc_attack`]),                     weapon_bonus = int(values[`repeating_weapons_${id}_weapon_bonus`]),                     weapon_bonus_max = int(values[`repeating_weapons_${id}_weapon_bonus_max`]),                     weapon_bonus_tot = weapon_skill_tot + Math.min(weapon_bonus, weapon_bonus_max) + weapon_misc_attack;                 // no need to calculate damage total, its not affected by external factors                 settings[`repeating_weapons_${id}_weapon_skill_tot`] = weapon_skill_tot;                 settings[`repeating_weapons_${id}_weapon_bonus_tot`] = weapon_bonus_tot;             });             setAttrs(settings, {silent: true});         });     }); }); I realised after writing that I could have checked the bonus_tot and skill_tot against the existing values, and only update them if they have changed. But since you are changing all the attributes at once, it's fast enough you wont notice (unless you have literally hundreds of weapons).-  PS: you could add sheet:opened  to that last worker, but if the three attributes set by these workers are readonly, you shouldnt need it. There should never be a change to the attributes that sheet:opened  would be needed to fix.
1578726000

Edited 1578726019
vÍnce
Pro
Sheet Author
Wow GiGs!  You always deliver way more than any of us can give back. ;-) I think 90% of the sheetworker code in my projects was actually written by you.  lol As always, I'm much appreciated for the help and explanations.
1578728046

Edited 1578728056
GiGs
Pro
Sheet Author
API Scripter
You're welcome :) I had a typo in the name  getSectionIDs , i had typed   g etSectionIDS . I always have to double check that, hehe. Fixed now.
1578728211

Edited 1578728613
vÍnce
Pro
Sheet Author
GiGs said: You're welcome :) I had a typo in the name  getSectionIDs , i had typed   g etSectionIDS . I always have to double check that, hehe. Fixed now. I found that.  ;-) what does this line do? const int = (score, on_error = 0) => parseInt(score) || on_error; Is that a substitute or better method than using parseInt for each attribute? example; strength = int(values.strength_total); instead of  strength = parseInt(values.strength_total, 10) || 0;
1578728983

Edited 1578729163
GiGs
Pro
Sheet Author
API Scripter
Its an alternative  to writing out the full parseInt function every time you want it. Not better - it's functionally identical. It's just a lot quicker Making it a function called  int  saves a lot of typing. You can call it the normal way const stat = int(values.stat); and this will work like the usual parseInt command, and sets a dfault value of 0, when the stat isnt able to be read. You do sometimes need to have a different default value from 0, so you can supply an alternative default value, like const stat = int(values.stat, 1); if you want the value to be 1 on a fail. That second version is the equivalent of typing const stat = parseInt(values.stat) || 1; That's what the on_error parameter in the int function is for. Note that you dont need to include the ,10 part in parseInt - lots of people write parseInt like this: const stat = parseInt(values.stat , 10 ) || 0; But that's legacy syntax - the bold part isn't needed any more, and I'm not sure it ever was needed on roll20. 
1578729136
vÍnce
Pro
Sheet Author
+1
1578730611

Edited 1578730758
GiGs
Pro
Sheet Author
API Scripter
By the way, the three functions I put at the start of every character sheet's sheet workers now are  const int = (score, on_error = 0) => parseInt(score) || on_error; const float = (score, on_error = 0) => parseFloat(score) || on_error; const clog = (text, title = '', color = 'green', style='font-size:12px; font-weight:normal;', headerstyle = 'font-size:13px; font-weight:bold;') => {     let titleStyle = `color:${color}; ${headerstyle} text-decoration:underline;`;     let textStyle = `color:${color}; ${style}`;     let output =  `%c${title} %c${text}`;     if(title) {         console.log(output,titleStyle,textStyle);     } else {         output = `%c${text}`;         console.log(output,textStyle);     } }; int and float are the handy parseInt and parseFloat functions. clog is an alternative to writing console.log all the time, and lets you format the output. If you call it like clog('weapon_bonus_tot: '+ weapon_bonus_tot); it will print out the line in green 12point text, making it stand out and easy to find without having to use <===== ====> type identifiers. And this is all you need to use, but if you want extra options:  If you call it like this clog('repeating weapons','weapon_bonus_tot: '+ weapon_bonus_tot + 'weapon_skill_tot: ' + weapon_skill_tot); it will print it on two lines, with Repeating_weapons as a bigger title, and the rest in smaller green text. If you call it like clog('weapon_bonus_tot: '+ weapon_bonus_tot, '', 'red'); it will print it out as a single line without a title, but in red instead of the default green. (You always need to include something for the second parameter (title) if you want to set a different color, but can use ' ' to set title as empty). The 4th and 5th parameters let you replace the styling for text and title lines, if you want to set your own. I do keep tweaking the clog function - havent found a finalised all-purpose version I'm completely happy with, but this is my current version.
1578730787
vÍnce
Pro
Sheet Author
I like it.  Kind of like "shorthand" code.  I noticed that Chris B did a lot of this on the PF Community sheet.
1578730966

Edited 1578731044
GiGs
Pro
Sheet Author
API Scripter
I wouldnt mind looking at that for ideas - the version on github uses compressed code for the sheet worker that is unreadable without the source files. Edit: oh! i didnt realise the source was linked from the github readme. And thats a LOT of js files, lol.
1578731771
vÍnce
Pro
Sheet Author
LOTS.  There are about 25+ modules of js.  I have to compile with NPM in order to upload to the repo.  I suppose the various js modules are better for such a a huge sheetworker (14k+ lines of js), but for a novice, it's very hard to follow/trace...
1578732690
GiGs
Pro
Sheet Author
API Scripter
To be fair, 14,000 lines of code in a single file would be pretty hard to follow for a novice - even if it was readable! Does NPM have the option to compile the code without minifying? It might be easier to make sense of if you were able to do that.
1578733175
vÍnce
Pro
Sheet Author
The npm commands builds the html file, but puts all the js on one line. I'll have to see if there's an option to leave the js un-minified format.  I think we were hitting some limit in Roll20 before it was modified though...
1578738054
GiGs
Pro
Sheet Author
API Scripter
I wonder if thats because it doesnt recognise the type="text/worker"   bit. VS Code cant format sheet workers properly in html files for this reason. I would try changing the part of the code that creates this line <script type="text/worker"> to just <script> and see if it formats it properly. If it works, you can add the  type="text/worker" manually afterwards. This is just a willd guess of course, not knowing what the process it uses is.
1578775278
vÍnce
Pro
Sheet Author
GiGs said: I wonder if thats because it doesnt recognise the type="text/worker"   bit. VS Code cant format sheet workers properly in html files for this reason. I would try changing the part of the code that creates this line <script type="text/worker"> to just <script> and see if it formats it properly. If it works, you can add the  type="text/worker" manually afterwards. This is just a willd guess of course, not knowing what the process it uses is. PM sent since this has gotten off-topic. ;-)