From 5f58f9cae8c636bd6daa48d5fe1da5dab2bcaa6c Mon Sep 17 00:00:00 2001 From: Ean Milligan Date: Sat, 28 Jun 2025 05:16:48 -0400 Subject: [PATCH] Properly handle mt! or !m (or other future no number mashups) --- src/artigen/dice/diceOptions.ts | 52 +++++++++++++ src/artigen/dice/getRollConf.ts | 130 +++++++++++++++++--------------- 2 files changed, 121 insertions(+), 61 deletions(-) create mode 100644 src/artigen/dice/diceOptions.ts diff --git a/src/artigen/dice/diceOptions.ts b/src/artigen/dice/diceOptions.ts new file mode 100644 index 0000000..f60cacf --- /dev/null +++ b/src/artigen/dice/diceOptions.ts @@ -0,0 +1,52 @@ +export const DiceOptions = Object.freeze({ + Drop: 'd', + DropLow: 'dl', + DropHigh: 'dh', + Keep: 'k', + KeepLow: 'kl', + KeepHigh: 'kh', + Reroll: 'r', + RerollLt: 'r<', + RerollGtr: 'r>', + RerollEqu: 'r=', + RerollOnce: 'ro', + RerollOnceLt: 'ro<', + RerollOnceGtr: 'ro>', + RerollOnceEqu: 'ro=', + CritSuccess: 'cs', + CritSuccessLt: 'cs<', + CritSuccessGtr: 'cs>', + CritSuccessEqu: 'cs=', + CritFail: 'cf', + CritFailLt: 'cf<', + CritFailGtr: 'cf>', + CritFailEqu: 'cf=', + Exploding: '!', + ExplodingLt: '!<', + ExplodingGtr: '!>', + ExplodingEqu: '!=', + ExplodeOnce: '!o', + ExplodeOnceLt: '!o<', + ExplodeOnceGtr: '!o>', + ExplodeOnceEqu: '!o=', + PenetratingExplosion: '!p', + PenetratingExplosionLt: '!p<', + PenetratingExplosionGtr: '!p>', + PenetratingExplosionEqu: '!p=', + CompoundingExplosion: '!!', + CompoundingExplosionLt: '!!<', + CompoundingExplosionGtr: '!!>', + CompoundingExplosionEqu: '!!=', + Matching: 'm', + MatchingTotal: 'mt', +}); + +// Should be ordered such that 'mt' will be encountered before 'm' +export const NumberlessDiceOptions = [ + DiceOptions.MatchingTotal, + DiceOptions.Matching, + DiceOptions.CompoundingExplosion, + DiceOptions.PenetratingExplosion, + DiceOptions.ExplodeOnce, + DiceOptions.Exploding, +]; diff --git a/src/artigen/dice/getRollConf.ts b/src/artigen/dice/getRollConf.ts index 1acce41..58703f3 100644 --- a/src/artigen/dice/getRollConf.ts +++ b/src/artigen/dice/getRollConf.ts @@ -5,6 +5,7 @@ import { RollConf } from 'artigen/dice/dice.d.ts'; import { getLoopCount, loopCountCheck } from 'artigen/managers/loopManager.ts'; import { loggingEnabled } from 'artigen/utils/logFlag.ts'; +import { DiceOptions, NumberlessDiceOptions } from 'artigen/dice/diceOptions.ts'; const throwDoubleSepError = (sep: string): void => { throw new Error(`DoubleSeparator_${sep}`); @@ -198,11 +199,24 @@ export const getRollConf = (rollStr: string): RollConf => { if (afterSepIdx < 0) { afterSepIdx = remains.length; } + + // Determine if afterSepIdx needs to be moved up (cases like mt! or !mt) + const tempSep = remains.slice(0, afterSepIdx); + let noNumberAfter = false; + NumberlessDiceOptions.some((opt) => { + if (tempSep.startsWith(opt) && tempSep !== opt) { + afterSepIdx = opt.length; + noNumberAfter = true; + return true; + } + return tempSep === opt; + }); + // Save the rule name to tSep and remove it from remains const tSep = remains.slice(0, afterSepIdx); remains = remains.slice(afterSepIdx); // Find the next non-number in the remains to be able to cut out the count/num - let afterNumIdx = remains.search(/\D/); + let afterNumIdx = noNumberAfter ? 0 : remains.search(/\D/); if (afterNumIdx < 0) { afterNumIdx = remains.length; } @@ -211,8 +225,8 @@ export const getRollConf = (rollStr: string): RollConf => { // Switch on rule name switch (tSep) { - case 'dl': - case 'd': + case DiceOptions.Drop: + case DiceOptions.DropLow: if (rollConf.drop.on) { // Ensure we do not override existing settings throwDoubleSepError(tSep); @@ -221,8 +235,8 @@ export const getRollConf = (rollStr: string): RollConf => { rollConf.drop.on = true; rollConf.drop.count = tNum; break; - case 'kh': - case 'k': + case DiceOptions.Keep: + case DiceOptions.KeepHigh: if (rollConf.keep.on) { // Ensure we do not override existing settings throwDoubleSepError(tSep); @@ -231,7 +245,7 @@ export const getRollConf = (rollStr: string): RollConf => { rollConf.keep.on = true; rollConf.keep.count = tNum; break; - case 'dh': + case DiceOptions.DropHigh: if (rollConf.dropHigh.on) { // Ensure we do not override existing settings throwDoubleSepError(tSep); @@ -240,7 +254,7 @@ export const getRollConf = (rollStr: string): RollConf => { rollConf.dropHigh.on = true; rollConf.dropHigh.count = tNum; break; - case 'kl': + case DiceOptions.KeepLow: if (rollConf.keepLow.on) { // Ensure we do not override existing settings throwDoubleSepError(tSep); @@ -249,20 +263,20 @@ export const getRollConf = (rollStr: string): RollConf => { rollConf.keepLow.on = true; rollConf.keepLow.count = tNum; break; - case 'ro': - case 'ro=': + case DiceOptions.RerollOnce: + case DiceOptions.RerollOnceEqu: rollConf.reroll.once = true; // falls through as ro/ro= functions the same as r/r= in this context - case 'r': - case 'r=': + case DiceOptions.Reroll: + case DiceOptions.RerollEqu: // Configure Reroll (this can happen multiple times) rollConf.reroll.on = true; !rollConf.reroll.nums.includes(tNum) && rollConf.reroll.nums.push(tNum); break; - case 'ro>': + case DiceOptions.RerollOnceGtr: rollConf.reroll.once = true; // falls through as ro> functions the same as r> in this context - case 'r>': + case DiceOptions.RerollGtr: // Configure reroll for all numbers greater than or equal to tNum (this could happen multiple times, but why) rollConf.reroll.on = true; for (let i = tNum; i <= rollConf.dieSize; i++) { @@ -272,10 +286,10 @@ export const getRollConf = (rollStr: string): RollConf => { !rollConf.reroll.nums.includes(i) && rollConf.reroll.nums.push(i); } break; - case 'ro<': + case DiceOptions.RerollOnceLt: rollConf.reroll.once = true; // falls through as ro< functions the same as r< in this context - case 'r<': + case DiceOptions.RerollLt: // Configure reroll for all numbers less than or equal to tNum (this could happen multiple times, but why) rollConf.reroll.on = true; for (let i = 1; i <= tNum; i++) { @@ -285,13 +299,13 @@ export const getRollConf = (rollStr: string): RollConf => { !rollConf.reroll.nums.includes(i) && rollConf.reroll.nums.push(i); } break; - case 'cs': - case 'cs=': + case DiceOptions.CritSuccess: + case DiceOptions.CritSuccessEqu: // Configure CritScore for one number (this can happen multiple times) rollConf.critScore.on = true; !rollConf.critScore.range.includes(tNum) && rollConf.critScore.range.push(tNum); break; - case 'cs>': + case DiceOptions.CritSuccessGtr: // Configure CritScore for all numbers greater than or equal to tNum (this could happen multiple times, but why) rollConf.critScore.on = true; for (let i = tNum; i <= rollConf.dieSize; i++) { @@ -301,7 +315,7 @@ export const getRollConf = (rollStr: string): RollConf => { !rollConf.critScore.range.includes(i) && rollConf.critScore.range.push(i); } break; - case 'cs<': + case DiceOptions.CritSuccessLt: // Configure CritScore for all numbers less than or equal to tNum (this could happen multiple times, but why) rollConf.critScore.on = true; for (let i = 0; i <= tNum; i++) { @@ -311,13 +325,13 @@ export const getRollConf = (rollStr: string): RollConf => { !rollConf.critScore.range.includes(i) && rollConf.critScore.range.push(i); } break; - case 'cf': - case 'cf=': + case DiceOptions.CritFail: + case DiceOptions.CritFailEqu: // Configure CritFail for one number (this can happen multiple times) rollConf.critFail.on = true; !rollConf.critFail.range.includes(tNum) && rollConf.critFail.range.push(tNum); break; - case 'cf>': + case DiceOptions.CritFailGtr: // Configure CritFail for all numbers greater than or equal to tNum (this could happen multiple times, but why) rollConf.critFail.on = true; for (let i = tNum; i <= rollConf.dieSize; i++) { @@ -327,7 +341,7 @@ export const getRollConf = (rollStr: string): RollConf => { !rollConf.critFail.range.includes(i) && rollConf.critFail.range.push(i); } break; - case 'cf<': + case DiceOptions.CritFailLt: // Configure CritFail for all numbers less than or equal to tNum (this could happen multiple times, but why) rollConf.critFail.on = true; for (let i = 0; i <= tNum; i++) { @@ -337,32 +351,29 @@ export const getRollConf = (rollStr: string): RollConf => { !rollConf.critFail.range.includes(i) && rollConf.critFail.range.push(i); } break; - case '!': - case '!o': - case '!p': - case '!!': + case DiceOptions.Exploding: + case DiceOptions.ExplodeOnce: + case DiceOptions.PenetratingExplosion: + case DiceOptions.CompoundingExplosion: // Configure Exploding rollConf.exploding.on = true; if (afterNumIdx > 0) { // User gave a number to explode on, save it !rollConf.exploding.nums.includes(tNum) && rollConf.exploding.nums.push(tNum); - } else { - // User did not give number, use cs - afterNumIdx = 1; } break; - case '!=': - case '!o=': - case '!p=': - case '!!=': + case DiceOptions.ExplodingEqu: + case DiceOptions.ExplodeOnceEqu: + case DiceOptions.PenetratingExplosionEqu: + case DiceOptions.CompoundingExplosionEqu: // Configure Exploding (this can happen multiple times) rollConf.exploding.on = true; !rollConf.exploding.nums.includes(tNum) && rollConf.exploding.nums.push(tNum); break; - case '!>': - case '!o>': - case '!p>': - case '!!>': + case DiceOptions.ExplodingGtr: + case DiceOptions.ExplodeOnceGtr: + case DiceOptions.PenetratingExplosionGtr: + case DiceOptions.CompoundingExplosionGtr: // Configure Exploding for all numbers greater than or equal to tNum (this could happen multiple times, but why) rollConf.exploding.on = true; for (let i = tNum; i <= rollConf.dieSize; i++) { @@ -372,10 +383,10 @@ export const getRollConf = (rollStr: string): RollConf => { !rollConf.exploding.nums.includes(i) && rollConf.exploding.nums.push(i); } break; - case '!<': - case '!o<': - case '!p<': - case '!!<': + case DiceOptions.ExplodingLt: + case DiceOptions.ExplodeOnceLt: + case DiceOptions.PenetratingExplosionLt: + case DiceOptions.CompoundingExplosionLt: // Configure Exploding for all numbers less than or equal to tNum (this could happen multiple times, but why) rollConf.exploding.on = true; for (let i = 1; i <= tNum; i++) { @@ -385,15 +396,12 @@ export const getRollConf = (rollStr: string): RollConf => { !rollConf.exploding.nums.includes(i) && rollConf.exploding.nums.push(i); } break; - case 'm': - case 'mt': + case DiceOptions.Matching: + case DiceOptions.MatchingTotal: rollConf.match.on = true; if (afterNumIdx > 0) { - // User gave a number to explode on, save it + // User gave a number to work with, save it rollConf.match.minCount = tNum; - } else { - // User did not give number, use cs - afterNumIdx = 1; } break; default: @@ -403,25 +411,25 @@ export const getRollConf = (rollStr: string): RollConf => { // Followup switch to avoid weird duplicated code switch (tSep) { - case '!o': - case '!o=': - case '!o>': - case '!o<': + case DiceOptions.ExplodeOnce: + case DiceOptions.ExplodeOnceLt: + case DiceOptions.ExplodeOnceGtr: + case DiceOptions.ExplodeOnceEqu: rollConf.exploding.once = true; break; - case '!p': - case '!p=': - case '!p>': - case '!p<': + case DiceOptions.PenetratingExplosion: + case DiceOptions.PenetratingExplosionLt: + case DiceOptions.PenetratingExplosionGtr: + case DiceOptions.PenetratingExplosionEqu: rollConf.exploding.penetrating = true; break; - case '!!': - case '!!=': - case '!!>': - case '!!<': + case DiceOptions.CompoundingExplosion: + case DiceOptions.CompoundingExplosionLt: + case DiceOptions.CompoundingExplosionGtr: + case DiceOptions.CompoundingExplosionEqu: rollConf.exploding.compounding = true; break; - case 'mt': + case DiceOptions.MatchingTotal: rollConf.match.returnTotal = true; break; }