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.
This commit is contained in:
Matt Smith 2020-08-06 23:21:15 -05:00
parent 3e2a95f5f1
commit 7c676b14ee
3 changed files with 98 additions and 7 deletions

View file

@ -21,7 +21,7 @@ export class SimpleActorSheet extends ActorSheet {
/** @override */ /** @override */
getData() { getData() {
const data = super.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) ) { for ( let attr of Object.values(data.data.attributes) ) {
attr.isCheckbox = attr.dtype === "Boolean"; attr.isCheckbox = attr.dtype === "Boolean";
} }

View file

@ -8,28 +8,119 @@ export class SimpleActor extends Actor {
getRollData() { getRollData() {
const data = super.getRollData(); const data = super.getRollData();
const shorthand = game.settings.get("worldbuilding", "macroShorthand"); const shorthand = game.settings.get("worldbuilding", "macroShorthand");
const formulaAttributes = [];
// Re-map all attributes onto the base roll data // Re-map all attributes onto the base roll data
if ( !!shorthand ) { if ( !!shorthand ) {
for ( let [k, v] of Object.entries(data.attributes) ) { 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; 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 // Map all items data using their slugified names
data.items = this.data.items.reduce((obj, i) => { data.items = this.data.items.reduce((obj, i) => {
let key = i.name.slugify({strict: true}); let key = i.name.slugify({strict: true});
let itemData = duplicate(i.data); let itemData = duplicate(i.data);
const itemAttributes = [];
// Add items as shorthand.
if ( !!shorthand ) { if ( !!shorthand ) {
for ( let [k, v] of Object.entries(itemData.attributes) ) { 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"]; 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; obj[key] = itemData;
return obj; 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; 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;
}
} }

View file

@ -20,7 +20,7 @@ export class SimpleItemSheet extends ItemSheet {
/** @override */ /** @override */
getData() { getData() {
const data = super.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) ) { for ( let attr of Object.values(data.data.attributes) ) {
attr.isCheckbox = attr.dtype === "Boolean"; attr.isCheckbox = attr.dtype === "Boolean";
} }