Pattern Revealing Module
Il modello di moduloemula il concetto di classe di altri linguaggi, incapsulando i membri privati e pubblici all'interno di un oggetto. Il Pattern Revealing Module migliora il Pattern Modulo rendendo la sintassi più coerente.
var myRevealingModule = myRevealingModule || (function() {
var privateVar = 'Questa variabile è privata',
publicVar = 'Questa variabile è pubblica';
function privateFunction() {
log(privateVar);
}
function publicSet(text) {
privateVar = text;
}
function publicGet() {
privateFunction();
}
return {
setFunc: publicSet,
myVar: publicVar,
getFunc: publicGet
};
}());
log(myRevealingModule.getFunc()); // "Questa variabile è privata"
myRevealingModule.setFunc('Ma posso cambiarne il valore');
log(myRevealingModule.getFunc()); // "Ma posso cambiarne il valore"
log(myRevealingModule.myVar); // "Questa variabile è pubblica"
myRevealingModule.myVar = 'Quindi posso cambiarla quanto voglio';
log(myRevealingModule.myVar); // "Quindi posso cambiarla quanto voglio"
Memoizzazione
La memoizzazione è una tecnica di ottimizzazione che memorizza il risultato per un determinato input, consentendo di produrre lo stesso output senza calcolarlo due volte. Questa tecnica è particolarmente utile nei calcoli costosi. Naturalmente, se è raro che la tua funzione riceva lo stesso input, la memoizzazione avrà una utilità limitata mentre i requisiti di archiviazione continueranno a crescere.
var fattorialeCache = {};
funzione fattoriale(n) {
var x;
n = parseInt(n || 0);
if (n < 0) {
throw 'I fattoriali di numeri negativi non sono ben definiti';
}
if (n === 0) {
return 1;
} else if (fattorialeCache[n]) {
return fattorialeCache[n];
}
x = fattoriale(n - 1) * n;
fattorialeCache[n] = x;
return x;
}
In uno script Mod (API) di Roll20, i valori memorizzati potrebbero potenzialmente essere immagazzinati in state
, che persisterà tra le sessioni di gioco. Se ha un numero elevato di ingressi potenziali, tuttavia, sappia che Roll20 potrebbe limitare l'uso dello stato.
Semaphore asincrono
Un semaforo asincrono ti consente di attivare un metodo di callback dopo che un insieme di operazioni asincrone (come chiamate a sendChat) sono state completate. Sebbene non sia possibile garantirel'ordine in cuile operazioni verranno completate, è possibile garantire che tuttele operazioni siano statecompletate quando viene attivato il callback del semaforo.
Quando utilizza un semaforo, chiamiv()
prima di chiamare ogni operazione asincrona e chiamip()
come ultima istruzione di ogni operazione asincrona. Se il numero di operazioni che intende eseguire è noto in anticipo, può anche fornire questo numero al costruttore del semaforo e omettere le chiamate av
.
Questa particolare implementazione di un semaforo asincrono le consente anche di fornire un contesto per la richiamata (imposta il valore dithis
), nonché di passare dei parametri alla richiamata. I parametri possono essere indicati sia nel costruttore che nella chiamata ap
. (I parametri dip
hanno la precedenza sui parametri del costruttore).
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 + ' completato per ultimo');
log(this);
}, 2, { foo: 'bar', fizz: 'buzz' }, 'Signore non appare in questo callback');
sendChat('', '/roll d20', function(ops) {
log('Esecuzione della prima sendChat');
sem.p('Prima chiamata sendChat');
});
sendChat('', '/roll d20', function(ops) {
log('Esecuzione della seconda sendChat');
sem.p('Seconda chiamata sendChat');
});
Esempio di uscita:
"Esecuzione della seconda sendChat"
"Esecuzione della prima sendChat"
"La prima chiamata sendChat è stata completata per ultima"
{ pippo: "bar", fizz: "buzz" }
Dispense & Personaggi
Creare un opuscolo
A causa del modo in cui vengono gestiti i blocchi di testo dell'Handout, la creazione di un oggetto Handout deve essere eseguita in due fasi: Prima crea l'oggetto, poi imposta i blocchi di testo:
//Creare una nuova Handout disponibile a tutti i giocatori
var handout = createObj("handout", {
name: "Il nome della handout",
inplayerjournals: "all",
archived: false
});
handout.set('notes', 'Le note devono essere impostate dopo la creazione della handout.');
handout.set('gmnotes', 'Anche le note del GM devono essere impostate dopo la creazione della handout.');
Gestione della Codifica
I blocchi di testo in Handouts (Note e Note del GM) e Personaggi (Scheda e Note del GM) che vengono impostati attraverso l'interfaccia utente sono memorizzati nel formato x-www-form-urlencoded. Puoi riconoscerlo dalla sequenza di codici %## presenti nel testo:
"Erik%20%28Viking%2BScienziato%29%20%5BCombattente%3A%203%2C%20Stregone%3A%202%5D"
Questo testo può essere inviato in chat e verrà tradotto dal browser, ma se hai bisogno di apportare modifiche al testo, potresti volerlo gestire come è stato inserito:
Erik (Vichingo+Scienziato) [Combattente: 3, Mago: 2]"
Puoi decodificare il testo codificato con la seguente funzione:
var decodificaEncodingUrl = function(t){
return t.replace(
/%([0-9A-Fa-f]{1,2})/g,
function(f,n){
return String.fromCharCode(parseInt(n,16));
}
);
}
Funzioni Utilità
Le funzioni utilità completano compiti comuni che potresti voler utilizzare in molti script. Se inserisci una funzione nello scope esterno di una scheda di script, quella funzione dovrebbe essere disponibile per tutti gli script, riducendo il tuo overhead. Di seguito è riportata una selezione di tali funzioni.
decodificaTestoEditore
Dipendenze: Nessuna
Il nuovo editor di testo in-game è abbastanza bello, ma presenta un problema per gli script Mod (API) che dipendono dalla lettura delle informazioni da una delle grandi aree di testo nel set di dati. Questa funzione aiuta con questo.
Dato il testo di una proprietàGraphic'sgmnotes
, o di una proprietàCharacter'sbio
ogmnotes
, o di una proprietàHandout'snotes
ogmnotes
, questo restituirà una versione con la formattazione dell'editore inserita automaticamente e rimossa.
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;
};
The first argument is the text to process.
const text = decodeEditorText(token.get('gmnotes'));
By default, the lines of text will be separated by\r\n
.
Il secondo argomento opzionale è un oggetto con delle opzioni.
separatore
-- specifica con cosa separare le righe di testo. Default:\r\n
const text = decodeEditorText(token.get('gmnotes'),{separator:'<BR>'});
comeArray
-- specifica di restituire invece le righe come un array. Default:falso
const text = decodeEditorText(token.get('gmnotes'),{asArray:true});
NOTA: Tag <p>
sannai confondere e interrompere la decodifica. Se incontri quel problema e hai bisogno di aiuto, scrivi un messaggio privato aThe Aaron e sará felice di aiutarti.
getCleanImgsrc
Dipendenze: Nessuna
Dato l'URL di un'immagine presa da un token o da una risorsa, ottieni una versione pulita che può essere utilizzata per creare un token tramite l'API, o undefined
se non può essere creata tramite l'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: L'API è in grado solo di creare immagini di cui la fonte si trova in una libreria dell'utente. L' imgsrc
deve essere anche la versione thumb dell'immagine.
ottieniMittentePerNome
Dipendenze: Nessuna
Dato un nome stringa, questa funzione restituisce una stringa appropriata per il primo parametro di sendChat. Se c'è un personaggio che condivide un nome con un giocatore, verrà utilizzato il giocatore. È inoltre possibile passare un oggettoopzioni
, che ha la stessa struttura del parametroopzioni
di findObjs.
funzione getSenderForName(nome, opzioni) {
var personaggio = findObjs({
type: 'character',
name: name
}, opzioni)[0],
giocatore = findObjs({
type: 'player',
displayname: 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;
}
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. Se viene fornito un nome, il giocatore o il personaggio con il nome più vicino alla stringa fornita riceverà il sussurro.
opzioni
è tecnicamente facoltativo, ma se lo ometti (o non fornisci una combinazione di giocatore/personaggio + id/nome), la funzione restituirà una stringa vuota.
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) {
// Ordina tutti i giocatori o i personaggi (come appropriato) il cui nome *contiene* il nome fornito,
// poi ordinali in base a quanto sono vicini al nome fornito.
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
Questa funzione scansionerà msg.content
e sostituirà i tiri in linea con il loro risultato totale. Questo è particolarmente utile per i comandi Mod (API) a cui l'utente potrebbe voler passare i tiri in linea come parametri.
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;
}
}
Ecco una versione leggermente più complicata che gestisce anche la conversione dei tableItem nel loro testo:
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
L'inverso di objectToStatusmarkers; trasforma una stringa adatta ad essere utilizzata come valore della proprietàstatusmarkers
di un oggetto token Roll20 in un semplice oggetto JavaScript.
Nota che una stringa statusmarker può contenere statusmarker duplicati, mentre un oggetto non può contenere proprietà duplicate.
funzione statusmarkersToObject(stats) {
return _.riduci(stats.split(/,/), function(memo, value) {
var parti = value.split(/@/),
num = parseInt(parti[1] || '0', 10);
if (parti[0].length) {
memo[parti[0]] = Math.max(num, memo[parti[0]] || 0);
}
return memo;
}, {});
}
objectToStatusmarkers
L'inverso di statusmarkersToObject; trasforma un semplice oggetto JavaScript in una stringa delimitata da virgole adatta ad essere utilizzata come valore della proprietàstatusmarkers
di un oggetto token Roll20.
Nota che una stringa statusmarker può contenere statusmarker duplicati, mentre un oggetto non può contenere proprietà duplicate.
function objectToStatusmarkers(obj) {
return _.map(obj, function(value, key) {
return key === 'dead' || value < 1 || value > 9 ? chiave : chiave + '@' + parseInt(valore);
})
.join(',');
}
Underscore.js
Il sito webUnderscore.jsè più un riferimento alle API che una guida all'uso della libreria. Sebbene utile per cercare quali funzioni sono disponibili e quali parametri accettano, non aiuta qualcuno che cerca di scoprire come usare appieno il potere della libreria.
Raccolte
Scrivere script spesso significa farequalcosaa un insieme dicose. Quando parliamo di collezioni, possono essere array:var foo = [0,1,10, "banana"];
oppure oggetti:var bar = { one: 1, two: 2, banana: 'fruit' };
. Gli array sono indicizzati da numeri (di solito partendo da 0 e contando verso l'alto), gli oggetti hanno proprietà che possono essere utilizzate per gli indici:bar['banana'] === 'frutta'; // vero!
. Gli oggetti agiscono effettivamente come array associativi di altri linguaggi.
Matrice di esempio:
var foo = [0,1,10,"banana"];
// Oggetto di esempio
var bar = { one: 1, two: 2, banana: 'fruit' };
Chiamare una funzione con ogni elemento [ _.each() ]
È molto comune dover eseguire un'operazione con ogni elemento di una collezione. Di solito le persone utilizzeranno per
cicli o simili. Underscore fornisce _.each(), un modo per chiamare una funzione con ogni elemento di una collezione come argomento.
_.each(foo, function(element){
log('elemento è '+element);
"elemento è 0"
"elemento è 1"
"elemento è 10"
"elemento è banana"
Ciò che rende ciò così potente è che il codice identico funziona indipendentemente dal fatto che si stia utilizzando un array o un oggetto:
_.each(bar, function(element){
log('elemento è '+element);
"elemento è 1"
"elemento è 2"
"elemento è frutta"
Le funzioni non devono essere in linea. Ricevono anche parametri aggiuntivi. (Vedi la documentazione per ulteriori parametri.):
var logKeyValueMapping = function( value, key ) {
log(key + " :: " + value);
};
log("Un Array:");
_.each(foo, logKeyValueMapping);
log("Un Oggetto:");
_.each(bar, logKeyValueMapping);
"Un Array:"
"0 :: 0"
"1 :: 1"
"2 :: 10"
"3 :: banana"
"Un Oggetto:"
"uno :: 1"
"due :: 2"
"banana :: frutta"
Trasformazione di ogni elemento [ _.map() ]
La cosa più comune da fare con una collezione è trasformare tutti gli elementi contenuti in elementi di un altro tipo. Spesso le persone possono farlo creando un'altra collezione, quindi utilizzando un ciclofor
per iterare sulla prima collezione, trasformando il valore e spingendolo nel nuovo contenitore. Tutto questo codice può essere semplificato con _.map()di Underscore, un modo per applicare una funzione a una collezione di elementi e ottenere una collezione di risultati. Se sembra simile a _.each(), è perché lo è, infatti, ha la stessa 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']
Il risultato di _.map() è sempre un array di risultati (vedi Converting Collections qui sotto per ottenere oggetti.) e, proprio come _.each(), la funzione riceve più argomenti e può essere definita separatamente.
[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);
"An Array:"
"['0 :: 0', '1 :: 1', '2 :: 10', '3 :: banana']"
"An Object:"
"['one :: 1', 'two :: 2', 'banana :: fruit']"