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] Advanced summing with TheAaronSheet

1523665982

Edited 1523666549
What I have is a repeating section of wounds: <fieldset class="repeating_wounds"> <div class="sheet-table-row  sheet-table-first-only"> <div class="sheet-table-cell" style="width: 200px"> <select name="attr_woundlocation"> <option value="general">General</option> <option value="head">Head</option> <option value="rightarm">Right Arm</option> ... etc ... </select> </div> <div class="sheet-table-cell" style="width: 50px"> <input type="number" name="attr_woundhp" value="0" /> </div> </div> </fieldset> And I'm able to sum ALL of the wounds to an attribute called "attr_total_damage_sustained", and this is great and I want to keep this. on('change:repeating_wounds', function() { TAS.repeatingSimpleSum('wounds', 'woundhp', 'total_damage_sustained'); }); What I'd also like to be able to do is sum all damage where 'woundlocation' is equal to "head" to "attr_head_damage_sustained" and all damage where 'woundlocation' is equal to "rightarm" to "attr_rightarm_damage_sustained", and so on. So basically a "SUMIF" situation.  I've read TheAaronSheet readme, and several posts about it but I'm not getting it. I'm suspecting that I need to use  TAS.repeating and then specify the target attribute in "attrs" and the fields I care about from the repeating section, "woundlocation" and "woundhp", in "fields", and then use one of the other functions like "tap" and finally "execute", but the tap example is just logging it. Any help appreciated.
I've been banging my head on this and looking at other sheets for examples.  This is what I have so far, but it's still not working: on('change:repeating_wounds', function() { TAS.repeating('wounds') .attrs('head_damage_sustained') .fields('woundlocation', 'woundhp') .reduce(function (memo,row) { if(row.S.woundlocation == "head") { memo.headsustained += row.I.woundhp; return memo; } },  {headsustained:0}, function(memo,row,attrSet) { attrSet.head_damage_sustained = memo.headsustained; }) .execute(); }); I'm looking for changes to "repeating_wounds".  I get access to the non-repeating attribute "head_damage_sustained".  I declare that I'll be using the fields "woundlocation" and "woundhp" from the rows.  Then if the woundlocation is "head" I'm adding the woundhp as an Integer to the memo's headsustained property.  "headsustained" has an initial value of 0, and Finally I put the headsustained sum in the attrSet.  
1523691958

Edited 1523692772
GiGs
Pro
Sheet Author
API Scripter
I have no idea how to use TAS for advanced stuff like this. I find the documentation very lacking indeed. (Sorry, Aaron!) I had a similar need, and figured out how to do it without using TAS. check the notes under the function on("change:repeating_wounds:woundlocation repeating_wounds:woundhp remove:repeating_wounds", function () { getSectionIDs("repeating_wounds", function (IDarray) { var fieldNames = []; let outputGlory = {}; for (var i = 0; i < IDarray.length; i++) { fieldNames.push("repeating_wounds_" + IDarray[i] + "_woundlocation"); fieldNames.push("repeating_wounds_" + IDarray[i] + "_woundhp"); } var totalWounds = 0; var locations = { general: 0, head: 0, rightarm: 0, // add an entry for each of the options in the repeating_wounds_locations select - and spell them EXACTLY the same. } getAttrs(fieldNames, function (values) { for (var i = 0; i < IDarray.length; i++) { let thisLocation = values["repeating_wounds_" + IDarray[i] + "_woundlocation"]; let thisWound = parseInt(values["repeating_wounds_" + IDarray[i] + "_woundhp"]) || 0; totalWounds += thisWound; locations[thisLocation] += thisWound; } // at this point, "locations" should be an object containing each wound, like {general: 4, head: 7, rightarm 0, } etc. // now to convert it into the same stats as those used on the character sheet let settings = {}; for (var key in locations) { if (locations.hasOwnProperty(key)) { // build a new object, accessing each damage with locations[key] settings[key + '_damage_sustained'] = locations[key]; } } settings['total_damage_sustained'] = totalWounds; setAttrs(settings); }); }); }); For this to work, your sustained_damage attributes must map exactly to the damage locations in the repeating wounds location dropdown. For location head, you must have a head_sustained_damage, for rightarm, you must have rightarm_sustained_damage. The location parts must be spelled exactly the same. If you dont have a general_sustained_damage attribute, amend the for loop to this: for (var key in locations) { if (locations.hasOwnProperty(key)) { if(key !== 'general} { settings[key + '_damage_sustained'] = locations[key]; } } } Alternatively you can skip the for loop altogether and do something like settings['right_arm_sustained_damage'] = locations['rightarm']; And repeat for each location. You can spell them out individually, just make sure to get the spellings correct. Hope this helps!
Thanks for the detailed reply.  I tried that earlier today and wasn't getting it to work.  Here's what I ended up with, but I suppose I'm not done banging my head against it! on("change:repeating_wounds:woundlocation repeating_wounds:woundhp remove:repeating_wounds", function() { console.log("**** repeating wounds"); getSectionIDs("repeating_wounds", function (IDarray) { var fieldNames = []; let outputGlory = {}; //what? for (var i = 0; i < IDarray.length; i++) { fieldNames.push("repeating_wounds_" + IDarray[i] + "_woundlocation"); fieldNames.push("repeating_wounds_" + IDarray[i] + "_woundhp"); } var totalWounds = 0; var locations = { general: 0, head: 0, rightarm: 0 }; getAttrs(fieldNames, function (values) { for (var i = 0; i < IDarray.length; i++) { let thisLocation = values["repeating_wounds_" + IDarray[i] + "_woundlocation"; let thisWound = parseInt(values["repeating_wounds_" + IDarray[i] + "_woundhp"]) || 0; totalWounds += thisWound; locations[thisLocation] += thisWound; } let settings = {}; for (var key in locations) { if (locations.hasOwnProperty(key)) { settings[key + '_damage_sustained'] = locations[key]; } } settings['total_damage_sustained'] = totalWounds; setAttrs(settings); }); }); });
Ok, I got it!  I was missing the "change" on the second part of the string in the first parameter of "on", namely "change:repeating_wounds:woundhp".  Also I was missing a "]" at the end of "let thisLocation".  Thanks G G!  I believe I'm off to the races, but I'm also sure I'll be back with the next thing! on("change:repeating_wounds:woundlocation change:repeating_wounds:woundhp remove:repeating_wounds", function() { console.log("**** repeating wounds"); getSectionIDs("repeating_wounds", function (IDarray) { var fieldNames = []; let outputGlory = {}; //what? for (var i = 0; i < IDarray.length; i++) { fieldNames.push("repeating_wounds_" + IDarray[i] + "_woundlocation"); fieldNames.push("repeating_wounds_" + IDarray[i] + "_woundhp"); } var totalWounds = 0; var locations = { general: 0, head: 0, rightarm: 0 }; getAttrs(fieldNames, function (values) { for (var i = 0; i < IDarray.length; i++) { console.log("** repeating_wounds_" + IDarray[i] + "_woundlocation"); console.log("** repeating_wounds_" + IDarray[i] + "_woundhp"); let thisLocation = values["repeating_wounds_" + IDarray[i] + "_woundlocation"]; let thisWound = parseInt(values["repeating_wounds_" + IDarray[i] + "_woundhp"]) || 0; totalWounds += thisWound; locations[thisLocation] += thisWound; } let settings = {}; for (var key in locations) { if (locations.hasOwnProperty(key)) { settings[key + '_damage_sustained'] = locations[key]; } } settings['total_damage_sustained'] = totalWounds; setAttrs(settings); }); }); });
1523768470

Edited 1523768663
GiGs
Pro
Sheet Author
API Scripter
Yay! Glad to hear it. Sorry about those typos (leaving out the necessary change, etc). Dont forgot to add in the rest of the locations to this: var locations = { general: 0, head: 0, rightarm: 0 }; e.g. rightarm, chest, leftleg, etc, whatever they are. Include every single entry from that select dropdown that defines possible locations, that has a corresponding "_sustained_damage" attribute.
1523810917

Edited 1523810937
Don't apologize!  Your code was awesome!  I typed it in myself to make sure I understood it.  I'm a developer, but in the .NET world and with system to system integration so my javascript is not my strongest.   I just wanted to get it working with a few fields before going whole hog.  My ultimate goal is to have a repeating list of hit locations.  Humans and humanoids all have the same locations but other creatures do not.  Then it would use that list of hit locations as the source for the select in the wounds table.  You got me well down that road.  Thanks!
1523815999
GiGs
Pro
Sheet Author
API Scripter
You're welcome :)
1523900405
The Aaron
Pro
API Scripter
Glad you got it working, sorry, I was on vacation until this morning.  Here's how I think you'd write that in TAS (untested): on("change:repeating_wounds:woundlocation change:repeating_wounds:woundhp remove:repeating_wounds", function() {     let locations = ['general','head','rightarm']; // add the rest here TAS.repeating('wounds')         .attrs(locations.map((l)=>`${l}_damage_sustained`))         .fields('woundlocation', 'woundhp')         .reduce(function (memo,row) {             memo[row.S.woundlocation] = memo[row.S.woundlocation] || 0;             memo[row.S.woundlocation] += row.I.woundhp;             return memo;         },         {},         function(memo,row,attrSet) {             object.keys(memo).forEach((k)=>attrSet[`${k}_damage_sustained`]=memo[k]);         })         .execute(); }); I think the issue you likely ran into with your original code was only returning the memo inside the if.  You have to always return the memo from reduce, otherwise you will get a reference on undefined. G.G. No offense taken on the documentation, it needs some updating. =D  (like everything else I do!)
Thanks for eventually getting back on this!   I hope your vacation was great!  I'll have to look at this when I haven't had a long day, but at this point I feel like I understand how the non-TAS way is working.  Probably just because I worked through it! I see what you're saying about the memo thing. I think what I'd want to see improved in the documentation is when to use .tap vs .each vs .map vs .reduce, and I'd be happy to help you try to improve that if I can, but I have to grok it first. So the setup is fairly clear, call TAS repeating with the name of the group, the attrs outside of the group that you may want to read from (or write to?), and the fields inside the group that you may want to read from or write to. .tap does it's function once, so it would be good for summing all of the values in a field or, as your example shows, concatenating all of the keys or rows, or for any other operations you can do on an entire array of values at once.  So counting the rows would be a good use of tap, or summing up all the values in one field without conditions. .each does it's function on each row and doesn't really return anything.  So this would be good for enumerating values in rows, or perhaps for setting values in rows when you already have an array of values?  Or for setting fields in a row depending upon other field values in the same row. .map is similar to .each except it accumulates whatever the function returns into an array.  This might be good for formatting the rows for later display in a text box?  Or summing up one field, with possible conditions, like "if equipped, add encumbrance to the result set" and then you can sum the array of results. .reduce is similar to map but it lets you keep a more complicated object as it goes through each row, so this would be good for summing up several values, possibly with conditions. I think the names map and reduce were particularly confusing to me, but I'm not sure I have a better suggestion. I think some "use cases" framed in the rpg context would be helpful, like "for encumbrance you'd use .map in this way".
1523970919

Edited 1523970930
The Aaron
Pro
API Scripter
I was influenced heavily by Underscore.js when writing TAS, so looking at its equivalently named functions would probably help with understanding, particularly the chain() function and operations.&nbsp; The functions on TAS.repeating() fall into two categories: Specifying Data and Operations.&nbsp; .attrs() specifies attributes (things outside the repeating section) that should be available during execution, either for reading or writing.&nbsp; They will all be in the AttrSet that is passed to each Operation.&nbsp; You'd use these either for something you're referencing (what was the dex bonus to add to each ranged attack?) or something you're updating (how encumbered is the player?). .fields() specifies the attributes in the repeating row that should be available for reading and writing.&nbsp; Weapon name, weight, cost, etc.&nbsp; They will be available in the RowSet passed to each Operation.&nbsp; Depending on the Operation, they will either be a single row, or a collection of rows. .execute() is a special operation that must appear last.&nbsp; Nothing actually happens until it is called.&nbsp; All the other Operations and Specifying Data functions merely queue up things to do, .execute() is where they all happen.&nbsp; First all the specified data is loaded, then each operation is run in the order it was specified.&nbsp; One of the most powerful features of TAS is that for a given TAS.repeating() call, it only does a single setAttrs() operation, and it only sets things that have actually changed value.&nbsp; This makes it really fast as the setAttrs() call is the most expensive part of the update process.&nbsp; .execute() actually takes 2 more arguments (which I need to add to the documentation), callback and context.&nbsp; When the set operation is fired, it will call the callback in the context of the context object passed to it.&nbsp; This is handy for doing something once the TAS.repeating() is done. The rest of the Operations behave like the .chain() functions in Underscore.js.&nbsp; The state of the RowSet and AttrSet passed into them is the output from the prior step. .tap() comes directly from Underscore.js.&nbsp; It is simply passed the full state of RowSet and AttrSet in a single call to the callback function.&nbsp; It's useful when you want to examine the whole thing at once, say if you are searching for one particular row, but don't want to pay the cost of iterating over the whole thing.&nbsp; I usually use it for debugging, to see what is being passed into the next operation, or what came out of the prior operation. .map() implements the Map programming idiom.&nbsp; Map is a pretty standard operation in programming.&nbsp; It means to apply a given function to each element of a collection.&nbsp; map(f,[a,b,c]) is equal to [f(a),f(b),f(c)], the result of map is a collection of the same length with the results of calling the function on each element in the same location where that element was.&nbsp; It's useful for conversions that are independent of other elements.&nbsp; In TAS, the main use is assembling a collection of results to pass to the Final function.&nbsp; You might use .map() to collect a list of the names of all the equipment a character has, which you then join and store in a summary field in the Final function. .each() is identical to .map(), save that it doesn't collect results and pass them to the Final function.&nbsp; You might use it if you are doing row based updates, say totaling the weight for the row based on a weight per item and quantity. .reduce() is the Reduce programming idiom.&nbsp; Reduce is a pretty standard operation in programming (though I can't find a nice wikipedia link for it!).&nbsp; Effectively, it consumes each element of a collection, building a combined state often called a memo and returning it.&nbsp; Each iteration of the reduce function is passed the memo from the previous operation.&nbsp; The classic reduce example is summing a list of numbers.&nbsp; The function passed to reduce just returns the memo plus the current element, which becomes the memo for the next operation.&nbsp; reduce(f,[a,b,c],m) is equal to f(f(f(m,a),b),c).&nbsp; It's probably one of the most powerful concepts in modern programming.&nbsp; You can think of it as transforming one data structure into another data structure iteratively.&nbsp; It's useful for building any sort of summary, be it the total weight of equipment, the total cost, a list of all the one handed weapons, all the spell components needed, whatever. There's one other undocumented operation, .after() .&nbsp; .after takes a callback and a context and queues them up to be called after .execute is finished (in fact, .execute just passes it's arguments to .after() ) BTW, did you see the fully fleshed out and working examples here:&nbsp; <a href="https://github.com/shdwjk/TheAaronSheet/tree/maste" rel="nofollow">https://github.com/shdwjk/TheAaronSheet/tree/maste</a>... Hope that helps!
1523971271
Jakob
Sheet Author
API Scripter
Wiki for reduce:&nbsp; Fold .
1523971649
The Aaron
Pro
API Scripter
There it is!&nbsp; Thanks Jakob. =D
1523973259
The Aaron
Pro
API Scripter
(Updated documentation to include .after() and .execute()'s arguments)
1523978527

Edited 1523978605
GiGs
Pro
Sheet Author
API Scripter
I don't really understand how underscore works, which is why I can't get to grips with TAS either. I'm still at a pretty basic javascript level, and the way you chain multiple operations together with TAS/underscore is completely alien to me. I can use TAS for the simplest operations, like encumbrance (add up all items of a specific name in each row of a repeating set), but have no idea how it actually works, I'm just copying the example. If I have to do something more complicated, I'm completely lost and have no idea how to progress. That's my failing though. I have no idea how to get from where I am now, to the stage where i understand these things. There seems such a huge gulf, with so many concepts and principles to pick up first, and online instruction is not geared to the beginner as far as I can find. I'll have a reach of your guide to functions above later, when I'm more awake and see if it helps any :)
Ah!&nbsp; Somehow I missed the examples!&nbsp; I will delve into those when I get a chance. As I mentioned, I'm a software developer, but I don't work much in JavaScript.&nbsp; I'm forcing myself to come to grips with the callback patterns, and other patterns used here, which is great for me to expand my own skills.&nbsp; I'm a self taught programmer and I've often said that everything I've ever truly learned about programming has come from trying to make RPG related tools, since I was 10 with my Atari 800 and BASIC.&nbsp; I really appreciate you both hanging with me and pointing me in the right directions, since I can justify my time spent on this as career enhancement!&nbsp; ;)
1523985824

Edited 1523986695
The Aaron
Pro
API Scripter
I've got some examples that might help here: <a href="https://wiki.roll20.net/API:Cookbook#Underscore.j" rel="nofollow">https://wiki.roll20.net/API:Cookbook#Underscore.j</a>... Let's take a fairly simple script: on('ready',() =&gt; { on('chat:message',(msg) =&gt; { if('api' === msg.type && /!show-rep\b/i.test(msg.content) ){ let who = getObj('player',msg.playerid).get('displayname'); if(msg.selected){ _.chain(msg.selected) .map( o =&gt; getObj('graphic',o._id)) .reject(_.isUndefined) .filter( t =&gt; t.get('represents').length ) .map( t =&gt;({token:t,character:getObj('character',t.get('represents'))})) .reject( o =&gt;_.isUndefined(o.character)) .map( o =&gt; `&lt;div&gt;&lt;div&gt;&lt;img style="max-width: 3em; max-height: 3em; border: 1px solid #999; background-color: white;" src="${o.token.get('imgsrc')}"&gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;${o.character.get('name')}&lt;/b&gt;&lt;/div&gt;&lt;/div&gt;`) .tap(out =&gt; { sendChat('',`/w "${who}" ${out.join('')}`); }); } else { sendChat('',`/w "${who}" &lt;b&gt;No tokens selected&lt;/b&gt;`); } } }); }); All this does is print the icon and name for each selected token that represents a character: !show-rep Here is the code that does the work: _.chain(msg.selected) .map( o =&gt; getObj('graphic',o._id)) .reject(_.isUndefined) .filter( t =&gt; t.get('represents').length ) .map( t =&gt;({token:t,character:getObj('character',t.get('represents'))})) .reject( o =&gt;_.isUndefined(o.character)) .map( o =&gt; `&lt;div&gt;&lt;div&gt;&lt;img style="max-width: 3em; max-height: 3em; border: 1px solid #999; background-color: white;" src="${o.token.get('imgsrc')}"&gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;${o.character.get('name')}&lt;/b&gt;&lt;/div&gt;&lt;/div&gt;`) .tap(out =&gt; { sendChat('',`/w "${who}" ${out.join('')}`); }); This could be rewritten in a traditional form like this: on('ready',() =&gt; { on('chat:message',(msg) =&gt; { if('api' === msg.type && /!show-rep\b/i.test(msg.content) ){ let who = getObj('player',msg.playerid).get('displayname'); if(msg.selected){ let data = []; msg.selected.forEach( s =&gt; { let t = getObj('graphic', s._id); if(t && t.get('represents').length ){ let c = getObj('character', t.get('represents')); data.push( `&lt;div&gt;&lt;div&gt;&lt;img style="max-width: 3em; max-height: 3em; border: 1px solid #999; background-color: white;" src="${t.get('imgsrc')}"&gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;${c.get('name')}&lt;/b&gt;&lt;/div&gt;&lt;/div&gt;` ); } }); sendChat('',`/w "${who}" ${data.join('')}`); } else { sendChat('',`/w "${who}" &lt;b&gt;No tokens selected&lt;/b&gt;`); } } }); }); This has identical output. It's about the same length, so why would you ever use that chaining method? Clarity and agility. Once you understand the chaining pattern, it's really easy to see what it's doing and modify it to do other things. The first 6 lines of that chain() show up over and over again in scripts I write. Lets go over the whole thing line by line. The idea of chain() is that the output of each step is the input to the next step. The argument to chain() becomes the first state (here state just means what's being passed between the functions, not the global state object in the API): _.chain(msg.selected) Here I've passed the msg.selected, which means the starting state will look something like: [ {_type: 'graphic', _id: 'abc'}, {_type: 'graphic', _id: 'cda'}, ... ] Next I take this state and turn it into an array of objects using .map() . Normally, _.map() 's first argument would be the collection to map over. In a chain, the first argument is fulfilled by the chain's current state instead and the argument becomes the function to apply (all the arguments shift to the left). The o passed to the function will be each of the objects in the above msg.selected. I could have passed o._type as the first argument to getObj() , but since I'm only interested in graphics (there might be drawings, text, etc), forcing it to 'graphic' means I'll get undefined for some entries, the ones that aren't token s. The first iteration o._id will be 'abc' , the second it will be 'cda' , etc. .map( o =&gt; getObj('graphic',o._id)) Now the state will look something like: [ {Roll20 Graphic Object}, {Roll20 Graphic Object}, undefined, {Roll20 Graphic Object}, ...] Next I want to get rid of those not graphic objects, so I can .reject() them by passing a function that will match the ones I don't want. In this case, I don't want the undefined objects, so passing _.isUndefined &nbsp;will cause _.isUndefined() to be called on each entry, similar to .map() , except anything that the function is true for will be omitted from the result. ( .filter() is the opposite function, which will omit anything that the function returns false for.)&nbsp; Note that I am passing the _.isUndefined Function Object, not an _.isUndefined() invocation of the function. .reject(_.isUndefined) Now the state will look like this: [ {Roll20 Graphic Object}, {Roll20 Graphic Object}, {Roll20 Graphic Object}, ...] Next I want to only keep the tokens that represent a character. I do this with .filter() and pass it a function that returns the length of the represents id for each graphic. The default is '' , which has a length of 0 , so only tokens that represent something will be retained. .filter( t =&gt; t.get('represents').length ) Now I might be down to only a few results: [ {Roll20 Graphic Object}, {Roll20 Graphic Object}] Next I want to change my representation to contain the character and the token, so I map across my state and return a new object with the token and the character: .map( t =&gt;({token:t,character:getObj('character',t.get('represents'))})) Now my state could look like this: [ { token: {Roll20 Graphic Object} character: {Roll20 Character Object}},{ token: {Roll20 Graphic Object} character: undefined }] Next I get rid of any undefined characters (technically, this shouldn't happen since I checked for only tokens that represent, but in the odd case that a token represents a character that doesn't exist, I want to ignore them.). This is just like above, but since my undefined is in a property, I pass a function that checks that property: .reject( o =&gt;_.isUndefined(o.character)) Now my state would be something like: [ { token: {Roll20 Graphic Object} character: {Roll20 Character Object}}] Next I want to change my representation again to be just the output for each token/character entry. I use map to take in the {token:...,character:...} object and return a string: .map( o =&gt; `&lt;div&gt;&lt;div&gt;&lt;img style="max-width: 3em; max-height: 3em; border: 1px solid #999; background-color: white;" src="${o.token.get('imgsrc')}"&gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;${o.character.get('name')}&lt;/b&gt;&lt;/div&gt;&lt;/div&gt;`) Now my state will be something like: ["&lt;div&gt;&lt;div&gt;&lt;img ... src="..."&gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;ABC Character&lt;/b&gt;&lt;/div&gt;&lt;/div&gt;"] and lastly, I take this array into .tap() in whole, join it together and output it as a whisper. .tap(out =&gt; { sendChat('',`/w "${who}" ${out.join('')}`); }); Hopefully that helps you understand what's going on with the chaining stuff. Jakob will point out (if I don't mention it first) that there are more idiomatic ways of doing this in modern Javascript. =D&nbsp; I still think it's worthwhile to learn this pattern as it is somewhat clearer in many cases, and since it's the basis for TAS, will make understanding TAS easier. =D
1523985973
The Aaron
Pro
API Scripter
Moonpile said: I'm a self taught programmer and I've often said that everything I've ever truly learned about programming has come from trying to make RPG related tools, since I was 10 with my Atari 800 and BASIC.&nbsp;&nbsp; A buddy of mine used to say that everyone learns programming so they can write an RPG.&nbsp; I have to say that when I worked in the Video Game Industry, that was certainly often the case. =D&nbsp; I started on a TI-99-4A (with optional speech adaption pack!) and started writing an RPG that quickly filled the memory. =D&nbsp; (32k ought to be enough for anyone!)&nbsp; &nbsp;Years later I realize that I was keeping separate copies of every graphic for every position, but hey.. hindsight. =D
1523986656
The Aaron
Pro
API Scripter
Here's the idiomatic Javascript version: on('ready',() =&gt; { &nbsp; on('chat:message',(msg) =&gt; { &nbsp; &nbsp; if('api' === msg.type && /!show-rep\b/i.test(msg.content) ){ &nbsp; &nbsp; &nbsp; let who = getObj('player',msg.playerid).get('displayname'); &nbsp; &nbsp; &nbsp; if(msg.selected){ &nbsp; &nbsp; &nbsp; &nbsp; sendChat('',`/w "${who}" ${ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; msg.selected &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .map( o =&gt; getObj('graphic', o._id) ) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .filter( o =&gt; undefined !== o ) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .filter( t =&gt; t.get('represents').length ) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .map( t =&gt;({token:t,character:getObj('character',t.get('represents'))})) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .filter( o =&gt;undefined !== o.character) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .map( o =&gt; `&lt;div&gt;&lt;div&gt;&lt;img style="max-width: 3em; max-height: 3em; border: 1px solid #999; background-color: white;" src="${o.token.get('imgsrc')}"&gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;${o.character.get('name')}&lt;/b&gt;&lt;/div&gt;&lt;/div&gt;`) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .join('') &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }`); &nbsp; &nbsp; &nbsp; } else { &nbsp; &nbsp; &nbsp; &nbsp; sendChat('',`/w "${who}" &lt;b&gt;No tokens selected&lt;/b&gt;`); &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; } &nbsp; }); }); The only difference is that the chaining is implicit to the Javascript Array objects, the rejects become negated filters, and there's no tap so the result is just joined and passed into the output of sendChat directly using Javascript Template Literals.
1524029618
vÍnce
Pro
Sheet Author
G G said: I don't really understand how underscore works, which is why I can't get to grips with TAS either. I'm still at a pretty basic javascript level, and the way you chain multiple operations together with TAS/underscore is completely alien to me. I can use TAS for the simplest operations, like encumbrance (add up all items of a specific name in each row of a repeating set), but have no idea how it actually works, I'm just copying the example. If I have to do something more complicated, I'm completely lost and have no idea how to progress. That's my failing though. I have no idea how to get from where I am now, to the stage where i understand these things. There seems such a huge gulf, with so many concepts and principles to pick up first, and online instruction is not geared to the beginner as far as I can find. I'll have a reach of your guide to functions above later, when I'm more awake and see if it helps any :) THIS and in regards to my own struggles with javascript... “I can’t lie to you about your chances, but… you have my sympathies.” &nbsp;- Ash ( <a href="https://www.youtube.com/watch?v=cS3612EIcF4" rel="nofollow">https://www.youtube.com/watch?v=cS3612EIcF4</a> ) That said.&nbsp; Thank you very much Aaron for going the extra mile(as always) and clarifying through extensive example.
1524043472
GiGs
Pro
Sheet Author
API Scripter
lol at your link, Vince.
1524052137
The Aaron
Pro
API Scripter
=D