Skip to content

Commit

Permalink
Minion damage
Browse files Browse the repository at this point in the history
  • Loading branch information
Haxxer committed Oct 9, 2023
1 parent 355e270 commit dfb0204
Show file tree
Hide file tree
Showing 6 changed files with 66 additions and 66 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Binary file added docs/number-of-minions.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions scripts/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const CONSTANTS = {
}
}],
ATTACK_TYPES: ["mwak", "rwak", "msak", "rsak"],
NUMBER_MINIONS_BONUS: "@numberOfMinions",
FLAGS: {
COMBATANTS: `${FLAG}.combatants`,
GROUP_NUMBER: `${FLAG}.groupNumber`,
Expand Down
14 changes: 10 additions & 4 deletions scripts/lib.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}]` : ""}`
})
}
46 changes: 32 additions & 14 deletions scripts/plugins/midiqol.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
});
Expand All @@ -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: `
<p>${game.i18n.localize("MINIONMANAGER.Dialogs.MinionAttack.Label")}</p>
<p><input name="numberOfAttacks" type="number" value="1"></p>
`,
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) => {
Expand Down
65 changes: 17 additions & 48 deletions scripts/plugins/vanilla.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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);

});
Expand All @@ -52,58 +55,23 @@ export default {

});

const minionOnlyDamages = {};

Hooks.on("dnd5e.preRollDamage", (item, rollConfig) => {

if (item.system?.damage?.parts?.length < 1) return true;
if (!lib.getSetting(CONSTANTS.SETTING_KEYS.ENABLE_GROUP_ATTACKS)) return true;
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"),
Expand All @@ -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);
Expand Down

0 comments on commit dfb0204

Please sign in to comment.