Introduction to Macro Writing With Javascript
BEGINNER
THIS IS A BEGINNER ARTICLE
Introduction
Since MapTool 1.10 there is a new JavaScript environment that allows the use of JavaScript to write macros. This is a tutorial for anybody that hasn't used Javascript in MapTool before. This tutorial assumes no prior knowledge of Javascript, programming or macro writing in general. For reasons why you would want to write a macro see the start of Introduction to Macro Writing
MapTool Environments
The first thing that any person wanting to write JavaScript for MapTool should know is that there are 2 different Javascript environments in MapTool. The first is for macros, related to the js.eval() family of functions, the other Javascript environment lives inside the frames and overlays as part of HTML. To avoid confusion the first JavaScript environment is referred to as "GraalVM JavaScript" and the second "Frame JavaScript". This tutorial focuses on GraalVM Javascript so assume any reference to only Javascript is a reference to GraalVM Javascript.
Setting Up Macro Environment
You can just write Javascript inside a string to be evaluated by js.eval(). If you want to do anything slightly more complex than a single line this approach quickly becomes unwieldy, so setting up a area to develop complex macros is a good idea. For this tutorial, do this by following these steps;
- create a Library Token
- set allows URI acess to true
- create a macro with Javascript
- execute it.
If you are unsure of how to do any of these steps here is a verbose description of the procedure;
- pick a token and change its name to start with lib: like lib:javascript-tutorial
- still inside the edit token window go on the "Lib:Token properties" tab and click the checkbox so that allows URI access has a checkmark besides it
- to create a macro on the lib token first make sure you have a token related panel
- to show a token panel on the top of the window on the menu bar click on window and then select either impersonate or selected (if you selected the impersonate panel make sure to impersonate the library token)
- to add a new macro, right click on the panel and select "Add New Macro" from the context menu
- to edit the macro, right click it and select "Edit.." from the context menu
- in the second tab you can change its name from (new)
- To execute the text inside a macro on a lib token create another macro and inside it call the js.evalURI() function
- to call the function write
[r:js.evalURI("javascript-tutorial",lib URI)]
- a lib URI to a macro is a link of the form lib://libraryTokenName/macro/macroName so if you lib token is lib:javascript-tutorial and your macro is js then to refer to it the link is lib://javascript-tutorial/macro/js
- the first argument is the namespace try to use a unique one or it may cause problems with other macros written in javascript
- to call the function write
As an alternative to using the built-in macro editor you can use a Javascript IDE or a text editor, they can be an immense help. If you are editing outside MapTool and then copy pasting your code, consider Creating A MapTool Addon.
First Javascript Program
This is the part where you start to learn Javascript, if you already know Javascript you can read the following articles to get started js:MapTool , js:MapTool.chat ,js:MapTool.tokens , js:MapTool.clientInfo and last but most important js:MTScript. Javascript is a multi-paradigm programming language the exact meaning of this isn't important what is important is that we can treat it as a imperative programming language. An imperative program is just a sequence of commands. Insert the following in your macro holding javascript.
"use strict";
try {
let message = "hello world";
MapTool.chat.broadcast(message);
} catch(e) {
MapTool.chat.broadcast(""+e+"\n"+e.stack);
}
if you inserted the text correctly, when executed hello world appeared in the chat, if you got something else check the text carefully, code is very fragile and a single misplaced { or } could break it.
this snippet contains a lot of things which need to be explained, most of them will be explained now but the try and the catch will be explained next section
Javascript Statements
Firstly look at the end of each line besides the second and the and fifth one, notice that there is a semicolon. The semicolon denotes the end of a statement. In this instance is optional as each statement is in a separate line but it is good practise to include it. If you don't an automated program inserts it automatically and it may not get it right, which you certainly don't want. a statement is basically a command, if you rolled on MapTool before you probably used a command, this is the same thing, a statement does something.
Javascript Strict Mode
on the first line there is a statement which causes Javascript to enter strict mode, on strict mode Javascript turns things which are probably mistakes into errors which if you are inside a try catch block will give what is wrong and where, the full list of what happens can be found here but just to talk about a example say you mistyped something Javascript normally won't complain and continue using a value called undefined, creating subtle bugs, compare that to getting "on line x column y you asked for a value which doesn't exist."
Javascript variables
On the third line we have a statement containing let message = "hello world";
what this does is create a variable. A variable stores a changeable value so instead of using "hello world" we can use message, you can declare a variable with var, let or const. A variable name must start with a letter or underline and may contain numbers, letters and underline, a variable name can't be any the Javascript keywords like let, const, var and more.
In the last paragraph it was mentioned that a variable can hold a value, this value can be one of the basic types in Javascript they are
- string: a string is a fragment of text enclosed in either quotes or double quotes, a string enclosed with backticks is called a [template literal] which is special.
- number: a number is a number they can be either integer like -1, 3 or it can be fractional number like 0.2, 0.1 3.5 they are called floating point numbers do note that because of their representation in the computer using floats does have some imprecision 0.1 + 0.2 isn't equal to 0.3 but rather 0.30000000000000004. this also happens for really big integers as they are also floating point numbers
- objects: a object is a group of properties with associated values, they are created with curly braces enclosing the properties and pairs of propertyName:propertyValue like this
{property1:value1,property2:2}
, only strings, a special type called Symbol and numbers can be properties but anything can be a value in an object,this includes other objects and functions. To access a object properties you use ["property"] or .property after the object, there is a lot more to know about objects but for now this is enough. - arrays: a array is a collection of values, the literal form of a array is braces with values like this
[value1,value2,etc]
there are special functions to working with arrays like objects there is a lot more to them. - functions: they will be talked about more later but note that a function can be assigned to a variable and later called by that name.
Javascript Functions
On the fourth line there is MapTool.chat.broadcast(message);
which is a function call. A function is basically a group of statements which can optionally receive arguments. when you call a function you supply the arguments and it performs the statements it contains. In this case we are calling the broadcast function of the chat object which is contained in the MapTool object. This function calls java code to output a string to chat, nb: if you give as argument something other than a string like a number or a object you will get an error. To declare a function use
function functionName (argument1,argument2,etc){
insert your statements here;
}
Javascript Control Structures
You may have noticed that with the tools we have currently it isn't possible to build anything complex. For instance: What if we want check if a roll is higher than the targets DC and if so roll an attack? What if we want to deal damage to all characters in the map? Even worse, what if we make an error like dividing by 0? Control structures solve this problem, see the following example of implementing a fireball spell for d&d
"use strict";
function roll(dice,sides){
let string = MTScript.execMacro(`[r:roll(${dice},${sides})]`);
return Number(string);
}
var checkNumber = (value => Number.isNaN(value)||value == undefined);
function makeSave(token,spellLevel,spellDC){
const dex = Number(token.getProperty("dexterity"));
let message = token.getName()+":"
let dexModifier = (dex-10)/2;
if(checkNumber(dexModifier)){
dexModifier = 0;
}
const result = dexModifier + roll(1,20);
let previousLife = Number(token.getProperty("HP"));
if(checkNumber(previousLife)){
previousLife = 0;
}
let newLife;
const damageRoll = (roll(8,6)+roll(spellLevel-4,6))
if(result >=spellDC){
message = message +"succeded on its dex save with "+result;
newLife = Math.ceil(previousLife-damageRoll/2);
}
else{
message = message + "failed on its dex save with "+result;
newLife = previousLife -damageRoll;
}
token.setProperty("HP",""+newLife);
MapTool.chat.broadcast(message + " and is now at "+newLife+"HP");
return token;
}
function getInput(names){
let commandStart = "[h:input(";
let commandEnd = `[h:value = "[]"]`;
const argNum = names.length -1;
for(let i = 0; i<argNum;i++){
commandStart = commandStart +`"${names[i]}||${names[i]}|TEXT|",`
commandEnd = commandEnd + `\n[h:value = json.append(value,${names[i]})]`
}
commandStart = commandStart +`"${names[argNum]}||${names[argNum]}|TEXT|")]`;
commandEnd = commandEnd + `\n[r:json.append(value,${names[argNum]})]`;
return JSON.parse(MTScript.execMacro(commandStart+commandEnd));
}
try{
const tokens = MapTool.tokens.getMapTokens();
const [spellLevel,spellDC] = getInput(["spellLevel","spellDC"]);
for(const token of tokens){
makeSave(token,spellLevel,spellDC);
}
}
catch(e){
MapTool.chat.broadcast(""+e+"\n"+e.stack);
}
Javascript try and catch
Like the previous example this code uses a try catch block. In a try block if any part of the code throws a exception then the catch block is executed. The variable inside parenthesis near the catch keyword represents the error. In MapTool and most browsers Error objects contain a [stack] property that indicates from where it was thrown. When programming in Javascript try to always have at least a try catch block so you get useful errors. If you don't then you will get a generic there is something wrong in this code.
Javascript If
In the makeSave function we have various ifs keywords, when the result inside the parenthesis of the if is truthy it executes the code otherwise it executes the else if present. A truthy result, is either true, a number other than 0, a non empty string or any object including a empty object. A falsy result is the empty string, 0, null and undefined. related to the if keyword there are comparison operators == checks for equality but converts types 0 == "0" evaluates to true. to truly check for equality use the === operator the opposite is the not equal operator != like the basic equality operator it converts types so 0 != "0" is false to check for inequality without conversion use !== there also are comparison operators for numbers like greater than >, greater or equal than >= and others.
Javascript For
On the getInput function there is a use of a for keyword, in the basic for block you define a variable, declare an exit condition, and finally declare a statement to be executed at the end of the loop, it will execute the code block inside the for loop until the end condition is reached. Besides that there is a for of and a for in block. The difference is that the for of loops over the elements of an iterable meaning array like objects, and the for in loops over enumerable properties meaning visible properties of a object.
Complete Explanation Of Code Example
This code block:
- defines some functions, then
- in a try block gets all tokens on the map from MapTool.token.getMapTokens,
- information of the cast fireball from the get input function
- it then loops over all tokens with the information of the fireball spell.
Roll and Check Number Function
The roll function calls some MapTool Script to roll dice, via the MTScript.execMacro function. The checkNumber function checks if the entered number is either undefined or the special number NaN, it uses the "or" || operator there also is the "and" && logical operator. The check number function is a example of a arrow function, an alternative syntax for defining functions, as the checkNumber function is only a single statement it does not require curly braces due to the arrow notation.
MakeSave Function
The makeSave function implements the logic of a fireball spell, it shows an example of building a message by parts to later use in a call to MapTool.chat.broadcast.
GetInput Function
The getInput function builds a complex macro expression to call the input function at the end, it uses a for loop to do so, this get input function could be reused for any number of inputs, it also is an example of using the JSON builtin object to transform a textual json object into a javascript object.
Javascript Macro Writing Tips
Always Use Try Catch Blocks
if you don't you will get cryptic error messages that give no information
Calling A Previously Defined Function In A Namespace
When you define a global variable it lives in the Javascript namespace until the campaign is reloaded. you can just add MTScript.registerMacro(macroName, function)
to define a UDF named js.macroName that calls the function, note however that as of 1.11.5 you can't access json objects received from Maptool