Scripts Mod (API): Manual de Receitas

Os seguintes não são guiões completos. Eles devem ser combinados com a lógica de negócios para auxiliar na criação de scripts completos, e não para criar scripts por conta própria.


Revelando o Padrão de Módulo

O Padrão de Móduloemula o conceito de classes de outras linguagens, encapsulando membros privados e públicos dentro de um objeto. O Padrão de Módulo Revelador aprimora 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() {
        privateFunction();
    }

    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 conforme desejar';
log(myRevealingModule.myVar); // "Portanto, posso alterá-la conforme desejar"

Memorização

A memoização é uma técnica de otimização que armazena o resultado para uma determinada entrada, permitindo que a mesma saída seja produzida sem a necessidade de calculá-la duas vezes. Isto é particularmente útil em cálculos dispendiosos. É evidente que, se for raro que a sua função receba a mesma entrada, a memoização terá uma utilidade limitada, enquanto os requisitos de armazenamento para ela continuarem a aumentar.

var factorialCache = {};
function factorial(n) {
    var x;

    n = parseInt(n || 0);
    if (n < 0) {
        throw 'Fatoriais de números negativos não sã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 Roll20 Mod (API), os valores armazenados em cache podem ser potencialmente guardados no estado, que permanecerá entre as sessões do jogo. No entanto, caso tenha um grande número de entradas potenciais, esteja ciente de que o Roll20 pode limitar o uso doestado.


Semáforo assíncrono

Um semáforo assíncrono permite acionar um método de retorno de chamada após a conclusão de um conjunto de operações assíncronas (como chamadas para sendChat). Embora não seja possível garantira ordem em queas operações serão concluídas, é possível garantir que todas elasestarãoconcluídas quando a chamada de retorno do semáforo for acionada.

Ao utilizar um semáforo, executev()antes de iniciar cada operação assíncrona e executep()como a última instrução de cada operação assíncrona. Se o número de operações que você irá realizar for conhecido antecipadamente, você 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 permite fornecer um contexto para o callback (definir o valordeste), bem como passar parâmetros para o callback. Os parâmetros podem ser fornecidos no construtor ou na chamada parap. (Os parâmetros emptêm precedência sobre os parâmetros no construtor.)

função Semaphore(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, ...) substitua os argumentos passados para o construtor 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 + ' concluído por último');
    log(this);
}, 2, { foo: 'bar', fizz: 'buzz' }, 'Sir não aparece nesta chamada de retorno');

sendChat('', '/roll d20', function(ops) {
    log('Executando primeiro sendChat');
    sem.p('Primeira chamada sendChat');
});
sendChat('', '/roll d20', function(ops) {
    log('Executando segundo sendChat');
    sem.p('Segunda chamada sendChat');
});

Exemplo de resultado:

"Executando o segundo sendChat"
"Executando o primeiro sendChat"
"Primeira chamada sendChat concluída por último"
{ foo: "bar", fizz: "buzz" }

Folhetos & Personagens

Criando um folheto

Devido à forma como os blocos de texto 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:

//Crie 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 precisam ser definidas após a criação do folheto.');
    handout.set('gmnotes', 'As notas do GM também precisam ser definidas após a criação do material.');

Tratamento da codificação

Os blocos de texto em Handouts (Notas e Notas do GM) e Personagens (Biografia e Notas do GM) que são definidos através da Interface do Utilizador são armazenados no formatox-www-form-urlencoded. É possível 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"

Este texto pode ser enviado para o chat e será traduzido pelo navegador. No entanto, caso necessite fazer alterações no texto, é recomendável que o faça conforme foi inserido:

Erik (Viking + Cientista) [Lutador: 3, Mago: 2]

É possível descodificar 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 utilitárias

As funções utilitárias realizam tarefas comuns que podem ser necessárias em vários scripts. Se colocar uma função no âmbito mais externo de um separador de script, essa função deverá estar disponível para todos os seus scripts, reduzindo a sua sobrecarga. Abaixo encontra-se uma seleção dessas funções.

descodificarTextoEditor

Dependências:Nenhuma

O novo editor de texto do jogo é bastante agradável, mas apresenta 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 auxilia nesse processo.

Dado o texto da propriedadegmnotesde um gráfico, ou da propriedadebioougmnotesde um personagem, ou da propriedadenotesougmnotesde um folheto, isto retornará 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);
  /* 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 ? linhas: linhas.join(o.separator);
  }
  /*nem um nem outro*/
  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 o que deve separar as linhas de texto. Padrão: \r\n

const text = decodeEditorText(token.get('gmnotes'),{separator:'<BR>'});

 

asArray— especifica que as linhas devem ser retornadas como uma matriz. Padrão:falso

const text = decodeEditorText(token.get('gmnotes'),{asArray:true});

NOTA: Tags aninhadas<p>podem causar confusão e interromper a descodificação. Caso encontre esse problema e necessite de assistência, envie uma mensagem privadapara o Aaron, que ele terá todo o prazer em analisar a situação.


obterImagensLimpas

Dependências:Nenhuma

Dada uma URL de imagem obtida de um token ou outro recurso, obtenha uma versão limpa dela que possa ser utilizada para criar um token através da API, ouindefinidase 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 é capaz apenas de criar imagens cuja fonte esteja localizada numa biblioteca do utilizador. Oimgsrctambém deve ser a versãoem miniaturada imagem.


obterRemetenteParaNome

Dependências:Nenhuma

Dado um nome de cadeia de caracteres, esta função retornará uma cadeia de caracteres apropriada para o primeiro parâmetro de sendChat. Caso exista um personagem com o mesmo nome de um jogador, o jogador será utilizado. Também é possível passar um objetode opções, que é estruturado de forma idêntica ao parâmetrode opçõesdo findObjs.

função getSenderForName(nome, opções) {
    var personagem = findObjs({
            type: 'character',
            name: name
        }, opções)[0],
        jogador = findObjs({
            tipo: 'jogador',
            nome de exibição: nome.lastIndexOf(' (GM)') === nome.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;
}

obterDestinoSussurro

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 enviarChat. O parâmetrooptionsdeve conterplayer: trueoucharacter: truee um valor paraidouname. 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.

As opçõessão tecnicamente opcionais, mas se forem omitidas (ou se não for fornecida 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 (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) {
        // Classificar todos os jogadores ou personagens (conforme apropriado) cujo nome *contém* o nome fornecido,
        // em seguida, classificá-los por quão próximos estão do 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á analisarmsg.content e substituir os rolos inline pelo seu resultado total. Isto é particularmente útil para comandos Mod (API) aos quais o utilizador pode querer passar rolagens inline como parâmetros.

função processInlinerolls(msg) {
    se (_.has(msg, 'inlinerolls')) {
        retorne _.chain(msg.inlinerolls)
                .reduce(função(anterior, atual, índice) {
                    anterior['$[[' + índice + ']]'] = atual.resultados.total || 0;
                    retornar anterior;
                },{})
                .reduce(function(anterior, atual, índice) {
                    retornar anterior.substituir(índice, atual);
                }, msg.content)
                .value();
    } else {
        retornar 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) {
	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);
						retorne m3;
					},[]).join(', '));
				}
				retorne m2;
			},[]).join(', ');
			m['$[['+k+']]']= (ti.length && ti) || v.results.total || 0;
			retorne m;
		},{})
		.reduce(function(m,v,k){
			retorne m.replace(k,v);
		},msg.content)
		.value();
	} else {
		retorne msg.content;
	}
}
 

marcadores de estado para objeto

O inverso de objectToStatusmarkers; transforma uma string adequada para uso como valor da propriedadestatusmarkersde um objeto token Roll20 em um objeto JavaScript simples.

Observe que uma string de marcador 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(/,/), 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;
    }, {});
}

objetoParaMarcadoresDeStatus

O inverso de statusmarkersToObject; transforma um objeto JavaScript simples em uma string delimitada por vírgulas, adequada para uso como o valor da propriedadestatusmarkersde um objeto token Roll20.

Observe que uma string de marcador 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 < 1 || value > 9 ? chave: chave + '@' + parseInt(valor);
            })
            .join(',');
}

 


Underscore.js

site Underscore.js é mais uma referência de API do que um guia para utilizar a biblioteca. Embora seja útil para consultar quais funções estão disponíveis e quais parâmetros elas aceitam, isso não auxilia alguém que está a tentar explorar ao máximo o potencial da biblioteca.

Coleções

Escrever scripts frequentemente envolve realizaraçõesem um conjunto deelementos. Quando nos referimos a coleções, elas podem ser matrizes:var foo = [0,1,10,"banana"];ou objetos:var bar = { one: 1, two: 2, banana: 'fruit' };. As matrizes são indexadas por números (geralmente começando em 0 e contando para cima), os objetos possuem propriedades que podem ser utilizadas para índices:bar['banana'] === 'fruit'; // verdadeiro!. Os objetos funcionam efetivamente como matrizes associativas de outras linguagens.

Dados de amostra
Matriz de amostra:
var foo = [0,1,10,"banana"];

// Objeto de amostra
var bar = { one: 1, two: 2, banana: 'fruit' };
Chamando uma função com Each Element [ _.each() ]

É muito comum precisar realizar alguma operação com cada elemento de uma coleção. Normalmente, as pessoas utilizam loopsforou similares. O Underscore fornece _.each(), uma maneira de chamar uma função com cada elemento de uma coleção como argumento.

_.each(foo, função(elemento){
  log('elemento é '+elemento);
"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, função(elemento){
  log('elemento é '+elemento);
"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("An Array:");
_.each(foo, logKeyValueMapping);

log("An Object:");
_.each(bar, logKeyValueMapping);
"Uma matriz:"
"0 :: 0"
"1 :: 1"
"2 :: 10"
"3 :: banana"
"Um objeto:"
"um :: 1"
"dois :: 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. Frequentemente, as pessoas podem fazer isso criando outra coleção e, em seguida, utilizando um loopforpara iterar pela primeira coleção, transformando o valor e inserindo-o no novo contêiner. É uma quantidade significativa de código que pode ser simplificada com o _.map() do Underscore, uma maneira de aplicar uma função a uma coleção de elementos e obter uma coleção dos resultados. Se isso parece semelhante a _.each(), é porque, de facto, possui a mesma assinatura.

var res = _.map(foo, function(element){
  return 'element is '+element;
});
log(res);
['elemento é 0','elemento é 1','elemento é 10','elemento é banana']

O retorno de _.map() é sempre uma matriz dos resultados (consulte Conversão de coleções 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("An Array:");
var resA = _.map(foo, getKeyValueMapping);
log(resA);

log("An Object:");
var resB_.map(bar, getKeyValueMapping);
log(resB);
"Uma matriz:"
"['0 :: 0', '1 :: 1', '2 :: 10', '3 :: banana']"
"Um objeto:"
"['one :: 1', 'two :: 2', 'banana :: fruit']"
Este artigo foi útil?
16 de 18 acharam isto útil