Patrón de Módulo Revelador
ElModule Patternemula el concepto de clases de otros lenguajes al encapsular miembros públicos y privados dentro de un objeto. El Patrón de Módulo Revelador mejora el Patrón de Módulo al hacer que la sintaxis sea más consistente.
var miModuloRevelador = miModuloRevelador || (function() {
var privateVar = 'Esta variable es privada',
publicVar = 'Esta variable es pública';
function privateFunction() {
log(privateVar);
}
function publicSet(texto) {
privateVar = texto;
}
función publicGet() {
función privada();
}
retorno {
setFunc: publicSet,
myVar: publicVar,
getFunc: publicGet
};
}());
registro(myRevealingModule.getFunc()); // "Esta variable es privada"
myRevealingModule.setFunc('Pero puedo cambiar su valor');
registro(myRevealingModule.getFunc()); // "Pero puedo cambiar su valor"
log(myRevealingModule.myVar); // "Esta variable es pública"
myRevealingModule.myVar = 'Así puedo cambiarla todo lo que quiera';
log(myRevealingModule.myVar); // "Así puedo cambiarlo todo lo que quiera"
Memoización
La memoización es una técnica de optimización que almacena el resultado de una entrada determinada, permitiendo que se pueda producir la misma salida sin calcularla dos veces. Esto es especialmente útil en cálculos costosos. Por supuesto, si es raro que tu función recibirá la misma entrada, la memoización tendrá una utilidad limitada mientras que los requisitos de almacenamiento continúen creciendo.
var factorialCache = {};
function factorial(n) {
var x;
n = parseInt(n || 0);
if (n < 0) {
throw '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 (API) de Roll20, los valores almacenados en caché podrían ser almacenados potencialmente en state
, que persistirá entre sesiones de juego. Sin embargo, si tiene una gran cantidad de entradas potenciales, tenga en cuenta que Roll20 puede limitar el uso del estado.
Semáforo asíncrono
Un semáforo asíncrono te permite activar un método de devolución de llamada después de que un conjunto de operaciones asíncronas (como llamadas a sendChat) se haya completado. Si bien no puede garantizarenorden se completarán las operaciones, puede garantizar que todasse hayan completadocuando se active la devolución de llamada del semáforo.
Cuando utilice un semáforo, llame av()
antes de llamar a cada operación asincrónica y llame ap()
como última declaración de cada operación asincrónica. Si conoce de antemano el número de operaciones que va a realizar, también puede proporcionar ese número al constructor del semáforo y omitir las llamadas av
.
Esta implementación particular de un semáforo asincrónico también le permite proporcionar un contexto para la devolución de llamada (establezca el valor dea
), así como pasar parámetros a la devolución de llamada. Los parámetros se pueden dar en el constructor o en la llamada ap
. (Los parámetros enp
tienen prioridad sobre los parámetros en el constructor).
function Semaphore(callback, initial, context) {
var args = (arguments.length === 1 ? [arguments[0]] : Array.apply(null, arguments));
this.lock = parseInt(inicial, 10) || 0;
this.callback = devolución de llamada;
this.context = contexto || devolución de llamada;
this.args = args.slice(3);
}
Protocolo.prototype = {
v: function() { this.lock++; },
p: function() {
var parámetros;
this.lock--;
if (this.lock === 0 && this.callback) {
// permitir que sem.p(arg1, arg2, ...) anule los args pasados al constructor de Protocolo
if (arguments.length > 0) { parámetros = argumentos; }
else { parámetros = this.args; }
this.callback.apply(this.context, parámetros);
}
}
};
Ejemplo de uso:
var sem = new Semaphore(function(lastAsync) {
log(lastAsync + ' completado último');
log(this);
}, 2, { foo: 'bar', fizz: 'buzz' }, 'Señor que no aparece en esta devolución de llamada');
sendChat('', '/roll d20', function(ops) {
log('Ejecutando el primer sendChat');
sem.p('Llamada del primer sendChat');
});
sendChat('', '/roll d20', function(ops) {
log('Ejecutando el segundo sendChat');
sem.p('Llamada del segundo sendChat');
});
Ejemplo de salida:
"Ejecutando el segundo sendChat"
"Ejecutando el primer sendChat"
"Llamada del primer sendChat completado último"
{ foo: "bar", fizz: "buzz" }
Documentos de consulta & Personajes
Creando un documento de consulta
Debido a la forma en que se manejan los bloques de texto en los documentos de consulta, la creación de un objeto de documento de consulta debe hacerse en dos pasos: primero crear el objeto, luego establecer los bloques de texto:
//Crear un nuevo Handout disponible para todos los jugadores
var handout = createObj("handout", {
name: "El nombre del handout",
inplayerjournals: "todos",
archived: false
});
handout.set('notes', 'Las notas deben ser establecidas después de que el handout sea creado.');
handout.set('gmnotes', 'Las notas del GM también deben ser establecidas después de que el handout sea creado.');
Manejo de la Codificación
Los bloques de texto en los Handouts (Notas y Notas del GM) y en los Personajes (Biografía y Notas del GM) que son establecidos a través de la Interfaz de Usuario se guardan en formato x-www-form-urlencoded
. Puedes reconocer esto por la secuencia de códigos %## a través 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 será traducido por el navegador, pero si necesitas hacer cambios al texto, es posible que quieras tratarlo tal como se ingresó:
Erik (Viking+Científico) [Luchador: 3, Mago: 2]"
Puedes decodificar 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 completan tareas comunes que es posible que desees utilizar en muchos scripts. Si colocas una función en el ámbito externo de una pestaña de script, esa función debería estar disponible para todos tus scripts, reduciendo tus gastos generales. A continuación se muestra una selección de tales funciones.
decodeEditorText
Dependencias: Ninguna
El nuevo editor de texto en el juego es bastante bueno, pero presenta un problema para los Scripts de Mod (API) que dependen de la lectura de información de uno de los grandes bloques de texto en el conjunto de datos. Esta función ayuda con eso.
Dado el texto de una propiedad 'Graphic' 's' 'gmnotes
o una propiedad 'Character' 's' 'bio' '
o 'gmnotes' '
o una propiedad 'Handout' 's' 'notes' '
o 'gmnotes' '
, esto devolverá una versión con el formato de edición automática eliminado.
const decodeEditorText = (t, o) =>{
let w = t;
o = Object.assign({ separador: '\r\n', asArray: false },o);
/* Notas del GM simbólico */
if(/^%3Cp%3E/.test(w)){
w = unescape(w);
}
if(/^<p>/.test(w)){
let líneas = w.match(/<p>.*?<\/p>/g)
.map( l => l.replace(/^<p>(.*?)<\/p>$/,'$1'));
retorno o.asArray? líneas: líneas.join(o.separador);
}
/* ninguno */
return t;
};
El primer argumento es el texto a procesar.
const text = decodeEditorText(token.get('gmnotes'));
Por defecto, 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. Por defecto: \r\n
const text = decodeEditorText(token.get('gmnotes'),{separator:'<BR>'});
comoArray
- especifica devolver las líneas como un array. Por defecto: falso
const text = decodeEditorText(token.get('gmnotes'),{asArray:true});
NOTA: Las etiquetas anidadas <p>
confundirán y romperán la decodificación. Si te encuentras con ese problema y necesitas ayuda, envía un mensaje privado aThe Aaron y él estará encantado de ayudarte.
getCleanImgsrc
Dependencias: Ninguna
Dado un URL de imagen tomado de un token u otro recurso, obtén una versión limpia de la misma que pueda ser utilizada para crear un token a través de la API, o indefinido
si no se puede crear por medio de 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 puede crear imágenes cuya fuente se encuentre en una biblioteca de usuario. El imgsrc
también debe ser la versión thumb de la imagen.
getRemitenteParaNombre
Dependencias:Ninguna
Dado un nombre de cadena, esta función devolverá una cadena apropiada 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 objetoopciones
, que está estructurado de manera idéntica al parámetroopciones
de findObjs.
función getSenderForName(nombre, opciones) {
var carácter = findObjs({
type: 'character',
name: name
}, opciones)[0],
jugador = findObjs({
tipo: 'jugador',
nombre para mostrar: nombre.lastIndexOf(' (GM)') == = nombre.longitud - 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. Theoptions
parameter should contain eitherplayer: true
orcharacter: true
and a value for eitherid
orname
. Players are preferred over characters if both are true, and ids are preferred over names if both have a valid value. Si se proporciona un nombre, el jugador o personaje con el nombre más cercano a la cadena proporcionada recibirá el susurro.
opciones
es técnicamente opcional, pero si lo omites (o no proporcionas una combinación de jugador/personaje + id/nombre), la función devolverá una cadena vacía.
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) {
// Ordenar todos los jugadores o personajes (según corresponda) cuyo nombre *contenga* el nombre proporcionado,
// luego ordenarlos por qué tan cerca están del nombre proporcionado.
objetivos = _.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 (objetivos[0]) {
return '/w ' + objetivos[0].get(nameProperty).split(' ')[0] + ' ';
}
}
return '';
}
processInlinerolls
Esta función examinará msg.content
y reemplazará las tiradas en línea por su resultado total. Esto es particularmente útil para comandos de Mod (API) a los que el usuario pueda querer pasar tiradas en línea como parámetros.
función procesoInlinerolls(msg) {
if (_.has(msg, 'inlinerolls')) {
return _.chain(msg.inlinerolls)
.reduce(function(anterior, actual, índice) {
anterior['$[ [' + índice + ']]'] = actual.resultados.total || 0;
retorno anterior;
},{})
.reduce(función(anterior, actual, índice) {
retorno anterior.reemplazar(índice , actual);
}, msg.content)
.value();
} else {
return msg.content;
}
}
Aquí hay una versión ligeramente más complicada que también maneja la conversión de tableItems a su texto:
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
Lo inverso de objectToStatusmarkers; transforma una cadena adecuada para usar como valor de la propiedadstatusmarkers
de un objeto token Roll20 en un objeto JavaScript antiguo y simple.
Ten en cuenta que una cadena de statusmarker puede contener statusmarkers duplicados, mientras que un objeto no puede contener propiedades duplicadas.
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
Lo inverso de statusmarkersToObject; transforma un antiguo objeto JavaScript en una cadena delimitada por comas adecuada para usar como valor de la propiedadstatusmarkers
de un objeto token Roll20.
Ten en cuenta que una cadena de statusmarker puede contener statusmarkers duplicados, mientras que un objeto no puede contener propiedades duplicadas.
function objectToStatusmarkers(obj) {
return _.map(obj, function(value, key) {
return key === 'dead' || value < 1 || value > 9 ? key : key + '@' + parseInt(value);
})
.join(',');
}
Underscore.js
El sitio webunderscore.jsAPI es más un referencia que una guía para usar la biblioteca. Si bien es útil para buscar qué funciones están disponibles y qué parámetros aceptan, no ayuda a alguien que intenta aprovechar al máximo el poder de la biblioteca.
Colecciones
Escribir scripts a menudo implica haceralgoa una colección decosas. Cuando hablamos de colecciones, pueden ser matrices:var foo = [0,1,10,"banana"];
o objetos:var bar = { one: 1, two: 2, banana: 'fruit' };
. Las matrices se indexan por números (generalmente comenzando desde 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' };
Llamando a una función con cada elemento [ _.each() ]
Es muy común necesitar realizar alguna operación con cada elemento de una colección. Por lo general, las personas usaránpara
bucles 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(elemento){
registro('el elemento es '+elemento);
"el elemento es 0"
"el elemento es 1"
"el elemento es 10"
"el elemento es banana"
Lo que hace esto tan poderoso es que el código idéntico funciona independientemente de si estás usando una matriz u objeto:
_.each(bar, function(elemento){
registro('el elemento es '+elemento);
"elemento es 1"
"elemento es 2"
"elemento es fruta"
Las funciones no necesitan estar en línea. También reciben parámetros adicionales. (Ver documentación para obtener aún 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 :: plátano"
"Un objeto:"
"uno :: 1"
"dos :: 2"
"plátano :: fruta"
Transformación de cada elemento [_.map()]
Lo siguiente más común que se hace con una colección es transformar todos los elementos contenidos en elementos de otro tipo. A menudo la gente puede hacer esto creando otra colección, luego usando un buclepara
recorriendo la primera colección, transformando el valor y empujándolo al nuevo contenedor. Es una gran cantidad de código que se puede simplificar con_.map()de Underscore, una forma de aplicar una función en una colección de elementos y obtener una colección de resultados. Si eso suena similar a _.each(), es porque lo es, de hecho, tiene la misma firma.
var res = _.map(foo, function(element){
return 'element is '+element;
});
log(res);
['element is 0','element is 1','element is 10','element is banana']
El resultado de _.map() siempre es un array de los resultados (ver Convirtiendo Colecciones más abajo para obtener objetos.) y al igual que _.each(), la función recibe más argumentos y puede ser definida por separado.
[var getKeyValueMapping = function( valor, clave ) {
return clave + " :: " + valor;
};
log("Una matriz:");
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 :: plátano']"
"Un objeto:"
"['uno :: 1', 'dos :: 2', 'plátano :: fruta']"