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

Getting more info from a blindroll with a roll template

I am using The Aaron's mod of the broll API script (<a href="https://app.roll20.net/forum/permalink/6419743/" rel="nofollow">https://app.roll20.net/forum/permalink/6419743/</a>) In that thread I asked about a tooltip showing roll details when using a roll template, and The Aaron said it was not easily doable. At the time I was not too concerned, but this is getting to be something that would make my GMing a little easier. I was hoping that there may be some way to get the raw die roll to display. It does not have to be a tooltip - even just appending the original roll as a new line to the whispered result would be fine. Anyone have any ideas? Or should I convert all these to not use roll templates? Thanks in advance!
1606615333

Edited 1606836340
Oosh
Sheet Author
API Scripter
Which roll template are you using? This should work for &amp;{template:default}, or anything that has an allprops output where you can do {{any property=any text}}. If you need a different output (like a 5e template with strict property names) it'll need some adjusting. -- snip -- One day, I need to get around to understanding Aaron's _.chain.reduce() function there. The bit I've added probably could've just gone in there with the rest of the inline roll getting picked apart but.... yeah I'm just not quite getting how it works. Oh, and I was a bit lazy with that, too. If you have mixed die types in a single expression, like [[2d10 + 8d6]], it's going to need another loop in there to pick those up. I was assuming it's mostly for d20 rolls though.
1606803425

Edited 1606803459
Many thanks for the assist, Oosh! I hear you about that reduce syntax... I have no clue how it works its magic. I am using a custom rolltemplate, highly modified from the D&amp;D 3.5 one. Your script revision is essentially working (Yay!). It throws a couple of ugly undefined statements, but no one will see that but me, and the d20 roll is clearly visible! If you care to tackle cleaning that up (and/or explaining what your script is doing), I would be appreciative. I am not sure why it thinks there are two rolls to display? These are all going to be skill rolls with a single d20 roll plus some modifiers. Here is a sample macro (linebreaks manually added for legibility): ?{DM Only?|Yes,!broll|No,} &amp;{template:StdRoll} {{skillflag=1}} @{character_name_option} {{check=Perception check:}} {{checkroll= [[ 1d20 + [[floor(@{perception_ranks})]] [Ranks] + @{wis_mod} [Wis] + @{perception_race} [Race] + @{perception_magic} [Magic] + @{perception_misc} [Misc] + ?{Additional Skill Modifier? (Do not use + sign)|0} [Add'l Skill Mod] ]] }} {{notes=@{perception_notes}} } Here is the rolltemplate: &lt;rolltemplate class="sheet-rolltemplate-StdRoll"&gt; &nbsp;&nbsp;&nbsp; &lt;div class="rt-wrapper"&gt; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {{#abilityflag}}&lt;div class="rt-header rt-header-ability"&gt;{{name}}&lt;/div&gt;{{/abilityflag}} &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {{#actionflag}}&lt;div class="rt-header rt-header-action"&gt;{{name}}&lt;/div&gt;{{/actionflag}} &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {{#initflag}}&lt;div class="rt-header rt-header-init"&gt;{{name}}&lt;/div&gt;{{/initflag}} &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {{#ruleflag}}&lt;div class="rt-header rt-header-rule"&gt;{{name}}&lt;/div&gt;{{/ruleflag}} &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {{#saveflag}}&lt;div class="rt-header rt-header-save"&gt;{{name}}&lt;/div&gt;{{/saveflag}} &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {{#skillflag}}&lt;div class="rt-header rt-header-skill"&gt;{{name}}&lt;/div&gt;{{/skillflag}} &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {{#spellflag}}&lt;div class="rt-header rt-header-spell"&gt;{{name}}&lt;/div&gt;{{/spellflag}} &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {{#subtags}}&lt;div class="rt-subheader"&gt;{{subtags}}&lt;/div&gt;{{/subtags}} &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {{#abilityflag}}&lt;div class="rt-divider rt-divider-ability"&gt;&lt;/div&gt;{{/abilityflag}} &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {{#actionflag}}&lt;div class="rt-divider rt-divider-action"&gt;&lt;/div&gt;{{/actionflag}} &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {{#initflag}}&lt;div class="rt-divider rt-divider-init"&gt;&lt;/div&gt;{{/initflag}} &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {{#ruleflag}}&lt;div class="rt-divider rt-divider-rule"&gt;&lt;/div&gt;{{/ruleflag}} &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {{#saveflag}}&lt;div class="rt-divider rt-divider-save"&gt;&lt;/div&gt;{{/saveflag}} &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {{#skillflag}}&lt;div class="rt-divider rt-divider-skill"&gt;&lt;/div&gt;{{/skillflag}} &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {{#spellflag}}&lt;div class="rt-divider rt-divider-spell"&gt;&lt;/div&gt;{{/spellflag}} &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;&nbsp; {{#checkroll}}&lt;div class="rt-checkroll"&gt;{{check}} {{checkroll}}&lt;/div&gt;{{/checkroll}} &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {{#gmcheckroll}}&lt;div class="gm-notes"&gt;&lt;span&gt;{{gmcheckroll}}&lt;/span&gt;&lt;/div&gt;{{/gmcheckroll}} &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {{#gmcheckroll2}}&lt;div class="gm-notes"&gt;&lt;span&gt;{{gmcheckroll2}}&lt;/span&gt;&lt;/div&gt;{{/gmcheckroll2}} &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {{#gmcheckroll3}}&lt;div class="gm-notes"&gt;&lt;span&gt;{{gmcheckroll3}}&lt;/span&gt;&lt;/div&gt;{{/gmcheckroll3}} &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {{#spellflag}}&lt;div class="rt-spell-details"&gt;{{/spellflag}} &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {{#allprops() source gmnotes spelldescription notes subtags subtags2 compcheck failcheck succeedcheck check checkroll gmcheckroll gmcheckroll2 gmcheckroll3 critroll fumbleroll actionflag initflag ruleflag ruletext skillflag saveflag abilityflag spellflag name}} &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &lt;div&gt;&lt;span class="rt-spell-key"&gt;&lt;b&gt;{{key}}&lt;/b&gt;&lt;/span&gt;&lt;span class="rt-spell-value"&gt;{{value}}&lt;/span&gt;&lt;/div&gt; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;&nbsp; {{/allprops() source gmnotes spelldescription notes subtags subtags2 compcheck failcheck succeedcheck check checkroll gmcheckroll gmcheckroll2 gmcheckroll3 critroll fumbleroll actionflag initflag ruleflag ruletext skillflag saveflag abilityflag spellflag name}} &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;&nbsp; {{#spellflag}}&lt;/div&gt;{{/spellflag}} &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {{#spelldescription}}&lt;div&gt;&lt;span class="rt-textblock"&gt;{{spelldescription}}&lt;/span&gt;&lt;/div&gt;{{/spelldescription}} &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {{#source}}&lt;div&gt;&lt;span class="rt-textblock"&gt;&lt;b&gt;Source:&lt;/b&gt; {{source}}&lt;/span&gt;&lt;/div&gt;{{/source}} &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {{#ruletext}}&lt;div&gt;&lt;span class="rt-textblock"&gt;{{ruletext}}&lt;/span&gt;&lt;/div&gt;{{/ruletext}} &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {{#notes}}&lt;div class="rt-divider-thin"&gt;&lt;/div&gt; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &lt;div class="rt-notes"&gt;{{notes}}&lt;/div&gt;{{/notes}} &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {{#gmnotes}}&lt;div class="rt-divider-thin gm-notes"&gt;&lt;/div&gt; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &lt;div class="gm-notes"&gt;{{gmnotes}}&lt;/div&gt;{{/gmnotes}} &nbsp;&nbsp;&nbsp; &lt;/div&gt;
1606835970

Edited 1606836286
Oosh
Sheet Author
API Scripter
Whoops, I messed up the search, that [[floor(@{perception_ranks})]] shouldn't have been getting picked up. I'm not very qualified to explain, but I'll try :) Each set of inline brackets is stored as its own "roll" in the inlinerolls object of the chat message. If you follow that link, there's an "expand" button just under the first table which shows you the structure of an inline roll object. The code I added just grabs all the separate die results from the object if they meet the criteria of have a roll expression (dX, X being at least one digit) and having a total more than 0. You can certainly get more restrictive with the search, but hopefully it isn't needed. The rest is just getting them into the right place in a string to send to chat. I've also added an extra step for grabbing rolls with multiple die types, (for example a 1d20 roll with a 1d4 bonus added). Hopefully no more dramas! on("chat:message", function(msg_orig) { "use strict"; var msg = _.clone(msg_orig), who; if (msg.type !== "api") return; if(_.has(msg,'inlinerolls')){ msg.content = _.chain(msg.inlinerolls) .reduce(function(m,v,k){ m['$[['+k+']]']=v.results.total || 0; return m; },{}) .reduce(function(m,v,k){ return m.replace(k,v); },msg.content) .value(); if (_.has(msg, 'rolltemplate')) { let rNum = 0; msg.inlinerolls.forEach(roll =&gt; { if (roll.expression.search(/d\d+/i) !== -1 &amp;&amp; roll.results.total &gt; 0) { rNum ++; msg.content += `{{R${rNum} `; let dieRolls = '='; for (let x = 0; x &lt; roll.results.rolls.length; x ++ ) { if (_.has(roll.results.rolls[x], 'sides')) { msg.content += (x &gt; 0) ? `&lt;br&gt;${roll.results.rolls[x].dice}d${roll.results.rolls[x].sides}` : `${roll.results.rolls[x].dice}d${roll.results.rolls[x].sides}`; dieRolls += (x &gt; 0) ? `&lt;br&gt;${_.pluck(roll.results.rolls[x].results, 'v').join(', ')}` : `${_.pluck(roll.results.rolls[x].results, 'v').join(', ')}`; } } msg.content += `${dieRolls}}}` } }) } } who=getObj('player',msg.playerid).get('_displayname').split(' ')[0]; var cmdName = "!broll "; var msgTxt = msg.content; var msgWho = msg.who; var msgFormula = msgTxt.slice(cmdName.length); if(msg.type == "api" &amp;&amp; msgTxt.indexOf(cmdName) !== -1) { if(_.has(msg,'rolltemplate')) { sendChat(msgWho,'/w gm &amp;{template:'+msg.rolltemplate+'}'+msg.content); sendChat(msgWho, "/w " + who + " secret rolltemplate sent to GM"); } else { sendChat(msgWho, "/gmroll " + msgFormula); sendChat(msgWho, "/w " + who + " secret roll sent to GM (" + msgFormula + ")"); } } });
Works wonderfully! Still not sure I understand what it is doing, but that can be for another day! ;-) Many thanks again, Oosh!
1606921134

Edited 1606921569
Oosh
Sheet Author
API Scripter
No problem - happy to attempt to explain more of it, if you like? Probably not the best qualified to do so, though - is it the entire script, or Aaron's modification, or the bit I added you'd like to understand? The script could probably do with some updating (complete rewriting) but I think it's best left to someone like... Aaron and Timmaugh, potentially integrating an [{inlinerolls}] passthrough into Timmaugh's DiscreteWhispers script. It's tricky, but it can be done. Just not by someone with my limited skills. The Aaron already has some tooltip magic happening with !groupinit, so passing all the roll info into Timmaugh's script to be presented in a mouse-over friendly, customisable way should be possible, I just wish I was smart enough to do it. edit - alluded to here by both the gentlemen mentioned above. I'm currently stuck with the docker error others are getting, but 99% sure timmy's script doesn't pass through that kind of inline roll info
1606933019
The Aaron
Roll20 Production Team
API Scripter
Oosh said: I just wish I was smart enough to do it. You seem smart enough to me, and to others who have sung your praises to me. ;)&nbsp; It's really just a matter of learning how to do it.&nbsp; Definitely ask me about anything, anytime. I know Tim is going to post something here after he finishes eating some tacos, so I'll let him jump in. =D
1606933442

Edited 1607435864
timmaugh
Pro
API Scripter
I'll take a stab at explaining the reduce function, though it's quite possible my explanation will require more elucidation by someone more knowledgable... Primer: reduce() and _.reduce() You two probably don't need this, but for anyone else that might read and want to understand, think of reduce() as a snake eating its own tail, segment by segment. It passes over an array and, for each element, returns some piece of information that is fed to the reduce for the next element in the array. That returned piece of information is called the "memo", and it will obviously grow as the reduce function moves from element to element. This is different from, say, map() , which changes each element in the array in place, returning an array: let outputArray = inputArray.map(a=&gt;a*2); /* produces the following process... ================================================ inputArray&nbsp; &nbsp;| map(a=&gt;a*2)&nbsp; | outputArray ================================================ 1&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |&nbsp;&nbsp;&nbsp;&nbsp;=&gt;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;&nbsp;2 2&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |&nbsp;&nbsp;&nbsp;&nbsp;=&gt;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;&nbsp;4 3&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |&nbsp;&nbsp;&nbsp;&nbsp;=&gt;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;&nbsp;6 4&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |&nbsp;&nbsp;&nbsp;&nbsp;=&gt;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;&nbsp;8 */ // outputArray === [2,4,6,8] By contrast, reduce() returns an object of the type of your memo. That means you can use reduce() to return a string, a number, or a more complex object. The call looks a lot like map() , except we need to include the memo argument (we'll use m ), and we have to supply an initial value for the memo for the first time through the loop (if yo don't supply a starting value, it will use the first item in the array). &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;┌───────────┐ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; v &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ^ let output = inputArray.reduce((m,a)=&gt;m+a*2,0); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;^ ^ ^ memo─┘ | └─initial value for memo &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp;└─array element Given the same inputArray as the first example, m will return as 20, since it starts at 0 and adds 2x the value of each array element: let output = inputArray.reduce((m,a)=&gt;m+a*2); /* produces the process... ================================================ inputArray&nbsp; &nbsp;| reduce((m,a)=&gt;m+a*2,0)&nbsp; | &nbsp;&nbsp;&nbsp;m ================================================ 1&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;0&nbsp;+ 1*2&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |&nbsp;&nbsp;&nbsp;&nbsp;2 2&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;2 + 2*2&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;&nbsp;6 3&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;6 + 3*2&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;&nbsp;12 4&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;12 + 4*2&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;&nbsp;20 */ // output === 20 In that case, we fed in a number, and every time we returned that number, so our final output was a number. You can build an object by starting with an initialized object (a prefilled object or an empty object {}), then returning that object in the reduce callback. Index (or Key) - The other argument relevant to this explanation (as it was utilized in the referenced code) is the index of the array element. It would be the third argument, if included, so k in the following implementation: let output = inputArray.reduce((m,a,k) =&gt; { &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;m['Item' + k] = a; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;return m; &nbsp;&nbsp;&nbsp;&nbsp;}, {}); That utilizes the variable naming pattern for an object (obj[variable]), obtaining unique property names because each index is unique. At the end of that, output is an object with properties of Item0, Item1, Item2, etc. Underscore Version The underscore version of reduce() &nbsp;is very similar, except that (1) it works on objects as well as arrays (pure javascript function only works on arrays), and (2) it gets the list it should operate over passed to it as the first argument (before the callback). If the reduce() comes within the _.chain() block, it will grab the wrapped object by default: _.chain(list).reduce((m,v,k)=&gt;{},{}); ...will operate on the list as returned by _.chain() . As Utilized In the referenced code, then, we have: msg.content = _.chain(msg.inlinerolls) .reduce(function(m,v,k){ m['$[['+k+']]']=v.results.total || 0; return m; },{}) .reduce(function(m,v,k){ return m.replace(k,v); },msg.content) .value(); The first reduce() gets inlinerolls &nbsp;, which is an array, as wrapped by chain() . It builds an object (m) by using the index (k) of the item's position, referring to each by the variable-name pattern: m['$[['+k+']]']=v.results.total || 0; Each property of the memo object will have a value of either the total (if present) or 0. The returned object will look like: { &nbsp; &nbsp; '$[[0]]': 10, &nbsp; &nbsp; '$[[1]]': 14 &nbsp; &nbsp; // etc. } The second reduce() is where I think most people lose the train of what is happening... (and to be fair, I may not have the full/right of it). Since the second reduce() is tacked onto the _.chain() block, it receives the object we just built with the first reduce() as the source of the value (v) and key (k). The slight of hand comes in spotting that it is being fed the msg.content as the initial value of the memo even though it's reading from the object we've constructed. So when it performs the line: return m.replace(k,v); ...it is taking the key (the '$[[0]]') and replacing it with the value (10, from my example, just above). It is performing that replace() operation on the memo, which was initialized to be the msg.content (a string). So where it sees '$[[1]]' it replaces it with the total value of that roll. Then it returns the version of the memo (m) that has the replacement done, and proceeds to the next property/value of the list object. TL;DR version: the first reduce() builds an object out of the inlinerolls array that lets us (in the second reduce() function) perform a set of replacement operations on the msg.content . In the end, the value() function unwraps the chain() &nbsp;object and returns the value to the msg.content .
1606940998

Edited 1606941019
vÍnce
Pro
Sheet Author
1606943384
timmaugh
Pro
API Scripter
I know, Vince... those tacos *were* mind-blowing. ... ...wait a minute... how did you know how good my tacos were?! =D
1606945905
vÍnce
Pro
Sheet Author
I'm sure the tacos were great, but you lost me at " I'll take a stab at explaining the reduce function,... "&nbsp; lol Joking aside.&nbsp; Thanks for posting this.&nbsp; It serves to reinforce my appreciation of programmers.
1606955338
Oosh
Sheet Author
API Scripter
Ok, that explanation was amazing. I would have had to lock it in a console.log basement and feed it a bunch of times to try to work out what was going on - that second reduce function is really clever. I think I might actually understand that enough to get more information passed through it without causing my skull to bleed. And I'm (slightly) less terrified of _.chain(). Thanks! Quick question though - what method would either of you go for if you were to try to iterate through the v.expression.rolls[x] array inside? Currently the chain doesn't need to, as v.results.total is outside that, you only have the outer array to deal with. But the roll results are inside another array. Would a forEach() &amp; a map() (or pluck, as a shortcut) do the trick, being able to store the index of both the $[[k]] outer expression, and the rolls[k] index, along with grabbing all the rolls[k].results.v values? Or is there a smarterer way?
1606956637

Edited 1607216987
The Aaron
Roll20 Production Team
API Scripter
It really depends on what you want to do with it.&nbsp; In this example, I'm expanding Rollable table text: const processInlinerolls = (msg) =&gt; { if(_.has(msg,'inlinerolls')){ return _.chain(msg.inlinerolls) .reduce(function(m,v,k){ let ti=_.reduce(v.results.rolls,function(m2,v2){ if(_.has(v2,'table')){ m2.push(_.reduce(v2.results,function(m3,v3){ m3.push(v3.tableItem.name); return m3; },[]).join(', ')); } return m2; },[]).join(', '); m['$[['+k+']]']= (ti.length &amp;&amp; ti) || v.results.total || 0; return m; },{}) .reduce(function(m,v,k){ return m.replace(k,v); },msg.content) .value(); } else { return msg.content; } }; I choose .reduce() when I want to build a single value (like a sum) or a new data structure (like a tree). I choose .map() when I want to transform each individual value into a new value (like building a table of squares, or converting strings to numbers). I choose .forEach() when I want to do something with each individual item of a collection (like logging each individually, or sending each player a message). Lately, I've been moving toward the native operations rather than using Underscore.js.&nbsp; The above would look something like ( untested tested and fixed. =D): const processInlinerollsNative = (msg) =&gt; { if(msg.hasOwnProperty('inlinerolls')){ return msg.inlinerolls .reduce((m,v,k) =&gt; { let ti=v.results.rolls.reduce((m2,v2) =&gt; { if(v2.hasOwnProperty('table')){ m2.push(v2.results.reduce((m3,v3) =&gt; [...m3,v3.tableItem.name],[]).join(", ")); } return m2; },[]).join(', '); return [...m,{k:`$[[${k}]]`, v:(ti.length &amp;&amp; ti) || v.results.total || 0}]; },[]) .reduce((m,o) =&gt; m.replace(o.k,o.v), msg.content); } else { return msg.content; } }; There's nothing wrong with Underscore.js, but it's mostly not necessary the way it used to be.
Thanks Oosh (again!), The Aaron, and&nbsp;timmaugh for going over all this for me. I think i am beginning to get a better idea of how reduce works now. I will have to go over this a few more times, though!