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 @@
+