From 7056f2c526159640611a8cdbd09ab1a81223e97e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mang=C3=B3?= Date: Mon, 5 Sep 2022 19:31:43 +0200 Subject: [PATCH 1/8] Fix for halfling lucky --- src/utils/item.js | 2 -- src/utils/render.js | 19 ++++++++++++++----- src/utils/roll.js | 4 ++++ 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/utils/item.js b/src/utils/item.js index c59d24c..085b67e 100644 --- a/src/utils/item.js +++ b/src/utils/item.js @@ -22,8 +22,6 @@ export class ItemUtility { const isAltRoll = params?.isAltRoll ?? false; let fields = []; - console.log(item); - if (ItemUtility.getFlagValueFromItem(item, "quickFlavor", isAltRoll)) { addFieldFlavor(fields, chatData); } diff --git a/src/utils/render.js b/src/utils/render.js index e6885ff..c2f460b 100644 --- a/src/utils/render.js +++ b/src/utils/render.js @@ -116,18 +116,27 @@ async function renderMultiRoll(renderData = {}) { const bonusRoll = bonusTerms ? Roll.fromTerms(bonusTerms) : null; const d20Rolls = roll.dice.find(d => d.faces === 20); - for (let i = 0; i < d20Rolls.number; i++) { + for (let i = 0; i < d20Rolls.results.length; i++) { // Die terms must have active results or the base roll total of the generated roll is 0. - let tmpResult = d20Rolls.results[i]; - tmpResult.active = true; + let tmpResult = []; + tmpResult.push(d20Rolls.results[i]); - const baseTerm = new Die({number: 1, faces: 20, results: [tmpResult]}); + if (roll.options.halflingLucky && d20Rolls.results[i].result === 1) { + i++; + tmpResult.push(d20Rolls.results[i]); + } + + tmpResult.forEach(r => { + r.active = !r.rerolled ?? true; + }); + + const baseTerm = new Die({number: 1, faces: 20, results: tmpResult}); const baseRoll = Roll.fromTerms([baseTerm]); entries.push({ roll: baseRoll, total: baseRoll.total + (bonusRoll?.total ?? 0), - ignored: d20Rolls.results[i].discarded ? true : undefined, + ignored: tmpResult.some(r => r.discarded) ? true : undefined, isCrit: roll.isCritical, critType: RollUtility.getCritTypeForDie(baseTerm), d20Result: SettingsUtility.getSettingValue(SETTING_NAMES.D20_ICONS_ENABLED) ? d20Rolls.results[i].result : null diff --git a/src/utils/roll.js b/src/utils/roll.js index 64efe80..5a27347 100644 --- a/src/utils/roll.js +++ b/src/utils/roll.js @@ -287,6 +287,10 @@ function countCritsFumbles(die, critThreshold, fumbleThreshold) if (die.faces > 1) { for (const result of die.results) { + if (result.rerolled) { + continue; + } + if (result.result >= (critThreshold || die.faces)) { crit += 1; } else if (result.result <= (fumbleThreshold || 1)) { From 12f29089f7fb7d4304a717bf3faa3449deb9d853 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mang=C3=B3?= Date: Mon, 5 Sep 2022 19:37:03 +0200 Subject: [PATCH 2/8] Rename for clarity --- src/utils/render.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/utils/render.js b/src/utils/render.js index c2f460b..e630f23 100644 --- a/src/utils/render.js +++ b/src/utils/render.js @@ -118,25 +118,25 @@ async function renderMultiRoll(renderData = {}) { const d20Rolls = roll.dice.find(d => d.faces === 20); for (let i = 0; i < d20Rolls.results.length; i++) { // Die terms must have active results or the base roll total of the generated roll is 0. - let tmpResult = []; - tmpResult.push(d20Rolls.results[i]); + let tmpResults = []; + tmpResults.push(d20Rolls.results[i]); if (roll.options.halflingLucky && d20Rolls.results[i].result === 1) { i++; - tmpResult.push(d20Rolls.results[i]); + tmpResults.push(d20Rolls.results[i]); } - tmpResult.forEach(r => { + tmpResults.forEach(r => { r.active = !r.rerolled ?? true; }); - const baseTerm = new Die({number: 1, faces: 20, results: tmpResult}); + const baseTerm = new Die({number: 1, faces: 20, results: tmpResults}); const baseRoll = Roll.fromTerms([baseTerm]); entries.push({ roll: baseRoll, total: baseRoll.total + (bonusRoll?.total ?? 0), - ignored: tmpResult.some(r => r.discarded) ? true : undefined, + ignored: tmpResults.some(r => r.discarded) ? true : undefined, isCrit: roll.isCritical, critType: RollUtility.getCritTypeForDie(baseTerm), d20Result: SettingsUtility.getSettingValue(SETTING_NAMES.D20_ICONS_ENABLED) ? d20Rolls.results[i].result : null From 026e2bff89f18a687bb9c65d021d0d7cf75ca201 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mang=C3=B3?= Date: Mon, 5 Sep 2022 19:54:52 +0200 Subject: [PATCH 3/8] Fix issue with incorrect dice count when critting --- src/utils/item.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/utils/item.js b/src/utils/item.js index 085b67e..dc72f7f 100644 --- a/src/utils/item.js +++ b/src/utils/item.js @@ -242,13 +242,14 @@ async function addFieldDamage(fields, item, params) { if (params?.isCrit) { const critTerms = roll.options.multiplyNumeric ? group.terms : group.terms.filter(t => !(t instanceof NumericTerm)); const firstDie = critTerms.find(t => t instanceof Die); + const index = critTerms.indexOf(firstDie); if (i === 0 && firstDie) { - critTerms[critTerms.indexOf(firstDie)] = new Die({ + critTerms.splice(index, 1, new Die({ number: firstDie.number + roll.options.criticalBonusDice ?? 0, faces: firstDie.faces, results: firstDie.results - }); + })); } critRoll = await Roll.fromTerms(Roll.simplifyTerms(critTerms)).reroll({ From 332d69ba19a11097215e526c711b8a83a1541a92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mang=C3=B3?= Date: Mon, 5 Sep 2022 21:09:42 +0200 Subject: [PATCH 4/8] Fix issues with damage fields not being correctly outputted --- src/utils/hooks.js | 2 +- src/utils/item.js | 42 +++++++++++++++++++++++++++++------------- src/utils/sheet.js | 3 ++- 3 files changed, 32 insertions(+), 15 deletions(-) diff --git a/src/utils/hooks.js b/src/utils/hooks.js index 0e57f3a..ada981d 100644 --- a/src/utils/hooks.js +++ b/src/utils/hooks.js @@ -61,7 +61,7 @@ export class HooksUtility { */ static registerItemHooks() { Hooks.on("createItem", (item) => { - ItemUtility.ensureFlagsOnItem(item); + ItemUtility.refreshFlagsOnItem(item); }); Hooks.on("dnd5e.useItem", (item, config, options) => { diff --git a/src/utils/item.js b/src/utils/item.js index dc72f7f..45660e2 100644 --- a/src/utils/item.js +++ b/src/utils/item.js @@ -22,27 +22,38 @@ export class ItemUtility { const isAltRoll = params?.isAltRoll ?? false; let fields = []; + if (ItemUtility.getFlagValueFromItem(item, "quickFlavor", isAltRoll)) { addFieldFlavor(fields, chatData); } + if (ItemUtility.getFlagValueFromItem(item, "quickDesc", isAltRoll)) { addFieldDescription(fields, chatData); } + if (ItemUtility.getFlagValueFromItem(item, "quickSave", isAltRoll)) { addFieldSave(fields, item); } + if (ItemUtility.getFlagValueFromItem(item, "quickAttack", isAltRoll)) { await addFieldAttack(fields, item, params); } + if (ItemUtility.getFlagValueFromItem(item, "quickCheck", isAltRoll)) { await addFieldToolCheck(fields, item, params); - } - if (ItemUtility.getFlagValueFromItem(item, "quickDamage", isAltRoll)) { + } + + params = params ?? {}; + + params.damageFlags = ItemUtility.getFlagValueFromItem(item, "quickDamage", isAltRoll) + if (params.damageFlags) { await addFieldDamage(fields, item, params); } + if (ItemUtility.getFlagValueFromItem(item, "quickOther", isAltRoll)) { await addFieldOtherFormula(fields, item); } + if (ItemUtility.getFlagValueFromItem(item, "quickFooter", isAltRoll)) { addFieldFooter(fields, chatData); } @@ -64,21 +75,22 @@ export class ItemUtility { const config = {} if (item?.hasAreaTarget && item?.flags[`${MODULE_SHORT}`].quickTemplate) { - config["createMeasuredTemplate"] = item.flags[`${MODULE_SHORT}`].quickTemplate[isAltRoll ? "altValue" : "value"]; + config.createMeasuredTemplate = item.flags[`${MODULE_SHORT}`].quickTemplate[isAltRoll ? "altValue" : "value"]; } if (item?.hasQuantity && item?.flags[`${MODULE_SHORT}`].consumeQuantity) { - config["consumeQuantity"] = item.flags[`${MODULE_SHORT}`].consumeQuantity[isAltRoll ? "altValue" : "value"]; + config.consumeQuantity = item.flags[`${MODULE_SHORT}`].consumeQuantity[isAltRoll ? "altValue" : "value"]; } if (item?.hasUses && item?.flags[`${MODULE_SHORT}`].consumeUses) { - config["consumeUsage"] = item.flags[`${MODULE_SHORT}`].consumeUses[isAltRoll ? "altValue" : "value"]; + config.consumeUsage = item.flags[`${MODULE_SHORT}`].consumeUses[isAltRoll ? "altValue" : "value"]; } if (item?.hasResource && item?.flags[`${MODULE_SHORT}`].consumeResource) { - config["consumeResource"] = item.flags[`${MODULE_SHORT}`].consumeResource[isAltRoll ? "altValue" : "value"]; + config.consumeResource = item.flags[`${MODULE_SHORT}`].consumeResource[isAltRoll ? "altValue" : "value"]; } if (item?.hasRecharge && item?.flags[`${MODULE_SHORT}`].consumeRecharge) { - config["consumeRecharge"] = item.flags[`${MODULE_SHORT}`].consumeRecharge[isAltRoll ? "altValue" : "value"]; + config.consumeRecharge = item.flags[`${MODULE_SHORT}`].consumeRecharge[isAltRoll ? "altValue" : "value"]; } + console.log(config); return config; } @@ -90,8 +102,8 @@ export class ItemUtility { return false; } - static ensureFlagsOnItem(item) { - LogUtility.log("Ensuring item flags for module."); + static refreshFlagsOnItem(item) { + LogUtility.log(`Refreshing ${MODULE_SHORT} item flags.`); if (!item || !CONFIG[`${MODULE_SHORT}`].validItemTypes.includes(item.type)) { return; @@ -226,11 +238,15 @@ async function addFieldDamage(fields, item, params) { }); let damageTermGroups = []; - item.system.damage.parts.forEach(part => { - const tmpRoll = new Roll(part[0]); - damageTermGroups.push({ type: part[1], terms: roll.terms.splice(0, tmpRoll.terms.length) }); + for (let i = 0; i < item.system.damage.parts.length; i++) { + const tmpRoll = new Roll(item.system.damage.parts[i][0]); + const partTerms = roll.terms.splice(0, tmpRoll.terms.length); roll.terms.shift(); - }); + + if (params?.damageFlags[i] ?? true) { + damageTermGroups.push({ type: item.system.damage.parts[i][1], terms: partTerms}); + } + } if (roll.terms.length > 0) damageTermGroups[0].terms.push(...roll.terms); diff --git a/src/utils/sheet.js b/src/utils/sheet.js index 079c6f7..92d3bd1 100644 --- a/src/utils/sheet.js +++ b/src/utils/sheet.js @@ -28,7 +28,7 @@ export class SheetUtility { return; } - ItemUtility.ensureFlagsOnItem(item); + ItemUtility.refreshFlagsOnItem(item); let html = protoHtml; if (html[0].localName !== "div") { @@ -63,6 +63,7 @@ async function addItemOptions(item, html) { const properties = { dnd5e: CONFIG.DND5E, altRollEnabled: SettingsUtility.getSettingValue(SETTING_NAMES.ALT_ROLL_ENABLED), + item, flags: item.flags, defLabel: CoreUtility.localize(`${MODULE_SHORT}.sheet.tab.section.defaultRoll`), altLabel: CoreUtility.localize(`${MODULE_SHORT}.sheet.tab.section.alternateRoll`), From 3441b222b9197efb66379cf6ed7a54d6a5907a38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mang=C3=B3?= Date: Mon, 5 Sep 2022 21:09:54 +0200 Subject: [PATCH 5/8] Fix edge case with consume quantity but no use --- src/utils/roll.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/utils/roll.js b/src/utils/roll.js index 5a27347..e1531a0 100644 --- a/src/utils/roll.js +++ b/src/utils/roll.js @@ -72,6 +72,21 @@ export class RollUtility { const advMode = CoreUtility.eventToAdvantage(options?.event); const config = ItemUtility.getRollConfigFromItem(caller, isAltRoll) + // Handle quantity when uses are not consumed + // While the rest can be handled by Item._getUsageUpdates(), this one thing cannot + if (config.consumeQuantity && !config.consumeUsage) { + if (caller.system.quantity === 0) { + ui.notifications.warn(CoreUtility.localize("DND5E.ItemNoUses", {name: caller.name})); + return; + } + + config.consumeQuantity = false; + + const itemUpdates = {}; + itemUpdates["system.quantity"] = Math.max(0, caller.system.quantity - 1); + await caller.update(itemUpdates); + } + return await wrapper.call(caller, config, { configureDialog: caller?.type === ITEM_TYPE.SPELL ? true : false, createMessage: false, From bad11c8f6932fad158fe74b5da5734d1d96cdebc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mang=C3=B3?= Date: Mon, 5 Sep 2022 21:57:47 +0200 Subject: [PATCH 6/8] Fix crit bug with trailing operators --- src/utils/item.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/utils/item.js b/src/utils/item.js index 45660e2..a112188 100644 --- a/src/utils/item.js +++ b/src/utils/item.js @@ -268,6 +268,11 @@ async function addFieldDamage(fields, item, params) { })); } + // Remove trailing operators to avoid errors. + while (critTerms.at(-1) instanceof OperatorTerm) { + critTerms.pop(); + } + critRoll = await Roll.fromTerms(Roll.simplifyTerms(critTerms)).reroll({ maximize: roll.options.powerfulCritical, async: true From 9d578c5ede5323c2c087e0b0e846a604a6fba249 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mang=C3=B3?= Date: Tue, 6 Sep 2022 12:25:46 +0200 Subject: [PATCH 7/8] Add flag check and fix default ammop consumption --- src/utils/hooks.js | 2 +- src/utils/item.js | 64 +++++++++++++++++++++++++++++++--------------- 2 files changed, 45 insertions(+), 21 deletions(-) diff --git a/src/utils/hooks.js b/src/utils/hooks.js index ada981d..56914e6 100644 --- a/src/utils/hooks.js +++ b/src/utils/hooks.js @@ -61,7 +61,7 @@ export class HooksUtility { */ static registerItemHooks() { Hooks.on("createItem", (item) => { - ItemUtility.refreshFlagsOnItem(item); + ItemUtility.ensureFlagsOnitem(item); }); Hooks.on("dnd5e.useItem", (item, config, options) => { diff --git a/src/utils/item.js b/src/utils/item.js index a112188..cbfbb2f 100644 --- a/src/utils/item.js +++ b/src/utils/item.js @@ -18,6 +18,8 @@ export const ITEM_TYPE = { export class ItemUtility { static async getFieldsFromItem(item, params) { + ItemUtility.ensureFlagsOnitem(item); + const chatData = await item.getChatData(); const isAltRoll = params?.isAltRoll ?? false; let fields = []; @@ -70,6 +72,7 @@ export class ItemUtility { } static getRollConfigFromItem(item, isAltRoll = false) { + ItemUtility.ensureFlagsOnitem(item); ItemUtility.ensureConsumePropertiesOnItem(item); const config = {} @@ -89,8 +92,7 @@ export class ItemUtility { if (item?.hasRecharge && item?.flags[`${MODULE_SHORT}`].consumeRecharge) { config.consumeRecharge = item.flags[`${MODULE_SHORT}`].consumeRecharge[isAltRoll ? "altValue" : "value"]; } - - console.log(config); + return config; } @@ -102,6 +104,31 @@ export class ItemUtility { return false; } + static ensureFlagsOnitem(item) { + if (!item || !CONFIG[`${MODULE_SHORT}`].validItemTypes.includes(item.type)) { + return; + } + + if (item.flags && item.flags[`${MODULE_SHORT}`]) { + return; + } + + this.refreshFlagsOnItem(item); + } + + static ensureConsumePropertiesOnItem(item) { + if (item) { + // For items with quantity (weapons, tools, consumables...) + item.hasQuantity = ("quantity" in item.system); + // For items with "Limited Uses" configured + item.hasUses = !!(item.system.uses?.value || item.system.uses?.max || item.system.uses?.per); + // For items with "Resource Consumption" configured + item.hasResource = !!(item.system.consume?.target); + // For abilities with "Action Recharge" configured + item.hasRecharge = !!(item.system.recharge?.value); + } + } + static refreshFlagsOnItem(item) { LogUtility.log(`Refreshing ${MODULE_SHORT} item flags.`); @@ -138,19 +165,6 @@ export class ItemUtility { ItemUtility.ensureConsumePropertiesOnItem(item); } - - static ensureConsumePropertiesOnItem(item) { - if (item) { - // For items with quantity (weapons, tools, consumables...) - item.hasQuantity = ("quantity" in item.system); - // For items with "Limited Uses" configured - item.hasUses = !!(item.system.uses?.value || item.system.uses?.max || item.system.uses?.per); - // For items with "Resource Consumption" configured - item.hasResource = !!(item.system.consume?.target); - // For abilities with "Action Recharge" configured - item.hasRecharge = !!(item.system.recharge?.value); - } - } } function addFieldFlavor(fields, chatData) { @@ -203,6 +217,12 @@ function addFieldSave(fields, item) { async function addFieldAttack(fields, item, params) { if (item.hasAttack) { + let ammoConsumeBypass = false; + if (item.system?.consume?.type === "ammo") { + item.system.consume.type = "rsr5e"; + ammoConsumeBypass = true; + } + const roll = await item.rollAttack({ fastForward: true, chatMessage: false, @@ -210,6 +230,14 @@ async function addFieldAttack(fields, item, params) { disadvantage: params?.advMode < 0 ?? false }); + if (params) { + params.isCrit = params.isCrit || roll.isCritical; + } + + if (ammoConsumeBypass) { + item.system.consume.type = "ammo"; + } + fields.push([ FIELD_TYPE.ATTACK, { @@ -218,10 +246,6 @@ async function addFieldAttack(fields, item, params) { consume: ItemUtility.getConsumeTargetFromItem(item) } ]); - - if (params) { - params.isCrit = params.isCrit || roll.isCritical; - } } } @@ -262,7 +286,7 @@ async function addFieldDamage(fields, item, params) { if (i === 0 && firstDie) { critTerms.splice(index, 1, new Die({ - number: firstDie.number + roll.options.criticalBonusDice ?? 0, + number: firstDie.number + (roll.options.criticalBonusDice ?? 0), faces: firstDie.faces, results: firstDie.results })); From 0c5cd6cd1bbec3255d070a69e8839ce287ce29bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mang=C3=B3?= Date: Tue, 6 Sep 2022 14:32:01 +0200 Subject: [PATCH 8/8] Move function to private and add documentation --- src/utils/item.js | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/utils/item.js b/src/utils/item.js index cbfbb2f..0cb5fd2 100644 --- a/src/utils/item.js +++ b/src/utils/item.js @@ -62,14 +62,6 @@ export class ItemUtility { return fields; } - - static getConsumeTargetFromItem(item) { - if (item.system.consume.type === "ammo") { - return item.actor.items.get(item.system.consume.target); - } - - return undefined; - } static getRollConfigFromItem(item, isAltRoll = false) { ItemUtility.ensureFlagsOnitem(item); @@ -167,6 +159,14 @@ export class ItemUtility { } } +function getConsumeTargetFromItem(item) { + if (item.system.consume.type === "ammo") { + return item.actor.items.get(item.system.consume.target); + } + + return undefined; +} + function addFieldFlavor(fields, chatData) { if (chatData.chatFlavor && chatData.chatFlavor !== "") { fields.push([ @@ -217,6 +217,8 @@ function addFieldSave(fields, item) { async function addFieldAttack(fields, item, params) { if (item.hasAttack) { + // The dnd5e default attack roll automatically consumes ammo without any option for external configuration. + // This code will bypass this consumption since we have already consumed or not consumed via the roll config earlier. let ammoConsumeBypass = false; if (item.system?.consume?.type === "ammo") { item.system.consume.type = "rsr5e"; @@ -234,6 +236,7 @@ async function addFieldAttack(fields, item, params) { params.isCrit = params.isCrit || roll.isCritical; } + // Reset ammo type to avoid issues. if (ammoConsumeBypass) { item.system.consume.type = "ammo"; } @@ -243,7 +246,7 @@ async function addFieldAttack(fields, item, params) { { roll, rollType: ROLL_TYPE.ATTACK, - consume: ItemUtility.getConsumeTargetFromItem(item) + consume: getConsumeTargetFromItem(item) } ]); }