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
(change:)
m (Update to reference of the async library)
 
(184 intermediate revisions by 32 users not shown)
Line 1: Line 1:
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.
+
{{revdate}}{{BCS}}
  
==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:
+
{{NavSheetDoc}}
 +
'''Sheet Worker Scripts'''(AKA simply as "'''sheetworkers'''"), are an advanced feature of the [[Building_Character_Sheets|Character Sheets]] system, which are written in [https://developer.mozilla.org/en-US/docs/Web/javascript JavaScript](they are a type of [https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API Web Workers], hence the name). They will allow making automatic & complex changes to the character sheet stats, based on certain events, such as whenever a certain attribute's value on a character sheet is modified, or when an [[Button#Action_Button|action button]] is pressed.
  
<pre language="javascript">
+
[[Sheetworker examples for Non-programmers]], [[Sheet Worker Snippets]] and [[UniversalSheetWorkers|Universal Sheetworkers]] are the best starting points for practical and working examples on how to use sheetworkers.
 +
 
 +
This page gives a general overview of the available Roll20-specific features that can be used in sheetworkers, but is not a great guide for practical implementations. The pages/links found under the '''[[#Related_Pages|Related Pages]]''' and '''[[#See_Also|See Also]]'''-sections have links to more examples of how to use sheetworkers.
 +
 
 +
__TOC__
 +
 
 +
==General==
 +
As sheetworkers are written in JavaScript, its a good idea to take some general guides and learn the basics of it to be able to understand & create sheetworkers.
 +
 
 +
'''[https://developer.mozilla.org/en-US/docs/Learn/JavaScript/First_steps Introduction to JavaScript]''' - Mozilla Developer Network(MDN) web docs
 +
 
 +
* Few things that often come up in existing sheetworkers code, which isn't always covered that early in general JS tutorials & guides.
 +
** [https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions Arrow functions] - An alternative way to write [https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Functions functions] in JavaScript. Used in many sheets.
 +
** [https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals Template literals] - string literals allowing embedded expressions: <code>`This is a ${word} example`</code>
 +
* [[Javascript:Best Practices]] - tips for both [[API]] & sheetworkers
 +
 
 +
===JavaScript Restrictions===
 +
Many JavaScript functions or functionalities can't be used in Roll20 character sheets due to the [[Building_Character_Sheets#Restrictions|security filter]]. One should check existing sheets for examples of what can be used, if you're attempting to do any slightly more advanced  data-handling. '''Sheetworkers have access to the [https://underscorejs.org/ Underscore] library.'''
 +
* [https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model DOM] can't be used directly in basically any way, so event listeners like <code>[https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers/onclick onclick]</code> won't work. (Partial exception: CSS classes manipulated via [[jQuery]])
 +
* All JavaScript must be inside a single <code><script type="text/worker"></code> element in the HTML file. Otherwise some may fail to trigger or lead to uncertain behaviour
 +
* Only the Roll20-designed event listeners can trigger JS on the character sheet:
 +
** stat change on a sheet, done either by the user, or another sheetworker
 +
** stat change done by an API
 +
** triggered by an [[Button#Action_Button|Action Sheet Button]]
 +
 
 +
 
 +
See Also '''[[BCS/Bugs#Sheetworkers]]'''.
 +
<br>
 +
 
 +
===Structure of a Sheetworker===
 +
Sheetworkers in Roll20 character sheets are pieces of JavaScript, intertwined with a few custom features made specifically for handling Roll20 character sheet information. All the sheetworkers in a character sheet are places inside a '''single''' <code><nowiki><script type="text/worker"></nowiki></code>-element.
 +
 
 +
Usually a sheetworker works as follows.
 +
 
 +
'''1.''' Listens for changes to the sheet with an '''Event Listener''' (<code>"change:attributename</code> <code>sheet:opened</code>) (explained in next section) <br>
 +
'''2.''' Retrieves values of a number of attributes from the sheet with the <code>getAttrs</code> function, usually including the attributes that were monitored.<br>
 +
'''3.''' Create some temporary variables and uses the values received from <code>getAttrs</code> and does something based on the info using normal JavaScript.<br>
 +
'''4.''' Save to the character sheet some of the new values made in '''Step 3''' to some number of attributes with the <code>setAttrs</code> function.<br>
 +
 
 +
 
 +
The following is an example of a single sheetworker placed in a <code><nowiki><script type="text/worker"></nowiki></code>-element, taken from the [[Sheetworker examples for Non-programmers]]-page.
 +
 +
<pre data-language="javascript">
 +
<script type="text/worker">
 +
on("change:siz change:con sheet:opened", function() { 
 +
  getAttrs(["siz","con"], function(values) {
 +
    let siz = parseInt(values.siz)||0;
 +
    let con = parseInt(values.con)||0;
 +
    let hp = siz + con;
 +
    setAttrs({                           
 +
      "hitpoints": hp
 +
    });
 +
  });
 +
});
 +
</script>
 +
</pre>
 +
 
 +
====<script type="text/worker">====
 +
 
 +
In a character sheet, all sheetworkers are saved on the <code>.html</code>-file for the sheet, inside a '''single''' HTML element named <code><script type="text/worker"></code>. This is critical for the sheetworkers to function in Roll20. If sheetworkers are split into more than one <code><script type="text/worker"></code>-element, it's almost guaranteed all of them will fail to function. Most Sheet Creator place the sheetworker-block at the bottom of the HTML-file.
 +
 
 +
====Event listener====
 +
 
 +
<code>on("change:siz change:con sheet:opened", function()</code> is the Event listener, which checks the character sheet for changes to specific attributes, if the sheet have been opened, or if a button have been pressed. In the example, the sheetworker is checking if the <code>siz</code> or <code>con</code>-attribute have been changed, or if the sheet have been opened. These two represents the size and constitution of the character.
 +
 
 +
====Value retrieval====
 +
 
 +
<code>getAttrs(["siz","con"], function(values)</code> looks up the values of the <code>siz</code> or <code>con</code>-attributes on the character sheet, so they can be used in the sheetworker.
 +
 
 +
 
 +
====Making changes====
 +
 
 +
<pre data-language="javascript">
 +
    let siz = parseInt(values.siz)||0;
 +
    let con = parseInt(values.con)||0;
 +
    let hp = siz + con;
 +
</pre>
 +
 
 +
This is the main part of the sheetworker. It first defines two temporary varible, <code>siz</code> and <code>con</code>, that saves the values of their attributes in a way that JavaScript knows it's a number.
 +
 
 +
On the third row, it defines the <code>hp</code>-varaible to be <code>siz + con</code>, which means that the characters Hit Points are equal to the sum of their Size and Constitution.
 +
 
 +
====Saving the changes====
 +
 
 +
<pre data-language="javascript">
 +
    setAttrs({                           
 +
      "hitpoints": hp
 +
    });
 +
</pre>
 +
 
 +
Finally, the sheetworker decides to define the character sheet's <code>hitpoints</code>-attribute to be equal to the newly calculated <code>hp</code>-variable, which is the last action of the sheetworker. Note that it does not make changes to the attributes it was listening to.
 +
 
 +
====Other things====
 +
 
 +
It's good practice to always use lower case when referencing attribute names in your sheet workers, regardless of how the attributes are defined in the HTML. This helps to avoid a number of issues.
 +
 
 +
Outside of this, there may exist some other things saved in the <code><nowiki><script type="text/worker"></nowiki></code>, such as:
 +
 
 +
* Saved list of information, used by the sheetworkers.
 +
** '''Example:''' <code>const stats = ["strength", "intelligence", "charm", "arcana", "grace"];</code>
 +
* the Event Listener might loop through some JavaScript, that decides what attributes to listen/change/save
 +
** '''Example:''' <code>stats.forEach(stat => { some code });</code>
 +
* Define [https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Functions JavaScript functions] to be used in the sheetworkers, instead of repeating the same code snippets in several places.
 +
 
 +
===Adding a Sheetworker===
 +
 
 +
To add a sheetworker to a Character Sheet, simply add the script to the bottom of your <code>HTML</code>-section of the sheet using the following format:
 +
 
 +
<pre data-language="javascript">
 
<script type="text/worker">
 
<script type="text/worker">
  
Line 17: Line 125:
 
</pre>
 
</pre>
  
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.
+
The <code><nowiki><script></nowiki></code>-element '''must''' have a type of <code>"text/worker"</code> 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, <code><nowiki><script></nowiki></code>-elements with a type of <code>"text/worker"</code> 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 should be contained within a single <code><nowiki><script type="text/worker"></nowiki></code> element, and preferably be placed at the bottom of the <code>html</code>-file.
 +
 
 +
'''[[Sheetworker_examples_for_Non-programmers|Sheetworker examples for Non-programmers]]'''
 +
 
 +
'''Warning about "global" variable scope:''' the scope of a variable declared ''inside'' the <code><nowiki><script type="text/worker"></nowiki></code> 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 <code>getAttrs</code> rather than doing <code>getAttrs</code> -> calculations -> <code>setAttrs</code> -> <code>getAttrs</code> -> calculations -> <code>setAttrs</code> …
 +
===Understanding sheetworkers===
 +
(credit: [https://app.roll20.net/users/726129 Jakob])
 +
 
 +
Usually, the best way to learn something is to do it! Start solving the problems you want to solve using sheet workers, and learn along the way :).
 +
 
 +
Now, that probably wasn't very helpful. I could also encourage you to look at code others have written, which you should do; just realize that many sheet authors are also amateurs (such as me), hence the code may not be the most elegantly-written example.
 +
 
 +
That probably also wasn't helpful. Let me try something else: depending on what your course taught you, much of it (everything dealing with manipulation of the DOM) is not going to be very helpful. Here's some important things you need to keep in mind that might make sheet workers different from what you've learned:
 +
 
 +
:1. '''Sheet workers are event-driven.''' Everything interesting your workers do will happen in response to a certain event on the sheet; most of the time, the triggering event will be changing an attribute on the sheet (i.e., the value of some input, be it a checkbox, radio, text, number input, or textarea). This is what the <code>on("change:attr", function () { ...})</code> is there for: it's about registering a function that's supposed to be executed every time attr changes.
 +
 
 +
:2. '''Sheet workers can only really do one thing.''' Sheet workers can change the value of attributes, and that's pretty much it (they can also remove repeating rows, but that's not their main use). If you want your sheet workers to do a thing, ask yourself if it can be accomplished by changing the value of an attribute (this includes being able to (un)check checkboxes), +perhaps some CSS. If yes, you can do it with sheet workers. If not, you cannot.
 +
 
 +
:3. '''Sheet workers are asynchronous.''' The main interesting functions you have access to in sheet workers, namely getAttrs, setAttrs, and getSectionIDs, take as their second argument functions that do other stuff. This means that you cannot trust that your code will simply be executed from top to bottom, and have to write in such a way that it makes sense no matter when asynchronous functions are executed. This is often a source of confusion for people.
 +
 
 +
===Good Practices===
 +
(credit: [https://app.roll20.net/users/157788 GiGs])
 +
 
 +
* I think it's a good idea (especially when still learning) to get into the habit of putting all the values you are using in variables at the top of the code. It makes writing the code a bit more laborious, but if you reuse values, or have long macros, it can make things clearer, and most importantly, you can then use console.log statements to check what their values are. (they will appear in the browser's dev console, which you can usually find by pressing {{button|F12}}).
 +
 
 +
* Another tip is its a good idea to just use a single setAttrs statement, at the end of the sheet worker. For this one which is just modifying a single attribute, it doesn't matter, but you'll eventually get into making workers that set multiple attributes. when that happens, you definitely want to group them into a single statement, to avoid unpredictable errors.
 +
 
 +
* Also its a good idea to use variable names that match the value they are calling. In the code below, for example, {{c|i}} renamed index to {{c|currenthp}}, which I think is more descriptive.
 +
 
 +
* It's also a good idea to use lower case only for attribute names, and use underscores instead of dashes or other symbols in the name. For example, use {{c|strengthmod}} or {{c|strength_mod}}, and avoid {{c|strength}}-mod.
 +
 
 +
* One final tip: if you are using {{c|parseInt}} on cells that users can enter values into, it's a good idea to add a default value. For instance <code>parseInt('something')||0</code> means that if the cell contains text, or not a number, you'll get a number in the code. Your if statements will fail without this, if the cells being called don't actually have numbers in them.
  
==Sheet Workers vs. Auto-Calculating Fields: Which should I use?==
+
===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:
 
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.
+
* The [[Auto-Calc|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).
 
* 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).
Line 31: Line 174:
  
 
'''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.
 
'''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==
 
==Sheet Worker API==
 +
A list of sheetworker features accessible in Roll20. Not to be confused with the [[API|Roll20 API]].
  
 
===Events===
 
===Events===
 +
{{orange|'''Note:''' All attribute names are lowercased in events. So even if you normally refer to an attribute as <code>Strength</code>, use <code>change:strength</code> in the event listener.}}
 +
====eventInfo Object====
 +
Many of the events are passed an <code>eventInfo</code> object that gives you additional detail about the circumstances of the event.
  
====change:<attribute_name>====
+
{| class="wikitable"
 +
|- style="vertical-align:top;"
 +
!Property
 +
!Description
 +
|- style="vertical-align:top;"
 +
| <code>sourceAttribute</code>
 +
| The original attribute that triggered the event.  It is the full name (including [[RowID]] if in a [[BCS/Repeating Sections|repeating section]]) of the attribute that originally triggered this event. 
  
This is currently the only supported event. It 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 straigthforward:
+
'''Note''': The entire string will have been translated into lowercase and thus might not be suitable for being fed directly into {{c|getAttrs()}}.
 +
|- style="vertical-align:top;"
 +
| <code>sourceType</code>
 +
| The agent that triggered the event, either <code>player</code> or <code>sheetworker</code>
 +
|- style="vertical-align:top;"
 +
| <code>previousValue</code>
 +
| The original value of the attribute in an <code>on:change</code> event, before the attribute was changed.
 +
|- style="vertical-align:top;"
 +
| <code>newValue</code>
 +
| The value to which the attribute in an <code>on:change</code> event has changed.
 +
|- style="vertical-align:top;"
 +
| <code>removedInfo</code>
 +
| An object containing the values of all the attributes removed in a <code>remove:repeating_groupname</code> event.
 +
|- style="vertical-align:top;"
 +
| <code>triggerName</code>
 +
| When changing a value it is equal to the <code>sourceAttribute</code>, being the full name (including [[RowID]] if in a [[BCS/Repeating Sections|repeating section]])
  
<pre language="javascript">
+
When removing a repeating row it contains the bound trigger name for the repeating section, including the <code>remove</code> keyword, i.e. <code>remove:repeating_section</code>
 +
|}
  
on("change:strength change:StrengthMod change:StrengthOtherThing", function() {
+
====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:
 +
 
 +
<pre data-language="javascript" style="overflow:auto;white-space:pre-wrap;">
 +
 
 +
on("change:strength change:strengthmod change:strengthotherthing", function(eventInfo) {
 
   //Do something here
 
   //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() {
+
on("change:repeating_spells:spellname", function(eventInfo) {
 
   //Do something here
 
   //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() {
+
on("change:repeating_spells", function(eventInfo) {
 
   //Would be triggered when any attribute in the repeating_spells section is changed
 
   //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.
 
});
 
});
  
 
</pre>
 
</pre>
  
'''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.'''
+
 
 +
For attributes in repeating fields, all of the following would be triggered when the <code>repeating_spells_SpellName</code> attribute is updated:
 +
<code>change:repeating_spells:spellname</code>,
 +
<code>change:repeating_spells</code>,
 +
<code>change:spellname</code>,
 +
<code>change:spellname_max</code>.
 +
This gives you maximum flexibility in choosing what "level" of change event you want to bind your function to.
 +
 
 +
 
 +
{{notebox|'''Note:''' This supports the <code>_max</code> suffix in that ether <code>change:strength</code> or <code>change:strength_max</code> will fire an event, however these two variations seem to be interchangeable, in that ether or both will fire when ether <code>strength</code> or <code>strength_max</code> changes.}}
 +
 
 +
====change:_reporder:<sectionname>====
 +
Sheetworkers can also listen for a change event of a special attribute that is modified whenever a repeating section is re-ordered.
 +
<pre data-language="javascript" style="overflow:auto;white-space:pre-wrap;">
 +
on("change:_reporder:<sectionname>", function(eventInfo) {
 +
  // Where <sectionname> above should be a repeating section name, such as skills or spells
 +
});
 +
</pre>
 +
 
 +
====remove:repeating_<groupname>====
 +
 
 +
This is an event which will fire whenever a row is deleted from a [[Building_Character_Sheets#Repeating_Sections|repeating field]] section. You can also listen for a specific row to be deleted if you know its ID, such as on(<code>remove:repeating_inventory:-ABC123</code>)
 +
 
 +
<pre data-language="javascript" style="white-space: pre-wrap;">
 +
 
 +
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)
 +
});
 +
 
 +
</pre>
 +
 
 +
The <code>removed:repeating_<groupname></code> function's <code>eventinfo</code> includes a special property <code>removedInfo</code>, that displays all of the attributes of the now removed repeating section.
 +
<pre data-language="javascript">
 +
on("remove:repeating_inventory", function(eventinfo) {
 +
  console.log(eventinfo.removedInfo);
 +
});
 +
</pre>
 +
 
 +
====sheet:opened====
 +
This event will fire every time a sheet is opened by a player in a game session. It should be useful for doing things like checking for needed sheet upgrades.
 +
 
 +
<pre data-language="javascript">
 +
on('sheet:opened',function(){
 +
 
 +
// Do something the first time the sheet is opened by a player in a session
 +
 
 +
});
 +
</pre>
 +
 
 +
====clicked:<button_name>====
 +
 
 +
This event will trigger when an action button is clicked. The button's name will need to start with <code>act_</code>. Example:
 +
 
 +
 
 +
<pre data-language="html">
 +
<button type="action" name="act_activate">Activate!</button>
 +
 
 +
<script type="text/worker">
 +
on("clicked:activate", function() {
 +
  console.log("Activate button clicked");
 +
});
 +
</script>
 +
</pre>
 +
 
 +
====sheet:compendium-drop====
 +
Only relevant if you're working on [[Compendium Integration]].
 +
 
 +
Can be used to set Default Token info for characters drag&dropped from the compendium to your game.
 +
 
 +
{{repo|Roll20/roll20-character-sheets/blob/3f34ccd323226bf6400298e5b6be79eb6d4406c4/DD5thEditionLegacy/src/js/sheetworkers.js#L48 D&D 5E by Roll20 example}}
  
 
===Functions===
 
===Functions===
  
 
====getAttrs(attributeNameArray, callback)====
 
====getAttrs(attributeNameArray, callback)====
 +
{{Asynchronous}}
  
The getAttrs function allows you to get the values of a set of attributes from the sheet. 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.
+
The <code>getAttrs</code> function allows you to get the values of a set of attributes from the sheet. The {{code|_max}} suffix is supported, so <code>getAttrs( ["Strength", "Strength_max"], func)</code> will get both the current and max values of <code>Strength</code>. Note that the function is asynchronous, which means that there is no guarantee that the order in which multiple <code>getAttrs</code> 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:
+
'''Example:'''
  
<pre language="javascript">
+
<pre data-language="javascript">
  
 
on("change:strength", function() {
 
on("change:strength", function() {
 
   getAttrs(["Strength", "Level"], function(values) {
 
   getAttrs(["Strength", "Level"], function(values) {
       //Do something with values.Strength, values.Level
+
       //Do something with values.Strength and/or values[ "Level" ]
 
   });
 
   });
 
});
 
});
Line 76: Line 330:
 
</pre>
 
</pre>
  
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:
+
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 <code>repeating_spells</code> section that has both <code>SpellName</code>, <code>SpellLevel</code>, and <code>SpellDamage</code>, then:
 
+
<pre language="javascript">
+
  
 +
<pre data-language="javascript">
 
on("change:repeating_spells:spelllevel", function() {
 
on("change:repeating_spells:spelllevel", function() {
 
   getAttrs([
 
   getAttrs([
Line 85: Line 338:
 
       "repeating_spells_SpellName"
 
       "repeating_spells_SpellName"
 
     ], function(values) {
 
     ], 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.
+
       //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.
 
   });
 
   });
 
});
 
});
 +
</pre>
 +
 +
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:
  
 +
<pre data-language="javascript">
 +
getAttrs(["repeating_spells_-ABC123_SpellDamage"]...
 
</pre>
 
</pre>
  
On the other hand, if 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:
+
You can also request the <code>_reporder_repeating_<sectionname></code> attribute with <code>getAttrs()</code> 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.
  
<pre language="javascript">
+
====setAttrs(values,options,callback)====
 +
{{Asynchronous}}
  
getAttrs(["repeatng_spells_-ABC123_SpellDamage"]...
+
'''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.
  
</pre>
+
'''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.
  
====setAttrs(values)====
+
'''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.
+
The setAttrs function allows you to set the attributes of the character sheet. Use the <code>_max</code>-suffix to set the max value of an attribute. For example <code>Strength_max</code>.  
  
<pre language="javascript">
+
<pre data-language="javascript">
  
 
on("change:strength", function() {
 
on("change:strength", function() {
Line 115: Line 375:
 
</pre>
 
</pre>
  
Note that although the setAttrs call does not take a callback, it is an asynchronous function and there is no guarantee to which order the actual attributes will be set for multiple setAttrs() calls.
+
'''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 <code>setAttrs</code> method to set a hidden input, then set the disabled input to the hidden element. In this example we have an attribute named <code>will</code>, and we want to calculate <code>judgement</code> based off 1/2 of the <code>will</code> stat, but not to allow it to exceed 90. See below.
  
For repeating sections, again, 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.
+
<pre data-language="html">
 +
<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" />
 +
</pre>
 +
 
 +
<pre data-language="javascript">
 +
on("change:will", function() {
 +
  getAttrs(["will"], function(values) {
 +
    setAttrs({ foo_judgment: Math.min(90, (values.will/2)) });
 +
  });
 +
});
 +
</pre>
 +
 +
Although <code>setAttrs</code> is an asynchronous function and there is no guarantee to which order the actual attributes will be set for multiple <code>setAttrs()</code> 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.
  
<pre language="javascript">
+
<pre data-language="javascript">
  
 
on("change:repeating_spells:spelllevel", function() {
 
on("change:repeating_spells:spelllevel", function() {
Line 131: Line 407:
 
</pre>
 
</pre>
  
====getSectionIDs(section_name)====
+
====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.
 
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.
  
<pre language="javascript">
+
<pre data-language="javascript">
  
 
on("change:repeating_inventory", function() {
 
on("change:repeating_inventory", function() {
   getSectionIDs("repeating_inventory", function(idarray) {
+
   getSectionIDs("inventory", function(idarray) {
 
       for(var i=0; i < idarray.length; i++) {
 
       for(var i=0; i < idarray.length; i++) {
 
         //Do something with the IDs
 
         //Do something with the IDs
Line 146: Line 423:
  
 
</pre>
 
</pre>
 +
Note that you may use <code>GetAttrs()</code> (described above) to request the <code>_reporder_repeating_<sectionname></code> 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. <br>
 +
Which is to say that <code>getSectionIDs()</code> will get all IDs - but not in the order that they are displayed to the user. <code>getAttrs( _reporder_repeating_<sectionname></code>, ... ) 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 <code>getSectionIDs</code> to get the IDs in the order that they are displayed in instead.
 +
 +
<pre data-language="javascript">
 +
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);
 +
    });
 +
  });
 +
};
 +
</pre>
 +
 +
====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 <code>setAttrs()</code> 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:
 +
 +
<pre data-language="javascript">
 +
 +
var newrowid = generateRowID();
 +
var newrowattrs = {};
 +
newrowattrs["repeating_inventory_" + newrowid + "_weight"] = "testnewrow";
 +
setAttrs(newrowattrs);
 +
 +
</pre>
 +
 +
=====WARNING=====
 +
 +
This function does not generate reliably unique row IDs, which may lead to unexpected behavior. For example, if you use generated IDs as keys in <code>value</code> for use in <code>setAttrs(value)</code> as intended, you may end up overwriting one or more existing entries and thus expected repeating section items will never be rendered. Here's an example you can use to work around this issue:
 +
 +
<pre data-language="javascript">
 +
 +
var newweightid = "repeating_inventory_" + generateRowID() + "_weight";
 +
 +
while (newweightid in newrowattrs) {
 +
  newweightid = "repeating_inventory_" + generateRowID() + "_weight";
 +
}
 +
 +
newrowattrs[newweightid] = "testnewrow";
 +
 +
</pre>
 +
 +
====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 <code>repeating_<sectionname>_<rowid></code>. For example, <code>repeating_skills_-KbjuRmBRiJNeAJBJeh2</code>.
 +
 +
Here is an example of clearing out a summary list when the original list changes:
 +
<pre data-language="javascript">
 +
on("change:repeating_inventory", function() {
 +
  getSectionIDs("repeating_inventorysummary", function(idarray) {
 +
      for(var i=0; i < idarray.length; i++) {
 +
        removeRepeatingRow("repeating_inventorysummary_" + idarray[i]);
 +
      }
 +
  });
 +
});
 +
</pre>
 +
 +
====setSectionOrder(<Repeating Section Name>, <Section Array>, <Callback>)====
 +
<pre data-language="javascript"> setSectionOrder("proficiencies", final-array, callbackFunction); </pre>
 +
 +
The setSectionOrder function allow the ordering of repeating sections according to your preference. The function accepts the name of a repeating section **without** `repeating_` (e.g. `repeating_proficiencies` would be passed simply as `proficiencies`) and an array of row IDs in the order that you want them.
 +
 +
=====WARNING=====
 +
This function's behavior does not match its documentation. The function does not appear to have any callback functionality. Additionally, reordering sections via this function can cause significant graphical glitches and even some data corruption depending on what a user does in reaction to the graphic glitches.
 +
 +
====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
 +
<pre data-language="javascript">
 +
{
 +
    "str": "Strength",
 +
    "dex": "Dexterity"
 +
}
 +
</pre>
 +
 +
<pre data-language="javascript">
 +
 +
var strTranslation = getTranslationByKey('str'); // "Strength"
 +
var dexTranslation = getTranslationByKey('dex'); // "Dexterity"
 +
var intTranslation = getTranslationByKey('int'); // false
 +
 +
</pre>
 +
 +
====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:
 +
 +
<pre data-language="javascript">
 +
 +
var translationLang = getTranslationLanguage(); // "en" , for an account using English
 +
 +
</pre>
 +
 +
====setDefaultToken(values)====
 +
 +
A function that allows the sheet author to determine what attributes are set on character dropped from the [[Compendium Integration|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 <code>attr_hit_points</code>.
 +
 +
The list of token attributes that can be set by <code>setDefaultToken</code> are:
 +
<pre style="white-space: pre-wrap;">
 +
["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"]
 +
</pre>
 +
For more information about this attributes and what they do, please see the the [[API:Objects#Graphic_.28Token.2FMap.2FCard.2FEtc..29|API Objects]]-page.
 +
 +
'''Example:'''
 +
 +
<pre data-language="javascript">
 +
 +
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);
 +
});
 +
 +
</pre>
 +
<br>
 +
 +
===Custom Roll Parsing===
 +
{{main|Custom Roll Parsing}}
 +
 +
As of July 13th, 2021, character sheets can now combine the functionality of roll buttons and action buttons to allow for roll parsing.
 +
 +
You essentially now have an [[Button#Action_Button|action button]] that will perform dice rolls, with input from sheetworkers. CRP also requires changes to the [[Building_Character_Sheets/Roll_Templates|Roll Template]].
 +
 +
Currently there's you can only display computed values in the '''first 10 fields''' that are sent in the macro.
 +
 +
{{ex}}
 +
 +
<pre style="overflow:auto;white-space:pre-wrap;" data-language="html">
 +
<button type="action" name="act_test">Click me</button>
 +
 +
<rolltemplate class="sheet-rolltemplate-test">
 +
    <div class="sheet-template-container">
 +
        <h1>{{name}}</h1>
 +
        <div class="sheet-results">
 +
            <h4>Result = {{roll1}}</h4>
 +
            <h4>Custom Result = {{computed::roll1}}</h4>
 +
        </div>
 +
    </div>
 +
</rolltemplate>
 +
 +
<script type="text/worker">
 +
    on('clicked:test', (info) => {
 +
        startRoll("&{template:test} {{name=Test}} {{roll1=[[1d20]]}}", (results) => {
 +
            const total = results.results.roll1.result
 +
            const computed = total % 4;
 +
 +
            finishRoll(
 +
                results.rollId,
 +
                {
 +
                    roll1: computed
 +
                }
 +
            );
 +
        });
 +
    });
 +
</script></pre>
 +
 +
===[[jQuery]]'''(NEW)'''===
 +
{{main|jQuery}}
 +
 +
=Troubleshooting=
 +
If your sheetworkers don't work, it's worthwhile to use [https://developer.mozilla.org/en-US/docs/Web/API/Console/log console.log()] and check the console returns with [[Sheet_Author_Tips#Web_Developer_Tools|Web Developer Tools]], to get better idea of what went wrong.
 +
 +
Also doing code validation
 +
 +
=Related Pages=
 +
* Sheetworker
 +
** '''[[:Category:Sheetworker|List of All Sheetworker pages]]'''
 +
** [[Sheetworker examples for Non-programmers]]
 +
** [[Sheet_Worker_Snippets|Sheet Worker Snippets]] - short and practical examples of sheetworkers
 +
** [[UniversalSheetWorkers|Universal Sheet Workers]] - How to create one function that can handle a bunch of similar sheet workers
 +
** [[Javascript:Best Practices]]
 +
* [[Building Character Sheets]]
 +
** [[Auto-Calc]]
 +
** [[CSS Wizardry]] - Contains multiple examples that makes use of sheetworkers.
 +
** [[Button#Action_Button| Action Button]] - how to use sheetworkers in combination with action buttons
 +
** [[Andreas Guide to Sheet Development]] - general stuff on design & code best practice regarding sheets
 +
** [[Sheet Author Tips]]
 +
* [[Complete Guide to Macros & Rolls]] - guide to how the dice rolling & macro syntax works. Useful for creating more complicated rolls through sheetworkers
 +
 +
 +
=See Also=
 +
{{col|500px|
 +
Various Roll20 Forum threads , github repositories, or other great external resources helping sheetworker creation.
 +
 +
* {{fpl|10697275/ A Sheet Author's Journey(Part 3) - Repeating sections and writing sheetworkers!}} Feb 2022
 +
* {{fpl|8034567/ Sheet Worker Optimization}} by [[Scott C.]]
 +
* {{forum|post/7664924/multiversal-sheet-worker-generator-this-is-bonkers/ Multiversal Sheetworker Generator}} by GiGs
 +
* {{fpl|6964447/ How to integrate table of stats into a sheet}} by [[GiGs]]
 +
* {{fpl|8450168/ getSetAttrs: an alternative API for getAttrs/SetAttrs}} by [[Jakob]]
 +
* {{repo|shdwjk/TheAaronSheet TheAaronSheet}} - A facade for Sheet Worker Tasks and Utility Functions. Contains a great function for sheet troubleshooting/debugging
 +
* {{repo|onyxring/orcsAsync orcsAync}} - A script which enables setTimeout(), setInterval(), JavaScript Promises,  and use of the Async/Await syntax when referencing character attributes in sheetworkers. by [[OnyxRing]]
 +
* {{hc|articles/360037773513-Sheet-Worker-Scripts sheet workers}} - Almost always outdated/lacking compared to any pages on sheet development on the wiki
 +
<br>
 +
 +
 +
'''General stuff about JavaScript:'''
 +
* [https://developer.mozilla.org/en-US/docs/Learn/JavaScript/First_steps Introduction to JavaScript] - Mozilla Developer Network(MDN) web docs
 +
** [https://developer.mozilla.org/en-US/docs/Learn/Accessibility/CSS_and_JavaScript#JavaScript JavaScript Best Practices]
 +
** [https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions Arrow functions] - An alternative way to write [https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Functions functions] in JavaScript. Used in many sheets.
 +
** [https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals Template literals] - string literals allowing embedded expressions: <code>`This is a ${word} example`</code>
 +
** [https://developer.mozilla.org/en-US/docs/Web/API/Console/log console.log()] - good for troubleshooting
 +
** [https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Conditional_Operator Conditional (ternary) operator]
 +
** [https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Objects/JSON Working with JSON] - useful if you want to create an import function for your sheet.
 +
** [https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Objects/Object-oriented_JS Object Oriented JS]
 +
* [https://ibaslogic.com/javascript-foreach/ JavaScript Foreach: A Comprehensive Guide for Beginners] - help with understanding <code>.foreach()</code>
 +
* [https://closure-compiler.appspot.com/home Closure JavaScript Compiler] Good for checking your sheetworkers for errors
 +
* [https://developer.mozilla.org/en-US/docs/Tools/Debugger/UI_Tour Firefox JavaScript Debugger]
 +
}}
 +
[[Category:Docs]]
 +
[[Category:Sheetworker]]
 +
[[Category:Character Sheet Creation]]
 +
[[Category:Character Sheet Development]]

Latest revision as of 19:26, 16 November 2023


Sheet Worker Scripts(AKA simply as "sheetworkers"), are an advanced feature of the Character Sheets system, which are written in JavaScript(they are a type of Web Workers, hence the name). They will allow making automatic & complex changes to the character sheet stats, based on certain events, such as whenever a certain attribute's value on a character sheet is modified, or when an action button is pressed.

Sheetworker examples for Non-programmers, Sheet Worker Snippets and Universal Sheetworkers are the best starting points for practical and working examples on how to use sheetworkers.

This page gives a general overview of the available Roll20-specific features that can be used in sheetworkers, but is not a great guide for practical implementations. The pages/links found under the Related Pages and See Also-sections have links to more examples of how to use sheetworkers.

Contents


[edit] General

As sheetworkers are written in JavaScript, its a good idea to take some general guides and learn the basics of it to be able to understand & create sheetworkers.

Introduction to JavaScript - Mozilla Developer Network(MDN) web docs

  • Few things that often come up in existing sheetworkers code, which isn't always covered that early in general JS tutorials & guides.
  • Javascript:Best Practices - tips for both API & sheetworkers

[edit] JavaScript Restrictions

Many JavaScript functions or functionalities can't be used in Roll20 character sheets due to the security filter. One should check existing sheets for examples of what can be used, if you're attempting to do any slightly more advanced data-handling. Sheetworkers have access to the Underscore library.

  • DOM can't be used directly in basically any way, so event listeners like onclick won't work. (Partial exception: CSS classes manipulated via jQuery)
  • All JavaScript must be inside a single <script type="text/worker"> element in the HTML file. Otherwise some may fail to trigger or lead to uncertain behaviour
  • Only the Roll20-designed event listeners can trigger JS on the character sheet:
    • stat change on a sheet, done either by the user, or another sheetworker
    • stat change done by an API
    • triggered by an Action Sheet Button


See Also BCS/Bugs#Sheetworkers.

[edit] Structure of a Sheetworker

Sheetworkers in Roll20 character sheets are pieces of JavaScript, intertwined with a few custom features made specifically for handling Roll20 character sheet information. All the sheetworkers in a character sheet are places inside a single <script type="text/worker">-element.

Usually a sheetworker works as follows.

1. Listens for changes to the sheet with an Event Listener ("change:attributename sheet:opened) (explained in next section)
2. Retrieves values of a number of attributes from the sheet with the getAttrs function, usually including the attributes that were monitored.
3. Create some temporary variables and uses the values received from getAttrs and does something based on the info using normal JavaScript.
4. Save to the character sheet some of the new values made in Step 3 to some number of attributes with the setAttrs function.


The following is an example of a single sheetworker placed in a <script type="text/worker">-element, taken from the Sheetworker examples for Non-programmers-page.

<script type="text/worker">
on("change:siz change:con sheet:opened", function() {  
  getAttrs(["siz","con"], function(values) {
    let siz = parseInt(values.siz)||0;
    let con = parseInt(values.con)||0;
    let hp = siz + con;
    setAttrs({                            
      "hitpoints": hp
    });
  });
});
</script>

[edit] <script type="text/worker">

In a character sheet, all sheetworkers are saved on the .html-file for the sheet, inside a single HTML element named <script type="text/worker">. This is critical for the sheetworkers to function in Roll20. If sheetworkers are split into more than one <script type="text/worker">-element, it's almost guaranteed all of them will fail to function. Most Sheet Creator place the sheetworker-block at the bottom of the HTML-file.

[edit] Event listener

on("change:siz change:con sheet:opened", function() is the Event listener, which checks the character sheet for changes to specific attributes, if the sheet have been opened, or if a button have been pressed. In the example, the sheetworker is checking if the siz or con-attribute have been changed, or if the sheet have been opened. These two represents the size and constitution of the character.

[edit] Value retrieval

getAttrs(["siz","con"], function(values) looks up the values of the siz or con-attributes on the character sheet, so they can be used in the sheetworker.


[edit] Making changes

    let siz = parseInt(values.siz)||0;
    let con = parseInt(values.con)||0;
    let hp = siz + con;

This is the main part of the sheetworker. It first defines two temporary varible, siz and con, that saves the values of their attributes in a way that JavaScript knows it's a number.

On the third row, it defines the hp-varaible to be siz + con, which means that the characters Hit Points are equal to the sum of their Size and Constitution.

[edit] Saving the changes

    setAttrs({                            
      "hitpoints": hp
    });

Finally, the sheetworker decides to define the character sheet's hitpoints-attribute to be equal to the newly calculated hp-variable, which is the last action of the sheetworker. Note that it does not make changes to the attributes it was listening to.

[edit] Other things

It's good practice to always use lower case when referencing attribute names in your sheet workers, regardless of how the attributes are defined in the HTML. This helps to avoid a number of issues.

Outside of this, there may exist some other things saved in the <script type="text/worker">, such as:

  • Saved list of information, used by the sheetworkers.
    • Example: const stats = ["strength", "intelligence", "charm", "arcana", "grace"];
  • the Event Listener might loop through some JavaScript, that decides what attributes to listen/change/save
    • Example: stats.forEach(stat => { some code });
  • Define JavaScript functions to be used in the sheetworkers, instead of repeating the same code snippets in several places.

[edit] Adding a Sheetworker

To add a sheetworker to a Character Sheet, simply add the script to the bottom of your HTML-section of the sheet using the following format:

<script type="text/worker">

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

});

// ... etc

</script>

The <script>-element 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>-elements 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 should be contained within a single <script type="text/worker"> element, and preferably be placed at the bottom of the html-file.

Sheetworker examples for 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

[edit] Understanding sheetworkers

(credit: Jakob)

Usually, the best way to learn something is to do it! Start solving the problems you want to solve using sheet workers, and learn along the way :).

Now, that probably wasn't very helpful. I could also encourage you to look at code others have written, which you should do; just realize that many sheet authors are also amateurs (such as me), hence the code may not be the most elegantly-written example.

That probably also wasn't helpful. Let me try something else: depending on what your course taught you, much of it (everything dealing with manipulation of the DOM) is not going to be very helpful. Here's some important things you need to keep in mind that might make sheet workers different from what you've learned:

1. Sheet workers are event-driven. Everything interesting your workers do will happen in response to a certain event on the sheet; most of the time, the triggering event will be changing an attribute on the sheet (i.e., the value of some input, be it a checkbox, radio, text, number input, or textarea). This is what the on("change:attr", function () { ...}) is there for: it's about registering a function that's supposed to be executed every time attr changes.
2. Sheet workers can only really do one thing. Sheet workers can change the value of attributes, and that's pretty much it (they can also remove repeating rows, but that's not their main use). If you want your sheet workers to do a thing, ask yourself if it can be accomplished by changing the value of an attribute (this includes being able to (un)check checkboxes), +perhaps some CSS. If yes, you can do it with sheet workers. If not, you cannot.
3. Sheet workers are asynchronous. The main interesting functions you have access to in sheet workers, namely getAttrs, setAttrs, and getSectionIDs, take as their second argument functions that do other stuff. This means that you cannot trust that your code will simply be executed from top to bottom, and have to write in such a way that it makes sense no matter when asynchronous functions are executed. This is often a source of confusion for people.

[edit] Good Practices

(credit: GiGs)

  • I think it's a good idea (especially when still learning) to get into the habit of putting all the values you are using in variables at the top of the code. It makes writing the code a bit more laborious, but if you reuse values, or have long macros, it can make things clearer, and most importantly, you can then use console.log statements to check what their values are. (they will appear in the browser's dev console, which you can usually find by pressing F12).
  • Another tip is its a good idea to just use a single setAttrs statement, at the end of the sheet worker. For this one which is just modifying a single attribute, it doesn't matter, but you'll eventually get into making workers that set multiple attributes. when that happens, you definitely want to group them into a single statement, to avoid unpredictable errors.
  • Also its a good idea to use variable names that match the value they are calling. In the code below, for example, i renamed index to currenthp, which I think is more descriptive.
  • It's also a good idea to use lower case only for attribute names, and use underscores instead of dashes or other symbols in the name. For example, use strengthmod or strength_mod, and avoid strength-mod.
  • One final tip: if you are using parseInt on cells that users can enter values into, it's a good idea to add a default value. For instance parseInt('something')||0 means that if the cell contains text, or not a number, you'll get a number in the code. Your if statements will fail without this, if the cells being called don't actually have numbers in them.

[edit] 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.

[edit] Sheet Worker API

A list of sheetworker features accessible in Roll20. Not to be confused with the Roll20 API.

[edit] Events

[edit] 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.
triggerName When changing a value it is equal to the sourceAttribute, being the full name (including RowID if in a repeating section)

When removing a repeating row it contains the bound trigger name for the repeating section, including the remove keyword, i.e. remove:repeating_section

[edit] 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. 
});


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.



[edit] change:_reporder:<sectionname>

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:<sectionname>", function(eventInfo) {
   // Where <sectionname> above should be a repeating section name, such as skills or spells
});

[edit] 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);
});

[edit] sheet:opened

This event will fire every time a sheet is opened by a player in a game session. 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

});

[edit] clicked:<button_name>

This event will trigger when an action button is clicked. The button's name will need to start with act_. Example:


<button type="action" name="act_activate">Activate!</button>

<script type="text/worker">
on("clicked:activate", function() {
  console.log("Activate button clicked");
});
</script>

[edit] sheet:compendium-drop

Only relevant if you're working on Compendium Integration.

Can be used to set Default Token info for characters drag&dropped from the compendium to your game.

D&D 5E by Roll20 example

[edit] Functions

[edit] 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.

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.

[edit] 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 attribute 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)) });
  });
});

Although 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
      });
   });
});

[edit] 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);
    });
  });
};

[edit] 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);

[edit] WARNING

This function does not generate reliably unique row IDs, which may lead to unexpected behavior. For example, if you use generated IDs as keys in value for use in setAttrs(value) as intended, you may end up overwriting one or more existing entries and thus expected repeating section items will never be rendered. Here's an example you can use to work around this issue:


var newweightid = "repeating_inventory_" + generateRowID() + "_weight";

while (newweightid in newrowattrs) {
  newweightid = "repeating_inventory_" + generateRowID() + "_weight";
}

newrowattrs[newweightid] = "testnewrow";

[edit] 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]);
      }
   });
});

[edit] setSectionOrder(<Repeating Section Name>, <Section Array>, <Callback>)

 setSectionOrder("proficiencies", final-array, callbackFunction); 

The setSectionOrder function allow the ordering of repeating sections according to your preference. The function accepts the name of a repeating section **without** `repeating_` (e.g. `repeating_proficiencies` would be passed simply as `proficiencies`) and an array of row IDs in the order that you want them.

[edit] WARNING

This function's behavior does not match its documentation. The function does not appear to have any callback functionality. Additionally, reordering sections via this function can cause significant graphical glitches and even some data corruption depending on what a user does in reaction to the graphic glitches.

[edit] 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

[edit] 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

[edit] 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.

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);
});


[edit] Custom Roll Parsing

Main Page: Custom Roll Parsing

As of July 13th, 2021, character sheets can now combine the functionality of roll buttons and action buttons to allow for roll parsing.

You essentially now have an action button that will perform dice rolls, with input from sheetworkers. CRP also requires changes to the Roll Template.

Currently there's you can only display computed values in the first 10 fields that are sent in the macro.


Example:

<button type="action" name="act_test">Click me</button>

<rolltemplate class="sheet-rolltemplate-test">
    <div class="sheet-template-container">
        <h1>{{name}}</h1>
        <div class="sheet-results">
            <h4>Result = {{roll1}}</h4>
            <h4>Custom Result = {{computed::roll1}}</h4>
        </div>
    </div>
</rolltemplate>

<script type="text/worker">
    on('clicked:test', (info) => {
        startRoll("&{template:test} {{name=Test}} {{roll1=[[1d20]]}}", (results) => {
            const total = results.results.roll1.result
            const computed = total % 4;

            finishRoll(
                results.rollId,
                {
                    roll1: computed
                }
            );
        });
    });
</script>

[edit] jQuery(NEW)

Main Page: jQuery

[edit] Troubleshooting

If your sheetworkers don't work, it's worthwhile to use console.log() and check the console returns with Web Developer Tools, to get better idea of what went wrong.

Also doing code validation

[edit] Related Pages


[edit] See Also

Various Roll20 Forum threads , github repositories, or other great external resources helping sheetworker creation.



General stuff about JavaScript: