No, not quite, Tye. That line would be one way to add a spell from the code, itself. You can't send javascript through the chat. If you want to do that, what you do is create a way for your script to "catch" a particular sort of user input, and use it to feed the javascript line the right variables.
The Flow (thinking through the development)
When a chat message is sent, a chat event is created, and with it comes a message object. A script can listen for the chat event by having a function set to run on('chat:message'). Your script establishes that here:
const registerEventHandlers = () => {
on('chat:message', handleInput);
};
...and says that when there is a chat event, the function to run is handleInput().
handleInput() first needs to make sure this is the right kind of message. Since it is running for EVERY chat event, it has to first make sure that the chat message is one it should worry about. It does that by testing if the message type is 'api' and then by somehow testing that the opening few characters in the message match the particular invocation pattern for this script.
const handleInput = (msg) => {
if (msg.type !== "api") { // <== tests the msg type to be 'api'
return;
}
let args = msg.content.split(/\s+/); // <== splits the msg content into groups delimited by 1 or more white space characters
switch(args[0]) {
case '!customSpellbook': // <== looks for your particular invocation
Once you make it that far, you know you're handling a message intended for your particular script. This is where it gets interesting. You have the rest of the user input in an array (args), broken on white space characters... hang on to that a minute while we figure out if, at this point, you have what you need.
Parsing Arguments to Fit Script Functionalities
If you want to give your user (or just you) the ability to add a spell from the chat input (or another source -- we'll get to that in a minute), you need to make sure you have certain pieces of information. Let's assume those are name, damage, and type (I know you have a few more properties than that, but we'll keep it simple for the sake of example). If you're going to get all of those in a single chat message, we need a way to differentiate one piece of information from another.
!customspellbook data data data data
Really quickly we realize we can't split just on white space characters, because the names of these spells, themselves, contain white space characters. So we need a different way to differentiate the beginning of a single argument, one that is unique enough to not occur naturally in the text you want to preserve and parse, but which is easy enough to type for the user. A common one is a double-hyphen.
!customspellbook --data --data --data --data
By incorporating the double-hyphen into the regex, we can split on "1-or-more whitespace characters followed by double-hyphen"...
let args = msg.content.split(/\s+--/);
That would give us an array of your api handle followed by each piece of data as discrete elements, provided your user prefaced each argument with the double-hyphens.
Stop there just a minute. Parsing the input string is a matter of balancing coding superstructure (work for you) against simplicity and ease-of-use for your user. We are trying to make sure that you have the data you need and that the user can easily understand and use what you give them. Can you make this format work? Probably. Your data is differentiated, but your only way to identify each piece is the order that it comes in. You would have to mandate that the user enters the spell name first, followed by the damage, followed by the type. That doesn't sound too hard for the user to remember the order for three pieces of data, but what if there are 5 pieces? Or 6? Your user is going to start to not you very much. And what if your user has a spell that requires data points 1, 2, 4, and 6, but not 3 and 5? Do they have to put in empty arguments?
!customspellbook --data --data -- --data -- --data
Tagged Arguments
If you give the user a way to tag each piece of data as it comes to you, you could take care of both problems: they could enter the data in any order and you would still be able to recognize it.... AND they would be able to omit any data they didn't need.
Common intra-arg delimiters are # (hash), | (pipe), and : (colon), but you could use anything -- again, looking for something easily typed, but not commonly included in your input. From personal experience, a colon can interfere with creating an api button, and a pipe can be a problem down the line if you want to include queries in your input. Let's go with the hash for now.
!customspellbook --name#data --damage#data --type#data
Now we're getting somewhere. Using our existing parsing (splitting on /\s+--/), we get an args array of:
['!customspellbook', 'name#data', 'damage#data', 'type#data']
Let's go ahead and get rid of the first element, since we don't need it anymore:
args.shift();
In fact, since shift() not only removes the first item from an array it also RETURNS that item, we could have used this in our handleInput during the test of whether this script should handle the message...
let args = msg.content.split(/s+--/);
if(args.shift() === '!customspellbook') customspellbook(msg, args); // the args passed to csb would have the api handle already removed
In any case, we now have the discrete data points, and only the discrete data points. Now we want to split each of them into their key#value parts.
args.map(m => m.split("#"));
...would give us, for each element, an array of [key, value]. Except if the value happened to contain another hash... so we need to fix that. Adding another map operation can join up elements after the first (all of the parts that should be together as the value).
args.map(m => m.split("#")).map(m => [m[0], m.slice(1).join("#")]);
Note, you could also accomplish the same splitting on the hash with an indexOf() function, if you wanted to:
args.map(m => {
let h = m.indexOf("#");
return [m.slice(0, h), m.slice(h)];
});
A Word on Lifespan, Order, and Sanitization
The map operations are not permanent changes to your data; they last only as long as the line unless you assign the newly formatted data to a variable. Map returns an array, so you could feed it to a function that is looking for an array (we're about to), but at some point you have to assign that mapped, formatted, and massaged data to a variable for safe-keeping while you do other stuff... like SANITIZING user input. Whatever that looks like to your script, you have to make sure that the user data is appropriate and formatted properly. Maybe there are only three available types of spells, and the user has to enter one or else the spell is invalid and you don't want to load it.
Because these parsing operations (splitting on white space double hyphen, splitting each on the first hash) can be done back-to-back, and because they have to be written to a variable at some point, people will often do it as part of the declaration of the args variable in the first place...
args = msg.content.split(/\s+--/)
.slice(1)
.map(m => {
let h = m.indexOf("#");
return [m.slice(0, h), m.slice(h)];
});
You saw the shift() in there getting rid of the api handle (the '!customspellbook)? That means that this would have to happen *after* you check for whether this script should handle the message. Without the split to isolate the api handle, that means that you'd be using a startsWith() test, or maybe a regex test, and if that passed you could move on to the above parsing.
So you can go back and alter your handleInput function to make sure the args are parsed correctly, or you can leave the map/parsing operation of the # exactly where we left it (downstream, in the customspellbook function), and just assign the results to args again.
args = args.map(m => {
let h = m.indexOf("#");
return [m.slice(0, h), m.slice(h)];
});
Either way, you now have an array of key/value arrays. Which you will sanitize. Right?
Object Magic
Now that you have an array of key/value arrays (sanitized, both key and value), you can use the fromEntries() function to create an object:
let spell = Object.fromEntries(args);
That takes you from this:
[['name','data'], ['damage', 'data'], ['type', 'data']]
...to...
{ name: 'data', damage: 'data', type: 'data' }
NOW you're ready to add it to your spellBook.
...
...if your script knew to do that.
Branching the Input
See, we're still working in the main branch of your script. EVERY time it gets a chat event that it needs to respond to, it will do the process we just outlined. That would be fine if EVERY time we used the !customspellbook handle we wanted to invoke this spell-add process... but we don't. Somehow, then, we have to trip a mechanism to have the script know when it needs to add a spell versus other activities. We could do that with a named argument:
!customspellbook --action#addspell --name#data ...
...or just knowing that the first argument following the api handle is the "action" argument that drives what process the script will follow:
!customspellbook --addspell --name#data ...
Whatever the case is, we build our parsing engine to look for it, and if we find it, we know to add the spell object we just built to our spellbook.
Other Considerations
This works for basic input, but you also need to make sure that what the user inputs reaches your script without the Roll20 parser eating it, first. For instance, your damage values will get turned into inline rolls if you try to type them into an argument in a call to your function. You would need a way to escape this process (character replacement, space insertions, etc.).
Also, what you add at "game" time only lasts for as long as your sandbox/state is around. If you restart the sandbox, your spellbook resets to only the spells coded in during dev time... not the ones that were entered from chat. You would have to re-enter them. This would make me think about a way to have them picked up automatically... probably from a library handout that I could then parse into game-time spells. Every time the sandbox restarts, it throws a "ready" event, so I could have a listener in an on('ready) event go to see if there is a spell library handout and, if it finds one, to load it up.
...but that is development for another day...