您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
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); })();