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

capturing dice rolls

I will start this message by saying I barely know what I am doing. I am trying to learn to code javascript through a combination of free online tutorials and information about the roll20 api.&nbsp; So if you are inclined to help, don't think you will be talking beneath my level to get it through my head... you can't talk beneath my level.&nbsp;&nbsp; That being said - I can't figure out how to capture a dice roll. This is what I think is my closest attempt yet, at least it doesn't fill up the output console with errors. on("ready",function() { &nbsp; &nbsp; on("chat:message",function(msg){ &nbsp; &nbsp; &nbsp; &nbsp; if(msg.type=="api" &amp;&amp; msg.content.indexOf("!Sacred Geometry")==0) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; var x = msg.content.charAt(16); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; var y = msg.content.charAt(17); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; var z = msg.content.charAt(18); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; var b = y + z; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; var maxRolls = (+b) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; var spellLevel = (+x) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; var newRolls = msg.inlinerolls; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; sendChat("API", newRolls) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; sendChat("API", "maxRolls="+maxRolls*spellLevel); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; for (let i = 1; i &lt;= maxRolls; i++) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; sendChat("API", "Sacred Geometry roll: [[1d6]]" ); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; var newRolls = msg.inlinerolls; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; sendChat("API", "captured: " + newRolls) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; }); }); But in the chat, it shows all the attempts to capture the rolls occur first, then it shows all the rolls. Can anybody straiten this newb&nbsp; scripter out? API: maxRolls=15 captured: undefined captured: undefined captured: undefined captured: undefined captured: undefined API: Sacred Geometry roll:&nbsp;<span class="inlinerollresult showtip tipsy-n-right fullcrit" original-title=" Rolling 1d6 = ( 6 )" style="box-sizing: content-box; background-color: rgb(254, 246, 142); border: 2px solid rgb(63, 179, 21); padding: 0px 3px; font-weight: bold; cursor: help; font-size: 1.1em;">6 Sacred Geometry roll:&nbsp;<span class="inlinerollresult showtip tipsy-n-right" original-title=" Rolling 1d6 = ( 2 )" style="box-sizing: content-box; background-color: rgb(254, 246, 142); border: 2px solid rgb(254, 246, 142); padding: 0px 3px; font-weight: bold; cursor: help; font-size: 1.1em;">2 Sacred Geometry roll:&nbsp;<span class="inlinerollresult showtip tipsy-n-right fullfail" original-title=" Rolling 1d6 = ( 1 )" style="box-sizing: content-box; background-color: rgb(254, 246, 142); border: 2px solid rgb(179, 21, 21); padding: 0px 3px; font-weight: bold; cursor: help; font-size: 1.1em;">1 Sacred Geometry roll:&nbsp;<span class="inlinerollresult showtip tipsy-n-right fullfail" original-title=" Rolling 1d6 = ( 1 )" style="box-sizing: content-box; background-color: rgb(254, 246, 142); border: 2px solid rgb(179, 21, 21); padding: 0px 3px; font-weight: bold; cursor: help; font-size: 1.1em;">1 Sacred Geometry roll:&nbsp;<span class="inlinerollresult showtip tipsy-n-right fullfail" original-title=" Rolling 1d6 = ( 1 )" style="box-sizing: content-box; background-color: rgb(254, 246, 142); border: 2px solid rgb(179, 21, 21); padding: 0px 3px; font-weight: bold; cursor: help; font-size: 1.1em;">1
1677689791
timmaugh
Pro
API Scripter
Hey, Melvin... welcome to the world of scripting! I think you need to understand that your message object isn't guaranteed to have an inlinerolls property, and I think you also have to keep straight which message is doing what in your chain of events... Your function is going to watch the chats that come through for something that begins: !Sacred Geometry Then it is going to take the next 3 characters as variables... so your initial command to this script will probably look something like: !Sacred Geometry123 (note: when you understand why your script isn't working as you intended, I'd suggest reading this post , especially the link for command line construction ... it will save you headaches down the line!) This command line has no inline rolls, so when you try to reference msg.inlinerolls, you're going to get undefined. var newRolls = msg.inlinerolls; sendChat("API", newRolls) Later, you use the three variables you pulled in order to dispatch messages: &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; for (let i = 1; i &lt;= maxRolls; i++) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; sendChat("API", "Sacred Geometry roll: [[1d6]]" ); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; var newRolls = msg.inlinerolls; Those messages go out, and they DO include inline rolls (a single inlineroll). You also redefine your newRolls variable (more on that in a minute), but you assign it BACK to the same msg object... at this point in your code, msg still points to the original message object you received and from which you derived your 3 variables, and which doesn't have inline rolls. It will still be undefined. The messages you send from within your for() block are not being caught by your script... they're not prepended with an "!", so they don't match this scripts initial test for whether to take action. DO NOT PUT AN "!" IN FRONT OF THEM, YET! Sorry... had to make sure, there. If you turn them into bangsy messages (with the exclamation point), you will either set off an infinite loop -- the script will keep catching its own messages and firing off new ones -- or you will generate an error because you don't sanitize the characters at positions 16, 17, and 18 before you try to do math on them. I'm not sure if you need the extra step of dispatching the secondary messages, but if you do, you will need to catch THOSE messages to know what the rolls are. You can either catch them by using the callback version of sendChat or by dispatching them with a handle that you know to listen for (perhaps with another listener function).... so your current listener (your "on('chat:message')") is listening for "!Sacred Geometry" and then sending messages that begin, "!GeometryResult". Your second listener would be listening for "!GeometryResult", and looking at the inlinerolls contained in *that* message. Make sense? Also, a couple of things to think about that will also save you headaches down the line: ...unless you have a good reason to use "var", use "let" or "const" for your declarations. In my time coding on Roll20, there has only been one case where "var" has proven necessary. 999 times out of 1000 you will want let or const. ...unless you have a good reason, use strict equality (the triple-equals) rather than loose equality (the double-equals). ...I would suggest a different line logic for your script... removing the space in "Sacred Geometry" and also not grabbing the characters at positions 16-18. Read the command line construction link I referenced, above, for ideas on ways to put markers in your line to help locate and identify your data. Even just putting spaces between the three variables would let you go from single-digit values to larger multi-digit numbers (you'd split the string on whitespace, leaving you with the numbers -- no matter how many digits they were).
Thank you so much for responding. I'm going to have to read this a few times to get it, but I think I will understand it eventually. The point that my msg object is still going to the original chat message is very helpful - and very obvious now that you pointed it out.&nbsp; What I have googled and googled for is an example of a command that first rolls a die that will show up in chat, then catches that result and assigns it a variable.&nbsp; Can you show me how to do that? If so you will be my personal hero for say... 1 hour? I can negotiate on the duration.
1677700175

Edited 1677700201
GiGs
Pro
Sheet Author
API Scripter
Melvin the Mediocre said: What I have googled and googled for is an example of a command that first rolls a die that will show up in chat, then catches that result and assigns it a variable.&nbsp; That's harder than you might expect (or at least less straightforward). Before anyone (Tim?) suggests a solution, I think it would be best for you to say how you intend to use the dice roll variable. People might spot a better way to do what you want to do.
Short answer - I'm going to load them into an array and do a bunch of math with them, so in the loop I started above it should end up something like: newRolls[i] = msgRoll.inlinerolls; Ultimately, just to make myself do code I'm hoping to see if I can write a program in roll20 to do the math in this feat . But since I am stuck on getting the rolls into a variable, I'm stuck stuck.
I could just as easily make the roll inside the call to the script, so I would enter into chat something like !SacredGeometry4[[5d6]] And I know &nbsp;I have seen examples of how to pull the dice out, but I can not find it again.
1677708434
timmaugh
Pro
API Scripter
*sigh* I had a much longer post about working through this command line... with much annotation and links to places to learn more things... and I lost it with a browser refresh. I will try again later tonight.
Sorry bro - I feel your pain. You've helped me before, and I really appreciate all you do for this community. In this instance, I think I'm going to throw in the towel and look for some easier projects to train myself with.
1677796976

Edited 1678198540
timmaugh
Pro
API Scripter
Melvin the Mediocre &nbsp;said: What I have googled and googled for is an example of a command that first rolls a die that will show up in chat, then catches that result and assigns it a variable.&nbsp; Can you show me how to do that? If so you will be my personal hero for say... 1 hour? I can negotiate on the duration. My hero-dom is bookable in 4 hour blocks, minimum. =D What you're going to run into in what you described (the roll hitting chat AND a script working with it) is that when you send a message through chat, you're sending one of 2 types: a normal/flat message that hits the chat output, or a message intended for the Script Moderator which does not hit the chat output. Scripts can listen for both kinds of messages and take action based on them, however in the case of a normal message, anything a script would do would come separately (and after the message hit the chat output). CustomizableRollListener is one script that listens to normal messages in order to take action, but I believe it is checking for a particular kind of roll template that clues it in that there will be rolls. You won't have that luxury. There are dozens and dozens of rolls made during a game; how will your script know which to act on? That's why most scripts operate on a script handle (like yours is looking for "!Sacred Geometry"), so that they know "this message is intended for me and I should take this/that action." However, relying on a script handle and a bangsy message (prepended with an "!"), nothing hits the chat automatically, so it's up to your script to construct the output (ie, if you want the roll/hover tip with the roll result). This can be done, but it's a bit of a pain. It's much easier to use libInline and let it construct the roll tip for you, and then your script just inserts it where it needs to be. That's also a bit down the road for you. Right now you just want to capture the rolls, so let's start there. Where Rolls Come From Your rolls will either be in your command line, or you will need to build your equation for the roll and then use the sendChat callback to issue/catch a message with the inline roll in its command line. I'll try to give an example of each... First, I would suggest your command line logic gets a bit stiffer. Currently you have a space in your script handle, and your three variables are hard-coded to appear in positions 16, 17, and 18. Not only are they therefore limited to a single digit, but you have to remember what order to supply them. That's not ideal. Let's try for a command line more like this: !sacredgeometry [[4d6]] [[3d6]] ...for the example of getting rolls from your command line, and more like this: !sacredgeometry --level=2 --roll1=4 --roll2=3 ...for building the roll equation from your supplied arguments and then using the sendChat callback. Before we split into 2 paths, validating the script handle will be the same for both approaches, so let's do that: on("ready",function() { &nbsp; on("chat:message",function(msg){ &nbsp; &nbsp; if(msg.type!=="api" || !/^!sacredgeometry\b/i.test(msg.content) return; I prefer to save white-space indenting for the rest of the code by reversing the check, here. Look for a reason to exit and then return. After that, your lines of code can continue at a normal indentation. I used a regular expression (you can play with it here ), but if you're not comfortable with that, you can still use your indexOf() method, or even a startsWith() method... just make sure you negate it (as in, "does not start with") so that you're looking for a reason to exit. Now let's parse the line and get the rolls. Rolls in the Original Command Line When you send rolls through the chat, your command line will change, removing the roll formation and leaving behind the roll index marker. So this command line: !sacredgeometry [[4d6]] [[3d6]] ...becomes !sacredgeometry $[[0]] $[[1]] You don't know how convoluted or complex the roll equation was for that roll -- someone could have homebrewed a method for arriving at the number of dice that took into account averages of attributes from a character, turned that into a number of d2, then used that result as the number of d6 to roll... You. Don't. Know. What you do know is that however many nested rolls and operations are in that position, the roll marker returned from Roll20 parsing will represent the outer-most roll (the one that would report to the chat if you just sent it through as part of a normal message). So if the roll had 10 nested rolls, it would appear to be $[[10]] (the index is zero based, so this is actually the 11th roll). The index of the roll marker is important because if an inline roll is included in the command line of the message, your message object will have an inlinerolls property, which will be an array of all of the rolls included in the message. This array is also zero-based, so your roll marker tells you exactly which roll index you need from the array for this position in the command line. For this method, where you're expecting inline rolls in the command line, you'll want to make sure they're there. I'm showing a command line with multiple rolls (and honestly, if as a scripter I'm going to allow multiple rolls like this supplied to my script, without argument names, it's only because I'm going to treat every roll the same -- ie, I'm going to try to make sure the user doesn't have to remember the order you'd enter the rolls; if the rolls would be used differently, I'd include the formatting and steps I'll discuss with the alternate command line... parsing arguments by name). Even though I show multiple rolls, your need is just to make sure that there is at least 1 roll there. So our next step would be to test if the inlinerolls property is there: if (!(msg.inlinerolls &amp;&amp; msg.inlinerolls.length)) { &nbsp; // handle bad message syntax here... sending a notification to the user... &nbsp; return; } If we survive past that, we're ready to parse our line. let args = msg.content.split(/\s+/).slice(1); That says we'll split the command line on whitespace. split() returns an array, and the first (0 th ) entry of the array will be the script handle, which we don't need, so we finish that line by using slice() to get everything after the 0 th item in the array. Now we have an array of one or more things that may, or may not, be rolls... so let's sanitize them by filtering for only those that match a regular expression of what a roll marker should look like. This can happen as a new assignment back to the args object, or it can happen tagged right onto the back of the slice() from the previous step (slice() returns an array, and filter() requires an array). I'll do that, and reformat the line since line breaks won't affect it: let args = msg.content .split(/\s+/) .slice(1) .filter(a =&gt; /^\$\[\[\d+]]$/.test(a)); Now one more step... if we tweak that regular expression to capture the digits between the double brackets, we can use it to pull the correct roll from the inlinerolls property. We can do that in a map operation. Again, we can chain that on the back end of filter() (since filter() returns an array and map() requires an array): let args = msg.content &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;.split(/\s+/) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;.slice(1) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;.filter(a =&gt; /^\$\[\[\(d+)]]$/.test(a)) .map(a =&gt; msg.inlinerolls[/^\$\[\[(\d+)]]$/.exec(a)[1]]); That one is a bit of a doozy. To read/understand it, start with the regex exec-ing on a (which is our individual argument under evaluation in every iteration). Most times you'll see people use exec and assign it to an object, then test if the object is "undefined" (meaning the regex didn't match): let result = rx.exec(a); if (result) { &nbsp; // there is a result, it was a good match } else { &nbsp; // there was no match, result is undefined } In our case, we know the regex will find a match, because we just filtered for the args that matched the same regex. We also know that the first matching group (represented by [1] ) will be the digits we are looking for. All of that is enclosed by brackets attached to the msg.inlinerolls property: msg.inlinerolls[ ... ] msg.inlinerolls is an array. We can get an n th element using [n]. We use the digits extracted by the regex to pull the correct roll, and the map operation remaps this entry in the args array to be the roll object. Make sense? Now you have an array of your inline rolls, you can do what you like with them. This bit of code will be very helpful to you in this endeavor, as inlinerolls can be nested pretty deep: args.forEach((a,i) =&gt; log(`Roll ${i}: ${JSON.stringify(a, undefined, 2)}`)); Rolls Built from Command Line Components The other way of going about getting rolls would parse command line elements to let you build your own roll, which you then pass to the Roll20 parser through a sendChat(). Before I show that method, I should point out that unless you specifically want to display the roll as an inline roll in your scripts output (ie, with a hover roll tip), you can get an array of random numbers much more easily. The Roll20 randomInteger() function is, as I understand it, also tied into the quantum roll server algorithms to produce better randomized results... so if you know how many dice and of what type, you can just call it. A quick way to get an array is by using Array(), then fill(), then map(). This will ensure an array that has independent/unique elements. To get an array of 6 items would be: let dice = Array(6); To get the slots able to accept data, however, we have to initially fill them: let dice = Array(6).fill(); You can fill with whatever you want, but supplying nothing in this case is fine (it will use "undefined"). Next we will map each entry (e) into a new random integer. We'll pretend we're looking for d10 results, so we'll use a 10 in the randomInteger() function: let dice = Array(6).fill().map(e =&gt; randomInteger(10)); OK, so imagining a command line of: !sacredgeometry --level=2 --roll1=4 --roll2=3 ...where level is your spell level and roll1 and roll2 are the two components that work together to produce the maximum number of rolls, we just have to parse our line to isolate those arguments. We'll split on whitespace-double-hyphen first: let args = msg.content.split(/\s+--/); That will give us an array of entries like: level=2 roll1=4 roll2=3 So we see we could then map() those entries into a form where we've split them at the "=": let args = msg.content.split(/\s+--/).map(a =&gt; a.split(/=/)); Now we have entries more like: [level, 2] [roll1, 4] [roll2, 3] Each item is an individual array. The first item (0 th ) is the name of the argument; the next item is the value. So now let's make sure that the names of the supplied arguments are what we're looking for... we don't need to do anything with a supplied argument that doesn't read "level", "roll1", or "roll2". We can accomplish that with a filter() that checks the 0 th item against an array of those three strings. First, though, we'll use another map operation to render the name as lowercase. Again, I'll reformat the line to add line breaks for readability: let args = msg.content &nbsp; .split(/\s+--/) &nbsp; .map(a =&gt; a.split(/\s*=\s*/)) .map(a =&gt; [a[0].toLowerCase(),a[1]]) &nbsp; .filter(a =&gt; [ "level", "roll1", "roll2"].includes(a[0])) ; EDIT : You'll always want to be looking for ways that a user could break what you've written. For instance, in the above, if the user supplied an argument without an equals sign, then you wouldn't end up with a 2 item array when you split on an equals sign, and the reference to a[1] would be undefined. If the user supplied text or a decimal number on the right side of the equals instead of an integer, you'll encounter an error when you try to treat that as an&nbsp; integer (like in a roll equation). As you think of these edge cases, you can plug the fixes into what your doing (perhaps filtering for a regex that detects properly formatted arguments prior to splitting on the equals: .filter(a =&gt; /[A-Za-z]+\s*=\s*\d+$/.test(a)) ...that would take care of both issues. At this point, you might want to consider whether you have ALL of the components you need, and/or whether you have EXTRA instances of them (ie, if the command line included two values for "level"). An easy way to manage both is to convert these [key,value] entries to properties on an object. We can do that with Object.fromEntries() . Since the object we create will have properties that must have unique names, we'll preserve only one copy of each supplied argument (the last instance). Then, once we know we have a unique set of properties, we can count them to make sure there are 3 of them: let argObj = Object.fromEntries(args); if (Object.keys(argObj).length !== 3) { &nbsp; // handle user notification here ... &nbsp; return; } If we still survive past this point in the code, we now have an argObj with three properties of our 3 required components for the roll. Maybe you want to test that the values of each of the properties is an integer... I'll leave that to you (I would do it during the maps and filters, above, before the Object.fromEntries()). But basically at this point you're ready to send your message to the sendChat callback . The idea of the callback is that instead of your message going to the chat output, it comes straight back to your code. Your callback function is like the chat handler for those specific messages... but the kicker is that it will run asynchronously. That can be a real mind-twist if you're just learning. The pitfall you have to worry about is that your code will progress up to your asynchronous sendChat() call, it will dispatch that call, and then it will continue on past the sendChat() . In other words, it will not wait for the sendChat callback to complete. For this reason, as you're just learning, I would suggest a very simple solution... when we get the return from the sendChat(), we'll pipe everything we need into another function. Here's what that would look like... I don't know how the spell level affects the rolls, so I'll leave it off for now and just use roll1 + roll2: const continueHandler = (msg, rolls) =&gt; { &nbsp; // code to continue processing your rolls, here }; sendChat('',`[[${roll1+roll2}d6]]`,(msgs =&gt; { &nbsp; let rolls = []; &nbsp; msgs.forEach(m =&gt; rolls = [...rolls,...m.inlinerolls]); &nbsp; continueHandler(msg, rolls); })); I build the command line I want to send through the sendChat() function (really, just the inline roll notation), then I capture the return as "msgs". I use the plural, here, because the callback returns an array of message objects. I use a forEach() to loop through the messages and append the inline rolls to an array object I declared (rolls). That "..." is the spread operator . The rolls variable is only block scoped, so it won't persist beyond the callback block if we just let the function finish, so while we're still in there, we'll call the continueHandler() function and pass the original message object (msg), and the rolls array we just built. Note that the callback can access the variables in the parent closure (your on('chat:message') function), so it can utilize the message object. (Also, we could have just assigned the rolls to occupy the inlinerolls property of the original message object if we'd wanted to, then passed only our message object to the continueHandler() function. These things come down to preference...) Eyes Glazed Over, Yet? I know you said you were looking for something simpler to start learning with, but by that point I had 2/3 of this post written. If it doesn't help you (at least right now), it might help you later. Or it might help someone else. The point is these are a couple of ways to connect with the inlinerolls of a message to work with the data. When it comes to crafting an output from your script (where you might want to show the inline rolls with a hover roll tip), that's where libInline can help. When you get to that point, if the documentation for that library isn't enough to get you where you need to be, start a new post and ask your questions!
I bookmarked this :)