Demiplane 2 Roll20

Allows rolling from demiplane character sheets in roll20.

// ==UserScript==
// @name         Demiplane 2 Roll20
// @namespace    jackpoll4100
// @version      1.6
// @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 {
      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'
              }
          },
          '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);
          let sessionID = parsedSession + '-dice-history';
          let lState = localStorage.getItem(sessionID);
          if (!lState){
              sessionID = sessionID.replace('dice-history', 'dicerolls');
              lState = localStorage.getItem(sessionID);
          }
          if (charHash !== parsedSession){
              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;
          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);
      }
      rollWatcher(lState, parsedSession);
  }

})();