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-10 03:42:55 +00:00 committed by Andrew
parent 3e2a95f5f1
commit a490448a15
3 changed files with 131 additions and 14 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,145 @@ 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 // Handle formula attributes when the short syntax is disabled.
this._applyShorthand(data, formulaAttributes, shorthand);
// Map all items data using their slugified names
this._applyItems(data, shorthand);
// Evaluate formula attributes after all other attributes have been handled,
// including items.
this._applyFormulaReplacements(data, formulaAttributes, shorthand);
// Remove the attributes if necessary.
if ( !!shorthand ) { if ( !!shorthand ) {
for ( let [k, v] of Object.entries(data.attributes) ) {
if ( !(k in data) ) data[k] = v.value;
}
delete data.attributes; delete data.attributes;
delete data.attr;
delete data.abil;
} }
return data;
}
/**
* Apply shorthand syntax to actor roll data.
* @param {Object} data The actor's data object.
* @param {Array} formulaAttributes Array of attributes that are derived formulas.
* @param {Boolean} shorthand Whether or not the shorthand syntax is used.
*/
_applyShorthand(data, formulaAttributes, shorthand) {
// Handle formula attributes when the short syntax is disabled.
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);
// Add shortened version of the attributes.
if ( !!shorthand ) {
if ( !(k in data) ) {
data[k] = v.value;
}
}
}
}
/**
* Add items to the actor roll data object. Handles regular and shorthand
* syntax, and calculates derived formula attributes on the items.
* @param {Object} data The actor's data object.
* @param {Boolean} shorthand Whether or not the shorthand syntax is used.
*/
_applyItems(data, shorthand) {
// 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);
if ( !!shorthand ) { const itemAttributes = [];
// Add items to shorthand and note which ones are formula attributes.
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 ( v.dtype == "Formula" ) itemAttributes.push(k);
// Add shortened version of the attributes.
if ( !!shorthand ) {
if ( !(k in itemData) ) {
itemData[k] = v.value;
} }
delete itemData["attributes"];
} }
}
// Evaluate formula attributes after all other attributes have been handled.
for ( let k of itemAttributes ) {
if ( itemData.attributes[k].value ) {
itemData.attributes[k].value = this._replaceData(itemData.attributes[k].value, itemData);
itemData.attributes[k].value = this._replaceData(itemData.attributes[k].value, data, {missing: "0"});
// TODO: Replace with:
// itemData.attributes[k].value = Roll.replaceFormulaData(itemData.attributes[k].value, itemData);
// itemData.attributes[k].value = Roll.replaceFormulaData(itemData.attributes[k].value, data, {missing: "0"});
}
// Duplicate values to shorthand.
if ( !!shorthand ) {
itemData[k] = itemData.attributes[k].value;
}
}
// Delete the original attributes key if using the shorthand syntax.
if ( !!shorthand ) {
delete itemData.attributes;
}
obj[key] = itemData; obj[key] = itemData;
return obj; return obj;
}, {}); }, {});
return data; }
/**
* Apply replacements for derived formula attributes.
* @param {Object} data The actor's data object.
* @param {Array} formulaAttributes Array of attributes that are derived formulas.
* @param {Boolean} shorthand Whether or not the shorthand syntax is used.
*/
_applyFormulaReplacements(data, formulaAttributes, shorthand) {
// Evaluate formula attributes after all other attributes have been handled,
// including items.
for ( let k of formulaAttributes ) {
if ( data.attributes[k].value ) {
data.attributes[k].value = this._replaceData(data.attributes[k].value, data, {missing: "0"});
// TODO: Replace with:
// data.attributes[k].value = Roll.replaceFormulaData(data.attributes[k].value, data, {missing: "0"});
}
// Duplicate values to shorthand.
if ( !!shorthand ) {
data[k] = data.attributes[k].value;
}
}
}
/**
* Replace referenced data attributes in the roll formula with the syntax `@attr` with the corresponding key from
* the provided `data` object. This is a temporary helper function that will be replaced with Roll.replaceFormulaData()
* in Foundry 0.7.1.
*
* @param {String} formula The original formula within which to replace.
* @param {Object} data Data object to use for value replacements.
* @param {Object} missing Value to use as missing replacements, such as {missing: "0"}.
* @return {String} The formula with attributes replaced with values.
*/
_replaceData(formula, data, {missing=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) => {
// Replace matches with the value, or the missing value.
let value = getProperty(data, term);
return value ? String(value).trim() : (missing != null ? missing : `@${term}`);
});
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";
} }