
Great progress!
Issue 1: You are absolutely correct. Functions create a scope all their own, and only things created in that scope have access to it's contents. (This function scope is called a Closure. Each execution of the function creates a unique Closure. The Closure persists after the call to the function if there is anything created in the Closure that persists outside of it, such as a function returned from executing the original function.)
Issue 2: Taking the second half of this first, there are two different "object" types at work here. There are Javascript Objects (which is pretty much what we've been talking about up until now--key/value collections) and there are Roll20 Objects (Characters, Attributes, Paths, Graphics, etc). Whenever you're calling a Roll20 function to get an "Object", you're getting a Roll20 Object. Those functions are things like createObj(), findObjs(), getObj(), filterObjs(), allObjs(), etc. Objects passed to add/change/destroy events in the first parameter are Roll20 Objects. The second parameter is a Javascript Object that represents the prior state of the Roll20 Object, not to be too confusing.
Roll20 Objects have an public interface for getting and setting their properties (via .get() and .set() ) and destroying them ( .remove() ) and a few other things. This is because Roll20 Objects are persisted and synchronized between the API and the various connected players. A Roll20 Object is basically a specially constructed Javascript object. It looks something like:
{ id: '-Jaldkjfadf', get: (attr) => lookupAttrInPrivateRepresentation(attr), set: (attr, value) => { setAttrInPrivateRepresenation(attr,value); syncAndPersistValueThroughFirebase(); } /* ... */ }
Forcing the interface through .set() allows them to easily hook the changing of that attribute and notify everything that needs to know about it.
Getting back to the first half of Issue 2, you haven't changed the Roll20 Character Object at all, you've created a new Javascript Object which contains some of the same values. The variable name charactersIndexedByName is no more than an alias, it doesn't create a type, and a type property is not a part of the Javascript Object, it's one of the properties that Roll20 stores on a Roll20 object.
Incidentally, when you log a Roll20 object, it's converting the Roll20 Object to a format called JSON. This can be done automatically by defining a method named toJSON() on an object, which is then called whenever you use JSON.stringify( thatObject ); in your code.
Issue 3: To get ShowOneChar() to work like you want, it would need to be defined in the same scope as charactersIndexedByName. The easiest way to do that is to put them both in the on('ready',...) event:
on('ready', () => { const characters = findObjs({type:'character'}); const charactersIndexedByName = _.reduce(characters,(memo,char)=>{ memo[char.get('name')]={ name:char.get('name'), id:char.id, caste: ( findObjs({ type: 'attribute', characterid: char.id, name: 'caste' }, {caseInsensitive: true} )[0] || {get:()=>'[MISSING]'} ).get('current'), sphere: ( findObjs({ type: 'attribute', characterid: char.id, name: 'sphere' }, {caseInsensitive: true} )[0] || {get:()=>'[MISSING]'} ).get('current') }; return memo; },{}); _.each(charactersIndexedByName,(obj)=>{ 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[who]); } showOneChar('Morty'); });
Back to that cLookup thing:
const cLookup = findObjs({type:'character'}).reduce((m,c)=>{m[c.id]=c.get('name'); return m},{});
This isn't defining a function, it's creating a lookup object named cLookup. cLookup will have as keys (properties) the ids of all existing characters. Those keys (properties) will have as values the corresponding character's name.
const cLookup = /* <-- Defines the storage for the lookup */ findObjs({type:'character'}) /* <-- gets an array of all the Roll20 Character Objects in the Game */ .reduce((m,c)=>{ /* <-- Reduce to build a new collection. m is the Memo, c is each successive Roll20 Character Object */ m[c.id]=c.get('name'); /* <-- in the Memo, store for the Roll20 Character Object's ID, that Character's Name */ return m /* <-- Return the memo for the next iteration */ }, {} /* <-- Start the memo as an empty object (key/value collection ) */ );
At the end, if you need to know what a character's name is, and you have that character's id, you can just:
let name = cLookup[attr.get('characterid')];
If you wanted to do the reverse lookup, you could change what goes into the memo for the index, and what gets stored there. You usually won't need a lookup like that though, as you'll almost always have the character id, or you'll want to convert to the character id. The problem with using the name as a key is that names change and aren't unique. You could always use a reduce to build a lookup from the cLookup:
const idLookup = Object.keys(cLookup) /* <-- get the keys (ids) as an array */ .reduce((m,id)=>{ /* <-- Reduce to build a new collection. */ m[cLookup[id]]=m[cLookup[id]] || []; /* <-- Since names aren't unique, initialize to an array (or use the one we already made) */ m[cLookup[id]].push(id); /* <-- add the id to the list of ids for this name */ return m; /* <-- return the memo for the next iteration */ }, {} /* <-- start the memo as an object (key/value collection) */ );
let arrayOfMaxIds = idLookup['Max'];
In reality, you'd probably want to do a bit more, like make everything lower case, match partial words, etc... (if you wanna see REALLY crazy... checkout my Search script: https://app.roll20.net/forum/post/5250301/script-search-full-text-search-for-gms-and-players-with-respect-for-permissions-and-many-query-options )