diff --git a/src/commands/apiCmd.ts b/src/commands/apiCmd.ts index 6bcd972..78d4cdb 100644 --- a/src/commands/apiCmd.ts +++ b/src/commands/apiCmd.ts @@ -31,7 +31,7 @@ export const api = async (message: DiscordenoMessage, args: string[]) => { if (await hasGuildPermissions(message.authorId, message.guildId, ['ADMINISTRATOR'])) { // [[api help // Shows API help details - if (apiArg === 'help') { + if (apiArg === 'help' || apiArg === 'h') { apiCommands.help(message); } // [[api allow/block // Lets a guild admin allow or ban API rolls from happening in said guild diff --git a/src/commands/roll.ts b/src/commands/roll.ts index a999133..c35f223 100644 --- a/src/commands/roll.ts +++ b/src/commands/roll.ts @@ -10,7 +10,8 @@ import { sendDirectMessage, } from '../../deps.ts'; import solver from '../solver/_index.ts'; -import { constantCmds, generateDMFailed } from '../constantCmds.ts'; +import { SolvedRoll } from '../solver/solver.d.ts'; +import { constantCmds, generateCountDetailsEmbed, generateDMFailed, generateRollEmbed } from '../constantCmds.ts'; import rollFuncs from './roll/_index.ts'; export const roll = async (message: DiscordenoMessage, args: string[], command: string) => { @@ -31,11 +32,15 @@ export const roll = async (message: DiscordenoMessage, args: string[], command: try { const originalCommand = `${config.prefix}${command} ${args.join(' ')}`; - const m = await message.send(constantCmds.rolling); + const m = await message.reply(constantCmds.rolling); // Get modifiers from command const modifiers = rollFuncs.getModifiers(m, args, command, originalCommand); + // gmModifiers used to create gmEmbed (basically just turn off the gmRoll) + const gmModifiers = JSON.parse(JSON.stringify(modifiers)); + gmModifiers.gmRoll = false; + // Return early if the modifiers were invalid if (!modifiers.valid) { return; @@ -43,111 +48,58 @@ export const roll = async (message: DiscordenoMessage, args: string[], command: // Rejoin all of the args and send it into the solver, if solver returns a falsy item, an error object will be substituded in const rollCmd = `${command} ${args.join(' ')}`; - const returnmsg = solver.parseRoll(rollCmd, modifiers) || { error: true, errorCode: 'EmptyMessage', errorMsg: 'Error: Empty message', line1: '', line2: '', line3: '' }; + const returnmsg = solver.parseRoll(rollCmd, modifiers) || { error: true, errorCode: 'EmptyMessage', errorMsg: 'Error: Empty message' }; - let returnText = ''; + const pubEmbedDetails = await generateRollEmbed(message.authorId, returnmsg, modifiers); + const gmEmbedDetails = await generateRollEmbed(message.authorId, returnmsg, gmModifiers); + const countEmbed = generateCountDetailsEmbed(returnmsg.counts); // If there was an error, report it to the user in hopes that they can determine what they did wrong if (returnmsg.error) { - returnText = returnmsg.errorMsg; - m.edit(returnText); + m.edit({embeds: [pubEmbedDetails.embed]}); if (DEVMODE && config.logRolls) { - // If enabled, log rolls so we can verify the bots math + // If enabled, log rolls so we can see what went wrong dbClient.execute(queries.insertRollLogCmd(0, 1), [originalCommand, returnmsg.errorCode, m.id]).catch((e) => { log(LT.ERROR, `Failed to insert into DB: ${JSON.stringify(e)}`); }); } - return; } else { - // Else format the output using details from the solver - returnText = `<@${message.authorId}>${returnmsg.line1}\n${returnmsg.line2}`; + // Determine if we are to send a GM roll or a normal roll + if (modifiers.gmRoll) { + // Send the public embed to correct channel + m.edit({embeds: [pubEmbedDetails.embed]}); - if (!modifiers.superNoDetails) { - if (modifiers.noDetails) { - returnText += '\nDetails suppressed by -nd flag.'; - } else { - returnText += `\nDetails:\n${modifiers.spoiler}${returnmsg.line3}${modifiers.spoiler}`; - } - } - } - - // If the roll was a GM roll, send DMs to all the GMs - if (modifiers.gmRoll) { - // Make a new return line to be sent to the roller - const normalText = `<@${message.authorId}>${returnmsg.line1}\nResults have been messaged to the following GMs: ${modifiers.gms.join(' ')}`; - - // And message the full details to each of the GMs, alerting roller of every GM that could not be messaged - modifiers.gms.forEach(async (e) => { - log(LT.LOG, `Messaging GM ${e}`); - // If its too big, collapse it into a .txt file and send that instead. - const b = await new Blob([returnText as BlobPart], { 'type': 'text' }); - - if (b.size > 8388290) { - // Update return text - // todo: embedify - returnText = - `<@${message.authorId}>${returnmsg.line1}\n${returnmsg.line2}\nFull details could not be attached to this messaged as a \`.txt\` file as the file would be too large for Discord to handle. If you would like to see the details of rolls, please send the rolls in multiple messages instead of bundled into one.`; - - // Attempt to DM the GMs and send a warning if it could not DM a GM - await sendDirectMessage(BigInt(e.substring(2, e.length - 1)), returnText).catch(() => { - message.send(generateDMFailed(e)); + // And message the full details to each of the GMs, alerting roller of every GM that could not be messaged + modifiers.gms.forEach(async (gm) => { + log(LT.LOG, `Messaging GM ${gm}`); + // Attempt to DM the GM and send a warning if it could not DM a GM + await sendDirectMessage(BigInt(gm.substring(2, gm.length - 1)), { + embeds: modifiers.count ? [gmEmbedDetails.embed, countEmbed] : [gmEmbedDetails.embed], + }).then(async () => { + // Check if we need to attach a file and send it after the initial details sent + if (gmEmbedDetails.hasAttachment) { + await sendDirectMessage(BigInt(gm.substring(2, gm.length - 1)), { + file: gmEmbedDetails.attachment, + }).catch(() => { + message.reply(generateDMFailed(gm)); + }); + } + }).catch(() => { + message.reply(generateDMFailed(gm)); }); - } else { - // Update return - // todo: embedify - returnText = `<@${message.authorId}>${returnmsg.line1}\n${returnmsg.line2}\nFull details have been attached to this messaged as a \`.txt\` file for verification purposes.`; - - // Attempt to DM the GMs and send a warning if it could not DM a GM - await sendDirectMessage(BigInt(e.substring(2, e.length - 1)), { 'content': returnText, 'file': { 'blob': b, 'name': 'rollDetails.txt' } }).catch(() => { - message.send(generateDMFailed(e)); - }); - } - }); - - // Finally send the text - m.edit(normalText); - - if (DEVMODE && config.logRolls) { - // If enabled, log rolls so we can verify the bots math - dbClient.execute(queries.insertRollLogCmd(0, 0), [originalCommand, returnText, m.id]).catch((e) => { - log(LT.ERROR, `Failed to insert into DB: ${JSON.stringify(e)}`); }); - } - } else { - // When not a GM roll, make sure the message is not too big - if (returnText.length > 2000) { - // If its too big, collapse it into a .txt file and send that instead. - const b = await new Blob([returnText as BlobPart], { 'type': 'text' }); - - if (b.size > 8388290) { - // Update return text - returnText = - `<@${message.authorId}>${returnmsg.line1}\n${returnmsg.line2}\nDetails have been ommitted from this message for being over 2000 characters. Full details could not be attached to this messaged as a \`.txt\` file as the file would be too large for Discord to handle. If you would like to see the details of rolls, please send the rolls in multiple messages instead of bundled into one.`; - - // Send the results - m.edit(returnText); - } else { - // Update return text - returnText = - `<@${message.authorId}>${returnmsg.line1}\n${returnmsg.line2}\nDetails have been ommitted from this message for being over 2000 characters. Full details have been attached to this messaged as a \`.txt\` file for verification purposes.`; - - // Remove the original message to send new one with attachment - m.delete(); - - // todo: embedify - await message.send({ 'content': returnText, 'file': { 'blob': b, 'name': 'rollDetails.txt' } }); - } } else { - // Finally send the text - m.edit(returnText); - } - - if (DEVMODE && config.logRolls) { - // If enabled, log rolls so we can verify the bots math - dbClient.execute(queries.insertRollLogCmd(0, 0), [originalCommand, returnText, m.id]).catch((e) => { - log(LT.ERROR, `Failed to insert into DB: ${JSON.stringify(e)}`); + // Not a gm roll, so just send normal embed to correct channel + await m.edit({ + embeds: modifiers.count ? [pubEmbedDetails.embed, countEmbed] : [pubEmbedDetails.embed], }); + if (pubEmbedDetails.hasAttachment) { + // Attachment requires you to send a new message + message.send({ + file: pubEmbedDetails.attachment, + }); + } } } } catch (e) { diff --git a/src/constantCmds.ts b/src/constantCmds.ts index 342c48e..c0d5764 100644 --- a/src/constantCmds.ts +++ b/src/constantCmds.ts @@ -1,5 +1,7 @@ import config from '../config.ts'; -import { CountDetails } from './solver/solver.d.ts'; +import { DiscordenoMessage } from '../deps.ts'; +import { CountDetails, SolvedRoll } from './solver/solver.d.ts'; +import { RollModifiers } from './mod.d.ts'; const failColor = 0xe71212; const warnColor = 0xe38f28; @@ -625,41 +627,144 @@ export const generateRollError = (errorType: string, errorMsg: string) => ({ }], }); -export const generateCountDetails = (counts: CountDetails) => ({ - embeds: [{ - color: infoColor1, - title: 'Roll Count Details:', - fields: [ - { - name: 'Total Rolls:', - details: `${counts.total}`, - inline: true, - }, - { - name: 'Successful Rolls:', - details: `${counts.successful}`, - inline: true, - }, - { - name: 'Failed Rolls:', - details: `${counts.failed}`, - inline: true, - }, - { - name: 'Rerolled Dice:', - details: `${counts.rerolled}`, - inline: true, - }, - { - name: 'Dropped Dice:', - details: `${counts.dropped}`, - inline: true, - }, - { - name: 'Exploded Dice:', - details: `${counts.exploded}`, - inline: true, - }, - ], - }], +export const generateCountDetailsEmbed = (counts: CountDetails) => ({ + color: infoColor1, + title: 'Roll Count Details:', + fields: [ + { + name: 'Total Rolls:', + value: `${counts.total}`, + inline: true, + }, + { + name: 'Successful Rolls:', + value: `${counts.successful}`, + inline: true, + }, + { + name: 'Failed Rolls:', + value: `${counts.failed}`, + inline: true, + }, + { + name: 'Rerolled Dice:', + value: `${counts.rerolled}`, + inline: true, + }, + { + name: 'Dropped Dice:', + value: `${counts.dropped}`, + inline: true, + }, + { + name: 'Exploded Dice:', + value: `${counts.exploded}`, + inline: true, + }, + ], }); + +export const generateRollEmbed = async (authorId: bigint, returnDetails: SolvedRoll, modifiers: RollModifiers) => { + if (returnDetails.error) { + // Roll had an error, send error embed + return { + embed: { + color: failColor, + title: 'Roll failed:', + description: `${returnDetails.errorMsg}`, + }, + hasAttachment: false, + attachment: { + 'blob': await new Blob(['' as BlobPart], { 'type': 'text'}), + 'name': 'rollDetails.txt', + }, + }; + } else { + if (modifiers.gmRoll) { + // Roll is a GM Roll, send this in the pub channel (this funciton will be ran again to get details for the GMs) + return { + embed: { + color: infoColor2, + description: `<@${authorId}>${returnDetails.line1} + +Results have been messaged to the following GMs: ${modifiers.gms.join(' ')}`, + }, + hasAttachment: false, + attachment: { + 'blob': await new Blob(['' as BlobPart], { 'type': 'text'}), + 'name': 'rollDetails.txt', + }, + }; + } else { + // Roll is normal, make normal embed + const line2Details = returnDetails.line2.split(': '); + let details = ''; + + if (!modifiers.superNoDetails) { + if (modifiers.noDetails) { + details = `**Details:** +Suppressed by -nd flag`; + } else { + details = `**Details:** +${modifiers.spoiler}${returnDetails.line3}${modifiers.spoiler}`; + } + } + + const baseDesc = `<@${authorId}>${returnDetails.line1} +**${line2Details.shift()}:** +${line2Details.join(': ')}`; + + if (baseDesc.length + details.length < 4090) { + return { + embed: { + color: infoColor2, + description: `${baseDesc} + +${details}`, + }, + hasAttachment: false, + attachment: { + 'blob': await new Blob(['' as BlobPart], { 'type': 'text'}), + 'name': 'rollDetails.txt', + }, + }; + } else { + // If its too big, collapse it into a .txt file and send that instead. + const b = await new Blob([`${baseDesc}\n\n${details}` as BlobPart], { 'type': 'text' }); + details = 'Details have been ommitted from this message for being over 2000 characters.'; + if (b.size > 8388290) { + details += + 'Full details could not be attached to this messaged as a \`.txt\` file as the file would be too large for Discord to handle. If you would like to see the details of rolls, please send the rolls in multiple messages instead of bundled into one.'; + return { + embed: { + color: infoColor2, + description: `${baseDesc} + + ${details}`, + }, + hasAttachment: false, + attachment: { + 'blob': await new Blob(['' as BlobPart], { 'type': 'text'}), + 'name': 'rollDetails.txt', + }, + }; + } else { + details += 'Full details have been attached to this messaged as a \`.txt\` file for verification purposes.'; + return { + embed: { + color: infoColor2, + description: `${baseDesc} + +${details}`, + }, + hasAttachment: true, + attachment: { + 'blob': b, + 'name': 'rollDetails.txt', + }, + }; + } + } + } + } +}; diff --git a/src/solver/counter.ts b/src/solver/counter.ts new file mode 100644 index 0000000..3ad92d6 --- /dev/null +++ b/src/solver/counter.ts @@ -0,0 +1,23 @@ +import { CountDetails, RollSet } from './solver.d.ts'; + +export const rollCounter = (rollSet: RollSet[]): CountDetails => { + const countDetails = { + total: 0, + successful: 0, + failed: 0, + rerolled: 0, + dropped: 0, + exploded: 0, + }; + + rollSet.forEach((roll) => { + countDetails.total++; + if (roll.critHit) countDetails.successful++; + if (roll.critFail) countDetails.failed++; + if (roll.rerolled) countDetails.rerolled++; + if (roll.dropped) countDetails.dropped++; + if (roll.exploding) countDetails.exploded++; + }); + + return countDetails; +}; diff --git a/src/solver/parser.ts b/src/solver/parser.ts index 3c29ec6..bf0e670 100644 --- a/src/solver/parser.ts +++ b/src/solver/parser.ts @@ -7,7 +7,7 @@ import { import config from '../../config.ts'; import { RollModifiers } from '../mod.d.ts'; -import { ReturnData, SolvedRoll, SolvedStep } from './solver.d.ts'; +import { CountDetails, ReturnData, SolvedRoll, SolvedStep } from './solver.d.ts'; import { compareTotalRolls, escapeCharacters } from './rollUtils.ts'; import { formatRoll } from './rollFormatter.ts'; import { fullSolver } from './solver.ts'; @@ -15,13 +15,8 @@ import { fullSolver } from './solver.ts'; // parseRoll(fullCmd, modifiers) // parseRoll handles converting fullCmd into a computer readable format for processing, and finally executes the solving export const parseRoll = (fullCmd: string, modifiers: RollModifiers): SolvedRoll => { - const returnmsg = { + const returnmsg = { error: false, - errorMsg: '', - errorCode: '', - line1: '', - line2: '', - line3: '', }; // Whole function lives in a try-catch to allow safe throwing of errors on purpose @@ -30,6 +25,7 @@ export const parseRoll = (fullCmd: string, modifiers: RollModifiers): SolvedRoll const sepRolls = fullCmd.split(config.prefix); const tempReturnData: ReturnData[] = []; + const tempCountDetails: CountDetails[] = []; // Loop thru all roll/math ops for (const sepRoll of sepRolls) { @@ -68,7 +64,9 @@ export const parseRoll = (fullCmd: string, modifiers: RollModifiers): SolvedRoll mathConf[i] = parseFloat(mathConf[i].toString()); } else if (/([0123456789])/g.test(mathConf[i].toString())) { // If there is a number somewhere in mathconf[i] but there are also other characters preventing it from parsing correctly as a number, it should be a dice roll, parse it as such (if it for some reason is not a dice roll, formatRoll/roll will handle it) - mathConf[i] = formatRoll(mathConf[i].toString(), modifiers.maxRoll, modifiers.nominalRoll); + const formattedRoll = formatRoll(mathConf[i].toString(), modifiers.maxRoll, modifiers.nominalRoll); + mathConf[i] = formattedRoll.solvedStep; + tempCountDetails.push(formattedRoll.countDetails); } else if (mathConf[i].toString().toLowerCase() === 'e') { // If the operand is the constant e, create a SolvedStep for it mathConf[i] = { @@ -77,7 +75,7 @@ export const parseRoll = (fullCmd: string, modifiers: RollModifiers): SolvedRoll containsCrit: false, containsFail: false, }; - } else if (mathConf[i].toString().toLowerCase() === 'pi' || mathConf[i].toString().toLowerCase() == '𝜋') { + } else if (mathConf[i].toString().toLowerCase() === 'pi' || mathConf[i].toString().toLowerCase() === '𝜋') { // If the operand is the constant pi, create a SolvedStep for it mathConf[i] = { total: Math.PI, @@ -190,6 +188,16 @@ export const parseRoll = (fullCmd: string, modifiers: RollModifiers): SolvedRoll returnmsg.line1 = line1; returnmsg.line2 = line2; returnmsg.line3 = line3; + + // Reduce counts to a single object + returnmsg.counts = tempCountDetails.reduce((acc, cnt) => ({ + total: acc.total + cnt.total, + successful: acc.successful + cnt.successful, + failed: acc.failed + cnt.failed, + rerolled: acc.rerolled + cnt.rerolled, + dropped: acc.dropped + cnt.dropped, + exploded: acc.exploded + cnt.exploded, + })); } catch (solverError) { // Welp, the unthinkable happened, we hit an error diff --git a/src/solver/rollFormatter.ts b/src/solver/rollFormatter.ts index dc2d212..245dd93 100644 --- a/src/solver/rollFormatter.ts +++ b/src/solver/rollFormatter.ts @@ -5,11 +5,12 @@ import { } from '../../deps.ts'; import { roll } from './roller.ts'; -import { SolvedStep } from './solver.d.ts'; +import { rollCounter } from './counter.ts'; +import { RollFormat } from './solver.d.ts'; // formatRoll(rollConf, maximiseRoll, nominalRoll) returns one SolvedStep // formatRoll handles creating and formatting the completed rolls into the SolvedStep format -export const formatRoll = (rollConf: string, maximiseRoll: boolean, nominalRoll: boolean): SolvedStep => { +export const formatRoll = (rollConf: string, maximiseRoll: boolean, nominalRoll: boolean): RollFormat => { let tempTotal = 0; let tempDetails = '['; let tempCrit = false; @@ -59,9 +60,12 @@ export const formatRoll = (rollConf: string, maximiseRoll: boolean, nominalRoll: tempDetails += ']'; return { - total: tempTotal, - details: tempDetails, - containsCrit: tempCrit, - containsFail: tempFail, + solvedStep: { + total: tempTotal, + details: tempDetails, + containsCrit: tempCrit, + containsFail: tempFail, + }, + countDetails: rollCounter(tempRollSet), }; }; diff --git a/src/solver/solver.d.ts b/src/solver/solver.d.ts index 3c96f5e..8862acf 100644 --- a/src/solver/solver.d.ts +++ b/src/solver/solver.d.ts @@ -29,16 +29,6 @@ export type ReturnData = { initConfig: string; }; -// SolvedRoll is the complete solved and formatted roll, or the error said roll created -export type SolvedRoll = { - error: boolean; - errorMsg: string; - errorCode: string; - line1: string; - line2: string; - line3: string; -}; - // CountDetails is the object holding the count data for creating the Count Embed export type CountDetails = { total: number; @@ -48,3 +38,20 @@ export type CountDetails = { dropped: number; exploded: number; }; + +// RollFormat is the return structure for the rollFormatter +export type RollFormat = { + solvedStep: SolvedStep; + countDetails: CountDetails; +}; + +// SolvedRoll is the complete solved and formatted roll, or the error said roll created +export type SolvedRoll = { + error: boolean; + errorMsg: string; + errorCode: string; + line1: string; + line2: string; + line3: string; + counts: CountDetails; +};