[Feature] Active Effects toggle effects based in token distance (#452)

* Added the ability for effects to automatically activate/deactivate depending on range between tokens

* Fixed to use Foundry's measuring instead.

* .
This commit is contained in:
WBHarry 2025-07-30 15:57:06 +02:00 committed by GitHub
parent 18fac18df3
commit 46baef65a6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 162 additions and 29 deletions

View file

@ -235,3 +235,48 @@ Hooks.on('renderJournalDirectory', async (tab, html, _, options) => {
};
}
});
Hooks.on('moveToken', async (movedToken, data) => {
const effectsAutomation = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).effects;
if (!effectsAutomation.rangeDependent) return;
const rangeDependantEffects = movedToken.actor.effects.filter(effect => effect.system.rangeDependence.enabled);
const updateEffects = async (disposition, token, effects, effectUpdates) => {
const rangeMeasurement = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.RangeMeasurement);
for (let effect of effects.filter(x => x.system.rangeDependence.enabled)) {
const { target, range, type } = effect.system.rangeDependence;
if ((target === 'friendly' && disposition !== 1) || (target === 'hostile' && disposition !== -1))
return false;
const distanceBetween = canvas.grid.measurePath([
{ ...movedToken.toObject(), x: data.destination.x, y: data.destination.y },
token
]).distance;
const distance = rangeMeasurement[range];
const reverse = type === CONFIG.DH.GENERAL.rangeInclusion.outsideRange.id;
const newDisabled = reverse ? distanceBetween <= distance : distanceBetween > distance;
const oldDisabled = effectUpdates[effect.uuid] ? effectUpdates[effect.uuid].disabled : newDisabled;
effectUpdates[effect.uuid] = {
disabled: oldDisabled || newDisabled,
value: effect
};
}
};
const effectUpdates = {};
for (let token of game.scenes.find(x => x.active).tokens) {
if (token.id !== movedToken.id) {
await updateEffects(token.disposition, token, rangeDependantEffects, effectUpdates);
}
if (token.actor) await updateEffects(movedToken.disposition, token, token.actor.effects, effectUpdates);
}
for (let key in effectUpdates) {
const effect = effectUpdates[key];
await effect.value.update({ disabled: effect.disabled });
}
});

View file

@ -88,6 +88,17 @@
}
}
},
"ACTIVEEFFECT": {
"Config": {
"rangeDependence": {
"title": "Range Dependence"
}
},
"RangeDependance": {
"hint": "Settings for an optional distance at which this effect should activate",
"title": "Range Dependant"
}
},
"ACTORS": {
"Adversary": {
"FIELDS": {
@ -758,6 +769,10 @@
"oneHanded": "One-Handed",
"twoHanded": "Two-Handed"
},
"RangeInclusion": {
"withinRange": "Within Range",
"outsideRange": "Outside Range"
},
"Condition": {
"dead": {
"name": "Dead",
@ -2005,6 +2020,12 @@
"hordeDamage": {
"label": "Automatic Horde Damage",
"hint": "Automatically active horde effect to lower damage when reaching half or lower HP."
},
"effects": {
"rangeDependent": {
"label": "Effect Range Dependent",
"hint": "Effects with defined range dependency will automatically turn on/off depending on range"
}
}
}
},

View file

@ -27,7 +27,7 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac
header: { template: 'systems/daggerheart/templates/sheets/activeEffect/header.hbs' },
tabs: { template: 'templates/generic/tab-navigation.hbs' },
details: { template: 'systems/daggerheart/templates/sheets/activeEffect/details.hbs', scrollable: [''] },
duration: { template: 'systems/daggerheart/templates/sheets/activeEffect/duration.hbs' },
settings: { template: 'systems/daggerheart/templates/sheets/activeEffect/settings.hbs' },
changes: {
template: 'systems/daggerheart/templates/sheets/activeEffect/changes.hbs',
scrollable: ['ol[data-changes]']
@ -39,7 +39,7 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac
sheet: {
tabs: [
{ id: 'details', icon: 'fa-solid fa-book' },
{ id: 'duration', icon: 'fa-solid fa-clock' },
{ id: 'settings', icon: 'fa-solid fa-bars', label: 'DAGGERHEART.GENERAL.Tabs.settings' },
{ id: 'changes', icon: 'fa-solid fa-gears' }
],
initial: 'details',

View file

@ -43,25 +43,6 @@ export const actionTypes = {
}
};
export const targetTypes = {
self: {
id: 'self',
label: 'Self'
},
friendly: {
id: 'friendly',
label: 'Friendly'
},
hostile: {
id: 'hostile',
label: 'Hostile'
},
any: {
id: 'any',
label: 'Any'
}
};
export const damageOnSave = {
none: {
id: 'none',

View file

@ -43,6 +43,40 @@ export const range = {
}
};
export const rangeInclusion = {
withinRange: {
id: 'withinRange',
label: 'DAGGERHEART.CONFIG.RangeInclusion.withinRange'
},
outsideRange: {
id: 'outsideRange',
label: 'DAGGERHEART.CONFIG.RangeInclusion.outsideRange'
}
};
export const otherTargetTypes = {
friendly: {
id: 'friendly',
label: 'Friendly'
},
hostile: {
id: 'hostile',
label: 'Hostile'
},
any: {
id: 'any',
label: 'Any'
}
};
export const targetTypes = {
self: {
id: 'self',
label: 'Self'
},
...otherTargetTypes
};
export const burden = {
oneHanded: {
value: 'oneHanded',

View file

@ -1,7 +1,9 @@
import BaseEffect from './baseEffect.mjs';
import BeastformEffect from './beastformEffect.mjs';
export { BeastformEffect };
export { BaseEffect, BeastformEffect };
export const config = {
base: BaseEffect,
beastform: BeastformEffect
};

View file

@ -0,0 +1,33 @@
export default class BaseEffect extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
rangeDependence: new fields.SchemaField({
enabled: new fields.BooleanField({
required: true,
initial: false,
label: 'DAGGERHEART.GENERAL.enabled'
}),
type: new fields.StringField({
required: true,
choices: CONFIG.DH.GENERAL.rangeInclusion,
initial: CONFIG.DH.GENERAL.rangeInclusion.withinRange.id,
label: 'DAGGERHEART.GENERAL.type'
}),
target: new fields.StringField({
required: true,
choices: CONFIG.DH.GENERAL.otherTargetTypes,
initial: CONFIG.DH.GENERAL.otherTargetTypes.hostile.id,
label: 'DAGGERHEART.GENERAL.Target.single'
}),
range: new fields.StringField({
required: true,
choices: CONFIG.DH.GENERAL.range,
initial: CONFIG.DH.GENERAL.range.melee.id,
label: 'DAGGERHEART.GENERAL.range'
})
})
};
}
}

View file

@ -1,6 +1,7 @@
import { updateActorTokens } from '../../helpers/utils.mjs';
import BaseEffect from './baseEffect.mjs';
export default class BeastformEffect extends foundry.abstract.TypeDataModel {
export default class BeastformEffect extends BaseEffect {
static defineSchema() {
const fields = foundry.data.fields;
return {

View file

@ -4,8 +4,8 @@ export default class TargetField extends fields.SchemaField {
constructor(options = {}, context = {}) {
const targetFields = {
type: new fields.StringField({
choices: CONFIG.DH.ACTIONS.targetTypes,
initial: CONFIG.DH.ACTIONS.targetTypes.any.id,
choices: CONFIG.DH.GENERAL.targetTypes,
initial: CONFIG.DH.GENERAL.targetTypes.any.id,
nullable: true
}),
amount: new fields.NumberField({ nullable: true, initial: null, integer: true, min: 0 })
@ -16,11 +16,11 @@ export default class TargetField extends fields.SchemaField {
static prepareConfig(config) {
if (!this.target?.type) return [];
let targets;
if (this.target?.type === CONFIG.DH.ACTIONS.targetTypes.self.id)
if (this.target?.type === CONFIG.DH.GENERAL.targetTypes.self.id)
targets = [this.actor.token ?? this.actor.prototypeToken];
else {
targets = Array.from(game.user.targets);
if (this.target.type !== CONFIG.DH.ACTIONS.targetTypes.any.id) {
if (this.target.type !== CONFIG.DH.GENERAL.targetTypes.any.id) {
targets = targets.filter(t => TargetField.isTargetFriendly.call(this, t));
if (this.target.amount && targets.length > this.target.amount) targets = [];
}
@ -43,9 +43,9 @@ export default class TargetField extends fields.SchemaField {
: this.actor.prototypeToken.disposition,
targetDisposition = target.document.disposition;
return (
(this.target.type === CONFIG.DH.ACTIONS.targetTypes.friendly.id &&
(this.target.type === CONFIG.DH.GENERAL.targetTypes.friendly.id &&
actorDisposition === targetDisposition) ||
(this.target.type === CONFIG.DH.ACTIONS.targetTypes.hostile.id &&
(this.target.type === CONFIG.DH.GENERAL.targetTypes.hostile.id &&
actorDisposition + targetDisposition === 0)
);
}

View file

@ -23,6 +23,12 @@ export default class DhAutomation extends foundry.abstract.DataModel {
required: true,
initial: true,
label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.hordeDamage.label'
}),
effects: new fields.SchemaField({
rangeDependent: new fields.BooleanField({
initial: true,
label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.effects.rangeDependent.label'
})
})
};
}

View file

@ -10,6 +10,7 @@
</div>
{{formGroup settingFields.schema.fields.actionPoints value=settingFields._source.actionPoints localize=true}}
{{formGroup settingFields.schema.fields.hordeDamage value=settingFields._source.hordeDamage localize=true}}
{{formGroup settingFields.schema.fields.effects.fields.rangeDependent value=settingFields._source.effects.rangeDependent localize=true}}
<footer class="form-footer">
<button data-action="reset">

View file

@ -1,4 +1,13 @@
<section class="tab{{#if tab.active}} active{{/if}}" data-group="{{tab.group}}" data-tab="{{tab.id}}">
<fieldset class="one-column">
<legend>{{localize "DAGGERHEART.ACTIVEEFFECT.Config.rangeDependence.title"}}</legend>
{{formGroup document.system.schema.fields.rangeDependence.fields.enabled value=source.system.rangeDependence.enabled localize=true }}
{{formGroup document.system.schema.fields.rangeDependence.fields.type value=source.system.rangeDependence.type localize=true }}
{{formGroup document.system.schema.fields.rangeDependence.fields.target value=source.system.rangeDependence.target localize=true }}
{{formGroup document.system.schema.fields.rangeDependence.fields.range value=source.system.rangeDependence.range localize=true }}
</fieldset>
<fieldset class="one-column">
{{formGroup fields.duration.fields.seconds value=source.duration.seconds rootId=rootId}}
{{formGroup fields.duration.fields.startTime value=source.duration.startTime rootId=rootId}}