From e623498b46480c38fb2a0db04a4fb688c1fb5d20 Mon Sep 17 00:00:00 2001 From: Chris Seieroe Date: Sun, 25 Feb 2024 18:00:40 -0800 Subject: [PATCH] Update the system-defined status effects to include advantage/disadvantage flags (#62) * add updateStatusEffects * add hooks to add changes to Exhaustion active effect * add updateStatusEffects setting to control feature * update changelog * fix VSCode warnings about testing empty objects * bit of cleanup --- CHANGELOG.md | 4 + lang/en.json | 4 + module.json | 2 +- src/module.js | 291 +++++++++++++++++++++++++++++++++++++++++++++-- src/reminders.js | 8 +- src/settings.js | 10 ++ src/util.js | 9 ++ 7 files changed, 314 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d5f775..8279488 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# 3.4.0 + +- feature: Update the system-defined status effects to include advantage/disadvantage flags. Controlled by a setting. + # 3.3.2 - bug fix: [#60](https://github.com/kaelad02/adv-reminder/issues/60) Fix tool checks to show advantage and messages again after the check was moved from the item to the actor diff --git a/lang/en.json b/lang/en.json index 7aa9d22..7b19c13 100644 --- a/lang/en.json +++ b/lang/en.json @@ -27,5 +27,9 @@ "Dis": "Disadvantage from {sources}", "Crit": "Critical Hit from {sources}", "Norm": "Normal from {sources}" + }, + "adv-reminder.UpdateStatusEffects": { + "Name": "Update Status Effects", + "Hint": "Update the status effects that the system adds to include the appropriate advantage/disadvantage flags. Not recommended if you are using another module to modify or replace them." } } diff --git a/module.json b/module.json index cc888b3..cd16c14 100644 --- a/module.json +++ b/module.json @@ -14,7 +14,7 @@ "id": "dnd5e", "compatibility": { "minimum": "3.0.0", - "verified": "3.0.0" + "verified": "3.0.3" } }, { diff --git a/src/module.js b/src/module.js index dd9a89b..62c499f 100644 --- a/src/module.js +++ b/src/module.js @@ -39,6 +39,285 @@ function applyMidiCustom(actor, change) { } } +Hooks.once("setup", () => { + if (game.settings.get("adv-reminder", "updateStatusEffects")) { + updateStatusEffects(); + Hooks.on("preCreateActiveEffect", addExhaustionEffects); + Hooks.on("preUpdateActiveEffect", addExhaustionEffects); + } +}); + +function updateStatusEffects() { + debug("updateStatusEffects"); + + const effectChanges = { + blinded: { + changes: [ + { + key: "flags.midi-qol.disadvantage.attack.all", + mode: CONST.ACTIVE_EFFECT_MODES.CUSTOM, + value: "1", + }, + { + key: "flags.midi-qol.grants.advantage.attack.all", + mode: CONST.ACTIVE_EFFECT_MODES.CUSTOM, + value: "1", + }, + ], + }, + dodging: { + flags: { + dae: { + specialDuration: ["turnStart"], + }, + }, + changes: [ + { + key: "flags.midi-qol.grants.disadvantage.attack.all", + mode: CONST.ACTIVE_EFFECT_MODES.CUSTOM, + value: "1", + }, + { + key: "flags.midi-qol.advantage.ability.save.dex", + mode: CONST.ACTIVE_EFFECT_MODES.CUSTOM, + value: "1", + }, + ], + }, + frightened: { + changes: [ + { + key: "flags.midi-qol.disadvantage.attack.all", + mode: CONST.ACTIVE_EFFECT_MODES.CUSTOM, + value: "1", + }, + { + key: "flags.midi-qol.disadvantage.ability.check.all", + mode: CONST.ACTIVE_EFFECT_MODES.CUSTOM, + value: "1", + }, + ], + }, + hidden: { + changes: [ + { + key: "flags.midi-qol.advantage.attack.all", + mode: CONST.ACTIVE_EFFECT_MODES.CUSTOM, + value: "1", + }, + { + key: "flags.midi-qol.grants.disadvantage.attack.all", + mode: CONST.ACTIVE_EFFECT_MODES.CUSTOM, + value: "1", + }, + ], + }, + invisible: { + changes: [ + { + key: "flags.midi-qol.advantage.attack.all", + mode: CONST.ACTIVE_EFFECT_MODES.CUSTOM, + value: "1", + }, + { + key: "flags.midi-qol.grants.disadvantage.attack.all", + mode: CONST.ACTIVE_EFFECT_MODES.CUSTOM, + value: "1", + }, + ], + }, + paralyzed: { + changes: [ + { + key: "flags.midi-qol.fail.ability.save.dex", + mode: CONST.ACTIVE_EFFECT_MODES.CUSTOM, + value: "1", + }, + { + key: "flags.midi-qol.fail.ability.save.str", + mode: CONST.ACTIVE_EFFECT_MODES.CUSTOM, + value: "1", + }, + { + key: "flags.midi-qol.grants.advantage.attack.all", + mode: CONST.ACTIVE_EFFECT_MODES.CUSTOM, + value: "1", + }, + { + key: "flags.midi-qol.grants.critical.range", + mode: CONST.ACTIVE_EFFECT_MODES.OVERRIDE, + value: "5", + }, + ], + }, + petrified: { + changes: [ + { + key: "flags.midi-qol.grants.advantage.attack.all", + mode: CONST.ACTIVE_EFFECT_MODES.CUSTOM, + value: "1", + }, + { + key: "flags.midi-qol.fail.ability.save.dex", + mode: CONST.ACTIVE_EFFECT_MODES.CUSTOM, + value: "1", + }, + { + key: "flags.midi-qol.fail.ability.save.str", + mode: CONST.ACTIVE_EFFECT_MODES.CUSTOM, + value: "1", + }, + ], + }, + poisoned: { + changes: [ + { + key: "flags.midi-qol.disadvantage.attack.all", + mode: CONST.ACTIVE_EFFECT_MODES.CUSTOM, + value: "1", + }, + { + key: "flags.midi-qol.disadvantage.ability.check.all", + mode: CONST.ACTIVE_EFFECT_MODES.CUSTOM, + value: "1", + }, + ], + }, + prone: { + changes: [ + { + key: "flags.midi-qol.grants.advantage.attack.mwak", + mode: CONST.ACTIVE_EFFECT_MODES.CUSTOM, + value: "1", + }, + { + key: "flags.midi-qol.grants.advantage.attack.msak", + mode: CONST.ACTIVE_EFFECT_MODES.CUSTOM, + value: "1", + }, + { + key: "flags.midi-qol.grants.disadvantage.attack.rwak", + mode: CONST.ACTIVE_EFFECT_MODES.CUSTOM, + value: "1", + }, + { + key: "flags.midi-qol.grants.disadvantage.attack.rsak", + mode: CONST.ACTIVE_EFFECT_MODES.CUSTOM, + value: "1", + }, + { + key: "flags.midi-qol.disadvantage.attack.all", + mode: CONST.ACTIVE_EFFECT_MODES.CUSTOM, + value: "1", + }, + ], + }, + restrained: { + changes: [ + { + key: "flags.midi-qol.disadvantage.ability.save.dex", + mode: CONST.ACTIVE_EFFECT_MODES.CUSTOM, + value: "1", + }, + { + key: "flags.midi-qol.disadvantage.attack.all", + mode: CONST.ACTIVE_EFFECT_MODES.CUSTOM, + value: "1", + }, + { + key: "flags.midi-qol.grants.advantage.attack.all", + mode: CONST.ACTIVE_EFFECT_MODES.CUSTOM, + value: "1", + }, + ], + }, + stunned: { + changes: [ + { + key: "flags.midi-qol.fail.ability.save.dex", + mode: CONST.ACTIVE_EFFECT_MODES.CUSTOM, + value: "1", + }, + { + key: "flags.midi-qol.fail.ability.save.str", + mode: CONST.ACTIVE_EFFECT_MODES.CUSTOM, + value: "1", + }, + { + key: "flags.midi-qol.grants.advantage.attack.all", + mode: CONST.ACTIVE_EFFECT_MODES.CUSTOM, + value: "1", + }, + ], + }, + unconscious: { + changes: [ + { + key: "flags.midi-qol.fail.ability.save.dex", + mode: CONST.ACTIVE_EFFECT_MODES.CUSTOM, + value: "1", + }, + { + key: "flags.midi-qol.fail.ability.save.str", + mode: CONST.ACTIVE_EFFECT_MODES.CUSTOM, + value: "1", + }, + { + key: "flags.midi-qol.grants.advantage.attack.all", + mode: CONST.ACTIVE_EFFECT_MODES.CUSTOM, + value: "1", + }, + { + key: "flags.midi-qol.grants.critical.range", + mode: CONST.ACTIVE_EFFECT_MODES.OVERRIDE, + value: "5", + }, + ], + }, + }; + + Object.entries(effectChanges).forEach(([id, data]) => { + const effect = CONFIG.statusEffects.find((e) => e.id === id); + if (effect) foundry.utils.mergeObject(effect, data); + }); +} + +function addExhaustionEffects(effect, updates) { + debug("addExhaustionEffects"); + + if (effect.id !== dnd5e.documents.ActiveEffect5e.ID.EXHAUSTION) return; + const level = foundry.utils.getProperty(updates, "flags.dnd5e.exhaustionLevel"); + if (!level) return; + // build the changes based on exhaustion level + const changes = [ + { + key: "flags.midi-qol.disadvantage.ability.check.all", + mode: CONST.ACTIVE_EFFECT_MODES.CUSTOM, + value: "1", + }, + { + key: "flags.dnd5e.initiativeDisadv", + mode: CONST.ACTIVE_EFFECT_MODES.CUSTOM, + value: "1", + }, + ]; + if (level >= 3) + changes.push( + { + key: "flags.midi-qol.disadvantage.attack.all", + mode: CONST.ACTIVE_EFFECT_MODES.CUSTOM, + value: "1", + }, + { + key: "flags.midi-qol.disadvantage.ability.save.all", + mode: CONST.ACTIVE_EFFECT_MODES.CUSTOM, + value: "1", + } + ); + // add changes to the active effect + effect.updateSource({ changes }); +} + // Add message flags to DAE so it shows them in the AE editor Hooks.once("DAE.setupComplete", () => { debug("adding Advantage Reminder flags to DAE"); @@ -53,8 +332,7 @@ Hooks.once("DAE.setupComplete", () => { fields.push("flags.adv-reminder.message.deathSave"); fields.push("flags.adv-reminder.message.damage.all"); - const actionTypes = - game.system.id === "sw5e" ? ["mwak", "rwak", "mpak", "rpak"] : ["mwak", "rwak", "msak", "rsak"]; + const actionTypes = game.system.id === "sw5e" ? ["mwak", "rwak", "mpak", "rpak"] : ["mwak", "rwak", "msak", "rsak"]; actionTypes.forEach((actionType) => fields.push(`flags.adv-reminder.message.attack.${actionType}`)); Object.keys(CONFIG.DND5E.itemActionTypes).forEach((actionType) => @@ -67,9 +345,7 @@ Hooks.once("DAE.setupComplete", () => { fields.push(`flags.adv-reminder.message.ability.save.${abilityId}`); }); - Object.keys(CONFIG.DND5E.skills).forEach((skillId) => - fields.push(`flags.adv-reminder.message.skill.${skillId}`) - ); + Object.keys(CONFIG.DND5E.skills).forEach((skillId) => fields.push(`flags.adv-reminder.message.skill.${skillId}`)); window.DAE.addAutoFields(fields); }); @@ -132,10 +408,7 @@ async function prepareMessage(dialogOptions) { if (messages.length) { // build message - const message = await renderTemplate( - "modules/adv-reminder/templates/roll-dialog-messages.hbs", - { messages } - ); + const message = await renderTemplate("modules/adv-reminder/templates/roll-dialog-messages.hbs", { messages }); // enrich message, specifically replacing rolls const enriched = await TextEditor.enrichHTML(message, { secrets: true, diff --git a/src/reminders.js b/src/reminders.js index 1608298..ca51e43 100644 --- a/src/reminders.js +++ b/src/reminders.js @@ -1,4 +1,4 @@ -import { debug } from "./util.js"; +import { debug, isEmpty } from "./util.js"; class BaseReminder { constructor(actor) { @@ -61,7 +61,7 @@ export class AttackReminder extends BaseReminder { this._message(); // quick return if there are no flags - if (this.actorFlags === {} && this.targetFlags === {}) return; + if (isEmpty(this.actorFlags) && isEmpty(this.targetFlags)) return; // build the active effect keys applicable for this roll const advKeys = [ @@ -113,7 +113,7 @@ class AbilityBaseReminder extends BaseReminder { this._message(); // quick return if there are no flags - if (this.actorFlags === {}) return; + if (isEmpty(this.actorFlags)) return; // get the active effect keys applicable for this roll const advKeys = this.advantageKeys; @@ -254,7 +254,7 @@ export class CriticalReminder extends BaseReminder { this._message(); // quick return if there are no flags - if (this.actorFlags === {} && this.targetFlags === {}) return; + if (isEmpty(this.actorFlags) && isEmpty(this.targetFlags)) return; // build the active effect keys applicable for this roll const critKeys = ["critical.all", `critical.${this.actionType}`]; diff --git a/src/settings.js b/src/settings.js index 0c13709..69b856c 100644 --- a/src/settings.js +++ b/src/settings.js @@ -54,6 +54,16 @@ Hooks.once("init", () => { default: true, onChange: (value) => (showSources = value), }); + + game.settings.register("adv-reminder", "updateStatusEffects", { + name: "adv-reminder.UpdateStatusEffects.Name", + hint: "adv-reminder.UpdateStatusEffects.Hint", + scope: "world", + config: true, + requiresReload: true, + type: Boolean, + default: false, + }); }); Hooks.once("ready", () => { diff --git a/src/util.js b/src/util.js index 32426ad..bb91e20 100644 --- a/src/util.js +++ b/src/util.js @@ -28,3 +28,12 @@ export function isMinVersion(name, version) { export function getTarget() { return [...game.user.targets][0]?.actor; } + +/** + * Test if an object is empty. + * @param {object} obj object to test + * @returns true if an object is empty, false otherwise + */ +export function isEmpty(obj) { + return !Object.keys(obj).length; +}