Mod-Skripte: 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() {
    return privateVar;
  }
  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 negativer Zahlen sind nicht eindeutig 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 Mod-Skript können zwischengespeicherte Werte in state gespeichert werden, was zwischen Sitzungen bestehen bleibt. state wird von jedem Skript im Spiel gemeinsam genutzt, also halte den Cache klein.

Asynchrones Semaphor

Mit einem asynchronen Semaphor kannst du eine Callback-Methode auslösen, nachdem eine Reihe asynchroner Operationen (wie beispielsweise Aufrufe von „sendChat“) abgeschlossen sind. Zwar kannst du nicht garantieren, in welcher Reihenfolge die Operationen abgeschlossen werden, aber du kannst sicherstellen, dass sie alle abgeschlossen sind, wenn der Callback des Semaphors ausgelöst wird.

Wenn du ein Semaphor verwendest, rufe v() vor jedem asynchronen Vorgang auf und p() als letzte Anweisung jedes asynchronen Vorgangs. Wenn die Anzahl der Operationen, die du ausführen wirst, im Voraus bekannt ist, kannst du diese Zahl auch im Konstruktor des Semaphors angeben und die Aufrufe von v() weglassen.

Bei dieser speziellen Implementierung eines asynchronen Semaphors kannst du außerdem einen Kontext für den Callback angeben (den Wert von this festlegen) sowie Parameter an den Callback übergeben. Die Parameter können entweder im Konstruktor oder beim Aufruf von p übergeben werden. (Parameter in p haben Vorrang vor 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) {
      // Ermöglicht es sem.p(arg1, arg2, ...), die an den Semaphore-Konstruktor übergebenen Argumente zu ü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 + ' wurde als letztes abgeschlossen');
  log(this);
}, 2, { foo: 'bar', fizz: 'buzz' }, 'Sir erscheint nicht in diesem Callback');
sendChat('', '/roll d20', function(ops) {
  log('Ersten sendChat-Aufruf ausführen');
  sem.p('Erster sendChat-Aufruf');
});
sendChat('', '/roll d20', function(ops) {
  log('Zweiten sendChat ausführen');
  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 allen Spielern zur Verfügung steht
var handout = createObj("handout", {
  name: "Der Name der Notiz",
  inplayerjournals: "all",
  archived: false
});
handout.set('notes', 'Notizen müssen nach dem Erstellen der Notiz festgelegt werden.');
handout.set('gmnotes', 'SL Notizen müssen ebenfalls nach dem Erstellen der Notiz festgelegt werden.');

Umgang mit der Kodierung

Die Textblöcke in Notizen (Notizen und SL Notizen) und Charakteren (Bio und SL Notizen), die über die Oberfläche festgelegt werden, werden im Format x-www-form-urlencoded gespeichert. Das erkennst du an der Abfolge von %##-Codes im gesamten Text:

„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 decodeURIComponent(t.replace(/\+/g, " "));
}

Werkzeugfunktionen

Werkzeugfunktionen erledigen gängige Aufgaben, die du vielleicht in vielen Skripten brauchst. Eine Funktion im äußersten Gültigkeitsbereich eines Skript-Tabs ist für jedes Skript im Spiel sichtbar, da sie sich denselben globalen Gültigkeitsbereich teilen. Zwei Skripte, die denselben Namen haben, überschreiben sich gegenseitig. Hier ist eine Auswahl solcher Funktionen.

decodeEditorText

Abhängigkeiten: Keine

Der Texteditor im Spiel ist ziemlich gut, hat aber ein Problem bei Mod-Skripten, die darauf angewiesen sind, Informationen aus einem der großen Textbereiche im Datensatz auszulesen. 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, gibt diese Funktion eine Version zurück, 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 = decodeURIComponent(w);
  }
  if(/^<p>/.test(w)){
    let lines = w.match(/<p>.*?<\/p>/g);
    if (!lines) return t;
    lines = lines.map(l => l.replace(/^<p>(.*?)<\/p>$/, '$1'));
    return o.asArray ? lines : lines.join(o.separator);
  }
  /* weder noch */
  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 voneinander 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 werden nicht verarbeitet. bio, notes und gmnotes von Charakteren und Notizen sind HTML-Blobs (lies sie mit einem Callback aus). gmnotes von Graphic ist eine synchrone Zeichenkette und das Feld, für das diese %3Cp%3E-Prüfung gedacht ist.

getCleanImgsrc

Abhängigkeiten: Keine

Wenn du eine Bild-URL aus einem Spielmarker oder einer anderen Ressource hast, erhältst du eine bereinigte Version davon, die du zum Erstellen eines Spielmarkers über ein Mod-Skript verwenden kannst – oder undefined, falls sie nicht per Mod-Skript erstellt werden kann.

var getCleanImgsrc = function (imgsrc) {
  var parts = imgsrc.match(/(.*\/images\/.*)(thumb|med|original|max)([^?]*)(\?[^?]+)?$/);
  if(parts) {
    return parts[1]+parts[2]+parts[3]+(parts[4]?parts[4]:`?${Math.round(Math.random()*9999999)}`);
  }
  return;
};

Hinweis: Mod-Skripte können Bilder nur aus einer Benutzerbibliothek erstellen. Die Größe thumb wird nicht mehr benötigt. get("imgsrc") stimmt möglicherweise nicht mit der von dir übergebenen URL überein, da Speicher-URLs umgeschrieben werden. Siehe Objekte: Einschränkungen für imgsrc.

getSenderForName

Abhängigkeiten: Keine

Wenn du einen Stringnamen angibst, gibt diese Funktion einen String zurück, der für den ersten Parameter von sendChat geeignet ist. 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.endsWith(' (GM)') ? name.slice(0, -5) : name
  }, options)[0];
  if (player) {
    return 'player|' + player.id;
  }
  if (character) {
    return 'character|' + character.id;
  }
  return name;
}

getWhisperTarget

Abhängigkeiten: levenshteinDistance

Anhand einer Reihe von Optionen versucht diese Funktion, den Teil /w name eines Flüsterns für einen Aufruf von sendChat zu erstellen. Der Parameter options sollte entweder player: true oder character: true sowie einen Wert für id oder name enthalten. 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 gesehen 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) {
    // Sortiere alle Spieler oder Charaktere (je nach Fall), deren Name den angegebenen Namen *enthält*,
    // und sortiere sie anschließend danach, wie nah ihr Name an dem angegebenen Namen liegt.
    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 durchsucht msg.content und ersetzt Inline-Würfe durch deren Gesamtergebnis. Das ist besonders nützlich für Mod-Skript-Befehle, denen der Benutzer möglicherweise Inline-Würfe 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.split(index).join(String(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.split(k).join(String(v));
      },msg.content)
      .value();
  } else {
    return msg.content;
  }
}

statusmarkersToObject

Das Gegenteil von objectToStatusmarkers; wandelt eine Zeichenkette, die als Wert für die Eigenschaft statusmarkers eines Roll20-Spielmarker-Objekts geeignet ist, in ein ganz normales JavaScript-Objekt um.

Beachte, dass eine Statusmarker-Zeichenkette doppelte statusmarkers enthalten kann, während ein Objekt keine doppelten Eigenschaften enthalten 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; wandelt ein normales JavaScript-Objekt in eine durch Kommas getrennte Zeichenfolge um, die als Wert für die Eigenschaft statusmarkers eines Roll20-Spielmarker-Objekts verwendet werden kann.

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

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

Underscore.js

Die Underscore.js-Website ist eher ein Nachschlagewerk als eine Anleitung zur Verwendung der Bibliothek. Das ist zwar nützlich, um nachzuschlagen, welche Funktionen verfügbar sind und welche Parameter sie akzeptieren, hilft aber niemandem, der versucht, das volle Potenzial der Bibliothek auszuschöpfen.

Sammlungen

Beim Schreiben von Skripten geht es oft darum, eine Reihe von Dingen zu bearbeiten. Sammlungen können Arrays sein, z. B. var foo = [0, 1, 10, "banana"];, oder Objekte, z. B. var bar = { one: 1, two: 2, banana: "fruit" };. Arrays werden mit Zahlen indiziert, wobei die Indizierung normalerweise bei 0 beginnt. Objekte werden anhand ihrer Eigenschaftsnamen indiziert: bar["banana"] === "fruit". Objekte verhalten sich 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 sehr häufig vor, dass man mit jedem Element einer Sammlung eine bestimmte Operation durchführen muss. Normalerweise benutzt man dafür for-Schleifen oder Ähnliches. Underscore bietet die Methode _.each(), mit der du eine Funktion auf jedes Element einer Sammlung anwenden kannst.

_.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 die Leute das so, dass sie erst mal eine neue Sammlung anlegen, dann mit einer for-Schleife die erste Sammlung durchlaufen, den Wert umwandeln und ihn in den neuen Container einfügen. Das ist eine Menge Code, der sich mit _.map() von Underscore vereinfachen lässt – damit kannst du eine Funktion auf eine Sammlung von Elementen anwenden und eine Sammlung der Ergebnisse erhalten. Wenn dir das ähnlich wie _.each() vorkommt, dann liegt das daran, dass es tatsächlich so ist – es hat nämlich dieselbe Signatur.

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

Der Rückgabewert von _.map() ist immer ein Array mit den Ergebnissen (siehe „Umwandeln von Sammlungen“ weiter unten für Objekte). Genau wie bei _.each() erhält die Funktion weitere 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']“

Sammlungen umwandeln [ _.reduce() ]

_.reduce() reduziert eine Sammlung auf einen einzigen Wert, indem eine Funktion mit einem Akkumulator und jedem Element aufgerufen wird. Die vollständige Signatur und Beispiele findest du in der Underscore-Dokumentation.

War dieser Beitrag hilfreich?
17 von 20 fanden dies hilfreich