From b5e0bb7c27ffa7ca82b1fc82354170fc2a365526 Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Sat, 21 Mar 2026 00:53:03 +0100 Subject: [PATCH] [Feature] ArmorEffect reworked into ChangeType on BaseEffect (#1739) * Initial * . * Single armor rework start * More fixes * Fixed DamageReductionDialog * Removed last traces of ArmorEffect * . --- daggerheart.mjs | 14 +- lang/en.json | 32 ++- .../dialogs/damageReductionDialog.mjs | 5 +- .../applications/sheets-configs/_module.mjs | 1 - .../sheets-configs/activeEffectConfig.mjs | 54 +++- .../armorActiveEffectConfig.mjs | 67 ----- .../applications/sheets/actors/character.mjs | 36 +-- .../sheets/api/application-mixin.mjs | 4 - module/applications/sheets/items/armor.mjs | 2 +- module/config/generalConfig.mjs | 16 +- module/data/activeEffect/_module.mjs | 11 +- module/data/activeEffect/armorEffect.mjs | 244 ------------------ module/data/activeEffect/baseEffect.mjs | 16 +- .../data/activeEffect/changeTypes/_module.mjs | 9 + .../data/activeEffect/changeTypes/armor.mjs | 145 +++++++++++ module/data/actor/character.mjs | 21 +- module/data/item/armor.mjs | 6 +- module/documents/activeEffect.mjs | 2 +- module/systemRegistration/handlebars.mjs | 3 +- module/systemRegistration/migrations.mjs | 11 +- .../sheets/activeEffects/activeEffects.less | 22 ++ .../sheets/activeEffects/armorEffects.less | 5 - styles/less/sheets/index.less | 1 - system.json | 3 +- templates/sheets/activeEffect/changes.hbs | 15 ++ .../activeEffect/typeChanges/armorChange.hbs | 10 + 26 files changed, 339 insertions(+), 416 deletions(-) delete mode 100644 module/applications/sheets-configs/armorActiveEffectConfig.mjs delete mode 100644 module/data/activeEffect/armorEffect.mjs create mode 100644 module/data/activeEffect/changeTypes/_module.mjs create mode 100644 module/data/activeEffect/changeTypes/armor.mjs delete mode 100644 styles/less/sheets/activeEffects/armorEffects.less create mode 100644 templates/sheets/activeEffect/typeChanges/armorChange.hbs diff --git a/daggerheart.mjs b/daggerheart.mjs index 67d1c24f..6c7fb17e 100644 --- a/daggerheart.mjs +++ b/daggerheart.mjs @@ -43,7 +43,7 @@ CONFIG.Item.dataModels = models.items.config; CONFIG.ActiveEffect.documentClass = documents.DhActiveEffect; CONFIG.ActiveEffect.dataModels = models.activeEffects.config; -CONFIG.ActiveEffect.changeTypes = { ...CONFIG.ActiveEffect.changeTypes, ...models.activeEffects.changeTypes }; +CONFIG.ActiveEffect.changeTypes = { ...CONFIG.ActiveEffect.changeTypes, ...models.activeEffects.changeEffects }; CONFIG.Combat.documentClass = documents.DhpCombat; CONFIG.Combat.dataModels = { base: models.DhCombat }; @@ -217,17 +217,6 @@ Hooks.once('init', () => { label: sheetLabel('DOCUMENT.ActiveEffect') } ); - DocumentSheetConfig.registerSheet( - CONFIG.ActiveEffect.documentClass, - SYSTEM.id, - applications.sheetConfigs.ArmorActiveEffectConfig, - { - types: ['armor'], - makeDefault: true, - label: () => - `${game.i18n.localize('TYPES.ActiveEffect.armor')} ${game.i18n.localize('DAGGERHEART.GENERAL.effect')}` - } - ); game.socket.on(`system.${SYSTEM.id}`, socketRegistration.handleSocketEvent); @@ -281,7 +270,6 @@ Hooks.on('setup', () => { ...damageThresholds, 'proficiency', 'evasion', - 'armorScore', 'scars', 'levelData.level.current' ] diff --git a/lang/en.json b/lang/en.json index fa3099df..056c7964 100755 --- a/lang/en.json +++ b/lang/en.json @@ -16,8 +16,7 @@ "ActiveEffect": { "base": "Standard", "beastform": "Beastform", - "horde": "Horde", - "armor": "Armor" + "horde": "Horde" }, "Actor": { "character": "Character", @@ -778,8 +777,8 @@ }, "ArmorInteraction": { "none": { "label": "Ignores Armor" }, - "active": { "label": "Only Active With Armor" }, - "inactive": { "label": "Only Active Without Armor" } + "active": { "label": "Active w/ Armor" }, + "inactive": { "label": "Inactive w/ Armor" } }, "ArmorFeature": { "burning": { @@ -1864,6 +1863,17 @@ "name": "Healing Roll" } }, + "ChangeTypes": { + "armor": { + "newArmorEffect": "Armor Effect", + "FIELDS": { + "armorInteraction": { + "label": "Armor Interaction", + "hint": "Does the character wearing armor suppress this effect?" + } + } + } + }, "Duration": { "passive": "Passive", "temporary": "Temporary" @@ -1885,15 +1895,6 @@ "Attachments": { "attachHint": "Drop items here to attach them", "transferHint": "If checked, this effect will be applied to any actor that owns this Effect's parent Item. The effect is always applied if this Item is attached to another one." - }, - "Armor": { - "newArmorEffect": "Armor Effect", - "FIELDS": { - "armorInteraction": { - "label": "Armor Interaction", - "hint": "Does the character wearing armor suppress this effect?" - } - } } }, "GENERAL": { @@ -3079,10 +3080,7 @@ "tokenActorsMissing": "[{names}] missing Actors", "domainTouchRequirement": "This domain card requires {nr} {domain} cards in the loadout to be used", "knowTheTide": "Know The Tide gained a token", - "lackingItemTransferPermission": "User {user} lacks owner permission needed to transfer items to {target}", - "cannotAlterArmorEffectChanges": "You cannot alter the changes length of an armor effect", - "cannotAlterArmorEffectType": "You cannot alter the type of armor effect changes", - "cannotAlterArmorEffectKey": "You cannot alter they key of armor effect changes" + "lackingItemTransferPermission": "User {user} lacks owner permission needed to transfer items to {target}" }, "Progress": { "migrationLabel": "Performing system migration. Please wait and do not close Foundry." diff --git a/module/applications/dialogs/damageReductionDialog.mjs b/module/applications/dialogs/damageReductionDialog.mjs index 6aeb2cf9..5e858e4b 100644 --- a/module/applications/dialogs/damageReductionDialog.mjs +++ b/module/applications/dialogs/damageReductionDialog.mjs @@ -21,13 +21,12 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap this.rulesDefault ); - const allArmorEffects = Array.from(actor.allApplicableEffects()).filter(x => x.type === 'armor'); - const orderedArmorEffects = game.system.api.data.activeEffects.ArmorEffect.orderEffectsForAutoChange( + const allArmorEffects = Array.from(actor.allApplicableEffects()).filter(x => x.system.armorData); + const orderedArmorEffects = game.system.api.data.activeEffects.changeTypes.armor.orderEffectsForAutoChange( allArmorEffects, true ); const armor = orderedArmorEffects.reduce((acc, effect) => { - if (effect.type !== 'armor') return acc; const { value, max } = effect.system.armorData; acc.push({ effect: effect, diff --git a/module/applications/sheets-configs/_module.mjs b/module/applications/sheets-configs/_module.mjs index 8b09dc29..d3fb3c39 100644 --- a/module/applications/sheets-configs/_module.mjs +++ b/module/applications/sheets-configs/_module.mjs @@ -6,7 +6,6 @@ export { default as CompanionSettings } from './companion-settings.mjs'; export { default as SettingActiveEffectConfig } from './setting-active-effect-config.mjs'; export { default as SettingFeatureConfig } from './setting-feature-config.mjs'; export { default as EnvironmentSettings } from './environment-settings.mjs'; -export { default as ArmorActiveEffectConfig } from './armorActiveEffectConfig.mjs'; export { default as ActiveEffectConfig } from './activeEffectConfig.mjs'; export { default as DhTokenConfig } from './token-config.mjs'; export { default as DhPrototypeTokenConfig } from './prototype-token-config.mjs'; diff --git a/module/applications/sheets-configs/activeEffectConfig.mjs b/module/applications/sheets-configs/activeEffectConfig.mjs index 2bd7d5b9..339c2c86 100644 --- a/module/applications/sheets-configs/activeEffectConfig.mjs +++ b/module/applications/sheets-configs/activeEffectConfig.mjs @@ -150,6 +150,10 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac minLength: 0 }); }); + + htmlElement + .querySelector('.armor-change-checkbox') + ?.addEventListener('change', this.armorChangeToggle.bind(this)); } async _prepareContext(options) { @@ -187,38 +191,74 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac break; case 'changes': const fields = this.document.system.schema.fields.changes.element.fields; + + const singleTypes = ['armor']; + const { base, ...typedChanges } = context.source.changes.reduce((acc, change, index) => { + const type = CONFIG.DH.GENERAL.baseActiveEffectModes[change.type] ? 'base' : change.type; + if (singleTypes.includes(type)) { + acc[type] = { ...change, index }; + } else { + if (!acc[type]) acc[type] = []; + acc[type].push({ ...change, index }); + } + + return acc; + }, {}); partContext.changes = await Promise.all( - foundry.utils - .deepClone(context.source.changes) - .map((c, i) => this._prepareChangeContext(c, i, fields)) + foundry.utils.deepClone(base ?? []).map(c => this._prepareChangeContext(c, fields)) ); + partContext.typedChanges = typedChanges; break; } return partContext; } - _prepareChangeContext(change, index, fields) { + armorChangeToggle(event) { + if (event.target.checked) { + this.addArmorChange(); + } else { + this.removeTypedChange(event.target.dataset.index); + } + } + + /* Could be generalised if needed later */ + addArmorChange() { + const submitData = this._processFormData(null, this.form, new FormDataExtended(this.form)); + const changes = Object.values(submitData.system?.changes ?? {}); + changes.push(game.system.api.data.activeEffects.changeTypes.armor.getInitialValue()); + return this.submit({ updateData: { system: { changes } } }); + } + + removeTypedChange(indexString) { + const submitData = this._processFormData(null, this.form, new FormDataExtended(this.form)); + const changes = Object.values(submitData.system.changes); + const index = Number(indexString); + changes.splice(index, 1); + return this.submit({ updateData: { system: { changes } } }); + } + + _prepareChangeContext(change, fields) { if (typeof change.value !== 'string') change.value = JSON.stringify(change.value); const defaultPriority = game.system.api.documents.DhActiveEffect.CHANGE_TYPES[change.type]?.defaultPriority; Object.assign( change, ['key', 'type', 'value', 'priority'].reduce((paths, fieldName) => { - paths[`${fieldName}Path`] = `system.changes.${index}.${fieldName}`; + paths[`${fieldName}Path`] = `system.changes.${change.index}.${fieldName}`; return paths; }, {}) ); return ( game.system.api.documents.DhActiveEffect.CHANGE_TYPES[change.type].render?.( change, - index, + change.index, defaultPriority ) ?? foundry.applications.handlebars.renderTemplate( 'systems/daggerheart/templates/sheets/activeEffect/change.hbs', { change, - index, + index: change.index, defaultPriority, fields } diff --git a/module/applications/sheets-configs/armorActiveEffectConfig.mjs b/module/applications/sheets-configs/armorActiveEffectConfig.mjs deleted file mode 100644 index 3dca8ef1..00000000 --- a/module/applications/sheets-configs/armorActiveEffectConfig.mjs +++ /dev/null @@ -1,67 +0,0 @@ -const { HandlebarsApplicationMixin, DocumentSheetV2 } = foundry.applications.api; - -export default class ArmorActiveEffectConfig extends HandlebarsApplicationMixin(DocumentSheetV2) { - static DEFAULT_OPTIONS = { - tag: 'form', - classes: ['daggerheart', 'sheet', 'dh-style', 'active-effect-config', 'armor-effect-config'], - form: { - handler: this.updateForm, - submitOnChange: true, - closeOnSubmit: false - }, - position: { width: 560 }, - actions: { - finish: ArmorActiveEffectConfig.#finish - } - }; - - static PARTS = { - header: { template: 'systems/daggerheart/templates/sheets/activeEffect/header.hbs' }, - tabs: { template: 'templates/generic/tab-navigation.hbs' }, - details: { template: 'systems/daggerheart/templates/sheets/activeEffect/armor/details.hbs' }, - settings: { template: 'systems/daggerheart/templates/sheets/activeEffect/armor/settings.hbs' }, - footer: { template: 'systems/daggerheart/templates/sheets/activeEffect/armor/footer.hbs' } - }; - - static TABS = { - sheet: { - tabs: [ - { id: 'details', icon: 'fa-solid fa-book' }, - { id: 'settings', icon: 'fa-solid fa-bars', label: 'DAGGERHEART.GENERAL.Tabs.settings' } - ], - initial: 'details', - labelPrefix: 'EFFECT.TABS' - } - }; - - async _prepareContext(options) { - const context = await super._prepareContext(options); - context.systemFields = context.document.system.schema.fields; - - return context; - } - - /** @inheritDoc */ - async _preparePartContext(partId, context) { - const partContext = await super._preparePartContext(partId, context); - if (partId in partContext.tabs) partContext.tab = partContext.tabs[partId]; - - switch (partId) { - case 'details': - partContext.isActorEffect = this.document.parent?.documentName === 'Actor'; - partContext.isItemEffect = this.document.parent?.documentName === 'Item'; - break; - } - - return partContext; - } - - static async updateForm(_event, _form, formData) { - await this.document.update(formData.object); - this.render(); - } - - static #finish() { - this.close(); - } -} diff --git a/module/applications/sheets/actors/character.mjs b/module/applications/sheets/actors/character.mjs index db05a9c7..15e83645 100644 --- a/module/applications/sheets/actors/character.mjs +++ b/module/applications/sheets/actors/character.mjs @@ -966,10 +966,13 @@ export default class CharacterSheet extends DHBaseActorSheet { const armorSources = []; for (var effect of Array.from(this.document.allApplicableEffects())) { const origin = effect.origin ? await foundry.utils.fromUuid(effect.origin) : effect.parent; - if (effect.type !== 'armor' || effect.disabled || effect.isSuppressed) continue; + if (!effect.system.armorData || effect.disabled || effect.isSuppressed) continue; + + const originIsActor = origin instanceof Actor; + const name = originIsActor ? effect.name : origin.name; armorSources.push({ uuid: effect.uuid, - name: origin.name, + name, ...effect.system.armorData }); } @@ -1018,15 +1021,14 @@ export default class CharacterSheet extends DHBaseActorSheet { /** Update specific armor source */ static async armorSourceUpdate(event) { const effect = await foundry.utils.fromUuid(event.target.dataset.uuid); - if (effect.system.changes.length !== 1) return; + const armorChange = effect.system.armorChange; + if (!armorChange) return; const value = Math.max(Math.min(Number.parseInt(event.target.value), effect.system.armorData.max), 0); - const newChanges = [ - { - ...effect.system.changes[0], - value - } - ]; + const newChanges = effect.system.changes.map(change => ({ + ...change, + value: change.type === 'armor' ? value : change.value + })); event.target.value = value; const progressBar = event.target.closest('.status-bar.armor-slots').querySelector('progress'); @@ -1038,19 +1040,19 @@ export default class CharacterSheet extends DHBaseActorSheet { static async armorSourcePipUpdate(event) { const target = event.target.closest('.armor-slot'); const effect = await foundry.utils.fromUuid(target.dataset.uuid); - if (effect.system.changes.length !== 1) return; - const { value, max } = effect.system.armorData; + const armorChange = effect.system.armorChange; + if (!armorChange) return; + + const { value } = effect.system.armorData; const inputValue = Number.parseInt(target.dataset.value); const decreasing = value >= inputValue; const newValue = decreasing ? inputValue - 1 : inputValue; - const newChanges = [ - { - ...effect.system.changes[0], - value: newValue - } - ]; + const newChanges = effect.system.changes.map(change => ({ + ...change, + value: change.type === 'armor' ? newValue : change.value + })); const container = target.closest('.slot-bar'); for (const armorSlot of container.querySelectorAll('.armor-slot i')) { diff --git a/module/applications/sheets/api/application-mixin.mjs b/module/applications/sheets/api/application-mixin.mjs index 98d68ed9..83313454 100644 --- a/module/applications/sheets/api/application-mixin.mjs +++ b/module/applications/sheets/api/application-mixin.mjs @@ -762,10 +762,6 @@ export default function DHApplicationMixin(Base) { data.system.domain = parent.system.domains[0]; } - if (documentClass === 'ActiveEffect') { - return cls.createDialog(data, { parent: this.document }); - } - const doc = await cls.create(data, { parent, renderSheet: !event.shiftKey }); if (parentIsItem && type === 'feature') { await this.document.update({ diff --git a/module/applications/sheets/items/armor.mjs b/module/applications/sheets/items/armor.mjs index 4c69c822..93325405 100644 --- a/module/applications/sheets/items/armor.mjs +++ b/module/applications/sheets/items/armor.mjs @@ -64,7 +64,7 @@ export default class ArmorSheet extends ItemAttachmentSheet(DHBaseItemSheet) { const armorEffect = this.document.system.armorEffect; if (Number.isNaN(value) || !armorEffect) return; - await armorEffect.system.updateArmorMax(value); + await armorEffect.system.armorChange.typeData.updateArmorMax(value); this.render(); } diff --git a/module/config/generalConfig.mjs b/module/config/generalConfig.mjs index 4ac46618..391f0699 100644 --- a/module/config/generalConfig.mjs +++ b/module/config/generalConfig.mjs @@ -959,12 +959,7 @@ export const sceneRangeMeasurementSetting = { } }; -export const activeEffectModes = { - armor: { - id: 'armor', - priority: 20, - label: 'TYPES.ActiveEffect.armor' - }, +export const baseActiveEffectModes = { custom: { id: 'custom', priority: 0, @@ -1002,6 +997,15 @@ export const activeEffectModes = { } }; +export const activeEffectModes = { + armor: { + id: 'armor', + priority: 20, + label: 'TYPES.ActiveEffect.armor' + }, + ...baseActiveEffectModes +}; + export const activeEffectArmorInteraction = { none: { id: 'none', label: 'DAGGERHEART.CONFIG.ArmorInteraction.none.label' }, active: { id: 'active', label: 'DAGGERHEART.CONFIG.ArmorInteraction.active.label' }, diff --git a/module/data/activeEffect/_module.mjs b/module/data/activeEffect/_module.mjs index 62f10e3e..3c933a9c 100644 --- a/module/data/activeEffect/_module.mjs +++ b/module/data/activeEffect/_module.mjs @@ -1,17 +1,12 @@ import BaseEffect from './baseEffect.mjs'; import BeastformEffect from './beastformEffect.mjs'; import HordeEffect from './hordeEffect.mjs'; -import ArmorEffect from './armorEffect.mjs'; +export { changeTypes, changeEffects } from './changeTypes/_module.mjs'; -export { BaseEffect, BeastformEffect, HordeEffect, ArmorEffect }; +export { BaseEffect, BeastformEffect, HordeEffect }; export const config = { base: BaseEffect, beastform: BeastformEffect, - horde: HordeEffect, - armor: ArmorEffect -}; - -export const changeTypes = { - armor: ArmorEffect.armorChangeEffect + horde: HordeEffect }; diff --git a/module/data/activeEffect/armorEffect.mjs b/module/data/activeEffect/armorEffect.mjs deleted file mode 100644 index 6bf0ecec..00000000 --- a/module/data/activeEffect/armorEffect.mjs +++ /dev/null @@ -1,244 +0,0 @@ -import { getScrollTextData, itemAbleRollParse } from '../../helpers/utils.mjs'; - -/** - * ArmorEffects are ActiveEffects that have a static changes field of length 1. It includes current and maximum armor. - * When applied to a character, it adds to their currently marked and maximum armor. - */ -export default class ArmorEffect extends foundry.data.ActiveEffectTypeDataModel { - static defineSchema() { - const fields = foundry.data.fields; - - return { - ...super.defineSchema(), - changes: new fields.ArrayField( - new fields.SchemaField({ - key: new fields.StringField({ - required: true, - nullable: false, - initial: 'system.armorScore' - }), - type: new fields.StringField({ - required: true, - blank: false, - initial: CONFIG.DH.GENERAL.activeEffectModes.armor.id, - validate: ArmorEffect.#validateType - }), - phase: new fields.StringField({ required: true, blank: false, initial: 'initial' }), - priority: new fields.NumberField({ integer: true, initial: 20 }), - value: new fields.NumberField({ - required: true, - integer: true, - initial: 0, - min: 0, - label: 'DAGGERHEART.GENERAL.value' - }), - max: new fields.StringField({ - required: true, - nullable: false, - initial: '1', - label: 'DAGGERHEART.GENERAL.max' - }) - }), - { - initial: [ - { - key: 'system.armorScore', - type: CONFIG.DH.GENERAL.activeEffectModes.armor.id, - phase: 'initial', - priority: 20, - value: 0, - max: '1' - } - ] - } - ), - armorInteraction: new fields.StringField({ - required: true, - choices: CONFIG.DH.GENERAL.activeEffectArmorInteraction, - initial: CONFIG.DH.GENERAL.activeEffectArmorInteraction.none.id, - label: 'DAGGERHEART.EFFECTS.Armor.FIELDS.armorInteraction.label', - hint: 'DAGGERHEART.EFFECTS.Armor.FIELDS.armorInteraction.hint' - }) - }; - } - - get isSuppressed() { - if (this.parent.actor?.type !== 'character') return false; - - switch (this.armorInteraction) { - case CONFIG.DH.GENERAL.activeEffectArmorInteraction.active.id: - return !this.parent.actor.system.armor; - case CONFIG.DH.GENERAL.activeEffectArmorInteraction.inactive.id: - return Boolean(this.parent.actor.system.armor); - default: - return false; - } - } - - /* Type Functions */ - - /** - * Validate that an {@link EffectChangeData#type} string is well-formed. - * @param {string} type The string to be validated - * @returns {true} - * @throws {Error} An error if the type string is malformed - */ - static #validateType(type) { - if (type !== CONFIG.DH.GENERAL.activeEffectModes.armor.id) - throw new Error('An armor effect must have change.type "armor"'); - - return true; - } - - static armorChangeEffect = { - label: 'Armor', - defaultPriortiy: 20, - handler: (actor, change, _options, _field, replacementData) => { - game.system.api.documents.DhActiveEffect.applyChange( - actor, - { - ...change, - key: 'system.armorScore.value', - type: CONFIG.DH.GENERAL.activeEffectModes.add.id, - value: change.value - }, - replacementData - ); - game.system.api.documents.DhActiveEffect.applyChange( - actor, - { - ...change, - key: 'system.armorScore.max', - type: CONFIG.DH.GENERAL.activeEffectModes.add.id, - value: change.max - }, - replacementData - ); - return {}; - }, - render: null - }; - - /* Helpers */ - - get armorChange() { - if (this.changes.length !== 1) - throw new Error('Unexpected error. An armor effect should have a changes field of length 1.'); - - const actor = this.parent.actor?.type === 'character' ? this.parent.actor : null; - const changeData = this.changes[0]; - const maxParse = actor ? itemAbleRollParse(changeData.max, actor, this.parent.parent) : null; - const maxRoll = maxParse ? new Roll(maxParse).evaluateSync() : null; - const maxEvaluated = maxRoll ? (maxRoll.isDeterministic ? maxRoll.total : null) : null; - - return { - ...changeData, - max: maxEvaluated ?? changeData.max - }; - } - - get armorData() { - return { value: this.armorChange.value, max: this.armorChange.max }; - } - - async updateArmorMax(newMax) { - const { effect, ...baseChange } = this.armorChange; - const newChanges = [ - { - ...baseChange, - max: newMax, - value: Math.min(this.armorChange.value, newMax) - } - ]; - await this.parent.update({ 'system.changes': newChanges }); - } - - static orderEffectsForAutoChange(armorEffects, increasing) { - const getEffectWeight = effect => { - switch (effect.parent.type) { - case 'class': - case 'subclass': - case 'ancestry': - case 'community': - case 'feature': - case 'domainCard': - return 2; - case 'armor': - return 3; - case 'loot': - case 'consumable': - return 4; - case 'weapon': - return 5; - case 'character': - return 6; - default: - return 1; - } - }; - - return armorEffects - .filter(x => !x.disabled && !x.isSuppressed) - .sort((a, b) => - increasing ? getEffectWeight(b) - getEffectWeight(a) : getEffectWeight(a) - getEffectWeight(b) - ); - } - - /* Overrides */ - - static getDefaultObject() { - return { - key: 'system.armorScore', - type: 'armor', - name: game.i18n.localize('DAGGERHEART.EFFECTS.Armor.newArmorEffect'), - img: 'icons/equipment/chest/breastplate-helmet-metal.webp' - }; - } - - async _preUpdate(changes, options, user) { - const allowed = await super._preUpdate(changes, options, user); - if (allowed === false) return false; - - if (changes.system?.changes) { - const changesChanged = changes.system.changes.length !== this.changes.length; - if (changesChanged) { - ui.notifications.error( - game.i18n.localize('DAGGERHEART.UI.Notifications.cannotAlterArmorEffectChanges') - ); - return false; - } - - if (changes.system.changes.length === 1) { - if (changes.system.changes[0].type !== CONFIG.DH.GENERAL.activeEffectModes.armor.id) { - ui.notifications.error( - game.i18n.localize('DAGGERHEART.UI.Notifications.cannotAlterArmorEffectType') - ); - return false; - } - - if (changes.system.changes[0].key !== 'system.armorScore') { - ui.notifications.error( - game.i18n.localize('DAGGERHEART.UI.Notifications.cannotAlterArmorEffectKey') - ); - return false; - } - - if ( - changes.system.changes[0].value !== this.armorChange.value && - this.parent.actor?.type === 'character' - ) { - options.scrollingTextData = [ - getScrollTextData(this.parent.actor, changes.system.changes[0], 'armor') - ]; - } - } - } - } - - _onUpdate(changes, options, userId) { - super._onUpdate(changes, options, userId); - - if (options.scrollingTextData && this.parent.actor?.type === 'character') - this.parent.actor.queueScrollText(options.scrollingTextData); - } -} diff --git a/module/data/activeEffect/baseEffect.mjs b/module/data/activeEffect/baseEffect.mjs index 98a961d7..fc87b353 100644 --- a/module/data/activeEffect/baseEffect.mjs +++ b/module/data/activeEffect/baseEffect.mjs @@ -12,6 +12,8 @@ * "Anything that uses another data model value as its value": +1 - Effects that increase traits have to be calculated first at Base priority. (EX: Raise evasion by half your agility) */ +import { changeTypes } from './_module.mjs'; + export default class BaseEffect extends foundry.data.ActiveEffectTypeDataModel { static defineSchema() { const fields = foundry.data.fields; @@ -30,7 +32,8 @@ export default class BaseEffect extends foundry.data.ActiveEffectTypeDataModel { }), value: new fields.AnyField({ required: true, nullable: true, serializable: true, initial: '' }), phase: new fields.StringField({ required: true, blank: false, initial: 'initial' }), - priority: new fields.NumberField() + priority: new fields.NumberField(), + typeData: new fields.TypedSchemaField(changeTypes, { nullable: true, initial: null }) }) ), duration: new fields.SchemaField({ @@ -86,6 +89,17 @@ export default class BaseEffect extends foundry.data.ActiveEffectTypeDataModel { return true; } + get armorChange() { + return this.changes.find(x => x.type === CONFIG.DH.GENERAL.activeEffectModes.armor.id); + } + + get armorData() { + const armorChange = this.armorChange; + if (!armorChange) return null; + + return armorChange.typeData.getArmorData(armorChange); + } + static getDefaultObject() { return { name: 'New Effect', diff --git a/module/data/activeEffect/changeTypes/_module.mjs b/module/data/activeEffect/changeTypes/_module.mjs new file mode 100644 index 00000000..cf872304 --- /dev/null +++ b/module/data/activeEffect/changeTypes/_module.mjs @@ -0,0 +1,9 @@ +import Armor from './armor.mjs'; + +export const changeEffects = { + armor: Armor.changeEffect +}; + +export const changeTypes = { + armor: Armor +}; diff --git a/module/data/activeEffect/changeTypes/armor.mjs b/module/data/activeEffect/changeTypes/armor.mjs new file mode 100644 index 00000000..f368fa78 --- /dev/null +++ b/module/data/activeEffect/changeTypes/armor.mjs @@ -0,0 +1,145 @@ +import { itemAbleRollParse } from '../../../helpers/utils.mjs'; + +const fields = foundry.data.fields; + +export default class Armor extends foundry.abstract.DataModel { + static defineSchema() { + return { + type: new fields.StringField({ required: true, initial: 'armor', blank: false }), + max: new fields.StringField({ + required: true, + nullable: false, + initial: '1', + label: 'DAGGERHEART.GENERAL.max' + }), + armorInteraction: new fields.StringField({ + required: true, + choices: CONFIG.DH.GENERAL.activeEffectArmorInteraction, + initial: CONFIG.DH.GENERAL.activeEffectArmorInteraction.none.id, + label: 'DAGGERHEART.EFFECTS.ChangeTypes.armor.FIELDS.armorInteraction.label', + hint: 'DAGGERHEART.EFFECTS.ChangeTypes.armor.FIELDS.armorInteraction.hint' + }) + }; + } + + static changeEffect = { + label: 'Armor', + defaultPriortiy: 20, + handler: (actor, change, _options, _field, replacementData) => { + game.system.api.documents.DhActiveEffect.applyChange( + actor, + { + ...change, + key: 'system.armorScore.value', + type: CONFIG.DH.GENERAL.activeEffectModes.add.id, + value: change.value + }, + replacementData + ); + game.system.api.documents.DhActiveEffect.applyChange( + actor, + { + ...change, + key: 'system.armorScore.max', + type: CONFIG.DH.GENERAL.activeEffectModes.add.id, + value: change.typeData.max + }, + replacementData + ); + return {}; + }, + render: null + }; + + get isSuppressed() { + switch (this.armorInteraction) { + case CONFIG.DH.GENERAL.activeEffectArmorInteraction.active.id: + return !this.parent.parent?.actor.system.armor; + case CONFIG.DH.GENERAL.activeEffectArmorInteraction.inactive.id: + return Boolean(this.parent.parent?.actor.system.armor); + default: + return false; + } + } + + static getInitialValue(locked) { + return { + key: 'Armor', + type: CONFIG.DH.GENERAL.activeEffectModes.armor.id, + value: 0, + typeData: { + type: 'armor', + max: 0, + locked + }, + phase: 'initial', + priority: 20 + }; + } + + static getDefaultArmorEffect() { + return { + name: game.i18n.localize('DAGGERHEART.EFFECTS.ChangeTypes.armor.newArmorEffect'), + img: 'icons/equipment/chest/breastplate-helmet-metal.webp', + system: { + changes: [Armor.getInitialValue(true)] + } + }; + } + + /* Helpers */ + + getArmorData(parentChange) { + const actor = this.parent.parent?.actor?.type === 'character' ? this.parent.parent.actor : null; + const maxParse = actor ? itemAbleRollParse(this.max, actor, this.parent.parent.parent) : null; + const maxRoll = maxParse ? new Roll(maxParse).evaluateSync() : null; + const maxEvaluated = maxRoll ? (maxRoll.isDeterministic ? maxRoll.total : null) : null; + + return { + value: parentChange.value, + max: maxEvaluated ?? this.max + }; + } + + async updateArmorMax(newMax) { + const newChanges = [ + ...this.parent.changes.map(change => ({ + ...change, + value: change.type === 'armor' ? Math.min(change.value, newMax) : change.value, + typeData: change.type === 'armor' ? { ...change.typeData, max: newMax } : change.typeData + })) + ]; + await this.parent.parent.update({ 'system.changes': newChanges }); + } + + static orderEffectsForAutoChange(armorEffects, increasing) { + const getEffectWeight = effect => { + switch (effect.parent.type) { + case 'class': + case 'subclass': + case 'ancestry': + case 'community': + case 'feature': + case 'domainCard': + return 2; + case 'armor': + return 3; + case 'loot': + case 'consumable': + return 4; + case 'weapon': + return 5; + case 'character': + return 6; + default: + return 1; + } + }; + + return armorEffects + .filter(x => !x.disabled && !x.isSuppressed) + .sort((a, b) => + increasing ? getEffectWeight(b) - getEffectWeight(a) : getEffectWeight(a) - getEffectWeight(b) + ); + } +} diff --git a/module/data/actor/character.mjs b/module/data/actor/character.mjs index 9036d3ca..3d8580f0 100644 --- a/module/data/actor/character.mjs +++ b/module/data/actor/character.mjs @@ -469,8 +469,8 @@ export default class DhCharacter extends DhCreature { const increasing = armorChange >= 0; let remainingChange = Math.abs(armorChange); - const armorEffects = Array.from(this.parent.allApplicableEffects()).filter(x => x.type === 'armor'); - const orderedEffects = game.system.api.data.activeEffects.ArmorEffect.orderEffectsForAutoChange( + const armorEffects = Array.from(this.parent.allApplicableEffects()).filter(x => x.system.armorData); + const orderedEffects = game.system.api.data.activeEffects.changeTypes.armor.orderEffectsForAutoChange( armorEffects, increasing ); @@ -482,11 +482,11 @@ export default class DhCharacter extends DhCreature { usedArmorChange -= armorEffect.system.armorChange.value; } else { if (increasing) { - const remainingArmor = armorEffect.system.armorChange.max - armorEffect.system.armorChange.value; + const remainingArmor = armorEffect.system.armorData.max - armorEffect.system.armorData.value; usedArmorChange = Math.min(remainingChange, remainingArmor); remainingChange -= usedArmorChange; } else { - const changeChange = Math.min(armorEffect.system.armorChange.value, remainingChange); + const changeChange = Math.min(armorEffect.system.armorData.value, remainingChange); usedArmorChange -= changeChange; remainingChange -= changeChange; } @@ -499,12 +499,13 @@ export default class DhCharacter extends DhCreature { embeddedUpdates[armorEffect.parent.id].updates.push({ '_id': armorEffect.id, - 'system.changes': [ - { - ...armorEffect.system.armorChange, - value: armorEffect.system.armorChange.value + usedArmorChange - } - ] + 'system.changes': armorEffect.system.changes.map(change => ({ + ...change, + value: + change.type === 'armor' + ? armorEffect.system.armorChange.value + usedArmorChange + : change.value + })) }); } diff --git a/module/data/item/armor.mjs b/module/data/item/armor.mjs index 76f05859..ba70e4b9 100644 --- a/module/data/item/armor.mjs +++ b/module/data/item/armor.mjs @@ -52,7 +52,7 @@ export default class DHArmor extends AttachableItem { } get armorEffect() { - return this.parent.effects.find(x => x.type === 'armor'); + return this.parent.effects.find(x => x.system.armorData); } get armorData() { @@ -80,9 +80,9 @@ export default class DHArmor extends AttachableItem { async _onCreate(_data, _options, userId) { if (userId !== game.user.id) return; - if (!this.parent.effects.some(x => x.type === 'armor')) { + if (!this.parent.effects.some(x => x.system.armorData)) { this.parent.createEmbeddedDocuments('ActiveEffect', [ - game.system.api.data.activeEffects.ArmorEffect.getDefaultObject() + game.system.api.data.activeEffects.changeTypes.armor.getDefaultArmorEffect() ]); } } diff --git a/module/documents/activeEffect.mjs b/module/documents/activeEffect.mjs index 0fa84255..043d7f33 100644 --- a/module/documents/activeEffect.mjs +++ b/module/documents/activeEffect.mjs @@ -78,7 +78,7 @@ export default class DhActiveEffect extends foundry.documents.ActiveEffect { throw new Error('The array of sub-types to restrict to must not be empty.'); } - const creatableEffects = types || ['base', 'armor']; + const creatableEffects = types || ['base']; const documentTypes = this.TYPES.filter(type => creatableEffects.includes(type)).map(type => { const labelKey = `TYPES.ActiveEffect.${type}`; const label = game.i18n.has(labelKey) ? game.i18n.localize(labelKey) : type; diff --git a/module/systemRegistration/handlebars.mjs b/module/systemRegistration/handlebars.mjs index f51e1035..058b1b56 100644 --- a/module/systemRegistration/handlebars.mjs +++ b/module/systemRegistration/handlebars.mjs @@ -47,6 +47,7 @@ export const preloadHandlebarsTemplates = async function () { 'systems/daggerheart/templates/ui/chat/parts/button-part.hbs', 'systems/daggerheart/templates/ui/itemBrowser/itemContainer.hbs', 'systems/daggerheart/templates/scene/dh-config.hbs', - 'systems/daggerheart/templates/settings/appearance-settings/diceSoNiceTab.hbs' + 'systems/daggerheart/templates/settings/appearance-settings/diceSoNiceTab.hbs', + 'systems/daggerheart/templates/sheets/activeEffect/typeChanges/armorChange.hbs' ]); }; diff --git a/module/systemRegistration/migrations.mjs b/module/systemRegistration/migrations.mjs index dff85fef..7426cff4 100644 --- a/module/systemRegistration/migrations.mjs +++ b/module/systemRegistration/migrations.mjs @@ -374,15 +374,18 @@ export async function runMigrations() { if (migrationArmorScore !== undefined && !hasArmorEffect) { await item.createEmbeddedDocuments('ActiveEffect', [ { - ...game.system.api.data.activeEffects.ArmorEffect.getDefaultObject(), + ...game.system.api.data.activeEffects.changeTypes.armor.getDefaultArmorEffect(), changes: [ { - key: 'system.armorScore', - type: CONFIG.DH.GENERAL.activeEffectModes.armor.id, + key: 'Armor', + type: CONFIG.DH.GENERAL.activeEffectModes.armor, phase: 'initial', priority: 20, value: 0, - max: migrationArmorScore.toString() + typeData: { + type: 'armor', + max: migrationArmorScore.toString() + } } ] } diff --git a/styles/less/sheets/activeEffects/activeEffects.less b/styles/less/sheets/activeEffects/activeEffects.less index ba3ff43f..077369cf 100644 --- a/styles/less/sheets/activeEffects/activeEffects.less +++ b/styles/less/sheets/activeEffects/activeEffects.less @@ -31,5 +31,27 @@ text-align: center; } } + + .armor-change-container { + padding-top: 0; + padding-bottom: 4px; + row-gap: 0; + + legend { + display: flex; + align-items: center; + padding-left: 3px; + } + + header { + padding: 0; + left: -0.25rem; // TODO: Find why this header is offset 0.25rem to the right so this can be removed. + } + + header, + ol { + grid-template-columns: 6rem 6rem 12rem 4rem; + } + } } } diff --git a/styles/less/sheets/activeEffects/armorEffects.less b/styles/less/sheets/activeEffects/armorEffects.less deleted file mode 100644 index fd5c89b1..00000000 --- a/styles/less/sheets/activeEffects/armorEffects.less +++ /dev/null @@ -1,5 +0,0 @@ -.application.sheet.daggerheart.dh-style.armor-effect-config { - .tab-form-footer { - margin-top: 8px; - } -} diff --git a/styles/less/sheets/index.less b/styles/less/sheets/index.less index 25ec6fc3..e5ffbf3e 100644 --- a/styles/less/sheets/index.less +++ b/styles/less/sheets/index.less @@ -44,4 +44,3 @@ @import './actions/actions.less'; @import './activeEffects/activeEffects.less'; -@import './activeEffects/armorEffects.less'; diff --git a/system.json b/system.json index dfbb473e..41e46edb 100644 --- a/system.json +++ b/system.json @@ -278,8 +278,7 @@ }, "ActiveEffect": { "beastform": {}, - "horde": {}, - "armor": {} + "horde": {} }, "Combat": { "combat": {} diff --git a/templates/sheets/activeEffect/changes.hbs b/templates/sheets/activeEffect/changes.hbs index 026ffd90..e687c2e9 100644 --- a/templates/sheets/activeEffect/changes.hbs +++ b/templates/sheets/activeEffect/changes.hbs @@ -13,4 +13,19 @@ {{{change}}} {{/each}} + +
diff --git a/templates/sheets/activeEffect/typeChanges/armorChange.hbs b/templates/sheets/activeEffect/typeChanges/armorChange.hbs new file mode 100644 index 00000000..6a04aba2 --- /dev/null +++ b/templates/sheets/activeEffect/typeChanges/armorChange.hbs @@ -0,0 +1,10 @@ +