diff --git a/lang/en.json b/lang/en.json index 24d6168a..2b304688 100755 --- a/lang/en.json +++ b/lang/en.json @@ -138,6 +138,9 @@ "Config": { "rangeDependence": { "title": "Range Dependence" + }, + "conditional": { + "title": "Conditional Application" } }, "RangeDependance": { @@ -684,6 +687,9 @@ } }, "CONFIG": { + "ActiveEffectConditionalTarget": { + "self": "Self" + }, "ActiveEffectDuration": { "temporary": "Temporary", "act": "Next Spotlight", @@ -1015,6 +1021,13 @@ "withinRange": "Within Range", "outsideRange": "Outside Range" }, + "Comparator": { + "eq": "Equals", + "gt": "Greater Than", + "gte": "Greater Or Equals", + "lt": "Lesser Than", + "lte": "Lesser Or Equals" + }, "Condition": { "deathMove": { "name": "Death Move", @@ -2196,7 +2209,8 @@ "triggers": "Triggers", "deathMoves": "Deathmoves", "sources": "Sources", - "packs": "Packs" + "packs": "Packs", + "conditionals": "Conditionals" }, "Tiers": { "singular": "Tier", @@ -2224,6 +2238,7 @@ "basics": "Basics", "bonus": "Bonus", "burden": "Burden", + "comparator": "Comparator", "condition": "Condition", "continue": "Continue", "criticalSuccess": "Critical Success", diff --git a/module/applications/sheets-configs/activeEffectConfig.mjs b/module/applications/sheets-configs/activeEffectConfig.mjs index 75173f8d..fab2f122 100644 --- a/module/applications/sheets-configs/activeEffectConfig.mjs +++ b/module/applications/sheets-configs/activeEffectConfig.mjs @@ -63,7 +63,11 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac } static DEFAULT_OPTIONS = { - classes: ['daggerheart', 'sheet', 'dh-style'] + classes: ['daggerheart', 'sheet', 'dh-style'], + actions: { + addConditional: DhActiveEffectConfig.#addConditional, + removeConditional: DhActiveEffectConfig.#removeConditional + } }; static PARTS = { @@ -71,6 +75,7 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac tabs: { template: 'templates/generic/tab-navigation.hbs' }, details: { template: 'systems/daggerheart/templates/sheets/activeEffect/details.hbs', scrollable: [''] }, settings: { template: 'systems/daggerheart/templates/sheets/activeEffect/settings.hbs' }, + conditionals: { template: 'systems/daggerheart/templates/sheets/activeEffect/conditionals.hbs' }, changes: { template: 'systems/daggerheart/templates/sheets/activeEffect/changes.hbs', templates: ['systems/daggerheart/templates/sheets/activeEffect/change.hbs'], @@ -84,6 +89,11 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac tabs: [ { id: 'details', icon: 'fa-solid fa-book' }, { id: 'settings', icon: 'fa-solid fa-bars', label: 'DAGGERHEART.GENERAL.Tabs.settings' }, + { + id: 'conditionals', + icon: 'fa-solid fa-person-circle-question', + label: 'DAGGERHEART.GENERAL.Tabs.conditionals' + }, { id: 'changes', icon: 'fa-solid fa-gears' } ], initial: 'details', @@ -177,6 +187,12 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac group: CONST.ACTIVE_EFFECT_TIME_DURATION_UNITS.includes(value) ? groups.time : groups.combat })); break; + case 'conditionals': + partContext.conditionals = this.document.system.conditionals.map(conditional => ({ + ...conditional, + ...game.system.api.data.activeEffects.EffectConditional.getConditionalFieldUseage(conditional.type) + })); + break; case 'changes': const fields = this.document.system.schema.fields.changes.element.fields; partContext.changes = await Promise.all( @@ -218,6 +234,21 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac ); } + static #addConditional() { + const submitData = this._processFormData(null, this.form, new FormDataExtended(this.form)); + const conditionals = Object.values(submitData.system?.conditionals ?? {}); + conditionals.push(this.document.system.schema.fields.conditionals.element.getInitialValue()); + return this.submit({ updateData: { system: { conditionals } } }); + } + + static async #removeConditional(_event, button) { + const submitData = this._processFormData(null, this.form, new FormDataExtended(this.form)); + const conditionals = Object.values(submitData.system.conditionals); + const index = Number(button.dataset.index) || 0; + conditionals.splice(index, 1); + return this.submit({ updateData: { system: { conditionals: conditionals } } }); + } + /** @inheritDoc */ _onChangeForm(_formConfig, event) { if (foundry.utils.isElementInstanceOf(event.target, 'select') && event.target.name === 'system.duration.type') { @@ -229,6 +260,22 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac if (event.target.value === 'temporary') durationDescription.classList.add('visible'); else durationDescription.classList.remove('visible'); } + + if ( + foundry.utils.isElementInstanceOf(event.target, 'select') && + event.target.name.startsWith('system.conditionals') && + event.target.name.endsWith('type') + ) { + const container = event.target.closest('.conditional-container'); + const { usesValue, usesComparator } = + game.system.api.data.activeEffects.EffectConditional.getConditionalFieldUseage(event.target.value); + + if (usesValue) container.querySelector('.form-group.value').classList.remove('not-visible'); + else container.querySelector('.form-group.value').classList.add('not-visible'); + + if (usesComparator) container.querySelector('.form-group.comparator').classList.remove('not-visible'); + else container.querySelector('.form-group.comparator').classList.add('not-visible'); + } } /** @inheritDoc */ diff --git a/module/config/generalConfig.mjs b/module/config/generalConfig.mjs index 165342b4..733cb18a 100644 --- a/module/config/generalConfig.mjs +++ b/module/config/generalConfig.mjs @@ -916,3 +916,48 @@ export const activeEffectDurations = { label: 'DAGGERHEART.CONFIG.ActiveEffectDuration.custom' } }; + +export const activeEffectConditionalTarget = { + self: { + id: 'self', + label: 'DAGGERHEART.CONFIG.ActiveEffectConditionalTarget.self' + } + // target: { + // id: 'target', + // label: 'DAGGERHEART.CONFIG.ActiveEffectConditionalTarget.target' + // } +}; + +export const activeEffectConditionalType = { + attribute: { + id: 'attribute', + label: 'Attribute' + }, + status: { + id: 'status', + label: 'Status' + } +}; + +export const comparator = { + eq: { + id: 'eq', + label: 'DAGGERHEART.CONFIG.Comparator.eq' + }, + gt: { + id: 'gt', + label: 'DAGGERHEART.CONFIG.Comparator.gt' + }, + gte: { + id: 'gte', + label: 'DAGGERHEART.CONFIG.Comparator.gte' + }, + lt: { + id: 'lt', + label: 'DAGGERHEART.CONFIG.Comparator.lt' + }, + lte: { + id: 'lte', + label: 'DAGGERHEART.CONFIG.Comparator.lte' + } +}; diff --git a/module/data/activeEffect/_module.mjs b/module/data/activeEffect/_module.mjs index 1a50088a..9b4e3263 100644 --- a/module/data/activeEffect/_module.mjs +++ b/module/data/activeEffect/_module.mjs @@ -1,8 +1,9 @@ import BaseEffect from './baseEffect.mjs'; import BeastformEffect from './beastformEffect.mjs'; import HordeEffect from './hordeEffect.mjs'; +import { EffectConditionals, EffectConditional } from './effectConditional.mjs'; -export { BaseEffect, BeastformEffect, HordeEffect }; +export { BaseEffect, BeastformEffect, HordeEffect, EffectConditionals, EffectConditional }; export const config = { base: BaseEffect, diff --git a/module/data/activeEffect/baseEffect.mjs b/module/data/activeEffect/baseEffect.mjs index 98a961d7..d45ba15a 100644 --- a/module/data/activeEffect/baseEffect.mjs +++ b/module/data/activeEffect/baseEffect.mjs @@ -1,3 +1,5 @@ +import { EffectConditionals } from './effectConditional.mjs'; + /** -- Changes Type Priorities -- * - Base Number - * Custom: 0 @@ -33,6 +35,7 @@ export default class BaseEffect extends foundry.data.ActiveEffectTypeDataModel { priority: new fields.NumberField() }) ), + conditionals: new EffectConditionals(), duration: new fields.SchemaField({ type: new fields.StringField({ choices: CONFIG.DH.GENERAL.activeEffectDurations, diff --git a/module/data/activeEffect/effectConditional.mjs b/module/data/activeEffect/effectConditional.mjs new file mode 100644 index 00000000..6191176b --- /dev/null +++ b/module/data/activeEffect/effectConditional.mjs @@ -0,0 +1,80 @@ +import { compareValues, itemAbleRollParse } from '../../helpers/utils.mjs'; + +export class EffectConditionals extends foundry.data.fields.ArrayField { + constructor(context) { + super(new EffectConditional(), context); + } + + static isConditionalSuspended(effect) { + const actor = + effect.parent.type === 'character' + ? effect.parent + : effect.parent.parent?.type === 'character' + ? effect.parent.parent + : null; + if (!actor) return false; + + for (const conditional of effect.system.conditionals) { + switch (conditional.type) { + case CONFIG.DH.GENERAL.activeEffectConditionalType.status.id: + const hasStatus = Array.from(actor.allApplicableEffects()).some( + x => !x.disabled && x.statuses.has(conditional.key) + ); + if (!hasStatus) return true; + case CONFIG.DH.GENERAL.activeEffectConditionalType.attribute.id: + const actorValue = foundry.utils.getProperty(actor, conditional.key); + const conditionalValue = game.system.api.documents.DhActiveEffect.effectSafeEval( + itemAbleRollParse(conditional.value, actor) + ); + if (!compareValues(actorValue, conditionalValue, conditional.comparator)) return true; + } + } + + return false; + } +} + +export class EffectConditional extends foundry.data.fields.SchemaField { + constructor(context) { + const fields = foundry.data.fields; + + const schema = { + target: new fields.StringField({ + required: true, + choices: CONFIG.DH.GENERAL.activeEffectConditionalTarget, + initial: CONFIG.DH.GENERAL.activeEffectConditionalTarget.self.id, + label: 'DAGGERHEART.GENERAL.Target.single' + }), + type: new fields.StringField({ + required: true, + choices: CONFIG.DH.GENERAL.activeEffectConditionalType, + initial: CONFIG.DH.GENERAL.activeEffectConditionalType.status.id, + label: 'DAGGERHEART.GENERAL.type' + }), + key: new fields.StringField({ required: true, label: 'EFFECT.FIELDS.changes.element.key.label' }), + value: new fields.StringField({ nullable: true, label: 'EFFECT.FIELDS.changes.element.value.label' }), + comparator: new fields.StringField({ + choices: CONFIG.DH.GENERAL.comparator, + initial: CONFIG.DH.GENERAL.comparator.eq.id, + label: 'DAGGERHEART.GENERAL.comparator' + }) + }; + + super(schema, context); + } + + static getConditionalFieldUseage(conditionalType) { + let usesValue = false; + let usesComparator = false; + + if ([CONFIG.DH.GENERAL.activeEffectConditionalType.attribute.id].includes(conditionalType)) { + usesValue = true; + usesComparator = true; + } + + return { + usesValue, + usesComparator + }; + } +} diff --git a/module/documents/activeEffect.mjs b/module/documents/activeEffect.mjs index f8b19a3a..f407ac4d 100644 --- a/module/documents/activeEffect.mjs +++ b/module/documents/activeEffect.mjs @@ -26,6 +26,9 @@ export default class DhActiveEffect extends foundry.documents.ActiveEffect { return isVaultSupressed || domainTouchedSupressed; } + const conditionalSuspended = game.system.api.data.activeEffects.EffectConditionals.isConditionalSuspended(this); + if (conditionalSuspended) return true; + return super.isSuppressed; } diff --git a/module/helpers/utils.mjs b/module/helpers/utils.mjs index b1b74d9f..901463fd 100644 --- a/module/helpers/utils.mjs +++ b/module/helpers/utils.mjs @@ -538,7 +538,7 @@ export function getIconVisibleActiveEffects(effects) { const alwaysShown = effect.showIcon === CONST.ACTIVE_EFFECT_SHOW_ICON.ALWAYS; const conditionalShown = effect.showIcon === CONST.ACTIVE_EFFECT_SHOW_ICON.CONDITIONAL && !effect.transfer; // TODO: system specific logic - return !effect.disabled && (alwaysShown || conditionalShown); + return !effect.active && (alwaysShown || conditionalShown); }); } @@ -587,3 +587,25 @@ export function calculateExpectedValue(formulaOrTerms) { : [formulaOrTerms]; return terms.reduce((r, t) => r + (t.bonus ?? 0) + (t.diceQuantity ? (t.diceQuantity * (t.faces + 1)) / 2 : 0), 0); } + +/** + * + * @param {Number} valA The number being compared to a second one + * @param {Number} valB The number the first is being compared to + * @param {Comparator} comparator The type of comparison + * @returns { Boolean } Whether valA passes the comparison + */ +export function compareValues(valA, valB, comparator) { + switch (comparator) { + case CONFIG.DH.GENERAL.comparator.eq.id: + return valA === valB; + case CONFIG.DH.GENERAL.comparator.gt.id: + return valA > valB; + case CONFIG.DH.GENERAL.comparator.gte.id: + return valA >= valB; + case CONFIG.DH.GENERAL.comparator.lt.id: + return valA < valB; + case CONFIG.DH.GENERAL.comparator.lte.id: + return valA <= valB; + } +} diff --git a/styles/less/sheets/activeEffects/activeEffects.less b/styles/less/sheets/activeEffects/activeEffects.less index ba3ff43f..42f79456 100644 --- a/styles/less/sheets/activeEffects/activeEffects.less +++ b/styles/less/sheets/activeEffects/activeEffects.less @@ -23,6 +23,18 @@ } } + .conditionals-container { + display: flex; + flex-direction: column; + gap: 8px; + + .conditional-container { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 4px; + } + } + .tab.changes { gap: 0; @@ -32,4 +44,8 @@ } } } + + .form-group.not-visible { + display: none; + } } diff --git a/templates/sheets/activeEffect/conditionals.hbs b/templates/sheets/activeEffect/conditionals.hbs new file mode 100644 index 00000000..80fdf445 --- /dev/null +++ b/templates/sheets/activeEffect/conditionals.hbs @@ -0,0 +1,33 @@ +
+ + + {{#each conditionals as |conditional index|}} +
+ + +
+
+ {{!-- {{formGroup ../systemFields.conditionals.element.fields.target value=conditional.target name=(concat "system.conditionals." index ".target") localize=true }} --}} + {{formGroup ../systemFields.conditionals.element.fields.type value=conditional.type name=(concat "system.conditionals." index ".type") localize=true }} + {{formGroup ../systemFields.conditionals.element.fields.key value=conditional.key name=(concat "system.conditionals." index ".key") localize=true }} + +
+ + +
+ {{formInput ../systemFields.conditionals.element.fields.value value=conditional.value name=(concat "system.conditionals." index ".value") localize=true }} +
+
+ +
+ + +
+ {{formInput ../systemFields.conditionals.element.fields.comparator value=conditional.comparator name=(concat "system.conditionals." index ".comparator") localize=true }} +
+
+
+
+
+ {{/each}} +
\ No newline at end of file diff --git a/templates/sheets/activeEffect/settings.hbs b/templates/sheets/activeEffect/settings.hbs index 9443edfb..0bf55a00 100644 --- a/templates/sheets/activeEffect/settings.hbs +++ b/templates/sheets/activeEffect/settings.hbs @@ -7,6 +7,7 @@ {{formGroup systemFields.rangeDependence.fields.target value=source.system.rangeDependence.target localize=true }} {{formGroup systemFields.rangeDependence.fields.range value=source.system.rangeDependence.range localize=true }} +
{{localize "EFFECT.DURATION.Label"}}