Initial
This commit is contained in:
177
resources/app/client-esm/dice/terms/function.mjs
Normal file
177
resources/app/client-esm/dice/terms/function.mjs
Normal file
@@ -0,0 +1,177 @@
|
||||
import RollTerm from "./term.mjs";
|
||||
import DiceTerm from "./dice.mjs";
|
||||
|
||||
/**
|
||||
* A type of RollTerm used to apply a function.
|
||||
* @extends {RollTerm}
|
||||
*/
|
||||
export default class FunctionTerm extends RollTerm {
|
||||
constructor({fn, terms=[], rolls=[], result, options}={}) {
|
||||
super({options});
|
||||
this.fn = fn;
|
||||
this.terms = terms;
|
||||
this.rolls = (rolls.length === terms.length) ? rolls : this.terms.map(t => Roll.create(t));
|
||||
this.result = result;
|
||||
if ( result !== undefined ) this._evaluated = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* The name of the configured function, or one in the Math environment, which should be applied to the term
|
||||
* @type {string}
|
||||
*/
|
||||
fn;
|
||||
|
||||
/**
|
||||
* An array of string argument terms for the function
|
||||
* @type {string[]}
|
||||
*/
|
||||
terms;
|
||||
|
||||
/**
|
||||
* The cached Roll instances for each function argument
|
||||
* @type {Roll[]}
|
||||
*/
|
||||
rolls = [];
|
||||
|
||||
/**
|
||||
* The cached result of evaluating the method arguments
|
||||
* @type {string|number}
|
||||
*/
|
||||
result;
|
||||
|
||||
/** @inheritdoc */
|
||||
isIntermediate = true;
|
||||
|
||||
/** @inheritdoc */
|
||||
static SERIALIZE_ATTRIBUTES = ["fn", "terms", "rolls", "result"];
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Function Term Attributes */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* An array of evaluated DiceTerm instances that should be bubbled up to the parent Roll
|
||||
* @type {DiceTerm[]}
|
||||
*/
|
||||
get dice() {
|
||||
return this.rolls.flatMap(r => r.dice);
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
get total() {
|
||||
return this.result;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
get expression() {
|
||||
return `${this.fn}(${this.terms.join(",")})`;
|
||||
}
|
||||
|
||||
/**
|
||||
* The function this term represents.
|
||||
* @returns {RollFunction}
|
||||
*/
|
||||
get function() {
|
||||
return CONFIG.Dice.functions[this.fn] ?? Math[this.fn];
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
get isDeterministic() {
|
||||
if ( this.function?.constructor.name === "AsyncFunction" ) return false;
|
||||
return this.terms.every(t => Roll.create(t).isDeterministic);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Math Term Methods */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
_evaluate(options={}) {
|
||||
if ( RollTerm.isDeterministic(this, options) ) return this._evaluateSync(options);
|
||||
return this._evaluateAsync(options);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Evaluate this function when it contains any non-deterministic sub-terms.
|
||||
* @param {object} [options]
|
||||
* @returns {Promise<RollTerm>}
|
||||
* @protected
|
||||
*/
|
||||
async _evaluateAsync(options={}) {
|
||||
const args = await Promise.all(this.rolls.map(async roll => {
|
||||
if ( this._root ) roll._root = this._root;
|
||||
await roll.evaluate({ ...options, allowStrings: true });
|
||||
roll.propagateFlavor(this.flavor);
|
||||
return this.#parseArgument(roll);
|
||||
}));
|
||||
this.result = await this.function(...args);
|
||||
if ( !options.allowStrings ) this.result = Number(this.result);
|
||||
return this;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Evaluate this function when it contains only deterministic sub-terms.
|
||||
* @param {object} [options]
|
||||
* @returns {RollTerm}
|
||||
* @protected
|
||||
*/
|
||||
_evaluateSync(options={}) {
|
||||
const args = [];
|
||||
for ( const roll of this.rolls ) {
|
||||
roll.evaluateSync({ ...options, allowStrings: true });
|
||||
roll.propagateFlavor(this.flavor);
|
||||
args.push(this.#parseArgument(roll));
|
||||
}
|
||||
this.result = this.function(...args);
|
||||
if ( !options.allowStrings ) this.result = Number(this.result);
|
||||
return this;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Parse a function argument from its evaluated Roll instance.
|
||||
* @param {Roll} roll The evaluated Roll instance that wraps the argument.
|
||||
* @returns {string|number}
|
||||
*/
|
||||
#parseArgument(roll) {
|
||||
const { product } = roll;
|
||||
if ( typeof product !== "string" ) return product;
|
||||
const [, value] = product.match(/^\$([^$]+)\$$/) || [];
|
||||
return value ? JSON.parse(value) : product;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Saving and Loading */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritDoc */
|
||||
static _fromData(data) {
|
||||
data.rolls = (data.rolls || []).map(r => r instanceof Roll ? r : Roll.fromData(r));
|
||||
return super._fromData(data);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritDoc */
|
||||
toJSON() {
|
||||
const data = super.toJSON();
|
||||
data.rolls = data.rolls.map(r => r.toJSON());
|
||||
return data;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
static fromParseNode(node) {
|
||||
const rolls = node.terms.map(t => {
|
||||
return Roll.defaultImplementation.fromTerms(Roll.defaultImplementation.instantiateAST(t));
|
||||
});
|
||||
const modifiers = Array.from((node.modifiers || "").matchAll(DiceTerm.MODIFIER_REGEXP)).map(([m]) => m);
|
||||
return this.fromData({ ...node, rolls, modifiers, terms: rolls.map(r => r.formula) });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user