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

Help requested with the repeatingSum sheetworker - "advanced" multi column calculation

1706888114

Edited 1706888197
Olaf
Pro
Sheet Author
This is probably mainly a question for GIG  as it concerns the sheetworker code he developed and has so kindly shared.  Posting it here as perhaps others might have an insight too, and the answer might help other people too. I'm using the repeatingSum sheetworker as shared via the community wiki (  RepeatingSum - Roll20 Wiki  ) to do some adding up of my repeating section. What I'm not sure if it can do, and if so how to do it is the following: I have multiple values that need to be calculated, and each destination value is the result of the multiplication of two values/columns in the repeating section (one armour value and one checkbox). In addition, some other columns are used across all destination values ( "is carried" and "weight"). In the example given of using an array to handle multiple destinations, the value array has one matching input column for each destination. So I'm not sure how to write the executing sheetworker to map two input columns to one destination value for an array of destination values. Is this even possible, and if so, what is the sytax? Thanks in advance for your time reading and any feedback provided! Context of what I'm trying to do: The roleplay system the sheet supports (Harnmaster 3) defines armor as covering one or more locations (out of 16 locations on the body). For each location covered it provides 4 different armour values, indicating protecting against 4 different attack types (blunt, edge, pierce and fire/frost respectively).Each armour item can be worn ( ïs carried" is checked) or just listed in the inventory. When an item is carried, the armour value per protection type needs to be added up  per location.  This looks likes this on the sheet: Input End result: (note the description fields in the screenshot are not generated based on the input, just manually added by the player) Code I've got now: I know this is messy so I was trying to clean it up by merging this into one line if possible: on('change:repeating_armoritems remove:repeating_armoritems', function() { repeatingSum('sk_b', 'armoritems',['armor_sk_bvalue', 'armor_loc_sk','armor_iscarried']); repeatingSum('sk_e', 'armoritems',['armor_sk_evalue', 'armor_loc_sk','armor_iscarried']); repeatingSum('sk_p', 'armoritems',['armor_sk_pvalue', 'armor_loc_sk','armor_iscarried']); repeatingSum('sk_f', 'armoritems',['armor_sk_fvalue', 'armor_loc_sk','armor_iscarried']); repeatingSum('fa_b', 'armoritems',['armor_fa_bvalue', 'armor_loc_fa','armor_iscarried']); repeatingSum('fa_e', 'armoritems',['armor_fa_evalue', 'armor_loc_fa','armor_iscarried']); repeatingSum('fa_p', 'armoritems',['armor_fa_pvalue', 'armor_loc_fa','armor_iscarried']); repeatingSum('fa_f', 'armoritems',['armor_fa_fvalue', 'armor_loc_fa','armor_iscarried']); repeatingSum('nk_b', 'armoritems',['armor_nk_bvalue', 'armor_loc_nk','armor_iscarried']); repeatingSum('nk_e', 'armoritems',['armor_nk_evalue', 'armor_loc_nk','armor_iscarried']); repeatingSum('nk_p', 'armoritems',['armor_nk_pvalue', 'armor_loc_nk','armor_iscarried']); repeatingSum('nk_f', 'armoritems',['armor_nk_fvalue', 'armor_loc_nk','armor_iscarried']); repeatingSum('sh_b', 'armoritems',['armor_sh_bvalue', 'armor_loc_sh','armor_iscarried']); repeatingSum('sh_e', 'armoritems',['armor_sh_evalue', 'armor_loc_sh','armor_iscarried']); repeatingSum('sh_p', 'armoritems',['armor_sh_pvalue', 'armor_loc_sh','armor_iscarried']); repeatingSum('sh_f', 'armoritems',['armor_sh_fvalue', 'armor_loc_sh','armor_iscarried']); repeatingSum('ua_b', 'armoritems',['armor_ua_bvalue', 'armor_loc_ua','armor_iscarried']); repeatingSum('ua_e', 'armoritems',['armor_ua_evalue', 'armor_loc_ua','armor_iscarried']); repeatingSum('ua_p', 'armoritems',['armor_ua_pvalue', 'armor_loc_ua','armor_iscarried']); repeatingSum('ua_f', 'armoritems',['armor_ua_fvalue', 'armor_loc_ua','armor_iscarried']); repeatingSum('el_b', 'armoritems',['armor_el_bvalue', 'armor_loc_el','armor_iscarried']); repeatingSum('el_e', 'armoritems',['armor_el_evalue', 'armor_loc_el','armor_iscarried']); repeatingSum('el_p', 'armoritems',['armor_el_pvalue', 'armor_loc_el','armor_iscarried']); repeatingSum('el_f', 'armoritems',['armor_el_fvalue', 'armor_loc_el','armor_iscarried']); repeatingSum('fo_b', 'armoritems',['armor_fo_bvalue', 'armor_loc_fo','armor_iscarried']); repeatingSum('fo_e', 'armoritems',['armor_fo_evalue', 'armor_loc_fo','armor_iscarried']); repeatingSum('fo_p', 'armoritems',['armor_fo_pvalue', 'armor_loc_fo','armor_iscarried']); repeatingSum('fo_f', 'armoritems',['armor_fo_fvalue', 'armor_loc_fo','armor_iscarried']); repeatingSum('ha_b', 'armoritems',['armor_ha_bvalue', 'armor_loc_ha','armor_iscarried']); repeatingSum('ha_e', 'armoritems',['armor_ha_evalue', 'armor_loc_ha','armor_iscarried']); repeatingSum('ha_p', 'armoritems',['armor_ha_pvalue', 'armor_loc_ha','armor_iscarried']); repeatingSum('ha_f', 'armoritems',['armor_ha_fvalue', 'armor_loc_ha','armor_iscarried']); repeatingSum('tx_b', 'armoritems',['armor_tx_bvalue', 'armor_loc_tx','armor_iscarried']); repeatingSum('tx_e', 'armoritems',['armor_tx_evalue', 'armor_loc_tx','armor_iscarried']); repeatingSum('tx_p', 'armoritems',['armor_tx_pvalue', 'armor_loc_tx','armor_iscarried']); repeatingSum('tx_f', 'armoritems',['armor_tx_fvalue', 'armor_loc_tx','armor_iscarried']); repeatingSum('ab_b', 'armoritems',['armor_ab_bvalue', 'armor_loc_ab','armor_iscarried']); repeatingSum('ab_e', 'armoritems',['armor_ab_evalue', 'armor_loc_ab','armor_iscarried']); repeatingSum('ab_p', 'armoritems',['armor_ab_pvalue', 'armor_loc_ab','armor_iscarried']); repeatingSum('ab_f', 'armoritems',['armor_ab_fvalue', 'armor_loc_ab','armor_iscarried']); repeatingSum('hp_b', 'armoritems',['armor_hp_bvalue', 'armor_loc_hp','armor_iscarried']); repeatingSum('hp_e', 'armoritems',['armor_hp_evalue', 'armor_loc_hp','armor_iscarried']); repeatingSum('hp_p', 'armoritems',['armor_hp_pvalue', 'armor_loc_hp','armor_iscarried']); repeatingSum('hp_f', 'armoritems',['armor_hp_fvalue', 'armor_loc_hp','armor_iscarried']); repeatingSum('gr_b', 'armoritems',['armor_gr_bvalue', 'armor_loc_gr','armor_iscarried']); repeatingSum('gr_e', 'armoritems',['armor_gr_evalue', 'armor_loc_gr','armor_iscarried']); repeatingSum('gr_p', 'armoritems',['armor_gr_pvalue', 'armor_loc_gr','armor_iscarried']); repeatingSum('gr_f', 'armoritems',['armor_gr_fvalue', 'armor_loc_gr','armor_iscarried']); repeatingSum('th_b', 'armoritems',['armor_th_bvalue', 'armor_loc_th','armor_iscarried']); repeatingSum('th_e', 'armoritems',['armor_th_evalue', 'armor_loc_th','armor_iscarried']); repeatingSum('th_p', 'armoritems',['armor_th_pvalue', 'armor_loc_th','armor_iscarried']); repeatingSum('th_f', 'armoritems',['armor_th_fvalue', 'armor_loc_th','armor_iscarried']); repeatingSum('kn_b', 'armoritems',['armor_kn_bvalue', 'armor_loc_kn','armor_iscarried']); repeatingSum('kn_e', 'armoritems',['armor_kn_evalue', 'armor_loc_kn','armor_iscarried']); repeatingSum('kn_p', 'armoritems',['armor_kn_pvalue', 'armor_loc_kn','armor_iscarried']); repeatingSum('kn_f', 'armoritems',['armor_kn_fvalue', 'armor_loc_kn','armor_iscarried']); repeatingSum('ca_b', 'armoritems',['armor_ca_bvalue', 'armor_loc_ca','armor_iscarried']); repeatingSum('ca_e', 'armoritems',['armor_ca_evalue', 'armor_loc_ca','armor_iscarried']); repeatingSum('ca_p', 'armoritems',['armor_ca_pvalue', 'armor_loc_ca','armor_iscarried']); repeatingSum('ca_f', 'armoritems',['armor_ca_fvalue', 'armor_loc_ca','armor_iscarried']); repeatingSum('ft_b', 'armoritems',['armor_ft_bvalue', 'armor_loc_ft','armor_iscarried']); repeatingSum('ft_e', 'armoritems',['armor_ft_evalue', 'armor_loc_ft','armor_iscarried']); repeatingSum('ft_p', 'armoritems',['armor_ft_pvalue', 'armor_loc_ft','armor_iscarried']); repeatingSum('ft_f', 'armoritems',['armor_ft_fvalue', 'armor_loc_ft','armor_iscarried']); }); full sheet code here:  roll20-character-sheets/HarnMaster3/harnsheet.html at master · Roll20/roll20-character-sheets (github.com) (note: this doesn't include weight yet, which is calculated in a separate sheetworker but I'll also merge that in here, I understand how to do that) (note2: if what I've asked is not possible I do realise I can at least condens this into a quarter of the lines by merging each location and calculate the armour values array per location).
1706897964
GiGs
Pro
Sheet Author
API Scripter
repeatingSum is designed for getting just a small number of values. You can use it in the way you have, but are probably better off creating your own getSectionIDs function, and outputing them all as one single worker. One issue with the code you have is that each attribute is calculated independently, so earlier values will be overwritten. This may or may not matter - I'll have a look at the full sheet code in a bit. If the armour in a specific location is simply added together, it seems like it would be fairly simple to have a data object indexed by armour type, and each armour listing weight and locations, then you'd only record the armours you wear and everything gets calculated automatically. This works best if armour types are preset - if users can create their own armours, its more complicated but not insurmountable.
1706898015
GiGs
Pro
Sheet Author
API Scripter
Are you creating a custom mod of the existing sheet?
1706899551
Olaf
Pro
Sheet Author
Hi GiGs, thanks for taking the time to check this out! I created this version of the sheet (a heavy edit from the original that I found in the roll20 repo), I'm the current sheet author. I tried my best to bring it up to standards as described in the sheet building wiki, and redesigning the css with grid layouts etc. But i'll be the first to admit I have very limited coding capabilities. Anyway, I'm working on an update to the sheet to streamline this bit of the sheetworkers, so any help is appreciated! Once done I'll submit a PR so the version in the roll20 repo can be updated. If the armour in a specific location is simply added together, Yes this is exactly the case. All the values for "blunt" for the skull location need to be added up, so that in the Armor values overview (the second screenshot) there is 1 total value in the "blunt" column on the skull line. Etc for each armour value per location. The armours are free form, so both the armour values and the checkboxes can be set (in the first screenshot) as the player desire per entry in the repeating section. The source books provide a baseline standard set, but I fully expect groups to have own homebrew versions, so I can't count on a predefined set. The setup as I currently have it does work as intended (at least, the output matches what it should be according to game rules), but I fully realise calling the repeatingSum 64 times , each with it's own getAttributes and setAttributes, is far from ideal. 
1706900834

Edited 1706904535
GiGs
Pro
Sheet Author
API Scripter
Yes, 64 repeatingSum functions is not ideal :) I can see a way to cut it down to 16, or create a single separate worker. But I have two questions: What happens with the AQ stat when yiou have multiple armours in the same location? Are they added together, do you use the highest, is this ignored, or what? Each repeatingsum line is like this: repeatingSum('sk_b', 'armoritems',['armor_sk_bvalue', 'armor_loc_sk','armor_iscarried']); what does armor_loc_sk represent there? is it needed in the calculation? There are 16 different items, but what do they do?
1706906766
Olaf
Pro
Sheet Author
Thanks again for thinking along! The cutting down to 16 I think I also figured out, per armor_loc_xx I could do a repeating sum, using output arrays for [ xx_b, xx_e, xx_p, xx_f ] . If you can figure out one single worker that would be amazing! The output /end result t is basically the location where the armor sits, which you can see in the second screenshot, the output of all the armour calculations.  Because each armour only covers certain locations, to fit it all in one repeating section where you can input any armour type (even ones you make up on the spot), the armor_loc_xx checkbox indicates of the armour protects that body location. The top line in the second screenshot is sk_b sk_e sk_p and sk_f  (for skull) . Here all the armour protection values, so the B (blunt), E (edge), P (pierce) and F (fire/frost) protection are added up for that location. If an armour doesn't cover the location, because the armor_loc_xx box is not checked, the protection value is 0, which is exactly what's needed (the armour provides 0 protection in that location). You can see this in the first screenshot.: the helmet on the second line provides 6/9/7/4 B/E/P/F protection on Skull and Neck locations, and 0 ( <any value> * 0 from unchecked checkbox ) in the other locations. All armours are added together per location, for each protection type separately. If the destination array goes from sk_b to ft_f , each value in that area is calculated from location_type * armor_loc_location (does the armor cover the location?) * armor_iscarried (is the armor actually worn? ; ps - the iscarried is a bit of a misnomer, that should have been isequipped but was to lazy to adjust that after it was already in use) . So if someone wears say a quilt undershirt providing 5 Blunt protection on the thorax combined with a mail shirt providing 10 blunt protection on the thorax, the total thorax line ( tx_x ) would have 15 in the blunt (B) column.  I hope I explained that clearly :) 
1706906914

Edited 1706907014
Olaf
Pro
Sheet Author
Oh, and the AQ stat can be ignored, that doesn't factor in, at least not in the calculations. In the system there are pre-defined armor types with fixed B/E/P/F values. The AQ (armour quality) indicator would then be a straight bonus to those templates values. So if base for an item is 1/1/1/1 , a +2 item would be 3/3/3/3 . There are exceptions, and limits (an initial value can never be more then doubled by AQ modifier) however and GM's may have homebrew items, so I've decided to let players just directly edit the desired b/e/p/f values, and the AQ column is another manual field that doesn't impact other values. And the armor_iscarried column is "E" in the first screenshot (I did change to Equipped, E for short, in the UI)
1706908952

Edited 1706912653
GiGs
Pro
Sheet Author
API Scripter
Olaf said: The cutting down to 16 I think I also figured out, per armor_loc_xx I could do a repeating sum, using output arrays for [ xx_b, xx_e, xx_p, xx_f ] . It sounds like you've got that method. Olaf said: I hope I explained that clearly :)  Your explanations have been very good - especially for such a complex topic. I'm asking about armor_loc_sk because they seem to be redundant. If an armour prtects only the skull, the 'armor_sk_bvalue', 'armor_sk_evalue', 'armor_sk_pvalue', 'armor_sk_fvalue' already tell you that. Do you need that armor_loc_sk value? I've tested the sheet, and I know how that works now, so I think this question is immaterial.
1706909806
GiGs
Pro
Sheet Author
API Scripter
As an aside, while looking at that sheet weorker, I noticed the one above it, which is very inefficient. You have getAttrs and setAttrs inside a loop of 16 items, which you should avoid. It's based on the same repeating section, so that loop can be pit inside the same worker that will do these armour pieces and the locations array you created at the start will come handy for both: const locations = ['sk','fa','nk','sh','ua','el','fo','ha','tx','ab','hp','gr','th','kn','ca','ft'];
1706914351

Edited 1706939864
GiGs
Pro
Sheet Author
API Scripter
Here's a worker that replaces three of your existing workers. The first two on lines 2480 - 2570, then the armour weight calculation on lines 2089-2093. Before I post the worker: you have a lot of invalid comment lines, like this: <!-- armor load totals /--> That syntax is for HTML, but in sheet workers, you are in JavaScript and the proper form is /* armor load totals */ (for multi-line comments) // armor load totals (for single-line comments) I'm surprised those comments aren't breaking your sheet workers due to invalid syntax. Since they aren't breaking things, there's no urgent need to fix them, but I would do it at some point. If they are all single-line comments, an easy fix is to do a find/replace: "<!--" to "//" Anyway, here is the worker: const int = ( score , fallback = 0 ) => parseInt ( score ) || fallback ; const section_name = ( section , id , field ) => `repeating_ ${ section } _ ${ id } _ ${ field } ` ; on ( 'change:repeating_armoritems remove:repeating_armoritems sheet:opened' , function () {     const locations = [ 'sk' , 'fa' , 'nk' , 'sh' , 'ua' , 'el' , 'fo' , 'ha' , 'tx' , 'ab' , 'hp' , 'gr' , 'th' , 'kn' , 'ca' , 'ft' ];     const damage_types = [ 'b' , 'e' , 'p' , 'f' ];     const section = 'armoritems' ;         getSectionIDs ( `repeating_ ${ section } ` , function ( id_array ) {         const carried = [];         const armor_total = [];         const armor_location = [];         const armor_info = [];         const armour_weight = [];                 locations . forEach ( location => damage_types . forEach ( damage => armor_total . push ( ` ${ location } _ ${ damage } ` )));         id_array . forEach ( id => {             armour_weight . push ( section_name ( section , id , 'armor_wgt' ));             carried . push ( section_name ( section , id , 'armor_iscarried' ));             locations . forEach ( location => {                 armor_location . push ( section_name ( section , id , `armor_loc_ ${ location } ` ));             });             damage_types . forEach ( damage => armor_info . push ( section_name ( section , id , `armor_ ${ damage } ` )));         });                 getAttrs ([... carried , ... armor_total , ... armor_location , ... armor_info , ... armour_weight ], function ( values ) {             const output = {};                         locations . forEach ( location => {                 damage_types . forEach ( damage => output [ ` ${ location } _ ${ damage } ` ] = 0 );             });             id_array . forEach ( id => {                 const equipped = int ( values [ section_name ( section , id , 'armor_iscarried' )]);                 damage_types . forEach ( damage => {                     const armor = int ( values [ section_name ( section , id , `armor_ ${ damage } ` )]);                     if ( armor && equipped ) {                         locations . forEach ( location => {                             const covered = int ( values [ section_name ( section , id , `armor_loc_ ${ location } ` )]);                             output [ ` ${ location } _ ${ damage } ` ] += ( armor * covered );                             // copy the armour for each piece to a hidden attribute for each location                             output [ section_name ( section , id , `armor_ ${ location } _ ${ damage } value` )] = ( armor * covered );                         })                       }                 });                 // calculate armour weight and add it.                 let load_armor_total = 0 ;                 id_array . forEach ( id => {                     const equipped = int ( values [ section_name ( section , id , 'armor_iscarried' )]);                     load_armor_total += ( equipped * int ( values [ section_name ( section , id , 'armor_wgt' )]));                 });                                 output [ 'load_armor_total' ] = load_armor_total ;             });                         setAttrs ( output );         });     }); }); I've done some very rudimentary testing and it seems to work, but if you have any issues, let me know what wasnt working. It could do with some decent documentation, but I don't have time - if you have any questions, ask away.
1706929968

Edited 1706937197
GiGs
Pro
Sheet Author
API Scripter
I deleted the previous comment briefly when I found an error, but I've fixed it and restored it. The sheet:opened entry on the event line is there to facilitate testing (changes occur on any opening of the sheet). You'll want to remove it when you are happy everything is working.
1706938403

Edited 1706982208
GiGs
Pro
Sheet Author
API Scripter
By the way, there's an issue in your carrying capacity sheet worker. It includes this: if ( values . hrcondition4endurance != 0 ) {             var carrycapacity = parseInt ( values [ 'combat_effectivecondition' ]);         } else {             var carrycapacity = parseInt ( values [ 'combat_endurance' ]);         } If those attributes do not exist, Encumbrance will be shown as Infinite.I recommend inserting a default value, something like: if ( values . hrcondition4endurance != 0 ) {             var carrycapacity = parseInt ( values [ 'combat_effectivecondition' ]) || 1;         } else {             var carrycapacity = parseInt ( values [ 'combat_endurance' ]) || 1;         } In this case, if the attributes do not exist, or are zero, a value of 1 will be used instead. Change that default to whatever works best for you. At the top of the sheet worker above, I put a custom parseInt function that you can move up to the first line in your script block, and then use it in any sheet worker. This one line function is what I'm referring to: const int = (score, fallback = 0) => parseInt(score) || fallback; Then you could change the above to if ( values . hrcondition4endurance != 0 ) {             var carrycapacity = int ( values [ 'combat_effectivecondition' ], 1);         } else {             var carrycapacity = int ( values [ 'combat_endurance' ], 1);         }        The standard default value is zero, which you can use like this             var carrycapacity = int(values['combat_endurance']); But whenever you want to set a default value other than zero, put a comma than the number used as default (as in the examples above). It's less typing than the parseInt you do all over the place, and has some basic error checking built in.
1706967330
Olaf
Pro
Sheet Author
Amazing, thanks GiGs! I'll embed the code you provided and do further testing before submitting my pull request to the roll20 master. Also very greatful for the bonus tips. I'll sort out that comment syntax issue, never thought a second about difference between html and the script section. And I'll update the carying capacity worker with your suggestions. I'll also see if I can apply that to other workers, if relevant.
1706971312
GiGs
Pro
Sheet Author
API Scripter
One reason I like the int function and use it in all my character sheets is because it is very common to need to set a default value, to  make sure that a variable is a number (you can sometimes hit a snag from roll20 defaulting to strings) and that sets a default of 0. Plus it requires less writing :)
1706990882

Edited 1707005664
GiGs
Pro
Sheet Author
API Scripter
Anotgher thing you could do is make the armour name addition automatic. This requires a bit of HTML and CSS adjustment too. First, you have this section on the Character tab The armour combined layers are named like "name="attr_skull_layer", neck layer, forearm_layer, etc. Change those to match the locations used in the worker, so name="attr_sk_layer", nk_layer, fo_layer, etc/ And make those elements readonly. Then on the inventory layer, you have a wide box for Armour/Clothing. Add a smaller width item, Short, for short names (limited to 4-6 characters). The large name is almost the same width. Call it something like name="attr_armor_name_short" I changed the CSS for this section from .armorinvengrid , .armorinvengrid .repcontainer > .repitem  {     display : grid ;     grid-template-columns : 1fr 1.5em 2.5em repeat ( 21 , 2em );     align-items : center ;     align-content : baseline ;   } to .armorinvengrid , .armorinvengrid .repcontainer > .repitem  {     display : grid ;     grid-template-columns : 1fr 4em 1.5em 2.5em repeat ( 21 , 2em );     align-items : center ;     align-content : baseline ;   } (changing the grid-template-colums) which gave this appearance You'd probably want to massage the CSS a bit. Now we are ready to alter the sheet worker. It now looks like this: const int = ( score , fallback = 0 ) => parseInt ( score ) || fallback ; const section_name = ( section , id , field ) => `repeating_ ${ section } _ ${ id } _ ${ field } ` ; on ( 'change:repeating_armoritems remove:repeating_armoritems sheet:opened' , function () {     const locations = [ 'sk' , 'fa' , 'nk' , 'sh' , 'ua' , 'el' , 'fo' , 'ha' , 'tx' , 'ab' , 'hp' , 'gr' , 'th' , 'kn' , 'ca' , 'ft' ];     const damage_types = [ 'b' , 'e' , 'p' , 'f' ];     const section = 'armoritems' ;         getSectionIDs ( `repeating_ ${ section } ` , function ( id_array ) {         const carried = [];         const armor_location = [];         const armor_info = [];         const armour_weight = [];         const short_names = [];                 id_array . forEach ( id => {             armour_weight . push ( section_name ( section , id , 'armor_wgt' ));             carried . push ( section_name ( section , id , 'armor_iscarried' ));             locations . forEach ( location => {                 armor_location . push ( section_name ( section , id , `armor_loc_ ${ location } ` ));             });             damage_types . forEach ( damage => armor_info . push ( section_name ( section , id , `armor_ ${ damage } ` )));             short_names . push ( section_name ( section , id , 'armor_name_short' ))         });                 getAttrs ([... carried , ... armor_location , ... armor_info , ... armour_weight , ... short_names ], function ( values ) {             const output = {};                         locations . forEach ( location => {                 damage_types . forEach ( damage => output [ ` ${ location } _ ${ damage } ` ] = 0 );             });             id_array . forEach ( id => {                 const equipped = int ( values [ section_name ( section , id , 'armor_iscarried' )]);                 damage_types . forEach ( damage => {                     const armor = int ( values [ section_name ( section , id , `armor_ ${ damage } ` )]);                     if ( armor && equipped ) {                         locations . forEach ( location => {                             const covered = int ( values [ section_name ( section , id , `armor_loc_ ${ location } ` )]);                             output [ ` ${ location } _ ${ damage } ` ] += ( armor * covered );                             // copy the armour for each piece to a hidden attribute for each location                             output [ section_name ( section , id , `armor_ ${ location } _ ${ damage } value` )] = ( armor * covered );                         });                       }                 });                 // calculate armour weight and add it.                 let load_armor_total = 0 ;                 id_array . forEach ( id => {                     const equipped = int ( values [ section_name ( section , id , 'armor_iscarried' )]);                     load_armor_total += ( equipped * int ( values [ section_name ( section , id , 'armor_wgt' )]));                 });                 output [ 'load_armor_total' ] = load_armor_total ;                                 // calculate the name string                 const layers = {};                 locations . forEach ( location => layers [ location ] = []);                 id_array . forEach ( id => {                     const equipped = int ( values [ section_name ( section , id , 'armor_iscarried' )]);                     if ( equipped ) {                         const short_name = values [ section_name ( section , id , 'armor_name_short' )];                         locations . forEach ( location => {                             const covered = int ( values [ section_name ( section , id , `armor_loc_ ${ location } ` )]);                             if ( covered ) {                                 layers [ location ]. push ( short_name );                             }                         });                     }                 });                 locations . forEach ( location => {                     const layer = layers [ location ];                     if ( layer . length ) {                         const layer_string = layer . join ( ' + ' );                         output [ ` ${ location } _layers` ] = layer_string;                     } else {                         output [ ` ${ location } _layers` ] = '';                     }                 });             });                         setAttrs ( output );         });     }); }); This worker reads the existing short names, and combines them on the player page wherever the character is wearing them. Armour thats not equipped isn't listed. You can see in my screenshot some very rudimentary armour names (purely for testing)! You didnt ask for this, but it seems like a sensible addition to me, saving players some work :)
1707007124

Edited 1707007196
vÍnce
Pro
Sheet Author
GiGs said: One reason I like the int function and use it in all my character sheets is because it is very common to need to set a default value, to  make sure that a variable is a number (you can sometimes hit a snag from roll20 defaulting to strings) and that sets a default of 0. Plus it requires less writing :) Apologies Olaf/GiGs for a quick side-trek js question... are these equivalent/acceptable? const covered = int ( values [ section_name ( section , id , `armor_loc_ ${ location } ` )]); const covered = + values [ section_name ( section , id , `armor_loc_ ${ location } ` )] || 0; Thanks
1707014845

Edited 1707015003
GiGs
Pro
Sheet Author
API Scripter
No, Vince. There are two main types of number in JavaScript, Integers (which are whole numbers), and floating point numbers (which contain decimal points, and can be, for example, 0.03). The first of those examples you list creates integers, the second creates floating point numbers. For floating point numbers you can use any of these: const covered = parseFloat(values[section_name(section, id, `armor_loc_${location}`)]) || 0; const covered = Number(values[section_name(section, id, `armor_loc_${location}`)]) || 0; const covered = +values[section_name(section, id, `armor_loc_${location}`)] || 0; The last two are identical (+score is a shorthand for Number(score)). There are some subtle technical differences between Number and parseFloat, but for our purposes they can be treated as identical. People generally use the + form because its faster to type - not because of any inherent superiority, but because its easy. If you need a number with decimals, use +; if you need whole numbers use parseInt (or my int function, which is using parseInt).
1707018733
vÍnce
Pro
Sheet Author
Thank you.  I think I've been using "+values" for whole numbers... If I know a value is a decimal I use parseFloat.  I'll make sure to swap out those non-decimal calls from +values to parseInt
1707021709

Edited 1707021876
GiGs
Pro
Sheet Author
API Scripter
One problem with floating point numbers, you can't control how many decimale places a number will have. You could get 1, 1.35, 1.356789, and worse. If you only want to show a specific amount of decimal places (common for weights and money), you can do this: // a function to convert a number to specifical decimals const dec = (score, decimals = 0) => +score.toFixed(decimals); Place this function at the start of your script block, then when you want to show money, you can do const money_i_have = dec(calculation, 2); With calculation being any valid calculation, or the result of a calculation, and dec being short for decimals or decimal places. Your numbers should be floating point numbers, and you dont need to worry aout all those extra decimal places that usually crop up. It'll be rounded to the nearest. By default, the .toFixed() function converts numbers to a string. The + there converts it back into a number.
1707073473
Olaf
Pro
Sheet Author
GiGs said: Anotgher thing you could do is make the armour name addition automatic. This requires a bit of HTML and CSS adjustment too. First, you have this section on the Character tab  <snip> You didnt ask for this, but it seems like a sensible addition to me, saving players some work :) I like that a lot, thanks for thinking along. This had crossed my mind when I first rebuilt the sheet, but couldn't execute it due to my limited coding knowledge. Now that you have provided the tech, I do realise I need to think this through more: The sheet is currently in use in multiple groups, and I don't know how people are using those armour text fields on the character page. If I roll out this change blindly I might be overwriting people's creativity. My first thought is to add an extra field, and then a setting that people can toggle to choose between manual name writing vs autocalculation (both fields will exist in parallel, the setting will determine which of the pair is shown on the sheet via css). I'll leave that for a future update, I'll finish my testing tonight so I can push this update to the master first, that will speed up the sheet. 
1707075718

Edited 1707675430
GiGs
Pro
Sheet Author
API Scripter
One of the advantages of renaming the fields was, the original fields will still exist they just aren't displayed on the character sheet. You can tell people to look at the Attributes & Abilities tab - they;ll be in that big list there, if they actually want to look back at the old values. I did think about having some kind of component to grab the existing data and transfer it to the new short name attribute, but that would require some work and rely on everyone using that box in a consistent way, which won't be the case. Better to let them find the data if they still need it in the Attributes tab - but most people probably don't want to. You could do as you suggest, and have toggle to switch between the old way and short name way. That's a nice compromise, and allows people to choose which they prefer, or to easily see what they have already used.
1707077125
Olaf
Pro
Sheet Author
Thanks for the feedback, I'll play around with that. I realise I hadn't looked at your code closely enough yet to see you had already used a new variable name :) One thing to quickly check in on, I see you've also used your int function for the armourweight: load_armor_total += (equipped * int(values[section_name(section, id, 'armor_wgt')])); Armour weight can have decimals thought (only 1, but that's more than none :) ) so I can't use integers there.  I assume I can adjust this specific line to load_armor_total += (equipped * parseFloat(values[section_name(section, id, 'armor_wgt')])); Should I set a fallback there (0) in case roll20 gives back a string?
1707077684
GiGs
Pro
Sheet Author
API Scripter
Yes, you can use parseFloat. I always set a fallback, every single time. In this case, you shouldn't need to, but it i always safer. That would look like load_armor_total += (equipped * (parseFloat(values[section_name(section, id, 'armor_wgt')]) || 0)); Notice the placement of the brackets. The || 0 fallback needs to be inside the same brackets as parseFloat.
1707079089
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
I generally use just the +something . I don't think I've ever run into an issue where the user entering a decimal number when I expect a whole number would cause an issue, and it just reads better to me (completely personal preference on that). That said, something that is important to note is that while parseInt returns integers and parseFloat returns decimal numbers, they are all still of the JS type number . From that perspective, parseInt can really be looked at as a simple "round down" function, whereas parseFloat (and the shortcut of +) will return the number version of exactly what the user entered in the input. E.g.: These are identical parseInt("1"); // => 1 parseFloat("1"); // => 1 These are different: parseInt("1.5"); // => 1 parseFloat("1.5"); // => 1.5
1707165843
Olaf
Pro
Sheet Author
The code changes (without the auto combine strings for now) has been merged, sheet feels "snappier" again :) Thanks again @GiGs !
1707178915
GiGs
Pro
Sheet Author
API Scripter
Nice!
1707181378

Edited 1707182765
GiGs
Pro
Sheet Author
API Scripter
Scott C. said: I generally use just the +something . I don't think I've ever run into an issue where the user entering a decimal number when I expect a whole number would cause an issue, and it just reads better to me (completely personal preference on that). I'm not entirely consistent here, but the rule I try to follow is: use parseInt whenever both of these conditions are true: An integer is needed, and The player can manually change the value Because people using the sheet can make typos (and sometimes try to break things deliberately!). The rest of the time I use +. There is also value in a function like int (and a companion function for floating point numbers), for when people try to do multiple operations on the same line, like const test = +values.one || 0 + +values.two || 0; People don't always realise you have to put them in separate brackets (to isolate the || 0 tests), like const test = (+values.one || 0) + (+values.two || 0); Having a function like int (or the float equivalent) does that for them automatically const test = int(values.one) + int(values.two);