Roll20 uses cookies to improve your experience on our site. Cookies enable you to enjoy certain features, social sharing functionality, and tailor message and display ads to your interests on our site and others. They also help us understand how our site is being used. By continuing to use our site, you consent to our use of cookies. Update your cookie preferences .
×
Create a free account

I Wrote My First API Script: It Allows the GM to Whisper the Player's Controlling the Selected Tokens

1593237220

Edited 1593237533
Here it is. I haven't written code since college so this was fun. I'm baby when it comes to this kind of thing so be kind. Hopefully this hasn't been done before! Even if so, it was a good exercise and I learned a lot. on('chat:message', function(msg) {     if (msg.type !== 'api') return;     if (msg.content.indexOf("!gwisp ") === -1) return;     var PlayerList = '';     _.each(msg.selected, function(obj) {         var token, character;         token = getObj('graphic', obj._id);         if (token) {             character = getObj('character', token.get('represents'));         }         if (character) {             PlayerList = PlayerList + character.get('controlledby')+ ',';         }     });     PlayerList = PlayerList.substr(0, PlayerList.length-1);     var InputString = msg.content;     var TheMessage = InputString.substr(InputString.indexOf(" ") + 1, InputString.length);     var GroupArray = PlayerList.split(',');     var UniqueArray = [];     for(var i = 0; i < GroupArray.length; i++){         if(UniqueArray.indexOf(GroupArray[i]) == -1){             var PlayerVar = getObj("player", GroupArray[i]);             var PlayerName = PlayerVar.get("_displayname");             if (PlayerName.indexOf(" ") !== -1){                 PlayerName = PlayerName.substr(0,PlayerName.indexOf(" "));             }             sendChat('The Moderator', '/w ' + PlayerName + ' ' + TheMessage);             UniqueArray.push(GroupArray[i])         }     } });
With all my long form comments, explaining what I did and why. //gwisp function, written by Ysabel, Version 1.0, 6/27/2020.  //The purpose of this function is to allow the gm to send a whisper to each player who's token is selected. //I want to be able to do this because I often like to alert players to what they see or sense individually, //and then allow them to communicate what they are sensing to one another. //For example, if there are a preist and a paladin in party, and only they can sense evil, //instead of saying outloud to the entire party "The priest and paladin sense evil", //I select the priest's token and paladin's token and type //"!gwisp You sense an evil presence." //Then the players will get the information and can choose to share it when and how they wish. //I use this function mostly with pre-prepped language. In my notebook I will have the !gwisp call //with the text ready to go so I can copy and paste it into the chat, select the players' tokens that should recieve the message //and then send away. //The main purpose of this is to, in theory, take a passive GM narration and replace it //with character interactions. It prompts players to roleplay, rather allowing them to passively absorb narration. //This is my first function I have written myself, so I have very heavily commented it for my own future reference. on('chat:message', function(msg) {     //This if statement checks each message to see if the message is supposed to use the api.     //If not, the function ends.     if (msg.type !== 'api') return;     //This if statement checks to see if the gwisp command was used.     //gwisp stands for gm whisper     if (msg.content.indexOf("!gwisp ") === -1) return;     //this variable will contain a list of all the players who control all the tokens selected     var PlayerList = '';     //this loop iterates through selected tokens. I am not entirely sure how it works. I stole it from some other code.     _.each(msg.selected, function(obj) {         var token, character;         //Stole this as well, I am pretty sure this is just a check to make sure the selected token has a character assigned.         //Wish I could give credit but I lost the page that had it...         token = getObj('graphic', obj._id);         if (token) {             character = getObj('character', token.get('represents'));         }         if (character) {             //Here we are adding the players that control the character the token represents to a string.             //The controlled by field can contain multiple players, and they are separated by commas.             //I add a last comma at the end so a comma separates the list of players for the next token checked.             PlayerList = PlayerList + character.get('controlledby')+ ',';         }     });     //because the list always ends with a hanging comma when the loop ends, I pluck it off with subtring.     PlayerList = PlayerList.substr(0, PlayerList.length-1);     //Now I grab the message I want to send to players. First I get all the text entered into the chat box.     var InputString = msg.content;     //Second I chop of the first word, which should be !gwisp. The syntax is such that !gwisp is followed by a space,     //and then the message starts, so I can simply search for the first space as the beginning of my new string.     var TheMessage = InputString.substr(InputString.indexOf(" ") + 1, InputString.length);     //Next split the comma delineated list of players apart and store in the players in an array.     //NOTE: When I say store the players in an array, I am referring to the player's unique IDs!!!     //This is not the name that get's whispered, which is a property of the player object (most of the time, see below).     var GroupArray = PlayerList.split(',');     //Because players may control multiple of the tokens selected, I need to make sure each player     //only gets messaged once, instead of for each token they control.     //The unique array will hold each player when they get messaged and the loop will skip repeats by checking     //the unique array and seeing if the player is already in it.     var UniqueArray = [];     //Here I loop through the array of players.     for(var i = 0; i < GroupArray.length; i++){         //Check to see if player is already in the unique array, if not, send a message         if(UniqueArray.indexOf(GroupArray[i]) == -1){             //get the player object based on the player's ID             var PlayerVar = getObj("player", GroupArray[i]);             //Then get the playwer's name, which is a property of the player object             //Note all of these property and the get function are described in the Roll 20 API Objects documentation.             var PlayerName = PlayerVar.get("_displayname");             //This if statement is subtly important.             //Some players may have names with multiple words/containing spaces.             //The /w whisper functionality always uses only the player's "first name".             //For example if I want to whisper my player "Glitter Goblin" I would type "/w Glitter"             //This if statement checks for a space then trims off everything after the space.             if (PlayerName.indexOf(" ") !== -1){                 PlayerName = PlayerName.substr(0,PlayerName.indexOf(" "));             }             //Finally send the message! We've got the /w for whisper then the Player's trimmed name, a space,             //and finally the contents of the message.             sendChat('The Moderator', '/w ' + PlayerName + ' ' + TheMessage);             //add the Player to the unique array so they don't get messaged again.             UniqueArray.push(GroupArray[i])         }     } });
1593240836
The Aaron
Roll20 Production Team
API Scripter
Neat!  Looks pretty good for a first script!
1593241652

Edited 1593242787
GiGs
Pro
Sheet Author
API Scripter
Great idea for a script, and very nicely commented! i havent seen anyone create this before. Some suggestions. I would make PlayerList an array. _.each( is a function from the underscore library, a suite of add on functions to make Javascript easier to use. That particular function isnt needed anymore as javascript has a native forEach function, which would change your code like this: msg.selected.forEach(function(obj) { I think its a bit easier to understand, or at least guess, what that's doing. Essentially it takes msg.selected, and loops through every selected thing. In each loop, it puts the next selected thing in something called obj, and you can access that things properties by using obj, as the script does later on. Once all things in the selected group have been dealt with, the forEach loop is finished, and the code moves past it. _.each and forEach do the same work, just the syntax is slightly different. If you setup playerList as an array, you'd have to initialise it as so: var PlayerList = []; Then you'd need to edit this section:         if (character) {             PlayerList = PlayerList + character.get('controlledby')+ ',';         } one way would be:         if (character) {             // get the comma separated list of players             let controlled = character.get('controlledby').split(',');             // loop through the players.             controlled.forEach(function(char) {                 // check if the player is already in PlayerList                 if(!PlayerList.includes(char)) {                     //if not, add to the playerlist                     PlayerList.push(char);                 }             });         } This routine ensures your PlayerList only has unique entries from the outset, and is already an array, which simplifies the follow code. Note: if controlledby contains "All", this would need work to get the player list. One approach would be to simply check if the playerlist includes "All" , and then change the ending to not bother whispering if true, just sending chat to everyone. You'll see I've done that in the code below. Here's a quick update of your code with the above tweaks. i made some other minor tweaks I'll explain below. on('chat:message', function(msg) {     if (msg.type !== 'api') return;     if (msg.content.indexOf("!gwisp ") === -1) return;     let PlayerList = [];     msg.selected.forEach(function(obj) {         let token, character;         token = getObj('graphic', obj._id);         if (token) {             character = getObj('character', token.get('represents'));         }         if (character) {             // get the comma separated list of players             const controlled = character.get('controlledby').split(',');             // loop through the players.             controlled.forEach(function(char) {                 // check if the player is already in PlayerList                 if(!PlayerList.includes(char)) {                     //if not, add to the playerlist                     PlayerList.push(char);                 }             });          }     });     const TheMessage = msg.content . split ( ' ' ). slice ( 1 ). join ( ' ' ) ;     if(PlayerList.includes('all')) {         sendChat('The Moderator', TheMessage);     } else {         PlayerList.forEach(function(player) {             var PlayerVar = getObj("player", player);             var PlayerName = PlayerVar.get("_displayname");             sendChat('The Moderator', '/w "' + PlayerName + '" ' + TheMessage);         });     } }); I switched most declarations from var to either let or const. This is a more modern syntax and there are some advantages to doing so, plus also some pitfalls. const is for variables that will not change, and let is for variables that might change. These variables respect scope, which means they only exist within the block of code in which they are declared.  For example, the Playervar variable only exists within that final PlayerList.foreach section. Variables declared with var exist from the beginning of the code, even if declared late in the function, and this is messy from an efficiency point of view, and can be a source of subtle errors. So its better to use the more modern let and const.  But you have to be careful to declare them where needed. A common mistake would be to do something like this:         let token = getObj('graphic', obj._id);         if (token) {             let character = getObj('character', token.get('represents'));         }         if (character) { This would appear to work, but character would be erased as soon as the } was passed, so that if( character ) statement would always be false. Luckily the way youve declared them avoids that problem. I also changed that for  loop to a forEach  loop. These are usually much easier to use once youre familiar with them - no need to mess around with indexes to get the item in the array, a foreach loop gives you the item directly. for getting rid of the first word in the msg.content, I used a different method: split on spaces to turn it into an array, remove the first item with slice, and rejoin into an array. Finally, there's a way around the way player names break on spaces: if you put quotes around the name, then whispers work properly. I've done that here: sendChat ( 'The Moderator' ,  '/w "'  +  PlayerName  +  '" '  +  TheMessage ); Javascript allows you to use different types of quotes, and the inner quotes will be treated as part of the string. So here PlayerName gets quotes put around it, and avoids the "breaking on spaces" problem. Hope this helps, and feel free to use or discard any of these changes :)
1593242483

Edited 1593242535
GiGs
Pro
Sheet Author
API Scripter
I forgot to explain this bit         //Stole this as well, I am pretty sure this is just a check to make sure the selected token has a character assigned.         //Wish I could give credit but I lost the page that had it...         token = getObj('graphic', obj._id);         if (token) {             character = getObj('character', token.get('represents'));         }         if (character) { This is fairly standard code, so no need to give credit. There are two tests here. Remember we are looping through all the selected objects. Its very possible to select things that are not tokens. There may be drawings, text boxes, rollable table graphics, cards, and so on.  so we need to check that the current  obj  is both a token, and a token that has a character assigned.
1593267348
The Aaron
Roll20 Production Team
API Scripter
Using a Set guarantees uniqueness: on('chat:message', function(msg) { if (msg.type !== 'api') return; if (msg.content.indexOf("!gwisp ") === -1) return; let PlayerList = new Set(); msg.selected.forEach(function(obj) { let token, character; token = getObj('graphic', obj._id); if (token) { character = getObj('character', token.get('represents')); } if (character) { // get the comma separated list of players const controlled = character.get('controlledby').split(','); // loop through the players. controlled.forEach(c => PlayerList.add(c)); } }); const TheMessage = msg.content.split(' ').slice(1).join(' '); if( PlayerList.has('all') ) { sendChat('The Moderator', TheMessage); } else { PlayerList.forEach(function(player) { var PlayerVar = getObj("player", player); var PlayerName = PlayerVar.get("_displayname"); sendChat('The Moderator', '/w "' + PlayerName + '" ' + TheMessage); }); } });  
1593272464
keithcurtis
Forum Champion
Marketplace Creator
API Scripter
Thanks Ysabel, Aaron and GiGs. Ysabel for the great first script that found a need and filled it, and all three of you for the methodical explanations. Very educational!
1593274432
GiGs
Pro
Sheet Author
API Scripter
The Aaron said: Using a Set guarantees uniqueness: Very nice!
1593276415

Edited 1593276549
The Aaron
Roll20 Production Team
API Scripter
This is definitely a great first foray into API scripts.  Here's a bit more feedback if you're interested: // Wrapping your script in the on('ready',...) event is a good habit to get into. // The API Sandbox will receive create events for all the existing entities of the game // as it starts up. This can be useful, but if your script has logic that deals with create // or modify events, it can be confusing or even damaging to react to them. Once the sandbox // is fully loaded, it will issue the 'ready' event, which allows your script to start registering // for events and processing as normal. on('ready',()=>{ // A more modern way of writing functions is using the "fat arrow" syntax. // function (x,y,z) { /* stuff */ } becomes (x,y,z) => { /* stuff */ } // It's a very minor difference, but tends to make the code a little clearer // once you're used to the syntax. There are some other differences which come // up with objects and the binding of the `this` variable, but you can ignore those // for now. on('chat:message', (msg) => { if (msg.type !== 'api') return; // It's better to check that the string starts with the command. // Checking non-negative could end up matching a command like: // !makebutton !gwisp I pushed the button! // which would be unfortunate. if (msg.content.startsWith('!gwisp ')) return; let PlayerList = new Set(); // If nothing is selected, there won't be a .selected property // on the msg object. That would cause an error: // TypeError: Cannot read property 'forEach' of undefined // appending ||[] substitues an empty array for those cases. // Be sure the .forEach is called on the parenthesized expression (msg.selected||[]).forEach((obj) => { // Modern Javascript style prefers variables to be individually // declared. Since they are scoped and not hoisted with let and const // that means they come into existence as you need them, so you only need // to declare them up front if you're setting them in a tighter scope. let token = getObj('graphic', obj._id); if (token) { let character = getObj('character', token.get('represents')); if (character) { // get the comma separated list of players const controlled = character.get('controlledby').split(','); // loop through the players. controlled.forEach(c => PlayerList.add(c)); } } }); const TheMessage = msg.content.slice(7); if(PlayerList.has('all')) { sendChat('The Moderator', TheMessage); } else { PlayerList.forEach((player) => { let PlayerVar = getObj("player", player); let PlayerName = PlayerVar.get("_displayname"); // Javascript has a system called Template Literals that makes // building strings with embedded variables much nicer: sendChat('The Moderator', `/w "${PlayerName}" ${TheMessage}`); }); } }); });
1593292629
GiGs
Pro
Sheet Author
API Scripter
Oh yes, i should have recommended the on('ready') section, especially since i lost half an hour earlier the same day trying to figure out why some code wasn't working out properly, and that was the reason.
Holy heck everyone! That was an immense about of useful feedback, so thank you! I was slow to respond because yesterday I had to run my game, and it got long in the tooth (5.5 hours, and I think it best to keep it to 4). The script was definitely useful. One thing I am noticing about using a VTT is its a very McLuhanesque; that is to say, the style of role playing is defined by the communication medium. Being able to whisper details to specific players changes the role playing dynamic A LOT. Similarly, I find myself prepping lots of interactive tokens which whisper players.  I'm looking forward to finding more ways to make use of the API in the future and improve the player experience!  
1593404114
GiGs
Pro
Sheet Author
API Scripter
I'm glad you enjoyed the feedback. I do think this is a great idea for a script. I know I'll use it on occasion.