Fix editing homebrew resources with a custom ResourcesField

This commit is contained in:
Carlos Fernandez 2026-03-11 04:50:34 -04:00
parent f298b2160a
commit 859263e2fd
9 changed files with 109 additions and 83 deletions

View file

@ -44,8 +44,32 @@ export default class DHBaseActorSettings extends DHApplicationMixin(DocumentShee
const context = await super._prepareContext(options); const context = await super._prepareContext(options);
context.isNPC = this.actor.isNPC; context.isNPC = this.actor.isNPC;
if (context.systemFields.attack) if (context.systemFields.attack) {
context.systemFields.attack.fields = this.actor.system.attack.schema.fields; 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; return context;
} }

View file

@ -30,7 +30,6 @@ const characterBaseResources = Object.freeze({
hope: { hope: {
id: 'hope', id: 'hope',
initial: 2, initial: 2,
min: 0,
reverse: false, reverse: false,
label: 'DAGGERHEART.GENERAL.hope' label: 'DAGGERHEART.GENERAL.hope'
} }
@ -65,7 +64,6 @@ const companionBaseResources = Object.freeze({
hope: { hope: {
id: 'hope', id: 'hope',
initial: 0, initial: 0,
min: 0,
reverse: false, reverse: false,
label: 'DAGGERHEART.GENERAL.hope' label: 'DAGGERHEART.GENERAL.hope'
} }

View file

@ -587,6 +587,7 @@ export default class DhCharacter extends DhCreature {
} }
prepareBaseData() { prepareBaseData() {
super.prepareBaseData();
this.evasion += this.class.value?.system?.evasion ?? 0; this.evasion += this.class.value?.system?.evasion ?? 0;
const currentLevel = this.levelData.level.current; const currentLevel = this.levelData.level.current;

View file

@ -123,6 +123,7 @@ export default class DhCompanion extends DhCreature {
} }
prepareBaseData() { prepareBaseData() {
super.prepareBaseData();
this.attack.roll.bonus = this.partner?.system?.spellcastModifier ?? 0; this.attack.roll.bonus = this.partner?.system?.spellcastModifier ?? 0;
for (let levelKey in this.levelData.levelups) { for (let levelKey in this.levelData.levelups) {

View file

@ -1,4 +1,4 @@
import { resourceField } from '../fields/actorField.mjs'; import { ResourcesField } from '../fields/actorField.mjs';
import BaseDataActor from './base.mjs'; import BaseDataActor from './base.mjs';
export default class DhCreature extends BaseDataActor { export default class DhCreature extends BaseDataActor {
@ -8,36 +8,7 @@ export default class DhCreature extends BaseDataActor {
return { return {
...super.defineSchema(), ...super.defineSchema(),
resources: new fields.SchemaField({ resources: new ResourcesField(this.metadata.type),
...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;
},
{}
)
}),
advantageSources: new fields.ArrayField(new fields.StringField(), { advantageSources: new fields.ArrayField(new fields.StringField(), {
label: 'DAGGERHEART.ACTORS.Character.advantageSources.label', label: 'DAGGERHEART.ACTORS.Character.advantageSources.label',
hint: 'DAGGERHEART.ACTORS.Character.advantageSources.hint' hint: 'DAGGERHEART.ACTORS.Character.advantageSources.hint'
@ -56,17 +27,6 @@ export default class DhCreature extends BaseDataActor {
return !vulnerableAppliedByOther; 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) { async _preUpdate(changes, options, userId) {
const allowed = await super._preUpdate(changes, options, userId); const allowed = await super._preUpdate(changes, options, userId);
if (allowed === false) return; if (allowed === false) return;

View file

@ -6,21 +6,6 @@ const attributeField = label =>
tierMarked: new fields.BooleanField({ initial: false }) 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 => const stressDamageReductionRule = localizationPath =>
new fields.SchemaField({ new fields.SchemaField({
cost: new fields.NumberField({ cost: new fields.NumberField({
@ -36,4 +21,67 @@ const bonusField = label =>
dice: new fields.ArrayField(new fields.StringField(), { label: `${game.i18n.localize(label)} Dice` }) 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 };

View file

@ -20,14 +20,10 @@
<div class="fieldsets-section"> <div class="fieldsets-section">
<fieldset class="flex"> <fieldset class="flex">
<legend>{{localize "DAGGERHEART.GENERAL.HitPoints.plural"}}</legend> <legend>{{localize "DAGGERHEART.GENERAL.Resource.plural"}}</legend>
{{formGroup systemFields.resources.fields.hitPoints.fields.value value=document._source.system.resources.hitPoints.value label=(localize "DAGGERHEART.ACTORS.Adversary.FIELDS.resources.hitPoints.value.label")}} {{#each resources as |resource|}}
{{formGroup systemFields.resources.fields.hitPoints.fields.max value=document._source.system.resources.hitPoints.max label=(localize "DAGGERHEART.ACTORS.Adversary.FIELDS.resources.hitPoints.max.label")}} {{formGroup resource.field value=resource.value name=resource.name}}
</fieldset> {{/each}}
<fieldset class="flex">
<legend>{{localize "DAGGERHEART.GENERAL.stress"}}</legend>
{{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")}}
</fieldset> </fieldset>
</div> </div>

View file

@ -22,15 +22,12 @@
<legend>{{localize 'DAGGERHEART.GENERAL.basics'}}</legend> <legend>{{localize 'DAGGERHEART.GENERAL.basics'}}</legend>
<div class="two-columns even"> <div class="two-columns even">
{{formGroup systemFields.resources.fields.hitPoints.fields.value value=document._source.system.resources.hitPoints.value localize=true}} {{#each resources as |resource|}}
<span data-tooltip-text="{{localize "DAGGERHEART.UI.Tooltip.maxHPClassBound"}}"> <span {{#if resource.tooltip}}data-tooltip-text="{{resource.tooltip}}"{{/if}}>
{{formGroup systemFields.resources.fields.hitPoints.fields.max value=document._source.system.resources.hitPoints.max localize=true}} {{formGroup resource.field value=resource.value name=resource.name}}
</span> </span>
{{/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.scars value=document._source.system.scars localize=true}}
{{formGroup systemFields.proficiency value=document._source.system.proficiency localize=true}} {{formGroup systemFields.proficiency value=document._source.system.proficiency localize=true}}

View file

@ -7,8 +7,9 @@
<legend>{{localize 'DAGGERHEART.GENERAL.basics'}}</legend> <legend>{{localize 'DAGGERHEART.GENERAL.basics'}}</legend>
<div class="nest-inputs"> <div class="nest-inputs">
{{formGroup systemFields.evasion value=document._source.system.evasion localize=true}} {{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}} {{#each resources as |resource|}}
{{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}} {{formGroup resource.field value=resource.value name=resource.name}}
{{/each}}
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="form-fields"> <div class="form-fields">