Greasy Fork 支持简体中文。

Beyonder for dndbeyond.com

Enhanced player character info for DMs.

// ==UserScript==
// @name        Beyonder for dndbeyond.com
// @namespace   Violentmonkey Scripts
// @match       https://www.dndbeyond.com/campaigns/*
// @grant       none
// @grant       GM_addStyle
// @version     1.2
// @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 { 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; }
  .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; }
  `,
  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 = 'https://character-service.dndbeyond.com/character/v5/character/';
const ddb_character_list = 'rpgcharacter-listing'
const ddb_character_list_item = 'ddb-campaigns-character-card';
let characters_ready = false;

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

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

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

function GetCharacterData() {
  if (!characters_ready) {
    const characterCards = document.getElementsByClassName(ddb_character_list_item);

    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"
      if (!characterID) {
        return
      }

      let characterData;

      async function getCharacterData() {
        let json;
        const res = await fetch(`${ddb_character_api_url}${characterID}`)
        json = await res.json();
        characterData = json.data

        if (json.success) {
          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,
            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) => {
            return stat === 1 ? -5 :
              stat === 2 ? -4 :
                stat === 3 ? -4 :
                  stat === 4 ? -3 :
                    stat === 5 ? -3 :
                      stat === 6 ? -2 :
                        stat === 7 ? -2 :
                          stat === 8 ? -1 :
                            stat === 9 ? -1 :
                              stat === 10 ? 0 :
                                stat === 11 ? 0 :
                                  stat === 12 ? 1 :
                                    stat === 13 ? 1 :
                                      stat === 14 ? 2 :
                                        stat === 15 ? 2 :
                                          stat === 16 ? 3 :
                                            stat === 17 ? 3 :
                                              stat === 18 ? 4 :
                                                stat === 19 ? 4 :
                                                  stat === 20 ? 5 :
                                                    stat === 21 ? 5 :
                                                      stat === 22 ? 6 :
                                                        stat === 23 ? 6 :
                                                          stat === 24 ? 7 :
                                                            stat === 25 ? 7 :
                                                              stat === 26 ? 8 :
                                                                stat === 27 ? 8 :
                                                                  stat === 28 ? 9 :
                                                                    stat === 29 ? 9 :
                                                                      stat === 30 ? 10 :
                                                                        -5
          }

          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, selfModifierClass) => {
            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) {
                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, Tools)
           const miscBlock = document.createElement("div");
           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")
          //  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)

          console.log(character.name, characterData, character)

          // 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;";
        }
      }
      getCharacterData();
    });
  }
  characters_ready = true;
}

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