“What Can I Do” Code How-To

Recently, I’ve been playing dungeons and dragons on Roll20. Playing with new players, one question that keeps coming up is: what can I do?

this question came up so often that I wanted to build a macro in roll20 to answer it. And so I did. And so this post is how I did it, my learning about the roll20 API along the way, and how you could use the same techniques to build your own macro.

Video Overview

Take a quick watch of this video overview of how I built the macro. Then will get into the step-by-step details and building blocks of how I did it.

Ok, first let’s talk about the relationship between macros and APIs.

An API is a code process in Roll20 to perform some action in the Roll20 system. So think about the API as wiring in your house. A Macro is the light switch. You use a macro to trigger an API. In this case the “What Can I Do?” API will build out the list of actions, format the actions in HTML and show the HTML content through the Chat. The macro is just a button that executes the API to do all that stuff.

APIs in Roll20 are Javascript code blocks. Start with the following:

on("chat:message", function(msg){
    if(msg.type == "api" && msg.content.indexOf("!whatcanido") == 0){
        // code will go here
        // send message to user goes here
    }
});

This is a basic chat message listener that is watching for some keyword (in this case “!whatcanido”) to be typed into the chat box. The msg.type == “api” means the message isn’t coming from a user but it is coming from a macro.

Now, let’s grab information about who pressed the macro button (remember in Roll20, Macros can be in the Macro Bar which creates a button on the screen). Put the following into the //code goes here area:

var who = [msg.playerid, msg.who];
var html = makingAdventure.whatCanIDo(who);

This first sets “who” to be an array of the playerid (later used to identify who is controlling the character) and msg.who which is the name of the person who pushed the macro button (used when we respond in chat so only the person who requested “what can I do” is told what they can do.

The line makingAdventure.whatCanIDo(who) generates the HTML output for our message. Let’s create a placeholder for that now. Outside of the “on” function you are writing, add the following code:

var makingAdventure = makingAdventure || {};
makingAdventure.whatCanIDo = function(who){
    var output = "hello!";
    return output;
}

Now, one last thing to make your code work, you need to respond to the user who pushed the macro. Go back to the “on” function and replace the // send message to user comment with the following:

sendChat(msg.who, html, null, { noarchive : true });

Now you have the shell of an call and respond API. The API takes who sent a message, prepares some HTML and then displays that HTML in the chat window. Any API that is a send information, get information type API can start with this framework. Just change the !whatcanido to be the name you want to use.

Getting more Action Oriented

Let’s flesh out the whatCanIDo function to make it actually produce something useful. Create a new function outside of all the other functions with the following code:

makingAdventure.action = function(name, description){
    return {
        "name": name,
        "description": description
    }
}

makingAdventure._formatActions = function(actions, playerId, characterName){
    var output = "";
    _.each(actions, function(action){
        output += "<a href='!whatisthataction|{0}|{1}|{2}|{3}'></a> ".format(playerId, action.name, makingAdventure._cleanseText(action.description), characterName);
    });
    return output;
}

makingAdventure._getBasicActions = function(playerId, characterName) {
    var actions = [
        new makingAdventure.action("Attack", "Going on the offensive with melee or ranged weapons."),
        new makingAdventure.action("Dash", "Doubling your movement for a turn."),
        new makingAdventure.action("Disengage", "Move away from an attacker and escaping attack for the turn."),
        new makingAdventure.action("Dodge", "Focusing on avoiding attack making your attacker roll at Disadvantage."),
        new makingAdventure.action("Help", "Help another character to achieve a task. The character gets Advantage to complete that task."),
        new makingAdventure.action("Hide", "Use your Stealth ability to hide from other characters."),
        new makingAdventure.action("Ready", "Allows you to prepare to react to a perceivable circumstance before your next turn."),
        new makingAdventure.action("Search", "Try to find something using your perception (wisdom)."),
        new makingAdventure.action("Use Object", "Interact with some object in your inventory.")
    ];

    return makingAdventure._formatActions(actions, playerId, characterName);
}

One at a time here. First, I create a simple “Action” object (makingAdventure.action) that I can use to keep my data structured. Next, I create a helper function (makingAdventure._formatActions) that will take the Action object and format it into an anchor tag. This function is about standardizing the output of the HTML that will come out of our API.

Finally, we use action and _formatActions to create a list of known and common D&D actions. This is just content from the Players Handbook paraphrased for quick reading. Now replace “hello” in the makingAdventure.whatCanIDo function to the following:

var output = "<h3>Basic Actions</h3><p></p>"
        .format(makingAdventure._getBasicActions(who[0], who[1]));

Basic actions are all set. Two more helper functions you’ll need:

makingAdventure._cleanseText = function(input){
    var output = input.replace(/\n/g, "!br!");
    output = output.replace(/\"/g, "&quote;");
    output = output.replace(/'/g, "&quote;");
    output = output.replace(/:/g, "-_-");
    return output;
}
makingAdventure._restoreText = function(input){
    var output = input.replace(/!br!/g, "<br/>");
    output = output.replace(/&quote;/g, "'");
    output = output.replace(/-_-/g, ":");
    return output;
}

These are going to be useful in the next section of code. The first function (makingAdventure.cleanseText) takes out content that will create issues for the anchor tag as well as format to a consistent line break (using the !br! placeholder). This is not an exhaustive list of all the replacings that need to happen but it is a start for basic/simple testing. The second function (makingAdventure.restoreText) but all the stuff we cleaned out back in.

More Complicated Actions

Characters can do more than basic actions. They have a bunch of other stuff too. That other stuff is in their Traits section of the character sheet (for D&D 5e). I wanted to make sure I grabbed that content too.

This was not as easy.

So, let’s get started. Create the following function:

makingAdventure._getCharacterActions = function(playerId, characterName) {
    var sendingCharacter = findObjs({ type: "character", controlledby: playerId})[0];
    var attrs = findObjs({ _type: "attribute", _characterid: sendingCharacter.id })
    var actions = new Array();
    var i = 0;
    _.each(attrs, function(item){
        item = JSON.parse(JSON.stringify(item));
        var s = new String(item.name);
        if(s.startsWith("repeating_traits")){
            if(s.endsWith("_name")){
                actions.push(new makingAdventure.action(item.current, ""));
            } else if(s.endsWith("_description")){
                actions[i].description = item.current;
                i++;
            }
        }
    });

    return makingAdventure._formatActions(actions, playerId, characterName);
}

Starting at the top, first I find the character controlled by the player according to the PlayerId. This is so I can get all the attributes (next line) for that character sheet. Then, after some simple declarations, I use underscore (javascript _ library) to loop through each attribute. I stringify the item then parse that string to JSON. Yes, I know this seems strange but it wasn’t working if I didn’t do this stringify THEN parse approach (the attributes of the item object were not being recognized as attributes - i.e. item.name was undefined).

With the item ready, we check the item name to see if it starts with repeating_traits. If it does, then we check to see if the item name ends with “name” or “description”. Each repeating_trait item is actually multiple “items” with different ids. So the item.name could be repeating_trait_12312344f_name and the next item.name could be repeating_trait_12312344f_description so we are creating a new Action item in the array of actions on endsWith(“_name”) and completing the Action item with a description on endsWith(“_description”). After we have a description for the action, we move on to the next new Action.

All together now…

Here is all the code together as I have it in Roll20:

if (!String.prototype.format) {
    String.prototype.format = function () {
        var args = arguments;
        return this.replace(//g, function (match, number) {
            return typeof args[number] != 'undefined'
                ? args[number]
                : match
                ;
        });
    };
}

var makingAdventure = makingAdventure || {};

///<region>Private Functions</region>
makingAdventure._getBasicActions = function(playerId, characterName) {
    ///<summary>Gets a list of basic actions</summary>
    ///<param type="string" name="playerId">ID of the player who made the request</param>
    ///<param type="string" name="characterName">Name of the character requesting the action</param>
    ///<returns type='string'>Returns an HTML list of actions</returns>
    var actions = [
        new makingAdventure.action("Attack", "Going on the offensive with melee or ranged weapons."),
        new makingAdventure.action("Dash", "Doubling your movement for a turn."),
        new makingAdventure.action("Disengage", "Move away from an attacker and escaping attack for the turn."),
        new makingAdventure.action("Dodge", "Focusing on avoiding attack making your attacker roll at Disadvantage."),
        new makingAdventure.action("Help", "Help another character to achieve a task. The character gets Advantage to complete that task."),
        new makingAdventure.action("Hide", "Use your Stealth ability to hide from other characters."),
        new makingAdventure.action("Ready", "Allows you to prepare to react to a perceivable circumstance before your next turn."),
        new makingAdventure.action("Search", "Try to find something using your perception (wisdom)."),
        new makingAdventure.action("Use Object", "Interact with some object in your inventory.")
    ];

    return makingAdventure._formatActions(actions, playerId, characterName);
}

makingAdventure._getCharacterActions = function(playerId, characterName) {
    ///<summary>Gets a list of actions based on the character sheet</summary>
    ///<param type="string" name="playerId">ID of the player who made the request</param>
    ///<param type="string" name="characterName">Name of the character requesting the action</param>
    ///<returns type='string'>Returns an HTML list of actions</returns>

    var sendingCharacter = findObjs({ type: "character", controlledby: playerId})[0];
    var attrs = findObjs({ _type: "attribute", _characterid: sendingCharacter.id })
    var actions = new Array();
    var i = 0;
    _.each(attrs, function(item){
        item = JSON.parse(JSON.stringify(item));
        var s = new String(item.name);
        if(s.startsWith("repeating_traits")){
            if(s.endsWith("_name")){
                actions.push(new makingAdventure.action(item.current, ""));
            } else if(s.endsWith("_description")){
                actions[i].description = item.current;
                i++;
            }
        }
    });

    return makingAdventure._formatActions(actions, playerId, characterName);
}

makingAdventure._cleanseText = function(input){
    var output = input.replace(/\n/g, "!br!");
    output = output.replace(/\"/g, "&quote;");
    output = output.replace(/'/g, "&quote;");
    output = output.replace(/:/g, "-_-");
    return output;
}
makingAdventure._restoreText = function(input){
    var output = input.replace(/!br!/g, "<br/>");
    output = output.replace(/&quote;/g, "'");
    output = output.replace(/-_-/g, ":");
    return output;
}

makingAdventure._formatActions = function(actions, playerId, characterName){
    var output = "";
    _.each(actions, function(action){
        output += "<a href='!whatisthataction|{0}|{1}|{2}|{3}'></a> ".format(playerId, action.name, makingAdventure._cleanseText(action.description), characterName);
    });
    return output;
}
///<region>End Private Functions</region>

///<region>Public Functions</region>
makingAdventure.action = function(name, description){
    return {
        "name": name,
        "description": description
    }
}

makingAdventure.whatCanIDo = function(who){
    ///<summary>Gets a list of what the character can do</summary>
    ///<param type='object' name='who'>The id of the player and name of the character asking what they can do</param>
    ///<returns type='string'>HTML string of actions that can occur</returns>
    var output = "<h3>Basic Actions</h3><p></p><h3>Character Actions</h3><p></p>"
        .format(makingAdventure._getBasicActions(who[0], who[1]), 
                makingAdventure._getCharacterActions(who[0], who[1])
        );
    return output;
}
///<region>End Public Functions</region>

///<region>Events</region>
on("chat:message", function(msg){
    if(msg.type == "api" && msg.content.indexOf("!whatcanido") == 0){
        var who = [msg.playerid, msg.who];
        var html = makingAdventure.whatCanIDo(who);
        sendChat(msg.who, "/w  ".format(msg.who, html), null, { noarchive : true });
    }
});

on("chat:message", function(msg){
    if(msg.type == "api" && msg.content.indexOf("!whatisthataction") == 0){
        var contents = msg.content.split("|");
        var playerId = contents[1];
        var actionName = contents[2];
        var actionDesc = contents[3];
        var characterName = contents[4];
        var output = "<div class='sheet-rolltemplate-traits'><div class='sheet-row sheet-header'>";
        output += "<span></span>".format(actionName);
        output += "</div><div class='sheet-row'><span class='sheet-desc'></span></div></div>".format(makingAdventure._restoreText(actionDesc));
        sendChat(msg.who, "/w  ".format(msg.who, output), null, { noarchive : true });
    }
});
///<region>End Events</region>

This is not a masterpiece of code :) I built it in a few hours (took a while to figure out some of the Roll20 API documentation). This code was really more about me exploring building APIs in Roll20 and providing a useful tool for my players. I hope this is useful to you and your players as well.

Thanks for stopping by!