모드 스크립트: 레시피 모음

다음은 전체 대본이 아닙니다. 이들은 자체적으로 스크립트를 생성하기 위한 것이 아니라, 비즈니스 로직과 함께 결합되어 완전한 스크립트를 작성하는 데 도움을 주기 위한 것입니다.

모듈 패턴 분석

모듈 패턴은 객체 내에 비공개 및 공개 멤버를 캡슐화함으로써 다른 언어의 클래스 개념을 모방합니다. ‘리빌링 모듈 패턴(Revealing 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); // "이 변수는 public입니다"
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;
}

모드 스크립트에서는 캐시된 값을 상태에 저장할 수 있으며, 이 값은 세션 간에도 유지됩니다. 이 상태는 게임 내 모든 스크립트가 공유하므로, 캐시 크기를 작게 유지하십시오.

비동기 세마포어

비동기 세마포어를 사용하면 일련의 비동기 작업(예: 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('첫 번째 sendChat 호출');
});
sendChat('', '/roll d20', function(ops) {
  log('두 번째 sendChat 실행 중');
  sem.p('두 번째 sendChat 호출');
});

출력 예시:

"두 번째 sendChat 실행 중"
"첫 번째 sendChat 실행 중"
"첫 번째 sendChat 호출이 마지막으로 완료됨"
{ foo: "bar", fizz: "buzz" }

핸드아웃 & 캐릭터

핸드아웃 만들기

핸드아웃 텍스트 블록이 처리되는 방식 때문에, 핸드아웃 객체를 생성하려면 두 단계로 진행해야 합니다. 먼저 객체를 생성한 다음, 텍스트 블록을 설정해야 합니다:

//모든 플레이어가 이용할 수 있는 새로운 핸드아웃을 생성합니다.
var handout = createObj("handout", {
  name: "핸드아웃 이름",
  inplayerjournals: "all",
  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, " "));
}

유틸리티 함수

유틸리티 함수는 여러 스크립트에서 자주 필요로 하는 일반적인 작업을 수행합니다. 스크립트 탭의 최외곽 범위에서 정의된 함수는 게임 내 모든 스크립트에서 볼 수 있는데, 이는 모든 스크립트가 하나의 전역 범위를 공유하기 때문입니다. 동일한 이름을 선언하는 두 스크립트는 서로를 덮어씁니다. 아래는 그러한 기능들의 일부입니다.

디코드 에디터 텍스트

의존성: 없음

게임 내 텍스트 편집기는 꽤 훌륭하지만, 데이터 세트에 포함된 큰 텍스트 영역 중 하나에서 정보를 읽어와야 하는 모드 스크립트의 경우 문제가 발생합니다. 이 기능이 그 일을 돕습니다.

그래픽의 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로 구분됩니다.

선택적 두 번째 인수는 옵션이 포함된 객체입니다.

  • 구분자 – 텍스트 줄을 무엇으로 구분할지 지정합니다. 기본값: \r\n
const text = decodeEditorText(token.get('gmnotes'),{separator:'<BR>'});
  • asArray – 각 줄을 배열로 반환하도록 지정합니다. 기본값: false
const text = decodeEditorText(token.get('gmnotes'),{asArray:true});

참고: 중첩된 <p> 태그는 처리되지 않습니다. 캐릭터 및 핸드아웃용 인물 소개, 메모, GM 노트는 HTML 블롭 형식입니다(콜백을 통해 읽어보세요). 'Graphic gmnotes'는 동기식 문자열이며, 이 %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;
};

참고: 모드 스크립트는 사용자 라이브러리에 있는 이미지만 생성할 수 있습니다. 썸네일 이미지는 더 이상 필요하지 않습니다. 저장소 URL은 재작성되므로, get("imgsrc")가 전달한 URL과 일치하지 않을 수 있습니다. ‘개체’ 항목 참조: imgsrc 제한 사항.

getSenderForName

의존성: 없음

이 함수는 주어진 문자열 이름을 받아 sendChat의 첫 번째 매개변수로 사용하기에 적합한 문자열을 반환합니다. 플레이어와 이름이 같은 캐릭터가 존재할 경우, 해당 플레이어가 사용됩니다. 또한 findObjs의 options 매개변수와 구조가 동일한 옵션 객체를 전달할 수도 있습니다.

function getSenderForName(name, options) {
  var character = findObjs({
    type: '캐릭터',
    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 이름 부분을 구성하려고 시도합니다. options 매개변수에는 player: true 또는 캐릭터: true 중 하나와 id 또는 name 중 하나의 값이 포함되어야 합니다. 플레이어와 캐릭터가 모두 존재할 경우 플레이어를 우선하며, ID와 이름이 모두 유효한 값을 가질 경우 ID를 우선합니다. 이름이 지정된 경우, 지정된 문자열과 가장 유사한 이름을 가진 플레이어 또는 캐릭터에게 귓속말이 전송됩니다.

options는 엄밀히 말해 선택 사항이지만, 이를 생략하거나(또는 플레이어/캐릭터와 ID/이름의 조합을 지정하지 않을 경우) 함수는 빈 문자열을 반환합니다.
















    function getWhisperTarget(options) {
  var nameProperty, targets, type;
  options = options || {};



    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를 스캔하여 인라인 롤을 해당 총 결과로 대체합니다. 이는 사용자가 인라인 롤을 매개변수로 전달하고자 하는 모드 스크립트 명령어에 특히 유용합니다.

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

다음은 테이블 항목을 텍스트로 변환하는 기능도 포함된 약간 더 복잡한 버전입니다:

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', '요소는 바나나']"

_.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명이 도움이 되었다고 했습니다.