setAttrs and Saving Attributes

If all you want to do is save attributes, that was covered way back in Anatomy of a Sheet Worker. But setAttrs has a few subtleties and extra functions we’ll cover here.

A Refresher on the Basics

If you want to save a single value, do this:

setAttrs({
   attribute: value
});Code language: JavaScript (javascript)

The order is attribute name: value and value can be a variable. You can save multiple attributes at once, like

setAttrs({
   attribute1: value1,
   attribute2: value2
});Code language: JavaScript (javascript)

The First Subtlety

If the attribute name is the same as the variable name, you don’t need to list them both. This is fine:

setAttrs({
   attribute
});Code language: JavaScript (javascript)

And so is this:

setAttrs({
   attribute1, 
   attribute2
});Code language: JavaScript (javascript)

You can only do this if the attribute name is identical to the variable name. This is one reason to name your variables the same as the attribute they represent.

Dynamic Attribute Names and Square Brackets

Your code sometimes doesn’t know the attribute name. Instead, you can use a variable name. For example, imagine you have a bunch of stats (str, dex, con), and each has a modifier named based on those (str_mod, dex_mod, con_mod). Using eventInfo, a single sheet worker can monitor all three stats, identify which one changed, and recalculate its modifier, like so:

on('change:str change:dex change:con', function(event) {
   getAttrs(['str', 'dex', 'con'], function(values) {
       const which = event.sourceName;
       // which tells us which attribute just changed
       const score = parseInt(values[which]) || 0;
       // here we use getAttrs; but we could use event.newValue just as easily.

       const mod = Math.floor(score/2)-5;
       // once we have calculated the modifier, we need to save it.
       setAttrs({
          [which + "_mod"]: mod
       });
   });
});Code language: JavaScript (javascript)

When you are saving a dynamic attribute (a variable), enclose it in square brackets.

setAttrs expects an Object

You might notice part of this is an object. The basic setAttrs statement is

setAttrs( );Code language: JavaScript (javascript)

And you supply an object to it.

{
  attribute: variable
}Code language: JavaScript (javascript)

That means you can create objects and supply them to setAttrs. Imagine you create an object like:

let output = {};
output.attribute = variable;
setAttrs(output);Code language: JavaScript (javascript)

This looks a bit much for one attribute, but when you have multiple attributes, and you don’t know what they are, this technique is very valuable, like:

let output = {};
output[attribute1] = variable1;
output[attribute2] = variable2;
output[attribute3] = variable3;
setAttrs(output);Code language: JavaScript (javascript)

When working with variables and dynamic attribute names, the second approach might be required. When working with a lot of attributes, it’s always easier. And finally, if you want to debug the values of attributes with console.log, it’s easier to create the variable before setAttrs runs.

So, to refresh, while getAttrs creates a JavaScript Object, setAttrs expects to receive an object. The bit inside the curly brackets is an object. These two bits of code are equivalent:

setAttrs({
   attribute: 10
});Code language: JavaScript (javascript)
const output = {attribute: 10};
setAttrs(output);Code language: JavaScript (javascript)

Silent Workers

Quite often, you might rely on something called cascading. Imagine a sheet worker that calculate a stat bonus. Then when that stat bonus changes, all skills and saves using that stat bonus are recalulated. This is a simple cascade – one sheet worker triggers another.

There are times you don’t want such a cascade to run. In those cases, setAttrs is said to be silent. You supply silent:true to the worker after any attributes are set, like so:

setAttrs({
   attribute1: value1, 
   attribute2: value2,
   {silent: true}
});Code language: JavaScript (javascript)

Just make sure it comes after all attribute assigments. You can do with with a variable too:

setAttrs({
   output,
   {silent: true}
});Code language: JavaScript (javascript)

Using eventInfo to block Cascades

There are times you might want sheet workers to cascade unless certain conditions are true. You can use eventInfo to check for certain conditions – a common one is whether a worker was triggered by a player or by another sheet worker.

on('change:attribute,function(event) {
   if(event.sourceType === 'sheetworker') return;
   /* rest of sheet worker */
});Code language: JavaScript (javascript)

This kind of thing is handy to block a worker triggering itself. Imagine the player changes an attribute. The worker is triggered, it changes itself, which triggers the worker again. But this time, it’s not a manual change – it’s caused by the sheet worker firing, and so is blocked.

Cascading Sheet Workers

I’ve mentioned the possibility of cascading sheet workers above. I usually build my sheets to rely on cascades. But if you have very heavy, very complex sheets with hundreds of values that each trigger other sheet workers, cascading sheet workers can cause lag.

If you want to avoid this, one thing you can do is do more work in each sheet worker. One common case is, you have a worker that calculates a stat bonus, which then triggers 20 skill values to calculate a new score wth that new attribute modifier. You could instead, have a sheet worker that triggers when any attribute is changed, recalculates all stat modifiers, then because the new modifier is known, calculates all the skill values in that worker.

This takes a bit more care to build but is much more efficient. You have to be careful to grab (using getAttrs) all the values you need (you may need all the skill levels and modifiers, for example).

You will probably need to add all those values to the change row, too, so that you can calculate all the scores whenever any of the constituent attributes change. Bear in mind, your sheet workers can be as complex as you want them to be – modern computers are so powerful, there will be no speed issue. The only thing that slows your game down are the asynchronous functions (getAttrs and setAttrs), and they take the same amount of time whether affecting one attribute or a hundred.

So building complex workers and avoiding cascades is more efficient. Cascades are a lot easier to write, though.

When building such a complex worker, it’s handy to put all the attributes in variables first, to make the event and collection lines easier. For example:

const stats = ['str', 'dex', 'con', 'int', 'wis', 'cha'];
const skills = {
    athletics: str,
    knowledge: int,
    /* a whole bunch of skills */
};
// the following expects entries to be an array
function build_changes(entries) {
   let changes = '';
   entries.forEach(function(entry) {
      changes += `change:${entry.toLowerCase()} `;
   });
   return changes;
}; // this function takes an array of attributes and creates a 'change:attribute' string , function() {
on(`${build_changes(stats)}${build_changes(Object.keys(skills))}`, function() {
   getAttrs([...stats, ...Object.keys(skills)], function(values) {
     /* the rest of the worker */Code language: JavaScript (javascript)

This function lets you avoid writing out a long list of change:this change:that for 20 or more values. The build_changes function can be inserted in the event line and will construct them for you.

The Object.keys(object_name) function creates an array of the keys of an object. We need an array in the event line and the getAttrs line, so that creates them.

Finally the spread operator, , breates an array into its individual elements. If you want to combine two arrays, you can do that like […array 1, ….array2], which is very handy.

Callback Functions

The final thing you can do with setAttrs is create a callback function after the {silent} object.

setAttrs({
   output,
   {silent: false},
   function_name()
});Code language: JavaScript (javascript)

A function declared here will not run until after the setAttrs is completed. You can make sure that attributes have been properly updated before this function runs. This is very handy for things like version updates, where you might want several functions to run in sequence and want to make sure each is completely finished before the next one starts.

This is a big topic that will need another post, top be discussed later. For now, I’ll let you know the option exists. One thing to be aware of here: this callback function must be placed after the silent item. If you omit it, there must still be a comma to show it might have been there, like this:

setAttrs({
   output,
   ,
   function_name()
});
setAttrs({
   output, , function_name()
});Code language: JavaScript (javascript)

Both of the versions above are fine. The extra lines in the first one are just for clarity. It’s a good thing to write your code in a way that those who come after you find it readable.

So that’s setAttrs. That’s probably more information than you wanted! i hope it’s useful.

Series Navigation

Leave a Reply

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