Modello modulare rivelatore
Il modello a moduliemula il concetto di classi di altri linguaggi incapsulando membri privati e pubblici all'interno di un oggetto. Il modello rivelatore migliora il modello modulare 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('Tuttavia, posso modificarne il valore');
log(myRevealingModule.getFunc()); // "Tuttavia, posso modificarne il valore"
log(myRevealingModule.myVar); // "Questa variabile è pubblica"
myRevealingModule.myVar = 'Quindi posso modificarla come desidero';
log(myRevealingModule.myVar); // "Quindi posso modificarla come desidero"
Memorizzazione
La memorizzazione è una tecnica di ottimizzazione che archivia il risultato di un determinato input, consentendo di ottenere lo stesso output senza doverlo calcolare due volte. Ciò risulta particolarmente utile nei calcoli complessi. Naturalmente, se è raro che la funzione riceva lo stesso input, la memoizzazione avrà un strumento limitato, mentre i requisiti di archiviazione continueranno ad aumentare.
var factorialCache = {};
function factorial(n) {
var x;
n = parseInt(n || 0);
if (n < 0) {
throw 'I fattoriali dei numeri negativi non sono ben definiti';
}
if (n === 0) {
return 1;
} else if (factorialCache[n]) {
return factorialCache[n];
}
x = factorial(n - 1) * n;
factorialCache[n] = x;
return x;
}
In uno script Roll20 Mod (API), i valori memorizzati nella cache potrebbero essere potenzialmente archiviati nello stato, che rimarrà invariato tra una sessione di gioco e l'altra. Tuttavia, se si dispone di un numero elevato di potenziali input, è opportuno tenere presente che Roll20 potrebbe limitare l'utilizzo dellostato.
Semifaro asincrono
Un semaforo asincrono consente di attivare un metodo di callback al termine di una serie di operazioni asincrone (come le chiamate a sendChat). Sebbene non sia possibile garantirel'ordinein cui le operazioni saranno completate, è possibile garantire che tuttesarannocompletate quando verrà attivata la callback del semaforo.
Quando si utilizza un semaforo, è necessario richiamarev()prima di richiamare ciascuna operazione asincrona e richiamarep()come ultima istruzione di ciascuna operazione asincrona. Se il numero di operazioni che si intende eseguire è noto in anticipo, è possibile fornire tale numero al costruttore del semaforo e omettere le chiamate av.
Questa particolare implementazione di un semaforo asincrono consente anche di fornire un contesto per la callback (impostare il valore dithis), nonché di passare parametri alla callback. I parametri possono essere specificati sia nel costruttore che nella chiamata ap. (I parametri inphanno la precedenza sui parametri nel costruttore.)
funzione Semaphore(callback, iniziale, contesto) {
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) {
// consentire a sem.p(arg1, arg2, ...) di sovrascrivere gli argomenti passati al costruttore Semaphore
if (arguments.length > 0) { parameters = arguments; }
else { parameters = this.args; }
this.callback.apply(this.context, parameters);
}
}
};
Esempio di utilizzo:
var sem = new Semaphore(function(lastAsync) {
log(lastAsync + ' completato per ultimo');
log(this);
}, 2, { foo: 'bar', fizz: 'buzz' }, 'Sir non compare in questa callback');
sendChat('', '/roll d20', function(ops) {
log('Esecuzione del primo sendChat');
sem.p('Prima chiamata sendChat');
});
sendChat('', '/roll d20', function(ops) {
log('Esecuzione del secondo sendChat');
sem.p('Seconda chiamata sendChat');
});
Esempio di output:
"Esecuzione del secondo sendChat"
"Esecuzione del primo sendChat"
"Prima chiamata sendChat completata per ultima"
{ foo: "bar", fizz: "buzz" }
Materiali di gioco & personaggi
Creazione di un materiale di gioco
A causa del modo in cui vengono gestiti i blocchi di testo Handout, la creazione di un oggetto Handout deve essere eseguita in due passaggi: prima creare l'oggetto, quindi impostare i blocchi di testo:
//Creare un nuovo materiale di gioco disponibile per tutti i giocatori
var handout = createObj("handout", {
name: "Il nome del materiale di gioco",
inplayerjournals: "all",
archived: false
});
handout.set('notes', 'Le note devono essere impostate dopo la creazione del materiale di gioco.');
handout.set('note del GM', 'Anche la note del GM deve essere impostata dopo la creazione del volantino.');
Gestione della codifica
I blocchi di testo in materiali di gioco (Note e note del GM) e personaggi (Biografia e note del GM) impostati tramite l'interfaccia utente sono memorizzati in formatox-www-form-urlencoded. È possibile riconoscerlo dalla sequenza di codici %## presenti nel testo:
"Erik%20%28Viking%2BScienziato%29%20%5BCombattente%3A%203%2C%20Mago%3A%202%5D"
Questo testo può essere inviato alla chat e verrà tradotto dal browser; tuttavia, se è necessario apportare modifiche al testo, si consiglia di gestirlo così come è stato inserito:
Erik (Vichingo+Scienziato) [Combattente: 3, Mago: 2]"
È possibile decodificare il testo codificato utilizzando la seguente funzione:
var decodeUrlEncoding = function(t){
return t.replace(
/%([0-9A-Fa-f]{1,2})/g,
function(f,n){
return String.fromCharCode(parseInt(n,16));
}
);
}
Funzioni di strumento
Le funzioni di strumento svolgono attività comuni che potrebbero essere utili in molti script. Se si colloca una funzione nell'ambito più esterno di una scheda di script, tale funzione dovrebbe essere disponibile per tutti gli script, riducendo così il carico di lavoro. Di seguito è riportata una selezione di tali funzioni.
decodificaTestoEditor
Dipendenze:Nessuna
Il nuovo editor di testo integrato nel gioco è piuttosto interessante, ma presenta un inconveniente per gli script Mod (API) che dipendono dalla lettura delle informazioni da una delle aree di testo più estese nel set di dati. Questa funzione è utile a tal fine.
Dato il testo della proprietàgmnotesdi un grafico, o della proprietàbioogmnotesdi un personaggio, o della proprietànotesogmnotesdi un materiale di gioco, questo restituirà una versione senza la formattazione dell'editor inserita automaticamente.
const decodeEditorText = (t, o) =>{
let w = t;
o = Object.assign({ separator: '\r\n', asArray: false },o);
/* note del GM */
if(/^%3Cp%3E/.test(w)){
w = unescape(w);
}
if(/^<p>/.test(w)){
let lines = w.match(/<p>.*?<\/p>/g)
.mappa( l => l.replace(/^<p>(.*?)<\/p>$/,'$1'));
return o.asArray ? linee: lines.join(o.separator);
}
/* né */
return t;
};
Il primo argomento è il testo da elaborare.
const text = decodeEditorText(token.get('gmnotes'));
Per impostazione predefinita, le righe di testo saranno separate da \r\n.
Il secondo argomento opzionale è un oggetto con le opzioni.
separatore-- specifica con cosa separare le righe di testo. Predefinito: \r\n
const text = decodeEditorText(token.get('gmnotes'),{separator:'<BR>'});
asArray: specifica di restituire le righe come array. Impostazione predefinita:falso
const text = decodeEditorText(token.get('gmnotes'),{asArray:true});
NOTA: I tag nidificati<p>possono causare confusione e compromettere la decodifica. Se dovesse incontrare questo problema e necessitare di assistenza, invii un messaggioprivato a Aaron, che sarà lieto di esaminarlo.
getCleanImgsrc
Dipendenze:Nessuna
Dato un URL di immagine proveniente da un token o da un'altra risorsa, si ottiene una versione pulita dello stesso che può essere utilizzata per creare un token tramite l'API, oppureundefinedse non è possibile crearlo 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 di creare solo immagini la cui fonte si trova in una libreria utente. L'imgsrcdeve inoltre corrispondere alla versioneridottadell'immagine.
getSenderForName
Dipendenze:Nessuna
Data una stringa nome, questa funzione restituirà una stringa adatta al primo parametro di sendChat. Se esiste un personaggio che condivide il nome con un giocatore, verrà utilizzato il giocatore. È inoltre possibile passare un oggettoopzioni, strutturato in modo identico al parametroopzionidi findObjs.
funzione getSenderForName(nome, opzioni) {
var personaggio = findObjs({
type: 'character',
name: name
}, opzioni)[0],
player = 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 (personaggio) {
return 'personaggio|' + personaggio.id;
}
return name;
}
getWhisperTarget
Dipendenze:levenshteinDistance
Data una serie di opzioni, questa funzione cerca di costruire la parte "/w nome" di un sussurro per una chiamata a sendChat. Il parametro"options"deve contenere "player: true" o "personaggio: true" e un valore per "id" o "name". Se entrambe le condizioni sono vere, i giocatori hanno la precedenza sui personaggi, e gli ID hanno la precedenza sui nomi se entrambi hanno un valore valido. Se viene fornito un nome, il sussurro verrà inviato al giocatore o al personaggio con il nome più simile alla stringa fornita.
L'opzioneè tecnicamente facoltativa, ma se viene omessa (o se non viene fornita una combinazione di giocatore/personaggio + id/nome), la funzione restituirà una stringa vuota.
funzione getWhisperTarget(opzioni) {
var nameProperty, targets, type;
opzioni = opzioni || {};
if (options.player) {xml-ph-0003@deepl.internamnProperty = 'displayname';xml-ph-0004@deepl.interntype = 'player';xml-ph-0005@deepl.intern} else if (options.character) {xml-ph-0006@deepl.internamnProperty = 'name';xml-ph-0007@deepl.interntype = 'character';xml-ph-0008@deepl.intern} else {xml-ph-0009@deepl.internreturn '';xml-ph-0010@deepl.intern}xml-ph-0011@deepl.internif (options.id) {xml-ph-0012@deepl.interntargets = [getObj(type, options.id)];xml-ph-0013@deepl.intern} if (targets[0]) {
return '/w ' + targets[0].get(nameProperty).split(' ')[0] + ' ';
}
}
if (options.name) {
//Ordina tutti i giocatori o i personaggi (a seconda dei casi) il cui nome *contiene* il nome fornito,
//quindi ordinali in base alla loro vicinanza 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 '';
}
processoInlinerolls
Questa funzione esegue una scansione dimsg.content e sostituisce i roll in linea con il loro risultato totale. Ciò è particolarmente utile per i comandi Mod (API) ai quali l'utente potrebbe voler passare i tiri in linea come parametri.
funzione processInlinerolls(msg) {
se (_.has(msg, 'inlinerolls')) {
restituisci _.chain(msg.inlinerolls)
.reduce(funzione(precedente, corrente, indice) {
precedente['$[[' + indice + ']]'] = corrente.risultati.totale || 0;
restituisci previous;
},{})
.reduce(function(previous, current, index) {
restituisci previous.replace(index, current);
}, msg.content)
.value();
} else {
restituisci msg.content;
}
}
Di seguito è riportata una versione leggermente più complessa che gestisce anche la conversione degli elementi della tabella nel loro testo:
funzione 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);
restituisce m3;
},[]).join(', '));
}
restituisce m2;
},[]).join(', ');
m['$[['+k+']]']= (ti.length && ti) || v.results.total || 0;
restituisci m;
},{})
.reduce(function(m,v,k){
restituisci m.replace(k,v);
},msg.content)
.value();
} else {
restituisci msg.content;
}
}
indicatori di statoToObject
L'inverso di objectToStatusmarkers; trasforma una stringa adatta all'uso come valore della proprietàstatusmarkersdi un oggetto segnalino Roll20 in un semplice oggetto JavaScript.
Si prega di notare che una stringa di indicatori di stato può contenere indicatori di stato duplicati, mentre un oggetto non può contenere proprietà duplicate.
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;
}, {});
}
indicatori di stato degli oggetti
L'inverso di statusmarkersToObject; trasforma un semplice oggetto JavaScript in una stringa delimitata da virgole adatta all'uso come valore della proprietàstatusmarkersdi un oggetto segnalino Roll20.
Si prega di notare che una stringa di indicatori di stato può contenere indicatori di stato duplicati, mentre un oggetto non può contenere proprietà duplicate.
funzione objectToStatusmarkers(obj) {
return _.map(obj, function(value, key) {
return key === 'dead' || value < 1 || value > 9 ? chiave: chiave + '@' + parseInt(valore);
})
.join(',');
}
Underscore.js
L' sito web Underscore.js è più un riferimento API che una guida all'utilizzo della libreria. Sebbene sia utile per verificare quali funzioni sono disponibili e quali parametri accettano, non è di aiuto per chi desidera sfruttare appieno le potenzialità della libreria.
Collezioni
La scrittura di script spesso comporta l'esecuzionedi operazionisu un insieme dielementi. Quando si parla di raccolte, queste 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 a partire da 0 e in ordine crescente), mentre gli oggetti hanno proprietà che possono essere utilizzate come indici:bar['banana'] === 'fruit'; // vero!. Gli oggetti si comportano in modo simile agli array associativi di altri linguaggi.
Esempio di array:
var foo = [0,1,10,"banana"];
// Esempio di oggetto
var bar = { one: 1, two: 2, banana: 'fruit' };
Chiamare una funzione con Each Element [ _.each() ]
È molto comune dover eseguire alcune operazioni su ciascun elemento di una raccolta. Generalmente si utilizzano cicliforo simili. Underscore offre _.each(), un metodo per richiamare una funzione con ciascun elemento di una collezione come argomento.
_.each(foo, function(element){
log('element is '+element);
"elemento è 0"
"elemento è 1"
"elemento è 10"
"elemento è banana"
Ciò che rende questo codice così potente è che funziona in modo identico sia che si utilizzi un array sia che si utilizzi un oggetto:
_.each(bar, function(element){
log('element is '+element);
"elemento è 1"
"elemento è 2"
"elemento è frutta"
Le funzioni non devono necessariamente essere inline. Ricevono anche parametri aggiuntivi. (Per ulteriori parametri, consultare la documentazione.):
var logKeyValueMapping = function(value, key) {
log(key + " :: " + value);
};
log("An Array:");
_.each(foo, logKeyValueMapping);
log("An Object:");
_.each(bar, logKeyValueMapping);
"Un array:"
"0 :: 0"
"1 :: 1"
"2 :: 10"
"3 :: banana"
"Un oggetto:"
"uno :: 1"
"due :: 2"
"banana :: frutta"
Trasformazione di ciascun elemento [ _.map() ]
La seconda operazione più comune da eseguire con una raccolta è trasformare tutti gli articoli contenuti in articoli di un altro tipo. Spesso si procede creando un'altra raccolta, quindi utilizzando un cicloforper iterare sulla prima raccolta, trasformando il valore e inserendolo nel nuovo contenitore. Si tratta di una quantità significativa di codice che può essere semplificata utilizzando la funzione _.map() di Underscore, che consente di applicare una funzione a una serie di elementi e ottenere una serie di risultati. Se questo sembra simile a _.each(), è perché in effetti lo è, poiché presenta la stessa firma.
var res = _.map(foo, function(element){
return 'element is '+element;
});
log(res);
['elemento è 0','elemento è 1','elemento è 10','elemento è banana']
Il ritorno di _.map() è sempre un array dei risultati (vedere Conversione delle raccolte di seguito 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(barra, getKeyValueMapping);
log(resB);
"Un array:"
"['0 :: 0', '1 :: 1', '2 :: 10', '3 :: banana']"
"Un oggetto:"
"['uno :: 1', 'due :: 2', 'banana :: frutta']"