Demiplane 2 Roll20

Allows rolling from demiplane character sheets in roll20.

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Demiplane 2 Roll20
// @namespace    jackpoll4100
// @version      1.11
// @description  Allows rolling from demiplane character sheets in roll20.
// @author       jackpoll4100
// @match        https://app.demiplane.com/*
// @match        https://app.roll20.net/*
// @match        https://*.discordsays.com/*
// @icon         https://raw.githubusercontent.com/jackpoll4100/Demiplane2Roll20/main/d20.png
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addValueChangeListener
// ==/UserScript==

(function() {
  'use strict';
  if (!window.location.href.includes('demiplane')){
      window.demiplaneEnabled = false;
      function demiplaneToggle(){
          window.demiplaneEnabled = !window.demiplaneEnabled;
      };
      let demiplaneSettingsTemplate =
          `<div id="demiplaneSettings" style="display: flex; flex-direction: row; justify-content: space-between;">
              <input type="checkbox" id="demiplaneEnabled" title="Enables rolling from your Demiplane character sheet in another tab.">
              <input id="autoCheckLabel" style="margin: 5px 5px 5px 5px; width: 90%" disabled value="Enable rolls from Demiplane" type="text" title="Enables rolling from your Demiplane character sheet in another tab.">
           </div>`;
      function GM_onMessage(label, callback){
          GM_addValueChangeListener(label, function(){
              callback.apply(undefined, arguments[2]);
          });
      }
      function execMacro(macro){
          console.log('Demiplane - Executing Macro: ', macro);
          if (!window.demiplaneEnabled){
              console.log('cancelling macro execution, demiplane connection not enabled.');
              return;
          }
          document.querySelectorAll('[title="Text Chat Input"]')[0].value = macro;
          document.getElementById('chatSendBtn').click();
      }
      GM_onMessage('demiplane-pipe', function(message) {
          console.log('demiplane message received: ', message);
          if (message.includes('template')){
              let cleanedString = message.split('---')[1];
              execMacro(cleanedString);
          }
      });
      function appendDemiplaneSettings(){
          let uiContainer = document.createElement('div');
          uiContainer.innerHTML = demiplaneSettingsTemplate;
          document.getElementById('textchat-input').appendChild(uiContainer);
          document.getElementById('demiplaneEnabled').addEventListener('click', demiplaneToggle);
      }
      function timer (){
          if (document.getElementById('chatSendBtn')){
              appendDemiplaneSettings();
          }
          else{
              setTimeout(timer, 500);
          }
      }
      setTimeout(timer, 0);
      console.log('demiplane listener registered');
  }
  else {
      // Watch for focus.
      let focused = true;
      document.addEventListener("visibilitychange", () => {
          if (document.hidden) {
              focused = false;
          }
          else
          {
              focused = true;
          }
      });
      function GM_sendMessage(label){
          GM_setValue(label, Array.from(arguments).slice(1));
      }
      console.log('sending open message');
      GM_sendMessage('demiplane-pipe', 'demiplane opened');
      let demiGameClassMap = {
          'cosmere': {
              rollVals: ['dice-history-item-result-value'],
              nameVal: 'dice-history-item-name',
              secondaryNameVal: 'dice-history-item-name--source',
              charName: 'character-name',
              modifiers: {
                  'complication': 'Complication',
                  'opportunity': 'Opportunity'
              }
          },
          'cyberpunkred': {
              rollVals: ['dice-history-item-result-value'],
              nameVal: 'dice-history-item-name',
              secondaryNameVal: 'dice-history-item-name--source',
              charName: 'character-name',
              modifiers: {
                  'dice-roller-history--critical-failure': 'Critical Failure',
                  'dice-roller-history--critical-success': 'Critical Success'
              }
          },
          'marvelrpg': {
              rollVals: ['dice-history-item-result-value'],
              nameVal: 'dice-history-item-name',
              damageVal: 'dice-history-damage-total-container',
              secondaryNameVal: 'dice-history-item-origin',
              charName: 'character-name',
              modifiers: {
                  'dice-roller-history--fantastic': 'Fantastic',
                  'dice-roller-history--ultimate-fantastic': 'Ultimate Fantastic'
              }
          },
          'daggerheart': {
              rollVals: ['dice-history-item-result-value'],
              nameVal: 'dice-history-item-name',
              secondaryNameVal: 'dice-history-item-name--source',
              charName: 'character-name',
              modifiers: {
                  'with-hope': 'Hope',
                  'with-fear': 'Fear',
                  'critical-success': 'Critical Success'
              }
          },
          'candelaobscura': {
              rollVals: ['dice-roller-history'],
              nameVal: 'dice-history-item-name',
              secondaryNameVal: 'dice-history-item-origin',
              charName: 'character-name',
              modifiers: {
                  'dice-roller-history--critical ': 'Critical Success'
              }
          },
          'avatar': {
              rollVals: ['dice-roll__total'],
              nameVal: 'dice-roll__name',
              secondaryNameVal: 'dice-roll__origin',
              charName: 'header-character-name-container',
              rollsClosed: 'dice-roller__fab--expanded',
              orderReversed: true,
              modifiers: {
                  'dice-roll--miss': 'Miss',
                  'dice-roll--weak-hit': 'Weak Hit',
                  'dice-roll--strong-hit': 'Strong Hit'
              }
          },
          'starfinder': {
              rollVals: ['dice-roll__total'],
              nameVal: 'dice-roll__name',
              secondaryNameVal: 'dice-roll__origin',
              charName: 'character-name',
              rollsClosed: 'dice-roller__fab--expanded',
              orderReversed: true,
              modifiers: {
                  '20': 'Natural 20'
              }
          },
          'pathfinder': {
              rollVals: ['dice-roll__total'],
              nameVal: 'dice-roll__name',
              secondaryNameVal: 'dice-roll__origin',
              charName: 'character-name',
              rollsClosed: 'dice-roller__fab--expanded',
              orderReversed: true,
              modifiers: {
                  '20': 'Natural 20'
              }
          },
          'alienrpg': {
              rollVals: ['dice-history-successes-value','dice-history-item-result-value'],
              nameVal: 'dice-history-item-name',
              charName: 'character-name',
              modifierToken: ' - ',
              modifiers: {
                  'with-panic': 'Panic',
                  'panic-table-result-name': 'innerHTML',
                  'panic-table-result-description': 'innerHTML'
              }
          },
          'vampire': {
              rollVals: ['dice-history-successes-container'],
              nameVal: 'dice-history-name',
              charName: 'character-name',
              modifiers: {
                  'history-item-result__die--hunger-critical': 'Messy Critical',
                  'history-item-result__die--hunger-1': 'Bestial Failure',
                  'history-item-result__die--standard-critical': 'Standard Critical'
              }
          }
      };
      function getGame(){
          let gameSet = Object.keys(demiGameClassMap);
          for (let g of gameSet){
              if (window.location.href.includes(g)){
                  return g;
              }
          }
          return 'cosmere';
      }
      function rollWatcher(prevLState, charHash, execute){
          let game = getGame();
          let menuOpen = document.getElementsByClassName(demiGameClassMap?.[game]?.rollsClosed || 'dice-close-button').length;
          let parsedSession = window.location.href.substring(window.location.href.lastIndexOf('/') + 1);
          if (parsedSession?.includes('?')){
              parsedSession = parsedSession.split('?')[0];
          }
          let sessionID = parsedSession + '-dice-history';
          let lState = localStorage.getItem(sessionID);
          if (!lState){
              sessionID = sessionID.replace('dice-history', 'dicerolls');
              lState = localStorage.getItem(sessionID);
          }
          // This is hacky, I feel like npc history is probably not supposed to be stored under "undefined".
          if (!lState && window.location.href.includes('npc-sheet'))
          {
              sessionID = 'undefined-dice-history';
              lState = localStorage.getItem(sessionID);
          }
          if (charHash !== parsedSession || !focused){
              setTimeout(()=>{ rollWatcher(lState, parsedSession); }, 500);
              return;
          }
          if (!menuOpen){
              setTimeout(()=>{ rollWatcher(prevLState, parsedSession); }, 500);
              return;
          }
          let shouldRoll = false;
          if (prevLState !== lState){
              shouldRoll = true;
          }
          if (!shouldRoll){
              setTimeout(()=>{ rollWatcher(prevLState, parsedSession); }, 500);
              return;
          }
          else if (shouldRoll && !execute){
              setTimeout(()=>{ rollWatcher(prevLState, parsedSession, true); }, 100);
              return;
          }
          let rollEls = document.querySelectorAll(demiGameClassMap?.[game]?.rollVals ? `.${ demiGameClassMap?.[game]?.rollVals.join(',.') }` : 'nothing');
          let rolls = [];
          for (let e of rollEls){
              let rollForm = [];
              if (game === 'candelaobscura'){
                  let tempRolls = e.getElementsByClassName('history-item-result__label');
                  for (let r of tempRolls){
                      rollForm.push(r.innerHTML);
                  }
              }
              rolls.push(rollForm.length ? rollForm.join(', ') : e.innerHTML);
          }
          if (!rolls.length){
              setTimeout(()=>{ rollWatcher(prevLState, parsedSession); }, 500);
              return;
          }
          if (demiGameClassMap?.[game]?.orderReversed){
              rolls = rolls.reverse();
          }
          let rollNamesEls = document.getElementsByClassName(demiGameClassMap?.[game]?.nameVal || 'nothing');
          let rollNames = [];
          for (let e of rollNamesEls){
              rollNames.push(e.innerHTML);
          }
          let rollCasesEls = document.getElementsByClassName('dice-roller-history');
          if (!rollCasesEls.length){
              rollCasesEls = document.querySelectorAll('.dice-roll--expanded,.dice-roll--collapsed');
          }
          let secondaryRollNamesEls = [];
          let damageRollEls = [];
          let damageRolls = [];
          for (let e of rollCasesEls){
              secondaryRollNamesEls.push(e.getElementsByClassName(demiGameClassMap?.[game]?.secondaryNameVal || 'nothing')?.[0] || 0);
              damageRollEls.push(e.getElementsByClassName(demiGameClassMap?.[game]?.damageVal || 'nothing')?.[0] || 0);
              if (game === 'cosmere'){
                  let hit = e.getElementsByClassName('dice-history-damage-container--hit-container')?.[0]?.innerHTML?.replace('Hit', ' Hit ') || 0;
                  let graze = e.getElementsByClassName('dice-history-damage-container--graze-container')?.[0]?.innerHTML?.replace('Graze', ' Graze ') || 0;
                  let crit = e.getElementsByClassName('dice-history-damage-container--critical-hit-container')?.[0]?.innerHTML?.replace('Critical Hit', ' Critical Hit ') || 0;
                  if (hit || graze || crit){
                      damageRolls.push(`${ hit ? hit : '' }${ graze ? graze : '' }${ crit ? crit : '' }`);
                  }
                  else {
                      damageRolls.push(0);
                  }
              }
          }
          let rollTypes = [];
          for (let e of secondaryRollNamesEls){
              rollTypes.push(e ? e.innerHTML : '');
          }
          if (game !== 'cosmere'){
              for (let e of damageRollEls){
                  damageRolls.push(e ? e.innerHTML : '');
              }
          }
          if (demiGameClassMap?.[game]?.orderReversed){
              rollNames = rollNames.reverse();
              rollTypes = rollTypes.reverse();
              damageRolls = damageRolls.reverse();
          }
          let rollCases = [];
          if (demiGameClassMap?.[game]?.modifiers){
              for (let e of rollCasesEls){
                  let modSet = [];
                  for (let m of Object.keys(demiGameClassMap?.[game]?.modifiers)){
                      if (game === 'vampire'){
                          if (e.innerHTML.includes(m)){
                              modSet.push(demiGameClassMap?.[game]?.modifiers?.[m]);
                          }
                      }
                      else if (game === 'starfinder' || game === 'pathfinder'){
                          if (e.getElementsByClassName('dice-roll-details-dice__value')?.[0]?.innerHTML?.includes(m)){
                              modSet.push(demiGameClassMap?.[game]?.modifiers?.[m]);
                          }
                      }
                      else if (demiGameClassMap?.[game]?.modifiers?.[m] === 'innerHTML'){
                          let elem = e.getElementsByClassName(m)?.[0];
                          if (elem){
                              modSet.push(elem.innerHTML);
                          }
                      }
                      else if (e.classList.value.includes(m)){
                          modSet.push(demiGameClassMap?.[game]?.modifiers?.[m]);
                      }
                  }
                  if (!modSet.length){
                      rollCases.push(false);
                  }
                  else{
                      rollCases.push(modSet.join(demiGameClassMap?.[game]?.modifierToken || ', '));
                  }
              }
              if (demiGameClassMap?.[game]?.orderReversed){
                  rollCases = rollCases.reverse();
              }
          }
          let charName = document?.getElementsByClassName(demiGameClassMap?.[game]?.charName || 'nothing')?.[0]?.children?.[0]?.innerHTML || document?.getElementsByClassName('stat-block-name')?.[0]?.innerHTML || '';
          let constructedMessage = `&{template:default} {{name=${ charName ? `${ charName } - ` : '' }${ rollNames[rolls.length - 1] }}} ${ rollTypes[rolls.length - 1] ? `{{type=${ rollTypes[rolls.length - 1] }}}` : '' } {{result=${ rolls[rolls.length - 1] }}} ${ rollCases?.[rolls.length - 1] ? '{{additional effects=' + rollCases[rolls.length - 1] + '}}' : '' } ${ damageRolls[rolls.length - 1] ? `{{damage=${ damageRolls[rolls.length - 1] }}}` : '' }`;
          console.log('Sending message to roll20: ', constructedMessage);
          GM_sendMessage('demiplane-pipe', `${ Math.random() }---` + constructedMessage);
          setTimeout(()=>{ rollWatcher(lState, sessionID); }, 500);
      }
      let parsedSession = window.location.href.substring(window.location.href.lastIndexOf('/') + 1);
      let sessionID = parsedSession + '-dice-history';
      let lState = localStorage.getItem(sessionID);
      if (!lState){
          sessionID = sessionID.replace('dice-history', 'dicerolls');
          lState = localStorage.getItem(sessionID);
      }
      // This is hacky, I feel like npc history is probably not supposed to be stored under "undefined".
      if (!lState && window.location.href.includes('npc-sheet'))
      {
          sessionID = 'undefined-dice-history';
          lState = localStorage.getItem(sessionID);
      }
      rollWatcher(lState, parsedSession);
  }

})();