Mod (API): Kochbuch

Das hier sind keine kompletten Skripte. Sie sind dafür gedacht, zusammen mit der Geschäftslogik zu einem Skript zusammengefügt zu werden, um bei der Erstellung vollständiger Skripte zu helfen, und nicht, um selbstständig Skripte zu erstellen.


Revealing Module Pattern

Das Module Pattern emuliert das Konzept von Klassen aus anderen Sprachen, indem es private und öffentliche Mitglieder in einem Objekt kapselt. Das Revealing Module Pattern macht das Module Pattern besser, indem es die Syntax einheitlicher 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 ihren Wert ändern"

log(myRevealingModule.myVar); // „Diese Variable ist öffentlich”
myRevealingModule.myVar = 'Also kann ich sie nach Belieben ändern';
log(myRevealingModule.myVar); // „Also kann ich sie nach Belieben ändern”

Memoization

Memoization ist eine Optimierungstechnik, bei der das Ergebnis für eine bestimmte Eingabe gespeichert wird, sodass dieselbe Ausgabe erzeugt werden kann, ohne sie zweimal berechnen zu müssen. Das ist besonders bei aufwendigen Berechnungen nützlich. Wenn deine Funktion nur selten die gleiche Eingabe bekommt, ist Memoisierung natürlich nicht so nützlich, während der Speicherbedarf dafür immer weiter steigt.

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

    n = parseInt(n || 0);
    if (n < 0) {
        throw 'Fakultäten von negativen Zahlen sind nicht klar definiert';
    }

    if (n === 0) {
        return 1;
    }else if (factorialCache[n]) {
        return factorialCache[n];
    }

    x = factorial(n - 1) * n;
    factorialCache[n] = x;
    return 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 aber viele mögliche Eingaben hast, denk dran, dass Roll20 deine Nutzung von state einschränken könnte.


Asynchrones Semaphor

Mit einem asynchronen Semaphor kannst du eine Callback-Methode auslösen, nachdem ein paar asynchrone Vorgänge (wie zum Beispiel Aufrufe von sendChat) fertig sind. Du kannst zwar nicht garantieren, in welcher Reihenfolge die Operationen abgeschlossen werden, aber du kannst garantieren, dass alle abgeschlossen sind, wenn der Callback des Semaphors ausgelöst wird.

Wenn du ein Semaphor benutzt, ruf v() vor jedem asynchronen Vorgang auf und p() als letzte Anweisung jedes asynchronen Vorgangs. Wenn du schon vorher weißt, wie viele Operationen du machen wirst, kannst du diese Zahl auch beim Semaphor-Konstruktor angeben und die Aufrufe von v weglassen.

Mit dieser speziellen Version eines asynchronen Semaphors kannst du auch einen Kontext für den Callback angeben (den Wert von this festlegen) und Parameter an den Callback übergeben. Die Parameter kannst du entweder im Konstruktor oder beim Aufruf von p angeben. (Die Parameter in p haben Vorrang vor den Parametern im Konstruktor.)

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) {
            // sem.p(arg1, arg2, ...) darf die an den Semaphore-Konstruktor übergebenen Argumente überschreiben
            if (arguments.length > 0) { parameters = arguments; }
            else { parameters = this.args; }

            this.callback.apply(this.context, parameters);
        }
    }
};

Anwendungsbeispiel:

var sem = new Semaphore(function(lastAsync) {
    log(lastAsync + ' completed last');
    log(this);
}, 2, { foo: 'bar', fizz: 'buzz' }, 'Sir not appearing in this callback');

sendChat('', '/roll d20', function(ops) {
    log('Executing first sendChat');
    sem.p('Erster sendChat-Aufruf');
});
sendChat('', '/roll d20', function(ops) {
    log('Ausführung des zweiten sendChat');
    sem.p('Zweiter sendChat-Aufruf');
});

Beispielausgabe:

„Zweites sendChat ausführen“
„Erstes sendChat ausführen“
„Erster sendChat-Aufruf zuletzt abgeschlossen“
{ foo: „bar“, fizz: „buzz“ }

Notizen & Charaktere

Eine Notiz erstellen

Wegen der Art und Weise, wie Textblöcke in Notizen behandelt werden, musst du beim Erstellen eines Notiz-Objekts zwei Schritte machen: Erst das Objekt erstellen, dann die Textblöcke festlegen:

//Erstelle eine neue Notiz, die für alle Spieler verfügbar ist
    var handout = createObj("handout", {
                name: "Der Name der Notiz",
                inplayerjournals: "all",
                archived: false
    });
    handout.set('notes', 'Notizen müssen nach der Erstellung der Notiz festgelegt werden.');
    handout.set('gmnotes', 'SL Notizen müssen ebenfalls erst nach dem Erstellen der Notiz festgelegt werden.');

Umgang mit der Kodierung

Die Textblöcke in Notizen (Notes und SL Notizen) und Charakteren (Bio und SL Notizen), die über die Benutzeroberfläche festgelegt werden, werden im Format x-www-form-urlencoded gespeichert. Du kannst das an der Reihenfolge der %##-Codes im ganzen Text erkennen:

„Erik%20%28Wikinger%2BWissenschaftler%29%20%5BKämpfer%3A%203%2C%20Zauberer%3A%202%5D“

Du kannst diesen Text im Chat schicken und er wird vom Browser übersetzt. Wenn du aber was am Text ändern musst, solltest du ihn so lassen, wie er eingegeben wurde:

Erik (Wikinger + Wissenschaftler) [Kämpfer: 3, Zauberer: 2]"

Du kannst den kodierten 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));
        }
    );
}

Werkzeugfunktionen

Werkzeugfunktionen erledigen allgemeine Aufgaben, die du vielleicht in vielen Skripten verwenden möchtest. Wenn du eine Funktion ganz oben in einem Skript-Tab platzierst, sollte diese Funktion für alle deine Skripte verfügbar sein, was deinen Aufwand reduziert. Hier ist 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. Diese Funktion hilft dabei.

Wenn du den Text aus der Eigenschaft gmnotes eines Graphic-Objekts, der Eigenschaft bio oder gmnotes eines Charakters oder der Eigenschaft notes oder gmnotes einer Notiz angibst, bekommst du eine Version, bei der die automatisch eingefügte Editorformatierung 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)){
    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;
};

Das erste Argument ist der Text, den du bearbeiten willst.

const text = decodeEditorText(token.get('gmnotes'));

Standardmäßig werden die Textzeilen durch \r\n getrennt.

Das optionale zweite Argument ist ein Objekt mit Optionen.

separator -- legt fest, womit Textzeilen getrennt werden sollen. Standard: \r\n

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

 

asArray-- gibt an, dass die Zeilen stattdessen als Array zurückgegeben werden sollen. Standard: false

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

HINWEIS: Verschachtelte <p>-Tags führen zu Problemen und stören die Dekodierung. Wenn du dieses Problem hast und Hilfe brauchst, schick The Aaron eine PN, er schaut sich das gern an.


getCleanImgsrc

Abhängigkeiten: Keine

Wenn du eine Bild-URL aus einem Spielmarker oder einer anderen Ressource hast, hol dir eine bereinigte Version davon, die du benutzen kannst, um über die API einen Spielmarker zu erstellen, oder undefined, wenn sie 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 kann nur Bilder erstellen, deren Quelle sich in einer Benutzerbibliothek befindet. Außerdem muss imgsrc die thumb-Version des Bildes sein.


getSenderForName

Abhängigkeiten: Keine

Wenn du einen Stringnamen angibst, gibt diese Funktion einen String zurück, der für den ersten Parameter von sendChat passt. Wenn es einen Charakter gibt, der denselben Namen wie ein Spieler hat, 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.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

Abhängigkeiten: levenshteinDistance

Mit ein paar Optionen versucht diese Funktion, den Teil „/w name “ eines Whispers für einen Aufruf von sendChat zu erstellen. Der Parameter options sollte entweder player: true oder character: true enthalten sowie einen Wert für id oder name. Spieler werden gegenüber Charakteren bevorzugt, wenn beides zutrifft, und IDs werden gegenüber Namen bevorzugt, wenn beide einen gültigen Wert haben. Wenn du einen Namen angibst, bekommt der Spieler oder Charakter, dessen Name dem eingegebenen Text am ähnlichsten ist, die Flüsternachricht.

options ist technisch optional, aber wenn du es weglässt (oder keine Kombination aus player/character + id/name angibst), gibt die Funktion eine leere Zeichenfolge zurück.

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) {
        // Alle Spieler oder Charaktere (je nach Bedarf) sortieren, deren Name den angegebenen Namen *enthält*,
        // dann sortiere sie danach, wie nah sie dem 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-Würfen durch ihr Gesamtergebnis ersetzen. Dies ist besonders nützlich für API-Befehle, an die der Benutzer möglicherweise Inline-Würfen als Parameter übergeben möchte.

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;
    }
}

Hier ist eine etwas kompliziertere Version, die auch die Umwandlung 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

Das Gegenteil von objectToStatusmarkers; verwandelt eine Zeichenfolge, die als Wert für die Eigenschaft statusmarkers eines Roll20-Spielmarker-Objekts verwendet werden kann, in ein normales JavaScript-Objekt.

Beachte, dass eine Statusmarker-Zeichenkette doppelte Statusmarker haben kann, während ein Objekt keine doppelten Eigenschaften haben darf.

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

Das Gegenteil von statusmarkersToObject; verwandelt ein normales JavaScript-Objekt in eine durch Kommas getrennte Zeichenfolge, die als Wert für die Eigenschaft statusmarkers eines Roll20-Spielmarker-Objekts verwendet werden kann.

Beachte, dass eine Statusmarker-Zeichenkette doppelte Statusmarker haben kann, während ein Objekt keine doppelten Eigenschaften haben darf.

function objectToStatusmarkers(obj) {
    return _.map(obj, function(value, key) {
                return key === 'dead' || value < 1 || value > 9 ? key : key + '@' + parseInt(value);
            })
            .join(',');
}

 


Underscore.js

Die Underscore.js-Website ist eher eine API-Referenz als eine Anleitung zur Nutzung der Bibliothek. Das ist zwar praktisch, um zu checken, welche Funktionen es gibt und welche Parameter sie brauchen, aber es hilft nicht wirklich, wenn man die Bibliothek voll ausnutzen will.

Sammlungen

Beim Schreiben von Skripten geht es oft darum, mit einer Sammlung von Dingen etwas zu tun. Wenn wir über Sammlungen reden, können das entweder Arrays sein:var foo = [0,1,10,"banana"];oder Objekte:var bar = { one: 1, two: 2, banana: 'fruit' };. Arrays werden mit Zahlen indiziert (normalerweise fängt man bei 0 an und zählt hoch), Objekte haben Eigenschaften, die man als Indizes benutzen kann:bar['banana'] === 'fruit'; // true!. Objekte funktionieren im Grunde wie assoziative Arrays aus anderen Sprachen.

Beispieldaten
Beispiel-Array:
var foo = [0,1,10,"banana"];

// Beispiel-Objekt
var bar = { one: 1, two: 2, banana: 'fruit' };
Aufruf einer Funktion mit jedem Element [ _.each() ]

Es kommt echt oft vor, dass man mit jedem Element einer Sammlung irgendwas machen muss. Normalerweise benutzt du for-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 das so cool macht, ist, dass der gleiche Code funktioniert, egal ob du ein Array oder ein Objekt benutzt:

_.each(bar, function(element){
  log('element is '+element);
„Element ist 1”
„Element ist 2”
„Element ist Obst”

Funktionen müssen nicht inline sein. Sie kriegen auch noch ein paar zusätzliche Parameter. (Weitere Parameter findest du 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 :: banana“
„Ein Objekt:“
„one :: 1“
„two :: 2“
„banana :: fruit“
Jedes Element umwandeln [ _.map() ]

Das Nächsthäufigste, was man mit einer Sammlung machen kann, ist, alle enthaltenen Elemente in Elemente eines anderen Typs umzuwandeln. Oft machen Leute das, indem sie eine neue Sammlung erstellen, dann mit einerfor-Schleife die erste Sammlung durchlaufen, den Wert umwandeln und ihn in den neuen Container packen. Das ist echt viel Code, den man mit Underscore's_.map() vereinfachen kann. Damit kann man eine Funktion auf eine Sammlung von Elementen anwenden und bekommt eine Sammlung der Ergebnisse. Wenn das ähnlich wie _.each() klingt, dann stimmt das, weil es tatsächlich die gleiche 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 (siehe „Konvertieren von Sammlungen“ weiter unten, um Objekte zu erhalten). Genau wie _.each() bekommt 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 :: banana']“
„Ein Objekt:“
„['one :: 1', 'two :: 2', 'banana :: fruit']“
War dieser Beitrag hilfreich?
16 von 18 fanden dies hilfreich