diff --git a/README.md b/README.md index 07fbefd..cdff1a6 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,12 @@ You can right-click on actors to turn them into minions, and then right-click on ![Turning a token into a minion](docs/right-click-actor.png) ![Turning a minion's attack into a group attack](docs/right-click-item.png) +You can also include the identifier `@numberOfMinions` anywhere in the attacks to reference the number of minion attacking. + +**Note:** This is automatically included in any damage part that does not already have it. + +![Number of minions modifier in the damage](docs/number-of-minions.png) + ## Group Initiative You can set this by right-clicking on the token HUD's "add to initiative" button to open the group initiative interface - clicking on a number within that UI moves all the selected tokens into that initiative group. diff --git a/docs/number-of-minions.png b/docs/number-of-minions.png new file mode 100644 index 0000000..554a52f Binary files /dev/null and b/docs/number-of-minions.png differ diff --git a/scripts/constants.js b/scripts/constants.js index 9fea435..3d4151d 100644 --- a/scripts/constants.js +++ b/scripts/constants.js @@ -14,6 +14,7 @@ const CONSTANTS = { } }], ATTACK_TYPES: ["mwak", "rwak", "msak", "rsak"], + NUMBER_MINIONS_BONUS: "@numberOfMinions", FLAGS: { COMBATANTS: `${FLAG}.combatants`, GROUP_NUMBER: `${FLAG}.groupNumber`, diff --git a/scripts/lib.js b/scripts/lib.js index 635165f..a5d4325 100644 --- a/scripts/lib.js +++ b/scripts/lib.js @@ -38,8 +38,14 @@ export function isValidOverkillItem(item) { } -export function getActiveGM() { - return game.users - .filter(u => u.active && u.isGM) - .sort((a, b) => a.isGM && !b.isGM ? -1 : 1)?.[0]; + +export function patchItemDamageRollConfig(item){ + return (item.system?.damage?.parts ?? []).map((part) => { + const firstDamage = part[0].toString(); + const containsNumberOfMinions = firstDamage.includes(CONSTANTS.NUMBER_MINIONS_BONUS); + const newFormula = containsNumberOfMinions ? firstDamage : `(${firstDamage} * ${CONSTANTS.NUMBER_MINIONS_BONUS})`; + const damageType = part[1]; + + return `${newFormula}${damageType ? `[${damageType}]` : ""}` + }) } diff --git a/scripts/plugins/midiqol.js b/scripts/plugins/midiqol.js index 92f696d..9db3c4f 100644 --- a/scripts/plugins/midiqol.js +++ b/scripts/plugins/midiqol.js @@ -28,14 +28,16 @@ export default { options: { height: "100%" } }) - const numMinionsAttacked = Number(result) || 1; - minionAttacks[workflow.id] = numMinionsAttacked; + const numberOfMinions = Math.max(1, Number(result) || 1) + minionAttacks[workflow.id] = numberOfMinions; if (!lib.getSetting(CONSTANTS.SETTING_KEYS.ENABLE_GROUP_ATTACK_BONUS)) return true; const attackHookId = Hooks.on("dnd5e.preRollAttack", (rolledItem, rollConfig) => { if (rolledItem !== workflow.item) return true; - rollConfig.parts.push(numMinionsAttacked); + rollConfig.data.numberOfMinions = numberOfMinions; + const containsNumberOfMinions = rollConfig.parts.some(part => part[0].includes(CONSTANTS.NUMBER_MINIONS_BONUS)) + if(!containsNumberOfMinions) rollConfig.parts.push(CONSTANTS.NUMBER_MINIONS_BONUS); Hooks.off("dnd5e.preRollAttack", attackHookId); return true; }); @@ -45,30 +47,46 @@ export default { Hooks.on("midi-qol.preDamageRoll", async (workflow) => { - if (!minionAttacks?.[workflow.id]) return true; if (workflow.item.system?.damage?.parts?.length < 1) return true; - const newDamageParts = []; + if (!lib.getSetting(CONSTANTS.SETTING_KEYS.ENABLE_GROUP_ATTACKS)) return true; + + const isGroupAttack = getProperty(workflow.item, CONSTANTS.FLAGS.MIDI_GROUP_ATTACK) ?? false; + + if (!api.isMinion(workflow.actor) || !isGroupAttack) return true; + + if (!minionAttacks?.[workflow.id]) { + + const result = await Dialog.confirm({ + title: game.i18n.localize("MINIONMANAGER.Dialogs.MinionAttack.Title"), + content: ` +

${game.i18n.localize("MINIONMANAGER.Dialogs.MinionAttack.Label")}

+

+ `, + yes: (html) => { + return html.find('input[name="numberOfAttacks"]').val() + }, + options: { height: "100%" } + }) + + minionAttacks[workflow.id] = Math.max(1, Number(result) || 1); - for (let index = 0; index < workflow.item.system.damage.parts.length; index++) { - const firstDamage = workflow.item.system.damage.parts[index][0]; - const newFormula = isNaN(Number(firstDamage)) - ? "(" + firstDamage.toString() + " * " + minionAttacks[workflow.id].toString() + ")" - : Number(firstDamage) * minionAttacks[workflow.id]; - const damageType = workflow.item.system.damage.parts[index][1]; - newDamageParts.push(`${newFormula}${damageType ? `[${damageType}]` : ""}`); } - delete minionAttacks[workflow.id]; + const numberOfMinions = minionAttacks[workflow.id]; const damageHookId = Hooks.on("dnd5e.preRollDamage", (rolledItem, rollConfig) => { if (rolledItem !== workflow.item) return true; - rollConfig.parts = newDamageParts; + rollConfig.data.numberOfMinions = numberOfMinions; + rollConfig.parts = lib.patchItemDamageRollConfig(workflow.item); Hooks.off("dnd5e.preRollDamage", damageHookId); return true; }); + delete minionAttacks[workflow.id]; + return true; + }); Hooks.on("midi-qol.postCheckSaves", async (workflow) => { diff --git a/scripts/plugins/vanilla.js b/scripts/plugins/vanilla.js index b8cedb3..0c4970d 100644 --- a/scripts/plugins/vanilla.js +++ b/scripts/plugins/vanilla.js @@ -18,7 +18,6 @@ export default { // If we've already prompted the user, and the attack hasn't gone through, then we continue the original attack if (minionAttacks[item.parent.uuid] && !minionAttacks[item.parent.uuid].attacked) { - rollConfig.parts = minionAttacks[item.parent.uuid].rollConfig.parts; minionAttacks[item.parent.uuid].attacked = true; return true; } @@ -35,15 +34,19 @@ export default { options: { height: "100%" } }).then(result => { - const numMinionsAttacked = Number(result) || 1; + const numberOfMinions = Math.max(1, Number(result) || 1); + minionAttacks[item.parent.uuid] = { + numberOfMinions, + attacked: false + }; + + rollConfig.data.numberOfMinions = numberOfMinions; if (lib.getSetting(CONSTANTS.SETTING_KEYS.ENABLE_GROUP_ATTACK_BONUS)) { - rollConfig.parts.push(numMinionsAttacked); + const containsNumberOfMinions = rollConfig.parts.some(part => part[0].includes(CONSTANTS.NUMBER_MINIONS_BONUS)) + if (!containsNumberOfMinions) rollConfig.parts.push(CONSTANTS.NUMBER_MINIONS_BONUS); } - minionAttacks[item.parent.uuid] = { numMinionsAttacked, rollConfig, attacked: false }; - - // Roll the attack with the existing config, which just includes the minion bonus item.rollAttack(rollConfig); }); @@ -52,8 +55,6 @@ export default { }); - const minionOnlyDamages = {}; - Hooks.on("dnd5e.preRollDamage", (item, rollConfig) => { if (item.system?.damage?.parts?.length < 1) return true; @@ -61,49 +62,16 @@ export default { const isGroupAttack = getProperty(item, CONSTANTS.FLAGS.MIDI_GROUP_ATTACK) ?? false; if (!api.isMinion(item.parent) || !isGroupAttack) return true; - if (minionAttacks?.[item.parent.uuid]) { - - for (let index = 0; index < item.system.damage.parts.length; index++) { + if (minionAttacks?.[item.parent.uuid] && minionAttacks?.[item.parent.uuid].attacked) { - const firstDamage = item.system.damage.parts[index][0]; - const newFormula = isNaN(Number(firstDamage)) - ? "(" + firstDamage.toString() + " * " + minionOnlyDamages[item.parent.uuid].numMinionsAttacked.toString() + ")" - : Number(firstDamage) * minionOnlyDamages[item.parent.uuid].numMinionsAttacked; - - const damageType = item.system.damage.parts[index][1]; - - if (lib.getSetting(CONSTANTS.SETTING_KEYS.ENABLE_GROUP_ATTACK_BONUS)) { - rollConfig.parts[index] = [`${newFormula}${damageType ? `[${damageType}]` : ""}`]; - } - } + rollConfig.data.numberOfMinions = minionAttacks[item.parent.uuid].numberOfMinions; + rollConfig.parts = lib.patchItemDamageRollConfig(item); delete minionAttacks[item.parent.uuid]; return true; - } else { - - // If we've already prompted the user, and the attack hasn't gone through, then we continue the original attack - if (minionOnlyDamages[item.parent.uuid] && !minionOnlyDamages[item.parent.uuid].attacked) { - - for (let index = 0; index < item.system.damage.parts.length; index++) { - - const firstDamage = item.system.damage.parts[index][0]; - const newFormula = isNaN(Number(firstDamage)) - ? "(" + firstDamage.toString() + " * " + minionOnlyDamages[item.parent.uuid].numMinionsAttacked.toString() + ")" - : Number(firstDamage) * minionOnlyDamages[item.parent.uuid].numMinionsAttacked; - - const damageType = item.system.damage.parts[index][1]; - - if (lib.getSetting(CONSTANTS.SETTING_KEYS.ENABLE_GROUP_ATTACK_BONUS)) { - rollConfig.parts[index] = [`${newFormula}${damageType ? `[${damageType}]` : ""}`]; - } - } - - delete minionOnlyDamages[item.parent.uuid]; - - return true; - } + } else if (!minionAttacks?.[item.parent.uuid]){ Dialog.confirm({ title: game.i18n.localize("MINIONMANAGER.Dialogs.MinionAttack.Title"), @@ -117,9 +85,10 @@ export default { options: { height: "100%" } }).then(result => { - const numMinionsAttacked = Number(result) || 1; - - minionOnlyDamages[item.parent.uuid] = { numMinionsAttacked, rollConfig, attacked: false }; + minionAttacks[item.parent.uuid] = { + numberOfMinions: Math.max(1, Number(result) || 1), + attacked: true + }; // Roll the damage with the existing config, which just includes the minion bonus item.rollDamage(rollConfig);