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);
  }
}