The last few posts were very code-heavy, so here’s a little diversion. There’s still a lot of code, but it is much simpler, and gives you the opportnunity to laugh at my coding in the second half.
I recieved a question asking why a sheet worker isn’t working, and fixing it might illuminate some things.
The Faulty Code
The asker sent in this code saying they were having issues with if statements. Funnily enough, they were working fine.
The first thing I want to say is congratulations to the submitter. This is an excellent submission. It includes all the code relevant to the problem (of calculating Proficiency) and includes everything we need to spot the issue(s).
I didn’t spot it all at once, but that’s my bad!
<span>Level</span>
<input class="level" type="number" name="attr_Level" value="0" max="20">
<span>Proficiency</span>
<input class="Proficiency" type="number" name="attr_Proficiency" value="0" disabled="true" />
<script>
const Level = parseInt(values.Level) || 0;
let Proficiency = 1;
if (Level >= 17) {
Proficiency = 6;
}
else if (Level >=13) {
Proficiency = 5;
}
else if (Level >=9) {
Proficiency = 4;
}
else if (Level >=5) {
Proficiency = 3;
}
else if (Level >=2) {
Proficiency = 2;
}
</script>
</div>
Code language: Markdown (markdown)
The Script Block
Notice this line?
<script>
Code language: JavaScript (javascript)
This is an improperly formed script block. Script blocks must always begin as follows
<script type="text/worker">
Code language: JavaScript (javascript)
In traditional webpages, the first version might work, but Roll20 has its own scripting environment and works a little differently.
Forming a Sheet Worker
The secod thing to notice, is there is no sheet worker there. There is just code that runs whenever Roll20 starts and never again. The code needs to be wrapped in an on function, like this:
on('change:level", function() {
getAttrs(['level'], function(values_ {
const Level = parseInt(values.Level) || 0;
let Proficiency = 1;
if (Level >= 17) {
Proficiency = 6;
}
else if (Level >=13) {
Proficiency = 5;
}
else if (Level >=9) {
Proficiency = 4;
}
else if (Level >=5) {
Proficiency = 3;
}
else if (Level >=2) {
Proficiency = 2;
}
setAttrs({
Proficiency: Proficiency
});
});
Code language: JavaScript (javascript)
The first two lines are what link the if statement to a character.
The first line is the event line, which tells the worker to watch for that attribute changing on all characters. When it changes, the worker will know which character it applies to.
Sheet workers don’t know anything about characters- not their stats or values. You need to use getAttrs to grab the values of attributes. Anything inside [ ] should be a list of attributes whose value you want.
Finally the setAttrs at the end is what updates the character sheet.
The Common Errors
There were those things I spotted immediately and as often happens, when you spot a solution you stop looking. “Aha, found it.” But there was more to do.
The poster tried out these changes and the code sill didn’t work, so I had a second look and spotted some remaining errors, including a couple I had created! These are extremely common errors. It’s likely even experienced coders still do these from time to time, so it’s handy to know what to look for.
Disabled vs Readonly
When creating inputs you can create what are called Autocalc attributes. These will run a calculation, and look like this:
<input class="Proficiency" type="number" name="attr_Proficiency" value="0" disabled="true" />
Code language: HTML, XML (xml)
But one serious problem is that disabled attributes are completely incompatible with sheet workers. If you have a diabled attribute, it cannot be altered y a sheet worker.
You could create a hidden attribute with the same name that is not disabled: that will be updated, and so the disabled attribute which always has the same value will then be updated. But there is a better way: changed the disabled tag to readonly.
<input class="Proficiency" type="number" name="attr_Proficiency" value="0" readonly />
Code language: HTML, XML (xml)
Readonly attributes cannot be altered directly by players, but sheet workers can modify them just fine. So, if you are using sheet workers, you need to change any relevant attributes to readonly.
Capitals vs Lower Case
You have to be very careful about case in JavaScript, and roll20 particularly. Here’s a snippet of the fixed code:
on('change:level", function() {
getAttrs(['level'], function(values) {
const Level = parseInt(values.Level) || 0;
if (Level >= 17) {
Code language: JavaScript (javascript)
Attribtes on the event line (the first line) must always be lowercase, regardless of how they appear in HTML.
But look at the getAttrs line and the line immediately following. level is created in getAttrs, and is accessed with values. But that next line uses values.Level – there’s a capital letter there that isn’t in the original. Here are rules you must follow:
- The on(‘change line must always be entirely in lower case
- When creating names in getAttrs, they don’t have to match the case in your HTML.
- When you create variable names on getAttrs, you must use the case you created there.
- Variable names can be anything – they dont have to match the attribute names. It just makes things easier.
So that code could have been written like this:
on('change:level", function() {
getAttrs(['lEveL'], function(values) {
const my_level = parseInt(values.lEveL) || 0;
if (my_level >= 17) {
Code language: JavaScript (javascript)
This can get complicated. It’s easiers to use lower case in your HTML, and always use all lower case in your sheet workers, and copy attribute names with your variables. This just keeps things seasier.
Closures
When I wtrote the original ‘fixed’ worker, I had this format:
on(/*stuff*/ {
getAttrs(/* stuff */ {
setAttrs({
/* stuff */
});
});
Code language: JavaScript (javascript)
There is an eerror here that is obvious if you know sheet workers. Remember to close each set of brackets – th4e on, getAttrs, and setAttrs each open a ( and { bracket, but only two sets are closed. Nesting your workers makes this a lot easier to see.
on(/*stuff*/ {
getAttrs(/* stuff */ {
setAttrs({
/* stuff */
});
});
});
Code language: JavaScript (javascript)
Always indent code that starts with a {, and indent code that starts with ( if it goes on to another line. Do that and these errors will be spotted immediately.
Watch For Typos
When I submitted a fix, I included this typo:
getAttrs(['level'], function(values_ {
Code language: JavaScript (javascript)
That’s a silly mistake, but is easily done. Typos are easy to make but can be very hard to spot. That should be:
getAttrs(['level'], function(values) {
Code language: JavaScript (javascript)
Streamlining The If
So, we now have a working function. But looking at the code so much, I noticed something. The value of proficency increases whenever level inceases by 4. In fact it is always equal to level/4. So we can make the sheet worker shorter. In the end it looks like this:
on('change:level", function() {
getAttrs(['level'], function(values) {
const Level = parseInt(values.level) || 0;
const Proficiency = 1 + Math.ceil(Level/4);
setAttrs({
Proficiency: Proficiency
});
});
});
Code language: JavaScript (javascript)
Math is an object that contains many functions, and one of those functios, ceil, rounds up.
Whenever you have an if statement, its work lookign to see if yiu can make it simpler. You can’t always do this – sometimes the if statement is the right tool for the job. But when you can remove it, the code is often a lot shrter and involves less typing.
Finishing Up
Some errors happen again and again, and once you know how to spot them, it’s easy to fix them. But javaScript is such a pain to work in – since you’re writing text, typos are easy. These reasons are why you should always use a code editor. They’ll instantly identifya lot of problems you might otherwise spend hours trying to tack down.