From 3864cb91fc36a1e2219d9de7350eaf399c44cbf5 Mon Sep 17 00:00:00 2001 From: Ean Milligan Date: Mon, 28 Apr 2025 22:57:49 -0400 Subject: [PATCH] Add support for d% dice --- src/solver/parser.ts | 2 +- src/solver/rollUtils.ts | 14 +++++++------ src/solver/roller.ts | 44 ++++++++++++++++++++++++++++------------- src/solver/solver.d.ts | 7 +++++++ 4 files changed, 46 insertions(+), 21 deletions(-) diff --git a/src/solver/parser.ts b/src/solver/parser.ts index 543c22e..d5a6fcf 100644 --- a/src/solver/parser.ts +++ b/src/solver/parser.ts @@ -57,7 +57,7 @@ export const parseRoll = (fullCmd: string, modifiers: RollModifiers): SolvedRoll 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) - 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(/([-+()*/^]|(? oper.name); // genRoll(size) returns number // 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) { - return size; + result = size; } else if (modifiers.minRoll) { - return 1; + result = 1; } 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 - 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 @@ -31,7 +33,7 @@ export const genFateRoll = (modifiers: RollModifiers): number => { return 0; } else { const sides = [-1, -1, 0, 0, 1, 1]; - return sides[genRoll(6, modifiers) - 1]; + return sides[genRoll(6, modifiers, { on: false }) - 1]; } }; diff --git a/src/solver/roller.ts b/src/solver/roller.ts index 59668e8..d17c07d 100644 --- a/src/solver/roller.ts +++ b/src/solver/roller.ts @@ -44,6 +44,11 @@ export const roll = (rollStr: string, modifiers: RollModifiers): RollSet[] => { const rollConf: RollConf = { dieCount: 0, dieSize: 0, + dPercent: { + on: false, + sizeAdjustment: 0, + critVal: 0, + }, drop: { on: false, count: 0, @@ -147,15 +152,26 @@ export const roll = (rollStr: string, modifiers: RollModifiers): RollSet[] => { rollConf.dieCount = parseInt(tempDC); // 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) { afterDieIdx = dPts[0].length; } // 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); + 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) { throw new Error('WholeDieCountSizeOnly'); } @@ -436,12 +452,12 @@ export const roll = (rollStr: string, modifiers: RollModifiers): RollSet[] => { if (rollConf.keepLow.on && rollConf.keepLow.count === 0) { 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'); } // 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.critScore.range = rollConf.critScore.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 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 - 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 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)) { rolling.critHit = true; } 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 (rollConf.critFail.on && rollConf.critFail.range.includes(rolling.roll)) { @@ -514,7 +530,7 @@ export const roll = (rollStr: string, modifiers: RollModifiers): RollSet[] => { if (rollType === 'fate') { rolling.critFail = rolling.roll === -1; } 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) { // 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); if (!rollConf.reroll.nums.includes(m)) { @@ -563,20 +579,20 @@ export const roll = (rollStr: string, modifiers: RollModifiers): RollSet[] => { newReroll.roll = minMaxOverride; } else { // 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 (rollConf.critScore.on && rollConf.critScore.range.includes(newReroll.roll)) { newReroll.critHit = true; } 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 (rollConf.critFail.on && rollConf.critFail.range.includes(newReroll.roll)) { newReroll.critFail = true; } 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 @@ -593,7 +609,7 @@ export const roll = (rollStr: string, modifiers: RollModifiers): RollSet[] => { // Copy the template to fill out for this iteration 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 - newExplodingRoll.roll = genRoll(rollConf.dieSize, modifiers); + newExplodingRoll.roll = genRoll(rollConf.dieSize, modifiers, rollConf.dPercent); // Always mark this roll as exploding 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)) { newExplodingRoll.critHit = true; } 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 (rollConf.critFail.on && rollConf.critFail.range.includes(newExplodingRoll.roll)) { newExplodingRoll.critFail = true; } 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 diff --git a/src/solver/solver.d.ts b/src/solver/solver.d.ts index 1153982..77ad9c3 100644 --- a/src/solver/solver.d.ts +++ b/src/solver/solver.d.ts @@ -59,10 +59,17 @@ export type SolvedRoll = { counts: CountDetails; }; +export type DPercentConf = { + on: boolean; + sizeAdjustment: number; + critVal: number; +}; + // RollConf is used by the roll20 setup export type RollConf = { dieCount: number; dieSize: number; + dPercent: DPercentConf; drop: { on: boolean; count: number;