UNPKG

@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
/** * @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