Beyonder for dndbeyond.com

Enhanced player character info for DMs.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name        Beyonder for dndbeyond.com
// @namespace   Violentmonkey Scripts
// @match       https://www.dndbeyond.com/campaigns/*
// @grant       GM_addStyle
// @version     1.3.4
// @author      lumbearjack
// @description Enhanced player character info for DMs.
// @license     GNU GPLv3
// ==/UserScript==

// Custom Styles
const lightColor = 'rgba(255,255,255,1)';
const darkColor = '#111';
var css = `

  .beyonder.ddb-campaigns-character-card { height: 100%; }
  
  .ddb-campaigns-character-card-wrapper {position: relative; }
  .ddb-campaigns-character-card { display: flex; flex-direction: column; filter: drop-shadow(2px 4px 6px rgba(0,0,0,0.05)); border: 1px solid #dedede; border-radius: 9px; }
  .ddb-campaigns-character-card::after { display: none; }
  
  .ddb-campaigns-character-card-header { display: flex; order: -1; padding: 10px 10px; position: static; filter: none !important; }
  .ddb-campaigns-character-card-header-cover-image { border-radius: 9px 9px 0 0; overflow: hidden; bottom: 30px; }
  .ddb-campaigns-character-card-header-cover-image::after { backdrop-filter: none; }
  .ddb-campaigns-character-card-header-upper { align-items: center; width: 100%; }
  .ddb-campaigns-character-card-header-upper-portrait { position: relative; }
  .ddb-campaigns-character-card-header-upper-character-info-primary:hover { opacity: 0.8; transition: all .2s ease;}
  .ddb-campaigns-character-card-footer { order: 9999; z-index: 1; background: white; border-radius: 0 0 9px 9px; border: 0; }
  .ddb-campaigns-character-card-footer-links { height: 30px !important; }
  .ddb-campaigns-detail-body-listing-inactive .ddb-campaigns-character-card-header-cover-image { filter: saturate(0); }
  .ddb-campaigns-detail-body-listing-inactive .ddb-campaigns-character-card-header-upper-portrait { filter: saturate(0); }

  .beyonder_container { display: flex; flex-direction: column; grid-gap: 6px; padding: 0 10px 10px; height: 100%; z-index: 1; }
  .beyonder_group { display: flex; grid-gap: 0 3px; }
  .beyonder_group--grid_thirds { display: grid; grid-template-columns: repeat(3, 1fr); grid-gap: 0 3px; width: 100%; }
  .beyonder_group--grid_fifths { display: grid; grid-template-columns: repeat(5, 1fr); grid-gap: 0 3px; width: 100%; }
  .beyonder_group--grid_sixths { display: grid; grid-template-columns: repeat(6, 1fr); grid-gap: 0 3px; width: 100%; }
  .beyonder_group--column { display: flex; flex-direction: column; }
  .beyonder_group--nested { display: flex; grid-gap: 0 3px; flex-wrap: wrap; width: 100%; }
  .beyonder_block { display:flex; flex-direction: column; align-items: center; width: 100%; border-radius: 4px; background-color: ${lightColor}; color: ${darkColor}; padding: 2px 3px; }
  .beyonder_block--nested { display: flex; flex-direction: column; text-align: center; flex-grow: 1; }
  .beyonder_header { display: flex; justify-content: center; align-items: center; text-transform: uppercase; font-weight: bold; font-size: 10px; background-color: rgba(0,0,0,0); width: 100%; text-align: center; padding: 1px 0 0;}
  .beyonder_subheader { display: flex; justify-content: center; align-items: center; text-transform: uppercase; font-weight: bold; font-size: 8px; background-color: rgba(0,0,0,0); width: 100%; text-align: center; padding: 1px 0;}
  .beyonder_body_text { font-size: 16px; font-weight: 500; }
  .beyonder_body_text--large { display: flex; text-transform: uppercase; font-weight: 500; font-size: 16px; padding: 0px 6px; align-items: center; justify-content: center; }

  .beyonder_proficient { position: relative; background: #f2faff; outline: 1px solid #00ccff; outline-offset: -1px; color: #004557 }
  .beyonder_proficient:before { content: 'P'; position: absolute; left: 6px; bottom: 2px; font-size: 10px; font-weight: 500; color: #008fb3; opacity: 0.4; }

  .beyonder_expertise { background: #fffdf1; outline: 1px solid gold; outline-offset: -1px; filter: drop-shadow(0px 0px 3px gold); color: #574400; }
  .beyonder_expertise:before { content: 'E'; position: absolute; left: 6px; bottom: 2px; font-size: 10px; font-weight: 500; color: #ae9100; }

  .beyonder_advantage { position: relative; }
  .beyonder_advantage:after { content: 'A'; position: absolute; right: 2px; bottom: 2px; display: flex; height: 11px; width: 11px; background-color: #73c573; border-radius: 50%; font-size: 9px; font-weight: 900; color: white; align-items: center; justify-content: center; }

  .beyonder_proficient--subdued { position: relative; }
  .beyonder_proficient--subdued:before { content: 'P'; position: absolute; left: 4px; bottom: 1.5px; display: flex; font-size: 10px; font-weight: 900; color: #00ccff; }

  .beyonder_expertise--subdued { position: relative; }
  .beyonder_expertise--subdued:before { content: 'E'; position: absolute; left: 4px; bottom: 1.5px; display: flex; font-size: 10px; font-weight: 900; color: gold; }

  .beyonder_tabs { position: absolute; right: 0; bottom: 0; display: flex; flex-direction: column; grid-gap: 3px; background: rgba(255,255,255,0); padding: 0px; border-radius: 11px; color: #aaa; font-size: 10px; font-weight: 500; }
  .beyonder_tabs > .beyonder_tab { padding: 1px 8px; border-radius: 1000px; transition: all 0.3s ease; cursor: pointer; user-select: none; }
  .beyonder_tabs > .beyonder_tab:not(.active):hover { background: rgba(255,255,255,0.3); color: #eee }
  .beyonder_tabs > .beyonder_tab.active { background: rgba(255,255,255,0.6); color: #333 }
  .page:not(.active) { display: none; }

  .beyonder_passives .beyonder_block { background: none; color: white; }
  .beyonder_skills_block { flex-direction: row; flex-wrap: wrap; grid-gap: 3px; }
  .beyonder_skills_block > .beyonder_block { width: auto; flex: 1 1 32%; }
  .beyonder_skills_block > .beyonder_block:nth-child(n+1):nth-child(-n+4), .beyonder_skills_block > .beyonder_block:nth-child(n+8):nth-child(-n+11), .beyonder_skills_block > .beyonder_block:nth-child(n+15):nth-child(-n+18) { width: auto; flex: 1 1 calc(100% / 4 - 9px); }
  .beyonder_skills_block .beyonder_header { font-size: 8px; text-align: center; }

  .beyonder_simple_list { flex-wrap: wrap; grid-gap: 3px; }
  .beyonder_simple_list .beyonder_block { flex: 1 1 32%; }
  .beyonder_simple_list .beyonder_block--full { flex: 1 0 100%; }
  .beyonder_simple_list .beyonder_body_text--large { text-transform: none; font-size: 12px; font-weight: 400; text-align: center; }
  .beyonder_info { position: absolute; top: 1rem; right: 0; min-width: 100px; border: solid 1px black; border-radius: 9px; padding: 8px 12px; margin-right: 10px; }
  .beyonder_info .beyonder_header { margin-bottom: 2px; justify-content: start; font-size: 12px; }
  .beyonder_info .beyonder_body { margin-bottom: 2px; }
  .beyonder_info .beyonder_status { position: absolute; top: 8px; right: 12px; }
  .beyonder_currency { flex: 1 1 50% !important; }
  .beyonder_order-button-label { position: absolute; top: 12px; right: 12px; height: 20px; z-index: 50; cursor: pointer; opacity: 0.25; transition: opacity 0.33s ease; }
  .beyonder_order-button-label:hover { opacity: 0.6;  }
  .beyonder_order-button-label svg { height: 100%; transform: rotateZ(45deg) }
  li input.beyonder_order-button:checked + .beyonder_order-button-label { opacity: 0.6; }
  `,
  head = document.head || document.getElementsByTagName('head')[0],
  style = document.createElement('style');

head.appendChild(style);
style.type = 'text/css';
style.appendChild(document.createTextNode(css));


// Get data
const ddb_character_api_url_main = 'https://character-service.dndbeyond.com/character/v';
let ddb_character_api_url_version = 5;
const ddb_character_api_url_endpoint = '/character/';
// let ddb_character_api_url = `${ddb_character_api_url_main}${ddb_character_api_url_version}${ddb_character_api_url_endpoint}`;
const ddb_character_list = 'rpgcharacter-listing'
const ddb_character_list_item = 'ddb-campaigns-character-card';
let characters_ready = false;
let beyonderBoxLoaded = false;

let pinnedCharacters = []

waitForKeyElements(`div.${ddb_character_list_item},div.${ddb_character_list}`, Main);

function Main() {
  if (!beyonderBoxLoaded) {
    // beyonderInfoBox()
  }

  if (IsCharacterCards()) {
    GetCharacterData();
    return
  }
  console.error("Failed to retrieve character data");
}

function beyonderInfoBox() {
  const infoBoxParent = document.getElementsByClassName('ddb-campaigns-detail-body-listing-active')[0]
  const infoBox = document.createElement('div');
  const infoBoxHeader = document.createElement('p');
  const infoBoxHeaderText = document.createTextNode('Beyonder');
  const infoBoxBody = document.createElement('p');
  const infoBoxBodyText = document.createTextNode('Description');
  const infoBoxStatus = document.createElement('div');
  // const infoBoxStatusText = document.createTextNode('...');

  infoBox.classList.add("beyonder_info");
  infoBoxHeader.classList.add("beyonder_header");
  infoBoxBody.classList.add("beyonder_body");
  infoBoxStatus.classList.add("beyonder_status");

  infoBoxParent.style = "position: relative;"

  infoBoxParent.appendChild(infoBox)

  infoBox.appendChild(infoBoxHeader)
  infoBoxHeader.appendChild(infoBoxHeaderText)

  infoBox.appendChild(infoBoxBody)
  infoBoxBody.appendChild(infoBoxBodyText)

  infoBox.appendChild(infoBoxStatus)
  // infoBoxStatus.appendChild(infoBoxStatusText)

  infoBox.appendChild(infoBoxStatus)

  beyonderBoxLoaded = true

}

function IsCharacterCards() {
  return (document.getElementsByClassName(ddb_character_list_item)[0] != null);
}

function GetCharacterData() {
  if (!characters_ready) {
    const characterCards = document.getElementsByClassName(ddb_character_list_item);
    let validAPIVersion = false
    let charactersChecked = 0

    Array.from(characterCards).forEach(card => {

      const characterID = card.getElementsByClassName('ddb-campaigns-character-card-footer-links-item-view')[0].href.split("/")[6];
      const unloadedCharacterViewUrl = card.getElementsByClassName('ddb-campaigns-character-card-header-upper-details-link')[0];
      unloadedCharacterViewUrl.target = "_blank"
      unloadedCharacterViewUrl.rel = "noopener noreferrer"
      card.style.order = '99'

      if (!characterID) {
        return
      }

      let characterData;

      async function getCharacterData() {
        let json;
        const apiURL = `${ddb_character_api_url_main}${ddb_character_api_url_version}${ddb_character_api_url_endpoint}`
        const res = await fetch(`${apiURL}${characterID}`)
        json = await res.json();
        characterData = json.data
        charactersChecked++

        if (json.success) {
          validAPIVersion = true
          card.classList.add("beyonder")
          const character_name_el = card.getElementsByClassName('ddb-campaigns-character-card-header-upper-character-info-primary')[0];
          const character_image_el = card.getElementsByClassName('ddb-campaigns-character-card-header-upper-portrait')[0];
          const original_link_el = card.getElementsByClassName('ddb-campaigns-character-card-header-upper-details-link')[0];
          const character_link = card.getElementsByClassName('ddb-campaigns-character-card-footer-links-item-view')[0].getAttribute("href");
          const new_character_link1 = document.createElement('a');
          original_link_el.style = "display: none;";
          character_name_el.style = "position: relative; display: inline-flex;"
          new_character_link1.href = character_link;
          new_character_link1.target = "_blank"
          new_character_link1.rel = "noopener noreferrer"
          new_character_link1.style = "position: absolute; top: 0; left: 0; bottom: 0; right: 0;"
          character_name_el.appendChild(new_character_link1)
          const new_character_link2 = document.createElement('a')
          new_character_link2.href = character_link;
          new_character_link2.target = "_blank"
          new_character_link2.rel = "noopener noreferrer"
          new_character_link2.style = "position: absolute; top: 0; left: 0; bottom: 0; right: 0;"
          character_image_el.appendChild(new_character_link2)
          card.getElementsByClassName('ddb-campaigns-character-card-footer-links-item-view')[0].target = "_blank"
          card.getElementsByClassName('ddb-campaigns-character-card-footer-links-item-view')[0].rel = "noopener noreferrer"

          const abilities_list = ['strength', 'dexterity', 'constitution', 'intelligence', 'wisdom', 'charisma']
          const abilities = { 'STR': 'strength', 'DEX': 'dexterity', 'CON': 'constitution', 'INT': 'intelligence', 'WIS': 'wisdom', 'CHA': 'charisma' }
          const strength_skills = ['athletics'];
          const dexterity_skills = ['acrobatics', 'sleight_of_hand', 'stealth'];
          const intelligence_skills = ['arcana', 'history', 'investigation', 'nature', 'religion'];
          const wisdom_skills = ['animal_handling', 'insight', 'medicine', 'perception', 'survival'];
          const charisma_skills = ['deception', 'intimidation', 'performance', 'persuasion'];
          const skills = strength_skills.concat(dexterity_skills).concat(intelligence_skills).concat(wisdom_skills).concat(charisma_skills)

          const deriveProficiency = (level) => {
            return level >= 1 && level <= 4 ? 2 :
              level >= 5 && level <= 8 ? 3 :
                level >= 9 && level <= 12 ? 4 :
                  level >= 13 && level <= 16 ? 5 :
                    level >= 17 && level <= 20 ? 6 : 0
          }

          const charLevel = characterData.classes.reduce((total, obj) => obj.level + total, 0)

          let character = {
            test: null,
            name: characterData.name,
            baseHitPoints: characterData.baseHitPoints,
            bonusHitPoints: characterData.bonusHitPoints,
            currentHitPoints: null,
            totalHitPoints: 0,
            armorClass: 10,
            classSave: 0,
            initiative: 0,
            level: charLevel,
            currencies: characterData.currencies,
            languages: [],
            size: null,
            proficiency: deriveProficiency(charLevel),
            proficiencies: {
              armor: [],
              savingThrows: [],
              skills: [],
              stats: [],
              tools: [],
              weapon: [],
            },
            expertise: {
              skills: [],
              unsorted: []
            },
            resistances: [],
            savingThrows: [],
            savingThrowAdvantages: [],
            skillAdvantages: [],
            speeds: {
              walk: characterData.race.weightSpeeds.normal.walk,
              swim: 0,
              fly: 0,
              burrow: 0,
              climb: 0,
            },
            vision: {
              dark: 0,
            },
            abilityAdvantages: [],
            stats: {
              strength: {
                bonuses: [],
                bonusScore: null,
                mod: 0,
                set: false,
                setScore: null,
                savingThrow: 0,
                savingThrowBonuses: [],
                score: characterData.stats[0].value,
                baseScore: characterData.stats[0].value,
              },
              dexterity: {
                bonuses: [],
                bonusScore: null,
                mod: 0,
                set: false,
                setScore: null,
                savingThrow: 0,
                savingThrowBonuses: [],
                score: characterData.stats[1].value,
                baseScore: characterData.stats[1].value,
              },
              constitution: {
                bonuses: [],
                bonusScore: null,
                mod: 0,
                set: false,
                setScore: null,
                savingThrow: 0,
                savingThrowBonuses: [],
                score: characterData.stats[2].value,
                baseScore: characterData.stats[2].value,
              },
              intelligence: {
                bonuses: [],
                bonusScore: null,
                mod: 0,
                set: false,
                setScore: null,
                savingThrow: 0,
                savingThrowBonuses: [],
                score: characterData.stats[3].value,
                baseScore: characterData.stats[3].value,
              },
              wisdom: {
                bonuses: [],
                bonusScore: null,
                mod: 0,
                set: false,
                setScore: null,
                savingThrow: 0,
                savingThrowBonuses: [],
                score: characterData.stats[4].value,
                baseScore: characterData.stats[4].value,
              },
              charisma: {
                bonuses: [],
                bonusScore: null,
                mod: 0,
                set: false,
                setScore: null,
                savingThrow: 0,
                savingThrowBonuses: [],
                score: characterData.stats[5].value,
                baseScore: characterData.stats[5].value,
              }
            },
            skills: {
              acrobatics: {
                passive: 10,
                bonus: 0,
              },
              animal_handling: {
                passive: 10,
                bonus: 0,
              },
              arcana: {
                passive: 10,
                bonus: 0,
              },
              athletics: {
                passive: 10,
                bonus: 0,
              },
              deception: {
                passive: 10,
                bonus: 0,
              },
              history: {
                passive: 10,
                bonus: 0,
              },
              insight: {
                passive: 10,
                bonus: 0,
              },
              intimidation: {
                passive: 10,
                bonus: 0,
              },
              investigation: {
                passive: 10,
                bonus: 0,
              },
              medicine: {
                passive: 10,
                bonus: 0,
              },
              nature: {
                passive: 10,
                bonus: 0,
              },
              perception: {
                passive: 10,
                bonus: 0,
              },
              performance: {
                passive: 10,
                bonus: 0,
              },
              persuasion: {
                passive: 10,
                bonus: 0,
              },
              religion: {
                passive: 10,
                bonus: 0,
              },
              sleight_of_hand: {
                passive: 10,
                bonus: 0,
              },
              stealth: {
                passive: 10,
                bonus: 0,
              },
              survival: {
                passive: 10,
                bonus: 0,
              },
            },
            handled: {
              race: [],
              class: [],
              background: [],
              feat: [],
              item: [],
            },
            unhandled: {
              race: [],
              class: [],
              background: [],
              feat: [],
              item: [],
            }
          }

          const deriveModifier = stat => Math.floor((stat - 10) / 2);

          let delayedModifiers = [];

          // Modifier loop update
          for (const [type, modifiers] of Object.entries(characterData.modifiers)) {
            modifiers.forEach((mod) => {
              let skillSubType = skills.filter((skill) => mod.subType.split('-').join('_') === skill)[0] || null
              let abilitySubType = abilities_list.filter((skill) => mod.subType.split('-')[0] === skill)[0] || null

              if (mod.duration) {
                character.unhandled[type].push({ type: mod.type, subType: mod.subType, fixedValue: mod.fixedValue, restriction: mod.restriction, mod: mod })
                return
              }

              if (mod.type === 'advantage') {
                if (mod.subType.includes('-ability-checks')) {
                  character.abilityAdvantages.push(abilitySubType)
                } else if (mod.subType.includes('-saving-throws')) {
                  character.savingThrows.push(mod.subType.split('-saving-throws')[0])
                } else if (mod.subType === 'saving-throws' && mod.restriction) {
                  character.savingThrowAdvantages.push(mod.restriction)
                } else if (skillSubType) {
                  character.skillAdvantages.push(mod.subType)
                } else {
                  character.unhandled[type].push({ type: mod.type, subType: mod.subType, fixedValue: mod.fixedValue, restriction: mod.restriction, mod: mod })
                  return
                }
              } else if (mod.type === 'bonus') {
                if (mod.subType === 'saving-throws') {
                  abilities_list.forEach((ability) => character.stats[ability].savingThrowBonuses.push({ type: type, value: mod.fixedValue }))
                } else if (mod.subType.includes('-score') && !mod.subType.includes('choose-an-ability-score')) {
                  if (abilitySubType) { character.stats[abilitySubType].bonuses.push({ type: type, value: mod.fixedValue }) }
                } else if (mod.subType === 'hit-points-per-level') {
                  character.bonusHitPoints += mod.fixedValue * character.level
                } else if (mod.subType === 'speed') {
                  character.speeds.walk += mod.fixedValue
                } else if (mod.subType === 'initiative') {
                  character.initiative += mod.fixedValue
                } else if (mod.subType.includes('passive-')) {
                  const skill = mod.subType.split('passive-')[1]
                  character.skills[skill].passive += mod.fixedValue
                } else {
                  character.unhandled[type].push({ type: mod.type, subType: mod.subType, fixedValue: mod.fixedValue, restriction: mod.restriction, mod: mod })
                  return
                }
              } else if (mod.type === 'expertise') {
                skills.forEach((skill) => {
                  if (mod.subType === skill) { character.expertise.skills.push(skill) }
                });
              } else if (mod.type === 'set-base') {
                if (mod.subType === 'darkvision') {
                  character.vision.dark = mod.fixedValue
                } else {
                  character.unhandled[type].push({ type: mod.type, subType: mod.subType, fixedValue: mod.fixedValue, restriction: mod.restriction, mod: mod })
                  return
                }
              } else if (mod.type === 'language') {
                !mod.subType.includes('choose') && character.languages.push(mod.friendlySubtypeName)
              } else if (mod.type === 'resistance') {
                character.resistances.push(mod.friendlySubtypeName)
              } else if (mod.subType === 'saving-throws') {
                character.savingThrows.push(mod)
              } else if (mod.type === 'set') {
                if (mod.subType === 'unarmored-armor-class') {
                  delayedModifiers.push(mod.subType)
                } else if (mod.subType.includes('innate-speed')) {
                  if (mod.subType.includes("swimming")) {
                    character.speeds.swim = characterData.race.weightSpeeds.normal.walk
                  } else if (mod.subType.includes("flying")) {
                    character.speeds.fly = characterData.race.weightSpeeds.normal.walk
                  } else if (mod.subType.includes("burrowing")) {
                    character.speeds.burrow = characterData.race.weightSpeeds.normal.walk
                  } else if (mod.subType.includes("climbing")) {
                    character.speeds.climb = characterData.race.weightSpeeds.normal.walk
                  } else {
                    character.unhandled[type].push({ type: mod.type, subType: mod.subType, fixedValue: mod.fixedValue, restriction: mod.restriction, mod: mod })
                    return
                  }
                } else if (mod.subType === `${abilitySubType}-score`) {
                  character.stats[abilitySubType].setScore = mod.fixedValue
                  character.stats[abilitySubType].set = true
                } else {
                  character.unhandled[type].push({ type: mod.type, subType: mod.subType, fixedValue: mod.fixedValue, restriction: mod.restriction, mod: mod })
                  return
                }
              } else if (mod.type === 'size') {
                character.size = mod.friendlySubtypeName
              } else if (mod.type === 'proficiency') {
                if (mod.subType.includes('-saving-throws')) {
                  character.proficiencies.savingThrows.push(abilitySubType)
                } else if (mod.subType.includes('-armor')) {
                  character.proficiencies.armor.push(mod.friendlySubtypeName)
                } else if (mod.subType === 'shields') {
                  character.proficiencies.armor.push(mod.friendlySubtypeName)
                } else if (mod.subType.includes('-tools')) {
                  !mod.subType.includes('choose') && character.proficiencies.tools.push(mod.friendlySubtypeName)
                } else if (mod.subType === 'unarmored-armor-class') {
                  character.armorClass = 10 + mod.fixedValue;
                } else if (skillSubType) {
                  character.proficiencies.skills.push(skillSubType)
                } else {
                  character.unhandled[type].push({ type: mod.type, subType: mod.subType, fixedValue: mod.fixedValue, restriction: mod.restriction, mod: mod })
                  return
                }
              } else {
                character.unhandled[type].push({ type: mod.type, subType: mod.subType, fixedValue: mod.fixedValue, restriction: mod.restriction, mod: mod })
                return
              }
              character.handled[type].push({ type: mod.type, subType: mod.subType, fixedValue: mod.fixedValue, restriction: mod.restriction, mod: mod })
            });
          }

          // Build Elements
          const topBlock = document.createElement("div");
          topBlock.classList.add("beyonder_group");

          const midBlock = document.createElement("div");
          const statBlock = document.createElement("div");
          midBlock.classList.add("beyonder_group")
          statBlock.classList.add("beyonder_group--grid_sixths")
          midBlock.append(statBlock);

          const passiveBlock = document.createElement("div");
          passiveBlock.classList.add("beyonder_group", "beyonder_group--column")

          const addElement = (element, data, header, parent, rider = null, parentModifierClass = null, selfModifierClass = null) => {
            const block = document.createElement(element);
            const titleBlock = document.createElement("div");
            const textBlock = document.createElement("div");
            const title = document.createTextNode(header);
            titleBlock.classList.add("beyonder_header")
            titleBlock.appendChild(title);
            block.append(titleBlock)
            if (Array.isArray(data)) {
              const groupBlock = document.createElement("div");
              groupBlock.classList.add("beyonder_group--nested")
              data.forEach((item) => {
                const subBlock = document.createElement("div");
                const subtitleBlock = document.createElement("div");
                const subtextBlock = document.createElement("div");
                const subtitleText = document.createTextNode(item.title)
                const subtextText = document.createTextNode(item.text)
                subtitleBlock.appendChild(subtitleText);
                subtextBlock.appendChild(subtextText);
                subBlock.appendChild(subtitleBlock);
                subBlock.appendChild(subtextBlock);
                subtextBlock.classList.add("beyonder_body_text")
                subtitleBlock.classList.add("beyonder_header", "beyonder_subheader")
                subBlock.classList.add("beyonder_block--nested")
                groupBlock.append(subBlock)
              })
              block.classList.add("beyonder_block");
              block.append(groupBlock)
            } else {
              const text = document.createTextNode(data);
              textBlock.classList.add("beyonder_body_text--large")
              textBlock.appendChild(text);
              block.append(textBlock)
              block.classList.add("beyonder_block");
              if (selfModifierClass && selfModifierClass.isArray) {
                block.classList.add(selfModifierClass.split(',').join(' '))
                // selfModifierClass.forEach((modClass) => {
                //   block.classList.add(` beyonder_${modClass}`)
                // })
              } else if (selfModifierClass) {
                block.classList.add(`beyonder_${selfModifierClass}`)
              }
              if (rider) {
                if (rider.context) {
                  if (rider.context === "fullSkills") {
                    character.proficiencies.skills.forEach((skill) => {
                      if (rider.data === skill) {
                        block.classList.add("beyonder_proficient");
                      }
                    })
                    character.expertise.skills.forEach((skill) => {
                      if (rider.data === skill) {
                        block.classList.add("beyonder_expertise");
                      }
                    })
                    character.skillAdvantages.forEach((skill) => {
                      if (rider.data === skill) {
                        block.classList.add("beyonder_advantage");
                      }
                    })
                    // if (character.skillDisdvantages.includes(skill)) {
                    //   block.classList.add("beyonder_disadvantage");
                    // }
                  }
                }
              }
            }
            if (parentModifierClass) {
              parent.classList.add(`beyonder_${parentModifierClass}`)
            }
            parent.append(block)
          }

          // loop, update ability scores/modifiers/saves
          for (const [key, stat] of Object.entries(abilities)) {
            let score, mod, save;

            // Calculate score adjustment from bonuses
            character.stats[stat].bonuses.forEach(bonus => {
              character.stats[stat].bonusScore += bonus.value
            });

            // Set stat scores and derive modifiers
            if (character.stats[stat].set) {
              score = character.stats[stat].setScore
            } else {
              score = character.stats[stat].baseScore + character.stats[stat].bonusScore
            }
            mod = deriveModifier(score)
            save = mod
            character.stats[stat].mod = mod

            // Calculate saving throws
            character.stats[stat].savingThrowBonuses.forEach((bonus) => save += bonus.value)
            character.proficiencies.savingThrows.forEach(saveAbility => {
              saveAbility === stat && (save += character.proficiency)
            });

            const abilityData = [
              {
                title: "SCORE",
                text: score,
              },
              {
                title: "MOD",
                text: mod >= 0 ? `+${mod}` : mod,
              },
              {
                title: "SAVE",
                text: save >= 0 ? `+${save}` : save
              }
            ]

            // add modifiers to skills
            let skills;
            if (stat === 'strength') {
              skills = strength_skills
            } else if (stat === 'dexterity') {
              skills = dexterity_skills
            } else if (stat === 'intelligence') {
              skills = intelligence_skills
            } else if (stat === 'wisdom') {
              skills = wisdom_skills
            } else if (stat === 'charisma') {
              skills = charisma_skills
            }
            if (stat != 'constitution') {
              skills.forEach((skill) => {
                character.skills[skill].bonus += mod
                character.skills[skill].passive += mod
              });
            }

            addElement("div", abilityData, key, statBlock, null)
          }

          // TO-DO: Recalculate AC, ddbs armor data is unhinged
          // Inventory
          let armorBonusAC = 0;
          let equippedArmor;
          const equippedArmors = characterData.inventory.filter(item => item.equipped && item.definition.armorClass > 0)
          if (equippedArmors.length) {
            const bestArmorIndex = Object.keys(equippedArmors).reduce((a, b) => equippedArmors[a].definition.armorClass > equippedArmors[b].definition.armorClass ? a : b);
            equippedArmor = equippedArmors[bestArmorIndex]
            if (equippedArmor.definition.armorClass > 2 && equippedArmor.definition.grantedModifiers) {
              equippedArmor.definition.grantedModifiers.forEach((mod) => {
                if (mod.type === "bonus" && mod.subType === "armor-class") {
                  armorBonusAC += mod.fixedValue
                }
              });
            }
          }
          const equippedShields = characterData.inventory.filter(item => item.equipped && item.definition.armorClass === 2)
          if (equippedShields.length) {
            armorBonusAC += 2
          }
          if (equippedArmor) {
            character.armorClass = equippedArmor.definition.armorClass + armorBonusAC
          }

          // Adjust skill proficiencies
          character.proficiencies.skills.forEach((skill) => {
            character.skills[skill].bonus += character.proficiency
            character.skills[skill].passive += character.proficiency
          });
          character.expertise.skills.forEach((skill) => {
            character.skills[skill].bonus += character.proficiency
            character.skills[skill].passive += character.proficiency
          });

          // Final Stat / Skills value Adjustments
          character.languages.sort();
          character.resistances.sort();
          character.totalHitPoints = character.baseHitPoints + (character.stats.constitution.mod * character.level) + character.bonusHitPoints;
          character.currentHitPoints = character.totalHitPoints - characterData.removedHitPoints;
          character.initiative += character.stats.dexterity.mod;
          character.armorClass += character.stats.dexterity.mod;
          character.classSave = characterData.classes[0].definition.spellCastingAbilityId > 0 ? character.stats[abilities_list[characterData.classes[0].definition.spellCastingAbilityId - 1]].mod + character.proficiency + 8 : '-'

          characterData.race.racialTraits.forEach((trait) => {
            if (!character.size && trait.definition.name === "Size") {
              let sizeDescription = trait.definition.description
              if (sizeDescription.includes('our size is ')) {
                character.size = sizeDescription.split('our size is ')[1].split('.')[0]
              } else if (sizeDescription.includes('ou are ')) {
                character.size = sizeDescription.split('ou are ')[1].split('.')[0]
              }
              return
            }
          })

          // Apply delayed modifiers
          delayedModifiers.forEach((mod) => {
            if (mod === "unarmored-armor-class") {
              character.armorClass += character.stats.wisdom.mod
            }
          });

          // Classic Passives
          const passiveGroup = document.createElement("div");
          passiveGroup.classList.add("beyonder_group")
          const passiveScoresShort = [
            { score: 'Perception', value: character.skills.perception.passive },
            { score: 'Investigation', value: character.skills.investigation.passive },
            { score: 'Insight', value: character.skills.insight.passive },
          ]
          passiveScoresShort.forEach((passive) => {
            addElement("div", passive.value, passive.score, passiveGroup, null, "passives");
          });

          // Vision
          const visionBlock = document.createElement("div");
          visionBlock.classList.add("beyonder_group")
          const visionBlocks = [
            { score: 'Darkvision', value: character.vision.dark > 0 ? `${character.vision.dark} ft.` : '-' },
          ]
          visionBlocks.forEach((vision) => {
            addElement("div", vision.value, vision.score, passiveGroup, null, "vision_block");
          });

          // Skills (Passives + Modifiers)
          const fullSkillsBlock = document.createElement("div");
          fullSkillsBlock.classList.add("beyonder_group")

          for (const [key, value] of Object.entries(character.skills)) {
            addElement("div", `${value.passive} (${value.bonus >= 0 ? `+${value.bonus}` : `${value.bonus}`})`, key.split('_').join(' '), fullSkillsBlock, { context: "fullSkills", data: key }, "skills_block")
          };

          // Misc (Languages, Currencies)
          const miscBlock = document.createElement("div");
          const currencyString = `${character.currencies.pp > 0 ? `${character.currencies.pp}p` : ''} ${character.currencies.gp > 0 ? ` ${character.currencies.gp}g` : ''} ${character.currencies.ep > 0 ? ` ${character.currencies.ep}e` : ''} ${character.currencies.sp > 0 ? ` ${character.currencies.sp}s` : ''} ${character.currencies.cp > 0 ? `${character.currencies.cp}c` : ''} `;

          miscBlock.classList.add("beyonder_group")
          addElement("div", character.languages.join(', '), "Languages", miscBlock, null, "simple_list")

          character.resistances.length && addElement("div", character.resistances.join(', '), "Resistances", miscBlock, null, "simple_list")
          character.proficiencies.tools.length && addElement("div", character.proficiencies.tools.join(', '), "Tools", miscBlock, null, "simple_list")
          character.savingThrowAdvantages.length && addElement("div", character.savingThrowAdvantages.join(', '), "Advantage on Saving Throws...", miscBlock, null, null, "block--full")
          addElement("div", currencyString, "Currencies", miscBlock, null, null, "currency")
          //  character.savingThrows.length && addElement("div", character.savingThrows.join(', '), "Saving Throws", miscBlock, null, "simple_list")

          // Build main info items
          addElement("div", `${character.initiative >= 0 ? `+${character.initiative}` : `${character.initiative}`}`, "Initiative", topBlock, null)
          addElement("div", character.speeds.walk, "Speed", topBlock, null)
          addElement("div", character.classSave, "Save DC", topBlock, null)
          addElement("div", character.armorClass, "AC", topBlock, null)
          addElement("div", `${character.currentHitPoints}/${character.totalHitPoints}`, "HP", topBlock, null)

          

          // page 1
          const cardBodyA = document.createElement("div");
          cardBodyA.classList.add("beyonder_container", "page", "page-1", "active")
          cardBodyA.setAttribute("page", "page-1");
          card.append(cardBodyA)
          cardBodyA.append(topBlock);
          midBlock.append(statBlock);
          cardBodyA.append(midBlock);
          cardBodyA.append(passiveGroup);

          // page 2
          const cardBodyB = document.createElement("div");
          cardBodyB.classList.add("beyonder_container", "page", "page-2")
          cardBodyB.setAttribute("page", "page-2");
          card.append(cardBodyB)
          cardBodyB.append(fullSkillsBlock);

          // page 3
          const cardBodyC = document.createElement("div");
          cardBodyC.classList.add("beyonder_container", "page", "page-3")
          cardBodyC.setAttribute("page", "page-3");
          card.append(cardBodyC)
          cardBodyC.append(miscBlock);

          // Tabs
          const cardTabs = card.getElementsByClassName('ddb-campaigns-character-card-header-upper')[0];
          const toggleTab = (event) => {
            let targetGroup;
            targetGroup = event.shiftKey ? [card] : characterCards;
            const thisPage = event.target.getAttribute('page')

            Array.from(targetGroup).forEach((target) => {
              const pages = target.querySelectorAll(`[page]`);
              const activePages = target.querySelectorAll(`[page=${thisPage}]`);

              Array.from(pages).forEach((page) => {
                page.classList.remove('active')
              })
              Array.from(activePages).forEach((page) => {
                page.classList.add('active')
              })
            })
          }

          const tabsEl = document.createElement("div");
          tabsEl.classList.add("beyonder_tabs")

          const tabs = ["Main", "Skills", "Misc"]
          tabs.forEach((tab, i) => {
            const tabEl = document.createElement("div");
            tabEl.appendChild(document.createTextNode(tab))
            i === 0 ? tabEl.classList.add("beyonder_tab", "active") : tabEl.classList.add("beyonder_tab")
            tabEl.setAttribute("page", `page-${i + 1}`);
            tabEl.addEventListener("click", (e) => toggleTab(e));
            tabsEl.append(tabEl)
          })

          cardTabs.append(tabsEl)

          // Header & Footer
          card.style = "display: flex; flex-direction: column;";

          // If Active Character, setup pinning
          const activeCharacter = card.closest('.ddb-campaigns-detail-body-listing-active') || false;
          const unassignedCharacter = card.closest('.ddb-campaigns-detail-body-listing-unassigned-active-listing') || false

          if (activeCharacter && !unassignedCharacter) {
            const liParent = card.closest('li');
            liParent.dataset.id = characterID

            const toggle = document.createElement('input');
            toggle.classList.add("beyonder_order-button")
            toggle.type = 'checkbox';
            toggle.style.display = 'none';
            toggle.id = `${'toggle-'+characterID}`;

            const label = document.createElement('label');
            label.classList.add("beyonder_order-button-label")
            label.title = 'Pin to top'
            label.htmlFor = `${'toggle-'+characterID}`;
            
            const pinLineSvg = `<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="m624-480 96 96v72H516v228l-36 36-36-36v-228H240v-72l96-96v-264h-48v-72h384v72h-48v264Zm-282 96h276l-66-66v-294H408v294l-66 66Zm138 0Z"/></svg>`;
            const pinFillSvg = `<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="m624-480 96 96v72H516v228l-36 36-36-36v-228H240v-72l96-96v-264h-48v-72h384v72h-48v264Z"/></svg>`
            
            label.innerHTML = pinLineSvg

            function updateAllOrders() {
              const parentContainer = document.querySelectorAll('.ddb-campaigns-detail-body-listing-active')[0]
              const container = parentContainer.querySelectorAll('.rpgcharacter-listing')[0]

              
              console.log(container)

              container.querySelectorAll('li').forEach(li => {
                const id = li.dataset.id
                const toggleLabel = container.querySelector(`label[for="${`${'toggle-'+id}`}"]`);
                toggleLabel.innerHTML = pinLineSvg
                li.style.order = '99';
              });

              pinnedCharacters.forEach((id, index) => {
                const toggleLabel = container.querySelector(`label[for="${`${'toggle-'+id}`}"]`);
                const selectedLi = container.querySelector(`li[data-id="${id}"]`);
                if (selectedLi) {
                  toggleLabel.innerHTML = pinFillSvg
                  selectedLi.style.order = (index + 1);
                }
              });
            }

            toggle.addEventListener('change', function () {
              if (liParent) {
                // Get the characterID from data-id attribute
                const characterId = liParent.dataset.id;

                if (this.checked) {
                  if (!pinnedCharacters.includes(characterId)) {
                    pinnedCharacters.push(characterId);
                  }
                } else {
                  // Remove from array
                  pinnedCharacters = pinnedCharacters.filter(id => id !== characterId);
                }

                // Update all orders based on current array
                updateAllOrders();
              }
            });

            liParent.append(toggle)
            liParent.append(label)
          }

        }
        else {
          // console.error(res)
          if (!validAPIVersion && charactersChecked === characterCards.length) {
            console.log('Checked:', charactersChecked, '/', characterCards.length, '. API Version', ddb_character_api_url_version, 'no longer supported.')
            ddb_character_api_url_version++
            characters_ready = false;
            ReattemptGetCharacterData()
          }
        }
      }

      getCharacterData();

    });
    characters_ready = true;
  }

}

function ReattemptGetCharacterData() {
  GetCharacterData()
}

//https://github.com/CoeJoder/waitForKeyElements.js
/**
 * A utility function for userscripts that detects and handles AJAXed content.
 *
 * @example
 * waitForKeyElements("div.comments", (element) => {
 *   element.innerHTML = "This text inserted by waitForKeyElements().";
 * });
 *
 * waitForKeyElements(() => {
 *   const iframe = document.querySelector('iframe');
 *   if (iframe) {
 *     const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
 *     return iframeDoc.querySelectorAll("div.comments");
 *   }
 *   return null;
 * }, callbackFunc);
 *
 * @param {(string|function)} selectorOrFunction - The selector string or function.
 * @param {function}          callback           - The callback function; takes a single DOM element as parameter.
 *                                                 If returns true, element will be processed again on subsequent iterations.
 * @param {boolean}           [waitOnce=true]    - Whether to stop after the first elements are found.
 * @param {number}            [interval=300]     - The time (ms) to wait between iterations.
 * @param {number}            [maxIntervals=-1]  - The max number of intervals to run (negative number for unlimited).
 */
function waitForKeyElements(selectorOrFunction, callback, waitOnce, interval, maxIntervals) {
  if (typeof waitOnce === "undefined") {
    waitOnce = true;
  }
  if (typeof interval === "undefined") {
    interval = 300;
  }
  if (typeof maxIntervals === "undefined") {
    maxIntervals = -1;
  }
  var targetNodes = (typeof selectorOrFunction === "function")
    ? selectorOrFunction()
    : document.querySelectorAll(selectorOrFunction);

  var targetsFound = targetNodes && targetNodes.length > 0;
  if (targetsFound) {
    targetNodes.forEach(function (targetNode) {
      var attrAlreadyFound = "data-userscript-alreadyFound";
      var alreadyFound = targetNode.getAttribute(attrAlreadyFound) || false;
      if (!alreadyFound) {
        var cancelFound = callback(targetNode);
        if (cancelFound) {
          targetsFound = false;
        }
        else {
          targetNode.setAttribute(attrAlreadyFound, true);
        }
      }
    });
  }

  if (maxIntervals !== 0 && !(targetsFound && waitOnce)) {
    maxIntervals -= 1;
    setTimeout(function () {
      waitForKeyElements(selectorOrFunction, callback, waitOnce, interval, maxIntervals);
    }, interval);
  }
}