Added functionality

This commit is contained in:
WBHarry 2026-04-16 00:37:26 +02:00
parent 8808e4646d
commit 5e608dea99
12 changed files with 219 additions and 77 deletions

View file

@ -161,7 +161,12 @@
"rangeDependence": { "rangeDependence": {
"title": "Range Dependence" "title": "Range Dependence"
}, },
"stacking": { "title": "Stacking" } "stacking": { "title": "Stacking" },
"area": {
"title": "Area",
"shape": "Shape",
"size": "Size"
}
}, },
"RangeDependance": { "RangeDependance": {
"hint": "Settings for an optional distance at which this effect should activate", "hint": "Settings for an optional distance at which this effect should activate",
@ -1967,6 +1972,9 @@
"passive": "Passive", "passive": "Passive",
"temporary": "Temporary" "temporary": "Temporary"
}, },
"AreaTypes": {
"placed": { "name": "Placed Area" }
},
"Types": { "Types": {
"damage": { "damage": {
"name": "Damage" "name": "Damage"

View file

@ -5,6 +5,7 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac
super(options); super(options);
this.changeChoices = DhActiveEffectConfig.getChangeChoices(); this.changeChoices = DhActiveEffectConfig.getChangeChoices();
this.areaDaggerheartRange = true;
} }
static DEFAULT_OPTIONS = { static DEFAULT_OPTIONS = {
@ -162,6 +163,14 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac
htmlElement htmlElement
.querySelector('.armor-damage-thresholds-checkbox') .querySelector('.armor-damage-thresholds-checkbox')
?.addEventListener('change', this.armorDamageThresholdToggle.bind(this)); ?.addEventListener('change', this.armorDamageThresholdToggle.bind(this));
htmlElement
.querySelector('.area-checkbox')
?.addEventListener('change', this.areaToggle.bind(this));
htmlElement
.querySelector('.area-range-type-input')
?.addEventListener('change', this.areaRangeTypeToggle.bind(this));
} }
async _prepareContext(options) { async _prepareContext(options) {
@ -196,6 +205,8 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac
label: _loc(`EFFECT.DURATION.UNITS.${value}`), label: _loc(`EFFECT.DURATION.UNITS.${value}`),
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
})); }));
partContext.areaDaggerheartRange = this.areaDaggerheartRange;
partContext.templateRanges = CONFIG.DH.GENERAL.templateRanges;
break; break;
case 'changes': case 'changes':
const singleTypes = ['armor']; const singleTypes = ['armor'];
@ -260,6 +271,28 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac
return this.submit({ updateData: { system: { changes } } }); return this.submit({ updateData: { system: { changes } } });
} }
areaToggle(event) {
const submitData = this._processFormData(null, this.form, new FormDataExtended(this.form));
if(event.target.checked) {
const fields = game.system.api.data.activeEffects.BaseEffect._schema.fields.area.fields;
submitData.system.area = {
type: fields.type.initial,
shape: fields.shape.initial,
size: CONFIG.DH.GENERAL.range.veryClose.id,
};
} else {
submitData.system.area = null;
}
return this.submit({ updateData: { system: { area: submitData.system.area } } });
}
areaRangeTypeToggle(_event) {
this.areaDaggerheartRange = !this.areaDaggerheartRange;
this.render();
}
/** @inheritdoc */ /** @inheritdoc */
_renderChange(context) { _renderChange(context) {
const { change, index, defaultPriority } = context; const { change, index, defaultPriority } = context;

View file

@ -95,4 +95,48 @@ export default class DhRegionLayer extends foundry.canvas.layers.RegionLayer {
}); });
return inBounds.length === 1 ? inBounds[0] : null; return inBounds.length === 1 ? inBounds[0] : null;
} }
static getTemplateShape({ type, angle, range, direction } = {}) {
const { line, rectangle, inFront, cone } = CONFIG.DH.GENERAL.templateTypes;
const usedAngle =
type === cone.id ? (angle ?? CONFIG.MeasuredTemplate.defaults.angle) : type === inFront.id ? '180' : undefined;
const { grid, distance } = CONFIG.Scene.documentClass.schema.fields.grid.fields;
const sceneGridSize = canvas.scene?.grid.size ?? grid.size.initial;
const sceneGridDistance = canvas.scene?.grid.distance ?? distance.getInitialValue();
const dimensionConstant = sceneGridSize / sceneGridDistance;
const rangeNumber = Number(range);
const settings = canvas.scene?.rangeSettings;
const baseDistance = (!Number.isNaN(rangeNumber) ? rangeNumber : (settings ? settings[range] : 0)) * dimensionConstant;
const length = baseDistance;
const radius = length;
const shapeWidth = type === line.id ? 5 * dimensionConstant : type === rectangle.id ? length : undefined;
const shapeType = type === inFront.id ? cone.id : type;
const { width, height } = game.canvas.scene.dimensions;
return {
x: width / 2,
y: height / 2,
base: {
type: 'token',
x: 0,
y: 0,
width: 1,
height: 1,
shape: game.canvas.grid.isHexagonal ? CONST.TOKEN_SHAPES.ELLIPSE_1 : CONST.TOKEN_SHAPES.RECTANGLE_1
},
t: shapeType,
length: length,
width: shapeWidth,
height: length,
angle: usedAngle,
radius: radius,
direction: direction ?? 0,
type: shapeType
};
}
} }

View file

@ -62,3 +62,10 @@ export const effectTypes = {
} }
} }
}; };
export const areaTypes = {
placed: {
id: 'placed',
label: 'DAGGERHEART.EFFECTS.AreaTypes.placed.name'
}
};

View file

@ -22,13 +22,6 @@ export const ruleChoice = {
}; };
export const templateRanges = { export const templateRanges = {
self: {
id: 'self',
short: 's',
label: 'DAGGERHEART.CONFIG.Range.self.name',
description: 'DAGGERHEART.CONFIG.Range.self.description',
distance: 0
},
melee: { melee: {
id: 'melee', id: 'melee',
short: 'm', short: 'm',
@ -80,12 +73,30 @@ export const groupAttackRange = {
/* circle|cone|rect|ray used to be CONST.MEASURED_TEMPLATE_TYPES. Hardcoded for now */ /* circle|cone|rect|ray used to be CONST.MEASURED_TEMPLATE_TYPES. Hardcoded for now */
export const templateTypes = { export const templateTypes = {
CIRCLE: 'circle', circle: {
CONE: 'cone', id: 'circle',
RECTANGLE: 'rectangle', label: 'Circle'
LINE: 'line', },
EMANATION: 'emanation', cone: {
INFRONT: 'inFront' id: 'cone',
label: 'Cone'
},
rectangle: {
id: 'rectangle',
label: 'Rectangle'
},
line: {
id: 'line',
label: 'Line'
},
emanation: {
id: 'emanation',
label: 'Emanation'
},
inFront: {
id: 'inFront',
label: 'In Front'
}
}; };
export const rangeInclusion = { export const rangeInclusion = {

View file

@ -93,7 +93,26 @@ export default class BaseEffect extends foundry.data.ActiveEffectTypeDataModel {
max: new fields.NumberField({ integer: true, label: 'DAGGERHEART.GENERAL.max' }) max: new fields.NumberField({ integer: true, label: 'DAGGERHEART.GENERAL.max' })
}, },
{ nullable: true, initial: null } { nullable: true, initial: null }
) ),
area: new fields.SchemaField({
type: new fields.StringField({
nullable: false,
choices: CONFIG.DH.EFFECTS.areaTypes,
initial: CONFIG.DH.EFFECTS.areaTypes.placed.id,
label: 'DAGGERHEART.GENERAL.type'
}),
shape: new fields.StringField({
nullable: false,
choices: CONFIG.DH.GENERAL.templateTypes,
initial: CONFIG.DH.GENERAL.templateTypes.circle.id,
label: 'DAGGERHEART.ACTIVEEFFECT.Config.area.shape'
}),
size: new fields.StringField({
nullable: false,
initial: CONFIG.DH.GENERAL.range.veryClose.id,
label: 'DAGGERHEART.ACTIVEEFFECT.Config.area.size'
}),
}, { nullable: true, initial: null })
}; };
} }

View file

@ -45,10 +45,18 @@ export default class EffectsField extends fields.ArrayField {
* @param {object[]} targets Array of formatted targets * @param {object[]} targets Array of formatted targets
*/ */
static async applyEffects(targets) { static async applyEffects(targets) {
if (!this.effects?.length || !targets?.length) return; if (!this.effects?.length) return;
let effects = this.effects.map(e => (this.item.applyEffects ?? this.item.effects).get(e._id));
const targettingRequired = effects.some(x => x.system.area?.type !== CONFIG.DH.EFFECTS.areaTypes.placed.id);
if (targettingRequired && !targets?.length)
return ui.notifications.info(game.i18n.localize('DAGGERHEART.UI.Notifications.noTargetsSelectedOrPerm'));;
for(const effect of effects.filter(effect => effect.system.area?.type === CONFIG.DH.EFFECTS.areaTypes.placed.id)) {
await EffectsField.placeEffectRegion(effect);
}
const conditions = CONFIG.DH.GENERAL.conditions(); const conditions = CONFIG.DH.GENERAL.conditions();
let effects = this.effects;
const messageTargets = []; const messageTargets = [];
targets.forEach(async baseToken => { targets.forEach(async baseToken => {
if (this.hasSave && baseToken.saved.success === true) effects = this.effects.filter(e => e.onSave === true); if (this.hasSave && baseToken.saved.success === true) effects = this.effects.filter(e => e.onSave === true);
@ -72,20 +80,24 @@ export default class EffectsField extends fields.ArrayField {
: null : null
}); });
effects.forEach(async e => { effects.forEach(async effect => {
const effect = (this.item.applyEffects ?? this.item.effects).get(e._id);
if (!token.actor || !effect) return; if (!token.actor || !effect) return;
if (effect.system.area?.type !== CONFIG.DH.EFFECTS.areaTypes.placed.id)
await EffectsField.applyEffect(effect, token.actor); await EffectsField.applyEffect(effect, token.actor);
}); });
}); });
if (messageTargets.length === 0) return; if (targettingRequired && messageTargets.length === 0)
return ui.notifications.info(game.i18n.localize('DAGGERHEART.UI.Notifications.noTargetsSelectedOrPerm'));;
const summaryMessageSettings = game.settings.get( const summaryMessageSettings = game.settings.get(
CONFIG.DH.id, CONFIG.DH.id,
CONFIG.DH.SETTINGS.gameSettings.Automation CONFIG.DH.SETTINGS.gameSettings.Automation
).summaryMessages; ).summaryMessages;
if (!summaryMessageSettings.effects) return; const appliedEffects = effects
.filter(e => e.system.area?.type !== CONFIG.DH.EFFECTS.areaTypes.placed.id);
if (!summaryMessageSettings.effects || !appliedEffects.length) return;
const cls = getDocumentClass('ChatMessage'); const cls = getDocumentClass('ChatMessage');
const msg = { const msg = {
@ -96,7 +108,7 @@ export default class EffectsField extends fields.ArrayField {
content: await foundry.applications.handlebars.renderTemplate( content: await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/ui/chat/effectSummary.hbs', 'systems/daggerheart/templates/ui/chat/effectSummary.hbs',
{ {
effects: this.effects.map(e => (this.item.applyEffects ?? this.item.effects).get(e._id)), effects: appliedEffects,
targets: messageTargets targets: messageTargets
} }
) )
@ -120,6 +132,34 @@ export default class EffectsField extends fields.ArrayField {
await ActiveEffect.implementation.create(effectData, { parent: actor }); await ActiveEffect.implementation.create(effectData, { parent: actor });
} }
/**
*
*/
static async placeEffectRegion(effect) {
const { shape: type, size: range } = effect.system.area;
const shapeData = CONFIG.Canvas.layers.regions.layerClass.getTemplateShape({ type, range });
await canvas.regions.placeRegion(
{
name: effect.name,
shapes: [shapeData],
restriction: { enabled: false, type: 'move', priority: 0 },
behaviors: [{
name: game.i18n.localize('TYPES.RegionBehavior.applyActiveEffect'),
type: 'applyActiveEffect',
system: {
effects: [effect.uuid]
}
}],
displayMeasurements: true,
locked: false,
ownership: { default: CONST.DOCUMENT_OWNERSHIP_LEVELS.NONE },
visibility: CONST.REGION_VISIBILITY.ALWAYS
},
{ create: true }
);
}
/** /**
* Return the automation setting for execute method for current user role * Return the automation setting for execute method for current user role
* @returns {boolean} If execute should be triggered automatically * @returns {boolean} If execute should be triggered automatically

View file

@ -243,8 +243,6 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage {
const targets = this.filterPermTargets(this.system.hitTargets), const targets = this.filterPermTargets(this.system.hitTargets),
config = foundry.utils.deepClone(this.system); config = foundry.utils.deepClone(this.system);
config.event = event; config.event = event;
if (targets.length === 0)
ui.notifications.info(game.i18n.localize('DAGGERHEART.UI.Notifications.noTargetsSelectedOrPerm'));
this.consumeOnSuccess(); this.consumeOnSuccess();
this.system.action?.workflow.get('effects')?.execute(config, targets, true); this.system.action?.workflow.get('effects')?.execute(config, targets, true);
} }

View file

@ -12,7 +12,7 @@ export default function DhTemplateEnricher(match, _options) {
)?.id )?.id
: params.range; : params.range;
if (!Object.values(CONFIG.DH.GENERAL.templateTypes).find(x => x === type) || !range) return match[0]; if (!CONFIG.DH.GENERAL.templateTypes[type] || !range) return match[0];
const label = game.i18n.localize(`DAGGERHEART.CONFIG.TemplateTypes.${type}`); const label = game.i18n.localize(`DAGGERHEART.CONFIG.TemplateTypes.${type}`);
const rangeDisplay = Number.isNaN(Number(range)) const rangeDisplay = Number.isNaN(Number(range))
@ -49,8 +49,6 @@ export default function DhTemplateEnricher(match, _options) {
} }
export const renderMeasuredTemplate = async event => { export const renderMeasuredTemplate = async event => {
const { LINE, RECTANGLE, INFRONT, CONE } = CONFIG.DH.GENERAL.templateTypes;
const button = event.currentTarget, const button = event.currentTarget,
type = button.dataset.type, type = button.dataset.type,
range = button.dataset.range, range = button.dataset.range,
@ -59,49 +57,16 @@ export const renderMeasuredTemplate = async event => {
if (!type || !range || !game.canvas.scene) return; if (!type || !range || !game.canvas.scene) return;
const usedType = type === 'inFront' ? 'cone' : type; const shapeData = CONFIG.Canvas.layers.regions.layerClass.getTemplateShape({
const usedAngle = type,
type === CONE ? (angle ?? CONFIG.MeasuredTemplate.defaults.angle) : type === INFRONT ? '180' : undefined; angle,
range,
let baseDistance = getTemplateDistance(range); direction,
});
const { grid, distance } = CONFIG.Scene.documentClass.schema.fields.grid.fields;
const sceneGridSize = canvas.scene?.grid.size ?? grid.size.initial;
const sceneGridDistance = canvas.scene?.grid.distance ?? distance.getInitialValue();
const dimensionConstant = sceneGridSize / sceneGridDistance;
baseDistance *= dimensionConstant;
const length = baseDistance;
const radius = length;
const shapeWidth = type === LINE ? 5 * dimensionConstant : type === RECTANGLE ? length : undefined;
const { width, height } = game.canvas.scene.dimensions;
const shapeData = {
x: width / 2,
y: height / 2,
base: {
type: 'token',
x: 0,
y: 0,
width: 1,
height: 1,
shape: game.canvas.grid.isHexagonal ? CONST.TOKEN_SHAPES.ELLIPSE_1 : CONST.TOKEN_SHAPES.RECTANGLE_1
},
t: usedType,
length: length,
width: shapeWidth,
height: length,
angle: usedAngle,
radius: radius,
direction: direction,
type: usedType
};
await canvas.regions.placeRegion( await canvas.regions.placeRegion(
{ {
name: usedType.capitalize(), name: type.capitalize(),
shapes: [shapeData], shapes: [shapeData],
restriction: { enabled: false, type: 'move', priority: 0 }, restriction: { enabled: false, type: 'move', priority: 0 },
behaviors: [], behaviors: [],
@ -113,11 +78,3 @@ export const renderMeasuredTemplate = async event => {
{ create: true } { create: true }
); );
}; };
const getTemplateDistance = range => {
const rangeNumber = Number(range);
if (!Number.isNaN(rangeNumber)) return rangeNumber;
const settings = canvas.scene?.rangeSettings;
return settings ? settings[range] : 0;
};

View file

@ -298,7 +298,6 @@
padding-top: 0; padding-top: 0;
padding-bottom: 4px; padding-bottom: 4px;
min-height: auto; min-height: auto;
row-gap: 0;
legend { legend {
display: flex; display: flex;

View file

@ -33,6 +33,8 @@
} }
.armor-change-container { .armor-change-container {
row-gap: 0;
header { header {
padding: 0; padding: 0;
left: -0.25rem; // TODO: Find why this header is offset 0.25rem to the right so this can be removed. left: -0.25rem; // TODO: Find why this header is offset 0.25rem to the right so this can be removed.

View file

@ -48,4 +48,28 @@
</div> </div>
</div> </div>
</fieldset> </fieldset>
<fieldset class="one-column optional">
<legend>
{{localize "DAGGERHEART.ACTIVEEFFECT.Config.area.title"}}
<input type="checkbox" class="area-checkbox" {{checked source.system.area}} />
</legend>
{{#if document.system.area}}
{{formGroup systemFields.area.fields.type value=source.system.area.type localize=true blank=false }}
{{formGroup systemFields.area.fields.shape value=source.system.area.shape localize=true blank=false }}
<div class="form-group">
<div class="form-fields">
{{#if areaDaggerheartRange}}
{{formGroup systemFields.area.fields.size value=source.system.area.size choices=templateRanges blank=false localize=true }}
{{else}}
{{formGroup systemFields.area.fields.size value=source.system.area.size localize=true }}
{{/if}}
<label>{{localize "Use Daggerheart Range"}}</label>
<input type="checkbox" class="area-range-type-input" {{checked areaDaggerheartRange}} />
</div>
</div>
{{/if}}
</fieldset>
</section> </section>