Mod (API): Livre de recettes

Les éléments suivants ne sont pas des scripts complets. Ils sont destinés à être assemblés avec la logique métier pour aider à la création de scripts complets, et non pas à créer des scripts seuls.


Pattern Revealing Module

Le modèle de moduleémule le concept de classes d'autres langages en encapsulant les membres privés et publics dans un objet. Le schéma de module révélateur améliore le schéma de module en rendant la syntaxe plus cohérente.

var myRevealingModule = myRevealingModule || (function() {
    var privateVar = 'Cette variable est privée',
        publicVar  = 'Cette variable est publique';

    function privateFunction() {
        log(privateVar);
    }

    function publicSet(text) {
        privateVar = text;
    }

    function publicGet() {
        privateFunction();
    }

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

log(myRevealingModule.getFunc()); // "Cette variable est privée"
myRevealingModule.setFunc('Mais je peux changer sa valeur');
log(myRevealingModule.getFunc()); // "Mais je peux changer sa valeur"

log(myRevealingModule.myVar); // "Cette variable est publique"
myRevealingModule.myVar = 'Je peux donc la changer à ma guise';
log(myRevealingModule.myVar); // "Je peux donc la changer à ma guise"

Memoization

La mémorisation Cela est particulièrement utile dans les calculs coûteux. Bien sûr, si la probabilité que votre fonction reçoive la même entrée est faible, la mémorisation aura une utilité limitée tandis que les besoins en stockage continueront de croître.

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

Dans un script Mod (API) Roll20, les valeurs mises en cache pourraient potentiellement être stockées dans state, ce qui persistera entre les sessions de jeu. Toutefois, si vous avez un grand nombre d'entrées potentielles, sachez que Roll20 peut limiter votre utilisation de l'état.


Sémaphore asynchrone

Un sémaphore asynchrone vous permet d'appeler une méthode de rappel après un ensemble d'opérations asynchrones (telles que les appels à sendChat) qui ont été terminées. Bien que vous ne puissiez pas garantirl'ordre dans lequelles opérations se termineront, vous pouvez garantir que toutesontété effectuées lorsque le rappel du sémaphore se déclenche.

Lorsque tu utilises un sémaphore, appellev()avant d'appeler chaque opération asynchrone, et appellep()en tant que dernière instruction de chaque opération asynchrone. Si tu connais à l'avance le nombre d'opérations que tu vas effectuer, tu peux également fournir ce nombre au constructeur du sémaphore et omettre les appels àv.

Cette implémentation particulière d'un sémaphore asynchrone te permet également de fournir un contexte pour le rappel (définir la valeur dethis), ainsi que de transmettre des paramètres au rappel. Les paramètres peuvent être donnés soit dans le constructeur, soit dans l'appel àp. (Les paramètres danspont la priorité sur les paramètres du constructeur.)

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

Exemple d'utilisation:

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

Exemple de résultat:

"Exécuter second envoiChat"
"Exécuter premier envoiChat"
"Premier appel envoiChat terminé en dernier"
{ foo: "bar", fizz: "buzz" }

Documents & Personnages

Créer un document

En raison de la façon dont les blocs de texte de document sont gérés, la création d'un objet document doit être effectuée en deux étapes : d'abord créer l'objet, puis définir les blocs de texte :

//Créez une nouvelle Notice disponible pour tous les joueurs
    var notice = createObj("notice", {
                nom: "Le nom de la notice",
                dansjournaljoueurs: "tous",
                archivé: false
    });
    notice.définir('notes', 'Les notes doivent être définies après la création de la notice.');
    notice.définir('gmnotes', 'Les notes du MJ doivent également être définies après la création de la notice.');

Traitement de l'encodage

Les blocs de texte dans les Notices (Notes et Notes du MJ) et les Personnages (Bio et Notes du MJ) qui sont définis via l'interface utilisateur sont enregistrés au format x-www-form-urlencoded. Vous pouvez les reconnaître grâce à la séquence de codes %## présents dans tout le texte :

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

Ce texte peut être envoyé via le chat et sera traduit par le navigateur, mais si vous avez besoin de modifier le texte, vous voudrez peut-être le traiter tel qu'il a été saisi :

Erik (Viking+Scientifique) [Combattant: 3, Magicien: 2]"

Vous pouvez décoder le texte encodé avec la fonction suivante :

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

Fonctions utilitaires

Les fonctions utilitaires accomplissent des tâches courantes que vous pouvez vouloir utiliser dans de nombreux scripts. Si vous placez une fonction dans la portée extérieure d'un onglet de script, cette fonction devrait être disponible pour tous vos scripts, réduisant ainsi votre surcharge. Voici une sélection de telles fonctions.

decodeEditorText

Dépendances : Aucune

Le nouvel éditeur de texte en jeu est assez sympa, mais pose un problème pour les scripts Mod (API) qui dépendent de la lecture d'informations provenant de l'une des grandes zones de texte de l'ensemble de données. Cette fonction aide à cela.

Étant donné le texte d'une propriétéGraphic'sgmnotes, ou d'une propriétéCharacter'sbioougmnotes, ou d'une propriétéHandout'snotesougmnotes, cela renverra une version dont le formatage de l'éditeur auto-inséré aura été supprimé.

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

Le premier argument est le texte à traiter.

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

Par défaut, les lignes de texte seront séparées par\r\n.

Le deuxième argument optionnel est un objet avec des options.

séparateur-- spécifie ce qui sépare les lignes de texte. Par défaut:\r\n

const text = decodeEditorText(token.get('gmnotes'),{separator:'<BR>'});

 

asArray-- spécifie de renvoyer les lignes sous forme de tableau. Par défaut:false

const text = decodeEditorText(token.get('gmnotes'),{asArray:true});

NOTE: Balises imbriquées<p>peuvent embrouiller et empêcher le décodage. Si vous rencontrez ce problème et avez besoin d'aide, envoyez un message privé àThe Aaron et il sera heureux de jeter un coup d'œil.


getCleanImgsrc

Dépendances : Aucune

Étant donné une URL d'image provenant d'un pion ou d'une autre ressource, obtenez une version propre de celle-ci qui peut être utilisée pour créer un pion via l'API, ou undefined si elle ne peut pas être créée par l'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;
};

Remarque : L'API est uniquement capable de créer des images dont la source est située dans une bibliothèque utilisateur. L'image imgsrc doit également être la version thumb de l'image.


getSenderForName

Dépendances : Aucune

Étant donné une chaîne de caractères name, cette fonction renverra une chaîne de caractères appropriée pour le premier paramètre de sendChat. Si un personnage partage un nom avec un joueur, le joueur sera utilisé. Vous pouvez également transmettre un objetoptions, dont la structure est identique à celle du paramètreoptionsde findObjs.

fonction 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. Si un nom est fourni, le joueur ou le personnage portant le nom le plus proche de la chaîne fournie recevra un message privé.

optionsest techniquement facultatif, mais si vous l'omettez (ou si vous ne fournissez pas une combinaison de joueur/personnage + id/nom), la fonction renverra une chaîne vide.

function getWhisperTarget(options) {
    var nameProperty, targets, type;
    
    options = options || {};
    
    if (options.player) {
        nameProperty = 'displayname';
        type = 'joueur';
    } else if (options.character) {
        nameProperty = 'nom';
        type = 'personnage';
    } else {
        return '';
    }
    
    if (options.id) {
        targets = [getObj(type, options.id)];
        
        if (targets[0]) {
            return '/w ' + targets[0].get(nameProperty).split(' ')[0] + ' ';
        }
    }
    if (options.name) {
        // Trier tous les joueurs ou personnages (selon le cas) dont le nom *contient* le nom fourni,
        // puis les trier en fonction de leur proximité avec le nom fourni.
        cibles = _.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 (cibles[0]) {
            return '/w ' + cibles[0].get(nameProperty).split(' ')[0] + ' ';
        }
    }
    
    return '';
}

processInlinerolls

Cette fonction analysera msg.content et remplacera les jets en ligne par leur résultat total. Ceci est particulièrement utile pour les commandes Mod (API) auxquelles l'utilisateur pourrait vouloir transmettre des jets en ligne en tant que paramètres.

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

statusmarkersToObject

L'inverse de objectToStatusmarkers ; transforme une chaîne utilisable comme valeur de la propriétéstatusmarkersd'un objet jeton Roll20 en un simple objet JavaScript.

Notez qu'une chaîne de statusmarker peut contenir des statusmarkers en double, tandis qu'un objet ne peut pas contenir de propriétés en double.

fonction 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

L'inverse de statusmarkersToObject ; transforme un vieil objet JavaScript en une chaîne délimitée par des virgules pouvant être utilisée comme valeur de la propriétéstatusmarkersd'un objet Roll20 token.

Notez qu'une chaîne de statusmarker peut contenir des statusmarkers en double, tandis qu'un objet ne peut pas contenir de propriétés en double.

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

 


Underscore.js

Le siteUnderscore.jsest davantage une référence à l'API qu'un guide d'utilisation de la bibliothèque. Bien qu'il soit utile pour rechercher les fonctions disponibles et les paramètres qu'elles acceptent, il n'aide pas quelqu'un qui essaie d'exploiter pleinement la puissance de la bibliothèque.

Collection

L'écriture de scripts consiste souvent à fairequelque choseà une collection dechoses. Les collections peuvent être des tableaux :var foo = [0,1,10, "banana"] ;ou des objets :var bar = { one: 1, two: 2, banana: 'fruit' };. Les tableaux sont indexés par des nombres (généralement à partir de 0), les objets ont des propriétés qui peuvent être utilisées comme index :bar['banana'] === 'fruit' ; // true !. Les objets agissent efficacement comme des tableaux associatifs d'autres langages.

Données d'exemple
Échantillon de tableau:
var foo = [0,1,10,"banane"];

// Objet d'échantillon
var bar = { one: 1, two: 2, banana: 'fruit' };
Appel d'une fonction avec chaque élément [ _.each() ]

Il est très courant d'avoir besoin d'effectuer une opération avec chaque élément d'une collection. Généralement, les gens utiliserontpourboucles ou similaires. Underscore fournit_.each(), une façon d'appeler une fonction avec chaque élément d'une collection en tant qu'argument.

_.each(foo, function(element){
  log('element is '+element);
"l'élément est 0"
"l'élément est 1"
"l'élément est 10"
"l'élément est banane"

Ce qui rend cela si puissant, c'est que le code identique fonctionne que vous utilisiez un tableau ou un objet:

_.each(bar, function(element){
  log('element is '+element);
"élément est 1"
"élément est 2"
"élément est fruit"

Les fonctions n'ont pas besoin d'être en ligne. Ils reçoivent également des paramètres supplémentaires. (Voir la documentation pour encore plus de paramètres.):

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

log("Un tableau:");
_.each(foo, logKeyValueMapping);

log("Un objet:");
_.each(bar, logKeyValueMapping);
"Un tableau:"
"0 :: 0"
"1 :: 1"
"2 :: 10"
"3 :: banane"
"Un objet:"
"un :: 1"
"deux :: 2"
"banane :: fruit"
Transformation de chaque élément [_.map()]

La prochaine chose la plus courante à faire avec une collection est de transformer tous les éléments qu'elle contient en éléments d'un autre type. Souvent, les gens font cela en créant une autre collection, puis en utilisant une boucleforpour itérer à travers la première collection, en transformant la valeur et en la poussant dans le nouveau conteneur. Cela fait beaucoup de code qui peut être simplifié grâce à_.map()d'Underscore, un moyen d'appliquer une fonction à une collection d'éléments et d'obtenir une collection de résultats. Si cela semble similaire à _.each(), c'est parce que c'est le cas, en fait, elle a la même signature.

var res = _.map(foo, function(element){
  return 'element is '+element ;
}) ;
log(res) ;
['l'élément est 0','l'élément est 1','l'élément est 10','l'élément est banane']

Le résultat de _.map() est toujours un tableau contenant les résultats (voir Converting Collections ci-dessous pour obtenir des objets.) et tout comme _.each(), la fonction reçoit plus d'arguments et peut être définie séparément.

[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']"
Cet article vous a-t-il été utile ?
Utilisateurs qui ont trouvé cela utile : 12 sur 14