diff --git a/lang/en.json b/lang/en.json index bc0cf05a..afc9bb2d 100755 --- a/lang/en.json +++ b/lang/en.json @@ -161,7 +161,12 @@ "rangeDependence": { "title": "Range Dependence" }, - "stacking": { "title": "Stacking" } + "stacking": { "title": "Stacking" }, + "area": { + "title": "Area", + "shape": "Shape", + "size": "Size" + } }, "RangeDependance": { "hint": "Settings for an optional distance at which this effect should activate", @@ -1967,6 +1972,9 @@ "passive": "Passive", "temporary": "Temporary" }, + "AreaTypes": { + "placed": { "name": "Placed Area" } + }, "Types": { "damage": { "name": "Damage" diff --git a/module/applications/sheets-configs/activeEffectConfig.mjs b/module/applications/sheets-configs/activeEffectConfig.mjs index 834a57a8..9e9a930e 100644 --- a/module/applications/sheets-configs/activeEffectConfig.mjs +++ b/module/applications/sheets-configs/activeEffectConfig.mjs @@ -5,6 +5,7 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac super(options); this.changeChoices = DhActiveEffectConfig.getChangeChoices(); + this.areaDaggerheartRange = true; } static DEFAULT_OPTIONS = { @@ -162,6 +163,14 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac htmlElement .querySelector('.armor-damage-thresholds-checkbox') ?.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) { @@ -196,6 +205,8 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac label: _loc(`EFFECT.DURATION.UNITS.${value}`), group: CONST.ACTIVE_EFFECT_TIME_DURATION_UNITS.includes(value) ? groups.time : groups.combat })); + partContext.areaDaggerheartRange = this.areaDaggerheartRange; + partContext.templateRanges = CONFIG.DH.GENERAL.templateRanges; break; case 'changes': const singleTypes = ['armor']; @@ -260,6 +271,28 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac 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 */ _renderChange(context) { const { change, index, defaultPriority } = context; diff --git a/module/canvas/placeables/regionLayer.mjs b/module/canvas/placeables/regionLayer.mjs index 81fd96db..44a5e630 100644 --- a/module/canvas/placeables/regionLayer.mjs +++ b/module/canvas/placeables/regionLayer.mjs @@ -95,4 +95,48 @@ export default class DhRegionLayer extends foundry.canvas.layers.RegionLayer { }); 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 + }; + } } diff --git a/module/config/effectConfig.mjs b/module/config/effectConfig.mjs index 97a03fad..5519de36 100644 --- a/module/config/effectConfig.mjs +++ b/module/config/effectConfig.mjs @@ -62,3 +62,10 @@ export const effectTypes = { } } }; + +export const areaTypes = { + placed: { + id: 'placed', + label: 'DAGGERHEART.EFFECTS.AreaTypes.placed.name' + } +}; diff --git a/module/config/generalConfig.mjs b/module/config/generalConfig.mjs index 58a9dff9..93807334 100644 --- a/module/config/generalConfig.mjs +++ b/module/config/generalConfig.mjs @@ -22,13 +22,6 @@ export const ruleChoice = { }; export const templateRanges = { - self: { - id: 'self', - short: 's', - label: 'DAGGERHEART.CONFIG.Range.self.name', - description: 'DAGGERHEART.CONFIG.Range.self.description', - distance: 0 - }, melee: { id: 'melee', short: 'm', @@ -80,12 +73,30 @@ export const groupAttackRange = { /* circle|cone|rect|ray used to be CONST.MEASURED_TEMPLATE_TYPES. Hardcoded for now */ export const templateTypes = { - CIRCLE: 'circle', - CONE: 'cone', - RECTANGLE: 'rectangle', - LINE: 'line', - EMANATION: 'emanation', - INFRONT: 'inFront' + circle: { + id: 'circle', + label: 'Circle' + }, + cone: { + 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 = { diff --git a/module/data/activeEffect/baseEffect.mjs b/module/data/activeEffect/baseEffect.mjs index bac50c56..2ddedc35 100644 --- a/module/data/activeEffect/baseEffect.mjs +++ b/module/data/activeEffect/baseEffect.mjs @@ -93,7 +93,26 @@ export default class BaseEffect extends foundry.data.ActiveEffectTypeDataModel { max: new fields.NumberField({ integer: true, label: 'DAGGERHEART.GENERAL.max' }) }, { 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 }) }; } diff --git a/module/data/fields/action/effectsField.mjs b/module/data/fields/action/effectsField.mjs index 9a4ffc31..fcbdd6ee 100644 --- a/module/data/fields/action/effectsField.mjs +++ b/module/data/fields/action/effectsField.mjs @@ -45,10 +45,18 @@ export default class EffectsField extends fields.ArrayField { * @param {object[]} targets Array of formatted 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(); - let effects = this.effects; const messageTargets = []; targets.forEach(async baseToken => { 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 }); - effects.forEach(async e => { - const effect = (this.item.applyEffects ?? this.item.effects).get(e._id); + effects.forEach(async effect => { if (!token.actor || !effect) return; - await EffectsField.applyEffect(effect, token.actor); + if (effect.system.area?.type !== CONFIG.DH.EFFECTS.areaTypes.placed.id) + 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( CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation ).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 msg = { @@ -96,7 +108,7 @@ export default class EffectsField extends fields.ArrayField { content: await foundry.applications.handlebars.renderTemplate( '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 } ) @@ -120,6 +132,34 @@ export default class EffectsField extends fields.ArrayField { 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 * @returns {boolean} If execute should be triggered automatically diff --git a/module/documents/chatMessage.mjs b/module/documents/chatMessage.mjs index ad64336f..46b19742 100644 --- a/module/documents/chatMessage.mjs +++ b/module/documents/chatMessage.mjs @@ -243,8 +243,6 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage { const targets = this.filterPermTargets(this.system.hitTargets), config = foundry.utils.deepClone(this.system); config.event = event; - if (targets.length === 0) - ui.notifications.info(game.i18n.localize('DAGGERHEART.UI.Notifications.noTargetsSelectedOrPerm')); this.consumeOnSuccess(); this.system.action?.workflow.get('effects')?.execute(config, targets, true); } diff --git a/module/enrichers/TemplateEnricher.mjs b/module/enrichers/TemplateEnricher.mjs index cd0e7d9c..f8de4887 100644 --- a/module/enrichers/TemplateEnricher.mjs +++ b/module/enrichers/TemplateEnricher.mjs @@ -12,7 +12,7 @@ export default function DhTemplateEnricher(match, _options) { )?.id : 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 rangeDisplay = Number.isNaN(Number(range)) @@ -49,8 +49,6 @@ export default function DhTemplateEnricher(match, _options) { } export const renderMeasuredTemplate = async event => { - const { LINE, RECTANGLE, INFRONT, CONE } = CONFIG.DH.GENERAL.templateTypes; - const button = event.currentTarget, type = button.dataset.type, range = button.dataset.range, @@ -59,49 +57,16 @@ export const renderMeasuredTemplate = async event => { if (!type || !range || !game.canvas.scene) return; - const usedType = type === 'inFront' ? 'cone' : type; - const usedAngle = - type === CONE ? (angle ?? CONFIG.MeasuredTemplate.defaults.angle) : type === INFRONT ? '180' : undefined; - - let baseDistance = getTemplateDistance(range); - - 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 - }; + const shapeData = CONFIG.Canvas.layers.regions.layerClass.getTemplateShape({ + type, + angle, + range, + direction, + }); await canvas.regions.placeRegion( { - name: usedType.capitalize(), + name: type.capitalize(), shapes: [shapeData], restriction: { enabled: false, type: 'move', priority: 0 }, behaviors: [], @@ -113,11 +78,3 @@ export const renderMeasuredTemplate = async event => { { 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; -}; diff --git a/styles/less/global/elements.less b/styles/less/global/elements.less index c5bca1da..91df54c6 100755 --- a/styles/less/global/elements.less +++ b/styles/less/global/elements.less @@ -298,7 +298,6 @@ padding-top: 0; padding-bottom: 4px; min-height: auto; - row-gap: 0; legend { display: flex; diff --git a/styles/less/sheets/activeEffects/activeEffects.less b/styles/less/sheets/activeEffects/activeEffects.less index e4f5c541..fac31d81 100644 --- a/styles/less/sheets/activeEffects/activeEffects.less +++ b/styles/less/sheets/activeEffects/activeEffects.less @@ -33,6 +33,8 @@ } .armor-change-container { + row-gap: 0; + header { padding: 0; left: -0.25rem; // TODO: Find why this header is offset 0.25rem to the right so this can be removed. diff --git a/templates/sheets/activeEffect/settings.hbs b/templates/sheets/activeEffect/settings.hbs index 09b78856..c1b76a7d 100644 --- a/templates/sheets/activeEffect/settings.hbs +++ b/templates/sheets/activeEffect/settings.hbs @@ -48,4 +48,28 @@ +
+ + {{localize "DAGGERHEART.ACTIVEEFFECT.Config.area.title"}} + + + + {{#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 }} +
+
+ {{#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}} + + +
+
+ + {{/if}} +
\ No newline at end of file