Os textos que se seguem não são guiões completos. Destinam-se a ser integrados com a lógica de negócio para auxiliar na criação de scripts completos, e não para criar scripts por si só.
Padrão de Módulo Revelador
O Padrão de Módulo emula o conceito de classes de outras linguagens de programação, encapsulando membros privados e públicos num objeto. O Padrão de Módulo Revelador aperfeiçoa o Padrão de 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() {
return privateVar;
}
return {
setFunc: publicSet,
myVar: publicVar,
getFunc: publicGet
};
}());
log(myRevealingModule.getFunc()); // «Esta variável é privada»
myRevealingModule.setFunc('Mas posso alterar o seu valor');
log(myRevealingModule.getFunc()); // «Mas posso alterar o seu valor»
log(myRevealingModule.myVar); // «Esta variável é pública»
myRevealingModule.myVar = 'Portanto, posso alterá-la como quiser';
log(myRevealingModule.myVar); // «Portanto, posso alterá-la como quiser»
Memoização
A memoização é uma técnica de otimização que armazena o resultado para uma determinada entrada, permitindo que se obtenha o mesmo resultado sem ser necessário calculá-lo duas vezes. Isto é especialmente útil em cálculos dispendiosos. É claro que, se for raro que a sua função receba a mesma entrada, a memoização terá uma utilidade limitada, ao mesmo tempo que as necessidades de armazenamento associadas a ela continuam a aumentar.
var factorialCache = {};
function factorial(n) {
var x;
n = parseInt(n || 0);
if (n < 0) {
throw 'Os fatoriais 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 script Mod, os valores armazenados em cache podem ser guardados no estado, que persiste entre sessões. O estado é partilhado por todos os scripts do jogo, pelo que deve manter a cache pequena.
Semáforo assíncrono
Um semáforo assíncrono permite-lhe acionar um método de callback após a conclusão de um conjunto de operações assíncronas (tais como chamadas ao método `sendChat`). Embora não seja possível garantir a ordem em que as operações serão concluídas, é possível garantir que todas elas já tenham sido concluídas quando a função de retorno do semáforo for acionada.
Ao utilizar um semáforo, chame a função v() antes de chamar cada operação assíncrona e chame a função p() como a última instrução de cada operação assíncrona. Se o número de operações que vai realizar for conhecido antecipadamente, também pode indicar esse número ao construtor do semáforo e omitir as chamadas à função v().
Esta implementação específica de um semáforo assíncrono também lhe permite definir um contexto para a função de retorno (definir o valor de «this»), bem como passar parâmetros para a função de retorno. Os parâmetros podem ser fornecidos quer no construtor, quer na chamada à função p. (Os parâmetros em p têm prioridade sobre os parâmetros no construtor.)
função 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, ...) substitua os argumentos passados ao construtor do Semaphore
if (arguments.length > 0) { parameters = arguments; }
else { parameters = this.args; }
this.callback.apply(this.context, parameters);
}
}
};
Exemplo de utilização:
var sem = new Semaphore(function(lastAsync) {
log(lastAsync + ' foi concluído em último lugar');
log(this);
}, 2, { foo: 'bar', fizz: 'buzz' }, 'O senhor não aparece neste callback');
sendChat('', '/roll d20', function(ops) {
log('A executar o primeiro sendChat');
sem.p('Primeira chamada ao sendChat');
});
sendChat('', '/roll d20', function(ops) {
log('A executar o segundo sendChat');
sem.p('Segunda chamada ao sendChat');
});
Exemplo de resultado:
"A executar o segundo sendChat"
"A executar o primeiro sendChat"
"A primeira chamada ao sendChat foi concluída em último lugar"
{ foo: "bar", fizz: "buzz" }
Folhetos & Personagens
Criação de um folheto informativo
Devido à forma como os blocos de texto do Handout são tratados, a criação de um objeto Handout deve ser realizada em duas etapas: primeiro, crie o objeto e, em seguida, defina os blocos de texto:
//Criar um novo folheto disponível para todos os jogadores
var handout = createObj("handout", {
name: "O nome do folheto",
inplayerjournals: "all",
archived: false
});
handout.set('notes', 'As notas têm de ser definidas após a criação do folheto.');
handout.set('gmnotes', 'As notas do GM também têm de ser definidas após a criação do folheto.');
Tratamento da codificação
Os blocos de texto nas secções «Folhetos» (Notas e Notas do Mestre) e «Personagens» (Biografia e Notas do Mestre), definidos através da Interface do Utilizador, são armazenados no formato x-www-form-urlencoded. Pode reconhecer isto pela sequência de códigos %## ao longo do texto:
"Erik%20%28Viking%2BCientista%29%20%5BLutador%3A%203%2C%20Feiticeiro%3A%202%5D"
Este texto pode ser enviado para o chat e será traduzido pelo navegador, mas se precisar de fazer alterações ao texto, talvez seja melhor tratá-lo tal como foi introduzido:
«Erik (Viking + Cientista) [Lutador: 3, Feiticeiro: 2]»
Pode descodificar o texto codificado com a seguinte função:
var decodeUrlEncoding = function(t) {
return decodeURIComponent(t.replace(/\+/g, " "));
}
Funções utilitárias
As funções utilitárias executam tarefas comuns que poderá querer utilizar em muitos scripts. Uma função no âmbito mais externo de um separador de script é visível para todos os scripts do jogo, uma vez que partilham um único âmbito global. Dois scripts que declarem o mesmo nome irão substituir-se mutuamente. Abaixo encontra-se uma seleção dessas funções.
descodificarTextoEditor
Dependências: Nenhuma
O editor de texto do jogo é bastante bom, mas apresenta um problema para os scripts de modificação que dependem da leitura de informações de uma das grandes áreas de texto do conjunto de dados. Esta função auxilia nesse processo.
A partir do texto da propriedade «gmnotes» de um «Graphic», ou da propriedade «bio» ou «gmnotes» de um «Character», ou ainda da propriedade «notes» ou «gmnotes» de um «Handout», esta função irá devolver uma versão sem a formatação do editor inserida automaticamente.
const decodeEditorText = (t, o) => {
let w = t;
o = Object.assign({ separator: '\r\n', asArray: false }, o);
/* Notas sobre o token GM */
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 ? linhas: linhas.join(o.separator);
}
/* nem uma nem outra */
return t;
};
O primeiro argumento é o texto a ser processado.
const texto = decodeEditorText(token.get('gmnotes'));
Por predefinição, as linhas de texto serão separadas por \r\n.
O segundo argumento opcional é um objeto com opções.
-
separador– especifica o símbolo a utilizar para separar as linhas de texto. Valor predefinido:\r\n
const text = decodeEditorText(token.get('gmnotes'),{separator:'<BR>'});
-
asArray– especifica que, em vez disso, as linhas devem ser devolvidas como um array. Valor predefinido:false
const text = decodeEditorText(token.get('gmnotes'),{asArray:true});
Nota: As etiquetas aninhadas «<» e «>» não são processadas. A biografia da personagem e o folheto informativo, bem como as notas e as notas do mestre, são blocos de HTML (leia-os através de um callback). O «gmnotes» gráfico é uma cadeia de caracteres síncrona e é o campo a que se refere esta verificação: %3Cp%3E.
obterImagensLimpas
Dependências: Nenhuma
Dado um URL de imagem obtido a partir de um token ou de outro recurso, obtenha uma versão limpa da mesma que possa ser utilizada para criar um token através de um Mod Script, ou o valor «undefined» caso não seja possível criá-lo através de um Mod Script.
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: Os scripts de modificação só podem criar imagens a partir da biblioteca de um utilizador. Já não é necessário indicar o tamanho do polegar. A função get("imgsrc") poderá não corresponder ao URL que indicou, uma vez que os URLs de armazenamento são reescritos. Consulte «Objetos: restrições do imgsrc ».
obterRemetenteParaNome
Dependências: Nenhuma
Dado um nome na forma de cadeia de caracteres, esta função irá devolver uma cadeia de caracteres adequada para o primeiro parâmetro da função sendChat. Caso exista um personagem com o mesmo nome de um jogador, o jogador será utilizado. Também pode passar um objeto de opções, cuja estrutura é idêntica à do parâmetro «options» da função «findObjs».
função getSenderForName(name, options) {
var character = findObjs({
type: 'character',
name: name
}, options)[0],
player = findObjs({
type: 'player',
displayname: name.endsWith(' (GM)') ? name.slice(0, -5) : name
}, options)[0];
if (player) {
return 'player|' + player.id;
}
if (character) {
return 'character|' + character.id;
}
return name;
}
obterDestinoSussurro
Dependências: levenshteinDistance
Dado um conjunto de opções, esta função tenta construir a parte «/w nome» de uma mensagem sussurrada para uma chamada à função sendChat. O parâmetro «options» deve conter «player: true» ou «character: true» e um valor para «id » ou «name». Os jogadores são preferidos aos personagens se ambos forem verdadeiros, e os IDs são preferidos aos nomes se ambos tiverem um valor válido. Caso um nome seja fornecido, o jogador ou personagem com o nome mais próximo da sequência de caracteres fornecida receberá a mensagem privada.
A opção «options» é, tecnicamente, opcional, mas se a omitir (ou não indicar uma combinação de jogador/personagem + id/nome), a função devolverá uma cadeia de caracteres vazia.
função 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) {
// Ordena todos os jogadores ou personagens (conforme o caso) cujo nome *contém* o nome fornecido,
// e, em seguida, ordena-os de acordo com a proximidade em relação ao nome fornecido.
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 '';
}
processInlinerolls
Esta função irá analisar o conteúdo de «msg.content» e substituir os resultados das jogadas inseridos no texto pelo seu resultado total. Isto é particularmente útil para comandos de scripts de modificação aos quais o utilizador possa querer passar resultados de lançamentos de dados em linha como parâmetros.
função processInlinerolls(msg) {
se (_.has(msg, 'inlinerolls')) {
devolva _.chain(msg.inlinerolls)
.reduce(função(anterior, atual, índice) {
anterior['$[[' + índice + ']]'] = atual.results.total || 0;
return previous;
},{})
.reduce(function(previous, current, index) {
return previous.split(index).join(String(current));
}, msg.content)
.value();
} else {
return msg.content;
}
}
Aqui está uma versão um pouco mais complexa que também lida com a conversão de tableItems para o seu texto:
função processInlinerolls(msg) {
se(_.has(msg, 'inlinerolls')){
devolva _.chain(msg.inlinerolls)
.reduce(função(m, v, k){
var ti = _.reduce(v.results.rolls, função(m2, v2){
if(_.has(v2, 'table')){
m2.push(_.reduce(v2.results, função(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 para objeto
O inverso de `objectToStatusmarkers`; transforma uma cadeia de caracteres adequada para ser utilizada como valor da propriedade ` statusmarkers ` de um objeto token do Roll20 num objeto JavaScript clássico.
Note-se que uma cadeia de caracteres de marcadores de estado pode conter marcadores de estado duplicados, enquanto um objeto não pode conter propriedades duplicadas.
função statusmarkersToObject(stats) {
return _.reduce(stats.split(/,/), função(memo, valor) {
var partes = valor.split(/@/),
num = parseInt(partes[1] || '0', 10);
if (parts[0].length) {
memo[parts[0]] = Math.max(num, memo[parts[0]] || 0);
}
return memo;
}, {});
}
objetoParaMarcadoresDeStatus
O inverso de `statusmarkersToObject`; transforma um objeto JavaScript simples num cadeia de caracteres delimitada por vírgulas, adequada para ser utilizada como valor da propriedade ` statusmarkers ` de um objeto token do Roll20.
Note-se que uma cadeia de caracteres de marcadores de estado pode conter marcadores de estado duplicados, enquanto um objeto não pode conter propriedades duplicadas.
função 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
O site do Underscore.js é mais uma referência do que um guia para a utilização da biblioteca. Embora seja útil para consultar quais as funções disponíveis e quais os parâmetros que estas aceitam, não ajuda quem está a tentar dar os primeiros passos para tirar o máximo partido da biblioteca.
Coleções
A criação de scripts envolve, muitas vezes, realizar uma ação sobre um conjunto de elementos. As coleções podem ser matrizes, var foo = [0, 1, 10, "banana"];, ou objetos, var bar = { one: 1, two: 2, banana: "fruit" };. Os tabuletos são indexados por números, começando normalmente em 0. Os objetos são indexados pelos nomes das propriedades: bar["banana"] === "fruta". Os objetos funcionam como matrizes associativas de outras linguagens de programação.
Dados de amostra
// Exemplo de matriz:
var foo = [0,1,10,"banana"];
// Exemplo de objeto
var bar = { one: 1, two: 2, banana: 'fruit' };
Chamando uma função com Each Element [ _.each() ]
É muito comum ser necessário realizar uma operação com cada elemento de uma coleção. Normalmente, as pessoas utilizam loops «for» ou algo semelhante. O Underscore disponibiliza o método _.each(), que permite chamar uma função utilizando cada elemento de uma coleção como argumento.
_.each(foo, function(element){
log('element is '+element);
});
"elemento é 0"
"elemento é 1"
"elemento é 10"
"elemento é banana"
O que torna isso tão poderoso é que o código idêntico funciona independentemente de você estar a utilizar uma matriz ou um objeto:
_.each(bar, function(element){
log('element is '+element);
});
"elemento é 1"
"elemento é 2"
"elemento é fruta"
As funções não precisam ser inline. Eles também recebem parâmetros adicionais. (Consulte a documentação para obter ainda mais parâmetros.):
var logKeyValueMapping = function( value, key ) {
log(key + " :: " + value);
};
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 segunda coisa mais comum a fazer com uma coleção é transformar todos os itens contidos nela em itens de outro tipo. Muitas vezes, as pessoas podem fazer isto criando outra coleção e, em seguida, utilizando um ciclo «for» para percorrer a primeira coleção, transformando o valor e inserindo-o no novo contentor. É uma grande quantidade de código que pode ser simplificada com o método _.map() do Underscore, uma forma de aplicar uma função a um conjunto de elementos e obter um conjunto com os resultados. Se isto lhe parecer semelhante a _.each(), é porque, de facto, é; tem, de facto, a mesma assinatura.
var res = _.map(foo, function(element){
return 'element is '+element;
});
log(res);
"['elemento é 0', 'elemento é 1', 'elemento é 10', 'elemento é banana']"
O valor devolvido por _.map() é sempre um conjunto dos resultados (consulte «Conversão de coleções» abaixo, no que diz respeito aos objetos). Tal como acontece com o _.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']»
Conversão de coleções [ _.reduce() ]
_.reduce() reduz uma coleção a um único valor, chamando uma função com um acumulador e cada elemento. Consulte a documentação do Underscore para obter a assinatura completa e exemplos.