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
(Underscore.js: Added Collections section)
(Collections)
Line 225: Line 225:
  
 
=== Collections ===
 
=== Collections ===
Writing scripts often involves doing *something* to a collection of *things*.   When we talk about collections, they can either be arrays:
+
Writing scripts often involves operating on 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>. Array's are indexed by numbers (usually starting from 0 and counting up), objects have property which can be used for indexes: <code> bar['banana'] === 'fruit';  // true!</code>. Objects effectively act like associative arrays from other languages.
+
<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 property which can be used for indexes: <code>bar['banana'] === 'fruit';  // true!</code>. Objects effectively act like associative arrays or dictionaries from other languages.
  
 
==== Calling a function with Each Element ====
 
==== Calling a function with Each Element ====
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 [http://underscorejs.org/#each _.each], a way to call a function with each element of a collection as the argument.
+
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">_.each(foo, function(element){
 
<pre data-language="javascript">_.each(foo, function(element){
Line 248: Line 248:
  
 
Functions do not need to be inline.  They also receive additional parameters.:
 
Functions do not need to be inline.  They also receive additional parameters.:
<pre data-language="javascript">var logKeyValueMapping = function( value, key ) {
+
<pre data-language="javascript">var logKeyValueMapping = function(value, key, list) {
     log(key + " :: " + value);
+
    list[key] = value + ' ' + 1;
 +
     log(key + " :: " + list[key]);
 
};
 
};
  
 
log("An Array:");
 
log("An Array:");
 
_.each(foo, logKeyValueMapping);
 
_.each(foo, logKeyValueMapping);
 +
log(foo);
  
 
log("An Object:");
 
log("An Object:");
 
_.each(bar, logKeyValueMapping);
 
_.each(bar, logKeyValueMapping);
 +
log(bar);
 
</pre>
 
</pre>
 
<pre>An Array:
 
<pre>An Array:
0 :: 0
+
0 :: 0 1
1 :: 1
+
1 :: 1 1
2 :: 10
+
2 :: 10 1
3 :: banana
+
3 :: banana 1
 +
["0 1", "1 1", "10 1", "banana 1"]
 
An Object:
 
An Object:
one :: 1
+
one :: 1 1
two :: 2
+
two :: 2 1
banana :: fruit</pre>
+
banana :: fruit 1
 +
{one: "1 1", two: "2 1", banana: "fruit 1"}</pre>
  
 
==== Transforming Each Element ====
 
==== Transforming Each Element ====

Revision as of 16:29, 30 January 2015

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

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"

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.

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) {
    this.lock = initial || 0;
    this.callback = callback;
    this.context = context || callback;
    this.args = arguments.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(context, parameters);
        }
    }
};

Example of use:

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

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

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.

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;
}

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 '';
}

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();
    }
}

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;
    }, {});
}

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(',');
}

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.


Collections

Writing scripts often involves operating on 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 property which can be used for indexes: bar['banana'] === 'fruit'; // true!. Objects effectively act like associative arrays or dictionaries from other languages.

Calling a function with Each Element

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 if 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.:

var logKeyValueMapping = function(value, key, list) {
    list[key] = value + ' ' + 1;
    log(key + " :: " + list[key]);
};

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

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

Transforming Each Element

Converting Collections