Revelando el patrón modular
El patrón de módulosemula el concepto de clases de otros lenguajes al encapsular miembros privados y públicos dentro de un objeto. El patrón de módulos revelador mejora el patrón de módulos al hacer que la sintaxis sea más coherente.
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() {
privateFunction();
}
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 todo lo que quiera';
log(myRevealingModule.myVar); // «Así que puedo cambiarla todo lo que quiera»
Memorización
La memorización es una técnica de optimización que almacena el resultado de una entrada determinada, lo que permite obtener el mismo resultado sin necesidad de volver a calcularlo. Esto resulta especialmente útil en cálculos costosos. Por supuesto, si es poco frecuente que su función reciba la misma entrada, la memoización tendrá una utilidad limitada, mientras que los requisitos de almacenamiento para ella seguirán aumentando.
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;
}
En un script Roll20 Mod (API), los valores almacenados en caché podrían guardarse en el estado, que se mantendrá entre sesiones de juego. Sin embargo, si tiene una gran cantidad de entradas potenciales, tenga en cuenta que Roll20 puede limitar su uso delestado.
Semáforo asíncrono
Un semáforo asíncrono le permite activar un método de devolución de llamada después de que se hayan completado un conjunto de operaciones asíncronas (como las llamadas a sendChat). Aunque no puede garantizarel orden en quese completarán las operaciones, sí puede garantizar que todas ellasse habráncompletado cuando se active la devolución de llamada del semáforo.
Cuando utilice un semáforo, llamea v()antes de llamar a cada operación asíncrona y llame ap()como última instrucción de cada operación asíncrona. Si el número de operaciones que va a realizar se conoce de antemano, también puede proporcionar ese número al constructor del semáforo y omitir las llamadas av.
Esta implementación concreta de un semáforo asíncrono también le permite proporcionar un contexto para la devolución de llamada (establecer el valor dethis), así como pasar parámetros a la devolución de llamada. Los parámetros se pueden proporcionar en el constructor o en la llamada ap. (Los parámetros enptienen prioridad sobre los parámetros en el constructor).
función Semáforo(callback, inicial, contexto) {
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) {
// permitir que sem.p(arg1, arg2, ...) anule los argumentos pasados al constructor 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 + ' 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('Primera llamada a sendChat');
});
sendChat('', '/roll d20', function(ops) {
log('Ejecutando segundo sendChat');
sem.p('Segunda llamada a sendChat');
});
Ejemplo de salida:
«Ejecutando segundo sendChat»
«Ejecutando primer sendChat»
«Primera llamada sendChat completada por último»
{ foo: «bar», fizz: «buzz» }
Folletos & Personajes
Creación de un folleto
Debido a la forma en que se gestionan los bloques de texto de los folletos, la creación de un objeto Folleto debe realizarse en dos pasos: primero cree el objeto y, 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 después de crear el folleto.');
handout.set('gmnotes', 'Las notas del GM también deben configurarse después de crear el folleto.');
Manejo de la codificación
Los bloques de texto de los Folletos (Notas y Notas del GM) y Personajes (Biografía y Notas del GM) que se configuran a través de la interfaz de usuario se almacenan en formatox-www-form-urlencoded. Puede reconocerlo por la secuencia de códigos %## a lo largo del texto:
«Erik%20%28Viking%2BCientífico%29%20%5BLuchador%3A%203%2C%20Mago%3A%202%5D»
Este texto se puede enviar al chat y será traducido por el navegador, pero si necesita realizar cambios en el texto, es posible que desee modificarlo tal y como se ha introducido:
Erik (vikingo + científico) [Luchador: 3, Mago: 2]»
Puede descodificar el texto codificado con la siguiente función:
var decodeUrlEncoding = function(t){
return t.replace(
/%([0-9A-Fa-f]{1,2})/g,
function(f,n){
return String.fromCharCode(parseInt(n,16));
}
);
}
Funciones de utilidad
Las funciones de utilidad realizan tareas comunes que es posible que desee utilizar en muchos scripts. Si coloca una función en el ámbito más externo de una pestaña de script, esa función debería estar disponible para todos sus scripts, lo que reduciría su sobrecarga. A continuación se muestra una selección de dichas funciones.
decodificarTextoEditor
Dependencias:Ninguna
El nuevo editor de texto del juego es bastante bueno, pero presenta un problema para los scripts Mod (API) que dependen de la lectura de información de una de las grandes áreas de texto del conjunto de datos. Esta función ayuda con eso.
Dado el texto de la propiedadgmnotesde un gráfico, o la propiedadbioogmnotesde un personaje, o la propiedadnotesogmnotesde un folleto, esto devolverá una versión sin el formato del editor insertado automáticamente.
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 ? líneas: líneas.join(o.separator);
}
/*ni uno ni otro*/
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 estarán separadas por \r\n.
El segundo argumento opcional es un objeto con opciones.
separador: especifica con qué separar las líneas de texto. Predeterminado: \r\n
const text = decodeEditorText(token.get('gmnotes'),{separator:'<BR>'});
asArray: especifica que, en su lugar, se devuelvan las líneas como una matriz. Predeterminado:falso
const text = decodeEditorText(token.get('gmnotes'),{asArray:true});
NOTA: Las etiquetas anidadas <p> confundirán y romperán la decodificación. Si se encuentra con ese problema y necesita ayuda, envíeun mensaje privado a Aarony él estará encantado de echarle un vistazo.
obtenerImágenesLimpias
Dependencias:Ninguna
Dada una URL de imagen tomada de un token u otro recurso, obtenga una versión limpia de la misma que pueda utilizarse para crear un token a través de la API, oindefinidasi no puede crearse mediante la 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;
};
Nota:La API solo es capaz de crear imágenes cuya fuente se encuentre en una biblioteca de usuarios. Elimgsrctambién debe ser la versiónen miniaturade la imagen.
getSenderForName
Dependencias:Ninguna
Dado un nombre 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 objetode opciones, cuya estructura es idéntica a la del parámetrode opcionesde findObjs.
función getSenderForName(nombre, opciones) {
var personaje = findObjs({
type: 'character',
name: name
}, opciones)[0],
jugador = findObjs({
tipo: 'jugador',
nombre de usuario: nombre.lastIndexOf(' (GM)') === nombre.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;
}
obtenerDestinoSusurro
Dependencias:levenshteinDistance
Dado un conjunto de opciones, esta función intenta construir la parte «/w nombre» de un susurro para una llamada a sendChat. El parámetrooptionsdebe contenerplayer: trueocharacter: truey un valor paraidoname. 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.
Las opcionesson técnicamente opcionales, pero si las omite (o no proporciona una combinación de jugador/personaje + id/nombre), la función devolverá una cadena vacía.
función getWhisperTarget(opciones) {
var nombrePropiedad, objetivos, tipo;
opciones = opciones || {};
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) {
// Ordenar todos los jugadores o personajes (según corresponda) cuyo nombre *contenga* el nombre proporcionado,
// y luego ordenarlos según su proximidad al 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 escanearámsg.content y sustituirá los rollos en línea por su resultado total. Esto resulta especialmente útil para los comandos Mod (API) a los que el usuario puede querer pasar tiradas en línea como parámetros.
función 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;
}
}
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.replace(k,v);
},msg.content)
.value();
} else {
return msg.content;
}
}
marcadores de estado a objeto
Lo contrario de objectToStatusmarkers; transforma una cadena adecuada para su uso como valor de la propiedadstatusmarkersde un objeto token Roll20 en un objeto JavaScript antiguo sin formato.
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
Lo contrario de statusmarkersToObject; transforma un objeto JavaScript antiguo en una cadena delimitada por comas adecuada para su uso como valor de la propiedadstatusmarkersde un objeto token 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 < 1 || value > 9 ? clave: clave + '@' + parseInt(valor);
})
.join(',');
}
Underscore.js
El sitio web de Underscore.js es más una referencia de API que una guía para utilizar la biblioteca. Aunque resulta útil para consultar qué funciones están disponibles y qué parámetros aceptan, no ayuda a quien intenta aprovechar al máximo el potencial de la biblioteca.
Colecciones
Escribir scripts a menudo implica haceralgocon un conjunto deelementos. Cuando hablamos de colecciones, pueden ser matrices:var foo = [0,1,10,"banana"];u objetos:var bar = { one: 1, two: 2, banana: 'fruit' };. Las matrices se indexan mediante números (normalmente empezando por 0 y contando hacia arriba), los objetos tienen propiedades que se pueden utilizar como índices:bar['banana'] === 'fruit'; // ¡verdadero!. Los objetos actúan efectivamente como matrices asociativas de otros lenguajes.
Matriz de ejemplo:
var foo = [0,1,10,"banana"];
// Objeto de ejemplo
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. Normalmente, la gente utiliza bucles«for»o similares. Underscore proporciona _.each(), una forma de llamar a una función con cada elemento de una colección como argumento.
_.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(valor, clave) {
log(clave + " :: " + valor);
};
log("Una matriz:");
_.each(foo, logKeyValueMapping);
log("Un objeto:");
_.each(bar, logKeyValueMapping);
«Una matriz:»
«0 :: 0»
«1 :: 1»
«2 :: 10»
«3 :: banana»
«Un objeto:»
«uno :: 1»
«dos :: 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, las personas pueden hacer esto creando otra colección y, a continuación, utilizando un bucle«for»para iterar a través de la primera colección, transformando el valor e introduciéndolo en el nuevo contenedor. Es mucho código que se puede simplificar con_.map() de Underscore, una forma de aplicar una función a una colección de elementos y obtener una colección de resultados. Si eso le suena similar a _.each(), es porque, de hecho, lo es, ya que tiene la misma firma.
var res = _.map(foo, function(element){
return 'element is '+element;
});
log(res);
['elemento es 0','elemento es 1','elemento es 10','elemento es plátano']
El resultado de _.map() es siempre una matriz con los resultados (consulte Conversión de colecciones más abajo para obtener objetos) y, al igual que _.each(), la función recibe más argumentos y se puede definir por separado.
[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);
«Una matriz:»
«['0 :: 0', '1 :: 1', '2 :: 10', '3 :: banana']»
«Un objeto:»
«['uno :: 1', 'dos :: 2', 'banana :: fruta']»