Padrão do Módulo Revelador
OModule Patternemula o conceito de classes de outras linguagens, encapsulando membros privados e públicos num objeto. O Padrão do Módulo Revelador melhora o Padrão do Módulo tornando a sintaxe mais consistente.
var myRevealingModule = myRevealingModule || (function() {
var privateVar = 'Esta variável é privada',
publicVar = 'Esta variável é 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 variável é privada"
myRevealingModule.setFunc('Mas eu posso mudar o valor dela');
log(myRevealingModule.getFunc()); // "Mas eu posso mudar o valor dela"
log(myRevealingModule.myVar); // "Esta variável é pública"
myRevealingModule.myVar = 'Então eu posso mudar o quanto eu quiser';
log(myRevealingModule.myVar); // "Então eu posso mudar o quanto eu quiser"
Memorização
A memorização é uma técnica de otimização que armazena o resultado para uma determinada entrada, permitindo que a mesma saída seja produzida sem calculá-la duas vezes. Isso é especialmente útil em cálculos caros. Claro, se for raro que sua função receba a mesma entrada, a memorização terá utilidade limitada enquanto os requisitos de armazenamento continuarem crescendo.
var factorialCache = {};
function factorial(n) {
var x;
n = parseInt(n || 0);
if (n < 0) {
throw 'Factoriais de números negativos não estão bem definidos';
}
if (n === 0) {
return 1;
} else if (factorialCache[n]) {
return factorialCache[n];
}
x = factorial(n - 1) * n;
factorialCache[n] = x;
return x;
}
Num Mod (API) script do Roll20, os valores em cache podem ser potencialmente armazenados em state
, que persistirá entre as sessões de jogo. No entanto, se tiver um grande número de potenciais entradas, tenha em atenção que o Roll20 pode limitar a sua utilização do estado.
Semáforo assíncrono
Um semáforo assíncrono permite que você dispare um método de retorno de chamada após um conjunto de operações assíncronas (como chamadas para sendChat) ser concluído. Embora você não possa garantirem que ordemas operações serão concluídas, você pode garantir que todas elastenhamsido concluídas quando a chamada de retorno do semáforo for ativada.
Quando utilizar um semáforo, chamev()
antes de chamar cada operação assíncrona e chamep()
como última instrução de cada operação assíncrona. Se o número de operações que vai efetuar for conhecido antecipadamente, também pode fornecer esse número ao construtor do semáforo e omitir as chamadas parav
.
Esta implementação específica de um semáforo assíncrono também lhe permite fornecer um contexto para a chamada de retorno (defina o valor dethis
), bem como passar parâmetros para a chamada de retorno. Os parâmetros podem ser fornecidos no construtor ou na chamada parap
. (Os parâmetros emp
têm precedência sobre os parâmetros no construtor).
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);
}
}
};
Example of use:
var sem = new Semaphore(function(lastAsync) {
log(lastAsync + ' completed last');
log(this);
}, 2, { foo: 'bar', fizz: 'buzz' }, 'Sr. que não aparece nesta chamada de retorno');
sendChat('', '/roll d20', function(ops) {
log('Executando primeiro sendChat');
sem.p('Primeira chamada de sendChat');
});
sendChat('', '/roll d20', function(ops) {
log('Executando segundo sendChat');
sem.p('Segunda chamada de sendChat');
});
Exemplo de saída:
"Executando segundo sendChat"
"Executando primeiro sendChat"
"Primeira chamada de sendChat concluída"
{ foo: "bar", fizz: "buzz" }
Folhetos & Personagens
Criando um Folheto
Devido à forma como os blocos de texto do Folheto são tratados, a criação de um objeto de Folheto precisa ser feita em duas etapas: Primeiro crie o objeto e depois defina os blocos de texto:
//Criar um novo Handout disponível para todos os jogadores
var handout = createObj("handout", {
name: "O nome do handout",
inplayerjournals: "all",
archived: false
});
handout.set('notes', 'Notas precisam ser definidas após a criação do handout.');
handout.set('gmnotes', 'Notas do Mestre também precisam ser definidas após a criação do handout.');
Manipulação de Codificação
Os blocos de texto em Handouts (Notas e Notas do Mestre) e Personagens (Biografia e Notas do Mestre) que são definidos através da Interface do Usuário são armazenados no formato x-www-form-urlencoded
. Você pode reconhecer isso pela sequência de códigos %## ao longo do texto:
"Erik%20%28Viking%2BCientista%29%20%5BLutador%3A%203%2C%20Mago%3A%202%5D"
Esse texto pode ser enviado para o chat e será traduzido pelo navegador, mas se você precisar fazer alterações no texto, talvez queira lidar com ele como foi inserido:
Erik (Viking+Cientista) [Lutador: 3, Mago: 2]"
Você pode decodificar o texto codificado com a seguinte função:
var decodeUrlEncoding = function(t){
return t.replace(
/%([0-9A-Fa-f]{1,2})/g,
function(f,n){
return String.fromCharCode(parseInt(n,16));
}
);
}
Funções de Utilidade
Funções de utilidade completam tarefas comuns que você pode querer usar em muitos scripts. Se você colocar uma função no escopo mais externo de uma guia de script, essa função deve estar disponível para todos os seus scripts, reduzindo sua sobrecarga. Abaixo está uma seleção dessas funções.
decodeEditorText
Dependências: Nenhuma
O novo editor de texto no jogo é bastante agradável, mas traz um problema para os Scripts Mod (API) que dependem da leitura de informações de uma das grandes áreas de texto no conjunto de dados. Esta função ajuda com isso.
Dado o texto de uma propriedadeGraphic'sgmnotes
, ou de uma propriedadeCharacter'sbio
ougmnotes
, ou de uma propriedadeHandout'snotes
ougmnotes
, devolverá uma versão com a formatação do editor inserida automaticamente.
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;
};
O primeiro argumento é o texto a ser processado.
const texto = decodeEditorText(token.get('gmnotes'));
Por padrão, as linhas de texto serão separadas por\r\n
.
O segundo argumento opcional é um objeto com opções.
separador
-- especifica com o que separar as linhas de texto. Padrão: \r\n
const text = decodeEditorText(token.get('gmnotes'),{separator:'<BR>'});
asArray
-- especifica retornar as linhas como um array. Padrão: false
const text = decodeEditorText(token.get('gmnotes'),{asArray:true});
NOTA: Tags Aninhadas <p>
confundirão e quebrarão a decodificação. Se você encontrar esse problema e precisar de ajuda, envie uma mensagem paraThe Aaron e ele ficará feliz em analisar isso.
getCleanImgsrc
Dependências: Nenhum
Dada uma URL de imagem tirada de um token ou outro recurso, obtenha uma versão limpa dela que possa ser usada para criar um token via API, ou indefinido
se não puder ser criada pela 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;
};
Observação: A API só é capaz de criar imagens cuja fonte está localizada em uma biblioteca do usuário. A imgsrc
também deve ser a versão thumb da imagem.
getSenderForName
Dependências: Nenhuma
Dado um nome de string, esta função retornará uma string apropriada para o primeiro parâmetro de sendChat. Se houver um personagem que compartilhe um nome com um jogador, o jogador será usado. Também pode passar um objetooptions
, que é estruturado de forma idêntica ao parâmetrooptions
de findObjs.
function 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): nome
}, opções)[0];
if (jogador) {
return 'jogador|' + jogador.id;
}
if (personagem) {
return 'personagem|' + personagem.id;
}
return nome;
}
getWhisperTarget
Dependências:levenshteinDistance
Dado um conjunto de opções, esta função tenta construir a parte "/w nome " de um sussurro para uma chamada para sendChat. O parâmetro opções
deve conter ou jogador: true
ou personagem: true
e um valor para ou id
ou nome
. Os jogadores são preferidos em relação aos personagens se ambos forem verdadeiros, e os IDs são preferidos em relação aos nomes se ambos tiverem um valor válido. Se um nome for fornecido, o jogador ou personagem com o nome mais próximo à string fornecida receberá o sussurro.
opções
é tecnicamente opcional, mas se você omitir (ou não fornecer uma combinação de jogador/personagem + id/nome), a função retornará uma string vazia.
função getWhisperTarget(opções) {
var nameProperty, targets, type;
opções = opções || {};
if (opções.jogador) {
nameProperty = 'displayname';
type = 'jogador';
} else if (opções.personagem) {
nameProperty = 'name';
type = 'personagem';
} else {
return '';
}
if (opções.id) {
targets = [getObj(type, opções.id)];
if (targets[0]) {
return '/w ' + targets[0].get(nameProperty).split(' ')[0] + ' ';
}
}
if (opções.nome) {
// Classifique todos os jogadores ou personagens (conforme apropriado) cujo nome *contém* o nome fornecido,
// então classifique-os por proximidade com o nome fornecido.
alvos = _.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 (alvos[0]) {
return '/w ' + alvos[0].get(nameProperty).split(' ')[0] + ' ';
}
}
return '';
}
processarInlinerolls
Esta função irá examinar o msg.content
e substituir as rolagens incorporadas pelo seu resultado total. Isto é particularmente útil para comandos Mod (API) aos quais o utilizador possa querer passar rolagens incorporadas como parâmetros.
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;
}
}
Aqui está uma versão um pouco mais complicada que também lida com a conversão de tableItems para seu 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
O inverso de objectToStatusmarkers; transforma uma cadeia de caracteres adequada para utilização como o valor da propriedadestatusmarkers
de um objeto token Roll20 num objeto JavaScript antigo.
Observe que uma string de statusmarker pode conter statusmarkers duplicados, enquanto um objeto não pode conter propriedades duplicadas.
função 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
O inverso de statusmarkersToObject; transforma um objeto JavaScript simples e antigo numa cadeia delimitada por vírgulas adequada para utilização como o valor da propriedadestatusmarkers
de um objeto token Roll20.
Observe que uma string de statusmarker pode conter statusmarkers duplicados, enquanto um objeto não pode conter propriedades duplicadas.
função objectToStatusmarkers(obj) {
return _.map(obj, function(value, key) {
return key === 'morto' || value < 1 || value > 9 ? chave : chave + '@' + parseInt(valor);
})
.join(',');
}
Underscore.js
O siteUnderscore.jsé mais uma referência da API do que um guia para usar a biblioteca. Embora útil para procurar quais funções estão disponíveis e quais parâmetros elas aceitam, não ajuda alguém que está tentando usar todo o poder da biblioteca.
Coleções
Escrever scripts envolve frequentemente fazeralgoa uma coleção decoisas. Quando falamos de colecções, estas podem ser arrays:var foo = [0,1,10, "banana"];
ou objectos:var bar = { one: 1, two: 2, banana: 'fruit' };
. Os arrays são indexados por números (normalmente a partir de 0 e contando para cima), os objectos têm propriedades que podem ser usadas para índices:bar['banana'] === 'fruit'; // true!
. Objetos agem efetivamente como arrays associativos de outras linguagens.
Array de exemplo:
var foo = [0,1,10,"banana"];
// Objeto de exemplo
var bar = { one: 1, two: 2, banana: 'fruit' };
Chamando uma função com cada elemento [ _.each() ]
É muito comum precisar realizar alguma operação com cada elemento de uma coleção. Normalmente as pessoas usarão para
loops ou similares. Underscore fornece _.each(), uma forma de chamar uma função com cada elemento de uma coleção como argumento.
_.each(foo, function(element){
log('elemento é '+element);
"elemento é 0"
"elemento é 1"
"elemento é 10"
"elemento é banana"
O que torna isso tão poderoso é que o código idêntico funciona independentemente se você estiver usando um array ou objeto:
_.each(bar, function(element){
log('elemento é '+element);
"elemento é 1"
"elemento é 2"
"elemento é fruta"
As funções não precisam ser inline. Elas também recebem parâmetros adicionais. (Veja a documentação para obter ainda mais parâmetros.):
var logKeyValueMapping = function( valor, chave ) {
log(chave + " :: " + valor);
};
log("Um Array:");
_.each(foo, logKeyValueMapping);
log("Um Objeto:");
_.each(bar, logKeyValueMapping);
"Um Array:"
"0 :: 0"
"1 :: 1"
"2 :: 10"
"3 :: banana"
"Um Objeto:"
"one :: 1"
"two :: 2"
"banana :: fruta"
Transformando cada elemento [ _.map() ]
A próxima coisa mais comum a se fazer com uma coleção é transformar todos os itens contidos em itens de outro tipo. Muitas vezes as pessoas podem fazer isso criando outra coleção e, em seguida, usando um loop depara
para percorrer a primeira coleção, transformando o valor e adicionando-o ao novo recipiente. Isso é muito código que pode ser simplificado com o _.map()do Underscore, uma maneira de aplicar uma função a uma coleção de elementos e obter uma coleção de resultados. Se isso parece semelhante ao _.each(), é porque é, na verdade, tem a mesma assinatura.
var res = _.map(foo, function(element){
return 'element is '+element;
});
log(res);
['element is 0','element is 1','element is 10','element is banana']
O retorno de _.map() é sempre uma matriz dos resultados (veja Converting Collections abaixo para obter objetos) e, assim como _.each(), a função recebe mais argumentos e pode ser definida separadamente.
[var getKeyValueMapping = function(value, key) {
return key + " :: " + value;
};
log("Um Array:");
var resA = _.map(foo, getKeyValueMapping);
log(resA);
log("Um Objeto:");
var resB_.map(bar, getKeyValueMapping);
log(resB);
"Um Array:"
"['0 :: 0', '1 :: 1', '2 :: 10', '3 :: banana']"
"Um Objeto:"
"['one :: 1', 'two :: 2', 'banana :: fruit']"