Compare commits
246 Commits
Author | SHA1 | Date |
---|---|---|
|
83595482b7 | |
|
cdc710385f | |
|
59d4435c32 | |
|
2cfa923093 | |
|
c58ebcc9f9 | |
|
e01097b1e3 | |
|
4efbed7424 | |
|
4a5e33c9a0 | |
|
6bf671f82d | |
|
587d5aa19d | |
|
1bb8c1a308 | |
|
e06abac9cf | |
|
c80b7c2235 | |
|
959fd6e120 | |
|
de02ebcc09 | |
|
f989be56db | |
|
255955d854 | |
|
2f088907ad | |
|
f38096fafb | |
|
67d5704a76 | |
|
3f6162a1a4 | |
|
d715214c01 | |
|
1eac531b41 | |
|
b9c5af6c73 | |
|
b9d436ba92 | |
|
e553042e88 | |
|
657765df00 | |
|
719de69350 | |
|
b0e2abb941 | |
|
799efd38f3 | |
|
f704cd9fa0 | |
|
152b9ff153 | |
|
e957e181d5 | |
|
a37f5eb7d5 | |
|
d6c5dfee77 | |
|
d2692f1805 | |
|
509147d651 | |
|
ef5709b6a6 | |
|
0006e66b16 | |
|
57cae9c01f | |
|
bc6ad87aff | |
|
f1025ce129 | |
|
633e786d95 | |
|
6d731951c7 | |
|
a4ddce0bc8 | |
|
50d9c78b45 | |
|
44a1200084 | |
|
96cffd9c4c | |
|
2d55c073ee | |
|
3146f2bc96 | |
|
65fc9e56de | |
|
46757b31bc | |
|
1052e7d2e2 | |
|
5e94056fc5 | |
|
7744d3a72e | |
|
6e71878d42 | |
|
b0401809c4 | |
|
79d72654e2 | |
|
0767b66f33 | |
|
d41ce7fc34 | |
|
5233d029f3 | |
|
f2ed8a4f16 | |
|
d677fc6a0c | |
|
d386561855 | |
|
3b3ce821bd | |
|
60f996b0a9 | |
|
660f9e9bc2 | |
|
500579f2a3 | |
|
7301df4d9d | |
|
b055e0174d | |
|
f19c4216c0 | |
|
fe70166e6a | |
|
4bbdb59f3d | |
|
fedba62d52 | |
|
c8af87e407 | |
|
05174aecd9 | |
|
075fa1de71 | |
|
6d987a6d83 | |
|
6299cdc7ec | |
|
31e32a29c6 | |
|
dd2fc5b8df | |
|
ff9168e493 | |
|
66010047b5 | |
|
754ce054b5 | |
|
cb5522345a | |
|
515021a295 | |
|
e0fdb1eb31 | |
|
d9c49c588c | |
|
c445364aa7 | |
|
156f3d528f | |
|
30f0314695 | |
|
b2d4d0c0a4 | |
|
3d146e9c7b | |
|
6aa4c73d5d | |
|
4b6683525c | |
|
7a4a33f661 | |
|
6e88e96cda | |
|
02e2faf58f | |
|
4e12e11fe2 | |
|
bddf7c6fdb | |
|
d989e9d473 | |
|
ad0aef6c94 | |
|
499c277fba | |
|
41214bd0d3 | |
|
cbac134f79 | |
|
cb3cb6777d | |
|
69e9d6ca1d | |
|
abe49d49c2 | |
|
139ef44556 | |
|
2b9de4be81 | |
|
5ee02241a8 | |
|
2867e5f557 | |
|
3ea48838f4 | |
|
b6b1f872d2 | |
|
4ed561a15b | |
|
15fd57ea18 | |
|
38bc021455 | |
|
ed5f7d9f1e | |
|
f0ea31edf3 | |
|
c9772d3ccf | |
|
5f58f9cae8 | |
|
0e009441ca | |
|
791dd3a626 | |
|
57ce6e1d1b | |
|
7116851139 | |
|
41fbb1bd50 | |
|
fcae60cb69 | |
|
f4fcb097c3 | |
|
ac63642d3a | |
|
4516f17949 | |
|
414bab3a0d | |
|
ae321885d6 | |
|
5b7ceb9f88 | |
|
6162339942 | |
|
0066652590 | |
|
9d6b389d71 | |
|
8793011350 | |
|
babc57497e | |
|
308f897eb7 | |
|
b1fce05149 | |
|
7a0b49dc0c | |
|
5e1a509c96 | |
|
14e909c89f | |
|
b169ee8632 | |
|
2fe2c5f296 | |
|
3bda7a1187 | |
|
46026d6a08 | |
|
5d1d39bebf | |
|
7ba6da1404 | |
|
902dcb00ea | |
|
acc3522c2a | |
|
4f3a9bb797 | |
|
55b2367900 | |
|
5da94fafa0 | |
|
3aa8faf991 | |
|
ede1562f33 | |
|
1ea2f64341 | |
|
e5e77189ec | |
|
426f0d7069 | |
|
897fe80533 | |
|
0b864b0f21 | |
|
fafaee67b3 | |
|
504df7c80f | |
|
058695415e | |
|
65a182cb50 | |
|
aa7814d1fe | |
|
9a5574f91e | |
|
3e6844ed10 | |
|
f2797e6c33 | |
|
f6eb3b2b69 | |
|
4de1115fa9 | |
|
99c2d096f7 | |
|
b7e58f56a5 | |
|
d142b35522 | |
|
d094efb279 | |
|
7808093bc7 | |
|
9d7aed2773 | |
|
c31436e45d | |
|
829ec0ecea | |
|
9bd757741b | |
|
bb9a2014ed | |
|
c6c1a8918e | |
|
bd8f7c8a6f | |
|
69f95bf701 | |
|
c9aff85452 | |
|
1b2851353e | |
|
07e76733ec | |
|
8befcc1ca1 | |
|
9582d91ac1 | |
|
03a2acc386 | |
|
bdec5e7850 | |
|
a7856bc3f2 | |
|
2b579eb4ac | |
|
730c441645 | |
|
7053c25719 | |
|
6ab0923d71 | |
|
7230a7e6b5 | |
|
c981fe5664 | |
|
cb00147f6b | |
|
f50dec29c7 | |
|
6e1c6e8db3 | |
|
b9c7fac984 | |
|
861d1e00cd | |
|
ed60f10d04 | |
|
047d41a20e | |
|
57bde22e1a | |
|
0250135c5f | |
|
15ecb45e65 | |
|
ba51bd471c | |
|
8cf55aacf5 | |
|
3864cb91fc | |
|
f44014c22a | |
|
1e350d4e2a | |
|
bba4217bd7 | |
|
6b198ecb47 | |
|
ac8602f598 | |
|
fd7bc5f152 | |
|
5b0de24466 | |
|
dcd49e20dd | |
|
e3dc7bc8b4 | |
|
4207021fa9 | |
|
5bef3878cc | |
|
06095e3bdc | |
|
2f2a8f67e0 | |
|
e69806c443 | |
|
73e4ca94b7 | |
|
2e2e08f48a | |
|
105bc14e71 | |
|
5c517fad67 | |
|
ed80e08755 | |
|
951d15fcf9 | |
|
6afaf882fa | |
|
1d31a4f059 | |
|
a42b75aa51 | |
|
f3a6a36fb0 | |
|
8d3c22a39f | |
|
864f281c60 | |
|
74c733308f | |
|
103ad8a8f5 | |
|
09e97eabc1 | |
|
8f24a3bfae | |
|
44d966971a | |
|
a017793997 | |
|
2c991e44f8 | |
|
957e84ea2b | |
|
4fbcf15953 |
|
@ -0,0 +1,11 @@
|
|||
meta {
|
||||
name: Bot Statistics
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
get {
|
||||
url: http://localhost:8166/api/stats
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
meta {
|
||||
name: Ping
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: http://localhost:8166/api/ping
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
meta {
|
||||
name: General Utility
|
||||
seq: 5
|
||||
}
|
||||
|
||||
auth {
|
||||
mode: inherit
|
||||
}
|
|
@ -5,7 +5,7 @@ meta {
|
|||
}
|
||||
|
||||
get {
|
||||
url: http://localhost:8166/api/roll?user=[discord-user-id]&channel=[discord-channel-id]&rollstr=[artificer-roll-cmd]&documentation=All items below are optional. Flags do not need values.&nd=[no-details-flag]&snd=[super-no-details-flag]&s=[spoiler-results-flag]&m=[max-roll-flag, cannot be used with n flag]&n=[nominal-roll-flag, cannot be used with m flag]&gms=[csv-of-discord-user-ids-to-be-dmed-results]&o=[order-rolls, must be a or d]&c=[count-flag]
|
||||
url: http://localhost:8166/api/roll?user=[discord-user-id]&channel=[discord-channel-id]&rollstr=[artificer-roll-cmd]&documentation=All items below are optional. Flags do not need values.&nd=[no-details-flag]&snd=[super-no-details-flag]&hr=[hide-raw-roll-details-flag]&s=[spoiler-results-flag]&m-or-max=[max-roll-flag, cannot be used with n flag]&min=[min-roll-flag, cannot be used with n, sn, or max]&n=[nominal-roll-flag, cannot be used with sn, max or min flag]&sn=[simulated-nominal-flag, can pass number with it, cannot be used with max, min, n. or cc]&gms=[csv-of-discord-user-ids-to-be-dmed-results]&o=[order-rolls, must be a or d]&c=[count-flag]&cc=[confirm-crit-flag, cannot be used with sn]&rd=[roll-dist-flag]&nv-or-vn=[number-variables-flag]&cd=[custom-dice, format value as name:[side1,side2,...,sideN], use ; to separate multiple custom dice]&ns=[no-spaces, removes the default added space between rolls]
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
@ -17,10 +17,18 @@ params:query {
|
|||
documentation: All items below are optional. Flags do not need values.
|
||||
nd: [no-details-flag]
|
||||
snd: [super-no-details-flag]
|
||||
hr: [hide-raw-roll-details-flag]
|
||||
s: [spoiler-results-flag]
|
||||
m: [max-roll-flag, cannot be used with n flag]
|
||||
n: [nominal-roll-flag, cannot be used with m flag]
|
||||
m-or-max: [max-roll-flag, cannot be used with n flag]
|
||||
min: [min-roll-flag, cannot be used with n, sn, or max]
|
||||
n: [nominal-roll-flag, cannot be used with sn, max or min flag]
|
||||
sn: [simulated-nominal-flag, can pass number with it, cannot be used with max, min, n. or cc]
|
||||
gms: [csv-of-discord-user-ids-to-be-dmed-results]
|
||||
o: [order-rolls, must be a or d]
|
||||
c: [count-flag]
|
||||
cc: [confirm-crit-flag, cannot be used with sn]
|
||||
rd: [roll-dist-flag]
|
||||
nv-or-vn: [number-variables-flag]
|
||||
cd: [custom-dice, format value as name:[side1,side2,...,sideN], use ; to separate multiple custom dice]
|
||||
ns: [no-spaces, removes the default added space between rolls]
|
||||
}
|
||||
|
|
|
@ -1,7 +1,14 @@
|
|||
meta {
|
||||
name: Authenticated
|
||||
seq: 1
|
||||
}
|
||||
|
||||
headers {
|
||||
X-Api-Key: [YOUR-API-KEY-HERE]
|
||||
auth {
|
||||
mode: apikey
|
||||
}
|
||||
|
||||
auth:apikey {
|
||||
key: X-Api-Key
|
||||
value: [YOUR-API-KEY-HERE]
|
||||
placement: header
|
||||
}
|
|
@ -12,5 +12,4 @@ sonar.exclusions=emojis
|
|||
sonar.sourceEncoding=UTF-8
|
||||
|
||||
# Exclusions for copy-paste detection
|
||||
# src/commands/rollHelp.ts, src/commands/rollDecorators.ts are excluded to get rid of the duplicate code compliant. Sonar does not like you initializing JSON in ts files.
|
||||
sonar.cpd.exclusions=src/commands/rollHelp.ts,src/commands/rollDecorators.ts
|
||||
#sonar.cpd.exclusions=
|
|
@ -1,12 +1,64 @@
|
|||
{
|
||||
"deno.enable": true,
|
||||
"deno.lint": true,
|
||||
"deno.unstable": true,
|
||||
"deno.import_intellisense_origins": {
|
||||
"https://deno.land": true
|
||||
},
|
||||
"spellright.language": [
|
||||
"en"
|
||||
],
|
||||
"spellright.documentTypes": []
|
||||
"spellright.documentTypes": [],
|
||||
"cSpell.words": [
|
||||
"artigen",
|
||||
"channelid",
|
||||
"CWOD",
|
||||
"DEVMODE",
|
||||
"Discordeno",
|
||||
"Dists",
|
||||
"dkdk",
|
||||
"EMDAS",
|
||||
"Empheral",
|
||||
"Exponentials",
|
||||
"funciton",
|
||||
"guildid",
|
||||
"hidewarn",
|
||||
"imagescript",
|
||||
"indev",
|
||||
"Inno",
|
||||
"LOCALMODE",
|
||||
"localtoken",
|
||||
"longtext",
|
||||
"mtsf",
|
||||
"Mult",
|
||||
"nojs",
|
||||
"noodp",
|
||||
"noopener",
|
||||
"noydir",
|
||||
"oldcnt",
|
||||
"oper",
|
||||
"ovad",
|
||||
"PEMDAS",
|
||||
"ralias",
|
||||
"Rehost",
|
||||
"Rehosts",
|
||||
"resultid",
|
||||
"rolla",
|
||||
"rollalias",
|
||||
"rolldecorators",
|
||||
"rollhelp",
|
||||
"rollstr",
|
||||
"rsfop",
|
||||
"sproc",
|
||||
"Tarantallegra",
|
||||
"tinyint",
|
||||
"tinytext",
|
||||
"unauthorised",
|
||||
"unban",
|
||||
"xcwody",
|
||||
"xdydz",
|
||||
"xdydzracsq",
|
||||
"xovady",
|
||||
"yvar",
|
||||
"yvariables"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
## Imports
|
||||
- Should be grouped the following order:
|
||||
- `@` (third party)
|
||||
- `/` (root directory)
|
||||
- `artigen`
|
||||
- `commands`
|
||||
- `db`
|
||||
- `endpoints`
|
||||
- `src`
|
||||
- Should be alphabetical by import source file name
|
78
PRIVACY.md
78
PRIVACY.md
|
@ -3,25 +3,57 @@
|
|||
### Public Bot Information
|
||||
Publicly available versions of `The Artificer#8166` (Discord ID: `789045930011656223`) (herein referred to as _The Bot_ or _Bot_) do not track or collect user information via Discord.
|
||||
|
||||
Upon inviting _The Bot_ to a user's guild, _The Bot_ sends the guild name, Discord Guild ID, and current count of guild members to Burn_E99#1062 (herein referred to as _The Developer_) via a private Discord Guild. The guild name, Discord Guild ID, and current count of guild members are only used to roughly gage how popular _The Bot_ is and to determine if _The Bot_'s hosting solution needs to be improved. These pieces of information will never be sold or shared with anyone.
|
||||
Upon inviting _The Bot_ to a user's guild, _The Bot_ sends the guild name, Discord Guild ID, and current count of guild members to Burn_E99 (herein referred to as _The Developer_) via a private Discord Guild. The guild name, Discord Guild ID, and current count of guild members are only used to roughly gage how popular _The Bot_ is and to determine if _The Bot_'s hosting solution needs to be improved. These pieces of information will never be sold or shared with anyone.
|
||||
|
||||
Like all Discord bots, _The Bot_ reads every message that it is allowed to, meaning if _The Bot_ is allowed to see a channel in a guild, it reads every new message sent in said channel. This is due to the way the Discord API itself is designed. _The Bot_ does not read any messages sent in the past.
|
||||
* Messages that do not begin with _The Bot_'s command prefix are not saved or stored anywhere. Messages that do not begin with _The Bot_'s command prefix are ignored and not processed.
|
||||
* Messages that do begin with _The Bot_'s command prefix do not log user data, and most commands to not log any data. The commands that log data are the report command (in Discord, this command is known as `[[report` or `[[r`) and the API enable/disable commands (in Discord, these commands are known as `[[api enable`, `[[api allow`, `[[api disable`, and `[[api block`).
|
||||
* The report command only stores the text placed within the message that is directly after the command (herein referred to as _The Report Text_). This command is entirely optional, meaning users never need to run this command under normal usage of _The Bot_. This command is only intended to be used to report roll commands that did not output what was expected. This command will accept any value for _The Report Text_, thus it is up to the user to remove any sensitive information before sending the command. _The Report Text_ is stored in a private Discord Guild in a channel that only _The Developer_ can see. _The Report Text_ is solely used to improve _The Bot_, either by providing a feature suggestions or alerting _The Developer_ to bugs that need patched.
|
||||
* The API enable/disable commands only stores the Discord Guild ID upon usage. These commands are entirely optional, meaning users never need to run this command under normal usage of _The Bot_. These commands only need to be used when the user desires to utilize the optional API. Discord Guild IDs are internal IDs generated and provided by Discord. _The Bot_ only uses the stored Discord Guild IDs to ensure that API users cannot interact with Guilds that do not allow it or to check if an API user is a member of said Guild. The Guild IDs are only visible to _The Developer_ thru direct database administration. This direct database administration is only used when there are issues with _The Bot_'s database.
|
||||
* Messages sent in a Guild with the Inline Roll System disabled and do not begin with _The Bot_'s command prefix are not scanned for commands. Data regarding these messages are not saved or stored anywhere.
|
||||
* Messages sent in a Guild with the Inline Roll System enabled and do not begin with _The Bot_'s command prefix are only scanned for Inline Roll commands. If an Inline Roll command is found, it is sent through the Roll command handler. If an Inline Roll command is not found, the message is ignored and not scanned for other inline commands. Data regarding these messages are not saved or stored anywhere.
|
||||
* Messages that do begin with _The Bot_'s command prefix are scanned for commands that _The Bot_ supports. Any commands that store any amount or kind of data are listed below. If the command or system is not listed, then it does not log or store any data.
|
||||
* The report command (in Discord, this command is known as `/report`, `[[report`, or `[[r`):
|
||||
* This command is entirely optional, meaning users never need to run this command under normal usage of _The Bot_. This command is only intended to be used to report roll commands that did not output what was expected.
|
||||
* The report command only stores the text placed within the message that is directly after the command (herein referred to as _The Report Text_). This command will accept any value for _The Report Text_, thus it is up to the user to remove any sensitive information before sending the command.
|
||||
* _The Report Text_ is solely used to improve _The Bot_, either by providing a feature suggestions or alerting _The Developer_ to bugs that need patched.
|
||||
* _The Report Text_ is stored in a private Discord Guild in a channel that only _The Developer_ can see.
|
||||
* The API Control System (in Discord, these commands are known as `[[api enable`, `[[api allow`, `[[api disable`, and `[[api block`):
|
||||
* These commands are entirely optional, meaning users never need to run this command under normal usage of _The Bot_. These commands only need to be used when the user desires to utilize the optional API.
|
||||
* The API enable/disable commands only stores the Discord Guild ID and Discord Channel ID upon usage. Discord Guild IDs and Channel IDs are internal IDs generated and provided by Discord.
|
||||
* _The Bot_ only uses the stored Discord Guild IDs to ensure that API users cannot interact with Guilds that do not allow it or to check if an API user is a member of said Guild.
|
||||
* The Discord Guild IDs and Discord Channel IDs are only visible to _The Developer_ thru direct database administration. This direct database administration is only used when there are issues with _The Bot_'s database.
|
||||
* The Opt Out System (in Discord, this command is known as `[[opt-out` or `[[ignore-me`):
|
||||
* This command is optional, meaning users normally do not need to run this command under normal usage of _The Bot_. This command is intended for privacy, allowing a user to block _The Bot_ from reading their messages.
|
||||
* The Opt Out command only stores the Discord User ID of the user who ran the command. Discord User IDs are internal IDs generated and provided by Discord.
|
||||
* _The Bot_ only uses the stored Discord User IDs to filter what messages it will scan for commands. If a Discord User ID is in the Opt Out list, messages from this user will be immediately ignored.
|
||||
* The Discord User IDs are only visible to _The Developer_ thru direct database administration. This direct database administration is only used when there are issues with _The Bot_'s database.
|
||||
* The Inline Roll System (in Discord, these commands are known as `/toggle-inline-rolls enable`, `[[inline enable`, `[[inline allow`, `/toggle-inline-rolls disable`, `[[inline block`, `[[inline disable`, and `[[inline delete`):
|
||||
* This system is entirely optional, meaning users never need to run these commands under normal usage of _The Bot_. This system is only intended to be used when a user wants to utilize Inline Rolls in their Guild.
|
||||
* The Inline Roll System only stores the Discord Guild ID upon usage. Discord Guild IDs are internal IDs generated and provided by Discord.
|
||||
* _The Bot_ only uses the stored Discord Guild IDs to determine which Guilds it should do a preliminary scan for Inline Rolls for all messages sent.
|
||||
* The Discord Guild IDs are only visible to _The Developer_ thru direct database administration. This direct database administration is only used when there are issues with _The Bot_'s database.
|
||||
* The Unrestricted Repeat Roll System (in Discord, these commands are known as `/toggle-unrestricted-repeat enable`, `[[repeat enable`, `[[repeat allow`, `/toggle-unrestricted-repeat disable`, `[[repeat block`, `[[repeat disable`, and `[[repeat delete`):
|
||||
* This system is entirely optional, meaning users never need to run these commands under normal usage of _The Bot_. This system is only intended to be used when a user wants to utilize Unrestricted Repeat Rolls in their Guild.
|
||||
* The Unrestricted Repeat Roll System only stores the Discord Guild ID upon usage. Discord Guild IDs are internal IDs generated and provided by Discord.
|
||||
* _The Bot_ only uses the stored Discord Guild IDs to determine which Guilds it should allow Unrestricted Repeat Rolls in.
|
||||
* The Discord Guild IDs are only visible to _The Developer_ thru direct database administration. This direct database administration is only used when there are issues with _The Bot_'s database.
|
||||
* The Roll Alias System (in Discord, this system contains all commands starting with `/alias`, `[[rollalias`, `[[ralias`, `[[alias`, `[[rolla`, and `[[ra`, and the subcommands that _The Bot_ will store data from are `add`, `create`, `set`, `update`, `replace`, `copy`, `clone`, and `rename`):
|
||||
* This system is entirely optional, meaning users never need to run these commands under normal usage of _The Bot_. This system is only intended to be used when a user wants to save roll commands for later reuse.
|
||||
* The Roll Alias System stores the user provided Alias Name, the user provided Roll String, and in Guild mode, the Discord Guild ID of the Guild the command was run in; and in personal mode, the Discord User ID of the user who ran the command. The Alias Name is string of up to 200 characters that the user provided to save the Roll String under. The Roll String is a string of up to 4,000 characters containing roll commands and formatting text the user provided. Discord Guild IDs and Discord User IDs are internal IDs generated and provided by Discord.
|
||||
* _The Bot_ uses the Discord Guild IDs and Discord User IDs to determine who and where an alias can be used. The Alias Names are used to look up the requested Roll String. The Roll Strings are used to execute the requested alias.
|
||||
* The Alias Names and Roll Strings may be shown in Discord by using the following subcommands: `list`, `list-all`, `preview`, and `view`. In Guild mode, these subcommands will only show the Alias Names and Roll Strings created in the Guild the command was run in. In Personal mode, these subcommands will only show the Alias Names and Roll Strings the user created. The Alias Names, Roll Strings, Discord Guild IDs and Discord User IDs are visible to _The Developer_ thru direct database administration. This direct database administration is only used when there are issues with _The Bot_'s database.
|
||||
|
||||
All commands contribute to a global counter to track the number of times a command is used. These counters do not keep track of where commands were run, only counting the number of times the command has been called. These counters have no way of being tracked back to the individual commands run by the users.
|
||||
|
||||
If the Discord interaction is not explicitly mentioned above, it does not collect any information at all.
|
||||
|
||||
### Private Bot Information
|
||||
Privately hosted versions of The Artificer (in other words, bots running The Artificer's source code, but not running under the publicly available _Bot_, `The Artificer#8166`) (herein referred to as _Rehosts_ or _Rehost_) may be running in DEVMODE, a mode that allows the _Rehost_ to log every roll command used. This mode is intended for development use only, and only allows the roll command to function in the Guild specified in `config.ts` as `config.devServer`. _The Developer_ is not responsible for _Rehosts_, thus _Rehosts_ of _The Bot_ are not recommended to be used.
|
||||
|
||||
All policies described in **Public Bot Information** apply to _Rehosts_.
|
||||
Privately hosted versions of The Artificer (in other words, bots running The Artificer's source code, but not running under the publicly available _Bot_, `The Artificer#8166` (Discord ID: `789045930011656223`)) (herein referred to as _Rehosts_ or _Rehost_) may contain modifications that can log data not mentioned in this document.
|
||||
|
||||
Due to the nature of open source code, _Rehosts_ may not use the same codebase that is available in this repository. _The Developer_ does not moderate what other developers do to this codebase. This means that if you are not using the publicly available _Bot_ and instead using a _Rehost_, this _Rehost_ could collect any information it desires.
|
||||
|
||||
All policies described in **Public Bot Information** apply to _Rehosts_ running the official codebase. If the _Rehost_ has additional modifications, the developer(s) behind it should list any new instances data storage/logging.
|
||||
|
||||
# Direct Database Administration
|
||||
_The Developer_ will only use direct database administration when there are issues with _The Bot_'s database. These issues are include, but are not limited to, the following: data corruption, data recovery, data migration, manual data deletion, database schema updates. While handling these issues, _The Developer_ will only view the required tables, and will not exfiltrate any data unless required by the issue (such as by data recovery). Any data retrieved from the database during an aforementioned issue will be stored securely.
|
||||
|
||||
# Information relating to the Optional API Interactions
|
||||
_The Bot_'s API (herein referred to as _The API_) does not automatically collect any information. Users utilizing _The API_ are required to provide a small amount of information before using _The API_.
|
||||
|
||||
|
@ -45,5 +77,31 @@ If your guild has been banned from using _The API_, the Discord Guild ID will no
|
|||
|
||||
The data described above is considered necessary to prevent users from abusing the API and ban evading by deleting and recreating their account.
|
||||
|
||||
## Discord Command Data Deletion
|
||||
If you would like to ensure that all of your submitted reports are removed from _The Bot_'s private development server, please contact _The Developer_ via Discord (by sending a direct message to Burn_E99#1062) or via email (<ean@milligan.dev>) with a message along the lines of `"Please remove all of my submitted reports from your development server."`. Submitted reports are deleted from the server as they are processed, which happens roughly once a week, but this can be accelerated if requested.
|
||||
## Report Command Data Deletion
|
||||
If you would like to ensure that all of your submitted reports are removed from _The Bot_'s private development server, please contact _The Developer_ via Discord (by sending a direct message to @burn_e99) or via email (<ean@milligan.dev>) with a message along the lines of `"Please remove all of my submitted reports from your development server."`. Submitted reports are deleted from the server as they are processed, which happens roughly once a week, but this can be accelerated if requested.
|
||||
|
||||
## Opt Out System Data Deletion
|
||||
If you would like to remove your Discord User ID from _The Bot_'s database, simply send `[[opt-in` as a Direct Message to _The Bot_. Please note the bot will no longer be ignoring your messages, and it will resume scanning our messages for commands.
|
||||
|
||||
## Inline Roll System Data Deletion
|
||||
If you would like to remove your Discord Guild ID from _The Bot_'s database, simply send one of the following commands in the Guild: `/toggle-inline-rolls disable`, `[[inline disable`, `[[inline block`, or `[[inline delete`. All variants of this delete the Discord Guild ID from _The Bot_'s database.
|
||||
|
||||
Additionally, _The Bot_ will automatically delete the Discord Guild ID from _The Bot_'s database when _The Bot_ is removed from your guild.
|
||||
|
||||
## Unrestricted Repeat Roll System Data Deletion
|
||||
If you would like to remove your Discord Guild ID from _The Bot_'s database, simply send one of the following commands in the Guild: `/toggle-unrestricted-repeat disable`, `[[repeat disable`, `[[repeat block`, or `[[repeat delete`. All variants of this delete the Discord Guild ID from _The Bot_'s database.
|
||||
|
||||
Additionally, _The Bot_ will automatically delete the Discord Guild ID from _The Bot_'s database when _The Bot_ is removed from your guild.
|
||||
|
||||
## Alias System Data Deletion
|
||||
### Personal Mode
|
||||
If you would like to remove all your Personal aliases from _The Bot_'s database, simply send one of the following commands in any channel you share with _The Bot_: `/alias personal delete-all`, `[[alias delete-all`, or `[[alias remove-all`. As deletion is irreversible, _The Bot_ requires a verification code to execute the deletion. _The Bot_ will create a verification code for you when you first send the command.
|
||||
|
||||
If you only need to delete one alias, the `delete-one` subcommand is available.
|
||||
|
||||
### Guild Mode
|
||||
If you would like to remove all Guild aliases from _The Bot_'s database, simply send one of the following commands in the Guild: `/alias guild delete-all`, `[[alias guild delete-all`, or `[[alias guild remove-all`. As deletion is irreversible, _The Bot_ requires a verification code to execute the deletion. _The Bot_ will create a verification code for you when you first send the command.
|
||||
|
||||
If you only need to delete one alias, the `delete-one` subcommand is available.
|
||||
|
||||
Additionally, _The Bot_ will automatically delete all Guild aliases when _The Bot_ is removed from your guild.
|
||||
|
|
162
README.md
162
README.md
|
@ -1,4 +1,4 @@
|
|||
# The Artificer - A Dice Rolling Discord Bot | V3.0.0 - 2025/04/26
|
||||
# The Artificer - A Dice Rolling Discord Bot | V4.1.2 - 2025/08/06
|
||||
[](https://sonarcloud.io/summary/new_code?id=TheArtificer)
|
||||
[](https://sonarcloud.io/summary/new_code?id=TheArtificer) [](https://sonarcloud.io/summary/new_code?id=TheArtificer) [](https://sonarcloud.io/summary/new_code?id=TheArtificer) [](https://sonarcloud.io/summary/new_code?id=TheArtificer) [](https://sonarcloud.io/summary/new_code?id=TheArtificer) [](https://sonarcloud.io/summary/new_code?id=TheArtificer)
|
||||
|
||||
|
@ -7,7 +7,7 @@ The Artificer is a Discord bot that specializes in rolling dice. The bot utiliz
|
|||
This bot was developed to replace the Sidekick discord bot after it went offline many times for extended periods, and is now dead according to their GitHub. This was also developed to fix some annoyances that were found with Sidekick, specifically its vague error messages (such as `"Tarantallegra!"`, what is that supposed to mean) and its inability to handle implicit multiplication (such as `4(12 + 20)`).
|
||||
|
||||
## Using The Artificer
|
||||
I am hosting this bot for public use and you may find its invite link below. If you would like to host this bot yourself, details of how to do so are found at the end of this README, but I do not recommend this unless you are experienced with running Discord bots.
|
||||
I am hosting this bot for public use and you may find its invite link below. If you would like to host this bot yourself, details of how to do so are located [here](https://github.com/Burn-E99/TheArtificer/blob/master/docs/SELF_HOSTING.md), but I do not recommend this unless you are experienced with running Discord bots.
|
||||
|
||||
After inviting the bot, if you would like it to remove the message requesting the popcat emoji, you will need to give the `The Artificer` role the `Manage Messages` permission. All other permissions needed are handled by the invite link.
|
||||
|
||||
|
@ -17,164 +17,30 @@ After inviting the bot, if you would like it to remove the message requesting th
|
|||
|
||||
---
|
||||
|
||||
## Available Commands
|
||||
The Artificer comes with a few supplemental commands to the main rolling command.
|
||||
|
||||
* `[[help` or `[[h` or `[[?`
|
||||
* Provides a message similar to this available commands block.
|
||||
* `[[rollhelp` or `[[??` or `[[rh` or `[[hr`
|
||||
* Details on how to use the roll command, listed as `[[xdy...]]` below.
|
||||
* `[[rolldecorators` or `[[???` or `[[rd` or `[[dr`
|
||||
* Details on how to use decorators on the roll command.
|
||||
* `[[api [subcommand]`
|
||||
* Administrative tools for the bots's API. These commands may only be used by the Owner or Admins of your guild.
|
||||
* Available Subcommands:
|
||||
* `[[api help`
|
||||
* Provides a message similar to this subcommand description.
|
||||
* `[[api status`
|
||||
* Shows the current status of the API for this guild.
|
||||
* `[[api allow` or `[[api enable`
|
||||
* Allows API Rolls to be sent to this guild.
|
||||
* `[[api block` or `[[api disable`
|
||||
* Blocks API Rolls from being sent to this guild.
|
||||
* `[[api delete`
|
||||
* Deletes this guild from The Artificer's database.
|
||||
* `[[ping`
|
||||
* Tests the latency between you, Discord, and the bot.
|
||||
* `[[info` or `[[i`
|
||||
* Outputs some information and links relating to the bot.
|
||||
* `[[privacy`
|
||||
* Prints some information about the Privacy Policy, found in `PRIVACY.md`.
|
||||
* `[[version` or `[[v`
|
||||
* Prints out the current version of the bot.
|
||||
* `[[popcat` or `[[pop` or `[[p`
|
||||
* Sends the animated popcat emote for those who do not have Discord Nitro.
|
||||
* If bot is given the permission `Manage Messages`, the bot will remove the message requesting the emote.
|
||||
* `[[stats` or `[[s`
|
||||
* Prints out how many users, channels, and servers the bot is currently serving.
|
||||
* `[[heatmap` or `[[hm`
|
||||
* Heatmap of when the roll command is run the most.
|
||||
* `[[report` or `[[r [command that failed]`
|
||||
* People aren't perfect, but this bot is trying to be.
|
||||
* If you encounter a command that errors out or returns something unexpected, please use this command to alert the developers of the problem.
|
||||
* Example:
|
||||
* `[[report [[2+2]] returned 5 when I expected it to return 4` will send the entire message after `[[report` to the devs via Discord.
|
||||
* `[[opt-out` or `[[ignore-me`
|
||||
* Adds you to an ignore list so the bot will never respond to you
|
||||
* `[[opt-in` **Available via DM ONLY**
|
||||
* Removes you from the ignore list
|
||||
* `[[xdydzracsq!]]`
|
||||
* This is the command the bot was built specifically for.
|
||||
* It looks a little complicated at first, but if you are familiar with the [Roll20 formatting](https://artificer.eanm.dev/roll20), 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.
|
||||
* Parameters for rolling:
|
||||
|
||||
| Paramater | Required? | Repeatable? | Description |
|
||||
|---------------|-------------|---------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| x | Optional | No | number of dice to roll, if omitted, 1 is used, additionally, replace x with `F` to roll the dice as Fate dice |
|
||||
| dy | Required | No | size of dice to roll, d20 = 20 sided die |
|
||||
| dz or dlz | Optional | No | drops the lowest z dice, cannot be used with any other drop or keep options |
|
||||
| kz or khz | Optional | No | keeps the highest z dice, cannot be used with any other drop or keep options |
|
||||
| dhz | Optional | No | drops the highest z dice, cannot be used with any other drop or keep options |
|
||||
| klz | Optional | No | keeps the lowest z dice, cannot be used with any other drop or keep options |
|
||||
| ra or r=a | Optional | Yes | rerolls any rolls that match a, r3 will reroll every die that land on 3, throwing out old rolls, cannot be used with ro |
|
||||
| r<a | Optional | Yes | rerolls any rolls that are less than or equal to a, r3 will reroll every die that land on 3, 2, or 1, throwing out old rolls, cannot be used with ro |
|
||||
| r>a | Optional | Yes | rerolls any rolls that are greater than or equal to a, r3 will reroll every die that land on 3 or greater, throwing out old rolls, cannot be used with ro |
|
||||
| roa or ro=a | Optional | Yes | rerolls any rolls that match a, r3 will reroll each die that lands on 3 ONLY ONE TIME, throwing out old rolls, cannot be used with r |
|
||||
| ro<a | Optional | Yes | rerolls any rolls that are less than or equal to a, r3 will reroll each die that lands on 3, 2, or 1 ONLY ONE TIME, throwing out old rolls, cannot be used with r |
|
||||
| ro>a | Optional | Yes | rerolls any rolls that are greater than or equal to a, r3 will reroll each die that lands on 3 or greater ONLY ONE TIME, throwing out old rolls, cannot be used with r |
|
||||
| csq or cs=q | Optional | Yes | changes crit score to q |
|
||||
| cs<q | Optional | Yes | changes crit score to be less than or equal to q |
|
||||
| cs>q | Optional | Yes | changes crit score to be greater than or equal to q |
|
||||
| cfq or cf=q | Optional | Yes | changes crit fail to q |
|
||||
| cf<q | Optional | Yes | changes crit fail to be less than or equal to q |
|
||||
| cf>q | Optional | Yes | changes crit fail to be greater than or equal to q |
|
||||
| ! | Optional | No | exploding, rolls another dy for every crit success |
|
||||
| !o | Optional | No | exploding once, rolls another dy for each original crit success |
|
||||
| !p | Optional | No | penetrating explosion, rolls one dy for each crit success, but subtracts one from each resulting explosion |
|
||||
| !! | Optional | No | compounding explosion, rolls one dy for each crit success, but adds the resulting explosion to the die that caused this explosion |
|
||||
| !=u | Optional | Yes | exploding, rolls another dy for every die that lands on u |
|
||||
| !>u | Optional | Yes | exploding, rolls another dy for every die that lands on u or greater |
|
||||
| !<u> | Optional | Yes | exploding, rolls another dy for every die that lands on u or less |
|
||||
| !o=u | Optional | Yes | exploding once, rolls another dy for each original die that landed on u |
|
||||
| !o>u | Optional | Yes | exploding once, rolls another dy for each original die that landed on u or greater |
|
||||
| !o<u | Optional | Yes | exploding once, rolls another dy for each original die that landed on u or less |
|
||||
| !p=u | Optional | Yes | penetrating explosion, rolls one dy for each die that lands on u, but subtracts one from each resulting explosion |
|
||||
| !p>u | Optional | Yes | penetrating explosion, rolls one dy for each die that lands on u or greater, but subtracts one from each resulting explosion |
|
||||
| !p<u | Optional | Yes | penetrating explosion, rolls one dy for each die that lands on u or under, but subtracts one from each resulting explosion |
|
||||
| !!=u | Optional | Yes | compounding explosion, rolls one dy for each die that lands on u, but adds the resulting explosion to the die that caused this explosion |
|
||||
| !!>u | Optional | Yes | compounding explosion, rolls one dy for each die that lands on u or greater, but adds the resulting explosion to the die that caused this explosion |
|
||||
| !!<u | Optional | Yes | compounding explosion, rolls one dy for each die that lands on u or under, but adds the resulting explosion to the die that caused this explosion |
|
||||
|
||||
* 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 can also handle some custom format dice:
|
||||
* CWOD Dice - `[[xcwody]]`
|
||||
* `x` - Number of CWOD dice to roll
|
||||
* `y` - Difficulty to roll at
|
||||
* OVA Dice - `[[xovady]]`
|
||||
* `x` - Number of OVA dice to roll
|
||||
* `y` - Size of the die to roll (defaults to 6 if omitted)
|
||||
* This command also has some useful decorators that can used. These decorators simply need to be placed after all rolls in the message:
|
||||
* `-c` - Count - Shows the Count Embed, containing the count of successful rolls, failed rolls, rerolls, drops, and explosions
|
||||
* `-nd` - No Details - Suppresses all details of the requested roll
|
||||
* `-snd` - Super No Details - Suppresses all details of the requested roll and hides no details message
|
||||
* `-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 results have some formatting applied on them to provide details on what happened during this roll.
|
||||
* Critical successes will be **bolded**
|
||||
* Critical fails will be <ins>underlined</ins>
|
||||
* Rolls that were dropped or rerolled ~~crossed out~~
|
||||
|
||||
## 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.
|
||||
|
||||
Guilds Owners or Admins must run the `[[api allow` command for any users to be able to use the `/api/roll` endpoint.
|
||||
|
||||
Every API request **requires** the header `X-Api-Key` with the value set to the API key granted to you.
|
||||
|
||||
* If an API fails, these are the possible responses:
|
||||
* `400` - Bad Request - Query parameters missing or malformed.
|
||||
* `403` - Forbidden - API Key is not authenticated or user does not match the owner of the API Key.
|
||||
* `404` - Not Found - Requested endpoint does not exist.
|
||||
* `429` - Too Many Requests - API rate limit exceeded, please slow down.
|
||||
* `500` - Internal Server Error - Something broke, if this continues to happen, please submit a GitHub issue.
|
||||
|
||||
Official API URL: `https://artificer.eanm.dev/api/`
|
||||
|
||||
API Documentation can be found in the `.bruno` folder, which can be viewed in [Bruno](https://www.usebruno.com/).
|
||||
|
||||
API Key management via a basic GUI is availble on the [API Tools](https://artificer.eanm.dev/) website.
|
||||
|
||||
## Problems? Feature requests?
|
||||
If you run into any errors or problems with the bot, or think you have a good idea to add to the bot, please submit a new GitHub issue detailing it. If you don't have a GitHub account, a report command (detailed above) is provided for use in Discord.
|
||||
|
||||
---
|
||||
|
||||
## Self Hosting The Artificer
|
||||
The Artificer is built on [Deno](https://deno.land/) `v2.2.7` using [Discordeno](https://discordeno.mod.land/) `v12.0.1`. 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.
|
||||
## Available Commands
|
||||
Available commands are listed [here](https://github.com/Burn-E99/TheArtificer/blob/master/docs/COMMANDS.md).
|
||||
|
||||
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 `db\initialize.ts` to create the schema and tables. After this, run `db\populateDefaults.ts` to insert some needed values into the tables.
|
||||
|
||||
Once everything is set up, starting the bot can simply be done with the command in `start.command`.
|
||||
|
||||
If you choose to run version `1.1.0` or newer, ensure you disable the API in `config.ts` or verify you have properly secured your instance of The Artificer. If you enable the API, you should manually generate a 25 char nanoid and place it in `config.api.adminKey` and copy your `userid` and place it in `config.api.admin` before running `db\populateDefaults.ts`.
|
||||
## API Details
|
||||
API details are listed [here](https://github.com/Burn-E99/TheArtificer/blob/master/docs/API.md).
|
||||
|
||||
---
|
||||
|
||||
## Privacy Policy and Terms of Service
|
||||
The Artificer has a Privacy Policy and Terms of Service to detail expectations of what user data is stored and how users should use The Artificer. The following Privacy Policy and Terms of Service only apply to the officially hosted version of The Artificer (`The Artificer#8166`, Discord ID: `789045930011656223`).
|
||||
|
||||
Privacy Policy TL;DR: Only report command data is stored if you do not use the API, if you use the API, submitted Discord Ids will be stored linked to your email. For more detailed information, please check out the full [PRIVACY POLICY](https://github.com/Burn-E99/TheArtificer/blob/master/PRIVACY.md).
|
||||
Privacy Policy TL;DR:
|
||||
- If you use the report command, the text submitted will be stored for a short period of time.
|
||||
- If you use the Roll Alias System, any data submitted to it will be stored until you delete it.
|
||||
- If you use the API, submitted Discord Ids will be stored linked to your email.
|
||||
- If you use the Inline Roll System, Discord Guild Ids will be stored.
|
||||
- If you use the Opt Out System, Discord User Ids will be stored.
|
||||
|
||||
For more detailed information, please check out the full [PRIVACY POLICY](https://github.com/Burn-E99/TheArtificer/blob/master/PRIVACY.md).
|
||||
|
||||
Terms of Service TL;DR: Don't abuse or attempt to hack/damage The Artificer or its API. If you do, you may be banned from use. For more detailed information, please check out the full [TERMS OF SERVICE](https://github.com/Burn-E99/TheArtificer/blob/master/TERMS.md).
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ pidfile="/var/dbots/TheArtificer/artificer.pid"
|
|||
|
||||
artificer_root="/var/dbots/TheArtificer"
|
||||
artificer_write="./logs/,./src/endpoints/gets/heatmap.png"
|
||||
artificer_read="./src/solver/,./src/endpoints/gets/heatmap-base.png,./src/endpoints/gets/heatmap.png,./config.ts,./deps.ts,./src/mod.d.ts"
|
||||
artificer_read="./src/artigen/,./src/endpoints/gets/heatmap-base.png,./src/endpoints/gets/heatmap.png,./config.ts,./flags.ts"
|
||||
artificer_log="/var/log/artificer.log"
|
||||
|
||||
artificer_chdir="${artificer_root}"
|
||||
|
|
|
@ -6,7 +6,7 @@ After=network.target
|
|||
[Service]
|
||||
Type=simple
|
||||
PIDFile=/run/deno.pid
|
||||
ExecStart=/root/.deno/bin/deno run --allow-write=./logs/,./src/endpoints/gets/heatmap.png --allow-read=./src/solver/,./src/endpoints/gets/heatmap-base.png,./src/endpoints/gets/heatmap.png,./config.ts,./deps.ts,./src/mod.d.ts --allow-net --allow-import .\mod.ts
|
||||
ExecStart=/root/.deno/bin/deno run --allow-write=./logs/,./src/endpoints/gets/heatmap.png --allow-read=./src/artigen/,./src/endpoints/gets/heatmap-base.png,./src/endpoints/gets/heatmap.png,./config.ts,./flags.ts --allow-net --allow-import .\mod.ts
|
||||
RestartSec=60
|
||||
Restart=on-failure
|
||||
|
||||
|
|
|
@ -1,15 +1,26 @@
|
|||
export const config = {
|
||||
name: 'The Artificer', // Name of the bot
|
||||
version: '3.0.0', // Version of the bot
|
||||
maxFileSize: 8_388_290, // Max file size bot can send
|
||||
version: '4.1.2', // Version of the bot
|
||||
token: 'the_bot_token', // Discord API Token for this bot
|
||||
localtoken: 'local_testing_token', // Discord API Token for a secondary OPTIONAL testing bot, THIS MUST BE DIFFERENT FROM "token"
|
||||
prefix: '[[', // Prefix for all commands
|
||||
postfix: ']]', // Postfix for rolling command
|
||||
limits: {
|
||||
// Limits for the bot functions
|
||||
maxLoops: 5000000, // Determines how long the bot will attempt a roll, number of loops before it kills a roll. Increase this at your own risk, originally was set to 5 Million before rollWorkers were added, increased to 10 Million since multiple rolls can be handled concurrently
|
||||
alias: {
|
||||
// Roll Alias system
|
||||
maxNameLength: 200, // Max alias name length allowed in DB
|
||||
free: {
|
||||
user: 100, // Allows users to have 100 aliased rolls for free
|
||||
guild: 1_000, // Allows guilds to have 1000 aliased rolls for free
|
||||
},
|
||||
},
|
||||
maxLoops: 2_000_000, // Determines how long the bot will attempt a roll, number of loops before it kills a roll. Increase this at your own risk.
|
||||
maxWorkers: 16, // Maximum number of worker threads to spawn at once (Set this to less than the number of threads your CPU has, Artificer will eat it all if too many rolls happen at once)
|
||||
workerTimeout: 300000, // Maximum time before the bot kills a worker thread in ms
|
||||
workerTimeout: 300_000, // Maximum time before the bot kills a worker thread in ms
|
||||
defaultSimulatedNominal: 10_000, // Default number of loops to run for simulating a nominal
|
||||
maxSimulatedNominal: 100_000, // Max number of loops a user can specify for simulating a nominal
|
||||
},
|
||||
api: {
|
||||
// Setting for the built-in API
|
||||
|
@ -17,25 +28,34 @@ export const config = {
|
|||
publicDomain: 'http://example.com/', // Public domain that the API is behind, should end with a /
|
||||
port: 8080, // Port for the API to listen on
|
||||
supportURL: 'your_support_url_for_api_abuse', // Fill this in with the way you wish to be contacted when somebody needs to report API key abuse
|
||||
rateLimitTime: 10000, // Time range for how often the API rate limits will be lifted (time in ms)
|
||||
rateLimitTime: 10_000, // Time range for how often the API rate limits will be lifted (time in ms)
|
||||
rateLimitCnt: 10, // Amount of requests that can be made (successful or not) during above time range before getting rate limited
|
||||
admin: 0n, // Discord user ID of the bot admin, this user will be the user that can ban/unban user/channel combos and API keys
|
||||
adminKey: 'your_25char_api_token', // API Key generated by nanoid that is 25 char long, this gets pre-populated into all_keys
|
||||
email: 0n, // Temporary set up for email, this will be adjusted to an actual email using deno-smtp in the future.
|
||||
},
|
||||
db: {
|
||||
// Settings for the MySQL database, this is required for use with the API, if you do not want to set this up, you will need to rip all code relating to the DB out of the bot
|
||||
// Settings for the MySQL database, this is required for use with the API, roll aliases, and inline rolling
|
||||
host: '', // IP address for the db, usually localhost
|
||||
localhost: '', // IP address for a secondary OPTIONAL local testing DB, usually also is localhost, but depends on your dev environment
|
||||
port: 3306, // Port for the db
|
||||
username: '', // Username for the account that will access your DB, this account will need "DB Manager" admin rights and "REFERENCES" Global Privalages
|
||||
username: '', // Username for the account that will access your DB, this account will need "DB Manager" admin rights and "REFERENCES" Global Privileges
|
||||
password: '', // Password for the account, user account may need to be authenticated with the "Standard" Authentication Type if this does not work out of the box
|
||||
name: '', // Name of the database Schema to use for the bot
|
||||
},
|
||||
logRolls: false, // Enables logging of roll commands, this should be left disabled for privacy, but exists to allow verification of rolls before deployment, all API rolls will always be logged no matter what this is set to
|
||||
links: {
|
||||
// Links that are used in the bot
|
||||
sourceCode: 'https://github.com/Burn-E99/TheArtificer', // Link to the repository
|
||||
supportServer: '', // Invite link to the Discord support server
|
||||
roll20Formatting: 'https://help.roll20.net/hc/en-us/articles/360037773133-Dice-Reference', // Link to Roll20 Dice Reference
|
||||
mathDocs: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math', // Link to the MDN docs for Math
|
||||
homePage: '', // Link to the bot's home/ad page
|
||||
privacyPolicy: '', // Link to the current Privacy Policy
|
||||
termsOfService: '', // Link to the current Terms of Service
|
||||
},
|
||||
logChannel: 0n, // Discord channel ID where the bot should put startup messages and other error messages needed
|
||||
reportChannel: 0n, // Discord channel ID where reports will be sent when using the built-in report command
|
||||
devServer: 0n, // Discord guild ID where testing of indev features/commands will be handled, used in conjuction with the DEVMODE bool in mod.ts
|
||||
devServer: 0n, // Discord guild ID where testing of indev features/commands will be handled, used in conjunction with the DEVMODE bool in mod.ts
|
||||
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
|
||||
{
|
||||
|
|
242
db/initialize.ts
242
db/initialize.ts
|
@ -1,12 +1,11 @@
|
|||
// This file will create all tables for the artificer schema
|
||||
// DATA WILL BE LOST IF DB ALREADY EXISTS, RUN AT OWN RISK
|
||||
import config from '~config';
|
||||
|
||||
import config from '../config.ts';
|
||||
import dbClient from '../src/db/client.ts';
|
||||
import dbClient from 'db/client.ts';
|
||||
|
||||
console.log('Attempting to create DB');
|
||||
await dbClient.execute(`CREATE SCHEMA IF NOT EXISTS ${config.db.name};`);
|
||||
console.log('test');
|
||||
await dbClient.execute(`USE ${config.db.name}`);
|
||||
console.log('DB created');
|
||||
|
||||
|
@ -21,162 +20,203 @@ await dbClient.execute(`DROP TABLE IF EXISTS roll_time_heatmap;`);
|
|||
await dbClient.execute(`DROP PROCEDURE IF EXISTS INC_CNT;`);
|
||||
await dbClient.execute(`DROP TABLE IF EXISTS command_cnt;`);
|
||||
await dbClient.execute(`DROP TABLE IF EXISTS ignore_list;`);
|
||||
await dbClient.execute(`DROP TABLE IF EXISTS allow_inline;`);
|
||||
await dbClient.execute(`DROP TABLE IF EXISTS aliases;`);
|
||||
await dbClient.execute(`DROP TABLE IF EXISTS allow_unrestricted_repeat;`);
|
||||
console.log('Tables dropped');
|
||||
|
||||
// Holds guilds that have explicitly allowed anyone to repeat anyone's rolls
|
||||
console.log('Attempting to create table allow_unrestricted_repeat');
|
||||
await dbClient.execute(`
|
||||
CREATE TABLE allow_unrestricted_repeat (
|
||||
guildid bigint unsigned NOT NULL,
|
||||
PRIMARY KEY (guildid),
|
||||
UNIQUE KEY allow_unrestricted_repeat_guildid_UNIQUE (guildid)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
`);
|
||||
console.log('Table created');
|
||||
|
||||
// Holds all aliases that have been created
|
||||
console.log('Attempting to create table aliases');
|
||||
await dbClient.execute(`
|
||||
CREATE TABLE aliases (
|
||||
guildid bigint unsigned NOT NULL,
|
||||
userid bigint unsigned NOT NULL,
|
||||
aliasName varchar(200) NOT NULL,
|
||||
rollStr varchar(4000) NOT NULL,
|
||||
yVarCnt tinyint unsigned NOT NULL,
|
||||
premium tinyint(1) NOT NULL,
|
||||
createdAt timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (guildid, userid, aliasName)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
`);
|
||||
console.log('Table created');
|
||||
|
||||
// Holds guilds that have explicitly allowed inline rolls
|
||||
console.log('Attempting to create table allow_inline');
|
||||
await dbClient.execute(`
|
||||
CREATE TABLE allow_inline (
|
||||
guildid bigint unsigned NOT NULL,
|
||||
PRIMARY KEY (guildid),
|
||||
UNIQUE KEY allow_inline_guildid_UNIQUE (guildid)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
`);
|
||||
console.log('Table created');
|
||||
|
||||
// Table to hold list of users who want to be ignored by the bot
|
||||
console.log('Attempting to create table ignore_list');
|
||||
await dbClient.execute(`
|
||||
CREATE TABLE ignore_list (
|
||||
userid bigint unsigned NOT NULL,
|
||||
PRIMARY KEY (userid),
|
||||
UNIQUE KEY ignore_list_userid_UNIQUE (userid)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
CREATE TABLE ignore_list (
|
||||
userid bigint unsigned NOT NULL,
|
||||
PRIMARY KEY (userid),
|
||||
UNIQUE KEY ignore_list_userid_UNIQUE (userid)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
`);
|
||||
console.log('Table created');
|
||||
|
||||
// Light telemetry on how many commands have been run
|
||||
console.log('Attempting to create table command_cnt');
|
||||
await dbClient.execute(`
|
||||
CREATE TABLE command_cnt (
|
||||
command char(20) NOT NULL,
|
||||
count bigint unsigned NOT NULL DEFAULT 0,
|
||||
hourlyRate float unsigned NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (command),
|
||||
UNIQUE KEY command_cnt_command_UNIQUE (command)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
CREATE TABLE command_cnt (
|
||||
command char(20) NOT NULL,
|
||||
count bigint unsigned NOT NULL DEFAULT 0,
|
||||
hourlyRate float unsigned NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (command),
|
||||
UNIQUE KEY command_cnt_command_UNIQUE (command)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
`);
|
||||
console.log('Table created');
|
||||
|
||||
console.log('Attempt creating increment count Stored Procedure');
|
||||
await dbClient.execute(`
|
||||
CREATE PROCEDURE INC_CNT(
|
||||
IN cmd CHAR(20)
|
||||
)
|
||||
BEGIN
|
||||
declare oldcnt bigint unsigned;
|
||||
set oldcnt = (SELECT count FROM command_cnt WHERE command = cmd);
|
||||
UPDATE command_cnt SET count = oldcnt + 1 WHERE command = cmd;
|
||||
END
|
||||
CREATE PROCEDURE INC_CNT(
|
||||
IN cmd CHAR(20)
|
||||
)
|
||||
BEGIN
|
||||
declare oldcnt bigint unsigned;
|
||||
set oldcnt = (SELECT count FROM command_cnt WHERE command = cmd);
|
||||
UPDATE command_cnt SET count = oldcnt + 1 WHERE command = cmd;
|
||||
END
|
||||
`);
|
||||
console.log('Stored Procedure created');
|
||||
|
||||
// Holds daily average of commands
|
||||
console.log('Attempting to create table roll_time_heatmap');
|
||||
await dbClient.execute(`
|
||||
CREATE TABLE roll_time_heatmap (
|
||||
hour tinyint(1) unsigned NOT NULL,
|
||||
sunday bigint unsigned NOT NULL DEFAULT 0,
|
||||
monday bigint unsigned NOT NULL DEFAULT 0,
|
||||
tuesday bigint unsigned NOT NULL DEFAULT 0,
|
||||
wednesday bigint unsigned NOT NULL DEFAULT 0,
|
||||
thursday bigint unsigned NOT NULL DEFAULT 0,
|
||||
friday bigint unsigned NOT NULL DEFAULT 0,
|
||||
saturday bigint unsigned NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (hour),
|
||||
UNIQUE KEY roll_time_heatmap_hour_UNIQUE (hour)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
CREATE TABLE roll_time_heatmap (
|
||||
hour tinyint(1) unsigned NOT NULL,
|
||||
sunday bigint unsigned NOT NULL DEFAULT 0,
|
||||
monday bigint unsigned NOT NULL DEFAULT 0,
|
||||
tuesday bigint unsigned NOT NULL DEFAULT 0,
|
||||
wednesday bigint unsigned NOT NULL DEFAULT 0,
|
||||
thursday bigint unsigned NOT NULL DEFAULT 0,
|
||||
friday bigint unsigned NOT NULL DEFAULT 0,
|
||||
saturday bigint unsigned NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (hour),
|
||||
UNIQUE KEY roll_time_heatmap_hour_UNIQUE (hour)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
`);
|
||||
console.log('Table created');
|
||||
|
||||
console.log('Attempt creating increment heatmap Stored Procedure');
|
||||
await dbClient.execute(`
|
||||
CREATE PROCEDURE INC_HEATMAP(
|
||||
IN dy varchar(10),
|
||||
IN hr tinyint(1)
|
||||
)
|
||||
BEGIN
|
||||
SET @s1=CONCAT('SELECT ',dy,' FROM roll_time_heatmap WHERE hour = ',hr,' INTO @oldcnt');
|
||||
PREPARE stmt1 FROM @s1;
|
||||
EXECUTE stmt1;
|
||||
DEALLOCATE PREPARE stmt1;
|
||||
CREATE PROCEDURE INC_HEATMAP(
|
||||
IN dy varchar(10),
|
||||
IN hr tinyint(1)
|
||||
)
|
||||
BEGIN
|
||||
SET @s1=CONCAT('SELECT ',dy,' FROM roll_time_heatmap WHERE hour = ',hr,' INTO @oldcnt');
|
||||
PREPARE stmt1 FROM @s1;
|
||||
EXECUTE stmt1;
|
||||
DEALLOCATE PREPARE stmt1;
|
||||
|
||||
SET @s2=CONCAT('UPDATE roll_time_heatmap SET ',dy,' = @oldcnt + 1 WHERE hour = ',hr);
|
||||
PREPARE stmt2 FROM @s2;
|
||||
EXECUTE stmt2;
|
||||
DEALLOCATE PREPARE stmt2;
|
||||
END
|
||||
SET @s2=CONCAT('UPDATE roll_time_heatmap SET ',dy,' = @oldcnt + 1 WHERE hour = ',hr);
|
||||
PREPARE stmt2 FROM @s2;
|
||||
EXECUTE stmt2;
|
||||
DEALLOCATE PREPARE stmt2;
|
||||
END
|
||||
`);
|
||||
console.log('Stored Procedure created');
|
||||
|
||||
// Roll log, holds rolls when requests
|
||||
console.log('Attempting to create table roll_log');
|
||||
await dbClient.execute(`
|
||||
CREATE TABLE roll_log (
|
||||
id int unsigned NOT NULL AUTO_INCREMENT,
|
||||
input text NOT NULL,
|
||||
resultid bigint NULL,
|
||||
result longtext NOT NULL,
|
||||
createdAt timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
api tinyint(1) NOT NULL,
|
||||
error tinyint(1) NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY roll_log_id_UNIQUE (id),
|
||||
UNIQUE KEY roll_log_resultid_UNIQUE (resultid)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
CREATE TABLE roll_log (
|
||||
id int unsigned NOT NULL AUTO_INCREMENT,
|
||||
input text NOT NULL,
|
||||
resultid bigint NULL,
|
||||
result longtext NOT NULL,
|
||||
createdAt timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
api tinyint(1) NOT NULL,
|
||||
error tinyint(1) NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY roll_log_id_UNIQUE (id),
|
||||
UNIQUE KEY roll_log_resultid_UNIQUE (resultid)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
`);
|
||||
console.log('Table created');
|
||||
|
||||
// Api guild settings
|
||||
console.log('Attempting to create table allowed_guilds');
|
||||
await dbClient.execute(`
|
||||
CREATE TABLE allowed_guilds (
|
||||
guildid bigint unsigned NOT NULL,
|
||||
channelid bigint unsigned NOT NULL,
|
||||
createdAt timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
active tinyint(1) NOT NULL DEFAULT 0,
|
||||
banned tinyint(1) NOT NULL DEFAULT 0,
|
||||
hidewarn tinyint(1) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (guildid, channelid)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
CREATE TABLE allowed_guilds (
|
||||
guildid bigint unsigned NOT NULL,
|
||||
channelid bigint unsigned NOT NULL,
|
||||
createdAt timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
active tinyint(1) NOT NULL DEFAULT 0,
|
||||
banned tinyint(1) NOT NULL DEFAULT 0,
|
||||
hidewarn tinyint(1) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (guildid, channelid)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
`);
|
||||
console.log('Table created');
|
||||
|
||||
// Api keys
|
||||
console.log('Attempting to create table all_keys');
|
||||
await dbClient.execute(`
|
||||
CREATE TABLE all_keys (
|
||||
userid bigint unsigned NOT NULL,
|
||||
apiKey char(25) NOT NULL,
|
||||
deleteCode char(10) NULL,
|
||||
email char(255) NULL,
|
||||
createdAt timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
active tinyint(1) NOT NULL DEFAULT 1,
|
||||
banned tinyint(1) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (userid),
|
||||
UNIQUE KEY all_keys_userid_UNIQUE (userid),
|
||||
UNIQUE KEY all_keys_apiKey_UNIQUE (apiKey),
|
||||
UNIQUE KEY all_keys_email_UNIQUE (email)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
CREATE TABLE all_keys (
|
||||
userid bigint unsigned NOT NULL,
|
||||
apiKey char(25) NOT NULL,
|
||||
deleteCode char(10) NULL,
|
||||
email char(255) NULL,
|
||||
createdAt timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
active tinyint(1) NOT NULL DEFAULT 1,
|
||||
banned tinyint(1) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (userid),
|
||||
UNIQUE KEY all_keys_userid_UNIQUE (userid),
|
||||
UNIQUE KEY all_keys_apiKey_UNIQUE (apiKey),
|
||||
UNIQUE KEY all_keys_email_UNIQUE (email)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
`);
|
||||
console.log('Table created');
|
||||
|
||||
// Api user settings
|
||||
console.log('Attempting to create table allowed_channels');
|
||||
await dbClient.execute(`
|
||||
CREATE TABLE allowed_channels (
|
||||
userid bigint unsigned NOT NULL,
|
||||
channelid bigint unsigned NOT NULL,
|
||||
createdAt timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
active tinyint(1) NOT NULL DEFAULT 1,
|
||||
banned tinyint(1) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (userid, channelid),
|
||||
CONSTRAINT allowed_channels_userid_FK FOREIGN KEY (userid) REFERENCES all_keys (userid) ON DELETE RESTRICT ON UPDATE RESTRICT
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
CREATE TABLE allowed_channels (
|
||||
userid bigint unsigned NOT NULL,
|
||||
channelid bigint unsigned NOT NULL,
|
||||
createdAt timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
active tinyint(1) NOT NULL DEFAULT 1,
|
||||
banned tinyint(1) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (userid, channelid),
|
||||
CONSTRAINT allowed_channels_userid_FK FOREIGN KEY (userid) REFERENCES all_keys (userid) ON DELETE RESTRICT ON UPDATE RESTRICT
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
`);
|
||||
console.log('Table created');
|
||||
|
||||
// Database sizes view
|
||||
console.log('Attempting to create view db_size');
|
||||
await dbClient.execute(`
|
||||
CREATE VIEW db_size AS
|
||||
SELECT
|
||||
table_name AS "table",
|
||||
ROUND(((data_length + index_length) / 1024 / 1024), 3) AS "size",
|
||||
table_rows AS "rows"
|
||||
FROM information_schema.TABLES
|
||||
WHERE
|
||||
table_schema = "${config.db.name}"
|
||||
AND table_name <> "db_size";
|
||||
CREATE VIEW db_size AS
|
||||
SELECT
|
||||
table_name AS "table",
|
||||
ROUND(((data_length + index_length) / 1024 / 1024), 3) AS "size",
|
||||
table_rows AS "rows"
|
||||
FROM information_schema.TABLES
|
||||
WHERE
|
||||
table_schema = "${config.db.name}"
|
||||
AND table_name <> "db_size";
|
||||
`);
|
||||
console.log('View Created');
|
||||
|
||||
|
|
|
@ -1,34 +1,37 @@
|
|||
// This file will populate the tables with default values
|
||||
import config from '~config';
|
||||
|
||||
import config from '../config.ts';
|
||||
import dbClient from '../src/db/client.ts';
|
||||
import dbClient from 'db/client.ts';
|
||||
|
||||
console.log('Attempting to populate DB Admin API key');
|
||||
await dbClient.execute('INSERT INTO all_keys(userid,apiKey) values(?,?)', [config.api.admin, config.api.adminKey]).catch((e) => {
|
||||
console.log('Failed to insert into database', e);
|
||||
});
|
||||
console.log('Inesrtion done');
|
||||
console.log('Insertion done');
|
||||
|
||||
console.log('Attempting to insert default commands into command_cnt');
|
||||
const commands = [
|
||||
'ping',
|
||||
'rip',
|
||||
'rollhelp',
|
||||
'alias',
|
||||
'api',
|
||||
'audit',
|
||||
'emojis',
|
||||
'heatmap',
|
||||
'help',
|
||||
'info',
|
||||
'version',
|
||||
'report',
|
||||
'stats',
|
||||
'roll',
|
||||
'emojis',
|
||||
'api',
|
||||
'privacy',
|
||||
'inline',
|
||||
'mention',
|
||||
'audit',
|
||||
'heatmap',
|
||||
'rollDecorators',
|
||||
'opt-out',
|
||||
'opt-in',
|
||||
'opt-out',
|
||||
'ping',
|
||||
'privacy',
|
||||
'rip',
|
||||
'repeat',
|
||||
'report',
|
||||
'roll',
|
||||
'rolldecorators',
|
||||
'rollhelp',
|
||||
'stats',
|
||||
'version',
|
||||
];
|
||||
for (const command of commands) {
|
||||
await dbClient.execute('INSERT INTO command_cnt(command) values(?)', [command]).catch((e) => {
|
||||
|
|
22
deno.json
22
deno.json
|
@ -19,5 +19,27 @@
|
|||
"indentWidth": 2,
|
||||
"singleQuote": true,
|
||||
"proseWrap": "preserve"
|
||||
},
|
||||
"nodeModulesDir": "none",
|
||||
"imports": {
|
||||
"@discordeno": "https://deno.land/x/discordeno@12.0.1/mod.ts",
|
||||
"@imagescript": "https://deno.land/x/imagescript@1.3.0/mod.ts",
|
||||
"@Log4Deno": "https://raw.githubusercontent.com/Burn-E99/Log4Deno/V2.1.1/mod.ts",
|
||||
"@mysql": "https://deno.land/x/mysql@v2.12.1/mod.ts",
|
||||
"@nanoid": "https://deno.land/x/nanoid@v3.0.0/mod.ts",
|
||||
"@showdown": "npm:showdown@2.1.0",
|
||||
"@std/http": "jsr:@std/http@1.0.15",
|
||||
"~config": "./config.ts",
|
||||
"~flags": "./flags.ts",
|
||||
"artigen/": "./src/artigen/",
|
||||
"commands/": "./src/commands/",
|
||||
"db/": "./src/db/",
|
||||
"embeds/": "./src/embeds/",
|
||||
"endpoints/": "./src/endpoints/",
|
||||
"events/": "./src/events/",
|
||||
"utils/": "./src/utils/",
|
||||
"src/api.ts": "./src/api.ts",
|
||||
"src/events.ts": "./src/events.ts",
|
||||
"src/mod.d.ts": "./src/mod.d.ts"
|
||||
}
|
||||
}
|
30
deno.lock
30
deno.lock
|
@ -6,10 +6,12 @@
|
|||
"jsr:@std/fmt@^1.0.7": "1.0.7",
|
||||
"jsr:@std/html@^1.0.3": "1.0.3",
|
||||
"jsr:@std/http@1.0.15": "1.0.15",
|
||||
"jsr:@std/io@0.225.2": "0.225.2",
|
||||
"jsr:@std/media-types@^1.1.0": "1.1.0",
|
||||
"jsr:@std/net@^1.0.4": "1.0.4",
|
||||
"jsr:@std/path@^1.0.9": "1.0.9",
|
||||
"jsr:@std/streams@^1.0.9": "1.0.9"
|
||||
"jsr:@std/streams@^1.0.9": "1.0.9",
|
||||
"npm:showdown@2.1.0": "2.1.0"
|
||||
},
|
||||
"jsr": {
|
||||
"@std/cli@1.0.17": {
|
||||
|
@ -37,6 +39,9 @@
|
|||
"jsr:@std/streams"
|
||||
]
|
||||
},
|
||||
"@std/io@0.225.2": {
|
||||
"integrity": "3c740cd4ee4c082e6cfc86458f47e2ab7cb353dc6234d5e9b1f91a2de5f4d6c7"
|
||||
},
|
||||
"@std/media-types@1.1.0": {
|
||||
"integrity": "c9d093f0c05c3512932b330e3cc1fe1d627b301db33a4c2c2185c02471d6eaa4"
|
||||
},
|
||||
|
@ -50,6 +55,17 @@
|
|||
"integrity": "a9d26b1988cdd7aa7b1f4b51e1c36c1557f3f252880fa6cc5b9f37078b1a5035"
|
||||
}
|
||||
},
|
||||
"npm": {
|
||||
"commander@9.5.0": {
|
||||
"integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ=="
|
||||
},
|
||||
"showdown@2.1.0": {
|
||||
"integrity": "sha512-/6NVYu4U819R2pUIk79n67SYgJHWCce0a5xTP979WbNp0FL9MN1I1QK662IDU1b6JzKTvmhgI7T7JYIxBi3kMQ==",
|
||||
"dependencies": [
|
||||
"commander"
|
||||
]
|
||||
}
|
||||
},
|
||||
"redirects": {
|
||||
"https://deno.land/std/hash/mod.ts": "https://deno.land/std@0.224.0/hash/mod.ts"
|
||||
},
|
||||
|
@ -823,6 +839,18 @@
|
|||
"https://raw.githubusercontent.com/Burn-E99/Log4Deno/V2.0.0/deps.ts": "9a1b2d559fc8c33ae1aeed899aa821f53f9d094e9df40bd4b51b099c58961cd7",
|
||||
"https://raw.githubusercontent.com/Burn-E99/Log4Deno/V2.0.0/mod.ts": "d9c38a41a405cf5732c9233c2391a1d7f5a12d0e464aace6f8f596fabf5f21ba",
|
||||
"https://raw.githubusercontent.com/Burn-E99/Log4Deno/V2.0.0/src/logger.ts": "a1924f1f02b35a7501161349de90b60a3aa329e12f1033fdb212b598542897c4",
|
||||
"https://raw.githubusercontent.com/Burn-E99/Log4Deno/V2.1.0/deps.ts": "3ab026026d146ca5e7160b16146d5665e45487a62749a7970f8e00c0c934874d",
|
||||
"https://raw.githubusercontent.com/Burn-E99/Log4Deno/V2.1.0/mod.ts": "d9c38a41a405cf5732c9233c2391a1d7f5a12d0e464aace6f8f596fabf5f21ba",
|
||||
"https://raw.githubusercontent.com/Burn-E99/Log4Deno/V2.1.0/src/logger.ts": "78072b8257a25b4e6adc03d5b92d64ef68b215159a732832fe6020bdebce2ec7",
|
||||
"https://raw.githubusercontent.com/Burn-E99/Log4Deno/V2.1.1/deps.ts": "3ab026026d146ca5e7160b16146d5665e45487a62749a7970f8e00c0c934874d",
|
||||
"https://raw.githubusercontent.com/Burn-E99/Log4Deno/V2.1.1/mod.ts": "d9c38a41a405cf5732c9233c2391a1d7f5a12d0e464aace6f8f596fabf5f21ba",
|
||||
"https://raw.githubusercontent.com/Burn-E99/Log4Deno/V2.1.1/src/logger.ts": "b3a39724d58102dfbcdcd640a829cbfe1f083065060f68003f9c8fd49fdd658a",
|
||||
"https://unpkg.com/@evan/wasm@0.0.65/target/zlib/deno.js": "36cd3f1edd2f3a6d6fd4c2376f701c2748338c132703810d4866cfa52b5e7bf9"
|
||||
},
|
||||
"workspace": {
|
||||
"dependencies": [
|
||||
"jsr:@std/http@1.0.15",
|
||||
"npm:showdown@2.1.0"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
28
deps.ts
28
deps.ts
|
@ -1,28 +0,0 @@
|
|||
// All external dependancies are to be loaded here to make updating dependancy versions much easier
|
||||
export {
|
||||
botId,
|
||||
cache,
|
||||
cacheHandlers,
|
||||
DiscordActivityTypes,
|
||||
editBotNickname,
|
||||
editBotStatus,
|
||||
hasGuildPermissions,
|
||||
Intents,
|
||||
sendDirectMessage,
|
||||
sendMessage,
|
||||
startBot,
|
||||
} from 'https://deno.land/x/discordeno@12.0.1/mod.ts';
|
||||
|
||||
export type { CreateMessage, DiscordenoGuild, DiscordenoMessage, EmbedField } from 'https://deno.land/x/discordeno@12.0.1/mod.ts';
|
||||
|
||||
export { Client } from 'https://deno.land/x/mysql@v2.12.1/mod.ts';
|
||||
|
||||
export { STATUS_CODE, STATUS_TEXT } from 'jsr:@std/http@1.0.15';
|
||||
|
||||
export type { StatusCode } from 'jsr:@std/http@1.0.15';
|
||||
|
||||
export { nanoid } from 'https://deno.land/x/nanoid@v3.0.0/mod.ts';
|
||||
|
||||
export { initLog, log, LogTypes as LT } from 'https://raw.githubusercontent.com/Burn-E99/Log4Deno/V2.0.0/mod.ts';
|
||||
|
||||
export * as is from 'https://deno.land/x/imagescript@1.3.0/mod.ts';
|
|
@ -0,0 +1,19 @@
|
|||
## 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.
|
||||
|
||||
Guilds Owners or Admins must run the `[[api allow` command for any users to be able to use the `/api/roll` endpoint.
|
||||
|
||||
Every API request **requires** the header `X-Api-Key` with the value set to the API key granted to you.
|
||||
|
||||
* If an API fails, these are the possible responses:
|
||||
* `400` - Bad Request - Query parameters missing or malformed.
|
||||
* `403` - Forbidden - API Key is not authenticated or user does not match the owner of the API Key.
|
||||
* `404` - Not Found - Requested endpoint does not exist.
|
||||
* `429` - Too Many Requests - API rate limit exceeded, please slow down.
|
||||
* `500` - Internal Server Error - Something broke, if this continues to happen, please submit a GitHub issue.
|
||||
|
||||
Official API URL: `https://artificer.eanm.dev/api/`
|
||||
|
||||
API Documentation can be found in the `.bruno` folder, which can be viewed in [Bruno](https://www.usebruno.com/). API requests listed in the `Authenticated/Admin Requests` are only available to the admin user defined in `config.ts`.
|
||||
|
||||
API Key management via a basic GUI is available on the [API Tools](https://artificer.eanm.dev/) website.
|
|
@ -0,0 +1,232 @@
|
|||
## Available Commands
|
||||
The Artificer comes with a few supplemental commands to the main rolling command.
|
||||
|
||||
This document uses the default prefix (`[[`) on all commands listed. If a command starts with `/` (such as `/help`), this means the command is available as a Discord Slash Command.
|
||||
|
||||
* `/help` or `[[help` or `[[h` or `[[?`
|
||||
* Provides a message similar to this available commands block.
|
||||
* `[[rollhelp` or `[[??` or `[[rh` or `[[hr`
|
||||
* Opens the new help library.
|
||||
* `[[api [subcommand]`
|
||||
* Administrative tools for the bots's API. These commands may only be used by the Owner or Admins of your guild.
|
||||
* Available Subcommands:
|
||||
* `[[api help`
|
||||
* Provides a message similar to this subcommand description.
|
||||
* `[[api status`
|
||||
* Shows the current status of the API for this guild.
|
||||
* `[[api allow` or `[[api enable`
|
||||
* Allows API Rolls to be sent to this guild.
|
||||
* `[[api block` or `[[api disable`
|
||||
* Blocks API Rolls from being sent to this guild.
|
||||
* `[[api delete`
|
||||
* Deletes this guild from The Artificer's database.
|
||||
* `[[ping`
|
||||
* Tests the latency between you, Discord, and the bot.
|
||||
* `/info` or `[[info` or `[[i`
|
||||
* Outputs some information and links relating to the bot.
|
||||
* `/privacy` or `[[privacy` or `[[tos`
|
||||
* Prints some information about the Privacy Policy, found in `PRIVACY.md`.
|
||||
* `/version` or `[[version` or `[[v`
|
||||
* Prints out the current version of the bot.
|
||||
* `[[popcat` or `[[pop` or `[[p`
|
||||
* Sends the animated popcat emote for those who do not have Discord Nitro.
|
||||
* If bot is given the permission `Manage Messages`, the bot will remove the message requesting the emote.
|
||||
* `/stats` or `[[stats` or `[[s`
|
||||
* Prints out how many users, channels, and servers the bot is currently serving.
|
||||
* `/heatmap` or `[[heatmap` or `[[hm`
|
||||
* Heatmap of when the roll command is run the most.
|
||||
* `/report report-text:[issue-or-feature]` or `[[report [issue-or-feature]`
|
||||
* People aren't perfect, but this bot is trying to be.
|
||||
* If you encounter a command that errors out or returns something unexpected, please use this command to alert the developers of the problem.
|
||||
* Example:
|
||||
* `[[report [[2+2]] returned 5 when I expected it to return 4` will send the entire message after `[[report` to the devs via Discord.
|
||||
* `[[opt-out` or `[[ignore-me`
|
||||
* Adds you to an ignore list so the bot will never respond to you
|
||||
* `[[opt-in` **Available via DM ONLY**
|
||||
* Removes you from the ignore list
|
||||
* `/toggle-inline-rolls [subcommand]` or `[[inline [subcommand]`
|
||||
* Controls whether or not inline rolls can be done in a guild, defaults off. These commands may only be used by the Owner or Admins of your guild.
|
||||
* An inline roll is a roll that does not immediately start with `[[`, such as `test [[d20]]`.
|
||||
* Available subcommands:
|
||||
* `/toggle-inline-rolls help`
|
||||
* `[[inline help`
|
||||
* Provides a message similar to this subcommand description.
|
||||
* `/toggle-inline-rolls status`
|
||||
* `[[inline status`
|
||||
* Shows the current status of inline rolls for this guild.
|
||||
* `/toggle-inline-rolls enable`
|
||||
* `[[inline allow` or `[[inline enable`
|
||||
* Allows inline rolls in the guild.
|
||||
* `/toggle-inline-rolls disable`
|
||||
* `[[inline block` or `[[inline disable` or `[[inline delete`
|
||||
* Blocks inline rolls in the guild.
|
||||
* `/toggle-unrestricted-repeat [subcommand]` or `[[repeat [subcommand]`
|
||||
* Controls whether or not unrestricted repeat rolls can be done in a guild, defaults off. Unrestricted Repeat Rolls are whether or not anyone in a guild can use the `Repeat Roll` button on anyone's roll or only the original roller can use them. These commands may only be used by the Owner or Admins of your guild.
|
||||
* An inline roll is a roll that does not immediately start with `[[`, such as `test [[d20]]`.
|
||||
* Available subcommands:
|
||||
* `/toggle-unrestricted-repeat help`
|
||||
* `[[repeat help`
|
||||
* Provides a message similar to this subcommand description.
|
||||
* `/toggle-unrestricted-repeat status`
|
||||
* `[[repeat status`
|
||||
* Shows the current status of unrestricted repeat rolls for this guild.
|
||||
* `/toggle-unrestricted-repeat enable`
|
||||
* `[[repeat allow` or `[[repeat enable`
|
||||
* Allows unrestricted repeat rolls in the guild.
|
||||
* `/toggle-unrestricted-repeat disable`
|
||||
* `[[repeat block` or `[[repeat disable` or `[[repeat delete`
|
||||
* Blocks unrestricted repeat rolls in the guild.
|
||||
* `/alias [subcommand]` or `[[rollalias [subcommand]` or `[[ralias [subcommand]` or `[[alias [subcommand]` or `[[rolla [subcommand]` or `[[ra [subcommand]`
|
||||
* Custom Roll Alias System
|
||||
* Allows anyone to store a roll string as a shortcut/alias for later use/reuse.
|
||||
* Supports full roll syntax, plus y variables that are set every time the alias is called.
|
||||
* Every command has a matching "Guild Mode" command that modifies aliases linked to a guild instead of linked to a user account.
|
||||
* Available subcommands:
|
||||
* `/alias personal help`
|
||||
* `/alias guild help`
|
||||
* `[[ra help`
|
||||
* `[[ra guild help`
|
||||
* Provides a message similar to this subcommand description.
|
||||
* `/alias personal list-all`
|
||||
* `/alias guild list-all`
|
||||
* `[[ra list`
|
||||
* `[[ra guild list`
|
||||
* Lists all aliases currently set for your account or the guild you are in.
|
||||
* `/alias personal create alias-name:[aliasName] roll-string:[rollString...]`
|
||||
* `/alias guild create alias-name:[aliasName] roll-string:[rollString...]`
|
||||
* `[[ra add [aliasName] [rollString...]`
|
||||
* `[[ra guild add [aliasName] [rollString...]`
|
||||
* Creates the desired alias, saving the roll string to your account or the guild you are in.
|
||||
* `/alias personal replace alias-name:[aliasName] roll-string:[rollString...]`
|
||||
* `/alias guild replace alias-name:[aliasName] roll-string:[rollString...]`
|
||||
* `[[ra update [aliasName] [rollString...]`
|
||||
* `[[ra guild update [aliasName] [rollString...]`
|
||||
* Updates the desired alias, replacing the old roll string in your account or the guild you are in with the newly provided roll string.
|
||||
* `/alias personal view alias-name:[aliasName]`
|
||||
* `/alias guild view alias-name:[aliasName]`
|
||||
* `[[ra view [aliasName]`
|
||||
* `[[ra guild view [aliasName]`
|
||||
* View the saved roll string and how many yVars are needed for it.
|
||||
* `/alias personal delete-one alias-name:[aliasName] [verification-code:[verificationCode]?]`
|
||||
* `/alias guild delete-one alias-name:[aliasName] [verification-code:[verificationCode]?]`
|
||||
* `[[ra delete [aliasName] [verificationCode?]`
|
||||
* `[[ra guild delete [aliasName] [verificationCode?]`
|
||||
* Deletes the desired alias from your account or the guild you are in. Can be run without a verification code to get the needed code for deletion.
|
||||
* `/alias personal delete-all alias-name:[aliasName] [verification-code:[verificationCode]?]`
|
||||
* `/alias guild delete-all alias-name:[aliasName] [verification-code:[verificationCode]?]`
|
||||
* `[[ra delete-all [aliasName] [verificationCode?]`
|
||||
* `[[ra guild delete-all [aliasName] [verificationCode?]`
|
||||
* Deletes all aliases from your account or the guild you are in. Can be run without a verification code to get the needed code for deletion.
|
||||
* `/alias personal copy alias-name:[aliasName]`
|
||||
* `[[ra clone [aliasName]`
|
||||
* Copies the specified alias from your account to the guild you are in.
|
||||
* `/alias guild copy alias-name:[aliasName]`
|
||||
* `[[ra guild clone [aliasName]`
|
||||
* Copies the specified alias from the guild you are in to your account.
|
||||
* `/alias personal rename alias-name:[oldAliasName] alias-name-new:[newAliasName]`
|
||||
* `/alias guild rename alias-name:[oldAliasName] alias-name-new:[newAliasName]`
|
||||
* `[[ra rename [oldAliasName] [newAliasName]`
|
||||
* `[[ra guild rename [oldAliasName] [newAliasName]`
|
||||
* Renames the specified alias for your account or the guild you are in.
|
||||
* `/alias personal run alias-name:[aliasName] [y-variables:[yVars...]?]`
|
||||
* `[[ra [aliasName] [yVars?...]`
|
||||
* `[[ra run [aliasName] [yVars?...]`
|
||||
* Runs the desired personal alias with the specified yVars (if any are needed). If the alias is not found on your account, it will check the guild aliases and use a match from there if one exists.
|
||||
* `/alias guild run alias-name:[aliasName] [y-variables:[yVars...]?]`
|
||||
* `[[ra guild [aliasName] [yVars?...]`
|
||||
* `[[ra guild run [aliasName] [yVars?...]`
|
||||
* Runs the desired guild alias with the specified yVars (if any are needed).
|
||||
* `/roll roll-string:[rollString...]` or `[[xdydzracsq!]]` AKA Roll Command
|
||||
* This is the command the bot was built specifically for.
|
||||
* It looks a little complicated at first, but if you are familiar with the [Roll20 formatting](https://artificer.eanm.dev/roll20), this will be no different.
|
||||
* Any math (limited to exponential, 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.
|
||||
* Parameters for rolling:
|
||||
|
||||
| Parameter | Required? | Repeatable? | Description |
|
||||
|---------------|-------------|---------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| x | Optional | No | number of dice to roll, if omitted, 1 is used |
|
||||
| dy | Required | No | size of dice to roll, d20 = 20 sided die, replace y with `F` to roll the dice as Fate dice |
|
||||
| dz or dlz | Optional | No | drops the lowest z dice, cannot be used with any other drop or keep options |
|
||||
| kz or khz | Optional | No | keeps the highest z dice, cannot be used with any other drop or keep options |
|
||||
| dhz | Optional | No | drops the highest z dice, cannot be used with any other drop or keep options |
|
||||
| klz | Optional | No | keeps the lowest z dice, cannot be used with any other drop or keep options |
|
||||
| ra or r=a | Optional | Yes | rerolls any rolls that match a, r3 will reroll every die that land on 3, throwing out old rolls, cannot be used with ro |
|
||||
| r<a | Optional | Yes | rerolls any rolls that are less than or equal to a, r3 will reroll every die that land on 3, 2, or 1, throwing out old rolls, cannot be used with ro |
|
||||
| r>a | Optional | Yes | rerolls any rolls that are greater than or equal to a, r3 will reroll every die that land on 3 or greater, throwing out old rolls, cannot be used with ro |
|
||||
| roa or ro=a | Optional | Yes | rerolls any rolls that match a, r3 will reroll each die that lands on 3 ONLY ONE TIME, throwing out old rolls, cannot be used with r |
|
||||
| ro<a | Optional | Yes | rerolls any rolls that are less than or equal to a, r3 will reroll each die that lands on 3, 2, or 1 ONLY ONE TIME, throwing out old rolls, cannot be used with r |
|
||||
| ro>a | Optional | Yes | rerolls any rolls that are greater than or equal to a, r3 will reroll each die that lands on 3 or greater ONLY ONE TIME, throwing out old rolls, cannot be used with r |
|
||||
| csq or cs=q | Optional | Yes | changes crit score to q |
|
||||
| cs<q | Optional | Yes | changes crit score to be less than or equal to q |
|
||||
| cs>q | Optional | Yes | changes crit score to be greater than or equal to q |
|
||||
| cfq or cf=q | Optional | Yes | changes crit fail to q |
|
||||
| cf<q | Optional | Yes | changes crit fail to be less than or equal to q |
|
||||
| cf>q | Optional | Yes | changes crit fail to be greater than or equal to q |
|
||||
| ! | Optional | No | exploding, rolls another dy for every crit success |
|
||||
| !o | Optional | No | exploding once, rolls another dy for each original crit success |
|
||||
| !p | Optional | No | penetrating explosion, rolls one dy for each crit success, but subtracts one from each resulting explosion |
|
||||
| !! | Optional | No | compounding explosion, rolls one dy for each crit success, but adds the resulting explosion to the die that caused this explosion |
|
||||
| !=u | Optional | Yes | exploding, rolls another dy for every die that lands on u |
|
||||
| !>u | Optional | Yes | exploding, rolls another dy for every die that lands on u or greater |
|
||||
| !<u> | Optional | Yes | exploding, rolls another dy for every die that lands on u or less |
|
||||
| !o=u | Optional | Yes | exploding once, rolls another dy for each original die that landed on u |
|
||||
| !o>u | Optional | Yes | exploding once, rolls another dy for each original die that landed on u or greater |
|
||||
| !o<u | Optional | Yes | exploding once, rolls another dy for each original die that landed on u or less |
|
||||
| !p=u | Optional | Yes | penetrating explosion, rolls one dy for each die that lands on u, but subtracts one from each resulting explosion |
|
||||
| !p>u | Optional | Yes | penetrating explosion, rolls one dy for each die that lands on u or greater, but subtracts one from each resulting explosion |
|
||||
| !p<u | Optional | Yes | penetrating explosion, rolls one dy for each die that lands on u or under, but subtracts one from each resulting explosion |
|
||||
| !!=u | Optional | Yes | compounding explosion, rolls one dy for each die that lands on u, but adds the resulting explosion to the die that caused this explosion |
|
||||
| !!>u | Optional | Yes | compounding explosion, rolls one dy for each die that lands on u or greater, but adds the resulting explosion to the die that caused this explosion |
|
||||
| !!<u | Optional | Yes | compounding explosion, rolls one dy for each die that lands on u or under, but adds the resulting explosion to the die that caused this explosion |
|
||||
| m | Optional | No | matching dice, adds labels to any dice that match, cannot be combined with Target Number/Successes or Target Failures |
|
||||
| mz | Optional | No | matching dice, adds labels to any dice that have z or more matches, cannot be combined with Target Number/Successes or Target Failures |
|
||||
| mt | Optional | No | matching dice, adds labels to any dice that match, changes result to be the count of labels added, cannot be combined with Target Number/Successes or Target Failures |
|
||||
| mtz | Optional | No | matching dice, adds labels to any dice that have z or more matches, changes result to be the count of labels added, cannot be combined with Target Number/Successes or Target Failures |
|
||||
| s or sa | Optional | No | sort dice, sorts the list of dice for a roll in ascending order |
|
||||
| sd | Optional | No | sort dice, sorts the list of dice for a roll in descending order |
|
||||
| =z | Optional | Yes | target number/success, counts and marks dice as successful when they land on z, cannot be combined with the Dice Matching option |
|
||||
| <z | Optional | Yes | target number/success, counts and marks dice as successful when they land on z or less, cannot be combined with the Dice Matching option |
|
||||
| >z | Optional | Yes | target number/success, counts and marks dice as successful when they land on z or greater, cannot be combined with the Dice Matching option |
|
||||
| fz or f=z | Optional | Yes | target failures, counts and marks dice as failed when they land on z, cannot be combined with the Dice Matching option |
|
||||
| f<z | Optional | Yes | target failures, counts and marks dice as failed when they land on z or less, cannot be combined with the Dice Matching option |
|
||||
| f>z | Optional | Yes | target failures, counts and marks dice as failed when they land on z or greater, cannot be combined with the Dice Matching option |
|
||||
|
||||
* 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 can also handle some custom format dice:
|
||||
* CWOD Dice - `[[xcwody]]`
|
||||
* `x` - Number of CWOD dice to roll
|
||||
* `y` - Difficulty to roll at
|
||||
* OVA Dice - `[[xovady]]`
|
||||
* `x` - Number of OVA dice to roll
|
||||
* `y` - Size of the die to roll (defaults to 6 if omitted)
|
||||
* This command also has some useful decorators that can used. These decorators simply need to be placed after all rolls in the message:
|
||||
* `-c` - Count - Shows the Count Embed, containing the count of successful rolls, failed rolls, rerolls, drops, and explosions
|
||||
* `-nd` - No Details - Suppresses all details of the requested roll
|
||||
* `-snd` - Super No Details - Suppresses all details of the requested roll and hides no details message
|
||||
* `-s` - Spoiler - Spoilers all details of the requested roll
|
||||
* `-m` or `-max` - Maximize Roll - Rolls the theoretical maximum roll, cannot be used with `-n`, `-min`, or `-sn`
|
||||
* `-min` - Minimize Roll - Rolls the theoretical minimum roll, cannot be used with `-m`, `-max`, `-n`, or `-sn`
|
||||
* `-n` - Nominal Roll - Rolls the theoretical nominal roll, cannot be used with `-m`, `-max`, `-min`, or `-sn`
|
||||
* `-sn` or `-sn [number]` - Simulated Nominal - Rolls the requests roll many times to approximately simulate the nominal of complex rolls, can specify the amount or accept default amount by not specify the amount, cannot be used with `-m`, `-max`, `-min`, `-n`, or `-cc`
|
||||
* `-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
|
||||
* `-ct` - Comma Totals - Adds commas to totals for readability
|
||||
* `-cc` - Confirm Critical Hits - Automatically rerolls whenever a crit hits, cannot be used with `-sn`
|
||||
* `-rd` - Roll Distribution - Shows a raw roll distribution of all dice in roll
|
||||
* `-hr` - Hide Raw - Hide the raw input, showing only the results/details of the roll
|
||||
* `-nv` or `-vn` - Number Variables - Adds `xN` before each roll command in the details section for debug reasons
|
||||
* `-cd` - Custom Dice shapes - Allows a list of `name:[side1,side2,...,sideN]` separated by `;` to be passed to create special shaped dice
|
||||
* `-ns` - No Spaces - Removes the default padding added space between rolls (`[[d4]][[d4]]` will output `22` instead of `2 2`)
|
||||
* `-yvariables y0,y1,...,yN` - Y Variables - Intended for internal use only, but is mentioned here since it is available externally. Takes a comma separated list of numbers. Unlike other decorators, this one will not be shown in the raw output.
|
||||
* The results have some formatting applied on them to provide details on what happened during this roll.
|
||||
* Critical successes will be **bolded**
|
||||
* Critical fails will be <ins>underlined</ins>
|
||||
* Rolls that were dropped or rerolled ~~crossed out~~
|
||||
* Rolls that exploded have an `!` added after them
|
|
@ -0,0 +1,10 @@
|
|||
## Self Hosting The Artificer
|
||||
The Artificer is built on [Deno](https://deno.land/) `v2.2.9` using [Discordeno](https://discordeno.mod.land/) `v12.0.1`. 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 Privileges. Once the DB is installed and a user is setup, run the provided `db\initialize.ts` to create the schema and tables. After this, run `db\populateDefaults.ts` to insert some needed values into the tables.
|
||||
|
||||
Once everything is set up, starting the bot can simply be done with the command in `start.command`.
|
||||
|
||||
If you choose to run version `1.1.0` or newer, ensure you disable the API in `config.ts` or verify you have properly secured your instance of The Artificer. If you enable the API, you should manually generate a 25 char nanoid and place it in `config.api.adminKey` and copy your `userid` and place it in `config.api.admin` before running `db\populateDefaults.ts`.
|
||||
|
||||
If you disable the API, please note some features like the roll heatmap and roll webview will not be available.
|
4
flags.ts
4
flags.ts
|
@ -1,6 +1,6 @@
|
|||
// DEVMODE is to prevent users from accessing parts of the bot that are currently broken
|
||||
export const DEVMODE = false;
|
||||
// DEBUG is used to toggle the cmdPrompt
|
||||
// DEBUG is used to toggle the cmdPrompt and show debug log messages
|
||||
export const DEBUG = false;
|
||||
// LOCALMODE is used to run a differnt bot token for local testing
|
||||
// LOCALMODE is used to run a different bot token for local testing
|
||||
export const LOCALMODE = false;
|
||||
|
|
332
mod.ts
332
mod.ts
|
@ -3,31 +3,23 @@
|
|||
*
|
||||
* December 21, 2020
|
||||
*/
|
||||
import { Intents, startBot } from '@discordeno';
|
||||
import { initLog } from '@Log4Deno';
|
||||
|
||||
import config from './config.ts';
|
||||
import { DEBUG, DEVMODE, LOCALMODE } from './flags.ts';
|
||||
import {
|
||||
botId,
|
||||
cache,
|
||||
DiscordActivityTypes,
|
||||
DiscordenoGuild,
|
||||
DiscordenoMessage,
|
||||
editBotNickname,
|
||||
editBotStatus,
|
||||
initLog,
|
||||
Intents,
|
||||
log,
|
||||
LT,
|
||||
sendMessage,
|
||||
startBot,
|
||||
} from './deps.ts';
|
||||
import api from './src/api.ts';
|
||||
import dbClient from './src/db/client.ts';
|
||||
import { ignoreList } from './src/db/common.ts';
|
||||
import commands from './src/commands/_index.ts';
|
||||
import intervals from './src/intervals.ts';
|
||||
import { successColor, warnColor } from './src/commandUtils.ts';
|
||||
import utils from './src/utils.ts';
|
||||
import config from '~config';
|
||||
import { DEBUG, LOCALMODE } from '~flags';
|
||||
|
||||
import api from 'src/api.ts';
|
||||
import eventHandlers from 'src/events.ts';
|
||||
|
||||
// Extend the BigInt prototype to support JSON.stringify
|
||||
interface BigIntX extends BigInt {
|
||||
// Convert to BigInt to string form in JSON.stringify
|
||||
toJSON: () => string;
|
||||
}
|
||||
(BigInt.prototype as BigIntX).toJSON = function () {
|
||||
return this.toString();
|
||||
};
|
||||
|
||||
// Initialize logging client with folder to use for logs, needs --allow-write set on Deno startup
|
||||
initLog('logs', DEBUG);
|
||||
|
@ -36,299 +28,9 @@ initLog('logs', DEBUG);
|
|||
startBot({
|
||||
token: LOCALMODE ? config.localtoken : config.token,
|
||||
intents: [Intents.GuildMessages, Intents.DirectMessages, Intents.Guilds],
|
||||
eventHandlers: {
|
||||
ready: () => {
|
||||
log(LT.INFO, `${config.name} Logged in!`);
|
||||
editBotStatus({
|
||||
activities: [
|
||||
{
|
||||
name: 'Booting up . . .',
|
||||
type: DiscordActivityTypes.Game,
|
||||
createdAt: new Date().getTime(),
|
||||
},
|
||||
],
|
||||
status: 'online',
|
||||
});
|
||||
|
||||
// Interval to rotate the status text every 30 seconds to show off more commands
|
||||
setInterval(async () => {
|
||||
log(LT.LOG, 'Changing bot status');
|
||||
try {
|
||||
// Wrapped in try-catch due to hard crash possible
|
||||
editBotStatus({
|
||||
activities: [
|
||||
{
|
||||
name: await intervals.getRandomStatus(),
|
||||
type: DiscordActivityTypes.Game,
|
||||
createdAt: new Date().getTime(),
|
||||
},
|
||||
],
|
||||
status: 'online',
|
||||
});
|
||||
} catch (e) {
|
||||
log(LT.ERROR, `Failed to update status: ${JSON.stringify(e)}`);
|
||||
}
|
||||
}, 30000);
|
||||
|
||||
// Interval to update bot list stats every 24 hours
|
||||
LOCALMODE
|
||||
? log(LT.INFO, 'updateListStatistics not running')
|
||||
: setInterval(() => {
|
||||
log(LT.LOG, 'Updating all bot lists statistics');
|
||||
intervals.updateListStatistics(botId, cache.guilds.size + cache.dispatchedGuildIds.size);
|
||||
}, 86400000);
|
||||
|
||||
// Interval to update hourlyRates every hour
|
||||
setInterval(() => {
|
||||
log(LT.LOG, 'Updating all command hourlyRates');
|
||||
intervals.updateHourlyRates();
|
||||
}, 3600000);
|
||||
|
||||
// Interval to update heatmap.png every hour
|
||||
setInterval(() => {
|
||||
log(LT.LOG, 'Updating heatmap.png');
|
||||
intervals.updateHeatmapPng();
|
||||
}, 3600000);
|
||||
|
||||
// setTimeout added to make sure the startup message does not error out
|
||||
setTimeout(() => {
|
||||
LOCALMODE && editBotNickname(config.devServer, `LOCAL - ${config.name}`);
|
||||
LOCALMODE ? log(LT.INFO, 'updateListStatistics not running') : intervals.updateListStatistics(botId, cache.guilds.size + cache.dispatchedGuildIds.size);
|
||||
intervals.updateHourlyRates();
|
||||
intervals.updateHeatmapPng();
|
||||
editBotStatus({
|
||||
activities: [
|
||||
{
|
||||
name: 'Booting Complete',
|
||||
type: DiscordActivityTypes.Game,
|
||||
createdAt: new Date().getTime(),
|
||||
},
|
||||
],
|
||||
status: 'online',
|
||||
});
|
||||
sendMessage(config.logChannel, {
|
||||
embeds: [
|
||||
{
|
||||
title: `${config.name} is now Online`,
|
||||
color: successColor,
|
||||
fields: [
|
||||
{
|
||||
name: 'Version:',
|
||||
value: `${config.version}`,
|
||||
inline: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}).catch((e: Error) => utils.commonLoggers.messageSendError('mod.ts:88', 'Startup', e));
|
||||
}, 1000);
|
||||
},
|
||||
guildCreate: (guild: DiscordenoGuild) => {
|
||||
log(LT.LOG, `Handling joining guild ${JSON.stringify(guild)}`);
|
||||
sendMessage(config.logChannel, {
|
||||
embeds: [
|
||||
{
|
||||
title: 'New Guild Joined!',
|
||||
color: successColor,
|
||||
fields: [
|
||||
{
|
||||
name: 'Name:',
|
||||
value: `${guild.name}`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: 'Id:',
|
||||
value: `${guild.id}`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: 'Member Count:',
|
||||
value: `${guild.memberCount}`,
|
||||
inline: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}).catch((e: Error) => utils.commonLoggers.messageSendError('mod.ts:95', 'Join Guild', e));
|
||||
},
|
||||
guildDelete: (guild: DiscordenoGuild) => {
|
||||
log(LT.LOG, `Handling leaving guild ${JSON.stringify(guild)}`);
|
||||
sendMessage(config.logChannel, {
|
||||
embeds: [
|
||||
{
|
||||
title: 'Removed from Guild',
|
||||
color: warnColor,
|
||||
fields: [
|
||||
{
|
||||
name: 'Name:',
|
||||
value: `${guild.name}`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: 'Id:',
|
||||
value: `${guild.id}`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: 'Member Count:',
|
||||
value: `${guild.memberCount}`,
|
||||
inline: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}).catch((e: Error) => utils.commonLoggers.messageSendError('mod.ts:99', 'Leave Guild', e));
|
||||
dbClient
|
||||
.execute('DELETE FROM allowed_guilds WHERE guildid = ? AND banned = 0', [guild.id])
|
||||
.catch((e) => utils.commonLoggers.dbError('mod.ts:100', 'delete from', e));
|
||||
},
|
||||
debug: DEVMODE ? (dmsg) => log(LT.LOG, `Debug Message | ${JSON.stringify(dmsg)}`) : undefined,
|
||||
messageCreate: (message: DiscordenoMessage) => {
|
||||
// Ignore all other bots
|
||||
if (message.isBot) return;
|
||||
|
||||
// Ignore users who requested to be ignored
|
||||
if (ignoreList.includes(message.authorId) && (!message.content.startsWith(`${config.prefix}opt-in`) || message.guildId !== 0n)) return;
|
||||
|
||||
// Ignore all messages that are not commands
|
||||
if (message.content.indexOf(config.prefix) !== 0) {
|
||||
// Handle @bot messages
|
||||
if (message.mentionedUserIds[0] === botId && (message.content.trim().startsWith(`<@${botId}>`) || message.content.trim().startsWith(`<@!${botId}>`))) {
|
||||
commands.handleMentions(message);
|
||||
}
|
||||
|
||||
// return as we are done handling this command
|
||||
return;
|
||||
}
|
||||
|
||||
log(LT.LOG, `Handling ${config.prefix}command message: ${JSON.stringify(message)}`);
|
||||
|
||||
// Split into standard command + args format
|
||||
const args = message.content
|
||||
.slice(config.prefix.length)
|
||||
.trim()
|
||||
.split(/[ \n]+/g);
|
||||
const command = args.shift()?.toLowerCase();
|
||||
|
||||
// All commands below here
|
||||
|
||||
switch (command) {
|
||||
case 'opt-out':
|
||||
case 'ignore-me':
|
||||
// [[opt-out or [[ignore-me
|
||||
// Tells the bot to add you to the ignore list.
|
||||
commands.optOut(message);
|
||||
break;
|
||||
case 'opt-in':
|
||||
// [[opt-in
|
||||
// Tells the bot to remove you from the ignore list.
|
||||
commands.optIn(message);
|
||||
break;
|
||||
case 'ping':
|
||||
// [[ping
|
||||
// Its a ping test, what else do you want.
|
||||
commands.ping(message);
|
||||
break;
|
||||
case 'rip':
|
||||
case 'memory':
|
||||
// [[rip [[memory
|
||||
// Displays a short message I wanted to include
|
||||
commands.rip(message);
|
||||
break;
|
||||
case 'rollhelp':
|
||||
case 'rh':
|
||||
case 'hr':
|
||||
case '??':
|
||||
// [[rollhelp or [[rh or [[hr or [[??
|
||||
// Help command specifically for the roll command
|
||||
commands.rollHelp(message);
|
||||
break;
|
||||
case 'rolldecorators':
|
||||
case 'rd':
|
||||
case 'dr':
|
||||
case '???':
|
||||
// [[rollDecorators or [[rd or [[dr or [[???
|
||||
// Help command specifically for the roll command decorators
|
||||
commands.rollDecorators(message);
|
||||
break;
|
||||
case 'help':
|
||||
case 'h':
|
||||
case '?':
|
||||
// [[help or [[h or [[?
|
||||
// Help command, prints from help file
|
||||
commands.help(message);
|
||||
break;
|
||||
case 'info':
|
||||
case 'i':
|
||||
// [[info or [[i
|
||||
// Info command, prints short desc on bot and some links
|
||||
commands.info(message);
|
||||
break;
|
||||
case 'privacy':
|
||||
// [[privacy
|
||||
// Privacy command, prints short desc on bot's privacy policy
|
||||
commands.privacy(message);
|
||||
break;
|
||||
case 'version':
|
||||
case 'v':
|
||||
// [[version or [[v
|
||||
// Returns version of the bot
|
||||
commands.version(message);
|
||||
break;
|
||||
case 'report':
|
||||
case 'r':
|
||||
// [[report or [[r (command that failed)
|
||||
// Manually report a failed roll
|
||||
commands.report(message, args);
|
||||
break;
|
||||
case 'stats':
|
||||
case 's':
|
||||
// [[stats or [[s
|
||||
// Displays stats on the bot
|
||||
commands.stats(message);
|
||||
break;
|
||||
case 'api':
|
||||
// [[api arg
|
||||
// API sub commands
|
||||
commands.api(message, args);
|
||||
break;
|
||||
case 'audit':
|
||||
// [[audit arg
|
||||
// Audit sub commands
|
||||
commands.audit(message, args);
|
||||
break;
|
||||
case 'heatmap':
|
||||
case 'hm':
|
||||
// [[heatmap or [[hm
|
||||
// Audit sub commands
|
||||
commands.heatmap(message);
|
||||
break;
|
||||
default:
|
||||
// Non-standard commands
|
||||
if (command?.startsWith('xdy')) {
|
||||
// [[xdydz (aka someone copy pasted the template as a roll)
|
||||
// Help command specifically for the roll command
|
||||
commands.rollHelp(message);
|
||||
} else if (command && `${command}${args.join('')}`.indexOf(config.postfix) > -1) {
|
||||
// [[roll]]
|
||||
// Dice rolling commence!
|
||||
commands.roll(message, args, command);
|
||||
} else if (command) {
|
||||
// [[emoji or [[emojialias
|
||||
// Check if the unhandled command is an emoji request
|
||||
commands.emoji(message, command);
|
||||
}
|
||||
break;
|
||||
}
|
||||
},
|
||||
},
|
||||
eventHandlers,
|
||||
});
|
||||
|
||||
// Start up the command prompt for debug usage
|
||||
if (DEBUG) {
|
||||
utils.cmdPrompt(config.logChannel, config.name);
|
||||
}
|
||||
|
||||
// Start up the API for rolling from third party apps (like excel macros)
|
||||
if (config.api.enable) {
|
||||
api.start();
|
||||
|
|
39
src/api.ts
39
src/api.ts
|
@ -3,16 +3,14 @@
|
|||
*
|
||||
* December 21, 2020
|
||||
*/
|
||||
import { log, LogTypes as LT } from '@Log4Deno';
|
||||
|
||||
import config from '../config.ts';
|
||||
import {
|
||||
// Log4Deno deps
|
||||
log,
|
||||
LT,
|
||||
} from '../deps.ts';
|
||||
import dbClient from './db/client.ts';
|
||||
import endpoints from './endpoints/_index.ts';
|
||||
import stdResp from './endpoints/stdResponses.ts';
|
||||
import config from '~config';
|
||||
|
||||
import dbClient from 'db/client.ts';
|
||||
|
||||
import endpoints from 'endpoints/_index.ts';
|
||||
import stdResp from 'endpoints/stdResponses.ts';
|
||||
|
||||
// start() returns nothing
|
||||
// start initializes and runs the entire API for the bot
|
||||
|
@ -82,13 +80,14 @@ const start = () => {
|
|||
const query = new Map<string, string>();
|
||||
if (tempQ !== undefined) {
|
||||
tempQ.split('&').forEach((e: string) => {
|
||||
log(LT.LOG, `Parsing request query ${request} ${e}`);
|
||||
log(LT.LOG, `Parsing request query ${JSON.stringify(request)} ${e}`);
|
||||
const [option, params] = e.split('=');
|
||||
query.set(option.toLowerCase(), params);
|
||||
});
|
||||
}
|
||||
|
||||
if (path) {
|
||||
const lowerCasePath = path.toLowerCase().trim();
|
||||
if (authenticated) {
|
||||
// Update rate limit details
|
||||
if (updateRateLimitTime) {
|
||||
|
@ -99,7 +98,13 @@ const start = () => {
|
|||
// Handle the authenticated request
|
||||
switch (request.method) {
|
||||
case 'GET':
|
||||
switch (path.toLowerCase()) {
|
||||
switch (lowerCasePath) {
|
||||
case '/ping':
|
||||
case '/ping/':
|
||||
return endpoints.get.apiPing();
|
||||
case '/stats':
|
||||
case '/stats/':
|
||||
return endpoints.get.apiStats();
|
||||
case '/key':
|
||||
case '/key/':
|
||||
return endpoints.get.apiKeyAdmin(query, apiUserid);
|
||||
|
@ -115,7 +120,7 @@ const start = () => {
|
|||
}
|
||||
break;
|
||||
case 'POST':
|
||||
switch (path.toLowerCase()) {
|
||||
switch (lowerCasePath) {
|
||||
case '/channel/add':
|
||||
case '/channel/add/':
|
||||
return endpoints.post.apiChannelAdd(query, apiUserid);
|
||||
|
@ -125,7 +130,7 @@ const start = () => {
|
|||
}
|
||||
break;
|
||||
case 'PUT':
|
||||
switch (path.toLowerCase()) {
|
||||
switch (lowerCasePath) {
|
||||
case '/key/ban':
|
||||
case '/key/ban/':
|
||||
case '/key/unban':
|
||||
|
@ -151,7 +156,7 @@ const start = () => {
|
|||
}
|
||||
break;
|
||||
case 'DELETE':
|
||||
switch (path.toLowerCase()) {
|
||||
switch (lowerCasePath) {
|
||||
case '/key/delete':
|
||||
case '/key/delete/':
|
||||
return endpoints.delete.apiKeyDelete(query, apiUserid, apiUserEmail, apiUserDelCode);
|
||||
|
@ -165,15 +170,17 @@ const start = () => {
|
|||
return stdResp.MethodNotAllowed('Auth');
|
||||
}
|
||||
} else {
|
||||
// Handle the unathenticated request
|
||||
// Handle the unauthenticated request
|
||||
switch (request.method) {
|
||||
case 'GET':
|
||||
switch (path.toLowerCase()) {
|
||||
switch (lowerCasePath) {
|
||||
case '/key':
|
||||
case '/key/':
|
||||
return endpoints.get.apiKey(query);
|
||||
case '/heatmap.png':
|
||||
return endpoints.get.heatmapPng();
|
||||
case '/webview':
|
||||
return endpoints.get.generateWebView(query);
|
||||
default:
|
||||
// Alert API user that they messed up
|
||||
return stdResp.NotFound('NoAuth Get');
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
# artigen - The Artificer's Dice and Math Engine
|
||||
|
||||
artigen is the core engine powering The Artificer.
|
|
@ -0,0 +1,43 @@
|
|||
import { Embed, FileContent } from '@discordeno';
|
||||
|
||||
import { CountDetails, RollDistributionMap } from 'artigen/dice/dice.d.ts';
|
||||
|
||||
// ReturnData is the temporary internal type used before getting turned into SolvedRoll
|
||||
export interface ReturnData {
|
||||
origIdx?: number;
|
||||
rollTotal: number;
|
||||
rollPreFormat: string;
|
||||
rollPostFormat: string;
|
||||
rollDetails: string;
|
||||
containsCrit: boolean;
|
||||
containsFail: boolean;
|
||||
initConfig: string;
|
||||
isComplex: boolean;
|
||||
}
|
||||
|
||||
// SolvedRoll is the complete solved and formatted roll, or the error said roll created
|
||||
export interface SolvedRoll {
|
||||
error: boolean;
|
||||
errorMsg: string;
|
||||
errorCode: string;
|
||||
line1: string;
|
||||
line2: string;
|
||||
line3: string;
|
||||
footer: string;
|
||||
counts: CountDetails;
|
||||
rollDistributions: RollDistributionMap;
|
||||
}
|
||||
|
||||
interface basicArtigenEmbed {
|
||||
charCount: number;
|
||||
embed: Embed;
|
||||
}
|
||||
|
||||
export interface ArtigenEmbedNoAttachment extends basicArtigenEmbed {
|
||||
hasAttachment: false;
|
||||
}
|
||||
|
||||
export interface ArtigenEmbedWithAttachment extends basicArtigenEmbed {
|
||||
hasAttachment: true;
|
||||
attachment: FileContent;
|
||||
}
|
|
@ -0,0 +1,185 @@
|
|||
import { log, LogTypes as LT } from '@Log4Deno';
|
||||
|
||||
import { SolvedRoll } from 'artigen/artigen.d.ts';
|
||||
import { tokenizeCmd } from 'artigen/cmdTokenizer.ts';
|
||||
|
||||
import { Modifiers } from 'artigen/dice/getModifiers.ts';
|
||||
|
||||
import { getLoopCount, loopCountCheck } from 'artigen/managers/loopManager.ts';
|
||||
import { QueuedRoll } from 'artigen/managers/manager.d.ts';
|
||||
|
||||
import { reduceCountDetails } from 'artigen/utils/counter.ts';
|
||||
import { cmdSplitRegex, escapeCharacters, withYVarsDash } from 'artigen/utils/escape.ts';
|
||||
import { loggingEnabled, showLoopCountDebug } from 'artigen/utils/logFlag.ts';
|
||||
import { assertPrePostBalance } from 'artigen/utils/parenBalance.ts';
|
||||
import { reduceRollDistMaps } from 'artigen/utils/rollDist.ts';
|
||||
import { compareTotalRolls, compareTotalRollsReverse, sortYVars } from 'artigen/utils/sortFuncs.ts';
|
||||
import { translateError } from 'artigen/utils/translateError.ts';
|
||||
|
||||
// runCmd(rollRequest)
|
||||
// runCmd handles converting rollRequest into a computer readable format for processing, and finally executes the solving
|
||||
export const runCmd = (rollRequest: QueuedRoll): SolvedRoll => {
|
||||
const returnMsg: SolvedRoll = {
|
||||
error: false,
|
||||
errorCode: '',
|
||||
errorMsg: '',
|
||||
line1: '',
|
||||
line2: '',
|
||||
line3: '',
|
||||
footer: '',
|
||||
counts: {
|
||||
total: 0,
|
||||
successful: 0,
|
||||
failed: 0,
|
||||
rerolled: 0,
|
||||
dropped: 0,
|
||||
exploded: 0,
|
||||
success: 0,
|
||||
fail: 0,
|
||||
matches: new Map<string, number>(),
|
||||
},
|
||||
rollDistributions: new Map<string, number[]>(),
|
||||
};
|
||||
|
||||
// Whole processor lives in a try-catch to catch artigen's intentional error conditions
|
||||
try {
|
||||
loggingEnabled && log(LT.LOG, `rollRequest received! ${JSON.stringify(rollRequest)}`);
|
||||
// filter removes all null/empty strings since we don't care about them
|
||||
const sepCmds = rollRequest.rollCmd.split(cmdSplitRegex).filter((x) => x);
|
||||
loggingEnabled && log(LT.LOG, `Split cmd into parts ${JSON.stringify(sepCmds)}`);
|
||||
|
||||
// Verify prefix/postfix balance
|
||||
assertPrePostBalance(sepCmds);
|
||||
|
||||
// Send the split roll into the command tokenizer to get raw response data
|
||||
const [tempReturnData, tempCountDetails, tempRollDists] = tokenizeCmd(sepCmds, rollRequest.modifiers, true);
|
||||
loggingEnabled && log(LT.LOG, `Return data is back ${JSON.stringify(tempReturnData)} ${JSON.stringify(tempCountDetails)} ${JSON.stringify(tempRollDists)}`);
|
||||
|
||||
// Remove any floating spaces from originalCommand
|
||||
// Escape any | and ` chars in originalCommand to prevent spoilers and code blocks from acting up
|
||||
let rawCmd = escapeCharacters(rollRequest.originalCommand.trim(), '|').replace(/`/g, '');
|
||||
|
||||
// Remove yvariables from the rawCmd since this is intended for internal use only
|
||||
if (rawCmd.includes(Modifiers.YVars)) {
|
||||
rawCmd = rawCmd.replaceAll(new RegExp(`( ${Modifiers.YVars} (\\d+,)+\\d+)`, 'g'), '');
|
||||
}
|
||||
|
||||
let line1 = '';
|
||||
let line2 = '';
|
||||
let line3 = '';
|
||||
|
||||
// The ': ' is used by generateRollEmbed to split line 2 up
|
||||
const resultStr = tempReturnData.length > 1 ? 'Results: ' : 'Result: ';
|
||||
line2 = resultStr;
|
||||
|
||||
// If a theoretical roll is requested, mark the output as such, else use default formatting
|
||||
const theoreticalBools = [
|
||||
rollRequest.modifiers.maxRoll,
|
||||
rollRequest.modifiers.minRoll,
|
||||
rollRequest.modifiers.nominalRoll,
|
||||
rollRequest.modifiers.simulatedNominal > 0,
|
||||
];
|
||||
if (theoreticalBools.includes(true)) {
|
||||
const theoreticalTexts = ['Theoretical Maximum', 'Theoretical Minimum', 'Theoretical Nominal', 'Simulated Nominal'];
|
||||
const theoreticalText = theoreticalTexts[theoreticalBools.indexOf(true)];
|
||||
|
||||
line1 = ` requested the ${theoreticalText.toLowerCase()} of:\n\`${rawCmd}\``;
|
||||
line2 = `${theoreticalText} ${resultStr}`;
|
||||
} else if (rollRequest.modifiers.order === 'a') {
|
||||
line1 = ` requested the following rolls to be ordered from least to greatest:\n\`${rawCmd}\``;
|
||||
tempReturnData.sort(compareTotalRolls);
|
||||
} else if (rollRequest.modifiers.order === 'd') {
|
||||
line1 = ` requested the following rolls to be ordered from greatest to least:\n\`${rawCmd}\``;
|
||||
tempReturnData.sort(compareTotalRollsReverse);
|
||||
} else {
|
||||
line1 = ` rolled:\n\`${rawCmd}\``;
|
||||
}
|
||||
|
||||
if (rollRequest.modifiers.yVars.size) {
|
||||
line1 += `\n${withYVarsDash} With yVars: ${
|
||||
rollRequest.modifiers.yVars
|
||||
.entries()
|
||||
.toArray()
|
||||
.sort((a, b) => sortYVars(a[0], b[0]))
|
||||
.map((yVar) => `\`${yVar[0]}=${yVar[1]}\``)
|
||||
.join(' ')
|
||||
}`;
|
||||
}
|
||||
|
||||
// List number of iterations on simulated nominals
|
||||
if (rollRequest.modifiers.simulatedNominal) line2 += `Iterations performed per roll: \`${rollRequest.modifiers.simulatedNominal.toLocaleString()}\`\n`;
|
||||
|
||||
// Reduce counts to a single object
|
||||
if (rollRequest.modifiers.count) returnMsg.counts = reduceCountDetails(tempCountDetails);
|
||||
|
||||
// If a regular nominal and roll looks somewhat complex, alert user simulatedNominal exists
|
||||
if (rollRequest.modifiers.nominalRoll && tempReturnData.filter((data) => data.isComplex).length) {
|
||||
line2 +=
|
||||
"One or more of the rolls requested appear to be more complex than what the Nominal calculator is intended for. For a better approximation of this roll's nominal value, please rerun this roll with the `-sn` flag.\n";
|
||||
}
|
||||
|
||||
const line2Space = rollRequest.modifiers.noSpaces ? '' : ' ';
|
||||
// Fill out all of the details and results now
|
||||
tempReturnData.forEach((e, i) => {
|
||||
loopCountCheck('artigen.ts - tempReturnData');
|
||||
|
||||
loggingEnabled && log(LT.LOG, `Parsing roll ${rollRequest.rollCmd} | Making return text ${JSON.stringify(e)}`);
|
||||
let preFormat = '';
|
||||
let postFormat = '';
|
||||
|
||||
if (!rollRequest.modifiers.simulatedNominal) {
|
||||
// If the roll contained a crit success or fail, set the formatting around it
|
||||
if (e.containsCrit) {
|
||||
preFormat = `**${preFormat}`;
|
||||
postFormat = `${postFormat}**`;
|
||||
}
|
||||
if (e.containsFail) {
|
||||
preFormat = `__${preFormat}`;
|
||||
postFormat = `${postFormat}__`;
|
||||
}
|
||||
}
|
||||
|
||||
// Populate line2 (the results) and line3 (the details) with their data
|
||||
if (rollRequest.modifiers.order === '') {
|
||||
line2 += `${e.rollPreFormat ? escapeCharacters(e.rollPreFormat, '|*_~`') : line2Space}${preFormat}${
|
||||
rollRequest.modifiers.commaTotals ? e.rollTotal.toLocaleString() : e.rollTotal
|
||||
}${postFormat}${e.rollPostFormat ? escapeCharacters(e.rollPostFormat, '|*_~`') : ''}`;
|
||||
} else {
|
||||
// If order is on, turn rolls into csv without formatting
|
||||
line2 += `${preFormat}${rollRequest.modifiers.commaTotals ? e.rollTotal.toLocaleString() : e.rollTotal}${postFormat}, `;
|
||||
}
|
||||
|
||||
const varNum = `\`x${i}\`: `;
|
||||
const rollDetails = rollRequest.modifiers.noDetails || rollRequest.modifiers.simulatedNominal > 0 ? ' = ' : ` = ${e.rollDetails} = `;
|
||||
line3 += `${rollRequest.modifiers.numberVariables && i + 1 !== tempReturnData.length ? varNum : ''}\`${
|
||||
e.initConfig.replaceAll(
|
||||
' ',
|
||||
'',
|
||||
)
|
||||
}\`${rollDetails}${preFormat}${rollRequest.modifiers.commaTotals ? e.rollTotal.toLocaleString() : e.rollTotal}${postFormat}\n`;
|
||||
});
|
||||
|
||||
// If order is on, remove trailing ", "
|
||||
if (rollRequest.modifiers.order !== '') {
|
||||
line2 = line2.substring(0, line2.length - 2);
|
||||
}
|
||||
|
||||
// Fill in the return block
|
||||
returnMsg.line1 = line1;
|
||||
returnMsg.line2 = line2;
|
||||
returnMsg.line3 = line3;
|
||||
|
||||
// Reduce rollDist maps into a single map
|
||||
if (rollRequest.modifiers.rollDist) returnMsg.rollDistributions = reduceRollDistMaps(tempRollDists);
|
||||
} catch (e) {
|
||||
// Fill in the return block
|
||||
const solverError = e as Error;
|
||||
loggingEnabled && log(LT.ERROR, `Error hit: ${solverError.message} | ${rollRequest.rollCmd}`);
|
||||
returnMsg.error = true;
|
||||
[returnMsg.errorCode, returnMsg.errorMsg] = translateError(solverError);
|
||||
}
|
||||
|
||||
if (showLoopCountDebug) returnMsg.footer = `Loop Count: ${getLoopCount()}`;
|
||||
|
||||
return returnMsg;
|
||||
};
|
|
@ -0,0 +1,235 @@
|
|||
import { log, LogTypes as LT } from '@Log4Deno';
|
||||
|
||||
import config from '~config';
|
||||
|
||||
import { ReturnData } from 'artigen/artigen.d.ts';
|
||||
|
||||
import { CountDetails, RollDistributionMap, RollModifiers } from 'artigen/dice/dice.d.ts';
|
||||
import { handleGroup } from 'artigen/dice/groupHandler.ts';
|
||||
|
||||
import { loopCountCheck } from 'artigen/managers/loopManager.ts';
|
||||
|
||||
import { tokenizeMath } from 'artigen/math/mathTokenizer.ts';
|
||||
|
||||
import { reduceCountDetails } from 'artigen/utils/counter.ts';
|
||||
import { closeInternal, closeInternalGrp, internalGrpWrapRegex, internalWrapRegex, mathSplitRegex, openInternal, openInternalGrp } from 'artigen/utils/escape.ts';
|
||||
import { loggingEnabled } from 'artigen/utils/logFlag.ts';
|
||||
import { assertGroupBalance, getMatchingGroupIdx, getMatchingInternalGrpIdx, getMatchingInternalIdx, getMatchingPostfixIdx } from 'artigen/utils/parenBalance.ts';
|
||||
import { basicReducer } from 'artigen/utils/reducers.ts';
|
||||
|
||||
// tokenizeCmd expects a string[] of items that are either config.prefix/config.postfix or some text that contains math and/or dice rolls
|
||||
export const tokenizeCmd = (
|
||||
cmd: string[],
|
||||
modifiers: RollModifiers,
|
||||
topLevel: boolean,
|
||||
previousResults: number[] = [],
|
||||
): [ReturnData[], CountDetails[], RollDistributionMap[]] => {
|
||||
loggingEnabled && log(LT.LOG, `Tokenizing command ${JSON.stringify(cmd)}`);
|
||||
|
||||
const returnData: ReturnData[] = [];
|
||||
const countDetails: CountDetails[] = [];
|
||||
const rollDists: RollDistributionMap[] = [];
|
||||
|
||||
// Wrapped commands still exist, unwrap them
|
||||
while (cmd.includes(config.prefix)) {
|
||||
loopCountCheck('cmdTokenizer.ts - while cmd includes prefix');
|
||||
|
||||
const openIdx = cmd.indexOf(config.prefix);
|
||||
const closeIdx = getMatchingPostfixIdx(cmd, openIdx);
|
||||
|
||||
const currentCmd = cmd.slice(openIdx + 1, closeIdx);
|
||||
|
||||
const simulatedLoopCount = modifiers.simulatedNominal || 1;
|
||||
|
||||
loggingEnabled &&
|
||||
log(
|
||||
LT.LOG,
|
||||
`Setting previous results: topLevel:${topLevel} ${topLevel ? returnData.map((rd) => rd.rollTotal) : previousResults} simulatedLoopCount:${simulatedLoopCount}`,
|
||||
);
|
||||
|
||||
const simulatedData: ReturnData[] = [];
|
||||
for (let i = 0; i < simulatedLoopCount; i++) {
|
||||
loopCountCheck(`cmdTokenizer.ts - simulate nominal loop #${i}`);
|
||||
|
||||
loggingEnabled && log(LT.LOG, `In simLoop:${i} "${currentCmd}" of ${JSON.stringify(cmd)}`);
|
||||
|
||||
// Handle any nested commands
|
||||
const [tempData, tempCounts, tempDists] = tokenizeCmd(currentCmd, modifiers, false, topLevel ? returnData.map((rd) => rd.rollTotal) : previousResults);
|
||||
const data = tempData[0];
|
||||
loggingEnabled && log(LT.LOG, `Data back from tokenizeCmd, "${currentCmd}" of "${JSON.stringify(cmd)}" ${JSON.stringify(data)}`);
|
||||
|
||||
// Only run this on first loop
|
||||
if (topLevel && i === 0) {
|
||||
// Handle saving any formatting between dice
|
||||
if (openIdx !== 0) {
|
||||
data.rollPreFormat = cmd.slice(0, openIdx).join('');
|
||||
}
|
||||
|
||||
// Chop off all formatting between cmds along with the processed cmd
|
||||
cmd.splice(0, closeIdx + 1);
|
||||
}
|
||||
// Store results
|
||||
modifiers.simulatedNominal ? simulatedData.push(data) : returnData.push(data);
|
||||
countDetails.push(...tempCounts);
|
||||
rollDists.push(...tempDists);
|
||||
|
||||
// Handle ConfirmCrit if its on
|
||||
if (topLevel && modifiers.confirmCrit && reduceCountDetails(tempCounts).successful) {
|
||||
loggingEnabled && log(LT.LOG, `ConfirmCrit on ${JSON.stringify(currentCmd)}`);
|
||||
let done = false;
|
||||
while (!done) {
|
||||
loopCountCheck('cmdTokenizer.ts - confirming crit');
|
||||
|
||||
// Keep running the same roll again until its not successful
|
||||
const [ccTempData, ccTempCounts, ccTempDists] = tokenizeCmd(
|
||||
currentCmd,
|
||||
modifiers,
|
||||
false,
|
||||
topLevel ? returnData.map((rd) => rd.rollTotal) : previousResults,
|
||||
);
|
||||
const ccData = ccTempData[0];
|
||||
ccData.rollPreFormat = '\nAuto-Confirming Crit: ';
|
||||
|
||||
loggingEnabled &&
|
||||
log(LT.LOG, `ConfirmCrit on ${JSON.stringify(currentCmd)} | Rolled again ${JSON.stringify(ccData)} ${JSON.stringify(ccTempCounts)}`);
|
||||
|
||||
// Store CC results
|
||||
returnData.push(ccData);
|
||||
countDetails.push(...ccTempCounts);
|
||||
rollDists.push(...ccTempDists);
|
||||
|
||||
done = reduceCountDetails(ccTempCounts).successful === 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Turn the simulated return data into a single usable payload
|
||||
if (modifiers.simulatedNominal) {
|
||||
loggingEnabled && log(LT.LOG, `SN on, condensing array into single item ${JSON.stringify(simulatedData)}`);
|
||||
returnData.push({
|
||||
rollTotal: simulatedData.map((data) => data.rollTotal).reduce(basicReducer, 0) / simulatedData.length,
|
||||
rollPreFormat: simulatedData[0].rollPreFormat,
|
||||
rollPostFormat: simulatedData[0].rollPostFormat,
|
||||
rollDetails: simulatedData[0].rollDetails,
|
||||
containsCrit: simulatedData.some((data) => data.containsCrit),
|
||||
containsFail: simulatedData.some((data) => data.containsFail),
|
||||
initConfig: simulatedData[0].initConfig,
|
||||
isComplex: simulatedData[0].isComplex,
|
||||
});
|
||||
loggingEnabled && log(LT.LOG, `SN on, returnData updated ${JSON.stringify(returnData)}`);
|
||||
}
|
||||
|
||||
// Finally, if we are handling a nested [[cmd]], fill in the rollTotal correctly
|
||||
if (!topLevel) {
|
||||
cmd.splice(openIdx, closeIdx - openIdx + 1, `${openInternal}${Math.round(returnData[returnData.length - 1].rollTotal)}${closeInternal}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (topLevel) {
|
||||
if (cmd.length) {
|
||||
loggingEnabled && log(LT.LOG, `Adding leftover formatting to last returnData ${JSON.stringify(cmd)}`);
|
||||
returnData[returnData.length - 1].rollPostFormat = cmd.join('');
|
||||
}
|
||||
return [returnData, countDetails, rollDists];
|
||||
} else {
|
||||
// Check for any groups and handle them
|
||||
const groupParts = cmd
|
||||
.join('')
|
||||
.split(/([{}])/g)
|
||||
.filter((x) => x);
|
||||
const groupResults: ReturnData[] = [];
|
||||
if (groupParts.includes('{')) {
|
||||
assertGroupBalance(groupParts);
|
||||
}
|
||||
while (groupParts.includes('{')) {
|
||||
loggingEnabled && log(LT.LOG, `Handling Groups | Current cmd: ${JSON.stringify(groupParts)}`);
|
||||
|
||||
const openIdx = groupParts.indexOf('{');
|
||||
const closeIdx = getMatchingGroupIdx(groupParts, openIdx);
|
||||
|
||||
const currentGrp = groupParts.slice(openIdx + 1, closeIdx);
|
||||
|
||||
// Try to find and "eat" any modifiers from the next groupPart
|
||||
let thisGrpMods = '';
|
||||
const possibleMods = groupParts[closeIdx + 1]?.trim() ?? '';
|
||||
if (possibleMods.match(/^[dk<>=f].*/g)) {
|
||||
const items = groupParts[closeIdx + 1].split(mathSplitRegex).filter((x) => x);
|
||||
thisGrpMods = items.shift() ?? '';
|
||||
groupParts[closeIdx + 1] = items.join('');
|
||||
}
|
||||
|
||||
const [tempData, tempCounts, tempDists] = handleGroup(currentGrp, thisGrpMods, modifiers, previousResults);
|
||||
const data = tempData[0];
|
||||
log(LT.LOG, `Solved Group is back ${JSON.stringify(data)} | ${JSON.stringify(returnData)} ${JSON.stringify(tempCounts)} ${JSON.stringify(tempDists)}`);
|
||||
|
||||
countDetails.push(...tempCounts);
|
||||
rollDists.push(...tempDists);
|
||||
|
||||
// Merge result back into groupParts
|
||||
groupParts.splice(openIdx, closeIdx - openIdx + 1, `${openInternalGrp}${groupResults.length}${closeInternalGrp}`);
|
||||
groupResults.push(data);
|
||||
}
|
||||
|
||||
const cmdForMath = groupParts.join('');
|
||||
loggingEnabled && log(LT.LOG, `Tokenizing math ${cmdForMath}`);
|
||||
|
||||
// Solve the math and rolls for this cmd
|
||||
const [tempData, tempCounts, tempDists] = tokenizeMath(cmdForMath, modifiers, previousResults, groupResults);
|
||||
const data = tempData[0];
|
||||
loggingEnabled &&
|
||||
log(
|
||||
LT.LOG,
|
||||
`Solved math is back ${JSON.stringify(data)} | ${JSON.stringify(returnData)} ${JSON.stringify(groupResults)} ${
|
||||
JSON.stringify(
|
||||
tempCounts,
|
||||
)
|
||||
} ${JSON.stringify(tempDists)}`,
|
||||
);
|
||||
|
||||
// Merge counts
|
||||
countDetails.push(...tempCounts);
|
||||
rollDists.push(...tempDists);
|
||||
|
||||
// Handle merging group data into initConfig first since a group could "smuggle" a returnData in it
|
||||
const tempInitConf = data.initConfig.split(internalGrpWrapRegex).filter((x) => x);
|
||||
loggingEnabled && log(LT.LOG, `Split solved math into tempInitConf ${JSON.stringify(tempInitConf)}`);
|
||||
while (tempInitConf.includes(openInternalGrp)) {
|
||||
loopCountCheck('cmdTokenizer.ts - handling internal group result merging');
|
||||
|
||||
const openIdx = tempInitConf.indexOf(openInternalGrp);
|
||||
const closeIdx = getMatchingInternalGrpIdx(tempInitConf, openIdx);
|
||||
|
||||
// Take first groupResult out of array
|
||||
const dataToMerge = groupResults.shift();
|
||||
|
||||
// Replace the found pair with the nested tempInitConfig and result
|
||||
tempInitConf.splice(openIdx, closeIdx - openIdx + 1, `${dataToMerge?.initConfig}`);
|
||||
loggingEnabled && log(LT.LOG, `Current tempInitConf state ${JSON.stringify(tempInitConf)}`);
|
||||
}
|
||||
|
||||
// Handle merging returnData into tempData
|
||||
const initConf = tempInitConf
|
||||
.join('')
|
||||
.split(internalWrapRegex)
|
||||
.filter((x) => x);
|
||||
loggingEnabled && log(LT.LOG, `Split tempInitConfig into initConf ${JSON.stringify(initConf)}`);
|
||||
while (initConf.includes(openInternal)) {
|
||||
loopCountCheck('cmdTokenizer.ts - handling internal nested roll result merging');
|
||||
|
||||
const openIdx = initConf.indexOf(openInternal);
|
||||
const closeIdx = getMatchingInternalIdx(initConf, openIdx);
|
||||
|
||||
// Take first returnData out of array
|
||||
const dataToMerge = returnData.shift();
|
||||
|
||||
// Replace the found pair with the nested initConfig and result
|
||||
initConf.splice(openIdx, closeIdx - openIdx + 1, `${config.prefix}${dataToMerge?.initConfig}=${dataToMerge?.rollTotal}${config.postfix}`);
|
||||
loggingEnabled && log(LT.LOG, `Current initConf state ${JSON.stringify(initConf)}`);
|
||||
}
|
||||
|
||||
// Join all parts/remainders
|
||||
data.initConfig = initConf.join('');
|
||||
loggingEnabled && log(LT.LOG, `ReturnData merged into solved math ${JSON.stringify(data)} | ${JSON.stringify(countDetails)}`);
|
||||
return [[data], countDetails, rollDists];
|
||||
}
|
||||
};
|
|
@ -0,0 +1,170 @@
|
|||
import { SolvedStep } from 'artigen/math/math.d.ts';
|
||||
|
||||
// Available Roll Types
|
||||
type RollType = '' | 'custom' | 'roll20' | 'fate' | 'cwod' | 'ova';
|
||||
|
||||
// RollSet is used to preserve all information about a calculated roll
|
||||
export interface RollSet {
|
||||
type: RollType;
|
||||
rollGrpIdx?: number;
|
||||
origIdx: number;
|
||||
roll: number;
|
||||
size: number;
|
||||
dropped: boolean;
|
||||
rerolled: boolean;
|
||||
exploding: boolean;
|
||||
critHit: boolean;
|
||||
critFail: boolean;
|
||||
isComplex: boolean;
|
||||
matchLabel: string;
|
||||
success: boolean;
|
||||
fail: boolean;
|
||||
}
|
||||
|
||||
// CountDetails is the object holding the count data for creating the Count Embed
|
||||
export interface CountDetails {
|
||||
total: number;
|
||||
successful: number;
|
||||
failed: number;
|
||||
rerolled: number;
|
||||
dropped: number;
|
||||
exploded: number;
|
||||
success: number;
|
||||
fail: number;
|
||||
matches: Map<string, number>;
|
||||
}
|
||||
|
||||
// RollDistribution is used for storing the raw roll distribution
|
||||
// use rollDistKey to generate the key
|
||||
export type RollDistributionMap = Map<string, number[]>;
|
||||
|
||||
export type CustomDiceShapes = Map<string, number[]>;
|
||||
|
||||
// RollFormat is the return structure for the rollFormatter
|
||||
export interface FormattedRoll {
|
||||
solvedStep: SolvedStep;
|
||||
countDetails: CountDetails;
|
||||
rollDistributions: RollDistributionMap;
|
||||
}
|
||||
|
||||
// RollModifiers is the structure to keep track of the decorators applied to a roll command
|
||||
export interface RollModifiers {
|
||||
noDetails: boolean;
|
||||
superNoDetails: boolean;
|
||||
hideRaw: boolean;
|
||||
spoiler: string;
|
||||
maxRoll: boolean;
|
||||
minRoll: boolean;
|
||||
nominalRoll: boolean;
|
||||
simulatedNominal: number;
|
||||
gmRoll: boolean;
|
||||
gms: string[];
|
||||
order: string;
|
||||
count: boolean;
|
||||
commaTotals: boolean;
|
||||
confirmCrit: boolean;
|
||||
rollDist: boolean;
|
||||
numberVariables: boolean;
|
||||
customDiceShapes: CustomDiceShapes;
|
||||
noSpaces: boolean;
|
||||
yVars: Map<string, number>;
|
||||
apiWarn: string;
|
||||
valid: boolean;
|
||||
error: Error;
|
||||
}
|
||||
|
||||
// Basic conf interfaces
|
||||
interface CountConf {
|
||||
on: boolean;
|
||||
count: number;
|
||||
}
|
||||
interface RangeConf {
|
||||
on: boolean;
|
||||
range: number[];
|
||||
}
|
||||
interface GroupRangeConf extends RangeConf {
|
||||
// minValue carries the minimum number for the specified option to trigger
|
||||
// ex: if set to 4, 4 and greater will trigger the option
|
||||
minValue: number | null;
|
||||
// maxValue carries the minimum number for the specified option to trigger
|
||||
// ex: if set to 4, 4 and less will trigger the option
|
||||
maxValue: number | null;
|
||||
}
|
||||
|
||||
// Sort interface
|
||||
interface SortDisabled {
|
||||
on: false;
|
||||
direction: '';
|
||||
}
|
||||
interface SortEnabled {
|
||||
on: true;
|
||||
direction: 'a' | 'd';
|
||||
}
|
||||
|
||||
// D% configuration
|
||||
export interface DPercentConf {
|
||||
on: boolean;
|
||||
sizeAdjustment: number;
|
||||
critVal: number;
|
||||
}
|
||||
|
||||
interface BaseConf {
|
||||
drop: CountConf;
|
||||
keep: CountConf;
|
||||
dropHigh: CountConf;
|
||||
keepLow: CountConf;
|
||||
}
|
||||
// GroupConf carries the machine readable group configuration the user specified
|
||||
export interface GroupConf extends BaseConf {
|
||||
success: GroupRangeConf;
|
||||
fail: GroupRangeConf;
|
||||
}
|
||||
|
||||
// RollConf carries the machine readable roll configuration the user specified
|
||||
export interface RollConf extends BaseConf {
|
||||
type: RollType;
|
||||
customType: string | null;
|
||||
dieCount: number;
|
||||
dieSize: number;
|
||||
dPercent: DPercentConf;
|
||||
reroll: {
|
||||
on: boolean;
|
||||
once: boolean;
|
||||
nums: number[];
|
||||
};
|
||||
critScore: RangeConf;
|
||||
critFail: RangeConf;
|
||||
exploding: {
|
||||
on: boolean;
|
||||
once: boolean;
|
||||
compounding: boolean;
|
||||
penetrating: boolean;
|
||||
nums: number[];
|
||||
};
|
||||
match: {
|
||||
on: boolean;
|
||||
minCount: number;
|
||||
returnTotal: boolean;
|
||||
};
|
||||
sort: SortDisabled | SortEnabled;
|
||||
success: RangeConf;
|
||||
fail: RangeConf;
|
||||
}
|
||||
|
||||
export interface SumOverride {
|
||||
on: boolean;
|
||||
value: number;
|
||||
}
|
||||
|
||||
export interface ExecutedRoll {
|
||||
rollSet: RollSet[];
|
||||
countSuccessOverride: boolean;
|
||||
countFailOverride: boolean;
|
||||
sumOverride: SumOverride;
|
||||
}
|
||||
|
||||
export interface GroupResultFlags {
|
||||
dropped: boolean;
|
||||
success: boolean;
|
||||
failed: boolean;
|
||||
}
|
|
@ -0,0 +1,355 @@
|
|||
import { log, LogTypes as LT } from '@Log4Deno';
|
||||
|
||||
import { ExecutedRoll, RollModifiers, RollSet, SumOverride } from 'artigen/dice/dice.d.ts';
|
||||
import { generateRoll } from 'artigen/dice/randomRoll.ts';
|
||||
import { getRollConf } from 'artigen/dice/getRollConf.ts';
|
||||
|
||||
import { loggingEnabled } from 'artigen/utils/logFlag.ts';
|
||||
import { compareOrigIdx, compareRolls, compareRollsReverse } from 'artigen/utils/sortFuncs.ts';
|
||||
|
||||
import { flagRoll } from 'artigen/utils/diceFlagger.ts';
|
||||
import { getLoopCount, loopCountCheck } from 'artigen/managers/loopManager.ts';
|
||||
import { generateRollVals } from 'artigen/utils/rollValCounter.ts';
|
||||
|
||||
// roll(rollStr, modifiers) returns RollSet
|
||||
// roll parses and executes the rollStr
|
||||
export const executeRoll = (rollStr: string, modifiers: RollModifiers): ExecutedRoll => {
|
||||
/* Roll Capabilities
|
||||
* Deciphers and rolls a single dice roll set
|
||||
*
|
||||
* Check the README.md of this project for details on the roll options. I gave up trying to keep three places updated at once.
|
||||
*/
|
||||
|
||||
// Make entire roll lowercase for ease of parsing
|
||||
rollStr = rollStr.toLowerCase();
|
||||
|
||||
// Turn the rollStr into a machine readable rollConf
|
||||
const rollConf = getRollConf(rollStr, modifiers.customDiceShapes);
|
||||
|
||||
// Roll the roll
|
||||
const rollSet: RollSet[] = [];
|
||||
/* Roll will contain objects of the following format:
|
||||
* {
|
||||
* origIdx: 0,
|
||||
* roll: 0,
|
||||
* dropped: false,
|
||||
* rerolled: false,
|
||||
* exploding: false,
|
||||
* critHit: false,
|
||||
* critFail: false
|
||||
* }
|
||||
*
|
||||
* Each of these is defined as following:
|
||||
* {
|
||||
* origIdx: The original index of the roll
|
||||
* roll: The resulting roll on this die in the set
|
||||
* dropped: This die is to be dropped as it was one of the dy lowest dice
|
||||
* rerolled: This die has been rerolled as it matched rz, it is replaced by the very next die in the set
|
||||
* exploding: This die was rolled as the previous die exploded (was a crit hit)
|
||||
* critHit: This die matched csq[-u], max die value used if cs not used
|
||||
* critFail: This die rolled a nat 1, a critical failure
|
||||
* }
|
||||
*/
|
||||
|
||||
// Initialize a template rollSet to copy multiple times
|
||||
const getTemplateRoll = (): RollSet => ({
|
||||
type: rollConf.type,
|
||||
origIdx: 0,
|
||||
roll: 0,
|
||||
size: 0,
|
||||
dropped: false,
|
||||
rerolled: false,
|
||||
exploding: false,
|
||||
critHit: false,
|
||||
critFail: false,
|
||||
isComplex: rollConf.drop.on ||
|
||||
rollConf.keep.on ||
|
||||
rollConf.dropHigh.on ||
|
||||
rollConf.keepLow.on ||
|
||||
rollConf.critScore.on ||
|
||||
rollConf.critFail.on ||
|
||||
rollConf.exploding.on ||
|
||||
rollConf.success.on ||
|
||||
rollConf.fail.on,
|
||||
matchLabel: '',
|
||||
success: false,
|
||||
fail: false,
|
||||
});
|
||||
|
||||
// Initial rolling, not handling reroll or exploding here
|
||||
for (let i = 0; i < rollConf.dieCount; i++) {
|
||||
loggingEnabled && log(LT.LOG, `${getLoopCount()} Handling ${rollConf.type} ${rollStr} | Initial rolling ${i} of ${JSON.stringify(rollConf)}`);
|
||||
// If loopCount gets too high, stop trying to calculate infinity
|
||||
loopCountCheck('executeRoll.ts - handling initial rolling');
|
||||
|
||||
// Copy the template to fill out for this iteration
|
||||
const rolling = getTemplateRoll();
|
||||
// If maximizeRoll is on, set the roll to the dieSize, else if nominalRoll is on, set the roll to the average roll of dieSize, else generate a new random roll
|
||||
rolling.roll = generateRoll(rollConf, modifiers);
|
||||
rolling.size = rollConf.dieSize;
|
||||
// Set origIdx of roll
|
||||
rolling.origIdx = i;
|
||||
|
||||
flagRoll(rollConf, rolling, modifiers.customDiceShapes);
|
||||
|
||||
loggingEnabled && log(LT.LOG, `${getLoopCount()} Roll done ${JSON.stringify(rolling)}`);
|
||||
|
||||
// Push the newly created roll and loop again
|
||||
rollSet.push(rolling);
|
||||
}
|
||||
|
||||
// If needed, handle rerolling and exploding dice now
|
||||
if (rollConf.reroll.on || rollConf.exploding.on) {
|
||||
let minMaxOverride = 0;
|
||||
for (let i = 0; i < rollSet.length; i++) {
|
||||
loggingEnabled && log(LT.LOG, `${getLoopCount()} Handling ${rollConf.type} ${rollStr} | Handling rerolling and exploding ${JSON.stringify(rollSet[i])}`);
|
||||
// If loopCount gets too high, stop trying to calculate infinity
|
||||
loopCountCheck('executeRoll.ts - handling rerolling and exploding');
|
||||
|
||||
// This big boolean statement first checks if reroll is on, if the roll is within the reroll range, and finally if ro is ON, make sure we haven't already rerolled the roll
|
||||
if (rollConf.reroll.on && rollConf.reroll.nums.includes(rollSet[i].roll) && (!rollConf.reroll.once || !rollSet[i ? i - 1 : i].rerolled)) {
|
||||
// If we need to reroll this roll, flag its been replaced and...
|
||||
rollSet[i].rerolled = true;
|
||||
|
||||
// Copy the template to fill out for this iteration
|
||||
const newReroll = getTemplateRoll();
|
||||
newReroll.size = rollConf.dieSize;
|
||||
if (modifiers.maxRoll && !minMaxOverride) {
|
||||
// If maximizeRoll is on and we've entered the reroll code, dieSize is not allowed, determine the next best option and always return that
|
||||
mmMaxLoop: for (let m = rollConf.dieSize - 1; m > 0; m--) {
|
||||
loopCountCheck('executeRoll.ts - maximizeRoll');
|
||||
|
||||
if (!rollConf.reroll.nums.includes(m)) {
|
||||
minMaxOverride = m;
|
||||
break mmMaxLoop;
|
||||
}
|
||||
}
|
||||
} else if (modifiers.minRoll && !minMaxOverride) {
|
||||
// If minimizeRoll is on and we've entered the reroll code, 1 is not allowed, determine the next best option and always return that
|
||||
mmMinLoop: for (let m = rollConf.dPercent.on ? 1 : 2; m <= rollConf.dieSize; m++) {
|
||||
loopCountCheck('executeRoll.ts - minimizeRoll');
|
||||
|
||||
if (!rollConf.reroll.nums.includes(m)) {
|
||||
minMaxOverride = m;
|
||||
break mmMinLoop;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (modifiers.maxRoll || modifiers.minRoll) {
|
||||
newReroll.roll = minMaxOverride;
|
||||
} else {
|
||||
// If nominalRoll is on, set the roll to the average roll of dieSize, otherwise generate a new random roll
|
||||
newReroll.roll = generateRoll(rollConf, modifiers);
|
||||
}
|
||||
|
||||
flagRoll(rollConf, newReroll, modifiers.customDiceShapes);
|
||||
|
||||
loggingEnabled && log(LT.LOG, `${getLoopCount()} Roll done ${JSON.stringify(newReroll)}`);
|
||||
|
||||
// Slot this new roll in after the current iteration so it can be processed in the next loop
|
||||
rollSet.splice(i + 1, 0, newReroll);
|
||||
} else if (
|
||||
rollConf.exploding.on &&
|
||||
!rollSet[i].rerolled &&
|
||||
(rollConf.exploding.nums.length ? rollConf.exploding.nums.includes(rollSet[i].roll) : rollSet[i].critHit) &&
|
||||
(!rollConf.exploding.once || !rollSet[i].exploding)
|
||||
) {
|
||||
// If we have exploding.nums set, use those to determine the exploding range, and make sure if !o is on, make sure we don't repeatedly explode
|
||||
// If it exploded, we keep both, so no flags need to be set
|
||||
|
||||
// Copy the template to fill out for this iteration
|
||||
const newExplodingRoll = getTemplateRoll();
|
||||
// If maximizeRoll is on, set the roll to the dieSize, else if nominalRoll is on, set the roll to the average roll of dieSize, else generate a new random roll
|
||||
newExplodingRoll.roll = generateRoll(rollConf, modifiers);
|
||||
newExplodingRoll.size = rollConf.dieSize;
|
||||
// Always mark this roll as exploding
|
||||
newExplodingRoll.exploding = true;
|
||||
|
||||
flagRoll(rollConf, newExplodingRoll, modifiers.customDiceShapes);
|
||||
|
||||
loggingEnabled && log(LT.LOG, `${getLoopCount()} Roll done ${JSON.stringify(newExplodingRoll)}`);
|
||||
|
||||
// Slot this new roll in after the current iteration so it can be processed in the next loop
|
||||
rollSet.splice(i + 1, 0, newExplodingRoll);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If penetrating is on, do the decrements
|
||||
if (rollConf.exploding.penetrating) {
|
||||
for (const penRoll of rollSet) {
|
||||
loggingEnabled && log(LT.LOG, `${getLoopCount()} Handling ${rollConf.type} ${rollStr} | Handling penetrating explosions ${JSON.stringify(penRoll)}`);
|
||||
// If loopCount gets too high, stop trying to calculate infinity
|
||||
loopCountCheck('executeRoll.ts - penetrating explosion');
|
||||
|
||||
// If the die was from an explosion, decrement it by one
|
||||
if (penRoll.exploding) {
|
||||
penRoll.roll--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle compounding explosions
|
||||
if (rollConf.exploding.compounding) {
|
||||
for (let i = 0; i < rollSet.length; i++) {
|
||||
loggingEnabled && log(LT.LOG, `${getLoopCount()} Handling ${rollConf.type} ${rollStr} | Handling compounding explosions ${JSON.stringify(rollSet[i])}`);
|
||||
// If loopCount gets too high, stop trying to calculate infinity
|
||||
loopCountCheck('executeRoll.ts - compounding explosion');
|
||||
|
||||
// Compound the exploding rolls, including the exploding flag and
|
||||
if (rollSet[i].exploding) {
|
||||
rollSet[i - 1].roll = rollSet[i - 1].roll + rollSet[i].roll;
|
||||
rollSet[i - 1].exploding = true;
|
||||
rollSet[i - 1].critFail = rollSet[i - 1].critFail || rollSet[i].critFail;
|
||||
rollSet[i - 1].critHit = rollSet[i - 1].critHit || rollSet[i].critHit;
|
||||
rollSet.splice(i, 1);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we need to handle the drop/keep flags
|
||||
if (rollConf.drop.on || rollConf.keep.on || rollConf.dropHigh.on || rollConf.keepLow.on) {
|
||||
// Count how many rerolled dice there are if the reroll flag was on
|
||||
let rerollCount = 0;
|
||||
if (rollConf.reroll.on) {
|
||||
for (let j = 0; j < rollSet.length; j++) {
|
||||
loopCountCheck('executeRoll.ts - count rerolls');
|
||||
|
||||
loggingEnabled && log(LT.LOG, `${getLoopCount()} Handling ${rollConf.type} ${rollStr} | Setting originalIdx on ${JSON.stringify(rollSet[j])}`);
|
||||
rollSet[j].origIdx = j;
|
||||
|
||||
if (rollSet[j].rerolled) {
|
||||
rerollCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Order the rolls from least to greatest (by RollSet.roll)
|
||||
rollSet.sort(compareRolls);
|
||||
|
||||
// Determine how many valid rolls there are to drop from (may not be equal to dieCount due to exploding)
|
||||
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) {
|
||||
dropCount = rollConf.drop.count;
|
||||
if (dropCount > validRolls) {
|
||||
dropCount = validRolls;
|
||||
}
|
||||
} else if (rollConf.keep.on) {
|
||||
dropCount = validRolls - rollConf.keep.count;
|
||||
if (dropCount < 0) {
|
||||
dropCount = 0;
|
||||
}
|
||||
} // 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();
|
||||
dropCount = rollConf.dropHigh.count;
|
||||
if (dropCount > validRolls) {
|
||||
dropCount = validRolls;
|
||||
}
|
||||
} else if (rollConf.keepLow.on) {
|
||||
rollSet.reverse();
|
||||
dropCount = validRolls - rollConf.keepLow.count;
|
||||
if (dropCount < 0) {
|
||||
dropCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Now its time to drop all dice needed
|
||||
let i = 0;
|
||||
while (dropCount > 0 && i < rollSet.length) {
|
||||
loopCountCheck('executeRoll.ts - dropping/keeping');
|
||||
|
||||
loggingEnabled && log(LT.LOG, `${getLoopCount()} Handling ${rollConf.type} ${rollStr} | Dropping dice ${dropCount} ${JSON.stringify(rollSet[i])}`);
|
||||
// Skip all rolls that were rerolled
|
||||
if (!rollSet[i].rerolled) {
|
||||
rollSet[i].dropped = true;
|
||||
dropCount--;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
|
||||
// Finally, return the rollSet to its original order
|
||||
rollSet.sort(compareOrigIdx);
|
||||
}
|
||||
|
||||
// Handle OVA dropping/keeping
|
||||
if (rollConf.type === 'ova') {
|
||||
const rollVals: Array<number> = generateRollVals(rollConf, rollSet, rollStr, false);
|
||||
|
||||
// Find max value, using lastIndexOf to use the greatest die size max in case of duplicate maximums
|
||||
const maxRoll = rollVals.lastIndexOf(Math.max(...rollVals)) + 1;
|
||||
|
||||
// Drop all dice that are not a part of the max
|
||||
for (const ovaRoll of rollSet) {
|
||||
loopCountCheck('executeRoll.ts - OVA');
|
||||
|
||||
loggingEnabled &&
|
||||
log(LT.LOG, `${getLoopCount()} Handling ${rollConf.type} ${rollStr} | checking if this roll should be dropped ${ovaRoll.roll} | to keep: ${maxRoll}`);
|
||||
if (ovaRoll.roll !== maxRoll) {
|
||||
ovaRoll.dropped = true;
|
||||
ovaRoll.critFail = false;
|
||||
ovaRoll.critHit = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const sumOverride: SumOverride = {
|
||||
on: rollConf.match.returnTotal,
|
||||
value: 0,
|
||||
};
|
||||
if (rollConf.match.on) {
|
||||
const rollVals: Array<number> = generateRollVals(rollConf, rollSet, rollStr, true).map((count) => (count >= rollConf.match.minCount ? count : 0));
|
||||
const labels = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||
let labelIdx = 0;
|
||||
const rollLabels: Array<string> = rollVals.map((count) => {
|
||||
loopCountCheck('executeRoll.ts - matching');
|
||||
|
||||
if (labelIdx >= labels.length) {
|
||||
throw new Error(`TooManyLabels_${labels.length}`);
|
||||
}
|
||||
|
||||
if (count) {
|
||||
return labels[labelIdx++];
|
||||
}
|
||||
return '';
|
||||
});
|
||||
|
||||
loggingEnabled && log(LT.LOG, `${getLoopCount()} Handling ${rollConf.type} ${rollStr} | current match state: ${rollVals} | ${rollLabels}`);
|
||||
|
||||
// Apply labels
|
||||
for (const roll of rollSet) {
|
||||
loopCountCheck('executeRoll.ts - labeling matches');
|
||||
|
||||
loggingEnabled && log(LT.LOG, `${getLoopCount()} Handling ${rollConf.type} ${rollStr} | trying to add a label to ${JSON.stringify(roll)}`);
|
||||
if (rollLabels[roll.roll - 1]) {
|
||||
roll.matchLabel = rollLabels[roll.roll - 1];
|
||||
} else if (rollConf.match.returnTotal) {
|
||||
roll.dropped = true;
|
||||
}
|
||||
}
|
||||
|
||||
loggingEnabled && log(LT.LOG, `${getLoopCount()} Handling ${rollConf.type} ${rollStr} | labels added: ${JSON.stringify(rollSet)}`);
|
||||
|
||||
if (rollConf.match.returnTotal) {
|
||||
sumOverride.value = rollVals.filter((count) => count !== 0).length;
|
||||
}
|
||||
}
|
||||
|
||||
if (rollConf.sort.on) {
|
||||
rollSet.sort(rollConf.sort.direction === 'a' ? compareRolls : compareRollsReverse);
|
||||
}
|
||||
|
||||
return {
|
||||
rollSet,
|
||||
sumOverride,
|
||||
countSuccessOverride: rollConf.success.on,
|
||||
countFailOverride: rollConf.fail.on,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,102 @@
|
|||
import { log, LogTypes as LT } from '@Log4Deno';
|
||||
|
||||
import { ExecutedRoll, FormattedRoll, RollModifiers } from 'artigen/dice/dice.d.ts';
|
||||
|
||||
import { loopCountCheck } from 'artigen/managers/loopManager.ts';
|
||||
|
||||
import { rollCounter } from 'artigen/utils/counter.ts';
|
||||
import { loggingEnabled } from 'artigen/utils/logFlag.ts';
|
||||
import { createRollDistMap } from 'artigen/utils/rollDist.ts';
|
||||
|
||||
// generateFormattedRoll(executedRoll, modifiers) returns one SolvedStep
|
||||
// generateFormattedRoll handles creating and formatting the completed rolls into the SolvedStep format
|
||||
export const formatRoll = (executedRoll: ExecutedRoll, modifiers: RollModifiers): FormattedRoll => {
|
||||
let tempTotal = 0;
|
||||
let tempDetails = '[';
|
||||
let tempCrit = false;
|
||||
let tempFail = false;
|
||||
let tempComplex = false;
|
||||
|
||||
// Loop thru all parts of the roll to document everything that was done to create the total roll
|
||||
loggingEnabled && log(LT.LOG, `Formatting roll ${JSON.stringify(executedRoll)}`);
|
||||
executedRoll.rollSet.forEach((e) => {
|
||||
loopCountCheck('generateFormattedRoll.ts - formatting executed roll');
|
||||
|
||||
loggingEnabled && log(LT.LOG, `At ${JSON.stringify(e)}`);
|
||||
let preFormat = '';
|
||||
let postFormat = '';
|
||||
|
||||
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;
|
||||
if (e.critHit) {
|
||||
tempCrit = true;
|
||||
}
|
||||
if (e.critFail) {
|
||||
tempFail = true;
|
||||
}
|
||||
if (e.isComplex) {
|
||||
tempComplex = true;
|
||||
}
|
||||
}
|
||||
// If the roll was a crit hit or fail, or dropped/rerolled, add the formatting needed
|
||||
if (e.critHit) {
|
||||
// Bold for crit success
|
||||
preFormat = `**${preFormat}`;
|
||||
postFormat = `${postFormat}**`;
|
||||
}
|
||||
if (e.critFail) {
|
||||
// Underline for crit fail
|
||||
preFormat = `__${preFormat}`;
|
||||
postFormat = `${postFormat}__`;
|
||||
}
|
||||
if (e.dropped || e.rerolled) {
|
||||
// Strikethrough for dropped/rerolled rolls
|
||||
preFormat = `~~${preFormat}`;
|
||||
postFormat = `${postFormat}~~`;
|
||||
}
|
||||
if (e.exploding) {
|
||||
// Add ! to indicate the roll came from an explosion
|
||||
postFormat = `!${postFormat}`;
|
||||
}
|
||||
|
||||
let rollLabel = '';
|
||||
if (e.matchLabel) {
|
||||
rollLabel = `${e.matchLabel}:`;
|
||||
}
|
||||
|
||||
// Finally add this to the roll's details
|
||||
tempDetails += `${preFormat}${rollLabel}${e.roll}${postFormat} + `;
|
||||
});
|
||||
// After the looping is done, remove the extra " + " from the details and cap it with the closing ]
|
||||
tempDetails = tempDetails.substring(0, tempDetails.length - 3);
|
||||
if (executedRoll.countSuccessOverride) {
|
||||
const successCnt = executedRoll.rollSet.filter((e) => !e.dropped && !e.rerolled && e.success).length;
|
||||
tempDetails += `, ${successCnt} Success${successCnt !== 1 ? 'es' : ''}`;
|
||||
|
||||
executedRoll.sumOverride.on = true;
|
||||
executedRoll.sumOverride.value += successCnt;
|
||||
}
|
||||
if (executedRoll.countFailOverride) {
|
||||
const failCnt = executedRoll.rollSet.filter((e) => !e.dropped && !e.rerolled && e.fail).length;
|
||||
tempDetails += `, ${failCnt} Fail${failCnt !== 1 ? 's' : ''}`;
|
||||
|
||||
executedRoll.sumOverride.on = true;
|
||||
if (executedRoll.rollSet[0]?.type !== 'cwod') {
|
||||
executedRoll.sumOverride.value -= failCnt;
|
||||
}
|
||||
}
|
||||
tempDetails += ']';
|
||||
|
||||
return {
|
||||
solvedStep: {
|
||||
total: executedRoll.sumOverride.on ? executedRoll.sumOverride.value : tempTotal,
|
||||
details: tempDetails,
|
||||
containsCrit: tempCrit,
|
||||
containsFail: tempFail,
|
||||
isComplex: tempComplex,
|
||||
},
|
||||
countDetails: modifiers.count || modifiers.confirmCrit ? rollCounter(executedRoll.rollSet) : rollCounter([]),
|
||||
rollDistributions: modifiers.rollDist ? createRollDistMap(executedRoll.rollSet) : new Map<string, number[]>(),
|
||||
};
|
||||
};
|
|
@ -0,0 +1,73 @@
|
|||
import { log, LogTypes as LT } from '@Log4Deno';
|
||||
|
||||
import { GroupConf } from 'artigen/dice/dice.d.ts';
|
||||
import { getRollConf } from 'artigen/dice/getRollConf.ts';
|
||||
import { GroupOptions } from 'artigen/dice/rollOptions.ts';
|
||||
|
||||
import { loopCountCheck } from 'artigen/managers/loopManager.ts';
|
||||
|
||||
import { loggingEnabled } from 'artigen/utils/logFlag.ts';
|
||||
|
||||
// Wrapper to abuse getRollConf, produces a GroupConf by making the groupStr into a rollStr by adding a 1d1 onto it
|
||||
export const getGroupConf = (groupStr: string, rawStr: string): GroupConf => {
|
||||
const numberMatches = rawStr.match(/\d+/g) ?? ['1'];
|
||||
|
||||
let biggest = parseInt(numberMatches.length ? numberMatches[0] : '1');
|
||||
for (const num of numberMatches) {
|
||||
loopCountCheck('getGroupConf.ts - finding biggest number for die size');
|
||||
|
||||
const curNum = parseInt(num);
|
||||
loggingEnabled && log(LT.LOG, `Finding biggest number to use as die size, ${curNum} ${biggest}`);
|
||||
if (curNum > biggest) {
|
||||
biggest = curNum;
|
||||
}
|
||||
}
|
||||
|
||||
loggingEnabled && log(LT.LOG, `Abusing getRollConf with "1d${biggest} ${groupStr}"`);
|
||||
const fakeRollConf = getRollConf(`1d${biggest}${groupStr}`);
|
||||
loggingEnabled && log(LT.LOG, `Abused rollConf back for ${groupStr}: ${JSON.stringify(fakeRollConf)}`);
|
||||
|
||||
// Apply > to minValue and < to maxValue for success and fail
|
||||
const groupSplit = groupStr.split(/(\d+)/g).filter((x) => x);
|
||||
loggingEnabled && log(LT.LOG, `Handling success/fail gt/lt ${JSON.stringify(groupSplit)}`);
|
||||
|
||||
let minSuccess: number | null = null;
|
||||
let maxSuccess: number | null = null;
|
||||
let minFail: number | null = null;
|
||||
let maxFail: number | null = null;
|
||||
|
||||
while (groupSplit.length) {
|
||||
loopCountCheck('getGroupConf.ts - parsing groupConf');
|
||||
|
||||
const option = groupSplit.shift() ?? '';
|
||||
const value = parseInt(groupSplit.shift() ?? '');
|
||||
|
||||
if (!isNaN(value)) {
|
||||
switch (option) {
|
||||
case GroupOptions.SuccessLt:
|
||||
maxSuccess = maxSuccess && value < maxSuccess ? maxSuccess : value;
|
||||
break;
|
||||
case GroupOptions.SuccessGtr:
|
||||
minSuccess = minSuccess && value > minSuccess ? minSuccess : value;
|
||||
break;
|
||||
case GroupOptions.FailLt:
|
||||
maxFail = maxFail && value < maxFail ? maxFail : value;
|
||||
break;
|
||||
case GroupOptions.FailGtr:
|
||||
minFail = minFail && value > minFail ? minFail : value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
loggingEnabled && log(LT.LOG, `Parsed GT/LT: minSuccess: ${minSuccess} maxSuccess: ${maxSuccess} minFail: ${minFail} maxFail: ${maxFail}`);
|
||||
|
||||
return {
|
||||
drop: fakeRollConf.drop,
|
||||
keep: fakeRollConf.keep,
|
||||
dropHigh: fakeRollConf.dropHigh,
|
||||
keepLow: fakeRollConf.keepLow,
|
||||
success: { ...fakeRollConf.success, minValue: minSuccess, maxValue: maxSuccess },
|
||||
fail: { ...fakeRollConf.fail, minValue: minFail, maxValue: maxFail },
|
||||
};
|
||||
};
|
|
@ -0,0 +1,257 @@
|
|||
import { log, LogTypes as LT } from '@Log4Deno';
|
||||
|
||||
import config from '~config';
|
||||
|
||||
import { RollModifiers } from 'artigen/dice/dice.d.ts';
|
||||
|
||||
export const reservedCharacters = ['d', '%', '^', '*', '(', ')', '{', '}', '/', '+', '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
|
||||
export const Modifiers = Object.freeze({
|
||||
Count: '-c',
|
||||
NoDetails: '-nd',
|
||||
SuperNoDetails: '-snd',
|
||||
HideRaw: '-hr',
|
||||
Spoiler: '-s',
|
||||
Max: '-max',
|
||||
MaxShorthand: '-m',
|
||||
Min: '-min',
|
||||
Nominal: '-n',
|
||||
SimulatedNominal: '-sn',
|
||||
GM: '-gm',
|
||||
Order: '-o',
|
||||
CommaTotals: '-ct',
|
||||
ConfirmCrit: '-cc',
|
||||
RollDistribution: '-rd',
|
||||
NumberVariables: '-nv',
|
||||
VariablesNumber: '-vn',
|
||||
CustomDiceShapes: '-cd',
|
||||
NoSpaces: '-ns',
|
||||
YVars: '-yvariables',
|
||||
});
|
||||
|
||||
// args will look like this: ['-sn', ' ', '10'] as spaces/newlines are split on their own
|
||||
export const getModifiers = (args: string[]): [RollModifiers, string[]] => {
|
||||
const modifiers: RollModifiers = {
|
||||
noDetails: false,
|
||||
superNoDetails: false,
|
||||
hideRaw: false,
|
||||
spoiler: '',
|
||||
maxRoll: false,
|
||||
minRoll: false,
|
||||
nominalRoll: false,
|
||||
simulatedNominal: 0,
|
||||
gmRoll: false,
|
||||
gms: [],
|
||||
order: '',
|
||||
count: false,
|
||||
commaTotals: false,
|
||||
confirmCrit: false,
|
||||
rollDist: false,
|
||||
numberVariables: false,
|
||||
customDiceShapes: new Map<string, number[]>(),
|
||||
noSpaces: false,
|
||||
yVars: new Map<string, number>(),
|
||||
apiWarn: '',
|
||||
valid: true,
|
||||
error: new Error(),
|
||||
};
|
||||
|
||||
// Check if any of the args are command flags and pull those out into the modifiers object
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
log(LT.LOG, `Checking ${args.join(' ')} for command modifiers ${i} | ${args[i]}`);
|
||||
let defaultCase = false;
|
||||
switch (args[i].toLowerCase()) {
|
||||
case Modifiers.Count:
|
||||
modifiers.count = true;
|
||||
break;
|
||||
case Modifiers.NoDetails:
|
||||
modifiers.noDetails = true;
|
||||
break;
|
||||
case Modifiers.SuperNoDetails:
|
||||
modifiers.superNoDetails = true;
|
||||
break;
|
||||
case Modifiers.HideRaw:
|
||||
modifiers.hideRaw = true;
|
||||
break;
|
||||
case Modifiers.Spoiler:
|
||||
modifiers.spoiler = '||';
|
||||
break;
|
||||
case Modifiers.Max:
|
||||
case Modifiers.MaxShorthand:
|
||||
modifiers.maxRoll = true;
|
||||
break;
|
||||
case Modifiers.Min:
|
||||
modifiers.minRoll = true;
|
||||
break;
|
||||
case Modifiers.Nominal:
|
||||
modifiers.nominalRoll = true;
|
||||
break;
|
||||
case Modifiers.SimulatedNominal:
|
||||
if (args[i + 2] && parseInt(args[i + 2]).toString() === args[i + 2]) {
|
||||
// Shift the ["-sn", " "] out so the next item is the amount
|
||||
args.splice(i, 2);
|
||||
|
||||
modifiers.simulatedNominal = parseInt(args[i]);
|
||||
} else {
|
||||
modifiers.simulatedNominal = config.limits.defaultSimulatedNominal;
|
||||
}
|
||||
break;
|
||||
case Modifiers.ConfirmCrit:
|
||||
modifiers.confirmCrit = true;
|
||||
break;
|
||||
case Modifiers.GM:
|
||||
modifiers.gmRoll = true;
|
||||
|
||||
// -gm is a little more complex, as we must get all of the GMs that need to be DMd
|
||||
log(LT.LOG, `Finding all GMs, checking args ${JSON.stringify(args)}`);
|
||||
while (i + 2 < args.length && args[i + 2].startsWith('<@')) {
|
||||
// Keep looping thru the rest of the args until one does not start with the discord mention code
|
||||
modifiers.gms.push(args[i + 2].replace(/!/g, ''));
|
||||
args.splice(i + 1, 2);
|
||||
}
|
||||
if (modifiers.gms.length < 1) {
|
||||
// If -gm is on and none were found, throw an error
|
||||
modifiers.error.name = 'NoGMsFound';
|
||||
modifiers.error.message = 'Must specify at least one GM by @mentioning them';
|
||||
modifiers.valid = false;
|
||||
return [modifiers, args];
|
||||
}
|
||||
log(LT.LOG, `Found all GMs, ${modifiers.gms}`);
|
||||
break;
|
||||
case Modifiers.Order:
|
||||
// Shift the -o out of the array so the next item is the direction
|
||||
args.splice(i, 2);
|
||||
|
||||
if (!args[i] || (args[i].toLowerCase()[0] !== 'd' && args[i].toLowerCase()[0] !== 'a')) {
|
||||
// If -o is on and asc or desc was not specified, error out
|
||||
modifiers.error.name = 'NoOrderFound';
|
||||
modifiers.error.message = 'Must specify `a` or `d` to order the rolls ascending or descending';
|
||||
modifiers.valid = false;
|
||||
return [modifiers, args];
|
||||
}
|
||||
|
||||
modifiers.order = args[i].toLowerCase()[0];
|
||||
break;
|
||||
case Modifiers.CommaTotals:
|
||||
modifiers.commaTotals = true;
|
||||
break;
|
||||
case Modifiers.RollDistribution:
|
||||
modifiers.rollDist = true;
|
||||
break;
|
||||
case Modifiers.NumberVariables:
|
||||
case Modifiers.VariablesNumber:
|
||||
modifiers.numberVariables = true;
|
||||
break;
|
||||
case Modifiers.CustomDiceShapes: {
|
||||
// Shift the -cd out of the array so the dice shapes are next
|
||||
args.splice(i, 2);
|
||||
|
||||
const cdSyntaxMessage =
|
||||
'Must specify at least one custom dice shape using the `name:[side1,side2,...,sideN]` syntax. If multiple custom dice shapes are needed, use a `;` to separate the list.';
|
||||
|
||||
const shapes = (args[i] ?? '').split(';').filter((x) => x);
|
||||
if (!shapes.length) {
|
||||
modifiers.error.name = 'NoShapesSpecified';
|
||||
modifiers.error.message = `No custom shaped dice found.\n\n${cdSyntaxMessage}`;
|
||||
modifiers.valid = false;
|
||||
return [modifiers, args];
|
||||
}
|
||||
|
||||
for (const shape of shapes) {
|
||||
const [name, rawSides] = shape.split(':').filter((x) => x);
|
||||
if (!name || !rawSides || !rawSides.includes('[') || !rawSides.includes(']')) {
|
||||
modifiers.error.name = 'InvalidShapeSpecified';
|
||||
modifiers.error.message = `One of the custom dice is not formatted correctly.\n\n${cdSyntaxMessage}`;
|
||||
modifiers.valid = false;
|
||||
return [modifiers, args];
|
||||
}
|
||||
|
||||
if (modifiers.customDiceShapes.has(name)) {
|
||||
modifiers.error.name = 'ShapeAlreadySpecified';
|
||||
modifiers.error.message = `Shape \`${name}\` is already specified, please give it a different name.\n\n${cdSyntaxMessage}`;
|
||||
modifiers.valid = false;
|
||||
return [modifiers, args];
|
||||
}
|
||||
|
||||
if (reservedCharacters.some((char) => name.includes(char))) {
|
||||
modifiers.error.name = 'InvalidCharacterInCDName';
|
||||
modifiers.error.message = `Custom dice names cannot include any of the following characters:\n${
|
||||
JSON.stringify(
|
||||
reservedCharacters,
|
||||
)
|
||||
}\n\n${cdSyntaxMessage}`;
|
||||
modifiers.valid = false;
|
||||
return [modifiers, args];
|
||||
}
|
||||
|
||||
const sides = rawSides
|
||||
.replaceAll('[', '')
|
||||
.replaceAll(']', '')
|
||||
.split(',')
|
||||
.filter((x) => x)
|
||||
.map((side) => parseFloat(side));
|
||||
if (!sides.length) {
|
||||
modifiers.error.name = 'NoCustomSidesSpecified';
|
||||
modifiers.error.message = `No sides found for \`${name}\`.\n\n${cdSyntaxMessage}`;
|
||||
modifiers.valid = false;
|
||||
return [modifiers, args];
|
||||
}
|
||||
|
||||
modifiers.customDiceShapes.set(name, sides);
|
||||
}
|
||||
|
||||
log(LT.LOG, `Generated Custom Dice: ${JSON.stringify(modifiers.customDiceShapes.entries().toArray())}`);
|
||||
break;
|
||||
}
|
||||
case Modifiers.NoSpaces:
|
||||
modifiers.noSpaces = true;
|
||||
break;
|
||||
case Modifiers.YVars: {
|
||||
// Shift the -yvariables out of the array so the next item is the first yVar
|
||||
args.splice(i, 2);
|
||||
const yVars = args[i].split(',');
|
||||
yVars.forEach((yVar, idx) => {
|
||||
modifiers.yVars.set(`y${idx}`, parseFloat(yVar));
|
||||
});
|
||||
break;
|
||||
}
|
||||
default:
|
||||
// Default case should not mess with the array
|
||||
defaultCase = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!defaultCase) {
|
||||
args.splice(i, 1);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
|
||||
// maxRoll, minRoll, nominalRoll, simulatedNominal cannot be on at same time, throw an error
|
||||
if ([modifiers.maxRoll, modifiers.minRoll, modifiers.nominalRoll, modifiers.simulatedNominal].filter((b) => b).length > 1) {
|
||||
modifiers.error.name = 'MaxAndNominal';
|
||||
modifiers.error.message = 'Can only use one of the following at a time:\n`maximize`, `minimize`, `nominal`, `simulatedNominal`';
|
||||
modifiers.valid = false;
|
||||
}
|
||||
|
||||
// simulatedNominal and confirmCrit cannot be used at same time, throw an error
|
||||
if ([modifiers.confirmCrit, modifiers.simulatedNominal].filter((b) => b).length > 1) {
|
||||
modifiers.error.name = 'SimNominalAndCC';
|
||||
modifiers.error.message = 'Cannot use the following at the same time:\n`confirmCrit`, `simulatedNominal`';
|
||||
modifiers.valid = false;
|
||||
}
|
||||
|
||||
// simulatedNominal cannot be greater than config.limits.simulatedNominal
|
||||
if (modifiers.simulatedNominal > config.limits.maxSimulatedNominal) {
|
||||
modifiers.error.name = 'SimNominalTooBig';
|
||||
modifiers.error.message = `Number of iterations for \`simulatedNominal\` cannot be greater than \`${config.limits.maxSimulatedNominal}\``;
|
||||
modifiers.valid = false;
|
||||
}
|
||||
|
||||
if (modifiers.simulatedNominal < 0) {
|
||||
modifiers.error.name = 'NegativeSimNominal';
|
||||
modifiers.error.message = 'Number of iterations for `simulatedNominal` must be at least 1';
|
||||
modifiers.valid = false;
|
||||
}
|
||||
|
||||
return [modifiers, args];
|
||||
};
|
|
@ -0,0 +1,548 @@
|
|||
import { log, LogTypes as LT } from '@Log4Deno';
|
||||
|
||||
import { CustomDiceShapes, RollConf } from 'artigen/dice/dice.d.ts';
|
||||
|
||||
import { DiceOptions, NumberlessDiceOptions } from 'artigen/dice/rollOptions.ts';
|
||||
|
||||
import { getLoopCount, loopCountCheck } from 'artigen/managers/loopManager.ts';
|
||||
|
||||
import { loggingEnabled } from 'artigen/utils/logFlag.ts';
|
||||
import { addToRange, gtrAddToRange, ltAddToRange } from 'artigen/utils/rangeAdder.ts';
|
||||
|
||||
const throwDoubleSepError = (sep: string): void => {
|
||||
throw new Error(`DoubleSeparator_${sep}`);
|
||||
};
|
||||
|
||||
// Converts a rollStr into a machine readable rollConf
|
||||
export const getRollConf = (rollStr: string, customTypes: CustomDiceShapes = new Map<string, number[]>()): RollConf => {
|
||||
// Split the roll on the die size (and the drop if its there)
|
||||
const dPts = rollStr.split('d');
|
||||
|
||||
// Initialize the configuration to store the parsed data
|
||||
const rollConf: RollConf = {
|
||||
type: '',
|
||||
customType: null,
|
||||
dieCount: 0,
|
||||
dieSize: 0,
|
||||
dPercent: {
|
||||
on: false,
|
||||
sizeAdjustment: 0,
|
||||
critVal: 0,
|
||||
},
|
||||
drop: {
|
||||
on: false,
|
||||
count: 0,
|
||||
},
|
||||
keep: {
|
||||
on: false,
|
||||
count: 0,
|
||||
},
|
||||
dropHigh: {
|
||||
on: false,
|
||||
count: 0,
|
||||
},
|
||||
keepLow: {
|
||||
on: false,
|
||||
count: 0,
|
||||
},
|
||||
reroll: {
|
||||
on: false,
|
||||
once: false,
|
||||
nums: [],
|
||||
},
|
||||
critScore: {
|
||||
on: false,
|
||||
range: [],
|
||||
},
|
||||
critFail: {
|
||||
on: false,
|
||||
range: [],
|
||||
},
|
||||
exploding: {
|
||||
on: false,
|
||||
once: false,
|
||||
compounding: false,
|
||||
penetrating: false,
|
||||
nums: [],
|
||||
},
|
||||
match: {
|
||||
on: false,
|
||||
minCount: 2,
|
||||
returnTotal: false,
|
||||
},
|
||||
sort: {
|
||||
on: false,
|
||||
direction: '',
|
||||
},
|
||||
success: {
|
||||
on: false,
|
||||
range: [],
|
||||
},
|
||||
fail: {
|
||||
on: false,
|
||||
range: [],
|
||||
},
|
||||
};
|
||||
|
||||
// If the dPts is not long enough, throw error
|
||||
if (dPts.length < 2) {
|
||||
throw new Error(`YouNeedAD_${rollStr}`);
|
||||
}
|
||||
|
||||
// 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 rawDC = dPts.shift() || '1';
|
||||
if (rawDC.includes('.')) {
|
||||
throw new Error('WholeDieCountSizeOnly');
|
||||
}
|
||||
const tempDC = rawDC.replace(/\D/g, '');
|
||||
const numberlessRawDC = rawDC.replace(/\d/g, '');
|
||||
if (!tempDC && !numberlessRawDC) {
|
||||
throw new Error(`CannotParseDieCount_${rawDC}`);
|
||||
}
|
||||
// Rejoin all remaining parts
|
||||
let remains = dPts.join('d');
|
||||
|
||||
loggingEnabled && log(LT.LOG, `Initial breaking of rollStr ${rawDC} ${tempDC} ${dPts} ${remains}`);
|
||||
|
||||
// Manual Parsing for custom roll types
|
||||
if (rawDC.endsWith('cwo')) {
|
||||
// CWOD dice parsing
|
||||
rollConf.type = 'cwod';
|
||||
|
||||
// Get CWOD parts, setting count and getting difficulty
|
||||
const cwodParts = rollStr.split('cwod');
|
||||
rollConf.dieCount = parseInt(cwodParts[0] || '1');
|
||||
rollConf.dieSize = 10;
|
||||
|
||||
// Use success to set the difficulty
|
||||
rollConf.success.on = true;
|
||||
rollConf.fail.on = true;
|
||||
addToRange('cwod', rollConf.fail.range, 1);
|
||||
const tempDifficulty = (cwodParts[1] ?? '').search(/\d/) === 0 ? cwodParts[1] : '';
|
||||
let afterDifficultyIdx = tempDifficulty.search(/[^\d]/);
|
||||
if (afterDifficultyIdx === -1) {
|
||||
afterDifficultyIdx = tempDifficulty.length;
|
||||
}
|
||||
const difficulty = parseInt(tempDifficulty.slice(0, afterDifficultyIdx) || '10');
|
||||
|
||||
for (let i = difficulty; i <= rollConf.dieSize; i++) {
|
||||
loopCountCheck('getRollConf.ts - setting cwod difficulty');
|
||||
|
||||
loggingEnabled && log(LT.LOG, `${getLoopCount()} Handling cwod ${rollStr} | Parsing difficulty ${i}`);
|
||||
rollConf.success.range.push(i);
|
||||
}
|
||||
|
||||
// Remove any garbage from the remains
|
||||
remains = remains.slice(afterDifficultyIdx);
|
||||
} else if (rawDC.endsWith('ova')) {
|
||||
// OVA dice parsing
|
||||
rollConf.type = 'ova';
|
||||
|
||||
// Get OVA parts, setting count and getting difficulty
|
||||
const ovaParts = rollStr.split('ovad');
|
||||
const tempOvaPart1 = (ovaParts[1] ?? '').search(/\d/) === 0 ? ovaParts[1] : '';
|
||||
if (tempOvaPart1.search(/\d+\.\d/) === 0) {
|
||||
throw new Error('WholeDieCountSizeOnly');
|
||||
}
|
||||
rollConf.dieCount = parseInt(ovaParts[0] || '1');
|
||||
|
||||
let afterOvaSizeIdx = tempOvaPart1.search(/[^\d]/);
|
||||
if (afterOvaSizeIdx === -1) {
|
||||
afterOvaSizeIdx = tempOvaPart1.length;
|
||||
}
|
||||
rollConf.dieSize = parseInt(tempOvaPart1.slice(0, afterOvaSizeIdx) || '6');
|
||||
|
||||
// Remove any garbage from the remains
|
||||
remains = remains.slice(afterOvaSizeIdx);
|
||||
} else if (remains.startsWith('f')) {
|
||||
// fate dice setup
|
||||
rollConf.type = 'fate';
|
||||
rollConf.dieCount = parseInt(tempDC);
|
||||
// dieSize set to 1 as 1 is max face value, a six sided die is used internally
|
||||
rollConf.dieSize = 1;
|
||||
|
||||
// remove F from the remains
|
||||
remains = remains.slice(1);
|
||||
} else if (customTypes.has(numberlessRawDC)) {
|
||||
// custom dice setup
|
||||
rollConf.type = 'custom';
|
||||
rollConf.customType = numberlessRawDC;
|
||||
rollConf.dieCount = isNaN(parseInt(tempDC ?? '1')) ? 1 : parseInt(tempDC ?? '1');
|
||||
rollConf.dieSize = Math.max(...(customTypes.get(numberlessRawDC) ?? []));
|
||||
} else {
|
||||
// roll20 dice setup
|
||||
rollConf.type = 'roll20';
|
||||
rollConf.dieCount = parseInt(tempDC);
|
||||
|
||||
// Finds the end of the die size/beginning of the additional options
|
||||
let afterDieIdx = dPts[0].search(/[^%\d]/);
|
||||
if (afterDieIdx === -1) {
|
||||
afterDieIdx = dPts[0].length;
|
||||
}
|
||||
|
||||
// Get the die size out of the remains and into the rollConf
|
||||
const rawDS = remains.slice(0, afterDieIdx);
|
||||
remains = remains.slice(afterDieIdx);
|
||||
|
||||
if (rawDS.startsWith('%')) {
|
||||
rollConf.dieSize = 10;
|
||||
rollConf.dPercent.on = true;
|
||||
const percentCount = rawDS.match(/%/g)?.length ?? 1;
|
||||
rollConf.dPercent.sizeAdjustment = Math.pow(10, percentCount - 1);
|
||||
rollConf.dPercent.critVal = Math.pow(10, percentCount) - rollConf.dPercent.sizeAdjustment;
|
||||
} else {
|
||||
rollConf.dieSize = parseInt(rawDS);
|
||||
}
|
||||
|
||||
if (remains.search(/\.\d/) === 0) {
|
||||
throw new Error('WholeDieCountSizeOnly');
|
||||
}
|
||||
}
|
||||
|
||||
loggingEnabled && log(LT.LOG, `${getLoopCount()} Handling ${rollConf.type} ${rollStr} | Parsed Die Count: ${rollConf.dieCount}`);
|
||||
loggingEnabled && log(LT.LOG, `${getLoopCount()} Handling ${rollConf.type} ${rollStr} | Parsed Die Size: ${rollConf.dieSize}`);
|
||||
loggingEnabled && log(LT.LOG, `${getLoopCount()} Handling ${rollConf.type} ${rollStr} | remains: ${remains}`);
|
||||
|
||||
if (!rollConf.dieCount || !rollConf.dieSize) {
|
||||
throw new Error(`YouNeedAD_${rollStr}`);
|
||||
}
|
||||
|
||||
// Finish parsing the roll
|
||||
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) {
|
||||
remains = `d${remains}`;
|
||||
}
|
||||
|
||||
// Loop until all remaining args are parsed
|
||||
while (remains.length > 0) {
|
||||
loopCountCheck('getRollConf.ts - parsing rollConf');
|
||||
|
||||
loggingEnabled && log(LT.LOG, `${getLoopCount()} Handling ${rollConf.type} ${rollStr} | Parsing remains ${remains}`);
|
||||
// Find the next number in the remains to be able to cut out the rule name
|
||||
let afterSepIdx = remains.search(/[-\d]/);
|
||||
if (afterSepIdx < 0) {
|
||||
afterSepIdx = remains.length;
|
||||
}
|
||||
|
||||
// Determine if afterSepIdx needs to be moved up (cases like mt! or !mt)
|
||||
const tempSep = remains.slice(0, afterSepIdx);
|
||||
loggingEnabled && log(LT.LOG, `tempSep: ${tempSep}`);
|
||||
|
||||
let noNumberAfter = false;
|
||||
if (!(Object.values(DiceOptions) as string[]).includes(tempSep)) {
|
||||
NumberlessDiceOptions.some((opt) => {
|
||||
loopCountCheck('getRollConf.ts - parsing numberlessDiceOptions');
|
||||
loggingEnabled && log(LT.LOG, `In NumberlessDiceOptions ${opt} ${tempSep.startsWith(opt) && tempSep !== opt}`);
|
||||
if (tempSep.startsWith(opt) && tempSep !== opt) {
|
||||
afterSepIdx = opt.length;
|
||||
noNumberAfter = true;
|
||||
return true;
|
||||
}
|
||||
return tempSep === opt;
|
||||
});
|
||||
}
|
||||
|
||||
// Save the rule name to tSep and remove it from remains
|
||||
const tSep = remains.slice(0, afterSepIdx);
|
||||
remains = remains.slice(afterSepIdx);
|
||||
loggingEnabled && log(LT.LOG, `tSep: ${tSep}, remains: ${remains}`);
|
||||
|
||||
// Find the next non-number in the remains to be able to cut out the count/num
|
||||
let afterNumIdx = noNumberAfter ? 0 : remains.search(/(?![-\d])/);
|
||||
if (afterNumIdx < 0) {
|
||||
afterNumIdx = remains.length;
|
||||
}
|
||||
// Save the count/num to tNum leaving it in remains for the time being
|
||||
const tNum = parseInt(remains.slice(0, afterNumIdx));
|
||||
|
||||
loggingEnabled && log(LT.LOG, `${getLoopCount()} tSep: ${tSep} ${afterSepIdx}, tNum: ${tNum} ${afterNumIdx}`);
|
||||
|
||||
// Switch on rule name
|
||||
switch (tSep) {
|
||||
case DiceOptions.Drop:
|
||||
case DiceOptions.DropLow:
|
||||
if (rollConf.drop.on) {
|
||||
// Ensure we do not override existing settings
|
||||
throwDoubleSepError(tSep);
|
||||
}
|
||||
// Configure Drop (Lowest)
|
||||
rollConf.drop.on = true;
|
||||
rollConf.drop.count = tNum;
|
||||
break;
|
||||
case DiceOptions.Keep:
|
||||
case DiceOptions.KeepHigh:
|
||||
if (rollConf.keep.on) {
|
||||
// Ensure we do not override existing settings
|
||||
throwDoubleSepError(tSep);
|
||||
}
|
||||
// Configure Keep (Highest)
|
||||
rollConf.keep.on = true;
|
||||
rollConf.keep.count = tNum;
|
||||
break;
|
||||
case DiceOptions.DropHigh:
|
||||
if (rollConf.dropHigh.on) {
|
||||
// Ensure we do not override existing settings
|
||||
throwDoubleSepError(tSep);
|
||||
}
|
||||
// Configure Drop (Highest)
|
||||
rollConf.dropHigh.on = true;
|
||||
rollConf.dropHigh.count = tNum;
|
||||
break;
|
||||
case DiceOptions.KeepLow:
|
||||
if (rollConf.keepLow.on) {
|
||||
// Ensure we do not override existing settings
|
||||
throwDoubleSepError(tSep);
|
||||
}
|
||||
// Configure Keep (Lowest)
|
||||
rollConf.keepLow.on = true;
|
||||
rollConf.keepLow.count = tNum;
|
||||
break;
|
||||
case DiceOptions.RerollOnce:
|
||||
case DiceOptions.RerollOnceEqu:
|
||||
rollConf.reroll.once = true;
|
||||
// falls through as ro/ro= functions the same as r/r= in this context
|
||||
case DiceOptions.Reroll:
|
||||
case DiceOptions.RerollEqu:
|
||||
// Configure Reroll (this can happen multiple times)
|
||||
rollConf.reroll.on = true;
|
||||
addToRange(tSep, rollConf.reroll.nums, tNum);
|
||||
break;
|
||||
case DiceOptions.RerollOnceGtr:
|
||||
rollConf.reroll.once = true;
|
||||
// falls through as ro> functions the same as r> in this context
|
||||
case DiceOptions.RerollGtr:
|
||||
// Configure reroll for all numbers greater than or equal to tNum (this could happen multiple times, but why)
|
||||
rollConf.reroll.on = true;
|
||||
gtrAddToRange(tSep, rollConf.reroll.nums, tNum, rollConf.dieSize);
|
||||
break;
|
||||
case DiceOptions.RerollOnceLt:
|
||||
rollConf.reroll.once = true;
|
||||
// falls through as ro< functions the same as r< in this context
|
||||
case DiceOptions.RerollLt:
|
||||
// Configure reroll for all numbers less than or equal to tNum (this could happen multiple times, but why)
|
||||
rollConf.reroll.on = true;
|
||||
ltAddToRange(tSep, rollConf.reroll.nums, tNum, rollConf.type);
|
||||
break;
|
||||
case DiceOptions.CritSuccess:
|
||||
case DiceOptions.CritSuccessEqu:
|
||||
// Configure CritScore for one number (this can happen multiple times)
|
||||
rollConf.critScore.on = true;
|
||||
addToRange(tSep, rollConf.critScore.range, tNum);
|
||||
break;
|
||||
case DiceOptions.CritSuccessGtr:
|
||||
// Configure CritScore for all numbers greater than or equal to tNum (this could happen multiple times, but why)
|
||||
rollConf.critScore.on = true;
|
||||
gtrAddToRange(tSep, rollConf.critScore.range, tNum, rollConf.dieSize);
|
||||
break;
|
||||
case DiceOptions.CritSuccessLt:
|
||||
// Configure CritScore for all numbers less than or equal to tNum (this could happen multiple times, but why)
|
||||
rollConf.critScore.on = true;
|
||||
ltAddToRange(tSep, rollConf.critScore.range, tNum, rollConf.type);
|
||||
break;
|
||||
case DiceOptions.CritFail:
|
||||
case DiceOptions.CritFailEqu:
|
||||
// Configure CritFail for one number (this can happen multiple times)
|
||||
rollConf.critFail.on = true;
|
||||
addToRange(tSep, rollConf.critFail.range, tNum);
|
||||
break;
|
||||
case DiceOptions.CritFailGtr:
|
||||
// Configure CritFail for all numbers greater than or equal to tNum (this could happen multiple times, but why)
|
||||
rollConf.critFail.on = true;
|
||||
gtrAddToRange(tSep, rollConf.critFail.range, tNum, rollConf.dieSize);
|
||||
break;
|
||||
case DiceOptions.CritFailLt:
|
||||
// Configure CritFail for all numbers less than or equal to tNum (this could happen multiple times, but why)
|
||||
rollConf.critFail.on = true;
|
||||
ltAddToRange(tSep, rollConf.critFail.range, tNum, rollConf.type);
|
||||
break;
|
||||
case DiceOptions.Exploding:
|
||||
case DiceOptions.ExplodeOnce:
|
||||
case DiceOptions.PenetratingExplosion:
|
||||
case DiceOptions.CompoundingExplosion:
|
||||
// Configure Exploding
|
||||
rollConf.exploding.on = true;
|
||||
if (afterNumIdx > 0) {
|
||||
// User gave a number to explode on, save it
|
||||
addToRange(tSep, rollConf.exploding.nums, tNum);
|
||||
}
|
||||
break;
|
||||
case DiceOptions.ExplodingEqu:
|
||||
case DiceOptions.ExplodeOnceEqu:
|
||||
case DiceOptions.PenetratingExplosionEqu:
|
||||
case DiceOptions.CompoundingExplosionEqu:
|
||||
// Configure Exploding (this can happen multiple times)
|
||||
rollConf.exploding.on = true;
|
||||
addToRange(tSep, rollConf.exploding.nums, tNum);
|
||||
break;
|
||||
case DiceOptions.ExplodingGtr:
|
||||
case DiceOptions.ExplodeOnceGtr:
|
||||
case DiceOptions.PenetratingExplosionGtr:
|
||||
case DiceOptions.CompoundingExplosionGtr:
|
||||
// Configure Exploding for all numbers greater than or equal to tNum (this could happen multiple times, but why)
|
||||
rollConf.exploding.on = true;
|
||||
gtrAddToRange(tSep, rollConf.exploding.nums, tNum, rollConf.dieSize);
|
||||
break;
|
||||
case DiceOptions.ExplodingLt:
|
||||
case DiceOptions.ExplodeOnceLt:
|
||||
case DiceOptions.PenetratingExplosionLt:
|
||||
case DiceOptions.CompoundingExplosionLt:
|
||||
// Configure Exploding for all numbers less than or equal to tNum (this could happen multiple times, but why)
|
||||
rollConf.exploding.on = true;
|
||||
ltAddToRange(tSep, rollConf.exploding.nums, tNum, rollConf.type);
|
||||
break;
|
||||
case DiceOptions.MatchingTotal:
|
||||
rollConf.match.returnTotal = true;
|
||||
// falls through as mt functions the same as m in this context
|
||||
case DiceOptions.Matching:
|
||||
if (rollConf.match.on) {
|
||||
// Ensure we do not override existing settings
|
||||
throwDoubleSepError(tSep);
|
||||
}
|
||||
rollConf.match.on = true;
|
||||
if (afterNumIdx > 0) {
|
||||
// User gave a number to work with, save it
|
||||
rollConf.match.minCount = tNum;
|
||||
}
|
||||
break;
|
||||
case DiceOptions.Sort:
|
||||
case DiceOptions.SortAsc:
|
||||
if (rollConf.sort.on) {
|
||||
// Ensure we do not override existing settings
|
||||
throwDoubleSepError(tSep);
|
||||
}
|
||||
rollConf.sort.on = true;
|
||||
rollConf.sort.direction = 'a';
|
||||
break;
|
||||
case DiceOptions.SortDesc:
|
||||
if (rollConf.sort.on) {
|
||||
// Ensure we do not override existing settings
|
||||
throwDoubleSepError(tSep);
|
||||
}
|
||||
rollConf.sort.on = true;
|
||||
rollConf.sort.direction = 'd';
|
||||
break;
|
||||
case DiceOptions.SuccessEqu:
|
||||
// Configure success (this can happen multiple times)
|
||||
rollConf.success.on = true;
|
||||
addToRange(tSep, rollConf.success.range, tNum);
|
||||
break;
|
||||
case DiceOptions.SuccessGtr:
|
||||
// Configure success for all numbers greater than or equal to tNum (this could happen multiple times, but why)
|
||||
rollConf.success.on = true;
|
||||
gtrAddToRange(tSep, rollConf.success.range, tNum, rollConf.dieSize);
|
||||
break;
|
||||
case DiceOptions.SuccessLt:
|
||||
// Configure success for all numbers less than or equal to tNum (this could happen multiple times, but why)
|
||||
rollConf.success.on = true;
|
||||
ltAddToRange(tSep, rollConf.success.range, tNum, rollConf.type);
|
||||
break;
|
||||
case DiceOptions.Fail:
|
||||
case DiceOptions.FailEqu:
|
||||
// Configure fail (this can happen multiple times)
|
||||
rollConf.fail.on = true;
|
||||
addToRange(tSep, rollConf.fail.range, tNum);
|
||||
break;
|
||||
case DiceOptions.FailGtr:
|
||||
// Configure fail for all numbers greater than or equal to tNum (this could happen multiple times, but why)
|
||||
rollConf.fail.on = true;
|
||||
gtrAddToRange(tSep, rollConf.fail.range, tNum, rollConf.dieSize);
|
||||
break;
|
||||
case DiceOptions.FailLt:
|
||||
// Configure fail for all numbers less than or equal to tNum (this could happen multiple times, but why)
|
||||
rollConf.fail.on = true;
|
||||
ltAddToRange(tSep, rollConf.fail.range, tNum, rollConf.type);
|
||||
break;
|
||||
default:
|
||||
// Throw error immediately if unknown op is encountered
|
||||
throw new Error(`UnknownOperation_${tSep}`);
|
||||
}
|
||||
|
||||
// Followup switch to avoid weird duplicated code
|
||||
switch (tSep) {
|
||||
case DiceOptions.ExplodeOnce:
|
||||
case DiceOptions.ExplodeOnceLt:
|
||||
case DiceOptions.ExplodeOnceGtr:
|
||||
case DiceOptions.ExplodeOnceEqu:
|
||||
rollConf.exploding.once = true;
|
||||
break;
|
||||
case DiceOptions.PenetratingExplosion:
|
||||
case DiceOptions.PenetratingExplosionLt:
|
||||
case DiceOptions.PenetratingExplosionGtr:
|
||||
case DiceOptions.PenetratingExplosionEqu:
|
||||
rollConf.exploding.penetrating = true;
|
||||
break;
|
||||
case DiceOptions.CompoundingExplosion:
|
||||
case DiceOptions.CompoundingExplosionLt:
|
||||
case DiceOptions.CompoundingExplosionGtr:
|
||||
case DiceOptions.CompoundingExplosionEqu:
|
||||
rollConf.exploding.compounding = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// Finally slice off everything else parsed this loop
|
||||
remains = remains.slice(afterNumIdx);
|
||||
}
|
||||
}
|
||||
|
||||
loggingEnabled && log(LT.LOG, `RollConf before cleanup: ${JSON.stringify(rollConf)}`);
|
||||
|
||||
// Verify the parse, throwing errors for every invalid config
|
||||
if (rollConf.dieCount < 0) {
|
||||
throw new Error('NoZerosAllowed_base');
|
||||
}
|
||||
if (rollConf.dieCount === 0 || rollConf.dieSize === 0) {
|
||||
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;
|
||||
[rollConf.drop.on, rollConf.keep.on, rollConf.dropHigh.on, rollConf.keepLow.on].forEach((e) => {
|
||||
loggingEnabled && log(LT.LOG, `Handling ${rollConf.type} ${rollStr} | Checking if drop/keep is on ${e}`);
|
||||
if (e) {
|
||||
dkdkCnt++;
|
||||
}
|
||||
});
|
||||
if (dkdkCnt > 1) {
|
||||
throw new Error('FormattingError_dk');
|
||||
}
|
||||
|
||||
if (rollConf.match.on && (rollConf.success.on || rollConf.fail.on)) {
|
||||
throw new Error('FormattingError_mtsf');
|
||||
}
|
||||
|
||||
if (rollConf.drop.on && rollConf.drop.count === 0) {
|
||||
throw new Error('NoZerosAllowed_drop');
|
||||
}
|
||||
if (rollConf.keep.on && rollConf.keep.count === 0) {
|
||||
throw new Error('NoZerosAllowed_keep');
|
||||
}
|
||||
if (rollConf.dropHigh.on && rollConf.dropHigh.count === 0) {
|
||||
throw new Error('NoZerosAllowed_dropHigh');
|
||||
}
|
||||
if (rollConf.keepLow.on && rollConf.keepLow.count === 0) {
|
||||
throw new Error('NoZerosAllowed_keepLow');
|
||||
}
|
||||
|
||||
// Filter rollConf num lists to only include valid numbers
|
||||
const validNumFilter = (curNum: number) => {
|
||||
if (rollConf.type === 'fate') {
|
||||
return [-1, 0, 1].includes(curNum);
|
||||
}
|
||||
return curNum <= rollConf.dieSize && curNum > (rollConf.dPercent.on ? -1 : 0);
|
||||
};
|
||||
rollConf.reroll.nums = rollConf.reroll.nums.filter(validNumFilter);
|
||||
rollConf.critScore.range = rollConf.critScore.range.filter(validNumFilter);
|
||||
rollConf.critFail.range = rollConf.critFail.range.filter(validNumFilter);
|
||||
rollConf.exploding.nums = rollConf.exploding.nums.filter(validNumFilter);
|
||||
rollConf.success.range = rollConf.success.range.filter(validNumFilter);
|
||||
rollConf.fail.range = rollConf.fail.range.filter(validNumFilter);
|
||||
|
||||
if (rollConf.reroll.on && rollConf.reroll.nums.length === (rollConf.type === 'fate' ? 3 : rollConf.dieSize)) {
|
||||
throw new Error('NoRerollOnAllSides');
|
||||
}
|
||||
|
||||
loggingEnabled && log(LT.LOG, `RollConf after cleanup: ${JSON.stringify(rollConf)}`);
|
||||
|
||||
return rollConf;
|
||||
};
|
|
@ -0,0 +1,284 @@
|
|||
import { log, LogTypes as LT } from '@Log4Deno';
|
||||
|
||||
import { ReturnData } from 'artigen/artigen.d.ts';
|
||||
|
||||
import { CountDetails, GroupConf, GroupResultFlags, RollDistributionMap, RollModifiers } from 'artigen/dice/dice.d.ts';
|
||||
|
||||
import { loopCountCheck } from 'artigen/managers/loopManager.ts';
|
||||
|
||||
import { tokenizeMath } from 'artigen/math/mathTokenizer.ts';
|
||||
|
||||
import { closeInternalGrp, internalGrpWrapRegex, mathSplitRegex, openInternalGrp } from 'artigen/utils/escape.ts';
|
||||
import { loggingEnabled } from 'artigen/utils/logFlag.ts';
|
||||
import { getMatchingGroupIdx, getMatchingInternalGrpIdx } from 'artigen/utils/parenBalance.ts';
|
||||
import { getGroupConf } from 'artigen/dice/getGroupConf.ts';
|
||||
import { compareOrigIdx, compareTotalRolls } from 'artigen/utils/sortFuncs.ts';
|
||||
import { applyFlags } from 'artigen/utils/groupResultFlagger.ts';
|
||||
|
||||
export const handleGroup = (
|
||||
groupParts: string[],
|
||||
groupModifiers: string,
|
||||
modifiers: RollModifiers,
|
||||
previousResults: number[],
|
||||
): [ReturnData[], CountDetails[], RollDistributionMap[]] => {
|
||||
let retData: ReturnData;
|
||||
const returnData: ReturnData[] = [];
|
||||
const countDetails: CountDetails[] = [];
|
||||
const rollDists: RollDistributionMap[] = [];
|
||||
const groupConf: GroupConf = getGroupConf(groupModifiers, groupParts.join(''));
|
||||
const prevGrpReturnData: ReturnData[] = [];
|
||||
|
||||
// Nested groups still exist, unwrap them
|
||||
while (groupParts.includes('{')) {
|
||||
loopCountCheck('groupHandler.ts - handling nested groups');
|
||||
|
||||
loggingEnabled && log(LT.LOG, `Handling Nested Groups | Current cmd: ${JSON.stringify(groupParts)}`);
|
||||
|
||||
const openIdx = groupParts.indexOf('{');
|
||||
const closeIdx = getMatchingGroupIdx(groupParts, openIdx);
|
||||
|
||||
const currentGrp = groupParts.slice(openIdx + 1, closeIdx);
|
||||
|
||||
// Try to find and "eat" any modifiers from the next groupPart
|
||||
let thisGrpMods = '';
|
||||
const possibleMods = groupParts[closeIdx + 1]?.trim() ?? '';
|
||||
if (possibleMods.match(/^[dk<>=f].*/g)) {
|
||||
const items = groupParts[closeIdx + 1].split(mathSplitRegex).filter((x) => x);
|
||||
thisGrpMods = items.shift() ?? '';
|
||||
groupParts[closeIdx + 1] = items.join('');
|
||||
}
|
||||
|
||||
const [tempData, tempCounts, tempDists] = handleGroup(currentGrp, thisGrpMods, modifiers, previousResults);
|
||||
const data = tempData[0];
|
||||
loggingEnabled && log(LT.LOG, `Solved Nested Group is back ${JSON.stringify(data)} | ${JSON.stringify(tempCounts)} ${JSON.stringify(tempDists)}`);
|
||||
|
||||
countDetails.push(...tempCounts);
|
||||
rollDists.push(...tempDists);
|
||||
|
||||
// Merge result back into groupParts
|
||||
groupParts.splice(openIdx, closeIdx - openIdx + 1, `${openInternalGrp}${prevGrpReturnData.length}${closeInternalGrp}`);
|
||||
prevGrpReturnData.push(data);
|
||||
}
|
||||
|
||||
// Handle the items in the groups
|
||||
const commaParts = groupParts
|
||||
.join('')
|
||||
.split(',')
|
||||
.filter((x) => x);
|
||||
|
||||
if (commaParts.length > 1) {
|
||||
loggingEnabled && log(LT.LOG, `In multi-mode ${JSON.stringify(commaParts)} ${groupModifiers} ${JSON.stringify(groupConf)}`);
|
||||
// Handle "normal operation" of group
|
||||
const groupResults: ReturnData[] = [];
|
||||
|
||||
for (const part of commaParts) {
|
||||
loopCountCheck('groupHandler.ts - solving commaParts');
|
||||
|
||||
loggingEnabled && log(LT.LOG, `Solving commaPart: ${part}`);
|
||||
const [tempData, tempCounts, tempDists] = tokenizeMath(part, modifiers, previousResults, prevGrpReturnData);
|
||||
const data = tempData[0];
|
||||
|
||||
loggingEnabled && log(LT.LOG, `Solved Math for Group is back ${JSON.stringify(data)} | ${JSON.stringify(tempCounts)} ${JSON.stringify(tempDists)}`);
|
||||
|
||||
countDetails.push(...tempCounts);
|
||||
rollDists.push(...tempDists);
|
||||
groupResults.push(data);
|
||||
}
|
||||
|
||||
if (groupModifiers.trim()) {
|
||||
// Handle the provided modifiers
|
||||
const getTemplateFlags = (): GroupResultFlags => ({ dropped: false, success: false, failed: false });
|
||||
|
||||
// Assign original indexes
|
||||
const resultFlags: GroupResultFlags[] = [];
|
||||
groupResults.forEach((rd, idx) => {
|
||||
rd.origIdx = idx;
|
||||
resultFlags.push(getTemplateFlags());
|
||||
});
|
||||
|
||||
// Handle drop/keep options
|
||||
if (groupConf.drop.on || groupConf.keep.on || groupConf.dropHigh.on || groupConf.keepLow.on) {
|
||||
groupResults.sort(compareTotalRolls);
|
||||
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 (groupConf.drop.on) {
|
||||
dropCount = groupConf.drop.count;
|
||||
if (dropCount > groupResults.length) {
|
||||
dropCount = groupResults.length;
|
||||
}
|
||||
} else if (groupConf.keep.on) {
|
||||
dropCount = groupResults.length - groupConf.keep.count;
|
||||
if (dropCount < 0) {
|
||||
dropCount = 0;
|
||||
}
|
||||
} // 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 (groupConf.dropHigh.on) {
|
||||
groupResults.reverse();
|
||||
dropCount = groupConf.dropHigh.count;
|
||||
if (dropCount > groupResults.length) {
|
||||
dropCount = groupResults.length;
|
||||
}
|
||||
} else if (groupConf.keepLow.on) {
|
||||
groupResults.reverse();
|
||||
dropCount = groupResults.length - groupConf.keepLow.count;
|
||||
if (dropCount < 0) {
|
||||
dropCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
let i = 0;
|
||||
while (dropCount > 0 && i < groupResults.length) {
|
||||
loopCountCheck('groupHandler.ts - handling group drop/keep');
|
||||
|
||||
loggingEnabled && log(LT.LOG, `Handling group dropping | Dropping ${dropCount}`);
|
||||
|
||||
resultFlags[groupResults[i].origIdx ?? -1].dropped = true;
|
||||
|
||||
dropCount--;
|
||||
i++;
|
||||
}
|
||||
|
||||
groupResults.sort(compareOrigIdx);
|
||||
}
|
||||
|
||||
let successCnt = 0;
|
||||
let failCnt = 0;
|
||||
if (groupConf.success.on || groupConf.fail.on) {
|
||||
groupResults.forEach((rd, idx) => {
|
||||
loopCountCheck('groupHandler.ts - handling group success/fail');
|
||||
|
||||
if (!resultFlags[idx].dropped) {
|
||||
if (
|
||||
groupConf.success.on &&
|
||||
(groupConf.success.range.includes(rd.rollTotal) ||
|
||||
(groupConf.success.minValue !== null && rd.rollTotal >= groupConf.success.minValue) ||
|
||||
(groupConf.success.maxValue !== null && rd.rollTotal <= groupConf.success.maxValue))
|
||||
) {
|
||||
successCnt++;
|
||||
resultFlags[idx].success = true;
|
||||
}
|
||||
if (
|
||||
groupConf.fail.on &&
|
||||
(groupConf.fail.range.includes(rd.rollTotal) ||
|
||||
(groupConf.fail.minValue !== null && rd.rollTotal >= groupConf.fail.minValue) ||
|
||||
(groupConf.fail.maxValue !== null && rd.rollTotal <= groupConf.fail.maxValue))
|
||||
) {
|
||||
failCnt++;
|
||||
resultFlags[idx].failed = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
loggingEnabled && log(LT.LOG, `Current Group Results: ${JSON.stringify(groupResults)}`);
|
||||
loggingEnabled && log(LT.LOG, `Applying group flags: ${JSON.stringify(resultFlags)}`);
|
||||
const data = groupResults.reduce(
|
||||
(prev, cur, idx) => ({
|
||||
rollTotal: resultFlags[idx].dropped ? prev.rollTotal : prev.rollTotal + cur.rollTotal,
|
||||
rollPreFormat: '',
|
||||
rollPostFormat: '',
|
||||
rollDetails: `${prev.rollDetails}${prev.rollDetails ? ', ' : ''}${applyFlags(cur.rollDetails, resultFlags[idx])}`,
|
||||
containsCrit: resultFlags[idx].dropped ? prev.containsCrit : prev.containsCrit || cur.containsCrit,
|
||||
containsFail: resultFlags[idx].dropped ? prev.containsFail : prev.containsFail || cur.containsFail,
|
||||
initConfig: `${prev.initConfig}${prev.initConfig ? ', ' : ''}${cur.initConfig}`,
|
||||
isComplex: prev.isComplex || cur.isComplex,
|
||||
}),
|
||||
{
|
||||
rollTotal: 0,
|
||||
rollPreFormat: '',
|
||||
rollPostFormat: '',
|
||||
rollDetails: '',
|
||||
containsCrit: false,
|
||||
containsFail: false,
|
||||
initConfig: '',
|
||||
isComplex: false,
|
||||
},
|
||||
);
|
||||
data.initConfig = `{${data.initConfig}}${groupModifiers.replaceAll(' ', '')}`;
|
||||
|
||||
if (groupConf.success.on || groupConf.fail.on) {
|
||||
data.rollTotal = 0;
|
||||
}
|
||||
if (groupConf.success.on) {
|
||||
data.rollTotal += successCnt;
|
||||
data.rollDetails += `, ${successCnt} Success${successCnt !== 1 ? 'es' : ''}`;
|
||||
}
|
||||
if (groupConf.fail.on) {
|
||||
data.rollTotal -= failCnt;
|
||||
data.rollDetails += `, ${failCnt} Fail${failCnt !== 1 ? 's' : ''}`;
|
||||
}
|
||||
|
||||
data.rollDetails = `{${data.rollDetails}}`;
|
||||
retData = data;
|
||||
} else {
|
||||
// Sum mode
|
||||
const data = groupResults.reduce(
|
||||
(prev, cur) => ({
|
||||
rollTotal: prev.rollTotal + cur.rollTotal,
|
||||
rollPreFormat: '',
|
||||
rollPostFormat: '',
|
||||
rollDetails: `${prev.rollDetails}${prev.rollDetails ? ' + ' : ''}${cur.rollDetails}`,
|
||||
containsCrit: prev.containsCrit || cur.containsCrit,
|
||||
containsFail: prev.containsFail || cur.containsFail,
|
||||
initConfig: `${prev.initConfig}${prev.initConfig ? ', ' : ''}${cur.initConfig}`,
|
||||
isComplex: prev.isComplex || cur.isComplex,
|
||||
}),
|
||||
{
|
||||
rollTotal: 0,
|
||||
rollPreFormat: '',
|
||||
rollPostFormat: '',
|
||||
rollDetails: '',
|
||||
containsCrit: false,
|
||||
containsFail: false,
|
||||
initConfig: '',
|
||||
isComplex: false,
|
||||
},
|
||||
);
|
||||
data.initConfig = `{${data.initConfig}}`;
|
||||
data.rollDetails = `{${data.rollDetails}}`;
|
||||
retData = data;
|
||||
}
|
||||
} else {
|
||||
loggingEnabled && log(LT.LOG, `In single-mode ${JSON.stringify(commaParts)} ${groupModifiers} ${JSON.stringify(groupConf)}`);
|
||||
const [tempData, tempCounts, tempDists] = tokenizeMath(
|
||||
commaParts[0],
|
||||
modifiers,
|
||||
previousResults,
|
||||
prevGrpReturnData,
|
||||
groupModifiers.trim() ? groupConf : null,
|
||||
);
|
||||
const data = tempData[0];
|
||||
|
||||
loggingEnabled && log(LT.LOG, `Solved Math for Group is back ${JSON.stringify(data)} | ${JSON.stringify(tempCounts)} ${JSON.stringify(tempDists)}`);
|
||||
|
||||
countDetails.push(...tempCounts);
|
||||
rollDists.push(...tempDists);
|
||||
data.initConfig = `{${data.initConfig}}${groupModifiers.trim() ? groupModifiers.replaceAll(' ', '') : ''}`;
|
||||
data.rollDetails = `{${data.rollDetails}}`;
|
||||
retData = data;
|
||||
}
|
||||
|
||||
// Handle merging back any nested groups to prevent an internalGrp marker from sneaking out
|
||||
const initConf = retData.initConfig.split(internalGrpWrapRegex).filter((x) => x);
|
||||
loggingEnabled && log(LT.LOG, `Split retData into initConf ${JSON.stringify(initConf)}`);
|
||||
while (initConf.includes(openInternalGrp)) {
|
||||
loopCountCheck('groupHandler.ts - handling merging nested groups up');
|
||||
|
||||
const openIdx = initConf.indexOf(openInternalGrp);
|
||||
const closeIdx = getMatchingInternalGrpIdx(initConf, openIdx);
|
||||
|
||||
// Take first groupResult out of array
|
||||
const dataToMerge = prevGrpReturnData.shift();
|
||||
|
||||
// Replace the found pair with the nested initConfig and result
|
||||
initConf.splice(openIdx, closeIdx - openIdx + 1, `${dataToMerge?.initConfig}`);
|
||||
loggingEnabled && log(LT.LOG, `Current initConf state ${JSON.stringify(initConf)}`);
|
||||
}
|
||||
|
||||
retData.initConfig = initConf.join('');
|
||||
returnData.push(retData);
|
||||
return [returnData, countDetails, rollDists];
|
||||
};
|
|
@ -0,0 +1,41 @@
|
|||
import { DPercentConf, RollConf, RollModifiers } from 'artigen/dice/dice.d.ts';
|
||||
|
||||
import { basicReducer } from 'artigen/utils/reducers.ts';
|
||||
|
||||
// genBasicRoll(size, modifiers, dPercent) returns number
|
||||
// genBasicRoll rolls a die of size size and returns the result
|
||||
const genBasicRoll = (size: number, modifiers: RollModifiers, dPercent: DPercentConf): number => {
|
||||
let result;
|
||||
if (modifiers.maxRoll) {
|
||||
result = size;
|
||||
} else if (modifiers.minRoll) {
|
||||
result = 1;
|
||||
} else {
|
||||
// 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
|
||||
result = modifiers.nominalRoll ? size / 2 + 0.5 : Math.floor(Math.random() * size + 1);
|
||||
}
|
||||
return dPercent.on ? (result - 1) * dPercent.sizeAdjustment : result;
|
||||
};
|
||||
|
||||
const getRollFromArray = (sides: number[], modifiers: RollModifiers): number => {
|
||||
if (modifiers.nominalRoll) {
|
||||
return sides.reduce(basicReducer, 0) / sides.length;
|
||||
} else if (modifiers.maxRoll) {
|
||||
return Math.max(...sides);
|
||||
} else if (modifiers.minRoll) {
|
||||
return Math.min(...sides);
|
||||
}
|
||||
|
||||
return sides[genBasicRoll(sides.length, modifiers, <DPercentConf> { on: false }) - 1];
|
||||
};
|
||||
|
||||
export const generateRoll = (rollConf: RollConf, modifiers: RollModifiers): number => {
|
||||
switch (rollConf.type) {
|
||||
case 'fate':
|
||||
return getRollFromArray([-1, -1, 0, 0, 1, 1], modifiers);
|
||||
case 'custom':
|
||||
return getRollFromArray(modifiers.customDiceShapes.get(rollConf.customType ?? '') ?? [], modifiers);
|
||||
default:
|
||||
return genBasicRoll(rollConf.dieSize, modifiers, rollConf.dPercent);
|
||||
}
|
||||
};
|
|
@ -0,0 +1,69 @@
|
|||
export const GroupOptions = Object.freeze({
|
||||
Drop: 'd',
|
||||
DropLow: 'dl',
|
||||
DropHigh: 'dh',
|
||||
Keep: 'k',
|
||||
KeepLow: 'kl',
|
||||
KeepHigh: 'kh',
|
||||
SuccessLt: '<',
|
||||
SuccessGtr: '>',
|
||||
SuccessEqu: '=',
|
||||
Fail: 'f',
|
||||
FailLt: 'f<',
|
||||
FailGtr: 'f>',
|
||||
FailEqu: 'f=',
|
||||
});
|
||||
|
||||
export const DiceOptions = Object.freeze({
|
||||
...GroupOptions,
|
||||
Reroll: 'r',
|
||||
RerollLt: 'r<',
|
||||
RerollGtr: 'r>',
|
||||
RerollEqu: 'r=',
|
||||
RerollOnce: 'ro',
|
||||
RerollOnceLt: 'ro<',
|
||||
RerollOnceGtr: 'ro>',
|
||||
RerollOnceEqu: 'ro=',
|
||||
CritSuccess: 'cs',
|
||||
CritSuccessLt: 'cs<',
|
||||
CritSuccessGtr: 'cs>',
|
||||
CritSuccessEqu: 'cs=',
|
||||
CritFail: 'cf',
|
||||
CritFailLt: 'cf<',
|
||||
CritFailGtr: 'cf>',
|
||||
CritFailEqu: 'cf=',
|
||||
Exploding: '!',
|
||||
ExplodingLt: '!<',
|
||||
ExplodingGtr: '!>',
|
||||
ExplodingEqu: '!=',
|
||||
ExplodeOnce: '!o',
|
||||
ExplodeOnceLt: '!o<',
|
||||
ExplodeOnceGtr: '!o>',
|
||||
ExplodeOnceEqu: '!o=',
|
||||
PenetratingExplosion: '!p',
|
||||
PenetratingExplosionLt: '!p<',
|
||||
PenetratingExplosionGtr: '!p>',
|
||||
PenetratingExplosionEqu: '!p=',
|
||||
CompoundingExplosion: '!!',
|
||||
CompoundingExplosionLt: '!!<',
|
||||
CompoundingExplosionGtr: '!!>',
|
||||
CompoundingExplosionEqu: '!!=',
|
||||
Matching: 'm',
|
||||
MatchingTotal: 'mt',
|
||||
Sort: 's',
|
||||
SortAsc: 'sa',
|
||||
SortDesc: 'sd',
|
||||
});
|
||||
|
||||
// Should be ordered such that 'mt' will be encountered before 'm'
|
||||
export const NumberlessDiceOptions = [
|
||||
DiceOptions.SortDesc,
|
||||
DiceOptions.SortAsc,
|
||||
DiceOptions.Sort,
|
||||
DiceOptions.MatchingTotal,
|
||||
DiceOptions.Matching,
|
||||
DiceOptions.CompoundingExplosion,
|
||||
DiceOptions.PenetratingExplosion,
|
||||
DiceOptions.ExplodeOnce,
|
||||
DiceOptions.Exploding,
|
||||
];
|
|
@ -0,0 +1,50 @@
|
|||
import { closeLog, initLog } from '@Log4Deno';
|
||||
|
||||
import { runCmd } from 'artigen/artigen.ts';
|
||||
import { SolvedRoll } from 'artigen/artigen.d.ts';
|
||||
|
||||
import { QueuedRoll } from 'artigen/managers/manager.d.ts';
|
||||
|
||||
import { loggingEnabled, loopLoggingEnabled } from 'artigen/utils/logFlag.ts';
|
||||
|
||||
if (loggingEnabled || loopLoggingEnabled) initLog('logs/worker', loggingEnabled || loopLoggingEnabled);
|
||||
|
||||
// Extend the BigInt prototype to support JSON.stringify
|
||||
interface BigIntX extends BigInt {
|
||||
// Convert to BigInt to string form in JSON.stringify
|
||||
toJSON: () => string;
|
||||
}
|
||||
(BigInt.prototype as BigIntX).toJSON = function () {
|
||||
return this.toString();
|
||||
};
|
||||
|
||||
// Alert rollQueue that this worker is ready
|
||||
self.postMessage('ready');
|
||||
|
||||
// Handle the roll
|
||||
self.onmessage = async (e: MessageEvent<QueuedRoll>) => {
|
||||
const payload = e.data;
|
||||
const returnMsg: SolvedRoll = runCmd(payload) || {
|
||||
error: true,
|
||||
errorCode: 'EmptyMessage',
|
||||
errorMsg: 'Error: Empty message',
|
||||
line1: '',
|
||||
line2: '',
|
||||
line3: '',
|
||||
footer: '',
|
||||
counts: {
|
||||
total: 0,
|
||||
successful: 0,
|
||||
failed: 0,
|
||||
rerolled: 0,
|
||||
dropped: 0,
|
||||
exploded: 0,
|
||||
success: 0,
|
||||
fail: 0,
|
||||
matches: new Map<string, number>(),
|
||||
},
|
||||
};
|
||||
self.postMessage(returnMsg);
|
||||
if (loggingEnabled || loopLoggingEnabled) await closeLog();
|
||||
self.close();
|
||||
};
|
|
@ -0,0 +1,5 @@
|
|||
let currentWorkers = 0;
|
||||
|
||||
export const addWorker = () => currentWorkers++;
|
||||
export const removeWorker = () => currentWorkers--;
|
||||
export const getWorkerCnt = () => currentWorkers;
|
|
@ -0,0 +1,322 @@
|
|||
import { botId, ButtonStyles, DiscordenoMessage, Embed, FileContent, MessageComponentTypes, sendDirectMessage, sendMessage } from '@discordeno';
|
||||
import { log, LogTypes as LT } from '@Log4Deno';
|
||||
|
||||
import config from '~config';
|
||||
|
||||
import { SolvedRoll } from 'artigen/artigen.d.ts';
|
||||
|
||||
import { RollModifiers } from 'artigen/dice/dice.d.ts';
|
||||
|
||||
import { removeWorker } from 'artigen/managers/countManager.ts';
|
||||
import { QueuedRoll } from 'artigen/managers/manager.d.ts';
|
||||
import { ApiResolveMap, TestResolveMap } from 'artigen/managers/resolveManager.ts';
|
||||
|
||||
import { generateCountDetailsEmbed, generateDMFailed, generateRollDistsEmbed, generateRollEmbed, toggleWebView } from 'artigen/utils/embeds.ts';
|
||||
import { loggingEnabled } from 'artigen/utils/logFlag.ts';
|
||||
import { basicReducer } from 'artigen/utils/reducers.ts';
|
||||
|
||||
import dbClient from 'db/client.ts';
|
||||
import { queries } from 'db/common.ts';
|
||||
|
||||
import { infoColor1 } from 'embeds/colors.ts';
|
||||
|
||||
import stdResp from 'endpoints/stdResponses.ts';
|
||||
|
||||
import { InteractionValueSeparator } from 'events/interactionCreate.ts';
|
||||
|
||||
import utils from 'utils/utils.ts';
|
||||
import { STATUS_CODE, STATUS_TEXT } from '@std/http/status';
|
||||
|
||||
const getUserIdForEmbed = (rollRequest: QueuedRoll): bigint => {
|
||||
if (rollRequest.apiRoll) return rollRequest.api.userId;
|
||||
if (rollRequest.ddRoll) {
|
||||
if (rollRequest.dd.overrideAuthorId === 0n) return rollRequest.dd.authorId;
|
||||
return rollRequest.dd.overrideAuthorId;
|
||||
}
|
||||
return 0n;
|
||||
};
|
||||
|
||||
const getAuthorIdForButton = (rollRequest: QueuedRoll): bigint => {
|
||||
if (rollRequest.apiRoll) return rollRequest.api.userId;
|
||||
if (rollRequest.ddRoll) return rollRequest.dd.authorId;
|
||||
return 0n;
|
||||
};
|
||||
|
||||
export const repeatRollCustomId = 'repeatRoll';
|
||||
|
||||
export const onWorkerComplete = async (workerMessage: MessageEvent<SolvedRoll>, workerTimeout: number, rollRequest: QueuedRoll) => {
|
||||
const apiResolve = rollRequest.apiRoll ? ApiResolveMap.get(rollRequest.resolve as string) : undefined;
|
||||
const testResolve = rollRequest.testRoll ? TestResolveMap.get(rollRequest.resolve as string) : undefined;
|
||||
rollRequest.apiRoll && ApiResolveMap.delete(rollRequest.resolve as string);
|
||||
rollRequest.testRoll && TestResolveMap.delete(rollRequest.resolve as string);
|
||||
|
||||
let apiErroredOut = false;
|
||||
try {
|
||||
removeWorker();
|
||||
clearTimeout(workerTimeout);
|
||||
|
||||
const returnMsg = workerMessage.data;
|
||||
loggingEnabled && log(LT.LOG, `Roll came back from worker: ${returnMsg.line1.length} |&| ${returnMsg.line2.length} |&| ${returnMsg.line3.length} `);
|
||||
loggingEnabled && log(LT.LOG, `Roll came back from worker: ${returnMsg.line1} |&| ${returnMsg.line2} |&| ${returnMsg.line3} `);
|
||||
const pubEmbedDetails = generateRollEmbed(getUserIdForEmbed(rollRequest), returnMsg, rollRequest.modifiers);
|
||||
const gmEmbedDetails = generateRollEmbed(getUserIdForEmbed(rollRequest), returnMsg, {
|
||||
...rollRequest.modifiers,
|
||||
gmRoll: false,
|
||||
});
|
||||
|
||||
let pubRespCharCount = pubEmbedDetails.charCount;
|
||||
let gmRespCharCount = gmEmbedDetails.charCount;
|
||||
const pubEmbeds: Embed[] = [pubEmbedDetails.embed];
|
||||
const gmEmbeds: Embed[] = [gmEmbedDetails.embed];
|
||||
const pubAttachments: FileContent[] = pubEmbedDetails.hasAttachment ? [pubEmbedDetails.attachment] : [];
|
||||
const gmAttachments: FileContent[] = gmEmbedDetails.hasAttachment ? [gmEmbedDetails.attachment] : [];
|
||||
let countEmbed, rollDistEmbed;
|
||||
|
||||
// Handle adding count embed to correct list
|
||||
if (rollRequest.modifiers.count) {
|
||||
countEmbed = generateCountDetailsEmbed(returnMsg.counts);
|
||||
if (rollRequest.modifiers.gmRoll) {
|
||||
gmEmbeds.push(countEmbed.embed);
|
||||
gmRespCharCount += countEmbed.charCount;
|
||||
} else {
|
||||
pubEmbeds.push(countEmbed.embed);
|
||||
pubRespCharCount += countEmbed.charCount;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle adding rollDist embed to correct list
|
||||
if (rollRequest.modifiers.rollDist) {
|
||||
rollDistEmbed = generateRollDistsEmbed(returnMsg.rollDistributions);
|
||||
if (rollRequest.modifiers.gmRoll) {
|
||||
gmEmbeds.push(rollDistEmbed.embed);
|
||||
rollDistEmbed.hasAttachment && gmAttachments.push(rollDistEmbed.attachment);
|
||||
gmRespCharCount += rollDistEmbed.charCount;
|
||||
} else {
|
||||
pubEmbeds.push(rollDistEmbed.embed);
|
||||
rollDistEmbed.hasAttachment && pubAttachments.push(rollDistEmbed.attachment);
|
||||
pubRespCharCount += rollDistEmbed.charCount;
|
||||
}
|
||||
}
|
||||
|
||||
loggingEnabled && log(LT.LOG, `Embeds are generated: ${pubRespCharCount} ${JSON.stringify(pubEmbeds)} |&| ${gmRespCharCount} ${JSON.stringify(gmEmbeds)}`);
|
||||
|
||||
// If there was an error, report it to the user in hopes that they can determine what they did wrong
|
||||
if (returnMsg.error) {
|
||||
if (rollRequest.apiRoll) {
|
||||
apiResolve && apiResolve(stdResp.InternalServerError(returnMsg.errorMsg));
|
||||
} else if (rollRequest.ddRoll) {
|
||||
rollRequest.dd.myResponse.edit({ embeds: pubEmbeds });
|
||||
} else if (rollRequest.testRoll) {
|
||||
testResolve &&
|
||||
testResolve({
|
||||
error: true,
|
||||
errorMsg: returnMsg.errorMsg,
|
||||
errorCode: returnMsg.errorCode,
|
||||
});
|
||||
}
|
||||
|
||||
if (rollRequest.apiRoll) {
|
||||
// If enabled, log rolls so we can see what went wrong
|
||||
dbClient
|
||||
.execute(queries.insertRollLogCmd(rollRequest.apiRoll ? 1 : 0, 1), [rollRequest.originalCommand, returnMsg.errorCode, null])
|
||||
.catch((e) => utils.commonLoggers.dbError('rollQueue.ts:82', 'insert into', e));
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Test roll will assume that messages send successfully
|
||||
if (rollRequest.testRoll) {
|
||||
testResolve &&
|
||||
testResolve({
|
||||
error: false,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let newMsg: DiscordenoMessage | void = undefined;
|
||||
// Determine if we are to send a GM roll or a normal roll
|
||||
if (rollRequest.modifiers.gmRoll) {
|
||||
if (rollRequest.apiRoll) {
|
||||
newMsg = await sendMessage(rollRequest.api.channelId, {
|
||||
content: rollRequest.modifiers.apiWarn,
|
||||
embeds: pubEmbeds,
|
||||
}).catch(() => {
|
||||
apiErroredOut = true;
|
||||
apiResolve && apiResolve(stdResp.InternalServerError('Message failed to send - location 0.'));
|
||||
});
|
||||
} else {
|
||||
// Send the public embed to correct channel
|
||||
rollRequest.dd.myResponse.edit({ embeds: pubEmbeds });
|
||||
}
|
||||
|
||||
// HOTFIX: makes discordeno actually be able to reply to any message (user or bot) while in dms
|
||||
if (newMsg && !newMsg.guildId) newMsg.guildId = -1n;
|
||||
|
||||
if (!apiErroredOut) {
|
||||
// And message the full details to each of the GMs, alerting roller of every GM that could not be messaged
|
||||
rollRequest.modifiers.gms.forEach(async (gm) => {
|
||||
const gmId: bigint = BigInt(gm.startsWith('<') ? gm.substring(2, gm.length - 1) : gm);
|
||||
log(LT.LOG, `Messaging GM ${gm} | ${gmId}`);
|
||||
// Attempt to DM the GM and send a warning if it could not DM a GM
|
||||
await sendDirectMessage(gmId, {
|
||||
content: `Original GM Roll Request: ${rollRequest.apiRoll ? newMsg && newMsg.link : rollRequest.dd.myResponse.link}`,
|
||||
embeds: gmEmbeds,
|
||||
})
|
||||
.then(async () => {
|
||||
// Check if we need to attach a file and send it after the initial details sent
|
||||
if (gmAttachments.length) {
|
||||
await sendDirectMessage(gmId, {
|
||||
file: gmAttachments,
|
||||
}).catch(() => {
|
||||
if (newMsg && rollRequest.apiRoll) {
|
||||
newMsg.reply(generateDMFailed(gmId));
|
||||
} else if (!rollRequest.apiRoll) {
|
||||
rollRequest.dd.originalMessage.reply(generateDMFailed(gmId));
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (rollRequest.apiRoll && newMsg) {
|
||||
newMsg.reply(generateDMFailed(gmId));
|
||||
} else if (!rollRequest.apiRoll) {
|
||||
rollRequest.dd.originalMessage.reply(generateDMFailed(gmId));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Not a gm roll, so just send normal embed to correct channel
|
||||
if (rollRequest.apiRoll) {
|
||||
newMsg = await sendMessage(rollRequest.api.channelId, {
|
||||
content: rollRequest.modifiers.apiWarn,
|
||||
embeds: pubEmbeds,
|
||||
}).catch(() => {
|
||||
apiErroredOut = true;
|
||||
apiResolve && apiResolve(stdResp.InternalServerError('Message failed to send - location 1.'));
|
||||
});
|
||||
} else {
|
||||
newMsg = await rollRequest.dd.myResponse.edit({
|
||||
content: rollRequest.dd.overrideAuthorId === 0n ? '' : `<@${rollRequest.dd.overrideAuthorId}> used the \`Repeat Roll\` button for the referenced message:`,
|
||||
embeds: pubEmbeds,
|
||||
components: [
|
||||
{
|
||||
type: MessageComponentTypes.ActionRow,
|
||||
components: [
|
||||
{
|
||||
type: MessageComponentTypes.Button,
|
||||
label: 'Repeat Roll',
|
||||
customId: `${repeatRollCustomId}${InteractionValueSeparator}${getAuthorIdForButton(rollRequest).toString()}`,
|
||||
style: ButtonStyles.Secondary,
|
||||
emoji: '🎲',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
// HOTFIX: makes discordeno actually be able to reply to any message (user or bot) while in dms
|
||||
if (newMsg && !newMsg.guildId) newMsg.guildId = -1n;
|
||||
|
||||
if (pubAttachments.length && newMsg) {
|
||||
// Attachment requires you to send a new message
|
||||
const respMessage: Embed[] = [
|
||||
{
|
||||
color: infoColor1,
|
||||
description: `**This message contains information for a previous roll.**
|
||||
Please click on "<@${botId}> *Click to see attachment*" above this message to see the previous roll.`,
|
||||
},
|
||||
];
|
||||
|
||||
if (pubAttachments.map((file) => file.blob.size).reduce(basicReducer, 0) < config.maxFileSize) {
|
||||
// All attachments will fit in one message
|
||||
newMsg &&
|
||||
newMsg
|
||||
.reply({
|
||||
embeds: respMessage,
|
||||
file: pubAttachments,
|
||||
})
|
||||
.then((attachmentMsg) => toggleWebView(attachmentMsg, getUserIdForEmbed(rollRequest).toString(), false))
|
||||
.catch((e) => utils.commonLoggers.messageSendError('workerComplete.ts:230', newMsg as DiscordenoMessage, e));
|
||||
} else {
|
||||
pubAttachments.forEach((file) => {
|
||||
newMsg &&
|
||||
newMsg
|
||||
.reply({
|
||||
embeds: respMessage,
|
||||
file,
|
||||
})
|
||||
.then((attachmentMsg) => toggleWebView(attachmentMsg, getUserIdForEmbed(rollRequest).toString(), false))
|
||||
.catch((e) => utils.commonLoggers.messageSendError('workerComplete.ts:240', newMsg as DiscordenoMessage, e));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (rollRequest.apiRoll && !apiErroredOut) {
|
||||
dbClient
|
||||
.execute(queries.insertRollLogCmd(1, 0), [rollRequest.originalCommand, returnMsg.errorCode, newMsg ? newMsg.id : null])
|
||||
.catch((e) => utils.commonLoggers.dbError('rollQueue.ts:155', 'insert into', e));
|
||||
|
||||
const headers = new Headers();
|
||||
headers.append('Content-Type', 'text/json');
|
||||
apiResolve &&
|
||||
apiResolve(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
discordEmbeds: {
|
||||
rollResponse: pubEmbedDetails,
|
||||
countsResponse: countEmbed ?? null,
|
||||
rollDistResponse: rollDistEmbed ?? null,
|
||||
},
|
||||
rawData: {
|
||||
roll: {
|
||||
raw: returnMsg.line1,
|
||||
results: returnMsg.line2,
|
||||
details: returnMsg.line3,
|
||||
},
|
||||
counts: rollRequest.modifiers.count ? returnMsg.counts : null,
|
||||
rollDistributions: rollRequest.modifiers.rollDist ? returnMsg.rollDistributions.entries().toArray() : null,
|
||||
},
|
||||
}),
|
||||
{
|
||||
status: STATUS_CODE.OK,
|
||||
statusText: STATUS_TEXT[STATUS_CODE.OK],
|
||||
headers,
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
log(LT.ERROR, `Unhandled rollRequest Error: ${JSON.stringify(e)}`);
|
||||
if (rollRequest.ddRoll) {
|
||||
rollRequest.dd.myResponse.edit({
|
||||
embeds: [
|
||||
(
|
||||
await generateRollEmbed(
|
||||
0n,
|
||||
<SolvedRoll> {
|
||||
error: true,
|
||||
errorMsg:
|
||||
`Something weird went wrong, likely the requested roll is too complex and caused the response to be too large for Discord. Try breaking the request down into smaller messages and try again.\n\nIf this error continues to come up, please \`${config.prefix}report\` this to my developer.`,
|
||||
errorCode: 'UnhandledWorkerComplete',
|
||||
},
|
||||
<RollModifiers> {},
|
||||
)
|
||||
).embed,
|
||||
],
|
||||
});
|
||||
} else if (rollRequest.apiRoll && !apiErroredOut) {
|
||||
apiResolve && apiResolve(stdResp.InternalServerError(JSON.stringify(e)));
|
||||
} else if (rollRequest.testRoll) {
|
||||
testResolve &&
|
||||
testResolve({
|
||||
error: true,
|
||||
errorMsg: 'Something weird went wrong.',
|
||||
errorCode: 'UnhandledWorkerComplete',
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
|
@ -0,0 +1,18 @@
|
|||
import { log, LogTypes as LT } from '@Log4Deno';
|
||||
import { nanoid } from '@nanoid';
|
||||
|
||||
import { QueuedRoll } from 'artigen/managers/manager.d.ts';
|
||||
import { ApiResolveMap, TestResolveMap } from 'artigen/managers/resolveManager.ts';
|
||||
|
||||
import { loggingEnabled } from 'artigen/utils/logFlag.ts';
|
||||
|
||||
export const onWorkerReady = (rollWorker: Worker, rollRequest: QueuedRoll) => {
|
||||
if ((rollRequest.apiRoll || rollRequest.testRoll) && typeof rollRequest.resolve !== 'string') {
|
||||
const resolveId = nanoid();
|
||||
rollRequest.apiRoll && ApiResolveMap.set(resolveId, rollRequest.resolve);
|
||||
rollRequest.testRoll && TestResolveMap.set(resolveId, rollRequest.resolve);
|
||||
rollRequest.resolve = resolveId;
|
||||
}
|
||||
loggingEnabled && log(LT.LOG, `Sending roll to worker: ${rollRequest.rollCmd}, ${JSON.stringify(rollRequest.modifiers)}`);
|
||||
rollWorker.postMessage(rollRequest);
|
||||
};
|
|
@ -0,0 +1,51 @@
|
|||
import { SolvedRoll } from 'artigen/artigen.d.ts';
|
||||
|
||||
import { RollModifiers } from 'artigen/dice/dice.d.ts';
|
||||
|
||||
import { removeWorker } from 'artigen/managers/countManager.ts';
|
||||
import { QueuedRoll } from 'artigen/managers/manager.d.ts';
|
||||
import { ApiResolveMap, TestResolveMap } from 'artigen/managers/resolveManager.ts';
|
||||
|
||||
import { generateRollEmbed } from 'artigen/utils/embeds.ts';
|
||||
|
||||
import stdResp from 'endpoints/stdResponses.ts';
|
||||
|
||||
import utils from 'utils/utils.ts';
|
||||
|
||||
export const terminateWorker = async (rollWorker: Worker, rollRequest: QueuedRoll) => {
|
||||
rollWorker.terminate();
|
||||
removeWorker();
|
||||
const apiResolve = rollRequest.apiRoll ? ApiResolveMap.get(rollRequest.resolve as string) : undefined;
|
||||
const testResolve = rollRequest.testRoll ? TestResolveMap.get(rollRequest.resolve as string) : undefined;
|
||||
rollRequest.apiRoll && ApiResolveMap.delete(rollRequest.resolve as string);
|
||||
rollRequest.testRoll && TestResolveMap.delete(rollRequest.resolve as string);
|
||||
|
||||
if (rollRequest.apiRoll) {
|
||||
apiResolve && apiResolve(stdResp.RequestTimeout('Roll took too long to process, try breaking roll down into simpler parts'));
|
||||
} else if (rollRequest.ddRoll) {
|
||||
rollRequest.dd.myResponse
|
||||
.edit({
|
||||
embeds: [
|
||||
(
|
||||
await generateRollEmbed(
|
||||
0n,
|
||||
<SolvedRoll> {
|
||||
error: true,
|
||||
errorCode: 'TooComplex',
|
||||
errorMsg: 'Error: Roll took too long to process, try breaking roll down into simpler parts',
|
||||
},
|
||||
<RollModifiers> {},
|
||||
)
|
||||
).embed,
|
||||
],
|
||||
})
|
||||
.catch((e) => utils.commonLoggers.messageEditError('rollQueue.ts:51', rollRequest.dd.myResponse, e));
|
||||
} else if (rollRequest.testRoll) {
|
||||
testResolve &&
|
||||
testResolve({
|
||||
error: true,
|
||||
errorCode: 'TooComplex',
|
||||
errorMsg: 'Error: Roll took too long to process, try breaking roll down into simpler parts',
|
||||
});
|
||||
}
|
||||
};
|
|
@ -0,0 +1,18 @@
|
|||
import { log, LogTypes as LT } from '@Log4Deno';
|
||||
|
||||
import config from '~config';
|
||||
|
||||
import { loopLoggingEnabled } from 'artigen/utils/logFlag.ts';
|
||||
|
||||
let loopCount = 0;
|
||||
|
||||
// Will ensure if maxLoops is 10, 10 loops will be allowed, 11 will not.
|
||||
export const loopCountCheck = (location = 'unset'): void => {
|
||||
loopCount++;
|
||||
loopLoggingEnabled && log(LT.LOG, `Loop #${loopCount} at "${location}"`);
|
||||
if (loopCount > config.limits.maxLoops) {
|
||||
throw new Error('MaxLoopsExceeded');
|
||||
}
|
||||
};
|
||||
|
||||
export const getLoopCount = (): number => loopCount;
|
|
@ -0,0 +1,49 @@
|
|||
import { DiscordenoMessage } from '@discordeno';
|
||||
|
||||
import { RollModifiers } from 'artigen/dice/dice.d.ts';
|
||||
|
||||
// QueuedRoll is the structure to track rolls we could not immediately handle
|
||||
interface BaseQueuedRoll {
|
||||
rollCmd: string;
|
||||
modifiers: RollModifiers;
|
||||
originalCommand: string;
|
||||
}
|
||||
export type ApiResolve = (value: Response | PromiseLike<Response>) => void;
|
||||
interface ApiQueuedRoll extends BaseQueuedRoll {
|
||||
apiRoll: true;
|
||||
ddRoll: false;
|
||||
testRoll: false;
|
||||
resolve: string | ApiResolve;
|
||||
api: {
|
||||
channelId: bigint;
|
||||
userId: bigint;
|
||||
};
|
||||
}
|
||||
interface DDQueuedRoll extends BaseQueuedRoll {
|
||||
apiRoll: false;
|
||||
ddRoll: true;
|
||||
testRoll: false;
|
||||
dd: {
|
||||
myResponse: DiscordenoMessage;
|
||||
originalMessage: DiscordenoMessage;
|
||||
overrideAuthorId: bigint;
|
||||
authorId: bigint;
|
||||
};
|
||||
}
|
||||
interface TestResultFail {
|
||||
error: true;
|
||||
errorMsg: string;
|
||||
errorCode: string;
|
||||
}
|
||||
interface TestResultSuccess {
|
||||
error: false;
|
||||
}
|
||||
export type TestResults = TestResultFail | TestResultSuccess;
|
||||
export type TestResolve = (value: TestResults) => void;
|
||||
interface TestQueuedRoll extends BaseQueuedRoll {
|
||||
apiRoll: false;
|
||||
ddRoll: false;
|
||||
testRoll: true;
|
||||
resolve: string | TestResolve;
|
||||
}
|
||||
export type QueuedRoll = ApiQueuedRoll | DDQueuedRoll | TestQueuedRoll;
|
|
@ -0,0 +1,56 @@
|
|||
import { log, LogTypes as LT } from '@Log4Deno';
|
||||
|
||||
import config from '~config';
|
||||
|
||||
import { getWorkerCnt } from 'artigen/managers/countManager.ts';
|
||||
import { QueuedRoll } from 'artigen/managers/manager.d.ts';
|
||||
import { handleRollRequest } from 'artigen/managers/workerManager.ts';
|
||||
|
||||
import { rollingEmbed } from 'artigen/utils/embeds.ts';
|
||||
|
||||
import { infoColor2 } from 'embeds/colors.ts';
|
||||
|
||||
import utils from 'utils/utils.ts';
|
||||
|
||||
const rollQueue: Array<QueuedRoll> = [];
|
||||
|
||||
// Runs the roll or queues it depending on how many workers are currently running
|
||||
export const sendRollRequest = (rollRequest: QueuedRoll) => {
|
||||
if (!rollQueue.length && getWorkerCnt() < config.limits.maxWorkers) {
|
||||
handleRollRequest(rollRequest);
|
||||
} else {
|
||||
rollQueue.push(rollRequest);
|
||||
rollRequest.ddRoll &&
|
||||
rollRequest.dd.myResponse
|
||||
.edit({
|
||||
embeds: [
|
||||
{
|
||||
color: infoColor2,
|
||||
title: `${config.name} currently has its hands full and has queued your roll.`,
|
||||
description: `There are currently ${getWorkerCnt() + rollQueue.length} rolls ahead of this roll.
|
||||
|
||||
The results for this roll will replace this message when it is done.`,
|
||||
},
|
||||
],
|
||||
})
|
||||
.catch((e: Error) => utils.commonLoggers.messageEditError('rollQueue.ts:197', rollRequest.dd.myResponse, e));
|
||||
}
|
||||
};
|
||||
|
||||
// Checks the queue constantly to make sure the queue stays empty
|
||||
setInterval(() => {
|
||||
log(
|
||||
LT.LOG,
|
||||
`Checking rollQueue for items, rollQueue length: ${rollQueue.length}, currentWorkers: ${getWorkerCnt()}, config.limits.maxWorkers: ${config.limits.maxWorkers}`,
|
||||
);
|
||||
if (rollQueue.length && getWorkerCnt() < config.limits.maxWorkers) {
|
||||
const rollRequest = rollQueue.shift();
|
||||
if (rollRequest) {
|
||||
rollRequest.ddRoll &&
|
||||
rollRequest.dd.myResponse
|
||||
.edit(rollingEmbed)
|
||||
.catch((e: Error) => utils.commonLoggers.messageEditError('rollQueue.ts:208', rollRequest.dd.myResponse, e));
|
||||
handleRollRequest(rollRequest);
|
||||
}
|
||||
}
|
||||
}, 1_000);
|
|
@ -0,0 +1,4 @@
|
|||
import { ApiResolve, TestResolve } from 'artigen/managers/manager.d.ts';
|
||||
|
||||
export const ApiResolveMap = new Map<string, ApiResolve>();
|
||||
export const TestResolveMap = new Map<string, TestResolve>();
|
|
@ -0,0 +1,23 @@
|
|||
import config from '~config';
|
||||
|
||||
import { addWorker } from 'artigen/managers/countManager.ts';
|
||||
import { QueuedRoll } from 'artigen/managers/manager.d.ts';
|
||||
|
||||
import { onWorkerComplete } from 'artigen/managers/handler/workerComplete.ts';
|
||||
import { onWorkerReady } from 'artigen/managers/handler/workerReady.ts';
|
||||
import { terminateWorker } from 'artigen/managers/handler/workerTerminate.ts';
|
||||
|
||||
export const handleRollRequest = (rollRequest: QueuedRoll) => {
|
||||
// Handle setting up and calling the rollWorker
|
||||
addWorker();
|
||||
const rollWorker = new Worker(new URL('./artigenWorker.ts', import.meta.url).href, { type: 'module' });
|
||||
const workerTimeout = setTimeout(() => terminateWorker(rollWorker, rollRequest), config.limits.workerTimeout);
|
||||
|
||||
// Handle events from the worker
|
||||
rollWorker.addEventListener('message', (workerMessage) => {
|
||||
if (workerMessage.data === 'ready') {
|
||||
return onWorkerReady(rollWorker, rollRequest);
|
||||
}
|
||||
onWorkerComplete(workerMessage, workerTimeout, rollRequest);
|
||||
});
|
||||
};
|
|
@ -0,0 +1,11 @@
|
|||
// SolvedStep is used to preserve information while math is being performed on the roll
|
||||
export interface SolvedStep {
|
||||
total: number;
|
||||
details: string;
|
||||
containsCrit: boolean;
|
||||
containsFail: boolean;
|
||||
isComplex: boolean;
|
||||
}
|
||||
|
||||
// Joined type for mathConf as its a "WIP" variable and moved everything from string->number->SolvedStep
|
||||
export type MathConf = string | number | SolvedStep;
|
|
@ -0,0 +1,220 @@
|
|||
/* The Artificer was built in memory of Babka
|
||||
* With love, Ean
|
||||
*
|
||||
* December 21, 2020
|
||||
*/
|
||||
import { log, LogTypes as LT } from '@Log4Deno';
|
||||
|
||||
import { MathConf, SolvedStep } from 'artigen/math/math.d.ts';
|
||||
|
||||
import { loopCountCheck } from 'artigen/managers/loopManager.ts';
|
||||
|
||||
import { legalMath, legalMathOperators } from 'artigen/utils/legalMath.ts';
|
||||
import { loggingEnabled } from 'artigen/utils/logFlag.ts';
|
||||
import { getMatchingParenIdx } from 'artigen/utils/parenBalance.ts';
|
||||
|
||||
// mathSolver(conf, wrapDetails) returns one condensed SolvedStep
|
||||
// mathSolver is a function that recursively solves the full roll and math
|
||||
export const mathSolver = (conf: MathConf[], wrapDetails = false): SolvedStep => {
|
||||
// Initialize PEMDAS
|
||||
const signs = ['^', '**', '*', '/', '%', '+', '-'];
|
||||
const stepSolve: SolvedStep = {
|
||||
total: 0,
|
||||
details: '',
|
||||
containsCrit: false,
|
||||
containsFail: false,
|
||||
isComplex: false,
|
||||
};
|
||||
|
||||
// If entering with a single number, note it now
|
||||
let singleNum = false;
|
||||
if (conf.length === 1) {
|
||||
singleNum = true;
|
||||
}
|
||||
|
||||
// Evaluate all parenthesis
|
||||
while (conf.includes('(')) {
|
||||
loopCountCheck('mathSolver.ts - evaluating parens');
|
||||
|
||||
loggingEnabled && log(LT.LOG, `Evaluating roll ${JSON.stringify(conf)} | Looking for (`);
|
||||
// Get first open parenthesis
|
||||
let openParenIdx = conf.indexOf('(');
|
||||
const closeParenIdx = getMatchingParenIdx(conf, openParenIdx);
|
||||
|
||||
// Call the solver on the items between openParenIdx and closeParenIdx (excluding the parens)
|
||||
const parenSolve = mathSolver(conf.slice(openParenIdx + 1, closeParenIdx), true);
|
||||
// Replace the items between openParenIdx and closeParenIdx (including the parens) with its solved equivalent
|
||||
conf.splice(openParenIdx, closeParenIdx - openParenIdx + 1, parenSolve);
|
||||
|
||||
// Determine if previous idx is a Math operator and execute it
|
||||
if (openParenIdx - 1 > -1 && legalMathOperators.includes(conf[openParenIdx - 1].toString())) {
|
||||
// Update total and details of parenSolve
|
||||
parenSolve.total = legalMath[legalMathOperators.indexOf(conf[openParenIdx - 1].toString())](parenSolve.total);
|
||||
parenSolve.details = `${conf[openParenIdx - 1]}${parenSolve.details}`;
|
||||
|
||||
conf.splice(openParenIdx - 1, 2, parenSolve);
|
||||
// shift openParenIdx as we have just removed something before it
|
||||
openParenIdx--;
|
||||
}
|
||||
|
||||
// Determining if we need to add in a multiplication sign to handle implicit multiplication (like "(4)2" = 8)
|
||||
// Check if a number was directly before openParenIdx and slip in the "*" if needed
|
||||
if (openParenIdx - 1 > -1 && !signs.includes(conf[openParenIdx - 1].toString())) {
|
||||
conf.splice(openParenIdx, 0, '*');
|
||||
// shift openParenIdx as we have just added something before it
|
||||
openParenIdx++;
|
||||
}
|
||||
// Check if a number is directly after the closing paren and slip in the "*" if needed
|
||||
// openParenIdx is used here as the conf array has already been collapsed down
|
||||
if (openParenIdx + 1 < conf.length && !signs.includes(conf[openParenIdx + 1].toString())) {
|
||||
conf.splice(openParenIdx + 1, 0, '*');
|
||||
}
|
||||
}
|
||||
|
||||
// Look for any implicit multiplication that may have been missed
|
||||
// Start at index 1 as there will never be implicit multiplication before the first element
|
||||
loggingEnabled && log(LT.LOG, `Checking for missing implicit multiplication ${JSON.stringify(conf)}`);
|
||||
for (let i = 1; i < conf.length; i++) {
|
||||
loopCountCheck('mathSolver.ts - checking for implicit multiplication');
|
||||
|
||||
const prevConfAsStr = <string> conf[i - 1];
|
||||
const curConfAsStr = <string> conf[i];
|
||||
if (!signs.includes(curConfAsStr) && !signs.includes(prevConfAsStr)) {
|
||||
// Both previous and current conf are operators, slip in the "*"
|
||||
conf.splice(i, 0, '*');
|
||||
}
|
||||
}
|
||||
|
||||
// At this point, conf should be [num, op, num, op, num, op, num, etc]
|
||||
|
||||
// Evaluate all EMDAS by looping thru each tier of operators (exponential is the highest tier, addition/subtraction the lowest)
|
||||
const allCurOps = [
|
||||
['^', '**'],
|
||||
['*', '/', '%'],
|
||||
['+', '-'],
|
||||
];
|
||||
allCurOps.forEach((curOps) => {
|
||||
// No loopCountCheck here since its finite/will always be 3 loops
|
||||
loggingEnabled && log(LT.LOG, `Cur Ops ${JSON.stringify(curOps)}`);
|
||||
// Iterate thru all operators/operands in the conf
|
||||
for (let i = 0; i < conf.length; i++) {
|
||||
loggingEnabled && log(LT.LOG, `Checking ${JSON.stringify(conf[i])}`);
|
||||
// Check if the current index is in the active tier of operators
|
||||
if (curOps.includes(conf[i].toString())) {
|
||||
loggingEnabled && log(LT.LOG, `Evaluating roll ${JSON.stringify(conf)} as ${JSON.stringify(conf[i])} is in curOps`);
|
||||
loopCountCheck('mathSolver.ts - evaluating roll');
|
||||
// Grab the operands from before and after the operator
|
||||
const operand1 = conf[i - 1];
|
||||
const operand2 = conf[i + 1];
|
||||
// Init temp math to NaN to catch bad parsing
|
||||
let oper1 = NaN;
|
||||
let oper2 = NaN;
|
||||
const subStepSolve: SolvedStep = {
|
||||
total: NaN,
|
||||
details: '',
|
||||
containsCrit: false,
|
||||
containsFail: false,
|
||||
isComplex: false,
|
||||
};
|
||||
|
||||
// If operand1 is a SolvedStep, populate our subStepSolve with its details and crit/fail flags
|
||||
if (typeof operand1 === 'object') {
|
||||
oper1 = operand1.total;
|
||||
subStepSolve.details = `${operand1.details}\\${conf[i]}`;
|
||||
subStepSolve.containsCrit = operand1.containsCrit;
|
||||
subStepSolve.containsFail = operand1.containsFail;
|
||||
subStepSolve.isComplex = operand1.isComplex;
|
||||
} else {
|
||||
// else parse it as a number and add it to the subStep details
|
||||
if (operand1 || operand1 == 0) {
|
||||
oper1 = parseFloat(operand1.toString());
|
||||
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') {
|
||||
oper2 = operand2.total;
|
||||
subStepSolve.details += operand2.details;
|
||||
subStepSolve.containsCrit = subStepSolve.containsCrit || operand2.containsCrit;
|
||||
subStepSolve.containsFail = subStepSolve.containsFail || operand2.containsFail;
|
||||
subStepSolve.isComplex = subStepSolve.isComplex || operand2.isComplex;
|
||||
} else {
|
||||
// else parse it as a number and add it to the subStep details
|
||||
oper2 = parseFloat(operand2.toString());
|
||||
subStepSolve.details += oper2;
|
||||
}
|
||||
|
||||
// Make sure neither operand is NaN before continuing
|
||||
if (isNaN(oper1) || isNaN(oper2)) {
|
||||
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') {
|
||||
// Finally do the operator on the operands, throw an error if the operator is not found
|
||||
switch (conf[i]) {
|
||||
case '^':
|
||||
case '**':
|
||||
subStepSolve.total = Math.pow(oper1, oper2);
|
||||
break;
|
||||
case '*':
|
||||
subStepSolve.total = oper1 * oper2;
|
||||
break;
|
||||
case '/':
|
||||
subStepSolve.total = oper1 / oper2;
|
||||
break;
|
||||
case '%':
|
||||
subStepSolve.total = oper1 % oper2;
|
||||
break;
|
||||
case '+':
|
||||
subStepSolve.total = oper1 + oper2;
|
||||
break;
|
||||
case '-':
|
||||
subStepSolve.total = oper1 - oper2;
|
||||
break;
|
||||
default:
|
||||
throw new Error('OperatorWhat');
|
||||
}
|
||||
} else {
|
||||
throw new Error('EMDASNotNumber');
|
||||
}
|
||||
|
||||
// Replace the two operands and their operator with our subStepSolve
|
||||
conf.splice(i - 1, 3, 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--;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// If we somehow have more than one item left in conf at this point, something broke, throw an error
|
||||
if (conf.length > 1) {
|
||||
loggingEnabled && log(LT.LOG, `ConfWHAT? ${JSON.stringify(conf)}`);
|
||||
throw new Error('ConfWhat');
|
||||
} 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.details = conf[0].toString();
|
||||
} else {
|
||||
// Else fully populate the stepSolve with what was computed
|
||||
const tempConf = <SolvedStep> conf[0];
|
||||
stepSolve.total = tempConf.total;
|
||||
stepSolve.details = tempConf.details;
|
||||
stepSolve.containsCrit = tempConf.containsCrit;
|
||||
stepSolve.containsFail = tempConf.containsFail;
|
||||
stepSolve.isComplex = tempConf.isComplex;
|
||||
}
|
||||
|
||||
// If this was a nested call, add on parens around the details to show what math we've done
|
||||
if (wrapDetails) {
|
||||
stepSolve.details = `(${stepSolve.details})`;
|
||||
}
|
||||
|
||||
// If our total has reached undefined for some reason, error out now
|
||||
if (stepSolve.total === undefined) {
|
||||
throw new Error('UndefinedStep');
|
||||
}
|
||||
|
||||
return stepSolve;
|
||||
};
|
|
@ -0,0 +1,347 @@
|
|||
import { log, LogTypes as LT } from '@Log4Deno';
|
||||
|
||||
import { ReturnData } from 'artigen/artigen.d.ts';
|
||||
|
||||
import { MathConf, SolvedStep } from 'artigen/math/math.d.ts';
|
||||
|
||||
import { CountDetails, ExecutedRoll, GroupConf, RollDistributionMap, RollModifiers, RollSet } from 'artigen/dice/dice.d.ts';
|
||||
import { formatRoll } from 'artigen/dice/generateFormattedRoll.ts';
|
||||
|
||||
import { loopCountCheck } from 'artigen/managers/loopManager.ts';
|
||||
|
||||
import { mathSolver } from 'artigen/math/mathSolver.ts';
|
||||
|
||||
import { closeInternalGrp, cmdSplitRegex, internalWrapRegex, mathSplitRegex, openInternalGrp } from 'artigen/utils/escape.ts';
|
||||
import { legalMathOperators } from 'artigen/utils/legalMath.ts';
|
||||
import { loggingEnabled } from 'artigen/utils/logFlag.ts';
|
||||
import { assertParenBalance } from 'artigen/utils/parenBalance.ts';
|
||||
import { executeRoll } from 'artigen/dice/executeRoll.ts';
|
||||
import { compareOrigIdx, compareRolls } from 'artigen/utils/sortFuncs.ts';
|
||||
|
||||
// minusOps are operators that will cause a negative sign to collapse into a number (in cases like + - 1)
|
||||
const minusOps = ['(', '^', '**', '*', '/', '%', '+', '-'];
|
||||
const allOps = [...minusOps, ')'];
|
||||
|
||||
export const tokenizeMath = (
|
||||
cmd: string,
|
||||
modifiers: RollModifiers,
|
||||
previousResults: number[],
|
||||
groupResults: ReturnData[],
|
||||
groupConf: GroupConf | null = null,
|
||||
): [ReturnData[], CountDetails[], RollDistributionMap[]] => {
|
||||
const countDetails: CountDetails[] = [];
|
||||
const rollDists: RollDistributionMap[] = [];
|
||||
const executedRolls: Map<number, ExecutedRoll> = new Map();
|
||||
|
||||
loggingEnabled && log(LT.LOG, `Parsing roll ${cmd} | ${JSON.stringify(modifiers)} | ${JSON.stringify(previousResults)}`);
|
||||
|
||||
// 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: MathConf[] = cmd
|
||||
.replace(cmdSplitRegex, '')
|
||||
.replace(internalWrapRegex, '')
|
||||
.replace(/ /g, '')
|
||||
.split(mathSplitRegex)
|
||||
.filter((x) => x);
|
||||
loggingEnabled && log(LT.LOG, `Split roll into mathConf ${JSON.stringify(mathConf)}`);
|
||||
|
||||
// Verify balanced parens before doing anything
|
||||
if (mathConf.includes('(') || mathConf.includes(')')) assertParenBalance(mathConf);
|
||||
|
||||
// Evaluate all rolls into stepSolve format and all numbers into floats
|
||||
for (let i = 0; i < mathConf.length; i++) {
|
||||
loopCountCheck('mathTokenizer.ts - parsing all tokens into MathConf');
|
||||
|
||||
loggingEnabled && log(LT.LOG, `Parsing roll ${JSON.stringify(cmd)} | Evaluating rolls into math-able items ${JSON.stringify(mathConf[i])}`);
|
||||
|
||||
const curMathConfStr = mathConf[i].toString();
|
||||
|
||||
if (curMathConfStr.length === 0) {
|
||||
// If its an empty string, get it out of here
|
||||
mathConf.splice(i, 1);
|
||||
i--;
|
||||
} else if (mathConf[i] == parseFloat(curMathConfStr)) {
|
||||
// If its a number, parse the number out
|
||||
mathConf[i] = parseFloat(curMathConfStr);
|
||||
} else if (curMathConfStr.startsWith(openInternalGrp)) {
|
||||
const groupIdx = parseInt(curMathConfStr.substring(1, curMathConfStr.indexOf(closeInternalGrp)));
|
||||
if (groupIdx >= groupResults.length) {
|
||||
throw new Error('InternalGroupMachineBroke');
|
||||
}
|
||||
mathConf[i] = {
|
||||
total: groupResults[groupIdx].rollTotal,
|
||||
details: groupResults[groupIdx].rollDetails,
|
||||
containsCrit: groupResults[groupIdx].containsCrit,
|
||||
containsFail: groupResults[groupIdx].containsFail,
|
||||
isComplex: groupResults[groupIdx].isComplex,
|
||||
};
|
||||
} else if (curMathConfStr.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,
|
||||
isComplex: false,
|
||||
};
|
||||
} else if (curMathConfStr.toLowerCase() === 'lemon' || curMathConfStr.toLowerCase() === '🍋') {
|
||||
mathConf[i] = {
|
||||
total: 5,
|
||||
details: '🍋',
|
||||
containsCrit: false,
|
||||
containsFail: false,
|
||||
isComplex: false,
|
||||
};
|
||||
} else if (curMathConfStr.toLowerCase() === 'horse' || curMathConfStr.toLowerCase() === '🐴') {
|
||||
mathConf[i] = {
|
||||
total: Math.sqrt(3),
|
||||
details: '🐴',
|
||||
containsCrit: false,
|
||||
containsFail: false,
|
||||
isComplex: false,
|
||||
};
|
||||
} else if (curMathConfStr.toLowerCase() === 'fart' || curMathConfStr.toLowerCase() === '💩') {
|
||||
mathConf[i] = {
|
||||
total: 7,
|
||||
details: '💩',
|
||||
containsCrit: false,
|
||||
containsFail: false,
|
||||
isComplex: false,
|
||||
};
|
||||
} else if (curMathConfStr.toLowerCase() === 'sex' || curMathConfStr.toLowerCase() === '🍆🍑' || curMathConfStr.toLowerCase() === '🍑🍆') {
|
||||
mathConf[i] = {
|
||||
total: 69,
|
||||
details: '( ͡° ͜ʖ ͡°)',
|
||||
containsCrit: false,
|
||||
containsFail: false,
|
||||
isComplex: false,
|
||||
};
|
||||
} else if (curMathConfStr.toLowerCase() === 'inf' || curMathConfStr.toLowerCase() === 'infinity' || curMathConfStr.toLowerCase() === '∞') {
|
||||
// If the operand is the constant Infinity, create a SolvedStep for it
|
||||
mathConf[i] = {
|
||||
total: Infinity,
|
||||
details: '∞',
|
||||
containsCrit: false,
|
||||
containsFail: false,
|
||||
isComplex: false,
|
||||
};
|
||||
} else if (curMathConfStr.toLowerCase() === 'pi' || curMathConfStr.toLowerCase() === '𝜋') {
|
||||
// If the operand is the constant pi, create a SolvedStep for it
|
||||
mathConf[i] = {
|
||||
total: Math.PI,
|
||||
details: '𝜋',
|
||||
containsCrit: false,
|
||||
containsFail: false,
|
||||
isComplex: false,
|
||||
};
|
||||
} else if (curMathConfStr.toLowerCase() === 'pie' || curMathConfStr.toLowerCase() === '🥧') {
|
||||
// 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,
|
||||
isComplex: false,
|
||||
};
|
||||
mathConf.splice(
|
||||
i + 1,
|
||||
0,
|
||||
...[
|
||||
'*',
|
||||
{
|
||||
total: Math.E,
|
||||
details: '*e*',
|
||||
containsCrit: false,
|
||||
containsFail: false,
|
||||
isComplex: false,
|
||||
},
|
||||
],
|
||||
);
|
||||
i += 2;
|
||||
} else if (!legalMathOperators.includes(curMathConfStr) && legalMathOperators.some((mathOp) => curMathConfStr.endsWith(mathOp))) {
|
||||
// Identify when someone does something weird like 4floor(2.5) and split 4 and floor
|
||||
const matchedMathOp = legalMathOperators.filter((mathOp) => curMathConfStr.endsWith(mathOp))[0];
|
||||
mathConf[i] = parseFloat(curMathConfStr.replace(matchedMathOp, ''));
|
||||
|
||||
mathConf.splice(i + 1, 0, ...['*', matchedMathOp]);
|
||||
i += 2;
|
||||
} else if (/(x\d+(\.\d*)?)/.test(curMathConfStr)) {
|
||||
// Identify when someone is using a variable from previous commands
|
||||
if (curMathConfStr.includes('.')) {
|
||||
// Verify someone did not enter x1.1 as a variable
|
||||
throw new Error(`IllegalVariable_${curMathConfStr}`);
|
||||
}
|
||||
|
||||
const varIdx = parseInt(curMathConfStr.replaceAll('x', ''));
|
||||
|
||||
// Get the index from the variable and attempt to use it to query the previousResults
|
||||
if (previousResults.length > varIdx) {
|
||||
mathConf[i] = parseFloat(previousResults[varIdx].toString());
|
||||
} else {
|
||||
throw new Error(`IllegalVariable_${curMathConfStr}`);
|
||||
}
|
||||
} else if (/(y\d+(\.\d*)?)/.test(curMathConfStr)) {
|
||||
// Identify when someone is using a variable from alias input
|
||||
if (curMathConfStr.includes('.')) {
|
||||
// Verify someone did not enter y1.1 as a variable
|
||||
throw new Error(`IllegalVariable_${curMathConfStr}`);
|
||||
}
|
||||
|
||||
const yValue = modifiers.yVars.get(curMathConfStr);
|
||||
if (typeof yValue === 'number') {
|
||||
mathConf[i] = yValue;
|
||||
} else {
|
||||
throw new Error(`VariableMissingValue_${curMathConfStr}`);
|
||||
}
|
||||
} else if (![...allOps, ...legalMathOperators].includes(curMathConfStr)) {
|
||||
// If nothing else has handled it by now, try it as a roll
|
||||
const executedRoll = executeRoll(curMathConfStr, modifiers);
|
||||
if (groupConf) {
|
||||
executedRolls.set(i, executedRoll);
|
||||
} else {
|
||||
const formattedRoll = formatRoll(executedRoll, modifiers);
|
||||
mathConf[i] = formattedRoll.solvedStep;
|
||||
countDetails.push(formattedRoll.countDetails);
|
||||
if (modifiers.rollDist) rollDists.push(formattedRoll.rollDistributions);
|
||||
}
|
||||
}
|
||||
|
||||
// Identify if we are in a state where the current number is a negative number
|
||||
if (mathConf[i - 1] === '-' && ((!mathConf[i - 2] && mathConf[i - 2] !== 0) || minusOps.includes(<string> mathConf[i - 2]))) {
|
||||
if (typeof mathConf[i] === 'string') {
|
||||
// Current item is a mathOp, need to insert a "-1 *" before it
|
||||
mathConf.splice(i - 1, 1, ...[parseFloat('-1'), '*']);
|
||||
i += 2;
|
||||
} else {
|
||||
// Handle normally, just set current item to negative
|
||||
if (typeof mathConf[i] === 'number') {
|
||||
mathConf[i] = <number> mathConf[i] * -1;
|
||||
} else {
|
||||
(<SolvedStep> mathConf[i]).total = (<SolvedStep> mathConf[i]).total * -1;
|
||||
(<SolvedStep> mathConf[i]).details = `-${(<SolvedStep> mathConf[i]).details}`;
|
||||
}
|
||||
mathConf.splice(i - 1, 1);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle applying the group config
|
||||
if (groupConf) {
|
||||
loggingEnabled && log(LT.LOG, `Applying groupConf to executedRolls | ${JSON.stringify(groupConf)} ${JSON.stringify(executedRolls.entries().toArray())}`);
|
||||
// Merge all rollSets into one array, adding the idx into each rollSet to allow separating them back out
|
||||
const allRollSets: RollSet[] = [];
|
||||
const executedRollArr = executedRolls.entries().toArray();
|
||||
executedRollArr.forEach(([rollGroupIdx, executedRoll]) => {
|
||||
executedRoll.rollSet.forEach((roll) => (roll.rollGrpIdx = rollGroupIdx));
|
||||
allRollSets.push(...executedRoll.rollSet);
|
||||
});
|
||||
loggingEnabled && log(LT.LOG, `raw rollSets: ${JSON.stringify(allRollSets)}`);
|
||||
|
||||
// Handle drop or keep operations
|
||||
if (groupConf.drop.on || groupConf.keep.on || groupConf.dropHigh.on || groupConf.keepLow.on) {
|
||||
allRollSets.sort(compareRolls);
|
||||
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 (groupConf.drop.on) {
|
||||
dropCount = groupConf.drop.count;
|
||||
if (dropCount > allRollSets.length) {
|
||||
dropCount = allRollSets.length;
|
||||
}
|
||||
} else if (groupConf.keep.on) {
|
||||
dropCount = allRollSets.length - groupConf.keep.count;
|
||||
if (dropCount < 0) {
|
||||
dropCount = 0;
|
||||
}
|
||||
} // 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 (groupConf.dropHigh.on) {
|
||||
allRollSets.reverse();
|
||||
dropCount = groupConf.dropHigh.count;
|
||||
if (dropCount > allRollSets.length) {
|
||||
dropCount = allRollSets.length;
|
||||
}
|
||||
} else if (groupConf.keepLow.on) {
|
||||
allRollSets.reverse();
|
||||
dropCount = allRollSets.length - groupConf.keepLow.count;
|
||||
if (dropCount < 0) {
|
||||
dropCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
let i = 0;
|
||||
while (dropCount > 0 && i < allRollSets.length) {
|
||||
loopCountCheck('mathTokenizer.ts - handling group dropping');
|
||||
|
||||
loggingEnabled && log(LT.LOG, `Handling group dropping | Dropping ${dropCount}, looking at ${JSON.stringify(allRollSets[i])}`);
|
||||
|
||||
if (!allRollSets[i].dropped && !allRollSets[i].rerolled) {
|
||||
allRollSets[i].dropped = true;
|
||||
allRollSets[i].success = false;
|
||||
allRollSets[i].fail = false;
|
||||
allRollSets[i].matchLabel = '';
|
||||
dropCount--;
|
||||
}
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
allRollSets.sort(compareOrigIdx);
|
||||
}
|
||||
|
||||
// Handle marking new successes/fails
|
||||
if (groupConf.success.on || groupConf.fail.on) {
|
||||
allRollSets.forEach((rs) => {
|
||||
loopCountCheck('mathTokenizer.ts - handling group success/fails');
|
||||
|
||||
if (!rs.dropped && !rs.rerolled) {
|
||||
if (groupConf.success.on && groupConf.success.range.includes(rs.roll)) {
|
||||
rs.success = true;
|
||||
rs.matchLabel = 'S';
|
||||
}
|
||||
if (groupConf.fail.on && groupConf.fail.range.includes(rs.roll)) {
|
||||
rs.fail = true;
|
||||
rs.matchLabel = 'F';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Handle separating the rollSets back out, recalculating the success/fail count, assigning them to the correct mathConf slots
|
||||
executedRollArr.forEach(([rollGroupIdx, executedRoll]) => {
|
||||
// Update flags on executedRoll
|
||||
executedRoll.countSuccessOverride = executedRoll.countSuccessOverride || groupConf.success.on;
|
||||
executedRoll.countFailOverride = executedRoll.countFailOverride || groupConf.fail.on;
|
||||
executedRoll.rollSet = allRollSets.filter((rs) => rs.rollGrpIdx === rollGroupIdx);
|
||||
|
||||
const formattedRoll = formatRoll(executedRoll, modifiers);
|
||||
mathConf[rollGroupIdx] = formattedRoll.solvedStep;
|
||||
countDetails.push(formattedRoll.countDetails);
|
||||
if (modifiers.rollDist) rollDists.push(formattedRoll.rollDistributions);
|
||||
});
|
||||
}
|
||||
|
||||
// Now that mathConf is parsed, send it into the solver
|
||||
loggingEnabled && log(LT.LOG, `Sending mathConf to solver ${JSON.stringify(mathConf)}`);
|
||||
const tempSolved = mathSolver(mathConf);
|
||||
loggingEnabled && log(LT.LOG, `SolvedStep back from mathSolver ${JSON.stringify(tempSolved)}`);
|
||||
|
||||
// Push all of this step's solved data into the temp array
|
||||
return [
|
||||
[
|
||||
{
|
||||
rollTotal: tempSolved.total,
|
||||
rollPreFormat: '',
|
||||
rollPostFormat: '',
|
||||
rollDetails: tempSolved.details,
|
||||
containsCrit: tempSolved.containsCrit,
|
||||
containsFail: tempSolved.containsFail,
|
||||
initConfig: cmd,
|
||||
isComplex: tempSolved.isComplex,
|
||||
},
|
||||
],
|
||||
countDetails,
|
||||
rollDists,
|
||||
];
|
||||
};
|
|
@ -0,0 +1,65 @@
|
|||
import { CountDetails, RollSet } from 'artigen/dice/dice.d.ts';
|
||||
|
||||
import { loopCountCheck } from 'artigen/managers/loopManager.ts';
|
||||
|
||||
export const rollCounter = (rollSet: RollSet[]): CountDetails => {
|
||||
const countDetails: CountDetails = {
|
||||
total: 0,
|
||||
successful: 0,
|
||||
failed: 0,
|
||||
rerolled: 0,
|
||||
dropped: 0,
|
||||
exploded: 0,
|
||||
success: 0,
|
||||
fail: 0,
|
||||
matches: new Map<string, number>(),
|
||||
};
|
||||
|
||||
rollSet.forEach((roll) => {
|
||||
loopCountCheck('counter.ts - summing RollSet into CountDetails');
|
||||
countDetails.total++;
|
||||
if (roll.critHit) countDetails.successful++;
|
||||
if (roll.critFail) countDetails.failed++;
|
||||
if (roll.rerolled) countDetails.rerolled++;
|
||||
if (roll.dropped) countDetails.dropped++;
|
||||
if (roll.exploding) countDetails.exploded++;
|
||||
if (roll.success) countDetails.success++;
|
||||
if (roll.fail) countDetails.fail++;
|
||||
if (roll.matchLabel) countDetails.matches.set(roll.matchLabel, (countDetails.matches.get(roll.matchLabel) ?? 0) + 1);
|
||||
});
|
||||
|
||||
return countDetails;
|
||||
};
|
||||
|
||||
export const reduceCountDetails = (counts: CountDetails[]): CountDetails =>
|
||||
counts.reduce(
|
||||
(acc, cur) => {
|
||||
loopCountCheck('counter.ts - merging array of CountDetails down to single CountDetail');
|
||||
cur.matches.forEach((cnt, label) => {
|
||||
loopCountCheck('counter.ts - merging matches');
|
||||
acc.matches.set(label, (acc.matches.get(label) ?? 0) + cnt);
|
||||
});
|
||||
return {
|
||||
total: acc.total + cur.total,
|
||||
successful: acc.successful + cur.successful,
|
||||
failed: acc.failed + cur.failed,
|
||||
rerolled: acc.rerolled + cur.rerolled,
|
||||
dropped: acc.dropped + cur.dropped,
|
||||
exploded: acc.exploded + cur.exploded,
|
||||
success: acc.success + cur.success,
|
||||
fail: acc.fail + cur.fail,
|
||||
matches: acc.matches,
|
||||
};
|
||||
},
|
||||
{
|
||||
total: 0,
|
||||
successful: 0,
|
||||
failed: 0,
|
||||
rerolled: 0,
|
||||
dropped: 0,
|
||||
exploded: 0,
|
||||
success: 0,
|
||||
fail: 0,
|
||||
matches: new Map<string, number>(),
|
||||
},
|
||||
);
|
|
@ -0,0 +1,35 @@
|
|||
import { CustomDiceShapes, RollConf, RollSet } from 'artigen/dice/dice.d.ts';
|
||||
|
||||
export const flagRoll = (rollConf: RollConf, rollSet: RollSet, customDiceShapes: CustomDiceShapes) => {
|
||||
// If critScore arg is on, check if the roll should be a crit, if its off, check if the roll matches the die size
|
||||
if (rollConf.critScore.on && rollConf.critScore.range.includes(rollSet.roll)) {
|
||||
rollSet.critHit = true;
|
||||
} else if (!rollConf.critScore.on) {
|
||||
rollSet.critHit = rollSet.roll === (rollConf.dPercent.on ? rollConf.dPercent.critVal : 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.includes(rollSet.roll)) {
|
||||
rollSet.critFail = true;
|
||||
} else if (!rollConf.critFail.on) {
|
||||
if (rollConf.type === 'fate') {
|
||||
rollSet.critFail = rollSet.roll === -1;
|
||||
} else if (rollConf.type === 'custom') {
|
||||
rollSet.critFail = rollSet.roll === Math.min(...(customDiceShapes.get(rollConf.customType ?? '') ?? []));
|
||||
} else {
|
||||
rollSet.critFail = rollSet.roll === (rollConf.dPercent.on ? 0 : 1);
|
||||
}
|
||||
}
|
||||
|
||||
// If success arg is on, check if roll should be successful
|
||||
if (rollConf.success.on && rollConf.success.range.includes(rollSet.roll)) {
|
||||
rollSet.success = true;
|
||||
rollSet.matchLabel = 'S';
|
||||
}
|
||||
|
||||
// If fail arg is on, check if roll should be failed
|
||||
if (rollConf.fail.on && rollConf.fail.range.includes(rollSet.roll)) {
|
||||
rollSet.fail = true;
|
||||
rollSet.matchLabel = 'F';
|
||||
}
|
||||
};
|
|
@ -0,0 +1,356 @@
|
|||
import { ButtonStyles, CreateMessage, DiscordenoMessage, EmbedField, MessageComponentTypes } from '@discordeno';
|
||||
import { log, LogTypes as LT } from '@Log4Deno';
|
||||
|
||||
import config from '~config';
|
||||
|
||||
import { ArtigenEmbedNoAttachment, ArtigenEmbedWithAttachment, SolvedRoll } from 'artigen/artigen.d.ts';
|
||||
|
||||
import { CountDetails, RollDistributionMap, RollModifiers } from 'artigen/dice/dice.d.ts';
|
||||
|
||||
import { loggingEnabled } from 'artigen/utils/logFlag.ts';
|
||||
import { basicReducer } from 'artigen/utils/reducers.ts';
|
||||
|
||||
import { failColor, infoColor1, infoColor2 } from 'embeds/colors.ts';
|
||||
|
||||
import { InteractionValueSeparator } from 'events/interactionCreate.ts';
|
||||
|
||||
import utils from 'utils/utils.ts';
|
||||
|
||||
export const rollingEmbed: CreateMessage = {
|
||||
embeds: [
|
||||
{
|
||||
color: infoColor1,
|
||||
title: 'Rolling . . .',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const generateDMFailed = (user: bigint): CreateMessage => ({
|
||||
embeds: [
|
||||
{
|
||||
color: failColor,
|
||||
title: `WARNING: <@${user}> could not be messaged.`,
|
||||
description: 'If this issue persists, make sure direct messages are allowed from this server.',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
export const generateRollError = (errorType: string, errorName: string, errorMsg: string): CreateMessage => ({
|
||||
embeds: [
|
||||
{
|
||||
color: failColor,
|
||||
title: 'Roll command encountered the following error:',
|
||||
fields: [
|
||||
{
|
||||
name: errorType,
|
||||
value: `${errorMsg}\n\nPlease try again. If the error is repeated, please report the issue using the \`${config.prefix}report\` command.`,
|
||||
},
|
||||
],
|
||||
footer: {
|
||||
text: errorName,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
export const generateCountDetailsEmbed = (counts: CountDetails): ArtigenEmbedNoAttachment => {
|
||||
const title = 'Roll Count Details:';
|
||||
const fields: EmbedField[] = [
|
||||
{
|
||||
name: 'Total Rolls:',
|
||||
value: `${counts.total}`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: 'Critically Successful Rolls:',
|
||||
value: `${counts.successful}`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: 'Critically Failed Rolls:',
|
||||
value: `${counts.failed}`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: 'Rerolled Dice:',
|
||||
value: `${counts.rerolled}`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: 'Dropped Dice:',
|
||||
value: `${counts.dropped}`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: 'Exploded Dice:',
|
||||
value: `${counts.exploded}`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: 'Successful Rolls:',
|
||||
value: `${counts.success}`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: 'Failed Rolls:',
|
||||
value: `${counts.fail}`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: 'Matched Roll Labels:',
|
||||
value: `${
|
||||
counts.matches
|
||||
.entries()
|
||||
.toArray()
|
||||
.sort()
|
||||
.map(([label, count]) => `${label}: ${count}`)
|
||||
.join(', ')
|
||||
}`,
|
||||
inline: true,
|
||||
},
|
||||
];
|
||||
|
||||
return {
|
||||
charCount: title.length + fields.map((field) => field.name.length + field.value.length).reduce(basicReducer, 0),
|
||||
embed: {
|
||||
color: infoColor1,
|
||||
title,
|
||||
fields,
|
||||
},
|
||||
hasAttachment: false,
|
||||
};
|
||||
};
|
||||
|
||||
const getDistName = (key: string) => {
|
||||
const [type, size] = key.split('-');
|
||||
switch (type) {
|
||||
case 'fate':
|
||||
return 'Fate dice';
|
||||
case 'cwod':
|
||||
return `CWOD d${size}`;
|
||||
case 'ova':
|
||||
return `OVA d${size}`;
|
||||
case 'custom':
|
||||
return `Custom d${size}`;
|
||||
default:
|
||||
return `d${size}`;
|
||||
}
|
||||
};
|
||||
|
||||
export const generateRollDistsEmbed = (rollDists: RollDistributionMap): ArtigenEmbedNoAttachment | ArtigenEmbedWithAttachment => {
|
||||
const fields = rollDists
|
||||
.entries()
|
||||
.toArray()
|
||||
.map(([key, distArr]) => {
|
||||
const total = distArr.reduce(basicReducer, 0);
|
||||
return {
|
||||
name: `${getDistName(key)} (Total rolls: ${total}):`,
|
||||
value: distArr
|
||||
.map((cnt, dieIdx) => key.startsWith('custom') && cnt === 0 ? '' : `${key.startsWith('fate') ? dieIdx - 1 : dieIdx + 1}: ${cnt} (${((cnt / total) * 100).toFixed(1)}%)`)
|
||||
.filter((x) => x)
|
||||
.join('\n'),
|
||||
inline: true,
|
||||
};
|
||||
});
|
||||
const rollDistTitle = 'Roll Distributions:';
|
||||
|
||||
const totalSize = fields.map((field) => field.name.length + field.value.length).reduce(basicReducer, 0);
|
||||
if (totalSize > 4_000 || fields.length > 25 || fields.some((field) => field.name.length > 256 || field.value.length > 1024)) {
|
||||
const rollDistBlob = new Blob([fields.map((field) => `# ${field.name}\n${field.value}`).join('\n\n') as BlobPart], { type: 'text' });
|
||||
let rollDistErrDesc = 'The roll distribution was omitted from this message as it was over 4,000 characters, ';
|
||||
if (rollDistBlob.size > config.maxFileSize) {
|
||||
rollDistErrDesc +=
|
||||
'and was too large to be attached as the file would be too large for Discord to handle. If you would like to see the roll distribution details, please simplify or send the rolls in multiple messages.';
|
||||
return {
|
||||
charCount: rollDistTitle.length + rollDistErrDesc.length,
|
||||
embed: {
|
||||
color: failColor,
|
||||
title: rollDistTitle,
|
||||
description: rollDistErrDesc,
|
||||
},
|
||||
hasAttachment: false,
|
||||
};
|
||||
} else {
|
||||
rollDistErrDesc += 'and has been attached to a followup message as a formatted `.md` file.';
|
||||
return {
|
||||
charCount: rollDistTitle.length + rollDistErrDesc.length,
|
||||
embed: {
|
||||
color: failColor,
|
||||
title: rollDistTitle,
|
||||
description: rollDistErrDesc,
|
||||
},
|
||||
hasAttachment: true,
|
||||
attachment: {
|
||||
name: 'rollDistributions.md',
|
||||
blob: rollDistBlob,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
charCount: rollDistTitle.length + totalSize,
|
||||
embed: {
|
||||
color: infoColor1,
|
||||
title: rollDistTitle,
|
||||
fields,
|
||||
},
|
||||
hasAttachment: false,
|
||||
};
|
||||
};
|
||||
|
||||
export const generateRollEmbed = (
|
||||
authorId: bigint,
|
||||
returnDetails: SolvedRoll,
|
||||
modifiers: RollModifiers,
|
||||
): ArtigenEmbedNoAttachment | ArtigenEmbedWithAttachment => {
|
||||
if (returnDetails.error) {
|
||||
// Roll had an error, send error embed
|
||||
const errTitle = 'Roll failed:';
|
||||
const errDesc = `${returnDetails.errorMsg}`;
|
||||
const errCode = `Code: ${returnDetails.errorCode}`;
|
||||
|
||||
return {
|
||||
charCount: errTitle.length + errDesc.length + errCode.length,
|
||||
embed: {
|
||||
color: failColor,
|
||||
title: errTitle,
|
||||
description: errDesc,
|
||||
footer: {
|
||||
text: errCode,
|
||||
},
|
||||
},
|
||||
hasAttachment: false,
|
||||
};
|
||||
}
|
||||
|
||||
const line1Details = modifiers.hideRaw ? '' : `<@${authorId}>${returnDetails.line1}\n\n`;
|
||||
if (modifiers.gmRoll) {
|
||||
// Roll is a GM Roll, send this in the pub channel (this funciton will be ran again to get details for the GMs)
|
||||
const desc = `${line1Details}${line1Details ? '\n' : ''}Results have been messaged to the following GMs: ${
|
||||
modifiers.gms
|
||||
.map((gm) => (gm.startsWith('<') ? gm : `<@${gm}>`))
|
||||
.join(' ')
|
||||
}`;
|
||||
|
||||
return {
|
||||
charCount: desc.length,
|
||||
embed: {
|
||||
color: infoColor2,
|
||||
description: desc,
|
||||
},
|
||||
hasAttachment: false,
|
||||
};
|
||||
}
|
||||
|
||||
// Roll is normal, make normal embed
|
||||
const line2Details = returnDetails.line2.split(': ');
|
||||
let details = '';
|
||||
|
||||
if (!modifiers.superNoDetails) {
|
||||
details = `**Details:**\n${modifiers.spoiler}${returnDetails.line3}${modifiers.spoiler}`;
|
||||
loggingEnabled && log(LT.LOG, `${returnDetails.line3} |&| ${details}`);
|
||||
}
|
||||
|
||||
const baseDesc = `${line1Details}**${line2Details.shift()}:**\n${line2Details.join(': ')}`;
|
||||
const fullDesc = `${baseDesc}\n\n${details}`;
|
||||
|
||||
const formattingCount = (fullDesc.match(/(\*\*)|(__)|(~~)|(`)/g) ?? []).length / 2 + (fullDesc.match(/(<@)|(<#)/g) ?? []).length;
|
||||
|
||||
// Embed desc limit is 4096
|
||||
// Discord only formats 200 items per message
|
||||
const fullSize = fullDesc.length + returnDetails.footer.length;
|
||||
if (fullSize < 4_000 && formattingCount <= 200) {
|
||||
// Response is valid size
|
||||
return {
|
||||
charCount: fullSize,
|
||||
embed: {
|
||||
color: infoColor2,
|
||||
description: fullDesc,
|
||||
footer: {
|
||||
text: returnDetails.footer,
|
||||
},
|
||||
},
|
||||
hasAttachment: false,
|
||||
};
|
||||
}
|
||||
|
||||
// Response is too big, collapse it into a .md file and send that instead.
|
||||
const b = new Blob([fullDesc as BlobPart], { type: 'text' });
|
||||
details = `${baseDesc}\n\nDetails have been omitted from this message for ${fullDesc.length < 4_000 ? 'being over 4,000 characters' : 'having over 200 formatted items'}.`;
|
||||
if (b.size > config.maxFileSize) {
|
||||
// blob is too big, don't attach it
|
||||
details +=
|
||||
'\n\nFull details could not be attached as the file would be too large for Discord to handle. If you would like to see the details of rolls, please simplify or send the rolls in multiple messages.';
|
||||
return {
|
||||
charCount: details.length,
|
||||
embed: {
|
||||
color: infoColor2,
|
||||
description: details,
|
||||
},
|
||||
hasAttachment: false,
|
||||
};
|
||||
}
|
||||
|
||||
// blob is small enough, attach it
|
||||
details += '\n\nFull details have been attached to a followup message as a formatted `.md` file for verification purposes.';
|
||||
return {
|
||||
charCount: details.length,
|
||||
embed: {
|
||||
color: infoColor2,
|
||||
description: details,
|
||||
},
|
||||
hasAttachment: true,
|
||||
attachment: {
|
||||
blob: b,
|
||||
name: 'rollDetails.md',
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const webViewCustomId = 'webview';
|
||||
export const disabledStr = 'disabled';
|
||||
export const toggleWebView = (attachmentMessage: DiscordenoMessage, ownerId: string, enableWebView: boolean) => {
|
||||
attachmentMessage
|
||||
.edit({
|
||||
embeds: [
|
||||
{
|
||||
...attachmentMessage.embeds[0],
|
||||
fields: [
|
||||
{
|
||||
name: 'Web View Link:',
|
||||
value: enableWebView
|
||||
? `[Open Web View](${config.api.publicDomain}api/webview?c=${attachmentMessage.channelId}&m=${attachmentMessage.id}#${new Date().getTime()}#${ownerId})`
|
||||
: `Web View is ${disabledStr}.\n- Click the button below to enable Web View and generate a link for this roll.`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: enableWebView ? 'Web View Status:' : 'What is Web View?',
|
||||
value: enableWebView
|
||||
? `For privacy, Web View will automatically time out on this roll <t:${
|
||||
Math.floor(
|
||||
(new Date().getTime() + 1_000 * 60 * 60) / 1000,
|
||||
)
|
||||
}:R>. The link will still show on this message after it has timed out, so clicking on the link after it has been timed out will show an error and remove the link.`
|
||||
: '- Web View is a system for viewing extremely large or complex rolls with full formatting.\n- As anyone with the Web View link can view the roll, Web View is disabled by default for privacy.',
|
||||
inline: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
components: [
|
||||
{
|
||||
type: MessageComponentTypes.ActionRow,
|
||||
components: [
|
||||
{
|
||||
type: MessageComponentTypes.Button,
|
||||
label: enableWebView ? 'Disable Web View' : 'Enable Web View',
|
||||
customId: `${webViewCustomId}${InteractionValueSeparator}${ownerId}${InteractionValueSeparator}${enableWebView ? 'disable' : 'enable'}`,
|
||||
style: ButtonStyles.Secondary,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
.catch((e) => utils.commonLoggers.messageEditError('embeds.ts:304', attachmentMessage, e));
|
||||
};
|
|
@ -0,0 +1,48 @@
|
|||
import { log, LogTypes as LT } from '@Log4Deno';
|
||||
|
||||
import config from '~config';
|
||||
|
||||
import { loopCountCheck } from 'artigen/managers/loopManager.ts';
|
||||
|
||||
import { loggingEnabled } from 'artigen/utils/logFlag.ts';
|
||||
|
||||
// escapeCharacters(str, esc) returns str
|
||||
// escapeCharacters escapes all characters listed in esc
|
||||
export const escapeCharacters = (str: string, esc: string): string => {
|
||||
// Loop thru each esc char one at a time
|
||||
for (const e of esc) {
|
||||
loggingEnabled && log(LT.LOG, `Escaping character ${e} | ${str}, ${esc}`);
|
||||
if (str.includes(e)) {
|
||||
loopCountCheck(`escape.ts - escaping character ${e}`);
|
||||
|
||||
// Create a new regex to look for that char that needs replaced and escape it
|
||||
const tempRgx = new RegExp(`[${e}]`, 'g');
|
||||
str = str.replace(tempRgx, `\\${e}`);
|
||||
}
|
||||
}
|
||||
return str;
|
||||
};
|
||||
|
||||
// escapePrefixPostfix(str) returns str
|
||||
// Escapes all characters that need escaped in a regex string to allow prefix/postfix to be configurable
|
||||
const escapePrefixPostfix = (str: string): string => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
export const cmdSplitRegex = new RegExp(`(${escapePrefixPostfix(config.prefix)})|(${escapePrefixPostfix(config.postfix)})`, 'g');
|
||||
|
||||
// breaks the string on the following: (\*\*) ** for exponents ([+()*/^] for basic algebra (?<![d%])% for breaking on d%%%% dice correctly (?<![rsfop!=<>])- for breaking on - correctly with fate dice) (x\d+(\.\d*)?) x# for variables
|
||||
export const mathSplitRegex = /(\*\*)|([+()*/^]|(?<![d%])%|(?<![rsfop!=<>])-)|([xy]\d+(\.\d*)?)/g;
|
||||
|
||||
// breaks the string on spaces and newlines, but keeps them in the array to allow for recreating the input correctly
|
||||
export const argSpacesSplitRegex = /([ \n]+)/g;
|
||||
|
||||
// Internal is used for recursive text replacement, these will always be the top level as they get replaced with config.prefix/postfix when exiting each level
|
||||
export const openInternal = '\u2045';
|
||||
export const closeInternal = '\u2046';
|
||||
export const internalWrapRegex = new RegExp(`([${openInternal}${closeInternal}])`, 'g');
|
||||
|
||||
// Internal Group is used for marking handled groups
|
||||
export const openInternalGrp = '\u2e20';
|
||||
export const closeInternalGrp = '\u2e21';
|
||||
export const internalGrpWrapRegex = new RegExp(`([${openInternalGrp}${closeInternalGrp}])`, 'g');
|
||||
|
||||
// Marker to look for when repeating a roll AND it was from a SLASH COMMAND alias run command
|
||||
export const withYVarsDash = '\u2043';
|
|
@ -0,0 +1,13 @@
|
|||
import { GroupResultFlags } from 'artigen/dice/dice.d.ts';
|
||||
|
||||
export const applyFlags = (rollDetails: string, flags: GroupResultFlags): string => {
|
||||
if (flags.dropped) {
|
||||
return `~~${rollDetails.replaceAll('~', '')}~~`;
|
||||
} else if (flags.success) {
|
||||
return `S:${rollDetails}`;
|
||||
} else if (flags.failed) {
|
||||
return `F:${rollDetails}`;
|
||||
} else {
|
||||
return rollDetails;
|
||||
}
|
||||
};
|
|
@ -0,0 +1,11 @@
|
|||
type MathFunction = (arg: number) => number;
|
||||
|
||||
export const legalMath: MathFunction[] = [];
|
||||
(Object.getOwnPropertyNames(Math) as (keyof Math)[]).forEach((propName) => {
|
||||
const mathProp = Math[propName];
|
||||
if (typeof mathProp === 'function' && mathProp.length === 1) {
|
||||
legalMath.push(mathProp as MathFunction);
|
||||
}
|
||||
});
|
||||
|
||||
export const legalMathOperators = legalMath.map((oper) => oper.name);
|
|
@ -0,0 +1,3 @@
|
|||
export const loggingEnabled = false;
|
||||
export const loopLoggingEnabled = false;
|
||||
export const showLoopCountDebug = false;
|
|
@ -0,0 +1,74 @@
|
|||
import { log, LogTypes as LT } from '@Log4Deno';
|
||||
|
||||
import config from '~config';
|
||||
|
||||
import { loopCountCheck } from 'artigen/managers/loopManager.ts';
|
||||
|
||||
import { MathConf } from 'artigen/math/math.d.ts';
|
||||
|
||||
import { closeInternal, closeInternalGrp, openInternal, openInternalGrp } from 'artigen/utils/escape.ts';
|
||||
import { loggingEnabled } from 'artigen/utils/logFlag.ts';
|
||||
|
||||
const checkBalance = (
|
||||
conf: MathConf[],
|
||||
openStr: string,
|
||||
closeStr: string,
|
||||
errorType: string,
|
||||
getMatching: boolean,
|
||||
openIdx: number,
|
||||
countLoops = true,
|
||||
): number => {
|
||||
let parenCnt = 0;
|
||||
|
||||
// Verify there are equal numbers of opening and closing parenthesis by adding 1 for opening parens and subtracting 1 for closing parens
|
||||
for (let i = openIdx; i < conf.length; i++) {
|
||||
countLoops &&
|
||||
loopCountCheck(`parenBalance.ts - ${getMatching ? 'Looking for matching' : 'Checking'} ${openStr}/${closeStr}${getMatching ? '' : ' balance'}`);
|
||||
loggingEnabled &&
|
||||
log(
|
||||
LT.LOG,
|
||||
`${getMatching ? 'Looking for matching' : 'Checking'} ${openStr}/${closeStr} ${getMatching ? '' : 'balance '}on ${
|
||||
JSON.stringify(
|
||||
conf,
|
||||
)
|
||||
} | at ${JSON.stringify(conf[i])}`,
|
||||
);
|
||||
if (conf[i] === openStr) {
|
||||
parenCnt++;
|
||||
} else if (conf[i] === closeStr) {
|
||||
parenCnt--;
|
||||
}
|
||||
|
||||
// If parenCnt ever goes below 0, that means too many closing paren appeared before opening parens
|
||||
if (parenCnt < 0) {
|
||||
throw new Error(`Unbalanced${errorType}`);
|
||||
}
|
||||
|
||||
// When parenCnt reaches 0 again, we will have found the matching closing parenthesis and can safely exit the for loop
|
||||
if (getMatching && parenCnt === 0) {
|
||||
loggingEnabled && log(LT.LOG, `Matching ${openStr}/${closeStr} found at "${i}" | ${JSON.stringify(conf[i])}`);
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
// If the parenCnt is not 0, then we do not have balanced parens and need to error out now
|
||||
// If getMatching flag is set and we have exited the loop, we did not find a matching paren
|
||||
if (parenCnt !== 0 || getMatching) {
|
||||
throw new Error(`Unbalanced${errorType}`);
|
||||
}
|
||||
|
||||
// getMatching flag not set, this value is unused
|
||||
return 0;
|
||||
};
|
||||
|
||||
// assertXBalance verifies the entire conf has balanced X
|
||||
export const assertGroupBalance = (conf: MathConf[]) => checkBalance(conf, '{', '}', 'Group', false, 0);
|
||||
export const assertParenBalance = (conf: MathConf[]) => checkBalance(conf, '(', ')', 'Paren', false, 0);
|
||||
export const assertPrePostBalance = (conf: MathConf[], countLoops = true) => checkBalance(conf, config.prefix, config.postfix, 'PrefixPostfix', false, 0, countLoops);
|
||||
|
||||
// getMatchingXIdx gets the matching X, also partially verifies the conf has balanced X
|
||||
export const getMatchingGroupIdx = (conf: MathConf[], openIdx: number): number => checkBalance(conf, '{', '}', 'Group', true, openIdx);
|
||||
export const getMatchingInternalIdx = (conf: MathConf[], openIdx: number): number => checkBalance(conf, openInternal, closeInternal, 'Internal', true, openIdx);
|
||||
export const getMatchingInternalGrpIdx = (conf: MathConf[], openIdx: number): number => checkBalance(conf, openInternalGrp, closeInternalGrp, 'InternalGrp', true, openIdx);
|
||||
export const getMatchingParenIdx = (conf: MathConf[], openIdx: number): number => checkBalance(conf, '(', ')', 'Paren', true, openIdx);
|
||||
export const getMatchingPostfixIdx = (conf: MathConf[], openIdx: number, countLoops = true): number => checkBalance(conf, config.prefix, config.postfix, 'PrefixPostfix', true, openIdx, countLoops);
|
|
@ -0,0 +1,26 @@
|
|||
import { log, LogTypes as LT } from '@Log4Deno';
|
||||
|
||||
import { RollType } from 'artigen/dice/dice.d.ts';
|
||||
|
||||
import { getLoopCount, loopCountCheck } from 'artigen/managers/loopManager.ts';
|
||||
|
||||
import { loggingEnabled } from 'artigen/utils/logFlag.ts';
|
||||
|
||||
// Add tNum to range
|
||||
export const addToRange = (tSep: string, range: Array<number>, tNum: number) => {
|
||||
loggingEnabled && log(LT.LOG, `${getLoopCount()} addToRange on ${tSep} attempting to add: ${tNum}`);
|
||||
!range.includes(tNum) && range.push(tNum);
|
||||
};
|
||||
|
||||
const internalAddMultipleToRange = (tSep: string, range: Array<number>, start: number, end: number) => {
|
||||
for (let i = start; i <= end; i++) {
|
||||
loopCountCheck(`rangeAdder.ts - ${tSep} range adder`);
|
||||
addToRange(tSep, range, i);
|
||||
}
|
||||
};
|
||||
|
||||
// Add numbers less than or equal to tNum to range
|
||||
export const ltAddToRange = (tSep: string, range: Array<number>, tNum: number, rollType: RollType) => internalAddMultipleToRange(tSep, range, rollType === 'fate' ? -1 : 0, tNum);
|
||||
|
||||
// Add numbers greater than or equal to tNum to range
|
||||
export const gtrAddToRange = (tSep: string, range: Array<number>, tNum: number, dieSize: number) => internalAddMultipleToRange(tSep, range, tNum, dieSize);
|
|
@ -0,0 +1 @@
|
|||
export const basicReducer = (prev: number, cur: number) => prev + cur;
|
|
@ -0,0 +1,42 @@
|
|||
import { RollDistributionMap, RollSet, RollType } from 'artigen/dice/dice.d.ts';
|
||||
|
||||
import { loopCountCheck } from 'artigen/managers/loopManager.ts';
|
||||
|
||||
// Used to generate consistent keys for rollDistributions
|
||||
export const rollDistKey = (type: RollType, size: number) => `${type}-${size}`;
|
||||
|
||||
// Converts a RollSet into a RollDistMap
|
||||
export const createRollDistMap = (rollSet: RollSet[]): RollDistributionMap => {
|
||||
const rollDistMap = new Map<string, number[]>();
|
||||
|
||||
rollSet.forEach((roll) => {
|
||||
loopCountCheck('rollDist.ts - convert RollSet into RollDist');
|
||||
const tempArr: number[] = rollDistMap.get(rollDistKey(roll.type, roll.size)) ?? new Array<number>(roll.type === 'fate' ? roll.size + 2 : roll.size).fill(0);
|
||||
tempArr[roll.type === 'fate' ? roll.roll + 1 : roll.roll - 1]++;
|
||||
rollDistMap.set(rollDistKey(roll.type, roll.size), tempArr);
|
||||
});
|
||||
|
||||
return rollDistMap;
|
||||
};
|
||||
|
||||
// Collapses an array of RollDistMaps into a single RollDistMap
|
||||
export const reduceRollDistMaps = (rollDistArr: RollDistributionMap[]): RollDistributionMap =>
|
||||
rollDistArr.reduce((acc, cur) => {
|
||||
loopCountCheck('rollDist.ts - merge array of RollDists into single RollDist');
|
||||
|
||||
cur
|
||||
.entries()
|
||||
.toArray()
|
||||
.forEach(([key, value]) => {
|
||||
loopCountCheck('rollDist.ts - doing the merge on each item of current');
|
||||
|
||||
const tempArr = acc.get(key) ?? new Array<number>(value.length).fill(0);
|
||||
for (let i = 0; i < tempArr.length; i++) {
|
||||
loopCountCheck('rollDist.ts - doing the merge');
|
||||
tempArr[i] += value[i];
|
||||
}
|
||||
|
||||
acc.set(key, tempArr);
|
||||
});
|
||||
return acc;
|
||||
}, new Map<string, number[]>());
|
|
@ -0,0 +1,25 @@
|
|||
import { log, LogTypes as LT } from '@Log4Deno';
|
||||
|
||||
import { RollConf, RollSet } from 'artigen/dice/dice.d.ts';
|
||||
|
||||
import { getLoopCount, loopCountCheck } from 'artigen/managers/loopManager.ts';
|
||||
|
||||
import { loggingEnabled } from 'artigen/utils/logFlag.ts';
|
||||
|
||||
// Can either count or sum each die
|
||||
export const generateRollVals = (rollConf: RollConf, rollSet: RollSet[], rollStr: string, count: boolean): Array<number> => {
|
||||
const rollVals = new Array(rollConf.dieSize).fill(0);
|
||||
|
||||
// Count up all rolls
|
||||
for (const ovaRoll of rollSet) {
|
||||
loopCountCheck('rollValCounter.ts - counting roll vals');
|
||||
|
||||
loggingEnabled && log(LT.LOG, `${getLoopCount()} Handling ${rollConf.type} ${rollStr} | incrementing rollVals for ${JSON.stringify(ovaRoll)}`);
|
||||
if (!ovaRoll.dropped && !ovaRoll.rerolled) {
|
||||
rollVals[ovaRoll.roll - 1] += count ? 1 : ovaRoll.roll;
|
||||
}
|
||||
}
|
||||
|
||||
loggingEnabled && log(LT.LOG, `${getLoopCount()} Handling ${rollConf.type} ${rollStr} | rollVals ${rollVals}`);
|
||||
return rollVals;
|
||||
};
|
|
@ -0,0 +1,60 @@
|
|||
import { ReturnData } from 'artigen/artigen.d.ts';
|
||||
|
||||
import { RollSet } from 'artigen/dice/dice.d.ts';
|
||||
|
||||
const internalCompareRolls = (a: RollSet, b: RollSet, dir: 1 | -1): number => {
|
||||
if (a.roll < b.roll) {
|
||||
return -1 * dir;
|
||||
}
|
||||
if (a.roll > b.roll) {
|
||||
return 1 * dir;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
// compareRolls(a, b) returns -1|0|1
|
||||
// compareRolls is used to order an array of RollSets by RollSet.roll
|
||||
export const compareRolls = (a: RollSet, b: RollSet): number => internalCompareRolls(a, b, 1);
|
||||
|
||||
// compareRolls(a, b) returns -1|0|1
|
||||
// compareRolls is used to order an array of RollSets by RollSet.roll reversed
|
||||
export const compareRollsReverse = (a: RollSet, b: RollSet): number => internalCompareRolls(a, b, -1);
|
||||
|
||||
const internalCompareTotalRolls = (a: ReturnData, b: ReturnData, dir: 1 | -1): number => {
|
||||
if (a.rollTotal < b.rollTotal) {
|
||||
return -1 * dir;
|
||||
}
|
||||
if (a.rollTotal > b.rollTotal) {
|
||||
return 1 * dir;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
// compareTotalRolls(a, b) returns -1|0|1
|
||||
// compareTotalRolls is used to order an array of RollSets by RollSet.roll
|
||||
export const compareTotalRolls = (a: ReturnData, b: ReturnData): number => internalCompareTotalRolls(a, b, 1);
|
||||
|
||||
// compareTotalRollsReverse(a, b) returns 1|0|-1
|
||||
// compareTotalRollsReverse is used to order an array of RollSets by RollSet.roll reversed
|
||||
export const compareTotalRollsReverse = (a: ReturnData, b: ReturnData): number => internalCompareTotalRolls(a, b, -1);
|
||||
|
||||
// compareRolls(a, b) returns -1|0|1
|
||||
// compareRolls is used to order an array of RollSet or ReturnData by X.origIdx
|
||||
export const compareOrigIdx = (a: RollSet | ReturnData, b: RollSet | ReturnData): number => {
|
||||
if ((a.origIdx ?? 0) < (b.origIdx ?? 0)) {
|
||||
return -1;
|
||||
}
|
||||
if ((a.origIdx ?? 0) > (b.origIdx ?? 0)) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
// Sort yVars by their name
|
||||
export const sortYVars = (a: string, b: string) => {
|
||||
if (a.length < b.length) return -1;
|
||||
if (a.length > b.length) return 1;
|
||||
if (a < b) return -1;
|
||||
if (a > b) return 1;
|
||||
return 0;
|
||||
};
|
|
@ -0,0 +1,139 @@
|
|||
import { log, LogTypes as LT } from '@Log4Deno';
|
||||
|
||||
import config from '~config';
|
||||
|
||||
export const translateError = (solverError: Error): [string, string] => {
|
||||
// Welp, the unthinkable happened, we hit an error
|
||||
|
||||
// Split on _ for the error messages that have more info than just their name
|
||||
const errorSplits = solverError.message.split('_');
|
||||
const errorName = errorSplits.shift();
|
||||
const errorDetails = errorSplits.join('_');
|
||||
|
||||
let errorMsg = '';
|
||||
|
||||
// Translate the errorName to a specific errorMsg
|
||||
switch (errorName) {
|
||||
case 'WholeDieCountSizeOnly':
|
||||
errorMsg = 'Error: Die Size and Die Count must be positive whole numbers';
|
||||
break;
|
||||
case 'YouNeedAD':
|
||||
errorMsg = `Error: Attempted to parse \`${errorDetails}\` as a dice configuration, `;
|
||||
if (errorDetails.includes('d')) {
|
||||
errorMsg += '`d` was found, but the die size and/or count were missing or zero when they should be a positive whole number';
|
||||
} else {
|
||||
errorMsg += '`d` was not found in the dice config for specifying die size and/or count';
|
||||
}
|
||||
break;
|
||||
case 'CannotParseDieCount':
|
||||
errorMsg = `Formatting Error: Cannot parse \`${errorDetails}\` as a number`;
|
||||
break;
|
||||
case 'DoubleSeparator':
|
||||
errorMsg = `Formatting Error: \`${errorDetails}\` should only be specified once per roll, remove all but one and repeat roll`;
|
||||
break;
|
||||
case 'FormattingError':
|
||||
errorMsg = 'Formatting Error: ';
|
||||
switch (errorDetails) {
|
||||
case 'dk':
|
||||
errorMsg += 'Cannot use Keep and Drop at the same time, remove all but one and repeat roll';
|
||||
break;
|
||||
case 'mtsf':
|
||||
errorMsg += 'Cannot use Match with CWOD Dice, or the Success or Fail options, remove all but one and repeat roll';
|
||||
break;
|
||||
default:
|
||||
errorMsg += `Unhandled - ${errorDetails}`;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 'NoMaxWithDash':
|
||||
errorMsg = 'Formatting Error: CritScore range specified without a maximum, remove - or add maximum to correct';
|
||||
break;
|
||||
case 'UnknownOperation':
|
||||
errorMsg = `Error: Unknown Operation ${errorDetails}`;
|
||||
if (errorDetails === '-') {
|
||||
errorMsg += '\nNote: Negative numbers are not supported';
|
||||
} else if (errorDetails === ' ') {
|
||||
errorMsg += `\nNote: Every roll must be closed by ${config.postfix}`;
|
||||
}
|
||||
break;
|
||||
case 'NoZerosAllowed':
|
||||
errorMsg = 'Formatting Error: ';
|
||||
switch (errorDetails) {
|
||||
case 'base':
|
||||
errorMsg += 'Die Size and Die Count';
|
||||
break;
|
||||
case 'drop':
|
||||
errorMsg += 'Drop (`d` or `dl`)';
|
||||
break;
|
||||
case 'keep':
|
||||
errorMsg += 'Keep (`k` or `kh`)';
|
||||
break;
|
||||
case 'dropHigh':
|
||||
errorMsg += 'Drop Highest (`dh`)';
|
||||
break;
|
||||
case 'keepLow':
|
||||
errorMsg += 'Keep Lowest (`kl`)';
|
||||
break;
|
||||
case 'reroll':
|
||||
errorMsg += 'Reroll (`r`)';
|
||||
break;
|
||||
case 'critScore':
|
||||
errorMsg += 'Crit Score (`cs`)';
|
||||
break;
|
||||
case 'critFail':
|
||||
errorMsg += 'Crit Fail (`cf`)';
|
||||
break;
|
||||
default:
|
||||
errorMsg += `Unhandled - ${errorDetails}`;
|
||||
break;
|
||||
}
|
||||
errorMsg += ' cannot be zero';
|
||||
break;
|
||||
case 'NoRerollOnAllSides':
|
||||
errorMsg = 'Error: Cannot reroll all sides of a die, must have at least one side that does not get rerolled';
|
||||
break;
|
||||
case 'CritScoreMinGtrMax':
|
||||
errorMsg = 'Formatting Error: CritScore maximum cannot be greater than minimum, check formatting and flip min/max';
|
||||
break;
|
||||
case 'Invalid string length':
|
||||
case 'MaxLoopsExceeded':
|
||||
errorMsg = 'Error: Roll is too complex or reaches infinity';
|
||||
break;
|
||||
case 'UnbalancedParen':
|
||||
errorMsg = 'Formatting Error: At least one of the equations contains unbalanced `(`/`)`';
|
||||
break;
|
||||
case 'UnbalancedPrefixPostfix':
|
||||
errorMsg = `Formatting Error: At least one of the equations contains unbalanced \`${config.prefix}\`/\`${config.postfix}\``;
|
||||
break;
|
||||
case 'EMDASNotNumber':
|
||||
errorMsg = 'Error: One or more operands is not a number';
|
||||
break;
|
||||
case 'ConfWhat':
|
||||
errorMsg = 'Error: Not all values got processed, please report the command used';
|
||||
break;
|
||||
case 'OperatorWhat':
|
||||
errorMsg = 'Error: Something really broke with the Operator, try again';
|
||||
break;
|
||||
case 'OperandNaN':
|
||||
errorMsg = 'Error: One or more operands reached NaN, check input';
|
||||
break;
|
||||
case 'UndefinedStep':
|
||||
errorMsg = 'Error: Roll became undefined, one or more operands are not a roll or a number, check input';
|
||||
break;
|
||||
case 'IllegalVariable':
|
||||
errorMsg = `Error: \`${errorDetails}\` is not a valid variable`;
|
||||
break;
|
||||
case 'VariableMissingValue':
|
||||
errorMsg = `Error: \`${errorDetails}\` is missing a valid value`;
|
||||
break;
|
||||
case 'TooManyLabels':
|
||||
errorMsg = `Error: ${config.name} can only support a maximum of \`${errorDetails}\` labels when using the dice matching options (\`m\` or \`mt\`)`;
|
||||
break;
|
||||
default:
|
||||
log(LT.ERROR, `Unhandled Parser Error: ${errorName}, ${errorDetails}`);
|
||||
errorMsg = `Unhandled Error: ${solverError.message}\nCheck input and try again, if issue persists, please use \`${config.prefix}report\` to alert the devs of the issue`;
|
||||
break;
|
||||
}
|
||||
|
||||
return [solverError.message, errorMsg];
|
||||
};
|
|
@ -1,316 +0,0 @@
|
|||
import config from '../config.ts';
|
||||
import { CountDetails, SolvedRoll } from './solver/solver.d.ts';
|
||||
import { RollModifiers } from './mod.d.ts';
|
||||
|
||||
export const failColor = 0xe71212;
|
||||
export const warnColor = 0xe38f28;
|
||||
export const successColor = 0x0f8108;
|
||||
export const infoColor1 = 0x313bf9;
|
||||
export const infoColor2 = 0x6805e9;
|
||||
|
||||
export const rollingEmbed = {
|
||||
embeds: [{
|
||||
color: infoColor1,
|
||||
title: 'Rolling . . .',
|
||||
}],
|
||||
};
|
||||
|
||||
export const generatePing = (time: number) => ({
|
||||
embeds: [{
|
||||
color: infoColor1,
|
||||
title: time === -1 ? 'Ping?' : `Pong! Latency is ${time}ms.`,
|
||||
}],
|
||||
});
|
||||
|
||||
export const generateReport = (msg: string) => ({
|
||||
embeds: [{
|
||||
color: infoColor2,
|
||||
title: 'USER REPORT:',
|
||||
description: msg || 'No message',
|
||||
}],
|
||||
});
|
||||
|
||||
export const generateStats = (guildCount: number, channelCount: number, memberCount: number, rollCount: bigint, utilityCount: bigint, rollRate: number, utilityRate: number) => ({
|
||||
embeds: [{
|
||||
color: infoColor2,
|
||||
title: "The Artificer's Statistics:",
|
||||
timestamp: new Date().toISOString(),
|
||||
fields: [
|
||||
{
|
||||
name: 'Guilds:',
|
||||
value: `${guildCount}`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: 'Channels:',
|
||||
value: `${channelCount}`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: 'Active Members:',
|
||||
value: `${memberCount}`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: 'Roll Commands:',
|
||||
value: `${rollCount}
|
||||
(${rollRate.toFixed(2)} per hour)`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: 'Utility Commands:',
|
||||
value: `${utilityCount}
|
||||
(${utilityRate.toFixed(2)} per hour)`,
|
||||
inline: true,
|
||||
},
|
||||
],
|
||||
}],
|
||||
});
|
||||
|
||||
export const generateApiFailed = (args: string) => ({
|
||||
embeds: [{
|
||||
color: failColor,
|
||||
title: `Failed to ${args} API rolls for this guild.`,
|
||||
description: 'If this issue persists, please report this to the developers.',
|
||||
}],
|
||||
});
|
||||
|
||||
export const generateApiStatus = (banned: boolean, active: boolean) => {
|
||||
const apiStatus = active ? 'allowed' : 'blocked from being used';
|
||||
return {
|
||||
embeds: [{
|
||||
color: infoColor1,
|
||||
title: `The Artificer's API is ${config.api.enable ? 'currently enabled' : 'currently disabled'}.`,
|
||||
description: banned ? 'API rolls are banned from being used in this guild.\n\nThis will not be reversed.' : `API rolls are ${apiStatus} in this guild.`,
|
||||
}],
|
||||
};
|
||||
};
|
||||
|
||||
export const generateApiSuccess = (args: string) => ({
|
||||
embeds: [{
|
||||
color: successColor,
|
||||
title: `API rolls have successfully been ${args} for this guild.`,
|
||||
}],
|
||||
});
|
||||
|
||||
export const generateDMFailed = (user: string) => ({
|
||||
embeds: [{
|
||||
color: failColor,
|
||||
title: `WARNING: ${user} could not be messaged.`,
|
||||
description: 'If this issue persists, make sure direct messages are allowed from this server.',
|
||||
}],
|
||||
});
|
||||
|
||||
export const generateApiKeyEmail = (email: string, key: string) => ({
|
||||
content: `<@${config.api.admin}> A USER HAS REQUESTED AN API KEY`,
|
||||
embeds: [{
|
||||
color: infoColor1,
|
||||
fields: [
|
||||
{
|
||||
name: 'Send to:',
|
||||
value: email,
|
||||
},
|
||||
{
|
||||
name: 'Subject:',
|
||||
value: 'Artificer API Key',
|
||||
},
|
||||
{
|
||||
name: 'Body:',
|
||||
value: `Hello Artificer API User,
|
||||
|
||||
Welcome aboard The Artificer's API. You can find full details about the API on the GitHub: https://github.com/Burn-E99/TheArtificer
|
||||
|
||||
Your API Key is: ${key}
|
||||
|
||||
Guard this well, as there is zero tolerance for API abuse.
|
||||
|
||||
Welcome aboard,
|
||||
The Artificer Developer - Ean Milligan`,
|
||||
},
|
||||
],
|
||||
}],
|
||||
});
|
||||
|
||||
export const generateApiDeleteEmail = (email: string, deleteCode: string) => ({
|
||||
content: `<@${config.api.admin}> A USER HAS REQUESTED A DELETE CODE`,
|
||||
embeds: [{
|
||||
color: infoColor1,
|
||||
fields: [
|
||||
{
|
||||
name: 'Send to:',
|
||||
value: email,
|
||||
},
|
||||
{
|
||||
name: 'Subject:',
|
||||
value: 'Artificer API Delete Code',
|
||||
},
|
||||
{
|
||||
name: 'Body:',
|
||||
value: `Hello Artificer API User,
|
||||
|
||||
I am sorry to see you go. If you would like, please respond to this email detailing what I could have done better.
|
||||
|
||||
As requested, here is your delete code: ${deleteCode}
|
||||
|
||||
Sorry to see you go,
|
||||
The Artificer Developer - Ean Milligan`,
|
||||
},
|
||||
],
|
||||
}],
|
||||
});
|
||||
|
||||
export const generateRollError = (errorType: string, errorMsg: string) => ({
|
||||
embeds: [{
|
||||
color: failColor,
|
||||
title: 'Roll command encountered the following error:',
|
||||
fields: [{
|
||||
name: errorType,
|
||||
value: `${errorMsg}\n\nPlease try again. If the error is repeated, please report the issue using the \`${config.prefix}report\` command.`,
|
||||
}],
|
||||
}],
|
||||
});
|
||||
|
||||
export const generateCountDetailsEmbed = (counts: CountDetails) => ({
|
||||
color: infoColor1,
|
||||
title: 'Roll Count Details:',
|
||||
fields: [
|
||||
{
|
||||
name: 'Total Rolls:',
|
||||
value: `${counts.total}`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: 'Successful Rolls:',
|
||||
value: `${counts.successful}`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: 'Failed Rolls:',
|
||||
value: `${counts.failed}`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: 'Rerolled Dice:',
|
||||
value: `${counts.rerolled}`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: 'Dropped Dice:',
|
||||
value: `${counts.dropped}`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: 'Exploded Dice:',
|
||||
value: `${counts.exploded}`,
|
||||
inline: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
export const generateRollEmbed = async (authorId: bigint, returnDetails: SolvedRoll, modifiers: RollModifiers) => {
|
||||
if (returnDetails.error) {
|
||||
// Roll had an error, send error embed
|
||||
return {
|
||||
embed: {
|
||||
color: failColor,
|
||||
title: 'Roll failed:',
|
||||
description: `${returnDetails.errorMsg}`,
|
||||
footer: {
|
||||
text: `Code: ${returnDetails.errorCode}`,
|
||||
},
|
||||
},
|
||||
hasAttachment: false,
|
||||
attachment: {
|
||||
'blob': await new Blob(['' as BlobPart], { 'type': 'text' }),
|
||||
'name': 'rollDetails.txt',
|
||||
},
|
||||
};
|
||||
} else {
|
||||
if (modifiers.gmRoll) {
|
||||
// Roll is a GM Roll, send this in the pub channel (this funciton will be ran again to get details for the GMs)
|
||||
return {
|
||||
embed: {
|
||||
color: infoColor2,
|
||||
description: `<@${authorId}>${returnDetails.line1}
|
||||
|
||||
Results have been messaged to the following GMs: ${modifiers.gms.join(' ')}`,
|
||||
},
|
||||
hasAttachment: false,
|
||||
attachment: {
|
||||
'blob': await new Blob(['' as BlobPart], { 'type': 'text' }),
|
||||
'name': 'rollDetails.txt',
|
||||
},
|
||||
};
|
||||
} else {
|
||||
// Roll is normal, make normal embed
|
||||
const line2Details = returnDetails.line2.split(': ');
|
||||
let details = '';
|
||||
|
||||
if (!modifiers.superNoDetails) {
|
||||
if (modifiers.noDetails) {
|
||||
details = `**Details:**
|
||||
Suppressed by -nd flag`;
|
||||
} else {
|
||||
details = `**Details:**
|
||||
${modifiers.spoiler}${returnDetails.line3}${modifiers.spoiler}`;
|
||||
}
|
||||
}
|
||||
|
||||
const baseDesc = `<@${authorId}>${returnDetails.line1}
|
||||
**${line2Details.shift()}:**
|
||||
${line2Details.join(': ')}`;
|
||||
|
||||
if (baseDesc.length + details.length < 4090) {
|
||||
return {
|
||||
embed: {
|
||||
color: infoColor2,
|
||||
description: `${baseDesc}
|
||||
|
||||
${details}`,
|
||||
},
|
||||
hasAttachment: false,
|
||||
attachment: {
|
||||
'blob': await new Blob(['' as BlobPart], { 'type': 'text' }),
|
||||
'name': 'rollDetails.txt',
|
||||
},
|
||||
};
|
||||
} else {
|
||||
// If its too big, collapse it into a .txt file and send that instead.
|
||||
const b = await new Blob([`${baseDesc}\n\n${details}` as BlobPart], { 'type': 'text' });
|
||||
details = 'Details have been ommitted from this message for being over 2000 characters.';
|
||||
if (b.size > 8388290) {
|
||||
details +=
|
||||
'\n\nFull details could not be attached to this messaged as a \`.txt\` file as the file would be too large for Discord to handle. If you would like to see the details of rolls, please send the rolls in multiple messages instead of bundled into one.';
|
||||
return {
|
||||
embed: {
|
||||
color: infoColor2,
|
||||
description: `${baseDesc}
|
||||
|
||||
${details}`,
|
||||
},
|
||||
hasAttachment: false,
|
||||
attachment: {
|
||||
'blob': await new Blob(['' as BlobPart], { 'type': 'text' }),
|
||||
'name': 'rollDetails.txt',
|
||||
},
|
||||
};
|
||||
} else {
|
||||
details += '\n\nFull details have been attached to this messaged as a \`.txt\` file for verification purposes.';
|
||||
return {
|
||||
embed: {
|
||||
color: infoColor2,
|
||||
description: `${baseDesc}
|
||||
|
||||
${details}`,
|
||||
},
|
||||
hasAttachment: true,
|
||||
attachment: {
|
||||
'blob': b,
|
||||
'name': 'rollDetails.txt',
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
|
@ -1,39 +1,64 @@
|
|||
import { ping } from './ping.ts';
|
||||
import { rip } from './rip.ts';
|
||||
import { rollHelp } from './rollHelp.ts';
|
||||
import { rollDecorators } from './rollDecorators.ts';
|
||||
import { help } from './help.ts';
|
||||
import { info } from './info.ts';
|
||||
import { privacy } from './privacy.ts';
|
||||
import { version } from './version.ts';
|
||||
import { report } from './report.ts';
|
||||
import { stats } from './stats.ts';
|
||||
import { api } from './apiCmd.ts';
|
||||
import { emoji } from './emoji.ts';
|
||||
import { roll } from './roll.ts';
|
||||
import { handleMentions } from './handleMentions.ts';
|
||||
import { audit } from './audit.ts';
|
||||
import { heatmap } from './heatmap.ts';
|
||||
import { optOut } from './optOut.ts';
|
||||
import { optIn } from './optIn.ts';
|
||||
import { upsertSlashCommands } from '@discordeno';
|
||||
|
||||
export default {
|
||||
ping,
|
||||
rip,
|
||||
rollHelp,
|
||||
rollDecorators,
|
||||
import { alias, aliasSC } from 'commands/aliasCmd.ts';
|
||||
import { api } from 'commands/apiCmd.ts';
|
||||
import { audit } from 'commands/audit.ts';
|
||||
import { emoji } from 'commands/emoji.ts';
|
||||
import { handleMentions } from 'commands/handleMentions.ts';
|
||||
import { heatmap, heatmapSC } from 'commands/heatmap.ts';
|
||||
import { help, helpSC } from 'commands/help.ts';
|
||||
import { info, infoSC } from 'commands/info.ts';
|
||||
import { optIn } from 'commands/optIn.ts';
|
||||
import { optOut } from 'commands/optOut.ts';
|
||||
import { ping } from 'commands/ping.ts';
|
||||
import { privacy, privacySC } from 'commands/privacy.ts';
|
||||
import { report, reportSC } from 'commands/report.ts';
|
||||
import { rip, ripSC } from 'commands/rip.ts';
|
||||
import { roll, rollSC } from 'commands/roll.ts';
|
||||
import { rollHelp } from 'commands/rollHelp.ts';
|
||||
import { stats, statsSC } from 'commands/stats.ts';
|
||||
import { toggleInline, toggleInlineSC } from 'commands/toggleInline.ts';
|
||||
import { toggleRepeat, toggleRepeatSC } from 'commands/toggleUnrestrictedRepeat.ts';
|
||||
import { version, versionSC } from 'commands/version.ts';
|
||||
|
||||
export const announceSlashCommands = () => {
|
||||
upsertSlashCommands([aliasSC, heatmapSC, helpSC, infoSC, privacySC, reportSC, ripSC, rollSC, statsSC, toggleInlineSC, toggleRepeatSC, versionSC]);
|
||||
};
|
||||
|
||||
export const commands = {
|
||||
alias,
|
||||
api,
|
||||
audit,
|
||||
emoji,
|
||||
handleMentions,
|
||||
heatmap,
|
||||
help,
|
||||
info,
|
||||
privacy,
|
||||
version,
|
||||
report,
|
||||
stats,
|
||||
api,
|
||||
emoji,
|
||||
roll,
|
||||
handleMentions,
|
||||
audit,
|
||||
heatmap,
|
||||
optOut,
|
||||
optIn,
|
||||
optOut,
|
||||
ping,
|
||||
privacy,
|
||||
report,
|
||||
rip,
|
||||
roll,
|
||||
rollHelp,
|
||||
stats,
|
||||
toggleInline,
|
||||
toggleRepeat,
|
||||
version,
|
||||
};
|
||||
|
||||
export const slashCommandDetails = {
|
||||
aliasSC,
|
||||
heatmapSC,
|
||||
helpSC,
|
||||
infoSC,
|
||||
privacySC,
|
||||
ripSC,
|
||||
reportSC,
|
||||
rollSC,
|
||||
statsSC,
|
||||
toggleInlineSC,
|
||||
toggleRepeatSC,
|
||||
versionSC,
|
||||
};
|
||||
|
|
|
@ -0,0 +1,199 @@
|
|||
import { ApplicationCommandOption, CreateGlobalApplicationCommand, DiscordApplicationCommandOptionTypes, DiscordenoMessage } from '@discordeno';
|
||||
|
||||
import aliasCommands from 'commands/aliasCmd/_index.ts';
|
||||
|
||||
import { generateHelpMessage } from 'commands/helpLibrary/generateHelpMessage.ts';
|
||||
|
||||
import dbClient from 'db/client.ts';
|
||||
import { queries } from 'db/common.ts';
|
||||
|
||||
import { failColor } from 'embeds/colors.ts';
|
||||
|
||||
import { SlashCommandInteractionWithGuildId } from 'src/mod.d.ts';
|
||||
|
||||
import utils from 'utils/utils.ts';
|
||||
|
||||
const aliasNameOption = (action: string, rename = false): ApplicationCommandOption => ({
|
||||
type: DiscordApplicationCommandOptionTypes.String,
|
||||
name: `alias-name${rename ? '-new' : ''}`,
|
||||
description: `The ${rename ? 'new ' : ''}name of the alias${rename ? '' : `you wish to ${action}`}.`,
|
||||
required: true,
|
||||
});
|
||||
|
||||
const rollStringOption = (action: string): ApplicationCommandOption => ({
|
||||
type: DiscordApplicationCommandOptionTypes.String,
|
||||
name: 'roll-string',
|
||||
description: `The the full roll string to ${action}.`,
|
||||
required: true,
|
||||
});
|
||||
|
||||
const verificationCodeOption: ApplicationCommandOption = {
|
||||
type: DiscordApplicationCommandOptionTypes.String,
|
||||
name: 'verification-code',
|
||||
description: 'The four digit confirmation code for deletion. Can be left blank to generate one.',
|
||||
required: false,
|
||||
};
|
||||
|
||||
const aliasOptions = (guildMode: boolean): ApplicationCommandOption[] => [
|
||||
{
|
||||
type: DiscordApplicationCommandOptionTypes.SubCommand,
|
||||
name: 'help',
|
||||
description: `Opens the help library to the ${guildMode ? 'Guild' : 'Personal'} Mode Alias System section.`,
|
||||
},
|
||||
{
|
||||
type: DiscordApplicationCommandOptionTypes.SubCommand,
|
||||
name: 'list-all',
|
||||
description: `List all available aliases ${guildMode ? 'in this Guild' : 'on your account'}.`,
|
||||
},
|
||||
{
|
||||
type: DiscordApplicationCommandOptionTypes.SubCommand,
|
||||
name: 'view',
|
||||
description: `Preview the roll string behind an alias ${guildMode ? 'in this Guild' : 'on your account'}.`,
|
||||
options: [aliasNameOption('view')],
|
||||
},
|
||||
{
|
||||
type: DiscordApplicationCommandOptionTypes.SubCommand,
|
||||
name: 'create',
|
||||
description: `Create a new alias ${guildMode ? 'in this Guild' : 'on your account'}.`,
|
||||
options: [aliasNameOption('create'), rollStringOption('create')],
|
||||
},
|
||||
{
|
||||
type: DiscordApplicationCommandOptionTypes.SubCommand,
|
||||
name: 'replace',
|
||||
description: `Update an alias ${guildMode ? 'in this Guild' : 'on your account'} with a new roll string.`,
|
||||
options: [aliasNameOption('replace'), rollStringOption('replace the old one with')],
|
||||
},
|
||||
{
|
||||
type: DiscordApplicationCommandOptionTypes.SubCommand,
|
||||
name: 'rename',
|
||||
description: `Rename an alias ${guildMode ? 'in this Guild' : 'on your account'}.`,
|
||||
options: [aliasNameOption('rename'), aliasNameOption('rename', true)],
|
||||
},
|
||||
{
|
||||
type: DiscordApplicationCommandOptionTypes.SubCommand,
|
||||
name: 'delete-one',
|
||||
description: `Delete an alias from ${guildMode ? 'this Guild' : 'your account'}.`,
|
||||
options: [aliasNameOption('delete'), verificationCodeOption],
|
||||
},
|
||||
{
|
||||
type: DiscordApplicationCommandOptionTypes.SubCommand,
|
||||
name: 'delete-all',
|
||||
description: `Delete all aliases from ${guildMode ? 'this Guild' : 'your account'}.`,
|
||||
options: [verificationCodeOption],
|
||||
},
|
||||
{
|
||||
type: DiscordApplicationCommandOptionTypes.SubCommand,
|
||||
name: 'copy',
|
||||
description: `Copy an alias from ${guildMode ? 'this Guild' : 'your account'} to ${guildMode ? 'your account' : 'this Guild'}.`,
|
||||
options: [aliasNameOption(`copy to ${guildMode ? 'your personal account' : 'this guild'}`)],
|
||||
},
|
||||
{
|
||||
type: DiscordApplicationCommandOptionTypes.SubCommand,
|
||||
name: 'run',
|
||||
description: `Runs the specified ${guildMode ? 'Guild' : 'Personal'} alias.`,
|
||||
options: [
|
||||
aliasNameOption('run'),
|
||||
{
|
||||
type: DiscordApplicationCommandOptionTypes.String,
|
||||
name: 'y-variables',
|
||||
description: 'A space separated list of numbers. Can be left blank if an alias does not require any.',
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const aliasSC: CreateGlobalApplicationCommand = {
|
||||
name: 'alias',
|
||||
description: 'Custom Roll Alias system, create and use Roll Aliases for easily reusing the same rolls.',
|
||||
options: [
|
||||
{
|
||||
type: DiscordApplicationCommandOptionTypes.SubCommandGroup,
|
||||
name: 'personal',
|
||||
description: 'Manage and run Personal aliases.',
|
||||
options: aliasOptions(false),
|
||||
},
|
||||
{
|
||||
type: DiscordApplicationCommandOptionTypes.SubCommandGroup,
|
||||
name: 'guild',
|
||||
description: 'Manage and run Guild aliases.',
|
||||
options: aliasOptions(true),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const alias = (msgOrInt: DiscordenoMessage | SlashCommandInteractionWithGuildId, argSpaces: string[]) => {
|
||||
// Light telemetry to see how many times a command is being run
|
||||
dbClient.execute(queries.callIncCnt('alias')).catch((e) => utils.commonLoggers.dbError('aliasCmd.ts:125', 'call sproc INC_CNT on', e));
|
||||
|
||||
// argSpaces will come in with a space or \n before every real arg, so extra shifts exist to remove them
|
||||
argSpaces.shift();
|
||||
let aliasArg = (argSpaces.shift() || '').toLowerCase().trim();
|
||||
argSpaces.shift();
|
||||
|
||||
let guildMode = false;
|
||||
if (aliasArg === 'guild') {
|
||||
guildMode = true;
|
||||
aliasArg = (argSpaces.shift() || '').toLowerCase().trim();
|
||||
argSpaces.shift();
|
||||
}
|
||||
|
||||
if (guildMode && BigInt(msgOrInt.guildId) === 0n) {
|
||||
utils.sendOrInteract(msgOrInt, 'aliasCmd.ts:140', {
|
||||
embeds: [
|
||||
{
|
||||
color: failColor,
|
||||
title: 'Guild Aliases can only be modified from within the desired guild.',
|
||||
},
|
||||
],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
switch (aliasArg) {
|
||||
case 'help':
|
||||
case 'h':
|
||||
case '?':
|
||||
case '':
|
||||
utils.sendOrInteract(msgOrInt, 'aliasCmd.ts:156', generateHelpMessage('alias'));
|
||||
break;
|
||||
case 'list':
|
||||
case 'list-all':
|
||||
aliasCommands.list(msgOrInt, guildMode);
|
||||
break;
|
||||
case 'add':
|
||||
case 'create':
|
||||
case 'set':
|
||||
aliasCommands.add(msgOrInt, guildMode, argSpaces);
|
||||
break;
|
||||
case 'update':
|
||||
case 'replace':
|
||||
aliasCommands.update(msgOrInt, guildMode, argSpaces);
|
||||
break;
|
||||
case 'preview':
|
||||
case 'view':
|
||||
aliasCommands.view(msgOrInt, guildMode, argSpaces);
|
||||
break;
|
||||
case 'delete':
|
||||
case 'remove':
|
||||
case 'delete-one':
|
||||
aliasCommands.deleteOne(msgOrInt, guildMode, argSpaces);
|
||||
break;
|
||||
case 'delete-all':
|
||||
case 'remove-all':
|
||||
aliasCommands.deleteAll(msgOrInt, guildMode, argSpaces);
|
||||
break;
|
||||
case 'clone':
|
||||
case 'copy':
|
||||
aliasCommands.clone(msgOrInt, guildMode, argSpaces);
|
||||
break;
|
||||
case 'rename':
|
||||
aliasCommands.rename(msgOrInt, guildMode, argSpaces);
|
||||
break;
|
||||
case 'run':
|
||||
case 'execute':
|
||||
default:
|
||||
aliasCommands.run(msgOrInt, guildMode, aliasArg, argSpaces);
|
||||
break;
|
||||
}
|
||||
};
|
|
@ -0,0 +1,19 @@
|
|||
import { add, update } from 'commands/aliasCmd/aliasAddUpdate.ts';
|
||||
import { clone } from 'commands/aliasCmd/clone.ts';
|
||||
import { deleteAll, deleteOne } from 'commands/aliasCmd/aliasDelete.ts';
|
||||
import { list } from 'commands/aliasCmd/list.ts';
|
||||
import { rename } from 'commands/aliasCmd/rename.ts';
|
||||
import { run } from 'commands/aliasCmd/run.ts';
|
||||
import { view } from 'commands/aliasCmd/view.ts';
|
||||
|
||||
export default {
|
||||
add,
|
||||
clone,
|
||||
deleteAll,
|
||||
deleteOne,
|
||||
list,
|
||||
rename,
|
||||
run,
|
||||
update,
|
||||
view,
|
||||
};
|
|
@ -0,0 +1,411 @@
|
|||
import { DiscordenoMessage, EmbedField, hasGuildPermissions } from '@discordeno';
|
||||
import { log, LogTypes as LT } from '@Log4Deno';
|
||||
|
||||
import config from '~config';
|
||||
|
||||
import { getModifiers } from 'artigen/dice/getModifiers.ts';
|
||||
|
||||
import { TestResults } from 'artigen/managers/manager.d.ts';
|
||||
import { sendRollRequest } from 'artigen/managers/queueManager.ts';
|
||||
|
||||
import { cmdSplitRegex } from 'artigen/utils/escape.ts';
|
||||
import { assertPrePostBalance, getMatchingPostfixIdx } from 'artigen/utils/parenBalance.ts';
|
||||
import { sortYVars } from 'artigen/utils/sortFuncs.ts';
|
||||
|
||||
import { ReservedWords } from 'commands/aliasCmd/reservedWords.ts';
|
||||
|
||||
import dbClient from 'db/client.ts';
|
||||
|
||||
import { generateAliasError } from 'embeds/alias.ts';
|
||||
import { failColor, infoColor1, successColor } from 'embeds/colors.ts';
|
||||
|
||||
import { SlashCommandInteractionWithGuildId } from 'src/mod.d.ts';
|
||||
|
||||
import utils from 'utils/utils.ts';
|
||||
|
||||
interface QueryShape {
|
||||
aliasName: string;
|
||||
}
|
||||
|
||||
const handleAddUpdate = async (
|
||||
msgOrInt: DiscordenoMessage | SlashCommandInteractionWithGuildId,
|
||||
guildMode: boolean,
|
||||
argSpaces: string[],
|
||||
replaceAlias: boolean,
|
||||
) => {
|
||||
if (guildMode && !(await hasGuildPermissions(BigInt(msgOrInt.guildId), utils.getAuthorIdFromMessageOrInteraction(msgOrInt), ['ADMINISTRATOR']))) {
|
||||
utils.sendOrInteract(msgOrInt, 'aliasAddUpdate.ts:43', {
|
||||
embeds: [
|
||||
{
|
||||
color: failColor,
|
||||
title: `Error: Only Guild Owners and Admins can add/update guild aliases`,
|
||||
},
|
||||
],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const aliasName = (argSpaces.shift() || '').trim();
|
||||
argSpaces.shift();
|
||||
|
||||
if (aliasName.length > config.limits.alias.maxNameLength) {
|
||||
utils.sendOrInteract(msgOrInt, 'aliasAddUpdate.ts:59', {
|
||||
embeds: [
|
||||
{
|
||||
color: failColor,
|
||||
title: 'Error: Alias Name is too long',
|
||||
description:
|
||||
`\`${aliasName}\` (\`${aliasName.length}\` characters) is longer than the allowed max length of \`${config.limits.alias.maxNameLength}\` characters. Please choose a shorter alias name.`,
|
||||
},
|
||||
],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (ReservedWords.includes(aliasName?.toLowerCase())) {
|
||||
utils.sendOrInteract(msgOrInt, 'aliasAddUpdate.ts:74', {
|
||||
embeds: [
|
||||
{
|
||||
color: failColor,
|
||||
title: `Error: \`${aliasName}\` is a reserved word`,
|
||||
description: `Please choose a different name for this alias.
|
||||
|
||||
You cannot use any of the following reserved words: \`${ReservedWords.join('`, `')}\`.`,
|
||||
},
|
||||
],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let errorOut = false;
|
||||
const query: QueryShape[] = await dbClient
|
||||
.query(
|
||||
`SELECT aliasName FROM aliases WHERE guildid = ? AND userid = ? AND aliasName = ?`,
|
||||
guildMode ? [BigInt(msgOrInt.guildId), 0n, aliasName.toLowerCase()] : [0n, utils.getAuthorIdFromMessageOrInteraction(msgOrInt), aliasName.toLowerCase()],
|
||||
)
|
||||
.catch((e0) => {
|
||||
utils.commonLoggers.dbError('add.ts:44', 'query', e0);
|
||||
utils.sendOrInteract(
|
||||
msgOrInt,
|
||||
'aliasAddUpdate.ts:97',
|
||||
generateAliasError(
|
||||
'DB Query Failed.',
|
||||
`add-q0-${guildMode ? 't' : 'f'}-${aliasName}-${guildMode ? BigInt(msgOrInt.guildId) : utils.getAuthorIdFromMessageOrInteraction(msgOrInt)}`,
|
||||
),
|
||||
);
|
||||
errorOut = true;
|
||||
});
|
||||
if (errorOut) return;
|
||||
|
||||
if (!replaceAlias && query.length) {
|
||||
utils.sendOrInteract(msgOrInt, 'aliasAddUpdate.ts:109', {
|
||||
embeds: [
|
||||
{
|
||||
color: failColor,
|
||||
title: `Error: \`${aliasName}\` already exists as a ${guildMode ? 'guild' : 'personal'} alias`,
|
||||
description: 'Please choose a different name for this alias.',
|
||||
},
|
||||
],
|
||||
});
|
||||
return;
|
||||
} else if (replaceAlias && !query.length) {
|
||||
utils.sendOrInteract(msgOrInt, 'aliasAddUpdate.ts:121', {
|
||||
embeds: [
|
||||
{
|
||||
color: failColor,
|
||||
title: `Error: \`${aliasName}\` does not exist as a ${guildMode ? 'guild' : 'personal'} alias`,
|
||||
description: `If you are trying to create a new ${guildMode ? 'guild' : 'personal'} alias, please run the following command:
|
||||
\`${config.prefix}ra ${guildMode ? 'guild ' : ''}add\` followed by the desired alias name and roll string.
|
||||
|
||||
If you are trying to update an existing alias, but forgot the name, please run the following command to view all your ${guildMode ? 'guild ' : ''}aliases:
|
||||
\`${config.prefix}ra ${guildMode ? 'guild ' : ''}list\``,
|
||||
},
|
||||
],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const rawRollStr = argSpaces.join('').trim();
|
||||
const newMsg: DiscordenoMessage | void = await utils.sendOrInteract(
|
||||
msgOrInt,
|
||||
'aliasAddUpdate.ts:139',
|
||||
{
|
||||
embeds: [
|
||||
{
|
||||
color: infoColor1,
|
||||
title: 'Please wait, testing your roll string . . .',
|
||||
description: `The following roll string is being tested. Once the verdict of your roll has been determined, this message will be updated.
|
||||
|
||||
\`${rawRollStr}\``,
|
||||
},
|
||||
],
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
||||
if (!newMsg) {
|
||||
log(LT.ERROR, `My message didn't send! ${msgOrInt}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const [modifiers, remainingArgs] = getModifiers(argSpaces);
|
||||
const failedRollMsg = `The provided roll string (listed below) encountered an error. Please try this roll outside the roll alias system and resolve the error before trying again.
|
||||
|
||||
\`${rawRollStr}\`${rawRollStr.length > 1_700 ? ' (trimmed to 2,000 characters to fit in the error message)' : ''}`.slice(0, 2_000);
|
||||
|
||||
if (!modifiers.valid) {
|
||||
newMsg
|
||||
.edit({
|
||||
embeds: [
|
||||
{
|
||||
color: failColor,
|
||||
title: 'Roll failed',
|
||||
description: failedRollMsg,
|
||||
fields: [
|
||||
{
|
||||
name: 'Error Details:',
|
||||
value: modifiers.error.message,
|
||||
},
|
||||
],
|
||||
footer: {
|
||||
text: modifiers.error.name,
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
.catch((e: Error) => utils.commonLoggers.messageEditError('add.ts:116', newMsg, e));
|
||||
return;
|
||||
}
|
||||
|
||||
const rollCmd = remainingArgs.join('');
|
||||
const testCmdConf = rollCmd
|
||||
.toLowerCase()
|
||||
.split(cmdSplitRegex)
|
||||
.filter((x) => x);
|
||||
try {
|
||||
assertPrePostBalance(testCmdConf, false);
|
||||
let openIdx = testCmdConf.indexOf(config.prefix);
|
||||
while (openIdx !== -1) {
|
||||
const closeIdx = getMatchingPostfixIdx(testCmdConf, openIdx, false);
|
||||
const possibleYVars = testCmdConf
|
||||
.slice(openIdx + 1, closeIdx)
|
||||
.join('')
|
||||
.split(/(y\d+(\.\d*)?)/g)
|
||||
.filter((y) => y && y.startsWith('y'));
|
||||
for (const yVar of possibleYVars) {
|
||||
if (yVar.includes('.')) {
|
||||
newMsg
|
||||
.edit({
|
||||
embeds: [
|
||||
{
|
||||
color: failColor,
|
||||
title: 'Roll failed',
|
||||
description: failedRollMsg,
|
||||
fields: [
|
||||
{
|
||||
name: 'Error Details:',
|
||||
value: `yVars cannot have decimals`,
|
||||
},
|
||||
],
|
||||
footer: {
|
||||
text: 'yVarDecimal',
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
.catch((e: Error) => utils.commonLoggers.messageEditError('add.ts:163', newMsg, e));
|
||||
return;
|
||||
}
|
||||
if (!modifiers.yVars.has(yVar)) {
|
||||
modifiers.yVars.set(yVar, Math.ceil(Math.random() * 20));
|
||||
}
|
||||
}
|
||||
openIdx = testCmdConf.indexOf(config.prefix, closeIdx);
|
||||
}
|
||||
} catch (e) {
|
||||
const err = e as Error;
|
||||
newMsg
|
||||
.edit({
|
||||
embeds: [
|
||||
{
|
||||
color: failColor,
|
||||
title: 'Roll failed',
|
||||
description: failedRollMsg,
|
||||
fields: [
|
||||
{
|
||||
name: 'Error Details:',
|
||||
value: `Failed to find yVars, requested rollStr likely has unbalanced \`${config.prefix}\`/\`${config.postfix}\``,
|
||||
},
|
||||
{
|
||||
name: 'Raw Error:',
|
||||
value: `${err.name}: ${err.message}`,
|
||||
},
|
||||
],
|
||||
footer: {
|
||||
text: 'caughtErrYVarUnbalanced',
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
.catch((e: Error) => utils.commonLoggers.messageEditError('add.ts:191', newMsg, e));
|
||||
return;
|
||||
}
|
||||
|
||||
let i = 0;
|
||||
while (i < modifiers.yVars.size) {
|
||||
if (!modifiers.yVars.has(`y${i}`)) {
|
||||
modifiers.yVars.set(`y${i}`, 0);
|
||||
}
|
||||
i++;
|
||||
}
|
||||
|
||||
const rollStrVerdict = await new Promise<TestResults>((resolve) => {
|
||||
sendRollRequest({
|
||||
apiRoll: false,
|
||||
ddRoll: false,
|
||||
testRoll: true,
|
||||
resolve,
|
||||
rollCmd,
|
||||
modifiers,
|
||||
originalCommand: rawRollStr,
|
||||
});
|
||||
});
|
||||
|
||||
if (rollStrVerdict.error) {
|
||||
const errorFields: EmbedField[] = [
|
||||
{
|
||||
name: 'Error Details:',
|
||||
value: rollStrVerdict.errorMsg,
|
||||
},
|
||||
];
|
||||
if (modifiers.yVars.size) {
|
||||
errorFields.push({
|
||||
name: 'The following YVars were used in testing:',
|
||||
value: modifiers.yVars
|
||||
.entries()
|
||||
.toArray()
|
||||
.sort((a, b) => sortYVars(a[0], b[0]))
|
||||
.map(([yVar, value]) => `\`${yVar}\`: \`${value}\``)
|
||||
.join('\n'),
|
||||
});
|
||||
}
|
||||
newMsg
|
||||
.edit({
|
||||
embeds: [
|
||||
{
|
||||
color: failColor,
|
||||
title: 'Roll failed',
|
||||
description: failedRollMsg,
|
||||
fields: errorFields,
|
||||
footer: {
|
||||
text: rollStrVerdict.errorCode,
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
.catch((e: Error) => utils.commonLoggers.messageEditError('add.ts:153', newMsg, e));
|
||||
return;
|
||||
}
|
||||
|
||||
if (replaceAlias) {
|
||||
await dbClient
|
||||
.execute('UPDATE aliases SET rollStr = ?, yVarCnt = ? WHERE guildid = ? AND userid = ? AND aliasName = ?', [
|
||||
rawRollStr,
|
||||
modifiers.yVars.size,
|
||||
guildMode ? BigInt(msgOrInt.guildId) : 0n,
|
||||
guildMode ? 0n : utils.getAuthorIdFromMessageOrInteraction(msgOrInt),
|
||||
aliasName.toLowerCase(),
|
||||
])
|
||||
.catch((e0) => {
|
||||
utils.commonLoggers.dbError('add.ts:169', 'update', e0);
|
||||
newMsg
|
||||
.edit(
|
||||
generateAliasError(
|
||||
'DB Update Failed.',
|
||||
`add-q1-${guildMode ? 't' : 'f'}-${aliasName}-${guildMode ? BigInt(msgOrInt.guildId) : utils.getAuthorIdFromMessageOrInteraction(msgOrInt)}`,
|
||||
),
|
||||
)
|
||||
.catch((e: Error) => utils.commonLoggers.messageSendError('add.ts:170', msgOrInt, e));
|
||||
errorOut = true;
|
||||
});
|
||||
} else {
|
||||
const currentAliases: QueryShape[] = await dbClient
|
||||
.query(
|
||||
'SELECT aliasName FROM aliases WHERE guildid = ? AND userid = ?',
|
||||
guildMode ? [BigInt(msgOrInt.guildId), 0n] : [0n, utils.getAuthorIdFromMessageOrInteraction(msgOrInt)],
|
||||
)
|
||||
.catch((e0) => {
|
||||
utils.commonLoggers.dbError('add.ts:266', 'get count', e0);
|
||||
newMsg
|
||||
.edit(
|
||||
generateAliasError(
|
||||
'DB Query Failed.',
|
||||
`add-q2-${guildMode ? 't' : 'f'}-${guildMode ? BigInt(msgOrInt.guildId) : utils.getAuthorIdFromMessageOrInteraction(msgOrInt)}`,
|
||||
),
|
||||
)
|
||||
.catch((e: Error) => utils.commonLoggers.messageSendError('add.ts:269', msgOrInt, e));
|
||||
errorOut = true;
|
||||
});
|
||||
if (errorOut) return;
|
||||
|
||||
if (currentAliases.length < (guildMode ? config.limits.alias.free.guild : config.limits.alias.free.user)) {
|
||||
await dbClient
|
||||
.execute('INSERT INTO aliases(guildid,userid,aliasName,rollStr,yVarCnt,premium) values(?,?,?,?,?,?)', [
|
||||
guildMode ? BigInt(msgOrInt.guildId) : 0n,
|
||||
guildMode ? 0n : utils.getAuthorIdFromMessageOrInteraction(msgOrInt),
|
||||
aliasName.toLowerCase(),
|
||||
rawRollStr,
|
||||
modifiers.yVars.size,
|
||||
0,
|
||||
])
|
||||
.catch((e0) => {
|
||||
utils.commonLoggers.dbError('add.ts:169', 'insert into', e0);
|
||||
newMsg
|
||||
.edit(
|
||||
generateAliasError(
|
||||
'DB Insert Failed.',
|
||||
`add-q3-${guildMode ? 't' : 'f'}-${aliasName}-${guildMode ? BigInt(msgOrInt.guildId) : utils.getAuthorIdFromMessageOrInteraction(msgOrInt)}`,
|
||||
),
|
||||
)
|
||||
.catch((e: Error) => utils.commonLoggers.messageSendError('add.ts:187', msgOrInt, e));
|
||||
errorOut = true;
|
||||
});
|
||||
} else {
|
||||
newMsg
|
||||
.edit({
|
||||
embeds: [
|
||||
{
|
||||
color: failColor,
|
||||
title: `Over ${guildMode ? 'guild' : 'personal'} Alias Limit`,
|
||||
description: `Cannot add another alias as this account already has \`${currentAliases.length}\` aliases saved.
|
||||
|
||||
The current limits imposed on the Alias System are \`${config.limits.alias.free.guild}\` guild aliases and \`${config.limits.alias.free.user}\` account aliases.
|
||||
|
||||
If you need this limit raised, please join the [support server](${config.links.supportServer})`,
|
||||
},
|
||||
],
|
||||
})
|
||||
.catch((e: Error) => utils.commonLoggers.messageEditError('add.ts:302', newMsg, e));
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (errorOut) return;
|
||||
|
||||
const yVarString = ' ' + modifiers.yVars.keys().toArray().sort(sortYVars).join(' ');
|
||||
newMsg
|
||||
.edit({
|
||||
embeds: [
|
||||
{
|
||||
color: successColor,
|
||||
title: `Successfully ${replaceAlias ? 'replaced' : 'added'} the ${guildMode ? 'guild' : 'personal'} alias \`${aliasName}\`!`,
|
||||
description: `You can try it out now using the following command:
|
||||
\`${config.prefix}ra ${guildMode ? 'guild ' : ''}${aliasName}${modifiers.yVars.size ? yVarString : ''}\``,
|
||||
},
|
||||
],
|
||||
})
|
||||
.catch((e: Error) => utils.commonLoggers.messageSendError('add.ts:321', msgOrInt, e));
|
||||
};
|
||||
|
||||
// Using wrappers to limit "magic" booleans
|
||||
export const add = (msgOrInt: DiscordenoMessage | SlashCommandInteractionWithGuildId, guildMode: boolean, argSpaces: string[]) => handleAddUpdate(msgOrInt, guildMode, argSpaces, false);
|
||||
export const update = (msgOrInt: DiscordenoMessage | SlashCommandInteractionWithGuildId, guildMode: boolean, argSpaces: string[]) => handleAddUpdate(msgOrInt, guildMode, argSpaces, true);
|
|
@ -0,0 +1,202 @@
|
|||
import { DiscordenoMessage, hasGuildPermissions } from '@discordeno';
|
||||
|
||||
import config from '~config';
|
||||
|
||||
import dbClient from 'db/client.ts';
|
||||
|
||||
import { generateAliasError } from 'embeds/alias.ts';
|
||||
import { failColor, successColor, warnColor } from 'embeds/colors.ts';
|
||||
|
||||
import { SlashCommandInteractionWithGuildId } from 'src/mod.d.ts';
|
||||
|
||||
import utils from 'utils/utils.ts';
|
||||
|
||||
const handleDelete = async (msgOrInt: DiscordenoMessage | SlashCommandInteractionWithGuildId, guildMode: boolean, argSpaces: string[], deleteAll: boolean) => {
|
||||
if (guildMode && !(await hasGuildPermissions(BigInt(msgOrInt.guildId), utils.getAuthorIdFromMessageOrInteraction(msgOrInt), ['ADMINISTRATOR']))) {
|
||||
utils.sendOrInteract(msgOrInt, 'aliasDelete.ts:16', {
|
||||
embeds: [
|
||||
{
|
||||
color: failColor,
|
||||
title: 'Error: Only Guild Owners and Admins can delete guild aliases',
|
||||
},
|
||||
],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const verificationCode = (guildMode ? BigInt(msgOrInt.guildId) : utils.getAuthorIdFromMessageOrInteraction(msgOrInt)).toString().slice(-4);
|
||||
const aliasName = (argSpaces.shift() || '').trim();
|
||||
argSpaces.shift();
|
||||
const userEnteredVCode = (argSpaces.shift() || '').trim();
|
||||
let errorOut = false;
|
||||
|
||||
if (!deleteAll) {
|
||||
if (!aliasName) {
|
||||
utils.sendOrInteract(msgOrInt, 'aliasDelete.ts:36', {
|
||||
embeds: [
|
||||
{
|
||||
color: failColor,
|
||||
title: 'Error: Please specify one alias to delete',
|
||||
},
|
||||
],
|
||||
});
|
||||
return;
|
||||
} else if (!userEnteredVCode) {
|
||||
utils.sendOrInteract(msgOrInt, 'aliasDelete.ts:47', {
|
||||
embeds: [
|
||||
{
|
||||
color: warnColor,
|
||||
title: `Deletion is permanent, please confirm you want to delete \`${aliasName}\``,
|
||||
description: `Are you sure you want to delete the ${guildMode ? 'guild' : 'personal'} alias \`${aliasName}\`?
|
||||
|
||||
If you are certain you want to delete \`${aliasName}\` from ${guildMode ? 'this guild' : 'your account'}, please run the following command:
|
||||
\`${config.prefix}ra ${guildMode ? 'guild ' : ''}delete ${aliasName} ${verificationCode}\``,
|
||||
},
|
||||
],
|
||||
});
|
||||
return;
|
||||
} else if (userEnteredVCode !== verificationCode) {
|
||||
utils.sendOrInteract(msgOrInt, 'aliasDelete.ts:62', {
|
||||
embeds: [
|
||||
{
|
||||
color: failColor,
|
||||
title: 'Error: Incorrect verification code',
|
||||
description: `If you are certain you want to delete \`${aliasName}\` from ${guildMode ? 'this guild' : 'your account'}, please run the following command:
|
||||
\`${config.prefix}ra ${guildMode ? 'guild ' : ''}delete ${aliasName} ${verificationCode}\``,
|
||||
},
|
||||
],
|
||||
});
|
||||
return;
|
||||
} else if (userEnteredVCode === verificationCode) {
|
||||
const deleteResults = await dbClient
|
||||
.execute('DELETE FROM aliases WHERE guildid = ? AND userid = ? AND aliasName = ?', [
|
||||
guildMode ? BigInt(msgOrInt.guildId) : 0n,
|
||||
guildMode ? 0n : utils.getAuthorIdFromMessageOrInteraction(msgOrInt),
|
||||
aliasName,
|
||||
])
|
||||
.catch((e) => {
|
||||
utils.commonLoggers.dbError('aliasDelete.ts:76', 'delete from aliases', e);
|
||||
errorOut = true;
|
||||
});
|
||||
if (errorOut || !deleteResults) {
|
||||
utils.sendOrInteract(
|
||||
msgOrInt,
|
||||
'aliasDelete.ts:86',
|
||||
generateAliasError(
|
||||
'Delete failed.',
|
||||
`delete-q0-${guildMode ? 't' : 'f'}-${aliasName}-${guildMode ? BigInt(msgOrInt.guildId) : utils.getAuthorIdFromMessageOrInteraction(msgOrInt)}`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
} else if (deleteResults.affectedRows) {
|
||||
utils.sendOrInteract(msgOrInt, 'aliasDelete.ts:95', {
|
||||
embeds: [
|
||||
{
|
||||
color: successColor,
|
||||
title: 'Alias Deleted Successfully',
|
||||
description: `The ${guildMode ? 'guild' : 'personal'} alias named \`${aliasName}\` was successfully deleted.`,
|
||||
},
|
||||
],
|
||||
});
|
||||
} else {
|
||||
utils.sendOrInteract(msgOrInt, 'aliasDelete.ts:105', {
|
||||
embeds: [
|
||||
{
|
||||
color: warnColor,
|
||||
title: 'Nothing deleted',
|
||||
description: `Looks like you${guildMode ? "r guild doesn't" : " don't"} have an alias named \`${aliasName}\`.
|
||||
|
||||
Please run \`${config.prefix}ra ${guildMode ? 'guild ' : ''}list\` to view the current aliases for ${guildMode ? 'this guild' : 'your account'}.`,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
utils.sendOrInteract(msgOrInt, 'aliasDelete.ts:119', generateAliasError('How are you here?', 'deleteOne-how'));
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// We're in deleteAll mode, so aliasName will carry the user verification code.
|
||||
// Since one wasn't provided, prompt for confirmation
|
||||
if (!aliasName) {
|
||||
utils.sendOrInteract(msgOrInt, 'aliasDelete.ts:119', {
|
||||
embeds: [
|
||||
{
|
||||
color: warnColor,
|
||||
title: 'Deletion is permanent, please confirm you want to delete all aliases',
|
||||
description: `Are you sure you want to delete all aliases for ${guildMode ? 'this guild' : 'your account'}?
|
||||
|
||||
If you are certain you want to delete all aliases for ${guildMode ? 'this guild' : 'your account'}, please run the following command:
|
||||
\`${config.prefix}ra ${guildMode ? 'guild ' : ''}delete-all ${verificationCode}\``,
|
||||
},
|
||||
],
|
||||
});
|
||||
return;
|
||||
} else if (aliasName !== verificationCode) {
|
||||
utils.sendOrInteract(msgOrInt, 'aliasDelete.ts:142', {
|
||||
embeds: [
|
||||
{
|
||||
color: failColor,
|
||||
title: 'Error: Incorrect verification code',
|
||||
description: `If you are certain you want to delete all aliases for ${guildMode ? 'this guild' : 'your account'}, please run the following command:
|
||||
\`${config.prefix}ra ${guildMode ? 'guild ' : ''}delete-all ${verificationCode}\``,
|
||||
},
|
||||
],
|
||||
});
|
||||
return;
|
||||
} else if (aliasName === verificationCode) {
|
||||
const deleteResults = await dbClient
|
||||
.execute('DELETE FROM aliases WHERE guildid = ? AND userid = ?', [
|
||||
guildMode ? BigInt(msgOrInt.guildId) : 0n,
|
||||
guildMode ? 0n : utils.getAuthorIdFromMessageOrInteraction(msgOrInt),
|
||||
])
|
||||
.catch((e) => {
|
||||
utils.commonLoggers.dbError('aliasDelete.ts:159', 'delete from aliases', e);
|
||||
errorOut = true;
|
||||
});
|
||||
if (errorOut || !deleteResults) {
|
||||
utils.sendOrInteract(
|
||||
msgOrInt,
|
||||
'aliasDelete.ts:165',
|
||||
generateAliasError(
|
||||
'Delete failed.',
|
||||
`delete-q1-${guildMode ? 't' : 'f'}-${guildMode ? BigInt(msgOrInt.guildId) : utils.getAuthorIdFromMessageOrInteraction(msgOrInt)}`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
} else if (deleteResults.affectedRows) {
|
||||
utils.sendOrInteract(msgOrInt, 'aliasDelete.ts:174', {
|
||||
embeds: [
|
||||
{
|
||||
color: successColor,
|
||||
title: 'All Aliases Deleted Successfully',
|
||||
description: `All ${guildMode ? 'guild' : 'personal'} aliases for ${guildMode ? 'this guild' : 'your account'} were successfully deleted.`,
|
||||
},
|
||||
],
|
||||
});
|
||||
} else {
|
||||
utils.sendOrInteract(msgOrInt, 'aliasDelete.ts:184', {
|
||||
embeds: [
|
||||
{
|
||||
color: warnColor,
|
||||
title: 'Nothing deleted',
|
||||
description: `Looks like you${guildMode ? "r guild doesn't" : " don't"} have any aliases to delete.
|
||||
|
||||
Please run \`${config.prefix}ra ${guildMode ? 'guild ' : ''}list\` to view the current aliases for ${guildMode ? 'this guild' : 'your account'}.
|
||||
If anything shows up there after running this command, please \`${config.prefix}report\` this to the developer.`,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
utils.sendOrInteract(msgOrInt, 'aliasDelete.ts:199', generateAliasError('How are you here?', 'deleteAll-how'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Using wrappers to limit "magic" booleans
|
||||
export const deleteOne = (msgOrInt: DiscordenoMessage | SlashCommandInteractionWithGuildId, guildMode: boolean, argSpaces: string[]) => handleDelete(msgOrInt, guildMode, argSpaces, false);
|
||||
export const deleteAll = (msgOrInt: DiscordenoMessage | SlashCommandInteractionWithGuildId, guildMode: boolean, argSpaces: string[]) => handleDelete(msgOrInt, guildMode, argSpaces, true);
|
|
@ -0,0 +1,145 @@
|
|||
import { DiscordenoMessage, hasGuildPermissions } from '@discordeno';
|
||||
|
||||
import config from '~config';
|
||||
|
||||
import dbClient from 'db/client.ts';
|
||||
|
||||
import { generateAliasError } from 'embeds/alias.ts';
|
||||
import { failColor, successColor } from 'embeds/colors.ts';
|
||||
|
||||
import { SlashCommandInteractionWithGuildId } from 'src/mod.d.ts';
|
||||
|
||||
import utils from 'utils/utils.ts';
|
||||
|
||||
interface QueryShape {
|
||||
aliasName: string;
|
||||
yVarCnt: number;
|
||||
rollStr: string;
|
||||
}
|
||||
|
||||
export const clone = async (msgOrInt: DiscordenoMessage | SlashCommandInteractionWithGuildId, guildMode: boolean, argSpaces: string[]) => {
|
||||
if (!guildMode && !(await hasGuildPermissions(BigInt(msgOrInt.guildId), utils.getAuthorIdFromMessageOrInteraction(msgOrInt), ['ADMINISTRATOR']))) {
|
||||
utils.sendOrInteract(msgOrInt, 'clone.ts:22', {
|
||||
embeds: [
|
||||
{
|
||||
color: failColor,
|
||||
title: `Error: Only Guild Owners and Admins can copy a personal alias to a guild aliases`,
|
||||
},
|
||||
],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const aliasName = (argSpaces.shift() || '').trim().toLowerCase();
|
||||
|
||||
if (!aliasName) {
|
||||
utils.sendOrInteract(msgOrInt, 'clone.ts:37', {
|
||||
embeds: [
|
||||
{
|
||||
color: failColor,
|
||||
title: `Error: Please specify an alias to copy to ${guildMode ? 'your account' : 'this guild'}`,
|
||||
},
|
||||
],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let errorOut = false;
|
||||
const query: QueryShape[] = await dbClient
|
||||
.query(
|
||||
`SELECT aliasName, yVarCnt, rollStr FROM aliases WHERE guildid = ? AND userid = ? AND aliasName = ?`,
|
||||
guildMode ? [BigInt(msgOrInt.guildId), 0n, aliasName] : [0n, utils.getAuthorIdFromMessageOrInteraction(msgOrInt), aliasName],
|
||||
)
|
||||
.catch((e0) => {
|
||||
utils.commonLoggers.dbError('clone.ts:51', 'query', e0);
|
||||
utils.sendOrInteract(
|
||||
msgOrInt,
|
||||
'clone.ts:57',
|
||||
generateAliasError(
|
||||
'DB Query Failed.',
|
||||
`clone-q0-${guildMode ? 't' : 'f'}-${aliasName}-${guildMode ? BigInt(msgOrInt.guildId) : utils.getAuthorIdFromMessageOrInteraction(msgOrInt)}`,
|
||||
),
|
||||
);
|
||||
errorOut = true;
|
||||
});
|
||||
if (errorOut) return;
|
||||
|
||||
const details = query[0];
|
||||
|
||||
if (!details) {
|
||||
utils.sendOrInteract(msgOrInt, 'clone.ts:71', {
|
||||
embeds: [
|
||||
{
|
||||
color: failColor,
|
||||
title: `\`${aliasName}\` does not exist as a${guildMode ? ' guild alias' : 'n alias on your account'}.`,
|
||||
description: `Did you mean to run \`${config.prefix}ra ${guildMode ? '' : 'guild '}clone ${aliasName}\`?`,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
const targetQuery: QueryShape[] = await dbClient
|
||||
.query(
|
||||
`SELECT aliasName, yVarCnt, rollStr FROM aliases WHERE guildid = ? AND userid = ? AND aliasName = ?`,
|
||||
guildMode ? [0n, utils.getAuthorIdFromMessageOrInteraction(msgOrInt), aliasName] : [BigInt(msgOrInt.guildId), 0n, aliasName],
|
||||
)
|
||||
.catch((e0) => {
|
||||
utils.commonLoggers.dbError('clone.ts:82', 'query', e0);
|
||||
utils.sendOrInteract(
|
||||
msgOrInt,
|
||||
'clone.ts:90',
|
||||
generateAliasError(
|
||||
'DB Query Failed.',
|
||||
`clone-q1-${guildMode ? 't' : 'f'}-${aliasName}-${guildMode ? BigInt(msgOrInt.guildId) : utils.getAuthorIdFromMessageOrInteraction(msgOrInt)}`,
|
||||
),
|
||||
);
|
||||
errorOut = true;
|
||||
});
|
||||
if (errorOut) return;
|
||||
|
||||
if (targetQuery.length) {
|
||||
utils.sendOrInteract(msgOrInt, 'clone.ts:102', {
|
||||
embeds: [
|
||||
{
|
||||
color: failColor,
|
||||
title: `\`${aliasName}\` already exists as an alias ${guildMode ? 'on your account' : 'in this guild'}.`,
|
||||
description: `Please delete or rename the ${guildMode ? 'personal' : 'guild'} alias \`${aliasName}\` and try again.`,
|
||||
},
|
||||
],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await dbClient
|
||||
.execute(`INSERT INTO aliases(guildid,userid,aliasName,rollStr,yVarCnt,premium) values(?,?,?,?,?,?)`, [
|
||||
guildMode ? 0n : BigInt(msgOrInt.guildId),
|
||||
guildMode ? utils.getAuthorIdFromMessageOrInteraction(msgOrInt) : 0n,
|
||||
aliasName,
|
||||
details.rollStr,
|
||||
details.yVarCnt,
|
||||
0,
|
||||
])
|
||||
.catch((e0) => {
|
||||
utils.commonLoggers.dbError('clone.ts:110', 'query', e0);
|
||||
utils.sendOrInteract(
|
||||
msgOrInt,
|
||||
'clone.ts:126',
|
||||
generateAliasError(
|
||||
'DB Insert Failed.',
|
||||
`clone-q2-${guildMode ? 't' : 'f'}-${aliasName}-${guildMode ? BigInt(msgOrInt.guildId) : utils.getAuthorIdFromMessageOrInteraction(msgOrInt)}`,
|
||||
),
|
||||
);
|
||||
errorOut = true;
|
||||
});
|
||||
if (errorOut) return;
|
||||
|
||||
utils.sendOrInteract(msgOrInt, 'clone.ts:137', {
|
||||
embeds: [
|
||||
{
|
||||
color: successColor,
|
||||
title: `Successfully copied the ${guildMode ? 'guild' : 'personal'} alias \`${aliasName}\`!`,
|
||||
description: `\`${aliasName}\` is now available as an alias ${guildMode ? 'on your account' : 'in this guild'}.`,
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
|
@ -0,0 +1,51 @@
|
|||
import { DiscordenoMessage } from '@discordeno';
|
||||
|
||||
import dbClient from 'db/client.ts';
|
||||
|
||||
import { generateAliasError } from 'embeds/alias.ts';
|
||||
import { successColor } from 'embeds/colors.ts';
|
||||
|
||||
import { SlashCommandInteractionWithGuildId } from 'src/mod.d.ts';
|
||||
|
||||
import utils from 'utils/utils.ts';
|
||||
|
||||
interface QueryShape {
|
||||
aliasName: string;
|
||||
yVarCnt: number;
|
||||
}
|
||||
|
||||
export const list = async (msgOrInt: DiscordenoMessage | SlashCommandInteractionWithGuildId, guildMode: boolean) => {
|
||||
let errorOut = false;
|
||||
const query: QueryShape[] = await dbClient
|
||||
.query(
|
||||
`SELECT aliasName, yVarCnt FROM aliases WHERE guildid = ? AND userid = ? ORDER BY createdAt ASC`,
|
||||
guildMode ? [BigInt(msgOrInt.guildId), 0n] : [0n, utils.getAuthorIdFromMessageOrInteraction(msgOrInt)],
|
||||
)
|
||||
.catch((e0) => {
|
||||
utils.commonLoggers.dbError('list.ts:10', 'query', e0);
|
||||
utils.sendOrInteract(
|
||||
msgOrInt,
|
||||
'list.ts:26',
|
||||
generateAliasError(
|
||||
'DB Query Failed.',
|
||||
`list-q0-${guildMode ? 't' : 'f'}-${guildMode ? BigInt(msgOrInt.guildId) : utils.getAuthorIdFromMessageOrInteraction(msgOrInt)}`,
|
||||
),
|
||||
);
|
||||
errorOut = true;
|
||||
});
|
||||
if (errorOut) return;
|
||||
|
||||
utils.sendOrInteract(msgOrInt, 'list.ts:33', {
|
||||
embeds: [
|
||||
{
|
||||
color: successColor,
|
||||
title: `Found ${query.length} alias${query.length === 1 ? '' : 'es'} for ${guildMode ? 'this guild' : 'your account'}:`,
|
||||
description: query.length
|
||||
? `Format shown is \`alias-name\` followed by the number of yVars required for the alias in parenthesis, if there are any required.
|
||||
|
||||
${query.map((a) => `\`${a.aliasName}\`${a.yVarCnt ? ` (${a.yVarCnt})` : ''}`).join(', ')}`
|
||||
: '',
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
|
@ -0,0 +1,144 @@
|
|||
import { DiscordenoMessage, hasGuildPermissions } from '@discordeno';
|
||||
|
||||
import config from '~config';
|
||||
|
||||
import dbClient from 'db/client.ts';
|
||||
|
||||
import { generateAliasError } from 'embeds/alias.ts';
|
||||
import { failColor, successColor } from 'embeds/colors.ts';
|
||||
|
||||
import { SlashCommandInteractionWithGuildId } from 'src/mod.d.ts';
|
||||
|
||||
import utils from 'utils/utils.ts';
|
||||
|
||||
interface QueryShape {
|
||||
aliasName: string;
|
||||
}
|
||||
|
||||
export const rename = async (msgOrInt: DiscordenoMessage | SlashCommandInteractionWithGuildId, guildMode: boolean, argSpaces: string[]) => {
|
||||
if (guildMode && !(await hasGuildPermissions(BigInt(msgOrInt.guildId), utils.getAuthorIdFromMessageOrInteraction(msgOrInt), ['ADMINISTRATOR']))) {
|
||||
utils.sendOrInteract(msgOrInt, 'rename.ts:20', {
|
||||
embeds: [
|
||||
{
|
||||
color: failColor,
|
||||
title: `Error: Only Guild Owners and Admins can rename a guild aliases`,
|
||||
},
|
||||
],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const oldAliasName = (argSpaces.shift() || '').trim().toLowerCase();
|
||||
argSpaces.shift();
|
||||
const newAliasName = (argSpaces.shift() || '').trim().toLowerCase();
|
||||
|
||||
if (!oldAliasName || !newAliasName) {
|
||||
utils.sendOrInteract(msgOrInt, 'rename.ts:37', {
|
||||
embeds: [
|
||||
{
|
||||
color: failColor,
|
||||
title: `Error: Please specify both an alias to rename, and the new name to set it to.`,
|
||||
},
|
||||
],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// make sure old alias exists, and new doesn't exist first
|
||||
let errorOut = false;
|
||||
const queryOld: QueryShape[] = await dbClient
|
||||
.query(
|
||||
`SELECT aliasName FROM aliases WHERE guildid = ? AND userid = ? AND aliasName = ?`,
|
||||
guildMode ? [BigInt(msgOrInt.guildId), 0n, oldAliasName] : [0n, utils.getAuthorIdFromMessageOrInteraction(msgOrInt), oldAliasName],
|
||||
)
|
||||
.catch((e0) => {
|
||||
utils.commonLoggers.dbError('rename.ts:44', 'query', e0);
|
||||
utils.sendOrInteract(
|
||||
msgOrInt,
|
||||
'rename.ts:58',
|
||||
generateAliasError(
|
||||
'DB Query Failed.',
|
||||
`rename-q0-${guildMode ? 't' : 'f'}-${oldAliasName}-${guildMode ? BigInt(msgOrInt.guildId) : utils.getAuthorIdFromMessageOrInteraction(msgOrInt)}`,
|
||||
),
|
||||
);
|
||||
errorOut = true;
|
||||
});
|
||||
if (errorOut) return;
|
||||
|
||||
if (!queryOld.length) {
|
||||
utils.sendOrInteract(msgOrInt, 'rename.ts:70', {
|
||||
embeds: [
|
||||
{
|
||||
color: failColor,
|
||||
title: `Error: \`${oldAliasName}\` does not exist as a ${guildMode ? 'guild' : 'personal'} alias.`,
|
||||
description: `If you are trying to update an existing alias, but forgot the name, please run the following command to view all your ${guildMode ? 'guild ' : ''}aliases:
|
||||
\`${config.prefix}ra ${guildMode ? 'guild ' : ''}list\``,
|
||||
},
|
||||
],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const queryNew: QueryShape[] = await dbClient
|
||||
.query(
|
||||
`SELECT aliasName FROM aliases WHERE guildid = ? AND userid = ? AND aliasName = ?`,
|
||||
guildMode ? [BigInt(msgOrInt.guildId), 0n, newAliasName] : [0n, utils.getAuthorIdFromMessageOrInteraction(msgOrInt), newAliasName],
|
||||
)
|
||||
.catch((e0) => {
|
||||
utils.commonLoggers.dbError('rename.ts:44', 'query', e0);
|
||||
utils.sendOrInteract(
|
||||
msgOrInt,
|
||||
'rename.ts:91',
|
||||
generateAliasError(
|
||||
'DB Query Failed.',
|
||||
`rename-q1-${guildMode ? 't' : 'f'}-${newAliasName}-${guildMode ? BigInt(msgOrInt.guildId) : utils.getAuthorIdFromMessageOrInteraction(msgOrInt)}`,
|
||||
),
|
||||
);
|
||||
errorOut = true;
|
||||
});
|
||||
if (errorOut) return;
|
||||
|
||||
if (queryNew.length) {
|
||||
utils.sendOrInteract(msgOrInt, 'rename.ts:103', {
|
||||
embeds: [
|
||||
{
|
||||
color: failColor,
|
||||
title: `Error: \`${newAliasName}\` already exists as a ${guildMode ? 'guild' : 'personal'} alias.`,
|
||||
description: 'Please choose a different name for this alias.',
|
||||
},
|
||||
],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// do the rename
|
||||
await dbClient
|
||||
.execute('UPDATE aliases SET aliasName = ? WHERE guildid = ? AND userid = ? AND aliasName = ?', [
|
||||
newAliasName,
|
||||
guildMode ? BigInt(msgOrInt.guildId) : 0n,
|
||||
guildMode ? 0n : utils.getAuthorIdFromMessageOrInteraction(msgOrInt),
|
||||
oldAliasName,
|
||||
])
|
||||
.catch((e0) => {
|
||||
utils.commonLoggers.dbError('rename.ts:169', 'update', e0);
|
||||
utils.sendOrInteract(
|
||||
msgOrInt,
|
||||
'rename.ts:126',
|
||||
generateAliasError(
|
||||
'DB Update Failed.',
|
||||
`rename-q2-${guildMode ? 't' : 'f'}-${oldAliasName}-${newAliasName}-${guildMode ? BigInt(msgOrInt.guildId) : utils.getAuthorIdFromMessageOrInteraction(msgOrInt)}`,
|
||||
),
|
||||
);
|
||||
errorOut = true;
|
||||
});
|
||||
|
||||
utils.sendOrInteract(msgOrInt, 'rename.ts:136', {
|
||||
embeds: [
|
||||
{
|
||||
color: successColor,
|
||||
title: `Successfully renamed the ${guildMode ? 'guild' : 'personal'} alias \`${oldAliasName}\` to \`${newAliasName}\`!`,
|
||||
description: `\`${newAliasName}\` is now available as an alias ${guildMode ? 'in this guild' : 'on your account'}.`,
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
|
@ -0,0 +1,25 @@
|
|||
export const ReservedWords = Object.freeze([
|
||||
'guild',
|
||||
'help',
|
||||
'h',
|
||||
'?',
|
||||
'list',
|
||||
'list-all',
|
||||
'add',
|
||||
'create',
|
||||
'set',
|
||||
'update',
|
||||
'replace',
|
||||
'preview',
|
||||
'view',
|
||||
'delete',
|
||||
'remove',
|
||||
'delete-one',
|
||||
'delete-all',
|
||||
'remove-all',
|
||||
'run',
|
||||
'execute',
|
||||
'clone',
|
||||
'copy',
|
||||
'rename',
|
||||
]);
|
|
@ -0,0 +1,141 @@
|
|||
import { DiscordenoMessage, hasOwnProperty } from '@discordeno';
|
||||
import { log, LogTypes as LT } from '@Log4Deno';
|
||||
|
||||
import config from '~config';
|
||||
|
||||
import { getModifiers } from 'artigen/dice/getModifiers.ts';
|
||||
|
||||
import { sendRollRequest } from 'artigen/managers/queueManager.ts';
|
||||
|
||||
import { rollingEmbed } from 'artigen/utils/embeds.ts';
|
||||
import { argSpacesSplitRegex } from 'artigen/utils/escape.ts';
|
||||
|
||||
import dbClient from 'db/client.ts';
|
||||
import { queries } from 'db/common.ts';
|
||||
|
||||
import { generateAliasError } from 'embeds/alias.ts';
|
||||
import { failColor } from 'embeds/colors.ts';
|
||||
|
||||
import { SlashCommandInteractionWithGuildId } from 'src/mod.d.ts';
|
||||
|
||||
import utils from 'utils/utils.ts';
|
||||
|
||||
interface QueryShape {
|
||||
aliasName: string;
|
||||
yVarCnt: number;
|
||||
rollStr: string;
|
||||
}
|
||||
|
||||
export const run = async (msgOrInt: DiscordenoMessage | SlashCommandInteractionWithGuildId, guildMode: boolean, command: string, argSpaces: string[]) => {
|
||||
let errorOut = false;
|
||||
const aliasName = (command === 'run' || command === 'execute' ? argSpaces.shift() || '' : command)?.trim().toLowerCase();
|
||||
const yVars = new Map<string, number>();
|
||||
argSpaces
|
||||
.join('')
|
||||
.trim()
|
||||
.replaceAll('\n', ' ')
|
||||
.split(' ')
|
||||
.filter((x) => x)
|
||||
.forEach((yVar, idx) => yVars.set(`y${idx}`, parseFloat(yVar)));
|
||||
|
||||
let query: QueryShape[] = await dbClient
|
||||
.query(
|
||||
`SELECT aliasName, yVarCnt, rollStr FROM aliases WHERE guildid = ? AND userid = ? AND aliasName = ?`,
|
||||
guildMode ? [BigInt(msgOrInt.guildId), 0n, aliasName] : [0n, utils.getAuthorIdFromMessageOrInteraction(msgOrInt), aliasName],
|
||||
)
|
||||
.catch((e0) => {
|
||||
utils.commonLoggers.dbError('run.ts:30', 'query', e0);
|
||||
utils.sendOrInteract(
|
||||
msgOrInt,
|
||||
'run.ts:47',
|
||||
generateAliasError(
|
||||
'DB Query Failed.',
|
||||
`run-q0-${guildMode ? 't' : 'f'}-${aliasName}-${guildMode ? BigInt(msgOrInt.guildId) : utils.getAuthorIdFromMessageOrInteraction(msgOrInt)}`,
|
||||
),
|
||||
);
|
||||
errorOut = true;
|
||||
});
|
||||
if (errorOut) return;
|
||||
|
||||
if (!guildMode && !query.length) {
|
||||
// Didn't find an alias for the user, maybe their doing an implicit guild mode?
|
||||
query = await dbClient
|
||||
.query(`SELECT aliasName, yVarCnt, rollStr FROM aliases WHERE guildid = ? AND userid = ? AND aliasName = ?`, [BigInt(msgOrInt.guildId), 0n, aliasName])
|
||||
.catch((e0) => {
|
||||
utils.commonLoggers.dbError('run.ts:43', 'query', e0);
|
||||
utils.sendOrInteract(
|
||||
msgOrInt,
|
||||
'run.ts:64',
|
||||
generateAliasError(
|
||||
'DB Query Failed.',
|
||||
`run-q1-${guildMode ? 't' : 'f'}-${aliasName}-${guildMode ? BigInt(msgOrInt.guildId) : utils.getAuthorIdFromMessageOrInteraction(msgOrInt)}`,
|
||||
),
|
||||
);
|
||||
errorOut = true;
|
||||
});
|
||||
if (errorOut) return;
|
||||
}
|
||||
|
||||
const details = query.shift();
|
||||
if (!details) {
|
||||
utils.sendOrInteract(msgOrInt, 'run.ts:78', {
|
||||
embeds: [
|
||||
{
|
||||
color: failColor,
|
||||
title: `No alias named \`${aliasName}\` found${guildMode ? ' ' : ' on your account or '}in this guild`,
|
||||
description: `Please run \`${config.prefix}ra ${guildMode ? 'guild ' : ''}list\` to view the available aliases.`,
|
||||
},
|
||||
],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (yVars.size < details.yVarCnt) {
|
||||
utils.sendOrInteract(msgOrInt, 'run.ts:92', {
|
||||
embeds: [
|
||||
{
|
||||
color: failColor,
|
||||
title: 'Not enough yVars provided',
|
||||
description: `The alias \`${aliasName}\` requires \`${details.yVarCnt}\` yVars, but only \`${yVars.size}\` were provided. The roll string for this alias is:
|
||||
\`${details.rollStr}\``.slice(0, 3_000),
|
||||
},
|
||||
],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const m = await utils.sendOrInteract(msgOrInt, 'run.ts:115', rollingEmbed, true);
|
||||
if (!m) {
|
||||
log(LT.ERROR, `My message didn't send! ${msgOrInt}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const rollStrArgSpaces = details.rollStr.split(argSpacesSplitRegex).filter((x) => x);
|
||||
const [modifiers, remainingArgs] = getModifiers(rollStrArgSpaces);
|
||||
|
||||
if (!modifiers.valid) {
|
||||
// m.edit(generateRollError('Modifiers invalid:', modifiers.error.name, modifiers.error.message)).catch((e) =>
|
||||
// utils.commonLoggers.messageEditError('run.ts:96', m, e)
|
||||
// );
|
||||
return;
|
||||
}
|
||||
|
||||
const currDateTime = new Date();
|
||||
dbClient.execute(queries.callIncCnt('roll')).catch((e) => utils.commonLoggers.dbError('run.ts:104', 'call sproc INC_CNT on', e));
|
||||
dbClient.execute(queries.callIncHeatmap(currDateTime)).catch((e) => utils.commonLoggers.dbError('run.ts:105', 'update', e));
|
||||
|
||||
modifiers.yVars = yVars;
|
||||
sendRollRequest({
|
||||
apiRoll: false,
|
||||
ddRoll: true,
|
||||
testRoll: false,
|
||||
dd: {
|
||||
myResponse: m,
|
||||
originalMessage: hasOwnProperty(msgOrInt, 'token') ? m : msgOrInt,
|
||||
authorId: utils.getAuthorIdFromMessageOrInteraction(msgOrInt),
|
||||
},
|
||||
rollCmd: remainingArgs.join(''),
|
||||
modifiers,
|
||||
originalCommand: details.rollStr,
|
||||
});
|
||||
};
|
|
@ -0,0 +1,84 @@
|
|||
import { DiscordenoMessage } from '@discordeno';
|
||||
|
||||
import config from '~config';
|
||||
|
||||
import dbClient from 'db/client.ts';
|
||||
|
||||
import { generateAliasError } from 'embeds/alias.ts';
|
||||
import { failColor, successColor } from 'embeds/colors.ts';
|
||||
|
||||
import { SlashCommandInteractionWithGuildId } from 'src/mod.d.ts';
|
||||
|
||||
import utils from 'utils/utils.ts';
|
||||
|
||||
interface QueryShape {
|
||||
aliasName: string;
|
||||
yVarCnt: number;
|
||||
rollStr: string;
|
||||
}
|
||||
|
||||
export const view = async (msgOrInt: DiscordenoMessage | SlashCommandInteractionWithGuildId, guildMode: boolean, argSpaces: string[]) => {
|
||||
const aliasName = argSpaces.shift();
|
||||
|
||||
if (!aliasName) {
|
||||
utils.sendOrInteract(msgOrInt, 'view.ts:24', {
|
||||
embeds: [
|
||||
{
|
||||
color: failColor,
|
||||
title: 'No alias provided.',
|
||||
description: `Please run this command again with an alias to search for, for example
|
||||
|
||||
If you need to see all aliases for ${guildMode ? 'this guild' : 'your account'}, please run \`${config.prefix}ra ${guildMode ? 'guild ' : ''}list\` to see all of ${
|
||||
guildMode ? "this guild's" : 'your'
|
||||
} current aliases.`,
|
||||
},
|
||||
],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let errorOut = false;
|
||||
const query: QueryShape[] = await dbClient
|
||||
.query(
|
||||
`SELECT aliasName, yVarCnt, rollStr FROM aliases WHERE guildid = ? AND userid = ? AND aliasName = ?`,
|
||||
guildMode ? [BigInt(msgOrInt.guildId), 0n, aliasName.toLowerCase()] : [0n, utils.getAuthorIdFromMessageOrInteraction(msgOrInt), aliasName.toLowerCase()],
|
||||
)
|
||||
.catch((e0) => {
|
||||
utils.commonLoggers.dbError('view.ts:46', 'query', e0);
|
||||
utils.sendOrInteract(
|
||||
msgOrInt,
|
||||
'view.ts:50',
|
||||
generateAliasError(
|
||||
'DB Query Failed.',
|
||||
`view-q0-${guildMode ? 't' : 'f'}-${aliasName}-${guildMode ? BigInt(msgOrInt.guildId) : utils.getAuthorIdFromMessageOrInteraction(msgOrInt)}`,
|
||||
),
|
||||
);
|
||||
errorOut = true;
|
||||
});
|
||||
if (errorOut) return;
|
||||
|
||||
const details = query[0];
|
||||
|
||||
if (details) {
|
||||
utils.sendOrInteract(msgOrInt, 'view.ts:63', {
|
||||
embeds: [
|
||||
{
|
||||
color: successColor,
|
||||
title: `Found the alias \`${aliasName}\` for ${guildMode ? 'this guild' : 'your account'}:`,
|
||||
description: `Y Var Count: \`${details.yVarCnt}\` Alias Name: \`${details.aliasName}\`
|
||||
${details.rollStr}`,
|
||||
},
|
||||
],
|
||||
});
|
||||
} else {
|
||||
utils.sendOrInteract(msgOrInt, 'view.ts:74', {
|
||||
embeds: [
|
||||
{
|
||||
color: failColor,
|
||||
title: `\`${aliasName}\` does not exist as a${guildMode ? ' guild alias' : 'n alias on your account'}.`,
|
||||
description: `Did you mean to run \`${config.prefix}ra ${guildMode ? '' : 'guild '}view ${aliasName}\`?`,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
};
|
|
@ -1,13 +1,17 @@
|
|||
import dbClient from '../db/client.ts';
|
||||
import { queries } from '../db/common.ts';
|
||||
import {
|
||||
// Discordeno deps
|
||||
DiscordenoMessage,
|
||||
hasGuildPermissions,
|
||||
} from '../../deps.ts';
|
||||
import apiCommands from './apiCmd/_index.ts';
|
||||
import { failColor } from '../commandUtils.ts';
|
||||
import utils from '../utils.ts';
|
||||
import { DiscordenoMessage, hasGuildPermissions } from '@discordeno';
|
||||
|
||||
import config from '~config';
|
||||
|
||||
import apiCommands from 'commands/apiCmd/_index.ts';
|
||||
|
||||
import { generateHelpMessage } from 'commands/helpLibrary/generateHelpMessage.ts';
|
||||
|
||||
import dbClient from 'db/client.ts';
|
||||
import { queries } from 'db/common.ts';
|
||||
|
||||
import { failColor } from 'embeds/colors.ts';
|
||||
|
||||
import utils from 'utils/utils.ts';
|
||||
|
||||
export const api = async (message: DiscordenoMessage, args: string[]) => {
|
||||
// Light telemetry to see how many times a command is being run
|
||||
|
@ -32,13 +36,13 @@ export const api = async (message: DiscordenoMessage, args: string[]) => {
|
|||
}
|
||||
|
||||
// Makes sure the user is authenticated to run the API command
|
||||
if (await hasGuildPermissions(message.authorId, message.guildId, ['ADMINISTRATOR'])) {
|
||||
if (await hasGuildPermissions(message.guildId, message.authorId, ['ADMINISTRATOR'])) {
|
||||
switch (apiArg) {
|
||||
case 'help':
|
||||
case 'h':
|
||||
// [[api help
|
||||
// Shows API help details
|
||||
apiCommands.help(message);
|
||||
message.send(generateHelpMessage('api')).catch((e: Error) => utils.commonLoggers.messageSendError('apiCmd.ts:44', message, e));
|
||||
break;
|
||||
case 'allow':
|
||||
case 'block':
|
||||
|
@ -74,7 +78,7 @@ export const api = async (message: DiscordenoMessage, args: string[]) => {
|
|||
{
|
||||
color: failColor,
|
||||
title: 'API commands are powerful and can only be used by guild Owners and Admins.',
|
||||
description: 'For information on how to use the API, please check the GitHub README for more information [here](https://github.com/Burn-E99/TheArtificer).',
|
||||
description: `For information on how to use the API, please check the GitHub README for more information [here](${config.links.sourceCode}).`,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
import { help } from './apiHelp.ts';
|
||||
import { allowBlock } from './allowBlock.ts';
|
||||
import { deleteGuild } from './deleteGuild.ts';
|
||||
import { status } from './status.ts';
|
||||
import { showHideWarn } from './showHideWarn.ts';
|
||||
import { allowBlock } from 'commands/apiCmd/allowBlock.ts';
|
||||
import { deleteGuild } from 'commands/apiCmd/deleteGuild.ts';
|
||||
import { showHideWarn } from 'commands/apiCmd/showHideWarn.ts';
|
||||
import { status } from 'commands/apiCmd/status.ts';
|
||||
|
||||
export default {
|
||||
help,
|
||||
allowBlock,
|
||||
deleteGuild,
|
||||
status,
|
||||
showHideWarn,
|
||||
status,
|
||||
};
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import dbClient from '../../db/client.ts';
|
||||
import {
|
||||
// Discordeno deps
|
||||
DiscordenoMessage,
|
||||
} from '../../../deps.ts';
|
||||
import { generateApiFailed, generateApiSuccess } from '../../commandUtils.ts';
|
||||
import utils from '../../utils.ts';
|
||||
import { DiscordenoMessage } from '@discordeno';
|
||||
|
||||
import dbClient from 'db/client.ts';
|
||||
|
||||
import { generateApiFailed, generateApiSuccess } from 'embeds/api.ts';
|
||||
|
||||
import utils from 'utils/utils.ts';
|
||||
|
||||
export const allowBlock = async (message: DiscordenoMessage, apiArg: string) => {
|
||||
let errorOutInitial = false;
|
||||
|
|
|
@ -1,65 +0,0 @@
|
|||
import config from '../../../config.ts';
|
||||
import {
|
||||
// Discordeno deps
|
||||
DiscordenoMessage,
|
||||
} from '../../../deps.ts';
|
||||
import { infoColor1, infoColor2 } from '../../commandUtils.ts';
|
||||
import utils from '../../utils.ts';
|
||||
|
||||
export const help = (message: DiscordenoMessage) => {
|
||||
message.send({
|
||||
embeds: [
|
||||
{
|
||||
color: infoColor2,
|
||||
title: "The Artificer's API Details:",
|
||||
description:
|
||||
`The Artificer has a built in API that allows user to roll dice into Discord using third party programs. By default, API rolls are blocked from being sent in your guild. The API warning is also enabled by default. These commands may only be used by the Owner or Admins of your guild.
|
||||
|
||||
For information on how to use the API, please check the GitHub README for more information [here](https://github.com/Burn-E99/TheArtificer).
|
||||
|
||||
You may enable and disable the API rolls for your guild as needed.`,
|
||||
},
|
||||
{
|
||||
color: infoColor1,
|
||||
title: 'Available API Commands:',
|
||||
fields: [
|
||||
{
|
||||
name: `\`${config.prefix}api help\``,
|
||||
value: 'This command',
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: `\`${config.prefix}api status\``,
|
||||
value: 'Shows the current status of the API for the channel this was run in',
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: `\`${config.prefix}api allow/enable\``,
|
||||
value: 'Allows API Rolls to be sent to the channel this was run in',
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: `\`${config.prefix}api block/disable\``,
|
||||
value: 'Blocks API Rolls from being sent to the channel this was run in',
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: `\`${config.prefix}api delete\``,
|
||||
value: "Deletes this channel's settings from The Artificer's database",
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: `\`${config.prefix}api show-warn\``,
|
||||
value: 'Shows the API warning on all rolls sent to the channel this was run in',
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: `\`${config.prefix}api hide-warn\``,
|
||||
value: 'Hides the API warning on all rolls sent to the channel this was run in',
|
||||
inline: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}).catch((e: Error) => utils.commonLoggers.messageSendError('apiHelp.ts:67', message, e));
|
||||
};
|
|
@ -1,10 +1,12 @@
|
|||
import dbClient from '../../db/client.ts';
|
||||
import {
|
||||
// Discordeno deps
|
||||
DiscordenoMessage,
|
||||
} from '../../../deps.ts';
|
||||
import { failColor, successColor } from '../../commandUtils.ts';
|
||||
import utils from '../../utils.ts';
|
||||
import { DiscordenoMessage } from '@discordeno';
|
||||
|
||||
import config from '~config';
|
||||
|
||||
import dbClient from 'db/client.ts';
|
||||
|
||||
import { failColor, successColor } from 'embeds/colors.ts';
|
||||
|
||||
import utils from 'utils/utils.ts';
|
||||
|
||||
export const deleteGuild = async (message: DiscordenoMessage) => {
|
||||
let errorOut = false;
|
||||
|
@ -31,7 +33,7 @@ export const deleteGuild = async (message: DiscordenoMessage) => {
|
|||
embeds: [
|
||||
{
|
||||
color: successColor,
|
||||
title: "This guild's API setting has been removed from The Artifier's Database.",
|
||||
title: `This guild's API setting has been removed from ${config.name}'s Database.`,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import dbClient from '../../db/client.ts';
|
||||
import {
|
||||
// Discordeno deps
|
||||
DiscordenoMessage,
|
||||
} from '../../../deps.ts';
|
||||
import { generateApiFailed, generateApiSuccess } from '../../commandUtils.ts';
|
||||
import utils from '../../utils.ts';
|
||||
import { DiscordenoMessage } from '@discordeno';
|
||||
|
||||
import dbClient from 'db/client.ts';
|
||||
|
||||
import { generateApiFailed, generateApiSuccess } from 'embeds/api.ts';
|
||||
|
||||
import utils from 'utils/utils.ts';
|
||||
|
||||
export const showHideWarn = async (message: DiscordenoMessage, apiArg: string) => {
|
||||
let errorOutInitial = false;
|
||||
|
@ -23,7 +23,7 @@ export const showHideWarn = async (message: DiscordenoMessage, apiArg: string) =
|
|||
await dbClient
|
||||
.execute(`INSERT INTO allowed_guilds(guildid,channelid,hidewarn) values(?,?,?)`, [message.guildId, message.channelId, apiArg === 'hide-warn' ? 1 : 0])
|
||||
.catch((e0) => {
|
||||
utils.commonLoggers.dbError('showHideWarn.ts:25', 'insert inot', e0);
|
||||
utils.commonLoggers.dbError('showHideWarn.ts:25', 'insert into', e0);
|
||||
message.send(generateApiFailed(`${apiArg} on`)).catch((e: Error) => utils.commonLoggers.messageSendError('showHideWarn.ts:26', message, e));
|
||||
errorOut = true;
|
||||
});
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import dbClient from '../../db/client.ts';
|
||||
import {
|
||||
// Discordeno deps
|
||||
DiscordenoMessage,
|
||||
} from '../../../deps.ts';
|
||||
import { failColor, generateApiStatus } from '../../commandUtils.ts';
|
||||
import utils from '../../utils.ts';
|
||||
import { DiscordenoMessage } from '@discordeno';
|
||||
|
||||
import dbClient from 'db/client.ts';
|
||||
|
||||
import { generateApiStatus } from 'embeds/api.ts';
|
||||
import { failColor } from 'embeds/colors.ts';
|
||||
|
||||
import utils from 'utils/utils.ts';
|
||||
|
||||
export const status = async (message: DiscordenoMessage) => {
|
||||
// Get status of guild from the db
|
||||
|
|
|
@ -1,15 +1,17 @@
|
|||
import config from '../../config.ts';
|
||||
import dbClient from '../db/client.ts';
|
||||
import { queries } from '../db/common.ts';
|
||||
import {
|
||||
// Discordeno deps
|
||||
DiscordenoMessage,
|
||||
} from '../../deps.ts';
|
||||
import auditCommands from './auditCmd/_index.ts';
|
||||
import { failColor } from '../commandUtils.ts';
|
||||
import utils from '../utils.ts';
|
||||
import { DiscordenoMessage } from '@discordeno';
|
||||
|
||||
export const audit = async (message: DiscordenoMessage, args: string[]) => {
|
||||
import config from '~config';
|
||||
|
||||
import auditCommands from 'commands/auditCmd/_index.ts';
|
||||
|
||||
import dbClient from 'db/client.ts';
|
||||
import { queries } from 'db/common.ts';
|
||||
|
||||
import { failColor } from 'embeds/colors.ts';
|
||||
|
||||
import utils from 'utils/utils.ts';
|
||||
|
||||
export const audit = (message: DiscordenoMessage, args: string[]) => {
|
||||
// Light telemetry to see how many times a command is being run
|
||||
dbClient.execute(queries.callIncCnt('audit')).catch((e) => utils.commonLoggers.dbError('audit.ts:16', 'call sproc INC_CNT on', e));
|
||||
|
||||
|
@ -32,7 +34,7 @@ export const audit = async (message: DiscordenoMessage, args: string[]) => {
|
|||
break;
|
||||
case 'guilds':
|
||||
// [[audit guilds
|
||||
// Shows breakdown of guilds and detials on them
|
||||
// Shows breakdown of guilds and details on them
|
||||
auditCommands.auditGuilds(message);
|
||||
break;
|
||||
default:
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { auditHelp } from './auditHelp.ts';
|
||||
import { auditDB } from './auditDB.ts';
|
||||
import { auditGuilds } from './auditGuilds.ts';
|
||||
import { auditDB } from 'commands/auditCmd/auditDB.ts';
|
||||
import { auditGuilds } from 'commands/auditCmd/auditGuilds.ts';
|
||||
import { auditHelp } from 'commands/auditCmd/auditHelp.ts';
|
||||
|
||||
export default {
|
||||
auditHelp,
|
||||
auditDB,
|
||||
auditGuilds,
|
||||
auditHelp,
|
||||
};
|
||||
|
|
|
@ -1,12 +1,17 @@
|
|||
import dbClient from '../../db/client.ts';
|
||||
import {
|
||||
// Discordeno deps
|
||||
DiscordenoMessage,
|
||||
EmbedField,
|
||||
} from '../../../deps.ts';
|
||||
import { infoColor2 } from '../../commandUtils.ts';
|
||||
import { compilingStats } from '../../commonEmbeds.ts';
|
||||
import utils from '../../utils.ts';
|
||||
import { DiscordenoMessage, EmbedField } from '@discordeno';
|
||||
|
||||
import dbClient from 'db/client.ts';
|
||||
|
||||
import { infoColor2 } from 'embeds/colors.ts';
|
||||
import { compilingStats } from 'embeds/common.ts';
|
||||
|
||||
import utils from 'utils/utils.ts';
|
||||
|
||||
interface DBSizeData {
|
||||
table: string;
|
||||
size: string;
|
||||
rows: number;
|
||||
}
|
||||
|
||||
export const auditDB = async (message: DiscordenoMessage) => {
|
||||
try {
|
||||
|
@ -15,9 +20,9 @@ export const auditDB = async (message: DiscordenoMessage) => {
|
|||
// Get DB statistics
|
||||
const auditQuery = await dbClient.query(`SELECT * FROM db_size;`).catch((e) => utils.commonLoggers.dbError('auditDB.ts:19', 'query', e));
|
||||
|
||||
// Turn all tables into embed fields, currently only properly will handle 25 tables, but we'll fix that when artificer gets 26 tables
|
||||
// Turn all tables into embed fields, currently only properly will handle 25 tables, but we'll fix that when it gets 26 tables
|
||||
const embedFields: Array<EmbedField> = [];
|
||||
auditQuery.forEach((row: any) => {
|
||||
auditQuery.forEach((row: DBSizeData) => {
|
||||
embedFields.push({
|
||||
name: `${row.table}`,
|
||||
value: `**Size:** ${row.size} MB
|
||||
|
@ -39,6 +44,6 @@ export const auditDB = async (message: DiscordenoMessage) => {
|
|||
],
|
||||
}).catch((e: Error) => utils.commonLoggers.messageEditError('auditDB.ts:43', message, e));
|
||||
} catch (e) {
|
||||
utils.commonLoggers.messageSendError('auditDB.ts:45', message, e);
|
||||
utils.commonLoggers.messageSendError('auditDB.ts:45', message, e as Error);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,94 +1,148 @@
|
|||
import config from '../../../config.ts';
|
||||
import {
|
||||
// Discordeno deps
|
||||
cache,
|
||||
cacheHandlers,
|
||||
DiscordenoMessage,
|
||||
} from '../../../deps.ts';
|
||||
import { infoColor2 } from '../../commandUtils.ts';
|
||||
import utils from '../../utils.ts';
|
||||
import { cache, cacheHandlers, DiscordenoGuild, DiscordenoMessage } from '@discordeno';
|
||||
|
||||
import config from '~config';
|
||||
|
||||
import { infoColor2 } from 'embeds/colors.ts';
|
||||
|
||||
import utils from 'utils/utils.ts';
|
||||
|
||||
const sortGuildByMemberCount = (a: DiscordenoGuild, b: DiscordenoGuild) => {
|
||||
if (a.memberCount < b.memberCount) {
|
||||
return 1;
|
||||
}
|
||||
if (a.memberCount > b.memberCount) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
export const auditGuilds = async (message: DiscordenoMessage) => {
|
||||
const cachedGuilds = await cacheHandlers.size('guilds');
|
||||
const guildOwnerCounts = new Map<bigint, number>();
|
||||
const sizeCats = [10_000, 5_000, 1_000, 500, 100, 50, 25, 10, 1];
|
||||
const guildSizeDist = new Map<number, number>(sizeCats.map((size) => [size, 0]));
|
||||
|
||||
let totalCount = 0;
|
||||
let realCount = 0;
|
||||
let botsCount = 0;
|
||||
|
||||
let auditText = '';
|
||||
|
||||
cache.guilds.forEach((guild) => {
|
||||
totalCount += guild.memberCount;
|
||||
let localBotCount = 0;
|
||||
let localRealCount = 0;
|
||||
guild.members.forEach((member) => {
|
||||
if (member.bot) {
|
||||
botsCount++;
|
||||
localBotCount++;
|
||||
} else {
|
||||
realCount++;
|
||||
localRealCount++;
|
||||
}
|
||||
});
|
||||
cache.guilds
|
||||
.array()
|
||||
.sort(sortGuildByMemberCount)
|
||||
.forEach((guild) => {
|
||||
totalCount += guild.memberCount;
|
||||
let localBotCount = 0;
|
||||
let localRealCount = 0;
|
||||
guild.members.forEach((member) => {
|
||||
if (member.bot) {
|
||||
botsCount++;
|
||||
localBotCount++;
|
||||
} else {
|
||||
realCount++;
|
||||
localRealCount++;
|
||||
}
|
||||
});
|
||||
|
||||
auditText += `Guild: ${guild.name} (${guild.id})
|
||||
sizeCats.some((size) => {
|
||||
if (guild.memberCount >= size) {
|
||||
guildSizeDist.set(size, (guildSizeDist.get(size) ?? 0) + 1);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
// Track repeat guild owners
|
||||
guildOwnerCounts.set(guild.ownerId, (guildOwnerCounts.get(guild.ownerId) ?? 0) + 1);
|
||||
|
||||
// Add guild to output text
|
||||
auditText += `Guild: ${guild.name} (${guild.id})
|
||||
Owner: ${guild.owner?.username}#${guild.owner?.discriminator} (${guild.ownerId})
|
||||
Tot mem: ${guild.memberCount} | Real: ${localRealCount} | Bot: ${localBotCount}
|
||||
|
||||
`;
|
||||
});
|
||||
|
||||
const b = await new Blob([auditText as BlobPart], { type: 'text' });
|
||||
const tooBig = await new Blob(['tooBig' as BlobPart], { type: 'text' });
|
||||
|
||||
// Condense repeat guild owners
|
||||
const repeatCounts: number[] = [];
|
||||
Array.from(guildOwnerCounts).map(([_owenId, cnt]) => {
|
||||
repeatCounts[cnt - 1] = (repeatCounts[cnt - 1] ?? 0) + 1;
|
||||
});
|
||||
|
||||
const b = await new Blob([auditText as BlobPart], { 'type': 'text' });
|
||||
const tooBig = await new Blob(['tooBig' as BlobPart], { 'type': 'text' });
|
||||
|
||||
message.send({
|
||||
embeds: [{
|
||||
color: infoColor2,
|
||||
title: 'Guilds Audit',
|
||||
description: `Shows details of the guilds that ${config.name} serves.
|
||||
message
|
||||
.send({
|
||||
embeds: [
|
||||
{
|
||||
color: infoColor2,
|
||||
title: 'Guilds Audit',
|
||||
description: `Shows details of the guilds that ${config.name} serves.
|
||||
|
||||
Please see attached file for audit details on cached guilds and members.`,
|
||||
timestamp: new Date().toISOString(),
|
||||
fields: [
|
||||
{
|
||||
name: 'Total Guilds:',
|
||||
value: `${cache.guilds.size}`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: 'Cached Guilds:',
|
||||
value: `${cachedGuilds}`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: 'Uncached Guilds:',
|
||||
value: `${cache.dispatchedGuildIds.size}`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: 'Total Members\n(may be artificially higher if 1 user is in multiple guilds the bot is in):',
|
||||
value: `${totalCount}`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: 'Cached Real People:',
|
||||
value: `${realCount}`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: 'Cached Bots:',
|
||||
value: `${botsCount}`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: 'Average members per guild:',
|
||||
value: `${(totalCount / cache.guilds.size).toFixed(2)}`,
|
||||
inline: true,
|
||||
timestamp: new Date().toISOString(),
|
||||
fields: [
|
||||
{
|
||||
name: 'Total Guilds:',
|
||||
value: `${cache.guilds.size}`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: 'Cached Guilds:',
|
||||
value: `${cachedGuilds}`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: 'Uncached Guilds:',
|
||||
value: `${cache.dispatchedGuildIds.size}`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: 'Total Members\n(may be artificially higher if 1 user is in multiple guilds the bot is in):',
|
||||
value: `${totalCount.toLocaleString()}`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: 'Cached Real People:',
|
||||
value: `${realCount}`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: 'Cached Bots:',
|
||||
value: `${botsCount}`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: 'Average members per guild:',
|
||||
value: `${(totalCount / cache.guilds.size).toFixed(2)}`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: 'Repeat Guild Owners:',
|
||||
value: repeatCounts
|
||||
.map((ownerCnt, serverIdx) => `${ownerCnt} ${ownerCnt === 1 ? 'person has' : 'people have'} me in ${serverIdx + 1} of their guilds`)
|
||||
.filter((str) => str)
|
||||
.join('\n') || 'No Repeat Guild Owners',
|
||||
},
|
||||
{
|
||||
name: 'Guild Size Dist:',
|
||||
value: Array.from(guildSizeDist)
|
||||
.map(
|
||||
([size, count], idx) =>
|
||||
`${count} Guild${count === 1 ? ' has' : 's have'} ${
|
||||
guildSizeDist.has(sizeCats[idx - 1]) ? `${size.toLocaleString()} - ${(sizeCats[idx - 1] - 1).toLocaleString()}` : `at least ${size.toLocaleString()}`
|
||||
} Member${size === 1 ? '' : 's'}`,
|
||||
)
|
||||
.join('\n') || 'Not available',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}],
|
||||
file: {
|
||||
'blob': b.size > 8388290 ? tooBig : b,
|
||||
'name': 'auditDetails.txt',
|
||||
},
|
||||
}).catch((e: Error) => utils.commonLoggers.messageSendError('auditGuild.ts:19', message, e));
|
||||
file: {
|
||||
blob: b.size > config.maxFileSize ? tooBig : b,
|
||||
name: 'auditDetails.txt',
|
||||
},
|
||||
})
|
||||
.catch((e: Error) => utils.commonLoggers.messageSendError('auditGuild.ts:19', message, e));
|
||||
};
|
||||
|
|
|
@ -1,33 +1,37 @@
|
|||
import config from '../../../config.ts';
|
||||
import {
|
||||
// Discordeno deps
|
||||
DiscordenoMessage,
|
||||
} from '../../../deps.ts';
|
||||
import { infoColor1 } from '../../commandUtils.ts';
|
||||
import utils from '../../utils.ts';
|
||||
import { DiscordenoMessage } from '@discordeno';
|
||||
|
||||
import config from '~config';
|
||||
|
||||
import { infoColor1 } from 'embeds/colors.ts';
|
||||
|
||||
import utils from 'utils/utils.ts';
|
||||
|
||||
export const auditHelp = (message: DiscordenoMessage) => {
|
||||
message.send({
|
||||
embeds: [{
|
||||
color: infoColor1,
|
||||
title: 'Audit Help',
|
||||
fields: [
|
||||
message
|
||||
.send({
|
||||
embeds: [
|
||||
{
|
||||
name: `\`${config.prefix}audit help\``,
|
||||
value: 'This command',
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: `\`${config.prefix}audit db\``,
|
||||
value: 'Shows current DB table sizes',
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: `\`${config.prefix}audit guilds\``,
|
||||
value: 'Shows breakdown of guilds and detials on them',
|
||||
inline: true,
|
||||
color: infoColor1,
|
||||
title: 'Audit Help',
|
||||
fields: [
|
||||
{
|
||||
name: `\`${config.prefix}audit help\``,
|
||||
value: 'This command',
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: `\`${config.prefix}audit db\``,
|
||||
value: 'Shows current DB table sizes',
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: `\`${config.prefix}audit guilds\``,
|
||||
value: 'Shows breakdown of guilds and details on them',
|
||||
inline: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}],
|
||||
}).catch((e: Error) => utils.commonLoggers.messageSendError('auditHelp.ts:35', message, e));
|
||||
})
|
||||
.catch((e: Error) => utils.commonLoggers.messageSendError('auditHelp.ts:35', message, e));
|
||||
};
|
||||
|
|
|
@ -1,39 +1,43 @@
|
|||
import config from '../../config.ts';
|
||||
import dbClient from '../db/client.ts';
|
||||
import { queries } from '../db/common.ts';
|
||||
import {
|
||||
// Discordeno deps
|
||||
DiscordenoMessage,
|
||||
// Log4Deno deps
|
||||
log,
|
||||
LT,
|
||||
} from '../../deps.ts';
|
||||
import { EmojiConf } from '../mod.d.ts';
|
||||
import utils from '../utils.ts';
|
||||
import { DiscordenoMessage } from '@discordeno';
|
||||
import { log, LogTypes as LT } from '@Log4Deno';
|
||||
|
||||
import config from '~config';
|
||||
|
||||
import dbClient from 'db/client.ts';
|
||||
import { queries } from 'db/common.ts';
|
||||
|
||||
import utils from 'utils/utils.ts';
|
||||
|
||||
interface EmojiConf {
|
||||
name: string;
|
||||
aliases: string[];
|
||||
id: string;
|
||||
animated: boolean;
|
||||
deleteSender: boolean;
|
||||
}
|
||||
|
||||
const allEmojiAliases: string[] = [];
|
||||
|
||||
config.emojis.forEach((emji: EmojiConf) => {
|
||||
allEmojiAliases.push(...emji.aliases);
|
||||
config.emojis.forEach((curEmoji: EmojiConf) => {
|
||||
allEmojiAliases.push(...curEmoji.aliases);
|
||||
});
|
||||
|
||||
export const emoji = (message: DiscordenoMessage, command: string) => {
|
||||
// shortcut
|
||||
if (allEmojiAliases.indexOf(command)) {
|
||||
if (allEmojiAliases.includes(command)) {
|
||||
// Start looping thru the possible emojis
|
||||
config.emojis.some((emji: EmojiConf) => {
|
||||
log(LT.LOG, `Checking if command was emoji ${JSON.stringify(emji)}`);
|
||||
config.emojis.some((curEmoji: EmojiConf) => {
|
||||
log(LT.LOG, `Checking if command was emoji ${JSON.stringify(curEmoji)}`);
|
||||
// If a match gets found
|
||||
if (emji.aliases.indexOf(command || '') > -1) {
|
||||
if (curEmoji.aliases.includes(command || '')) {
|
||||
// Light telemetry to see how many times a command is being run
|
||||
dbClient.execute(queries.callIncCnt('emojis')).catch((e) => utils.commonLoggers.dbError('emojis.ts:28', 'call sproc INC_CNT on', e));
|
||||
|
||||
// Send the needed emoji
|
||||
message
|
||||
.send(`<${emji.animated ? 'a' : ''}:${emji.name}:${emji.id}>`)
|
||||
.send(`<${curEmoji.animated ? 'a' : ''}:${curEmoji.name}:${curEmoji.id}>`)
|
||||
.catch((e: Error) => utils.commonLoggers.messageSendError('emoji.ts:33', message, e));
|
||||
// And attempt to delete if needed
|
||||
if (emji.deleteSender) {
|
||||
if (curEmoji.deleteSender) {
|
||||
message.delete().catch((e: Error) => utils.commonLoggers.messageDeleteError('emoji.ts:36', message, e));
|
||||
}
|
||||
return true;
|
||||
|
|
|
@ -1,15 +1,14 @@
|
|||
import config from '../../config.ts';
|
||||
import dbClient from '../db/client.ts';
|
||||
import { queries } from '../db/common.ts';
|
||||
import {
|
||||
// Discordeno deps
|
||||
DiscordenoMessage,
|
||||
// Log4Deno deps
|
||||
log,
|
||||
LT,
|
||||
} from '../../deps.ts';
|
||||
import { infoColor1 } from '../commandUtils.ts';
|
||||
import utils from '../utils.ts';
|
||||
import { DiscordenoMessage } from '@discordeno';
|
||||
import { log, LogTypes as LT } from '@Log4Deno';
|
||||
|
||||
import config from '~config';
|
||||
|
||||
import dbClient from 'db/client.ts';
|
||||
import { queries } from 'db/common.ts';
|
||||
|
||||
import { infoColor1 } from 'embeds/colors.ts';
|
||||
|
||||
import utils from 'utils/utils.ts';
|
||||
|
||||
export const handleMentions = (message: DiscordenoMessage) => {
|
||||
log(LT.LOG, `Handling @mention message: ${JSON.stringify(message)}`);
|
||||
|
|
|
@ -1,50 +1,52 @@
|
|||
import dbClient from '../db/client.ts';
|
||||
import { queries } from '../db/common.ts';
|
||||
import {
|
||||
// Discordeno deps
|
||||
DiscordenoMessage,
|
||||
} from '../../deps.ts';
|
||||
import config from '../../config.ts';
|
||||
import { failColor, infoColor2 } from '../commandUtils.ts';
|
||||
import utils from '../utils.ts';
|
||||
import intervals from '../intervals.ts';
|
||||
import { CreateGlobalApplicationCommand, DiscordenoMessage, Interaction } from '@discordeno';
|
||||
|
||||
export const heatmap = async (message: DiscordenoMessage) => {
|
||||
import config from '~config';
|
||||
|
||||
import dbClient from 'db/client.ts';
|
||||
import { queries } from 'db/common.ts';
|
||||
|
||||
import { failColor, infoColor2 } from 'embeds/colors.ts';
|
||||
|
||||
import intervals from 'utils/intervals.ts';
|
||||
import utils from 'utils/utils.ts';
|
||||
|
||||
export const heatmapSC: CreateGlobalApplicationCommand = {
|
||||
name: 'heatmap',
|
||||
description: 'Shows a heatmap of when the roll command is run the most.',
|
||||
};
|
||||
|
||||
export const heatmap = (msgOrInt: DiscordenoMessage | Interaction) => {
|
||||
// Light telemetry to see how many times a command is being run
|
||||
dbClient.execute(queries.callIncCnt('heatmap')).catch((e) => utils.commonLoggers.dbError('heatmap.ts:14', 'call sproc INC_CNT on', e));
|
||||
|
||||
if (config.api.enable) {
|
||||
message
|
||||
.send({
|
||||
embeds: [
|
||||
{
|
||||
title: 'Roll Heatmap',
|
||||
description: `Over time, this image will show a nice pattern of when rolls are requested the most.
|
||||
utils.sendOrInteract(msgOrInt, 'heatmap.ts:23', {
|
||||
embeds: [
|
||||
{
|
||||
title: 'Roll Heatmap',
|
||||
description: `Over time, this image will show a nice pattern of when rolls are requested the most.
|
||||
|
||||
Least Rolls: ${intervals.getMinRollCnt()}
|
||||
Most Rolls: ${intervals.getMaxRollCnt()}`,
|
||||
footer: {
|
||||
text: 'Data is shown in US Eastern Time. | This heatmap uses data starting 6/26/2022.',
|
||||
},
|
||||
color: infoColor2,
|
||||
image: {
|
||||
url: `${config.api.publicDomain}api/heatmap.png`,
|
||||
},
|
||||
footer: {
|
||||
text: 'Data is shown in US Eastern Time. | This heatmap uses data starting 6/26/2022.',
|
||||
},
|
||||
],
|
||||
})
|
||||
.catch((e) => utils.commonLoggers.messageSendError('heatmap.ts:21', message, e));
|
||||
color: infoColor2,
|
||||
image: {
|
||||
url: `${config.api.publicDomain}api/heatmap.png?now=${new Date().getTime()}`,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
} else {
|
||||
message
|
||||
.send({
|
||||
embeds: [
|
||||
{
|
||||
title: 'Roll Heatmap Disabled',
|
||||
description: "This command requires the bot's API to be enabled. If you are the host of this bot, check your `config.ts` file to enable it.",
|
||||
color: failColor,
|
||||
},
|
||||
],
|
||||
})
|
||||
.catch((e) => utils.commonLoggers.messageSendError('heatmap.ts:21', message, e));
|
||||
utils.sendOrInteract(msgOrInt, 'heatmap.ts:42', {
|
||||
embeds: [
|
||||
{
|
||||
title: 'Roll Heatmap Disabled',
|
||||
description: "This command requires the bot's API to be enabled. If you are the host of this bot, check your `config.ts` file to enable it.",
|
||||
color: failColor,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,103 +1,21 @@
|
|||
import config from '../../config.ts';
|
||||
import dbClient from '../db/client.ts';
|
||||
import { queries } from '../db/common.ts';
|
||||
import {
|
||||
// Discordeno deps
|
||||
DiscordenoMessage,
|
||||
} from '../../deps.ts';
|
||||
import { infoColor2 } from '../commandUtils.ts';
|
||||
import utils from '../utils.ts';
|
||||
import { CreateGlobalApplicationCommand, DiscordenoMessage, Interaction } from '@discordeno';
|
||||
|
||||
export const help = (message: DiscordenoMessage) => {
|
||||
// Light telemetry to see how many times a command is being run
|
||||
dbClient.execute(queries.callIncCnt('help')).catch((e) => utils.commonLoggers.dbError('htlp.ts:15', 'call sproc INC_CNT on', e));
|
||||
import config from '~config';
|
||||
|
||||
message
|
||||
.send({
|
||||
embeds: [
|
||||
{
|
||||
color: infoColor2,
|
||||
title: "The Artificer's Available Commands:",
|
||||
fields: [
|
||||
{
|
||||
name: `\`${config.prefix}?\``,
|
||||
value: 'This command',
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: `\`${config.prefix}rollhelp\` or \`${config.prefix}??\``,
|
||||
value: `Details on how to use the roll command, listed as \`${config.prefix}xdy...${config.postfix}\` below`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: `\`${config.prefix}rollDecorators\` or \`${config.prefix}???\``,
|
||||
value: `Details on how to use decorators on the roll command`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: `\`${config.prefix}api [subcommand]\``,
|
||||
value: `Administrative tools for the bots's API, run \`${config.prefix}api help\` for more details`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: `\`${config.prefix}ping\``,
|
||||
value: 'Pings the bot to check connectivity',
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: `\`${config.prefix}info\``,
|
||||
value: 'Prints some information and links relating to the bot',
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: `\`${config.prefix}privacy\``,
|
||||
value: 'Prints some information about the Privacy Policy',
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: `\`${config.prefix}version\``,
|
||||
value: 'Prints the bots version',
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: `\`${config.prefix}popcat\``,
|
||||
value: 'Popcat',
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: `\`${config.prefix}report [text]\``,
|
||||
value: 'Report a command that failed to run',
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: `\`${config.prefix}stats\``,
|
||||
value: 'Statistics on the bot',
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: `\`${config.prefix}heatmap\``,
|
||||
value: 'Heatmap of when the roll command is run the most',
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: `\`${config.prefix}opt-out\` or \`${config.prefix}ignore-me\``,
|
||||
value: 'Adds you to an ignore list so the bot will never respond to you',
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: `\`${config.prefix}opt-in\` **Available via DM ONLY**`,
|
||||
value: 'Removes you from the ignore list',
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: `\`${config.prefix}xdydzracsq!${config.postfix}\` ...`,
|
||||
value:
|
||||
`Rolls all configs requested, you may repeat the command multiple times in the same message (just ensure you close each roll with \`${config.postfix}\`), run \`${config.prefix}??\` for more details`,
|
||||
inline: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
.catch((e: Error) => utils.commonLoggers.messageSendError('help.ts:82', message, e));
|
||||
import { generateHelpMessage } from 'commands/helpLibrary/generateHelpMessage.ts';
|
||||
|
||||
import dbClient from 'db/client.ts';
|
||||
import { queries } from 'db/common.ts';
|
||||
|
||||
import utils from 'utils/utils.ts';
|
||||
|
||||
export const helpSC: CreateGlobalApplicationCommand = {
|
||||
name: 'help',
|
||||
description: `Opens ${config.name}'s Help Library.`,
|
||||
};
|
||||
|
||||
export const help = (msgOrInt: DiscordenoMessage | Interaction) => {
|
||||
// Light telemetry to see how many times a command is being run
|
||||
dbClient.execute(queries.callIncCnt('help')).catch((e) => utils.commonLoggers.dbError('help.ts:15', 'call sproc INC_CNT on', e));
|
||||
utils.sendOrInteract(msgOrInt, 'help.ts:20', generateHelpMessage());
|
||||
};
|
||||
|
|
|
@ -0,0 +1,120 @@
|
|||
import config from '~config';
|
||||
|
||||
import { RollAliasHelpPages } from 'commands/helpLibrary/aliasHelp.ts';
|
||||
import { ApiHelpPages } from 'commands/helpLibrary/apiHelp.ts';
|
||||
import { HelpContents, HelpPage } from 'commands/helpLibrary/helpLibrary.d.ts';
|
||||
import { InlineHelpPages } from 'commands/helpLibrary/inlineHelp.ts';
|
||||
import { RepeatHelpPages } from 'commands/helpLibrary/repeatHelp.ts';
|
||||
|
||||
import { RootRollHelpPages } from 'commands/helpLibrary/rollHelp/_rootRollHelp.ts';
|
||||
|
||||
const name = `${config.name}'s Help Library`;
|
||||
const description = 'Please use the dropdown menus below to get help on any commands you need assistance with.';
|
||||
const dict = new Map<string, HelpPage | HelpContents>([
|
||||
['roll-help', RootRollHelpPages],
|
||||
['alias', RollAliasHelpPages],
|
||||
['inline', InlineHelpPages],
|
||||
['repeat', RepeatHelpPages],
|
||||
[
|
||||
'opt-out',
|
||||
{
|
||||
name: 'Opt Out',
|
||||
description: `**Usage:** \`${config.prefix}opt-out\` or \`${config.prefix}ignore-me\`
|
||||
|
||||
Adds you to an ignore list so the bot will never respond to you.`,
|
||||
},
|
||||
],
|
||||
[
|
||||
'opt-in',
|
||||
{
|
||||
name: 'Opt In',
|
||||
description: `**Usage:** \`${config.prefix}opt-in\`
|
||||
|
||||
Removes you from the ignore list.
|
||||
|
||||
**Notice:** This command is only available while Direct Messaging @$.`,
|
||||
},
|
||||
],
|
||||
[
|
||||
'info',
|
||||
{
|
||||
name: 'Info/About',
|
||||
description: `**Usage:** \`${config.prefix}info\` or \`${config.prefix}i\`
|
||||
|
||||
Displays information about ${config.name} and its developer.`,
|
||||
},
|
||||
],
|
||||
[
|
||||
'report',
|
||||
{
|
||||
name: 'Report an Issue',
|
||||
description: `**Usage:** \`${config.prefix}report [text]\` or \`${config.prefix}re [text]\`
|
||||
|
||||
Report an issue or feature request to ${config.name}'s developer.
|
||||
|
||||
**Notice:** \`[text]\` will be sent to a private Discord channel that only ${config.name} and its developer can see.`,
|
||||
},
|
||||
],
|
||||
[
|
||||
'stats',
|
||||
{
|
||||
name: 'Statistics',
|
||||
description: `**Usage:** \`${config.prefix}stats\` or \`${config.prefix}s\`
|
||||
|
||||
Displays basic statistics on ${config.name}'s usage.`,
|
||||
},
|
||||
],
|
||||
[
|
||||
'heatmap',
|
||||
{
|
||||
name: 'Roll Heatmap',
|
||||
description: `**Usage:** \`${config.prefix}heatmap\` or \`${config.prefix}hm\`
|
||||
|
||||
Displays a heatmap showing when rolls are happening across a week.`,
|
||||
},
|
||||
],
|
||||
['api', ApiHelpPages],
|
||||
[
|
||||
'version',
|
||||
{
|
||||
name: 'Version',
|
||||
description: `**Usage:** \`${config.prefix}version\` or \`${config.prefix}v\`
|
||||
|
||||
Displays the current version of ${config.name}.`,
|
||||
},
|
||||
],
|
||||
[
|
||||
'privacy',
|
||||
{
|
||||
name: 'Privacy/Terms of Service',
|
||||
description: `**Usage:** \`${config.prefix}privacy\` or \`${config.prefix}tos\`
|
||||
|
||||
Displays a summary of the Privacy Policy and Terms of Service, along with links to the full documents.`,
|
||||
},
|
||||
],
|
||||
[
|
||||
'ping',
|
||||
{
|
||||
name: 'Ping!',
|
||||
description: `**Usage:** \`${config.prefix}ping\`
|
||||
|
||||
Pings ${config.name} to see if its online and how responsive its connection to Discord's API is.`,
|
||||
},
|
||||
],
|
||||
[
|
||||
'help',
|
||||
{
|
||||
name: 'Help',
|
||||
description: `**Usage:** \`${config.prefix}help\` or \`${config.prefix}h\` or \`${config.prefix}?\`
|
||||
|
||||
This command, opens an interactive help library for all commands.`,
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
export const RootHelpPages: HelpPage = {
|
||||
name,
|
||||
description,
|
||||
isPage: true,
|
||||
dict,
|
||||
};
|
|
@ -0,0 +1,227 @@
|
|||
import config from '~config';
|
||||
|
||||
import { ReservedWords } from 'commands/aliasCmd/reservedWords.ts';
|
||||
|
||||
import { HelpContents, HelpPage } from 'commands/helpLibrary/helpLibrary.d.ts';
|
||||
|
||||
const nameRestrictions = `Alias names are case-insensitive (\`tEsT\` is stored as \`test\`, but can still be called as \`tEsT\`).
|
||||
|
||||
Alias Naming Restrictions:
|
||||
- Max allowed length:\`${config.limits.alias.maxNameLength}\`
|
||||
- Cannot include any spaces/whitespace/newlines
|
||||
- Cannot be named any of the following words:
|
||||
\`${ReservedWords.join('`, `')}\``;
|
||||
|
||||
const name = 'Roll Alias System';
|
||||
const description = `This system allows you to save any roll string to a short, custom, memorable alias.
|
||||
|
||||
Currently, you may create up to \`${config.limits.alias.free.guild.toLocaleString()}\` per guild and \`${config.limits.alias.free.user.toLocaleString()}\` per user account. This limit may increase or decrease in the future.
|
||||
|
||||
The following commands are all linked to the Roll Alias System:
|
||||
\`${config.prefix}rollalias\`, \`${config.prefix}ralias\`, \`${config.prefix}alias\`, \`${config.prefix}rolla\`, \`${config.prefix}ra\`
|
||||
For simplicity, all help documents use \`${config.prefix}ra\`, but any of the other commands listed above are valid.
|
||||
|
||||
${nameRestrictions}`;
|
||||
const dict = new Map<string, HelpContents>([
|
||||
[
|
||||
'run',
|
||||
{
|
||||
name: 'Run Alias',
|
||||
description: `**Personal Mode Usage:**
|
||||
- \`${config.prefix}ra [aliasName] [yVars...?]\`
|
||||
- \`${config.prefix}ra run [aliasName] [yVars...?]\`
|
||||
- \`${config.prefix}ra execute [aliasName] [yVars...?]\`
|
||||
**Guild Mode Usage:**
|
||||
- \`${config.prefix}ra guild [aliasName] [yVars...?]\`
|
||||
- \`${config.prefix}ra guild run [aliasName] [yVars...?]\`
|
||||
- \`${config.prefix}ra guild execute [aliasName] [yVars...?]\`
|
||||
|
||||
**Params:**
|
||||
- \`[aliasName]\` - The name of the alias you wish to run.
|
||||
- \`[yVars...?]\` - List of numbers to use for required y variables. If no y variables are required for the alias, no y variables need to be listed.
|
||||
|
||||
Runs the specified personal or guild alias with the provided yVars. If an alias is not found in Personal Mode, this will check and use a guild alias if one exists.`,
|
||||
example: [
|
||||
'`[[ra simpleAlias` => Runs the alias `simpleAlias`. Will first check for a personal alias named `simpleAlias`, and if one is not found, will check the guild for one.',
|
||||
'`[[ra run simpleAlias` => Runs the alias `simpleAlias`. Will first check for a personal alias named `complexAlias`, and if one is not found, will check the guild for one.',
|
||||
'`[[ra guild simpleTest` => Explicitly runs the guild alias `simpleTest`.',
|
||||
'`[[ra guild run simpleTest` => Explicitly runs the guild alias `simpleTest`.',
|
||||
'`[[ra complexAlias 10 4 7` => Runs the alias `complexAlias` with `y0=10`, `y1=4`, and `y2=7`. Check out the `Roll Alias System > Add New Alias` help page to see how this one was initially created. Will first check for a personal alias named `complexAlias`, and if one is not found, will check the guild for one.',
|
||||
],
|
||||
},
|
||||
],
|
||||
[
|
||||
'add',
|
||||
{
|
||||
name: 'Add New Alias',
|
||||
description: `**Personal Mode Usage:**
|
||||
- \`${config.prefix}ra add [aliasName] [rollString...]\`
|
||||
- \`${config.prefix}ra set [aliasName] [rollString...]\`
|
||||
- \`${config.prefix}ra create [aliasName] [rollString...]\`
|
||||
**Guild Mode Usage:** [Can be only used by Guild Owners/Admins!]
|
||||
- \`${config.prefix}ra guild add [aliasName] [rollString...]\`
|
||||
- \`${config.prefix}ra guild set [aliasName] [rollString...]\`
|
||||
- \`${config.prefix}ra guild create [aliasName] [rollString...]\`
|
||||
|
||||
**Params:**
|
||||
- \`[aliasName]\` - The name of the alias you wish to create.
|
||||
- \`[rollString]\` - The roll string to save. This can include any valid roll command, and allows the usage of y variables that will be required by the alias.
|
||||
|
||||
Creates a new alias with the specified roll string. This is saved for use either to your personal account or to the guild the command was run in.`,
|
||||
example: [
|
||||
'`[[ra add simpleAlias [[4d20+5]] Random Text! [[4d6d1]]` => Saves `[[4d20+5]] Random Text! [[4d6d1]]` as a personal alias named `simpleAlias`',
|
||||
'`[[ra guild add simpleTest Random Text! [[4d6d1]] [[4d20+5]]` => Saves `Random Text! [[4d6d1]] [[4d20+5]]` as a guild alias named `simpleTest`',
|
||||
'',
|
||||
'`[[ra add complexAlias Attack Roll: [[4d20+5+y0]]\nDamage Roll: [[y1 * 4d8 + y2]]`\nSaves `[[4d20+5+y0]]\nDamage Roll: [[y1 * 4d8 + y2]]` as a personal alias named `complexAlias` with 3 y variables (`y0`, `y1`, and `y2`). Check out the `Roll Alias System > Run Alias` help page to see how this one is run.',
|
||||
],
|
||||
},
|
||||
],
|
||||
[
|
||||
'update',
|
||||
{
|
||||
name: 'Update Existing Alias',
|
||||
description: `**Personal Mode Usage:**
|
||||
- \`${config.prefix}ra update [aliasName] [rollString...]\`
|
||||
- \`${config.prefix}ra replace [aliasName] [rollString...]\`
|
||||
**Guild Mode Usage:** [Can be only used by Guild Owners/Admins!]
|
||||
- \`${config.prefix}ra guild update [aliasName] [rollString...]\`
|
||||
- \`${config.prefix}ra guild replace [aliasName] [rollString...]\`
|
||||
|
||||
**Params:**
|
||||
- \`[aliasName]\` - The name of the alias you wish to update.
|
||||
- \`[rollString]\` - New roll string to update to. This can include any valid roll command, and allows the usage of y variables that will be required by the alias.
|
||||
|
||||
Updates the specified alias to the new roll string. This overwrites the alias saved to your personal account or to the guild the command was run in.`,
|
||||
example: [
|
||||
'`[[ra update simpleAlias [[20d7r3!]] TEXT -snd` => Saves `[[20d7r3!]] TEXT -snd` over the existing personal alias `simpleAlias`.',
|
||||
'`[[ra guild update simpleTest [[8d%! + 40]]` => Saves `[[8d%! + 40]]` over the existing guild alias `simpleTest`.',
|
||||
],
|
||||
},
|
||||
],
|
||||
[
|
||||
'list',
|
||||
{
|
||||
name: 'List All Aliases',
|
||||
description: `**Personal Mode Usage:**
|
||||
- \`${config.prefix}ra list\`
|
||||
- \`${config.prefix}ra list-all\`
|
||||
**Guild Mode Usage:**
|
||||
- \`${config.prefix}ra guild list\`
|
||||
- \`${config.prefix}ra guild list-all\`
|
||||
|
||||
Lists all aliases (and their number of yVars) that are saved to your personal account or to the guild the command was run in.`,
|
||||
},
|
||||
],
|
||||
[
|
||||
'view',
|
||||
{
|
||||
name: 'Preview One Alias',
|
||||
description: `**Personal Mode Usage:**
|
||||
- \`${config.prefix}ra preview [aliasName]\`
|
||||
- \`${config.prefix}ra view [aliasName]\`
|
||||
**Guild Mode Usage:**
|
||||
- \`${config.prefix}ra guild preview [aliasName]\`
|
||||
- \`${config.prefix}ra guild view [aliasName]\`
|
||||
|
||||
**Params:**
|
||||
- \`[aliasName]\` - The name of the alias you wish to preview.
|
||||
|
||||
Shows the saved roll string for the specified personal or guild alias.`,
|
||||
example: ['`[[ra preview testAlias`', '`[[ra guild preview testGuildAlias`'],
|
||||
},
|
||||
],
|
||||
[
|
||||
'rename',
|
||||
{
|
||||
name: 'Rename Alias',
|
||||
description: `**Personal Mode Usage:**
|
||||
- \`${config.prefix}ra rename [oldAliasName] [newAliasName]\`
|
||||
**Guild Mode Usage:** [Can be only used by Guild Owners/Admins!]
|
||||
- \`${config.prefix}ra guild rename [oldAliasName] [newAliasName]\`
|
||||
|
||||
**Params:**
|
||||
- \`[oldAliasName]\` - The name of the alias you wish to rename.
|
||||
- \`[newAliasName]\` - The new name of the alias.
|
||||
|
||||
Renames the specified alias saved to your personal account or to the guild the command was run in.`,
|
||||
example: ['`[[ra rename testAlias newName`', '`[[ra guild rename testGuildAlias newNameToo`'],
|
||||
},
|
||||
],
|
||||
[
|
||||
'clone',
|
||||
{
|
||||
name: 'Copy Alias to/from Guild',
|
||||
description: `**Copy to Guild Usage:** [Can be only used by Guild Owners/Admins!]
|
||||
- \`${config.prefix}ra clone [aliasName]\`
|
||||
- \`${config.prefix}ra copy [aliasName]\`
|
||||
**Copy from Guild Usage:**
|
||||
- \`${config.prefix}ra guild clone [aliasName]\`
|
||||
- \`${config.prefix}ra guild copy [aliasName]\`
|
||||
|
||||
**Params:**
|
||||
- \`[aliasName]\` - The name of the alias you wish to copy.
|
||||
|
||||
Copies the specified alias to/from the guild the command was run in.`,
|
||||
example: ['`[[ra clone testAlias`', '`[[ra guild clone testGuildAlias`'],
|
||||
},
|
||||
],
|
||||
[
|
||||
'delete',
|
||||
{
|
||||
name: 'Delete One Alias',
|
||||
description: `**Personal Mode Usage:**
|
||||
- \`${config.prefix}ra delete [aliasName] [verificationCode?]\`
|
||||
- \`${config.prefix}ra remove [aliasName] [verificationCode?]\`
|
||||
**Guild Mode Usage:** [Can be only used by Guild Owners/Admins!]
|
||||
- \`${config.prefix}ra guild delete [aliasName] [verificationCode?]\`
|
||||
- \`${config.prefix}ra guild remove [aliasName] [verificationCode?]\`
|
||||
|
||||
**Params:**
|
||||
- \`[aliasName]\` - The name of the alias you wish to delete.
|
||||
- \`[verificationCode?]\` - The 4 digit code to confirm that you do want to delete the alias. If omitted, ${config.name} will request confirmation and provide a verification code to re-run the command with.
|
||||
|
||||
Deletes the specified alias from your personal account or to the guild the command was run in. This is a permanent deletion and cannot be undone.`,
|
||||
example: ['`[[ra delete testAlias`', '`[[ra guild delete testGuildAlias`'],
|
||||
},
|
||||
],
|
||||
[
|
||||
'delete-all',
|
||||
{
|
||||
name: 'Delete All Aliases',
|
||||
description: `**Personal Mode Usage:**
|
||||
- \`${config.prefix}ra delete-all [verificationCode?]\`
|
||||
- \`${config.prefix}ra remove-all [verificationCode?]\`
|
||||
**Guild Mode Usage:** [Can be only used by Guild Owners/Admins!]
|
||||
- \`${config.prefix}ra guild delete-all [verificationCode?]\`
|
||||
- \`${config.prefix}ra guild remove-all [verificationCode?]\`
|
||||
|
||||
**Params:**
|
||||
- \`[verificationCode?]\` - The 4 digit code to confirm that you do want to delete the alias. If omitted, ${config.name} will request confirmation and provide a verification code to re-run the command with.
|
||||
|
||||
Deletes all aliases saved to your personal account or to the guild the command was run in. This is a permanent deletion and cannot be undone.`,
|
||||
},
|
||||
],
|
||||
[
|
||||
'help',
|
||||
{
|
||||
name: 'Help',
|
||||
description: `**Personal Mode Usage:**
|
||||
- \`${config.prefix}ra help\`
|
||||
- \`${config.prefix}ra h\`
|
||||
- \`${config.prefix}ra ?\`
|
||||
**Guild Mode Usage:**
|
||||
- \`${config.prefix}ra guild help\`
|
||||
- \`${config.prefix}ra guild h\`
|
||||
- \`${config.prefix}ra guild ?\`
|
||||
|
||||
Opens the help library to the Roll Alias System section.`,
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
export const RollAliasHelpPages: HelpPage = {
|
||||
name,
|
||||
description,
|
||||
isPage: true,
|
||||
dict,
|
||||
};
|
|
@ -0,0 +1,83 @@
|
|||
import config from '~config';
|
||||
|
||||
import { HelpContents, HelpPage } from 'commands/helpLibrary/helpLibrary.d.ts';
|
||||
|
||||
const name = 'API Rolling';
|
||||
const description =
|
||||
`${config.name} has a built in API that allows user to roll dice into Discord using third party programs. By default, API rolls are blocked from being sent in your guild. The API warning is also enabled by default.
|
||||
|
||||
These commands may only be used by the Owner or Admins of your guild. You may enable and disable the API rolls for channels in your guild as needed.
|
||||
|
||||
For information on how to use the API, please check the GitHub README for more information [here](${config.links.sourceCode}).`;
|
||||
const dict = new Map<string, HelpContents>([
|
||||
[
|
||||
'status',
|
||||
{
|
||||
name: 'Status',
|
||||
description: `**Usage:** \`${config.prefix}api status\`
|
||||
|
||||
Shows the current status of the API for the channel this was run in.`,
|
||||
},
|
||||
],
|
||||
[
|
||||
'allow',
|
||||
{
|
||||
name: 'Allow API Rolls',
|
||||
description: `**Usage:** \`${config.prefix}api allow\` or \`${config.prefix}api enable\`
|
||||
|
||||
Allows API Rolls to be sent to the channel this command was run in.`,
|
||||
},
|
||||
],
|
||||
[
|
||||
'block',
|
||||
{
|
||||
name: 'Block API Rolls',
|
||||
description: `**Usage:** \`${config.prefix}api block\` or \`${config.prefix}api disable\`
|
||||
|
||||
Blocks API Rolls from being sent to the channel this command was run in.`,
|
||||
},
|
||||
],
|
||||
[
|
||||
'hide-warn',
|
||||
{
|
||||
name: 'Hide API Warning',
|
||||
description: `**Usage:** \`${config.prefix}api hide-warn\`
|
||||
|
||||
Hides the API warning on all rolls sent to the channel this command was run in.`,
|
||||
},
|
||||
],
|
||||
[
|
||||
'show-warn',
|
||||
{
|
||||
name: 'Show API Warning',
|
||||
description: `**Usage:** \`${config.prefix}api show-warn\`
|
||||
|
||||
Shows the API warning on all rolls sent to the channel this command was run in`,
|
||||
},
|
||||
],
|
||||
[
|
||||
'delete',
|
||||
{
|
||||
name: 'Delete API Settings',
|
||||
description: `**Usage:** \`${config.prefix}api delete\`
|
||||
|
||||
Deletes this channel's settings from ${config.name}'s database.`,
|
||||
},
|
||||
],
|
||||
[
|
||||
'help',
|
||||
{
|
||||
name: 'Help',
|
||||
description: `**Usage:** \`${config.prefix}api help\` or \`${config.prefix}api h\`
|
||||
|
||||
Opens the help library to the Api Help section.`,
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
export const ApiHelpPages: HelpPage = {
|
||||
name,
|
||||
description,
|
||||
isPage: true,
|
||||
dict,
|
||||
};
|
|
@ -0,0 +1,137 @@
|
|||
import { ActionRow, botId, CreateMessage, Embed, MessageComponentTypes, SelectOption } from '@discordeno';
|
||||
|
||||
import config from '~config';
|
||||
|
||||
import { HelpContents, HelpDict, HelpPage } from 'commands/helpLibrary/helpLibrary.d.ts';
|
||||
|
||||
import { RootHelpPages } from 'commands/helpLibrary/_rootHelp.ts';
|
||||
|
||||
import { infoColor1 } from 'embeds/colors.ts';
|
||||
|
||||
import { InteractionValueSeparator } from 'events/interactionCreate.ts';
|
||||
|
||||
export const helpCustomId = 'help';
|
||||
const homeId = 'home';
|
||||
|
||||
const generateActionRowWithSelectMenu = (selected: string, helpDict: HelpDict, parent?: string): ActionRow => ({
|
||||
type: MessageComponentTypes.ActionRow,
|
||||
components: [
|
||||
{
|
||||
type: MessageComponentTypes.SelectMenu,
|
||||
customId: `${helpCustomId}${InteractionValueSeparator}${selected}`,
|
||||
options: [
|
||||
{
|
||||
label: 'Home',
|
||||
value: parent ? `${parent}${InteractionValueSeparator}${homeId}` : homeId,
|
||||
default: selected === '',
|
||||
},
|
||||
...helpDict
|
||||
.entries()
|
||||
.toArray()
|
||||
.map(
|
||||
(page): SelectOption => ({
|
||||
label: page[1].name,
|
||||
value: parent ? `${parent}${InteractionValueSeparator}${page[0]}` : page[0],
|
||||
default: page[0] === selected,
|
||||
}),
|
||||
),
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const makeHelpEmbed = (helpDict: HelpContents | HelpPage, parentTitle?: string): Embed => ({
|
||||
color: infoColor1,
|
||||
author: {
|
||||
name: parentTitle ? `Help > ${parentTitle}` : 'Help',
|
||||
},
|
||||
title: helpDict.name,
|
||||
description: helpDict.description.replaceAll('@$', `<@${botId}>`).replaceAll('[[', config.prefix).replaceAll(']]', config.postfix),
|
||||
fields: !helpDict.isPage && helpDict.example
|
||||
? [
|
||||
{
|
||||
name: `Example${helpDict.example.length > 1 ? 's' : ''}:`,
|
||||
value: helpDict.example.join('\n').replaceAll('@$', `<@${botId}>`).replaceAll('[[', config.prefix).replaceAll(']]', config.postfix),
|
||||
},
|
||||
]
|
||||
: [],
|
||||
});
|
||||
|
||||
const defaultHelpMessage = (showError = ''): CreateMessage => ({
|
||||
embeds: [
|
||||
{
|
||||
...makeHelpEmbed(RootHelpPages),
|
||||
footer: {
|
||||
text: showError ? `Error${showError}: Something went wrong, please try again.` : '',
|
||||
},
|
||||
},
|
||||
],
|
||||
components: [generateActionRowWithSelectMenu('', RootHelpPages.dict)],
|
||||
});
|
||||
|
||||
export const generateHelpMessage = (helpPath?: string): CreateMessage => {
|
||||
if (!helpPath) return defaultHelpMessage();
|
||||
|
||||
const path = helpPath
|
||||
.replaceAll(homeId, '')
|
||||
.split(InteractionValueSeparator)
|
||||
.filter((x) => x);
|
||||
const page = path.shift();
|
||||
const item = path.shift();
|
||||
const subItem = path.shift();
|
||||
|
||||
if (!page) return defaultHelpMessage();
|
||||
|
||||
// Get the first layer dictionary
|
||||
const rootHelpDict = RootHelpPages.dict.get(page);
|
||||
if (!rootHelpDict) return defaultHelpMessage('1');
|
||||
|
||||
if (!rootHelpDict.isPage) {
|
||||
return {
|
||||
embeds: [makeHelpEmbed(rootHelpDict)],
|
||||
components: [generateActionRowWithSelectMenu(page, RootHelpPages.dict)],
|
||||
};
|
||||
}
|
||||
|
||||
if (!item) {
|
||||
return {
|
||||
embeds: [makeHelpEmbed(rootHelpDict)],
|
||||
components: [generateActionRowWithSelectMenu(page, RootHelpPages.dict), generateActionRowWithSelectMenu('', rootHelpDict.dict, page)],
|
||||
};
|
||||
}
|
||||
|
||||
// Get second layer dictionary
|
||||
const helpDict = rootHelpDict.dict.get(item);
|
||||
if (!helpDict) return defaultHelpMessage('2');
|
||||
|
||||
if (!helpDict.isPage) {
|
||||
return {
|
||||
embeds: [makeHelpEmbed(helpDict, rootHelpDict.name)],
|
||||
components: [generateActionRowWithSelectMenu(page, RootHelpPages.dict), generateActionRowWithSelectMenu(item, rootHelpDict.dict, page)],
|
||||
};
|
||||
}
|
||||
|
||||
if (!subItem) {
|
||||
return {
|
||||
embeds: [makeHelpEmbed(helpDict, rootHelpDict.name)],
|
||||
components: [
|
||||
generateActionRowWithSelectMenu(page, RootHelpPages.dict),
|
||||
generateActionRowWithSelectMenu(item, rootHelpDict.dict, page),
|
||||
generateActionRowWithSelectMenu('', helpDict.dict, `${page}${InteractionValueSeparator}${item}`),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
// Get third layer dictionary
|
||||
const helpItem = helpDict.dict.get(subItem);
|
||||
if (!helpItem) return defaultHelpMessage('3');
|
||||
|
||||
return {
|
||||
embeds: [makeHelpEmbed(helpItem, `${rootHelpDict.name} > ${helpItem.isPage ? '' : helpDict.name}`)],
|
||||
components: [
|
||||
generateActionRowWithSelectMenu(page, RootHelpPages.dict),
|
||||
generateActionRowWithSelectMenu(item, rootHelpDict.dict, page),
|
||||
generateActionRowWithSelectMenu(subItem, helpDict.dict, `${page}${InteractionValueSeparator}${item}`),
|
||||
],
|
||||
};
|
||||
};
|
|
@ -0,0 +1,16 @@
|
|||
interface HelpItem {
|
||||
name: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export type HelpDict = Map<string, HelpContents | HelpPage>;
|
||||
|
||||
export interface HelpContents extends HelpItem {
|
||||
isPage?: false;
|
||||
example?: string[];
|
||||
}
|
||||
|
||||
export interface HelpPage extends HelpItem {
|
||||
isPage: true;
|
||||
dict: HelpDict;
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
import config from '~config';
|
||||
|
||||
import { HelpContents, HelpPage } from 'commands/helpLibrary/helpLibrary.d.ts';
|
||||
|
||||
const name = 'Inline Rolling';
|
||||
const description =
|
||||
`${config.name} has an option to allow inline rolls in your guild. An inline roll is a roll that does not immediately start with \`${config.prefix}\`, such as \`test ${config.prefix}d20${config.postfix}\`.
|
||||
|
||||
By default, Inline Rolls are blocked from being sent in your guild. These commands may only be used by the Owner or Admins of your guild.`;
|
||||
const dict = new Map<string, HelpContents>([
|
||||
[
|
||||
'status',
|
||||
{
|
||||
name: 'Status',
|
||||
description: `**Usage:** \`${config.prefix}inline status\`
|
||||
|
||||
Shows the current status of Inline Rolls for this guild.`,
|
||||
},
|
||||
],
|
||||
[
|
||||
'enable',
|
||||
{
|
||||
name: 'Allow Inline Rolls',
|
||||
description: `**Usage:** \`${config.prefix}inline allow\` or \`${config.prefix}inline enable\`
|
||||
|
||||
Allows Inline Rolls for this guild.`,
|
||||
},
|
||||
],
|
||||
[
|
||||
'disable',
|
||||
{
|
||||
name: 'Block Inline Rolls',
|
||||
description: `**Usage:** \`${config.prefix}inline block\` or \`${config.prefix}inline disable\` or \`${config.prefix}inline delete\`
|
||||
|
||||
Blocks inline rolls for this guild.`,
|
||||
},
|
||||
],
|
||||
[
|
||||
'help',
|
||||
{
|
||||
name: 'Help',
|
||||
description: `**Usage:** \`${config.prefix}inline help\` or \`${config.prefix}inline h\`
|
||||
|
||||
Opens the help library to the Inline Help section.`,
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
export const InlineHelpPages: HelpPage = {
|
||||
name,
|
||||
description,
|
||||
isPage: true,
|
||||
dict,
|
||||
};
|
|
@ -0,0 +1,53 @@
|
|||
import config from '~config';
|
||||
|
||||
import { HelpContents, HelpPage } from 'commands/helpLibrary/helpLibrary.d.ts';
|
||||
|
||||
const name = 'Unrestricted Repeat';
|
||||
const description = `${config.name} has an option to allow anyone to use the \`Repeat Roll\` button.
|
||||
|
||||
By default, Unrestricted Repeat Rolls are disabled in your guild, meaning only the original roller can use the \`Repeat Roll\` button. These commands may only be used by the Owner or Admins of your guild.`;
|
||||
const dict = new Map<string, HelpContents>([
|
||||
[
|
||||
'status',
|
||||
{
|
||||
name: 'Status',
|
||||
description: `**Usage:** \`${config.prefix}repeat status\`
|
||||
|
||||
Shows the current status of Repeat Rolling for this guild.`,
|
||||
},
|
||||
],
|
||||
[
|
||||
'enable',
|
||||
{
|
||||
name: 'Allow Unrestricted Repeat',
|
||||
description: `**Usage:** \`${config.prefix}repeat allow\` or \`${config.prefix}repeat enable\`
|
||||
|
||||
Allows Unrestricted Repeat Rolls for this guild. This allows anyone in the guild to use the \`Repeat Roll\` button on any roll from anyone.`,
|
||||
},
|
||||
],
|
||||
[
|
||||
'disable',
|
||||
{
|
||||
name: 'Block Unrestricted Repeat',
|
||||
description: `**Usage:** \`${config.prefix}repeat block\` or \`${config.prefix}repeat disable\` or \`${config.prefix}repeat delete\`
|
||||
|
||||
Blocks Unrestricted Repeat rolls for this guild. This only allows the original roller to use the \`Repeat Roll\` button.`,
|
||||
},
|
||||
],
|
||||
[
|
||||
'help',
|
||||
{
|
||||
name: 'Help',
|
||||
description: `**Usage:** \`${config.prefix}repeat help\` or \`${config.prefix}repeat h\`
|
||||
|
||||
Opens the help library to the Unrestricted Repeat Help section.`,
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
export const RepeatHelpPages: HelpPage = {
|
||||
name,
|
||||
description,
|
||||
isPage: true,
|
||||
dict,
|
||||
};
|
|
@ -0,0 +1,44 @@
|
|||
import config from '~config';
|
||||
|
||||
import { HelpPage } from 'commands/helpLibrary/helpLibrary.d.ts';
|
||||
|
||||
import { DecoratorsHelpPages } from 'commands/helpLibrary/rollHelp/decorators.ts';
|
||||
import { DiceOptionsHelpPages } from 'commands/helpLibrary/rollHelp/diceOptions.ts';
|
||||
import { DiceTypesHelpPages } from 'commands/helpLibrary/rollHelp/diceTypes.ts';
|
||||
import { DifferencesHelpPages } from 'commands/helpLibrary/rollHelp/differences.ts';
|
||||
import { FormattingHelpPages } from 'commands/helpLibrary/rollHelp/formatting.ts';
|
||||
import { LegalMathComplexFuncsHelpPages } from 'commands/helpLibrary/rollHelp/legalMathComplexFuncs.ts';
|
||||
import { LegalMathConstsHelpPages } from 'commands/helpLibrary/rollHelp/legalMathConsts.ts';
|
||||
import { LegalMathFuncsHelpPages } from 'commands/helpLibrary/rollHelp/legalMathFuncs.ts';
|
||||
import { LegalMathOperators } from 'commands/helpLibrary/rollHelp/legalMathOperators.ts';
|
||||
import { LegalMathTrigFuncsHelpPages } from 'commands/helpLibrary/rollHelp/legalMathTrigFuncs.ts';
|
||||
import { MiscFeaturesHelpPages } from 'commands/helpLibrary/rollHelp/miscFeatures.ts';
|
||||
|
||||
const name = 'Dice/Roll/Math Command';
|
||||
const description = `You can chain as many of these options as you want, as long as the option does not disallow it. This command also can fully solve math equations with parenthesis.
|
||||
|
||||
The help options in this group use the notation \`xdy\` to indicate the basic/required dice notation for die count and size as detailed in the \`Dice Options>Basic Dice Options\` page.
|
||||
|
||||
As this supports the [Roll20 formatting](${config.links.roll20Formatting}) syntax fully, more details and examples can be found [here](${config.links.roll20Formatting}).
|
||||
|
||||
Please use the dropdown/select menus to search through the provided documentation.`;
|
||||
const dict = new Map<string, HelpPage>([
|
||||
['differences', DifferencesHelpPages],
|
||||
['dice-types', DiceTypesHelpPages],
|
||||
['dice-options', DiceOptionsHelpPages],
|
||||
['decorators', DecoratorsHelpPages],
|
||||
['formatting', FormattingHelpPages],
|
||||
['misc-features', MiscFeaturesHelpPages],
|
||||
['legal-math-operators', LegalMathOperators],
|
||||
['legal-math-consts', LegalMathConstsHelpPages],
|
||||
['legal-math-funcs', LegalMathFuncsHelpPages],
|
||||
['legal-math-trig-funcs', LegalMathTrigFuncsHelpPages],
|
||||
['legal-math-complex-funcs', LegalMathComplexFuncsHelpPages],
|
||||
]);
|
||||
|
||||
export const RootRollHelpPages: HelpPage = {
|
||||
name,
|
||||
description,
|
||||
isPage: true,
|
||||
dict,
|
||||
};
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue