Skip to content

Commit

Permalink
[#2161] Move base ability & skill prep into MappingField (#2179)
Browse files Browse the repository at this point in the history
Co-authored-by: Kim Mantas <[email protected]>
  • Loading branch information
arbron and Fyorl authored May 23, 2023
1 parent 4084e86 commit 0adeca8
Show file tree
Hide file tree
Showing 5 changed files with 72 additions and 60 deletions.
27 changes: 26 additions & 1 deletion module/data/actor/templates/common.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
7 changes: 6 additions & 1 deletion module/data/actor/templates/creature.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -100,6 +103,8 @@ export default class CreatureTemplate extends CommonTemplate {
return [...levels, "pact"];
}

/* -------------------------------------------- */
/* Migrations */
/* -------------------------------------------- */

/** @inheritdoc */
Expand Down
49 changes: 39 additions & 10 deletions module/data/fields.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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`?
*/

/**
Expand All @@ -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) {
Expand All @@ -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
});
}

Expand All @@ -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");
Expand Down Expand Up @@ -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;
}

/* -------------------------------------------- */
Expand Down
2 changes: 1 addition & 1 deletion module/data/shared/currency.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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"})
};
}
}
47 changes: 0 additions & 47 deletions module/documents/actor/actor.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,6 @@ export default class Actor5e extends Actor {
return this.system._prepareBaseData();
}

this._prepareBaseAbilities();
this._prepareBaseSkills();
this._prepareBaseArmorClass();

// Type-specific preparation
Expand Down Expand Up @@ -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.
Expand Down

0 comments on commit 0adeca8

Please sign in to comment.