Skip to content

Commit

Permalink
v0.3.12
Browse files Browse the repository at this point in the history
- Changed "groupUpdate" hook name to "ctgGroupUpdate" for improved namespace clarity.
- Likewise "mobUpdate" hook name was changed to "matMobUpdate".
- Hovering over mob attack roll results will now also show the discarded roll result in the "title" tooltip if said roll was made with advantage/disadvantage. This relates to issue #35.
  • Loading branch information
Stendarpaval committed Nov 10, 2021
1 parent 733822f commit 7e3414d
Show file tree
Hide file tree
Showing 8 changed files with 133 additions and 38 deletions.
2 changes: 2 additions & 0 deletions lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@
"SETTINGS.MAT.enableMidiHint": "Enable this setting to use Midi-QOL for dice rolls if you have that module installed. Disable this setting to prevent Mob Attack Tool from using it.",
"SETTINGS.MAT.enableMidiOnUseMacro": "Enable On Use Macro",
"SETTINGS.MAT.enableMidiOnUseMacroHint": "If this setting is enabled, Mob Attack Tool will execute macros that are named in the On Use Macro fields of weapon or spell items. This feature requires Midi-QOL.",
"SETTINGS.MAT.onUseMacroOnlyOnHits": "Limit On Use Macros to attacks that hit",
"SETTINGS.MAT.onUseMacroOnlyOnHitsHint": "While this setting is enabled (in combination with the \"Enable On Use Macro\" setting), only the attack rolls of a mob attack that successfully hit the target will trigger Midi-QOL's On Use Macro feature. Disable this setting to cause all attacks to trigger the On Use Macro of their respective weapon or spell items.",
"SETTINGS.MAT.dontSendItemCardId": "Don't share itemCardId",
"SETTINGS.MAT.dontSendItemCardIdHint": "This is related to Midi-QOL's On Use Macro feature. Midi-QOL uses the value of itemCardId to merge dice rolls to one chat message card, or to add extra damage rolls to such cards. Mob Attack Tool uses Midi's DamageOnlyWorkflow, which creates chat messages with rolls that are considered \"Other Damage\" rolls. Unfortunately, that means that any On Use Macros that also use DamageOnlyWorkflow will overwrite Mob Attack Tool's damage roll card. If you enable this setting, the value of itemCardId won't be shared, which causes new chat messages to be created instead.",
"SETTINGS.MAT.enableBetterRolls": "Enable Better Rolls for 5e",
Expand Down
4 changes: 2 additions & 2 deletions module.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
}
],
"url": "https://github.com/Stendarpaval/mob-attack-tool",
"version": "0.3.11",
"version": "0.3.12",
"minimumCoreVersion": "0.8.8",
"compatibleCoreVersion": "0.8.9",
"system": ["dnd5e"],
Expand All @@ -27,5 +27,5 @@
"styles/mob-attack-tool.css"
],
"manifest": "https://raw.githubusercontent.com/Stendarpaval/mob-attack-tool/main/module.json",
"download": "https://github.com/Stendarpaval/mob-attack-tool/releases/download/0.3.11/module.zip"
"download": "https://github.com/Stendarpaval/mob-attack-tool/releases/download/0.3.12/module.zip"
}
89 changes: 75 additions & 14 deletions scripts/individualRolls.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,15 @@ export async function rollMobAttackIndividually(data) {
}

let tokenAttackList = [];
let discardedRollTotal;
let discarded = false;
for (let i = 0; i < availableAttacks; i++) {
attackRoll = new Roll(attackFormula);
attackRollEvaluated[i] = await attackRoll.evaluate({async: true});
if (attackRollEvaluated[i].dice[0].results.length > 1) {
discardedRollTotal = attackRollEvaluated[i].dice[0].results.filter(r => r.discarded)[0].result + finalAttackBonus;
discarded = true;
}

// Check settings for rolling 3d dice from Dice So Nice
if (game.user.getFlag(moduleName,"showIndividualAttackRolls") ?? game.settings.get(moduleName,"showIndividualAttackRolls")) {
Expand All @@ -75,20 +81,20 @@ export async function rollMobAttackIndividually(data) {
numCrits++;
numHitAttacks += 1;
successfulAttackRolls.push(attackRollEvaluated[i]);
atkRollData.push({roll: attackRollEvaluated[i].total, color: "max", finalAttackBonus: finalAttackBonus});
atkRollData.push({roll: attackRollEvaluated[i].total, color: "max", finalAttackBonus, discarded, discardedRollTotal});
attackToken = availableTokens[Math.floor(Math.random()*availableTokens.length)];
} else if (attackRollEvaluated[i].total - finalAttackBonus === 1) {
numCritFails++;
if (game.user.getFlag(moduleName,"showAllAttackRolls") ?? game.settings.get(moduleName,"showAllAttackRolls")) {
atkRollData.push({roll: attackRollEvaluated[i].total, color: "min", finalAttackBonus: finalAttackBonus});
atkRollData.push({roll: attackRollEvaluated[i].total, color: "min", finalAttackBonus, discarded, discardedRollTotal});
}
} else if (attackRollEvaluated[i].total >= ((targetAC) ? targetAC : 0) && attackRollEvaluated[i].total - finalAttackBonus > 1) {
numHitAttacks += 1;
successfulAttackRolls.push(attackRollEvaluated[i]);
atkRollData.push({roll: attackRollEvaluated[i].total, color: "", finalAttackBonus: finalAttackBonus});
atkRollData.push({roll: attackRollEvaluated[i].total, color: "", finalAttackBonus, discarded, discardedRollTotal});
attackToken = availableTokens[Math.floor(Math.random()*availableTokens.length)];
} else if (game.user.getFlag(moduleName,"showAllAttackRolls") ?? game.settings.get(moduleName,"showAllAttackRolls")) {
atkRollData.push({roll: attackRollEvaluated[i].total, color: "discarded", finalAttackBonus: finalAttackBonus});
atkRollData.push({roll: attackRollEvaluated[i].total, color: "discarded", finalAttackBonus, discarded, discardedRollTotal});
}
if (attackToken) tokenAttackList.push(attackToken);
}
Expand Down Expand Up @@ -149,6 +155,7 @@ export async function rollMobAttackIndividually(data) {
data,
weaponData,
finalAttackBonus,
availableAttacks,
successfulAttackRolls,
numHitAttacks,
numCrits,
Expand All @@ -173,7 +180,7 @@ export async function rollMobAttackIndividually(data) {

// Process damage rolls
for (let attack of attackData) {
await processIndividualDamageRolls(attack.data, attack.weaponData, attack.finalAttackBonus, attack.successfulAttackRolls, attack.numHitAttacks, attack.numCrits, attack.isVersatile, attack.tokenAttackList, attack.targetId);
await processIndividualDamageRolls(attack.data, attack.weaponData, attack.finalAttackBonus, attack.availableAttacks, attack.successfulAttackRolls, attack.numHitAttacks, attack.numCrits, attack.isVersatile, attack.tokenAttackList, attack.targetId);
await new Promise(resolve => setTimeout(resolve, 500));
}

Expand All @@ -183,7 +190,7 @@ export async function rollMobAttackIndividually(data) {
}


export async function processIndividualDamageRolls(data, weaponData, finalAttackBonus, successfulAttackRolls, numHitAttacks, numCrits, isVersatile, tokenAttackList, targetId) {
export async function processIndividualDamageRolls(data, weaponData, finalAttackBonus, availableAttacks, successfulAttackRolls, numHitAttacks, numCrits, isVersatile, tokenAttackList, targetId) {

// Check for betterrolls5e and midi-qol
let betterrollsActive = false;
Expand All @@ -202,6 +209,16 @@ export async function processIndividualDamageRolls(data, weaponData, finalAttack
// Process attack and damage rolls
let showAttackRolls = game.user.getFlag(moduleName,"showIndividualAttackRolls") ?? game.settings.get(moduleName,"showIndividualAttackRolls");
let showDamageRolls = game.user.getFlag(moduleName,"showIndividualDamageRolls") ?? game.settings.get(moduleName,"showIndividualDamageRolls");

// Determine target token
let targetToken = canvas.tokens.get(targetId);
if (targetToken?.actor === null && game.modules.get("multilevel-tokens").active) {
let mltFlags = targetToken.data.flags["multilevel-tokens"];
if (mltFlags?.sscene) {
targetToken = game.scenes.get(mltFlags.sscene).data.tokens.get(mltFlags.stoken);
}
}

if (numHitAttacks != 0) {
// Better Rolls 5e active
if (betterrollsActive) {
Expand Down Expand Up @@ -279,13 +296,6 @@ export async function processIndividualDamageRolls(data, weaponData, finalAttack
}
}
damageRoll = await damageRoll.evaluate({async: true});
let targetToken = canvas.tokens.get(targetId);
if (targetToken?.actor === null && game.modules.get("multilevel-tokens").active) {
let mltFlags = targetToken.data.flags["multilevel-tokens"];
if (mltFlags?.sscene) {
targetToken = game.scenes.get(mltFlags.sscene).data.tokens.get(mltFlags.stoken);
}
}

// Roll Dice so Nice dice
if (game.modules.get("dice-so-nice")?.active && game.settings.get(moduleName, "enableDiceSoNice")) game.dice3d.showForRoll(damageRoll);
Expand Down Expand Up @@ -342,6 +352,7 @@ export async function processIndividualDamageRolls(data, weaponData, finalAttack
templateId: workflow.templateId,
templateUuid: workflow.templateUuid
}

let j = 0;
for (let i = 0; i < numHitAttacks; i++) {
if (j < tokenAttackList.length) {
Expand Down Expand Up @@ -424,7 +435,57 @@ export async function processIndividualDamageRolls(data, weaponData, finalAttack
}
}
}
}
} // else if (midi_QOL_Active && !game.settings.get(moduleName,"onUseMacroOnlyOnHits") && game.settings.get(moduleName, "enableMidiOnUseMacro") && getProperty(weaponData, "data.flags.midi-qol.onUseMacroName")) {

// await new Promise(resolve => setTimeout(resolve, 300));
// let workflow = new MidiQOL.Workflow(weaponData.actor,weaponData,game.user,[],{});
// const macroData = {
// actor: weaponData.actor.data,
// actorUuid: weaponData.actor.uuid,
// targets: targetToken ? [targetToken] : [],
// hitTargets: targetToken ? [targetToken] : [],
// damageRoll: null,
// damageRollHTML: null,
// attackRoll: successfulAttackRolls[0],
// attackTotal: 0,
// itemCardId: null,
// isCritical: false,
// isFumble: false,
// spellLevel: 0,
// powerLevel: 0,
// damageTotal: 0,
// damageDetail: [],
// damageList: [],
// otherDamageTotal: 0,
// otherDamageDetail: [],
// otherDamageList: [{damage: 0, type: ""}],
// rollOptions: {advantage: data.withAdvantage, disadvantage: data.withDisadvantage, versatile: isVersatile, fastForward: true},
// advantage: data.withAdvantage,
// disadvantage: data.withDisadvantage,
// event: null,
// uuid: workflow.uuid,
// rollData: weaponData.actor.getRollData(),
// tag: "OnUse",
// concentrationData: getProperty(weaponData.actor.data.flags, "midi-qol.concentration-data"),
// templateId: workflow.templateId,
// templateUuid: workflow.templateUuid
// }
// let availableTokens = data.selectedTokenIds.filter(t => (canvas.tokens.get(t.tokenId).actor.id === weaponData.actor.id));
// let j = 0;
// for (let i = 0; i < availableAttacks; i++) {
// if (j < availableTokens.length - 1) {
// j = i;
// } else {
// j = availableTokens.length - 1;
// }
// if (j < availableTokens.length) {
// macroData.tokenId = availableTokens[j].tokenId;
// macroData.tokenUuid = availableTokens[j].tokenUuid;
// await callMidiMacro(weaponData, macroData);
// }
// }
// }

// Allow DSN 3d dice to be rolled again
if (game.user.isGM) await game.settings.set(moduleName, "hiddenDSNactiveFlag", true);
}
16 changes: 11 additions & 5 deletions scripts/mobAttack.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,17 @@ Hooks.on("ready", async () => {
window.MobAttacks = MobAttacks();

// check if CTG's groups have changed
Hooks.on("groupUpdate", async (args) => {
Hooks.on("ctgGroupUpdate", async (args) => {
let groups;
if (Array.isArray(args)) {
groups = args;
} else {
groups = args.groups;
}
if (!game.settings.get(moduleName, "autoSaveCTGgroups")) return;
if (args.groups[0]) {
if (args.groups[0].filter(c => c.initiative).length > 0) {
await MobAttacks().createSavedMobsFromCTGgroups(args.groups);
if (groups[0]) {
if (groups[0].filter(c => c.initiative).length > 0) {
await MobAttacks().createSavedMobsFromCTGgroups(groups);
const dialogId = game.settings.get(moduleName, "currentDialogId");
let mobDialog = game.mobAttackTool.dialogs.get(dialogId);
if (mobDialog) mobDialog.render();
Expand All @@ -56,7 +62,7 @@ Hooks.on("ready", async () => {
}
}
}
})
});
})

// update dialog windows if new tokens are selected
Expand Down
23 changes: 16 additions & 7 deletions scripts/mobAttackTool.js
Original file line number Diff line number Diff line change
Expand Up @@ -509,7 +509,7 @@ export class MobAttackDialog extends FormApplication {
async function saveMobList(mobList, mobName, monsterArray, selectedTokenIds, numSelected) {
mobList[mobName] = {mobName: mobName, monsters: monsterArray, selectedTokenIds: selectedTokenIds, numSelected: numSelected, userId: game.user.id};
await game.settings.set(moduleName,"hiddenMobList",mobList);
Hooks.call("mobUpdate", {mobList, mobName, type: "save"});
Hooks.call("matMobUpdate", {mobList, mobName, type: "save"});
if (game.combat) await game.combat.update();
ui.notifications.info(game.i18n.format("MAT.savedMobNotify",{mobName: mobName}));
}
Expand Down Expand Up @@ -580,7 +580,7 @@ export class MobAttackDialog extends FormApplication {
}
}
await game.settings.set(moduleName,"hiddenMobList",mobList);
Hooks.call("mobUpdate", {mobList, mobName: mobSelected, type: "delete"});
Hooks.call("matMobUpdate", {mobList, mobName: mobSelected, type: "delete"});
ui.notifications.info(game.i18n.format("MAT.deleteMobNotify",{mobName: mobSelected}));
mobSelected = Object.keys(mobList)[0];
await game.settings.set(moduleName,'hiddenMobName',mobSelected);
Expand All @@ -591,13 +591,13 @@ export class MobAttackDialog extends FormApplication {
}
}
await game.settings.set(moduleName,"hiddenMobList",mobList);
Hooks.call("mobUpdate", {mobList, mobName: mobSelected, type: "reset"});
Hooks.call("matMobUpdate", {mobList, mobName: mobSelected, type: "reset"});
ui.notifications.info(game.i18n.localize("MAT.resetAllMobsNotify"));
mobSelected = initialMobName;
await game.settings.set(moduleName,'hiddenMobName',mobSelected);
} else if (html.find(`input[name="resetAllMobs"]`)[0]?.checked) {
await game.settings.set(moduleName,"hiddenMobList",{});
Hooks.call("mobUpdate", {mobList, mobName: mobSelected, type: "resetAll"});
Hooks.call("matMobUpdate", {mobList, mobName: mobSelected, type: "resetAll"});
ui.notifications.info(game.i18n.localize("MAT.resetMobsNotify"));
mobSelected = initialMobName;
await game.settings.set(moduleName,'hiddenMobName',mobSelected);
Expand Down Expand Up @@ -891,8 +891,17 @@ export function MobAttacks() {
}
mobList[mobName] = {mobName: mobName, monsters: monsterArray, selectedTokenIds: selectedTokenIds, numSelected: numSelected, userId: game.user.id, type: type};
await game.settings.set(moduleName,"hiddenMobList",mobList);
Hooks.call("mobUpdate", {mobList, mobName, type: "save"});

Hooks.call("matMobUpdate", {mobList, mobName, type: "save"});
if (game.combat) await game.combat.update();

const dialogId = game.settings.get(moduleName, "currentDialogId");
let mobDialog = game.mobAttackTool.dialogs.get(dialogId);
if (mobDialog) {
mobDialog.localUpdate = true;
mobDialog.render();
}

return mobList;
}

Expand All @@ -916,13 +925,13 @@ export function MobAttacks() {
}
}
await game.settings.set("mob-attack-tool","hiddenMobList",mobList);
Hooks.call("mobUpdate", {mobList, mobName, type: "delete"});
Hooks.call("matMobUpdate", {mobList, mobName, type: "delete"});
const dialogId = game.settings.get(moduleName, "currentDialogId");
let mobDialog = game.mobAttackTool.dialogs.get(dialogId);
if (mobDialog) {
mobDialog.localUpdate = true;
await game.settings.set(moduleName, "hiddenChangedMob", false);
mobDialog.render();
mobDialog.render();
}
if (game.combat) await game.combat.update();
return mobList;
Expand Down
19 changes: 10 additions & 9 deletions scripts/mobRules.js
Original file line number Diff line number Diff line change
Expand Up @@ -241,17 +241,18 @@ export async function processMobRulesDamageRolls(data, weaponData, numHitAttacks
templateId: workflow.templateId,
templateUuid: workflow.templateUuid
}

let j = 0;
for (let i = 0; i < numHitAttacks; i++) {
if (j < tokenAttackList.length) {
j = i;
} else {
j = tokenAttackList.length - 1;
}
macroData.tokenId = tokenAttackList[j].tokenId;
macroData.tokenUuid = tokenAttackList[j].tokenUuid;
await callMidiMacro(weaponData, macroData);
for (let i = 0; i < numHitAttacks; i++) {
if (j < tokenAttackList.length) {
j = i;
} else {
j = tokenAttackList.length - 1;
}
macroData.tokenId = tokenAttackList[j].tokenId;
macroData.tokenUuid = tokenAttackList[j].tokenUuid;
await callMidiMacro(weaponData, macroData);
}
}
Hooks.call("midi-qol.DamageRollComplete", workflow);

Expand Down
16 changes: 16 additions & 0 deletions scripts/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,14 @@ const matSettings = {
default: true,
type: Boolean
},
// "onUseMacroOnlyOnHits": {
// name: "SETTINGS.MAT.onUseMacroOnlyOnHits",
// hint: "SETTINGS.MAT.onUseMacroOnlyOnHitsHint",
// scope: "world",
// config: false,
// default: true,
// type: Boolean
// },
"dontSendItemCardId": {
name: "SETTINGS.MAT.dontSendItemCardId",
hint: "SETTINGS.MAT.dontSendItemCardIdHint",
Expand Down Expand Up @@ -506,6 +514,14 @@ class RollSettingsMenu extends FormApplication {
isCheckbox: true,
client: game.user.isGM
},
// onUseMacroOnlyOnHits: {
// name: matSettings.onUseMacroOnlyOnHits.name,
// hint: matSettings.onUseMacroOnlyOnHits.hint,
// value: game.settings.get(moduleName,"onUseMacroOnlyOnHits"),
// id: "onUseMacroOnlyOnHits",
// isCheckbox: true,
// client: game.user.isGM
// },
dontSendItemCardId: {
name: matSettings.dontSendItemCardId.name,
hint: matSettings.dontSendItemCardId.hint,
Expand Down
2 changes: 1 addition & 1 deletion templates/mat-msg-individual-rolls.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
<div class="dice">
<ol class="dice-rolls">
{{#each this.atkRollData}}
<li class="roll die d20 {{this.color}}" title="{{localize 'Attack Roll'}} (+{{this.finalAttackBonus}} {{localize 'MAT.toHit'}}) {{#if ../withAdvantage}}(Advantage){{/if}}{{#if ../withDisadvantage}}(Disadvantage){{/if}}">{{this.roll}}</li>
<li class="roll die d20 {{this.color}}" title="{{localize 'Attack Roll'}} (+{{this.finalAttackBonus}} {{localize 'MAT.toHit'}}) {{#if ../withAdvantage}}(Advantage){{/if}}{{#if ../withDisadvantage}}(Disadvantage){{/if}} {{#if this.discarded}}{{localize 'Discarded Result'}}: {{this.discardedRollTotal}}{{/if}}">{{this.roll}}</li>
{{/each}}
</ol>
</div>
Expand Down

0 comments on commit 7e3414d

Please sign in to comment.