Added daggerheart durations and auto expiration of them

This commit is contained in:
WBHarry 2026-02-04 13:01:17 +01:00
parent 115a31423e
commit 0ba17117ea
14 changed files with 183 additions and 42 deletions

View file

@ -659,6 +659,14 @@
}
},
"CONFIG": {
"ActiveEffectDuration": {
"temporary": "Temporary",
"scene": "Next Scene",
"shortRest": "Next Rest",
"longRest": "Next Long Rest",
"session": "Next Session",
"custom": "Custom"
},
"AdversaryTrait": {
"relentless": {
"name": "Relentless",
@ -2455,6 +2463,10 @@
"hint": "Automatically increase the GM's fear pool on a fear duality roll result."
},
"FIELDS": {
"autoExpireActiveEffects": {
"label": "Auto Expire Active Effects",
"hint": "Active Effects with set durations will automatically be removed when their durations are up"
},
"damageReductionRulesDefault": {
"label": "Damage Reduction Rules Default",
"hint": "Wether using armor and reductions has rules on by default"

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;
@ -260,6 +260,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

@ -122,6 +122,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(
@ -162,4 +173,17 @@ 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');
}
}
}

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

@ -783,3 +783,30 @@ export const activeEffectModes = {
label: 'EFFECT.CHANGES.TYPES.override'
}
};
export const activeEffectDurations = {
temporary: {
id: 'temporary',
label: 'DAGGERHEART.CONFIG.ActiveEffectDuration.temporary'
},
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,28 @@ export function 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 refreshIsAllowed(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);

View file

@ -1,4 +1,28 @@
.application.sheet.daggerheart.dh-style.active-effect-config {
.custom-duration-section {
width: 100%;
display: flex;
flex-direction: column;
gap: 10px;
overflow: hidden;
height: 0;
transition: height ease-in-out 0.3s;
&.visible {
height: auto;
}
}
.duration-description {
height: 0;
overflow: hidden;
transition: height ease-in-out 0.3s;
&.visible {
height: 100px;
}
}
.tab.changes {
gap: 0;

View file

@ -42,5 +42,12 @@
color: @golden;
font-size: 12px;
}
.duration-container {
display: flex;
flex-direction: column;
text-align: center;
margin-top: 0.5rem;
}
}
}

View file

@ -14,6 +14,7 @@
{{formGroup settingFields.schema.fields.summaryMessages.fields.effects value=settingFields._source.summaryMessages.effects localize=true}}
</div>
{{formGroup settingFields.schema.fields.autoExpireActiveEffects value=settingFields._source.autoExpireActiveEffects localize=true}}
{{formGroup settingFields.schema.fields.countdownAutomation value=settingFields._source.countdownAutomation localize=true}}
{{formGroup settingFields.schema.fields.actionPoints value=settingFields._source.actionPoints localize=true}}
{{formGroup settingFields.schema.fields.hordeDamage value=settingFields._source.hordeDamage localize=true}}

View file

@ -7,47 +7,31 @@
{{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>
{{!--
{{#if start}}
<fieldset class="start-data">
<legend>{{localize "EFFECT.START.Header"}}</legend>
<div class="form-group">
<span class="label">{{localize "EFFECT.FIELDS.start.time.label"}}</span>
<div class="form-fields">{{start.time}}</div>
</div>
{{#if start.combat}}
<div class="form-group">
<span class="label">{{localize "DOCUMENT.Combat"}}</span>
<div class="form-fields">
{{localize "EFFECT.START.Combat" round=start.round turn=start.turn}}
</div>
</div>
{{#if start.combatant}}
<div class="form-group">
<span class="label">{{localize "DOCUMENT.Combatant"}}</span>
<div class="form-fields">
{{localize "EFFECT.START.Combatant" combatant=start.combatantName initiative=start.combatantInitiative}}
</div>
</div>
{{/if}}
{{/if}}
</fieldset>
{{/if}} --}}
<fieldset class="one-column">
<legend>{{localize "EFFECT.DURATION.Label"}}</legend>
{{!-- <fieldset>
<legend>{{localize "EFFECT.DURATION.Header"}}</legend>
<div class="form-group slim duration" data-duration>
<label class="flexrow" for="{{rootId}}-duration.value">
<span>{{localize "EFFECT.DURATION.Label"}}</span>
<span class="warning" hidden><i class="fa-duotone fa-triangle-exclamation"></i></span>
</label>
<div class="form-fields">
{{formInput fields.duration.fields.value value=source.duration.value id=(concat rootId "-duration.value")
aria=(object label=(localize "EFFECT.FIELDS.duration.value.label"))}}
{{formInput fields.duration.fields.units value=source.duration.units id=(concat rootId "-duration.units")
options=durationUnits aria=(object label=(localize "EFFECT.FIELDS.duration.units.label"))}}
{{formGroup systemFields.duration.fields.type value=source.system.duration.type localize=true }}
<div class="form-group slim duration-description">
<div class="form-fields">
{{formInput systemFields.duration.fields.description value=source.system.duration.description localize=true }}
</div>
</div>
<div class="custom-duration-section">
{{formGroup fields.start.fields.time value=source.start.time localize=true }}
<div class="form-group slim duration" data-duration>
<label class="flexrow" for="{{rootId}}-duration.value">
<span>{{localize "EFFECT.DURATION.Label"}}</span>
<span class="warning" hidden><i class="fa-duotone fa-triangle-exclamation"></i></span>
</label>
<div class="form-fields">
{{formInput fields.duration.fields.value value=source.duration.value id=(concat rootId "-duration.value")
aria=(object label=(localize "EFFECT.FIELDS.duration.value.label"))}}
{{formInput fields.duration.fields.units value=source.duration.units id=(concat rootId "-duration.units")
options=durationUnits aria=(object label=(localize "EFFECT.FIELDS.duration.units.label")) localize=true }}
</div>
</div>
</div>
{{formGroup fields.duration.fields.expiry choices=expiryEvents value=source.duration.expiry rootId=rootId}}
</fieldset> --}}
</fieldset>
</section>

View file

@ -16,6 +16,13 @@
</div>
{{/if}}
{{#if effect.system.duration.type}}
<div class="duration-container">
<strong>{{localize "EFFECT.DURATION.Label"}}</strong>
<span>{{localize (concat "DAGGERHEART.CONFIG.ActiveEffectDuration." effect.system.duration.type )}}</span>
</div>
{{/if}}
{{#unless effect.isLockedCondition}}
<p class="close-hint">
<i class="fa-solid fa-computer-mouse"></i> {{localize "DAGGERHEART.UI.EffectsDisplay.removeThing" thing=(localize "DAGGERHEART.GENERAL.Effect.single")}}