Roll20 uses cookies to improve your experience on our site. Cookies enable you to enjoy certain features, social sharing functionality, and tailor message and display ads to your interests on our site and others. They also help us understand how our site is being used. By continuing to use our site, you consent to our use of cookies. Update your cookie preferences .
×
Create a free account

I'm looking for someone to make an API for this YouTube video that I really want to do

<a href="https://www.youtube.com/watch?v=Q6Z0a3eb-8c" rel="nofollow">https://www.youtube.com/watch?v=Q6Z0a3eb-8c</a> &nbsp; &nbsp; I &nbsp; want to create a conversation where the screen comes up like this. The character image
<a href="https://github.com/kibkibe/roll20-api-scripts/blob/master/visual_dialogue/README.md" rel="nofollow">https://github.com/kibkibe/roll20-api-scripts/blob/master/visual_dialogue/README.md</a> based on the YT video - web tabs, found this link also, it seems the OP has messaged the YT creator who recently replied and posted this link <a href="https://www.postype.com/@rnfckck/post/14493705" rel="nofollow">https://www.postype.com/@rnfckck/post/14493705</a> for non Koreans, get your google translate ready.
1757817286

Edited 1757817317
Here is the code for any of the scripter writers to review and understand what he/she did and maybe improve on it skip the first several lines that are in korean but the code itself is not. /* <a href="https://github.com/kibkibe/roll20-api-scripts/tree/master/visual_dialogue" rel="nofollow">https://github.com/kibkibe/roll20-api-scripts/tree/master/visual_dialogue</a> */ /* (visual_dialogue.js) 220118 코드 시작 */ // define: global constant state.api_tag = "&lt;a href=\"#vd-permitted-api-chat\"&gt;&lt;/a&gt;"; state.vd_divider = "ℍ"; state.last_displayed_time = 0; // /define: global constant // define: option const vd_setting = { // option: 한 화면에 표시할 수 있는 스탠딩 이미지의 최대 개수를 설정합니다. // 이 숫자를 넘어가면 엑스트라, 혹은 채팅기록이 가장 오래된 캐릭터의 스탠딩이 삭제되고 그 위치에 새 스탠딩이 추가됩니다. max_number: 5, // option: 표시할 스탠딩 이미지들의 가로 사이즈입니다. width: 420, // option: 표시할 스탠딩 이미지들의 세로 사이즈입니다. height: 420, // option: 스탠딩 이미지의 가로 너비 중 화면 밖으로 빠져나가지 않도록 보장할 가로 사이즈입니다. fit_width: 200, // option: 캐릭터들이 여러 감정표현을 사용할지(true) 대표 스탠딩 하나만 사용할지(false) 설정합니다. // false일 경우 deck_name에 설정한 카드 덱에서 모든 캐릭터의 스탠딩 이미지를 가져옵니다. use_emotion: true, // option: /as를 이용해 저널에 없는 캐릭터로 채팅할 경우 엑스트라 전용 스탠딩을 표시할지 (true) 스탠딩을 생략할지(false) 설정합니다. // true일 경우 extra_name에 설정한 이름에 따라 엑스트라용 스탠딩을 가져옵니다. show_extra_standing: true, // option: use_emotion가 false일 경우에 캐릭터의 스탠딩 이미지를 가져올 카드덱의 이름을 설정합니다. deck_name: "standings", // option: use_emotion이 true일 경우에 엑스트라용 스탠딩 이미지를 가져올 카드덱의 이름을 설정합니다. extra_name: "extra", // option: show_extra_standing 옵션과 별개로 스탠딩을 표시하지 않을 캐릭터의 이름을 기입합니다. 여러개일 경우 콤마(,)로 구분합니다. ignore_list: "GM", // option: 스크립트를 사용할 페이지의 이름을 지정합니다. 여러개일 경우 콤마(,)로 구분합니다. // 페이지가 여럿일 경우 Player 북마크가 설정된 페이지에 우선적으로 대사가 표시됩니다. page_list: "conversation,intro", // option: 캐릭터의 이름이 표시되는 텍스트 박스의 폰트 사이즈를 설정합니다. name_font_size: 18, // option: 캐릭터 이름의 글씨색을 설정합니다. name_font_color: "rgb(255, 255, 255)", // option: 대사 내용이 표시되는 텍스트 박스의 폰트 사이즈를 설정합니다. dialogue_font_size: 16, // option: 대사 내용의 글씨색을 설정합니다. dialogue_font_color: "rgb(255, 255, 255)", // option: /desc나 /em으로 표시되는 강조된 텍스트 박스의 폰트 사이즈를 설정합니다. desc_font_size: 20, // option: /desc, /em의 글씨색을 설정합니다. desc_font_color: "rgb(255, 255, 255)", // option: 한번에 여러 채팅이 몰려서 순차적으로 표시해야 할 경우 채팅당 최소 노출시간을 설정합니다. (1000=1초) min_showtime: 1000, // option: 채팅 1글자당 표시 시간. 숫자가 커질수록 글자수 대비 대사의 표시시간이 길어집니다. showtime_ratio: 20, // option: (고급설정) 각 열이 간격이 font_size 대비 얼마만큼의 픽셀을 차지하는지의 비율을 지정합니다. line_height: 1.7, // option: (고급설정) 각 글자가 font_size 대비 얼마만큼의 픽셀을 차지하는지의 비율을 지정합니다. letter_spacing: 0.85 }; // /define: option on('ready', function() { // on.ready state.vd_stock = []; // /on.ready on("add:card", function(obj) { updateMacro(obj); }); }); on("change:card", function(obj,prev) { // on.change:card updateMacro(obj); // /on.change:card }); on("destroy:card", function(obj) { // on.destroy:card updateMacro(obj); // /on.destroy:card }); on("destroy:graphic", function(obj) { // on.destroy:graphic if (obj.get('name') == "vd_standing") { arrangeStandings(false); } // /on.destroy:graphic }); on("chat:message", function(msg) { // on.chat:message if ((msg.type == "general" || msg.type == "desc" || msg.type == "emote") &amp;&amp; (msg.playerid != 'API' || msg.content.includes(state.api_tag)) &amp;&amp; !msg.rolltemplate){ if (findCharacterWithName(msg.who) || findObjs({_type:'player',_displayname:msg.who.replace(' (GM)','')}).length == 0) { if (msg.content.length &gt; 0) { msg.content = msg.content.replace(state.api_tag,'').replace(/&lt;br&gt;/g,state.vd_divider); msg.time = new Date().getTime(); state.vd_stock.push(msg); if (state.vd_stock.length == 1 || (state.vd_stock.length &gt; 1 &amp;&amp; state.last_displayed_time + 5000 &lt; msg.time)) { setTimeout(showDialogue, 100); } } } } // /on.chat:message // on.chat:message:api if (msg.type == "api" &amp;&amp; msg.content.indexOf("!@") === 0) { const current_page_id = vdGetCurrentPage(); if (!current_page_id) { return; } if (msg.content == '!@숨김' || msg.content == '!@hide') { showHideDecorations('vd_deco',false); showHideDecorations('vd_panel',false); let bg_name = findObjs({ _type: 'graphic', name:'vd_name', _pageid:current_page_id}); let bg_dialogue = findObjs({ _type: 'graphic', name:'vd_dialogue', _pageid:current_page_id}); if (bg_name.length &gt; 0) { bg_name = bg_name[0]; } else { sendChat("error","/w gm **" + getObj('page',current_page_id).get('name') + "** 페이지에 vd_name 토큰이 없습니다.",null,{noarchive:true}); return; } if (bg_dialogue.length &gt; 0) { bg_dialogue = bg_dialogue[0]; } else { sendChat("error","/w gm **" + getObj('page',current_page_id).get('name') + "** 페이지에 vd_dialogue 토큰이 없습니다.",null,{noarchive:true}); return; } let text_name = getObj('text', bg_name.get('gmnotes')); let text_dialogue = getObj('text', bg_dialogue.get('gmnotes')); text_name.remove(); text_dialogue.remove(); sendChat('vd-api-wildcard','!@퇴장:전원'); } else if (msg.content.indexOf("!@퇴장") == 0 || msg.content.indexOf("!@exit") == 0) { const keyword = msg.content.replace("!@퇴장","").replace("!@exit","").replace(state.api_tag,''); if (keyword.length == 0) { removeStanding(msg); } else if (playerIsGM(msg.playerid) || msg.playerid == 'API' || msg.who == 'vd-api-wildcard') { if (keyword == ":전원" || keyword == ":전체" || keyword == ":all") { let tokens = findObjs({ _type: 'graphic', name: 'vd_standing', _pageid: current_page_id}); tokens.forEach(token =&gt; { token.remove(); }); } else if (keyword == ":엑스트라" || keyword == ":extra") { let tokens = findObjs({ _type: 'graphic', name: 'vd_standing', represents: '', _pageid: current_page_id}); tokens.forEach(token =&gt; { token.remove(); }); } else { msg.who = keyword.replace(":",""); removeStanding(msg); } } } else if (playerIsGM(msg.playerid) &amp;&amp; (msg.content == "!@리셋" || msg.content == "!@reset")) { state.vd_stock = []; sendChat("error","/w gm 표시 대기열에 쌓여있던 대사들을 초기화했습니다.",null,{noarchive:true}); } else if (playerIsGM(msg.playerid) &amp;&amp; (msg.content == "!@강제진행" || msg.content == "!@force-progress")) { showNextDialogue(); } else if (playerIsGM(msg.playerid) &amp;&amp; (msg.content.indexOf("!@배경 ") == 0 || msg.content.indexOf("!@background ") == 0)) { let bg_background = findObjs({ _type: 'graphic', name:'vd_background', _pageid:current_page_id}); let bg_deck = findObjs({_type: 'deck', name:'background'}); if (bg_background.length == 0) { sendChat("error","/w gm **" + getObj('page',current_page_id).get('name') + "** 페이지에 vd_background 토큰이 없습니다.",null,{noarchive:true}); return; } bg_background = bg_background[0]; const current_url = bg_background.get('imgsrc'); const new_bg = msg.content.replace('!@배경 ','').replace('!@background ',''); let is_url_input = (msg.content.indexOf('https://') &gt; -1); if (is_url_input) { bg_background.set('imgsrc',new_bg.replace('med','thumb').replace('max','thumb').replace(' ','')); if (current_url == bg_background.get('imgsrc')) { sendChat("error","/w gm 배경 이미지가 정상적으로 변경되지 않았습니다. 주소나 명령어 형식이 올바른지 확인해주세요. (ex: !@배경 https://이미지주소...)",null,{noarchive:true}); } } else { if (bg_deck.length == 0) { sendChat("error","/w gm **background** 덱이 콜렉션에 없습니다. 사용할 배경 이미지를 background 덱에 넣거나 !@배경 https://이미지주소... 형식으로 이미지 주소를 직접 입력하세요.",null,{noarchive:true}); return; } else { let bg_cards = findObjs({_type:'card', _deckid: bg_deck[0].get('_id'), name: new_bg}); if (bg_cards.length == 0) { sendChat("error","/w gm 이름이 '**" + new_bg + "'**인 배경 카드가 **background** 덱에 없습니다.", null,{noarchive:true}); return; } else { bg_background.set('imgsrc',bg_cards[0].get('avatar').replace('med','thumb').replace('max','thumb')); } } } } else if (vd_setting.use_emotion) { let cha_name = msg.who; let content_str = msg.content.replace(state.api_tag,'').replace('!@',''); let emot = content_str; if (content_str.lastIndexOf(':') &gt; -1 &amp;&amp; (playerIsGM(msg.playerid) || msg.playerid == 'API')) { cha_name = content_str.substring(0,content_str.lastIndexOf(':')); emot = content_str.substring(content_str.lastIndexOf(':')+1,content_str.length); } let chat_cha = findCharacterWithName(cha_name); let current_token = null; if (chat_cha || vd_setting.show_extra_standing) { current_token = findTokenWithCharacter(chat_cha?chat_cha.get('_id'):'', cha_name); } if (current_token) { let rt = findObjs({ _type: 'deck', name: vd_setting.deck_name}); if (rt.length == 0) { sendChat("error","/w gm 이름이 **" + vd_setting.deck_name +"**인 카드 덱이 없습니다.",null,{noarchive:true}); return; } else { let opt = { name: 'vd_standing', _subtype: 'card', _pageid: current_token.get('_pageid'), width: vd_setting.width, height: vd_setting.height, bar1_value: cha_name, left:current_token.get('left'), top:current_token.get('top'), layer: 'map', represents: chat_cha? chat_cha.get('_id'): '', tint_color: current_token.get('tint_color') }; let search_opt = { _type: 'card', _deckid: rt[0].get('_id')}; if (!vd_setting.use_emotion) { search_opt.name = chat_cha ? cha_name : vd_setting.extra_name; } else if (emot.length &gt; 0) { search_opt.name = cha_name + "-" + emot; } else { search_opt.name = cha_name; } let rt_items = findObjs(search_opt); if (rt_items.length &gt; 0) { opt.imgsrc = rt_items[0].get('avatar').replace('med','thumb').replace('max','thumb'); } else { sendChat("error","/w gm **"+ vd_setting.deck_name + "** 카드 덱에 이름이 **" + search_opt.name + "**인 카드가 없습니다.",null,{noarchive:true}); return; } current_token.set(opt); } } else { sendChat("error","/w \"" + msg.who + "\" 이름이 **" + cha_name +"**인 캐릭터가 없습니다.",null,{noarchive:true}); } } } // /on.chat:message:api }); // define: global function const vdGetCurrentPage = function() { const page_list = vd_setting.page_list.replace(/, /g,',').replace(/ ,/g,',').split(','); if (page_list.indexOf(getObj('page',Campaign().get("playerpageid")).get('name')) &gt; -1) { return Campaign().get("playerpageid"); } else { const page = findObjs({type:'page',name:page_list[0]}); if (page.length &gt; 0) { return page[0].get('_id'); } else { sendChat("error","/w gm 이름이 **" + page_list[0] + "**인 페이지가 없습니다.",null,{noarchive:true}); } } } const showDialogue = function() { let msg = state.vd_stock[0]; for (let index = 1; index &lt; state.vd_stock.length; index++) { const element = state.vd_stock[index]; if (element.who == msg.who &amp;&amp; Math.abs(element.time - msg.time) &lt; 100) { msg.content = msg.content + state.vd_divider + element.content; state.vd_stock.splice(index,1); index--; } else { break; } } const current_page_id = vdGetCurrentPage(); if (!current_page_id) { return; } let is_general = msg.type == "general"; const font_color = vd_setting[is_general?'dialogue_font_color':'desc_font_color']; let font_size = vd_setting[is_general ? 'dialogue_font_size' : 'desc_font_size']; let bg_area = findObjs({ _type: 'graphic', name:'vd_area', _pageid:current_page_id}); let bg_name = findObjs({ _type: 'graphic', name:'vd_name', _pageid:current_page_id}); let bg_dialogue = findObjs({ _type: 'graphic', name:'vd_dialogue', _pageid:current_page_id}); let bg_panel = findObjs({ _type: 'graphic', name:'vd_panel', _pageid:current_page_id}); let split = []; if (bg_area.length &gt; 0) { bg_area = bg_area[0]; } else { sendChat("error","/w gm **" + getObj('page',current_page_id).get('name') + "** 페이지에 vd_area 토큰이 없습니다.",null,{noarchive:true}); showNextDialogue(); return; } if (bg_name.length &gt; 0) { bg_name = bg_name[0]; } else { sendChat("error","/w gm **" + getObj('page',current_page_id).get('name') + "** 페이지에 vd_name 토큰이 없습니다.",null,{noarchive:true}); showNextDialogue(); return; } if (bg_dialogue.length &gt; 0) { bg_dialogue = bg_dialogue[0]; } else { sendChat("error","/w gm **" + getObj('page',current_page_id).get('name') + "** 페이지에 vd_dialogue 토큰이 없습니다.",null,{noarchive:true}); showNextDialogue(); return; } if (bg_panel.length &gt; 0) { bg_panel = bg_panel[0]; } else { sendChat("error","/w gm **" + getObj('page',current_page_id).get('name') + "** 페이지에 vd_panel 토큰이 없습니다.",null,{noarchive:true}); showNextDialogue(); return; } const width = bg_dialogue.get('width'); const name_width = bg_name.get('width'); let blank_name = ''; let blank_dialogue = ''; let text_name = getObj('text', bg_name.get('gmnotes')); let text_dialogue = getObj('text', bg_dialogue.get('gmnotes')); while (name_width &gt; blank_name.length*vd_setting['name_font_size']*vd_setting['letter_spacing']*1.2) { blank_name += " "; } while (width&gt;blank_dialogue.length*font_size*vd_setting['letter_spacing']*1.15) { blank_dialogue += " "; } if (text_name &amp;&amp; text_name.get('_pageid') != current_page_id) { text_name.remove(); text_name = null; } if (text_dialogue &amp;&amp; text_dialogue.get('_pageid') != current_page_id) { text_dialogue.remove(); text_dialogue = null; } if (!text_name) { text_name = createObj('text', { _pageid: bg_area.get('_pageid'), left: bg_name.get('left'), top: bg_name.get('top'), width: bg_name.get('width'), height: bg_name.get('height'), layer: 'objects', font_family: 'Arial', text: '', font_size: vd_setting['name_font_size'], color: vd_setting['name_font_color'] }); bg_name.set({'gmnotes':text_name.get('_id')}); } if (!text_dialogue) { text_dialogue = createObj('text', { _pageid: bg_dialogue.get('_pageid'), left: bg_dialogue.get('left'), top: bg_dialogue.get('top'), width: width, height: bg_dialogue.get('height'), layer: 'objects', font_family: 'Arial', text: '', font_size: font_size, color: font_color }); bg_dialogue.set({'gmnotes':text_dialogue.get('_id')}); } // 예외처리할 텍스트 제외 let name = msg.who + '\n' + blank_name; let filtered = msg.content; let filter_word = [ {regex:/\*.+\*/g,replace:/\*/g}, // *, **, *** {regex:/``.+``/g,replace:/``/g}, // `` {regex:/\[[^\(\)\[\]]*\]\(http[^\(\)\[\]]+\)/g,replace:/\[[^\(\)\[\]]*\]\(http[^\(\)\[\]]+\)/g}, // [](http...) {regex:/&lt;[^&gt;]*&gt;/g,replace:/&lt;[^&gt;]*&gt;/g}, // &lt;html&gt; {regex:/\(.{1}\" style=\"[^\)]+\)/g,replace:/\((?:[^)(]+|\((?:[^)(]+|\([^)(]*\))*\))*\)/g}, // [](#" style="...) {regex:/\$\[\[.+\]\]/g,replace:/\$\[\[.+\]\]/g}]; // [[]] for (let i=0;i&lt;filter_word.length;i++) { let match = filtered.match(filter_word[i].regex); if (match) { for (let j=0;j&lt;match.length;j++) { filtered = filtered.replace(match[j], match[j].replace(filter_word[i].replace,'')); } } } let ruby_match = filtered.match(/\([^\(\)\[\]]+\)\[[^\(\)\[\]]*\]/g); if (ruby_match) { for (let j=0;j&lt;ruby_match.length;j++) { let rubystr_split = ruby_match[j].substring(1,ruby_match[j].length-1).split(')['); filtered = filtered.replace(ruby_match[j], rubystr_split[1]+"("+rubystr_split[0]+")"); } } if (filtered.length == 0){ showNextDialogue(); return; } let str = filtered; let desc_ratio = is_general ? 1 : 0.8; let amount = Math.ceil(width/font_size/vd_setting['letter_spacing']*3) -3; let idx = 0; let length = 0; const thirdchar = ['\'',' ',',','.','!',':',';','"']; const halfchar = ['[',']','(',')','*','^','-','~','&lt;','&gt;','+','l','i','1']; const arr = thirdchar.concat(halfchar); let divided = false; for (let i=0;i&lt;str.length;i++){ let c = str[i]; length += 3; for (let j=0;j&lt;arr.length;j++) { if (c==arr[j]) { length -= (j&lt;thirdchar.length ? 2 : 1); break; } } if (length &gt;= amount * desc_ratio || c == state.vd_divider) { let substr = str.substring(idx,i+1).replace(state.vd_divider,''); split.push(is_general || msg.who.length &gt; 0 ? substr:getStringWithMargin(amount,length,desc_ratio,substr)); idx = i+1; length = 0; if ((split.length+1) * font_size * vd_setting['letter_spacing']*3 &gt; bg_dialogue.get('height')) { state.vd_stock.splice(1,0,{content:filtered.substring(idx,str.length),time:msg.time,playerid:msg.playerid,type:msg.type,who:msg.who}); divided = true; break; } } } if (idx &lt; str.length &amp;&amp; !divided) { let substr = str.substring(idx,str.length); split.push(is_general || msg.who.length &gt; 0 ? substr:getStringWithMargin(amount,length,desc_ratio,substr)); } if (is_general || msg.who.length &gt; 0) { while ((split.length+1) * font_size * vd_setting['line_height'] &lt; bg_dialogue.get('height')) { split.push(' '); } } else { split.splice(0,0,' '); } split.push(blank_dialogue); text_name.set({text:name,left:bg_name.get('left'),font_size: vd_setting['name_font_size'],color: vd_setting['name_font_color'], top:bg_name.get('top')+vd_setting['name_font_size']*vd_setting['line_height']/2}); text_dialogue.set({text:split.join('\n'),font_size: font_size, color: font_color, left: msg.type == 'desc' ? bg_panel.get('left'):bg_dialogue.get('left'), top: msg.type == 'desc'? bg_panel.get('top'):bg_dialogue.get('top')}); toFront(text_name); toFront(text_dialogue); setTimeout(() =&gt; { showHideDecorations('vd_panel',true); showHideDecorations('vd_deco',msg.type != 'desc'); toFront(text_name); toFront(text_dialogue); }, 100); clearTextWithout(text_name, text_dialogue); const ignore_list = vd_setting.ignore_list.replace(/, /g,',').replace(/ ,/g,',').split(','); if (msg.type != "desc" &amp;&amp; ignore_list.indexOf(msg.who) &lt; 0) { let chat_cha = findCharacterWithName(msg.who); let current_token = null; if (chat_cha || vd_setting.show_extra_standing) { current_token = findTokenWithCharacter(chat_cha?chat_cha.get('_id'):'', msg.who); } let tokens = findObjs({ _type: 'graphic', name: 'vd_standing', _pageid: current_page_id}); let lowest_priority = tokens[0]; for (var i=0;i&lt;tokens.length;i++) { var token = tokens[i]; token.set('tint_color','#000000'); if (parseInt(token.get('gmnotes')) &lt; parseInt(lowest_priority.get('gmnotes'))) { lowest_priority = token; } } if (current_token == null &amp;&amp; (chat_cha || vd_setting.show_extra_standing)) { let nm = chat_cha?msg.who:vd_setting.extra_name; let rt = findObjs({ _type: 'deck', name: vd_setting.deck_name}); if (rt.length == 0) { sendChat("error","/w gm 이름이 **"+ vd_setting.deck_name + "**인 카드 덱이 없습니다.",null,{noarchive:true}); showNextDialogue(); return; } else { let opt = { name: 'vd_standing', _pageid: bg_area.get('_pageid'), width: vd_setting.width, height: vd_setting.height, bar1_value: msg.who, layer: 'gmlayer', imgsrc: rt[0].get('avatar').replace('med','thumb').replace('max','thumb'), represents: chat_cha? chat_cha.get('_id'): '' }; const std = findObjs({ _type: 'card', _deckid: rt[0].get('_id'), name: msg.who}); if (std.length &gt; 0) { opt.imgsrc = std[0].get('avatar').replace('med','thumb').replace('max','thumb'); } if (tokens.length &gt;= vd_setting.max_number) { opt.left = lowest_priority.get('left'); } else { opt.left = arrangeStandings(true); } opt.top = bg_area.get('top'); if (tokens.length &gt;= vd_setting.max_number) { lowest_priority.set(opt); current_token = lowest_priority; } else { current_token = createObj('graphic', opt); } toFront(current_token); setTimeout(() =&gt; { current_token.set({tint_color:'transparent',gmnotes:Date.now(),layer:"map"}); }, 100); } } else if (current_token) { toFront(current_token); current_token.set({tint_color:'transparent',gmnotes:Date.now()}); } } state.last_displayed_time = new Date().getTime(); setTimeout(showNextDialogue, Math.max(vd_setting.min_showtime, str.length * vd_setting.showtime_ratio)); } const clearTextWithout = function(name_txt, dial_txt) { let filtered_txt = filterObjs(function(obj) { return (obj.get('_type') == 'text' &amp;&amp; obj.get("_pageid") == name_txt.get('_pageid') &amp;&amp; obj.get("_id") != name_txt.get('_id') &amp;&amp; obj.get("_id") != dial_txt.get('_id') &amp;&amp; ((Math.abs(obj.get("left") - name_txt.get('left')) &lt; 100 &amp;&amp; Math.abs(obj.get("top") - name_txt.get('top')) &lt; 100) || (Math.abs(obj.get("left") - dial_txt.get('left')) &lt; 100 &amp;&amp; Math.abs(obj.get("top") - dial_txt.get('top')) &lt; 100))); }); filtered_txt.forEach(txt =&gt; { txt.remove(); }); } const updateMacro = function(obj) { let background_deck = findObjs({type:'deck',name:'background'}); if (background_deck.length &gt; 0 &amp;&amp; obj.get('deckid') == background_deck[0].get('id')) { if (background_deck.length &gt; 1) { sendChat("error","/w gm **background** 덱이 **" + background_deck.length + "**개 있습니다. 먼저 생성된 1개 덱을 기준으로 매크로를 생성합니다.",null,{noarchive:true}); } let players = findObjs({type:'player'}); let bg_images = findObjs({_type:'card',_deckid:background_deck[0].get('id')}); let bg_macro = findObjs({_type:'macro',name:"배경전환"}); let action_str = "!@배경 ?{배경을 선택하세요"; let gm_list = ""; for (let index = 0; index &lt; bg_images.length; index++) { const card = bg_images[index]; action_str += "|" + card.get('name'); } for (let index = 0; index &lt; players.length; index++) { const player = players[index]; if (playerIsGM(player.id)) { gm_list += player.id + ","; } } gm_list = gm_list.substring(0,gm_list.length - 1); action_str += "}"; let options = {name:"배경전환",action:action_str,visibleto:gm_list}; if (bg_macro.length &gt; 0) { bg_macro = bg_macro[0]; bg_macro.set(options); } else { options.playerid = players[0].get('id'); bg_macro = createObj('macro',options); } } } const showHideDecorations = function(name, show) { const text_deco = findObjs({name: name, _type: 'graphic'}); for (let index = 0; index &lt; text_deco.length; index++) { const itm = text_deco[index]; if (show) { if (itm.get('gmnotes').includes('/')) { const wh = itm.get('gmnotes').split('/'); itm.set({width:parseInt(wh[0]),height:parseInt(wh[1]),layer:'objects'}); } toFront(itm); } else { if (itm.get('gmnotes').length == 0) { itm.set({gmnotes:itm.get('width')+"/"+itm.get('height')}); } itm.set({width:1,height:1,layer:'gmlayer'}); } } } const showNextDialogue = function() { state.vd_stock.splice(0,1); if (state.vd_stock.length &gt; 0) { showDialogue(); } } const getStringWithMargin = function(amount, length, ratio, str) { let margin = Math.round((amount - length)/4*ratio); for (var j=0;j&lt;margin;j++){ str = "ㅤ" + str + "ㅤ"; } return str; } const findCharacterWithName = function(who) { let chat_cha = findObjs({ _type: 'character', name: who}); if (chat_cha.length &gt; 0) { return chat_cha[0]; } else { return null; } } const findTokenWithCharacter = function(id, who) { let arr = findObjs({ _type: 'graphic', name: 'vd_standing', represents: id, _pageid: vdGetCurrentPage(), bar1_value: who}); if (arr.length &gt; 0) { return arr[0]; } return null; } const removeStanding = function(msg) { let character = findCharacterWithName(msg.who); let token = findTokenWithCharacter(character?character.get('_id'):'', msg.who); if (token) { token.remove(); arrangeStandings(false); } } const arrangeStandings = function(addNew) { const currernt_page_id = vdGetCurrentPage(); let tokens = findObjs({ _type: 'graphic', name: 'vd_standing', _pageid: currernt_page_id}); if (tokens.length &gt; 0 || addNew) { let bg_area = findObjs({ _type: 'graphic', name:'vd_area', _pageid:currernt_page_id}); if (bg_area.length &gt; 0) { bg_area = bg_area[0]; } else { sendChat("error1","/w gm vd_area 토큰이 없습니다.",null,{noarchive:true}); return; } let tokens_position = []; const compare = function( a, b ) { if ( a.left &lt; b.left ){ return -1; } if ( a.left &gt; b.left ){ return 1; } return 0; } for (var i=0;i&lt;tokens.length;i++) { tokens_position.push({idx:i,left:tokens[i].get('left')}); } tokens_position.sort(compare); let final_count = tokens.length + (addNew?1:0); final_count = final_count&lt;2? 2: final_count; let space = Math.floor(bg_area.get('width')/final_count); let left = bg_area.get('left') - Math.floor(bg_area.get('width')/2); if (space &lt; vd_setting.fit_width) { left += vd_setting.fit_width/2; space = Math.floor((bg_area.get('width')-vd_setting.fit_width)/(final_count-1)); } else { left += space/2 } let rand = addNew ? Math.floor(Math.random()*(tokens_position.length-1))+1 : Infinity; rand = rand &lt; 0 ? 0 : rand; for (var i=0;i&lt;tokens_position.length;i++) { let token = tokens[tokens_position[i].idx]; token.set('left',left + space * (i + (i&gt;=rand?1:0))); } return addNew ? left + space * (rand == Infinity ? 0 : rand) : false; } } // /define: global function /* (visual_dialogue.js) 220118 코드 종료 */