const MODULE_ID = 'pf2e-modifiers-matter' // TODO - currently impossible, but in the future may be possible to react to effects that change embedded DCs in Note rule elements. // See: https://github.com/foundryvtt/pf2e/issues/9824 // for example, the Monk's Stunning Fist uses a Class DC but this module won't recognize modifiers to that DC in this situation. // Helpful for testing - replace random dice roller with 1,2,3,4....19,20 by putting this in the console: /* NEXT_RND_ROLLS_D20 = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20] rndIndex = -1 CONFIG.Dice.randomUniform = () => {rndIndex = (rndIndex + 1) % NEXT_RND_ROLLS_D20.length; return NEXT_RND_ROLLS_D20[rndIndex] / 20 - 0.001} */ // this file has a ton of math (mostly simple). // I did my best to make it all easily understandable math, but there are limits to what I can do. /** * ESSENTIAL (strong green) - This modifier was necessary to achieve this degree of success (DoS). Others were * potentially also necessary. You should thank the character who caused this modifier! * * HELPFUL (weak green) - This modifier was not necessary to achieve this DoS, but degree of success did change due to * modifiers in this direction, and at least one of the helpful modifiers was needed. For example, if you rolled a 14, * had +1 & +2, and needed a 15, both the +1 and +2 are weak green because neither is necessary on its own, but they * were necessary together. If you had rolled a 13 in this case, the +2 would be strong green but the +1 would still be * weak green, simply because it's difficult to come up with an algorithm that would solve complex cases. * Note, by the way, that in case of multiple non-stacking modifiers, PF2e hides some of them from the chat card. * * NONE - This modifier did not affect the DoS at all, this time. * * HARMFUL (orange) - Like HELPFUL but in the opposite direction. Without all the harmful modifiers you had (but * not without any one of them), you would've gotten a better DoS. * * DETRIMENTAL (red) - Like ESSENTIAL but in the opposite direction. Without this, you would've gotten a better DoS. */ const SIGNIFICANCE = Object.freeze({ ESSENTIAL: 'ESSENTIAL', HELPFUL: 'HELPFUL', NONE: 'NONE', HARMFUL: 'HARMFUL', DETRIMENTAL: 'DETRIMENTAL', }) const COLOR_BY_SIGNIFICANCE = Object.freeze({ ESSENTIAL: '#008000', HELPFUL: '#91a82a', NONE: '#000000', HARMFUL: '#ff0000', DETRIMENTAL: '#ff852f', }) let IGNORED_MODIFIER_LABELS = [] let IGNORED_MODIFIER_LABELS_FOR_AC_ONLY = [] let warnedAboutLocalization = false const tryLocalize = (key, defaultValue) => { const localized = game.i18n.localize(key) if (localized === key) { if (!warnedAboutLocalization) { console.warn(`${MODULE_ID}: failed to localize ${key}`) warnedAboutLocalization = true } return defaultValue } return localized } const initializeIgnoredModifiers = () => { const IGNORED_MODIFIERS_I18N = [ 'PF2E.BaseModifier', 'PF2E.ModifierTitle', 'PF2E.MultipleAttackPenalty', 'PF2E.ProficiencyLevel0', 'PF2E.ProficiencyLevel1', 'PF2E.ProficiencyLevel2', 'PF2E.ProficiencyLevel3', 'PF2E.ProficiencyLevel4', 'PF2E.AbilityStr', 'PF2E.AbilityCon', 'PF2E.AbilityDex', 'PF2E.AbilityInt', 'PF2E.AbilityWis', 'PF2E.AbilityCha', 'PF2E.PotencyRuneLabel', 'PF2E.RuleElement.WeaponPotency', 'PF2E.AutomaticBonusProgression.attackPotency', 'PF2E.AutomaticBonusProgression.defensePotency', 'PF2E.AutomaticBonusProgression.savePotency', 'PF2E.AutomaticBonusProgression.perceptionPotency', 'PF2E.NPC.Adjustment.EliteLabel', 'PF2E.NPC.Adjustment.WeakLabel', 'PF2E.MasterSavingThrow.fortitude', 'PF2E.MasterSavingThrow.reflex', 'PF2E.MasterSavingThrow.will', `${MODULE_ID}.IgnoredModifiers.DeviseAStratagem`, // Investigator `${MODULE_ID}.IgnoredModifiers.HuntersEdgeFlurry1`, // Ranger, replaces multiple attack penalty `${MODULE_ID}.IgnoredModifiers.HuntersEdgeFlurry2`, // same `${MODULE_ID}.IgnoredModifiers.HuntersEdgeFlurry3`, // same, Ranger's companion // NOTE: all spells that end in "form" are also ignored for the attack bonus; e.g. Ooze Form // also some battle form spells with different names: `${MODULE_ID}.IgnoredModifiers.BattleForm1`, // battle form `${MODULE_ID}.IgnoredModifiers.BattleForm2`, // battle form `${MODULE_ID}.IgnoredModifiers.BattleForm3`, // battle form `${MODULE_ID}.IgnoredModifiers.BattleForm4`, // battle form // yes I'm gonna add my houserules to my module, you can't stop me. // https://discord.com/channels/880968862240239708/880969943724728391/1082678343234760704 `${MODULE_ID}.IgnoredModifiers.SpellAttackHouserule`, `${MODULE_ID}.IgnoredModifiers.SpellPotency1`, `${MODULE_ID}.IgnoredModifiers.SpellPotency2`, `${MODULE_ID}.IgnoredModifiers.SkillPotency1`, `${MODULE_ID}.IgnoredModifiers.SkillPotency2`, // compatibility with a module, pf2e-flatten, which adds modifiers to match the PWoL variants. // https://github.com/League-of-Foundry-Developers/pf2e-flatten/blob/main/bundle.js#L41 `${MODULE_ID}.IgnoredModifiers3p.pf2e-flatten_pwol`, `${MODULE_ID}.IgnoredModifiers3p.pf2e-flatten_pwol_half`, ] IGNORED_MODIFIER_LABELS = IGNORED_MODIFIERS_I18N.map(str => tryLocalize(str, str)). concat(getSetting('additional-ignored-labels').split(';')) IGNORED_MODIFIER_LABELS_FOR_AC_ONLY = [ // effect that replaces your AC item bonus and dex cap - super hard to calculate its "true" bonus so I just ignore. // however, this effect also has other modifiers which I don't want to ignore. `${MODULE_ID}.IgnoredModifiers.DrakeheartMutagen`, ].map(str => tryLocalize(str, str)) } const sumMods = (modsList) => modsList.reduce((accumulator, curr) => accumulator + curr.modifier, 0) const modifierPositive = m => m.modifier > 0 const modifierNegative = m => m.modifier < 0 const getOffGuardAcMod = () => { const offGuardSlug = isNewerVersion(game.version, '5.3') ? 'off-guard' : 'flat-footed' const systemOffGuardCondition = game.pf2e.ConditionManager.getCondition(offGuardSlug) return { label: systemOffGuardCondition.name, modifier: -2, type: 'circumstance', } } const dcModsOfStatistic = (dcStatistic, actorWithDc) => { return dcStatistic.modifiers // remove if not enabled, or ignored .filter(m => m.enabled && !m.ignored) // remove everything that should be ignored (including user-defined) .filter(m => !IGNORED_MODIFIER_LABELS.includes(m.label)) // ignore item bonuses that come from armor, they're Resilient runes .filter(m => !( m.type === 'item' // comparing the modifier label to the names of the actor's Armor items && actorWithDc?.attributes.ac.modifiers.some(m2 => m2.label === m.label) )) } const rollModsFromChatMessage = (modifiersFromChatMessage, rollingActor, dcType) => { return modifiersFromChatMessage // enabled is false for one of the conditions if it can't stack with others .filter(m => m.enabled && !m.ignored) // ignoring standard things from list (including user-defined) .filter(m => !IGNORED_MODIFIER_LABELS.includes(m.label)) // for attacks, ignore all "form" spells that replace your attack bonus .filter(m => !(dcType === 'armor' && m.slug.endsWith('-form'))) // for attacks/skills, ignore Doubling Rings which are basically a permanent item bonus .filter(m => !m.slug.startsWith('doubling-rings')) // TODO - ignore item bonuses that are permanent (mostly skill items) // TODO - can next thing be removed? // for saving throws, ignore item bonuses that come from armor, they're Resilient runes .filter(m => !( m.type === 'item' // comparing the modifier label to the name of the rolling actor's Armor item && rollingActor?.attributes.ac.modifiers.some(m2 => m2.label === m.label) )) } const DEGREES = Object.freeze({ CRIT_SUCC: 'CRIT_SUCC', SUCCESS: 'SUCCESS', FAILURE: 'FAILURE', CRIT_FAIL: 'CRIT_FAIL', }) // REMEMBER: in Pf2e, delta 0-9 means SUCCESS, delta 10+ means CRIT SUCCESS, delta -1-9 is FAIL, delta -10- is CRIT FAIL const calcDegreeOfSuccess = (deltaFromDc) => { switch (true) { case deltaFromDc >= 10: return DEGREES.CRIT_SUCC case deltaFromDc <= -10: return DEGREES.CRIT_FAIL case deltaFromDc >= 1: return DEGREES.SUCCESS case deltaFromDc <= -1: return DEGREES.FAILURE case deltaFromDc === 0: return DEGREES.SUCCESS } // impossible console.error(`${MODULE_ID} | calcDegreeOfSuccess got wrong number: ${deltaFromDc}`) return DEGREES.CRIT_FAIL } const calcDegreePlusRoll = (deltaFromDc, dieRoll) => { const degree = calcDegreeOfSuccess(deltaFromDc) // handle natural 20 and natural 1 if (dieRoll === 20) { switch (degree) { case 'CRIT_SUCC': return DEGREES.CRIT_SUCC case 'SUCCESS': return DEGREES.CRIT_SUCC case 'FAILURE': return DEGREES.SUCCESS case 'CRIT_FAIL': return DEGREES.FAILURE } } else if (dieRoll === 1) { switch (degree) { case 'CRIT_SUCC': return DEGREES.SUCCESS case 'SUCCESS': return DEGREES.FAILURE case 'FAILURE': return DEGREES.CRIT_FAIL case 'CRIT_FAIL': return DEGREES.CRIT_FAIL } } else return degree } const shouldIgnoreStrikeCritFailToFail = (oldDOS, newDOS, isStrike) => { // only ignore in this somewhat common edge case: return ( // fail changed to crit fail, or vice versa ((oldDOS === DEGREES.FAILURE && newDOS === DEGREES.CRIT_FAIL) || (oldDOS === DEGREES.CRIT_FAIL && newDOS === DEGREES.FAILURE)) // and this game setting is enabled && getSetting('ignore-crit-fail-over-fail-on-attacks') // and it was a Strike attack && isStrike ) } /** * dcFlavorSuffix will be e.g. 'Off-Guard -2, Frightened -1' */ const insertDcFlavorSuffix = ($flavorText, dcFlavorSuffix, dcActorType) => { const showDefenseHighlightsToEveryone = getSetting('show-defense-highlights-to-everyone') const dataVisibility = showDefenseHighlightsToEveryone ? 'all' : 'gm' const messageKey = dcActorType === 'target' ? `${MODULE_ID}.Message.TargetHas` : dcActorType === 'caster' ? `${MODULE_ID}.Message.CasterHas` : `${MODULE_ID}.Message.ActorHas` $flavorText.find('div.degree-of-success').before( `
${tryLocalize(messageKey, 'Target has:')} ${dcFlavorSuffix}
`) } const hook_preCreateChatMessage = async (chatMessage, data) => { // continue only if message is a PF2e roll message with a rolling actor if ( !chatMessage.flags || !chatMessage.flags.pf2e || !chatMessage.flags.pf2e.modifiers || !chatMessage.flags.pf2e.context.dc || !chatMessage.flags.pf2e.context.actor ) return true const rollingActor = game.actors.get(chatMessage.flags.pf2e.context.actor) // here I assume the PF2E system always includes the d20 roll as the first roll! and as the first term of that roll! const roll = chatMessage.rolls[0] const rollTotal = roll?.total !== undefined ? roll.total : parseInt(chatMessage.content) // dc.value is usually defined, but apparently not when Escaping vs an enemy's Athletics DC const rollDc = chatMessage.flags.pf2e.context.dc.value ?? chatMessage.flags.pf2e.context.dc.parent?.dc?.value const deltaFromDc = rollTotal - rollDc // using roll.terms[0].total will work when rolling 1d20+9, or 2d20kh+9 (RollTwice RE), or 10+9 (SubstituteRoll RE) const dieRoll = roll.terms[0].total const currentDegreeOfSuccess = calcDegreePlusRoll(deltaFromDc, dieRoll) const dcSlug = chatMessage.flags.pf2e.context.dc.slug ?? chatMessage.flags.pf2e.context.dc.parent?.slug const isStrike = dcSlug === 'armor' const isSpell = chatMessage.flags.pf2e.origin?.type === 'spell' const targetedTokenUuid = chatMessage.flags.pf2e.context.target?.token const targetedActorUuid = chatMessage.flags.pf2e.context.target?.actor const targetedToken = targetedTokenUuid ? fromUuidSync(targetedTokenUuid) : undefined // targetedActorUuid will return the TOKEN uuid if it's an unlinked token! so, we're probably going to ignore it const targetedActor = targetedToken?.actor ? targetedToken.actor : targetedActorUuid ? fromUuidSync(targetedActorUuid) : undefined const originUuid = chatMessage.flags.pf2e.origin?.uuid const originItem = originUuid ? fromUuidSync(originUuid) : undefined const allModifiersInChatMessage = chatMessage.flags.pf2e.modifiers /* NOTE - from this point on, I use the term "modifier" or "mod" to refer to conditions/effects/feats that have granted a bonus or penalty to the roll or to the DC the roll was against. I will filter rollMods and dcMods to only include relevant non-ignored modifiers, and then calculate which modifiers actually made a significant impact on the outcome. The "modifier" objects in these lists are generally ModifierPf2e class objects, which have a "label", a "type", and a "modifier" field (their signed numerical value). */ const rollMods = rollModsFromChatMessage(allModifiersInChatMessage, rollingActor, dcSlug) let dcMods let actorWithDc if (isStrike && targetedActor) { actorWithDc = targetedActor dcMods = dcModsOfStatistic(targetedActor.system.attributes.ac, actorWithDc) const offGuardMod = getOffGuardAcMod() const isTargetEphemerallyOffGuard = chatMessage.flags.pf2e.context.options.includes( 'target:condition:off-guard') if (isTargetEphemerallyOffGuard && !dcMods.some(m => m.label === offGuardMod.label)) { const messageFlavorHtml = $(`
${chatMessage.flavor}
`) const dcTooltipsStr = messageFlavorHtml.find('div.target-dc > span > span.adjusted').attr('data-tooltip') if (dcTooltipsStr === undefined) { // TODO - find when it happens and fix it console.error(`${MODULE_ID} | failed to find target DC tooltips in message flavor:`, chatMessage.flavor) console.error(`${MODULE_ID} | message flavor as string: ${chatMessage.flavor}`) console.error( `${MODULE_ID} | please show these three error messages to shemetz on Discord and include a bit of context to explain what happened! 🙏`) offGuardMod.label = 'Off-Guard' dcMods.push(offGuardMod) } else { const dcTooltips = dcTooltipsStr.split('\n').map(s => s.replace('
', '').replace('
', '')) const offGuardTooltip = dcTooltips.find(t => t.includes(game.i18n.localize('PF2E.condition.off-guard.name'))) offGuardMod.label = offGuardTooltip.split(':')[0] dcMods.push(offGuardMod) } } dcMods = dcMods.filter(m => !IGNORED_MODIFIER_LABELS_FOR_AC_ONLY.includes(m.label)) } else if (isSpell && !!originItem) { // (note: originItem will be undefined in the rare case of a message created through a module like Quick Send To Chat) // if saving against spell, DC is the Spellcasting DC which means it's affected by stuff like Frightened and Stupefied actorWithDc = originItem.actor dcMods = dcModsOfStatistic(originItem.spellcasting.statistic.dc, actorWithDc) } else if (originItem?.category === 'class') { // if saving against a class feat/feature, DC is the Class DC which means it's affected by stuff like Frightened and Enfeebled/Drained/etc, depending // NOTE: this will not work for embedded Check buttons that come from Note REs. see https://github.com/foundryvtt/pf2e/issues/9824 actorWithDc = originItem.actor dcMods = dcModsOfStatistic(originItem.parent.classDC, actorWithDc) } else if (targetedActor && dcSlug) { // if there's a target, but it's not an attack, then it's probably a skill check against one of the target's // save DCs or perception DC or possibly a skill DC actorWithDc = targetedActor const dcStatistic = targetedActor.saves[dcSlug] || targetedActor.skills[dcSlug] || targetedActor[dcSlug] // dcStatistic should always be defined. (otherwise it means I didn't account for all cases here!) dcMods = dcModsOfStatistic(dcStatistic.dc, actorWithDc) } else { // happens if e.g. rolling from a @Check style button dcMods = [] } /** * wouldChangeOutcome(x) returns true if a bonus of x ("penalty" if x is negative) changes the degree of success */ const wouldChangeOutcome = (extra) => { const newDegreeOfSuccess = calcDegreePlusRoll(deltaFromDc + extra, dieRoll) return newDegreeOfSuccess !== currentDegreeOfSuccess && !shouldIgnoreStrikeCritFailToFail(currentDegreeOfSuccess, newDegreeOfSuccess, isStrike) } const positiveRollMods = rollMods.filter(modifierPositive) const negativeRollMods = rollMods.filter(modifierNegative) const positiveDcMods = dcMods.filter(modifierPositive) const negativeDcMods = dcMods.filter(modifierNegative) const necessaryPositiveRollMods = positiveRollMods.filter(m => wouldChangeOutcome(-m.modifier)) const necessaryNegativeRollMods = negativeRollMods.filter(m => wouldChangeOutcome(-m.modifier)) const necessaryPositiveDcMods = positiveDcMods.filter(m => wouldChangeOutcome(m.modifier)) const necessaryNegativeDcMods = negativeDcMods.filter(m => wouldChangeOutcome(m.modifier)) const rollModsPositiveTotal = sumMods(positiveRollMods) - sumMods(negativeDcMods) const rollModsNegativeTotal = sumMods(negativeRollMods) - sumMods(positiveDcMods) // sum of modifiers that were necessary to reach the current outcome - these are the biggest bonuses/penalties. const rollModsNecessaryPositiveTotal = sumMods(necessaryPositiveRollMods) - sumMods(necessaryPositiveDcMods) const rollModsNecessaryNegativeTotal = sumMods(necessaryNegativeRollMods) - sumMods(necessaryNegativeDcMods) // sum of all other modifiers. if this sum's changing does not affect the outcome it means modifiers were unnecessary const rollModsRemainingPositiveTotal = rollModsPositiveTotal - rollModsNecessaryPositiveTotal const rollModsRemainingNegativeTotal = rollModsNegativeTotal - rollModsNecessaryNegativeTotal // based on the above sums and the following booleans, we can determine which modifiers were significant and how much const didPositiveModifiersChangeOutcome = wouldChangeOutcome(-rollModsPositiveTotal) const didNegativeModifiersChangeOutcome = wouldChangeOutcome(-rollModsNegativeTotal) const didRemainingPositivesChangeOutcome = wouldChangeOutcome(-rollModsRemainingPositiveTotal) const didRemainingNegativesChangeOutcome = wouldChangeOutcome(-rollModsRemainingNegativeTotal) const calcSignificance = (modifierValue) => { const isNegativeMod = modifierValue < 0 const isPositiveMod = modifierValue > 0 const changedOutcome = wouldChangeOutcome(-modifierValue) if (isPositiveMod && changedOutcome) return SIGNIFICANCE.ESSENTIAL if (isPositiveMod && !changedOutcome && didPositiveModifiersChangeOutcome && didRemainingPositivesChangeOutcome) return SIGNIFICANCE.HELPFUL if (isNegativeMod && changedOutcome) return SIGNIFICANCE.HARMFUL if (isNegativeMod && !changedOutcome && didNegativeModifiersChangeOutcome && didRemainingNegativesChangeOutcome) return SIGNIFICANCE.DETRIMENTAL return SIGNIFICANCE.NONE } const significantModifiers = [] rollMods.forEach(m => { const modVal = m.modifier const significance = calcSignificance(modVal) if (significance === SIGNIFICANCE.NONE) return significantModifiers.push({ appliedTo: 'roll', name: m.label, value: modVal, significance: significance, }) }) dcMods.forEach(m => { const modVal = m.modifier const significance = calcSignificance(-modVal) significantModifiers.push({ appliedTo: 'dc', name: m.label, value: modVal, significance: significance, }) }) const oldFlavor = chatMessage.flavor // adding an artificial div to have a single parent element, enabling nicer editing of html const $editedFlavor = $(`
${oldFlavor}
`) // remove old highlights, in case of a reroll within the same message $editedFlavor.find('.pf2emm-highlight'). removeClass('pf2emm-highlight'). removeClass(`pf2emm-is-${SIGNIFICANCE.HARMFUL}`). removeClass(`pf2emm-is-${SIGNIFICANCE.HELPFUL}`). removeClass(`pf2emm-is-${SIGNIFICANCE.ESSENTIAL}`). removeClass(`pf2emm-is-${SIGNIFICANCE.DETRIMENTAL}`) significantModifiers.filter(m => m.appliedTo === 'roll').forEach(m => { const modVal = m.value const modName = m.name const modSignificance = m.significance if (modSignificance === SIGNIFICANCE.NONE) return const modValStr = (modVal < 0 ? '' : '+') + modVal $editedFlavor.find(`span.tag:contains(${modName} ${modValStr})`). addClass('pf2emm-highlight'). addClass(`pf2emm-is-${m.significance}`) }) const dcFlavorSuffixHtmls = [] significantModifiers.filter(m => m.appliedTo === 'dc').forEach(m => { const modVal = m.value const modName = m.name const modSignificance = m.significance if (modSignificance === SIGNIFICANCE.NONE) if (!(isStrike && getSetting('always-show-defense-conditions', false))) return // remove number from end of name, because it's better to see "Frightened (-3)" than "Frightened 3 (-3)" const modNameNoNum = modName.match(/.* \d+/) ? modName.substring(0, modName.lastIndexOf(' ')) : modName const modValStr = (modVal < 0 ? '' : '+') + modVal dcFlavorSuffixHtmls.push( `${modNameNoNum} ${modValStr}`) }) const dcFlavorSuffix = dcFlavorSuffixHtmls.join(', ') $editedFlavor.find('.pf2emm-suffix').remove() if (dcFlavorSuffix) { // dcActorType is only used to make the string slightly more fitting const dcActorType = targetedActor ? 'target' : isSpell ? 'caster' : 'actor' insertDcFlavorSuffix($editedFlavor, dcFlavorSuffix, dcActorType) } // newFlavor will be the inner HTML without the artificial div const newFlavor = $editedFlavor.html() if (newFlavor !== oldFlavor) { data.flavor = newFlavor // just in case other hooks rely on it await chatMessage.updateSource({ 'flavor': newFlavor }) } // hook call - to allow other modules/macros to trigger based on MM if (significantModifiers.length > 0) { Hooks.callAll('modifiersMatter', { rollingActor, actorWithDc, // can be undefined targetedToken, // can be undefined significantModifiers, // list of: {name: string, value: number, significance: string} chatMessage, }) } return true } const exampleHookInspireCourage = () => { // this hook call is an example! // it will play a nice chime sound each time an Inspire Courage effect turns a miss into a hit (or hit to crit) Hooks.on('modifiersMatter', ({ rollingActor, significantModifiers }) => { console.log(`${rollingActor} was helped!`) significantModifiers.forEach(({ name, significance }) => { if (name.includes('Inspire Courage') && significance === 'ESSENTIAL') { AudioHelper.play({ src: 'https://cdn.pixabay.com/audio/2022/01/18/audio_8db1f1b5a5.mp3', volume: 1.0, autoplay: true, loop: false, }, true) } }) }) } const getSetting = (settingName) => game.settings.get(MODULE_ID, settingName) Hooks.on('init', function () { game.settings.register(MODULE_ID, 'show-defense-highlights-to-everyone', { name: `${MODULE_ID}.Settings.show-defense-highlights-to-everyone.name`, hint: `${MODULE_ID}.Settings.show-defense-highlights-to-everyone.hint`, scope: 'world', config: true, default: true, type: Boolean, }) game.settings.register(MODULE_ID, 'ignore-crit-fail-over-fail-on-attacks', { name: `${MODULE_ID}.Settings.ignore-crit-fail-over-fail-on-attacks.name`, hint: `${MODULE_ID}.Settings.ignore-crit-fail-over-fail-on-attacks.hint`, scope: 'client', config: true, default: false, type: Boolean, }) game.settings.register(MODULE_ID, 'additional-ignored-labels', { name: `${MODULE_ID}.Settings.additional-ignored-labels.name`, hint: `${MODULE_ID}.Settings.additional-ignored-labels.hint`, scope: 'world', config: true, default: 'Example;Skill Potency', type: String, onChange: initializeIgnoredModifiers, }) game.settings.register(MODULE_ID, 'always-show-defense-conditions', { name: `${MODULE_ID}.Settings.always-show-defense-conditions.name`, hint: `${MODULE_ID}.Settings.always-show-defense-conditions.hint`, scope: 'world', config: true, default: false, type: Boolean, }) }) Hooks.once('setup', function () { Hooks.on('preCreateChatMessage', hook_preCreateChatMessage) initializeIgnoredModifiers() console.info(`${MODULE_ID} | initialized`) })