Skip to content

Commit

Permalink
added DC scaling, group attack checkbox, tidy5e compatibility
Browse files Browse the repository at this point in the history
  • Loading branch information
roth-michael committed May 19, 2024
1 parent d0fdb13 commit d327b8b
Show file tree
Hide file tree
Showing 7 changed files with 258 additions and 30 deletions.
9 changes: 9 additions & 0 deletions languages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@
"TurnIntoMinionFeature": "Set as minion group attack",
"RevertFromMinionFeature": "Set as regular attack"
},
"ItemOverrides": {
"ScaleDC": "DC scales with minions",
"Enabled": "Enabled",
"GroupAttack": "Is a minion group attack"
},
"Errors": {
"MidiActive": "MidiQOL is required for Minion Manager to function! Please install & activate the module."
},
Expand Down Expand Up @@ -57,6 +62,10 @@
"Title": "Enable Minion Group Attack Bonus",
"Hint": "When enabled, minions get a bonus to their group attack rolls equal to the number of attacking minions."
},
"EnableGroupDCBonus": {
"Title": "Enable Minion Group DC Bonus",
"Hint": "When enabled, minions get a bonus to the DC of any of their group attack rolls equal to the number of attacking minions."
},
"EnableMinionSuperSave": {
"Title": "Enable Minion Super Saves",
"Hint": "When enabled, minions that are forced to make a saving throw, they instead take no damage if they would take half when they succeed on their saving throw."
Expand Down
13 changes: 12 additions & 1 deletion scripts/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ const CONSTANTS = {
GROUP_NUMBER: `${FLAG}.groupNumber`,
DELETE_GROUP_NUMBER: `${FLAG}.-=groupNumber`,
MIDI_GROUP_ATTACK: "flags.midiProperties.grpact",
MINION_FEATURE: `${FLAG}.minionfeature.`
MINION_FEATURE: `${FLAG}.minionfeature.`,
DC_SCALING_ENABLED: `${FLAG}.dcScaling`
},
MODULES: {
MIDI: false
Expand All @@ -35,6 +36,7 @@ CONSTANTS["SETTING_KEYS"] = {
ENABLE_OVERKILL_MESSAGE: "enableOverkillMessage",
ENABLE_GROUP_ATTACKS: "enableGroupAttacks",
ENABLE_GROUP_ATTACK_BONUS: "enableGroupAttackBonus",
ENABLE_GROUP_DC_BONUS: "enableGroupDCBonus",
ENABLE_MINION_SUPER_SAVE: "enableMinionSuperSave",
MINION_FEATURE_NAME: "minionFeatureName",
MINION_FEATURE_DESCRIPTION: "minionFeatureDescription",
Expand Down Expand Up @@ -114,6 +116,15 @@ CONSTANTS["SETTINGS"] = () => {
type: Boolean
},

[CONSTANTS.SETTING_KEYS.ENABLE_GROUP_DC_BONUS]: {
name: "MINIONMANAGER.Settings.EnableGroupDCBonus.Title",
hint: "MINIONMANAGER.Settings.EnableGroupDCBonus.Hint",
scope: "world",
config: true,
default: true,
type: Boolean
},

[CONSTANTS.SETTING_KEYS.ENABLE_MINION_SUPER_SAVE]: {
name: "MINIONMANAGER.Settings.EnableMinionSuperSave.Title",
hint: "MINIONMANAGER.Settings.EnableMinionSuperSave.Hint",
Expand Down
5 changes: 3 additions & 2 deletions scripts/interface.js
Original file line number Diff line number Diff line change
Expand Up @@ -194,14 +194,15 @@ export function initializeInterface() {

Hooks.on("dnd5e.getItemContextOptions", (item, menuItems) => {
const actor = item.parent;
const isGroupAttackable = ["feat", "weapon", "spell"].includes(item.type);
menuItems.push({
name: game.i18n.localize("MINIONMANAGER.ItemContextMenu.TurnIntoMinionFeature"),
icon: `<i class="fas fa-users"></i>`,
callback: async () => {
return api.setActorItemToGroupAttack(item, true);
},
condition: () => {
return game.user.isGM && !api.isItemGroupAttack(item) && api.isMinion(actor);
return game.user.isGM && !api.isItemGroupAttack(item) && api.isMinion(actor) && isGroupAttackable;
}
}, {
name: game.i18n.localize("MINIONMANAGER.ItemContextMenu.RevertFromMinionFeature"),
Expand All @@ -210,7 +211,7 @@ export function initializeInterface() {
return api.setActorItemToGroupAttack(item, false);
},
condition: () => {
return game.user.isGM && api.isItemGroupAttack(item) && api.isMinion(actor);
return game.user.isGM && api.isItemGroupAttack(item) && api.isMinion(actor) && isGroupAttackable;
}
});
});
Expand Down
2 changes: 2 additions & 0 deletions scripts/module.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import CONSTANTS from "./constants.js";
import { initializeMinions } from "./minion.js";
import { initializeInterface } from "./interface.js";
import { initializeInitiative } from "./initiative.js";
import { registerSheetOverrides } from "./sheet-overrides.js";
import * as API from "./api.js";

Hooks.on("init", () => {
Expand All @@ -10,6 +11,7 @@ Hooks.on("init", () => {
initializeMinions();
initializeInterface();
initializeInitiative();
registerSheetOverrides();
});

Hooks.once("ready", () => {
Expand Down
37 changes: 36 additions & 1 deletion scripts/plugins/midiqol.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,12 +83,47 @@ export default {
return true;
});

delete minionAttacks[workflow.id];
if (!workflow.hasSave) delete minionAttacks[workflow.id];

return true;

});

Hooks.on("midi-qol.preCheckSaves", async (workflow) => {
if (!lib.getSetting(CONSTANTS.SETTING_KEYS.ENABLE_GROUP_ATTACKS)) return true;

const isGroupAttack = getProperty(workflow.item, CONSTANTS.FLAGS.MIDI_GROUP_ATTACK) ?? false;

const isScaleDC = getProperty(workflow.item, CONSTANTS.FLAGS.DC_SCALING_ENABLED) ?? false;

if (!workflow.actor || !api.isMinion(workflow?.actor) || !isGroupAttack || !isScaleDC) 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);;
}

const numberOfMinions = minionAttacks[workflow.id];

if (!lib.getSetting(CONSTANTS.SETTING_KEYS.ENABLE_GROUP_DC_BONUS)) return true;

workflow.item = workflow.item.clone({"system.save": {"dc": workflow.item.system.save.dc + (numberOfMinions > 1 ? numberOfMinions : 0), "scaling": "flat"}}, {"keepId": true});
workflow.item.prepareData();
workflow.item.prepareFinalAttributes();
delete minionAttacks[workflow.id];
});

Hooks.on("midi-qol.postCheckSaves", async (workflow) => {
if (!lib.getSetting(CONSTANTS.SETTING_KEYS.ENABLE_MINION_SUPER_SAVE)) return;
for (const savedToken of workflow.saves) {
Expand Down
110 changes: 84 additions & 26 deletions scripts/plugins/vanilla.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,41 +16,52 @@ export default {

if (!api.isMinion(item.parent) || !isGroupAttack) return true;

// 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) {
// If we've already prompted the user, and the attack hasn't gone through (and we didn't prompt for a save DC), then we continue the original attack
if (minionAttacks[item.parent.uuid] && !minionAttacks[item.parent.uuid].attacked && !minionAttacks[item.parent.uuid].oldDC) {
minionAttacks[item.parent.uuid].attacked = true;
return true;
}

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%" }
}).then(result => {

const numberOfMinions = Math.max(1, Number(result) || 1);

minionAttacks[item.parent.uuid] = {
numberOfMinions,
attacked: false
};

if (!minionAttacks[item.parent.uuid]) {
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%" }
}).then(result => {

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)) {
const containsNumberOfMinions = rollConfig.parts.includes(CONSTANTS.NUMBER_MINIONS_BONUS);
rollConfig.parts = [];
if (!containsNumberOfMinions) rollConfig.parts.push(numberOfMinions > 1 ? CONSTANTS.NUMBER_MINIONS_BONUS : '');
}

item.rollAttack(rollConfig);

});
} else {
const numberOfMinions = minionAttacks[item.parent.uuid].numberOfMinions;
rollConfig.data.numberOfMinions = numberOfMinions;
if (lib.getSetting(CONSTANTS.SETTING_KEYS.ENABLE_GROUP_ATTACK_BONUS)) {
const containsNumberOfMinions = rollConfig.parts.includes(CONSTANTS.NUMBER_MINIONS_BONUS);
rollConfig.parts = [];
if (!containsNumberOfMinions) rollConfig.parts.push(numberOfMinions > 1 ? CONSTANTS.NUMBER_MINIONS_BONUS : '');
}

item.rollAttack(rollConfig);

});
minionAttacks[item.parent.uuid].attacked = true;
return true;
}

return false;

Expand Down Expand Up @@ -102,6 +113,53 @@ export default {

});

Hooks.on("dnd5e.preUseItem", (item, config, options) => {
if (!lib.getSetting(CONSTANTS.SETTING_KEYS.ENABLE_GROUP_ATTACKS)) return true;

const isGroupAttack = getProperty(item, CONSTANTS.FLAGS.MIDI_GROUP_ATTACK) ?? false;

const isScaleDC = getProperty(item, CONSTANTS.FLAGS.DC_SCALING_ENABLED) ?? false;

if (!api.isMinion(item.parent) || !isGroupAttack || !isScaleDC) return true;

if (!lib.getSetting(CONSTANTS.SETTING_KEYS.ENABLE_GROUP_DC_BONUS)) return true;

if (!item.system.save || item.system.save?.ability == "" || !item.system.save?.dc) return true;

if (minionAttacks?.[item.parent.uuid]?.oldDC) return true;
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%" }
}).then(result => {
const numberOfMinions = Math.max(1, Number(result) || 1)

minionAttacks[item.parent.uuid] = {
numberOfMinions,
attacked: false,
oldDC: item.system.save?.dc
};

item.system.save.dc = item.system.save.dc + (numberOfMinions > 1 ? numberOfMinions : 0);
item.use(config, options);
});
return false;
});

Hooks.on("dnd5e.useItem", async (item) => {
let minionAttackData = minionAttacks?.[item.parent.uuid];
if (minionAttackData?.oldDC) {
item.system.save.dc = minionAttackData.oldDC;
}
if (!item.hasAttack) delete minionAttacks?.[item.parent.uuid];
return true;
});

Hooks.on("dnd5e.rollDamage", async (item, damageRoll) => {

Expand Down
112 changes: 112 additions & 0 deletions scripts/sheet-overrides.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import CONSTANTS from "./constants.js";
import * as lib from "./lib.js";
import * as api from "./api.js";

export function registerSheetOverrides() {
Hooks.on("renderItemSheet5e", patchItemSheet);
Hooks.on("tidy5e-sheet.renderItemSheet", patchTidyItemSheet);
}

function patchItemSheet(app, html, { item } = {}) {
if (!app.options.classes.includes("tidy5e-sheet")) {
patchGroupAttack(html, item);
patchDCScaling(html, item);
}
}

function patchTidyItemSheet(app, element, { item }, forced) {
patchTidyGroupAttack(element, item);
patchTidyDCScaling(element, item);
}

function patchGroupAttack(html, item) {
if (lib.getSetting(CONSTANTS.SETTING_KEYS.ENABLE_GROUP_ATTACKS) && api.isMinion(item.parent)) {
if (["feat", "weapon", "spell"].includes(item.type)) {
const featureHeader = game.i18n.localize("DND5E.FeatureUsage");
const weaponHeader = game.i18n.localize("DND5E.ItemWeaponUsage");
const spellHeader = game.i18n.localize("DND5E.SpellCastingHeader");
let targetElem = html.find(`
.form-header:contains(${featureHeader}),
.form-header:contains(${weaponHeader}),
.form-header:contains(${spellHeader})`)[0];
if (!targetElem) return;
$(getGroupAttackHtml(item)).insertBefore(targetElem);
}
}
}

function patchTidyGroupAttack(element, item) {
if (lib.getSetting(CONSTANTS.SETTING_KEYS.ENABLE_GROUP_ATTACKS) && api.isMinion(item.parent)) {
const html = $(element);
const markupToInject = `
<div style="display: contents;" data-tidy-render-scheme="handlebars">
${getGroupAttackHtml(item)}
</div>
`;
if (["feat", "weapon", "spell"].includes(item.type)) {
const featureHeader = game.i18n.localize("DND5E.FeatureUsage");
const weaponHeader = game.i18n.localize("DND5E.ItemWeaponUsage");
const spellHeader = game.i18n.localize("DND5E.SpellCastingHeader");
let targetElem = html.find(`
.form-header:contains(${featureHeader}),
.form-header:contains(${weaponHeader}),
.form-header:contains(${spellHeader})`)[0];
if (!targetElem) return;
$(markupToInject).insertBefore(targetElem);
}
}
}

function patchDCScaling(html, item) {
if (lib.getSetting(CONSTANTS.SETTING_KEYS.ENABLE_GROUP_DC_BONUS) && getProperty(item, CONSTANTS.FLAGS.MIDI_GROUP_ATTACK)) {
let targetElem = html.find('[name="system.save.ability"]')?.parent()?.parent()?.[0];
if (!targetElem) return;
$(getDCScalingHtml(item)).insertAfter(targetElem);
}
}

function patchTidyDCScaling(element, item) {
if (lib.getSetting(CONSTANTS.SETTING_KEYS.ENABLE_GROUP_DC_BONUS) && getProperty(item, CONSTANTS.FLAGS.MIDI_GROUP_ATTACK)) {
const html = $(element);
const markupToInject = `
<div style="display: contents;" data-tidy-render-scheme="handlebars">
${getDCScalingHtml(item)}
</div>
`;
let targetElem = html.find('[data-tidy-field="system.save.ability"]')?.parent()?.parent()?.[0];
if (!targetElem) return;
$(markupToInject).insertAfter(targetElem);
}
}

function getGroupAttackHtml(item) {
const groupAttackEnabled = foundry.utils.getProperty(item, CONSTANTS.FLAGS.MIDI_GROUP_ATTACK) ?? false;
let idealWidth = Math.ceil((1.2 * game.i18n.localize("MINIONMANAGER.ItemOverrides.Enabled").length) + 3);
return `
<div class="form-group" title="Module: Minion Manager">
<label>${game.i18n.localize("MINIONMANAGER.ItemOverrides.GroupAttack")} <i class="fas fa-info-circle"></i></label>
<div class="form-fields">
<label class="checkbox" style="width: ${idealWidth ?? 12}ch">
<input type="checkbox" name="${CONSTANTS.FLAGS.MIDI_GROUP_ATTACK}" ${groupAttackEnabled ? "checked" : ""}>
${game.i18n.localize("MINIONMANAGER.ItemOverrides.Enabled")}
</label>
</div>
</div>
`
}

function getDCScalingHtml(item) {
const dcScalingEnabled = foundry.utils.getProperty(item, `${CONSTANTS.FLAGS.DC_SCALING_ENABLED}`) ?? false;
let idealWidth = Math.ceil((1.2 * game.i18n.localize("MINIONMANAGER.ItemOverrides.Enabled").length) + 3);
return `
<div class="form-group" title="Module: Minion Manager" style="display:${item.system.hasSave ? "flex" : "none"}">
<label>${game.i18n.localize("MINIONMANAGER.ItemOverrides.ScaleDC")} <i class="fas fa-info-circle"></i></label>
<div class="form-fields">
<label class="checkbox" style="width: ${idealWidth ?? 12}ch">
<input type="checkbox" name="${CONSTANTS.FLAGS.DC_SCALING_ENABLED}" ${dcScalingEnabled ? "checked" : ""}>
${game.i18n.localize("MINIONMANAGER.ItemOverrides.Enabled")}
</label>
</div>
</div>
`;
}

0 comments on commit d327b8b

Please sign in to comment.