@dice-roller/rpg-dice-roller
Version:
An advanced JS based dice roller that can roll various types of dice and modifiers, along with mathematical equations.
1,835 lines (1,709 loc) • 197 kB
JavaScript
/**
* @dice-roller/rpg-dice-roller - An advanced JS based dice roller that can roll various types of dice and modifiers, along with mathematical equations.
*
* @version 5.5.1
* @license MIT
* @author GreenImp Media <info@greenimp.co.uk> (https://greenimp.co.uk)
* @link https://dice-roller.github.io/documentation
*/
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('mathjs'), require('random-js')) :
typeof define === 'function' && define.amd ? define(['exports', 'mathjs', 'random-js'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.rpgDiceRoller = {}, global.math, global.Random));
})(this, (function (exports, mathjs, randomJs) { 'use strict';
/**
* An error thrown when a comparison operator is invalid
*/
class CompareOperatorError extends TypeError {
/**
* Create a `CompareOperatorError`
*
* @param {*} operator The invalid operator
*/
constructor(operator) {
super(`Operator "${operator}" is invalid`);
// Maintains proper stack trace for where our error was thrown (only available on V8)
if (TypeError.captureStackTrace) {
TypeError.captureStackTrace(this, CompareOperatorError);
}
this.name = 'CompareOperatorError';
this.operator = operator;
}
}
/**
* An error thrown when a data format is invalid
*/
class DataFormatError extends Error {
/**
* Create a `DataFormatError`
*
* @param {*} data The invalid data
*/
constructor(data) {
super(`Invalid data format: ${data}`);
// Maintains proper stack trace for where our error was thrown (only available on V8)
if (Error.captureStackTrace) {
Error.captureStackTrace(this, DataFormatError);
}
this.name = 'ImportError';
this.data = data;
}
}
/**
* An error thrown when an invalid die action (e.g. Exploding on a d1) occurs
*/
class DieActionValueError extends Error {
/**
* Create a `DieActionValueError`
*
* @param {StandardDice} die The die the action was on
* @param {string|null} [action=null] The invalid action
*/
constructor(die) {
let action = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
super(`Die "${die}" must have more than 1 possible value to ${action || 'do this action'}`);
// Maintains proper stack trace for where our error was thrown (only available on V8)
if (Error.captureStackTrace) {
Error.captureStackTrace(this, DieActionValueError);
}
this.name = 'DieActionValueError';
this.action = action;
this.die = die;
}
}
/**
* An error thrown when the notation is invalid
*/
class NotationError extends Error {
/**
* Create a `NotationError`
*
* @param {*} notation The invalid notation
*/
constructor(notation) {
super(`Notation "${notation}" is invalid`);
// Maintains proper stack trace for where our error was thrown (only available on V8)
if (Error.captureStackTrace) {
Error.captureStackTrace(this, NotationError);
}
this.name = 'NotationError';
this.notation = notation;
}
}
/**
* An error thrown when a required argument is missing
*/
class RequiredArgumentError extends Error {
/**
* Create a `RequiredArgumentError`
*
* @param {string|null} [argumentName=null] The argument name
*/
constructor() {
let argumentName = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null;
super(`Missing argument${argumentName ? ` "${argumentName}"` : ''}`);
// Maintains proper stack trace for where our error was thrown (only available on V8)
if (Error.captureStackTrace) {
Error.captureStackTrace(this, RequiredArgumentError);
}
this.argumentName = argumentName;
}
}
var index$3 = /*#__PURE__*/Object.freeze({
__proto__: null,
CompareOperatorError: CompareOperatorError,
DataFormatError: DataFormatError,
DieActionValueError: DieActionValueError,
NotationError: NotationError,
RequiredArgumentError: RequiredArgumentError
});
/**
* Check if `a` is comparative to `b` with the given operator.
*
* @example <caption>Is `a` greater than `b`?</caption>
* const a = 4;
* const b = 2;
*
* compareNumber(a, b, '>'); // true
*
* @example <caption>Is `a` equal to `b`?</caption>
* const a = 4;
* const b = 2;
*
* compareNumber(a, b, '='); // false
*
* @param {number} a The number to compare with `b`
* @param {number} b The number to compare with `a`
* @param {string} operator A valid comparative operator: `=, <, >, <=, >=, !=, <>`
*
* @returns {boolean} `true` if the comparison matches, `false` otherwise
*/
const compareNumbers = (a, b, operator) => {
const aNum = Number(a);
const bNum = Number(b);
let result;
if (Number.isNaN(aNum) || Number.isNaN(bNum)) {
return false;
}
switch (operator) {
case '=':
case '==':
result = aNum === bNum;
break;
case '<':
result = aNum < bNum;
break;
case '>':
result = aNum > bNum;
break;
case '<=':
result = aNum <= bNum;
break;
case '>=':
result = aNum >= bNum;
break;
case '!':
case '!=':
case '<>':
result = aNum !== bNum;
break;
default:
result = false;
break;
}
return result;
};
/**
* Evaluate mathematical strings.
*
* @example
* evaluate('5+6'); // 11
*
* @param {string} equation The mathematical equation to compute.
*
* @returns {number} The result of the equation
*/
const evaluate = equation => mathjs.evaluate(equation);
/**
* Check if the given value is a valid finite number.
*
* @param {*} val
*
* @returns {boolean} `true` if it is a finite number, `false` otherwise
*/
const isNumeric = val => {
if (typeof val !== 'number' && typeof val !== 'string') {
return false;
}
return !Number.isNaN(val) && Number.isFinite(Number(val));
};
/**
* Check if the given value is a "safe" number.
*
* A "safe" number falls within the `Number.MAX_SAFE_INTEGER` and `Number.MIN_SAFE_INTEGER` values
* (Inclusive).
*
* @param {*} val
*
* @returns {boolean} `true` if the value is a "safe" number, `false` otherwise
*/
const isSafeNumber = val => {
if (!isNumeric(val)) {
return false;
}
const castVal = Number(val);
return castVal <= Number.MAX_SAFE_INTEGER && castVal >= Number.MIN_SAFE_INTEGER;
};
/**
* Take an array of numbers and add the values together.
*
* @param {number[]} numbers
*
* @returns {number} The summed value
*/
const sumArray = numbers => !Array.isArray(numbers) ? 0 : numbers.reduce((prev, current) => prev + (isNumeric(current) ? parseFloat(`${current}`) : 0), 0);
/**
* Round a number to the given amount of digits after the decimal point, removing any trailing
* zeros after the decimal point.
*
* @example
* toFixed(1.236, 2); // 1.24
* toFixed(30.1, 2); // 30.1
* toFixed(4.0000000004, 3); // 4
*
* @param {number} num The number to round
* @param {number} [precision=0] The number of digits after the decimal point
*
* @returns {number}
*/
const toFixed = function (num) {
let precision = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
return (
// round to precision, then cast to a number to remove trailing zeroes after the decimal point
parseFloat(parseFloat(`${num}`).toFixed(precision || 0))
);
};
/**
* The engine
*
* @type {symbol}
*
* @private
*/
const engineSymbol = Symbol('engine');
/**
* The random object
*
* @type {symbol}
*
* @private
*/
const randomSymbol = Symbol('random');
/**
* Engine that always returns the maximum value.
* Used internally for calculating max roll values.
*
* @since 4.2.0
*
* @type {{next(): number, range: number[]}}
*/
const maxEngine = {
/**
* The min / max number range (e.g. `[1, 10]`).
*
* This _must_ be set for the `next()` method to return the correct last index.
*
* @example
* maxEngine.range = [1, 10];
*
* @type {number[]}
*/
range: [],
/**
* Returns the maximum number index for the range
*
* @returns {number}
*/
next() {
// calculate the index of the max number
return this.range[1] - this.range[0];
}
};
/**
* Engine that always returns the minimum value.
* Used internally for calculating min roll values.
*
* @since 4.2.0
*
* @type {{next(): number}}
*/
const minEngine = {
/**
* Returns the minimum number index, `0`
*
* @returns {number}
*/
next() {
return 0;
}
};
/**
* List of built-in number generator engines.
*
* @since 4.2.0
*
* @see This uses [random-js](https://github.com/ckknight/random-js).
* For details of the engines, check the [documentation](https://github.com/ckknight/random-js#engines).
*
* @type {{
* min: {next(): number},
* max: {next(): number, range: number[]},
* browserCrypto: Engine,
* nodeCrypto: Engine,
* MersenneTwister19937: MersenneTwister19937,
* nativeMath: Engine
* }}
*/
const engines = {
browserCrypto: randomJs.browserCrypto,
nodeCrypto: randomJs.nodeCrypto,
MersenneTwister19937: randomJs.MersenneTwister19937,
nativeMath: randomJs.nativeMath,
min: minEngine,
max: maxEngine
};
/**
* The `NumberGenerator` is capable of generating random numbers.
*
* @since 4.2.0
*
* @see This uses [random-js](https://github.com/ckknight/random-js).
* For details of the engines, check the [documentation](https://github.com/ckknight/random-js#engines).
*/
class NumberGenerator {
/**
* Create a `NumberGenerator` instance.
*
* The `engine` can be any object that has a `next()` method, which returns a number.
*
* @example <caption>Built-in engine</caption>
* new NumberGenerator(engines.nodeCrypto);
*
* @example <caption>Custom engine</caption>
* new NumberGenerator({
* next() {
* // return a random number
* },
* });
*
* @param {Engine|{next(): number}} [engine=nativeMath] The RNG engine to use
*
* @throws {TypeError} engine must have function `next()`
*/
constructor() {
let engine = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : randomJs.nativeMath;
this.engine = engine || randomJs.nativeMath;
}
/**
* The current engine.
*
* @returns {Engine|{next(): number}}
*/
get engine() {
return this[engineSymbol];
}
/**
* Set the engine.
*
* The `engine` can be any object that has a `next()` method, which returns a number.
*
* @example <caption>Built-in engine</caption>
* numberGenerator.engine = engines.nodeCrypto;
*
* @example <caption>Custom engine</caption>
* numberGenerator.engine = {
* next() {
* // return a random number
* },
* });
*
* @see {@link engines}
*
* @param {Engine|{next(): number}} engine
*
* @throws {TypeError} engine must have function `next()`
*/
set engine(engine) {
if (engine && typeof engine.next !== 'function') {
throw new TypeError('engine must have function `next()`');
}
// set the engine and re-initialise the random engine
this[engineSymbol] = engine || randomJs.nativeMath;
this[randomSymbol] = new randomJs.Random(this[engineSymbol]);
}
/**
* Generate a random integer within the inclusive range `[min, max]`.
*
* @param {number} min The minimum integer value, inclusive.
* @param {number} max The maximum integer value, inclusive.
*
* @returns {number} The random integer
*/
integer(min, max) {
this[engineSymbol].range = [min, max];
return this[randomSymbol].integer(min, max);
}
/**
* Returns a floating-point value within `[min, max)` or `[min, max]`.
*
* @param {number} min The minimum floating-point value, inclusive.
* @param {number} max The maximum floating-point value.
* @param {boolean} [inclusive=false] If `true`, `max` will be inclusive.
*
* @returns {number} The random floating-point value
*/
real(min, max) {
let inclusive = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
this[engineSymbol].range = [min, max];
return this[randomSymbol].real(min, max, inclusive);
}
}
const generator = new NumberGenerator();
var NumberGenerator$1 = /*#__PURE__*/Object.freeze({
__proto__: null,
engines: engines,
generator: generator
});
const textSymbol = Symbol('text');
const typeSymbol = Symbol('type');
/**
* Represents a Roll / Roll group description.
*/
class Description {
static types = {
MULTILINE: 'multiline',
INLINE: 'inline'
};
/**
* Create a `Description` instance.
*
* @param {string} text
* @param {string} [type=inline]
*/
constructor(text) {
let type = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : this.constructor.types.INLINE;
this.text = text;
this.type = type;
}
/**
* The description text.
*
* @return {string}
*/
get text() {
return this[textSymbol];
}
/**
* Set the description text.
*
* @param {string|number} text
*/
set text(text) {
if (typeof text === 'object') {
throw new TypeError('Description text is invalid');
} else if (!text && text !== 0 || `${text}`.trim() === '') {
throw new TypeError('Description text cannot be empty');
}
this[textSymbol] = `${text}`.trim();
}
/**
* The description type.
*
* @return {string} "inline" or "multiline"
*/
get type() {
return this[typeSymbol];
}
/**
* Set the description type.
*
* @param {string} type
*/
set type(type) {
const types = Object.values(this.constructor.types);
if (typeof type !== 'string') {
throw new TypeError('Description type must be a string');
} else if (!types.includes(type)) {
throw new RangeError(`Description type must be one of; ${types.join(', ')}`);
}
this[typeSymbol] = type;
}
/**
* Return an object for JSON serialising.
*
* This is called automatically when JSON encoding the object.
*
* @return {{text: string, type: string}}
*/
toJSON() {
const {
text,
type
} = this;
return {
text,
type
};
}
/**
* Return the String representation of the object.
*
* This is called automatically when casting the object to a string.
*
* @see {@link Description#text}
*
* @returns {string}
*/
toString() {
if (this.type === this.constructor.types.INLINE) {
return `# ${this.text}`;
}
return `[${this.text}]`;
}
}
const descriptionSymbol = Symbol('description');
/**
* A base class for description functionality.
*
* @abstract
*/
class HasDescription {
constructor() {
let text = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null;
this.description = text;
}
/**
* The description for the group.
*
* @return {Description|null}
*/
get description() {
return this[descriptionSymbol] || null;
}
/**
* Set the description on the group.
*
* @param {Description|string|null} description
*/
set description(description) {
if (!description && description !== 0) {
this[descriptionSymbol] = null;
} else if (description instanceof Description) {
this[descriptionSymbol] = description;
} else if (typeof description === 'string') {
this[descriptionSymbol] = new Description(description);
} else {
throw new TypeError(`description must be of type Description, string or null. Received ${typeof description}`);
}
}
/**
* Return an object for JSON serialising.
*
* This is called automatically when JSON encoding the object.
*
* @returns {{description: (Description|null)}}
*/
toJSON() {
const {
description
} = this;
return {
description
};
}
/**
* Return the String representation of the object.
*
* This is called automatically when casting the object to a string.
*
* @see {@link RollGroup#notation}
*
* @returns {string}
*/
toString() {
if (this.description) {
return `${this.description}`;
}
return '';
}
}
/**
* A `Modifier` is the base modifier class that all others extend from.
*
* @abstract
*/
class Modifier {
/**
* The default modifier execution order.
*
* @type {number}
*/
static order = 999;
/**
* Create a `Modifier` instance.
*/
constructor() {
// set the modifier's sort order
this.order = this.constructor.order;
}
/* eslint-disable class-methods-use-this */
/**
* The name of the modifier.
*
* @returns {string} 'modifier'
*/
get name() {
return 'modifier';
}
/* eslint-enable class-methods-use-this */
/* eslint-disable class-methods-use-this */
/**
* The modifier's notation.
*
* @returns {string}
*/
get notation() {
return '';
}
/* eslint-enable class-methods-use-this */
/* eslint-disable class-methods-use-this */
/**
* The maximum number of iterations that the modifier can apply to a single die roll
*
* @returns {number} `1000`
*/
get maxIterations() {
return 1000;
}
/**
* No default values present
*
* @param {StandardDice|RollGroup} _context The object that the modifier is attached to
*
* @returns {object}
*/
defaults(_context) {
return {};
}
/* eslint-enable class-methods-use-this */
/**
* Processing default values definitions
*
* @param {StandardDice|RollGroup} _context The object that the modifier is attached to
*
* @returns {void}
*/
useDefaultsIfNeeded(_context) {
Object.entries(this.defaults(_context)).forEach(_ref => {
let [field, value] = _ref;
if (typeof this[field] === 'undefined') {
this[field] = value;
}
});
}
/* eslint-disable class-methods-use-this */
/**
* Run the modifier on the results.
*
* @param {RollResults} results The results to run the modifier against
* @param {StandardDice|RollGroup} _context The object that the modifier is attached to
*
* @returns {RollResults} The modified results
*/
run(results, _context) {
this.useDefaultsIfNeeded(_context);
return results;
}
/* eslint-enable class-methods-use-this */
/**
* Return an object for JSON serialising.
*
* This is called automatically when JSON encoding the object.
*
* @returns {{notation: string, name: string, type: string}}
*/
toJSON() {
const {
notation,
name
} = this;
return {
name,
notation,
type: 'modifier'
};
}
/**
* Return the String representation of the object.
*
* This is called automatically when casting the object to a string.
*
* @see {@link Modifier#notation}
*
* @returns {string}
*/
toString() {
return this.notation;
}
}
const flags = {
compound: '!',
explode: '!',
'critical-failure': '__',
'critical-success': '**',
drop: 'd',
max: 'v',
min: '^',
penetrate: 'p',
're-roll': 'r',
're-roll-once': 'ro',
'target-failure': '_',
'target-success': '*',
unique: 'u',
'unique-once': 'uo'
};
/**
* Return the flags for the given list of modifiers
*
* @param {...Modifier|string} modifiers
*
* @returns {string}
*/
const getModifierFlags = function () {
for (var _len = arguments.length, modifiers = new Array(_len), _key = 0; _key < _len; _key++) {
modifiers[_key] = arguments[_key];
}
return (
// @todo need a better way of mapping modifiers to symbols
[...modifiers].reduce((acc, modifier) => {
let name;
if (modifier instanceof Modifier) {
name = modifier.name;
} else {
name = modifier;
}
return acc + (flags[name] || name);
}, '')
);
};
const calculationValueSymbol$1 = Symbol('calculation-value');
const modifiersSymbol$3 = Symbol('modifiers');
const initialValueSymbol = Symbol('initial-value');
const useInTotalSymbol$1 = Symbol('use-in-total');
const valueSymbol$1 = Symbol('value');
/**
* A `RollResult` represents the value and applicable modifiers for a single die roll
*
* ::: tip
* You will probably not need to create your own `RollResult` instances, unless you're importing
* rolls, but `RollResult` objects will be returned when rolling dice.
* :::
*/
class RollResult {
/**
* Create a `RollResult` instance.
*
* `value` can be a number, or an object containing a list of different values.
* This allows you to specify the `initialValue`, `value` and `calculationValue` with different
* values.
*
* @example <caption>Numerical value</caption>
* const result = new RollResult(4);
*
* @example <caption>Object value</caption>
* // must provide either `value` or `initialValue`
* // `calculationValue` is optional.
* const result = new RollResult({
* value: 6,
* initialValue: 4,
* calculationValue: 8,
* });
*
* @example <caption>With modifiers</caption>
* const result = new RollResult(4, ['explode', 'critical-success']);
*
* @param {number|{value: number, initialValue: number, calculationValue: number}} value The value
* rolled
* @param {number} [value.value] The value with modifiers applied
* @param {number} [value.initialValue] The initial, unmodified value rolled
* @param {number} [value.calculationValue] The value used in calculations
* @param {string[]|Set<string>} [modifiers=[]] List of modifier names that affect this roll
* @param {boolean} [useInTotal=true] Whether to include the roll value when calculating totals
*
* @throws {TypeError} Result value, calculation value, or modifiers are invalid
*/
constructor(value) {
let modifiers = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : [];
let useInTotal = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : true;
if (isNumeric(value)) {
this[initialValueSymbol] = Number(value);
this.modifiers = modifiers || [];
this.useInTotal = useInTotal;
} else if (value && typeof value === 'object' && !Array.isArray(value)) {
// ensure that we have a valid value
const initialVal = isNumeric(value.initialValue) ? value.initialValue : value.value;
if (!isNumeric(initialVal)) {
throw new TypeError(`Result value is invalid: ${initialVal}`);
}
this[initialValueSymbol] = Number(initialVal);
if (isNumeric(value.value) && Number(value.value) !== this[initialValueSymbol]) {
this.value = value.value;
}
if (isNumeric(value.calculationValue) && parseFloat(`${value.calculationValue}`) !== this.value) {
this.calculationValue = value.calculationValue;
}
this.modifiers = value.modifiers || modifiers || [];
this.useInTotal = typeof value.useInTotal === 'boolean' ? value.useInTotal : useInTotal || false;
} else if (value === Infinity) {
throw new RangeError('Result value must be a finite number');
} else {
throw new TypeError(`Result value is invalid: ${value}`);
}
}
/**
* The value to use in calculations.
* This may be changed by modifiers.
*
* @returns {number}
*/
get calculationValue() {
return isNumeric(this[calculationValueSymbol$1]) ? parseFloat(this[calculationValueSymbol$1]) : this.value;
}
/**
* Set the value to use in calculations.
*
* @param {number} value
*
* @throws {TypeError} value is invalid
*/
set calculationValue(value) {
const isValNumeric = isNumeric(value);
if (value === Infinity) {
throw new RangeError('Result calculation value must be a finite number');
}
if (value && !isValNumeric) {
throw new TypeError(`Result calculation value is invalid: ${value}`);
}
this[calculationValueSymbol$1] = isValNumeric ? parseFloat(`${value}`) : null;
}
/**
* The initial roll value before any modifiers.
*
* Not used for calculations and is just for reference.
* You probably want `value` instead.
*
* @see {@link RollResult#value}
*
* @returns {number}
*/
get initialValue() {
return this[initialValueSymbol];
}
/**
* The visual flags for the modifiers that affect the roll.
*
* @see {@link RollResult#modifiers}
*
* @returns {string}
*/
get modifierFlags() {
return getModifierFlags(...this.modifiers);
}
/**
* The names of modifiers that affect the roll.
*
* @returns {Set<string>}
*/
get modifiers() {
return this[modifiersSymbol$3];
}
/**
* Set the modifier names that affect the roll.
*
* @example
* rollResult.modifiers = ['explode', 're-roll'];
*
* @param {string[]|Set<string>} value
*
* @throws {TypeError} modifiers must be a Set or array of modifier names
*/
set modifiers(value) {
if ((Array.isArray(value) || value instanceof Set) && [...value].every(item => typeof item === 'string')) {
this[modifiersSymbol$3] = new Set([...value]);
return;
}
if (!value && value !== 0) {
// clear the modifiers
this[modifiersSymbol$3] = new Set();
return;
}
throw new TypeError(`modifiers must be a Set or array of modifier names: ${value}`);
}
/**
* Whether to use the value in total calculations or not.
*
* @returns {boolean}
*/
get useInTotal() {
return !!this[useInTotalSymbol$1];
}
/**
* Set whether to use the value in total calculations or not.
*
* @param {boolean} value
*/
set useInTotal(value) {
this[useInTotalSymbol$1] = !!value;
}
/**
* Value of the roll after modifiers have been applied.
*
* @returns {number}
*/
get value() {
return isNumeric(this[valueSymbol$1]) ? this[valueSymbol$1] : this[initialValueSymbol];
}
/**
* Set the roll value.
*
* @param {number} value
*
* @throws {RangeError} value must be finite
* @throws {TypeError} value is invalid
*/
set value(value) {
if (value === Infinity) {
throw new RangeError('Result value must be a finite number');
}
if (!isNumeric(value)) {
throw new TypeError(`Result value is invalid: ${value}`);
}
this[valueSymbol$1] = Number(value);
}
/**
* Return an object for JSON serialising.
*
* This is called automatically when JSON encoding the object.
*
* @returns {{
* calculationValue: number,
* modifierFlags: string,
* modifiers: string[],
* type: string,
* initialValue: number,
* useInTotal: boolean,
* value: number
* }}
*/
toJSON() {
const {
calculationValue,
initialValue,
modifierFlags,
modifiers,
useInTotal,
value
} = this;
return {
calculationValue,
initialValue,
modifierFlags,
modifiers: [...modifiers],
type: 'result',
useInTotal,
value
};
}
/**
* Return the String representation of the object.
*
* This is called automatically when casting the object to a string.
*
* @returns {string}
*/
toString() {
return this.value + this.modifierFlags;
}
}
const rollsSymbol$1 = Symbol('rolls');
/**
* A collection of die roll results
*
* ::: tip
* You will probably not need to create your own `RollResults` instances, unless you're importing
* rolls, but RollResults objects will be returned when rolling dice.
* :::
*/
class RollResults {
/**
* Create a `RollResults` instance.
*
* @example <caption>`RollResult` objects</caption>
* const results = new RollResults([
* new RollResult(4),
* new RollResult(3),
* new RollResult(5),
* ]);
*
* @example <caption>Numerical results</caption>
* const results = new RollResults([4, 3, 5]);
*
* @example <caption>A mix</caption>
* const results = new RollResults([
* new RollResult(4),
* 3,
* new RollResult(5),
* ]);
*
* @param {Array.<RollResult|number>} [rolls=[]] The roll results
*
* @throws {TypeError} Rolls must be an array
*/
constructor() {
let rolls = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
this.rolls = rolls;
}
/**
* The number of roll results.
*
* @returns {number}
*/
get length() {
return this.rolls.length || 0;
}
/**
* List of roll results.
*
* @returns {RollResult[]}
*/
get rolls() {
return [...this[rollsSymbol$1]];
}
/**
* Set the rolls.
*
* @param {RollResult[]|number[]} rolls
*
* @throws {TypeError} Rolls must be an array
*/
set rolls(rolls) {
if (!rolls || !Array.isArray(rolls)) {
// roll is not an array
throw new TypeError(`rolls must be an array: ${rolls}`);
}
// loop through each result and add it to the rolls list
this[rollsSymbol$1] = [];
rolls.forEach(result => {
this.addRoll(result);
});
}
/**
* The total value of all the rolls after modifiers have been applied.
*
* @returns {number}
*/
get value() {
return this.rolls.reduce((v, roll) => v + (roll.useInTotal ? roll.calculationValue : 0), 0);
}
/**
* Add a single roll to the list.
*
* @param {RollResult|number} value
*/
addRoll(value) {
const result = value instanceof RollResult ? value : new RollResult(value);
this[rollsSymbol$1].push(result);
}
/**
* Return an object for JSON serialising.
*
* This is called automatically when JSON encoding the object.
*
* @returns {{rolls: RollResult[], value: number}}
*/
toJSON() {
const {
rolls,
value
} = this;
return {
rolls,
type: 'roll-results',
value
};
}
/**
* Return the String representation of the object.
*
* This is called automatically when casting the object to a string.
*
* @returns {string}
*/
toString() {
return `[${this.rolls.join(', ')}]`;
}
}
const modifiersSymbol$2 = Symbol('modifiers');
const qtySymbol$1 = Symbol('qty');
const sidesSymbol = Symbol('sides');
const minSymbol$1 = Symbol('min-value');
const maxSymbol$1 = Symbol('max-value');
/**
* Represents a standard numerical die.
*/
class StandardDice extends HasDescription {
/**
* Create a `StandardDice` instance.
*
* @param {number} sides The number of sides the die has (.e.g `6`)
* @param {number} [qty=1] The number of dice to roll (e.g. `4`)
* @param {Map<string, Modifier>|Modifier[]|{}|null} [modifiers] The modifiers that affect the die
* @param {number|null} [min=1] The minimum possible roll value
* @param {number|null} [max=null] The maximum possible roll value. Defaults to number of `sides`
* @param {Description|string|null} [description=null] The roll description.
*
* @throws {RequiredArgumentError} sides is required
* @throws {TypeError} qty must be a positive integer, and modifiers must be valid
*/
constructor(sides) {
let qty = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 1;
let modifiers = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null;
let min = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 1;
let max = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : null;
let description = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : null;
super(description);
if (!sides && sides !== 0) {
throw new RequiredArgumentError('sides');
} else if (sides === Infinity) {
throw new RangeError('numerical sides must be finite number');
} else if (isNumeric(sides)) {
if (sides < 1 || !isSafeNumber(sides)) {
throw new RangeError('numerical sides must be a positive finite number');
}
} else if (typeof sides !== 'string') {
throw new TypeError('non-numerical sides must be a string');
}
if (!isNumeric(qty)) {
throw new TypeError('qty must be a positive finite integer');
} else if (qty < 1 || qty > 999) {
throw new RangeError('qty must be between 1 and 999');
}
let minVal = min;
if (minVal === null || minVal === undefined) {
minVal = 1;
} else if (!isNumeric(minVal)) {
throw new TypeError('min must a finite number');
} else if (!isSafeNumber(minVal)) {
throw new RangeError('min must a finite number');
}
if (max && !isNumeric(max)) {
throw new TypeError('max must a finite number');
} else if (max && !isSafeNumber(max)) {
throw new RangeError('max must a finite number');
}
this[qtySymbol$1] = parseInt(`${qty}`, 10);
this[sidesSymbol] = sides;
if (modifiers) {
this.modifiers = modifiers;
}
this[minSymbol$1] = parseInt(minVal, 10);
this[maxSymbol$1] = max ? parseInt(`${max}`, 10) : sides;
}
/**
* The average value that the die can roll (Excluding modifiers).
*
* @returns {number}
*/
get average() {
return (this.min + this.max) / 2;
}
/**
* The modifiers that affect this die roll.
*
* @returns {Map<string, Modifier>|null}
*/
get modifiers() {
if (this[modifiersSymbol$2]) {
// ensure modifiers are ordered correctly
return new Map([...this[modifiersSymbol$2]].sort((a, b) => a[1].order - b[1].order));
}
return null;
}
/**
* Set the modifiers that affect this roll.
*
* @param {Map<string, Modifier>|Modifier[]|{}|null} value
*
* @throws {TypeError} Modifiers should be a Map, array of Modifiers, or an Object
*/
set modifiers(value) {
let modifiers;
if (value instanceof Map) {
modifiers = value;
} else if (Array.isArray(value)) {
// loop through and get the modifier name of each item and use it as the map key
modifiers = new Map(value.map(modifier => [modifier.name, modifier]));
} else if (typeof value === 'object') {
modifiers = new Map(Object.entries(value));
} else {
throw new TypeError('modifiers should be a Map, array, or an Object containing Modifiers');
}
if (modifiers.size && [...modifiers.entries()].some(entry => !(entry[1] instanceof Modifier))) {
throw new TypeError('modifiers must only contain Modifier instances');
}
this[modifiersSymbol$2] = modifiers;
}
/**
* The maximum value that can be rolled on the die, excluding modifiers.
*
* @returns {number}
*/
get max() {
return this[maxSymbol$1];
}
/**
* The minimum value that can be rolled on the die, excluding modifiers.
*
* @returns {number}
*/
get min() {
return this[minSymbol$1];
}
/* eslint-disable class-methods-use-this */
/**
* The name of the die.
*
* @returns {string} 'standard'
*/
get name() {
return 'standard';
}
/* eslint-enable class-methods-use-this */
/**
* The dice notation. e.g. `4d6!`.
*
* @returns {string}
*/
get notation() {
let notation = `${this.qty}d${this.sides}`;
if (this.modifiers && this.modifiers.size) {
notation += [...this.modifiers.values()].reduce((acc, modifier) => acc + modifier.notation, '');
}
return notation;
}
/**
* The number of dice that should be rolled.
*
* @returns {number}
*/
get qty() {
return this[qtySymbol$1];
}
/**
* The number of sides the die has.
*
* @returns {number}
*/
get sides() {
return this[sidesSymbol];
}
/**
* Roll the dice for the specified quantity and apply any modifiers.
*
* @returns {RollResults} The result of the roll
*/
roll() {
// create a result object to hold the rolls
const rollResult = new RollResults();
// loop for the quantity and roll the die
for (let i = 0; i < this.qty; i++) {
// add the rolls to the list
rollResult.addRoll(this.rollOnce());
}
// loop through each modifier and carry out its actions
(this.modifiers || []).forEach(modifier => {
modifier.run(rollResult, this);
});
return rollResult;
}
/**
* Roll a single die and return the value.
*
* @returns {RollResult} The value rolled
*/
rollOnce() {
return new RollResult(generator.integer(this.min, this.max));
}
/**
* Return an object for JSON serialising.
*
* This is called automatically when JSON encoding the object.
*
* @returns {{
* average: number,
* min: number,
* max: number,
* notation: string,
* qty: number,
* name: string,
* sides: number,
* modifiers: (Map<string, Modifier>|null),
* type: string
* }}
*/
toJSON() {
const {
average,
max,
min,
modifiers,
name,
notation,
qty,
sides
} = this;
return Object.assign(super.toJSON(), {
average,
max,
min,
modifiers,
name,
notation,
qty,
sides,
type: 'die'
});
}
/**
* Return the String representation of the object.
*
* This is called automatically when casting the object to a string.
*
* @see {@link StandardDice#notation}
*
* @returns {string}
*/
toString() {
return `${this.notation}${this.description ? ` ${this.description}` : ''}`;
}
}
/**
* Represents a Fudge / Fate type die.
*
* @extends StandardDice
*/
class FudgeDice extends StandardDice {
/**
* Create a `FudgeDice` instance.
*
* @param {number} [nonBlanks=2] The number of sides each symbol should cover (`1` or `2`)
* @param {number} [qty=1] The number of dice to roll (e.g. `4`)
* @param {Map<string, Modifier>|Modifier[]|{}|null} [modifiers] The modifiers that affect the die
* @param {Description|string|null} [description=null] The roll description.
*
* @throws {RangeError} nonBlanks must be 1 or 2
* @throws {TypeError} modifiers must be valid
*/
constructor() {
let nonBlanks = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 2;
let qty = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 1;
let modifiers = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null;
let description = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : null;
let numNonBlanks = nonBlanks;
if (!numNonBlanks && numNonBlanks !== 0) {
numNonBlanks = 2;
} else if (numNonBlanks !== 1 && numNonBlanks !== 2) {
throw new RangeError('nonBlanks must be 1 or 2');
}
super(numNonBlanks, qty, modifiers, -1, 1, description);
}
/* eslint-disable class-methods-use-this */
/**
* The name of the die.
*
* @returns {string} 'fudge'
*/
get name() {
return 'fudge';
}
/* eslint-enable class-methods-use-this */
/**
* The number of sides that each symbol (+, -) covers.
*
* @returns {number} `1` or `2`
*/
get nonBlanks() {
return super.sides;
}
/**
* The number of sides the die has.
*
* @returns {string} 'F.2' or 'F.1'
*/
get sides() {
return `F.${this.nonBlanks}`;
}
/**
* Roll a single die and return the value.
*
* @returns {RollResult} The value rolled
*/
rollOnce() {
let total = 0;
if (this.nonBlanks === 2) {
// default fudge (2 of each non-blank) = 1d3 - 2
total = generator.integer(1, 3) - 2;
} else if (this.nonBlanks === 1) {
// only 1 of each non-blank
// on 1d6 a roll of 1 = -1, 6 = +1, others = 0
const num = generator.integer(1, 6);
if (num === 1) {
total = -1;
} else if (num === 6) {
total = 1;
}
}
return new RollResult(total);
}
}
/**
* Represents a percentile die.
*
* @extends StandardDice
*/
class PercentileDice extends StandardDice {
/**
* Create a `PercentileDice` instance.
*
* @param {number} [qty=1] The number of dice to roll (e.g. `4`)
* @param {Map<string, Modifier>|Modifier[]|{}|null} [modifiers] The modifiers that affect the die
* @param {boolean} [sidesAsNumber=false] Whether to show the sides as `%` (default) or `100`
* @param {Description|string|null} [description=null] The roll description.
*
* @throws {TypeError} qty must be a positive integer, and modifiers must be valid
*/
constructor() {
let qty = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 1;
let modifiers = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
let sidesAsNumber = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
let description = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : null;
super(100, qty, modifiers, null, null, description);
this.sidesAsNumber = !!sidesAsNumber;
}
/* eslint-disable class-methods-use-this */
/**
* The name of the die.
*
* @returns {string} 'percentile'
*/
get name() {
return 'percentile';
}
/* eslint-enable class-methods-use-this */
/**
* The number of sides the die has
*
* @returns {number|string} `%` if `sidesAsNumber == false`, or `100` otherwise
*/
get sides() {
return this.sidesAsNumber ? super.sides : '%';
}
}
var index$2 = /*#__PURE__*/Object.freeze({
__proto__: null,
FudgeDice: FudgeDice,
PercentileDice: PercentileDice,
StandardDice: StandardDice
});
/**
* The operator
*
* @type {symbol}
*
* @private
*/
const operatorSymbol = Symbol('operator');
/**
* The value
*
* @type {symbol}
*
* @private
*/
const valueSymbol = Symbol('value');
/**
* A `ComparePoint` object compares numbers against each other.
* For example, _is 6 greater than 3_, or _is 8 equal to 10_.
*/
class ComparePoint {
/**
* Create a `ComparePoint` instance.
*
* @param {string} operator The comparison operator (One of `=`, `!=`, `<>`, `<`, `>`, `<=`, `>=`)
* @param {number} value The value to compare to
*
* @throws {CompareOperatorError} operator is invalid
* @throws {RequiredArgumentError} operator and value are required
* @throws {TypeError} value must be numeric
*/
constructor(operator, value) {
if (!operator) {
throw new RequiredArgumentError('operator');
} else if (!value && value !== 0) {
throw new RequiredArgumentError('value');
}
this.operator = operator;
this.value = value;
}
/**
* Check if the operator is valid.
*
* @param {string} operator
*
* @returns {boolean} `true` if the operator is valid, `false` otherwise
*/
static isValidOperator(operator) {
return typeof operator === 'string' && /^(?:[<>!]?=|[<>]|<>)$/.test(operator);
}
/**
* Set the comparison operator.
*
* @param {string} operator One of `=`, `!=`, `<>`, `<`, `>`, `<=`, `>=`
*
* @throws CompareOperatorError operator is invalid
*/
set operator(operator) {
if (!this.constructor.isValidOperator(operator)) {
throw new CompareOperatorError(operator);
}
this[operatorSymbol] = operator;
}
/**
* The comparison operator.
*
* @returns {string}
*/
get operator() {
return this[operatorSymbol];
}
/**
* Set the value.
*
* @param {number} value
*
* @throws {TypeError} value must be numeric
*/
set value(value) {
if (!isNumeric(value)) {
throw new TypeError('value must be a finite number');
}
this[valueSymbol] = Number(value);
}
/**
* The comparison value
*
* @returns {number}
*/
get value() {
return this[valueSymbol];
}
/**
* Check whether value matches the compare point
*
* @param {number} value The number to compare
*
* @returns {boolean} `true` if it is a match, `false` otherwise
*/
isMatch(value) {
return compareNumbers(value, this.value, this.operator);
}
/**
* Return an object for JSON serialising.
*
* This is called automatically when JSON encoding the object.
*
* @returns {{type: string, value: number, operator: string}}
*/
toJSON() {
const {
operator,
value
} = this;
return {
operator,
type: 'compare-point',
value
};
}
/**
* Return the String representation of the object.
*
* This is called automatically when casting the object to a string.
*
* @returns {string}
*/
toString() {
return `${this.operator}${this.value}`;
}
}
const comparePointSymbol = Symbol('compare-point');
/**
* A `ComparisonModifier` is the base modifier class for comparing values.
*
* @abstract
*
* @extends Modifier
*
* @see {@link CriticalFailureModifier}
* @see {@link CriticalSuccessModifier}
* @see {@link ExplodeModifier}
* @see {@link ReRollModifier}
* @see {@link TargetModifier}
*/
class ComparisonModifier extends Modifier {
/**
* Create a `ComparisonModifier` instance.
*
* @param {ComparePoint} [comparePoint] The comparison object
*
* @throws {TypeError} `comparePoint` must be an instance of `ComparePoint` or `undefined`
*/
constructor(comparePoint) {
super();
if (comparePoint) {
this.comparePoint = comparePoint;
}
}
/**
* The compare point.
*
* @returns {ComparePoint|undefined}
*/
get comparePoint() {
return this[comparePointSymbol];
}
/**
* Set the compare point.
*
* @param {ComparePoint} comparePoint
*
* @throws {TypeError} value must be an instance of `ComparePoint`
*/
set comparePoint(comparePoint) {
if (!(comparePoint instanceof ComparePoint)) {
throw new TypeError('comparePoint must be instance of ComparePoint');
}
this[comparePointSymbol] = comparePoint;
}
/* eslint-disable class-methods-use-this */
/**
* The name of the modifier.
*
* @returns {string} 'comparison'
*/
get name() {
return 'comparison';
}
/* eslint-enable class-methods-use-this */
/**
* The modifier's notation.
*
* @returns {string}
*/
get notation() {
return `${this.comparePoint || ''}`;
}
/* eslint-disable class-methods-use-this */
/**
* Empty default compare point definition
*
* @param {StandardDice|RollGroup} _context The object that the modifier is attached to
*
* @returns {null}
*/
defaultComparePoint(_context) {
return {};
}
/* eslint-enable class-methods-use-this */
/**
* Eases processing of simple "compare point only" defaults
*
* @param {StandardDice|RollGroup} _context The object that the modifier is attached to
*
* @returns {object}
*/
defaults(_context) {
const comparePointConfig = this.defaultComparePoint(_context);
if (typeof comparePointConfig === 'object' && comparePointConfig.length === 2) {
return {
compareP