Add support for d% dice

This commit is contained in:
Ean Milligan 2025-04-28 22:57:49 -04:00
parent f44014c22a
commit 3864cb91fc
4 changed files with 46 additions and 21 deletions

View File

@ -57,7 +57,7 @@ export const parseRoll = (fullCmd: string, modifiers: RollModifiers): SolvedRoll
const [tempConf, tempFormat] = sepRoll.split(config.postfix); const [tempConf, tempFormat] = sepRoll.split(config.postfix);
// Remove all spaces from the operation config and split it by any operator (keeping the operator in mathConf for fullSolver to do math on) // Remove all spaces from the operation config and split it by any operator (keeping the operator in mathConf for fullSolver to do math on)
const mathConf: (string | number | SolvedStep)[] = <(string | number | SolvedStep)[]> tempConf.replace(/ /g, '').split(/([-+()*/%^])/g); const mathConf: (string | number | SolvedStep)[] = <(string | number | SolvedStep)[]> tempConf.replace(/ /g, '').split(/([-+()*/^]|(?<![d%])%)/g);
// Verify there are equal numbers of opening and closing parenthesis by adding 1 for opening parens and subtracting 1 for closing parens // Verify there are equal numbers of opening and closing parenthesis by adding 1 for opening parens and subtracting 1 for closing parens
let parenCnt = 0; let parenCnt = 0;

View File

@ -5,7 +5,7 @@ import {
} from '../../deps.ts'; } from '../../deps.ts';
import { RollModifiers } from '../mod.d.ts'; import { RollModifiers } from '../mod.d.ts';
import { ReturnData, RollSet } from './solver.d.ts'; import { DPercentConf, ReturnData, RollSet } from './solver.d.ts';
export const loggingEnabled = false; export const loggingEnabled = false;
export const legalMath = [Math.abs, Math.ceil, Math.floor, Math.round, Math.sqrt, Math.cbrt]; export const legalMath = [Math.abs, Math.ceil, Math.floor, Math.round, Math.sqrt, Math.cbrt];
@ -13,15 +13,17 @@ export const legalMathOperators = legalMath.map((oper) => oper.name);
// genRoll(size) returns number // genRoll(size) returns number
// genRoll rolls a die of size size and returns the result // genRoll rolls a die of size size and returns the result
export const genRoll = (size: number, modifiers: RollModifiers): number => { export const genRoll = (size: number, modifiers: RollModifiers, dPercent: DPercentConf): number => {
let result;
if (modifiers.maxRoll) { if (modifiers.maxRoll) {
return size; result = size;
} else if (modifiers.minRoll) { } else if (modifiers.minRoll) {
return 1; result = 1;
} else { } else {
// Math.random * size will return a decimal number between 0 and size (excluding size), so add 1 and floor the result to not get 0 as a result // Math.random * size will return a decimal number between 0 and size (excluding size), so add 1 and floor the result to not get 0 as a result
return modifiers.nominalRoll ? size / 2 + 0.5 : Math.floor(Math.random() * size + 1); result = modifiers.nominalRoll ? size / 2 + 0.5 : Math.floor(Math.random() * size + 1);
} }
return dPercent.on ? (result - 1) * dPercent.sizeAdjustment : result;
}; };
// genFateRoll returns -1|0|1 // genFateRoll returns -1|0|1
@ -31,7 +33,7 @@ export const genFateRoll = (modifiers: RollModifiers): number => {
return 0; return 0;
} else { } else {
const sides = [-1, -1, 0, 0, 1, 1]; const sides = [-1, -1, 0, 0, 1, 1];
return sides[genRoll(6, modifiers) - 1]; return sides[genRoll(6, modifiers, <DPercentConf> { on: false }) - 1];
} }
}; };

View File

@ -44,6 +44,11 @@ export const roll = (rollStr: string, modifiers: RollModifiers): RollSet[] => {
const rollConf: RollConf = { const rollConf: RollConf = {
dieCount: 0, dieCount: 0,
dieSize: 0, dieSize: 0,
dPercent: {
on: false,
sizeAdjustment: 0,
critVal: 0,
},
drop: { drop: {
on: false, on: false,
count: 0, count: 0,
@ -147,15 +152,26 @@ export const roll = (rollStr: string, modifiers: RollModifiers): RollSet[] => {
rollConf.dieCount = parseInt(tempDC); rollConf.dieCount = parseInt(tempDC);
// Finds the end of the die size/beginning of the additional options // Finds the end of the die size/beginning of the additional options
let afterDieIdx = dPts[0].search(/\D/); let afterDieIdx = dPts[0].search(/[^%\d]/);
if (afterDieIdx === -1) { if (afterDieIdx === -1) {
afterDieIdx = dPts[0].length; afterDieIdx = dPts[0].length;
} }
// Get the die size out of the remains and into the rollConf // Get the die size out of the remains and into the rollConf
rollConf.dieSize = parseInt(remains.slice(0, afterDieIdx)); const rawDS = remains.slice(0, afterDieIdx);
remains = remains.slice(afterDieIdx); remains = remains.slice(afterDieIdx);
if (rawDS.startsWith('%')) {
rollConf.dieSize = 10;
rollConf.dPercent.on = true;
const percentCount = rawDS.match(/%/g)?.length ?? 1;
rollConf.dPercent.sizeAdjustment = Math.pow(10, percentCount - 1);
rollConf.dPercent.critVal = Math.pow(10, percentCount) - rollConf.dPercent.sizeAdjustment;
console.log(percentCount, rollConf.dPercent);
} else {
rollConf.dieSize = parseInt(rawDS);
}
if (remains.search(/\.\d/) === 0) { if (remains.search(/\.\d/) === 0) {
throw new Error('WholeDieCountSizeOnly'); throw new Error('WholeDieCountSizeOnly');
} }
@ -436,12 +452,12 @@ export const roll = (rollStr: string, modifiers: RollModifiers): RollSet[] => {
if (rollConf.keepLow.on && rollConf.keepLow.count === 0) { if (rollConf.keepLow.on && rollConf.keepLow.count === 0) {
throw new Error('NoZerosAllowed_keepLow'); throw new Error('NoZerosAllowed_keepLow');
} }
if (rollConf.reroll.on && rollConf.reroll.nums.includes(0)) { if (rollConf.reroll.on && !rollConf.dPercent.on && rollConf.reroll.nums.includes(0)) {
throw new Error('NoZerosAllowed_reroll'); throw new Error('NoZerosAllowed_reroll');
} }
// Filter rollConf num lists to only include valid numbers // Filter rollConf num lists to only include valid numbers
const validNumFilter = (curNum: number) => curNum <= rollConf.dieSize && curNum > 0; const validNumFilter = (curNum: number) => curNum <= rollConf.dieSize && curNum > (rollConf.dPercent.on ? -1 : 0);
rollConf.reroll.nums = rollConf.reroll.nums.filter(validNumFilter); rollConf.reroll.nums = rollConf.reroll.nums.filter(validNumFilter);
rollConf.critScore.range = rollConf.critScore.range.filter(validNumFilter); rollConf.critScore.range = rollConf.critScore.range.filter(validNumFilter);
rollConf.critFail.range = rollConf.critFail.range.filter(validNumFilter); rollConf.critFail.range = rollConf.critFail.range.filter(validNumFilter);
@ -497,7 +513,7 @@ export const roll = (rollStr: string, modifiers: RollModifiers): RollSet[] => {
// Copy the template to fill out for this iteration // Copy the template to fill out for this iteration
const rolling = getTemplateRoll(); const rolling = getTemplateRoll();
// If maximizeRoll is on, set the roll to the dieSize, else if nominalRoll is on, set the roll to the average roll of dieSize, else generate a new random roll // If maximizeRoll is on, set the roll to the dieSize, else if nominalRoll is on, set the roll to the average roll of dieSize, else generate a new random roll
rolling.roll = rollType === 'fate' ? genFateRoll(modifiers) : genRoll(rollConf.dieSize, modifiers); rolling.roll = rollType === 'fate' ? genFateRoll(modifiers) : genRoll(rollConf.dieSize, modifiers, rollConf.dPercent);
// Set origIdx of roll // Set origIdx of roll
rolling.origIdx = i; rolling.origIdx = i;
@ -505,7 +521,7 @@ export const roll = (rollStr: string, modifiers: RollModifiers): RollSet[] => {
if (rollConf.critScore.on && rollConf.critScore.range.includes(rolling.roll)) { if (rollConf.critScore.on && rollConf.critScore.range.includes(rolling.roll)) {
rolling.critHit = true; rolling.critHit = true;
} else if (!rollConf.critScore.on) { } else if (!rollConf.critScore.on) {
rolling.critHit = rolling.roll === rollConf.dieSize; rolling.critHit = rolling.roll === (rollConf.dPercent.on ? rollConf.dPercent.critVal : rollConf.dieSize);
} }
// If critFail arg is on, check if the roll should be a fail, if its off, check if the roll matches 1 // If critFail arg is on, check if the roll should be a fail, if its off, check if the roll matches 1
if (rollConf.critFail.on && rollConf.critFail.range.includes(rolling.roll)) { if (rollConf.critFail.on && rollConf.critFail.range.includes(rolling.roll)) {
@ -514,7 +530,7 @@ export const roll = (rollStr: string, modifiers: RollModifiers): RollSet[] => {
if (rollType === 'fate') { if (rollType === 'fate') {
rolling.critFail = rolling.roll === -1; rolling.critFail = rolling.roll === -1;
} else { } else {
rolling.critFail = rolling.roll === 1; rolling.critFail = rolling.roll === (rollConf.dPercent.on ? 0 : 1);
} }
} }
@ -549,7 +565,7 @@ export const roll = (rollStr: string, modifiers: RollModifiers): RollSet[] => {
} }
} else if (modifiers.minRoll && !minMaxOverride) { } else if (modifiers.minRoll && !minMaxOverride) {
// If minimizeRoll is on and we've entered the reroll code, 1 is not allowed, determine the next best option and always return that // If minimizeRoll is on and we've entered the reroll code, 1 is not allowed, determine the next best option and always return that
mmMinLoop: for (let m = 2; m <= rollConf.dieSize; m++) { mmMinLoop: for (let m = rollConf.dPercent.on ? 1 : 2; m <= rollConf.dieSize; m++) {
loopCountCheck(++loopCount); loopCountCheck(++loopCount);
if (!rollConf.reroll.nums.includes(m)) { if (!rollConf.reroll.nums.includes(m)) {
@ -563,20 +579,20 @@ export const roll = (rollStr: string, modifiers: RollModifiers): RollSet[] => {
newReroll.roll = minMaxOverride; newReroll.roll = minMaxOverride;
} else { } else {
// If nominalRoll is on, set the roll to the average roll of dieSize, otherwise generate a new random roll // If nominalRoll is on, set the roll to the average roll of dieSize, otherwise generate a new random roll
newReroll.roll = genRoll(rollConf.dieSize, modifiers); newReroll.roll = genRoll(rollConf.dieSize, modifiers, rollConf.dPercent);
} }
// If critScore arg is on, check if the roll should be a crit, if its off, check if the roll matches the die size // If critScore arg is on, check if the roll should be a crit, if its off, check if the roll matches the die size
if (rollConf.critScore.on && rollConf.critScore.range.includes(newReroll.roll)) { if (rollConf.critScore.on && rollConf.critScore.range.includes(newReroll.roll)) {
newReroll.critHit = true; newReroll.critHit = true;
} else if (!rollConf.critScore.on) { } else if (!rollConf.critScore.on) {
newReroll.critHit = newReroll.roll === rollConf.dieSize; newReroll.critHit = newReroll.roll === (rollConf.dPercent.on ? rollConf.dPercent.critVal : rollConf.dieSize);
} }
// If critFail arg is on, check if the roll should be a fail, if its off, check if the roll matches 1 // If critFail arg is on, check if the roll should be a fail, if its off, check if the roll matches 1
if (rollConf.critFail.on && rollConf.critFail.range.includes(newReroll.roll)) { if (rollConf.critFail.on && rollConf.critFail.range.includes(newReroll.roll)) {
newReroll.critFail = true; newReroll.critFail = true;
} else if (!rollConf.critFail.on) { } else if (!rollConf.critFail.on) {
newReroll.critFail = newReroll.roll === 1; newReroll.critFail = newReroll.roll === (rollConf.dPercent.on ? 0 : 1);
} }
// Slot this new roll in after the current iteration so it can be processed in the next loop // Slot this new roll in after the current iteration so it can be processed in the next loop
@ -593,7 +609,7 @@ export const roll = (rollStr: string, modifiers: RollModifiers): RollSet[] => {
// Copy the template to fill out for this iteration // Copy the template to fill out for this iteration
const newExplodingRoll = getTemplateRoll(); const newExplodingRoll = getTemplateRoll();
// If maximizeRoll is on, set the roll to the dieSize, else if nominalRoll is on, set the roll to the average roll of dieSize, else generate a new random roll // If maximizeRoll is on, set the roll to the dieSize, else if nominalRoll is on, set the roll to the average roll of dieSize, else generate a new random roll
newExplodingRoll.roll = genRoll(rollConf.dieSize, modifiers); newExplodingRoll.roll = genRoll(rollConf.dieSize, modifiers, rollConf.dPercent);
// Always mark this roll as exploding // Always mark this roll as exploding
newExplodingRoll.exploding = true; newExplodingRoll.exploding = true;
@ -601,13 +617,13 @@ export const roll = (rollStr: string, modifiers: RollModifiers): RollSet[] => {
if (rollConf.critScore.on && rollConf.critScore.range.includes(newExplodingRoll.roll)) { if (rollConf.critScore.on && rollConf.critScore.range.includes(newExplodingRoll.roll)) {
newExplodingRoll.critHit = true; newExplodingRoll.critHit = true;
} else if (!rollConf.critScore.on) { } else if (!rollConf.critScore.on) {
newExplodingRoll.critHit = newExplodingRoll.roll === rollConf.dieSize; newExplodingRoll.critHit = newExplodingRoll.roll === (rollConf.dPercent.on ? rollConf.dPercent.critVal : rollConf.dieSize);
} }
// If critFail arg is on, check if the roll should be a fail, if its off, check if the roll matches 1 // If critFail arg is on, check if the roll should be a fail, if its off, check if the roll matches 1
if (rollConf.critFail.on && rollConf.critFail.range.includes(newExplodingRoll.roll)) { if (rollConf.critFail.on && rollConf.critFail.range.includes(newExplodingRoll.roll)) {
newExplodingRoll.critFail = true; newExplodingRoll.critFail = true;
} else if (!rollConf.critFail.on) { } else if (!rollConf.critFail.on) {
newExplodingRoll.critFail = newExplodingRoll.roll === 1; newExplodingRoll.critFail = newExplodingRoll.roll === (rollConf.dPercent.on ? 0 : 1);
} }
// Slot this new roll in after the current iteration so it can be processed in the next loop // Slot this new roll in after the current iteration so it can be processed in the next loop

View File

@ -59,10 +59,17 @@ export type SolvedRoll = {
counts: CountDetails; counts: CountDetails;
}; };
export type DPercentConf = {
on: boolean;
sizeAdjustment: number;
critVal: number;
};
// RollConf is used by the roll20 setup // RollConf is used by the roll20 setup
export type RollConf = { export type RollConf = {
dieCount: number; dieCount: number;
dieSize: number; dieSize: number;
dPercent: DPercentConf;
drop: { drop: {
on: boolean; on: boolean;
count: number; count: number;