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

Basic Sheetworker Help Needed

1585703342

Edited 1585705186
As part of developing a new sheet, I'm trying to piece by piece test the code (I'm really new at this). I'm currently at a point where I'm trying to test the getAttrs and setAttrs to get a value from one field (in this case, called attr_athletics_max) and whenever it updates, simply print its value to another field (called "attr_unspent_points). Eventually, once this is working, I'm going to have the value of unspent points be (50-all of the different max fields) and update each time one of the max values is updated.  However, when I put the script onto my sheet and update the athletics_max field, nothing happens. I'm sure its a really stupid error in how I'm using the functions, but I can't seem to find it, and I'm stuck. Can anyone look at the code below and tell me where I'm wrong? <tr>General Abilities</tr> <tr><td><button type="roll" type="roll" value="@{character_name} rolls a [[ 1d6 + ?{How Many Points?|0}]]">Athletics</button></td><td><input class="max" name="attr_athletics_current" type="number" value="0"></td><td><input class="max" name="attr_athletics_max" type="number" value="0"></td></tr> <label>Unspent Ability Points:</label> <input type="text" name="attr_unspent_points"><br> <script type="text/worker"> on("change:athletics_max", function() { getAttrs(["Athletics_max"]), function(values){     setAttrs({unspent_points: Math.floor(values.Athletics_max)              }); }); }); </script>
1585705597

Edited 1585705620
vÍnce
Pro
Sheet Author
Hi Tom, first off, I'm not the best person to give sheetworker advice... ;-) Someone much more qualified will no likely comment very soon. A couple things I noticed; Make sure you remove the duplicate type="roll" attributes in your button html and you also need to give those button's names ie name="roll_athletics".  This will allow your buttons to be called via macros and drag/drop of those buttons to the quickbar if desired. Make auto calculated inputs readonly and I usually assign a value <input type="text" name="attr_unspent_points" value="0" readonly /> Your function appears to include an extra ")", try changing to on("change:athletics_max", function() { getAttrs(["Athletics_max"], function(values){ setAttrs({ unspent_points: Math.floor(values.Athletics_max) }); }); });
1585706325
GiGs
Pro
Sheet Author
API Scripter
I don't have much to add to Vince's excellent advice, but I would suggest getting in the habit of declaring variables and doing your calculations before you do the setAttrs step. So instead of this         setAttrs({ unspent_points: Math.floor(values.Athletics_max) }); Do this         let unspent =  Math.floor(values.Athletics_max);          setAttrs({ unspent_points: Math.floor(values.Athletics_max) }); This is so that when you inevitably have issues and are trying to figure out what's causing them, you can enter a console.log command before the setAttrs, to check the value of the calculation and make sure it is giving the result you want. Like so:         let unspent = Math.floor(values.Athletics_max);         console.log('unspent = ' + unspent);         setAttrs({ unspent_points: Math.floor(values.Athletics_max) }); This will reveal the source of a lot of errors.
Good advice from both of you. That misplaced parenthese was causing the whole problem. And doing all the calculations before I set is going to be really helpful. Oddly, though, because of a quirk in the GUMSHOE system, all skill rolls are done in exactly the same way (except for how many points you spend) so it's actually possible to just use the same function for all skill rolls. Someday, when I get ambitious and set an auto-decrement of the pool for each spend, I'll update the names of the buttons. Do you think such a thing would be best done in a page handler or an api script? I guess my pseudo-code for how a skill check should go is below: On Button Click: set attribute to be used check that attribute is >0 or is scuffling or shooting Ask for point spend Confirm that points are available Make roll Reduce pool for attribute by points spent Report Results of Check
1585707576
GiGs
Pro
Sheet Author
API Scripter
there are two different situations here: If you need to set up a common function in sheet workers, to be used by multiple sheet workers, that's pretty easy. If you want to set up a common function in the html, for roll buttons, that's not really possible. You have to repeat the code. It is sometimes  possible to be a bit clever in how you construct the roll code to minimise repetition, taking advantage of the fact that roll20 macros will replace an attribute call with its contents - so you could put all the repetivive bits in a separate attribute, and just place the attribute call in the macro. To illustrate, lets say you have a roll macro that does this value="&{template:default} {{name=@{character_name} }} {{my roll=[[1d20+@str}}} " and imagine the code is a lot longer. You could set up a hidden attribute named template, like so name="attr_template" value="&{template:default} {{name=@{character_name} }} " Then in your button roll, you could write value="@{template} {{my roll=[[1d20+@str}}}" and that would be a valid roll. Admittedly, its a waste of effort of a macro like this, but there are situations where this approach is very handy. Back to your specific question. It sounds like you are both altering a value on the character sheet, and making a roll. the only way to do that is by using the API. You can put a command to the API as a roll button, but that's not generally recommended because it limits the sheet to Pro users. But if its a sheet for your own use, you can do it freely.
That last bit is EXTEMELY helpful. The functionality that I'm envisioning isn't even in the official Roll20 sheet for GUMSHOE and I wondered why. I think you hit the nail on the head. Thank you so much for letting me know and setting me straight.
1585708332
vÍnce
Pro
Sheet Author
@GiGs Love your tip on reusing code by embedding it within an attribute.  Fiendish. ;-)
I took both your advice and declared variables and checked the console log. I'm trying to use math.sum to add a number of values together that I got from getAttrs, but the console says that's not right. What function should I be using where I used math.sum in the code below? <script type="text/worker"> on("change:athletics_max", function() {       getAttrs(["Athletics_max", "Burglary_max"], function(values){           let unspent = Math.sum(values.Athletics_max, values.Burglary_max);           console.log('unspent =' + unspent);          setAttrs({             unspent_points: Math.sum(values.Athletics_max, values.Burglary_max)          });       });    }); </script>
1585713183

Edited 1585716710
GiGs
Pro
Sheet Author
API Scripter
When pasting code to the forums, its easier to read if you format it as a code block. In the edit window, at the top there's a row of buttons - B, I, U, etc. At the very left is something that looks like a torch. It's a drop down, and Code is in there. Your code would look like on("change:athletics_max", function() {     getAttrs(["Athletics_max", "Burglary_max"], function(values){         let unspent = Math.sum(values.Athletics_max, values.Burglary_max);         console.log('unspent =' + unspent);         setAttrs({             unspent_points: Math.sum(values.Athletics_max, values.Burglary_max)         });     }); }); Three things: I dont think Math.sum is valid. At least, I've never seen it. For two numbers, its easier just adding them anyway. Attributes are stored as text, and javascript is very finicky about this - you need to convert the values into a number to perform math. Finally, since you adding burglary_max, you might want to add that to the change line. Putting that together on("change:athletics_max change:burglary_max", function() {     getAttrs(["Athletics_max", "Burglary_max"], function(values){         let athletics = +values.Athletics_max || 0;         let burglary = +values.Burglary_max || 0;         let unspent = athletics + burglary;         console.log('unspent =' + unspent);         setAttrs({             unspent_points: unspent         });     }); }); A couple of things here:  I defined each value separately. This is better for error checking - you can check the value of each attribute if you need to. This is more typing, but is very important especially when you are new and still figuring things out. You need to be able to look at each element of the script and figure out where the problems are occurring. It also makes it easier when you are doing something to this attributes: in this case I've use this expression: + stat ||0;  to convert the value into a number. the + at the start turns it into a number, if it is possible . The ||0 at the end says OR ZERO - and returns a value of 0 when it is not possible. So if someone somehow entered the value "six", which is a word, that cant be converted directly to a number, so it becomes zero. This avoids the script breaking, and also can be handy to spot errors when you look at the character sheet and can see an attribute isnt being added properly. You can have issues with empty cells, so even with number inputs, if someone deleted the contents, that might cause an error depending on how you use it, since '' isnt a number. For this reason I always put some error handling (like ||0), even when I cant imagine how an input error can happen. Users will find a way! The only time i dont use error handling is when i am testing a script and want to see the error being created - that's useful information when chasing a problem.
Sorry about the code formatting; I'd looked for the option and didn't find it. What you're doing is very similar to using ParseInt(), right? Is there any advantage to one over the other?
1585713972

Edited 1585714066
GiGs
Pro
Sheet Author
API Scripter
No need to apologise. It's easy to miss, or not be able to find that formatting section even if you know it exists. I wasnt sure if your attributes could be decimal so I didn't use parseInt. But yes, parseInt, parseFloat, Number, and the + coercion method all serve the same purpose on roll20 (converting strings to numbers so we can safely do arithmetic) and you should use whichever method you are most comfortable with.
That's great; I feel like I'm almost there. I just have one more question and then I'll leave you alone. I have a whole set of max attribute fields (their class is set to "max" and their names all include "*_max". I want to run this same script whenever any of them are updated. Do I have to hard code a change trigger for each attribute by name, or is there a way to "listen" for any time a field with the "max" class is updated? Hard coding them all seems like a process ripe for error, and inefficient.
1585717035
GiGs
Pro
Sheet Author
API Scripter
There are ways to do this. Unfortunately you cant simply search for changes to all attributes that have _max in their them, the script block can only respond to specific changes you tell it to. There are two main ways to do this, that I know of. One is the method on my universal sheet workers page, where you make a template sheet worker, then run a function that makes a copy of that worker dynamically as many times as needed with the array of attributes you supply. Another is to create a function that does everything inside the sheet worker, and you have separate on(change) lines that listen, then run the function when needed. Assuming all scripts work the same way as this one (two attributes added together), I'll give an example of both approaches. There's an extra consideration: do the max attributes overlap, so they get used for anything else? If so, it's worth stepping back and looking at the big picture. ideally you want to minimise the number of getAttrs/setAttrs calls you make across the whole sheet, and might want to combine several events into one worker. I was going to post examples of both methods I mentioned, but then I realised I dont know what you want to do with the calculation result. In the previous script, they were saved to a stat called unspent_points. If all these pairs are adding to the same unspent_points attribute, they aren't independent and its best to create one big sheet worker to handle it all. So, I need more info:  what are the pairs of the stats? what stat does the addition get saved PS: I noticed the setAttrs line of my last post was incorrect and I've edited it and corrected it.
The attributes aren't in pairs; there are 13 General abilities. The player has a pool of 50 points to spend among them, and my final goal is to have the Unspent points box update to 50 - (the sum of all attributes) whenever any of them are updated (so that players don't have to manually calculate how many points they have left to spend. I've got the summing working correctly with all attributes, now I just needed a way to run that function whenever any of the attributes are changed. For testing, I had it run only on a change in the first one, which seemed to work just fine (see code below and please disregard the variable setting inconsistency) on("change:athletics_max" , function() {       getAttrs(["Athletics_max", "Burglary_max", "Chronal_stability_max", "Disguise_max", "Health_max", "Medic_max", "Preparedness_max", "Reality_anchor_max", "Scuffling_max", "Shooting_max", "Tinkering_max", "Unobtrusiveness_max", "Vehicles_max"], function(values){           let a = parseInt(values.Athletics_max)           let b = parseInt(values.Burglary_max)           let c = parseInt(values.Scuffling_max)           let d = parseInt(values.Shooting_max)           let unspent = (a + b + c + d + parseInt(values.Chronal_stability_max)+parseInt(values.Disguise_max)+parseInt(values.Health_max)+parseInt(values.Medic_max)+parseInt(values.Preparedness_max)+parseInt(values.Reality_anchor_max)+parseInt(values.Tinkering_max)+parseInt(values.Unobtrusiveness_max)+parseInt(values.Vehicles_max))          setAttrs({             unspent_points: (50 - unspent)          });       });    }); Where I have change: athletics_max, should I just copy and paste the whole shebang and make one version for each of the attributes that might change?
1585746399

Edited 1585746427
GiGs
Pro
Sheet Author
API Scripter
You just need to add another change statement along with athletics (notice how I did it for the pairs above). So you do this on("change:athletics_max change:burglary_max change:chronal_stability_max" , function() { and so on, just add an extra change statement for all the attributes on that line. That's all you need to do, but if you want to use more advanced techniques, you can simplify the function by making an array of the attribute names, and then referring to the array in the change, getattrs, and addition lines, like so // first create a function to take an array and output a change string for all of them const buildChanges = (list) => list.map(item => `change:${item.toLowerCase()}`).join(' '); // now define your attributes const arrayOfMaxes = ["Athletics_max", "Burglary_max", "Chronal_stability_max", "Disguise_max", "Health_max", "Medic_max", "Preparedness_max", "Reality_anchor_max", "Scuffling_max", "Shooting_max", "Tinkering_max", "Unobtrusiveness_max", "Vehicles_max"]; // now the sheet worker on(buildChanges(arrayOfMaxes) , function() {     getAttrs(arrayOfMaxes, function(values){         // forEach loops through each item in the array         let unspent = 50;         arrayOfMaxes.forEach(stat => unspent -= (parseInt(values[stat])||0)));         setAttrs({           unspent_points: unspent        });     }); });
That's some beautiful code! I'll refactor mine so that it works that way. Thank you so much, you've helped me out a out a ton!
1585752052
GiGs
Pro
Sheet Author
API Scripter
You're welcome :)