diff --git a/CHANGELOG.md b/CHANGELOG.md index 2091036..3b16b63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ - feature: Support the system's new concentration rolls with sources and messages with `flags.adv-reminder.message.ability.concentration` (system already handles advantage/disadvantage) - feature: Support `flags.midi-qol.grants.critical.range` for conditions like Paralyzed that turn hits into crits if the attacker is adjacent. Does not currently work with Ready Set Roll, will just be ignored. +- feature: Apply conditions directly bases on the `statuses` instead of adding Midi's flags to status effects # 3.4.1 diff --git a/lang/en.json b/lang/en.json index 7b19c13..fef839e 100644 --- a/lang/en.json +++ b/lang/en.json @@ -29,7 +29,7 @@ "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." + "Name": "Enable Condition Effects", + "Hint": "Apply adv/dis/crit with the system's conditions (e.g. apply disadvantage to attacks when Poisoned). Not recommended if you are using another module to modify or replace the status effects." } } diff --git a/src/fails.js b/src/fails.js index 057589f..52d8c36 100644 --- a/src/fails.js +++ b/src/fails.js @@ -20,6 +20,10 @@ class BaseFail { return ["fail.all"]; } + get failCondition() { + return undefined; + } + /** * Check for auto-fail flags to see if this roll should fail. * @param {object} options the roll options @@ -33,7 +37,9 @@ class BaseFail { debug("failKeys", failKeys); const actorFlags = this._getFlags(this.actor); - const shouldFail = failKeys.reduce((accum, curr) => actorFlags[curr] || accum, false); + const shouldFail = + failKeys.reduce((accum, curr) => actorFlags[curr] || accum, false) || + this.actor.hasConditionEffect(this.failCondition); if (shouldFail) { const messageData = this.createMessageData(options); this.toMessage(messageData); @@ -78,6 +84,14 @@ export class AbilitySaveFail extends BaseFail { ]); } + /** @override */ + get failCondition() { + switch (this.abilityId) { + case "dex": return "advReminderFailDexSave"; + case "str": return "advReminderFailDexSave"; + } + } + createMessageData(options = {}) { // build title, probably used as chat message flavor const label = CONFIG.DND5E.abilities[this.abilityId]; diff --git a/src/module.js b/src/module.js index 62c499f..e6a0aba 100644 --- a/src/module.js +++ b/src/module.js @@ -40,282 +40,30 @@ function applyMidiCustom(actor, change) { } Hooks.once("setup", () => { - if (game.settings.get("adv-reminder", "updateStatusEffects")) { - updateStatusEffects(); - Hooks.on("preCreateActiveEffect", addExhaustionEffects); - Hooks.on("preUpdateActiveEffect", addExhaustionEffects); - } + if (game.settings.get("adv-reminder", "updateStatusEffects")) updateConditionEffects(); }); -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 advantage-like condition effects for all status effects. + * details: when adding to CONFIG.DND5E.conditionEffects, make sure to include an "advReminder" + * prefix to serve as a namespace to avoid conflicts + */ +function updateConditionEffects() { + const ce = CONFIG.DND5E.conditionEffects; + ce.advReminderAdvantageAttack = new Set(["hiding", "invisible"]); + ce.advReminderAdvantageDexSave = new Set(["dodging"]); + ce.advReminderDisadvantageAttack = new Set(["blinded", "frightened", "poisoned", "prone", "restrained"]); + ce.advReminderDisadvantageAbility = new Set(["exhaustion-1", "frightened", "poisoned"]); + ce.advReminderDisadvantageSave = new Set(["exhaustion-3"]); + ce.advReminderDisadvantageDexSave = new Set(["restrained"]); + ce.advReminderDisadvantagePhysicalRolls = new Set(["heavilyEncumbered"]); + ce.advReminderFailDexSave = new Set(["paralyzed", "petrified", "stunned", "unconscious"]); + ce.advReminderFailStrSave = new Set(["paralyzed", "petrified", "stunned", "unconscious"]); + ce.advReminderGrantAdvantageAttack = new Set(["blinded", "paralyzed", "petrified", "restrained", "stunned", "unconscious"]); + ce.advReminderGrantAdjacentCritical = new Set(["paralyzed", "unconscious"]); + ce.advReminderGrantDisadvantageAttack = new Set(["dodging", "exhaustion-3", "hidden", "invisible"]); + // if adjacent, grant advantage on the attack, else grant disadvantage + ce.advReminderGrantAdjacentAttack = new Set(["prone"]); } // Add message flags to DAE so it shows them in the AE editor diff --git a/src/reminders.js b/src/reminders.js index 093e266..935738a 100644 --- a/src/reminders.js +++ b/src/reminders.js @@ -2,6 +2,8 @@ import { debug, isEmpty } from "./util.js"; class BaseReminder { constructor(actor) { + /** @type {Actor5e*} */ + this.actor = actor; /** @type {object} */ this.actorFlags = this._getFlags(actor); } @@ -20,6 +22,26 @@ class BaseReminder { debug("checking for adv/dis effects for the roll"); } + /** + * Modeled after Actor5e#hasConditionEffect, check to see if the actor is under the effect of + * this property from some status or due to its level of exhaustion. But instead of a true/false, + * it returns the ID of the condition. + * @param {Actor5e} actor the actor + * @param {string} key the effect key + * @returns {Set} a set of the condition IDs the actor has for the effect + */ + _getConditionForEffect(actor, key) { + const props = CONFIG.DND5E.conditionEffects[key] ?? new Set(); + const level = actor.system.attributes?.exhaustion ?? null; + const imms = actor.system.traits?.ci?.value ?? new Set(); + const statuses = actor.statuses; + return props.filter(k => { + const l = Number(k.split("-").pop()); + return (statuses.has(k) && !imms.has(k)) + || (!imms.has("exhaustion") && (level !== null) && Number.isInteger(l) && (level >= l)); + }).toObject(); + } + /** * An accumulator that looks for matching keys and tracks advantage/disadvantage. * @param {Object} options @@ -34,6 +56,14 @@ class BaseReminder { advantage = advKeys.reduce((accum, curr) => accum || actorFlags[curr], advantage); disadvantage = disKeys.reduce((accum, curr) => accum || actorFlags[curr], disadvantage); }, + fromConditions: (actor, advConditions, disConditions) => { + if (!actor) return; + if (advConditions.flatMap(c => this._getConditionForEffect(actor, c)).length) advantage = true; + if (disConditions.flatMap(c => this._getConditionForEffect(actor, c)).length) disadvantage = true; + }, + advantage: (label) => { + if (label) advantage = true; + }, disadvantage: (label) => { if (label) disadvantage = true; }, @@ -56,23 +86,24 @@ class BaseReminder { } export class AttackReminder extends BaseReminder { - constructor(actor, targetActor, item) { + constructor(actor, targetActor, item, distanceFn) { super(actor); + /** @type {Actor5e*} */ + this.targetActor = targetActor; /** @type {object} */ this.targetFlags = this._getFlags(targetActor); /** @type {string} */ this.actionType = item.system.actionType; /** @type {string} */ this.abilityId = item.abilityMod; + /** @type {function} */ + this.distanceFn = distanceFn; } updateOptions(options) { this._message(); - // quick return if there are no flags - if (isEmpty(this.actorFlags) && isEmpty(this.targetFlags)) return; - // build the active effect keys applicable for this roll const advKeys = [ "advantage.all", @@ -94,11 +125,30 @@ export class AttackReminder extends BaseReminder { "grants.disadvantage.attack.all", `grants.disadvantage.attack.${this.actionType}`, ]; - - // find matching keys and update options + // build the condition effect keys for this roll + const advConditions = ["advReminderAdvantageAttack"]; + const disConditions = ["advReminderDisadvantageAttack"]; + const grantsAdvConditions = ["advReminderGrantAdvantageAttack"]; + const grantsDisConditions = ["advReminderGrantDisadvantageAttack"]; + if (this.abilityId === "str" || this.abilityId === "dex" || this.abilityId === "con") + disConditions.push("advReminderDisadvantagePhysicalRolls"); + + // find matching keys const accumulator = this._accumulator(); accumulator.add(this.actorFlags, advKeys, disKeys); accumulator.add(this.targetFlags, grantsAdvKeys, grantsDisKeys); + // handle status effects + accumulator.fromConditions(this.actor, advConditions, disConditions); + accumulator.fromConditions(this.targetActor, grantsAdvConditions, grantsDisConditions); + // handle distance-based status effects + if (this.targetActor) { + const grantAdjacentAttack = this._getConditionForEffect(this.targetActor, "advReminderGrantAdjacentAttack"); + if (grantAdjacentAttack.length) { + const distance = this.distanceFn(); + const accumFn = distance <= 5 ? accumulator.advantage : accumulator.disadvantage; + grantAdjacentAttack.forEach(accumFn); + } + } accumulator.update(options); } } @@ -119,20 +169,28 @@ class AbilityBaseReminder extends BaseReminder { return ["disadvantage.all", "disadvantage.ability.all"]; } + get advantageConditions() { + return []; + } + + get disadvantageConditions() { + if (this.abilityId === "str" || this.abilityId === "dex" || this.abilityId === "con") + return ["advReminderDisadvantagePhysicalRolls"]; + return []; + } + updateOptions(options) { this._message(); - // quick return if there are no flags - if (isEmpty(this.actorFlags)) return; - // get the active effect keys applicable for this roll const advKeys = this.advantageKeys; const disKeys = this.disadvantageKeys; debug("advKeys", advKeys, "disKeys", disKeys); - // find matching keys and update options + // find matching keys, status effects, and update options const accumulator = options.isConcentration ? this._accumulator(options) : this._accumulator(); accumulator.add(this.actorFlags, advKeys, disKeys); + accumulator.fromConditions(this.actor, this.advantageConditions, this.disadvantageConditions); accumulator.update(options); } } @@ -153,6 +211,12 @@ export class AbilityCheckReminder extends AbilityBaseReminder { `disadvantage.ability.check.${this.abilityId}`, ]); } + + get disadvantageConditions() { + const conditions = super.disadvantageConditions; + conditions.push("advReminderDisadvantageAbility"); + return conditions; + } } export class AbilitySaveReminder extends AbilityBaseReminder { @@ -171,6 +235,20 @@ export class AbilitySaveReminder extends AbilityBaseReminder { `disadvantage.ability.save.${this.abilityId}`, ]); } + + /** @override */ + get advantageConditions() { + const conditions = []; + if (this.abilityId === "dex") conditions.push("advReminderAdvantageDexSave"); + return conditions; + } + + get disadvantageConditions() { + const conditions = super.disadvantageConditions; + conditions.push("advReminderDisadvantageSave"); + if (this.abilityId === "dex") conditions.push("advReminderDisadvantageDexSave"); + return conditions; + } } export class SkillReminder extends AbilityCheckReminder { @@ -213,6 +291,7 @@ export class SkillReminder extends AbilityCheckReminder { accumulator.disadvantage(this._armorStealthDisadvantage()); } accumulator.add(this.actorFlags, advKeys, disKeys); + accumulator.fromConditions(this.actor, this.advantageConditions, this.disadvantageConditions); accumulator.update(options); } @@ -254,10 +333,14 @@ export class CriticalReminder extends BaseReminder { constructor(actor, targetActor, item, distanceFn) { super(actor); + /** @type {Actor5e*} */ + this.targetActor = targetActor; /** @type {object} */ this.targetFlags = this._getFlags(targetActor); /** @type {string} */ this.actionType = item.system.actionType; + /** @type {function} */ + this.distanceFn = distanceFn; // get the Range directly from the actor's flags if (targetActor) { @@ -278,9 +361,6 @@ export class CriticalReminder extends BaseReminder { updateOptions(options, critProp = "critical") { this._message(); - // quick return if there are no flags - if (isEmpty(this.actorFlags) && isEmpty(this.targetFlags)) return; - // build the active effect keys applicable for this roll const critKeys = ["critical.all", `critical.${this.actionType}`]; const normalKeys = ["noCritical.all", `noCritical.${this.actionType}`]; @@ -295,6 +375,14 @@ export class CriticalReminder extends BaseReminder { const accumulator = this._accumulator(); accumulator.add(this.actorFlags, critKeys, normalKeys); accumulator.add(this.targetFlags, grantsCritKeys, grantsNormalKeys); + // handle distance-based status effects + if (this.targetActor) { + const grantAdjacentCritical = this._getConditionForEffect(this.targetActor, "advReminderGrantAdjacentCritical"); + if (grantAdjacentCritical.length) { + const distance = this.distanceFn(); + if (distance <= 5) grantAdjacentCritical.forEach(accumulator.critical); + } + } accumulator.update(options, critProp); } @@ -308,6 +396,9 @@ export class CriticalReminder extends BaseReminder { crit = critKeys.reduce((accum, curr) => accum || actorFlags[curr], crit); normal = normalKeys.reduce((accum, curr) => accum || actorFlags[curr], normal); }, + critical: (label) => { + if (label) crit = true; + }, update: (options, critProp) => { // a normal hit overrides a crit const critical = normal ? false : !!crit; diff --git a/src/rollers/core.js b/src/rollers/core.js index a44eab4..c030efd 100644 --- a/src/rollers/core.js +++ b/src/rollers/core.js @@ -70,10 +70,11 @@ export default class CoreRollerHooks { if (this.isFastForwarding(config)) return; const target = getTarget(); + const distanceFn = getDistanceToTargetFn(config.messageData.speaker); new AttackMessage(item.actor, target, item).addMessage(config); - if (showSources) new AttackSource(item.actor, target, item).updateOptions(config); - new AttackReminder(item.actor, target, item).updateOptions(config); + if (showSources) new AttackSource(item.actor, target, item, distanceFn).updateOptions(config); + new AttackReminder(item.actor, target, item, distanceFn).updateOptions(config); } preRollAbilitySave(actor, config, abilityId) { diff --git a/src/rollers/midi.js b/src/rollers/midi.js index 0150601..425f507 100644 --- a/src/rollers/midi.js +++ b/src/rollers/midi.js @@ -33,9 +33,16 @@ export default class MidiRollerHooks extends CoreRollerHooks { if (this.isFastForwarding(config)) return; const target = getTarget(); + // use distance from Midi's Workflow + const distanceFn = () => { + const workflow = MidiQOL.Workflow.getWorkflow(item.uuid); + if (!workflow) return Infinity; + const firstTarget = workflow.hitTargets.values().next().value; + return MidiQOL.computeDistance(firstTarget, workflow.token, false); + } new AttackMessage(item.actor, target, item).addMessage(config); - if (showSources) new AttackSource(item.actor, target, item).updateOptions(config); + if (showSources) new AttackSource(item.actor, target, item, distanceFn).updateOptions(config); } preRollAbilitySave(actor, config, abilityId) { diff --git a/src/rollers/rsr.js b/src/rollers/rsr.js index 48621a4..15fce74 100644 --- a/src/rollers/rsr.js +++ b/src/rollers/rsr.js @@ -26,7 +26,7 @@ import { SkillSource, } from "../sources.js"; import { showSources } from "../settings.js"; -import { debug, getTarget } from "../util.js"; +import { debug, getDistanceToTargetFn, getTarget } from "../util.js"; import CoreRollerHooks from "./core.js"; // disable the grants.critical.range flag since RSR can't have it's critical flag changed anyways, @@ -51,14 +51,15 @@ export default class ReadySetRollHooks extends CoreRollerHooks { debug("preRollAttack hook called"); const target = getTarget(); + const distanceFn = getDistanceToTargetFn(config.messageData.speaker); if (this._doMessages(config)) { new AttackMessage(item.actor, target, item).addMessage(config); - if (showSources) new AttackSource(item.actor, target, item).updateOptions(config); + if (showSources) new AttackSource(item.actor, target, item, distanceFn).updateOptions(config); } if (this._doReminder(config)) - new AttackReminder(item.actor, target, item).updateOptions(config); + new AttackReminder(item.actor, target, item, distanceFn).updateOptions(config); } preRollAbilitySave(actor, config, abilityId) { diff --git a/src/sources.js b/src/sources.js index 6c3899a..452f47e 100644 --- a/src/sources.js +++ b/src/sources.js @@ -34,6 +34,24 @@ const SourceMixin = (superclass) => debug("checking for adv/dis effects to display their source"); } + _getConditionForEffect(actor, key) { + const props = super._getConditionForEffect(actor, key); + return ( + props + // remove the number after exhaustion + .map((k) => k.split("-").shift()) + .flatMap((k) => { + // look for active effects with this status in it, get their names + const activeEffectNames = actor.appliedEffects + .filter((e) => e.statuses.some((s) => s === k)) + .map((e) => e.name); + if (activeEffectNames.length) return activeEffectNames; + // fallback to the status effect's name (mostly for exhaustion) + return CONFIG.statusEffects.filter((s) => s.id === k).map((s) => s.name); + }) + ); + } + _accumulator() { const advantageLabels = []; const disadvantageLabels = []; @@ -47,6 +65,11 @@ const SourceMixin = (superclass) => if (changes[key]) disadvantageLabels.push(...changes[key]); }); }, + fromConditions: (actor, advConditions, disConditions) => { + if (!actor) return; + advantageLabels.push(...advConditions.flatMap(c => this._getConditionForEffect(actor, c))); + disadvantageLabels.push(...disConditions.flatMap(c => this._getConditionForEffect(actor, c))); + }, advantage: (label) => { if (label) advantageLabels.push(label); }, @@ -126,6 +149,9 @@ export class CriticalSource extends SourceMixin(CriticalReminder) { if(changes[key]) normalLabels.push(...changes[key]); }); }, + critical: (label) => { + if (label) criticalLabels.push(label); + }, update: (options) => { debug("criticalLabels", criticalLabels, "normalLabels", normalLabels); const merge = (newLabels, key) => { diff --git a/test/common.js b/test/common.js index a9aaaf8..19ccdd7 100644 --- a/test/common.js +++ b/test/common.js @@ -4,13 +4,36 @@ export default function commonTestInit() { globalThis.createActorWithFlags = (...keys) => { const actor = { flags: {}, + hasConditionEffect: () => false, + system: {}, }; keys.forEach((k) => setProperty(actor, k, true)); return actor; }; + globalThis.CONFIG = {}; + globalThis.CONFIG.DND5E = {}; + globalThis.CONFIG.DND5E.conditionEffects = {}; + // copied from Foundry + function filter(test) { + const filtered = new Set(); + let i = 0; + for ( const v of this ) { + if ( test(v, i, this) ) filtered.add(v); + i++; + } + return filtered; + } + function toObject() { + return Array.from(this); + } + Object.defineProperties(Set.prototype, { + filter: {value: filter}, + toObject: {value: toObject} + }); + globalThis.setProperty = (object, key, value) => { // split the key into parts, removing the last one const parts = key.split(".");