From 7c676b14ee8bd5d881e5f3784b48495622ece601 Mon Sep 17 00:00:00 2001 From: Matt Smith Date: Thu, 6 Aug 2020 23:21:15 -0500 Subject: [PATCH] 9: Add formula (derived) attributes - Added a new "Formula" attribute type for both actors and items. - When iterating through attributes, formula attributes are noted in an array so that they can be iterated through in a second pass to evaluate their referenced attributes. The second iteration just handles attribute replacement rather than rolling the formula, that's deferred until the attributes are used in an actual roll formula. - Items also have the same behavior. If an attribute isn't on the item, it will fall back to check the parent actor instead. - Likely areas with issues would be formula attributes that reference other formula attributes, or item formula attributes that reference actor formula attributes. --- module/actor-sheet.js | 6 +-- module/actor.js | 95 ++++++++++++++++++++++++++++++++++++++++++- module/item-sheet.js | 4 +- 3 files changed, 98 insertions(+), 7 deletions(-) diff --git a/module/actor-sheet.js b/module/actor-sheet.js index dd5add0..e8a153a 100644 --- a/module/actor-sheet.js +++ b/module/actor-sheet.js @@ -21,7 +21,7 @@ export class SimpleActorSheet extends ActorSheet { /** @override */ getData() { const data = super.getData(); - data.dtypes = ["String", "Number", "Boolean"]; + data.dtypes = ["String", "Number", "Boolean", "Formula"]; for ( let attr of Object.values(data.data.attributes) ) { attr.isCheckbox = attr.dtype === "Boolean"; } @@ -112,7 +112,7 @@ export class SimpleActorSheet extends ActorSheet { obj[k] = v; return obj; }, {}); - + // Remove attributes which are no longer used for ( let k of Object.keys(this.object.data.data.attributes) ) { if ( !attributes.hasOwnProperty(k) ) attributes[`-=${k}`] = null; @@ -123,7 +123,7 @@ export class SimpleActorSheet extends ActorSheet { obj[e[0]] = e[1]; return obj; }, {_id: this.object._id, "data.attributes": attributes}); - + // Update the Actor return this.object.update(formData); } diff --git a/module/actor.js b/module/actor.js index 0f85c7f..ae4a321 100644 --- a/module/actor.js +++ b/module/actor.js @@ -8,28 +8,119 @@ export class SimpleActor extends Actor { getRollData() { const data = super.getRollData(); const shorthand = game.settings.get("worldbuilding", "macroShorthand"); + const formulaAttributes = []; // Re-map all attributes onto the base roll data if ( !!shorthand ) { for ( let [k, v] of Object.entries(data.attributes) ) { - if ( !(k in data) ) data[k] = v.value; + if ( !(k in data) ) { + data[k] = v.value; + // Make an array of formula attributes for later reference. + if ( v.dtype == "Formula" ) formulaAttributes.push(k); + } } delete data.attributes; } + // Handle formula attributes when the short syntax is disabled. + else { + for ( let [k, v] of Object.entries(data.attributes) ) { + // Make an array of formula attributes for later reference. + if ( v.dtype == "Formula" ) formulaAttributes.push(k); + } + } // Map all items data using their slugified names data.items = this.data.items.reduce((obj, i) => { let key = i.name.slugify({strict: true}); let itemData = duplicate(i.data); + const itemAttributes = []; + + // Add items as shorthand. if ( !!shorthand ) { for ( let [k, v] of Object.entries(itemData.attributes) ) { - if ( !(k in itemData) ) itemData[k] = v.value; + if ( !(k in itemData) ) { + itemData[k] = v.value; + if ( v.dtype == "Formula" ) itemAttributes.push(k); + } } delete itemData["attributes"]; } + // Add formula items when shorthand isn't enabled. + else { + for ( let [k, v] of Object.entries(itemData.attributes) ) { + if ( v.dtype == "Formula" ) itemAttributes.push(k); + } + } + + // Evaluate formula attributes after all other attributes have been handled. + for ( let k of itemAttributes ) { + // Shorthand. + if ( !!shorthand ) { + if ( itemData[k] ) { + itemData[k] = this._replaceData(itemData[k], itemData, data); + } + } + // Full syntax. + else { + if ( itemData.attributes[k].value ) { + itemData.attributes[k].value = this._replaceData(itemData.attributes[k].value, itemData, data); + } + } + } + obj[key] = itemData; return obj; }, {}); + + // Evaluate formula attributes after all other attributes have been handled, + // including items. + for ( let k of formulaAttributes ) { + // Shorthand. + if ( !!shorthand ) { + if ( data[k] ) { + data[k] = this._replaceData(data[k], data); + } + } + // Full syntax. + else { + if ( data.attributes[k].value ) { + data.attributes[k].value = this._replaceData(data.attributes[k].value, data); + } + } + } + return data; } + + /** + * Replace referenced data attributes in the roll formula with the syntax `@attr` with the corresponding key from + * the provided `data` object. + * @param {String} formula The original formula within which to replace. + * @return {String} The formula with attributes replaced with values. + */ + _replaceData(formula, dataPrimary, dataSecondary = null) { + // Exit early if the formula is invalid. + if ( typeof formula != "string" ) { + return 0; + } + + // Replace attributes with their numeric equivalents. + let dataRgx = new RegExp(/@([a-z.0-9_\-]+)/gi); + let rollFormula = formula.replace(dataRgx, (match, term) => { + // Try the primary data source first (ex: actor, item). + let value = getProperty(dataPrimary, term); + if ( value ) { + return String(value).trim(); + } + // Try the secondary data source next (ex: actor that owns item); + else if (dataSecondary) { + value = getProperty(dataSecondary, term); + return value ? String(value).trim() : "0"; + } + // Otherwise, return 0. + return "0"; + }); + + return rollFormula; + } } diff --git a/module/item-sheet.js b/module/item-sheet.js index 8d378e1..a743a5f 100644 --- a/module/item-sheet.js +++ b/module/item-sheet.js @@ -20,7 +20,7 @@ export class SimpleItemSheet extends ItemSheet { /** @override */ getData() { const data = super.getData(); - data.dtypes = ["String", "Number", "Boolean"]; + data.dtypes = ["String", "Number", "Boolean", "Formula"]; for ( let attr of Object.values(data.data.attributes) ) { attr.isCheckbox = attr.dtype === "Boolean"; } @@ -97,7 +97,7 @@ export class SimpleItemSheet extends ItemSheet { obj[k] = v; return obj; }, {}); - + // Remove attributes which are no longer used for ( let k of Object.keys(this.object.data.data.attributes) ) { if ( !attributes.hasOwnProperty(k) ) attributes[`-=${k}`] = null;