Auto stash before rebase of "refs/heads/master"
This commit is contained in:
		
							parent
							
								
									920c8824fe
								
							
						
					
					
						commit
						96a4a88c2f
					
				| 
						 | 
					@ -2,3 +2,5 @@
 | 
				
			||||||
A dice roller Discord bot using roll20 format, also functioning as a basic calculator.
 | 
					A dice roller Discord bot using roll20 format, also functioning as a basic calculator.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
https://discord.com/api/oauth2/authorize?client_id=789045930011656223&permissions=2048&scope=bot
 | 
					https://discord.com/api/oauth2/authorize?client_id=789045930011656223&permissions=2048&scope=bot
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					https://discord.com/api/oauth2/authorize?client_id=789045930011656223&permissions=10240&scope=bot
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5,6 +5,8 @@ export const config = {
 | 
				
			||||||
	"prefix": "[[",
 | 
						"prefix": "[[",
 | 
				
			||||||
	"postfix": "]]",
 | 
						"postfix": "]]",
 | 
				
			||||||
	"logChannel": "the_log_channel",
 | 
						"logChannel": "the_log_channel",
 | 
				
			||||||
 | 
						"reportChannel": "the_report_channel",
 | 
				
			||||||
 | 
						"devServer": "the_dev_server",
 | 
				
			||||||
	"help": [
 | 
						"help": [
 | 
				
			||||||
		"```fix",
 | 
							"```fix",
 | 
				
			||||||
		"The Artificer Help",
 | 
							"The Artificer Help",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										48
									
								
								mod.ts
								
								
								
								
							
							
						
						
									
										48
									
								
								mod.ts
								
								
								
								
							| 
						 | 
					@ -4,7 +4,10 @@
 | 
				
			||||||
 * December 21, 2020
 | 
					 * December 21, 2020
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// DEVMODE is to prevent users from accessing parts of the bot that are currently broken
 | 
				
			||||||
const DEVMODE = false;
 | 
					const DEVMODE = false;
 | 
				
			||||||
 | 
					// DEBUG is used to toggle the cmdPrompt
 | 
				
			||||||
 | 
					const DEBUG = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
	startBot, editBotsStatus,
 | 
						startBot, editBotsStatus,
 | 
				
			||||||
| 
						 | 
					@ -25,6 +28,7 @@ startBot({
 | 
				
			||||||
		ready: () => {
 | 
							ready: () => {
 | 
				
			||||||
			console.log("Logged in!");
 | 
								console.log("Logged in!");
 | 
				
			||||||
			editBotsStatus(StatusTypes.Online, `${config.prefix}help for commands`, ActivityType.Game);
 | 
								editBotsStatus(StatusTypes.Online, `${config.prefix}help for commands`, ActivityType.Game);
 | 
				
			||||||
 | 
								// setTimeout added to make sure the startup message does not error out
 | 
				
			||||||
			setTimeout(() => {
 | 
								setTimeout(() => {
 | 
				
			||||||
				sendMessage(config.logChannel, `${config.name} has started, running version ${config.version}.`).catch(() => {
 | 
									sendMessage(config.logChannel, `${config.name} has started, running version ${config.version}.`).catch(() => {
 | 
				
			||||||
					console.error("Failed to send message 00");
 | 
										console.error("Failed to send message 00");
 | 
				
			||||||
| 
						 | 
					@ -59,7 +63,6 @@ startBot({
 | 
				
			||||||
			// Its a ping test, what else do you want.
 | 
								// Its a ping test, what else do you want.
 | 
				
			||||||
			if (command === "ping") {
 | 
								if (command === "ping") {
 | 
				
			||||||
				// Calculates ping between sending a message and editing it, giving a nice round-trip latency.
 | 
									// Calculates ping between sending a message and editing it, giving a nice round-trip latency.
 | 
				
			||||||
				// The second ping is an average latency between the bot and the websocket server (one-way, not round-trip)
 | 
					 | 
				
			||||||
				try {
 | 
									try {
 | 
				
			||||||
					const m = await utils.sendIndirectMessage(message, "Ping?", sendMessage, sendDirectMessage);
 | 
										const m = await utils.sendIndirectMessage(message, "Ping?", sendMessage, sendDirectMessage);
 | 
				
			||||||
					m.edit(`Pong! Latency is ${m.timestamp - message.timestamp}ms.`);
 | 
										m.edit(`Pong! Latency is ${m.timestamp - message.timestamp}ms.`);
 | 
				
			||||||
| 
						 | 
					@ -76,7 +79,7 @@ startBot({
 | 
				
			||||||
				});
 | 
									});
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			// [[v or [[version
 | 
								// [[version or [[v
 | 
				
			||||||
			// Returns version of the bot
 | 
								// Returns version of the bot
 | 
				
			||||||
			else if (command === "version" || command === "v") {
 | 
								else if (command === "version" || command === "v") {
 | 
				
			||||||
				utils.sendIndirectMessage(message, `My current version is ${config.version}.`, sendMessage, sendDirectMessage).catch(err => {
 | 
									utils.sendIndirectMessage(message, `My current version is ${config.version}.`, sendMessage, sendDirectMessage).catch(err => {
 | 
				
			||||||
| 
						 | 
					@ -84,9 +87,9 @@ startBot({
 | 
				
			||||||
				});
 | 
									});
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			// [[popcat
 | 
								// [[popcat or [[pop or [[p
 | 
				
			||||||
			// popcat animated emoji
 | 
								// popcat animated emoji
 | 
				
			||||||
			else if (command === "popcat") {
 | 
								else if (command === "popcat" || command === "pop" || command === "p") {
 | 
				
			||||||
				utils.sendIndirectMessage(message, `<${config.emojis.popcat.animated ? "a" : ""}:${config.emojis.popcat.name}:${config.emojis.popcat.id}>`, sendMessage, sendDirectMessage).catch(err => {
 | 
									utils.sendIndirectMessage(message, `<${config.emojis.popcat.animated ? "a" : ""}:${config.emojis.popcat.name}:${config.emojis.popcat.id}>`, sendMessage, sendDirectMessage).catch(err => {
 | 
				
			||||||
					console.error("Failed to send message 40", message, err);
 | 
										console.error("Failed to send message 40", message, err);
 | 
				
			||||||
				});
 | 
									});
 | 
				
			||||||
| 
						 | 
					@ -98,7 +101,7 @@ startBot({
 | 
				
			||||||
			// [[report or [[r (command that failed)
 | 
								// [[report or [[r (command that failed)
 | 
				
			||||||
			// Manually report a failed roll
 | 
								// Manually report a failed roll
 | 
				
			||||||
			else if (command === "report" || command === "r") {
 | 
								else if (command === "report" || command === "r") {
 | 
				
			||||||
				sendMessage(config.logChannel, ("USER REPORT:\n" + args.join(" "))).catch(err => {
 | 
									sendMessage(config.reportChannel, ("USER REPORT:\n" + args.join(" "))).catch(err => {
 | 
				
			||||||
					console.error("Failed to send message 50", message, err);
 | 
										console.error("Failed to send message 50", message, err);
 | 
				
			||||||
				});
 | 
									});
 | 
				
			||||||
				utils.sendIndirectMessage(message, "Failed command has been reported to my developer.", sendMessage, sendDirectMessage).catch(err => {
 | 
									utils.sendIndirectMessage(message, "Failed command has been reported to my developer.", sendMessage, sendDirectMessage).catch(err => {
 | 
				
			||||||
| 
						 | 
					@ -117,13 +120,15 @@ startBot({
 | 
				
			||||||
			// [[
 | 
								// [[
 | 
				
			||||||
			// Dice rolling commence!
 | 
								// Dice rolling commence!
 | 
				
			||||||
			else {
 | 
								else {
 | 
				
			||||||
				if (DEVMODE && message.guildID !== "317852981733097473") {
 | 
									// If DEVMODE is on, only allow this command to be used in the devServer
 | 
				
			||||||
 | 
									if (DEVMODE && message.guildID !== config.devServer) {
 | 
				
			||||||
					utils.sendIndirectMessage(message, "Command is in development, please try again later.", sendMessage, sendDirectMessage).catch(err => {
 | 
										utils.sendIndirectMessage(message, "Command is in development, please try again later.", sendMessage, sendDirectMessage).catch(err => {
 | 
				
			||||||
						console.error("Failed to send message 70", message, err);
 | 
											console.error("Failed to send message 70", message, err);
 | 
				
			||||||
					});
 | 
										});
 | 
				
			||||||
					return;
 | 
										return;
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									// Rest of this command is in a try-catch to protect all sends/edits from erroring out
 | 
				
			||||||
				try {
 | 
									try {
 | 
				
			||||||
					const m = await utils.sendIndirectMessage(message, "Rolling...", sendMessage, sendDirectMessage);
 | 
										const m = await utils.sendIndirectMessage(message, "Rolling...", sendMessage, sendDirectMessage);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -135,8 +140,10 @@ startBot({
 | 
				
			||||||
						gmRoll: false,
 | 
											gmRoll: false,
 | 
				
			||||||
						gms: <string[]>[]
 | 
											gms: <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++) {
 | 
										for (let i = 0; i < args.length; i++) {
 | 
				
			||||||
						switch (args[i]) {
 | 
											switch (args[i].toLowerCase()) {
 | 
				
			||||||
							case "-nd":
 | 
												case "-nd":
 | 
				
			||||||
								modifiers.noDetails = true;
 | 
													modifiers.noDetails = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -163,11 +170,15 @@ startBot({
 | 
				
			||||||
								break;
 | 
													break;
 | 
				
			||||||
							case "-gm":
 | 
												case "-gm":
 | 
				
			||||||
								modifiers.gmRoll = true;
 | 
													modifiers.gmRoll = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
													// -gm is a little more complex, as we must get all of the GMs that need to be DMd
 | 
				
			||||||
								while (((i + 1) < args.length) && args[i + 1].startsWith("<@!")) {
 | 
													while (((i + 1) < args.length) && args[i + 1].startsWith("<@!")) {
 | 
				
			||||||
 | 
														// Keep looping thru the rest of the args until one does not start with the discord mention code
 | 
				
			||||||
									modifiers.gms.push(args[i + 1]);
 | 
														modifiers.gms.push(args[i + 1]);
 | 
				
			||||||
									args.splice((i + 1), 1);
 | 
														args.splice((i + 1), 1);
 | 
				
			||||||
								}
 | 
													}
 | 
				
			||||||
								if (modifiers.gms.length < 1) {
 | 
													if (modifiers.gms.length < 1) {
 | 
				
			||||||
 | 
														// If -gm is on and none were found, throw an error
 | 
				
			||||||
									m.edit("Error: Must specifiy at least one GM by mentioning them");
 | 
														m.edit("Error: Must specifiy at least one GM by mentioning them");
 | 
				
			||||||
									return;
 | 
														return;
 | 
				
			||||||
								}
 | 
													}
 | 
				
			||||||
| 
						 | 
					@ -180,14 +191,25 @@ startBot({
 | 
				
			||||||
						}
 | 
											}
 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
					const rollCmd = command + " " + args.join(" ");
 | 
										// maxRoll and nominalRoll cannot both be on, throw an error
 | 
				
			||||||
 | 
										if (modifiers.maxRoll && modifiers.nominalRoll) {
 | 
				
			||||||
 | 
											m.edit("Error: Cannot maximise and nominise the roll at the same time");
 | 
				
			||||||
 | 
											return;
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
										// 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, config.prefix, config.postfix, modifiers.maxRoll, modifiers.nominalRoll) || { error: true, errorMsg: "Error: Empty message", line1: "", line2: "", line3: "" };
 | 
										const returnmsg = solver.parseRoll(rollCmd, config.prefix, config.postfix, modifiers.maxRoll, modifiers.nominalRoll) || { error: true, errorMsg: "Error: Empty message", line1: "", line2: "", line3: "" };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
					let returnText = "";
 | 
										let returnText = "";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
										// If there was an error, report it to the user in hopes that they can determine what they did wrong
 | 
				
			||||||
					if (returnmsg.error) {
 | 
										if (returnmsg.error) {
 | 
				
			||||||
						returnText = returnmsg.errorMsg;
 | 
											returnText = returnmsg.errorMsg;
 | 
				
			||||||
 | 
											m.edit(returnText);
 | 
				
			||||||
 | 
											return;
 | 
				
			||||||
					} else {
 | 
										} else {
 | 
				
			||||||
 | 
											// Else format the output using details from the solver
 | 
				
			||||||
						returnText = "<@" + message.author.id + ">" + returnmsg.line1 + "\n" + returnmsg.line2;
 | 
											returnText = "<@" + message.author.id + ">" + returnmsg.line1 + "\n" + returnmsg.line2;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
						if (modifiers.noDetails) {
 | 
											if (modifiers.noDetails) {
 | 
				
			||||||
| 
						 | 
					@ -197,9 +219,12 @@ startBot({
 | 
				
			||||||
						}
 | 
											}
 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
										// If the roll was a GM roll, send DMs to all the GMs
 | 
				
			||||||
					if (modifiers.gmRoll) {
 | 
										if (modifiers.gmRoll) {
 | 
				
			||||||
 | 
											// Make a new return line to be sent to the roller
 | 
				
			||||||
						const normalText = "<@" + message.author.id + ">" + returnmsg.line1 + "\nResults have been messaged to the following GMs: " + modifiers.gms.join(" ");
 | 
											const normalText = "<@" + message.author.id + ">" + 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 => {
 | 
											modifiers.gms.forEach(async e => {
 | 
				
			||||||
							const msgs = utils.split2k(returnText);
 | 
												const msgs = utils.split2k(returnText);
 | 
				
			||||||
							const failedDMs = <string[]>[];
 | 
												const failedDMs = <string[]>[];
 | 
				
			||||||
| 
						 | 
					@ -213,7 +238,9 @@ startBot({
 | 
				
			||||||
 | 
					
 | 
				
			||||||
						m.edit(normalText);
 | 
											m.edit(normalText);
 | 
				
			||||||
					} else {
 | 
										} else {
 | 
				
			||||||
 | 
											// When not a GM roll, make sure the message is not too big
 | 
				
			||||||
						if (returnText.length > 2000) {
 | 
											if (returnText.length > 2000) {
 | 
				
			||||||
 | 
												// If its too big, attempt to DM details to the roller
 | 
				
			||||||
							const msgs = utils.split2k(returnText);
 | 
												const msgs = utils.split2k(returnText);
 | 
				
			||||||
							let failed = false;
 | 
												let failed = false;
 | 
				
			||||||
							for (let i = 0; (!failed && (i < msgs.length)); i++) {
 | 
												for (let i = 0; (!failed && (i < msgs.length)); i++) {
 | 
				
			||||||
| 
						 | 
					@ -221,6 +248,7 @@ startBot({
 | 
				
			||||||
									failed = true;
 | 
														failed = true;
 | 
				
			||||||
								});
 | 
													});
 | 
				
			||||||
							}
 | 
												}
 | 
				
			||||||
 | 
												// If DM fails to send, alert roller of the failure, else handle normally
 | 
				
			||||||
							if (failed) {
 | 
												if (failed) {
 | 
				
			||||||
								returnText = "<@" + message.author.id + ">" + returnmsg.line1 + "\n" + returnmsg.line2 + "\nDetails have been ommitted from this message for being over 2000 characters.  WARNING: <@" + message.author.id + "> could **NOT** be messaged full details for verification purposes.";
 | 
													returnText = "<@" + message.author.id + ">" + returnmsg.line1 + "\n" + returnmsg.line2 + "\nDetails have been ommitted from this message for being over 2000 characters.  WARNING: <@" + message.author.id + "> could **NOT** be messaged full details for verification purposes.";
 | 
				
			||||||
							} else {
 | 
												} else {
 | 
				
			||||||
| 
						 | 
					@ -228,6 +256,7 @@ startBot({
 | 
				
			||||||
							}
 | 
												}
 | 
				
			||||||
						}
 | 
											}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
											// Finally send the text
 | 
				
			||||||
						m.edit(returnText);
 | 
											m.edit(returnText);
 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
				} catch (err) {
 | 
									} catch (err) {
 | 
				
			||||||
| 
						 | 
					@ -238,4 +267,7 @@ startBot({
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Start up the command prompt for debug usage
 | 
				
			||||||
 | 
					if (DEBUG) {
 | 
				
			||||||
	utils.cmdPrompt(config.logChannel, config.name, sendMessage);
 | 
						utils.cmdPrompt(config.logChannel, config.name, sendMessage);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,3 +1,6 @@
 | 
				
			||||||
 | 
					// solver.ts custom types
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// RollSet is used to preserve all information about a calculated roll
 | 
				
			||||||
export type RollSet = {
 | 
					export type RollSet = {
 | 
				
			||||||
	origidx: number,
 | 
						origidx: number,
 | 
				
			||||||
	roll: number,
 | 
						roll: number,
 | 
				
			||||||
| 
						 | 
					@ -8,6 +11,7 @@ export type RollSet = {
 | 
				
			||||||
	critFail: boolean
 | 
						critFail: boolean
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// SolvedStep is used to preserve information while math is being performed on the roll
 | 
				
			||||||
export type SolvedStep = {
 | 
					export type SolvedStep = {
 | 
				
			||||||
	total: number,
 | 
						total: number,
 | 
				
			||||||
	details: string,
 | 
						details: string,
 | 
				
			||||||
| 
						 | 
					@ -15,6 +19,7 @@ export type SolvedStep = {
 | 
				
			||||||
	containsFail: boolean
 | 
						containsFail: boolean
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// SolvedRoll is the complete solved and formatted roll, or the error said roll created
 | 
				
			||||||
export type SolvedRoll = {
 | 
					export type SolvedRoll = {
 | 
				
			||||||
	error: boolean,
 | 
						error: boolean,
 | 
				
			||||||
	errorMsg: string,
 | 
						errorMsg: string,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										239
									
								
								src/solver.ts
								
								
								
								
							
							
						
						
									
										239
									
								
								src/solver.ts
								
								
								
								
							| 
						 | 
					@ -1,11 +1,19 @@
 | 
				
			||||||
import { RollSet, SolvedStep, SolvedRoll } from "./solver.d.ts";
 | 
					import { RollSet, SolvedStep, SolvedRoll } from "./solver.d.ts";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// MAXLOOPS determines how long the bot will attempt a roll
 | 
				
			||||||
 | 
					// Default is 5000000 (5 million), which results in at most a 10 second delay before the bot calls the roll infinite or too complex
 | 
				
			||||||
 | 
					// Increase at your own risk
 | 
				
			||||||
const MAXLOOPS = 5000000;
 | 
					const MAXLOOPS = 5000000;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// genRoll(size) returns number
 | 
				
			||||||
 | 
					// genRoll rolls a die of size size and returns the result
 | 
				
			||||||
const genRoll = (size: number): number => {
 | 
					const genRoll = (size: number): number => {
 | 
				
			||||||
 | 
						// 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 Math.floor((Math.random() * size) + 1);
 | 
						return Math.floor((Math.random() * size) + 1);
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// compareRolls(a, b) returns -1|0|1
 | 
				
			||||||
 | 
					// compareRolls is used to order an array of RollSets by RollSet.roll
 | 
				
			||||||
const compareRolls = (a: RollSet, b: RollSet): number => {
 | 
					const compareRolls = (a: RollSet, b: RollSet): number => {
 | 
				
			||||||
	if (a.roll < b.roll) {
 | 
						if (a.roll < b.roll) {
 | 
				
			||||||
		return -1;
 | 
							return -1;
 | 
				
			||||||
| 
						 | 
					@ -16,6 +24,8 @@ const compareRolls = (a: RollSet, b: RollSet): number => {
 | 
				
			||||||
	return 0;
 | 
						return 0;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// compareRolls(a, b) returns -1|0|1
 | 
				
			||||||
 | 
					// compareRolls is used to order an array of RollSets by RollSet.origidx
 | 
				
			||||||
const compareOrigidx = (a: RollSet, b: RollSet): number => {
 | 
					const compareOrigidx = (a: RollSet, b: RollSet): number => {
 | 
				
			||||||
	if (a.origidx < b.origidx) {
 | 
						if (a.origidx < b.origidx) {
 | 
				
			||||||
		return -1;
 | 
							return -1;
 | 
				
			||||||
| 
						 | 
					@ -26,32 +36,48 @@ const compareOrigidx = (a: RollSet, b: RollSet): number => {
 | 
				
			||||||
	return 0;
 | 
						return 0;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// escapeCharacters(str, esc) returns str
 | 
				
			||||||
 | 
					// escapeCharacters escapes all characters listed in esc
 | 
				
			||||||
const escapeCharacters = (str: string, esc: string): string => {
 | 
					const escapeCharacters = (str: string, esc: string): string => {
 | 
				
			||||||
 | 
						// Loop thru each esc char one at a time
 | 
				
			||||||
	for (let i = 0; i < esc.length; i++) {
 | 
						for (let i = 0; i < esc.length; i++) {
 | 
				
			||||||
 | 
							// Create a new regex to look for that char that needs replaced and escape it
 | 
				
			||||||
		const temprgx = new RegExp(`[${esc[i]}]`, "g");
 | 
							const temprgx = new RegExp(`[${esc[i]}]`, "g");
 | 
				
			||||||
		str = str.replace(temprgx, ("\\" + esc[i]));
 | 
							str = str.replace(temprgx, ("\\" + esc[i]));
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	return str;
 | 
						return str;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// roll(rollStr, maximiseRoll, nominalRoll) returns RollSet
 | 
				
			||||||
 | 
					// roll parses and executes the rollStr, if needed it will also make the roll the maximum or average
 | 
				
			||||||
const roll = (rollStr: string, maximiseRoll: boolean, nominalRoll: boolean): RollSet[] => {
 | 
					const roll = (rollStr: string, maximiseRoll: boolean, nominalRoll: boolean): RollSet[] => {
 | 
				
			||||||
	/* Roll const Capabilities ==>  
 | 
						/* Roll Capabilities
 | 
				
			||||||
	 * Deciphers and rolls a single dice roll set
 | 
						 * Deciphers and rolls a single dice roll set
 | 
				
			||||||
	 * xdydzracsq!
 | 
						 * xdydzracsq!
 | 
				
			||||||
	 *
 | 
						 *
 | 
				
			||||||
	 * x            [OPT] - number of dice to roll, if omitted, 1 is used
 | 
						 * x            [OPT] - number of dice to roll, if omitted, 1 is used
 | 
				
			||||||
	 * dy           [REQ] - size of dice to roll, d20 = 20 sided die
 | 
						 * dy           [REQ] - size of dice to roll, d20 = 20 sided die
 | 
				
			||||||
	 * dz   [OPT] - drops the lowest z dice, cannot be used with kz
 | 
						 * dz || dlz    [OPT] - drops the lowest z dice, cannot be used with kz
 | 
				
			||||||
	 * kz   [OPT] - keeps the highest z dice, cannot be used with dz
 | 
						 * kz || khz    [OPT] - keeps the highest z dice, cannot be used with dz
 | 
				
			||||||
 | 
						 * dhz          [OPT] - drops the highest z dice, cannot be used with kz
 | 
				
			||||||
 | 
						 * klz          [OPT] - keeps the lowest z dice, cannot be used with dz
 | 
				
			||||||
	 * ra           [OPT] - rerolls any rolls that match a, r3 will reroll any dice that land on 3, throwing out old rolls
 | 
						 * ra           [OPT] - rerolls any rolls that match a, r3 will reroll any dice that land on 3, throwing out old rolls
 | 
				
			||||||
	 * csq  [OPT] - changes crit score to q, where q can be a single number or a range formatted as q-u
 | 
						 * csq || cs=q  [OPT] - changes crit score to q
 | 
				
			||||||
 | 
						 * cs<q         [OPT] - changes crit score to be less than or equal to q
 | 
				
			||||||
 | 
						 * cs>q         [OPT] - changes crit score to be greater than or equal to q	 
 | 
				
			||||||
 | 
						 * cfq || cs=q  [OPT] - changes crit fail to q
 | 
				
			||||||
 | 
						 * cf<q         [OPT] - changes crit fail to be less than or equal to q
 | 
				
			||||||
 | 
						 * cf>q         [OPT] - changes crit fail to be greater than or equal to q
 | 
				
			||||||
	 * !            [OPT] - exploding, rolls another dy for every crit roll
 | 
						 * !            [OPT] - exploding, rolls another dy for every crit roll
 | 
				
			||||||
	 */
 | 
						 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Make entire roll lowercase for ease of parsing
 | 
				
			||||||
	rollStr = rollStr.toLowerCase();
 | 
						rollStr = rollStr.toLowerCase();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Split the roll on the die size (and the drop if its there)
 | 
				
			||||||
	const dpts = rollStr.split("d");
 | 
						const dpts = rollStr.split("d");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Initialize the configuration to store the parsed data
 | 
				
			||||||
	const rollConf = {
 | 
						const rollConf = {
 | 
				
			||||||
		dieCount: 0,
 | 
							dieCount: 0,
 | 
				
			||||||
		dieSize: 0,
 | 
							dieSize: 0,
 | 
				
			||||||
| 
						 | 
					@ -73,89 +99,109 @@ const roll = (rollStr: string, maximiseRoll: boolean, nominalRoll: boolean): Rol
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		reroll: {
 | 
							reroll: {
 | 
				
			||||||
			on: false,
 | 
								on: false,
 | 
				
			||||||
			nums: [0]
 | 
								nums: <number[]>[]
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		critScore: {
 | 
							critScore: {
 | 
				
			||||||
			on: false,
 | 
								on: false,
 | 
				
			||||||
			range: [0]
 | 
								range: <number[]>[]
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		critFail: {
 | 
							critFail: {
 | 
				
			||||||
			on: false,
 | 
								on: false,
 | 
				
			||||||
			range: [0]
 | 
								range: <number[]>[]
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		exploding: false
 | 
							exploding: false
 | 
				
			||||||
	};
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// If the dpts is not long enough, throw error
 | 
				
			||||||
	if (dpts.length < 2) {
 | 
						if (dpts.length < 2) {
 | 
				
			||||||
		throw new Error("YouNeedAD");
 | 
							throw new Error("YouNeedAD");
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 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 tempDC = dpts.shift();
 | 
						const tempDC = dpts.shift();
 | 
				
			||||||
	rollConf.dieCount = parseInt(tempDC || "1");
 | 
						rollConf.dieCount = parseInt(tempDC || "1");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Finds the end of the die size/beginnning of the additional options
 | 
				
			||||||
	let afterDieIdx = dpts[0].search(/\D/);
 | 
						let afterDieIdx = dpts[0].search(/\D/);
 | 
				
			||||||
	if (afterDieIdx === -1) {
 | 
						if (afterDieIdx === -1) {
 | 
				
			||||||
		afterDieIdx = dpts[0].length;
 | 
							afterDieIdx = dpts[0].length;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Rejoin all remaining parts
 | 
				
			||||||
	let remains = dpts.join("");
 | 
						let remains = dpts.join("");
 | 
				
			||||||
 | 
						// Get the die size out of the remains and into the rollConf
 | 
				
			||||||
	rollConf.dieSize = parseInt(remains.slice(0, afterDieIdx));
 | 
						rollConf.dieSize = parseInt(remains.slice(0, afterDieIdx));
 | 
				
			||||||
	remains = remains.slice(afterDieIdx);
 | 
						remains = remains.slice(afterDieIdx);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Finish parsing the roll
 | 
						// Finish parsing the roll
 | 
				
			||||||
	if (remains.length > 0) {
 | 
						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) {
 | 
							if (remains.search(/\D/) !== 0 || remains.indexOf("l") === 0 || remains.indexOf("h") === 0) {
 | 
				
			||||||
			remains = "d" + remains;
 | 
								remains = "d" + remains;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Loop until all remaining args are parsed
 | 
				
			||||||
		while (remains.length > 0) {
 | 
							while (remains.length > 0) {
 | 
				
			||||||
 | 
								// Find the next number in the remains to be able to cut out the rule name
 | 
				
			||||||
			let afterSepIdx = remains.search(/\d/);
 | 
								let afterSepIdx = remains.search(/\d/);
 | 
				
			||||||
			if (afterSepIdx < 0) {
 | 
								if (afterSepIdx < 0) {
 | 
				
			||||||
				afterSepIdx = remains.length;
 | 
									afterSepIdx = remains.length;
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
								// Save the rule name to tSep and remove it from remains
 | 
				
			||||||
			const tSep = remains.slice(0, afterSepIdx);
 | 
								const tSep = remains.slice(0, afterSepIdx);
 | 
				
			||||||
			remains = remains.slice(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 = remains.search(/\D/);
 | 
				
			||||||
			if (afterNumIdx < 0) {
 | 
								if (afterNumIdx < 0) {
 | 
				
			||||||
				afterNumIdx = remains.length;
 | 
									afterNumIdx = remains.length;
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
								// Save the count/num to tNum leaving it in remains for the time being
 | 
				
			||||||
			const tNum = parseInt(remains.slice(0, afterNumIdx));
 | 
								const tNum = parseInt(remains.slice(0, afterNumIdx));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// Switch on rule name
 | 
				
			||||||
			switch (tSep) {
 | 
								switch (tSep) {
 | 
				
			||||||
				case "dl":
 | 
									case "dl":
 | 
				
			||||||
				case "d":
 | 
									case "d":
 | 
				
			||||||
 | 
										// Configure Drop (Lowest)
 | 
				
			||||||
					rollConf.drop.on = true;
 | 
										rollConf.drop.on = true;
 | 
				
			||||||
					rollConf.drop.count = tNum;
 | 
										rollConf.drop.count = tNum;
 | 
				
			||||||
					break;
 | 
										break;
 | 
				
			||||||
				case "kh":
 | 
									case "kh":
 | 
				
			||||||
				case "k":
 | 
									case "k":
 | 
				
			||||||
 | 
										// Configure Keep (Highest)
 | 
				
			||||||
					rollConf.keep.on = true;
 | 
										rollConf.keep.on = true;
 | 
				
			||||||
					rollConf.keep.count = tNum;
 | 
										rollConf.keep.count = tNum;
 | 
				
			||||||
					break;
 | 
										break;
 | 
				
			||||||
				case "dh":
 | 
									case "dh":
 | 
				
			||||||
 | 
										// Configure Drop (Highest)
 | 
				
			||||||
					rollConf.dropHigh.on = true;
 | 
										rollConf.dropHigh.on = true;
 | 
				
			||||||
					rollConf.dropHigh.count = tNum;
 | 
										rollConf.dropHigh.count = tNum;
 | 
				
			||||||
					break;
 | 
										break;
 | 
				
			||||||
				case "kl":
 | 
									case "kl":
 | 
				
			||||||
 | 
										// Configure Keep (Lowest)
 | 
				
			||||||
					rollConf.keepLow.on = true;
 | 
										rollConf.keepLow.on = true;
 | 
				
			||||||
					rollConf.keepLow.count = tNum;
 | 
										rollConf.keepLow.count = tNum;
 | 
				
			||||||
					break;
 | 
										break;
 | 
				
			||||||
				case "r":
 | 
									case "r":
 | 
				
			||||||
 | 
										// Configure Reroll (this can happen multiple times)
 | 
				
			||||||
					rollConf.reroll.on = true;
 | 
										rollConf.reroll.on = true;
 | 
				
			||||||
					rollConf.reroll.nums.push(tNum);
 | 
										rollConf.reroll.nums.push(tNum);
 | 
				
			||||||
					break;
 | 
										break;
 | 
				
			||||||
				case "cs":
 | 
									case "cs":
 | 
				
			||||||
				case "cs=":
 | 
									case "cs=":
 | 
				
			||||||
 | 
										// Configure CritScore for one number (this can happen multiple times)
 | 
				
			||||||
					rollConf.critScore.on = true;
 | 
										rollConf.critScore.on = true;
 | 
				
			||||||
					rollConf.critScore.range.push(tNum);
 | 
										rollConf.critScore.range.push(tNum);
 | 
				
			||||||
					break;
 | 
										break;
 | 
				
			||||||
				case "cs>":
 | 
									case "cs>":
 | 
				
			||||||
 | 
										// Configure CritScore for all numbers greater than or equal to tNum (this could happen multiple times, but why)
 | 
				
			||||||
					rollConf.critScore.on = true;
 | 
										rollConf.critScore.on = true;
 | 
				
			||||||
					for (let i = tNum; i <= rollConf.dieSize; i++) {
 | 
										for (let i = tNum; i <= rollConf.dieSize; i++) {
 | 
				
			||||||
						rollConf.critScore.range.push(i);
 | 
											rollConf.critScore.range.push(i);
 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
					break;
 | 
										break;
 | 
				
			||||||
				case "cs<":
 | 
									case "cs<":
 | 
				
			||||||
 | 
										// Configure CritScore for all numbers less than or equal to tNum (this could happen multiple times, but why)
 | 
				
			||||||
					rollConf.critScore.on = true;
 | 
										rollConf.critScore.on = true;
 | 
				
			||||||
					for (let i = 0; i <= tNum; i++) {
 | 
										for (let i = 0; i <= tNum; i++) {
 | 
				
			||||||
						rollConf.critScore.range.push(i);
 | 
											rollConf.critScore.range.push(i);
 | 
				
			||||||
| 
						 | 
					@ -163,39 +209,46 @@ const roll = (rollStr: string, maximiseRoll: boolean, nominalRoll: boolean): Rol
 | 
				
			||||||
					break;
 | 
										break;
 | 
				
			||||||
				case "cf":
 | 
									case "cf":
 | 
				
			||||||
				case "cf=":
 | 
									case "cf=":
 | 
				
			||||||
 | 
										// Configure CritFail for one number (this can happen multiple times)
 | 
				
			||||||
					rollConf.critFail.on = true;
 | 
										rollConf.critFail.on = true;
 | 
				
			||||||
					rollConf.critFail.range.push(tNum);
 | 
										rollConf.critFail.range.push(tNum);
 | 
				
			||||||
					break;
 | 
										break;
 | 
				
			||||||
				case "cf>":
 | 
									case "cf>":
 | 
				
			||||||
 | 
										// Configure CritFail for all numbers greater than or equal to tNum (this could happen multiple times, but why)
 | 
				
			||||||
					rollConf.critFail.on = true;
 | 
										rollConf.critFail.on = true;
 | 
				
			||||||
					for (let i = tNum; i <= rollConf.dieSize; i++) {
 | 
										for (let i = tNum; i <= rollConf.dieSize; i++) {
 | 
				
			||||||
						rollConf.critFail.range.push(i);
 | 
											rollConf.critFail.range.push(i);
 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
					break;
 | 
										break;
 | 
				
			||||||
				case "cf<":
 | 
									case "cf<":
 | 
				
			||||||
 | 
										// Configure CritFail for all numbers less than or equal to tNum (this could happen multiple times, but why)
 | 
				
			||||||
					rollConf.critFail.on = true;
 | 
										rollConf.critFail.on = true;
 | 
				
			||||||
					for (let i = 0; i <= tNum; i++) {
 | 
										for (let i = 0; i <= tNum; i++) {
 | 
				
			||||||
						rollConf.critFail.range.push(i);
 | 
											rollConf.critFail.range.push(i);
 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
					break;
 | 
										break;
 | 
				
			||||||
				case "!":
 | 
									case "!":
 | 
				
			||||||
 | 
										// Configure Exploding
 | 
				
			||||||
					rollConf.exploding = true;
 | 
										rollConf.exploding = true;
 | 
				
			||||||
					afterNumIdx = 1;
 | 
										afterNumIdx = 1;
 | 
				
			||||||
					break;
 | 
										break;
 | 
				
			||||||
				default:
 | 
									default:
 | 
				
			||||||
 | 
										// Throw error immediately if unknown op is encountered
 | 
				
			||||||
					throw new Error("UnknownOperation_" + tSep);
 | 
										throw new Error("UnknownOperation_" + tSep);
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
								// Finally slice off everything else parsed this loop
 | 
				
			||||||
			remains = remains.slice(afterNumIdx);
 | 
								remains = remains.slice(afterNumIdx);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Verify the parse
 | 
						// Verify the parse, throwing errors for every invalid config
 | 
				
			||||||
	if (rollConf.dieCount < 0) {
 | 
						if (rollConf.dieCount < 0) {
 | 
				
			||||||
		throw new Error("NoZerosAllowed_base");
 | 
							throw new Error("NoZerosAllowed_base");
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	if (rollConf.dieCount === 0 || rollConf.dieSize === 0) {
 | 
						if (rollConf.dieCount === 0 || rollConf.dieSize === 0) {
 | 
				
			||||||
		throw new Error("NoZerosAllowed_base");
 | 
							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;
 | 
						let dkdkCnt = 0;
 | 
				
			||||||
	[rollConf.drop.on, rollConf.keep.on, rollConf.dropHigh.on, rollConf.keepLow.on].forEach(e => {
 | 
						[rollConf.drop.on, rollConf.keep.on, rollConf.dropHigh.on, rollConf.keepLow.on].forEach(e => {
 | 
				
			||||||
		if (e) {
 | 
							if (e) {
 | 
				
			||||||
| 
						 | 
					@ -246,6 +299,7 @@ const roll = (rollStr: string, maximiseRoll: boolean, nominalRoll: boolean): Rol
 | 
				
			||||||
	 * 	}
 | 
						 * 	}
 | 
				
			||||||
	 */
 | 
						 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Initialize a templet rollSet to copy multiple times
 | 
				
			||||||
	const templateRoll = {
 | 
						const templateRoll = {
 | 
				
			||||||
		origidx: 0,
 | 
							origidx: 0,
 | 
				
			||||||
		roll: 0,
 | 
							roll: 0,
 | 
				
			||||||
| 
						 | 
					@ -256,71 +310,95 @@ const roll = (rollStr: string, maximiseRoll: boolean, nominalRoll: boolean): Rol
 | 
				
			||||||
		critFail: false
 | 
							critFail: false
 | 
				
			||||||
	};
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Begin counting the number of loops to prevent from getting into an infinite loop
 | 
				
			||||||
	let loopCount = 0;
 | 
						let loopCount = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Initial rolling, not handling reroll or exploding here
 | 
				
			||||||
	for (let i = 0; i < rollConf.dieCount; i++) {
 | 
						for (let i = 0; i < rollConf.dieCount; i++) {
 | 
				
			||||||
 | 
							// If loopCount gets too high, stop trying to calculate infinity
 | 
				
			||||||
		if (loopCount > MAXLOOPS) {
 | 
							if (loopCount > MAXLOOPS) {
 | 
				
			||||||
			throw new Error("MaxLoopsExceeded");
 | 
								throw new Error("MaxLoopsExceeded");
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Copy the template to fill out for this iteration
 | 
				
			||||||
		const rolling = JSON.parse(JSON.stringify(templateRoll));
 | 
							const rolling = JSON.parse(JSON.stringify(templateRoll));
 | 
				
			||||||
 | 
							// If maximiseRoll 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 = maximiseRoll ? rollConf.dieSize : (nominalRoll ? ((rollConf.dieSize / 2) + 0.5) : genRoll(rollConf.dieSize));
 | 
							rolling.roll = maximiseRoll ? rollConf.dieSize : (nominalRoll ? ((rollConf.dieSize / 2) + 0.5) : genRoll(rollConf.dieSize));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// 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.indexOf(rolling.roll) >= 0) {
 | 
							if (rollConf.critScore.on && rollConf.critScore.range.indexOf(rolling.roll) >= 0) {
 | 
				
			||||||
			rolling.critHit = true;
 | 
								rolling.critHit = true;
 | 
				
			||||||
		} else if (!rollConf.critScore.on) {
 | 
							} else if (!rollConf.critScore.on) {
 | 
				
			||||||
			rolling.critHit = (rolling.roll === rollConf.dieSize);
 | 
								rolling.critHit = (rolling.roll === 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.indexOf(rolling.roll) >= 0) {
 | 
							if (rollConf.critFail.on && rollConf.critFail.range.indexOf(rolling.roll) >= 0) {
 | 
				
			||||||
			rolling.critFail = true;
 | 
								rolling.critFail = true;
 | 
				
			||||||
		} else if (!rollConf.critFail.on) {
 | 
							} else if (!rollConf.critFail.on) {
 | 
				
			||||||
			rolling.critFail = (rolling.roll === 1);
 | 
								rolling.critFail = (rolling.roll === 1);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Push the newly created roll and loop again
 | 
				
			||||||
		rollSet.push(rolling);
 | 
							rollSet.push(rolling);
 | 
				
			||||||
		loopCount++;
 | 
							loopCount++;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// If needed, handle rerolling and exploding dice now
 | 
				
			||||||
	if (rollConf.reroll.on || rollConf.exploding) {
 | 
						if (rollConf.reroll.on || rollConf.exploding) {
 | 
				
			||||||
		for (let i = 0; i < rollSet.length; i++) {
 | 
							for (let i = 0; i < rollSet.length; i++) {
 | 
				
			||||||
 | 
								// If loopCount gets too high, stop trying to calculate infinity
 | 
				
			||||||
			if (loopCount > MAXLOOPS) {
 | 
								if (loopCount > MAXLOOPS) {
 | 
				
			||||||
				throw new Error("MaxLoopsExceeded");
 | 
									throw new Error("MaxLoopsExceeded");
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// If we need to reroll this roll, flag its been replaced and...
 | 
				
			||||||
			if (rollConf.reroll.on && rollConf.reroll.nums.indexOf(rollSet[i].roll) >= 0) {
 | 
								if (rollConf.reroll.on && rollConf.reroll.nums.indexOf(rollSet[i].roll) >= 0) {
 | 
				
			||||||
				rollSet[i].rerolled = true;
 | 
									rollSet[i].rerolled = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									// Copy the template to fill out for this iteration
 | 
				
			||||||
				const newRoll = JSON.parse(JSON.stringify(templateRoll));
 | 
									const newRoll = JSON.parse(JSON.stringify(templateRoll));
 | 
				
			||||||
 | 
									// If maximiseRoll 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
 | 
				
			||||||
				newRoll.roll = maximiseRoll ? rollConf.dieSize : (nominalRoll ? ((rollConf.dieSize / 2) + 0.5) : genRoll(rollConf.dieSize));
 | 
									newRoll.roll = maximiseRoll ? rollConf.dieSize : (nominalRoll ? ((rollConf.dieSize / 2) + 0.5) : genRoll(rollConf.dieSize));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									// 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.indexOf(newRoll.roll) >= 0) {
 | 
									if (rollConf.critScore.on && rollConf.critScore.range.indexOf(newRoll.roll) >= 0) {
 | 
				
			||||||
					newRoll.critHit = true;
 | 
										newRoll.critHit = true;
 | 
				
			||||||
				} else if (!rollConf.critScore.on) {
 | 
									} else if (!rollConf.critScore.on) {
 | 
				
			||||||
					newRoll.critHit = (newRoll.roll === rollConf.dieSize);
 | 
										newRoll.critHit = (newRoll.roll === 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.indexOf(newRoll.roll) >= 0) {
 | 
									if (rollConf.critFail.on && rollConf.critFail.range.indexOf(newRoll.roll) >= 0) {
 | 
				
			||||||
					newRoll.critFail = true;
 | 
										newRoll.critFail = true;
 | 
				
			||||||
				} else if (!rollConf.critFail.on) {
 | 
									} else if (!rollConf.critFail.on) {
 | 
				
			||||||
					newRoll.critFail = (newRoll.roll === 1);
 | 
										newRoll.critFail = (newRoll.roll === 1);
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									// Slot this new roll in after the current iteration so it can be processed in the next loop
 | 
				
			||||||
				rollSet.splice(i + 1, 0, newRoll);
 | 
									rollSet.splice(i + 1, 0, newRoll);
 | 
				
			||||||
			} else if (rollConf.exploding && !rollSet[i].rerolled && rollSet[i].critHit) {
 | 
								} else if (rollConf.exploding && !rollSet[i].rerolled && rollSet[i].critHit) {
 | 
				
			||||||
 | 
									//If it exploded, we keep both, so no flags need to be set
 | 
				
			||||||
 | 
									
 | 
				
			||||||
 | 
									// Copy the template to fill out for this iteration
 | 
				
			||||||
				const newRoll = JSON.parse(JSON.stringify(templateRoll));
 | 
									const newRoll = JSON.parse(JSON.stringify(templateRoll));
 | 
				
			||||||
 | 
									// If maximiseRoll 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
 | 
				
			||||||
				newRoll.roll = maximiseRoll ? rollConf.dieSize : (nominalRoll ? ((rollConf.dieSize / 2) + 0.5) : genRoll(rollConf.dieSize));
 | 
									newRoll.roll = maximiseRoll ? rollConf.dieSize : (nominalRoll ? ((rollConf.dieSize / 2) + 0.5) : genRoll(rollConf.dieSize));
 | 
				
			||||||
 | 
									// Always mark this roll as exploding
 | 
				
			||||||
				newRoll.exploding = true;
 | 
									newRoll.exploding = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									// 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.indexOf(newRoll.roll) >= 0) {
 | 
									if (rollConf.critScore.on && rollConf.critScore.range.indexOf(newRoll.roll) >= 0) {
 | 
				
			||||||
					newRoll.critHit = true;
 | 
										newRoll.critHit = true;
 | 
				
			||||||
				} else if (!rollConf.critScore.on) {
 | 
									} else if (!rollConf.critScore.on) {
 | 
				
			||||||
					newRoll.critHit = (newRoll.roll === rollConf.dieSize);
 | 
										newRoll.critHit = (newRoll.roll === 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.indexOf(newRoll.roll) >= 0) {
 | 
									if (rollConf.critFail.on && rollConf.critFail.range.indexOf(newRoll.roll) >= 0) {
 | 
				
			||||||
					newRoll.critFail = true;
 | 
										newRoll.critFail = true;
 | 
				
			||||||
				} else if (!rollConf.critFail.on) {
 | 
									} else if (!rollConf.critFail.on) {
 | 
				
			||||||
					newRoll.critFail = (newRoll.roll === 1);
 | 
										newRoll.critFail = (newRoll.roll === 1);
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									// Slot this new roll in after the current iteration so it can be processed in the next loop
 | 
				
			||||||
				rollSet.splice(i + 1, 0, newRoll);
 | 
									rollSet.splice(i + 1, 0, newRoll);
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -328,7 +406,11 @@ const roll = (rollStr: string, maximiseRoll: boolean, nominalRoll: boolean): Rol
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// If we need to handle the drop/keep flags
 | 
				
			||||||
 | 
						if (dkdkCnt > 0) {
 | 
				
			||||||
 | 
							// Count how many rerolled dice there are if the reroll flag was on
 | 
				
			||||||
		let rerollCount = 0;
 | 
							let rerollCount = 0;
 | 
				
			||||||
 | 
							if (rollConf.reroll.on) {
 | 
				
			||||||
			for (let i = 0; i < rollSet.length; i++) {
 | 
								for (let i = 0; i < rollSet.length; i++) {
 | 
				
			||||||
				rollSet[i].origidx = i;
 | 
									rollSet[i].origidx = i;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -336,36 +418,38 @@ const roll = (rollStr: string, maximiseRoll: boolean, nominalRoll: boolean): Rol
 | 
				
			||||||
					rerollCount++;
 | 
										rerollCount++;
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (rollConf.drop.on || rollConf.keep.on || rollConf.dropHigh.on || rollConf.keepLow.on) {
 | 
							// Order the rolls from least to greatest (by RollSet.roll)
 | 
				
			||||||
		rollSet.sort(compareRolls);
 | 
							rollSet.sort(compareRolls);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		let dropCount = 0;
 | 
							// Determine how many valid rolls there are to drop from (may not be equal to dieCount due to exploding)
 | 
				
			||||||
		const validRolls = rollSet.length - rerollCount;
 | 
							const validRolls = rollSet.length - rerollCount;
 | 
				
			||||||
 | 
							let dropCount = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// For normal drop and keep, simple subtraction is enough to determine how many to drop
 | 
				
			||||||
 | 
							// Protections are in to prevent the dropCount from going below 0 or more than the valid rolls to drop
 | 
				
			||||||
		if (rollConf.drop.on) {
 | 
							if (rollConf.drop.on) {
 | 
				
			||||||
			dropCount = rollConf.drop.count;
 | 
								dropCount = rollConf.drop.count;
 | 
				
			||||||
			if (dropCount > validRolls) {
 | 
								if (dropCount > validRolls) {
 | 
				
			||||||
				dropCount = validRolls;
 | 
									dropCount = validRolls;
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							} else if (rollConf.keep.on) {
 | 
				
			||||||
 | 
					 | 
				
			||||||
		if (rollConf.keep.on) {
 | 
					 | 
				
			||||||
			dropCount = validRolls - rollConf.keep.count;
 | 
								dropCount = validRolls - rollConf.keep.count;
 | 
				
			||||||
			if (dropCount < 0) {
 | 
								if (dropCount < 0) {
 | 
				
			||||||
				dropCount = 0;
 | 
									dropCount = 0;
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if (rollConf.dropHigh.on) {
 | 
							// For inverted drop and keep, order must be flipped to greatest to least before the simple subtraction can determine how many to drop
 | 
				
			||||||
 | 
							// Protections are in to prevent the dropCount from going below 0 or more than the valid rolls to drop
 | 
				
			||||||
 | 
							else if (rollConf.dropHigh.on) {
 | 
				
			||||||
			rollSet.reverse();
 | 
								rollSet.reverse();
 | 
				
			||||||
			dropCount = rollConf.dropHigh.count;
 | 
								dropCount = rollConf.dropHigh.count;
 | 
				
			||||||
			if (dropCount > validRolls) {
 | 
								if (dropCount > validRolls) {
 | 
				
			||||||
				dropCount = validRolls;
 | 
									dropCount = validRolls;
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							} else if (rollConf.keepLow.on) {
 | 
				
			||||||
 | 
					 | 
				
			||||||
		if (rollConf.keepLow.on) {
 | 
					 | 
				
			||||||
			rollSet.reverse();
 | 
								rollSet.reverse();
 | 
				
			||||||
			dropCount = validRolls - rollConf.keepLow.count;
 | 
								dropCount = validRolls - rollConf.keepLow.count;
 | 
				
			||||||
			if (dropCount < 0) {
 | 
								if (dropCount < 0) {
 | 
				
			||||||
| 
						 | 
					@ -373,8 +457,10 @@ const roll = (rollStr: string, maximiseRoll: boolean, nominalRoll: boolean): Rol
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Now its time to drop all dice needed
 | 
				
			||||||
		let i = 0;
 | 
							let i = 0;
 | 
				
			||||||
		while (dropCount > 0 && i < rollSet.length) {
 | 
							while (dropCount > 0 && i < rollSet.length) {
 | 
				
			||||||
 | 
								// Skip all rolls that were rerolled
 | 
				
			||||||
			if (!rollSet[i].rerolled) {
 | 
								if (!rollSet[i].rerolled) {
 | 
				
			||||||
				rollSet[i].dropped = true;
 | 
									rollSet[i].dropped = true;
 | 
				
			||||||
				dropCount--;
 | 
									dropCount--;
 | 
				
			||||||
| 
						 | 
					@ -382,24 +468,31 @@ const roll = (rollStr: string, maximiseRoll: boolean, nominalRoll: boolean): Rol
 | 
				
			||||||
			i++;
 | 
								i++;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Finally, return the rollSet to its original order
 | 
				
			||||||
		rollSet.sort(compareOrigidx);
 | 
							rollSet.sort(compareOrigidx);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return rollSet;
 | 
						return rollSet;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// formatRoll(rollConf, maximiseRoll, nominalRoll) returns one SolvedStep
 | 
				
			||||||
 | 
					// formatRoll handles creating and formatting the completed rolls into the SolvedStep format
 | 
				
			||||||
const formatRoll = (rollConf: string, maximiseRoll: boolean, nominalRoll: boolean): SolvedStep => {
 | 
					const formatRoll = (rollConf: string, maximiseRoll: boolean, nominalRoll: boolean): SolvedStep => {
 | 
				
			||||||
	let tempTotal = 0;
 | 
						let tempTotal = 0;
 | 
				
			||||||
	let tempDetails = "[";
 | 
						let tempDetails = "[";
 | 
				
			||||||
	let tempCrit = false;
 | 
						let tempCrit = false;
 | 
				
			||||||
	let tempFail = false;
 | 
						let tempFail = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Generate the roll, passing flags thru
 | 
				
			||||||
	const tempRollSet = roll(rollConf, maximiseRoll, nominalRoll);
 | 
						const tempRollSet = roll(rollConf, maximiseRoll, nominalRoll);
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						// Loop thru all parts of the roll to document everything that was done to create the total roll
 | 
				
			||||||
	tempRollSet.forEach(e => {
 | 
						tempRollSet.forEach(e => {
 | 
				
			||||||
		let preFormat = "";
 | 
							let preFormat = "";
 | 
				
			||||||
		let postFormat = "";
 | 
							let postFormat = "";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if (!e.dropped && !e.rerolled) {
 | 
							if (!e.dropped && !e.rerolled) {
 | 
				
			||||||
 | 
								// If the roll was not dropped or rerolled, add it to the stepTotal and flag the critHit/critFail
 | 
				
			||||||
			tempTotal += e.roll;
 | 
								tempTotal += e.roll;
 | 
				
			||||||
			if (e.critHit) {
 | 
								if (e.critHit) {
 | 
				
			||||||
				tempCrit = true;
 | 
									tempCrit = true;
 | 
				
			||||||
| 
						 | 
					@ -408,21 +501,27 @@ const formatRoll = (rollConf: string, maximiseRoll: boolean, nominalRoll: boolea
 | 
				
			||||||
				tempFail = true;
 | 
									tempFail = true;
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
							// If the roll was a crit hit or fail, or dropped/rerolled, add the formatting needed
 | 
				
			||||||
		if (e.critHit) {
 | 
							if (e.critHit) {
 | 
				
			||||||
 | 
								// Bold for crit success
 | 
				
			||||||
			preFormat = "**" + preFormat;
 | 
								preFormat = "**" + preFormat;
 | 
				
			||||||
			postFormat = postFormat + "**";
 | 
								postFormat = postFormat + "**";
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		if (e.critFail) {
 | 
							if (e.critFail) {
 | 
				
			||||||
 | 
								// Underline for crit fail
 | 
				
			||||||
			preFormat = "__" + preFormat;
 | 
								preFormat = "__" + preFormat;
 | 
				
			||||||
			postFormat = postFormat + "__";
 | 
								postFormat = postFormat + "__";
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		if (e.dropped || e.rerolled) {
 | 
							if (e.dropped || e.rerolled) {
 | 
				
			||||||
 | 
								// Strikethrough for dropped/rerolled rolls
 | 
				
			||||||
			preFormat = "~~" + preFormat;
 | 
								preFormat = "~~" + preFormat;
 | 
				
			||||||
			postFormat = postFormat + "~~";
 | 
								postFormat = postFormat + "~~";
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Finally add this to the roll's details
 | 
				
			||||||
		tempDetails += preFormat + e.roll + postFormat + " + ";
 | 
							tempDetails += preFormat + e.roll + postFormat + " + ";
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
 | 
						// After the looping is done, remove the extra " + " from the details and cap it with the closing ]
 | 
				
			||||||
	tempDetails = tempDetails.substr(0, (tempDetails.length - 3));
 | 
						tempDetails = tempDetails.substr(0, (tempDetails.length - 3));
 | 
				
			||||||
	tempDetails += "]";
 | 
						tempDetails += "]";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -434,7 +533,10 @@ const formatRoll = (rollConf: string, maximiseRoll: boolean, nominalRoll: boolea
 | 
				
			||||||
	};
 | 
						};
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// fullSolver(conf, wrapDetails) returns one condensed SolvedStep
 | 
				
			||||||
 | 
					// fullSolver is a function that recursively solves the full roll and math
 | 
				
			||||||
const fullSolver = (conf: (string | number | SolvedStep)[], wrapDetails: boolean): SolvedStep => {
 | 
					const fullSolver = (conf: (string | number | SolvedStep)[], wrapDetails: boolean): SolvedStep => {
 | 
				
			||||||
 | 
						// Initialize PEMDAS
 | 
				
			||||||
	const signs = ["^", "*", "/", "%", "+", "-"];
 | 
						const signs = ["^", "*", "/", "%", "+", "-"];
 | 
				
			||||||
	const stepSolve = {
 | 
						const stepSolve = {
 | 
				
			||||||
		total: 0,
 | 
							total: 0,
 | 
				
			||||||
| 
						 | 
					@ -443,6 +545,7 @@ const fullSolver = (conf: (string | number | SolvedStep)[], wrapDetails: boolean
 | 
				
			||||||
		containsFail: false
 | 
							containsFail: false
 | 
				
			||||||
	};
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// If entering with a single number, note it now
 | 
				
			||||||
	let singleNum = false;
 | 
						let singleNum = false;
 | 
				
			||||||
	if (conf.length === 1) {
 | 
						if (conf.length === 1) {
 | 
				
			||||||
		singleNum = true;
 | 
							singleNum = true;
 | 
				
			||||||
| 
						 | 
					@ -450,47 +553,63 @@ const fullSolver = (conf: (string | number | SolvedStep)[], wrapDetails: boolean
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Evaluate all parenthesis
 | 
						// Evaluate all parenthesis
 | 
				
			||||||
	while (conf.indexOf("(") > -1) {
 | 
						while (conf.indexOf("(") > -1) {
 | 
				
			||||||
 | 
							// Get first open parenthesis
 | 
				
			||||||
		const openParen = conf.indexOf("(");
 | 
							const openParen = conf.indexOf("(");
 | 
				
			||||||
		let closeParen = -1;
 | 
							let closeParen = -1;
 | 
				
			||||||
		let nextParen = 0;
 | 
							let nextParen = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Using nextParen to count the opening/closing parens, find the matching paren to openParen above
 | 
				
			||||||
		for (let i = openParen; i < conf.length; i++) {
 | 
							for (let i = openParen; i < conf.length; i++) {
 | 
				
			||||||
 | 
								// If we hit an open, add one (this includes the openParen we start with), if we hit a close, subtract one
 | 
				
			||||||
			if (conf[i] === "(") {
 | 
								if (conf[i] === "(") {
 | 
				
			||||||
				nextParen++;
 | 
									nextParen++;
 | 
				
			||||||
			} else if (conf[i] === ")") {
 | 
								} else if (conf[i] === ")") {
 | 
				
			||||||
				nextParen--;
 | 
									nextParen--;
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			
 | 
								
 | 
				
			||||||
 | 
								// When nextParen reaches 0 again, we will have found the matching closing parenthesis and can safely exit the for loop
 | 
				
			||||||
			if (nextParen === 0) {
 | 
								if (nextParen === 0) {
 | 
				
			||||||
				closeParen = i;
 | 
									closeParen = i;
 | 
				
			||||||
				break;
 | 
									break;
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Make sure we did find the correct closing paren, if not, error out now
 | 
				
			||||||
		if (closeParen === -1 || closeParen < openParen) {
 | 
							if (closeParen === -1 || closeParen < openParen) {
 | 
				
			||||||
			throw new Error("UnbalancedParens");
 | 
								throw new Error("UnbalancedParens");
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		conf.splice(openParen, closeParen, fullSolver(conf.slice((openParen + 1), closeParen), true));
 | 
							// Replace the itemes between openParen and closeParen (including the parens) with its solved equilvalent by calling the solver on the items between openParen and closeParen (excluding the parens)
 | 
				
			||||||
 | 
							conf.splice(openParen, (closeParen + 1), fullSolver(conf.slice((openParen + 1), closeParen), true));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Determing if we need to add in a multiplication sign to handle implicit multiplication (like "(4)2" = 8)
 | 
				
			||||||
 | 
							// insertedMult flags if there was a multiplication sign inserted before the parens
 | 
				
			||||||
		let insertedMult = false;
 | 
							let insertedMult = false;
 | 
				
			||||||
 | 
							// Check if a number was directly before openParen and slip in the "*" if needed
 | 
				
			||||||
		if (((openParen - 1) > -1) && (signs.indexOf(conf[openParen - 1].toString()) === -1)) {
 | 
							if (((openParen - 1) > -1) && (signs.indexOf(conf[openParen - 1].toString()) === -1)) {
 | 
				
			||||||
			insertedMult = true;
 | 
								insertedMult = true;
 | 
				
			||||||
			conf.splice(openParen, 0, "*");
 | 
								conf.splice(openParen, 0, "*");
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
							// Check if a number is directly after closeParen and slip in the "*" if needed
 | 
				
			||||||
		if (!insertedMult && (((openParen + 1) < conf.length) && (signs.indexOf(conf[openParen + 1].toString()) === -1))) {
 | 
							if (!insertedMult && (((openParen + 1) < conf.length) && (signs.indexOf(conf[openParen + 1].toString()) === -1))) {
 | 
				
			||||||
			conf.splice((openParen + 1), 0, "*");
 | 
								conf.splice((openParen + 1), 0, "*");
 | 
				
			||||||
		} else if (insertedMult && (((openParen + 2) < conf.length) && (signs.indexOf(conf[openParen + 2].toString()) === -1))) {
 | 
							} else if (insertedMult && (((openParen + 2) < conf.length) && (signs.indexOf(conf[openParen + 2].toString()) === -1))) {
 | 
				
			||||||
 | 
								// insertedMult is utilized here to let us account for an additional item being inserted into the array (the "*" from before openParn)
 | 
				
			||||||
			conf.splice((openParen + 2), 0, "*");
 | 
								conf.splice((openParen + 2), 0, "*");
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Evaluate all EMMDAS
 | 
						// Evaluate all EMMDAS by looping thru each teir of operators (exponential is the higehest teir, addition/subtraction the lowest)
 | 
				
			||||||
	const allCurOps = [["^"], ["*", "/", "%"], ["+", "-"]];
 | 
						const allCurOps = [["^"], ["*", "/", "%"], ["+", "-"]];
 | 
				
			||||||
	allCurOps.forEach(curOps => {
 | 
						allCurOps.forEach(curOps => {
 | 
				
			||||||
 | 
							// Iterate thru all operators/operands in the conf
 | 
				
			||||||
		for (let i = 0; i < conf.length; i++) {
 | 
							for (let i = 0; i < conf.length; i++) {
 | 
				
			||||||
 | 
								// Check if the current index is in the active teir of operators
 | 
				
			||||||
			if (curOps.indexOf(conf[i].toString()) > -1) {
 | 
								if (curOps.indexOf(conf[i].toString()) > -1) {
 | 
				
			||||||
 | 
									// Grab the operands from before and after the operator
 | 
				
			||||||
				const operand1 = conf[i - 1];
 | 
									const operand1 = conf[i - 1];
 | 
				
			||||||
				const operand2 = conf[i + 1];
 | 
									const operand2 = conf[i + 1];
 | 
				
			||||||
 | 
									// Init temp math to NaN to catch bad parsing
 | 
				
			||||||
				let oper1 = NaN;
 | 
									let oper1 = NaN;
 | 
				
			||||||
				let oper2 = NaN;
 | 
									let oper2 = NaN;
 | 
				
			||||||
				const subStepSolve = {
 | 
									const subStepSolve = {
 | 
				
			||||||
| 
						 | 
					@ -500,31 +619,38 @@ const fullSolver = (conf: (string | number | SolvedStep)[], wrapDetails: boolean
 | 
				
			||||||
					containsFail: false
 | 
										containsFail: false
 | 
				
			||||||
				};
 | 
									};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									// If operand1 is a SolvedStep, populate our subStepSolve with its details and crit/fail flags
 | 
				
			||||||
				if (typeof operand1 === "object") {
 | 
									if (typeof operand1 === "object") {
 | 
				
			||||||
					oper1 = operand1.total;
 | 
										oper1 = operand1.total;
 | 
				
			||||||
					subStepSolve.details = operand1.details + "\\" + conf[i];
 | 
										subStepSolve.details = operand1.details + "\\" + conf[i];
 | 
				
			||||||
					subStepSolve.containsCrit = operand1.containsCrit;
 | 
										subStepSolve.containsCrit = operand1.containsCrit;
 | 
				
			||||||
					subStepSolve.containsFail = operand1.containsFail;
 | 
										subStepSolve.containsFail = operand1.containsFail;
 | 
				
			||||||
				} else {
 | 
									} else {
 | 
				
			||||||
 | 
										// else parse it as a number and add it to the subStep details
 | 
				
			||||||
					oper1 = parseFloat(operand1.toString());
 | 
										oper1 = parseFloat(operand1.toString());
 | 
				
			||||||
					subStepSolve.details = oper1.toString() + conf[i];
 | 
										subStepSolve.details = oper1.toString() + "\\" + conf[i];
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									// If operand2 is a SolvedStep, populate our subStepSolve with its details without overriding what operand1 filled in
 | 
				
			||||||
				if (typeof operand2 === "object") {
 | 
									if (typeof operand2 === "object") {
 | 
				
			||||||
					oper2 = operand2.total;
 | 
										oper2 = operand2.total;
 | 
				
			||||||
					subStepSolve.details += operand2.details;
 | 
										subStepSolve.details += operand2.details;
 | 
				
			||||||
					subStepSolve.containsCrit = subStepSolve.containsCrit || operand2.containsCrit;
 | 
										subStepSolve.containsCrit = subStepSolve.containsCrit || operand2.containsCrit;
 | 
				
			||||||
					subStepSolve.containsFail = subStepSolve.containsFail || operand2.containsFail;
 | 
										subStepSolve.containsFail = subStepSolve.containsFail || operand2.containsFail;
 | 
				
			||||||
				} else {
 | 
									} else {
 | 
				
			||||||
 | 
										// else parse it as a number and add it to the subStep details
 | 
				
			||||||
					oper2 = parseFloat(operand2.toString());
 | 
										oper2 = parseFloat(operand2.toString());
 | 
				
			||||||
					subStepSolve.details += oper2;
 | 
										subStepSolve.details += oper2;
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									// Make sure neither operand is NaN before continuing
 | 
				
			||||||
				if (isNaN(oper1) || isNaN(oper2)) {
 | 
									if (isNaN(oper1) || isNaN(oper2)) {
 | 
				
			||||||
					throw new Error("OperandNaN");
 | 
										throw new Error("OperandNaN");
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									// Verify a second time that both are numbers before doing math, throwing an error if necessary
 | 
				
			||||||
				if ((typeof oper1 === "number") && (typeof oper2 === "number")) {
 | 
									if ((typeof oper1 === "number") && (typeof oper2 === "number")) {
 | 
				
			||||||
 | 
										// Finally do the operator on the operands, throw an error if the operator is not found
 | 
				
			||||||
					switch (conf[i]) {
 | 
										switch (conf[i]) {
 | 
				
			||||||
						case "^":
 | 
											case "^":
 | 
				
			||||||
							subStepSolve.total = Math.pow(oper1, oper2);
 | 
												subStepSolve.total = Math.pow(oper1, oper2);
 | 
				
			||||||
| 
						 | 
					@ -551,28 +677,35 @@ const fullSolver = (conf: (string | number | SolvedStep)[], wrapDetails: boolean
 | 
				
			||||||
					throw new Error("EMDASNotNumber");
 | 
										throw new Error("EMDASNotNumber");
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									// Replace the two operands and their operator with our subStepSolve
 | 
				
			||||||
				conf.splice((i - 1), (i + 2), subStepSolve);
 | 
									conf.splice((i - 1), (i + 2), subStepSolve);
 | 
				
			||||||
 | 
									// Because we are messing around with the array we are iterating thru, we need to back up one idx to make sure every operator gets processed
 | 
				
			||||||
				i--;
 | 
									i--;
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// If we somehow have more than one item left in conf at this point, something broke, throw an error
 | 
				
			||||||
	if (conf.length > 1) {
 | 
						if (conf.length > 1) {
 | 
				
			||||||
		throw new Error("ConfWhat");
 | 
							throw new Error("ConfWhat");
 | 
				
			||||||
	} else if (singleNum && (typeof (conf[0]) === "number")) {
 | 
						} else if (singleNum && (typeof (conf[0]) === "number")) {
 | 
				
			||||||
 | 
							// If we are only left with a number, populate the stepSolve with it
 | 
				
			||||||
		stepSolve.total = conf[0];
 | 
							stepSolve.total = conf[0];
 | 
				
			||||||
		stepSolve.details = conf[0].toString();
 | 
							stepSolve.details = conf[0].toString();
 | 
				
			||||||
	} else {
 | 
						} else {
 | 
				
			||||||
 | 
							// Else fully populate the stepSolve with what was computed
 | 
				
			||||||
		stepSolve.total = (<SolvedStep>conf[0]).total;
 | 
							stepSolve.total = (<SolvedStep>conf[0]).total;
 | 
				
			||||||
		stepSolve.details = (<SolvedStep>conf[0]).details;
 | 
							stepSolve.details = (<SolvedStep>conf[0]).details;
 | 
				
			||||||
		stepSolve.containsCrit = (<SolvedStep>conf[0]).containsCrit;
 | 
							stepSolve.containsCrit = (<SolvedStep>conf[0]).containsCrit;
 | 
				
			||||||
		stepSolve.containsFail = (<SolvedStep>conf[0]).containsFail;
 | 
							stepSolve.containsFail = (<SolvedStep>conf[0]).containsFail;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// If this was a nested call, add on parens around the details to show what math we've done
 | 
				
			||||||
	if (wrapDetails) {
 | 
						if (wrapDetails) {
 | 
				
			||||||
		stepSolve.details = "(" + stepSolve.details + ")";
 | 
							stepSolve.details = "(" + stepSolve.details + ")";
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// If our total has reached undefined for some reason, error out now
 | 
				
			||||||
	if (stepSolve.total === undefined) {
 | 
						if (stepSolve.total === undefined) {
 | 
				
			||||||
		throw new Error("UndefinedStep");
 | 
							throw new Error("UndefinedStep");
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
| 
						 | 
					@ -580,6 +713,8 @@ const fullSolver = (conf: (string | number | SolvedStep)[], wrapDetails: boolean
 | 
				
			||||||
	return stepSolve;
 | 
						return stepSolve;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// parseRoll(fullCmd, localPrefix, localPostfix, maximiseRoll, nominalRoll)
 | 
				
			||||||
 | 
					// parseRoll handles converting fullCmd into a computer readable format for processing, and finally executes the solving
 | 
				
			||||||
const parseRoll = (fullCmd: string, localPrefix: string, localPostfix: string, maximiseRoll: boolean, nominalRoll: boolean): SolvedRoll => {
 | 
					const parseRoll = (fullCmd: string, localPrefix: string, localPostfix: string, maximiseRoll: boolean, nominalRoll: boolean): SolvedRoll => {
 | 
				
			||||||
	const returnmsg = {
 | 
						const returnmsg = {
 | 
				
			||||||
		error: false,
 | 
							error: false,
 | 
				
			||||||
| 
						 | 
					@ -589,16 +724,22 @@ const parseRoll = (fullCmd: string, localPrefix: string, localPostfix: string, m
 | 
				
			||||||
		line3: ""
 | 
							line3: ""
 | 
				
			||||||
	};
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Whole function lives in a try-catch to allow safe throwing of errors on purpose
 | 
				
			||||||
	try {
 | 
						try {
 | 
				
			||||||
 | 
							// Split the fullCmd by the command prefix to allow every roll/math op to be handled individually
 | 
				
			||||||
		const sepRolls = fullCmd.split(localPrefix);
 | 
							const sepRolls = fullCmd.split(localPrefix);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		const tempReturnData = [];
 | 
							const tempReturnData = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Loop thru all roll/math ops
 | 
				
			||||||
		for (let i = 0; i < sepRolls.length; i++) {
 | 
							for (let i = 0; i < sepRolls.length; i++) {
 | 
				
			||||||
 | 
								// Split the current iteration on the command postfix to separate the operation to be parsed and the text formatting after the opertaion
 | 
				
			||||||
			const [tempConf, tempFormat] = sepRolls[i].split(localPostfix);
 | 
								const [tempConf, tempFormat] = sepRolls[i].split(localPostfix);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// 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(/([-+()*/%^])/g);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// Verify there are equal numbers of opening and closing parenthesis by adding 1 for opening parens and subtracting 1 for closing parens
 | 
				
			||||||
			let parenCnt = 0;
 | 
								let parenCnt = 0;
 | 
				
			||||||
			mathConf.forEach(e => {
 | 
								mathConf.forEach(e => {
 | 
				
			||||||
				if (e === "(") {
 | 
									if (e === "(") {
 | 
				
			||||||
| 
						 | 
					@ -608,6 +749,7 @@ const parseRoll = (fullCmd: string, localPrefix: string, localPostfix: string, m
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// If the parenCnt is not 0, then we do not have balanced parens and need to error out now
 | 
				
			||||||
			if (parenCnt !== 0) {
 | 
								if (parenCnt !== 0) {
 | 
				
			||||||
				throw new Error("UnbalancedParens");
 | 
									throw new Error("UnbalancedParens");
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
| 
						 | 
					@ -615,17 +757,52 @@ const parseRoll = (fullCmd: string, localPrefix: string, localPostfix: string, m
 | 
				
			||||||
			// Evaluate all rolls into stepSolve format and all numbers into floats
 | 
								// Evaluate all rolls into stepSolve format and all numbers into floats
 | 
				
			||||||
			for (let i = 0; i < mathConf.length; i++) {
 | 
								for (let i = 0; i < mathConf.length; i++) {
 | 
				
			||||||
				if (mathConf[i].toString().length === 0) {
 | 
									if (mathConf[i].toString().length === 0) {
 | 
				
			||||||
 | 
										// If its an empty string, get it out of here
 | 
				
			||||||
					mathConf.splice(i, 1);
 | 
										mathConf.splice(i, 1);
 | 
				
			||||||
					i--;
 | 
										i--;
 | 
				
			||||||
				} else if (mathConf[i] == parseFloat(mathConf[i].toString())) {
 | 
									} else if (mathConf[i] == parseFloat(mathConf[i].toString())) {
 | 
				
			||||||
 | 
										// If its a number, parse the number out
 | 
				
			||||||
					mathConf[i] = parseFloat(mathConf[i].toString());
 | 
										mathConf[i] = parseFloat(mathConf[i].toString());
 | 
				
			||||||
				} else if (/([0123456789])/g.test(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(), maximiseRoll, nominalRoll);
 | 
										mathConf[i] = formatRoll(mathConf[i].toString(), maximiseRoll, nominalRoll);
 | 
				
			||||||
 | 
									} else if (mathConf[i].toString().toLowerCase() === "e") {
 | 
				
			||||||
 | 
										// If the operand is the constant e, create a SolvedStep for it
 | 
				
			||||||
 | 
										mathConf[i] = {
 | 
				
			||||||
 | 
											total: Math.E,
 | 
				
			||||||
 | 
											details: "*e*",
 | 
				
			||||||
 | 
											containsCrit: false,
 | 
				
			||||||
 | 
											containsFail: false
 | 
				
			||||||
 | 
										};
 | 
				
			||||||
 | 
									} else if (mathConf[i].toString().toLowerCase() === "pi") {
 | 
				
			||||||
 | 
										// If the operand is the constant pi, create a SolvedStep for it
 | 
				
			||||||
 | 
										mathConf[i] = {
 | 
				
			||||||
 | 
											total: Math.PI,
 | 
				
			||||||
 | 
											details: "𝜋",
 | 
				
			||||||
 | 
											containsCrit: false,
 | 
				
			||||||
 | 
											containsFail: false
 | 
				
			||||||
 | 
										};
 | 
				
			||||||
 | 
									} else if (mathConf[i].toString().toLowerCase() === "pie") {
 | 
				
			||||||
 | 
										// If the operand is pie, pi*e, create a SolvedStep for e and pi (and the multiplication symbol between them)
 | 
				
			||||||
 | 
										mathConf[i] = {
 | 
				
			||||||
 | 
											total: Math.PI,
 | 
				
			||||||
 | 
											details: "𝜋",
 | 
				
			||||||
 | 
											containsCrit: false,
 | 
				
			||||||
 | 
											containsFail: false
 | 
				
			||||||
 | 
										};
 | 
				
			||||||
 | 
										mathConf.splice((i + 1), 0, ...["*", {
 | 
				
			||||||
 | 
											total: Math.E,
 | 
				
			||||||
 | 
											details: "*e*",
 | 
				
			||||||
 | 
											containsCrit: false,
 | 
				
			||||||
 | 
											containsFail: false
 | 
				
			||||||
 | 
										}]);
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// Now that mathConf is parsed, send it into the solver
 | 
				
			||||||
			const tempSolved = fullSolver(mathConf, false);
 | 
								const tempSolved = fullSolver(mathConf, false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// Push all of this step's solved data into the temp array
 | 
				
			||||||
			tempReturnData.push({
 | 
								tempReturnData.push({
 | 
				
			||||||
				rollTotal: tempSolved.total,
 | 
									rollTotal: tempSolved.total,
 | 
				
			||||||
				rollPostFormat: tempFormat,
 | 
									rollPostFormat: tempFormat,
 | 
				
			||||||
| 
						 | 
					@ -636,14 +813,21 @@ const parseRoll = (fullCmd: string, localPrefix: string, localPostfix: string, m
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Parsing/Solving done, time to format the output for Discord
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Remove any floating spaces from fullCmd
 | 
				
			||||||
		if (fullCmd[fullCmd.length - 1] === " ") {
 | 
							if (fullCmd[fullCmd.length - 1] === " ") {
 | 
				
			||||||
			fullCmd = escapeCharacters(fullCmd.substr(0, (fullCmd.length - 1)), "|");
 | 
								fullCmd = fullCmd.substr(0, (fullCmd.length - 1));
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Escape any | chars in fullCmd to prevent spoilers from acting up
 | 
				
			||||||
 | 
							fullCmd = escapeCharacters(fullCmd, "|");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		let line1 = "";
 | 
							let line1 = "";
 | 
				
			||||||
		let line2 = "";
 | 
							let line2 = "";
 | 
				
			||||||
		let line3 = "";
 | 
							let line3 = "";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// If maximiseRoll or nominalRoll are on, mark the output as such, else use default formatting
 | 
				
			||||||
		if (maximiseRoll) {
 | 
							if (maximiseRoll) {
 | 
				
			||||||
			line1 = " requested the theoretical maximum of: `[[" + fullCmd + "`";
 | 
								line1 = " requested the theoretical maximum of: `[[" + fullCmd + "`";
 | 
				
			||||||
			line2 = "Theoretical Maximum Results: ";
 | 
								line2 = "Theoretical Maximum Results: ";
 | 
				
			||||||
| 
						 | 
					@ -655,9 +839,12 @@ const parseRoll = (fullCmd: string, localPrefix: string, localPostfix: string, m
 | 
				
			||||||
			line2 = "Results: ";
 | 
								line2 = "Results: ";
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Fill out all of the details and results now
 | 
				
			||||||
		tempReturnData.forEach(e => {
 | 
							tempReturnData.forEach(e => {
 | 
				
			||||||
			let preFormat = "";
 | 
								let preFormat = "";
 | 
				
			||||||
			let postFormat = "";
 | 
								let postFormat = "";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// If the roll containted a crit success or fail, set the formatting around it
 | 
				
			||||||
			if (e.containsCrit) {
 | 
								if (e.containsCrit) {
 | 
				
			||||||
				preFormat = "**" + preFormat;
 | 
									preFormat = "**" + preFormat;
 | 
				
			||||||
				postFormat = postFormat + "**";
 | 
									postFormat = postFormat + "**";
 | 
				
			||||||
| 
						 | 
					@ -667,19 +854,26 @@ const parseRoll = (fullCmd: string, localPrefix: string, localPostfix: string, m
 | 
				
			||||||
				postFormat = postFormat + "__";
 | 
									postFormat = postFormat + "__";
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// Populate line2 (the results) and line3 (the details) with their data
 | 
				
			||||||
			line2 += preFormat + e.rollTotal + postFormat + escapeCharacters(e.rollPostFormat, "|*_~`");
 | 
								line2 += preFormat + e.rollTotal + postFormat + escapeCharacters(e.rollPostFormat, "|*_~`");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			line3 += "`" + e.initConfig + "` = " + e.rollDetails + " = " + preFormat + e.rollTotal + postFormat + "\n";
 | 
								line3 += "`" + e.initConfig + "` = " + e.rollDetails + " = " + preFormat + e.rollTotal + postFormat + "\n";
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Fill in the return block
 | 
				
			||||||
		returnmsg.line1 = line1;
 | 
							returnmsg.line1 = line1;
 | 
				
			||||||
		returnmsg.line2 = line2;
 | 
							returnmsg.line2 = line2;
 | 
				
			||||||
		returnmsg.line3 = line3;
 | 
							returnmsg.line3 = line3;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	} catch (solverError) {
 | 
						} catch (solverError) {
 | 
				
			||||||
 | 
							// Welp, the unthinkable happened, we hit an error
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Split on _ for the error messages that have more info than just their name
 | 
				
			||||||
		const [errorName, errorDetails] = solverError.message.split("_");
 | 
							const [errorName, errorDetails] = solverError.message.split("_");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		let errorMsg = "";
 | 
							let errorMsg = "";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Translate the errorName to a specific errorMsg
 | 
				
			||||||
		switch (errorName) {
 | 
							switch (errorName) {
 | 
				
			||||||
			case "YouNeedAD":
 | 
								case "YouNeedAD":
 | 
				
			||||||
				errorMsg = "Formatting Error: Missing die size and count config";
 | 
									errorMsg = "Formatting Error: Missing die size and count config";
 | 
				
			||||||
| 
						 | 
					@ -754,10 +948,11 @@ const parseRoll = (fullCmd: string, localPrefix: string, localPostfix: string, m
 | 
				
			||||||
				break;
 | 
									break;
 | 
				
			||||||
			default:
 | 
								default:
 | 
				
			||||||
				console.error(errorName, errorDetails);
 | 
									console.error(errorName, errorDetails);
 | 
				
			||||||
				errorMsg = "Unhandled Error: " + solverError.message;
 | 
									errorMsg = "Unhandled Error: " + solverError.message + "\nCheck input and try again, if issue persists, please use `[[report` to alert the devs of the issue";
 | 
				
			||||||
				break;
 | 
									break;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Fill in the return block
 | 
				
			||||||
		returnmsg.error = true;
 | 
							returnmsg.error = true;
 | 
				
			||||||
		returnmsg.errorMsg = errorMsg;
 | 
							returnmsg.errorMsg = errorMsg;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										76
									
								
								src/utils.ts
								
								
								
								
							
							
						
						
									
										76
									
								
								src/utils.ts
								
								
								
								
							| 
						 | 
					@ -1,18 +1,26 @@
 | 
				
			||||||
import { Message } from "https://deno.land/x/discordeno@10.0.0/mod.ts";
 | 
					import { Message } from "https://deno.land/x/discordeno@10.0.0/mod.ts";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// split2k(longMessage) returns shortMessage[]
 | 
				
			||||||
 | 
					// split2k takes a long string in and cuts it into shorter strings to be sent in Discord
 | 
				
			||||||
const split2k = (chunk: string): string[] => {
 | 
					const split2k = (chunk: string): string[] => {
 | 
				
			||||||
 | 
						// Replace any malformed newline characters
 | 
				
			||||||
	chunk = chunk.replace(/\\n/g, "\n");
 | 
						chunk = chunk.replace(/\\n/g, "\n");
 | 
				
			||||||
	const bites = [];
 | 
						const bites = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// While there is more characters than allowed to be sent in discord
 | 
				
			||||||
	while (chunk.length > 2000) {
 | 
						while (chunk.length > 2000) {
 | 
				
			||||||
		// take 2001 chars to see if word magically ends on char 2000
 | 
							// Take 2001 chars to see if word magically ends on char 2000
 | 
				
			||||||
		let bite = chunk.substr(0, 2001);
 | 
							let bite = chunk.substr(0, 2001);
 | 
				
			||||||
		const etib = bite.split("").reverse().join("");
 | 
							const lastI = bite.lastIndexOf(" ");
 | 
				
			||||||
		const lastI = etib.indexOf(" "); // might be able to do lastIndexOf now
 | 
							if (lastI < 2000) {
 | 
				
			||||||
		if (lastI > 0) {
 | 
								// If there is a final word before the 2000 split point, split right after that word
 | 
				
			||||||
			bite = bite.substr(0, 2000 - lastI);
 | 
								bite = bite.substr(0, lastI);
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
 | 
								// Else cut exactly 2000 characters
 | 
				
			||||||
			bite = bite.substr(0, 2000);
 | 
								bite = bite.substr(0, 2000);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Push and remove the bite taken out of the chunk
 | 
				
			||||||
		bites.push(bite);
 | 
							bites.push(bite);
 | 
				
			||||||
		chunk = chunk.slice(bite.length);
 | 
							chunk = chunk.slice(bite.length);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
| 
						 | 
					@ -22,7 +30,9 @@ const split2k = (chunk: string): string[] => {
 | 
				
			||||||
	return bites;
 | 
						return bites;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const ask = async (question: string, stdin = Deno.stdin, stdout = Deno.stdout) => {
 | 
					// ask(prompt) returns string
 | 
				
			||||||
 | 
					// ask prompts the user at command line for message
 | 
				
			||||||
 | 
					const ask = async (question: string, stdin = Deno.stdin, stdout = Deno.stdout): Promise<string> => {
 | 
				
			||||||
	const buf = new Uint8Array(1024);
 | 
						const buf = new Uint8Array(1024);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Write question to console
 | 
						// Write question to console
 | 
				
			||||||
| 
						 | 
					@ -35,25 +45,44 @@ const ask = async (question: string, stdin = Deno.stdin, stdout = Deno.stdout) =
 | 
				
			||||||
	return answer.trim();
 | 
						return answer.trim();
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// cmdPrompt(logChannel, botName, sendMessage) returns nothing
 | 
				
			||||||
 | 
					// cmdPrompt creates an interactive CLI for the bot, commands can vary
 | 
				
			||||||
const cmdPrompt = async (logChannel: string, botName: string, sendMessage: (c: string, m: string) => Promise<Message>): Promise<void> => {
 | 
					const cmdPrompt = async (logChannel: string, botName: string, sendMessage: (c: string, m: string) => Promise<Message>): Promise<void> => {
 | 
				
			||||||
	let done = false;
 | 
						let done = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	while (!done) {
 | 
						while (!done) {
 | 
				
			||||||
 | 
							// Get a command and its args
 | 
				
			||||||
		const fullCmd = await ask("cmd> ");
 | 
							const fullCmd = await ask("cmd> ");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Split the args off of the command and prep the command
 | 
				
			||||||
		const args = fullCmd.split(" ");
 | 
							const args = fullCmd.split(" ");
 | 
				
			||||||
		const command = args.shift()?.toLowerCase();
 | 
							const command = args.shift()?.toLowerCase();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// All commands below here
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// exit or e
 | 
				
			||||||
 | 
							// Fully closes the bot
 | 
				
			||||||
		if (command === "exit" || command === "e") {
 | 
							if (command === "exit" || command === "e") {
 | 
				
			||||||
			console.log(`${botName} Shutting down.\n\nGoodbye.`);
 | 
								console.log(`${botName} Shutting down.\n\nGoodbye.`);
 | 
				
			||||||
			done = true;
 | 
								done = true;
 | 
				
			||||||
			Deno.exit(0);
 | 
								Deno.exit(0);
 | 
				
			||||||
		} else if (command === "stop") {
 | 
							}
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							// stop
 | 
				
			||||||
 | 
							// Closes the CLI only, leaving the bot running truly headless
 | 
				
			||||||
 | 
							else if (command === "stop") {
 | 
				
			||||||
			console.log(`Closing ${botName} CLI.  Bot will continue to run.\n\nGoodbye.`);
 | 
								console.log(`Closing ${botName} CLI.  Bot will continue to run.\n\nGoodbye.`);
 | 
				
			||||||
			done = true;
 | 
								done = true;
 | 
				
			||||||
		} else if (command === "m") {
 | 
							}
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							// m [channel] [message]
 | 
				
			||||||
 | 
							// Sends [message] to specified [channel]
 | 
				
			||||||
 | 
							else if (command === "m") {
 | 
				
			||||||
			try {
 | 
								try {
 | 
				
			||||||
				const channelID = args.shift() || "";
 | 
									const channelID = args.shift() || "";
 | 
				
			||||||
				const message = args.join(" ");
 | 
									const message = args.join(" ");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									// Utilize the split2k function to ensure a message over 2000 chars is not sent
 | 
				
			||||||
				const messages = split2k(message);
 | 
									const messages = split2k(message);
 | 
				
			||||||
				for (let i = 0; i < messages.length; i++) {
 | 
									for (let i = 0; i < messages.length; i++) {
 | 
				
			||||||
					sendMessage(channelID, messages[i]).catch(reason => {
 | 
										sendMessage(channelID, messages[i]).catch(reason => {
 | 
				
			||||||
| 
						 | 
					@ -64,27 +93,44 @@ const cmdPrompt = async (logChannel: string, botName: string, sendMessage: (c: s
 | 
				
			||||||
			catch (e) {
 | 
								catch (e) {
 | 
				
			||||||
				console.error(e);
 | 
									console.error(e);
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		} else if (command === "ml") {
 | 
							}
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							// ml [message]
 | 
				
			||||||
 | 
							// Sends a message to the specified log channel
 | 
				
			||||||
 | 
							else if (command === "ml") {
 | 
				
			||||||
			const message = args.join(" ");
 | 
								const message = args.join(" ");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// Utilize the split2k function to ensure a message over 2000 chars is not sent
 | 
				
			||||||
			const messages = split2k(message);
 | 
								const messages = split2k(message);
 | 
				
			||||||
			for (let i = 0; i < messages.length; i++) {
 | 
								for (let i = 0; i < messages.length; i++) {
 | 
				
			||||||
				sendMessage(logChannel, messages[i]).catch(reason => {
 | 
									sendMessage(logChannel, messages[i]).catch(reason => {
 | 
				
			||||||
					console.error(reason);
 | 
										console.error(reason);
 | 
				
			||||||
				});
 | 
									});
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		} else if (command === "help" || command === "h") {
 | 
							}
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							// help or h
 | 
				
			||||||
 | 
							// Shows a basic help menu
 | 
				
			||||||
 | 
							else if (command === "help" || command === "h") {
 | 
				
			||||||
			console.log(`${botName} CLI Help:\n\nAvailable Commands:\n  exit - closes bot\n  stop - closes the CLI\n  m [ChannelID] [messgae] - sends message to specific ChannelID as the bot\n  ml [message] sends a message to the specified botlog\n  help - this message`);
 | 
								console.log(`${botName} CLI Help:\n\nAvailable Commands:\n  exit - closes bot\n  stop - closes the CLI\n  m [ChannelID] [messgae] - sends message to specific ChannelID as the bot\n  ml [message] sends a message to the specified botlog\n  help - this message`);
 | 
				
			||||||
		} else {
 | 
							}
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							// Unhandled commands die here
 | 
				
			||||||
 | 
							else {
 | 
				
			||||||
			console.log("undefined command");
 | 
								console.log("undefined command");
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const sendIndirectMessage = async (message: Message, messageContent: string, sendMessage: (c: string, m: string) => Promise<Message>, sendDirectMessage: (c: string, m: string) => Promise<Message>): Promise<Message> => {
 | 
					// sendIndirectMessage(originalMessage, messageContent, sendMessage, sendDirectMessage) returns Message
 | 
				
			||||||
	if (message.guildID === "") {
 | 
					// sendIndirectMessage determines if the message needs to be sent as a direct message or as a normal message
 | 
				
			||||||
		return await sendDirectMessage(message.author.id, messageContent);
 | 
					const sendIndirectMessage = async (originalMessage: Message, messageContent: string, sendMessage: (c: string, m: string) => Promise<Message>, sendDirectMessage: (c: string, m: string) => Promise<Message>): Promise<Message> => {
 | 
				
			||||||
 | 
						if (originalMessage.guildID === "") {
 | 
				
			||||||
 | 
							// guildID was empty, meaning the original message was sent as a DM
 | 
				
			||||||
 | 
							return await sendDirectMessage(originalMessage.author.id, messageContent);
 | 
				
			||||||
	} else {
 | 
						} else {
 | 
				
			||||||
		return await sendMessage(message.channelID, messageContent);
 | 
							// guildID was not empty, meaning the original message was sent in a server
 | 
				
			||||||
 | 
							return await sendMessage(originalMessage.channelID, messageContent);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue