//-----------------------------------------------------------------------------
// Dice
//-----------------------------------------------------------------------------
/**
* @class RezDie
* @category Utilities
* @description Represents a single die with a configurable number of sides.
*
* Provides basic rolling functionality including standard rolls and "open" (exploding)
* rolls where rolling the maximum value causes a re-roll that adds to the total.
*
* @example
* const d6 = new RezDie(6);
* const result = d6.roll(); // Returns 1-6
*
* @example
* // Open roll - if you roll max, roll again and add
* const d6 = new RezDie(6);
* const result = d6.open_roll(); // Could return 7+ if 6 was rolled
*/
class RezDie {
/** @type {number} */
#sides;
/**
* @function constructor
* @memberof RezDie
* @description Creates a new die.
*
* @param {number} [sides=6] - The number of sides on the die
*/
constructor(sides = 6) {
this.#sides = sides;
}
/**
* The number of sides on this die.
* @type {number}
*/
get sides() {
return this.#sides;
}
/**
* Rolls the die once.
*
* @returns {number} A random value between 1 and the number of sides
*/
roll() {
return Math.rand_int_between(1, this.sides);
}
/**
* Performs an "open" or "exploding" roll.
*
* If the maximum value is rolled, the die is rolled again and the results
* are summed. This continues until a non-maximum value is rolled.
*
* @returns {number} The total of all rolls
*
* @example
* // On a d6, rolling 6, 6, 3 would return 15
*/
open_roll() {
let roll, total = 0;
do {
roll = this.roll();
total += roll;
} while(roll === this.sides);
return total;
}
}
window.Rez.RezDie = RezDie;
/**
* @class RezDieRoll
* @category Utilities
* @description Represents a dice roll configuration with multiple dice, modifiers, and special rules.
*
* Supports:
* - Multiple dice of the same type (e.g., 3d6)
* - Numeric modifiers (e.g., 2d6+3)
* - Exploding dice (re-roll and add on max)
* - Advantage (roll twice, take higher)
* - Disadvantage (roll twice, take lower)
* - Multiple rounds with averaging
*
* @example
* // Roll 2d6+3
* const roll = new RezDieRoll(2, 6, 3);
* const result = roll.roll();
*
* @example
* // Roll with advantage
* const roll = new RezDieRoll(1, 20, 0);
* roll.advantage = true;
* const result = roll.roll(); // Rolls twice, takes higher
*/
class RezDieRoll {
/** @type {RezDie} */
#die;
/** @type {number} */
#count;
/** @type {number} */
#modifier;
/** @type {number} */
#rounds;
/** @type {boolean} */
#exploding;
/** @type {boolean} */
#advantage;
/** @type {boolean} */
#disadvantage;
/**
* @function constructor
* @memberof RezDieRoll
* @description Creates a new dice roll configuration.
*
* @param {number} count - Number of dice to roll
* @param {number} [sides=6] - Number of sides per die
* @param {number} [modifier=0] - Flat modifier to add to the result
* @param {number} [rounds=1] - Number of rounds to roll (results are averaged)
*/
constructor(count, sides = 6, modifier = 0, rounds = 1) {
this.#die = new RezDie(sides);
this.#count = count;
this.#modifier = modifier;
this.#rounds = rounds;
this.#exploding = false;
this.#advantage = false;
this.#disadvantage = false;
}
/**
* Number of dice to roll.
* @type {number}
*/
get count() {
return this.#count;
}
/**
* The underlying die object.
* @type {RezDie}
*/
get die() {
return this.#die;
}
/**
* Number of sides on each die.
* @type {number}
*/
get sides() {
return this.#die.sides;
}
/**
* Flat modifier added to the roll result.
* @type {number}
*/
get modifier() {
return this.#modifier;
}
/**
* Number of rounds to roll (results are averaged).
* @type {number}
*/
get rounds() {
return this.#rounds;
}
/**
* Enables or disables exploding dice.
*
* When enabled, rolling the maximum value causes a re-roll that adds to the total.
* Mutually exclusive with advantage and disadvantage.
*
* @type {boolean}
*/
set exploding(exploding) {
this.#exploding = exploding;
if(exploding) {
this.#advantage = false;
this.#disadvantage = false;
}
}
/**
* Enables or disables advantage.
*
* When enabled, rolls twice and takes the higher result.
* Mutually exclusive with exploding and disadvantage.
*
* @type {boolean}
*/
set advantage(advantage) {
this.#advantage = advantage;
if(advantage) {
this.#exploding = false;
this.#disadvantage = false;
}
}
/**
* Enables or disables disadvantage.
*
* When enabled, rolls twice and takes the lower result.
* Mutually exclusive with exploding and advantage.
*
* @type {boolean}
*/
set disadvantage(disadvantage) {
this.#disadvantage = disadvantage;
if(disadvantage) {
this.#exploding = false;
this.#advantage = false;
}
}
/**
* Creates a copy of this dice roll configuration.
*
* @returns {RezDieRoll} A new RezDieRoll with the same settings
*/
copy() {
const die = new RezDieRoll(this.count, this.sides, this.modifier, this.rounds);
die.#exploding = this.#exploding;
die.#advantage = this.#advantage;
die.#disadvantage = this.#disadvantage;
return die;
}
/**
* Rolls all dice once and returns the sum plus modifier.
*
* If exploding is enabled, uses open rolls for each die.
*
* @returns {number} The total of all dice plus modifier
*/
rollDice() {
let sum = this.modifier;
for(let i = 0; i < this.count; i++) {
if(this.#exploding) {
sum += this.die.open_roll();
} else {
sum += this.die.roll();
}
}
return sum;
}
/**
* Rolls twice and returns the higher result.
*
* @returns {number} The higher of two roll results
*/
rollWithAdvantage() {
return [this.rollDice(), this.rollDice()].max();
}
/**
* Rolls twice and returns the lower result.
*
* @returns {number} The lower of two roll results
*/
rollWithDisadvange() {
return [this.rollDice(), this.rollDice()].min();
}
/**
* Performs a single round of rolling, applying advantage/disadvantage if set.
*
* @returns {number} The result of this round
*/
rollRound() {
if(this.#advantage) {
return this.rollWithAdvantage();
} else if(this.#disadvantage) {
return this.rollWithDisadvange();
} else {
return this.rollDice();
}
}
/**
* Performs the complete roll.
*
* If multiple rounds are configured, rolls each round and returns
* the ceiling average of all rounds.
*
* @returns {number} The final roll result
*/
roll() {
if(this.rounds === 1) {
return this.rollRound();
} else {
const sum = Math.range(1, this.rounds)
.map(() => this.rollRound())
.reduce((sum, round) => sum + round, 0);
return sum.cl_avg(this.rounds);
}
}
/**
* Returns a string description of this dice roll (e.g., "2d6+3").
*
* @returns {string} The dice notation string
*/
description() {
return `${this.desc_count()}d${this.die.sides}${this.desc_mod()}`;
}
/**
* Returns the count portion of the dice notation.
*
* @returns {string} The count as a string, or empty if count is 0
*/
desc_count() {
if(this.count > 0) {
return `${this.count}`;
} else {
return "";
}
}
/**
* Returns the modifier portion of the dice notation.
*
* @returns {string} The modifier with sign (e.g., "+3" or "-2"), or empty if 0
*/
desc_mod() {
if(this.modifier < 0) {
return `${this.modifier}`;
} else if(this.modifier > 0) {
return `+${this.modifier}`;
} else {
return "";
}
}
}
window.Rez.RezDieRoll = RezDieRoll;
/**
* @function makeDie
* @memberof Rez
* @description Parses a dice notation string and creates a RezDieRoll.
*
* Supports standard dice notation with optional modifiers and special flags:
* - Basic: "d6", "2d6", "3d8"
* - With modifier: "2d6+3", "d20-1"
* - With advantage: "d20a"
* - With disadvantage: "d20d"
* - Exploding: "2d6!"
*
* @param {string} diceStr - The dice notation string to parse
* @returns {RezDieRoll} A configured RezDieRoll object
* @throws {Error} If the dice format is invalid
*
* @example
* const roll = Rez.makeDie("2d6+3");
* const result = roll.roll();
*
* @example
* const roll = Rez.makeDie("d20a"); // d20 with advantage
*/
window.Rez.makeDie = (diceStr) => {
const regex = /^(\d+)?d(\d+)([+-]\d+)?([!ad])?$/i;
const match = diceStr.match(regex);
if(!match) {
throw new Error('Invalid dice format');
}
const [_, count = '1', sides, modifier = '0', special] = match;
const numDice = parseInt(count, 10);
const numSides = parseInt(sides, 10);
const mod = parseInt(modifier, 10);
const die = new RezDieRoll(numDice, numSides, mod);
if(special === "a") {
die.advantage = true;
} else if(special === "d") {
die.disadvantage = true;
} else if(special === "!") {
die.exploding = true
};
return die;
}
/** Pre-configured d4 */
window.Rez.D4 = Rez.makeDie("d4");
/** Rolls a d4 @returns {number} */
window.Rez.rollD4 = () => window.Rez.D4.roll();
/** Pre-configured d6 */
window.Rez.D6 = Rez.makeDie("d6");
/** Rolls a d6 @returns {number} */
window.Rez.rollD6 = () => window.Rez.D6.roll();
/** Pre-configured d8 */
window.Rez.D8 = Rez.makeDie("d8");
/** Rolls a d8 @returns {number} */
window.Rez.rollD8 = () => window.Rez.D8.roll();
/** Pre-configured d10 */
window.Rez.D10 = Rez.makeDie("d10");
/** Rolls a d10 @returns {number} */
window.Rez.rollD10 = () => window.Rez.D10.roll();
/** Pre-configured d12 */
window.Rez.D12 = Rez.makeDie("d12");
/** Rolls a d12 @returns {number} */
window.Rez.rollD12 = () => window.Rez.D12.roll();
/** Pre-configured d20 */
window.Rez.D20 = Rez.makeDie("d20");
/** Rolls a d20 @returns {number} */
window.Rez.rollD20 = () => window.Rez.D20.roll();
/** Pre-configured d100 */
window.Rez.D100 = Rez.makeDie("d100");
/** Rolls a d100 @returns {number} */
window.Rez.rollD100 = () => window.Rez.D100.roll();
Source