Skip to content

Commit

Permalink
Merge branch 'develop'
Browse files Browse the repository at this point in the history
  • Loading branch information
MangoFVTT committed Sep 6, 2022
2 parents 153f7a8 + 0c5cd6c commit bfc293c
Show file tree
Hide file tree
Showing 5 changed files with 125 additions and 49 deletions.
2 changes: 1 addition & 1 deletion src/utils/hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export class HooksUtility {
*/
static registerItemHooks() {
Hooks.on("createItem", (item) => {
ItemUtility.ensureFlagsOnItem(item);
ItemUtility.ensureFlagsOnitem(item);
});

Hooks.on("dnd5e.useItem", (item, config, options) => {
Expand Down
131 changes: 89 additions & 42 deletions src/utils/item.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,69 +18,73 @@ 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 = [];

console.log(item);

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);
}

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);
ItemUtility.ensureConsumePropertiesOnItem(item);

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"];
}

return config;
}

Expand All @@ -92,8 +96,33 @@ export class ItemUtility {
return false;
}

static ensureFlagsOnItem(item) {
LogUtility.log("Ensuring item flags for module.");
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.`);

if (!item || !CONFIG[`${MODULE_SHORT}`].validItemTypes.includes(item.type)) {
return;
Expand Down Expand Up @@ -128,19 +157,14 @@ 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 getConsumeTargetFromItem(item) {
if (item.system.consume.type === "ammo") {
return item.actor.items.get(item.system.consume.target);
}

return undefined;
}

function addFieldFlavor(fields, chatData) {
Expand Down Expand Up @@ -193,25 +217,38 @@ 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";
ammoConsumeBypass = true;
}

const roll = await item.rollAttack({
fastForward: true,
chatMessage: false,
advantage: params?.advMode > 0 ?? false,
disadvantage: params?.advMode < 0 ?? false
});

if (params) {
params.isCrit = params.isCrit || roll.isCritical;
}

// Reset ammo type to avoid issues.
if (ammoConsumeBypass) {
item.system.consume.type = "ammo";
}

fields.push([
FIELD_TYPE.ATTACK,
{
roll,
rollType: ROLL_TYPE.ATTACK,
consume: ItemUtility.getConsumeTargetFromItem(item)
consume: getConsumeTargetFromItem(item)
}
]);

if (params) {
params.isCrit = params.isCrit || roll.isCritical;
}
}
}

Expand All @@ -228,11 +265,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);

Expand All @@ -244,13 +285,19 @@ 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({
number: firstDie.number + roll.options.criticalBonusDice ?? 0,
critTerms.splice(index, 1, new Die({
number: firstDie.number + (roll.options.criticalBonusDice ?? 0),
faces: firstDie.faces,
results: firstDie.results
});
}));
}

// Remove trailing operators to avoid errors.
while (critTerms.at(-1) instanceof OperatorTerm) {
critTerms.pop();
}

critRoll = await Roll.fromTerms(Roll.simplifyTerms(critTerms)).reroll({
Expand Down
19 changes: 14 additions & 5 deletions src/utils/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 tmpResults = [];
tmpResults.push(d20Rolls.results[i]);

const baseTerm = new Die({number: 1, faces: 20, results: [tmpResult]});
if (roll.options.halflingLucky && d20Rolls.results[i].result === 1) {
i++;
tmpResults.push(d20Rolls.results[i]);
}

tmpResults.forEach(r => {
r.active = !r.rerolled ?? true;
});

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: d20Rolls.results[i].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
Expand Down
19 changes: 19 additions & 0 deletions src/utils/roll.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -287,6 +302,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)) {
Expand Down
3 changes: 2 additions & 1 deletion src/utils/sheet.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export class SheetUtility {
return;
}

ItemUtility.ensureFlagsOnItem(item);
ItemUtility.refreshFlagsOnItem(item);

let html = protoHtml;
if (html[0].localName !== "div") {
Expand Down Expand Up @@ -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`),
Expand Down

0 comments on commit bfc293c

Please sign in to comment.