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.

July 19 (6 years ago)
The Aaron
Pro
API Scripter

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) */ 
  );
After the above, you could find the all the ids for characters with the name "Max" by doing:
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 )

July 19 (6 years ago)

Some comments and one follow up question:

OMG! [in my best yuppie voice] I am actually understanding most of what you guys are saying with the first read through! I'll be playing with this stuff tomorrow morning, but I am following this a whole lot easier now! It's so exciting. Guys. Truly. Thank you.

Jakob: "it's probably best to think of Roll20 objects in the API as references to the objects existing in Roll20's backend, rather than the objects themselves"

Aaron: "Roll20 Objects are persisted and synchronized between the API and the various connected players... Forcing the interface through .set() allows them to easily hook the changing of that attribute and notify everything that needs to know about it."

Righto! That makes sense to me now. If I want the GM and players to see something in game (vs the console) it's pretty much going to have to be through a Roll20 API object or method.

Jakob:

const cLookup = findObjs({
  type: 'character'
}).reduce((m, c) => {
  m[c.id] = c.get('name');
  return m;
}, {});

Aaron: "This isn't defining a function, it's creating a lookup object named cLookup."

So, Aaron's lookup object was pretty much Scott's original suggestion to me (just more inline and less verbose) plus using .reduce vs. _.reduce and showing that a lookup could be id gets a name or name gets an id. Yeah, I thought this was going to be a called like e.g. cLookup('Morty') which is wrong. As I understand it now, I'll need to define the lookup object to a constant or variable at the initial point of the code block. I'll be doing that tomorrow. Today, mind is already gone. =) Is there a term for this scope or position? Like public scope, root scope, or top of the block? 

P.S. I don't want to have this all within the on('ready') as Aaron suggested because I am trying to learn how to pass information back and forth between blocks of code now.

July 19 (6 years ago)
The Aaron
Pro
API Scripter

Fantastic!  I'm very happy to hear this and your progress is great, despite being under constant threat from a matrimonial assassin!


Tom said:

If I want the GM and players to see something in game (vs the console) it's pretty much going to have to be through a Roll20 API object or method.

That's completely spot on!  All the code in the world will have zero effect on the players and the game unless it eventually manipulates a Roll20 Object via the Roll20 Object Interface or Roll20 Functions.

About Scope

There are basically 3 scopes in Javascript.  Scopes are nested, with the Global scope at the root of all other scopes. 

Global Scope

If you make something in the "root" of the file, you're in global scope.  In the Roll20 API, there isn't an object that represents the global scope. In some other contexts (like a browser) there is.  In Roll20, the only way to put something in the global scope is to define it there:

const characters = [];

There is now a variable in the global scope named characters.  As a const variable, any other script attempting to declare it will fail and prevent the sandbox from starting up.  Effectively, the name characters is now fixed to be an array.  This sounds harsh, but in reality, if you are going to put something in the global scope, this is how you should do it.  The reason is that if there IS a conflict, you want to know about it as soon as possible.

SyntaxError: Identifier 'characters' has already been declared

Is much clearer than 

TypeError: characters.reduce is not a function

Variables declared in the global scope can be accessed from anywhere, with the caveat that their existence may be shadowed by variables declared in a narrower scope:

const character = 'Bob';

const foo = () => {
    log(`foo(): My character is named ${character}.`);
};

const bar = () => {
    let character = 'Sue';  /* <-- function scope declaration shadows global */
    log(`bar(): My character is named ${character}.`);
};

foo();
bar();

Will output:

foo(): My character is named Bob.
bar(): My character is named Sue.

Function Scope

Function scope, as seen above, is when you declare variables within the confines of a function.

const foo = () => {
    let character = 'Sue';
};

There is a variable at the Function Scope named character.

Block Scope

Block scope is the scope you have inside of loops and control structures, or even just bare blocks.

const character = 'Bob';
if( 'Bob' === character) {
    const character = 'Sue';
    log(`if(): My character is named ${character}.`);
}
log(`global: My character is named ${character}.`);

This will output:

if(): My character is named Sue.
global: My character is named Bob.

Once again, shadowing happens inside the if's block.

I've written about variable hoisting in other places, so I'll skip it here.  It only applies to variables defined with the var keyword, which you should never do and thus it doesn't matter. =D

When a variable name is used, the tightest scope is searched first for a variable with that name. If it isn't found there, the next scope outward is searched, and so on, until a variable with that name is found, or it runs out of scopes.


Finally, if you wanted a function that did the lookup, an efficient way to write that would be:

const cLookup = (()=>{
    const cLookup = findObjs({type:'character'}).reduce((m,c)=>{m[c.id]=c.get('name'); return m},{});
    return (id) => cLookup[id];
})();

let charName = cLookup(someid);

This is what's called an IIFE or "Iffy".  And Immediately Invoked Function Expression.  IIFEs are unnamed functions that are not captured in variables, but are instead immediately called go create a value.  What's going on here is that the IIFE creates a function scope (a Closure) where it defines a function scoped cLookup variable which contains the id -> name data.  It then returns a function (out of the Closure) which takes an id and returns the name.  That returned function retains it's access to the variables inside the Closure, and since it's assigned to a variable in a different scope, the Closure continues to exist.

July 20 (6 years ago)

This is what's called an IIFE or "Iffy".  And Immediately Invoked Function Expression.  IIFEs are unnamed functions that are not captured in variables, but are instead immediately called go create a value.  What's going on here is that the IIFE creates a function scope (a Closure) where it defines a function scoped cLookup variable which contains the id -> name data.  It then returns a function (out of the Closure) which takes an id and returns the name.  That returned function retains it's access to the variables inside the Closure, and since it's assigned to a variable in a different scope, the Closure continues to exist.

Aaron: I copy loud and clear on the scope discussion, excepting this IIFE thing which has me scratching my head. See my interpretation below:

const cLookup = (()=>{
/* Tom: Creates a function but I don't understand why two left parenthesis and one right?*/ 
    const cLookup = findObjs({type:'character'}).reduce((m,c)=>{m[c.id]=c.get('name'); return m},{});
    /* Tom: Uses cLookup shadow for reduced character object indexed on id and holding the key&value for the character name.*/
    return (id) => cLookup[id];
        /* Aaron: It then returns a function (out of the Closure) which takes an id and returns the name.
                  That returned function retains it's access to the variables inside the Closure, and 
                  since it's assigned to a variable in a different scope, the Closure continues to exist. */
    /* Tom: I must be reading this line wrong. To me it looks like an unamed function that takes 
    a character.id as a parameter and then matches the passed parameter to an existing id
    and then returns whatever is in the array element named "id" which should be the character.id? In short, I don't see how this passes back the character name?*/
})();
let charName = cLookup(someid);




July 20 (6 years ago)

P.S. The discussion on scope was a great help. Now I know what terminology to use. How declarations can be shadowed, etc. Now, I can just come right back to here to refresh my memory whenever needed. Thank you. 

July 20 (6 years ago)
Jakob
Sheet Author
API Scripter
  1. The closing parenthesis belonging to the initial opening parenthesis is on the second-to-last line.
  2. The function that is returned takes id to the value of the cLookup object assigned to the key id.  And if you look at what line 3 does, if c is a character, then the value of this object at key c.id is c.get("name"), since we assign
m[c.id]=c.get('name')


July 20 (6 years ago)
The Aaron
Pro
API Scripter

No problem!  IIFEs and Closures are super powerful, but also a bit confusing initially.

Why Two Left Parentheses?

This is just a bit of syntactic necessity. The closing parenthesis for that opening one is actually on the second to last line:

})();

Simplified to show the structure:

const cLookup = ( <A FUNCTION> )();

The parentheses around the function declaration are required to form a valid expression.  The interpreter doesn't know what to do with:

()=>{}()

But does understand:

(()=>{})()

All symbols in a programming language have an order of precedence, just like in math.  Just like in math, placing parentheses around an expression causes that expression to be evaluated first, before resuming with the normal precedence. 

You can look at this page to understand Javascript's Operator Precedence: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_Precedence

In the first example, the Function Call operator [19] has a higher precedence than the (implied) Assignment operator [3].  Wrapping the function creation in the Grouping operator [20] lets things go how they should.

What's returned.

The function that is being returned is:

(id) => cLookup[id];

Because of the cLookup variable in the Closure shadowing the global cLookup, this is going to return whatever value is stored in the id property of the in-closure cLookup variable.  To take a page from Jakob's book, if you had a character with id abc123 named Jane, it might go something like this:

const cLookup = (()=>{
    const cLookup = findObjs({type:'character'}).reduce((m,c)=>{m[c.id]=c.get('name'); return m},{});
    /* cLookup is something like:
        { 'abc123': 'Jane' }
    */ 
    return (id) => cLookup[id];
})();

/* If someid = 'abc123', this will lookup from the closure cLookup what what 'abc123' has for a value
    and return 'Jane' */
let charName = cLookup(someid);


July 20 (6 years ago)


Jakob said:

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");

Yup, that works like a champ. But let's see if I understand the showOneChar syntax line:

  log(index[who]);

I know what 'log()' is. It looks like '[who]' is looking to match a passed parameter to charactersIndexedByName object. Sinc charactersIndexedByName only has one index (and it is by name), you get the name if there is a match and probably undefined if there is not. I hope that's right?

For reference here is log of the current state of the tutorial code and it works:

function listChars() {
    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 the Sanchez";
    log(charactersIndexedByName.Rick.name);
    return charactersIndexedByName;
}
function showOneChar(index, who) {
  log(index[who]);
}
on('ready', ()=>{
    characterIndex = listChars();
    showOneChar(characterIndex, "Summer");
})

July 20 (6 years ago)
The Aaron
Pro
API Scripter

The [] allows you to access a property using the name stored in a variable:

index[who]

In the case where who = "Morty", this would be the same as writing:

index.Morty

However, sometimes you can't just write the literal property name like that, and must use the [ ] instead:

index[who] /* <-- we don't know what property as we get it dynamically */
index["my characters"] /* <-- the space in the name would be a syntax error */
index["numº"] /* <-- special characters would also be a syntax error */ 


July 20 (6 years ago)

Hi Guys:

I think I am good to go as far the subject of this thread goes -----> AND THEN SOME. heh heh. 

If it's not obvious I am still a little fuzzy on all the various ways that structures (objects, arrays, and key&value pairs) are accessed and manipulated. But rather than beat my head against the wall understanding all the nuances of the subject, I think it's time for me to build a namespaced script template from Aaron's template above, which I think is smart to start on in another thread? I'll keep coming back here to review what you guys have showed me quite often I am sure. Hopefully as I work on actual solutions for my homebrew that all the mind-fog I've shown in this thread will burn off. 

So, I am off to study the namespaced script template! I am sure I'll have a bunch of questions regarding it -- I wouldn't want you to be missing me! =)

July 20 (6 years ago)
The Aaron
Pro
API Scripter

That sounds great!  Learning to make some nice API scripts in the public eye is a great way for many people to learn that would otherwise be afraid to as, so definitely keep the questions coming!