This commit is contained in:
WBHarry 2026-02-21 13:49:30 +01:00
parent 4aab5d315a
commit bafc53488f
11 changed files with 270 additions and 4 deletions

View file

@ -138,6 +138,9 @@
"Config": { "Config": {
"rangeDependence": { "rangeDependence": {
"title": "Range Dependence" "title": "Range Dependence"
},
"conditional": {
"title": "Conditional Application"
} }
}, },
"RangeDependance": { "RangeDependance": {
@ -684,6 +687,9 @@
} }
}, },
"CONFIG": { "CONFIG": {
"ActiveEffectConditionalTarget": {
"self": "Self"
},
"ActiveEffectDuration": { "ActiveEffectDuration": {
"temporary": "Temporary", "temporary": "Temporary",
"act": "Next Spotlight", "act": "Next Spotlight",
@ -1015,6 +1021,13 @@
"withinRange": "Within Range", "withinRange": "Within Range",
"outsideRange": "Outside Range" "outsideRange": "Outside Range"
}, },
"Comparator": {
"eq": "Equals",
"gt": "Greater Than",
"gte": "Greater Or Equals",
"lt": "Lesser Than",
"lte": "Lesser Or Equals"
},
"Condition": { "Condition": {
"deathMove": { "deathMove": {
"name": "Death Move", "name": "Death Move",
@ -2196,7 +2209,8 @@
"triggers": "Triggers", "triggers": "Triggers",
"deathMoves": "Deathmoves", "deathMoves": "Deathmoves",
"sources": "Sources", "sources": "Sources",
"packs": "Packs" "packs": "Packs",
"conditionals": "Conditionals"
}, },
"Tiers": { "Tiers": {
"singular": "Tier", "singular": "Tier",
@ -2224,6 +2238,7 @@
"basics": "Basics", "basics": "Basics",
"bonus": "Bonus", "bonus": "Bonus",
"burden": "Burden", "burden": "Burden",
"comparator": "Comparator",
"condition": "Condition", "condition": "Condition",
"continue": "Continue", "continue": "Continue",
"criticalSuccess": "Critical Success", "criticalSuccess": "Critical Success",

View file

@ -63,7 +63,11 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac
} }
static DEFAULT_OPTIONS = { static DEFAULT_OPTIONS = {
classes: ['daggerheart', 'sheet', 'dh-style'] classes: ['daggerheart', 'sheet', 'dh-style'],
actions: {
addConditional: DhActiveEffectConfig.#addConditional,
removeConditional: DhActiveEffectConfig.#removeConditional
}
}; };
static PARTS = { static PARTS = {
@ -71,6 +75,7 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac
tabs: { template: 'templates/generic/tab-navigation.hbs' }, tabs: { template: 'templates/generic/tab-navigation.hbs' },
details: { template: 'systems/daggerheart/templates/sheets/activeEffect/details.hbs', scrollable: [''] }, details: { template: 'systems/daggerheart/templates/sheets/activeEffect/details.hbs', scrollable: [''] },
settings: { template: 'systems/daggerheart/templates/sheets/activeEffect/settings.hbs' }, settings: { template: 'systems/daggerheart/templates/sheets/activeEffect/settings.hbs' },
conditionals: { template: 'systems/daggerheart/templates/sheets/activeEffect/conditionals.hbs' },
changes: { changes: {
template: 'systems/daggerheart/templates/sheets/activeEffect/changes.hbs', template: 'systems/daggerheart/templates/sheets/activeEffect/changes.hbs',
templates: ['systems/daggerheart/templates/sheets/activeEffect/change.hbs'], templates: ['systems/daggerheart/templates/sheets/activeEffect/change.hbs'],
@ -84,6 +89,11 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac
tabs: [ tabs: [
{ id: 'details', icon: 'fa-solid fa-book' }, { id: 'details', icon: 'fa-solid fa-book' },
{ id: 'settings', icon: 'fa-solid fa-bars', label: 'DAGGERHEART.GENERAL.Tabs.settings' }, { 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' } { id: 'changes', icon: 'fa-solid fa-gears' }
], ],
initial: 'details', 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 group: CONST.ACTIVE_EFFECT_TIME_DURATION_UNITS.includes(value) ? groups.time : groups.combat
})); }));
break; break;
case 'conditionals':
partContext.conditionals = this.document.system.conditionals.map(conditional => ({
...conditional,
...game.system.api.data.activeEffects.EffectConditional.getConditionalFieldUseage(conditional.type)
}));
break;
case 'changes': case 'changes':
const fields = this.document.system.schema.fields.changes.element.fields; const fields = this.document.system.schema.fields.changes.element.fields;
partContext.changes = await Promise.all( 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 */ /** @inheritDoc */
_onChangeForm(_formConfig, event) { _onChangeForm(_formConfig, event) {
if (foundry.utils.isElementInstanceOf(event.target, 'select') && event.target.name === 'system.duration.type') { 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'); if (event.target.value === 'temporary') durationDescription.classList.add('visible');
else durationDescription.classList.remove('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 */ /** @inheritDoc */

View file

@ -916,3 +916,48 @@ export const activeEffectDurations = {
label: 'DAGGERHEART.CONFIG.ActiveEffectDuration.custom' 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'
}
};

View file

@ -1,8 +1,9 @@
import BaseEffect from './baseEffect.mjs'; import BaseEffect from './baseEffect.mjs';
import BeastformEffect from './beastformEffect.mjs'; import BeastformEffect from './beastformEffect.mjs';
import HordeEffect from './hordeEffect.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 = { export const config = {
base: BaseEffect, base: BaseEffect,

View file

@ -1,3 +1,5 @@
import { EffectConditionals } from './effectConditional.mjs';
/** -- Changes Type Priorities -- /** -- Changes Type Priorities --
* - Base Number - * - Base Number -
* Custom: 0 * Custom: 0
@ -33,6 +35,7 @@ export default class BaseEffect extends foundry.data.ActiveEffectTypeDataModel {
priority: new fields.NumberField() priority: new fields.NumberField()
}) })
), ),
conditionals: new EffectConditionals(),
duration: new fields.SchemaField({ duration: new fields.SchemaField({
type: new fields.StringField({ type: new fields.StringField({
choices: CONFIG.DH.GENERAL.activeEffectDurations, choices: CONFIG.DH.GENERAL.activeEffectDurations,

View file

@ -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
};
}
}

View file

@ -26,6 +26,9 @@ export default class DhActiveEffect extends foundry.documents.ActiveEffect {
return isVaultSupressed || domainTouchedSupressed; return isVaultSupressed || domainTouchedSupressed;
} }
const conditionalSuspended = game.system.api.data.activeEffects.EffectConditionals.isConditionalSuspended(this);
if (conditionalSuspended) return true;
return super.isSuppressed; return super.isSuppressed;
} }

View file

@ -538,7 +538,7 @@ export function getIconVisibleActiveEffects(effects) {
const alwaysShown = effect.showIcon === CONST.ACTIVE_EFFECT_SHOW_ICON.ALWAYS; 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 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]; : [formulaOrTerms];
return terms.reduce((r, t) => r + (t.bonus ?? 0) + (t.diceQuantity ? (t.diceQuantity * (t.faces + 1)) / 2 : 0), 0); 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;
}
}

View file

@ -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 { .tab.changes {
gap: 0; gap: 0;
@ -32,4 +44,8 @@
} }
} }
} }
.form-group.not-visible {
display: none;
}
} }

View file

@ -0,0 +1,33 @@
<section class="tab{{#if tab.active}} active{{/if}}" data-group="{{tab.group}}" data-tab="{{tab.id}}">
<button type="button" data-action="addConditional">{{localize "Add Conditional"}}<i class="fa-solid fa-plus"></i></button>
{{#each conditionals as |conditional index|}}
<fieldset class="one-column">
<legend><a data-action="removeConditional" data-index="{{index}}"><i class="fa-solid fa-trash"></i></a></legend>
<div class="conditionals-container">
<div class="conditional-container">
{{!-- {{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 }}
<div class="form-group value {{#unless conditional.usesValue}}not-visible{{/unless}}">
<label>{{localize "EFFECT.FIELDS.changes.element.value.label"}}</label>
<div class="form-fields">
{{formInput ../systemFields.conditionals.element.fields.value value=conditional.value name=(concat "system.conditionals." index ".value") localize=true }}
</div>
</div>
<div class="form-group comparator {{#unless conditional.usesComparator}}not-visible{{/unless}}">
<label>{{localize "DAGGERHEART.GENERAL.comparator"}}</label>
<div class="form-fields">
{{formInput ../systemFields.conditionals.element.fields.comparator value=conditional.comparator name=(concat "system.conditionals." index ".comparator") localize=true }}
</div>
</div>
</div>
</div>
</fieldset>
{{/each}}
</section>

View file

@ -7,6 +7,7 @@
{{formGroup systemFields.rangeDependence.fields.target value=source.system.rangeDependence.target localize=true }} {{formGroup systemFields.rangeDependence.fields.target value=source.system.rangeDependence.target localize=true }}
{{formGroup systemFields.rangeDependence.fields.range value=source.system.rangeDependence.range localize=true }} {{formGroup systemFields.rangeDependence.fields.range value=source.system.rangeDependence.range localize=true }}
</fieldset> </fieldset>
<fieldset class="one-column"> <fieldset class="one-column">
<legend>{{localize "EFFECT.DURATION.Label"}}</legend> <legend>{{localize "EFFECT.DURATION.Label"}}</legend>