Modスクリプト:クックブック

以下は完全な台本ではありません。これらは、単独でスクリプトを作成するためではなく、ビジネスロジックと組み合わせて、完全なスクリプトの作成を支援することを目的としています。

モジュールパターンの解明

モジュールパターンは、オブジェクト内にプライベートメンバーとパブリックメンバーをカプセル化することで、他の言語におけるクラスの概念を模倣しています。 「Revealing Module Pattern」は、構文の一貫性を高めることで、Module Pattern を改良したものです。

var myRevealingModule = myRevealingModule || (function() {
  var privateVar = 'この変数はプライベートです',
    publicVar = 'この変数はパブリックです';
  function privateFunction() {
    log(privateVar);
  }
  function publicSet(text) {
    privateVar = text;
  }
  function publicGet() {
    return privateVar;
  }
  return {
    setFunc: publicSet,
    myVar: publicVar,
    getFunc: publicGet
  };
}());
log(myRevealingModule.getFunc()); // "この変数はプライベートです"
myRevealingModule.setFunc('でも、その値は変更できます');
log(myRevealingModule.getFunc()); // "でも、その値は変更できます"
log(myRevealingModule.myVar); // "この変数はパブリックです"
myRevealingModule.myVar = 'だから、好きなだけ変更できます';
log(myRevealingModule.myVar); // "だから、好きなだけ変更できます"

メモ化

メモ化とは、特定の入力に対する結果を保存しておくことで、同じ出力を計算し直すことなく生成できるようにする最適化手法です。これは、計算コストが高い場合において特に有用です。もちろん、関数が同じ入力を受け取るケースが稀であるならば、メモ化のユーティリティは限定的である一方、それに伴うストレージ要件は増え続けることになります。

var factorialCache = {};
function factorial(n) {
  var x;
  n = parseInt(n || 0);
  if (n < 0) {
    throw '負の数の階乗は定義が不明確です';
  }
  if (n === 0) {
    return 1;
  } else if (factorialCache[n]) {
    return factorialCache[n];
  }
  x = factorial(n - 1) * n;
  factorialCache[n] = x;
  return x;
}

Modスクリプトでは、キャッシュされた値を「state」に保存することができ、これによりセッションをまたいで値が保持されます。この状態はゲーム内のすべてのスクリプトで共有されるため、キャッシュのサイズを小さく保ってください。

非同期セマフォ

非同期セマフォを使用すると、一連の非同期操作(sendChat の呼び出しなど)が完了した後に、コールバックメソッドを実行することができます。演算がどの順序で完了するかは保証できませんが、セマフォのコールバックが実行される時点では、すべての演算が完了していることは保証できます。

セマフォを使用する場合は、各非同期操作を呼び出す前にv() を呼び出し、各非同期操作の最後の文としてp() を呼び出してください。実行する操作の回数があらかじめ分かっている場合は、その回数をセマフォのコンストラクタに指定し、v() の呼び出しを省略することもできます。

この非同期セマフォの実装では、コールバックのコンテキスト(this の値を設定)を指定できるほか、コールバックにパラメータを渡すことも可能です。パラメータは、コンストラクタ内か、p の呼び出しのいずれかで指定できます。 (p内のパラメータは、コンストラクタ内のパラメータよりも優先されます。)

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, ...) が Semaphore コンストラクタに渡された引数を上書きできるようにする
      if (arguments.length > 0) { parameters = arguments; }
      else { parameters = this.args; }
      this.callback.apply(this.context, parameters);
    }
  }
};

使用例:

var sem = new Semaphore(function(lastAsync) {
  log(lastAsync + ' 最後に完了した');
  log(this);
} , 2, { foo: 'bar', fizz: 'buzz' }, 'Sir not appearing in this callback');
sendChat('', '/roll d20', function(ops) {
  log('最初の sendChat を実行中');
  sem.p('1回目の sendChat 呼び出し');
});
sendChat('', '/roll d20', function(ops) {
  log('2回目の sendChat を実行中');
  sem.p('2回目の sendChat 呼び出し');
});

出力例:

"2回目のsendChatの実行"
"1回目のsendChatの実行"
"1回目のsendChatの呼び出しが最後に完了しました"
{ foo: "bar", fizz: "buzz" }

ハンドアウト & キャラクター

ハンドアウトの作成

ハンドアウトのテキストブロックの処理方法の都合上、ハンドアウトオブジェクトの作成は2つの手順で行う必要があります。まずオブジェクトを作成し、次にテキストブロックを設定します:


  //すべてのプレイヤーが利用可能な新しいハンドアウトを作成する
var handout = createObj("handout", {
  name:
  archived: false
});
handout.set('notes', 'メモはハンドアウト作成後に設定する必要があります。');
handout.set('gmnotes', 'GM メモもハンドアウト作成後に設定する必要があります。');

エンコーディングの取り扱い

「ハンドアウト」(ノートおよびGM メモ)および「キャラクター」(プロフィールおよびGM メモ)内の、インタフェースを通じて設定されたテキストブロックは、x-www-form-urlencoded形式で保存されます。これは、本文中に散見される%##コードの並びから確認できます:

"エリック%20%28バイキング%2B科学者%29%20%5B戦士%3A%203%2C%20魔法使い%3A%202%5D"

このテキストはチャットに送信でき、ブラウザによって翻訳されますが、テキストに変更を加える必要がある場合は、入力されたままの状態で処理することをお勧めします:

「エリック(バイキング+科学者) [ファイター:3、ウィザード:2]」

以下の関数を使って、エンコードされたテキストをデコードすることができます:

var decodeUrlEncoding = function(t) {
  return decodeURIComponent(t.replace(/\+/g, " "));
}

ユーティリティ関数

ユーティリティ関数は、多くのスクリプトで必要となるような一般的な処理を実行します。スクリプトタブの最外側のスコープにある関数は、ゲーム内のすべてのスクリプトから参照可能です。これは、すべてのスクリプトが1つのグローバルスコープを共有しているためです。同じ名前を宣言する2つのスクリプトは、互いに上書きし合います。以下に、そのような機能の一部を挙げます。

decodeEditorText

依存関係:なし

ゲーム内のテキストエディタはかなり使いやすいのですが、データセット内の大きなテキスト領域の1つから情報を読み取る必要があるModスクリプトにとっては、ある問題が生じます。この関数はそのためのものです。

グラフィックの 「gmnotes」プロパティ、キャラクターの「bio」または「gmnotes」プロパティ、あるいはハンドアウトの「notes」または「gmnotes」プロパティからテキストを受け取り、エディタによって自動的に挿入された書式設定を削除したバージョンを返します。

const decodeEditorText = (t, o) => {
  let w = t;
  o = Object.assign({ separator: '\r\n', asArray: false }, o);
  /* トークン GM メモ */
  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);
  }
  /* どちらもなし */
  return t;
 } ;

最初の引数は処理対象のテキストです。

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

デフォルトでは、テキストの行は \r\nで区切られます。

オプションの第二引数は、オプションを含むオブジェクトです。

  • separator– テキストの行を区切る記号を指定します。デフォルト: \r\n
const text = decodeEditorText(token.get('gmnotes'),{separator:'<BR>'});
  • asArray– 行を配列として返すように指定します。デフォルト:false
const text = decodeEditorText(token.get('gmnotes'),{asArray:true});

注:ネストされた `<` `>` タグは処理されません。キャラクターやハンドアウト、メモ、GM用メモはHTMLの塊です(コールバックを使って読み込んでください)。 「Graphicgmnotes」は同期文字列であり、この%3Cp%3Eチェックの対象となるフィールドです。

クリーンな画像ソースを取得する

依存関係:なし

トークンやその他のリソースから取得した画像のURLに対し、Modスクリプトを介してトークンを作成するために使用できるクリーンなバージョンを取得します。Modスクリプトでトークンを作成できない場合は、undefinedを返します。

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

注:Modスクリプトでは、ユーザーライブラリ内の画像のみを作成できます。サムネイル画像は不要になりました。ストレージのURLは書き換えられるため、get("imgsrc")で取得されるURLが、渡されたURLと一致しない可能性があります。「オブジェクト」の「imgsrcの制限」を参照してください。

getSenderForName

依存関係:なし

文字列 name が与えられると、この関数はsendChat の最初の引数として適切な文字列を返します。プレイヤーと同名のキャラクターが存在する場合、そのキャラクターが使用される。また、findObjs のoptionsパラメータとまったく同じ構造を持つオプションオブジェクトを渡すこともできます。

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

依存関係: levenshteinDistance

一連の選択肢が与えられると、この関数はsendChat 呼び出し用の秘話の/w name部分を生成しようとします。 optionsパラメータには、player: trueまたはキャラクター: trueのいずれかと、idまたはname のいずれかの値を指定する必要があります。両方が真の場合、プレイヤーがキャラクターより優先され、両方に有効な値がある場合、IDが名前より優先される。名前が指定された場合、指定された文字列に最も近い名前を持つプレイヤーまたはキャラクターにささやきが送信されます。

optionsは厳密には省略可能ですが、これを省略した場合(またはプレイヤー/キャラクターと ID/名前の組み合わせを指定しなかった場合)、この関数は空の文字列を返します。




  function getWhisperTarget(options) {
  var nameProperty, targets, type;
  options = options || {};
  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) {
    // 指定された名前が*含まれている*すべてのプレイヤーまたはキャラクター(状況に応じて)をソートし、
    // 指定された名前にどれだけ近いかでそれらをソートします。
    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 '';
}

プロセスインラインロール

この関数は、msg.contentをスキャンし、インラインのロール値をその合計値に置き換えます。これは、ユーザーがインラインロールをパラメータとして渡したい場合のあるModスクリプトのコマンドにおいて、特に有用です。

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

以下は、tableItemsをテキストに変換する処理も追加した、やや複雑なバージョンです:

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

ステータスマーカーオブジェクト化

objectToStatusmarkers の逆関数です。Roll20 コマオブジェクトのstatusmarkersプロパティの値として使用できる文字列を、ごく一般的な JavaScript オブジェクトに変換します。

ステータスマーカーの文字列には重複するステータスマーカーを含めることができますが、オブジェクトには重複するプロパティを含めることはできない点に注意してください。

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;
  }, {});
}

オブジェクトステータスマーカー

statusmarkersToObject の逆関数です。通常の JavaScript オブジェクトを、Roll20 コマオブジェクトのstatusmarkersプロパティの値として使用できる、コンマ区切りの文字列に変換します。

ステータスマーカーの文字列には重複するステータスマーカーを含めることができますが、オブジェクトには重複するプロパティを含めることはできない点に注意してください。

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

Underscore.jsのウェブサイトは、ライブラリの使用方法に関するガイドというよりは、むしろリファレンスとしての性格が強いものです。利用可能な関数や、それらの関数が受け付けるパラメータを調べるには便利ですが、ライブラリの機能を最大限に活用しようとしている人にとっては、あまり役に立ちません。

コレクション

スクリプトを書く際には、多くの場合、一連のオブジェクトに対して何らかの処理を行うことになります。コレクションには、配列(var foo = [0, 1, 10, "banana"];)やオブジェクト(var bar = { one: 1, two: 2, banana: "fruit" };)があります。配列は番号で参照され、通常は0から始まります。オブジェクトはプロパティ名によってインデックス付けされます:bar["banana"] === "fruit"。オブジェクトは、他の言語の連想配列のように動作します。

サンプルデータ

// 配列の例:
var foo = [0, 1, 10, "banana"];
// オブジェクトの例
var bar = { one: 1, two: 2, banana: 'fruit' };

各要素に対して関数を呼び出す [ _.each() ]

コレクションの各要素に対して何らかの操作を行う必要があることは、ごく一般的なことです。通常、forループなどを使用します。 Underscore には_.each() というメソッドがあり、コレクションの各要素を引数として関数を呼び出すことができます。

_.each(foo, function(element){
  log('element is '+element);
});
"element is 0"
"element is 1"
"element is 10"
"element is banana"

このコードが非常に強力である理由は、配列を使用している場合でもオブジェクトを使用している場合でも、まったく同じコードが機能する点にあります:

_.each(bar, function(element){
  log('element is '+element);
});
"要素は1"
"要素は2"
"要素は果物"

関数はインラインである必要はありません。それらは追加のパラメータも受け取ります。(さらに多くのパラメータについては、ドキュメントを参照してください。)

var logKeyValueMapping = function( value, key ) {
  log(key + " :: " + value);
};
log("配列:");
_.each(foo, logKeyValueMapping);
log("オブジェクト:");
_.each(bar, logKeyValueMapping);
「配列:」
「0 :: 0」
「1 :: 1」
「2 :: 10」
「3 :: banana」
「オブジェクト:」
「one :: 1」
「two :: 2」
「banana :: fruit」

各要素をマップする [ _.map() ]

コレクションに対して次に最もよく行われる操作は、含まれるすべてのアイテムを別の型のアイテムに変換することである。多くの場合、人々は別のコレクションを作成し、forループを使って最初のコレクションを反復処理しながら、値を変換して新しいコンテナに追加するという方法をとることがあります。これはかなりの量のコードですが、Underscoreの_.map()を使えば簡略化できます。_.map()は、関数を要素のコレクション全体に適用し、その結果のコレクションを取得する方法です。もしこれが _.each() に似ていると感じるなら、それは実際、同じシグネチャを持っているからです。

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

_.map()の戻り値は、常に結果の配列となります(オブジェクトについては、後述の「コレクションの変換」を参照してください)。 _.each() と同様に、この関数にはより多くの引数が渡され、個別に定義することもできます。

var getKeyValueMapping = function( value, key ) {
  return key + " :: " + value;
};
log("配列:");
var resA = _.map(foo, getKeyValueMapping);
log(resA);
log("オブジェクト:");
var resB = _.map(bar, getKeyValueMapping);
log(resB);
"配列:"
"['0 :: 0', '1 :: 1', '2 :: 10', '3 :: banana']"
"オブジェクト:"
"['one :: 1', 'two :: 2', 'banana :: fruit']"

コレクションの変換 [ _.reduce() ]

_.reduce()は、アキュムレータと各要素を引数として関数を呼び出すことで、コレクションを単一の値に集約します。完全なシグネチャや使用例については、Underscoreのドキュメントを参照してください。

この記事は役に立ちましたか?
20人中17人がこの記事が役に立ったと言っています