This commit is contained in:
WBHarry 2026-03-07 02:00:54 +01:00 committed by GitHub
commit 1d4786c341
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 485 additions and 61 deletions

View file

@ -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",
@ -2198,7 +2211,8 @@
"triggers": "Triggers",
"deathMoves": "Deathmoves",
"sources": "Sources",
"packs": "Packs"
"packs": "Packs",
"conditionals": "Conditionals"
},
"Tiers": {
"singular": "Tier",
@ -2226,6 +2240,7 @@
"basics": "Basics",
"bonus": "Bonus",
"burden": "Burden",
"comparator": "Comparator",
"condition": "Condition",
"continue": "Continue",
"criticalSuccess": "Critical Success",

View file

@ -8,7 +8,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 = {
@ -16,6 +20,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'],
@ -29,6 +34,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',
@ -185,6 +195,16 @@ 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)
}));
partContext.statusChoices = Object.values(CONFIG.statusEffects).map(x => ({
id: x.id,
label: x.name
}));
break;
case 'changes':
const fields = this.document.system.schema.fields.changes.element.fields;
partContext.changes = await Promise.all(
@ -226,6 +246,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') {
@ -237,6 +272,35 @@ 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 statusSelect = container.querySelector('.form-group.status-select');
const attributeAuto = container.querySelector('.form-group.attribute-auto');
if (event.target.value === CONFIG.DH.GENERAL.activeEffectConditionalType.status.id) {
statusSelect.classList.remove('not-visible');
attributeAuto.classList.add('not-visible');
} else {
statusSelect.classList.add('not-visible');
attributeAuto.classList.remove('not-visible');
}
statusSelect.querySelector('select').selectedIndex = '-1';
attributeAuto.querySelector('input').value = '';
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 */

View file

@ -917,3 +917,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'
}
};

View file

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

View file

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

View file

@ -0,0 +1,82 @@
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;
break;
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;
break;
}
}
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

@ -8,6 +8,9 @@ export default class DhActiveEffect extends foundry.documents.ActiveEffect {
/**@override */
get isSuppressed() {
const conditionalSuspended = game.system.api.data.activeEffects.EffectConditionals.isConditionalSuspended(this);
if (conditionalSuspended) return true;
// If this is a copied effect from an attachment, never suppress it
// (These effects have attachmentSource metadata)
if (this.flags?.daggerheart?.attachmentSource) {

View file

@ -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);
});
}
export async function getFeaturesHTMLData(features) {
@ -603,6 +603,28 @@ export function calculateExpectedValue(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;
}
}
export function parseRallyDice(value, effect) {
const legacyStartsWithPrefix = value.toLowerCase().startsWith('d');
const workingValue = legacyStartsWithPrefix ? value.slice(1) : value;

View file

@ -28,27 +28,36 @@
"type": "withinRange",
"target": "hostile",
"range": "melee"
}
},
"changes": [
{
"key": "system.rules.damageReduction.thresholdImmunities.minor",
"value": 1,
"priority": null,
"type": "override"
}
],
"duration": {
"type": ""
},
"conditionals": [
{
"type": "attribute",
"key": "system.resources.hitPoints.value",
"value": "@system.resources.hitPoints.max - 2",
"comparator": "gte",
"target": "self"
}
]
},
"_id": "UJTsJlnhi5Zi0XQ2",
"img": "systems/daggerheart/assets/icons/domains/domain-card/bone.png",
"changes": [
{
"key": "system.rules.damageReduction.thresholdImmunities.minor",
"mode": 5,
"value": "1",
"priority": null
}
],
"disabled": true,
"disabled": false,
"duration": {
"startTime": null,
"combat": null,
"seconds": null,
"rounds": null,
"turns": null,
"startRound": null,
"startTurn": null
"value": null,
"units": "seconds",
"expiry": null,
"expired": false
},
"description": "<p class=\"Body\">When you have 2 or fewer Hit Points unmarked, you dont take Minor damage.</p>",
"origin": null,
@ -60,6 +69,16 @@
"_stats": {
"compendiumSource": null
},
"start": {
"time": 0,
"combat": null,
"combatant": null,
"initiative": null,
"round": null,
"turn": null
},
"showIcon": 1,
"folder": null,
"_key": "!items.effects!zbxPl81kbWEegKQN.UJTsJlnhi5Zi0XQ2"
}
],

View file

@ -30,31 +30,40 @@
"type": "withinRange",
"target": "hostile",
"range": "melee"
}
},
"changes": [
{
"key": "system.bonuses.damage.physical.bonus",
"mode": 2,
"value": "@system.levelData.level.current",
"priority": null
},
{
"key": "system.bonuses.damage.magical.bonus",
"mode": 2,
"value": "@system.levelData.level.current",
"priority": null
}
],
"disabled": true,
"changes": [
{
"key": "system.bonuses.damage.physical.bonus",
"value": "@system.levelData.level.current",
"priority": null,
"type": "add"
},
{
"key": "system.bonuses.damage.magical.bonus",
"value": "@system.levelData.level.current",
"priority": null,
"type": "add"
}
],
"duration": {
"type": ""
},
"conditionals": [
{
"type": "status",
"key": "vulnerable",
"value": "",
"comparator": "eq",
"target": "self"
}
]
},
"disabled": false,
"duration": {
"startTime": null,
"combat": null,
"seconds": null,
"rounds": null,
"turns": null,
"startRound": null,
"startTurn": null
"value": null,
"units": "seconds",
"expiry": null,
"expired": false
},
"description": "<p>While you're Vulnerable, add your level to your damage rolls.</p>",
"tint": "#ffffff",
@ -64,6 +73,16 @@
"_stats": {
"compendiumSource": null
},
"start": {
"time": 0,
"combat": null,
"combatant": null,
"initiative": null,
"round": null,
"turn": null
},
"showIcon": 1,
"folder": null,
"_key": "!items.effects!uByM34yQlw38yf1V.HMx9uZ54mvMiH95x"
}
],

View file

@ -26,27 +26,36 @@
"type": "withinRange",
"target": "hostile",
"range": "melee"
}
},
"changes": [
{
"key": "system.evasion",
"value": "@system.proficiency",
"priority": 21,
"type": "add"
}
],
"duration": {
"type": ""
},
"conditionals": [
{
"type": "attribute",
"key": "system.resources.hope.value",
"value": "2",
"comparator": "gte",
"target": "self"
}
]
},
"_id": "0i7GVOvjH6bK5AUM",
"img": "icons/magic/defensive/barrier-shield-dome-blue-purple.webp",
"changes": [
{
"key": "system.evasion",
"mode": 2,
"value": "@system.proficiency",
"priority": 21
}
],
"disabled": false,
"duration": {
"startTime": null,
"combat": null,
"seconds": null,
"rounds": null,
"turns": null,
"startRound": null,
"startTurn": null
"value": null,
"units": "seconds",
"expiry": null,
"expired": false
},
"description": "<p>While you have at least 2 Hope, you add your Proficiency to your Evasion.</p>",
"origin": null,
@ -58,6 +67,16 @@
"_stats": {
"compendiumSource": null
},
"start": {
"time": 0,
"combat": null,
"combatant": null,
"initiative": null,
"round": null,
"turn": null
},
"showIcon": 1,
"folder": null,
"_key": "!items.effects!oirsCnN66GOlK3Fa.0i7GVOvjH6bK5AUM"
}
],

View file

@ -16,7 +16,72 @@
"artist": ""
}
},
"effects": [],
"effects": [
{
"name": "Rise To The Challenge",
"type": "base",
"system": {
"changes": [
{
"key": "system.rules.dualityRoll.defaultHopeDice",
"type": "upgrade",
"value": 20,
"priority": null,
"phase": "initial"
}
],
"conditionals": [
{
"type": "attribute",
"key": "system.resources.hitPoints.value",
"value": "@system.resources.hitPoints.max - 2",
"comparator": "gte",
"target": "self"
}
],
"duration": {
"description": "",
"type": ""
},
"rangeDependence": {
"enabled": false,
"type": "withinRange",
"target": "hostile",
"range": "melee"
}
},
"_id": "MA744uJrGMQCDywg",
"img": "icons/magic/control/debuff-energy-hold-levitate-yellow.webp",
"disabled": false,
"start": {
"time": 0,
"combat": null,
"combatant": null,
"initiative": null,
"round": null,
"turn": null
},
"duration": {
"value": null,
"units": "seconds",
"expiry": null,
"expired": false
},
"description": "<p>While you have 2 or fewer Hit Points unmarked, you can roll a d20 as your Hope Die.</p>",
"origin": null,
"tint": "#ffffff",
"transfer": true,
"statuses": [],
"showIcon": 1,
"folder": null,
"sort": 0,
"flags": {},
"_stats": {
"compendiumSource": null
},
"_key": "!items.effects!dcutk8RVOJ2sEkO1.MA744uJrGMQCDywg"
}
],
"sort": 0,
"ownership": {
"default": 0,

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

View file

@ -0,0 +1,50 @@
<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 }}
<div class="form-group status-select {{#unless (eq conditional.type 'status')}}not-visible{{/unless}}">
<label>{{localize "EFFECT.FIELDS.changes.element.key.label"}}</label>
<div class="form-fields">
<select name="{{concat "system.conditionals." index ".key"}}">
{{selectOptions ../statusChoices selected=conditional.key labelAttr="label" valueAttr="id" blank="" localize=true}}
</select>
</div>
</div>
<div class="form-group attribute-auto {{#unless (eq conditional.type 'attribute')}}not-visible{{/unless}}">
<label>{{localize "EFFECT.FIELDS.changes.element.key.label"}}</label>
<div class="form-fields">
<input type="text" class="effect-change-input" name="{{concat "system.conditionals." index ".key"}}" value="{{conditional.key}}" />
</div>
</div>
<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.range value=source.system.rangeDependence.range localize=true }}
</fieldset>
<fieldset class="one-column">
<legend>{{localize "EFFECT.DURATION.Label"}}</legend>