Greasy Fork is available in English.

PowerBox4e for Roll20

Creates a ui for roll20 that allows for importing dnd 4e character sheets and rolling powers/skills from them.

// ==UserScript==
// @name         PowerBox4e for Roll20
// @namespace    jackpoll4100
// @version      1.3
// @description  Creates a ui for roll20 that allows for importing dnd 4e character sheets and rolling powers/skills from them.
// @author       jackson pollard
// @match        https://app.roll20.net/editor/
// @match        https://*.discordsays.com/*
// @icon         https://raw.githubusercontent.com/jackpoll4100/PowerBox4e/main/d20.png
// @grant        none
// ==/UserScript==

(function() {
    'use strict';
    function onDOMLoad(){
        let uiContainer = document.createElement('div');
        uiContainer.innerHTML = `
        <div id="powerBoxUI" class="hideUI">
        <div id="charBox" style="display: flex; flex-direction: row; justify-content: space-between;">
            <input id="charName" style="margin: 5px; width: 100%" disabled value="No character found" type="text">
        </div>
        <div class="hideUI" style="padding: 5px; display: flex; flex-direction: column; border-radius: 5px; justify-content: space-between; row-gap: 5px;border:  1px solid;" id="situationalModifiers">
            <div style="display: flex;flex-direction: row;min-width: 100%;justify-content: space-between;">
                Situational Modifiers:
            </div>
            <div style="display: flex;flex-direction: row;min-width: 100%;justify-content: space-between;">
                <input style="width: 50%;" type="text" placeholder="Attack Modifier" id="attackInput" title="Situational Attack Modifier">
                <input style="width: 50%;margin-left:  10px;" type="text" placeholder="Damage Modifier" id="damageInput" title="Situational Damage Modifier">
            </div>
            <div style="display: flex; flex-direction: row; width: 100%; justify-content: space-between;">
                <input style="width: 50%;" type="text" placeholder="Skill Modifier" id="skillInput" title="Situational Skill Modifier">
                <input style="width: 50%;margin-left:  10px;" type="text" placeholder="Saving Throw Modifier" id="savingInput" title="Situational Saving Throw Modifier">
            </div>
            <div style="display: flex; flex-direction: row; width: 100%; justify-content: space-between;">
                <input style="width: 25%;" type="text" placeholder="AC Mod" id="acInput" title="Situational Armor Class Modifier">
                <input style="width: 25%;margin-left:  10px;" type="text" placeholder="Fort Mod" id="fortInput" title="Situational Fortitude Modifier">
                <input style="width: 25%;margin-left:  10px;" type="text" placeholder="Ref Mod" id="refInput" title="Situational Reflex Modifier">
                <input style="width: 25%;margin-left:  10px;" type="text" placeholder="Will Mod" id="willInput" title="Situational Will Modifier">
            </div>
        </div>
        </div>
        <div class="hideUI" style="display: flex; flex-direction: row; width: 100%; justify-content: space-between;" id="skillActions">
        </div>
        <div class="hideUI" style="display: flex; flex-direction: row; width: 100%; justify-content: space-between;" id="powerActions">
        </div>
        <div style="display: flex; flex-direction: row; justify-content: space-between;">
            <input style="margin: 5px; width: 100%;" type="file" id="uploadedFile" />
            <span id="initiativeButton" class="btn" style="
                padding: 2px;
                margin: 5px;
                min-width: 48px;
                border-radius: 5px;
                display: inline-grid;
                align-items: center;
                text-align: center;
                cursor: pointer;"
                class="hideUI">
                    Initiative
            </span>
            <span id="usePower" class="btn" style="
                padding: 3px;
                margin: 5px;
                min-width: 60px;
                border-radius: 5px;
                display: inline-grid;
                align-items: center;
                text-align: center;
                cursor: pointer;"
                class="hideUI">
                    Use Power
            </span>
        </div>`;
        document.getElementById('textchat-input').appendChild(uiContainer);

        // Get the chat button and append the new "hide ui" button
        let cb = document.getElementById('chatSendBtn');
        if (cb){
            cb.outerHTML += '<style>.hideUI{ display: none !important; } .pb-button{ padding-left: 4px !important; padding-right: 1px !important; }</style><button class="btn pb-button" id="pbuiBtn" style="margin-left:  10px;">Power Box <b id="expandoIcon" style="font-size: 0.9em;">◃</b></button>';
            document.getElementById("pbuiBtn").addEventListener("click",function() {
                let box = document.getElementById('powerBoxUI');
                let icon = document.getElementById('expandoIcon');
                icon.innerHTML = icon.innerHTML === '◃' ? '▿' : '◃';
                icon.style = icon.innerHTML === '◃' ? 'font-size: 0.9em;' : 'font-size: 1.3em;';
                document.getElementById('powerBoxUI').classList.toggle('hideUI');
                window.dispatchEvent(new Event('resize'));
            });
        }
        else {
            console.log('PowerBox - Error: could not locate chat button to append content.');
        }

        let actionBox = document.getElementById('powerActions');
        let skillBox = document.getElementById('skillActions');

        let elStyles = 'margin: 5px;';

        //Create and append power ui elements
        let selectList = document.createElement("select");
        selectList.id = 'powerDropdown';
        selectList.style = elStyles + ' width: 75%';
        actionBox.appendChild(selectList);
        let targetsInput = document.createElement("input");
        targetsInput.type = 'number';
        targetsInput.id = 'myTargets';
        targetsInput.placeholder = '# of Targets';
        targetsInput.style = elStyles + ' width: 25%;';
        actionBox.appendChild(targetsInput);

        document.getElementById('initiativeButton').addEventListener('click', function(){
            window.constructInitiativeRoll();
        });

        document.getElementById('usePower').addEventListener('click', function() {
            if (!window?.foundCharacter){
                window.execMacro('Error: No character found. Please import a character first.');
            }
            let powerQuery = document.getElementById('powerDropdown').value;
            powerQuery = powerQuery.split('||');
            let targets = parseInt(document.getElementById('myTargets').value || 1);
            let macro = window.constructMacro(window.powerObject[powerQuery[0]], powerQuery.length > 1 ? window.powerObject[powerQuery[0]]['WeaponMap'][powerQuery[1]] : {}, targets);
            window.execMacro(macro);
            if (window?.powerBoxSettings?.autoCheck && window.frames?.length > 1){
                for (let y = 1; y < window.frames?.length; y++){
                    let w = window.frames[y];
                    for (let x = 1; x < 100; x++){
                        if (w?.document?.querySelector(`[name="attr_power-${ x }-name"]`)?.value?.replaceAll(' ', '')?.toLowerCase() === window.powerObject[powerQuery[0]].Name.replaceAll(' ', '').toLowerCase()){
                            if (w?.document?.querySelectorAll(`[name="attr_power-${ x }-used"]`)?.[0]){
                                w.document.querySelectorAll(`[name="attr_power-${ x }-used"]`)[0].click();
                            }
                        }
                    }
                }
            }
        });

        //Create and append skill ui elements
        let selectSkillList = document.createElement("select");
        selectSkillList.id = 'skillDropdown';
        selectSkillList.style = elStyles + ' width: 45%';
        skillBox.appendChild(selectSkillList);
        let buttonCss = `
                padding: 5px;
                margin: 5px;
                min-width: 70px;
                border-radius: 5px;
                display: inline-grid;
                align-items: center;
                text-align: center;
                cursor: pointer;`;
        skillBox.innerHTML += `<span class="btn" style="${ buttonCss } width: 25%;" id="skillCheckButton">Skill Check</span>
                               <span class="btn" style="${ buttonCss } width: 30%;" id="savingThrowButton">Saving Throw</span>`;
        document.getElementById("skillCheckButton").addEventListener("click",()=>{ window.constructSkillCheck(); });
        document.getElementById("savingThrowButton").addEventListener("click",()=>{ window.constructSavingThrow(); });

        const fileInputElement = document.getElementById('uploadedFile');

        // Improved dice process with 'or' handler and multiple dice matching.
        function processDiceFormula(macroString, weapon){
            // Construct the weapon dice from the first part of the damage formula (if applicable).
            let wDice = window?.foundCharacter?.weaponDiceMap?.[weapon?.Weapon];
            let statMods = window?.foundCharacter?.statMods;
            let reconstructedString = macroString;

            if (statMods){
                let stats = ['Charisma', 'Constitution', 'Dexterity', 'Strength', 'Wisdom', 'Intelligence'];
                for (let stat of stats){
                    for (let stat2 of stats){
                        if (stat !== stat2 && reconstructedString.includes(`${ stat } or ${ stat2 } Modifier`)){
                            reconstructedString = reconstructedString.replaceAll(`${ stat } or ${ stat2 } Modifier`,
                                                                                 statMods[`${ stat } Modifier`] > statMods[`${ stat2 } Modifier`] ? statMods[`${ stat } Modifier`] : statMods[`${ stat2 } Modifier`]);
                        }
                    }
                }
                for (let stat in statMods){
                    reconstructedString = reconstructedString.replaceAll(stat, statMods[stat]);
                }
            }
            let diceFound = reconstructedString.match(/[0-9][0-9]*d[0-9][0-9]*/g);
            if (diceFound?.length){
                let matchedSet = [];
                for (let x = 0; x < diceFound.length; x++){
                    let add = true;
                    for (let y = 0; y < diceFound.length; y++){
                        if (x !== y && diceFound[y] !== diceFound[x] && diceFound[y].includes(diceFound[x])){
                            add = false;
                        }
                    }
                    if (add && !matchedSet.includes(diceFound[x])){
                        matchedSet.push(diceFound[x]);
                    }
                }
                for (let match of matchedSet){
                    reconstructedString = reconstructedString.replaceAll(match, `[[${ match }]]`);
                }
            }
            // Replace weapon dice with expressions.
            for (let x = 1; x < 10; x++){
                reconstructedString = reconstructedString.replaceAll(`${ x }[W]`, `${ x }${ wDice } (Roll: [[${ x }${ wDice }]])`);
            }
            return reconstructedString;
        }

        function execMacro(macro){
            console.log('PowerBox - Executing Macro: ', macro);
            document.querySelectorAll('[title="Text Chat Input"]')[0].value = macro;
            document.getElementById('chatSendBtn').click();
        }

        function constructSkillCheck(){
            let skill = document.getElementById('skillDropdown')?.value;
            let sitBonus = document.getElementById('skillInput')?.value;
            let joiningChar = '+';
            if (sitBonus?.length && ['+','-'].includes(sitBonus[0])){
                joiningChar = '';
            }
            let macro = `&{template:default} {{name=${ skill } Check}} {{result=[[1d20+${ window.foundCharacter.skillMods[skill] }${ sitBonus ? `${ joiningChar }${ sitBonus }` : '' }]]}}`;
            window.execMacro(macro);
        }

        function buildSkillDropdown(){
            let sd = document.getElementById('skillDropdown');
            sd.innerHTML = '';
            let skills = ['Acrobatics','Arcana','Athletics','Bluff','Diplomacy','Dungeoneering','Endurance','Heal','History','Insight','Intimidate','Nature','Perception','Religion','Stealth','Streetwise','Thievery'];

            for (let skill of skills) {
                let option = document.createElement("option");
                option.text = skill;
                option.value = skill;
                sd.appendChild(option);
            }
        }

        function constructSavingThrow(){
            let sitBonus = document.getElementById('savingInput')?.value;
            let joiningChar = '+';
            if (sitBonus?.length && ['+','-'].includes(sitBonus[0])){
                joiningChar = '';
            }
            let macro = `&{template:default} {{name=Saving Throw}} {{result=[[1d20${ sitBonus ? `${ joiningChar }${ sitBonus }` : '' }]]}}`;
            window.execMacro(macro);
        }

        function constructInitiativeRoll(){
            if (!window?.foundCharacter?.initiativeBonus){
                window.execMacro('Error: No initiative bonus found, please import a character first (or reimport your character sheet if it was added in an older version).');
                return;
            }
            let macro = `&{template:default} {{name=Initiative}} {{result=[[1d20+${ window.foundCharacter.initiativeBonus } &{tracker}]]}}`;
            window.execMacro(macro);
        }

        function constructMacro(power, weapon, targets){
            let macro = `&{template:default} {{name=${power.Name}}}`;
            let attributes = ['Flavor','Power','Charge','Display','Channel Divinity','Power Type','Attack','Effect','Aftereffect','Axe','Mace','Heavy Blade','Spear or Polearm','Hit','Miss','Power Usage','Keywords','Action Type','Attack Type','Target','Targets','Requirement','Special','Weapon','Conditions'];
            let processForDice = ['Power','Charge','Channel Divinity','Effect','Aftereffect','Axe','Mace','Heavy Blade','Spear or Polearm','Hit','Miss','Power Usage','Requirement','Special','Weapon','Conditions'];
            for (let att of attributes){
                if (power?.[att]?.replaceAll(' ', '') || weapon?.[att]?.replaceAll(' ', '')){
                    if (processForDice.includes(att)){
                        macro += processDiceFormula(`{{${ att }=${ power[att] || weapon[att] }}}`, weapon);
                    }
                    else {
                        macro += `{{${ att }=${ power[att] || weapon[att] }}}`;
                    }
                }
            }
            if (power['Attack Bonus'] || weapon['Attack Bonus']){
                let sitBonus = document.getElementById('attackInput')?.value;
                let joiningChar = '+';
                if (sitBonus?.length && ['+','-'].includes(sitBonus[0])){
                    joiningChar = '';
                }
                macro += `{{attack=[[1d20+${ power['Attack Bonus'] || weapon['Attack Bonus'] || 0 }${ sitBonus ? `${ joiningChar }${ sitBonus }` : '' }]]}}`;
                for (let x = 1; x < targets; x++){
                    macro += `{{attack ${ x+1 }=[[1d20+${ power['Attack Bonus'] || weapon['Attack Bonus'] || 0 }${ sitBonus ? `${ joiningChar }${ sitBonus }` : '' }]]}}`;
                }
            }
            if (power?.Damage?.replaceAll(' ', '') || weapon?.Damage?.replaceAll(' ', '')){
                let sitBonus = document.getElementById('damageInput')?.value;
                let joiningChar = '+';
                if (sitBonus?.length && ['+','-'].includes(sitBonus[0])){
                    joiningChar = '';
                }
                macro += `{{damage=[[${ power['Damage'] || weapon['Damage'] }${ sitBonus ? `${ joiningChar }${ sitBonus }` : '' }]]}}`;
                if (window?.powerBoxSettings?.multiDamage){
                    for (let x = 1; x < targets; x++){
                        macro += `{{damage ${ x+1 }=[[${ power['Damage'] || weapon['Damage'] }${ sitBonus ? `${ joiningChar }${ sitBonus }` : '' }]]}}`;
                    }
                }
            }
            if (power?.['Crit Damage']?.replaceAll(' ', '') || weapon?.['Crit Damage']?.replaceAll(' ', '')){
                let sitBonus = document.getElementById('damageInput')?.value;
                let joiningChar = '+';
                if (sitBonus?.length && ['+','-'].includes(sitBonus[0])){
                    joiningChar = '';
                }
                macro += `{{Crit Damage=[[${ power['Crit Damage'] || weapon['Crit Damage'] }${ sitBonus ? `${ joiningChar }${ sitBonus.replaceAll('d', '*') }` : '' }]]}}`;
                if (window?.powerBoxSettings?.multiDamage){
                    for (let x = 1; x < targets; x++){
                        macro += `{{Crit Damage ${ x+1 }=[[${ power['Crit Damage'] || weapon['Crit Damage'] }${ sitBonus ? `${ joiningChar }${ sitBonus.replaceAll('d', '*') }` : '' }]]}}`;
                    }
                }
            }
            return macro;
        }

        // Add macro constructor to the window context.
        window.constructMacro = constructMacro;

        // Add skill check constructor to the window context.
        window.constructSkillCheck = constructSkillCheck;

        // Add saving throw constructor to the window context.
        window.constructSavingThrow = constructSavingThrow;

        // Add initiative constructor to the window context.
        window.constructInitiativeRoll = constructInitiativeRoll;

        // Add macro exec to the window context.
        window.execMacro = execMacro;

        function buildPowerDropdown(powersConstruct){
            let pd = document.getElementById('powerDropdown');
            pd.innerHTML = '';
            for (let p of powersConstruct) {
                if (p?.Weapons?.length){
                    for (let w of p?.Weapons){
                        let option = document.createElement("option");
                        option.text = p.Name + ', Weapon: ' + w.Weapon;
                        option.value = p.Name + '||' + w.Weapon;
                        pd.appendChild(option);
                    }
                }
                else{
                    let option = document.createElement("option");
                    option.text = p.Name;
                    option.value = p.Name;
                    pd.appendChild(option);
                }
            }
        }

        function buildCharacter(characterObj){
            window.foundCharacter = characterObj;
            window.powerBoxSettings = JSON.parse(localStorage.getItem('powerBoxSettings')) || { autoCheck: false, multiDamage: false };
            let cBox = document.getElementById('charBox');
            cBox.innerHTML = `
                <input id="charName" style="margin: 5px 5px 5px 5px; width: 74%" disabled value="Character: ${characterObj.name}" type="text">
                <input id="charLevel" style="margin: 5px 5px 5px 5px; width: 26%" disabled value="Level: ${characterObj.level}" type="text">`;
            if (!document.getElementById('settingsRow1')){
                cBox.outerHTML += `
                    <div style="margin-bottom: 5px; padding: 5px; display: flex; flex-direction: column; border-radius: 5px; justify-content: space-between; border:  1px solid; row-gap: 5px;" id="settingsWrapper">
                    <div>Settings:</div>
                    <div id="settingsRow1" style="display: flex; flex-direction: row; justify-content: space-between;">
                        <input type="checkbox" id="autoCheck" title="Check off powers when used (requires character sheet to be open).">
                        <input id="autoCheckLabel" style="margin: 5px 5px 5px 5px; width: 45%" disabled value="Auto Check Powers" type="text" title="Check off powers when used (requires character sheet to be open).">
                        <input type="checkbox" id="multiDamage" title="Roll damage for each target of the chosen power.">
                        <input id="multiDamageLabel" style="margin: 5px 5px 5px 5px; width: 45%; font-size: 0.9em;" title="Roll damage for each target of the chosen power." disabled value="Roll Damage Per Target" type="text">
                    </div>
                    </div>
                `;
            }
            let autoCheckBox = document.getElementById("autoCheck");
            autoCheckBox.checked = window?.powerBoxSettings?.autoCheck ? true : false;
            autoCheckBox.addEventListener("click",function() {
                window.powerBoxSettings.autoCheck = !window.powerBoxSettings.autoCheck;
                localStorage.setItem('powerBoxSettings', JSON.stringify(window.powerBoxSettings));
            });
            let multiDamageBox = document.getElementById("multiDamage");
            multiDamageBox.checked = window?.powerBoxSettings?.multiDamage ? true : false;
            multiDamageBox.addEventListener("click",function() {
                window.powerBoxSettings.multiDamage = !window.powerBoxSettings.multiDamage;
                localStorage.setItem('powerBoxSettings', JSON.stringify(window.powerBoxSettings));
            });
            document.getElementById('powerActions').classList.remove('hideUI');
            document.getElementById('usePower').classList.remove('hideUI');
            document.getElementById('skillActions').classList.remove('hideUI');
            document.getElementById('situationalModifiers').classList.remove('hideUI');
            buildSkillDropdown();
            window.dispatchEvent(new Event('resize'));
        }

        // Check localStorage for an existing character
        let foundCharacter = localStorage.getItem('powerBox');
        if (foundCharacter){
            foundCharacter = JSON.parse(foundCharacter);
            buildCharacter(foundCharacter);
            window.powerObject = foundCharacter.powerObject;
            buildPowerDropdown(foundCharacter.powersConstruct);
        }

        window.dispatchEvent(new Event('resize'));

        // Add event listener to the file input
        fileInputElement.addEventListener('change', (event) => {
            var file = document.getElementById("uploadedFile").files[0];
            var reader = new FileReader();
            reader.readAsText(file);
            reader.onloadend = function(){
                var parser = new DOMParser();
                var doc = parser.parseFromString(reader.result, "text/xml");
                let name = doc.getElementsByTagName('name')[0].innerHTML;
                let level = doc.getElementsByTagName('Level')[0].innerHTML;
                let experience = doc.getElementsByTagName('Experience')[0].innerHTML;
                let carriedMoney = doc.getElementsByTagName('CarriedMoney')[0].innerHTML;
                let storedMoney = doc.getElementsByTagName('StoredMoney')[0].innerHTML;

                // Construct stat mod array
                let statMods = {};
                let stats = ['Strength', 'Intelligence', 'Wisdom', 'Charisma', 'Constitution', 'Dexterity'];
                for (let stat of stats){
                    statMods[`${ stat } modifier`] = doc.querySelectorAll(`[name="${ stat } modifier"]`)[0].parentElement.getAttribute('value');
                }

                // Construct skill mod array
                let skillMods = {};
                let skills = ['Acrobatics','Arcana','Athletics','Bluff','Diplomacy','Dungeoneering','Endurance','Heal','History','Insight','Intimidate','Nature','Perception','Religion','Stealth','Streetwise','Thievery'];
                for (let skill of skills){
                    skillMods[`${ skill }`] = doc.querySelectorAll(`alias[name="${ skill }"]`)[0].parentElement.getAttribute('value');
                }

                // Construct initiative bonus
                let initiativeBonus = doc.querySelectorAll(`alias[name="Initiative"]`)[0].parentElement.getAttribute('value');

                let weaponDiceMap = {};

                // Construct the power set
                let powers = doc.getElementsByTagName('Power');
                let powersConstruct = [];
                window.powerObject = {};
                for (let x = 0; x < powers.length; x++){
                    console.log('PowerBox - Importing Power: ', powers[x].getAttribute('name'));
                    let tempPower = {
                        Name: powers[x].getAttribute('name')
                    };
                    let specs = powers[x].getElementsByTagName('specific');
                    let weirdFields = ['Aftereffect', 'Axe', 'Mace', 'Heavy Blade', 'Spear or Polearm'];
                    for (let y = 0; y < specs.length; y++){
                        tempPower[specs[y].getAttribute('name')] = specs[y].innerHTML;
                    }
                    for (let field of weirdFields){
                        for (let y = 0; y < specs.length; y++){
                            if (specs[y].getAttribute('name').includes(field)){
                                tempPower[field] = specs[y].innerHTML;
                            }
                        }
                    }
                    let weapons = powers[x].getElementsByTagName('Weapon');
                    tempPower.Weapons = [];
                    tempPower.WeaponMap = {};

                    // Construct the weapon set for each power
                    for (let y = 0; y < weapons.length; y++){
                        let tempWeapon = {};
                        tempWeapon.Weapon = weapons[y].getAttribute('name');
                        tempWeapon['Attack Bonus'] = weapons[y].getElementsByTagName('AttackBonus')[0].innerHTML;
                        tempWeapon.Damage = weapons[y].getElementsByTagName('Damage')[0].innerHTML;
                        let diceNoSpaces = tempWeapon?.Damage?.replaceAll(' ', '');
                        if (diceNoSpaces?.length > 2){
                            if (diceNoSpaces?.length > 3 && diceNoSpaces[3] !== '+'){
                                weaponDiceMap[tempWeapon.Weapon] = `${ diceNoSpaces.slice(1, 4) }`;
                            }
                            else{
                                weaponDiceMap[tempWeapon.Weapon] = `${ diceNoSpaces.slice(1, 3) }`;
                            }
                        }
                        let hasCrit = false;
                        if (weapons[y].getElementsByTagName('CritDamage')?.length){
                            hasCrit = true;
                            tempWeapon['Crit Damage'] = weapons[y].getElementsByTagName('CritDamage')[0].innerHTML;
                        }
                        else {
                            tempWeapon['Crit Damage'] = `${ weapons[y].getElementsByTagName('Damage')[0].innerHTML.replace('d','*') }`;
                        }
                        if (weapons[y].getElementsByTagName('Conditions')?.length){
                            tempWeapon.Conditions = weapons[y].getElementsByTagName('Conditions')[0].innerHTML;
                        }
                        if (weapons[y].getElementsByTagName('CritComponents')?.length){
                            tempWeapon['Crit Components'] = weapons[y].getElementsByTagName('CritComponents')[0].innerHTML;
                        }
                        else {
                            let shortRule = weapons[y].querySelector('RulesElement[type="Magic Item"]')?.getAttribute('internal-id');
                            if (shortRule){
                                let critSet = doc.querySelectorAll(`specific[name="Critical"]`);
                                for (let x of critSet){
                                    if (x?.parentElement?.getAttribute('internal-id') === shortRule){
                                        tempWeapon['Crit Components'] = x.innerHTML;
                                    }
                                }
                            }
                        }
                        function sanitizeCrits(crit, reconstructedString, plus){
                            let finalString = crit.replaceAll(' ', '') || '0';
                            let diceFound = reconstructedString.match(/[0-9][0-9]*d[0-9][0-9]*/g);
                            if (diceFound?.length){
                                let matchedSet = [];
                                for (let x = 0; x < diceFound.length; x++){
                                    let add = true;
                                    for (let y = 0; y < diceFound.length; y++){
                                        if (x !== y && diceFound[y] !== diceFound[x] && diceFound[y].includes(diceFound[x])){
                                            add = false;
                                        }
                                    }
                                    if (add && !matchedSet.includes(diceFound[x])){
                                        matchedSet.push(diceFound[x]);
                                    }
                                }
                                for (let match of matchedSet){
                                    if (reconstructedString.includes('per plus') && plus){
                                        for (let x = 0; x < plus; x++){
                                            finalString += '+' + match;
                                        }
                                    }
                                    else {
                                        finalString += '+' + match;
                                    }
                                }
                            }
                            return finalString;
                        }
                        if (!hasCrit && tempWeapon['Crit Components']){
                            let bonus = parseInt(tempWeapon.Weapon.replace(/\D/g,''));
                            tempWeapon['Crit Damage'] = sanitizeCrits(tempWeapon['Crit Damage'], tempWeapon['Crit Components'], bonus);
                        }
                        tempPower.Weapons.push(tempWeapon);
                        tempPower.WeaponMap[tempWeapon.Weapon] = tempWeapon;
                    }
                    powersConstruct.push(tempPower);
                    window.powerObject[tempPower.Name] = tempPower;

                    // Add Charge power options
                    if (tempPower.Name === 'Melee Basic Attack' || tempPower.Name === 'Bull Rush Attack'){
                        let chargePower = Object.assign({}, tempPower);
                        chargePower.Name = `Charge (${ chargePower.Name })`;
                        chargePower.Flavor = `You throw yourself into the fight, dashing forward and launching an attack. ${ chargePower.Flavor }`;
                        chargePower.Charge = `Move your speed as part of the charge and make an attack at the end of your move. You gain a +1 bonus to the attack roll.`
                        chargePower.Weapons = [];
                        chargePower.WeaponMap = {};
                        for (let wpn of tempPower.Weapons) {
                            let tmp = Object.assign({}, wpn);
                            tmp['Attack Bonus'] = tmp['Attack Bonus'] ? parseInt(tmp['Attack Bonus'])+1 : 1;
                            chargePower.Weapons.push(tmp);
                            chargePower.WeaponMap[tmp.Weapon] = tmp;
                        }
                        powersConstruct.push(chargePower);
                        window.powerObject[chargePower.Name] = chargePower;
                    }
                }

                let magicItems = doc.querySelectorAll('LootTally > loot > RulesElement[type="Magic Item"]');
                if (magicItems?.length){
                    for (let item of magicItems){
                        let flavor = item.querySelector('Flavor');
                        let power = item.querySelector('specific[name="Power"]');
                        let tempPower = {
                            Name: item.getAttribute('name'),
                            Flavor: flavor?.innerHTML || '',
                            Power: power?.innerHTML || ''
                        };
                        if (tempPower?.Power){
                            powersConstruct.push(tempPower);
                            window.powerObject[tempPower.Name] = tempPower;
                        }
                    }
                }

                console.log('PowerBox - Imported ' + powersConstruct.length + ' powers.');

                //Create and append the options
                powersConstruct = powersConstruct.sort((p1, p2)=>{
                    if (p1.Name < p2.Name) {
                        return -1;
                    } else if (p2.Name < p1.Name) {
                        return 1;
                    }
                    return 0;
                });
                let characterObj = {
                    powersConstruct: powersConstruct,
                    powerObject: window.powerObject,
                    name,
                    carriedMoney,
                    storedMoney,
                    experience,
                    level,
                    statMods,
                    skillMods,
                    initiativeBonus,
                    weaponDiceMap
                };
                window.foundCharacter = characterObj;
                buildCharacter(characterObj);
                buildPowerDropdown(powersConstruct);
                localStorage.setItem('powerBox', JSON.stringify(characterObj));
            };
        });
    }
    function timer (){
        if (document.getElementById('chatSendBtn')){
            onDOMLoad();
        }
        else{
            setTimeout(timer, 500);
        }
    }
    setTimeout(timer, 0);
})();