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

Finding characters

April 21 (8 years ago)

Edited April 21 (8 years ago)
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
Wondering if I'm designing a script correctly. I'm trying to find any characters that have a value entered into the player-name attribute (from the pathfinder char sheet). Would this return any populated player-name attributes (along with their associated character id's)?

getPlayers = function() {
return findObjs({_type: "attribute",
name: 'player-name',
current: !null});
},

If not, how would I search for an attribute that has any entry?
April 21 (8 years ago)

Edited April 21 (8 years ago)
Ada L.
Marketplace Creator
Sheet Author
API Scripter
No, you can't use a boolean value as a wildcard to see if any value is provided for an object's property. In your example, it would look for 'player-name' attributes whose current values are literally true.

You could do what you're wanting to accomplish like this:
// Get all the 'player-name' attributes.
var playerNames = findObjs({
	_type: 'attribute',
	name: 'player-name'
});

// Get the set of IDs for characters for which the 'player-name' attribute is set.
// We use a set here to avoid duplicates.
var charIds = {};
_.each(playerNames, function(attr) {
	if(!!attr.get('current')) {
		var charId = attr.get('_characterid');
		charIds[charId] = true;
	}	
});

// Return the Characters for each of the good character IDs.
return _.map(_.keys(charIds), function(charId) {
	return findObjs({
		_type: 'character',
		_id: charId
	})[0];
});

Hope that helps!
April 21 (8 years ago)
The Aaron
Pro
API Scripter
You can use filterObjs() to build a collection of objects based on specific criteria:
var getPlayers = function() {
  return filterObjs( (o)=>{
    return o.get('type')==='character' &&
      o.get('name').length;
  });
};
April 21 (8 years ago)

Edited April 21 (8 years ago)
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
hmm, ok Aaron, that's going on the right track I think, but it's returning every character (164 of them at least). How could I limit it to only those that have an entry in the player-name attribute?

Stephen,

I'm not sure I understand all of the code that you put up. It appears to work, but I'd like to understand how it works.

// Get all the 'player-name' attributes.
var playerNames = findObjs({
	_type: 'attribute',
	name: 'player-name'
});

// Get the set of IDs for characters for which the 'player-name' attribute is set.
// We use a set here to avoid duplicates.
var charIds = {};
_.each(playerNames, function(attr) {
	if(!!attr.get('current')) {
		var charId = attr.get('_characterid');
		charIds[charId] = true;
	}	
});

// Return the Characters for each of the good character IDs.
return _.map(_.keys(charIds), function(charId) {
	return findObjs({
		_type: 'character',
		_id: charId
	})[0];
});
The first part just returns all the attributes that are 'player-name' attributes along with the character ID's associated with those attributes. Makes sense so far. 

The second part returns 'player-name' attributes that have an entry. I've got a few questions here. I've read the underscore documentation, but I'm not quite clear on exactly how _.each works. The second question is, what does the '!!' before the 'attr.get('current')) {...' do? And then what is a set? is that the CharIds = {}?

As with _.each, I'm not sure how _.map functions, so I'm not really sure how the third part functions at all.

Thank you,

Scott
April 21 (8 years ago)
The Aaron
Pro
API Scripter
Sorry, I misunderstood what you needed before.
var getPlayers = function(){
  "use strict";
  return _.chain(filterObj((o)=>{
   return (o.get('type')==='attribute' &&
      o.get('name')==='player-name');
    }))
    .map((o)=>{return o.get('characterid');})
    .map((cid)=>{return getObj('character',cid);})
    .reject(_.isUndefined)
    .value();
};

I wrote some documentation which you might find useful on Underscore's _.each() and _.map(): https://wiki.roll20.net/API:Cookbook#Underscore.js

!! converts an r-value to a boolean.  It's shorthand for something like ( thing ? true : false ).  It's actually not necessary in Javascript because of its notion of all things having a truthy value.  You'd only need it if you were comparing something literally to true or false with === or !==.
April 21 (8 years ago)

Edited April 21 (8 years ago)
Ada L.
Marketplace Creator
Sheet Author
API Scripter
Aaron explained most of what's going on in my code example pretty well. Aaron's new example is actually a much more compact, and more efficient way to do the same thing.

As for what sets are:
A set is a mathematical construct which is basically just a collection of distinct items. It is like a list of items, except that each item in a set appears only once.

In Javascript, there isn't a built-in set type (not before ES6 anyways), so instead I used {} to declare an empty object to be used as a map of key-value pairs. Like a set, each key in a map can only appear once. So, by using the character IDs as the keys in the map, it can effectively be used as a set. In this case, it didn't matter what the values are since the keys are the only part we're interested in for it, so I just assigned them to true. 
April 21 (8 years ago)

Edited April 22 (8 years ago)
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
Thanks for all the explanations Stephen and Aaron. Let me see if I can figure out what your new code does Aaron.
    var getPlayers = function(){
        return _.chain(filterObj((o)=>{//creates an array of filtered objects if they returned true in the following function and wraps all future _. calls
            return (o.get('type')==='attribute' && o.get('name')==='player-name')/*&& o.get('current')*/;})) /* returns whether the object is an attribute, named 
            player-nam, and has a current value*/
        _.map((o)=>{return o.get('characterid');})//returns an array of character id's
        _.map((cid)=>{return getObj('character',cid);})//returns an array of characters
        _.reject(_.isUndefined)//rejects any result that is undefined
        _.value(); //returns the final _.chain result | final chain result is an array of attributes, characterids and characters
    };
I'm sure I'm off on most if not all of that.

*EDIT: updated comments
April 22 (8 years ago)
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
Hmm, ok, I've got another issue with this script. I can't seem to get .set to work.

var challengeComplete = function(){
var current = getAttrByName(ExperienceThresholds.id, 'Session XP'),//ExperienceThresholds is a global variable
xp = 500;
sendChat('challengeComplete Start', 'challengeComplete entered. Current XP = ' + current + ' | New XP = ' + xp);
ExperienceThresholds.set('Session XP', (current+xp));
sendChat('challengeComplete End', 'challengeComplete finished. New Current XP = ' + current);
},

The sendChats are just there for the time being to give me a chat readout of what is going on in the script. The function successfully gets the current value of the character attribute 'Session XP', so I know that it is referencing the right sheet. Experiencethresholds.set does not modify the value of 'Session XP' though; the current is the same in both chat messages and the attribute is not changed in the character. Any thoughts?

Thanks,

Scott
April 22 (8 years ago)
Ada L.
Marketplace Creator
Sheet Author
API Scripter
What type of object is ExperienceThresholds? It would help to see the source code for that.
April 22 (8 years ago)

Edited April 22 (8 years ago)
Lithl
Pro
Sheet Author
API Scripter

Scott C. said:

Thanks for all the explanations Stephen and Aaron. Let me see if I can figure out what your new code does Aaron.
    var getPlayers = function(){
        return _.chain(filterObj((o)=>{//creates an array of filtered objects if they returned true in the following function and wraps all future _. calls
            return (o.get('type')==='attribute' && o.get('name')==='player-name')/*&& o.get('current')*/;})) /* returns whether the object is an attribute, named 
            player-nam, and has a current value*/
        _.map((o)=>{return o.get('characterid');})//returns an array of character id's
        _.map((cid)=>{return getObj('character',cid);})//returns an array of characters
        _.reject(_.isUndefined)//rejects any result that is undefined
        _.value(); //returns the final _.chain result | final chain result is an array of attributes, characterids and characters
    };
I'm sure I'm off on most if not all of that.

*EDIT: updated comments
When using _.chain(), you don't need to include (and in fact cannot include) the underscore for each subsequent call. The code above is going to return the intermediate result of the chain function, before any of the other functions have been called. (It might also throw an error, I'm not certain.)

Essentially, the chain function lets you do this:
_.chain(/* collection */).map(/* operation 1 */).map(/* operation 2 */).reject(/* operation 3 */).value()
Instead of this:
_.reject(_.map(_.map(/* collection */, /* operation 1 */), /* operation 2 */), /* operation 3 */)
The code you have is essentially this:
return _.chain(/* collection */)
_.map(/* invalid parameter */) // unreachable function call
_.map(/* invalid parameter */) // unreachable function call
_.reject(/* invalid parameter */) // unreachable function call
_.value() // invalid & unreachable function call
April 22 (8 years ago)
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
ah, that was my bad interpretation of Aaron's code, thanks for correcting me Brian.
April 22 (8 years ago)
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
Stephen,

I've uploaded a Gist of what I've got so far.
April 22 (8 years ago)
The Aaron
Pro
API Scripter
With _.chain() returns a wrapped collection which shortcuts a bunch of operations that can be used to manipulate or inspect the collection.  It's called "chain" because you then chain together invokations of those function with the output of the prior link becoming the input of the next.

I've bolded each of the arguments below to make them stand out and I'll describe them.  I added one more link in the chain based on Stephen's Discussion of Sets (uniq):
var getPlayers = function(){
  "use strict";

  /* start the chain with all the attribute objects named 'player-name' */
  return _.chain(filterObj((o)=>{
   return (o.get('type')==='attribute' &&
      o.get('name')==='player-name');
    }))

    /* IN: Array of Attribute Objects */
    /* extract the characterid from each */
    .map((o)=>{return o.get('characterid');})

    /* IN: Array of Character IDs (Possible Duplicates) */
    /* Remove any duplicate IDs */
    .uniq()

    /* IN: Array of Character IDs */
    /* Get the character object for each id */ 
    .map((cid)=>{return getObj('character',cid);})

    /* IN: Array of Character Objects or Undefined */
    /* remove any entries that didn't have Characters */
    .reject(_.isUndefined)

    /* IN: Array of Character Objects */
    /* Unwrap Chain and return the array */
    .value();
};
April 22 (8 years ago)
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
Ah ha, that makes much more sense Aaron. Thank you for that explanation. The final array only has the character objects themselves right? So if I later wanted to retrieve or modify another attribute of these characters, could I simply use the array to call them?
April 22 (8 years ago)

Edited April 27 (8 years ago)
The Aaron
Pro
API Scripter
You could generalize this into:
var getCharactersWithAttrByName = function(attributeName){
  "use strict";

  /* start the chain with all the attribute objects named 'player-name' */
  return _.chain(filterObj((o)=>{
   return (o.get('type')==='attribute' &&
      o.get('name')===attributeName);
    }))

    /* IN: Array of Attribute Objects */
    /* extract the characterid from each */
    .map((o)=>{return o.get('characterid');})

    /* IN: Array of Character IDs (Possible Duplicates) */
    /* Remove any duplicate IDs */
    .uniq()

    /* IN: Array of Character IDs */
    /* Get the character object for each id */ 
    .map((cid)=>{return getObj('character',cid);})

    /* IN: Array of Character Objects or Undefined */
    /* remove any entries that didn't have Characters */
    .reject(_.isUndefined)

    /* IN: Array of Character Objects */
    /* Unwrap Chain and return the array */
    .value();
};
and then call it like:
var charsWithPN = getCharactersWithAttrByName('player-name'),
    charsWithVS = getCharactersWithAttrByName('vow-sworn');


If you wanted to retain the attribute as well as the characters, you could modify the above to return an object with both:
var getCharactersWithAttrByName = function(attributeName){
  "use strict";

  /* start the chain with all the attribute objects named 'player-name' */
  return _.chain(filterObjs((o)=>{
   return (o.get('type')==='attribute' &&
      o.get('name')===attributeName);
    }))

    /* IN: Array of Attribute Objects */
    /* extract the characterid from each */
    .reduce((m,o)=>{
let obj={};
obj.cid=o.get('characterid');
obj[attributeName]=o;
m.push(obj);
return m;
},
[]
) /* IN: Array of Objects with * Character ID in property cid * attribute in [attributeName] */ /* add characters to the objects */     .map((o)=>{
o.char=getObj('character',o.cid);
return o;
}
) /* IN: Array of Objects with * Character ID in property cid * attribute in [attributeName] * character in property char */ /* remove any entries that didn't have Characters */     .reject( (o)=> {return _.isUndefined(o.char);} ) /* IN: Array of Character Objects */ /* Unwrap Chain and return the array */     .value(); };
Assuming you did that, you would then have both available:
var charsWithPN = getCharactersWithAttrByName('player-name');
_.each(charsWithPN,(o)=>{
  log(`Character ${o.char.get('name')} has player-name of ${o['player-name'].get('current')}/${o['player-name'].get('max')}`);
});

April 22 (8 years ago)
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
ah, thank you Aaron, I think that will pretty much do what I need. If I wanted to change the value of these, would I then do something like:

var charsWithPN = getCharactersWithAttrByName('player-name');
_.each(charsWithPN,(o)=>{
  o['experience'].set('current', currValue + additionalValue);
};
assuming that I included the experience attribute in the chain of getCharactersWithAttrByName.
April 22 (8 years ago)
The Aaron
Pro
API Scripter
If you added another .reduce that added that attribute in, that would work. 

Note: this will definitely work, but might not be the ideal way to structure what you want to do. I need to read you're code example to give better feedback though. 
April 23 (8 years ago)
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
Sounds good, although I'm not sure I'd describe it as code so much as a general prototype atm.
April 27 (8 years ago)

Edited April 27 (8 years ago)
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
Havent' had a chance to work on this for a bit. Now that I am though, I'm getting a reference error that filterObj is undefined. Do I need to define that somewhere. I could swear that it is used in other scripts just fine.

Full error:
Your scripts are currently disabled due to an error that was detected. Please make appropriate changes to your scripts and click the "Save Script" button and we'll attempt to start running them again. More info...
For reference, the error message generated was: ReferenceError: filterObj is not definedReferenceError: filterObj is not defined at _.chain.filterObj.reduce.map (apiscript.js:751:23) at eval (eval at <anonymous> (/home/node/d20-api-server/api.js:105:34), <anonymous>:65:16) at Object.publish (eval at <anonymous> (/home/node/d20-api-server/api.js:105:34), <anonymous>:70:8) at TrackedObj.set (/home/node/d20-api-server/api.js:704:14) at updateLocalCache (/home/node/d20-api-server/api.js:933:18) at /home/node/d20-api-server/api.js:1076:11 at /home/node/d20-api-server/node_modules/firebase/lib/firebase-node.js:93:560 at hc (/home/node/d20-api-server/node_modules/firebase/lib/firebase-node.js:39:147) at Kd (/home/node/d20-api-server/node_modules/firebase/lib/firebase-node.js:93:546) at Id.Mb (/home/node/d20-api-server/node_modules/firebase/lib/firebase-node.js:93:489) at Ld.Mb (/home/node/d20-api-server/node_modules/firebase/lib/firebase-node.js:94:425)

April 27 (8 years ago)
The Aaron
Pro
API Scripter
Ah.. typo.  filterObjs() - https://wiki.roll20.net/API:Objects#filterObjs.28c...

I'll correct the above code. Wonder why I didn't have that problem when I ran it...
April 27 (8 years ago)
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
doh, can't believe I missed that too.