diff --git a/.bruno/Authenticated/Roll Requests/Roll Dice.bru b/.bruno/Authenticated/Roll Requests/Roll Dice.bru index e5d2360..348c81c 100644 --- a/.bruno/Authenticated/Roll Requests/Roll Dice.bru +++ b/.bruno/Authenticated/Roll Requests/Roll Dice.bru @@ -5,7 +5,7 @@ meta { } get { - url: http://localhost:8166/api/roll?user=[discord-user-id]&channel=[discord-channel-id]&rollstr=[artificer-roll-cmd]&documentation=All items below are optional. Flags do not need values.&nd=[no-details-flag]&snd=[super-no-details-flag]&hr=[hide-raw-roll-details-flag]&s=[spoiler-results-flag]&m-or-max=[max-roll-flag, cannot be used with n flag]&min=[min-roll-flag, cannot be used with n, sn, or max]&n=[nominal-roll-flag, cannot be used with sn, max or min flag]&sn=[simulated-nominal-flag, can pass number with it, cannot be used with max, min, n. or cc]&gms=[csv-of-discord-user-ids-to-be-dmed-results]&o=[order-rolls, must be a or d]&c=[count-flag]&cc=[confirm-crit-flag, cannot be used with sn]&rd=[roll-dist-flag]&nv-or-vn=[number-variables-flag] + url: http://localhost:8166/api/roll?user=[discord-user-id]&channel=[discord-channel-id]&rollstr=[artificer-roll-cmd]&documentation=All items below are optional. Flags do not need values.&nd=[no-details-flag]&snd=[super-no-details-flag]&hr=[hide-raw-roll-details-flag]&s=[spoiler-results-flag]&m-or-max=[max-roll-flag, cannot be used with n flag]&min=[min-roll-flag, cannot be used with n, sn, or max]&n=[nominal-roll-flag, cannot be used with sn, max or min flag]&sn=[simulated-nominal-flag, can pass number with it, cannot be used with max, min, n. or cc]&gms=[csv-of-discord-user-ids-to-be-dmed-results]&o=[order-rolls, must be a or d]&c=[count-flag]&cc=[confirm-crit-flag, cannot be used with sn]&rd=[roll-dist-flag]&nv-or-vn=[number-variables-flag]&cd=[custom-dice, format value as name:[side1,side2,...,sideN], use ; to separate multiple custom dice] body: none auth: inherit } @@ -29,4 +29,5 @@ params:query { cc: [confirm-crit-flag, cannot be used with sn] rd: [roll-dist-flag] nv-or-vn: [number-variables-flag] + cd: [custom-dice, format value as name:[side1,side2,...,sideN], use ; to separate multiple custom dice] } diff --git a/README.md b/README.md index 2706a4d..136c526 100644 --- a/README.md +++ b/README.md @@ -149,6 +149,7 @@ The Artificer comes with a few supplemental commands to the main rolling command * `-rd` - Roll Distribution - Shows a raw roll distribution of all dice in roll * `-hr` - Hide Raw - Hide the raw input, showing only the results/details of the roll * `-nv` or `-vn` - Number Variables - Adds `xN` before each roll command in the details section for debug reasons + * `-cd` - Custom Dice shapes - Allows a list of `name:[side1,side2,...,sideN]` separated by `;` to be passed to create special shaped dice * The results have some formatting applied on them to provide details on what happened during this roll. * Critical successes will be **bolded** * Critical fails will be underlined diff --git a/src/artigen/dice/dice.d.ts b/src/artigen/dice/dice.d.ts index 644d835..08ffc24 100644 --- a/src/artigen/dice/dice.d.ts +++ b/src/artigen/dice/dice.d.ts @@ -1,7 +1,7 @@ import { SolvedStep } from 'artigen/math/math.d.ts'; // Available Roll Types -type RollType = '' | 'roll20' | 'fate' | 'cwod' | 'ova'; +type RollType = '' | 'custom' | 'roll20' | 'fate' | 'cwod' | 'ova'; // RollSet is used to preserve all information about a calculated roll export interface RollSet { @@ -35,6 +35,8 @@ export interface CountDetails { // use rollDistKey to generate the key export type RollDistributionMap = Map; +export type CustomDiceShapes = Map; + // RollFormat is the return structure for the rollFormatter export interface FormattedRoll { solvedStep: SolvedStep; @@ -60,6 +62,7 @@ export interface RollModifiers { confirmCrit: boolean; rollDist: boolean; numberVariables: boolean; + customDiceShapes: CustomDiceShapes; apiWarn: string; valid: boolean; error: Error; @@ -115,6 +118,7 @@ export interface GroupConf extends BaseConf { // RollConf carries the machine readable roll configuration the user specified export interface RollConf extends BaseConf { type: RollType; + customType: string | null; dieCount: number; dieSize: number; dPercent: DPercentConf; diff --git a/src/artigen/dice/executeRoll.ts b/src/artigen/dice/executeRoll.ts index ab0e1a4..e436e44 100644 --- a/src/artigen/dice/executeRoll.ts +++ b/src/artigen/dice/executeRoll.ts @@ -1,7 +1,7 @@ import { log, LogTypes as LT } from '@Log4Deno'; import { ExecutedRoll, RollModifiers, RollSet, SumOverride } from 'artigen/dice/dice.d.ts'; -import { genFateRoll, genRoll } from 'artigen/dice/randomRoll.ts'; +import { generateRoll } from 'artigen/dice/randomRoll.ts'; import { getRollConf } from 'artigen/dice/getRollConf.ts'; import { loggingEnabled } from 'artigen/utils/logFlag.ts'; @@ -24,7 +24,7 @@ export const executeRoll = (rollStr: string, modifiers: RollModifiers): Executed rollStr = rollStr.toLowerCase(); // Turn the rollStr into a machine readable rollConf - const rollConf = getRollConf(rollStr); + const rollConf = getRollConf(rollStr, modifiers.customDiceShapes); // Roll the roll const rollSet: RollSet[] = []; @@ -85,12 +85,12 @@ export const executeRoll = (rollStr: string, modifiers: RollModifiers): Executed // 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 = rollConf.type === 'fate' ? genFateRoll(modifiers) : genRoll(rollConf.dieSize, modifiers, rollConf.dPercent); + rolling.roll = generateRoll(rollConf, modifiers); rolling.size = rollConf.dieSize; // Set origIdx of roll rolling.origIdx = i; - flagRoll(rollConf, rolling); + flagRoll(rollConf, rolling, modifiers.customDiceShapes); loggingEnabled && log(LT.LOG, `${getLoopCount()} Roll done ${JSON.stringify(rolling)}`); @@ -140,10 +140,10 @@ export const executeRoll = (rollStr: string, modifiers: RollModifiers): Executed 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 = rollConf.type === 'fate' ? genFateRoll(modifiers) : genRoll(rollConf.dieSize, modifiers, rollConf.dPercent); + newReroll.roll = generateRoll(rollConf, modifiers); } - flagRoll(rollConf, newReroll); + flagRoll(rollConf, newReroll, modifiers.customDiceShapes); loggingEnabled && log(LT.LOG, `${getLoopCount()} Roll done ${JSON.stringify(newReroll)}`); @@ -161,12 +161,12 @@ export const executeRoll = (rollStr: string, modifiers: RollModifiers): Executed // 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 = rollConf.type === 'fate' ? genFateRoll(modifiers) : genRoll(rollConf.dieSize, modifiers, rollConf.dPercent); + newExplodingRoll.roll = generateRoll(rollConf, modifiers); newExplodingRoll.size = rollConf.dieSize; // Always mark this roll as exploding newExplodingRoll.exploding = true; - flagRoll(rollConf, newExplodingRoll); + flagRoll(rollConf, newExplodingRoll, modifiers.customDiceShapes); loggingEnabled && log(LT.LOG, `${getLoopCount()} Roll done ${JSON.stringify(newExplodingRoll)}`); diff --git a/src/artigen/dice/generateFormattedRoll.ts b/src/artigen/dice/generateFormattedRoll.ts index 0b901e4..cb64fe8 100644 --- a/src/artigen/dice/generateFormattedRoll.ts +++ b/src/artigen/dice/generateFormattedRoll.ts @@ -28,16 +28,7 @@ export const formatRoll = (executedRoll: ExecutedRoll, modifiers: RollModifiers) if (!e.dropped && !e.rerolled) { // If the roll was not dropped or rerolled, add it to the stepTotal and flag the critHit/critFail - switch (e.type) { - case 'ova': - case 'roll20': - case 'fate': - tempTotal += e.roll; - break; - case 'cwod': - tempTotal += e.success ? 1 : 0; - break; - } + tempTotal += e.roll; if (e.critHit) { tempCrit = true; } diff --git a/src/artigen/dice/getModifiers.ts b/src/artigen/dice/getModifiers.ts index 5249f5e..f6ee66c 100644 --- a/src/artigen/dice/getModifiers.ts +++ b/src/artigen/dice/getModifiers.ts @@ -1,8 +1,10 @@ import { log, LogTypes as LT } from '@Log4Deno'; -import { RollModifiers } from 'artigen/dice/dice.d.ts'; import config from '~config'; +import { RollModifiers } from 'artigen/dice/dice.d.ts'; + +export const reservedCharacters = ['d', '%', '^', '*', '(', ')', '{', '}', '/', '+', '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9']; export const Modifiers = Object.freeze({ Count: '-c', NoDetails: '-nd', @@ -21,6 +23,7 @@ export const Modifiers = Object.freeze({ RollDistribution: '-rd', NumberVariables: '-nv', VariablesNumber: '-vn', + CustomDiceShapes: '-cd', }); export const getModifiers = (args: string[]): [RollModifiers, string[]] => { @@ -41,6 +44,7 @@ export const getModifiers = (args: string[]): [RollModifiers, string[]] => { confirmCrit: false, rollDist: false, numberVariables: false, + customDiceShapes: new Map(), apiWarn: '', valid: true, error: new Error(), @@ -48,7 +52,7 @@ export const getModifiers = (args: string[]): [RollModifiers, string[]] => { // Check if any of the args are command flags and pull those out into the modifiers object for (let i = 0; i < args.length; i++) { - log(LT.LOG, `Checking ${args.join(' ')} for command modifiers ${i}`); + log(LT.LOG, `Checking ${args.join(' ')} for command modifiers ${i} | ${args[i]}`); let defaultCase = false; switch (args[i].toLowerCase()) { case Modifiers.Count: @@ -103,6 +107,7 @@ export const getModifiers = (args: string[]): [RollModifiers, string[]] => { // If -gm is on and none were found, throw an error modifiers.error.name = 'NoGMsFound'; modifiers.error.message = 'Must specify at least one GM by @mentioning them'; + modifiers.valid = false; return [modifiers, args]; } break; @@ -114,6 +119,7 @@ export const getModifiers = (args: string[]): [RollModifiers, string[]] => { // If -o is on and asc or desc was not specified, error out modifiers.error.name = 'NoOrderFound'; modifiers.error.message = 'Must specify `a` or `d` to order the rolls ascending or descending'; + modifiers.valid = false; return [modifiers, args]; } @@ -129,6 +135,65 @@ export const getModifiers = (args: string[]): [RollModifiers, string[]] => { case Modifiers.VariablesNumber: modifiers.numberVariables = true; break; + case Modifiers.CustomDiceShapes: { + // Shift the -cd out of the array so the dice shapes are next + args.splice(i, 1); + + const cdSyntaxMessage = + 'Must specify at least one custom dice shape using the `name:[side1,side2,...,sideN]` syntax. If multiple custom dice shapes are needed, use a `;` to separate the list.'; + + const shapes = (args[i] ?? '').split(';').filter((x) => x); + if (!shapes.length) { + modifiers.error.name = 'NoShapesSpecified'; + modifiers.error.message = `No custom shaped dice found.\n\n${cdSyntaxMessage}`; + modifiers.valid = false; + return [modifiers, args]; + } + + for (const shape of shapes) { + const [name, rawSides] = shape.split(':').filter((x) => x); + if (!name || !rawSides || !rawSides.includes('[') || !rawSides.includes(']')) { + modifiers.error.name = 'InvalidShapeSpecified'; + modifiers.error.message = `One of the custom dice is not formatted correctly.\n\n${cdSyntaxMessage}`; + modifiers.valid = false; + return [modifiers, args]; + } + + if (modifiers.customDiceShapes.has(name)) { + modifiers.error.name = 'ShapeAlreadySpecified'; + modifiers.error.message = `Shape \`${name}\` is already specified, please give it a different name.\n\n${cdSyntaxMessage}`; + modifiers.valid = false; + return [modifiers, args]; + } + + if (reservedCharacters.some((char) => name.includes(char))) { + modifiers.error.name = 'InvalidCharacterInCDName'; + modifiers.error.message = `Custom dice names cannot include any of the following characters:\n${JSON.stringify( + reservedCharacters + )}\n\n${cdSyntaxMessage}`; + modifiers.valid = false; + return [modifiers, args]; + } + + const sides = rawSides + .replaceAll('[', '') + .replaceAll(']', '') + .split(',') + .filter((x) => x) + .map((side) => parseInt(side)); + if (!sides.length) { + modifiers.error.name = 'NoCustomSidesSpecified'; + modifiers.error.message = `No sides found for \`${name}\`.\n\n${cdSyntaxMessage}`; + modifiers.valid = false; + return [modifiers, args]; + } + + modifiers.customDiceShapes.set(name, sides); + } + + log(LT.LOG, `Generated Custom Dice: ${JSON.stringify(modifiers.customDiceShapes.entries().toArray())}`); + break; + } default: // Default case should not mess with the array defaultCase = true; diff --git a/src/artigen/dice/getRollConf.ts b/src/artigen/dice/getRollConf.ts index 1177ca9..f78dbae 100644 --- a/src/artigen/dice/getRollConf.ts +++ b/src/artigen/dice/getRollConf.ts @@ -1,6 +1,6 @@ import { log, LogTypes as LT } from '@Log4Deno'; -import { RollConf } from 'artigen/dice/dice.d.ts'; +import { CustomDiceShapes, RollConf } from 'artigen/dice/dice.d.ts'; import { DiceOptions, NumberlessDiceOptions } from 'artigen/dice/rollOptions.ts'; @@ -14,13 +14,14 @@ const throwDoubleSepError = (sep: string): void => { }; // Converts a rollStr into a machine readable rollConf -export const getRollConf = (rollStr: string): RollConf => { +export const getRollConf = (rollStr: string, customTypes: CustomDiceShapes = new Map()): 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: '', + customType: null, dieCount: 0, dieSize: 0, dPercent: { @@ -92,10 +93,12 @@ export const getRollConf = (rollStr: string): RollConf => { 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, ''); + const numberlessRawDC = rawDC.replace(/\d/g, ''); + if (!tempDC && !numberlessRawDC) { + throw new Error(`CannotParseDieCount_${rawDC}`); + } // Rejoin all remaining parts let remains = dPts.join('d'); @@ -160,6 +163,12 @@ export const getRollConf = (rollStr: string): RollConf => { // remove F from the remains remains = remains.slice(1); + } else if (customTypes.has(numberlessRawDC)) { + // custom dice setup + rollConf.type = 'custom'; + rollConf.customType = numberlessRawDC; + rollConf.dieCount = isNaN(parseInt(tempDC ?? '1')) ? 1 : parseInt(tempDC ?? '1'); + rollConf.dieSize = Math.max(...(customTypes.get(numberlessRawDC) ?? [])); } else { // roll20 dice setup rollConf.type = 'roll20'; diff --git a/src/artigen/dice/randomRoll.ts b/src/artigen/dice/randomRoll.ts index 605db0d..d0e6872 100644 --- a/src/artigen/dice/randomRoll.ts +++ b/src/artigen/dice/randomRoll.ts @@ -1,8 +1,10 @@ -import { DPercentConf, RollModifiers } from 'artigen/dice/dice.d.ts'; +import { DPercentConf, RollConf, RollModifiers } from 'artigen/dice/dice.d.ts'; -// genRoll(size) returns number -// genRoll rolls a die of size size and returns the result -export const genRoll = (size: number, modifiers: RollModifiers, dPercent: DPercentConf): number => { +import { basicReducer } from 'artigen/utils/reducers.ts'; + +// genBasicRoll(size, modifiers, dPercent) returns number +// genBasicRoll rolls a die of size size and returns the result +const genBasicRoll = (size: number, modifiers: RollModifiers, dPercent: DPercentConf): number => { let result; if (modifiers.maxRoll) { result = size; @@ -15,13 +17,25 @@ export const genRoll = (size: number, modifiers: RollModifiers, dPercent: DPerce return dPercent.on ? (result - 1) * dPercent.sizeAdjustment : result; }; -// genFateRoll returns -1|0|1 -// genFateRoll turns a d6 into a fate die, with sides: -1, -1, 0, 0, 1, 1 -export const genFateRoll = (modifiers: RollModifiers): number => { +const getRollFromArray = (sides: number[], modifiers: RollModifiers): number => { if (modifiers.nominalRoll) { - return 0; - } else { - const sides = [-1, -1, 0, 0, 1, 1]; - return sides[genRoll(6, modifiers, { on: false }) - 1]; + return sides.reduce(basicReducer, 0) / sides.length; + } else if (modifiers.maxRoll) { + return Math.max(...sides); + } else if (modifiers.minRoll) { + return Math.min(...sides); + } + + return sides[genBasicRoll(sides.length, modifiers, { on: false }) - 1]; +}; + +export const generateRoll = (rollConf: RollConf, modifiers: RollModifiers): number => { + switch (rollConf.type) { + case 'fate': + return getRollFromArray([-1, -1, 0, 0, 1, 1], modifiers); + case 'custom': + return getRollFromArray(modifiers.customDiceShapes.get(rollConf.customType ?? '') ?? [], modifiers); + default: + return genBasicRoll(rollConf.dieSize, modifiers, rollConf.dPercent); } }; diff --git a/src/artigen/math/mathTokenizer.ts b/src/artigen/math/mathTokenizer.ts index cfa2a18..8598fe0 100644 --- a/src/artigen/math/mathTokenizer.ts +++ b/src/artigen/math/mathTokenizer.ts @@ -310,7 +310,9 @@ export const tokenizeMath = ( } // Now that mathConf is parsed, send it into the solver + loggingEnabled && log(LT.LOG, `Sending mathConf to solver ${JSON.stringify(mathConf)}`); const tempSolved = mathSolver(mathConf); + loggingEnabled && log(LT.LOG, `SolvedStep back from mathSolver ${JSON.stringify(tempSolved)}`); // Push all of this step's solved data into the temp array return [ diff --git a/src/artigen/utils/diceFlagger.ts b/src/artigen/utils/diceFlagger.ts index b20fb6e..e3daca1 100644 --- a/src/artigen/utils/diceFlagger.ts +++ b/src/artigen/utils/diceFlagger.ts @@ -1,6 +1,6 @@ -import { RollConf, RollSet } from 'artigen/dice/dice.d.ts'; +import { CustomDiceShapes, RollConf, RollSet } from 'artigen/dice/dice.d.ts'; -export const flagRoll = (rollConf: RollConf, rollSet: RollSet) => { +export const flagRoll = (rollConf: RollConf, rollSet: RollSet, customDiceShapes: CustomDiceShapes) => { // 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(rollSet.roll)) { rollSet.critHit = true; @@ -14,6 +14,8 @@ export const flagRoll = (rollConf: RollConf, rollSet: RollSet) => { } else if (!rollConf.critFail.on) { if (rollConf.type === 'fate') { rollSet.critFail = rollSet.roll === -1; + } else if (rollConf.type === 'custom') { + rollSet.critFail = rollSet.roll === Math.min(...(customDiceShapes.get(rollConf.customType ?? '') ?? [])); } else { rollSet.critFail = rollSet.roll === (rollConf.dPercent.on ? 0 : 1); } diff --git a/src/artigen/utils/embeds.ts b/src/artigen/utils/embeds.ts index cd09ad1..5bc9373 100644 --- a/src/artigen/utils/embeds.ts +++ b/src/artigen/utils/embeds.ts @@ -103,7 +103,8 @@ const getDistName = (key: string) => { return `CWOD d${size}`; case 'ova': return `OVA d${size}`; - case 'roll20': + case 'custom': + return `Custom d${size}`; default: return `d${size}`; } @@ -113,19 +114,21 @@ export const generateRollDistsEmbed = (rollDists: RollDistributionMap): ArtigenE const fields = rollDists .entries() .toArray() - .slice(0, 25) .map(([key, distArr]) => { const total = distArr.reduce(basicReducer, 0); return { name: `${getDistName(key)} (Total rolls: ${total}):`, - value: distArr.map((cnt, dieIdx) => `${key.startsWith('fate') ? dieIdx - 1 : dieIdx + 1}: ${cnt} (${((cnt / total) * 100).toFixed(1)}%)`).join('\n'), + value: distArr + .map((cnt, dieIdx) => key.startsWith('custom') && cnt === 0 ? '' : `${key.startsWith('fate') ? dieIdx - 1 : dieIdx + 1}: ${cnt} (${((cnt / total) * 100).toFixed(1)}%)`) + .filter((x) => x) + .join('\n'), inline: true, }; }); const rollDistTitle = 'Roll Distributions:'; const totalSize = fields.map((field) => field.name.length + field.value.length).reduce(basicReducer, 0); - if (totalSize > 4000 || fields.some((field) => field.name.length > 256 || field.value.length > 1024)) { + if (totalSize > 4000 || fields.length > 25 || fields.some((field) => field.name.length > 256 || field.value.length > 1024)) { const rollDistBlob = new Blob([fields.map((field) => `# ${field.name}\n${field.value}`).join('\n\n') as BlobPart], { type: 'text' }); if (rollDistBlob.size > config.maxFileSize) { const rollDistErrDesc = diff --git a/src/endpoints/gets/apiRoll.ts b/src/endpoints/gets/apiRoll.ts index 30f5eb2..135b78f 100644 --- a/src/endpoints/gets/apiRoll.ts +++ b/src/endpoints/gets/apiRoll.ts @@ -4,6 +4,7 @@ import { log, LogTypes as LT } from '@Log4Deno'; import config from '~config'; import { RollModifiers } from 'artigen/dice/dice.d.ts'; +import { reservedCharacters } from 'artigen/dice/getModifiers.ts'; import { sendRollRequest } from 'artigen/managers/queueManager.ts'; @@ -100,11 +101,48 @@ export const apiRoll = async (query: Map, apiUserid: bigint): Pr confirmCrit: query.has('cc'), rollDist: query.has('rd'), numberVariables: query.has('nv') || query.has('vn'), + customDiceShapes: new Map(), apiWarn: hideWarn ? '' : apiWarning, valid: true, error: new Error(), }; + // Handle adding all sides into the cds array + if (query.has('cd')) { + const shapes = (query.get('cd') ?? '').split(';').filter((x) => x); + if (!shapes.length) { + return stdResp.BadRequest('cd specified without any shapes provided'); + } + for (const shape of shapes) { + const [name, rawSides] = shape.split(':').filter((x) => x); + if (!name || !rawSides) { + return stdResp.BadRequest( + 'cd specified with invalid pattern. Must be in format of `name:[side1,side2,...,sideN]`. If multiple custom dice shapes are needed, use a `;` to separate the list' + ); + } + + if (modifiers.customDiceShapes.has(name)) { + return stdResp.BadRequest('cd specified, cannot repeat names'); + } + + if (reservedCharacters.some((char) => name.includes(char))) { + return stdResp.BadRequest('cd specified, die name includes invalid characters. Reserved Character List: ' + JSON.stringify(reservedCharacters)); + } + + const sides = rawSides + .replaceAll('[', '') + .replaceAll(']', '') + .split(',') + .filter((x) => x) + .map((side) => parseInt(side)); + if (!sides.length) { + return stdResp.BadRequest('cd specified without any sides provided'); + } + + modifiers.customDiceShapes.set(name, sides); + } + } + // maxRoll, minRoll, and nominalRoll cannot be on at same time, throw an error if ([modifiers.maxRoll, modifiers.minRoll, modifiers.nominalRoll, modifiers.simulatedNominal].filter((b) => b).length > 1) { return stdResp.BadRequest('Can only use one of the following at a time:\n`maximize`, `minimize`, `nominal`, `simulatedNominal`'); @@ -139,7 +177,7 @@ export const apiRoll = async (query: Map, apiUserid: bigint): Pr } else { // Alert API user that they messed up return stdResp.Forbidden( - `Verify you are a member of the guild you are sending this roll to. If you are, the ${config.name} may not have that registered, please send a message in the guild so ${config.name} can register this. This registration is temporary, so if you see this error again, just poke your server again.`, + `Verify you are a member of the guild you are sending this roll to. If you are, the ${config.name} may not have that registered, please send a message in the guild so ${config.name} can register this. This registration is temporary, so if you see this error again, just poke your server again.` ); } } else {