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",
|
||||||
|
|
52
mod.ts
52
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,26 +191,40 @@ 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) {
|
||||||
returnText += "\nDetails suppressed by -nd flag.";
|
returnText += "\nDetails suppressed by -nd flag.";
|
||||||
} else {
|
} else {
|
||||||
returnText += "\nDetails:\n" + modifiers.spoiler + returnmsg.line3 + modifiers.spoiler;
|
returnText += "\nDetails:\n" + modifiers.spoiler + returnmsg.line3 + modifiers.spoiler;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the roll was a GM roll, send DMs to all the GMs
|
||||||
if (modifiers.gmRoll) {
|
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({
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
utils.cmdPrompt(config.logChannel, config.name, sendMessage);
|
// Start up the command prompt for debug usage
|
||||||
|
if (DEBUG) {
|
||||||
|
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,
|
||||||
|
|
265
src/solver.ts
265
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
|
||||||
* ra [OPT] - rerolls any rolls that match a, r3 will reroll any dice that land on 3, throwing out old rolls
|
* dhz [OPT] - drops the highest z dice, cannot be used with kz
|
||||||
* csq [OPT] - changes crit score to q, where q can be a single number or a range formatted as q-u
|
* klz [OPT] - keeps the lowest z dice, cannot be used with dz
|
||||||
* ! [OPT] - exploding, rolls another dy for every crit roll
|
* ra [OPT] - rerolls any rolls that match a, r3 will reroll any dice that land on 3, throwing out old rolls
|
||||||
|
* 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
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
@ -233,7 +286,7 @@ const roll = (rollStr: string, maximiseRoll: boolean, nominalRoll: boolean): Rol
|
||||||
* critHit: false,
|
* critHit: false,
|
||||||
* critFail: false
|
* critFail: false
|
||||||
* }
|
* }
|
||||||
*
|
*
|
||||||
* Each of these is defined as following:
|
* Each of these is defined as following:
|
||||||
* {
|
* {
|
||||||
* origidx: The original index of the roll
|
* origidx: The original index of the roll
|
||||||
|
@ -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,44 +406,50 @@ const roll = (rollStr: string, maximiseRoll: boolean, nominalRoll: boolean): Rol
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let rerollCount = 0;
|
// If we need to handle the drop/keep flags
|
||||||
for (let i = 0; i < rollSet.length; i++) {
|
if (dkdkCnt > 0) {
|
||||||
rollSet[i].origidx = i;
|
// Count how many rerolled dice there are if the reroll flag was on
|
||||||
|
let rerollCount = 0;
|
||||||
|
if (rollConf.reroll.on) {
|
||||||
|
for (let i = 0; i < rollSet.length; i++) {
|
||||||
|
rollSet[i].origidx = i;
|
||||||
|
|
||||||
if (rollSet[i].rerolled) {
|
if (rollSet[i].rerolled) {
|
||||||
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