diff --git a/CHANGELOG.md b/CHANGELOG.md index ade75bf..1b77e17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# upcoming + +- feature: Support the system's new concentration rolls with sources and messages (system already handles advantage/disadvantage) + # 3.4.1 - feature: [#64](https://github.com/kaelad02/adv-reminder/pull/64) Add pt-BR translation, thanks @Kharmans diff --git a/src/messages.js b/src/messages.js index 64df4f0..20b79db 100644 --- a/src/messages.js +++ b/src/messages.js @@ -29,10 +29,15 @@ class BaseMessage { addMessage(options) { debug("checking for message effects"); + // get any existing messages + const messages = getProperty(options, "dialogOptions.adv-reminder.messages") ?? []; + + // get messages from the actor and merge const keys = this.messageKeys; - const messages = this.changes + const actorMessages = this.changes .filter((change) => keys.includes(change.key)) .map((change) => change.value); + messages.push(...actorMessages); // get messages from the target and merge const targetKeys = this.targetKeys; @@ -114,6 +119,14 @@ export class AbilitySaveMessage extends AbilityBaseMessage { } } +export class ConcentrationMessage extends AbilityBaseMessage { + /** @override */ + get messageKeys() { + // don't call super since the system will trigger a saving throw + return ["flags.adv-reminder.message.ability.concentration"]; + } +} + export class SkillMessage extends AbilityCheckMessage { constructor(actor, abilityId, skillId) { super(actor, abilityId); diff --git a/src/reminders.js b/src/reminders.js index ca51e43..2ae2d98 100644 --- a/src/reminders.js +++ b/src/reminders.js @@ -22,12 +22,14 @@ class BaseReminder { /** * An accumulator that looks for matching keys and tracks advantage/disadvantage. + * @param {Object} options + * @param {boolean} options.advantage initial value for advantage + * @param {boolean} options.disadvantage initial value for disadvantage */ - _accumulator() { - let advantage; - let disadvantage; - + _accumulator({advantage, disadvantage} = {}) { return { + advantage, + disadvantage, add: (actorFlags, advKeys, disKeys) => { advantage = advKeys.reduce((accum, curr) => accum || actorFlags[curr], advantage); disadvantage = disKeys.reduce((accum, curr) => accum || actorFlags[curr], disadvantage); @@ -38,8 +40,16 @@ class BaseReminder { update: (options) => { debug(`updating options with {advantage: ${advantage}, disadvantage: ${disadvantage}}`); // only set if adv or dis, the die roller doesn't handle when both are true correctly - if (advantage && !disadvantage) options.advantage = true; - else if (!advantage && disadvantage) options.disadvantage = true; + if (advantage && !disadvantage) { + options.advantage = true; + if (options.disadvantage) options.disadvantage = false; + } else if (!advantage && disadvantage) { + if (options.advantage) options.advantage = false; + options.disadvantage = true; + } else { + if (options.advantage) options.advantage = false; + if (options.disadvantage) options.disadvantage = false; + } }, }; } @@ -121,7 +131,7 @@ class AbilityBaseReminder extends BaseReminder { debug("advKeys", advKeys, "disKeys", disKeys); // find matching keys and update options - const accumulator = this._accumulator(); + const accumulator = options.isConcentration ? this._accumulator(options) : this._accumulator(); accumulator.add(this.actorFlags, advKeys, disKeys); accumulator.update(options); } diff --git a/src/rollers/core.js b/src/rollers/core.js index 95c5d2a..6981a03 100644 --- a/src/rollers/core.js +++ b/src/rollers/core.js @@ -3,6 +3,7 @@ import { AbilityCheckMessage, AbilitySaveMessage, AttackMessage, + ConcentrationMessage, DamageMessage, DeathSaveMessage, SkillMessage, @@ -19,6 +20,7 @@ import { AbilityCheckSource, AbilitySaveSource, AttackSource, + ConcentrationSource, CriticalSource, DeathSaveSource, SkillSource, @@ -47,6 +49,7 @@ export default class CoreRollerHooks { // register all the dnd5e.pre hooks Hooks.on("dnd5e.preRollAttack", this.preRollAttack.bind(this)); Hooks.on("dnd5e.preRollAbilitySave", this.preRollAbilitySave.bind(this)); + Hooks.on("dnd5e.preRollConcentration", this.preRollConcentration.bind(this)); Hooks.on("dnd5e.preRollAbilityTest", this.preRollAbilityTest.bind(this)); Hooks.on("dnd5e.preRollSkill", this.preRollSkill.bind(this)); Hooks.on("dnd5e.preRollToolCheck", this.preRollToolCheck.bind(this)); @@ -86,6 +89,16 @@ export default class CoreRollerHooks { new AbilitySaveReminder(actor, abilityId).updateOptions(config); } + preRollConcentration(actor, options) { + debug("preRollConcentration hook called"); + + if (this.isFastForwarding(options)) return; + + new ConcentrationMessage(actor, options.ability).addMessage(options); + if (showSources) new ConcentrationSource(actor, options.ability).updateOptions(options); + // don't need a reminder, the system will set advantage/disadvantage + } + preRollAbilityTest(actor, config, abilityId) { debug("preRollAbilityTest hook called"); diff --git a/src/rollers/midi.js b/src/rollers/midi.js index 33ee16a..5afe23c 100644 --- a/src/rollers/midi.js +++ b/src/rollers/midi.js @@ -2,6 +2,7 @@ import { AbilityCheckMessage, AbilitySaveMessage, AttackMessage, + ConcentrationMessage, DamageMessage, DeathSaveMessage, SkillMessage, @@ -10,6 +11,7 @@ import { AbilityCheckSource, AbilitySaveSource, AttackSource, + ConcentrationSource, CriticalSource, DeathSaveSource, SkillSource, @@ -45,6 +47,15 @@ export default class MidiRollerHooks extends CoreRollerHooks { if (showSources) new AbilitySaveSource(actor, abilityId).updateOptions(config); } + preRollConcentration(actor, options) { + debug("preRollConcentration hook called"); + + if (this.isFastForwarding(options)) return; + + new ConcentrationMessage(actor, options.ability).addMessage(options); + if (showSources) new ConcentrationSource(actor, options.ability).updateOptions(options); + } + preRollAbilityTest(actor, config, abilityId) { debug("preRollAbilityTest hook called"); diff --git a/src/rollers/rsr.js b/src/rollers/rsr.js index 69481b3..648340c 100644 --- a/src/rollers/rsr.js +++ b/src/rollers/rsr.js @@ -3,6 +3,7 @@ import { AbilityCheckMessage, AbilitySaveMessage, AttackMessage, + ConcentrationMessage, DamageMessage, DeathSaveMessage, SkillMessage, @@ -19,6 +20,7 @@ import { AbilityCheckSource, AbilitySaveSource, AttackSource, + ConcentrationSource, CriticalSource, DeathSaveSource, SkillSource, @@ -69,6 +71,16 @@ export default class ReadySetRollHooks extends CoreRollerHooks { if (this._doReminder(config)) new AbilitySaveReminder(actor, abilityId).updateOptions(config); } + preRollConcentration(actor, options) { + debug("preRollConcentration hook called"); + + if (this._doMessages(options)) { + new ConcentrationMessage(actor, options.ability).addMessage(options); + if (showSources) new ConcentrationSource(actor, options.ability).updateOptions(options); + } + // don't need a reminder, the system will set advantage/disadvantage + } + preRollAbilityTest(actor, config, abilityId) { debug("preRollAbilityTest hook called"); diff --git a/src/sources.js b/src/sources.js index 9c0eca5..8877433 100644 --- a/src/sources.js +++ b/src/sources.js @@ -47,6 +47,9 @@ const SourceMixin = (superclass) => if (changes[key]) disadvantageLabels.push(...changes[key]); }); }, + advantage: (label) => { + if (label) advantageLabels.push(label); + }, disadvantage: (label) => { if (label) disadvantageLabels.push(label); }, @@ -70,6 +73,31 @@ export class AttackSource extends SourceMixin(AttackReminder) {} export class AbilitySaveSource extends SourceMixin(AbilitySaveReminder) {} +export class ConcentrationSource extends SourceMixin(Object) { + constructor(actor, abilityId) { + super(); + + /** @type {object} */ + this.conc = actor.system.attributes?.concentration; + /** @type {string} */ + this.abilityId = abilityId; + } + + updateOptions(options) { + this._message(); + + const modes = CONFIG.Dice.D20Roll.ADV_MODE; + const source = () => + game.i18n.localize("DND5E.Concentration") + " " + game.i18n.localize("DND5E.AdvantageMode"); + + // check Concentration's roll mode to look for advantage/disadvantage + const accumulator = this._accumulator(); + if (this.conc.roll.mode === modes.ADVANTAGE) accumulator.advantage(source()); + else if (this.conc.roll.mode === modes.DISADVANTAGE) accumulator.disadvantage(source()); + accumulator.update(options); + } +} + export class AbilityCheckSource extends SourceMixin(AbilityCheckReminder) {} export class SkillSource extends SourceMixin(SkillReminder) {} diff --git a/test/common.js b/test/common.js index fd820b3..a9aaaf8 100644 --- a/test/common.js +++ b/test/common.js @@ -24,6 +24,19 @@ export default function commonTestInit() { lastObj[lastProp] = value; }; + globalThis.getProperty = (object, key) => { + if (!key) return undefined; + if (key in object) return object[key]; + let target = object; + for (let p of key.split(".")) { + getType(target); + if (!(typeof target === "object")) return undefined; + if (p in target) target = target[p]; + else return undefined; + } + return target; + }; + globalThis.flattenObject = (obj, _d = 0) => { const flat = {}; if (_d > 100) { diff --git a/test/messages.test.js b/test/messages.test.js index a61f15d..e3d634c 100644 --- a/test/messages.test.js +++ b/test/messages.test.js @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, jest, test } from "@jest/globals"; +import { beforeAll, describe, expect, jest, test } from "@jest/globals"; import { AbilityCheckMessage, AbilitySaveMessage, @@ -7,20 +7,7 @@ import { DeathSaveMessage, SkillMessage, } from "../src/messages"; - -// fakes -globalThis.setProperty = (object, key, value) => { - // split the key into parts, removing the last one - const parts = key.split("."); - const lastProp = parts.pop(); - // recursively create objects out the key parts - const lastObj = parts.reduce((obj, prop) => { - if (!obj.hasOwnProperty(prop)) obj[prop] = {}; - return obj[prop]; - }, object); - // set the value using the last key part - lastObj[lastProp] = value; -}; +import commonTestInit from "./common.js"; function createActorWithEffects(...keyValuePairs) { const appliedEffects = keyValuePairs.map(createEffect); @@ -52,6 +39,10 @@ function createItem(actionType, abilityMod) { }; } +beforeAll(() => { + commonTestInit(); +}); + describe("AttackMessage no legit active effects", () => { test("attack with no active effects should not add a message", () => { const actor = createActorWithEffects();