diff --git a/CHANGELOG.md b/CHANGELOG.md index acce0a9..f40e273 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,17 +1,22 @@ +# 1.0 + +- feature: [#3](https://github.com/kaelad02/adv-reminder/issues/3) Messages that show up on the roll dialogs +- feature: improve MRE compatibility to skip when it fast-forwards rolls + # 0.4 -* feature: support armor that imposes stealth disadvantage +- feature: [#7](https://github.com/kaelad02/adv-reminder/issues/7) support armor that imposes stealth disadvantage # 0.3 -* bug fix: Active effects from unequipped items weren't being ignored -* documentation: Update readme with module compatibility info +- bug fix: [#2](https://github.com/kaelad02/adv-reminder/issues/2) Active effects from unequipped items weren't being ignored +- documentation: Update readme with module compatibility info # 0.2 -* feature: add Tool checks support -* mark compatible with v9 +- feature: add Tool checks support +- mark compatible with v9 # 0.1 -* Initial release +- Initial release diff --git a/README.md b/README.md index e811073..cddd4b7 100644 --- a/README.md +++ b/README.md @@ -10,13 +10,13 @@ In addition to the active effects, this module supports armor that imposes steal Supports active effects on the following rolls: -* Attack rolls -* Ability checks -* Saving throws, including auto-fail (e.g. Stunned) -* Skill checks -* Tool checks -* Death saves -* Damage rolls +- Attack rolls +- Ability checks +- Saving throws, including auto-fail (e.g. Stunned) +- Skill checks +- Tool checks +- Death saves +- Damage rolls ## Auto-Fail Rolls @@ -26,6 +26,31 @@ There are active effect keys for automatically failing ability checks, saving th If the player holds down one of the Ctrl/Alt/Shift/Meta keys to fast-forward the die roll (e.g. hold Alt to roll with advantage, skipping the roll dialog) then this module WILL NOT do anything. Holding down one of those keys stops the roll dialog from popping up so it's interpreted as overriding what this module does. +## Messages + +In addition to active effects adding advantage or disadvantage, you can also add messages to remind you of conditional bonuses or advantage. For example, features like Dwarven Resilience give advantage on saving throws against poison don't work with the advantage flags since there isn't a way to limit it to poison. Now you can add a message to the dialog right above the buttons to remind you about Dwarven Resilience. + +![Saving Throw screenshot with message](screenshot2.png?raw=true) + +You have control over when the message appears and what it contains, including HTML formatting. In the screenshot above it just reminds you to roll with advantage on saving throws against poison. You are free to change it to just include `Dwarven Resilience` if that's all the reminder you need or `Advantage against poison` that doesn't mention saving throws since it only appears on CON saving throws. + +The active effects keys are listed below and should be set with the change mode of `Custom`. + +- `flags.adv-reminder.message.all` for all rolls +- `flags.adv-reminder.message.attack.all` for all Attack rolls +- `flags.adv-reminder.message.attack.mwak/rwak/msak/rsak` for Attack rolls of a specific action type +- `flags.adv-reminder.message.attack.str/dex/con/int/wis/cha` for Attack rolls using a specific ability +- `flags.adv-reminder.message.ability.all` for all Ability checks, Saving throws, Skill checks, and Death saves +- `flags.adv-reminder.message.ability.check.all` for all Ability checks and Skill checks +- `flags.adv-reminder.message.ability.check.str/dex/con/int/wis/cha` for specific Ability checks and Skill checks +- `flags.adv-reminder.message.ability.save.all` for all Saving throws and Death saves +- `flags.adv-reminder.message.ability.save.str/dex/con/int/wis/cha` for specific Saving throws +- `flags.adv-reminder.message.skill.all` for all Skill checks +- `flags.adv-reminder.message.skill.acr/ath/.../sur` for specific Skill checks +- `flags.adv-reminder.message.deathSave` for Death saves +- `flags.adv-reminder.message.damage.all` for all Damage rolls +- `flags.adv-reminder.message.damage.mwak/rwak/msak/rsak` for Damage rolls of a specific action type + ## Other Modules Notes about other modules. @@ -44,9 +69,11 @@ Notes about other modules. ### Compatibility Notes -[Better Rolls for 5e](https://foundryvtt.com/packages/betterrolls5e) This module works with Better Rolls, making rolls with advantage and disadvantage with some caviats. +[Better Rolls for 5e](https://foundryvtt.com/packages/betterrolls5e) This module works with Better Rolls, making rolls with advantage and disadvantage with the following known issue(s). -* Active effects for critical hits do not work. -* The "d20 Mode" Better Rolls setting of "Single Roll Upgradeable" does not give the hint in the pop-up asking what kind of roll to perform. It will still apply the active effects though possibly leading to some confusion, especially since advantage and disadvantage will not cancel each other out like they should. +- Active effects for critical hits do not work. +- The "d20 Mode" Better Rolls setting of "Single Roll Upgradeable" does not give the hint in the pop-up asking what kind of roll to perform. It will still apply the active effects though possibly leading to some confusion, especially since advantage and disadvantage will not cancel each other out like they should. [Midi QOL](https://foundryvtt.com/packages/midi-qol) This module is compatible with Midi QOL. However, if you've enabled Midi QOL's workflow then it is not necessary to use this module as well since Midi QOL will already do this for you. + +[Minimal Rolling Enhancements for D&D5e](https://foundryvtt.com/packages/mre-dnd5e) This module works with MRE, making rolls with advantage/disadvantage and showing messages if you hold the "Roll Dialog Modifier Key" (an MRE setting). diff --git a/css/adv-reminder.css b/css/adv-reminder.css index 19330fc..96aa2fb 100644 --- a/css/adv-reminder.css +++ b/css/adv-reminder.css @@ -9,3 +9,16 @@ .dialog .dialog-buttons button.default.critical { font-weight: bold; } + +.dialog .dialog-content .adv-reminder-messages { + background: rgba(0, 0, 0, 0.05); + padding: 3px 5px; + margin: 8px 0; + color: #191813; + border: 1px solid #7a7971; + border-radius: 3px; +} + +.adv-reminder-messages > div:not(:last-child) { + margin-bottom: 10px; +} diff --git a/screenshot2.png b/screenshot2.png new file mode 100644 index 0000000..12e7f54 Binary files /dev/null and b/screenshot2.png differ diff --git a/src/messages.js b/src/messages.js new file mode 100644 index 0000000..3773f08 --- /dev/null +++ b/src/messages.js @@ -0,0 +1,159 @@ +import { debug } from "./util.js"; + +class BaseMessage { + constructor(actor) { + /** @type {EffectChangeData[]} */ + this.changes = actor.effects + .filter((effect) => !effect.isSuppressed && !effect.data.disabled) + .flatMap((effect) => effect.data.changes) + .sort((a, b) => a.priority - b.priority); + } + + get messageKeys() { + return ["flags.adv-reminder.message.all"]; + } + + async addMessage(options) { + const keys = this.messageKeys; + const messages = this.changes + .filter((change) => keys.includes(change.key)) + .map((change) => change.value); + + if (messages.length > 0) { + // add id to dialogOptions to put the message on the correct roll dialog + const messageId = randomID(); + options.dialogOptions = options.dialogOptions || {}; + options.dialogOptions.id = messageId; + // build message + const message = await renderTemplate( + "modules/adv-reminder/templates/roll-dialog-messages.hbs", + { messages } + ); + debug("adding hook to renderDialog w/ ", message); + const hookId = Hooks.on("renderDialog", (dialog, html, data) => { + debug("called on hook for renderDialog"); + if (dialog.options.id !== messageId) return; + // add message at the end + const formGroups = html.find(".form-group:last"); + formGroups.after(message); + // reset dialog height + const position = dialog.position; + position.height = "auto"; + dialog.setPosition(position); + }); + setTimeout(() => { + Hooks.off("renderDialog", hookId); + }, 10_000); + } + + return messages; + } +} + +export class AttackMessage extends BaseMessage { + constructor(actor, item) { + super(actor); + + /** @type {string} */ + this.actionType = item.data.data.actionType; + /** @type {string} */ + this.abilityId = item.abilityMod; + } + + /** @override */ + get messageKeys() { + return super.messageKeys.concat( + "flags.adv-reminder.message.attack.all", + `flags.adv-reminder.message.attack.${this.actionType}`, + `flags.adv-reminder.message.attack.${this.abilityId}` + ); + } +} + +class AbilityBaseMessage extends BaseMessage { + constructor(actor, abilityId) { + super(actor); + + /** @type {string} */ + this.abilityId = abilityId; + } + + /** @override */ + get messageKeys() { + return super.messageKeys.concat("flags.adv-reminder.message.ability.all"); + } +} + +export class AbilityCheckMessage extends AbilityBaseMessage { + /** @override */ + get messageKeys() { + return super.messageKeys.concat( + "flags.adv-reminder.message.ability.check.all", + `flags.adv-reminder.message.ability.check.${this.abilityId}` + ); + } +} + +export class AbilitySaveMessage extends AbilityBaseMessage { + /** @override */ + get messageKeys() { + return super.messageKeys.concat( + "flags.adv-reminder.message.ability.save.all", + `flags.adv-reminder.message.ability.save.${this.abilityId}` + ); + } +} + +export class SkillMessage extends AbilityCheckMessage { + constructor(actor, skillId) { + super(actor, actor.data.data.skills[skillId].ability); + + /** @type {string} */ + this.skillId = skillId; + } + + /** @override */ + get messageKeys() { + return super.messageKeys.concat( + "flags.adv-reminder.message.skill.all", + `flags.adv-reminder.message.skill.${this.skillId}` + ); + } +} + +export class DeathSaveMessage extends AbilityBaseMessage { + constructor(actor) { + super(actor, null); + } + + /** @override */ + get messageKeys() { + return super.messageKeys.concat( + "flags.adv-reminder.message.ability.save.all", + "flags.adv-reminder.message.deathSave" + ); + } +} + +export class DamageMessage extends BaseMessage { + constructor(actor, item) { + super(actor); + + /** @type {string} */ + this.actionType = item.data.data.actionType; + } + + /** @override */ + get messageKeys() { + return super.messageKeys.concat( + "flags.adv-reminder.message.damage.all", + `flags.adv-reminder.message.damage.${this.actionType}` + ); + } + + async addMessage(options) { + // Damage options has a nested options variable, add that and pass it to super + options.options = options.options || {}; + return super.addMessage(options.options); + } +} diff --git a/src/module.js b/src/module.js index 77bcb1d..f0622a0 100644 --- a/src/module.js +++ b/src/module.js @@ -1,4 +1,12 @@ import { AbilitySaveFail } from "./fails.js"; +import { + AbilityCheckMessage, + AbilitySaveMessage, + AttackMessage, + DamageMessage, + DeathSaveMessage, + SkillMessage, +} from "./messages.js"; import { AttackReminder, AbilityCheckReminder, @@ -72,7 +80,7 @@ Hooks.once("init", () => { ); }); -function onRollAttack(wrapped, options) { +async function onRollAttack(wrapped, options) { debug("onRollAttack method called"); // check for adv/dis flags unless the user pressed a fast-forward key @@ -80,6 +88,8 @@ function onRollAttack(wrapped, options) { if (isFF) { debug("held down a fast-foward key, skip checking for adv/dis"); } else { + debug("checking for message effects on this attack roll"); + await new AttackMessage(this.actor, this).addMessage(options); debug("checking for adv/dis effects on this attack roll"); const reminder = new AttackReminder(this.actor, getTarget(), this); reminder.updateOptions(options); @@ -88,7 +98,7 @@ function onRollAttack(wrapped, options) { return wrapped(options); } -function onRollAbilitySave(wrapped, abilityId, options) { +async function onRollAbilitySave(wrapped, abilityId, options) { debug("onRollAbilitySave method called"); // check if an effect says to fail this roll @@ -102,6 +112,8 @@ function onRollAbilitySave(wrapped, abilityId, options) { if (isFF) { debug("held down a fast-foward key, skip checking for adv/dis"); } else { + debug("checking for message effects on this saving throw"); + await new AbilitySaveMessage(this, abilityId).addMessage(options); debug("checking for adv/dis effects on this saving throw"); const reminder = new AbilitySaveReminder(this, abilityId); reminder.updateOptions(options); @@ -110,7 +122,7 @@ function onRollAbilitySave(wrapped, abilityId, options) { return wrapped(abilityId, options); } -function onRollAbilityTest(wrapped, abilityId, options) { +async function onRollAbilityTest(wrapped, abilityId, options) { debug("onRollAbilityTest method called"); // check for adv/dis flags unless the user pressed a fast-forward key @@ -118,6 +130,8 @@ function onRollAbilityTest(wrapped, abilityId, options) { if (isFF) { debug("held down a fast-foward key, skip checking for adv/dis"); } else { + debug("checking for message effects on this ability check"); + await new AbilityCheckMessage(this, abilityId).addMessage(options); debug("checking for adv/dis effects on this ability check"); const reminder = new AbilityCheckReminder(this, abilityId); reminder.updateOptions(options); @@ -126,7 +140,7 @@ function onRollAbilityTest(wrapped, abilityId, options) { return wrapped(abilityId, options); } -function onRollSkill(wrapped, skillId, options) { +async function onRollSkill(wrapped, skillId, options) { debug("onRollSkill method called"); // check for adv/dis flags unless the user pressed a fast-forward key @@ -134,6 +148,8 @@ function onRollSkill(wrapped, skillId, options) { if (isFF) { debug("held down a fast-foward key, skip checking for adv/dis"); } else { + debug("checking for message effects on this skill check"); + await new SkillMessage(this, skillId).addMessage(options); debug("checking for adv/dis effects on this skill check"); const reminder = new SkillReminder(this, skillId); reminder.updateOptions(options); @@ -142,7 +158,7 @@ function onRollSkill(wrapped, skillId, options) { return wrapped(skillId, options); } -function onRollToolCheck(wrapped, options) { +async function onRollToolCheck(wrapped, options) { debug("onRollToolCheck method called"); // check for adv/dis flags unless the user pressed a fast-forward key @@ -150,6 +166,11 @@ function onRollToolCheck(wrapped, options) { if (isFF) { debug("held down a fast-foward key, skip checking for adv/dis"); } else { + debug("checking for message effects on this tool check"); + await new AbilityCheckMessage( + this.actor, + this.data.data.ability + ).addMessage(options); debug("checking for adv/dis effects on this tool check"); const reminder = new AbilityCheckReminder( this.actor, @@ -161,7 +182,7 @@ function onRollToolCheck(wrapped, options) { return wrapped(options); } -function onRollDeathSave(wrapped, options) { +async function onRollDeathSave(wrapped, options) { debug("onRollDeathSave method called"); // check for adv/dis flags unless the user pressed a fast-forward key @@ -169,6 +190,8 @@ function onRollDeathSave(wrapped, options) { if (isFF) { debug("held down a fast-foward key, skip checking for adv/dis"); } else { + debug("checking for message effects on this death save"); + await new DeathSaveMessage(this).addMessage(options); debug("checking for adv/dis effects on this death save"); const reminder = new DeathSaveReminder(this); reminder.updateOptions(options); @@ -177,14 +200,16 @@ function onRollDeathSave(wrapped, options) { return wrapped(options); } -function onRollDamage(wrapped, options) { +async function onRollDamage(wrapped, options) { debug("onRollDamage method called"); // check for critical flags unless the user pressed a fast-forward key - const isFF = isFastForwarding(options); + const isFF = isFastForwardingDamage(options.event); if (isFF) { debug("held down a fast-foward key, skip checking for adv/dis"); } else { + debug("checking for message effects on this damage roll"); + await new DamageMessage(this.actor, this).addMessage(options); debug("checking for critical/normal effects on this damage roll"); const reminder = new CriticalReminder(this.actor, getTarget(), this); reminder.updateOptions(options); @@ -195,11 +220,28 @@ function onRollDamage(wrapped, options) { /** * Check if the user is holding down a fast-forward key. - * @param {Event} event the event + * @param {object} options the options * @returns {Boolean} true if they are fast-forwarding, false otherwise */ function isFastForwarding(options) { - return !!(options.fastForward || options.event?.shiftKey || options.event?.altKey || options.event?.ctrlKey || options.event?.metaKey); + return !!( + options.fastForward || + options.event?.shiftKey || + options.event?.altKey || + options.event?.ctrlKey || + options.event?.metaKey + ); +} + +/** + * Check if the user is holding down a fast-forward key for a damage roll. + * @param {Event} event the event + * @returns {Boolean} true if they are fast-forwarding, false otherwise + */ +function isFastForwardingDamage(event) { + // special handling for MRE and damage rolls, always process since it will run after this module + if (game.modules.get("mre-dnd5e")?.active) return false; + return event?.shiftKey || event?.altKey || event?.ctrlKey || event?.metaKey; } /** diff --git a/templates/roll-dialog-messages.hbs b/templates/roll-dialog-messages.hbs new file mode 100644 index 0000000..714af89 --- /dev/null +++ b/templates/roll-dialog-messages.hbs @@ -0,0 +1,5 @@ +
diff --git a/test/messages.test.js b/test/messages.test.js new file mode 100644 index 0000000..4c4c9f2 --- /dev/null +++ b/test/messages.test.js @@ -0,0 +1,911 @@ +import { beforeEach, describe, expect, jest, test } from "@jest/globals"; +import { + AbilityCheckMessage, + AbilitySaveMessage, + AttackMessage, + DamageMessage, + DeathSaveMessage, + SkillMessage, +} from "../src/messages"; + +// fakes +globalThis.renderTemplate = () => {}; +globalThis.randomID = () => ""; +globalThis.Hooks = {}; +globalThis.Hooks.on = () => ""; +globalThis.Hooks.off = () => {}; + +function createActorWithEffects(...keyValuePairs) { + const effects = keyValuePairs.map(createEffect); + return { + data: { + data: { + skills: { + acr: { + ability: "dex", + }, + ani: { + ability: "wis", + }, + arc: { + ability: "int", + }, + ath: { + ability: "str", + }, + dec: { + ability: "cha", + }, + his: { + ability: "int", + }, + ins: { + ability: "wis", + }, + itm: { + ability: "cha", + }, + inv: { + ability: "int", + }, + med: { + ability: "wis", + }, + nat: { + ability: "int", + }, + prc: { + ability: "wis", + }, + prf: { + ability: "cha", + }, + per: { + ability: "cha", + }, + rel: { + ability: "int", + }, + slt: { + ability: "dex", + }, + ste: { + ability: "dex", + }, + sur: { + ability: "wis", + }, + }, + }, + }, + effects, + }; +} + +function createEffect([key, value]) { + return { + isSuppressed: false, + data: { + changes: [ + { + key, + value, + mode: 0, + priority: "0", + }, + ], + disabled: false, + }, + }; +} + +function createItem(actionType, abilityMod) { + return { + abilityMod, + data: { + data: { + actionType, + }, + }, + }; +} + +describe("AttackMessage no legit active effects", () => { + test("attack with no active effects should not add a message", async () => { + const actor = createActorWithEffects(); + const item = createItem("mwak", "str"); + const options = {}; + + const messages = await new AttackMessage(actor, item).addMessage(options); + + expect(messages).toStrictEqual([]); + expect(options.dialogOptions).toBeUndefined(); + }); + + test("attack with a suppressed active effect should not add a message", async () => { + const actor = createActorWithEffects([ + "flags.adv-reminder.message.all", + "some message", + ]); + actor.effects[0].isSuppressed = true; + const item = createItem("mwak", "str"); + const options = {}; + + const messages = await new AttackMessage(actor, item).addMessage(options); + + expect(messages).toStrictEqual([]); + expect(options.dialogOptions).toBeUndefined(); + }); + + test("attack with a disabled active effect should not add a message", async () => { + const actor = createActorWithEffects([ + "flags.adv-reminder.message.all", + "some message", + ]); + actor.effects[0].data.disabled = true; + const item = createItem("mwak", "str"); + const options = {}; + + const messages = await new AttackMessage(actor, item).addMessage(options); + + expect(messages).toStrictEqual([]); + expect(options.dialogOptions).toBeUndefined(); + }); +}); + +describe("AttackMessage message flags", () => { + test("attack with message.all flag should add the message", async () => { + const actor = createActorWithEffects([ + "flags.adv-reminder.message.all", + "message.all message", + ]); + const item = createItem("mwak", "str"); + const options = {}; + + const messages = await new AttackMessage(actor, item).addMessage(options); + + expect(messages).toStrictEqual(["message.all message"]); + expect(options.dialogOptions?.id).toBe(""); + }); + + test("attack with message.attack.all flag should add the message", async () => { + const actor = createActorWithEffects([ + "flags.adv-reminder.message.attack.all", + "message.attack.all message", + ]); + const item = createItem("mwak", "str"); + const options = {}; + + const messages = await new AttackMessage(actor, item).addMessage(options); + + expect(messages).toStrictEqual(["message.attack.all message"]); + expect(options.dialogOptions?.id).toBe(""); + }); + + test("attack with message.attack.mwak flag should add the message for Melee Weapon Attack", async () => { + const actor = createActorWithEffects([ + "flags.adv-reminder.message.attack.mwak", + "message.attack.mwak message", + ]); + const item = createItem("mwak", "str"); + const options = {}; + + const messages = await new AttackMessage(actor, item).addMessage(options); + + expect(messages).toStrictEqual(["message.attack.mwak message"]); + expect(options.dialogOptions?.id).toBe(""); + }); + + test("attack with message.attack.mwak flag should not add the message for Ranged Weapon Attack", async () => { + const actor = createActorWithEffects([ + "flags.adv-reminder.message.attack.mwak", + "message.attack.mwak message", + ]); + const item = createItem("rwak", "dex"); + const options = {}; + + const messages = await new AttackMessage(actor, item).addMessage(options); + + expect(messages).toStrictEqual([]); + expect(options.dialogOptions).toBeUndefined(); + }); + + test("attack with message.attack.cha flag should add the message for Charisma Attack", async () => { + const actor = createActorWithEffects([ + "flags.adv-reminder.message.attack.cha", + "message.attack.cha message", + ]); + const item = createItem("rsak", "cha"); + const options = {}; + + const messages = await new AttackMessage(actor, item).addMessage(options); + + expect(messages).toStrictEqual(["message.attack.cha message"]); + expect(options.dialogOptions?.id).toBe(""); + }); + + test("attack with message.attack.cha flag should not add the message for Intelligence Attack", async () => { + const actor = createActorWithEffects([ + "flags.adv-reminder.message.attack.cha", + "message.attack.cha", + ]); + const item = createItem("rsak", "int"); + const options = {}; + + const messages = await new AttackMessage(actor, item).addMessage(options); + + expect(messages).toStrictEqual([]); + expect(options.dialogOptions).toBeUndefined(); + }); + + test("attack with two messages should add both messages", async () => { + const actor = createActorWithEffects( + ["flags.adv-reminder.message.attack.all", "first"], + ["flags.adv-reminder.message.attack.mwak", "second"] + ); + const item = createItem("mwak", "str"); + const options = {}; + + const messages = await new AttackMessage(actor, item).addMessage(options); + + expect(messages).toStrictEqual(["first", "second"]); + expect(options.dialogOptions?.id).toBe(""); + }); +}); + +describe("AbilityCheckMessage no legit active effects", () => { + test("ability check with no active effects should not add a message", async () => { + const actor = createActorWithEffects(); + const options = {}; + + const messages = await new AbilityCheckMessage(actor, "int").addMessage( + options + ); + + expect(messages).toStrictEqual([]); + expect(options.dialogOptions).toBeUndefined(); + }); + + test("ability check with a suppressed active effect should not add a message", async () => { + const actor = createActorWithEffects([ + "flags.adv-reminder.message.all", + "some message", + ]); + actor.effects[0].isSuppressed = true; + const options = {}; + + const messages = await new AbilityCheckMessage(actor, "int").addMessage( + options + ); + + expect(messages).toStrictEqual([]); + expect(options.dialogOptions).toBeUndefined(); + }); + + test("ability check with a disabled active effect should not add a message", async () => { + const actor = createActorWithEffects([ + "flags.adv-reminder.message.all", + "some message", + ]); + actor.effects[0].data.disabled = true; + const options = {}; + + const messages = await new AbilityCheckMessage(actor, "int").addMessage( + options + ); + + expect(messages).toStrictEqual([]); + expect(options.dialogOptions).toBeUndefined(); + }); +}); + +describe("AbilityCheckMessage message flags", () => { + test("ability check with message.all flag should add the message", async () => { + const actor = createActorWithEffects([ + "flags.adv-reminder.message.all", + "message.all message", + ]); + const options = {}; + + const messages = await new AbilityCheckMessage(actor, "int").addMessage( + options + ); + + expect(messages).toStrictEqual(["message.all message"]); + expect(options.dialogOptions?.id).toBe(""); + }); + + test("ability check with message.ability.all flag should add the message", async () => { + const actor = createActorWithEffects([ + "flags.adv-reminder.message.ability.all", + "message.ability.all message", + ]); + const options = {}; + + const messages = await new AbilityCheckMessage(actor, "int").addMessage( + options + ); + + expect(messages).toStrictEqual(["message.ability.all message"]); + expect(options.dialogOptions?.id).toBe(""); + }); + + test("ability check with message.ability.check.all flag should add the message", async () => { + const actor = createActorWithEffects([ + "flags.adv-reminder.message.ability.check.all", + "message.ability.check.all message", + ]); + const options = {}; + + const messages = await new AbilityCheckMessage(actor, "int").addMessage( + options + ); + + expect(messages).toStrictEqual(["message.ability.check.all message"]); + expect(options.dialogOptions?.id).toBe(""); + }); + + test("ability check with message.ability.check.int flag should add the message for Intelligence Check", async () => { + const actor = createActorWithEffects([ + "flags.adv-reminder.message.ability.check.int", + "message.ability.check.int message", + ]); + const options = {}; + + const messages = await new AbilityCheckMessage(actor, "int").addMessage( + options + ); + + expect(messages).toStrictEqual(["message.ability.check.int message"]); + expect(options.dialogOptions?.id).toBe(""); + }); + + test("ability check with message.ability.check.int flag should not add the message for Dexterity Check", async () => { + const actor = createActorWithEffects([ + "flags.adv-reminder.message.ability.check.int", + "message.ability.check.int", + ]); + const options = {}; + + const messages = await new AbilityCheckMessage(actor, "dex").addMessage( + options + ); + + expect(messages).toStrictEqual([]); + expect(options.dialogOptions).toBeUndefined(); + }); + + test("ability check with two messages should add both messages", async () => { + const actor = createActorWithEffects( + ["flags.adv-reminder.message.ability.check.all", "first"], + ["flags.adv-reminder.message.ability.check.dex", "second"] + ); + const options = {}; + + const messages = await new AbilityCheckMessage(actor, "dex").addMessage( + options + ); + + expect(messages).toStrictEqual(["first", "second"]); + expect(options.dialogOptions?.id).toBe(""); + }); +}); + +describe("AbilitySaveMessage no legit active effects", () => { + test("saving throw with no active effects should not add a message", async () => { + const actor = createActorWithEffects(); + const options = {}; + + const messages = await new AbilitySaveMessage(actor, "int").addMessage( + options + ); + + expect(messages).toStrictEqual([]); + expect(options.dialogOptions).toBeUndefined(); + }); + + test("saving throw with a suppressed active effect should not add a message", async () => { + const actor = createActorWithEffects([ + "flags.adv-reminder.message.all", + "some message", + ]); + actor.effects[0].isSuppressed = true; + const options = {}; + + const messages = await new AbilitySaveMessage(actor, "int").addMessage( + options + ); + + expect(messages).toStrictEqual([]); + expect(options.dialogOptions).toBeUndefined(); + }); + + test("saving throw with a disabled active effect should not add a message", async () => { + const actor = createActorWithEffects([ + "flags.adv-reminder.message.all", + "some message", + ]); + actor.effects[0].data.disabled = true; + const options = {}; + + const messages = await new AbilitySaveMessage(actor, "int").addMessage( + options + ); + + expect(messages).toStrictEqual([]); + expect(options.dialogOptions).toBeUndefined(); + }); +}); + +describe("AbilitySaveMessage message flags", () => { + test("saving throw with message.all flag should add the message", async () => { + const actor = createActorWithEffects([ + "flags.adv-reminder.message.all", + "message.all message", + ]); + const options = {}; + + const messages = await new AbilitySaveMessage(actor, "int").addMessage( + options + ); + + expect(messages).toStrictEqual(["message.all message"]); + expect(options.dialogOptions?.id).toBe(""); + }); + + test("saving throw with message.ability.all flag should add the message", async () => { + const actor = createActorWithEffects([ + "flags.adv-reminder.message.ability.all", + "message.ability.all message", + ]); + const options = {}; + + const messages = await new AbilitySaveMessage(actor, "int").addMessage( + options + ); + + expect(messages).toStrictEqual(["message.ability.all message"]); + expect(options.dialogOptions?.id).toBe(""); + }); + + test("saving throw with message.ability.save.all flag should add the message", async () => { + const actor = createActorWithEffects([ + "flags.adv-reminder.message.ability.save.all", + "message.ability.save.all message", + ]); + const options = {}; + + const messages = await new AbilitySaveMessage(actor, "int").addMessage( + options + ); + + expect(messages).toStrictEqual(["message.ability.save.all message"]); + expect(options.dialogOptions?.id).toBe(""); + }); + + test("saving throw with message.ability.save.int flag should add the message for Intelligence Check", async () => { + const actor = createActorWithEffects([ + "flags.adv-reminder.message.ability.save.int", + "message.ability.save.int message", + ]); + const options = {}; + + const messages = await new AbilitySaveMessage(actor, "int").addMessage( + options + ); + + expect(messages).toStrictEqual(["message.ability.save.int message"]); + expect(options.dialogOptions?.id).toBe(""); + }); + + test("saving throw with message.ability.save.int flag should not add the message for Dexterity Check", async () => { + const actor = createActorWithEffects([ + "flags.adv-reminder.message.ability.save.int", + "message.ability.save.int", + ]); + const options = {}; + + const messages = await new AbilitySaveMessage(actor, "dex").addMessage( + options + ); + + expect(messages).toStrictEqual([]); + expect(options.dialogOptions).toBeUndefined(); + }); + + test("saving throw with two messages should add both messages", async () => { + const actor = createActorWithEffects( + ["flags.adv-reminder.message.ability.save.all", "first"], + ["flags.adv-reminder.message.ability.save.dex", "second"] + ); + const options = {}; + + const messages = await new AbilitySaveMessage(actor, "dex").addMessage( + options + ); + + expect(messages).toStrictEqual(["first", "second"]); + expect(options.dialogOptions?.id).toBe(""); + }); +}); + +describe("SkillMessage no legit active effects", () => { + test("skill check with no active effects should not add a message", async () => { + const actor = createActorWithEffects(); + const options = {}; + + const messages = await new SkillMessage(actor, "ath").addMessage(options); + + expect(messages).toStrictEqual([]); + expect(options.dialogOptions).toBeUndefined(); + }); + + test("skill check with a suppressed active effect should not add a message", async () => { + const actor = createActorWithEffects([ + "flags.adv-reminder.message.all", + "some message", + ]); + actor.effects[0].isSuppressed = true; + const options = {}; + + const messages = await new SkillMessage(actor, "ath").addMessage(options); + + expect(messages).toStrictEqual([]); + expect(options.dialogOptions).toBeUndefined(); + }); + + test("skill check with a disabled active effect should not add a message", async () => { + const actor = createActorWithEffects([ + "flags.adv-reminder.message.all", + "some message", + ]); + actor.effects[0].data.disabled = true; + const options = {}; + + const messages = await new SkillMessage(actor, "ath").addMessage(options); + + expect(messages).toStrictEqual([]); + expect(options.dialogOptions).toBeUndefined(); + }); +}); + +describe("SkillMessage message flags", () => { + test("skill check with message.all flag should add the message", async () => { + const actor = createActorWithEffects([ + "flags.adv-reminder.message.all", + "message.all message", + ]); + const options = {}; + + const messages = await new SkillMessage(actor, "prc").addMessage(options); + + expect(messages).toStrictEqual(["message.all message"]); + expect(options.dialogOptions?.id).toBe(""); + }); + + test("skill check with message.ability.all flag should add the message", async () => { + const actor = createActorWithEffects([ + "flags.adv-reminder.message.ability.all", + "message.ability.all message", + ]); + const options = {}; + + const messages = await new SkillMessage(actor, "prc").addMessage(options); + + expect(messages).toStrictEqual(["message.ability.all message"]); + expect(options.dialogOptions?.id).toBe(""); + }); + + test("skill check with message.ability.check.all flag should add the message", async () => { + const actor = createActorWithEffects([ + "flags.adv-reminder.message.ability.check.all", + "message.ability.check.all message", + ]); + const options = {}; + + const messages = await new SkillMessage(actor, "prc").addMessage(options); + + expect(messages).toStrictEqual(["message.ability.check.all message"]); + expect(options.dialogOptions?.id).toBe(""); + }); + + test("skill check with message.ability.check.wis flag should add the message for Perception Check", async () => { + const actor = createActorWithEffects([ + "flags.adv-reminder.message.ability.check.wis", + "message.ability.check.int message", + ]); + const options = {}; + + const messages = await new SkillMessage(actor, "prc").addMessage(options); + + expect(messages).toStrictEqual(["message.ability.check.int message"]); + expect(options.dialogOptions?.id).toBe(""); + }); + + test("skill check with message.ability.check.wis flag should not add the message for Acrobatics Check", async () => { + const actor = createActorWithEffects([ + "flags.adv-reminder.message.ability.check.wis", + "message.ability.check.int", + ]); + const options = {}; + + const messages = await new SkillMessage(actor, "acr").addMessage(options); + + expect(messages).toStrictEqual([]); + expect(options.dialogOptions).toBeUndefined(); + }); + + test("skill check with message.skill.all flag should add the message", async () => { + const actor = createActorWithEffects([ + "flags.adv-reminder.message.skill.all", + "message.skill.all message", + ]); + const options = {}; + + const messages = await new SkillMessage(actor, "prc").addMessage(options); + + expect(messages).toStrictEqual(["message.skill.all message"]); + expect(options.dialogOptions?.id).toBe(""); + }); + + test("skill check with message.skill.prc flag should add the message for Perception Check", async () => { + const actor = createActorWithEffects([ + "flags.adv-reminder.message.skill.prc", + "message.skill.prc message", + ]); + const options = {}; + + const messages = await new SkillMessage(actor, "prc").addMessage(options); + + expect(messages).toStrictEqual(["message.skill.prc message"]); + expect(options.dialogOptions?.id).toBe(""); + }); + + test("skill check with message.skill.prc flag should not add the message for Nature Check", async () => { + const actor = createActorWithEffects([ + "flags.adv-reminder.message.skill.prc", + "message.skill.prc message", + ]); + const options = {}; + + const messages = await new SkillMessage(actor, "nat").addMessage(options); + + expect(messages).toStrictEqual([]); + expect(options.dialogOptions).toBeUndefined(); + }); + + test("skill check with two messages should add both messages", async () => { + const actor = createActorWithEffects( + ["flags.adv-reminder.message.ability.check.dex", "first"], + ["flags.adv-reminder.message.skill.ste", "second"] + ); + const options = {}; + + const messages = await new SkillMessage(actor, "ste").addMessage(options); + + expect(messages).toStrictEqual(["first", "second"]); + expect(options.dialogOptions?.id).toBe(""); + }); +}); + +describe("DeathSaveMessage no legit active effects", () => { + test("death save with no active effects should not add a message", async () => { + const actor = createActorWithEffects(); + const options = {}; + + const messages = await new DeathSaveMessage(actor).addMessage(options); + + expect(messages).toStrictEqual([]); + expect(options.dialogOptions).toBeUndefined(); + }); + + test("death save with a suppressed active effect should not add a message", async () => { + const actor = createActorWithEffects([ + "flags.adv-reminder.message.all", + "some message", + ]); + actor.effects[0].isSuppressed = true; + const options = {}; + + const messages = await new DeathSaveMessage(actor).addMessage(options); + + expect(messages).toStrictEqual([]); + expect(options.dialogOptions).toBeUndefined(); + }); + + test("death save with a disabled active effect should not add a message", async () => { + const actor = createActorWithEffects([ + "flags.adv-reminder.message.all", + "some message", + ]); + actor.effects[0].data.disabled = true; + const options = {}; + + const messages = await new DeathSaveMessage(actor).addMessage(options); + + expect(messages).toStrictEqual([]); + expect(options.dialogOptions).toBeUndefined(); + }); +}); + +describe("DeathSaveMessage message flags", () => { + test("death save with message.all flag should add the message", async () => { + const actor = createActorWithEffects([ + "flags.adv-reminder.message.all", + "message.all message", + ]); + const options = {}; + + const messages = await new DeathSaveMessage(actor).addMessage(options); + + expect(messages).toStrictEqual(["message.all message"]); + expect(options.dialogOptions?.id).toBe(""); + }); + + test("death save with message.ability.all flag should add the message", async () => { + const actor = createActorWithEffects([ + "flags.adv-reminder.message.ability.all", + "message.ability.all message", + ]); + const options = {}; + + const messages = await new DeathSaveMessage(actor).addMessage(options); + + expect(messages).toStrictEqual(["message.ability.all message"]); + expect(options.dialogOptions?.id).toBe(""); + }); + + test("death save with message.ability.save.all flag should add the message", async () => { + const actor = createActorWithEffects([ + "flags.adv-reminder.message.ability.save.all", + "message.ability.save.all message", + ]); + const options = {}; + + const messages = await new DeathSaveMessage(actor).addMessage(options); + + expect(messages).toStrictEqual(["message.ability.save.all message"]); + expect(options.dialogOptions?.id).toBe(""); + }); + + test("death save with message.deathSave flag should add the message", async () => { + const actor = createActorWithEffects([ + "flags.adv-reminder.message.deathSave", + "message.deathSave message", + ]); + const options = {}; + + const messages = await new DeathSaveMessage(actor).addMessage(options); + + expect(messages).toStrictEqual(["message.deathSave message"]); + expect(options.dialogOptions?.id).toBe(""); + }); + + test("death save with two messages should add both messages", async () => { + const actor = createActorWithEffects( + ["flags.adv-reminder.message.ability.save.all", "first"], + ["flags.adv-reminder.message.deathSave", "second"] + ); + const options = {}; + + const messages = await new DeathSaveMessage(actor).addMessage(options); + + expect(messages).toStrictEqual(["first", "second"]); + expect(options.dialogOptions?.id).toBe(""); + }); +}); + +describe("DamageMessage no legit active effects", () => { + test("damage with no active effects should not add a message", async () => { + const actor = createActorWithEffects(); + const item = createItem("mwak", "str"); + const options = {}; + + const messages = await new DamageMessage(actor, item).addMessage(options); + + expect(messages).toStrictEqual([]); + expect(options.options?.dialogOptions).toBeUndefined(); + }); + + test("damage with a suppressed active effect should not add a message", async () => { + const actor = createActorWithEffects([ + "flags.adv-reminder.message.all", + "some message", + ]); + actor.effects[0].isSuppressed = true; + const item = createItem("mwak", "str"); + const options = {}; + + const messages = await new DamageMessage(actor, item).addMessage(options); + + expect(messages).toStrictEqual([]); + expect(options.options?.dialogOptions).toBeUndefined(); + }); + + test("damage with a disabled active effect should not add a message", async () => { + const actor = createActorWithEffects([ + "flags.adv-reminder.message.all", + "some message", + ]); + actor.effects[0].data.disabled = true; + const item = createItem("mwak", "str"); + const options = {}; + + const messages = await new DamageMessage(actor, item).addMessage(options); + + expect(messages).toStrictEqual([]); + expect(options.options?.dialogOptions).toBeUndefined(); + }); +}); + +describe("DamageMessage message flags", () => { + test("damage with message.all flag should add the message", async () => { + const actor = createActorWithEffects([ + "flags.adv-reminder.message.all", + "message.all message", + ]); + const item = createItem("mwak", "str"); + const options = {}; + + const messages = await new DamageMessage(actor, item).addMessage(options); + + expect(messages).toStrictEqual(["message.all message"]); + expect(options.options?.dialogOptions?.id).toBe(""); + }); + + test("damage with message.damage.all flag should add the message", async () => { + const actor = createActorWithEffects([ + "flags.adv-reminder.message.damage.all", + "message.damage.all message", + ]); + const item = createItem("mwak", "str"); + const options = {}; + + const messages = await new DamageMessage(actor, item).addMessage(options); + + expect(messages).toStrictEqual(["message.damage.all message"]); + expect(options.options?.dialogOptions?.id).toBe(""); + }); + + test("damage with message.damage.mwak flag should add the message for Melee Weapon damage", async () => { + const actor = createActorWithEffects([ + "flags.adv-reminder.message.damage.mwak", + "message.damage.mwak message", + ]); + const item = createItem("mwak", "str"); + const options = {}; + + const messages = await new DamageMessage(actor, item).addMessage(options); + + expect(messages).toStrictEqual(["message.damage.mwak message"]); + expect(options.options?.dialogOptions?.id).toBe(""); + }); + + test("damage with message.damage.mwak flag should not add the message for Ranged Weapon damage", async () => { + const actor = createActorWithEffects([ + "flags.adv-reminder.message.damage.mwak", + "message.damage.mwak message", + ]); + const item = createItem("rwak", "dex"); + const options = {}; + + const messages = await new DamageMessage(actor, item).addMessage(options); + + expect(messages).toStrictEqual([]); + expect(options.options?.dialogOptions).toBeUndefined(); + }); + + test("damage with two messages should add both messages", async () => { + const actor = createActorWithEffects( + ["flags.adv-reminder.message.damage.all", "first"], + ["flags.adv-reminder.message.damage.mwak", "second"] + ); + const item = createItem("mwak", "str"); + const options = {}; + + const messages = await new DamageMessage(actor, item).addMessage(options); + + expect(messages).toStrictEqual(["first", "second"]); + expect(options.options?.dialogOptions?.id).toBe(""); + }); +});