モジュール・パターンの公開
モジュール・パターンは、プライベート・メンバーとパブリック・メンバーをオブジェクト内にカプセル化することで、他の言語のクラスの概念をエミュレートしている。 Revealing Module Patternは、構文をより一貫したものにすることで、Module Patternを改良したものである。
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()); // "この変数はプライベートです"
myRevealingModule.setFunc('But I can change its value');
log(myRevealingModule.getFunc()); // "But I can change its value"
log(myRevealingModule.myVar); // "This variable is public"
myRevealingModule.myVar = 'So I can change it all I want';
log(myRevealingModule.myVar); // "So I can change it all I want".
メモ化
メモ化とは、与えられた入力に対する結果を保存し、同じ出力を2度計算することなく出せるようにする最適化手法である。 これは特に高価な計算で役に立つ。 もちろん、関数が同じ入力を受け取ることが稀であれば、メモ化の有用性は限られ、その一方でメモ化のためのストレージ要件は増大し続ける。
var factorialCache = {};
function factorial(n) {
var x;
n = parseInt(n || 0);
if (n < 0) {
throw 'Factorials of negative numbers are not well-defined';
}
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)スクリプトでは、キャッシュされた値は、ゲームセッション間で持続するステートに
保存される可能性があります。 しかし、インプットの可能性が多数ある場合、Roll20はステートの
使用を制限する可能性があることに注意してください。
非同期セマフォ
非同期セマフォを使うと、一連の非同期操作(sendChatの呼び出しなど)が完了した後にコールバック・メソッドを起動できる。 どのような順序で操作が完了するかは保証できないが、セマフォのコールバックが発火したときにすべての操作が完了していることは保証できる。
セマフォを使用する場合は、各非同期操作を呼び出す前にv()を
呼び出し、各非同期操作の最後のステートメントとしてp()を
呼び出す。 実行する操作の数が前もってわかっている場合は、その数をセマフォのコンストラクタに与え、vの
呼び出しを省略することもできる。
非同期セマフォのこの特別な実装では、コールバックにコンテキストを提供したり(この
値を設定する)、コールバックにパラメータを渡すこともできる。 パラメーターはコンストラクターか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、...)を許可する。) to override args passed to Semaphore constructor
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('First sendChat call');
});
sendChat(', '/roll d20', function(ops) {
log('Executing second sendChat');
sem.p('Second sendChat call');
});
出力例:
"Executing second sendChat"
"Executing first sendChat"
"First sendChat call completed last"
{ foo:"bar", fizz:"buzz" }
配布資料 & キャラクター
配布資料の作成
Handoutテキストブロックの扱い方のため、Handoutオブジェクトの作成は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));
}.
);
}.
ユーティリティ関数
ユーティリティ関数は、多くのスクリプトで使用する一般的なタスクを実行します。 ある関数をスクリプトタブの一番外側のスコープに配置すると、その関数はすべてのスクリプトで利用できるようになり、オーバーヘッドを減らすことができる。 以下はそのような機能の一部である。
デコードエディターテキスト
依存関係なし
新しいインゲーム・テキスト・エディタはかなり良いが、データ・セット内の大きなテキスト・エリアからの情報の読み取りに依存するMOD(API)スクリプトには問題がある。 この機能はその助けとなる。
Graphicの gmnotes
プロパティ、Characterの bio
またはgmnotes
プロパティ、Handoutの 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 = 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
.
オプションの第2引数は、オプションを含むオブジェクトである。
separator
-- テキストの行を何で区切るかを指定する。 デフォルト: \r\n
const text = decodeEditorText(token.get('gmnotes'),{separator:'<BR>'});
asArray
-- 行を配列として返すように指定する。 デフォルト:false
const text = decodeEditorText(token.get('gmnotes'),{asArray:true});
注意:<p>
タグを入れ子にすると、デコードが混乱し、壊れてしまう。 もしそのような問題に遭遇して助けが必要なら、アーロンにPMを送れば喜んで見てくれるだろう。
ゲットクリーン
依存関係なし
トークンや他のリソースから取得した画像URLが与えられた場合、API経由でトークンを作成するために使用できるクリーンバージョンを取得します。
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は
画像のサムバージョンでもなければならない。
送信者名
依存関係なし
文字列名を指定すると、この関数は sendChat の最初のパラメータに適切な文字列を返します。 選手と同じ名前のキャラクターがいる場合は、その選手が使用されます。 findObjsのoptions
パラメータと同じ構造の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;
}.
ゲットウィスパーターゲット
依存関係:levenshteinDistance
一連のオプションが与えられた場合、この関数は sendChat を呼び出す際のささやきの"/w name " 部分を作成しようとする。 options
パラメータには、player: trueか
character: trueの
どちらかと、idか
nameの
値を指定する。 両方がtrueの場合、キャラクターよりもプレーヤーが優先され、両方が有効な値を持つ場合、名前よりもidが優先される。 名前が指定された場合、指定された文字列に最も近い名前を持つプレイヤーまたはキャラクターに囁きが送られます。
optionsは
厳密にはオプションだが、省略した場合(あるいはplayer/character + id/nameの組み合わせを指定しなかった場合)、関数は空の文字列を返す。
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
アンダースコア.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);
"element is 1"
"element is 2"
"element is fruit"
関数はインラインである必要はない。 また、追加パラメータも受け取る。 (さらに詳しいパラメータはドキュメントを参照):
var logKeyValueMapping = function( value, key ) {
log(key + " :: " + value);
};
log("配列:");
_.each(foo, logKeyValueMapping);
log("オブジェクト:");
_.each(bar, logKeyValueMapping);
"An Array:"
"0 :: 0"
"1 :: 1"
"2 :: 10"
"3 :: banana"
"An Object:"
"one :: 1"
"two :: 2"
"banana :: fruit"
各要素の変換 [ _.map() ]。
コレクションで次によくやることは、含まれるすべてのアイテムを別の型のアイテムに変換することである。 多くの場合、別のコレクションを作成し、for
ループを使用して最初のコレクションを反復処理し、値を変換して新しいコンテナにプッシュすることでこれを行う。 Underscoreの _.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);
"An Array:"
"['0 :: 0', '1 :: 1', '2 :: 10', '3 :: banana']"
"オブジェクト:"
"['1 :: 1', '2 :: 2', 'バナナ :: フルーツ']"