[V14] 1604 - ActiveEffect Durations (#1634)

* Added daggerheart durations and auto expiration of them

* Added duration to all tier1 adversaries

* Finished all adversaries and environments

* Remaining compendiums updated

* Improved styling of duration in tooltips

* .
This commit is contained in:
WBHarry 2026-02-17 18:57:03 +01:00 committed by GitHub
parent e2eb31c12e
commit 4aab5d315a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
120 changed files with 2514 additions and 1256 deletions

View file

@ -1,4 +1,4 @@
import { refreshIsAllowed } from '../../helpers/utils.mjs';
import { expireActiveEffects, refreshIsAllowed } from '../../helpers/utils.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
@ -264,6 +264,8 @@ export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV
await feature.update({ 'system.resource.value': resetValue });
}
expireActiveEffects(this.actor, [this.shortRest ? 'shortRest' : 'longRest']);
this.close();
} else {
this.render();

View file

@ -166,6 +166,17 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac
}));
}
break;
case 'settings':
const groups = {
time: _loc('EFFECT.DURATION.UNITS.GROUPS.time'),
combat: _loc('EFFECT.DURATION.UNITS.GROUPS.combat')
};
partContext.durationUnits = CONST.ACTIVE_EFFECT_DURATION_UNITS.map(value => ({
value,
label: _loc(`EFFECT.DURATION.UNITS.${value}`),
group: CONST.ACTIVE_EFFECT_TIME_DURATION_UNITS.includes(value) ? groups.time : groups.combat
}));
break;
case 'changes':
const fields = this.document.system.schema.fields.changes.element.fields;
partContext.changes = await Promise.all(
@ -206,4 +217,26 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac
)
);
}
/** @inheritDoc */
_onChangeForm(_formConfig, event) {
if (foundry.utils.isElementInstanceOf(event.target, 'select') && event.target.name === 'system.duration.type') {
const durationSection = this.element.querySelector('.custom-duration-section');
if (event.target.value === 'custom') durationSection.classList.add('visible');
else durationSection.classList.remove('visible');
const durationDescription = this.element.querySelector('.duration-description');
if (event.target.value === 'temporary') durationDescription.classList.add('visible');
else durationDescription.classList.remove('visible');
}
}
/** @inheritDoc */
_processFormData(event, form, formData) {
const submitData = super._processFormData(event, form, formData);
if (submitData.start && !submitData.start.time) submitData.start.time = '0';
else if (!submitData) submitData.start = null;
return submitData;
}
}

View file

@ -1,4 +1,4 @@
import { refreshIsAllowed } from '../../../helpers/utils.mjs';
import { expireActiveEffects, refreshIsAllowed } from '../../../helpers/utils.mjs';
const { HandlebarsApplicationMixin } = foundry.applications.api;
const { AbstractSidebarTab } = foundry.applications.sidebar;
@ -58,6 +58,8 @@ export default class DaggerheartMenu extends HandlebarsApplicationMixin(Abstract
const refreshedActors = {};
for (let actor of game.actors) {
if (['character', 'adversary'].includes(actor.type) && actor.prototypeToken.actorLink) {
expireActiveEffects(actor, types);
const updates = {};
for (let item of actor.items) {
if (item.system.metadata?.hasResource && refreshIsAllowed(types, item.system.resource?.recovery)) {

View file

@ -1,4 +1,5 @@
import { AdversaryBPPerEncounter } from '../../config/encounterConfig.mjs';
import { expireActiveEffects } from '../../helpers/utils.mjs';
export default class DhCombatTracker extends foundry.applications.sidebar.tabs.CombatTracker {
static DEFAULT_OPTIONS = {
@ -177,6 +178,8 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C
if (autoPoints) {
update.system.actionTokens = Math.max(combatant.system.actionTokens - 1, 0);
}
if (combatant.actor) expireActiveEffects(combatant.actor, [CONFIG.DH.GENERAL.activeEffectDurations.act.id]);
}
await this.viewed.update({

View file

@ -885,3 +885,34 @@ export const activeEffectModes = {
label: 'EFFECT.CHANGES.TYPES.override'
}
};
export const activeEffectDurations = {
temporary: {
id: 'temporary',
label: 'DAGGERHEART.CONFIG.ActiveEffectDuration.temporary'
},
act: {
id: 'act',
label: 'DAGGERHEART.CONFIG.ActiveEffectDuration.act'
},
scene: {
id: 'scene',
label: 'DAGGERHEART.CONFIG.ActiveEffectDuration.scene'
},
shortRest: {
id: 'shortRest',
label: 'DAGGERHEART.CONFIG.ActiveEffectDuration.shortRest'
},
longRest: {
id: 'longRest',
label: 'DAGGERHEART.CONFIG.ActiveEffectDuration.longRest'
},
session: {
id: 'session',
label: 'DAGGERHEART.CONFIG.ActiveEffectDuration.session'
},
custom: {
id: 'custom',
label: 'DAGGERHEART.CONFIG.ActiveEffectDuration.custom'
}
};

View file

@ -33,6 +33,14 @@ export default class BaseEffect extends foundry.data.ActiveEffectTypeDataModel {
priority: new fields.NumberField()
})
),
duration: new fields.SchemaField({
type: new fields.StringField({
choices: CONFIG.DH.GENERAL.activeEffectDurations,
blank: true,
label: 'DAGGERHEART.GENERAL.type'
}),
description: new fields.HTMLField({ label: 'DAGGERHEART.GENERAL.description' })
}),
rangeDependence: new fields.SchemaField({
enabled: new fields.BooleanField({
required: true,

View file

@ -192,6 +192,11 @@ export default class DhAutomation extends foundry.abstract.DataModel {
})
})
}),
autoExpireActiveEffects: new fields.BooleanField({
required: true,
initial: true,
label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.autoExpireActiveEffects.label'
}),
triggers: new fields.SchemaField({
enabled: new fields.BooleanField({
nullable: false,

View file

@ -50,6 +50,20 @@ export default class DhActiveEffect extends foundry.documents.ActiveEffect {
});
}
/**
* Whether this Active Effect is eligible to be registered with the {@link ActiveEffectRegistry}
*/
get isExpiryTrackable() {
return (
this.persisted &&
!this.inCompendium &&
this.modifiesActor &&
this.start &&
this.isTemporary &&
!this.isExpired
);
}
/* -------------------------------------------- */
/* Event Handlers */
/* -------------------------------------------- */

View file

@ -473,6 +473,8 @@ export async function waitForDiceSoNice(message) {
}
export function refreshIsAllowed(allowedTypes, typeToCheck) {
if (!allowedTypes) return true;
switch (typeToCheck) {
case CONFIG.DH.GENERAL.refreshTypes.scene.id:
case CONFIG.DH.GENERAL.refreshTypes.session.id:
@ -489,6 +491,34 @@ export function refreshIsAllowed(allowedTypes, typeToCheck) {
}
}
function expireActiveEffectIsAllowed(allowedTypes, typeToCheck) {
if (typeToCheck === CONFIG.DH.GENERAL.activeEffectDurations.act.id) return true;
return refreshIsAllowed(allowedTypes, typeToCheck);
}
export function expireActiveEffects(actor, allowedTypes = null) {
const shouldExpireEffects = game.settings.get(
CONFIG.DH.id,
CONFIG.DH.SETTINGS.gameSettings.Automation
).autoExpireActiveEffects;
if (!shouldExpireEffects) return;
const effectsToExpire = actor
.getActiveEffects()
.filter(effect => {
if (!effect.system?.duration.type) return false;
const { temporary, custom } = CONFIG.DH.GENERAL.activeEffectDurations;
if ([temporary.id, custom.id].includes(effect.system.duration.type)) return false;
return expireActiveEffectIsAllowed(allowedTypes, effect.system.duration.type);
})
.map(x => x.id);
actor.deleteEmbeddedDocuments('ActiveEffect', effectsToExpire);
}
export async function getCritDamageBonus(formula) {
const critRoll = new Roll(formula);
return critRoll.dice.reduce((acc, dice) => acc + dice.faces * dice.number, 0);
@ -503,6 +533,8 @@ export function htmlToText(html) {
export function getIconVisibleActiveEffects(effects) {
return effects.filter(effect => {
if (!(effect instanceof game.system.api.documents.DhActiveEffect)) return true;
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