Initial
This commit is contained in:
705
resources/app/client-esm/dice/terms/dice.mjs
Normal file
705
resources/app/client-esm/dice/terms/dice.mjs
Normal file
@@ -0,0 +1,705 @@
|
||||
import RollTerm from "./term.mjs";
|
||||
|
||||
/**
|
||||
* @typedef {import("../_types.mjs").DiceTermResult} DiceTermResult
|
||||
*/
|
||||
|
||||
/**
|
||||
* An abstract base class for any type of RollTerm which involves randomized input from dice, coins, or other devices.
|
||||
* @extends RollTerm
|
||||
*/
|
||||
export default class DiceTerm extends RollTerm {
|
||||
/**
|
||||
* @param {object} termData Data used to create the Dice Term, including the following:
|
||||
* @param {number|Roll} [termData.number=1] The number of dice of this term to roll, before modifiers are applied, or
|
||||
* a Roll instance that will be evaluated to a number.
|
||||
* @param {number|Roll} termData.faces The number of faces on each die of this type, or a Roll instance that
|
||||
* will be evaluated to a number.
|
||||
* @param {string[]} [termData.modifiers] An array of modifiers applied to the results
|
||||
* @param {object[]} [termData.results] An optional array of pre-cast results for the term
|
||||
* @param {object} [termData.options] Additional options that modify the term
|
||||
*/
|
||||
constructor({number=1, faces=6, method, modifiers=[], results=[], options={}}) {
|
||||
super({options});
|
||||
|
||||
this._number = number;
|
||||
this._faces = faces;
|
||||
this.method = method;
|
||||
this.modifiers = modifiers;
|
||||
this.results = results;
|
||||
|
||||
// If results were explicitly passed, the term has already been evaluated
|
||||
if ( results.length ) this._evaluated = true;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* The resolution method used to resolve this DiceTerm.
|
||||
* @type {string}
|
||||
*/
|
||||
get method() {
|
||||
return this.#method;
|
||||
}
|
||||
|
||||
set method(method) {
|
||||
if ( this.#method || !(method in CONFIG.Dice.fulfillment.methods) ) return;
|
||||
this.#method = method;
|
||||
}
|
||||
|
||||
#method;
|
||||
|
||||
/**
|
||||
* An Array of dice term modifiers which are applied
|
||||
* @type {string[]}
|
||||
*/
|
||||
modifiers;
|
||||
|
||||
/**
|
||||
* The array of dice term results which have been rolled
|
||||
* @type {DiceTermResult[]}
|
||||
*/
|
||||
results;
|
||||
|
||||
/**
|
||||
* Define the denomination string used to register this DiceTerm type in CONFIG.Dice.terms
|
||||
* @type {string}
|
||||
*/
|
||||
static DENOMINATION = "";
|
||||
|
||||
/**
|
||||
* Define the named modifiers that can be applied for this particular DiceTerm type.
|
||||
* @type {Record<string, string|Function>}
|
||||
*/
|
||||
static MODIFIERS = {};
|
||||
|
||||
/**
|
||||
* A regular expression pattern which captures the full set of term modifiers
|
||||
* Anything until a space, group symbol, or arithmetic operator
|
||||
* @type {string}
|
||||
*/
|
||||
static MODIFIERS_REGEXP_STRING = "([^ (){}[\\]+\\-*/]+)";
|
||||
|
||||
/**
|
||||
* A regular expression used to separate individual modifiers
|
||||
* @type {RegExp}
|
||||
*/
|
||||
static MODIFIER_REGEXP = /([A-z]+)([^A-z\s()+\-*\/]+)?/g
|
||||
|
||||
/** @inheritdoc */
|
||||
static REGEXP = new RegExp(`^([0-9]+)?[dD]([A-z]|[0-9]+)${this.MODIFIERS_REGEXP_STRING}?${this.FLAVOR_REGEXP_STRING}?$`);
|
||||
|
||||
/** @inheritdoc */
|
||||
static SERIALIZE_ATTRIBUTES = ["number", "faces", "modifiers", "results", "method"];
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Dice Term Attributes */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* The number of dice of this term to roll. Returns undefined if the number is a complex term that has not yet been
|
||||
* evaluated.
|
||||
* @type {number|void}
|
||||
*/
|
||||
get number() {
|
||||
if ( typeof this._number === "number" ) return this._number;
|
||||
else if ( this._number?._evaluated ) return this._number.total;
|
||||
}
|
||||
|
||||
/**
|
||||
* The number of dice of this term to roll, before modifiers are applied, or a Roll instance that will be evaluated to
|
||||
* a number.
|
||||
* @type {number|Roll}
|
||||
* @protected
|
||||
*/
|
||||
_number;
|
||||
|
||||
set number(value) {
|
||||
this._number = value;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* The number of faces on the die. Returns undefined if the faces are represented as a complex term that has not yet
|
||||
* been evaluated.
|
||||
* @type {number|void}
|
||||
*/
|
||||
get faces() {
|
||||
if ( typeof this._faces === "number" ) return this._faces;
|
||||
else if ( this._faces?._evaluated ) return this._faces.total;
|
||||
}
|
||||
|
||||
/**
|
||||
* The number of faces on the die, or a Roll instance that will be evaluated to a number.
|
||||
* @type {number|Roll}
|
||||
* @protected
|
||||
*/
|
||||
_faces;
|
||||
|
||||
set faces(value) {
|
||||
this._faces = value;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
get expression() {
|
||||
const x = this.constructor.DENOMINATION === "d" ? this._faces : this.constructor.DENOMINATION;
|
||||
return `${this._number}d${x}${this.modifiers.join("")}`;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* The denomination of this DiceTerm instance.
|
||||
* @type {string}
|
||||
*/
|
||||
get denomination() {
|
||||
return this.constructor.DENOMINATION;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* An array of additional DiceTerm instances involved in resolving this DiceTerm.
|
||||
* @type {DiceTerm[]}
|
||||
*/
|
||||
get dice() {
|
||||
const dice = [];
|
||||
if ( this._number instanceof Roll ) dice.push(...this._number.dice);
|
||||
if ( this._faces instanceof Roll ) dice.push(...this._faces.dice);
|
||||
return dice;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
get total() {
|
||||
if ( !this._evaluated ) return undefined;
|
||||
let total = this.results.reduce((t, r) => {
|
||||
if ( !r.active ) return t;
|
||||
if ( r.count !== undefined ) return t + r.count;
|
||||
else return t + r.result;
|
||||
}, 0);
|
||||
if ( this.number < 0 ) total *= -1;
|
||||
return total;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Return an array of rolled values which are still active within this term
|
||||
* @type {number[]}
|
||||
*/
|
||||
get values() {
|
||||
return this.results.reduce((arr, r) => {
|
||||
if ( !r.active ) return arr;
|
||||
arr.push(r.result);
|
||||
return arr;
|
||||
}, []);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
get isDeterministic() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Dice Term Methods */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Alter the DiceTerm by adding or multiplying the number of dice which are rolled
|
||||
* @param {number} multiply A factor to multiply. Dice are multiplied before any additions.
|
||||
* @param {number} add A number of dice to add. Dice are added after multiplication.
|
||||
* @returns {DiceTerm} The altered term
|
||||
*/
|
||||
alter(multiply, add) {
|
||||
if ( this._evaluated ) throw new Error(`You may not alter a DiceTerm after it has already been evaluated`);
|
||||
multiply = Number.isFinite(multiply) && (multiply >= 0) ? multiply : 1;
|
||||
add = Number.isInteger(add) ? add : 0;
|
||||
if ( multiply >= 0 ) {
|
||||
if ( this._number instanceof Roll ) this._number = Roll.create(`(${this._number} * ${multiply})`);
|
||||
else this._number = Math.round(this.number * multiply);
|
||||
}
|
||||
if ( add ) {
|
||||
if ( this._number instanceof Roll ) this._number = Roll.create(`(${this._number} + ${add})`);
|
||||
else this._number += add;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritDoc */
|
||||
_evaluate(options={}) {
|
||||
if ( RollTerm.isDeterministic(this, options) ) return this._evaluateSync(options);
|
||||
return this._evaluateAsync(options);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Evaluate this dice term asynchronously.
|
||||
* @param {object} [options] Options forwarded to inner Roll evaluation.
|
||||
* @returns {Promise<DiceTerm>}
|
||||
* @protected
|
||||
*/
|
||||
async _evaluateAsync(options={}) {
|
||||
for ( const roll of [this._faces, this._number] ) {
|
||||
if ( !(roll instanceof Roll) ) continue;
|
||||
if ( this._root ) roll._root = this._root;
|
||||
await roll.evaluate(options);
|
||||
}
|
||||
if ( Math.abs(this.number) > 999 ) {
|
||||
throw new Error("You may not evaluate a DiceTerm with more than 999 requested results");
|
||||
}
|
||||
// If this term was an intermediate term, it has not yet been added to the resolver, so we add it here.
|
||||
if ( this.resolver && !this._id ) await this.resolver.addTerm(this);
|
||||
for ( let n = this.results.length; n < Math.abs(this.number); n++ ) await this.roll(options);
|
||||
await this._evaluateModifiers();
|
||||
return this;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Evaluate deterministic values of this term synchronously.
|
||||
* @param {object} [options]
|
||||
* @param {boolean} [options.maximize] Force the result to be maximized.
|
||||
* @param {boolean} [options.minimize] Force the result to be minimized.
|
||||
* @param {boolean} [options.strict] Throw an error if attempting to evaluate a die term in a way that cannot be
|
||||
* done synchronously.
|
||||
* @returns {DiceTerm}
|
||||
* @protected
|
||||
*/
|
||||
_evaluateSync(options={}) {
|
||||
if ( this._faces instanceof Roll ) this._faces.evaluateSync(options);
|
||||
if ( this._number instanceof Roll ) this._number.evaluateSync(options);
|
||||
if ( Math.abs(this.number) > 999 ) {
|
||||
throw new Error("You may not evaluate a DiceTerm with more than 999 requested results");
|
||||
}
|
||||
for ( let n = this.results.length; n < Math.abs(this.number); n++ ) {
|
||||
const roll = { active: true };
|
||||
if ( options.minimize ) roll.result = Math.min(1, this.faces);
|
||||
else if ( options.maximize ) roll.result = this.faces;
|
||||
else if ( options.strict ) throw new Error("Cannot synchronously evaluate a non-deterministic term.");
|
||||
else continue;
|
||||
this.results.push(roll);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Roll the DiceTerm by mapping a random uniform draw against the faces of the dice term.
|
||||
* @param {object} [options={}] Options which modify how a random result is produced
|
||||
* @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.
|
||||
* @returns {Promise<DiceTermResult>} The produced result
|
||||
*/
|
||||
async roll({minimize=false, maximize=false, ...options}={}) {
|
||||
const roll = {result: undefined, active: true};
|
||||
roll.result = await this._roll(options);
|
||||
if ( minimize ) roll.result = Math.min(1, this.faces);
|
||||
else if ( maximize ) roll.result = this.faces;
|
||||
else if ( roll.result === undefined ) roll.result = this.randomFace();
|
||||
this.results.push(roll);
|
||||
return roll;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Generate a roll result value for this DiceTerm based on its fulfillment method.
|
||||
* @param {object} [options] Options forwarded to the fulfillment method handler.
|
||||
* @returns {Promise<number|void>} Returns a Promise that resolves to the fulfilled number, or undefined if it could
|
||||
* not be fulfilled.
|
||||
* @protected
|
||||
*/
|
||||
async _roll(options={}) {
|
||||
return this.#invokeFulfillmentHandler(options);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Invoke the configured fulfillment handler for this term to produce a result value.
|
||||
* @param {object} [options] Options forwarded to the fulfillment method handler.
|
||||
* @returns {Promise<number|void>} Returns a Promise that resolves to the fulfilled number, or undefined if it could
|
||||
* not be fulfilled.
|
||||
*/
|
||||
async #invokeFulfillmentHandler(options={}) {
|
||||
const config = game.settings.get("core", "diceConfiguration");
|
||||
const method = config[this.denomination] || CONFIG.Dice.fulfillment.defaultMethod;
|
||||
if ( (method === "manual") && !game.user.hasPermission("MANUAL_ROLLS") ) return;
|
||||
const { handler, interactive } = CONFIG.Dice.fulfillment.methods[method] ?? {};
|
||||
if ( interactive && this.resolver ) return this.resolver.resolveResult(this, method, options);
|
||||
return handler?.(this, options);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Maps a randomly-generated value in the interval [0, 1) to a face value on the die.
|
||||
* @param {number} randomUniform A value to map. Must be in the interval [0, 1).
|
||||
* @returns {number} The face value.
|
||||
*/
|
||||
mapRandomFace(randomUniform) {
|
||||
return Math.ceil((1 - randomUniform) * this.faces);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Generate a random face value for this die using the configured PRNG.
|
||||
* @returns {number}
|
||||
*/
|
||||
randomFace() {
|
||||
return this.mapRandomFace(CONFIG.Dice.randomUniform());
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Return a string used as the label for each rolled result
|
||||
* @param {DiceTermResult} result The rolled result
|
||||
* @returns {string} The result label
|
||||
*/
|
||||
getResultLabel(result) {
|
||||
return String(result.result);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Get the CSS classes that should be used to display each rolled result
|
||||
* @param {DiceTermResult} result The rolled result
|
||||
* @returns {string[]} The desired classes
|
||||
*/
|
||||
getResultCSS(result) {
|
||||
const hasSuccess = result.success !== undefined;
|
||||
const hasFailure = result.failure !== undefined;
|
||||
const isMax = result.result === this.faces;
|
||||
const isMin = result.result === 1;
|
||||
return [
|
||||
this.constructor.name.toLowerCase(),
|
||||
"d" + this.faces,
|
||||
result.success ? "success" : null,
|
||||
result.failure ? "failure" : null,
|
||||
result.rerolled ? "rerolled" : null,
|
||||
result.exploded ? "exploded" : null,
|
||||
result.discarded ? "discarded" : null,
|
||||
!(hasSuccess || hasFailure) && isMin ? "min" : null,
|
||||
!(hasSuccess || hasFailure) && isMax ? "max" : null
|
||||
]
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Render the tooltip HTML for a Roll instance
|
||||
* @returns {object} The data object used to render the default tooltip template for this DiceTerm
|
||||
*/
|
||||
getTooltipData() {
|
||||
const { total, faces, flavor } = this;
|
||||
const method = CONFIG.Dice.fulfillment.methods[this.method];
|
||||
const icon = method?.interactive ? (method.icon ?? '<i class="fas fa-bluetooth"></i>') : null;
|
||||
return {
|
||||
total, faces, flavor, icon,
|
||||
method: method?.label,
|
||||
formula: this.expression,
|
||||
rolls: this.results.map(r => {
|
||||
return {
|
||||
result: this.getResultLabel(r),
|
||||
classes: this.getResultCSS(r).filterJoin(" ")
|
||||
};
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Modifier Methods */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Sequentially evaluate each dice roll modifier by passing the term to its evaluation function
|
||||
* Augment or modify the results array.
|
||||
* @internal
|
||||
*/
|
||||
async _evaluateModifiers() {
|
||||
const cls = this.constructor;
|
||||
const requested = foundry.utils.deepClone(this.modifiers);
|
||||
this.modifiers = [];
|
||||
|
||||
// Sort modifiers from longest to shortest to ensure that the matching algorithm greedily matches the longest
|
||||
// prefixes first.
|
||||
const allModifiers = Object.keys(cls.MODIFIERS).sort((a, b) => b.length - a.length);
|
||||
|
||||
// Iterate over requested modifiers
|
||||
for ( const m of requested ) {
|
||||
let command = m.match(/[A-z]+/)[0].toLowerCase();
|
||||
|
||||
// Matched command
|
||||
if ( command in cls.MODIFIERS ) {
|
||||
await this._evaluateModifier(command, m);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Unmatched compound command
|
||||
while ( command ) {
|
||||
let matched = false;
|
||||
for ( const modifier of allModifiers ) {
|
||||
if ( command.startsWith(modifier) ) {
|
||||
matched = true;
|
||||
await this._evaluateModifier(modifier, modifier);
|
||||
command = command.replace(modifier, "");
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ( !matched ) command = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Asynchronously evaluate a single modifier command, recording it in the array of evaluated modifiers
|
||||
* @param {string} command The parsed modifier command
|
||||
* @param {string} modifier The full modifier request
|
||||
* @internal
|
||||
*/
|
||||
async _evaluateModifier(command, modifier) {
|
||||
let fn = this.constructor.MODIFIERS[command];
|
||||
if ( typeof fn === "string" ) fn = this[fn];
|
||||
if ( fn instanceof Function ) {
|
||||
const result = await fn.call(this, modifier);
|
||||
const earlyReturn = (result === false) || (result === this); // handling this is backwards compatibility
|
||||
if ( !earlyReturn ) this.modifiers.push(modifier.toLowerCase());
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* A helper comparison function.
|
||||
* Returns a boolean depending on whether the result compares favorably against the target.
|
||||
* @param {number} result The result being compared
|
||||
* @param {string} comparison The comparison operator in [=,<,<=,>,>=]
|
||||
* @param {number} target The target value
|
||||
* @returns {boolean} Is the comparison true?
|
||||
*/
|
||||
static compareResult(result, comparison, target) {
|
||||
switch ( comparison ) {
|
||||
case "=":
|
||||
return result === target;
|
||||
case "<":
|
||||
return result < target;
|
||||
case "<=":
|
||||
return result <= target;
|
||||
case ">":
|
||||
return result > target;
|
||||
case ">=":
|
||||
return result >= target;
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* A helper method to modify the results array of a dice term by flagging certain results are kept or dropped.
|
||||
* @param {object[]} results The results array
|
||||
* @param {number} number The number to keep or drop
|
||||
* @param {boolean} [keep] Keep results?
|
||||
* @param {boolean} [highest] Keep the highest?
|
||||
* @returns {object[]} The modified results array
|
||||
*/
|
||||
static _keepOrDrop(results, number, {keep=true, highest=true}={}) {
|
||||
|
||||
// Sort remaining active results in ascending (keep) or descending (drop) order
|
||||
const ascending = keep === highest;
|
||||
const values = results.reduce((arr, r) => {
|
||||
if ( r.active ) arr.push(r.result);
|
||||
return arr;
|
||||
}, []).sort((a, b) => ascending ? a - b : b - a);
|
||||
|
||||
// Determine the cut point, beyond which to discard
|
||||
number = Math.clamp(keep ? values.length - number : number, 0, values.length);
|
||||
const cut = values[number];
|
||||
|
||||
// Track progress
|
||||
let discarded = 0;
|
||||
const ties = [];
|
||||
let comp = ascending ? "<" : ">";
|
||||
|
||||
// First mark results on the wrong side of the cut as discarded
|
||||
results.forEach(r => {
|
||||
if ( !r.active ) return; // Skip results which have already been discarded
|
||||
let discard = this.compareResult(r.result, comp, cut);
|
||||
if ( discard ) {
|
||||
r.discarded = true;
|
||||
r.active = false;
|
||||
discarded++;
|
||||
}
|
||||
else if ( r.result === cut ) ties.push(r);
|
||||
});
|
||||
|
||||
// Next discard ties until we have reached the target
|
||||
ties.forEach(r => {
|
||||
if ( discarded < number ) {
|
||||
r.discarded = true;
|
||||
r.active = false;
|
||||
discarded++;
|
||||
}
|
||||
});
|
||||
return results;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* A reusable helper function to handle the identification and deduction of failures
|
||||
*/
|
||||
static _applyCount(results, comparison, target, {flagSuccess=false, flagFailure=false}={}) {
|
||||
for ( let r of results ) {
|
||||
let success = this.compareResult(r.result, comparison, target);
|
||||
if (flagSuccess) {
|
||||
r.success = success;
|
||||
if (success) delete r.failure;
|
||||
}
|
||||
else if (flagFailure ) {
|
||||
r.failure = success;
|
||||
if (success) delete r.success;
|
||||
}
|
||||
r.count = success ? 1 : 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* A reusable helper function to handle the identification and deduction of failures
|
||||
*/
|
||||
static _applyDeduct(results, comparison, target, {deductFailure=false, invertFailure=false}={}) {
|
||||
for ( let r of results ) {
|
||||
|
||||
// Flag failures if a comparison was provided
|
||||
if (comparison) {
|
||||
const fail = this.compareResult(r.result, comparison, target);
|
||||
if ( fail ) {
|
||||
r.failure = true;
|
||||
delete r.success;
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise treat successes as failures
|
||||
else {
|
||||
if ( r.success === false ) {
|
||||
r.failure = true;
|
||||
delete r.success;
|
||||
}
|
||||
}
|
||||
|
||||
// Deduct failures
|
||||
if ( deductFailure ) {
|
||||
if ( r.failure ) r.count = -1;
|
||||
}
|
||||
else if ( invertFailure ) {
|
||||
if ( r.failure ) r.count = -1 * r.result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Factory Methods */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Determine whether a string expression matches this type of term
|
||||
* @param {string} expression The expression to parse
|
||||
* @param {object} [options={}] Additional options which customize the match
|
||||
* @param {boolean} [options.imputeNumber=true] Allow the number of dice to be optional, i.e. "d6"
|
||||
* @returns {RegExpMatchArray|null}
|
||||
*/
|
||||
static matchTerm(expression, {imputeNumber=true}={}) {
|
||||
const match = expression.match(this.REGEXP);
|
||||
if ( !match ) return null;
|
||||
if ( (match[1] === undefined) && !imputeNumber ) return null;
|
||||
return match;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Construct a term of this type given a matched regular expression array.
|
||||
* @param {RegExpMatchArray} match The matched regular expression array
|
||||
* @returns {DiceTerm} The constructed term
|
||||
*/
|
||||
static fromMatch(match) {
|
||||
let [number, denomination, modifiers, flavor] = match.slice(1);
|
||||
|
||||
// Get the denomination of DiceTerm
|
||||
denomination = denomination.toLowerCase();
|
||||
const cls = denomination in CONFIG.Dice.terms ? CONFIG.Dice.terms[denomination] : CONFIG.Dice.terms.d;
|
||||
if ( !foundry.utils.isSubclass(cls, foundry.dice.terms.DiceTerm) ) {
|
||||
throw new Error(`DiceTerm denomination ${denomination} not registered to CONFIG.Dice.terms as a valid DiceTerm class`);
|
||||
}
|
||||
|
||||
// Get the term arguments
|
||||
number = Number.isNumeric(number) ? parseInt(number) : 1;
|
||||
const faces = Number.isNumeric(denomination) ? parseInt(denomination) : null;
|
||||
|
||||
// Match modifiers
|
||||
modifiers = Array.from((modifiers || "").matchAll(this.MODIFIER_REGEXP)).map(m => m[0]);
|
||||
|
||||
// Construct a term of the appropriate denomination
|
||||
return new cls({number, faces, modifiers, options: {flavor}});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
static fromParseNode(node) {
|
||||
let { number, faces } = node;
|
||||
let denomination = "d";
|
||||
if ( number === null ) number = 1;
|
||||
if ( number.class ) {
|
||||
number = Roll.defaultImplementation.fromTerms(Roll.defaultImplementation.instantiateAST(number));
|
||||
}
|
||||
if ( typeof faces === "string" ) denomination = faces.toLowerCase();
|
||||
else if ( faces.class ) {
|
||||
faces = Roll.defaultImplementation.fromTerms(Roll.defaultImplementation.instantiateAST(faces));
|
||||
}
|
||||
const modifiers = Array.from((node.modifiers || "").matchAll(this.MODIFIER_REGEXP)).map(([m]) => m);
|
||||
const cls = CONFIG.Dice.terms[denomination];
|
||||
const data = { ...node, number, modifiers, class: cls.name };
|
||||
if ( denomination === "d" ) data.faces = faces;
|
||||
return this.fromData(data);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Serialization & Loading */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritDoc */
|
||||
toJSON() {
|
||||
const data = super.toJSON();
|
||||
if ( this._number instanceof Roll ) data._number = this._number.toJSON();
|
||||
if ( this._faces instanceof Roll ) data._faces = this._faces.toJSON();
|
||||
return data;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritDoc */
|
||||
static _fromData(data) {
|
||||
if ( data._number ) data.number = Roll.fromData(data._number);
|
||||
if ( data._faces ) data.faces = Roll.fromData(data._faces);
|
||||
return super._fromData(data);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user