import { log, LogTypes as LT } from '@Log4Deno'; import { RollConf } from 'artigen/dice/dice.d.ts'; import { getLoopCount, loopCountCheck } from 'artigen/managers/loopManager.ts'; import { DiceOptions, NumberlessDiceOptions } from 'artigen/dice/diceOptions.ts'; import { loggingEnabled } from 'artigen/utils/logFlag.ts'; import { addToRange, gtrAddToRange, ltAddToRange } from 'artigen/utils/rangeAdder.ts'; const throwDoubleSepError = (sep: string): void => { throw new Error(`DoubleSeparator_${sep}`); }; // Converts a rollStr into a machine readable rollConf export const getRollConf = (rollStr: string): RollConf => { // Split the roll on the die size (and the drop if its there) const dPts = rollStr.split('d'); // Initialize the configuration to store the parsed data const rollConf: RollConf = { type: '', dieCount: 0, dieSize: 0, dPercent: { on: false, sizeAdjustment: 0, critVal: 0, }, drop: { on: false, count: 0, }, keep: { on: false, count: 0, }, dropHigh: { on: false, count: 0, }, keepLow: { on: false, count: 0, }, reroll: { on: false, once: false, nums: [], }, critScore: { on: false, range: [], }, critFail: { on: false, range: [], }, exploding: { on: false, once: false, compounding: false, penetrating: false, nums: [], }, match: { on: false, minCount: 2, returnTotal: false, }, sort: { on: false, direction: '', }, success: { on: false, range: [], }, fail: { on: false, range: [], }, }; // If the dPts is not long enough, throw error if (dPts.length < 2) { throw new Error(`YouNeedAD_${rollStr}`); } // Fill out the die count, first item will either be an int or empty string, short circuit execution will take care of replacing the empty string with a 1 const rawDC = dPts.shift() || '1'; if (rawDC.includes('.')) { throw new Error('WholeDieCountSizeOnly'); } else if (!rawDC.endsWith('cwo') && !rawDC.endsWith('ova') && rawDC.match(/\D/)) { throw new Error(`CannotParseDieCount_${rawDC}`); } const tempDC = rawDC.replace(/\D/g, ''); // Rejoin all remaining parts let remains = dPts.join('d'); loggingEnabled && log(LT.LOG, `Initial breaking of rollStr ${rawDC} ${tempDC} ${dPts} ${remains}`); // Manual Parsing for custom roll types if (rawDC.endsWith('cwo')) { // CWOD dice parsing rollConf.type = 'cwod'; // Get CWOD parts, setting count and getting difficulty const cwodParts = rollStr.split('cwod'); rollConf.dieCount = parseInt(cwodParts[0] || '1'); rollConf.dieSize = 10; // Use success to set the difficulty rollConf.success.on = true; rollConf.fail.on = true; addToRange('cwod', rollConf.fail.range, 1); const tempDifficulty = (cwodParts[1] ?? '').search(/\d/) === 0 ? cwodParts[1] : ''; let afterDifficultyIdx = tempDifficulty.search(/[^\d]/); if (afterDifficultyIdx === -1) { afterDifficultyIdx = tempDifficulty.length; } const difficulty = parseInt(tempDifficulty.slice(0, afterDifficultyIdx) || '10'); for (let i = difficulty; i <= rollConf.dieSize; i++) { loopCountCheck(); loggingEnabled && log(LT.LOG, `${getLoopCount()} Handling cwod ${rollStr} | Parsing difficulty ${i}`); rollConf.success.range.push(i); } // Remove any garbage from the remains remains = remains.slice(afterDifficultyIdx); } else if (rawDC.endsWith('ova')) { // OVA dice parsing rollConf.type = 'ova'; // Get OVA parts, setting count and getting difficulty const ovaParts = rollStr.split('ovad'); const tempOvaPart1 = (ovaParts[1] ?? '').search(/\d/) === 0 ? ovaParts[1] : ''; if (tempOvaPart1.search(/\d+\.\d/) === 0) { throw new Error('WholeDieCountSizeOnly'); } rollConf.dieCount = parseInt(ovaParts[0] || '1'); let afterOvaSizeIdx = tempOvaPart1.search(/[^\d]/); if (afterOvaSizeIdx === -1) { afterOvaSizeIdx = tempOvaPart1.length; } rollConf.dieSize = parseInt(tempOvaPart1.slice(0, afterOvaSizeIdx) || '6'); // Remove any garbage from the remains remains = remains.slice(afterOvaSizeIdx); } else if (remains.startsWith('f')) { // fate dice setup rollConf.type = 'fate'; rollConf.dieCount = parseInt(tempDC); // dieSize set to 1 as 1 is max face value, a six sided die is used internally rollConf.dieSize = 1; // remove F from the remains remains = remains.slice(1); } else { // roll20 dice setup rollConf.type = 'roll20'; rollConf.dieCount = parseInt(tempDC); // Finds the end of the die size/beginning of the additional options 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 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; } else { rollConf.dieSize = parseInt(rawDS); } if (remains.search(/\.\d/) === 0) { throw new Error('WholeDieCountSizeOnly'); } } loggingEnabled && log(LT.LOG, `${getLoopCount()} Handling ${rollConf.type} ${rollStr} | Parsed Die Count: ${rollConf.dieCount}`); loggingEnabled && log(LT.LOG, `${getLoopCount()} Handling ${rollConf.type} ${rollStr} | Parsed Die Size: ${rollConf.dieSize}`); loggingEnabled && log(LT.LOG, `${getLoopCount()} Handling ${rollConf.type} ${rollStr} | remains: ${remains}`); if (!rollConf.dieCount || !rollConf.dieSize) { throw new Error(`YouNeedAD_${rollStr}`); } // Finish parsing the roll if (remains.length > 0) { // Determine if the first item is a drop, and if it is, add the d back in if (remains.search(/\D/) > 0 || remains.indexOf('l') === 0 || remains.indexOf('h') === 0) { remains = `d${remains}`; } // Loop until all remaining args are parsed while (remains.length > 0) { loopCountCheck(); loggingEnabled && log(LT.LOG, `${getLoopCount()} Handling ${rollConf.type} ${rollStr} | Parsing remains ${remains}`); // Find the next number in the remains to be able to cut out the rule name let afterSepIdx = remains.search(/[-\d]/); 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) => { loopCountCheck(); 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 = noNumberAfter ? 0 : remains.search(/(?![-\d])/); if (afterNumIdx < 0) { afterNumIdx = remains.length; } // Save the count/num to tNum leaving it in remains for the time being const tNum = parseInt(remains.slice(0, afterNumIdx)); loggingEnabled && log(LT.LOG, `${getLoopCount()} tSep: ${tSep} ${afterSepIdx}, tNum: ${tNum} ${afterNumIdx}`); // Switch on rule name switch (tSep) { case DiceOptions.Drop: case DiceOptions.DropLow: if (rollConf.drop.on) { // Ensure we do not override existing settings throwDoubleSepError(tSep); } // Configure Drop (Lowest) rollConf.drop.on = true; rollConf.drop.count = tNum; break; case DiceOptions.Keep: case DiceOptions.KeepHigh: if (rollConf.keep.on) { // Ensure we do not override existing settings throwDoubleSepError(tSep); } // Configure Keep (Highest) rollConf.keep.on = true; rollConf.keep.count = tNum; break; case DiceOptions.DropHigh: if (rollConf.dropHigh.on) { // Ensure we do not override existing settings throwDoubleSepError(tSep); } // Configure Drop (Highest) rollConf.dropHigh.on = true; rollConf.dropHigh.count = tNum; break; case DiceOptions.KeepLow: if (rollConf.keepLow.on) { // Ensure we do not override existing settings throwDoubleSepError(tSep); } // Configure Keep (Lowest) rollConf.keepLow.on = true; rollConf.keepLow.count = tNum; break; 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 DiceOptions.Reroll: case DiceOptions.RerollEqu: // Configure Reroll (this can happen multiple times) rollConf.reroll.on = true; addToRange(tSep, rollConf.reroll.nums, tNum); break; case DiceOptions.RerollOnceGtr: rollConf.reroll.once = true; // falls through as ro> functions the same as r> in this context 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; gtrAddToRange(tSep, rollConf.reroll.nums, tNum, rollConf.dieSize); break; case DiceOptions.RerollOnceLt: rollConf.reroll.once = true; // falls through as ro< functions the same as r< in this context 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; ltAddToRange(tSep, rollConf.reroll.nums, tNum, rollConf.type); break; case DiceOptions.CritSuccess: case DiceOptions.CritSuccessEqu: // Configure CritScore for one number (this can happen multiple times) rollConf.critScore.on = true; addToRange(tSep, rollConf.critScore.range, tNum); break; 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; gtrAddToRange(tSep, rollConf.critScore.range, tNum, rollConf.dieSize); break; 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; ltAddToRange(tSep, rollConf.critScore.range, tNum, rollConf.type); break; case DiceOptions.CritFail: case DiceOptions.CritFailEqu: // Configure CritFail for one number (this can happen multiple times) rollConf.critFail.on = true; addToRange(tSep, rollConf.critFail.range, tNum); break; 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; gtrAddToRange(tSep, rollConf.critFail.range, tNum, rollConf.dieSize); break; 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; ltAddToRange(tSep, rollConf.critFail.range, tNum, rollConf.type); break; 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 addToRange(tSep, rollConf.exploding.nums, tNum); } break; case DiceOptions.ExplodingEqu: case DiceOptions.ExplodeOnceEqu: case DiceOptions.PenetratingExplosionEqu: case DiceOptions.CompoundingExplosionEqu: // Configure Exploding (this can happen multiple times) rollConf.exploding.on = true; addToRange(tSep, rollConf.exploding.nums, tNum); break; 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; gtrAddToRange(tSep, rollConf.exploding.nums, tNum, rollConf.dieSize); break; 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; ltAddToRange(tSep, rollConf.exploding.nums, tNum, rollConf.type); break; case DiceOptions.MatchingTotal: rollConf.match.returnTotal = true; // falls through as mt functions the same as m in this context case DiceOptions.Matching: if (rollConf.match.on) { // Ensure we do not override existing settings throwDoubleSepError(tSep); } rollConf.match.on = true; if (afterNumIdx > 0) { // User gave a number to work with, save it rollConf.match.minCount = tNum; } break; case DiceOptions.Sort: case DiceOptions.SortAsc: if (rollConf.sort.on) { // Ensure we do not override existing settings throwDoubleSepError(tSep); } rollConf.sort.on = true; rollConf.sort.direction = 'a'; break; case DiceOptions.SortDesc: if (rollConf.sort.on) { // Ensure we do not override existing settings throwDoubleSepError(tSep); } rollConf.sort.on = true; rollConf.sort.direction = 'd'; break; case DiceOptions.SuccessEqu: // Configure success (this can happen multiple times) rollConf.success.on = true; addToRange(tSep, rollConf.success.range, tNum); break; case DiceOptions.SuccessGtr: // Configure success for all numbers greater than or equal to tNum (this could happen multiple times, but why) rollConf.success.on = true; gtrAddToRange(tSep, rollConf.success.range, tNum, rollConf.dieSize); break; case DiceOptions.SuccessLt: // Configure success for all numbers less than or equal to tNum (this could happen multiple times, but why) rollConf.success.on = true; ltAddToRange(tSep, rollConf.success.range, tNum, rollConf.type); break; case DiceOptions.Fail: case DiceOptions.FailEqu: // Configure fail (this can happen multiple times) rollConf.fail.on = true; addToRange(tSep, rollConf.fail.range, tNum); break; case DiceOptions.FailGtr: // Configure fail for all numbers greater than or equal to tNum (this could happen multiple times, but why) rollConf.fail.on = true; gtrAddToRange(tSep, rollConf.fail.range, tNum, rollConf.dieSize); break; case DiceOptions.FailLt: // Configure fail for all numbers less than or equal to tNum (this could happen multiple times, but why) rollConf.fail.on = true; ltAddToRange(tSep, rollConf.fail.range, tNum, rollConf.type); break; default: // Throw error immediately if unknown op is encountered throw new Error(`UnknownOperation_${tSep}`); } // Followup switch to avoid weird duplicated code switch (tSep) { case DiceOptions.ExplodeOnce: case DiceOptions.ExplodeOnceLt: case DiceOptions.ExplodeOnceGtr: case DiceOptions.ExplodeOnceEqu: rollConf.exploding.once = true; break; case DiceOptions.PenetratingExplosion: case DiceOptions.PenetratingExplosionLt: case DiceOptions.PenetratingExplosionGtr: case DiceOptions.PenetratingExplosionEqu: rollConf.exploding.penetrating = true; break; case DiceOptions.CompoundingExplosion: case DiceOptions.CompoundingExplosionLt: case DiceOptions.CompoundingExplosionGtr: case DiceOptions.CompoundingExplosionEqu: rollConf.exploding.compounding = true; break; } // Finally slice off everything else parsed this loop remains = remains.slice(afterNumIdx); } } loggingEnabled && log(LT.LOG, `RollConf before cleanup: ${JSON.stringify(rollConf)}`); // Verify the parse, throwing errors for every invalid config if (rollConf.dieCount < 0) { throw new Error('NoZerosAllowed_base'); } if (rollConf.dieCount === 0 || rollConf.dieSize === 0) { throw new Error('NoZerosAllowed_base'); } // Since only one drop or keep option can be active, count how many are active to throw the right error let dkdkCnt = 0; [rollConf.drop.on, rollConf.keep.on, rollConf.dropHigh.on, rollConf.keepLow.on].forEach((e) => { loggingEnabled && log(LT.LOG, `Handling ${rollConf.type} ${rollStr} | Checking if drop/keep is on ${e}`); if (e) { dkdkCnt++; } }); if (dkdkCnt > 1) { throw new Error('FormattingError_dk'); } if (rollConf.match.on && (rollConf.success.on || rollConf.fail.on)) { throw new Error('FormattingError_mtsf'); } if (rollConf.drop.on && rollConf.drop.count === 0) { throw new Error('NoZerosAllowed_drop'); } if (rollConf.keep.on && rollConf.keep.count === 0) { throw new Error('NoZerosAllowed_keep'); } if (rollConf.dropHigh.on && rollConf.dropHigh.count === 0) { throw new Error('NoZerosAllowed_dropHigh'); } if (rollConf.keepLow.on && rollConf.keepLow.count === 0) { throw new Error('NoZerosAllowed_keepLow'); } // Filter rollConf num lists to only include valid numbers const validNumFilter = (curNum: number) => { if (rollConf.type === 'fate') { return [-1, 0, 1].includes(curNum); } return 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); rollConf.exploding.nums = rollConf.exploding.nums.filter(validNumFilter); rollConf.success.range = rollConf.success.range.filter(validNumFilter); rollConf.fail.range = rollConf.fail.range.filter(validNumFilter); if (rollConf.reroll.on && rollConf.reroll.nums.length === (rollConf.type === 'fate' ? 3 : rollConf.dieSize)) { throw new Error('NoRerollOnAllSides'); } loggingEnabled && log(LT.LOG, `RollConf after cleanup: ${JSON.stringify(rollConf)}`); return rollConf; };