Scripts de mod : Recettes

Ce qui suit ne constitue pas des scripts complets. Ils sont destinés à être assemblés avec la logique métier afin de faciliter la création de scripts complets, et non à créer des scripts de manière autonome.

Mise en évidence du modèle de module

Le modèle « Module » reproduit le concept de classes présent dans d'autres langages en encapsulant les membres privés et publics au sein d'un objet. Le modèle « Revealing Module » apporte des améliorations au modèle « 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() {
    return privateVar;
  }
  return {
    setFunc: publicSet,
    myVar: publicVar,
    getFunc: publicGet
  };
}());
log(myRevealingModule.getFunc()); // « Cette variable est privée »
myRevealingModule.setFunc('Mais je peux modifier sa valeur');
log(myRevealingModule.getFunc()); // « Mais je peux modifier sa valeur »
log(myRevealingModule.myVar); // « Cette variable est publique »
myRevealingModule.myVar = 'Je peux donc la modifier comme bon me semble';
log(myRevealingModule.myVar); // « Je peux donc la modifier comme bon me semble »

Mémoisation

La mémorisation est une technique d'optimisation qui consiste à enregistrer le résultat obtenu pour une entrée donnée, ce qui permet d'obtenir le même résultat sans avoir à le calculer une deuxième fois. Cela s'avère particulièrement utile lors de calculs coûteux. Bien sûr, s'il est rare que votre fonction reçoive la même entrée, la mémorisation n'aura qu'une utilité limitée, tandis que les besoins en mémoire qu'elle engendre continueront d'augmenter.

var factorialCache = {};
function factorial(n) {
  var x;
  n = parseInt(n || 0);
  if (n < 0) {
    throw 'Les factoriels des nombres négatifs ne sont pas bien définis';
  }
  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 de mod, les valeurs mises en cache peuvent être stockées dans l'état, qui est conservé d'une session à l'autre. Cet état est partagé par tous les scripts du jeu ; veillez donc à ce que la taille du cache reste réduite.

Sémaphore asynchrone

Un sémaphore asynchrone vous permet de déclencher une méthode de rappel une fois qu'une série d'opérations asynchrones (telles que des appels à la méthode `sendChat`) s'est achevée. Même si vous ne pouvez pas garantir l'ordre dans lequel les opérations seront exécutées, vous pouvez garantir qu'elles seront toutes terminées lorsque la fonction de rappel du sémaphore sera déclenchée.

Lorsque vous utilisez un sémaphore, appelez la fonction v() avant d'appeler chaque opération asynchrone, et appelez la fonction p() en tant que dernière instruction de chaque opération asynchrone. Si le nombre d'opérations que vous comptez effectuer est connu à l'avance, vous pouvez également indiquer ce nombre au constructeur du sémaphore et omettre les appels à la fonction v().

Cette implémentation particulière d'un sémaphore asynchrone vous permet également de fournir un contexte pour la fonction de rappel (en définissant la valeur de cette variable), ainsi que de lui transmettre des paramètres. Les paramètres peuvent être fournis soit dans le constructeur, soit lors de l'appel à p. (Les paramètres de p ont priorité sur ceux 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) {
      // autorise sem.p(arg1, arg2, ...) à remplacer les arguments transmis au constructeur Semaphore
      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 + ' a terminé en dernier') ;
  log(this) ;
}, 2, { foo: 'bar', fizz: 'buzz' }, 'Monsieur n'apparaît pas dans cette fonction de rappel') ;
sendChat('', '/roll d20', function(ops) {
  log('Exécution du premier sendChat') ;
  sem.p('Premier appel sendChat');
});
sendChat('', '/roll d20', function(ops) {
  log('Exécution du deuxième sendChat');
  sem.p('Deuxième appel sendChat');
});

Exemple de résultat :

« Exécution du deuxième sendChat »
« Exécution du premier sendChat »
« Le premier appel sendChat s'est terminé en dernier »
{ foo : « bar », fizz : « buzz » }

Documents à distribuer & Personnages

Création d'un document à distribuer

En raison de la manière dont les blocs de texte des Documents sont gérés, la création d'un objet « Document » doit s'effectuer en deux étapes : créez d'abord l'objet, puis définissez les blocs de texte :

//Créer un nouveau Document mis à la disposition de tous les joueurs
var handout = createObj("handout", {
  name:
  inplayerjournals: "all",
  archived: false
});
handout.set('notes', 'Les notes doivent être définies après la création du document.');
handout.set('gmnotes', 'Les notes du MJ doivent également être définies après la création du document.');

Gestion de l'encodage

Les blocs de texte des rubriques « Documents » (Notes et Notes du MJ) et « Personnages » (Biographie et Notes du MJ) qui sont définis via l'interface utilisateur sont enregistrés au format x-www-form-urlencoded. Vous pouvez le reconnaître à la présence de la séquence de codes %## tout au long du texte :

« Erik%20%28Viking%2BScientifique%29%20%5BCombattant%3A%203%2C%20Sorcier%3A%202%5D»

Ce texte peut être envoyé dans le Chat et sera traduit par le navigateur, mais si vous devez y apporter des modifications, vous préférerez peut-être le traiter tel qu'il a été saisi :

« Erik (Viking + Scientifique) [Combattant : 3, Sorcier : 2] »

Vous pouvez décoder le texte codé à l'aide de la fonction suivante :

var decodeUrlEncoding = function(t) {
  return decodeURIComponent(t.replace(/\+/g, " "));
}

Fonctions outils

Les outils permettent d'effectuer des tâches courantes dont vous pourriez avoir besoin dans de nombreux scripts. Une fonction située dans la portée la plus externe d'un onglet de script est visible par tous les scripts du jeu, car ceux-ci partagent une seule portée globale. Deux scripts portant le même nom se remplaceront mutuellement. Vous trouverez ci-dessous une sélection de ces fonctions.

décoder le texte de l'éditeur

Dépendances : Aucune

L'éditeur de texte intégré au jeu est plutôt pratique, mais il pose un problème pour les scripts de mod qui dépendent de la lecture d'informations provenant de l'une des grandes zones de texte du jeu de données. Cette fonction facilite cette tâche.

À partir du texte de la propriété « gmnotes » d'un élément graphique, de la propriété « bio » ou « gmnotes » d'un personnage, ou encore de la propriété « notes » ou « gmnotes » d'un Document, cette fonction renverra une version dont la mise en forme ajoutée automatiquement par l'éditeur aura été supprimée.

const decodeEditorText = (t, o) => {
  let w = t ;
  o = Object.assign({ separator: '\r\n', asArray: false }, o) ;
  /* Notes du MJ sur les jetons */
  if(/^%3Cp%3E/.test(w)){
    w = decodeURIComponent(w) ;
  }
  if(/^<p>/.test(w)){
    let lines = w.match(/<p>.*?<\/p>/g);
    if (!lines) return t;
    lines = lines.map(l => l.replace(/^<p>(.*?)<\/p>$/, '$1'));
    return o.asArray ? lignes : lignes.join(o.separator) ;
  }
  /* ni l'un ni l'autre */
  return t ;
 } ;

Le premier argument est le texte à traiter.

const text = décoderLeTexteDeL'Éditeur(token.get('gmnotes'));

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

Le deuxième argument facultatif est un objet contenant des options.

  • séparateur – permet de définir le caractère utilisé pour séparer les lignes de texte. Par défaut : \r\n
const text = decodeEditorText(token.get('gmnotes'),{separator:'<BR>'});
  • asArray – indique de renvoyer les lignes sous forme de tableau. Valeur par défaut : false
const text = decodeEditorText(token.get('gmnotes'),{asArray:true});

Remarque : les tags imbriqués <p> ne sont pas pris en charge. Les fiches de personnage, les biographies, les documents et les notes du MJ sont des blocs HTML (veuillez les lire à l'aide d'une fonction de rappel). « Graphic gmnotes » est une chaîne synchrone et correspond au champ concerné par cette vérification : %3Cp%3E.

obtenirCleanImgsrc

Dépendances : Aucune

À partir d'une URL d'image provenant d'un jeton ou d'une autre ressource, récupérez une version épurée de celle-ci pouvant être utilisée pour créer un jeton via un script de mod, ou la valeur « undefined » si celle-ci ne peut pas être créée par un script de mod.

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

Remarque : les scripts de mod ne peuvent créer des images qu'à partir de la bibliothèque d'un utilisateur. La taille du pouce n'est plus requise. La fonction get("imgsrc") peut ne pas correspondre à l'URL que vous avez fournie, car les URL de stockage sont réécrites. Voir la section « Objets » : restrictions relatives à imgsrc.

getSenderForName

Dépendances : aucune

À partir d'une chaîne de caractères « name », cette fonction renvoie une chaîne de caractères adaptée au premier paramètre de la fonction sendChat. Si un personnage partage le même nom qu'un joueur, c'est ce dernier qui sera utilisé. Vous pouvez également transmettre un objet « options », dont la structure est identique à celle du paramètre « options » de la fonction findObjs.

function getSenderForName(name, options) {
  var character = findObjs({
    type: 'personnage',
    name: name
  }, options)[0],
  player = findObjs({
    type: 'personnage',
    displayname: name.endsWith(' (MJ)') ? name.slice(0, -5) : name
  }, options)[0] ;
  if (player) {
    return 'player|' + player.id ;
  }
  if (character) {
    return 'character|' + character.id ;
  }
  return name ;
}

obtenirCibleChuchotement

Dépendances : levenshteinDistance

À partir d'un ensemble d'options, cette fonction tente de construire la partie « /w » d'un message chuchoté destiné à la fonction sendChat. Le paramètre « options » doit contenir soit « player: true », soit « personnage: true », ainsi qu'une valeur pour « id » ou « name ». Les joueurs sont préférés aux personnages si les deux sont valides, et les identifiants sont préférés aux noms si les deux ont une valeur valide. Si un nom est fourni, le joueur ou le personnage dont le nom est le plus proche de la chaîne fournie recevra le message privé.

L'option « options » est techniquement facultative, mais si vous l'omettez (ou si vous ne fournissez pas une combinaison « joueur/personnage + identifiant/nom »), la fonction renverra une chaîne vide.

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) {
    // Triez tous les joueurs ou personnages (selon le cas) dont le nom *contient* le nom fourni,
    // puis triez-les en fonction de leur proximité avec le nom fourni.
    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 '';
}

processusInlinerolls

Cette fonction analysera le contenu de « msg.content » et remplacera les jets effectués en ligne par leur résultat total. Cela s'avère particulièrement utile pour les commandes Mod Script auxquelles l'utilisateur pourrait souhaiter transmettre des jets de dés intégrés 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.split(index).join(String(current));
      }, msg.content)
      .value();
  } else {
    return msg.content;
  }
}

Voici une version légèrement plus complexe qui gère également la conversion des éléments de table en texte :

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.split(k).join(String(v));
      },msg.content)
      .value();
  } else {
    return msg.content;
  }
}

marqueurs d'état vers objet

L'inverse de la fonction `objectToStatusmarkers`; transforme une chaîne de caractères pouvant servir de valeur à la propriété `statusmarkers` d'un Objet jeton Roll20 en un objet JavaScript classique.

Veuillez noter qu'une chaîne de marqueurs d'état peut contenir des marqueurs d'état en double, alors qu'un objet ne peut pas contenir de propriétés en double.

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

marqueurs d'état des objets

L'inverse de la fonction `statusmarkersToObject`; transforme un objet JavaScript classique en une chaîne de caractères délimitée par des virgules, pouvant servir de valeur à la propriété `statusmarkers ` d'un Objet jeton de Roll20.

Veuillez noter qu'une chaîne de marqueurs d'état peut contenir des marqueurs d'état en double, alors qu'un objet ne peut pas contenir de propriétés en double.

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

Underscore.js

Le site web d'Underscore.js constitue davantage une référence qu'un guide d'utilisation de la bibliothèque. Bien que cela soit utile pour connaître les fonctions disponibles et les paramètres qu'elles acceptent, cela n'aide pas quelqu'un qui cherche à se familiariser avec la bibliothèque afin d'exploiter pleinement ses capacités.

Collections

La rédaction de scripts implique souvent d'effectuer une action sur un ensemble d'éléments. Les collections peuvent être des tableaux, par exemple : `var foo = [0, 1, 10, "banane"];`, ou des objets, par exemple : ` var bar = { one: 1, two: 2, banana: "fruit" };`. Les tableaux sont indexés par des nombres, en commençant généralement par 0. Les objets sont indexés par le nom de leurs propriétés : bar["banane"] === "fruit". Les objets se comportent comme des tableaux associatifs dans d'autres langages.

Exemple de données

// Exemple de tableau :
var foo = [0, 1, 10, « banana »] ;
// Exemple d'objet
var bar = { one : 1, two : 2, banana : 'fruit' } ;

Appeler une fonction avec chaque élément [ _.each() ]

Il est très courant de devoir effectuer une opération sur chaque élément d’une collection. En général, on utilise des boucles « for » ou des constructions similaires. Underscore propose la méthode _.each(), qui permet d'appeler une fonction en lui passant chaque élément d'une collection comme argument.

_.each(foo, function(élément) {
  log('élément est ' + élément);
});
« élément est 0 »
« élément est 1 »
« élément est 10 »
« élément est banane »

Ce qui rend cette fonctionnalité si puissante, c'est que le même code 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. (Veuillez consulter la documentation pour obtenir davantage 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 deuxième chose la plus courante à faire avec une collection est de transformer tous les articles qu'elle contient en articles d'un autre type. Souvent, on procède ainsi en créant une autre collection, puis en utilisant une boucle « for » pour parcourir la première collection, en transformant la valeur et en l'ajoutant au nouveau conteneur. Cela représente beaucoup de code qui peut être simplifié grâce à la méthode _.map() d'Underscore, qui permet d'appliquer une fonction à un ensemble d'éléments et d'obtenir un ensemble de résultats. Si cela vous rappelle _.each(), c'est parce que c'est effectivement le cas : cette méthode a en effet la même signature.

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

La méthode _.map() renvoie toujours un tableau contenant les résultats (voir la section « Conversion des collections » ci-dessous pour les objets). Tout comme pour _.each(), la fonction reçoit davantage d'arguments et peut être définie séparément.

var getKeyValueMapping = function( value, key ) {
  return key + " :: " + value;
};
log("Un tableau :");
var resA = _.map(foo, getKeyValueMapping);
log(resA);
log("Un objet :");
var resB = _.map(bar, getKeyValueMapping);
log(resB);
« Un tableau : »
« ['0 :: 0', '1 :: 1', '2 :: 10', '3 :: banana'] »
« Un objet : »
« ['one :: 1', 'two :: 2', 'banana :: fruit'] »

Conversion de collections [ _.reduce() ]

_.reduce() réduit une collection à une seule valeur en appelant une fonction avec un accumulateur et chaque élément. Veuillez consulter la documentation d'Underscore pour connaître la signature complète et découvrir des exemples.

Cet article vous a-t-il été utile ?
Utilisateurs qui ont trouvé cela utile : 17 sur 20