Scripts de mod: Libro de recetas

Los siguientes textos no son guiones completos. Su finalidad es combinarse con la lógica de negocio para facilitar la creación de scripts completos, no para crear scripts por sí mismos.

Patrón del módulo de revelación

El patrón «Módulo» emula el concepto de clases de otros lenguajes mediante la encapsulación de miembros privados y públicos dentro de un objeto. El patrón «Revealing Module» mejora el patrón «Module» al dotar a la sintaxis de mayor coherencia.

var myRevealingModule = myRevealingModule || (function() {
  var privateVar = 'Esta variable es privada',
    publicVar = 'Esta variable es pública';
  function privateFunction() {
    log(privateVar);
  }
  function publicSet(text) {
    privateVar = text;
  }
  function publicGet() {
    return privateVar;
  }
  return {
    setFunc: publicSet,
    myVar: publicVar,
    getFunc: publicGet
  };
}());
log(myRevealingModule.getFunc()); // «Esta variable es privada»
myRevealingModule.setFunc('Pero puedo cambiar su valor');
log(myRevealingModule.getFunc()); // «Pero puedo cambiar su valor»
log(myRevealingModule.myVar); // «Esta variable es pública»
myRevealingModule.myVar = 'Así que puedo cambiarla como quiera';
log(myRevealingModule.myVar); // «Así que puedo cambiarla como quiera»

Memorización

La memorización es una técnica de optimización que almacena el resultado correspondiente a una entrada determinada, lo que permite obtener el mismo resultado sin tener que calcularlo dos veces. Esto resulta especialmente útil en cálculos que requieren un gran esfuerzo computacional. Por supuesto, si es poco frecuente que su función reciba la misma entrada, la memoización tendrá una utilidad limitada, mientras que las necesidades de almacenamiento seguirán aumentando.

var factorialCache = {};
function factorial(n) {
  var x;
  n = parseInt(n || 0);
  if (n < 0) {
    throw 'Los factoriales de números negativos no están bien definidos';
  }
  if (n === 0) {
    return 1;
  } else if (factorialCache[n]) {
    return factorialCache[n];
  }
  x = factorial(n - 1) * n;
  factorialCache[n] = x;
  return x;
}

En un script de Mod, los valores almacenados en caché pueden guardarse en el estado, que se mantiene entre sesiones. El estado es compartido por todos los scripts del juego, por lo que debe mantener la caché de tamaño reducido.

Semáforo asíncrono

Un semáforo asíncrono le permite ejecutar un método de devolución de llamada una vez que se haya completado un conjunto de operaciones asíncronas (como las llamadas a `sendChat`). Aunque no se puede garantizar el orden en el que se completarán las operaciones, sí se puede garantizar que todas ellas se habrán completado cuando se active la llamada de retorno del semáforo.

Cuando utilice un semáforo, llame a v() antes de llamar a cada operación asíncrona, y llame a p() como última instrucción de cada operación asíncrona. Si se conoce de antemano el número de operaciones que va a realizar, también puede indicar dicho número al constructor del semáforo y omitir las llamadas a v().

Esta implementación concreta de un semáforo asíncrono también le permite proporcionar un contexto para la llamada de retorno (establezca el valor de «this»), así como pasar parámetros a dicha llamada de retorno. Los parámetros pueden indicarse bien en el constructor, bien en la llamada a p. (Los parámetros de p tienen prioridad sobre los del constructor.)

función 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) {
      // permite que sem.p(arg1, arg2, ...) anule los argumentos pasados al constructor de Semaphore
      if (arguments.length > 0) { parameters = arguments; }
      else { parameters = this.args; }
      this.callback.apply(this.context, parameters);
    }
  }
};

Ejemplo de uso:

var sem = new Semaphore(function(lastAsync) {
  log(lastAsync + ' se completó en último lugar');
  log(this);
}, 2, { foo: 'bar', fizz: 'buzz' }, 'Sir no aparece en esta llamada de retorno');
sendChat('', '/roll d20', function(ops) {
  log('Ejecutando la primera llamada a sendChat');
  sem.p('Primera llamada a sendChat');
});
sendChat('', '/roll d20', function(ops) {
  log('Ejecutando el segundo sendChat');
  sem.p('Segunda llamada a sendChat');
});

Ejemplo de resultado:

«Ejecutando la segunda llamada a sendChat»
«Ejecutando la primera llamada a sendChat»
«La primera llamada a sendChat se completó en último lugar»
{ foo: «bar», fizz: «buzz» }

Fichas informativas & Personajes

Cómo elaborar un folleto

Debido a la forma en que se gestionan los bloques de texto de los folletos, la creación de un objeto de folleto debe realizarse en dos pasos: primero, cree el objeto; a continuación, configure los bloques de texto:

//Crear un nuevo folleto disponible para todos los jugadores
var handout = createObj("handout", {
  name: "El nombre del folleto",
  inplayerjournals: "all",
  archived: false
});
handout.set('notes', 'Las notas deben configurarse una vez creado el folleto.');
handout.set('gmnotes', 'Las notas del director de juego también deben configurarse una vez creado el folleto.');

Gestión de la codificación

Los bloques de texto de «Fichas» (Notas y Notas del DJ) y «Personajes» (Biografía y Notas del DJ) que se configuran a través de la interfaz de usuario se almacenan en formato x-www-form-urlencoded. Podrá reconocerlo por la secuencia de códigos %## que aparecen a lo largo del texto:

«Erik%20%28Vikingo%2BCientífico%29%20%5BLuchador%3A%203%2C%20Mago%3A%202%5D»

Este texto se puede enviar al chat y el navegador lo traducirá, pero si necesita realizar cambios en el texto, quizá le convenga trabajar con él tal y como se ha introducido:

«Erik (vikingo y científico) [Luchador: 3, Mago: 2]»

Puede descodificar el texto codificado con la siguiente función:

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

Funciones de utilidad

Las funciones de utilidad realizan tareas habituales que puede que desee utilizar en muchos scripts. Una función situada en el ámbito más externo de una pestaña de script es visible para todos los scripts del juego, ya que comparten un único ámbito global. Dos scripts que tengan el mismo nombre se sobrescribirán entre sí. A continuación se muestra una selección de dichas funciones.

decodificarTextoEditor

Dependencias: Ninguna

El editor de texto integrado en el juego es bastante bueno, pero presenta un problema para los scripts de mods que dependen de la lectura de información procedente de una de las áreas de texto extensas del conjunto de datos. Esta función ayuda con eso.

A partir del texto de la propiedad «gmnotes» de un elemento gráfico, de la propiedad «bio» o «gmnotes» de un personaje, o de la propiedad «notes» o «gmnotes» de un documento de referencia, esta función devolverá una versión en la que se haya eliminado el formato de editor insertado automáticamente.

const decodeEditorText = (t, o) => {
  let w = t;
  o = Object.assign({ separator: '\r\n', asArray: false }, o);
  /* Notas de GM sobre tokens */
  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 ? líneas: líneas.join(o.separator);
  }
  /* tampoco */
  return t;
 } ;

El primer argumento es el texto que se va a procesar.

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

De forma predeterminada, las líneas de texto quedarán separadas por \r\n.

El segundo argumento opcional es un objeto con opciones.

  • separador: especifica con qué se deben separar las líneas de texto. Valor por defecto: \r\n
const text = decodeEditorText(token.get('gmnotes'),{separator:'<BR>'});
  • asArray: indica que, en su lugar, se devuelvan las líneas como una matriz. Valor predeterminado: false
const text = decodeEditorText(token.get('gmnotes'),{asArray:true});

Nota: No se gestionan las etiquetas anidadas de tipo «<» y «> ». La ficha del personaje, la biografía, las notas y las notas del director de juego son fragmentos de código HTML (léalas mediante una función de devolución de llamada). «Graphic gmnotes» es una cadena sincrónica y es el campo al que se refiere esta comprobación de%3Cp%3E.

obtenerImágenesLimpias

Dependencias: Ninguna

A partir de una URL de imagen procedente de un token u otro recurso, obtenga una versión limpia de la misma que pueda utilizarse para crear un token mediante un script de mod, o bien «undefined» si no es posible crearla mediante 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;
};

Nota: Los scripts de mod solo pueden crear imágenes a partir de la biblioteca de un usuario. Ya no es necesario indicar el tamaño del pulgar. Es posible que get("imgsrc") no coincida con la URL que ha introducido, ya que las URL de almacenamiento se reescriben. Consulte «Objetos»: restricciones de imgsrc.

getSenderForName

Dependencias: Ninguna

Dado un nombre en forma de cadena, esta función devolverá una cadena adecuada para el primer parámetro de sendChat. Si hay un personaje que comparte nombre con un jugador, se utilizará el jugador. También puede pasar un objeto de opciones, cuya estructura es idéntica a la del parámetro «options» de la función ` findObjs`.

función getSenderForName(nombre, opciones) {
  var personaje = findObjs({
    tipo: 'personaje',
    nombre: nombre
  }, opciones)[0],
  jugador = findObjs({
    tipo: 'jugador',
    nombre de usuario: nombre.endsWith(' (GM)') ? name.slice(0, -5) : name
  }, options)[0];
  if (player) {
    return 'player|' + player.id;
  }
  if (character) {
    return 'character|' + character.id;
  }
  return name;
}

obtenerDestinoSusurro

Dependencias: levenshteinDistance

A partir de un conjunto de opciones, esta función intenta construir la parte del nombre «/w» de un mensaje privado para una llamada a la función sendChat. El parámetro «options» debe contener «player: true» o «character: true», así como un valor para «id » o «name». Se prefiere a los jugadores antes que a los personajes si ambos son verdaderos, y se prefiere a los identificadores antes que a los nombres si ambos tienen un valor válido. Si se proporciona un nombre, el jugador o personaje cuyo nombre sea más parecido a la cadena proporcionada recibirá el susurro.

El parámetro «options» es técnicamente opcional, pero si lo omite (o no proporciona una combinación de jugador/personaje + id/nombre), la función devolverá una cadena vacía.

función 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) {
    // Ordene todos los jugadores o personajes (según corresponda) cuyo nombre *contenga* el nombre proporcionado,
    // y, a continuación, ordénelos según su similitud con el nombre proporcionado.
    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 '';
}

procesoEnrolladores

Esta función analizará el contenido de «msg.content» y sustituirá las tiradas en línea por su resultado total. Esto resulta especialmente útil para los comandos de Mod Script a los que el usuario desee pasar tiradas en línea como parámetros.

función processInlinerolls(msg) {
  if (_.has(msg, 'inlinerolls')) {
    return _.chain(msg.inlinerolls)
      .reduce(función(anterior, actual, índice) {
        anterior['$[[' + índice + ']]'] = actual.results.total || 0;
        return previous;
      },{})
      .reduce(function(previous, current, index) {
        return previous.split(index).join(String(current));
      }, msg.content)
      .value();
  } else {
    return msg.content;
  }
}

Aquí hay una versión un poco más complicada que también se encarga de convertir los elementos de la tabla en texto:

función 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;
  }
}

marcadores de estado a objeto

Es el inverso de `objectToStatusmarkers`; transforma una cadena apta para utilizarse como valor de la propiedad `statusmarkers` de un objeto token de Roll20 en un objeto JavaScript clásico.

Tenga en cuenta que una cadena de marcadores de estado puede contener marcadores de estado duplicados, mientras que un objeto no puede contener propiedades duplicadas.

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

objetoParaMarcadoresDeEstado

Es la inversa de la función «statusmarkersToObject»; transforma un objeto JavaScript convencional en una cadena delimitada por comas, adecuada para utilizarse como valor de la propiedad «statusmarkers» de un objeto «token» de Roll20.

Tenga en cuenta que una cadena de marcadores de estado puede contener marcadores de estado duplicados, mientras que un objeto no puede contener propiedades duplicadas.

función 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

La página web de Underscore.js es más una referencia que una guía para el uso de la biblioteca. Aunque resulta útil para consultar qué funciones hay disponibles y qué parámetros admiten, no sirve de ayuda a quien intente iniciarse en el uso de todas las posibilidades que ofrece la biblioteca.

Colecciones

La creación de scripts suele consistir en realizar una acción sobre un conjunto de elementos. Las colecciones pueden ser matrices, como var foo = [0, 1, 10, «banana»];, u objetos, como var bar = { one: 1, two: 2, banana: «fruit» };. Las matrices se indexan mediante números, que suelen comenzar por 0. Los objetos se indexan por los nombres de sus propiedades: bar["banana"] === "fruta". Los objetos se comportan como los tableros asociativos de otros lenguajes.

Datos de muestra

// Ejemplo de matriz:
var foo = [0, 1, 10, "banana"];
// Ejemplo de objeto
var bar = { one: 1, two: 2, banana: 'fruit' };

Llamar a una función con cada elemento [ _.each() ]

Es muy habitual tener que realizar alguna operación con cada elemento de una colección. Por lo general, la gente suele utilizar bucles «for» o similares. Underscore ofrece _.each(), una forma de llamar a una función pasando como argumento cada elemento de una colección.

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

Lo que hace que esto sea tan potente es que el código idéntico funciona independientemente de si utiliza una matriz u objeto:

_.each(bar, function(element){
  log('element is '+element);
});
«elemento es 1»
«elemento es 2»
«elemento es fruta»

Las funciones no necesitan estar en línea. También reciben parámetros adicionales. (Consulte la documentación para obtener más parámetros.):

var logKeyValueMapping = function(value, key) {
  log(key + " :: " + value);
};
log("Un array:");
_.each(foo, logKeyValueMapping);
log("Un objeto:");
_.each(bar, logKeyValueMapping);
«Una matriz:»
«0 :: 0»
«1 :: 1»
«2 :: 10»
«3 :: banana»
«Un objeto:»
«one :: 1»
«two :: 2»
«banana :: fruta»

Transformando cada elemento [ _.map() ]

Lo siguiente más habitual que se hace con una colección es transformar todos los elementos que contiene en elementos de otro tipo. A menudo, se suele hacer esto creando otra colección y, a continuación, utilizando un bucle «for» para recorrer la primera colección, transformar el valor e insertarlo en el nuevo contenedor. Es una gran cantidad de código que se puede simplificar con el método _.map() de Underscore, una forma de aplicar una función a una colección de elementos y obtener una colección con los resultados. Si le parece similar a _.each(), es porque, de hecho, lo es; tiene la misma firma.

var res = _.map(foo, function(element){
  return 'element is '+element;
});
log(res);
"['el elemento es 0', 'el elemento es 1', 'el elemento es 10', 'el elemento es plátano']"

El valor devuelto por _.map() es siempre una matriz con los resultados (véase «Conversión de colecciones» más abajo para los objetos). Al igual que con _.each(), la función admite más argumentos y puede definirse por separado.

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

Conversión de colecciones [ _.reduce() ]

_.reduce() reduce una colección a un único valor mediante la llamada a una función con un acumulador y cada elemento. Consulte la documentación de Underscore para ver la firma completa y los ejemplos.

¿Fue útil este artículo?
Usuarios a los que les pareció útil: 17 de 20