V1.3.2 Released

Mostly just minor bug fixes, along with a new -o flag for the roll command.

config.example.ts - Refactored help command to provide more detail
mod.ts - Added new rollhelp command, added new order flag for the roll command, adjusted method of handling messages of over 2k characters to attach a .txt file instead of spamming the user, API got order flag aswell, along with the 2k char adjustment
README.md - Spelling corrected, added documentation for roll command flags
solver.d.ts - Added new needed type
solver.ts - Add new order feature, minor bug fix to correct dropping rolls correctly
utils.ts - Added support for new 2k char handling
www/* - Built website to showcase bot
This commit is contained in:
Ean Milligan (Bastion) 2021-01-27 02:35:46 -05:00
parent 0476678dbf
commit 85d9a5b2ee
14 changed files with 532 additions and 101 deletions

View File

@ -4,5 +4,9 @@
"deno.unstable": true,
"deno.import_intellisense_origins": {
"https://deno.land": true
}
},
"spellright.language": [
"en"
],
"spellright.documentTypes": []
}

View File

@ -38,7 +38,7 @@ The Artificer comes with a few supplemental commands to the main rolling command
* It looks a little complicated at first, but if you are familiar with the [Roll20 formatting](https://roll20.zendesk.com/hc/en-us/articles/360037773133-Dice-Reference), this will no different.
* Any math (limited to exponentials, multiplication, division, modulus, addition, and subtraction) will be correctly handled in PEMDAS order, so use parenthesis as needed.
* PI and e are available for use.
* Paramaters for rolling:
* Parameters for rolling:
| Paramater | Required? | Repeatable? | Description |
|---------------|-------------|---------------|--------------------------------------------------------------------------------------------------|
@ -57,13 +57,20 @@ The Artificer comes with a few supplemental commands to the main rolling command
| cf>q | Optional | Yes | changes crit fail to be greater than or equal to q |
| ! | Optional | No | exploding, rolls another dy for every crit roll |
* If the paramater is Required, it must be provided at all times.
* If the paramater is Repeatable, it may occur multiple times in the roll configuration.
* If the parameter is Required, it must be provided at all times.
* If the parameter is Repeatable, it may occur multiple times in the roll configuration.
* Examples:
* `[[4d20]]` will roll 4 d20 dice and add them together.
* `[[4d20r1!]]` will roll 4 d20 dice, rerolling any dice that land on 1, and repeatedly rolling a new d20 for any critical success rolled.
* `[[d20/40]]` will roll a d20 die and divide it by 40.
* `[[((d20+20) - 10) / 5]]` will roll a d20, add 20 to that roll, subtract off 10, and finally divide by 5.
* This command also has some useful flags that can used. These flags simply need to be placed after all rolls in the message:
* `-nd` - No Details - Suppresses all details of the requested roll
* `-s` - Spoiler - Spoilers all details of the requested roll
* `-m` - Maximize Roll - Rolls the theoretical maximum roll, cannot be used with -n
* `-n` - Nominal Roll - Rolls the theoretical nominal roll, cannot be used with -m
* `-gm @user1 @user2 ... @usern` - GM Roll - Rolls the requested roll in GM mode, suppressing all publicly shown results and details and sending the results directly to the specified GMs
* `-o a` or `-o d` - Order Roll - Rolls the requested roll and orders the results in the requested direction
## The Artificer API
The Artificer features an API that allows authenticated users to roll dice into Discord from third party applications (such as Excel macros). The API has a couple endpoints exposed to all authenticated users allowing management of channels that your API key can send rolls to. APIs requiring administrative access are not listed below.
@ -84,6 +91,13 @@ Available Endpoints:
* `rollstr` - A roll string formatted identically to the roll command detailed in the "Available Commands" section.
* `channel` - The Discord Channel ID that the bot is to send the results into.
* `user` - Your Discord User ID.
* Optional query parameters (these parameters do not require values unless specified):
* `nd` - No Details - Suppresses all details of the requested roll.
* `s` - Spoiler - Spoilers all details of the requested roll.
* `m` - Maximize Roll - Rolls the theoretical maximum roll, cannot be used with Nominal roll.
* `n` - Nominal Roll - Rolls the theoretical nominal roll, cannot be used with Maximise roll.
* `gm` - GM Roll - Rolls the requested roll in GM mode, suppressing all publicly shown results and details and sending the results directly to the specified GMs. Takes a comma separated list of Discord User IDs.
* `o` - Order Roll - Rolls the requested roll and orders the results in the requested direction. Takes a single character: `a` or `d`.
* Returns:
* `200` - OK - Results of the roll should be found in Discord, but also are returned as a string via the API.
* `/api/channel`
@ -118,7 +132,7 @@ If you run into any errors or problems with the bot, or think you have a good id
## Self Hosting The Artificer
The Artificer was built on Deno `v1.6.3` using Discodeno `v10.0.0`. If you choose to run this yourself, you will need to rename `config.example.ts` to `config.ts` and edit some values. You will need to create a new [Discord Application](https://discord.com/developers/applications) and copy the newly generated token into the `"token"` key. If you want to utilize some of the bots dev features, you will need to fill in the keys `"logChannel"` and `"reportChannel"` with text channel IDs and `"devServer"` with a guild ID.
You will also need to install and setup a MySQL database with a user for the bot to use to add/modify the database. This user must have the "DB Manager" admin rights and "REFERENCES" Global Privalages. Once the DB is installed and a user is setup, run the provided `initDB.ts` to create the schema and tables.
You will also need to install and setup a MySQL database with a user for the bot to use to add/modify the database. This user must have the "DB Manager" admin rights and "REFERENCES" Global Privileges. Once the DB is installed and a user is setup, run the provided `initDB.ts` to create the schema and tables.
Once everything is set up, starting the bot can simply be done with `deno run --allow-net .\mod.ts`.

View File

@ -1,6 +1,6 @@
export const config = {
"name": "The Artificer", // Name of the bot
"version": "1.3.1", // Version of the bot
"version": "1.3.2", // Version of the bot
"token": "the_bot_token", // Discord API Token for this bot
"prefix": "[[", // Prefix for all commands
"postfix": "]]", // Postfix for rolling command
@ -29,30 +29,48 @@ export const config = {
"```",
"__**Commands:**__",
"```",
"[[? - This Command",
"[[ping - Pings the bot to check connectivity",
"[[version - Prints the bots version",
"[[popcat - Popcat",
"[[report [text] - Report a command that failed to run",
"[[stats - Statistics on the bot",
"[[? - This Command",
"[[rollhelp or ?? - Details on how to use the roll command, listed as [[xdy...]] below",
"[[ping - Pings the bot to check connectivity",
"[[version - Prints the bots version",
"[[popcat - Popcat",
"[[report [text] - Report a command that failed to run",
"[[stats - Statistics on the bot",
"[[xdydzracsq!]] ... - Rolls all configs requested, you may repeat the command multiple times in the same message (just ensure you close each roll with ]]), run [[?? for more details",
"```"
],
"rollhelp": [ // Array of strings that makes up the rollhelp command, placed here to keep source code cleaner
"```fix",
"The Artificer Roll Command Details",
"```",
"```",
"[[xdydzracsq!]] ... - Rolls all configs requested, you may repeat the command multiple times in the same message (just ensure you close each roll with ]])",
"* x [OPT] - number of dice to roll, if omitted, 1 is used",
"* dy [REQ] - size of dice to roll, d20 = 20 sided die",
"* dz || dlz [OPT] - drops the lowest z dice, cannot be used with kz",
"* 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",
"* 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",
"* x [OPT] - number of dice to roll, if omitted, 1 is used",
"* dy [REQ] - size of dice to roll, d20 = 20 sided die",
"* dz or dlz [OPT] - drops the lowest z dice, cannot be used with kz",
"* kz or 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",
"* csq or 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 or 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",
"*",
"* This command also can fully solve math equations (containing exponentials, multiplication, division, modulus, addition, and subtraction)",
"* Parenthesis are also fully supported, so use as much as needed",
"* This command also can fully solve math equations with parenthesis",
"*",
"* This command also has some useful flags that can used. These flags simply need to be placed after all rolls in the message:",
" * -nd No Details - Suppresses all details of the requested roll",
" * -s Spoiler - Spoilers all details of the requested roll",
" * -m Maximize Roll - Rolls the theoretical maximum roll, cannot be used with -n",
" * -n Nominal Roll - Rolls the theoretical nominal roll, cannot be used with -m",
" * -gm @user1 @user2 @usern",
" * GM Roll - Rolls the requested roll in GM mode, suppressing all publicly shown results and details and sending the results directly to the specified GMs",
" * -o a or -o d",
" * Order Roll - Rolls the requested roll and orders the results in the requested direction",
"```"
],
"emojis": [ // Array of objects containing all emojis that the bot can send on your behalf, empty this array if you don't want any of them

168
mod.ts
View File

@ -13,7 +13,8 @@ import {
startBot, editBotsStatus,
Intents, StatusTypes, ActivityType,
Message, Guild, sendMessage, sendDirectMessage,
cache
cache,
MessageContent
} from "https://deno.land/x/discordeno@10.0.0/mod.ts";
import { serve } from "https://deno.land/std@0.83.0/http/server.ts";
@ -95,6 +96,14 @@ startBot({
});
}
// [[rollhelp or [[rh or [[hr
// Help command specifically for the roll command
else if (command === "rollhelp" || command === "rh" || command === "hr" || command === "??") {
utils.sendIndirectMessage(message, config.rollhelp.join("\n"), sendMessage, sendDirectMessage).catch(err => {
console.error("Failed to send message 21", message, err);
});
}
// [[help or [[h or [[?
// Help command, prints from help file
else if (command === "help" || command === "h" || command === "?") {
@ -153,7 +162,8 @@ startBot({
maxRoll: false,
nominalRoll: false,
gmRoll: false,
gms: <string[]>[]
gms: <string[]>[],
order: ""
};
// Check if any of the args are command flags and pull those out into the modifiers object
@ -205,6 +215,27 @@ startBot({
return;
}
args.splice(i, 1);
i--;
break;
case "-o":
args.splice(i, 1);
if (args[i].toLowerCase() !== "d" && args[i].toLowerCase() !== "a") {
// If -o is on and asc or desc was not specified, error out
m.edit("Error: Must specifiy a or d to order the rolls ascending or descending");
if (config.logRolls) {
// If enabled, log rolls so we can verify the bots math
dbClient.execute("INSERT INTO roll_log(input,result,resultid,api,error) values(?,?,?,0,1)", [originalCommand, "NoOrderFound", m.id]).catch(e => {
console.log("Failed to insert into database 05", e);
});
}
return;
}
modifiers.order = args[i].toLowerCase();
args.splice(i, 1);
i--;
break;
@ -228,7 +259,7 @@ startBot({
// 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, errorCode: "EmptyMessage", errorMsg: "Error: Empty message", line1: "", line2: "", line3: "" };
const returnmsg = solver.parseRoll(rollCmd, config.prefix, config.postfix, modifiers.maxRoll, modifiers.nominalRoll, modifiers.order) || { error: true, errorCode: "EmptyMessage", errorMsg: "Error: Empty message", line1: "", line2: "", line3: "" };
let returnText = "";
@ -262,14 +293,16 @@ startBot({
// 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 => {
const msgs = utils.split2k(returnText);
const failedDMs = <string[]>[];
for (let i = 0; ((failedDMs.indexOf(e) === -1) && (i < msgs.length)); i++) {
await sendDirectMessage(e.substr(2, (e.length - 3)), msgs[i]).catch(() => {
failedDMs.push(e);
utils.sendIndirectMessage(message, "WARNING: " + e + " could not be messaged. If this issue persists, make sure direct messages are allowed from this server.", sendMessage, sendDirectMessage);
});
}
// If its too big, collapse it into a .txt file and send that instead.
const b = await new Blob([returnText as BlobPart], {"type": "text"});
// Update return text
returnText = "<@" + message.author.id + ">" + returnmsg.line1 + "\n" + returnmsg.line2 + "\nFull details have been attached to this messaged as a `.txt` file for verification purposes.";
// Attempt to DM the GMs and send a warning if it could not DM a GM
await sendDirectMessage(e.substr(2, (e.length - 3)), {"content": returnText, "file": {"blob": b, "name": "rollDetails.txt"}}).catch(() => {
utils.sendIndirectMessage(message, "WARNING: " + e + " could not be messaged. If this issue persists, make sure direct messages are allowed from this server.", sendMessage, sendDirectMessage);
});
});
// Finally send the text
@ -284,26 +317,21 @@ startBot({
} else {
// When not a GM roll, make sure the message is not too big
if (returnText.length > 2000) {
// If its too big, attempt to DM details to the roller
const msgs = utils.split2k(returnText);
let failed = false;
for (let i = 0; (!failed && (i < msgs.length)); i++) {
await sendDirectMessage(message.author.id, msgs[i]).catch(() => {
failed = true;
});
}
// If its too big, collapse it into a .txt file and send that instead.
const b = await new Blob([returnText as BlobPart], {"type": "text"});
// If DM fails to send, alert roller of the failure, else handle normally
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.";
} else {
returnText = "<@" + message.author.id + ">" + returnmsg.line1 + "\n" + returnmsg.line2 + "\nDetails have been ommitted from this message for being over 2000 characters. Full details have been messaged to <@" + message.author.id + "> for verification purposes.";
}
// Update return text
returnText = "<@" + message.author.id + ">" + returnmsg.line1 + "\n" + returnmsg.line2 + "\nDetails have been ommitted from this message for being over 2000 characters. Full details have been attached to this messaged as a `.txt` file for verification purposes.";
// Remove the original message to send new one with attachment
m.delete();
await utils.sendIndirectMessage(message, {"content": returnText, "file": {"blob": b, "name": "rollDetails.txt"}}, sendMessage, sendDirectMessage);
} else {
// Finally send the text
m.edit(returnText);
}
// Finally send the text
m.edit(returnText);
if (config.logRolls) {
// If enabled, log rolls so we can verify the bots math
dbClient.execute("INSERT INTO roll_log(input,result,resultid,api,error) values(?,?,?,0,0)", [originalCommand, returnText, m.id]).catch(e => {
@ -682,11 +710,22 @@ if (config.api.enable) {
break;
}
if (query.has("o") && (query.get("o")?.toLowerCase() !== "d" && query.get("o")?.toLowerCase() !== "a")) {
// Alert API user that they messed up
request.respond({ status: Status.BadRequest, body: STATUS_TEXT.get(Status.BadRequest) });
// Always log API rolls for abuse detection
dbClient.execute("INSERT INTO roll_log(input,result,resultid,api,error) values(?,?,?,1,1)", [originalCommand, "BadOrder", null]).catch(e => {
console.log("Failed to insert into database 10", e);
});
break;
}
// Clip off the leading prefix. API calls must be formatted with a prefix at the start to match how commands are sent in Discord
rollCmd = rollCmd.substr(rollCmd.indexOf(config.prefix) + 2).replace(/%20/g, " ");
// Parse the roll and get the return text
const returnmsg = solver.parseRoll(rollCmd, config.prefix, config.postfix, query.has("m"), query.has("n"));
const returnmsg = solver.parseRoll(rollCmd, config.prefix, config.postfix, query.has("m"), query.has("n"), query.has("o") ? (query.get("o")?.toLowerCase() || "") : "");
// Alert users why this message just appeared and how they can report abues pf this feature
const apiPrefix = "The following roll was conducted using my built in API. If someone in this channel did not request this roll, please report API abuse here: <" + config.api.supportURL + ">\n\n";
@ -754,26 +793,28 @@ if (config.api.enable) {
// And message the full details to each of the GMs, alerting roller of every GM that could not be messaged
gms.forEach(async e => {
const msgs = utils.split2k(returnText);
const failedDMs = <string[]>[];
for (let i = 0; ((failedDMs.indexOf(e) === -1) && (i < msgs.length)); i++) {
await sendDirectMessage(e, msgs[i]).catch(async () => {
failedDMs.push(e);
const failedSend = "WARNING: <@" + e + "> could not be messaged. If this issue persists, make sure direct messages are allowed from this server."
// Send the return message as a DM or normal message depening on if the channel is set
if ((query.get("channel") || "").length > 0) {
m = await sendMessage(query.get("channel") || "", failedSend).catch(() => {
request.respond({ status: Status.InternalServerError, body: "Message 10 failed to send." });
errorOut = true;
});
} else {
m = await sendDirectMessage(query.get("user") || "", failedSend).catch(() => {
request.respond({ status: Status.InternalServerError, body: "Message 11 failed to send." });
errorOut = true;
});
}
});
}
// If its too big, collapse it into a .txt file and send that instead.
const b = await new Blob([returnText as BlobPart], {"type": "text"});
// Update return text
returnText = apiPrefix + "<@" + query.get("user") + ">" + returnmsg.line1 + "\n" + returnmsg.line2 + "\nFull details have been attached to this messaged as a `.txt` file for verification purposes.";
// Attempt to DM the GMs and send a warning if it could not DM a GM
await sendDirectMessage(e, {"content": returnText, "file": {"blob": b, "name": "rollDetails.txt"}}).catch(async () => {
const failedSend = "WARNING: <@" + e + "> could not be messaged. If this issue persists, make sure direct messages are allowed from this server."
// Send the return message as a DM or normal message depening on if the channel is set
if ((query.get("channel") || "").length > 0) {
m = await sendMessage(query.get("channel") || "", failedSend).catch(() => {
request.respond({ status: Status.InternalServerError, body: "Message 10 failed to send." });
errorOut = true;
});
} else {
m = await sendDirectMessage(query.get("user") || "", failedSend).catch(() => {
request.respond({ status: Status.InternalServerError, body: "Message 11 failed to send." });
errorOut = true;
});
}
});
});
// Always log API rolls for abuse detection
@ -789,33 +830,30 @@ if (config.api.enable) {
break;
}
} else {
const newMessage : MessageContent= {};
newMessage.content = returnText;
// When not a GM roll, make sure the message is not too big
if (returnText.length > 2000) {
// If its too big, attempt to DM details to the roller
const msgs = utils.split2k(returnText);
let failed = false;
for (let i = 0; (!failed && (i < msgs.length)); i++) {
await sendDirectMessage(query.get("user") || "", msgs[i]).catch(() => {
failed = true;
});
}
// If its too big, collapse it into a .txt file and send that instead.
const b = await new Blob([returnText as BlobPart], {"type": "text"});
// If DM fails to send, alert roller of the failure, else handle normally
if (failed) {
returnText = apiPrefix + "<@" + query.get("user") + ">" + returnmsg.line1 + "\n" + returnmsg.line2 + "\nDetails have been ommitted from this message for being over 2000 characters. WARNING: <@" + query.get("user") + "> could **NOT** be messaged full details for verification purposes.";
} else {
returnText = apiPrefix + "<@" + query.get("user") + ">" + returnmsg.line1 + "\n" + returnmsg.line2 + "\nDetails have been ommitted from this message for being over 2000 characters. Full details have been messaged to <@" + query.get("user") + "> for verification purposes.";
}
// Update return text
returnText = "<@" + query.get("user") + ">" + returnmsg.line1 + "\n" + returnmsg.line2 + "\nDetails have been ommitted from this message for being over 2000 characters. Full details have been attached to this messaged as a `.txt` file for verification purposes.";
// Set info into the newMessage
newMessage.content = returnText;
newMessage.file = {"blob": b, "name": "rollDetails.txt"};
}
// Send the return message as a DM or normal message depening on if the channel is set
if ((query.get("channel") || "").length > 0) {
m = await sendMessage(query.get("channel") || "", returnText).catch(() => {
m = await sendMessage(query.get("channel") || "", newMessage).catch(() => {
request.respond({ status: Status.InternalServerError, body: "Message 20 failed to send." });
errorOut = true;
});
} else {
m = await sendDirectMessage(query.get("user") || "", returnText).catch(() => {
m = await sendDirectMessage(query.get("user") || "", newMessage).catch(() => {
request.respond({ status: Status.InternalServerError, body: "Message 21 failed to send." });
errorOut = true;
});

10
src/solver.d.ts vendored
View File

@ -19,6 +19,16 @@ export type SolvedStep = {
containsFail: boolean
};
// ReturnData is the temporary internal type used before getting turned into SolvedRoll
export type ReturnData = {
rollTotal: number,
rollPostFormat: string,
rollDetails: string,
containsCrit: boolean,
containsFail: boolean,
initConfig: string
};
// SolvedRoll is the complete solved and formatted roll, or the error said roll created
export type SolvedRoll = {
error: boolean,

View File

@ -4,7 +4,7 @@
* December 21, 2020
*/
import { RollSet, SolvedStep, SolvedRoll } from "./solver.d.ts";
import { RollSet, SolvedStep, SolvedRoll, ReturnData } 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
@ -30,6 +30,18 @@ const compareRolls = (a: RollSet, b: RollSet): number => {
return 0;
};
// compareTotalRolls(a, b) returns -1|0|1
// compareTotalRolls is used to order an array of RollSets by RollSet.roll
const compareTotalRolls = (a: ReturnData, b: ReturnData): number => {
if (a.rollTotal < b.rollTotal) {
return -1;
}
if (a.rollTotal > b.rollTotal) {
return 1;
}
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 => {
@ -134,7 +146,7 @@ const roll = (rollStr: string, maximiseRoll: boolean, nominalRoll: boolean): Rol
}
// Rejoin all remaining parts
let remains = dpts.join("");
let remains = dpts.join("d");
// Get the die size out of the remains and into the rollConf
rollConf.dieSize = parseInt(remains.slice(0, afterDieIdx));
remains = remains.slice(afterDieIdx);
@ -721,7 +733,7 @@ const fullSolver = (conf: (string | number | SolvedStep)[], wrapDetails: boolean
// 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, order: string): SolvedRoll => {
const returnmsg = {
error: false,
errorMsg: "",
@ -736,7 +748,7 @@ const parseRoll = (fullCmd: string, localPrefix: string, localPostfix: string, m
// Split the fullCmd by the command prefix to allow every roll/math op to be handled individually
const sepRolls = fullCmd.split(localPrefix);
const tempReturnData = [];
const tempReturnData: ReturnData[] = [];
// Loop thru all roll/math ops
for (let i = 0; i < sepRolls.length; i++) {
@ -841,6 +853,15 @@ const parseRoll = (fullCmd: string, localPrefix: string, localPostfix: string, m
} else if (nominalRoll) {
line1 = " requested the theoretical nominal of: `[[" + fullCmd + "`";
line2 = "Theoretical Nominal Results: ";
} else if (order === "a") {
line1 = " requested the following rolls to be ordered from least to greatest: `[[" + fullCmd + "`";
line2 = "Results: ";
tempReturnData.sort(compareTotalRolls);
} else if (order === "d") {
line1 = " requested the following rolls to be ordered from greatest to least: `[[" + fullCmd + "`";
line2 = "Results: ";
tempReturnData.sort(compareTotalRolls);
tempReturnData.reverse();
} else {
line1 = " rolled: `[[" + fullCmd + "`";
line2 = "Results: ";
@ -862,11 +883,23 @@ const parseRoll = (fullCmd: string, localPrefix: string, localPostfix: string, m
}
// Populate line2 (the results) and line3 (the details) with their data
line2 += preFormat + e.rollTotal + postFormat + escapeCharacters(e.rollPostFormat, "|*_~`") + " ";
if (order === "") {
line2 += preFormat + e.rollTotal + postFormat + escapeCharacters(e.rollPostFormat, "|*_~`");
} else {
// If order is on, turn rolls into csv without formatting
line2 += preFormat + e.rollTotal + postFormat + ", ";
}
line2 = line2.replace(/\*\*\*\*/g, "** **").replace(/____/g, "__ __").replace(/~~~~/g, "~~ ~~");
line3 += "`" + e.initConfig + "` = " + e.rollDetails + " = " + preFormat + e.rollTotal + postFormat + "\n";
});
// If order is on, remove trailing ", "
if (order !== "") {
line2 = line2.substr(0, (line2.length - 2));
}
// Fill in the return block
returnmsg.line1 = line1;
returnmsg.line2 = line2;
@ -951,7 +984,7 @@ const parseRoll = (fullCmd: string, localPrefix: string, localPostfix: string, m
errorMsg = "Error: One or more operands reached NaN, check input";
break;
case "UndefinedStep":
errorMsg = "Error: Roll became undefined, one ore more operands are not a roll or a number, check input";
errorMsg = "Error: Roll became undefined, one or more operands are not a roll or a number, check input";
break;
default:
console.error(errorName, errorDetails);

View File

@ -4,7 +4,7 @@
* December 21, 2020
*/
import { Message } from "https://deno.land/x/discordeno@10.0.0/mod.ts";
import { Message, MessageContent } 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
@ -130,7 +130,7 @@ const cmdPrompt = async (logChannel: string, botName: string, sendMessage: (c: s
// sendIndirectMessage(originalMessage, messageContent, sendMessage, sendDirectMessage) returns Message
// sendIndirectMessage determines if the message needs to be sent as a direct message or as a normal message
const sendIndirectMessage = async (originalMessage: Message, messageContent: string, sendMessage: (c: string, m: string) => Promise<Message>, sendDirectMessage: (c: string, m: string) => Promise<Message>): Promise<Message> => {
const sendIndirectMessage = async (originalMessage: Message, messageContent: (string | MessageContent), sendMessage: (c: string, m: (string | MessageContent)) => Promise<Message>, sendDirectMessage: (c: string, m: (string | MessageContent)) => 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);

BIN
www/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

BIN
www/img/TheArtificer.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

BIN
www/img/Thumbs.db Normal file

Binary file not shown.

98
www/index.html Normal file
View File

@ -0,0 +1,98 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" />
<meta name="HandheldFriendly" content="true"/>
<meta name="author" content="Ean Milligan (ean@milligan.dev)">
<meta name="designer" content="Ean Milligan (ean@milligan.dev)">
<meta name="publisher" content="Ean Milligan (ean@milligan.dev)">
<title>The Artificer Discord Bot</title>
<meta name="description" content="The Artificer Discord Bot is a advanced dice rolling bot utilizing the Roll20 format. The Artificer is fast and reliable and free to use.">
<meta name="keywords" content="The Artificer, Discord bot, dice roller, dice rolling, roll, rolling, dice, roller, bot, artificer">
<meta name="robots" content="index, follow">
<meta name="revisit-after" content="7 days">
<meta name="distribution" content="web">
<meta name="robots" content="noodp, noydir">
<meta name="distribution" content="web">
<meta name="web_author" content="Ean Milligan (ean@milligan.dev)">
<link rel="shortcut icon" href="./favicon.ico">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Cinzel|Play">
<link rel="stylesheet" href="./theme.css">
<link rel="stylesheet" href="./main.css">
<script type="text/javascript" src="./main.js"></script>
</head>
<body>
<div id="page">
<div id="header">
<div id="header-left">
The Artificer
</div>
<div id="header-right">
<a href="https://discord.com/api/oauth2/authorize?client_id=789045930011656223&permissions=2048&scope=bot" target="_blank">Invite Me!</a>
<span>|</span>
<a href="https://artificer.eanm.dev/" target="_blank">API Tools</a>
</div>
</div>
<div id="page-contents">
<div id="slogan">
<h1>The One-Stop Discord Bot for Rolling Dice</h1>
</div>
<div id="logo-desc" class="slug">
<div id="logo">
<img src="./img/TheArtificer.png" alt="The Artificer Logo" />
</div>
<div id="description">
<p>The Artificer is an open source Discord bot that specializes in rolling dice. The bot utilizes the compact <a href="https://roll20.zendesk.com/hc/en-us/articles/360037773133-Dice-Reference" target="_blank">Roll20 formatting</a> for ease of use and will correctly perform any needed math on the roll (limited to basic algebra).</p>
<p>This bot was developed to replace the Sidekick discord bot after it went offline many times for extended periods. This was also developed to fix some annoyances that were found with Sidekick, specifically its vague error messages (such as <code>"Tarantallegra!"</code>, what is that supposed to mean) and its inability to handle implicit multiplication (such as <code>4(12 + 20)</code>).</p>
<p><a href="https://discord.com/api/oauth2/authorize?client_id=789045930011656223&permissions=2048&scope=bot" target="_blank">Invite The Artificer to Your Server</a> | <a href="https://discord.gg/peHASXMZYv" target="_blank">Support Server</a> | <a href="https://github.com/Burn-E99/TheArtificer" target="_blank">GitHub Repository</a></p>
</div>
</div>
<div id="examples">
<h2>Example Commands:</h2>
<div class="slug">
<h3>Rolling Command:</h3>
<p>This command is what the bot is all about. Using the <a href="https://roll20.zendesk.com/hc/en-us/articles/360037773133-Dice-Reference" target="_blank">Roll20 format</a>, any form of dice roll can be performed, with any needed math calculated into the results. This command can even be used as a fairly advanced calculator, supporting parenthesis, exponentials, multiplication, division, modulus, addition, and subtraction.</p>
<h4 class="example">Examples:</h4>
<p class="example"><code>[[d20]]</code> - Rolls a simple d20 without anything fancy</p>
<p class="example"><code>[[4d20r1!]]</code> - Rolls 4 d20 dice, rerolling any dice that land on 1, and repeatedly rolling a new d20 for any critical success rolled</p>
<p class="example"><code>[[((d20+20) - 10) / 5]]</code> - Rolls a d20 die, adds 20 to that roll, subtracts off 10, and finally divide by 5</p>
<p class="example"><code>[[7d67d5]]</code> - Rolls a 7 d67 dice, dropping the lowest 5 dice</p>
<p class="example"><code>[[(32 * 12) + 5]]</code> - Multiplies 32 and 12, and adds 5 to that result</p>
<p class="example">And infinitely more possibilities. Any equations and/or rolls can be thrown at the bot and it will compute it quickly. Run the <code>[[rollhelp</code> command for full details of this command.</p>
<br/>
<h3>Supporting Commands:</h3>
<p>The following commands are just some basic utility commands.</p>
<h4 class="example">Examples:</h4>
<p class="example"><code>[[stats</code> or <code>[[s</code> - Shows the stats on how many servers and users are using the bot</p>
<p class="example"><code>[[help</code> or <code>[[?</code> - Gives the full list of available commands</p>
<p class="example"><code>[[rollhelp</code> or <code>[[??</code> - Gives the full details on the roll command, explaining the <a href="https://roll20.zendesk.com/hc/en-us/articles/360037773133-Dice-Reference" target="_blank">Roll20 format</a></p>
</div>
</div>
<div id="api">
<h2>The Artificer API</h2>
<div class="slug">
<p>Trusted users will be provided with an API key that allows rolling of dice from third party programs. This API was developed to let DnD groups that use Excel to manage player sheets to roll dice directly from Excel to Discord. The API is very restricted and has a harsh rate limit to prevent spam.</p>
<p>There is a ZERO tolerance for API abuse. If abuse is detected or reported, the user will be banned with no chance to appeal. Currently, API keys are only given out to users known personally to the devs of this bot, but plans are in the works to open this up to more users using a form of registration.</p>
<p>If you happen to be a trusted user, information on the API endpoints can be found in the <a href="https://github.com/Burn-E99/TheArtificer" target="_blank">GitHub Repository</a>. Basic administration of your API key can be done using the API Tools linked at the top of this page.</p>
</div>
</div>
<div id="final">$nbsp;</div>
</div>
<div id="footer">
<div id="footer-left">
Built by <a href="https://github.com/Burn-E99/" target="_blank">Ean Milligan</a>
</div>
<div id="footer-right">
Version 1.3.2
</div>
</div>
</div>
</body>
</html>

165
www/main.css Normal file
View File

@ -0,0 +1,165 @@
body {
font-family: "Play", sans-serif;
padding: 0;
margin: 0;
overflow: hidden;
}
#page {
height: 100vh;
display: grid;
grid-template-columns: auto;
grid-template-rows: 3rem calc(100vh - 5rem) 2rem;
grid-template-areas: "header" "page-contents" "footer";
color: var(--page-font-color);
background-color: var(--page-bg-color);
}
#header {
grid-area: header;
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: auto;
grid-template-areas: "header-left header-right";
font-family: "Cinzel", serif;
font-size: 2rem;
line-height: 3rem;
font-weight: 500;
padding: 0 10px;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
background-color: var(--header-bg-color);
color: var(--header-font-color);
border-bottom: 1px solid var(--header-font-color);
}
#header-left {
grid-area: header-left;
}
#header-right {
grid-area: header-right;
justify-self: end;
font-size: 1.75rem;
}
#footer {
grid-area: footer;
display: grid;
grid-template-columns: 1fr 1.5fr 1.5fr 1fr;
grid-template-rows: auto;
grid-template-areas: ". footer-left footer-right .";
line-height: 2rem;
height: 2rem;
background-color: var(--footer-bg-color);
}
#footer-left {
grid-area: footer-left;
}
#footer-right {
grid-area: footer-right;
justify-self: end;
}
#page-contents {
grid-area: page-contents;
padding: 0 20rem;
height: calc(100vh - 5rem);
display: grid;
grid-template-columns: auto;
grid-template-rows: fit-content(5rem) fit-content(10rem) fit-content(37rem) auto 1rem;
grid-template-areas: "slogan" "logo-desc" "examples" "api" "final";
overflow-y: auto;
}
#slogan {
grid-area: slogan;
}
#slogan h1 {
line-height: 2.5rem;
font-size: 2.5rem;
}
#logo-desc {
grid-area: logo-desc;
margin-bottom: 0.5rem;
display: grid;
grid-template-columns: 11rem auto;
grid-template-rows: auto;
grid-template-areas: "logo description";
}
#logo {
grid-area: logo;
margin: auto;
}
#logo img {
height: 10rem;
}
#description {
grid-area: description;
}
#examples {
grid-area: examples;
}
h4.example {
line-height: 0.25rem;
}
p.example {
margin: 0;
margin-bottom: 0.1rem;
padding: 0;
}
.slug h3 {
margin-top: 0;
}
#api {
grid-area: api;
}
#final {
grid-area: final;
}
@media screen and (max-width: 1900px) {
#page-contents {
padding: 0 10rem;
}
}
@media screen and (max-width: 1400px) {
#page-contents {
padding: 0 5rem;
}
}
@media screen and (max-width: 1000px) {
#page-contents {
padding: 0 1rem;
}
}

0
www/main.js Normal file
View File

51
www/theme.css Normal file
View File

@ -0,0 +1,51 @@
/* Color theme roughly based on:
* https://paletton.com/#uid=5000p0kw8dLmVn9rgiuHN93Sj4E
*/
:root {
--header-font-color: rgb(238, 237, 226);
--header-bg-color: rgb(110, 0, 0);
--page-font-color: white;
--page-bg-color: rgb(32, 34, 37);
--footer-bg-color: rgb(54, 57, 63);
--link-new-color: rgb(147, 22, 22);
--link-hover-color: rgb(255, 94, 94);
--link-visited-color: rgb(184, 52, 52);
--code-bg: rgb(47, 49, 54);
--slug-bg: rgb(15, 16, 17);
--slug-border: rgb(0, 0, 0);
}
#header a {
color: var(--header-font-color);
text-decoration: none;
}
a {
color: var(--link-new-color);
}
a:active, a:visited {
color: var(--link-visited-color);
}
a:hover,
#header a:hover {
cursor: pointer;
color: var(--link-hover-color);
}
code {
background-color: var(--code-bg);
}
.slug {
padding: 0.5rem;
background-color: var(--slug-bg);
border: 4px solid var(--slug-border);
border-radius: 1.5rem;
}