diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0b3f3cd..cfe9912 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -62,7 +62,7 @@ The `module/data/` directory is laid out as follows: ### Macros -Many facets of automation must be handled with macros. These are attached using [Item Macro](https://foundryvtt.com/packages/itemacro/), which appear as an escaped string in data. To add some human-readability, macros are handled separately and merged during the module build. +Many facets of automation must be handled with macros. These are attached using [DAE](https://foundryvtt.com/packages/dae)'s "DIME", and appear as an escaped string in data. To add some human-readability, macros are handled separately and merged during the module build. In the [`macro-item/`](./macro-item) directory is a directory for each datatype. Save your (well-formatted, commented) macro code as a Javascript file in one of these directories, structured as an async function named `macro`, with filename `_.js` (e.g. `XGE_toll-the-dead.js`). **Note that the first and last lines of the file**—the ones that turn the macro into an async function—**are stripped on compilation into the module's data**. diff --git a/README.md b/README.md index f0671ef..5a33949 100644 --- a/README.md +++ b/README.md @@ -25,10 +25,8 @@ While the module is activated in your world, each time Plutonium would import a document, it will check if any automation data is available for that document. If so, it will import the document with the additional automation data included. If not, it will import the document as if *Plutonium Addon: Automation* were not enabled (although sometimes slightly and non-destructively tweaked for better compatibility with the modules below). -Some automations don't use effects directly, but rather call item macros. If you, as the GM, wish to allow players to view/edit macros on items they own, install [Item Macro](https://foundryvtt.com/packages/itemacro) and configure it to allow the _Player access_ option. - > [!TIP] -> **There's no need to import everything prematurely**, 'just in case'. As with Plutonium, all data is included in the module itself, and, if you use any [module integrations](#optional-integrations), you'll be missing out on updates from them. +> **There's no need to import everything prematurely**, 'just in case'. As with Plutonium, all data is included in the module itself, and, if you use any [module integrations](#optional-integrations), you'll be missing out on updates from them. ## Installation @@ -47,7 +45,6 @@ Besides *Plutonium* itself, *Plutonium Addon: Automation* **requires**: Some specific automations require one or more additional modules: - [Active-Auras](https://foundryvtt.com/packages/ActiveAuras) - [Active Token Effects](https://foundryvtt.com/packages/ATL) -- [Item Macro](https://foundryvtt.com/packages/itemacro) - [Warp Gate](https://foundryvtt.com/packages/warpgate) If you import a document which requires one of these modules, you will be prompted to install/activate it. The automation is highly unlikely to function as intended if you use it before activating the module. @@ -62,12 +59,6 @@ You **must** configure some modules in a specific way, or the automations won't - _**DFreds Convenient Effects** > Modify Status Effects_ — select either `Replace` (preferred) or `Add`. - _**Midi QoL** > Midi QoL config > Workflow > Apply Convenient Effects_ — select `Apply Item effects, if absent apply CE`. -If you have [Item Macro](https://foundryvtt.com/packages/itemacro) activated, you also need to configure it as follows: - -- _**Item Macro** > Override default macro execution_ — uncheck this. -- _**Item Macro** > Character Sheet Hook_ — uncheck this. -- (If installed) _**[Token Action HUD](https://foundryvtt.com/packages/token-action-hud)** > Item-Macro: item macro, original item, or both_ — select `Show the original item`. (Note this is a user setting, so either ensure that each user configures this or use a module such as [Force Client Settings](https://foundryvtt.com/packages/force-client-settings) to guarantee it. Also note that this is _not_ required if you use the system-specific [Token Action HUD Core](https://foundryvtt.com/packages/token-action-hud-core) and [Token Action HUD D&D 5e](https://foundryvtt.com/packages/token-action-hud-dnd5e) modules.) - ### Optional integrations *Plutonium Addon: Automation* can make use of data provided by other modules to provide better automations for more documents. An integration is available for: diff --git a/macro-item/spell/Generic_ddbi-aa-damage-on-entry.js b/macro-item/spell/Generic_ddbi-aa-damage-on-entry.js index 1354d80..be47752 100644 --- a/macro-item/spell/Generic_ddbi-aa-damage-on-entry.js +++ b/macro-item/spell/Generic_ddbi-aa-damage-on-entry.js @@ -23,23 +23,27 @@ async function macro (args) { const saveAbility = ddbEffectFlags.save; const casterToken = canvas.tokens.placeables.find((t) => t.actor?.uuid === caster.uuid); const scalingDiceArray = item.system.scaling.formula.split("d"); - const scalingDiceNumber = itemLevel - item.system.level; + const scalingDiceNumber = item.system.scaling.mode === "none" + ? 0 + : itemLevel - item.system.level; const upscaledDamage = isCantrip ? `${game.modules.get("plutonium-addon-automation").api.DdbImporter.effects.getCantripDice(caster)}d${scalingDiceArray[1]}[${damageType}]` : scalingDiceNumber > 0 ? `${scalingDiceNumber}d${scalingDiceArray[1]}[${damageType}] + ${damageDice}` : damageDice; - const workflowItemData = duplicate(item); + const workflowItemData = foundry.utils.duplicate(item); workflowItemData.system.target = {value: 1, units: "", type: "creature"}; workflowItemData.system.save.ability = saveAbility; - workflowItemData.system.components.concentration = false; + workflowItemData.system.properties = game.modules.get("plutonium-addon-automation").api.DdbImporter.effects.removeFromProperties(workflowItemData.system.properties, "concentration") ?? []; workflowItemData.system.level = itemLevel; workflowItemData.system.duration = {value: null, units: "inst"}; workflowItemData.system.target = {value: null, width: null, units: "", type: "creature"}; + workflowItemData.system.uses = {value: null, max: "", per: null, recovery: "", autoDestroy: false}; + workflowItemData.system.consume = {"type": "", "target": null, "amount": null}; - setProperty(workflowItemData, "flags.itemacro", {}); - setProperty(workflowItemData, "flags.midi-qol", {}); - setProperty(workflowItemData, "flags.dae", {}); - setProperty(workflowItemData, "effects", []); + foundry.utils.setProperty(workflowItemData, "flags.itemacro", {}); + foundry.utils.setProperty(workflowItemData, "flags.midi-qol", {}); + foundry.utils.setProperty(workflowItemData, "flags.dae", {}); + foundry.utils.setProperty(workflowItemData, "effects", []); delete workflowItemData._id; const saveOnEntry = ddbEffectFlags.saveOnEntry; @@ -48,19 +52,11 @@ async function macro (args) { // eslint-disable-next-line new-cap const entryItem = new CONFIG.Item.documentClass(workflowItemData, {parent: caster}); // console.warn("Saving item on entry", {entryItem, targetToken}); - const options = { - showFullCard: false, - createWorkflow: true, - targetUuids: [targetToken.document.uuid], - configureDialog: false, - versatile: false, - consumeResource: false, - consumeSlot: false, - }; - await MidiQOL.completeItemUse(entryItem, {}, options); + const [config, options] = game.modules.get("plutonium-addon-automation").api.DdbImporter.effects.syntheticItemWorkflowOptions({targets: [targetToken.document.uuid]}); + await MidiQOL.completeItemUse(entryItem, config, options); } else { const damageRoll = await new CONFIG.Dice.DamageRoll(upscaledDamage).evaluate({async: true}); - if (game.dice3d) game.dice3d.showForRoll(damageRoll); + await MidiQOL.displayDSNForRoll(damageRoll, "damageRoll"); workflowItemData.name = `${workflowItemData.name}: Turn Entry Damage`; @@ -72,7 +68,7 @@ async function macro (args) { [targetToken], damageRoll, { - flavor: `(${CONFIG.DND5E.damageTypes[damageType]})`, + flavor: `(${CONFIG.DND5E.damageTypes[damageType].label})`, itemCardId: "new", itemData: workflowItemData, isCritical: false, @@ -84,7 +80,7 @@ async function macro (args) { if (args[0].tag === "OnUse" && args[0].macroPass === "preActiveEffects") { const safeName = lastArg.itemData.name.replace(/\s|'|\.|’/g, "_"); const dataTracker = { - randomId: randomID(), + randomId: foundry.utils.randomID(), targetUuids: lastArg.targetUuids, startRound: game.combat.round, startTurn: game.combat.turn, @@ -92,10 +88,10 @@ async function macro (args) { }; const item = await fromUuid(lastArg.itemUuid); - await DAE.unsetFlag(item, `${safeName}Tracker`); - await DAE.setFlag(item, `${safeName}Tracker`, dataTracker); + await DAE.unsetFlag(item.actor, `${safeName}Tracker`); + await DAE.setFlag(item.actor, `${safeName}Tracker`, dataTracker); - const ddbEffectFlags = lastArg.item.flags["plutonium-addon-automation"]?.effect; + const ddbEffectFlags = lastArg.item.flags.ddbimporter?.effect; if (ddbEffectFlags) { const sequencerFile = ddbEffectFlags.sequencerFile; @@ -114,8 +110,8 @@ async function macro (args) { }); return effect; }); - args[0].item.effects = duplicate(newEffects); - args[0].itemData.effects = duplicate(newEffects); + args[0].item.effects = foundry.utils.duplicate(newEffects); + args[0].itemData.effects = foundry.utils.duplicate(newEffects); } const template = await fromUuid(lastArg.templateUuid); await template.update({"flags.effect": ddbEffectFlags}); @@ -128,7 +124,7 @@ async function macro (args) { const targetItemTracker = DAE.getFlag(item.parent, `${safeName}Tracker`); const originalTarget = targetItemTracker.targetUuids.includes(lastArg.tokenUuid); const target = canvas.tokens.get(lastArg.tokenId); - const targetTokenTrackerFlag = DAE.getFlag(target, `${safeName}Tracker`); + const targetTokenTrackerFlag = DAE.getFlag(target.actor, `${safeName}Tracker`); const targetedThisCombat = targetTokenTrackerFlag && targetItemTracker.randomId === targetTokenTrackerFlag.randomId; const targetTokenTracker = targetedThisCombat ? targetTokenTrackerFlag @@ -154,17 +150,17 @@ async function macro (args) { targetTokenTracker.hasLeft = false; await rollItemDamage(target, lastArg.efData.origin, targetItemTracker.spellLevel); } - await DAE.setFlag(target, `${safeName}Tracker`, targetTokenTracker); + await DAE.setFlag(target.actor, `${safeName}Tracker`, targetTokenTracker); } else if (args[0] === "off") { const safeName = (lastArg.efData.name ?? lastArg.efData.label).replace(/\s|'|\.|’/g, "_"); const target = canvas.tokens.get(lastArg.tokenId); - const targetTokenTracker = DAE.getFlag(target, `${safeName}Tracker`); + const targetTokenTracker = DAE.getFlag(target.actor, `${safeName}Tracker`); if (targetTokenTracker) { targetTokenTracker.hasLeft = true; targetTokenTracker.turn = game.combat.turn; targetTokenTracker.round = game.combat.round; - await DAE.setFlag(target, `${safeName}Tracker`, targetTokenTracker); + await DAE.setFlag(target.actor, `${safeName}Tracker`, targetTokenTracker); } } } diff --git a/macro-item/spell/PHB_branding-smite.js b/macro-item/spell/PHB_branding-smite.js deleted file mode 100644 index 9fcf398..0000000 --- a/macro-item/spell/PHB_branding-smite.js +++ /dev/null @@ -1,57 +0,0 @@ -/** - * Credit to D&D Beyond Importer (https://github.com/MrPrimate/ddb-importer) - * See: `../../license/ddb-importer.md` - */ -async function macro (args) { - try { - if (!["mwak", "rwak"].includes(args[0].item.system.actionType)) return {}; - if (args[0].hitTargetUuids.length === 0) return {}; // did not hit anyone - for (let tokenUuid of args[0].hitTargetUuids) { - const target = await fromUuid(tokenUuid); - const targetActor = target.actor; - if (!targetActor) continue; - // remove the invisible condition - const effect = targetActor?.effects.find((ef) => (ef.name ?? ef.label) === game.i18n.localize("midi-qol.invisible")); - if (effect) { await MidiQOL.socket().executeAsGM("removeEffects", {actorUuid: targetActor.uuid, effects: [effect.id]}); } - // create the dim light effect on the target - let bsEffect = new ActiveEffect({ - label: "Branding Smite", - name: "Branding Smite", - icon: "icons/magic/fire/dagger-rune-enchant-flame-strong-purple.webp", - changes: [ - { - value: 5, - mode: CONST.ACTIVE_EFFECT_MODES.UPGRADE, - priority: 20, - key: "ATL.light.dim", - }, - ], - duration: {seconds: 60}, - }); - // 60 seconds is wrong - should look for the branding smite effect and use the remaining duration - but hey - - await MidiQOL.socket().executeAsGM("createEffects", { - actorUuid: targetActor.uuid, - effects: [bsEffect.toObject()], - }); - } - Hooks.once("midi-qol.RollComplete", (workflow) => { - console.log("Deleting concentration"); - const effect = MidiQOL.getConcentrationEffect(actor); - if (effect) effect.delete(); - return true; - }); - const spellLevel = actor.flags["midi-qol"].brandingSmite.level; - const workflow = args[0].workflow; - const rollOptions = { - critical: workflow.isCritical, - criticalMultiplier: workflow.damageRoll?.options?.criticalMultiplier, - powerfulCritical: workflow.damageRoll?.options?.powerfulCritical, - multiplyNumeric: workflow.damageRoll?.options?.multiplyNumeric, - }; - const damageFormula = new CONFIG.Dice.DamageRoll(`${spellLevel}d6[radiant]`, {}, rollOptions); - return {damageRoll: damageFormula.formula, flavor: "Branding Smite"}; - } catch (err) { - console.error(`${args[0].itemData.name} - Branding Smite`, err); - } -} diff --git a/macro-item/spell/PHB_color-spray.js b/macro-item/spell/PHB_color-spray.js index b42cc91..ffbff9c 100644 --- a/macro-item/spell/PHB_color-spray.js +++ b/macro-item/spell/PHB_color-spray.js @@ -4,15 +4,8 @@ */ async function macro (args) { // based on @ccjmk and @crymic macro for sleep. - // uses convinient effects // Midi-qol "On Use" - async function wait (ms) { - return new Promise((resolve) => { - setTimeout(resolve, ms); - }); - } - const blindHp = await args[0].damageTotal; const immuneConditions = [game.i18n.localize("Blinded"), game.i18n.localize("Unconscious")]; console.log(`Color Spray Spell => Available HP Pool [${blindHp}] points`); @@ -29,7 +22,7 @@ async function macro (args) { if (remainingBlindHp >= targetHpValue) { remainingBlindHp -= targetHpValue; - console.log(`Color Spray Results => Target: ${findTarget.name} | HP: ${targetHpValue} | HP Pool: ${remainingBlindHp} | Status: Blinded`); + console.log(`Color Spray Results => Target: ${findTarget.name} | HP: ${targetHpValue} | HP Pool: ${remainingBlindHp} | Status: Blinded`); blindTarget.push(`
Blinded
${findTarget.name}
`); const gameRound = game.combat ? game.combat.round : 0; const effectData = { @@ -41,7 +34,7 @@ async function macro (args) { duration: {startRound: gameRound, startTime: game.time.worldTime}, flags: {dae: {specialDuration: ["turnEndSource"]}}, changes: [ - {key: "macro.CE", mode: CONST.ACTIVE_EFFECT_MODES.CUSTOM, value: game.i18n.localize("Blinded"), priority: 20}, + game.modules.get("plutonium-addon-automation").api.DdbImporter.effects.generateStatusEffectChange(game.i18n.localize("Blinded"), 20), ], }; @@ -51,10 +44,10 @@ async function macro (args) { blindTarget.push(`
misses
${findTarget.name}
`); } } - await wait(500); + await game.modules.get("plutonium-addon-automation").api.DdbImporter.effects.wait(500); const blindResults = `
${blindTarget.join("")}
`; const chatMessage = game.messages.get(args[0].itemCardId); - let content = duplicate(chatMessage.content); + let content = foundry.utils.duplicate(chatMessage.content); const searchString = /
[\s\S]*
/g; const replaceString = `
${blindResults}`; content = await content.replace(searchString, replaceString); diff --git a/macro-item/spell/PHB_elemental-weapon.js b/macro-item/spell/PHB_elemental-weapon.js deleted file mode 100644 index 384cf92..0000000 --- a/macro-item/spell/PHB_elemental-weapon.js +++ /dev/null @@ -1,198 +0,0 @@ -/** - * Credit to D&D Beyond Importer (https://github.com/MrPrimate/ddb-importer) - * See: `../../license/ddb-importer.md` - */ -async function macro (args) { - const lastArg = args[args.length - 1]; - const tokenOrActor = await fromUuid(lastArg.actorUuid); - const targetActor = tokenOrActor.actor ? tokenOrActor.actor : tokenOrActor; - - function valueLimit (val, min, max) { - return val < min ? min : val > max ? max : val; - } - - async function selectDamage () { - const damageTypes = { - acid: "icons/magic/acid/dissolve-bone-white.webp", - cold: "icons/magic/water/barrier-ice-crystal-wall-jagged-blue.webp", - fire: "icons/magic/fire/barrier-wall-flame-ring-yellow.webp", - lightning: "icons/magic/lightning/bolt-strike-blue.webp", - thunder: "icons/magic/sonic/explosion-shock-wave-teal.webp", - }; - function generateEnergyBox (type) { - return ` - - `; - } - const damageSelection = Object.keys(damageTypes).map((type) => generateEnergyBox(type)).join("\n"); - const content = ` - -
-
- ${damageSelection} -
-
- `; - const damageType = await new Promise((resolve) => { - new Dialog({ - title: "Choose a damage type", - content, - buttons: { - ok: { - label: "Choose!", - callback: async (html) => { - const element = html.find("input[type='radio'][name='type']:checked").val(); - resolve(element); - }, - }, - }, - }).render(true); - }); - return damageType; - } - - /** - * Select for weapon and apply bonus based on spell level - */ - if (args[0] === "on") { - const weapons = targetActor.items.filter((i) => i.type === "weapon" && !i.system.properties.mgc); - let weaponContent = ""; - - // Filter for weapons - weapons.forEach((weapon) => { - weaponContent += ``; - }); - - let content = ` - -
-
- ${weaponContent} -
-
- `; - - new Dialog({ - content, - buttons: { - ok: { - label: `Ok`, - callback: async () => { - const itemId = $("input[type='radio'][name='weapon']:checked").val(); - const weaponItem = targetActor.items.get(itemId); - let copyItem = duplicate(weaponItem); - const spellLevel = Math.floor(args[1] / 2); - const bonus = valueLimit(spellLevel, 1, 3); - const wpDamage = copyItem.system.damage.parts[0][0]; - const verDamage = copyItem.system.damage.versatile; - DAE.setFlag(targetActor, "magicWeapon", { - name: weaponItem.name, - attackBonus: weaponItem.system.attackBonus, - weapon: itemId, - damage: weaponItem.system.damage, - mgc: copyItem.system.properties.mgc, - }); - copyItem.name = `${weaponItem.name} (Elemental Weapon)`; - if (copyItem.system.attackBonus === "") copyItem.system.attackBonus = "0"; - copyItem.system.attackBonus = `${parseInt(copyItem.system.attackBonus) + bonus}`; - copyItem.system.damage.parts[0][0] = `${wpDamage} + ${bonus}`; - copyItem.system.properties.mgc = true; - if (verDamage !== "" && verDamage !== null) copyItem.system.damage.versatile = `${verDamage} + ${bonus}`; - - const damageType = await selectDamage(); - copyItem.system.damage.parts.push([`${bonus}d4[${damageType}]`, damageType]); - targetActor.updateEmbeddedDocuments("Item", [copyItem]); - }, - }, - cancel: { - label: `Cancel`, - }, - }, - }).render(true); - } - - // Revert weapon and unset flag. - if (args[0] === "off") { - const {name, attackBonus, weapon, damage, mgc} = DAE.getFlag(targetActor, "magicWeapon"); - const weaponItem = targetActor.items.get(weapon); - let copyItem = duplicate(weaponItem); - copyItem.name = name; - copyItem.system.attackBonus = attackBonus; - copyItem.system.damage = damage; - copyItem.system.properties.mgc = mgc; - targetActor.updateEmbeddedDocuments("Item", [copyItem]); - DAE.unsetFlag(targetActor, "magicWeapon"); - } -} diff --git a/macro-item/spell/PHB_ensnaring-strike.js b/macro-item/spell/PHB_ensnaring-strike.js index d1f3920..addc4f7 100644 --- a/macro-item/spell/PHB_ensnaring-strike.js +++ b/macro-item/spell/PHB_ensnaring-strike.js @@ -6,39 +6,40 @@ async function macro (args) { // Based on a macro by Elwin#1410 with permission. Thanks Elwin! // // Optional AA setup: - // A special custom restrained condition can be added for Ensnaring Strike. - // - In the AA Auto Rec menu, select the Active Effects tab. - // - Duplicate the Restrained entry. - // - Rename the duplicate to "Restrained [Ensnaring Strike]" - // - Change the animation to something more appropriate for the spell, e.g.: - // - Type: Spell - // - Animation: Entangle - // - Variant: 01 - // - Color: Green + // A special custom restrained condition can be added for Ensnaring Strike. + // - In the AA Auto Rec menu, select the Active Effects tab. + // - Create a new entry.name "Restrained [Ensnaring Strike]" + // - Animation Type: On Token + // - Primary Animation: (Set the animation to something more appropriate for the spell), e.g.: + // - Type: Spell + // - Animation: Entangle + // - Variant: 01 + // - Color: Green + // - Options: + // - Persistent: (checked) if (args[0].tag === "OnUse" && ["preTargeting"].includes(args[0].macroPass)) { args[0].workflow.item.system["target"]["type"] = "self"; return; } - const itemName = "Ensnaring Strike"; - const icon = "icons/magic/nature/root-vine-entangled-hand.webp"; + const DEFAULT_ITEM_NAME = "Ensnaring Strike"; /** * Returns a temporary spell item data for the Ensnaring Strike effect. * - * @param {*} sourceActor the actor that casted the origin spell item. - * @param {*} originItem the origin spell item that was cast. - * @param {*} originEffect the effect from the origin spell item that was cast. + * @param {Actor5e} sourceActor the actor that casted the origin spell item. + * @param {Item5e} originItem the origin spell item that was cast. + * @param {ActiveEffect5e} originEffect the effect from the origin spell item that was cast. * @returns temporary spell item data for Ensnaring Strike effect. */ function getTempSpellData (sourceActor, originItem, originEffect) { - const level = getProperty(originEffect, "flags.midi-qol.castData.castLevel") ?? 1; + const level = foundry.utils.getProperty(originEffect, "flags.midi-qol.castData.castLevel") ?? 1; const nbDice = level; // Get restrained condition id - const statusId = CONFIG.statusEffects.find(se => (se.name ?? se.label) === CONFIG.DND5E.conditionTypes["restrained"])?.id; - const conEffect = MidiQOL.getConcentrationEffect(sourceActor); + const statusId = CONFIG.statusEffects.find(se => se.name === CONFIG.DND5E.conditionTypes["restrained"].label)?.id; + const conEffect = MidiQOL.getConcentrationEffect(sourceActor, originItem); // Temporary spell data for the ensnaring effect. // Note: we keep same id as origin spell to make sure that the AEs have the same origin @@ -57,7 +58,6 @@ async function macro (args) { }, effects: [ { - _id: randomID(), changes: [ { key: "StatusEffect", @@ -65,26 +65,19 @@ async function macro (args) { value: statusId, priority: 20, }, - { - key: "flags.dae.deleteUuid", - mode: CONST.ACTIVE_EFFECT_MODES.CUSTOM, - value: conEffect.uuid, - priority: 20, - }, { key: "flags.midi-qol.OverTime", mode: CONST.ACTIVE_EFFECT_MODES.CUSTOM, - value: `turn=start,damageRoll=${nbDice}d6,damageType=piercing,label=${originItem.name}: Effect,actionSave=true,rollType=check,saveAbility=str,saveDC=@attributes.spelldc,killAnim=true`, + value: `turn=start,damageRoll=${nbDice}d6,damageType=piercing,label=${originItem.name}: Effect,actionSave=roll,rollType=check,saveAbility=str,saveDC=@attributes.spelldc,killAnim=true`, priority: 20, }, ], origin: originItem.uuid, disabled: false, transfer: false, - icon, - label: originItem.name, + img: originItem.img, name: originItem.name, - duration: game.modules.get("plutonium-addon-automation")?.api.DdbImporter.effects.getRemainingDuration(conEffect.duration), + duration: game.modules.get("plutonium-addon-automation").api.DdbImporter.effects.getRemainingDuration(conEffect.duration), }, ], }; @@ -93,94 +86,78 @@ async function macro (args) { if (args[0].tag === "OnUse" && args[0].macroPass === "postActiveEffects") { const macroData = args[0]; - const workflow = MidiQOL.Workflow.getWorkflow(macroData.uuid); - if (workflow?.options?.skipOnUse) { + if (workflow.options?.skipOnUse) { // Skip onUse when temporary effect item is used (this is a custom option that is passed to completeItemUse) return; } - if (macroData.hitTargets.length < 1) { + if (workflow.hitTargets.size < 1) { // No target hit return; } - if (!["mwak", "rwak"].includes(macroData.itemData.system.actionType)) { + if (!["mwak", "rwak"].includes(rolledItem?.system?.actionType)) { // Not a weapon attack return; } - const originItem = await fromUuid(macroData.sourceItemUuid); - if (!originItem) { - // Could not find origin item - console.error(`${itemName}: origin item ${macroData.sourceItemUuid} not found.`); - return; - } - const sourceActor = await fromUuid(macroData.actorUuid); - if (sourceActor.getFlag("world", "ensnaring-strike.used")) { - // Effect already applied to target - console.warn(`${itemName}: spell already used.`); - return; - } - - const originEffect = sourceActor.effects.find((ef) => - ef.getFlag("midi-qol", "castData.itemUuid") === macroData.sourceItemUuid, + const originEffect = actor.effects.find((ef) => + ef.getFlag("midi-qol", "castData.itemUuid") === macroItem.uuid, ); if (!originEffect) { - console.error(`${itemName}: spell active effect was not found.`); + console.error(`${DEFAULT_ITEM_NAME}: spell active effect was not found.`); return; } - // Flag the spell as used. - let originEffectChanges = duplicate(originEffect.changes); - originEffectChanges.push({ - key: "flags.world.ensnaring-strike.used", - mode: CONST.ACTIVE_EFFECT_MODES.OVERRIDE, - value: "1", - priority: 20, - }); - await originEffect.update({changes: originEffectChanges}); - // Temporary spell data for the ensnaring effect - const spellData = getTempSpellData(sourceActor, originItem, originEffect); + const spellData = getTempSpellData(actor, macroItem, originEffect); // eslint-disable-next-line new-cap const spell = new Item.implementation(spellData, { - parent: sourceActor, + parent: actor, temporary: true, }); // If AA has a special custom effect for the restrained condition, use it instead of standard one if (game.modules.get("autoanimations")?.active) { - game.modules.get("plutonium-addon-automation")?.api.DdbImporter.effects.configureCustomAAForCondition("restrained", macroData, originItem.name, spell.uuid); + game.modules.get("plutonium-addon-automation").api.DdbImporter.effects.configureCustomAAForCondition("restrained", macroData, macroItem.name, spell.uuid); } // Check if target is large or larger and give it advantage on next save - const targetActor = macroData.hitTargets[0].actor; - if (dnd5e.config.tokenSizes[targetActor?.system.traits.size ?? "med"] >= dnd5e.config.tokenSizes["lg"]) { - await game.modules.get("plutonium-addon-automation")?.api.DdbImporter.effects.addSaveAdvantageToTarget(targetActor, originItem, "str", " (Large Creature)"); + const targetActor = workflow.hitTargets.first().actor; + if (getActorSizeValue(targetActor) >= getSizeValue("lg")) { + await game.modules.get("plutonium-addon-automation").api.DdbImporter.effects.addSaveAdvantageToTarget(targetActor, macroItem, "str", " (Large Creature)"); } - const options = { - createWorkflow: true, - targetUuids: [macroData.hitTargetUuids[0]], - configureDialog: false, - skipOnUse: true, - }; - const spellEffectWorkflow = await MidiQOL.completeItemUse(spell, {}, options); - const conEffect = MidiQOL.getConcentrationEffect(sourceActor); + + const conEffect = MidiQOL.getConcentrationEffect(actor, macroItem); + const [config, options] = game.modules.get("plutonium-addon-automation").api.DdbImporter.effects.syntheticItemWorkflowOptions({targets: [macroData.hitTargetUuids[0]]}); + options.skipOnUse = true; + foundry.utils.setProperty(options, "flags.dnd5e.use.concentrationId", conEffect?.id); + const spellEffectWorkflow = await MidiQOL.completeItemUse(spell, config, options); if (spellEffectWorkflow.hitTargets.size > 0 && spellEffectWorkflow.failedSaves.size > 0) { - // Transfer target of concentration from the caster to the target to allow removing effect on caster. - const conTargets = []; - spellEffectWorkflow.failedSaves.forEach((token) => - conTargets.push({ - tokenUuid: token.document?.uuid ?? token.uuid, - actorUuid: token.actor?.uuid ?? "", - }), - ); - await sourceActor.setFlag("midi-qol", "concentration-data.targets", conTargets); + // At least one target has an effect, we can remove the original effect from the caster await originEffect.delete(); } else { // Remove concentration and the effect causing it since the effect has been used - if (conEffect) { - conEffect.delete(); - } + conEffect?.delete(); } } + + /** + * Returns the numeric value of the specified actor's size. + * + * @param {Actor5e} actor actor for which to get the size value. + * @returns {number} the numeric value of the specified actor's size. + */ + function getActorSizeValue (actor) { + return getSizeValue(actor?.system?.traits?.size ?? "med"); + } + + /** + * Returns the numeric value of the specified size. + * + * @param {string} size the size name for which to get the size value. + * @returns {number} the numeric value of the specified size. + */ + function getSizeValue (size) { + return Object.keys(CONFIG.DND5E.actorSizes).indexOf(size ?? "med"); + } } diff --git a/macro-item/spell/PHB_sleep.js b/macro-item/spell/PHB_sleep.js index c8c8ddc..049a07a 100644 --- a/macro-item/spell/PHB_sleep.js +++ b/macro-item/spell/PHB_sleep.js @@ -7,12 +7,6 @@ async function macro (args) { // uses convinient effects // Midi-qol "On Use" - async function wait (ms) { - return new Promise((resolve) => { - setTimeout(resolve, ms); - }); - } - const sleepHp = await args[0].damageTotal; const condition = "Unconscious"; console.log(`Sleep Spell => Available HP Pool [${sleepHp}] points`); @@ -24,9 +18,8 @@ async function macro (args) { for (let target of targets) { const findTarget = await canvas.tokens.get(target.id); - const immuneType = findTarget.actor.type === "character" - ? ["undead", "construct"].some((race) => (findTarget.actor.system.details.race || "").toLowerCase().includes(race)) - : ["undead", "construct"].some((value) => (findTarget.actor.system.details.type.value || "").toLowerCase().includes(value)); + const targetRaceOrType = game.modules.get("plutonium-addon-automation").api.DdbImporter.effects.getRaceOrType(findTarget.actor); + const immuneType = ["undead", "construct", "elf", "half-elf"].some((race) => targetRaceOrType.includes(race)); const immuneCI = findTarget.actor.system.traits.ci.custom.includes("Sleep"); const sleeping = findTarget.actor.effects.find((i) => (i.name ?? i.label) === condition); const targetHpValue = findTarget.actor.system.attributes.hp.value; @@ -36,7 +29,7 @@ async function macro (args) { sleepTarget.push(`
Resists
${findTarget.name}
`); } else if (remainingSleepHp >= targetHpValue) { remainingSleepHp -= targetHpValue; - console.log(`Sleep Results => Target: ${findTarget.name} | HP: ${targetHpValue} | HP Pool: ${remainingSleepHp} | Status: Slept`); + console.log(`Sleep Results => Target: ${findTarget.name} | HP: ${targetHpValue} | HP Pool: ${remainingSleepHp} | Status: Slept`); sleepTarget.push(`
Slept
${findTarget.name}
`); const gameRound = game.combat ? game.combat.round : 0; const effectData = { @@ -48,8 +41,7 @@ async function macro (args) { duration: {rounds: 10, seconds: 60, startRound: gameRound, startTime: game.time.worldTime}, flags: {dae: {specialDuration: ["isDamaged"]}}, changes: [ - // { key: "macro.CE", mode: CONST.ACTIVE_EFFECT_MODES.CUSTOM, value: "Prone", priority: 20 }, - {key: "macro.CE", mode: CONST.ACTIVE_EFFECT_MODES.CUSTOM, value: "Unconscious", priority: 20}, + game.modules.get("plutonium-addon-automation").api.DdbImporter.effects.generateStatusEffectChange(game.i18n.localize("Unconscious"), 20), ], }; @@ -59,10 +51,10 @@ async function macro (args) { sleepTarget.push(`
misses
${findTarget.name}
`); } } - await wait(500); + await game.modules.get("plutonium-addon-automation").api.DdbImporter.effects.wait(500); const sleptResults = `
${sleepTarget.join("")}
`; const chatMessage = game.messages.get(args[0].itemCardId); - let content = duplicate(chatMessage.content); + let content = foundry.utils.duplicate(chatMessage.content); const searchString = /
[\s\S]*
/g; const replaceString = `
${sleptResults}`; content = await content.replace(searchString, replaceString); diff --git a/macro-item/spell/PHB_spirit-guardians.js b/macro-item/spell/PHB_spirit-guardians.js deleted file mode 100644 index df4a09a..0000000 --- a/macro-item/spell/PHB_spirit-guardians.js +++ /dev/null @@ -1,47 +0,0 @@ -/** - * Credit to D&D Beyond Importer (https://github.com/MrPrimate/ddb-importer) - * See: `../../license/ddb-importer.md` - */ -async function macro (args) { - const lastArg = args[args.length - 1]; - - // Check when applying the effect - if the token is not the caster and it IS the tokens turn they take damage - if (args[0] === "on" && args[1] !== lastArg.tokenId && lastArg.tokenId === game.combat?.current.tokenId) { - const sourceItem = await fromUuid(lastArg.origin); - const tokenOrActor = await fromUuid(lastArg.actorUuid); - const theActor = tokenOrActor.actor ? tokenOrActor.actor : tokenOrActor; - const DAEItem = lastArg.efData.flags.dae.itemData; - const damageType = getProperty(DAEItem, "flags.ddbimporter.damageType") || "radiant"; - - const itemData = mergeObject( - duplicate(sourceItem.data), - { - type: "weapon", - effects: [], - flags: { - "midi-qol": { - noProvokeReaction: true, // no reactions triggered - onUseMacroName: null, // - }, - }, - data: { - equipped: true, - actionType: "save", - save: {dc: Number.parseInt(args[3]), ability: "wis", scaling: "flat"}, - damage: {parts: [[`${args[2]}d8`, damageType]]}, - "target.type": "self", - components: {concentration: false, material: false, ritual: false, somatic: false, value: "", vocal: false}, - duration: {units: "inst", value: undefined}, - weaponType: "improv", - }, - }, - {overwrite: true, inlace: true, insertKeys: true, insertValues: true}, - ); - itemData.system.target.type = "self"; - setProperty(itemData.flags, "autoanimations.killAnim", true); - // eslint-disable-next-line new-cap - const item = new CONFIG.Item.documentClass(itemData, {parent: theActor}); - const options = {showFullCard: false, createWorkflow: true, versatile: false, configureDialog: false}; - await MidiQOL.completeItemUse(item, {}, options); - } -} diff --git a/macro-item/spell/PHB_witch-bolt.js b/macro-item/spell/PHB_witch-bolt.js index d4c8690..4b7c6af 100644 --- a/macro-item/spell/PHB_witch-bolt.js +++ b/macro-item/spell/PHB_witch-bolt.js @@ -14,7 +14,7 @@ async function macro (args) { })); const casterToken = await fromUuid(options.sourceUuid); const itemData = sourceItem.toObject(); - setProperty(itemData, "system.components.concentration", false); + itemData.system.properties = game.modules.get("plutonium-addon-automation").api.DdbImporter.effects.removeFromProperties(itemData.system.properties, "concentration") ?? []; itemData.effects = []; delete itemData._id; @@ -26,7 +26,7 @@ async function macro (args) { targets, damageRoll, { - flavor: `(${CONFIG.DND5E.damageTypes[damageType]})`, + flavor: `(${CONFIG.DND5E.damageTypes[damageType].label})`, itemCardId: "new", itemData, isCritical: false, @@ -35,7 +35,9 @@ async function macro (args) { } async function cancel (caster) { - const concentration = caster.effects.find((i) => i.name ?? i.label === "Concentrating"); + // Remove concentration and the effect causing it since the effect has been used + const concentration = MidiQOL.getConcentrationEffect(macroData.actor); + if (concentration) { await MidiQOL.socket().executeAsGM("removeEffects", {actorUuid: caster.uuid, effects: [concentration.id]}); } @@ -55,12 +57,7 @@ async function macro (args) { icon: args[0].item.img, duration: {rounds: 10, startTime: game.time.worldTime}, origin: args[0].item.uuid, - changes: [{ - key: "macro.itemMacro.local", - value: "", - mode: CONST.ACTIVE_EFFECT_MODES.CUSTOM, - priority: 20, - }], + changes: [game.modules.get("plutonium-addon-automation").api.DdbImporter.macros({macroType: "spell", macroName: "witchBolt.js", document: {name: "Witch Bolt"}})], disabled: false, "flags.dae.macroRepeat": "startEveryTurn", }]; @@ -72,7 +69,7 @@ async function macro (args) { userId: game.userId, }; - DAE.setFlag(args[0].actor, "witchBoltSpell", options); + await DAE.setFlag(args[0].actor, "witchBoltSpell", options); await args[0].actor.createEmbeddedDocuments("ActiveEffect", effectData); } else if (args[0] === "off") { const sourceItem = await fromUuid(lastArg.origin); @@ -82,29 +79,29 @@ async function macro (args) { const sourceItem = await fromUuid(lastArg.origin); const caster = sourceItem.parent; const options = DAE.getFlag(caster, "witchBoltSpell"); - const isInRange = await game.modules.get("plutonium-addon-automation")?.api.DdbImporter.effects.checkTargetInRange(options); + const isInRange = await game.modules.get("plutonium-addon-automation").api.DdbImporter.effects.checkTargetInRange(options); if (isInRange) { const userIds = Object.entries(caster.ownership).filter((k) => k[1] === CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER).map((k) => k[0]); - const mes = await ChatMessage.create({ - content: `

${caster.name} may use their action to sustain Witch Bolt.


`, + await ChatMessage.create({ + content: `

${caster.name} may use their action to sustain Witch Bolt. Asking them now...


`, type: CONST.CHAT_MESSAGE_TYPES.OTHER, speaker: caster.uuid, whisper: game.users.filter((u) => userIds.includes(u.id) || u.isGM), }); - new Dialog({ - title: sourceItem.name, + const result = await game.modules.get("plutonium-addon-automation").api.DdbImporter.dialog.AskUserButtonDialog(options.userId, { + buttons: [ + {label: "Yes, damage!", value: true}, + {label: "No, end concentration", value: false}, + ], + title: "Witch Bolt", content: "

Use action to sustain Witch Bolt?

", - buttons: { - continue: { - label: "Yes, damage!", - callback: () => sustainedDamage({options, damageType, damageDice, sourceItem, caster}), - }, - end: { - label: "No, end concentration", - callback: () => cancel(caster), - }, - }, - }).render(true); + }, + "column"); + if (result) { + sustainedDamage({options, damageType, damageDice, sourceItem, caster}); + } else { + cancel(caster); + } } } } diff --git a/macro-item/spell/XGE_crown-of-stars.js b/macro-item/spell/XGE_crown-of-stars.js index 1d339ba..ed81eb7 100644 --- a/macro-item/spell/XGE_crown-of-stars.js +++ b/macro-item/spell/XGE_crown-of-stars.js @@ -18,7 +18,7 @@ async function macro (args) { if (!castItem) { const DAEItem = lastArg.efData.flags.dae.itemData; const stars = 7 + ((args[1] - 7) * 2); - const uuid = randomID(); + const uuid = foundry.utils.randomID(); const weaponData = { _id: uuid, name: castItemName, @@ -31,11 +31,15 @@ async function macro (args) { uses: {value: stars, max: stars, per: "charges"}, ability: DAEItem.system.ability, actionType: "rsak", - attackBonus: DAEItem.system.attackBonus, + attack: { + bonus: DAEItem.system.attack.bonus, + }, chatFlavor: "", critical: null, damage: {parts: [["4d12", "radiant"]], versatile: ""}, - weaponType: "simpleR", + type: { + value: "natural", + }, proficient: true, equipped: true, description: DAEItem.system.description, diff --git a/macro-item/spell/XGE_ice-knife.js b/macro-item/spell/XGE_ice-knife.js index f79dc50..67dff9a 100644 --- a/macro-item/spell/XGE_ice-knife.js +++ b/macro-item/spell/XGE_ice-knife.js @@ -3,19 +3,19 @@ * See: `../../license/ddb-importer.md` */ async function macro (args) { - // Midi-qol "on use" const lastArg = args[args.length - 1]; const tokenOrActor = await fromUuid(lastArg.actorUuid); const casterActor = tokenOrActor.actor ? tokenOrActor.actor : tokenOrActor; if (lastArg.targets.length > 0) { - let areaSpellData = duplicate(lastArg.item); + let areaSpellData = foundry.utils.duplicate(lastArg.item); const damageDice = 1 + lastArg.spellLevel; - delete (areaSpellData.effects); - delete (areaSpellData.id); - delete (areaSpellData.flags["midi-qol"].onUseMacroName); - delete (areaSpellData.flags["midi-qol"].onUseMacroParts); - delete (areaSpellData.flags.itemacro); + delete areaSpellData.effects; + delete areaSpellData.id; + delete areaSpellData.flags["midi-qol"].onUseMacroName; + delete areaSpellData.flags["midi-qol"].onUseMacroParts; + if (foundry.utils.hasProperty(areaSpellData, "flags.itemacro")) delete areaSpellData.flags.itemacro; + if (foundry.utils.hasProperty(areaSpellData, "flags.dae.macro")) delete areaSpellData.flags.dae.macro; areaSpellData.name = "Ice Knife: Explosion"; areaSpellData.system.damage.parts = [[`${damageDice}d6[cold]`, "cold"]]; areaSpellData.system.actionType = "save"; @@ -23,34 +23,29 @@ async function macro (args) { areaSpellData.system.scaling = {mode: "level", formula: "1d6"}; areaSpellData.system.preparation.mode = "atwill"; areaSpellData.system.target.value = 99; + + foundry.utils.hasProperty(areaSpellData, "flags.midiProperties.magicdam", true); + foundry.utils.hasProperty(areaSpellData, "flags.midiProperties.saveDamage", "nodam"); + foundry.utils.hasProperty(areaSpellData, "flags.midiProperties.bonusSaveDamage", "nodam"); + // eslint-disable-next-line new-cap const areaSpell = new CONFIG.Item.documentClass(areaSpellData, {parent: casterActor}); + areaSpell.prepareData(); + areaSpell.prepareFinalAttributes(); const target = canvas.tokens.get(lastArg.targets[0].id); const aoeTargets = MidiQOL .findNearby(null, target, 5, {includeIncapacitated: true}) .filter((possible) => { const collisionRay = new Ray(target, possible); - const collision = canvas.walls.checkCollision(collisionRay, {mode: "any", type: "sight"}); + const collision = game.modules.get("plutonium-addon-automation").api.DdbImporter.effects.checkCollision(collisionRay, ["sight"]); if (collision) return false; else return true; }) .concat(target) .map((t) => t.document.uuid); - const options = { - showFullCard: false, - createWorkflow: true, - targetUuids: aoeTargets, - configureDialog: false, - versatile: false, - consumeResource: false, - consumeSlot: false, - // workflowOptions: { - // autoRollDamage: 'always' - // } - }; - - await MidiQOL.completeItemUse(areaSpell, {}, options); + const [config, options] = game.modules.get("plutonium-addon-automation").api.DdbImporter.effects.syntheticItemWorkflowOptions({targets: aoeTargets}); + await MidiQOL.completeItemUse(areaSpell, config, options); } else { ui.notifications.error("Ice Knife: No target selected: unable to automate burst effect."); } diff --git a/macro-item/spell/XGE_zephyr-strike.js b/macro-item/spell/XGE_zephyr-strike.js index 9e83fdd..10a00b7 100644 --- a/macro-item/spell/XGE_zephyr-strike.js +++ b/macro-item/spell/XGE_zephyr-strike.js @@ -4,8 +4,6 @@ */ async function macro (args) { const lastArg = args[args.length - 1]; - const tokenOrActor = await fromUuid(lastArg.actorUuid); - const targetActor = tokenOrActor.actor ? tokenOrActor.actor : tokenOrActor; const gameRound = game.combat ? game.combat.round : 0; const effectData = { @@ -26,7 +24,7 @@ async function macro (args) { flags: {dae: {specialDuration: ["turnEndSource"]}}, }; - ChatMessage.create({content: `${targetActor.name} gains 30ft of movement until the end of their turn`}); + ChatMessage.create({content: `${lastArg.actor.name} gains 30ft of movement until the end of their turn`}); - await MidiQOL.socket().executeAsGM("createEffects", {actorUuid: targetActor.uuid, effects: [effectData]}); + await MidiQOL.socket().executeAsGM("createEffects", {actorUuid: lastArg.actorUuid, effects: [effectData]}); } diff --git a/module/data/classFeature/__core.json b/module/data/classFeature/__core.json index bde7686..1df0b92 100644 --- a/module/data/classFeature/__core.json +++ b/module/data/classFeature/__core.json @@ -203,10 +203,7 @@ } ], "itemMacro": { - "file": "PHB_rage.js", - "requires": { - "itemacro": true - } + "file": "PHB_rage.js" }, "ignoreSrdEffects": true }, diff --git a/module/data/spell/__core.json b/module/data/spell/__core.json index ceda5bd..d4f7133 100644 --- a/module/data/spell/__core.json +++ b/module/data/spell/__core.json @@ -92,55 +92,6 @@ } ] }, - { - "name": "Branding Smite", - "source": "PHB", - "effects": [ - { - "changes": [ - { - "key": "flags.dnd5e.DamageBonusMacro", - "value": "ItemMacro.Branding Smite", - "mode": "CUSTOM", - "priority": "20" - }, - { - "key": "flags.midi-qol.brandingSmite.level", - "value": "@item.level", - "mode": "OVERRIDE", - "priority": "20" - } - ], - "flags": { - "dae": { - "stackable": "noneName", - "specialDuration": [ - "1Hit:rwak", - "1Hit:mwak" - ] - }, - "midi-qol": { - "forceCEOff": true - } - }, - "name": "Branding Smite" - } - ], - "flags": { - "midi-qol": { - "onUseMacroName": "[postActiveEffects]ItemMacro", - "forceCEOff": true - } - }, - "itemMacro": { - "file": "PHB_branding-smite.js" - }, - "system": { - "target.type": "self", - "actionType": "other", - "damage.parts": [] - } - }, { "name": "Color Spray", "source": "PHB", @@ -189,75 +140,12 @@ ], "flags": { "midi-qol": { - "forceCEOff": true - } - } - }, - { - "name": "Create Bonfire", - "source": "XGE", - "effects": [ - { - "changes": [ - { - "key": "flags.midi-qol.OverTime", - "mode": "CUSTOM", - "value": "turn=end,label=Create Bonfire (End of Turn),damageRoll=(@cantripDice)d8,damageType=fire,saveRemove=false,saveDC=@attributes.spelldc,saveAbility=dex,saveDamage=nodamage,killAnim=true", - "priority": "20" - }, - { - "key": "macro.itemMacro", - "value": "", - "mode": "CUSTOM", - "priority": 20 - } - ], - "duration": { - "seconds": 60, - "rounds": 10 - }, - "flags": { - "dae": { - "stackable": "noneName" - }, - "midi-qol": { - "forceCEOff": true - }, - "ActiveAuras": { - "isAura": true, - "aura": "All", - "save": "dex", - "displayTemp": true - } - }, - "name": "Create Bonfire", - "requires": { - "ActiveAuras": true - } - } - ], - "flags": { - "midi-qol": { - "onUseMacroName": "[preActiveEffects]ItemMacro", + "removeAttackDamageButtons": "default", "forceCEOff": true }, - "plutonium-addon-automation": { - "effect": { - "dice": "1d8[fire]", - "damageType": "fire", - "save": "", - "sequencerFile": "jb2a.flames.01.orange", - "sequencerScale": 2, - "isCantrip": true, - "saveOnEntry": true - } + "midiProperties": { + "confirmTargets": "default" } - }, - "itemMacro": { - "file": "Generic_ddbi-aa-damage-on-entry.js" - }, - "system": { - "save.ability": null } }, { @@ -343,43 +231,6 @@ "damage.parts": [] } }, - { - "name": "Elemental Weapon", - "source": "PHB", - "effects": [ - { - "changes": [ - { - "key": "macro.itemMacro", - "value": "@item.level", - "mode": "CUSTOM", - "priority": 0 - } - ], - "flags": { - "dae": { - "stackable": "noneName" - }, - "midi-qol": { - "forceCEOff": true - } - }, - "name": "Elemental Weapon" - } - ], - "flags": { - "midi-qol": { - "forceCEOff": true - } - }, - "itemMacro": { - "file": "PHB_elemental-weapon.js" - }, - "system": { - "scaling.mode": "none", - "scaling.formula": "" - } - }, { "name": "Ensnaring Strike", "source": "PHB", @@ -499,8 +350,6 @@ "dae": { "stackable": "noneName", "specialDuration": [ - "isSkill", - "isCheck", "isInitiative" ] }, @@ -513,7 +362,11 @@ ], "flags": { "midi-qol": { + "removeAttackDamageButtons": "default", "forceCEOff": true + }, + "midiProperties": { + "confirmTargets": "default" } } }, @@ -548,37 +401,6 @@ } ] }, - { - "name": "Heroism", - "source": "PHB", - "_TODO": [ - "Does not clear temphp on spell expiration" - ], - "system": { - "damage.parts": [] - }, - "effects": [ - { - "changes": [ - { - "key": "system.traits.ci.value", - "mode": "ADD", - "value": "frightened", - "priority": 20 - }, - { - "key": "flags.midi-qol.OverTime", - "mode": "OVERRIDE", - "value": "turn=start,damageRoll=@mod,damageType=temphp,label=Heroism", - "priority": 20 - } - ], - "duration": { - "seconds": 60 - } - } - ] - }, { "name": "Ice Knife", "source": "XGE", @@ -739,61 +561,15 @@ ], "flags": { "midiProperties": { - "autoFailFriendly": true + "autoFailFriendly": true, + "confirmTargets": "default" }, "midi-qol": { + "removeAttackDamageButtons": "default", "forceCEOff": true } } }, - { - "name": "Melf's Acid Arrow", - "source": "PHB", - "effects": [ - { - "changes": [ - { - "key": "flags.midi-qol.OverTime", - "mode": "CUSTOM", - "value": "label=Melf's Acid Arrow (End of Turn),turn=end,damageRoll=(@spellLevel)d4[acid],damageType=acid,killAnim=true", - "priority": "20" - } - ], - "duration": { - "rounds": 1 - }, - "flags": { - "dae": { - "stackable": "noneName", - "specialDuration": [ - "turnEnd" - ] - }, - "midi-qol": { - "forceCEOff": true - } - }, - "name": "Melf's Acid Arrow" - } - ], - "flags": { - "midi-qol": { - "forceCEOff": true - } - }, - "system": { - "target.value": 1, - "target.units": null, - "target.type": "creature", - "damage.parts": [ - [ - "4d4", - "acid" - ] - ], - "formula": "2d4[acid]" - } - }, { "name": "Mind Blank", "source": "PHB", @@ -818,12 +594,13 @@ "name": "Mind Blank" } ], - "system": { - "target.type": "" - }, "flags": { "midi-qol": { + "removeAttackDamageButtons": "default", "forceCEOff": true + }, + "midiProperties": { + "confirmTargets": "default" } } }, @@ -859,7 +636,11 @@ ], "flags": { "midi-qol": { + "removeAttackDamageButtons": "default", "forceCEOff": true + }, + "midiProperties": { + "confirmTargets": "default" } } }, @@ -922,7 +703,11 @@ "name": "Polymorph", "source": "PHB", "flags": { + "midi-qol": { + "removeAttackDamageButtons": "default" + }, "midiProperties": { + "confirmTargets": "default", "autoFailFriendly": true } } @@ -934,9 +719,9 @@ { "changes": [ { - "key": "macro.CE", - "mode": "CUSTOM", - "value": "Stunned", + "key": "macro.StatusEffect", + "mode": "ADD", + "value": "stunned", "priority": 20 }, { @@ -957,19 +742,17 @@ "forceCEOff": true } }, - "name": "Psychic Scream" + "name": "Psychic Scream - Stunned" } ], - "system": { - "target.value": 10, - "target.units": null, - "target.type": "creature" - }, "flags": { "midiProperties": { - "halfdam": true + "halfdam": true, + "saveDamage": "halfdam", + "confirmTargets": "default" }, "midi-qol": { + "removeAttackDamageButtons": "default", "forceCEOff": true } } @@ -1009,7 +792,11 @@ ], "flags": { "midi-qol": { + "removeAttackDamageButtons": "default", "forceCEOff": true + }, + "midiProperties": { + "confirmTargets": "default" } } }, @@ -1122,76 +909,6 @@ ] } }, - { - "name": "Spirit Guardians", - "source": "PHB", - "effects": [ - { - "changes": [ - { - "key": "system.attributes.movement.all", - "mode": "CUSTOM", - "value": "/2", - "priority": "20" - }, - { - "key": "flags.midi-qol.OverTime", - "mode": "ADD", - "value": "turn=start,label=Spirit Guardians (Start of Turn),damageRoll=(@spellLevel)d8,damageType=radiant,saveRemove=false,saveDC=@attributes.spelldc,saveAbility=wis,saveDamage=halfdamage,killAnim=true", - "priority": "20" - }, - { - "key": "macro.itemMacro", - "value": "@token @spellLevel @attributes.spelldc", - "mode": "CUSTOM", - "priority": 20 - } - ], - "flags": { - "dae": { - "stackable": "noneName", - "selfTarget": true, - "selfTargetAlways": true - }, - "midi-qol": { - "forceCEOff": true - }, - "ActiveAuras": { - "isAura": true, - "aura": "Enemy", - "radius": 15, - "ignoreSelf": true, - "displayTemp": true - } - }, - "name": "Spirit Guardians", - "requires": { - "ActiveAuras": true - } - } - ], - "flags": { - "midi-qol": { - "forceCEOff": true - } - }, - "itemMacro": { - "file": "PHB_spirit-guardians.js" - }, - "_merge": { - "system": true - }, - "system": { - "target.value": null, - "target.units": null, - "target.type": "self", - "range.value": 15, - "range.units": "ft", - "actionType": "other", - "damage.parts": [], - "save.ability": null - } - }, { "name": "Tongues", "source": "PHB", diff --git a/module/js/SettingsManager.js b/module/js/SettingsManager.js index 0d6b9a1..c5546e6 100644 --- a/module/js/SettingsManager.js +++ b/module/js/SettingsManager.js @@ -26,7 +26,6 @@ export class SettingsManager extends StartupHookMixin(class {}) { static _MODULE_ID__DFREDS_CONVENIENT_EFFECTS = "dfreds-convenient-effects"; static _MODULE_ID__MIDI_QOL = "midi-qol"; - static _MODULE_ID__ITEM_MACRO = "itemacro"; static _MODULE_ID__TOKEN_ACTION_HUD = "token-action-hud"; static _MODULE_ID__CHRIS_PREMADES = "chris-premades"; @@ -46,18 +45,6 @@ export class SettingsManager extends StartupHookMixin(class {}) { expectedValue: "itempri", displaySettingName: "midi-qol.AutoCEEffects.Name", }, - { - moduleId: this._MODULE_ID__ITEM_MACRO, - settingKey: "defaultmacro", - isGmOnly: true, - expectedValue: false, - }, - { - moduleId: this._MODULE_ID__ITEM_MACRO, - settingKey: "charsheet", - isGmOnly: true, - expectedValue: false, - }, { moduleId: this._MODULE_ID__TOKEN_ACTION_HUD, settingKey: "itemMacroReplace", diff --git a/module/js/api/DdbImporter.js b/module/js/api/DdbImporter.js index e7051ca..46c3e22 100644 --- a/module/js/api/DdbImporter.js +++ b/module/js/api/DdbImporter.js @@ -1,293 +1,306 @@ -// Helpers copied from D&D Beyond Importer's `helpers.js` +// API functions copied from D&D Beyond Importer // Credit to D&D Beyond Importer (https://github.com/MrPrimate/ddb-importer) // See: `../../../license/ddb-importer.md` -/** - * If the requirements are met, returns true, false otherwise. - * - * @returns true if the requirements are met, false otherwise. - */ -export function requirementsSatisfied (name, dependencies) { - let missingDep = false; - dependencies.forEach((dep) => { - if (!game.modules.get(dep)?.active) { - const errorMsg = `${name}: ${dep} must be installed and active.`; - ui.notifications.error(errorMsg); - logger.warn(errorMsg); - missingDep = true; - } - }); - return !missingDep; -} +const utils = { + isArray: (arr) => { + return Array.isArray(arr); + }, -/** - * If a custom AA condition animation exists for the specified name, registers the appropriate hook with AA - * to be able to replace the default condition animation by the custom one. - * - * @param {*} condition condition for which to replace its AA animation by a custom one (it must be a value from CONFIG.DND5E.conditionTypes). - * @param {*} macroData the midi-qol macro data. - * @param {*} originItemName the name of item used for AA customization of the condition. - * @param {*} conditionItemUuid the UUID of the item applying the condition. - */ -export function configureCustomAAForCondition (condition, macroData, originItemName, conditionItemUuid) { - // Get default condition label - const statusName = CONFIG.DND5E.conditionTypes[condition]; - const customStatusName = `${statusName} [${originItemName}]`; - if (AutomatedAnimations.AutorecManager.getAutorecEntries().aefx.find((a) => (a.name ?? a.label) === customStatusName)) { - const aaHookId = Hooks.on("AutomatedAnimations-WorkflowStart", (data) => { - if ( - data.item instanceof CONFIG.ActiveEffect.documentClass - && (data.item.name ?? data.item.label) === statusName - && data.item.origin === macroData.sourceItemUuid - ) { - data.recheckAnimation = true; - if (isNewerVersion(game.version, 11)) { - data.item.name = customStatusName; - } else { - data.item.label = customStatusName; - } - Hooks.off("AutomatedAnimations-WorkflowStart", aaHookId); - } - }); - // Make sure that the hook is removed when the special spell effect is completed - Hooks.once(`midi-qol.RollComplete.${conditionItemUuid}`, () => { - Hooks.off("AutomatedAnimations-WorkflowStart", aaHookId); - }); - } -} + removeFromProperties: (properties, value) => { + const setProperties = properties + ? utils.isArray(properties) + ? new Set(properties) + : properties + : new Set(); -export function checkJB2a (free = true, patreon = true, notify = false) { - if (patreon && game.modules.get("jb2a_patreon")?.active) { - return true; - } else if (!free) { - if (notify) ui.notifications.error("This macro requires the patreon version of JB2A"); - return false; - } - if (free && game.modules.get("JB2A_DnD5e")?.active) return true; - if (notify) ui.notifications.error("This macro requires either the patreon or free version of JB2A"); - return false; -} + setProperties.delete(value); + return Array.from(setProperties); + }, +}; -/** - * Adds a save advantage effect for the next save on the specified target actor. - * - * @param {*} targetActor the target actor on which to add the effect. - * @param {*} originItem the item that is the origin of the effect. - * @param {*} ability the short ability name to use for save, e.g. str - */ -export async function addSaveAdvantageToTarget (targetActor, originItem, ability, additionLabel = "", icon = null) { - const effectData = { - _id: randomID(), - changes: [ - { - key: `flags.midi-qol.advantage.ability.save.${ability}`, - mode: CONST.ACTIVE_EFFECT_MODES.CUSTOM, - value: "1", - priority: 20, - }, - ], - origin: originItem.uuid, - disabled: false, - transfer: false, - icon, - duration: {turns: 1}, - flags: { - dae: { - specialDuration: [`isSave.${ability}`], +class DDBEffectHelper { + static addToProperties = utils.addToProperties; + + static removeFromProperties = utils.removeFromProperties; + + /** + * Adds a save advantage effect for the next save on the specified target actor. + * + * @param {*} targetActor the target actor on which to add the effect. + * @param {*} originItem the item that is the origin of the effect. + * @param {*} ability the short ability name to use for save, e.g. str + */ + static async addSaveAdvantageToTarget (targetActor, originItem, ability, additionLabel = "", icon = null) { + const effectData = { + _id: foundry.utils.randomID(), + changes: [ + { + key: `flags.midi-qol.advantage.ability.save.${ability}`, + mode: CONST.ACTIVE_EFFECT_MODES.CUSTOM, + value: "1", + priority: 20, + }, + ], + origin: originItem.uuid, + disabled: false, + transfer: false, + icon, + img: icon, + duration: {turns: 1}, + flags: { + dae: { + specialDuration: [`isSave.${ability}`], + }, }, - }, - }; - if (isNewerVersion(game.version, 11)) { + }; effectData.name = `${originItem.name}${additionLabel}: Save Advantage`; - } else { - effectData.label = `${originItem.name}${additionLabel}: Save Advantage`; + await MidiQOL.socket().executeAsGM("createEffects", {actorUuid: targetActor.uuid, effects: [effectData]}); } - await MidiQOL.socket().executeAsGM("createEffects", {actorUuid: targetActor.uuid, effects: [effectData]}); -} -/** - * Returns ids of tokens in template - * - * @param {*} templateDoc the templatedoc to check - */ -export function findContainedTokensInTemplate (templateDoc) { - const contained = new Set(); - for (const tokenDoc of templateDoc.parent.tokens) { - const startX = tokenDoc.width >= 1 ? 0.5 : tokenDoc.width / 2; - const startY = tokenDoc.height >= 1 ? 0.5 : tokenDoc.height / 2; - for (let x = startX; x < tokenDoc.width; x++) { - for (let y = startY; y < tokenDoc.width; y++) { - const curr = { - x: tokenDoc.x + (x * templateDoc.parent.grid.size) - templateDoc.x, - y: tokenDoc.y + (y * templateDoc.parent.grid.size) - templateDoc.y, - }; - const contains = templateDoc.object.shape.contains(curr.x, curr.y); - if (contains) contained.add(tokenDoc.id); + static async attachSequencerFileToTemplate (templateUuid, sequencerFile, originUuid, scale = 1) { + if (game.modules.get("sequencer")?.active) { + if (Sequencer.Database.entryExists(sequencerFile)) { + logger.debug(`Trying to apply sequencer effect (${sequencerFile}) to ${templateUuid} from ${originUuid}`, sequencerFile); + const template = await fromUuid(templateUuid); + new Sequence() + .effect() + .file(Sequencer.Database.entryExists(sequencerFile)) + .size({ + width: canvas.grid.size * (template.width / canvas.dimensions.distance), + height: canvas.grid.size * (template.width / canvas.dimensions.distance), + }) + .persist(true) + .origin(originUuid) + .belowTokens() + .opacity(0.5) + .attachTo(template, {followRotation: true}) + .scaleToObject(scale) + .play(); } } } - return [...contained]; -} -export async function checkTargetInRange ({sourceUuid, targetUuid, distance}) { - if (!game.modules.get("midi-qol")?.active) { - ui.notifications.error("checkTargetInRange requires midiQoL, not checking"); - logger.error("checkTargetInRange requires midiQoL, not checking"); - return true; + static checkCollision (ray, types = ["sight", "move"], mode = "any") { + for (const type of types) { + const result = CONFIG.Canvas.polygonBackends[type].testCollision(ray.A, ray.B, {mode, type}); + if (result) return result; + } + return false; } - const sourceToken = await fromUuid(sourceUuid); - if (!sourceToken) return false; - const targetsInRange = MidiQOL.findNearby(null, sourceUuid, distance); - const isInRange = targetsInRange.reduce((result, possible) => { - const collisionRay = new Ray(sourceToken, possible); - const collision = canvas.walls.checkCollision(collisionRay, {mode: "any", type: "sight"}); - if (possible.uuid === targetUuid && !collision) result = true; - return result; - }, false); - return isInRange; -} -/** - * Returns true if the attack is a ranged weapon attack that hit. It also supports melee weapons - * with the thrown property. - * @param {*} macroData the midi-qol macro data. - * @returns true if the attack is a ranged weapon attack that hit - */ -export function isRangedWeaponAttack (macroData) { - if (macroData.hitTargets.length < 1) { - return false; + /** + * If a custom AA condition animation exists for the specified name, registers the appropriate hook with AA + * to be able to replace the default condition animation by the custom one. + * + * @param {*} condition condition for which to replace its AA animation by a custom one (it must be a value from CONFIG.DND5E.conditionTypes). + * @param {*} macroData the midi-qol macro data. + * @param {*} originItemName the name of item used for AA customization of the condition. + * @param {*} conditionItemUuid the UUID of the item applying the condition. + */ + static configureCustomAAForCondition (condition, macroData, originItemName, conditionItemUuid) { + // Get default condition label + const statusName = CONFIG.DND5E.conditionTypes[condition]; + if (!statusName) { + return; + } + const customStatusName = `${statusName.label} [${originItemName}]`; + if (AutomatedAnimations.AutorecManager.getAutorecEntries().aefx.find((a) => (a.label ?? a.name) === customStatusName)) { + const aaHookId = Hooks.on("AutomatedAnimations-WorkflowStart", (data) => { + if ( + data.item instanceof CONFIG.ActiveEffect.documentClass + && data.item.name === statusName.label + && data.item.origin === macroData.sourceItemUuid + ) { + data.recheckAnimation = true; + data.item.name = customStatusName; + Hooks.off("AutomatedAnimations-WorkflowStart", aaHookId); + } + }); + // Make sure that the hook is removed when the special spell effect is completed + Hooks.once(`midi-qol.RollComplete.${conditionItemUuid}`, () => { + Hooks.off("AutomatedAnimations-WorkflowStart", aaHookId); + }); + } } - if (macroData.item?.system.actionType === "rwak") { - return true; + + static async checkTargetInRange ({sourceUuid, targetUuid, distance}) { + if (!game.modules.get("midi-qol")?.active) { + ui.notifications.error("checkTargetInRange requires midiQoL, not checking"); + logger.error("checkTargetInRange requires midiQoL, not checking"); + return true; + } + const sourceToken = await fromUuid(sourceUuid); + if (!sourceToken) return false; + const targetsInRange = MidiQOL.findNearby(null, sourceUuid, distance); + const isInRange = targetsInRange.reduce((result, possible) => { + const collisionRay = new Ray(sourceToken, possible); + const collision = DDBEffectHelper.checkCollision(collisionRay, ["sight"]); + if (possible.uuid === targetUuid && !collision) result = true; + return result; + }, false); + return isInRange; } - if (macroData.item?.system.actionType !== "mwak" || !macroData.item?.system.properties?.thr) { - return false; + + /** + * Return actor from a UUID + * + * @param {string} uuid - The UUID of the actor. + * @return {object|null} - Returns the actor document or null if not found. + */ + static fromActorUuid (uuid) { + const doc = fromUuidSync(uuid); + if (doc instanceof CONFIG.Token.documentClass) return doc.actor; + if (doc instanceof CONFIG.Actor.documentClass) return doc; + return null; } - const sourceToken = canvas.tokens?.get(macroData.tokenId); - const targetToken = macroData.hitTargets[0].object; - const distance = MidiQOL.getDistance(sourceToken, targetToken, true, true); - const meleeDistance = 5; // Would it be possible to have creatures with reach and thrown weapon? - return distance >= 0 && distance > meleeDistance; -} + /** + * Returns the actor object associated with the given actor reference. + * + * @param {any} actorRef - The actor reference to retrieve the actor from. + * @return {Actor|null} The actor object associated with the given actor reference, or null if no actor is found. + */ + static getActor (actorRef) { + if (actorRef instanceof Actor) return actorRef; + if (actorRef instanceof Token) return actorRef.actor; + if (actorRef instanceof TokenDocument) return actorRef.actor; + if (utils.isString(actorRef)) return DDBEffectHelper.fromActorUuid(actorRef); + return null; + } -/** - * Selects all the tokens that are within X distance of the source token for the current game user. - * @param {Token} sourceToken the reference token from which to compute the distance. - * @param {number} distance the distance from the reference token. - * @param {boolean} includeSource flag to indicate if the reference token should be included or not in the selected targets. - * @returns an array of Token instances that were selected. - */ -export function selectTargetsWithinX (sourceToken, distance, includeSource) { - let aoeTargets = MidiQOL.findNearby(null, sourceToken, distance); - if (includeSource) { - aoeTargets.unshift(sourceToken); + /** + * Retrieves the number of cantrip dice based on the level of the actor. + * + * @param {Actor} actor - The actor object + * @return {number} The number of cantrip dice. + */ + static getCantripDice (actor) { + const level = actor.type === "character" + ? actor.system.details.level + : actor.system.details.cr; + return 1 + Math.floor((level + 1) / 6); } - const aoeTargetIds = aoeTargets.map((t) => t.document.id); - game.user?.updateTokenTargets(aoeTargetIds); - game.user?.broadcastActivity({aoeTargetIds}); - return aoeTargets; -} -export async function attachSequencerFileToTemplate (templateUuid, sequencerFile, originUuid, scale = 1) { - if (game.modules.get("sequencer")?.active) { - if (Sequencer.Database.entryExists(sequencerFile)) { - logger.debug(`Trying to apply sequencer effect (${sequencerFile}) to ${templateUuid} from ${originUuid}`, sequencerFile); - const template = await fromUuid(templateUuid); - new Sequence() - .effect() - .file(Sequencer.Database.entryExists(sequencerFile)) - .size({ - width: canvas.grid.size * (template.width / canvas.dimensions.distance), - height: canvas.grid.size * (template.width / canvas.dimensions.distance), - }) - .persist(true) - .origin(originUuid) - .belowTokens() - .opacity(0.5) - .attachTo(template, {followRotation: true}) - .scaleToObject(scale) - .play(); + // eslint-disable-next-line no-unused-vars + static getConcentrationEffect (actor, _name = null) { + return actor?.effects.find((ef) => foundry.utils.getProperty(ef, "flags.midi-qol.isConcentration")); + } + + /** + * Returns the race or type of the given entity. + * + * @param {object} entity - The entity for which to retrieve the race or type. + * @return {string} The race or type of the entity, in lowercase. + */ + static getRaceOrType (entity) { + const actor = DDBEffectHelper.getActor(entity); + const systemData = actor?.system; + if (!systemData) return ""; + if (systemData.details.race) { + return (systemData.details?.race?.name ?? systemData.details?.race)?.toLocaleLowerCase() ?? ""; } + return systemData.details.type?.value.toLocaleLowerCase() ?? ""; } -} -/** - * Returns a new duration which reflects the remaining duration of the specified one. - * - * @param {*} duration the source duration - * @returns a new duration which reflects the remaining duration of the specified one. - */ -export function getRemainingDuration (duration) { - const newDuration = {}; - if (duration.type === "seconds") { - newDuration.seconds = duration.remaining; - } else if (duration.type === "turns") { - const remainingRounds = Math.floor(duration.remaining); - const remainingTurns = (duration.remaining - remainingRounds) * 100; - newDuration.rounds = remainingRounds; - newDuration.turns = remainingTurns; + /** + * Returns a new duration which reflects the remaining duration of the specified one. + * + * @param {*} duration the source duration + * @returns a new duration which reflects the remaining duration of the specified one. + */ + static getRemainingDuration (duration) { + const newDuration = {}; + if (duration.type === "seconds") { + newDuration.seconds = duration.remaining; + } else if (duration.type === "turns") { + const remainingRounds = Math.floor(duration.remaining); + const remainingTurns = (duration.remaining - remainingRounds) * 100; + newDuration.rounds = remainingRounds; + newDuration.turns = remainingTurns; + } + return newDuration; } - return newDuration; -} -export async function wait (ms) { - return new Promise((resolve) => { - setTimeout(resolve, ms); - }); -} + static async wait (ms) { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); + } -export function getHighestAbility (actor, abilities) { - if (typeof abilities === "string") { - return abilities; - } else if (Array.isArray(abilities)) { - return abilities.reduce((prv, current) => { - if (actor.system.abilities[current].value > actor.system.abilities[prv].value) return current; - else return prv; - }, abilities[0]); + static syntheticItemWorkflowOptions ({ + targets = undefined, showFullCard = false, useSpellSlot = false, castLevel = false, consume = false, + configureDialog = false, targetConfirmation = undefined, + } = {}) { + return [ + { + showFullCard, + createWorkflow: true, + consumeResource: consume, + consumeRecharge: consume, + consumeQuantity: consume, + consumeUsage: consume, + consumeSpellSlot: useSpellSlot, + consumeSpellLevel: castLevel, + slotLevel: castLevel, + }, + { + targetUuids: targets, + configureDialog, + workflowOptions: { + autoRollDamage: "always", + autoFastDamage: true, + autoRollAttack: true, + targetConfirmation, + }, + }, + ]; } - return undefined; } -export function getCantripDice (actor) { - const level = actor.type === "character" ? actor.system.details.level : actor.system.details.cr; - return 1 + Math.floor((level + 1) / 6); -} +class DDBMacros { + static generateMacroChange ({macroValues = "", macroType = null, macroName = null, keyPostfix = "", priority = 20} = {}) { + const macroKey = "macro.itemMacro"; + const macroValuePrefix = ""; -export async function createJB2aActors (subFolderName, name) { - const packKeys = ["jb2a_patreon.jb2a-actors", "JB2A_DnD5e.jb2a-actors"]; - for (let key of packKeys) { - let pack = game.packs.get(key); - // eslint-disable-next-line no-continue - if (!pack) continue; - const actors = pack.index.filter((f) => f.name.includes(name)); - const subFolder = await utils.getFolder("npc", subFolderName, "JB2A Actors", "#ceb180", "#cccc00", false); + return { + key: `${macroKey}${keyPostfix}`, + value: `${macroValuePrefix}${macroValues}`, + mode: CONST.ACTIVE_EFFECT_MODES.CUSTOM, + priority: priority, + }; + } +} - for (const actor of actors) { - if (!game.actors.find((a) => a.name === actor.name && a.folder?.id === subFolder.id)) { - await game.actors.importFromCompendium(pack, actor._id, { - folder: subFolder.id, - }); - } - } +class DialogHelper { + static async buttonDialog ({title = "", content = "", buttons, options = {height: "auto"}} = {}, direction = "row") { + return new Promise((resolve) => { + new Dialog( + { + title, + content, + buttons: buttons.reduce((o, button) => ({ + ...o, + [button.label]: {label: button.label, callback: () => resolve(button.value)}, + }), {}), + close: () => resolve(this), + }, + { + classes: ["dialog"], + ...options, + }, + ).render(true); + }); + } + static async AskUserButtonDialog (user, ...buttonArgs) { + return DialogHelper.buttonDialog(...buttonArgs); } } export default { - effects: { - requirementsSatisfied, - configureCustomAAForCondition, - checkJB2a, - addSaveAdvantageToTarget, - findContainedTokensInTemplate, - checkTargetInRange, - isRangedWeaponAttack, - selectTargetsWithinX, - attachSequencerFileToTemplate, - getRemainingDuration, - wait, - getHighestAbility, - getCantripDice, - createJB2aActors, - }, + effects: DDBEffectHelper, + + macros: DDBMacros, + + dialog: DialogHelper, }; diff --git a/module/js/data-source/DataSourceSelf.js b/module/js/data-source/DataSourceSelf.js index ed1dae7..e810cbc 100644 --- a/module/js/data-source/DataSourceSelf.js +++ b/module/js/data-source/DataSourceSelf.js @@ -129,21 +129,18 @@ export class DataSourceSelf extends StartupHookMixin(DataSourceBase) { out = foundry.utils.deepClone(out); - out.flags = out.flags || {}; - out.flags.itemacro = { - "macro": { - "_id": null, - "name": "-", - "type": "script", - "author": game.userId, - "img": "icons/svg/dice-target.svg", - "scope": "global", - "command": out.itemMacro.script, - "folder": null, - "sort": 0, - "ownership": {"default": 0}, - "flags": {}, - }, + ((out.flags ||= {}).dae ||= {}).macro = { + "_id": null, + "name": "-", + "type": "script", + "author": game.userId, + "img": "icons/svg/dice-target.svg", + "scope": "global", + "command": out.itemMacro.script, + "folder": null, + "sort": 0, + "ownership": {"default": 0}, + "flags": {}, }; delete out.itemMacro; diff --git a/script/build-task.js b/script/build-task.js index 84a7499..9c2d9b3 100644 --- a/script/build-task.js +++ b/script/build-task.js @@ -47,7 +47,7 @@ export const buildTask = async ( changelog: "https://raw.githubusercontent.com/TheGiddyLimit/plutonium-addon-automation/master/CHANGELOG.md", compatibility: { minimum: "10", - verified: "11.315", + verified: "12.328", }, url: "https://www.patreon.com/Giddy5e", bugs: "https://discord.gg/nGvRCDs", @@ -114,11 +114,6 @@ export const buildTask = async ( type: "module", reason: "Enables additional automations", }, - { - id: "itemacro", - type: "module", - reason: "Enables additional automations; allows editing of some automations", - }, // endregion // region Optional integrations @@ -226,6 +221,7 @@ export const buildTask = async ( }; if (ent.flags?.itemacro) throw new Error(`Entity had both "itemMacro" and "itemacro" flags!`); + if (ent.flags?.dae?.macro) throw new Error(`Entity had both "itemMacro" and "dae.macro" flags!`); isMod = true; }); diff --git a/tool/foundry-data-converter/client.js b/tool/foundry-data-converter/client.js index fe31b5c..8404561 100644 --- a/tool/foundry-data-converter/client.js +++ b/tool/foundry-data-converter/client.js @@ -209,6 +209,11 @@ class Converter { } class FlagConverter { + static _handleUnknownFlags ({outFlags, k, flags}) { + console.warn(`Unknown flag property "${k}"--copying as-is`); + outFlags[k] = flags; + } + static getFlags ({json, name, source, scriptHeader = null}) { if (!Object.keys(json.flags || {}).length) return {}; @@ -253,7 +258,8 @@ class FlagConverter { // endregion // region Special handling for item macros - case "itemacro": { + case "itemacro": + case "dae": { const filename = getMacroFilename({name, source}); const lines = flags.macro.command @@ -288,14 +294,20 @@ class FlagConverter { script, }; + if (k === "dae") { + const cpyFlags = {...flags}; + delete cpyFlags.macro; + if (!Object.keys(cpyFlags).length) break; + + // We do not expect "dae" flags to have any other properties + this._handleUnknownFlags({outFlags, k, flags: cpyFlags}); + } + break; } // endregion - default: { - console.warn(`Unknown flag property "${k}"--copying as-is`); - outFlags[k] = flags; - } + default: this._handleUnknownFlags({outFlags, k, flags}); } }); @@ -332,7 +344,7 @@ class EffectConverter { static _mutPreClean (eff) { // N.b. "selectedKey" is midi-qol UI QoL tracking data, and can be safely skipped - ["_id", "icon", "label", "origin", "tint", "selectedKey"].forEach(prop => delete eff[prop]); + ["_id", "icon", "img", "label", "origin", "tint", "selectedKey"].forEach(prop => delete eff[prop]); ["statuses"].filter(prop => !eff[prop].length).forEach(prop => delete eff[prop]); // Delete these only if falsy--we only store `"true"` disabled/transfer values