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