247 lines
7.3 KiB
JavaScript
247 lines
7.3 KiB
JavaScript
import { deepClone } from "../../../common/utils/helpers.mjs";
|
|
|
|
/**
|
|
* @typedef {import("../_types.mjs").RollParseNode} RollParseNode
|
|
*/
|
|
|
|
/**
|
|
* An abstract class which represents a single token that can be used as part of a Roll formula.
|
|
* Every portion of a Roll formula is parsed into a subclass of RollTerm in order for the Roll to be fully evaluated.
|
|
*/
|
|
export default class RollTerm {
|
|
/**
|
|
* @param {object} [options] An object of additional options which describes and modifies the term.
|
|
*/
|
|
constructor({options={}}={}) {
|
|
this.options = options;
|
|
}
|
|
|
|
/**
|
|
* An object of additional options which describes and modifies the term.
|
|
* @type {object}
|
|
*/
|
|
options;
|
|
|
|
/**
|
|
* An internal flag for whether the term has been evaluated
|
|
* @type {boolean}
|
|
* @internal
|
|
*/
|
|
_evaluated = false;
|
|
|
|
/**
|
|
* A reference to the Roll at the root of the evaluation tree.
|
|
* @type {Roll}
|
|
* @internal
|
|
*/
|
|
_root;
|
|
|
|
/**
|
|
* Is this term intermediate, and should be evaluated first as part of the simplification process?
|
|
* @type {boolean}
|
|
*/
|
|
isIntermediate = false;
|
|
|
|
/**
|
|
* A regular expression pattern which identifies optional term-level flavor text
|
|
* @type {string}
|
|
*/
|
|
static FLAVOR_REGEXP_STRING = "(?:\\[([^\\]]+)\\])";
|
|
|
|
/**
|
|
* A regular expression which identifies term-level flavor text
|
|
* @type {RegExp}
|
|
*/
|
|
static FLAVOR_REGEXP = new RegExp(RollTerm.FLAVOR_REGEXP_STRING, "g");
|
|
|
|
/**
|
|
* A regular expression used to match a term of this type
|
|
* @type {RegExp}
|
|
*/
|
|
static REGEXP = undefined;
|
|
|
|
/**
|
|
* An array of additional attributes which should be retained when the term is serialized
|
|
* @type {string[]}
|
|
*/
|
|
static SERIALIZE_ATTRIBUTES = [];
|
|
|
|
/* -------------------------------------------- */
|
|
/* RollTerm Attributes */
|
|
/* -------------------------------------------- */
|
|
|
|
/**
|
|
* A string representation of the formula expression for this RollTerm, prior to evaluation.
|
|
* @type {string}
|
|
*/
|
|
get expression() {
|
|
throw new Error(`The ${this.constructor.name} class must implement the expression attribute`);
|
|
}
|
|
|
|
/**
|
|
* A string representation of the formula, including optional flavor text.
|
|
* @type {string}
|
|
*/
|
|
get formula() {
|
|
let f = this.expression;
|
|
if ( this.flavor ) f += `[${this.flavor}]`;
|
|
return f;
|
|
}
|
|
|
|
/**
|
|
* A string or numeric representation of the final output for this term, after evaluation.
|
|
* @type {number|string}
|
|
*/
|
|
get total() {
|
|
throw new Error(`The ${this.constructor.name} class must implement the total attribute`);
|
|
}
|
|
|
|
/**
|
|
* Optional flavor text which modifies and describes this term.
|
|
* @type {string}
|
|
*/
|
|
get flavor() {
|
|
return this.options.flavor || "";
|
|
}
|
|
|
|
/**
|
|
* Whether this term is entirely deterministic or contains some randomness.
|
|
* @type {boolean}
|
|
*/
|
|
get isDeterministic() {
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* A reference to the RollResolver app being used to externally resolve this term.
|
|
* @type {RollResolver}
|
|
*/
|
|
get resolver() {
|
|
return this._root?._resolver;
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
/* RollTerm Methods */
|
|
/* -------------------------------------------- */
|
|
|
|
/**
|
|
* Evaluate the term, processing its inputs and finalizing its total.
|
|
* @param {object} [options={}] Options which modify how the RollTerm is evaluated
|
|
* @param {boolean} [options.minimize=false] Minimize the result, obtaining the smallest possible value.
|
|
* @param {boolean} [options.maximize=false] Maximize the result, obtaining the largest possible value.
|
|
* @param {boolean} [options.allowStrings=false] If true, string terms will not throw an error when evaluated.
|
|
* @returns {Promise<RollTerm>|RollTerm} Returns a Promise if the term is non-deterministic.
|
|
*/
|
|
evaluate(options={}) {
|
|
if ( this._evaluated ) {
|
|
throw new Error(`The ${this.constructor.name} has already been evaluated and is now immutable`);
|
|
}
|
|
this._evaluated = true;
|
|
return this._evaluate(options);
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
/**
|
|
* Evaluate the term.
|
|
* @param {object} [options={}] Options which modify how the RollTerm is evaluated, see RollTerm#evaluate
|
|
* @returns {Promise<RollTerm>|RollTerm} Returns a Promise if the term is non-deterministic.
|
|
* @protected
|
|
*/
|
|
_evaluate(options={}) {
|
|
return this;
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
/**
|
|
* Determine if evaluating a given RollTerm with certain evaluation options can be done so deterministically.
|
|
* @param {RollTerm} term The term.
|
|
* @param {object} [options] Options for evaluating the term.
|
|
* @param {boolean} [options.maximize] Force the result to be maximized.
|
|
* @param {boolean} [options.minimize] Force the result to be minimized.
|
|
*/
|
|
static isDeterministic(term, { maximize, minimize }={}) {
|
|
return maximize || minimize || term.isDeterministic;
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
/* Serialization and Loading */
|
|
/* -------------------------------------------- */
|
|
|
|
/**
|
|
* Construct a RollTerm from a provided data object
|
|
* @param {object} data Provided data from an un-serialized term
|
|
* @returns {RollTerm} The constructed RollTerm
|
|
*/
|
|
static fromData(data) {
|
|
let cls = CONFIG.Dice.termTypes[data.class];
|
|
if ( !cls ) {
|
|
cls = Object.values(CONFIG.Dice.terms).find(c => c.name === data.class) || foundry.dice.terms.Die;
|
|
}
|
|
return cls._fromData(data);
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
/**
|
|
* Construct a RollTerm from parser information.
|
|
* @param {RollParseNode} node The node.
|
|
* @returns {RollTerm}
|
|
*/
|
|
static fromParseNode(node) {
|
|
return this.fromData(deepClone(node));
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
/**
|
|
* Define term-specific logic for how a de-serialized data object is restored as a functional RollTerm
|
|
* @param {object} data The de-serialized term data
|
|
* @returns {RollTerm} The re-constructed RollTerm object
|
|
* @protected
|
|
*/
|
|
static _fromData(data) {
|
|
if ( data.roll && !(data.roll instanceof Roll) ) data.roll = Roll.fromData(data.roll);
|
|
const term = new this(data);
|
|
term._evaluated = data.evaluated ?? true;
|
|
return term;
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
/**
|
|
* Reconstruct a RollTerm instance from a provided JSON string
|
|
* @param {string} json A serialized JSON representation of a DiceTerm
|
|
* @return {RollTerm} A reconstructed RollTerm from the provided JSON
|
|
*/
|
|
static fromJSON(json) {
|
|
let data;
|
|
try {
|
|
data = JSON.parse(json);
|
|
} catch(err) {
|
|
throw new Error("You must pass a valid JSON string");
|
|
}
|
|
return this.fromData(data);
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
/**
|
|
* Serialize the RollTerm to a JSON string which allows it to be saved in the database or embedded in text.
|
|
* This method should return an object suitable for passing to the JSON.stringify function.
|
|
* @return {object}
|
|
*/
|
|
toJSON() {
|
|
const data = {
|
|
class: this.constructor.name,
|
|
options: this.options,
|
|
evaluated: this._evaluated
|
|
};
|
|
for ( let attr of this.constructor.SERIALIZE_ATTRIBUTES ) {
|
|
data[attr] = this[attr];
|
|
}
|
|
return data;
|
|
}
|
|
}
|