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,849 lines (1,720 loc) 206 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 */ import { evaluate as evaluate$1 } from 'mathjs'; /** * 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, action = 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(argumentName = 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$4 = /*#__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) => evaluate$1(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 = (num, precision = 0) => ( // round to precision, then cast to a number to remove trailing zeroes after the decimal point parseFloat(parseFloat(`${num}`).toFixed(precision || 0)) ); const SMALLEST_UNSAFE_INTEGER = 0x20000000000000; const LARGEST_SAFE_INTEGER = SMALLEST_UNSAFE_INTEGER - 1; const UINT32_MAX = -1 >>> 0; const UINT32_SIZE = UINT32_MAX + 1; const INT32_SIZE = UINT32_SIZE / 2; const INT32_MAX = INT32_SIZE - 1; const UINT21_SIZE = 1 << 21; const UINT21_MAX = UINT21_SIZE - 1; /** * Returns a value within [-0x80000000, 0x7fffffff] */ function int32(engine) { return engine.next() | 0; } function add(distribution, addend) { if (addend === 0) { return distribution; } else { return engine => distribution(engine) + addend; } } /** * Returns a value within [-0x20000000000000, 0x1fffffffffffff] */ function int53(engine) { const high = engine.next() | 0; const low = engine.next() >>> 0; return ((high & UINT21_MAX) * UINT32_SIZE + low + (high & UINT21_SIZE ? -SMALLEST_UNSAFE_INTEGER : 0)); } /** * Returns a value within [-0x20000000000000, 0x20000000000000] */ function int53Full(engine) { while (true) { const high = engine.next() | 0; if (high & 0x400000) { if ((high & 0x7fffff) === 0x400000 && (engine.next() | 0) === 0) { return SMALLEST_UNSAFE_INTEGER; } } else { const low = engine.next() >>> 0; return ((high & UINT21_MAX) * UINT32_SIZE + low + (high & UINT21_SIZE ? -SMALLEST_UNSAFE_INTEGER : 0)); } } } /** * Returns a value within [0, 0xffffffff] */ function uint32(engine) { return engine.next() >>> 0; } /** * Returns a value within [0, 0x1fffffffffffff] */ function uint53(engine) { const high = engine.next() & UINT21_MAX; const low = engine.next() >>> 0; return high * UINT32_SIZE + low; } /** * Returns a value within [0, 0x20000000000000] */ function uint53Full(engine) { while (true) { const high = engine.next() | 0; if (high & UINT21_SIZE) { if ((high & UINT21_MAX) === 0 && (engine.next() | 0) === 0) { return SMALLEST_UNSAFE_INTEGER; } } else { const low = engine.next() >>> 0; return (high & UINT21_MAX) * UINT32_SIZE + low; } } } function isPowerOfTwoMinusOne(value) { return ((value + 1) & value) === 0; } function bitmask(masking) { return (engine) => engine.next() & masking; } function downscaleToLoopCheckedRange(range) { const extendedRange = range + 1; const maximum = extendedRange * Math.floor(UINT32_SIZE / extendedRange); return engine => { let value = 0; do { value = engine.next() >>> 0; } while (value >= maximum); return value % extendedRange; }; } function downscaleToRange(range) { if (isPowerOfTwoMinusOne(range)) { return bitmask(range); } else { return downscaleToLoopCheckedRange(range); } } function isEvenlyDivisibleByMaxInt32(value) { return (value | 0) === 0; } function upscaleWithHighMasking(masking) { return engine => { const high = engine.next() & masking; const low = engine.next() >>> 0; return high * UINT32_SIZE + low; }; } function upscaleToLoopCheckedRange(extendedRange) { const maximum = extendedRange * Math.floor(SMALLEST_UNSAFE_INTEGER / extendedRange); return engine => { let ret = 0; do { const high = engine.next() & UINT21_MAX; const low = engine.next() >>> 0; ret = high * UINT32_SIZE + low; } while (ret >= maximum); return ret % extendedRange; }; } function upscaleWithinU53(range) { const extendedRange = range + 1; if (isEvenlyDivisibleByMaxInt32(extendedRange)) { const highRange = ((extendedRange / UINT32_SIZE) | 0) - 1; if (isPowerOfTwoMinusOne(highRange)) { return upscaleWithHighMasking(highRange); } } return upscaleToLoopCheckedRange(extendedRange); } function upscaleWithinI53AndLoopCheck(min, max) { return engine => { let ret = 0; do { const high = engine.next() | 0; const low = engine.next() >>> 0; ret = (high & UINT21_MAX) * UINT32_SIZE + low + (high & UINT21_SIZE ? -SMALLEST_UNSAFE_INTEGER : 0); } while (ret < min || ret > max); return ret; }; } /** * Returns a Distribution to return a value within [min, max] * @param min The minimum integer value, inclusive. No less than -0x20000000000000. * @param max The maximum integer value, inclusive. No greater than 0x20000000000000. */ function integer(min, max) { min = Math.floor(min); max = Math.floor(max); if (min < -SMALLEST_UNSAFE_INTEGER || !isFinite(min)) { throw new RangeError(`Expected min to be at least ${-SMALLEST_UNSAFE_INTEGER}`); } else if (max > SMALLEST_UNSAFE_INTEGER || !isFinite(max)) { throw new RangeError(`Expected max to be at most ${SMALLEST_UNSAFE_INTEGER}`); } const range = max - min; if (range <= 0 || !isFinite(range)) { return () => min; } else if (range === UINT32_MAX) { if (min === 0) { return uint32; } else { return add(int32, min + INT32_SIZE); } } else if (range < UINT32_MAX) { return add(downscaleToRange(range), min); } else if (range === LARGEST_SAFE_INTEGER) { return add(uint53, min); } else if (range < LARGEST_SAFE_INTEGER) { return add(upscaleWithinU53(range), min); } else if (max - 1 - min === LARGEST_SAFE_INTEGER) { return add(uint53Full, min); } else if (min === -SMALLEST_UNSAFE_INTEGER && max === SMALLEST_UNSAFE_INTEGER) { return int53Full; } else if (min === -SMALLEST_UNSAFE_INTEGER && max === LARGEST_SAFE_INTEGER) { return int53; } else if (min === -LARGEST_SAFE_INTEGER && max === SMALLEST_UNSAFE_INTEGER) { return add(int53, 1); } else if (max === SMALLEST_UNSAFE_INTEGER) { return add(upscaleWithinI53AndLoopCheck(min - 1, max - 1), 1); } else { return upscaleWithinI53AndLoopCheck(min, max); } } function isLeastBitTrue(engine) { return (engine.next() & 1) === 1; } function lessThan(distribution, value) { return engine => distribution(engine) < value; } function probability(percentage) { if (percentage <= 0) { return () => false; } else if (percentage >= 1) { return () => true; } else { const scaled = percentage * UINT32_SIZE; if (scaled % 1 === 0) { return lessThan(int32, (scaled - INT32_SIZE) | 0); } else { return lessThan(uint53, Math.round(percentage * SMALLEST_UNSAFE_INTEGER)); } } } function bool(numerator, denominator) { if (denominator == null) { if (numerator == null) { return isLeastBitTrue; } return probability(numerator); } else { if (numerator <= 0) { return () => false; } else if (numerator >= denominator) { return () => true; } return lessThan(integer(0, denominator - 1), numerator); } } /** * Returns a Distribution that returns a random `Date` within the inclusive * range of [`start`, `end`]. * @param start The minimum `Date` * @param end The maximum `Date` */ function date(start, end) { const distribution = integer(+start, +end); return engine => new Date(distribution(engine)); } /** * Returns a Distribution to return a value within [1, sideCount] * @param sideCount The number of sides of the die */ function die(sideCount) { return integer(1, sideCount); } /** * Returns a distribution that returns an array of length `dieCount` of values * within [1, `sideCount`] * @param sideCount The number of sides of each die * @param dieCount The number of dice */ function dice(sideCount, dieCount) { const distribution = die(sideCount); return engine => { const result = []; for (let i = 0; i < dieCount; ++i) { result.push(distribution(engine)); } return result; }; } // tslint:disable:unified-signatures // has 2**x chars, for faster uniform distribution const DEFAULT_STRING_POOL = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-"; function string(pool = DEFAULT_STRING_POOL) { const poolLength = pool.length; if (!poolLength) { throw new Error("Expected pool not to be an empty string"); } const distribution = integer(0, poolLength - 1); return (engine, length) => { let result = ""; for (let i = 0; i < length; ++i) { const j = distribution(engine); result += pool.charAt(j); } return result; }; } const LOWER_HEX_POOL = "0123456789abcdef"; const lowerHex = string(LOWER_HEX_POOL); const upperHex = string(LOWER_HEX_POOL.toUpperCase()); /** * Returns a Distribution that returns a random string comprised of numbers * or the characters `abcdef` (or `ABCDEF`) of length `length`. * @param length Length of the result string * @param uppercase Whether the string should use `ABCDEF` instead of `abcdef` */ function hex(uppercase) { if (uppercase) { return upperHex; } else { return lowerHex; } } function convertSliceArgument(value, length) { if (value < 0) { return Math.max(value + length, 0); } else { return Math.min(value, length); } } function toInteger(value) { const num = +value; if (num < 0) { return Math.ceil(num); } else { return Math.floor(num); } } /** * Returns a random value within the provided `source` within the sliced * bounds of `begin` and `end`. * @param source an array of items to pick from * @param begin the beginning slice index (defaults to `0`) * @param end the ending slice index (defaults to `source.length`) */ function pick(engine, source, begin, end) { const length = source.length; if (length === 0) { throw new RangeError("Cannot pick from an empty array"); } const start = begin == null ? 0 : convertSliceArgument(toInteger(begin), length); const finish = end === void 0 ? length : convertSliceArgument(toInteger(end), length); if (start >= finish) { throw new RangeError(`Cannot pick between bounds ${start} and ${finish}`); } const distribution = integer(start, finish - 1); return source[distribution(engine)]; } function multiply(distribution, multiplier) { if (multiplier === 1) { return distribution; } else if (multiplier === 0) { return () => 0; } else { return engine => distribution(engine) * multiplier; } } /** * Returns a floating-point value within [0.0, 1.0) */ function realZeroToOneExclusive(engine) { return uint53(engine) / SMALLEST_UNSAFE_INTEGER; } /** * Returns a floating-point value within [0.0, 1.0] */ function realZeroToOneInclusive(engine) { return uint53Full(engine) / SMALLEST_UNSAFE_INTEGER; } /** * Returns a floating-point value within [min, max) or [min, max] * @param min The minimum floating-point value, inclusive. * @param max The maximum floating-point value. * @param inclusive If true, `max` will be inclusive. */ function real(min, max, inclusive = false) { if (!isFinite(min)) { throw new RangeError("Expected min to be a finite number"); } else if (!isFinite(max)) { throw new RangeError("Expected max to be a finite number"); } return add(multiply(inclusive ? realZeroToOneInclusive : realZeroToOneExclusive, max - min), min); } const sliceArray = Array.prototype.slice; /** * Shuffles an array in-place * @param engine The Engine to use when choosing random values * @param array The array to shuffle * @param downTo minimum index to shuffle. Only used internally. */ function shuffle(engine, array, downTo = 0) { const length = array.length; if (length) { for (let i = (length - 1) >>> 0; i > downTo; --i) { const distribution = integer(0, i); const j = distribution(engine); if (i !== j) { const tmp = array[i]; array[i] = array[j]; array[j] = tmp; } } } return array; } /** * From the population array, produce an array with sampleSize elements that * are randomly chosen without repeats. * @param engine The Engine to use when choosing random values * @param population An array that has items to choose a sample from * @param sampleSize The size of the result array */ function sample(engine, population, sampleSize) { if (sampleSize < 0 || sampleSize > population.length || !isFinite(sampleSize)) { throw new RangeError("Expected sampleSize to be within 0 and the length of the population"); } if (sampleSize === 0) { return []; } const clone = sliceArray.call(population); const length = clone.length; if (length === sampleSize) { return shuffle(engine, clone, 0); } const tailLength = length - sampleSize; return shuffle(engine, clone, tailLength - 1).slice(tailLength); } const stringRepeat = (() => { try { if ("x".repeat(3) === "xxx") { return (pattern, count) => pattern.repeat(count); } } catch (_) { // nothing to do here } return (pattern, count) => { let result = ""; while (count > 0) { if (count & 1) { result += pattern; } count >>= 1; pattern += pattern; } return result; }; })(); function zeroPad(text, zeroCount) { return stringRepeat("0", zeroCount - text.length) + text; } /** * Returns a Universally Unique Identifier Version 4. * * See http://en.wikipedia.org/wiki/Universally_unique_identifier */ function uuid4(engine) { const a = engine.next() >>> 0; const b = engine.next() | 0; const c = engine.next() | 0; const d = engine.next() >>> 0; return (zeroPad(a.toString(16), 8) + "-" + zeroPad((b & 0xffff).toString(16), 4) + "-" + zeroPad((((b >> 4) & 0x0fff) | 0x4000).toString(16), 4) + "-" + zeroPad(((c & 0x3fff) | 0x8000).toString(16), 4) + "-" + zeroPad(((c >> 4) & 0xffff).toString(16), 4) + zeroPad(d.toString(16), 8)); } /** * An int32-producing Engine that uses `Math.random()` */ const nativeMath = { next() { return (Math.random() * UINT32_SIZE) | 0; } }; // tslint:disable:unified-signatures /** * A wrapper around an Engine that provides easy-to-use methods for * producing values based on known distributions */ class Random { /** * Creates a new Random wrapper * @param engine The engine to use (defaults to a `Math.random`-based implementation) */ constructor(engine = nativeMath) { this.engine = engine; } /** * Returns a value within [-0x80000000, 0x7fffffff] */ int32() { return int32(this.engine); } /** * Returns a value within [0, 0xffffffff] */ uint32() { return uint32(this.engine); } /** * Returns a value within [0, 0x1fffffffffffff] */ uint53() { return uint53(this.engine); } /** * Returns a value within [0, 0x20000000000000] */ uint53Full() { return uint53Full(this.engine); } /** * Returns a value within [-0x20000000000000, 0x1fffffffffffff] */ int53() { return int53(this.engine); } /** * Returns a value within [-0x20000000000000, 0x20000000000000] */ int53Full() { return int53Full(this.engine); } /** * Returns a value within [min, max] * @param min The minimum integer value, inclusive. No less than -0x20000000000000. * @param max The maximum integer value, inclusive. No greater than 0x20000000000000. */ integer(min, max) { return integer(min, max)(this.engine); } /** * Returns a floating-point value within [0.0, 1.0] */ realZeroToOneInclusive() { return realZeroToOneInclusive(this.engine); } /** * Returns a floating-point value within [0.0, 1.0) */ realZeroToOneExclusive() { return realZeroToOneExclusive(this.engine); } /** * Returns a floating-point value within [min, max) or [min, max] * @param min The minimum floating-point value, inclusive. * @param max The maximum floating-point value. * @param inclusive If true, `max` will be inclusive. */ real(min, max, inclusive = false) { return real(min, max, inclusive)(this.engine); } bool(numerator, denominator) { return bool(numerator, denominator)(this.engine); } /** * Return a random value within the provided `source` within the sliced * bounds of `begin` and `end`. * @param source an array of items to pick from * @param begin the beginning slice index (defaults to `0`) * @param end the ending slice index (defaults to `source.length`) */ pick(source, begin, end) { return pick(this.engine, source, begin, end); } /** * Shuffles an array in-place * @param array The array to shuffle */ shuffle(array) { return shuffle(this.engine, array); } /** * From the population array, returns an array with sampleSize elements that * are randomly chosen without repeats. * @param population An array that has items to choose a sample from * @param sampleSize The size of the result array */ sample(population, sampleSize) { return sample(this.engine, population, sampleSize); } /** * Returns a value within [1, sideCount] * @param sideCount The number of sides of the die */ die(sideCount) { return die(sideCount)(this.engine); } /** * Returns an array of length `dieCount` of values within [1, sideCount] * @param sideCount The number of sides of each die * @param dieCount The number of dice */ dice(sideCount, dieCount) { return dice(sideCount, dieCount)(this.engine); } /** * Returns a Universally Unique Identifier Version 4. * * See http://en.wikipedia.org/wiki/Universally_unique_identifier */ uuid4() { return uuid4(this.engine); } string(length, pool) { return string(pool)(this.engine, length); } /** * Returns a random string comprised of numbers or the characters `abcdef` * (or `ABCDEF`) of length `length`. * @param length Length of the result string * @param uppercase Whether the string should use `ABCDEF` instead of `abcdef` */ hex(length, uppercase) { return hex(uppercase)(this.engine, length); } /** * Returns a random `Date` within the inclusive range of [`start`, `end`]. * @param start The minimum `Date` * @param end The maximum `Date` */ date(start, end) { return date(start, end)(this.engine); } } /** * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Int32Array */ const I32Array = (() => { try { const buffer = new ArrayBuffer(4); const view = new Int32Array(buffer); view[0] = INT32_SIZE; if (view[0] === -INT32_SIZE) { return Int32Array; } } catch (_) { // nothing to do here } return Array; })(); let data = null; const COUNT = 128; let index$3 = COUNT; /** * An Engine that relies on the globally-available `crypto.getRandomValues`, * which is typically available in modern browsers. * * See https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues * * If unavailable or otherwise non-functioning, then `browserCrypto` will * likely `throw` on the first call to `next()`. */ const browserCrypto = { next() { if (index$3 >= COUNT) { if (data === null) { data = new I32Array(COUNT); } crypto.getRandomValues(data); index$3 = 0; } return data[index$3++] | 0; } }; /** * Returns an array of random int32 values, based on current time * and a random number engine * * @param engine an Engine to pull random values from, default `nativeMath` * @param length the length of the Array, minimum 1, default 16 */ function createEntropy(engine = nativeMath, length = 16) { const array = []; array.push(new Date().getTime() | 0); for (let i = 1; i < length; ++i) { array[i] = engine.next() | 0; } return array; } /** * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/imul */ const imul = (() => { try { if (Math.imul(UINT32_MAX, 5) === -5) { return Math.imul; } } catch (_) { // nothing to do here } const UINT16_MAX = 0xffff; return (a, b) => { const ah = (a >>> 16) & UINT16_MAX; const al = a & UINT16_MAX; const bh = (b >>> 16) & UINT16_MAX; const bl = b & UINT16_MAX; // the shift by 0 fixes the sign on the high part // the final |0 converts the unsigned value into a signed value return (al * bl + (((ah * bl + al * bh) << 16) >>> 0)) | 0; }; })(); const ARRAY_SIZE = 624; const ARRAY_MAX = ARRAY_SIZE - 1; const M = 397; const ARRAY_SIZE_MINUS_M = ARRAY_SIZE - M; const A = 0x9908b0df; /** * An Engine that is a pseudorandom number generator using the Mersenne * Twister algorithm based on the prime 2**19937 − 1 * * See http://en.wikipedia.org/wiki/Mersenne_twister */ class MersenneTwister19937 { /** * MersenneTwister19937 should not be instantiated directly. * Instead, use the static methods `seed`, `seedWithArray`, or `autoSeed`. */ constructor() { this.data = new I32Array(ARRAY_SIZE); this.index = 0; // integer within [0, 624] this.uses = 0; } /** * Returns a MersenneTwister19937 seeded with an initial int32 value * @param initial the initial seed value */ static seed(initial) { return new MersenneTwister19937().seed(initial); } /** * Returns a MersenneTwister19937 seeded with zero or more int32 values * @param source A series of int32 values */ static seedWithArray(source) { return new MersenneTwister19937().seedWithArray(source); } /** * Returns a MersenneTwister19937 seeded with the current time and * a series of natively-generated random values */ static autoSeed() { return MersenneTwister19937.seedWithArray(createEntropy()); } /** * Returns the next int32 value of the sequence */ next() { if ((this.index | 0) >= ARRAY_SIZE) { refreshData(this.data); this.index = 0; } const value = this.data[this.index]; this.index = (this.index + 1) | 0; this.uses += 1; return temper(value) | 0; } /** * Returns the number of times that the Engine has been used. * * This can be provided to an unused MersenneTwister19937 with the same * seed, bringing it to the exact point that was left off. */ getUseCount() { return this.uses; } /** * Discards one or more items from the engine * @param count The count of items to discard */ discard(count) { if (count <= 0) { return this; } this.uses += count; if ((this.index | 0) >= ARRAY_SIZE) { refreshData(this.data); this.index = 0; } while (count + this.index > ARRAY_SIZE) { count -= ARRAY_SIZE - this.index; refreshData(this.data); this.index = 0; } this.index = (this.index + count) | 0; return this; } seed(initial) { let previous = 0; this.data[0] = previous = initial | 0; for (let i = 1; i < ARRAY_SIZE; i = (i + 1) | 0) { this.data[i] = previous = (imul(previous ^ (previous >>> 30), 0x6c078965) + i) | 0; } this.index = ARRAY_SIZE; this.uses = 0; return this; } seedWithArray(source) { this.seed(0x012bd6aa); seedWithArray(this.data, source); return this; } } function refreshData(data) { let k = 0; let tmp = 0; for (; (k | 0) < ARRAY_SIZE_MINUS_M; k = (k + 1) | 0) { tmp = (data[k] & INT32_SIZE) | (data[(k + 1) | 0] & INT32_MAX); data[k] = data[(k + M) | 0] ^ (tmp >>> 1) ^ (tmp & 0x1 ? A : 0); } for (; (k | 0) < ARRAY_MAX; k = (k + 1) | 0) { tmp = (data[k] & INT32_SIZE) | (data[(k + 1) | 0] & INT32_MAX); data[k] = data[(k - ARRAY_SIZE_MINUS_M) | 0] ^ (tmp >>> 1) ^ (tmp & 0x1 ? A : 0); } tmp = (data[ARRAY_MAX] & INT32_SIZE) | (data[0] & INT32_MAX); data[ARRAY_MAX] = data[M - 1] ^ (tmp >>> 1) ^ (tmp & 0x1 ? A : 0); } function temper(value) { value ^= value >>> 11; value ^= (value << 7) & 0x9d2c5680; value ^= (value << 15) & 0xefc60000; return value ^ (value >>> 18); } function seedWithArray(data, source) { let i = 1; let j = 0; const sourceLength = source.length; let k = Math.max(sourceLength, ARRAY_SIZE) | 0; let previous = data[0] | 0; for (; (k | 0) > 0; --k) { data[i] = previous = ((data[i] ^ imul(previous ^ (previous >>> 30), 0x0019660d)) + (source[j] | 0) + (j | 0)) | 0; i = (i + 1) | 0; ++j; if ((i | 0) > ARRAY_MAX) { data[0] = data[ARRAY_MAX]; i = 1; } if (j >= sourceLength) { j = 0; } } for (k = ARRAY_MAX; (k | 0) > 0; --k) { data[i] = previous = ((data[i] ^ imul(previous ^ (previous >>> 30), 0x5d588b65)) - i) | 0; i = (i + 1) | 0; if ((i | 0) > ARRAY_MAX) { data[0] = data[ARRAY_MAX]; i = 1; } } data[0] = INT32_SIZE; } let data$1 = null; const COUNT$1 = 128; let index$1$1 = COUNT$1; /** * An Engine that relies on the node-available * `require('crypto').randomBytes`, which has been available since 0.58. * * See https://nodejs.org/api/crypto.html#crypto_crypto_randombytes_size_callback * * If unavailable or otherwise non-functioning, then `nodeCrypto` will * likely `throw` on the first call to `next()`. */ const nodeCrypto = { next() { if (index$1$1 >= COUNT$1) { data$1 = new Int32Array(new Int8Array(require("crypto").randomBytes(4 * COUNT$1)).buffer); index$1$1 = 0; } return data$1[index$1$1++] | 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, nodeCrypto, MersenneTwister19937, 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(engine = nativeMath) { this.engine = engine || 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 || nativeMath; this[randomSymbol] = new 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, inclusive = 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, type = 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(text = 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(([field, value]) => { 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 = (...modifiers) => ( // @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, modifiers = [], useInTotal = 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;