From 859263e2fd66f497b2f16d088cebe697f1f6e114 Mon Sep 17 00:00:00 2001 From: Carlos Fernandez Date: Wed, 11 Mar 2026 04:50:34 -0400 Subject: [PATCH] Fix editing homebrew resources with a custom ResourcesField --- .../applications/sheets/api/actor-setting.mjs | 26 +++++- module/config/resourceConfig.mjs | 2 - module/data/actor/character.mjs | 1 + module/data/actor/companion.mjs | 1 + module/data/actor/creature.mjs | 44 +--------- module/data/fields/actorField.mjs | 80 +++++++++++++++---- .../adversary-settings/details.hbs | 16 ++-- .../character-settings/details.hbs | 15 ++-- .../companion-settings/details.hbs | 7 +- 9 files changed, 109 insertions(+), 83 deletions(-) diff --git a/module/applications/sheets/api/actor-setting.mjs b/module/applications/sheets/api/actor-setting.mjs index d8cfb40f..738f7002 100644 --- a/module/applications/sheets/api/actor-setting.mjs +++ b/module/applications/sheets/api/actor-setting.mjs @@ -44,8 +44,32 @@ export default class DHBaseActorSettings extends DHApplicationMixin(DocumentShee const context = await super._prepareContext(options); context.isNPC = this.actor.isNPC; - if (context.systemFields.attack) + if (context.systemFields.attack) { context.systemFields.attack.fields = this.actor.system.attack.schema.fields; + } + + // Create fake fields for actor configurable max resource value. + const resourceConfig = CONFIG.DH.RESOURCE[this.actor.type]?.all; + if (resourceConfig) { + const relevant = ['hitPoints', 'stress'].filter(r => r in resourceConfig); + context.resources = relevant.map(key => { + const data = this.actor._source.system.resources[key]; + const config = resourceConfig[key]; + return { + label: config.label, + name: `system.resources.${key}.max`, + value: data.max ?? config.max, + tooltip: key === 'hitPoints' ? game.i18n.localize('DAGGERHEART.UI.Tooltip.maxHPClassBound') : null, + field: new foundry.data.fields.NumberField({ + initial: config.max, + integer: true, + label: game.i18n.format('DAGGERHEART.GENERAL.maxWithThing', { + thing: game.i18n.localize(config.label) + }) + }) + }; + }); + } return context; } diff --git a/module/config/resourceConfig.mjs b/module/config/resourceConfig.mjs index 1c24213a..65f9584a 100644 --- a/module/config/resourceConfig.mjs +++ b/module/config/resourceConfig.mjs @@ -30,7 +30,6 @@ const characterBaseResources = Object.freeze({ hope: { id: 'hope', initial: 2, - min: 0, reverse: false, label: 'DAGGERHEART.GENERAL.hope' } @@ -65,7 +64,6 @@ const companionBaseResources = Object.freeze({ hope: { id: 'hope', initial: 0, - min: 0, reverse: false, label: 'DAGGERHEART.GENERAL.hope' } diff --git a/module/data/actor/character.mjs b/module/data/actor/character.mjs index cc9801a3..68f7f3a8 100644 --- a/module/data/actor/character.mjs +++ b/module/data/actor/character.mjs @@ -587,6 +587,7 @@ export default class DhCharacter extends DhCreature { } prepareBaseData() { + super.prepareBaseData(); this.evasion += this.class.value?.system?.evasion ?? 0; const currentLevel = this.levelData.level.current; diff --git a/module/data/actor/companion.mjs b/module/data/actor/companion.mjs index ae85ff69..7a8f0e64 100644 --- a/module/data/actor/companion.mjs +++ b/module/data/actor/companion.mjs @@ -123,6 +123,7 @@ export default class DhCompanion extends DhCreature { } prepareBaseData() { + super.prepareBaseData(); this.attack.roll.bonus = this.partner?.system?.spellcastModifier ?? 0; for (let levelKey in this.levelData.levelups) { diff --git a/module/data/actor/creature.mjs b/module/data/actor/creature.mjs index 438c88be..601068ad 100644 --- a/module/data/actor/creature.mjs +++ b/module/data/actor/creature.mjs @@ -1,4 +1,4 @@ -import { resourceField } from '../fields/actorField.mjs'; +import { ResourcesField } from '../fields/actorField.mjs'; import BaseDataActor from './base.mjs'; export default class DhCreature extends BaseDataActor { @@ -8,36 +8,7 @@ export default class DhCreature extends BaseDataActor { return { ...super.defineSchema(), - resources: new fields.SchemaField({ - ...Object.values(CONFIG.DH.RESOURCE[this.metadata.type].all).reduce( - (acc, resource) => { - if (resource.max !== undefined) { - acc[resource.id] = resourceField( - resource.max, - resource.initial, - resource.label, - resource.maxLabel - ); - } else { - acc[resource.id] = new fields.SchemaField( - { - value: new fields.NumberField({ - initial: resource.initial, - min: resource.min, - integer: true, - label: resource.label - }), - isReversed: new fields.BooleanField({ initial: resource.reverse }) - }, - { label: resource.label } - ); - } - - return acc; - }, - {} - ) - }), + resources: new ResourcesField(this.metadata.type), advantageSources: new fields.ArrayField(new fields.StringField(), { label: 'DAGGERHEART.ACTORS.Character.advantageSources.label', hint: 'DAGGERHEART.ACTORS.Character.advantageSources.hint' @@ -56,17 +27,6 @@ export default class DhCreature extends BaseDataActor { return !vulnerableAppliedByOther; } - prepareDerivedData() { - super.prepareDerivedData(); - const resources = CONFIG.DH.RESOURCE[this.metadata.type].all; - if (resources) { - for (const [key, value] of Object.entries(this.resources)) { - value.label = resources[key]?.label; - value.isReversed = resources[key]?.reverse; - } - } - } - async _preUpdate(changes, options, userId) { const allowed = await super._preUpdate(changes, options, userId); if (allowed === false) return; diff --git a/module/data/fields/actorField.mjs b/module/data/fields/actorField.mjs index 2c9bb83d..65e7abbf 100644 --- a/module/data/fields/actorField.mjs +++ b/module/data/fields/actorField.mjs @@ -6,21 +6,6 @@ const attributeField = label => tierMarked: new fields.BooleanField({ initial: false }) }); -const resourceField = (max = 0, initial = 0, label, maxLabel) => - new fields.SchemaField( - { - value: new fields.NumberField({ initial: initial, min: 0, integer: true, label }), - max: new fields.NumberField({ - initial: max, - integer: true, - label: - maxLabel ?? - game.i18n.format('DAGGERHEART.GENERAL.maxWithThing', { thing: game.i18n.localize(label) }) - }) - }, - { label } - ); - const stressDamageReductionRule = localizationPath => new fields.SchemaField({ cost: new fields.NumberField({ @@ -36,4 +21,67 @@ const bonusField = label => dice: new fields.ArrayField(new fields.StringField(), { label: `${game.i18n.localize(label)} Dice` }) }); -export { attributeField, resourceField, stressDamageReductionRule, bonusField }; +/** + * Field used for actor resources. It is a resource that validates dynamically based on the config. + * Because "max" may be defined during runtime, we don't attempt to clamp the maximum value. + */ +class ResourcesField extends fields.TypedObjectField { + constructor(actorType) { + super( + new fields.SchemaField({ + value: new fields.NumberField({ min: 0, initial: 0, integer: true }), + // Some resources allow changing max. A null max means its the default + max: new fields.NumberField({ initial: null, integer: true, nullable: true }) + }) + ); + this.actorType = actorType; + } + + getInitialValue() { + const resources = CONFIG.DH.RESOURCE[this.actorType].all; + return Object.values(resources).reduce((result, resource) => { + result[resource.id] = { + value: resource.initial, + max: null + }; + return result; + }, {}); + } + + _validateKey(key) { + return key in CONFIG.DH.RESOURCE[this.actorType].all; + } + + _cleanType(value, options) { + value = super._cleanType(value, options); + + // If not partial, ensure all data exists + if (!options.partial) { + value = foundry.utils.mergeObject(this.getInitialValue(), value); + } + + return value; + } + + /** Initializes the original source data, returning prepared data */ + initialize(...args) { + const data = super.initialize(...args); + const resources = CONFIG.DH.RESOURCE[this.actorType].all; + for (const [key, value] of Object.entries(data)) { + // TypedObjectField only calls _validateKey when persisting, so we also call it here + if (!this._validateKey(key)) { + delete value[key]; + continue; + } + + // Add basic prepared data. + const resource = resources[key]; + value.label = resource.label; + value.isReversed = resources[key].reverse; + value.max = typeof resource.max === 'number' ? value.max ?? resource.max : null; + } + return data; + } +} + +export { attributeField, ResourcesField, stressDamageReductionRule, bonusField }; diff --git a/templates/sheets-settings/adversary-settings/details.hbs b/templates/sheets-settings/adversary-settings/details.hbs index 065ebe74..dc2fd386 100644 --- a/templates/sheets-settings/adversary-settings/details.hbs +++ b/templates/sheets-settings/adversary-settings/details.hbs @@ -20,15 +20,11 @@
- {{localize "DAGGERHEART.GENERAL.HitPoints.plural"}} - {{formGroup systemFields.resources.fields.hitPoints.fields.value value=document._source.system.resources.hitPoints.value label=(localize "DAGGERHEART.ACTORS.Adversary.FIELDS.resources.hitPoints.value.label")}} - {{formGroup systemFields.resources.fields.hitPoints.fields.max value=document._source.system.resources.hitPoints.max label=(localize "DAGGERHEART.ACTORS.Adversary.FIELDS.resources.hitPoints.max.label")}} -
-
- {{localize "DAGGERHEART.GENERAL.stress"}} - {{formGroup systemFields.resources.fields.stress.fields.value value=document._source.system.resources.stress.value label=(localize "DAGGERHEART.ACTORS.Adversary.FIELDS.resources.stress.value.label")}} - {{formGroup systemFields.resources.fields.stress.fields.max value=document._source.system.resources.stress.max label=(localize "DAGGERHEART.ACTORS.Adversary.FIELDS.resources.stress.max.label")}} -
+ {{localize "DAGGERHEART.GENERAL.Resource.plural"}} + {{#each resources as |resource|}} + {{formGroup resource.field value=resource.value name=resource.name}} + {{/each}} +
@@ -36,4 +32,4 @@ {{formGroup systemFields.damageThresholds.fields.major value=document._source.system.damageThresholds.major label=(localize "DAGGERHEART.GENERAL.DamageThresholds.majorThreshold")}} {{formGroup systemFields.damageThresholds.fields.severe value=document._source.system.damageThresholds.severe label=(localize "DAGGERHEART.GENERAL.DamageThresholds.severeThreshold")}}
- \ No newline at end of file + diff --git a/templates/sheets-settings/character-settings/details.hbs b/templates/sheets-settings/character-settings/details.hbs index 3f9247e0..4bda501e 100644 --- a/templates/sheets-settings/character-settings/details.hbs +++ b/templates/sheets-settings/character-settings/details.hbs @@ -22,15 +22,12 @@ {{localize 'DAGGERHEART.GENERAL.basics'}}
- {{formGroup systemFields.resources.fields.hitPoints.fields.value value=document._source.system.resources.hitPoints.value localize=true}} - - {{formGroup systemFields.resources.fields.hitPoints.fields.max value=document._source.system.resources.hitPoints.max localize=true}} - + {{#each resources as |resource|}} + + {{formGroup resource.field value=resource.value name=resource.name}} + + {{/each}} - {{formGroup systemFields.resources.fields.stress.fields.value value=document._source.system.resources.stress.value localize=true}} - {{formGroup systemFields.resources.fields.stress.fields.max value=document._source.system.resources.stress.max localize=true}} - - {{formGroup systemFields.resources.fields.hope.fields.value value=document._source.system.resources.hope.value localize=true}} {{formGroup systemFields.scars value=document._source.system.scars localize=true}} {{formGroup systemFields.proficiency value=document._source.system.proficiency localize=true}} @@ -39,4 +36,4 @@
- \ No newline at end of file + diff --git a/templates/sheets-settings/companion-settings/details.hbs b/templates/sheets-settings/companion-settings/details.hbs index 88878d67..6a602d38 100644 --- a/templates/sheets-settings/companion-settings/details.hbs +++ b/templates/sheets-settings/companion-settings/details.hbs @@ -7,8 +7,9 @@ {{localize 'DAGGERHEART.GENERAL.basics'}}
{{formGroup systemFields.evasion value=document._source.system.evasion localize=true}} - {{formGroup systemFields.resources.fields.stress.fields.value value=document._source.system.resources.stress.value label='DAGGERHEART.ACTORS.Companion.FIELDS.resources.stress.currentStress.label' localize=true}} - {{formGroup systemFields.resources.fields.stress.fields.max value=document._source.system.resources.stress.max label='DAGGERHEART.ACTORS.Companion.FIELDS.resources.stress.maxStress.label' localize=true}} + {{#each resources as |resource|}} + {{formGroup resource.field value=resource.value name=resource.name}} + {{/each}}
@@ -19,4 +20,4 @@
- \ No newline at end of file +