|
(async () => { |
|
console.log("=== STARTING PF2E SPELL UPDATER ==="); |
|
|
|
// --- CONFIGURATION --- |
|
// Set to true if you have Dangerous Sorcery (adds spell level to damage). |
|
// Set to false to explicitly set the Damage MISC field to 0. |
|
const DANGEROUS_SORCERY = false; |
|
|
|
// Set your spellcasting tradition here. |
|
// OPTIONS: 'arcane', 'divine', 'primal', 'occult', or 'empty-string' |
|
const MAGIC_TRADITION = 'primal'; |
|
|
|
// --- GLOBAL TARGETS --- |
|
// Fetch the target global spell attack and spell DC from the top of the character sheet. |
|
const expectedAttack = parseInt(document.querySelector('span[name="attr_spell_attack"]')?.textContent, 10) || 0; |
|
const expectedDC = parseInt(document.querySelector('span[name="attr_spell_dc"]')?.textContent, 10) || 0; |
|
|
|
console.log(`[GLOBAL] Found Expected Attack: ${expectedAttack} | Expected DC: ${expectedDC}`); |
|
|
|
// Abort if the script can't find the global stats (usually means the sheet isn't popped out). |
|
if (!expectedAttack || !expectedDC) { |
|
console.error("[ERROR] Could not find the global fields. Ensure the sheet is popped out into its own window!"); |
|
return; |
|
} |
|
|
|
// --- GATHER & FILTER SPELLS --- |
|
// Grab every repeating item on the sheet, then filter down to just the ones we want to edit. |
|
const allRepItems = document.querySelectorAll('.repitem'); |
|
const targetSpells = Array.from(allRepItems).filter(spell => { |
|
const groupName = spell.closest('.repcontainer')?.getAttribute('data-groupname') || ''; |
|
|
|
// Only keep standard spells and cantrips |
|
if (!groupName.includes('spell') && !groupName.includes('cantrip')) return false; |
|
|
|
// Explicitly ignore Innate Spells and Focus Spells |
|
if (groupName.includes('spellinnate') || groupName.includes('spellfocus')) return false; |
|
|
|
return true; |
|
}); |
|
|
|
const totalSpells = targetSpells.length; |
|
console.log(`[SCAN] Found ${totalSpells} target spells to update.`); |
|
|
|
if (totalSpells === 0) return; |
|
|
|
// --- SAVE ORIGINAL WINDOW TITLE --- |
|
// We save this so we can restore it after using the title bar as a progress indicator. |
|
const originalTitle = document.title; |
|
|
|
// --- PROCESS SPELLS --- |
|
let currentIndex = 0; |
|
for (const spell of targetSpells) { |
|
currentIndex++; |
|
|
|
// Update the browser tab title with progress (e.g., "[1/20] Updating Spells...") |
|
document.title = `[${currentIndex}/${totalSpells}] Updating Spells...`; |
|
|
|
const nameEl = spell.querySelector('input[name*="name"]'); |
|
const spellName = nameEl ? nameEl.value : "Unnamed Spell"; |
|
console.log(`\n--- Processing [${currentIndex}/${totalSpells}]: ${spellName} ---`); |
|
|
|
// --- OPEN SPELL BLOCK --- |
|
// Click the settings button to reveal the backend HTML fields so they can be edited. |
|
const editBtn = spell.querySelector('button[name="act_settings"]'); |
|
if (editBtn) { |
|
editBtn.click(); |
|
// Wait for Roll20 to animate and render the internal fields |
|
await new Promise(resolve => setTimeout(resolve, 300)); |
|
} |
|
|
|
// --- UPDATE HELPER FUNCTION --- |
|
// Bypasses framework state managers to inject a value, then forces DOM events so Roll20 saves it. |
|
const updateField = (selector, val, fieldName, isSelect = false) => { |
|
const el = spell.querySelector(selector); |
|
if (el) { |
|
el.focus(); // Emulate a user clicking into the field |
|
|
|
// Call the native value setter directly |
|
const proto = isSelect ? window.HTMLSelectElement.prototype : window.HTMLInputElement.prototype; |
|
Object.getOwnPropertyDescriptor(proto, "value").set.call(el, val); |
|
|
|
// Dispatch standard browser events |
|
el.dispatchEvent(new Event('input', { bubbles: true })); |
|
el.dispatchEvent(new Event('change', { bubbles: true })); |
|
el.dispatchEvent(new Event('blur', { bubbles: true })); // Blur is critical for Roll20 saves |
|
|
|
// Force jQuery events for Roll20 sheet workers |
|
if (typeof $ !== 'undefined') { |
|
$(el).trigger('change').trigger('blur'); |
|
} |
|
console.log(`[WRITE] Updated ${fieldName} to: ${val}`); |
|
} else { |
|
console.warn(`[WARN] Could not find field for ${fieldName}`); |
|
} |
|
}; |
|
|
|
// --- PHASE 1: CLEAN SLATE --- |
|
// Set tradition and zero out MISC fields so Roll20 can calculate the pure base stats |
|
console.log(`[ACTION] Setting tradition and forcing Roll20 to recalculate base stats...`); |
|
updateField('select[name*="magic_tradition"]', MAGIC_TRADITION, 'Tradition', true); |
|
updateField('input[name*="spellattack_misc"]', '0', 'Temp Attack MISC', false); |
|
updateField('input[name*="spelldc_misc"]', '0', 'Temp Save DC MISC', false); |
|
|
|
// Wait for Roll20's sheet workers to process the tradition and update the DOM |
|
await new Promise(resolve => setTimeout(resolve, 600)); |
|
|
|
// --- PHASE 2: CALCULATE TRUE DIFFERENCE --- |
|
// Now that the base stats are accurate, calculate the exact gap to reach the global target. |
|
const currentAttackTotal = parseInt(spell.querySelector('span[name="attr_spellattack"]')?.textContent, 10) || 0; |
|
const currentDCTotal = parseInt(spell.querySelector('span[name="attr_spelldc"]')?.textContent, 10) || 0; |
|
|
|
const newAttackMisc = expectedAttack - currentAttackTotal; |
|
const newDCMisc = expectedDC - currentDCTotal; |
|
|
|
console.log(`[MATH] Attack gap: Expected (${expectedAttack}) - Current Base (${currentAttackTotal}) = ${newAttackMisc}`); |
|
console.log(`[MATH] Save DC gap: Expected (${expectedDC}) - Current Base (${currentDCTotal}) = ${newDCMisc}`); |
|
|
|
// --- PHASE 3: APPLY FINAL VALUES --- |
|
// Inject the calculated MISC values |
|
updateField('input[name*="spellattack_misc"]', newAttackMisc.toString(), 'Final Attack MISC', false); |
|
updateField('input[name*="spelldc_misc"]', newDCMisc.toString(), 'Final Save DC MISC', false); |
|
|
|
// Grab the base level of the spell to use for Dangerous Sorcery |
|
const levelInput = spell.querySelector('input[name*="spelllevel"]'); |
|
const spellLevel = levelInput ? levelInput.value : "0"; |
|
|
|
// Conditionally apply Damage MISC based on the configuration variable |
|
if (DANGEROUS_SORCERY) { |
|
updateField('input[name*="damage_misc"]', spellLevel, 'Damage MISC', false); |
|
} else { |
|
updateField('input[name*="damage_misc"]', '0', 'Damage MISC', false); |
|
} |
|
|
|
// --- CLOSE AND SAVE --- |
|
console.log(`[WAIT] Saving data to Roll20 backend...`); |
|
// Pause to let the database catch up before removing the element from the DOM |
|
await new Promise(resolve => setTimeout(resolve, 500)); |
|
|
|
// Click the gear icon again to collapse the spell block |
|
if (editBtn) { |
|
editBtn.click(); |
|
await new Promise(resolve => setTimeout(resolve, 150)); |
|
} |
|
} |
|
|
|
// --- RESTORE ORIGINAL TITLE --- |
|
// Put the browser tab title back to normal once we're done |
|
document.title = originalTitle; |
|
console.log("\n=== DONE UPDATING ALL SPELLS ==="); |
|
})(); |