Events, and watching Attributes

Sheet workers all start something like this:

on('change:attribute', function() {Code language: JavaScript (javascript)

That first line is the event line, and it has some subtleties to be aware of and some special things you can do with it. It might also look like one of these:

on('change:attribute change:another_attribute sheet:opened', function() {
on('change:attribute', function(eventInfo) {Code language: JavaScript (javascript)

There are three obvious takeaways here, each of which is optional and might or might not be used:

  1. You can include more than one attribute at a time
  2. There is something called sheet:opened
  3. There is also something called eventInfo.

We’ll look at all three in this post and see how they are used. There are also some other things to understand, that are not at all optional and might be impossible to understand at all without help:

  1. Attributes are always lowercase – even if they are uppercase in the HTML.
  2. The event ‘change:attribute’ is a string – we can use that in clever ways.
  3. There is a difference when a sheet worker is launched manually or by another sheet worker.

Let’s look at how it all fits together.

Attributes in the Event Line

Sheet Workers know nothing about the character sheet that includes them. Say you want speed to be based on dexterity and strength. You need to know when one of those attributes change so you can recalculate the speed. That’s where the event line comes in. You are creating a sheet worker that watches the attributes listed, and runs whenever the stats change. So let’s say you have:

on('change:strength change:dexterity', function() {Code language: JavaScript (javascript)

This means that when strength or dexterity change (and only when a change occurs), the sheet worker runs. If you list an attribute that doesn’t exist (for example, when you misspell the attribute name), the sheet worker will never run. You can test if it’s working with logging. Here’s an example with a log statement to check if a worker is running, and a typo to show some cases it wouldn’t.

on('change:strngth change:dexterity', function() {
   console.log('strength and dexterity worker triggered');Code language: JavaScript (javascript)

If a worker isn’t triggering, and values aren’t changing when they should, logging like this is very helpful.

Attribute Names

Attributes must always be lower-case on the event line. Let’s say the above stats use upper case in the HTML:

<input type="text" name="attr_Strength" value="10" title="@{strength}">Code language: HTML, XML (xml)

This input creates the Strength attribute, and in HTML, Roll20 is case-insensitive, so you can use @{strength} or {Strength} or even @{stReNgTh} to access that attribute in macros. But in the event line of a sheet worker, you must write that as:

on('change:strength', function() {Code language: JavaScript (javascript)

This is easily missed because the sheet worker will sometimes work normally. But then one day, it fails without warning, and you have no idea why. Then when you change it to lower-case, it starts working again. So, always use lower-case to avoid this problem.

Similarly, you can include spaces in attribute names in HTML, but this is a very bad idea for sheet workers. Remember HTML and JavaScript are different languages, and some parts of Roll20 use one, and some parts use another. So, when building a character sheet, give your attributes names that will work in sheet workers, as described in Variables.

Stringing Attributes Together

You often want a sheet worker to fire when any of a bunch of stats change. Let’s say you have a sheet worker to calculate the total of your stats. If any one of the stats change, the total will change, so you need to monitor them all, like so:

on('change:str change:dex change:con change:int change:wis change:cha', function() {
   getAttrs(['str', 'dex', 'con', 'int', 'wis', 'cha'], function(values) {
      const str = +values.str || 0;
      const dex = +values.dex || 0;
      const con = +values.con || 0;
      const int = +values.int || 0;
      const wis = +values.wis || 0;
      const cha = +values.cha || 0;
      const sum = str + dex + con + int + wis + cha;
      setAttrs({
         stat_sum: sum
      });
   });
});Code language: JavaScript (javascript)

This sheet worker is pretty long to just add 6 stats. It can be significantly streamlined. Recall that you can use join to turn an array into a string. So you could do this:

const stats = ['str', 'dex', 'con', 'int', 'wis', 'cha'];
const stat_changes = stats.join(' change: ');
// gives 'str change:dex change:con change:int change:wis change:cha'Code language: JavaScript (javascript)

The join function only adds the stated string between each stat – you need something before the first one. But you can add strings together, so you can do this:

const stat_changes = 'change:' + stats.join(' change: ');
// gives 'change:str change:dex change:con change:int change:wis change:cha'Code language: JavaScript (javascript)

And then you can loop through each stat in the stats array, and add its value to the sum, like so:

const stats = ['str', 'dex', 'con', 'int', 'wis', 'cha'];
const stat_changes = stats.join(' change: ');

on(stat_changes, function() {
   getAttrs(stats, function(values) {
      let sum = 0;
      stats.forEach(function(stat) {
         const this_stat = +values.stat || 0;
         sum += this_stat;
      });
      setAttrs({
         stat_sum: sum
      });
   });
});Code language: JavaScript (javascript)

Notice how you can include a variable in the event line and the getAttrs line. So if you have that variable already, you might as well re-use it. if you later change the names of the stats, you only have to change the code in one place!

Change and Clicked

change: is used to watch for attribute changes. There is also a clicked: item, which is used to detect when an action button is clicked. You’ll learn about those soon. There is also sheet:opened.

Sheet:opened

If you include sheet:opened, this worker will run every time a specific character sheet is opened. So, if Bill, Bob, and Andrea each have characters, and Andrea opens her sheet, then that sheet worker will run for her and her alone. Bill and Bob’s sheets will be unaffected, until they open their character sheets and at that point the worker will run for them.

Be careful. If the sheet has a lot of workers each with their own getAttrs and setAttrs functions, they all have to query Roll20’s servers. This can create a lot of lag when a player in your game first opens a character sheet, and every time you open an NPC’s sheet.

So, you should only include sheet:opened in those workers that must run when a sheet is opened. There are very few workers like this (see Versioning below for one example). For most sheet workers, you don’t need it and should remove it.

Testing Sheets

When creating or working on a character sheet, it can be very handy to put sheet:opened in any workers you want to keep an eye on. You’ll often see if the sheet worker is doing what it is supposed to instantly. But once the sheet is ready for prime time, it’s good practice to remove them all, except for those few workers that really need it.

Versioning

Let’s say you have released a sheet including the attribute strength. But then you notice you have a typo, you sheet has the attribute sterngth. You want to fix that, but people are already using the sheet. if you simply correct that type, everyone will see their sterngth attribute vanish, and the new strength attribute will be reset to default. people will lose data.

You could easily add a sheet worker that transfers the sterngth value to the new strength attribute.

on('sheet:opened', function() {
   getAttrs(['sterngth'], function(stats) {
      setAttrs({
         strength: sterngth
      });
   });
});Code language: JavaScript (javascript)

But there’s a problem here. Every time the sheet is opened, the new strength attribute will be overwitten by the old sterngth attribute. If players increase their strength, that change will be negated next time they open the sheet.

A common solution to this kind of thing is the version attribute. You include a hidden version attribute.

<input type="number" name="attr_version" value="0">Code language: HTML, XML (xml)

Then when the sheet is opened, check the sheet’s version attribute.

  • If below the change’s version number, make the change and also update the version number.
  • If the version is at least equal to the change’s version number, do nothing.
on('sheet:opened', function() {
   getAttrs(['version', 'sterngth'], function(stats) {
      const version = +version || 0;
      if (version >= 1) { 
         return;
      }
      setAttrs({
         strength: sterngth,
         version: 1
      });
   });
});Code language: JavaScript (javascript)

In JavaScript, return means stop the code at this point. So the worker ends without making changes.

Version code can get much more complex than this, especially if you have a sheet with multiple changes over time. But this should give the basic idea.

Onwards

There’s more to events. In the next post, we’ll look at the eventInfo object, which lets you control more about a sheet worker.

Series Navigation

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.