Character Vault
Any Concept / Any System
Compendium
Your System Come To Life
Roll20 for Android
Streamlined for your Tablet
Roll20 for iPad
Streamlined for your Tablet

Personal tools

Difference between revisions of "Mod:Cookbook"

From Roll20 Wiki

Jump to: navigation, search
m (Asynchronous Semaphore)
m (1223200 moved page API:Cookbook to Mod:Cookbook)
 
(36 intermediate revisions by 3 users not shown)
Line 1: Line 1:
 +
{{revdate}}{{HCbox| {{hc|articles/360037772893-API-Cookbook Here}} }}
 +
{{apibox}}
 +
 
The following are not full scripts. They are meant to be stitched together along with business logic to assist in the creation of full scripts, not create scripts on their own.
 
The following are not full scripts. They are meant to be stitched together along with business logic to assist in the creation of full scripts, not create scripts on their own.
  
 
== Revealing Module Pattern ==
 
== Revealing Module Pattern ==
 
The ''Module Pattern'' emulates the concept of classes from other languages by encapsulating private and public members within an object. The Revealing Module Pattern improves upon the Module Pattern by making the syntax more consistent.
 
The ''Module Pattern'' emulates the concept of classes from other languages by encapsulating private and public members within an object. The Revealing Module Pattern improves upon the Module Pattern by making the syntax more consistent.
<pre data-language="javascript">var myRevealingModule = myRevealingModule || (function() {
+
<pre data-language="javascript" style="overflow:auto;white-space:pre-wrap;">var myRevealingModule = myRevealingModule || (function() {
 
     var privateVar = 'This variable is private',
 
     var privateVar = 'This variable is private',
 
         publicVar  = 'This variable is public';
 
         publicVar  = 'This variable is public';
Line 24: Line 27:
 
         getFunc: publicGet
 
         getFunc: publicGet
 
     };
 
     };
})();
+
}());
  
 
log(myRevealingModule.getFunc()); // "This variable is private"
 
log(myRevealingModule.getFunc()); // "This variable is private"
Line 36: Line 39:
 
== Memoization ==
 
== Memoization ==
 
Memoization is an optimization technique which stores the result for a given input, allowing the same output to be produced without computing it twice. This is especially useful in expensive computations. Of course, if it is rare that your function will receive the same input, memoization will be of limited utility while the storage requirements for it continue to grow.
 
Memoization is an optimization technique which stores the result for a given input, allowing the same output to be produced without computing it twice. This is especially useful in expensive computations. Of course, if it is rare that your function will receive the same input, memoization will be of limited utility while the storage requirements for it continue to grow.
<pre data-language="javascript">var factorialCache = {};
+
<pre data-language="javascript" style="overflow:auto;white-space:pre-wrap;">var factorialCache = {};
 
function factorial(n) {
 
function factorial(n) {
 
     var x;
 
     var x;
Line 64: Line 67:
  
 
This particular implementation of an asynchronous semaphore also lets you supply a context for the callback (set the value of <code>this</code>), as well as pass parameters to the callback. The parameters can be given either in the constructor or in the call to <code>p</code>. (Parameters in <code>p</code> take precedence over parameters in the constructor.)
 
This particular implementation of an asynchronous semaphore also lets you supply a context for the callback (set the value of <code>this</code>), as well as pass parameters to the callback. The parameters can be given either in the constructor or in the call to <code>p</code>. (Parameters in <code>p</code> take precedence over parameters in the constructor.)
<pre data-language="javascript">function Semaphore(callback, initial, context) {
+
<pre data-language="javascript" style="overflow:auto;white-space:pre-wrap;">function Semaphore(callback, initial, context) {
     this.lock = initial || 0;
+
    var args = (arguments.length === 1 ? [arguments[0]] : Array.apply(null, arguments));
 +
 
 +
     this.lock = parseInt(initial, 10) || 0;
 
     this.callback = callback;
 
     this.callback = callback;
 
     this.context = context || callback;
 
     this.context = context || callback;
     this.args = arguments.slice(3);
+
     this.args = args.slice(3);
 
}
 
}
 
Semaphore.prototype = {
 
Semaphore.prototype = {
Line 82: Line 87:
 
             else { parameters = this.args; }
 
             else { parameters = this.args; }
  
             this.callback.apply(context, parameters);
+
             this.callback.apply(this.context, parameters);
 
         }
 
         }
 
     }
 
     }
Line 88: Line 93:
  
 
Example of use:
 
Example of use:
<pre data-language="javascript">var sem = new Semaphore(function(lastAsync) {
+
<pre data-language="javascript" style="overflow:auto;white-space:pre-wrap;">var sem = new Semaphore(function(lastAsync) {
 
     log(lastAsync + ' completed last');
 
     log(lastAsync + ' completed last');
     log(this); // { foo: "bar", fizz: "buzz" }
+
     log(this);
 
}, 2, { foo: 'bar', fizz: 'buzz' }, 'Sir not appearing in this callback');
 
}, 2, { foo: 'bar', fizz: 'buzz' }, 'Sir not appearing in this callback');
  
 
sendChat('', '/roll d20', function(ops) {
 
sendChat('', '/roll d20', function(ops) {
 +
    log('Executing first sendChat');
 
     sem.p('First sendChat call');
 
     sem.p('First sendChat call');
 
});
 
});
 
sendChat('', '/roll d20', function(ops) {
 
sendChat('', '/roll d20', function(ops) {
 +
    log('Executing second sendChat');
 
     sem.p('Second sendChat call');
 
     sem.p('Second sendChat call');
 
});</pre>
 
});</pre>
 +
 +
Example output:
 +
<pre style="background-color:#333; color: #E6DB74;">"Executing second sendChat"
 +
"Executing first sendChat"
 +
"First sendChat call completed last"
 +
{ foo: "bar", fizz: "buzz" }</pre>
 +
 +
== Handouts &amp; Characters ==
 +
=== Creating a Handout ===
 +
Because of the way that [[Handout]] text blocks are handled, creating a Handout object needs to be done in two steps:  First create the object, then set the text blocks:
 +
<pre data-language="javascript" style="overflow:auto;white-space:pre-wrap;">
 +
    //Create a new Handout available to all players
 +
    var handout = createObj("handout", {
 +
                name: "The name of the handout",
 +
                inplayerjournals: "all",
 +
                archived: false
 +
    });
 +
    handout.set('notes', 'Notes need to be set after the handout is created.');
 +
    handout.set('gmnotes', 'GM notes also need to be set after the handout is created.');
 +
</pre>
 +
 +
=== Handling Encoding ===
 +
The text blocks in Handouts (Notes and GM Notes) and Characters (Bio and GM Notes) are that are set through the User Interface are stored in <code>x-www-form-urlencoded</code> format.  You can recognize this by the sequence of <code>%##</code> codes throughout the text:
 +
 +
<pre data-language="javascript" style="overflow:auto;white-space:pre-wrap;">
 +
"Erik%20%28Viking%2BScientist%29%20%5BFighter%3A%203%2C%20Wizard%3A%202%5D"
 +
</pre>
 +
 +
This text can be sent to the chat and will be translated by the browser, but if you need to make changes to the text, you might want to deal with it as it was entered:
 +
<pre data-language="javascript" style="overflow:auto;white-space:pre-wrap;">
 +
"Erik (Viking+Scientist) [Fighter: 3, Wizard: 2]"
 +
</pre>
 +
 +
You can decode the encoded text with the following function:
 +
 +
<pre data-language="javascript" style="overflow:auto;white-space:pre-wrap;">
 +
var decodeUrlEncoding = function(t){
 +
  return t.replace(
 +
        /%([0-9A-Fa-f]{1,2})/g,
 +
        function(f,n){
 +
            return String.fromCharCode(parseInt(n,16));
 +
        }
 +
    );
 +
}
 +
</pre>
  
 
== Utility Functions ==
 
== Utility Functions ==
 
Utility functions complete common tasks that you may want to use throughout many scripts. If you place a function at the outermost scope of a script tab, that function should be available to all of your scripts, reducing your overhead. Below is a selection of such functions.
 
Utility functions complete common tasks that you may want to use throughout many scripts. If you place a function at the outermost scope of a script tab, that function should be available to all of your scripts, reducing your overhead. Below is a selection of such functions.
 +
 +
=== decodeEditorText ===
 +
'''Dependencies:''' None
 +
 +
The in-game text editor is pretty nice, but comes with a problem for API Scripts that depend on reading information from one of the large text areas in the data set.  This function helps with that.
 +
 +
Given the text from a '''Graphic''''s <code>gmnotes</code> property, or a '''Character''''s <code>bio</code> or <code>gmnotes</code> property, or a '''Handout''''s <code>notes</code> or <code>gmnotes</code> property, this will return a version with the auto-inserted editor formatting stripped out.
 +
 +
<pre data-language="javascript" style="overflow:auto;white-space:pre-wrap;">const decodeEditorText = (t, o) =>{
 +
  let w = t;
 +
  o = Object.assign({ separator: '\r\n', asArray: false },o);
 +
  /* Token GM Notes */
 +
  if(/^%3Cp%3E/.test(w)){
 +
    w = unescape(w);
 +
  }
 +
  if(/^<p>/.test(w)){
 +
    let lines = w.match(/<p>.*?<\/p>/g)
 +
      .map( l => l.replace(/^<p>(.*?)<\/p>$/,'$1'));
 +
    return o.asArray ? lines : lines.join(o.separator);
 +
  }
 +
  /* neither */
 +
  return t;
 +
};</pre>
 +
 +
The first argument is the text to process. 
 +
<pre data-language="javascript" style="overflow:auto;white-space:pre-wrap;">const text = decodeEditorText(token.get('gmnotes'));</pre>
 +
By default, the lines of text will be separated by <code>\r\n</code>.
 +
 +
The optional second argument is an object with options. 
 +
* <code>separator</code> -- specifies what to separate lines of text with.  Default: <code>\r\n</code>
 +
<pre data-language="javascript">const text = decodeEditorText(token.get('gmnotes'),{separator:'<BR>'});</pre>
 +
* <code>asArray</code> -- specifies to instead return the lines as an array.  Default: <code>false</code>
 +
<pre data-language="javascript">const text = decodeEditorText(token.get('gmnotes'),{asArray:true});</pre>
 +
 +
'''''NOTE:''''' ''Nested <code>&lt;p&gt;</code> tags will confuse and break the decoding.  If you run into that problem and need help, PM [[Aaron|The Aaron]] and he'll be happy to look at it.''
 +
 +
=== getCleanImgsrc ===
 +
'''Dependencies:''' None
 +
 +
Given an image URL taken from a [[token]] or other resource, get a clean version of it that can be used to create a token via the API, or <code>undefined</code> if it cannot be created by the API.
 +
 +
<pre data-language="javascript" style="overflow:auto;white-space:pre-wrap;">var getCleanImgsrc = function (imgsrc) {
 +
  var parts = imgsrc.match(/(.*\/images\/.*)(thumb|med|original|max)([^\?]*)(\?[^?]+)?$/);
 +
  if(parts) {
 +
      return parts[1]+'thumb'+parts[3]+(parts[4]?parts[4]:`?${Math.round(Math.random()*9999999)}`);
 +
  }
 +
  return;
 +
};</pre>
 +
 +
'''Note:'''  The API is only capable of creating images whose source is located in a [[Art Library|user library]].  The <code>imgsrc</code> must also be the '''thumb''' version of the image.
  
 
=== getSenderForName ===
 
=== getSenderForName ===
 
'''Dependencies:''' None
 
'''Dependencies:''' None
  
Given a string name, this function will return a string appropriate for the first parameter of [[API:Chat#Chat Functions|sendChat]]. If there is a character that shares a name with a player, the player will be used. You may also pass an <code>options</code> object, which is structured identically to the <code>options</code> parameter of [[API:Objects#Finding/Filtering Objects|findObjs]].
+
Given a string name, this function will return a string appropriate for the first parameter of [[API:Chat#Chat Functions|sendChat]]. If there is a character that shares a name with a [[player]], the player will be used. You may also pass an <code>options</code> object, which is structured identically to the <code>options</code> parameter of [[API:Objects#Finding/Filtering Objects|findObjs]].
  
<pre data-language="javascript">function getSenderForName(name, options) {
+
<pre data-language="javascript" style="overflow:auto;white-space:pre-wrap;">function getSenderForName(name, options) {
 
     var character = findObjs({
 
     var character = findObjs({
 
             type: 'character',
 
             type: 'character',
Line 125: Line 227:
 
     }
 
     }
 
     return name;
 
     return name;
}</pre>
 
 
=== levenshteinDistance ===
 
'''Source:''' [http://en.wikibooks.org/wiki/Algorithm_Implementation/Strings/Levenshtein_distance#JavaScript en.wikibooks.org]
 
 
[[wikipedia:Levenshtein distance|''Levenshtein Distance'']] is a metric for measuring the difference between strings. The return value for this function will be the number of substitutions, insertions, and deletions required to transform string ''a'' into string ''b''.
 
<pre data-language="javascript">function levenshteinDistance(a, b) {
 
    var i, j,
 
matrix = [];
 
   
 
    if (a.length === 0) {
 
return b.length;
 
    }
 
    if (b.length === 0) {
 
return a.length;
 
    }
 
   
 
    // Increment along the first column of each row
 
    for (i = 0; i <= b.length; i++) {
 
matrix[i] = [i];
 
    }
 
   
 
    // Increment each column in the first row
 
    for (j = 0; j <= a.length; j++) {
 
matrix[0][j] = j;
 
    }
 
   
 
    // Fill in the rest of the matrix
 
    for (i = 1; i <= b.length; i++) {
 
for (j = 1; j <= a.length; j++) {
 
    if (b.charAt(i - 1) === a.charAt(j - 1)) {
 
matrix[i][j] = matrix[i - 1][j - 1];
 
    } else {
 
matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1, // Substitution
 
      Math.min(matrix[i][j - 1] + 1,    // Insertion
 
matrix[i - 1][j] + 1));  // Deletion
 
    }
 
}
 
    }
 
   
 
    return matrix[b.length][a.length];
 
 
}</pre>
 
}</pre>
  
 
=== getWhisperTarget ===
 
=== getWhisperTarget ===
'''Dependencies:''' [[#levenshteinDistance|levenshteinDistance]]
+
'''Dependencies:''' [[Script:levenshteinDistance|levenshteinDistance]]
  
Given a set of options, this function tries to construct the "''/w name ''" portion of a whisper for a call to [[API:Chat#Chat Functions|sendChat]]. The <code>options</code> parameter should contain either <code>player: true</code> or <code>character: true</code> and a value for either <code>id</code> or <code>name</code>. Players are preferred over characters if both are true, and ids are preferred over names if both have a valid value. If a name is supplied, the player or character with the name closest to the supplied string will be sent the whisper.
+
Given a set of options, this function tries to construct the <code>/w name</code> portion of a whisper for a call to [[API:Chat#Chat Functions|sendChat]]. The <code>options</code> parameter should contain either <code>player: true</code> or <code>character: true</code> and a value for either <code>id</code> or <code>name</code>. Players are preferred over characters if both are true, and ids are preferred over names if both have a valid value. If a name is supplied, the player or character with the name closest to the supplied string will be sent the whisper.
  
 
<code>options</code> is technically optional, but if you omit it (or don't supply a combination of player/character + id/name), the function will return an empty string.
 
<code>options</code> is technically optional, but if you omit it (or don't supply a combination of player/character + id/name), the function will return an empty string.
<pre data-language="javascript">function getWhisperTarget(options) {
+
<pre data-language="javascript" style="overflow:auto;white-space:pre-wrap;">function getWhisperTarget(options) {
 
     var nameProperty, targets, type;
 
     var nameProperty, targets, type;
 
      
 
      
Line 212: Line 273:
 
      
 
      
 
     return '';
 
     return '';
}</pre>
 
 
=== splitArgs ===
 
'''Source:''' [https://github.com/elgs/splitargs Elgs Qian Chen on GitHub.com]
 
 
It is true that <code>msg.content.split(' ')</code> is frequently ''good enough'' for handling API commands and their parameters, but sometimes greater control over the tokenization is desired. <code>splitArgs</code> lets you quote parameters to your API command (including nested quotes).
 
<pre data-language="javascript">function splitArgs(input, separator) {
 
    var singleQuoteOpen = false,
 
doubleQuoteOpen = false,
 
tokenBuffer = [],
 
ret = [],
 
arr = input.split(''),
 
element, i, matches;
 
    separator = separator || /\s/g;
 
   
 
    for (i = 0; i < arr.length; i++) {
 
element = arr[i];
 
matches = element.match(separator);
 
if (element === '\'') {
 
    if (!doubleQuoteOpen) {
 
singleQuoteOpen = !singleQuoteOpen;
 
continue;
 
    }
 
} else if (element === '"') {
 
    if (!singleQuoteOpen) {
 
doubleQuoteOpen = !doubleQuoteOpen;
 
continue;
 
    }
 
}
 
 
if (!singleQuoteOpen && !doubleQuoteOpen) {
 
    if (matches) {
 
if (tokenBuffer && tokenBuffer.length > 0) {
 
    ret.push(tokenBuffer.join(''));
 
    tokenBuffer = [];
 
}
 
    } else {
 
tokenBuffer.push(element);
 
    }
 
} else if (singleQuoteOpen || doubleQuoteOpen) {
 
    tokenBuffer.push(element);
 
}
 
    }
 
    if (tokenBuffer && tokenBuffer.length > 0) {
 
ret.push(tokenBuffer.join(''));
 
    }
 
   
 
    return ret;
 
 
}</pre>
 
}</pre>
  
 
=== processInlinerolls ===
 
=== processInlinerolls ===
This function will scan through <code>msg.content</code> and replace inline rolls with their total result. This is particularly useful for API commands to which the user may want to pass inline rolls as parameters.
+
This function will scan through <code>msg.content</code> and replace [[Inline_Rolls|inline rolls]] with their total result. This is particularly useful for API commands to which the user may want to pass inline rolls as parameters.
<pre data-langauge="javascript">function processInlinerolls(msg) {
+
<pre data-language="javascript" style="overflow:auto;white-space:pre-wrap;">function processInlinerolls(msg) {
 
     if (_.has(msg, 'inlinerolls')) {
 
     if (_.has(msg, 'inlinerolls')) {
 
         return _.chain(msg.inlinerolls)
 
         return _.chain(msg.inlinerolls)
Line 275: Line 288:
 
                 }, msg.content)
 
                 }, msg.content)
 
                 .value();
 
                 .value();
 +
    } else {
 +
        return msg.content;
 
     }
 
     }
 +
}</pre>
 +
 +
Here is a slightly more complicated version which also handles converting tableItems to their text:
 +
<pre data-language="javascript">function processInlinerolls(msg) {
 +
if(_.has(msg,'inlinerolls')){
 +
return _.chain(msg.inlinerolls)
 +
.reduce(function(m,v,k){
 +
var ti=_.reduce(v.results.rolls,function(m2,v2){
 +
if(_.has(v2,'table')){
 +
m2.push(_.reduce(v2.results,function(m3,v3){
 +
m3.push(v3.tableItem.name);
 +
return m3;
 +
},[]).join(', '));
 +
}
 +
return m2;
 +
},[]).join(', ');
 +
m['$[['+k+']]']= (ti.length && ti) || v.results.total || 0;
 +
return m;
 +
},{})
 +
.reduce(function(m,v,k){
 +
return m.replace(k,v);
 +
},msg.content)
 +
.value();
 +
} else {
 +
return msg.content;
 +
}
 
}</pre>
 
}</pre>
  
Line 282: Line 323:
  
 
Note that a statusmarker string can contain duplicate statusmarkers, while an object cannot contain duplicate properties.
 
Note that a statusmarker string can contain duplicate statusmarkers, while an object cannot contain duplicate properties.
<pre data-language="javascript">function statusmarkersToObject(stats) {
+
<pre data-language="javascript" style="overflow:auto;white-space:pre-wrap;">function statusmarkersToObject(stats) {
 
     return _.reduce(stats.split(/,/), function(memo, value) {
 
     return _.reduce(stats.split(/,/), function(memo, value) {
 
         var parts = value.split(/@/),
 
         var parts = value.split(/@/),
Line 299: Line 340:
  
 
Note that a statusmarker string can contain duplicate statusmarkers, while an object cannot contain duplicate properties.
 
Note that a statusmarker string can contain duplicate statusmarkers, while an object cannot contain duplicate properties.
<pre data-language="javascript">function objectToStatusmarkers(obj) {
+
<pre data-language="javascript" style="overflow:auto;white-space:pre-wrap;">function objectToStatusmarkers(obj) {
 
     return _.map(obj, function(value, key) {
 
     return _.map(obj, function(value, key) {
 
                 return key === 'dead' || value < 1 || value > 9 ? key : key + '@' + parseInt(value);
 
                 return key === 'dead' || value < 1 || value > 9 ? key : key + '@' + parseInt(value);
Line 306: Line 347:
 
}</pre>
 
}</pre>
  
[[Category:API|Cookbook]]
+
== Underscore.js ==
 +
{{main|API:Cookbook/Underscore.js}}
 +
 
 +
The [http://underscorejs.org Underscore.js] website is more of an API reference than a guide to using the library. While useful for looking up what functions are available and what parameters they accept, it doesn't help someone trying to break into using the library's power to its fullest extent.
 +
 
 +
{{stub|section=section|Section stubs|Cookbook}}
 +
 
 +
=== Collections ===
 +
Writing scripts often involves doing '''something''' to a collection of '''things'''.  When we talk about collections, they can either be arrays:
 +
<code>var foo = [0,1,10,"banana"];</code> or objects: <code>var bar = { one: 1, two: 2, banana: 'fruit' };</code>.  Arrays are indexed by numbers (usually starting from 0 and counting up), objects have properties which can be used for indexes: <code> bar['banana'] === 'fruit';  // true!</code>.  Objects effectively act like associative arrays from other languages.
 +
 
 +
==== Sample Data ====
 +
<pre data-language="javascript" style="overflow:auto;white-space:pre-wrap;">// Sample Array:
 +
var foo = [0,1,10,"banana"];
 +
 
 +
// Sample Object
 +
var bar = { one: 1, two: 2, banana: 'fruit' };</pre>
 +
 
 +
==== Calling a function with Each Element [ _.each() ] ====
 +
It's very common to need to perform some operation with each element of a collection.  Usually people will use <code>for</code> loops or similar.  Underscore provides [http://underscorejs.org/#each _.each()], a way to call a function with each element of a collection as the argument.
 +
 
 +
<pre data-language="javascript" style="overflow:auto;white-space:pre-wrap;">_.each(foo, function(element){
 +
  log('element is '+element);
 +
});</pre>
 +
<pre style="background-color:#333; color: #E6DB74;">"element is 0"
 +
"element is 1"
 +
"element is 10"
 +
"element is banana"</pre>
 +
 
 +
What makes this so powerful is that the identical code works regardless of whether you are using an array or object:
 +
<pre data-language="javascript" style="overflow:auto;white-space:pre-wrap;">_.each(bar, function(element){
 +
  log('element is '+element);
 +
});</pre>
 +
<pre style="background-color:#333; color: #E6DB74;">"element is 1"
 +
"element is 2"
 +
"element is fruit"</pre>
 +
 
 +
Functions do not need to be inline.  They also receive additional parameters. (See documentation fore even more parameters.):
 +
<pre data-language="javascript" style="overflow:auto;white-space:pre-wrap;">var logKeyValueMapping = function( value, key ) {
 +
    log(key + " :: " + value);
 +
};
 +
 
 +
log("An Array:");
 +
_.each(foo, logKeyValueMapping);
 +
 
 +
log("An Object:");
 +
_.each(bar, logKeyValueMapping);
 +
</pre>
 +
<pre style="background-color:#333; color: #E6DB74;">"An Array:"
 +
"0 :: 0"
 +
"1 :: 1"
 +
"2 :: 10"
 +
"3 :: banana"
 +
"An Object:"
 +
"one :: 1"
 +
"two :: 2"
 +
"banana :: fruit"</pre>
 +
 
 +
==== Transforming Each Element [ _.map() ] ====
 +
The next most common thing to do with a collection is to transform all of the contained items into items of another type.  Often people might do this by making another collection, then using a <code>for</code> loop to iterate across the first collection, transforming the value and pushing it into the new container.  That's a lot of code that can be simplified with Underscore's [http://underscorejs.org/#map _.map()], a way to apply a function across a collection of elements and get a collection of the results.  If that sounds similar to _.each(), that's because it is, in fact, it has the same signature.
 +
<pre data-language="javascript" style="overflow:auto;white-space:pre-wrap;">var res = _.map(foo, function(element){
 +
  return 'element is '+element;
 +
});
 +
log(res);</pre>
 +
<pre style="background-color:#333; color: #E6DB74;">"['element is 0','element is 1','element is 10','element is banana']"</pre>
 +
 
 +
The return of _.map() is always an array of the results (see [[#Converting Collections]] below for getting objects.) and just like _.each(), the function gets more arguments and can be separately defined.
 +
 
 +
<pre data-language="javascript" style="overflow:auto;white-space:pre-wrap;">var  getKeyValueMapping = function( value, key ) {
 +
    return key + " :: " + value;
 +
};
 +
 
 +
log("An Array:");
 +
var resA = _.map(foo, getKeyValueMapping);
 +
log(resA);
 +
 
 +
log("An Object:");
 +
var resB_.map(bar, getKeyValueMapping);
 +
log(resB);
 +
</pre>
 +
<pre style="background-color:#333; color: #E6DB74;">"An Array:"
 +
"['0 :: 0', '1 :: 1', '2 :: 10', '3 :: banana']"
 +
"An Object:"
 +
"['one :: 1', 'two :: 2', 'banana :: fruit']"</pre>
 +
 
 +
==== Converting Collections [ _.reduce() ] ====
 +
 
 +
 
 +
 
 +
[[Category:API Development]]

Latest revision as of 09:08, 9 June 2024

Attention: This page is community-maintained. For the official Roll20 version of this article, see the Help Center for assistance: Here .


The following are not full scripts. They are meant to be stitched together along with business logic to assist in the creation of full scripts, not create scripts on their own.

Contents

[edit] Revealing Module Pattern

The Module Pattern emulates the concept of classes from other languages by encapsulating private and public members within an object. The Revealing Module Pattern improves upon the Module Pattern by making the syntax more consistent.

var myRevealingModule = myRevealingModule || (function() {
    var privateVar = 'This variable is private',
        publicVar  = 'This variable is public';

    function privateFunction() {
        log(privateVar);
    }

    function publicSet(text) {
        privateVar = text;
    }

    function publicGet() {
        privateFunction();
    }

    return {
        setFunc: publicSet,
        myVar: publicVar,
        getFunc: publicGet
    };
}());

log(myRevealingModule.getFunc()); // "This variable is private"
myRevealingModule.setFunc('But I can change its value');
log(myRevealingModule.getFunc()); // "But I can change its value"

log(myRevealingModule.myVar); // "This variable is public"
myRevealingModule.myVar = 'So I can change it all I want';
log(myRevealingModule.myVar); // "So I can change it all I want"

[edit] Memoization

Memoization is an optimization technique which stores the result for a given input, allowing the same output to be produced without computing it twice. This is especially useful in expensive computations. Of course, if it is rare that your function will receive the same input, memoization will be of limited utility while the storage requirements for it continue to grow.

var factorialCache = {};
function factorial(n) {
    var x;

    n = parseInt(n || 0);
    if (n < 0) {
        throw 'Factorials of negative numbers are not well-defined';
    }

    if (n === 0) {
        return 1;
    } else if (factorialCache[n]) {
        return factorialCache[n];
    }

    x = factorial(n - 1) * n;
    factorialCache[n] = x;
    return x;
}

In a Roll20 API script, the cached values could potentially be stored in state, which will persist between game sessions. If you have a large number of potential inputs, however, be aware that Roll20 may throttle your use of state.

[edit] Asynchronous Semaphore

An asynchronous semaphore allows you to fire a callback method after a set of asynchronous operations (such as calls to sendChat) have completed. While you can't guarantee what order the operations will complete in, you can guarantee that all of them have completed when the semaphore's callback fires.

When using a semaphore, call v() prior to calling each asynchronous operation, and call p() as the last statement of each asynchronous operation. If the number of operations you're going to perform is known ahead of time, you can also supply that number to the semaphore constructor and omit the calls to v.

This particular implementation of an asynchronous semaphore also lets you supply a context for the callback (set the value of this), as well as pass parameters to the callback. The parameters can be given either in the constructor or in the call to p. (Parameters in p take precedence over parameters in the constructor.)

function Semaphore(callback, initial, context) {
    var args = (arguments.length === 1 ? [arguments[0]] : Array.apply(null, arguments));

    this.lock = parseInt(initial, 10) || 0;
    this.callback = callback;
    this.context = context || callback;
    this.args = args.slice(3);
}
Semaphore.prototype = {
    v: function() { this.lock++; },
    p: function() {
        var parameters;

        this.lock--;

        if (this.lock === 0 && this.callback) {
            // allow sem.p(arg1, arg2, ...) to override args passed to Semaphore constructor
            if (arguments.length > 0) { parameters = arguments; }
            else { parameters = this.args; }

            this.callback.apply(this.context, parameters);
        }
    }
};

Example of use:

var sem = new Semaphore(function(lastAsync) {
    log(lastAsync + ' completed last');
    log(this);
}, 2, { foo: 'bar', fizz: 'buzz' }, 'Sir not appearing in this callback');

sendChat('', '/roll d20', function(ops) {
    log('Executing first sendChat');
    sem.p('First sendChat call');
});
sendChat('', '/roll d20', function(ops) {
    log('Executing second sendChat');
    sem.p('Second sendChat call');
});

Example output:

"Executing second sendChat"
"Executing first sendChat"
"First sendChat call completed last"
{ foo: "bar", fizz: "buzz" }

[edit] Handouts & Characters

[edit] Creating a Handout

Because of the way that Handout text blocks are handled, creating a Handout object needs to be done in two steps: First create the object, then set the text blocks:

    //Create a new Handout available to all players
    var handout = createObj("handout", {
                name: "The name of the handout",
                inplayerjournals: "all",
                archived: false
    });
    handout.set('notes', 'Notes need to be set after the handout is created.');
    handout.set('gmnotes', 'GM notes also need to be set after the handout is created.');

[edit] Handling Encoding

The text blocks in Handouts (Notes and GM Notes) and Characters (Bio and GM Notes) are that are set through the User Interface are stored in x-www-form-urlencoded format. You can recognize this by the sequence of %## codes throughout the text:

"Erik%20%28Viking%2BScientist%29%20%5BFighter%3A%203%2C%20Wizard%3A%202%5D"

This text can be sent to the chat and will be translated by the browser, but if you need to make changes to the text, you might want to deal with it as it was entered:

"Erik (Viking+Scientist) [Fighter: 3, Wizard: 2]"

You can decode the encoded text with the following function:

var decodeUrlEncoding = function(t){
  return t.replace(
        /%([0-9A-Fa-f]{1,2})/g,
        function(f,n){
            return String.fromCharCode(parseInt(n,16));
        }
    );
}

[edit] Utility Functions

Utility functions complete common tasks that you may want to use throughout many scripts. If you place a function at the outermost scope of a script tab, that function should be available to all of your scripts, reducing your overhead. Below is a selection of such functions.

[edit] decodeEditorText

Dependencies: None

The in-game text editor is pretty nice, but comes with a problem for API Scripts that depend on reading information from one of the large text areas in the data set. This function helps with that.

Given the text from a Graphic's gmnotes property, or a Character's bio or gmnotes property, or a Handout's notes or gmnotes property, this will return a version with the auto-inserted editor formatting stripped out.

const decodeEditorText = (t, o) =>{
  let w = t;
  o = Object.assign({ separator: '\r\n', asArray: false },o);
  /* Token GM Notes */
  if(/^%3Cp%3E/.test(w)){
    w = unescape(w);
  }
  if(/^<p>/.test(w)){
    let lines = w.match(/<p>.*?<\/p>/g)
      .map( l => l.replace(/^<p>(.*?)<\/p>$/,'$1'));
    return o.asArray ? lines : lines.join(o.separator);
  }
  /* neither */
  return t;
};

The first argument is the text to process.

const text = decodeEditorText(token.get('gmnotes'));

By default, the lines of text will be separated by \r\n.

The optional second argument is an object with options.

  • separator -- specifies what to separate lines of text with. Default: \r\n
const text = decodeEditorText(token.get('gmnotes'),{separator:'<BR>'});
  • asArray -- specifies to instead return the lines as an array. Default: false
const text = decodeEditorText(token.get('gmnotes'),{asArray:true});

NOTE: Nested <p> tags will confuse and break the decoding. If you run into that problem and need help, PM The Aaron and he'll be happy to look at it.

[edit] getCleanImgsrc

Dependencies: None

Given an image URL taken from a token or other resource, get a clean version of it that can be used to create a token via the API, or undefined if it cannot be created by the API.

var getCleanImgsrc = function (imgsrc) {
   var parts = imgsrc.match(/(.*\/images\/.*)(thumb|med|original|max)([^\?]*)(\?[^?]+)?$/);
   if(parts) {
      return parts[1]+'thumb'+parts[3]+(parts[4]?parts[4]:`?${Math.round(Math.random()*9999999)}`);
   }
   return;
};

Note: The API is only capable of creating images whose source is located in a user library. The imgsrc must also be the thumb version of the image.

[edit] getSenderForName

Dependencies: None

Given a string name, this function will return a string appropriate for the first parameter of sendChat. If there is a character that shares a name with a player, the player will be used. You may also pass an options object, which is structured identically to the options parameter of findObjs.

function getSenderForName(name, options) {
    var character = findObjs({
            type: 'character',
            name: name
        }, options)[0],
        player = findObjs({
            type: 'player',
            displayname: name.lastIndexOf(' (GM)') === name.length - 5 ? name.substring(0, name.length - 5) : name
        }, options)[0];
    
    if (player) {
        return 'player|' + player.id;
    }
    if (character) {
        return 'character|' + character.id;
    }
    return name;
}

[edit] getWhisperTarget

Dependencies: levenshteinDistance

Given a set of options, this function tries to construct the /w name portion of a whisper for a call to sendChat. The options parameter should contain either player: true or character: true and a value for either id or name. Players are preferred over characters if both are true, and ids are preferred over names if both have a valid value. If a name is supplied, the player or character with the name closest to the supplied string will be sent the whisper.

options is technically optional, but if you omit it (or don't supply a combination of player/character + id/name), the function will return an empty string.

function getWhisperTarget(options) {
    var nameProperty, targets, type;
    
    options = options || {};
    
    if (options.player) {
        nameProperty = 'displayname';
        type = 'player';
    } else if (options.character) {
        nameProperty = 'name';
        type = 'character';
    } else {
        return '';
    }
    
    if (options.id) {
        targets = [getObj(type, options.id)];
        
        if (targets[0]) {
            return '/w ' + targets[0].get(nameProperty).split(' ')[0] + ' ';
        }
    }
    if (options.name) {
        // Sort all players or characters (as appropriate) whose name *contains* the supplied name,
        // then sort them by how close they are to the supplied name.
        targets = _.sortBy(filterObjs(function(obj) {
            if (obj.get('type') !== type) return false;
            return obj.get(nameProperty).indexOf(options.name) >= 0;
        }), function(obj) {
            return Math.abs(levenshteinDistance(obj.get(nameProperty), options.name));
        });
        
        if (targets[0]) {
            return '/w ' + targets[0].get(nameProperty).split(' ')[0] + ' ';
        }
    }
    
    return '';
}

[edit] processInlinerolls

This function will scan through msg.content and replace inline rolls with their total result. This is particularly useful for API commands to which the user may want to pass inline rolls as parameters.

function processInlinerolls(msg) {
    if (_.has(msg, 'inlinerolls')) {
        return _.chain(msg.inlinerolls)
                .reduce(function(previous, current, index) {
                    previous['$[[' + index + ']]'] = current.results.total || 0;
                    return previous;
                },{})
                .reduce(function(previous, current, index) {
                    return previous.replace(index, current);
                }, msg.content)
                .value();
    } else {
        return msg.content;
    }
}

Here is a slightly more complicated version which also handles converting tableItems to their text:

function processInlinerolls(msg) {
	if(_.has(msg,'inlinerolls')){
		return _.chain(msg.inlinerolls)
		.reduce(function(m,v,k){
			var ti=_.reduce(v.results.rolls,function(m2,v2){
				if(_.has(v2,'table')){
					m2.push(_.reduce(v2.results,function(m3,v3){
						m3.push(v3.tableItem.name);
						return m3;
					},[]).join(', '));
				}
				return m2;
			},[]).join(', ');
			m['$[['+k+']]']= (ti.length && ti) || v.results.total || 0;
			return m;
		},{})
		.reduce(function(m,v,k){
			return m.replace(k,v);
		},msg.content)
		.value();
	} else {
		return msg.content;
	}
}

[edit] statusmarkersToObject

The inverse of objectToStatusmarkers; transforms a string suitable for use as the value of the statusmarkers property of a Roll20 token object into a plain old JavaScript object.

Note that a statusmarker string can contain duplicate statusmarkers, while an object cannot contain duplicate properties.

function statusmarkersToObject(stats) {
    return _.reduce(stats.split(/,/), function(memo, value) {
        var parts = value.split(/@/),
            num = parseInt(parts[1] || '0', 10);

        if (parts[0].length) {
            memo[parts[0]] = Math.max(num, memo[parts[0]] || 0);
        }

        return memo;
    }, {});
}

[edit] objectToStatusmarkers

The inverse of statusmarkersToObject; transforms a plain old JavaScript object into a comma-delimited string suitable for use as the value of the statusmarkers property of a Roll20 token object.

Note that a statusmarker string can contain duplicate statusmarkers, while an object cannot contain duplicate properties.

function objectToStatusmarkers(obj) {
    return _.map(obj, function(value, key) {
                return key === 'dead' || value < 1 || value > 9 ? key : key + '@' + parseInt(value);
            })
            .join(',');
}

[edit] Underscore.js

Main Page: API:Cookbook/Underscore.js

The Underscore.js website is more of an API reference than a guide to using the library. While useful for looking up what functions are available and what parameters they accept, it doesn't help someone trying to break into using the library's power to its fullest extent.


[edit] Collections

Writing scripts often involves doing something to a collection of things. When we talk about collections, they can either be arrays: var foo = [0,1,10,"banana"]; or objects: var bar = { one: 1, two: 2, banana: 'fruit' };. Arrays are indexed by numbers (usually starting from 0 and counting up), objects have properties which can be used for indexes: bar['banana'] === 'fruit'; // true!. Objects effectively act like associative arrays from other languages.

[edit] Sample Data

// Sample Array:
var foo = [0,1,10,"banana"];

// Sample Object
var bar = { one: 1, two: 2, banana: 'fruit' };

[edit] Calling a function with Each Element [ _.each() ]

It's very common to need to perform some operation with each element of a collection. Usually people will use for loops or similar. Underscore provides _.each(), a way to call a function with each element of a collection as the argument.

_.each(foo, function(element){
  log('element is '+element);
});
"element is 0"
"element is 1"
"element is 10"
"element is banana"

What makes this so powerful is that the identical code works regardless of whether you are using an array or object:

_.each(bar, function(element){
  log('element is '+element);
});
"element is 1"
"element is 2"
"element is fruit"

Functions do not need to be inline. They also receive additional parameters. (See documentation fore even more parameters.):

var logKeyValueMapping = function( value, key ) {
    log(key + " :: " + value);
};

log("An Array:");
_.each(foo, logKeyValueMapping);

log("An Object:");
_.each(bar, logKeyValueMapping);
"An Array:"
"0 :: 0"
"1 :: 1"
"2 :: 10"
"3 :: banana"
"An Object:"
"one :: 1"
"two :: 2"
"banana :: fruit"

[edit] Transforming Each Element [ _.map() ]

The next most common thing to do with a collection is to transform all of the contained items into items of another type. Often people might do this by making another collection, then using a for loop to iterate across the first collection, transforming the value and pushing it into the new container. That's a lot of code that can be simplified with Underscore's _.map(), a way to apply a function across a collection of elements and get a collection of the results. If that sounds similar to _.each(), that's because it is, in fact, it has the same signature.

var res = _.map(foo, function(element){
  return 'element is '+element;
});
log(res);
"['element is 0','element is 1','element is 10','element is banana']"

The return of _.map() is always an array of the results (see #Converting Collections below for getting objects.) and just like _.each(), the function gets more arguments and can be separately defined.

var  getKeyValueMapping = function( value, key ) {
    return key + " :: " + value;
};

log("An Array:");
var resA = _.map(foo, getKeyValueMapping);
log(resA);

log("An Object:");
var resB_.map(bar, getKeyValueMapping);
log(resB);
"An Array:"
"['0 :: 0', '1 :: 1', '2 :: 10', '3 :: banana']"
"An Object:"
"['one :: 1', 'two :: 2', 'banana :: fruit']"

[edit] Converting Collections [ _.reduce() ]