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

[Script Help] Adding values retrieved through getAttrByName

So I'm at a loss here, and I'm sure it is something simple. So I am fetching 2 attributes and want to sum them. However, it seems to be concatenating instead of summing up.  I'm getting the right attribute values of 3 and 2 for attribute1 and attribute 2, but then when I try to calculate totalDice it keeps returning 32 instead of 6. What am I doing wrong? var int;attribute1 = getAttrByName(character.id, attributeName[0]); var int;attribute2 = getAttrByName(character.id, attributeName[1]); var totalDice = attribute1 + attribute2; Thanks!
1676863023
timmaugh
Pro
API Scripter
The additive operator is the same as the concatenate operator, so you have to make sure you're dealing with numbers or it will take its best guess at what you want. You just have to coerce them directly: let totalDice = Number(attribute1) + Number(attribute2);
1676873035

Edited 1676873127
GiGs
Pro
Sheet Author
API Scripter
Also you want to make sure they actually are numbers (null strings "" can be a problem). var attribute1 = +getAttrByName(character.id, attributeName[0]) || 0; var attribute2 = +getAttrByName(character.id, attributeName[1]) || 0; var totalDice = attribute1 + attribute2; Note: Javascript doesnt recognise syntax like this: var int;attribute1 = You dont declare the variable type - JS assigns it behind the scenes, so you do your best to make sure it recognises the variable properly. The + coerces it into a number (it is the same as Number( ) ) - if you specifically want to make sure its an integer, you could do var attribute1 = parseInt(getAttrByName(character.id, attributeName[0])) || 0; var attribute2 = parseInt(getAttrByName(character.id, attributeName[1])) || 0; var totalDice = attribute1 + attribute2; The || 0 will ensure you have a zero value if the parseInt, +/Number(), or getAttrbyName report an error. As Tim says, using the + operator is risky unless you know for sure all variables are number types - otehrwise both variables will be treated as strings and concatenated together.
1676896663
timmaugh
Pro
API Scripter
Good catch, GiGs... I hadn't even noticed the problems with the declarations! Price of answering posts too late in the evening, I guess.
1676897095
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
Also, also; did you mean total should be 5? Or are you intending to multipy? If you mean to multiply, you don't need to do the number coercion.
timmaugh said: The additive operator is the same as the concatenate operator, so you have to make sure you're dealing with numbers or it will take its best guess at what you want. You just have to coerce them directly: let totalDice = Number(attribute1) + Number(attribute2);. Perfect. That worked exactly like I wanted. I knew it was going to be something simple.  GiGs said: Also you want to make sure they actually are numbers (null strings "" can be a problem). var attribute1 = +getAttrByName(character.id, attributeName[0]) || 0; var attribute2 = +getAttrByName(character.id, attributeName[1]) || 0; var totalDice = attribute1 + attribute2; Note: Javascript doesnt recognise syntax like this: var int;attribute1 = You dont declare the variable type - JS assigns it behind the scenes, so you do your best to make sure it recognises the variable properly. The + coerces it into a number (it is the same as Number( ) ) - if you specifically want to make sure its an integer, you could do var attribute1 = parseInt(getAttrByName(character.id, attributeName[0])) || 0; var attribute2 = parseInt(getAttrByName(character.id, attributeName[1])) || 0; var totalDice = attribute1 + attribute2; The || 0 will ensure you have a zero value if the parseInt, +/Number(), or getAttrbyName report an error. As Tim says, using the + operator is risky unless you know for sure all variables are number types - otherwise both variables will be treated as strings and concatenated together. Also good to know. I've had problems with this in the past.  Scott C. said: Also, also; did you mean total should be 5? Or are you intending to multipy? If you mean to multiply, you don't need to do the number coercion. Yep. I meant 5. That is what I write scripts. I can't add without them! Sooooo, since I have you giants of scripting here, perhaps you can assist me with another issue.  This script is designed for World of Darkness rolls that have exploding 10s and count successes, while 1s cancel out the highest successes first, but any 1s rolled on the exploding 10s don't cancel any.  I'm fairly certain I can get the number of successes that I want, but then how do I use that number, and perhaps the results of the dice, in other APIs like Scriptcards or anything else like that? //Version 1.00 //Example Roll Command: !nWODroll Dexterity+Firearms on("chat:message",function(msg){ if(msg.type=="api" && msg.content.indexOf("!nWODroll")==0) { //Must have a token selected var selected = msg.selected; if (selected===undefined) { sendChat("API","Please select a token."); return; };    //selected token  var token = getObj("graphic",selected[0]._id); var character = getObj("character",token.get("represents")); let args = msg.content.split(" "); let attributeName = args[1].split("+"); var attribute1 = getAttrByName(character.id, attributeName[0]) || 0; var attribute2 = getAttrByName(character.id, attributeName[1]) || 0; let totalDice = Number(attribute1) + Number(attribute2); var diceResults = []; for (i = 0; i < totalDice; i++) {   var diceRoll = randomInteger(10);   diceResults.push(diceRoll) }; var successes = 0; var ones = 0; var tens = 0; for (i = 0; i < diceResults.length; i++) {   if (diceResults[i] > 7 && diceResults[i] < 10) { successes++   };   if (diceResults[i] == 1) { ones++   };   if (diceResults[i] == 10) { tens++   }; }; if (tens > 0 && tens > ones) {   for (i = 0; i < tens; i++) { var diceTensRoll = randomInteger(10); if (diceTensRoll > 7 && diceTensRoll < 10) {   successes++ }; if (diceTensRoll == 10) {   tens++ };   }; }; var totalSuccesses = tens - ones + successes if (totalSuccesses > 0){ sendChat("GM",`Your total successes are ${totalSuccesses} for your ${attributeName[0]} + ${attributeName[1]} roll.`) } else { sendChat("GM",`Your total successes are less than 0 and so you have experienced a dramatic failure for your ${attributeName[0]} + ${attributeName[1]} roll.`) }; }; }); Thanks again!
1677019067

Edited 1677094002
timmaugh
Pro
API Scripter
To state the goal: the idea would be to get the dice, count the 1s in the first f(x) = #dice, and subtract that from the successes. A cancelled 10 should not explode. A couple of ideas... First, if you wanted to support inline rolls, you can implement  libInline and let it do some of the work for you. It looks like you want to do the calculations yourself, however, so here's how I'd approach it: const getRollResult = (n, options = {}) => {   let o = {     cancel: 1,     again: 10   };   Object.keys(o).forEach(k => o[k] = options[k] || o[k]);   const roll = (d) => [...Array(d).fill().map(e => randomInteger(10))];   let r = roll(n);   let [c,f,s] = [r.filter(e => e <= o.cancel), r.filter(e => e > o.cancel && e < 8), r.filter(e => e >= 8).sort((a,b) => a > b ? -1 : 1)];   let xs = roll(s.slice(c.length).filter(e => e >= o.again).length);   let xd = [];   while (xs.length > 0) {     xd = [...xd,...xs];     xs = xs.filter(e => e >= o.again);     xs = xs.length ? roll(xs.length) : xs;   } s = [...s,...xd.filter(e => e >= 8)];   return {orig: r, final: [...r,...xd], c: c, f: f, s: s, xs: xs, successes: Math.max(s.length - c.length,0)}; }; Then you could get the roll result for an 8d10 pool using something like: let result = getRollResult(8); You can get the original roll off the returned object: result.orig ...or the number of successes: result.successes You can also pass in different configurations if you want a 9-again roll: let result = getRollResult(8, {again: 9}); Same for an 8-again roll (using 8, instead). Or if you don't want the 1s to cancel, pass in a cancel option that is something less than 1: let result = getRollResult(8, {cancel: 0}); So a 4d10 9-again roll where you don't want the 1s to cancel would be: let result = getRollResult(4, {cancel: 0, again: 9}); Now, as far as plugging that number into ScriptCards... you're starting to get into the realm of metascripts. Plugger is a metascript that can let you run your script at "meta-speed"... and return a value from your command line to the ScriptCards command line. What that value looks like is up to you. You could pass just the successes, or you could pass a string that ScriptCards will know how to parse in order to extract the dice rolled AND the successes. Get your script in the shape you need it in and post back, and I'll show you how to mod your script to work with Plugger as a metascript. "WE'RE COOKIN' WITH THE METASAUCE NOW!"
I think my brain just melted. 
1677098901

Edited 1677099167
timmaugh
Pro
API Scripter
I actually caught an error in the above code and fixed it... as well as a couple of cosmetic changes that were leftover code bloat from when I was testing the code. Here is an annotated version, if it helps. First, this is declared as a function using fat arrow syntax . This would be in your code body so that you could call it once you had determined how many dice to roll with. const getRollResult = (n, options = {}) => { n  will represent the number of dice to roll; options will be an object with settings we can supply to the function. The next part declares an object to establish some default values for our config options.   let o = {     cancel: 1,     again: 10   }; Doing it that way (as opposed to supplying default values to these properties in the function declaration) allows you to pass in a limited number of properties without overwriting the default options completely. In other words, if I'd supplied this info in the function declaration, when you wanted to call the function you could have called it without supplying any  options value (which you can still do in the alternate way I wrote it)... but if you wanted to overwrite a single property (say, to make it a 9-again roll instead of a 10-again), you'd have to supply *all* of the properties... even the default value for the cancel property, which you never meant to alter. You'd have to do that in every call to this function, everywhere in your code. Much better if the object (the function, in this case) is in charge of its own properties. So, now that we have our default config values attached to an object, the next bit of code sets about writing any properties you *did* supply over the top of those established defaults. This next bit says, "For each of the keys of our config object, pass them into a function; in that function, the value of that key ( k ) on our config object ( o ) will be assigned either the value of that key from the options that were passed in (ie, options[k] ) OR what was already defined on our config object (ie, o[k] )."   Object.keys(o).forEach(k => o[k] = options[k] || o[k]); And since the logical OR operator returns the first truthy evaluation, it will either take the property from what you pass (if it exists), or it will take the value from what's already there. (Undefined properties are false-y values, so if the property isn't on your passed object, evaluation proceeds to the next operand.) Next is our actual rolling process abstracted into a function. We needed this in a few different places in the code, so I put it in a function that gives us a single point of maintenance if we need to tweak it down the line.   const roll = (d) => [...Array(d).fill().map(e => randomInteger(10))]; The roll function takes a single argument, d ,  representing the number of dice to roll. The code reads easiest starting with Array() , which will create an array numbering  d entries. We then fill() that (we supply nothing to the function, so they are simply filled to undefined ), and finally we map those undefined entries into new values. The map function can take more arguments than this, but we only need the first, which represents each element, e , we will be mapping to a new value. The value returned from the map operation is the result of the call to Roll20s randomInteger() function... so, a number between 1 and 10. map returns an array.  Then, in deciphering the code, you can back up to the spread operator ( ... ) which is going to feed each element of the returned array into an array constructor (the brackets enclosing it all). This might be overkill, but there are cases where you'll get undefined or null where you'd instead want an empty array, and this sort of syntax solves that problem. In short, the roll function will return an array of numbers between 1 and 10, with the size of the array being whatever you feed it. So let's use it:   let r = roll(n); That's the same n as was supplied to the getRollResults() function initially. Now r represents the array of our initial roll of n dice. It will stay as it is for the entire function. From our base roll, we need to derive our cancels (defaulted to be 1s), our successes (8, 9, or 10), and everything else (the filler). That's what the next bit of code does:   let [c,f,s] = [r.filter(e => e <= o.cancel), r.filter(e => e > o.cancel && e < 8), r.filter(e => e >= 8).sort((a,b) => a > b ? -1 : 1)]; This is an example of destructuring assignment . You can see the above assignment in a simplified form as: [a,b,c] = [1,2,3] So that a = 1, b = 2, and c = 3. Except what we're doing is to filter our initial roll ( r ) in three different ways. In our case, c (for cancels) will be the array resulting from filtering the original roll and testing every element ( e ) to be less than or equal to our config option for cancel . f (for filler) will be everything between the cancel and again config options (again, using an array returned by a filter() function applied to our original roll). And s (for successes) will be what is left when we filter for everything at or above our again option. For s, however, we're going to apply one more operation to the returned array; we're going to sort it in descending order so that the higher success values (ie, 10s) will appear first. Now that we have that sorted, we need to handle explosions.   let xs = roll(s.slice(c.length).filter(e => e >= o.again).length);   let xd = []; In the first line, we declare xs , which will represent our "exploded successes". In the second line, we declare xd , which will hold all of the dice returned from any explosion roll. xs is set to equal the result of the roll() function (an array). The value for the number of dice we want to roll (the size of the array) is arrived at by slicing the s array at a position equal to the number of items in the c array (in other words, subtracting our 1s from our successes). Since slice operates from the left (with a positive value, anyway), and since we already sorted s to have the higher values first, we're losing our highest successes. Then we filter what is left, comparing each item, e , against our again config setting. In plain english, "we drop as many of our highest successes as we have cancellations, then we test what is left to see if anything is above our roll again threshold." Since filter()  returns an array, we get the length to know how many dice should be rolled as exploded successes. Now we have to process the returns to look for recurring explosions:   while (xs.length > 0) {     xd = [...xd,...xs];     xs = xs.filter(e => e >= o.again);     xs = xs.length ? roll(xs.length) : xs;   } The while loop will run as long as there is anything in the xs array, so if there were no exploding successes in the initial roll (eg, no 10s in a 10-again roll), this will be skipped. Our exploding successes were already depleted by any cancellations we had (any 1s), so they've had their effect. The rest of the while loop assigns the contents of the xs array to the xd array (again using the spread operator). Then xs , which has the results of our latest exploded success roll, is reassigned to be the filtered set of values which are equal to or greater than our again setting. We only need the dice which rolled a value which should be rolled again. xs at this point could have no entries, or it could have a number of entries equaling the number of dice we need to reroll, so the last line of active code in the while loop tests that... reassigning to xs the results of a roll() function where we ask for the number of dice we need (that is, the length of the current xs array), or it is assigned to be itself. This is handled with a ternary check . That means that when we cycle back up to our while and our condition is reevaluated, either xs is empty (and we exit the while ), or it has the results of another roll which we need to manage (and we step through the while code again). This might be the step that makes it clearer why we're appending the results of xs onto xd in the while loop. Once we're out of the while loop, we need to append anything in the xd array which is greater than or equal to 8 (the WoD success threshold) to our successes array ( s ):   s = [...s,...xd.filter(e => e >= 8)]; That line, again, uses the spread operator to merge the elements of two arrays. Finally we're done, and we can return our results... but there are a couple of things that happen in the first line, here:   return {orig: r, final: [...r,...xd], c: c, f: f, s: s, xs: xs, successes: Math.max(s.length - c.length,0)}; }; Our final roll is all of the dice rolled originally ( r , which was never changed) as well as all of the dice rolled during any of our explosion rerolls ( xd ). Also our number of successes can't be simply related to our successes array ( s ), since that contains successes that were "cancelled" should any cancel values been rolled in the original roll. Instead, we subtract the number of cancellations (in c ) from our successes (in s ). The math is correct because c never inflates with new values from the while loop... it only ever has the amount from the original roll. Lastly, the Math.max()  function makes sure that if we had more cancellations than successes it will report as a 0 rather than a negative number. Hope that helps!