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 "UniversalSheetWorkers"

From Roll20 Wiki

Jump to: navigation, search
m
m (See Also)
 
(6 intermediate revisions by 2 users not shown)
Line 1: Line 1:
 +
''Main Article:'' '''[[Sheet Worker Scripts]]'''
 +
 
= Universal Sheet Workers =
 
= Universal Sheet Workers =
 
In this page, I'll show you how to make one function to handle a bunch of similar sheet workers.
 
In this page, I'll show you how to make one function to handle a bunch of similar sheet workers.
Line 4: Line 6:
 
For example, in many games you have a bunch of stats, and each has a stat modifier.
 
For example, in many games you have a bunch of stats, and each has a stat modifier.
 
You'd normally create a sheet worker for each stat, to create the modifier. That might look something like this:
 
You'd normally create a sheet worker for each stat, to create the modifier. That might look something like this:
<pre>
+
<pre data-language="javascript" style="overflow:auto; width:auto;">
 
on("change:strength sheet:opened", function () {
 
on("change:strength sheet:opened", function () {
 
     getAttrs(['strength'], function (values) {
 
     getAttrs(['strength'], function (values) {
Line 24: Line 26:
 
So, assuming this is true (don't worry, I'll handle below how to deal with situations where that isnt true), the first step is to create an array of your stat names.
 
So, assuming this is true (don't worry, I'll handle below how to deal with situations where that isnt true), the first step is to create an array of your stat names.
  
<pre>const stats = ['str','dex','con','int','wis','cha'];</pre>
+
<pre data-language="javascript" style="overflow:auto; width:auto;">
 +
const stats = ['str','dex','con','int','wis','cha'];
 +
</pre>
  
 
Then you create a for each loop like so:
 
Then you create a for each loop like so:
<pre>
+
<pre data-language="javascript" style="overflow:auto; width:auto;">
 
stats.forEach(function (stat) {
 
stats.forEach(function (stat) {
  
Line 35: Line 39:
 
== Simple Looped Worker: DnD & Traveller Compatible ==
 
== Simple Looped Worker: DnD & Traveller Compatible ==
 
That looks like this:
 
That looks like this:
<pre>
+
<pre data-language="javascript" style="overflow:auto; width:auto;">
 
const stats = ['str','dex','con','int','wis','cha'];
 
const stats = ['str','dex','con','int','wis','cha'];
 
stats.forEach(function (stat) {
 
stats.forEach(function (stat) {
Line 50: Line 54:
 
</pre>
 
</pre>
 
The only tricky part is the setAttrs function. Notice the line is  
 
The only tricky part is the setAttrs function. Notice the line is  
<pre>[stat + '_mod']: stat_modifier </pre>
+
<pre data-language="javascript" style="overflow:auto; width:auto;">
 +
[stat + '_mod']: stat_modifier
 +
</pre>
  
 
since you are building the attribute name, you have to enclose it in brackets to avoid an error.
 
since you are building the attribute name, you have to enclose it in brackets to avoid an error.
  
 
If you like arrow notation, and string literals, another way to write the function above is:
 
If you like arrow notation, and string literals, another way to write the function above is:
<pre>
+
<pre data-language="javascript" style="overflow:auto; width:auto;">
 
const stats = ['str','dex','con','int','wis','cha'];
 
const stats = ['str','dex','con','int','wis','cha'];
 
stats.forEach(stat => {
 
stats.forEach(stat => {
Line 76: Line 82:
  
 
An object is made up of pairs of data, each pair being a key and value. It looks like this:
 
An object is made up of pairs of data, each pair being a key and value. It looks like this:
<pre>
+
<pre data-language="javascript" style="overflow:auto; width:auto;">
 
const stats = {
 
const stats = {
 
     str: strength_modifier,
 
     str: strength_modifier,
Line 83: Line 89:
 
};</pre>
 
};</pre>
 
Now you can loop over the keys, using Object.keys(stats) like so:
 
Now you can loop over the keys, using Object.keys(stats) like so:
<pre>
+
<pre data-language="javascript" style="overflow:auto; width:auto;">
 
Object.keys(stats).forEach(function(stat) {
 
Object.keys(stats).forEach(function(stat) {
 
   console.log(stat, stats[stat]);
 
   console.log(stat, stats[stat]);
Line 89: Line 95:
 
</pre>
 
</pre>
 
The above function will print to the console the stat key, followed by its value. So to use this, the above function would be written as
 
The above function will print to the console the stat key, followed by its value. So to use this, the above function would be written as
<pre>
+
<pre data-language="javascript" style="overflow:auto; width:auto;">
 
const stats = {
 
const stats = {
 
     str: strength_modifier, dex: dexterity_modifier, int: intelligence_modifier
 
     str: strength_modifier, dex: dexterity_modifier, int: intelligence_modifier
Line 116: Line 122:
  
 
So the first thing is to build a function to calculate the bonus a given stat applies.
 
So the first thing is to build a function to calculate the bonus a given stat applies.
<pre>
+
<pre data-language="javascript" style="overflow:auto; width:auto;">
 
function calcBonus(score, secondary, negative) {
 
function calcBonus(score, secondary, negative) {
 
     // secondary/negative: 1 = true, 0 = false
 
     // secondary/negative: 1 = true, 0 = false
Line 134: Line 140:
  
 
So now we need to build the object we can use to build the sheet worker. But first, lets see what the sheet worker would look like for Manipulation.
 
So now we need to build the object we can use to build the sheet worker. But first, lets see what the sheet worker would look like for Manipulation.
<pre>
+
<pre data-language="javascript" style="overflow:auto; width:auto;">
 
on("change:str change:dex change:int change:pow sheet:opened", function () {
 
on("change:str change:dex change:int change:pow sheet:opened", function () {
 
     getAttrs(['str','dex','int','pow'], function (values) {
 
     getAttrs(['str','dex','int','pow'], function (values) {
Line 155: Line 161:
 
Brace yourself, this gets complicated:
 
Brace yourself, this gets complicated:
  
<pre>
+
<pre data-language="javascript" style="overflow:auto; width:auto;">
 
const skill_categories = {
 
const skill_categories = {
 
manipulation: {
 
manipulation: {
Line 171: Line 177:
 
When you need to know the stats inside the manipulation object, you can call them by grabbing the skill_categories[manipulation], then iterating over the keys of that object. Here's an example where we print the names of each of the stats in the manipulation group, and show what their primary rating is (always 0 or 1).
 
When you need to know the stats inside the manipulation object, you can call them by grabbing the skill_categories[manipulation], then iterating over the keys of that object. Here's an example where we print the names of each of the stats in the manipulation group, and show what their primary rating is (always 0 or 1).
  
<pre>
+
<pre data-language="javascript" style="overflow:auto; width:auto;">
 
Object.keys(skill_categores[manipulation]).forEach(stat => {
 
Object.keys(skill_categores[manipulation]).forEach(stat => {
 
console.log(`${stat}: primary: ${skill_categories[category][stat]['secondary']});   
 
console.log(`${stat}: primary: ${skill_categories[category][stat]['secondary']});   
Line 179: Line 185:
 
So with this knowledge we can now build our sheet worker loop. The tricky parts are the on(change) line and the getAttrs line. So the first part of the function, before the on(change) section lay the groundwork for that.
 
So with this knowledge we can now build our sheet worker loop. The tricky parts are the on(change) line and the getAttrs line. So the first part of the function, before the on(change) section lay the groundwork for that.
  
<pre>
+
<pre data-language="javascript" style="overflow:auto; width:auto;">
 
Object.keys(skill_categories).forEach(category => {
 
Object.keys(skill_categories).forEach(category => {
 
     let stats = [];
 
     let stats = [];
Line 210: Line 216:
 
== Runequest: Glorantha Sheet Worker ==
 
== Runequest: Glorantha Sheet Worker ==
  
<pre>
+
<pre data-language="javascript" style="overflow:auto; width:auto;">
 
const calcBonus = (score, secondary, negative) => {
 
const calcBonus = (score, secondary, negative) => {
 
     // secondary/negative: 1 = true, 0 = false
 
     // secondary/negative: 1 = true, 0 = false
Line 286: Line 292:
 
== Practical Examples From the Forums ==
 
== Practical Examples From the Forums ==
 
I'll link here to threads where I've already created some examples of universal sheet workers.
 
I'll link here to threads where I've already created some examples of universal sheet workers.
* Replacing a Nested getSectionIDs function: [https://app.roll20.net/forum/permalink/7130713/ This post] and my reply 3 posts below it.
+
* '''Simplest example:''' [https://app.roll20.net/forum/permalink/7600200/ forcing a number to be an integer]
* Replacing EventInfo and a convoluted Switch Case: [https://app.roll20.net/forum/permalink/7061285/ This post] and the one two posts below it.
+
* '''A Simple Example:''' [https://app.roll20.net/forum/permalink/7237292/ Calculate modifiers for group of stats]
* A Simple Example: [https://app.roll20.net/forum/permalink/7237292/ Basic Sheet Worker]
+
* '''Replacing a Nested getSectionIDs function:''' [https://app.roll20.net/forum/permalink/7130713/ This post] and my reply 3 posts below it.
* Champions/Hero Games: [https://app.roll20.net/forum/permalink/7284677/ Attributes]
+
* '''Replacing EventInfo and a convoluted Switch Case:''' [https://app.roll20.net/forum/permalink/7061285/ This post] and the one two posts below it.
 +
* '''Champions/Hero Games Attributes:''' [https://app.roll20.net/forum/permalink/7284677/ Attributes]
 +
* '''Limit Structure Part by their Max:''' [https://app.roll20.net/forum/permalink/8269344/ Structure Limits]
  
  
Line 297: Line 305:
  
 
==See Also==
 
==See Also==
* [[Sheetworker_examples_for_Non-programmers]]
+
* [[Sheetworker examples for Non-programmers]]
* [https://app.roll20.net/forum/post/6963354/build-lookup-table-into-a-character-sheet/?pageforid=6964447#post-6964447 How to integrate table of stats into a sheet]
+
* '''[[:Category:Sheetworker|List of all Sheetworker-articles]]'''
 +
* [https://app.roll20.net/forum/post/8033979/advice-api-sheet-workers-and-performance/?pageforid=8034567#post-8034567 Sheet Worker Optimization] by Scott C.
 +
* [https://app.roll20.net/forum/post/6963354/build-lookup-table-into-a-character-sheet/?pageforid=6964447#post-6964447 How to integrate table of stats into a sheet] - Forum Post
 +
* [https://app.roll20.net/forum/post/7664924/multiversal-sheet-worker-generator-this-is-bonkers/ Multiversal Sheetworker Generator] by GiGs
 +
* [https://developer.mozilla.org/en-US/docs/Learn/Accessibility/CSS_and_JavaScript#JavaScript JavaScript Best Practices] - MDN web docs
 +
* [https://developer.mozilla.org/en-US/docs/Learn/JavaScript/First_steps Introduction to JavaScript] - MDN web docs
  
 
<br>
 
<br>

Latest revision as of 14:05, 4 April 2020

Main Article: Sheet Worker Scripts

Contents

[edit] Universal Sheet Workers

In this page, I'll show you how to make one function to handle a bunch of similar sheet workers.

For example, in many games you have a bunch of stats, and each has a stat modifier. You'd normally create a sheet worker for each stat, to create the modifier. That might look something like this:

on("change:strength sheet:opened", function () {
    getAttrs(['strength'], function (values) {
        const strength = parseInt(values['strength'], 10)||0; // this extracts the stat, and assumes a stat score of 0 the stat is not recognised as a number.
        const modifier = Math.floor(strength/2) -5; // this gives a +0 at 10-11, and +/1 for 2 point difference.
        setAttrs({
            strength_modifier: modifier
        });
    });
});

If you have stats of dexterity, constitution, intelligence, wisdom, and charisma, you could easily copy the above sheet worker, and just replace the stat names. But there's a better way: you can rewrite that one function to account for all the stats.

The easiest way is to make sure you have a consistent naming pattern. So for example, if you might have stats named str, dex, con, etc, and the modifiers are always str_mod, dex_mod, con_mod, etc.

So, assuming this is true (don't worry, I'll handle below how to deal with situations where that isnt true), the first step is to create an array of your stat names.

const stats = ['str','dex','con','int','wis','cha'];

Then you create a for each loop like so:

stats.forEach(function (stat) {

});

This is going to loop through each stat in the stats array, and send the stat name to the function inside. So you just need to replace each instance of the stat name with the variable stat.

[edit] Simple Looped Worker: DnD & Traveller Compatible

That looks like this:

const stats = ['str','dex','con','int','wis','cha'];
stats.forEach(function (stat) {
    on("change:" + stat + " sheet:opened", function () {
        getAttrs([stat], function (values) {
            const stat_score = parseInt(values[stat], 10)||0; // this extracts the stat, and assumes a stat score of 0 the stat is not recognised as a number.
            const stat_modifier = Math.floor(stat_score/2) -5; // this gives a +0 at 10-11, and +/1 for 2 point difference.
            setAttrs({
                [stat + '_mod']: stat_modifier 
            });
        });
    });
});

The only tricky part is the setAttrs function. Notice the line is

[stat + '_mod']: stat_modifier

since you are building the attribute name, you have to enclose it in brackets to avoid an error.

If you like arrow notation, and string literals, another way to write the function above is:

const stats = ['str','dex','con','int','wis','cha'];
stats.forEach(stat => {
    on(`change:${stat} sheet:opened`, () => {
        getAttrs([stat], values => {
            const stat_score = parseInt(values[stat], 10)||0;
            const stat_modifier = Math.floor(stat_score/2) -5;
            setAttrs({
                [`${stat}_mod`]: stat_modifier 
            });
        });
    });
});

It doesnt matter which you use. They are identical in all ways that matter.

[edit] Handling Tricky Name Combinations

What about when your names dont follow a consistent naming pattern? If your attributes are named str, strength_mod, int, intelligence_mod, etc. (Obviously this is a contrived example, but some situations like this will crop up). The best course of action would be to rename your attributes, it's just neater. But there may be reasons you cant do that. In that case, you can use an object instead of array.

An object is made up of pairs of data, each pair being a key and value. It looks like this:

const stats = {
     str: strength_modifier,
	 dex: dexterity_modifier,
	 int: intelligence_modifier
};

Now you can loop over the keys, using Object.keys(stats) like so:

Object.keys(stats).forEach(function(stat) {
  console.log(stat, stats[stat]);
});

The above function will print to the console the stat key, followed by its value. So to use this, the above function would be written as

const stats = {
    str: strength_modifier, dex: dexterity_modifier, int: intelligence_modifier
};
Object.keys(stats).forEach(stat => {
    on(`change:${stat} sheet:opened`, function () {
        getAttrs([stat], values => {
            const stat_score = parseInt(values[stat], 10)||0;
            const stat_modifier = Math.floor(stat_score/2) -5;
            setAttrs({
                [stats[stat]]: stat_modifier 
            });
        });
    });
});

The only real difference is the setAttrs function. You get the name of the modifier attribute by calling the keys value like object[key], or stats[stat] in this case.

[edit] Building an Advanced Sheet Worker

Sometimes your sheet worker is more complicated. In BRP games like Runequest, you often have to calculate the bonus from 2 or more attributes, and they each might apply in a different way. I'm going to build a very complex example, the latest edition of Runequest: Glorantha. In this system, you have several skill categories, like Agility, Magic, Manipulation, Perception, etc. Each category can have 2-4 stats. Some stats are primary, giving of +5% at 13-16, +10% at 17-20, etc. Others are less important, where the bonus starts at 17-20. And some stats are negative: they reverse the bonus (so you get a bonus for low scores and a penalty for high scores). For example, in Stealth, a high POW and SIZ makes it harder for you to hide.

So the first thing is to build a function to calculate the bonus a given stat applies.

function calcBonus(score, secondary, negative) {
    // secondary/negative: 1 = true, 0 = false
    let bonus = 0;
	if(score > 12) {
        bonus = Math.ceil((score-12)/4) *5;
        if(secondary) bonus -=5;
    } else if (score < 9) {
        bonus = Math.ceil((9-score)/4) *5;
        if(secondary) bonus +=5;
    }
    if(negative) bonus = -bonus;
    return bonus;
};

So, given a stat score, whether it is primary or secondary, or positive or negative, this function will return that stats modifier.

So now we need to build the object we can use to build the sheet worker. But first, lets see what the sheet worker would look like for Manipulation.

on("change:str change:dex change:int change:pow sheet:opened", function () {
    getAttrs(['str','dex','int','pow'], function (values) {
        const str = parseInt(values.str, 10)||0;
        const dex = parseInt(values.dex, 10)||0;
        const int = parseInt(values.int, 10)||0;
        const pow = parseInt(values.pow, 10)||0;
        const str_bonus = calcBonus(str,1,0); // str is a secondary bonus, so secondary parameter is 1
        const dex_bonus = calcBonus(dex,0,0); // dex is primary, so secondary value is 0.
        const int_bonus = calcBonus(int,0,0);
        const pow_bonus = calcBonus(pow,1,0);
        setAttrs({
            manipulation: str_bonus + dex_bonus + int_bonus + pow_bonus
        });
    });
});

Using the function makes that a lot simpler than it would otherwise be, but it also lets us theorycraft what a unified sheetworker looks like. First we need an object to loop over, that for each category, includes 2-4 stats, and for each stat, lists whether it is primary or secondary, and positive or negative.

Brace yourself, this gets complicated:

const skill_categories = {
	manipulation: {
        str: { secondary: 1, negative: 0 },
        dex: { secondary: 0, negative: 0 },
        int: { secondary: 0, negative: 0 },
        pow: { secondary: 1, negative: 0 },
    },
} 

So here we have a skill_categories object. Inside that another object called manipulation. And inside that there are 4 more objects, one for each stat, which contains whether that stat is secondary or negative.


When you need to know the stats inside the manipulation object, you can call them by grabbing the skill_categories[manipulation], then iterating over the keys of that object. Here's an example where we print the names of each of the stats in the manipulation group, and show what their primary rating is (always 0 or 1).

Object.keys(skill_categores[manipulation]).forEach(stat => {
	console.log(`${stat}: primary: ${skill_categories[category][stat]['secondary']});  
}

So with this knowledge we can now build our sheet worker loop. The tricky parts are the on(change) line and the getAttrs line. So the first part of the function, before the on(change) section lay the groundwork for that.

Object.keys(skill_categories).forEach(category => {
    let stats = [];
    let change = '';
    Object.keys(skill_categories[category]).forEach(stat => { 
        stats.push(stat); // this is the array for the getAttrs line, which needs an array of the stats.
        change += `change:${stat} `; // here we build the string for the on(change) line.
    });
    on(`${change}sheet:opened`, function () {
        getAttrs(stats, values => {
            let bonus = 0;
            Object.keys(skill_categories[category]).forEach(stat => { 
				// here we loop through all the stats in the current category, and get the bonus for that stat.
                const score = parseInt(values[stat], 10) ||0;
                const secondary = skill_categories[category][stat]['secondary'];
                const negative = skill_categories[category][stat]['negative'];
                bonus += calcBonus(score, secondary, negative);
            });
			// at this point the bonus for this category is complete so can print it to the sheet.
            setAttrs({
                [category]: bonus 
            });
        });
    });
}); 

So, congratulations on following all that. Here are the final, complete sheet worker functions for Runequest: Glorantha stat bonus calculations, with stat names adjusted to match the sheet on Roll20's github site.

[edit] Runequest: Glorantha Sheet Worker

const calcBonus = (score, secondary, negative) => {
    // secondary/negative: 1 = true, 0 = false
        let bonus = 0;
        if(score > 12) {
            bonus = Math.ceil((score-12)/4) *5;
            if(secondary) bonus -=5;
        } else if (score < 9) {
            bonus = Math.ceil((9-score)/4) *5;
            if(secondary) bonus +=5;
        }
        if(negative) bonus = -bonus;
        return bonus;
    },
    skill_categories = {
        agility: {
            curstr: { secondary: 1, negative: 0 },
            curdex: { secondary: 0, negative: 0 },
            cursiz: { secondary: 1, negative: 1 },
            curpow: { secondary: 1, negative: 0 },
        },
        communication: {
            curcha: { secondary: 0, negative: 0 },
            curint: { secondary: 1, negative: 0 },
            curpow: { secondary: 1, negative: 0 },
        },
        knowledge: {
            curint: { secondary: 0, negative: 0 },
            curpow: { secondary: 1, negative: 0 },
        },
        magic: {
            curpow: { secondary: 1, negative: 0 },
            curcha: { secondary: 0, negative: 0 },
        },
        manipulation: {
            curstr: { secondary: 1, negative: 0 },
            curdex: { secondary: 0, negative: 0 },
            curint: { secondary: 0, negative: 0 },
            curpow: { secondary: 1, negative: 0 },
        },
        perception: {
            curint: { secondary: 0, negative: 0 },
            curpow: { secondary: 1, negative: 0 },
        },
        stealth: {
            cursiz: { secondary: 0, negative: 1 },
            curdex: { secondary: 0, negative: 0 },
            curint: { secondary: 0, negative: 0 },
            curpow: { secondary: 1, negative: 1 },
        },
};
Object.keys(skill_categories).forEach(category => {
    let stats = [];
    let change = '';
    Object.keys(skill_categories[category]).forEach(stat => { 
        stats.push(stat);
        change += `change:${stat} `;
    });
    on(`${change}sheet:opened`, function () {
        getAttrs(stats, values => {
            let bonus = 0;
            Object.keys(skill_categories[category]).forEach(stat => { 
                const score = parseInt(values[stat], 10) ||0;
                const secondary = skill_categories[category][stat]['secondary'];
                const negative = skill_categories[category][stat]['negative'];
                bonus += calcBonus(score, secondary, negative);
            });
            setAttrs({
                [`${category}_mod`]: bonus 
            });
        });
    });
}); 

[edit] Practical Examples From the Forums

I'll link here to threads where I've already created some examples of universal sheet workers.


Author: GiGs(G-G-G on github).


[edit] See Also