Aufschlussreiches Modulmuster
DasModulmusteremuliert das Konzept von Klassen aus anderen Sprachen, indem private und öffentliche Mitglieder in einem Objekt gekapselt werden. Das Revealing Module Pattern verbessert das Module Pattern, indem es die Syntax konsistenter macht.
var myRevealingModule = myRevealingModule || (function() {
var privateVar = 'Diese Variable ist privat',
publicVar = 'Diese Variable ist öffentlich';
function privateFunction() {
log(privateVar);
}
function publicSet(text) {
privateVar = text;
}
function publicGet() {
privateFunction();
}
return {
setFunc: publicSet,
myVar: publicVar,
getFunc: publicGet
};
}());
log(myRevealingModule.getFunc()); // „Diese Variable ist privat“
myRevealingModule.setFunc('Aber ich kann ihren Wert ändern');
log(myRevealingModule.getFunc()); // „Aber ich kann seinen Wert ändern“
log(myRevealingModule.myVar); // "Diese Variable ist öffentlich"
myRevealingModule.myVar = 'Damit ich alles ändern kann, was ich will';
log(myRevealingModule.myVar); // „Damit ich alles ändern kann, was ich will“
Auswendiglernen
Memoisierung ist eine Optimierungstechnik, die das Ergebnis für eine bestimmte Eingabe speichert und es ermöglicht, dieselbe Ausgabe zu erzeugen, ohne sie zweimal berechnen zu müssen. Dies ist besonders bei aufwendigen Berechnungen nützlich. Wenn deine Funktion nur selten die gleiche Eingabe erhält, ist die Memoisierung natürlich nur von begrenztem Nutzen, während der Speicherbedarf dafür weiter steigt.
var factialCache = {};
Funktion Fakultät(n) {
var x;
n = parseInt(n || 0);
if (n < 0) {
throw 'Fakultäten negativer Zahlen sind nicht wohldefiniert';
}
if (n === 0) {
return 1;
} else if (factorialCache[n]) {
return factorialCache[n];
}
x = Fakultät(n - 1) * n;
faktorielleCache[n] = x;
Rückkehr x;
}
In einem Roll20 Mod (API) Skript könnten die zwischengespeicherten Werte potenziell in state
gespeichert werden, was zwischen den Spielsitzungen bestehen bleibt. Wenn du eine große Anzahl potenzieller Eingaben hast, solltest du dir jedoch darüber im Klaren sein, dass Roll20 deine Nutzung vondrosseln kann
.
Asynchrones Semaphor
Eine asynchrone Semaphore ermöglicht es dir, eine Callback-Methode auszulösen, nachdem eine Reihe von asynchronen Operationen (wie z.B. Aufrufe an sendChat) abgeschlossen sind. Sie können zwar nicht garantieren, in welcher Reihenfolge die Vorgänge abgeschlossen werden, Sie können jedoch garantieren, dass alle abgeschlossen sind wenn der Callback des Semaphors ausgelöst wird.
Wenn du eine Semaphore verwendest, rufev()
vor dem Aufruf jeder asynchronen Operation auf, und rufep()
als letzte Anweisung jeder asynchronen Operation auf. Wenn die Anzahl der Operationen, die du durchführen willst, im Voraus bekannt ist, kannst du diese Anzahl auch an den Semaphor-Konstruktor übergeben und die Aufrufe vonund
auslassen.
Mit dieser speziellen Implementierung eines asynchronen Semaphors können Sie auch einen Kontext für den Rückruf bereitstellen (den Wertauf
setzen) sowie Parameter an den Rückruf übergeben. Die Parameter können entweder im Konstruktor oder im Aufruf vonp
angegeben werden. (Parameter inp
haben Vorrang vor Parametern im Konstruktor.)
function Semaphore(callback, initial, context) {
var args = (arguments.length === 1 ? [Argumente[0]]: Array.apply(null, Argumente));
this.lock = parseInt(initial, 10) || 0;
this.callback = Rückruf;
this.context = Kontext || Ruf zurück;
this.args = args.slice(3);
}
Semaphore.prototype = {
v: function() { this.lock++; },
p: function() {
var-Parameter;
this.lock--;
if (this.lock === 0 && this.callback) {
// sem.p(arg1, arg2, ...) erlauben, an den Semaphore-Konstruktor übergebene Argumente zu überschreiben
if (arguments.length > 0) { Parameter = Argumente; }
else { Parameter = this.args; }
this.callback.apply(this.context, Parameters);
}
}
};
Anwendungsbeispiel:
var sem = new Semaphore(function(lastAsync) {
log(lastAsync + 'letzt abgeschlossen');
log(this);
}, 2, { foo: 'bar', fizz: 'buzz' }, 'Sir erscheint nicht in diesem Rückruf');
sendChat('', '/roll d20', function(ops) {
log('Executing first sendChat');
sem.p('First sendChat call');
});
sendChat('', '/roll d20', function(ops) {
log('Ausführen des zweiten sendChat');
sem.p('Zweiter sendChat-Aufruf');
});
Beispielausgabe:
„Zweiter sendChat wird ausgeführt“
„Erster sendChat wird ausgeführt“
„Erster sendChat-Aufruf zuletzt abgeschlossen“
{ foo: „bar“, fizz: „buzz“ }
Notizen & Charaktere
Erstellen einer Notiz
Aufgrund der Art und Weise, wie Handout-Textblöcke behandelt werden, muss ein Handout-Objekt in zwei Schritten erstellt werden: Erstelle zuerst das Objekt und setze dann die Textblöcke:
//Erstelle ein neues Handout, das allen Spielern zur Verfügung steht
var handout = createObj("handout", {
name: "Der Name des Handouts",
inplayerjournals: "all",
archived: false
});
handout.set('notes', 'Notizen müssen nach der Erstellung des Handouts festgelegt werden.');
handout.set('gmnotes', 'GM-Notizen müssen ebenfalls festgelegt werden, nachdem das Handout erstellt wurde.');
Umgang mit der Kodierung
Die Textblöcke in Handouts (Notizen und GM-Notizen) und Charakteren (Bio- und GM-Notizen), die über die Benutzeroberfläche festgelegt werden, werden im Formatx-www-form-urlencoded
gespeichert. Du erkennst dies an der Abfolge der %##-Codes im Text:
„Erik%20%28Wikinger%2BWissenschaftler%29%20%5BKämpfer%3A%203%2C%20Zauberer%3A%202%5D“
Dieser Text kann an den Chat gesendet werden und wird vom Browser übersetzt. Wenn Sie jedoch Änderungen am Text vornehmen müssen, möchten Sie ihn möglicherweise so behandeln, wie er eingegeben wurde:
Erik (Wikinger+Wissenschaftler) [Kämpfer: 3, Zauberer: 2]“
Sie können den codierten Text mit der folgenden Funktion dekodieren:
var decodeUrlEncoding = function(t){
return t.replace(
/%([0-9A-Fa-f]{1,2})/g,
function(f,n){
return String.fromCharCode(parseInt(n,16));
}
) ;
}
Dienstprogrammfunktionen
Hilfsfunktionen erledigen allgemeine Aufgaben, die Sie möglicherweise in vielen Skripts verwenden möchten. Wenn du eine Funktion im äußersten Bereich einer Skript-Registerkarte platzierst, sollte diese Funktion für alle deine Skripte verfügbar sein, was deinen Overhead reduziert. Unten findest du eine Auswahl solcher Funktionen.
decodeEditorText
Abhängigkeiten:Keine
Der neue In-Game-Texteditor ist ziemlich nett, aber bringt ein Problem für Mod (API) Skripte mit sich, die davon abhängen, Informationen aus einem der großen Textbereiche im Datensatz zu lesen. Dabei hilft diese Funktion.
Wenn der Text aus einer Eigenschaft Graphic's gmnotes
oder einer Eigenschaft Character's bio
oder gmnotes
oder einer Eigenschaft Handout's Notes
oder gmnotes
stammt, wird dies zurückgegeben eine Version, bei der die Formatierung des automatisch eingefügten Editors entfernt wurde.
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)){
letlines = w.match(/<p>.*?<\/p>/g)
.map( l => l.replace(/^<p>(.*?)<\/p>$/,'$1'));
return o.asArray ? Zeilen:lines.join(o.separator);
}
/* weder */
return t;
};
Das erste Argument ist der zu verarbeitende Text.
const text = decodeEditorText(token.get('gmnotes'));
Standardmäßig werden die Textzeilen durch\r\n
getrennt.
Das optionale zweite Argument ist ein Objekt mit Optionen.
Trennzeichen
– gibt an, womit Textzeilen getrennt werden sollen. Standard:\r\n
const text = decodeEditorText(token.get('gmnotes'),{separator:'<BR>'});
asArray
– gibt an, die Zeilen stattdessen als Array zurückzugeben. Standard:false
const text = decodeEditorText(token.get('gmnotes'),{asArray:true});
HINWEIS:Verschachtelte<p>
Tags verwirren und unterbrechen die Dekodierung. Wenn du dieses Problem hast und Hilfe brauchst, schreibe eine PM an The Aaron und er wird sich das Problem gerne ansehen.
getCleanImgsrc
Abhängigkeiten:Keine
Bei einer Bild-URL, die von einem Spielmarker oder einer anderen Ressource stammt, erhältst du eine saubere Version davon, die zur Erstellung eines Spielmarkers über die API verwendet werden kann, oder undefined
wenn es nicht über die API erstellt werden kann.
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;
};
Hinweis:Die API ist nur in der Lage, Bilder zu erstellen, deren Quelle sich in einer Benutzerbibliothek befindet. Dieimgsrc
muss auch dieThumbVersion des Bildes sein.
getSenderForName
Abhängigkeiten:Keine
Bei einem gegebenen String-Namen gibt diese Funktion einen String zurück, der für den ersten Parameter von sendChat geeignet ist. Wenn es einen Charakter gibt, der einen Namen mit einem Spieler teilt, wird der Spieler verwendet. Du kannst auch ein options
Objekt übergeben, das genauso aufgebaut ist wie der options
Parameter von findObjs.
function getSenderForName(name, options) {
var Character = findObjs({
type: 'character',
name: name
}, options)[0],
player = findObjs({
type: 'player',
displayname: name.lastIndexOf(' (GM)') === name .Länge - 5 ? name.substring(0, name.length - 5): name
}, Optionen)[0];
if (player) {
return 'player|' + Spieler.id;
}
if (character) {
return 'character|' + charakter.id;
}
Rückgabename;
}
getWhisperTarget
Abhängigkeiten:levenshteinDistance
Bei einer Reihe von Optionen versucht diese Funktion, den „/w name“-Teil eines Whispers für einen Aufruf von sendChat zu erstellen. Der Parameter Optionen
sollte entweder Spieler: wahr
oder Zeichen: wahr
und einen Wert für entweder ID
oder Name
enthalten. Spieler werden gegenüber Charakteren bevorzugt, wenn beide wahr sind, und IDs werden gegenüber Namen bevorzugt, wenn beide einen gültigen Wert haben. Wenn ein Name angegeben wird, wird der Flüsterton an den Spieler oder Charakter gesendet, dessen Name der angegebenen Zeichenfolge am nächsten kommt.
options
ist technisch gesehen optional, aber wenn du sie weglässt (oder keine Kombination aus Spieler/Charakter + id/Name angibst), gibt die Funktion einen leeren String zurück.
function getWhisperTarget(options) {
var nameProperty, targets, type;
Optionen = Optionen || {};
if (options.player) {
nameProperty = 'displayname';
Typ = 'Spieler';
} else if (options.character) {
nameProperty = 'name';
Typ = 'Zeichen';
} else {
return '';
}
if (options.id) {
targets = [getObj(type, options.id)];
if (targets[0]) {
return '/w ' + targets[0].get(nameProperty).split(' ')[0] + ' ';
}
}
if (options.name) {
// Alle Spieler oder Charaktere (je nach Bedarf) sortieren, deren Name den angegebenen Namen *enthält*,
// dann danach sortieren, wie nah sie am angegebenen Namen sind.
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
Diese Funktion wird durch msg.content
scannen und Inline-Rolls durch ihr Gesamtergebnis ersetzen. Dies ist besonders nützlich für API-Befehle, an die der Benutzer möglicherweise Inline-Rolls als Parameter übergeben möchte.
functionprocessInlinerolls(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;
}
}
Hier ist eine etwas kompliziertere Version, die auch die Konvertierung von tableItems in ihren Text übernimmt:
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
Die Umkehrung von objectToStatusmarkers; wandelt eine Zeichenfolge, die zur Verwendung als Wert der Eigenschaftstatusmarkers
eines Roll20-Token-Objekts geeignet ist, in ein einfaches altes JavaScript-Objekt um.
Beachte, dass ein Statusmarker-String doppelte Statusmarker enthalten kann, während ein Objekt keine doppelten Eigenschaften enthalten kann.
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;
}, {} );
}
objectToStatusmarkers
Die Umkehrung von statusmarkersToObject; Wandelt ein einfaches altes JavaScript-Objekt in eine durch Kommas getrennte Zeichenfolge um, die zur Verwendung als Wert der Eigenschaftstatusmarkers
eines Roll20-Token-Objekts geeignet ist.
Beachten Sie, dass eine Statusmarkerzeichenfolge doppelte Statusmarker enthalten kann, während ein Objekt keine doppelten Eigenschaften enthalten kann.
function objectToStatusmarkers(obj) {
return _.map(obj, function(value, key) {
return key === 'dead' || value < 1 || value > 9 ? Schlüssel: Schlüssel + '@' + parseInt(value);
})
.join(',');
}
Underscore.js
Die WebsiteUnderscore.jsist eher eine API-Referenz als eine Anleitung zur Verwendung der Bibliothek. Es ist zwar nützlich, um herauszufinden, welche Funktionen verfügbar sind und welche Parameter sie akzeptieren, aber es hilft niemandem, der versucht, die Leistungsfähigkeit der Bibliothek in vollem Umfang zu nutzen.
Sammlungen
Beim Schreiben von Skripten geht es oft darum, etwas für eine Sammlung von Dingen zu tun. Wenn wir über Sammlungen sprechen, können sie entweder Arrays sein:var foo = [0,1,10,"banana"];
oder Objekte:var bar = { one: 1, two: 2, banana: 'fruit' };
. Arrays werden durch Zahlen indiziert (normalerweise beginnend bei 0 und hochzählend), Objekte haben Eigenschaften, die für Indizes verwendet werden können:bar['banana'] === 'fruit'; // WAHR!
. Objekte verhalten sich praktisch wie assoziative Arrays aus anderen Sprachen.
Beispielarray:
var foo = [0,1,10,"banana"];
// Beispielobjekt
var bar = { one: 1, two: 2, banana: 'fruit' };
Aufrufen einer Funktion mit Each Element [ _.each() ]
Es kommt sehr häufig vor, dass mit jedem Element einer Sammlung eine Operation ausgeführt werden muss. Normalerweise verwenden die Leutefür
Schleifen oder ähnliches. Underscore bietet_.each(), eine Möglichkeit, eine Funktion mit jedem Element einer Sammlung als Argument aufzurufen.
_.each(foo, function(element){
log('element is '+element);
„Element ist 0“
„Element ist 1“
„Element ist 10“
„Element ist Banane“
Was dies so leistungsstark macht, ist, dass der identische Code funktioniert, unabhängig davon, ob Sie ein Array oder ein Objekt verwenden:
_.each(bar, function(element){
log('element is '+element);
„Element ist 1“
„Element ist 2“
„Element ist Frucht“
Funktionen müssen nicht inline sein. Sie erhalten außerdem zusätzliche Parameter. (Weitere Parameter finden Sie in der Dokumentation.):
var logKeyValueMapping = function( value, key ) {
log(key + " :: " + value);
};
log("Ein Array:");
_.each(foo, logKeyValueMapping);
log("Ein Objekt:");
_.each(bar, logKeyValueMapping);
„Ein Array:“
„0 :: 0“
„1 :: 1“
„2 :: 10“
„3 :: Banane“
„Ein Objekt:“
„eins :: 1“
„zwei: : 2"
"Banane :: Frucht"
Jedes Element transformieren [ _.map() ]
Die zweithäufigste Vorgehensweise bei einer Sammlung besteht darin, alle darin enthaltenen Elemente in Elemente eines anderen Typs umzuwandeln. Dies geschieht oft, indem man eine weitere Sammlung erstellt und dann eine für
-Schleife verwendet, um die erste Sammlung zu durchlaufen, den Wert umzuwandeln und ihn in den neuen Container zu verschieben. Das ist eine Menge Code, der mit_.map()von Underscore vereinfacht werden kann, einer Möglichkeit, eine Funktion auf eine Sammlung von Elementen anzuwenden und eine Sammlung der Ergebnisse zu erhalten. Wenn das ähnlich klingt wie _.each(), liegt das daran, dass es tatsächlich dieselbe Signatur hat.
var res = _.map(foo, function(element){
return 'element is '+element;
});
Log(res);
['Element ist 0','Element ist 1','Element ist 10','Element ist Banane']
Die Rückgabe von _.map() ist immer ein Array der Ergebnisse (Informationen zum Abrufen von Objekten finden Sie weiter unten unter „Konvertieren von Sammlungen“). Genau wie _.each() erhält die Funktion mehr Argumente und kann separat definiert werden.
[var getKeyValueMapping = function( value, key ) {
return key + " :: " + value;
};
log("Ein Array:");
var resA = _.map(foo, getKeyValueMapping);
log(resA);
log("Ein Objekt:");
var resB_.map(bar, getKeyValueMapping);
log(resB);
"Ein Array:"
"['0 :: 0', '1 :: 1', '2 :: 10', '3 :: banane']"
"Ein Objekt:"
"['one :: 1‘, ‚zwei :: 2‘, ‚Banane :: Frucht‘]“