Character Vault
Any Concept / Any System
Compendium
Your System Come To Life
Roll20 for Android
Streamlined for your Tablet
Roll20 for iPad
Streamlined for your Tablet

Personal tools

Difference between revisions of "Sheet Worker Scripts"

From Roll20 Wiki

Jump to: navigation, search
(Adding a Sheet Worker Script)
(Adding a Sheet Worker Script)
Line 23: Line 23:
 
'''Warning about "global" variable scope:''' the scope of a variable declared ''inside'' the `<script type="text/worker">` but ''outside'' a function are currently per '''player''', not per character. Changing its value will change it for all the characters of your player's session.
 
'''Warning about "global" variable scope:''' the scope of a variable declared ''inside'' the `<script type="text/worker">` but ''outside'' a function are currently per '''player''', not per character. Changing its value will change it for all the characters of your player's session.
  
As a best practice Asynchronous cascades whenever possible. It is generally a better practice to get all attributes needed for calculations in one getAttrs rather than doing getAttrs -> calculations -> setAttrs -> getAttrs -> calculations -> setAttrs…
+
As a best practice Asynchronous cascades should be avoided whenever possible. It is generally a better practice to get all attributes needed for calculations in one getAttrs rather than doing getAttrs -> calculations -> setAttrs -> getAttrs -> calculations -> setAttrs…
  
 
==Sheet Workers vs. Auto-Calculating Fields: Which should I use?==
 
==Sheet Workers vs. Auto-Calculating Fields: Which should I use?==

Revision as of 21:22, 26 September 2018

Sheet Worker Scripts are an advanced feature of the Character Sheets system which allows the sheet author to specify JavaScript which will execute during certain events, such as whenever the values on a sheet are modified.

Contents

Adding a Sheet Worker Script

To add a sheet worker script to a Character Sheet, simply add the script into the "HTML" section of the sheet using the following format:

<script type="text/worker">

on("change:strength", function() {

});

// ... etc

</script>

Note that the script tag must have a type of "text/worker" to be properly handled by the system. When the game loads, all script tags are removed from the sheet's template for security purposes. However, script tags with a type of "text/worker" will be used to spin up a background worker on each player's computer that can respond to changing values in their sheets and take action as needed. All sheetworker scripts need to be contained within a single script tag.

More comprehensive examples of the basics aimed to non-programmers

Warning about "global" variable scope: the scope of a variable declared inside the `<script type="text/worker">` but outside a function are currently per player, not per character. Changing its value will change it for all the characters of your player's session.

As a best practice Asynchronous cascades should be avoided whenever possible. It is generally a better practice to get all attributes needed for calculations in one getAttrs rather than doing getAttrs -> calculations -> setAttrs -> getAttrs -> calculations -> setAttrs…

Sheet Workers vs. Auto-Calculating Fields: Which should I use?

There's no hard-and-fast rule about this. Both of these tools are capable of achieving the same thing. Let's take a hypothetical use-case of a "Strength" attribute, which we want to use to keep a "Strength Mod" attribute updated. Here would be the differences between the two tools for this use case:

  • The auto-calculating fields are all re-calculated every time a sheet is opened for the first time. The Sheet Worker fields, on the other hand, only recalculate when their dependent values change. This means that sheets utilizing the Sheet Worker option will be much, much faster for players to open and interact with.
  • In addition, the Sheet Worker calculations run on a background process, meaning that there is no user interface lag created while the calculations are run. Disabled fields, on the other hand, run on the main process and as such can cause "lockups" or "stuttering" if there are large numbers of them being calculated at once (for example, if you have a very complicated sheet using thousands of disabled fields).
  • The auto-calculating Strength Mod field would appear disabled to the player. A Strength Mod field that is updated by a Sheet Worker, on the other hand, would be modifiable after the calculation has run (although any value entered would be overwritten when the Strength value changes). So a Sheet Worker would better support homebrew rules since the player could simply modify the Mod value after Strength changes. On the other hand, the auto-calculating field would not allow such a change, so rules would be "enforced" more rigidly.


In general, our recommendation is that you use auto-calculating fields sparingly. Give yourself a budget of 500 to 1,000 auto-calculating fields at most. We recommend using the new Sheet Worker functionality for most calculations, especially calculations which only need to be performed rarely (for example, your Strength value (and therefore your Strength Mod) probably only changes at most once per session when the character levels up. There's no need to re-run the same calculation over and over again every time the sheet is opened.

Sheet Worker API

Events

eventInfo Object

Many of the events are passed an eventInfo object that gives you additional detail about the circumstances of the event.

Property Description
sourceAttribute The original attribute that triggered the event. It is the full name (including RowID if in a repeating section) of the attribute that originally triggered this event.

Note: The entire string will have been translated into lowercase and thus might not be suitable for being fed directly into getAttrs().

sourceType The agent that triggered the event, either player or sheetworker
previousValue The original value of the attribute in an on:change event, before the attribute was changed.
newValue The value to which the attribute in an on:change event has changed.
removedInfo An object containing the values of all the attributes removed in a remove:repeating_groupname event.

change:<attribute_name>

Allows you to listen to the changes of specific attributes, or in the case of a repeating section any changes in the entire section. It's very straightforward:


on("change:strength change:StrengthMod change:StrengthOtherThing", function(eventInfo) {
   //Do something here
   // eventInfo.previousValue is the original value of the attribute that triggered this event, before being changed.
   // eventInfo.newValue is the current value of the attribute that triggered this event, having been changed.
});

on("change:repeating_spells:spellname", function(eventInfo) {
   //Do something here
   // eventInfo.sourceAttribute is the full name (including repeating ID) of the attribute 
   // that originally triggered this event, 
   // however the entire string will have been translated into lowercase and thus might not be 
   // suitable for being fed directly
   // into getAttrs() or other uses without being devandalized first.
});

on("change:repeating_spells", function(eventInfo) {
   //Would be triggered when any attribute in the repeating_spells section is changed
   // eventInfo.sourceAttribute is the full name (lowercased)(including repeating ID) 
   // of the attribute that originally triggered this event. 
});

Note: All attribute names are lowercased in events. So even if you normally refer to an attribute as "Strength", use "change:strength" in the event listener.

Note: This supports the "_max" suffix in that ether "change:strength" or "change:strength_max" will fire an event, however these two variations seem to be interchangeable, in that ether or both will fire when ether "strength" or "strength_max" changes.

For attributes in repeating fields, all of the following would be triggered when the "repeating_spells_SpellName" attribute is updated: "change:repeating_spells:spellname", "change:repeating_spells", "change:spellname", "change:spellname_max". This gives you maximum flexibility in choosing what "level" of change event you want to bind your function to.

Sheetworkers can also listen for a change event of a special attribute that is modified whenever a repeating section is re-ordered.

on("change:_reporder_repeating_<sectionname>", function(eventInfo) {
   // Where <sectionname> above should be a repeating section name, such as skills or spells
});

remove:repeating_<groupname>

This is an event which will fire whenever a row is deleted from a repeating field section. You can also listen for a specific row to be deleted if you know its ID, such as on("remove:repeating_inventory:-ABC123")


on("remove:repeating_inventory", function(eventInfo) {
     //Fired whenever a row is removed from repeating_inventory
     // eventInfo.sourceAttribute is the full name (including ID) of the first attribute that triggered the event (you can use this to determined the ID of the row that was deleted)
});

The removed:repeating_<groupname> function's eventinfo includes a special property removedInfo that displays all of the attributes of the now removed repeating section.

on("remove:repeating_inventory", function(eventinfo) {
   console.log(eventinfo.removedInfo);
});

sheet:opened

This event will fire the first time a sheet is opened by a player in a game session. Note that it will fire again the next game session, and it most likely will fire on multiple people's computers (each time a person opens the sheet). However, it should be useful for doing things like checking for needed sheet upgrades.

on('sheet:opened',function(){

// Do something the first time the sheet is opened by a player in a session

});

Functions

getAttrs(attributeNameArray, callback) [Asynchronous]

The getAttrs function allows you to get the values of a set of attributes from the sheet. The "_max" suffix is supported, so getAttrs( ["Strength", "Strength_max"], func) will get both the current and max values of "Strength". Note that the function is asynchronous, which means that there is no guarantee that the order in which multiple getAttrs calls are made is the order in which the results will be returned. Rather, you pass a callback function which will be executed when the values have been calculated. The callback function receives a simple Javascript object with a list of key-value pairs, one for each attribute that you requested.

Here's an example:


on("change:strength", function() {
   getAttrs(["Strength", "Level"], function(values) {
      //Do something with values.Strength and/or values[ "Level" ]
   });
});

Values in repeating sections require a little special handling. If the event that you are inside of is already inside of a repeating section, you can simply request the variable using its name prefaced by the repeating group name and you will receive the value in the same repeating section the event was triggered in. For example, if we have a repeating_spells section that has both SpellName, SpellLevel, and SpellDamage, then:


on("change:repeating_spells:spelllevel", function() {
   getAttrs([
      "repeating_spells_SpellDamage",
      "repeating_spells_SpellName"
    ], function(values) {
      //values.repeating_spells_SpellDamage and values.repeating_spells_SpellName 
//will both be from the same repeating section row that the SpellLevel that changed is in.
   });
});

On the other hand, if you aren't currently in a repeating section, you can specifically request that value of a field in a repeating section row by specifying its ID manually:


getAttrs(["repeating_spells_-ABC123_SpellDamage"]...

You can also request the "_reporder_repeating_<sectionname>" attribute with getAttrs() to get a list of all the IDs in the section that have been ordered. However note that this may not include the full listing of all IDs in a section. Any IDs not in the list that are in the section are assumed to come after the ordered IDs in lexographic order.

setAttrs(values,options,callback) [Asynchronous]

values -- This is an object whose properties are the names of attributes (without the attr_ prefix) and whose values are what you want to set that attribute to.

options -- (Optional) This is an object that specifies optional behavior for the function. Currently the only option is "silent", which prevents propagation of change events from setting the supplied attributes.

callback -- (Optional) This is a callback function which will be executed when the set attributes have been updated.

The setAttrs function allows you to set the attributes of the character sheet. Use the "_max" suffix to set the max value of an attribute. For example "Strength_max".


on("change:strength", function() {
   getAttrs(["Strength", "Level"], function(values) {
      setAttrs({
          StrengthMod: Math.floor(values.Strength / 2)
      });
   });
});

Note: If you are trying to update a disabled input field with this method you may run into trouble. One option is to use this `setAttrs` method to set a hidden input, then set the disabled input to the hidden element. In this example we have an attr named will, and we want to calculate judgement based off 1/2 of the will stat, but not to allow it to exceed 90. See below.

<label>Judgment</label>
<input type="hidden" name="attr_foo_judgment" value="0" />
<input type="number" name="attr_judgment" value="@{foo_judgment}" disabled="true" title="1/2 of will rounded down, 90 max" />
on("change:will", function() {
  getAttrs(["will"], function(values) {
    setAttrs({ foo_judgment: Math.min(90, (values.will/2)) });
  });
});

Note that although the setAttrs is an asynchronous function and there is no guarantee to which order the actual attributes will be set for multiple setAttrs() calls.

For repeating sections, you have the option of using the simple variable name if the original event is in a repeating section, or you can specify the full repeating section variable name including ID in any event.


on("change:repeating_spells:spelllevel", function() {
   getAttrs(["repeating_spells_SpellLevel", "repeating_spells_SpellName"], function(values) {
      setAttrs({
         repeating_spells_SpellDamage: Math.floor(values.repeating_spells_SpellLevel / 2) + 10
      });
   });
});

getSectionIDs(section_name,callback) [Asynchronous]

This function allows you to get a list of the IDs which currently exist for a given repeating section. This is useful for calculating things such as inventory where there may be a variable number of rows.


on("change:repeating_inventory", function() {
   getSectionIDs("inventory", function(idarray) {
      for(var i=0; i < idarray.length; i++) {
         //Do something with the IDs
      }
   });
});

Note that you may use GetAttrs() (described above) to request the "_reporder_repeating_<sectionname>" attribute to get a list of all the IDs in the section that have been ordered. However note that this may not include the full listing of all IDs in a section. Any IDs not in the list that are in the section are assumed to come after the ordered IDs in lexographic order.
Which is to say that getSectionIDs() will get all IDs - but not in the order that they are displayed to the user. getAttrs( "_reporder_repeating_<sectionname>", ... ) will return a list of all IDs that have been moved out of their normal lexographic order. You can use the following function as a replacement for getSectionIDs to get the IDs in the order that they are displayed in instead.

var getSectionIDsOrdered = function (sectionName, callback) {
  'use strict';
  getAttrs([`_reporder_${sectionName}`], function (v) {
    getSectionIDs(sectionName, function (idArray) {
      let reporderArray = v[`_reporder_${sectionName}`] ? v[`_reporder_${sectionName}`].toLowerCase().split(',') : [],
        ids = [...new Set(reporderArray.filter(x => idArray.includes(x)).concat(idArray))];
      callback(ids);
    });
  });
};

generateRowID()

A synchronous function which immediately returns a new random ID which you can use to create a new repeating section row. If you use setAttrs() and pass in the ID of a repeating section row that doesn't exist, one will be created with that ID.

Here's an example you can use to create a new row in a repeating section:


var newrowid = generateRowID();
var newrowattrs = {};
newrowattrs["repeating_inventory_" + newrowid + "_weight"] = "testnewrow";
setAttrs(newrowattrs);

removeRepeatingRow( RowID )

A synchronous function which will immediately remove all the attributes associated with a given RowID and then remove the row from the character sheet. The RowID should be of the format "repeating_<sectionname>_<rowid>". For example, "repeating_skills_-KbjuRmBRiJNeAJBJeh2".

Here is an example of clearing out a summary list when the original list changes:

on("change:repeating_inventory", function() {
   getSectionIDs("repeating_inventorysummary", function(idarray) {
      for(var i=0; i < idarray.length; i++) {
        removeRepeatingRow("repeating_inventorysummary_" + idarray[i]);
      }
   });
});

getTranslationByKey([key])

A synchronous function which immediately returns the translation string related to the given key. If no key exists, false will be returned and a message in the console will be thrown which list the specific key that was not found in the translation JSON.

Here's an example you can use to fetch a translation string from the translation JSON: With the following translation JSON

{
    "str": "Strength",
    "dex": "Dexterity"
}

var strTranslation = getTranslationByKey('str'); // "Strength"
var dexTranslation = getTranslationByKey('dex'); // "Dexterity"
var intTranslation = getTranslationByKey('int'); // false

getTranslationLanguage()

A synchronous function which immediately returns the 2-character language code for the user's selected language.

Here's an example you can use to fetch the translation language:


var translationLang = getTranslationLanguage(); // "en" , for an account using English

setDefaultToken(values)

A function that allows the sheet author to determine what attributes are set on character dropped from the compendium. When setting the default token after a compendium drop, this function can set any attributes on the default token to tie in important attributes specific to that character sheet, such as attr_hit_points.

The list of token attributes that can be set by setDefaultToken are:

["bar1_value","bar1_max","bar2_value","bar2_max","bar3_value","bar3_max","aura1_square","aura1_radius","aura1_color","aura2_square","aura2_radius","aura2_color",
"tint_color","showname","showplayers_name","playersedit_name","showplayers_bar1","playersedit_bar1","showplayers_bar2","playersedit_bar2","showplayers_bar3",
"playersedit_bar3","showplayers_aura1","playersedit_aura1","showplayers_aura2","playersedit_aura2","light_radius","light_dimradius","light_angle","light_otherplayers",
"light_hassight","light_losangle","light_multiplier"]

For more information about this attributes and what they do, please see the the API Objects Page.

Here's an example:


on("sheet:compendium-drop", function() {
    var default_attr = {};
    default_attr["width"] = 70;
    default_attr["height"] = 70;
    default_attr["bar1_value"] = 10;
    default_attr["bar1_max"] = 15;
    setDefaultToken(default_attr);
});