(async () => { if (!token) { ui.notifications.warn("You must select a token!"); return; } const ac = actor; const magusLevel = ac.classes?.magus?.level ?? 0; const enhancementCap = Math.max(1, Math.floor((magusLevel - 1) / 4 + 1)); const arcanePool = ac.system?.resources?.classFeat_arcanePool?.value ?? 0; const FLAG_SCOPE = "world"; const FLAG_ROOT = "arcanePool"; const flagPath = (key) => `${FLAG_ROOT}.${key}`; const getFlag = (doc, key, fallback) => { const value = doc.getFlag(FLAG_SCOPE, flagPath(key)); return value === undefined ? fallback : value; }; const setFlag = (doc, key, value) => doc.setFlag(FLAG_SCOPE, flagPath(key), value); const readBuffFlag = (doc, key, fallback) => { const value = doc.getFlag(FLAG_SCOPE, flagPath(key)); return value === undefined ? fallback : value; }; const setBuffFlag = (doc, key, value) => doc.setFlag(FLAG_SCOPE, flagPath(key), value); function isAttackActive(item) { if (item.type !== "attack") return false; const equipped = item.system?.equipped; if (typeof equipped === "boolean") return equipped; const held = item.system?.held; if (typeof held === "string" && held.length) { const normalized = held.toLowerCase(); if (["none", "not", "unheld", "disabled", "inactive"].includes(normalized)) return false; } return true; } const equippedWeapons = (ac.itemTypes?.attack ?? ac.items.filter((item) => item.type === "attack")).filter( isAttackActive ); if (!equippedWeapons.length) { ui.notifications.error("No equipped weapon found! Equip a weapon before using Arcane Pool."); return; } const existingConfig = getFlag(ac, "config", {}) ?? {}; const currentWeapon = equippedWeapons.find((item) => item.id === existingConfig.weaponId) ?? equippedWeapons[0]; const SYNC_MACRO_NAME = "_arcanePoolApplyEffects"; const OPTION_KEY_MAP = { Keen: "keen", "Ghost Touch": "ghostTouch", Speed: "speed", Dancing: "dancing", "Brilliant Energy": "brilliantEnergy", Vorpal: "vorpal", Flaming: "flaming", "Flaming Burst": "flamingBurst", Frost: "frost", "Icy Burst": "icyBurst", Shock: "shock", "Shocking Burst": "shockingBurst", }; const BUFF_DEFINITIONS = { base: { name: "Arcane Pool (Enhancement)", aliases: ["Arcane Pool", "Arcane Pool - Enhancement"], img: "icons/magic/defensive/shield-barrier-flaming-pentagon.webp", role: "base", }, keen: { name: "Keen", aliases: ["Arcane Pool - Keen"], img: "icons/skills/melee/weapons-crossed-swords-yellow.webp", property: "keen", }, ghostTouch: { name: "Ghost Touch", aliases: ["Arcane Pool - Ghost Touch"], img: "icons/magic/perception/eye-slit-pink.webp", property: "ghost-touch", }, speed: { name: "Speed", aliases: ["Arcane Pool - Speed"], img: "icons/magic/movement/trail-streak-zigzag-yellow.webp", property: "speed", }, dancing: { name: "Dancing", aliases: ["Arcane Pool - Dancing"], img: "icons/magic/control/fear-fright-monster-green.webp", property: "dancing", }, brilliantEnergy: { name: "Brilliant Energy", aliases: ["Arcane Pool - Brilliant Energy"], img: "icons/magic/light/projectile-beam-strike-yellow.webp", property: "brilliant-energy", }, vorpal: { name: "Vorpal", aliases: ["Arcane Pool - Vorpal"], img: "icons/skills/melee/blade-tip-triple-blue.webp", property: "vorpal", }, flaming: { name: "Flaming", aliases: ["Arcane Pool - Flaming"], img: "icons/magic/fire/flame-burning-sword.webp", property: "flaming", damageFormula: "1d6[fire]", }, flamingBurst: { name: "Flaming Burst", aliases: ["Arcane Pool - Flaming Burst"], img: "icons/magic/fire/explosion-fireball-medium-red.webp", property: "flaming-burst", damageFormula: "1d6[fire]+1d10[fire]", }, frost: { name: "Frost", aliases: ["Arcane Pool - Frost"], img: "icons/magic/water/snowflake-ice-blue.webp", property: "frost", damageFormula: "1d6[cold]", }, icyBurst: { name: "Icy Burst", aliases: ["Arcane Pool - Icy Burst"], img: "icons/magic/water/projectile-ice-snowball.webp", property: "icy-burst", damageFormula: "1d6[cold]+1d10[cold]", }, shock: { name: "Shock", aliases: ["Arcane Pool - Shock"], img: "icons/magic/lightning/bolt-forked-blue.webp", property: "shock", damageFormula: "1d6[electricity]", }, shockingBurst: { name: "Shocking Burst", aliases: ["Arcane Pool - Shocking Burst"], img: "icons/magic/lightning/bolt-strike-beam-blue.webp", property: "shocking-burst", damageFormula: "1d6[electricity]+1d10[electricity]", }, }; async function ensureSyncScript(buff) { const script = ` const macro = game.macros.getName("${SYNC_MACRO_NAME}"); if (macro) { macro.execute([{ actorId: this.actor?.id }]); }`; const scriptCalls = foundry.utils.duplicate(buff.system?.scriptCalls ?? []); const existing = scriptCalls.find((entry) => entry.name === "Arcane Pool Sync"); if (existing) { if (existing.value !== script) { existing.value = script; await buff.update({ "system.scriptCalls": scriptCalls }); } return; } scriptCalls.push({ _id: foundry.utils.randomID(8), name: "Arcane Pool Sync", img: "icons/svg/coins.svg", type: "script", category: "toggle", value: script, hidden: false, }); await buff.update({ "system.scriptCalls": scriptCalls }); } async function ensureArcanePoolBuff(actorDocument, key) { const definition = BUFF_DEFINITIONS[key]; if (!definition) return null; const lookupNames = [definition.name, ...(definition.aliases ?? [])]; const buff = actorDocument.items.find( (item) => item.type === "buff" && lookupNames.includes(item.name) ); if (!buff) { const message = `Arcane Pool requires the "${definition.name}" buff on ${actorDocument.name}. Please add it to the actor.`; ui.notifications.error(message); throw new Error(message); } if (readBuffFlag(buff, "tag") !== key) await setBuffFlag(buff, "tag", key); if (readBuffFlag(buff, "role") !== (definition.role ?? "property")) { await setBuffFlag(buff, "role", definition.role ?? "property"); } if (readBuffFlag(buff, "property") !== (definition.property ?? null)) { await setBuffFlag(buff, "property", definition.property ?? null); } if (readBuffFlag(buff, "damageFormula") !== (definition.damageFormula ?? null)) { await setBuffFlag(buff, "damageFormula", definition.damageFormula ?? null); } await ensureSyncScript(buff); return buff; } const DIALOG_WIDTH = 480; // 20% wider than the default ~400px dialog function buildDialog(weaponName) { return `

⚒️ ${weaponName}

Enhancements are applied via Arcane Pool buff items and stay in sync with any toggle source.

🔮 Arcana Options:
Enhancement Options Cost
Keen [1] 0
Ghost Touch [1] 0
Speed [3] 0
Dancing [4] 0
Brilliant Energy [4] 0
Vorpal [5] 0
Fire None Flaming Flaming Burst 0
Ice None Frost Icy Burst 0
Lightning None Shock Shocking Burst 0
Enhancement Bonus Used: 0 / ${enhancementCap}
Arcane Pool Cost: 1 point(s)
Weapon Enhancement Applied: ${enhancementCap}
`; } function gatherSelections(html) { const selections = []; html.find('input[type="checkbox"]:checked').each(function () { if (this.id && this.id !== "enduring-blade" && this.id !== "ghost-blade") { const value = parseInt(this.value) || 0; selections.push({ id: this.id, cost: value, damage: $(this).data("damage") || "" }); } }); html.find('input[type="radio"]:checked').each(function () { const value = parseInt(this.value) || 0; if (value > 0 && this.id) { selections.push({ id: this.id, cost: value, damage: $(this).data("damage") || "" }); } }); return selections; } new Dialog({ title: `Arcane Pool - ${currentWeapon.name}`, content: buildDialog(currentWeapon.name), buttons: { confirm: { label: "Apply Enhancements", icon: '', callback: async (html) => { try { const syncMacro = game.macros.getName(SYNC_MACRO_NAME); if (!syncMacro) { ui.notifications.error( `Missing helper macro "${SYNC_MACRO_NAME}". Please create it with macro_arcanePool_applyEffects.js.` ); return; } const enhancementUsed = parseInt(html.find("#enhancement-used").text()) || 0; const poolCost = parseInt(html.find("#pool-cost").text()) || 1; const enduringBlade = html.find("#enduring-blade").is(":checked"); const ghostBlade = html.find("#ghost-blade").is(":checked"); if (enhancementUsed > enhancementCap) { ui.notifications.error("Enhancement bonus exceeds current cap."); return; } const currentPool = ac.system.resources.classFeat_arcanePool.value ?? 0; if (poolCost > currentPool) { ui.notifications.error("Insufficient Arcane Pool points."); return; } const selections = gatherSelections(html); const desiredKeys = new Set(["base"]); for (const sel of selections) { const mapped = OPTION_KEY_MAP[sel.id]; if (mapped) desiredKeys.add(mapped); } const baseEnhancement = Math.max(0, enhancementCap - enhancementUsed); const ensuredBuffs = new Map(); for (const key of desiredKeys) { const buff = await ensureArcanePoolBuff(ac, key); ensuredBuffs.set(key, buff); } const baseBuff = ensuredBuffs.get("base"); if (baseBuff) { await setBuffFlag(baseBuff, "weaponId", currentWeapon.id); await setBuffFlag(baseBuff, "enhancementBonus", baseEnhancement); await setBuffFlag(baseBuff, "poolCost", poolCost); await setBuffFlag(baseBuff, "enduringBlade", enduringBlade); await setBuffFlag(baseBuff, "ghostBlade", ghostBlade); } const arcaneBuffs = ac.items.filter( (item) => item.type === "buff" && readBuffFlag(item, "tag") ); for (const buff of arcaneBuffs) { const tag = readBuffFlag(buff, "tag"); const shouldBeActive = desiredKeys.has(tag); if (buff.system?.active !== shouldBeActive) { await buff.update({ "system.active": shouldBeActive }); } } await setFlag(ac, "config", { weaponId: currentWeapon.id, enhancementBonus: baseEnhancement, poolCost, enduringBlade, ghostBlade, }); await syncMacro.execute([ { actorId: ac.id, weaponId: currentWeapon.id, enhancementBonus: baseEnhancement, poolCost, enduringBlade, ghostBlade, }, ]); await ac.update({ "system.resources.classFeat_arcanePool.value": Math.max(currentPool - poolCost, 0), }); const labels = selections.map((sel) => sel.id); const appliedLabel = labels.length > 0 ? `${labels.join(", ")} with +${baseEnhancement} enhancement` : `+${baseEnhancement} weapon enhancement`; ui.notifications.info( `Arcane Pool applied to ${currentWeapon.name}: ${appliedLabel}` ); } catch (error) { console.error("Arcane Pool selector failed:", error); ui.notifications.error(error.message ?? "Failed to apply Arcane Pool enhancements."); } }, disabled: true, }, cancel: { label: "Cancel", }, }, default: "confirm", render: (html) => { const confirmButton = html.closest(".dialog").find('button[data-button="confirm"]'); html.find("#ghost-blade").on("change", function () { const isChecked = $(this).is(":checked"); html.find('.enhancement-option[data-requires="ghost-blade"]').prop("disabled", !isChecked); if (!isChecked) { html.find('.enhancement-option[data-requires="ghost-blade"]').prop("checked", false); } calculateSum(); }); html.find(".action, .arcana-checkbox, #input-value").on("change", calculateSum); calculateSum(); function calculateSum() { let enhancementSum = 0; const cap = parseInt(html.find("#input-value").val()) || 0; const poolAvailable = parseInt(html.find("#arcane-pool-value").val()) || 0; html.find('[name="InputTable"] tbody tr').each(function () { const actionCell = $(this).find(".action:checked"); const valueCell = $(this).find(".value"); const value = actionCell.length ? parseInt(actionCell.val()) : 0; if (value > 0) enhancementSum += value; valueCell.text(value); valueCell.removeClass("cost-high cost-medium cost-low"); if (value >= 4) valueCell.addClass("cost-high"); else if (value >= 2) valueCell.addClass("cost-medium"); else if (value > 0) valueCell.addClass("cost-low"); }); let poolCost = 1; html.find(".arcana-checkbox:checked").each(function () { poolCost += parseInt($(this).val()) || 0; }); html.find("#enhancement-used").text(enhancementSum); html.find("#pool-cost").text(poolCost); const baseEnhancement = Math.max(0, cap - enhancementSum); html.find("#base-enhancement").text(baseEnhancement); const enhancementExceeded = enhancementSum > cap; const poolInsufficient = poolCost > poolAvailable; html.find("#enhancement-used").toggleClass("warning", enhancementExceeded); html.find("#pool-cost").toggleClass("warning", poolInsufficient); confirmButton.prop("disabled", enhancementExceeded || poolInsufficient); } }, }, { width: DIALOG_WIDTH }).render(true); })();