더블크로스 LOIS 자동 등록 API/매크로
API를 등록해두고 매크로를 사용하면, 자동으로 시트에 등록이 되고 챗창에 올라옵니다. 한번에 시나리오 로이스도 삭제 가능합니다.
단순하게 만든 것이니 추가적으로 수정하고 싶으면 편하게 수정해서 사용해주세요.

1. 준비물
1) 더블크로스 시트
- 아본님 시트: https://x.com/eggpowder_abon/status/1096955967915192320
- roll20 기본 시트(by. 타텍님)
2) ROLL20 프로계정
API를 필수로 사용합니다.
3) AS(화자)
AS(화자)를 반드시 캐릭터로 바꿔둡니다.
2. 사용 방법
1) 작성
· !l 관계명,취득대상,p감정내용,n감정내용,p감정/n감정
혹은 !로 관계명,취득대상,p감정내용,n감정내용,p감정/n감정
· 편한 방법: 하단의 미리 적어둔 매크로를 사용합니다.
2) 삭제
· !l delete를 입력하면 시나리오 로이스를 모두 삭제합니다.
· 고정 로이스는 삭제되지 않습니다.
2. 설치 방법
1-1) Roll20 방을 만들고, 시트 템플릿을 'Custom'으로 맞춘 다음 아본님의 더블크로스 시트로 설정합니다.
1-2) Roll20 방을 만들 때, 시트를 Double Cross 3rd로 맞춥니다. 타텍님의 더블크로스 시트입니다.
2) 설정 > 모드(API) 스크립트 > New Script를 누른 뒤 해당 API를 작성하고, Save Script 버튼을 눌러주세요.

3) 게임에 입장하셔서, 매크로를 등록합니다.

4) 반드시 AS를 사용할 캐릭터로 바꾸고, 매크로를 사용한 뒤 제대로 작동하는지 확인합니다.
5) TRPG 세션에 사용합시다.
※쓸 때 주의사항
1. 반드시 AS는 로이스를 등록할 캐릭터로 맞춥니다.
2. 관계/이름/감정P/감정N/속성 중 한 칸이라도 내용이 있으면 다음 줄로 밀려서 작성되니, 반드시 PL들에게 줄이 비어있는지 확인 바랍니다.
아래로는 API와 매크로가 이어집니다.
* LOIS API(아본님)
on("chat:message", function(msg) {
if (msg.type == "api" && (msg.content.indexOf("!l ") === 0 || msg.content.indexOf("!로 ") === 0)) {
var character = findObjs({_type: "character", name: msg.who})[0];
if (!character) {
sendChat("API", "Error: 캐릭터를 찾을 수 없습니다.");
return;
}
var attributeNums = ["lois1_", "lois2_", "lois3_", "lois4_", "lois5_", "lois6_", "lois7_"];
var attributeSuffixes = ["rltn", "name", "pemo01", "nemo01", "elem"];
var messageContent = msg.content.split(" ").slice(1).join(" ").trim();
var attributeValues = messageContent.split(",");
// !l delete : 로이스 4~7번 슬롯 비우기
if (messageContent.toLowerCase() === "delete") {
var cleared = 0;
for (var j = 3; j < attributeNums.length; j++) {
for (var i = 0; i < attributeSuffixes.length; i++) {
var attrName = attributeNums[j] + attributeSuffixes[i];
var attrObj = findObjs({ _type: "attribute", _characterid: character.id, name: attrName })[0];
if (attrObj) {
attrObj.set("current", "");
cleared++;
}
}
}
var characterName = character.get("name") || msg.who || "???";
sendChat("System", characterName + "의 시나리오 로이스를 비웠습니다.", null, {noarchive: false});
return;
}
if (attributeValues.length < 5) {
sendChat("API", "Error: 입력값 부족.");
return;
}
var jetztlois = [attributeValues[0], attributeValues[1], attributeValues[2], attributeValues[3], attributeValues[4]];
var nextNumIndex = 0;
for (var i = 0; i < attributeValues.length; i++) {
var attributeName = "";
var attributeValue = attributeValues[i] ? attributeValues[i].trim() : "";
if (attributeValue === "") {
sendChat("API", "Error:" + attributeName + "를 못찾음.");
return;
}
for (var j = nextNumIndex; j < attributeNums.length; j++) {
attributeName = attributeNums[j] + attributeSuffixes[i];
var attributeObj = findObjs({ _type: "attribute", _characterid: character.id, name: attributeName })[0];
if (!attributeObj || attributeObj.get("current") === "") {
if (!attributeObj) {
attributeObj = createObj("attribute", { _characterid: character.id, name: attributeName, current: attributeValue });
} else {
attributeObj.set("current", attributeValue);
}
nextNumIndex = j;
break;
}
}
if (nextNumIndex === attributeNums.length) {
sendChat("API", "Error: 머임");
return;
}
}
// 채팅 메시지 전송
const loiStyle = "padding: 8px 12px; font-weight: bold; line-height: 170%; background: #D0CCFF; display: inline-block; text-decoration: none;";
try {
var chat_id = "character|" + character.id;
var characterName = character.get("name") || msg.who || "???";
sendChat(chat_id, "[img](https://files.d20.io/images/390944966/OJhlJm_H_qemso_rOa2NKA/med.png)", null, {noarchive: false});
sendChat(chat_id, "<span style='" + loiStyle + "'>" +
"【"+characterName + "】" +
"<br>" +
jetztlois[1] + "에게 관계: " +
jetztlois[0] + " P감정: " +
jetztlois[2] + " N감정: " +
jetztlois[3] + "중 " +
jetztlois[4] + "으로 로이스 취득" +
"</span>", null, {noarchive: false});
} catch (err) {
sendChat('error', '/w GM ' + err, null, {noarchive: true});
}
}
});* LOIS API(타텍님)
on("chat:message", function(msg) {
if (msg.type == "api" && (msg.content.indexOf("!l ") === 0 || msg.content.indexOf("!로 ") === 0)) {
var character = findObjs({_type: "character", name: msg.who})[0];
if (!character) {
sendChat("API", "Error: 캐릭터를 찾을 수 없습니다.");
return;
}
var attributeNums = ["lois1_", "lois2_", "lois3_", "lois4_", "lois5_", "lois6_", "lois7_"];
var attributeSuffixes = ["rel", "tit", "emot_good", "emot_bad", "desc"];
var messageContent = msg.content.split(" ").slice(1).join(" ").trim();
var attributeValues = messageContent.split(",");
// !l delete : 로이스 4~7번 슬롯 비우기
if (messageContent.toLowerCase() === "delete") {
var cleared = 0;
for (var j = 3; j < attributeNums.length; j++) {
for (var i = 0; i < attributeSuffixes.length; i++) {
var attrName = attributeNums[j] + attributeSuffixes[i];
var attrObj = findObjs({ _type: "attribute", _characterid: character.id, name: attrName })[0];
if (attrObj) {
attrObj.set("current", "");
cleared++;
}
}
}
var characterName = character.get("name") || msg.who || "???";
sendChat("System", characterName + "의 시나리오 로이스를 비웠습니다.", null, {noarchive: false});
return;
}
if (attributeValues.length < 5) {
sendChat("API", "Error: 입력값 부족.");
return;
}
var jetztlois = [attributeValues[0], attributeValues[1], attributeValues[2], attributeValues[3], attributeValues[4]];
var nextNumIndex = 0;
for (var i = 0; i < attributeValues.length; i++) {
var attributeName = "";
var attributeValue = attributeValues[i] ? attributeValues[i].trim() : "";
if (attributeValue === "") {
sendChat("API", "Error:" + attributeName + "를 못찾음.");
return;
}
for (var j = nextNumIndex; j < attributeNums.length; j++) {
attributeName = attributeNums[j] + attributeSuffixes[i];
var attributeObj = findObjs({ _type: "attribute", _characterid: character.id, name: attributeName })[0];
if (!attributeObj || attributeObj.get("current") === "") {
if (!attributeObj) {
attributeObj = createObj("attribute", { _characterid: character.id, name: attributeName, current: attributeValue });
} else {
attributeObj.set("current", attributeValue);
}
nextNumIndex = j;
break;
}
}
if (nextNumIndex === attributeNums.length) {
sendChat("API", "Error: 머임");
return;
}
}
// 채팅 메시지 전송
const loiStyle = "padding: 8px 12px; font-weight: bold; line-height: 170%; background: #D0CCFF; display: inline-block; text-decoration: none;";
try {
var chat_id = "character|" + character.id;
var characterName = character.get("name") || msg.who || "???";
sendChat(chat_id, "[img](https://files.d20.io/images/390944966/OJhlJm_H_qemso_rOa2NKA/med.png)", null, {noarchive: false});
sendChat(chat_id, "<span style='" + loiStyle + "'>" +
"【"+characterName + "】" +
"<br>" +
jetztlois[1] + "에게 관계: " +
jetztlois[0] + " P감정: " +
jetztlois[2] + " N감정: " +
jetztlois[3] + "중 " +
jetztlois[4] + "으로 로이스 취득" +
"</span>", null, {noarchive: false});
} catch (err) {
sendChat('error', '/w GM ' + err, null, {noarchive: true});
}
}
});
※ 간단 수정
1) 배경색인 연보라색은 # D0CCFF 를 찾아서 HEX 코드만 바꿔주세요.
2) 로이스 등록 이미지를 삭제하시고 싶은 분은 코드에서 형광 노란색 바탕이 걸린 부분만 지워주세요.
*매크로
!l ?{관계명| }, ?{취득 대상| }, ?{P감정|감정1|감정2|감정3|감정4|감정5|감정6|감정7|감정8|감정9|감정10|감정11|감정12|감정13|감정14|감정15|감정16|감정17|감정18|감정19|감정20|감정21|감정22|임의}, ?{N감정|감정1|감정2|감정3|감정4|감정5|감정6|감정7|감정8|감정9|감정10|감정11|감정12|감정13|감정14|감정15|감정16|감정17|감정18|감정19|감정20|감정21|감정22|임의}, ?{감정|P감정|N감정}※ 룰북의 내용을 적는 것이라 감정 1,2,3…으로 적어두었습니다. 코어 룰북 1권 P.298/299를 보고 감정을 직접 적어주세요.
즐거운 덥크 즐기시길!