Introduction to Macro Writing With Javascript/de

From RPTools Wiki
Jump to navigation Jump to search

Languages:  English  • Deutsch  • 日本語

BEGINNER
THIS IS A BEGINNER ARTICLE

Einleitung

Seit Version 1.10 bietet Maptool eine JavaScript-Umgebung, um Makros auch in dieser Sprache entwickeln zu können. Dieses Tutorial wendet sich an Anwender, die bisher noch keine Makros mit Javascript in Maptool geschrieben haben. Es werden keine Kenntnisse in der Programmierung mit Javascript, Programmierung allgemein oder aich nur Makroerstellung erwartet. Warum Makros überhaupt interessant für dich sein könnten, erfährst du im Artikel zum Makros schreiben


Programmierumgebungen

Das Erste, was man zur Orientierung über das Programmieren in MapTool wissen sollte ist, dass es mehrere Umgebungen zur Erstellung von Makros gibt. Neben der althergebrachten Möglichkeiten in MTScript (die auch mittels JavaScript genutzt werden können), hat MapTool nun auch zwei JavaScript-Umgebungen eingebunden. Die Eine ist die mit der js.eval()-Familie verfügbaren Funktionen. Die Andere wird über Frames und Overlays in HTML bereitgestellt. Um Verwirrung zu vermeiden, sprechen wir bei der erstgenannten Variante von Javascript von "GraalVM JavaScript",die zweite "Frame JavaScript". Dieser Text beschäftigt sich mit GraalVM Javascript, auch wenn nur von Javascript gesprochen wird.

Die Makroumgebung

Auch wenn man einfach Anweisungen mitjs.eval() in eine Klammer packen kann, wird das schnell unübersichtlich. Grössere Projekte lassen sich besser in einer Bibliotheks-Spielmarke (Lib-Token) verwalten. Um so ein Projekt zu erstellen sind die folgenden Schritte notwendig:

  1. erstelle einen Library Token
  2. setze in den Einstellungen "allow URI acess"
  3. erstelle ein Javascript-Makro
  4. führe es aus.

Solltest du bei einem dieser Punkte nicht wissen wie es geht, dann lies die folgende, ausführliche Beschreibung.

  1. Erstelle eine Spielmarke und ändere ihren Namen so, dass er mit lib: beginnt, also etwa lib:javascript-tutorial
  2. Setze im Bearbeiten-Dialog im Reiter Eigenschaften die Option "URI-Zugriff zulassen".
  3. Um ein Makro in die Bibliothek zu schreiben brauchst du nun ein Editor-Fenster. Das kann das Ausgewählt- oder Verkörpert-Fenster sein.
    1. Um in die richtige Spielmarke zu schreiben, sollte natürlich die Bibliothek ausgewählt oder verkörpert sein.
    2. Mit einem Rechtsklick auf das Makrofenster öffnest du ein Kontextmenü, in dem du mit "Neues Makro hinzufügen" ein neues Makro erstellen kannst.
    3. Ein weiterer Rechtsklick auf die neu entstandene Makroschaltfläche lässt dich jetzt das Makro "Bearbeiten...".
    4. Im zweiten Reiter Details kannst du den Namen von 'Neu' auf etwas passenderes ändern
  4. Um das Makro in einem Lib Token zu verwenden brauchst du dann noch ein weiteres Makro, in dem dein vorher erstelltes mit der Anweisung js.evalURI() aufgerufen wird. Dieser Aufruf sieht dann etwas so aus:
    [r:js.evalURI("javascript-tutorial",lib URI)]

URI bezeichnet einen "Uniform Resource Identifier", also eine eindeutige Quelle - in diesem Fall für das Makro.

  1. Der Platzhalter "lib URI" in der Anweisung wird also gegen einen Link nach dem Muster lib://libraryTokenName/macro/macroName ersetzt. Heißt dein Lib Token also lib:javascript-tutorial und dein Makro js, dann wäre der Link lib://javascript-tutorial/macro/js.
  2. Das erste Argument der Anweisung ist der Namensraum, den du individuell wählen solltest.

Statt des Makro-Editors von MapTool kannst du auch eine Javascript IDE oder einen Texteditor verwenden. Deren Auszeichnung der Elemente kann eine große Hilfe sein. Bearbeitest du deinen Code außerhalb von MapTool, dann könnte ein MapTool Addon eine gute (und modernere) Alternative für dich sein.

Das erste Javascript Makro

Hier beginnt die Einführung in Javascript. Wenn du dich damit schon auskennst, dann kannst du auch gleich bei js:MapTool , js:MapTool.chat ,js:MapTool.tokens , js:MapTool.clientInfo und nicht zuletzt auch js:MTScript weiterlesen. Da Programmierung ohne Englischkenntnisse kaum möglich ist, geht es hier auch englisch weiter. Schreib mir (dwian) über Discord, wenn du das sehr doof findest.

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

See also