From 0adeca898158f9461272ef79d37dd820d52c1e85 Mon Sep 17 00:00:00 2001 From: Jeff Hitchcock Date: Tue, 23 May 2023 12:59:11 -0700 Subject: [PATCH] [#2161] Move base ability & skill prep into MappingField (#2179) Co-authored-by: Kim Mantas --- module/data/actor/templates/common.mjs | 27 ++++++++++++- module/data/actor/templates/creature.mjs | 7 +++- module/data/fields.mjs | 49 +++++++++++++++++++----- module/data/shared/currency.mjs | 2 +- module/documents/actor/actor.mjs | 47 ----------------------- 5 files changed, 72 insertions(+), 60 deletions(-) diff --git a/module/data/actor/templates/common.mjs b/module/data/actor/templates/common.mjs index bcd0b23de8..6b6c787212 100644 --- a/module/data/actor/templates/common.mjs +++ b/module/data/actor/templates/common.mjs @@ -31,12 +31,37 @@ export default class CommonTemplate extends SystemDataModel.mixin(CurrencyTempla check: new FormulaField({required: true, label: "DND5E.AbilityCheckBonus"}), save: new FormulaField({required: true, label: "DND5E.SaveBonus"}) }, {label: "DND5E.AbilityBonuses"}) - }), {initialKeys: CONFIG.DND5E.abilities, label: "DND5E.Abilities"}) + }), { + initialKeys: CONFIG.DND5E.abilities, initialValue: this._initialAbilityValue.bind(this), + initialKeysOnly: true, label: "DND5E.Abilities" + }) }); } /* -------------------------------------------- */ + /** + * Populate the proper initial value for abilities. + * @param {string} key Key for which the initial data will be created. + * @param {object} initial The initial skill object created by SkillData. + * @param {object} existing Any existing mapping data. + * @returns {object} Initial ability object. + * @private + */ + static _initialAbilityValue(key, initial, existing) { + const config = CONFIG.DND5E.abilities[key]; + if ( config ) { + let defaultValue = config.defaults?.[this._systemType] ?? initial.value; + if ( typeof defaultValue === "string" ) defaultValue = existing[defaultValue]?.value ?? initial.value; + initial.value = defaultValue; + } + return initial; + } + + /* -------------------------------------------- */ + /* Migrations */ + /* -------------------------------------------- */ + /** @inheritdoc */ static migrateData(source) { super.migrateData(source); diff --git a/module/data/actor/templates/creature.mjs b/module/data/actor/templates/creature.mjs index 10bde7885c..80ca0b08ff 100644 --- a/module/data/actor/templates/creature.mjs +++ b/module/data/actor/templates/creature.mjs @@ -53,7 +53,10 @@ export default class CreatureTemplate extends CommonTemplate { check: new FormulaField({required: true, label: "DND5E.SkillBonusCheck"}), passive: new FormulaField({required: true, label: "DND5E.SkillBonusPassive"}) }, {label: "DND5E.SkillBonuses"}) - }), {initialKeys: CONFIG.DND5E.skills, initialValue: this._initialSkillValue}), + }), { + initialKeys: CONFIG.DND5E.skills, initialValue: this._initialSkillValue, + initialKeysOnly: true, label: "DND5E.Skills" + }), tools: new MappingField(new foundry.data.fields.SchemaField({ value: new foundry.data.fields.NumberField({ required: true, min: 0, max: 2, step: 0.5, initial: 1, label: "DND5E.ProficiencyLevel" @@ -100,6 +103,8 @@ export default class CreatureTemplate extends CommonTemplate { return [...levels, "pact"]; } + /* -------------------------------------------- */ + /* Migrations */ /* -------------------------------------------- */ /** @inheritdoc */ diff --git a/module/data/fields.mjs b/module/data/fields.mjs index c4973445aa..da7246987f 100644 --- a/module/data/fields.mjs +++ b/module/data/fields.mjs @@ -150,9 +150,20 @@ export class IdentifierField extends foundry.data.fields.StringField { /* -------------------------------------------- */ +/** + * @callback MappingFieldInitialValueBuilder + * @param {string} key The key within the object where this new value is being generated. + * @param {*} initial The generic initial data provided by the contained model. + * @param {object} existing Any existing mapping data. + * @returns {object} Value to use as default for this key. + */ + /** * @typedef {DataFieldOptions} MappingFieldOptions - * @property {string[]} [initialKeys] Keys that will be created if no data is provided. + * @property {string[]} [initialKeys] Keys that will be created if no data is provided. + * @property {MappingFieldInitialValueBuilder} [initialValue] Function to calculate the initial value for a key. + * @property {boolean} [initialKeysOnly=false] Should the keys in the initialized data be limited to the keys provided + * by `options.initialKeys`? */ /** @@ -161,6 +172,9 @@ export class IdentifierField extends foundry.data.fields.StringField { * @param {DataField} model The class of DataField which should be embedded in this field. * @param {MappingFieldOptions} [options={}] Options which configure the behavior of the field. * @property {string[]} [initialKeys] Keys that will be created if no data is provided. + * @property {MappingFieldInitialValueBuilder} [initialValue] Function to calculate the initial value for a key. + * @property {boolean} [initialKeysOnly=false] Should the keys in the initialized data be limited to the keys provided + * by `options.initialKeys`? */ export class MappingField extends foundry.data.fields.ObjectField { constructor(model, options) { @@ -182,7 +196,8 @@ export class MappingField extends foundry.data.fields.ObjectField { static get _defaults() { return foundry.utils.mergeObject(super._defaults, { initialKeys: null, - initialValue: null + initialValue: null, + initialKeysOnly: false }); } @@ -202,15 +217,25 @@ export class MappingField extends foundry.data.fields.ObjectField { const initial = super.getInitialValue(data); if ( !keys || !foundry.utils.isEmpty(initial) ) return initial; if ( !(keys instanceof Array) ) keys = Object.keys(keys); - for ( const key of keys ) { - const modelInitial = this.model.getInitialValue(); - initial[key] = this.initialValue?.(key, modelInitial) ?? modelInitial; - } + for ( const key of keys ) initial[key] = this._getInitialValueForKey(key); return initial; } /* -------------------------------------------- */ + /** + * Get the initial value for the provided key. + * @param {string} key Key within the object being built. + * @param {object} [object] Any existing mapping data. + * @returns {*} Initial value based on provided field type. + */ + _getInitialValueForKey(key, object) { + const initial = this.model.getInitialValue(); + return this.initialValue?.(key, initial, object) ?? initial; + } + + /* -------------------------------------------- */ + /** @override */ _validateType(value, options={}) { if ( foundry.utils.getType(value) !== "Object" ) throw new Error("must be an Object"); @@ -240,10 +265,14 @@ export class MappingField extends foundry.data.fields.ObjectField { /** @override */ initialize(value, model, options={}) { if ( !value ) return value; - return Object.entries(value).reduce((obj, [k, v]) => { - obj[k] = this.model.initialize(v, model, options); - return obj; - }, {}); + const obj = {}; + const initialKeys = (this.initialKeys instanceof Array) ? this.initialKeys : Object.keys(this.initialKeys ?? {}); + const keys = this.initialKeysOnly ? initialKeys : Object.keys(value); + for ( const key of keys ) { + const data = value[key] ?? this._getInitialValueForKey(key, value); + obj[key] = this.model.initialize(data, model, options); + } + return obj; } /* -------------------------------------------- */ diff --git a/module/data/shared/currency.mjs b/module/data/shared/currency.mjs index 2d463877e8..f4a33fb80d 100644 --- a/module/data/shared/currency.mjs +++ b/module/data/shared/currency.mjs @@ -12,7 +12,7 @@ export default class CurrencyTemplate extends foundry.abstract.DataModel { return { currency: new MappingField(new foundry.data.fields.NumberField({ required: true, nullable: false, integer: true, min: 0, initial: 0 - }), {initialKeys: CONFIG.DND5E.currencies, label: "DND5E.Currency"}) + }), {initialKeys: CONFIG.DND5E.currencies, initialKeysOnly: true, label: "DND5E.Currency"}) }; } } diff --git a/module/documents/actor/actor.mjs b/module/documents/actor/actor.mjs index 8f0da16711..cb56a42af0 100644 --- a/module/documents/actor/actor.mjs +++ b/module/documents/actor/actor.mjs @@ -109,8 +109,6 @@ export default class Actor5e extends Actor { return this.system._prepareBaseData(); } - this._prepareBaseAbilities(); - this._prepareBaseSkills(); this._prepareBaseArmorClass(); // Type-specific preparation @@ -225,51 +223,6 @@ export default class Actor5e extends Actor { /* Base Data Preparation Helpers */ /* -------------------------------------------- */ - /** - * Update the actor's abilities list to match the abilities configured in `DND5E.abilities`. - * Mutates the system.abilities object. - * @protected - */ - _prepareBaseAbilities() { - if ( !("abilities" in this.system) ) return; - const abilities = {}; - for ( const [key, config] of Object.entries(CONFIG.DND5E.abilities) ) { - abilities[key] = this.system.abilities[key]; - if ( !abilities[key] ) { - abilities[key] = foundry.utils.deepClone(game.system.template.Actor.templates.common.abilities.cha); - - let defaultValue = config.defaults?.[this.type] ?? 10; - if ( typeof defaultValue === "string" ) { - defaultValue = abilities[defaultValue].value ?? this.system.abilities[defaultValue] ?? 10; - } - abilities[key].value = defaultValue; - } - } - this.system.abilities = abilities; - } - - /* -------------------------------------------- */ - - /** - * Update the actor's skill list to match the skills configured in `DND5E.skills`. - * Mutates the system.skills object. - * @protected - */ - _prepareBaseSkills() { - if ( !("skills" in this.system) ) return; - const skills = {}; - for ( const [key, skill] of Object.entries(CONFIG.DND5E.skills) ) { - skills[key] = this.system.skills[key]; - if ( !skills[key] ) { - skills[key] = foundry.utils.deepClone(game.system.template.Actor.templates.creature.skills.acr); - skills[key].ability = skill.ability; - } - } - this.system.skills = skills; - } - - /* -------------------------------------------- */ - /** * Initialize derived AC fields for Active Effects to target. * Mutates the system.attributes.ac object.