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

Creating an Object that Holds specific character._id and character_name.

Hi All: My JavaScript experience is zilch, but I did complete the sololearn.com JS tutorial. And I spent the last couple of weeks grinding out a custom character sheet for my homebrew system. Anyway, I am just dipping my toe into the API script/console and so I hope its okay to come here and get some mentoring? This is the little bit that I have figured out: on("ready", function() {     var oRune = oRune || {};     oRune.xTestorName = getAttrByName("-LHOo_pVgMtkTOT7_YzZ", "character_name");     log(oRune.xTestorName + " is the name of the First Test character.");     oRune.AllObjs = getAllObjs();     log(oRune.AllObjs); }); oRune is the "pseudo namespace" I am using for my script. So what I really want to understand is how to pull out from .AllObjs each character's character._id and character_name and put that into a new object (basically creating a cross-reference table-object). Then I'll try to loop log to the console some output like: The characters currently in the campaign are: Name: Rick; ID#: -abc... Name: Morty; ID#: -dEf... Name: Beth; ID#: -ghI... Name: Jerry; ID#: -KLm... This is mainly a learning exercise so I can start building up some skills.  Anyway, thanks in advance for any help.
1531625671

Edited 1531625952
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
Tom said: Hi All: My JavaScript experience is zilch, but I did complete the sololearn.com JS tutorial. And I spent the last couple of weeks grinding out a custom character sheet for my homebrew system. Anyway, I am just dipping my toe into the API script/console and so I hope its okay to come here and get some mentoring? That's exactly what this forum is for, and welcome to the scripting community. Now, as to your actual question. I would avoid using getAllObjs as it will cause some high overhead as your campaign gets larger (it returns all characters, handouts, players, graphics, pages, etc). This eventually becomes a massive amount of data, and I'm having trouble coming up with a use case where you'd need it all in a single object. Additionally findObjs, which is what I'm going to use below, has been optimized to be extremely fast at returning data; I'm not sure if anything similar has been done to getAllObjs. I'd recommend doing something like this: const characters = findObjs({type:'character'});//returns an array of all the characters in the campaign const charactersIndexedByName = _.reduce(characters,(memo,char)=>{//iterate through the returned characters, and assemble a new object (the memo) see underscore.org for more info on underscore functions     memo[char.get('character_name')]={name:char.get('character_name'),id:char.id};//You could instead index by character id of course     return memo;//pass the new object to the next instance of the iteration },{}); _.each(charactersIndexByName,(index)=>{//iterate through the index, and log it all.     log('name: '+index.name+' |id: '+index.id); }); I would also look into changing how you are doing your nameSpacing. I tend to favor Aaron's method, although since I'm just a tyro I couldn't explain to you why it is better. Hope that helps, Scott
1531627014
GiGs
Pro
Sheet Author
API Scripter
Here's a basic template using aaron's method, I believe .
1531628752
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
Thanks GG. That's exactly what I meant.
1531675507

Edited 1531675569
The Aaron
Pro
API Scripter
Good info in that thread G G linked.&nbsp; Here's my current template for API scripts: // Github: &lt;WHERE YOU STORE IT&gt; // By: &lt;YOU&gt; // Contact: <a href="https://app.roll20.net/users/&lt;YOUR" rel="nofollow">https://app.roll20.net/users/&lt;YOUR</a> ID&gt; const NAME = (() =&gt; { // eslint-disable-line no-unused-vars const version = '0.1.0'; const lastUpdate = 1531675536; const schemaVersion = 0.1; const checkInstall = () =&gt; { log('-=&gt; NAME v'+version+' &lt;=- ['+(new Date(lastUpdate*1000))+']'); if( ! state.hasOwnProperty('NAME') || state.NAME.version !== schemaVersion) { log(` &gt; Updating Schema to v${schemaVersion} &lt;`); state.NAME = { version: schemaVersion }; } }; const handleInput = (msg) =&gt; { if (msg.type !== "api") { return; } let args = msg.content.split(/\s+/); switch(args[0]) { case '!NAME': break; } }; const registerEventHandlers = () =&gt; { on('chat:message', handleInput); }; on('ready', () =&gt; { checkInstall(); registerEventHandlers(); }); return { // Public interface here }; })(); As mentioned in the link above, this is called the Revealing Module Pattern.&nbsp; It's nice for several reasons, not least of which are: It doesn't pollute the global name space unnecessarily It encapsulates all of your functionality and prevents accidental dependencies, unintentional side-effects, and unintended use. It allows you to expose a minimal interface for other scripts, should you find you want to do that. It's a recognizable structure that you and every one using your script will recognize All of my released scripts follow basically this pattern.&nbsp; For small one-off scripts, which I call "Snippets", I use the pattern you have above of nesting some functionality inside an on('ready',...) event handler.&nbsp; In the case where you don't have an interface for other scripts, that provides all the benefits bulleted above. Speaking to optimization, findObjs() is much more efficient in the case where you provide it with a type as Scott did in his example.&nbsp; That lets it prune the search tree to only that subset of objects at almost no cost. Just giving some feed back on your original code: on("ready", function() { // This will set the local variable oRune to the global variable oRune, or an empty object {} &nbsp;&nbsp;&nbsp;&nbsp;// That probably isn't what you need here. simply const oRune = {}; is sufficient, but see below. var oRune = oRune || {}; &nbsp;&nbsp;&nbsp;&nbsp;// you could define oRune here by compositing the results of these calls: /* const oRune = { xTestorName: getAttrByName("-LHOo_pVgMtkTOT7_YzZ", "character_name"), AllObjs: getAllObjs() }; */ oRune.xTestorName = getAttrByName("-LHOo_pVgMtkTOT7_YzZ", "character_name"); log(oRune.xTestorName + " is the name of the First Test character."); oRune.AllObjs = getAllObjs(); log(oRune.AllObjs); }); Building lookups is super useful.&nbsp; Here's building a character id to character name lookup as I would write it: const cLookup = findObjs({type:'character'}).reduce((m,c)=&gt;{m[c.id]=c.get('name'); return m},{}); And logging them all: Object.keys(cLookup).map(k =&gt; log(`name: ${cLookup[k]} |id: ${k}`)); Many ways to skin a cat in the API. =D And as they've said, definitely post back with more questions, we love helping people.&nbsp; I'll talk about coding till I'm blue in the face. =D
1531696270
Kirsty
Pro
Sheet Author
Aaaaaaand stolen. Thank you, Aaron.
Hi Everyone: My wife was trying to kill me with outside projects in the blistering heat over the weekend and was too wiped out to even play with the code until today. I swear she's trying to cash in on my life insurance policy (j/k)! Anyway, this will take me quite some time to root through as I have so little programming experience but I am going to playing with each and every suggestion above to see if I can really follow what's going on.&nbsp; Thank you all so much for the assist. I truly appreciate it. The help gives me hope that I will be able to do this.
1531843324
The Aaron
Pro
API Scripter
I'm serious when I say I'll talk about coding in excruciating detail.&nbsp; (in fact, I think you can find me talking about it in excruciating detail by searching the api forum for excruciating detail.... yep:&nbsp; <a href="https://app.roll20.net/forum/post/6237754/slug%7D" rel="nofollow">https://app.roll20.net/forum/post/6237754/slug%7D</a> &nbsp;) Definitely post back if you have any questions at all.
Scott &amp; Others: Scott C. said: I'd recommend doing something like this: const characters = findObjs({type:'character'});//returns an array of all the characters in the campaign const charactersIndexedByName = _.reduce(characters,(memo,char)=&gt;{//iterate through the returned characters, and assemble a new object (the memo) see underscore.org for more info on underscore functions &nbsp;&nbsp;&nbsp;&nbsp;memo[char.get('character_name')]={name:char.get('character_name'),id:char.id};//You could instead index by character id of course &nbsp;&nbsp;&nbsp;&nbsp;return memo;//pass the new object to the next instance of the iteration },{}); _.each(charactersIndexByName,(index)=&gt;{//iterate through the index, and log it all. &nbsp;&nbsp;&nbsp;&nbsp;log('name: '+index.name+' |id: '+index.id); }); Okay, I've read once through everyone's suggestion and I am getting the gist of quite a bit there, but I am still pretty shaky so I hope that you all will be okay with me asking questions about each of the suggestions rather than just trying to adopt a solution?&nbsp; I am not looking for the shortcut "right" way to do something as much as I am trying to learn and understand this stuff.&nbsp;I'll try to work through one suggestion at a time in the order the suggestions were given if that's okay with folks. My hope is that I'll pick up skills from investigating each suggestion and get to the point where I can interpret suggestions with more clarity and confidence. Thanks everyone. I am trying hard and will diligently check references given, however in this case, I couldn't get to underscore.org as the SSL Cert is invalid 'fuweb.com uses an invalid security certificate' according to Firefox -- also Chromium blocked me from going there as well. So I don't really know how to proceed there at the moment, I'll search for "javascript underscore functions" if no one has something specific they'd like me to look at. As you will see below I am quite lost as to the second statement from the example code Scott provided. I understand the result (I believe) but not the syntax that got the result. Maybe the easiest way to help me would be to give me the -- I don't know what to call it -- the general syntax labeling of the charactersByName statement (I think I got a good handle on the other statements): i.e. charactersIndexedByName // Name of object being created. const // Object type. _.reduce&nbsp;// ??? A function for objects used in this case to filter out unwanted properties from the preexisting object. _.reduce(characters, (memo,char) // ??? Parameter 'characters' is the preexisting object to be processed. &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;// ??? Parameter 'memo' is the new object that the preexisting object is being reduced to. &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;// ??? Subparameter? 'char' is ___ =&gt; // ??? is the _____ operator??? memo[char.get('character_name')] // ??? Overall this creates a ??? .get // ??? Is a common method for objects, here it ____ ('character_name') // ??? This creates the memo.character_name property??? ={name:char.get('character_name'),id:char.id} // ??? Overall this populates properties from the preexisting object to the new memo object. name: // ??? create a 'name' property and populate it from the preexisting object 'character_names' properties . id: // ??? create an 'id' property and populate it he preexisting object 'char_id' properties.&nbsp; Also, I don't understand why you: 'get' the character_name but you only reference char.id. have a ,{} in the },{}); closing line of your statement? FWIW from the 'scratch the surface' Sololearn.com tutorial: &nbsp;While many programming languages support arrays with named indexes (text instead of numbers), called associative arrays, JavaScript does not. However, you still can use the named array syntax, which will produce an object. For example: var person = []; //empty array person["name"] = "John"; person["age"] = 46; document.write(person["age"]); //Outputs "46" Now, person is treated as an object, instead of being an array. The named indexes “name” and “age” become properties of the person object. As the person array is treated as an object, the standard array methods and properties will produce incorrect results. For example, person.length will return 0. Remember that JavaScript does not support arrays with named indexes. In JavaScript, arrays always use numbered indexes. It is better to use an object when you want the index to be a string (text). Use an array when you want the index to be a number. If you use a named index, JavaScript will redefine the array to a standard object.&nbsp;
P.S. My wife 'forced' me to rebuild our porch after I first asked for help. Nearly killed me in the hot sun and just getting back to feeling normal today. I think she was trying to collect on my life insurance police but I tricked her and survived. :)
1531851100
The Aaron
Pro
API Scripter
The right URL is&nbsp; <a href="https://underscorejs.org/" rel="nofollow">https://underscorejs.org/</a> That other looks like a phishing site.
1531852335

Edited 1531852429
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
Tom said: Scott &amp; Others: ...snip... Okay, I've read once through everyone's suggestion and I am getting the gist of quite a bit there, but I am still pretty shaky so I hope that you all will be okay with me asking questions about each of the suggestions rather than just trying to adopt a solution?&nbsp; I am not looking for the shortcut "right" way to do something as much as I am trying to learn and understand this stuff.&nbsp;I'll try to work through one suggestion at a time in the order the suggestions were given if that's okay with folks. My hope is that I'll pick up skills from investigating each suggestion and get to the point where I can interpret suggestions with more clarity and confidence. Thanks everyone. This a great way to do it. We all start somewhere (and I started my coding on Roll20 as well, and not from as solid a base as you've got), just ask Aaron how much I pestered him (and still do) ;) I am trying hard and will diligently check references given, however in this case, I couldn't get to underscore.org as the SSL Cert is invalid 'fuweb.com uses an invalid security certificate' according to Firefox -- also Chromium blocked me from going there as well. So I don't really know how to proceed there at the moment, I'll search for "javascript underscore functions" if no one has something specific they'd like me to look at. Ah, that's my bad on the web address. It should be underscorejs.org I'll try to answer your code questions below: charactersIndexedByName // Name of object being created. const // Object type. &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;//Not quite, it's similar to var, but unlike variable, you can't point this to a new thing, so it will always reference the object it starts out referencing. You can change the contents of the object. _.reduce&nbsp;// ??? A function for objects used in this case to filter out unwanted properties from the preexisting object. _.reduce(characters, (memo,char) // ??? Parameter 'characters' is the preexisting object to be processed. &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;//Scott - So, this is an underscore library function (denoted by the _). It takes three arguments(the third is technically optional, although I've never gotten the hang of doing it without the third) &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;//The arguments are the iterable object (a JS object or an array) to iterate through, a callback function that will do some sort of work on the object/array, and then the third argument is typically called memo. It stores the results of the modifications we do to each piece of the object as we iterate through it. &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;//The call back function that is the second argument accepts the memo as its first argument and then the second argument is the piece of the iterable object we are working on during this instance. You always return memo at the end of the call back function so that your work is saved, and there is a memo to work with in the next &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;// ??? Parameter 'memo' is the new object that the preexisting object is being reduced to. &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;// ??? Subparameter? 'char' is ___ &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;//The specific character entry that we are working on at the time =&gt; // ??? is the _____ operator??? &nbsp;&nbsp;&nbsp;// This is called fat arrow syntax. It's shorthand for function(memo,char) = {}. Aaron can better explain the benefits of doing it memo[char.get('character_name')] // ??? Overall this creates a ??? &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;//so we are creating a object of all characters indexed by name, so this creates a key in the memo object using the characters name .get // ??? Is a common method for objects, here it ____ // .get and .set are used to manipulate roll20 objects as they are actually pseudoObjects and most of their keys can't be directly interacted with like you would a normal object (the exception is id, which you can use the .id syntax or ['id'] syntax to access ('character_name') // ??? This creates the memo.character_name property??? //We're telling .get what piece of the character we want to extract ={name:char.get('character_name'),id:char.id} // ??? Overall this populates properties from the preexisting object to the new memo object. &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;//Yep name: // ??? create a 'name' property and populate it from the preexisting object 'character_names' properties . &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;//Would probably be more accurate to say a name key id: // ??? create an 'id' property and populate it he preexisting object 'char_id' properties. //as with name, more accurately a key, but otherwise, yes Also, I don't understand why you: 'get' the character_name but you only reference char.id. have a ,{} in the },{}); closing line of your statement? So, the answers to these are in my answers in the code above, but just to make sure they get noticed. .get is how we get the values from the Roll20 pseudoObjects. id is the only key of the pseudoobject that we can directly reference as if it was a key in a normal object. The {} at the end of the _.reduce is the third argument, the initial memo state. Here I'm defining it as an empty object, but it could be a string, a number, a more complex object. Whatever you need it to be based on how you are manipulating the initial object that we are reducing. FWIW from the 'scratch the surface' Sololearn.com tutorial: &nbsp;While many programming languages support arrays with named indexes (text instead of numbers), called associative arrays, JavaScript does not. However, you still can use the named array syntax, which will produce an object. For example: var person = []; //empty array person["name"] = "John"; person["age"] = 46; document.write(person["age"]); //Outputs "46" Now, person is treated as an object, instead of being an array. The named indexes “name” and “age” become properties of the person object. As the person array is treated as an object, the standard array methods and properties will produce incorrect results. For example, person.length will return 0. Remember that JavaScript does not support arrays with named indexes. In JavaScript, arrays always use numbered indexes. It is better to use an object when you want the index to be a string (text). Use an array when you want the index to be a number. If you use a named index, JavaScript will redefine the array to a standard object.&nbsp; While this is probably technically correct, it's better practice to just declare person as an object and work with it like that, as they actually say later on. Not sure if that will all make sense, but hopefully it helps. EDIT: Tiny ninja by Aaron ;)
1531853259

Edited 1531853857
The Aaron
Pro
API Scripter
For a discussion of what const means (as well as let and var ) check out (about half way down the post):&nbsp; <a href="https://app.roll20.net/forum/permalink/6255896/" rel="nofollow">https://app.roll20.net/forum/permalink/6255896/</a> Probably easiest to understand by talking about the Map and Reduce operations in general.&nbsp; Map and Reduce are two of the most important operations for manipulating data in modern computer science.&nbsp; Whole systems are built that only perform Map and Reduce (Such as Hadoop).&nbsp; Underscore JS has a pretty good simple example of each here:&nbsp; <a href="https://underscorejs.org/#map" rel="nofollow">https://underscorejs.org/#map</a> Note that map and reduce haven't always had native support in the Javascript language, which is why there are _.map() and _.reduce() functions in Underscore.js.&nbsp; I'll talk about them in general and then give a simple example in Underscore syntax and Javascript ES6 syntax. Map The Map operations is "Map a Function across a Collection of Data and return a new Collection with the Result of the Function).&nbsp; Mathematically, you'd probably write it something like map(f,[a,b,c]) ::= [f(a),f(b),f(c)] or some such (Help me out, Jakob!). Practically speaking, Map takes an array of values and returns and array of the result of calling a function on those values.&nbsp; The classic example is squaring a list of integers.&nbsp; You could write it out with a loop like this: let data = [1,2,3,4,5]; let result = []; for(let i = 0; i &lt; data.length; ++i){ &nbsp;&nbsp;&nbsp;&nbsp;result.push( data[i] * data[i] ); } log(result); With Map, you can do it more succinctly (ES6): let data = [1,2,3,4,5]; let result = data.map( function( value ) { return value * value; }); log(result); or (Underscore.js): let data = [1,2,3,4,5]; let result = _.map( data, function( value ) { return value * value; }); log(result); The beauty of this is that you don't necessarily need to write the function where you're calling it, so const getLen = ( s ) =&gt; s.length; let data = ['bob', 'nancy', 'sue', 'paul', 'tom']; let result = data.map(getLen); log(result); Just remember that Map will return a collections where the elements are transformed by a supplied function.&nbsp; Reduce The Reduce operation is "Reduce a Collection of Data to the Result of a Function applied successively to itself and the next element of Data."&nbsp; That's a little wordy, but mathematically, it's something like reduce(f,[a,b,c],m) ::= f(f(f(m,a),b),c) (or something like that.. Jakob? =D). Practically speaking, Reduce takes a collection of values and returns some accumulated value.&nbsp; The classic example is summing a list of integers.&nbsp; You could do it with a loop: let data = [1,2,3,4,5]; let result = 0; for(let i = 0; i &lt; data.length; ++i){ &nbsp;&nbsp;&nbsp;&nbsp;result = result + data[i]; } log(result); With Reduce, you can do it more succinctly (ES6): let data = [1,2,3,4,5]; let result = data.reduce( function(m, value) { return m + value; }, 0 ); log(result); or (Underscore.js): let data = [1,2,3,4,5]; let result = _.reduce( data, function(m, value) { return m + value; }, 0 ); log(result); Reduce is very powerful because it doesn't just have to build numbers, it can build any sort of value: let data = [1,2,3,4,5]; let oddEven = data.reduce( (m, v) =&gt; { m[v%2 ? 'even':'odd'].push(v); return m;}, { odd:[],even:[]}); log(oddEven); Most often when you see Reduce, it's to build a new collection in a new format from an old collection. Syntax Map takes a collection and a function to apply&nbsp; ES6's map only operates on arrays, Underscores will work on arrays or objects (basically, key:value):&nbsp; let RESULT = ARRAY.map( FUNCTION ); let RESULT = _.map( ARRAY|OBJECT , FUNCTION ); The function is passed the several arguments, usually you just deal with the first one, the value: function( VALUE, INDEX, ARRAY ) And must return the new value for that index.&nbsp; (Note that the original collection is not modified.) Reduce is slightly more complicated.&nbsp; It takes a collection, a function and an initial value (called a memo).&nbsp;&nbsp; let RESULT = ARRAY.reduce( FUNCTION, MEMO ); let RESULT = _.reduce( ARRAY|OBJECT, FUNCTION, MEMO ); And it's function is passed the return of the prior call of the function (or the memo value if this is the first execution), the value, the index, and the collection, but you usually only deal with the first two: function( MEMO, VALUE, INDEX, COLLECTION ); Must always return the next memo value, which becomes the final result.&nbsp; If you ever have an error with a reduce not working, it's almost always that you didn't return the memo. Reduce has one more thing to be aware of.&nbsp; If you don't supply a memo to the initial call, the first value of the collection is used instead.&nbsp; I could have written the sum call above as: let result = data.reduce( function(m, value) { return m + value; }); and gotten the same result. Edit: Supplemental ninja by Scott! =D Edit, the second: BTW, all these code samples should work in the Roll20 API Sandbox, so feel free to experiment with them!
1531853600
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
Yeah, but your explanation is, as usual, much easier to follow
1531854030
The Aaron
Pro
API Scripter
=D Once you learn Map and Reduce, you find that much of the initial parts of your programs become map and reduce operations. (In fact, that's the whole point/observation of Hadoop.)&nbsp; They are particularly useful in "Big Data" as Map in particularly is massively parallelizable.
lol Scott:&nbsp; Aaron, G G, others and yourself have given me tons to chew on. I'll come back when I've worked through, tested, and digested as much as I can on my own! You guys are really really cool!
1531855535
The Aaron
Pro
API Scripter
=D
1531861319

Edited 1531861684
Scott &amp; Others: Scott C. said: I'd recommend doing something like this: const characters = findObjs({type:'character'}); const charactersIndexedByName = _.reduce(characters,(memo,char)=&gt;{ memo[char.get('character_name')]={name:char.get('character_name'),id:char.id}; return memo; },{}); _.each(charactersIndexByName,(index)=&gt;{ log('name: '+index.name+' |id: '+index.id); }); [Comments removed by Tom.] After going through everyone's responses a couple of times I finally thought I could follow it and tried running the above as an Roll20 API script I but I get this error: ReferenceError: charactersIndexByName is not defined EDIT: Oops pulled the trigger on this too quick, I now see&nbsp; charactersIndexByName&nbsp; should have been&nbsp; charactersIndexedByName&nbsp; there was an "ed" missing there. But now the sandbox seems to be stuck on spinup? I suspect that I don't have the code encapsulated in an event or something like that? I don't see how this would run as a Roll20 script without there being some event that calls it? Also -- I am not looking for detailed answers here, a loose explanation would more than suffice -- so anyway: Does the underscore.js library have to be supported by the browser, installed at the server, or both? Outside of roll20.net would you need to have the library stored on your computer somewhere and some sort of include statement in your script? It looks like you can use Node.JS to get the library on your computer but I've only used Node.JS to run an etherpad service on my home network. I haven't really used Node.JS in a development sense.
1531862379

Edited 1531862436
The Aaron
Pro
API Scripter
You don't need to do anything to have access to Underscore.js in the API.&nbsp; The _ is defined globally in the sandbox, so you just use it. You don't need to encapsulate that in an event, but I would recommend wrapping it in on('ready',...).&nbsp; In particular, you won't get any output if it isn't delayed until the ready event. on('ready', ()=&gt;{ const characters = findObjs({type:'character'}); const charactersIndexedByName = _.reduce(characters,(memo,char)=&gt;{ memo[char.get('character_name')]={name:char.get('character_name'),id:char.id}; return memo; },{}); _.each(charactersIndexedByName,(index)=&gt;{ log('name: '+index.name+' |id: '+index.id); }); }); A Brief Discussion of the API Sandbox Model: Behind the scenes, this is the order of events that occurs when you save a script: Concatenate all scripts together into one single script file. Run that script precisely one time. Load all the Roll20 Objects, triggering create events on each one. Issue the ready event precisely one time. Issue events as needed by actions on the table top. Given the above, each time you save your script, it will get run once as step 2.&nbsp; If it doesn't register for any events, it will only do anything that one time at start.&nbsp; Since no objects are loaded yet, calling findObjs() isn't going to find anything.&nbsp; This is important and intentional.&nbsp; If you wrote a script that needed to catalog all the graphics in the game, getting them each as a create event greatly simplifies your script structure when building the initial data set for your script. In practice though, you almost always want to ignore events at startup, and not register event handlers until the ready event is issued.&nbsp; That's easiest to do by registering a single event in the initial execution of your script, the ready event.
1531903446
Jakob
Sheet Author
API Scripter
Nah, Aaron, I don't have anything to add, your explanations are better than a formula :D. As to Tom's question, if you were to use underscore in, say, Node.js on your computer, you would have to install it. It would need to be said, though, that Underscore is a bit outdated, you should probably use Lodash instead (or, depending on how deep down the rabbit hole you want to go, something completely different like Ramda...) .
1531920390

Edited 1531920448
The Aaron
Pro
API Scripter
Actually, Underscore.js and Lodash have different design goals.&nbsp; Lodash split off of Underscore.js when there wasn't agreement by the team on what choice to make when implementing the algorithms behind the functions. Underscore.js always uses the fastest possible implementation for the platform, including built-in functions if they are available. Lodash always uses the same implementation to give a uniform operation and never uses built-in functions. That makes Underscore.js the ideal choice for a sandbox, as you will only ever be on one platform, so there's no point in arbitrarily choosing a slower implementation.&nbsp; Lodash is kind of the Harrison Bergeron approach to software. =D As for being outdated... Underscore.js v1.9.1 was released on May 31, 2018 Lodash v4.17.10 was released on Apr 24, 2018 Ramda looks cool.&nbsp; I'm a fan of purple. =D
Hi All: The support here is AMAZING!!! I apologize for not being able to articulate my issues and questsions in precise terms, that's 1 part being a scatter-brain and 3 parts having so little server-side and JavaScript client-side www knowledge. Even my overall coding experience is very limited as well as intermittent over my lifetime. I am trying to use the correct terminology for you all, but it's not easy just yet. However, I have built a couple of desktop applications many years ago so... what I do remember is likely to be fuzzy and certainly outdated. I mention this as a "heads up" because I am quite positive that I'll be mucking up terms and modern programming techniques regularly here at the start. Case in point, what I meant by "encapsulated" was that I thought I might need to trigger Scott's code somehow. As I recall now, that was an object oriented programming term (I have no idea if it still is) and so maybe that's what Aaron thought I was thinking about? Again, so sorry for the confusion I cause. The API Sandbox explanation is great, thanks Aaron. Invoking the on ready event (as provided by Aaron) worked and I did get output: Spinning up new sandbox... "name: undefined |id: -LHOo_pVgMtkTOT7_YzZ" Restarting sandbox due to script changes... Previous shutdown complete, starting up... Spinning up new sandbox... "name: undefined |id: -LHi8JqNvw-fS3cPihca" The first time I only had 1 character, and the second time I had 5 {Rick, Morty, Summer, Jerry, and Beth} and I tested @{selected|character_name} for all 5 in the campaign chat window and all the names came back. Still playing with it, but that's where I am ATM.
1531931750
The Aaron
Pro
API Scripter
No worries!&nbsp; We love to discuss this stuff, so if it doesn't bother you to hear what we thing you mean, it won't bother us to tell you. =D
1531931815

Edited 1531932062
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
Hi Tom. Sorry, I've been working in character sheets a lot lately and forgot that the key for a character's name using .get is "name", not "character_name" like in sheet workers. Change all the .get('character_name') To .get('name') Sorry for the confusion. Check out the API object wiki for all the properties.
Thanks Jakob, that was what I was trying to get at! Sounds like using underscore is not going to help me work on other projects unless the server has the library installed. And if I were to try to do some sort of Electron desktop app, then I'd have to know how to include this library. Anyway, I've wanted to write a document management and archiving system for years now and thought maybe I could leapfrog off my RPG automation skills to that at some point in the future... probably in the year 2525 at the pace I'm going. =) Jakob said: As to Tom's question, if you were to use underscore in, say, Node.js on your computer, you would have to install it.
1531932460

Edited 1531932475
The Aaron
Pro
API Scripter
Ah!&nbsp; You could totally use it in that sort of case.&nbsp; Your Electron App is basically just a Node App with Electron installed.&nbsp; You'd just install Underscore (or Lodash) along side it. If you started from something like this:&nbsp; <a href="https://github.com/electron/electron-quick-start" rel="nofollow">https://github.com/electron/electron-quick-start</a> You'd just need to install it in the app with something akin to: npm install underscore Then in your source files where you're using it, you'd just need to include it: const _ = require('underscore'); or (depending on your setup with babel, webpack, etc) import _ from 'underscore'; (For the record, I much prefer yarn over npm )
LOL Aaron! Scott C. AHHHHHH!!! That makes sense and works! So sweet! No worries, I haven't spent any significant time pulling my hair out on that. But it does bring up another question (of course) -- So say instead of .name I wanted something off my character sheet like 'Caste' (I have social caste in my game)? I tried this but I got undefined. Am I not able to pull attributes off the sheet that are not listed as a property on the Roll20 API character object? I tried this but got 'undefined' for caste. on('ready', ()=&gt;{ const characters = findObjs({type:'character'}); const charactersIndexedByName = _.reduce(characters,(memo,char)=&gt;{ memo[char.get('name')]={name:char.get('name'),id:char.id,caste:char.get('Caste')}; return memo; },{}); _.each(charactersIndexedByName,(index)=&gt;{ log('name: '+index.name+' |id: '+index.id+' |caste: '+index.Caste); }); });
1531933731

Edited 1531934776
The Aaron
Pro
API Scripter
Name is a special case.&nbsp; Attributes are their own objects that must be fetched separately. on('ready', ()=&gt;{ const characters = findObjs({type:'character'}); const charactersIndexedByName = _.reduce(characters,(memo,char)=&gt;{ memo[char.get('name')]={ name:char.get('name'), id:char.id, caste: ( findObjs({ type: 'attribute', name: 'caste', characterid: char.id }, {caseInsensitive: true} )[0] || {get:()=&gt;'[MISSING]'} ).get('current') }; return memo; },{}); _.each(charactersIndexedByName,(index)=&gt;{ log('name: '+index.name+' |id: '+index.id+' |caste: '+index.cast ); }); }); I'm passing findObjs() an object to match all the properties to.&nbsp; findObjs() returns an array, so I'm taking the first element with the [0].&nbsp; If there is at least one entry, I'm using it.&nbsp; If there are none, indexing the empty array returned at 0 will yield the special undefined value.&nbsp; Side Discussion: &amp;&amp; and || Javascript has an interesting philosophy on &amp;&amp; (and) and || (or).&nbsp; Everything in Javascript can be evaluated as either "truthy" or "falsey".&nbsp; The &amp;&amp; and || operators don't return boolean true or false, they return the thing that was last evaluated. In the case of &amp;&amp;, that will be the final argument if everything was "truthy", or the first "falsey" value.&nbsp; For ||, it returns the first "truthy" argument, or the final "falsey" argument.&nbsp; Both operators are "short circuiting", which means they only evaluate as far as they need to in order to return a value. In practice, this means you will see || used a lot in initializing the value of something: const foo = bar() || 10; foo will have the return value of bar() assigned, unless bar returns something "falsey", in which case foo will have 10 assigned to it. With short circuiting: const foo = bar() || qux(); foo will have the return value of bar() assigned and qux() won't be called at all, unless the return of bar() was "falsey", in which case foo will get the return value of qux(). Back to findObjs() In the event that findObjs() didn't find anything, the || will cause the second object to be the result of the parenthesized expression.&nbsp; That second object is just mocking the .get() interface to return a value to use for castes that aren't set. Note that I also added {caseInsensitve: true} as a second argument to findObjs(), so it can match caste or Caste or CaSTe, etc. Look here for the Attributes Object:&nbsp; <a href="https://wiki.roll20.net/API:Objects#Attribute" rel="nofollow">https://wiki.roll20.net/API:Objects#Attribute</a> (Edit: In my haste to beat Scott C. to the punch, I made a few errors that should be corrected now. =D)
1531933769

Edited 1531933796
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
Attributes are their own pseudoobjects. You'll need to do another findobjs: Const caste_attributes = findObjs({type:'attribute',name:'caste'}); You can also look for a specific character's caste by also telling findobjs to filter by characterid Ninja'd ;)
1531935101
The Aaron
Pro
API Scripter
Oh, one thing I forgot to mention: _.each(charactersIndexedByName,(index)=&gt;{ log('name: '+index.name+' |id: '+index.id+' |caste: '+index.cast ); }); What you have there isn't actually the index, it's the value.&nbsp; The index is the second parameter.&nbsp; That feels weird initially, but makes sense when you realize that most of the time you don't care what the index was.&nbsp; It should probably be: _.each(charactersIndexedByName,(obj)=&gt;{ log('name: '+obj.name+' |id: '+obj.id+' |caste: '+obj.cast ); }); Something else you might like is the modern Javascript Template Literal: _.each(charactersIndexedByName,(obj)=&gt;{ log(`name: ${obj.name} |id: ${obj.id} |caste: ${obj.cast}` ); }); Not much different in this case, but it does make things easier to read, and you can use ' and " both inside it without escaping them.
1531937084
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
Yeah, thanks for the correction Aaron, and yeah, I haven't gotten used to template literals yet :(
Hi All: I have some -- wait for it -- wait -- more questions!&nbsp; Just reviewing and reprocessing info. The Aaron said: Reduce is slightly more complicated.&nbsp; It takes a collection, a function and an initial value (called a memo).&nbsp;&nbsp; let RESULT = ARRAY.reduce( FUNCTION, MEMO ); let RESULT = _.reduce( ARRAY|OBJECT, FUNCTION, MEMO ); And it's function is passed the return of the prior call of the function (or the memo value if this is the first execution), the value, the index, and the collection, but you usually only deal with the first two: function( MEMO, VALUE, INDEX, COLLECTION ); Must always return the next memo value, which becomes the final result.&nbsp; If you ever have an error with a reduce not working, it's almost always that you didn't return the memo. (Geez loops seem so much more straightforward, but I am going to need to understand this if I am going to be reading and utilizing code and ideas from other scripts.&nbsp; Ok. Take a deep breath Tom. You can do this!) So to me the fact that memo is virtually required and called memo makes the _reduce function look unnecessarily verbose, so I am most likely missing something there? Anyway, as I understand it, map &amp; reduce transform the array|object. So once you get to&nbsp; let RESULT = _.reduce( ARRAY|OBJECT &nbsp;you don't have to refer to the object being transformed from anymore. So in that sense, you aren't copying values from the original object, to a new object, instead you copy the whole object right off the bat and then transform the copy to RESULT in this case. Am I conceiving this correctly?&nbsp; Assuming so it's the transforming function that has me utterly baffled still. So in Scott's suggestion (with my non-working caste addition) that function is: (memo,char)=&gt;{ memo[char.get('name')]={name:char.get('name'),id:char.id,caste:char.get('Caste')}; return memo; } In my mind I am looking at an object that has this structure: memo.this.name memo.this.id memo.this.caste etc... Is that a good way to envision the object? Probably not because we're really dealing with key&amp;value pairs? Anyway, so memo[char.get('name')] is this just setting what the index is going to be? If not, what is this doing? Is it setting the index? Scott said "so we are creating a object of all characters indexed by name, so this creates a key in the memo object using the characters name". I am confused because memo.name key&amp;value already exists doesn't it? Moving along: {name:char.get('name'),id:char.id,caste:char.get('Caste')} Now I interpret this to mean get rid of all the key&amp;values that are not memo.name, memo.id, and memo.caste? If anyone -- off the top of the head -- knows of a step-by-step or visual aid to figure this out&nbsp;it would be most appreciated? Otherwise I think I'll do a Google search on javascript reducing tutorial and see if I can find one that can break through this concrete I used to think was a brain. =)
P.S. I haven't yet went through everyone's latest responses. Maybe there's something there that will help me. I'll let you all know. Thanks again fellows!
Okay went through the latest and really love the literal that is sweet as butter there as well as the caseinsensitive&nbsp; &amp;&amp; and || tips. I don't understand where I would have found the .get('current') property/method is that in the API documentation or ?
1531940866

Edited 1531941378
The Aaron
Pro
API Scripter
There's some info here:&nbsp; <a href="https://wiki.roll20.net/API:Use_Guide#Reactive_Scripts:_Listen_to_Events.2C_Modify_Objects" rel="nofollow">https://wiki.roll20.net/API:Use_Guide#Reactive_Scripts:_Listen_to_Events.2C_Modify_Objects</a> The power of Map and Reduce really comes out when you start chaining them together: on('ready',() =&gt; { on('chat:message',(msg) =&gt; { if('api' === msg.type &amp;&amp; /!show-rep\b/i.test(msg.content) ){ let who = getObj('player',msg.playerid).get('displayname'); if(msg.selected){ sendChat('',`/w "${who}" ${ msg.selected .map( o =&gt; getObj('graphic', o._id) ) .filter( o =&gt; undefined !== o ) .filter( t =&gt; t.get('represents').length ) .map( t =&gt;({token:t,character:getObj('character',t.get('represents'))})) .filter( o =&gt;undefined !== 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;`) .join('') }`); } else { sendChat('',`/w "${who}" &lt;b&gt;No tokens selected&lt;/b&gt;`); } } }); }); This whispers to the player that calls it a message showing the tokens they have selected and the names of the characters they represent: !show-rep Notice that for each successive .map() or .filter() call there is no variable capturing the returned collection, it's just used "in place."&nbsp; Once you've internalized the functionality, reading down that is much simpler than if it were cluttered up with a bunch of variables and loops.&nbsp; There are also things the Javascript engine can do to speed up the operations since it never has to keep a given collection in scope. Incidentally, .filter() is really just a Reduce operation with a specific behavior: const filter = (a,f) =&gt; a.reduce((m,i)=&gt;[...m, ...(f(i)?[i]:[])],[]); or, less tersely: const filter = (a,f) =&gt; { &nbsp;&nbsp;&nbsp;&nbsp;a.reduce((m,i)=&gt; { &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if( f(i) ){ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;m.push(i); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}&nbsp;&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;return m; &nbsp;&nbsp;&nbsp;&nbsp;},[]); So, with Reduce, the transforming function with it's memo is a little confusing (it took me quite a while to "get it").&nbsp; Remember that memo is just "the value returned last time."&nbsp; and the 2nd parameter of the Reduce call sets what the value is for the first call.&nbsp; With Map, your transforming function is always dealing with just the one element with no knowledge of what came before and what will come next. With Reduce, your transforming function is "integrating the current value with what came before."&nbsp; In the case of a sum, it's adding the current value to the prior sums.&nbsp; In the case of filter, it's deciding if the current value should be included in the final collection. In the case of Scott's code, I'd think of memo as "the collection of processed characters" and what you put in it as "a record containing the data I've collected about the characters." Reduce builds a single object from a collection of objects.&nbsp; That "single object" might be a simple value (the case of sum), a restricted collection (the case of filter), or a whole new structure of object (the case of Scott's code). It might help to think of Map as "always returns an Array" and Reduce as "always returns an Object (which might be an Array)".&nbsp; In Scott's case, he's using Reduce because he want's a lookup by a specific value, and Objects are the general purpose Key to Value collection that Javascript has.&nbsp; His function then is doing two things.&nbsp; 1) it's building the record object and 2) it's building an indexed collection of those record objects.&nbsp; You could separate those steps into two, which might make it clearer what is going on: const charactersIndexedByName = characters.map( (c) =&gt; ({ &nbsp;&nbsp;&nbsp;&nbsp;name:char.get('name'), &nbsp;&nbsp;&nbsp;&nbsp;id:char.id, &nbsp;&nbsp;&nbsp;&nbsp;caste: ( &nbsp;&nbsp;&nbsp;&nbsp;findObjs({ &nbsp;&nbsp;&nbsp;&nbsp;type: 'attribute', &nbsp;&nbsp;&nbsp;&nbsp;name: 'caste', &nbsp;&nbsp;&nbsp;&nbsp; characterid: char.id &nbsp;&nbsp;&nbsp;&nbsp;}, {caseInsensitive: true} )[0] &nbsp;&nbsp;&nbsp;&nbsp;|| {get:()=&gt;'[MISSING]'} &nbsp;&nbsp;&nbsp;&nbsp;).get('current') &nbsp;&nbsp;&nbsp;&nbsp;})) &nbsp;&nbsp;&nbsp;&nbsp;.reduce( (m,c) =&gt; { &nbsp;&nbsp;&nbsp;&nbsp;m[c.name] = c; return m; &nbsp;&nbsp;&nbsp;&nbsp;}, {}); The .map() part now is transforming the collection of characters into a "record of what you want to know about the character", and the .reduce() is building an object that indexes the name to that object. Hope that helps!
1531941563

Edited 1531942960
Jakob
Sheet Author
API Scripter
I think it's easiest to look at .reduce (which is called fold &nbsp;in some other languages) by going through what it does. It goes through all the array elements in turn, and combines them with the memo. In iteration 0 , we have the starting case. The memo is a new empty object {}, which is passed to the function. In iteration 1, the function is then called with arguments ({}, char1), where char1 is the first character in the array. The line memo[char.get('name')]= { na me: char.get('name'), id: c har.id, }; the sets the property with name equal to the character name to the object { na me: char.get('name'), id: c har.id, } That is, after iteration 1 , the memo looks like this, if char1 has name "John" and id "abc123": { "John": { "name": "John", "id": "abc123" } } Then we go to iteration &nbsp;2 , and the function is called again with arguments (memo, char2). So it takes the existing memo object, which already has a "John" property, and sets a new property according to char2. So, we might imagine that after iteration 2 , the memo looks like the following { "John": { "name": "John", "id": "abc123" }, "Sue": { "name": "Sue", "id": "def456" } } This will go on as long as there are elements in the array, and the last memo is then returned. It's helpful to put this into a tree to visualize what the compositions looks like .. there's a (not so great) picture in the wiki article&nbsp; here &nbsp;(the right hand side tree, where nodes are composition). EDIT: Ninja'd, as expected.
1531942147
The Aaron
Pro
API Scripter
The great thing about multiple explanations is that one might make sense to someone, where as another might be confusing. =D&nbsp;
1531942772
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
Heh, heck, I'm learning things by getting these reexplained as well (I think this makes the 12th time for some of these concepts ;) )
1531947800

Edited 1531949853
The Aaron said: The great thing about multiple explanations is that one might make sense to someone, where as another might be confusing. =D&nbsp; Now that's something that I wholeheartedly agree with. For instance, some of Scott's explanations were laser-focused and helped me get a bit of an anchor. Then along comes Aaron that explains in a different way and also expands which has really helped as well. On Jakob's last post especially, DING! the light goes on! Yes yes yes!!!!&nbsp; While much -- but not all of -- Aaron's latest explanation was too far above my paygrade at the moment, but I am still excited to see it as I can foresee the advantages once I get a handle the syntax and programming logic to make it happen. I'll tinker and modify Scott's code (with Aaron's addition showing me how to handle attributes) just to get more than one attribute, different attributes, maybe get a different API property, log it out with the literal syntax. Just play with that bit and get comfy with it. Which I wasn't going to be able to do until Jakob hit me between the eyes with the object result after each iteration. Also I will reread all of the API notes completely through again as well. Then I'll come back and work through Aaron's stuff. This is really exciting but I'll need to stock up on the Excedrin that's for sure. =) Thanks All!
1531947968

Edited 1531948041
The Aaron
Pro
API Scripter
=D&nbsp; Sounds awesome! I recommend liberal use of: log(memo); and other variables. =D
1531952838

Edited 1531952902
The Caste attribute is still coming back as undefined. I have checked in the chat @{selected|caste} works. on('ready', ()=&gt;{ const characters = findObjs({type:'character'}); const charactersIndexedByName = _.reduce(characters,(memo,char)=&gt;{ memo[char.get('name')]={ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; name:char.get('name'), &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; id:char.id, &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; caste: ( &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; findObjs({ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; type: 'attribute',&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; name: 'caste', &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; characterid: char.id &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }, {caseInsensitive: true} )[0] &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; || {get:()=&gt;'[MISSING]'} &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ).get('current') &nbsp; &nbsp; &nbsp; &nbsp; }; return memo; },{}); &nbsp; &nbsp; _.each(charactersIndexedByName,(obj)=&gt;{ &nbsp; &nbsp; &nbsp; &nbsp; log(`name: ${obj.name} |id: ${obj.id} |caste: ${obj.cast}` ); &nbsp; &nbsp; }); }); And the result: Restarting sandbox due to script changes... Previous shutdown complete, starting up... Spinning up new sandbox... "name: Rick |id: -LHOo_pVgMtkTOT7_YzZ |caste: undefined" "name: Jerry |id: -LHi6cFqSXoIFxpaMjTU |caste: undefined" "name: Morty |id: -LHi6jvAlOUko2l2oLXl |caste: undefined" "name: Summer |id: -LHi6nEm-Pq9d7EaV_YL |caste: undefined" "name: Beth |id: -LHi8JqNvw-fS3cPihca |caste: undefined" I tried checking the API documentation: <a href="https://wiki.roll20.net/API:Objects#findObjs.28attrs.29" rel="nofollow">https://wiki.roll20.net/API:Objects#findObjs.28attrs.29</a> and the example for the onSheetWorkerCompleted function (because it had a findObjs() example to look at).&nbsp; <a href="https://wiki.roll20.net/API:Function_documentation#onSheetWorkerCompleted" rel="nofollow">https://wiki.roll20.net/API:Function_documentation#onSheetWorkerCompleted</a> But I can't figure out what the problem is?
1531953522
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
So, when things like this happen, I like to pull out the creation of the variable into stages. Lets try this: on('ready', ()=&gt;{ const characters = findObjs({type:'character'}); const charactersIndexedByName = _.reduce(characters,(memo,char)=&gt;{ memo[char.get('name')]={ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;name:char.get('name'),&nbsp;&nbsp;&nbsp;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; id:char.id &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; let caste = findObjs({ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; type: 'attribute',&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; name: 'caste', &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; characterid: char.id &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }, {caseInsensitive: true}); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;log(`${char.get('name')} caste Array: ${JSON.stringify(caste)}`); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;caste = caste[0]; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;log(`${char.get('name')} caste: ${caste}`); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;memo[char.get('name')].caste=caste; return memo; },{}); &nbsp; &nbsp; _.each(charactersIndexedByName,(obj)=&gt;{ &nbsp; &nbsp; &nbsp; &nbsp; log(`name: ${obj.name} |id: ${obj.id} |caste: ${obj.cast}` ); &nbsp; &nbsp; }); }); This will let us make sure that we are getting back what we think we are.
Thanks Scott: Output is: "name: Beth |id: -LHi8JqNvw-fS3cPihca |caste: undefined"
1531955742
The Aaron
Pro
API Scripter
Ah, typos... log(`name: ${obj.name} |id: ${obj.id} |caste: ${obj.cast e }` ); Sorry about that, I introduced the bug up above. =D
1531955793

Edited 1531955835
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
hehe, I totally missed that. Also, tom, the point of my code was that it would give additional logs as the cast attribute was gotten to make sure that it was returning what you expected.
lol i should have found that, i was specifically looking for simple typos.&nbsp; Jeez.&nbsp;
1531960276
The Aaron
Pro
API Scripter
Scott C. said: hehe, I totally missed that. Also, tom, the point of my code was that it would give additional logs as the cast attribute was gotten to make sure that it was returning what you expected. ^^^&nbsp; HEHEHEH
1531960370
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
Gah, It's contagious!!!!
1531981839
Jakob
Sheet Author
API Scripter
Happy that our combined explanations could help :).
Good Afternoon (at least for me) Everyone: I modified the "tutorial" code to add another attribute to the _reduced object and also separated the function from the on('ready') event. Got that to work just fine, and once again reviewed what has been offered in this thread and I am understanding even more -- to the point that I think I pretty much have the tutorial code interpreted correctly (except I am a little fuzzy as to whether something is an array, an object, and-or a key&amp;value pair). Also, I added some investigative log() 's and changed Rick to Rick Sanchez using object.dot.syntax for learning purposes only. So then I wanted to play with the scope a bit and to sort of analyze the charactersIndexedByName object (to help with my array vs object fuzziness). So here is what I came up with (for a reference point of my mindset, questions, and discussion): function listChars() { const characters = findObjs({type:'character'}); const charactersIndexedByName = _.reduce(characters,(memo,char)=&gt;{ memo[char.get('name')]={ name:char.get('name'), id:char.id, caste: ( findObjs({ type: 'attribute', characterid: char.id, name: 'caste' }, {caseInsensitive: true} )[0] || {get:()=&gt;'[MISSING]'} ).get('current'), sphere: ( findObjs({ type: 'attribute', characterid: char.id, name: 'sphere' }, {caseInsensitive: true} )[0] || {get:()=&gt;'[MISSING]'} ).get('current') }; return memo; },{}); _.each(charactersIndexedByName,(obj)=&gt;{ log(`name: ${obj.name} |id: ${obj.id} |caste: ${obj.caste} |sphere: ${obj.sphere}` ); }); log(charactersIndexedByName); log(findObjs({type:'character'})); log(charactersIndexedByName.name); //Returns undefined log(charactersIndexedByName.Rick); log(charactersIndexedByName.Rick.id); charactersIndexedByName.Rick.name = "Rick Sanchez"; log(charactersIndexedByName.Rick.name); } function showOneChar(who) { // log(charactersIndexedByName.Morty); } on('ready', ()=&gt;{ listChars(); showOneChar("Morty"); }) Issue 1: Scope So it seems that&nbsp; charactersIndexedByName &nbsp;is scoped to (and available only within) the listChars() function. Is that correct? Issue 2: Type of Object We transformed a 'character' object simply reducing but it also appears that the object "type" changes as well? To be more precise, the object has no Roll20&nbsp; charactersIndexedByName&nbsp; type? If so doesn't that mean I should be able to read and write to the object with ordinary JavaScript? But the API docs for "createObj(type, attributes)" seems to indicate that you can only create&nbsp;'graphic', 'text', 'path', 'character', 'ability', 'attribute', 'handout', 'rollabletable', 'tableitem', and 'macro' objects. Yet when I look at the above code's output, the object appears to be a non-Roll20-typed object? I am confused. LOL Obviously. Issue 3: ShowOneChar So I commented out the call for and function ShowOneChar(). It doesn't work. I'm think it firstly fails because the&nbsp; charactersIndexedByName &nbsp;object is out of scope? So I am thinking I am ready to attempt to learn how to do a character to id lookup that Aaron suggested: const cLookup = findObjs({type:'character'}).reduce((m,c)=&gt;{m[c.id]=c.get('name'); return m},{}); But I don't really don't understand this syntax. Is m standing in for memo and c standing in for character? To me it looks like it's going to pull all of the characters and not just one? Also, I'd think that it should be a callback function? And I am not seeing how the character name is being passed into this function so that the id can be returned or vice versa. Any help understanding this and-or incorporating it as a function into the tutorial example is much appreciated.
1532029272
Jakob
Sheet Author
API Scripter
So it seems that&nbsp; charactersIndexedByName &nbsp;is scoped to (and available only within) the listChars() function. Is that correct? Yup. Issue 2: Type of Object I'm sure Aaron will give a more in-depth explanation, but within the Roll20 API, there are normal Javascript objects like charactersIndexedByName, and Roll20 objects, which are special in that they are the only types of objects that allow you to interact with the Roll20 part of the interface (e.g. you can call their "set" methods to change the state of the game). characters &nbsp;is an array containing Roll20 objects ... charactersIndexedByName &nbsp;is a normal Javascript object with no roll20 objects anywhere within it. The creation advice in createObjs deals with creating Roll20 objects, not normal Javascript objects.* * in fact, it's a bit more complicated... it's probably best to think of Roll20 objects in the API as references &nbsp;to the objects existing in Roll20's backend, rather than the objects themselves... Issue 3: ShowOneChar Right, here in fact charactersIndexedByName is out of scope. You can change that by returning it in the listChars() function (add return charactersIndexedByName; at the end of it), and assigning it to a variable when calling the function: characterIndex = listChars(); Now, your showOneChar function could look like this: function showOneChar(index, who) { log(index[who]); } and you could call it as characterIndex = listChars(); showOneChar(characterIndex, "Morty"); About the cLookup function, let's expand it: const cLookup = findObjs({ type: 'character' }).reduce((m, c) =&gt; { m[c.id] = c.get('name'); return m; }, {}); This will create an object containing all characters, and the index is the other way around from what you want: it's an ID-to-index dictionary, so (to stay with my former example) log(cLookup["abc123"] === "John"); // logs true