モジュールパターンの解明
モジュールパターンは、オブジェクト内にプライベートメンバーとパブリックメンバーをカプセル化することで、他の言語におけるクラスの概念を模倣します。 リヴィールモジュールパターンは、構文の一貫性を高めることでモジュールパターンを改良したものです。
var myRevealingModule = myRevealingModule || (function() {
var privateVar = 'この変数はプライベートです',
publicVar = 'この変数はパブリックです';
function privateFunction() {
log(privateVar);
}
function publicSet(text) {
privateVar = text;
}
function publicGet() {
privateFunction();
}
return {
setFunc: publicSet,
myVar: publicVar,
getFunc: publicGet
};
}());
log(myRevealingModule.getFunc()); // "This variable is private"
myRevealingModule.setFunc('But I can change its value');
log(myRevealingModule.getFunc()); // "But I can change its value"
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;
}
Roll20 Mod(API)スクリプトでは、キャッシュされた値は状態(state)に保存される可能性があり、これはゲームセッション間で永続化されます。 ただし、入力候補が非常に多い場合、Roll20がステートの使用を制限する可能性があることに注意してください。
非同期セマフォ
非同期セマフォを使用すると、一連の非同期操作(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, ...) がセマフォコンストラクタに渡された引数を上書きできるようにする
if (arguments.length > 0) { parameters = arguments; }
else { parameters = this.args; }
this.callback.apply(this.context, parameters);
}
}
};
使用例:
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('最初のsendChat呼び出し');
});
sendChat('', '/roll d20', function(ops) {
log('2回目のsendChat実行中');
sem.p('2回目のsendChat呼び出し');
});
出力例:
"2回目のsendChatを実行中"
"1回目のsendChatを実行中"
"最初のsendChat呼び出しが最後に完了"
{ foo: "bar", fizz: "buzz" }
ハンドアウト & キャラクター
ハンドアウトの作成
ハンドアウトテキストブロックの処理方法のため、ハンドアウトオブジェクトの作成は2段階で行う必要があります。まずオブジェクトを作成し、次にテキストブロックを設定します:
//すべてのプレイヤーが利用可能な新しいハンドアウトを作成する
var handout = createObj("handout", {
name: "ハンドアウトの名前",
inplayerjournals: "all",
archived: false
});
handout.set('notes', 'ハンドアウト作成後にノートを設定する必要があります。');
handout.set('gmnotes', 'GMメモもハンドアウト作成後に設定する必要があります。');
エンコーディングの処理
ハンドアウト(メモとGM メモ)およびキャラクター(経歴とGM メモ)内のテキストブロックは、ユーザーインタフェースを通じて設定されたものがx-www-form-urlencoded形式で保存されます。 テキスト全体に散りばめられた %## コードの配列によってこれを認識できます:
"Erik%20%28Viking%2BScientist%29%20%5BFighter%3A%203%2C%20Wizard%3A%202%5D"
このテキストはチャットに送信でき、ブラウザによって翻訳されます。ただし、テキストに変更を加える必要がある場合は、入力されたままの状態で処理することをお勧めします:
エリック(バイキング+科学者)[戦士:3、魔法使い:2]
以下の関数でエンコードされたテキストをデコードできます:
var decodeUrlEncoding = function(t){
return t.replace(
/%([0-9A-Fa-f]{1,2})/g,
function(f,n){
return String.fromCharCode(parseInt(n,16));
}
);
}
ユーティリティ関数
ユーティリティ関数は、複数のスクリプトで繰り返し使用したい一般的なタスクを処理します。 スクリプトタブの最外スコープに関数を配置すれば、その関数はすべてのスクリプトで利用可能となり、オーバーヘッドを削減できます。 以下に、そのような機能の一部を挙げます。
decodeEditorText
依存関係:なし
新しいゲーム内テキストエディタはなかなか優れているが、データセット内の大きなテキスト領域から情報を読み取る必要があるMod(API)スクリプトにとっては問題となる。 この関数はそのためのものです。
グラフィックのgmnotesプロパティ、キャラクターのbioまたはgmnotesプロパティ、ハンドアウトのnotesまたはgmnotesプロパティからテキストを受け取ると、自動挿入されたエディター書式が除去されたバージョンを返します。
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);
}
/* どちらでもない */
return t;
};
最初の引数は処理対象のテキストです。
const text = decodeEditorText(token.get('gmnotes'));
デフォルトでは、テキストの行は \r\nで区切られます。
オプションの第二引数は、オプションを含むオブジェクトです。
区切り文字-- テキスト行を区切るために使用する文字を指定します。 デフォルト: \r\n
const text = decodeEditorText(token.get('gmnotes'),{separator:'<BR>'});
asArray-- 行を配列として返すように指定します。 デフォルト:false
const text = decodeEditorText(token.get('gmnotes'),{asArray:true});
注記: ネストされた `<` と `>` タグは混乱を招き、デコードを失敗させます。 その問題に遭遇して助けが必要な場合は、アーロンに直接メッセージを送ってください。喜んで対応します。
クリーンな画像ソースを取得する
依存関係:なし
トークンまたはその他のリソースから取得した画像URLに対し、API経由でトークン作成に使用可能なクリーンなバージョンを取得する。APIで作成できない場合はundefinedを返す。
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;
};
注:このAPIは、ソースがユーザーライブラリ内にある画像のみを作成できます。 imgsrcは必ず画像のサムネイル版である必要があります。
getSenderForName
依存関係:なし
文字列名を受け取り、sendChatの最初の引数に適した文字列を返す関数です。 プレイヤーと同名のキャラクターが存在する場合、そのキャラクターが使用される。 オプションオブジェクトを渡すこともできます。このオブジェクトは、findObjsのoptionsパラメータと全く同じ構造です。
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
依存関係:levenshteinDistance
一連のオプションが与えられた場合、この関数はsendChat呼び出し用の秘話において「/w name 」部分を構築しようと試みます。 optionsパラメータには、player: trueまたはcharacter: trueのいずれかと、idまたはname のいずれかの値を含める必要があります。 両方が真の場合、プレイヤーがキャラクターより優先され、両方に有効な値がある場合、IDが名前より優先される。 名前が指定された場合、指定された文字列に最も近い名前を持つプレイヤーまたはキャラクターにささやきが送信されます。
optionsは技術的にはオプションですが、これを省略した場合(またはプレイヤー/キャラクターとID/名前の組み合わせを指定しない場合)、関数は空の文字列を返します。
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) {
// 指定された名前を *含む* すべてのプレイヤーまたはキャラクター(適宜)をソートし、
// 指定された名前に近い順にソートします。
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(API)コマンドにおいて有用です。
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;
}
}
以下は、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.replace(k,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 < 1 || value > 9 ? key : key + '@' + parseInt(value);
})
.join(',');
}
アンダースコア・ジェイエス
アンダースコア.js Underscore.js ウェブサイトは、ライブラリの使用ガイドというより、APIリファレンスとしての側面が強い。 利用可能な関数やその引数を確認するには有用だが、ライブラリの機能を最大限に活用しようとする者にとっては役立たない。
コレクション
スクリプトを書くことは、しばしば複数の対象に対して何らかの操作を行うことを伴う。 コレクションについて話すとき、それらは配列(例:var foo = [0,1,10,"banana"];)またはオブジェクト(例:var bar = { one: 1, two: 2, banana: 'fruit' };)のいずれかです。 配列は数値でインデックス付けされます(通常は0から始まり順にカウントアップ)。オブジェクトにはインデックスとして使用できるプロパティがあります:bar['banana'] === 'fruit'; // true!. オブジェクトは、他の言語における連想配列のように機能します。
サンプル配列:
var foo = [0,1,10,"banana"];
// サンプルオブジェクト
var bar = { one: 1, two: 2, banana: 'fruit' };
各要素に対して関数を呼び出す [ _.each() ]
コレクションの各要素に対して何らかの操作を実行する必要が生じることは非常に一般的です。 通常、人々はforループや類似のものを使用します。 アンダースコアは _.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("An Array:");
_.each(foo, logKeyValueMapping);
log("An Object:");
_.each(bar, logKeyValueMapping);
"配列:"
"0 :: 0"
"1 :: 1"
"2 :: 10"
"3 :: banana"
"オブジェクト:"
"one :: 1"
"two :: 2"
"banana :: fruit"
各要素をマップする [ _.map() ]
コレクションに対して次に最もよく行われる操作は、含まれるすべてのアイテムを別の型のアイテムに変換することである。 多くの場合、人々は別のコレクションを作成し、forループで最初のコレクションを反復処理しながら値を変換し、新しいコンテナに追加することでこれを実現します。 これはかなりの量のコードですが、アンダースコアの`_.map()` を使えば簡略化できます。これは要素のコレクションに対して関数を適用し、結果のコレクションを取得する方法です。 それが_.each()に似ているように聞こえるなら、それは実際に同じシグネチャを持っているからです。
var res = _.map(foo, function(element){
return 'element is '+element;
});
log(res);
['要素は0','要素は1','要素は10','要素はバナナ']
_.map() の戻り値は常に結果の配列です(オブジェクトを取得するには、以下の「コレクションの変換」を参照)。_.each() と同様に、この関数はより多くの引数を受け取り、個別に定義することも可能です。
[var getKeyValueMapping = function(value, key) {
return key + " :: " + value;
};
log("An Array:");
var resA = _.map(foo, getKeyValueMapping);
log(resA);
log("An Object:");
var resB = _.map(bar, getKeyValueMapping);
log(resB);
"配列:"
"['0 :: 0', '1 :: 1', '2 :: 10', '3 :: banana']"
"オブジェクト:"
"['one :: 1', 'two :: 2', 'banana :: fruit']"